// Drop-pool behaviour for unlock-group prerequisites (REQ-LOCK-PREREQ). An // unlock group enters the drop pool only when every id in its `requires` names a // group that has already been awarded, on top of the station-level check. // // Configs are built in memory (load the test config, then mutate the unlock // groups) so each case is isolated and does not perturb the shared test config // used by other suites. #include #include #include #include #include "catch.hpp" #include "ConfigLoader.h" #include "FactionComponent.h" #include "GameConfig.h" #include "HealthComponent.h" #include "SchematicChoiceOption.h" #include "Simulation.h" #include "SimulationTestAccess.h" #include "StationBodyComponent.h" #include "UnlocksConfig.h" #include "TestConfig.h" namespace { UnlockGroupDef& findGroup(GameConfig& cfg, const std::string& id) { for (UnlockGroupDef& group : cfg.unlocks.groups) { if (group.id == id) { return group; } } FAIL("unlock group not found: " + id); return cfg.unlocks.groups.front(); } // Zeros the HP of both enemy defence stations and advances one tick so that // tickDeathsAndLoot fires, triggering the push and schematic choices. void killEnemyStations(Simulation& sim) { sim.getAdmin().forEach( [](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health) { if (faction.isEnemy) { health.hp = 0.0f; } }); sim.tick(); } void killEnemyStationsAndApply(Simulation& sim) { killEnemyStations(sim); if (sim.hasSchematicChoicesPending()) { SimulationTestAccess::applySchematicChoice(sim, 0); } } // True if any pending option grants the given schematic id. bool optionOffered(const Simulation& sim, const std::string& id) { const std::vector& options = sim.getPendingSchematicChoices(); return std::any_of(options.begin(), options.end(), [&](const SchematicChoiceOption& option) { return std::any_of(option.grantedItems.begin(), option.grantedItems.end(), [&](const GrantedSchematic& grant) { return grant.id == id; }); }); } } // namespace TEST_CASE("UnlockPrereq: a group gated behind a locked ship is withheld from the pool", "[unlock_prereq]") { // quick_circuit (recipe group, level 0) would normally be eligible at the // first station destruction. Gate it behind the repair_ship group (level 0, // locked at game start). GameConfig cfg = loadTestConfig(); findGroup(cfg, "quick_circuit").requiredGroupIds = {"repair_ship"}; Simulation sim(std::move(cfg), 123); REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship")); killEnemyStations(sim); // destroyed set level 0 REQUIRE(sim.hasSchematicChoicesPending()); // Prerequisite unmet -> the gated group must not be offered, while the // prerequisite group itself (the only other eligible pick) is. CHECK_FALSE(optionOffered(sim, "quick_circuit")); CHECK(optionOffered(sim, "repair_ship")); } TEST_CASE("UnlockPrereq: the gated group becomes eligible once its prerequisite is awarded", "[unlock_prereq]") { GameConfig cfg = loadTestConfig(); findGroup(cfg, "quick_circuit").requiredGroupIds = {"repair_ship"}; Simulation sim(std::move(cfg), 123); // Applying choice 0 each drop unlocks repair_ship first (the only eligible // pick at level 0), which then opens the quick_circuit group for a later drop. bool unlocked = false; for (int i = 0; i < 150 && !unlocked; ++i) { killEnemyStationsAndApply(sim); unlocked = sim.isRecipeUnlocked("quick_circuit"); } CHECK(sim.isSchematicUnlocked("repair_ship")); // prerequisite got unlocked along the way CHECK(unlocked); } TEST_CASE("UnlockPrereq: a module group gated behind another module is withheld until it unlocks", "[unlock_prereq]") { // Make laser_cannon and armor_plate (both start-unlocked in the test config) // lockable via new unlock groups, with armor_plate gated behind laser_cannon. GameConfig cfg = loadTestConfig(); cfg.unlocks.groups.push_back( UnlockGroupDef{"laser_cannon", 0, {}, {}, {"laser_cannon"}, {}, {}}); cfg.unlocks.groups.push_back( UnlockGroupDef{"armor_plate", 0, {"laser_cannon"}, {}, {"armor_plate"}, {}, {}}); Simulation sim(std::move(cfg), 7); REQUIRE_FALSE(sim.isModuleSchematicUnlocked("laser_cannon")); REQUIRE_FALSE(sim.isModuleSchematicUnlocked("armor_plate")); killEnemyStations(sim); // level 0 REQUIRE(sim.hasSchematicChoicesPending()); // armor_plate is gated behind the still-locked laser_cannon module. CHECK_FALSE(optionOffered(sim, "armor_plate")); // Drive the drops until the gated module unlocks; it can only do so after its // prerequisite has itself been unlocked. bool armorUnlocked = false; for (int i = 0; i < 300 && !armorUnlocked; ++i) { killEnemyStationsAndApply(sim); armorUnlocked = sim.isModuleSchematicUnlocked("armor_plate"); if (armorUnlocked) { CHECK(sim.isModuleSchematicUnlocked("laser_cannon")); } } CHECK(armorUnlocked); }