#include "ArenaSimulation.h" #include #include #include #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 "ScrapSystem.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_winnerTeam(-1) , m_stopRequested(false) { m_buildingSystem = std::make_unique( m_gameConfig, m_beltSystem, [this]() { return allocateBuildingId(); }, [](int) {}, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, m_rng); m_shipSystem = std::make_unique(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(m_gameConfig); m_movementIntentSystem = std::make_unique(); m_dynamicBodySystem = std::make_unique(); m_combatSystem = std::make_unique(m_gameConfig); m_scrapSystem = std::make_unique(m_admin); m_salvagerSystem = std::make_unique(m_admin); m_repairSystem = std::make_unique(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& modules = shipEntry.layout ? shipEntry.layout->placedModules : std::vector{}; const double shipThreat = calculateShipThreatCost( m_gameConfig.threatCosts, m_gameConfig, shipEntry.schematicId, modules); teamThreat += shipThreat * shipEntry.count; } m_teamThreat[ti] = teamThreat; } placeStructures(); spawnShips(); m_shipSystem->triggerRallyDeparture(); updateStatus(); } 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( m_gameConfig.stations.hq.hpFormula.evaluate(1.0)); const QPoint anchor(anchorX, anchorY); std::vector 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(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( m_gameConfig.stations.hq.hpFormula.evaluate(1.0)); const QPoint anchor(anchorX, anchorY); std::vector 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(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(entry.level); const float tileSize = static_cast(m_gameConfig.world.tileSize_m); const std::vector& mask = isEnemy ? m_gameConfig.stations.enemyStation.surfaceMask : m_gameConfig.stations.playerStation.surfaceMask; if (entry.stationType == "player_station") { hp = static_cast( m_gameConfig.stations.playerStation.hpFormula.evaluate(lv)); weapon.damage = static_cast( m_gameConfig.stations.playerStation.damageFormula.evaluate(lv)); weapon.range_tiles = static_cast( m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv)) / tileSize; weapon.fireRateHz = static_cast( m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv)); } else { hp = static_cast( m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv)); weapon.damage = static_cast( m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv)); weapon.range_tiles = static_cast( m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv)) / tileSize; weapon.fireRateHz = static_cast( m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv)); } const ParsedSurfaceMask parsed = parseSurfaceMask(mask, Rotation::East); const QPoint& anchor = entry.position; std::vector 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(wChild, weapon); m_admin.addComponent(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 yDist(0.0f, static_cast(m_arenaConfig.heightTiles)); // Team 1: isEnemy=false, spawn in player buffer zone. { std::uniform_real_distribution xDist(0.0f, static_cast(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 xDist( static_cast(team2Start), static_cast(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 lock(m_statusMutex); m_status.finished = true; } } void ArenaSimulation::requestStop() { m_stopRequested.store(true, std::memory_order_relaxed); } ArenaStatus ArenaSimulation::status() const { std::lock_guard 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_buildingSystem, *m_scrapSystem); std::vector beamFiredEvents; m_salvagerSystem->tick(m_currentTick, *m_scrapSystem, *m_buildingSystem, beamFiredEvents); m_repairSystem->tick(m_currentTick, beamFiredEvents); // Combat resolution (tick step 8). m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, 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_scrapSystem->tickDespawn(m_currentTick); ++m_currentTick; if (m_currentTick % 30 == 0) { updateStatus(); } } void ArenaSimulation::tickDeaths() { // Dead ships. std::vector deadShips; m_admin.forEach( [&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(deadEntity); const PositionComponent& pos = m_admin.get(deadEntity); if (si.scrapDrop > 0) { const Tick despawnAt = m_currentTick + secondsToTicks(m_gameConfig.world.scrapDespawnSeconds); m_scrapSystem->spawn(pos.value, si.scrapDrop, despawnAt); } m_shipSystem->despawn(deadEntity); } // Dead stations. std::vector deadStations; m_admin.forEach( [&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(deadEntity); m_buildingSystem->unregisterTileOccupancy(sb.bodyCells); { std::vector stationChildren; m_admin.forEach( [&](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(m_team1HqEntity).hp <= 0.0f; const bool team2HqGone = !m_admin.isValid(m_team2HqEntity) || m_admin.get(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( [&team1HasUnits, &team2HasUnits](entt::entity /*e*/, const ShipIdentityComponent& /*si*/, const FactionComponent& f) { if (f.isEnemy) { team2HasUnits = true; } else { team1HasUnits = true; } }); m_admin.forEach( [&team1HasUnits, &team2HasUnits](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f) { 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 ArenaSimulation::drainBeamFiredEvents() { std::vector result; result.swap(m_beamFiredEvents); return result; } bool ArenaSimulation::isFinished() const { return m_finished; } int ArenaSimulation::winnerTeam() const { return m_winnerTeam; } Tick ArenaSimulation::currentTick() const { return m_currentTick; } const ArenaConfig& ArenaSimulation::arenaConfig() const { return m_arenaConfig; } const BuildingSystem& ArenaSimulation::buildings() const { return *m_buildingSystem; } const ShipSystem& ArenaSimulation::ships() const { return *m_shipSystem; } 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; newStatus.finished = m_finished; newStatus.winnerTeam = m_winnerTeam; 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]; // 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(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( [&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( [&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 lock(m_statusMutex); m_status = newStatus; }