From 64f7c9dcc1fefbe00b41318f9336c8ebae83a848 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Tue, 2 Jun 2026 21:39:05 +0200 Subject: [PATCH] add tests for salvager range and cooldown --- src/test/BehaviorSystemTest.cpp | 175 ++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/src/test/BehaviorSystemTest.cpp b/src/test/BehaviorSystemTest.cpp index 1c08bdd..b2e97a9 100644 --- a/src/test/BehaviorSystemTest.cpp +++ b/src/test/BehaviorSystemTest.cpp @@ -103,6 +103,22 @@ static ShipLayoutConfig makeSingleModuleLayout(const std::string& moduleId) return layout; } +static ShipLayoutConfig makeTwoModuleLayout(const std::string& moduleId) +{ + ShipLayoutConfig layout; + PlacedModule pm1; + pm1.moduleId = moduleId; + pm1.position = QPoint(0, 0); + pm1.rotation = Rotation::East; + layout.placedModules.push_back(pm1); + PlacedModule pm2; + pm2.moduleId = moduleId; + pm2.position = QPoint(0, 1); + pm2.rotation = Rotation::East; + layout.placedModules.push_back(pm2); + return layout; +} + static entt::entity firstSalvageChild(EntityAdmin& admin, entt::entity ship) { entt::entity result = entt::null; @@ -452,6 +468,165 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b REQUIRE(i.target.x() < pos(f.admin, ship).value.x()); } +// --------------------------------------------------------------------------- +// Collection range (per-module) +// --------------------------------------------------------------------------- + +static int totalSalvageCurrent(EntityAdmin& admin, entt::entity ship) +{ + int total = 0; + admin.forEach( + [&](entt::entity /*ce*/, const SalvageCargoComponent& c, const ModuleOwnerComponent& o) + { + if (o.owner == ship) { total += c.current; } + }); + return total; +} + +TEST_CASE("BehaviorSystem: salvage module does not collect scrap beyond its collection range", + "[behavior]") +{ + // collection_range_formula = "50"; scrap at distance 55 must not be collected. + Fixture f; + const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module"); + const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), + false, salvageLayout); + f.scraps.spawn(QVector2D(55.0f, 0.0f), 1, 100000); + + f.ships.clearMovementIntents(); + f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings); + + REQUIRE(f.admin.get(firstSalvageChild(f.admin, ship)).current == 0); +} + +TEST_CASE("BehaviorSystem: salvage module collects scrap within its collection range", + "[behavior]") +{ + // collection_range_formula = "50"; scrap at distance 45 must be collected. + Fixture f; + const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module"); + const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), + false, salvageLayout); + f.scraps.spawn(QVector2D(45.0f, 0.0f), 1, 100000); + + f.ships.clearMovementIntents(); + f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings); + + REQUIRE(f.admin.get(firstSalvageChild(f.admin, ship)).current == 1); +} + +// --------------------------------------------------------------------------- +// Collection rate (per-module cooldown) +// --------------------------------------------------------------------------- + +TEST_CASE("BehaviorSystem: salvage collection sets cooldown on module", "[behavior]") +{ + Fixture f; + const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module"); + const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), + false, salvageLayout); + f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000); + + f.ships.clearMovementIntents(); + f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings); + + const SalvageCargoComponent& cargo = + f.admin.get(firstSalvageChild(f.admin, ship)); + REQUIRE(cargo.current == 1); + REQUIRE(cargo.cooldownTicksRemaining == cargo.collectionIntervalTicks); +} + +TEST_CASE("BehaviorSystem: salvage module on cooldown does not collect scrap", "[behavior]") +{ + Fixture f; + const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module"); + const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), + false, salvageLayout); + f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000); + + f.admin.get(firstSalvageChild(f.admin, ship)).cooldownTicksRemaining = 10; + + f.ships.clearMovementIntents(); + f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings); + + REQUIRE(f.admin.get(firstSalvageChild(f.admin, ship)).current == 0); +} + +TEST_CASE("BehaviorSystem: salvage module collects again after cooldown expires", "[behavior]") +{ + Fixture f; + const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module"); + const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), + false, salvageLayout); + const entt::entity sc = firstSalvageChild(f.admin, ship); + + f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000); + f.ships.clearMovementIntents(); + f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings); + REQUIRE(f.admin.get(sc).current == 1); + + // Shorten cooldown to 1 tick and place a second scrap. + f.admin.get(sc).cooldownTicksRemaining = 1; + f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000); + + // Next tick: cooldown decrements to 0, module collects the second scrap. + f.ships.clearMovementIntents(); + f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings); + + REQUIRE(f.admin.get(sc).current == 2); +} + +// --------------------------------------------------------------------------- +// Multiple salvage modules +// --------------------------------------------------------------------------- + +TEST_CASE("BehaviorSystem: two salvage modules collect independently in same tick", "[behavior]") +{ + Fixture f; + const ShipLayoutConfig salvageLayout = makeTwoModuleLayout("salvage_bay_module"); + const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), + false, salvageLayout); + + f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000); + f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000); + + f.ships.clearMovementIntents(); + f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings); + + REQUIRE(totalSalvageCurrent(f.admin, ship) == 2); +} + +TEST_CASE("BehaviorSystem: second salvage module does not collect when first module is on cooldown", + "[behavior]") +{ + // One module on cooldown, one ready: only the ready module collects. + Fixture f; + const ShipLayoutConfig salvageLayout = makeTwoModuleLayout("salvage_bay_module"); + const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), + false, salvageLayout); + + // Put the first salvage child on cooldown. + entt::entity blocked = entt::null; + f.admin.forEach( + [&](entt::entity ce, SalvageCargoComponent& c, const ModuleOwnerComponent& o) + { + if (o.owner == ship && blocked == entt::null) + { + c.cooldownTicksRemaining = 99; + blocked = ce; + } + }); + + f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000); + f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000); + + f.ships.clearMovementIntents(); + f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings); + + // Only one module was ready, so only one scrap is collected. + REQUIRE(totalSalvageCurrent(f.admin, ship) == 1); +} + // --------------------------------------------------------------------------- // Sensor range — spawn // ---------------------------------------------------------------------------