Compare commits
3 Commits
2cbcf1554f
...
cd31af2611
| Author | SHA1 | Date | |
|---|---|---|---|
| cd31af2611 | |||
| fd6a7c5815 | |||
| 18cfe238f6 |
@@ -348,7 +348,6 @@ deconstruct_tint = "#ff000033" # deconstruct-mode hover tint
|
||||
selection_rect = "#00ff00" # box-drag selection rectangle (REQ-UI-MULTI-SELECT)
|
||||
tile_highlight = "#ffffff22" # tile under cursor
|
||||
selected_outline = "#ffff00" # outline drawn around currently-selected building(s)
|
||||
copy_config = "#33ccff66" # copy-settings eligible-target tint + copy/paste flash (REQ-BLD-COPY-CONFIG-FEEDBACK)
|
||||
locked_asteroid = "#0000007f" # tint over the asteroid left of the buildable edge (not yet unlocked by expansion)
|
||||
modal_dim = "#00000099" # semi-transparent black dim behind modal dialogs/menus (REQ-UI-MODAL-DIM)
|
||||
tunnel_preview = "#00ff0055" # tunnel connection preview: matched end + tiles between (REQ-BLD-TUNNEL-MODE)
|
||||
|
||||
@@ -333,7 +333,7 @@ The game world is drawn into a single `GameWorldView` widget that inherits `QOpe
|
||||
|
||||
The drawing itself lives in `WorldRenderer`, not in the widget. `paintGL` is a call sequence: build the frame's `WorldCoordinates`, hand the renderer a `WorldRenderFrame`, then draw the screen-anchored chrome. The split is the world-space / screen-space line, and it is exact: the renderer draws everything positioned in tiles, while everything positioned in pixels — the pause and deconstruct vignettes, the replay overlay, the debug stats panel — stays with the widget. A useful consequence is that the renderer draws no translatable text at all (its text is config-driven glyphs, ASCII port arrows, and numbers), so it needs no `tr()` and no tie to the meta-object system.
|
||||
|
||||
`WorldRenderFrame` is what makes the renderer independent of the widget. The renderer reads the simulation directly, but everything else it draws is interaction state the widget owns — the selection, the active build mode, live beams, the copy-settings feedback, the box-select rectangle. Those are gathered into the frame each `paintGL` and passed by reference, so the renderer keeps no copy that a later click could invalidate. The renderer knows nothing about input: the widget resolves clicks and hit-tests, and the renderer only draws the result.
|
||||
`WorldRenderFrame` is what makes the renderer independent of the widget. The renderer reads the simulation directly, but everything else it draws is interaction state the widget owns — the selection, the active build mode, live beams, the box-select rectangle. Those are gathered into the frame each `paintGL` and passed by reference, so the renderer keeps no copy that a later click could invalidate. The renderer knows nothing about input: the widget resolves clicks and hit-tests, and the renderer only draws the result.
|
||||
|
||||
### Render Loop
|
||||
|
||||
|
||||
@@ -147,12 +147,6 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
||||
- REQ-BLD-DECONSTRUCT-CLICK: While in deconstruct mode (REQ-UI-HOTKEYS, REQ-UI-DECONSTRUCT-BUTTON), left-clicking a placed factory building or construction site in the game world marks it for demolition, following the rules of REQ-BLD-DECONSTRUCT: 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 deconstructed (the HQ or a player defence station, per REQ-BLD-DECONSTRUCT), or clicking empty world space, has no effect. Deconstruct 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 Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON).
|
||||
- REQ-BLD-DECONSTRUCT-BOX: While in deconstruct mode (REQ-UI-HOTKEYS, REQ-UI-DECONSTRUCT-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-DECONSTRUCT: every construction site covered by the box is removed instantly with the full refund; and among the fully-built deconstructible 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 deconstructed (the HQ and player defence stations, per REQ-BLD-DECONSTRUCT) 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 deconstruct 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.
|
||||
- **Shift + left-click** a building of the **same type** as the cached one applies the cached settings to it, exactly as if the player had made that selection through the selected building panel — with the same effects as a normal selection change (buffer clearing per REQ-MAT-INPUT-BUFFER and REQ-MAT-OUTPUT-BUFFER, and, for a Shipyard, in-progress cycle cancellation per REQ-BLD-SHIPYARD). This can be repeated on any number of same-type buildings while Shift stays held. Shift + left-clicking a building of a different type than the cached one, any building while the cache is empty, or empty world space, has no effect.
|
||||
- Both operational buildings and construction sites take part as source and target (REQ-BLD-SITE-CONFIG); settings applied to a construction site carry over unchanged when it finishes building.
|
||||
- **Releasing Shift clears the temporary cache.** It is never persisted and does not survive Shift being released; the next copy starts fresh.
|
||||
- Because the cached settings were already valid on a same-type source building, they remain valid and available on the target (a selected recipe/schematic stays unlocked per REQ-LOCK-UI-RECIPE and REQ-LOCK-UI-SCHEMATIC; splitter filter item types stay unlocked per REQ-LOCK-UI-SPLITTER).
|
||||
|
||||
## Building Types
|
||||
|
||||
@@ -323,7 +317,7 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
||||
|
||||
- REQ-MOD-UI-EMPTY-PULSE: While a module is selected for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG), the empty buildable cells of the layout grid pulse smoothly around their normal fill shade, oscillating between a slightly darker and a slightly brighter shade at approximately 1 Hz (one full cycle per second), to draw the player's attention to where the module can be placed. All empty buildable cells pulse in phase. When no module is selected for placement (including remove mode), empty buildable cells render at their normal static shade. Non-buildable cells and cells occupied by a placed module do not pulse.
|
||||
|
||||
- REQ-MOD-UI-AUTO-DIALOG: When the player selects a schematic for a shipyard (operational building or construction site) through the schematic selection dialog (REQ-UI-SELECT-BUTTON), and the chosen schematic **differs** from the shipyard's current schematic, the layout configuration dialog (REQ-MOD-UI-DIALOG) opens automatically and immediately once the selection dialog closes — exactly as if the player had then clicked "Configure". Re-selecting the schematic already set does not reopen the dialog. This auto-open applies only to the manual schematic selection dialog; schematic changes applied via the copy-settings gesture (REQ-BLD-COPY-CONFIG) or blueprint placement (REQ-UI-BLUEPRINT-PLACE) do **not** auto-open the dialog. The player may still cancel the auto-opened dialog (REQ-MOD-UI-DIALOG), which leaves the newly selected schematic in place with its default empty layout; the "Configure" button (REQ-MOD-UI-PREVIEW) remains available to open the dialog again later.
|
||||
- REQ-MOD-UI-AUTO-DIALOG: When the player selects a schematic for a shipyard (operational building or construction site) through the schematic selection dialog (REQ-UI-SELECT-BUTTON), and the chosen schematic **differs** from the shipyard's current schematic, the layout configuration dialog (REQ-MOD-UI-DIALOG) opens automatically and immediately once the selection dialog closes — exactly as if the player had then clicked "Configure". Re-selecting the schematic already set does not reopen the dialog. This auto-open applies only to the manual schematic selection dialog; a schematic applied by blueprint placement (REQ-UI-BLUEPRINT-PLACE) does **not** auto-open the dialog. The player may still cancel the auto-opened dialog (REQ-MOD-UI-DIALOG), which leaves the newly selected schematic in place with its default empty layout; the "Configure" button (REQ-MOD-UI-PREVIEW) remains available to open the dialog again later.
|
||||
|
||||
- REQ-MOD-UI-MODULE-TOOLTIP: Each module selection button in the layout configuration dialog (REQ-MOD-UI-DIALOG) shows a hover tooltip with the descriptive text defined for that module type in `modules.toml` (the optional per-module tooltip field). If a module type defines no tooltip text, its button shows no tooltip. The "Remove" button is not a module type and has no config-defined tooltip.
|
||||
|
||||
@@ -433,7 +427,7 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
|
||||
|
||||
### Layout
|
||||
|
||||
The screen is divided into two columns: a main column (75% width) containing the header bar and game world, and a side panel column (25% width) containing the two UI panels stacked vertically. The build button bar (REQ-UI-BUILD-BAR) is not part of either column — it floats over the game world at its bottom center:
|
||||
The screen is divided into two columns: a main column (75% width) containing the header bar and game world, and a side panel column (25% width) holding the selected building panel. The build button bar (REQ-UI-BUILD-BAR) is not part of either column — it floats over the game world at its bottom center. Blueprints have no permanent screen real estate; they are reached through modal dialogs (REQ-UI-BLUEPRINT-DIALOG):
|
||||
|
||||
```
|
||||
+--------------------------------------+--------------+
|
||||
@@ -442,10 +436,10 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
| | Building |
|
||||
| | Panel |
|
||||
| Game World | |
|
||||
| +--------------+
|
||||
| | |
|
||||
| +------------------+ | Blueprint |
|
||||
| | Build Button Bar | | Panel |
|
||||
| | |
|
||||
| +------------------+ | |
|
||||
| | Build Button Bar | | |
|
||||
+--------+------------------+----------+--------------+
|
||||
(75% width) (25% width)
|
||||
```
|
||||
@@ -460,8 +454,8 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
- REQ-UI-DECONSTRUCT-BORDER: While deconstruct mode is active (REQ-UI-DECONSTRUCT-BUTTON, REQ-UI-HOTKEYS), a vignette border is drawn around the edges of the game world view to signal the mode, matching the geometry of the paused-state vignette (REQ-UI-PAUSE-BORDER): a 100-pixel thickness (capped at half the smaller viewport dimension on very small views) with the four sides meeting along mitred corner diagonals. It fades in the alpha channel from fully transparent at its inner (center-facing) edge to the deconstruct tint color at the viewport edge. The color — including its alpha, which sets the peak opacity at the viewport edge — is read from `visuals.toml [overlays].deconstruct_tint`, the same deconstruct-mode color used for the hover tint. The border is presentation-only and has no effect on the simulation. If the game is both paused and in deconstruct mode, both vignettes are drawn and compose over each other.
|
||||
- REQ-UI-EXPAND-BUTTON: The header bar shows an asteroid expansion button captioned `Expand: <x>` followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON) in place of the trailing `Blocks` word, where `<x>` is the current expansion cost computed from `world.toml [expansion].cost_building_blocks_formula` at the current number of purchased expansions (REQ-EXP-COST). When no icon file exists for `building_block`, the caption falls back to the `Expand: <x> Blocks` text. Clicking the button unlocks the next asteroid expansion (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND), spending that many building blocks from the global stock. The button is disabled when the player cannot currently afford the cost (consistent with REQ-UI-BUILD-DISABLED). The caption updates as the cost changes with each purchased expansion.
|
||||
- REQ-UI-WORLD-SIZE: The game world view occupies the full height below the header bar in the main column (75% of the screen width).
|
||||
- REQ-UI-PANEL-COLUMN: The side panel column occupies 25% of the screen width and the full screen height. It is divided into two equal-height panels stacked top to bottom: selected building panel (top) and blueprint panel (bottom). The build buttons are not part of this column; they float over the game world (REQ-UI-BUILD-BAR).
|
||||
- REQ-UI-MODAL-DIM: While a modal dialog, menu, or full-screen state screen is open on top of the game, a transparent black overlay (a dim/scrim) is drawn over the **entire game window** — the header bar, the game world view, and the side panel column — behind that modal, so the game reads as inactive while the modal holds focus. The overlay is shown for every modal that auto-pauses the simulation — the escape menu (REQ-UI-GAME-MENU), the recipe/schematic selection dialog (REQ-UI-SELECT-BUTTON), the layout configuration dialog (REQ-MOD-UI-DIALOG), and the schematic choice dialog (REQ-DEF-SCHEMATIC-DROP) — as well as the game-over screen (REQ-HQ-GAME-OVER) and the win screen (REQ-WIN-SCREEN), which end rather than pause the game. When modals are nested (for example the Create Blueprint name dialog (REQ-MOD-UI-BLUEPRINT-CREATE) opened from the layout configuration dialog), only a single dim is shown over the game window; nested modals do not stack additional overlays. The dim color and opacity are read from `visuals.toml [overlays]` (a semi-transparent black modal-dim color), consistent with the other overlay colors. The overlay is presentation-only and has no effect on the simulation.
|
||||
- REQ-UI-PANEL-COLUMN: The side panel column occupies 25% of the screen width and the full screen height. It holds a single panel filling that full height: the selected building panel. The build buttons are not part of this column; they float over the game world (REQ-UI-BUILD-BAR). Blueprints are not part of this column either; they are reached through the blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG).
|
||||
- REQ-UI-MODAL-DIM: While a modal dialog, menu, or full-screen state screen is open on top of the game, a transparent black overlay (a dim/scrim) is drawn over the **entire game window** — the header bar, the game world view, and the side panel column — behind that modal, so the game reads as inactive while the modal holds focus. The overlay is shown for every modal that auto-pauses the simulation — the escape menu (REQ-UI-GAME-MENU), the recipe/schematic selection dialog (REQ-UI-SELECT-BUTTON), the layout configuration dialog (REQ-MOD-UI-DIALOG), the schematic choice dialog (REQ-DEF-SCHEMATIC-DROP), the blueprint save dialog (REQ-UI-BLUEPRINT-CREATE), and the blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG) — as well as the game-over screen (REQ-HQ-GAME-OVER) and the win screen (REQ-WIN-SCREEN), which end rather than pause the game. When modals are nested (for example the Create Blueprint name dialog (REQ-MOD-UI-BLUEPRINT-CREATE) opened from the layout configuration dialog), only a single dim is shown over the game window; nested modals do not stack additional overlays. The same applies when one modal hands directly off to another — the blueprint save dialog opening the blueprint selection dialog on confirm (REQ-UI-BLUEPRINT-CREATE): the dim persists across the handoff rather than flickering off and back on, and the simulation is not resumed in between. The dim color and opacity are read from `visuals.toml [overlays]` (a semi-transparent black modal-dim color), consistent with the other overlay colors. The overlay is presentation-only and has no effect on the simulation.
|
||||
|
||||
### Game World
|
||||
|
||||
@@ -495,8 +489,11 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
- **A / D** — scroll the view left / right (REQ-UI-SCROLL).
|
||||
- **Q** — context-sensitive. If a build mode is active (builder mode or blueprint placement mode), pressing Q exits it. Otherwise, pressing Q toggles deconstruct mode: it enters deconstruct mode if inactive, or exits deconstruct mode if already active. (See also REQ-UI-DECONSTRUCT-BUTTON for the equivalent button.)
|
||||
- **R / Shift+R** — in builder mode, rotate the ghost counter-clockwise / clockwise (REQ-BLD-ROTATE).
|
||||
- **T** — create a temporary blueprint from the current selection and enter its placement mode (REQ-UI-BLUEPRINT-TEMP).
|
||||
- **Escape** — opens the escape menu (REQ-UI-GAME-MENU).
|
||||
- **C** — create a temporary blueprint from the current selection and enter its placement mode (REQ-UI-BLUEPRINT-TEMP). Has effect only when at least one player-placeable building is selected; otherwise it does nothing.
|
||||
- **V** — re-enter placement mode for the last temporary blueprint created with C (REQ-UI-BLUEPRINT-TEMP). Does nothing when no temporary blueprint exists.
|
||||
- **Ctrl+C** — save the current selection as a named blueprint: opens the blueprint save dialog (REQ-UI-BLUEPRINT-CREATE). Has effect only when at least one player-placeable building is selected; otherwise it does nothing.
|
||||
- **Ctrl+V** — opens the blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG), from which a saved blueprint is picked for placement. It is available whenever the game is being played, regardless of the current selection or of which build mode is active, and opens the dialog even when no blueprints are saved yet.
|
||||
- **Escape** — opens the escape menu (REQ-UI-GAME-MENU). While a blueprint dialog is open, Escape closes that dialog instead (REQ-UI-BLUEPRINT-DIALOG).
|
||||
- **Build mode selection** — pressing a build hotkey activates builder mode for the corresponding building type, equivalent to clicking its build button (REQ-BLD-BUILDER-MODE):
|
||||
- **1** — Belt, **2** — Splitter, **3** — Tunnel (the unified tunnel build mode, REQ-BLD-TUNNEL-MODE). Hotkey 4 is unused.
|
||||
- **Shift+1** — Miner, **Shift+2** — Smelter, **Shift+3** — Assembler, **Shift+4** — Shipyard, **Shift+5** — Salvage Bay, **Shift+6** — Reprocessing Plant.
|
||||
@@ -527,7 +524,7 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
- REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows: building name, current recipe or schematic selection, input buffer contents, and output buffer contents. Buffer counts are displayed as `a/b` where `a` is the current item count and `b` is the per-cycle amount (items consumed per run for inputs; items produced per run for outputs). For a selected construction site, the recipe/schematic selection (and, for a shipyard, the layout preview and "Configure" button) are shown but the buffer rows are omitted (REQ-BLD-SITE-CONFIG).
|
||||
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the selected building panel also shows: (a) the cycle time of the currently selected recipe or schematic in seconds, and (b) the completion percentage of the active production cycle as an integer (e.g. `42%`), or the text `idle` when no production cycle is active. When no recipe or schematic is selected, neither the cycle time nor the progress indicator is shown.
|
||||
- REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or debris selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
|
||||
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown. The panel additionally shows the **total building block cost** of the selection — the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button bar); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-BUTTON). Construction sites count at their building type's full placement cost regardless of construction progress.
|
||||
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown. The panel additionally shows the **total building block cost** of the selection — the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button bar); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-CARD). Construction sites count at their building type's full placement cost regardless of construction progress.
|
||||
- REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel. Recipe selection (miner, assembler) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW).
|
||||
- REQ-UI-SELECT-BUTTON: **Recipe and schematic selection control.** Recipe selection (Miner ore type, Assembler recipe) and schematic selection (Shipyard) are each presented in the selected building panel as a single **selection button** whose caption is the name of the currently selected recipe or schematic, or a placeholder ("Select recipe" / "Select schematic") when none is selected. Clicking the button opens a modal **selection dialog** that pauses the game (speed set to 0×; on close, the speed is restored to what it was before the dialog was opened). The dialog contains a grid of option buttons, one per selectable option — only options that are currently unlocked are shown (REQ-LOCK-UI-RECIPE for recipes, REQ-LOCK-UI-SCHEMATIC for schematics). Hovering an option button shows the selection info tooltip (REQ-UI-SELECT-TOOLTIP). Clicking an option button selects that recipe/schematic, closes the dialog, and updates the selection button's caption in the selected building panel. The dialog can be dismissed without changing the current selection (e.g. closing it without clicking an option). Selecting a new recipe or schematic has the same effects as before (REQ-MAT-INPUT-BUFFER, REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
|
||||
- REQ-UI-SELECT-TOOLTIP: **Selection info tooltip.** Hovering an option button in the selection dialog (REQ-UI-SELECT-BUTTON), and hovering the selection button in the selected building panel when a selection is set, displays an info tooltip:
|
||||
@@ -563,23 +560,56 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
- REQ-UI-BUILD-DISABLED: Buttons for buildings the player cannot currently afford are shown as disabled. A disabled button's icon (REQ-UI-BUILD-ICON) is rendered in a greyed variant, with its colored chip background recolored grey while the white glyph is retained.
|
||||
- REQ-UI-DECONSTRUCT-BUTTON: A dedicated **Deconstruct** button is shown in the build button bar (REQ-UI-BUILD-BAR), as the last entry of the row and **visually separated** from the building-type buttons by a gap (not a divider line), because it toggles a mode rather than selecting a building type. Its face follows REQ-UI-BUILD-COST with two differences: its hotkey badge reads `Q`, and because it has no building block cost it shows its **Deconstruct** name as a text caption where the building-type buttons show their cost — so it is the one labelled button in the bar. It is therefore wider than the building-type buttons, which share a uniform width. Clicking it toggles deconstruct mode on and off, equivalent to the Q deconstruct toggle (REQ-UI-HOTKEYS). The button is shown in a visually active/pressed state while deconstruct mode is active. The button shows a hover tooltip stating the deconstruction refund (REQ-BLD-DECONSTRUCT): that deconstructing a fully-built building returns `world.toml [world].refund_percentage` percent of its building block cost once deconstruction completes, and that a construction site removed before it finishes building is refunded in full. When `refund_percentage` is 100% both cases yield the same refund, and the tooltip is simplified to state the single refund percentage without distinguishing the two cases. Unlike the building-type button tooltips (REQ-UI-BUILD-TOOLTIP), this tooltip is not config-defined text but is composed from the refund percentage.
|
||||
|
||||
### Blueprint Panel
|
||||
### Blueprints
|
||||
|
||||
- REQ-UI-BLUEPRINT-PANEL: The blueprint panel is the lower of the two side panel column panels, below the selected building panel (REQ-UI-PANEL-COLUMN). It contains, from top to bottom: a "Create Blueprint" button, and a list of blueprint entries (one per saved blueprint, in creation order). The panel has no Save or Load buttons; blueprints are persisted automatically (REQ-UI-BLUEPRINT-SAVE) and restored at startup (REQ-UI-BLUEPRINT-LOAD).
|
||||
Blueprints occupy no permanent screen space. They are saved with **Ctrl+C** from the current selection (REQ-UI-BLUEPRINT-CREATE) and picked for placement from the blueprint selection dialog, opened with **Ctrl+V** (REQ-UI-BLUEPRINT-DIALOG). The unmodified **C** and **V** keys are the throwaway counterparts of the same two gestures: they capture and re-place a single unnamed temporary blueprint that is never saved and never listed (REQ-UI-BLUEPRINT-TEMP). There is no blueprint panel in the side panel column (REQ-UI-PANEL-COLUMN). (The ship layout blueprint panel of the layout configuration dialog, REQ-MOD-UI-BLUEPRINT-PANEL, is a separate feature and is unaffected.)
|
||||
|
||||
- REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button bar) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. A selected player-placeable building may be either an operational building or a construction site (a building placed but not yet fully built, REQ-BLD-SITE-CONFIG); both count toward this condition and are captured identically (REQ-UI-BLUEPRINT-STORAGE). When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list.
|
||||
- REQ-UI-BLUEPRINT-CREATE: Pressing **Ctrl+C** (REQ-UI-HOTKEYS) opens the modal **blueprint save dialog**, which pauses the simulation and dims the game window (REQ-UI-MODAL-DIM). It has effect only when at least one player-placeable building (i.e. a building with a button in the build button bar) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition, and pressing Ctrl+C with an empty selection or a selection of only non-player-placeable buildings does nothing (no dialog opens). A selected player-placeable building may be either an operational building or a construction site (a building placed but not yet fully built, REQ-BLD-SITE-CONFIG); both count toward this condition and are captured identically (REQ-UI-BLUEPRINT-STORAGE). The dialog prompts the player to enter a name and has Confirm and Cancel buttons. Clicking Cancel — or pressing Escape, or closing the dialog — closes it with no effect and does not open the blueprint selection dialog. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, appends it to the blueprint list, closes the save dialog, and immediately opens the blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG) showing the new blueprint among the others.
|
||||
|
||||
- REQ-UI-BLUEPRINT-TEMP: Pressing the **T** key (REQ-UI-HOTKEYS) creates a **temporary blueprint** from the current selection and immediately enters blueprint placement mode for it, without opening the naming dialog. It has effect only when at least one player-placeable building is currently selected — the same condition as REQ-UI-BLUEPRINT-CREATE; pressing T with an empty selection, or a selection containing only non-player-placeable buildings (HQ, defence stations), does nothing. Entering this mode replaces any currently active build, blueprint placement, or deconstruct mode. The temporary blueprint is captured exactly as a saved blueprint (REQ-UI-BLUEPRINT-STORAGE), silently excluding any non-player-placeable buildings from the selection, but it is never named, never shown in the blueprint panel (REQ-UI-BLUEPRINT-PANEL), and never persisted to `blueprints.toml` (REQ-UI-BLUEPRINT-SAVE). Placement behaves identically to a saved blueprint's placement mode (REQ-UI-BLUEPRINT-MODE, REQ-UI-BLUEPRINT-PLACE): a ghost is rendered per building, R / Shift+R rotate the entire constellation, placement follows the same per-building validity and total-cost rules, and after a successful placement the mode stays active so the blueprint can be placed again. Right-clicking in the game world exits placement mode, at which point the temporary blueprint is discarded.
|
||||
- REQ-UI-BLUEPRINT-DIALOG: The **blueprint selection dialog** is the only place saved blueprints are shown. It is opened by pressing **Ctrl+V** (REQ-UI-HOTKEYS) and by confirming a save (REQ-UI-BLUEPRINT-CREATE). It is modal, pauses the simulation, and dims the game window (REQ-UI-MODAL-DIM). The dialog has a fixed size and consists of:
|
||||
- A **title bar** reading `Blueprints`, followed by a small dimmed **hotkey badge** reading `Ctrl+V` — the same "learn the shortcut from the widget" device as the build button badges (REQ-UI-BUILD-COST) — and, at the far right, a **close ("×") button**.
|
||||
- Below it, a **scrollable two-column grid of blueprint cards** (REQ-UI-BLUEPRINT-CARD), one per saved blueprint, filling the grid left to right and top to bottom in creation order. The column count is fixed at two; the grid scrolls vertically when the cards do not fit, and does not scroll horizontally.
|
||||
- When no blueprints are saved, the dialog still opens and shows an empty-state message in place of the grid, telling the player that blueprints are created with Ctrl+C from a selection of buildings.
|
||||
|
||||
Clicking the close button, pressing Escape, or closing the dialog through the window manager closes it with no other effect: the current selection, build mode, and blueprint list are unchanged, and the simulation speed is restored to what it was before the dialog was opened. While the dialog is open, Escape closes it rather than opening the escape menu (REQ-UI-GAME-MENU).
|
||||
|
||||
```
|
||||
+------------------------------------------------------+
|
||||
| Blueprints [Ctrl+V] [x] |
|
||||
+------------------------------------------------------+
|
||||
| +---------------------+ +---------------------+ |^| |
|
||||
| | Smelter array | | Gear cell | | | |
|
||||
| | 4 Smelters, 1 Miner | | 2 Assemblers, ... | | | |
|
||||
| | 528 [blk] (x) | | 460 [blk] (x) | | | |
|
||||
| +---------------------+ +---------------------+ | | |
|
||||
| +---------------------+ | | |
|
||||
| | Defence line | |_v_|
|
||||
+------------------------------------------------------+
|
||||
```
|
||||
|
||||
- REQ-UI-BLUEPRINT-TEMP: Pressing the **C** key (REQ-UI-HOTKEYS) creates a **temporary blueprint** from the current selection and immediately enters blueprint placement mode for it, without opening the naming dialog. It has effect only when at least one player-placeable building is currently selected — the same condition as REQ-UI-BLUEPRINT-CREATE; pressing C with an empty selection, or a selection containing only non-player-placeable buildings (HQ, defence stations), does nothing at all, and in particular leaves any existing temporary blueprint in place. Entering this mode replaces any currently active build, blueprint placement, or deconstruct mode. The temporary blueprint is captured exactly as a saved blueprint (REQ-UI-BLUEPRINT-STORAGE), silently excluding any non-player-placeable buildings from the selection, but it is never named, never shown in the blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG), and never persisted to `blueprints.toml` (REQ-UI-BLUEPRINT-SAVE). Placement behaves identically to a saved blueprint's placement mode (REQ-UI-BLUEPRINT-MODE, REQ-UI-BLUEPRINT-PLACE): a ghost is rendered per building, R / Shift+R rotate the entire constellation, placement follows the same per-building validity and total-cost rules, and after a successful placement the mode stays active so the blueprint can be placed again. Right-clicking in the game world exits placement mode; unlike the mode, the temporary blueprint itself survives, so it can be entered again with V.
|
||||
|
||||
Pressing the **V** key re-enters blueprint placement mode for the temporary blueprint, capturing nothing new: it is independent of the current selection, can be pressed any number of times, and yields exactly the mode described above. Like C it replaces any currently active build, blueprint placement, or deconstruct mode, and it does not test whether the player can currently afford the blueprint — cost is enforced at placement (REQ-UI-BLUEPRINT-PLACE), consistently with C. Pressing V when no temporary blueprint exists does nothing: no mode is entered and any currently active mode is left untouched.
|
||||
|
||||
There is at most one temporary blueprint at a time; pressing C replaces the previous one. It is held only in memory for the current run: it is discarded when the application closes and when the simulation is restarted from the escape menu (REQ-UI-GAME-MENU), after which V does nothing until C captures a new one.
|
||||
|
||||
- REQ-UI-BLUEPRINT-STORAGE: A blueprint stores its name and, for each building in the selection, the building type, its rotation, its tile offset (integer dx, dy) from the center of the bounding box of all selected buildings' footprints, and — where applicable — the selected recipe ID (miners and assemblers) or schematic ID (shipyards), and for splitters the two output filters (each a list of item types; an empty list means accept-all), at the time of capture. A source building may be either an operational building or a construction site (REQ-BLD-SITE-CONFIG); a construction site is captured identically, storing whatever configuration it currently holds and never any buffer or construction-progress state. If no recipe or schematic was selected at capture time, none is stored; for a splitter with no filters set, no filter lists are stored. This structure maps directly to a TOML representation (e.g. one `[[building]]` array entry per constituent building, with the splitter filters as `filter_a`/`filter_b` arrays of item-type ids).
|
||||
|
||||
- REQ-UI-BLUEPRINT-BUTTON: Each blueprint entry consists of a blueprint button and a dedicated delete icon ("×") placed to the right of the button. The blueprint button displays the blueprint name and, below it, the total building block cost of the blueprint (sum of the individual costs of all constituent buildings). A blueprint button is disabled when the player cannot afford the total cost. Clicking an enabled blueprint button enters blueprint placement mode for that blueprint. The delete icon is always enabled regardless of whether the player can afford the blueprint.
|
||||
- REQ-UI-BLUEPRINT-CARD: Each blueprint in the blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG) is shown as a **card**. All cards share a uniform size. A card contains, from top to bottom:
|
||||
- The **blueprint name**.
|
||||
- A **contents line** summarizing what the blueprint holds: one `<building name> x <count>` entry per building type it contains, comma-separated, in descending count order with ties broken by the order the types appear in the build button bar (REQ-UI-BUILD-BAR). This is the same `x`-count notation as the building multi-selection summary (REQ-UI-MULTI-SELECTION). If the entries do not fit on the line, the line is elided at its end rather than wrapped or shrunk, so every card keeps the same height.
|
||||
- The **total building block cost** of the blueprint (sum of the individual costs of all constituent buildings), shown with the `building_block` item icon to the right of the number in place of a trailing `Blocks` word, exactly as elsewhere in the UI (REQ-UI-BLOCKS-ICON, REQ-UI-BUILD-COST). When no icon file exists for `building_block`, the cost is shown as the bare number.
|
||||
- A **delete icon ("×")** in the card's bottom-right corner (REQ-UI-BLUEPRINT-DELETE).
|
||||
|
||||
- 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. 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. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint.
|
||||
A card shows no preview of the blueprint's layout.
|
||||
|
||||
Clicking anywhere on an enabled card other than its delete icon closes the dialog and enters blueprint placement mode for that blueprint (REQ-UI-BLUEPRINT-MODE). A card is disabled when the player cannot currently afford its total cost; a disabled card is rendered dimmed and clicking it does nothing (consistent with REQ-UI-BUILD-DISABLED), and neither closes the dialog nor enters placement mode. The delete icon is always enabled regardless of whether the player can afford the blueprint.
|
||||
|
||||
- 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. 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: 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. If both conditions are met, a construction site is added to the build queue for each 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-DELETE: Clicking the delete icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited.
|
||||
- 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.
|
||||
|
||||
- REQ-UI-BLUEPRINT-SAVE: On application shutdown, all current blueprints are serialized to a file named `blueprints.toml` located in the same directory as the application executable. The TOML structure matches REQ-UI-BLUEPRINT-STORAGE. Write errors are silently ignored on shutdown (no button, no dialog).
|
||||
|
||||
|
||||
11
src/lib/eventsystem/event/BlueprintSaveRequestedEvent.h
Normal file
11
src/lib/eventsystem/event/BlueprintSaveRequestedEvent.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
// Ctrl+C: save the current building selection as a named blueprint
|
||||
// (REQ-UI-BLUEPRINT-CREATE, REQ-UI-HOTKEYS). MainWindow owns the modal flow, because
|
||||
// it is the only widget that can pause the game and raise the dim overlay; whether
|
||||
// anything placeable is selected is decided there, not by the key handler.
|
||||
class BlueprintSaveRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
11
src/lib/eventsystem/event/BlueprintSelectionRequestedEvent.h
Normal file
11
src/lib/eventsystem/event/BlueprintSelectionRequestedEvent.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
// Ctrl+V: open the blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG,
|
||||
// REQ-UI-HOTKEYS). The Ctrl+C path does not go through this event -- it opens the
|
||||
// dialog directly so one pause scope and one dim scope span both dialogs
|
||||
// (REQ-UI-MODAL-DIM).
|
||||
class BlueprintSelectionRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
@@ -10,11 +10,14 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameResetEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WinEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ArtifactCountChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/UnlockedBuildingsChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuilderModeExitedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintModeExitedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSaveRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/PanDirectionChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/PauseToggleRequestedEvent.h
|
||||
@@ -29,7 +32,8 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructModeToggleRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ExitBlueprintModeRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TemporaryBlueprintRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TemporaryBlueprintCaptureRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TemporaryBlueprintPlaceRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SpeedChangeRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/LayoutDialogRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionRequestedEvent.h
|
||||
|
||||
13
src/lib/eventsystem/event/GameResetEvent.h
Normal file
13
src/lib/eventsystem/event/GameResetEvent.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
// Emitted once per restart, after a queued ResetCommand has been applied and the view has
|
||||
// been reset for the new run (REQ-UI-GAME-MENU). It lets presentation state that does not
|
||||
// live in the simulation -- e.g. the temporary blueprint, REQ-UI-BLUEPRINT-TEMP -- be
|
||||
// dropped for the fresh run. Sent from GameWorldView::resetForNewGame(), which is where a
|
||||
// restart actually lands: the escape menu, game over, and win dialogs only enqueue the
|
||||
// command.
|
||||
class GameResetEvent : public Event
|
||||
{
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
// Emitted when the player presses C to capture a temporary blueprint (REQ-UI-BLUEPRINT-TEMP).
|
||||
// Carries no payload: the blueprint is built from the current selection by the receiver.
|
||||
class TemporaryBlueprintCaptureRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
// Emitted when the player presses V to re-enter placement mode for the temporary blueprint
|
||||
// captured with C (REQ-UI-BLUEPRINT-TEMP). Carries no payload: the receiver holds the
|
||||
// blueprint, and nothing is captured from the current selection.
|
||||
class TemporaryBlueprintPlaceRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
// Emitted when the player presses the temporary-blueprint hotkey (REQ-UI-BLUEPRINT-TEMP).
|
||||
// Carries no payload: the blueprint is built from the current selection by the receiver.
|
||||
class TemporaryBlueprintRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
@@ -3,10 +3,14 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
|
||||
#include "BeltSystem.h"
|
||||
#include "Building.h"
|
||||
#include "BuildingsConfig.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "DisplayName.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
namespace
|
||||
@@ -49,6 +53,21 @@ std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, Building
|
||||
resolved.bodyCells = building ? &building->bodyCells : &site->bodyCells;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Position of a building type in buildings.toml, which is the order the build button
|
||||
// bar lays out its buttons (REQ-UI-BUILD-BAR) and so the tie-break order for a
|
||||
// blueprint's contents line. A type with no config entry sorts after every known one.
|
||||
std::size_t configOrderIndex(BuildingType type, const BuildingsConfig& buildings)
|
||||
{
|
||||
for (std::size_t i = 0; i < buildings.buildings.size(); ++i)
|
||||
{
|
||||
if (buildings.buildings[i].type == type)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return buildings.buildings.size();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
|
||||
@@ -138,9 +157,9 @@ Blueprint captureBlueprintFromSelection(const Simulation& sim,
|
||||
building.type = e.type;
|
||||
building.rotation = e.rotation;
|
||||
building.offset = e.anchor - center;
|
||||
// Recipe / schematic / layout / splitter-filter capture is shared with the
|
||||
// copy-settings gesture (REQ-BLD-COPY-CONFIG) via readBuildingConfig, which
|
||||
// handles operational buildings and construction sites alike.
|
||||
// Recipe / schematic / layout / splitter-filter capture goes through
|
||||
// readBuildingConfig, which handles operational buildings and construction
|
||||
// sites alike.
|
||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, e.id);
|
||||
if (config.has_value())
|
||||
{
|
||||
@@ -166,3 +185,56 @@ bool selectionHasPlaceableBuilding(const Simulation& sim,
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<BlueprintContentEntry> summarizeBlueprintContents(
|
||||
const Blueprint& blueprint, const BuildingsConfig& buildings)
|
||||
{
|
||||
std::map<BuildingType, int> counts;
|
||||
for (const BlueprintBuilding& building : blueprint.buildings)
|
||||
{
|
||||
++counts[building.type];
|
||||
}
|
||||
|
||||
// The config index is carried through the sort so the comparator stays a strict
|
||||
// total order; the enum value is the final tie-break, which only two config-less
|
||||
// types could ever reach.
|
||||
struct RankedType
|
||||
{
|
||||
BuildingType type;
|
||||
int count;
|
||||
std::size_t order;
|
||||
};
|
||||
std::vector<RankedType> ranked;
|
||||
ranked.reserve(counts.size());
|
||||
for (const std::pair<const BuildingType, int>& entry : counts)
|
||||
{
|
||||
ranked.push_back({entry.first, entry.second,
|
||||
configOrderIndex(entry.first, buildings)});
|
||||
}
|
||||
std::sort(ranked.begin(), ranked.end(),
|
||||
[](const RankedType& left, const RankedType& right)
|
||||
{
|
||||
if (left.count != right.count) { return left.count > right.count; }
|
||||
if (left.order != right.order) { return left.order < right.order; }
|
||||
return static_cast<int>(left.type) < static_cast<int>(right.type);
|
||||
});
|
||||
|
||||
std::vector<BlueprintContentEntry> summary;
|
||||
summary.reserve(ranked.size());
|
||||
for (const RankedType& entry : ranked)
|
||||
{
|
||||
summary.push_back({toDisplayName(buildingTypeId(entry.type)), entry.count});
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
int computeBlueprintCost(const Blueprint& blueprint, const BuildingsConfig& buildings)
|
||||
{
|
||||
int total = 0;
|
||||
for (const BlueprintBuilding& building : blueprint.buildings)
|
||||
{
|
||||
const BuildingDef* def = buildings.findBuildingDef(building.type);
|
||||
if (def) { total += def->cost; }
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@
|
||||
#include "ShipLayout.h"
|
||||
|
||||
class Simulation;
|
||||
struct BuildingsConfig;
|
||||
|
||||
// The user-configurable settings of a single building or construction site: the
|
||||
// selected recipe / ship schematic, the shipyard module layout, and (for
|
||||
// splitters) the two output filters. Shared by the copy-settings gesture
|
||||
// (REQ-BLD-COPY-CONFIG) and blueprint capture (REQ-UI-BLUEPRINT-STORAGE).
|
||||
// splitters) the two output filters. This is what blueprint capture records per
|
||||
// constituent building (REQ-UI-BLUEPRINT-STORAGE).
|
||||
struct BuildingConfig
|
||||
{
|
||||
BuildingType type = BuildingType::Miner;
|
||||
@@ -49,6 +50,29 @@ Blueprint captureBlueprintFromSelection(const Simulation& sim,
|
||||
const std::vector<BuildingId>& selectedIds);
|
||||
|
||||
// True if any selected id refers to a player-placeable building or construction site
|
||||
// (the enable condition for the Create Blueprint button, REQ-UI-BLUEPRINT-CREATE).
|
||||
// (the condition under which Ctrl+C opens the blueprint save dialog,
|
||||
// REQ-UI-BLUEPRINT-CREATE).
|
||||
bool selectionHasPlaceableBuilding(const Simulation& sim,
|
||||
const std::vector<BuildingId>& selectedIds);
|
||||
|
||||
// One "<building name> x <count>" entry of a blueprint card's contents line
|
||||
// (REQ-UI-BLUEPRINT-CARD).
|
||||
struct BlueprintContentEntry
|
||||
{
|
||||
std::string buildingName;
|
||||
int count;
|
||||
};
|
||||
|
||||
// Summarizes what a blueprint holds: one entry per building type it contains, ordered
|
||||
// by descending count with ties broken by the order the types appear in buildings.toml
|
||||
// (which is the order of the build button bar, REQ-UI-BUILD-BAR). Building types absent
|
||||
// from the config sort last. Derived display data for the blueprint card, kept here so
|
||||
// it is unit-testable rather than buried in the dialog (REQ-UI-BLUEPRINT-CARD).
|
||||
std::vector<BlueprintContentEntry> summarizeBlueprintContents(
|
||||
const Blueprint& blueprint, const BuildingsConfig& buildings);
|
||||
|
||||
// Plain sum of the placement cost of every building in the blueprint
|
||||
// (REQ-UI-BLUEPRINT-CARD). Distinct from the total charged on placement
|
||||
// (REQ-UI-BLUEPRINT-PLACE), which additionally excludes locked building types and
|
||||
// rotate-in-place targets; see GameWorldView's placement path.
|
||||
int computeBlueprintCost(const Blueprint& blueprint, const BuildingsConfig& buildings);
|
||||
|
||||
@@ -56,7 +56,7 @@ static Rotation rotCCW(Rotation r)
|
||||
return Rotation::East;
|
||||
}
|
||||
|
||||
// Mirror of BlueprintPanel::createBlueprintFromSelection: given per-building
|
||||
// Mirror of BlueprintLibrary::createBlueprintFromSelection: given per-building
|
||||
// (anchor, bodyCells, type, rotation), compute Blueprint with floor-division
|
||||
// bounding-box center and per-building tile offsets.
|
||||
struct BuildingSpec
|
||||
@@ -149,7 +149,7 @@ static void applyRotationCCW(Blueprint& bp, const GameConfig& cfg)
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors BlueprintPanel::createBlueprintFromSelection's player-placeable filter:
|
||||
// Mirrors BlueprintLibrary::createBlueprintFromSelection's player-placeable filter:
|
||||
// building types absent from buildings.toml (HQ, stations) or with playerPlaceable=false
|
||||
// are silently excluded before the bounding-box center and offsets are computed.
|
||||
static Blueprint buildBlueprintFiltered(const std::vector<BuildingSpec>& specs,
|
||||
|
||||
@@ -82,7 +82,7 @@ TEST_CASE("Only one mode is active at a time", "[buildmode]")
|
||||
TEST_CASE("Entering builder mode announces that blueprint mode ended", "[buildmode]")
|
||||
{
|
||||
// Regression: entering builder mode used to drop the blueprint silently, so the
|
||||
// blueprint panel kept its button highlighted for a mode that was over.
|
||||
// blueprint library kept tracking an active blueprint for a mode that was over.
|
||||
BuildModeController controller;
|
||||
controller.enterBlueprintMode(makeBlueprint());
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "TestConfig.h"
|
||||
|
||||
// readBuildingConfig underpins the copy-settings gesture (REQ-BLD-COPY-CONFIG):
|
||||
// it extracts a building's recipe / schematic / layout / splitter filters so they
|
||||
// can be stamped onto a same-type building. It reads operational buildings and
|
||||
// construction sites alike.
|
||||
// readBuildingConfig underpins blueprint capture (REQ-UI-BLUEPRINT-STORAGE): it
|
||||
// extracts a building's recipe / schematic / layout / splitter filters so they can be
|
||||
// stored in the blueprint and reapplied on placement. It reads operational buildings
|
||||
// and construction sites alike.
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -60,7 +60,7 @@ const ShipDef* findAvailableSchematic(const GameConfig& cfg)
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]")
|
||||
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
Simulation sim(loadTestConfig(), 7);
|
||||
@@ -78,7 +78,7 @@ TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]"
|
||||
}
|
||||
|
||||
TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
|
||||
"[copyconfig]")
|
||||
"[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
Simulation sim(loadTestConfig(), 7);
|
||||
@@ -93,7 +93,7 @@ TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
|
||||
}
|
||||
|
||||
TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
|
||||
"[copyconfig]")
|
||||
"[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
Simulation sim(loadTestConfig(), 7);
|
||||
@@ -113,7 +113,7 @@ TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
|
||||
CHECK(config->shipLayout.has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
|
||||
TEST_CASE("readBuildingConfig reads a queued construction site", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
Simulation sim(loadTestConfig(), 7);
|
||||
@@ -134,8 +134,105 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
|
||||
CHECK(*config->recipeId == "mine_iron_ore");
|
||||
}
|
||||
|
||||
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[copyconfig]")
|
||||
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[blueprint]")
|
||||
{
|
||||
Simulation sim(loadTestConfig(), 7);
|
||||
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
|
||||
}
|
||||
|
||||
// The derived facts a blueprint card shows (REQ-UI-BLUEPRINT-CARD). They live here
|
||||
// rather than in the dialog so they can be tested: src/ui is off the test include path.
|
||||
|
||||
namespace
|
||||
{
|
||||
Blueprint makeBlueprint(const std::vector<BuildingType>& types)
|
||||
{
|
||||
Blueprint blueprint;
|
||||
for (const BuildingType type : types)
|
||||
{
|
||||
BlueprintBuilding building;
|
||||
building.type = type;
|
||||
building.rotation = Rotation::East;
|
||||
blueprint.buildings.push_back(building);
|
||||
}
|
||||
return blueprint;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("computeBlueprintCost sums the constituent placement costs", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
|
||||
// belt 2 + belt 2 + miner 15.
|
||||
const Blueprint blueprint = makeBlueprint(
|
||||
{BuildingType::Belt, BuildingType::Belt, BuildingType::Miner});
|
||||
CHECK(computeBlueprintCost(blueprint, cfg.buildings) == 19);
|
||||
}
|
||||
|
||||
TEST_CASE("computeBlueprintCost ignores types absent from buildings.toml", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
REQUIRE(cfg.buildings.findBuildingDef(BuildingType::Hq) == nullptr);
|
||||
|
||||
const Blueprint blueprint = makeBlueprint({BuildingType::Belt, BuildingType::Hq});
|
||||
CHECK(computeBlueprintCost(blueprint, cfg.buildings) == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("computeBlueprintCost is zero for an empty blueprint", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
CHECK(computeBlueprintCost(Blueprint{}, cfg.buildings) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("summarizeBlueprintContents orders by descending count", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
|
||||
const Blueprint blueprint = makeBlueprint(
|
||||
{BuildingType::Belt, BuildingType::Smelter, BuildingType::Miner,
|
||||
BuildingType::Miner, BuildingType::Smelter, BuildingType::Miner});
|
||||
|
||||
const std::vector<BlueprintContentEntry> summary =
|
||||
summarizeBlueprintContents(blueprint, cfg.buildings);
|
||||
REQUIRE(summary.size() == 3);
|
||||
CHECK(summary[0].buildingName == "Miner");
|
||||
CHECK(summary[0].count == 3);
|
||||
CHECK(summary[1].buildingName == "Smelter");
|
||||
CHECK(summary[1].count == 2);
|
||||
CHECK(summary[2].buildingName == "Belt");
|
||||
CHECK(summary[2].count == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("summarizeBlueprintContents breaks ties in buildings.toml order", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
|
||||
// Miner is declared after Belt in buildings.toml, which is the order the build
|
||||
// button bar lays its buttons out, so Belt wins the tie despite the equal counts
|
||||
// and despite sorting later by enum value.
|
||||
const Blueprint blueprint = makeBlueprint(
|
||||
{BuildingType::Miner, BuildingType::Belt, BuildingType::Miner, BuildingType::Belt});
|
||||
|
||||
const std::vector<BlueprintContentEntry> summary =
|
||||
summarizeBlueprintContents(blueprint, cfg.buildings);
|
||||
REQUIRE(summary.size() == 2);
|
||||
CHECK(summary[0].buildingName == "Belt");
|
||||
CHECK(summary[1].buildingName == "Miner");
|
||||
}
|
||||
|
||||
TEST_CASE("summarizeBlueprintContents spells multi-word ids as display names", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
|
||||
const Blueprint blueprint = makeBlueprint({BuildingType::TunnelEntry});
|
||||
const std::vector<BlueprintContentEntry> summary =
|
||||
summarizeBlueprintContents(blueprint, cfg.buildings);
|
||||
REQUIRE(summary.size() == 1);
|
||||
CHECK(summary[0].buildingName == "Tunnel Entry");
|
||||
}
|
||||
|
||||
TEST_CASE("summarizeBlueprintContents is empty for an empty blueprint", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
CHECK(summarizeBlueprintContents(Blueprint{}, cfg.buildings).empty());
|
||||
}
|
||||
|
||||
201
src/ui/BlueprintLibrary.cpp
Normal file
201
src/ui/BlueprintLibrary.cpp
Normal file
@@ -0,0 +1,201 @@
|
||||
#include "BlueprintLibrary.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
#include "BlueprintPlacementRequestedEvent.h"
|
||||
#include "BlueprintSerializer.h"
|
||||
#include "EventManager.h"
|
||||
#include "ExitBlueprintModeRequestedEvent.h"
|
||||
|
||||
#include "BuildingConfig.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
BlueprintLibrary::BlueprintLibrary(Simulation* sim, const GameConfig* config,
|
||||
QWidget* dialogParent)
|
||||
: m_sim(sim)
|
||||
, m_config(config)
|
||||
, m_dialogParent(dialogParent)
|
||||
{
|
||||
loadFromDisk();
|
||||
registerForEvents();
|
||||
}
|
||||
|
||||
BlueprintLibrary::~BlueprintLibrary()
|
||||
{
|
||||
saveToDisk();
|
||||
unregisterForEvents();
|
||||
}
|
||||
|
||||
bool BlueprintLibrary::getCanCaptureSelection() const
|
||||
{
|
||||
// A construction site counts the same as an operational building (REQ-UI-BLUEPRINT-CREATE).
|
||||
return selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds);
|
||||
}
|
||||
|
||||
void BlueprintLibrary::saveSelectionAs(const QString& name)
|
||||
{
|
||||
Blueprint blueprint = createBlueprintFromSelection();
|
||||
if (blueprint.buildings.empty()) { return; }
|
||||
|
||||
blueprint.name = name;
|
||||
m_blueprints.push_back(std::move(blueprint));
|
||||
}
|
||||
|
||||
const std::vector<Blueprint>& BlueprintLibrary::getBlueprints() const
|
||||
{
|
||||
return m_blueprints;
|
||||
}
|
||||
|
||||
QString BlueprintLibrary::getContentsSummary(int index) const
|
||||
{
|
||||
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return QString(); }
|
||||
|
||||
QStringList entries;
|
||||
for (const BlueprintContentEntry& entry : summarizeBlueprintContents(
|
||||
m_blueprints[static_cast<std::size_t>(index)], m_config->buildings))
|
||||
{
|
||||
// The same "<type> x <count>" notation the building multi-selection summary
|
||||
// uses (REQ-UI-MULTI-SELECTION), which also sidesteps plural forms.
|
||||
entries << QObject::tr("%1 x %2")
|
||||
.arg(QString::fromStdString(entry.buildingName))
|
||||
.arg(entry.count);
|
||||
}
|
||||
return entries.join(QStringLiteral(", "));
|
||||
}
|
||||
|
||||
int BlueprintLibrary::getCost(int index) const
|
||||
{
|
||||
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return 0; }
|
||||
return computeBlueprintCost(m_blueprints[static_cast<std::size_t>(index)],
|
||||
m_config->buildings);
|
||||
}
|
||||
|
||||
bool BlueprintLibrary::getCanAfford(int index) const
|
||||
{
|
||||
return m_sim->getBuildingBlocksStock() >= getCost(index);
|
||||
}
|
||||
|
||||
void BlueprintLibrary::remove(int index)
|
||||
{
|
||||
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return; }
|
||||
|
||||
if (m_activeIndex == index)
|
||||
{
|
||||
m_activeIndex = std::nullopt;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ExitBlueprintModeRequestedEvent>());
|
||||
}
|
||||
else if (m_activeIndex.has_value() && *m_activeIndex > index)
|
||||
{
|
||||
--*m_activeIndex;
|
||||
}
|
||||
m_blueprints.erase(m_blueprints.begin() + index);
|
||||
}
|
||||
|
||||
void BlueprintLibrary::beginPlacement(int index)
|
||||
{
|
||||
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return; }
|
||||
|
||||
m_activeIndex = index;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintPlacementRequestedEvent>(
|
||||
m_blueprints[static_cast<std::size_t>(index)]));
|
||||
}
|
||||
|
||||
void BlueprintLibrary::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||
{
|
||||
m_selectedBuildingIds = event->ids;
|
||||
}
|
||||
|
||||
void BlueprintLibrary::handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*event*/)
|
||||
{
|
||||
// Only the saved-blueprint index is cleared. A temporary blueprint deliberately
|
||||
// survives its placement mode so V can re-enter it (REQ-UI-BLUEPRINT-TEMP).
|
||||
m_activeIndex = std::nullopt;
|
||||
}
|
||||
|
||||
void BlueprintLibrary::handleEvent(
|
||||
std::shared_ptr<const TemporaryBlueprintCaptureRequestedEvent> /*event*/)
|
||||
{
|
||||
// C (REQ-UI-BLUEPRINT-TEMP): capture the current selection and enter placement mode
|
||||
// for it, without naming it, listing it, or persisting it. If nothing player-placeable
|
||||
// is selected, do nothing at all -- in particular, keep the previous temporary
|
||||
// blueprint, which V can still place.
|
||||
Blueprint blueprint = createBlueprintFromSelection();
|
||||
if (blueprint.buildings.empty()) { return; }
|
||||
|
||||
m_temporaryBlueprint = std::move(blueprint);
|
||||
// No saved blueprint is active while a temporary one is being placed.
|
||||
m_activeIndex = std::nullopt;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintPlacementRequestedEvent>(*m_temporaryBlueprint));
|
||||
}
|
||||
|
||||
void BlueprintLibrary::handleEvent(
|
||||
std::shared_ptr<const TemporaryBlueprintPlaceRequestedEvent> /*event*/)
|
||||
{
|
||||
// V (REQ-UI-BLUEPRINT-TEMP): re-enter placement mode for the temporary blueprint,
|
||||
// capturing nothing. With none captured, nothing happens -- no mode is entered and
|
||||
// whichever mode is active is left alone. Affordability is not checked here, matching
|
||||
// C; cost is enforced at placement (REQ-UI-BLUEPRINT-PLACE). The blueprint is copied,
|
||||
// not moved: it stays available for the next V.
|
||||
if (!m_temporaryBlueprint.has_value()) { return; }
|
||||
|
||||
m_activeIndex = std::nullopt;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintPlacementRequestedEvent>(*m_temporaryBlueprint));
|
||||
}
|
||||
|
||||
void BlueprintLibrary::handleEvent(std::shared_ptr<const GameResetEvent> /*event*/)
|
||||
{
|
||||
// A restart begins a new run, and the temporary blueprint belongs to the old one
|
||||
// (REQ-UI-BLUEPRINT-TEMP). The saved blueprints are not run state and stay.
|
||||
m_temporaryBlueprint = std::nullopt;
|
||||
}
|
||||
|
||||
Blueprint BlueprintLibrary::createBlueprintFromSelection() const
|
||||
{
|
||||
// Capture is shared, testable logic in lib/sim: it resolves each selected id as an
|
||||
// operational building or a construction site alike (REQ-UI-BLUEPRINT-CREATE,
|
||||
// REQ-UI-BLUEPRINT-STORAGE).
|
||||
return captureBlueprintFromSelection(*m_sim, m_selectedBuildingIds);
|
||||
}
|
||||
|
||||
void BlueprintLibrary::loadFromDisk()
|
||||
{
|
||||
// Load at startup (REQ-UI-BLUEPRINT-LOAD). Missing file: start empty, no error.
|
||||
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; }
|
||||
try
|
||||
{
|
||||
m_blueprints = BlueprintSerializer::deserialize(file.readAll().toStdString());
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
QMessageBox::critical(m_dialogParent, QObject::tr("Load Failed"),
|
||||
QObject::tr("Failed to load blueprints:\n%1").arg(e.what()));
|
||||
m_blueprints.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void BlueprintLibrary::saveToDisk() const
|
||||
{
|
||||
// Persist on shutdown; write errors are silently ignored (REQ-UI-BLUEPRINT-SAVE).
|
||||
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; }
|
||||
try
|
||||
{
|
||||
const std::string content = BlueprintSerializer::serialize(m_blueprints);
|
||||
file.write(QByteArray::fromStdString(content));
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
101
src/ui/BlueprintLibrary.h
Normal file
101
src/ui/BlueprintLibrary.h
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "Blueprint.h"
|
||||
#include "BlueprintModeExitedEvent.h"
|
||||
#include "BuildingId.h"
|
||||
#include "EventHandler.h"
|
||||
#include "GameConfig.h"
|
||||
#include "GameResetEvent.h"
|
||||
#include "SelectionChangedEvent.h"
|
||||
#include "TemporaryBlueprintCaptureRequestedEvent.h"
|
||||
#include "TemporaryBlueprintPlaceRequestedEvent.h"
|
||||
|
||||
class Simulation;
|
||||
class QWidget;
|
||||
|
||||
// The player's saved blueprints: the list itself, its persistence (REQ-UI-BLUEPRINT-SAVE,
|
||||
// REQ-UI-BLUEPRINT-LOAD), capture from the current selection (REQ-UI-BLUEPRINT-CREATE,
|
||||
// REQ-UI-BLUEPRINT-TEMP), and entry into blueprint placement mode.
|
||||
//
|
||||
// Deliberately not a widget: blueprints have no permanent place on screen any more
|
||||
// (REQ-UI-BLUEPRINT-DIALOG), and the modal dialogs that present them must be driven from
|
||||
// MainWindow, the only widget that can pause the game (ModalPauseScope) and raise the dim
|
||||
// overlay. This class is the model those dialogs read and mutate.
|
||||
class BlueprintLibrary
|
||||
: public CombinedEventHandler<SelectionChangedEvent,
|
||||
BlueprintModeExitedEvent,
|
||||
TemporaryBlueprintCaptureRequestedEvent,
|
||||
TemporaryBlueprintPlaceRequestedEvent,
|
||||
GameResetEvent>
|
||||
{
|
||||
public:
|
||||
// dialogParent parents the load-failure message box (REQ-UI-BLUEPRINT-LOAD) and is
|
||||
// not owned.
|
||||
BlueprintLibrary(Simulation* sim, const GameConfig* config, QWidget* dialogParent);
|
||||
~BlueprintLibrary();
|
||||
|
||||
// True when the current selection holds at least one player-placeable building or
|
||||
// construction site -- the condition under which Ctrl+C has any effect
|
||||
// (REQ-UI-BLUEPRINT-CREATE).
|
||||
bool getCanCaptureSelection() const;
|
||||
|
||||
// Captures the current selection under the given name and appends it to the list.
|
||||
// Silently does nothing when nothing player-placeable is selected.
|
||||
void saveSelectionAs(const QString& name);
|
||||
|
||||
const std::vector<Blueprint>& getBlueprints() const;
|
||||
|
||||
// The blueprint's contents line, ready for its card: one "<name> x <count>" entry
|
||||
// per building type, comma-separated, ordered by summarizeBlueprintContents
|
||||
// (REQ-UI-BLUEPRINT-CARD).
|
||||
QString getContentsSummary(int index) const;
|
||||
|
||||
// Sum of the constituent buildings' placement costs (REQ-UI-BLUEPRINT-CARD).
|
||||
int getCost(int index) const;
|
||||
|
||||
// Whether the player can currently afford the blueprint's total cost, which is what
|
||||
// enables or greys out its card (REQ-UI-BLUEPRINT-CARD).
|
||||
bool getCanAfford(int index) const;
|
||||
|
||||
// Removes the blueprint, exiting blueprint placement mode if it was the active one
|
||||
// (REQ-UI-BLUEPRINT-DELETE).
|
||||
void remove(int index);
|
||||
|
||||
// Enters blueprint placement mode for the blueprint (REQ-UI-BLUEPRINT-MODE).
|
||||
void beginPlacement(int index);
|
||||
|
||||
private:
|
||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> event) override;
|
||||
void handleEvent(
|
||||
std::shared_ptr<const TemporaryBlueprintCaptureRequestedEvent> event) override;
|
||||
void handleEvent(
|
||||
std::shared_ptr<const TemporaryBlueprintPlaceRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const GameResetEvent> event) override;
|
||||
|
||||
Blueprint createBlueprintFromSelection() const;
|
||||
void loadFromDisk();
|
||||
void saveToDisk() const;
|
||||
|
||||
// The simulation is the single source of truth for the block stock and the
|
||||
// selection; the change events are only refresh signals.
|
||||
Simulation* m_sim;
|
||||
const GameConfig* m_config;
|
||||
QWidget* m_dialogParent;
|
||||
std::vector<BuildingId> m_selectedBuildingIds;
|
||||
std::vector<Blueprint> m_blueprints;
|
||||
// Index of the blueprint currently in placement mode, so deleting it can exit that
|
||||
// mode (REQ-UI-BLUEPRINT-DELETE). nullopt = no saved blueprint is being placed.
|
||||
std::optional<int> m_activeIndex;
|
||||
// The one unnamed blueprint captured with C and re-placed with V
|
||||
// (REQ-UI-BLUEPRINT-TEMP). Deliberately kept out of m_blueprints: it is never named,
|
||||
// never listed in the selection dialog, and never written to blueprints.toml. It
|
||||
// outlives its placement mode, so V can re-enter it. nullopt = none captured since
|
||||
// startup or the last restart.
|
||||
std::optional<Blueprint> m_temporaryBlueprint;
|
||||
};
|
||||
@@ -1,281 +0,0 @@
|
||||
#include "BlueprintPanel.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QHBoxLayout>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BlueprintPlacementRequestedEvent.h"
|
||||
#include "BlueprintSerializer.h"
|
||||
#include "BuildingBlocksChangedEvent.h"
|
||||
#include "EventManager.h"
|
||||
#include "ExitBlueprintModeRequestedEvent.h"
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingConfig.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
BlueprintPanel::BlueprintPanel(Simulation* sim, const GameConfig* config, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_sim(sim)
|
||||
, m_config(config)
|
||||
{
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(4, 4, 4, 4);
|
||||
layout->setSpacing(4);
|
||||
|
||||
m_createBtn = new QPushButton(tr("Create Blueprint"), this);
|
||||
m_createBtn->setFixedHeight(48);
|
||||
m_createBtn->setEnabled(false);
|
||||
layout->addWidget(m_createBtn);
|
||||
|
||||
QScrollArea* scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
m_buttonsContainer = new QWidget(scrollArea);
|
||||
m_buttonsLayout = new QVBoxLayout(m_buttonsContainer);
|
||||
m_buttonsLayout->setContentsMargins(0, 0, 0, 0);
|
||||
m_buttonsLayout->setSpacing(4);
|
||||
m_buttonsLayout->addStretch();
|
||||
|
||||
scrollArea->setWidget(m_buttonsContainer);
|
||||
layout->addWidget(scrollArea, 1);
|
||||
|
||||
connect(m_createBtn, &QPushButton::clicked, this, &BlueprintPanel::onCreateClicked);
|
||||
|
||||
loadFromDisk();
|
||||
rebuildButtons();
|
||||
|
||||
registerForEvents();
|
||||
}
|
||||
|
||||
BlueprintPanel::~BlueprintPanel()
|
||||
{
|
||||
saveToDisk();
|
||||
unregisterForEvents();
|
||||
}
|
||||
|
||||
void BlueprintPanel::onSelectionChanged(const std::vector<BuildingId>& ids)
|
||||
{
|
||||
m_selectedBuildingIds = ids;
|
||||
refreshButtonStates();
|
||||
}
|
||||
|
||||
void BlueprintPanel::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> /*event*/)
|
||||
{
|
||||
refreshButtonStates();
|
||||
}
|
||||
|
||||
void BlueprintPanel::clearActiveBlueprintButton()
|
||||
{
|
||||
if (m_activeIndex.has_value() && *m_activeIndex < static_cast<int>(m_blueprintButtons.size()))
|
||||
{
|
||||
m_blueprintButtons[static_cast<std::size_t>(*m_activeIndex)]->setChecked(false);
|
||||
}
|
||||
m_activeIndex = std::nullopt;
|
||||
refreshButtonStates();
|
||||
}
|
||||
|
||||
void BlueprintPanel::onCreateClicked()
|
||||
{
|
||||
if (m_selectedBuildingIds.empty()) { return; }
|
||||
|
||||
Blueprint bp = createBlueprintFromSelection();
|
||||
if (bp.buildings.empty()) { return; }
|
||||
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
this, tr("Create Blueprint"), tr("Blueprint name:"), QLineEdit::Normal, QString(), &ok);
|
||||
if (!ok || name.trimmed().isEmpty()) { return; }
|
||||
|
||||
bp.name = name.trimmed();
|
||||
m_blueprints.push_back(bp);
|
||||
rebuildButtons();
|
||||
}
|
||||
|
||||
void BlueprintPanel::onDeleteBlueprintClicked(int index)
|
||||
{
|
||||
if (m_activeIndex == index)
|
||||
{
|
||||
m_activeIndex = std::nullopt;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ExitBlueprintModeRequestedEvent>());
|
||||
}
|
||||
else if (m_activeIndex.has_value() && *m_activeIndex > index)
|
||||
{
|
||||
--*m_activeIndex;
|
||||
}
|
||||
m_blueprints.erase(m_blueprints.begin() + index);
|
||||
rebuildButtons();
|
||||
}
|
||||
|
||||
void BlueprintPanel::onBlueprintButtonClicked(int index)
|
||||
{
|
||||
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return; }
|
||||
|
||||
if (m_activeIndex == index)
|
||||
{
|
||||
clearActiveBlueprintButton();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ExitBlueprintModeRequestedEvent>());
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_activeIndex.has_value() && *m_activeIndex < static_cast<int>(m_blueprintButtons.size()))
|
||||
{
|
||||
m_blueprintButtons[static_cast<std::size_t>(*m_activeIndex)]->setChecked(false);
|
||||
}
|
||||
|
||||
m_activeIndex = index;
|
||||
m_blueprintButtons[static_cast<std::size_t>(index)]->setChecked(true);
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintPlacementRequestedEvent>(m_blueprints[static_cast<std::size_t>(index)]));
|
||||
}
|
||||
|
||||
Blueprint BlueprintPanel::createBlueprintFromSelection() const
|
||||
{
|
||||
// Capture is shared, testable logic in lib/sim: it resolves each selected id as an
|
||||
// operational building or a construction site alike (REQ-UI-BLUEPRINT-CREATE,
|
||||
// REQ-UI-BLUEPRINT-STORAGE).
|
||||
return captureBlueprintFromSelection(*m_sim, m_selectedBuildingIds);
|
||||
}
|
||||
|
||||
int BlueprintPanel::computeBlueprintCost(const Blueprint& bp) const
|
||||
{
|
||||
int total = 0;
|
||||
for (const BlueprintBuilding& bb : bp.buildings)
|
||||
{
|
||||
const BuildingDef* def = m_config->buildings.findBuildingDef(bb.type);
|
||||
if (def) { total += def->cost; }
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void BlueprintPanel::rebuildButtons()
|
||||
{
|
||||
while (m_buttonsLayout->count() > 1)
|
||||
{
|
||||
QLayoutItem* item = m_buttonsLayout->takeAt(0);
|
||||
if (item->widget()) { delete item->widget(); }
|
||||
delete item;
|
||||
}
|
||||
m_blueprintButtons.clear();
|
||||
|
||||
for (int i = 0; i < static_cast<int>(m_blueprints.size()); ++i)
|
||||
{
|
||||
const Blueprint& bp = m_blueprints[static_cast<std::size_t>(i)];
|
||||
const int cost = computeBlueprintCost(bp);
|
||||
const QString label = bp.name + "\n" + tr("%1 Building Blocks").arg(cost);
|
||||
|
||||
QWidget* row = new QWidget(m_buttonsContainer);
|
||||
QHBoxLayout* rowLayout = new QHBoxLayout(row);
|
||||
rowLayout->setContentsMargins(0, 0, 0, 0);
|
||||
rowLayout->setSpacing(4);
|
||||
|
||||
QPushButton* btn = new QPushButton(label, row);
|
||||
btn->setCheckable(true);
|
||||
btn->setFixedHeight(48);
|
||||
|
||||
QPushButton* delBtn = new QPushButton("\xc3\x97", row);
|
||||
delBtn->setFixedWidth(28);
|
||||
delBtn->setFixedHeight(48);
|
||||
|
||||
rowLayout->addWidget(btn, 1);
|
||||
rowLayout->addWidget(delBtn, 0);
|
||||
|
||||
m_buttonsLayout->insertWidget(i, row);
|
||||
|
||||
const int capturedIndex = i;
|
||||
connect(btn, &QPushButton::clicked, this, [this, capturedIndex]() {
|
||||
onBlueprintButtonClicked(capturedIndex);
|
||||
});
|
||||
connect(delBtn, &QPushButton::clicked, this, [this, capturedIndex]() {
|
||||
onDeleteBlueprintClicked(capturedIndex);
|
||||
});
|
||||
|
||||
m_blueprintButtons.push_back(btn);
|
||||
}
|
||||
|
||||
refreshButtonStates();
|
||||
}
|
||||
|
||||
void BlueprintPanel::saveToDisk() const
|
||||
{
|
||||
// Persist on shutdown; write errors are silently ignored (REQ-UI-BLUEPRINT-SAVE).
|
||||
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; }
|
||||
try
|
||||
{
|
||||
const std::string content = BlueprintSerializer::serialize(m_blueprints);
|
||||
file.write(QByteArray::fromStdString(content));
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
|
||||
void BlueprintPanel::loadFromDisk()
|
||||
{
|
||||
// Load at startup (REQ-UI-BLUEPRINT-LOAD). Missing file: start empty, no error.
|
||||
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; }
|
||||
try
|
||||
{
|
||||
m_blueprints = BlueprintSerializer::deserialize(file.readAll().toStdString());
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Load Failed"),
|
||||
tr("Failed to load blueprints:\n%1").arg(e.what()));
|
||||
m_blueprints.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void BlueprintPanel::refreshButtonStates()
|
||||
{
|
||||
// A construction site counts the same as an operational building (REQ-UI-BLUEPRINT-CREATE).
|
||||
m_createBtn->setEnabled(selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds));
|
||||
|
||||
const int blocks = m_sim->getBuildingBlocksStock();
|
||||
|
||||
for (int i = 0; i < static_cast<int>(m_blueprintButtons.size()); ++i)
|
||||
{
|
||||
const int cost = computeBlueprintCost(m_blueprints[static_cast<std::size_t>(i)]);
|
||||
const bool canAfford = blocks >= cost;
|
||||
m_blueprintButtons[static_cast<std::size_t>(i)]->setEnabled(
|
||||
canAfford || m_activeIndex == i);
|
||||
}
|
||||
}
|
||||
|
||||
void BlueprintPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||
{
|
||||
onSelectionChanged(event->ids);
|
||||
}
|
||||
|
||||
void BlueprintPanel::handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*event*/)
|
||||
{
|
||||
clearActiveBlueprintButton();
|
||||
}
|
||||
|
||||
void BlueprintPanel::handleEvent(std::shared_ptr<const TemporaryBlueprintRequestedEvent> /*event*/)
|
||||
{
|
||||
// Temporary blueprint (REQ-UI-BLUEPRINT-TEMP): build from the current selection and
|
||||
// enter placement mode without adding it to the list or persisting it. If nothing
|
||||
// player-placeable is selected, do nothing.
|
||||
Blueprint bp = createBlueprintFromSelection();
|
||||
if (bp.buildings.empty()) { return; }
|
||||
|
||||
// No saved blueprint is active while a temporary one is being placed.
|
||||
clearActiveBlueprintButton();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintPlacementRequestedEvent>(std::move(bp)));
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "Blueprint.h"
|
||||
#include "BlueprintModeExitedEvent.h"
|
||||
#include "BuildingBlocksChangedEvent.h"
|
||||
#include "BuildingId.h"
|
||||
#include "EventHandler.h"
|
||||
#include "GameConfig.h"
|
||||
#include "SelectionChangedEvent.h"
|
||||
#include "TemporaryBlueprintRequestedEvent.h"
|
||||
#include "Tick.h"
|
||||
|
||||
class Simulation;
|
||||
class QPushButton;
|
||||
class QScrollArea;
|
||||
class QVBoxLayout;
|
||||
|
||||
class BlueprintPanel : public QWidget,
|
||||
public CombinedEventHandler<BuildingBlocksChangedEvent,
|
||||
SelectionChangedEvent,
|
||||
BlueprintModeExitedEvent,
|
||||
TemporaryBlueprintRequestedEvent>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
BlueprintPanel(Simulation* sim, const GameConfig* config, QWidget* parent = nullptr);
|
||||
~BlueprintPanel() override;
|
||||
|
||||
private:
|
||||
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const TemporaryBlueprintRequestedEvent> event) override;
|
||||
|
||||
private slots:
|
||||
void onCreateClicked();
|
||||
void onDeleteBlueprintClicked(int index);
|
||||
void onBlueprintButtonClicked(int index);
|
||||
|
||||
private:
|
||||
void onSelectionChanged(const std::vector<BuildingId>& ids);
|
||||
void clearActiveBlueprintButton();
|
||||
Blueprint createBlueprintFromSelection() const;
|
||||
int computeBlueprintCost(const Blueprint& bp) const;
|
||||
void rebuildButtons();
|
||||
void refreshButtonStates();
|
||||
void loadFromDisk();
|
||||
void saveToDisk() const;
|
||||
|
||||
// The simulation is the single source of truth for the block stock; the
|
||||
// change event is only a refresh signal.
|
||||
Simulation* m_sim;
|
||||
const GameConfig* m_config;
|
||||
std::vector<BuildingId> m_selectedBuildingIds;
|
||||
std::optional<int> m_activeIndex; // nullopt = no blueprint selected
|
||||
std::vector<Blueprint> m_blueprints;
|
||||
std::vector<QPushButton*> m_blueprintButtons;
|
||||
QPushButton* m_createBtn;
|
||||
QWidget* m_buttonsContainer;
|
||||
QVBoxLayout* m_buttonsLayout;
|
||||
};
|
||||
333
src/ui/BlueprintSelectionDialog.cpp
Normal file
333
src/ui/BlueprintSelectionDialog.cpp
Normal file
@@ -0,0 +1,333 @@
|
||||
#include "BlueprintSelectionDialog.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include <QChar>
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
#include <QLayoutItem>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QPixmap>
|
||||
#include <QPoint>
|
||||
#include <QPushButton>
|
||||
#include <QRect>
|
||||
#include <QScrollArea>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QStyle>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Blueprint.h"
|
||||
#include "BlueprintLibrary.h"
|
||||
#include "IconCaption.h"
|
||||
#include "ItemIconCache.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Fixed at two, per REQ-UI-BLUEPRINT-DIALOG; the grid scrolls rather than reflowing.
|
||||
const int kGridColumnCount = 2;
|
||||
|
||||
// Card geometry. Every card is the same size so the grid stays even
|
||||
// (REQ-UI-BLUEPRINT-CARD); the width is chosen to fit a name and a short contents
|
||||
// line, and the height follows from three text rows.
|
||||
const int kCardWidthPx = 220;
|
||||
const int kCardPaddingPx = 10;
|
||||
const int kCardRowGapPx = 4;
|
||||
|
||||
// Side length of the per-card delete icon and of the dialog's close button.
|
||||
const int kSmallButtonSizePx = 22;
|
||||
|
||||
// Spacing between cards, and the dialog's own content margin.
|
||||
const int kSpacingPx = 8;
|
||||
|
||||
// Card rows visible before the grid scrolls. The half row is deliberate: a cut-off
|
||||
// card reads as "there is more below" at a glance.
|
||||
const double kVisibleRowCount = 2.5;
|
||||
|
||||
// U+00D7 MULTIPLICATION SIGN, the close and delete glyph. Written as a code point
|
||||
// because the sources are not guaranteed to be read as UTF-8 by every compiler.
|
||||
const QChar kCrossGlyph(0x00D7);
|
||||
|
||||
// Inner size of a card's composed face: the name row, the contents row, and the
|
||||
// cost row, plus the gap above the cost.
|
||||
QSize getCardFaceSize(const QFont& font)
|
||||
{
|
||||
const int rowHeight = QFontMetrics(font).height();
|
||||
return QSize(kCardWidthPx - 2 * kCardPaddingPx, rowHeight * 3 + kCardRowGapPx);
|
||||
}
|
||||
|
||||
QSize getCardSize(const QFont& font)
|
||||
{
|
||||
const QSize faceSize = getCardFaceSize(font);
|
||||
return QSize(faceSize.width() + 2 * kCardPaddingPx,
|
||||
faceSize.height() + 2 * kCardPaddingPx);
|
||||
}
|
||||
|
||||
// One card face: the blueprint name, its contents line below in a dimmed color,
|
||||
// and the cost with its block icon at the bottom (REQ-UI-BLUEPRINT-CARD). The three
|
||||
// are composed into a single pixmap because a QPushButton holds only one icon --
|
||||
// the same device the build button faces use.
|
||||
QPixmap composeCardFace(const QString& name, const QString& contents,
|
||||
const QPixmap& cost, const QFont& font,
|
||||
const QColor& textColor, const QColor& dimColor,
|
||||
const QSize& faceSize)
|
||||
{
|
||||
const QFontMetrics metrics(font);
|
||||
const int rowHeight = metrics.height();
|
||||
|
||||
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
||||
QPixmap face(static_cast<int>(faceSize.width() * dpr),
|
||||
static_cast<int>(faceSize.height() * dpr));
|
||||
face.setDevicePixelRatio(dpr);
|
||||
face.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&face);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||
painter.setFont(font);
|
||||
|
||||
// Both text rows are elided rather than wrapped or shrunk, so a long name or a
|
||||
// many-typed blueprint cannot change a card's height (REQ-UI-BLUEPRINT-CARD).
|
||||
painter.setPen(textColor);
|
||||
painter.drawText(QRect(0, 0, faceSize.width(), rowHeight),
|
||||
Qt::AlignLeft | Qt::AlignVCenter,
|
||||
metrics.elidedText(name, Qt::ElideRight, faceSize.width()));
|
||||
|
||||
painter.setPen(dimColor);
|
||||
painter.drawText(QRect(0, rowHeight, faceSize.width(), rowHeight),
|
||||
Qt::AlignLeft | Qt::AlignVCenter,
|
||||
metrics.elidedText(contents, Qt::ElideRight, faceSize.width()));
|
||||
|
||||
const QSize costSize = getLogicalSize(cost);
|
||||
painter.drawPixmap(0, faceSize.height() - costSize.height(), cost);
|
||||
return face;
|
||||
}
|
||||
|
||||
// A composed card face and the size to show it at; the button needs both, and only
|
||||
// the composer knows the size it arrived at.
|
||||
struct CardFace { QIcon icon; QSize size; };
|
||||
|
||||
// The two-mode face of one card. The modes differ only in the text color, so an
|
||||
// unaffordable card greys itself when Qt swaps the pixmap on the disabled button
|
||||
// (REQ-UI-BLUEPRINT-CARD, consistent with REQ-UI-BUILD-DISABLED).
|
||||
CardFace buildCardFace(const QString& name, const QString& contents, int cost,
|
||||
const QPixmap& blockIcon, const QFont& font,
|
||||
const QPalette& palette)
|
||||
{
|
||||
const QSize faceSize = getCardFaceSize(font);
|
||||
const QColor dimColor = palette.color(QPalette::Disabled, QPalette::ButtonText);
|
||||
|
||||
CardFace result;
|
||||
for (QIcon::Mode mode : { QIcon::Normal, QIcon::Disabled })
|
||||
{
|
||||
const QColor textColor = palette.color(
|
||||
(mode == QIcon::Normal) ? QPalette::Active : QPalette::Disabled,
|
||||
QPalette::ButtonText);
|
||||
// The cost reads exactly like the header stock and the build button costs:
|
||||
// the number, then the block icon in place of a trailing "Blocks"
|
||||
// (REQ-UI-BLOCKS-ICON). A null icon leaves the bare number.
|
||||
const QPixmap costPixmap =
|
||||
renderCaptionWithIcon(QString::number(cost), blockIcon, font, textColor);
|
||||
const QPixmap face = composeCardFace(name, contents, costPixmap, font,
|
||||
textColor, dimColor, faceSize);
|
||||
result.icon.addPixmap(face, mode);
|
||||
result.size = result.size.expandedTo(getLogicalSize(face));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
BlueprintSelectionDialog::BlueprintSelectionDialog(BlueprintLibrary* library,
|
||||
ItemIconCache* itemIcons,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_library(library)
|
||||
, m_itemIcons(itemIcons)
|
||||
{
|
||||
setWindowTitle(tr("Blueprints"));
|
||||
setModal(true);
|
||||
// Frameless: the dialog draws its own header row, so an OS title bar would only
|
||||
// repeat it (REQ-UI-BLUEPRINT-DIALOG). Square corners rather than rounded ones --
|
||||
// rounding a top-level window needs a translucent background, which is unreliable
|
||||
// on Windows. The border matches the build bar and the side panel.
|
||||
setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint);
|
||||
setAttribute(Qt::WA_StyledBackground, true);
|
||||
setStyleSheet(QStringLiteral(
|
||||
"BlueprintSelectionDialog { background-color: palette(window);"
|
||||
" border: 1px solid palette(mid); }"));
|
||||
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(kSpacingPx, kSpacingPx, kSpacingPx, kSpacingPx);
|
||||
mainLayout->setSpacing(kSpacingPx);
|
||||
|
||||
QHBoxLayout* headerLayout = new QHBoxLayout();
|
||||
headerLayout->setSpacing(kSpacingPx);
|
||||
|
||||
QLabel* titleLabel = new QLabel(tr("Blueprints"), this);
|
||||
QFont headerFont = titleLabel->font();
|
||||
headerFont.setBold(true);
|
||||
titleLabel->setFont(headerFont);
|
||||
headerLayout->addWidget(titleLabel);
|
||||
|
||||
// Dimmed and bold, the treatment the build button hotkey badges use, so the
|
||||
// shortcut reads as a reminder rather than a second title (REQ-UI-BUILD-COST).
|
||||
QLabel* hotkeyBadge = new QLabel(tr("Ctrl+V"), this);
|
||||
hotkeyBadge->setFont(headerFont);
|
||||
QPalette badgePalette = hotkeyBadge->palette();
|
||||
badgePalette.setColor(hotkeyBadge->foregroundRole(),
|
||||
palette().color(QPalette::Disabled, QPalette::WindowText));
|
||||
hotkeyBadge->setPalette(badgePalette);
|
||||
headerLayout->addWidget(hotkeyBadge);
|
||||
|
||||
headerLayout->addStretch();
|
||||
|
||||
QPushButton* closeButton = new QPushButton(QString(kCrossGlyph), this);
|
||||
closeButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx);
|
||||
closeButton->setToolTip(tr("Close"));
|
||||
connect(closeButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||
headerLayout->addWidget(closeButton);
|
||||
|
||||
mainLayout->addLayout(headerLayout);
|
||||
|
||||
QScrollArea* scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
m_gridContainer = new QWidget(scrollArea);
|
||||
m_grid = new QGridLayout(m_gridContainer);
|
||||
m_grid->setContentsMargins(0, 0, 0, 0);
|
||||
m_grid->setSpacing(kSpacingPx);
|
||||
m_grid->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||||
scrollArea->setWidget(m_gridContainer);
|
||||
mainLayout->addWidget(scrollArea, 1);
|
||||
|
||||
rebuildGrid();
|
||||
|
||||
// Fixed size (REQ-UI-BLUEPRINT-DIALOG), sized to show kVisibleRowCount card rows.
|
||||
const QSize cardSize = getCardSize(font());
|
||||
const int gridWidth = kGridColumnCount * cardSize.width()
|
||||
+ (kGridColumnCount - 1) * kSpacingPx;
|
||||
const int gridHeight = static_cast<int>(
|
||||
cardSize.height() * kVisibleRowCount + kSpacingPx * (kVisibleRowCount - 1.0));
|
||||
setFixedSize(gridWidth + style()->pixelMetric(QStyle::PM_ScrollBarExtent)
|
||||
+ 2 * kSpacingPx,
|
||||
gridHeight + kSmallButtonSizePx + 3 * kSpacingPx);
|
||||
|
||||
// A frameless dialog does not get Qt's automatic centering on its parent.
|
||||
if (parent)
|
||||
{
|
||||
const QRect parentRect = parent->window()->geometry();
|
||||
move(parentRect.center() - QPoint(width() / 2, height() / 2));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<int> BlueprintSelectionDialog::getChosenIndex() const
|
||||
{
|
||||
return m_chosenIndex;
|
||||
}
|
||||
|
||||
void BlueprintSelectionDialog::rebuildGrid()
|
||||
{
|
||||
// deleteLater, not delete: this runs from a delete button's own clicked signal, and
|
||||
// destroying that button inside its emission would leave Qt holding a dangling
|
||||
// sender. hide() makes the removal immediate all the same.
|
||||
while (QLayoutItem* item = m_grid->takeAt(0))
|
||||
{
|
||||
if (item->widget())
|
||||
{
|
||||
item->widget()->hide();
|
||||
item->widget()->deleteLater();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
|
||||
const std::vector<Blueprint>& blueprints = m_library->getBlueprints();
|
||||
if (blueprints.empty())
|
||||
{
|
||||
QLabel* emptyLabel = new QLabel(
|
||||
tr("No blueprints yet.\n\nSelect one or more buildings and press Ctrl+C "
|
||||
"to save them as a blueprint."),
|
||||
m_gridContainer);
|
||||
emptyLabel->setAlignment(Qt::AlignCenter);
|
||||
emptyLabel->setWordWrap(true);
|
||||
m_grid->addWidget(emptyLabel, 0, 0, 1, kGridColumnCount);
|
||||
return;
|
||||
}
|
||||
|
||||
// Block icon shown to the right of each cost (REQ-UI-BLUEPRINT-CARD); null when no
|
||||
// building_block icon exists, in which case the cost is the bare number.
|
||||
const QPixmap blockIcon = m_itemIcons->hasIcon(kBlockItemId)
|
||||
? m_itemIcons->getPixmap(kBlockItemId, QFontMetrics(font()).height())
|
||||
: QPixmap();
|
||||
|
||||
const QSize cardSize = getCardSize(font());
|
||||
|
||||
for (int i = 0; i < static_cast<int>(blueprints.size()); ++i)
|
||||
{
|
||||
const Blueprint& blueprint = blueprints[static_cast<std::size_t>(i)];
|
||||
const CardFace face = buildCardFace(
|
||||
blueprint.name, m_library->getContentsSummary(i), m_library->getCost(i),
|
||||
blockIcon, font(), palette());
|
||||
|
||||
QWidget* card = new QWidget(m_gridContainer);
|
||||
card->setFixedSize(cardSize);
|
||||
|
||||
QVBoxLayout* cardLayout = new QVBoxLayout(card);
|
||||
cardLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QPushButton* body = new QPushButton(card);
|
||||
body->setIcon(face.icon);
|
||||
body->setIconSize(face.size);
|
||||
// An unaffordable card is greyed and does not respond, but its delete icon
|
||||
// stays live (REQ-UI-BLUEPRINT-CARD).
|
||||
body->setEnabled(m_library->getCanAfford(i));
|
||||
cardLayout->addWidget(body);
|
||||
|
||||
// A sibling of the body rather than one of its children, and outside the
|
||||
// layout: Qt disables a widget's children along with it, and the delete icon
|
||||
// must stay enabled on an unaffordable card (REQ-UI-BLUEPRINT-DELETE).
|
||||
QPushButton* deleteButton = new QPushButton(QString(kCrossGlyph), card);
|
||||
deleteButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx);
|
||||
deleteButton->setToolTip(tr("Delete blueprint"));
|
||||
deleteButton->move(cardSize.width() - kSmallButtonSizePx - kCardPaddingPx / 2,
|
||||
cardSize.height() - kSmallButtonSizePx - kCardPaddingPx / 2);
|
||||
deleteButton->raise();
|
||||
|
||||
m_grid->addWidget(card, i / kGridColumnCount, i % kGridColumnCount);
|
||||
|
||||
const int index = i;
|
||||
connect(body, &QPushButton::clicked, this, [this, index]()
|
||||
{
|
||||
onCardClicked(index);
|
||||
});
|
||||
connect(deleteButton, &QPushButton::clicked, this, [this, index]()
|
||||
{
|
||||
onDeleteClicked(index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void BlueprintSelectionDialog::onCardClicked(int index)
|
||||
{
|
||||
// Placement mode is entered by the caller once the dialog has closed
|
||||
// (REQ-UI-BLUEPRINT-CARD).
|
||||
m_chosenIndex = index;
|
||||
accept();
|
||||
}
|
||||
|
||||
void BlueprintSelectionDialog::onDeleteClicked(int index)
|
||||
{
|
||||
// No confirmation prompt, and the dialog stays open (REQ-UI-BLUEPRINT-DELETE).
|
||||
m_library->remove(index);
|
||||
rebuildGrid();
|
||||
}
|
||||
42
src/ui/BlueprintSelectionDialog.h
Normal file
42
src/ui/BlueprintSelectionDialog.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class BlueprintLibrary;
|
||||
class ItemIconCache;
|
||||
class QGridLayout;
|
||||
class QWidget;
|
||||
|
||||
// The blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG): a frameless modal panel
|
||||
// showing every saved blueprint as a card in a scrolling two-column grid. The caller
|
||||
// pauses the game and raises the dim overlay while it is open.
|
||||
//
|
||||
// Clicking a card accepts the dialog and reports that blueprint's index; the caller
|
||||
// enters placement mode afterwards, so the dialog is already closed by then
|
||||
// (REQ-UI-BLUEPRINT-CARD). Deleting acts on the library immediately and leaves the
|
||||
// dialog open (REQ-UI-BLUEPRINT-DELETE). Escape and the close button dismiss it with
|
||||
// no other effect.
|
||||
class BlueprintSelectionDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// Neither the library nor the icon cache is owned; both outlive the dialog.
|
||||
BlueprintSelectionDialog(BlueprintLibrary* library, ItemIconCache* itemIcons,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
std::optional<int> getChosenIndex() const;
|
||||
|
||||
private:
|
||||
void rebuildGrid();
|
||||
void onCardClicked(int index);
|
||||
void onDeleteClicked(int index);
|
||||
|
||||
BlueprintLibrary* m_library;
|
||||
ItemIconCache* m_itemIcons;
|
||||
QWidget* m_gridContainer;
|
||||
QGridLayout* m_grid;
|
||||
std::optional<int> m_chosenIndex; // nullopt = dismissed without picking
|
||||
};
|
||||
@@ -96,16 +96,6 @@ namespace
|
||||
return result;
|
||||
}
|
||||
|
||||
// A pixmap's size in device-independent pixels. The pixmaps composed here are
|
||||
// rasterized at the device pixel ratio, so their raw size is not their layout size.
|
||||
QSize getLogicalSize(const QPixmap& pixmap)
|
||||
{
|
||||
if (pixmap.isNull()) { return QSize(0, 0); }
|
||||
const qreal dpr = pixmap.devicePixelRatio();
|
||||
return QSize(static_cast<int>(pixmap.width() / dpr),
|
||||
static_cast<int>(pixmap.height() / dpr));
|
||||
}
|
||||
|
||||
// One button face: the hotkey badge in the top-left corner, the chip icon centered
|
||||
// below it, and the caption — the cost, or the Deconstruct name — centered at the
|
||||
// bottom (REQ-UI-BUILD-COST). The three are composed into a single pixmap because
|
||||
|
||||
@@ -13,7 +13,8 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.h
|
||||
@@ -38,7 +39,8 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.cpp
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
#include "EscapeMenuRequestedEvent.h"
|
||||
#include "TracePrintRequestedEvent.h"
|
||||
#include "BuildHotkeyPressedEvent.h"
|
||||
#include "TemporaryBlueprintRequestedEvent.h"
|
||||
#include "GameResetEvent.h"
|
||||
#include "BossWaveUpdatedEvent.h"
|
||||
#include "BuilderModeExitedEvent.h"
|
||||
#include "BlueprintModeExitedEvent.h"
|
||||
@@ -269,20 +269,6 @@ void GameWorldView::onFrame()
|
||||
pruneDespawnedDebris();
|
||||
pruneDespawnedActors();
|
||||
|
||||
// Expire copy/paste flashes. Lifetime is wall-clock (the frame delta), so the
|
||||
// flash plays for a fixed real duration regardless of game speed, including
|
||||
// while the game is paused (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||
if (!m_copyConfigFlashes.empty())
|
||||
{
|
||||
std::vector<CopyConfigFlash> live;
|
||||
for (CopyConfigFlash flash : m_copyConfigFlashes)
|
||||
{
|
||||
flash.remainingMs -= elapsed;
|
||||
if (flash.remainingMs > 0) { live.push_back(flash); }
|
||||
}
|
||||
m_copyConfigFlashes = std::move(live);
|
||||
}
|
||||
|
||||
// Apply held scroll
|
||||
{
|
||||
const bool viewMoved =
|
||||
@@ -414,9 +400,8 @@ void GameWorldView::paintGL()
|
||||
|
||||
WorldRenderFrame GameWorldView::makeRenderFrame() const
|
||||
{
|
||||
return WorldRenderFrame{m_selection, m_buildMode, m_activeBeams, m_copiedConfig,
|
||||
m_copyConfigFlashes, m_boxSelecting, m_boxStartTile,
|
||||
m_boxCurrentTile, m_debugDraw};
|
||||
return WorldRenderFrame{m_selection, m_buildMode, m_activeBeams, m_boxSelecting,
|
||||
m_boxStartTile, m_boxCurrentTile, m_debugDraw};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1026,10 +1011,6 @@ void GameWorldView::keyReleaseEvent(QKeyEvent* event)
|
||||
return;
|
||||
}
|
||||
if (m_inputMapper.handleKeyRelease(event)) { return; }
|
||||
// Releasing Shift discards the copied building settings (REQ-BLD-COPY-CONFIG).
|
||||
// Stays here rather than moving to the input mapper: Shift is the modifier of a
|
||||
// mouse gesture, not a keyboard action of its own.
|
||||
if (event->key() == Qt::Key_Shift) { m_copiedConfig.reset(); }
|
||||
QOpenGLWidget::keyReleaseEvent(event);
|
||||
}
|
||||
|
||||
@@ -1061,15 +1042,6 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
m_buildMode.exitCurrentMode();
|
||||
}
|
||||
else if (event->modifiers() & Qt::ShiftModifier)
|
||||
{
|
||||
// Shift + right-click copies a building's settings, but only in the
|
||||
// default selection mode (REQ-BLD-COPY-CONFIG).
|
||||
const QPoint tile = coordinates.widgetToTile(event->pos());
|
||||
std::optional<BuildingId> id = buildingAtTile(tile);
|
||||
if (!id.has_value()) { id = siteAtTile(tile); }
|
||||
if (id.has_value()) { copyConfigFrom(*id); }
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1105,20 +1077,6 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Shift + left-click applies the copied settings to a same-type building
|
||||
// (REQ-BLD-COPY-CONFIG). Consumes the click so it does not change the
|
||||
// selection. Only active in the default selection mode.
|
||||
if ((event->modifiers() & Qt::ShiftModifier) && m_copiedConfig.has_value())
|
||||
{
|
||||
std::optional<BuildingId> id = buildingAtTile(tile);
|
||||
if (!id.has_value()) { id = siteAtTile(tile); }
|
||||
if (id.has_value())
|
||||
{
|
||||
pasteConfigTo(*id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const bool ctrl = (event->modifiers() & Qt::ControlModifier) != 0;
|
||||
|
||||
// Only a click that hit nothing starts a box drag. Starting one on a hit
|
||||
@@ -1375,85 +1333,6 @@ void GameWorldView::rotateGhost(bool clockwise)
|
||||
}
|
||||
}
|
||||
|
||||
void GameWorldView::copyConfigFrom(BuildingId id)
|
||||
{
|
||||
const std::optional<BuildingConfig> config = readBuildingConfig(*m_sim, id);
|
||||
if (!config.has_value()) { return; }
|
||||
|
||||
// Only cache when there is something to copy: a selected recipe / schematic
|
||||
// (Miner, Assembler, Shipyard) or any Splitter (empty filters = accept-all).
|
||||
// Building types with no settings (Smelter, Reprocessing Plant, Salvage Bay,
|
||||
// belts / tunnels, HQ) leave any existing cache untouched (REQ-BLD-COPY-CONFIG).
|
||||
if (!config->recipeId.has_value() && !config->isSplitter) { return; }
|
||||
|
||||
m_copiedConfig = config;
|
||||
m_copyConfigFlashes.push_back({ id, kCopyFlashDurationMs });
|
||||
}
|
||||
|
||||
void GameWorldView::pasteConfigTo(BuildingId id)
|
||||
{
|
||||
if (!m_copiedConfig.has_value()) { return; }
|
||||
|
||||
const std::optional<BuildingConfig> target = readBuildingConfig(*m_sim, id);
|
||||
if (!target.has_value() || target->type != m_copiedConfig->type) { return; }
|
||||
|
||||
// The paste applies below; flash the target to confirm (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||
m_copyConfigFlashes.push_back({ id, kCopyFlashDurationMs });
|
||||
|
||||
const BuildingConfig& source = *m_copiedConfig;
|
||||
|
||||
// The cached settings were valid on a same-type source building, so they are
|
||||
// valid and available on the target: unlock state is global, so a selected
|
||||
// recipe / schematic (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SCHEMATIC) and splitter
|
||||
// filter item types (REQ-LOCK-UI-SPLITTER) remain unlocked. Applying reuses the
|
||||
// same configuration commands as the selected building panel, inheriting their
|
||||
// buffer-clearing and mid-cycle-cancel semantics (REQ-MAT-INPUT-BUFFER,
|
||||
// REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
|
||||
if (source.isSplitter)
|
||||
{
|
||||
// Operational splitters are configured by tile; sites by BuildingId
|
||||
// (mirrors SelectedBuildingPanel::onSplitterFilterChanged).
|
||||
if (const Building* building = findBuilding(m_sim->getFactoryState(), id))
|
||||
{
|
||||
std::shared_ptr<SetSplitterFiltersCommand> command =
|
||||
std::make_shared<SetSplitterFiltersCommand>();
|
||||
command->tile = building->anchor;
|
||||
command->filterA = source.splitterFilterA;
|
||||
command->filterB = source.splitterFilterB;
|
||||
enqueueCommand(command);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::shared_ptr<SetSiteSplitterFiltersCommand> command =
|
||||
std::make_shared<SetSiteSplitterFiltersCommand>();
|
||||
command->id = id;
|
||||
command->filterA = source.splitterFilterA;
|
||||
command->filterB = source.splitterFilterB;
|
||||
enqueueCommand(command);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.recipeId.has_value())
|
||||
{
|
||||
std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>();
|
||||
command->id = id;
|
||||
command->recipeId = *source.recipeId;
|
||||
enqueueCommand(command);
|
||||
}
|
||||
|
||||
// For a shipyard the schematic (above) must be applied before its module
|
||||
// layout, so both commands drain in order at the next tick boundary.
|
||||
if (source.type == BuildingType::Shipyard && source.shipLayout.has_value())
|
||||
{
|
||||
std::shared_ptr<SetShipLayoutCommand> command =
|
||||
std::make_shared<SetShipLayoutCommand>();
|
||||
command->id = id;
|
||||
command->layout = *source.shipLayout;
|
||||
enqueueCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
double GameWorldView::getGameSpeed() const
|
||||
{
|
||||
return m_gameSpeedMultiplier;
|
||||
@@ -1484,8 +1363,6 @@ void GameWorldView::resetForNewGame()
|
||||
m_activeBeams.clear();
|
||||
m_schematicChoiceShown = false;
|
||||
m_selection.clearAll();
|
||||
m_copiedConfig = std::nullopt;
|
||||
m_copyConfigFlashes.clear();
|
||||
m_boxSelecting = false;
|
||||
m_camera.reset();
|
||||
// Drops any key still held across the restart, which also republishes the pan
|
||||
@@ -1502,6 +1379,10 @@ void GameWorldView::resetForNewGame()
|
||||
m_lastArtifactCount = -1;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(std::vector<BuildingId>{}));
|
||||
// The one place a restart actually lands (the menu, game over and win dialogs only
|
||||
// enqueue the command), so it is where presentation state belonging to the finished
|
||||
// run is dropped -- e.g. the temporary blueprint (REQ-UI-BLUEPRINT-TEMP).
|
||||
EventManager::getInstance()->sendEventImmediately(std::make_shared<GameResetEvent>());
|
||||
setGameSpeed(1.0);
|
||||
// Rebase the wall-clock time source so a fresh run starts from a clean time
|
||||
// base. Without this, wall time accumulated while a modal (Game Over, Win, or
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
|
||||
#include "Blueprint.h"
|
||||
#include "BuildModeController.h"
|
||||
#include "BuildingConfig.h"
|
||||
#include "BlueprintModeExitedEvent.h"
|
||||
#include "BlueprintPlacementRequestedEvent.h"
|
||||
#include "BuilderModeExitedEvent.h"
|
||||
@@ -217,12 +216,6 @@ private:
|
||||
// Enqueues placements and rotate-in-place commands for the resolved path.
|
||||
void applyBeltDragPath();
|
||||
|
||||
// Copy-settings gesture (REQ-BLD-COPY-CONFIG): Shift+right-click copies a
|
||||
// building's configuration into m_copiedConfig; Shift+left-click applies it to
|
||||
// another building of the same type via the existing configuration commands.
|
||||
void copyConfigFrom(BuildingId id);
|
||||
void pasteConfigTo(BuildingId id);
|
||||
|
||||
// Turns the ghost and refreshes everything that depends on its facing: placement
|
||||
// validity, the tunnel completion match, and an in-progress belt drag's path.
|
||||
// The mode transitions themselves live on m_buildMode.
|
||||
@@ -271,17 +264,6 @@ private:
|
||||
// and to resolve the tunnel ghost sub-tile (REQ-BLD-TUNNEL-MODE).
|
||||
QVector2D m_cursorWorldPos;
|
||||
|
||||
// Temporary cache for the copy-settings gesture (REQ-BLD-COPY-CONFIG); held
|
||||
// only while Shift is down and cleared on Shift release.
|
||||
std::optional<BuildingConfig> m_copiedConfig;
|
||||
|
||||
// Brief outline flash shown on a building when settings are copied from it or
|
||||
// pasted onto it (REQ-BLD-COPY-CONFIG-FEEDBACK). remainingMs counts down in
|
||||
// wall-clock time so the flash plays at a fixed length regardless of game speed
|
||||
// (and while paused).
|
||||
std::vector<CopyConfigFlash> m_copyConfigFlashes;
|
||||
static constexpr qint64 kCopyFlashDurationMs = 300;
|
||||
|
||||
bool m_debugDraw;
|
||||
|
||||
// Owns the selection across all three categories and the rules for moving
|
||||
|
||||
@@ -46,3 +46,11 @@ QPixmap renderCaptionWithIcon(const QString& text, const QPixmap& icon,
|
||||
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
QSize getLogicalSize(const QPixmap& pixmap)
|
||||
{
|
||||
if (pixmap.isNull()) { return QSize(0, 0); }
|
||||
const qreal dpr = pixmap.devicePixelRatio();
|
||||
return QSize(static_cast<int>(pixmap.width() / dpr),
|
||||
static_cast<int>(pixmap.height() / dpr));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QPixmap>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
|
||||
// Renders `text` followed by `icon` to its right, vertically centered, onto a
|
||||
@@ -15,6 +16,11 @@
|
||||
QPixmap renderCaptionWithIcon(const QString& text, const QPixmap& icon,
|
||||
const QFont& font, const QColor& textColor);
|
||||
|
||||
// A pixmap's size in device-independent pixels. The pixmaps composed by
|
||||
// renderCaptionWithIcon and its callers are rasterized at the device pixel ratio, so
|
||||
// their raw size is not the size to lay them out at.
|
||||
QSize getLogicalSize(const QPixmap& pixmap);
|
||||
|
||||
// Item id of the building blocks resource, whose icon stands in for the "Blocks"
|
||||
// word wherever a cost or stock is captioned (REQ-UI-BLOCKS-ICON, REQ-UI-BUILD-COST,
|
||||
// REQ-UI-EXPAND-BUTTON). It lives next to the caption helper because every caller of
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include <QKeyEvent>
|
||||
|
||||
#include "BlueprintSaveRequestedEvent.h"
|
||||
#include "BlueprintSelectionRequestedEvent.h"
|
||||
#include "BuildHotkeyPressedEvent.h"
|
||||
#include "BuildingType.h"
|
||||
#include "DebugDrawToggleRequestedEvent.h"
|
||||
@@ -15,7 +17,8 @@
|
||||
#include "PanDirectionChangedEvent.h"
|
||||
#include "PauseToggleRequestedEvent.h"
|
||||
#include "SpeedStepRequestedEvent.h"
|
||||
#include "TemporaryBlueprintRequestedEvent.h"
|
||||
#include "TemporaryBlueprintCaptureRequestedEvent.h"
|
||||
#include "TemporaryBlueprintPlaceRequestedEvent.h"
|
||||
#include "TracePrintRequestedEvent.h"
|
||||
|
||||
namespace
|
||||
@@ -97,6 +100,28 @@ bool InputMapper::handleKeyPress(QKeyEvent* event)
|
||||
}
|
||||
}
|
||||
|
||||
// Blueprint chords (REQ-UI-HOTKEYS). Checked ahead of the plain-key switch below,
|
||||
// which binds bare A/D/W/S/R/Q/C/V and must not fire on a Ctrl chord -- bare C and V
|
||||
// are the temporary-blueprint counterparts of these two (REQ-UI-BLUEPRINT-TEMP). Both
|
||||
// requests are decided by MainWindow, the only widget that can pause the game and dim
|
||||
// the window for a modal.
|
||||
if ((event->modifiers() & Qt::ControlModifier) != 0)
|
||||
{
|
||||
switch (event->key())
|
||||
{
|
||||
case Qt::Key_C:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintSaveRequestedEvent>());
|
||||
return true;
|
||||
case Qt::Key_V:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintSelectionRequestedEvent>());
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (event->key())
|
||||
{
|
||||
case Qt::Key_A:
|
||||
@@ -129,12 +154,18 @@ bool InputMapper::handleKeyPress(QKeyEvent* event)
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ModeCancelRequestedEvent>());
|
||||
return true;
|
||||
case Qt::Key_T:
|
||||
// Request a temporary blueprint from the current selection (REQ-UI-BLUEPRINT-TEMP).
|
||||
// The BlueprintPanel owns the selection and blueprint-capture logic; it decides
|
||||
case Qt::Key_C:
|
||||
// Capture a temporary blueprint from the current selection (REQ-UI-BLUEPRINT-TEMP).
|
||||
// The BlueprintLibrary owns the selection and blueprint-capture logic; it decides
|
||||
// whether anything placeable is selected and drives placement mode from there.
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<TemporaryBlueprintRequestedEvent>());
|
||||
std::make_shared<TemporaryBlueprintCaptureRequestedEvent>());
|
||||
return true;
|
||||
case Qt::Key_V:
|
||||
// Re-enter placement mode for the temporary blueprint captured with C, if there is
|
||||
// one (REQ-UI-BLUEPRINT-TEMP). The library holds it; nothing is captured here.
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<TemporaryBlueprintPlaceRequestedEvent>());
|
||||
return true;
|
||||
case Qt::Key_F3:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
|
||||
@@ -9,12 +9,15 @@
|
||||
#include <QCloseEvent>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QResizeEvent>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BlueprintPanel.h"
|
||||
#include "BlueprintLibrary.h"
|
||||
#include "BlueprintSelectionDialog.h"
|
||||
#include "BuildButtonBar.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "Command.h"
|
||||
@@ -71,31 +74,29 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
||||
m_buildButtonBar = new BuildButtonBar(sim, &sim->getConfig(), iconDir,
|
||||
m_itemIcons.get(), this);
|
||||
|
||||
// The blueprints have no widget of their own: they are saved with Ctrl+C and picked
|
||||
// from a modal dialog (REQ-UI-BLUEPRINT-DIALOG), both driven from this window
|
||||
// because only it can pause the game and raise the dim overlay. Built after the
|
||||
// world view because loading blueprints.toml may put a message box on screen.
|
||||
m_blueprintLibrary = std::make_unique<BlueprintLibrary>(sim, &sim->getConfig(), this);
|
||||
|
||||
m_sidePanel = new QWidget(this);
|
||||
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
|
||||
sideLayout->setContentsMargins(1, 1, 1, 1);
|
||||
sideLayout->setSpacing(1);
|
||||
|
||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
|
||||
m_blueprintPanel = new BlueprintPanel(sim, &sim->getConfig(), m_sidePanel);
|
||||
|
||||
// Equal stretch gives the two panels half the column height each
|
||||
// The selected building panel is the column's only panel and fills its height
|
||||
// (REQ-UI-PANEL-COLUMN).
|
||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
|
||||
sideLayout->addWidget(m_selectedBuildingPanel, 1);
|
||||
sideLayout->addWidget(m_blueprintPanel, 1);
|
||||
|
||||
// Draw a thin border around each of the two side-panel sections. The class
|
||||
// scoped selectors keep the border on the panels themselves rather than
|
||||
// cascading onto their child widgets; WA_StyledBackground lets the plain
|
||||
// QWidget subclasses honor the stylesheet box (border/background).
|
||||
for (QWidget* panel : { static_cast<QWidget*>(m_selectedBuildingPanel),
|
||||
static_cast<QWidget*>(m_blueprintPanel) })
|
||||
{
|
||||
panel->setAttribute(Qt::WA_StyledBackground, true);
|
||||
}
|
||||
// Draw a thin border around the side panel section. The class scoped selector keeps
|
||||
// the border on the panel itself rather than cascading onto its child widgets;
|
||||
// WA_StyledBackground lets the plain QWidget subclass honor the stylesheet box
|
||||
// (border/background).
|
||||
m_selectedBuildingPanel->setAttribute(Qt::WA_StyledBackground, true);
|
||||
m_sidePanel->setStyleSheet(QStringLiteral(
|
||||
"SelectedBuildingPanel, BlueprintPanel {"
|
||||
" border: 1px solid palette(mid); }"));
|
||||
"SelectedBuildingPanel { border: 1px solid palette(mid); }"));
|
||||
|
||||
// Created last so it stacks above the other children; covers the whole window and
|
||||
// dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM).
|
||||
@@ -369,6 +370,47 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent> /*event*/)
|
||||
{
|
||||
// Ctrl+C has effect only when something player-placeable is selected; otherwise no
|
||||
// dialog opens at all (REQ-UI-BLUEPRINT-CREATE).
|
||||
if (!m_blueprintLibrary->getCanCaptureSelection()) { return; }
|
||||
|
||||
// Both scopes are held across the naming dialog and the selection dialog it hands
|
||||
// over to, so the dim never blinks off and the simulation is not resumed in between
|
||||
// (REQ-UI-MODAL-DIM).
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
this, tr("Create Blueprint"), tr("Blueprint name:"), QLineEdit::Normal,
|
||||
QString(), &ok);
|
||||
// Cancel, Escape, or an empty name: no blueprint, and no selection dialog.
|
||||
if (!ok || name.trimmed().isEmpty()) { return; }
|
||||
|
||||
m_blueprintLibrary->saveSelectionAs(name.trimmed());
|
||||
showBlueprintSelectionDialog();
|
||||
}
|
||||
|
||||
void MainWindow::handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> /*event*/)
|
||||
{
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
showBlueprintSelectionDialog();
|
||||
}
|
||||
|
||||
void MainWindow::showBlueprintSelectionDialog()
|
||||
{
|
||||
BlueprintSelectionDialog dialog(m_blueprintLibrary.get(), m_itemIcons.get(), this);
|
||||
if (dialog.exec() == QDialog::Accepted && dialog.getChosenIndex().has_value())
|
||||
{
|
||||
// Entered after the dialog has closed, which is the order REQ-UI-BLUEPRINT-CARD
|
||||
// describes: clicking a card closes the dialog and enters placement mode.
|
||||
m_blueprintLibrary->beginPlacement(*dialog.getChosenIndex());
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
||||
{
|
||||
const Tick tick = m_sim->getCurrentTick();
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "BlueprintSaveRequestedEvent.h"
|
||||
#include "BlueprintSelectionRequestedEvent.h"
|
||||
#include "BuildingId.h"
|
||||
#include "EscapeMenuRequestedEvent.h"
|
||||
#include "EventHandler.h"
|
||||
@@ -28,7 +30,7 @@ class GameWorldView;
|
||||
class HeaderBar;
|
||||
class SelectedBuildingPanel;
|
||||
class BuildButtonBar;
|
||||
class BlueprintPanel;
|
||||
class BlueprintLibrary;
|
||||
class ItemIconCache;
|
||||
class QCloseEvent;
|
||||
class QResizeEvent;
|
||||
@@ -39,7 +41,9 @@ class MainWindow : public QWidget,
|
||||
WinEvent,
|
||||
EscapeMenuRequestedEvent,
|
||||
LayoutDialogRequestedEvent,
|
||||
RecipeSelectionRequestedEvent>
|
||||
RecipeSelectionRequestedEvent,
|
||||
BlueprintSaveRequestedEvent,
|
||||
BlueprintSelectionRequestedEvent>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -59,6 +63,8 @@ private:
|
||||
void handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> event) override;
|
||||
|
||||
// Reloads the game config and visuals.toml from disk (REQ-CFG-RELOAD), shared
|
||||
// by every restart path. On success the reloaded visuals are applied to this
|
||||
@@ -71,6 +77,12 @@ private:
|
||||
void openShipLayoutDialog(BuildingId shipyardId,
|
||||
const std::string& schematicId,
|
||||
const ShipLayoutConfig& currentLayout);
|
||||
|
||||
// Runs the blueprint selection dialog and enters placement mode for whatever the
|
||||
// player picked (REQ-UI-BLUEPRINT-DIALOG). Holds no pause or dim scope of its own:
|
||||
// both callers already hold theirs, which is what keeps the dim continuous when a
|
||||
// confirmed save hands straight over to this dialog (REQ-UI-MODAL-DIM).
|
||||
void showBlueprintSelectionDialog();
|
||||
void layoutPanels();
|
||||
|
||||
private:
|
||||
@@ -84,7 +96,9 @@ private:
|
||||
HeaderBar* m_headerBar;
|
||||
SelectedBuildingPanel* m_selectedBuildingPanel;
|
||||
BuildButtonBar* m_buildButtonBar;
|
||||
BlueprintPanel* m_blueprintPanel;
|
||||
// The saved blueprints themselves; they have no widget of their own any more and
|
||||
// are reached through the two modal dialogs (REQ-UI-BLUEPRINT-DIALOG).
|
||||
std::unique_ptr<BlueprintLibrary> m_blueprintLibrary;
|
||||
QWidget* m_sidePanel;
|
||||
ModalDimOverlay* m_dimOverlay = nullptr;
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ struct OverlayVisuals
|
||||
QColor selectionRect;
|
||||
QColor tileHighlight;
|
||||
QColor selectedOutline;
|
||||
QColor copyConfig;
|
||||
QColor lockedAsteroid;
|
||||
QColor modalDim;
|
||||
QColor tunnelPreview; // tunnel connection preview highlight (REQ-BLD-TUNNEL-MODE)
|
||||
|
||||
@@ -224,7 +224,6 @@ VisualsConfig VisualsLoader::load(const std::string& path)
|
||||
cfg.overlays.selectionRect = parseColor(requireString(ov, "selection_rect", "overlays"), "overlays.selection_rect");
|
||||
cfg.overlays.tileHighlight = parseColor(requireString(ov, "tile_highlight", "overlays"), "overlays.tile_highlight");
|
||||
cfg.overlays.selectedOutline = parseColor(requireString(ov, "selected_outline", "overlays"), "overlays.selected_outline");
|
||||
cfg.overlays.copyConfig = parseColor(requireString(ov, "copy_config", "overlays"), "overlays.copy_config");
|
||||
cfg.overlays.lockedAsteroid = parseColor(requireString(ov, "locked_asteroid", "overlays"), "overlays.locked_asteroid");
|
||||
cfg.overlays.modalDim = parseColor(requireString(ov, "modal_dim", "overlays"), "overlays.modal_dim");
|
||||
cfg.overlays.tunnelPreview = parseColor(requireString(ov, "tunnel_preview", "overlays"), "overlays.tunnel_preview");
|
||||
|
||||
@@ -129,7 +129,6 @@ void WorldRenderer::render(QPainter& painter, const WorldCoordinates& coordinate
|
||||
// into the port and stay visible while crossing directly between two touching
|
||||
// buildings (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-INPUT-INTAKE, REQ-MAT-DIRECT-COUPLE).
|
||||
drawPortItems(painter, coordinates, frame);
|
||||
drawCopyConfigFeedback(painter, coordinates, frame);
|
||||
drawStations(painter, coordinates, frame);
|
||||
drawBeltItems(painter, coordinates, frame);
|
||||
drawDebris(painter, coordinates, frame);
|
||||
@@ -505,44 +504,6 @@ void WorldRenderer::drawSelectionHighlights(QPainter& painter, const WorldCoordi
|
||||
}
|
||||
}
|
||||
|
||||
void WorldRenderer::drawCopyConfigFeedback(QPainter& painter, const WorldCoordinates& coordinates,
|
||||
const WorldRenderFrame& frame)
|
||||
{
|
||||
const QColor color = m_visuals.overlays.copyConfig;
|
||||
|
||||
// Eligible-target tint: while a configuration is cached, every same-type
|
||||
// building and site (the source included) is a valid paste target and is
|
||||
// washed in the copy-settings color (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||
if (frame.copiedConfig.has_value())
|
||||
{
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(color);
|
||||
const BuildingType type = frame.copiedConfig->type;
|
||||
for (const Building& b : getAllBuildings(m_sim.getFactoryState()))
|
||||
{
|
||||
if (b.type != type) { continue; }
|
||||
const std::optional<QRectF> rect = footprintWidgetRect(coordinates, b.id);
|
||||
if (rect.has_value()) { painter.drawRect(*rect); }
|
||||
}
|
||||
for (const ConstructionSite& s : getAllSites(m_sim.getFactoryState()))
|
||||
{
|
||||
if (s.type != type) { continue; }
|
||||
const std::optional<QRectF> rect = footprintWidgetRect(coordinates, s.id);
|
||||
if (rect.has_value()) { painter.drawRect(*rect); }
|
||||
}
|
||||
}
|
||||
|
||||
// Copy / paste flashes: a brief outline in the same color, drawn like the
|
||||
// selection outline (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||
painter.setPen(QPen(color, 2));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
for (const CopyConfigFlash& flash : frame.copyConfigFlashes)
|
||||
{
|
||||
const std::optional<QRectF> rect = footprintWidgetRect(coordinates, flash.id);
|
||||
if (rect.has_value()) { painter.drawRect(rect->adjusted(-1, -1, 1, 1)); }
|
||||
}
|
||||
}
|
||||
|
||||
void WorldRenderer::drawPortItems(QPainter& painter, const WorldCoordinates& coordinates,
|
||||
const WorldRenderFrame& /*frame*/)
|
||||
{
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
#include "BeamFiredEvent.h"
|
||||
#include "BuildModeController.h"
|
||||
#include "BuildingConfig.h"
|
||||
#include "BuildingId.h"
|
||||
#include "BuildingType.h"
|
||||
#include "Rotation.h"
|
||||
@@ -36,16 +35,6 @@ struct ActiveBeam
|
||||
QVector2D targetOffset;
|
||||
};
|
||||
|
||||
// Brief outline flash shown on a building when settings are copied from it or
|
||||
// pasted onto it (REQ-BLD-COPY-CONFIG-FEEDBACK). remainingMs counts down in
|
||||
// wall-clock time so the flash plays at a fixed length regardless of game speed
|
||||
// (and while paused).
|
||||
struct CopyConfigFlash
|
||||
{
|
||||
BuildingId id;
|
||||
qint64 remainingMs;
|
||||
};
|
||||
|
||||
// Everything the renderer draws that the simulation does not know about: what the
|
||||
// player has selected, which build mode is active, and the other transient bits of
|
||||
// interaction state the view owns. Assembled fresh each frame and passed by
|
||||
@@ -55,8 +44,6 @@ struct WorldRenderFrame
|
||||
const SelectionController& selection;
|
||||
const BuildModeController& buildMode;
|
||||
const std::vector<ActiveBeam>& beams;
|
||||
const std::optional<BuildingConfig>& copiedConfig;
|
||||
const std::vector<CopyConfigFlash>& copyConfigFlashes;
|
||||
bool isBoxSelecting;
|
||||
QPoint boxStartTile;
|
||||
QPoint boxCurrentTile;
|
||||
@@ -94,8 +81,6 @@ private:
|
||||
const WorldRenderFrame& frame);
|
||||
void drawSelectionHighlights(QPainter& painter, const WorldCoordinates& coordinates,
|
||||
const WorldRenderFrame& frame);
|
||||
void drawCopyConfigFeedback(QPainter& painter, const WorldCoordinates& coordinates,
|
||||
const WorldRenderFrame& frame);
|
||||
void drawPortItems(QPainter& painter, const WorldCoordinates& coordinates,
|
||||
const WorldRenderFrame& frame);
|
||||
void drawStations(QPainter& painter, const WorldCoordinates& coordinates,
|
||||
@@ -145,8 +130,7 @@ private:
|
||||
const QColor& fill) const;
|
||||
|
||||
// Widget-space rectangle covering a building or construction site's footprint,
|
||||
// or nullopt if the id resolves to neither. Shared by the selection highlight
|
||||
// and the copy-settings feedback (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||
// or nullopt if the id resolves to neither. Used by the selection highlight.
|
||||
std::optional<QRectF> footprintWidgetRect(const WorldCoordinates& coordinates,
|
||||
BuildingId id) const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user