Move some position-related functions to PositionHelper.

Renames helper Entity.js to Position.js and moves to there:
- `EntitiesNearPoint` (from Attacking.js).
- `InterpolatedLocation` (from Attacking.js).
- `TestCollision` (from Attacking.js).
- `PredictTimeToTarget` (from Attack.js).

Also adds a test for the helper.

Differential Revision: D2940
Reviewed By: @wraitii
Comments by: @Stan, @vladislavbelov
This was SVN commit r24128.
This commit is contained in:
Freagarach 2020-11-04 18:56:45 +00:00
parent 0addd691d1
commit e98fce58a6
12 changed files with 462 additions and 180 deletions

View File

@ -40,7 +40,7 @@ Trigger.prototype.SpawnWolvesAndAttack = function()
// The returned entities are sorted by RangeManager already
// Only consider units implementing Health since wolves deal damage.
let targets = Attacking.EntitiesNearPoint(attackerPos, 200, players, IID_Health).filter(ent => {
let targets = PositionHelper.EntitiesNearPoint(attackerPos, 200, players, IID_Health).filter(ent => {
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses);
});

View File

@ -55,7 +55,7 @@ AlertRaiser.prototype.RaiseAlert = function()
return false;
// Ensure that the garrison holder is within range of the alert raiser
if (+this.template.EndOfAlertRange > 0 && DistanceBetweenEntities(this.entity, ent) > +this.template.EndOfAlertRange)
if (+this.template.EndOfAlertRange > 0 && PositionHelper.DistanceBetweenEntities(this.entity, ent) > +this.template.EndOfAlertRange)
return false;
if (!cmpUnitAI.CheckTargetVisible(ent))

View File

@ -485,7 +485,7 @@ Attack.prototype.PerformAttack = function(type, target)
let previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition();
let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength);
let timeToTarget = this.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition;
// Add inaccuracy based on spread.
@ -559,36 +559,6 @@ Attack.prototype.PerformAttack = function(type, target)
Attacking.HandleAttackEffects(target, type, this.GetAttackEffectsData(type), this.entity, attackerOwner);
};
/**
* 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} selfPosition - the 3D position of the projectile (or chaser).
* @param {number} horizSpeed - 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 {Vector3D|boolean} - the 3D predicted position or false if the collision will not happen.
*/
Attack.prototype.PredictTimeToTarget = function(selfPosition, horizSpeed, targetPosition, targetVelocity)
{
let relativePosition = new Vector3D.sub(targetPosition, selfPosition);
let a = targetVelocity.x * targetVelocity.x + targetVelocity.z * targetVelocity.z - horizSpeed * horizSpeed;
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;
};
Attack.prototype.OnValueModification = function(msg)
{
if (msg.component != "Attack")

View File

@ -67,7 +67,7 @@ DelayedDamage.prototype.MissileHit = function(data, lateness)
// Deal direct damage if we hit the main target
// and we could handle the attack.
if (Attacking.TestCollision(target, data.position, lateness) &&
if (PositionHelper.TestCollision(target, data.position, lateness) &&
Attacking.HandleAttackEffects(target, data.type, data.attackData, data.attacker, data.attackerOwner))
{
cmpProjectileManager.RemoveProjectile(data.projectileId);
@ -75,12 +75,12 @@ DelayedDamage.prototype.MissileHit = function(data, lateness)
}
// If we didn't hit the main target look for nearby units.
let ents = Attacking.EntitiesNearPoint(Vector2D.from3D(data.position), this.MISSILE_HIT_RADIUS,
let ents = PositionHelper.EntitiesNearPoint(Vector2D.from3D(data.position), this.MISSILE_HIT_RADIUS,
Attacking.GetPlayersToDamage(data.attackerOwner, data.friendlyFire));
for (let ent of ents)
{
if (!Attacking.TestCollision(ent, data.position, lateness) ||
if (!PositionHelper.TestCollision(ent, data.position, lateness) ||
!Attacking.HandleAttackEffects(ent, data.type, data.attackData, data.attacker, data.attackerOwner))
continue;

View File

@ -363,7 +363,7 @@ UnitAI.prototype.UnitFsmSpec = {
let cmpPassengerMotion = Engine.QueryInterface(this.order.data.target, IID_UnitMotion);
if (cmpPassengerMotion &&
cmpPassengerMotion.IsTargetRangeReachable(this.entity, range.min, range.max) &&
DistanceBetweenEntities(this.entity, this.order.data.target) < 200)
PositionHelper.DistanceBetweenEntities(this.entity, this.order.data.target) < 200)
this.SetNextState("INDIVIDUAL.PICKUP.LOADING");
else
this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
@ -1716,7 +1716,7 @@ UnitAI.prototype.UnitFsmSpec = {
"FLEEING": {
"enter": function() {
// We use the distance between the entities to account for ranged attacks
this.order.data.distanceToFlee = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
this.order.data.distanceToFlee = PositionHelper.DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
// Use unit motion directly to ignore the visibility check. TODO: change this if we add LOS to fauna.
if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) ||
@ -4831,7 +4831,7 @@ UnitAI.prototype.CheckTargetIsInVisionRange = function(target)
return false;
let range = cmpVision.GetRange();
let distance = DistanceBetweenEntities(this.entity, target);
let distance = PositionHelper.DistanceBetweenEntities(this.entity, target);
return distance < range;
};

View File

@ -13,20 +13,16 @@ let entityID = 903;
function attackComponentTest(defenderClass, isEnemy, test_function)
{
ResetState();
let playerEnt1 = 5;
{
let playerEnt1 = 5;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": () => playerEnt1
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": () => playerEnt1
});
AddMock(playerEnt1, IID_Player, {
"GetPlayerID": () => 1,
"IsEnemy": () => isEnemy
});
}
AddMock(playerEnt1, IID_Player, {
"GetPlayerID": () => 1,
"IsEnemy": () => isEnemy
});
let attacker = entityID;
@ -341,37 +337,3 @@ testGetBestAttackAgainst("Archer", "Ranged", undefined);
testGetBestAttackAgainst("Domestic", "Slaughter", "Slaughter");
testGetBestAttackAgainst("Structure", "Capture", "Capture", true);
testGetBestAttackAgainst("Structure", "Ranged", undefined, false);
function testPredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity)
{
ResetState();
let cmpAttack = ConstructComponent(1, "Attack", {});
let timeToTarget = cmpAttack.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity);
if (timeToTarget === false)
return;
// Position of the target after that time.
let targetPos = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
// Time that the projectile need to reach it.
let time = targetPos.horizDistanceTo(selfPosition) / horizSpeed;
TS_ASSERT_EQUALS(timeToTarget.toFixed(1), time.toFixed(1));
}
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(16, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-16, 0, 0));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 1));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 4));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 16));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 1));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(2, 0, 2));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(8, 0, 8));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 1));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-2, 0, 2));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-8, 0, 8));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 2));
testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 2));

View File

@ -1,5 +1,6 @@
Engine.LoadHelperScript("Attacking.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Position.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/DelayedDamage.js");
Engine.LoadComponentScript("interfaces/Health.js");

View File

@ -1,6 +1,6 @@
Engine.LoadHelperScript("FSM.js");
Engine.LoadHelperScript("Entity.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Position.js");
Engine.LoadHelperScript("Sound.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Builder.js");
@ -85,7 +85,7 @@ TestTargetEntityRenaming(
TestTargetEntityRenaming(
"INDIVIDUAL.FLEEING", "INDIVIDUAL.FLEEING",
(unitAI, player_ent, target_ent) => {
DistanceBetweenEntities = () => 10;
PositionHelper.DistanceBetweenEntities = () => 10;
unitAI.CheckTargetRangeExplicit = () => false;
AddMock(player_ent, IID_UnitMotion, {

View File

@ -202,64 +202,6 @@ Attacking.prototype.GetTotalAttackEffects = function(target, effectData, effectT
return total * bonusMultiplier;
};
/**
* Gives the position of the given entity, taking the lateness into account.
* @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 location of the entity.
*/
Attacking.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 of an entity's footprint.
* @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.
*/
Attacking.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");
return false;
};
/**
* Get the list of players affected by the damage.
* @param {number} attackerOwner - The player id of the attacker.
@ -289,7 +231,7 @@ Attacking.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
*/
Attacking.prototype.CauseDamageOverArea = function(data)
{
let nearEnts = this.EntitiesNearPoint(data.origin, data.radius,
let nearEnts = PositionHelper.EntitiesNearPoint(data.origin, data.radius,
this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire));
let damageMultiplier = 1;
@ -396,24 +338,6 @@ Attacking.prototype.HandleAttackEffects = function(target, attackType, attackDat
return true;
};
/**
* Gets entities near a give point for given players.
* @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} itf - Interface IID that returned entities must implement. Defaults to none.
* @return {number[]} The id's of the entities in range of the given point.
*/
Attacking.prototype.EntitiesNearPoint = function(origin, radius, players, itf = 0)
{
// If there is insufficient data return an empty array.
if (!origin || !radius || !players || !players.length)
return [];
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.ExecuteQueryAroundPos(origin, 0, radius, players, itf);
};
/**
* Calculates the attack damage multiplier against a target.
* @param {number} source - The source entity's id.

View File

@ -1,16 +0,0 @@
function DistanceBetweenEntities(first, second)
{
var cmpFirstPosition = Engine.QueryInterface(first, IID_Position);
if (!cmpFirstPosition || !cmpFirstPosition.IsInWorld())
return Infinity;
var cmpSecondPosition = Engine.QueryInterface(second, IID_Position);
if (!cmpSecondPosition || !cmpSecondPosition.IsInWorld())
return Infinity;
var firstPosition = cmpFirstPosition.GetPosition2D();
var secondPosition = cmpSecondPosition.GetPosition2D();
return firstPosition.distanceTo(secondPosition);
}
Engine.RegisterGlobal("DistanceBetweenEntities", DistanceBetweenEntities);

View File

@ -0,0 +1,136 @@
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);
};
/**
* 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;
};
Engine.RegisterGlobal("PositionHelper", new PositionHelper());

View File

@ -0,0 +1,305 @@
Engine.LoadHelperScript("Position.js");
Engine.LoadComponentScript("interfaces/Timer.js");
class testDistanceBetweenEntities
{
constructor()
{
this.firstEntity = 1;
this.secondEntity = 2;
}
testOutOfWorldEntity()
{
AddMock(this.firstEntity, IID_Position, {
"GetPosition2D": () => new Vector3D(1, 0, 0),
"IsInWorld": () => false
});
AddMock(this.secondEntity, IID_Position, {
"GetPosition2D": () => new Vector3D(2, 0, 0),
"IsInWorld": () => true
});
TS_ASSERT_EQUALS(PositionHelper.DistanceBetweenEntities(this.firstEntity, this.secondEntity), Infinity);
DeleteMock(this.firstEntity, IID_Position);
DeleteMock(this.secondEntity, IID_Position);
}
testDistanceBetweenEntities(positionOne, positionTwo, expectedDistance)
{
AddMock(this.firstEntity, IID_Position, {
"GetPosition2D": () => positionOne,
"IsInWorld": () => true
});
AddMock(this.secondEntity, IID_Position, {
"GetPosition2D": () => positionTwo,
"IsInWorld": () => true
});
TS_ASSERT_EQUALS(PositionHelper.DistanceBetweenEntities(this.firstEntity, this.secondEntity), expectedDistance);
DeleteMock(this.firstEntity, IID_Position);
DeleteMock(this.secondEntity, IID_Position);
}
test()
{
this.testOutOfWorldEntity();
this.testDistanceBetweenEntities(new Vector3D(1, 0, 0), new Vector3D(0, 0, 0), 1);
this.testDistanceBetweenEntities(new Vector3D(0, 0, 0), new Vector3D(0, 2, 0), 2);
this.testDistanceBetweenEntities(new Vector3D(1, 2, 5), new Vector3D(4, 2, 5), 3);
this.testDistanceBetweenEntities(new Vector3D(7, 7, 7), new Vector3D(7, 7, 7), 0);
}
}
class testInterpolatedLocation
{
constructor()
{
this.entity = 1;
this.turnLength = 200;
AddMock(SYSTEM_ENTITY, IID_Timer, {
"GetLatestTurnLength": () => this.turnLength
});
}
testOutOfWorldEntity()
{
AddMock(this.entity, IID_Position, {
"IsInWorld": () => false
});
TS_ASSERT_EQUALS(PositionHelper.InterpolatedLocation(this.entity, 0), undefined);
DeleteMock(this.entity, IID_Position);
}
testInterpolatedLocation(previousPosition, currentPosition, expectedPosition, lateness)
{
AddMock(this.entity, IID_Position, {
"GetPreviousPosition": () => previousPosition,
"GetPosition": () => currentPosition,
"IsInWorld": () => true
});
TS_ASSERT_UNEVAL_EQUALS(PositionHelper.InterpolatedLocation(this.entity, lateness), expectedPosition);
DeleteMock(this.entity, IID_Position);
}
test()
{
this.testOutOfWorldEntity();
this.testInterpolatedLocation(new Vector3D(1, 0, 1), new Vector3D(2, 0, 2), new Vector3D(2, 0, 2), 0);
this.testInterpolatedLocation(new Vector3D(1, 0, 1), new Vector3D(2, 0, 2), new Vector3D(1, 0, 1), this.turnLength);
this.testInterpolatedLocation(new Vector3D(0, 0, 0), new Vector3D(0, 0, 0), new Vector3D(0, 0, 0), this.turnLength / 2);
this.testInterpolatedLocation(new Vector3D(0, 0, 0), new Vector3D(0, 0, 2), new Vector3D(0, 0, 1), this.turnLength / 2);
this.testInterpolatedLocation(new Vector3D(0, 0, 1), new Vector3D(0, 0, 2), new Vector3D(0, 0, 1.5), this.turnLength / 2);
this.testInterpolatedLocation(new Vector3D(0, 0, 1), new Vector3D(0, 0, 5), new Vector3D(0, 0, 4), this.turnLength / 4);
this.testInterpolatedLocation(new Vector3D(0, 0, -1), new Vector3D(0, 0, 3), new Vector3D(0, 0, 1), this.turnLength / 2);
this.testInterpolatedLocation(new Vector3D(0, 0, -1), new Vector3D(0, 0, -3), new Vector3D(0, 0, -2), this.turnLength / 2);
// Y is ignored.
this.testInterpolatedLocation(new Vector3D(1, 1, 1), new Vector3D(3, 3, 3), new Vector3D(2.5, 0, 2.5), this.turnLength / 4);
this.testInterpolatedLocation(new Vector3D(0, 1, 0), new Vector3D(0, 3, 0), new Vector3D(0, 0, 0), this.turnLength / 2);
}
}
class testTestCollision
{
constructor()
{
this.entity = 1;
AddMock(SYSTEM_ENTITY, IID_Timer, {
"GetLatestTurnLength": () => 200
});
}
testOutOfWorldEntity()
{
AddMock(this.entity, IID_Position, {
"IsInWorld": () => false
});
TS_ASSERT(!PositionHelper.TestCollision(this.entity, new Vector3D(0, 0, 0), 0));
DeleteMock(this.entity, IID_Position);
}
testFootprintlessEntity()
{
AddMock(this.entity, IID_Position, {
"GetPreviousPosition": () => new Vector3D(0, 0, 0),
"GetPosition": () => new Vector3D(0, 0, 0),
"IsInWorld": () => true
});
TS_ASSERT(!PositionHelper.TestCollision(this.entity, new Vector3D(0, 0, 0), 0));
AddMock(this.entity, IID_Footprint, {
"GetShape": () => {}
});
TS_ASSERT(!PositionHelper.TestCollision(this.entity, new Vector3D(0, 0, 0), 0));
DeleteMock(this.entity, IID_Position);
DeleteMock(this.entity, IID_Footprint);
}
testTestCollision(footprint, collisionPoint, entityPosition, expectedResult)
{
PositionHelper.InterpolatedLocation = (ent, lateness) => entityPosition;
AddMock(this.entity, IID_Footprint, {
"GetShape": () => footprint
});
TS_ASSERT_EQUALS(PositionHelper.TestCollision(this.entity, collisionPoint, 0), expectedResult);
DeleteMock(this.entity, IID_Footprint);
}
testCircularFootprints()
{
// Interesting edge-case.
this.testTestCollision({ "type": "circle", "radius": 0 }, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0), false);
this.testTestCollision({ "type": "circle", "radius": 2 }, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0), true);
this.testTestCollision({ "type": "circle", "radius": 2 }, new Vector3D(0, 0, 0), new Vector3D(0, 0, 1), true);
this.testTestCollision({ "type": "circle", "radius": 2 }, new Vector3D(0, 0, 0), new Vector3D(2, 0, 2), false);
this.testTestCollision({ "type": "circle", "radius": 3 }, new Vector3D(-1, 0, -1), new Vector3D(1, 0, 1), true);
this.testTestCollision({ "type": "circle", "radius": 3 }, new Vector3D(-1, 0, 1), new Vector3D(1, 0, -1), true);
// Y is ignored.
this.testTestCollision({ "type": "circle", "radius": 2 }, new Vector3D(0, 0, 0), new Vector3D(1, 5, 1), true);
this.testTestCollision({ "type": "circle", "radius": 1 }, new Vector3D(0, -100, 0), new Vector3D(0, 100, 0), true);
}
/**
* Edges may be not colliding due to rounding issues.
*/
testSquareFootprints()
{
let square = { "type": "square", "width": 2, "depth": 2 };
AddMock(this.entity, IID_Position, {
"GetRotation": () => new Vector3D(0, 0, 0)
});
// Interesting edge-case.
this.testTestCollision({ "type": "square", "width": 0, "depth": 0 }, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0), false);
this.testTestCollision(square, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0), true);
this.testTestCollision(square, new Vector3D(0, 0, 0), new Vector3D(0.999, 0, 0), true);
this.testTestCollision(square, new Vector3D(0, 0, 0), new Vector3D(0.999, 0, 0.999), true);
this.testTestCollision(square, new Vector3D(0, 0, 0), new Vector3D(2, 0, 2), false);
this.testTestCollision(square, new Vector3D(-1, 0, -1), new Vector3D(-0.001, 0, -0.001), true);
this.testTestCollision({ "type": "square", "width": 4, "depth": 4 }, new Vector3D(-1, 0, 1), new Vector3D(0.999, 0, -0.999), true);
// Y is ignored.
this.testTestCollision(square, new Vector3D(0, 0, 0), new Vector3D(0.5, 10, 0.5), true);
this.testTestCollision(square, new Vector3D(-1, 50, 0), new Vector3D(-0.5, 10, 0), true);
// Test rotated.
this.testTestCollision(square, new Vector3D(0, 0, 0), new Vector3D(Math.sqrt(2), 0, 0), false);
AddMock(this.entity, IID_Position, {
"GetRotation": () => new Vector3D(0, Math.PI / 4, 0)
});
this.testTestCollision(square, new Vector3D(0, 0, 0), new Vector3D(Math.sqrt(2), 0, 0), true);
this.testTestCollision(square, new Vector3D(0, 0, 0), new Vector3D(0.999, 0, 0.999), false);
DeleteMock(this.entity, IID_Position);
}
/**
* Edges may be not colliding due to rounding issues.
*/
testRectangularFootprints()
{
let rectangle = { "type": "square", "width": 2, "depth": 4 };
AddMock(this.entity, IID_Position, {
"GetRotation": () => new Vector3D(0, 0, 0)
});
this.testTestCollision(rectangle, new Vector3D(0, 0, 0), new Vector3D(1.999, 0, 0), false);
this.testTestCollision(rectangle, new Vector3D(0, 0, 0), new Vector3D(0, 0, 1.999), true);
AddMock(this.entity, IID_Position, {
"GetRotation": () => new Vector3D(0, Math.PI / 2, 0)
});
this.testTestCollision(rectangle, new Vector3D(0, 0, 0), new Vector3D(1.999, 0, 0), true);
this.testTestCollision(rectangle, new Vector3D(0, 0, 0), new Vector3D(0, 0, 1.999), false);
DeleteMock(this.entity, IID_Position);
}
test()
{
this.testOutOfWorldEntity();
this.testFootprintlessEntity();
this.testCircularFootprints();
this.testSquareFootprints();
this.testRectangularFootprints();
}
}
class testPredictTimeToTarget
{
constructor()
{
this.uncertainty = 0.0001;
}
testPredictTimeToTarget(targetPosition, targetVelocity)
{
let selfPosition = new Vector3D(0, 0, 0);
let selfSpeed = 4;
let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, selfSpeed, targetPosition, targetVelocity);
if (timeToTarget === false)
return;
// Position of the target after that time.
let targetPos = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition);
// Time that the projectile need to reach it.
let time = targetPos.horizDistanceTo(selfPosition) / selfSpeed;
TS_ASSERT(Math.abs(timeToTarget - time) < this.uncertainty);
}
test()
{
this.testPredictTimeToTarget(new Vector3D(0, 0, 0), new Vector3D(0, 0, 0));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(0, 0, 0));
// Moving away from us in a straight line.
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(1, 0, 0));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(4, 0, 0));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(16, 0, 0));
// Moving towards us in a straight line.
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(-1, 0, 0));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(-4, 0, 0));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(-16, 0, 0));
// Away from us in right angle.
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(0, 0, 1));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(0, 0, 4));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(0, 0, 16));
// No straight lines away from us.
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(1, 0, 1));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(2, 0, 2));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(8, 0, 8));
// No straight lines towards us.
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(-1, 0, 1));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(-2, 0, 2));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(-8, 0, 8));
// Mixed numbers.
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(4, 0, 2));
this.testPredictTimeToTarget(new Vector3D(20, 0, 0), new Vector3D(-4, 0, 2));
}
}
new testDistanceBetweenEntities().test();
new testInterpolatedLocation().test();
new testTestCollision().test();
new testPredictTimeToTarget().test();