The constructor and reset() held a character-for-character identical 26-line subsystem construction block, including three capturing lambdas. Both run before the first tick, so the closures can be shared. Order is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
1250 lines
42 KiB
C++
1250 lines
42 KiB
C++
#include "Simulation.h"
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cmath>
|
|
|
|
#include "AiSystem.h"
|
|
#include "Command.h"
|
|
#include "DisplayName.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_beltSystem(m_config.world.beltSpeed_tps)
|
|
{
|
|
m_currentEnemyStationEntities[0] = entt::null;
|
|
m_currentEnemyStationEntities[1] = entt::null;
|
|
|
|
initializeSubsystems();
|
|
|
|
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_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
|
|
initializeSubsystems();
|
|
|
|
initializeUnlockState();
|
|
placeInitialStructures();
|
|
}
|
|
|
|
void Simulation::initializeSubsystems()
|
|
{
|
|
m_buildingSystem = std::make_unique<BuildingSystem>(
|
|
m_config,
|
|
m_beltSystem,
|
|
[this]() { return allocateBuildingId(); },
|
|
[this](int amount) { m_buildingBlocksStock += amount; },
|
|
[this](const std::string& id, QVector2D pos,
|
|
const std::optional<ShipLayoutConfig>& layout) {
|
|
const std::map<std::string, SchematicState>::const_iterator it =
|
|
m_schematicLevels.find(id);
|
|
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
|
{
|
|
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<ShipSystem>(m_config, m_admin);
|
|
m_aiSystem = std::make_unique<AiSystem>(m_config);
|
|
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
|
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
|
|
m_debrisSystem = std::make_unique<DebrisSystem>(m_admin);
|
|
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
|
|
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
|
|
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
|
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
|
}
|
|
|
|
void Simulation::initializeUnlockState()
|
|
{
|
|
// Cache the ids granted by some unlock group (REQ-LOCK-EXPLICIT); an item
|
|
// starts locked iff it is granted by a group.
|
|
m_grantedShipIds.clear();
|
|
m_grantedModuleIds.clear();
|
|
m_grantedBuildingIds.clear();
|
|
m_grantedRecipeIds.clear();
|
|
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
|
{
|
|
m_grantedShipIds.insert(group.ships.begin(), group.ships.end());
|
|
m_grantedModuleIds.insert(group.modules.begin(), group.modules.end());
|
|
m_grantedBuildingIds.insert(group.buildings.begin(), group.buildings.end());
|
|
m_grantedRecipeIds.insert(group.recipes.begin(), group.recipes.end());
|
|
}
|
|
|
|
m_awardedUnlockGroupIds.clear();
|
|
|
|
m_schematicLevels.clear();
|
|
for (const ShipDef& def : m_config.ships.ships)
|
|
{
|
|
SchematicState state;
|
|
state.unlocked = (m_grantedShipIds.count(def.id) == 0);
|
|
m_schematicLevels[def.id] = state;
|
|
}
|
|
|
|
m_moduleSchematicLevels.clear();
|
|
for (const ModuleDef& def : m_config.modules.modules)
|
|
{
|
|
SchematicState state;
|
|
state.unlocked = (m_grantedModuleIds.count(def.id) == 0);
|
|
m_moduleSchematicLevels[def.id] = state;
|
|
}
|
|
|
|
m_buildingLevels.clear();
|
|
for (const BuildingDef& def : m_config.buildings.buildings)
|
|
{
|
|
SchematicState state;
|
|
state.unlocked = (m_grantedBuildingIds.count(def.id) == 0);
|
|
m_buildingLevels[def.id] = state;
|
|
}
|
|
|
|
// Gated assembler recipes start locked; unlocked_at_start recipes are handled
|
|
// in the REQ-LOCK-IMPLICIT traversal, not tracked here.
|
|
m_unlockedRecipeSchematicIds.clear();
|
|
|
|
recomputeUnlocked();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// tick
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void Simulation::apply(const Command& command)
|
|
{
|
|
switch (command.kind)
|
|
{
|
|
case CommandKind::PlaceBuilding:
|
|
{
|
|
const PlaceBuildingCommand& c = static_cast<const PlaceBuildingCommand&>(command);
|
|
const std::optional<BuildingId> 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(id, *c.recipeId);
|
|
}
|
|
if (c.shipLayout.has_value())
|
|
{
|
|
m_buildingSystem->setShipLayout(id, *c.shipLayout);
|
|
}
|
|
if (c.hasSplitterFilters)
|
|
{
|
|
m_buildingSystem->setSiteSplitterFilters(id, c.splitterFilterA, c.splitterFilterB);
|
|
}
|
|
break;
|
|
}
|
|
case CommandKind::Deconstruct:
|
|
deconstruct(*static_cast<const DeconstructCommand&>(command).id);
|
|
break;
|
|
case CommandKind::CancelDeconstruction:
|
|
cancelDeconstruction(*static_cast<const CancelDeconstructionCommand&>(command).id);
|
|
break;
|
|
case CommandKind::RotateInPlace:
|
|
{
|
|
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
|
m_buildingSystem->rotateInPlace(*c.id, c.newRotation);
|
|
break;
|
|
}
|
|
case CommandKind::SetRecipe:
|
|
{
|
|
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
|
|
m_buildingSystem->setRecipe(*c.id, c.recipeId);
|
|
break;
|
|
}
|
|
case CommandKind::SetShipLayout:
|
|
{
|
|
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
|
|
m_buildingSystem->setShipLayout(*c.id, c.layout);
|
|
break;
|
|
}
|
|
case CommandKind::SetSiteSplitterFilters:
|
|
{
|
|
const SetSiteSplitterFiltersCommand& c =
|
|
static_cast<const SetSiteSplitterFiltersCommand&>(command);
|
|
m_buildingSystem->setSiteSplitterFilters(*c.id, c.filterA, c.filterB);
|
|
break;
|
|
}
|
|
case CommandKind::SetSplitterFilters:
|
|
{
|
|
const SetSplitterFiltersCommand& c =
|
|
static_cast<const SetSplitterFiltersCommand&>(command);
|
|
m_beltSystem.setSplitterFilters(c.tile, c.filterA, c.filterB);
|
|
break;
|
|
}
|
|
case CommandKind::ClearBeltTiles:
|
|
m_beltSystem.clearTiles(static_cast<const ClearBeltTilesCommand&>(command).tiles);
|
|
break;
|
|
case CommandKind::ApplySchematicChoice:
|
|
applySchematicChoice(static_cast<const ApplySchematicChoiceCommand&>(command).choiceIndex);
|
|
break;
|
|
case CommandKind::ExpandAsteroid:
|
|
tryExpandAsteroid();
|
|
break;
|
|
case CommandKind::Reset:
|
|
{
|
|
const ResetCommand& c = static_cast<const ResetCommand&>(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_currentTick);
|
|
m_buildingSystem->tickDeconstruction(m_currentTick); // parallel to construction
|
|
m_buildingSystem->tickBeltPull(); // step 3
|
|
m_buildingSystem->tickProduction(m_currentTick); // step 4
|
|
m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b
|
|
m_buildingSystem->tickOutputBelts(); // 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_buildingSystem, *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_buildingSystem, m_beamFiredEvents);
|
|
m_repairSystem->tick(m_currentTick, m_beamFiredEvents);
|
|
|
|
// Step 8: combat resolution
|
|
m_combatSystem->tick(m_currentTick, m_admin,
|
|
*m_buildingSystem, 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<float>(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);
|
|
|
|
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<double>(m_config.stations.playerStation.level);
|
|
const float psHp = static_cast<float>(
|
|
m_config.stations.playerStation.hpFormula.evaluate(psLevel));
|
|
|
|
const float tileSize = static_cast<float>(m_config.world.tileSize_m);
|
|
|
|
WeaponComponent psWeapon;
|
|
psWeapon.damage = static_cast<float>(
|
|
m_config.stations.playerStation.damageFormula.evaluate(psLevel));
|
|
psWeapon.range_tiles = static_cast<float>(
|
|
m_config.stations.playerStation.rangeFormula.evaluate(psLevel)) / tileSize;
|
|
psWeapon.fireRateHz = static_cast<float>(
|
|
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<QPoint> 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<WeaponComponent>(wChild, psWeapon);
|
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
|
ModuleOwnerComponent{m_playerStation1Entity});
|
|
}
|
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
|
}
|
|
{
|
|
const QPoint anchor(psAnchorX, ps2Y);
|
|
std::vector<QPoint> 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<WeaponComponent>(wChild, psWeapon);
|
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
|
ModuleOwnerComponent{m_playerStation2Entity});
|
|
}
|
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
|
}
|
|
|
|
// Rally point: center of the player defence stations' X column, world vertical midpoint.
|
|
const float rallyX = static_cast<float>(psAnchorX) + psParsed.footprint.width() / 2.0f;
|
|
const float rallyY = static_cast<float>(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<float>(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<double>(generation);
|
|
const float esHp = static_cast<float>(
|
|
m_config.stations.enemyStation.hpFormula.evaluate(genD));
|
|
|
|
WeaponComponent esWeapon;
|
|
esWeapon.damage = static_cast<float>(
|
|
m_config.stations.enemyStation.damageFormula.evaluate(genD));
|
|
esWeapon.range_tiles = static_cast<float>(
|
|
m_config.stations.enemyStation.rangeFormula.evaluate(genD)) / tileSize;
|
|
esWeapon.fireRateHz = static_cast<float>(
|
|
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<QPoint> 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<WeaponComponent>(wChild, esWeapon);
|
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
|
ModuleOwnerComponent{m_currentEnemyStationEntities[0]});
|
|
}
|
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
|
}
|
|
{
|
|
const QPoint anchor(anchorX, y2);
|
|
std::vector<QPoint> 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<WeaponComponent>(wChild, esWeapon);
|
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
|
ModuleOwnerComponent{m_currentEnemyStationEntities[1]});
|
|
}
|
|
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Deaths & loot (tick step 9)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void Simulation::tickDeathsAndLoot()
|
|
{
|
|
TRACE();
|
|
// --- 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_config.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);
|
|
const PositionComponent& pos = m_admin.get<PositionComponent>(deadEntity);
|
|
const FactionComponent& fac = m_admin.get<FactionComponent>(deadEntity);
|
|
|
|
const Tick despawnAt = m_currentTick
|
|
+ secondsToTicks(m_config.world.debrisDespawnSeconds);
|
|
int scrap = 0;
|
|
if (!fac.isEnemy)
|
|
{
|
|
const double lv = static_cast<double>(
|
|
m_config.stations.playerStation.level);
|
|
scrap = static_cast<int>(
|
|
m_config.stations.playerStation.scrapDropFormula.evaluate(lv));
|
|
}
|
|
else
|
|
{
|
|
const double genD = static_cast<double>(m_waveSystem->getGeneration());
|
|
scrap = static_cast<int>(
|
|
m_config.stations.enemyStation.scrapDropFormula.evaluate(genD));
|
|
}
|
|
if (scrap > 0)
|
|
{
|
|
m_debrisSystem->spawn(pos.value, scrap, despawnAt);
|
|
}
|
|
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);
|
|
}
|
|
|
|
// --- HQ death check ---
|
|
if (m_admin.isValid(m_hqProxyEntity))
|
|
{
|
|
const HealthComponent& hqHealth = m_admin.get<HealthComponent>(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<HealthComponent>(m_currentEnemyStationEntities[0]).hp <= 0.0f;
|
|
const bool es1Gone = !m_admin.isValid(m_currentEnemyStationEntities[1])
|
|
|| m_admin.get<HealthComponent>(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<const UnlockGroupDef*> pool;
|
|
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
|
{
|
|
if (m_awardedUnlockGroupIds.count(group.id) > 0) { continue; }
|
|
if (group.stationLevel < 0 || group.stationLevel > destroyedStationLevel) { continue; }
|
|
if (!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<double>(destroyedStationLevel)),
|
|
0.0, 1.0);
|
|
std::uniform_real_distribution<double> realDist(0.0, 1.0);
|
|
const bool artifactRolled = realDist(m_rng) < artifactChance;
|
|
|
|
const int numChoices = std::min(static_cast<int>(pool.size()), artifactRolled ? 2 : 3);
|
|
m_pendingSchematicChoices.clear();
|
|
|
|
for (int i = 0; i < numChoices; ++i)
|
|
{
|
|
std::uniform_int_distribution<int> dist(0, static_cast<int>(pool.size()) - 1 - i);
|
|
const int roll = dist(m_rng);
|
|
const std::size_t rollIdx = static_cast<std::size_t>(roll);
|
|
const std::size_t endIdx = pool.size() - 1 - static_cast<std::size_t>(i);
|
|
std::swap(pool[rollIdx], pool[endIdx]);
|
|
|
|
m_pendingSchematicChoices.push_back(makeUnlockOption(*pool[endIdx]));
|
|
}
|
|
|
|
if (artifactRolled)
|
|
{
|
|
SchematicChoiceOption artifactOption;
|
|
artifactOption.isArtifact = true;
|
|
artifactOption.displayName = "Artifact";
|
|
m_pendingSchematicChoices.push_back(std::move(artifactOption));
|
|
}
|
|
}
|
|
|
|
SchematicChoiceOption Simulation::makeUnlockOption(const UnlockGroupDef& group) const
|
|
{
|
|
SchematicChoiceOption option;
|
|
option.isArtifact = false;
|
|
option.unlockGroupId = group.id;
|
|
option.displayName = toDisplayName(group.id);
|
|
|
|
for (const std::string& id : group.ships)
|
|
{
|
|
option.grantedItems.push_back({SchematicType::Ship, id, toDisplayName(id)});
|
|
}
|
|
for (const std::string& id : group.modules)
|
|
{
|
|
option.grantedItems.push_back({SchematicType::Module, id, toDisplayName(id)});
|
|
}
|
|
for (const std::string& id : group.buildings)
|
|
{
|
|
option.grantedItems.push_back({SchematicType::Building, id, toDisplayName(id)});
|
|
}
|
|
for (const std::string& id : group.recipes)
|
|
{
|
|
option.grantedItems.push_back({SchematicType::Recipe, id, toDisplayName(id)});
|
|
}
|
|
|
|
// REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by
|
|
// awarding this whole group. Seed the hypothetical explicit-unlock sets with
|
|
// every grant (ship + module materials via step 1a, recipe outputs via step
|
|
// 1b), then diff against the current implicit set.
|
|
std::set<std::string> hypotheticalShipIds = getUnlockedShipSchematicIds();
|
|
std::set<std::string> hypotheticalModuleIds = getUnlockedModuleSchematicIds();
|
|
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
|
|
for (const std::string& id : group.ships) { hypotheticalShipIds.insert(id); }
|
|
for (const std::string& id : group.modules) { hypotheticalModuleIds.insert(id); }
|
|
for (const std::string& id : group.recipes) { hypotheticalRecipeSchematicIds.insert(id); }
|
|
|
|
const UnlockedSets hypothetical = computeUnlockedSets(
|
|
hypotheticalShipIds, hypotheticalModuleIds, hypotheticalRecipeSchematicIds);
|
|
option.newlyUnlockedRecipeIds = computeNewlyUnlockedRecipeIds(hypothetical);
|
|
|
|
return option;
|
|
}
|
|
|
|
void Simulation::applySchematicChoice(int choiceIndex)
|
|
{
|
|
assert(choiceIndex >= 0 && choiceIndex < static_cast<int>(m_pendingSchematicChoices.size()));
|
|
const SchematicChoiceOption& chosen = m_pendingSchematicChoices[static_cast<std::size_t>(choiceIndex)];
|
|
|
|
if (chosen.isArtifact)
|
|
{
|
|
m_artifactCount += 1;
|
|
if (m_artifactCount >= m_config.world.artifacts.artifactWinCount)
|
|
{
|
|
m_isWon = true;
|
|
}
|
|
m_pendingSchematicChoices.clear();
|
|
return;
|
|
}
|
|
|
|
// Award the whole unlock group (REQ-DEF-SCHEMATIC-DROP): unlock every granted
|
|
// ship, module, building, and assembler recipe at once.
|
|
m_awardedUnlockGroupIds.insert(chosen.unlockGroupId);
|
|
for (const GrantedSchematic& grant : chosen.grantedItems)
|
|
{
|
|
switch (grant.type)
|
|
{
|
|
case SchematicType::Ship: m_schematicLevels.at(grant.id).unlocked = true; break;
|
|
case SchematicType::Module: m_moduleSchematicLevels.at(grant.id).unlocked = true; break;
|
|
case SchematicType::Building: m_buildingLevels.at(grant.id).unlocked = true; break;
|
|
case SchematicType::Recipe: m_unlockedRecipeSchematicIds.insert(grant.id); break;
|
|
}
|
|
}
|
|
|
|
recomputeUnlocked();
|
|
m_pendingSchematicChoices.clear();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Implicit unlock computation (REQ-LOCK-IMPLICIT)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void Simulation::recomputeUnlocked()
|
|
{
|
|
const UnlockedSets result = computeUnlockedSets(
|
|
getUnlockedShipSchematicIds(), getUnlockedModuleSchematicIds(), m_unlockedRecipeSchematicIds);
|
|
m_unlockedItemIds = result.itemIds;
|
|
m_unlockedRecipeIds = result.recipeIds;
|
|
}
|
|
|
|
std::set<std::string> Simulation::getUnlockedShipSchematicIds() const
|
|
{
|
|
std::set<std::string> ids;
|
|
for (const auto& [id, state] : m_schematicLevels)
|
|
{
|
|
if (state.unlocked) { ids.insert(id); }
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
std::set<std::string> Simulation::getUnlockedModuleSchematicIds() const
|
|
{
|
|
std::set<std::string> ids;
|
|
for (const auto& [id, state] : m_moduleSchematicLevels)
|
|
{
|
|
if (state.unlocked) { ids.insert(id); }
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
bool Simulation::prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const
|
|
{
|
|
// A prerequisite is satisfied only once the named unlock group has been
|
|
// awarded (REQ-LOCK-PREREQ).
|
|
for (const std::string& groupId : requiredGroupIds)
|
|
{
|
|
if (m_awardedUnlockGroupIds.count(groupId) == 0) { return false; }
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Simulation::UnlockedSets Simulation::computeUnlockedSets(
|
|
const std::set<std::string>& unlockedShipSchematicIds,
|
|
const std::set<std::string>& unlockedModuleSchematicIds,
|
|
const std::set<std::string>& unlockedRecipeSchematicIds) const
|
|
{
|
|
UnlockedSets result;
|
|
|
|
for (const ShipDef& def : m_config.ships.ships)
|
|
{
|
|
if (unlockedShipSchematicIds.count(def.id) == 0) { continue; }
|
|
for (const RecipeIngredient& mat : def.schematic.materials)
|
|
{
|
|
result.itemIds.insert(mat.item);
|
|
}
|
|
}
|
|
for (const ModuleDef& def : m_config.modules.modules)
|
|
{
|
|
if (unlockedModuleSchematicIds.count(def.id) == 0) { continue; }
|
|
for (const RecipeIngredient& mat : def.materials)
|
|
{
|
|
result.itemIds.insert(mat.item);
|
|
}
|
|
}
|
|
for (const RecipeDef& def : m_config.recipes.recipes)
|
|
{
|
|
// An assembler recipe seeds the base set when it is explicitly available:
|
|
// flagged unlocked_at_start (base recipes the graph can't reach), or a
|
|
// gated recipe whose unlock group has been awarded (REQ-LOCK-EXPLICIT).
|
|
if (def.building == BuildingType::Assembler
|
|
&& (def.unlockedAtStart || unlockedRecipeSchematicIds.count(def.id) > 0))
|
|
{
|
|
for (const RecipeOutput& out : def.outputs)
|
|
{
|
|
result.itemIds.insert(out.item);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool changed = true;
|
|
while (changed)
|
|
{
|
|
changed = false;
|
|
for (const RecipeDef& recipe : m_config.recipes.recipes)
|
|
{
|
|
if (recipe.building != BuildingType::Miner
|
|
&& recipe.building != BuildingType::Smelter
|
|
&& recipe.building != BuildingType::Assembler)
|
|
{
|
|
continue;
|
|
}
|
|
// Skip a gated assembler recipe (granted by an unlock group) whose
|
|
// group has not yet been awarded (REQ-LOCK-IMPLICIT step 2).
|
|
if (recipe.building == BuildingType::Assembler
|
|
&& m_grantedRecipeIds.count(recipe.id) > 0
|
|
&& unlockedRecipeSchematicIds.count(recipe.id) == 0)
|
|
{
|
|
continue;
|
|
}
|
|
bool producesUnlocked = false;
|
|
for (const RecipeOutput& out : recipe.outputs)
|
|
{
|
|
if (result.itemIds.count(out.item) > 0)
|
|
{
|
|
producesUnlocked = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!producesUnlocked) { continue; }
|
|
|
|
if (recipe.building == BuildingType::Miner
|
|
|| recipe.building == BuildingType::Assembler)
|
|
{
|
|
result.recipeIds.insert(recipe.id);
|
|
}
|
|
for (const RecipeIngredient& ing : recipe.inputs)
|
|
{
|
|
if (result.itemIds.insert(ing.item).second)
|
|
{
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::string> Simulation::computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const
|
|
{
|
|
std::vector<std::string> recipeIds;
|
|
for (const std::string& recipeId : hypothetical.recipeIds)
|
|
{
|
|
if (m_unlockedRecipeIds.count(recipeId) > 0) { continue; }
|
|
recipeIds.push_back(recipeId);
|
|
}
|
|
std::sort(recipeIds.begin(), recipeIds.end(),
|
|
[](const std::string& lhs, const std::string& rhs)
|
|
{
|
|
return toDisplayName(lhs) < toDisplayName(rhs);
|
|
});
|
|
return recipeIds;
|
|
}
|
|
|
|
bool Simulation::isRecipeUnlocked(const std::string& recipeId) const
|
|
{
|
|
return m_unlockedRecipeIds.count(recipeId) > 0;
|
|
}
|
|
|
|
bool Simulation::isItemUnlocked(const std::string& itemId) const
|
|
{
|
|
return m_unlockedItemIds.count(itemId) > 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Determinism (see docs/replay_design.md)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void Simulation::appendSchematicMap(Hasher& hasher,
|
|
const std::map<std::string, SchematicState>& levels)
|
|
{
|
|
hasher.append(levels.size());
|
|
for (const std::pair<const std::string, SchematicState>& entry : levels)
|
|
{
|
|
hasher.append(entry.first);
|
|
hasher.append(entry.second.unlocked);
|
|
}
|
|
}
|
|
|
|
void Simulation::appendStringSet(Hasher& hasher, const std::set<std::string>& ids)
|
|
{
|
|
hasher.append(ids.size());
|
|
for (const std::string& id : ids)
|
|
{
|
|
hasher.append(id);
|
|
}
|
|
}
|
|
|
|
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).
|
|
appendSchematicMap(hasher, m_schematicLevels);
|
|
appendSchematicMap(hasher, m_moduleSchematicLevels);
|
|
appendSchematicMap(hasher, m_buildingLevels);
|
|
appendStringSet(hasher, m_awardedUnlockGroupIds);
|
|
appendStringSet(hasher, m_unlockedRecipeSchematicIds);
|
|
appendStringSet(hasher, m_unlockedRecipeIds);
|
|
appendStringSet(hasher, m_unlockedItemIds);
|
|
|
|
// Subsystems contribute their own state.
|
|
m_buildingSystem->appendChecksum(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<PositionComponent>(
|
|
[&hasher](entt::entity entity, const PositionComponent& c)
|
|
{
|
|
hasher.append(static_cast<std::uint32_t>(entity));
|
|
hasher.append(c.value);
|
|
});
|
|
m_admin.forEach<HealthComponent>(
|
|
[&hasher](entt::entity entity, const HealthComponent& c)
|
|
{
|
|
hasher.append(static_cast<std::uint32_t>(entity));
|
|
hasher.append(c.hp);
|
|
hasher.append(c.maxHp);
|
|
});
|
|
m_admin.forEach<FacingComponent>(
|
|
[&hasher](entt::entity entity, const FacingComponent& c)
|
|
{
|
|
hasher.append(static_cast<std::uint32_t>(entity));
|
|
hasher.append(c.radians);
|
|
});
|
|
m_admin.forEach<DynamicBodyComponent>(
|
|
[&hasher](entt::entity entity, const DynamicBodyComponent& c)
|
|
{
|
|
hasher.append(static_cast<std::uint32_t>(entity));
|
|
hasher.append(c.velocity_tpt);
|
|
hasher.append(c.angularVelocity_rpt);
|
|
hasher.append(c.linearAcceleration_tptt);
|
|
hasher.append(c.angularAcceleration_rptt);
|
|
});
|
|
m_admin.forEach<DebrisComponent>(
|
|
[&hasher](entt::entity entity, const DebrisComponent& c)
|
|
{
|
|
hasher.append(static_cast<std::uint32_t>(entity));
|
|
hasher.append(c.amount);
|
|
});
|
|
m_admin.forEach<ShipIdentityComponent>(
|
|
[&hasher](entt::entity entity, const ShipIdentityComponent& c)
|
|
{
|
|
hasher.append(static_cast<std::uint32_t>(entity));
|
|
hasher.append(c.schematicId);
|
|
});
|
|
|
|
return hasher.getValue();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Drains
|
|
// ---------------------------------------------------------------------------
|
|
|
|
std::vector<BeamFiredEvent> Simulation::drainBeamFiredEvents()
|
|
{
|
|
std::vector<BeamFiredEvent> result;
|
|
result.swap(m_beamFiredEvents);
|
|
return result;
|
|
}
|
|
|
|
const std::vector<SchematicChoiceOption>& 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<double>(m_expansionsPurchased));
|
|
return static_cast<int>(std::floor(cost));
|
|
}
|
|
|
|
void Simulation::tryExpandAsteroid()
|
|
{
|
|
const int cost = getCurrentExpansionCost();
|
|
if (m_buildingBlocksStock < cost)
|
|
{
|
|
return;
|
|
}
|
|
m_buildingBlocksStock -= cost;
|
|
++m_expansionsPurchased;
|
|
m_buildingSystem->setAsteroidWidth_tiles(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<double>(m_buildingSystem->getProductionBuildingCount());
|
|
}
|
|
|
|
double Simulation::getCurrentFactoryProductionThreatRate() const
|
|
{
|
|
return static_cast<double>(m_buildingSystem->getActiveProductionBuildingCount());
|
|
}
|
|
|
|
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
|
|
{
|
|
const std::map<std::string, SchematicState>::const_iterator it =
|
|
m_schematicLevels.find(shipId);
|
|
if (it == m_schematicLevels.end())
|
|
{
|
|
return false;
|
|
}
|
|
return it->second.unlocked;
|
|
}
|
|
|
|
bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
|
|
{
|
|
const std::map<std::string, SchematicState>::const_iterator it =
|
|
m_moduleSchematicLevels.find(moduleId);
|
|
if (it == m_moduleSchematicLevels.end())
|
|
{
|
|
return false;
|
|
}
|
|
return it->second.unlocked;
|
|
}
|
|
|
|
bool Simulation::isBuildingUnlocked(BuildingType type) const
|
|
{
|
|
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
|
if (def == nullptr)
|
|
{
|
|
// Types without a config entry (e.g. HQ, defence stations) are unrestricted.
|
|
return true;
|
|
}
|
|
const std::map<std::string, SchematicState>::const_iterator it =
|
|
m_buildingLevels.find(def->id);
|
|
return it == m_buildingLevels.end() ? true : it->second.unlocked;
|
|
}
|
|
|
|
std::optional<BuildingId> 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 (!m_buildingSystem->isPlacementValid(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(type, anchor, rotation, m_currentTick);
|
|
}
|
|
|
|
void Simulation::deconstruct(BuildingId id)
|
|
{
|
|
m_buildingBlocksStock += m_buildingSystem->deconstruct(id, m_currentTick);
|
|
}
|
|
|
|
void Simulation::cancelDeconstruction(BuildingId id)
|
|
{
|
|
m_buildingSystem->cancelDeconstruction(id);
|
|
}
|
|
|
|
BuildingSystem& Simulation::getBuildingsMutable()
|
|
{
|
|
return *m_buildingSystem;
|
|
}
|
|
|
|
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<const TracePrintRequestedEvent> event)
|
|
{
|
|
PRINT_TRACES();
|
|
}
|
|
|
|
BuildingId Simulation::allocateBuildingId()
|
|
{
|
|
return m_nextBuildingId++;
|
|
}
|