show recipes visually instead of describing them in text

This commit is contained in:
2026-08-11 21:40:36 +02:00
parent 3274df91d4
commit f255224ccd
38 changed files with 1023 additions and 452 deletions

View File

@@ -6,8 +6,7 @@ Config files use the TOML format. The following config files drive game paramete
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, building deconstruction time, wave timing, boss wave timing, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, scrap-per-threat conversion, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, artifact win count, view pan speeds (slow and fast horizontal pan speed and pan ramp band width), an optional building blocks tooltip string (shown as the header bar's building blocks stock hover tooltip, REQ-UI-BLOCKS-TOOLTIP; omitted when unset), and an optional artifact tooltip string (shown as the header bar's artifact count hover tooltip, REQ-UI-ARTIFACTS-TOOLTIP; omitted when unset). - **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, building deconstruction time, wave timing, boss wave timing, belt speed, starting building blocks, departure interval, ship orbit factor, rally orbit radius, scrap-per-threat conversion, combat target-selection parameters (target score formula, overclaim penalty formula, target hysteresis), artifact chance formula, artifact win count, view pan speeds (slow and fast horizontal pan speed and pan ramp band width), an optional building blocks tooltip string (shown as the header bar's building blocks stock hover tooltip, REQ-UI-BLOCKS-TOOLTIP; omitted when unset), and an optional artifact tooltip string (shown as the header bar's artifact count hover tooltip, REQ-UI-ARTIFACTS-TOOLTIP; omitted when unset).
- **buildings.toml** — building block cost and construction time per building type, plus an optional tooltip description string per building type (shown as the build button's hover tooltip, REQ-UI-BUILD-TOOLTIP; omitted when unset). Whether a building type is available from game start or must be unlocked during play is not defined here but in **unlocks.toml** (REQ-LOCK-EXPLICIT): a building type granted by an unlock group starts locked and is hidden from the build menu until its group is awarded (REQ-LOCK-BUILDING). - **buildings.toml** — building block cost and construction time per building type, plus an optional tooltip description string per building type (shown as the build button's hover tooltip, REQ-UI-BUILD-TOOLTIP; omitted when unset). Whether a building type is available from game start or must be unlocked during play is not defined here but in **unlocks.toml** (REQ-LOCK-EXPLICIT): a building type granted by an unlock group starts locked and is hidden from the build menu until its group is awarded (REQ-LOCK-BUILDING).
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlocked_at_start` (boolean, default false): when true the recipe is available from game start regardless of the implicit item graph — used for base recipes that no schematic's materials reach (such as building blocks; see REQ-LOCK-IMPLICIT). Which assembler recipes must instead be awarded during play (explicitly gated) is defined in **unlocks.toml**, not here (REQ-LOCK-EXPLICIT); every remaining assembler recipe is implicitly unlocked through the item graph (REQ-LOCK-IMPLICIT). Any recipe entry may optionally define `icon` (string): the id of an item whose icon represents the recipe in the recipe-selection dialog (REQ-UI-RECIPE-ICON); when omitted, the recipe's first output item is used. - **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlocked_at_start` (boolean, default false): when true the recipe is available from game start regardless of the implicit item graph — used for base recipes that no schematic's materials reach (such as building blocks; see REQ-LOCK-IMPLICIT). Which assembler recipes must instead be awarded during play (explicitly gated) is defined in **unlocks.toml**, not here (REQ-LOCK-EXPLICIT); every remaining assembler recipe is implicitly unlocked through the item graph (REQ-LOCK-IMPLICIT).- **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as plain values, required build materials, a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES). Whether a ship schematic is available from game start or must be unlocked during play is defined in **unlocks.toml** (REQ-LOCK-EXPLICIT), not here.
- **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as plain values, required build materials, a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES). Whether a ship schematic is available from game start or must be unlocked during play is defined in **unlocks.toml** (REQ-LOCK-EXPLICIT), not here.
- **modules.toml** — per module type: id, surface mask, materials list, production time, fill color, glyph, an optional tooltip description string (shown as the module selection button's hover tooltip, REQ-MOD-UI-MODULE-TOOLTIP; omitted when unset), and an optional capability section and/or stat modifier formulas. Whether a module schematic is available from game start or must be unlocked during play is defined in **unlocks.toml** (REQ-LOCK-EXPLICIT), not here. A module with a capability section (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas is a **capability module** that grants the ship a weapon, salvage bay, or repair tool per instance (see REQ-MOD-CONFIG for the full list of formulas per capability type). A module with only `added_*`/`multiplied_*` formulas is a **passive module** that modifies stats on the ship or on capability module instances (see REQ-MOD-STAT-CALC). - **modules.toml** — per module type: id, surface mask, materials list, production time, fill color, glyph, an optional tooltip description string (shown as the module selection button's hover tooltip, REQ-MOD-UI-MODULE-TOOLTIP; omitted when unset), and an optional capability section and/or stat modifier formulas. Whether a module schematic is available from game start or must be unlocked during play is defined in **unlocks.toml** (REQ-LOCK-EXPLICIT), not here. A module with a capability section (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas is a **capability module** that grants the ship a weapon, salvage bay, or repair tool per instance (see REQ-MOD-CONFIG for the full list of formulas per capability type). A module with only `added_*`/`multiplied_*` formulas is a **passive module** that modifies stats on the ship or on capability module instances (see REQ-MOD-STAT-CALC).
- **unlocks.toml** — unlock groups: each `[[unlock]]` entry names a group of ship schematics, module schematics, building types, and/or assembler recipes that are awarded together from a single defence station drop (see Unlock Group Format, REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP). Anything not granted by any unlock group is available from game start. - **unlocks.toml** — unlock groups: each `[[unlock]]` entry names a group of ship schematics, module schematics, building types, and/or assembler recipes that are awarded together from a single defence station drop (see Unlock Group Format, REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP). Anything not granted by any unlock group is available from game start.
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level. - **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
@@ -311,8 +310,8 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
The dialog contains: The dialog contains:
- **Top**: The ship's layout grid rendered at full scale. Buildable cells are white; non-buildable cells are black. Placed modules are rendered with their `fill_color` and `glyph`. The ghost of the currently selected module is shown at the cursor position when in placement mode. - **Top**: The ship's layout grid rendered at full scale. Buildable cells are white; non-buildable cells are black. Placed modules are rendered with their `fill_color` and `glyph`. The ghost of the currently selected module is shown at the cursor position when in placement mode.
- **Left** (below the grid): The ship stats panel (see REQ-MOD-UI-STATS-PANEL). - **Left** (below the grid): The ship stats panel (see REQ-MOD-UI-STATS-PANEL), and beneath it the layout's **build cost**, so the price of a configuration is visible while it is being assembled rather than only once it has been confirmed: the total materials required for the ship — the union of the schematic's base materials and those of every placed module, summed per item type (REQ-MOD-MATERIALS) — drawn as item icons on their colored squares with their amounts (REQ-UI-ITEM-ICON), and beside them the total production time (REQ-MOD-PRODUCTION-TIME). Both update in real time as modules are placed and removed. The cost sits beside the stats panel rather than within it because a build cost belongs to a ship being configured: the same panel serves an existing ship (REQ-UI-SHIP-STATS-PANEL) and the balancing tool, neither of which costs anything.
- **Center** (below the grid): A grid of module selection buttons (one per **unlocked** module type; see REQ-DEF-SCHEMATIC-DROP) plus a "Remove" button. Each module button shows the module id and its glyph. - **Center** (below the grid): A grid of module selection buttons (one per **unlocked** module type; see REQ-DEF-SCHEMATIC-DROP) plus a "Remove" button. A module button shows the module's name — its `id` under the usual display convention — on its first line, and beneath it what the module costs: the icons of its `materials` items on their colored squares with their amounts (REQ-UI-ITEM-ICON) and the production time it adds, as `+<n> s` (REQ-MOD-MATERIALS, REQ-MOD-PRODUCTION-TIME). This is the same form a recipe option button uses to state what it makes (REQ-UI-SELECT-OPTIONS), and it puts the price of a module in front of the player before it is placed. The "Remove" button costs nothing and shows its caption alone.
- **Right** (below the grid): The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD). - **Right** (below the grid): The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD).
- **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selection panel are recalculated, and the ship layout preview is refreshed. - **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selection panel are recalculated, and the ship layout preview is refreshed.
@@ -372,9 +371,9 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
Each option in the dialog displays the unlock group's display name — derived from its `id` (same display convention as building, module, and recipe ids) — and the list of items it would grant: its ship, module, building, and assembler-recipe ids (each shown with the same display convention as its respective selection dialog). The artifact option (if present) is displayed as a distinct entry with the name "Artifact". Each option in the dialog displays the unlock group's display name — derived from its `id` (same display convention as building, module, and recipe ids) — and the list of items it would grant: its ship, module, building, and assembler-recipe ids (each shown with the same display convention as its respective selection dialog). The artifact option (if present) is displayed as a distinct entry with the name "Artifact".
Each option additionally displays a vertical list of recipe names labeled "Unlocks recipes:", showing which miner and assembler recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the miner recipes and implicitly-gated assembler recipes that are not currently implicitly unlocked but would become so after applying this option's effect. To compute this, all `materials` of the group's granted ship and module schematics are added to the base set per REQ-LOCK-IMPLICIT step 1a, and the output items of the group's granted assembler recipes are added per step 1b, before recomputation. Each option additionally displays a vertical list of recipe lines labeled "Unlocks recipes:", showing which miner and assembler recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the miner recipes and implicitly-gated assembler recipes that are not currently implicitly unlocked but would become so after applying this option's effect. To compute this, all `materials` of the group's granted ship and module schematics are added to the base set per REQ-LOCK-IMPLICIT step 1a, and the output items of the group's granted assembler recipes are added per step 1b, before recomputation.
Each recipe is listed by its `id` (using the same display convention as the assembler recipe-selection dialog), sorted alphabetically. Hovering a recipe in this list displays the recipe info tooltip described for a recipe in REQ-UI-SELECT-TOOLTIP (the recipe name; the name and quantity of each input item; the completion time; and the name and quantity of the produced output item). If no recipes would be newly unlocked, the list shows "None". Each recipe is shown as a **recipe line** of the same form the item production tooltip uses (REQ-UI-ITEM-TOOLTIP): the icon of the building that runs it (REQ-UI-BUILD-ICON), the recipe's name — by its `id`, using the same display convention as the assembler recipe-selection dialog — and the recipe drawn as the recipe summary line draws it (REQ-UI-RECIPE-SUMMARY). The lines are sorted alphabetically by recipe name. The line says everything there is to say about the recipe, so nothing in this list carries a tooltip, as in the selection dialog (REQ-UI-SELECT-OPTIONS). If no recipes would be newly unlocked, the list shows "None".
The player selects one option by clicking it. If the player selects the artifact option, the player's artifact count is incremented by 1 (REQ-WIN-ARTIFACT-COUNT) and the dialog closes; no unlock is applied. Otherwise the selected unlock group is awarded and the dialog closes: every ship, module, building, and assembler recipe the group grants becomes unlocked at once — ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG); building types become available in the build menu (REQ-LOCK-BUILDING); assembler recipes become available in the assembler recipe-selection dialog (subject to REQ-LOCK-UI-RECIPE). The unlock group is removed from the pool permanently (REQ-LOCK-EXPLICIT), and the implicit unlock set is recomputed (REQ-LOCK-IMPLICIT). The player selects one option by clicking it. If the player selects the artifact option, the player's artifact count is incremented by 1 (REQ-WIN-ARTIFACT-COUNT) and the dialog closes; no unlock is applied. Otherwise the selected unlock group is awarded and the dialog closes: every ship, module, building, and assembler recipe the group grants becomes unlocked at once — ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG); building types become available in the build menu (REQ-LOCK-BUILDING); assembler recipes become available in the assembler recipe-selection dialog (subject to REQ-LOCK-UI-RECIPE). The unlock group is removed from the pool permanently (REQ-LOCK-EXPLICIT), and the implicit unlock set is recomputed (REQ-LOCK-IMPLICIT).
@@ -490,7 +489,7 @@ The screen is a single column: a header bar across the top and the game world vi
Because the contest-zone boundaries shift as the scrollable area grows with each push (REQ-GW-PUSH-EXPAND, REQ-GW-SCROLL-LIMIT), the ramp bands are recomputed from the current contest-zone boundaries. This is a presentation-only concern and does not affect the simulation, consistent with REQ-UI-NO-ZOOM. Because the contest-zone boundaries shift as the scrollable area grows with each push (REQ-GW-PUSH-EXPAND, REQ-GW-SCROLL-LIMIT), the ramp bands are recomputed from the current contest-zone boundaries. This is a presentation-only concern and does not affect the simulation, consistent with REQ-UI-NO-ZOOM.
- REQ-UI-WORLD-ICON: In the game world, a building is drawn with an icon's glyph symbol centered on its footprint, in place of the letter identity glyph. The icon is an SVG loaded from `data/icons/buildings/`; only the icon's glyph is drawn in the world — in a contrasting ink (white over dark fills, dark over light fills) so it stays legible — and its colored chip background is omitted, because the footprint is already filled with the building's `visuals.toml` fill color. This applies to the production buildings (Miner, Smelter, Assembler, Reprocessing Plant, Shipyard, Salvage Bay), the HQ, and the player and enemy defence stations, wherever the identity label appears: operational buildings, construction sites (REQ-UI-CONSTRUCTION-PROGRESS), and the builder-mode and blueprint-placement ghosts. **Belts, splitters, and tunnels are excluded** — they keep their existing tile rendering so their orientation and flow stay readable (a centered icon would obscure direction). Their build-menu buttons still use icons (REQ-UI-BUILD-ICON); in particular the shared Tunnel button's `tunnel_entry.svg` is a build-button icon only, not a world icon. The directional output-port glyphs (REQ-UI-PORT-GLYPH, REQ-UI-PORT-TARGET-GLYPH) are a separate indicator and are unaffected. A building or station with no icon file falls back to its `visuals.toml` text glyph; a type with neither icon nor glyph shows no identity label. A missing icon is not an error, consistent with REQ-UI-BUILD-ICON. - REQ-UI-WORLD-ICON: In the game world, a building is drawn with an icon's glyph symbol centered on its footprint, in place of the letter identity glyph. The icon is an SVG loaded from `data/icons/buildings/`; only the icon's glyph is drawn in the world — in a contrasting ink (white over dark fills, dark over light fills) so it stays legible — and its colored chip background is omitted, because the footprint is already filled with the building's `visuals.toml` fill color. This applies to the production buildings (Miner, Smelter, Assembler, Reprocessing Plant, Shipyard, Salvage Bay), the HQ, and the player and enemy defence stations, wherever the identity label appears: operational buildings, construction sites (REQ-UI-CONSTRUCTION-PROGRESS), and the builder-mode and blueprint-placement ghosts. **Belts, splitters, and tunnels are excluded** — they keep their existing tile rendering so their orientation and flow stay readable (a centered icon would obscure direction). Their build-menu buttons still use icons (REQ-UI-BUILD-ICON); in particular the shared Tunnel button's `tunnel_entry.svg` is a build-button icon only, not a world icon. The directional output-port glyphs (REQ-UI-PORT-GLYPH, REQ-UI-PORT-TARGET-GLYPH) are a separate indicator and are unaffected. A building or station with no icon file falls back to its `visuals.toml` text glyph; a type with neither icon nor glyph shows no identity label. A missing icon is not an error, consistent with REQ-UI-BUILD-ICON.
- REQ-UI-ITEM-ICON: In the game world, an item is drawn as its `visuals.toml` colored square (`fill` + `outline`, the square of REQ-GW-TILE-SIZE) carrying its **item icon** on top. The square is drawn for every item, with or without an icon: it is what gives the item contrast against the tile beneath it, and its outline is what separates neighbouring items where they overlap on a belt. The icon is a self-contained, full-color SVG (rendered as-is, unlike the glyph-only building icons of REQ-UI-WORLD-ICON), loaded at runtime from `data/icons/items/` — a sibling of the config directory, read the same way as the building icons (REQ-UI-BUILD-ICON) — one file per item type named after the item's id (e.g. `iron_ore.svg`). The square fills the item's half-tile rect, keeping the size, spacing, and draw-order rules of REQ-GW-TILE-SIZE; the icon is drawn **inset** within that rect so a frame of the square's color stays visible all around it — required because each icon's viewBox is cropped tight to its artwork, so an icon drawn at the full rect would cover the square entirely. This applies wherever an item is drawn: on belts, splitters, and tunnel ends, and while emerging from or sinking into a building port (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-INPUT-INTAKE). An item type with no icon file shows the colored square alone; a missing icon is not an error, consistent with REQ-UI-BUILD-ICON. **The colored square travels with the icon into the UI.** Wherever the UI displays an item *as an item* the recipe-selection dialog's option buttons (REQ-UI-RECIPE-ICON), the selection panel's item chips (REQ-UI-SINGLE-SELECTION, REQ-UI-HQ-PANEL), and the recipe summary's input and output icons (REQ-UI-RECIPE-SUMMARY) — the icon is drawn on that same square, by the same rules: the item's `fill` and `outline` from `visuals.toml`, the icon inset within the square so a frame of the square's color stays visible all around it, and the square alone when the item has no icon file. The square fills the rect the display allots to the icon, as it fills the half-tile rect in the world. One item therefore reads the same in a panel as it does on a belt. The one exception is the inline `building_block` icon that stands in for the word `Blocks` beside a number (REQ-UI-BLOCKS-ICON, REQ-UI-EXPAND-BUTTON, REQ-UI-BUILD-COST, REQ-UI-MULTI-SELECTION, REQ-UI-BLUEPRINT-CARD): there the icon is a decoration on a line of text rather than an item display, and it is drawn bare, without the square. For performance, each item icon is rasterized to a pixmap cached per target pixel size — re-rasterized only when the tile pixel size changes (e.g. on view resize) — rather than re-rendered from vector every frame. - REQ-UI-ITEM-ICON: In the game world, an item is drawn as its `visuals.toml` colored square (`fill` + `outline`, the square of REQ-GW-TILE-SIZE) carrying its **item icon** on top. The square is drawn for every item, with or without an icon: it is what gives the item contrast against the tile beneath it, and its outline is what separates neighbouring items where they overlap on a belt. The icon is a self-contained, full-color SVG (rendered as-is, unlike the glyph-only building icons of REQ-UI-WORLD-ICON), loaded at runtime from `data/icons/items/` — a sibling of the config directory, read the same way as the building icons (REQ-UI-BUILD-ICON) — one file per item type named after the item's id (e.g. `iron_ore.svg`). The square fills the item's half-tile rect, keeping the size, spacing, and draw-order rules of REQ-GW-TILE-SIZE; the icon is drawn **inset** within that rect so a frame of the square's color stays visible all around it — required because each icon's viewBox is cropped tight to its artwork, so an icon drawn at the full rect would cover the square entirely. This applies wherever an item is drawn: on belts, splitters, and tunnel ends, and while emerging from or sinking into a building port (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-INPUT-INTAKE). An item type with no icon file shows the colored square alone; a missing icon is not an error, consistent with REQ-UI-BUILD-ICON. **The colored square travels with the icon into the UI.** Wherever the UI displays an item *as an item* — the selection panel's item chips (REQ-UI-SINGLE-SELECTION, REQ-UI-HQ-PANEL), the recipe summary's input and output icons (REQ-UI-RECIPE-SUMMARY), and those same icons wherever a recipe is drawn in that form: on the selection dialog's option buttons (REQ-UI-SELECT-OPTIONS), in the item production tooltip (REQ-UI-ITEM-TOOLTIP), and in the unlock-choice dialog's recipe lines (REQ-DEF-SCHEMATIC-DROP) — the icon is drawn on that same square, by the same rules: the item's `fill` and `outline` from `visuals.toml`, the icon inset within the square so a frame of the square's color stays visible all around it, and the square alone when the item has no icon file. The square fills the rect the display allots to the icon, as it fills the half-tile rect in the world. One item therefore reads the same in a panel as it does on a belt. The one exception is the inline `building_block` icon that stands in for the word `Blocks` beside a number (REQ-UI-BLOCKS-ICON, REQ-UI-EXPAND-BUTTON, REQ-UI-BUILD-COST, REQ-UI-MULTI-SELECTION, REQ-UI-BLUEPRINT-CARD): there the icon is a decoration on a line of text rather than an item display, and it is drawn bare, without the square. For performance, each item icon is rasterized to a pixmap cached per target pixel size — re-rasterized only when the tile pixel size changes (e.g. on view resize) — rather than re-rendered from vector every frame.
- REQ-UI-CONSTRUCTION-PROGRESS: Construction sites display the building's identity symbol centered on the footprint (same as an operational building) — its icon glyph, or the text glyph as a fallback (REQ-UI-WORLD-ICON). Below the symbol — or centered on the footprint if the building has neither an icon nor a glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes. - REQ-UI-CONSTRUCTION-PROGRESS: Construction sites display the building's identity symbol centered on the footprint (same as an operational building) — its icon glyph, or the text glyph as a fallback (REQ-UI-WORLD-ICON). Below the symbol — or centered on the footprint if the building has neither an icon nor a glyph — a construction progress percentage is shown (integer, e.g. `42%`), increasing from 0% to 100% as construction completes.
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port. - REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
- REQ-UI-PORT-TARGET-GLYPH: While in builder mode (REQ-BLD-BUILDER-MODE), the builder-mode ghost additionally shows, for each of the building's output ports, a directional glyph drawn centered in the port's **target cell** — the cell immediately outside the footprint that the port pushes into, i.e. the cell the surface-mask output-port indicator occupies (see Surface Mask Format). As in REQ-UI-PORT-GLYPH the glyph is a `>` rotated to face the port's exit direction (`>` East, `^` North, `<` West, `v` South), previewing where the port's output will go before placement. This is in addition to the on-tile port glyph of REQ-UI-PORT-GLYPH, and — unlike that indicator — is shown only for the builder-mode ghost, not for operational buildings, construction sites, or the blueprint-placement ghost (REQ-UI-BLUEPRINT-PLACE). A building with multiple output ports (e.g. a splitter) shows one target-cell glyph per port. The target-cell glyph is drawn larger than the on-tile port glyph so it stands out as the flow-direction preview. Exceptions: the Tunnel Entry shows no target-cell glyph, because it receives items (which may arrive from any of its non-mouth edges, REQ-BLD-TUNNEL-ENTRY) rather than emitting into a single adjacent cell; the Shipyard shows none either, because its output port is a ship-spawn point (REQ-SHP-SPAWN-PLAYER) rather than a belt-item output (REQ-MAT-OUTPUT-EMERGE). - REQ-UI-PORT-TARGET-GLYPH: While in builder mode (REQ-BLD-BUILDER-MODE), the builder-mode ghost additionally shows, for each of the building's output ports, a directional glyph drawn centered in the port's **target cell** — the cell immediately outside the footprint that the port pushes into, i.e. the cell the surface-mask output-port indicator occupies (see Surface Mask Format). As in REQ-UI-PORT-GLYPH the glyph is a `>` rotated to face the port's exit direction (`>` East, `^` North, `<` West, `v` South), previewing where the port's output will go before placement. This is in addition to the on-tile port glyph of REQ-UI-PORT-GLYPH, and — unlike that indicator — is shown only for the builder-mode ghost, not for operational buildings, construction sites, or the blueprint-placement ghost (REQ-UI-BLUEPRINT-PLACE). A building with multiple output ports (e.g. a splitter) shows one target-cell glyph per port. The target-cell glyph is drawn larger than the on-tile port glyph so it stands out as the flow-direction preview. Exceptions: the Tunnel Entry shows no target-cell glyph, because it receives items (which may arrive from any of its non-mouth edges, REQ-BLD-TUNNEL-ENTRY) rather than emitting into a single adjacent cell; the Shipyard shows none either, because its output port is a ship-spawn point (REQ-SHP-SPAWN-PLAYER) rather than a belt-item output (REQ-MAT-OUTPUT-EMERGE).
@@ -580,7 +579,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
- **Debris** — several pieces of debris and nothing else. Their remaining scrap sums into one value (REQ-UI-DEBRIS-PANEL). - **Debris** — several pieces of debris and nothing else. Their remaining scrap sums into one value (REQ-UI-DEBRIS-PANEL).
Every other multi-selection falls back to the count summary. In particular a selection mixing a splitter with belts does not aggregate (a splitter carries per-object output filters, which have no aggregate), and neither do several production buildings of one type (per-building buffers and cycle progress have no aggregate). Every other multi-selection falls back to the count summary. In particular a selection mixing a splitter with belts does not aggregate (a splitter carries per-object output filters, which have no aggregate), and neither do several production buildings of one type (per-building buffers and cycle progress have no aggregate).
- REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows its symbol and name in the header (REQ-UI-SELECTION-CARD), its current recipe or schematic selection (REQ-UI-SELECT-BUTTON) and recipe summary (REQ-UI-RECIPE-SUMMARY) in the configuration group, and its input and output buffer contents in the runtime group. Each buffered item is shown as an **item chip** bearing that item's icon on its colored square (REQ-UI-ITEM-ICON) and its current count: - REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows its symbol and name in the header (REQ-UI-SELECTION-CARD), its current recipe or schematic selection (REQ-UI-SELECT-BUTTON) and recipe summary (REQ-UI-RECIPE-SUMMARY) in the configuration group, and its input and output buffer contents in the runtime group. Each buffered item is shown as an **item chip** bearing that item's icon on its colored square (REQ-UI-ITEM-ICON) and its current count, and hovering a chip shows that item's production tooltip (REQ-UI-ITEM-TOOLTIP):
- an **input** chip shows the per-cycle amount below the count (the items consumed per run, e.g. `/ 2 per cycle`), or the count alone when the building has no selected recipe or schematic to give one; - an **input** chip shows the per-cycle amount below the count (the items consumed per run, e.g. `/ 2 per cycle`), or the count alone when the building has no selected recipe or schematic to give one;
- an **output** chip shows the count against the output buffer's capacity as `a / b` (REQ-MAT-OUTPUT-BUFFER), with the item's name below. - an **output** chip shows the count against the output buffer's capacity as `a / b` (REQ-MAT-OUTPUT-BUFFER), with the item's name below.
@@ -594,13 +593,22 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
- 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-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 and the selection does not aggregate (REQ-UI-SELECTION-AGGREGATE), the panel shows a count summary. Its header names the size of the selection as `<n> buildings` in place of an object name, and carries no symbol and nothing in its right slot. Below it is one row per selected building type — the type's symbol, its name, and the number selected as `x<count>` — one type per row, and no per-building detail. A final row shows the **total building block cost** of the selection, captioned `Total cost` with the value followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON): 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-MULTI-SELECTION: When multiple buildings are selected and the selection does not aggregate (REQ-UI-SELECTION-AGGREGATE), the panel shows a count summary. Its header names the size of the selection as `<n> buildings` in place of an object name, and carries no symbol and nothing in its right slot. Below it is one row per selected building type — the type's symbol, its name, and the number selected as `x<count>` — one type per row, and no per-building detail. A final row shows the **total building block cost** of the selection, captioned `Total cost` with the value followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON): 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, in its configuration group (REQ-UI-SELECTION-CARD). 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-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel, in its configuration group (REQ-UI-SELECTION-CARD). 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 selection 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) and that opens centered on the selection panel, as every modal opened from the panel does (REQ-UI-PANEL-MODAL). 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 selection 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-BUTTON: **Recipe and schematic selection control.** Recipe selection (Miner ore type, Assembler recipe) and schematic selection (Shipyard) are each presented in the selection 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) and that opens centered on the selection panel, as every modal opened from the panel does (REQ-UI-PANEL-MODAL). The dialog contains a vertical list of option buttons, one per selectable option, each describing itself (REQ-UI-SELECT-OPTIONS) — only options that are currently unlocked are shown (REQ-LOCK-UI-RECIPE for recipes, REQ-LOCK-UI-SCHEMATIC for schematics). Neither the option buttons nor the selection button carries a tooltip: an option button states what it makes on its own face, and what the building has selected is drawn beneath the selection button as the recipe summary (REQ-UI-RECIPE-SUMMARY), with the production paths of the items involved reachable by hovering the card's item chips (REQ-UI-ITEM-TOOLTIP). Clicking an option button selects that recipe/schematic, closes the dialog, and updates the selection button's caption in the selection 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 selection panel when a selection is set, displays an info tooltip: - REQ-UI-SELECT-OPTIONS: **Option list of the selection dialog.** The selection dialog (REQ-UI-SELECT-BUTTON) lists its options as a **single vertical column** of buttons, one per option, rather than a grid: each button is as wide as the dialog and states what the option does, which needs a line of its own. No option button carries a tooltip — the button face is the whole description, so the player reads every option's inputs, product, and time from the list itself without hovering anything. Should the column be taller than the space the window leaves the dialog — a fully unlocked Assembler offers more options than any window can hold, and a modal is never resized to fit (REQ-UI-PANEL-MODAL) — the list **scrolls** within the dialog rather than the dialog growing past the window.
- For a **recipe** (Miner or Assembler): the recipe name; the name and quantity of each input item (no inputs are listed for miner recipes, which consume nothing); the completion time (`duration_seconds`); and the name and quantity of the produced output item. - A **recipe** option (Miner, Assembler) shows the **recipe name** on its first line and, beneath it, that recipe drawn as the recipe summary line draws it (REQ-UI-RECIPE-SUMMARY): each input item's icon on its colored square with its per-cycle amount, an arrow, each output item's icon with its amount, and the cycle time. A miner recipe consumes nothing, so its line begins at the arrow.
- For a **ship schematic** (Shipyard): the ship's `display_name`; the name and quantity of each base required material (`[ship.schematic].materials`, excluding any module contributions); the base production time (`[ship.schematic].production_time_seconds`); and "Produces: 1 <ship display name>". - A **ship schematic** option (Shipyard) shows the ship's `display_name` on its first line and, beneath it, the icons and quantities of its base required materials (`[ship.schematic].materials`, excluding any module contributions) with the base production time (`[ship.schematic].production_time_seconds`). A ship is not an item and has no icon of its own, so this line names no output: the button's caption is what it produces.
- REQ-UI-RECIPE-ICON: In the recipe-selection dialog (REQ-UI-SELECT-BUTTON) for a Miner or Assembler, each recipe option button shows the icon of the recipe's produced item **instead of** its name caption (icon-only). The item shown is the recipe's `icon` field if set, otherwise its first output item; the icon is that item's icon on its colored square per REQ-UI-ITEM-ICON. When the item has no icon file, the button shows that colored square alone rather than a name caption, so every option button keeps the same face. The recipe name and details remain available on hover via the selection info tooltip (REQ-UI-SELECT-TOOLTIP). The `(None)` option keeps its text caption. This applies only to recipe options; the Shipyard schematic-selection dialog is unaffected and continues to show ship name captions. - The `(None)` option shows its text caption alone.
- REQ-UI-ITEM-TOOLTIP: **Item production tooltip.** Hovering an item chip in the selection panel — an input or output buffer chip (REQ-UI-SINGLE-SELECTION) or the HQ's block stock chip (REQ-UI-HQ-PANEL) — shows a tooltip telling the player where that item comes from. It has a heading and a body:
- the heading is the **hovered item's name**. For an input chip this is the only place the item is named at all, since such a chip carries a count and no name (REQ-UI-SINGLE-SELECTION).
- the body is the caption `Produced by` followed by one **recipe line** per unlocked recipe that produces the item. Each line carries the icon of the building that runs that recipe (REQ-UI-BUILD-ICON), the recipe's name, and the recipe drawn as the recipe summary line draws it (REQ-UI-RECIPE-SUMMARY): each input item's icon on its colored square with its per-cycle amount, an arrow, each output item's icon with its amount, and the cycle time. An item may have several producers — an iron ingot is smelted from ore, smelted from scrap, and recovered by reprocessing — and the building icon and recipe name are what tell those lines apart and tell the player which building to place for which path.
**Only recipes the player can run are listed**, consistent with the rest of the UI hiding what the player cannot make yet. What that means differs by building, because only Miner and Assembler recipes are unlocked individually (REQ-LOCK-UI-RECIPE): those are listed once unlocked, while a Smelter's or Reprocessing Plant's implicit recipes (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) are listed once **their building** is unlocked (REQ-LOCK-BUILDING) — there is no sense in naming a path through a plant the player cannot place. Two cases have no recipe line to show, and each says so in place of the list rather than leaving the tooltip bare:
- **An item no recipe produces at all.** Scrap is salvaged from debris (REQ-RES-DEBRIS-DROP) rather than crafted, so its tooltip reads `Salvaged from debris` in place of the `Produced by` caption and lists nothing beneath it.
- **An item whose every producing recipe is still locked.** The caption stays `Produced by`, and the single line beneath it reads `Undiscovered`: the player is told the item is made somehow, without being shown a path they have not unlocked yet.
The tooltip belongs to item chips only. The selection dialog shows no tooltips at all, its buttons describing themselves (REQ-UI-SELECT-OPTIONS), and the icons of the recipe summary line (REQ-UI-RECIPE-SUMMARY) show none either: they are parts of one line that already describes one recipe, rather than standalone item displays.
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel's runtime group shows a **"Clear stuck items"** button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels. The button acts on every selected tile, which is why a selection of belts and tunnel ends aggregates into one content rather than a count summary (REQ-UI-SELECTION-AGGREGATE). - REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel's runtime group shows a **"Clear stuck items"** button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels. The button acts on every selected tile, which is why a selection of belts and tunnel ends aggregates into one content rather than a count summary (REQ-UI-SELECTION-AGGREGATE).
- REQ-UI-HQ-PANEL: When the HQ is selected, the panel shows the HQ's **HP** as a bar labelled `current / maximum` (REQ-HQ-STATS, REQ-UI-HP-BARS) and, beneath it, the **global building blocks stock** — the same value as the header bar's stock display (REQ-UI-BLOCKS-ICON), rendered as an item chip (REQ-UI-SINGLE-SELECTION) carrying the `building_block` icon on its colored square — a chip is an item display, so it takes the square even though the header bar's inline block icon does not (REQ-UI-ITEM-ICON, REQ-UI-BLOCKS-ICON). The HP comes first, as it does on every card that has it (REQ-UI-SELECTION-CARD). The HQ has no input or output buffers of its own: building blocks delivered by belt go straight into the global stock (REQ-HQ-BELT-INPUT), and showing that stock on the HQ is what tells the player to route blocks there. The HQ has no configuration group and no status indicator (REQ-UI-SELECTION-STATUS), and it is never a construction site. - REQ-UI-HQ-PANEL: When the HQ is selected, the panel shows the HQ's **HP** as a bar labelled `current / maximum` (REQ-HQ-STATS, REQ-UI-HP-BARS) and, beneath it, the **global building blocks stock** — the same value as the header bar's stock display (REQ-UI-BLOCKS-ICON), rendered as an item chip (REQ-UI-SINGLE-SELECTION) carrying the `building_block` icon on its colored square — a chip is an item display, so it takes the square even though the header bar's inline block icon does not (REQ-UI-ITEM-ICON, REQ-UI-BLOCKS-ICON), and hovering it shows the item production tooltip as any other chip does (REQ-UI-ITEM-TOOLTIP). The HP comes first, as it does on every card that has it (REQ-UI-SELECTION-CARD). The HQ has no input or output buffers of its own: building blocks delivered by belt go straight into the global stock (REQ-HQ-BELT-INPUT), and showing that stock on the HQ is what tells the player to route blocks there. The HQ has no configuration group and no status indicator (REQ-UI-SELECTION-STATUS), and it is never a construction site.
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. A plain click on a ship or defence station makes it the sole selection, clearing any previous selection. Ships and defence stations can be multi-selected — by Ctrl+clicking individual actors to add or remove them, or by box-drag (REQ-UI-MULTI-SELECT) — and can be selected together with debris and with one another in a single field selection (REQ-UI-SELECTION-CATEGORIES), freely mixing player and enemy actors. Actors cannot be selected together with buildings: selecting a ship or defence station clears any building selection, and selecting a building clears the actors (buildings win). Clicking a piece of debris adds to or establishes a field selection (REQ-UI-DEBRIS-CLICK-SELECT). Clicking empty world space (no building, ship, defence station, or piece of debris) clears the selection. - REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. A plain click on a ship or defence station makes it the sole selection, clearing any previous selection. Ships and defence stations can be multi-selected — by Ctrl+clicking individual actors to add or remove them, or by box-drag (REQ-UI-MULTI-SELECT) — and can be selected together with debris and with one another in a single field selection (REQ-UI-SELECTION-CATEGORIES), freely mixing player and enemy actors. Actors cannot be selected together with buildings: selecting a ship or defence station clears any building selection, and selecting a building clears the actors (buildings win). Clicking a piece of debris adds to or establishes a field selection (REQ-UI-DEBRIS-CLICK-SELECT). Clicking empty world space (no building, ship, defence station, or piece of debris) clears the selection.
- REQ-UI-SHIP-STATS-PANEL: When exactly one ship is selected (REQ-UI-ENTITY-CLICK-SELECT) and no debris is selected, the selection panel shows a **ship stats panel**. (If debris is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.) The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. Its header (REQ-UI-SELECTION-CARD) carries the schematic's color swatch and display name, with the ship's current behavior in the right slot (REQ-UI-SHIP-BEHAVIOR). The panel always shows all hull stats: HP (current / maximum) as a **bar** with the two values beside its caption, then max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed as label/value rows. In addition, capability module summaries are shown below the hull stats, each as its own outlined row, conditioned on which module types are installed and using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT). - REQ-UI-SHIP-STATS-PANEL: When exactly one ship is selected (REQ-UI-ENTITY-CLICK-SELECT) and no debris is selected, the selection panel shows a **ship stats panel**. (If debris is also selected, the panel shows the compact count summary instead, per REQ-UI-FIELD-MULTI-SELECTION.) The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. Its header (REQ-UI-SELECTION-CARD) carries the schematic's color swatch and display name, with the ship's current behavior in the right slot (REQ-UI-SHIP-BEHAVIOR). The panel always shows all hull stats: HP (current / maximum) as a **bar** with the two values beside its caption, then max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed as label/value rows. In addition, capability module summaries are shown below the hull stats, each as its own outlined row, conditioned on which module types are installed and using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
- REQ-UI-SHIP-BEHAVIOR: The ship stats panel (REQ-UI-SHIP-STATS-PANEL) additionally displays the selected ship's **current behavior** in its header's right slot (REQ-UI-SELECTION-CARD) — a single label naming the top-priority behavior currently governing the ship's navigation, as resolved by the fixed-priority behavior arbitration. Only the winning behavior is named; lower-priority behaviors that are suppressed are not shown, and neither are the salvage/repair cycles that run regardless of the active behavior (REQ-SHP-SALVAGE, REQ-SHP-REPAIR). The label updates live as the ship's behavior changes, and it is always shown (independent of debug draw mode, unlike the threat-cost line of REQ-UI-SHIP-STATS-PANEL). This applies to both player and enemy ships (REQ-UI-ENTITY-CLICK-SELECT); enemy ships only ever show **Engaging** or **Advancing**. The behavior labels (all wrapped in `tr()`) are: - REQ-UI-SHIP-BEHAVIOR: The ship stats panel (REQ-UI-SHIP-STATS-PANEL) additionally displays the selected ship's **current behavior** in its header's right slot (REQ-UI-SELECTION-CARD) — a single label naming the top-priority behavior currently governing the ship's navigation, as resolved by the fixed-priority behavior arbitration. Only the winning behavior is named; lower-priority behaviors that are suppressed are not shown, and neither are the salvage/repair cycles that run regardless of the active behavior (REQ-SHP-SALVAGE, REQ-SHP-REPAIR). The label updates live as the ship's behavior changes, and it is always shown (independent of debug draw mode, unlike the threat-cost line of REQ-UI-SHIP-STATS-PANEL). This applies to both player and enemy ships (REQ-UI-ENTITY-CLICK-SELECT); enemy ships only ever show **Engaging** or **Advancing**. The behavior labels (all wrapped in `tr()`) are:
@@ -626,7 +634,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
- REQ-UI-BUILD-COST: Each button is **icon-only with a cost**, its face composed of three elements: the button's **hotkey badge** in the top-left corner, the building's icon (REQ-UI-BUILD-ICON) centered below it, and the building block cost centered under the icon, shown with the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON) to the right of the number in place of the trailing `Blocks` word, e.g. `2` then a small block icon. The building name is not shown on the button; it is shown in the button's hover tooltip instead (REQ-UI-BUILD-TOOLTIP). When no icon file exists for `building_block`, the cost is shown as the bare number. The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) has no cost and shows its name as a text caption in the cost's place. - REQ-UI-BUILD-COST: Each button is **icon-only with a cost**, its face composed of three elements: the button's **hotkey badge** in the top-left corner, the building's icon (REQ-UI-BUILD-ICON) centered below it, and the building block cost centered under the icon, shown with the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON) to the right of the number in place of the trailing `Blocks` word, e.g. `2` then a small block icon. The building name is not shown on the button; it is shown in the button's hover tooltip instead (REQ-UI-BUILD-TOOLTIP). When no icon file exists for `building_block`, the cost is shown as the bare number. The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) has no cost and shows its name as a text caption in the cost's place.
- **Hotkey badge.** The badge names the build hotkey that activates the button (REQ-UI-HOTKEYS), so the player can learn the shortcuts from the bar itself. It is rendered dimmer than the cost so it reads as secondary, but at the same size and in bold, because a smaller badge is not legible. A plain-digit hotkey is shown as the bare digit (`1`, `2`, `3`); a Shift+digit hotkey is shown with an upwards arrow prefixed and no separator (`↑1``↑6`); the Deconstruct button shows `Q`. A button whose building type has no build hotkey shows no badge and keeps the same face size, so the row stays even. - **Hotkey badge.** The badge names the build hotkey that activates the button (REQ-UI-HOTKEYS), so the player can learn the shortcuts from the bar itself. It is rendered dimmer than the cost so it reads as secondary, but at the same size and in bold, because a smaller badge is not legible. A plain-digit hotkey is shown as the bare digit (`1`, `2`, `3`); a Shift+digit hotkey is shown with an upwards arrow prefixed and no separator (`↑1``↑6`); the Deconstruct button shows `Q`. A button whose building type has no build hotkey shows no badge and keeps the same face size, so the row stays even.
- REQ-UI-BUILD-ICON: Each build button shows an icon. Icons are SVG files loaded at runtime from `data/icons/buildings/` (a sibling of the config directory, read the same way as `visuals.toml`), one file per button named after the building's id (e.g. `belt.svg`, `reprocessing_plant.svg`). The shared Tunnel button (REQ-UI-BUILD-BAR) uses `tunnel_entry.svg`; the Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) uses `deconstruct.svg`. Each icon is a rounded colored "chip" bearing a white line glyph, the chip color following the building's fill color in `visuals.toml`. A missing icon file leaves the button showing its building name as a text caption in place of the icon, so the button stays identifiable in the icon-only bar (REQ-UI-BUILD-COST); it is not an error. - REQ-UI-BUILD-ICON: Each build button shows an icon. Icons are SVG files loaded at runtime from `data/icons/buildings/` (a sibling of the config directory, read the same way as `visuals.toml`), one file per button named after the building's id (e.g. `belt.svg`, `reprocessing_plant.svg`). The shared Tunnel button (REQ-UI-BUILD-BAR) uses `tunnel_entry.svg`; the Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) uses `deconstruct.svg`. Each icon is a rounded colored "chip" bearing a white line glyph, the chip color following the building's fill color in `visuals.toml`. A missing icon file leaves the button showing its building name as a text caption in place of the icon, so the button stays identifiable in the icon-only bar (REQ-UI-BUILD-COST); it is not an error.
- REQ-UI-BUILD-TOOLTIP: Each building-type button shows a hover tooltip consisting of the building name followed by the descriptive text defined for that building type in `buildings.toml` (the optional per-building tooltip field). Because the button caption is icon-only (REQ-UI-BUILD-COST), the name is always part of the tooltip; if a building type defines no tooltip text, the tooltip shows the name alone. This tooltip is distinct from the recipe/schematic selection tooltip (REQ-UI-SELECT-TOOLTIP). The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) is not a building type and so has no config-defined tooltip; it instead shows its own refund tooltip defined in REQ-UI-DECONSTRUCT-BUTTON. - REQ-UI-BUILD-TOOLTIP: Each building-type button shows a hover tooltip consisting of the building name followed by the descriptive text defined for that building type in `buildings.toml` (the optional per-building tooltip field). Because the button caption is icon-only (REQ-UI-BUILD-COST), the name is always part of the tooltip; if a building type defines no tooltip text, the tooltip shows the name alone. This tooltip is distinct from the item production tooltip of the selection panel's item chips (REQ-UI-ITEM-TOOLTIP); the recipe/schematic selection dialog has no tooltip at all (REQ-UI-SELECT-OPTIONS). The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) is not a building type and so has no config-defined tooltip; it instead shows its own refund tooltip defined in REQ-UI-DECONSTRUCT-BUTTON.
- 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-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. - 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.

View File

@@ -94,14 +94,6 @@ RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
const toml::array& outputs = utility::requireArray(mt["outputs"], file, elemPath + ".outputs"); const toml::array& outputs = utility::requireArray(mt["outputs"], file, elemPath + ".outputs");
def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs"); def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs");
// Optional icon item id (REQ-UI-RECIPE-ICON); defaults to the first output
// in the UI when unset. Not validated against known items here — a missing
// icon is not an error (REQ-UI-ITEM-ICON).
if (mt.contains("icon"))
{
def.icon = utility::requireString(mt["icon"], file, elemPath + ".icon");
}
cfg.recipes.push_back(std::move(def)); cfg.recipes.push_back(std::move(def));
} }

View File

@@ -32,10 +32,6 @@ struct RecipeDef
std::vector<RecipeIngredient> inputs; std::vector<RecipeIngredient> inputs;
std::vector<RecipeOutput> outputs; std::vector<RecipeOutput> outputs;
double durationSeconds; double durationSeconds;
// Optional id of the item whose icon represents this recipe in the recipe-
// selection dialog (REQ-UI-RECIPE-ICON). When unset, the first output item is
// used. A missing icon file for that item is not an error (REQ-UI-ITEM-ICON).
std::optional<std::string> icon;
// Assembler only. When true, this recipe is available from game start // Assembler only. When true, this recipe is available from game start
// regardless of the implicit item graph — used for base recipes that no // regardless of the implicit item graph — used for base recipes that no
// schematic's materials reach (e.g. building blocks). See REQ-LOCK-IMPLICIT. // schematic's materials reach (e.g. building blocks). See REQ-LOCK-IMPLICIT.

View File

@@ -81,6 +81,33 @@ computeShipyardRequiredMaterials(const GameConfig& config,
} }
return requiredMaterials; return requiredMaterials;
} }
double computeShipyardProductionTimeSeconds(
const GameConfig& config, const std::string& recipeId,
const std::optional<ShipLayoutConfig>& shipLayout)
{
const ShipDef* shipDef = config.ships.findShipDef(recipeId);
if (!shipDef)
{
return 0.0;
}
double seconds = shipDef->schematic.productionTimeSeconds;
if (shipLayout.has_value())
{
for (const PlacedModule& pm : shipLayout->placedModules)
{
const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
if (!modDef)
{
continue;
}
seconds += modDef->productionTimeSeconds;
}
}
return seconds;
}
bool hasInputsToStart(const GameConfig& config, const Building& b) bool hasInputsToStart(const GameConfig& config, const Building& b)
{ {
if (b.type == BuildingType::Shipyard) if (b.type == BuildingType::Shipyard)

View File

@@ -44,6 +44,12 @@ std::map<std::string, int> computeShipyardRequiredMaterials(
const GameConfig& config, const std::string& recipeId, const GameConfig& config, const std::string& recipeId,
const std::optional<ShipLayoutConfig>& shipLayout); const std::optional<ShipLayoutConfig>& shipLayout);
// The ship's base production time plus that of every module in the configured layout
// (REQ-MOD-PRODUCTION-TIME), over a stored configuration as above.
double computeShipyardProductionTimeSeconds(
const GameConfig& config, const std::string& recipeId,
const std::optional<ShipLayoutConfig>& shipLayout);
// True when a production cycle could start right now, ignoring output-buffer space. // True when a production cycle could start right now, ignoring output-buffer space.
bool hasInputsToStart(const GameConfig& config, const Building& b); bool hasInputsToStart(const GameConfig& config, const Building& b);

View File

@@ -390,39 +390,6 @@ duration_seconds = 1.0
std::runtime_error); std::runtime_error);
} }
TEST_CASE("Optional recipe icon field parses; absent leaves it unset", "[config]")
{
TempConfigDir dir;
writeFile(dir.path() / "recipes.toml", R"(
[[recipe]]
id = "with_icon"
building = "assembler"
inputs = [{item = "iron_ingot", amount = 1}]
outputs = [{item = "steel_plate", amount = 1}]
duration_seconds = 1.0
icon = "hardened_steel"
[[recipe]]
id = "without_icon"
building = "assembler"
inputs = [{item = "iron_ingot", amount = 1}]
outputs = [{item = "copper_wire", amount = 1}]
duration_seconds = 1.0
)");
const RecipesConfig cfg =
ConfigLoader::loadRecipes((dir.path() / "recipes.toml").string());
const RecipeDef& withIcon = cfg.recipes.at(0);
REQUIRE(withIcon.id == "with_icon");
REQUIRE(withIcon.icon.has_value());
REQUIRE(*withIcon.icon == "hardened_steel");
const RecipeDef& withoutIcon = cfg.recipes.at(1);
REQUIRE(withoutIcon.id == "without_icon");
REQUIRE_FALSE(withoutIcon.icon.has_value());
}
// --- unlock_requires (REQ-LOCK-PREREQ) ------------------------------------ // --- unlock_requires (REQ-LOCK-PREREQ) ------------------------------------
namespace namespace

View File

@@ -25,7 +25,9 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.h
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.h ${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.h
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h ${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.h ${CMAKE_CURRENT_SOURCE_DIR}/RecipeLineRow.h
${CMAKE_CURRENT_SOURCE_DIR}/OptionButton.h
${CMAKE_CURRENT_SOURCE_DIR}/ItemProducers.h
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.h ${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingIconCache.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingIconCache.h
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.h ${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.h
@@ -59,7 +61,9 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.cpp
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RecipeLineRow.cpp
${CMAKE_CURRENT_SOURCE_DIR}/OptionButton.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ItemProducers.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingIconCache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingIconCache.cpp
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.cpp ${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.cpp

View File

@@ -114,7 +114,7 @@ QPixmap ItemIconCache::getPixmap(const std::string& cacheKey, const std::string&
if (withSquare) if (withSquare)
{ {
// Null unless the item has something to draw -- a square, an icon, or both -- // Null unless the item has something to draw -- a square, an icon, or both --
// so a caller with nothing to show can fall back to text (REQ-UI-RECIPE-ICON). // so a caller with nothing to show can fall back to text (REQ-UI-ITEM-ICON).
const bool hasSquare = m_visuals != nullptr const bool hasSquare = m_visuals != nullptr
&& m_visuals->items.find(itemId) != m_visuals->items.end(); && m_visuals->items.find(itemId) != m_visuals->items.end();
if (hasSquare || hasIcon(itemId)) if (hasSquare || hasIcon(itemId))

View File

@@ -18,9 +18,11 @@ struct VisualsConfig;
// //
// The square is the item's `fill` and `outline` from visuals.toml with the icon inset // The square is the item's `fill` and `outline` from visuals.toml with the icon inset
// within it, and it backs the icon both in the game world and wherever the UI displays // within it, and it backs the icon both in the game world and wherever the UI displays
// an item as an item: the recipe-selection dialog's option buttons (REQ-UI-RECIPE-ICON), // an item as an item: the selection panel's item chips (REQ-UI-SINGLE-SELECTION,
// the selection panel's item chips (REQ-UI-SINGLE-SELECTION, REQ-UI-HQ-PANEL), and the // REQ-UI-HQ-PANEL), and every recipe drawn as a line -- the recipe summary, the
// recipe summary (REQ-UI-RECIPE-SUMMARY). Defined here once rather than per widget, in // selection dialog's option buttons, the item production tooltip
// (REQ-UI-RECIPE-SUMMARY, REQ-UI-SELECT-OPTIONS, REQ-UI-ITEM-TOOLTIP). Defined here
// once rather than per widget, in
// two forms: paintItem() for the world's fractional geometry, getSquarePixmap() for // two forms: paintItem() for the world's fractional geometry, getSquarePixmap() for
// widgets that want a ready-made pixmap. // widgets that want a ready-made pixmap.
// //

60
src/ui/ItemProducers.cpp Normal file
View File

@@ -0,0 +1,60 @@
#include "ItemProducers.h"
#include "GameConfig.h"
#include "RecipesConfig.h"
#include "Simulation.h"
namespace
{
bool producesItem(const RecipeDef& recipe, const std::string& itemId)
{
for (const RecipeOutput& output : recipe.outputs)
{
if (output.item == itemId)
{
return true;
}
}
return false;
}
bool isAvailable(const RecipeDef& recipe, const Simulation& sim)
{
if (recipe.building == BuildingType::Miner
|| recipe.building == BuildingType::Assembler)
{
return sim.isRecipeUnlocked(recipe.id);
}
return sim.isBuildingUnlocked(recipe.building);
}
} // namespace
ItemProduction findItemProduction(const std::string& itemId, const Simulation& sim,
const GameConfig& config)
{
ItemProduction result;
bool anyProducer = false;
for (const RecipeDef& recipe : config.recipes.recipes)
{
if (!producesItem(recipe, itemId)) { continue; }
anyProducer = true;
if (isAvailable(recipe, sim))
{
result.recipes.push_back(&recipe);
}
}
if (!result.recipes.empty())
{
result.origin = ItemOrigin::Produced;
}
else
{
result.origin = anyProducer ? ItemOrigin::Undiscovered : ItemOrigin::Salvaged;
}
return result;
}

35
src/ui/ItemProducers.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include <string>
#include <vector>
struct GameConfig;
struct RecipeDef;
class Simulation;
// Where an item comes from, as far as the player has discovered (REQ-UI-ITEM-TOOLTIP).
enum class ItemOrigin
{
Produced, // at least one recipe the player can run makes it
Undiscovered, // recipes make it, but none of them is available yet
Salvaged, // no recipe makes it at all -- scrap, which comes from debris
};
struct ItemProduction
{
ItemOrigin origin = ItemOrigin::Salvaged;
// The available producers, in config order. Empty unless origin is Produced.
std::vector<const RecipeDef*> recipes;
};
// Every way the player can currently produce the given item, for the tooltip that
// explains an item chip (REQ-UI-ITEM-TOOLTIP).
//
// What counts as available differs by building type, because only Miner and Assembler
// recipes are individually unlocked (REQ-LOCK-UI-RECIPE): those are filtered by the
// unlock state, while a Smelter's or Reprocessing Plant's implicit recipes
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) are filtered by whether their building is
// unlocked yet (REQ-LOCK-BUILDING) -- there is no sense in naming a path through a
// plant the player cannot place.
ItemProduction findItemProduction(const std::string& itemId, const Simulation& sim,
const GameConfig& config);

View File

@@ -224,7 +224,8 @@ void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEven
ModalPauseScope pause(*m_gameWorldView); ModalPauseScope pause(*m_gameWorldView);
ModalDimScope dim(*m_dimOverlay); ModalDimScope dim(*m_dimOverlay);
SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes, this); SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes,
m_itemIcons.get(), m_buildingIcons.get(), this);
dialog.exec(); dialog.exec();
std::shared_ptr<ApplySchematicChoiceCommand> command = std::shared_ptr<ApplySchematicChoiceCommand> command =
@@ -317,7 +318,7 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
m_layoutBlueprints, m_layoutBlueprints,
std::move(unlockedModuleIds), std::move(unlockedModuleIds),
m_gameWorldView->isDebugDrawEnabled(), m_gameWorldView->isDebugDrawEnabled(),
this); m_itemIcons.get(), this);
// Opened from the panel's "Configure" button (REQ-MOD-UI-PREVIEW) or straight after // Opened from the panel's "Configure" button (REQ-MOD-UI-PREVIEW) or straight after
// a schematic change (REQ-MOD-UI-AUTO-DIALOG), so it opens on the panel either way. // a schematic change (REQ-MOD-UI-AUTO-DIALOG), so it opens on the panel either way.
placeOnSelectionPanel(dialog); placeOnSelectionPanel(dialog);
@@ -427,7 +428,8 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
bool autoOpenLayout = false; bool autoOpenLayout = false;
std::string chosenSchematic; std::string chosenSchematic;
RecipeSelectionDialog dialog(options, title, m_itemIcons.get(), this); RecipeSelectionDialog dialog(options, title, m_itemIcons.get(),
m_buildingIcons.get(), this);
placeOnSelectionPanel(dialog); placeOnSelectionPanel(dialog);
if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value()) if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
{ {

35
src/ui/OptionButton.cpp Normal file
View File

@@ -0,0 +1,35 @@
#include "OptionButton.h"
#include <QLayout>
#include <QStyle>
#include <QStyleOptionButton>
OptionButton::OptionButton(QWidget* parent)
: QPushButton(parent)
{
}
QSize OptionButton::sizeHint() const
{
if (layout() == nullptr)
{
return QPushButton::sizeHint();
}
return sizeForFace(layout()->sizeHint());
}
QSize OptionButton::minimumSizeHint() const
{
if (layout() == nullptr)
{
return QPushButton::minimumSizeHint();
}
return sizeForFace(layout()->minimumSize());
}
QSize OptionButton::sizeForFace(const QSize& faceSize) const
{
QStyleOptionButton option;
initStyleOption(&option);
return style()->sizeFromContents(QStyle::CT_PushButton, &option, faceSize, this);
}

28
src/ui/OptionButton.h Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <QPushButton>
#include <QSize>
// A push button whose face is built of widgets rather than of a caption: an option's
// name over the recipe line stating what it makes (REQ-UI-SELECT-OPTIONS), or a module's
// name over what it costs (REQ-MOD-UI-DIALOG).
//
// It exists only to be measured correctly. QPushButton computes its size hint from its
// text and icon and ignores a layout set on it, so a button carrying a face of widgets
// would be sized as if it were empty and clip everything in it. This asks the layout
// instead, and then lets the style add what the button's own frame needs.
class OptionButton : public QPushButton
{
Q_OBJECT
public:
explicit OptionButton(QWidget* parent = nullptr);
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
private:
// The style's button size for the given face size, or the plain QPushButton hint
// when there is no face to measure.
QSize sizeForFace(const QSize& faceSize) const;
};

140
src/ui/RecipeLineRow.cpp Normal file
View File

@@ -0,0 +1,140 @@
#include "RecipeLineRow.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QLayoutItem>
#include <QPixmap>
#include "BuildingIconCache.h"
#include "ItemIconCache.h"
namespace
{
// Size the items' colored squares and the building's chip are drawn at on a line, in
// device-independent pixels. Larger than the artwork they carry, which the square
// insets (REQ-UI-ITEM-ICON).
const int kIconSizePx = 18;
// Adds a freshly built label to the line and shows it.
//
// The show is what makes it count: a widget created under an already-visible parent
// starts hidden, and a layout treats a hidden item as empty, so an unshown label would
// add nothing to the size hint the surrounding widget measures itself against as soon
// as this returns.
void addAndShow(QHBoxLayout* layout, QLabel* label)
{
layout->addWidget(label);
label->show();
}
} // namespace
RecipeLineRow::RecipeLineRow(ItemIconCache* itemIcons, BuildingIconCache* buildingIcons,
QWidget* parent)
: QWidget(parent)
, m_itemIcons(itemIcons)
, m_buildingIcons(buildingIcons)
{
m_layout = new QHBoxLayout(this);
m_layout->setContentsMargins(0, 0, 0, 0);
m_layout->setSpacing(4);
hide();
}
void RecipeLineRow::setLine(const Spec& spec)
{
if (spec.isEmpty())
{
// Forgotten as well as hidden, so re-selecting the same recipe later is seen as
// a change and shows the row again.
m_spec = Spec();
hide();
return;
}
if (spec == m_spec)
{
return;
}
m_spec = spec;
rebuild(spec);
show();
}
void RecipeLineRow::rebuild(const Spec& spec)
{
while (QLayoutItem* item = m_layout->takeAt(0))
{
if (item->widget())
{
item->widget()->deleteLater();
}
delete item;
}
// The building the recipe runs in, where the line has to distinguish one producer
// from another (REQ-UI-ITEM-TOOLTIP). A building with no chip file leaves the icon
// off, as everywhere else (REQ-UI-BUILD-ICON).
if (spec.building.has_value() && m_buildingIcons != nullptr)
{
const QPixmap chip =
m_buildingIcons->getChip(buildingTypeId(*spec.building), kIconSizePx);
if (!chip.isNull())
{
QLabel* chipLabel = new QLabel(this);
chipLabel->setPixmap(chip);
addAndShow(m_layout, chipLabel);
}
}
if (!spec.name.isEmpty())
{
addAndShow(m_layout, new QLabel(spec.name, this));
}
addAmounts(spec.inputs);
if (!spec.inputs.empty() && !spec.outputs.empty())
{
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
addAndShow(m_layout, new QLabel(QString(rightArrow), this));
}
addAmounts(spec.outputs);
if (spec.durationSeconds.has_value() && *spec.durationSeconds > 0.0)
{
const QChar middleDot(0x00B7); // U+00B7 MIDDLE DOT
const QString time = spec.durationIsAddition
? tr("+%1 s").arg(*spec.durationSeconds, 0, 'f', 1)
: tr("%1 s").arg(*spec.durationSeconds, 0, 'f', 1);
addAndShow(m_layout, new QLabel(
QStringLiteral("%1 %2").arg(middleDot).arg(time), this));
}
m_layout->addStretch(1);
}
void RecipeLineRow::addAmounts(const std::vector<Amount>& amounts)
{
for (const Amount& entry : amounts)
{
// The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file
// is not an error: the square stands alone then, and only an item with no square
// either falls back to its id in text.
const QPixmap icon = (m_itemIcons != nullptr)
? m_itemIcons->getSquarePixmap(entry.itemId, kIconSizePx)
: QPixmap();
if (!icon.isNull())
{
QLabel* iconLabel = new QLabel(this);
iconLabel->setPixmap(icon);
addAndShow(m_layout, iconLabel);
}
else
{
addAndShow(m_layout,
new QLabel(QString::fromStdString(entry.itemId), this));
}
addAndShow(m_layout, new QLabel(QString::number(entry.amount), this));
}
}

98
src/ui/RecipeLineRow.h Normal file
View File

@@ -0,0 +1,98 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
#include <QString>
#include <QWidget>
#include "BuildingType.h"
class BuildingIconCache;
class ItemIconCache;
class QHBoxLayout;
// One recipe drawn on a line: what it consumes, an arrow, what it produces, and how
// long a cycle takes (REQ-UI-RECIPE-SUMMARY). Every place the UI states what something
// makes draws this same line, so a recipe reads alike wherever it is shown:
//
// * beneath the selection button, for the recipe a building is running
// (REQ-UI-RECIPE-SUMMARY);
// * on a selection dialog's option buttons, under the option's name
// (REQ-UI-SELECT-OPTIONS);
// * in an item's production tooltip and in the unlock-choice dialog, there led by the
// icon of the building that runs the recipe and its name
// (REQ-UI-ITEM-TOOLTIP, REQ-DEF-SCHEMATIC-DROP);
// * on a module button and in the layout dialog's build cost, where there is a price
// and a time but nothing produced (REQ-MOD-UI-DIALOG, REQ-MOD-UI-STATS-PANEL).
class RecipeLineRow : public QWidget
{
Q_OBJECT
public:
// One item and how many of it the line states.
struct Amount
{
std::string itemId;
int amount = 0;
bool operator==(const Amount& other) const
{
return itemId == other.itemId && amount == other.amount;
}
};
// What the line says. Everything but the inputs is optional, which is what lets the
// one widget serve the summary, the option buttons, the tooltip lines and the cost
// lines.
struct Spec
{
// Drawn as a leading chip when the line has to say which building runs the
// recipe -- an item may have several producers (REQ-UI-ITEM-TOOLTIP). Unset
// wherever the surrounding widget already establishes the building.
std::optional<BuildingType> building;
// Leading recipe name, for the same reason. Empty where the name is the
// caption above the line instead.
QString name;
std::vector<Amount> inputs;
// Empty for a line that produces no item of its own: a ship schematic, or a
// module's price. No arrow is drawn then.
std::vector<Amount> outputs;
std::optional<double> durationSeconds;
// True where the time is added to something else rather than being a cycle of
// its own, and so reads "+3.0 s" (REQ-MOD-UI-DIALOG).
bool durationIsAddition = false;
bool operator==(const Spec& other) const
{
return building == other.building && name == other.name
&& inputs == other.inputs && outputs == other.outputs
&& durationSeconds == other.durationSeconds
&& durationIsAddition == other.durationIsAddition;
}
bool isEmpty() const { return inputs.empty() && outputs.empty(); }
};
// Both caches may be null, which leaves the icons off: an item with no square and
// no icon falls back to its id, and a building with no chip to its name alone.
// Neither is owned.
RecipeLineRow(ItemIconCache* itemIcons, BuildingIconCache* buildingIcons,
QWidget* parent = nullptr);
// Hides the row when the spec has nothing to state, which is how a building with no
// recipe shows no summary at all (REQ-UI-RECIPE-SUMMARY).
void setLine(const Spec& spec);
private:
void rebuild(const Spec& spec);
void addAmounts(const std::vector<Amount>& amounts);
ItemIconCache* m_itemIcons;
BuildingIconCache* m_buildingIcons;
QHBoxLayout* m_layout;
// What the row currently shows, so a refresh at tick rate rebuilds it only when the
// recipe actually changed.
Spec m_spec;
};

View File

@@ -2,60 +2,46 @@
#include <cstddef> #include <cstddef>
#include <QGridLayout> #include <QFont>
#include <QIcon> #include <QFrame>
#include <QPixmap> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QSize> #include <QScrollArea>
#include <QStringList>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "Building.h" #include "Building.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "DisplayName.h" #include "DisplayName.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "ItemIconCache.h" #include "OptionButton.h"
#include "RecipesConfig.h" #include "RecipesConfig.h"
#include "RecipeTooltip.h"
#include "ShipsConfig.h" #include "ShipsConfig.h"
#include "Simulation.h" #include "Simulation.h"
namespace namespace
{ {
QString itemLine(const std::string& itemId, int amount) std::vector<RecipeLineRow::Amount> toAmounts(
const std::vector<RecipeIngredient>& ingredients)
{ {
return QStringLiteral(" ") std::vector<RecipeLineRow::Amount> amounts;
+ QString::fromStdString(toDisplayName(itemId)) amounts.reserve(ingredients.size());
+ QStringLiteral(" x ") + QString::number(amount); for (const RecipeIngredient& ingredient : ingredients)
{
amounts.push_back(RecipeLineRow::Amount{ ingredient.item, ingredient.amount });
}
return amounts;
} }
QString shipTooltip(const ShipDef& def) std::vector<RecipeLineRow::Amount> toAmounts(const std::vector<RecipeOutput>& outputs)
{ {
const QString name = QString::fromStdString(toDisplayName(def.id)); std::vector<RecipeLineRow::Amount> amounts;
amounts.reserve(outputs.size());
QStringList lines; for (const RecipeOutput& output : outputs)
lines << QObject::tr("Ship: %1").arg(name);
if (def.schematic.materials.empty())
{ {
lines << QObject::tr("Materials: none"); amounts.push_back(RecipeLineRow::Amount{ output.item, output.amount });
} }
else return amounts;
{
lines << QObject::tr("Materials:");
for (const RecipeIngredient& material : def.schematic.materials)
{
lines << itemLine(material.item, material.amount);
}
lines << QObject::tr(" + installed modules");
}
lines << QObject::tr("Completion time: %1 s")
.arg(def.schematic.productionTimeSeconds);
lines << QObject::tr("Produces: 1 %1").arg(name);
return lines.join('\n');
} }
} // namespace } // namespace
@@ -64,16 +50,24 @@ std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
BuildingType type, Simulation& sim, const GameConfig& config) BuildingType type, Simulation& sim, const GameConfig& config)
{ {
std::vector<RecipeSelectionOption> options; std::vector<RecipeSelectionOption> options;
options.push_back({std::string(), QObject::tr("(None)"), QString()}); options.push_back({std::string(), QObject::tr("(None)"), RecipeLineRow::Spec()});
if (type == BuildingType::Shipyard) if (type == BuildingType::Shipyard)
{ {
for (const ShipDef& def : config.ships.ships) for (const ShipDef& def : config.ships.ships)
{ {
if (!sim.isSchematicUnlocked(def.id)) { continue; } if (!sim.isSchematicUnlocked(def.id)) { continue; }
// The base materials and base production time, excluding the modules the
// player has not placed yet (REQ-UI-SELECT-OPTIONS). No output item: a ship
// has no icon, so the caption is what the option produces.
RecipeLineRow::Spec line;
line.inputs = toAmounts(def.schematic.materials);
line.durationSeconds = def.schematic.productionTimeSeconds;
options.push_back({def.id, options.push_back({def.id,
QString::fromStdString(toDisplayName(def.id)), QString::fromStdString(toDisplayName(def.id)),
shipTooltip(def)}); line});
} }
} }
else else
@@ -87,16 +81,15 @@ std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
{ {
continue; continue;
} }
// Icon shown on the option button (REQ-UI-RECIPE-ICON): the recipe's
// configured icon item, else its first output item. RecipeLineRow::Spec line;
const std::string iconItemId = recipe.icon line.inputs = toAmounts(recipe.inputs);
? *recipe.icon line.outputs = toAmounts(recipe.outputs);
: (recipe.outputs.empty() ? std::string() line.durationSeconds = recipe.durationSeconds;
: recipe.outputs.front().item);
options.push_back({recipe.id, options.push_back({recipe.id,
QString::fromStdString(toDisplayName(recipe.id)), QString::fromStdString(toDisplayName(recipe.id)),
buildRecipeTooltip(recipe), line});
iconItemId});
} }
} }
@@ -105,51 +98,77 @@ std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
namespace namespace
{ {
// On-screen size of a recipe option button's item icon (REQ-UI-RECIPE-ICON). // Height the option list is allowed to take, as the dialog's parent window's height
const QSize kOptionIconSize(32, 32); // less what the window margins and the dialog's own frame need. Fully unlocked, an
// Assembler offers more options than any window can hold, and the modal is never
// resized to fit (REQ-UI-PANEL-MODAL) -- so the list scrolls instead.
const int kListHeightReservePx = 80;
// Fallback cap for a dialog built without a parent window to measure.
const int kFallbackListHeightPx = 600;
} }
RecipeSelectionDialog::RecipeSelectionDialog( RecipeSelectionDialog::RecipeSelectionDialog(
const std::vector<RecipeSelectionOption>& options, const std::vector<RecipeSelectionOption>& options,
const QString& title, ItemIconCache* itemIcons, QWidget* parent) const QString& title, ItemIconCache* itemIcons, BuildingIconCache* buildingIcons,
QWidget* parent)
: QDialog(parent) : QDialog(parent)
{ {
setWindowTitle(title); setWindowTitle(title);
setModal(true); setModal(true);
QVBoxLayout* mainLayout = new QVBoxLayout(this); QVBoxLayout* mainLayout = new QVBoxLayout(this);
QGridLayout* grid = new QGridLayout();
mainLayout->addLayout(grid);
const int columnCount = 2; // One vertical column of buttons, each stating what it makes (REQ-UI-SELECT-OPTIONS).
QScrollArea* scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
scrollArea->setMaximumHeight(parent != nullptr
? qMax(kListHeightReservePx, parent->height() - kListHeightReservePx)
: kFallbackListHeightPx);
mainLayout->addWidget(scrollArea);
QWidget* list = new QWidget(scrollArea);
QVBoxLayout* listLayout = new QVBoxLayout(list);
listLayout->setContentsMargins(0, 0, 0, 0);
listLayout->setSpacing(4);
for (int i = 0; i < static_cast<int>(options.size()); ++i) for (int i = 0; i < static_cast<int>(options.size()); ++i)
{ {
const RecipeSelectionOption& option = options[static_cast<std::size_t>(i)]; const RecipeSelectionOption& option = options[static_cast<std::size_t>(i)];
QPushButton* button = new QPushButton(this); // An OptionButton so the face of widgets below is what sizes the button
// Icon-only for a recipe option: the produced item's icon on its colored square, // (REQ-UI-SELECT-OPTIONS).
// or the square alone when the item has no icon file (REQ-UI-RECIPE-ICON). The OptionButton* button = new OptionButton(list);
// name stays reachable via the tooltip. Options carrying no item at all -- the if (option.line.isEmpty())
// "(None)" entry and the Shipyard's schematics -- keep their text caption, as
// does an item with neither square nor icon to show.
const QPixmap icon = option.iconItemId.empty()
? QPixmap()
: itemIcons->getSquarePixmap(option.iconItemId, kOptionIconSize.width());
if (!icon.isNull())
{ {
button->setIcon(QIcon(icon)); // The "(None)" option makes nothing and is a caption alone
button->setIconSize(kOptionIconSize); // (REQ-UI-SELECT-OPTIONS).
button->setText(option.caption);
} }
else else
{ {
button->setText(option.caption); // The name over the recipe it stands for. Both are transparent to the mouse
// so a click anywhere on the face still reaches the button beneath them.
QVBoxLayout* face = new QVBoxLayout(button);
face->setContentsMargins(8, 6, 8, 6);
face->setSpacing(2);
QLabel* nameLabel = new QLabel(option.caption, button);
QFont nameFont = nameLabel->font();
nameFont.setBold(true);
nameLabel->setFont(nameFont);
nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
face->addWidget(nameLabel);
RecipeLineRow* line = new RecipeLineRow(itemIcons, buildingIcons, button);
line->setAttribute(Qt::WA_TransparentForMouseEvents, true);
face->addWidget(line);
line->setLine(option.line);
} }
if (!option.tooltip.isEmpty()) listLayout->addWidget(button);
{
button->setToolTip(option.tooltip);
}
button->setMinimumHeight(40);
grid->addWidget(button, i / columnCount, i % columnCount);
m_optionIds.push_back(option.id); m_optionIds.push_back(option.id);
const int index = i; const int index = i;
@@ -158,6 +177,9 @@ RecipeSelectionDialog::RecipeSelectionDialog(
onOptionClicked(index); onOptionClicked(index);
}); });
} }
listLayout->addStretch(1);
scrollArea->setWidget(list);
} }
std::optional<std::string> RecipeSelectionDialog::getChosenId() const std::optional<std::string> RecipeSelectionDialog::getChosenId() const

View File

@@ -8,11 +8,13 @@
#include <QString> #include <QString>
#include "BuildingType.h" #include "BuildingType.h"
#include "RecipeLineRow.h"
struct GameConfig; struct GameConfig;
class BuildingIconCache;
class ItemIconCache;
class Simulation; class Simulation;
class QPushButton; class QPushButton;
class ItemIconCache;
// One selectable entry in the recipe/schematic selection dialog // One selectable entry in the recipe/schematic selection dialog
// (REQ-UI-SELECT-BUTTON). The "(None)" entry uses an empty id. // (REQ-UI-SELECT-BUTTON). The "(None)" entry uses an empty id.
@@ -20,23 +22,23 @@ struct RecipeSelectionOption
{ {
std::string id; std::string id;
QString caption; QString caption;
QString tooltip; // What the option makes, drawn beneath its caption on the button
// Id of the item whose icon is shown on the option button (REQ-UI-RECIPE-ICON); // (REQ-UI-SELECT-OPTIONS). Empty for the "(None)" option, which is a caption alone.
// empty for the "(None)" option and for Shipyard schematics, which keep their // A ship schematic fills the inputs and the time and leaves the outputs empty: a
// caption. When set but no icon file exists, the button falls back to the caption. // ship is not an item and has no icon, so the caption is what it produces.
std::string iconItemId; RecipeLineRow::Spec line;
}; };
// Builds the lock-aware list of selectable options for a production building // Builds the lock-aware list of selectable options for a production building
// of the given type, with display captions and info tooltips // of the given type, with display captions and recipe lines
// (REQ-UI-SELECT-BUTTON, REQ-UI-SELECT-TOOLTIP). The first entry is always a // (REQ-UI-SELECT-BUTTON, REQ-UI-SELECT-OPTIONS). The first entry is always a
// "(None)" option with an empty id. Shared by the selection dialog and the // "(None)" option with an empty id. Shared by the selection dialog and the
// panel selection button so both render identical captions and tooltips, and // panel selection button so both render identical captions, and
// usable for construction sites which have no Building yet (REQ-BLD-SITE-CONFIG). // usable for construction sites which have no Building yet (REQ-BLD-SITE-CONFIG).
std::vector<RecipeSelectionOption> buildRecipeSelectionOptions( std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
BuildingType type, Simulation& sim, const GameConfig& config); BuildingType type, Simulation& sim, const GameConfig& config);
// Modal dialog showing a grid of option buttons (REQ-UI-SELECT-BUTTON). The // Modal dialog listing the options in one vertical column (REQ-UI-SELECT-OPTIONS). The
// game is paused by the caller while it is open. Clicking an option selects it // game is paused by the caller while it is open. Clicking an option selects it
// and closes the dialog; dismissing it (close/Esc) leaves no choice. // and closes the dialog; dismissing it (close/Esc) leaves no choice.
class RecipeSelectionDialog : public QDialog class RecipeSelectionDialog : public QDialog
@@ -44,12 +46,10 @@ class RecipeSelectionDialog : public QDialog
Q_OBJECT Q_OBJECT
public: public:
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); used to // The icon caches draw each option's recipe line (REQ-UI-ITEM-ICON). Not owned.
// render recipe options icon-only (REQ-UI-RECIPE-ICON). Options whose item has
// no icon file fall back to their caption text. Not owned.
RecipeSelectionDialog(const std::vector<RecipeSelectionOption>& options, RecipeSelectionDialog(const std::vector<RecipeSelectionOption>& options,
const QString& title, ItemIconCache* itemIcons, const QString& title, ItemIconCache* itemIcons,
QWidget* parent = nullptr); BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
std::optional<std::string> getChosenId() const; std::optional<std::string> getChosenId() const;

View File

@@ -1,52 +0,0 @@
#include "RecipeTooltip.h"
#include <QObject>
#include <QStringList>
#include "DisplayName.h"
#include "RecipesConfig.h"
namespace
{
QString itemLine(const std::string& itemId, int amount)
{
return QStringLiteral(" ")
+ QString::fromStdString(toDisplayName(itemId))
+ QStringLiteral(" x ") + QString::number(amount);
}
} // namespace
QString buildRecipeTooltip(const RecipeDef& recipe)
{
QStringList lines;
lines << QObject::tr("Recipe: %1")
.arg(QString::fromStdString(toDisplayName(recipe.id)));
if (recipe.inputs.empty())
{
lines << QObject::tr("Inputs: none");
}
else
{
lines << QObject::tr("Inputs:");
for (const RecipeIngredient& ingredient : recipe.inputs)
{
lines << itemLine(ingredient.item, ingredient.amount);
}
}
lines << QObject::tr("Completion time: %1 s").arg(recipe.durationSeconds);
if (!recipe.outputs.empty())
{
lines << QObject::tr("Produces:");
for (const RecipeOutput& output : recipe.outputs)
{
lines << itemLine(output.item, output.amount);
}
}
return lines.join('\n');
}

View File

@@ -1,11 +0,0 @@
#pragma once
#include <QString>
struct RecipeDef;
// Builds the recipe info tooltip text (REQ-UI-SELECT-TOOLTIP): the recipe name,
// each input item name and quantity, the completion time, and the produced
// output item name and quantity. Shared by the recipe-selection dialog and the
// schematic choice dialog's "Unlocks recipes:" list so both render identically.
QString buildRecipeTooltip(const RecipeDef& recipe);

View File

@@ -6,12 +6,35 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include "DisplayName.h" #include "DisplayName.h"
#include "RecipeTooltip.h" #include "RecipeLineRow.h"
#include "RecipesConfig.h" #include "RecipesConfig.h"
namespace namespace
{ {
std::vector<RecipeLineRow::Amount> toAmounts(
const std::vector<RecipeIngredient>& ingredients)
{
std::vector<RecipeLineRow::Amount> amounts;
amounts.reserve(ingredients.size());
for (const RecipeIngredient& ingredient : ingredients)
{
amounts.push_back(RecipeLineRow::Amount{ ingredient.item, ingredient.amount });
}
return amounts;
}
std::vector<RecipeLineRow::Amount> toAmounts(const std::vector<RecipeOutput>& outputs)
{
std::vector<RecipeLineRow::Amount> amounts;
amounts.reserve(outputs.size());
for (const RecipeOutput& output : outputs)
{
amounts.push_back(RecipeLineRow::Amount{ output.item, output.amount });
}
return amounts;
}
QString grantKindLabel(SchematicType type) QString grantKindLabel(SchematicType type)
{ {
switch (type) switch (type)
@@ -28,7 +51,8 @@ QString grantKindLabel(SchematicType type)
SchematicChoiceDialog::SchematicChoiceDialog( SchematicChoiceDialog::SchematicChoiceDialog(
const std::vector<SchematicChoiceOption>& options, const std::vector<SchematicChoiceOption>& options,
const RecipesConfig& recipes, const RecipesConfig& recipes, ItemIconCache* itemIcons,
BuildingIconCache* buildingIcons,
QWidget* parent) QWidget* parent)
: QDialog(parent) : QDialog(parent)
, m_chosenIndex(0) , m_chosenIndex(0)
@@ -108,14 +132,23 @@ SchematicChoiceDialog::SchematicChoiceDialog(
{ {
for (const std::string& recipeId : option.newlyUnlockedRecipeIds) for (const std::string& recipeId : option.newlyUnlockedRecipeIds)
{ {
QLabel* recipeLabel = new QLabel( const RecipeDef* def = recipes.findRecipeDef(recipeId);
QString::fromStdString(toDisplayName(recipeId)), card); if (def == nullptr) { continue; }
recipeLabel->setAlignment(Qt::AlignCenter);
if (const RecipeDef* def = recipes.findRecipeDef(recipeId)) // The recipe drawn as it is everywhere else, led by the building
{ // that runs it and its name, so the line says everything there is
recipeLabel->setToolTip(buildRecipeTooltip(*def)); // to say about it and carries no tooltip (REQ-DEF-SCHEMATIC-DROP).
} RecipeLineRow::Spec spec;
cardLayout->addWidget(recipeLabel); spec.building = def->building;
spec.name = QString::fromStdString(toDisplayName(def->id));
spec.inputs = toAmounts(def->inputs);
spec.outputs = toAmounts(def->outputs);
spec.durationSeconds = def->durationSeconds;
RecipeLineRow* line =
new RecipeLineRow(itemIcons, buildingIcons, card);
cardLayout->addWidget(line);
line->setLine(spec);
} }
} }
} }

View File

@@ -7,14 +7,19 @@
#include "SchematicChoiceOption.h" #include "SchematicChoiceOption.h"
struct RecipesConfig; struct RecipesConfig;
class BuildingIconCache;
class ItemIconCache;
class SchematicChoiceDialog : public QDialog class SchematicChoiceDialog : public QDialog
{ {
Q_OBJECT Q_OBJECT
public: public:
// The icon caches draw the recipe lines of the "Unlocks recipes" list
// (REQ-DEF-SCHEMATIC-DROP, REQ-UI-ITEM-ICON, REQ-UI-BUILD-ICON). Not owned.
SchematicChoiceDialog(const std::vector<SchematicChoiceOption>& options, SchematicChoiceDialog(const std::vector<SchematicChoiceOption>& options,
const RecipesConfig& recipes, const RecipesConfig& recipes, ItemIconCache* itemIcons,
BuildingIconCache* buildingIcons,
QWidget* parent = nullptr); QWidget* parent = nullptr);
int getChosenIndex() const; int getChosenIndex() const;

View File

@@ -5,11 +5,16 @@
#include <functional> #include <functional>
#include "DisplayName.h" #include "DisplayName.h"
#include "OptionButton.h"
#include "ProductionRules.h"
#include "RecipeLineRow.h"
#include "SectionBox.h"
#include <QGridLayout> #include <QGridLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QInputDialog> #include <QInputDialog>
#include <QKeyEvent> #include <QKeyEvent>
#include <QLabel>
#include <QMouseEvent> #include <QMouseEvent>
#include <QPainter> #include <QPainter>
#include <QPushButton> #include <QPushButton>
@@ -22,6 +27,18 @@
namespace namespace
{ {
std::vector<RecipeLineRow::Amount> toAmounts(
const std::vector<RecipeIngredient>& ingredients)
{
std::vector<RecipeLineRow::Amount> amounts;
amounts.reserve(ingredients.size());
for (const RecipeIngredient& ingredient : ingredients)
{
amounts.push_back(RecipeLineRow::Amount{ ingredient.item, ingredient.amount });
}
return amounts;
}
const int kCellSize = 32; const int kCellSize = 32;
// Empty ship tiles pulse between this grey and white at 1 Hz while a module is // Empty ship tiles pulse between this grey and white at 1 Hz while a module is
@@ -401,9 +418,11 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
std::vector<ShipLayoutBlueprint>& allBlueprints, std::vector<ShipLayoutBlueprint>& allBlueprints,
std::set<std::string> unlockedModuleIds, std::set<std::string> unlockedModuleIds,
bool debugDraw, bool debugDraw,
ItemIconCache* itemIcons,
QWidget* parent) QWidget* parent)
: QDialog(parent) : QDialog(parent)
, m_config(config) , m_config(config)
, m_itemIcons(itemIcons)
, m_shipId(shipId) , m_shipId(shipId)
, m_unlockedModuleIds(std::move(unlockedModuleIds)) , m_unlockedModuleIds(std::move(unlockedModuleIds))
, m_rows(0) , m_rows(0)
@@ -463,10 +482,22 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
// Middle: three-column area (stats | module buttons | blueprints). // Middle: three-column area (stats | module buttons | blueprints).
QHBoxLayout* columnsLayout = new QHBoxLayout(); QHBoxLayout* columnsLayout = new QHBoxLayout();
// Left column: ship stats panel. // Left column: ship stats panel, and beneath it what the layout being configured
// would cost to build (REQ-MOD-UI-DIALOG). The cost sits outside the stats panel
// because that widget is shared with the live ship card and the balancing tool,
// neither of which costs anything -- and the balancing tool is deliberately built
// without the item icons this line draws with.
QVBoxLayout* leftLayout = new QVBoxLayout();
m_statsPanel = new ShipStatsPanel(config, this); m_statsPanel = new ShipStatsPanel(config, this);
m_statsPanel->setDebugDrawEnabled(m_debugDraw); m_statsPanel->setDebugDrawEnabled(m_debugDraw);
columnsLayout->addWidget(m_statsPanel); leftLayout->addWidget(m_statsPanel);
m_buildCostSection = new SectionBox(tr("Build cost"), this);
m_buildCostLine = new RecipeLineRow(m_itemIcons, nullptr, m_buildCostSection);
m_buildCostSection->getContentLayout()->addWidget(m_buildCostLine);
leftLayout->addWidget(m_buildCostSection);
leftLayout->addStretch(1);
columnsLayout->addLayout(leftLayout);
// Center column: module selection buttons. // Center column: module selection buttons.
QVBoxLayout* centerLayout = new QVBoxLayout(); QVBoxLayout* centerLayout = new QVBoxLayout();
@@ -487,11 +518,34 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
m_moduleButtons.push_back(nullptr); m_moduleButtons.push_back(nullptr);
continue; continue;
} }
const QString label = QString::fromStdString(toDisplayName(def.id)) // The module's name over what it costs -- its materials and the production time
+ "\n" + QString::fromStdString(def.glyph); // it adds (REQ-MOD-UI-DIALOG). The glyph is how a module is identified on the
QPushButton* btn = new QPushButton(label, this); // layout grid, not here. Both children are transparent to the mouse so a click
// anywhere on the face still reaches the button.
OptionButton* btn = new OptionButton(this);
btn->setCheckable(true); btn->setCheckable(true);
btn->setFixedHeight(48);
QVBoxLayout* face = new QVBoxLayout(btn);
face->setContentsMargins(8, 4, 8, 4);
face->setSpacing(1);
QLabel* nameLabel =
new QLabel(QString::fromStdString(toDisplayName(def.id)), btn);
nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
face->addWidget(nameLabel);
RecipeLineRow::Spec cost;
cost.inputs = toAmounts(def.materials);
cost.durationSeconds = def.productionTimeSeconds;
cost.durationIsAddition = true;
RecipeLineRow* costLine = new RecipeLineRow(m_itemIcons, nullptr, btn);
costLine->setAttribute(Qt::WA_TransparentForMouseEvents, true);
face->addWidget(costLine);
costLine->setLine(cost);
// The config tooltip stays: it says what the module does, which the cost line
// does not (REQ-MOD-UI-MODULE-TOOLTIP).
if (def.tooltip) if (def.tooltip)
{ {
btn->setToolTip(QString::fromStdString(*def.tooltip)); btn->setToolTip(QString::fromStdString(*def.tooltip));
@@ -731,6 +785,29 @@ void ShipLayoutDialog::updateGridWidget()
void ShipLayoutDialog::updateStats() void ShipLayoutDialog::updateStats()
{ {
m_statsPanel->refresh(m_shipId, m_placedModules); m_statsPanel->refresh(m_shipId, m_placedModules);
// What the layout as it currently stands would cost to build, so the price follows
// every placement and removal (REQ-MOD-UI-DIALOG). The sums come from the
// simulation's own rules rather than being added up a second time here
// (REQ-MOD-MATERIALS, REQ-MOD-PRODUCTION-TIME).
ShipLayoutConfig layout;
layout.placedModules = m_placedModules;
const std::map<std::string, int> materials =
computeShipyardRequiredMaterials(*m_config, m_shipId, layout);
RecipeLineRow::Spec cost;
cost.inputs.reserve(materials.size());
for (const std::pair<const std::string, int>& entry : materials)
{
cost.inputs.push_back(RecipeLineRow::Amount{ entry.first, entry.second });
}
cost.durationSeconds =
computeShipyardProductionTimeSeconds(*m_config, m_shipId, layout);
m_buildCostLine->setLine(cost);
// A schematic costing no materials at all has no line to draw, and the section then
// stays out of the way.
m_buildCostSection->setVisible(!cost.inputs.empty());
} }
bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position, bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position,

View File

@@ -14,7 +14,10 @@
#include "ShipLayout.h" #include "ShipLayout.h"
#include "ShipLayoutBlueprint.h" #include "ShipLayoutBlueprint.h"
class ItemIconCache;
class QPushButton; class QPushButton;
class RecipeLineRow;
class SectionBox;
class ShipStatsPanel; class ShipStatsPanel;
class ShipLayoutDialog : public QDialog class ShipLayoutDialog : public QDialog
@@ -22,12 +25,15 @@ class ShipLayoutDialog : public QDialog
Q_OBJECT Q_OBJECT
public: public:
// itemIcons draws what each module costs and what the layout costs in total
// (REQ-MOD-UI-DIALOG, REQ-MOD-UI-STATS-PANEL). Not owned.
ShipLayoutDialog(const GameConfig* config, ShipLayoutDialog(const GameConfig* config,
const std::string& shipId, const std::string& shipId,
const ShipLayoutConfig& currentLayout, const ShipLayoutConfig& currentLayout,
std::vector<ShipLayoutBlueprint>& allBlueprints, std::vector<ShipLayoutBlueprint>& allBlueprints,
std::set<std::string> unlockedModuleIds, std::set<std::string> unlockedModuleIds,
bool debugDraw, bool debugDraw,
ItemIconCache* itemIcons,
QWidget* parent = nullptr); QWidget* parent = nullptr);
std::optional<ShipLayoutConfig> getResult() const; std::optional<ShipLayoutConfig> getResult() const;
@@ -60,6 +66,7 @@ private:
void loadLayoutBlueprint(const std::vector<PlacedModule>& modules); void loadLayoutBlueprint(const std::vector<PlacedModule>& modules);
const GameConfig* m_config; const GameConfig* m_config;
ItemIconCache* m_itemIcons;
std::string m_shipId; std::string m_shipId;
std::set<std::string> m_unlockedModuleIds; std::set<std::string> m_unlockedModuleIds;
std::vector<std::string> m_shipLayout; std::vector<std::string> m_shipLayout;
@@ -81,6 +88,8 @@ private:
QPushButton* m_removeButton; QPushButton* m_removeButton;
QWidget* m_gridWidget; QWidget* m_gridWidget;
ShipStatsPanel* m_statsPanel; ShipStatsPanel* m_statsPanel;
SectionBox* m_buildCostSection;
RecipeLineRow* m_buildCostLine;
bool m_debugDraw; bool m_debugDraw;
std::optional<ShipLayoutConfig> m_result; std::optional<ShipLayoutConfig> m_result;

View File

@@ -10,7 +10,7 @@
#include "DisplayName.h" #include "DisplayName.h"
#include "FactoryQueries.h" #include "FactoryQueries.h"
#include "ProductionSection.h" #include "ProductionSection.h"
#include "RecipeSummaryRow.h" #include "RecipeLineRow.h"
#include "SectionBox.h" #include "SectionBox.h"
#include "SelectionNames.h" #include "SelectionNames.h"
#include "Simulation.h" #include "Simulation.h"
@@ -18,13 +18,13 @@
namespace namespace
{ {
std::vector<RecipeSummaryRow::Amount> toAmounts(const std::map<std::string, int>& map) std::vector<RecipeLineRow::Amount> toAmounts(const std::map<std::string, int>& map)
{ {
std::vector<RecipeSummaryRow::Amount> amounts; std::vector<RecipeLineRow::Amount> amounts;
amounts.reserve(map.size()); amounts.reserve(map.size());
for (const std::pair<const std::string, int>& entry : map) for (const std::pair<const std::string, int>& entry : map)
{ {
amounts.push_back(RecipeSummaryRow::Amount{ entry.first, entry.second }); amounts.push_back(RecipeLineRow::Amount{ entry.first, entry.second });
} }
return amounts; return amounts;
} }
@@ -66,17 +66,17 @@ BufferedBuildingContent::BufferedBuildingContent(const SelectionContext& context
{ {
// The summary is configuration -- what the building will do -- so it sits with the // The summary is configuration -- what the building will do -- so it sits with the
// selection control and is shown for a construction site too (REQ-UI-RECIPE-SUMMARY). // selection control and is shown for a construction site too (REQ-UI-RECIPE-SUMMARY).
m_recipeSummary = new RecipeSummaryRow(context.itemIcons, this); m_recipeSummary = new RecipeLineRow(context.itemIcons, context.buildingIcons, this);
getConfigurationLayout()->addWidget(m_recipeSummary); getConfigurationLayout()->addWidget(m_recipeSummary);
m_inputSection = new SectionBox(tr("Input buffers"), this); m_inputSection = new SectionBox(tr("Input buffers"), this);
m_inputChips = new ItemChipRow(context.itemIcons, m_inputSection); m_inputChips = new ItemChipRow(context, m_inputSection);
m_inputSection->getContentLayout()->addWidget(m_inputChips); m_inputSection->getContentLayout()->addWidget(m_inputChips);
m_production = new ProductionSection(this); m_production = new ProductionSection(this);
m_outputSection = new SectionBox(tr("Output buffer"), this); m_outputSection = new SectionBox(tr("Output buffer"), this);
m_outputChips = new ItemChipRow(context.itemIcons, m_outputSection); m_outputChips = new ItemChipRow(context, m_outputSection);
m_outputSection->getContentLayout()->addWidget(m_outputChips); m_outputSection->getContentLayout()->addWidget(m_outputChips);
// In the direction the materials flow: what goes in, what is being made of it, what // In the direction the materials flow: what goes in, what is being made of it, what
@@ -98,10 +98,14 @@ void BufferedBuildingContent::refreshConfiguration()
setBuildingIdentity(target.type, getBuildingTypeName(target.type)); setBuildingIdentity(target.type, getBuildingTypeName(target.type));
// The card already names the building in its header, so the summary states the cycle
// alone -- no building chip, no recipe name (REQ-UI-RECIPE-SUMMARY).
const CycleInfo cycle = getCycleInfo(target); const CycleInfo cycle = getCycleInfo(target);
m_recipeSummary->setSummary(toAmounts(cycle.perCycleInputs), RecipeLineRow::Spec summary;
toAmounts(cycle.perCycleOutputs), summary.inputs = toAmounts(cycle.perCycleInputs);
cycle.durationSeconds); summary.outputs = toAmounts(cycle.perCycleOutputs);
summary.durationSeconds = cycle.durationSeconds;
m_recipeSummary->setLine(summary);
refreshControls(target); refreshControls(target);
} }

View File

@@ -11,7 +11,7 @@
struct Building; struct Building;
struct BuildingTarget; struct BuildingTarget;
class ProductionSection; class ProductionSection;
class RecipeSummaryRow; class RecipeLineRow;
class SectionBox; class SectionBox;
// Shared body of the four cards that show one building with buffers -- the Miner and // Shared body of the four cards that show one building with buffers -- the Miner and
@@ -74,7 +74,7 @@ private:
BuildingId m_id; BuildingId m_id;
RecipeSummaryRow* m_recipeSummary; RecipeLineRow* m_recipeSummary;
// Input buffers, production progress, output buffer -- in that order, so the card // Input buffers, production progress, output buffer -- in that order, so the card
// reads the way the materials flow (REQ-UI-SINGLE-SELECTION). // reads the way the materials flow (REQ-UI-SINGLE-SELECTION).

View File

@@ -14,7 +14,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.h ${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.h
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.h ${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.h
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.h ${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.h
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.h ${CMAKE_CURRENT_SOURCE_DIR}/ItemTooltip.h
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.h ${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.h
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.h ${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.h
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.h ${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.h
@@ -49,7 +49,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.cpp ${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.cpp
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ItemTooltip.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.cpp
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.cpp

View File

@@ -23,7 +23,7 @@ HqContent::HqContent(const SelectionContext& context, const SelectionRequest& re
m_hpBar = new BarRow(tr("HP"), this); m_hpBar = new BarRow(tr("HP"), this);
m_stockSection = new SectionBox(tr("Building blocks"), this); m_stockSection = new SectionBox(tr("Building blocks"), this);
m_stockChips = new ItemChipRow(context.itemIcons, m_stockSection); m_stockChips = new ItemChipRow(context, m_stockSection);
m_stockSection->getContentLayout()->addWidget(m_stockChips); m_stockSection->getContentLayout()->addWidget(m_stockChips);
// HP first, as on every card that has it (REQ-UI-SELECTION-CARD, REQ-UI-HQ-PANEL). // HP first, as on every card that has it (REQ-UI-SELECTION-CARD, REQ-UI-HQ-PANEL).

View File

@@ -1,11 +1,15 @@
#include "ItemChip.h" #include "ItemChip.h"
#include <QCursor>
#include <QFont> #include <QFont>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QPalette> #include <QPalette>
#include <QTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "ItemTooltip.h"
namespace namespace
{ {
@@ -13,11 +17,18 @@ namespace
const int kCountSizeRisePt = 2; const int kCountSizeRisePt = 2;
const int kSubLineSizeDropPt = 1; const int kSubLineSizeDropPt = 1;
// How long the cursor rests on a chip before its tooltip appears, in milliseconds.
// Matches the feel of an ordinary tooltip, which this one replaces (REQ-UI-ITEM-TOOLTIP).
const int kHoverDelayMs = 500;
} // namespace } // namespace
ItemChip::ItemChip(const QPixmap& icon, QWidget* parent) ItemChip::ItemChip(const SelectionContext& context, const std::string& itemId,
const QPixmap& icon, QWidget* parent)
: QWidget(parent) : QWidget(parent)
, m_context(context)
, m_itemId(itemId)
{ {
// Its own boxed chrome, drawn with palette colors like the rest of the panel's // Its own boxed chrome, drawn with palette colors like the rest of the panel's
// furniture rather than from visuals.toml, which is for world rendering. // furniture rather than from visuals.toml, which is for world rendering.
@@ -58,6 +69,11 @@ ItemChip::ItemChip(const QPixmap& icon, QWidget* parent)
layout->addWidget(m_iconLabel); layout->addWidget(m_iconLabel);
layout->addWidget(text); layout->addWidget(text);
m_hoverTimer = new QTimer(this);
m_hoverTimer->setSingleShot(true);
m_hoverTimer->setInterval(kHoverDelayMs);
connect(m_hoverTimer, &QTimer::timeout, this, &ItemChip::showTooltip);
} }
void ItemChip::setCount(const QString& count) void ItemChip::setCount(const QString& count)
@@ -70,3 +86,28 @@ void ItemChip::setSubLine(const QString& subLine)
m_subLineLabel->setText(subLine); m_subLineLabel->setText(subLine);
m_subLineLabel->setVisible(!subLine.isEmpty()); m_subLineLabel->setVisible(!subLine.isEmpty());
} }
void ItemChip::enterEvent(QEvent* event)
{
m_hoverTimer->start();
QWidget::enterEvent(event);
}
void ItemChip::leaveEvent(QEvent* event)
{
m_hoverTimer->stop();
if (m_tooltip != nullptr)
{
m_tooltip->hide();
}
QWidget::leaveEvent(event);
}
void ItemChip::showTooltip()
{
if (m_tooltip == nullptr)
{
m_tooltip = new ItemTooltip(m_context, m_itemId, this);
}
m_tooltip->showAt(QCursor::pos());
}

View File

@@ -1,28 +1,51 @@
#pragma once #pragma once
#include <string>
#include <QPixmap> #include <QPixmap>
#include <QString> #include <QString>
#include <QWidget> #include <QWidget>
#include "SelectionContext.h"
class ItemTooltip;
class QEvent;
class QLabel; class QLabel;
class QTimer;
// One buffered item: its icon, its count in a larger type, and a sub-line beneath the // One buffered item: its icon, its count in a larger type, and a sub-line beneath the
// count (REQ-UI-SINGLE-SELECTION). Boxed so a row of them reads as separate quantities // count (REQ-UI-SINGLE-SELECTION). Boxed so a row of them reads as separate quantities
// rather than as a run of text. // rather than as a run of text.
//
// Hovering it explains where its item comes from (REQ-UI-ITEM-TOOLTIP).
class ItemChip : public QWidget class ItemChip : public QWidget
{ {
Q_OBJECT Q_OBJECT
public: public:
// An empty icon leaves the icon off and the chip laid out around the text alone. // An empty icon leaves the icon off and the chip laid out around the text alone.
ItemChip(const QPixmap& icon, QWidget* parent = nullptr); ItemChip(const SelectionContext& context, const std::string& itemId,
const QPixmap& icon, QWidget* parent = nullptr);
void setCount(const QString& count); void setCount(const QString& count);
// Left empty for an item with nothing to say beneath its count. // Left empty for an item with nothing to say beneath its count.
void setSubLine(const QString& subLine); void setSubLine(const QString& subLine);
protected:
void enterEvent(QEvent* event) override;
void leaveEvent(QEvent* event) override;
private: private:
void showTooltip();
SelectionContext m_context;
std::string m_itemId;
QLabel* m_iconLabel; QLabel* m_iconLabel;
QLabel* m_countLabel; QLabel* m_countLabel;
QLabel* m_subLineLabel; QLabel* m_subLineLabel;
// Held back so a cursor crossing the chip on its way elsewhere does not flash the
// tooltip, the way an ordinary tooltip is.
QTimer* m_hoverTimer;
// Built on the first hover, because most chips are never hovered at all.
ItemTooltip* m_tooltip = nullptr;
}; };

View File

@@ -19,9 +19,9 @@ const int kChipsPerRow = 2;
} // namespace } // namespace
ItemChipRow::ItemChipRow(ItemIconCache* itemIcons, QWidget* parent) ItemChipRow::ItemChipRow(const SelectionContext& context, QWidget* parent)
: QWidget(parent) : QWidget(parent)
, m_itemIcons(itemIcons) , m_context(context)
{ {
m_layout = new QGridLayout(this); m_layout = new QGridLayout(this);
m_layout->setContentsMargins(0, 0, 0, 0); m_layout->setContentsMargins(0, 0, 0, 0);
@@ -68,9 +68,10 @@ void ItemChipRow::rebuildChips(const std::vector<Entry>& entries)
// The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file // The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file
// is not an error: the chip then carries the square alone, and only an item with // is not an error: the chip then carries the square alone, and only an item with
// no square either leaves the chip laid out around its count alone. // no square either leaves the chip laid out around its count alone.
const QPixmap icon = m_itemIcons->getSquarePixmap(itemId, kChipIconSizePx); const QPixmap icon =
m_context.itemIcons->getSquarePixmap(itemId, kChipIconSizePx);
ItemChip* chip = new ItemChip(icon, this); ItemChip* chip = new ItemChip(m_context, itemId, icon, this);
m_layout->addWidget(chip, static_cast<int>(index) / kChipsPerRow, m_layout->addWidget(chip, static_cast<int>(index) / kChipsPerRow,
static_cast<int>(index) % kChipsPerRow); static_cast<int>(index) % kChipsPerRow);
// Shown right away, because the panel measures itself as soon as this returns. A // Shown right away, because the panel measures itself as soon as this returns. A

View File

@@ -6,8 +6,9 @@
#include <QString> #include <QString>
#include <QWidget> #include <QWidget>
#include "SelectionContext.h"
class ItemChip; class ItemChip;
class ItemIconCache;
class QGridLayout; class QGridLayout;
// The buffered items of one building, each as a chip carrying the item's icon, its // The buffered items of one building, each as a chip carrying the item's icon, its
@@ -28,16 +29,18 @@ public:
QString subLine; QString subLine;
}; };
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); an item with // The context supplies the icon cache each chip draws its item with
// no icon file simply shows no icon, which is not an error. Not owned. // (REQ-UI-ITEM-ICON) -- an item with no icon file simply shows no icon, which is not
explicit ItemChipRow(ItemIconCache* itemIcons, QWidget* parent = nullptr); // an error -- and what a chip's tooltip reads to explain its item
// (REQ-UI-ITEM-TOOLTIP).
explicit ItemChipRow(const SelectionContext& context, QWidget* parent = nullptr);
void setEntries(const std::vector<Entry>& entries); void setEntries(const std::vector<Entry>& entries);
private: private:
void rebuildChips(const std::vector<Entry>& entries); void rebuildChips(const std::vector<Entry>& entries);
ItemIconCache* m_itemIcons; SelectionContext m_context;
QGridLayout* m_layout; QGridLayout* m_layout;
std::vector<std::string> m_itemIds; // what the chips currently stand for std::vector<std::string> m_itemIds; // what the chips currently stand for
std::vector<ItemChip*> m_chips; std::vector<ItemChip*> m_chips;

View File

@@ -0,0 +1,163 @@
#include "ItemTooltip.h"
#include <QFont>
#include <QGuiApplication>
#include <QLabel>
#include <QLayoutItem>
#include <QPalette>
#include <QRect>
#include <QScreen>
#include <QVBoxLayout>
#include "DisplayName.h"
#include "GameConfig.h"
#include "ItemProducers.h"
#include "RecipeLineRow.h"
#include "RecipesConfig.h"
#include "Simulation.h"
namespace
{
// Gap between the cursor and the tooltip's corner, in device-independent pixels. Wide
// enough that the cursor never covers the heading.
const int kCursorOffsetPx = 16;
std::vector<RecipeLineRow::Amount> toAmounts(
const std::vector<RecipeIngredient>& ingredients)
{
std::vector<RecipeLineRow::Amount> amounts;
amounts.reserve(ingredients.size());
for (const RecipeIngredient& ingredient : ingredients)
{
amounts.push_back(RecipeLineRow::Amount{ ingredient.item, ingredient.amount });
}
return amounts;
}
std::vector<RecipeLineRow::Amount> toAmounts(const std::vector<RecipeOutput>& outputs)
{
std::vector<RecipeLineRow::Amount> amounts;
amounts.reserve(outputs.size());
for (const RecipeOutput& output : outputs)
{
amounts.push_back(RecipeLineRow::Amount{ output.item, output.amount });
}
return amounts;
}
// The screen the cursor is on, falling back to the primary screen when the position is
// on none of them (a cursor between two screens of different heights).
QRect availableScreenRect(const QPoint& globalPos)
{
const QScreen* screen = QGuiApplication::screenAt(globalPos);
if (screen == nullptr)
{
screen = QGuiApplication::primaryScreen();
}
return (screen != nullptr) ? screen->availableGeometry() : QRect();
}
} // namespace
ItemTooltip::ItemTooltip(const SelectionContext& context, const std::string& itemId,
QWidget* parent)
: QFrame(parent, Qt::ToolTip)
, m_context(context)
, m_itemId(itemId)
{
// The palette's tooltip colors behind a themed border, so it reads as a tooltip
// rather than as a stray window.
setFrameShape(QFrame::StyledPanel);
setAutoFillBackground(true);
QPalette tooltipPalette = palette();
tooltipPalette.setColor(QPalette::Window,
tooltipPalette.color(QPalette::ToolTipBase));
tooltipPalette.setColor(QPalette::WindowText,
tooltipPalette.color(QPalette::ToolTipText));
setPalette(tooltipPalette);
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(6, 4, 6, 4);
m_layout->setSpacing(2);
}
void ItemTooltip::showAt(const QPoint& globalPos)
{
rebuild();
adjustSize();
// Kept on the screen the cursor is on: a tooltip for a chip near the right or
// bottom edge would otherwise be cut off.
const QRect screen = availableScreenRect(globalPos);
QPoint topLeft(globalPos.x() + kCursorOffsetPx, globalPos.y() + kCursorOffsetPx);
if (screen.isValid())
{
topLeft.setX(qMax(screen.left(),
qMin(topLeft.x(), screen.right() - width() + 1)));
topLeft.setY(qMax(screen.top(),
qMin(topLeft.y(), screen.bottom() - height() + 1)));
}
move(topLeft);
show();
}
void ItemTooltip::rebuild()
{
while (QLayoutItem* item = m_layout->takeAt(0))
{
if (item->widget())
{
item->widget()->deleteLater();
}
delete item;
}
// The heading names the item. For an input chip this is the only place it is named
// at all, since such a chip carries a count and no name (REQ-UI-SINGLE-SELECTION).
QLabel* heading =
new QLabel(QString::fromStdString(toDisplayName(m_itemId)), this);
QFont headingFont = heading->font();
headingFont.setBold(true);
heading->setFont(headingFont);
m_layout->addWidget(heading);
heading->show();
const ItemProduction production =
findItemProduction(m_itemId, *m_context.sim, *m_context.config);
// Scrap is salvaged from debris rather than crafted (REQ-RES-DEBRIS-DROP), so it
// says where it comes from in place of the caption and lists nothing.
QLabel* caption = new QLabel(production.origin == ItemOrigin::Salvaged
? tr("Salvaged from debris")
: tr("Produced by"), this);
m_layout->addWidget(caption);
caption->show();
if (production.origin == ItemOrigin::Undiscovered)
{
// Made somehow, but by nothing the player has unlocked -- named without being
// shown a path they cannot take yet (REQ-UI-ITEM-TOOLTIP).
QLabel* undiscovered = new QLabel(tr("Undiscovered"), this);
m_layout->addWidget(undiscovered);
undiscovered->show();
return;
}
for (const RecipeDef* recipe : production.recipes)
{
RecipeLineRow::Spec spec;
spec.building = recipe->building;
spec.name = QString::fromStdString(toDisplayName(recipe->id));
spec.inputs = toAmounts(recipe->inputs);
spec.outputs = toAmounts(recipe->outputs);
spec.durationSeconds = recipe->durationSeconds;
RecipeLineRow* line =
new RecipeLineRow(m_context.itemIcons, m_context.buildingIcons, this);
m_layout->addWidget(line);
line->setLine(spec);
}
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <QFrame>
#include "SelectionContext.h"
class QVBoxLayout;
// The tooltip an item chip shows on hover: the item's name, and every way the player
// can currently produce it, each drawn as a recipe line (REQ-UI-ITEM-TOOLTIP).
//
// A window of its own rather than a Qt tooltip, because Qt's are text and this one is
// drawn -- item squares, building chips and all. It is a Qt::ToolTip window, so it
// floats above everything and takes no input.
class ItemTooltip : public QFrame
{
Q_OBJECT
public:
ItemTooltip(const SelectionContext& context, const std::string& itemId,
QWidget* parent = nullptr);
// Rebuilds the content and shows the tooltip near the given screen position, kept
// inside the screen. Rebuilt on every show because what produces an item changes as
// the player unlocks recipes (REQ-LOCK-UI-RECIPE).
void showAt(const QPoint& globalPos);
private:
void rebuild();
SelectionContext m_context;
std::string m_itemId;
QVBoxLayout* m_layout;
};

View File

@@ -45,13 +45,14 @@ void RecipeSelectionControl::setRecipeId(const std::string& recipeId)
if (option.id == recipeId && !option.id.empty()) if (option.id == recipeId && !option.id.empty())
{ {
m_button->setText(option.caption); m_button->setText(option.caption);
m_button->setToolTip(option.tooltip);
return; return;
} }
} }
// No tooltip of its own: what the building has selected is drawn beneath the button
// as the recipe summary, and where its items come from is reachable by hovering the
// card's item chips (REQ-UI-SELECT-BUTTON, REQ-UI-ITEM-TOOLTIP).
m_button->setText(m_type == BuildingType::Shipyard m_button->setText(m_type == BuildingType::Shipyard
? tr("Select schematic") ? tr("Select schematic")
: tr("Select recipe")); : tr("Select recipe"));
m_button->setToolTip(QString());
} }

View File

@@ -1,123 +0,0 @@
#include "RecipeSummaryRow.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QLayoutItem>
#include <QPixmap>
#include "ItemIconCache.h"
namespace
{
// Size the items' colored squares are drawn at on the summary line, in device-independent
// pixels. Larger than the artwork they carry, which the square insets (REQ-UI-ITEM-ICON).
const int kSummaryIconSizePx = 18;
// Adds a freshly built label to the summary and shows it.
//
// The show is what makes it count: a widget created under an already-visible parent
// starts hidden, and a layout treats a hidden item as empty, so an unshown label would
// add nothing to the size hint the panel measures itself against as soon as this
// returns.
void addAndShow(QHBoxLayout* layout, QLabel* label)
{
layout->addWidget(label);
label->show();
}
} // namespace
RecipeSummaryRow::RecipeSummaryRow(ItemIconCache* itemIcons, QWidget* parent)
: QWidget(parent)
, m_itemIcons(itemIcons)
{
m_layout = new QHBoxLayout(this);
m_layout->setContentsMargins(0, 0, 0, 0);
m_layout->setSpacing(4);
hide();
}
void RecipeSummaryRow::setSummary(const std::vector<Amount>& inputs,
const std::vector<Amount>& outputs,
double durationSeconds)
{
if (outputs.empty() && inputs.empty())
{
// Forgotten as well as hidden, so re-selecting the same recipe later is seen as
// a change and shows the row again.
m_inputs.clear();
m_outputs.clear();
m_durationSeconds = -1.0;
hide();
return;
}
if (inputs == m_inputs && outputs == m_outputs
&& durationSeconds == m_durationSeconds)
{
return;
}
m_inputs = inputs;
m_outputs = outputs;
m_durationSeconds = durationSeconds;
rebuild(inputs, outputs, durationSeconds);
show();
}
void RecipeSummaryRow::rebuild(const std::vector<Amount>& inputs,
const std::vector<Amount>& outputs,
double durationSeconds)
{
while (QLayoutItem* item = m_layout->takeAt(0))
{
if (item->widget())
{
item->widget()->deleteLater();
}
delete item;
}
addAmounts(inputs);
if (!inputs.empty())
{
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
addAndShow(m_layout, new QLabel(QString(rightArrow), this));
}
addAmounts(outputs);
if (durationSeconds > 0.0)
{
const QChar middleDot(0x00B7); // U+00B7 MIDDLE DOT
addAndShow(m_layout, new QLabel(
QStringLiteral("%1 %2").arg(middleDot).arg(
tr("%1 s").arg(durationSeconds, 0, 'f', 1)), this));
}
m_layout->addStretch(1);
}
void RecipeSummaryRow::addAmounts(const std::vector<Amount>& amounts)
{
for (const Amount& entry : amounts)
{
// The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file
// is not an error: the square stands alone then, and only an item with no square
// either falls back to its id in text.
const QPixmap icon =
m_itemIcons->getSquarePixmap(entry.itemId, kSummaryIconSizePx);
if (!icon.isNull())
{
QLabel* iconLabel = new QLabel(this);
iconLabel->setPixmap(icon);
addAndShow(m_layout, iconLabel);
}
else
{
addAndShow(m_layout,
new QLabel(QString::fromStdString(entry.itemId), this));
}
addAndShow(m_layout, new QLabel(QString::number(entry.amount), this));
}
}

View File

@@ -1,52 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include <QWidget>
class ItemIconCache;
class QHBoxLayout;
// What one production cycle does, on a line: each input item with its per-cycle amount,
// an arrow, each output with its amount, and the cycle time
// (REQ-UI-RECIPE-SUMMARY). It restates the selected recipe without the player having to
// open the selection dialog, and it is the panel's only display of the cycle time.
class RecipeSummaryRow : public QWidget
{
Q_OBJECT
public:
struct Amount
{
std::string itemId;
int amount = 0;
bool operator==(const Amount& other) const
{
return itemId == other.itemId && amount == other.amount;
}
};
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); an item with
// no icon file falls back to its id. Not owned.
explicit RecipeSummaryRow(ItemIconCache* itemIcons, QWidget* parent = nullptr);
// Hides the row when there is nothing selected to produce, which is how a building
// with no recipe shows no summary at all.
void setSummary(const std::vector<Amount>& inputs,
const std::vector<Amount>& outputs, double durationSeconds);
private:
void rebuild(const std::vector<Amount>& inputs, const std::vector<Amount>& outputs,
double durationSeconds);
void addAmounts(const std::vector<Amount>& amounts);
ItemIconCache* m_itemIcons;
QHBoxLayout* m_layout;
// What the row currently shows, so a refresh at tick rate rebuilds it only when the
// recipe actually changed.
std::vector<Amount> m_inputs;
std::vector<Amount> m_outputs;
double m_durationSeconds = -1.0;
};

View File

@@ -83,20 +83,11 @@ BufferedBuildingContent::CycleInfo ShipyardContent::getCycleInfo(
*getContext().config, target.recipeId, target.shipLayout); *getContext().config, target.recipeId, target.shipLayout);
// A shipyard's output is the ship itself, which never lands in an item buffer -- so // A shipyard's output is the ship itself, which never lands in an item buffer -- so
// the summary shows one ship rather than an item id. // the summary shows no output item at all. The time is the schematic's plus its
info.durationSeconds = shipDef->schematic.productionTimeSeconds; // modules', from the same place the layout dialog's build cost reads it
if (target.shipLayout.has_value()) // (REQ-MOD-PRODUCTION-TIME).
{ info.durationSeconds = computeShipyardProductionTimeSeconds(
for (const PlacedModule& placed : target.shipLayout->placedModules) *getContext().config, target.recipeId, target.shipLayout);
{
const ModuleDef* moduleDef =
getContext().config->modules.findModuleDef(placed.moduleId);
if (moduleDef)
{
info.durationSeconds += moduleDef->productionTimeSeconds;
}
}
}
info.runsProduction = true; info.runsProduction = true;
return info; return info;
} }