The eleven forwarding members added last commit are gone; callers now read the data directly through FactoryQueries.h. Simulation and ArenaSimulation expose getFactoryState() so the UI, the balancing view and the tests can reach it. isQueuedForDeconstruction joined the free functions along the way — it only reaches findBuilding, so it was state-pure too. No facade was introduced. The chained form was the reason one looked attractive, but rewriting sim.getBuildings().findBuilding(id) to findBuilding(sim.getFactoryState(), id) turned out to be mechanical, and the result says which data is read rather than which system happens to own it. BuildingSystem.cpp is down to 1735 lines and no longer answers questions about the factory — it only changes it. What remains on it are the mutators, the tick phases, and the queries that also need GameConfig. Verified with a golden-checksum capture before and after — all four sample ticks identical. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
632 lines
22 KiB
C++
632 lines
22 KiB
C++
#include "ArenaSimulation.h"
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <string>
|
|
|
|
#include <QVector2D>
|
|
|
|
#include "AiSystem.h"
|
|
#include "Building.h"
|
|
#include "BuildingSystem.h"
|
|
#include "BuildingType.h"
|
|
#include "CombatSystem.h"
|
|
#include "DynamicBodySystem.h"
|
|
#include "EntityAdmin.h"
|
|
#include "FactionComponent.h"
|
|
#include "HealthComponent.h"
|
|
#include "HqProxyComponent.h"
|
|
#include "ModuleOwnerComponent.h"
|
|
#include "MovementIntentSystem.h"
|
|
#include "PositionComponent.h"
|
|
#include "RepairSystem.h"
|
|
#include "SalvagerSystem.h"
|
|
#include "DebrisSystem.h"
|
|
#include "ShipIdentityComponent.h"
|
|
#include "ShipSystem.h"
|
|
#include "ShipsConfig.h"
|
|
#include "StationBodyComponent.h"
|
|
#include "StationsConfig.h"
|
|
#include "SurfaceMask.h"
|
|
#include "ThreatCostCalculator.h"
|
|
#include "WeaponComponent.h"
|
|
|
|
ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
|
ArenaConfig arenaConfig,
|
|
unsigned int seed)
|
|
: m_gameConfig(gameConfig)
|
|
, m_arenaConfig(std::move(arenaConfig))
|
|
, m_rng(seed)
|
|
, m_currentTick(0)
|
|
, m_nextBuildingId(1)
|
|
, m_beltSystem(1.0)
|
|
, m_team1HqEntity(entt::null)
|
|
, m_team2HqEntity(entt::null)
|
|
, m_finished(false)
|
|
, m_stopRequested(false)
|
|
{
|
|
m_buildingSystem = std::make_unique<BuildingSystem>(
|
|
m_gameConfig,
|
|
m_factoryState,
|
|
m_beltSystem,
|
|
[this]() { return allocateBuildingId(); },
|
|
[](int) {},
|
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
|
[](const std::string&) -> bool { return true; },
|
|
m_rng);
|
|
|
|
m_shipSystem = std::make_unique<ShipSystem>(m_gameConfig, m_admin);
|
|
// Arena fights are symmetric and aggressive: player-faction ships must not
|
|
// retreat (REQ-BAL-SIM-AI). Only one faction would otherwise get retreat.
|
|
m_shipSystem->setRetreatEnabled(false);
|
|
m_aiSystem = std::make_unique<AiSystem>(m_gameConfig);
|
|
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
|
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
|
|
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
|
|
m_debrisSystem = std::make_unique<DebrisSystem>(m_admin);
|
|
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
|
|
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
|
|
|
|
// Static accumulated threat per team: sum of count * per-ship threat cost
|
|
// (REQ-MOD-THREAT) over the configured ship roster. Ships only; HQ and
|
|
// defence stations are excluded. Level-independent, so computed once here.
|
|
for (int ti = 0; ti < 2; ++ti)
|
|
{
|
|
double teamThreat = 0.0;
|
|
for (const ArenaShipEntry& shipEntry : m_arenaConfig.teams[ti].ships)
|
|
{
|
|
const std::vector<PlacedModule>& modules = shipEntry.layout
|
|
? shipEntry.layout->placedModules
|
|
: std::vector<PlacedModule>{};
|
|
const double shipThreat = calculateShipThreatCost(
|
|
m_gameConfig.threatCosts, m_gameConfig, shipEntry.schematicId, modules);
|
|
teamThreat += shipThreat * shipEntry.count;
|
|
}
|
|
m_teamThreat[ti] = teamThreat;
|
|
}
|
|
|
|
placeStructures();
|
|
spawnShips();
|
|
computeTeamMaxEhp();
|
|
|
|
m_shipSystem->triggerRallyDeparture();
|
|
|
|
updateStatus();
|
|
}
|
|
|
|
std::string ArenaStatus::TeamStatus::getEhpPercentText() const
|
|
{
|
|
if (maxEhp <= 0.0)
|
|
{
|
|
return "n/a";
|
|
}
|
|
const int percent = static_cast<int>(std::lround(100.0 * currentEhp / maxEhp));
|
|
return std::to_string(percent) + "%";
|
|
}
|
|
|
|
void ArenaSimulation::computeTeamMaxEhp()
|
|
{
|
|
m_teamMaxEhp[0] = 0.0;
|
|
m_teamMaxEhp[1] = 0.0;
|
|
|
|
// Ships contribute their full max HP.
|
|
m_admin.forEach<ShipIdentityComponent, FactionComponent, HealthComponent>(
|
|
[this](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
|
const FactionComponent& f, const HealthComponent& h)
|
|
{
|
|
m_teamMaxEhp[f.isEnemy ? 1 : 0] += static_cast<double>(h.maxHp);
|
|
});
|
|
|
|
// Defence stations contribute their full max HP; the HQ is excluded.
|
|
m_admin.forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
|
[this](entt::entity e, const StationBodyComponent& /*sb*/,
|
|
const FactionComponent& f, const HealthComponent& h)
|
|
{
|
|
if (m_admin.hasAll<HqProxyComponent>(e))
|
|
{
|
|
return;
|
|
}
|
|
m_teamMaxEhp[f.isEnemy ? 1 : 0] += static_cast<double>(h.maxHp);
|
|
});
|
|
}
|
|
|
|
ArenaSimulation::~ArenaSimulation() = default;
|
|
|
|
BuildingId ArenaSimulation::allocateBuildingId()
|
|
{
|
|
return m_nextBuildingId++;
|
|
}
|
|
|
|
void ArenaSimulation::placeStructures()
|
|
{
|
|
const int totalWidth = m_arenaConfig.playerBufferWidth_tiles
|
|
+ m_arenaConfig.contestZoneWidth_tiles
|
|
+ m_arenaConfig.enemyBufferWidth_tiles;
|
|
const int midY = m_arenaConfig.heightTiles / 2;
|
|
|
|
// Team 1 HQ — ECS proxy entity, player faction (isEnemy=false).
|
|
{
|
|
const ParsedSurfaceMask hqParsed =
|
|
parseSurfaceMask(m_gameConfig.stations.hq.surfaceMask, Rotation::East);
|
|
const int anchorX = 0;
|
|
const int anchorY = midY - hqParsed.footprint.height() / 2;
|
|
const float hp = static_cast<float>(
|
|
m_gameConfig.stations.hq.hpFormula.evaluate(1.0));
|
|
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_team1HqEntity = m_admin.spawnStation(anchor, hqParsed.footprint, absCells,
|
|
hp, hp, false);
|
|
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
|
m_admin.addComponent<HqProxyComponent>(m_team1HqEntity);
|
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
|
}
|
|
|
|
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
|
|
{
|
|
const ParsedSurfaceMask hqParsed =
|
|
parseSurfaceMask(m_gameConfig.stations.hq.surfaceMask, Rotation::West);
|
|
const int anchorX = totalWidth - hqParsed.footprint.width();
|
|
const int anchorY = midY - hqParsed.footprint.height() / 2;
|
|
const float hp = static_cast<float>(
|
|
m_gameConfig.stations.hq.hpFormula.evaluate(1.0));
|
|
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);
|
|
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
|
m_admin.addComponent<HqProxyComponent>(m_team2HqEntity);
|
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
|
}
|
|
|
|
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
|
|
{
|
|
float hp = 0.0f;
|
|
WeaponComponent weapon;
|
|
weapon.cooldownTicks = 0.0f;
|
|
weapon.currentTarget = std::nullopt;
|
|
const double lv = static_cast<double>(entry.level);
|
|
const float tileSize = static_cast<float>(m_gameConfig.world.tileSize_m);
|
|
|
|
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_tiles = static_cast<float>(
|
|
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv)) / tileSize;
|
|
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_tiles = static_cast<float>(
|
|
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv)) / tileSize;
|
|
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);
|
|
{
|
|
entt::entity wChild = m_admin.createModuleEntity();
|
|
m_admin.addComponent<WeaponComponent>(wChild, weapon);
|
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
|
ModuleOwnerComponent{stationEntity});
|
|
}
|
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
|
};
|
|
|
|
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
|
|
{
|
|
placeArenaStation(entry, false);
|
|
}
|
|
for (const ArenaStationEntry& entry : m_arenaConfig.teams[1].stations)
|
|
{
|
|
placeArenaStation(entry, true);
|
|
}
|
|
}
|
|
|
|
void ArenaSimulation::spawnShips()
|
|
{
|
|
const int contestStart = m_arenaConfig.playerBufferWidth_tiles;
|
|
const int team2Start = contestStart + m_arenaConfig.contestZoneWidth_tiles;
|
|
const int totalWidth = team2Start + m_arenaConfig.enemyBufferWidth_tiles;
|
|
|
|
std::uniform_real_distribution<float> yDist(0.0f,
|
|
static_cast<float>(m_arenaConfig.heightTiles));
|
|
|
|
// Team 1: isEnemy=false, spawn in player buffer zone.
|
|
{
|
|
std::uniform_real_distribution<float> xDist(0.0f,
|
|
static_cast<float>(m_arenaConfig.playerBufferWidth_tiles));
|
|
|
|
for (const ArenaShipEntry& entry : m_arenaConfig.teams[0].ships)
|
|
{
|
|
for (int i = 0; i < entry.count; ++i)
|
|
{
|
|
const QVector2D pos(xDist(m_rng), yDist(m_rng));
|
|
m_shipSystem->spawn(entry.schematicId, pos, false,
|
|
entry.layout);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Team 2: isEnemy=true, spawn in enemy buffer zone.
|
|
{
|
|
std::uniform_real_distribution<float> xDist(
|
|
static_cast<float>(team2Start),
|
|
static_cast<float>(totalWidth));
|
|
|
|
for (const ArenaShipEntry& entry : m_arenaConfig.teams[1].ships)
|
|
{
|
|
for (int i = 0; i < entry.count; ++i)
|
|
{
|
|
const QVector2D pos(xDist(m_rng), yDist(m_rng));
|
|
m_shipSystem->spawn(entry.schematicId, pos, true,
|
|
entry.layout);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ArenaSimulation::run()
|
|
{
|
|
while (!m_finished && !m_stopRequested.load(std::memory_order_relaxed))
|
|
{
|
|
tick();
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_statusMutex);
|
|
m_status.finished = true;
|
|
}
|
|
}
|
|
|
|
void ArenaSimulation::requestStop()
|
|
{
|
|
m_stopRequested.store(true, std::memory_order_relaxed);
|
|
}
|
|
|
|
ArenaStatus ArenaSimulation::getStatus() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_statusMutex);
|
|
return m_status;
|
|
}
|
|
|
|
void ArenaSimulation::tick()
|
|
{
|
|
// Ship behavior systems (tick step 7): evaluate, select winner, execute.
|
|
// Module + combat systems emit their tool beams into a shared buffer.
|
|
m_shipSystem->clearMovementIntents();
|
|
m_aiSystem->tick(m_admin, m_factoryState, *m_debrisSystem);
|
|
std::vector<BeamFiredEvent> beamFiredEvents;
|
|
m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, m_factoryState, beamFiredEvents);
|
|
m_repairSystem->tick(m_currentTick, beamFiredEvents);
|
|
|
|
// Combat resolution (tick step 8).
|
|
m_combatSystem->tick(m_currentTick, m_admin, beamFiredEvents);
|
|
m_beamFiredEvents.insert(m_beamFiredEvents.end(), beamFiredEvents.begin(), beamFiredEvents.end());
|
|
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
|
|
|
// Deaths (tick step 9, simplified).
|
|
tickDeaths();
|
|
|
|
// Movement (tick step 10).
|
|
m_movementIntentSystem->tick(m_admin);
|
|
m_dynamicBodySystem->tick(m_admin);
|
|
|
|
// Scrap despawn (tick step 11).
|
|
m_debrisSystem->tickDespawn(m_currentTick);
|
|
|
|
++m_currentTick;
|
|
|
|
if (m_currentTick % 30 == 0)
|
|
{
|
|
updateStatus();
|
|
}
|
|
}
|
|
|
|
void ArenaSimulation::tickDeaths()
|
|
{
|
|
// Dead ships.
|
|
std::vector<entt::entity> deadShips;
|
|
m_admin.forEach<ShipIdentityComponent, HealthComponent>(
|
|
[&deadShips](entt::entity e, const ShipIdentityComponent& /*si*/,
|
|
const HealthComponent& h)
|
|
{
|
|
if (h.hp <= 0.0f)
|
|
{
|
|
deadShips.push_back(e);
|
|
}
|
|
});
|
|
|
|
for (entt::entity deadEntity : deadShips)
|
|
{
|
|
const ShipIdentityComponent& si = m_admin.get<ShipIdentityComponent>(deadEntity);
|
|
const PositionComponent& pos = m_admin.get<PositionComponent>(deadEntity);
|
|
if (si.scrapDrop > 0)
|
|
{
|
|
const Tick despawnAt = m_currentTick
|
|
+ secondsToTicks(m_gameConfig.world.debrisDespawnSeconds);
|
|
m_debrisSystem->spawn(pos.value, si.scrapDrop, despawnAt);
|
|
}
|
|
m_shipSystem->despawn(deadEntity);
|
|
}
|
|
|
|
// Dead stations.
|
|
std::vector<entt::entity> deadStations;
|
|
m_admin.forEach<StationBodyComponent, HealthComponent>(
|
|
[&deadStations](entt::entity e, const StationBodyComponent& /*sb*/,
|
|
const HealthComponent& h)
|
|
{
|
|
if (h.hp <= 0.0f)
|
|
{
|
|
deadStations.push_back(e);
|
|
}
|
|
});
|
|
|
|
for (entt::entity deadEntity : deadStations)
|
|
{
|
|
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
|
|
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
|
{
|
|
std::vector<entt::entity> stationChildren;
|
|
m_admin.forEach<ModuleOwnerComponent>(
|
|
[&](entt::entity ce, const ModuleOwnerComponent& o)
|
|
{
|
|
if (o.owner == deadEntity) { stationChildren.push_back(ce); }
|
|
});
|
|
for (entt::entity ce : stationChildren) { m_admin.destroy(ce); }
|
|
}
|
|
m_admin.destroy(deadEntity);
|
|
}
|
|
|
|
// Check end conditions — HQ proxy entities.
|
|
const bool team1HqGone = !m_admin.isValid(m_team1HqEntity)
|
|
|| m_admin.get<HealthComponent>(m_team1HqEntity).hp <= 0.0f;
|
|
const bool team2HqGone = !m_admin.isValid(m_team2HqEntity)
|
|
|| m_admin.get<HealthComponent>(m_team2HqEntity).hp <= 0.0f;
|
|
|
|
if (team1HqGone || team2HqGone)
|
|
{
|
|
m_finished = true;
|
|
m_winnerTeam = team1HqGone ? 1 : 0;
|
|
updateStatus();
|
|
return;
|
|
}
|
|
|
|
// Check if all ships and defence stations of one team are destroyed.
|
|
bool team1HasUnits = false;
|
|
bool team2HasUnits = false;
|
|
m_admin.forEach<ShipIdentityComponent, FactionComponent>(
|
|
[&team1HasUnits, &team2HasUnits](entt::entity /*e*/,
|
|
const ShipIdentityComponent& /*si*/,
|
|
const FactionComponent& f)
|
|
{
|
|
if (f.isEnemy) { team2HasUnits = true; }
|
|
else { team1HasUnits = true; }
|
|
});
|
|
|
|
m_admin.forEach<StationBodyComponent, FactionComponent>(
|
|
[this, &team1HasUnits, &team2HasUnits](entt::entity e,
|
|
const StationBodyComponent& /*sb*/,
|
|
const FactionComponent& f)
|
|
{
|
|
// The HQ carries a StationBodyComponent but is not a defence station;
|
|
// its destruction is a separate end condition (REQ-BAL-SIM-END).
|
|
if (m_admin.hasAll<HqProxyComponent>(e)) { return; }
|
|
if (f.isEnemy) { team2HasUnits = true; }
|
|
else { team1HasUnits = true; }
|
|
});
|
|
|
|
if (!team1HasUnits || !team2HasUnits)
|
|
{
|
|
m_finished = true;
|
|
m_winnerTeam = team1HasUnits ? 0 : 1;
|
|
updateStatus();
|
|
}
|
|
}
|
|
|
|
void ArenaSimulation::tickOnce()
|
|
{
|
|
if (!m_finished)
|
|
{
|
|
tick();
|
|
updateStatus();
|
|
}
|
|
}
|
|
|
|
std::vector<BeamFiredEvent> ArenaSimulation::drainBeamFiredEvents()
|
|
{
|
|
std::vector<BeamFiredEvent> result;
|
|
result.swap(m_beamFiredEvents);
|
|
return result;
|
|
}
|
|
|
|
bool ArenaSimulation::isFinished() const
|
|
{
|
|
return m_finished;
|
|
}
|
|
|
|
std::optional<int> ArenaSimulation::getWinnerTeam() const
|
|
{
|
|
return m_winnerTeam;
|
|
}
|
|
|
|
Tick ArenaSimulation::getCurrentTick() const
|
|
{
|
|
return m_currentTick;
|
|
}
|
|
|
|
const ArenaConfig& ArenaSimulation::getArenaConfig() const
|
|
{
|
|
return m_arenaConfig;
|
|
}
|
|
|
|
const FactoryState& ArenaSimulation::getFactoryState() const
|
|
{
|
|
return m_factoryState;
|
|
}
|
|
|
|
const BuildingSystem& ArenaSimulation::getBuildings() const
|
|
{
|
|
return *m_buildingSystem;
|
|
}
|
|
|
|
const ShipSystem& ArenaSimulation::getShips() const
|
|
{
|
|
return *m_shipSystem;
|
|
}
|
|
|
|
const DebrisSystem& ArenaSimulation::getDebrisSystem() const
|
|
{
|
|
return *m_debrisSystem;
|
|
}
|
|
|
|
EntityAdmin& ArenaSimulation::getAdmin()
|
|
{
|
|
return m_admin;
|
|
}
|
|
|
|
const EntityAdmin& ArenaSimulation::getAdmin() const
|
|
{
|
|
return m_admin;
|
|
}
|
|
|
|
void ArenaSimulation::updateStatus()
|
|
{
|
|
ArenaStatus newStatus;
|
|
newStatus.finished = m_finished;
|
|
newStatus.winnerTeam = m_winnerTeam;
|
|
newStatus.durationSeconds = ticksToSeconds(m_currentTick);
|
|
|
|
// Live remaining HP of each team's ships and defence stations (HQ excluded);
|
|
// the EHP-percentage numerator (denominator is the fixed m_teamMaxEhp).
|
|
double currentEhp[2] = {0.0, 0.0};
|
|
m_admin.forEach<ShipIdentityComponent, FactionComponent, HealthComponent>(
|
|
[¤tEhp](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
|
const FactionComponent& f, const HealthComponent& h)
|
|
{
|
|
if (h.hp > 0.0f)
|
|
{
|
|
currentEhp[f.isEnemy ? 1 : 0] += static_cast<double>(h.hp);
|
|
}
|
|
});
|
|
m_admin.forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
|
[this, ¤tEhp](entt::entity e, const StationBodyComponent& /*sb*/,
|
|
const FactionComponent& f, const HealthComponent& h)
|
|
{
|
|
if (m_admin.hasAll<HqProxyComponent>(e))
|
|
{
|
|
return;
|
|
}
|
|
if (h.hp > 0.0f)
|
|
{
|
|
currentEhp[f.isEnemy ? 1 : 0] += static_cast<double>(h.hp);
|
|
}
|
|
});
|
|
|
|
for (int ti = 0; ti < 2; ++ti)
|
|
{
|
|
ArenaStatus::TeamStatus& teamStatus = newStatus.teams[ti];
|
|
teamStatus.name = m_arenaConfig.teams[ti].name;
|
|
teamStatus.threatLevel = m_teamThreat[ti];
|
|
teamStatus.currentEhp = currentEhp[ti];
|
|
teamStatus.maxEhp = m_teamMaxEhp[ti];
|
|
|
|
// HQ entry (always first).
|
|
{
|
|
ArenaStatus::Entry hqEntry;
|
|
hqEntry.displayName = "HQ";
|
|
hqEntry.level = 1;
|
|
hqEntry.total = 1;
|
|
const entt::entity hqEntity = (ti == 0) ? m_team1HqEntity : m_team2HqEntity;
|
|
hqEntry.surviving = (m_admin.isValid(hqEntity)
|
|
&& m_admin.get<HealthComponent>(hqEntity).hp > 0.0f) ? 1 : 0;
|
|
teamStatus.entries.push_back(hqEntry);
|
|
}
|
|
|
|
// Ship entries.
|
|
for (const ArenaShipEntry& shipEntry : m_arenaConfig.teams[ti].ships)
|
|
{
|
|
ArenaStatus::Entry entry;
|
|
entry.displayName = shipEntry.schematicId;
|
|
// Ships no longer carry a level (level suffix stays empty).
|
|
entry.total = shipEntry.count;
|
|
|
|
int surviving = 0;
|
|
const bool isEnemyTeam = (ti == 1);
|
|
m_admin.forEach<ShipIdentityComponent, FactionComponent, HealthComponent>(
|
|
[&surviving, &shipEntry, isEnemyTeam](entt::entity /*e*/,
|
|
const ShipIdentityComponent& si, const FactionComponent& f,
|
|
const HealthComponent& h)
|
|
{
|
|
if (f.isEnemy == isEnemyTeam
|
|
&& si.schematicId == shipEntry.schematicId
|
|
&& h.hp > 0.0f)
|
|
{
|
|
++surviving;
|
|
}
|
|
});
|
|
entry.surviving = surviving;
|
|
teamStatus.entries.push_back(entry);
|
|
}
|
|
|
|
// Station entries.
|
|
for (std::size_t si = 0; si < m_arenaConfig.teams[ti].stations.size(); ++si)
|
|
{
|
|
const ArenaStationEntry& stationEntry = m_arenaConfig.teams[ti].stations[si];
|
|
|
|
ArenaStatus::Entry entry;
|
|
entry.displayName = "Station";
|
|
entry.level = stationEntry.level;
|
|
entry.total = 1;
|
|
|
|
int surviving = 0;
|
|
const bool isEnemyTeam = (ti == 1);
|
|
m_admin.forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
|
[&surviving, &stationEntry, isEnemyTeam](entt::entity /*e*/,
|
|
const StationBodyComponent& sb, const FactionComponent& f,
|
|
const HealthComponent& h)
|
|
{
|
|
if (f.isEnemy == isEnemyTeam
|
|
&& sb.anchor == stationEntry.position
|
|
&& h.hp > 0.0f)
|
|
{
|
|
surviving = 1;
|
|
}
|
|
});
|
|
entry.surviving = surviving;
|
|
teamStatus.entries.push_back(entry);
|
|
}
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(m_statusMutex);
|
|
m_status = newStatus;
|
|
}
|