draw recipes wherever the UI states what something makes

Implements the requirements of 0bf24a7. Recipes were explained in prose:
the recipe dialog was a grid of icon-only buttons with a text tooltip
listing name, inputs, time and output, the unlock-choice dialog listed
recipe names with the same tooltip, an item chip said nothing about where
its item came from, and a module button never showed its cost.

RecipeLineRow is the one widget that draws a recipe -- optional building
chip and name, inputs, arrow, outputs, time -- and every place that states
what something makes now goes through it: the panel's recipe summary (which
it replaces RecipeSummaryRow for), the selection dialog's option buttons,
the item production tooltip, the unlock-choice dialog's recipe lines, the
module buttons' cost lines, and the layout dialog's build cost. Its arrow
is drawn only where there are outputs, so a shipyard summary no longer
points at nothing.

ItemProducers answers where an item comes from, filtering by unlock state
per building type, and ItemTooltip draws it as a window of its own -- Qt's
tooltips are text, this one has item squares and building chips in it.
ItemChip pops it after a hover delay.

The selection dialog becomes one scrolling vertical column of buttons, each
showing its name over its recipe line, and no dialog carries a tooltip any
more. RecipeTooltip and recipes.toml's unused "icon" field go with the
behavior they served.

Two requirement amendments the implementation forced, both in this commit:
the option column scrolls when taller than the window allows, and a Smelter
or Reprocessing recipe is listed once its building is unlocked, those
recipes carrying no unlock of their own. The layout dialog's build cost
also moved out of the shared ShipStatsPanel, which the balancing tool
compiles directly and is deliberately kept free of the icon caches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-11 21:20:40 +02:00
parent 0bf24a7c62
commit b7b5a92dbb
36 changed files with 934 additions and 442 deletions

View File

@@ -310,7 +310,7 @@ 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. A module button shows the module's name — its `id` under the usual display convention — and its glyph 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. - **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 — and its glyph 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.
@@ -340,8 +340,6 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
All capability module stat values incorporate passive modifiers targeting the relevant capability category per REQ-MOD-STAT-CALC. All capability module stat values incorporate passive modifiers targeting the relevant capability category per REQ-MOD-STAT-CALC.
The panel also shows 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, as every other value in this panel does. A build cost belongs to a ship being configured, so it appears here only: the selection panel's stats panel for an existing ship (REQ-UI-SHIP-STATS-PANEL) shows none.
While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT) for the current layout configuration. This value updates in real time as modules are placed or removed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT) for the current layout configuration. This value updates in real time as modules are placed or removed.
- REQ-MOD-UI-LAYOUT-SIZE: Ship layouts are small enough to display in the layout configuration dialog without scrolling (maximum grid size fits within the dialog). - REQ-MOD-UI-LAYOUT-SIZE: Ship layouts are small enough to display in the layout configuration dialog without scrolling (maximum grid size fits within the dialog).
@@ -596,7 +594,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
- 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 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-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-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. - 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.
- 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. - 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.
- 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. - 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.
- The `(None)` option shows its text caption alone. - The `(None)` option shows its text caption alone.
@@ -604,7 +602,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
- 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 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. - 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 unlocked recipes are listed** (REQ-LOCK-UI-RECIPE), consistent with the rest of the UI hiding what the player cannot make yet. Two cases have no recipe line to show, and each says so in place of the list rather than leaving the tooltip bare: **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 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. - **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.

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,8 @@ 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}/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 +60,8 @@ 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}/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())
{ {

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,45 @@
#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 "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 +49,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 +80,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 +97,75 @@ 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); QPushButton* button = new QPushButton(list);
// Icon-only for a recipe option: the produced item's icon on its colored square, if (option.line.isEmpty())
// or the square alone when the item has no icon file (REQ-UI-RECIPE-ICON). The
// name stays reachable via the tooltip. Options carrying no item at all -- the
// "(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 +174,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,15 @@
#include <functional> #include <functional>
#include "DisplayName.h" #include "DisplayName.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 +26,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 +417,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 +481,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 +517,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 and glyph over what it costs -- its materials and the
+ "\n" + QString::fromStdString(def.glyph); // production time it adds (REQ-MOD-UI-DIALOG). Both children are transparent to
QPushButton* btn = new QPushButton(label, this); // the mouse so a click anywhere on the face still reaches the button.
QPushButton* btn = new QPushButton(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))
+ " " + QString::fromStdString(def.glyph), 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 +784,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:
QLabel* m_iconLabel; void showTooltip();
QLabel* m_countLabel;
QLabel* m_subLineLabel; SelectionContext m_context;
std::string m_itemId;
QLabel* m_iconLabel;
QLabel* m_countLabel;
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;
} }