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.
This commit is contained in:
Atrik
2026-05-01 14:42:14 +02:00
committed by Vantha
parent f12975e9b9
commit db23584fc3
3 changed files with 49 additions and 20 deletions
@@ -353,12 +353,14 @@ Attack.prototype.GetPreference = function(target)
*/ */
Attack.prototype.GetFullAttackRange = function() Attack.prototype.GetFullAttackRange = function()
{ {
const ret = { "min": Infinity, "max": 0 }; const ret = { "min": Infinity, "max": 0, "parabolic": false };
for (const type of this.GetAttackTypes()) for (const type of this.GetAttackTypes())
{ {
const range = this.GetRange(type); const range = this.GetRange(type);
ret.min = Math.min(ret.min, range.min); ret.min = Math.min(ret.min, range.min);
ret.max = Math.max(ret.max, range.max); ret.max = Math.max(ret.max, range.max);
if (range.parabolic)
ret.parabolic = true;
} }
return ret; return ret;
}; };
@@ -475,7 +477,39 @@ Attack.prototype.GetRange = function(type)
let min = +(this.template[type].MinRange || 0); let min = +(this.template[type].MinRange || 0);
min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity); 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) Attack.prototype.GetAttackYOrigin = function(type)
@@ -813,14 +847,9 @@ Attack.prototype.PerformAttack = function(type, target)
*/ */
Attack.prototype.IsTargetInRange = function(target, type) Attack.prototype.IsTargetInRange = function(target, type)
{ {
const range = this.GetRange(type); const range = this.GetEffectiveAttackRange(target, type);
return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetParabolicRange( return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).IsInTargetRange(
this.entity, this.entity, target, range.min, range.max, false);
target,
range.min,
range.max,
this.GetAttackYOrigin(type),
false);
}; };
Attack.prototype.OnValueModification = function(msg) Attack.prototype.OnValueModification = function(msg)
@@ -5167,24 +5167,24 @@ UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
if (cmpFormation) if (cmpFormation)
target = cmpFormation.GetClosestMemberToEntity(this.entity); target = cmpFormation.GetClosestMemberToEntity(this.entity);
if (type != "Ranged")
return this.MoveToTargetRange(target, IID_Attack, type);
if (!this.CheckTargetVisible(target)) if (!this.CheckTargetVisible(target))
return false; return false;
const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack) if (!cmpAttack)
return false; 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 flatRange = cmpAttack.GetRange(type);
const parabolicMaxRange = Math.max(0, Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEffectiveParabolicRange(this.entity, target, range.max, cmpAttack.GetAttackYOrigin(type))); const effectiveRange = cmpAttack.GetEffectiveAttackRange(target, type);
if (effectiveRange.max < 0)
return false;
// The parabole changes while walking so be cautious: // The parabola changes while walking so be cautious:
const guessedMaxRange = parabolicMaxRange > range.max ? (range.max + parabolicMaxRange) / 2 : parabolicMaxRange; 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) UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
@@ -201,7 +201,7 @@ attackComponentTest(undefined, true, (attacker, cmpAttack, defender) =>
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["Civilian"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["Civilian"]);
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]); 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("Capture"), { "Capture": 8 });
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), { TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), {