diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index 8b75917aa5..a73158bfa5 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -338,12 +338,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; }; @@ -460,7 +462,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 Ranged attacks get parabolic elevation adjustment + if (type !== "Ranged") + 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) @@ -775,14 +809,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 4cb5d3fbfc..33467c0281 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -4959,24 +4959,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"), {