getAllDebrisInfo and collectOne only ever touched EntityAdmin — DebrisSystem holds nothing else — so they become free functions over the registry. That lets AiSystem, SalvagerSystem and SalvageScrapEvaluator drop their DebrisSystem& parameters entirely; SalvagerSystem already held the admin, and the other two were handed it alongside. No system in lib/ecs/system takes another system now. Every tick signature names the data it works on: the registry, the factory state, or both. DebrisSystem keeps spawn, tickDespawn and consume — the first two are genuine tick behaviour rather than lookups. 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
633 lines
22 KiB
C++
633 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_factoryState = makeFactoryState(m_gameConfig);
|
|
|
|
m_buildingSystem = std::make_unique<BuildingSystem>(
|
|
m_gameConfig,
|
|
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(m_factoryState, 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(m_factoryState, 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(m_factoryState, 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);
|
|
std::vector<BeamFiredEvent> beamFiredEvents;
|
|
m_salvagerSystem->tick(m_currentTick, 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(m_factoryState, 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;
|
|
}
|