Files
dota_factory/src/lib/sim/UnlockState.h
Malte Langkabel f5001d9ea1 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
2026-08-21 23:12:02 +02:00

138 lines
6.1 KiB
C++

#pragma once
#include <map>
#include <set>
#include <string>
#include <vector>
#include "BuildingType.h"
#include "GameConfig.h"
#include "SchematicChoiceOption.h"
class Hasher;
// Owns schematic/unlock bookkeeping for one run: which ship, module, and
// building schematics are unlocked (REQ-LOCK-EXPLICIT), which assembler recipe
// schematics have been explicitly granted, and the implicit recipe/item unlock
// sets derived from that state (REQ-LOCK-IMPLICIT). Reads config the same way
// BuildingSystem does (a bound const reference to Simulation::m_config, which is
// safe across restart because that member's storage address never changes —
// reset() move-assigns into it rather than replacing it).
//
// Simulation forwards its isXUnlocked-style public queries here and drives
// state changes (awarding an unlock group) here; the RNG-touching schematic
// choice generation itself stays in Simulation (call ordering of m_rng is the
// determinism backbone and must not move).
class UnlockState
{
public:
explicit UnlockState(const GameConfig& config);
// Builds the granted-id sets and initializes all per-item unlock maps from
// them (shared by the constructor and Simulation::reset). Ends with
// recomputeUnlocked().
void initializeUnlockState();
// Ship schematic state query.
bool isSchematicUnlocked(const std::string& shipId) const;
// Module schematic state query.
bool isModuleSchematicUnlocked(const std::string& moduleId) const;
// Implicit recipe/item unlock queries (REQ-LOCK-IMPLICIT).
bool isRecipeUnlocked(const std::string& recipeId) const;
bool isItemUnlocked(const std::string& itemId) const;
// Building unlock query (REQ-LOCK-BUILDING). True if the building type is not
// gated by any unlock group, or its granting group has been awarded.
bool isBuildingUnlocked(BuildingType type) const;
// True if the unlock group has already been awarded to the player.
bool isUnlockGroupAwarded(const std::string& groupId) const;
// True if every prerequisite unlock group has been awarded (REQ-LOCK-PREREQ).
bool prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const;
// Builds a schematic choice option for one unlock group (REQ-DEF-SCHEMATIC-DROP).
SchematicChoiceOption makeUnlockOption(const UnlockGroupDef& group) const;
// Awards the unlock group backing `chosen` (REQ-DEF-SCHEMATIC-DROP): marks
// every granted ship/module/building schematic unlocked, records granted
// recipe schematics, marks the group as awarded, and recomputes the implicit
// unlock sets. Mirrors the non-artifact branch of the original
// Simulation::applySchematicChoice exactly; callers still special-case
// chosen.isArtifact themselves before calling this.
void awardUnlockGroup(const SchematicChoiceOption& chosen);
// Determinism helper (see Simulation::computeStateChecksum): folds unlock
// state into the hasher via the same seven calls, in the same order, that
// used to live at the Simulation::computeStateChecksum call site.
void appendChecksum(Hasher& hasher) const;
private:
// Schematic unlock state (REQ-DEF-SCHEMATIC-DROP).
struct SchematicState
{
bool unlocked;
};
// Recomputes m_unlockedRecipeIds and m_unlockedItemIds from current schematic state.
void recomputeUnlocked();
// Result of the REQ-LOCK-IMPLICIT traversal.
struct UnlockedSets
{
std::set<std::string> itemIds;
std::set<std::string> recipeIds;
};
// Pure REQ-LOCK-IMPLICIT traversal given hypothetical explicit-unlock sets.
UnlockedSets computeUnlockedSets(const std::set<std::string>& unlockedShipSchematicIds,
const std::set<std::string>& unlockedModuleSchematicIds,
const std::set<std::string>& unlockedRecipeSchematicIds) const;
// 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;
// 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,
const std::map<std::string, SchematicState>& levels);
static void appendStringSet(Hasher& hasher, const std::set<std::string>& ids);
const GameConfig& m_config;
std::map<std::string, SchematicState> m_schematicLevels;
std::map<std::string, SchematicState> m_moduleSchematicLevels;
std::map<std::string, SchematicState> m_buildingLevels;
// Unlock groups awarded so far (REQ-LOCK-EXPLICIT). Group ids.
std::set<std::string> m_awardedUnlockGroupIds;
// Ids granted by some unlock group, per kind — cached from config at init.
// An item starts locked iff it appears in the corresponding set.
std::set<std::string> m_grantedShipIds;
std::set<std::string> m_grantedModuleIds;
std::set<std::string> m_grantedBuildingIds;
std::set<std::string> m_grantedRecipeIds;
// Explicitly unlocked assembler recipe schematics (REQ-LOCK-EXPLICIT).
std::set<std::string> m_unlockedRecipeSchematicIds;
// Implicit unlock sets derived from schematic state (REQ-LOCK-IMPLICIT).
std::set<std::string> m_unlockedRecipeIds;
std::set<std::string> m_unlockedItemIds;
};