175 lines
5.6 KiB
C++
175 lines
5.6 KiB
C++
// Drop-pool behaviour for unlock_requires schematic prerequisites
|
|
// (REQ-LOCK-PREREQ). A schematic enters the drop pool only when every id in its
|
|
// unlock_requires is already explicitly unlocked, on top of the station-level
|
|
// (and, for recipes, output-implicitly-unlocked) checks.
|
|
//
|
|
// Configs are built in memory (load the test config, then mutate) so each case
|
|
// is isolated and does not perturb the shared test config used by other suites.
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "catch.hpp"
|
|
|
|
#include "ConfigLoader.h"
|
|
#include "FactionComponent.h"
|
|
#include "GameConfig.h"
|
|
#include "HealthComponent.h"
|
|
#include "ModulesConfig.h"
|
|
#include "RecipesConfig.h"
|
|
#include "SchematicChoiceOption.h"
|
|
#include "ShipsConfig.h"
|
|
#include "Simulation.h"
|
|
#include "SimulationTestAccess.h"
|
|
#include "StationBodyComponent.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
GameConfig loadConfig()
|
|
{
|
|
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
|
}
|
|
|
|
ShipDef& findShip(GameConfig& cfg, const std::string& id)
|
|
{
|
|
for (ShipDef& def : cfg.ships.ships)
|
|
{
|
|
if (def.id == id) { return def; }
|
|
}
|
|
FAIL("ship not found: " + id);
|
|
return cfg.ships.ships.front();
|
|
}
|
|
|
|
ModuleDef& findModule(GameConfig& cfg, const std::string& id)
|
|
{
|
|
for (ModuleDef& def : cfg.modules.modules)
|
|
{
|
|
if (def.id == id) { return def; }
|
|
}
|
|
FAIL("module not found: " + id);
|
|
return cfg.modules.modules.front();
|
|
}
|
|
|
|
RecipeDef& findRecipe(GameConfig& cfg, const std::string& id)
|
|
{
|
|
for (RecipeDef& def : cfg.recipes.recipes)
|
|
{
|
|
if (def.id == id) { return def; }
|
|
}
|
|
FAIL("recipe not found: " + id);
|
|
return cfg.recipes.recipes.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.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
|
[](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);
|
|
}
|
|
}
|
|
|
|
bool optionOffered(const Simulation& sim, const std::string& id)
|
|
{
|
|
const std::vector<SchematicChoiceOption>& options = sim.getPendingSchematicChoices();
|
|
return std::any_of(options.begin(), options.end(),
|
|
[&](const SchematicChoiceOption& o) { return o.schematicId == id; });
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("UnlockPrereq: a recipe gated behind a locked ship is withheld from the pool",
|
|
"[unlock_prereq]")
|
|
{
|
|
// quick_circuit (assembler recipe, level 0, output circuit_board is implicitly
|
|
// unlocked) would normally be eligible at the first station destruction. Gate
|
|
// it behind repair_ship (ship, level 0, locked at game start).
|
|
GameConfig cfg = loadConfig();
|
|
findRecipe(cfg, "quick_circuit").unlockRequires = {"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 recipe must not be offered, while the
|
|
// prerequisite schematic itself (the only other eligible pick) is.
|
|
CHECK_FALSE(optionOffered(sim, "quick_circuit"));
|
|
CHECK(optionOffered(sim, "repair_ship"));
|
|
}
|
|
|
|
TEST_CASE("UnlockPrereq: the gated recipe becomes eligible once its prerequisite is unlocked",
|
|
"[unlock_prereq]")
|
|
{
|
|
GameConfig cfg = loadConfig();
|
|
findRecipe(cfg, "quick_circuit").unlockRequires = {"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 quick_circuit 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 gated behind another module is withheld until it unlocks",
|
|
"[unlock_prereq]")
|
|
{
|
|
// Mirror the app-config demo (laser_cannon_m -> laser_cannon_l) using two
|
|
// test-config modules made lockable at the same station level.
|
|
GameConfig cfg = loadConfig();
|
|
findModule(cfg, "laser_cannon").unlockAtStationLevel = 0;
|
|
ModuleDef& armor = findModule(cfg, "armor_plate");
|
|
armor.unlockAtStationLevel = 0;
|
|
armor.unlockRequires = {"laser_cannon"};
|
|
|
|
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);
|
|
}
|