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

@@ -10,6 +10,8 @@
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "CombatSystem.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "MovementSystem.h"
#include "ScrapSystem.h"
#include "Ship.h"
@@ -27,8 +29,8 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
, m_currentTick(0)
, m_nextId(1)
, m_beltSystem(1.0)
, m_team1HqId(kInvalidEntityId)
, m_team2HqId(kInvalidEntityId)
, m_team1HqEntity(entt::null)
, m_team2HqEntity(entt::null)
, m_finished(false)
, m_winnerTeam(-1)
, m_stopRequested(false)
@@ -41,13 +43,11 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
m_rng);
m_shipSystem = std::make_unique<ShipSystem>(
m_gameConfig, [this]() { return allocateId(); });
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
m_aiSystem = std::make_unique<AiSystem>();
m_movementSystem = std::make_unique<MovementSystem>();
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
m_scrapSystem = std::make_unique<ScrapSystem>(m_admin);
placeStructures();
spawnShips();
@@ -71,7 +71,7 @@ void ArenaSimulation::placeStructures()
+ m_arenaConfig.enemyBufferWidth;
const int midY = m_arenaConfig.heightTiles / 2;
// Team 1 HQ at left edge, placed as Hq (enemy ships target Hq).
// Team 1 HQ — ECS proxy entity, player faction (isEnemy=false).
{
const ParsedSurfaceMask hqParsed =
parseSurfaceMask(m_gameConfig.stations.hq.surfaceMask, Rotation::East);
@@ -79,16 +79,20 @@ void ArenaSimulation::placeStructures()
const int anchorY = midY - hqParsed.footprint.height() / 2;
const float hp = static_cast<float>(
m_gameConfig.stations.hq.hpFormula.evaluate(1.0));
m_team1HqId = m_buildingSystem->placeImmediate(
BuildingType::Hq,
m_gameConfig.stations.hq.surfaceMask,
QPoint(anchorX, anchorY),
Rotation::East, hp, hp);
const QPoint anchor(anchorX, anchorY);
std::vector<QPoint> absCells;
for (const QPoint& rel : hqParsed.bodyCells)
{
absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
}
const QVector2D center(anchorX + hqParsed.footprint.width() / 2.0f,
anchorY + hqParsed.footprint.height() / 2.0f);
m_team1HqEntity = m_admin.spawnStation(anchor, hqParsed.footprint, absCells,
hp, hp, false);
m_buildingSystem->registerTileOccupancy(absCells, allocateId());
}
// Team 2 HQ at right edge, placed as EnemyDefenceStation (player ships target these).
// No weapon — it's just a destructible target.
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
{
const ParsedSurfaceMask hqParsed =
parseSurfaceMask(m_gameConfig.stations.hq.surfaceMask, Rotation::West);
@@ -96,90 +100,72 @@ void ArenaSimulation::placeStructures()
const int anchorY = midY - hqParsed.footprint.height() / 2;
const float hp = static_cast<float>(
m_gameConfig.stations.hq.hpFormula.evaluate(1.0));
m_team2HqId = m_buildingSystem->placeImmediate(
BuildingType::EnemyDefenceStation,
m_gameConfig.stations.hq.surfaceMask,
QPoint(anchorX, anchorY),
Rotation::West, hp, hp);
const QPoint anchor(anchorX, anchorY);
std::vector<QPoint> absCells;
for (const QPoint& rel : hqParsed.bodyCells)
{
absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
}
m_team2HqEntity = m_admin.spawnStation(anchor, hqParsed.footprint, absCells,
hp, hp, true);
m_buildingSystem->registerTileOccupancy(absCells, allocateId());
}
// Team 1 defence stations (PlayerDefenceStation — targeted by team 2).
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
{
float hp = 0.0f;
StationWeapon weapon;
weapon.cooldownTicks = 0.0f;
weapon.currentTarget = std::nullopt;
const double lv = static_cast<double>(entry.level);
const std::vector<std::string>& mask = isEnemy
? m_gameConfig.stations.enemyStation.surfaceMask
: m_gameConfig.stations.playerStation.surfaceMask;
if (entry.stationType == "player_station")
{
hp = static_cast<float>(
m_gameConfig.stations.playerStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.playerStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv));
}
else
{
hp = static_cast<float>(
m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv));
}
const ParsedSurfaceMask parsed = parseSurfaceMask(mask, Rotation::East);
const QPoint& anchor = entry.position;
std::vector<QPoint> absCells;
for (const QPoint& rel : parsed.bodyCells)
{
absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
}
const entt::entity stationEntity = m_admin.spawnStation(
anchor, parsed.footprint, absCells, hp, hp, isEnemy);
m_admin.addComponent<StationWeapon>(stationEntity, weapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateId());
};
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
{
float hp = 0.0f;
StationWeapon weapon;
weapon.cooldownTicks = 0.0f;
const double lv = static_cast<double>(entry.level);
if (entry.stationType == "player_station")
{
hp = static_cast<float>(
m_gameConfig.stations.playerStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.playerStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv));
}
else
{
hp = static_cast<float>(
m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv));
}
const EntityId stationId = m_buildingSystem->placeImmediate(
BuildingType::PlayerDefenceStation,
m_gameConfig.stations.playerStation.surfaceMask,
entry.position,
Rotation::East, hp, hp);
m_buildingSystem->initStationWeapon(stationId, weapon);
placeArenaStation(entry, false);
}
// Team 2 defence stations (EnemyDefenceStation — targeted by team 1).
for (const ArenaStationEntry& entry : m_arenaConfig.teams[1].stations)
{
float hp = 0.0f;
StationWeapon weapon;
weapon.cooldownTicks = 0.0f;
const double lv = static_cast<double>(entry.level);
if (entry.stationType == "player_station")
{
hp = static_cast<float>(
m_gameConfig.stations.playerStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.playerStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv));
}
else
{
hp = static_cast<float>(
m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>(
m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>(
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv));
weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv));
}
const EntityId stationId = m_buildingSystem->placeImmediate(
BuildingType::EnemyDefenceStation,
m_gameConfig.stations.enemyStation.surfaceMask,
entry.position,
Rotation::East, hp, hp);
m_buildingSystem->initStationWeapon(stationId, weapon);
placeArenaStation(entry, true);
}
}
@@ -254,22 +240,22 @@ void ArenaSimulation::tick()
{
// Ship behavior systems (tick step 7).
m_shipSystem->clearMovementIntents();
m_aiSystem->tickHomeReturn(*m_shipSystem);
m_aiSystem->tickThreatResponse(*m_shipSystem, *m_buildingSystem);
m_aiSystem->tickRepairBehavior(*m_shipSystem, *m_buildingSystem);
m_aiSystem->tickScrapCollector(*m_shipSystem, *m_scrapSystem, *m_buildingSystem);
m_aiSystem->tickHomeReturn(m_admin);
m_aiSystem->tickThreatResponse(m_admin, *m_buildingSystem);
m_aiSystem->tickRepairBehavior(m_admin, *m_buildingSystem);
m_aiSystem->tickScrapCollector(m_admin, *m_scrapSystem, *m_buildingSystem);
// Combat resolution (tick step 8).
std::vector<FireEvent> fireEvents;
m_combatSystem->tick(m_currentTick, *m_shipSystem, *m_buildingSystem, fireEvents);
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, fireEvents);
m_fireEvents.insert(m_fireEvents.end(), fireEvents.begin(), fireEvents.end());
m_combatSystem->applyPendingDamage(m_currentTick, *m_shipSystem, *m_buildingSystem);
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
// Deaths (tick step 9, simplified).
tickDeaths();
// Movement (tick step 10).
m_movementSystem->tick(*m_shipSystem);
m_movementSystem->tick(m_admin);
// Scrap despawn (tick step 11).
m_scrapSystem->tickDespawn(m_currentTick);
@@ -285,58 +271,56 @@ void ArenaSimulation::tick()
void ArenaSimulation::tickDeaths()
{
// 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;
}
const ShipIdentity& si = m_admin.get<ShipIdentity>(deadEntity);
const Position& pos = m_admin.get<Position>(deadEntity);
for (const ShipDef& def : m_gameConfig.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_gameConfig.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 and 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)
{
m_buildingSystem->removeBuilding(deadId);
const StationBody& sb = m_admin.get<StationBody>(deadEntity);
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
m_admin.destroy(deadEntity);
}
// Check end conditions.
const bool team1HqGone =
(m_buildingSystem->findBuilding(m_team1HqId) == nullptr);
const bool team2HqGone =
(m_buildingSystem->findBuilding(m_team2HqId) == nullptr);
// Check end conditions — HQ proxy entities.
const bool team1HqGone = !m_admin.isValid(m_team1HqEntity)
|| m_admin.get<Health>(m_team1HqEntity).hp <= 0.0f;
const bool team2HqGone = !m_admin.isValid(m_team2HqEntity)
|| m_admin.get<Health>(m_team2HqEntity).hp <= 0.0f;
if (team1HqGone || team2HqGone)
{
@@ -349,30 +333,23 @@ void ArenaSimulation::tickDeaths()
// Check if all ships and defence stations of one team are destroyed.
bool team1HasUnits = false;
bool team2HasUnits = false;
m_shipSystem->forEach([&team1HasUnits, &team2HasUnits](Ship& s)
{
if (s.isEnemy)
m_admin.forEach<ShipIdentity, Faction>(
[&team1HasUnits, &team2HasUnits](entt::entity /*e*/,
const ShipIdentity& /*si*/,
const Faction& f)
{
team2HasUnits = true;
}
else
{
team1HasUnits = true;
}
});
if (f.isEnemy) { team2HasUnits = true; }
else { team1HasUnits = true; }
});
for (const Building& b : m_buildingSystem->allBuildings())
{
if (b.type == BuildingType::PlayerDefenceStation)
m_admin.forEach<StationBody, Faction>(
[&team1HasUnits, &team2HasUnits](entt::entity /*e*/,
const StationBody& /*sb*/,
const Faction& f)
{
team1HasUnits = true;
}
else if (b.type == BuildingType::EnemyDefenceStation
&& b.id != m_team2HqId)
{
team2HasUnits = true;
}
}
if (f.isEnemy) { team2HasUnits = true; }
else { team1HasUnits = true; }
});
if (!team1HasUnits || !team2HasUnits)
{
@@ -433,6 +410,16 @@ const ScrapSystem& ArenaSimulation::scraps() const
return *m_scrapSystem;
}
EntityAdmin& ArenaSimulation::admin()
{
return m_admin;
}
const EntityAdmin& ArenaSimulation::admin() const
{
return m_admin;
}
void ArenaSimulation::updateStatus()
{
ArenaStatus newStatus;
@@ -450,8 +437,9 @@ void ArenaSimulation::updateStatus()
hqEntry.displayName = "HQ";
hqEntry.level = 1;
hqEntry.total = 1;
const EntityId hqId = (ti == 0) ? m_team1HqId : m_team2HqId;
hqEntry.surviving = (m_buildingSystem->findBuilding(hqId) != nullptr) ? 1 : 0;
const entt::entity hqEntity = (ti == 0) ? m_team1HqEntity : m_team2HqEntity;
hqEntry.surviving = (m_admin.isValid(hqEntity)
&& m_admin.get<Health>(hqEntity).hp > 0.0f) ? 1 : 0;
teamStatus.entries.push_back(hqEntry);
}
@@ -465,13 +453,14 @@ void ArenaSimulation::updateStatus()
int surviving = 0;
const bool isEnemyTeam = (ti == 1);
m_shipSystem->forEach(
[&surviving, &shipEntry, isEnemyTeam](Ship& s)
m_admin.forEach<ShipIdentity, Faction, Health>(
[&surviving, &shipEntry, isEnemyTeam](entt::entity /*e*/,
const ShipIdentity& si, const Faction& f, const Health& h)
{
if (s.isEnemy == isEnemyTeam
&& s.schematicId == shipEntry.schematicId
&& s.level == shipEntry.level
&& s.hp > 0.0f)
if (f.isEnemy == isEnemyTeam
&& si.schematicId == shipEntry.schematicId
&& si.level == shipEntry.level
&& h.hp > 0.0f)
{
++surviving;
}
@@ -490,20 +479,19 @@ void ArenaSimulation::updateStatus()
entry.level = stationEntry.level;
entry.total = 1;
// Count surviving stations of this team at this position.
const BuildingType expectedType = (ti == 0)
? BuildingType::PlayerDefenceStation
: BuildingType::EnemyDefenceStation;
int surviving = 0;
for (const Building& b : m_buildingSystem->allBuildings())
{
if (b.type == expectedType && b.anchor == stationEntry.position)
const bool isEnemyTeam = (ti == 1);
m_admin.forEach<StationBody, Faction, Health>(
[&surviving, &stationEntry, isEnemyTeam](entt::entity /*e*/,
const StationBody& sb, const Faction& f, const Health& h)
{
surviving = 1;
break;
}
}
if (f.isEnemy == isEnemyTeam
&& sb.anchor == stationEntry.position
&& h.hp > 0.0f)
{
surviving = 1;
}
});
entry.surviving = surviving;
teamStatus.entries.push_back(entry);
}