implement beam rendering if shooter or target is already destroyed
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
#include "Ship.h"
|
||||
#include "ShipSystem.h"
|
||||
|
||||
static constexpr Tick kWeaponImpactDelayTicks = 5; // 0.15 s × 30 Hz, rounded to nearest
|
||||
|
||||
CombatSystem::CombatSystem(const GameConfig& config)
|
||||
: m_config(config)
|
||||
{
|
||||
@@ -71,15 +73,7 @@ void CombatSystem::resolveShipWeapon(Ship& ship, Tick currentTick,
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply damage to the correct pool.
|
||||
if (ships.findShip(targetId))
|
||||
{
|
||||
ships.damageShip(targetId, w.damage);
|
||||
}
|
||||
else
|
||||
{
|
||||
buildings.damageBuilding(targetId, w.damage);
|
||||
}
|
||||
m_pendingDamage.push_back({targetId, w.damage, currentTick + kWeaponImpactDelayTicks});
|
||||
|
||||
FireEvent evt;
|
||||
evt.shooter = ship.id;
|
||||
@@ -153,14 +147,7 @@ void CombatSystem::resolveStationWeapon(Building& station, Tick currentTick,
|
||||
return;
|
||||
}
|
||||
|
||||
if (ships.findShip(targetId))
|
||||
{
|
||||
ships.damageShip(targetId, w.damage);
|
||||
}
|
||||
else
|
||||
{
|
||||
buildings.damageBuilding(targetId, w.damage);
|
||||
}
|
||||
m_pendingDamage.push_back({targetId, w.damage, currentTick + kWeaponImpactDelayTicks});
|
||||
|
||||
FireEvent evt;
|
||||
evt.shooter = station.id;
|
||||
@@ -203,6 +190,32 @@ std::optional<EntityId> CombatSystem::acquireStationTarget(
|
||||
return best;
|
||||
}
|
||||
|
||||
void CombatSystem::applyPendingDamage(Tick currentTick,
|
||||
ShipSystem& ships,
|
||||
BuildingSystem& buildings)
|
||||
{
|
||||
auto it = m_pendingDamage.begin();
|
||||
while (it != m_pendingDamage.end())
|
||||
{
|
||||
if (it->appliesAt <= currentTick)
|
||||
{
|
||||
if (ships.findShip(it->target))
|
||||
{
|
||||
ships.damageShip(it->target, it->amount);
|
||||
}
|
||||
else if (buildings.findBuilding(it->target))
|
||||
{
|
||||
buildings.damageBuilding(it->target, it->amount);
|
||||
}
|
||||
it = m_pendingDamage.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<QVector2D> CombatSystem::targetPosition(
|
||||
EntityId id,
|
||||
const ShipSystem& ships,
|
||||
|
||||
@@ -23,15 +23,28 @@ public:
|
||||
explicit CombatSystem(const GameConfig& config);
|
||||
|
||||
// Advance weapon cooldowns, acquire targets for stations, fire when ready,
|
||||
// apply damage, and append FireEvents. Damage is applied immediately via
|
||||
// ShipSystem::damageShip and BuildingSystem::damageBuilding; step 9
|
||||
// removes entities whose HP dropped to zero or below.
|
||||
// queue deferred damage, and append FireEvents. Call applyPendingDamage()
|
||||
// after tick() (step 8b) and before death processing (step 9).
|
||||
void tick(Tick currentTick,
|
||||
ShipSystem& ships,
|
||||
BuildingSystem& buildings,
|
||||
std::vector<FireEvent>& outFireEvents);
|
||||
|
||||
// Apply any queued damage whose impact tick has arrived. Silently drops
|
||||
// damage if the target no longer exists.
|
||||
void applyPendingDamage(Tick currentTick,
|
||||
ShipSystem& ships,
|
||||
BuildingSystem& buildings);
|
||||
|
||||
private:
|
||||
struct PendingDamage
|
||||
{
|
||||
EntityId target;
|
||||
float amount;
|
||||
Tick appliesAt;
|
||||
};
|
||||
|
||||
std::vector<PendingDamage> m_pendingDamage;
|
||||
// Process one ship's weapon for this tick.
|
||||
void resolveShipWeapon(Ship& ship, Tick currentTick,
|
||||
ShipSystem& ships,
|
||||
|
||||
@@ -159,6 +159,9 @@ void Simulation::tick()
|
||||
m_combatSystem->tick(m_currentTick, *m_shipSystem,
|
||||
*m_buildingSystem, m_fireEvents);
|
||||
|
||||
// Step 8b: deferred damage whose impact tick has arrived
|
||||
m_combatSystem->applyPendingDamage(m_currentTick, *m_shipSystem, *m_buildingSystem);
|
||||
|
||||
// Step 9: deaths & loot
|
||||
if (!m_gameOver)
|
||||
{
|
||||
|
||||
@@ -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