switch to ECS architecture

This commit is contained in:
2026-05-22 20:31:39 +02:00
parent c18c4e4804
commit ca07cbaf0e
34 changed files with 1943 additions and 2074 deletions

View File

@@ -1,13 +1,13 @@
#include "catch.hpp"
#include <cmath>
#include <functional>
#include <string>
#include <vector>
#include <QVector2D>
#include "ConfigLoader.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "EntityId.h"
#include "Ship.h"
#include "ShipSystem.h"
@@ -25,70 +25,65 @@ static GameConfig loadConfig()
TEST_CASE("ShipSystem: interceptor spawn has weapon and threatResponse, no cargo or repair",
"[ship]")
{
EntityAdmin admin;
const GameConfig cfg = loadConfig();
EntityId nextId = 1;
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
ShipSystem ss(cfg, admin);
const EntityId id = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
const Ship* ship = ss.findShip(id);
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
REQUIRE(ship != nullptr);
REQUIRE(ship->weapon.has_value());
REQUIRE(ship->threatResponse.has_value());
REQUIRE_FALSE(ship->cargo.has_value());
REQUIRE_FALSE(ship->repairTool.has_value());
REQUIRE_FALSE(ship->repairBehavior.has_value());
REQUIRE_FALSE(ship->scrapCollector.has_value());
REQUIRE(admin.isValid(e));
REQUIRE(admin.hasAll<Weapon>(e));
REQUIRE(admin.hasAll<ThreatResponse>(e));
REQUIRE_FALSE(admin.hasAll<SalvageCargo>(e));
REQUIRE_FALSE(admin.hasAll<RepairTool>(e));
REQUIRE_FALSE(admin.hasAll<RepairBehavior>(e));
REQUIRE_FALSE(admin.hasAll<ScrapCollector>(e));
}
TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]")
{
EntityAdmin admin;
const GameConfig cfg = loadConfig();
EntityId nextId = 1;
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
ShipSystem ss(cfg, admin);
const EntityId id = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
const Ship* ship = ss.findShip(id);
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
REQUIRE(ship != nullptr);
// hp_formula = "40 + 5*x" at x=1 → 45
REQUIRE(ship->maxHp == Approx(45.0f));
REQUIRE(ship->hp == Approx(45.0f));
REQUIRE(admin.get<Health>(e).maxHp == Approx(45.0f));
REQUIRE(admin.get<Health>(e).hp == Approx(45.0f));
// damage_formula = "10 + 2*x" at x=1 → 12
REQUIRE(ship->weapon->damage == Approx(12.0f));
REQUIRE(admin.get<Weapon>(e).damage == Approx(12.0f));
// attack_range_formula = "150"
REQUIRE(ship->weapon->range == Approx(150.0f));
REQUIRE(admin.get<Weapon>(e).range == Approx(150.0f));
// sensor_range_formula = "200"
REQUIRE(ship->sensorRange == Approx(200.0f));
REQUIRE(admin.get<SensorRange>(e).value == Approx(200.0f));
// cooldownTicks starts at 0
REQUIRE(ship->weapon->cooldownTicks == Approx(0.0f));
REQUIRE(admin.get<Weapon>(e).cooldownTicks == Approx(0.0f));
}
TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]")
{
EntityAdmin admin;
const GameConfig cfg = loadConfig();
EntityId nextId = 1;
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
ShipSystem ss(cfg, admin);
const EntityId id = ss.spawn("interceptor", 5, QVector2D(0.0f, 0.0f));
const Ship* ship = ss.findShip(id);
const entt::entity e = ss.spawn("interceptor", 5, QVector2D(0.0f, 0.0f));
// hp_formula = "40 + 5*x" at x=5 → 65
REQUIRE(ship->maxHp == Approx(65.0f));
REQUIRE(admin.get<Health>(e).maxHp == Approx(65.0f));
}
TEST_CASE("ShipSystem: interceptor level 0 maxSpeedPerTick matches formula / kTickRateHz", "[ship]")
{
EntityAdmin admin;
const GameConfig cfg = loadConfig();
EntityId nextId = 1;
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
ShipSystem ss(cfg, admin);
const EntityId id = ss.spawn("interceptor", 0, QVector2D(0.0f, 0.0f));
const Ship* ship = ss.findShip(id);
const entt::entity e = ss.spawn("interceptor", 0, QVector2D(0.0f, 0.0f));
// speed_formula = "200 + 5*x" at x=0 → 200; maxSpeedPerTick = 200/30
const float expected = 200.0f / static_cast<float>(kTickRateHz);
REQUIRE(ship->maxSpeedPerTick == Approx(expected));
REQUIRE(admin.get<ShipDynamics>(e).maxSpeedPerTick == Approx(expected));
}
// ---------------------------------------------------------------------------
@@ -98,34 +93,31 @@ TEST_CASE("ShipSystem: interceptor level 0 maxSpeedPerTick matches formula / kTi
TEST_CASE("ShipSystem: salvage_ship spawn has cargo and scrapCollector, no weapon",
"[ship]")
{
EntityAdmin admin;
const GameConfig cfg = loadConfig();
EntityId nextId = 1;
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
ShipSystem ss(cfg, admin);
const EntityId id = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
const Ship* ship = ss.findShip(id);
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
REQUIRE(ship != nullptr);
REQUIRE(ship->cargo.has_value());
REQUIRE(ship->scrapCollector.has_value());
REQUIRE_FALSE(ship->weapon.has_value());
REQUIRE_FALSE(ship->repairTool.has_value());
REQUIRE(admin.hasAll<SalvageCargo>(e));
REQUIRE(admin.hasAll<ScrapCollector>(e));
REQUIRE_FALSE(admin.hasAll<Weapon>(e));
REQUIRE_FALSE(admin.hasAll<RepairTool>(e));
}
TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
{
EntityAdmin admin;
const GameConfig cfg = loadConfig();
EntityId nextId = 1;
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
ShipSystem ss(cfg, admin);
const EntityId id = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
const Ship* ship = ss.findShip(id);
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
// cargo_capacity = 10
REQUIRE(ship->cargo->capacity == 10);
REQUIRE(ship->cargo->current == 0);
REQUIRE(ship->scrapCollector->deliveryBay == kInvalidEntityId);
REQUIRE_FALSE(ship->scrapCollector->scrapTarget.has_value());
REQUIRE(admin.get<SalvageCargo>(e).capacity == 10);
REQUIRE(admin.get<SalvageCargo>(e).current == 0);
REQUIRE(admin.get<ScrapCollector>(e).deliveryBay == kInvalidEntityId);
REQUIRE_FALSE(admin.get<ScrapCollector>(e).scrapTarget.has_value());
}
// ---------------------------------------------------------------------------
@@ -135,61 +127,59 @@ TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
TEST_CASE("ShipSystem: repair_ship spawn has repairTool and repairBehavior, no weapon",
"[ship]")
{
EntityAdmin admin;
const GameConfig cfg = loadConfig();
EntityId nextId = 1;
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
ShipSystem ss(cfg, admin);
const EntityId id = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
const Ship* ship = ss.findShip(id);
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
REQUIRE(ship != nullptr);
REQUIRE(ship->repairTool.has_value());
REQUIRE(ship->repairBehavior.has_value());
REQUIRE_FALSE(ship->weapon.has_value());
REQUIRE_FALSE(ship->cargo.has_value());
REQUIRE(admin.hasAll<RepairTool>(e));
REQUIRE(admin.hasAll<RepairBehavior>(e));
REQUIRE_FALSE(admin.hasAll<Weapon>(e));
REQUIRE_FALSE(admin.hasAll<SalvageCargo>(e));
}
TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]")
{
EntityAdmin admin;
const GameConfig cfg = loadConfig();
EntityId nextId = 1;
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
ShipSystem ss(cfg, admin);
const EntityId id = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
const Ship* ship = ss.findShip(id);
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
// repair_rate_formula = "5 + x" at x=1 → 6
REQUIRE(ship->repairTool->ratePerTick == Approx(6.0f));
REQUIRE(admin.get<RepairTool>(e).ratePerTick == Approx(6.0f));
// repair_range_formula = "80"
REQUIRE(ship->repairTool->range == Approx(80.0f));
REQUIRE(admin.get<RepairTool>(e).range == Approx(80.0f));
}
// ---------------------------------------------------------------------------
// Entity ids and removal
// ---------------------------------------------------------------------------
TEST_CASE("ShipSystem: spawned ships receive strictly increasing entity ids", "[ship]")
TEST_CASE("ShipSystem: spawned ships are valid entities", "[ship]")
{
EntityAdmin admin;
const GameConfig cfg = loadConfig();
EntityId nextId = 1;
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
ShipSystem ss(cfg, admin);
const EntityId id1 = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
const EntityId id2 = ss.spawn("salvage_ship", 1, QVector2D(1.0f, 0.0f));
const entt::entity e1 = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
const entt::entity e2 = ss.spawn("salvage_ship", 1, QVector2D(1.0f, 0.0f));
REQUIRE(id2 > id1);
REQUIRE(admin.isValid(e1));
REQUIRE(admin.isValid(e2));
REQUIRE(e1 != e2);
}
TEST_CASE("ShipSystem: despawn removes the ship", "[ship]")
{
EntityAdmin admin;
const GameConfig cfg = loadConfig();
EntityId nextId = 1;
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
ShipSystem ss(cfg, admin);
const EntityId id = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
REQUIRE(ss.findShip(id) != nullptr);
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
REQUIRE(admin.isValid(e));
ss.despawn(id);
REQUIRE(ss.findShip(id) == nullptr);
ss.despawn(e);
REQUIRE_FALSE(admin.isValid(e));
}