Index: binaries/data/mods/public/maps/random/polar_sea_triggers.js =================================================================== --- binaries/data/mods/public/maps/random/polar_sea_triggers.js +++ binaries/data/mods/public/maps/random/polar_sea_triggers.js @@ -40,7 +40,7 @@ // 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); }); Index: binaries/data/mods/public/simulation/components/AlertRaiser.js =================================================================== --- binaries/data/mods/public/simulation/components/AlertRaiser.js +++ binaries/data/mods/public/simulation/components/AlertRaiser.js @@ -55,7 +55,7 @@ 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)) Index: binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/Attack.js +++ binaries/data/mods/public/simulation/components/Attack.js @@ -485,7 +485,7 @@ 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 @@ 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") Index: binaries/data/mods/public/simulation/components/DelayedDamage.js =================================================================== --- binaries/data/mods/public/simulation/components/DelayedDamage.js +++ binaries/data/mods/public/simulation/components/DelayedDamage.js @@ -67,7 +67,7 @@ // 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 @@ } // 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; Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -382,7 +382,7 @@ 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"); @@ -1791,7 +1791,7 @@ "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) || @@ -4971,7 +4971,7 @@ return false; let range = cmpVision.GetRange(); - let distance = DistanceBetweenEntities(this.entity, target); + let distance = PositionHelper.DistanceBetweenEntities(this.entity, target); return distance < range; }; Index: binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -13,20 +13,16 @@ 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("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)); Index: binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ binaries/data/mods/public/simulation/components/tests/test_Damage.js @@ -1,5 +1,6 @@ Engine.LoadHelperScript("Attacking.js"); Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("PositionHelper.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/DelayedDamage.js"); Engine.LoadComponentScript("interfaces/Health.js"); Index: binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -1,6 +1,6 @@ Engine.LoadHelperScript("FSM.js"); -Engine.LoadHelperScript("Entity.js"); Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("PositionHelper.js"); Engine.LoadHelperScript("Sound.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Builder.js"); @@ -85,7 +85,7 @@ TestTargetEntityRenaming( "INDIVIDUAL.FLEEING", "INDIVIDUAL.FLEEING", (unitAI, player_ent, target_ent) => { - DistanceBetweenEntities = () => 10; + PositionHelper.DistanceBetweenEntities = () => 10; unitAI.CheckTargetRangeExplicit = () => false; AddMock(player_ent, IID_UnitMotion, { Index: binaries/data/mods/public/simulation/helpers/Attacking.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Attacking.js +++ binaries/data/mods/public/simulation/helpers/Attacking.js @@ -203,64 +203,6 @@ }; /** - * 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. * @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged. @@ -289,7 +231,7 @@ */ 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; @@ -397,24 +339,6 @@ }; /** - * 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. * @param {number} target - The target entity's id. Index: binaries/data/mods/public/simulation/helpers/Entity.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Entity.js +++ binaries/data/mods/public/simulation/helpers/Entity.js @@ -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); Index: binaries/data/mods/public/simulation/helpers/PositionHelper.js =================================================================== --- binaries/data/mods/public/simulation/helpers/PositionHelper.js +++ binaries/data/mods/public/simulation/helpers/PositionHelper.js @@ -1,16 +1,139 @@ -function DistanceBetweenEntities(first, second) +/** + * Provides position-related helpers. + */ +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) { - var cmpFirstPosition = Engine.QueryInterface(first, IID_Position); + let cmpFirstPosition = Engine.QueryInterface(firstEntity, IID_Position); if (!cmpFirstPosition || !cmpFirstPosition.IsInWorld()) - return Infinity; + return Infinity; - var cmpSecondPosition = Engine.QueryInterface(second, IID_Position); + let cmpSecondPosition = Engine.QueryInterface(secondEntity, IID_Position); if (!cmpSecondPosition || !cmpSecondPosition.IsInWorld()) - return Infinity; + 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} 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. + */ +PositionHelper.prototype.EntitiesNearPoint = function(origin, radius, players, itf = 0) +{ + if (!origin || !radius || !players || !players.length) + return []; + + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + return cmpRangeManager.ExecuteQueryAroundPos(origin, 0, radius, players, itf); +}; + +/** + * 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); - var firstPosition = cmpFirstPosition.GetPosition2D(); - var secondPosition = cmpSecondPosition.GetPosition2D(); - return firstPosition.distanceTo(secondPosition); -} + return false; +}; -Engine.RegisterGlobal("DistanceBetweenEntities", DistanceBetweenEntities); +Engine.RegisterGlobal("PositionHelper", new PositionHelper()); Index: binaries/data/mods/public/simulation/helpers/tests/test_PositionHelper.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/helpers/tests/test_PositionHelper.js @@ -0,0 +1,290 @@ +Engine.LoadHelperScript("PositionHelper.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(selfPosition, selfSpeed, targetPosition, targetVelocity) + { + 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), 4, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 0)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 0)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 0)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(16, 0, 0)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 0)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 0)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-16, 0, 0)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 1)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 4)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 16)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 1)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(2, 0, 2)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(8, 0, 8)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 1)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-2, 0, 2)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-8, 0, 8)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 2)); + this.testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 2)); + } +} + +new testDistanceBetweenEntities().test(); +new testInterpolatedLocation().test(); +new testTestCollision().test(); +new testPredictTimeToTarget().test();