Implement config-driven unlock groups so that multiple things can be unlocked at once (including buildings)

This commit is contained in:
2026-07-22 21:34:59 +02:00
parent a8a6a04f1e
commit a9082c57f3
36 changed files with 1005 additions and 543 deletions

View File

@@ -57,36 +57,36 @@ static bool awaitRecipeUnlock(Simulation& sim, const std::string& recipeId,
}
// ---------------------------------------------------------------------------
// ConfigLoader: parsing unlock_at_station_level on assembler recipes
// ConfigLoader: unlocked_at_start and unlock-group gating on assembler recipes
// ---------------------------------------------------------------------------
TEST_CASE("RecipeSchematic: unlock_at_station_level = -1 parsed correctly", "[recipe_schematic]")
TEST_CASE("RecipeSchematic: unlocked_at_start = true parsed correctly", "[recipe_schematic]")
{
const GameConfig cfg = loadConfig();
const auto it = std::find_if(cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(),
[](const RecipeDef& r) { return r.id == "premium_circuit"; });
REQUIRE(it != cfg.recipes.recipes.end());
REQUIRE(it->unlockAtStationLevel.has_value());
CHECK(it->unlockAtStationLevel.value() == -1);
CHECK(it->unlockedAtStart);
}
TEST_CASE("RecipeSchematic: unlock_at_station_level = 0 parsed correctly", "[recipe_schematic]")
TEST_CASE("RecipeSchematic: a gated recipe is granted by an unlock group", "[recipe_schematic]")
{
const GameConfig cfg = loadConfig();
const auto it = std::find_if(cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(),
[](const RecipeDef& r) { return r.id == "quick_circuit"; });
REQUIRE(it != cfg.recipes.recipes.end());
REQUIRE(it->unlockAtStationLevel.has_value());
CHECK(it->unlockAtStationLevel.value() == 0);
const bool granted = std::any_of(cfg.unlocks.groups.begin(), cfg.unlocks.groups.end(),
[](const UnlockGroupDef& g)
{
return std::find(g.recipes.begin(), g.recipes.end(), "quick_circuit") != g.recipes.end();
});
CHECK(granted);
}
TEST_CASE("RecipeSchematic: assembler recipe without the key has nullopt", "[recipe_schematic]")
TEST_CASE("RecipeSchematic: an untagged assembler recipe has unlocked_at_start = false", "[recipe_schematic]")
{
const GameConfig cfg = loadConfig();
const auto it = std::find_if(cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(),
[](const RecipeDef& r) { return r.id == "circuit_board"; });
REQUIRE(it != cfg.recipes.recipes.end());
CHECK_FALSE(it->unlockAtStationLevel.has_value());
CHECK_FALSE(it->unlockedAtStart);
}
// ---------------------------------------------------------------------------
@@ -180,11 +180,11 @@ TEST_CASE("RecipeSchematic: eligible recipe schematic is eventually awarded on s
REQUIRE(awaitRecipeUnlock(sim, "quick_circuit"));
}
TEST_CASE("RecipeSchematic: recipe whose output is not implicitly unlocked is never awarded",
TEST_CASE("RecipeSchematic: an implicitly-gated recipe with no unlock group is never awarded",
"[recipe_schematic]")
{
// exotic_alloy produces exotic_alloy (not implicitly unlocked), so the
// output-unlocked guard must keep it out of the drop pool at every level.
// exotic_alloy is in no unlock group and its output/inputs are unreachable
// via the item graph, so it can never be dropped or implicitly unlocked.
Simulation sim(loadConfig());
for (int i = 0; i < 50; ++i)
{
@@ -241,11 +241,11 @@ TEST_CASE("RecipeSchematic: recipe schematic can appear in pending choices",
{
for (const SchematicChoiceOption& opt : sim.getPendingSchematicChoices())
{
if (opt.type == SchematicType::Recipe)
for (const GrantedSchematic& grant : opt.grantedItems)
{
foundRecipeChoice = true;
break;
if (grant.type == SchematicType::Recipe) { foundRecipeChoice = true; break; }
}
if (foundRecipeChoice) { break; }
}
SimulationTestAccess::applySchematicChoice(sim, 0);
}
@@ -377,11 +377,14 @@ TEST_CASE("SchematicDrop: an owned ship schematic is never offered again",
const std::vector<SchematicChoiceOption>& choices =
sim.getPendingSchematicChoices();
// Locate the repair_ship option, if present in this drop.
// Locate the option granting repair_ship, if present in this drop.
int repairShipIndex = -1;
for (int j = 0; j < static_cast<int>(choices.size()); ++j)
{
if (choices[j].schematicId == "repair_ship") { repairShipIndex = j; }
for (const GrantedSchematic& grant : choices[j].grantedItems)
{
if (grant.id == "repair_ship") { repairShipIndex = j; break; }
}
}
// Once owned, it must never appear in the pool again.