Introduce GetEffectiveAttackRange in Attack

To centralize the parabolic vs flat range decision
for a given attack type.
This replaces direct calls to GetEffectiveParabolicRange in UnitAI
and simplifies IsTargetInRange, MoveToTargetAttackRange, and CanAttack.
This commit is contained in:
Atrik 2026-05-01 14:42:14 +02:00
parent 41a68acc4c
commit fbad79241b
3 changed files with 49 additions and 20 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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"), {