From 27c232847463083b7b28a88cef2fbf3a8f089963 Mon Sep 17 00:00:00 2001 From: Atrik Date: Thu, 28 May 2026 16:27:45 +0200 Subject: [PATCH] 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. --- .../simulation/components/BuildingAI.js | 36 ++++--- .../public/simulation/components/UnitAI.js | 93 +++++++++++-------- .../components/CCmpRangeManager.cpp | 91 ++++++++++++++---- .../simulation2/components/ICmpRangeManager.h | 25 +++-- .../components/tests/test_RangeManager.h | 32 ++++--- 5 files changed, 182 insertions(+), 95 deletions(-) diff --git a/binaries/data/mods/public/simulation/components/BuildingAI.js b/binaries/data/mods/public/simulation/components/BuildingAI.js index 929bb83feb..8a2a7a594c 100644 --- a/binaries/data/mods/public/simulation/components/BuildingAI.js +++ b/binaries/data/mods/public/simulation/components/BuildingAI.js @@ -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); diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index f6dc26c6dd..2811b284e9 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -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,44 +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; - } - // In other stances, don't make the range parabolic, since they use vision/approach ranges - // that are larger than attack range, so targets will be spotted as they chase/approach anyway. - // TODO: With large height differences, the effective parabolic attack range can exceed - // vision/approach ranges, causing targets to be spotted later than ideal. - else if (this.GetStance().respondChase) - 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; }; diff --git a/source/simulation2/components/CCmpRangeManager.cpp b/source/simulation2/components/CCmpRangeManager.cpp index 6df271f087..fc113d00c1 100644 --- a/source/simulation2/components/CCmpRangeManager.cpp +++ b/source/simulation2/components/CCmpRangeManager.cpp @@ -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 includeMirages; // Include mirage entities regardless of interface checks }; /** @@ -320,6 +322,7 @@ struct SerializeHelper { 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 serialize.Bool("enabled", value.enabled); serialize.Bool("parabolic",value.parabolic); serialize.Bool("account for size",value.accountForSize); + serialize.Bool("includeMirages", value.includeMirages); } 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& owners, int requiredInterface, u8 flags, bool accountForSize) override + const std::vector& owners, int requiredInterface, u8 flags, + bool accountForSize, bool includeMirages) 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, includeMirages); return id; } tag_t CreateActiveParabolicQuery(entity_id_t source, - entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t yOrigin, - const std::vector& 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& owners, int requiredInterface, u8 flags, bool includeMirages = 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, includeMirages); 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 cmpMirage(GetSimContext(), id); + bool isMirage = !!cmpMirage; + + // If it's a mirage and we're not including mirages, skip it + if (isMirage && !q.includeMirages) return false; + // If it's not a mirage, check interface normally + if (!isMirage && q.interface && !GetSimContext().GetComponentManager().QueryInterface(id, q.interface)) + return false; + + // Skip hidden entities (not visible to source player) + if (q.source.GetId() != INVALID_ENTITY) + { + // Look up the source's current owner + EntityMap::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 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 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& owners, int requiredInterface, u8 flagsMask, bool accountForSize) const + const std::vector& owners, int requiredInterface, u8 flagsMask, + bool accountForSize, bool includeMirages = 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 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.includeMirages = includeMirages; 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& 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& owners, int requiredInterface, u8 flagsMask, + bool accountForSize, bool includeMirages = false) const { - Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flagsMask, accountForSize); + Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flagsMask, accountForSize, includeMirages); q.parabolic = true; q.yOrigin = yOrigin; + q.baseRange = baseRange; return q; } diff --git a/source/simulation2/components/ICmpRangeManager.h b/source/simulation2/components/ICmpRangeManager.h index 8ee4e0e1db..e4d0881371 100644 --- a/source/simulation2/components/ICmpRangeManager.h +++ b/source/simulation2/components/ICmpRangeManager.h @@ -161,29 +161,40 @@ 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 includeMirages if true, mirage entities are included in results without + * interface checks (needed for targeting fogged enemies). Default false. + * When false (default), mirages are excluded entirely. * @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& 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& owners, int requiredInterface, u8 flags, + bool accountForSize, bool includeMirages = 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 includeMirages if true, mirage entities are included in results without + * interface checks (needed for targeting fogged enemies). Default false. + * When false (default), mirages are excluded entirely. * 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& 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& owners, int requiredInterface, u8 flags, bool includeMirages = false) = 0; /** * Get the effective range in a parablic range query. diff --git a/source/simulation2/components/tests/test_RangeManager.h b/source/simulation2/components/tests/test_RangeManager.h index 557b33abfe..de32f4c6b7 100644 --- a/source/simulation2/components/tests/test_RangeManager.h +++ b/source/simulation2/components/tests/test_RangeManager.h @@ -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,41 +254,42 @@ public: move(100, position, fixed::FromInt(10), fixed::FromInt(10)); move(101, position2, fixed::FromInt(10), fixed::FromInt(20)); - std::vector 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 nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {-1}, 0, true); TS_ASSERT_EQUALS(nearby, std::vector{}); - 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{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{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{}); 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{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{}); 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{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{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{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{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{}); - 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{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{}); }