From 6b7c3df64ae7f8f182a995a311792c20fdb6a0a9 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Mon, 15 Jun 2026 20:52:43 +0200 Subject: [PATCH] advance towards enemy buildings --- src/lib/ecs/system/ai/AdvanceExecutor.cpp | 94 +++++++++++++++++++++-- src/test/BehaviorSystemTest.cpp | 53 +++++++++++++ 2 files changed, 141 insertions(+), 6 deletions(-) diff --git a/src/lib/ecs/system/ai/AdvanceExecutor.cpp b/src/lib/ecs/system/ai/AdvanceExecutor.cpp index 7a86bf9..0c6095b 100644 --- a/src/lib/ecs/system/ai/AdvanceExecutor.cpp +++ b/src/lib/ecs/system/ai/AdvanceExecutor.cpp @@ -1,30 +1,112 @@ #include "AdvanceExecutor.h" +#include + #include #include "AdvanceBehavior.h" #include "BehaviorKind.h" #include "EntityAdmin.h" #include "FactionComponent.h" +#include "HealthComponent.h" +#include "HqProxyComponent.h" #include "MovementIntentComponent.h" #include "PositionComponent.h" #include "SelectedBehaviorComponent.h" +#include "StationBodyComponent.h" #include "tracing.h" +namespace +{ +// Accumulates positions to produce their centroid (the center between them). +struct Centroid +{ + QVector2D sum; + int count = 0; + + void add(const QVector2D& point) + { + sum += point; + count += 1; + } + + std::optional value() const + { + if (count == 0) { return std::nullopt; } + return sum / static_cast(count); + } +}; +} + void AdvanceExecutor::execute(EntityAdmin& admin) { TRACE(); + + // Centroid of each faction's alive defence stations. In the arena the HQ is + // spawned as a station, so it is part of this centroid; in the main game the + // enemy side has only its defence stations. + Centroid enemyStations; + Centroid playerStations; + admin.forEach( + [&enemyStations, &playerStations](entt::entity /*e*/, + const StationBodyComponent& /*sb*/, const PositionComponent& pos, + const FactionComponent& faction, const HealthComponent& health) + { + if (health.hp <= 0.0f) { return; } + Centroid& centroid = faction.isEnemy ? enemyStations : playerStations; + centroid.add(pos.value); + }); + + // Fallback target per faction: the HQ proxy (main game only), used when a side + // has lost all of its defence stations. + Centroid enemyHq; + Centroid playerHq; + admin.forEach( + [&enemyHq, &playerHq](entt::entity /*e*/, const HqProxyComponent& /*hq*/, + const PositionComponent& pos, const FactionComponent& faction, + const HealthComponent& health) + { + if (health.hp <= 0.0f) { return; } + Centroid& centroid = faction.isEnemy ? enemyHq : playerHq; + centroid.add(pos.value); + }); + + const std::optional enemyStationCenter = enemyStations.value(); + const std::optional playerStationCenter = playerStations.value(); + const std::optional enemyHqCenter = enemyHq.value(); + const std::optional playerHqCenter = playerHq.value(); + admin.forEach( - [](entt::entity /*e*/, const AdvanceBehavior& /*advance*/, - const SelectedBehaviorComponent& selected, const PositionComponent& pos, - const FactionComponent& faction, MovementIntentComponent& intent) + [&](entt::entity /*e*/, const AdvanceBehavior& /*advance*/, + const SelectedBehaviorComponent& selected, const PositionComponent& pos, + const FactionComponent& faction, MovementIntentComponent& intent) { if (selected.winner != BehaviorKind::Advance) { return; } - const QVector2D target = faction.isEnemy - ? QVector2D(-10000.0f, pos.value.y()) - : QVector2D(pos.value.x() + 1000.0f, pos.value.y()); + // Aim at the center between the opposing side's defence stations; fall + // back to the opposing HQ, then to an off-world point in the advance + // direction so the ship keeps moving when no target structure exists. + const std::optional& stationCenter = + faction.isEnemy ? playerStationCenter : enemyStationCenter; + const std::optional& hqCenter = + faction.isEnemy ? playerHqCenter : enemyHqCenter; + + QVector2D target; + if (stationCenter) + { + target = *stationCenter; + } + else if (hqCenter) + { + target = *hqCenter; + } + else + { + target = faction.isEnemy + ? QVector2D(-10000.0f, pos.value.y()) + : QVector2D(pos.value.x() + 1000.0f, pos.value.y()); + } intent = MovementIntentComponent{true, target}; }); } diff --git a/src/test/BehaviorSystemTest.cpp b/src/test/BehaviorSystemTest.cpp index 9773648..61bfa92 100644 --- a/src/test/BehaviorSystemTest.cpp +++ b/src/test/BehaviorSystemTest.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "AdvanceBehavior.h" @@ -383,6 +384,58 @@ TEST_CASE("BehaviorSystem: enemy ship with no target advances leftward", REQUIRE(intent(f.admin, enemy).target.x() < 0.0f); } +TEST_CASE("BehaviorSystem: advancing ship targets center between enemy defence stations", + "[behavior]") +{ + Fixture f; + + // Two enemy defence stations far from the ship (out of sensor/attack range), + // 1x1 footprint so each center is anchor + (0.5, 0.5). + const std::vector body{QPoint(0, 0)}; + f.admin.spawnStation(QPoint(1000, 10), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true); + f.admin.spawnStation(QPoint(1000, 30), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true); + + const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f), + /*isEnemy=*/false); + // Player ships rally until departure; drop Rally so Advance is the fallback. + f.ships.triggerRallyDeparture(); + + f.decide(); + + // Centers (1000.5, 10.5) and (1000.5, 30.5) -> midpoint (1000.5, 20.5). + REQUIRE(winnerOf(f.admin, player) == BehaviorKind::Advance); + REQUIRE(intent(f.admin, player).active); + REQUIRE(intent(f.admin, player).target.x() == Approx(1000.5f)); + REQUIRE(intent(f.admin, player).target.y() == Approx(20.5f)); +} + +TEST_CASE("BehaviorSystem: advancing ship falls back to enemy HQ, then off-world", + "[behavior]") +{ + Fixture f; + + // Player HQ proxy (isEnemy=false) but no player defence stations. + const QVector2D hqPos(5.0f, 7.0f); + const entt::entity hq = f.admin.spawnHqProxy(hqPos, 100.0f, 100.0f); + + const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(1000.0f, 0.0f), + /*isEnemy=*/true); + + f.decide(); + + REQUIRE(winnerOf(f.admin, enemy) == BehaviorKind::Advance); + REQUIRE(intent(f.admin, enemy).active); + REQUIRE(intent(f.admin, enemy).target.x() == Approx(hqPos.x())); + REQUIRE(intent(f.admin, enemy).target.y() == Approx(hqPos.y())); + + // With the HQ gone too, the ship falls back to advancing off-world (leftward). + f.admin.get(hq).hp = 0.0f; + f.decide(); + + REQUIRE(winnerOf(f.admin, enemy) == BehaviorKind::Advance); + REQUIRE(intent(f.admin, enemy).target.x() < 0.0f); +} + // --------------------------------------------------------------------------- // RepairBehavior // ---------------------------------------------------------------------------