implement beam rendering if shooter or target is already destroyed
This commit is contained in:
@@ -87,6 +87,7 @@ TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[comb
|
||||
CombatSystem combat(cfg);
|
||||
std::vector<FireEvent> events;
|
||||
combat.tick(0, ships, buildings, events);
|
||||
combat.applyPendingDamage(5, ships, buildings);
|
||||
|
||||
float hpAfter = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
@@ -324,6 +325,227 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
|
||||
REQUIRE(playerFiredAtStation);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deferred damage timing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
|
||||
EntityId nextShipId = 1;
|
||||
EntityId nextBldId = 100;
|
||||
ShipSystem ships(cfg, [&nextShipId]() { return nextShipId++; });
|
||||
BuildingSystem buildings(cfg, belts,
|
||||
[&nextBldId]() { return nextBldId++; },
|
||||
[](int){},
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const EntityId playerId = ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
|
||||
ships.forEach([&](Ship& s)
|
||||
{
|
||||
if (s.id == enemyId && s.weapon)
|
||||
{
|
||||
s.weapon->currentTarget = playerId;
|
||||
s.weapon->cooldownTicks = 0.0f;
|
||||
if (s.threatResponse)
|
||||
{
|
||||
s.threatResponse->currentTarget = playerId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
float hpBefore = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
{
|
||||
if (s.id == playerId) { hpBefore = s.hp; }
|
||||
}
|
||||
|
||||
CombatSystem combat(cfg);
|
||||
std::vector<FireEvent> events;
|
||||
combat.tick(0, ships, buildings, events);
|
||||
|
||||
// Ticks 1-4: damage must not have arrived yet.
|
||||
for (Tick t = 1; t < 5; ++t)
|
||||
{
|
||||
combat.applyPendingDamage(t, ships, buildings);
|
||||
float hp = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
{
|
||||
if (s.id == playerId) { hp = s.hp; }
|
||||
}
|
||||
REQUIRE(hp == Approx(hpBefore));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: damage applied exactly at impact tick", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
|
||||
EntityId nextShipId = 1;
|
||||
EntityId nextBldId = 100;
|
||||
ShipSystem ships(cfg, [&nextShipId]() { return nextShipId++; });
|
||||
BuildingSystem buildings(cfg, belts,
|
||||
[&nextBldId]() { return nextBldId++; },
|
||||
[](int){},
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const EntityId playerId = ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
|
||||
ships.forEach([&](Ship& s)
|
||||
{
|
||||
if (s.id == enemyId && s.weapon)
|
||||
{
|
||||
s.weapon->currentTarget = playerId;
|
||||
s.weapon->cooldownTicks = 0.0f;
|
||||
if (s.threatResponse)
|
||||
{
|
||||
s.threatResponse->currentTarget = playerId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
float hpBefore = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
{
|
||||
if (s.id == playerId) { hpBefore = s.hp; }
|
||||
}
|
||||
|
||||
CombatSystem combat(cfg);
|
||||
std::vector<FireEvent> events;
|
||||
combat.tick(0, ships, buildings, events);
|
||||
combat.applyPendingDamage(5, ships, buildings);
|
||||
|
||||
float hpAfter = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
{
|
||||
if (s.id == playerId) { hpAfter = s.hp; }
|
||||
}
|
||||
|
||||
REQUIRE(hpAfter < hpBefore);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
|
||||
EntityId nextShipId = 1;
|
||||
EntityId nextBldId = 100;
|
||||
ShipSystem ships(cfg, [&nextShipId]() { return nextShipId++; });
|
||||
BuildingSystem buildings(cfg, belts,
|
||||
[&nextBldId]() { return nextBldId++; },
|
||||
[](int){},
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const EntityId playerId = ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
|
||||
ships.forEach([&](Ship& s)
|
||||
{
|
||||
if (s.id == enemyId && s.weapon)
|
||||
{
|
||||
s.weapon->currentTarget = playerId;
|
||||
s.weapon->cooldownTicks = 0.0f;
|
||||
if (s.threatResponse)
|
||||
{
|
||||
s.threatResponse->currentTarget = playerId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CombatSystem combat(cfg);
|
||||
std::vector<FireEvent> events;
|
||||
combat.tick(0, ships, buildings, events);
|
||||
|
||||
// Target is removed before impact.
|
||||
ships.despawn(playerId);
|
||||
|
||||
// Should not crash; damage is silently dropped.
|
||||
combat.applyPendingDamage(5, ships, buildings);
|
||||
|
||||
REQUIRE(ships.findShip(playerId) == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
|
||||
EntityId nextShipId = 1;
|
||||
EntityId nextBldId = 100;
|
||||
ShipSystem ships(cfg, [&nextShipId]() { return nextShipId++; });
|
||||
BuildingSystem buildings(cfg, belts,
|
||||
[&nextBldId]() { return nextBldId++; },
|
||||
[](int){},
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const EntityId playerId = ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
|
||||
ships.forEach([&](Ship& s)
|
||||
{
|
||||
if (s.id == enemyId && s.weapon)
|
||||
{
|
||||
s.weapon->currentTarget = playerId;
|
||||
s.weapon->cooldownTicks = 0.0f;
|
||||
if (s.threatResponse)
|
||||
{
|
||||
s.threatResponse->currentTarget = playerId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
float hpBefore = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
{
|
||||
if (s.id == playerId) { hpBefore = s.hp; }
|
||||
}
|
||||
|
||||
CombatSystem combat(cfg);
|
||||
std::vector<FireEvent> events;
|
||||
combat.tick(0, ships, buildings, events);
|
||||
|
||||
// Shooter is removed before impact.
|
||||
ships.despawn(enemyId);
|
||||
|
||||
// Damage must still land on the target.
|
||||
combat.applyPendingDamage(5, ships, buildings);
|
||||
|
||||
float hpAfter = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
{
|
||||
if (s.id == playerId) { hpAfter = s.hp; }
|
||||
}
|
||||
|
||||
REQUIRE(hpAfter < hpBefore);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deaths & loot (tick step 9)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user