Source: Position.js

/**
 * @class
 */
function PositionHelper() {}

/**
 * @param {number} firstEntity - The entityID of an entity.
 * @param {number} secondEntity - The entityID of an entity.
 *
 * @return {number} - The horizontal distance between the two given entities. Returns
 *			infinity when the distance cannot be calculated.
 */
PositionHelper.prototype.DistanceBetweenEntities = function(firstEntity, secondEntity)
{
	let cmpFirstPosition = Engine.QueryInterface(firstEntity, IID_Position);
	if (!cmpFirstPosition || !cmpFirstPosition.IsInWorld())
		return Infinity;

	let cmpSecondPosition = Engine.QueryInterface(secondEntity, IID_Position);
	if (!cmpSecondPosition || !cmpSecondPosition.IsInWorld())
		return Infinity;

	return cmpFirstPosition.GetPosition2D().distanceTo(cmpSecondPosition.GetPosition2D());
};

/**
 * @param {Vector2D} origin - The point to check around.
 * @param {number}   radius - The radius around the point to check.
 * @param {number[]} players - The players of which we need to check entities.
 * @param {number}   iid - Interface IID that returned entities must implement. Defaults to none.
 *
 * @return {number[]} The id's of the entities in range of the given point.
 */
PositionHelper.prototype.EntitiesNearPoint = function(origin, radius, players, iid = 0)
{
	if (!origin || !radius || !players || !players.length)
		return [];

	let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
	return cmpRangeManager.ExecuteQueryAroundPos(origin, 0, radius, players, iid, true);
};

/**
 * Gives the position of the given entity, taking the lateness into account.
 * Note that vertical movement is ignored.
 *
 * @param {number} ent - Entity id of the entity we are finding the location for.
 * @param {number} lateness - The time passed since the expected time to fire the function.
 *
 * @return {Vector3D} The interpolated location of the entity.
 */
PositionHelper.prototype.InterpolatedLocation = function(ent, lateness)
{
	let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
	if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
		return undefined;
	let curPos = cmpTargetPosition.GetPosition();
	let prevPos = cmpTargetPosition.GetPreviousPosition();
	let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
	let turnLength = cmpTimer.GetLatestTurnLength();
	return new Vector3D(
	    (curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength,
	    0,
	    (curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength
	);
};

/**
 * Test if a point is inside an entity's footprint.
 * Note that edges may be not included for square entities due to rounding.
 *
 * @param {number} ent - Id of the entity we are checking with.
 * @param {Vector3D} point - The point we are checking with.
 * @param {number} lateness - The time passed since the expected time to fire the function.
 *
 * @return {boolean} True if the point is inside of the entity's footprint.
 */
PositionHelper.prototype.TestCollision = function(ent, point, lateness)
{
	let targetPosition = this.InterpolatedLocation(ent, lateness);
	if (!targetPosition)
		return false;

	let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
	if (!cmpFootprint)
		return false;

	let targetShape = cmpFootprint.GetShape();
	if (!targetShape)
		return false;

	if (targetShape.type == "circle")
		return targetPosition.horizDistanceToSquared(point) < targetShape.radius * targetShape.radius;

	if (targetShape.type == "square")
	{
		let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
		let distance = Vector2D.from3D(Vector3D.sub(point, targetPosition)).rotate(-angle);
		return Math.abs(distance.x) < targetShape.width / 2 && Math.abs(distance.y) < targetShape.depth / 2;
	}

	warn("TestCollision called with an invalid footprint shape: " + targetShape.type + ".");
	return false;
};

/**
 * Get the predicted time of collision between a projectile (or a chaser)
 * and its target, assuming they both move in straight line at a constant speed.
 * Vertical component of movement is ignored.
 *
 * @param {Vector3D} firstPosition - The 3D position of the projectile (or chaser).
 * @param {number} selfSpeed - The horizontal speed of the projectile (or chaser).
 * @param {Vector3D} targetPosition - The 3D position of the target.
 * @param {Vector3D} targetVelocity - The 3D velocity vector of the target.
 *
 * @return {number|boolean} - The time to collision or false if the collision will not happen.
 */
PositionHelper.prototype.PredictTimeToTarget = function(firstPosition, selfSpeed, targetPosition, targetVelocity)
{
	let relativePosition = new Vector3D.sub(targetPosition, firstPosition);
	let a = targetVelocity.x * targetVelocity.x + targetVelocity.z * targetVelocity.z - selfSpeed * selfSpeed;
	let b = relativePosition.x * targetVelocity.x + relativePosition.z * targetVelocity.z;
	let c = relativePosition.x * relativePosition.x + relativePosition.z * relativePosition.z;

	// The predicted time to reach the target is the smallest non negative solution
	// (when it exists) of the equation a t^2 + 2 b t + c = 0.
	// Using c>=0, we can straightly compute the right solution.

	if (c == 0)
		return 0;

	let disc = b * b - a * c;
	if (a < 0 || b < 0 && disc >= 0)
		return c / (Math.sqrt(disc) - b);

	return false;
};

/**
 * @param {number} target - EntityID to find the spawn position for.
 * @param {number} entity - EntityID to find the spawn position for.
 * @param {boolean} forced - Optionally whether the spawning is forced.
 * @return {Vector3D} - An appropriate spawning position.
 */
PositionHelper.prototype.GetSpawnPosition = function(target, entity, forced)
{
	let cmpFootprint = Engine.QueryInterface(target, IID_Footprint);
	let cmpHealth = Engine.QueryInterface(target, IID_Health);
	let cmpIdentity = Engine.QueryInterface(target, IID_Identity);

	if (!cmpFootprint)
		return null;

	// If the spawner is a sinking ship, restrict the location to the intersection of both passabilities.
	// TODO: should use passability classes to be more generic.
	let pos;
	if ((!cmpHealth || cmpHealth.GetHitpoints() == 0) && cmpIdentity && cmpIdentity.HasClass("Ship"))
		pos = cmpFootprint.PickSpawnPointBothPass(entity);
	else
		pos = cmpFootprint.PickSpawnPoint(entity);

	if (pos.y < 0)
	{
		if (!forced)
			return null;

		// If ejection is forced, we need to continue, so use center of the entity.
		let cmpPosition = Engine.QueryInterface(target, IID_Position);
		pos = cmpPosition.GetPosition();
	}
	return pos;
};

Engine.RegisterGlobal("PositionHelper", new PositionHelper());