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:
2026-06-18 21:43:55 +02:00
parent abab2bbb6e
commit c371b43a6d
14 changed files with 208 additions and 4 deletions

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View 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;
});
}

View 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);
};

View 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};
});
}

View 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);
};