// Unlock groups as the unit of loot (REQ-LOCK-EXPLICIT, REQ-LOCK-BUILDING, // REQ-DEF-SCHEMATIC-DROP): buildings can be locked, and one drop can grant a // building and a module together. #include #include #include #include #include "catch.hpp" #include "BuildingType.h" #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 { void killEnemyStations(Simulation& sim) { sim.getAdmin().forEach( [](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health) { if (faction.isEnemy) { health.hp = 0.0f; } }); sim.tick(); } // A config whose only unlock group grants the salvager module and the salvage // bay together, gated to the given station level. GameConfig configWithSalvageGroup(int stationLevel) { GameConfig cfg = loadTestConfig(); cfg.unlocks.groups.clear(); cfg.unlocks.groups.push_back( UnlockGroupDef{"salvage_operations", stationLevel, {}, {}, {"salvager"}, {"salvage_bay"}, {}}); return cfg; } } // namespace TEST_CASE("UnlockGroup: a building granted by a group is locked at game start", "[unlock_group]") { const Simulation sim(configWithSalvageGroup(0)); CHECK_FALSE(sim.isBuildingUnlocked(BuildingType::SalvageBay)); CHECK_FALSE(sim.isModuleSchematicUnlocked("salvager")); // A building in no unlock group is available from the start. CHECK(sim.isBuildingUnlocked(BuildingType::Miner)); } TEST_CASE("UnlockGroup: the drop option lists every granted item", "[unlock_group]") { Simulation sim(configWithSalvageGroup(0)); killEnemyStations(sim); // level 0 REQUIRE(sim.hasSchematicChoicesPending()); const std::vector& options = sim.getPendingSchematicChoices(); const auto it = std::find_if(options.begin(), options.end(), [](const SchematicChoiceOption& o) { return o.unlockGroupId == "salvage_operations"; }); REQUIRE(it != options.end()); const bool grantsModule = std::any_of(it->grantedItems.begin(), it->grantedItems.end(), [](const GrantedSchematic& g) { return g.type == SchematicType::Module && g.id == "salvager"; }); const bool grantsBuilding = std::any_of(it->grantedItems.begin(), it->grantedItems.end(), [](const GrantedSchematic& g) { return g.type == SchematicType::Building && g.id == "salvage_bay"; }); CHECK(grantsModule); CHECK(grantsBuilding); } TEST_CASE("UnlockGroup: awarding a group unlocks the building and module at once", "[unlock_group]") { Simulation sim(configWithSalvageGroup(0)); killEnemyStations(sim); // level 0 REQUIRE(sim.hasSchematicChoicesPending()); // The only eligible group is salvage_operations, so option 0 grants it. REQUIRE(sim.getPendingSchematicChoices()[0].unlockGroupId == "salvage_operations"); SimulationTestAccess::applySchematicChoice(sim, 0); CHECK(sim.isBuildingUnlocked(BuildingType::SalvageBay)); CHECK(sim.isModuleSchematicUnlocked("salvager")); } TEST_CASE("UnlockGroup: a group above the destroyed station level is not offered", "[unlock_group]") { Simulation sim(configWithSalvageGroup(5)); // eligible only at level >= 5 killEnemyStations(sim); // level 0 push // Pool is empty at level 0, so no choice dialog opens at all. CHECK_FALSE(sim.hasSchematicChoicesPending()); CHECK_FALSE(sim.isBuildingUnlocked(BuildingType::SalvageBay)); } TEST_CASE("UnlockGroup: reset re-locks a previously awarded building", "[unlock_group]") { Simulation sim(configWithSalvageGroup(0)); killEnemyStations(sim); REQUIRE(sim.getPendingSchematicChoices()[0].unlockGroupId == "salvage_operations"); SimulationTestAccess::applySchematicChoice(sim, 0); REQUIRE(sim.isBuildingUnlocked(BuildingType::SalvageBay)); sim.reset(); CHECK_FALSE(sim.isBuildingUnlocked(BuildingType::SalvageBay)); CHECK_FALSE(sim.isModuleSchematicUnlocked("salvager")); }