diff --git a/binaries/data/mods/public/simulation/components/tests/test_Attack.js b/binaries/data/mods/public/simulation/components/tests/test_Attack.js index cac8a99d87..7adfcc334f 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -54,6 +54,15 @@ function attackComponentTest(defenderClass, isEnemy, test_function) "IsEnemy": () => isEnemy }); + AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { + "IsInTargetRange": () => true + }); + + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "GetEffectiveParabolicRange": () => 25, + "GetMaxParabolicHeightDiff": () => 15 + }); + const attacker = entityID; AddMock(attacker, IID_Position, { @@ -419,3 +428,105 @@ function testAttackPreference() } testAttackPreference(); +function testCanEverReachTarget() +{ + const attacker = ++entityID; + + AddMock(attacker, IID_Position, { + "IsInWorld": () => true, + "GetHeightOffset": () => 0, + "GetPosition2D": () => new Vector2D(1, 2) + }); + + const cmpAttack = ConstructComponent(attacker, "Attack", { + "Melee": { + "Damage": { "Hack": 10, "Pierce": 0, "Crush": 0 }, + "MaxRange": 5 + }, + "Ranged": { + "Damage": { "Hack": 0, "Pierce": 10, "Crush": 0 }, + "MaxRange": 30, + "Projectile": { "Speed": 50, "Spread": 1, "Gravity": 1, "FriendlyFire": "false" } + } + }); + + // Melee target within 3D range + (function() + { + const defender = ++entityID; + AddMock(defender, IID_Position, { + "IsInWorld": () => true, + "GetHeightOffset": () => 0 + }); + TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Melee"), true); + })(); + + // Melee target too high + (function() + { + const defender = ++entityID; + AddMock(defender, IID_Position, { + "IsInWorld": () => true, + "GetHeightOffset": () => 10 + }); + TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Melee"), false); + })(); + + // Melee target at same height, within range (close distance) + (function() + { + const defender = ++entityID; + AddMock(defender, IID_Position, { + "IsInWorld": () => true, + "GetHeightOffset": () => 4 + }); + // sqrt(0² + 4²) = 4 <= 5 + TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Melee"), true); + })(); + + // Ranged: target at same height — reachable from current position (check 1) + (function() + { + const defender = ++entityID; + AddMock(defender, IID_Position, { + "IsInWorld": () => true, + "GetHeightOffset": () => 0, + "GetPosition": () => new Vector3D(1, 0, 2) + }); + // Need RangeManager mock for IsTargetInRange (check 1) to work + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "GetEffectiveParabolicRange": () => 25, + "GetMaxParabolicHeightDiff": () => 15 + }); + AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { + "IsInTargetRange": () => true + }); + TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Ranged"), true); + })(); + + // Ranged: target too high for parabolic arc even at closest approach (check 2) + (function() + { + const defender = ++entityID; + AddMock(defender, IID_Position, { + "IsInWorld": () => true, + "GetHeightOffset": () => 20, + "GetPosition": () => new Vector3D(1, 20, 2) + }); + + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "GetEffectiveParabolicRange": () => -1, // out of range + "GetMaxParabolicHeightDiff": () => 10 + }); + + AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { + "IsInTargetRange": () => false + }); + + // heightDiff = 20 - 0 = 20, maxReachableHeightDiff = 10 → unreachable + TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Ranged"), false); + })(); +} +testCanEverReachTarget(); + + diff --git a/binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js b/binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js index 2d7ecf7450..5f7cfbe94d 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js +++ b/binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js @@ -15,6 +15,7 @@ const enemyPlayer = 2; const alliedPlayer = 3; const turretHolderID = 9; const entitiesToTest = [10, 11, 12, 13]; +let entityID = 100; AddMock(turretHolderID, IID_Ownership, { "GetOwner": () => player @@ -244,3 +245,81 @@ cmpTurretHolder.OnOwnershipChanged({ "from": INVALID_PLAYER }); TS_ASSERT(cmpTurretHolder.OccupiesTurretPoint(spawned)); + +// Test GetClosestApproachDistanceToTurretPoint +(function() +{ + const holder = ++entityID; + + // Mock the holder's obstruction + AddMock(holder, IID_Obstruction, { + "GetBlockMovementFlag": () => true, + "GetObstructionHalfSizes": () => ({ "X": 10, "Y": 15 }) + }); + + const cmpHolder = ConstructComponent(holder, "TurretHolder", { + "TurretPoints": { + "center": { + "X": "0", + "Y": "5.0", + "Z": "0" + }, + "edge": { + "X": "8.0", + "Y": "5.0", + "Z": "0" + }, + "corner": { + "X": "10.0", + "Y": "5.0", + "Z": "15.0" + }, + "outside": { + "X": "15.0", + "Y": "5.0", + "Z": "0" + } + } + }); + + // Center point (0,0) in 20x30 building → min(10, 15) = 10 + TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint("center"), 10); + + // Edge point (8,0) in 20x30 building → min(10-8, 15-0) = 2 + TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint("edge"), 2); + + // Corner point (10,15) in 20x30 building → min(10-10, 15-15) = 0 + TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint("corner"), 0); + + // Outside point (15,0) in 20x30 building → min(10-15, 15-0) = -5 → clamped to 0 + TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint("outside"), 0); + + // Nonexistent turret point + TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint("nonexistent"), 0); + + // Pass object directly + const turretPoint = cmpHolder.TurretPointByName("center"); + TS_ASSERT_EQUALS(cmpHolder.GetClosestApproachDistanceToTurretPoint(turretPoint), 10); + + // Passable building (no obstruction or doesn't block movement) + const passableHolder = ++entityID; + AddMock(passableHolder, IID_Obstruction, { + "GetBlockMovementFlag": () => false + }); + const cmpHolderPassable = ConstructComponent(passableHolder, "TurretHolder", { + "TurretPoints": { + "center": { "X": "0", "Y": "5.0", "Z": "0" } + } + }); + TS_ASSERT_EQUALS(cmpHolderPassable.GetClosestApproachDistanceToTurretPoint("center"), 0); + + // No obstruction component at all + const noObstructionHolder = ++entityID; + const cmpHolderNoObst = ConstructComponent(noObstructionHolder, "TurretHolder", { + "TurretPoints": { + "center": { "X": "0", "Y": "5.0", "Z": "0" } + } + }); + TS_ASSERT_EQUALS(cmpHolderNoObst.GetClosestApproachDistanceToTurretPoint("center"), 0); +})(); +