#include "Simulation.h" #include #include "AiSystem.h" #include "BuildingSystem.h" #include "EcsComponents.h" #include "CombatSystem.h" #include "MovementSystem.h" #include "ScrapSystem.h" #include "ShipSystem.h" #include "SurfaceMask.h" #include "WaveSystem.h" Simulation::Simulation(GameConfig config, unsigned int seed) : m_config(std::move(config)) , m_rng(seed) , m_currentTick(0) , m_nextDepartureTick(secondsToTicks(m_config.world.departureIntervalSeconds)) , m_nextId(1) , m_buildingBlocksStock(m_config.world.startingBuildingBlocks) , m_gameOver(false) , m_hqBuildingId(kInvalidEntityId) , m_hqProxyEntity(entt::null) , m_playerStation1Entity(entt::null) , m_playerStation2Entity(entt::null) , m_beltSystem(m_config.world.beltSpeedTilesPerSecond) { m_currentEnemyStationEntities[0] = entt::null; m_currentEnemyStationEntities[1] = entt::null; m_buildingSystem = std::make_unique( m_config, m_beltSystem, [this]() { return allocateId(); }, [this](int amount) { m_buildingBlocksStock += amount; }, [this](const std::string& id, QVector2D pos, const std::optional& layout) { const std::map::const_iterator it = m_schematicLevels.find(id); if (it == m_schematicLevels.end() || !it->second.unlocked) { return; } m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout); }, m_rng); m_shipSystem = std::make_unique(m_config, m_admin); m_aiSystem = std::make_unique(); m_movementSystem = std::make_unique(); m_scrapSystem = std::make_unique(m_admin); m_waveSystem = std::make_unique(m_config, m_rng); m_combatSystem = std::make_unique(m_config); // Initialize schematic unlock state. for (const ShipDef& def : m_config.ships.ships) { SchematicState state; state.unlocked = def.availableFromStart; state.level = def.availableFromStart ? def.schematic.playerProductionLevel : 0; m_schematicLevels[def.id] = state; } placeInitialStructures(); } Simulation::~Simulation() = default; const GameConfig& Simulation::config() const { return m_config; } void Simulation::reset(GameConfig newConfig, unsigned int seed) { m_config = std::move(newConfig); reset(seed); } void Simulation::reset(unsigned int seed) { m_rng.seed(seed); m_currentTick = 0; m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds); m_nextId = 1; m_buildingBlocksStock = m_config.world.startingBuildingBlocks; 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( m_config, m_beltSystem, [this]() { return allocateId(); }, [this](int amount) { m_buildingBlocksStock += amount; }, [this](const std::string& id, QVector2D pos, const std::optional& layout) { const std::map::const_iterator it = m_schematicLevels.find(id); if (it == m_schematicLevels.end() || !it->second.unlocked) { return; } m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout); }, m_rng); m_shipSystem = std::make_unique(m_config, m_admin); m_aiSystem = std::make_unique(); m_movementSystem = std::make_unique(); m_scrapSystem = std::make_unique(m_admin); m_waveSystem = std::make_unique(m_config, m_rng); m_combatSystem = std::make_unique(m_config); m_schematicLevels.clear(); for (const ShipDef& def : m_config.ships.ships) { SchematicState state; state.unlocked = def.availableFromStart; state.level = def.availableFromStart ? def.schematic.playerProductionLevel : 0; m_schematicLevels[def.id] = state; } placeInitialStructures(); } // --------------------------------------------------------------------------- // tick // --------------------------------------------------------------------------- void Simulation::tick() { // Step 1: wave scheduler m_waveSystem->tickWaveScheduler(m_currentTick, *m_shipSystem, m_config.world.heightTiles); // Step 2: threat accumulation m_waveSystem->tickThreatAccumulation(m_currentTick); // Construction + production pipeline m_buildingSystem->tickConstruction(m_currentTick); m_buildingSystem->tickBeltPull(); // step 3 m_buildingSystem->tickProduction(m_currentTick); // step 4 m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b m_buildingSystem->tickBeltPush(); // step 5 m_beltSystem.tick(); // step 6 // Step 7: ship behavior systems (movement arbitration via intent priority) // Departure timer: release gathered combat ships on a fixed interval (REQ-SHP-RALLY). if (m_currentTick >= m_nextDepartureTick) { m_shipSystem->triggerRallyDeparture(); m_nextDepartureTick += secondsToTicks(m_config.world.departureIntervalSeconds); } m_shipSystem->clearMovementIntents(); 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_admin, *m_buildingSystem, m_fireEvents); // Step 8b: deferred damage whose impact tick has arrived m_combatSystem->applyPendingDamage(m_currentTick, m_admin); // Step 9: deaths & loot if (!m_gameOver) { tickDeathsAndLoot(); } // Step 10: advance ship positions m_movementSystem->tick(m_admin); // Step 11: scrap despawn m_scrapSystem->tickDespawn(m_currentTick); ++m_currentTick; } // --------------------------------------------------------------------------- // Pre-placement // --------------------------------------------------------------------------- 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(); const int hqAnchorY = (m_config.world.heightTiles - hqParsed.footprint.height()) / 2; const float hqHp = static_cast(m_config.stations.hq.hpFormula.evaluate(0.0)); m_hqBuildingId = m_buildingSystem->placeImmediate( BuildingType::Hq, m_config.stations.hq.surfaceMask, QPoint(hqAnchorX, hqAnchorY), Rotation::East, hqHp, hqHp); 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 = m_config.world.regions.playerBufferWidth - psParsed.footprint.width(); const double psLevel = static_cast(m_config.stations.playerStation.level); const float psHp = static_cast( m_config.stations.playerStation.hpFormula.evaluate(psLevel)); StationWeapon psWeapon; psWeapon.damage = static_cast( m_config.stations.playerStation.damageFormula.evaluate(psLevel)); psWeapon.range = static_cast( m_config.stations.playerStation.rangeFormula.evaluate(psLevel)); psWeapon.fireRateHz = static_cast( 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; { const QPoint anchor(psAnchorX, ps1Y); std::vector 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(m_playerStation1Entity, psWeapon); m_buildingSystem->registerTileOccupancy(absCells, allocateId()); } { const QPoint anchor(psAnchorX, ps2Y); std::vector 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(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(psAnchorX) + psParsed.footprint.width() / 2.0f; const float rallyY = static_cast(m_config.world.heightTiles) / 2.0f; m_shipSystem->setRallyPoint(QVector2D(rallyX, rallyY)); // Enemy defence stations — generation 0 (initial set). placeEnemyStationSet(0); } void Simulation::placeEnemyStationSet(int generation) { const ParsedSurfaceMask esParsed = parseSurfaceMask(m_config.stations.enemyStation.surfaceMask, Rotation::East); const int rightEdgeX = m_config.world.regions.playerBufferWidth + m_config.world.regions.contestZoneWidth + generation * m_config.world.push.pushExpandColumns; const int anchorX = rightEdgeX - esParsed.footprint.width(); const double genD = static_cast(generation); const float esHp = static_cast( m_config.stations.enemyStation.hpFormula.evaluate(genD)); StationWeapon esWeapon; esWeapon.damage = static_cast( m_config.stations.enemyStation.damageFormula.evaluate(genD)); esWeapon.range = static_cast( m_config.stations.enemyStation.rangeFormula.evaluate(genD)); esWeapon.fireRateHz = static_cast( 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 QPoint anchor(anchorX, y1); std::vector 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(m_currentEnemyStationEntities[0], esWeapon); m_buildingSystem->registerTileOccupancy(absCells, allocateId()); } { const QPoint anchor(anchorX, y2); std::vector 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(m_currentEnemyStationEntities[1], esWeapon); m_buildingSystem->registerTileOccupancy(absCells, allocateId()); } } // --------------------------------------------------------------------------- // Deaths & loot (tick step 9) // --------------------------------------------------------------------------- void Simulation::tickDeathsAndLoot() { // --- Dead ships --- std::vector deadShips; m_admin.forEach( [&deadShips](entt::entity e, const ShipIdentity& /*si*/, const Health& h) { if (h.hp <= 0.0f) { deadShips.push_back(e); } }); for (entt::entity deadEntity : deadShips) { const ShipIdentity& si = m_admin.get(deadEntity); const Position& pos = m_admin.get(deadEntity); for (const ShipDef& def : m_config.ships.ships) { if (def.id == si.schematicId && def.loot.scrapDrop > 0) { const Tick despawnAt = m_currentTick + secondsToTicks(m_config.world.scrapDespawnSeconds); m_scrapSystem->spawn(pos.value, def.loot.scrapDrop, despawnAt); break; } } m_shipSystem->despawn(deadEntity); } // --- Dead stations --- std::vector deadStations; m_admin.forEach( [&deadStations](entt::entity e, const StationBody& /*sb*/, const Health& h) { if (h.hp <= 0.0f) { deadStations.push_back(e); } }); for (entt::entity deadEntity : deadStations) { const StationBody& sb = m_admin.get(deadEntity); const Position& pos = m_admin.get(deadEntity); const Faction& fac = m_admin.get(deadEntity); const Tick despawnAt = m_currentTick + secondsToTicks(m_config.world.scrapDespawnSeconds); int scrap = 0; if (!fac.isEnemy) { const double lv = static_cast( m_config.stations.playerStation.level); scrap = static_cast( m_config.stations.playerStation.scrapDropFormula.evaluate(lv)); } else { const double genD = static_cast(m_waveSystem->generation()); scrap = static_cast( 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(m_hqProxyEntity); if (hqHealth.hp <= 0.0f) { m_gameOver = true; } } // --- Push check: if both current enemy stations are gone, trigger push --- const bool es0Gone = !m_admin.isValid(m_currentEnemyStationEntities[0]) || m_admin.get(m_currentEnemyStationEntities[0]).hp <= 0.0f; const bool es1Gone = !m_admin.isValid(m_currentEnemyStationEntities[1]) || m_admin.get(m_currentEnemyStationEntities[1]).hp <= 0.0f; if (es0Gone && es1Gone && m_currentEnemyStationEntities[0] != entt::null) { m_waveSystem->applyPush(); placeEnemyStationSet(m_waveSystem->generation()); awardSchematicDrop(); } } void Simulation::awardSchematicDrop() { std::vector ids; ids.reserve(m_config.ships.ships.size()); for (const ShipDef& def : m_config.ships.ships) { ids.push_back(def.id); } std::uniform_int_distribution dist(0, static_cast(ids.size()) - 1); const std::string chosen = ids[static_cast(dist(m_rng))]; SchematicState& state = m_schematicLevels.at(chosen); const bool wasNew = !state.unlocked; state.unlocked = true; state.level += 1; SchematicDropEvent evt; evt.schematicId = chosen; evt.newLevel = state.level; evt.wasNewUnlock = wasNew; m_schematicDropEvents.push_back(evt); } // --------------------------------------------------------------------------- // Drains // --------------------------------------------------------------------------- std::vector Simulation::drainFireEvents() { std::vector result; result.swap(m_fireEvents); return result; } std::vector Simulation::drainSchematicDropEvents() { std::vector result; result.swap(m_schematicDropEvents); return result; } // --------------------------------------------------------------------------- // Accessors // --------------------------------------------------------------------------- Tick Simulation::currentTick() const { return m_currentTick; } int Simulation::buildingBlocksStock() const { return m_buildingBlocksStock; } bool Simulation::isGameOver() const { return m_gameOver; } double Simulation::threatLevel() const { return m_waveSystem->threatLevel(); } int Simulation::schematicLevel(const std::string& shipId) const { const std::map::const_iterator it = m_schematicLevels.find(shipId); if (it == m_schematicLevels.end()) { return 0; } return it->second.level; } bool Simulation::isSchematicUnlocked(const std::string& shipId) const { const std::map::const_iterator it = m_schematicLevels.find(shipId); if (it == m_schematicLevels.end()) { return false; } return it->second.unlocked; } EntityId Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation) { int cost = 0; for (const BuildingDef& def : m_config.buildings.buildings) { if (def.type == type) { cost = def.cost; break; } } if (m_buildingBlocksStock < cost) { return kInvalidEntityId; } m_buildingBlocksStock -= cost; return m_buildingSystem->place(type, anchor, rotation, m_currentTick); } void Simulation::demolish(EntityId id) { m_buildingBlocksStock += m_buildingSystem->demolish(id); } BuildingSystem& Simulation::buildings() { return *m_buildingSystem; } const BuildingSystem& Simulation::buildings() const { return *m_buildingSystem; } BeltSystem& Simulation::belts() { return m_beltSystem; } const BeltSystem& Simulation::belts() const { return m_beltSystem; } ShipSystem& Simulation::ships() { return *m_shipSystem; } const ShipSystem& Simulation::ships() const { return *m_shipSystem; } ScrapSystem& Simulation::scraps() { return *m_scrapSystem; } 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++; }