change repair_tool application and add beams for salvager and repair_tool

This commit is contained in:
2026-06-18 22:14:09 +02:00
parent 7924e037aa
commit 9573b9789a
37 changed files with 498 additions and 199 deletions

View File

@@ -70,6 +70,7 @@ struct Fixture
DynamicBodySystem dynamicBody;
ScrapSystem scraps;
Tick tick;
std::vector<BeamFiredEvent> beamEvents;
explicit Fixture()
: cfg(loadConfig())
@@ -102,8 +103,9 @@ struct Fixture
// World mutation: collection/delivery and healing.
void runModules()
{
salvager.tick(scraps, buildings);
repair.tick();
beamEvents.clear();
salvager.tick(tick, scraps, buildings, beamEvents);
repair.tick(tick, beamEvents);
}
// Run one full behavior+movement tick (steps 7 and 10).
@@ -115,6 +117,35 @@ struct Fixture
dynamicBody.tick(admin);
++tick;
}
// One repair-system tick at the current sim time (advances the tick counter).
// Starts cycles and applies any due (mid-beam-delayed) heals.
void repairTick()
{
beamEvents.clear();
repair.tick(tick, beamEvents);
++tick;
}
// Drive the repair system long enough for a started cycle's delayed heal to land.
void runRepairHeal()
{
for (int i = 0; i <= kBeamImpactDelayTicks; ++i) { repairTick(); }
}
// One salvage-system tick at the current sim time (advances the tick counter).
void salvageTick()
{
beamEvents.clear();
salvager.tick(tick, scraps, buildings, beamEvents);
++tick;
}
// Drive the salvage system long enough for a started cycle's delayed collection.
void runSalvageCollect()
{
for (int i = 0; i <= kBeamImpactDelayTicks; ++i) { salvageTick(); }
}
};
static ShipLayoutConfig makeSingleModuleLayout(const std::string& moduleId)
@@ -602,7 +633,7 @@ TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
f.admin.get<HealthComponent>(friendly).hp = initialHp;
f.decide();
f.runModules();
f.runRepairHeal();
REQUIRE(health(f.admin, friendly).hp > initialHp);
}
@@ -616,11 +647,8 @@ TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp - 0.001f;
for (int i = 0; i < 5; ++i)
{
f.decide();
f.runModules();
}
f.decide();
f.runRepairHeal();
const HealthComponent& h = health(f.admin, friendly);
REQUIRE(h.hp <= h.maxHp);
@@ -644,7 +672,7 @@ TEST_CASE("RepairSystem: tool heals the in-range damaged target chosen by the ex
f.admin.get<HealthComponent>(friendly).hp = initHp;
f.decide();
f.runModules();
f.runRepairHeal();
const entt::entity rc = firstRepairChild(f.admin, repairShip);
REQUIRE(f.admin.isValid(rc));
@@ -674,7 +702,7 @@ TEST_CASE("RepairSystem: tool falls back to in-range target when its target is o
const entt::entity rc = firstRepairChild(f.admin, repairShip);
f.admin.get<RepairToolComponent>(rc).currentTarget = outOfRange;
f.repair.tick();
f.runRepairHeal();
REQUIRE(f.admin.get<RepairToolComponent>(rc).currentTarget.has_value());
REQUIRE(*f.admin.get<RepairToolComponent>(rc).currentTarget == fallback);
@@ -699,7 +727,7 @@ TEST_CASE("RepairSystem: tool falls back when its target is fully healed",
const entt::entity rc = firstRepairChild(f.admin, repairShip);
f.admin.get<RepairToolComponent>(rc).currentTarget = healed;
f.repair.tick();
f.runRepairHeal();
REQUIRE(*f.admin.get<RepairToolComponent>(rc).currentTarget == fallback);
REQUIRE(health(f.admin, fallback).hp > fallbackInitHp);
@@ -722,7 +750,7 @@ TEST_CASE("RepairSystem: tool falls back when its target is destroyed",
f.admin.get<RepairToolComponent>(rc).currentTarget = gone;
f.ships.despawn(gone);
f.repair.tick();
f.runRepairHeal();
REQUIRE(*f.admin.get<RepairToolComponent>(rc).currentTarget == fallback);
REQUIRE(health(f.admin, fallback).hp > fallbackInitHp);
@@ -744,7 +772,7 @@ TEST_CASE("RepairSystem: tool target is cleared when no repairable target is in
const entt::entity rc = firstRepairChild(f.admin, repairShip);
f.admin.get<RepairToolComponent>(rc).currentTarget = outOfRange;
f.repair.tick();
f.runRepairHeal();
REQUIRE_FALSE(f.admin.get<RepairToolComponent>(rc).currentTarget.has_value());
REQUIRE(health(f.admin, outOfRange).hp == Approx(initHp));
@@ -763,11 +791,12 @@ TEST_CASE("RepairSystem: two repair modules both heal the chosen target additive
f.admin.get<HealthComponent>(targetA).hp = initHp;
f.decide();
f.runModules();
f.runRepairHeal();
// Both modules should have healed targetA — total increase is 2 * ratePerTick.
const float ratePerTick = (5.0f + 1.0f) / static_cast<float>(kTickRateHz);
REQUIRE(health(f.admin, targetA).hp == Approx(initHp + 2.0f * ratePerTick));
// Both modules run one cycle and heal targetA — total increase is 2 * repairAmountHp.
// repair_amount_hp_formula = "5 + x" at x=1 → 6 HP per cycle.
const float repairAmountHp = 5.0f + 1.0f;
REQUIRE(health(f.admin, targetA).hp == Approx(initHp + 2.0f * repairAmountHp));
const std::vector<entt::entity> children = allRepairChildren(f.admin, repairShip);
REQUIRE(children.size() == 2);
@@ -797,10 +826,10 @@ TEST_CASE("RepairSystem: two modules both fall back and heal the same target",
f.admin.get<RepairToolComponent>(child).currentTarget = healed;
}
f.repair.tick();
f.runRepairHeal();
const float ratePerTick = (5.0f + 1.0f) / static_cast<float>(kTickRateHz);
REQUIRE(health(f.admin, targetB).hp == Approx(initHp + 2.0f * ratePerTick));
const float repairAmountHp = 5.0f + 1.0f;
REQUIRE(health(f.admin, targetB).hp == Approx(initHp + 2.0f * repairAmountHp));
const std::vector<entt::entity> children = allRepairChildren(f.admin, repairShip);
REQUIRE(children.size() == 2);
@@ -819,14 +848,16 @@ TEST_CASE("RepairSystem: does not crash when a tool's owner is not a repair ship
const entt::entity ownerShip = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
const entt::entity moduleEntity = f.admin.createModuleEntity();
RepairToolComponent rt;
rt.ratePerTick = 1.0f;
rt.range_tiles = 10.0f;
rt.currentTarget = std::nullopt;
rt.repairAmountHp = 1.0f;
rt.repairIntervalTicks = kTickRateHz;
rt.cooldownTicksRemaining = 0;
rt.range_tiles = 10.0f;
rt.currentTarget = std::nullopt;
f.admin.addComponent<RepairToolComponent>(moduleEntity, rt);
f.admin.addComponent<ModuleOwnerComponent>(moduleEntity, ModuleOwnerComponent{ownerShip});
// Must not crash; no damaged friendly in range, so no target is set.
f.repair.tick();
f.runRepairHeal();
REQUIRE_FALSE(f.admin.get<RepairToolComponent>(moduleEntity).currentTarget.has_value());
}
@@ -866,7 +897,7 @@ TEST_CASE("BehaviorSystem: salvage ship collects scrap on arrival", "[behavior]"
false, salvageLayout);
const entt::entity scrapEntity = f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
f.salvager.tick(f.scraps, f.buildings);
f.runSalvageCollect();
const entt::entity sc = firstSalvageChild(f.admin, ship);
REQUIRE(f.admin.isValid(sc));
@@ -935,7 +966,7 @@ TEST_CASE("SalvagerSystem: module does not collect scrap beyond its collection r
false, salvageLayout);
f.scraps.spawn(QVector2D(55.0f, 0.0f), 1, 100000);
f.salvager.tick(f.scraps, f.buildings);
f.runSalvageCollect();
REQUIRE(f.admin.get<SalvageCargoComponent>(firstSalvageChild(f.admin, ship)).current == 0);
}
@@ -950,7 +981,7 @@ TEST_CASE("SalvagerSystem: module collects scrap within its collection range",
false, salvageLayout);
f.scraps.spawn(QVector2D(45.0f, 0.0f), 1, 100000);
f.salvager.tick(f.scraps, f.buildings);
f.runSalvageCollect();
REQUIRE(f.admin.get<SalvageCargoComponent>(firstSalvageChild(f.admin, ship)).current == 1);
}
@@ -967,11 +998,13 @@ TEST_CASE("SalvagerSystem: collection sets cooldown on module", "[behavior]")
false, salvageLayout);
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
f.salvager.tick(f.scraps, f.buildings);
// Starting a collection cycle sets the cooldown immediately; the scrap is not
// collected until mid-beam (REQ-SHP-SALVAGE), so cargo is still empty now.
f.salvageTick();
const SalvageCargoComponent& cargo =
f.admin.get<SalvageCargoComponent>(firstSalvageChild(f.admin, ship));
REQUIRE(cargo.current == 1);
REQUIRE(cargo.current == 0);
REQUIRE(cargo.cooldownTicksRemaining == cargo.collectionIntervalTicks);
}
@@ -985,7 +1018,7 @@ TEST_CASE("SalvagerSystem: module on cooldown does not collect scrap", "[behavio
f.admin.get<SalvageCargoComponent>(firstSalvageChild(f.admin, ship)).cooldownTicksRemaining = 10;
f.salvager.tick(f.scraps, f.buildings);
f.runSalvageCollect();
REQUIRE(f.admin.get<SalvageCargoComponent>(firstSalvageChild(f.admin, ship)).current == 0);
}
@@ -999,15 +1032,16 @@ TEST_CASE("SalvagerSystem: module collects again after cooldown expires", "[beha
const entt::entity sc = firstSalvageChild(f.admin, ship);
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
f.salvager.tick(f.scraps, f.buildings);
f.runSalvageCollect();
REQUIRE(f.admin.get<SalvageCargoComponent>(sc).current == 1);
// Shorten cooldown to 1 tick and place a second scrap.
f.admin.get<SalvageCargoComponent>(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.salvager.tick(f.scraps, f.buildings);
// Once the cooldown expires the module starts another cycle and collects the
// second scrap after the mid-beam delay.
f.runSalvageCollect();
REQUIRE(f.admin.get<SalvageCargoComponent>(sc).current == 2);
}
@@ -1026,7 +1060,7 @@ TEST_CASE("SalvagerSystem: two salvage modules collect independently in same tic
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
f.salvager.tick(f.scraps, f.buildings);
f.runSalvageCollect();
REQUIRE(totalSalvageCurrent(f.admin, ship) == 2);
}
@@ -1055,7 +1089,7 @@ TEST_CASE("SalvagerSystem: second salvage module does not collect when first is
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
f.salvager.tick(f.scraps, f.buildings);
f.runSalvageCollect();
// Only one module was ready, so only one scrap is collected.
REQUIRE(totalSalvageCurrent(f.admin, ship) == 1);