advance towards enemy buildings

This commit is contained in:
2026-06-15 20:52:43 +02:00
parent e8dd73bcb0
commit 6b7c3df64a
2 changed files with 141 additions and 6 deletions

View File

@@ -1,30 +1,112 @@
#include "AdvanceExecutor.h" #include "AdvanceExecutor.h"
#include <optional>
#include <QVector2D> #include <QVector2D>
#include "AdvanceBehavior.h" #include "AdvanceBehavior.h"
#include "BehaviorKind.h" #include "BehaviorKind.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "FactionComponent.h" #include "FactionComponent.h"
#include "HealthComponent.h"
#include "HqProxyComponent.h"
#include "MovementIntentComponent.h" #include "MovementIntentComponent.h"
#include "PositionComponent.h" #include "PositionComponent.h"
#include "SelectedBehaviorComponent.h" #include "SelectedBehaviorComponent.h"
#include "StationBodyComponent.h"
#include "tracing.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<QVector2D> value() const
{
if (count == 0) { return std::nullopt; }
return sum / static_cast<float>(count);
}
};
}
void AdvanceExecutor::execute(EntityAdmin& admin) void AdvanceExecutor::execute(EntityAdmin& admin)
{ {
TRACE(); 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<StationBodyComponent, PositionComponent, FactionComponent, HealthComponent>(
[&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<HqProxyComponent, PositionComponent, FactionComponent, HealthComponent>(
[&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<QVector2D> enemyStationCenter = enemyStations.value();
const std::optional<QVector2D> playerStationCenter = playerStations.value();
const std::optional<QVector2D> enemyHqCenter = enemyHq.value();
const std::optional<QVector2D> playerHqCenter = playerHq.value();
admin.forEach<AdvanceBehavior, SelectedBehaviorComponent, PositionComponent, admin.forEach<AdvanceBehavior, SelectedBehaviorComponent, PositionComponent,
FactionComponent, MovementIntentComponent>( FactionComponent, MovementIntentComponent>(
[](entt::entity /*e*/, const AdvanceBehavior& /*advance*/, [&](entt::entity /*e*/, const AdvanceBehavior& /*advance*/,
const SelectedBehaviorComponent& selected, const PositionComponent& pos, const SelectedBehaviorComponent& selected, const PositionComponent& pos,
const FactionComponent& faction, MovementIntentComponent& intent) const FactionComponent& faction, MovementIntentComponent& intent)
{ {
if (selected.winner != BehaviorKind::Advance) { return; } if (selected.winner != BehaviorKind::Advance) { return; }
const QVector2D target = faction.isEnemy // Aim at the center between the opposing side's defence stations; fall
? QVector2D(-10000.0f, pos.value.y()) // back to the opposing HQ, then to an off-world point in the advance
: QVector2D(pos.value.x() + 1000.0f, pos.value.y()); // direction so the ship keeps moving when no target structure exists.
const std::optional<QVector2D>& stationCenter =
faction.isEnemy ? playerStationCenter : enemyStationCenter;
const std::optional<QVector2D>& 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}; intent = MovementIntentComponent{true, target};
}); });
} }

View File

@@ -3,6 +3,7 @@
#include <random> #include <random>
#include <QPoint> #include <QPoint>
#include <QSize>
#include <QVector2D> #include <QVector2D>
#include "AdvanceBehavior.h" #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); 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<QPoint> 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<HealthComponent>(hq).hp = 0.0f;
f.decide();
REQUIRE(winnerOf(f.admin, enemy) == BehaviorKind::Advance);
REQUIRE(intent(f.admin, enemy).target.x() < 0.0f);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// RepairBehavior // RepairBehavior
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------