Compare commits

..

4 commits

Author SHA1 Message Date
Atrik
2abb7337fd Add test for queries with visibility checks 2026-06-15 01:34:53 +02:00
Atrik
5f63f9e497 Enable attacks on foes visible through shared LOS
Adds a baseRange parameter to parabolic queries providing simple
2D detection alongside parabolic detection.
StandGround and Chase stances now use full attack parabolic range
with vision range as baseRange, allowing units to attack enemies
visible through friendly vision.
2026-06-15 01:20:19 +02:00
Atrik
402de88f25 Filter out hidden targets in CanAttack
Units should not be able to attack entities with "hidden" visibility.
Visible and fogged (mirage/retainInFog) targets remain attackable.
2026-06-15 01:20:19 +02:00
Atrik
626c91e02b Add tests for CanEverReachTarget and its helpers 2026-06-15 01:20:19 +02:00
2 changed files with 86 additions and 19 deletions

View file

@ -313,12 +313,12 @@ TS_ASSERT(cmpTurretHolder.OccupiesTurretPoint(spawned));
TS_ASSERT_EQUALS(cmpHolderPassable.GetClosestApproachDistanceToTurretPoint("center"), 0);
// No obstruction component at all
const cmpHolderNoObst = ConstructComponent(++entityID, "TurretHolder", {
++entityID;
const cmpHolderNoObst = ConstructComponent(entityID, "TurretHolder", {
"TurretPoints": {
"center": { "X": "0", "Y": "5.0", "Z": "0" }
}
});
const result = cmpHolderNoObst.GetClosestApproachDistanceToTurretPoint("center");
TS_ASSERT_EQUALS(result, 0);
TS_ASSERT_EQUALS(cmpHolderNoObst.GetClosestApproachDistanceToTurretPoint("center"), 0);
}

View file

@ -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,111 @@ 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));
// Note: Full LOS testing (vision range, terrain, fog) requires a full game world
// with terrain, water, and proper pathfinding. That's beyond the scope of this
// unit test. The visibility test here verifies that ExecuteQuery respects the
// reveal whole map flag, which exercises the visibility check path in TestEntityQuery.
// Enable "reveal whole map" to force all entities to be visible
rangeManager->SetLosRevealWholeMap(1, true);
// Process an update
{ CMessageUpdate msg(fixed::FromInt(1)); rangeManager->HandleMessage(msg, false); }
// Entity 101 should be visible (due to reveal map) and in range
std::vector<entity_id_t> nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(50), {1}, 0, true);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
// Disable "reveal whole map" to test hidden entities
rangeManager->SetLosRevealWholeMap(1, false);
{ CMessageUpdate msg(fixed::FromInt(1)); rangeManager->HandleMessage(msg, false); }
// Entity 101 should now be hidden because LOS isn't properly set up
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(50), {1}, 0, true);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
// Re-enable reveal map to show it works again
rangeManager->SetLosRevealWholeMap(1, true);
{ CMessageUpdate msg(fixed::FromInt(1)); rangeManager->HandleMessage(msg, false); }
nearby = rangeManager->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(50), {1}, 0, true);
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
}
void test_ParabolicRangeBasic()
{
ComponentTestHelper test(*g_ScriptContext);