define ship roles via added modules and allow multiple weapons

This commit is contained in:
2026-06-01 22:57:53 +02:00
parent f363f7a67c
commit 9d0a60a93b
31 changed files with 873 additions and 407 deletions

View File

@@ -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();