switch to ECS architecture

This commit is contained in:
2026-05-22 20:31:39 +02:00
parent c18c4e4804
commit ca07cbaf0e
34 changed files with 1943 additions and 2074 deletions

View File

@@ -8,456 +8,440 @@
#include "Building.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "EntityId.h"
#include "MovementIntent.h"
#include "Scrap.h"
#include "ScrapSystem.h"
#include "Ship.h"
#include "ShipSystem.h"
static QVector2D buildingCenter(const Building& b)
{
return QVector2D(b.anchor.x() + b.footprint.width() / 2.0f,
b.anchor.y() + b.footprint.height() / 2.0f);
}
static bool isTargetValid(EntityId id, float range, const Ship& ship,
const ShipSystem& ships,
const BuildingSystem& buildings)
{
if (id == kInvalidEntityId) { return false; }
const Ship* target = ships.findShip(id);
if (target) { return (target->position - ship.position).length() <= range; }
const Building* bld = buildings.findBuilding(id);
if (bld) { return (buildingCenter(*bld) - ship.position).length() <= range; }
return false;
}
// ---------------------------------------------------------------------------
// tickHomeReturn (priority 4)
// ---------------------------------------------------------------------------
void AiSystem::tickHomeReturn(ShipSystem& ships)
void AiSystem::tickHomeReturn(EntityAdmin& admin)
{
ships.forEach([&](Ship& s)
{
if (!s.homeReturn) { return; }
if (s.hp / s.maxHp < s.homeReturn->retreatHpFraction)
admin.forEach<HomeReturn, Health, MovementIntent>(
[](entt::entity /*e*/, const HomeReturn& hr, const Health& h, MovementIntent& intent)
{
if (4 > s.intent.priority)
if (h.hp / h.maxHp < hr.retreatHpFraction)
{
s.intent = MovementIntent{4, s.homeReturn->homePos};
if (4 > intent.priority)
{
intent = MovementIntent{4, hr.homePos};
}
}
}
});
});
}
// ---------------------------------------------------------------------------
// tickThreatResponse (priority 3)
// ---------------------------------------------------------------------------
void AiSystem::tickThreatResponse(ShipSystem& ships, const BuildingSystem& buildings)
void AiSystem::tickThreatResponse(EntityAdmin& admin, const BuildingSystem& buildings)
{
const std::vector<Building> allBuildings = buildings.allBuildings();
const std::vector<Ship> allShips = ships.allShips();
ships.forEach([&](Ship& s)
// Snapshot all combatant entities for target acquisition.
struct CombatantInfo
{
if (!s.threatResponse) { return; }
entt::entity entity;
QVector2D position;
bool isEnemy;
bool isStation;
};
std::vector<CombatantInfo> combatants;
const float range = s.sensorRange;
if (!s.isEnemy)
admin.forEach<Position, Faction, ShipIdentity>(
[&combatants](entt::entity e, const Position& pos, const Faction& f, const ShipIdentity& /*si*/)
{
if (!isTargetValid(s.threatResponse->currentTarget.value_or(kInvalidEntityId),
range, s, ships, buildings))
combatants.push_back({e, pos.value, f.isEnemy, false});
});
admin.forEach<Position, Faction, StationBody>(
[&combatants](entt::entity e, const Position& pos, const Faction& f, const StationBody& /*sb*/)
{
combatants.push_back({e, pos.value, f.isEnemy, true});
});
admin.forEach<Position, Faction, HqProxy>(
[&combatants](entt::entity e, const Position& pos, const Faction& f, const HqProxy& /*hq*/)
{
combatants.push_back({e, pos.value, f.isEnemy, true});
});
admin.forEach<ThreatResponse, Position, Faction, SensorRange, MovementIntent>(
[&](entt::entity e, ThreatResponse& threat, Position& pos, Faction& faction,
SensorRange& sensor, MovementIntent& intent)
{
const float range = sensor.value;
// Validate current target.
bool targetValid = false;
if (threat.currentTarget)
{
s.threatResponse->currentTarget = std::nullopt;
const entt::entity t = *threat.currentTarget;
if (admin.isValid(t) && admin.hasAll<Position>(t))
{
const float dist = (admin.get<Position>(t).value - pos.value).length();
if (dist <= range)
{
targetValid = true;
}
}
}
if (!targetValid)
{
threat.currentTarget = std::nullopt;
float bestDist = range;
for (const Ship& candidate : allShips)
{
if (!candidate.isEnemy) { continue; }
float dist = (candidate.position - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = candidate.id;
}
}
for (const Building& b : allBuildings)
for (const CombatantInfo& c : combatants)
{
if (b.type != BuildingType::EnemyDefenceStation) { continue; }
float dist = (buildingCenter(b) - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = b.id;
}
}
}
if (c.entity == e) { continue; }
if (s.threatResponse->currentTarget)
{
QVector2D dest;
const Ship* tShip = ships.findShip(*s.threatResponse->currentTarget);
if (tShip)
{
dest = tShip->position;
}
else
{
const Building* tBld = buildings.findBuilding(
*s.threatResponse->currentTarget);
dest = tBld ? buildingCenter(*tBld) : s.position;
}
if (3 > s.intent.priority)
{
s.intent = MovementIntent{3, dest};
}
}
else
{
if (3 > s.intent.priority)
{
if (s.rallyBehavior)
bool isValidTarget = false;
if (!faction.isEnemy)
{
s.intent = MovementIntent{3, s.rallyBehavior->rallyPoint};
isValidTarget = c.isEnemy;
}
else
{
s.intent = MovementIntent{3, QVector2D(s.position.x() + 1000.0f,
s.position.y())};
isValidTarget = !c.isEnemy;
}
}
}
}
else
{
if (!isTargetValid(s.threatResponse->currentTarget.value_or(kInvalidEntityId),
range, s, ships, buildings))
{
s.threatResponse->currentTarget = std::nullopt;
float bestDist = range;
if (!isValidTarget) { continue; }
for (const Ship& candidate : allShips)
{
if (candidate.isEnemy) { continue; }
float dist = (candidate.position - s.position).length();
const float dist = (c.position - pos.value).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = candidate.id;
}
}
for (const Building& b : allBuildings)
{
if (b.type != BuildingType::PlayerDefenceStation
&& b.type != BuildingType::Hq)
{
continue;
}
float dist = (buildingCenter(b) - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = b.id;
threat.currentTarget = c.entity;
}
}
}
if (s.threatResponse->currentTarget)
if (threat.currentTarget)
{
QVector2D dest;
const Ship* tShip = ships.findShip(*s.threatResponse->currentTarget);
if (tShip)
const entt::entity t = *threat.currentTarget;
QVector2D dest = pos.value;
if (admin.isValid(t) && admin.hasAll<Position>(t))
{
dest = tShip->position;
dest = admin.get<Position>(t).value;
}
else
if (3 > intent.priority)
{
const Building* tBld = buildings.findBuilding(
*s.threatResponse->currentTarget);
dest = tBld ? buildingCenter(*tBld) : s.position;
}
if (3 > s.intent.priority)
{
s.intent = MovementIntent{3, dest};
intent = MovementIntent{3, dest};
}
}
else
{
if (3 > s.intent.priority)
if (3 > intent.priority)
{
s.intent = MovementIntent{3, QVector2D(-10000.0f, s.position.y())};
if (admin.hasAll<RallyBehavior>(e))
{
intent = MovementIntent{3, admin.get<RallyBehavior>(e).rallyPoint};
}
else if (!faction.isEnemy)
{
intent = MovementIntent{3, QVector2D(pos.value.x() + 1000.0f,
pos.value.y())};
}
else
{
intent = MovementIntent{3, QVector2D(-10000.0f, pos.value.y())};
}
}
}
}
});
});
}
// ---------------------------------------------------------------------------
// tickRepairBehavior (priority 2)
// ---------------------------------------------------------------------------
void AiSystem::tickRepairBehavior(ShipSystem& ships, BuildingSystem& buildings)
void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
{
const std::vector<Building> allBuildings = buildings.allBuildings();
const std::vector<Ship> allShips = ships.allShips();
ships.forEach([&](Ship& s)
// Snapshot all entities with health for repair targeting.
struct RepairableInfo
{
if (!s.repairBehavior || !s.repairTool) { return; }
entt::entity entity;
QVector2D position;
bool isEnemy;
bool isShip;
float hp;
float maxHp;
};
std::vector<RepairableInfo> repairables;
const float repairRange = s.repairTool->range;
admin.forEach<ShipIdentity, Position, Faction, Health>(
[&repairables](entt::entity e, const ShipIdentity& /*si*/,
const Position& pos, const Faction& f, const Health& h)
{
repairables.push_back({e, pos.value, f.isEnemy, true, h.hp, h.maxHp});
});
bool enemyNearby = false;
for (const Ship& candidate : allShips)
admin.forEach<StationBody, Position, Faction, Health>(
[&repairables](entt::entity e, const StationBody& /*sb*/,
const Position& pos, const Faction& f, const Health& h)
{
if (candidate.isEnemy
&& (candidate.position - s.position).length() <= s.sensorRange)
{
enemyNearby = true;
break;
}
}
if (enemyNearby)
{
if (2 > s.intent.priority)
{
s.intent = MovementIntent{2, QVector2D(-10000.0f, s.position.y())};
}
return;
}
repairables.push_back({e, pos.value, f.isEnemy, false, h.hp, h.maxHp});
});
EntityId currentId = s.repairBehavior->currentTarget.value_or(kInvalidEntityId);
bool targetValid = false;
if (currentId != kInvalidEntityId)
// Snapshot enemy ships for threat detection.
struct EnemyInfo
{
QVector2D position;
};
std::vector<EnemyInfo> enemies;
admin.forEach<ShipIdentity, Position, Faction>(
[&enemies](entt::entity /*e*/, const ShipIdentity& /*si*/,
const Position& pos, const Faction& f)
{
const Ship* tShip = ships.findShip(currentId);
if (tShip && !tShip->isEnemy && tShip->hp < tShip->maxHp)
if (f.isEnemy)
{
targetValid = true;
enemies.push_back({pos.value});
}
else
});
admin.forEach<RepairBehavior, RepairTool, Position, Faction, SensorRange, MovementIntent>(
[&](entt::entity e, RepairBehavior& rb, RepairTool& rt, Position& pos,
Faction& /*faction*/, SensorRange& sensor, MovementIntent& intent)
{
const float repairRange = rt.range;
// Flee if enemy nearby.
bool enemyNearby = false;
for (const EnemyInfo& enemy : enemies)
{
const Building* tBld = buildings.findBuilding(currentId);
if (tBld && tBld->type == BuildingType::PlayerDefenceStation
&& tBld->hp < tBld->maxHp)
if ((enemy.position - pos.value).length() <= sensor.value)
{
targetValid = true;
enemyNearby = true;
break;
}
}
}
if (!targetValid)
{
s.repairBehavior->currentTarget = std::nullopt;
currentId = kInvalidEntityId;
float bestDist = s.sensorRange;
for (const Ship& candidate : allShips)
if (enemyNearby)
{
if (candidate.isEnemy || candidate.id == s.id
|| candidate.hp >= candidate.maxHp)
if (2 > intent.priority)
{
continue;
intent = MovementIntent{2, QVector2D(-10000.0f, pos.value.y())};
}
float dist = (candidate.position - s.position).length();
if (dist < bestDist)
return;
}
// Validate current target.
bool targetValid = false;
if (rb.currentTarget)
{
const entt::entity t = *rb.currentTarget;
if (admin.isValid(t) && admin.hasAll<Health>(t))
{
bestDist = dist;
s.repairBehavior->currentTarget = candidate.id;
const Health& th = admin.get<Health>(t);
if (th.hp > 0.0f && th.hp < th.maxHp)
{
targetValid = true;
}
}
}
for (const Building& b : allBuildings)
if (!targetValid)
{
if (b.type != BuildingType::PlayerDefenceStation
|| b.hp >= b.maxHp)
rb.currentTarget = std::nullopt;
float bestDist = sensor.value;
for (const RepairableInfo& r : repairables)
{
continue;
}
float dist = (buildingCenter(b) - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.repairBehavior->currentTarget = b.id;
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;
}
}
}
currentId = s.repairBehavior->currentTarget.value_or(kInvalidEntityId);
}
if (currentId == kInvalidEntityId)
{
if (2 > s.intent.priority)
if (!rb.currentTarget)
{
s.intent = MovementIntent{2, QVector2D(s.position.x() + 1000.0f,
s.position.y())};
if (2 > intent.priority)
{
intent = MovementIntent{2, QVector2D(pos.value.x() + 1000.0f,
pos.value.y())};
}
return;
}
return;
}
QVector2D targetPos;
bool isShipTarget = false;
const Ship* tShip = ships.findShip(currentId);
if (tShip)
{
targetPos = tShip->position;
isShipTarget = true;
}
else
{
const Building* tBld = buildings.findBuilding(currentId);
targetPos = tBld ? buildingCenter(*tBld) : s.position;
}
float distToTarget = (targetPos - s.position).length();
if (distToTarget <= repairRange)
{
if (isShipTarget)
const entt::entity target = *rb.currentTarget;
QVector2D targetPos = pos.value;
bool isShipTarget = false;
if (admin.isValid(target) && admin.hasAll<Position>(target))
{
ships.healShip(currentId, s.repairTool->ratePerTick);
targetPos = admin.get<Position>(target).value;
isShipTarget = admin.hasAll<ShipIdentity>(target);
}
else
{
buildings.healBuilding(currentId, s.repairTool->ratePerTick);
}
}
if (2 > s.intent.priority)
{
s.intent = MovementIntent{2, targetPos};
}
});
const float distToTarget = (targetPos - pos.value).length();
if (distToTarget <= repairRange)
{
if (admin.isValid(target) && admin.hasAll<Health>(target))
{
Health& targetHealth = admin.get<Health>(target);
targetHealth.hp = std::min(targetHealth.hp + rt.ratePerTick,
targetHealth.maxHp);
}
}
if (2 > intent.priority)
{
intent = MovementIntent{2, targetPos};
}
});
}
// ---------------------------------------------------------------------------
// tickScrapCollector (priority 1)
// ---------------------------------------------------------------------------
void AiSystem::tickScrapCollector(ShipSystem& ships, ScrapSystem& scraps,
void AiSystem::tickScrapCollector(EntityAdmin& admin, ScrapSystem& scraps,
BuildingSystem& buildings)
{
const std::vector<Ship> allShips = ships.allShips();
ships.forEach([&](Ship& s)
// Snapshot enemy ships for threat detection.
struct EnemyShipPos
{
if (!s.scrapCollector || !s.cargo) { return; }
const float collectRange = s.cargo->collectionRange;
if (s.scrapCollector->deliveryBay == kInvalidEntityId)
QVector2D position;
};
std::vector<EnemyShipPos> enemyShips;
admin.forEach<ShipIdentity, Position, Faction>(
[&enemyShips](entt::entity /*e*/, const ShipIdentity& /*si*/,
const Position& pos, const Faction& f)
{
const Building* bay = buildings.findNearestBuilding(s.position,
BuildingType::SalvageBay);
if (bay)
if (f.isEnemy)
{
s.scrapCollector->deliveryBay = bay->id;
enemyShips.push_back({pos.value});
}
}
});
const EntityId bayId = s.scrapCollector->deliveryBay;
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
QVector2D bayPos = s.position;
if (bayId != kInvalidEntityId)
admin.forEach<ScrapCollector, SalvageCargo, Position, SensorRange, MovementIntent>(
[&](entt::entity /*e*/, ScrapCollector& sc, SalvageCargo& cargo,
Position& pos, SensorRange& sensor, MovementIntent& intent)
{
const Building* bay = buildings.findBuilding(bayId);
if (bay)
{
bayPos = buildingCenter(*bay);
}
}
const float collectRange = cargo.collectionRange;
const bool cargoFull = (s.cargo->current >= s.cargo->capacity);
if (cargoFull)
{
if (1 > s.intent.priority)
// Assign nearest SalvageBay if needed.
if (sc.deliveryBay == kInvalidEntityId)
{
s.intent = MovementIntent{1, bayPos};
}
if (bayId != kInvalidEntityId
&& (s.position - bayPos).length() <= 1.0f)
{
if (buildings.deliverScrapToSalvageBay(bayId))
const Building* bay = buildings.findNearestBuilding(pos.value,
BuildingType::SalvageBay);
if (bay)
{
--s.cargo->current;
sc.deliveryBay = bay->id;
}
}
return;
}
bool retreating = false;
if (s.cargo->current == 0)
{
for (const Ship& candidate : allShips)
const EntityId bayId = sc.deliveryBay;
QVector2D bayPos = pos.value;
if (bayId != kInvalidEntityId)
{
if (candidate.isEnemy
&& (candidate.position - s.position).length() <= collectRange)
const Building* bay = buildings.findBuilding(bayId);
if (bay)
{
if (1 > s.intent.priority)
bayPos = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,
bay->anchor.y() + bay->footprint.height() / 2.0f);
}
}
const bool cargoFull = (cargo.current >= cargo.capacity);
if (cargoFull)
{
if (1 > intent.priority)
{
intent = MovementIntent{1, bayPos};
}
if (bayId != kInvalidEntityId
&& (pos.value - bayPos).length() <= 1.0f)
{
if (buildings.deliverScrapToSalvageBay(bayId))
{
s.intent = MovementIntent{1, QVector2D(-10000.0f, s.position.y())};
--cargo.current;
}
}
return;
}
// Retreat if enemy near and cargo empty.
bool retreating = false;
if (cargo.current == 0)
{
for (const EnemyShipPos& enemy : enemyShips)
{
if ((enemy.position - pos.value).length() <= collectRange)
{
if (1 > intent.priority)
{
intent = MovementIntent{1, QVector2D(-10000.0f, pos.value.y())};
}
retreating = true;
break;
}
}
}
if (retreating) { return; }
// Collect nearby scrap.
for (const ScrapInfo& si : allScrap)
{
if ((si.position - pos.value).length() <= collectRange)
{
if (scraps.consume(si.entity))
{
++cargo.current;
sc.scrapTarget = std::nullopt;
}
retreating = true;
break;
}
}
}
if (retreating) { return; }
for (const Scrap& sc : scraps.allScraps())
{
if ((sc.position - s.position).length() <= collectRange)
// Move toward scrap target or find a new one.
if (sc.scrapTarget)
{
if (scraps.consume(sc.id))
if (1 > intent.priority)
{
++s.cargo->current;
s.scrapCollector->scrapTarget = std::nullopt;
}
break;
}
}
if (s.scrapCollector->scrapTarget)
{
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1, *s.scrapCollector->scrapTarget};
}
}
else
{
float bestDist = s.sensorRange;
std::optional<QVector2D> bestPos;
for (const Scrap& sc : scraps.allScraps())
{
float dist = (sc.position - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
bestPos = sc.position;
}
}
if (bestPos)
{
s.scrapCollector->scrapTarget = bestPos;
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1, *bestPos};
intent = MovementIntent{1, *sc.scrapTarget};
}
}
else
{
if (1 > s.intent.priority)
float bestDist = sensor.value;
std::optional<QVector2D> bestPos;
for (const ScrapInfo& si : allScrap)
{
s.intent = MovementIntent{1, QVector2D(s.position.x() + 1000.0f,
s.position.y())};
const float dist = (si.position - pos.value).length();
if (dist < bestDist)
{
bestDist = dist;
bestPos = si.position;
}
}
if (bestPos)
{
sc.scrapTarget = bestPos;
if (1 > intent.priority)
{
intent = MovementIntent{1, *bestPos};
}
}
else
{
if (1 > intent.priority)
{
intent = MovementIntent{1, QVector2D(pos.value.x() + 1000.0f,
pos.value.y())};
}
}
}
}
});
});
}