#include "catch.hpp" #include "Building.h" #include "BuildingSystem.h" #include "BuildingType.h" #include "ConfigLoader.h" #include "GameConfig.h" #include "ItemType.h" #include "Rotation.h" #include "Ship.h" #include "ShipSystem.h" #include "Simulation.h" #include "Tick.h" static GameConfig loadConfig() { return ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR); } static const ShipDef* findAvailableBlueprint(const GameConfig& cfg) { for (const ShipDef& def : cfg.ships.ships) { if (def.availableFromStart && !def.blueprint.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 EntityId placeShipyard(Simulation& sim, const BuildingDef& yardDef) { return sim.buildings().placeImmediate( BuildingType::Shipyard, yardDef.surfaceMask, QPoint(0, 0), Rotation::East, 100.0f, 100.0f); } static void fillMaterials(Simulation& sim, EntityId yardId, const ShipDef& def) { sim.buildings().forEachBuilding([&](Building& b) { if (b.id != yardId) { return; } for (const RecipeIngredient& ing : def.blueprint.materials) { b.inputBuffer.counts[ItemType{ing.item}] = ing.amount; } }); } // --------------------------------------------------------------------------- // Shipyard production // --------------------------------------------------------------------------- TEST_CASE("Shipyard: spawns a player ship after production cycle completes", "[shipyard]") { const GameConfig cfg = loadConfig(); Simulation sim(cfg, 42); const ShipDef* def = findAvailableBlueprint(cfg); REQUIRE(def != nullptr); const BuildingDef* yardDef = findShipyardDef(cfg); REQUIRE(yardDef != nullptr); const int shipsBefore = static_cast(sim.ships().allShips().size()); const EntityId yardId = placeShipyard(sim, *yardDef); REQUIRE(yardId != kInvalidEntityId); sim.buildings().setRecipe(yardId, def->id); fillMaterials(sim, yardId, *def); // First tick: materials consumed, production cycle starts — no ship yet. sim.tick(); REQUIRE(static_cast(sim.ships().allShips().size()) == shipsBefore); // Tick until the cycle completes. const Tick cycleTicks = secondsToTicks(def->blueprint.productionTimeSeconds); for (Tick i = 1; i < cycleTicks; ++i) { sim.tick(); } REQUIRE(static_cast(sim.ships().allShips().size()) == shipsBefore); // Final tick: cycle completes, ship spawns. sim.tick(); REQUIRE(static_cast(sim.ships().allShips().size()) == shipsBefore + 1); bool foundPlayerShip = false; for (const Ship& ship : sim.ships().allShips()) { if (!ship.isEnemy && ship.blueprintId == def->id) { foundPlayerShip = true; break; } } REQUIRE(foundPlayerShip); } TEST_CASE("Shipyard: does not spawn without a blueprint set", "[shipyard]") { const GameConfig cfg = loadConfig(); Simulation sim(cfg, 42); const BuildingDef* yardDef = findShipyardDef(cfg); REQUIRE(yardDef != nullptr); const int shipsBefore = static_cast(sim.ships().allShips().size()); placeShipyard(sim, *yardDef); sim.tick(); REQUIRE(static_cast(sim.ships().allShips().size()) == shipsBefore); } TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]") { const GameConfig cfg = loadConfig(); Simulation sim(cfg, 42); const ShipDef* def = findAvailableBlueprint(cfg); REQUIRE(def != nullptr); const BuildingDef* yardDef = findShipyardDef(cfg); REQUIRE(yardDef != nullptr); const int shipsBefore = static_cast(sim.ships().allShips().size()); const EntityId 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->blueprint.productionTimeSeconds); for (Tick i = 0; i <= cycleTicks; ++i) { sim.tick(); } REQUIRE(static_cast(sim.ships().allShips().size()) == shipsBefore); } TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipyard]") { const GameConfig cfg = loadConfig(); Simulation sim(cfg, 42); const ShipDef* def = findAvailableBlueprint(cfg); REQUIRE(def != nullptr); const BuildingDef* yardDef = findShipyardDef(cfg); REQUIRE(yardDef != nullptr); const EntityId yardId = placeShipyard(sim, *yardDef); sim.buildings().setRecipe(yardId, def->id); const Tick cycleTicks = secondsToTicks(def->blueprint.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 = static_cast(sim.ships().allShips().size()); // 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 = static_cast(sim.ships().allShips().size()); // 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); }