7 Commits

Author SHA1 Message Date
44c3c83080 List unlocked recipes with tooltips in schematic choice dialog
Implement REQ-DEF-SCHEMATIC-DROP: the schematic option's per-option list now
shows the miner/assembler recipes that would newly become implicitly unlocked
(labeled "Unlocks recipes:") rather than a deduplicated list of output items.
Each recipe entry shows the recipe info tooltip (REQ-UI-SELECT-TOOLTIP) on
hover.

- Extract the recipe tooltip builder from RecipeSelectionDialog's anonymous
  namespace into a shared RecipeTooltip.h/.cpp so both dialogs render identical
  tooltips.
- SchematicChoiceOption carries newlyUnlockedRecipeIds (sorted by display name)
  instead of newlyUnlockedItemNames; Simulation collects recipe ids directly.
- SchematicChoiceDialog takes the RecipesConfig to render one label per recipe
  with its tooltip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNNLeFWhVzvxkK9qVXP2K
2026-07-09 13:34:55 +02:00
410c9646b8 List unlocked recipes with tooltips in schematic choice dialog
Update REQ-DEF-SCHEMATIC-DROP: the schematic option's "Unlocks recipes for:"
item list becomes an "Unlocks recipes:" list of the recipes themselves (by
id), each showing the recipe info tooltip from REQ-UI-SELECT-TOOLTIP on hover.
Listing recipes rather than deduplicated items also removes the item-to-recipe
ambiguity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNNLeFWhVzvxkK9qVXP2K
2026-07-09 13:15:04 +02:00
17ef87dec2 Show recipe schematic id in schematic choice dialog
Implement REQ-DEF-SCHEMATIC-DROP: the schematic choice dialog now displays
the assembler recipe id instead of the recipe's output item type, matching
the ship and module branches which already show the id. This disambiguates
recipes that share an output item (e.g. shortcut_steel_plate vs steel_plate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNNLeFWhVzvxkK9qVXP2K
2026-07-09 12:26:19 +02:00
19ed70ca92 Show schematic id for all three types in choice dialog
Update REQ-DEF-SCHEMATIC-DROP so the schematic choice dialog displays the
id in all cases: ship id from ships.toml, module id from modules.toml, and
assembler recipe id from recipes.toml. Using the recipe id disambiguates
recipes that share an output item (e.g. shortcut_steel_plate vs steel_plate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNNLeFWhVzvxkK9qVXP2K
2026-07-09 12:15:01 +02:00
2b993af08c ui: auto-persist factory blueprints, drop Save/Load buttons
Load blueprints.toml in the BlueprintPanel constructor and write it in the
destructor, matching the ship-layout blueprint lifecycle (startup load,
shutdown save, silent write errors, modal on parse failure). Removes the
Save/Load buttons and their slots. Implements REQ-UI-BLUEPRINT-SAVE/-LOAD/-PANEL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNNLeFWhVzvxkK9qVXP2K
2026-07-09 11:47:46 +02:00
c042f8e38c docs: auto-persist factory blueprints, mirroring ship-layout blueprints
Replace the Save/Load buttons in the factory-building blueprint panel with
automatic persistence: save on shutdown, load at startup. REQ-UI-BLUEPRINT-SAVE,
-LOAD, and -PANEL now match the ship-layout blueprint rules (REQ-MOD-UI-BLUEPRINT-
STARTUP/SHUTDOWN).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNNLeFWhVzvxkK9qVXP2K
2026-07-09 11:39:51 +02:00
3148c9154c Fix Salvage Bay drop-off: config-driven output-buffer capacity
Salvage ships flew to their assigned Salvage Bay with full cargo and
waited there forever without delivering. A Salvage Bay's
outputBuffer.capacity was never initialized (stayed 0), so
deliverScrapToSalvageBay always rejected the hand-over, cargo never
drained, and DeliverScrap stayed the winning behavior indefinitely.

Give the bay a config-driven output-buffer capacity:
- BuildingDef gains optional outputBufferCapacity; ConfigLoader parses
  the new output_buffer_capacity key.
- salvage_bay in both buildings.toml files sets it to 20.
- initSalvageBayBuffer applies it on both operational-creation paths
  (construction completion and placeImmediate).
- REQ-BLD-SALVAGE-BAY documents the config-sized buffer and hand-over.
- Tests: config parse assertion and end-to-end delivery hand-over.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNNLeFWhVzvxkK9qVXP2K
2026-07-09 11:05:58 +02:00
22 changed files with 240 additions and 150 deletions

View File

@@ -81,6 +81,7 @@ id = "salvage_bay"
cost = 25 cost = 25
player_placeable = true player_placeable = true
construction_time_seconds = 1 construction_time_seconds = 1
output_buffer_capacity = 20
surface_mask = [ surface_mask = [
"<AAS", "<AAS",
" AAS", " AAS",

View File

@@ -83,6 +83,7 @@ id = "salvage_bay"
cost = 25 cost = 25
player_placeable = true player_placeable = true
construction_time_seconds = 15 construction_time_seconds = 15
output_buffer_capacity = 20
surface_mask = [ surface_mask = [
"SAA", "SAA",
"SAA>", "SAA>",

View File

@@ -120,7 +120,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
- REQ-BLD-ASSEMBLER: **Assembler** (3×3): The player selects a recipe from the config-defined crafting tree. Produces the selected output item at the rate defined in the corresponding `recipes.toml [[recipe]]` entry with `building = "assembler"`. Only implicitly unlocked recipes are available for selection (REQ-LOCK-UI-RECIPE). - REQ-BLD-ASSEMBLER: **Assembler** (3×3): The player selects a recipe from the config-defined crafting tree. Produces the selected output item at the rate defined in the corresponding `recipes.toml [[recipe]]` entry with `building = "assembler"`. Only implicitly unlocked recipes are available for selection (REQ-LOCK-UI-RECIPE).
- REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE); the pool of eligible outputs is restricted to implicitly unlocked item types (REQ-LOCK-REPROCESSING-POOL). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING. - REQ-BLD-REPROCESSING: **Reprocessing Plant** (3×3): Consumes scrap per cycle and produces exactly one higher-level intermediate product per cycle via weighted random pick. The input quantity, possible output items, per-output weights, and amounts are defined in `recipes.toml [[recipe]]` entries with `building = "reprocessing_plant"` (`inputs`, `outputs[].item`, `outputs[].amount`, `outputs[].weight`). Weights are normalized at load time; their sum does not need to equal 1. The output is rolled at cycle start (see REQ-MAT-CYCLE); the pool of eligible outputs is restricted to implicitly unlocked item types (REQ-LOCK-REPROCESSING-POOL). The output buffer holds at most one cycle's output — see REQ-MAT-OUTPUT-BUFFER-REPROCESSING.
- REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a schematic. When all required materials — the ship's base materials (`[ship.schematic].materials`) plus the materials of all modules in the configured layout (REQ-MOD-MATERIALS) — are present in its input buffer, the shipyard consumes them and begins a production cycle lasting the ship's base `[ship.schematic].production_time_seconds` plus the sum of production times contributed by all module instances in the configured layout (REQ-MOD-PRODUCTION-TIME). One ship of that type is spawned with the configured modules when the cycle completes. The shipyard cannot start a new cycle while one is in progress. If the player confirms a layout change (REQ-MOD-UI-DIALOG) while a production cycle is in progress, the current cycle is cancelled and all consumed materials are discarded; the shipyard returns to idle with the new layout configuration. - REQ-BLD-SHIPYARD: **Shipyard** (4×2): The player selects a schematic. When all required materials — the ship's base materials (`[ship.schematic].materials`) plus the materials of all modules in the configured layout (REQ-MOD-MATERIALS) — are present in its input buffer, the shipyard consumes them and begins a production cycle lasting the ship's base `[ship.schematic].production_time_seconds` plus the sum of production times contributed by all module instances in the configured layout (REQ-MOD-PRODUCTION-TIME). One ship of that type is spawned with the configured modules when the cycle completes. The shipyard cannot start a new cycle while one is in progress. If the player confirms a layout change (REQ-MOD-UI-DIALOG) while a production cycle is in progress, the current cycle is cancelled and all consumed materials are discarded; the shipyard returns to idle with the new layout configuration.
- REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. Scrap delivered here is placed onto connected output belts. - REQ-BLD-SALVAGE-BAY: **Salvage Bay** (3×2): A dedicated drop-off point for salvage ships. It has an output buffer whose holding capacity is defined by the `output_buffer_capacity` field of the `salvage_bay` entry in `buildings.toml` (rather than by a production cycle, since the Salvage Bay has no recipe). A ship at the bay hands over one unit of scrap per tick while the buffer has free space; a full buffer blocks further drop-off until space frees up (consistent with the buffer-full semantics of REQ-MAT-OUTPUT-BUFFER). Held scrap is pushed onto connected output belts.
- REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED). - REQ-BLD-BELT: **Belt** (1×1): Transports items. A belt tile has one direction (N, S, E, W) set at placement (modified by rotation). Curved belts are auto-derived: when a belt tile's outgoing direction leads into another belt whose direction is orthogonal, the downstream belt is rendered and behaves as a curve. Belt speed is defined in `world.toml [world].belt_speed_tiles_per_second` (REQ-GW-BELT-SPEED).
- REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Each output can optionally have a filter (a list of item types), configurable via the selected building panel; only implicitly unlocked item types are available as filter options (REQ-LOCK-UI-SPLITTER). Routing rules: - REQ-BLD-SPLITTER: **Splitter** (1×1): Distributes incoming items between two output directions. Each output can optionally have a filter (a list of item types), configurable via the selected building panel; only implicitly unlocked item types are available as filter options (REQ-LOCK-UI-SPLITTER). Routing rules:
- An item matching only one output's filter is routed to that output. - An item matching only one output's filter is routed to that output.
@@ -313,13 +313,13 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
In addition to the conditions above, a schematic is included in the eligible drop pool only when every prerequisite in its optional `unlock_requires` list is currently satisfied (REQ-LOCK-PREREQ). Because the pool is rebuilt for each drop, a schematic gated behind prerequisites first appears only after all of its prerequisites have themselves been unlocked. In addition to the conditions above, a schematic is included in the eligible drop pool only when every prerequisite in its optional `unlock_requires` list is currently satisfied (REQ-LOCK-PREREQ). Because the pool is rebuilt for each drop, a schematic gated behind prerequisites first appears only after all of its prerequisites have themselves been unlocked.
Each option in the dialog displays: the schematic name (ship `display_name` from `ships.toml`, module `id` from `modules.toml`, or the output item type for assembler recipes) and the schematic type (ship, module, or assembler recipe). The artifact option (if present) is displayed as a distinct entry with the name "Artifact". Each option in the dialog displays: the schematic name (ship `id` from `ships.toml`, module `id` from `modules.toml`, or assembler recipe `id` from `recipes.toml`) and the schematic type (ship, module, or assembler recipe). The artifact option (if present) is displayed as a distinct entry with the name "Artifact".
Each option additionally displays a vertical list of item names labeled "Unlocks recipes for:", showing which recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the output items of miner recipes and assembler recipes (without `unlock_at_station_level`) that are not currently implicitly unlocked but would become so after applying this option's effect: Each option additionally displays a vertical list of recipe names labeled "Unlocks recipes:", showing which miner and assembler recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the miner recipes and assembler recipes (without `unlock_at_station_level`) that are not currently implicitly unlocked but would become so after applying this option's effect:
- For a ship or module schematic, its `materials` are added to the base set per REQ-LOCK-IMPLICIT step 1a before recomputation. - For a ship or module schematic, its `materials` are added to the base set per REQ-LOCK-IMPLICIT step 1a before recomputation.
- For an assembler recipe schematic, its output item is added to the base set per REQ-LOCK-IMPLICIT step 1b before recomputation. - For an assembler recipe schematic, its output item is added to the base set per REQ-LOCK-IMPLICIT step 1b before recomputation.
Item names are deduplicated and sorted alphabetically. If no recipes would be newly unlocked, the list shows "None". Each recipe is listed by its `id` (using the same display convention as the assembler recipe-selection dialog), sorted alphabetically. Hovering a recipe in this list displays the recipe info tooltip described for a recipe in REQ-UI-SELECT-TOOLTIP (the recipe name; the name and quantity of each input item; the completion time; and the name and quantity of the produced output item). If no recipes would be newly unlocked, the list shows "None".
The player selects one option by clicking it. If the player selects the artifact option, the player's artifact count is incremented by 1 (REQ-WIN-ARTIFACT-COUNT) and the dialog closes; no schematic is applied. Otherwise, the selected schematic is applied and the dialog closes: The player selects one option by clicking it. If the player selects the artifact option, the player's artifact count is incremented by 1 (REQ-WIN-ARTIFACT-COUNT) and the dialog closes; no schematic is applied. Otherwise, the selected schematic is applied and the dialog closes:
@@ -471,7 +471,7 @@ The screen is divided into two columns: a main column (75% width) containing the
### Blueprint Panel ### Blueprint Panel
- REQ-UI-BLUEPRINT-PANEL: The blueprint panel is shown to the right of the build button grid. It contains, from top to bottom: a "Create Blueprint" button, and a list of blueprint entries (one per saved blueprint, in creation order). - REQ-UI-BLUEPRINT-PANEL: The blueprint panel is shown to the right of the build button grid. It contains, from top to bottom: a "Create Blueprint" button, and a list of blueprint entries (one per saved blueprint, in creation order). The panel has no Save or Load buttons; blueprints are persisted automatically (REQ-UI-BLUEPRINT-SAVE) and restored at startup (REQ-UI-BLUEPRINT-LOAD).
- REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button grid) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list. - REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button grid) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list.
@@ -485,9 +485,9 @@ The screen is divided into two columns: a main column (75% width) containing the
- REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited. - REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited.
- REQ-UI-BLUEPRINT-SAVE: A "Save" button is shown at the bottom of the blueprint panel. Clicking it serializes all current blueprints to a file named `blueprints.toml` located in the same directory as the application executable. The TOML structure matches REQ-UI-BLUEPRINT-STORAGE. If writing fails, a modal error dialog is shown describing the failure. - REQ-UI-BLUEPRINT-SAVE: On application shutdown, all current blueprints are serialized to a file named `blueprints.toml` located in the same directory as the application executable. The TOML structure matches REQ-UI-BLUEPRINT-STORAGE. Write errors are silently ignored on shutdown (no button, no dialog).
- REQ-UI-BLUEPRINT-LOAD: A "Load" button is shown at the bottom of the blueprint panel, to the right of the "Save" button. Clicking it shows a confirmation dialog ("Load blueprints? This will replace all current blueprints.") with Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm reads `blueprints.toml` from the same directory as the application executable, replaces all current blueprints with those from the file (in the order they appear in the file), and exits any active blueprint-related mode (blueprint placement mode, delete mode). If the file does not exist or cannot be parsed, a modal error dialog is shown describing the failure and the current blueprint list is left unchanged. - REQ-UI-BLUEPRINT-LOAD: At application startup, blueprints are loaded from `blueprints.toml` in the same directory as the application executable, populating the blueprint list (in the order they appear in the file). If the file does not exist, the blueprint list starts empty with no error. If the file exists but cannot be parsed (malformed TOML), a modal error dialog describes the failure and the blueprint list starts empty. There is no Load button and no runtime reload.
## Balancing Tool ## Balancing Tool

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -18,6 +19,10 @@ struct BuildingDef
// Stored as raw strings here; parsing into per-cell tiles + output ports // Stored as raw strings here; parsing into per-cell tiles + output ports
// happens when buildings are placed, not at load time. // happens when buildings are placed, not at load time.
std::vector<std::string> surfaceMask; std::vector<std::string> surfaceMask;
// Output-buffer holding size for buildings without a recipe-driven buffer.
// Only the Salvage Bay sets this (REQ-BLD-SALVAGE-BAY).
std::optional<int> outputBufferCapacity;
}; };
struct BuildingsConfig struct BuildingsConfig

View File

@@ -337,6 +337,12 @@ BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds"); def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask"); def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
if (mt.contains("output_buffer_capacity"))
{
def.outputBufferCapacity = static_cast<int>(
requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity"));
}
const std::optional<BuildingType> parsedType = parseBuildingType(def.id); const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
if (!parsedType) if (!parsedType)
{ {

View File

@@ -20,8 +20,8 @@ struct SchematicChoiceOption
SchematicType type; SchematicType type;
std::string displayName; std::string displayName;
// Display names of items produced by recipes that would newly become // Ids of miner/assembler recipes that would newly become implicitly
// implicitly unlocked (REQ-LOCK-IMPLICIT) if this option is selected. // unlocked (REQ-LOCK-IMPLICIT) if this option is selected. Sorted
// Deduplicated and sorted alphabetically; empty if none. // alphabetically by display name; empty if none.
std::vector<std::string> newlyUnlockedItemNames; std::vector<std::string> newlyUnlockedRecipeIds;
}; };

View File

@@ -154,6 +154,16 @@ void BuildingSystem::initShipyardBuffers(Building& b) const
} }
} }
void BuildingSystem::initSalvageBayBuffer(Building& b) const
{
// Salvage Bay has no recipe-driven buffer; its output-buffer holding size for
// ship drop-off is config-defined (REQ-BLD-SALVAGE-BAY).
b.outputBuffer.items.clear();
const BuildingDef* def = findBuildingDef(BuildingType::SalvageBay);
b.outputBuffer.capacity =
(def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
}
std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
{ {
// Build lookup sets for quick membership checks. // Build lookup sets for quick membership checks.
@@ -577,7 +587,11 @@ void BuildingSystem::tickConstruction(Tick currentTick)
} }
building.inputPorts = computeInputPorts(building); building.inputPorts = computeInputPorts(building);
if (!building.recipeId.empty()) if (building.type == BuildingType::SalvageBay)
{
initSalvageBayBuffer(building);
}
else if (!building.recipeId.empty())
{ {
if (building.type == BuildingType::Shipyard) if (building.type == BuildingType::Shipyard)
{ {
@@ -1238,6 +1252,11 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type,
} }
building.inputPorts = computeInputPorts(building); building.inputPorts = computeInputPorts(building);
if (type == BuildingType::SalvageBay)
{
initSalvageBayBuffer(building);
}
m_buildings.push_back(std::move(building)); m_buildings.push_back(std::move(building));
return id; return id;
} }

View File

@@ -170,6 +170,7 @@ private:
const ModuleDef* findModuleDef(const std::string& id) const; const ModuleDef* findModuleDef(const std::string& id) const;
void initBuffers(Building& b, const RecipeDef& recipe) const; void initBuffers(Building& b, const RecipeDef& recipe) const;
void initShipyardBuffers(Building& b) const; void initShipyardBuffers(Building& b) const;
void initSalvageBayBuffer(Building& b) const;
std::vector<Port> computeInputPorts(const Building& b) const; std::vector<Port> computeInputPorts(const Building& b) const;
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe); std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
bool bodyCellsWithinWorldBounds( bool bodyCellsWithinWorldBounds(

View File

@@ -707,14 +707,7 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
else else
{ {
option.type = SchematicType::Recipe; option.type = SchematicType::Recipe;
for (const RecipeDef& def : m_config.recipes.recipes) option.displayName = toDisplayName(entry.id);
{
if (def.id == entry.id && !def.outputs.empty())
{
option.displayName = toDisplayName(def.outputs[0].item);
break;
}
}
} }
// REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by this option. // REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by this option.
@@ -737,7 +730,7 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
const UnlockedSets hypothetical = computeUnlockedSets( const UnlockedSets hypothetical = computeUnlockedSets(
hypotheticalShipIds, hypotheticalModuleIds, hypotheticalRecipeSchematicIds); hypotheticalShipIds, hypotheticalModuleIds, hypotheticalRecipeSchematicIds);
option.newlyUnlockedItemNames = computeNewlyUnlockedItemNames(hypothetical); option.newlyUnlockedRecipeIds = computeNewlyUnlockedRecipeIds(hypothetical);
m_pendingSchematicChoices.push_back(option); m_pendingSchematicChoices.push_back(option);
} }
@@ -925,23 +918,20 @@ Simulation::UnlockedSets Simulation::computeUnlockedSets(
return result; return result;
} }
std::vector<std::string> Simulation::computeNewlyUnlockedItemNames(const UnlockedSets& hypothetical) const std::vector<std::string> Simulation::computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const
{ {
std::set<std::string> itemNames; std::vector<std::string> recipeIds;
for (const std::string& recipeId : hypothetical.recipeIds) for (const std::string& recipeId : hypothetical.recipeIds)
{ {
if (m_unlockedRecipeIds.count(recipeId) > 0) { continue; } if (m_unlockedRecipeIds.count(recipeId) > 0) { continue; }
for (const RecipeDef& def : m_config.recipes.recipes) recipeIds.push_back(recipeId);
}
std::sort(recipeIds.begin(), recipeIds.end(),
[](const std::string& lhs, const std::string& rhs)
{ {
if (def.id != recipeId) { continue; } return toDisplayName(lhs) < toDisplayName(rhs);
for (const RecipeOutput& out : def.outputs) });
{ return recipeIds;
itemNames.insert(toDisplayName(out.item));
}
break;
}
}
return std::vector<std::string>(itemNames.begin(), itemNames.end());
} }
bool Simulation::isRecipeUnlocked(const std::string& recipeId) const bool Simulation::isRecipeUnlocked(const std::string& recipeId) const

View File

@@ -233,9 +233,9 @@ private:
// True if every prerequisite in unlockRequires is explicitly unlocked (REQ-LOCK-PREREQ). // True if every prerequisite in unlockRequires is explicitly unlocked (REQ-LOCK-PREREQ).
bool prerequisitesSatisfied(const std::vector<std::string>& unlockRequires) const; bool prerequisitesSatisfied(const std::vector<std::string>& unlockRequires) const;
// Display names (deduplicated, alphabetical) of output items of recipes in // Ids (sorted alphabetically by display name) of the recipes in
// hypothetical.recipeIds that are not yet in m_unlockedRecipeIds. // hypothetical.recipeIds that are not yet in m_unlockedRecipeIds.
std::vector<std::string> computeNewlyUnlockedItemNames(const UnlockedSets& hypothetical) const; std::vector<std::string> computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const;
EntityAdmin m_admin; EntityAdmin m_admin;
BeltSystem m_beltSystem; BeltSystem m_beltSystem;

View File

@@ -984,6 +984,45 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
REQUIRE(i.target.x() < pos(f.admin, ship).value.x()); REQUIRE(i.target.x() < pos(f.admin, ship).value.x());
} }
TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo", "[behavior]")
{
Fixture f;
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
QPoint(-4, 0), Rotation::East, 0);
Tick t = 0;
for (int i = 0; i < 500; ++i)
{
f.buildings.tickConstruction(t++);
if (f.buildings.findBuilding(bayId) != nullptr) { break; }
}
const Building* bay = f.buildings.findBuilding(bayId);
REQUIRE(bay != nullptr);
// Config-driven output-buffer capacity is applied on placement (REQ-BLD-SALVAGE-BAY).
REQUIRE(bay->outputBuffer.capacity == 20);
const QVector2D bayCenter(bay->anchor.x() + bay->footprint.width() / 2.0f,
bay->anchor.y() + bay->footprint.height() / 2.0f);
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
const entt::entity ship = f.ships.spawn("salvage_ship", bayCenter, false, salvageLayout);
f.admin.get<PositionComponent>(ship).value = bayCenter;
CargoComponent& cargo = f.admin.get<CargoComponent>(ship);
cargo.current = cargo.maxCapacity; // full cargo
const int before = cargo.current;
REQUIRE(before > 0);
f.admin.get<DeliverScrapBehavior>(ship).deliveryBay = bayId;
f.salvageTick();
// One unit handed over from cargo into the bay's output buffer.
REQUIRE(f.admin.get<CargoComponent>(ship).current == before - 1);
const Building* bayAfter = f.buildings.findBuilding(bayId);
REQUIRE(bayAfter != nullptr);
REQUIRE(bayAfter->outputBuffer.items.size() == 1);
REQUIRE(bayAfter->outputBuffer.items.front().type.id == "scrap");
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Collection range (per-module) // Collection range (per-module)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -103,6 +103,15 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
REQUIRE(minerIt != cfg.buildings.buildings.end()); REQUIRE(minerIt != cfg.buildings.buildings.end());
REQUIRE(minerIt->cost == 15); REQUIRE(minerIt->cost == 15);
REQUIRE(minerIt->surfaceMask.size() == 2); REQUIRE(minerIt->surfaceMask.size() == 2);
// Miner has no output-buffer-capacity override; the Salvage Bay does.
REQUIRE_FALSE(minerIt->outputBufferCapacity.has_value());
const auto salvageBayIt = std::find_if(
cfg.buildings.buildings.begin(), cfg.buildings.buildings.end(),
[](const BuildingDef& b) { return b.type == BuildingType::SalvageBay; });
REQUIRE(salvageBayIt != cfg.buildings.buildings.end());
REQUIRE(salvageBayIt->outputBufferCapacity.has_value());
REQUIRE(*salvageBayIt->outputBufferCapacity == 20);
// recipes.toml — reprocessing cycle has three weighted outputs. // recipes.toml — reprocessing cycle has three weighted outputs.
const auto reproIt = std::find_if( const auto reproIt = std::find_if(

View File

@@ -283,7 +283,7 @@ TEST_CASE("RecipeSchematic: reset keeps -1 recipes unlocked and their seed items
// Unlock dialog: newly-unlocked recipe preview (REQ-DEF-SCHEMATIC-DROP) // Unlock dialog: newly-unlocked recipe preview (REQ-DEF-SCHEMATIC-DROP)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_CASE("RecipeSchematic: newlyUnlockedItemNames is sorted, deduplicated, and empty for level-ups", TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds is sorted, deduplicated, and empty for level-ups",
"[recipe_schematic]") "[recipe_schematic]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadConfig());
@@ -295,10 +295,11 @@ TEST_CASE("RecipeSchematic: newlyUnlockedItemNames is sorted, deduplicated, and
for (const SchematicChoiceOption& opt : sim.getPendingSchematicChoices()) for (const SchematicChoiceOption& opt : sim.getPendingSchematicChoices())
{ {
// Strictly ascending implies sorted and deduplicated. // Strictly ascending display names imply sorted and deduplicated.
for (std::size_t j = 1; j < opt.newlyUnlockedItemNames.size(); ++j) for (std::size_t j = 1; j < opt.newlyUnlockedRecipeIds.size(); ++j)
{ {
CHECK(opt.newlyUnlockedItemNames[j - 1] < opt.newlyUnlockedItemNames[j]); CHECK(toDisplayName(opt.newlyUnlockedRecipeIds[j - 1])
< toDisplayName(opt.newlyUnlockedRecipeIds[j]));
} }
} }
@@ -306,7 +307,7 @@ TEST_CASE("RecipeSchematic: newlyUnlockedItemNames is sorted, deduplicated, and
} }
} }
TEST_CASE("RecipeSchematic: newlyUnlockedItemNames matches recipes that actually become unlocked", TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds matches recipes that actually become unlocked",
"[recipe_schematic]") "[recipe_schematic]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadConfig());
@@ -336,21 +337,22 @@ TEST_CASE("RecipeSchematic: newlyUnlockedItemNames matches recipes that actually
SimulationTestAccess::applySchematicChoice(sim, 0); SimulationTestAccess::applySchematicChoice(sim, 0);
std::set<std::string> expectedNames; std::vector<std::string> expected;
for (const RecipeDef& def : cfg.recipes.recipes) for (const RecipeDef& def : cfg.recipes.recipes)
{ {
if ((def.building == BuildingType::Miner || def.building == BuildingType::Assembler) if ((def.building == BuildingType::Miner || def.building == BuildingType::Assembler)
&& sim.isRecipeUnlocked(def.id) && unlockedBefore.count(def.id) == 0) && sim.isRecipeUnlocked(def.id) && unlockedBefore.count(def.id) == 0)
{ {
for (const RecipeOutput& out : def.outputs) expected.push_back(def.id);
}
}
std::sort(expected.begin(), expected.end(),
[](const std::string& lhs, const std::string& rhs)
{ {
expectedNames.insert(toDisplayName(out.item)); return toDisplayName(lhs) < toDisplayName(rhs);
} });
}
}
const std::vector<std::string> expected(expectedNames.begin(), expectedNames.end());
REQUIRE(choice.newlyUnlockedItemNames == expected); REQUIRE(choice.newlyUnlockedRecipeIds == expected);
} }
} }

View File

@@ -53,23 +53,15 @@ BlueprintPanel::BlueprintPanel(Simulation* sim, const GameConfig* config, QWidge
connect(m_createBtn, &QPushButton::clicked, this, &BlueprintPanel::onCreateClicked); connect(m_createBtn, &QPushButton::clicked, this, &BlueprintPanel::onCreateClicked);
QHBoxLayout* ioLayout = new QHBoxLayout(); loadFromDisk();
m_saveBtn = new QPushButton(tr("Save"), this); rebuildButtons();
m_loadBtn = new QPushButton(tr("Load"), this);
m_saveBtn->setFixedHeight(36);
m_loadBtn->setFixedHeight(36);
ioLayout->addWidget(m_saveBtn);
ioLayout->addWidget(m_loadBtn);
layout->addLayout(ioLayout);
connect(m_saveBtn, &QPushButton::clicked, this, &BlueprintPanel::onSaveClicked);
connect(m_loadBtn, &QPushButton::clicked, this, &BlueprintPanel::onLoadClicked);
registerForEvents(); registerForEvents();
} }
BlueprintPanel::~BlueprintPanel() BlueprintPanel::~BlueprintPanel()
{ {
saveToDisk();
unregisterForEvents(); unregisterForEvents();
} }
@@ -281,65 +273,35 @@ void BlueprintPanel::rebuildButtons()
refreshButtonStates(); refreshButtonStates();
} }
void BlueprintPanel::onSaveClicked() void BlueprintPanel::saveToDisk() const
{ {
// Persist on shutdown; write errors are silently ignored (REQ-UI-BLUEPRINT-SAVE).
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml"; const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; }
try try
{ {
const std::string content = BlueprintSerializer::serialize(m_blueprints); const std::string content = BlueprintSerializer::serialize(m_blueprints);
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QMessageBox::critical(this, tr("Save Failed"),
tr("Could not open file for writing:\n%1").arg(path));
return;
}
file.write(QByteArray::fromStdString(content)); file.write(QByteArray::fromStdString(content));
} }
catch (const std::exception& e) catch (...) {}
{
QMessageBox::critical(this, tr("Save Failed"),
tr("Failed to save blueprints:\n%1").arg(e.what()));
}
} }
void BlueprintPanel::onLoadClicked() void BlueprintPanel::loadFromDisk()
{ {
QMessageBox box(this); // Load at startup (REQ-UI-BLUEPRINT-LOAD). Missing file: start empty, no error.
box.setWindowTitle(tr("Load Blueprints"));
box.setText(tr("Load blueprints? This will replace all current blueprints."));
QPushButton* confirmBtn = box.addButton(tr("Confirm"), QMessageBox::AcceptRole);
box.addButton(tr("Cancel"), QMessageBox::RejectRole);
box.exec();
if (box.clickedButton() != confirmBtn) { return; }
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml"; const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; }
try try
{ {
QFile file(path); m_blueprints = BlueprintSerializer::deserialize(file.readAll().toStdString());
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QMessageBox::critical(this, tr("Load Failed"),
tr("Could not open file:\n%1").arg(path));
return;
}
const std::string content = file.readAll().toStdString();
std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(content);
if (m_activeIndex >= 0)
{
EventManager::getInstance()->sendEventImmediately(
std::make_shared<ExitBlueprintModeRequestedEvent>());
m_activeIndex = -1;
}
m_blueprints = std::move(loaded);
rebuildButtons();
} }
catch (const std::exception& e) catch (const std::exception& e)
{ {
QMessageBox::critical(this, tr("Load Failed"), QMessageBox::critical(this, tr("Load Failed"),
tr("Failed to load blueprints:\n%1").arg(e.what())); tr("Failed to load blueprints:\n%1").arg(e.what()));
m_blueprints.clear();
} }
} }

View File

@@ -38,8 +38,6 @@ private slots:
void onCreateClicked(); void onCreateClicked();
void onDeleteBlueprintClicked(int index); void onDeleteBlueprintClicked(int index);
void onBlueprintButtonClicked(int index); void onBlueprintButtonClicked(int index);
void onSaveClicked();
void onLoadClicked();
private: private:
void onSelectionChanged(const std::vector<BuildingId>& ids); void onSelectionChanged(const std::vector<BuildingId>& ids);
@@ -48,6 +46,8 @@ private:
int computeBlueprintCost(const Blueprint& bp) const; int computeBlueprintCost(const Blueprint& bp) const;
void rebuildButtons(); void rebuildButtons();
void refreshButtonStates(); void refreshButtonStates();
void loadFromDisk();
void saveToDisk() const;
Simulation* m_sim; Simulation* m_sim;
const GameConfig* m_config; const GameConfig* m_config;
@@ -57,8 +57,6 @@ private:
std::vector<Blueprint> m_blueprints; std::vector<Blueprint> m_blueprints;
std::vector<QPushButton*> m_blueprintButtons; std::vector<QPushButton*> m_blueprintButtons;
QPushButton* m_createBtn; QPushButton* m_createBtn;
QPushButton* m_saveBtn;
QPushButton* m_loadBtn;
QWidget* m_buttonsContainer; QWidget* m_buttonsContainer;
QVBoxLayout* m_buttonsLayout; QVBoxLayout* m_buttonsLayout;
}; };

View File

@@ -13,6 +13,7 @@ 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
PARENT_SCOPE PARENT_SCOPE
) )
@@ -30,5 +31,6 @@ 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
PARENT_SCOPE PARENT_SCOPE
) )

View File

@@ -142,7 +142,7 @@ void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEven
const double prevSpeed = m_gameWorldView->gameSpeed(); const double prevSpeed = m_gameWorldView->gameSpeed();
m_gameWorldView->setGameSpeed(0.0); m_gameWorldView->setGameSpeed(0.0);
SchematicChoiceDialog dialog(event->choices, this); SchematicChoiceDialog dialog(event->choices, m_sim->config().recipes, this);
dialog.exec(); dialog.exec();
std::shared_ptr<ApplySchematicChoiceCommand> command = std::shared_ptr<ApplySchematicChoiceCommand> command =

View File

@@ -12,6 +12,7 @@
#include "DisplayName.h" #include "DisplayName.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "RecipesConfig.h" #include "RecipesConfig.h"
#include "RecipeTooltip.h"
#include "ShipsConfig.h" #include "ShipsConfig.h"
#include "Simulation.h" #include "Simulation.h"
@@ -25,39 +26,6 @@ QString itemLine(const std::string& itemId, int amount)
+ QStringLiteral(" ×") + QString::number(amount); + QStringLiteral(" ×") + QString::number(amount);
} }
QString recipeTooltip(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');
}
QString shipTooltip(const ShipDef& def) QString shipTooltip(const ShipDef& def)
{ {
const QString name = QString::fromStdString(toDisplayName(def.id)); const QString name = QString::fromStdString(toDisplayName(def.id));
@@ -116,7 +84,7 @@ std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
} }
options.push_back({recipe.id, options.push_back({recipe.id,
QString::fromStdString(toDisplayName(recipe.id)), QString::fromStdString(toDisplayName(recipe.id)),
recipeTooltip(recipe)}); buildRecipeTooltip(recipe)});
} }
} }

52
src/ui/RecipeTooltip.cpp Normal file
View File

@@ -0,0 +1,52 @@
#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(" ×") + 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');
}

11
src/ui/RecipeTooltip.h Normal file
View File

@@ -0,0 +1,11 @@
#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

@@ -3,11 +3,29 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QStringList>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "DisplayName.h"
#include "RecipeTooltip.h"
#include "RecipesConfig.h"
namespace
{
const RecipeDef* findRecipe(const RecipesConfig& recipes, const std::string& id)
{
for (const RecipeDef& recipe : recipes.recipes)
{
if (recipe.id == id) { return &recipe; }
}
return nullptr;
}
} // namespace
SchematicChoiceDialog::SchematicChoiceDialog( SchematicChoiceDialog::SchematicChoiceDialog(
const std::vector<SchematicChoiceOption>& options, const std::vector<SchematicChoiceOption>& options,
const RecipesConfig& recipes,
QWidget* parent) QWidget* parent)
: QDialog(parent) : QDialog(parent)
, m_chosenIndex(0) , m_chosenIndex(0)
@@ -70,30 +88,33 @@ SchematicChoiceDialog::SchematicChoiceDialog(
typeLabel->setAlignment(Qt::AlignCenter); typeLabel->setAlignment(Qt::AlignCenter);
cardLayout->addWidget(typeLabel); cardLayout->addWidget(typeLabel);
QLabel* unlocksHeaderLabel = new QLabel(tr("Unlocks recipes for:"), card); QLabel* unlocksHeaderLabel = new QLabel(tr("Unlocks recipes:"), card);
QFont unlocksHeaderFont = unlocksHeaderLabel->font(); QFont unlocksHeaderFont = unlocksHeaderLabel->font();
unlocksHeaderFont.setBold(true); unlocksHeaderFont.setBold(true);
unlocksHeaderLabel->setFont(unlocksHeaderFont); unlocksHeaderLabel->setFont(unlocksHeaderFont);
unlocksHeaderLabel->setAlignment(Qt::AlignCenter); unlocksHeaderLabel->setAlignment(Qt::AlignCenter);
cardLayout->addWidget(unlocksHeaderLabel); cardLayout->addWidget(unlocksHeaderLabel);
QString unlocksText; if (option.newlyUnlockedRecipeIds.empty())
if (option.newlyUnlockedItemNames.empty())
{ {
unlocksText = tr("None"); QLabel* noneLabel = new QLabel(tr("None"), card);
noneLabel->setAlignment(Qt::AlignCenter);
cardLayout->addWidget(noneLabel);
} }
else else
{ {
QStringList itemLines; for (const std::string& recipeId : option.newlyUnlockedRecipeIds)
for (const std::string& itemName : option.newlyUnlockedItemNames)
{ {
itemLines << QString::fromStdString(itemName); QLabel* recipeLabel = new QLabel(
QString::fromStdString(toDisplayName(recipeId)), card);
recipeLabel->setAlignment(Qt::AlignCenter);
if (const RecipeDef* def = findRecipe(recipes, recipeId))
{
recipeLabel->setToolTip(buildRecipeTooltip(*def));
}
cardLayout->addWidget(recipeLabel);
} }
unlocksText = itemLines.join("\n");
} }
QLabel* unlocksLabel = new QLabel(unlocksText, card);
unlocksLabel->setAlignment(Qt::AlignCenter);
cardLayout->addWidget(unlocksLabel);
} }
QPushButton* selectButton = new QPushButton(tr("Select"), card); QPushButton* selectButton = new QPushButton(tr("Select"), card);

View File

@@ -6,12 +6,15 @@
#include "SchematicChoiceOption.h" #include "SchematicChoiceOption.h"
struct RecipesConfig;
class SchematicChoiceDialog : public QDialog class SchematicChoiceDialog : public QDialog
{ {
Q_OBJECT Q_OBJECT
public: public:
SchematicChoiceDialog(const std::vector<SchematicChoiceOption>& options, SchematicChoiceDialog(const std::vector<SchematicChoiceOption>& options,
const RecipesConfig& recipes,
QWidget* parent = nullptr); QWidget* parent = nullptr);
int getChosenIndex() const; int getChosenIndex() const;