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..7a7575d181 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
+ const cmpHolderNoObst = ConstructComponent(++entityID, "TurretHolder", {
+ "TurretPoints": {
+ "center": { "X": "0", "Y": "5.0", "Z": "0" }
+ }
+ });
+ const result = cmpHolderNoObst.GetClosestApproachDistanceToTurretPoint("center");
+ TS_ASSERT_EQUALS(result, 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