define ship roles via added modules and allow multiple weapons
This commit is contained in:
@@ -2,10 +2,13 @@
|
||||
|
||||
#include <random>
|
||||
|
||||
#include <QPoint>
|
||||
#include <QVector2D>
|
||||
|
||||
#include "AiSystem.h"
|
||||
#include "BeltSystem.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "ShipLayout.h"
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "BuildingType.h"
|
||||
@@ -81,6 +84,7 @@ struct Fixture
|
||||
ai.tickHomeReturnBehavior(admin);
|
||||
ai.tickThreatResponseBehavior(admin, buildings);
|
||||
ai.tickRepairBehavior(admin, buildings);
|
||||
ai.tickRepairTools(admin);
|
||||
ai.tickSalvageBehavior(admin, scraps, buildings);
|
||||
movementIntent.tick(admin);
|
||||
dynamicBody.tick(admin);
|
||||
@@ -88,6 +92,28 @@ struct Fixture
|
||||
}
|
||||
};
|
||||
|
||||
static ShipLayoutConfig makeSingleModuleLayout(const std::string& moduleId)
|
||||
{
|
||||
PlacedModule pm;
|
||||
pm.moduleId = moduleId;
|
||||
pm.position = QPoint(1, 1);
|
||||
pm.rotation = Rotation::East;
|
||||
ShipLayoutConfig layout;
|
||||
layout.placedModules.push_back(pm);
|
||||
return layout;
|
||||
}
|
||||
|
||||
static entt::entity firstSalvageChild(EntityAdmin& admin, entt::entity ship)
|
||||
{
|
||||
entt::entity result = entt::null;
|
||||
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, const SalvageCargoComponent&, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helpers to read ECS data for a ship entity.
|
||||
static const MovementIntentComponent& intent(EntityAdmin& a, entt::entity e)
|
||||
{
|
||||
@@ -299,7 +325,9 @@ TEST_CASE("BehaviorSystem: repair ship writes intent toward damaged friendly shi
|
||||
"[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f));
|
||||
|
||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||
@@ -315,7 +343,9 @@ TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
|
||||
"[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
|
||||
|
||||
const float initialHp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||
@@ -323,6 +353,7 @@ TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
f.ai.tickRepairBehavior(f.admin, f.buildings);
|
||||
f.ai.tickRepairTools(f.admin);
|
||||
|
||||
REQUIRE(health(f.admin, friendly).hp > initialHp);
|
||||
}
|
||||
@@ -330,7 +361,8 @@ TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
|
||||
TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||
f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, repairLayout);
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
|
||||
|
||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp - 0.001f;
|
||||
@@ -339,6 +371,7 @@ TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
|
||||
{
|
||||
f.ships.clearMovementIntents();
|
||||
f.ai.tickRepairBehavior(f.admin, f.buildings);
|
||||
f.ai.tickRepairTools(f.admin);
|
||||
}
|
||||
|
||||
const HealthComponent& h = health(f.admin, friendly);
|
||||
@@ -353,7 +386,9 @@ TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
|
||||
TEST_CASE("BehaviorSystem: salvage ship writes intent toward nearest scrap", "[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
|
||||
const QVector2D scrapPos(100.0f, 0.0f);
|
||||
f.scraps.spawn(scrapPos, 1, 100000);
|
||||
@@ -368,13 +403,17 @@ TEST_CASE("BehaviorSystem: salvage ship writes intent toward nearest scrap", "[b
|
||||
TEST_CASE("BehaviorSystem: salvage ship collects scrap on arrival", "[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
const entt::entity scrapEntity = f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings);
|
||||
|
||||
REQUIRE(f.admin.get<SalvageCargoComponent>(ship).current == 1);
|
||||
const entt::entity sc = firstSalvageChild(f.admin, ship);
|
||||
REQUIRE(f.admin.isValid(sc));
|
||||
REQUIRE(f.admin.get<SalvageCargoComponent>(sc).current == 1);
|
||||
REQUIRE_FALSE(f.admin.isValid(scrapEntity));
|
||||
}
|
||||
|
||||
@@ -395,9 +434,15 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
||||
}
|
||||
REQUIRE(f.buildings.findBuilding(bayId) != nullptr);
|
||||
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(5.0f, 0.0f));
|
||||
SalvageCargoComponent& cargo = f.admin.get<SalvageCargoComponent>(ship);
|
||||
cargo.current = cargo.capacity; // full cargo
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(5.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
{
|
||||
const entt::entity sc = firstSalvageChild(f.admin, ship);
|
||||
REQUIRE(f.admin.isValid(sc));
|
||||
SalvageCargoComponent& cargo = f.admin.get<SalvageCargoComponent>(sc);
|
||||
cargo.current = cargo.capacity; // full cargo
|
||||
}
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings);
|
||||
@@ -467,7 +512,9 @@ TEST_CASE("SensorRange: enemy ship ignores player just outside sensor range", "[
|
||||
TEST_CASE("SensorRange: repair ship retreats from enemy within sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
f.ships.spawn("interceptor", 1, QVector2D(200.0f, 0.0f), /*isEnemy=*/true);
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
@@ -480,7 +527,9 @@ TEST_CASE("SensorRange: repair ship retreats from enemy within sensor range", "[
|
||||
TEST_CASE("SensorRange: repair ship does not retreat from enemy beyond sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f), /*isEnemy=*/true);
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
@@ -492,7 +541,9 @@ TEST_CASE("SensorRange: repair ship does not retreat from enemy beyond sensor ra
|
||||
TEST_CASE("SensorRange: repair ship does not acquire damaged ally beyond sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool_module");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f));
|
||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||
|
||||
@@ -509,7 +560,9 @@ TEST_CASE("SensorRange: repair ship does not acquire damaged ally beyond sensor
|
||||
TEST_CASE("SensorRange: salvage ship ignores scrap beyond sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
f.scraps.spawn(QVector2D(300.0f, 0.0f), 1, 100000);
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "FireEvent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "HqProxyComponent.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "ShipSystem.h"
|
||||
#include "Simulation.h"
|
||||
@@ -30,7 +31,7 @@ static const ShipDef* findCombatShip(const GameConfig& cfg)
|
||||
{
|
||||
for (const ShipDef& def : cfg.ships.ships)
|
||||
{
|
||||
if (def.combat)
|
||||
if (!def.defaultModules.empty())
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
@@ -38,6 +39,17 @@ static const ShipDef* findCombatShip(const GameConfig& cfg)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static entt::entity findWeaponChild(EntityAdmin& admin, entt::entity ship)
|
||||
{
|
||||
entt::entity result = entt::null;
|
||||
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, const WeaponComponent&, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper fixture for unit tests that need ships + combat but not a full Simulation.
|
||||
struct CombatFixture
|
||||
{
|
||||
@@ -67,10 +79,13 @@ struct CombatFixture
|
||||
|
||||
void wireEnemyTarget(entt::entity enemy, entt::entity playerTarget)
|
||||
{
|
||||
if (admin.hasAll<WeaponComponent>(enemy))
|
||||
// Set target on weapon child entity (CombatSystem syncs from ThreatResponse each tick,
|
||||
// but also setting directly ensures the first tick fires without waiting for sync).
|
||||
const entt::entity wc = findWeaponChild(admin, enemy);
|
||||
if (wc != entt::null)
|
||||
{
|
||||
admin.get<WeaponComponent>(enemy).currentTarget = playerTarget;
|
||||
admin.get<WeaponComponent>(enemy).cooldownTicks = 0.0f;
|
||||
admin.get<WeaponComponent>(wc).currentTarget = playerTarget;
|
||||
admin.get<WeaponComponent>(wc).cooldownTicks = 0.0f;
|
||||
}
|
||||
if (admin.hasAll<ThreatResponseBehaviorComponent>(enemy))
|
||||
{
|
||||
@@ -113,7 +128,11 @@ TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]"
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
f.admin.get<WeaponComponent>(enemy).cooldownTicks = 3.0f; // override to 3
|
||||
{
|
||||
const entt::entity wc = findWeaponChild(f.admin, enemy);
|
||||
REQUIRE(f.admin.isValid(wc));
|
||||
f.admin.get<WeaponComponent>(wc).cooldownTicks = 3.0f; // override to 3
|
||||
}
|
||||
|
||||
auto enemyFiredIn = [&enemy](const std::vector<FireEvent>& evts)
|
||||
{
|
||||
|
||||
@@ -108,23 +108,19 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
|
||||
REQUIRE(ironIngotIt->outputs.size() == 1);
|
||||
REQUIRE_FALSE(ironIngotIt->outputs[0].probability.has_value());
|
||||
|
||||
// ships.toml — combat ships have a combat section; salvage ships don't.
|
||||
// ships.toml — combat ships have default_modules with a weapon; salvage ships don't.
|
||||
const auto interceptorIt = std::find_if(
|
||||
cfg.ships.ships.begin(), cfg.ships.ships.end(),
|
||||
[](const ShipDef& s) { return s.id == "interceptor"; });
|
||||
REQUIRE(interceptorIt != cfg.ships.ships.end());
|
||||
REQUIRE(interceptorIt->combat.has_value());
|
||||
REQUIRE_FALSE(interceptorIt->salvage.has_value());
|
||||
REQUIRE_FALSE(interceptorIt->repair.has_value());
|
||||
REQUIRE(interceptorIt->combat->damageFormula.evaluate(5.0) == Approx(20.0)); // "10 + 2*x"
|
||||
REQUIRE_FALSE(interceptorIt->defaultModules.empty());
|
||||
REQUIRE(interceptorIt->defaultModules[0].moduleId == "laser_cannon");
|
||||
|
||||
const auto salvageShipIt = std::find_if(
|
||||
cfg.ships.ships.begin(), cfg.ships.ships.end(),
|
||||
[](const ShipDef& s) { return s.id == "salvage_ship"; });
|
||||
REQUIRE(salvageShipIt != cfg.ships.ships.end());
|
||||
REQUIRE_FALSE(salvageShipIt->combat.has_value());
|
||||
REQUIRE(salvageShipIt->salvage.has_value());
|
||||
REQUIRE(salvageShipIt->salvage->cargoCapacity == 10);
|
||||
REQUIRE(salvageShipIt->defaultModules.empty());
|
||||
|
||||
// stations.toml
|
||||
REQUIRE(cfg.stations.playerStation.level == 5);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
#include <QPoint>
|
||||
#include <QVector2D>
|
||||
|
||||
#include "BuildingId.h"
|
||||
@@ -10,11 +11,14 @@
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "RepairBehaviorComponent.h"
|
||||
#include "RepairToolComponent.h"
|
||||
#include "Rotation.h"
|
||||
#include "SalvageBehaviorComponent.h"
|
||||
#include "SalvageCargoComponent.h"
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "ShipLayout.h"
|
||||
#include "ShipSystem.h"
|
||||
#include "Tick.h"
|
||||
#include "ThreatResponseBehaviorComponent.h"
|
||||
@@ -26,10 +30,58 @@ static GameConfig loadConfig()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Combat ship
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ShipSystem: interceptor spawn has weapon and threatResponse, no cargo or repair",
|
||||
static entt::entity firstWeaponChild(EntityAdmin& admin, entt::entity ship)
|
||||
{
|
||||
entt::entity result = entt::null;
|
||||
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, const WeaponComponent&, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
static entt::entity firstSalvageChild(EntityAdmin& admin, entt::entity ship)
|
||||
{
|
||||
entt::entity result = entt::null;
|
||||
admin.forEach<SalvageCargoComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, const SalvageCargoComponent&, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
static entt::entity firstRepairChild(EntityAdmin& admin, entt::entity ship)
|
||||
{
|
||||
entt::entity result = entt::null;
|
||||
admin.forEach<RepairToolComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity ce, const RepairToolComponent&, const ModuleOwnerComponent& o)
|
||||
{
|
||||
if (o.owner == ship && result == entt::null) { result = ce; }
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
static ShipLayoutConfig makeSingleModuleLayout(const std::string& moduleId)
|
||||
{
|
||||
PlacedModule pm;
|
||||
pm.moduleId = moduleId;
|
||||
pm.position = QPoint(0, 0);
|
||||
pm.rotation = Rotation::East;
|
||||
ShipLayoutConfig layout;
|
||||
layout.placedModules.push_back(pm);
|
||||
return layout;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Combat ship (interceptor has default_modules = [laser_cannon])
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ShipSystem: interceptor spawn has weapon child and threatResponse, no cargo or repair",
|
||||
"[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
@@ -39,10 +91,10 @@ TEST_CASE("ShipSystem: interceptor spawn has weapon and threatResponse, no cargo
|
||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
|
||||
REQUIRE(admin.isValid(e));
|
||||
REQUIRE(admin.hasAll<WeaponComponent>(e));
|
||||
REQUIRE(admin.isValid(firstWeaponChild(admin, e)));
|
||||
REQUIRE(admin.hasAll<ThreatResponseBehaviorComponent>(e));
|
||||
REQUIRE_FALSE(admin.hasAll<SalvageCargoComponent>(e));
|
||||
REQUIRE_FALSE(admin.hasAll<RepairToolComponent>(e));
|
||||
REQUIRE_FALSE(admin.isValid(firstSalvageChild(admin, e)));
|
||||
REQUIRE_FALSE(admin.isValid(firstRepairChild(admin, e)));
|
||||
REQUIRE_FALSE(admin.hasAll<RepairBehaviorComponent>(e));
|
||||
REQUIRE_FALSE(admin.hasAll<SalvageBehaviorComponent>(e));
|
||||
}
|
||||
@@ -58,14 +110,15 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]
|
||||
// hp_formula = "40 + 5*x" at x=1 → 45
|
||||
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
|
||||
REQUIRE(admin.get<HealthComponent>(e).hp == Approx(45.0f));
|
||||
// damage_formula = "10 + 2*x" at x=1 → 12
|
||||
REQUIRE(admin.get<WeaponComponent>(e).damage == Approx(12.0f));
|
||||
// attack_range_formula = "150"
|
||||
REQUIRE(admin.get<WeaponComponent>(e).range == Approx(150.0f));
|
||||
// sensor_range_formula = "200"
|
||||
REQUIRE(admin.get<SensorRangeComponent>(e).value == Approx(200.0f));
|
||||
// cooldownTicks starts at 0
|
||||
REQUIRE(admin.get<WeaponComponent>(e).cooldownTicks == Approx(0.0f));
|
||||
|
||||
// laser_cannon: damage_formula = "2", attack_range_formula = "5"
|
||||
const entt::entity wc = firstWeaponChild(admin, e);
|
||||
REQUIRE(admin.isValid(wc));
|
||||
REQUIRE(admin.get<WeaponComponent>(wc).damage == Approx(2.0f));
|
||||
REQUIRE(admin.get<WeaponComponent>(wc).range == Approx(5.0f));
|
||||
REQUIRE(admin.get<WeaponComponent>(wc).cooldownTicks == Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]")
|
||||
@@ -94,22 +147,23 @@ TEST_CASE("ShipSystem: interceptor level 0 maxSpeedPerTick matches formula / kTi
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Salvage ship
|
||||
// Salvage ship (spawned with salvage_bay_module layout)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ShipSystem: salvage_ship spawn has cargo and scrapCollector, no weapon",
|
||||
TEST_CASE("ShipSystem: salvage_ship spawn with salvage module has cargo child and behavior, no weapon",
|
||||
"[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("salvage_bay_module");
|
||||
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||
|
||||
REQUIRE(admin.hasAll<SalvageCargoComponent>(e));
|
||||
REQUIRE(admin.isValid(firstSalvageChild(admin, e)));
|
||||
REQUIRE(admin.hasAll<SalvageBehaviorComponent>(e));
|
||||
REQUIRE_FALSE(admin.hasAll<WeaponComponent>(e));
|
||||
REQUIRE_FALSE(admin.hasAll<RepairToolComponent>(e));
|
||||
REQUIRE_FALSE(admin.isValid(firstWeaponChild(admin, e)));
|
||||
REQUIRE_FALSE(admin.isValid(firstRepairChild(admin, e)));
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
|
||||
@@ -118,32 +172,37 @@ TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("salvage_bay_module");
|
||||
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||
|
||||
// cargo_capacity = 10
|
||||
REQUIRE(admin.get<SalvageCargoComponent>(e).capacity == 10);
|
||||
REQUIRE(admin.get<SalvageCargoComponent>(e).current == 0);
|
||||
// salvage_bay_module: cargo_capacity_formula = "10", collection_range_formula = "50"
|
||||
const entt::entity sc = firstSalvageChild(admin, e);
|
||||
REQUIRE(admin.isValid(sc));
|
||||
REQUIRE(admin.get<SalvageCargoComponent>(sc).capacity == 10);
|
||||
REQUIRE(admin.get<SalvageCargoComponent>(sc).current == 0);
|
||||
REQUIRE(admin.get<SalvageBehaviorComponent>(e).deliveryBay == kInvalidBuildingId);
|
||||
REQUIRE_FALSE(admin.get<SalvageBehaviorComponent>(e).scrapTarget.has_value());
|
||||
REQUIRE(admin.get<SalvageBehaviorComponent>(e).maxCollectionRange == Approx(50.0f));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repair ship
|
||||
// Repair ship (spawned with repair_tool_module layout)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ShipSystem: repair_ship spawn has repairTool and repairBehavior, no weapon",
|
||||
TEST_CASE("ShipSystem: repair_ship spawn with repair module has repair child and behavior, no weapon",
|
||||
"[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool_module");
|
||||
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||
|
||||
REQUIRE(admin.hasAll<RepairToolComponent>(e));
|
||||
REQUIRE(admin.isValid(firstRepairChild(admin, e)));
|
||||
REQUIRE(admin.hasAll<RepairBehaviorComponent>(e));
|
||||
REQUIRE_FALSE(admin.hasAll<WeaponComponent>(e));
|
||||
REQUIRE_FALSE(admin.hasAll<SalvageCargoComponent>(e));
|
||||
REQUIRE_FALSE(admin.isValid(firstWeaponChild(admin, e)));
|
||||
REQUIRE_FALSE(admin.isValid(firstSalvageChild(admin, e)));
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]")
|
||||
@@ -152,12 +211,17 @@ TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas",
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool_module");
|
||||
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||
|
||||
// repair_rate_formula = "5 + x" at x=1 → 6
|
||||
REQUIRE(admin.get<RepairToolComponent>(e).ratePerTick == Approx(6.0f));
|
||||
// repair_tool_module: repair_rate_formula = "5 + x" at x=1 → 6 / kTickRateHz
|
||||
const float expectedRate = 6.0f / static_cast<float>(kTickRateHz);
|
||||
const entt::entity rc = firstRepairChild(admin, e);
|
||||
REQUIRE(admin.isValid(rc));
|
||||
REQUIRE(admin.get<RepairToolComponent>(rc).ratePerTick == Approx(expectedRate));
|
||||
// repair_range_formula = "80"
|
||||
REQUIRE(admin.get<RepairToolComponent>(e).range == Approx(80.0f));
|
||||
REQUIRE(admin.get<RepairToolComponent>(rc).range == Approx(80.0f));
|
||||
REQUIRE(admin.get<RepairBehaviorComponent>(e).maxRepairRange == Approx(80.0f));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -171,22 +235,26 @@ TEST_CASE("ShipSystem: spawned ships are valid entities", "[ship]")
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
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));
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvage_bay_module");
|
||||
const entt::entity e2 = ss.spawn("salvage_ship", 1, QVector2D(1.0f, 0.0f), false, salvageLayout);
|
||||
|
||||
REQUIRE(admin.isValid(e1));
|
||||
REQUIRE(admin.isValid(e2));
|
||||
REQUIRE(e1 != e2);
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: despawn removes the ship", "[ship]")
|
||||
TEST_CASE("ShipSystem: despawn removes the ship and its weapon children", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity wc = firstWeaponChild(admin, e);
|
||||
REQUIRE(admin.isValid(e));
|
||||
REQUIRE(admin.isValid(wc));
|
||||
|
||||
ss.despawn(e);
|
||||
REQUIRE_FALSE(admin.isValid(e));
|
||||
REQUIRE_FALSE(admin.isValid(wc));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "HqProxyComponent.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "Rotation.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "ShipSystem.h"
|
||||
@@ -166,13 +167,14 @@ TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
|
||||
|
||||
TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
|
||||
{
|
||||
const Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadConfig(), 42);
|
||||
|
||||
int armedPlayerStations = 0;
|
||||
sim.admin().forEach<StationBodyComponent, FactionComponent, WeaponComponent>(
|
||||
[&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f,
|
||||
const WeaponComponent& w)
|
||||
sim.admin().forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo)
|
||||
{
|
||||
if (!sim.admin().hasAll<StationBodyComponent>(mo.owner)) { return; }
|
||||
const FactionComponent& f = sim.admin().get<FactionComponent>(mo.owner);
|
||||
if (!f.isEnemy)
|
||||
{
|
||||
++armedPlayerStations;
|
||||
@@ -186,13 +188,14 @@ TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
|
||||
|
||||
TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
|
||||
{
|
||||
const Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadConfig(), 42);
|
||||
|
||||
int armedEnemyStations = 0;
|
||||
sim.admin().forEach<StationBodyComponent, FactionComponent, WeaponComponent>(
|
||||
[&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f,
|
||||
const WeaponComponent& w)
|
||||
sim.admin().forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*e*/, const WeaponComponent& w, const ModuleOwnerComponent& mo)
|
||||
{
|
||||
if (!sim.admin().hasAll<StationBodyComponent>(mo.owner)) { return; }
|
||||
const FactionComponent& f = sim.admin().get<FactionComponent>(mo.owner);
|
||||
if (f.isEnemy)
|
||||
{
|
||||
++armedEnemyStations;
|
||||
|
||||
Reference in New Issue
Block a user