Enable attacks on foes visible through shared LOS

Adds baseRange parameter to parabolic queries for combined
2D + parabolic detection. StandGround and Chase stances now
use attack range (parabolic) with vision as baseRange, allowing
units to attack enemies visible through friendly vision.
Buildings benefit as well via BuildingAI.
This commit is contained in:
Atrik 2026-05-28 16:27:45 +02:00 committed by Vantha
parent ceca608848
commit 38b33b0484
5 changed files with 184 additions and 92 deletions

View file

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

View file

@ -4248,16 +4248,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);
@ -5594,8 +5605,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)
@ -6637,14 +6656,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)
@ -6657,41 +6677,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;
};

View file

@ -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.
};
/**
@ -349,6 +351,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);
@ -357,6 +360,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&)
@ -992,21 +996,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;
}
@ -1297,10 +1300,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;
}
@ -1324,13 +1352,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)
{
@ -1340,6 +1373,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;
@ -1357,7 +1404,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);
@ -1554,7 +1601,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())
@ -1565,6 +1613,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;
@ -1573,6 +1623,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)
{
@ -1609,12 +1660,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;
}

View file

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

View file

@ -243,8 +243,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);
@ -254,41 +255,42 @@ 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>{});
}