Implement the deconstruction queue
Demolishing a fully-built building now queues it for timed deconstruction (world.toml deconstruction_time_seconds, default 0.1) running in parallel with the construction queue, instead of removing it instantly. The partial refund is credited on completion. A queued building stops operating at once (belt/tunnel/splitter tiles unregister from the belt subsystem, re-pairing tunnels) but keeps occupying its tiles. Construction sites are still removed instantly with the full refund. Clicking a queued building un-queues it (new CancelDeconstructionCommand); a demolish drag-box un-queues all covered built buildings when they are all already queued, otherwise queues the rest. Queued buildings render with the demolish tint. Implements REQ-BLD-DECON-QUEUE and the updated REQ-BLD-DEMOLISH / REQ-BLD-DEMOLISH-CLICK / REQ-BLD-DEMOLISH-BOX / REQ-BLD-TUNNEL-PAIR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
[world]
|
[world]
|
||||||
height_tiles = 40
|
height_tiles = 40
|
||||||
refund_percentage = 100
|
refund_percentage = 100
|
||||||
|
deconstruction_time_seconds = 0.1
|
||||||
starting_building_blocks = 200
|
starting_building_blocks = 200
|
||||||
scrap_despawn_seconds = 120
|
scrap_despawn_seconds = 120
|
||||||
scrap_per_threat = 0.25
|
scrap_per_threat = 0.25
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[world]
|
[world]
|
||||||
height_tiles = 60
|
height_tiles = 60
|
||||||
refund_percentage = 75
|
refund_percentage = 75
|
||||||
|
deconstruction_time_seconds = 0.1
|
||||||
starting_building_blocks = 100
|
starting_building_blocks = 100
|
||||||
scrap_despawn_seconds = 30
|
scrap_despawn_seconds = 30
|
||||||
scrap_per_threat = 1.0
|
scrap_per_threat = 1.0
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
|||||||
|
|
||||||
cfg.heightTiles = static_cast<int>(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
|
cfg.heightTiles = static_cast<int>(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
|
||||||
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
|
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
|
||||||
|
cfg.deconstructionTimeSeconds = requireDouble(tbl["world"]["deconstruction_time_seconds"], file, "world.deconstruction_time_seconds");
|
||||||
cfg.startingBuildingBlocks = static_cast<int>(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks"));
|
cfg.startingBuildingBlocks = static_cast<int>(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks"));
|
||||||
cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds");
|
cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds");
|
||||||
cfg.scrapPerThreat = requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat");
|
cfg.scrapPerThreat = requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat");
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ struct WorldConfig
|
|||||||
{
|
{
|
||||||
int heightTiles; // REQ-GW-HEIGHT
|
int heightTiles; // REQ-GW-HEIGHT
|
||||||
int refundPercentage; // REQ-BLD-DEMOLISH
|
int refundPercentage; // REQ-BLD-DEMOLISH
|
||||||
|
double deconstructionTimeSeconds; // REQ-BLD-DECON-QUEUE
|
||||||
int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS
|
int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS
|
||||||
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
|
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
|
||||||
double scrapPerThreat; // REQ-RES-SCRAP-DROP, REQ-THREAT-SCRAP (scrap dropped per unit threat)
|
double scrapPerThreat; // REQ-RES-SCRAP-DROP, REQ-THREAT-SCRAP (scrap dropped per unit threat)
|
||||||
|
|||||||
@@ -129,5 +129,10 @@ struct Building
|
|||||||
|
|
||||||
// Module layout for shipyards (REQ-MOD-LAYOUT).
|
// Module layout for shipyards (REQ-MOD-LAYOUT).
|
||||||
std::optional<ShipLayoutConfig> shipLayout;
|
std::optional<ShipLayoutConfig> shipLayout;
|
||||||
|
|
||||||
|
// True while this building sits in the deconstruction queue (REQ-BLD-DECON-QUEUE).
|
||||||
|
// A queued building stops operating immediately (all tick loops skip it) but keeps
|
||||||
|
// occupying its tiles until its deconstruction completes.
|
||||||
|
bool queuedForDeconstruction = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,17 @@ bool isAutoRecipeBuildingType(BuildingType type)
|
|||||||
|| type == BuildingType::ReprocessingPlant;
|
|| type == BuildingType::ReprocessingPlant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Belts, splitters, and tunnel ends keep their runtime data in the belt subsystem
|
||||||
|
// rather than in the Building instance, so placing/removing them must register or
|
||||||
|
// unregister a tile with BeltSystem.
|
||||||
|
bool isBeltSubsystemType(BuildingType type)
|
||||||
|
{
|
||||||
|
return type == BuildingType::Belt
|
||||||
|
|| type == BuildingType::Splitter
|
||||||
|
|| type == BuildingType::TunnelEntry
|
||||||
|
|| type == BuildingType::TunnelExit;
|
||||||
|
}
|
||||||
|
|
||||||
// The building body tile that owns an output port, given the port's outside tile
|
// The building body tile that owns an output port, given the port's outside tile
|
||||||
// (port.tile) and its facing direction. The virtual output belt occupies this tile
|
// (port.tile) and its facing direction. The virtual output belt occupies this tile
|
||||||
// and flows toward port.tile (REQ-MAT-OUTPUT-EMERGE).
|
// and flows toward port.tile (REQ-MAT-OUTPUT-EMERGE).
|
||||||
@@ -489,9 +500,10 @@ bool BuildingSystem::isPlacementValid(BuildingType type, QPoint anchor,
|
|||||||
// Demolish
|
// Demolish
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
int BuildingSystem::demolish(BuildingId id)
|
int BuildingSystem::demolish(BuildingId id, Tick currentTick)
|
||||||
{
|
{
|
||||||
// Construction queue?
|
// Construction site? Removed instantly with the full refund; never queued
|
||||||
|
// for deconstruction (REQ-BLD-DEMOLISH).
|
||||||
for (std::deque<ConstructionSite>::iterator it = m_constructionQueue.begin();
|
for (std::deque<ConstructionSite>::iterator it = m_constructionQueue.begin();
|
||||||
it != m_constructionQueue.end();
|
it != m_constructionQueue.end();
|
||||||
++it)
|
++it)
|
||||||
@@ -512,35 +524,60 @@ int BuildingSystem::demolish(BuildingId id)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Operational building?
|
// Operational building? Append it to the deconstruction queue rather than
|
||||||
for (std::vector<Building>::iterator it = m_buildings.begin();
|
// removing it now; the partial refund is credited on completion in
|
||||||
it != m_buildings.end();
|
// tickDeconstruction (REQ-BLD-DECON-QUEUE).
|
||||||
++it)
|
for (Building& building : m_buildings)
|
||||||
{
|
{
|
||||||
if (it->id == id)
|
if (building.id != id) { continue; }
|
||||||
|
if (building.queuedForDeconstruction) { return 0; } // already queued
|
||||||
|
|
||||||
|
building.queuedForDeconstruction = true;
|
||||||
|
|
||||||
|
DeconstructionEntry entry;
|
||||||
|
entry.id = id;
|
||||||
|
|
||||||
|
// A queued belt/tunnel/splitter stops transporting at once: capture a
|
||||||
|
// splitter's filters (so an un-queue can restore them), then unregister
|
||||||
|
// its tile, discarding items on it and re-pairing tunnels as if it were
|
||||||
|
// gone (REQ-BLD-TUNNEL-PAIR).
|
||||||
|
if (building.type == BuildingType::Splitter)
|
||||||
{
|
{
|
||||||
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter
|
if (const std::optional<BeltSystem::SplitterInfo> info =
|
||||||
|| it->type == BuildingType::TunnelEntry || it->type == BuildingType::TunnelExit)
|
m_belts.getSplitterInfo(building.anchor))
|
||||||
{
|
{
|
||||||
m_belts.removeTile(it->anchor);
|
entry.splitterFilterA = info->filterA;
|
||||||
|
entry.splitterFilterB = info->filterB;
|
||||||
}
|
}
|
||||||
const BuildingDef* def = findBuildingDef(it->type);
|
|
||||||
for (const QPoint& cell : it->bodyCells)
|
|
||||||
{
|
|
||||||
m_tileOccupancy.erase({cell.x(), cell.y()});
|
|
||||||
}
|
|
||||||
m_buildings.erase(it);
|
|
||||||
if (def)
|
|
||||||
{
|
|
||||||
return def->cost * m_config.world.refundPercentage / 100;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
if (isBeltSubsystemType(building.type))
|
||||||
|
{
|
||||||
|
m_belts.removeTile(building.anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool wasEmpty = m_deconstructionQueue.empty();
|
||||||
|
m_deconstructionQueue.push_back(std::move(entry));
|
||||||
|
if (wasEmpty)
|
||||||
|
{
|
||||||
|
startFrontDeconstruction(currentTick);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BuildingSystem::startFrontDeconstruction(Tick currentTick)
|
||||||
|
{
|
||||||
|
if (m_deconstructionQueue.empty()) { return; }
|
||||||
|
DeconstructionEntry& front = m_deconstructionQueue.front();
|
||||||
|
if (front.completesAt == 0)
|
||||||
|
{
|
||||||
|
front.completesAt =
|
||||||
|
currentTick + secondsToTicks(m_config.world.deconstructionTimeSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Set recipe
|
// Set recipe
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -780,31 +817,9 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register with BeltSystem before the move (mask stays valid).
|
// Register with BeltSystem before the move (mask/building stays valid). Any
|
||||||
if (front.type == BuildingType::Belt)
|
// filters configured while under construction carry over (REQ-BLD-SITE-CONFIG).
|
||||||
{
|
reregisterBeltTile(building, front.splitterFilterA, front.splitterFilterB);
|
||||||
m_belts.placeBelt(front.anchor, front.rotation);
|
|
||||||
}
|
|
||||||
else if (front.type == BuildingType::Splitter)
|
|
||||||
{
|
|
||||||
assert(mask.outputPorts.size() >= 2);
|
|
||||||
m_belts.placeSplitter(front.anchor,
|
|
||||||
mask.outputPorts[0].direction,
|
|
||||||
mask.outputPorts[1].direction);
|
|
||||||
// Carry over any filters configured while under construction
|
|
||||||
// (REQ-BLD-SITE-CONFIG).
|
|
||||||
m_belts.setSplitterFilters(front.anchor,
|
|
||||||
front.splitterFilterA,
|
|
||||||
front.splitterFilterB);
|
|
||||||
}
|
|
||||||
else if (front.type == BuildingType::TunnelEntry)
|
|
||||||
{
|
|
||||||
m_belts.placeTunnelEntry(front.anchor, front.rotation, m_config.world.tunnelMaxDistance_tiles);
|
|
||||||
}
|
|
||||||
else if (front.type == BuildingType::TunnelExit)
|
|
||||||
{
|
|
||||||
m_belts.placeTunnelExit(front.anchor, front.rotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_buildings.push_back(std::move(building));
|
m_buildings.push_back(std::move(building));
|
||||||
|
|
||||||
@@ -822,6 +837,114 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_deconstructionQueue.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeconstructionEntry& front = m_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-DEMOLISH).
|
||||||
|
// Belt/tunnel/splitter tiles were already unregistered when the building was
|
||||||
|
// queued (see demolish), so only tile occupancy and the record remain.
|
||||||
|
for (std::vector<Building>::iterator it = m_buildings.begin();
|
||||||
|
it != m_buildings.end();
|
||||||
|
++it)
|
||||||
|
{
|
||||||
|
if (it->id != front.id) { continue; }
|
||||||
|
|
||||||
|
const BuildingDef* def = findBuildingDef(it->type);
|
||||||
|
for (const QPoint& cell : it->bodyCells)
|
||||||
|
{
|
||||||
|
m_tileOccupancy.erase({cell.x(), cell.y()});
|
||||||
|
}
|
||||||
|
m_buildings.erase(it);
|
||||||
|
if (def)
|
||||||
|
{
|
||||||
|
m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_deconstructionQueue.pop_front();
|
||||||
|
|
||||||
|
// Start the next queued deconstruction, if any.
|
||||||
|
startFrontDeconstruction(currentTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildingSystem::cancelDeconstruction(BuildingId id)
|
||||||
|
{
|
||||||
|
for (std::deque<DeconstructionEntry>::iterator it = m_deconstructionQueue.begin();
|
||||||
|
it != m_deconstructionQueue.end();
|
||||||
|
++it)
|
||||||
|
{
|
||||||
|
if (it->id != id) { continue; }
|
||||||
|
|
||||||
|
// 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 = findBuildingMutable(id))
|
||||||
|
{
|
||||||
|
building->queuedForDeconstruction = false;
|
||||||
|
reregisterBeltTile(*building, it->splitterFilterA, it->splitterFilterB);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_deconstructionQueue.erase(it);
|
||||||
|
// If the running front was removed, the new front (completesAt == 0) has
|
||||||
|
// its timer started by the next tickDeconstruction guard.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BuildingSystem::isQueuedForDeconstruction(BuildingId id) const
|
||||||
|
{
|
||||||
|
const Building* building = findBuilding(id);
|
||||||
|
return building && building->queuedForDeconstruction;
|
||||||
|
}
|
||||||
|
|
||||||
void BuildingSystem::tickBeltPull()
|
void BuildingSystem::tickBeltPull()
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
@@ -831,6 +954,9 @@ void BuildingSystem::tickBeltPull()
|
|||||||
|
|
||||||
for (Building& building : m_buildings)
|
for (Building& building : m_buildings)
|
||||||
{
|
{
|
||||||
|
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||||
|
if (building.queuedForDeconstruction) { continue; }
|
||||||
|
|
||||||
const bool isHq = (building.type == BuildingType::Hq);
|
const bool isHq = (building.type == BuildingType::Hq);
|
||||||
|
|
||||||
// 1. Advance every input belt and deliver arrivals (progress >= 0.5) into
|
// 1. Advance every input belt and deliver arrivals (progress >= 0.5) into
|
||||||
@@ -922,6 +1048,10 @@ bool BuildingSystem::tryDirectCoupleDeposit(BuildingId producerId,
|
|||||||
{
|
{
|
||||||
return false; // an unbuilt construction site, or not an operational building
|
return false; // an unbuilt construction site, or not an operational building
|
||||||
}
|
}
|
||||||
|
if (consumer->queuedForDeconstruction)
|
||||||
|
{
|
||||||
|
return false; // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
|
||||||
|
}
|
||||||
|
|
||||||
// The coupling is the consumer input port meeting this output port: same flow
|
// The coupling is the consumer input port meeting this output port: same flow
|
||||||
// direction, feeding the producer's output-port tile (REQ-MAT-DIRECT-COUPLE).
|
// direction, feeding the producer's output-port tile (REQ-MAT-DIRECT-COUPLE).
|
||||||
@@ -943,6 +1073,9 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
|||||||
TRACE();
|
TRACE();
|
||||||
for (Building& building : m_buildings)
|
for (Building& building : m_buildings)
|
||||||
{
|
{
|
||||||
|
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||||
|
if (building.queuedForDeconstruction) { continue; }
|
||||||
|
|
||||||
// Skip types without a recipe-based production loop.
|
// Skip types without a recipe-based production loop.
|
||||||
if (building.type == BuildingType::Belt ||
|
if (building.type == BuildingType::Belt ||
|
||||||
building.type == BuildingType::Splitter ||
|
building.type == BuildingType::Splitter ||
|
||||||
@@ -1043,6 +1176,9 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
|||||||
TRACE();
|
TRACE();
|
||||||
for (Building& building : m_buildings)
|
for (Building& building : m_buildings)
|
||||||
{
|
{
|
||||||
|
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||||
|
if (building.queuedForDeconstruction) { continue; }
|
||||||
|
|
||||||
if (building.type != BuildingType::Shipyard)
|
if (building.type != BuildingType::Shipyard)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -1140,6 +1276,9 @@ void BuildingSystem::tickOutputBelts()
|
|||||||
|
|
||||||
for (Building& building : m_buildings)
|
for (Building& building : m_buildings)
|
||||||
{
|
{
|
||||||
|
// A building queued for deconstruction stops operating (REQ-BLD-DECON-QUEUE).
|
||||||
|
if (building.queuedForDeconstruction) { continue; }
|
||||||
|
|
||||||
for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
|
for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
|
||||||
{
|
{
|
||||||
const Port& port = building.outputPorts[p];
|
const Port& port = building.outputPorts[p];
|
||||||
@@ -1647,6 +1786,10 @@ bool BuildingSystem::deliverScrapToSalvageBay(BuildingId bayId)
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (bay->queuedForDeconstruction)
|
||||||
|
{
|
||||||
|
return false; // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
|
||||||
|
}
|
||||||
// Emerging scrap still counts against the bay's holding capacity
|
// Emerging scrap still counts against the bay's holding capacity
|
||||||
// (REQ-MAT-OUTPUT-EMERGE).
|
// (REQ-MAT-OUTPUT-EMERGE).
|
||||||
if (bay->getOutputItemCount() >= bay->outputBuffer.capacity)
|
if (bay->getOutputItemCount() >= bay->outputBuffer.capacity)
|
||||||
@@ -1820,6 +1963,7 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
|
|||||||
appendItems(hasher, b.production->chosenOutputs);
|
appendItems(hasher, b.production->chosenOutputs);
|
||||||
}
|
}
|
||||||
hasher.append(b.shipLayout.has_value());
|
hasher.append(b.shipLayout.has_value());
|
||||||
|
hasher.append(b.queuedForDeconstruction);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasher.append(m_constructionQueue.size());
|
hasher.append(m_constructionQueue.size());
|
||||||
@@ -1840,6 +1984,17 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
|
|||||||
for (const ItemType& type : s.splitterFilterB) { hasher.append(type.id); }
|
for (const ItemType& type : s.splitterFilterB) { hasher.append(type.id); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasher.append(m_deconstructionQueue.size());
|
||||||
|
for (const DeconstructionEntry& e : m_deconstructionQueue)
|
||||||
|
{
|
||||||
|
hasher.append(e.id);
|
||||||
|
hasher.append(e.completesAt);
|
||||||
|
hasher.append(e.splitterFilterA.size());
|
||||||
|
for (const ItemType& type : e.splitterFilterA) { hasher.append(type.id); }
|
||||||
|
hasher.append(e.splitterFilterB.size());
|
||||||
|
for (const ItemType& type : e.splitterFilterB) { hasher.append(type.id); }
|
||||||
|
}
|
||||||
|
|
||||||
// std::map iterates in sorted key order.
|
// std::map iterates in sorted key order.
|
||||||
hasher.append(m_tileOccupancy.size());
|
hasher.append(m_tileOccupancy.size());
|
||||||
for (const std::pair<const std::pair<int, int>, BuildingId>& entry : m_tileOccupancy)
|
for (const std::pair<const std::pair<int, int>, BuildingId>& entry : m_tileOccupancy)
|
||||||
|
|||||||
@@ -77,10 +77,22 @@ public:
|
|||||||
// Defaults to world.regions.asteroid_width_tiles at construction.
|
// Defaults to world.regions.asteroid_width_tiles at construction.
|
||||||
void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; }
|
void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; }
|
||||||
|
|
||||||
// Remove a building or construction site by id. Returns the refund in
|
// Mark a building or construction site for demolition (REQ-BLD-DEMOLISH).
|
||||||
// building blocks (floor(cost * refundPercentage / 100)). Returns 0 for
|
// A construction site is removed instantly and the full cost is returned.
|
||||||
// unknown ids.
|
// A fully-built building is instead appended to the deconstruction queue
|
||||||
int demolish(BuildingId id);
|
// (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 demolish(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);
|
||||||
|
|
||||||
|
// True if the building is currently in the deconstruction queue.
|
||||||
|
bool isQueuedForDeconstruction(BuildingId id) const;
|
||||||
|
|
||||||
// Set the recipe (or schematic id for shipyard) on a building or queued
|
// Set the recipe (or schematic id for shipyard) on a building or queued
|
||||||
// construction site. Clears both buffers on an operational building.
|
// construction site. Clears both buffers on an operational building.
|
||||||
@@ -104,6 +116,10 @@ public:
|
|||||||
|
|
||||||
// -- Tick hooks (called from Simulation::tick in the documented order) ---
|
// -- Tick hooks (called from Simulation::tick in the documented order) ---
|
||||||
void tickConstruction(Tick currentTick);
|
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 tickBeltPull();
|
||||||
void tickProduction(Tick currentTick);
|
void tickProduction(Tick currentTick);
|
||||||
void tickShipyardProduction(Tick currentTick);
|
void tickShipyardProduction(Tick currentTick);
|
||||||
@@ -205,6 +221,17 @@ public:
|
|||||||
void appendChecksum(Hasher& hasher) const;
|
void appendChecksum(Hasher& hasher) const;
|
||||||
|
|
||||||
private:
|
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);
|
||||||
|
|
||||||
Building* findBuildingMutable(BuildingId id);
|
Building* findBuildingMutable(BuildingId id);
|
||||||
// True if the consumer would accept `type` at the given input port right now:
|
// 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
|
// it is a required input (or a building block for the HQ), the reservation-aware
|
||||||
@@ -273,6 +300,19 @@ private:
|
|||||||
std::vector<Building> m_buildings;
|
std::vector<Building> m_buildings;
|
||||||
std::deque<ConstructionSite> m_constructionQueue;
|
std::deque<ConstructionSite> m_constructionQueue;
|
||||||
|
|
||||||
|
// One pending demolition of a fully-built building (REQ-BLD-DECON-QUEUE).
|
||||||
|
// completesAt == 0 means "queued but its timer has not started yet"
|
||||||
|
// (mirrors ConstructionSite). For a Splitter, the filters it had are captured
|
||||||
|
// here so cancelDeconstruction can restore them on re-registration.
|
||||||
|
struct DeconstructionEntry
|
||||||
|
{
|
||||||
|
BuildingId id = kInvalidBuildingId;
|
||||||
|
Tick completesAt = 0;
|
||||||
|
std::vector<ItemType> splitterFilterA;
|
||||||
|
std::vector<ItemType> splitterFilterB;
|
||||||
|
};
|
||||||
|
std::deque<DeconstructionEntry> m_deconstructionQueue;
|
||||||
|
|
||||||
// Maps every occupied body-cell coordinate to the entity that owns it.
|
// Maps every occupied body-cell coordinate to the entity that owns it.
|
||||||
std::map<std::pair<int, int>, BuildingId> m_tileOccupancy;
|
std::map<std::pair<int, int>, BuildingId> m_tileOccupancy;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ enum class CommandKind
|
|||||||
{
|
{
|
||||||
PlaceBuilding,
|
PlaceBuilding,
|
||||||
Demolish,
|
Demolish,
|
||||||
|
CancelDeconstruction,
|
||||||
RotateInPlace,
|
RotateInPlace,
|
||||||
SetRecipe,
|
SetRecipe,
|
||||||
SetShipLayout,
|
SetShipLayout,
|
||||||
@@ -72,12 +73,22 @@ struct PlaceBuildingCommand : Command
|
|||||||
std::vector<ItemType> splitterFilterB;
|
std::vector<ItemType> splitterFilterB;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Marks a building for demolition: a construction site is removed instantly, a
|
||||||
|
// built building is queued for deconstruction (REQ-BLD-DEMOLISH, REQ-BLD-DECON-QUEUE).
|
||||||
struct DemolishCommand : Command
|
struct DemolishCommand : Command
|
||||||
{
|
{
|
||||||
DemolishCommand() : Command(CommandKind::Demolish) {}
|
DemolishCommand() : Command(CommandKind::Demolish) {}
|
||||||
std::optional<BuildingId> id;
|
std::optional<BuildingId> id;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Takes a building back out of the deconstruction queue (REQ-BLD-DEMOLISH-CLICK,
|
||||||
|
// REQ-BLD-DEMOLISH-BOX). No-op if the id is not currently queued.
|
||||||
|
struct CancelDeconstructionCommand : Command
|
||||||
|
{
|
||||||
|
CancelDeconstructionCommand() : Command(CommandKind::CancelDeconstruction) {}
|
||||||
|
std::optional<BuildingId> id;
|
||||||
|
};
|
||||||
|
|
||||||
struct RotateInPlaceCommand : Command
|
struct RotateInPlaceCommand : Command
|
||||||
{
|
{
|
||||||
RotateInPlaceCommand() : Command(CommandKind::RotateInPlace) {}
|
RotateInPlaceCommand() : Command(CommandKind::RotateInPlace) {}
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ std::string serializeCommand(const Command& command)
|
|||||||
case CommandKind::Demolish:
|
case CommandKind::Demolish:
|
||||||
out << "demolish " << static_cast<const DemolishCommand&>(command).id.value();
|
out << "demolish " << static_cast<const DemolishCommand&>(command).id.value();
|
||||||
break;
|
break;
|
||||||
|
case CommandKind::CancelDeconstruction:
|
||||||
|
out << "cancel_deconstruct "
|
||||||
|
<< static_cast<const CancelDeconstructionCommand&>(command).id.value();
|
||||||
|
break;
|
||||||
case CommandKind::RotateInPlace:
|
case CommandKind::RotateInPlace:
|
||||||
{
|
{
|
||||||
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
||||||
@@ -247,6 +251,15 @@ std::shared_ptr<Command> parseCommand(const std::string& tokens)
|
|||||||
c->id = id;
|
c->id = id;
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
if (verb == "cancel_deconstruct")
|
||||||
|
{
|
||||||
|
std::shared_ptr<CancelDeconstructionCommand> c =
|
||||||
|
std::make_shared<CancelDeconstructionCommand>();
|
||||||
|
BuildingId id = 0;
|
||||||
|
if (!(in >> id)) { return nullptr; }
|
||||||
|
c->id = id;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
if (verb == "rotate")
|
if (verb == "rotate")
|
||||||
{
|
{
|
||||||
std::shared_ptr<RotateInPlaceCommand> c = std::make_shared<RotateInPlaceCommand>();
|
std::shared_ptr<RotateInPlaceCommand> c = std::make_shared<RotateInPlaceCommand>();
|
||||||
|
|||||||
@@ -234,6 +234,9 @@ void Simulation::apply(const Command& command)
|
|||||||
case CommandKind::Demolish:
|
case CommandKind::Demolish:
|
||||||
demolish(*static_cast<const DemolishCommand&>(command).id);
|
demolish(*static_cast<const DemolishCommand&>(command).id);
|
||||||
break;
|
break;
|
||||||
|
case CommandKind::CancelDeconstruction:
|
||||||
|
cancelDeconstruction(*static_cast<const CancelDeconstructionCommand&>(command).id);
|
||||||
|
break;
|
||||||
case CommandKind::RotateInPlace:
|
case CommandKind::RotateInPlace:
|
||||||
{
|
{
|
||||||
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
||||||
@@ -307,6 +310,7 @@ void Simulation::tick()
|
|||||||
|
|
||||||
// Construction + production pipeline
|
// Construction + production pipeline
|
||||||
m_buildingSystem->tickConstruction(m_currentTick);
|
m_buildingSystem->tickConstruction(m_currentTick);
|
||||||
|
m_buildingSystem->tickDeconstruction(m_currentTick); // parallel to construction
|
||||||
m_buildingSystem->tickBeltPull(); // step 3
|
m_buildingSystem->tickBeltPull(); // step 3
|
||||||
m_buildingSystem->tickProduction(m_currentTick); // step 4
|
m_buildingSystem->tickProduction(m_currentTick); // step 4
|
||||||
m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b
|
m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b
|
||||||
@@ -1196,7 +1200,12 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
|
|||||||
|
|
||||||
void Simulation::demolish(BuildingId id)
|
void Simulation::demolish(BuildingId id)
|
||||||
{
|
{
|
||||||
m_buildingBlocksStock += m_buildingSystem->demolish(id);
|
m_buildingBlocksStock += m_buildingSystem->demolish(id, m_currentTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Simulation::cancelDeconstruction(BuildingId id)
|
||||||
|
{
|
||||||
|
m_buildingSystem->cancelDeconstruction(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildingSystem& Simulation::getBuildingsMutable()
|
BuildingSystem& Simulation::getBuildingsMutable()
|
||||||
|
|||||||
@@ -139,9 +139,14 @@ private:
|
|||||||
// Returns the new entity id, or nullopt if blocks are insufficient.
|
// Returns the new entity id, or nullopt if blocks are insufficient.
|
||||||
std::optional<BuildingId> tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
|
std::optional<BuildingId> tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
|
||||||
|
|
||||||
// Demolishes the building with the given id and refunds building blocks.
|
// Marks the building with the given id for demolition: a construction site is
|
||||||
|
// removed instantly (full refund), a built building is queued for timed
|
||||||
|
// deconstruction (REQ-BLD-DECON-QUEUE), refunded on completion.
|
||||||
void demolish(BuildingId id);
|
void demolish(BuildingId id);
|
||||||
|
|
||||||
|
// Takes a queued building back out of the deconstruction queue (REQ-BLD-DECON-QUEUE).
|
||||||
|
void cancelDeconstruction(BuildingId id);
|
||||||
|
|
||||||
// Applies the player's chosen schematic from the pending choices.
|
// Applies the player's chosen schematic from the pending choices.
|
||||||
// choiceIndex must be in [0, pendingChoices.size()).
|
// choiceIndex must be in [0, pendingChoices.size()).
|
||||||
// Clears the pending choices after application.
|
// Clears the pending choices after application.
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ static void runTicks(BuildingSystem& bs, BeltSystem& belts, int n, Tick& tick)
|
|||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i)
|
||||||
{
|
{
|
||||||
bs.tickConstruction(tick);
|
bs.tickConstruction(tick);
|
||||||
|
bs.tickDeconstruction(tick);
|
||||||
bs.tickBeltPull();
|
bs.tickBeltPull();
|
||||||
bs.tickProduction(tick);
|
bs.tickProduction(tick);
|
||||||
bs.tickOutputBelts();
|
bs.tickOutputBelts();
|
||||||
@@ -265,33 +266,20 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
|
|||||||
REQUIRE(bs.findSite(id) != nullptr);
|
REQUIRE(bs.findSite(id) != nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building]")
|
TEST_CASE("BuildingSystem: demolishing a construction site removes it instantly with full refund",
|
||||||
|
"[building]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
PlacementFixture f;
|
||||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
const BuildingId id =
|
||||||
int stock = 0;
|
f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
std::mt19937 rng(0);
|
|
||||||
BuildingId nextBuildingId = 1;
|
|
||||||
BuildingSystem bs(cfg, belts,
|
|
||||||
[&nextBuildingId]() { return nextBuildingId++; },
|
|
||||||
[&stock](int n) { stock += n; },
|
|
||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
|
||||||
[](const std::string&) -> bool { return true; },
|
|
||||||
rng);
|
|
||||||
|
|
||||||
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
// Still queued for construction (not yet built): instant removal, full cost
|
||||||
|
// refunded immediately, never entering the deconstruction queue (REQ-BLD-DEMOLISH).
|
||||||
|
const int refund = f.bs.demolish(id, 0);
|
||||||
|
|
||||||
// Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300.
|
REQUIRE(refund == 15); // Miner cost = 15
|
||||||
// We need to process tick 300 itself, so run 301 ticks (ticks 0..300).
|
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0)));
|
||||||
Tick tick = 0;
|
REQUIRE(f.bs.getAllSites().empty());
|
||||||
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
|
||||||
|
|
||||||
const int refund = bs.demolish(id);
|
|
||||||
|
|
||||||
// Miner cost = 15, refund = floor(15 * 75 / 100) = 11.
|
|
||||||
REQUIRE(refund == 15 * cfg.world.refundPercentage / 100);
|
|
||||||
REQUIRE_FALSE(bs.isTileOccupied(QPoint(0, 0)));
|
|
||||||
REQUIRE(bs.getAllSites().empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -364,6 +352,147 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
|
|||||||
REQUIRE(bs.findBuilding(id) != nullptr);
|
REQUIRE(bs.findBuilding(id) != nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Deconstruction queue (REQ-BLD-DECON-QUEUE)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Runs ticks until the building with the given id is operational, or fails.
|
||||||
|
static void runUntilBuilt(PlacementFixture& f, BuildingId id, Tick& tick)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 100000 && f.bs.findBuilding(id) == nullptr; ++i)
|
||||||
|
{
|
||||||
|
runTicks(f.bs, f.belts, 1, tick);
|
||||||
|
}
|
||||||
|
REQUIRE(f.bs.findBuilding(id) != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: demolishing a built building queues it; refund credited on completion",
|
||||||
|
"[building][decon]")
|
||||||
|
{
|
||||||
|
PlacementFixture f;
|
||||||
|
const BuildingId id =
|
||||||
|
f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
|
Tick tick = 0;
|
||||||
|
runUntilBuilt(f, id, tick);
|
||||||
|
|
||||||
|
// Demolishing a built building returns nothing immediately and queues it,
|
||||||
|
// stopping it operating while its tiles stay occupied (REQ-BLD-DECON-QUEUE).
|
||||||
|
const int refund = f.bs.demolish(id, tick);
|
||||||
|
REQUIRE(refund == 0);
|
||||||
|
REQUIRE(f.bs.isQueuedForDeconstruction(id));
|
||||||
|
REQUIRE(f.bs.isTileOccupied(QPoint(0, 0)));
|
||||||
|
REQUIRE(f.stock == 0);
|
||||||
|
|
||||||
|
// After the deconstruction time (0.1s = 3 ticks) it is removed and the partial
|
||||||
|
// refund (15 * 75 / 100 = 11) is credited exactly once.
|
||||||
|
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
|
||||||
|
REQUIRE(f.bs.findBuilding(id) == nullptr);
|
||||||
|
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0)));
|
||||||
|
REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: deconstruction queue removes one building at a time", "[building][decon]")
|
||||||
|
{
|
||||||
|
PlacementFixture f;
|
||||||
|
const BuildingId a = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
const BuildingId b = f.bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0).value();
|
||||||
|
|
||||||
|
Tick tick = 0;
|
||||||
|
runUntilBuilt(f, a, tick);
|
||||||
|
runUntilBuilt(f, b, tick);
|
||||||
|
|
||||||
|
// Queue both in one tick; 'a' is at the front of the deconstruction queue.
|
||||||
|
f.bs.demolish(a, tick);
|
||||||
|
f.bs.demolish(b, tick);
|
||||||
|
REQUIRE(f.bs.isQueuedForDeconstruction(a));
|
||||||
|
REQUIRE(f.bs.isQueuedForDeconstruction(b));
|
||||||
|
|
||||||
|
// After one deconstruction interval only the front building is gone; the
|
||||||
|
// second is still queued and its refund not yet credited.
|
||||||
|
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
|
||||||
|
REQUIRE(f.bs.findBuilding(a) == nullptr);
|
||||||
|
REQUIRE(f.bs.findBuilding(b) != nullptr);
|
||||||
|
REQUIRE(f.bs.isQueuedForDeconstruction(b));
|
||||||
|
REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100);
|
||||||
|
|
||||||
|
// The second drains next.
|
||||||
|
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 2, tick);
|
||||||
|
REQUIRE(f.bs.findBuilding(b) == nullptr);
|
||||||
|
REQUIRE(f.stock == 2 * (15 * f.cfg.world.refundPercentage / 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: cancelling deconstruction resumes the building with no refund",
|
||||||
|
"[building][decon]")
|
||||||
|
{
|
||||||
|
PlacementFixture f;
|
||||||
|
const BuildingId id =
|
||||||
|
f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
|
Tick tick = 0;
|
||||||
|
runUntilBuilt(f, id, tick);
|
||||||
|
|
||||||
|
f.bs.demolish(id, tick);
|
||||||
|
REQUIRE(f.bs.isQueuedForDeconstruction(id));
|
||||||
|
|
||||||
|
// Un-queue before it drains: it operates again, no refund, tiles still occupied.
|
||||||
|
f.bs.cancelDeconstruction(id);
|
||||||
|
REQUIRE_FALSE(f.bs.isQueuedForDeconstruction(id));
|
||||||
|
REQUIRE(f.bs.findBuilding(id) != nullptr);
|
||||||
|
REQUIRE(f.bs.isTileOccupied(QPoint(0, 0)));
|
||||||
|
REQUIRE(f.stock == 0);
|
||||||
|
|
||||||
|
// It is never removed even after more than a deconstruction interval passes.
|
||||||
|
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 5, tick);
|
||||||
|
REQUIRE(f.bs.findBuilding(id) != nullptr);
|
||||||
|
REQUIRE(f.stock == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: queued belt stops transporting; cancel restores it", "[building][decon]")
|
||||||
|
{
|
||||||
|
PlacementFixture f;
|
||||||
|
const BuildingId id =
|
||||||
|
f.bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
|
Tick tick = 0;
|
||||||
|
runUntilBuilt(f, id, tick);
|
||||||
|
REQUIRE(f.belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East));
|
||||||
|
|
||||||
|
// Queuing a belt unregisters its tile from the belt subsystem, so it no longer
|
||||||
|
// accepts or transports items, though the tile stays occupied (REQ-BLD-DECON-QUEUE).
|
||||||
|
f.bs.demolish(id, tick);
|
||||||
|
REQUIRE_FALSE(f.belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East));
|
||||||
|
REQUIRE(f.bs.isTileOccupied(QPoint(0, 0)));
|
||||||
|
|
||||||
|
// Un-queuing re-registers the belt tile so it transports again.
|
||||||
|
f.bs.cancelDeconstruction(id);
|
||||||
|
REQUIRE(f.belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: splitter filters survive a queue/un-queue round-trip", "[building][decon]")
|
||||||
|
{
|
||||||
|
PlacementFixture f;
|
||||||
|
const BuildingId id =
|
||||||
|
f.bs.place(BuildingType::Splitter, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
|
Tick tick = 0;
|
||||||
|
runUntilBuilt(f, id, tick);
|
||||||
|
|
||||||
|
f.belts.setSplitterFilters(QPoint(0, 0), {ItemType{"iron_ore"}}, {});
|
||||||
|
|
||||||
|
// Queue: the belt subsystem tile (and its filters) are unregistered, but the
|
||||||
|
// filters are captured so an un-queue can restore them.
|
||||||
|
f.bs.demolish(id, tick);
|
||||||
|
REQUIRE_FALSE(f.belts.getSplitterInfo(QPoint(0, 0)).has_value());
|
||||||
|
|
||||||
|
f.bs.cancelDeconstruction(id);
|
||||||
|
const std::optional<BeltSystem::SplitterInfo> info = f.belts.getSplitterInfo(QPoint(0, 0));
|
||||||
|
REQUIRE(info.has_value());
|
||||||
|
REQUIRE(info->filterA.size() == 1);
|
||||||
|
REQUIRE(info->filterA[0].id == "iron_ore");
|
||||||
|
REQUIRE(info->filterB.empty());
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("BuildingSystem: second building starts after first completes", "[building]")
|
TEST_CASE("BuildingSystem: second building starts after first completes", "[building]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "SimulationTestAccess.h"
|
#include "SimulationTestAccess.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -78,6 +79,35 @@ TEST_CASE("apply(DemolishCommand) matches direct demolish", "[command]")
|
|||||||
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
|
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstruction", "[command]")
|
||||||
|
{
|
||||||
|
Simulation viaCommand(loadConfig(), 99);
|
||||||
|
Simulation viaDirect(loadConfig(), 99);
|
||||||
|
|
||||||
|
const BuildingId idA =
|
||||||
|
SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
|
||||||
|
const BuildingId idB =
|
||||||
|
SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
|
||||||
|
REQUIRE(idA == idB);
|
||||||
|
|
||||||
|
// Build the miners, then queue both for deconstruction identically.
|
||||||
|
for (int i = 0; i < static_cast<int>(secondsToTicks(10.0)) + 1; ++i)
|
||||||
|
{
|
||||||
|
viaCommand.tick();
|
||||||
|
viaDirect.tick();
|
||||||
|
}
|
||||||
|
SimulationTestAccess::demolish(viaCommand, idA);
|
||||||
|
SimulationTestAccess::demolish(viaDirect, idB);
|
||||||
|
|
||||||
|
CancelDeconstructionCommand command;
|
||||||
|
command.id = idA;
|
||||||
|
viaCommand.apply(command);
|
||||||
|
|
||||||
|
SimulationTestAccess::cancelDeconstruction(viaDirect, idB);
|
||||||
|
|
||||||
|
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("CommandManager drains queued commands in FIFO order through apply", "[command]")
|
TEST_CASE("CommandManager drains queued commands in FIFO order through apply", "[command]")
|
||||||
{
|
{
|
||||||
Simulation viaManager(loadConfig(), 99);
|
Simulation viaManager(loadConfig(), 99);
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ TEST_CASE("Missing field in world.toml is rejected with the field path", "[confi
|
|||||||
[world]
|
[world]
|
||||||
height_tiles = 60
|
height_tiles = 60
|
||||||
refund_percentage = 75
|
refund_percentage = 75
|
||||||
|
deconstruction_time_seconds = 0.1
|
||||||
scrap_despawn_seconds = 30
|
scrap_despawn_seconds = 30
|
||||||
scrap_per_threat = 0.01
|
scrap_per_threat = 0.01
|
||||||
tile_size_m = 10
|
tile_size_m = 10
|
||||||
@@ -262,6 +263,7 @@ TEST_CASE("Malformed formula in world.toml is rejected with field identification
|
|||||||
[world]
|
[world]
|
||||||
height_tiles = 60
|
height_tiles = 60
|
||||||
refund_percentage = 75
|
refund_percentage = 75
|
||||||
|
deconstruction_time_seconds = 0.1
|
||||||
scrap_despawn_seconds = 30
|
scrap_despawn_seconds = 30
|
||||||
scrap_per_threat = 0.01
|
scrap_per_threat = 0.01
|
||||||
tile_size_m = 10
|
tile_size_m = 10
|
||||||
@@ -314,6 +316,7 @@ TEST_CASE("Inverted wave gap range is rejected", "[config]")
|
|||||||
[world]
|
[world]
|
||||||
height_tiles = 60
|
height_tiles = 60
|
||||||
refund_percentage = 75
|
refund_percentage = 75
|
||||||
|
deconstruction_time_seconds = 0.1
|
||||||
scrap_despawn_seconds = 30
|
scrap_despawn_seconds = 30
|
||||||
scrap_per_threat = 0.01
|
scrap_per_threat = 0.01
|
||||||
tile_size_m = 10
|
tile_size_m = 10
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ struct SimulationTestAccess
|
|||||||
|
|
||||||
static void demolish(Simulation& sim, BuildingId id) { sim.demolish(id); }
|
static void demolish(Simulation& sim, BuildingId id) { sim.demolish(id); }
|
||||||
|
|
||||||
|
static void cancelDeconstruction(Simulation& sim, BuildingId id)
|
||||||
|
{
|
||||||
|
sim.cancelDeconstruction(id);
|
||||||
|
}
|
||||||
|
|
||||||
static void applySchematicChoice(Simulation& sim, int choiceIndex)
|
static void applySchematicChoice(Simulation& sim, int choiceIndex)
|
||||||
{
|
{
|
||||||
sim.applySchematicChoice(choiceIndex);
|
sim.applySchematicChoice(choiceIndex);
|
||||||
|
|||||||
@@ -1977,6 +1977,17 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Queued for deconstruction: tint every building currently in the
|
||||||
|
// deconstruction queue, regardless of mode (REQ-BLD-DECON-QUEUE).
|
||||||
|
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
||||||
|
{
|
||||||
|
if (!b.queuedForDeconstruction) { continue; }
|
||||||
|
for (const QPoint& cell : b.bodyCells)
|
||||||
|
{
|
||||||
|
painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Demolish tint: while dragging a demolish box, tint every covered
|
// Demolish tint: while dragging a demolish box, tint every covered
|
||||||
// building/site (REQ-BLD-DEMOLISH-BOX); otherwise tint the hovered one.
|
// building/site (REQ-BLD-DEMOLISH-BOX); otherwise tint the hovered one.
|
||||||
if (m_demolishMode && m_boxSelecting)
|
if (m_demolishMode && m_boxSelecting)
|
||||||
@@ -2623,17 +2634,62 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
|
|
||||||
if (m_demolishMode)
|
if (m_demolishMode)
|
||||||
{
|
{
|
||||||
// Demolish every covered building/site; the HQ is protected
|
const BuildingSystem& buildings = m_sim->getBuildings();
|
||||||
// (REQ-BLD-DEMOLISH, REQ-BLD-DEMOLISH-BOX).
|
|
||||||
|
// Split covered ids into construction sites (removed instantly) and
|
||||||
|
// operational demolishable buildings (the HQ is protected; player
|
||||||
|
// defence stations are not Buildings and never appear in the box).
|
||||||
|
std::vector<BuildingId> sites;
|
||||||
|
std::vector<BuildingId> operational;
|
||||||
for (BuildingId id : boxIds)
|
for (BuildingId id : boxIds)
|
||||||
{
|
{
|
||||||
const Building* b = m_sim->getBuildings().findBuilding(id);
|
if (const Building* b = buildings.findBuilding(id))
|
||||||
if (b && b->type == BuildingType::Hq) { continue; }
|
{
|
||||||
|
if (b->type == BuildingType::Hq) { continue; }
|
||||||
|
operational.push_back(id);
|
||||||
|
}
|
||||||
|
else if (buildings.findSite(id))
|
||||||
|
{
|
||||||
|
sites.push_back(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sites: instant demolition with the full refund (REQ-BLD-DEMOLISH).
|
||||||
|
for (BuildingId id : sites)
|
||||||
|
{
|
||||||
std::shared_ptr<DemolishCommand> command =
|
std::shared_ptr<DemolishCommand> command =
|
||||||
std::make_shared<DemolishCommand>();
|
std::make_shared<DemolishCommand>();
|
||||||
command->id = id;
|
command->id = id;
|
||||||
enqueueCommand(command);
|
enqueueCommand(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Operational buildings: if every covered one is already queued,
|
||||||
|
// un-queue them all; otherwise queue each one not yet queued. A plain
|
||||||
|
// click is the one-element case, giving the queue/un-queue toggle
|
||||||
|
// (REQ-BLD-DEMOLISH-BOX, REQ-BLD-DEMOLISH-CLICK).
|
||||||
|
bool allQueued = !operational.empty();
|
||||||
|
for (BuildingId id : operational)
|
||||||
|
{
|
||||||
|
if (!buildings.isQueuedForDeconstruction(id)) { allQueued = false; break; }
|
||||||
|
}
|
||||||
|
for (BuildingId id : operational)
|
||||||
|
{
|
||||||
|
if (allQueued)
|
||||||
|
{
|
||||||
|
std::shared_ptr<CancelDeconstructionCommand> command =
|
||||||
|
std::make_shared<CancelDeconstructionCommand>();
|
||||||
|
command->id = id;
|
||||||
|
enqueueCommand(command);
|
||||||
|
}
|
||||||
|
else if (!buildings.isQueuedForDeconstruction(id))
|
||||||
|
{
|
||||||
|
std::shared_ptr<DemolishCommand> command =
|
||||||
|
std::make_shared<DemolishCommand>();
|
||||||
|
command->id = id;
|
||||||
|
enqueueCommand(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_demolishHoverBuildingId = std::nullopt;
|
m_demolishHoverBuildingId = std::nullopt;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user