switch to ECS architecture
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
#include "BuildingType.h"
|
||||
#include "CombatSystem.h"
|
||||
#include "ConfigLoader.h"
|
||||
#include "EcsComponents.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FireEvent.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "Ship.h"
|
||||
@@ -20,7 +22,6 @@ static GameConfig loadConfig()
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
// Find the first ShipDef with a combat component.
|
||||
static const ShipDef* findCombatShip(const GameConfig& cfg)
|
||||
{
|
||||
for (const ShipDef& def : cfg.ships.ships)
|
||||
@@ -33,159 +34,104 @@ static const ShipDef* findCombatShip(const GameConfig& cfg)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Helper fixture for unit tests that need ships + combat but not a full Simulation.
|
||||
struct CombatFixture
|
||||
{
|
||||
GameConfig cfg;
|
||||
std::mt19937 rng;
|
||||
EntityAdmin admin;
|
||||
EntityId nextBldId;
|
||||
BeltSystem belts;
|
||||
ShipSystem ships;
|
||||
BuildingSystem buildings;
|
||||
CombatSystem combat;
|
||||
|
||||
explicit CombatFixture()
|
||||
: cfg(loadConfig())
|
||||
, rng(42)
|
||||
, nextBldId(1)
|
||||
, belts(cfg.world.beltSpeedTilesPerSecond)
|
||||
, ships(cfg, admin)
|
||||
, buildings(cfg, belts,
|
||||
[this]() { return nextBldId++; },
|
||||
[](int){},
|
||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||
rng)
|
||||
, combat(cfg)
|
||||
{
|
||||
}
|
||||
|
||||
void wireEnemyTarget(entt::entity enemy, entt::entity playerTarget)
|
||||
{
|
||||
if (admin.hasAll<Weapon>(enemy))
|
||||
{
|
||||
admin.get<Weapon>(enemy).currentTarget = playerTarget;
|
||||
admin.get<Weapon>(enemy).cooldownTicks = 0.0f;
|
||||
}
|
||||
if (admin.hasAll<ThreatResponse>(enemy))
|
||||
{
|
||||
admin.get<ThreatResponse>(enemy).currentTarget = playerTarget;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ship weapon firing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
CombatFixture f;
|
||||
const ShipDef* combatDef = findCombatShip(f.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, const std::optional<ShipLayoutConfig>&) {},
|
||||
rng);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
// Spawn an enemy combat ship close to the player side.
|
||||
const EntityId enemyId = ships.spawn(combatDef->id, 1,
|
||||
QVector2D(5.0f, 5.0f), /*isEnemy=*/true);
|
||||
const float hpBefore = f.admin.get<Health>(player).hp;
|
||||
|
||||
// Spawn a player combat ship in front of the enemy.
|
||||
const EntityId playerId = ships.spawn(combatDef->id, 1,
|
||||
QVector2D(4.0f, 5.0f), /*isEnemy=*/false);
|
||||
|
||||
// Wire the enemy's weapon target manually.
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Record player HP before combat.
|
||||
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);
|
||||
f.combat.tick(0, f.admin, f.buildings, events);
|
||||
f.combat.applyPendingDamage(5, f.admin);
|
||||
|
||||
float hpAfter = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
{
|
||||
if (s.id == playerId) { hpAfter = s.hp; }
|
||||
}
|
||||
|
||||
REQUIRE(hpAfter < hpBefore);
|
||||
REQUIRE(f.admin.get<Health>(player).hp < hpBefore);
|
||||
REQUIRE(events.size() >= 1);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
CombatFixture f;
|
||||
const ShipDef* combatDef = findCombatShip(f.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, const std::optional<ShipLayoutConfig>&) {},
|
||||
rng);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
|
||||
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);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
f.admin.get<Weapon>(enemy).cooldownTicks = 3.0f; // override to 3
|
||||
|
||||
// Set cooldown to 3 so it won't fire on tick 0 or 1 or 2.
|
||||
ships.forEach([&](Ship& s)
|
||||
{
|
||||
if (s.id == enemyId && s.weapon)
|
||||
{
|
||||
s.weapon->currentTarget = playerId;
|
||||
s.weapon->cooldownTicks = 3.0f;
|
||||
if (s.threatResponse)
|
||||
{
|
||||
s.threatResponse->currentTarget = playerId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CombatSystem combat(cfg);
|
||||
std::vector<FireEvent> events;
|
||||
|
||||
// Ticks 0 and 1: cooldown still > 0 after decrement → no fire.
|
||||
combat.tick(0, ships, buildings, events);
|
||||
combat.tick(1, ships, buildings, events);
|
||||
|
||||
f.combat.tick(0, f.admin, f.buildings, events);
|
||||
f.combat.tick(1, f.admin, f.buildings, events);
|
||||
REQUIRE(events.empty());
|
||||
|
||||
// Tick 2: cooldown reaches 0 → fires.
|
||||
combat.tick(2, ships, buildings, events);
|
||||
f.combat.tick(2, f.admin, f.buildings, events);
|
||||
REQUIRE(events.size() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
CombatFixture f;
|
||||
const ShipDef* combatDef = findCombatShip(f.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, const std::optional<ShipLayoutConfig>&) {},
|
||||
rng);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(0.0f, 0.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(500.0f, 0.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(0.0f, 0.0f), true);
|
||||
const EntityId playerId = ships.spawn(combatDef->id, 1, QVector2D(500.0f, 0.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);
|
||||
|
||||
f.combat.tick(0, f.admin, f.buildings, events);
|
||||
REQUIRE(events.empty());
|
||||
}
|
||||
|
||||
@@ -197,49 +143,37 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
|
||||
// Find the player defence station.
|
||||
EntityId stationId = kInvalidEntityId;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::PlayerDefenceStation)
|
||||
{
|
||||
stationId = b.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(stationId != kInvalidEntityId);
|
||||
|
||||
// Place an enemy ship close to the player station.
|
||||
// Find the player station entity via ECS.
|
||||
entt::entity stationEntity = entt::null;
|
||||
QVector2D stationCenter;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.id == stationId)
|
||||
sim.admin().forEach<StationBody, Faction>(
|
||||
[&](entt::entity e, const StationBody& sb, const Faction& f)
|
||||
{
|
||||
stationCenter = QVector2D(
|
||||
b.anchor.x() + b.footprint.width() / 2.0f,
|
||||
b.anchor.y() + b.footprint.height() / 2.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!f.isEnemy && stationEntity == entt::null)
|
||||
{
|
||||
stationEntity = e;
|
||||
stationCenter = QVector2D(
|
||||
sb.anchor.x() + sb.footprint.width() / 2.0f,
|
||||
sb.anchor.y() + sb.footprint.height() / 2.0f);
|
||||
}
|
||||
});
|
||||
REQUIRE(sim.admin().isValid(stationEntity));
|
||||
|
||||
// Find a combat ship schematic for the enemy.
|
||||
const ShipDef* combatDef = findCombatShip(sim.config());
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const EntityId enemyId = sim.ships().spawn(
|
||||
const entt::entity enemyShip = sim.ships().spawn(
|
||||
combatDef->id, 1,
|
||||
QVector2D(stationCenter.x() + 1.0f, stationCenter.y()),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
// Tick to let station auto-acquire and fire.
|
||||
sim.tick();
|
||||
|
||||
// Check that a fire event was emitted with stationId as shooter.
|
||||
const std::vector<FireEvent> events = sim.drainFireEvents();
|
||||
bool stationFired = false;
|
||||
for (const FireEvent& e : events)
|
||||
for (const FireEvent& evt : events)
|
||||
{
|
||||
if (e.shooter == stationId) { stationFired = true; }
|
||||
if (evt.shooter == stationEntity) { stationFired = true; }
|
||||
}
|
||||
REQUIRE(stationFired);
|
||||
}
|
||||
@@ -248,26 +182,24 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
|
||||
// Find the enemy defence station.
|
||||
EntityId stationId = kInvalidEntityId;
|
||||
entt::entity stationEntity = entt::null;
|
||||
QVector2D stationCenter;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation)
|
||||
sim.admin().forEach<StationBody, Faction>(
|
||||
[&](entt::entity e, const StationBody& sb, const Faction& f)
|
||||
{
|
||||
stationId = b.id;
|
||||
stationCenter = QVector2D(
|
||||
b.anchor.x() + b.footprint.width() / 2.0f,
|
||||
b.anchor.y() + b.footprint.height() / 2.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(stationId != kInvalidEntityId);
|
||||
if (f.isEnemy && stationEntity == entt::null)
|
||||
{
|
||||
stationEntity = e;
|
||||
stationCenter = QVector2D(
|
||||
sb.anchor.x() + sb.footprint.width() / 2.0f,
|
||||
sb.anchor.y() + sb.footprint.height() / 2.0f);
|
||||
}
|
||||
});
|
||||
REQUIRE(sim.admin().isValid(stationEntity));
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(sim.config());
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
// Spawn a player ship right next to the enemy station.
|
||||
sim.ships().spawn(
|
||||
combatDef->id, 1,
|
||||
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
|
||||
@@ -277,9 +209,9 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
|
||||
|
||||
const std::vector<FireEvent> events = sim.drainFireEvents();
|
||||
bool stationFired = false;
|
||||
for (const FireEvent& e : events)
|
||||
for (const FireEvent& evt : events)
|
||||
{
|
||||
if (e.shooter == stationId) { stationFired = true; }
|
||||
if (evt.shooter == stationEntity) { stationFired = true; }
|
||||
}
|
||||
REQUIRE(stationFired);
|
||||
}
|
||||
@@ -288,25 +220,25 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
|
||||
EntityId stationId = kInvalidEntityId;
|
||||
entt::entity stationEntity = entt::null;
|
||||
QVector2D stationCenter;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation)
|
||||
sim.admin().forEach<StationBody, Faction>(
|
||||
[&](entt::entity e, const StationBody& sb, const Faction& f)
|
||||
{
|
||||
stationId = b.id;
|
||||
stationCenter = QVector2D(
|
||||
b.anchor.x() + b.footprint.width() / 2.0f,
|
||||
b.anchor.y() + b.footprint.height() / 2.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(stationId != kInvalidEntityId);
|
||||
if (f.isEnemy && stationEntity == entt::null)
|
||||
{
|
||||
stationEntity = e;
|
||||
stationCenter = QVector2D(
|
||||
sb.anchor.x() + sb.footprint.width() / 2.0f,
|
||||
sb.anchor.y() + sb.footprint.height() / 2.0f);
|
||||
}
|
||||
});
|
||||
REQUIRE(sim.admin().isValid(stationEntity));
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(sim.config());
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const EntityId playerId = sim.ships().spawn(
|
||||
const entt::entity playerShip = sim.ships().spawn(
|
||||
combatDef->id, 1,
|
||||
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
|
||||
/*isEnemy=*/false);
|
||||
@@ -315,9 +247,9 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
|
||||
|
||||
const std::vector<FireEvent> events = sim.drainFireEvents();
|
||||
bool playerFiredAtStation = false;
|
||||
for (const FireEvent& e : events)
|
||||
for (const FireEvent& evt : events)
|
||||
{
|
||||
if (e.shooter == playerId && e.target == stationId)
|
||||
if (evt.shooter == playerShip && evt.target == stationEntity)
|
||||
{
|
||||
playerFiredAtStation = true;
|
||||
}
|
||||
@@ -331,219 +263,86 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
|
||||
|
||||
TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
CombatFixture f;
|
||||
const ShipDef* combatDef = findCombatShip(f.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, const std::optional<ShipLayoutConfig>&) {},
|
||||
rng);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
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);
|
||||
const float hpBefore = f.admin.get<Health>(player).hp;
|
||||
|
||||
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);
|
||||
f.combat.tick(0, f.admin, f.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));
|
||||
f.combat.applyPendingDamage(t, f.admin);
|
||||
REQUIRE(f.admin.get<Health>(player).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);
|
||||
CombatFixture f;
|
||||
const ShipDef* combatDef = findCombatShip(f.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, const std::optional<ShipLayoutConfig>&) {},
|
||||
rng);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
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);
|
||||
const float hpBefore = f.admin.get<Health>(player).hp;
|
||||
|
||||
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);
|
||||
f.combat.tick(0, f.admin, f.buildings, events);
|
||||
f.combat.applyPendingDamage(5, f.admin);
|
||||
|
||||
float hpAfter = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
{
|
||||
if (s.id == playerId) { hpAfter = s.hp; }
|
||||
}
|
||||
|
||||
REQUIRE(hpAfter < hpBefore);
|
||||
REQUIRE(f.admin.get<Health>(player).hp < 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);
|
||||
CombatFixture f;
|
||||
const ShipDef* combatDef = findCombatShip(f.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, const std::optional<ShipLayoutConfig>&) {},
|
||||
rng);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
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);
|
||||
f.combat.tick(0, f.admin, f.buildings, events);
|
||||
|
||||
// Target is removed before impact.
|
||||
ships.despawn(playerId);
|
||||
f.ships.despawn(player);
|
||||
|
||||
// Should not crash; damage is silently dropped.
|
||||
combat.applyPendingDamage(5, ships, buildings);
|
||||
// Should not crash.
|
||||
f.combat.applyPendingDamage(5, f.admin);
|
||||
|
||||
REQUIRE(ships.findShip(playerId) == nullptr);
|
||||
REQUIRE_FALSE(f.admin.isValid(player));
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
CombatFixture f;
|
||||
const ShipDef* combatDef = findCombatShip(f.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, const std::optional<ShipLayoutConfig>&) {},
|
||||
rng);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
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);
|
||||
const float hpBefore = f.admin.get<Health>(player).hp;
|
||||
|
||||
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);
|
||||
f.combat.tick(0, f.admin, f.buildings, events);
|
||||
|
||||
// Shooter is removed before impact.
|
||||
ships.despawn(enemyId);
|
||||
f.ships.despawn(enemy);
|
||||
|
||||
// Damage must still land on the target.
|
||||
combat.applyPendingDamage(5, ships, buildings);
|
||||
f.combat.applyPendingDamage(5, f.admin);
|
||||
|
||||
float hpAfter = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
{
|
||||
if (s.id == playerId) { hpAfter = s.hp; }
|
||||
}
|
||||
|
||||
REQUIRE(hpAfter < hpBefore);
|
||||
REQUIRE(f.admin.get<Health>(player).hp < hpBefore);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -557,22 +356,20 @@ TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
||||
const ShipDef* combatDef = findCombatShip(sim.config());
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const EntityId shipId = sim.ships().spawn(combatDef->id, 1,
|
||||
const entt::entity ship = sim.ships().spawn(combatDef->id, 1,
|
||||
QVector2D(10.0f, 10.0f));
|
||||
|
||||
// Set hp to lethal.
|
||||
sim.ships().damageShip(shipId, 9999.0f);
|
||||
sim.admin().get<Health>(ship).hp = -1.0f;
|
||||
|
||||
sim.tick();
|
||||
|
||||
REQUIRE(sim.ships().findShip(shipId) == nullptr);
|
||||
REQUIRE_FALSE(sim.admin().isValid(ship));
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
|
||||
// Find a ship def that drops scrap.
|
||||
const ShipDef* droppingDef = nullptr;
|
||||
for (const ShipDef& def : sim.config().ships.ships)
|
||||
{
|
||||
@@ -584,27 +381,25 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
||||
}
|
||||
REQUIRE(droppingDef != nullptr);
|
||||
|
||||
const EntityId shipId = sim.ships().spawn(droppingDef->id, 1,
|
||||
const entt::entity ship = sim.ships().spawn(droppingDef->id, 1,
|
||||
QVector2D(10.0f, 10.0f));
|
||||
sim.ships().damageShip(shipId, 9999.0f);
|
||||
sim.admin().get<Health>(ship).hp = -1.0f;
|
||||
|
||||
sim.tick();
|
||||
|
||||
// At least one scrap entity should now exist.
|
||||
REQUIRE(!sim.scraps().allScraps().empty());
|
||||
REQUIRE(!sim.scraps().allScrapInfo().empty());
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
|
||||
sim.buildings().forEachBuilding([](Building& b)
|
||||
{
|
||||
if (b.type == BuildingType::Hq)
|
||||
// Damage the HQ proxy entity (has HqProxy + Health).
|
||||
sim.admin().forEach<HqProxy, Health>(
|
||||
[](entt::entity /*e*/, const HqProxy& /*hq*/, Health& h)
|
||||
{
|
||||
b.hp = -1.0f;
|
||||
}
|
||||
});
|
||||
h.hp = -1.0f;
|
||||
});
|
||||
|
||||
sim.tick();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user