#include "catch.hpp" #include #include #include #include #include #include "ConfigLoader.h" #include "EntityId.h" #include "Ship.h" #include "ShipSystem.h" #include "Tick.h" static GameConfig loadConfig() { return ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR); } // --------------------------------------------------------------------------- // Combat ship // --------------------------------------------------------------------------- TEST_CASE("ShipSystem: interceptor spawn has weapon and threatResponse, no cargo or repair", "[ship]") { const GameConfig cfg = loadConfig(); EntityId nextId = 1; ShipSystem ss(cfg, [&nextId]() { return nextId++; }); const EntityId id = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); const Ship* ship = ss.findShip(id); REQUIRE(ship != nullptr); REQUIRE(ship->weapon.has_value()); REQUIRE(ship->threatResponse.has_value()); REQUIRE_FALSE(ship->cargo.has_value()); REQUIRE_FALSE(ship->repairTool.has_value()); REQUIRE_FALSE(ship->repairBehavior.has_value()); REQUIRE_FALSE(ship->scrapCollector.has_value()); } TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]") { const GameConfig cfg = loadConfig(); EntityId nextId = 1; ShipSystem ss(cfg, [&nextId]() { return nextId++; }); const EntityId id = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); const Ship* ship = ss.findShip(id); REQUIRE(ship != nullptr); // hp_formula = "40 + 5*x" at x=1 → 45 REQUIRE(ship->maxHp == Approx(45.0f)); REQUIRE(ship->hp == Approx(45.0f)); // damage_formula = "10 + 2*x" at x=1 → 12 REQUIRE(ship->weapon->damage == Approx(12.0f)); // attack_range_formula = "150" REQUIRE(ship->weapon->range == Approx(150.0f)); // threatResponse.engagementRange mirrors weapon range REQUIRE(ship->threatResponse->engagementRange == Approx(150.0f)); // cooldownTicks starts at 0 REQUIRE(ship->weapon->cooldownTicks == Approx(0.0f)); } TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]") { const GameConfig cfg = loadConfig(); EntityId nextId = 1; ShipSystem ss(cfg, [&nextId]() { return nextId++; }); const EntityId id = ss.spawn("interceptor", 5, QVector2D(0.0f, 0.0f)); const Ship* ship = ss.findShip(id); // hp_formula = "40 + 5*x" at x=5 → 65 REQUIRE(ship->maxHp == Approx(65.0f)); } TEST_CASE("ShipSystem: interceptor level 0 speedPerTick matches formula / kTickRateHz", "[ship]") { const GameConfig cfg = loadConfig(); EntityId nextId = 1; ShipSystem ss(cfg, [&nextId]() { return nextId++; }); const EntityId id = ss.spawn("interceptor", 0, QVector2D(0.0f, 0.0f)); const Ship* ship = ss.findShip(id); // speed_formula = "200 + 5*x" at x=0 → 200; speedPerTick = 200/30 const float expected = 200.0f / static_cast(kTickRateHz); REQUIRE(ship->speedPerTick == Approx(expected)); } // --------------------------------------------------------------------------- // Salvage ship // --------------------------------------------------------------------------- TEST_CASE("ShipSystem: salvage_ship spawn has cargo and scrapCollector, no weapon", "[ship]") { const GameConfig cfg = loadConfig(); EntityId nextId = 1; ShipSystem ss(cfg, [&nextId]() { return nextId++; }); const EntityId id = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f)); const Ship* ship = ss.findShip(id); REQUIRE(ship != nullptr); REQUIRE(ship->cargo.has_value()); REQUIRE(ship->scrapCollector.has_value()); REQUIRE_FALSE(ship->weapon.has_value()); REQUIRE_FALSE(ship->repairTool.has_value()); } TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]") { const GameConfig cfg = loadConfig(); EntityId nextId = 1; ShipSystem ss(cfg, [&nextId]() { return nextId++; }); const EntityId id = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f)); const Ship* ship = ss.findShip(id); // cargo_capacity = 10 REQUIRE(ship->cargo->capacity == 10); REQUIRE(ship->cargo->current == 0); REQUIRE(ship->scrapCollector->deliveryBay == kInvalidEntityId); REQUIRE_FALSE(ship->scrapCollector->scrapTarget.has_value()); } // --------------------------------------------------------------------------- // Repair ship // --------------------------------------------------------------------------- TEST_CASE("ShipSystem: repair_ship spawn has repairTool and repairBehavior, no weapon", "[ship]") { const GameConfig cfg = loadConfig(); EntityId nextId = 1; ShipSystem ss(cfg, [&nextId]() { return nextId++; }); const EntityId id = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f)); const Ship* ship = ss.findShip(id); REQUIRE(ship != nullptr); REQUIRE(ship->repairTool.has_value()); REQUIRE(ship->repairBehavior.has_value()); REQUIRE_FALSE(ship->weapon.has_value()); REQUIRE_FALSE(ship->cargo.has_value()); } TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]") { const GameConfig cfg = loadConfig(); EntityId nextId = 1; ShipSystem ss(cfg, [&nextId]() { return nextId++; }); const EntityId id = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f)); const Ship* ship = ss.findShip(id); // repair_rate_formula = "5 + x" at x=1 → 6 REQUIRE(ship->repairTool->ratePerTick == Approx(6.0f)); // repair_range_formula = "80" REQUIRE(ship->repairTool->range == Approx(80.0f)); } // --------------------------------------------------------------------------- // Entity ids and removal // --------------------------------------------------------------------------- TEST_CASE("ShipSystem: spawned ships receive strictly increasing entity ids", "[ship]") { const GameConfig cfg = loadConfig(); EntityId nextId = 1; ShipSystem ss(cfg, [&nextId]() { return nextId++; }); const EntityId id1 = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); const EntityId id2 = ss.spawn("salvage_ship", 1, QVector2D(1.0f, 0.0f)); REQUIRE(id2 > id1); } TEST_CASE("ShipSystem: despawn removes the ship", "[ship]") { const GameConfig cfg = loadConfig(); EntityId nextId = 1; ShipSystem ss(cfg, [&nextId]() { return nextId++; }); const EntityId id = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); REQUIRE(ss.findShip(id) != nullptr); ss.despawn(id); REQUIRE(ss.findShip(id) == nullptr); }