Implement config-driven unlock groups so that multiple things can be unlocked at once (including buildings)
This commit is contained in:
@@ -75,34 +75,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
||||
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
||||
|
||||
// Initialize ship schematic unlock state.
|
||||
for (const ShipDef& def : m_config.ships.ships)
|
||||
{
|
||||
SchematicState state;
|
||||
state.unlocked = (def.unlockAtStationLevel == -1);
|
||||
m_schematicLevels[def.id] = state;
|
||||
}
|
||||
|
||||
// Initialize module schematic unlock state.
|
||||
for (const ModuleDef& def : m_config.modules.modules)
|
||||
{
|
||||
SchematicState state;
|
||||
state.unlocked = (def.unlockAtStationLevel == -1);
|
||||
m_moduleSchematicLevels[def.id] = state;
|
||||
}
|
||||
|
||||
// Initialize assembler recipe schematic unlock state.
|
||||
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||
{
|
||||
if (def.building == BuildingType::Assembler
|
||||
&& def.unlockAtStationLevel.has_value()
|
||||
&& def.unlockAtStationLevel.value() == -1)
|
||||
{
|
||||
m_unlockedRecipeSchematicIds.insert(def.id);
|
||||
}
|
||||
}
|
||||
|
||||
recomputeUnlocked();
|
||||
initializeUnlockState();
|
||||
placeInitialStructures();
|
||||
registerForEvents();
|
||||
}
|
||||
@@ -174,11 +147,33 @@ void Simulation::reset(unsigned int seed)
|
||||
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
||||
|
||||
initializeUnlockState();
|
||||
placeInitialStructures();
|
||||
}
|
||||
|
||||
void Simulation::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 = (def.unlockAtStationLevel == -1);
|
||||
state.unlocked = (m_grantedShipIds.count(def.id) == 0);
|
||||
m_schematicLevels[def.id] = state;
|
||||
}
|
||||
|
||||
@@ -186,23 +181,23 @@ void Simulation::reset(unsigned int seed)
|
||||
for (const ModuleDef& def : m_config.modules.modules)
|
||||
{
|
||||
SchematicState state;
|
||||
state.unlocked = (def.unlockAtStationLevel == -1);
|
||||
state.unlocked = (m_grantedModuleIds.count(def.id) == 0);
|
||||
m_moduleSchematicLevels[def.id] = state;
|
||||
}
|
||||
|
||||
m_unlockedRecipeSchematicIds.clear();
|
||||
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||
m_buildingLevels.clear();
|
||||
for (const BuildingDef& def : m_config.buildings.buildings)
|
||||
{
|
||||
if (def.building == BuildingType::Assembler
|
||||
&& def.unlockAtStationLevel.has_value()
|
||||
&& def.unlockAtStationLevel.value() == -1)
|
||||
{
|
||||
m_unlockedRecipeSchematicIds.insert(def.id);
|
||||
}
|
||||
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();
|
||||
placeInitialStructures();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -628,43 +623,16 @@ void Simulation::tickDeathsAndLoot()
|
||||
|
||||
void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
||||
{
|
||||
enum class DropType { Ship, Module, Recipe };
|
||||
struct PoolEntry { std::string id; DropType type; };
|
||||
std::vector<PoolEntry> pool;
|
||||
|
||||
// Owned schematics leave the pool (REQ-DEF-SCHEMATIC-DROP): only offer
|
||||
// ship/module schematics that are gated to a station level in range and not
|
||||
// yet unlocked. Schematics with unlock_at_station_level -1 start unlocked, so
|
||||
// they are always owned and never eligible.
|
||||
for (const ShipDef& def : m_config.ships.ships)
|
||||
// Build the eligible pool of unlock groups (REQ-DEF-SCHEMATIC-DROP,
|
||||
// REQ-LOCK-EXPLICIT, REQ-LOCK-PREREQ): not yet awarded, station level in
|
||||
// range, and every prerequisite group already awarded.
|
||||
std::vector<const UnlockGroupDef*> pool;
|
||||
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
||||
{
|
||||
if (def.unlockAtStationLevel < 0 || def.unlockAtStationLevel > destroyedStationLevel) { continue; }
|
||||
if (m_schematicLevels.at(def.id).unlocked) { continue; }
|
||||
if (!prerequisitesSatisfied(def.unlockRequires)) { continue; }
|
||||
pool.push_back({def.id, DropType::Ship});
|
||||
}
|
||||
for (const ModuleDef& def : m_config.modules.modules)
|
||||
{
|
||||
if (def.unlockAtStationLevel < 0 || def.unlockAtStationLevel > destroyedStationLevel) { continue; }
|
||||
if (m_moduleSchematicLevels.at(def.id).unlocked) { continue; }
|
||||
if (!prerequisitesSatisfied(def.unlockRequires)) { continue; }
|
||||
pool.push_back({def.id, DropType::Module});
|
||||
}
|
||||
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||
{
|
||||
if (def.building != BuildingType::Assembler) { continue; }
|
||||
if (!def.unlockAtStationLevel.has_value()) { continue; }
|
||||
const int level = def.unlockAtStationLevel.value();
|
||||
if (level < 0 || level > destroyedStationLevel) { continue; }
|
||||
if (m_unlockedRecipeSchematicIds.count(def.id) > 0) { continue; }
|
||||
bool outputUnlocked = false;
|
||||
for (const RecipeOutput& out : def.outputs)
|
||||
{
|
||||
if (m_unlockedItemIds.count(out.item) > 0) { outputUnlocked = true; break; }
|
||||
}
|
||||
if (!outputUnlocked) { continue; }
|
||||
if (!prerequisitesSatisfied(def.unlockRequires)) { continue; }
|
||||
pool.push_back({def.id, DropType::Recipe});
|
||||
if (m_awardedUnlockGroupIds.count(group.id) > 0) { continue; }
|
||||
if (group.stationLevel < 0 || group.stationLevel > destroyedStationLevel) { continue; }
|
||||
if (!prerequisitesSatisfied(group.requiredGroupIds)) { continue; }
|
||||
pool.push_back(&group);
|
||||
}
|
||||
|
||||
if (pool.empty()) { return; }
|
||||
@@ -679,9 +647,6 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
||||
const int numChoices = std::min(static_cast<int>(pool.size()), artifactRolled ? 2 : 3);
|
||||
m_pendingSchematicChoices.clear();
|
||||
|
||||
const std::set<std::string> currentShipIds = getUnlockedShipSchematicIds();
|
||||
const std::set<std::string> currentModuleIds = getUnlockedModuleSchematicIds();
|
||||
|
||||
for (int i = 0; i < numChoices; ++i)
|
||||
{
|
||||
std::uniform_int_distribution<int> dist(0, static_cast<int>(pool.size()) - 1 - i);
|
||||
@@ -689,68 +654,67 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
||||
const std::size_t rollIdx = static_cast<std::size_t>(roll);
|
||||
const std::size_t endIdx = pool.size() - 1 - static_cast<std::size_t>(i);
|
||||
std::swap(pool[rollIdx], pool[endIdx]);
|
||||
const PoolEntry& entry = pool[endIdx];
|
||||
|
||||
SchematicChoiceOption option;
|
||||
option.schematicId = entry.id;
|
||||
|
||||
if (entry.type == DropType::Ship)
|
||||
{
|
||||
option.type = SchematicType::Ship;
|
||||
option.displayName = toDisplayName(entry.id);
|
||||
}
|
||||
else if (entry.type == DropType::Module)
|
||||
{
|
||||
option.type = SchematicType::Module;
|
||||
option.displayName = toDisplayName(entry.id);
|
||||
}
|
||||
else
|
||||
{
|
||||
option.type = SchematicType::Recipe;
|
||||
option.displayName = toDisplayName(entry.id);
|
||||
}
|
||||
|
||||
// REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by this option.
|
||||
std::set<std::string> hypotheticalShipIds = currentShipIds;
|
||||
std::set<std::string> hypotheticalModuleIds = currentModuleIds;
|
||||
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
|
||||
|
||||
if (entry.type == DropType::Ship)
|
||||
{
|
||||
hypotheticalShipIds.insert(entry.id);
|
||||
}
|
||||
else if (entry.type == DropType::Module)
|
||||
{
|
||||
hypotheticalModuleIds.insert(entry.id);
|
||||
}
|
||||
else if (entry.type == DropType::Recipe)
|
||||
{
|
||||
hypotheticalRecipeSchematicIds.insert(entry.id);
|
||||
}
|
||||
|
||||
const UnlockedSets hypothetical = computeUnlockedSets(
|
||||
hypotheticalShipIds, hypotheticalModuleIds, hypotheticalRecipeSchematicIds);
|
||||
option.newlyUnlockedRecipeIds = computeNewlyUnlockedRecipeIds(hypothetical);
|
||||
|
||||
m_pendingSchematicChoices.push_back(option);
|
||||
m_pendingSchematicChoices.push_back(makeUnlockOption(*pool[endIdx]));
|
||||
}
|
||||
|
||||
if (artifactRolled)
|
||||
{
|
||||
SchematicChoiceOption artifactOption;
|
||||
artifactOption.schematicId = "";
|
||||
artifactOption.type = SchematicType::Artifact;
|
||||
artifactOption.displayName = "Artifact";
|
||||
artifactOption.isArtifact = true;
|
||||
artifactOption.displayName = "Artifact";
|
||||
m_pendingSchematicChoices.push_back(std::move(artifactOption));
|
||||
}
|
||||
}
|
||||
|
||||
SchematicChoiceOption Simulation::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 Simulation::applySchematicChoice(int choiceIndex)
|
||||
{
|
||||
assert(choiceIndex >= 0 && choiceIndex < static_cast<int>(m_pendingSchematicChoices.size()));
|
||||
const SchematicChoiceOption& chosen = m_pendingSchematicChoices[static_cast<std::size_t>(choiceIndex)];
|
||||
|
||||
if (chosen.type == SchematicType::Artifact)
|
||||
if (chosen.isArtifact)
|
||||
{
|
||||
m_artifactCount += 1;
|
||||
if (m_artifactCount >= m_config.world.artifacts.artifactWinCount)
|
||||
@@ -761,16 +725,18 @@ void Simulation::applySchematicChoice(int choiceIndex)
|
||||
return;
|
||||
}
|
||||
|
||||
if (chosen.type == SchematicType::Recipe)
|
||||
// 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)
|
||||
{
|
||||
m_unlockedRecipeSchematicIds.insert(chosen.schematicId);
|
||||
}
|
||||
else
|
||||
{
|
||||
SchematicState& state = (chosen.type == SchematicType::Module)
|
||||
? m_moduleSchematicLevels.at(chosen.schematicId)
|
||||
: m_schematicLevels.at(chosen.schematicId);
|
||||
state.unlocked = true;
|
||||
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();
|
||||
@@ -809,28 +775,13 @@ std::set<std::string> Simulation::getUnlockedModuleSchematicIds() const
|
||||
return ids;
|
||||
}
|
||||
|
||||
bool Simulation::isSchematicIdExplicitlyUnlocked(const std::string& schematicId) const
|
||||
bool Simulation::prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const
|
||||
{
|
||||
// A prerequisite is satisfied only by an explicitly-unlocked schematic
|
||||
// (REQ-LOCK-PREREQ): a ship or module schematic marked unlocked, or a recipe
|
||||
// schematic in the explicit-unlock set. Cross-type ids are looked up with
|
||||
// find/count so an id from one namespace never throws against another.
|
||||
const std::map<std::string, SchematicState>::const_iterator shipIt =
|
||||
m_schematicLevels.find(schematicId);
|
||||
if (shipIt != m_schematicLevels.end() && shipIt->second.unlocked) { return true; }
|
||||
|
||||
const std::map<std::string, SchematicState>::const_iterator moduleIt =
|
||||
m_moduleSchematicLevels.find(schematicId);
|
||||
if (moduleIt != m_moduleSchematicLevels.end() && moduleIt->second.unlocked) { return true; }
|
||||
|
||||
return m_unlockedRecipeSchematicIds.count(schematicId) > 0;
|
||||
}
|
||||
|
||||
bool Simulation::prerequisitesSatisfied(const std::vector<std::string>& unlockRequires) const
|
||||
{
|
||||
for (const std::string& requiredId : unlockRequires)
|
||||
// A prerequisite is satisfied only once the named unlock group has been
|
||||
// awarded (REQ-LOCK-PREREQ).
|
||||
for (const std::string& groupId : requiredGroupIds)
|
||||
{
|
||||
if (!isSchematicIdExplicitlyUnlocked(requiredId)) { return false; }
|
||||
if (m_awardedUnlockGroupIds.count(groupId) == 0) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -860,9 +811,11 @@ Simulation::UnlockedSets Simulation::computeUnlockedSets(
|
||||
}
|
||||
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.unlockAtStationLevel.has_value()
|
||||
&& unlockedRecipeSchematicIds.count(def.id) > 0)
|
||||
&& (def.unlockedAtStart || unlockedRecipeSchematicIds.count(def.id) > 0))
|
||||
{
|
||||
for (const RecipeOutput& out : def.outputs)
|
||||
{
|
||||
@@ -883,8 +836,10 @@ Simulation::UnlockedSets Simulation::computeUnlockedSets(
|
||||
{
|
||||
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
|
||||
&& recipe.unlockAtStationLevel.has_value()
|
||||
&& m_grantedRecipeIds.count(recipe.id) > 0
|
||||
&& unlockedRecipeSchematicIds.count(recipe.id) == 0)
|
||||
{
|
||||
continue;
|
||||
@@ -1000,6 +955,8 @@ unsigned long long Simulation::computeStateChecksum() const
|
||||
// Schematic / unlock state (std::map and std::set iterate in sorted order).
|
||||
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);
|
||||
@@ -1193,8 +1150,28 @@ bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
|
||||
return it->second.unlocked;
|
||||
}
|
||||
|
||||
bool Simulation::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;
|
||||
}
|
||||
|
||||
std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
|
||||
{
|
||||
// Locked building types cannot be placed (REQ-LOCK-BUILDING); the build menu
|
||||
// hides them, this is the simulation-side backstop (e.g. blueprint placement).
|
||||
if (!isBuildingUnlocked(type))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!m_buildingSystem->isPlacementValid(type, anchor, rotation))
|
||||
{
|
||||
return std::nullopt;
|
||||
|
||||
@@ -100,6 +100,10 @@ public:
|
||||
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;
|
||||
|
||||
// -- Determinism (see docs/replay_design.md) -----------------------------
|
||||
// 64-bit fingerprint of the RNG stream state. Cheap; written to the replay
|
||||
// file periodically + after each command for desync detection.
|
||||
@@ -196,6 +200,24 @@ private:
|
||||
};
|
||||
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;
|
||||
|
||||
// Builds the granted-id sets and initializes all per-item unlock maps from
|
||||
// them (shared by the constructor and reset). Ends with recomputeUnlocked().
|
||||
void initializeUnlockState();
|
||||
|
||||
// Builds a schematic choice option for one unlock group (REQ-DEF-SCHEMATIC-DROP).
|
||||
SchematicChoiceOption makeUnlockOption(const UnlockGroupDef& group) const;
|
||||
|
||||
// Determinism helpers — fold sub-state into the hasher in deterministic order.
|
||||
static void appendSchematicMap(Hasher& hasher,
|
||||
@@ -228,11 +250,8 @@ private:
|
||||
std::set<std::string> getUnlockedShipSchematicIds() const;
|
||||
std::set<std::string> getUnlockedModuleSchematicIds() const;
|
||||
|
||||
// True if schematicId names a currently explicitly-unlocked ship, module, or
|
||||
// assembler recipe schematic (REQ-LOCK-PREREQ satisfaction basis).
|
||||
bool isSchematicIdExplicitlyUnlocked(const std::string& schematicId) const;
|
||||
// True if every prerequisite in unlockRequires is explicitly unlocked (REQ-LOCK-PREREQ).
|
||||
bool prerequisitesSatisfied(const std::vector<std::string>& unlockRequires) const;
|
||||
// True if every prerequisite unlock group has been awarded (REQ-LOCK-PREREQ).
|
||||
bool prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const;
|
||||
|
||||
// Ids (sorted alphabetically by display name) of the recipes in
|
||||
// hypothetical.recipeIds that are not yet in m_unlockedRecipeIds.
|
||||
|
||||
Reference in New Issue
Block a user