refactor AI system

This commit is contained in:
2026-06-15 09:16:56 +02:00
parent 8451f5a281
commit e8dd73bcb0
67 changed files with 1731 additions and 919 deletions

View File

@@ -1,587 +1,82 @@
#include "AiSystem.h"
#include <optional>
#include <unordered_map>
#include <vector>
#include <limits>
#include <QVector2D>
#include "Building.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "BuildingId.h"
#include "AdvanceBehavior.h"
#include "AttackBehavior.h"
#include "BehaviorKind.h"
#include "DeliverScrapBehavior.h"
#include "EntityAdmin.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "HomeReturnBehaviorComponent.h"
#include "HqProxyComponent.h"
#include "ModuleOwnerComponent.h"
#include "MovementIntentComponent.h"
#include "PositionComponent.h"
#include "RallyBehaviorComponent.h"
#include "RepairBehaviorComponent.h"
#include "RepairToolComponent.h"
#include "SalvageBehaviorComponent.h"
#include "SalvageCargoComponent.h"
#include "ScrapSystem.h"
#include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h"
#include "StationBodyComponent.h"
#include "ThreatResponseBehaviorComponent.h"
#include "RallyBehavior.h"
#include "RepairBehavior.h"
#include "RetreatBehavior.h"
#include "SalvageScrapBehavior.h"
#include "SelectedBehaviorComponent.h"
#include "tracing.h"
// ---------------------------------------------------------------------------
// Shared helpers for repair targeting
// ---------------------------------------------------------------------------
struct RepairableInfo
namespace
{
entt::entity entity;
QVector2D position;
bool isEnemy;
bool isShip;
float hp;
float maxHp;
};
static std::vector<RepairableInfo> buildRepairables(EntityAdmin& admin)
{
std::vector<RepairableInfo> repairables;
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent, HealthComponent>(
[&repairables](entt::entity e, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FactionComponent& f,
const HealthComponent& h)
{
repairables.push_back({e, pos.value, f.isEnemy, true, h.hp, h.maxHp});
});
admin.forEach<StationBodyComponent, PositionComponent, FactionComponent, HealthComponent>(
[&repairables](entt::entity e, const StationBodyComponent& /*sb*/,
const PositionComponent& pos, const FactionComponent& f,
const HealthComponent& h)
{
repairables.push_back({e, pos.value, f.isEnemy, false, h.hp, h.maxHp});
});
return repairables;
}
// ---------------------------------------------------------------------------
// tickHomeReturnBehavior (priority 4)
// ---------------------------------------------------------------------------
void AiSystem::tickHomeReturnBehavior(EntityAdmin& admin)
{
TRACE();
admin.forEach<HomeReturnBehaviorComponent, HealthComponent, MovementIntentComponent>(
[](entt::entity /*e*/, const HomeReturnBehaviorComponent& homeReturnBehavior,
const HealthComponent& h, MovementIntentComponent& intent)
{
if (h.hp / h.maxHp < homeReturnBehavior.retreatHpFraction)
{
if (4 > intent.priority)
{
intent = MovementIntentComponent{4, homeReturnBehavior.homePos};
}
}
});
}
// ---------------------------------------------------------------------------
// tickThreatResponseBehavior (priority 3)
// ---------------------------------------------------------------------------
void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSystem& buildings)
{
TRACE();
// Snapshot all combatant entities for target acquisition.
struct CombatantInfo
// Records a behavior's score for its owning ship, keeping the highest seen.
// Considered high-priority first, so strict '>' breaks ties toward priority.
template <typename Behavior>
void consider(EntityAdmin& admin, BehaviorKind kind)
{
entt::entity entity;
QVector2D position;
bool isEnemy;
bool isStation;
};
std::vector<CombatantInfo> combatants;
admin.forEach<PositionComponent, FactionComponent, ShipIdentityComponent>(
[&combatants](entt::entity e, const PositionComponent& pos,
const FactionComponent& f, const ShipIdentityComponent& /*si*/)
{
combatants.push_back({e, pos.value, f.isEnemy, false});
});
admin.forEach<PositionComponent, FactionComponent, StationBodyComponent>(
[&combatants](entt::entity e, const PositionComponent& pos,
const FactionComponent& f, const StationBodyComponent& /*sb*/)
{
combatants.push_back({e, pos.value, f.isEnemy, true});
});
admin.forEach<PositionComponent, FactionComponent, HqProxyComponent>(
[&combatants](entt::entity e, const PositionComponent& pos,
const FactionComponent& f, const HqProxyComponent& /*hq*/)
{
combatants.push_back({e, pos.value, f.isEnemy, true});
});
admin.forEach<ThreatResponseBehaviorComponent, PositionComponent, FactionComponent,
SensorRangeComponent, MovementIntentComponent>(
[&](entt::entity e, ThreatResponseBehaviorComponent& threatResponseBehavior,
PositionComponent& pos, FactionComponent& faction,
SensorRangeComponent& sensor, MovementIntentComponent& intent)
{
const float range = sensor.value_tiles;
// Validate current target.
bool targetValid = false;
if (threatResponseBehavior.currentTarget)
admin.forEach<Behavior, SelectedBehaviorComponent>(
[kind](entt::entity /*e*/, const Behavior& behavior,
SelectedBehaviorComponent& selected)
{
const entt::entity t = *threatResponseBehavior.currentTarget;
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
if (behavior.score > selected.bestScore)
{
const float dist =
(admin.get<PositionComponent>(t).value - pos.value).length();
if (dist <= range)
{
targetValid = true;
}
selected.bestScore = behavior.score;
selected.winner = kind;
}
}
if (!targetValid)
{
threatResponseBehavior.currentTarget = std::nullopt;
float bestDist = range;
for (const CombatantInfo& c : combatants)
{
if (c.entity == e) { continue; }
bool isValidTarget = false;
if (!faction.isEnemy)
{
isValidTarget = c.isEnemy;
}
else
{
isValidTarget = !c.isEnemy;
}
if (!isValidTarget) { continue; }
const float dist = (c.position - pos.value).length();
if (dist < bestDist)
{
bestDist = dist;
threatResponseBehavior.currentTarget = c.entity;
}
}
}
if (threatResponseBehavior.currentTarget)
{
const entt::entity t = *threatResponseBehavior.currentTarget;
QVector2D dest = pos.value;
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
{
dest = admin.get<PositionComponent>(t).value;
}
if (3 > intent.priority)
{
intent = MovementIntentComponent{3, dest};
}
}
else
{
if (3 > intent.priority)
{
if (admin.hasAll<RallyBehaviorComponent>(e))
{
intent = MovementIntentComponent{
3, admin.get<RallyBehaviorComponent>(e).rallyPoint};
}
else if (!faction.isEnemy)
{
intent = MovementIntentComponent{
3, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
}
else
{
intent = MovementIntentComponent{
3, QVector2D(-10000.0f, pos.value.y())};
}
}
}
});
});
}
}
// ---------------------------------------------------------------------------
// tickRepairBehavior (priority 2)
// ---------------------------------------------------------------------------
void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
const ScrapSystem& scraps)
{
TRACE();
std::vector<RepairableInfo> repairables = buildRepairables(admin);
TRACE();
// Snapshot enemy ships for threat detection.
struct EnemyInfo
{
QVector2D position;
};
std::vector<EnemyInfo> enemies;
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
[&enemies](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FactionComponent& f)
{
if (f.isEnemy)
{
enemies.push_back({pos.value});
}
});
// Phase 1: evaluators score behaviors and set their target data.
m_advanceEvaluator.evaluate(admin);
m_rallyEvaluator.evaluate(admin);
m_retreatEvaluator.evaluate(admin);
m_attackEvaluator.evaluate(admin);
m_repairEvaluator.evaluate(admin);
m_salvageScrapEvaluator.evaluate(admin, scraps);
m_deliverScrapEvaluator.evaluate(admin, buildings);
admin.forEach<RepairBehaviorComponent, PositionComponent,
FactionComponent, SensorRangeComponent, MovementIntentComponent>(
[&](entt::entity e, RepairBehaviorComponent& rb,
PositionComponent& pos, FactionComponent& /*faction*/,
SensorRangeComponent& sensor, MovementIntentComponent& intent)
{
// Flee if enemy nearby.
bool enemyNearby = false;
for (const EnemyInfo& enemy : enemies)
{
if ((enemy.position - pos.value).length() <= sensor.value_tiles)
{
enemyNearby = true;
break;
}
}
if (enemyNearby)
{
if (2 > intent.priority)
{
intent = MovementIntentComponent{
2, QVector2D(-10000.0f, pos.value.y())};
}
return;
}
// Phase 2: pick the highest-scoring behavior per ship.
selectWinningBehaviors(admin);
// Validate current target.
bool targetValid = false;
if (rb.currentTarget)
{
const entt::entity t = *rb.currentTarget;
if (admin.isValid(t) && admin.hasAll<HealthComponent>(t))
{
const HealthComponent& th = admin.get<HealthComponent>(t);
if (th.hp > 0.0f && th.hp < th.maxHp)
{
targetValid = true;
}
}
}
if (!targetValid)
{
rb.currentTarget = std::nullopt;
float bestDist = sensor.value_tiles;
for (const RepairableInfo& r : repairables)
{
if (r.entity == e) { continue; }
if (r.isEnemy) { continue; }
if (r.hp >= r.maxHp) { continue; }
const float dist = (r.position - pos.value).length();
if (dist < bestDist)
{
bestDist = dist;
rb.currentTarget = r.entity;
}
}
}
if (!rb.currentTarget)
{
if (2 > intent.priority)
{
intent = MovementIntentComponent{
2, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
}
return;
}
const entt::entity target = *rb.currentTarget;
QVector2D targetPos = pos.value;
if (admin.isValid(target) && admin.hasAll<PositionComponent>(target))
{
targetPos = admin.get<PositionComponent>(target).value;
}
if (2 > intent.priority)
{
intent = MovementIntentComponent{2, targetPos};
}
});
// Phase 3: executors run for the winning behavior.
m_advanceExecutor.execute(admin);
m_rallyExecutor.execute(admin);
m_retreatExecutor.execute(admin);
m_attackExecutor.execute(admin);
m_repairExecutor.execute(admin);
m_salvageScrapExecutor.execute(admin);
m_deliverScrapExecutor.execute(admin, buildings);
}
// ---------------------------------------------------------------------------
// tickRepairTools
// ---------------------------------------------------------------------------
void AiSystem::tickRepairTools(EntityAdmin& admin)
void AiSystem::selectWinningBehaviors(EntityAdmin& admin)
{
TRACE();
const std::vector<RepairableInfo> repairables = buildRepairables(admin);
admin.forEach<RepairToolComponent, ModuleOwnerComponent>(
[&](entt::entity /*e*/, RepairToolComponent& rt, const ModuleOwnerComponent& owner)
TRACE();
admin.forEach<SelectedBehaviorComponent>(
[](entt::entity /*e*/, SelectedBehaviorComponent& selected)
{
if (!admin.hasAll<RepairBehaviorComponent>(owner.owner)) { return; }
const RepairBehaviorComponent& rb =
admin.get<RepairBehaviorComponent>(owner.owner);
const PositionComponent& ownerPos =
admin.get<PositionComponent>(owner.owner);
// Try the ship's preferred nav target first.
if (rb.currentTarget)
{
const entt::entity preferred = *rb.currentTarget;
if (admin.isValid(preferred) && admin.hasAll<HealthComponent>(preferred)
&& admin.hasAll<PositionComponent>(preferred))
{
HealthComponent& th = admin.get<HealthComponent>(preferred);
const float dist =
(admin.get<PositionComponent>(preferred).value
- ownerPos.value).length();
if (th.hp > 0.0f && th.hp < th.maxHp && dist <= rt.range_tiles)
{
rt.currentTarget = rb.currentTarget;
th.hp = std::min(th.hp + rt.ratePerTick, th.maxHp);
return;
}
}
}
// Preferred target unavailable; scan for nearest damaged friendly in range.
rt.currentTarget = std::nullopt;
float bestDist = rt.range_tiles;
for (const RepairableInfo& r : repairables)
{
if (r.isEnemy) { continue; }
if (r.hp <= 0.0f || r.hp >= r.maxHp) { continue; }
const float dist = (r.position - ownerPos.value).length();
if (dist < bestDist)
{
bestDist = dist;
rt.currentTarget = r.entity;
}
}
if (!rt.currentTarget) { return; }
HealthComponent& targetHealth =
admin.get<HealthComponent>(*rt.currentTarget);
targetHealth.hp = std::min(targetHealth.hp + rt.ratePerTick, targetHealth.maxHp);
});
}
// ---------------------------------------------------------------------------
// tickSalvageBehavior (priority 1)
// ---------------------------------------------------------------------------
void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
BuildingSystem& buildings)
{
TRACE();
// Snapshot enemy ships for threat detection.
struct EnemyShipPos
{
QVector2D position;
};
std::vector<EnemyShipPos> enemyShips;
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
[&enemyShips](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FactionComponent& f)
{
if (f.isEnemy)
{
enemyShips.push_back({pos.value});
}
});
// Aggregate cargo across all salvage-module children per owning ship.
struct AggregatedCargo
{
int totalCurrent = 0;
int totalCapacity = 0;
};
std::unordered_map<entt::entity, AggregatedCargo> cargoByShip;
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
[&](entt::entity /*ce*/, const SalvageCargoComponent& c, const ModuleOwnerComponent& o)
{
AggregatedCargo& agg = cargoByShip[o.owner];
agg.totalCurrent += c.current;
agg.totalCapacity += c.capacity;
});
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
// Tick down per-module collection cooldowns.
admin.forEach<SalvageCargoComponent>(
[](entt::entity /*e*/, SalvageCargoComponent& c)
{
if (c.cooldownTicksRemaining > 0) { --c.cooldownTicksRemaining; }
});
admin.forEach<SalvageBehaviorComponent, PositionComponent,
SensorRangeComponent, MovementIntentComponent>(
[&](entt::entity e, SalvageBehaviorComponent& salvageBehavior,
PositionComponent& pos,
SensorRangeComponent& sensor, MovementIntentComponent& intent)
{
const float collectRange = salvageBehavior.maxCollectionRange_tiles;
const AggregatedCargo& cargoState = cargoByShip[e];
// Assign nearest SalvageBay if needed.
if (salvageBehavior.deliveryBay == kInvalidBuildingId)
{
const Building* bay = buildings.findNearestBuilding(pos.value,
BuildingType::SalvageBay);
if (bay)
{
salvageBehavior.deliveryBay = bay->id;
}
}
const BuildingId bayId = salvageBehavior.deliveryBay;
QVector2D bayPos = pos.value;
if (bayId != kInvalidBuildingId)
{
const Building* bay = buildings.findBuilding(bayId);
if (bay)
{
bayPos = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,
bay->anchor.y() + bay->footprint.height() / 2.0f);
}
}
const bool cargoFull = (cargoState.totalCurrent >= cargoState.totalCapacity
&& cargoState.totalCapacity > 0);
if (cargoFull)
{
if (1 > intent.priority)
{
intent = MovementIntentComponent{1, bayPos};
}
if (bayId != kInvalidBuildingId
&& (pos.value - bayPos).length() <= 1.0f)
{
// Decrement first non-empty salvage child.
bool delivered = false;
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
[&](entt::entity /*ce*/, SalvageCargoComponent& c,
const ModuleOwnerComponent& o)
{
if (delivered || o.owner != e || c.current <= 0) { return; }
if (buildings.deliverScrapToSalvageBay(bayId))
{
--c.current;
delivered = true;
}
});
}
return;
}
// Retreat if enemy near and cargo empty.
bool retreating = false;
if (cargoState.totalCurrent == 0)
{
for (const EnemyShipPos& enemy : enemyShips)
{
if ((enemy.position - pos.value).length() <= collectRange)
{
if (1 > intent.priority)
{
intent = MovementIntentComponent{
1, QVector2D(-10000.0f, pos.value.y())};
}
retreating = true;
break;
}
}
}
if (retreating) { return; }
// Per-module independent collection: each ready module collects one scrap.
bool anythingCollected = false;
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
[&](entt::entity /*ce*/, SalvageCargoComponent& c,
const ModuleOwnerComponent& o)
{
if (o.owner != e || c.current >= c.capacity
|| c.cooldownTicksRemaining > 0)
{
return;
}
for (const ScrapInfo& si : allScrap)
{
if ((si.position - pos.value).length() > c.collectionRange_tiles) { continue; }
if (scraps.consume(si.entity))
{
++c.current;
c.cooldownTicksRemaining = c.collectionIntervalTicks;
anythingCollected = true;
break;
}
}
});
if (anythingCollected)
{
salvageBehavior.scrapTarget = std::nullopt;
}
// Move toward scrap target or find a new one.
if (salvageBehavior.scrapTarget)
{
if (1 > intent.priority)
{
intent = MovementIntentComponent{1, *salvageBehavior.scrapTarget};
}
}
else
{
float bestDist = sensor.value_tiles;
std::optional<QVector2D> bestPos;
for (const ScrapInfo& si : allScrap)
{
const float dist = (si.position - pos.value).length();
if (dist < bestDist)
{
bestDist = dist;
bestPos = si.position;
}
}
if (bestPos)
{
salvageBehavior.scrapTarget = bestPos;
if (1 > intent.priority)
{
intent = MovementIntentComponent{1, *bestPos};
}
}
else
{
if (1 > intent.priority)
{
intent = MovementIntentComponent{
1, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
}
}
}
selected.winner = BehaviorKind::None;
selected.bestScore = std::numeric_limits<float>::lowest();
});
// Highest priority first so ties resolve toward the more urgent behavior.
consider<RetreatBehavior>(admin, BehaviorKind::Retreat);
consider<AttackBehavior>(admin, BehaviorKind::Attack);
consider<RepairBehavior>(admin, BehaviorKind::Repair);
consider<SalvageScrapBehavior>(admin, BehaviorKind::SalvageScrap);
consider<DeliverScrapBehavior>(admin, BehaviorKind::DeliverScrap);
consider<RallyBehavior>(admin, BehaviorKind::Rally);
consider<AdvanceBehavior>(admin, BehaviorKind::Advance);
}