From 67b52e53be68ecfc8e79d2973481fd7c141b369f Mon Sep 17 00:00:00 2001 From: Atrik Date: Fri, 1 May 2026 14:42:14 +0200 Subject: [PATCH] Add GetEffectiveAttackRange wrapper Wraps RangeManager.GetEffectiveParabolicRange for unified range resolution. Maintains symmetry with GetRange(), improves readability, and consistently translates NEVER_IN_RANGE. Simplifies callers in UnitAI and Attack. --- .../public/simulation/components/Attack.js | 49 +++++++++++++++---- .../public/simulation/components/UnitAI.js | 18 +++---- .../components/tests/test_Attack.js | 2 +- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index ec6162b2b5..9a6077b087 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -353,12 +353,14 @@ Attack.prototype.GetPreference = function(target) */ Attack.prototype.GetFullAttackRange = function() { - const ret = { "min": Infinity, "max": 0 }; + const ret = { "min": Infinity, "max": 0, "parabolic": false }; for (const type of this.GetAttackTypes()) { const range = this.GetRange(type); ret.min = Math.min(ret.min, range.min); ret.max = Math.max(ret.max, range.max); + if (range.parabolic) + ret.parabolic = true; } return ret; }; @@ -475,7 +477,39 @@ Attack.prototype.GetRange = function(type) let min = +(this.template[type].MinRange || 0); min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity); - return { "max": max, "min": min }; + return { + "max": max, + "min": min, + "parabolic": type === "Ranged" + }; +}; + +/** + * Get the effective range for attacking a specific target, accounting + * for elevation and projectile physics where applicable. + * @param {number} target - The target entity ID. + * @param {string} type - The attack type. + * @return {{ min: number, max: number }} - The min and max effective range. + */ +Attack.prototype.GetEffectiveAttackRange = function(target, type) +{ + const range = this.GetRange(type); + + // Only Parabolic attacks get parabolic elevation adjustment + if (!range.parabolic) + return range; + + const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + if (!cmpRangeManager) + return range; + + const effectiveMax = cmpRangeManager.GetEffectiveParabolicRange( + this.entity, target, range.max, this.GetAttackYOrigin(type)); + + if (effectiveMax < 0) + return { "min": Infinity, "max": 0 }; // Out of range + + return { "min": range.min, "max": effectiveMax }; }; Attack.prototype.GetAttackYOrigin = function(type) @@ -813,14 +847,9 @@ Attack.prototype.PerformAttack = function(type, target) */ Attack.prototype.IsTargetInRange = function(target, type) { - const range = this.GetRange(type); - return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetParabolicRange( - this.entity, - target, - range.min, - range.max, - this.GetAttackYOrigin(type), - false); + const range = this.GetEffectiveAttackRange(target, type); + return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetRange( + this.entity, target, range.min, range.max, false); }; Attack.prototype.OnValueModification = function(msg) diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 754bfb4a47..34ef07fd66 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -5167,24 +5167,24 @@ UnitAI.prototype.MoveToTargetAttackRange = function(target, type) if (cmpFormation) target = cmpFormation.GetClosestMemberToEntity(this.entity); - if (type != "Ranged") - return this.MoveToTargetRange(target, IID_Attack, type); - if (!this.CheckTargetVisible(target)) return false; const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return false; - const range = cmpAttack.GetRange(type); - // In case the range returns negative, we are probably too high compared to the target. Hope we come close enough. - const parabolicMaxRange = Math.max(0, Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEffectiveParabolicRange(this.entity, target, range.max, cmpAttack.GetAttackYOrigin(type))); + const flatRange = cmpAttack.GetRange(type); + const effectiveRange = cmpAttack.GetEffectiveAttackRange(target, type); + if (effectiveRange.max < 0) + return false; - // The parabole changes while walking so be cautious: - const guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange; + // The parabola changes while walking so be cautious: + const guessedMaxRange = effectiveRange.max > flatRange.max ? + (flatRange.max + effectiveRange.max) / 2 : + effectiveRange.max; - return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange); + return cmpUnitMotion && cmpUnitMotion.MoveToTargetRange(target, effectiveRange.min, guessedMaxRange); }; UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max) 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 3024fff1f7..f4530f834d 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -201,7 +201,7 @@ attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["Civilian"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 }); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80, "parabolic": true }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), {