mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
Compare commits
3 commits
1976b3a842
...
ec712463b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec712463b7 | ||
|
|
8922a056b0 | ||
|
|
daf6be2ac7 |
6 changed files with 294 additions and 93 deletions
|
|
@ -286,6 +286,15 @@ Attack.prototype.CanAttack = function(target, wantedTypes)
|
|||
if (!cmpTargetPlayer || !cmpEntityPlayer)
|
||||
return false;
|
||||
|
||||
// Must be visible or miraged / with retainInFog flag, not completely hidden
|
||||
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
||||
if (cmpRangeManager)
|
||||
{
|
||||
const visibility = cmpRangeManager.GetLosVisibility(target, cmpEntityPlayer.GetPlayerID());
|
||||
if (visibility == "hidden")
|
||||
return false;
|
||||
}
|
||||
|
||||
const types = this.GetAttackTypes(wantedTypes);
|
||||
const entityOwner = cmpEntityPlayer.GetPlayerID();
|
||||
const targetOwner = cmpTargetPlayer.GetPlayerID();
|
||||
|
|
|
|||
|
|
@ -127,10 +127,20 @@ BuildingAI.prototype.SetupRangeQuery = function()
|
|||
|
||||
const range = cmpAttack.GetRange(attackType);
|
||||
const yOrigin = cmpAttack.GetAttackYOrigin(attackType);
|
||||
|
||||
// Get building's vision range
|
||||
const cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
|
||||
const visionRange = cmpVision ? cmpVision.GetRange() : 0;
|
||||
|
||||
// Base range
|
||||
const baseRange = Math.min(visionRange, range.max);
|
||||
|
||||
// This takes entity sizes into accounts, so no need to compensate for structure size.
|
||||
this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
|
||||
this.entity, range.min, range.max, yOrigin,
|
||||
enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"));
|
||||
this.entity, range.min, range.max, baseRange, yOrigin,
|
||||
enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"),
|
||||
true // Allow mirages for attack queries
|
||||
);
|
||||
|
||||
cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
|
||||
};
|
||||
|
|
@ -156,10 +166,17 @@ BuildingAI.prototype.SetupGaiaRangeQuery = function()
|
|||
const range = cmpAttack.GetRange(attackType);
|
||||
const yOrigin = cmpAttack.GetAttackYOrigin(attackType);
|
||||
|
||||
// Get building's vision range
|
||||
const cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
|
||||
const visionRange = cmpVision ? cmpVision.GetRange() : 0;
|
||||
|
||||
// Base range
|
||||
const baseRange = Math.min(visionRange, range.max);
|
||||
|
||||
// This query is only interested in Gaia entities that can attack.
|
||||
// This takes entity sizes into accounts, so no need to compensate for structure size.
|
||||
this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
|
||||
this.entity, range.min, range.max, yOrigin,
|
||||
this.entity, range.min, range.max, baseRange, yOrigin,
|
||||
[0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal"));
|
||||
|
||||
cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery);
|
||||
|
|
@ -170,7 +187,6 @@ BuildingAI.prototype.SetupGaiaRangeQuery = function()
|
|||
*/
|
||||
BuildingAI.prototype.OnRangeUpdate = function(msg)
|
||||
{
|
||||
|
||||
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
if (!cmpAttack)
|
||||
return;
|
||||
|
|
@ -189,10 +205,10 @@ BuildingAI.prototype.OnRangeUpdate = function(msg)
|
|||
|
||||
// Add new targets.
|
||||
for (const entity of msg.added)
|
||||
if (cmpAttack.CanAttack(entity))
|
||||
if (!this.targetUnits.includes(entity))
|
||||
this.targetUnits.push(entity);
|
||||
|
||||
// Remove targets outside of vision-range.
|
||||
// Remove targets out of range.
|
||||
for (const entity of msg.removed)
|
||||
{
|
||||
const index = this.targetUnits.indexOf(entity);
|
||||
|
|
@ -375,13 +391,7 @@ BuildingAI.prototype.FireArrows = function()
|
|||
{
|
||||
|
||||
const selectedTarget = targets[targetIndex].entityId;
|
||||
if (this.CheckTargetVisible(selectedTarget) && cmpObstructionManager.IsInTargetParabolicRange(
|
||||
this.entity,
|
||||
selectedTarget,
|
||||
range.min,
|
||||
range.max,
|
||||
yOrigin,
|
||||
false))
|
||||
if (cmpAttack.CanAttack(selectedTarget, [attackType]))
|
||||
{
|
||||
cmpAttack.PerformAttack(attackType, selectedTarget);
|
||||
PlaySound("attack_" + attackType.toLowerCase(), this.entity);
|
||||
|
|
|
|||
|
|
@ -4052,16 +4052,27 @@ UnitAI.prototype.SetupAttackRangeQuery = function(enable = true)
|
|||
{
|
||||
const cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
|
||||
const yOrigin = cmpAttack ? cmpAttack.GetAttackYOrigin("Ranged") : 0;
|
||||
|
||||
// Do not compensate for entity sizes: LOS doesn't, and UnitAI relies on that.
|
||||
this.losAttackRangeQuery = cmpRangeManager.CreateActiveParabolicQuery(this.entity,
|
||||
range.min, range.max, yOrigin,
|
||||
players, IID_Resistance,
|
||||
cmpRangeManager.GetEntityFlagMask("normal"));
|
||||
this.losAttackRangeQuery = cmpRangeManager.CreateActiveParabolicQuery(
|
||||
this.entity,
|
||||
range.min,
|
||||
range.max,
|
||||
range.base,
|
||||
yOrigin,
|
||||
players,
|
||||
IID_Resistance,
|
||||
cmpRangeManager.GetEntityFlagMask("normal"),
|
||||
true // Allow mirages for attack queries
|
||||
);
|
||||
}
|
||||
else
|
||||
this.losAttackRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity,
|
||||
range.min, range.max, players, IID_Resistance,
|
||||
cmpRangeManager.GetEntityFlagMask("normal"), false);
|
||||
cmpRangeManager.GetEntityFlagMask("normal"),
|
||||
false,
|
||||
true // Allow mirages for attack queries
|
||||
);
|
||||
|
||||
if (enable)
|
||||
cmpRangeManager.EnableActiveQuery(this.losAttackRangeQuery);
|
||||
|
|
@ -5382,8 +5393,16 @@ UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
|
|||
if (!this.AbleToMove())
|
||||
return false;
|
||||
|
||||
// Check if we should chase based on stance
|
||||
if (this.GetStance().respondChase)
|
||||
return true;
|
||||
{
|
||||
// If we're allowed to chase beyond vision, always chase
|
||||
if (this.GetStance().respondChaseBeyondVision)
|
||||
return true;
|
||||
|
||||
// Otherwise, only chase if the target is within our personal vision
|
||||
return this.CheckTargetIsInVisionRange(target);
|
||||
}
|
||||
|
||||
// If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker
|
||||
if (this.isGuardOf)
|
||||
|
|
@ -6356,14 +6375,15 @@ UnitAI.prototype.FindWalkAndFightTargets = function()
|
|||
* the unit should "notice" an enemy and potentially start moving toward it.
|
||||
*
|
||||
* @param {number} iid - IID_Vision, IID_Heal, or IID_Attack
|
||||
* @returns {{min: number, max: number, parabolic: boolean}}
|
||||
* @returns {{min: number, max: number, base: number, parabolic: boolean}}
|
||||
* 'parabolic' indicates that the caller
|
||||
* should use a parabolic range query (accounting for elevation) instead of a
|
||||
* flat 2D one. Generally used for projectile attacks.
|
||||
* 'base' is a non-parabolic 2D detection range that always counts as in-range.
|
||||
*/
|
||||
UnitAI.prototype.GetQueryRange = function(iid)
|
||||
{
|
||||
const ret = { "min": 0, "max": 0, "parabolic": false };
|
||||
const ret = { "min": 0, "max": 0, "base": 0, "parabolic": false };
|
||||
|
||||
const cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
|
||||
if (!cmpVision)
|
||||
|
|
@ -6376,41 +6396,35 @@ UnitAI.prototype.GetQueryRange = function(iid)
|
|||
return ret;
|
||||
}
|
||||
|
||||
const range = this.GetRange(iid);
|
||||
if (!range)
|
||||
return ret;
|
||||
|
||||
// The query range depends on stance because it represents the distance at which
|
||||
// the unit should "notice" an enemy and potentially start moving toward it:
|
||||
if (this.GetStance().respondStandGround)
|
||||
{
|
||||
// StandGround: flat attack range only (won't move at all)
|
||||
const range = this.GetRange(iid);
|
||||
if (!range)
|
||||
return ret;
|
||||
ret.min = range.min;
|
||||
ret.max = Math.min(range.max, visionRange);
|
||||
// For StandGround, the 'parabolic' flag is set so that the caller can create a
|
||||
// parabolic query instead of a flat one. This ensures elevation bonuses are
|
||||
// properly accounted for when detecting enemies.
|
||||
// Without this, a unit on a hill could be in parabolic range of an enemy
|
||||
// but never notice them because the flat 2D detection circle is smaller.
|
||||
// Other stances don't need this since they'll chase/approach anyway, and
|
||||
// the attack validation (IsTargetInRange) does a precise parabolic check
|
||||
// before actually attacking.
|
||||
ret.parabolic = range.parabolic;
|
||||
}
|
||||
else if (this.GetStance().respondChase)
|
||||
// Chase stances: use full vision range (unit will chase anything it sees)
|
||||
ret.max = visionRange;
|
||||
// the unit should "notice" an enemy and potentially start moving toward it.
|
||||
|
||||
// In all stances, always spot targets within effective attack/heal range.
|
||||
Object.assign(ret, range);
|
||||
|
||||
let nonParabolicMax = 0;
|
||||
if (this.GetStance().respondChase)
|
||||
// Chase: Always spot targets within vision range, so we can chase them.
|
||||
nonParabolicMax = visionRange;
|
||||
else if (this.GetStance().respondHoldGround)
|
||||
{
|
||||
// HoldGround: vision range + half attack range (willing to move a bit)
|
||||
const range = this.GetRange(iid);
|
||||
if (!range)
|
||||
return ret;
|
||||
ret.max = Math.min(range.max + visionRange / 2, visionRange);
|
||||
}
|
||||
// HoldGround: willing to move a bit, so spot targets within attack range + half vision.
|
||||
nonParabolicMax = Math.min(range.max + visionRange / 2, visionRange);
|
||||
|
||||
// StandGround: nonParabolicMax stays 0, using only parabolic range.
|
||||
|
||||
// We probably have stance 'passive' and we wouldn't have a range,
|
||||
// but as it is the default for healers we need to set it to something sane.
|
||||
else if (iid === IID_Heal)
|
||||
ret.max = visionRange;
|
||||
nonParabolicMax = visionRange;
|
||||
|
||||
if (ret.parabolic)
|
||||
ret.base = nonParabolicMax;
|
||||
else
|
||||
ret.max = nonParabolicMax;
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ struct Query
|
|||
CEntityHandle source; // TODO: this could crash if an entity is destroyed while a Query is still referencing it
|
||||
entity_pos_t minRange;
|
||||
entity_pos_t maxRange;
|
||||
entity_pos_t baseRange; // Non-parabolic detection range
|
||||
entity_pos_t yOrigin; // Used for parabolas only.
|
||||
u32 ownersMask;
|
||||
i32 interface;
|
||||
|
|
@ -195,6 +196,7 @@ struct Query
|
|||
bool enabled;
|
||||
bool parabolic;
|
||||
bool accountForSize; // If true, the query accounts for unit sizes, otherwise it treats all entities as points.
|
||||
bool preferMirages; // If true, include mirages and filter HIDDEN entities. Otherwise exclude mirages and no visibility filter.
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -320,6 +322,7 @@ struct SerializeHelper<Query>
|
|||
{
|
||||
serialize.NumberFixed_Unbounded("min range", value.minRange);
|
||||
serialize.NumberFixed_Unbounded("max range", value.maxRange);
|
||||
serialize.NumberFixed_Unbounded("baseRange", value.baseRange);
|
||||
serialize.NumberFixed_Unbounded("yOrigin", value.yOrigin);
|
||||
serialize.NumberU32_Unbounded("owners mask", value.ownersMask);
|
||||
serialize.NumberI32_Unbounded("interface", value.interface);
|
||||
|
|
@ -328,6 +331,7 @@ struct SerializeHelper<Query>
|
|||
serialize.Bool("enabled", value.enabled);
|
||||
serialize.Bool("parabolic",value.parabolic);
|
||||
serialize.Bool("account for size",value.accountForSize);
|
||||
serialize.Bool("preferMirages", value.preferMirages);
|
||||
}
|
||||
|
||||
void operator()(ISerializer& serialize, const char* name, Query& value, const CSimContext&)
|
||||
|
|
@ -963,21 +967,20 @@ public:
|
|||
|
||||
tag_t CreateActiveQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags, bool accountForSize) override
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags,
|
||||
bool accountForSize, bool preferMirages) override
|
||||
{
|
||||
tag_t id = m_QueryNext++;
|
||||
m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags, accountForSize);
|
||||
|
||||
m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags, accountForSize, preferMirages);
|
||||
return id;
|
||||
}
|
||||
|
||||
tag_t CreateActiveParabolicQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags) override
|
||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t baseRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags, bool preferMirages = false) override
|
||||
{
|
||||
tag_t id = m_QueryNext++;
|
||||
m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, yOrigin, owners, requiredInterface, flags, true);
|
||||
|
||||
m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, baseRange, yOrigin, owners, requiredInterface, flags, true, preferMirages);
|
||||
return id;
|
||||
}
|
||||
|
||||
|
|
@ -1268,10 +1271,35 @@ public:
|
|||
if (id == q.source.GetId())
|
||||
return false;
|
||||
|
||||
// Ignore if it's missing the required interface
|
||||
if (q.interface && !GetSimContext().GetComponentManager().QueryInterface(id, q.interface))
|
||||
// Check if this is a mirage entity
|
||||
CmpPtr<ICmpMirage> cmpMirage(GetSimContext(), id);
|
||||
bool isMirage = !!cmpMirage;
|
||||
|
||||
// If it's a mirage and we're not including mirages, skip it
|
||||
if (isMirage && !q.preferMirages)
|
||||
return false;
|
||||
|
||||
// If it's not a mirage, check interface normally
|
||||
if (!isMirage && q.interface && !GetSimContext().GetComponentManager().QueryInterface(id, q.interface))
|
||||
return false;
|
||||
|
||||
// Filter hidden entities when we want mirages (i.e., we care about visibility)
|
||||
if (q.preferMirages && q.source.GetId() != INVALID_ENTITY)
|
||||
{
|
||||
// Look up the source's current owner
|
||||
EntityMap<EntityData>::const_iterator itSource = m_EntityData.find(q.source.GetId());
|
||||
if (itSource != m_EntityData.end())
|
||||
{
|
||||
player_id_t sourceOwner = itSource->second.owner;
|
||||
if (sourceOwner != INVALID_PLAYER)
|
||||
{
|
||||
LosVisibility vis = GetPlayerVisibility(entity.visibilities, sourceOwner);
|
||||
if (vis == LosVisibility::HIDDEN)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1295,13 +1323,18 @@ public:
|
|||
// Not the entire world, so check a parabolic range, or a regular range.
|
||||
else if (q.parabolic)
|
||||
{
|
||||
// The yOrigin is part of the 3D position, as the source is really that much heigher.
|
||||
// The yOrigin is part of the 3D position, as the source is really that much higher.
|
||||
CmpPtr<ICmpPosition> cmpSourcePosition(q.source);
|
||||
CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+
|
||||
CFixedVector3D(entity_pos_t::Zero(), q.yOrigin, entity_pos_t::Zero()) ;
|
||||
// Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange.
|
||||
CFixedVector3D pos3d = cmpSourcePosition->GetPosition() +
|
||||
CFixedVector3D(entity_pos_t::Zero(), q.yOrigin, entity_pos_t::Zero());
|
||||
// Get a quick list of entities that are potentially in range.
|
||||
// For parabolic queries, the search radius must cover:
|
||||
// 1. The baseRange circle (non-parabolic detection)
|
||||
// 2. The maximum possible horizontal extent of the parabolic range
|
||||
// Multiplying maxRange by 2 provides a safe upper bound for all possible height differences.
|
||||
entity_pos_t subdivisionRange = std::max(q.baseRange, q.maxRange * 2);
|
||||
subdivisionResultsBuffer.clear();
|
||||
m_Subdivision.GetNear(subdivisionResultsBuffer, pos, q.maxRange * 2);
|
||||
m_Subdivision.GetNear(subdivisionResultsBuffer, pos, subdivisionRange);
|
||||
|
||||
for (size_t i = 0; i < subdivisionResultsBuffer.size(); ++i)
|
||||
{
|
||||
|
|
@ -1311,6 +1344,20 @@ public:
|
|||
if (!TestEntityQuery(q, it->first, it->second))
|
||||
continue;
|
||||
|
||||
CFixedVector2D delta2D = CFixedVector2D(it->second.x, it->second.z) - pos;
|
||||
|
||||
// Check base range first
|
||||
bool inBaseRange = !q.baseRange.IsZero() && delta2D.CompareLength(q.baseRange) <= 0;
|
||||
|
||||
if (inBaseRange)
|
||||
{
|
||||
// In base range - no need for parabolic check
|
||||
if (q.minRange.IsZero() || delta2D.CompareLength(q.minRange) >= 0)
|
||||
r.push_back(it->first);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parabolic check for entities outside base range
|
||||
CmpPtr<ICmpPosition> cmpSecondPosition(GetSimContext(), subdivisionResultsBuffer[i]);
|
||||
if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld())
|
||||
continue;
|
||||
|
|
@ -1328,7 +1375,7 @@ public:
|
|||
continue;
|
||||
|
||||
if (!q.minRange.IsZero())
|
||||
if ((CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange) < 0)
|
||||
if (delta2D.CompareLength(q.minRange) < 0)
|
||||
continue;
|
||||
|
||||
r.push_back(it->first);
|
||||
|
|
@ -1525,7 +1572,8 @@ public:
|
|||
|
||||
Query ConstructQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flagsMask, bool accountForSize) const
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flagsMask,
|
||||
bool accountForSize, bool preferMirages = false) const
|
||||
{
|
||||
// Min range must be non-negative.
|
||||
if (minRange < entity_pos_t::Zero())
|
||||
|
|
@ -1536,6 +1584,8 @@ public:
|
|||
if (maxRange < entity_pos_t::Zero() && maxRange != ALWAYS_IN_RANGE)
|
||||
LOGWARNING("CCmpRangeManager: Invalid max range %f in query for entity %u", maxRange.ToDouble(), source);
|
||||
|
||||
CmpPtr<ICmpOwnership> cmpOwnership(GetSimContext(), source);
|
||||
|
||||
Query q;
|
||||
q.enabled = false;
|
||||
q.parabolic = false;
|
||||
|
|
@ -1544,6 +1594,7 @@ public:
|
|||
q.maxRange = maxRange;
|
||||
q.yOrigin = entity_pos_t::Zero();
|
||||
q.accountForSize = accountForSize;
|
||||
q.preferMirages = preferMirages;
|
||||
|
||||
if (q.accountForSize && q.source.GetId() != INVALID_ENTITY && q.maxRange != ALWAYS_IN_RANGE)
|
||||
{
|
||||
|
|
@ -1580,12 +1631,14 @@ public:
|
|||
}
|
||||
|
||||
Query ConstructParabolicQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flagsMask, bool accountForSize) const
|
||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t baseRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flagsMask,
|
||||
bool accountForSize, bool preferMirages = false) const
|
||||
{
|
||||
Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flagsMask, accountForSize);
|
||||
Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flagsMask, accountForSize, preferMirages);
|
||||
q.parabolic = true;
|
||||
q.yOrigin = yOrigin;
|
||||
q.baseRange = baseRange;
|
||||
return q;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -161,29 +161,42 @@ public:
|
|||
* @param requiredInterface if non-zero, an interface ID that matching entities must implement.
|
||||
* @param flags if a entity in range has one of the flags set it will show up.
|
||||
* @param accountForSize if true, compensate for source/target entity sizes.
|
||||
* @param preferMirages if true, mirage entities are included (bypassing interface checks)
|
||||
* and HIDDEN entities are filtered out (needed for targeting fogged enemies).
|
||||
* When false (default), mirages are excluded entirely and real entities
|
||||
* are returned regardless of visibility (even if hidden).
|
||||
* @return unique non-zero identifier of query.
|
||||
*/
|
||||
virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags, bool accountForSize) = 0;
|
||||
virtual tag_t CreateActiveQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags,
|
||||
bool accountForSize, bool preferMirages = false) = 0;
|
||||
|
||||
/**
|
||||
* Construct an active query of a paraboloic form around the unit.
|
||||
/**
|
||||
* Construct an active query of a parabolic form around the unit.
|
||||
* The query will be disabled by default.
|
||||
* @param source the entity around which the range will be computed.
|
||||
* @param minRange non-negative minimum horizontal distance in metres (inclusive). MinRange doesn't do parabolic checks.
|
||||
* @param maxRange non-negative maximum distance in metres (inclusive) for units on the same elevation;
|
||||
* or -1.0 to ignore distance.
|
||||
* For units on a different height positions, a physical correct paraboloid with height=maxRange/2 above the unit is used to query them
|
||||
* @param baseRange non-negative base detection range in metres (inclusive) for simple 2D circle checks.
|
||||
* Units within this horizontal distance are always considered in range regardless of height.
|
||||
* Set to 0 to disable (original parabolic-only behavior).
|
||||
* @param yOrigin extra bonus so the source can be placed higher and shoot further
|
||||
* @param owners list of player IDs that matching entities may have; -1 matches entities with no owner.
|
||||
* @param requiredInterface if non-zero, an interface ID that matching entities must implement.
|
||||
* @param flags if a entity in range has one of the flags set it will show up.
|
||||
* @param preferMirages if true, mirage entities are included (bypassing interface checks)
|
||||
* and HIDDEN entities are filtered out (needed for targeting fogged enemies).
|
||||
* When false (default), mirages are excluded entirely and real entities
|
||||
* are returned regardless of visibility (even if hidden).
|
||||
* NB: this one has no accountForSize parameter (assumed true), because we currently can only have 7 arguments for JS functions.
|
||||
* @return unique non-zero identifier of query.
|
||||
*/
|
||||
virtual tag_t CreateActiveParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags) = 0;
|
||||
|
||||
virtual tag_t CreateActiveParabolicQuery(entity_id_t source,
|
||||
entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t baseRange, entity_pos_t yOrigin,
|
||||
const std::vector<int>& owners, int requiredInterface, u8 flags, bool preferMirages = false) = 0;
|
||||
|
||||
/**
|
||||
* Get the effective range in a parablic range query.
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
void test_queries()
|
||||
void test_range_queries_distance_only()
|
||||
{
|
||||
ComponentTestHelper test(*g_ScriptContext);
|
||||
|
||||
|
|
@ -242,8 +242,9 @@ public:
|
|||
{ CMessageCreate msg(100); rangeManager->HandleMessage(msg, false); }
|
||||
{ CMessageCreate msg(101); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
{ CMessageOwnershipChanged msg(100, -1, 1); rangeManager->HandleMessage(msg, false); }
|
||||
{ CMessageOwnershipChanged msg(101, -1, 1); rangeManager->HandleMessage(msg, false); }
|
||||
// Don't set ownership for either entity - leave both as INVALID_PLAYER.
|
||||
// This bypasses the visibility check in TestEntityQuery, allowing us to test
|
||||
// the core distance calculation logic independently of the LOS system.
|
||||
|
||||
auto move = [&rangeManager](entity_id_t ent, MockPositionRgm& pos, fixed x, fixed z) {
|
||||
pos.m_Pos = CFixedVector3D(x, fixed::Zero(), z);
|
||||
|
|
@ -253,45 +254,146 @@ public:
|
|||
move(100, position, fixed::FromInt(10), fixed::FromInt(10));
|
||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(20));
|
||||
|
||||
std::vector<entity_id_t> nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
||||
// Query for owner -1 (INVALID_PLAYER) since both entities have no owner
|
||||
std::vector<entity_id_t> nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
|
||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(10));
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||
|
||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(13));
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||
|
||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(15));
|
||||
// In range thanks to self obstruction size.
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
// In range thanks to target obstruction size.
|
||||
nearby = rangeManager->ExecuteQuery(101, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(101, fixed::FromInt(0), fixed::FromInt(4), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{100});
|
||||
|
||||
// Trickier: min-range is closest-to-closest, but rotation may change the real distance.
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(2), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(2), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(5), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(6), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||
nearby = rangeManager->ExecuteQuery(101, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(101, fixed::FromInt(5), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{100});
|
||||
nearby = rangeManager->ExecuteQuery(101, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true);
|
||||
nearby = rangeManager->ExecuteQuery(101, fixed::FromInt(6), fixed::FromInt(50), {-1}, 0, true);
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
||||
|
||||
}
|
||||
|
||||
void test_range_queries_visibility_filtering()
|
||||
{
|
||||
ComponentTestHelper test(*g_ScriptContext);
|
||||
|
||||
ICmpRangeManager* rangeManager = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
||||
|
||||
MockVisionRgm vision, vision2;
|
||||
MockPositionRgm position, position2;
|
||||
MockObstructionRgm obs(fixed::FromInt(2)), obs2(fixed::Zero());
|
||||
test.AddMock(100, IID_Vision, vision);
|
||||
test.AddMock(100, IID_Position, position);
|
||||
test.AddMock(100, IID_Obstruction, obs);
|
||||
|
||||
test.AddMock(101, IID_Vision, vision2);
|
||||
test.AddMock(101, IID_Position, position2);
|
||||
test.AddMock(101, IID_Obstruction, obs2);
|
||||
|
||||
rangeManager->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512));
|
||||
rangeManager->Verify();
|
||||
{ CMessageCreate msg(100); rangeManager->HandleMessage(msg, false); }
|
||||
{ CMessageCreate msg(101); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
// Set ownership for both entities so they have proper owners
|
||||
{ CMessageOwnershipChanged msg(100, -1, 1); rangeManager->HandleMessage(msg, false); }
|
||||
{ CMessageOwnershipChanged msg(101, -1, 1); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
auto move = [&rangeManager](entity_id_t ent, MockPositionRgm& pos, fixed x, fixed z) {
|
||||
pos.m_Pos = CFixedVector3D(x, fixed::Zero(), z);
|
||||
{ CMessagePositionChanged msg(ent, true, x, z, entity_angle_t::Zero()); rangeManager->HandleMessage(msg, false); }
|
||||
};
|
||||
|
||||
move(100, position, fixed::FromInt(10), fixed::FromInt(10));
|
||||
move(101, position2, fixed::FromInt(10), fixed::FromInt(15));
|
||||
|
||||
std::vector<int> owners;
|
||||
owners.push_back(1);
|
||||
|
||||
// Test 1: With preferMirages = true, we should get the mirage entity when visible
|
||||
ICmpRangeManager::tag_t query = rangeManager->CreateActiveQuery(
|
||||
100, // source
|
||||
fixed::FromInt(0), // minRange
|
||||
fixed::FromInt(50), // maxRange
|
||||
owners, // owners
|
||||
0, // requiredInterface
|
||||
rangeManager->GetEntityFlagMask("normal"),
|
||||
true, // accountForSize
|
||||
true // preferMirages = true
|
||||
);
|
||||
rangeManager->EnableActiveQuery(query);
|
||||
|
||||
// With reveal map enabled, entity should be visible
|
||||
rangeManager->SetLosRevealWholeMap(1, true);
|
||||
{ CMessageUpdate msg(fixed::FromInt(1)); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
std::vector<entity_id_t> nearby = rangeManager->ResetActiveQuery(query);
|
||||
// Should return either the real entity (101) or a mirage.
|
||||
// For this test, we just verify something is returned.
|
||||
TS_ASSERT_EQUALS(nearby.size(), 1);
|
||||
|
||||
// Disable reveal map - entity should become hidden
|
||||
rangeManager->SetLosRevealWholeMap(1, false);
|
||||
{ CMessageUpdate msg(fixed::FromInt(1)); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
nearby = rangeManager->ResetActiveQuery(query);
|
||||
// Should return empty (no visible entities)
|
||||
TS_ASSERT_EQUALS(nearby.size(), 0);
|
||||
|
||||
// Re-enable reveal map
|
||||
rangeManager->SetLosRevealWholeMap(1, true);
|
||||
{ CMessageUpdate msg(fixed::FromInt(1)); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
nearby = rangeManager->ResetActiveQuery(query);
|
||||
TS_ASSERT_EQUALS(nearby.size(), 1);
|
||||
|
||||
// Test 2: With preferMirages = false (default), visibility filtering is disabled
|
||||
ICmpRangeManager::tag_t query2 = rangeManager->CreateActiveQuery(
|
||||
100, // source
|
||||
fixed::FromInt(0), // minRange
|
||||
fixed::FromInt(50), // maxRange
|
||||
owners, // owners
|
||||
0, // requiredInterface
|
||||
rangeManager->GetEntityFlagMask("normal"),
|
||||
true, // accountForSize
|
||||
false // preferMirages = false
|
||||
);
|
||||
rangeManager->EnableActiveQuery(query2);
|
||||
|
||||
// With preferMirages = false, entity should be returned even when hidden
|
||||
rangeManager->SetLosRevealWholeMap(1, false);
|
||||
{ CMessageUpdate msg(fixed::FromInt(1)); rangeManager->HandleMessage(msg, false); }
|
||||
|
||||
nearby = rangeManager->ResetActiveQuery(query2);
|
||||
// Should return the real entity (101) even though hidden
|
||||
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
||||
|
||||
// Clean up
|
||||
rangeManager->DestroyActiveQuery(query);
|
||||
rangeManager->DestroyActiveQuery(query2);
|
||||
}
|
||||
|
||||
void test_ParabolicRangeBasic()
|
||||
{
|
||||
ComponentTestHelper test(*g_ScriptContext);
|
||||
|
|
|
|||
Loading…
Reference in a new issue