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
This commit is contained in:
2026-07-09 13:34:55 +02:00
parent 410c9646b8
commit 44c3c83080
11 changed files with 134 additions and 78 deletions

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)
// ---------------------------------------------------------------------------
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]")
{
Simulation sim(loadConfig());
@@ -295,10 +295,11 @@ TEST_CASE("RecipeSchematic: newlyUnlockedItemNames is sorted, deduplicated, and
for (const SchematicChoiceOption& opt : sim.getPendingSchematicChoices())
{
// Strictly ascending implies sorted and deduplicated.
for (std::size_t j = 1; j < opt.newlyUnlockedItemNames.size(); ++j)
// Strictly ascending display names imply sorted and deduplicated.
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]")
{
Simulation sim(loadConfig());
@@ -336,21 +337,22 @@ TEST_CASE("RecipeSchematic: newlyUnlockedItemNames matches recipes that actually
SimulationTestAccess::applySchematicChoice(sim, 0);
std::set<std::string> expectedNames;
std::vector<std::string> expected;
for (const RecipeDef& def : cfg.recipes.recipes)
{
if ((def.building == BuildingType::Miner || def.building == BuildingType::Assembler)
&& sim.isRecipeUnlocked(def.id) && unlockedBefore.count(def.id) == 0)
{
for (const RecipeOutput& out : def.outputs)
{
expectedNames.insert(toDisplayName(out.item));
}
expected.push_back(def.id);
}
}
const std::vector<std::string> expected(expectedNames.begin(), expectedNames.end());
std::sort(expected.begin(), expected.end(),
[](const std::string& lhs, const std::string& rhs)
{
return toDisplayName(lhs) < toDisplayName(rhs);
});
REQUIRE(choice.newlyUnlockedItemNames == expected);
REQUIRE(choice.newlyUnlockedRecipeIds == expected);
}
}