Compare commits
8 Commits
b3d6264ed3
...
refactorin
| Author | SHA1 | Date | |
|---|---|---|---|
| 590bca458c | |||
| 59067a9c49 | |||
| 72d85d681c | |||
| a86ba3428a | |||
| 56b7248ac7 | |||
| 009f8c6d14 | |||
| a90218f5c0 | |||
| 7ce0751c60 |
@@ -1,3 +1,6 @@
|
||||
|
||||
|
||||
|
||||
set(TARGET_BASE_NAME "${PRODUCT_NAME}")
|
||||
|
||||
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
||||
|
||||
@@ -46,9 +46,10 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
||||
, m_finished(false)
|
||||
, m_stopRequested(false)
|
||||
{
|
||||
m_factoryState = makeFactoryState(m_gameConfig);
|
||||
|
||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
||||
m_gameConfig,
|
||||
m_factoryState,
|
||||
m_beltSystem,
|
||||
[this]() { return allocateBuildingId(); },
|
||||
[](int) {},
|
||||
@@ -163,7 +164,7 @@ void ArenaSimulation::placeStructures()
|
||||
hp, hp, false);
|
||||
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
||||
m_admin.addComponent<HqProxyComponent>(m_team1HqEntity);
|
||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||
}
|
||||
|
||||
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
|
||||
@@ -184,7 +185,7 @@ void ArenaSimulation::placeStructures()
|
||||
hp, hp, true);
|
||||
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
||||
m_admin.addComponent<HqProxyComponent>(m_team2HqEntity);
|
||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||
}
|
||||
|
||||
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
|
||||
@@ -238,7 +239,7 @@ void ArenaSimulation::placeStructures()
|
||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||
ModuleOwnerComponent{stationEntity});
|
||||
}
|
||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||
};
|
||||
|
||||
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
|
||||
@@ -323,9 +324,9 @@ void ArenaSimulation::tick()
|
||||
// Ship behavior systems (tick step 7): evaluate, select winner, execute.
|
||||
// Module + combat systems emit their tool beams into a shared buffer.
|
||||
m_shipSystem->clearMovementIntents();
|
||||
m_aiSystem->tick(m_admin, m_factoryState, *m_debrisSystem);
|
||||
m_aiSystem->tick(m_admin, m_factoryState);
|
||||
std::vector<BeamFiredEvent> beamFiredEvents;
|
||||
m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, m_factoryState, beamFiredEvents);
|
||||
m_salvagerSystem->tick(m_currentTick, m_factoryState, beamFiredEvents);
|
||||
m_repairSystem->tick(m_currentTick, beamFiredEvents);
|
||||
|
||||
// Combat resolution (tick step 8).
|
||||
@@ -393,7 +394,7 @@ void ArenaSimulation::tickDeaths()
|
||||
for (entt::entity deadEntity : deadStations)
|
||||
{
|
||||
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
|
||||
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
||||
m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
|
||||
{
|
||||
std::vector<entt::entity> stationChildren;
|
||||
m_admin.forEach<ModuleOwnerComponent>(
|
||||
|
||||
@@ -340,7 +340,7 @@ void ArenaView::drawBuildings(QPainter& painter)
|
||||
void ArenaView::drawDebris(QPainter& painter)
|
||||
{
|
||||
const float r = getTilePx() * 0.2f;
|
||||
for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
|
||||
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
|
||||
{
|
||||
const QPointF center = worldToWidget(debris.position);
|
||||
painter.setBrush(QColor(128, 110, 90));
|
||||
|
||||
@@ -43,8 +43,7 @@ AiSystem::AiSystem(const GameConfig& config)
|
||||
{
|
||||
}
|
||||
|
||||
void AiSystem::tick(EntityAdmin& admin, const FactoryState& state,
|
||||
const DebrisSystem& debris)
|
||||
void AiSystem::tick(EntityAdmin& admin, const FactoryState& state)
|
||||
{
|
||||
TRACE();
|
||||
|
||||
@@ -55,7 +54,7 @@ void AiSystem::tick(EntityAdmin& admin, const FactoryState& state,
|
||||
m_retreatEvaluator.evaluate(admin);
|
||||
m_attackEvaluator.evaluate(admin);
|
||||
m_repairEvaluator.evaluate(admin);
|
||||
m_salvageScrapEvaluator.evaluate(admin, debris);
|
||||
m_salvageScrapEvaluator.evaluate(admin);
|
||||
m_deliverScrapEvaluator.evaluate(admin, state);
|
||||
|
||||
// Phase 2: pick the highest-scoring behavior per ship.
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include "StandbyExecutor.h"
|
||||
|
||||
class EntityAdmin;
|
||||
class DebrisSystem;
|
||||
struct GameConfig;
|
||||
|
||||
// Orchestrates ship-behavior decision-making in three batched phases:
|
||||
@@ -35,7 +34,7 @@ class AiSystem
|
||||
public:
|
||||
explicit AiSystem(const GameConfig& config);
|
||||
|
||||
void tick(EntityAdmin& admin, const FactoryState& state, const DebrisSystem& debris);
|
||||
void tick(EntityAdmin& admin, const FactoryState& state);
|
||||
|
||||
private:
|
||||
void selectWinningBehaviors(EntityAdmin& admin);
|
||||
|
||||
@@ -46,13 +46,13 @@ std::optional<int> DebrisSystem::consume(entt::entity entity)
|
||||
return amount;
|
||||
}
|
||||
|
||||
bool DebrisSystem::collectOne(entt::entity entity)
|
||||
bool collectOne(EntityAdmin& admin, entt::entity entity)
|
||||
{
|
||||
if (!m_admin.isValid(entity) || !m_admin.hasAll<DebrisComponent>(entity))
|
||||
if (!admin.isValid(entity) || !admin.hasAll<DebrisComponent>(entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
DebrisComponent& data = m_admin.get<DebrisComponent>(entity);
|
||||
DebrisComponent& data = admin.get<DebrisComponent>(entity);
|
||||
if (data.amount <= 0)
|
||||
{
|
||||
return false;
|
||||
@@ -60,18 +60,18 @@ bool DebrisSystem::collectOne(entt::entity entity)
|
||||
--data.amount;
|
||||
if (data.amount <= 0)
|
||||
{
|
||||
m_admin.destroy(entity);
|
||||
admin.destroy(entity);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<DebrisInfo> DebrisSystem::getAllDebrisInfo() const
|
||||
std::vector<DebrisInfo> getAllDebrisInfo(const EntityAdmin& admin)
|
||||
{
|
||||
std::vector<DebrisInfo> result;
|
||||
m_admin.forEach<DebrisComponent>(
|
||||
[&result, this](entt::entity e, const DebrisComponent& sd)
|
||||
admin.forEach<DebrisComponent>(
|
||||
[&result, &admin](entt::entity e, const DebrisComponent& sd)
|
||||
{
|
||||
result.push_back(DebrisInfo{e, m_admin.get<PositionComponent>(e).value, sd.amount});
|
||||
result.push_back(DebrisInfo{e, admin.get<PositionComponent>(e).value, sd.amount});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -38,9 +38,17 @@ public:
|
||||
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
|
||||
bool collectOne(entt::entity entity);
|
||||
|
||||
// Lightweight snapshot for callers that need to iterate all debris.
|
||||
std::vector<DebrisInfo> getAllDebrisInfo() const;
|
||||
|
||||
private:
|
||||
EntityAdmin& m_admin;
|
||||
};
|
||||
|
||||
// Debris state read and changed straight off the registry — no system needed.
|
||||
|
||||
// Lightweight snapshot for callers that need to iterate all debris.
|
||||
std::vector<DebrisInfo> getAllDebrisInfo(const EntityAdmin& admin);
|
||||
|
||||
// Collects a single scrap unit from the debris: decrements its amount by one,
|
||||
// destroying the entity once depleted. Returns true if a scrap was collected,
|
||||
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
|
||||
bool collectOne(EntityAdmin& admin, entt::entity entity);
|
||||
|
||||
@@ -24,14 +24,14 @@ SalvagerSystem::SalvagerSystem(EntityAdmin& admin)
|
||||
{
|
||||
}
|
||||
|
||||
void SalvagerSystem::tick(Tick currentTick, DebrisSystem& debris, FactoryState& state,
|
||||
void SalvagerSystem::tick(Tick currentTick, FactoryState& state,
|
||||
std::vector<BeamFiredEvent>& outBeamFiredEvents)
|
||||
{
|
||||
TRACE();
|
||||
// Apply collections whose mid-beam delay has elapsed (cycles started earlier).
|
||||
applyPendingCollections(currentTick, debris);
|
||||
applyPendingCollections(currentTick);
|
||||
|
||||
const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
|
||||
const std::vector<DebrisInfo> allDebris = getAllDebrisInfo(m_admin);
|
||||
|
||||
// Tick down per-module collection cooldowns.
|
||||
m_admin.forEach<SalvagerComponent>(
|
||||
@@ -108,7 +108,7 @@ void SalvagerSystem::tick(Tick currentTick, DebrisSystem& debris, FactoryState&
|
||||
});
|
||||
}
|
||||
|
||||
void SalvagerSystem::applyPendingCollections(Tick currentTick, DebrisSystem& debris)
|
||||
void SalvagerSystem::applyPendingCollections(Tick currentTick)
|
||||
{
|
||||
std::vector<PendingCollection>::iterator it = m_pendingCollections.begin();
|
||||
while (it != m_pendingCollections.end())
|
||||
@@ -118,7 +118,7 @@ void SalvagerSystem::applyPendingCollections(Tick currentTick, DebrisSystem& deb
|
||||
if (m_admin.isValid(it->ship) && m_admin.hasAll<CargoComponent>(it->ship))
|
||||
{
|
||||
CargoComponent& cargo = m_admin.get<CargoComponent>(it->ship);
|
||||
if (cargo.current < cargo.maxCapacity && debris.collectOne(it->debris))
|
||||
if (cargo.current < cargo.maxCapacity && collectOne(m_admin, it->debris))
|
||||
{
|
||||
++cargo.current;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
class EntityAdmin;
|
||||
class DebrisSystem;
|
||||
|
||||
// World-mutation system for salvage modules: each module runs a collection cycle
|
||||
// on its own cooldown. When a cycle starts it emits a salvage beam toward an
|
||||
@@ -22,7 +21,7 @@ class SalvagerSystem
|
||||
public:
|
||||
explicit SalvagerSystem(EntityAdmin& admin);
|
||||
|
||||
void tick(Tick currentTick, DebrisSystem& debris, FactoryState& state,
|
||||
void tick(Tick currentTick, FactoryState& state,
|
||||
std::vector<BeamFiredEvent>& outBeamFiredEvents);
|
||||
|
||||
private:
|
||||
@@ -33,7 +32,7 @@ private:
|
||||
Tick appliesAt;
|
||||
};
|
||||
|
||||
void applyPendingCollections(Tick currentTick, DebrisSystem& debris);
|
||||
void applyPendingCollections(Tick currentTick);
|
||||
|
||||
EntityAdmin& m_admin;
|
||||
std::vector<PendingCollection> m_pendingCollections;
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "tracing.h"
|
||||
|
||||
void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const DebrisSystem& debris)
|
||||
void SalvageScrapEvaluator::evaluate(EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
|
||||
const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
|
||||
const std::vector<DebrisInfo> allDebris = getAllDebrisInfo(admin);
|
||||
|
||||
admin.forEach<SalvageScrapBehavior, PositionComponent, SensorRangeComponent>(
|
||||
[&](entt::entity e, SalvageScrapBehavior& salvage, const PositionComponent& pos,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
class EntityAdmin;
|
||||
class DebrisSystem;
|
||||
|
||||
// When cargo is not full, finds the nearest debris within sensor range and sets
|
||||
// it as the target, scoring high. Scores inactive when cargo is full or no debris
|
||||
@@ -9,5 +8,5 @@ class DebrisSystem;
|
||||
class SalvageScrapEvaluator
|
||||
{
|
||||
public:
|
||||
void evaluate(EntityAdmin& admin, const DebrisSystem& debris);
|
||||
void evaluate(EntityAdmin& admin);
|
||||
};
|
||||
|
||||
173
src/lib/sim/BuildingBuffers.cpp
Normal file
173
src/lib/sim/BuildingBuffers.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
#include "BuildingBuffers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
#include "BuildingType.h"
|
||||
#include "ItemType.h"
|
||||
#include "ModulesConfig.h"
|
||||
#include "ShipsConfig.h"
|
||||
|
||||
void initBuffers(Building& b, const RecipeDef& recipe)
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
b.inputBuffer.caps.clear();
|
||||
for (const RecipeIngredient& ing : recipe.inputs)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
b.inputBuffer.counts[type] = 0;
|
||||
b.inputBuffer.caps[type] = 2 * ing.amount;
|
||||
}
|
||||
|
||||
b.outputBuffer.items.clear();
|
||||
if (b.type == BuildingType::ReprocessingPlant)
|
||||
{
|
||||
// 1× max-per-roll (REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
|
||||
int maxAmount = 0;
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
if (out.amount > maxAmount)
|
||||
{
|
||||
maxAmount = out.amount;
|
||||
}
|
||||
}
|
||||
b.outputBuffer.capacity = maxAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 2× per-cycle output.
|
||||
int totalAmount = 0;
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
totalAmount += out.amount;
|
||||
}
|
||||
b.outputBuffer.capacity = 2 * totalAmount;
|
||||
}
|
||||
}
|
||||
|
||||
void initAutoBuffers(const GameConfig& config, Building& b)
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
b.inputBuffer.caps.clear();
|
||||
|
||||
// Union the inputs of every recipe of this building type; the cap for each
|
||||
// item is twice the largest per-cycle requirement across those recipes.
|
||||
// Output capacity follows the same rules as initBuffers: the Reprocessing
|
||||
// Plant holds one cycle's max output (REQ-MAT-OUTPUT-BUFFER-REPROCESSING),
|
||||
// other auto buildings hold twice the largest per-cycle output.
|
||||
int outputCapacity = 0;
|
||||
for (const RecipeDef& recipe : config.recipes.recipes)
|
||||
{
|
||||
if (recipe.building != b.type)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const RecipeIngredient& ing : recipe.inputs)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
b.inputBuffer.counts[type] = 0;
|
||||
b.inputBuffer.caps[type] =
|
||||
std::max(b.inputBuffer.caps[type], 2 * ing.amount);
|
||||
}
|
||||
|
||||
if (b.type == BuildingType::ReprocessingPlant)
|
||||
{
|
||||
int maxAmount = 0;
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
maxAmount = std::max(maxAmount, out.amount);
|
||||
}
|
||||
outputCapacity = std::max(outputCapacity, maxAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
int totalAmount = 0;
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
totalAmount += out.amount;
|
||||
}
|
||||
outputCapacity = std::max(outputCapacity, 2 * totalAmount);
|
||||
}
|
||||
}
|
||||
|
||||
b.outputBuffer.items.clear();
|
||||
b.outputBuffer.capacity = outputCapacity;
|
||||
}
|
||||
|
||||
void initShipyardBuffers(const GameConfig& config, Building& b)
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
b.inputBuffer.caps.clear();
|
||||
b.outputBuffer.items.clear();
|
||||
b.outputBuffer.capacity = 0;
|
||||
const ShipDef* def = config.ships.findShipDef(b.recipeId);
|
||||
if (!def)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (const RecipeIngredient& ing : def->schematic.materials)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
b.inputBuffer.counts[type] = 0;
|
||||
b.inputBuffer.caps[type] = 2 * ing.amount;
|
||||
}
|
||||
if (b.shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const RecipeIngredient& ing : modDef->materials)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
b.inputBuffer.counts.try_emplace(type, 0);
|
||||
b.inputBuffer.caps[type] += 2 * ing.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void initSalvageBayBuffer(const GameConfig& config, Building& b)
|
||||
{
|
||||
// Salvage Bay has no recipe-driven buffer; its output-buffer holding size for
|
||||
// ship drop-off is config-defined (REQ-BLD-SALVAGE-BAY).
|
||||
b.outputBuffer.items.clear();
|
||||
const BuildingDef* def = config.buildings.findBuildingDef(BuildingType::SalvageBay);
|
||||
b.outputBuffer.capacity =
|
||||
(def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
|
||||
}
|
||||
|
||||
|
||||
void reregisterBeltTile(BeltSystem& belts, const GameConfig& config,
|
||||
const Building& building,
|
||||
const std::vector<ItemType>& splitterFilterA,
|
||||
const std::vector<ItemType>& splitterFilterB)
|
||||
{
|
||||
switch (building.type)
|
||||
{
|
||||
case BuildingType::Belt:
|
||||
belts.placeBelt(building.anchor, building.rotation);
|
||||
break;
|
||||
case BuildingType::Splitter:
|
||||
assert(building.outputPorts.size() >= 2);
|
||||
belts.placeSplitter(building.anchor,
|
||||
building.outputPorts[0].direction,
|
||||
building.outputPorts[1].direction);
|
||||
belts.setSplitterFilters(building.anchor, splitterFilterA, splitterFilterB);
|
||||
break;
|
||||
case BuildingType::TunnelEntry:
|
||||
belts.placeTunnelEntry(building.anchor, building.rotation,
|
||||
config.world.tunnelMaxDistance_tiles);
|
||||
break;
|
||||
case BuildingType::TunnelExit:
|
||||
belts.placeTunnelExit(building.anchor, building.rotation);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
39
src/lib/sim/BuildingBuffers.h
Normal file
39
src/lib/sim/BuildingBuffers.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "BeltSystem.h"
|
||||
#include "Building.h"
|
||||
#include "GameConfig.h"
|
||||
#include "ItemType.h"
|
||||
#include "RecipesConfig.h"
|
||||
|
||||
// Setting a building up when it starts existing or is reconfigured: sizing its
|
||||
// input/output buffers from what it will produce, and handing belt-like types back
|
||||
// to BeltSystem. Free functions over the config and the building — they read no
|
||||
// factory state, so both BuildingSystem and ConstructionSystem can use them.
|
||||
|
||||
// Buffers for a building running one known recipe: inputs capped at twice each
|
||||
// ingredient's per-cycle amount, output at twice the per-cycle total (one cycle's
|
||||
// max for a Reprocessing Plant, REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
|
||||
void initBuffers(Building& b, const RecipeDef& recipe);
|
||||
|
||||
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant), unioned over
|
||||
// every recipe of its type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||
void initAutoBuffers(const GameConfig& config, Building& b);
|
||||
|
||||
// Buffers for a shipyard: its schematic's materials plus those of every placed
|
||||
// module (REQ-BLD-SHIPYARD).
|
||||
void initShipyardBuffers(const GameConfig& config, Building& b);
|
||||
|
||||
// The Salvage Bay holds no recipe inputs; its output capacity is config-defined
|
||||
// (REQ-BLD-SALVAGE-BAY).
|
||||
void initSalvageBayBuffer(const GameConfig& config, Building& b);
|
||||
|
||||
// Registers a belt, splitter or tunnel end with BeltSystem. A splitter's filters
|
||||
// live in BeltSystem and are lost by removeTile, so they are passed back in
|
||||
// (REQ-BLD-SPLITTER). No-op for every other building type.
|
||||
void reregisterBeltTile(BeltSystem& belts, const GameConfig& config,
|
||||
const Building& building,
|
||||
const std::vector<ItemType>& splitterFilterA,
|
||||
const std::vector<ItemType>& splitterFilterB);
|
||||
@@ -26,7 +26,6 @@ bool inputLaneEntryFree(const std::vector<BeltItemSlot>& lane)
|
||||
} // namespace
|
||||
|
||||
BuildingSystem::BuildingSystem(const GameConfig& config,
|
||||
FactoryState& state,
|
||||
BeltSystem& belts,
|
||||
std::function<BuildingId()> allocateBuildingId,
|
||||
std::function<void(int)> addBuildingBlocks,
|
||||
@@ -35,7 +34,6 @@ BuildingSystem::BuildingSystem(const GameConfig& config,
|
||||
std::function<bool(const std::string&)> isItemUnlocked,
|
||||
std::mt19937& rng)
|
||||
: m_config(config)
|
||||
, m_state(state)
|
||||
, m_belts(belts)
|
||||
, m_allocateBuildingId(std::move(allocateBuildingId))
|
||||
, m_addBuildingBlocks(std::move(addBuildingBlocks))
|
||||
@@ -43,146 +41,12 @@ BuildingSystem::BuildingSystem(const GameConfig& config,
|
||||
, m_isItemUnlocked(std::move(isItemUnlocked))
|
||||
, m_rng(rng)
|
||||
{
|
||||
m_state.asteroidWidth_tiles = config.world.regions.asteroidWidth_tiles;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void BuildingSystem::initBuffers(Building& b, const RecipeDef& recipe) const
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
b.inputBuffer.caps.clear();
|
||||
for (const RecipeIngredient& ing : recipe.inputs)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
b.inputBuffer.counts[type] = 0;
|
||||
b.inputBuffer.caps[type] = 2 * ing.amount;
|
||||
}
|
||||
|
||||
b.outputBuffer.items.clear();
|
||||
if (b.type == BuildingType::ReprocessingPlant)
|
||||
{
|
||||
// 1× max-per-roll (REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
|
||||
int maxAmount = 0;
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
if (out.amount > maxAmount)
|
||||
{
|
||||
maxAmount = out.amount;
|
||||
}
|
||||
}
|
||||
b.outputBuffer.capacity = maxAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 2× per-cycle output.
|
||||
int totalAmount = 0;
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
totalAmount += out.amount;
|
||||
}
|
||||
b.outputBuffer.capacity = 2 * totalAmount;
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::initAutoBuffers(Building& b) const
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
b.inputBuffer.caps.clear();
|
||||
|
||||
// Union the inputs of every recipe of this building type; the cap for each
|
||||
// item is twice the largest per-cycle requirement across those recipes.
|
||||
// Output capacity follows the same rules as initBuffers: the Reprocessing
|
||||
// Plant holds one cycle's max output (REQ-MAT-OUTPUT-BUFFER-REPROCESSING),
|
||||
// other auto buildings hold twice the largest per-cycle output.
|
||||
int outputCapacity = 0;
|
||||
for (const RecipeDef& recipe : m_config.recipes.recipes)
|
||||
{
|
||||
if (recipe.building != b.type)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const RecipeIngredient& ing : recipe.inputs)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
b.inputBuffer.counts[type] = 0;
|
||||
b.inputBuffer.caps[type] =
|
||||
std::max(b.inputBuffer.caps[type], 2 * ing.amount);
|
||||
}
|
||||
|
||||
if (b.type == BuildingType::ReprocessingPlant)
|
||||
{
|
||||
int maxAmount = 0;
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
maxAmount = std::max(maxAmount, out.amount);
|
||||
}
|
||||
outputCapacity = std::max(outputCapacity, maxAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
int totalAmount = 0;
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
totalAmount += out.amount;
|
||||
}
|
||||
outputCapacity = std::max(outputCapacity, 2 * totalAmount);
|
||||
}
|
||||
}
|
||||
|
||||
b.outputBuffer.items.clear();
|
||||
b.outputBuffer.capacity = outputCapacity;
|
||||
}
|
||||
|
||||
void BuildingSystem::initShipyardBuffers(Building& b) const
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
b.inputBuffer.caps.clear();
|
||||
b.outputBuffer.items.clear();
|
||||
b.outputBuffer.capacity = 0;
|
||||
const ShipDef* def = m_config.ships.findShipDef(b.recipeId);
|
||||
if (!def)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (const RecipeIngredient& ing : def->schematic.materials)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
b.inputBuffer.counts[type] = 0;
|
||||
b.inputBuffer.caps[type] = 2 * ing.amount;
|
||||
}
|
||||
if (b.shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const RecipeIngredient& ing : modDef->materials)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
b.inputBuffer.counts.try_emplace(type, 0);
|
||||
b.inputBuffer.caps[type] += 2 * ing.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::initSalvageBayBuffer(Building& b) const
|
||||
{
|
||||
// Salvage Bay has no recipe-driven buffer; its output-buffer holding size for
|
||||
// ship drop-off is config-defined (REQ-BLD-SALVAGE-BAY).
|
||||
b.outputBuffer.items.clear();
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(BuildingType::SalvageBay);
|
||||
b.outputBuffer.capacity =
|
||||
(def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
|
||||
}
|
||||
|
||||
|
||||
std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe)
|
||||
{
|
||||
@@ -213,7 +77,7 @@ std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe
|
||||
// Placement
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor,
|
||||
std::optional<BuildingId> BuildingSystem::place(FactoryState& state, BuildingType type, QPoint anchor,
|
||||
Rotation rotation, Tick currentTick)
|
||||
{
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
||||
@@ -221,7 +85,7 @@ std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
||||
|
||||
// Reject placements that fall outside the world (REQ-BLD-PLACE-VALID).
|
||||
if (!bodyCellsWithinWorldBounds(m_state, m_config, mask.bodyCells, anchor))
|
||||
if (!bodyCellsWithinWorldBounds(state, m_config, mask.bodyCells, anchor))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -232,7 +96,7 @@ std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor
|
||||
for (const QPoint& cell : mask.bodyCells)
|
||||
{
|
||||
const QPoint absCell = anchor + cell;
|
||||
m_state.grid.occupy(absCell, id);
|
||||
state.grid.occupy(absCell, id);
|
||||
}
|
||||
|
||||
// Build construction site.
|
||||
@@ -247,13 +111,13 @@ std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor
|
||||
site.bodyCells.push_back(anchor + cell);
|
||||
}
|
||||
|
||||
if (m_state.constructionQueue.empty())
|
||||
if (state.constructionQueue.empty())
|
||||
{
|
||||
site.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
|
||||
}
|
||||
// else: completesAt remains 0 (queued, not yet started).
|
||||
|
||||
m_state.constructionQueue.push_back(std::move(site));
|
||||
state.constructionQueue.push_back(std::move(site));
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -261,19 +125,19 @@ std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor
|
||||
// Deconstruct
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int BuildingSystem::deconstruct(BuildingId id, Tick currentTick)
|
||||
int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick currentTick)
|
||||
{
|
||||
// Construction site? Removed instantly with the full refund; never queued
|
||||
// for deconstruction (REQ-BLD-DECONSTRUCT).
|
||||
for (std::deque<ConstructionSite>::iterator it = m_state.constructionQueue.begin();
|
||||
it != m_state.constructionQueue.end();
|
||||
for (std::deque<ConstructionSite>::iterator it = state.constructionQueue.begin();
|
||||
it != state.constructionQueue.end();
|
||||
++it)
|
||||
{
|
||||
if (it->id == id)
|
||||
{
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
|
||||
m_state.grid.release(it->bodyCells);
|
||||
m_state.constructionQueue.erase(it);
|
||||
state.grid.release(it->bodyCells);
|
||||
state.constructionQueue.erase(it);
|
||||
if (def)
|
||||
{
|
||||
return def->cost;
|
||||
@@ -285,7 +149,7 @@ int BuildingSystem::deconstruct(BuildingId id, Tick currentTick)
|
||||
// Operational building? Append it to the deconstruction queue rather than
|
||||
// removing it now; the partial refund is credited on completion in
|
||||
// tickDeconstruction (REQ-BLD-DECON-QUEUE).
|
||||
for (Building& building : m_state.buildings)
|
||||
for (Building& building : state.buildings)
|
||||
{
|
||||
if (building.id != id) { continue; }
|
||||
if (building.queuedForDeconstruction) { return 0; } // already queued
|
||||
@@ -313,11 +177,11 @@ int BuildingSystem::deconstruct(BuildingId id, Tick currentTick)
|
||||
m_belts.removeTile(building.anchor);
|
||||
}
|
||||
|
||||
const bool wasEmpty = m_state.deconstructionQueue.empty();
|
||||
m_state.deconstructionQueue.push_back(std::move(entry));
|
||||
const bool wasEmpty = state.deconstructionQueue.empty();
|
||||
state.deconstructionQueue.push_back(std::move(entry));
|
||||
if (wasEmpty)
|
||||
{
|
||||
startFrontDeconstruction(currentTick);
|
||||
startFrontDeconstruction(state, m_config, currentTick);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -325,25 +189,14 @@ int BuildingSystem::deconstruct(BuildingId id, Tick currentTick)
|
||||
return 0;
|
||||
}
|
||||
|
||||
void BuildingSystem::startFrontDeconstruction(Tick currentTick)
|
||||
{
|
||||
if (m_state.deconstructionQueue.empty()) { return; }
|
||||
DeconstructionEntry& front = m_state.deconstructionQueue.front();
|
||||
if (front.completesAt == 0)
|
||||
{
|
||||
front.completesAt =
|
||||
currentTick + secondsToTicks(m_config.world.deconstructionTimeSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Set recipe
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
||||
void BuildingSystem::setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId)
|
||||
{
|
||||
// Construction site: store recipe for when building completes.
|
||||
for (ConstructionSite& site : m_state.constructionQueue)
|
||||
for (ConstructionSite& site : state.constructionQueue)
|
||||
{
|
||||
if (site.id == id)
|
||||
{
|
||||
@@ -366,7 +219,7 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
||||
}
|
||||
|
||||
// Operational building: clear buffers and re-init.
|
||||
for (Building& building : m_state.buildings)
|
||||
for (Building& building : state.buildings)
|
||||
{
|
||||
if (building.id == id)
|
||||
{
|
||||
@@ -400,7 +253,7 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
||||
{
|
||||
if (building.type == BuildingType::Shipyard)
|
||||
{
|
||||
initShipyardBuffers(building);
|
||||
initShipyardBuffers(m_config, building);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -416,9 +269,9 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout)
|
||||
void BuildingSystem::setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout)
|
||||
{
|
||||
for (ConstructionSite& site : m_state.constructionQueue)
|
||||
for (ConstructionSite& site : state.constructionQueue)
|
||||
{
|
||||
if (site.id == id)
|
||||
{
|
||||
@@ -427,7 +280,7 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout
|
||||
}
|
||||
}
|
||||
|
||||
for (Building& building : m_state.buildings)
|
||||
for (Building& building : state.buildings)
|
||||
{
|
||||
if (building.id == id)
|
||||
{
|
||||
@@ -444,18 +297,18 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout
|
||||
for (std::vector<BeltItemSlot>& lane : building.incomingItems) { lane.clear(); }
|
||||
if (!building.recipeId.empty() && building.type == BuildingType::Shipyard)
|
||||
{
|
||||
initShipyardBuffers(building);
|
||||
initShipyardBuffers(m_config, building);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::setSiteSplitterFilters(BuildingId id,
|
||||
void BuildingSystem::setSiteSplitterFilters(FactoryState& state, BuildingId id,
|
||||
const std::vector<ItemType>& filterA,
|
||||
const std::vector<ItemType>& filterB)
|
||||
{
|
||||
for (ConstructionSite& site : m_state.constructionQueue)
|
||||
for (ConstructionSite& site : state.constructionQueue)
|
||||
{
|
||||
if (site.id == id && site.type == BuildingType::Splitter)
|
||||
{
|
||||
@@ -470,188 +323,10 @@ void BuildingSystem::setSiteSplitterFilters(BuildingId id,
|
||||
// Tick hooks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
void BuildingSystem::cancelDeconstruction(FactoryState& state, BuildingId id)
|
||||
{
|
||||
TRACE();
|
||||
if (m_state.constructionQueue.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ConstructionSite& front = m_state.constructionQueue.front();
|
||||
|
||||
// Guard: if somehow the front site was never started, start it now.
|
||||
if (front.completesAt == 0)
|
||||
{
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
|
||||
if (def)
|
||||
{
|
||||
front.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTick < front.completesAt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Promote construction site to an operational Building.
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(
|
||||
def ? def->surfaceMask : std::vector<std::string>{},
|
||||
front.rotation);
|
||||
|
||||
Building building;
|
||||
building.id = front.id;
|
||||
building.anchor = front.anchor;
|
||||
building.footprint = front.footprint;
|
||||
building.rotation = front.rotation;
|
||||
building.type = front.type;
|
||||
building.recipeId = front.recipeId;
|
||||
building.shipLayout = front.shipLayout;
|
||||
|
||||
for (const QPoint& cell : mask.bodyCells)
|
||||
{
|
||||
building.bodyCells.push_back(front.anchor + cell);
|
||||
}
|
||||
for (const Port& port : mask.outputPorts)
|
||||
{
|
||||
Port absPort;
|
||||
absPort.tile = front.anchor + port.tile;
|
||||
absPort.direction = port.direction;
|
||||
building.outputPorts.push_back(absPort);
|
||||
}
|
||||
building.emergingItems.resize(building.outputPorts.size());
|
||||
building.inputPorts = computeInputPorts(building.bodyCells, building.outputPorts);
|
||||
building.incomingItems.assign(building.inputPorts.size(), {});
|
||||
|
||||
if (building.type == BuildingType::SalvageBay)
|
||||
{
|
||||
initSalvageBayBuffer(building);
|
||||
}
|
||||
else if (isAutoRecipeBuildingType(building.type))
|
||||
{
|
||||
// Smelter/Reprocessing Plant need no recipe selection; buffers are set
|
||||
// up from all recipes of the type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||
initAutoBuffers(building);
|
||||
}
|
||||
else if (!building.recipeId.empty())
|
||||
{
|
||||
if (building.type == BuildingType::Shipyard)
|
||||
{
|
||||
initShipyardBuffers(building);
|
||||
}
|
||||
else
|
||||
{
|
||||
const RecipeDef* recipe = m_config.recipes.findRecipeDef(building.recipeId, building.type);
|
||||
if (recipe)
|
||||
{
|
||||
initBuffers(building, *recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register with BeltSystem before the move (mask/building stays valid). Any
|
||||
// filters configured while under construction carry over (REQ-BLD-SITE-CONFIG).
|
||||
reregisterBeltTile(building, front.splitterFilterA, front.splitterFilterB);
|
||||
|
||||
m_state.buildings.push_back(std::move(building));
|
||||
|
||||
m_state.constructionQueue.pop_front();
|
||||
|
||||
// Start next queued site if present.
|
||||
if (!m_state.constructionQueue.empty() && m_state.constructionQueue.front().completesAt == 0)
|
||||
{
|
||||
const BuildingDef* nextDef =
|
||||
m_config.buildings.findBuildingDef(m_state.constructionQueue.front().type);
|
||||
if (nextDef)
|
||||
{
|
||||
m_state.constructionQueue.front().completesAt =
|
||||
currentTick + secondsToTicks(nextDef->constructionTimeSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::reregisterBeltTile(const Building& building,
|
||||
const std::vector<ItemType>& splitterFilterA,
|
||||
const std::vector<ItemType>& splitterFilterB)
|
||||
{
|
||||
switch (building.type)
|
||||
{
|
||||
case BuildingType::Belt:
|
||||
m_belts.placeBelt(building.anchor, building.rotation);
|
||||
break;
|
||||
case BuildingType::Splitter:
|
||||
assert(building.outputPorts.size() >= 2);
|
||||
m_belts.placeSplitter(building.anchor,
|
||||
building.outputPorts[0].direction,
|
||||
building.outputPorts[1].direction);
|
||||
m_belts.setSplitterFilters(building.anchor, splitterFilterA, splitterFilterB);
|
||||
break;
|
||||
case BuildingType::TunnelEntry:
|
||||
m_belts.placeTunnelEntry(building.anchor, building.rotation,
|
||||
m_config.world.tunnelMaxDistance_tiles);
|
||||
break;
|
||||
case BuildingType::TunnelExit:
|
||||
m_belts.placeTunnelExit(building.anchor, building.rotation);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::tickDeconstruction(Tick currentTick)
|
||||
{
|
||||
TRACE();
|
||||
if (m_state.deconstructionQueue.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DeconstructionEntry& front = m_state.deconstructionQueue.front();
|
||||
|
||||
// Guard: if the front entry's timer was never started, start it now.
|
||||
if (front.completesAt == 0)
|
||||
{
|
||||
startFrontDeconstruction(currentTick);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTick < front.completesAt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the building from the world and credit its refund (REQ-BLD-DECONSTRUCT).
|
||||
// Belt/tunnel/splitter tiles were already unregistered when the building was
|
||||
// queued (see deconstruct), so only tile occupancy and the record remain.
|
||||
for (std::vector<Building>::iterator it = m_state.buildings.begin();
|
||||
it != m_state.buildings.end();
|
||||
++it)
|
||||
{
|
||||
if (it->id != front.id) { continue; }
|
||||
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
|
||||
m_state.grid.release(it->bodyCells);
|
||||
m_state.buildings.erase(it);
|
||||
if (def)
|
||||
{
|
||||
m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
m_state.deconstructionQueue.pop_front();
|
||||
|
||||
// Start the next queued deconstruction, if any.
|
||||
startFrontDeconstruction(currentTick);
|
||||
}
|
||||
|
||||
void BuildingSystem::cancelDeconstruction(BuildingId id)
|
||||
{
|
||||
for (std::deque<DeconstructionEntry>::iterator it = m_state.deconstructionQueue.begin();
|
||||
it != m_state.deconstructionQueue.end();
|
||||
for (std::deque<DeconstructionEntry>::iterator it = state.deconstructionQueue.begin();
|
||||
it != state.deconstructionQueue.end();
|
||||
++it)
|
||||
{
|
||||
if (it->id != id) { continue; }
|
||||
@@ -659,27 +334,27 @@ void BuildingSystem::cancelDeconstruction(BuildingId id)
|
||||
// Resume operation: clear the flag and re-register belt/tunnel/splitter
|
||||
// tiles that were unregistered at enqueue (which re-pairs tunnels,
|
||||
// REQ-BLD-TUNNEL-PAIR). Deconstruction progress is discarded; no refund.
|
||||
if (Building* building = findBuilding(m_state, id))
|
||||
if (Building* building = findBuilding(state, id))
|
||||
{
|
||||
building->queuedForDeconstruction = false;
|
||||
reregisterBeltTile(*building, it->splitterFilterA, it->splitterFilterB);
|
||||
reregisterBeltTile(m_belts, m_config, *building, it->splitterFilterA, it->splitterFilterB);
|
||||
}
|
||||
|
||||
m_state.deconstructionQueue.erase(it);
|
||||
state.deconstructionQueue.erase(it);
|
||||
// If the running front was removed, the new front (completesAt == 0) has
|
||||
// its timer started by the next tickDeconstruction guard.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::tickBeltPull()
|
||||
void BuildingSystem::tickBeltPull(FactoryState& state)
|
||||
{
|
||||
TRACE();
|
||||
// Same per-tick step as the belts, so items travel inward at belt speed
|
||||
// (REQ-GW-BELT-SPEED, REQ-MAT-INPUT-INTAKE).
|
||||
const double progressPerTick = m_belts.getProgressPerTick_tpt();
|
||||
|
||||
for (Building& building : m_state.buildings)
|
||||
for (Building& building : state.buildings)
|
||||
{
|
||||
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||
if (building.queuedForDeconstruction) { continue; }
|
||||
@@ -759,17 +434,17 @@ void BuildingSystem::depositToInputBelt(Building& consumer,
|
||||
consumer.incomingItems[inputPortIndex].push_back(BeltItemSlot{item, 0.0});
|
||||
}
|
||||
|
||||
bool BuildingSystem::tryDirectCoupleDeposit(BuildingId producerId,
|
||||
bool BuildingSystem::tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
|
||||
const Port& outputPort,
|
||||
const Item& item)
|
||||
{
|
||||
const std::optional<BuildingId> ownerId = m_state.grid.findOwner(outputPort.tile);
|
||||
const std::optional<BuildingId> ownerId = state.grid.findOwner(outputPort.tile);
|
||||
if (!ownerId.has_value() || *ownerId == producerId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Building* consumer = findBuilding(m_state, *ownerId);
|
||||
Building* consumer = findBuilding(state, *ownerId);
|
||||
if (!consumer)
|
||||
{
|
||||
return false; // an unbuilt construction site, or not an operational building
|
||||
@@ -794,10 +469,10 @@ bool BuildingSystem::tryDirectCoupleDeposit(BuildingId producerId,
|
||||
return false;
|
||||
}
|
||||
|
||||
void BuildingSystem::tickProduction(Tick currentTick)
|
||||
void BuildingSystem::tickProduction(FactoryState& state, Tick currentTick)
|
||||
{
|
||||
TRACE();
|
||||
for (Building& building : m_state.buildings)
|
||||
for (Building& building : state.buildings)
|
||||
{
|
||||
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||
if (building.queuedForDeconstruction) { continue; }
|
||||
@@ -897,10 +572,10 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
||||
void BuildingSystem::tickShipyardProduction(FactoryState& state, Tick currentTick)
|
||||
{
|
||||
TRACE();
|
||||
for (Building& building : m_state.buildings)
|
||||
for (Building& building : state.buildings)
|
||||
{
|
||||
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||
if (building.queuedForDeconstruction) { continue; }
|
||||
@@ -993,14 +668,14 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::tickOutputBelts()
|
||||
void BuildingSystem::tickOutputBelts(FactoryState& state)
|
||||
{
|
||||
TRACE();
|
||||
// Use BeltSystem's own per-tick step so emerging items travel at exactly the
|
||||
// same speed as real belts (REQ-GW-BELT-SPEED, REQ-MAT-OUTPUT-EMERGE).
|
||||
const double progressPerTick = m_belts.getProgressPerTick_tpt();
|
||||
|
||||
for (Building& building : m_state.buildings)
|
||||
for (Building& building : state.buildings)
|
||||
{
|
||||
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||
if (building.queuedForDeconstruction) { continue; }
|
||||
@@ -1023,7 +698,7 @@ void BuildingSystem::tickOutputBelts()
|
||||
{
|
||||
const Item item = lane.front().item;
|
||||
if (m_belts.tryPutItem(port.tile, item, port.direction)
|
||||
|| tryDirectCoupleDeposit(building.id, port, item))
|
||||
|| tryDirectCoupleDeposit(state, building.id, port, item))
|
||||
{
|
||||
lane.erase(lane.begin());
|
||||
}
|
||||
@@ -1043,10 +718,10 @@ void BuildingSystem::tickOutputBelts()
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::forEachEmergingItem(
|
||||
void BuildingSystem::forEachEmergingItem(const FactoryState& state,
|
||||
const std::function<void(const ItemType&, QPointF)>& visit) const
|
||||
{
|
||||
for (const Building& building : m_state.buildings)
|
||||
for (const Building& building : state.buildings)
|
||||
{
|
||||
for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
|
||||
{
|
||||
@@ -1065,10 +740,10 @@ void BuildingSystem::forEachEmergingItem(
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::forEachIncomingItem(
|
||||
void BuildingSystem::forEachIncomingItem(const FactoryState& state,
|
||||
const std::function<void(const ItemType&, QPointF)>& visit) const
|
||||
{
|
||||
for (const Building& building : m_state.buildings)
|
||||
for (const Building& building : state.buildings)
|
||||
{
|
||||
for (std::size_t p = 0; p < building.inputPorts.size(); ++p)
|
||||
{
|
||||
@@ -1096,10 +771,10 @@ void BuildingSystem::forEachIncomingItem(
|
||||
|
||||
|
||||
|
||||
void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
|
||||
void BuildingSystem::rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation)
|
||||
{
|
||||
// Construction site path — just update rotation; no ports to recompute.
|
||||
for (ConstructionSite& site : m_state.constructionQueue)
|
||||
for (ConstructionSite& site : state.constructionQueue)
|
||||
{
|
||||
if (site.id == id)
|
||||
{
|
||||
@@ -1109,7 +784,7 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
|
||||
}
|
||||
|
||||
// Operational building path.
|
||||
for (Building& b : m_state.buildings)
|
||||
for (Building& b : state.buildings)
|
||||
{
|
||||
if (b.id != id) { continue; }
|
||||
|
||||
@@ -1154,14 +829,14 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
|
||||
}
|
||||
|
||||
m_belts.removeTile(b.anchor);
|
||||
reregisterBeltTile(b, splitterFilterA, splitterFilterB);
|
||||
reregisterBeltTile(m_belts, m_config, b, splitterFilterA, splitterFilterB);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
BuildingId BuildingSystem::placeImmediate(BuildingType type,
|
||||
BuildingId BuildingSystem::placeImmediate(FactoryState& state, BuildingType type,
|
||||
const std::vector<std::string>& surfaceMask,
|
||||
QPoint anchor, Rotation rotation)
|
||||
{
|
||||
@@ -1179,7 +854,7 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type,
|
||||
{
|
||||
const QPoint absCell = anchor + cell;
|
||||
building.bodyCells.push_back(absCell);
|
||||
m_state.grid.occupy(absCell, id);
|
||||
state.grid.occupy(absCell, id);
|
||||
}
|
||||
for (const Port& port : mask.outputPorts)
|
||||
{
|
||||
@@ -1194,17 +869,17 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type,
|
||||
|
||||
if (type == BuildingType::SalvageBay)
|
||||
{
|
||||
initSalvageBayBuffer(building);
|
||||
initSalvageBayBuffer(m_config, building);
|
||||
}
|
||||
|
||||
m_state.buildings.push_back(std::move(building));
|
||||
state.buildings.push_back(std::move(building));
|
||||
return id;
|
||||
}
|
||||
|
||||
bool BuildingSystem::removeBuilding(BuildingId id)
|
||||
bool BuildingSystem::removeBuilding(FactoryState& state, BuildingId id)
|
||||
{
|
||||
for (std::vector<Building>::iterator it = m_state.buildings.begin();
|
||||
it != m_state.buildings.end();
|
||||
for (std::vector<Building>::iterator it = state.buildings.begin();
|
||||
it != state.buildings.end();
|
||||
++it)
|
||||
{
|
||||
if (it->id == id)
|
||||
@@ -1214,31 +889,31 @@ bool BuildingSystem::removeBuilding(BuildingId id)
|
||||
{
|
||||
m_belts.removeTile(it->anchor);
|
||||
}
|
||||
m_state.grid.release(it->bodyCells);
|
||||
m_state.buildings.erase(it);
|
||||
state.grid.release(it->bodyCells);
|
||||
state.buildings.erase(it);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BuildingSystem::forEachBuilding(std::function<void(Building&)> fn)
|
||||
void BuildingSystem::forEachBuilding(FactoryState& state, std::function<void(Building&)> fn)
|
||||
{
|
||||
for (Building& b : m_state.buildings)
|
||||
for (Building& b : state.buildings)
|
||||
{
|
||||
fn(b);
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::registerTileOccupancy(const std::vector<QPoint>& cells,
|
||||
void BuildingSystem::registerTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells,
|
||||
BuildingId ownerPlaceholder)
|
||||
{
|
||||
m_state.grid.occupy(cells, ownerPlaceholder);
|
||||
state.grid.occupy(cells, ownerPlaceholder);
|
||||
}
|
||||
|
||||
void BuildingSystem::unregisterTileOccupancy(const std::vector<QPoint>& cells)
|
||||
void BuildingSystem::unregisterTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells)
|
||||
{
|
||||
m_state.grid.release(cells);
|
||||
state.grid.release(cells);
|
||||
}
|
||||
|
||||
namespace
|
||||
@@ -1270,12 +945,12 @@ void appendInputBuffer(Hasher& hasher, const InputBuffer& buffer)
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void BuildingSystem::appendChecksum(Hasher& hasher) const
|
||||
void BuildingSystem::appendChecksum(const FactoryState& state, Hasher& hasher) const
|
||||
{
|
||||
// m_state.buildings keeps a stable, deterministic order (append on build, swap-free
|
||||
// state.buildings keeps a stable, deterministic order (append on build, swap-free
|
||||
// erase aside — both runs perform identical operations, so order matches).
|
||||
hasher.append(m_state.buildings.size());
|
||||
for (const Building& b : m_state.buildings)
|
||||
hasher.append(state.buildings.size());
|
||||
for (const Building& b : state.buildings)
|
||||
{
|
||||
hasher.append(b.id);
|
||||
hasher.append(b.anchor);
|
||||
@@ -1318,8 +993,8 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
|
||||
hasher.append(b.queuedForDeconstruction);
|
||||
}
|
||||
|
||||
hasher.append(m_state.constructionQueue.size());
|
||||
for (const ConstructionSite& s : m_state.constructionQueue)
|
||||
hasher.append(state.constructionQueue.size());
|
||||
for (const ConstructionSite& s : state.constructionQueue)
|
||||
{
|
||||
hasher.append(s.id);
|
||||
hasher.append(s.anchor);
|
||||
@@ -1336,8 +1011,8 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
|
||||
for (const ItemType& type : s.splitterFilterB) { hasher.append(type.id); }
|
||||
}
|
||||
|
||||
hasher.append(m_state.deconstructionQueue.size());
|
||||
for (const DeconstructionEntry& e : m_state.deconstructionQueue)
|
||||
hasher.append(state.deconstructionQueue.size());
|
||||
for (const DeconstructionEntry& e : state.deconstructionQueue)
|
||||
{
|
||||
hasher.append(e.id);
|
||||
hasher.append(e.completesAt);
|
||||
@@ -1347,5 +1022,5 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
|
||||
for (const ItemType& type : e.splitterFilterB) { hasher.append(type.id); }
|
||||
}
|
||||
|
||||
m_state.grid.appendChecksum(hasher);
|
||||
state.grid.appendChecksum(hasher);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include "BeltSystem.h"
|
||||
#include "Building.h"
|
||||
#include "FactoryState.h"
|
||||
#include "BuildingBuffers.h"
|
||||
#include "DeconstructionSystem.h"
|
||||
#include "PlacementRules.h"
|
||||
#include "ProductionRules.h"
|
||||
#include "BuildingType.h"
|
||||
@@ -37,7 +39,6 @@ class BuildingSystem
|
||||
{
|
||||
public:
|
||||
BuildingSystem(const GameConfig& config,
|
||||
FactoryState& state,
|
||||
BeltSystem& belts,
|
||||
std::function<BuildingId()> allocateBuildingId,
|
||||
std::function<void(int)> addBuildingBlocks,
|
||||
@@ -53,7 +54,7 @@ public:
|
||||
// queue. Terrain type (A vs S) is NOT checked here so that tests can stage
|
||||
// arbitrary layouts; the player-facing entry point
|
||||
// (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid.
|
||||
std::optional<BuildingId> place(BuildingType type, QPoint anchor, Rotation rotation,
|
||||
std::optional<BuildingId> place(FactoryState& state, BuildingType type, QPoint anchor, Rotation rotation,
|
||||
Tick currentTick);
|
||||
|
||||
// Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and
|
||||
@@ -65,7 +66,8 @@ public:
|
||||
// Sets the current buildable asteroid width in tiles. Grows the left
|
||||
// placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
|
||||
// Defaults to world.regions.asteroid_width_tiles at construction.
|
||||
void setAsteroidWidth_tiles(int widthTiles) { m_state.asteroidWidth_tiles = widthTiles; }
|
||||
void setAsteroidWidth_tiles(FactoryState& state, int widthTiles) const
|
||||
{ state.asteroidWidth_tiles = widthTiles; }
|
||||
|
||||
// Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT).
|
||||
// A construction site is removed instantly and the full cost is returned.
|
||||
@@ -73,23 +75,23 @@ public:
|
||||
// (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is
|
||||
// credited later, on completion in tickDeconstruction, so this returns 0 for
|
||||
// it. Returns 0 for unknown ids and for a building already queued.
|
||||
int deconstruct(BuildingId id, Tick currentTick);
|
||||
int deconstruct(FactoryState& state, BuildingId id, Tick currentTick);
|
||||
|
||||
// Take a building back out of the deconstruction queue before it is removed
|
||||
// (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation
|
||||
// (re-registering belt/tunnel/splitter tiles); discards deconstruction
|
||||
// progress and credits no refund. No-op if the id is not queued.
|
||||
void cancelDeconstruction(BuildingId id);
|
||||
void cancelDeconstruction(FactoryState& state, BuildingId id);
|
||||
|
||||
// True if the building is currently in the deconstruction queue.
|
||||
|
||||
// Set the recipe (or schematic id for shipyard) on a building or queued
|
||||
// construction site. Clears both buffers on an operational building.
|
||||
void setRecipe(BuildingId id, const std::string& recipeId);
|
||||
void setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId);
|
||||
|
||||
// Set the module layout for a shipyard. Cancels in-progress production
|
||||
// (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD).
|
||||
void setShipLayout(BuildingId id, const ShipLayoutConfig& layout);
|
||||
void setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout);
|
||||
|
||||
// Splitter filter configuration for a queued/under-construction Splitter
|
||||
// site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through
|
||||
@@ -98,23 +100,21 @@ public:
|
||||
// output directions (derived from its surface mask) and stored filters, or
|
||||
// nullopt if the id is not a Splitter site. The stored filters are applied
|
||||
// to BeltSystem when the splitter finishes building (tickConstruction).
|
||||
void setSiteSplitterFilters(BuildingId id,
|
||||
void setSiteSplitterFilters(FactoryState& state, BuildingId id,
|
||||
const std::vector<ItemType>& filterA,
|
||||
const std::vector<ItemType>& filterB);
|
||||
|
||||
// -- Tick hooks (called from Simulation::tick in the documented order) ---
|
||||
void tickConstruction(Tick currentTick);
|
||||
// Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a
|
||||
// time, in parallel with tickConstruction. Removes the front building and
|
||||
// credits its refund when its timer elapses.
|
||||
void tickDeconstruction(Tick currentTick);
|
||||
void tickBeltPull();
|
||||
void tickProduction(Tick currentTick);
|
||||
void tickShipyardProduction(Tick currentTick);
|
||||
void tickBeltPull(FactoryState& state);
|
||||
void tickProduction(FactoryState& state, Tick currentTick);
|
||||
void tickShipyardProduction(FactoryState& state, Tick currentTick);
|
||||
// Advances each building's virtual output belts, hands finished items off onto
|
||||
// the adjacent real belt, and feeds new buffered items into them
|
||||
// (REQ-MAT-OUTPUT-EMERGE).
|
||||
void tickOutputBelts();
|
||||
void tickOutputBelts(FactoryState& state);
|
||||
|
||||
// -- Queries -------------------------------------------------------------
|
||||
|
||||
@@ -134,26 +134,19 @@ public:
|
||||
// virtual output belt (REQ-MAT-OUTPUT-EMERGE), passing the item type and its
|
||||
// world-space centre (in tile units). Least-progressed first (drawn bottom) so
|
||||
// callers can paint in visit order (REQ-GW-TILE-SIZE ordering).
|
||||
void forEachEmergingItem(
|
||||
void forEachEmergingItem(const FactoryState& state,
|
||||
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
||||
|
||||
// Visits every item currently travelling inward on a building input port's
|
||||
// virtual input belt (REQ-MAT-INPUT-INTAKE), passing the item type and its
|
||||
// world-space centre (in tile units). Least-progressed first (drawn bottom).
|
||||
void forEachIncomingItem(
|
||||
void forEachIncomingItem(const FactoryState& state,
|
||||
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
||||
|
||||
// Returns the entity id of the building or construction site whose footprint
|
||||
// exactly coincides with the ghost (type, anchor, rot) and is of the same
|
||||
// building type. Returns nullopt otherwise.
|
||||
std::optional<BuildingId> findRotateInPlaceTarget(BuildingType type,
|
||||
QPoint anchor,
|
||||
Rotation rot) const;
|
||||
|
||||
// Rotate an existing building or construction site to newRotation in place.
|
||||
// For belt-type operational buildings, re-registers with BeltSystem (items
|
||||
// currently on the tile are discarded by BeltSystem::removeTile).
|
||||
void rotateInPlace(BuildingId id, Rotation newRotation);
|
||||
void rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation);
|
||||
|
||||
|
||||
// Input-capable adjacent tiles for a building or construction site
|
||||
@@ -162,8 +155,8 @@ public:
|
||||
// the target. Output-port edges are excluded. Empty for an unknown id.
|
||||
|
||||
// Register / unregister tile occupancy for ECS station entities.
|
||||
void registerTileOccupancy(const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
|
||||
void unregisterTileOccupancy(const std::vector<QPoint>& cells);
|
||||
void registerTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
|
||||
void unregisterTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells);
|
||||
|
||||
// Place one "scrap" item into a SalvageBay's output buffer.
|
||||
// Returns false if bay not found, wrong type, or output buffer is full.
|
||||
@@ -171,33 +164,29 @@ public:
|
||||
// Bypass the construction queue and create a fully-operational Building
|
||||
// immediately. Used for pre-placed structures (HQ, defence stations).
|
||||
// surfaceMask comes from the relevant config struct.
|
||||
BuildingId placeImmediate(BuildingType type,
|
||||
BuildingId placeImmediate(FactoryState& state, BuildingType type,
|
||||
const std::vector<std::string>& surfaceMask,
|
||||
QPoint anchor, Rotation rotation);
|
||||
|
||||
// Remove an operational building by id without refund (used for deaths).
|
||||
// Returns true if found and removed.
|
||||
bool removeBuilding(BuildingId id);
|
||||
bool removeBuilding(FactoryState& state, BuildingId id);
|
||||
|
||||
// Mutable iteration over all operational buildings.
|
||||
void forEachBuilding(std::function<void(Building&)> fn);
|
||||
void forEachBuilding(FactoryState& state, std::function<void(Building&)> fn);
|
||||
|
||||
// -- Determinism ---------------------------------------------------------
|
||||
// Folds all building, construction-site, and tile-occupancy state into the
|
||||
// hasher in deterministic order (see docs/replay_design.md).
|
||||
void appendChecksum(Hasher& hasher) const;
|
||||
void appendChecksum(const FactoryState& state, Hasher& hasher) const;
|
||||
|
||||
private:
|
||||
// Starts the front deconstruction-queue entry's timer if not yet started
|
||||
// (mirrors how tickConstruction starts a queued construction site).
|
||||
void startFrontDeconstruction(Tick currentTick);
|
||||
|
||||
// Registers a belt/splitter/tunnel building's tile with the belt subsystem
|
||||
// (on construction completion, or when un-queuing a deconstruction). No-op for
|
||||
// non-belt-subsystem types. Splitter filters are (re)applied after placement.
|
||||
void reregisterBeltTile(const Building& building,
|
||||
const std::vector<ItemType>& splitterFilterA,
|
||||
const std::vector<ItemType>& splitterFilterB);
|
||||
|
||||
// True if the consumer would accept `type` at the given input port right now:
|
||||
// it is a required input (or a building block for the HQ), the reservation-aware
|
||||
@@ -213,7 +202,7 @@ private:
|
||||
// Attempts to hand an emerging output item straight into a directly adjacent
|
||||
// building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE).
|
||||
// Returns true if the item was accepted onto the consumer's input belt.
|
||||
bool tryDirectCoupleDeposit(BuildingId producerId,
|
||||
bool tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
|
||||
const Port& outputPort,
|
||||
const Item& item);
|
||||
|
||||
@@ -228,21 +217,14 @@ private:
|
||||
// (ignoring output-buffer space); drives the Starved/Blocked distinction of
|
||||
// the status light (REQ-UI-STATUS-LIGHT).
|
||||
|
||||
void initBuffers(Building& b, const RecipeDef& recipe) const;
|
||||
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input
|
||||
// caps span the union of every recipe of the building's type; no player
|
||||
// recipe is selected (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||
void initAutoBuffers(Building& b) const;
|
||||
void initShipyardBuffers(Building& b) const;
|
||||
void initSalvageBayBuffer(Building& b) const;
|
||||
// Core input-edge scan shared by operational buildings and construction sites.
|
||||
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
|
||||
|
||||
const GameConfig& m_config;
|
||||
|
||||
// The factory's world data — buildings, queued work, tile ownership. Owned by
|
||||
// Simulation, not by this system (see FactoryState.h).
|
||||
FactoryState& m_state;
|
||||
|
||||
BeltSystem& m_belts;
|
||||
std::function<BuildingId()> m_allocateBuildingId;
|
||||
|
||||
@@ -13,6 +13,9 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.h
|
||||
@@ -42,6 +45,9 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.cpp
|
||||
|
||||
112
src/lib/sim/ConstructionSystem.cpp
Normal file
112
src/lib/sim/ConstructionSystem.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
#include "ConstructionSystem.h"
|
||||
|
||||
#include "BuildingBuffers.h"
|
||||
#include "BuildingType.h"
|
||||
#include "FactoryQueries.h"
|
||||
#include "PortGeometry.h"
|
||||
#include "SurfaceMask.h"
|
||||
#include "tracing.h"
|
||||
|
||||
void ConstructionSystem::tick(FactoryState& state, BeltSystem& belts, Tick currentTick)
|
||||
{
|
||||
TRACE();
|
||||
if (state.constructionQueue.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ConstructionSite& front = state.constructionQueue.front();
|
||||
|
||||
// Guard: if somehow the front site was never started, start it now.
|
||||
if (front.completesAt == 0)
|
||||
{
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
|
||||
if (def)
|
||||
{
|
||||
front.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTick < front.completesAt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Promote construction site to an operational Building.
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(
|
||||
def ? def->surfaceMask : std::vector<std::string>{},
|
||||
front.rotation);
|
||||
|
||||
Building building;
|
||||
building.id = front.id;
|
||||
building.anchor = front.anchor;
|
||||
building.footprint = front.footprint;
|
||||
building.rotation = front.rotation;
|
||||
building.type = front.type;
|
||||
building.recipeId = front.recipeId;
|
||||
building.shipLayout = front.shipLayout;
|
||||
|
||||
for (const QPoint& cell : mask.bodyCells)
|
||||
{
|
||||
building.bodyCells.push_back(front.anchor + cell);
|
||||
}
|
||||
for (const Port& port : mask.outputPorts)
|
||||
{
|
||||
Port absPort;
|
||||
absPort.tile = front.anchor + port.tile;
|
||||
absPort.direction = port.direction;
|
||||
building.outputPorts.push_back(absPort);
|
||||
}
|
||||
building.emergingItems.resize(building.outputPorts.size());
|
||||
building.inputPorts = computeInputPorts(building.bodyCells, building.outputPorts);
|
||||
building.incomingItems.assign(building.inputPorts.size(), {});
|
||||
|
||||
if (building.type == BuildingType::SalvageBay)
|
||||
{
|
||||
initSalvageBayBuffer(m_config, building);
|
||||
}
|
||||
else if (isAutoRecipeBuildingType(building.type))
|
||||
{
|
||||
// Smelter/Reprocessing Plant need no recipe selection; buffers are set
|
||||
// up from all recipes of the type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||
initAutoBuffers(m_config, building);
|
||||
}
|
||||
else if (!building.recipeId.empty())
|
||||
{
|
||||
if (building.type == BuildingType::Shipyard)
|
||||
{
|
||||
initShipyardBuffers(m_config, building);
|
||||
}
|
||||
else
|
||||
{
|
||||
const RecipeDef* recipe = m_config.recipes.findRecipeDef(building.recipeId, building.type);
|
||||
if (recipe)
|
||||
{
|
||||
initBuffers(building, *recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register with BeltSystem before the move (mask/building stays valid). Any
|
||||
// filters configured while under construction carry over (REQ-BLD-SITE-CONFIG).
|
||||
reregisterBeltTile(belts, m_config, building, front.splitterFilterA, front.splitterFilterB);
|
||||
|
||||
state.buildings.push_back(std::move(building));
|
||||
|
||||
state.constructionQueue.pop_front();
|
||||
|
||||
// Start next queued site if present.
|
||||
if (!state.constructionQueue.empty() && state.constructionQueue.front().completesAt == 0)
|
||||
{
|
||||
const BuildingDef* nextDef =
|
||||
m_config.buildings.findBuildingDef(state.constructionQueue.front().type);
|
||||
if (nextDef)
|
||||
{
|
||||
state.constructionQueue.front().completesAt =
|
||||
currentTick + secondsToTicks(nextDef->constructionTimeSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
src/lib/sim/ConstructionSystem.h
Normal file
30
src/lib/sim/ConstructionSystem.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "BeltSystem.h"
|
||||
#include "FactoryState.h"
|
||||
#include "GameConfig.h"
|
||||
#include "Tick.h"
|
||||
|
||||
// Advances the construction queue and turns a finished site into an operational
|
||||
// building (REQ-BLD-CONSTRUCTION). One site is built at a time, in queue order:
|
||||
// the front site's timer runs, and when it elapses the site becomes a Building —
|
||||
// its ports and buffers are derived from its definition, its belt tile is handed
|
||||
// back to BeltSystem, and the next queued site starts.
|
||||
//
|
||||
// It completes the building itself rather than handing the finished site back to
|
||||
// BuildingSystem: everything materialisation needs is either in FactoryState, the
|
||||
// config, or a free function (see BuildingBuffers.h, PortGeometry.h), so there is
|
||||
// no intermediate value to pass and no ordering rule between two calls.
|
||||
//
|
||||
// Holds only the config; the world it works on arrives per tick, like the other
|
||||
// systems in lib/ecs/system.
|
||||
class ConstructionSystem
|
||||
{
|
||||
public:
|
||||
explicit ConstructionSystem(const GameConfig& config) : m_config(config) {}
|
||||
|
||||
void tick(FactoryState& state, BeltSystem& belts, Tick currentTick);
|
||||
|
||||
private:
|
||||
const GameConfig& m_config;
|
||||
};
|
||||
67
src/lib/sim/DeconstructionSystem.cpp
Normal file
67
src/lib/sim/DeconstructionSystem.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "DeconstructionSystem.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "Building.h"
|
||||
#include "tracing.h"
|
||||
|
||||
void startFrontDeconstruction(FactoryState& state, const GameConfig& config,
|
||||
Tick currentTick)
|
||||
{
|
||||
if (state.deconstructionQueue.empty()) { return; }
|
||||
DeconstructionEntry& front = state.deconstructionQueue.front();
|
||||
if (front.completesAt == 0)
|
||||
{
|
||||
front.completesAt =
|
||||
currentTick + secondsToTicks(config.world.deconstructionTimeSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void DeconstructionSystem::tick(FactoryState& state, Tick currentTick)
|
||||
{
|
||||
TRACE();
|
||||
if (state.deconstructionQueue.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DeconstructionEntry& front = state.deconstructionQueue.front();
|
||||
|
||||
// Guard: if the front entry's timer was never started, start it now.
|
||||
if (front.completesAt == 0)
|
||||
{
|
||||
startFrontDeconstruction(state, m_config, currentTick);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTick < front.completesAt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the building from the world and credit its refund (REQ-BLD-DECONSTRUCT).
|
||||
// Belt/tunnel/splitter tiles were already unregistered when the building was
|
||||
// queued (see deconstruct), so only tile occupancy and the record remain.
|
||||
for (std::vector<Building>::iterator it = state.buildings.begin();
|
||||
it != state.buildings.end();
|
||||
++it)
|
||||
{
|
||||
if (it->id != front.id) { continue; }
|
||||
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
|
||||
state.grid.release(it->bodyCells);
|
||||
state.buildings.erase(it);
|
||||
if (def)
|
||||
{
|
||||
m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
state.deconstructionQueue.pop_front();
|
||||
|
||||
// Start the next queued deconstruction, if any.
|
||||
startFrontDeconstruction(state, m_config, currentTick);
|
||||
}
|
||||
|
||||
36
src/lib/sim/DeconstructionSystem.h
Normal file
36
src/lib/sim/DeconstructionSystem.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "FactoryState.h"
|
||||
#include "GameConfig.h"
|
||||
#include "Tick.h"
|
||||
|
||||
// The queue timer for pending demolitions (REQ-BLD-DECON-QUEUE): one building at a
|
||||
// time, in parallel with construction. When the front entry's timer elapses the
|
||||
// building is removed from the world, its tiles are released, and its partial refund
|
||||
// is credited.
|
||||
//
|
||||
// It needs no BeltSystem: a belt, splitter or tunnel end is unregistered the moment
|
||||
// it is queued (see BuildingSystem::deconstruct), not when the timer completes.
|
||||
//
|
||||
// Holds the config and the refund sink; the world arrives per tick.
|
||||
class DeconstructionSystem
|
||||
{
|
||||
public:
|
||||
DeconstructionSystem(const GameConfig& config,
|
||||
std::function<void(int)> addBuildingBlocks)
|
||||
: m_config(config), m_addBuildingBlocks(std::move(addBuildingBlocks)) {}
|
||||
|
||||
void tick(FactoryState& state, Tick currentTick);
|
||||
|
||||
private:
|
||||
const GameConfig& m_config;
|
||||
std::function<void(int)> m_addBuildingBlocks;
|
||||
};
|
||||
|
||||
// Starts the timer on the front entry of the deconstruction queue, if it has one and
|
||||
// it has not started yet. Shared: BuildingSystem::deconstruct starts the timer when it
|
||||
// queues the first entry, and DeconstructionSystem restarts it after each completion.
|
||||
void startFrontDeconstruction(FactoryState& state, const GameConfig& config,
|
||||
Tick currentTick);
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "Building.h"
|
||||
#include "GameConfig.h"
|
||||
#include "BuildingGrid.h"
|
||||
#include "BuildingId.h"
|
||||
#include "ItemType.h"
|
||||
@@ -52,3 +53,14 @@ struct FactoryState
|
||||
// Seeded from config by BuildingSystem's constructor.
|
||||
int asteroidWidth_tiles = 0;
|
||||
};
|
||||
|
||||
// A fresh factory for a new run: nothing built, and the asteroid bound seeded from
|
||||
// config. Every owner of a FactoryState creates it this way — the bound has no
|
||||
// sensible default without the config, so a default-constructed state would refuse
|
||||
// every placement on the asteroid.
|
||||
inline FactoryState makeFactoryState(const GameConfig& config)
|
||||
{
|
||||
FactoryState state;
|
||||
state.asteroidWidth_tiles = config.world.regions.asteroidWidth_tiles;
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "Simulation.h"
|
||||
|
||||
#include "FactoryQueries.h"
|
||||
#include "ConstructionSystem.h"
|
||||
#include "DeconstructionSystem.h"
|
||||
#include "PlacementRules.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -50,6 +52,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
||||
{
|
||||
m_currentEnemyStationEntities[0] = entt::null;
|
||||
m_currentEnemyStationEntities[1] = entt::null;
|
||||
m_factoryState = makeFactoryState(m_config);
|
||||
|
||||
initializeSubsystems();
|
||||
|
||||
@@ -97,7 +100,7 @@ void Simulation::reset(unsigned int seed)
|
||||
m_pendingSchematicChoices.clear();
|
||||
|
||||
m_admin.clear();
|
||||
m_factoryState = FactoryState{};
|
||||
m_factoryState = makeFactoryState(m_config);
|
||||
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
|
||||
initializeSubsystems();
|
||||
|
||||
@@ -109,7 +112,6 @@ void Simulation::initializeSubsystems()
|
||||
{
|
||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
||||
m_config,
|
||||
m_factoryState,
|
||||
m_beltSystem,
|
||||
[this]() { return allocateBuildingId(); },
|
||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||
@@ -123,6 +125,9 @@ void Simulation::initializeSubsystems()
|
||||
},
|
||||
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
||||
m_rng);
|
||||
m_constructionSystem = std::make_unique<ConstructionSystem>(m_config);
|
||||
m_deconstructionSystem = std::make_unique<DeconstructionSystem>(
|
||||
m_config, [this](int amount) { m_buildingBlocksStock += amount; });
|
||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
||||
m_aiSystem = std::make_unique<AiSystem>(m_config);
|
||||
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
||||
@@ -153,15 +158,15 @@ void Simulation::apply(const Command& command)
|
||||
const BuildingId id = *placed;
|
||||
if (c.recipeId.has_value())
|
||||
{
|
||||
m_buildingSystem->setRecipe(id, *c.recipeId);
|
||||
m_buildingSystem->setRecipe(m_factoryState, id, *c.recipeId);
|
||||
}
|
||||
if (c.shipLayout.has_value())
|
||||
{
|
||||
m_buildingSystem->setShipLayout(id, *c.shipLayout);
|
||||
m_buildingSystem->setShipLayout(m_factoryState, id, *c.shipLayout);
|
||||
}
|
||||
if (c.hasSplitterFilters)
|
||||
{
|
||||
m_buildingSystem->setSiteSplitterFilters(id, c.splitterFilterA, c.splitterFilterB);
|
||||
m_buildingSystem->setSiteSplitterFilters(m_factoryState, id, c.splitterFilterA, c.splitterFilterB);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -174,26 +179,26 @@ void Simulation::apply(const Command& command)
|
||||
case CommandKind::RotateInPlace:
|
||||
{
|
||||
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
||||
m_buildingSystem->rotateInPlace(*c.id, c.newRotation);
|
||||
m_buildingSystem->rotateInPlace(m_factoryState, *c.id, c.newRotation);
|
||||
break;
|
||||
}
|
||||
case CommandKind::SetRecipe:
|
||||
{
|
||||
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
|
||||
m_buildingSystem->setRecipe(*c.id, c.recipeId);
|
||||
m_buildingSystem->setRecipe(m_factoryState, *c.id, c.recipeId);
|
||||
break;
|
||||
}
|
||||
case CommandKind::SetShipLayout:
|
||||
{
|
||||
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
|
||||
m_buildingSystem->setShipLayout(*c.id, c.layout);
|
||||
m_buildingSystem->setShipLayout(m_factoryState, *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);
|
||||
m_buildingSystem->setSiteSplitterFilters(m_factoryState, *c.id, c.filterA, c.filterB);
|
||||
break;
|
||||
}
|
||||
case CommandKind::SetSplitterFilters:
|
||||
@@ -243,12 +248,12 @@ void Simulation::tick()
|
||||
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_constructionSystem->tick(m_factoryState, m_beltSystem, m_currentTick);
|
||||
m_deconstructionSystem->tick(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)
|
||||
@@ -263,10 +268,10 @@ void Simulation::tick()
|
||||
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);
|
||||
m_aiSystem->tick(m_admin, m_factoryState);
|
||||
// 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_salvagerSystem->tick(m_currentTick, m_factoryState, m_beamFiredEvents);
|
||||
m_repairSystem->tick(m_currentTick, m_beamFiredEvents);
|
||||
|
||||
// Step 8: combat resolution
|
||||
@@ -306,8 +311,7 @@ void Simulation::placeInitialStructures()
|
||||
(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_hqBuildingId = m_buildingSystem->placeImmediate(m_factoryState, BuildingType::Hq,
|
||||
m_config.stations.hq.surfaceMask,
|
||||
QPoint(hqAnchorX, hqAnchorY),
|
||||
Rotation::East);
|
||||
@@ -356,7 +360,7 @@ void Simulation::placeInitialStructures()
|
||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||
ModuleOwnerComponent{m_playerStation1Entity});
|
||||
}
|
||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||
}
|
||||
{
|
||||
const QPoint anchor(psAnchorX, ps2Y);
|
||||
@@ -373,7 +377,7 @@ void Simulation::placeInitialStructures()
|
||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||
ModuleOwnerComponent{m_playerStation2Entity});
|
||||
}
|
||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||
}
|
||||
|
||||
// Rally point: center of the player defence stations' X column, world vertical midpoint.
|
||||
@@ -428,7 +432,7 @@ void Simulation::placeEnemyStationSet(int generation)
|
||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||
ModuleOwnerComponent{m_currentEnemyStationEntities[0]});
|
||||
}
|
||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||
}
|
||||
{
|
||||
const QPoint anchor(anchorX, y2);
|
||||
@@ -445,7 +449,7 @@ void Simulation::placeEnemyStationSet(int generation)
|
||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||
ModuleOwnerComponent{m_currentEnemyStationEntities[1]});
|
||||
}
|
||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
||||
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,7 +523,7 @@ void Simulation::tickDeathsAndLoot()
|
||||
{
|
||||
m_debrisSystem->spawn(pos.value, scrap, despawnAt);
|
||||
}
|
||||
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
||||
m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
|
||||
{
|
||||
std::vector<entt::entity> stationChildren;
|
||||
m_admin.forEach<ModuleOwnerComponent>(
|
||||
@@ -671,7 +675,7 @@ unsigned long long Simulation::computeStateChecksum() const
|
||||
m_unlockState.appendChecksum(hasher);
|
||||
|
||||
// Subsystems contribute their own state.
|
||||
m_buildingSystem->appendChecksum(hasher);
|
||||
m_buildingSystem->appendChecksum(m_factoryState, hasher);
|
||||
m_beltSystem.appendChecksum(hasher);
|
||||
|
||||
// ECS component state. View iteration order is a pure function of the
|
||||
@@ -784,7 +788,7 @@ void Simulation::tryExpandAsteroid()
|
||||
}
|
||||
m_buildingBlocksStock -= cost;
|
||||
++m_expansionsPurchased;
|
||||
m_buildingSystem->setAsteroidWidth_tiles(getCurrentAsteroidWidth_tiles());
|
||||
m_buildingSystem->setAsteroidWidth_tiles(m_factoryState, getCurrentAsteroidWidth_tiles());
|
||||
}
|
||||
|
||||
bool Simulation::isGameOver() const
|
||||
@@ -880,17 +884,17 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
|
||||
return std::nullopt;
|
||||
}
|
||||
m_buildingBlocksStock -= cost;
|
||||
return m_buildingSystem->place(type, anchor, rotation, m_currentTick);
|
||||
return m_buildingSystem->place(m_factoryState, type, anchor, rotation, m_currentTick);
|
||||
}
|
||||
|
||||
void Simulation::deconstruct(BuildingId id)
|
||||
{
|
||||
m_buildingBlocksStock += m_buildingSystem->deconstruct(id, m_currentTick);
|
||||
m_buildingBlocksStock += m_buildingSystem->deconstruct(m_factoryState, id, m_currentTick);
|
||||
}
|
||||
|
||||
void Simulation::cancelDeconstruction(BuildingId id)
|
||||
{
|
||||
m_buildingSystem->cancelDeconstruction(id);
|
||||
m_buildingSystem->cancelDeconstruction(m_factoryState, id);
|
||||
}
|
||||
|
||||
BuildingSystem& Simulation::getBuildingsMutable()
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
class AiSystem;
|
||||
class BuildingSystem;
|
||||
class ConstructionSystem;
|
||||
class DeconstructionSystem;
|
||||
struct Command;
|
||||
class Hasher;
|
||||
class CombatSystem;
|
||||
@@ -218,6 +220,8 @@ private:
|
||||
FactoryState m_factoryState;
|
||||
BeltSystem m_beltSystem;
|
||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||
std::unique_ptr<ConstructionSystem> m_constructionSystem;
|
||||
std::unique_ptr<DeconstructionSystem> m_deconstructionSystem;
|
||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||
std::unique_ptr<AiSystem> m_aiSystem;
|
||||
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "BeltSystem.h"
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "ConstructionSystem.h"
|
||||
#include "FactoryState.h"
|
||||
#include "BuildingType.h"
|
||||
#include "ConfigLoader.h"
|
||||
@@ -56,13 +57,14 @@
|
||||
struct Fixture
|
||||
{
|
||||
GameConfig cfg;
|
||||
FactoryState state;
|
||||
FactoryState state = makeFactoryState(cfg);
|
||||
BeltSystem belts;
|
||||
BuildingId nextBuildingId;
|
||||
int stock;
|
||||
std::mt19937 rng;
|
||||
EntityAdmin admin;
|
||||
BuildingSystem buildings;
|
||||
ConstructionSystem construction;
|
||||
ShipSystem ships;
|
||||
AiSystem ai;
|
||||
SalvagerSystem salvager;
|
||||
@@ -79,12 +81,13 @@ struct Fixture
|
||||
, nextBuildingId(1)
|
||||
, stock(0)
|
||||
, rng(42)
|
||||
, buildings(cfg, state, belts,
|
||||
, buildings(cfg, belts,
|
||||
[this]() { return nextBuildingId++; },
|
||||
[this](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||
[](const std::string&) -> bool { return true; },
|
||||
rng)
|
||||
, construction(cfg)
|
||||
, ships(cfg, admin)
|
||||
, ai(cfg)
|
||||
, salvager(admin)
|
||||
@@ -98,14 +101,14 @@ struct Fixture
|
||||
void decide()
|
||||
{
|
||||
ships.clearMovementIntents();
|
||||
ai.tick(admin, state, scraps);
|
||||
ai.tick(admin, state);
|
||||
}
|
||||
|
||||
// World mutation: collection/delivery and healing.
|
||||
void runModules()
|
||||
{
|
||||
beamEvents.clear();
|
||||
salvager.tick(tick, scraps, state, beamEvents);
|
||||
salvager.tick(tick, state, beamEvents);
|
||||
repair.tick(tick, beamEvents);
|
||||
}
|
||||
|
||||
@@ -138,7 +141,7 @@ struct Fixture
|
||||
void salvageTick()
|
||||
{
|
||||
beamEvents.clear();
|
||||
salvager.tick(tick, scraps, state, beamEvents);
|
||||
salvager.tick(tick, state, beamEvents);
|
||||
++tick;
|
||||
}
|
||||
|
||||
@@ -952,12 +955,12 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
||||
{
|
||||
Fixture f;
|
||||
|
||||
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
|
||||
const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay,
|
||||
QPoint(-4, 0), Rotation::East, 0).value();
|
||||
Tick t = 0;
|
||||
for (int i = 0; i < 500; ++i)
|
||||
{
|
||||
f.buildings.tickConstruction(t++);
|
||||
f.construction.tick(f.state, f.belts, t++);
|
||||
if (findBuilding(f.state, bayId) != nullptr)
|
||||
{
|
||||
break;
|
||||
@@ -987,12 +990,12 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo",
|
||||
{
|
||||
Fixture f;
|
||||
|
||||
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
|
||||
const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay,
|
||||
QPoint(-4, 0), Rotation::East, 0).value();
|
||||
Tick t = 0;
|
||||
for (int i = 0; i < 500; ++i)
|
||||
{
|
||||
f.buildings.tickConstruction(t++);
|
||||
f.construction.tick(f.state, f.belts, t++);
|
||||
if (findBuilding(f.state, bayId) != nullptr) { break; }
|
||||
}
|
||||
const Building* bay = findBuilding(f.state, bayId);
|
||||
|
||||
@@ -666,7 +666,7 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "
|
||||
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||
REQUIRE(id != kInvalidBuildingId);
|
||||
|
||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||
|
||||
const ConstructionSite* site = findSite(sim.getFactoryState(), id);
|
||||
REQUIRE(site != nullptr);
|
||||
@@ -680,7 +680,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
|
||||
|
||||
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||
REQUIRE(id != kInvalidBuildingId);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_copper_ore");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_copper_ore");
|
||||
|
||||
// Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300.
|
||||
// Run 301 ticks (0..300) to process the completion tick.
|
||||
@@ -726,7 +726,7 @@ TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blue
|
||||
const BuildingId id =
|
||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||
REQUIRE(id != kInvalidBuildingId);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||
|
||||
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
|
||||
|
||||
@@ -743,7 +743,7 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
|
||||
const BuildingId idA =
|
||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||
REQUIRE(idA != kInvalidBuildingId);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idA, "mine_iron_ore");
|
||||
for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); }
|
||||
REQUIRE(findBuilding(sim.getFactoryState(), idA) != nullptr);
|
||||
|
||||
@@ -751,7 +751,7 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
|
||||
const BuildingId idB =
|
||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East).value();
|
||||
REQUIRE(idB != kInvalidBuildingId);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idB, "mine_copper_ore");
|
||||
REQUIRE(findSite(sim.getFactoryState(), idB) != nullptr);
|
||||
|
||||
const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB });
|
||||
@@ -854,7 +854,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
|
||||
pm.rotation = Rotation::East;
|
||||
layout.placedModules.push_back(pm);
|
||||
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
|
||||
|
||||
const ConstructionSite* site = findSite(sim.getFactoryState(), id);
|
||||
REQUIRE(site != nullptr);
|
||||
@@ -878,7 +878,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
|
||||
pm.rotation = Rotation::North;
|
||||
layout.placedModules.push_back(pm);
|
||||
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
|
||||
|
||||
// Shipyard construction_time_seconds = 30 in the test config.
|
||||
double constructionTime = 0.0;
|
||||
|
||||
@@ -37,8 +37,7 @@ BuildingId placeOperational(Simulation& sim, const GameConfig& cfg,
|
||||
{
|
||||
const BuildingDef* def = findDef(cfg, type);
|
||||
REQUIRE(def != nullptr);
|
||||
return SimulationTestAccess::buildings(sim).placeImmediate(
|
||||
type, def->surfaceMask, anchor, Rotation::East);
|
||||
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), type, def->surfaceMask, anchor, Rotation::East);
|
||||
}
|
||||
|
||||
const ShipDef* findAvailableSchematic(const GameConfig& cfg)
|
||||
@@ -67,7 +66,7 @@ TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]"
|
||||
Simulation sim(loadTestConfig(), 7);
|
||||
|
||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
|
||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||
|
||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||
REQUIRE(config.has_value());
|
||||
@@ -103,8 +102,8 @@ TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
|
||||
REQUIRE(schematic != nullptr);
|
||||
|
||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0));
|
||||
SimulationTestAccess::buildings(sim).setRecipe(id, schematic->id);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(id, ShipLayoutConfig{});
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, schematic->id);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, ShipLayoutConfig{});
|
||||
|
||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||
REQUIRE(config.has_value());
|
||||
@@ -126,7 +125,7 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
|
||||
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
|
||||
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
|
||||
|
||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||
|
||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||
REQUIRE(config.has_value());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,7 +52,7 @@ static entt::entity findWeaponChild(EntityAdmin& admin, entt::entity ship)
|
||||
struct CombatFixture
|
||||
{
|
||||
GameConfig cfg;
|
||||
FactoryState state;
|
||||
FactoryState state = makeFactoryState(cfg);
|
||||
std::mt19937 rng;
|
||||
EntityAdmin admin;
|
||||
BuildingId nextBuildingId;
|
||||
@@ -67,7 +67,7 @@ struct CombatFixture
|
||||
, nextBuildingId(1)
|
||||
, belts(cfg.world.beltSpeed_tps)
|
||||
, ships(cfg, admin)
|
||||
, buildings(cfg, state, belts,
|
||||
, buildings(cfg, belts,
|
||||
[this]() { return nextBuildingId++; },
|
||||
[](int){},
|
||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||
@@ -415,7 +415,7 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
||||
|
||||
sim.tick();
|
||||
|
||||
const std::vector<DebrisInfo> scraps = sim.getDebrisSystem().getAllDebrisInfo();
|
||||
const std::vector<DebrisInfo> scraps = getAllDebrisInfo(sim.getAdmin());
|
||||
REQUIRE(scraps.size() == 1);
|
||||
CHECK(sim.getAdmin().get<DebrisComponent>(scraps[0].entity).amount == 59);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe"
|
||||
|
||||
const BuildingId id =
|
||||
SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||
SimulationTestAccess::buildings(viaDirect).setRecipe(id, "mine_iron_ore");
|
||||
SimulationTestAccess::buildings(viaDirect).setRecipe(SimulationTestAccess::state(viaDirect), id, "mine_iron_ore");
|
||||
|
||||
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
|
||||
}
|
||||
|
||||
@@ -116,16 +116,16 @@ TEST_CASE("DebrisSystem: collectOne depletes one scrap and keeps the debris unti
|
||||
|
||||
const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 3, 100);
|
||||
|
||||
REQUIRE(ss.collectOne(e));
|
||||
REQUIRE(collectOne(admin, e));
|
||||
REQUIRE(admin.isValid(e));
|
||||
REQUIRE(admin.get<DebrisComponent>(e).amount == 2);
|
||||
|
||||
REQUIRE(ss.collectOne(e));
|
||||
REQUIRE(collectOne(admin, e));
|
||||
REQUIRE(admin.isValid(e));
|
||||
REQUIRE(admin.get<DebrisComponent>(e).amount == 1);
|
||||
|
||||
// Final unit collected: the debris is removed once depleted.
|
||||
REQUIRE(ss.collectOne(e));
|
||||
REQUIRE(collectOne(admin, e));
|
||||
REQUIRE_FALSE(admin.isValid(e));
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ TEST_CASE("DebrisSystem: collectOne returns false for an invalid entity", "[debr
|
||||
EntityAdmin admin;
|
||||
DebrisSystem ss(admin);
|
||||
|
||||
REQUIRE_FALSE(ss.collectOne(entt::null));
|
||||
REQUIRE_FALSE(collectOne(admin, entt::null));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -149,7 +149,7 @@ TEST_CASE("DebrisSystem: getAllDebrisInfo returns all spawned debris", "[debris]
|
||||
ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
|
||||
ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
|
||||
|
||||
const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
|
||||
const std::vector<DebrisInfo> info = getAllDebrisInfo(admin);
|
||||
REQUIRE(info.size() == 2);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ TEST_CASE("DebrisSystem: getAllDebrisInfo reports each debris entry.s remaining
|
||||
const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
|
||||
const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
|
||||
|
||||
const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
|
||||
const std::vector<DebrisInfo> info = getAllDebrisInfo(admin);
|
||||
REQUIRE(info.size() == 2);
|
||||
for (const DebrisInfo& i : info)
|
||||
{
|
||||
|
||||
@@ -62,8 +62,7 @@ static const BuildingDef* findShipyardDef(const GameConfig& cfg)
|
||||
|
||||
static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
|
||||
{
|
||||
return SimulationTestAccess::buildings(sim).placeImmediate(
|
||||
BuildingType::Shipyard,
|
||||
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), BuildingType::Shipyard,
|
||||
yardDef.surfaceMask,
|
||||
QPoint(0, 0),
|
||||
Rotation::East);
|
||||
@@ -73,7 +72,7 @@ static void fillMaterials(Simulation& sim, BuildingId yardId,
|
||||
const ShipDef& def,
|
||||
const ShipLayoutConfig& layout)
|
||||
{
|
||||
SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b) {
|
||||
SimulationTestAccess::buildings(sim).forEachBuilding(SimulationTestAccess::state(sim), [&](Building& b) {
|
||||
if (b.id != yardId)
|
||||
{
|
||||
return;
|
||||
@@ -208,7 +207,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
|
||||
|
||||
ShipLayoutConfig layout;
|
||||
PlacedModule pm;
|
||||
@@ -217,7 +216,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
|
||||
pm.rotation = Rotation::East;
|
||||
layout.placedModules.push_back(pm);
|
||||
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
|
||||
|
||||
const Building* b = findBuilding(sim.getFactoryState(), yardId);
|
||||
REQUIRE(b != nullptr);
|
||||
@@ -237,7 +236,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
|
||||
|
||||
// Fill materials and tick to start production.
|
||||
ShipLayoutConfig emptyLayout;
|
||||
@@ -256,7 +255,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
|
||||
pm.rotation = Rotation::East;
|
||||
layout.placedModules.push_back(pm);
|
||||
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
|
||||
|
||||
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
|
||||
REQUIRE(b2 != nullptr);
|
||||
@@ -279,7 +278,7 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId, "interceptor");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, "interceptor");
|
||||
// Deliberately no setShipLayout: recipe set, layout left unconfigured.
|
||||
|
||||
// Charge only the base-hull materials (an empty layout adds none).
|
||||
@@ -314,7 +313,7 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
|
||||
|
||||
ShipLayoutConfig layout;
|
||||
PlacedModule pm;
|
||||
@@ -322,13 +321,13 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
||||
pm.position = QPoint(0, 0);
|
||||
pm.rotation = Rotation::East;
|
||||
layout.placedModules.push_back(pm);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
|
||||
|
||||
const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
|
||||
REQUIRE(b1 != nullptr);
|
||||
REQUIRE(b1->shipLayout.has_value());
|
||||
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"destroyer");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"destroyer");
|
||||
|
||||
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
|
||||
REQUIRE(b2 != nullptr);
|
||||
@@ -343,7 +342,7 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
|
||||
|
||||
ShipLayoutConfig layout;
|
||||
PlacedModule pm;
|
||||
@@ -351,14 +350,14 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
|
||||
pm.position = QPoint(0, 0);
|
||||
pm.rotation = Rotation::East;
|
||||
layout.placedModules.push_back(pm);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
|
||||
|
||||
const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
|
||||
REQUIRE(b1 != nullptr);
|
||||
REQUIRE(b1->shipLayout.has_value());
|
||||
|
||||
// Re-selecting the same recipe must be a no-op and preserve the layout.
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
|
||||
|
||||
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
|
||||
REQUIRE(b2 != nullptr);
|
||||
|
||||
@@ -58,8 +58,7 @@ static const BuildingDef* findShipyardDef(const GameConfig& cfg)
|
||||
|
||||
static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
|
||||
{
|
||||
return SimulationTestAccess::buildings(sim).placeImmediate(
|
||||
BuildingType::Shipyard,
|
||||
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), BuildingType::Shipyard,
|
||||
yardDef.surfaceMask,
|
||||
QPoint(0, 0),
|
||||
Rotation::East);
|
||||
@@ -75,7 +74,7 @@ static int countShips(Simulation& sim)
|
||||
|
||||
static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def)
|
||||
{
|
||||
SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b)
|
||||
SimulationTestAccess::buildings(sim).forEachBuilding(SimulationTestAccess::state(sim), [&](Building& b)
|
||||
{
|
||||
if (b.id != yardId)
|
||||
{
|
||||
@@ -107,7 +106,7 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
|
||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||
REQUIRE(yardId != kInvalidBuildingId);
|
||||
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
|
||||
fillMaterials(sim, yardId, *def);
|
||||
|
||||
// First tick: materials consumed, production cycle starts — no ship yet.
|
||||
@@ -166,7 +165,7 @@ TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
|
||||
const int shipsBefore = countShips(sim);
|
||||
|
||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
|
||||
// Materials remain at zero (default after setRecipe); no cycle starts.
|
||||
|
||||
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
|
||||
@@ -188,7 +187,7 @@ TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipya
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
|
||||
|
||||
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "BuildingId.h"
|
||||
#include "BuildingType.h"
|
||||
#include "Rotation.h"
|
||||
#include "FactoryState.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
class BeltSystem;
|
||||
@@ -25,6 +26,7 @@ class BuildingSystem;
|
||||
struct SimulationTestAccess
|
||||
{
|
||||
static BuildingSystem& buildings(Simulation& sim) { return sim.getBuildingsMutable(); }
|
||||
static FactoryState& state(Simulation& sim) { return sim.m_factoryState; }
|
||||
static BeltSystem& belts(Simulation& sim) { return sim.getBeltsMutable(); }
|
||||
|
||||
static std::optional<BuildingId> place(Simulation& sim, BuildingType type,
|
||||
|
||||
@@ -367,7 +367,7 @@ int FieldSelectionPanel::selectedDebrisScrapTotal() const
|
||||
{
|
||||
// Sum the remaining scrap across the still-living selected debris (REQ-UI-DEBRIS-PANEL).
|
||||
int total = 0;
|
||||
for (const DebrisInfo& info : m_sim->getDebrisSystem().getAllDebrisInfo())
|
||||
for (const DebrisInfo& info : getAllDebrisInfo(m_sim->getAdmin()))
|
||||
{
|
||||
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
|
||||
!= m_selectedDebris.end())
|
||||
|
||||
@@ -777,7 +777,7 @@ void GameWorldView::pruneDespawnedDebris()
|
||||
if (m_selectedDebris.empty()) { return; }
|
||||
|
||||
std::vector<entt::entity> live;
|
||||
for (const DebrisInfo& info : m_sim->getDebrisSystem().getAllDebrisInfo())
|
||||
for (const DebrisInfo& info : getAllDebrisInfo(m_sim->getAdmin()))
|
||||
{
|
||||
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
|
||||
!= m_selectedDebris.end())
|
||||
@@ -1510,7 +1510,7 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter)
|
||||
if (!m_selectedDebris.empty())
|
||||
{
|
||||
const qreal outlineRadius = static_cast<qreal>(getTilePx() * 0.2f) + 3.0;
|
||||
for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
|
||||
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
|
||||
{
|
||||
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), debris.entity)
|
||||
== m_selectedDebris.end()) { continue; }
|
||||
@@ -1605,8 +1605,8 @@ void GameWorldView::drawPortItems(QPainter& painter)
|
||||
|
||||
painter.save();
|
||||
painter.setClipRegion(clip);
|
||||
m_sim->getBuildings().forEachEmergingItem(drawItem);
|
||||
m_sim->getBuildings().forEachIncomingItem(drawItem);
|
||||
m_sim->getBuildings().forEachEmergingItem(m_sim->getFactoryState(), drawItem);
|
||||
m_sim->getBuildings().forEachIncomingItem(m_sim->getFactoryState(), drawItem);
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
@@ -1654,7 +1654,7 @@ void GameWorldView::drawBeltItems(QPainter& painter)
|
||||
void GameWorldView::drawDebris(QPainter& painter)
|
||||
{
|
||||
const float r = getTilePx() * 0.2f;
|
||||
for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
|
||||
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
|
||||
{
|
||||
const QPointF center = worldToWidget(debris.position);
|
||||
painter.setBrush(QColor(128, 110, 90));
|
||||
|
||||
Reference in New Issue
Block a user