diff --git a/docs/requirements.md b/docs/requirements.md index 8a8377d..97e9677 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -608,34 +608,36 @@ Blueprints occupy no permanent screen space. They are saved with **Ctrl+C** from - REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint (excluding any of a currently locked building type, REQ-LOCK-BUILDING, which is omitted entirely per REQ-LOCK-UI-BLUEPRINT) at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). A valid ghost uses its building type's semi-transparent per-building coloring (REQ-BLD-GHOST); an invalid ghost uses the distinct "invalid" color, as in single-building builder mode; a ghost over a configuration-transfer target uses the distinct "transfer" color instead of either (REQ-UI-BLUEPRINT-TRANSFER), and a ghost over a compatible overlap counts as valid and keeps the ordinary per-building coloring (REQ-UI-BLUEPRINT-OVERLAP). Pressing R / Shift+R rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Opening the blueprint selection dialog while placement mode is active (REQ-UI-BLUEPRINT-DIALOG) leaves the mode active — closing the dialog without picking a card returns to it unchanged — while clicking a card exits the current mode and enters blueprint placement mode for the newly picked blueprint. -- REQ-UI-BLUEPRINT-PLACE: This describes placing the blueprint as new construction sites. A single-building blueprint whose ghost sits on a configuration-transfer target instead transfers its settings to the existing building and places nothing (REQ-UI-BLUEPRINT-TRANSFER); the rules below do not apply to that click. Buildings of a currently locked building type (REQ-LOCK-BUILDING) are first excluded from the blueprint for this placement, per REQ-LOCK-UI-BLUEPRINT — they are not ghosted, not validity-checked, not placed, and their cost is excluded from the total. Left-clicking in blueprint placement mode then places the (remaining) blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. Buildings that are compatible overlaps (REQ-UI-BLUEPRINT-OVERLAP) are excluded from the total cost and are not placed, but do not block the placement. If both conditions are met, a construction site is added to the build queue for each remaining building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. If splitter output filters are stored, they are applied to the construction site immediately and carry over when it finishes building (REQ-BLD-SITE-CONFIG). Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately. +- REQ-UI-BLUEPRINT-PLACE: This describes placing the blueprint's buildings as new construction sites. Ghosts sitting on a building that is already there are handled elsewhere and place nothing: a configuration-transfer target receives the blueprint's stored settings instead (REQ-UI-BLUEPRINT-TRANSFER), and a compatible overlap is left alone (REQ-UI-BLUEPRINT-OVERLAP). Both still take part in the single all-or-nothing click described here. Buildings of a currently locked building type (REQ-LOCK-BUILDING) are first excluded from the blueprint for this placement, per REQ-LOCK-UI-BLUEPRINT — they are not ghosted, not validity-checked, not placed, and their cost is excluded from the total. Left-clicking in blueprint placement mode then places the (remaining) blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. Buildings that are compatible overlaps (REQ-UI-BLUEPRINT-OVERLAP) or configuration-transfer targets (REQ-UI-BLUEPRINT-TRANSFER) are excluded from the total cost and are not placed, but do not block the placement; a transfer target additionally receives the blueprint's stored settings. If both conditions are met, a construction site is added to the build queue for each remaining building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. If splitter output filters are stored, they are applied to the construction site immediately and carry over when it finishes building (REQ-BLD-SITE-CONFIG). Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately. - REQ-UI-BLUEPRINT-OVERLAP: **Compatible overlap.** In blueprint placement mode, a ghost whose footprint **exactly coincides** with the footprint of an existing placed building or construction site that is of the **same building type** and has the **same rotation** is a *compatible overlap*: the building the blueprint wants is already there. Such a ghost is **valid** despite the occupied tiles (REQ-BLD-PLACE-VALID condition (b)), so it does not block the placement of the rest of the constellation — dropping a blueprint over a partially-built copy of itself fills in what is missing. It is drawn in the ordinary per-building ghost color (REQ-BLD-GHOST), like any other valid ghost. - On placement the overlapped building is **left completely untouched**: no construction site is placed on it, no building blocks are charged for it (it is excluded from the total cost of REQ-UI-BLUEPRINT-PLACE), its rotation is not changed, its configuration is not changed, and a construction site's progress is preserved. This applies per building in the blueprint, independently, and to blueprints of any size. Unlike REQ-BLD-ROTATE-IN-PLACE, which it replaces in this mode, it applies to Tunnel Entries and Tunnel Exits too — nothing is re-oriented, so the reason for their exception does not arise. + On placement the overlapped building is **left completely untouched**: no construction site is placed on it, no building blocks are charged for it (it is excluded from the total cost of REQ-UI-BLUEPRINT-PLACE), its rotation is not changed, and a construction site's progress is preserved. This applies per building in the blueprint, independently, and to blueprints of any size. Unlike REQ-BLD-ROTATE-IN-PLACE, which it replaces in this mode, it applies to Tunnel Entries and Tunnel Exits too — nothing is re-oriented, so the reason for their exception does not arise. A coinciding same-type building whose **rotation differs** is not a compatible overlap: the blueprint cannot rotate it (REQ-BLD-ROTATE-IN-PLACE no longer applies here), so the position is an ordinary occupied-tile overlap and therefore invalid. - **Order of the two rules.** A ghost over a coinciding same-type building is tested for a configuration transfer (REQ-UI-BLUEPRINT-TRANSFER) first, and this requirement governs only what that test does not claim. Since a transfer requires a single-building blueprint of a configurable type, the split is: - - **Single-building blueprint, configurable type** (Miner, Assembler, Shipyard, Splitter) — always a transfer, at any target rotation. This requirement never applies. - - **Single-building blueprint, type with no settings** (Smelter, Reprocessing Plant, Salvage Bay, belt, tunnel end) — a compatible overlap if the rotation matches, invalid otherwise. Clicking places nothing and does nothing. - - **Multi-building blueprint** — every ghost is judged by this requirement alone, whatever its type: a coinciding same-type, same-rotation building is left untouched and the rest of the constellation is placed around it. No building in a multi-building blueprint ever transfers configuration. + **Order of the two rules.** A ghost over a coinciding same-type building is tested for a configuration transfer (REQ-UI-BLUEPRINT-TRANSFER) first, and this requirement governs only what that test does not claim. Because a coinciding building of a **configurable** type always transfers, this requirement covers exactly the types that have nothing to configure: + - **Configurable type** (Miner, Assembler, Shipyard, Splitter) — a transfer, never a compatible overlap. It is drawn in the transfer color and hands the blueprint's settings over. + - **Type with no settings** (Smelter, Reprocessing Plant, Salvage Bay, belt, tunnel end) — a compatible overlap if the rotation matches, invalid otherwise. There is nothing to hand over, so the building is simply left as it is and the ghost keeps its ordinary color. -- REQ-UI-BLUEPRINT-TRANSFER: **Configuration transfer.** A single-building blueprint doubles as a way to copy one building's settings onto other buildings of the same type: select a configured building, press **C** to capture it as a temporary blueprint and enter placement mode (REQ-UI-BLUEPRINT-TEMP), then click same-type buildings to stamp its settings onto them. **V** re-enters that mode later. + A single blueprint can hold both kinds at once, and each ghost is judged on its own: dropping a constellation over a partial copy of itself may reconfigure some of the buildings already there (cyan) while leaving others untouched (ordinary color) and placing the rest as new construction sites. + +- REQ-UI-BLUEPRINT-TRANSFER: **Configuration transfer.** A blueprint hands its stored settings to buildings that are already standing where it wants them, instead of only to ones it places. A single-building blueprint therefore doubles as a way to copy one building's settings onto others of the same type: select a configured building, press **C** to capture it as a temporary blueprint and enter placement mode (REQ-UI-BLUEPRINT-TEMP), then click same-type buildings to stamp its settings onto them. **V** re-enters that mode later. A multi-building blueprint does the same for each of its buildings as it is placed. A blueprint ghost is a **configuration-transfer target** when all of the following hold: - - The blueprint holds **exactly one** building. A blueprint of two or more buildings never transfers, in whole or in part. - - That building is of a **configurable building type** — one with player-facing settings: Miner and Assembler (recipe), Shipyard (schematic and module layout), Splitter (output filters). Whether anything was actually configured at capture time is irrelevant; an unconfigured source transfers its unconfigured state (see below). Building types with no settings at all (Smelter, Reprocessing Plant, Salvage Bay, belts, tunnel entries and exits, the HQ) never transfer. - - The ghost's footprint **exactly coincides** with the footprint of an existing placed building or construction site of the same building type: the same coincidence test as REQ-BLD-ROTATE-IN-PLACE. A ghost that merely overlaps such a building without coinciding is not a transfer target. The target's **rotation is irrelevant** — a transfer never rotates anything, so a differently-facing target transfers exactly like an identically-facing one. + - The building is of a **configurable building type** — one with player-facing settings: Miner and Assembler (recipe), Shipyard (schematic and module layout), Splitter (output filters). Whether anything was actually configured at capture time is irrelevant; an unconfigured source transfers its unconfigured state (see below). Building types with no settings at all (Smelter, Reprocessing Plant, Salvage Bay, belts, tunnel entries and exits, the HQ) never transfer; a coinciding building of those types is a compatible overlap instead (REQ-UI-BLUEPRINT-OVERLAP). + - The ghost's footprint **exactly coincides** with the footprint of an existing placed building or construction site of the same building type: the same coincidence test as REQ-BLD-ROTATE-IN-PLACE. A ghost that merely overlaps such a building without coinciding is not a transfer target. + - The target has the **same rotation** as the ghost — which is what makes the position valid at all (REQ-UI-BLUEPRINT-OVERLAP), since blueprint placement may not re-orient anything. **Exception:** when the blueprint holds **exactly one** building, rotation is disregarded and a differently-facing target transfers exactly like an identically-facing one. A transfer never rotates anything, so for the deliberate one-click gesture the target's facing is beside the point; inside a constellation it still has to match, or the placement would be invalid there. At a transfer target: - The ghost is drawn in a distinct **transfer** color read from `visuals.toml [overlays]`, overriding both the per-building coloring and the "invalid" color (REQ-BLD-GHOST, REQ-BLD-PLACE-VALID). The position counts as valid despite the occupied tiles (REQ-BLD-PLACE-VALID condition (b)). - - **Left-clicking transfers the configuration** to the existing building or site, making the target's settings **identical to the source's**: the recipe ID (Miner, Assembler), the schematic ID together with the ship layout (Shipyard), or the two output filters (Splitter). No construction site is placed, no building blocks are consumed, and the target's **rotation is not changed** — a transfer never rotates. - - The transfer is a **full mirror, including the absence of a setting**: where the blueprint stores no configuration for a field (REQ-UI-BLUEPRINT-STORAGE stores nothing for an unselected recipe or schematic, and no filter lists for a splitter whose filters were empty at capture time), the target's corresponding setting is **cleared** rather than left as it was. So a splitter captured with no filters clears the target splitter's filters back to accept-all, and a miner captured with no recipe selected clears the target miner's recipe. + - **Left-clicking transfers the configuration** to the existing building or site, making the target's settings **identical to the source's**: the recipe ID (Miner, Assembler), the schematic ID together with the ship layout (Shipyard), or the two output filters (Splitter). No construction site is placed, no building blocks are consumed (it is excluded from the total cost of REQ-UI-BLUEPRINT-PLACE), and the target's **rotation is not changed** — a transfer never rotates. + - The transfer is a **full mirror, including the absence of a setting**: where the blueprint stores no configuration for a field (REQ-UI-BLUEPRINT-STORAGE stores nothing for an unselected recipe or schematic, and no filter lists for a splitter whose filters were empty at capture time), the target's corresponding setting is **cleared** rather than left as it was. So a splitter captured with no filters clears the target splitter's filters back to accept-all, and a miner captured with no recipe selected clears the target miner's recipe. This holds for every blueprint size, so a constellation captured from unconfigured buildings clears the settings of every matching building it is dropped on. - The transfer has the same effects as making that selection through the selected building panel, clearing included: buffer clearing per REQ-MAT-INPUT-BUFFER and REQ-MAT-OUTPUT-BUFFER, and, for a Shipyard, in-progress cycle cancellation per REQ-BLD-SHIPYARD. It inherits the no-op rule of REQ-MAT-INPUT-BUFFER with them: a transfer onto a building whose settings already match the source changes nothing at all — no buffers cleared, no production cycle cancelled, no construction progress lost — so repeatedly clicking already-matching buildings is harmless. Each field is judged on its own, so transferring an identical recipe with a differing layout affects only the layout. The layout configuration dialog does not auto-open (REQ-MOD-UI-AUTO-DIALOG). - Unlock gating matches placement (REQ-UI-BLUEPRINT-PLACE): a stored schematic is applied only if it is currently unlocked, and locked recipe IDs and splitter filter entries for locked item types are handled per REQ-LOCK-UI-BLUEPRINT. - Both operational buildings and construction sites are transfer targets (REQ-BLD-SITE-CONFIG); a configuration applied to a site carries over unchanged when it finishes building. - After the transfer the game stays in blueprint placement mode, so further same-type buildings can be clicked in turn. + - A blueprint placement applies every transfer among its ghosts in the same click that places its new construction sites (REQ-UI-BLUEPRINT-PLACE); the placement is all-or-nothing, so if any ghost is invalid nothing is placed and nothing is transferred. - REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint card (REQ-UI-BLUEPRINT-CARD) immediately removes that blueprint from the list, without a confirmation prompt. The blueprint selection dialog stays open and its card grid reflows to close the gap. If the deleted blueprint was active in blueprint placement mode, that mode is exited. diff --git a/src/lib/sim/PlacementRules.cpp b/src/lib/sim/PlacementRules.cpp index 97e54bc..28ace96 100644 --- a/src/lib/sim/PlacementRules.cpp +++ b/src/lib/sim/PlacementRules.cpp @@ -144,24 +144,29 @@ BlueprintGhostResolved resolveBlueprintGhost(const FactoryState& state, const Ga findCoincidingSameTypeBuilding(state, config, type, anchor, rotation); if (coinciding.has_value()) { - // A single configurable building hands its settings over, whatever way the target - // faces: a transfer never rotates anything, so the target's facing does not matter - // (REQ-UI-BLUEPRINT-TRANSFER). Tested before the overlap rule, which then governs - // only what this does not claim. - if (blueprintHoldsOneBuilding && isConfigurableBuildingType(type)) - { - return BlueprintGhostResolved{BlueprintGhostAction::Transfer, coinciding->id}; - } + const bool configurable = isConfigurableBuildingType(type); - // Otherwise the building the blueprint wants must already be there in full, - // facing the same way, because nothing here may re-orient it - // (REQ-UI-BLUEPRINT-OVERLAP). Tunnels are not excluded: nothing is rotated, so - // the reason for their REQ-BLD-ROTATE-IN-PLACE exception does not arise. + // The building the blueprint wants is already there, facing the same way. It + // takes the blueprint's settings if it has any to take (REQ-UI-BLUEPRINT-TRANSFER) + // and is otherwise left exactly as it is (REQ-UI-BLUEPRINT-OVERLAP). Tunnels are + // not excluded from the latter: nothing is rotated, so the reason for their + // REQ-BLD-ROTATE-IN-PLACE exception does not arise. if (coinciding->rotation == rotation) { - return BlueprintGhostResolved{BlueprintGhostAction::CompatibleOverlap, + return BlueprintGhostResolved{configurable + ? BlueprintGhostAction::Transfer + : BlueprintGhostAction::CompatibleOverlap, coinciding->id}; } + + // Facing the other way. Placement may not re-orient it, so this is invalid -- + // except for the deliberate one-click gesture of a single-building blueprint, + // where a transfer rotates nothing anyway and the target's facing is beside the + // point (REQ-UI-BLUEPRINT-TRANSFER). + if (blueprintHoldsOneBuilding && configurable) + { + return BlueprintGhostResolved{BlueprintGhostAction::Transfer, coinciding->id}; + } return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt}; } diff --git a/src/lib/sim/PlacementRules.h b/src/lib/sim/PlacementRules.h index a8a5c5a..085ed94 100644 --- a/src/lib/sim/PlacementRules.h +++ b/src/lib/sim/PlacementRules.h @@ -82,9 +82,11 @@ struct BlueprintGhostResolved }; // Classifies one ghost of a blueprint against the current factory state -// (REQ-UI-BLUEPRINT-OVERLAP, REQ-UI-BLUEPRINT-TRANSFER). `blueprintHoldsOneBuilding` is -// the blueprint's stored size, counted before locked types are dropped, so the gesture -// does not change behavior as the player unlocks things. +// (REQ-UI-BLUEPRINT-OVERLAP, REQ-UI-BLUEPRINT-TRANSFER). `blueprintHoldsOneBuilding` +// only widens the transfer rule to differently-facing targets; a same-facing target +// transfers whatever the blueprint's size. It is the blueprint's stored size, counted +// before locked types are dropped, so the gesture does not change behavior as the player +// unlocks things. // // Shared by the ghost coloring and the click path so a preview cannot disagree with what // the click then does -- the same reason resolveBeltDragPath is shared. diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index d7c7dc8..e13d898 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -1198,24 +1198,72 @@ TEST_CASE("resolveBlueprintGhost: a single building with no settings overlaps in == BlueprintGhostAction::Invalid); } -TEST_CASE("resolveBlueprintGhost: nothing in a multi-building blueprint transfers", +TEST_CASE("resolveBlueprintGhost: a constellation transfers onto a matching building", "[blueprint]") { - // Even a configurable building coinciding with its twin only overlaps once the - // blueprint holds more than one building (REQ-UI-BLUEPRINT-TRANSFER). + // Blueprint size does not gate the transfer itself: a configurable building already + // standing where the blueprint wants it, facing the same way, takes its settings + // whatever else the blueprint holds (REQ-UI-BLUEPRINT-TRANSFER). PlacementFixture f; const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(-2, 0), Rotation::East, 0).value(); - const BlueprintGhostResolved matching = + const BlueprintGhostResolved resolved = resolveInConstellation(f, BuildingType::Miner, QPoint(-2, 0), Rotation::East); - REQUIRE(matching.action == BlueprintGhostAction::CompatibleOverlap); - CHECK(*matching.targetId == id); + REQUIRE(resolved.action == BlueprintGhostAction::Transfer); + CHECK(*resolved.targetId == id); +} - // ... and a differently-facing twin blocks the whole constellation. - CHECK(resolveInConstellation(f, BuildingType::Miner, QPoint(-2, 0), Rotation::North).action +TEST_CASE("resolveBlueprintGhost: only a single-building blueprint ignores target rotation", + "[blueprint]") +{ + // The one thing blueprint size still decides. Inside a constellation a differently + // facing twin cannot be re-oriented, so it blocks the whole placement; alone, the + // gesture transfers anyway because it rotates nothing (REQ-UI-BLUEPRINT-TRANSFER). + // + // A splitter, because the question only arises for a footprint that survives + // rotation. A miner's body is L-shaped ("AA" / "A>"), so a rotated miner ghost covers + // different tiles and coincides with nothing at all -- invalid for a reason that has + // nothing to do with these rules. + PlacementFixture f; + + const BuildingId id = + f.bs.place(f.state, BuildingType::Splitter, QPoint(-1, 0), Rotation::East, 0).value(); + + CHECK(resolveInConstellation(f, BuildingType::Splitter, QPoint(-1, 0), Rotation::North).action == BlueprintGhostAction::Invalid); + + const BlueprintGhostResolved alone = + resolveOne(f, BuildingType::Splitter, QPoint(-1, 0), Rotation::North); + REQUIRE(alone.action == BlueprintGhostAction::Transfer); + CHECK(*alone.targetId == id); +} + +TEST_CASE("resolveBlueprintGhost: a constellation mixes transfers and plain overlaps", + "[blueprint]") +{ + // One drop can reconfigure some of the buildings already there while leaving others + // alone: the split is by whether the type has settings at all, not by blueprint size + // (REQ-UI-BLUEPRINT-OVERLAP). + PlacementFixture f; + + const BuildingId minerId = + f.bs.place(f.state, BuildingType::Miner, QPoint(-2, 0), Rotation::East, 0).value(); + const BuildingId smelterId = + f.bs.place(f.state, BuildingType::Smelter, QPoint(-5, 0), Rotation::East, 0).value(); + + const BlueprintGhostResolved miner = + resolveInConstellation(f, BuildingType::Miner, QPoint(-2, 0), Rotation::East); + REQUIRE(miner.action == BlueprintGhostAction::Transfer); + CHECK(*miner.targetId == minerId); + + // A smelter runs an implicit recipe (REQ-BLD-SMELTER), so there is nothing to hand + // over and it is simply left as it is. + const BlueprintGhostResolved smelter = + resolveInConstellation(f, BuildingType::Smelter, QPoint(-5, 0), Rotation::East); + REQUIRE(smelter.action == BlueprintGhostAction::CompatibleOverlap); + CHECK(*smelter.targetId == smelterId); } TEST_CASE("resolveBlueprintGhost: an identical tunnel is a compatible overlap", "[blueprint]")