41 Commits

Author SHA1 Message Date
1f4503176b make construction its own system (extracted from BuildingSystem) 2026-08-05 06:57:12 +02:00
fd85e8e10a free the buffer setup and belt registration from BuildingSystem 2026-08-05 06:55:13 +02:00
114a43b205 make BuildingSystem stateless: FactoryState becomes a parameter 2026-08-05 06:50:11 +02:00
d87d063b10 move the placement rules and the config-dependent queries off BuildingSystem 2026-08-05 06:49:49 +02:00
537597c854 delete the unused getAllBeltTiles and BeltTileInfo 2026-08-05 06:49:28 +02:00
9c3be0fbd0 extract the production rules as free functions over config and building 2026-08-05 06:49:15 +02:00
58b94223f7 migrate every factory query off BuildingSystem onto the free functions 2026-08-05 06:46:13 +02:00
1fb63cce4e move the asteroid width bound into FactoryState 2026-08-05 06:45:33 +02:00
0b7e94b4e4 drop CombatSystem's unused BuildingSystem parameter 2026-08-05 06:45:16 +02:00
0408336cf9 depend on factory data instead of BuildingSystem in the AI path 2026-08-05 06:44:49 +02:00
46932e4abf move FactoryState ownership out of BuildingSystem to Simulation 2026-08-05 06:43:52 +02:00
0edea5d961 gather the factory's world data into FactoryState 2026-08-05 06:43:30 +02:00
60cc187d92 add BuildingGrid to manage tile occupancy 2026-08-04 18:26:01 +02:00
3990351a16 share BuildingSystem's free functions instead of copying them 2026-08-04 18:24:38 +02:00
bd344e4fbe add FieldSelectionPanel for extracting the ships/stations/debris selection 2026-08-04 18:23:28 +02:00
64c344c3a3 correct the belt subsystem interface description in architecture.md 2026-08-04 18:12:02 +02:00
02c7fed9b4 allow "auto" for named local lambdas and iterator types via claude.md 2026-08-04 18:11:37 +02:00
5d4a975384 cover unlock state in the determinism tests 2026-08-04 18:11:03 +02:00
475df0e5fd extract unlock state from Simulation to UnlockState class 2026-08-04 18:10:43 +02:00
61634f6fd2 move the shared TOML helpers into the utility namespace to avoid name collisions 2026-08-04 18:05:52 +02:00
c8ff7da345 extract load methods into their own files 2026-08-04 18:05:27 +02:00
d664ab54cc extract shared TOML helpers into TomlHelpers.h/.cpp 2026-08-04 18:02:23 +02:00
906000b0e9 drop the dead payloads from the state-change events 2026-08-03 22:02:47 +02:00
d9ef6aa728 make HeaderBar read the tick, artifacts and boss wave from the simulation instead of event 2026-08-03 22:02:19 +02:00
b44a85685e make BlueprintPanel read the block stock from the simulation instead of event 2026-08-03 22:02:05 +02:00
edd1c31785 remove duplicate findModuleDef from ShipLayoutPreview 2026-08-03 22:00:25 +02:00
785ce3ebfe remove duplicate findBuildingDef from BuildingSystem 2026-08-03 21:14:13 +02:00
7017f8b4dc route the win-path restart through ResetCommand 2026-08-03 21:13:43 +02:00
77a842f884 dedupe AttackExecutor and RepairExecutor via executeOrbitAndAssign 2026-08-03 21:13:24 +02:00
d5ba72d554 add ModalPauseScope for the pause-around-modal idiom 2026-08-03 21:11:52 +02:00
83177729e9 extract MainWindow::reloadConfig 2026-08-03 21:11:02 +02:00
a8e933b7f2 drop EntityAdmin::add in favour of addComponent 2026-08-03 21:09:18 +02:00
e02e323cb2 share a single ItemIconCache across the UI 2026-08-03 21:08:19 +02:00
b4e622daa5 extract the shared Centroid helper into ai/Centroid.h 2026-08-03 21:06:06 +02:00
1150985c1f share one loadTestConfig() helper across the tests 2026-08-03 21:05:28 +02:00
594c3b93c5 make HeaderBar read block stock and expansion cost from the simulation 2026-08-03 21:04:43 +02:00
932b57720c dedupe tunnel lookup and key tunnel tiles by QPoint 2026-08-03 21:02:06 +02:00
ca727bef35 extract Simulation::initializeSubsystems to remove duplicate code 2026-08-03 20:59:00 +02:00
553a7e0701 add findShipDef/findModuleDef/findRecipeDef to config structs and re-use them in the rest of the code base 2026-08-03 20:57:31 +02:00
af6828c348 remove duplicate findBuildingDef from GameWorldView 2026-08-03 20:50:03 +02:00
3671e1d7e6 fix splitter filters being lost when rotating in place and add test 2026-08-03 20:49:10 +02:00
24 changed files with 690 additions and 393 deletions

View File

@@ -1,6 +1,3 @@
set(TARGET_BASE_NAME "${PRODUCT_NAME}") set(TARGET_BASE_NAME "${PRODUCT_NAME}")
set(TARGET_APP_NAME "${TARGET_BASE_NAME}") set(TARGET_APP_NAME "${TARGET_BASE_NAME}")

View File

@@ -324,9 +324,9 @@ void ArenaSimulation::tick()
// Ship behavior systems (tick step 7): evaluate, select winner, execute. // Ship behavior systems (tick step 7): evaluate, select winner, execute.
// Module + combat systems emit their tool beams into a shared buffer. // Module + combat systems emit their tool beams into a shared buffer.
m_shipSystem->clearMovementIntents(); m_shipSystem->clearMovementIntents();
m_aiSystem->tick(m_admin, m_factoryState); m_aiSystem->tick(m_admin, m_factoryState, *m_debrisSystem);
std::vector<BeamFiredEvent> beamFiredEvents; std::vector<BeamFiredEvent> beamFiredEvents;
m_salvagerSystem->tick(m_currentTick, m_factoryState, beamFiredEvents); m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, m_factoryState, beamFiredEvents);
m_repairSystem->tick(m_currentTick, beamFiredEvents); m_repairSystem->tick(m_currentTick, beamFiredEvents);
// Combat resolution (tick step 8). // Combat resolution (tick step 8).

View File

@@ -340,7 +340,7 @@ void ArenaView::drawBuildings(QPainter& painter)
void ArenaView::drawDebris(QPainter& painter) void ArenaView::drawDebris(QPainter& painter)
{ {
const float r = getTilePx() * 0.2f; const float r = getTilePx() * 0.2f;
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin())) for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
{ {
const QPointF center = worldToWidget(debris.position); const QPointF center = worldToWidget(debris.position);
painter.setBrush(QColor(128, 110, 90)); painter.setBrush(QColor(128, 110, 90));

View File

@@ -43,7 +43,8 @@ AiSystem::AiSystem(const GameConfig& config)
{ {
} }
void AiSystem::tick(EntityAdmin& admin, const FactoryState& state) void AiSystem::tick(EntityAdmin& admin, const FactoryState& state,
const DebrisSystem& debris)
{ {
TRACE(); TRACE();
@@ -54,7 +55,7 @@ void AiSystem::tick(EntityAdmin& admin, const FactoryState& state)
m_retreatEvaluator.evaluate(admin); m_retreatEvaluator.evaluate(admin);
m_attackEvaluator.evaluate(admin); m_attackEvaluator.evaluate(admin);
m_repairEvaluator.evaluate(admin); m_repairEvaluator.evaluate(admin);
m_salvageScrapEvaluator.evaluate(admin); m_salvageScrapEvaluator.evaluate(admin, debris);
m_deliverScrapEvaluator.evaluate(admin, state); m_deliverScrapEvaluator.evaluate(admin, state);
// Phase 2: pick the highest-scoring behavior per ship. // Phase 2: pick the highest-scoring behavior per ship.

View File

@@ -20,6 +20,7 @@
#include "StandbyExecutor.h" #include "StandbyExecutor.h"
class EntityAdmin; class EntityAdmin;
class DebrisSystem;
struct GameConfig; struct GameConfig;
// Orchestrates ship-behavior decision-making in three batched phases: // Orchestrates ship-behavior decision-making in three batched phases:
@@ -34,7 +35,7 @@ class AiSystem
public: public:
explicit AiSystem(const GameConfig& config); explicit AiSystem(const GameConfig& config);
void tick(EntityAdmin& admin, const FactoryState& state); void tick(EntityAdmin& admin, const FactoryState& state, const DebrisSystem& debris);
private: private:
void selectWinningBehaviors(EntityAdmin& admin); void selectWinningBehaviors(EntityAdmin& admin);

View File

@@ -46,13 +46,13 @@ std::optional<int> DebrisSystem::consume(entt::entity entity)
return amount; return amount;
} }
bool collectOne(EntityAdmin& admin, entt::entity entity) bool DebrisSystem::collectOne(entt::entity entity)
{ {
if (!admin.isValid(entity) || !admin.hasAll<DebrisComponent>(entity)) if (!m_admin.isValid(entity) || !m_admin.hasAll<DebrisComponent>(entity))
{ {
return false; return false;
} }
DebrisComponent& data = admin.get<DebrisComponent>(entity); DebrisComponent& data = m_admin.get<DebrisComponent>(entity);
if (data.amount <= 0) if (data.amount <= 0)
{ {
return false; return false;
@@ -60,18 +60,18 @@ bool collectOne(EntityAdmin& admin, entt::entity entity)
--data.amount; --data.amount;
if (data.amount <= 0) if (data.amount <= 0)
{ {
admin.destroy(entity); m_admin.destroy(entity);
} }
return true; return true;
} }
std::vector<DebrisInfo> getAllDebrisInfo(const EntityAdmin& admin) std::vector<DebrisInfo> DebrisSystem::getAllDebrisInfo() const
{ {
std::vector<DebrisInfo> result; std::vector<DebrisInfo> result;
admin.forEach<DebrisComponent>( m_admin.forEach<DebrisComponent>(
[&result, &admin](entt::entity e, const DebrisComponent& sd) [&result, this](entt::entity e, const DebrisComponent& sd)
{ {
result.push_back(DebrisInfo{e, admin.get<PositionComponent>(e).value, sd.amount}); result.push_back(DebrisInfo{e, m_admin.get<PositionComponent>(e).value, sd.amount});
}); });
return result; return result;
} }

View File

@@ -38,17 +38,9 @@ public:
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE). // false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
bool collectOne(entt::entity entity); bool collectOne(entt::entity entity);
// Lightweight snapshot for callers that need to iterate all debris.
std::vector<DebrisInfo> getAllDebrisInfo() const;
private: private:
EntityAdmin& m_admin; 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);

View File

@@ -24,14 +24,14 @@ SalvagerSystem::SalvagerSystem(EntityAdmin& admin)
{ {
} }
void SalvagerSystem::tick(Tick currentTick, FactoryState& state, void SalvagerSystem::tick(Tick currentTick, DebrisSystem& debris, FactoryState& state,
std::vector<BeamFiredEvent>& outBeamFiredEvents) std::vector<BeamFiredEvent>& outBeamFiredEvents)
{ {
TRACE(); TRACE();
// Apply collections whose mid-beam delay has elapsed (cycles started earlier). // Apply collections whose mid-beam delay has elapsed (cycles started earlier).
applyPendingCollections(currentTick); applyPendingCollections(currentTick, debris);
const std::vector<DebrisInfo> allDebris = getAllDebrisInfo(m_admin); const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
// Tick down per-module collection cooldowns. // Tick down per-module collection cooldowns.
m_admin.forEach<SalvagerComponent>( m_admin.forEach<SalvagerComponent>(
@@ -108,7 +108,7 @@ void SalvagerSystem::tick(Tick currentTick, FactoryState& state,
}); });
} }
void SalvagerSystem::applyPendingCollections(Tick currentTick) void SalvagerSystem::applyPendingCollections(Tick currentTick, DebrisSystem& debris)
{ {
std::vector<PendingCollection>::iterator it = m_pendingCollections.begin(); std::vector<PendingCollection>::iterator it = m_pendingCollections.begin();
while (it != m_pendingCollections.end()) while (it != m_pendingCollections.end())
@@ -118,7 +118,7 @@ void SalvagerSystem::applyPendingCollections(Tick currentTick)
if (m_admin.isValid(it->ship) && m_admin.hasAll<CargoComponent>(it->ship)) if (m_admin.isValid(it->ship) && m_admin.hasAll<CargoComponent>(it->ship))
{ {
CargoComponent& cargo = m_admin.get<CargoComponent>(it->ship); CargoComponent& cargo = m_admin.get<CargoComponent>(it->ship);
if (cargo.current < cargo.maxCapacity && collectOne(m_admin, it->debris)) if (cargo.current < cargo.maxCapacity && debris.collectOne(it->debris))
{ {
++cargo.current; ++cargo.current;
} }

View File

@@ -10,6 +10,7 @@
#include "entt/entity/entity.hpp" #include "entt/entity/entity.hpp"
class EntityAdmin; class EntityAdmin;
class DebrisSystem;
// World-mutation system for salvage modules: each module runs a collection cycle // 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 // on its own cooldown. When a cycle starts it emits a salvage beam toward an
@@ -21,7 +22,7 @@ class SalvagerSystem
public: public:
explicit SalvagerSystem(EntityAdmin& admin); explicit SalvagerSystem(EntityAdmin& admin);
void tick(Tick currentTick, FactoryState& state, void tick(Tick currentTick, DebrisSystem& debris, FactoryState& state,
std::vector<BeamFiredEvent>& outBeamFiredEvents); std::vector<BeamFiredEvent>& outBeamFiredEvents);
private: private:
@@ -32,7 +33,7 @@ private:
Tick appliesAt; Tick appliesAt;
}; };
void applyPendingCollections(Tick currentTick); void applyPendingCollections(Tick currentTick, DebrisSystem& debris);
EntityAdmin& m_admin; EntityAdmin& m_admin;
std::vector<PendingCollection> m_pendingCollections; std::vector<PendingCollection> m_pendingCollections;

View File

@@ -15,11 +15,11 @@
#include "SensorRangeComponent.h" #include "SensorRangeComponent.h"
#include "tracing.h" #include "tracing.h"
void SalvageScrapEvaluator::evaluate(EntityAdmin& admin) void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const DebrisSystem& debris)
{ {
TRACE(); TRACE();
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin); const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
const std::vector<DebrisInfo> allDebris = getAllDebrisInfo(admin); const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
admin.forEach<SalvageScrapBehavior, PositionComponent, SensorRangeComponent>( admin.forEach<SalvageScrapBehavior, PositionComponent, SensorRangeComponent>(
[&](entt::entity e, SalvageScrapBehavior& salvage, const PositionComponent& pos, [&](entt::entity e, SalvageScrapBehavior& salvage, const PositionComponent& pos,

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
class EntityAdmin; class EntityAdmin;
class DebrisSystem;
// When cargo is not full, finds the nearest debris within sensor range and sets // 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 // it as the target, scoring high. Scores inactive when cargo is full or no debris
@@ -8,5 +9,5 @@ class EntityAdmin;
class SalvageScrapEvaluator class SalvageScrapEvaluator
{ {
public: public:
void evaluate(EntityAdmin& admin); void evaluate(EntityAdmin& admin, const DebrisSystem& debris);
}; };

View File

@@ -181,7 +181,7 @@ int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick current
state.deconstructionQueue.push_back(std::move(entry)); state.deconstructionQueue.push_back(std::move(entry));
if (wasEmpty) if (wasEmpty)
{ {
startFrontDeconstruction(state, m_config, currentTick); startFrontDeconstruction(state, currentTick);
} }
return 0; return 0;
} }
@@ -189,6 +189,17 @@ int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick current
return 0; return 0;
} }
void BuildingSystem::startFrontDeconstruction(FactoryState& state, Tick currentTick)
{
if (state.deconstructionQueue.empty()) { return; }
DeconstructionEntry& front = state.deconstructionQueue.front();
if (front.completesAt == 0)
{
front.completesAt =
currentTick + secondsToTicks(m_config.world.deconstructionTimeSeconds);
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Set recipe // Set recipe
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -323,6 +334,53 @@ void BuildingSystem::setSiteSplitterFilters(FactoryState& state, BuildingId id,
// Tick hooks // Tick hooks
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void BuildingSystem::tickDeconstruction(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, 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, currentTick);
}
void BuildingSystem::cancelDeconstruction(FactoryState& state, BuildingId id) void BuildingSystem::cancelDeconstruction(FactoryState& state, BuildingId id)
{ {
for (std::deque<DeconstructionEntry>::iterator it = state.deconstructionQueue.begin(); for (std::deque<DeconstructionEntry>::iterator it = state.deconstructionQueue.begin();

View File

@@ -17,7 +17,6 @@
#include "Building.h" #include "Building.h"
#include "FactoryState.h" #include "FactoryState.h"
#include "BuildingBuffers.h" #include "BuildingBuffers.h"
#include "DeconstructionSystem.h"
#include "PlacementRules.h" #include "PlacementRules.h"
#include "ProductionRules.h" #include "ProductionRules.h"
#include "BuildingType.h" #include "BuildingType.h"
@@ -108,6 +107,7 @@ public:
// Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a // Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a
// time, in parallel with tickConstruction. Removes the front building and // time, in parallel with tickConstruction. Removes the front building and
// credits its refund when its timer elapses. // credits its refund when its timer elapses.
void tickDeconstruction(FactoryState& state, Tick currentTick);
void tickBeltPull(FactoryState& state); void tickBeltPull(FactoryState& state);
void tickProduction(FactoryState& state, Tick currentTick); void tickProduction(FactoryState& state, Tick currentTick);
void tickShipyardProduction(FactoryState& state, Tick currentTick); void tickShipyardProduction(FactoryState& state, Tick currentTick);
@@ -183,6 +183,7 @@ public:
private: private:
// Starts the front deconstruction-queue entry's timer if not yet started // Starts the front deconstruction-queue entry's timer if not yet started
// (mirrors how tickConstruction starts a queued construction site). // (mirrors how tickConstruction starts a queued construction site).
void startFrontDeconstruction(FactoryState& state, Tick currentTick);
// Registers a belt/splitter/tunnel building's tile with the belt subsystem // Registers a belt/splitter/tunnel building's tile with the belt subsystem
// (on construction completion, or when un-queuing a deconstruction). No-op for // (on construction completion, or when un-queuing a deconstruction). No-op for

View File

@@ -14,7 +14,6 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.h
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h
${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h ${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h ${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h
@@ -46,7 +45,6 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp ${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp

View File

@@ -1,67 +0,0 @@
#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);
}

View File

@@ -1,36 +0,0 @@
#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);

View File

@@ -2,7 +2,6 @@
#include "FactoryQueries.h" #include "FactoryQueries.h"
#include "ConstructionSystem.h" #include "ConstructionSystem.h"
#include "DeconstructionSystem.h"
#include "PlacementRules.h" #include "PlacementRules.h"
#include <algorithm> #include <algorithm>
@@ -126,8 +125,6 @@ void Simulation::initializeSubsystems()
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); }, [this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
m_rng); m_rng);
m_constructionSystem = std::make_unique<ConstructionSystem>(m_config); 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_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
m_aiSystem = std::make_unique<AiSystem>(m_config); m_aiSystem = std::make_unique<AiSystem>(m_config);
m_movementIntentSystem = std::make_unique<MovementIntentSystem>(); m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
@@ -249,7 +246,7 @@ void Simulation::tick()
// Construction + production pipeline // Construction + production pipeline
m_constructionSystem->tick(m_factoryState, m_beltSystem, m_currentTick); m_constructionSystem->tick(m_factoryState, m_beltSystem, m_currentTick);
m_deconstructionSystem->tick(m_factoryState, m_currentTick); // parallel to construction m_buildingSystem->tickDeconstruction(m_factoryState, m_currentTick); // parallel to construction
m_buildingSystem->tickBeltPull(m_factoryState); // step 3 m_buildingSystem->tickBeltPull(m_factoryState); // step 3
m_buildingSystem->tickProduction(m_factoryState, m_currentTick); // step 4 m_buildingSystem->tickProduction(m_factoryState, m_currentTick); // step 4
m_buildingSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b m_buildingSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b
@@ -268,10 +265,10 @@ void Simulation::tick()
m_shipSystem->clearMovementIntents(); m_shipSystem->clearMovementIntents();
// Score-based behavior selection: evaluate, select winner, execute (sets // Score-based behavior selection: evaluate, select winner, execute (sets
// movement intent + preferred module targets only — no world mutation). // movement intent + preferred module targets only — no world mutation).
m_aiSystem->tick(m_admin, m_factoryState); m_aiSystem->tick(m_admin, m_factoryState, *m_debrisSystem);
// Module systems perform the world mutation (collection/delivery, healing). // Module systems perform the world mutation (collection/delivery, healing).
// Each emits its tool beams and applies its own delayed (mid-beam) effects. // Each emits its tool beams and applies its own delayed (mid-beam) effects.
m_salvagerSystem->tick(m_currentTick, m_factoryState, m_beamFiredEvents); m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, m_factoryState, m_beamFiredEvents);
m_repairSystem->tick(m_currentTick, m_beamFiredEvents); m_repairSystem->tick(m_currentTick, m_beamFiredEvents);
// Step 8: combat resolution // Step 8: combat resolution

View File

@@ -26,7 +26,6 @@
class AiSystem; class AiSystem;
class BuildingSystem; class BuildingSystem;
class ConstructionSystem; class ConstructionSystem;
class DeconstructionSystem;
struct Command; struct Command;
class Hasher; class Hasher;
class CombatSystem; class CombatSystem;
@@ -221,7 +220,6 @@ private:
BeltSystem m_beltSystem; BeltSystem m_beltSystem;
std::unique_ptr<BuildingSystem> m_buildingSystem; std::unique_ptr<BuildingSystem> m_buildingSystem;
std::unique_ptr<ConstructionSystem> m_constructionSystem; std::unique_ptr<ConstructionSystem> m_constructionSystem;
std::unique_ptr<DeconstructionSystem> m_deconstructionSystem;
std::unique_ptr<ShipSystem> m_shipSystem; std::unique_ptr<ShipSystem> m_shipSystem;
std::unique_ptr<AiSystem> m_aiSystem; std::unique_ptr<AiSystem> m_aiSystem;
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem; std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;

View File

@@ -101,14 +101,14 @@ struct Fixture
void decide() void decide()
{ {
ships.clearMovementIntents(); ships.clearMovementIntents();
ai.tick(admin, state); ai.tick(admin, state, scraps);
} }
// World mutation: collection/delivery and healing. // World mutation: collection/delivery and healing.
void runModules() void runModules()
{ {
beamEvents.clear(); beamEvents.clear();
salvager.tick(tick, state, beamEvents); salvager.tick(tick, scraps, state, beamEvents);
repair.tick(tick, beamEvents); repair.tick(tick, beamEvents);
} }
@@ -141,7 +141,7 @@ struct Fixture
void salvageTick() void salvageTick()
{ {
beamEvents.clear(); beamEvents.clear();
salvager.tick(tick, state, beamEvents); salvager.tick(tick, scraps, state, beamEvents);
++tick; ++tick;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -415,7 +415,7 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
sim.tick(); sim.tick();
const std::vector<DebrisInfo> scraps = getAllDebrisInfo(sim.getAdmin()); const std::vector<DebrisInfo> scraps = sim.getDebrisSystem().getAllDebrisInfo();
REQUIRE(scraps.size() == 1); REQUIRE(scraps.size() == 1);
CHECK(sim.getAdmin().get<DebrisComponent>(scraps[0].entity).amount == 59); CHECK(sim.getAdmin().get<DebrisComponent>(scraps[0].entity).amount == 59);
} }

View File

@@ -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); const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 3, 100);
REQUIRE(collectOne(admin, e)); REQUIRE(ss.collectOne(e));
REQUIRE(admin.isValid(e)); REQUIRE(admin.isValid(e));
REQUIRE(admin.get<DebrisComponent>(e).amount == 2); REQUIRE(admin.get<DebrisComponent>(e).amount == 2);
REQUIRE(collectOne(admin, e)); REQUIRE(ss.collectOne(e));
REQUIRE(admin.isValid(e)); REQUIRE(admin.isValid(e));
REQUIRE(admin.get<DebrisComponent>(e).amount == 1); REQUIRE(admin.get<DebrisComponent>(e).amount == 1);
// Final unit collected: the debris is removed once depleted. // Final unit collected: the debris is removed once depleted.
REQUIRE(collectOne(admin, e)); REQUIRE(ss.collectOne(e));
REQUIRE_FALSE(admin.isValid(e)); REQUIRE_FALSE(admin.isValid(e));
} }
@@ -134,7 +134,7 @@ TEST_CASE("DebrisSystem: collectOne returns false for an invalid entity", "[debr
EntityAdmin admin; EntityAdmin admin;
DebrisSystem ss(admin); DebrisSystem ss(admin);
REQUIRE_FALSE(collectOne(admin, entt::null)); REQUIRE_FALSE(ss.collectOne(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(1.0f, 2.0f), 3, 100);
ss.spawn(QVector2D(4.0f, 5.0f), 6, 200); ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
const std::vector<DebrisInfo> info = getAllDebrisInfo(admin); const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
REQUIRE(info.size() == 2); 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 a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200); const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
const std::vector<DebrisInfo> info = getAllDebrisInfo(admin); const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
REQUIRE(info.size() == 2); REQUIRE(info.size() == 2);
for (const DebrisInfo& i : info) for (const DebrisInfo& i : info)
{ {

View File

@@ -367,7 +367,7 @@ int FieldSelectionPanel::selectedDebrisScrapTotal() const
{ {
// Sum the remaining scrap across the still-living selected debris (REQ-UI-DEBRIS-PANEL). // Sum the remaining scrap across the still-living selected debris (REQ-UI-DEBRIS-PANEL).
int total = 0; int total = 0;
for (const DebrisInfo& info : getAllDebrisInfo(m_sim->getAdmin())) for (const DebrisInfo& info : m_sim->getDebrisSystem().getAllDebrisInfo())
{ {
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity) if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
!= m_selectedDebris.end()) != m_selectedDebris.end())

View File

@@ -777,7 +777,7 @@ void GameWorldView::pruneDespawnedDebris()
if (m_selectedDebris.empty()) { return; } if (m_selectedDebris.empty()) { return; }
std::vector<entt::entity> live; std::vector<entt::entity> live;
for (const DebrisInfo& info : getAllDebrisInfo(m_sim->getAdmin())) for (const DebrisInfo& info : m_sim->getDebrisSystem().getAllDebrisInfo())
{ {
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity) if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
!= m_selectedDebris.end()) != m_selectedDebris.end())
@@ -1510,7 +1510,7 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter)
if (!m_selectedDebris.empty()) if (!m_selectedDebris.empty())
{ {
const qreal outlineRadius = static_cast<qreal>(getTilePx() * 0.2f) + 3.0; const qreal outlineRadius = static_cast<qreal>(getTilePx() * 0.2f) + 3.0;
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin())) for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
{ {
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), debris.entity) if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), debris.entity)
== m_selectedDebris.end()) { continue; } == m_selectedDebris.end()) { continue; }
@@ -1654,7 +1654,7 @@ void GameWorldView::drawBeltItems(QPainter& painter)
void GameWorldView::drawDebris(QPainter& painter) void GameWorldView::drawDebris(QPainter& painter)
{ {
const float r = getTilePx() * 0.2f; const float r = getTilePx() * 0.2f;
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin())) for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
{ {
const QPointF center = worldToWidget(debris.position); const QPointF center = worldToWidget(debris.position);
painter.setBrush(QColor(128, 110, 90)); painter.setBrush(QColor(128, 110, 90));