add UnlockState class for schematic/unlock bookkeeping
Simulation.h/.cpp had grown a large block of schematic/unlock state (containers, the implicit-unlock traversal, checksum folding) that has nothing to do with tick orchestration. Split it into its own class so Simulation stays legible as "tick orchestration + subsystem handles". This commit only adds the new UnlockState.h/.cpp (registered in CMakeLists.txt) with the containers, types, and logic moved in verbatim; nothing references it yet, so this is a no-op for behavior. Simulation is wired to use it in the next commit. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
This commit is contained in:
@@ -19,6 +19,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.h
|
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/UnlockState.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
@@ -40,6 +41,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/UnlockState.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
352
src/lib/sim/UnlockState.cpp
Normal file
352
src/lib/sim/UnlockState.cpp
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
#include "UnlockState.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "DisplayName.h"
|
||||||
|
#include "StateChecksum.h"
|
||||||
|
|
||||||
|
UnlockState::UnlockState(const GameConfig& config)
|
||||||
|
: m_config(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::initializeUnlockState()
|
||||||
|
{
|
||||||
|
// Cache the ids granted by some unlock group (REQ-LOCK-EXPLICIT); an item
|
||||||
|
// starts locked iff it is granted by a group.
|
||||||
|
m_grantedShipIds.clear();
|
||||||
|
m_grantedModuleIds.clear();
|
||||||
|
m_grantedBuildingIds.clear();
|
||||||
|
m_grantedRecipeIds.clear();
|
||||||
|
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
||||||
|
{
|
||||||
|
m_grantedShipIds.insert(group.ships.begin(), group.ships.end());
|
||||||
|
m_grantedModuleIds.insert(group.modules.begin(), group.modules.end());
|
||||||
|
m_grantedBuildingIds.insert(group.buildings.begin(), group.buildings.end());
|
||||||
|
m_grantedRecipeIds.insert(group.recipes.begin(), group.recipes.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_awardedUnlockGroupIds.clear();
|
||||||
|
|
||||||
|
m_schematicLevels.clear();
|
||||||
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
|
{
|
||||||
|
SchematicState state;
|
||||||
|
state.unlocked = (m_grantedShipIds.count(def.id) == 0);
|
||||||
|
m_schematicLevels[def.id] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_moduleSchematicLevels.clear();
|
||||||
|
for (const ModuleDef& def : m_config.modules.modules)
|
||||||
|
{
|
||||||
|
SchematicState state;
|
||||||
|
state.unlocked = (m_grantedModuleIds.count(def.id) == 0);
|
||||||
|
m_moduleSchematicLevels[def.id] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_buildingLevels.clear();
|
||||||
|
for (const BuildingDef& def : m_config.buildings.buildings)
|
||||||
|
{
|
||||||
|
SchematicState state;
|
||||||
|
state.unlocked = (m_grantedBuildingIds.count(def.id) == 0);
|
||||||
|
m_buildingLevels[def.id] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gated assembler recipes start locked; unlocked_at_start recipes are handled
|
||||||
|
// in the REQ-LOCK-IMPLICIT traversal, not tracked here.
|
||||||
|
m_unlockedRecipeSchematicIds.clear();
|
||||||
|
|
||||||
|
recomputeUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isSchematicUnlocked(const std::string& shipId) const
|
||||||
|
{
|
||||||
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
|
m_schematicLevels.find(shipId);
|
||||||
|
if (it == m_schematicLevels.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return it->second.unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isModuleSchematicUnlocked(const std::string& moduleId) const
|
||||||
|
{
|
||||||
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
|
m_moduleSchematicLevels.find(moduleId);
|
||||||
|
if (it == m_moduleSchematicLevels.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return it->second.unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isRecipeUnlocked(const std::string& recipeId) const
|
||||||
|
{
|
||||||
|
return m_unlockedRecipeIds.count(recipeId) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isItemUnlocked(const std::string& itemId) const
|
||||||
|
{
|
||||||
|
return m_unlockedItemIds.count(itemId) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isBuildingUnlocked(BuildingType type) const
|
||||||
|
{
|
||||||
|
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
||||||
|
if (def == nullptr)
|
||||||
|
{
|
||||||
|
// Types without a config entry (e.g. HQ, defence stations) are unrestricted.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
|
m_buildingLevels.find(def->id);
|
||||||
|
return it == m_buildingLevels.end() ? true : it->second.unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isUnlockGroupAwarded(const std::string& groupId) const
|
||||||
|
{
|
||||||
|
return m_awardedUnlockGroupIds.count(groupId) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const
|
||||||
|
{
|
||||||
|
// A prerequisite is satisfied only once the named unlock group has been
|
||||||
|
// awarded (REQ-LOCK-PREREQ).
|
||||||
|
for (const std::string& groupId : requiredGroupIds)
|
||||||
|
{
|
||||||
|
if (m_awardedUnlockGroupIds.count(groupId) == 0) { return false; }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SchematicChoiceOption UnlockState::makeUnlockOption(const UnlockGroupDef& group) const
|
||||||
|
{
|
||||||
|
SchematicChoiceOption option;
|
||||||
|
option.isArtifact = false;
|
||||||
|
option.unlockGroupId = group.id;
|
||||||
|
option.displayName = toDisplayName(group.id);
|
||||||
|
|
||||||
|
for (const std::string& id : group.ships)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Ship, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
for (const std::string& id : group.modules)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Module, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
for (const std::string& id : group.buildings)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Building, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
for (const std::string& id : group.recipes)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Recipe, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
|
||||||
|
// REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by
|
||||||
|
// awarding this whole group. Seed the hypothetical explicit-unlock sets with
|
||||||
|
// every grant (ship + module materials via step 1a, recipe outputs via step
|
||||||
|
// 1b), then diff against the current implicit set.
|
||||||
|
std::set<std::string> hypotheticalShipIds = getUnlockedShipSchematicIds();
|
||||||
|
std::set<std::string> hypotheticalModuleIds = getUnlockedModuleSchematicIds();
|
||||||
|
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
|
||||||
|
for (const std::string& id : group.ships) { hypotheticalShipIds.insert(id); }
|
||||||
|
for (const std::string& id : group.modules) { hypotheticalModuleIds.insert(id); }
|
||||||
|
for (const std::string& id : group.recipes) { hypotheticalRecipeSchematicIds.insert(id); }
|
||||||
|
|
||||||
|
const UnlockedSets hypothetical = computeUnlockedSets(
|
||||||
|
hypotheticalShipIds, hypotheticalModuleIds, hypotheticalRecipeSchematicIds);
|
||||||
|
option.newlyUnlockedRecipeIds = computeNewlyUnlockedRecipeIds(hypothetical);
|
||||||
|
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::awardUnlockGroup(const SchematicChoiceOption& chosen)
|
||||||
|
{
|
||||||
|
// Award the whole unlock group (REQ-DEF-SCHEMATIC-DROP): unlock every granted
|
||||||
|
// ship, module, building, and assembler recipe at once.
|
||||||
|
m_awardedUnlockGroupIds.insert(chosen.unlockGroupId);
|
||||||
|
for (const GrantedSchematic& grant : chosen.grantedItems)
|
||||||
|
{
|
||||||
|
switch (grant.type)
|
||||||
|
{
|
||||||
|
case SchematicType::Ship: m_schematicLevels.at(grant.id).unlocked = true; break;
|
||||||
|
case SchematicType::Module: m_moduleSchematicLevels.at(grant.id).unlocked = true; break;
|
||||||
|
case SchematicType::Building: m_buildingLevels.at(grant.id).unlocked = true; break;
|
||||||
|
case SchematicType::Recipe: m_unlockedRecipeSchematicIds.insert(grant.id); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recomputeUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Implicit unlock computation (REQ-LOCK-IMPLICIT)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void UnlockState::recomputeUnlocked()
|
||||||
|
{
|
||||||
|
const UnlockedSets result = computeUnlockedSets(
|
||||||
|
getUnlockedShipSchematicIds(), getUnlockedModuleSchematicIds(), m_unlockedRecipeSchematicIds);
|
||||||
|
m_unlockedItemIds = result.itemIds;
|
||||||
|
m_unlockedRecipeIds = result.recipeIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> UnlockState::getUnlockedShipSchematicIds() const
|
||||||
|
{
|
||||||
|
std::set<std::string> ids;
|
||||||
|
for (const auto& [id, state] : m_schematicLevels)
|
||||||
|
{
|
||||||
|
if (state.unlocked) { ids.insert(id); }
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> UnlockState::getUnlockedModuleSchematicIds() const
|
||||||
|
{
|
||||||
|
std::set<std::string> ids;
|
||||||
|
for (const auto& [id, state] : m_moduleSchematicLevels)
|
||||||
|
{
|
||||||
|
if (state.unlocked) { ids.insert(id); }
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnlockState::UnlockedSets UnlockState::computeUnlockedSets(
|
||||||
|
const std::set<std::string>& unlockedShipSchematicIds,
|
||||||
|
const std::set<std::string>& unlockedModuleSchematicIds,
|
||||||
|
const std::set<std::string>& unlockedRecipeSchematicIds) const
|
||||||
|
{
|
||||||
|
UnlockedSets result;
|
||||||
|
|
||||||
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
|
{
|
||||||
|
if (unlockedShipSchematicIds.count(def.id) == 0) { continue; }
|
||||||
|
for (const RecipeIngredient& mat : def.schematic.materials)
|
||||||
|
{
|
||||||
|
result.itemIds.insert(mat.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const ModuleDef& def : m_config.modules.modules)
|
||||||
|
{
|
||||||
|
if (unlockedModuleSchematicIds.count(def.id) == 0) { continue; }
|
||||||
|
for (const RecipeIngredient& mat : def.materials)
|
||||||
|
{
|
||||||
|
result.itemIds.insert(mat.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||||
|
{
|
||||||
|
// An assembler recipe seeds the base set when it is explicitly available:
|
||||||
|
// flagged unlocked_at_start (base recipes the graph can't reach), or a
|
||||||
|
// gated recipe whose unlock group has been awarded (REQ-LOCK-EXPLICIT).
|
||||||
|
if (def.building == BuildingType::Assembler
|
||||||
|
&& (def.unlockedAtStart || unlockedRecipeSchematicIds.count(def.id) > 0))
|
||||||
|
{
|
||||||
|
for (const RecipeOutput& out : def.outputs)
|
||||||
|
{
|
||||||
|
result.itemIds.insert(out.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = true;
|
||||||
|
while (changed)
|
||||||
|
{
|
||||||
|
changed = false;
|
||||||
|
for (const RecipeDef& recipe : m_config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (recipe.building != BuildingType::Miner
|
||||||
|
&& recipe.building != BuildingType::Smelter
|
||||||
|
&& recipe.building != BuildingType::Assembler)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Skip a gated assembler recipe (granted by an unlock group) whose
|
||||||
|
// group has not yet been awarded (REQ-LOCK-IMPLICIT step 2).
|
||||||
|
if (recipe.building == BuildingType::Assembler
|
||||||
|
&& m_grantedRecipeIds.count(recipe.id) > 0
|
||||||
|
&& unlockedRecipeSchematicIds.count(recipe.id) == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool producesUnlocked = false;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
if (result.itemIds.count(out.item) > 0)
|
||||||
|
{
|
||||||
|
producesUnlocked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!producesUnlocked) { continue; }
|
||||||
|
|
||||||
|
if (recipe.building == BuildingType::Miner
|
||||||
|
|| recipe.building == BuildingType::Assembler)
|
||||||
|
{
|
||||||
|
result.recipeIds.insert(recipe.id);
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
|
{
|
||||||
|
if (result.itemIds.insert(ing.item).second)
|
||||||
|
{
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> UnlockState::computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const
|
||||||
|
{
|
||||||
|
std::vector<std::string> recipeIds;
|
||||||
|
for (const std::string& recipeId : hypothetical.recipeIds)
|
||||||
|
{
|
||||||
|
if (m_unlockedRecipeIds.count(recipeId) > 0) { continue; }
|
||||||
|
recipeIds.push_back(recipeId);
|
||||||
|
}
|
||||||
|
std::sort(recipeIds.begin(), recipeIds.end(),
|
||||||
|
[](const std::string& lhs, const std::string& rhs)
|
||||||
|
{
|
||||||
|
return toDisplayName(lhs) < toDisplayName(rhs);
|
||||||
|
});
|
||||||
|
return recipeIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Determinism (see docs/replay_design.md)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void UnlockState::appendSchematicMap(Hasher& hasher,
|
||||||
|
const std::map<std::string, SchematicState>& levels)
|
||||||
|
{
|
||||||
|
hasher.append(levels.size());
|
||||||
|
for (const std::pair<const std::string, SchematicState>& entry : levels)
|
||||||
|
{
|
||||||
|
hasher.append(entry.first);
|
||||||
|
hasher.append(entry.second.unlocked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::appendStringSet(Hasher& hasher, const std::set<std::string>& ids)
|
||||||
|
{
|
||||||
|
hasher.append(ids.size());
|
||||||
|
for (const std::string& id : ids)
|
||||||
|
{
|
||||||
|
hasher.append(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::appendChecksum(Hasher& hasher) const
|
||||||
|
{
|
||||||
|
appendSchematicMap(hasher, m_schematicLevels);
|
||||||
|
appendSchematicMap(hasher, m_moduleSchematicLevels);
|
||||||
|
appendSchematicMap(hasher, m_buildingLevels);
|
||||||
|
appendStringSet(hasher, m_awardedUnlockGroupIds);
|
||||||
|
appendStringSet(hasher, m_unlockedRecipeSchematicIds);
|
||||||
|
appendStringSet(hasher, m_unlockedRecipeIds);
|
||||||
|
appendStringSet(hasher, m_unlockedItemIds);
|
||||||
|
}
|
||||||
129
src/lib/sim/UnlockState.h
Normal file
129
src/lib/sim/UnlockState.h
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user