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 393c73da15..82c7d3ebc8 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -429,3 +429,100 @@ 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 + { + const defender = ++entityID; + AddMock(defender, IID_Position, { + "IsInWorld": () => true, + "GetHeightOffset": () => 0 + }); + TS_ASSERT_EQUALS(cmpAttack.CanEverReachTarget(defender, "Melee"), true); + } + + // Melee target too high + { + 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) + { + 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) + { + 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, + "GetMaxReachableParabolicHeight": () => 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) + { + 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 + "GetMaxReachableParabolicHeight": () => 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..5a63d77ab9 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,80 @@ cmpTurretHolder.OnOwnershipChanged({ "from": INVALID_PLAYER }); TS_ASSERT(cmpTurretHolder.OccupiesTurretPoint(spawned)); + +// Test GetClosestApproachDistanceToTurretPoint +{ + 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 + ++entityID; + const cmpHolderNoObst = ConstructComponent(entityID, "TurretHolder", { + "TurretPoints": { + "center": { "X": "0", "Y": "5.0", "Z": "0" } + } + }); + TS_ASSERT_EQUALS(cmpHolderNoObst.GetClosestApproachDistanceToTurretPoint("center"), 0); +} + diff --git a/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml b/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml index ad4420ea11..03986d1f16 100644 --- a/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml +++ b/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml @@ -67,7 +67,7 @@ 18.0 - 80 + 10 structures/fndn_3x3.xml diff --git a/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml b/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml index cdeb991da8..ba4a764782 100644 --- a/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml +++ b/binaries/data/mods/public/simulation/templates/template_unit_infantry.xml @@ -110,6 +110,6 @@ - 80 + 10 diff --git a/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml b/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml index 8abdc20655..454a7bd775 100644 --- a/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml +++ b/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml @@ -74,6 +74,6 @@ 0.75 - 100 + 10