Add deconstruction queue
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
[world]
|
||||
height_tiles = 40
|
||||
refund_percentage = 100
|
||||
deconstruction_time_seconds = 0.1
|
||||
starting_building_blocks = 200
|
||||
scrap_despawn_seconds = 120
|
||||
scrap_per_threat = 0.25
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[world]
|
||||
height_tiles = 60
|
||||
refund_percentage = 75
|
||||
deconstruction_time_seconds = 0.1
|
||||
starting_building_blocks = 100
|
||||
scrap_despawn_seconds = 30
|
||||
scrap_per_threat = 1.0
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Config files use the TOML format. The following config files drive game parameters:
|
||||
|
||||
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, scrap-per-threat conversion, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, artifact win count, view pan speeds (slow and fast horizontal pan speed and pan ramp band width), an optional building blocks tooltip string (shown as the header bar's building blocks stock hover tooltip, REQ-UI-BLOCKS-TOOLTIP; omitted when unset), and an optional artifact tooltip string (shown as the header bar's artifact count hover tooltip, REQ-UI-ARTIFACTS-TOOLTIP; omitted when unset).
|
||||
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, building deconstruction time, wave timing, boss wave timing, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, scrap-per-threat conversion, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, artifact win count, view pan speeds (slow and fast horizontal pan speed and pan ramp band width), an optional building blocks tooltip string (shown as the header bar's building blocks stock hover tooltip, REQ-UI-BLOCKS-TOOLTIP; omitted when unset), and an optional artifact tooltip string (shown as the header bar's artifact count hover tooltip, REQ-UI-ARTIFACTS-TOOLTIP; omitted when unset).
|
||||
- **buildings.toml** — building block cost and construction time per building type, plus an optional tooltip description string per building type (shown as the build button's hover tooltip, REQ-UI-BUILD-TOOLTIP; omitted when unset). Whether a building type is available from game start or must be unlocked during play is not defined here but in **unlocks.toml** (REQ-LOCK-EXPLICIT): a building type granted by an unlock group starts locked and is hidden from the build menu until its group is awarded (REQ-LOCK-BUILDING).
|
||||
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlocked_at_start` (boolean, default false): when true the recipe is available from game start regardless of the implicit item graph — used for base recipes that no schematic's materials reach (such as building blocks; see REQ-LOCK-IMPLICIT). Which assembler recipes must instead be awarded during play (explicitly gated) is defined in **unlocks.toml**, not here (REQ-LOCK-EXPLICIT); every remaining assembler recipe is implicitly unlocked through the item graph (REQ-LOCK-IMPLICIT).
|
||||
- **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as plain values, required build materials, a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES). Whether a ship schematic is available from game start or must be unlocked during play is defined in **unlocks.toml** (REQ-LOCK-EXPLICIT), not here.
|
||||
@@ -142,9 +142,10 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
||||
- **Resolving the type.** If neither match exists, the ghost is a Tunnel Entry (the default). If only one kind of match exists, the ghost is the kind that produces it (Tunnel Exit for an exit-completion match, Tunnel Entry for an entry-completion match). If **both** an exit-completion match (an existing Entry) and an entry-completion match (an existing Exit) exist, the mode resolves to the completion whose **existing partner building is closer to the mouse cursor position** — the actual sub-tile cursor position, not the hovered tile's center — and the ghost becomes the corresponding type (a Tunnel Exit to complete the nearer Entry, or a Tunnel Entry to complete the nearer Exit). Because the comparison uses the sub-tile cursor position, when the two partners are at the same tile distance the player can move the cursor within the hovered tile to switch which end is placed. When more than one candidate qualifies on a side, the nearest qualifying partner on that side is used.
|
||||
- **Connection preview (green).** Whenever a completion match is resolved, the matched existing partner building is highlighted green, and every tile strictly between that partner and the hovered ghost tile (along the tunnel's straight run) is marked green, previewing the connection that placing the ghost would create.
|
||||
- **Invalid positions.** The completion tests, type switch, and green preview apply only while the hovered position is a valid placement (REQ-BLD-PLACE-VALID). At an invalid position the ordinary invalid-colored ghost is shown (REQ-BLD-GHOST) with no green preview and no switch away from the default Tunnel Entry.
|
||||
- REQ-BLD-DEMOLISH: The player can demolish a placed factory building. Demolition returns `world.toml [world].refund_percentage` percent of the original building block cost (default 75%) to the global stock. Exception: if the building is still in the construction queue (not yet fully built, including the one currently being constructed), it is removed from the queue and the **full** building block cost is refunded. The HQ and player defence stations cannot be demolished.
|
||||
- REQ-BLD-DEMOLISH-CLICK: While in demolish mode (REQ-UI-HOTKEYS, REQ-UI-DEMOLISH-BUTTON), left-clicking a placed factory building or construction site in the game world demolishes it, following the refund rules of REQ-BLD-DEMOLISH — the partial refund for built buildings and the full refund for still-queued construction sites. Clicking a building that cannot be demolished (the HQ or a player defence station, per REQ-BLD-DEMOLISH), or clicking empty world space, has no effect. Demolish mode stays active after a demolition so the player can demolish further buildings without re-entering the mode; it is exited via the Q toggle (REQ-UI-HOTKEYS) or the Demolish button (REQ-UI-DEMOLISH-BUTTON).
|
||||
- REQ-BLD-DEMOLISH-BOX: While in demolish mode (REQ-UI-HOTKEYS, REQ-UI-DEMOLISH-BUTTON), the player can click and drag a selection box in the game world. A selection rectangle is drawn while dragging, using the same box-drag gesture and coverage semantics as the multi-select box (REQ-UI-MULTI-SELECT). On mouse up, every placed factory building and construction site covered by the box is demolished, each following the refund rules of REQ-BLD-DEMOLISH — the partial refund for built buildings and the full refund for still-queued construction sites. Buildings that cannot be demolished (the HQ and player defence stations, per REQ-BLD-DEMOLISH) are excluded from the box demolition; ships and defence stations are never affected.
|
||||
- REQ-BLD-DEMOLISH: The player can demolish a placed factory building. Demolishing a **fully-built** factory building does not remove it instantly: it is added to the deconstruction queue (REQ-BLD-DECON-QUEUE) and, once its deconstruction completes, `world.toml [world].refund_percentage` percent of the original building block cost (default 75%) is returned to the global stock. Exception: if the building is still in the construction queue (not yet fully built, including the one currently being constructed), it is **not** queued for deconstruction but removed instantly from the construction queue, and the **full** building block cost is refunded immediately. The HQ and player defence stations cannot be demolished.
|
||||
- REQ-BLD-DECON-QUEUE: Fully-built factory buildings marked for demolition (REQ-BLD-DEMOLISH) enter a **deconstruction queue** that is processed one building at a time and runs in parallel with the construction queue (REQ-BLD-QUEUE) — the two queues advance independently and simultaneously. Each building takes `world.toml [world].deconstruction_time_seconds` (default 0.1) to deconstruct, the same duration for every building type. When a building's deconstruction completes it is removed from the world and its refund is credited (REQ-BLD-DEMOLISH). A building **stops operating the moment it enters the queue**: it runs no production and transports no items, and no longer participates as a live building (its tunnel pairing is re-evaluated as if it were gone, REQ-BLD-TUNNEL-PAIR), but it still physically occupies its tiles until removed, so those tiles stay blocked for placement. A queued building can be taken back out of the deconstruction queue before it is removed (REQ-BLD-DEMOLISH-CLICK, REQ-BLD-DEMOLISH-BOX) — including the one currently being deconstructed; doing so discards any deconstruction progress, credits no refund, and the building resumes operating (and re-pairs, REQ-BLD-TUNNEL-PAIR). Construction sites never enter the deconstruction queue (REQ-BLD-DEMOLISH). Every building in the deconstruction queue is rendered with the demolish tint — the `visuals.toml [overlays].demolish_tint` color, the same tint applied to a building hovered in demolish mode (REQ-UI-DEMOLISH-BORDER) — so queued buildings are visually distinct.
|
||||
- REQ-BLD-DEMOLISH-CLICK: While in demolish mode (REQ-UI-HOTKEYS, REQ-UI-DEMOLISH-BUTTON), left-clicking a placed factory building or construction site in the game world marks it for demolition, following the rules of REQ-BLD-DEMOLISH: a fully-built building is added to the deconstruction queue (REQ-BLD-DECON-QUEUE), and a construction site is removed instantly with the full refund. Left-clicking a fully-built building that is **already in the deconstruction queue** instead removes it from the queue (un-queues it, REQ-BLD-DECON-QUEUE), with no refund; repeated clicks on the same building therefore alternate between queueing and un-queueing it. Clicking a building that cannot be demolished (the HQ or a player defence station, per REQ-BLD-DEMOLISH), or clicking empty world space, has no effect. Demolish mode stays active after each action so the player can continue without re-entering the mode; it is exited via the Q toggle (REQ-UI-HOTKEYS) or the Demolish button (REQ-UI-DEMOLISH-BUTTON).
|
||||
- REQ-BLD-DEMOLISH-BOX: While in demolish mode (REQ-UI-HOTKEYS, REQ-UI-DEMOLISH-BUTTON), the player can click and drag a selection box in the game world. A selection rectangle is drawn while dragging, using the same box-drag gesture and coverage semantics as the multi-select box (REQ-UI-MULTI-SELECT). On mouse up, following the rules of REQ-BLD-DEMOLISH: every construction site covered by the box is removed instantly with the full refund; and among the fully-built demolishable buildings covered by the box, if **all** of them are already in the deconstruction queue they are all removed from it (un-queued, REQ-BLD-DECON-QUEUE), otherwise every covered building not yet in the queue is added to the deconstruction queue (already-queued ones stay). Buildings that cannot be demolished (the HQ and player defence stations, per REQ-BLD-DEMOLISH) are excluded from the box demolition; ships and defence stations are never affected.
|
||||
- REQ-BLD-SITE-CONFIG: A construction site — a building that has been placed but is still queued or under construction (REQ-BLD-QUEUE) — can be selected and configured exactly like the equivalent operational building, before it finishes building. Whatever configuration the building type supports is available on the site: the recipe for a Miner or Assembler (REQ-UI-SELECT-BUTTON), the produced-ship schematic and its module layout for a Shipyard (REQ-UI-SELECT-BUTTON, REQ-MOD-UI-PREVIEW, REQ-MOD-UI-DIALOG), and the output filters for a Splitter (REQ-BLD-SPLITTER) — all set through the same Selected Building Panel controls (REQ-UI-CONFIG-INLINE). Only currently unlocked recipes and schematics are offered, exactly as for operational buildings (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SCHEMATIC, REQ-LOCK-UI-SPLITTER). The configuration is stored on the construction site and carries over unchanged when construction completes, so the building becomes operational already configured. A construction site has no input/output buffers and runs no production cycle, so the buffer and production-progress portions of the panel (REQ-UI-SINGLE-SELECTION, REQ-UI-PRODUCTION-PROGRESS) are not shown for it; only its construction progress (REQ-UI-CONSTRUCTION-PROGRESS) and its configuration controls appear. (Blueprint placement already applies a stored recipe or schematic to a construction site on placement per REQ-UI-BLUEPRINT-PLACE; this requirement additionally lets the player set or change that configuration directly on an existing site.)
|
||||
- REQ-BLD-COPY-CONFIG: **Copy building settings (hold Shift).** While the Shift key is held, the player can copy one building's settings onto other buildings of the same type, so several identical machines can be set up without opening each one's panel. This gesture is available only in the default selection mode; while a builder, blueprint placement, or demolish mode is active it is disabled, so it never clashes with placement or demolition clicks.
|
||||
- **Shift + right-click** a building copies its current settings into a temporary cache, along with the building's type. The settings copied are whatever that building type supports: the selected recipe (Miner, Assembler), the selected schematic together with its module layout (Shipyard), or the two output filters (Splitter, REQ-BLD-SPLITTER). Copying succeeds only when there is something to copy — a Miner or Assembler with a recipe selected, a Shipyard with a schematic selected, or any Splitter (whose output filters, even when empty/accept-all, always constitute valid settings). Shift + right-clicking a configurable building with nothing yet selected, a building type that has no settings at all (Smelter, Reprocessing Plant, Salvage Bay, belt/tunnel tiles, the HQ), or empty world space, has no effect and leaves any existing cache unchanged.
|
||||
@@ -170,7 +171,7 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
||||
- In all alternation cases, if one output is blocked the item goes to the other output until it unblocks.
|
||||
- REQ-BLD-TUNNEL-ENTRY: **Tunnel Entry** (1×1): The sending end of a tunnel pair. The player sets a direction (N, S, E, W) at placement, rotatable with R/Shift+R. Items arriving from an adjacent belt tile on a non-output edge (i.e. not the mouth edge in the entry's facing direction — see REQ-MAT-ACCEPT-DIR) whose direction points into the entry are forwarded through the tunnel to the paired Tunnel Exit (see REQ-BLD-TUNNEL-PAIR, REQ-BLD-TUNNEL-TRANSIT). If the entry is unpaired, or if the paired exit's output is blocked, the entry blocks like a full belt tile.
|
||||
- REQ-BLD-TUNNEL-EXIT: **Tunnel Exit** (1×1): The receiving end of a tunnel pair. The player sets a direction at placement, rotatable with R/Shift+R. Items received from the paired Tunnel Entry emerge from the output side of the exit tile — the tile adjacent in the exit's facing direction — continuing in that direction. If the exit is unpaired or its output is blocked, it holds received items until they can advance.
|
||||
- REQ-BLD-TUNNEL-PAIR: **Tunnel pairing rules.** Pairing is re-evaluated for all Tunnel Entries whenever any Tunnel Entry or Tunnel Exit is placed or demolished.
|
||||
- REQ-BLD-TUNNEL-PAIR: **Tunnel pairing rules.** Pairing is re-evaluated for all Tunnel Entries whenever any Tunnel Entry or Tunnel Exit is placed, demolished, or enters or leaves the deconstruction queue (REQ-BLD-DECON-QUEUE; a tunnel end that is queued for deconstruction counts as removed for pairing).
|
||||
- A Tunnel Entry searches tile-by-tile in its facing direction for a partner. Any tunnel building (entry or exit) that faces a *different* direction is ignored and skipped. The search stops at the first tunnel building that faces the *same* direction as the searching entry.
|
||||
- If that first same-direction tunnel building is a Tunnel Exit, is within `tunnel_max_distance` tiles of the entry, and is not already paired with a closer entry, the two form a pair.
|
||||
- Otherwise the entry is unpaired.
|
||||
|
||||
@@ -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.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.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");
|
||||
|
||||
@@ -68,6 +68,7 @@ struct WorldConfig
|
||||
{
|
||||
int heightTiles; // REQ-GW-HEIGHT
|
||||
int refundPercentage; // REQ-BLD-DEMOLISH
|
||||
double deconstructionTimeSeconds; // REQ-BLD-DECON-QUEUE
|
||||
int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS
|
||||
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
|
||||
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).
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
// (port.tile) and its facing direction. The virtual output belt occupies this tile
|
||||
// and flows toward port.tile (REQ-MAT-OUTPUT-EMERGE).
|
||||
@@ -489,9 +500,10 @@ bool BuildingSystem::isPlacementValid(BuildingType type, QPoint anchor,
|
||||
// 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();
|
||||
it != m_constructionQueue.end();
|
||||
++it)
|
||||
@@ -512,35 +524,60 @@ int BuildingSystem::demolish(BuildingId id)
|
||||
}
|
||||
}
|
||||
|
||||
// Operational building?
|
||||
for (std::vector<Building>::iterator it = m_buildings.begin();
|
||||
it != m_buildings.end();
|
||||
++it)
|
||||
// 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_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
|
||||
|| it->type == BuildingType::TunnelEntry || it->type == BuildingType::TunnelExit)
|
||||
if (const std::optional<BeltSystem::SplitterInfo> info =
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -780,31 +817,9 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
}
|
||||
}
|
||||
|
||||
// Register with BeltSystem before the move (mask stays valid).
|
||||
if (front.type == BuildingType::Belt)
|
||||
{
|
||||
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);
|
||||
}
|
||||
// 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_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()
|
||||
{
|
||||
TRACE();
|
||||
@@ -831,6 +954,9 @@ void BuildingSystem::tickBeltPull()
|
||||
|
||||
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);
|
||||
|
||||
// 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
|
||||
}
|
||||
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
|
||||
// direction, feeding the producer's output-port tile (REQ-MAT-DIRECT-COUPLE).
|
||||
@@ -943,6 +1073,9 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
||||
TRACE();
|
||||
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.
|
||||
if (building.type == BuildingType::Belt ||
|
||||
building.type == BuildingType::Splitter ||
|
||||
@@ -1043,6 +1176,9 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
||||
TRACE();
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
@@ -1140,6 +1276,9 @@ void BuildingSystem::tickOutputBelts()
|
||||
|
||||
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)
|
||||
{
|
||||
const Port& port = building.outputPorts[p];
|
||||
@@ -1647,6 +1786,10 @@ bool BuildingSystem::deliverScrapToSalvageBay(BuildingId bayId)
|
||||
{
|
||||
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
|
||||
// (REQ-MAT-OUTPUT-EMERGE).
|
||||
if (bay->getOutputItemCount() >= bay->outputBuffer.capacity)
|
||||
@@ -1820,6 +1963,7 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
|
||||
appendItems(hasher, b.production->chosenOutputs);
|
||||
}
|
||||
hasher.append(b.shipLayout.has_value());
|
||||
hasher.append(b.queuedForDeconstruction);
|
||||
}
|
||||
|
||||
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); }
|
||||
}
|
||||
|
||||
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.
|
||||
hasher.append(m_tileOccupancy.size());
|
||||
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.
|
||||
void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; }
|
||||
|
||||
// Remove a building or construction site by id. Returns the refund in
|
||||
// building blocks (floor(cost * refundPercentage / 100)). Returns 0 for
|
||||
// unknown ids.
|
||||
int demolish(BuildingId id);
|
||||
// Mark a building or construction site for demolition (REQ-BLD-DEMOLISH).
|
||||
// A construction site is removed instantly and the full cost is returned.
|
||||
// A fully-built building is instead appended to the deconstruction queue
|
||||
// (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
|
||||
// construction site. Clears both buffers on an operational building.
|
||||
@@ -104,6 +116,10 @@ public:
|
||||
|
||||
// -- 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);
|
||||
@@ -205,6 +221,17 @@ public:
|
||||
void appendChecksum(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);
|
||||
|
||||
Building* findBuildingMutable(BuildingId id);
|
||||
// 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
|
||||
@@ -273,6 +300,19 @@ private:
|
||||
std::vector<Building> m_buildings;
|
||||
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.
|
||||
std::map<std::pair<int, int>, BuildingId> m_tileOccupancy;
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ enum class CommandKind
|
||||
{
|
||||
PlaceBuilding,
|
||||
Demolish,
|
||||
CancelDeconstruction,
|
||||
RotateInPlace,
|
||||
SetRecipe,
|
||||
SetShipLayout,
|
||||
@@ -72,12 +73,22 @@ struct PlaceBuildingCommand : Command
|
||||
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
|
||||
{
|
||||
DemolishCommand() : Command(CommandKind::Demolish) {}
|
||||
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
|
||||
{
|
||||
RotateInPlaceCommand() : Command(CommandKind::RotateInPlace) {}
|
||||
|
||||
@@ -131,6 +131,10 @@ std::string serializeCommand(const Command& command)
|
||||
case CommandKind::Demolish:
|
||||
out << "demolish " << static_cast<const DemolishCommand&>(command).id.value();
|
||||
break;
|
||||
case CommandKind::CancelDeconstruction:
|
||||
out << "cancel_deconstruct "
|
||||
<< static_cast<const CancelDeconstructionCommand&>(command).id.value();
|
||||
break;
|
||||
case CommandKind::RotateInPlace:
|
||||
{
|
||||
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
||||
@@ -247,6 +251,15 @@ std::shared_ptr<Command> parseCommand(const std::string& tokens)
|
||||
c->id = id;
|
||||
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")
|
||||
{
|
||||
std::shared_ptr<RotateInPlaceCommand> c = std::make_shared<RotateInPlaceCommand>();
|
||||
|
||||
@@ -234,6 +234,9 @@ void Simulation::apply(const Command& command)
|
||||
case CommandKind::Demolish:
|
||||
demolish(*static_cast<const DemolishCommand&>(command).id);
|
||||
break;
|
||||
case CommandKind::CancelDeconstruction:
|
||||
cancelDeconstruction(*static_cast<const CancelDeconstructionCommand&>(command).id);
|
||||
break;
|
||||
case CommandKind::RotateInPlace:
|
||||
{
|
||||
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
||||
@@ -307,6 +310,7 @@ void Simulation::tick()
|
||||
|
||||
// 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
|
||||
@@ -1196,7 +1200,12 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
|
||||
|
||||
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()
|
||||
|
||||
@@ -139,9 +139,14 @@ private:
|
||||
// Returns the new entity id, or nullopt if blocks are insufficient.
|
||||
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);
|
||||
|
||||
// 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.
|
||||
// choiceIndex must be in [0, pendingChoices.size()).
|
||||
// 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)
|
||||
{
|
||||
bs.tickConstruction(tick);
|
||||
bs.tickDeconstruction(tick);
|
||||
bs.tickBeltPull();
|
||||
bs.tickProduction(tick);
|
||||
bs.tickOutputBelts();
|
||||
@@ -265,33 +266,20 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
|
||||
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();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
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);
|
||||
PlacementFixture f;
|
||||
const BuildingId id =
|
||||
f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||
|
||||
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.
|
||||
// We need to process tick 300 itself, so run 301 ticks (ticks 0..300).
|
||||
Tick tick = 0;
|
||||
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());
|
||||
REQUIRE(refund == 15); // Miner cost = 15
|
||||
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0)));
|
||||
REQUIRE(f.bs.getAllSites().empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -364,6 +352,147 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
|
||||
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]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "Rotation.h"
|
||||
#include "Simulation.h"
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "Tick.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -78,6 +79,35 @@ TEST_CASE("apply(DemolishCommand) matches direct demolish", "[command]")
|
||||
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]")
|
||||
{
|
||||
Simulation viaManager(loadConfig(), 99);
|
||||
|
||||
@@ -211,6 +211,7 @@ TEST_CASE("Missing field in world.toml is rejected with the field path", "[confi
|
||||
[world]
|
||||
height_tiles = 60
|
||||
refund_percentage = 75
|
||||
deconstruction_time_seconds = 0.1
|
||||
scrap_despawn_seconds = 30
|
||||
scrap_per_threat = 0.01
|
||||
tile_size_m = 10
|
||||
@@ -262,6 +263,7 @@ TEST_CASE("Malformed formula in world.toml is rejected with field identification
|
||||
[world]
|
||||
height_tiles = 60
|
||||
refund_percentage = 75
|
||||
deconstruction_time_seconds = 0.1
|
||||
scrap_despawn_seconds = 30
|
||||
scrap_per_threat = 0.01
|
||||
tile_size_m = 10
|
||||
@@ -314,6 +316,7 @@ TEST_CASE("Inverted wave gap range is rejected", "[config]")
|
||||
[world]
|
||||
height_tiles = 60
|
||||
refund_percentage = 75
|
||||
deconstruction_time_seconds = 0.1
|
||||
scrap_despawn_seconds = 30
|
||||
scrap_per_threat = 0.01
|
||||
tile_size_m = 10
|
||||
|
||||
@@ -35,6 +35,11 @@ struct SimulationTestAccess
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
// building/site (REQ-BLD-DEMOLISH-BOX); otherwise tint the hovered one.
|
||||
if (m_demolishMode && m_boxSelecting)
|
||||
@@ -2623,17 +2634,62 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
||||
|
||||
if (m_demolishMode)
|
||||
{
|
||||
// Demolish every covered building/site; the HQ is protected
|
||||
// (REQ-BLD-DEMOLISH, REQ-BLD-DEMOLISH-BOX).
|
||||
const BuildingSystem& buildings = m_sim->getBuildings();
|
||||
|
||||
// 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)
|
||||
{
|
||||
const Building* b = m_sim->getBuildings().findBuilding(id);
|
||||
if (b && b->type == BuildingType::Hq) { continue; }
|
||||
if (const Building* b = buildings.findBuilding(id))
|
||||
{
|
||||
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::make_shared<DemolishCommand>();
|
||||
command->id = id;
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user