implement scrap and ship skeleton
This commit is contained in:
@@ -55,7 +55,7 @@ set_property(TARGET ${TARGET_LIB_NAME} PROPERTY INCLUDE_DIRECTORIES
|
||||
"${TARGET_LIB_INCLUDE_DIRS}"
|
||||
"${LIB_INCLUDE_PATH}"
|
||||
)
|
||||
target_link_libraries(${TARGET_LIB_NAME} Qt5::Core)
|
||||
target_link_libraries(${TARGET_LIB_NAME} Qt5::Core Qt5::Gui)
|
||||
target_compile_definitions(${TARGET_LIB_NAME} PRIVATE TOML_FLOAT_CHARCONV=0)
|
||||
|
||||
set(CMAKE_AUTOMOC OFF)
|
||||
|
||||
@@ -5,6 +5,10 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Scrap.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Ship.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -14,6 +18,8 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
|
||||
14
src/lib/sim/Scrap.h
Normal file
14
src/lib/sim/Scrap.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "EntityId.h"
|
||||
#include "Tick.h"
|
||||
|
||||
struct Scrap
|
||||
{
|
||||
EntityId id;
|
||||
QVector2D position;
|
||||
int amount;
|
||||
Tick despawnAt;
|
||||
};
|
||||
44
src/lib/sim/ScrapSystem.cpp
Normal file
44
src/lib/sim/ScrapSystem.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#include "ScrapSystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
ScrapSystem::ScrapSystem(std::function<EntityId()> allocateId)
|
||||
: m_allocateId(std::move(allocateId))
|
||||
{
|
||||
}
|
||||
|
||||
EntityId ScrapSystem::spawn(QVector2D position, int amount, Tick despawnAt)
|
||||
{
|
||||
Scrap s;
|
||||
s.id = m_allocateId();
|
||||
s.position = position;
|
||||
s.amount = amount;
|
||||
s.despawnAt = despawnAt;
|
||||
m_scraps.push_back(s);
|
||||
return s.id;
|
||||
}
|
||||
|
||||
void ScrapSystem::tickDespawn(Tick currentTick)
|
||||
{
|
||||
m_scraps.erase(
|
||||
std::remove_if(m_scraps.begin(), m_scraps.end(),
|
||||
[currentTick](const Scrap& s) { return s.despawnAt <= currentTick; }),
|
||||
m_scraps.end());
|
||||
}
|
||||
|
||||
const Scrap* ScrapSystem::findScrap(EntityId id) const
|
||||
{
|
||||
for (const Scrap& s : m_scraps)
|
||||
{
|
||||
if (s.id == id)
|
||||
{
|
||||
return &s;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<Scrap> ScrapSystem::allScraps() const
|
||||
{
|
||||
return m_scraps;
|
||||
}
|
||||
26
src/lib/sim/ScrapSystem.h
Normal file
26
src/lib/sim/ScrapSystem.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "EntityId.h"
|
||||
#include "Scrap.h"
|
||||
#include "Tick.h"
|
||||
|
||||
class ScrapSystem
|
||||
{
|
||||
public:
|
||||
explicit ScrapSystem(std::function<EntityId()> allocateId);
|
||||
|
||||
EntityId spawn(QVector2D position, int amount, Tick despawnAt);
|
||||
void tickDespawn(Tick currentTick);
|
||||
|
||||
const Scrap* findScrap(EntityId id) const;
|
||||
std::vector<Scrap> allScraps() const;
|
||||
|
||||
private:
|
||||
std::function<EntityId()> m_allocateId;
|
||||
std::vector<Scrap> m_scraps;
|
||||
};
|
||||
90
src/lib/sim/Ship.h
Normal file
90
src/lib/sim/Ship.h
Normal file
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "EntityId.h"
|
||||
#include "MovementIntent.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hardware components — derived from config at spawn, stored on ship
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct Weapon
|
||||
{
|
||||
float damage;
|
||||
float range;
|
||||
float fireRateHz;
|
||||
float cooldownTicks;
|
||||
std::optional<EntityId> currentTarget;
|
||||
};
|
||||
|
||||
struct SalvageCargo
|
||||
{
|
||||
int capacity;
|
||||
int current;
|
||||
};
|
||||
|
||||
struct RepairTool
|
||||
{
|
||||
float ratePerTick;
|
||||
float range;
|
||||
std::optional<EntityId> currentTarget;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Behavior components — AI state consumed by step-6 behavior systems
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct ThreatResponse
|
||||
{
|
||||
float engagementRange;
|
||||
std::optional<EntityId> currentTarget;
|
||||
};
|
||||
|
||||
struct ScrapCollector
|
||||
{
|
||||
std::optional<QVector2D> scrapTarget;
|
||||
EntityId deliveryBay; // kInvalidEntityId until assigned at a salvage bay
|
||||
};
|
||||
|
||||
struct RepairBehavior
|
||||
{
|
||||
std::optional<EntityId> currentTarget;
|
||||
};
|
||||
|
||||
struct HomeReturn
|
||||
{
|
||||
float retreatHpFraction;
|
||||
QVector2D homePos;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ship
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct Ship
|
||||
{
|
||||
EntityId id;
|
||||
QVector2D position;
|
||||
QVector2D velocity;
|
||||
float hp;
|
||||
float maxHp;
|
||||
float speedPerTick; // pre-evaluated from speedFormula / kTickRateHz
|
||||
int level;
|
||||
std::string blueprintId;
|
||||
|
||||
std::optional<Weapon> weapon;
|
||||
std::optional<SalvageCargo> cargo;
|
||||
std::optional<RepairTool> repairTool;
|
||||
std::optional<ThreatResponse> threatResponse;
|
||||
std::optional<ScrapCollector> scrapCollector;
|
||||
std::optional<RepairBehavior> repairBehavior;
|
||||
std::optional<HomeReturn> homeReturn;
|
||||
|
||||
// Cleared at the start of the behavior step each tick; the highest-priority
|
||||
// write from behavior systems wins (architecture.md §Movement Arbitration).
|
||||
MovementIntent intent;
|
||||
};
|
||||
120
src/lib/sim/ShipSystem.cpp
Normal file
120
src/lib/sim/ShipSystem.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "ShipSystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
#include "Tick.h"
|
||||
|
||||
ShipSystem::ShipSystem(const GameConfig& config,
|
||||
std::function<EntityId()> allocateId)
|
||||
: m_config(config)
|
||||
, m_allocateId(std::move(allocateId))
|
||||
{
|
||||
}
|
||||
|
||||
const ShipDef* ShipSystem::findShipDef(const std::string& blueprintId) const
|
||||
{
|
||||
for (const ShipDef& def : m_config.ships.ships)
|
||||
{
|
||||
if (def.id == blueprintId)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EntityId ShipSystem::spawn(const std::string& blueprintId, int level, QVector2D position)
|
||||
{
|
||||
const ShipDef* def = findShipDef(blueprintId);
|
||||
assert(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(level);
|
||||
|
||||
Ship ship;
|
||||
ship.id = m_allocateId();
|
||||
ship.position = position;
|
||||
ship.velocity = QVector2D(0.0f, 0.0f);
|
||||
ship.maxHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
||||
ship.hp = ship.maxHp;
|
||||
ship.speedPerTick = static_cast<float>(
|
||||
def->movement.speedFormula.evaluate(x))
|
||||
/ static_cast<float>(kTickRateHz);
|
||||
ship.level = level;
|
||||
ship.blueprintId = blueprintId;
|
||||
ship.intent = MovementIntent{0, QVector2D(0.0f, 0.0f)};
|
||||
|
||||
if (def->combat)
|
||||
{
|
||||
Weapon w;
|
||||
w.damage = static_cast<float>(def->combat->damageFormula.evaluate(x));
|
||||
w.range = static_cast<float>(def->combat->attackRangeFormula.evaluate(x));
|
||||
w.fireRateHz = static_cast<float>(def->combat->attackRateFormula.evaluate(x));
|
||||
w.cooldownTicks = 0.0f;
|
||||
ship.weapon = w;
|
||||
|
||||
ThreatResponse tr;
|
||||
tr.engagementRange = w.range;
|
||||
ship.threatResponse = tr;
|
||||
}
|
||||
|
||||
if (def->salvage)
|
||||
{
|
||||
SalvageCargo cargo;
|
||||
cargo.capacity = def->salvage->cargoCapacity;
|
||||
cargo.current = 0;
|
||||
ship.cargo = cargo;
|
||||
|
||||
ScrapCollector sc;
|
||||
sc.scrapTarget = std::nullopt;
|
||||
sc.deliveryBay = kInvalidEntityId;
|
||||
ship.scrapCollector = sc;
|
||||
}
|
||||
|
||||
if (def->repair)
|
||||
{
|
||||
RepairTool rt;
|
||||
rt.ratePerTick = static_cast<float>(def->repair->repairRateFormula.evaluate(x));
|
||||
rt.range = static_cast<float>(def->repair->repairRangeFormula.evaluate(x));
|
||||
ship.repairTool = rt;
|
||||
|
||||
RepairBehavior rb;
|
||||
ship.repairBehavior = rb;
|
||||
}
|
||||
|
||||
m_ships.push_back(ship);
|
||||
return ship.id;
|
||||
}
|
||||
|
||||
void ShipSystem::despawn(EntityId id)
|
||||
{
|
||||
m_ships.erase(
|
||||
std::remove_if(m_ships.begin(), m_ships.end(),
|
||||
[id](const Ship& s) { return s.id == id; }),
|
||||
m_ships.end());
|
||||
}
|
||||
|
||||
const Ship* ShipSystem::findShip(EntityId id) const
|
||||
{
|
||||
for (const Ship& s : m_ships)
|
||||
{
|
||||
if (s.id == id)
|
||||
{
|
||||
return &s;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<Ship> ShipSystem::allShips() const
|
||||
{
|
||||
return m_ships;
|
||||
}
|
||||
|
||||
void ShipSystem::forEach(std::function<void(Ship&)> fn)
|
||||
{
|
||||
for (Ship& s : m_ships)
|
||||
{
|
||||
fn(s);
|
||||
}
|
||||
}
|
||||
31
src/lib/sim/ShipSystem.h
Normal file
31
src/lib/sim/ShipSystem.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "EntityId.h"
|
||||
#include "GameConfig.h"
|
||||
#include "Ship.h"
|
||||
|
||||
class ShipSystem
|
||||
{
|
||||
public:
|
||||
ShipSystem(const GameConfig& config,
|
||||
std::function<EntityId()> allocateId);
|
||||
|
||||
EntityId spawn(const std::string& blueprintId, int level, QVector2D position);
|
||||
void despawn(EntityId id);
|
||||
|
||||
const Ship* findShip(EntityId id) const;
|
||||
std::vector<Ship> allShips() const;
|
||||
void forEach(std::function<void(Ship&)> fn);
|
||||
|
||||
private:
|
||||
const ShipDef* findShipDef(const std::string& blueprintId) const;
|
||||
|
||||
const GameConfig& m_config;
|
||||
std::function<EntityId()> m_allocateId;
|
||||
std::vector<Ship> m_ships;
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "Simulation.h"
|
||||
|
||||
#include "BuildingSystem.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "ShipSystem.h"
|
||||
|
||||
Simulation::Simulation(const GameConfig& config, unsigned int seed)
|
||||
: m_config(config)
|
||||
@@ -16,6 +18,8 @@ Simulation::Simulation(const GameConfig& config, unsigned int seed)
|
||||
[this]() { return allocateId(); },
|
||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||
m_rng);
|
||||
m_shipSystem = std::make_unique<ShipSystem>(config, [this]() { return allocateId(); });
|
||||
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
|
||||
}
|
||||
|
||||
Simulation::~Simulation() = default;
|
||||
@@ -27,6 +31,7 @@ void Simulation::tick()
|
||||
m_buildingSystem->tickProduction(m_currentTick); // step 4
|
||||
m_buildingSystem->tickBeltPush(); // step 5
|
||||
m_beltSystem.tick(); // step 6
|
||||
m_scrapSystem->tickDespawn(m_currentTick); // step 11
|
||||
|
||||
++m_currentTick;
|
||||
}
|
||||
@@ -75,6 +80,26 @@ const BeltSystem& Simulation::belts() const
|
||||
return m_beltSystem;
|
||||
}
|
||||
|
||||
ShipSystem& Simulation::ships()
|
||||
{
|
||||
return *m_shipSystem;
|
||||
}
|
||||
|
||||
const ShipSystem& Simulation::ships() const
|
||||
{
|
||||
return *m_shipSystem;
|
||||
}
|
||||
|
||||
ScrapSystem& Simulation::scraps()
|
||||
{
|
||||
return *m_scrapSystem;
|
||||
}
|
||||
|
||||
const ScrapSystem& Simulation::scraps() const
|
||||
{
|
||||
return *m_scrapSystem;
|
||||
}
|
||||
|
||||
EntityId Simulation::allocateId()
|
||||
{
|
||||
return m_nextId++;
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "Tick.h"
|
||||
|
||||
class BuildingSystem;
|
||||
class ShipSystem;
|
||||
class ScrapSystem;
|
||||
|
||||
class Simulation
|
||||
{
|
||||
@@ -36,6 +38,10 @@ public:
|
||||
const BuildingSystem& buildings() const;
|
||||
BeltSystem& belts();
|
||||
const BeltSystem& belts() const;
|
||||
ShipSystem& ships();
|
||||
const ShipSystem& ships() const;
|
||||
ScrapSystem& scraps();
|
||||
const ScrapSystem& scraps() const;
|
||||
|
||||
private:
|
||||
EntityId allocateId(); // Strictly increasing; never returns kInvalidEntityId.
|
||||
@@ -49,6 +55,8 @@ private:
|
||||
|
||||
BeltSystem m_beltSystem;
|
||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||
std::unique_ptr<ScrapSystem> m_scrapSystem;
|
||||
|
||||
std::vector<FireEvent> m_fireEvents;
|
||||
std::vector<BlueprintDropEvent> m_blueprintDropEvents;
|
||||
|
||||
@@ -9,4 +9,6 @@ add_files(
|
||||
BeltSystemTest.cpp
|
||||
SurfaceMaskTest.cpp
|
||||
BuildingTest.cpp
|
||||
ShipTest.cpp
|
||||
ScrapTest.cpp
|
||||
)
|
||||
|
||||
83
src/test/ScrapTest.cpp
Normal file
83
src/test/ScrapTest.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "catch.hpp"
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "EntityId.h"
|
||||
#include "Scrap.h"
|
||||
#include "ScrapSystem.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spawn
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ScrapSystem: spawn returns a findable scrap with correct fields", "[scrap]")
|
||||
{
|
||||
EntityId nextId = 1;
|
||||
ScrapSystem ss([&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100);
|
||||
const Scrap* s = ss.findScrap(id);
|
||||
|
||||
REQUIRE(s != nullptr);
|
||||
REQUIRE(s->amount == 5);
|
||||
REQUIRE(s->despawnAt == 100);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Despawn timing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ScrapSystem: scrap still present one tick before despawnAt", "[scrap]")
|
||||
{
|
||||
EntityId nextId = 1;
|
||||
ScrapSystem ss([&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn(QVector2D(0.0f, 0.0f), 1, 50);
|
||||
|
||||
ss.tickDespawn(49);
|
||||
REQUIRE(ss.findScrap(id) != nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("ScrapSystem: scrap removed at despawnAt tick", "[scrap]")
|
||||
{
|
||||
EntityId nextId = 1;
|
||||
ScrapSystem ss([&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn(QVector2D(0.0f, 0.0f), 1, 50);
|
||||
|
||||
ss.tickDespawn(50);
|
||||
REQUIRE(ss.findScrap(id) == nullptr);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Selective removal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ScrapSystem: tickDespawn removes only expired scraps", "[scrap]")
|
||||
{
|
||||
EntityId nextId = 1;
|
||||
ScrapSystem ss([&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId earlyId = ss.spawn(QVector2D(0.0f, 0.0f), 1, 30);
|
||||
const EntityId lateId = ss.spawn(QVector2D(1.0f, 0.0f), 2, 60);
|
||||
|
||||
ss.tickDespawn(30);
|
||||
|
||||
REQUIRE(ss.findScrap(earlyId) == nullptr);
|
||||
REQUIRE(ss.findScrap(lateId) != nullptr);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entity ids
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ScrapSystem: spawned scraps receive strictly increasing entity ids", "[scrap]")
|
||||
{
|
||||
EntityId nextId = 1;
|
||||
ScrapSystem ss([&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id1 = ss.spawn(QVector2D(0.0f, 0.0f), 1, 100);
|
||||
const EntityId id2 = ss.spawn(QVector2D(1.0f, 0.0f), 2, 200);
|
||||
|
||||
REQUIRE(id2 > id1);
|
||||
}
|
||||
194
src/test/ShipTest.cpp
Normal file
194
src/test/ShipTest.cpp
Normal file
@@ -0,0 +1,194 @@
|
||||
#include "catch.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "ConfigLoader.h"
|
||||
#include "EntityId.h"
|
||||
#include "Ship.h"
|
||||
#include "ShipSystem.h"
|
||||
#include "Tick.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Combat ship
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ShipSystem: interceptor spawn has weapon and threatResponse, no cargo or repair",
|
||||
"[ship]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
EntityId nextId = 1;
|
||||
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const Ship* ship = ss.findShip(id);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
EntityId nextId = 1;
|
||||
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const Ship* ship = ss.findShip(id);
|
||||
|
||||
REQUIRE(ship != nullptr);
|
||||
// hp_formula = "40 + 5*x" at x=1 → 45
|
||||
REQUIRE(ship->maxHp == Approx(45.0f));
|
||||
REQUIRE(ship->hp == Approx(45.0f));
|
||||
// damage_formula = "10 + 2*x" at x=1 → 12
|
||||
REQUIRE(ship->weapon->damage == Approx(12.0f));
|
||||
// attack_range_formula = "150"
|
||||
REQUIRE(ship->weapon->range == Approx(150.0f));
|
||||
// threatResponse.engagementRange mirrors weapon range
|
||||
REQUIRE(ship->threatResponse->engagementRange == Approx(150.0f));
|
||||
// cooldownTicks starts at 0
|
||||
REQUIRE(ship->weapon->cooldownTicks == Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
EntityId nextId = 1;
|
||||
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn("interceptor", 5, QVector2D(0.0f, 0.0f));
|
||||
const Ship* ship = ss.findShip(id);
|
||||
|
||||
// hp_formula = "40 + 5*x" at x=5 → 65
|
||||
REQUIRE(ship->maxHp == Approx(65.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: interceptor level 0 speedPerTick matches formula / kTickRateHz", "[ship]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
EntityId nextId = 1;
|
||||
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn("interceptor", 0, QVector2D(0.0f, 0.0f));
|
||||
const Ship* ship = ss.findShip(id);
|
||||
|
||||
// speed_formula = "200 + 5*x" at x=0 → 200; speedPerTick = 200/30
|
||||
const float expected = 200.0f / static_cast<float>(kTickRateHz);
|
||||
REQUIRE(ship->speedPerTick == Approx(expected));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Salvage ship
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ShipSystem: salvage_ship spawn has cargo and scrapCollector, no weapon",
|
||||
"[ship]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
EntityId nextId = 1;
|
||||
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const Ship* ship = ss.findShip(id);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
EntityId nextId = 1;
|
||||
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const Ship* ship = ss.findShip(id);
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repair ship
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ShipSystem: repair_ship spawn has repairTool and repairBehavior, no weapon",
|
||||
"[ship]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
EntityId nextId = 1;
|
||||
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const Ship* ship = ss.findShip(id);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
EntityId nextId = 1;
|
||||
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const Ship* ship = ss.findShip(id);
|
||||
|
||||
// repair_rate_formula = "5 + x" at x=1 → 6
|
||||
REQUIRE(ship->repairTool->ratePerTick == Approx(6.0f));
|
||||
// repair_range_formula = "80"
|
||||
REQUIRE(ship->repairTool->range == Approx(80.0f));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entity ids and removal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ShipSystem: spawned ships receive strictly increasing entity ids", "[ship]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
EntityId nextId = 1;
|
||||
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
|
||||
|
||||
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));
|
||||
|
||||
REQUIRE(id2 > id1);
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: despawn removes the ship", "[ship]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
EntityId nextId = 1;
|
||||
ShipSystem ss(cfg, [&nextId]() { return nextId++; });
|
||||
|
||||
const EntityId id = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
REQUIRE(ss.findShip(id) != nullptr);
|
||||
|
||||
ss.despawn(id);
|
||||
REQUIRE(ss.findShip(id) == nullptr);
|
||||
}
|
||||
Reference in New Issue
Block a user