explicit recipe unlocking

This commit is contained in:
2026-06-12 16:39:18 +02:00
parent 54a6056b77
commit 1641189b75
9 changed files with 404 additions and 24 deletions

View File

@@ -367,6 +367,15 @@ RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
}
def.building = *parsedType;
if (def.building == BuildingType::Assembler)
{
const auto level = mt["unlock_at_station_level"].value<int64_t>();
if (level)
{
def.unlockAtStationLevel = static_cast<int>(*level);
}
}
// inputs may be omitted (e.g. miner recipes). An empty array is fine.
if (mt.contains("inputs"))
{

View File

@@ -32,6 +32,10 @@ struct RecipeDef
std::vector<RecipeIngredient> inputs;
std::vector<RecipeOutput> outputs;
double durationSeconds;
// Assembler only. nullopt = implicit-only locking. -1 = explicitly unlocked
// at game start. >= 0 = locked; schematic enters drop pool at that station
// level once the output item is implicitly unlocked (REQ-LOCK-EXPLICIT).
std::optional<int> unlockAtStationLevel;
};
struct RecipesConfig

View File

@@ -87,6 +87,17 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
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();
placeInitialStructures();
registerForEvents();
@@ -177,6 +188,17 @@ void Simulation::reset(unsigned int seed)
m_moduleSchematicLevels[def.id] = state;
}
m_unlockedRecipeSchematicIds.clear();
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();
placeInitialStructures();
}
@@ -516,40 +538,66 @@ void Simulation::tickDeathsAndLoot()
void Simulation::awardSchematicDrop(int destroyedStationLevel)
{
std::vector<std::pair<std::string, bool>> pool; // (id, isModule)
enum class DropType { Ship, Module, Recipe };
struct PoolEntry { std::string id; DropType type; };
std::vector<PoolEntry> pool;
for (const ShipDef& def : m_config.ships.ships)
{
if (def.unlockAtStationLevel == -1 || def.unlockAtStationLevel <= destroyedStationLevel)
{
pool.push_back({def.id, false});
pool.push_back({def.id, DropType::Ship});
}
}
for (const ModuleDef& def : m_config.modules.modules)
{
if (def.unlockAtStationLevel == -1 || def.unlockAtStationLevel <= destroyedStationLevel)
{
pool.push_back({def.id, true});
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; }
pool.push_back({def.id, DropType::Recipe});
}
std::uniform_int_distribution<int> dist(0, static_cast<int>(pool.size()) - 1);
const auto& [chosen, isModule] = pool[static_cast<std::size_t>(dist(m_rng))];
const PoolEntry& chosen = pool[static_cast<std::size_t>(dist(m_rng))];
SchematicState& state = isModule
? m_moduleSchematicLevels.at(chosen)
: m_schematicLevels.at(chosen);
const bool wasNew = !state.unlocked;
state.unlocked = true;
state.level += 1;
if (chosen.type == DropType::Recipe)
{
m_unlockedRecipeSchematicIds.insert(chosen.id);
recomputeUnlocked();
}
else
{
SchematicState& state = (chosen.type == DropType::Module)
? m_moduleSchematicLevels.at(chosen.id)
: m_schematicLevels.at(chosen.id);
const bool wasNew = !state.unlocked;
state.unlocked = true;
state.level += 1;
SchematicDropEvent evt;
evt.schematicId = chosen;
evt.newLevel = state.level;
evt.wasNewUnlock = wasNew;
evt.isModuleSchematic = isModule;
m_schematicDropEvents.push_back(evt);
SchematicDropEvent evt;
evt.schematicId = chosen.id;
evt.newLevel = state.level;
evt.wasNewUnlock = wasNew;
evt.isModuleSchematic = (chosen.type == DropType::Module);
m_schematicDropEvents.push_back(evt);
recomputeUnlocked();
recomputeUnlocked();
}
}
// ---------------------------------------------------------------------------
@@ -577,6 +625,18 @@ void Simulation::recomputeUnlocked()
m_unlockedItemIds.insert(mat.item);
}
}
for (const RecipeDef& def : m_config.recipes.recipes)
{
if (def.building == BuildingType::Assembler
&& def.unlockAtStationLevel.has_value()
&& m_unlockedRecipeSchematicIds.count(def.id) > 0)
{
for (const RecipeOutput& out : def.outputs)
{
m_unlockedItemIds.insert(out.item);
}
}
}
bool changed = true;
while (changed)
@@ -590,6 +650,12 @@ void Simulation::recomputeUnlocked()
{
continue;
}
if (recipe.building == BuildingType::Assembler
&& recipe.unlockAtStationLevel.has_value()
&& m_unlockedRecipeSchematicIds.count(recipe.id) == 0)
{
continue;
}
bool producesUnlocked = false;
for (const RecipeOutput& out : recipe.outputs)
{

View File

@@ -136,6 +136,9 @@ private:
std::map<std::string, SchematicState> m_schematicLevels;
std::map<std::string, SchematicState> m_moduleSchematicLevels;
// 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;