Implement config-driven unlock groups
Lift unlocking into first-class unlock groups defined in unlocks.toml, so one defence-station drop can grant a bundle of ships/modules/buildings/ recipes at once (e.g. the salvager module + salvage bay), and buildings can now be locked at game start and unlocked via a drop. Config: - New UnlocksConfig + ConfigLoader::loadUnlocks + validateUnlocks (unknown/ duplicate/non-assembler grants, unknown requires, empty group all fail load). - Drop unlock_at_station_level/unlock_requires from ship/module/recipe defs; add unlocked_at_start bool to recipes for graph-unreachable base recipes. Simulation: - Track awarded groups + per-building unlock state; an item starts unlocked iff no group grants it. Rework generateSchematicChoices/applySchematicChoice to operate on groups (grant all members at once). Extend state checksum. - isBuildingUnlocked query; tryPlaceBuilding rejects locked types. UI: - Build-menu buttons for locked buildings start hidden, revealed via a new UnlockedBuildingsChangedEvent (emitted from GameWorldView's poll loop). - Schematic choice dialog lists a group's granted items. - Blueprint placement excludes locked building types (REQ-LOCK-UI-BLUEPRINT). Data: unlocks.toml (app + test) reproduces the prior per-item gates as singleton groups and adds salvage_operations (salvager + salvage_bay, lvl 1) and reprocessing (reprocessing_plant, lvl 2). Tests: rewrote unlock/config/recipe-schematic/artifact/wave/shipyard tests for the group model; added UnlockGroupTest. All 443 cases pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
@@ -305,26 +305,35 @@ TEST_CASE("WaveSystem: push schematic choices have valid ids", "[wave]")
|
||||
|
||||
for (const SchematicChoiceOption& opt : choices)
|
||||
{
|
||||
bool validId = false;
|
||||
for (const ShipDef& def : sim.getConfig().ships.ships)
|
||||
if (opt.isArtifact) { continue; }
|
||||
bool validGroup = false;
|
||||
for (const UnlockGroupDef& group : sim.getConfig().unlocks.groups)
|
||||
{
|
||||
if (def.id == opt.schematicId) { validId = true; break; }
|
||||
if (group.id == opt.unlockGroupId) { validGroup = true; break; }
|
||||
}
|
||||
if (!validId)
|
||||
REQUIRE(validGroup);
|
||||
// Every granted item must resolve to a defined ship/module/building/recipe.
|
||||
for (const GrantedSchematic& grant : opt.grantedItems)
|
||||
{
|
||||
bool validId = false;
|
||||
for (const ShipDef& def : sim.getConfig().ships.ships)
|
||||
{
|
||||
if (def.id == grant.id) { validId = true; break; }
|
||||
}
|
||||
for (const ModuleDef& def : sim.getConfig().modules.modules)
|
||||
{
|
||||
if (def.id == opt.schematicId) { validId = true; break; }
|
||||
if (def.id == grant.id) { validId = true; break; }
|
||||
}
|
||||
for (const BuildingDef& def : sim.getConfig().buildings.buildings)
|
||||
{
|
||||
if (def.id == grant.id) { validId = true; break; }
|
||||
}
|
||||
}
|
||||
if (!validId)
|
||||
{
|
||||
for (const RecipeDef& def : sim.getConfig().recipes.recipes)
|
||||
{
|
||||
if (def.id == opt.schematicId) { validId = true; break; }
|
||||
if (def.id == grant.id) { validId = true; break; }
|
||||
}
|
||||
REQUIRE(validId);
|
||||
}
|
||||
REQUIRE(validId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +352,7 @@ TEST_CASE("WaveSystem: schematic choices have no duplicates", "[wave]")
|
||||
std::set<std::string> ids;
|
||||
for (const SchematicChoiceOption& opt : choices)
|
||||
{
|
||||
ids.insert(opt.schematicId);
|
||||
ids.insert(opt.isArtifact ? std::string("<artifact>") : opt.unlockGroupId);
|
||||
}
|
||||
REQUIRE(ids.size() == choices.size());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user