preview what an award adds, not which recipes it unlocks

The unlock dialog drew each previewed recipe with every output group its config
lists. Filtering those against the current unlock state would have been worse
than not filtering: these recipes are previewed precisely because the award
changes what they may yield, so the current state would have emptied the very
lines the preview exists to show.

So the preview stops being a recipe-id diff and becomes a usability diff.
A recipe is usable when the player may run it, may place the building that runs
it, and it has a group it could be handed; the option carries the difference
between usable-now and usable-after, per recipe, down to which groups. The
dialog then draws what it was handed instead of re-deriving eligibility against
a state that is not the one being previewed.

Two consequences fall out of the finer diff, both wanted:

Unlocking a building is now a gain in its own right. A recipe unlocked long ago
but useless for want of a plant belongs to the award that finally makes it
usable, not to the one that unlocked it.

A recipe that gains a group is a gain a list of names cannot express -- a plant
that starts yielding voidsteel is the game's headline unlock. Those get their
own list under "Upgrades recipes:", because a recipe the player already runs,
listed as newly unlocked, reads as a bug. Each line is drawn whole, groups the
player already had included; the caption carries the difference.

toOutputGroups' unfiltered form has no callers left and is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-21 23:12:02 +02:00
parent 63975d9ca8
commit f5001d9ea1
8 changed files with 431 additions and 113 deletions

View File

@@ -94,10 +94,18 @@ private:
// Current explicit-unlock id sets, derived from m_schematicLevels / m_moduleSchematicLevels.
std::set<std::string> getUnlockedShipSchematicIds() const;
std::set<std::string> getUnlockedModuleSchematicIds() const;
std::set<std::string> getUnlockedBuildingIds() const;
// Ids (sorted alphabetically by display name) of the recipes in
// hypothetical.recipeIds that are not yet in m_unlockedRecipeIds.
std::vector<std::string> computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const;
// What the player can actually get out of the game in a given state: for every recipe
// they could run, which of its output groups they could be handed. A recipe absent
// from the map is of no use to them -- either it is locked (REQ-LOCK-IMPLICIT) or the
// building that runs it cannot be placed (REQ-LOCK-BUILDING), and a recipe they
// cannot run is no different from one that does not exist.
//
// Diffing this between the current state and the state an award would bring about is
// what the schematic choice dialog previews (REQ-DEF-SCHEMATIC-DROP).
std::map<std::string, std::vector<int>> computeUsableRecipeGroups(
const UnlockedSets& sets, const std::set<std::string>& unlockedBuildingIds) const;
// Determinism helpers — fold sub-state into the hasher in deterministic order.
static void appendSchematicMap(Hasher& hasher,