make repair ships standby with rest of fleet if there is no one to repair (instead of advancing towards the enemy stations)
This commit is contained in:
@@ -6,6 +6,7 @@ enum class BehaviorKind
|
||||
{
|
||||
None,
|
||||
Advance,
|
||||
Standby,
|
||||
Rally,
|
||||
Retreat,
|
||||
Attack,
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
// Score bands for ship-behavior evaluation. The AiSystem selection pass picks
|
||||
// the behavior with the highest score per ship; these constants define a single
|
||||
// comparable scale so the desired priority falls out:
|
||||
// Retreat > Attack > Repair / Salvage / Deliver > Rally > Advance.
|
||||
// Retreat > Attack > Repair / Salvage / Deliver > Rally > Standby > Advance.
|
||||
// Evaluators may return kInactive when their behavior does not apply this tick.
|
||||
namespace BehaviorScores
|
||||
{
|
||||
constexpr float kInactive = 0.0f;
|
||||
constexpr float kAdvance = 0.05f; // baseline fallback; always present
|
||||
constexpr float kStandby = 0.10f; // repair-capable ships; hold with the fleet
|
||||
constexpr float kRally = 0.20f;
|
||||
constexpr float kDeliver = 0.50f; // cargo full
|
||||
constexpr float kRepair = 0.55f;
|
||||
|
||||
@@ -23,6 +23,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBehaviorComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SensorRangeComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipIdentityComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/StandbyBehavior.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/StationBodyComponent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WeaponComponent.h
|
||||
PARENT_SCOPE
|
||||
|
||||
11
src/lib/ecs/component/StandbyBehavior.h
Normal file
11
src/lib/ecs/component/StandbyBehavior.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
// Fallback for ships with a repair capability: instead of charging the enemy
|
||||
// like AdvanceBehavior, the ship holds with its fleet so damaged allies stay in
|
||||
// sensor range and it can heal them. Scored just above Advance and below Rally,
|
||||
// so it only wins when no more urgent behavior applies. The executor decides the
|
||||
// destination (StandbyExecutor).
|
||||
struct StandbyBehavior
|
||||
{
|
||||
float score = 0.0f;
|
||||
};
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "RetreatBehavior.h"
|
||||
#include "SalvageScrapBehavior.h"
|
||||
#include "SelectedBehaviorComponent.h"
|
||||
#include "StandbyBehavior.h"
|
||||
#include "tracing.h"
|
||||
|
||||
namespace
|
||||
@@ -48,6 +49,7 @@ void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
|
||||
|
||||
// Phase 1: evaluators score behaviors and set their target data.
|
||||
m_advanceEvaluator.evaluate(admin);
|
||||
m_standbyEvaluator.evaluate(admin);
|
||||
m_rallyEvaluator.evaluate(admin);
|
||||
m_retreatEvaluator.evaluate(admin);
|
||||
m_attackEvaluator.evaluate(admin);
|
||||
@@ -60,6 +62,7 @@ void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
|
||||
|
||||
// Phase 3: executors run for the winning behavior.
|
||||
m_advanceExecutor.execute(admin);
|
||||
m_standbyExecutor.execute(admin);
|
||||
m_rallyExecutor.execute(admin);
|
||||
m_retreatExecutor.execute(admin);
|
||||
m_attackExecutor.execute(admin);
|
||||
@@ -85,5 +88,6 @@ void AiSystem::selectWinningBehaviors(EntityAdmin& admin)
|
||||
consider<SalvageScrapBehavior>(admin, BehaviorKind::SalvageScrap);
|
||||
consider<DeliverScrapBehavior>(admin, BehaviorKind::DeliverScrap);
|
||||
consider<RallyBehavior>(admin, BehaviorKind::Rally);
|
||||
consider<StandbyBehavior>(admin, BehaviorKind::Standby);
|
||||
consider<AdvanceBehavior>(admin, BehaviorKind::Advance);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "RetreatExecutor.h"
|
||||
#include "SalvageScrapEvaluator.h"
|
||||
#include "SalvageScrapExecutor.h"
|
||||
#include "StandbyEvaluator.h"
|
||||
#include "StandbyExecutor.h"
|
||||
|
||||
class BuildingSystem;
|
||||
class EntityAdmin;
|
||||
@@ -38,6 +40,7 @@ private:
|
||||
void selectWinningBehaviors(EntityAdmin& admin);
|
||||
|
||||
AdvanceEvaluator m_advanceEvaluator;
|
||||
StandbyEvaluator m_standbyEvaluator;
|
||||
RallyEvaluator m_rallyEvaluator;
|
||||
RetreatEvaluator m_retreatEvaluator;
|
||||
AttackEvaluator m_attackEvaluator;
|
||||
@@ -46,6 +49,7 @@ private:
|
||||
DeliverScrapEvaluator m_deliverScrapEvaluator;
|
||||
|
||||
AdvanceExecutor m_advanceExecutor;
|
||||
StandbyExecutor m_standbyExecutor;
|
||||
RallyExecutor m_rallyExecutor;
|
||||
RetreatExecutor m_retreatExecutor;
|
||||
AttackExecutor m_attackExecutor;
|
||||
|
||||
@@ -15,6 +15,8 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RetreatExecutor.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/SalvageScrapEvaluator.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/SalvageScrapExecutor.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/StandbyEvaluator.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/StandbyExecutor.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.h
|
||||
@@ -43,6 +45,8 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RetreatExecutor.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/SalvageScrapEvaluator.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/SalvageScrapExecutor.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/StandbyEvaluator.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/StandbyExecutor.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.cpp
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "SalvageScrapBehavior.h"
|
||||
#include "SelectedBehaviorComponent.h"
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "StandbyBehavior.h"
|
||||
#include "Tick.h"
|
||||
#include "tracing.h"
|
||||
#include "WeaponComponent.h"
|
||||
@@ -401,6 +402,11 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
repair.orbitRadius_tiles =
|
||||
maxRepairRange * static_cast<float>(m_config.world.orbitFactor);
|
||||
m_admin.addComponent<RepairBehavior>(entity, repair);
|
||||
|
||||
// Repair-capable ships hold with the fleet (REQ-SHP-STANDBY) instead of
|
||||
// charging the enemy when no more urgent behavior applies; this applies
|
||||
// whether or not the ship also carries weapons.
|
||||
m_admin.addComponent<StandbyBehavior>(entity, StandbyBehavior{});
|
||||
}
|
||||
|
||||
return entity;
|
||||
|
||||
16
src/lib/ecs/system/ai/StandbyEvaluator.cpp
Normal file
16
src/lib/ecs/system/ai/StandbyEvaluator.cpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#include "StandbyEvaluator.h"
|
||||
|
||||
#include "BehaviorScores.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "StandbyBehavior.h"
|
||||
#include "tracing.h"
|
||||
|
||||
void StandbyEvaluator::evaluate(EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
admin.forEach<StandbyBehavior>(
|
||||
[](entt::entity /*e*/, StandbyBehavior& standby)
|
||||
{
|
||||
standby.score = BehaviorScores::kStandby;
|
||||
});
|
||||
}
|
||||
12
src/lib/ecs/system/ai/StandbyEvaluator.h
Normal file
12
src/lib/ecs/system/ai/StandbyEvaluator.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
class EntityAdmin;
|
||||
|
||||
// Constant low-priority fallback for repair-capable ships: gives a fixed score
|
||||
// just above Advance so a repair ship with nothing more urgent to do holds with
|
||||
// its fleet (StandbyExecutor) instead of charging the enemy.
|
||||
class StandbyEvaluator
|
||||
{
|
||||
public:
|
||||
void evaluate(EntityAdmin& admin);
|
||||
};
|
||||
101
src/lib/ecs/system/ai/StandbyExecutor.cpp
Normal file
101
src/lib/ecs/system/ai/StandbyExecutor.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#include "StandbyExecutor.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "BehaviorKind.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "MovementIntentComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "SelectedBehaviorComponent.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "StandbyBehavior.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<QVector2D> value() const
|
||||
{
|
||||
if (count == 0) { return std::nullopt; }
|
||||
return sum / static_cast<float>(count);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void StandbyExecutor::execute(EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
|
||||
// Centroid of each faction's alive ships; a standing-by ship steers toward the
|
||||
// center of its other same-faction ships so it stays among potential patients.
|
||||
Centroid enemyShips;
|
||||
Centroid playerShips;
|
||||
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent, HealthComponent>(
|
||||
[&enemyShips, &playerShips](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
||||
const PositionComponent& pos, const FactionComponent& faction,
|
||||
const HealthComponent& health)
|
||||
{
|
||||
if (health.hp <= 0.0f) { return; }
|
||||
Centroid& centroid = faction.isEnemy ? enemyShips : playerShips;
|
||||
centroid.add(pos.value);
|
||||
});
|
||||
|
||||
// Fallback per faction: the centroid of that side's own alive 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);
|
||||
});
|
||||
|
||||
const std::optional<QVector2D> enemyStationCenter = enemyStations.value();
|
||||
const std::optional<QVector2D> playerStationCenter = playerStations.value();
|
||||
|
||||
admin.forEach<StandbyBehavior, SelectedBehaviorComponent, PositionComponent,
|
||||
FactionComponent, MovementIntentComponent>(
|
||||
[&](entt::entity /*e*/, const StandbyBehavior& /*standby*/,
|
||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
||||
const FactionComponent& faction, MovementIntentComponent& intent)
|
||||
{
|
||||
if (selected.winner != BehaviorKind::Standby) { return; }
|
||||
|
||||
const Centroid& allyShips = faction.isEnemy ? enemyShips : playerShips;
|
||||
const std::optional<QVector2D>& friendlyStationCenter =
|
||||
faction.isEnemy ? enemyStationCenter : playerStationCenter;
|
||||
|
||||
// Aim at the centroid of the other allied ships (excluding self), then
|
||||
// fall back to the friendly stations, then hold position when alone.
|
||||
QVector2D target = pos.value;
|
||||
if (allyShips.count > 1)
|
||||
{
|
||||
target = (allyShips.sum - pos.value)
|
||||
/ static_cast<float>(allyShips.count - 1);
|
||||
}
|
||||
else if (friendlyStationCenter)
|
||||
{
|
||||
target = *friendlyStationCenter;
|
||||
}
|
||||
intent = MovementIntentComponent{true, target};
|
||||
});
|
||||
}
|
||||
12
src/lib/ecs/system/ai/StandbyExecutor.h
Normal file
12
src/lib/ecs/system/ai/StandbyExecutor.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
class EntityAdmin;
|
||||
|
||||
// Moves a standing-by ship toward the centroid of its other same-faction ships
|
||||
// (its fleet) so it stays among allies that may need repair, falling back to its
|
||||
// own defence stations and then to holding position when it has no allies.
|
||||
class StandbyExecutor
|
||||
{
|
||||
public:
|
||||
void execute(EntityAdmin& admin);
|
||||
};
|
||||
@@ -563,6 +563,33 @@ TEST_CASE("BehaviorSystem: repair ship orbits damaged friendly ship",
|
||||
REQUIRE(intent(f.admin, repairShip).orbitRadius_tiles == Approx(orbitRadius));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// StandbyBehavior (repair ships hold with the fleet when idle)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("BehaviorSystem: idle repair ship stands by with the fleet instead of charging the enemy",
|
||||
"[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
// A healthy ally (nothing to repair) and a far enemy station (no threat in range).
|
||||
const entt::entity ally = f.ships.spawn("interceptor", 1, QVector2D(50.0f, 0.0f));
|
||||
const std::vector<QPoint> body{QPoint(0, 0)};
|
||||
f.admin.spawnStation(QPoint(1000, 0), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
|
||||
// With no damaged ally and no enemy in sensor range the repair ship neither
|
||||
// repairs nor retreats: it stands by (REQ-SHP-STANDBY), steering toward its
|
||||
// fleet (the ally) rather than charging the distant enemy station.
|
||||
REQUIRE(winnerOf(f.admin, repairShip) == BehaviorKind::Standby);
|
||||
REQUIRE(intent(f.admin, repairShip).active);
|
||||
REQUIRE(intent(f.admin, repairShip).target.x() == Approx(pos(f.admin, ally).value.x()));
|
||||
REQUIRE(intent(f.admin, repairShip).target.y() == Approx(pos(f.admin, ally).value.y()));
|
||||
}
|
||||
|
||||
TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
|
||||
"[behavior]")
|
||||
{
|
||||
@@ -1114,8 +1141,11 @@ TEST_CASE("SensorRange: repair ship does not retreat from enemy beyond sensor ra
|
||||
|
||||
f.decide();
|
||||
|
||||
// Beyond sensor range the enemy is no threat, so the repair ship does not flee;
|
||||
// with nothing to repair it holds with the fleet (REQ-SHP-STANDBY) rather than
|
||||
// retreating or charging the enemy.
|
||||
REQUIRE(winnerOf(f.admin, repairShip) != BehaviorKind::Retreat);
|
||||
REQUIRE(intent(f.admin, repairShip).target.x() > pos(f.admin, repairShip).value.x());
|
||||
REQUIRE(winnerOf(f.admin, repairShip) == BehaviorKind::Standby);
|
||||
}
|
||||
|
||||
TEST_CASE("SensorRange: repair ship does not acquire damaged ally beyond sensor range", "[sensor]")
|
||||
|
||||
Reference in New Issue
Block a user