#include "catch.hpp" #include "Building.h" #include "BuildingSystem.h" #include "BuildingType.h" #include "ConfigLoader.h" #include "EntityAdmin.h" #include "FactionComponent.h" #include "GameConfig.h" #include "ItemType.h" #include "Rotation.h" #include "ShipIdentityComponent.h" #include "ShipSystem.h" #include "Simulation.h" #include "Tick.h" static GameConfig loadConfig() { return ConfigLoader::loadFromDirectory(CONFIG_DIR); } static const ShipDef* findAvailableSchematic(const GameConfig& cfg) { for (const ShipDef& def : cfg.ships.ships) { if (def.unlockAtStationLevel == -1 && !def.schematic.materials.empty()) { return &def; } } return nullptr; } static const BuildingDef* findShipyardDef(const GameConfig& cfg) { for (const BuildingDef& def : cfg.buildings.buildings) { if (def.type == BuildingType::Shipyard) { return &def; } } return nullptr; } static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef) { return sim.buildings().placeImmediate( BuildingType::Shipyard, yardDef.surfaceMask, QPoint(0, 0), Rotation::East); } static int countShips(Simulation& sim) { int n = 0; sim.admin().forEach( [&n](entt::entity /*e*/, const ShipIdentityComponent& /*si*/) { ++n; }); return n; } static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def) { sim.buildings().forEachBuilding([&](Building& b) { if (b.id != yardId) { return; } for (const RecipeIngredient& ing : def.schematic.materials) { b.inputBuffer.counts[ItemType{ing.item}] = ing.amount; } }); } // --------------------------------------------------------------------------- // Shipyard production // --------------------------------------------------------------------------- TEST_CASE("Shipyard: spawns a player ship after production cycle completes", "[shipyard]") { Simulation sim(loadConfig(), 42); const ShipDef* def = findAvailableSchematic(sim.config()); REQUIRE(def != nullptr); const BuildingDef* yardDef = findShipyardDef(sim.config()); REQUIRE(yardDef != nullptr); const int shipsBefore = countShips(sim); const BuildingId yardId = placeShipyard(sim, *yardDef); REQUIRE(yardId != kInvalidBuildingId); sim.buildings().setRecipe(yardId, def->id); fillMaterials(sim, yardId, *def); // First tick: materials consumed, production cycle starts — no ship yet. sim.tick(); REQUIRE(countShips(sim) == shipsBefore); // Tick until the cycle completes. const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds); for (Tick i = 1; i < cycleTicks; ++i) { sim.tick(); } REQUIRE(countShips(sim) == shipsBefore); // Final tick: cycle completes, ship spawns. sim.tick(); REQUIRE(countShips(sim) == shipsBefore + 1); bool foundPlayerShip = false; sim.admin().forEach( [&](entt::entity /*e*/, const ShipIdentityComponent& si, const FactionComponent& f) { if (!f.isEnemy && si.schematicId == def->id) { foundPlayerShip = true; } }); REQUIRE(foundPlayerShip); } TEST_CASE("Shipyard: does not spawn without a schematic set", "[shipyard]") { Simulation sim(loadConfig(), 42); const BuildingDef* yardDef = findShipyardDef(sim.config()); REQUIRE(yardDef != nullptr); const int shipsBefore = countShips(sim); placeShipyard(sim, *yardDef); sim.tick(); REQUIRE(countShips(sim) == shipsBefore); } TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]") { Simulation sim(loadConfig(), 42); const ShipDef* def = findAvailableSchematic(sim.config()); REQUIRE(def != nullptr); const BuildingDef* yardDef = findShipyardDef(sim.config()); REQUIRE(yardDef != nullptr); const int shipsBefore = countShips(sim); const BuildingId yardId = placeShipyard(sim, *yardDef); sim.buildings().setRecipe(yardId, def->id); // Materials remain at zero (default after setRecipe); no cycle starts. const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds); for (Tick i = 0; i <= cycleTicks; ++i) { sim.tick(); } REQUIRE(countShips(sim) == shipsBefore); } TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipyard]") { Simulation sim(loadConfig(), 42); const ShipDef* def = findAvailableSchematic(sim.config()); REQUIRE(def != nullptr); const BuildingDef* yardDef = findShipyardDef(sim.config()); REQUIRE(yardDef != nullptr); const BuildingId yardId = placeShipyard(sim, *yardDef); sim.buildings().setRecipe(yardId, def->id); const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds); // First cycle: capture count immediately after the spawn tick. fillMaterials(sim, yardId, *def); for (Tick i = 0; i <= cycleTicks; ++i) { sim.tick(); } const int after1 = countShips(sim); // Second cycle: capture count immediately after the next spawn tick. fillMaterials(sim, yardId, *def); for (Tick i = 0; i <= cycleTicks; ++i) { sim.tick(); } const int after2 = countShips(sim); // After each cycle one ship was added; ships from prior cycles may have died // from enemy fire, so we only assert the most-recent spawn is still present. REQUIRE(after2 >= 1); // Verify the shipyard production field cleared (i.e. the cycle completed // and is not still running). bool productionCleared = false; for (const Building& b : sim.buildings().allBuildings()) { if (b.id == yardId) { productionCleared = !b.production.has_value(); break; } } REQUIRE(productionCleared); }