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

@@ -4,6 +4,7 @@
#include "AiSystem.h"
#include "BuildingSystem.h"
#include "EcsComponents.h"
#include "CombatSystem.h"
#include "MovementSystem.h"
#include "ScrapSystem.h"
@@ -19,13 +20,14 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
, m_nextId(1)
, m_buildingBlocksStock(m_config.world.startingBuildingBlocks)
, m_gameOver(false)
, m_hqId(kInvalidEntityId)
, m_playerStation1Id(kInvalidEntityId)
, m_playerStation2Id(kInvalidEntityId)
, m_hqBuildingId(kInvalidEntityId)
, m_hqProxyEntity(entt::null)
, m_playerStation1Entity(entt::null)
, m_playerStation2Entity(entt::null)
, m_beltSystem(m_config.world.beltSpeedTilesPerSecond)
{
m_currentEnemyStationIds[0] = kInvalidEntityId;
m_currentEnemyStationIds[1] = kInvalidEntityId;
m_currentEnemyStationEntities[0] = entt::null;
m_currentEnemyStationEntities[1] = entt::null;
m_buildingSystem = std::make_unique<BuildingSystem>(
m_config,
@@ -43,10 +45,10 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
},
m_rng);
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
m_aiSystem = std::make_unique<AiSystem>();
m_movementSystem = std::make_unique<MovementSystem>();
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
m_combatSystem = std::make_unique<CombatSystem>(m_config);
@@ -82,15 +84,17 @@ void Simulation::reset(unsigned int seed)
m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds);
m_nextId = 1;
m_buildingBlocksStock = m_config.world.startingBuildingBlocks;
m_gameOver = false;
m_hqId = kInvalidEntityId;
m_playerStation1Id = kInvalidEntityId;
m_playerStation2Id = kInvalidEntityId;
m_currentEnemyStationIds[0] = kInvalidEntityId;
m_currentEnemyStationIds[1] = kInvalidEntityId;
m_gameOver = false;
m_hqBuildingId = kInvalidEntityId;
m_hqProxyEntity = entt::null;
m_playerStation1Entity = entt::null;
m_playerStation2Entity = entt::null;
m_currentEnemyStationEntities[0] = entt::null;
m_currentEnemyStationEntities[1] = entt::null;
m_fireEvents.clear();
m_schematicDropEvents.clear();
m_admin.clear();
m_beltSystem = BeltSystem(m_config.world.beltSpeedTilesPerSecond);
m_buildingSystem = std::make_unique<BuildingSystem>(
m_config,
@@ -108,10 +112,10 @@ void Simulation::reset(unsigned int seed)
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
},
m_rng);
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
m_aiSystem = std::make_unique<AiSystem>();
m_movementSystem = std::make_unique<MovementSystem>();
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
m_combatSystem = std::make_unique<CombatSystem>(m_config);
@@ -158,17 +162,17 @@ void Simulation::tick()
}
m_shipSystem->clearMovementIntents();
m_aiSystem->tickHomeReturn(*m_shipSystem); // priority 4
m_aiSystem->tickThreatResponse(*m_shipSystem, *m_buildingSystem); // priority 3
m_aiSystem->tickRepairBehavior(*m_shipSystem, *m_buildingSystem); // priority 2
m_aiSystem->tickScrapCollector(*m_shipSystem, *m_scrapSystem, *m_buildingSystem); // priority 1
m_aiSystem->tickHomeReturn(m_admin); // priority 4
m_aiSystem->tickThreatResponse(m_admin, *m_buildingSystem); // priority 3
m_aiSystem->tickRepairBehavior(m_admin, *m_buildingSystem); // priority 2
m_aiSystem->tickScrapCollector(m_admin, *m_scrapSystem, *m_buildingSystem); // priority 1
// Step 8: combat resolution
m_combatSystem->tick(m_currentTick, *m_shipSystem,
m_combatSystem->tick(m_currentTick, m_admin,
*m_buildingSystem, m_fireEvents);
// Step 8b: deferred damage whose impact tick has arrived
m_combatSystem->applyPendingDamage(m_currentTick, *m_shipSystem, *m_buildingSystem);
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
// Step 9: deaths & loot
if (!m_gameOver)
@@ -177,7 +181,7 @@ void Simulation::tick()
}
// Step 10: advance ship positions
m_movementSystem->tick(*m_shipSystem);
m_movementSystem->tick(m_admin);
// Step 11: scrap despawn
m_scrapSystem->tickDespawn(m_currentTick);
@@ -192,6 +196,7 @@ void Simulation::tick()
void Simulation::placeInitialStructures()
{
// HQ — right edge of asteroid (rightmost asteroid tile is x = -1).
// Placed as a Building (for belt input) plus an ECS proxy (for HP/targeting).
const ParsedSurfaceMask hqParsed =
parseSurfaceMask(m_config.stations.hq.surfaceMask, Rotation::East);
const int hqAnchorX = -hqParsed.footprint.width();
@@ -199,13 +204,18 @@ void Simulation::placeInitialStructures()
(m_config.world.heightTiles - hqParsed.footprint.height()) / 2;
const float hqHp =
static_cast<float>(m_config.stations.hq.hpFormula.evaluate(0.0));
m_hqId = m_buildingSystem->placeImmediate(
m_hqBuildingId = m_buildingSystem->placeImmediate(
BuildingType::Hq,
m_config.stations.hq.surfaceMask,
QPoint(hqAnchorX, hqAnchorY),
Rotation::East, hqHp, hqHp);
// Player defence stations — right edge of player buffer zone.
const QVector2D hqCenter(
hqAnchorX + hqParsed.footprint.width() / 2.0f,
hqAnchorY + hqParsed.footprint.height() / 2.0f);
m_hqProxyEntity = m_admin.spawnHqProxy(hqCenter, hqHp, hqHp);
// Player defence stations — ECS entities with tile occupancy.
const ParsedSurfaceMask psParsed =
parseSurfaceMask(m_config.stations.playerStation.surfaceMask, Rotation::East);
const int psAnchorX =
@@ -222,21 +232,35 @@ void Simulation::placeInitialStructures()
psWeapon.fireRateHz = static_cast<float>(
m_config.stations.playerStation.fireRateFormula.evaluate(psLevel));
psWeapon.cooldownTicks = 0.0f;
psWeapon.currentTarget = std::nullopt;
const int ps1Y = m_config.world.heightTiles / 4;
const int ps2Y = 3 * m_config.world.heightTiles / 4;
m_playerStation1Id = m_buildingSystem->placeImmediate(
BuildingType::PlayerDefenceStation,
m_config.stations.playerStation.surfaceMask,
QPoint(psAnchorX, ps1Y), Rotation::East, psHp, psHp);
m_buildingSystem->initStationWeapon(m_playerStation1Id, psWeapon);
m_playerStation2Id = m_buildingSystem->placeImmediate(
BuildingType::PlayerDefenceStation,
m_config.stations.playerStation.surfaceMask,
QPoint(psAnchorX, ps2Y), Rotation::East, psHp, psHp);
m_buildingSystem->initStationWeapon(m_playerStation2Id, psWeapon);
{
const QPoint anchor(psAnchorX, ps1Y);
std::vector<QPoint> absCells;
for (const QPoint& rel : psParsed.bodyCells)
{
absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
}
m_playerStation1Entity = m_admin.spawnStation(
anchor, psParsed.footprint, absCells, psHp, psHp, false);
m_admin.addComponent<StationWeapon>(m_playerStation1Entity, psWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateId());
}
{
const QPoint anchor(psAnchorX, ps2Y);
std::vector<QPoint> absCells;
for (const QPoint& rel : psParsed.bodyCells)
{
absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
}
m_playerStation2Entity = m_admin.spawnStation(
anchor, psParsed.footprint, absCells, psHp, psHp, false);
m_admin.addComponent<StationWeapon>(m_playerStation2Entity, psWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateId());
}
// Rally point: center of the player defence stations' X column, world vertical midpoint.
const float rallyX = static_cast<float>(psAnchorX) + psParsed.footprint.width() / 2.0f;
@@ -252,7 +276,6 @@ void Simulation::placeEnemyStationSet(int generation)
const ParsedSurfaceMask esParsed =
parseSurfaceMask(m_config.stations.enemyStation.surfaceMask, Rotation::East);
// Right edge of contest zone, shifted right by (generation * pushExpandColumns).
const int rightEdgeX = m_config.world.regions.playerBufferWidth
+ m_config.world.regions.contestZoneWidth
+ generation * m_config.world.push.pushExpandColumns;
@@ -270,24 +293,35 @@ void Simulation::placeEnemyStationSet(int generation)
esWeapon.fireRateHz = static_cast<float>(
m_config.stations.enemyStation.fireRateFormula.evaluate(genD));
esWeapon.cooldownTicks = 0.0f;
esWeapon.currentTarget = std::nullopt;
const int y1 = m_config.world.heightTiles / 4;
const int y2 = 3 * m_config.world.heightTiles / 4;
const EntityId id1 = m_buildingSystem->placeImmediate(
BuildingType::EnemyDefenceStation,
m_config.stations.enemyStation.surfaceMask,
QPoint(anchorX, y1), Rotation::East, esHp, esHp);
m_buildingSystem->initStationWeapon(id1, esWeapon);
const EntityId id2 = m_buildingSystem->placeImmediate(
BuildingType::EnemyDefenceStation,
m_config.stations.enemyStation.surfaceMask,
QPoint(anchorX, y2), Rotation::East, esHp, esHp);
m_buildingSystem->initStationWeapon(id2, esWeapon);
m_currentEnemyStationIds[0] = id1;
m_currentEnemyStationIds[1] = id2;
{
const QPoint anchor(anchorX, y1);
std::vector<QPoint> absCells;
for (const QPoint& rel : esParsed.bodyCells)
{
absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
}
m_currentEnemyStationEntities[0] = m_admin.spawnStation(
anchor, esParsed.footprint, absCells, esHp, esHp, true);
m_admin.addComponent<StationWeapon>(m_currentEnemyStationEntities[0], esWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateId());
}
{
const QPoint anchor(anchorX, y2);
std::vector<QPoint> absCells;
for (const QPoint& rel : esParsed.bodyCells)
{
absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
}
m_currentEnemyStationEntities[1] = m_admin.spawnStation(
anchor, esParsed.footprint, absCells, esHp, esHp, true);
m_admin.addComponent<StationWeapon>(m_currentEnemyStationEntities[1], esWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateId());
}
}
// ---------------------------------------------------------------------------
@@ -297,98 +331,92 @@ void Simulation::placeEnemyStationSet(int generation)
void Simulation::tickDeathsAndLoot()
{
// --- Dead ships ---
std::vector<EntityId> deadShipIds;
m_shipSystem->forEach([&deadShipIds](Ship& s)
{
if (s.hp <= 0.0f)
std::vector<entt::entity> deadShips;
m_admin.forEach<ShipIdentity, Health>(
[&deadShips](entt::entity e, const ShipIdentity& /*si*/, const Health& h)
{
deadShipIds.push_back(s.id);
}
});
if (h.hp <= 0.0f)
{
deadShips.push_back(e);
}
});
for (EntityId deadId : deadShipIds)
for (entt::entity deadEntity : deadShips)
{
const Ship* s = m_shipSystem->findShip(deadId);
if (!s)
{
continue;
}
// Look up scrap drop amount from config.
const ShipIdentity& si = m_admin.get<ShipIdentity>(deadEntity);
const Position& pos = m_admin.get<Position>(deadEntity);
for (const ShipDef& def : m_config.ships.ships)
{
if (def.id == s->schematicId && def.loot.scrapDrop > 0)
if (def.id == si.schematicId && def.loot.scrapDrop > 0)
{
const Tick despawnAt = m_currentTick
+ secondsToTicks(m_config.world.scrapDespawnSeconds);
m_scrapSystem->spawn(s->position, def.loot.scrapDrop, despawnAt);
m_scrapSystem->spawn(pos.value, def.loot.scrapDrop, despawnAt);
break;
}
}
m_shipSystem->despawn(deadId);
m_shipSystem->despawn(deadEntity);
}
// --- Dead buildings (HQ, player/enemy defence stations) ---
std::vector<EntityId> deadBuildingIds;
for (const Building& b : m_buildingSystem->allBuildings())
{
if (b.hp <= 0.0f &&
(b.type == BuildingType::Hq ||
b.type == BuildingType::PlayerDefenceStation ||
b.type == BuildingType::EnemyDefenceStation))
// --- Dead stations ---
std::vector<entt::entity> deadStations;
m_admin.forEach<StationBody, Health>(
[&deadStations](entt::entity e, const StationBody& /*sb*/, const Health& h)
{
deadBuildingIds.push_back(b.id);
}
}
if (h.hp <= 0.0f)
{
deadStations.push_back(e);
}
});
for (EntityId deadId : deadBuildingIds)
for (entt::entity deadEntity : deadStations)
{
const Building* b = m_buildingSystem->findBuilding(deadId);
if (!b)
{
continue;
}
const StationBody& sb = m_admin.get<StationBody>(deadEntity);
const Position& pos = m_admin.get<Position>(deadEntity);
const Faction& fac = m_admin.get<Faction>(deadEntity);
if (b->type == BuildingType::Hq)
const Tick despawnAt = m_currentTick
+ secondsToTicks(m_config.world.scrapDespawnSeconds);
int scrap = 0;
if (!fac.isEnemy)
{
m_gameOver = true;
const double lv = static_cast<double>(
m_config.stations.playerStation.level);
scrap = static_cast<int>(
m_config.stations.playerStation.scrapDropFormula.evaluate(lv));
}
else
{
const QVector2D center(
b->anchor.x() + b->footprint.width() / 2.0f,
b->anchor.y() + b->footprint.height() / 2.0f);
const Tick despawnAt = m_currentTick
+ secondsToTicks(m_config.world.scrapDespawnSeconds);
int scrap = 0;
if (b->type == BuildingType::PlayerDefenceStation)
{
const double lv = static_cast<double>(
m_config.stations.playerStation.level);
scrap = static_cast<int>(
m_config.stations.playerStation.scrapDropFormula.evaluate(lv));
}
else if (b->type == BuildingType::EnemyDefenceStation)
{
const double genD = static_cast<double>(m_waveSystem->generation());
scrap = static_cast<int>(
m_config.stations.enemyStation.scrapDropFormula.evaluate(genD));
}
if (scrap > 0)
{
m_scrapSystem->spawn(center, scrap, despawnAt);
}
const double genD = static_cast<double>(m_waveSystem->generation());
scrap = static_cast<int>(
m_config.stations.enemyStation.scrapDropFormula.evaluate(genD));
}
if (scrap > 0)
{
m_scrapSystem->spawn(pos.value, scrap, despawnAt);
}
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
m_admin.destroy(deadEntity);
}
// --- HQ death check ---
if (m_admin.isValid(m_hqProxyEntity))
{
const Health& hqHealth = m_admin.get<Health>(m_hqProxyEntity);
if (hqHealth.hp <= 0.0f)
{
m_gameOver = true;
}
m_buildingSystem->removeBuilding(deadId);
}
// --- Push check: if both current enemy stations are gone, trigger push ---
const bool es0Gone =
(m_buildingSystem->findBuilding(m_currentEnemyStationIds[0]) == nullptr);
const bool es1Gone =
(m_buildingSystem->findBuilding(m_currentEnemyStationIds[1]) == nullptr);
const bool es0Gone = !m_admin.isValid(m_currentEnemyStationEntities[0])
|| m_admin.get<Health>(m_currentEnemyStationEntities[0]).hp <= 0.0f;
const bool es1Gone = !m_admin.isValid(m_currentEnemyStationEntities[1])
|| m_admin.get<Health>(m_currentEnemyStationEntities[1]).hp <= 0.0f;
if (es0Gone && es1Gone &&
m_currentEnemyStationIds[0] != kInvalidEntityId)
m_currentEnemyStationEntities[0] != entt::null)
{
m_waveSystem->applyPush();
placeEnemyStationSet(m_waveSystem->generation());
@@ -548,6 +576,16 @@ const ScrapSystem& Simulation::scraps() const
return *m_scrapSystem;
}
EntityAdmin& Simulation::admin()
{
return m_admin;
}
const EntityAdmin& Simulation::admin() const
{
return m_admin;
}
EntityId Simulation::allocateId()
{
return m_nextId++;