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:
parent
0addd691d1
commit
e98fce58a6
@ -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);
|
||||
});
|
||||
|
@ -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))
|
||||
|
@ -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")
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -12,9 +12,6 @@ Engine.LoadComponentScript("Attack.js");
|
||||
let entityID = 903;
|
||||
|
||||
function attackComponentTest(defenderClass, isEnemy, test_function)
|
||||
{
|
||||
ResetState();
|
||||
|
||||
{
|
||||
let playerEnt1 = 5;
|
||||
|
||||
@ -26,7 +23,6 @@ function attackComponentTest(defenderClass, isEnemy, test_function)
|
||||
"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));
|
||||
|
@ -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");
|
||||
|
@ -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, {
|
||||
|
@ -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.
|
||||
|
@ -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);
|
136
binaries/data/mods/public/simulation/helpers/Position.js
Normal file
136
binaries/data/mods/public/simulation/helpers/Position.js
Normal 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());
|
@ -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();
|
Loading…
Reference in New Issue
Block a user