Source: TurretHolder.js

/**
* @class
 * This class holds the functions regarding entities being visible on
 * another entity, but tied to their parents location.
 */
class TurretHolder
{
	Init()
	{
		this.turretPoints = [];

		let points = this.template.TurretPoints;
		for (let point in points)
			this.turretPoints.push({
				"name": point,
				"offset": {
					"x": +points[point].X,
					"y": +points[point].Y,
					"z": +points[point].Z
				},
				"allowedClasses": points[point].AllowedClasses?._string,
				"angle": points[point].Angle ? +points[point].Angle * Math.PI / 180 : null,
				"entity": null,
				"template": points[point].Template,
				"ejectable": "Ejectable" in points[point] ? points[point].Ejectable == "true" : true
			});
	}

	/**
	 * Add a subunit as specified in the template.
	 * This function creates an entity and places it on the turret point.
	 *
	 * @param {Object} turretPoint - A turret point to (re)create the predefined subunit for.
	 *
	 * @return {boolean} - Whether the turret creation has succeeded.
	 */
	CreateSubunit(turretPointName)
	{
		let turretPoint = this.TurretPointByName(turretPointName);
		if (!turretPoint || turretPoint.entity ||
			this.initTurrets?.has(turretPointName) ||
			this.reservedTurrets?.has(turretPointName))
			return false;

		let ent = Engine.AddEntity(turretPoint.template);

		let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
		if (cmpOwnership)
		{
			let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
			cmpEntOwnership?.SetOwner(cmpOwnership.GetOwner());
		}

		let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
		return cmpTurretable?.OccupyTurret(this.entity, turretPoint.name, turretPoint.ejectable) || Engine.DestroyEntity(ent);
	}

	/**
	 * @param {string} name - The name of a turret point to reserve, e.g. for promotion.
	 */
	SetReservedTurretPoint(name)
	{
		if (!this.reservedTurrets)
			this.reservedTurrets = new Set();
		this.reservedTurrets.add(name);
	}

	/**
	 * @return {Object[]} - An array of the turret points this entity has.
	 */
	GetTurretPoints()
	{
		return this.turretPoints;
	}

	/**
	 * @param {number} entity - The entity to check for.
	 * @param {Object} turretPoint - The turret point to use.
	 *
	 * @return {boolean} - Whether the entity is allowed to occupy the specified turret point.
	 */
	AllowedToOccupyTurretPoint(entity, turretPoint)
	{
		if (!turretPoint || turretPoint.entity)
			return false;

		if (!IsOwnedByMutualAllyOfEntity(entity, this.entity))
			return false;

		if (!turretPoint.allowedClasses)
			return true;

		let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
		return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), turretPoint.allowedClasses);
	}

	/**
	 * @param {number} entity - The entity to check for.
	 * @return {boolean} - Whether the entity is allowed to occupy any turret point.
	 */
	CanOccupy(entity)
	{
		return !!this.turretPoints.find(turretPoint => this.AllowedToOccupyTurretPoint(entity, turretPoint));
	}

	/**
	 * Occupy a turret point with the given entity.
	 * @param {number} entity - The entity to use.
	 * @param {Object} requestedTurretPoint - Optionally the specific turret point to occupy.
	 *
	 * @return {boolean} - Whether the occupation was successful.
	 */
	OccupyTurretPoint(entity, requestedTurretPoint)
	{
		let cmpPositionOccupant = Engine.QueryInterface(entity, IID_Position);
		if (!cmpPositionOccupant)
			return false;

		let cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position);
		if (!cmpPositionSelf)
			return false;

		if (this.OccupiesTurretPoint(entity))
			return false;

		let turretPoint;
		if (requestedTurretPoint)
		{
			if (this.AllowedToOccupyTurretPoint(entity, requestedTurretPoint))
				turretPoint = requestedTurretPoint;
		}
		else
			turretPoint = this.turretPoints.find(turret => !turret.entity && this.AllowedToOccupyTurretPoint(entity, turret));

		if (!turretPoint)
			return false;

		turretPoint.entity = entity;

		// Angle of turrets:
		// Renamed entities (turretPoint != undefined) should keep their angle.
		// Otherwise if an angle is given in the turretPoint, use it.
		// If no such angle given (usually walls for which outside/inside not well defined), we keep
		// the current angle as it was used for garrisoning and thus quite often was from inside to
		// outside, except when garrisoning from outWorld where we take as default PI.
		if (!turretPoint && turretPoint.angle != null)
			cmpPositionOccupant.SetYRotation(cmpPositionSelf.GetRotation().y + turretPoint.angle);
		else if (!turretPoint && !cmpPosition.IsInWorld())
			cmpPositionOccupant.SetYRotation(cmpPositionSelf.GetRotation().y + Math.PI);

		cmpPositionOccupant.SetTurretParent(this.entity, turretPoint.offset);

		Engine.PostMessage(this.entity, MT_TurretsChanged, {
			"added": [entity],
			"removed": []
		});

		return true;
	}

	/**
	 * @param {number} entity - The entityID of the entity.
	 * @param {String} turretName - The name of the turret point to occupy.
	 * @return {boolean} - Whether the occupation has succeeded.
	 */
	OccupyNamedTurretPoint(entity, turretName)
	{
		return this.OccupyTurretPoint(entity, this.TurretPointByName(turretName));
	}

	/**
	 * @param {string} turretPointName - The name of the requested turret point.
	 * @return {Object} - The requested turret point.
	 */
	TurretPointByName(turretPointName)
	{
		return this.turretPoints.find(turret => turret.name == turretPointName);
	}

	/**
	 * Remove the entity from a turret.
	 * @param {number} entity - The specific entity to eject.
	 * @param {boolean} forced - Whether ejection is forced (e.g. due to death or renaming).
	 * @param {Object} turret - Optionally the turret to abandon.
	 *
	 * @return {boolean} - Whether the entity succesfully left us.
	 */
	LeaveTurretPoint(entity, forced, requestedTurretPoint)
	{
		let turretPoint;
		if (requestedTurretPoint)
		{
			if (requestedTurretPoint.entity == entity)
				turretPoint = requestedTurretPoint;
		}
		else
			turretPoint = this.GetOccupiedTurretPoint(entity);

		if (!turretPoint || (!turretPoint.ejectable && !forced))
			return false;

		turretPoint.entity = null;

		Engine.PostMessage(this.entity, MT_TurretsChanged, {
			"added": [],
			"removed": [entity]
		});

		return true;
	}

	/**
	 * @param {number} entity - The entity's id.
	 * @param {Object} turret - Optionally the turret to check.
	 *
	 * @return {boolean} - Whether the entity is positioned on a turret of this entity.
	 */
	OccupiesTurretPoint(entity, requestedTurretPoint)
	{
		return requestedTurretPoint ? requestedTurretPoint.entity == entity :
			!!this.GetOccupiedTurretPoint(entity);
	}

	/**
	 * @param {number} entity - The entity's id.
	 * @return {Object} - The turret this entity is positioned on, if applicable.
	 */
	GetOccupiedTurretPoint(entity)
	{
		return this.turretPoints.find(turretPoint => turretPoint.entity == entity);
	}

	/**
	 * @param {number} entity - The entity's id.
	 * @return {Object} - The turret this entity is positioned on, if applicable.
	 */
	GetOccupiedTurretPointName(entity)
	{
		let turret = this.GetOccupiedTurretPoint(entity);
		return turret ? turret.name : "";
	}

	/**
	 * @return {number[]} - The turretted entityIDs.
	 */
	GetEntities()
	{
		let entities = [];
		for (let turretPoint of this.turretPoints)
			if (turretPoint.entity)
				entities.push(turretPoint.entity);
		return entities;
	}

	/**
	 * @return {boolean} - Whether all the turret points are occupied.
	 */
	IsFull()
	{
		return !!this.turretPoints.find(turretPoint => turretPoint.entity == null);
	}

	/**
	 * @return {Object} - Max and min ranges at which entities can occupy any turret.
	 */
	LoadingRange()
	{
		return { "min": 0, "max": +(this.template.LoadingRange || 2) };
	}

	/**
	 * @param {number} ent - The entity ID of the turret to be potentially picked up.
	 * @return {boolean} - Whether this entity can pick the specified entity up.
	 */
	CanPickup(ent)
	{
		if (!this.template.Pickup || this.IsFull())
			return false;
		let cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership);
		return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent);
	}

	/**
	 * @param {number[]} entities - The entities to ask to leave or to kill.
	 */
	EjectOrKill(entities)
	{
		let removedEntities = [];
		for (let entity of entities)
		{
			let cmpTurretable = Engine.QueryInterface(entity, IID_Turretable);
			if (!cmpTurretable || !cmpTurretable.LeaveTurret(true))
			{
				let cmpHealth = Engine.QueryInterface(entity, IID_Health);
				if (cmpHealth)
					cmpHealth.Kill();
				else
					Engine.DestroyEntity(entity);
				removedEntities.push(entity);
			}
		}
		if (removedEntities.length)
			Engine.PostMessage(this.entity, MT_TurretsChanged, {
				"added": [],
				"removed": removedEntities
			});
	}

	/**
	 * Sets an init turret, present from game start. (E.g. set in Atlas.)
	 * @param {String} turretName - The name of the turret point to be used.
	 * @param {number} entity - The entity-ID to be placed.
	 */
	SetInitEntity(turretName, entity)
	{
		if (!this.initTurrets)
			this.initTurrets = new Map();

		if (this.initTurrets.has(turretName))
			warn("The turret position " + turretName + " of entity " +
				this.entity + " is already set! Overwriting.");

		this.initTurrets.set(turretName, entity);
	}

	/**
	 * Update list of turreted entities when a game inits.
	 */
	OnGlobalSkirmishReplacerReplaced(msg)
	{
		if (!this.initTurrets)
			return;

		if (msg.entity == this.entity)
		{
			let cmpTurretHolder = Engine.QueryInterface(msg.newentity, IID_TurretHolder);
			if (cmpTurretHolder)
				cmpTurretHolder.initTurrets = this.initTurrets;
		}
		else
		{
			let entityIndex = this.initTurrets.indexOf(msg.entity);
			if (entityIndex != -1)
				this.initTurrets[entityIndex] = msg.newentity;
		}
	}

	/**
	 * Initialise turreted units.
	 */
	OnGlobalInitGame(msg)
	{
		if (!this.initTurrets)
			return;

		for (let [turretPointName, entity] of this.initTurrets)
		{
			let cmpTurretable = Engine.QueryInterface(entity, IID_Turretable);
			if (!cmpTurretable || !cmpTurretable.OccupyTurret(this.entity, turretPointName, this.TurretPointByName(turretPointName).ejectable))
				warn("Entity " + entity + " could not occupy the turret point " +
					turretPointName + " of turret holder " + this.entity + ".");
		}

		delete this.initTurrets;
	}

	/**
	 * @param {Object} msg - { "entity": number, "newentity": number }.
	 */
	OnEntityRenamed(msg)
	{
		for (let entity of this.GetEntities())
		{
			let cmpTurretable = Engine.QueryInterface(entity, IID_Turretable);
			if (!cmpTurretable)
				continue;
			let currentPoint = this.GetOccupiedTurretPointName(entity);
			cmpTurretable.LeaveTurret(true);
			cmpTurretable.OccupyTurret(msg.newentity, currentPoint);
		}
	}

	/**
	 * @param {Object} msg - { "entity": number, "from": number, "to": number }.
	 */
	OnOwnershipChanged(msg)
	{
		if (msg.to === INVALID_PLAYER)
		{
			this.EjectOrKill(this.GetEntities());
			return;
		}
		for (let point of this.turretPoints)
		{
			// If we were created, create any subunits now.
			// This has to be done here (instead of on Init)
			// for Ownership ought to be initialised.
			if (point.template && msg.from === INVALID_PLAYER)
			{
				this.CreateSubunit(point.name);
				continue;
			}
			if (!point.entity)
				continue;
			if (!point.ejectable)
			{
				let cmpTurretOwnership = Engine.QueryInterface(point.entity, IID_Ownership);
				if (cmpTurretOwnership)
					cmpTurretOwnership.SetOwner(msg.to);
			}
			else if (!IsOwnedByMutualAllyOfEntity(point.entity, this.entity))
			{
				let cmpTurretable = Engine.QueryInterface(point.entity, IID_Turretable);
				if (cmpTurretable)
					cmpTurretable.LeaveTurret();
			}
		}
		delete this.reservedTurrets;
	}
}

TurretHolder.prototype.Schema =
	"<element name='TurretPoints' a:help='Points that will be used to visibly garrison a unit.'>" +
		"<oneOrMore>" +
			"<element a:help='Element containing the offset coordinates.'>" +
				"<anyName/>" +
				"<interleave>" +
					"<element name='X'>" +
						"<data type='decimal'/>" +
					"</element>" +
					"<element name='Y'>" +
						"<data type='decimal'/>" +
					"</element>" +
					"<element name='Z'>" +
						"<data type='decimal'/>" +
					"</element>" +
					"<optional>" +
						"<interleave>" +
							"<element name='Template'>" +
								"<text/>" +
							"</element>" +
							"<element name='Ejectable' a:help='Whether this template is tied to the turret position (i.e. not allowed to leave the turret point).'>" +
								"<data type='boolean'/>" +
							"</element>" +
						"</interleave>" +
					"</optional>" +
					"<optional>" +
						"<element name='AllowedClasses' a:help='If specified, only entities matching the given classes will be able to use this turret.'>" +
							"<attribute name='datatype'>" +
								"<value>tokens</value>" +
							"</attribute>" +
							"<text/>" +
						"</element>" +
					"</optional>"+
					"<optional>" +
						"<element name='Angle' a:help='Angle in degrees relative to the turretHolder direction.'>" +
							"<data type='decimal'/>" +
						"</element>" +
					"</optional>" +
				"</interleave>" +
			"</element>" +
		"</oneOrMore>" +
	"</element>" +
	"<optional>" +
		"<element name='LoadingRange' a:help='The maximum distance from this holder at which entities are allowed to occupy a turret point. Should be about 2.0 for land entities and preferably greater for ships.'>" +
			"<ref name='nonNegativeDecimal'/>" +
		"</element>" +
	"</optional>"
	"<optional>" +
		"<element name='Pickup' a:help='This entity will try to move to pick up units to be turreted.'>" +
			"<data type='boolean'/>" +
		"</element>" +
	"</optional>";

Engine.RegisterComponentType(IID_TurretHolder, "TurretHolder", TurretHolder);