#include "Simulation.h" #include "FactoryQueries.h" #include "PlacementRules.h" #include #include #include #include "AiSystem.h" #include "Command.h" #include "BuildingSystem.h" #include "CombatSystem.h" #include "DynamicBodyComponent.h" #include "DynamicBodySystem.h" #include "FacingComponent.h" #include "FactionComponent.h" #include "EventManager.h" #include "HealthComponent.h" #include "ModuleOwnerComponent.h" #include "MovementIntentSystem.h" #include "PositionComponent.h" #include "RepairSystem.h" #include "SalvagerSystem.h" #include "DebrisComponent.h" #include "DebrisSystem.h" #include "ShipIdentityComponent.h" #include "ShipSystem.h" #include "StateChecksum.h" #include "StationBodyComponent.h" #include "SurfaceMask.h" #include "tracing.h" #include "WaveSystem.h" #include "WeaponComponent.h" Simulation::Simulation(GameConfig config, unsigned int seed) : m_config(std::move(config)) , m_rng(seed) , m_seed(seed) , m_currentTick(0) , m_nextDepartureTick(secondsToTicks(m_config.world.departureIntervalSeconds)) , m_nextBuildingId(1) , m_buildingBlocksStock(m_config.world.startingBuildingBlocks) , m_gameOver(false) , m_hqProxyEntity(entt::null) , m_playerStation1Entity(entt::null) , m_playerStation2Entity(entt::null) , m_unlockState(m_config) , m_beltSystem(m_config.world.beltSpeed_tps) { m_currentEnemyStationEntities[0] = entt::null; m_currentEnemyStationEntities[1] = entt::null; m_factoryState = makeFactoryState(m_config); initializeSubsystems(); m_unlockState.initializeUnlockState(); placeInitialStructures(); registerForEvents(); } Simulation::~Simulation() { unregisterForEvents(); } const GameConfig& Simulation::getConfig() 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) { EventManager::getInstance()->clearEvents(); m_rng.seed(seed); m_seed = seed; m_currentTick = 0; m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds); m_nextBuildingId = 1; m_buildingBlocksStock = m_config.world.startingBuildingBlocks; m_expansionsPurchased = 0; m_gameOver = false; m_isWon = false; m_artifactCount = 0; m_hqBuildingId = std::nullopt; m_hqProxyEntity = entt::null; m_playerStation1Entity = entt::null; m_playerStation2Entity = entt::null; m_currentEnemyStationEntities[0] = entt::null; m_currentEnemyStationEntities[1] = entt::null; m_beamFiredEvents.clear(); m_pendingSchematicChoices.clear(); m_admin.clear(); m_factoryState = makeFactoryState(m_config); m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps); initializeSubsystems(); m_unlockState.initializeUnlockState(); placeInitialStructures(); } void Simulation::initializeSubsystems() { m_buildingSystem = std::make_unique( m_config, m_beltSystem, [this]() { return allocateBuildingId(); }, [this](int amount) { m_buildingBlocksStock += amount; }, [this](const std::string& id, QVector2D pos, const std::optional& layout) { if (!isSchematicUnlocked(id)) { return; } m_shipSystem->spawn(id, pos, /*isEnemy=*/false, layout); }, [this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); }, m_rng); m_shipSystem = std::make_unique(m_config, m_admin); m_aiSystem = std::make_unique(m_config); m_movementIntentSystem = std::make_unique(); m_dynamicBodySystem = std::make_unique(); m_debrisSystem = std::make_unique(m_admin); m_salvagerSystem = std::make_unique(m_admin); m_repairSystem = std::make_unique(m_admin); m_waveSystem = std::make_unique(m_config, m_rng); m_combatSystem = std::make_unique(m_config); } // --------------------------------------------------------------------------- // tick // --------------------------------------------------------------------------- void Simulation::apply(const Command& command) { switch (command.kind) { case CommandKind::PlaceBuilding: { const PlaceBuildingCommand& c = static_cast(command); const std::optional placed = tryPlaceBuilding(c.type, c.anchor, c.rotation); if (!placed.has_value()) { break; } const BuildingId id = *placed; if (c.recipeId.has_value()) { m_buildingSystem->setRecipe(m_factoryState, id, *c.recipeId); } if (c.shipLayout.has_value()) { m_buildingSystem->setShipLayout(m_factoryState, id, *c.shipLayout); } if (c.hasSplitterFilters) { m_buildingSystem->setSiteSplitterFilters(m_factoryState, id, c.splitterFilterA, c.splitterFilterB); } break; } case CommandKind::Deconstruct: deconstruct(*static_cast(command).id); break; case CommandKind::CancelDeconstruction: cancelDeconstruction(*static_cast(command).id); break; case CommandKind::RotateInPlace: { const RotateInPlaceCommand& c = static_cast(command); m_buildingSystem->rotateInPlace(m_factoryState, *c.id, c.newRotation); break; } case CommandKind::SetRecipe: { const SetRecipeCommand& c = static_cast(command); m_buildingSystem->setRecipe(m_factoryState, *c.id, c.recipeId); break; } case CommandKind::SetShipLayout: { const SetShipLayoutCommand& c = static_cast(command); m_buildingSystem->setShipLayout(m_factoryState, *c.id, c.layout); break; } case CommandKind::SetSiteSplitterFilters: { const SetSiteSplitterFiltersCommand& c = static_cast(command); m_buildingSystem->setSiteSplitterFilters(m_factoryState, *c.id, c.filterA, c.filterB); break; } case CommandKind::SetSplitterFilters: { const SetSplitterFiltersCommand& c = static_cast(command); m_beltSystem.setSplitterFilters(c.tile, c.filterA, c.filterB); break; } case CommandKind::ClearBeltTiles: m_beltSystem.clearTiles(static_cast(command).tiles); break; case CommandKind::ApplySchematicChoice: applySchematicChoice(static_cast(command).choiceIndex); break; case CommandKind::ExpandAsteroid: tryExpandAsteroid(); break; case CommandKind::Reset: { const ResetCommand& c = static_cast(command); if (c.config) { // operator* on a const shared_ptr yields a mutable GameConfig&, so // the move-only config moves into reset without a copy. The command // is applied once, so leaving its config moved-from is fine. reset(std::move(*c.config), c.seed); } else { reset(c.seed); } break; } } } void Simulation::tick() { EventManager::getInstance()->processEvents(); // Step 1: wave scheduler m_waveSystem->tickWaveScheduler(m_currentTick, *m_shipSystem, m_config.world.heightTiles); // Step 2: threat accumulation m_waveSystem->tickThreatAccumulation(); // Construction + production pipeline m_buildingSystem->tickConstruction(m_factoryState, m_currentTick); m_buildingSystem->tickDeconstruction(m_factoryState, m_currentTick); // parallel to construction m_buildingSystem->tickBeltPull(m_factoryState); // step 3 m_buildingSystem->tickProduction(m_factoryState, m_currentTick); // step 4 m_buildingSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b m_buildingSystem->tickOutputBelts(m_factoryState); // 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(); // Score-based behavior selection: evaluate, select winner, execute (sets // movement intent + preferred module targets only — no world mutation). m_aiSystem->tick(m_admin, m_factoryState, *m_debrisSystem); // Module systems perform the world mutation (collection/delivery, healing). // Each emits its tool beams and applies its own delayed (mid-beam) effects. m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, m_factoryState, m_beamFiredEvents); m_repairSystem->tick(m_currentTick, m_beamFiredEvents); // Step 8: combat resolution m_combatSystem->tick(m_currentTick, m_admin, m_beamFiredEvents); // 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_movementIntentSystem->tick(m_admin); m_dynamicBodySystem->tick(m_admin); // Step 11: debris despawn m_debrisSystem->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(m_factoryState, BuildingType::Hq, m_config.stations.hq.surfaceMask, QPoint(hqAnchorX, hqAnchorY), Rotation::East); 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_tiles - 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)); const float tileSize = static_cast(m_config.world.tileSize_m); WeaponComponent psWeapon; psWeapon.damage = static_cast( m_config.stations.playerStation.damageFormula.evaluate(psLevel)); psWeapon.range_tiles = static_cast( m_config.stations.playerStation.rangeFormula.evaluate(psLevel)) / tileSize; 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); { entt::entity wChild = m_admin.createModuleEntity(); m_admin.addComponent(wChild, psWeapon); m_admin.addComponent(wChild, ModuleOwnerComponent{m_playerStation1Entity}); } m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); } { 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); { entt::entity wChild = m_admin.createModuleEntity(); m_admin.addComponent(wChild, psWeapon); m_admin.addComponent(wChild, ModuleOwnerComponent{m_playerStation2Entity}); } m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); } // 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 float tileSize = static_cast(m_config.world.tileSize_m); const ParsedSurfaceMask esParsed = parseSurfaceMask(m_config.stations.enemyStation.surfaceMask, Rotation::East); const int rightEdgeX = m_config.world.regions.playerBufferWidth_tiles + m_config.world.regions.contestZoneWidth_tiles + generation * m_config.world.push.pushExpandColumns_tiles; 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)); WeaponComponent esWeapon; esWeapon.damage = static_cast( m_config.stations.enemyStation.damageFormula.evaluate(genD)); esWeapon.range_tiles = static_cast( m_config.stations.enemyStation.rangeFormula.evaluate(genD)) / tileSize; 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); { entt::entity wChild = m_admin.createModuleEntity(); m_admin.addComponent(wChild, esWeapon); m_admin.addComponent(wChild, ModuleOwnerComponent{m_currentEnemyStationEntities[0]}); } m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); } { 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); { entt::entity wChild = m_admin.createModuleEntity(); m_admin.addComponent(wChild, esWeapon); m_admin.addComponent(wChild, ModuleOwnerComponent{m_currentEnemyStationEntities[1]}); } m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId()); } } // --------------------------------------------------------------------------- // Deaths & loot (tick step 9) // --------------------------------------------------------------------------- void Simulation::tickDeathsAndLoot() { TRACE(); // --- 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_config.world.debrisDespawnSeconds); m_debrisSystem->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); const PositionComponent& pos = m_admin.get(deadEntity); const FactionComponent& fac = m_admin.get(deadEntity); const Tick despawnAt = m_currentTick + secondsToTicks(m_config.world.debrisDespawnSeconds); 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->getGeneration()); scrap = static_cast( m_config.stations.enemyStation.scrapDropFormula.evaluate(genD)); } if (scrap > 0) { m_debrisSystem->spawn(pos.value, scrap, despawnAt); } m_buildingSystem->unregisterTileOccupancy(m_factoryState, 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); } // --- HQ death check --- if (m_admin.isValid(m_hqProxyEntity)) { const HealthComponent& 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) { const int destroyedLevel = m_waveSystem->getGeneration(); m_waveSystem->onEnemyStationsDestroyed(); placeEnemyStationSet(m_waveSystem->getGeneration()); generateSchematicChoices(destroyedLevel); } } void Simulation::generateSchematicChoices(int destroyedStationLevel) { // Build the eligible pool of unlock groups (REQ-DEF-SCHEMATIC-DROP, // REQ-LOCK-EXPLICIT, REQ-LOCK-PREREQ): not yet awarded, station level in // range, and every prerequisite group already awarded. std::vector pool; for (const UnlockGroupDef& group : m_config.unlocks.groups) { if (m_unlockState.isUnlockGroupAwarded(group.id)) { continue; } if (group.stationLevel < 0 || group.stationLevel > destroyedStationLevel) { continue; } if (!m_unlockState.prerequisitesSatisfied(group.requiredGroupIds)) { continue; } pool.push_back(&group); } if (pool.empty()) { return; } const double artifactChance = std::clamp( m_config.world.artifacts.artifactChanceFormula.evaluate( static_cast(destroyedStationLevel)), 0.0, 1.0); std::uniform_real_distribution realDist(0.0, 1.0); const bool artifactRolled = realDist(m_rng) < artifactChance; const int numChoices = std::min(static_cast(pool.size()), artifactRolled ? 2 : 3); m_pendingSchematicChoices.clear(); for (int i = 0; i < numChoices; ++i) { std::uniform_int_distribution dist(0, static_cast(pool.size()) - 1 - i); const int roll = dist(m_rng); const std::size_t rollIdx = static_cast(roll); const std::size_t endIdx = pool.size() - 1 - static_cast(i); std::swap(pool[rollIdx], pool[endIdx]); m_pendingSchematicChoices.push_back(m_unlockState.makeUnlockOption(*pool[endIdx])); } if (artifactRolled) { SchematicChoiceOption artifactOption; artifactOption.isArtifact = true; artifactOption.displayName = "Artifact"; m_pendingSchematicChoices.push_back(std::move(artifactOption)); } } void Simulation::applySchematicChoice(int choiceIndex) { assert(choiceIndex >= 0 && choiceIndex < static_cast(m_pendingSchematicChoices.size())); const SchematicChoiceOption& chosen = m_pendingSchematicChoices[static_cast(choiceIndex)]; if (chosen.isArtifact) { m_artifactCount += 1; if (m_artifactCount >= m_config.world.artifacts.artifactWinCount) { m_isWon = true; } m_pendingSchematicChoices.clear(); return; } m_unlockState.awardUnlockGroup(chosen); m_pendingSchematicChoices.clear(); } bool Simulation::isRecipeUnlocked(const std::string& recipeId) const { return m_unlockState.isRecipeUnlocked(recipeId); } bool Simulation::isItemUnlocked(const std::string& itemId) const { return m_unlockState.isItemUnlocked(itemId); } // --------------------------------------------------------------------------- // Determinism (see docs/replay_design.md) // --------------------------------------------------------------------------- unsigned long long Simulation::getRngFingerprint() const { return fingerprintRng(m_rng); } unsigned long long Simulation::computeStateChecksum() const { Hasher hasher; // RNG stream — the most sensitive signal of divergence. hasher.append(fingerprintRng(m_rng)); // Top-level scalars. hasher.append(m_currentTick); hasher.append(m_nextDepartureTick); hasher.append(m_nextBuildingId); hasher.append(m_buildingBlocksStock); hasher.append(m_gameOver); hasher.append(m_isWon); hasher.append(m_artifactCount); hasher.append(m_expansionsPurchased); // WaveSystem scalar state, reached through existing accessors. hasher.append(getThreatLevel()); hasher.append(getThreatAccumulationRate()); hasher.append(getBossWaveCounter()); hasher.append(getBossCountdownTicks()); hasher.append(getNormalGapRemainingTicks()); // Schematic / unlock state (std::map and std::set iterate in sorted order). m_unlockState.appendChecksum(hasher); // Subsystems contribute their own state. m_buildingSystem->appendChecksum(m_factoryState, hasher); m_beltSystem.appendChecksum(hasher); // ECS component state. View iteration order is a pure function of the // (identical) operation sequence on a fixed binary; each entity's raw id is // folded in so the fingerprint is keyed, not merely a sum of fields. m_admin.forEach( [&hasher](entt::entity entity, const PositionComponent& c) { hasher.append(static_cast(entity)); hasher.append(c.value); }); m_admin.forEach( [&hasher](entt::entity entity, const HealthComponent& c) { hasher.append(static_cast(entity)); hasher.append(c.hp); hasher.append(c.maxHp); }); m_admin.forEach( [&hasher](entt::entity entity, const FacingComponent& c) { hasher.append(static_cast(entity)); hasher.append(c.radians); }); m_admin.forEach( [&hasher](entt::entity entity, const DynamicBodyComponent& c) { hasher.append(static_cast(entity)); hasher.append(c.velocity_tpt); hasher.append(c.angularVelocity_rpt); hasher.append(c.linearAcceleration_tptt); hasher.append(c.angularAcceleration_rptt); }); m_admin.forEach( [&hasher](entt::entity entity, const DebrisComponent& c) { hasher.append(static_cast(entity)); hasher.append(c.amount); }); m_admin.forEach( [&hasher](entt::entity entity, const ShipIdentityComponent& c) { hasher.append(static_cast(entity)); hasher.append(c.schematicId); }); return hasher.getValue(); } // --------------------------------------------------------------------------- // Drains // --------------------------------------------------------------------------- std::vector Simulation::drainBeamFiredEvents() { std::vector result; result.swap(m_beamFiredEvents); return result; } const std::vector& Simulation::getPendingSchematicChoices() const { return m_pendingSchematicChoices; } bool Simulation::hasSchematicChoicesPending() const { return !m_pendingSchematicChoices.empty(); } // --------------------------------------------------------------------------- // Accessors // --------------------------------------------------------------------------- Tick Simulation::getCurrentTick() const { return m_currentTick; } unsigned int Simulation::getSeed() const { return m_seed; } int Simulation::getBuildingBlocksStock() const { return m_buildingBlocksStock; } int Simulation::getCurrentAsteroidWidth_tiles() const { return m_config.world.regions.asteroidWidth_tiles + m_expansionsPurchased * m_config.world.expansion.columnsPerExpansion_tiles; } int Simulation::getCurrentExpansionCost() const { const double cost = m_config.world.expansion.costBuildingBlocksFormula.evaluate( static_cast(m_expansionsPurchased)); return static_cast(std::floor(cost)); } void Simulation::tryExpandAsteroid() { const int cost = getCurrentExpansionCost(); if (m_buildingBlocksStock < cost) { return; } m_buildingBlocksStock -= cost; ++m_expansionsPurchased; m_buildingSystem->setAsteroidWidth_tiles(m_factoryState, getCurrentAsteroidWidth_tiles()); } bool Simulation::isGameOver() const { return m_gameOver; } bool Simulation::isWon() const { return m_isWon; } int Simulation::getArtifactCount() const { return m_artifactCount; } double Simulation::getThreatLevel() const { return m_waveSystem->getThreatLevel(); } double Simulation::getThreatAccumulationRate() const { return m_waveSystem->getThreatAccumulationRate(); } double Simulation::getMaxFactoryProductionThreatRate() const { return static_cast(getProductionBuildingCount(m_factoryState)); } double Simulation::getCurrentFactoryProductionThreatRate() const { return static_cast(getActiveProductionBuildingCount(m_factoryState)); } int Simulation::getBossWaveCounter() const { return m_waveSystem->getBossWaveCounter(); } Tick Simulation::getBossCountdownTicks() const { return m_waveSystem->getBossCountdownTicks(); } Tick Simulation::getNormalGapRemainingTicks() const { return m_waveSystem->getNormalGapRemainingTicks(); } bool Simulation::isSchematicUnlocked(const std::string& shipId) const { return m_unlockState.isSchematicUnlocked(shipId); } bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const { return m_unlockState.isModuleSchematicUnlocked(moduleId); } bool Simulation::isBuildingUnlocked(BuildingType type) const { return m_unlockState.isBuildingUnlocked(type); } std::optional Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation) { // Locked building types cannot be placed (REQ-LOCK-BUILDING); the build menu // hides them, this is the simulation-side backstop (e.g. blueprint placement). if (!isBuildingUnlocked(type)) { return std::nullopt; } if (!isPlacementValid(m_factoryState, m_config, type, anchor, rotation)) { return std::nullopt; } int cost = 0; for (const BuildingDef& def : m_config.buildings.buildings) { if (def.type == type) { cost = def.cost; break; } } if (m_buildingBlocksStock < cost) { return std::nullopt; } m_buildingBlocksStock -= cost; return m_buildingSystem->place(m_factoryState, type, anchor, rotation, m_currentTick); } void Simulation::deconstruct(BuildingId id) { m_buildingBlocksStock += m_buildingSystem->deconstruct(m_factoryState, id, m_currentTick); } void Simulation::cancelDeconstruction(BuildingId id) { m_buildingSystem->cancelDeconstruction(m_factoryState, id); } BuildingSystem& Simulation::getBuildingsMutable() { return *m_buildingSystem; } const FactoryState& Simulation::getFactoryState() const { return m_factoryState; } const BuildingSystem& Simulation::getBuildings() const { return *m_buildingSystem; } BeltSystem& Simulation::getBeltsMutable() { return m_beltSystem; } const BeltSystem& Simulation::getBelts() const { return m_beltSystem; } ShipSystem& Simulation::getShips() { return *m_shipSystem; } const ShipSystem& Simulation::getShips() const { return *m_shipSystem; } DebrisSystem& Simulation::getDebrisSystem() { return *m_debrisSystem; } const DebrisSystem& Simulation::getDebrisSystem() const { return *m_debrisSystem; } EntityAdmin& Simulation::getAdmin() { return m_admin; } const EntityAdmin& Simulation::getAdmin() const { return m_admin; } void Simulation::handleEvent(std::shared_ptr event) { PRINT_TRACES(); } BuildingId Simulation::allocateBuildingId() { return m_nextBuildingId++; }