move ecs related code to own folder

This commit is contained in:
2026-05-25 08:46:58 +02:00
parent 8ad7530740
commit 25ff3c56c5
54 changed files with 877 additions and 680 deletions

View File

@@ -10,16 +10,20 @@
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "CombatSystem.h" #include "CombatSystem.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "DynamicBodySystem.h" #include "DynamicBodySystem.h"
#include "EntityAdmin.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "MovementIntentSystem.h" #include "MovementIntentSystem.h"
#include "PositionComponent.h"
#include "ScrapSystem.h" #include "ScrapSystem.h"
#include "Ship.h" #include "ShipIdentityComponent.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "ShipsConfig.h" #include "ShipsConfig.h"
#include "StationBodyComponent.h"
#include "StationsConfig.h" #include "StationsConfig.h"
#include "SurfaceMask.h" #include "SurfaceMask.h"
#include "WeaponComponent.h"
ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig, ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
ArenaConfig arenaConfig, ArenaConfig arenaConfig,
@@ -87,8 +91,6 @@ void ArenaSimulation::placeStructures()
{ {
absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y())); absCells.push_back(QPoint(anchor.x() + rel.x(), anchor.y() + rel.y()));
} }
const QVector2D center(anchorX + hqParsed.footprint.width() / 2.0f,
anchorY + hqParsed.footprint.height() / 2.0f);
m_team1HqEntity = m_admin.spawnStation(anchor, hqParsed.footprint, absCells, m_team1HqEntity = m_admin.spawnStation(anchor, hqParsed.footprint, absCells,
hp, hp, false); hp, hp, false);
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
@@ -116,7 +118,7 @@ void ArenaSimulation::placeStructures()
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy) auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
{ {
float hp = 0.0f; float hp = 0.0f;
Weapon weapon; WeaponComponent weapon;
weapon.cooldownTicks = 0.0f; weapon.cooldownTicks = 0.0f;
weapon.currentTarget = std::nullopt; weapon.currentTarget = std::nullopt;
const double lv = static_cast<double>(entry.level); const double lv = static_cast<double>(entry.level);
@@ -157,7 +159,7 @@ void ArenaSimulation::placeStructures()
} }
const entt::entity stationEntity = m_admin.spawnStation( const entt::entity stationEntity = m_admin.spawnStation(
anchor, parsed.footprint, absCells, hp, hp, isEnemy); anchor, parsed.footprint, absCells, hp, hp, isEnemy);
m_admin.addComponent<Weapon>(stationEntity, weapon); m_admin.addComponent<WeaponComponent>(stationEntity, weapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
}; };
@@ -275,8 +277,9 @@ void ArenaSimulation::tickDeaths()
{ {
// Dead ships. // Dead ships.
std::vector<entt::entity> deadShips; std::vector<entt::entity> deadShips;
m_admin.forEach<ShipIdentity, Health>( m_admin.forEach<ShipIdentityComponent, HealthComponent>(
[&deadShips](entt::entity e, const ShipIdentity& /*si*/, const Health& h) [&deadShips](entt::entity e, const ShipIdentityComponent& /*si*/,
const HealthComponent& h)
{ {
if (h.hp <= 0.0f) if (h.hp <= 0.0f)
{ {
@@ -286,8 +289,8 @@ void ArenaSimulation::tickDeaths()
for (entt::entity deadEntity : deadShips) for (entt::entity deadEntity : deadShips)
{ {
const ShipIdentity& si = m_admin.get<ShipIdentity>(deadEntity); const ShipIdentityComponent& si = m_admin.get<ShipIdentityComponent>(deadEntity);
const Position& pos = m_admin.get<Position>(deadEntity); const PositionComponent& pos = m_admin.get<PositionComponent>(deadEntity);
for (const ShipDef& def : m_gameConfig.ships.ships) for (const ShipDef& def : m_gameConfig.ships.ships)
{ {
if (def.id == si.schematicId && def.loot.scrapDrop > 0) if (def.id == si.schematicId && def.loot.scrapDrop > 0)
@@ -303,8 +306,9 @@ void ArenaSimulation::tickDeaths()
// Dead stations. // Dead stations.
std::vector<entt::entity> deadStations; std::vector<entt::entity> deadStations;
m_admin.forEach<StationBody, Health>( m_admin.forEach<StationBodyComponent, HealthComponent>(
[&deadStations](entt::entity e, const StationBody& /*sb*/, const Health& h) [&deadStations](entt::entity e, const StationBodyComponent& /*sb*/,
const HealthComponent& h)
{ {
if (h.hp <= 0.0f) if (h.hp <= 0.0f)
{ {
@@ -314,16 +318,16 @@ void ArenaSimulation::tickDeaths()
for (entt::entity deadEntity : deadStations) for (entt::entity deadEntity : deadStations)
{ {
const StationBody& sb = m_admin.get<StationBody>(deadEntity); const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells); m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
m_admin.destroy(deadEntity); m_admin.destroy(deadEntity);
} }
// Check end conditions — HQ proxy entities. // Check end conditions — HQ proxy entities.
const bool team1HqGone = !m_admin.isValid(m_team1HqEntity) const bool team1HqGone = !m_admin.isValid(m_team1HqEntity)
|| m_admin.get<Health>(m_team1HqEntity).hp <= 0.0f; || m_admin.get<HealthComponent>(m_team1HqEntity).hp <= 0.0f;
const bool team2HqGone = !m_admin.isValid(m_team2HqEntity) const bool team2HqGone = !m_admin.isValid(m_team2HqEntity)
|| m_admin.get<Health>(m_team2HqEntity).hp <= 0.0f; || m_admin.get<HealthComponent>(m_team2HqEntity).hp <= 0.0f;
if (team1HqGone || team2HqGone) if (team1HqGone || team2HqGone)
{ {
@@ -336,19 +340,19 @@ void ArenaSimulation::tickDeaths()
// Check if all ships and defence stations of one team are destroyed. // Check if all ships and defence stations of one team are destroyed.
bool team1HasUnits = false; bool team1HasUnits = false;
bool team2HasUnits = false; bool team2HasUnits = false;
m_admin.forEach<ShipIdentity, Faction>( m_admin.forEach<ShipIdentityComponent, FactionComponent>(
[&team1HasUnits, &team2HasUnits](entt::entity /*e*/, [&team1HasUnits, &team2HasUnits](entt::entity /*e*/,
const ShipIdentity& /*si*/, const ShipIdentityComponent& /*si*/,
const Faction& f) const FactionComponent& f)
{ {
if (f.isEnemy) { team2HasUnits = true; } if (f.isEnemy) { team2HasUnits = true; }
else { team1HasUnits = true; } else { team1HasUnits = true; }
}); });
m_admin.forEach<StationBody, Faction>( m_admin.forEach<StationBodyComponent, FactionComponent>(
[&team1HasUnits, &team2HasUnits](entt::entity /*e*/, [&team1HasUnits, &team2HasUnits](entt::entity /*e*/,
const StationBody& /*sb*/, const StationBodyComponent& /*sb*/,
const Faction& f) const FactionComponent& f)
{ {
if (f.isEnemy) { team2HasUnits = true; } if (f.isEnemy) { team2HasUnits = true; }
else { team1HasUnits = true; } else { team1HasUnits = true; }
@@ -442,7 +446,7 @@ void ArenaSimulation::updateStatus()
hqEntry.total = 1; hqEntry.total = 1;
const entt::entity hqEntity = (ti == 0) ? m_team1HqEntity : m_team2HqEntity; const entt::entity hqEntity = (ti == 0) ? m_team1HqEntity : m_team2HqEntity;
hqEntry.surviving = (m_admin.isValid(hqEntity) hqEntry.surviving = (m_admin.isValid(hqEntity)
&& m_admin.get<Health>(hqEntity).hp > 0.0f) ? 1 : 0; && m_admin.get<HealthComponent>(hqEntity).hp > 0.0f) ? 1 : 0;
teamStatus.entries.push_back(hqEntry); teamStatus.entries.push_back(hqEntry);
} }
@@ -456,9 +460,10 @@ void ArenaSimulation::updateStatus()
int surviving = 0; int surviving = 0;
const bool isEnemyTeam = (ti == 1); const bool isEnemyTeam = (ti == 1);
m_admin.forEach<ShipIdentity, Faction, Health>( m_admin.forEach<ShipIdentityComponent, FactionComponent, HealthComponent>(
[&surviving, &shipEntry, isEnemyTeam](entt::entity /*e*/, [&surviving, &shipEntry, isEnemyTeam](entt::entity /*e*/,
const ShipIdentity& si, const Faction& f, const Health& h) const ShipIdentityComponent& si, const FactionComponent& f,
const HealthComponent& h)
{ {
if (f.isEnemy == isEnemyTeam if (f.isEnemy == isEnemyTeam
&& si.schematicId == shipEntry.schematicId && si.schematicId == shipEntry.schematicId
@@ -484,9 +489,10 @@ void ArenaSimulation::updateStatus()
int surviving = 0; int surviving = 0;
const bool isEnemyTeam = (ti == 1); const bool isEnemyTeam = (ti == 1);
m_admin.forEach<StationBody, Faction, Health>( m_admin.forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[&surviving, &stationEntry, isEnemyTeam](entt::entity /*e*/, [&surviving, &stationEntry, isEnemyTeam](entt::entity /*e*/,
const StationBody& sb, const Faction& f, const Health& h) const StationBodyComponent& sb, const FactionComponent& f,
const HealthComponent& h)
{ {
if (f.isEnemy == isEnemyTeam if (f.isEnemy == isEnemyTeam
&& sb.anchor == stationEntry.position && sb.anchor == stationEntry.position
@@ -503,4 +509,3 @@ void ArenaSimulation::updateStatus()
std::lock_guard<std::mutex> lock(m_statusMutex); std::lock_guard<std::mutex> lock(m_statusMutex);
m_status = newStatus; m_status = newStatus;
} }

View File

@@ -9,7 +9,6 @@
#include "BalancingConfig.h" #include "BalancingConfig.h"
#include "BeltSystem.h" #include "BeltSystem.h"
#include "EcsComponents.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "BuildingId.h" #include "BuildingId.h"

View File

@@ -10,10 +10,15 @@
#include "ArenaSimulation.h" #include "ArenaSimulation.h"
#include "Building.h" #include "Building.h"
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "DynamicBodyComponent.h" #include "FacingComponent.h"
#include "EcsComponents.h" #include "FactionComponent.h"
#include "HealthComponent.h"
#include "PositionComponent.h"
#include "RepairToolComponent.h"
#include "SalvageCargoComponent.h"
#include "ScrapSystem.h" #include "ScrapSystem.h"
#include "Ship.h" #include "ShipIdentityComponent.h"
#include "StationBodyComponent.h"
namespace namespace
{ {
@@ -101,9 +106,9 @@ void ArenaView::onFrame()
{ {
float maxRadius = 0.125f; float maxRadius = 0.125f;
if (m_sim->admin().isValid(fe.target) if (m_sim->admin().isValid(fe.target)
&& m_sim->admin().hasAll<StationBody>(fe.target)) && m_sim->admin().hasAll<StationBodyComponent>(fe.target))
{ {
const StationBody& sb = m_sim->admin().get<StationBody>(fe.target); const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(fe.target);
const int shorter = std::min(sb.footprint.width(), const int shorter = std::min(sb.footprint.width(),
sb.footprint.height()); sb.footprint.height());
maxRadius = shorter / 2.0f; maxRadius = shorter / 2.0f;
@@ -197,11 +202,11 @@ QRectF ArenaView::tileRect(QPoint tile) const
std::optional<QVector2D> ArenaView::entityPosition(entt::entity entity) const std::optional<QVector2D> ArenaView::entityPosition(entt::entity entity) const
{ {
if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll<Position>(entity)) if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll<PositionComponent>(entity))
{ {
return std::nullopt; return std::nullopt;
} }
return m_sim->admin().get<Position>(entity).value; return m_sim->admin().get<PositionComponent>(entity).value;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -273,8 +278,8 @@ void ArenaView::drawScrap(QPainter& painter)
void ArenaView::drawStations(QPainter& painter) void ArenaView::drawStations(QPainter& painter)
{ {
m_sim->admin().forEach<StationBody, Faction, Health>( m_sim->admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[&](entt::entity /*e*/, const StationBody& sb, const Faction& f, const Health& h) [&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f, const HealthComponent& h)
{ {
const BuildingType visType = f.isEnemy const BuildingType visType = f.isEnemy
? BuildingType::EnemyDefenceStation ? BuildingType::EnemyDefenceStation
@@ -315,12 +320,14 @@ void ArenaView::drawStations(QPainter& painter)
void ArenaView::drawShips(QPainter& painter) void ArenaView::drawShips(QPainter& painter)
{ {
m_sim->admin().forEach<ShipIdentity, Position, Facing, Faction>( m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
[&](entt::entity e, const ShipIdentity& /*si*/, const Position& pos, FactionComponent>(
const Facing& facing, const Faction& fac) [&](entt::entity e, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FacingComponent& facing,
const FactionComponent& fac)
{ {
const bool hasCargo = m_sim->admin().hasAll<SalvageCargo>(e); const bool hasCargo = m_sim->admin().hasAll<SalvageCargoComponent>(e);
const bool hasRepair = m_sim->admin().hasAll<RepairTool>(e); const bool hasRepair = m_sim->admin().hasAll<RepairToolComponent>(e);
const ShipRole role = shipRoleFromComponents(fac.isEnemy, hasCargo, hasRepair); const ShipRole role = shipRoleFromComponents(fac.isEnemy, hasCargo, hasRepair);
const std::map<ShipRole, ShipVisuals>::const_iterator it = const std::map<ShipRole, ShipVisuals>::const_iterator it =
m_visuals->ships.find(role); m_visuals->ships.find(role);

View File

@@ -5,6 +5,7 @@ add_subdirectory(core)
add_subdirectory(config) add_subdirectory(config)
add_subdirectory(utility) add_subdirectory(utility)
add_subdirectory(sim) add_subdirectory(sim)
add_subdirectory(ecs)
SET(HDRS SET(HDRS
${HDRS} ${HDRS}

View File

@@ -4,14 +4,12 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/Rotation.h ${CMAKE_CURRENT_SOURCE_DIR}/Rotation.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.h
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h ${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodyComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/EcsComponents.h
${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h ${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingId.h
${CMAKE_CURRENT_SOURCE_DIR}/FireEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h ${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
${CMAKE_CURRENT_SOURCE_DIR}/Item.h ${CMAKE_CURRENT_SOURCE_DIR}/Item.h
${CMAKE_CURRENT_SOURCE_DIR}/Port.h ${CMAKE_CURRENT_SOURCE_DIR}/Port.h
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntent.h
${CMAKE_CURRENT_SOURCE_DIR}/FireEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/SchematicDropEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/SchematicDropEvent.h
PARENT_SCOPE PARENT_SCOPE
) )

View File

@@ -1,91 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include <QPoint>
#include <QSize>
#include <QVector2D>
#include "Tick.h"
#include "entt/entity/entity.hpp"
// ---------------------------------------------------------------------------
// Shared components (used by ships, stations, scrap, HQ proxy)
// ---------------------------------------------------------------------------
struct Position
{
QVector2D value;
};
struct Health
{
float hp;
float maxHp;
};
struct Faction
{
bool isEnemy;
};
// ---------------------------------------------------------------------------
// Ship components (always present on every ship)
// ---------------------------------------------------------------------------
struct Facing
{
float radians;
};
struct SensorRange
{
float value;
};
struct ShipIdentity
{
int level;
std::string schematicId;
};
// ---------------------------------------------------------------------------
// Ship optional components (hardware + behavior, in Ship.h)
// ---------------------------------------------------------------------------
// Weapon, SalvageCargo, RepairTool, ThreatResponseBehavior, SalvageBehavior,
// RepairBehavior, HomeReturnBehavior, RallyBehavior remain defined in Ship.h.
// ---------------------------------------------------------------------------
// Station components
// ---------------------------------------------------------------------------
struct StationBody
{
QPoint anchor;
QSize footprint;
std::vector<QPoint> bodyCells;
};
// ---------------------------------------------------------------------------
// Scrap components
// ---------------------------------------------------------------------------
struct ScrapData
{
int amount;
};
struct DespawnAt
{
Tick tick;
};
// ---------------------------------------------------------------------------
// HQ proxy (empty tag)
// ---------------------------------------------------------------------------
struct HqProxy { char unused = 0; };

View File

@@ -1,8 +1,17 @@
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "DespawnAtComponent.h"
#include "DynamicBodyComponent.h" #include "DynamicBodyComponent.h"
#include "EcsComponents.h" #include "FacingComponent.h"
#include "MovementIntent.h" #include "FactionComponent.h"
#include "HealthComponent.h"
#include "HqProxyComponent.h"
#include "MovementIntentComponent.h"
#include "PositionComponent.h"
#include "ScrapDataComponent.h"
#include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h"
#include "StationBodyComponent.h"
entt::entity EntityAdmin::createEntity() entt::entity EntityAdmin::createEntity()
{ {
@@ -31,10 +40,10 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
int level, const std::string& schematicId, bool isEnemy) int level, const std::string& schematicId, bool isEnemy)
{ {
entt::entity entity = createEntity(); entt::entity entity = createEntity();
add<Position>(entity, Position{position}); add<PositionComponent>(entity, PositionComponent{position});
add<Health>(entity, Health{hp, maxHp}); add<HealthComponent>(entity, HealthComponent{hp, maxHp});
add<Faction>(entity, Faction{isEnemy}); add<FactionComponent>(entity, FactionComponent{isEnemy});
add<Facing>(entity, Facing{0.0f}); add<FacingComponent>(entity, FacingComponent{0.0f});
add<DynamicBodyComponent>(entity, DynamicBodyComponent{ add<DynamicBodyComponent>(entity, DynamicBodyComponent{
maxSpeedPerTick, maxSpeedPerTick,
mainAccelPerTick, mainAccelPerTick,
@@ -46,9 +55,9 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
QVector2D(0.0f, 0.0f), // linearAcceleration QVector2D(0.0f, 0.0f), // linearAcceleration
0.0f // angularAcceleration 0.0f // angularAcceleration
}); });
add<SensorRange>(entity, SensorRange{sensorRange}); add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange});
add<ShipIdentity>(entity, ShipIdentity{level, schematicId}); add<ShipIdentityComponent>(entity, ShipIdentityComponent{level, schematicId});
add<MovementIntent>(entity, MovementIntent{0, QVector2D(0.0f, 0.0f)}); add<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
return entity; return entity;
} }
@@ -59,28 +68,28 @@ entt::entity EntityAdmin::spawnStation(QPoint anchor, QSize footprint,
entt::entity entity = createEntity(); entt::entity entity = createEntity();
QVector2D center(anchor.x() + footprint.width() / 2.0f, QVector2D center(anchor.x() + footprint.width() / 2.0f,
anchor.y() + footprint.height() / 2.0f); anchor.y() + footprint.height() / 2.0f);
add<Position>(entity, Position{center}); add<PositionComponent>(entity, PositionComponent{center});
add<Health>(entity, Health{hp, maxHp}); add<HealthComponent>(entity, HealthComponent{hp, maxHp});
add<Faction>(entity, Faction{isEnemy}); add<FactionComponent>(entity, FactionComponent{isEnemy});
add<StationBody>(entity, StationBody{anchor, footprint, bodyCells}); add<StationBodyComponent>(entity, StationBodyComponent{anchor, footprint, bodyCells});
return entity; return entity;
} }
entt::entity EntityAdmin::spawnScrap(QVector2D position, int amount, Tick despawnAt) entt::entity EntityAdmin::spawnScrap(QVector2D position, int amount, Tick despawnAt)
{ {
entt::entity entity = createEntity(); entt::entity entity = createEntity();
add<Position>(entity, Position{position}); add<PositionComponent>(entity, PositionComponent{position});
add<ScrapData>(entity, ScrapData{amount}); add<ScrapDataComponent>(entity, ScrapDataComponent{amount});
add<DespawnAt>(entity, DespawnAt{despawnAt}); add<DespawnAtComponent>(entity, DespawnAtComponent{despawnAt});
return entity; return entity;
} }
entt::entity EntityAdmin::spawnHqProxy(QVector2D position, float hp, float maxHp) entt::entity EntityAdmin::spawnHqProxy(QVector2D position, float hp, float maxHp)
{ {
entt::entity entity = createEntity(); entt::entity entity = createEntity();
add<Position>(entity, Position{position}); add<PositionComponent>(entity, PositionComponent{position});
add<Health>(entity, Health{hp, maxHp}); add<HealthComponent>(entity, HealthComponent{hp, maxHp});
add<Faction>(entity, Faction{false}); add<FactionComponent>(entity, FactionComponent{false});
add<HqProxy>(entity); add<HqProxyComponent>(entity);
return entity; return entity;
} }

View File

@@ -0,0 +1,17 @@
add_subdirectory(component)
add_subdirectory(system)
SET(HDRS
${HDRS}
PARENT_SCOPE
)
SET(SRCS
${SRCS}
PARENT_SCOPE
)
set(LIB_INCLUDE_PATH
${LIB_INCLUDE_PATH}
PARENT_SCOPE
)

View File

@@ -0,0 +1,30 @@
SET(HDRS
${HDRS}
${CMAKE_CURRENT_SOURCE_DIR}/DespawnAtComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodyComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/FacingComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/FactionComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/HealthComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/HomeReturnBehaviorComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/HqProxyComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/PositionComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/RallyBehaviorComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/RepairBehaviorComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/RepairToolComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/SalvageBehaviorComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/SalvageCargoComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/ScrapDataComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/SensorRangeComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipIdentityComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/StationBodyComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/ThreatResponseBehaviorComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/WeaponComponent.h
PARENT_SCOPE
)
set(LIB_INCLUDE_PATH
${LIB_INCLUDE_PATH}
${CMAKE_CURRENT_SOURCE_DIR}
PARENT_SCOPE
)

View File

@@ -0,0 +1,8 @@
#pragma once
#include "Tick.h"
struct DespawnAtComponent
{
Tick tick;
};

View File

@@ -0,0 +1,6 @@
#pragma once
struct FacingComponent
{
float radians;
};

View File

@@ -0,0 +1,6 @@
#pragma once
struct FactionComponent
{
bool isEnemy;
};

View File

@@ -0,0 +1,7 @@
#pragma once
struct HealthComponent
{
float hp;
float maxHp;
};

View File

@@ -0,0 +1,9 @@
#pragma once
#include <QVector2D>
struct HomeReturnBehaviorComponent
{
float retreatHpFraction;
QVector2D homePos;
};

View File

@@ -0,0 +1,6 @@
#pragma once
struct HqProxyComponent
{
char unused = 0;
};

View File

@@ -5,7 +5,7 @@
// A ship-behavior system writes this each tick before movement runs; the // A ship-behavior system writes this each tick before movement runs; the
// highest-priority write wins. Priority order is fixed globally — see // highest-priority write wins. Priority order is fixed globally — see
// architecture.md "Movement Arbitration". // architecture.md "Movement Arbitration".
struct MovementIntent struct MovementIntentComponent
{ {
int priority; int priority;
QVector2D target; QVector2D target;

View File

@@ -0,0 +1,8 @@
#pragma once
#include <QVector2D>
struct PositionComponent
{
QVector2D value;
};

View File

@@ -0,0 +1,8 @@
#pragma once
#include <QVector2D>
struct RallyBehaviorComponent
{
QVector2D rallyPoint;
};

View File

@@ -0,0 +1,10 @@
#pragma once
#include <optional>
#include "entt/entity/entity.hpp"
struct RepairBehaviorComponent
{
std::optional<entt::entity> currentTarget;
};

View File

@@ -0,0 +1,12 @@
#pragma once
#include <optional>
#include "entt/entity/entity.hpp"
struct RepairToolComponent
{
float ratePerTick;
float range;
std::optional<entt::entity> currentTarget;
};

View File

@@ -0,0 +1,13 @@
#pragma once
#include <optional>
#include <QVector2D>
#include "BuildingId.h"
struct SalvageBehaviorComponent
{
std::optional<QVector2D> scrapTarget;
BuildingId deliveryBay; // kInvalidBuildingId until assigned at a salvage bay
};

View File

@@ -0,0 +1,8 @@
#pragma once
struct SalvageCargoComponent
{
int capacity;
int current;
float collectionRange;
};

View File

@@ -0,0 +1,6 @@
#pragma once
struct ScrapDataComponent
{
int amount;
};

View File

@@ -0,0 +1,6 @@
#pragma once
struct SensorRangeComponent
{
float value;
};

View File

@@ -0,0 +1,9 @@
#pragma once
#include <string>
struct ShipIdentityComponent
{
int level;
std::string schematicId;
};

View File

@@ -0,0 +1,13 @@
#pragma once
#include <vector>
#include <QPoint>
#include <QSize>
struct StationBodyComponent
{
QPoint anchor;
QSize footprint;
std::vector<QPoint> bodyCells;
};

View File

@@ -0,0 +1,10 @@
#pragma once
#include <optional>
#include "entt/entity/entity.hpp"
struct ThreatResponseBehaviorComponent
{
std::optional<entt::entity> currentTarget;
};

View File

@@ -0,0 +1,14 @@
#pragma once
#include <optional>
#include "entt/entity/entity.hpp"
struct WeaponComponent
{
float damage;
float range;
float fireRateHz;
float cooldownTicks;
std::optional<entt::entity> currentTarget;
};

View File

@@ -8,12 +8,24 @@
#include "Building.h" #include "Building.h"
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "BuildingId.h" #include "BuildingId.h"
#include "MovementIntent.h" #include "EntityAdmin.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "HomeReturnBehaviorComponent.h"
#include "HqProxyComponent.h"
#include "MovementIntentComponent.h"
#include "PositionComponent.h"
#include "RallyBehaviorComponent.h"
#include "RepairBehaviorComponent.h"
#include "RepairToolComponent.h"
#include "SalvageBehaviorComponent.h"
#include "SalvageCargoComponent.h"
#include "ScrapSystem.h" #include "ScrapSystem.h"
#include "Ship.h" #include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h"
#include "StationBodyComponent.h"
#include "ThreatResponseBehaviorComponent.h"
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// tickHomeReturnBehavior (priority 4) // tickHomeReturnBehavior (priority 4)
@@ -21,14 +33,15 @@
void AiSystem::tickHomeReturnBehavior(EntityAdmin& admin) void AiSystem::tickHomeReturnBehavior(EntityAdmin& admin)
{ {
admin.forEach<HomeReturnBehavior, Health, MovementIntent>( admin.forEach<HomeReturnBehaviorComponent, HealthComponent, MovementIntentComponent>(
[](entt::entity /*e*/, const HomeReturnBehavior& homeReturnBehavior, const Health& h, MovementIntent& intent) [](entt::entity /*e*/, const HomeReturnBehaviorComponent& homeReturnBehavior,
const HealthComponent& h, MovementIntentComponent& intent)
{ {
if (h.hp / h.maxHp < homeReturnBehavior.retreatHpFraction) if (h.hp / h.maxHp < homeReturnBehavior.retreatHpFraction)
{ {
if (4 > intent.priority) if (4 > intent.priority)
{ {
intent = MovementIntent{4, homeReturnBehavior.homePos}; intent = MovementIntentComponent{4, homeReturnBehavior.homePos};
} }
} }
}); });
@@ -50,27 +63,32 @@ void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSyst
}; };
std::vector<CombatantInfo> combatants; std::vector<CombatantInfo> combatants;
admin.forEach<Position, Faction, ShipIdentity>( admin.forEach<PositionComponent, FactionComponent, ShipIdentityComponent>(
[&combatants](entt::entity e, const Position& pos, const Faction& f, const ShipIdentity& /*si*/) [&combatants](entt::entity e, const PositionComponent& pos,
const FactionComponent& f, const ShipIdentityComponent& /*si*/)
{ {
combatants.push_back({e, pos.value, f.isEnemy, false}); combatants.push_back({e, pos.value, f.isEnemy, false});
}); });
admin.forEach<Position, Faction, StationBody>( admin.forEach<PositionComponent, FactionComponent, StationBodyComponent>(
[&combatants](entt::entity e, const Position& pos, const Faction& f, const StationBody& /*sb*/) [&combatants](entt::entity e, const PositionComponent& pos,
const FactionComponent& f, const StationBodyComponent& /*sb*/)
{ {
combatants.push_back({e, pos.value, f.isEnemy, true}); combatants.push_back({e, pos.value, f.isEnemy, true});
}); });
admin.forEach<Position, Faction, HqProxy>( admin.forEach<PositionComponent, FactionComponent, HqProxyComponent>(
[&combatants](entt::entity e, const Position& pos, const Faction& f, const HqProxy& /*hq*/) [&combatants](entt::entity e, const PositionComponent& pos,
const FactionComponent& f, const HqProxyComponent& /*hq*/)
{ {
combatants.push_back({e, pos.value, f.isEnemy, true}); combatants.push_back({e, pos.value, f.isEnemy, true});
}); });
admin.forEach<ThreatResponseBehavior, Position, Faction, SensorRange, MovementIntent>( admin.forEach<ThreatResponseBehaviorComponent, PositionComponent, FactionComponent,
[&](entt::entity e, ThreatResponseBehavior& threatResponseBehavior, Position& pos, Faction& faction, SensorRangeComponent, MovementIntentComponent>(
SensorRange& sensor, MovementIntent& intent) [&](entt::entity e, ThreatResponseBehaviorComponent& threatResponseBehavior,
PositionComponent& pos, FactionComponent& faction,
SensorRangeComponent& sensor, MovementIntentComponent& intent)
{ {
const float range = sensor.value; const float range = sensor.value;
@@ -79,9 +97,10 @@ void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSyst
if (threatResponseBehavior.currentTarget) if (threatResponseBehavior.currentTarget)
{ {
const entt::entity t = *threatResponseBehavior.currentTarget; const entt::entity t = *threatResponseBehavior.currentTarget;
if (admin.isValid(t) && admin.hasAll<Position>(t)) if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
{ {
const float dist = (admin.get<Position>(t).value - pos.value).length(); const float dist =
(admin.get<PositionComponent>(t).value - pos.value).length();
if (dist <= range) if (dist <= range)
{ {
targetValid = true; targetValid = true;
@@ -122,31 +141,33 @@ void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSyst
{ {
const entt::entity t = *threatResponseBehavior.currentTarget; const entt::entity t = *threatResponseBehavior.currentTarget;
QVector2D dest = pos.value; QVector2D dest = pos.value;
if (admin.isValid(t) && admin.hasAll<Position>(t)) if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
{ {
dest = admin.get<Position>(t).value; dest = admin.get<PositionComponent>(t).value;
} }
if (3 > intent.priority) if (3 > intent.priority)
{ {
intent = MovementIntent{3, dest}; intent = MovementIntentComponent{3, dest};
} }
} }
else else
{ {
if (3 > intent.priority) if (3 > intent.priority)
{ {
if (admin.hasAll<RallyBehavior>(e)) if (admin.hasAll<RallyBehaviorComponent>(e))
{ {
intent = MovementIntent{3, admin.get<RallyBehavior>(e).rallyPoint}; intent = MovementIntentComponent{
3, admin.get<RallyBehaviorComponent>(e).rallyPoint};
} }
else if (!faction.isEnemy) else if (!faction.isEnemy)
{ {
intent = MovementIntent{3, QVector2D(pos.value.x() + 1000.0f, intent = MovementIntentComponent{
pos.value.y())}; 3, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
} }
else else
{ {
intent = MovementIntent{3, QVector2D(-10000.0f, pos.value.y())}; intent = MovementIntentComponent{
3, QVector2D(-10000.0f, pos.value.y())};
} }
} }
} }
@@ -171,16 +192,18 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
}; };
std::vector<RepairableInfo> repairables; std::vector<RepairableInfo> repairables;
admin.forEach<ShipIdentity, Position, Faction, Health>( admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent, HealthComponent>(
[&repairables](entt::entity e, const ShipIdentity& /*si*/, [&repairables](entt::entity e, const ShipIdentityComponent& /*si*/,
const Position& pos, const Faction& f, const Health& h) const PositionComponent& pos, const FactionComponent& f,
const HealthComponent& h)
{ {
repairables.push_back({e, pos.value, f.isEnemy, true, h.hp, h.maxHp}); repairables.push_back({e, pos.value, f.isEnemy, true, h.hp, h.maxHp});
}); });
admin.forEach<StationBody, Position, Faction, Health>( admin.forEach<StationBodyComponent, PositionComponent, FactionComponent, HealthComponent>(
[&repairables](entt::entity e, const StationBody& /*sb*/, [&repairables](entt::entity e, const StationBodyComponent& /*sb*/,
const Position& pos, const Faction& f, const Health& h) const PositionComponent& pos, const FactionComponent& f,
const HealthComponent& h)
{ {
repairables.push_back({e, pos.value, f.isEnemy, false, h.hp, h.maxHp}); repairables.push_back({e, pos.value, f.isEnemy, false, h.hp, h.maxHp});
}); });
@@ -191,9 +214,9 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
QVector2D position; QVector2D position;
}; };
std::vector<EnemyInfo> enemies; std::vector<EnemyInfo> enemies;
admin.forEach<ShipIdentity, Position, Faction>( admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
[&enemies](entt::entity /*e*/, const ShipIdentity& /*si*/, [&enemies](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const Position& pos, const Faction& f) const PositionComponent& pos, const FactionComponent& f)
{ {
if (f.isEnemy) if (f.isEnemy)
{ {
@@ -201,9 +224,11 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
} }
}); });
admin.forEach<RepairBehavior, RepairTool, Position, Faction, SensorRange, MovementIntent>( admin.forEach<RepairBehaviorComponent, RepairToolComponent, PositionComponent,
[&](entt::entity e, RepairBehavior& rb, RepairTool& rt, Position& pos, FactionComponent, SensorRangeComponent, MovementIntentComponent>(
Faction& /*faction*/, SensorRange& sensor, MovementIntent& intent) [&](entt::entity e, RepairBehaviorComponent& rb, RepairToolComponent& rt,
PositionComponent& pos, FactionComponent& /*faction*/,
SensorRangeComponent& sensor, MovementIntentComponent& intent)
{ {
const float repairRange = rt.range; const float repairRange = rt.range;
@@ -221,7 +246,8 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
{ {
if (2 > intent.priority) if (2 > intent.priority)
{ {
intent = MovementIntent{2, QVector2D(-10000.0f, pos.value.y())}; intent = MovementIntentComponent{
2, QVector2D(-10000.0f, pos.value.y())};
} }
return; return;
} }
@@ -231,9 +257,9 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
if (rb.currentTarget) if (rb.currentTarget)
{ {
const entt::entity t = *rb.currentTarget; const entt::entity t = *rb.currentTarget;
if (admin.isValid(t) && admin.hasAll<Health>(t)) if (admin.isValid(t) && admin.hasAll<HealthComponent>(t))
{ {
const Health& th = admin.get<Health>(t); const HealthComponent& th = admin.get<HealthComponent>(t);
if (th.hp > 0.0f && th.hp < th.maxHp) if (th.hp > 0.0f && th.hp < th.maxHp)
{ {
targetValid = true; targetValid = true;
@@ -264,27 +290,25 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
{ {
if (2 > intent.priority) if (2 > intent.priority)
{ {
intent = MovementIntent{2, QVector2D(pos.value.x() + 1000.0f, intent = MovementIntentComponent{
pos.value.y())}; 2, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
} }
return; return;
} }
const entt::entity target = *rb.currentTarget; const entt::entity target = *rb.currentTarget;
QVector2D targetPos = pos.value; QVector2D targetPos = pos.value;
bool isShipTarget = false; if (admin.isValid(target) && admin.hasAll<PositionComponent>(target))
if (admin.isValid(target) && admin.hasAll<Position>(target))
{ {
targetPos = admin.get<Position>(target).value; targetPos = admin.get<PositionComponent>(target).value;
isShipTarget = admin.hasAll<ShipIdentity>(target);
} }
const float distToTarget = (targetPos - pos.value).length(); const float distToTarget = (targetPos - pos.value).length();
if (distToTarget <= repairRange) if (distToTarget <= repairRange)
{ {
if (admin.isValid(target) && admin.hasAll<Health>(target)) if (admin.isValid(target) && admin.hasAll<HealthComponent>(target))
{ {
Health& targetHealth = admin.get<Health>(target); HealthComponent& targetHealth = admin.get<HealthComponent>(target);
targetHealth.hp = std::min(targetHealth.hp + rt.ratePerTick, targetHealth.hp = std::min(targetHealth.hp + rt.ratePerTick,
targetHealth.maxHp); targetHealth.maxHp);
} }
@@ -292,7 +316,7 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
if (2 > intent.priority) if (2 > intent.priority)
{ {
intent = MovementIntent{2, targetPos}; intent = MovementIntentComponent{2, targetPos};
} }
}); });
} }
@@ -310,9 +334,9 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
QVector2D position; QVector2D position;
}; };
std::vector<EnemyShipPos> enemyShips; std::vector<EnemyShipPos> enemyShips;
admin.forEach<ShipIdentity, Position, Faction>( admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
[&enemyShips](entt::entity /*e*/, const ShipIdentity& /*si*/, [&enemyShips](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
const Position& pos, const Faction& f) const PositionComponent& pos, const FactionComponent& f)
{ {
if (f.isEnemy) if (f.isEnemy)
{ {
@@ -322,9 +346,11 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo(); const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
admin.forEach<SalvageBehavior, SalvageCargo, Position, SensorRange, MovementIntent>( admin.forEach<SalvageBehaviorComponent, SalvageCargoComponent, PositionComponent,
[&](entt::entity /*e*/, SalvageBehavior& salvageBehavior, SalvageCargo& cargo, SensorRangeComponent, MovementIntentComponent>(
Position& pos, SensorRange& sensor, MovementIntent& intent) [&](entt::entity /*e*/, SalvageBehaviorComponent& salvageBehavior,
SalvageCargoComponent& cargo, PositionComponent& pos,
SensorRangeComponent& sensor, MovementIntentComponent& intent)
{ {
const float collectRange = cargo.collectionRange; const float collectRange = cargo.collectionRange;
@@ -358,7 +384,7 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
{ {
if (1 > intent.priority) if (1 > intent.priority)
{ {
intent = MovementIntent{1, bayPos}; intent = MovementIntentComponent{1, bayPos};
} }
if (bayId != kInvalidBuildingId if (bayId != kInvalidBuildingId
&& (pos.value - bayPos).length() <= 1.0f) && (pos.value - bayPos).length() <= 1.0f)
@@ -381,7 +407,8 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
{ {
if (1 > intent.priority) if (1 > intent.priority)
{ {
intent = MovementIntent{1, QVector2D(-10000.0f, pos.value.y())}; intent = MovementIntentComponent{
1, QVector2D(-10000.0f, pos.value.y())};
} }
retreating = true; retreating = true;
break; break;
@@ -409,7 +436,7 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
{ {
if (1 > intent.priority) if (1 > intent.priority)
{ {
intent = MovementIntent{1, *salvageBehavior.scrapTarget}; intent = MovementIntentComponent{1, *salvageBehavior.scrapTarget};
} }
} }
else else
@@ -430,15 +457,15 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
salvageBehavior.scrapTarget = bestPos; salvageBehavior.scrapTarget = bestPos;
if (1 > intent.priority) if (1 > intent.priority)
{ {
intent = MovementIntent{1, *bestPos}; intent = MovementIntentComponent{1, *bestPos};
} }
} }
else else
{ {
if (1 > intent.priority) if (1 > intent.priority)
{ {
intent = MovementIntent{1, QVector2D(pos.value.x() + 1000.0f, intent = MovementIntentComponent{
pos.value.y())}; 1, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
} }
} }
} }

View File

@@ -0,0 +1,27 @@
SET(HDRS
${HDRS}
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.h
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
PARENT_SCOPE
)
SET(SRCS
${SRCS}
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
PARENT_SCOPE
)
set(LIB_INCLUDE_PATH
${LIB_INCLUDE_PATH}
${CMAKE_CURRENT_SOURCE_DIR}
PARENT_SCOPE
)

View File

@@ -0,0 +1,142 @@
#include "CombatSystem.h"
#include "EntityAdmin.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "PositionComponent.h"
#include "ShipIdentityComponent.h"
#include "ThreatResponseBehaviorComponent.h"
#include "WeaponComponent.h"
static constexpr Tick kWeaponImpactDelayTicks = 5;
CombatSystem::CombatSystem(const GameConfig& config)
: m_config(config)
{
}
void CombatSystem::tick(Tick currentTick,
EntityAdmin& admin,
BuildingSystem& /*buildings*/,
std::vector<FireEvent>& outFireEvents)
{
// Ship weapons.
admin.forEach<WeaponComponent, ThreatResponseBehaviorComponent,
PositionComponent, FactionComponent>(
[&](entt::entity e, WeaponComponent& weapon,
ThreatResponseBehaviorComponent& threatResponseBehavior,
PositionComponent& pos, FactionComponent& faction)
{
weapon.currentTarget = threatResponseBehavior.currentTarget;
resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
});
// Station weapons.
admin.forEach<WeaponComponent, PositionComponent, FactionComponent>(
[&](entt::entity e, WeaponComponent& weapon, PositionComponent& pos,
FactionComponent& faction)
{
resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
});
}
void CombatSystem::resolveWeapon(
entt::entity shipEntity,
WeaponComponent& weapon,
const PositionComponent& ownPos,
const FactionComponent& ownFaction,
Tick currentTick,
EntityAdmin& admin,
std::vector<FireEvent>& out)
{
if (weapon.cooldownTicks > 0.0f)
{
weapon.cooldownTicks -= 1.0f;
}
if (weapon.cooldownTicks > 0.0f)
{
return;
}
// Validate or clear existing target.
if (weapon.currentTarget)
{
const entt::entity t = *weapon.currentTarget;
if (!admin.isValid(t) || !admin.hasAll<PositionComponent>(t))
{
weapon.currentTarget = std::nullopt;
}
else
{
const float distanceSquared =
(ownPos.value - admin.get<PositionComponent>(t).value).lengthSquared();
if (distanceSquared > weapon.range * weapon.range)
{
weapon.currentTarget = std::nullopt;
}
}
}
// Acquire a new target if needed (nearest opposing-faction ship).
if (!weapon.currentTarget)
{
float bestDistanceSquared = weapon.range * weapon.range;
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
[&](entt::entity candidate, const ShipIdentityComponent& /*si*/,
const PositionComponent& candidatePos,
const FactionComponent& candidateFaction)
{
const bool isValidTarget = ownFaction.isEnemy
? !candidateFaction.isEnemy
: candidateFaction.isEnemy;
if (!isValidTarget)
{
return;
}
const float distanceSquared =
(candidatePos.value - ownPos.value).lengthSquared();
if (distanceSquared < bestDistanceSquared)
{
bestDistanceSquared = distanceSquared;
weapon.currentTarget = candidate;
}
});
}
if (!weapon.currentTarget)
{
return;
}
const entt::entity targetEntity = *weapon.currentTarget;
m_pendingDamage.push_back({targetEntity, weapon.damage,
currentTick + kWeaponImpactDelayTicks});
FireEvent evt;
evt.shooter = shipEntity;
evt.target = targetEntity;
evt.emittedAt = currentTick;
out.push_back(evt);
weapon.cooldownTicks = static_cast<float>(kTickRateHz) / weapon.fireRateHz;
}
void CombatSystem::applyPendingDamage(Tick currentTick, EntityAdmin& admin)
{
std::vector<PendingDamage>::iterator it = m_pendingDamage.begin();
while (it != m_pendingDamage.end())
{
if (it->appliesAt <= currentTick)
{
if (admin.isValid(it->target) && admin.hasAll<HealthComponent>(it->target))
{
admin.get<HealthComponent>(it->target).hp -= it->amount;
}
it = m_pendingDamage.erase(it);
}
else
{
++it;
}
}
}

View File

@@ -6,11 +6,12 @@
#include <QVector2D> #include <QVector2D>
#include "Building.h" #include "Building.h"
#include "EcsComponents.h" #include "FactionComponent.h"
#include "FireEvent.h" #include "FireEvent.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "Ship.h" #include "PositionComponent.h"
#include "Tick.h" #include "Tick.h"
#include "WeaponComponent.h"
#include "entt/entity/entity.hpp" #include "entt/entity/entity.hpp"
@@ -40,14 +41,13 @@ private:
std::vector<PendingDamage> m_pendingDamage; std::vector<PendingDamage> m_pendingDamage;
void resolveWeapon( void resolveWeapon(
entt::entity shipEntity, entt::entity shipEntity,
Weapon& weapon, WeaponComponent& weapon,
const Position& ownPos, const PositionComponent& ownPos,
const Faction& ownFaction, const FactionComponent& ownFaction,
Tick currentTick, Tick currentTick,
EntityAdmin& admin, EntityAdmin& admin,
std::vector<FireEvent>& out); std::vector<FireEvent>& out);
const GameConfig& m_config; const GameConfig& m_config;
}; };

View File

@@ -6,8 +6,9 @@
#include <QVector2D> #include <QVector2D>
#include "DynamicBodyComponent.h" #include "DynamicBodyComponent.h"
#include "EcsComponents.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "FacingComponent.h"
#include "PositionComponent.h"
static float wrapAngle(float a) static float wrapAngle(float a)
{ {
@@ -20,8 +21,9 @@ static float wrapAngle(float a)
void DynamicBodySystem::tick(EntityAdmin& admin) void DynamicBodySystem::tick(EntityAdmin& admin)
{ {
admin.forEach<Position, Facing, DynamicBodyComponent>( admin.forEach<PositionComponent, FacingComponent, DynamicBodyComponent>(
[](entt::entity /*e*/, Position& pos, Facing& facing, DynamicBodyComponent& body) [](entt::entity /*e*/, PositionComponent& pos, FacingComponent& facing,
DynamicBodyComponent& body)
{ {
// Integrate angular velocity, clamp to max rotation speed, then advance facing. // Integrate angular velocity, clamp to max rotation speed, then advance facing.
body.angularVelocity += body.angularAcceleration; body.angularVelocity += body.angularAcceleration;

View File

@@ -6,9 +6,10 @@
#include <QVector2D> #include <QVector2D>
#include "DynamicBodyComponent.h" #include "DynamicBodyComponent.h"
#include "EcsComponents.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "MovementIntent.h" #include "FacingComponent.h"
#include "MovementIntentComponent.h"
#include "PositionComponent.h"
static float wrapAngle(float a) static float wrapAngle(float a)
{ {
@@ -21,9 +22,10 @@ static float wrapAngle(float a)
void MovementIntentSystem::tick(EntityAdmin& admin) void MovementIntentSystem::tick(EntityAdmin& admin)
{ {
admin.forEach<Position, Facing, DynamicBodyComponent, MovementIntent>( admin.forEach<PositionComponent, FacingComponent, DynamicBodyComponent,
[](entt::entity /*e*/, const Position& pos, const Facing& facing, MovementIntentComponent>(
DynamicBodyComponent& body, const MovementIntent& intent) [](entt::entity /*e*/, const PositionComponent& pos, const FacingComponent& facing,
DynamicBodyComponent& body, const MovementIntentComponent& intent)
{ {
if (intent.priority == 0) if (intent.priority == 0)
{ {
@@ -36,7 +38,8 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
const float angBraking = std::min(std::abs(body.angularVelocity), const float angBraking = std::min(std::abs(body.angularVelocity),
body.angularAccelerationPerTick); body.angularAccelerationPerTick);
body.angularAcceleration = (body.angularVelocity >= 0.0f) ? -angBraking : angBraking; body.angularAcceleration =
(body.angularVelocity >= 0.0f) ? -angBraking : angBraking;
return; return;
} }
@@ -58,7 +61,8 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
const float angleDiff = wrapAngle(desiredAngle - facing.radians); const float angleDiff = wrapAngle(desiredAngle - facing.radians);
const float rotDelta = std::max(-body.angularAccelerationPerTick, const float rotDelta = std::max(-body.angularAccelerationPerTick,
std::min(angleDiff, body.angularAccelerationPerTick)); std::min(angleDiff,
body.angularAccelerationPerTick));
float newAngVel = body.angularVelocity + rotDelta; float newAngVel = body.angularVelocity + rotDelta;
@@ -81,7 +85,8 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
// pointing when DynamicBodySystem applies the forces. // pointing when DynamicBodySystem applies the forces.
const float projectedRadians = wrapAngle(facing.radians + newAngVel); const float projectedRadians = wrapAngle(facing.radians + newAngVel);
const QVector2D facingVec(std::cos(projectedRadians), std::sin(projectedRadians)); const QVector2D facingVec(std::cos(projectedRadians),
std::sin(projectedRadians));
const float manAccel = body.maneuveringAccelerationPerTick; const float manAccel = body.maneuveringAccelerationPerTick;
const float stoppingDist = (body.maxSpeedPerTick * body.maxSpeedPerTick) const float stoppingDist = (body.maxSpeedPerTick * body.maxSpeedPerTick)
@@ -95,7 +100,8 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
const float mainAligned = std::max(0.0f, const float mainAligned = std::max(0.0f,
QVector2D::dotProduct(velError, facingVec)); QVector2D::dotProduct(velError, facingVec));
const float mainApplied = std::min(mainAligned, body.mainAccelerationPerTick); const float mainApplied = std::min(mainAligned,
body.mainAccelerationPerTick);
const QVector2D mainDelta = facingVec * mainApplied; const QVector2D mainDelta = facingVec * mainApplied;
const QVector2D remaining = velError - mainDelta; const QVector2D remaining = velError - mainDelta;

View File

@@ -1,6 +1,9 @@
#include "ScrapSystem.h" #include "ScrapSystem.h"
#include "DespawnAtComponent.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "PositionComponent.h"
#include "ScrapDataComponent.h"
ScrapSystem::ScrapSystem(EntityAdmin& admin) ScrapSystem::ScrapSystem(EntityAdmin& admin)
: m_admin(admin) : m_admin(admin)
@@ -15,8 +18,8 @@ entt::entity ScrapSystem::spawn(QVector2D position, int amount, Tick despawnAt)
void ScrapSystem::tickDespawn(Tick currentTick) void ScrapSystem::tickDespawn(Tick currentTick)
{ {
std::vector<entt::entity> expired; std::vector<entt::entity> expired;
m_admin.forEach<DespawnAt>( m_admin.forEach<DespawnAtComponent>(
[&expired, currentTick](entt::entity e, DespawnAt& d) [&expired, currentTick](entt::entity e, DespawnAtComponent& d)
{ {
if (d.tick <= currentTick) if (d.tick <= currentTick)
{ {
@@ -32,11 +35,11 @@ void ScrapSystem::tickDespawn(Tick currentTick)
std::optional<int> ScrapSystem::consume(entt::entity entity) std::optional<int> ScrapSystem::consume(entt::entity entity)
{ {
if (!m_admin.isValid(entity) || !m_admin.hasAll<ScrapData>(entity)) if (!m_admin.isValid(entity) || !m_admin.hasAll<ScrapDataComponent>(entity))
{ {
return std::nullopt; return std::nullopt;
} }
int amount = m_admin.get<ScrapData>(entity).amount; int amount = m_admin.get<ScrapDataComponent>(entity).amount;
m_admin.destroy(entity); m_admin.destroy(entity);
return amount; return amount;
} }
@@ -44,10 +47,10 @@ std::optional<int> ScrapSystem::consume(entt::entity entity)
std::vector<ScrapInfo> ScrapSystem::allScrapInfo() const std::vector<ScrapInfo> ScrapSystem::allScrapInfo() const
{ {
std::vector<ScrapInfo> result; std::vector<ScrapInfo> result;
m_admin.forEach<ScrapData>( m_admin.forEach<ScrapDataComponent>(
[&result, this](entt::entity e, const ScrapData& /*sd*/) [&result, this](entt::entity e, const ScrapDataComponent& /*sd*/)
{ {
result.push_back(ScrapInfo{e, m_admin.get<Position>(e).value}); result.push_back(ScrapInfo{e, m_admin.get<PositionComponent>(e).value});
}); });
return result; return result;
} }

View File

@@ -5,7 +5,6 @@
#include <QVector2D> #include <QVector2D>
#include "EcsComponents.h"
#include "Tick.h" #include "Tick.h"
#include "entt/entity/entity.hpp" #include "entt/entity/entity.hpp"

View File

@@ -5,10 +5,20 @@
#include <utility> #include <utility>
#include "DynamicBodyComponent.h" #include "DynamicBodyComponent.h"
#include "EcsComponents.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "ModulesConfig.h" #include "ModulesConfig.h"
#include "MovementIntentComponent.h"
#include "RallyBehaviorComponent.h"
#include "RepairBehaviorComponent.h"
#include "RepairToolComponent.h"
#include "SalvageBehaviorComponent.h"
#include "SalvageCargoComponent.h"
#include "SensorRangeComponent.h"
#include "Tick.h" #include "Tick.h"
#include "ThreatResponseBehaviorComponent.h"
#include "WeaponComponent.h"
ShipSystem::ShipSystem(const GameConfig& config, EntityAdmin& admin) ShipSystem::ShipSystem(const GameConfig& config, EntityAdmin& admin)
: m_config(config) : m_config(config)
@@ -40,8 +50,8 @@ const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
return nullptr; return nullptr;
} }
entt::entity ShipSystem::spawn(const std::string& schematicId, int level, QVector2D position, entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
bool isEnemy, QVector2D position, bool isEnemy,
const std::optional<ShipLayoutConfig>& layout) const std::optional<ShipLayoutConfig>& layout)
{ {
const ShipDef* def = findShipDef(schematicId); const ShipDef* def = findShipDef(schematicId);
@@ -52,12 +62,22 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, QVecto
float hp = static_cast<float>(def->health.hpFormula.evaluate(x)); float hp = static_cast<float>(def->health.hpFormula.evaluate(x));
float maxHp = hp; float maxHp = hp;
float maxSpeedPerTick = static_cast<float>(def->movement.speedFormula.evaluate(x)) / tickRate; float maxSpeedPerTick = static_cast<float>(def->movement.speedFormula.evaluate(x))
float mainAccelPerTick = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x)) / tickRate; / tickRate;
float maneuveringAccelPerTick = static_cast<float>(def->movement.maneuveringAccelerationFormula.evaluate(x)) / tickRate; float mainAccelPerTick = static_cast<float>(
float angularAccelPerTick = static_cast<float>(def->movement.angularAccelerationFormula.evaluate(x)) / tickRate; def->movement.mainAccelerationFormula.evaluate(x))
float maxRotationSpeedPerTick = static_cast<float>(def->movement.maxRotationSpeedFormula.evaluate(x)) / tickRate; / tickRate;
float sensorRange = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x)); float maneuveringAccelPerTick = static_cast<float>(
def->movement.maneuveringAccelerationFormula.evaluate(x))
/ tickRate;
float angularAccelPerTick = static_cast<float>(
def->movement.angularAccelerationFormula.evaluate(x))
/ tickRate;
float maxRotationSpeedPerTick = static_cast<float>(
def->movement.maxRotationSpeedFormula.evaluate(x))
/ tickRate;
float sensorRange = static_cast<float>(
def->sensor.sensorRangeFormula.evaluate(x));
entt::entity entity = m_admin.spawnShip( entt::entity entity = m_admin.spawnShip(
position, hp, maxHp, position, hp, maxHp,
@@ -68,45 +88,47 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, QVecto
// Optional components based on ship role. // Optional components based on ship role.
if (def->combat) if (def->combat)
{ {
Weapon w; WeaponComponent w;
w.damage = static_cast<float>(def->combat->damageFormula.evaluate(x)); w.damage = static_cast<float>(def->combat->damageFormula.evaluate(x));
w.range = static_cast<float>(def->combat->attackRangeFormula.evaluate(x)); w.range = static_cast<float>(def->combat->attackRangeFormula.evaluate(x));
w.fireRateHz = static_cast<float>(def->combat->attackRateFormula.evaluate(x)); w.fireRateHz = static_cast<float>(def->combat->attackRateFormula.evaluate(x));
w.cooldownTicks = 0.0f; w.cooldownTicks = 0.0f;
w.currentTarget = std::nullopt; w.currentTarget = std::nullopt;
m_admin.addComponent<Weapon>(entity, w); m_admin.addComponent<WeaponComponent>(entity, w);
m_admin.addComponent<ThreatResponseBehavior>(entity, ThreatResponseBehavior{}); m_admin.addComponent<ThreatResponseBehaviorComponent>(
entity, ThreatResponseBehaviorComponent{});
if (!isEnemy) if (!isEnemy)
{ {
m_admin.addComponent<RallyBehavior>(entity, RallyBehavior{m_rallyPoint}); m_admin.addComponent<RallyBehaviorComponent>(
entity, RallyBehaviorComponent{m_rallyPoint});
} }
} }
if (def->salvage) if (def->salvage)
{ {
SalvageCargo cargo; SalvageCargoComponent cargo;
cargo.capacity = def->salvage->cargoCapacity; cargo.capacity = def->salvage->cargoCapacity;
cargo.current = 0; cargo.current = 0;
cargo.collectionRange = static_cast<float>(def->salvage->collectionRange); cargo.collectionRange = static_cast<float>(def->salvage->collectionRange);
m_admin.addComponent<SalvageCargo>(entity, cargo); m_admin.addComponent<SalvageCargoComponent>(entity, cargo);
SalvageBehavior salvageBehavior; SalvageBehaviorComponent salvageBehavior;
salvageBehavior.scrapTarget = std::nullopt; salvageBehavior.scrapTarget = std::nullopt;
salvageBehavior.deliveryBay = kInvalidBuildingId; salvageBehavior.deliveryBay = kInvalidBuildingId;
m_admin.addComponent<SalvageBehavior>(entity, salvageBehavior); m_admin.addComponent<SalvageBehaviorComponent>(entity, salvageBehavior);
} }
if (def->repair) if (def->repair)
{ {
RepairTool rt; RepairToolComponent rt;
rt.ratePerTick = static_cast<float>(def->repair->repairRateFormula.evaluate(x)); rt.ratePerTick = static_cast<float>(def->repair->repairRateFormula.evaluate(x));
rt.range = static_cast<float>(def->repair->repairRangeFormula.evaluate(x)); rt.range = static_cast<float>(def->repair->repairRangeFormula.evaluate(x));
rt.currentTarget = std::nullopt; rt.currentTarget = std::nullopt;
m_admin.addComponent<RepairTool>(entity, rt); m_admin.addComponent<RepairToolComponent>(entity, rt);
m_admin.addComponent<RepairBehavior>(entity, RepairBehavior{}); m_admin.addComponent<RepairBehaviorComponent>(entity, RepairBehaviorComponent{});
} }
// Apply module stat modifiers (REQ-MOD-STAT-CALC). // Apply module stat modifiers (REQ-MOD-STAT-CALC).
@@ -142,13 +164,14 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, QVecto
if (it != mods.end()) if (it != mods.end())
{ {
stat = static_cast<float>( stat = static_cast<float>(
static_cast<double>(stat) * (1.0 + it->second.first) + it->second.second); static_cast<double>(stat) * (1.0 + it->second.first)
+ it->second.second);
} }
}; };
Health& health = m_admin.get<Health>(entity); HealthComponent& health = m_admin.get<HealthComponent>(entity);
DynamicBodyComponent& dynamics = m_admin.get<DynamicBodyComponent>(entity); DynamicBodyComponent& dynamics = m_admin.get<DynamicBodyComponent>(entity);
SensorRange& sensor = m_admin.get<SensorRange>(entity); SensorRangeComponent& sensor = m_admin.get<SensorRangeComponent>(entity);
applyMod(health.maxHp, "hp"); applyMod(health.maxHp, "hp");
health.hp = health.maxHp; health.hp = health.maxHp;
@@ -159,16 +182,16 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, QVecto
applyMod(dynamics.maxRotationSpeedPerTick, "max_rotation_speed"); applyMod(dynamics.maxRotationSpeedPerTick, "max_rotation_speed");
applyMod(sensor.value, "sensor_range"); applyMod(sensor.value, "sensor_range");
if (m_admin.hasAll<Weapon>(entity)) if (m_admin.hasAll<WeaponComponent>(entity))
{ {
Weapon& weapon = m_admin.get<Weapon>(entity); WeaponComponent& weapon = m_admin.get<WeaponComponent>(entity);
applyMod(weapon.damage, "damage"); applyMod(weapon.damage, "damage");
applyMod(weapon.range, "attack_range"); applyMod(weapon.range, "attack_range");
applyMod(weapon.fireRateHz, "attack_rate"); applyMod(weapon.fireRateHz, "attack_rate");
} }
if (m_admin.hasAll<RepairTool>(entity)) if (m_admin.hasAll<RepairToolComponent>(entity))
{ {
RepairTool& repairTool = m_admin.get<RepairTool>(entity); RepairToolComponent& repairTool = m_admin.get<RepairToolComponent>(entity);
applyMod(repairTool.ratePerTick, "repair_rate"); applyMod(repairTool.ratePerTick, "repair_rate");
applyMod(repairTool.range, "repair_range"); applyMod(repairTool.range, "repair_range");
} }
@@ -184,10 +207,11 @@ void ShipSystem::despawn(entt::entity entity)
void ShipSystem::clearMovementIntents() void ShipSystem::clearMovementIntents()
{ {
m_admin.forEach<MovementIntent>([](entt::entity /*e*/, MovementIntent& i) m_admin.forEach<MovementIntentComponent>(
{ [](entt::entity /*e*/, MovementIntentComponent& i)
i = MovementIntent{0, QVector2D(0.0f, 0.0f)}; {
}); i = MovementIntentComponent{0, QVector2D(0.0f, 0.0f)};
});
} }
void ShipSystem::setRallyPoint(QVector2D point) void ShipSystem::setRallyPoint(QVector2D point)
@@ -198,8 +222,9 @@ void ShipSystem::setRallyPoint(QVector2D point)
void ShipSystem::triggerRallyDeparture() void ShipSystem::triggerRallyDeparture()
{ {
std::vector<entt::entity> toRemove; std::vector<entt::entity> toRemove;
m_admin.forEach<RallyBehavior, Faction>( m_admin.forEach<RallyBehaviorComponent, FactionComponent>(
[&toRemove](entt::entity e, const RallyBehavior& /*rb*/, const Faction& f) [&toRemove](entt::entity e, const RallyBehaviorComponent& /*rb*/,
const FactionComponent& f)
{ {
if (!f.isEnemy) if (!f.isEnemy)
{ {
@@ -208,6 +233,6 @@ void ShipSystem::triggerRallyDeparture()
}); });
for (entt::entity e : toRemove) for (entt::entity e : toRemove)
{ {
m_admin.removeComponent<RallyBehavior>(e); m_admin.removeComponent<RallyBehaviorComponent>(e);
} }
} }

View File

@@ -6,7 +6,6 @@
#include <QVector2D> #include <QVector2D>
#include "GameConfig.h" #include "GameConfig.h"
#include "Ship.h"
#include "ShipLayout.h" #include "ShipLayout.h"
#include "entt/entity/entity.hpp" #include "entt/entity/entity.hpp"

View File

@@ -5,16 +5,9 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/Building.h ${CMAKE_CURRENT_SOURCE_DIR}/Building.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/Ship.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.h
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h ${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
PARENT_SCOPE PARENT_SCOPE
) )
@@ -24,13 +17,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp
PARENT_SCOPE PARENT_SCOPE
) )

View File

@@ -1,131 +0,0 @@
#include "CombatSystem.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
static constexpr Tick kWeaponImpactDelayTicks = 5;
CombatSystem::CombatSystem(const GameConfig& config)
: m_config(config)
{
}
void CombatSystem::tick(Tick currentTick,
EntityAdmin& admin,
BuildingSystem& /*buildings*/,
std::vector<FireEvent>& outFireEvents)
{
// Ship weapons.
admin.forEach<Weapon, ThreatResponseBehavior, Position, Faction>(
[&](entt::entity e, Weapon& weapon, ThreatResponseBehavior& threatResponseBehavior, Position& pos, Faction& faction)
{
weapon.currentTarget = threatResponseBehavior.currentTarget;
resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
});
// Station weapons.
admin.forEach<Weapon, Position, Faction>(
[&](entt::entity e, Weapon& weapon, Position& pos, Faction& faction)
{
resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
});
}
void CombatSystem::resolveWeapon(
entt::entity shipEntity,
Weapon& weapon,
const Position& ownPos,
const Faction& ownFaction,
Tick currentTick,
EntityAdmin& admin,
std::vector<FireEvent>& out)
{
if (weapon.cooldownTicks > 0.0f)
{
weapon.cooldownTicks -= 1.0f;
}
if (weapon.cooldownTicks > 0.0f)
{
return;
}
// Validate or clear existing target.
if (weapon.currentTarget)
{
const entt::entity t = *weapon.currentTarget;
if (!admin.isValid(t) || !admin.hasAll<Position>(t))
{
weapon.currentTarget = std::nullopt;
}
else
{
const float distanceSquared = (ownPos.value - admin.get<Position>(t).value).lengthSquared();
if (distanceSquared > weapon.range * weapon.range)
{
weapon.currentTarget = std::nullopt;
}
}
}
// Acquire a new target if needed (nearest opposing-faction ship).
if (!weapon.currentTarget)
{
float bestDistanceSquared = weapon.range * weapon.range;
admin.forEach<ShipIdentity, Position, Faction>(
[&](entt::entity candidate, const ShipIdentity& /*si*/,
const Position& candidatePos, const Faction& candidateFaction)
{
const bool isValidTarget = ownFaction.isEnemy
? !candidateFaction.isEnemy
: candidateFaction.isEnemy;
if (!isValidTarget)
{
return;
}
const float distanceSquared = (candidatePos.value - ownPos.value).lengthSquared();
if (distanceSquared < bestDistanceSquared)
{
bestDistanceSquared = distanceSquared;
weapon.currentTarget = candidate;
}
});
}
if (!weapon.currentTarget)
{
return;
}
const entt::entity targetEntity = *weapon.currentTarget;
m_pendingDamage.push_back({targetEntity, weapon.damage,
currentTick + kWeaponImpactDelayTicks});
FireEvent evt;
evt.shooter = shipEntity;
evt.target = targetEntity;
evt.emittedAt = currentTick;
out.push_back(evt);
weapon.cooldownTicks = static_cast<float>(kTickRateHz) / weapon.fireRateHz;
}
void CombatSystem::applyPendingDamage(Tick currentTick, EntityAdmin& admin)
{
std::vector<PendingDamage>::iterator it = m_pendingDamage.begin();
while (it != m_pendingDamage.end())
{
if (it->appliesAt <= currentTick)
{
if (admin.isValid(it->target) && admin.hasAll<Health>(it->target))
{
admin.get<Health>(it->target).hp -= it->amount;
}
it = m_pendingDamage.erase(it);
}
else
{
++it;
}
}
}

View File

@@ -1,69 +0,0 @@
#pragma once
#include <optional>
#include <string>
#include <QVector2D>
#include "BuildingId.h"
#include "MovementIntent.h"
#include "entt/entity/entity.hpp"
// ---------------------------------------------------------------------------
// Hardware components — derived from config at spawn, stored on ship
// ---------------------------------------------------------------------------
struct Weapon
{
float damage;
float range;
float fireRateHz;
float cooldownTicks;
std::optional<entt::entity> currentTarget;
};
struct SalvageCargo
{
int capacity;
int current;
float collectionRange;
};
struct RepairTool
{
float ratePerTick;
float range;
std::optional<entt::entity> currentTarget;
};
// ---------------------------------------------------------------------------
// Behavior components — AI state consumed by step-6 behavior systems
// ---------------------------------------------------------------------------
struct ThreatResponseBehavior
{
std::optional<entt::entity> currentTarget;
};
struct SalvageBehavior
{
std::optional<QVector2D> scrapTarget;
BuildingId deliveryBay; // kInvalidBuildingId until assigned at a salvage bay
};
struct RepairBehavior
{
std::optional<entt::entity> currentTarget;
};
struct HomeReturnBehavior
{
float retreatHpFraction;
QVector2D homePos;
};
struct RallyBehavior
{
QVector2D rallyPoint;
};

View File

@@ -4,14 +4,19 @@
#include "AiSystem.h" #include "AiSystem.h"
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "EcsComponents.h"
#include "CombatSystem.h" #include "CombatSystem.h"
#include "DynamicBodySystem.h" #include "DynamicBodySystem.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "MovementIntentSystem.h" #include "MovementIntentSystem.h"
#include "PositionComponent.h"
#include "ScrapSystem.h" #include "ScrapSystem.h"
#include "ShipIdentityComponent.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "StationBodyComponent.h"
#include "SurfaceMask.h" #include "SurfaceMask.h"
#include "WaveSystem.h" #include "WaveSystem.h"
#include "WeaponComponent.h"
Simulation::Simulation(GameConfig config, unsigned int seed) Simulation::Simulation(GameConfig config, unsigned int seed)
: m_config(std::move(config)) : m_config(std::move(config))
@@ -228,7 +233,7 @@ void Simulation::placeInitialStructures()
const float psHp = static_cast<float>( const float psHp = static_cast<float>(
m_config.stations.playerStation.hpFormula.evaluate(psLevel)); m_config.stations.playerStation.hpFormula.evaluate(psLevel));
Weapon psWeapon; WeaponComponent psWeapon;
psWeapon.damage = static_cast<float>( psWeapon.damage = static_cast<float>(
m_config.stations.playerStation.damageFormula.evaluate(psLevel)); m_config.stations.playerStation.damageFormula.evaluate(psLevel));
psWeapon.range = static_cast<float>( psWeapon.range = static_cast<float>(
@@ -250,7 +255,7 @@ void Simulation::placeInitialStructures()
} }
m_playerStation1Entity = m_admin.spawnStation( m_playerStation1Entity = m_admin.spawnStation(
anchor, psParsed.footprint, absCells, psHp, psHp, false); anchor, psParsed.footprint, absCells, psHp, psHp, false);
m_admin.addComponent<Weapon>(m_playerStation1Entity, psWeapon); m_admin.addComponent<WeaponComponent>(m_playerStation1Entity, psWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
} }
{ {
@@ -262,7 +267,7 @@ void Simulation::placeInitialStructures()
} }
m_playerStation2Entity = m_admin.spawnStation( m_playerStation2Entity = m_admin.spawnStation(
anchor, psParsed.footprint, absCells, psHp, psHp, false); anchor, psParsed.footprint, absCells, psHp, psHp, false);
m_admin.addComponent<Weapon>(m_playerStation2Entity, psWeapon); m_admin.addComponent<WeaponComponent>(m_playerStation2Entity, psWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
} }
@@ -289,7 +294,7 @@ void Simulation::placeEnemyStationSet(int generation)
const float esHp = static_cast<float>( const float esHp = static_cast<float>(
m_config.stations.enemyStation.hpFormula.evaluate(genD)); m_config.stations.enemyStation.hpFormula.evaluate(genD));
Weapon esWeapon; WeaponComponent esWeapon;
esWeapon.damage = static_cast<float>( esWeapon.damage = static_cast<float>(
m_config.stations.enemyStation.damageFormula.evaluate(genD)); m_config.stations.enemyStation.damageFormula.evaluate(genD));
esWeapon.range = static_cast<float>( esWeapon.range = static_cast<float>(
@@ -311,7 +316,7 @@ void Simulation::placeEnemyStationSet(int generation)
} }
m_currentEnemyStationEntities[0] = m_admin.spawnStation( m_currentEnemyStationEntities[0] = m_admin.spawnStation(
anchor, esParsed.footprint, absCells, esHp, esHp, true); anchor, esParsed.footprint, absCells, esHp, esHp, true);
m_admin.addComponent<Weapon>(m_currentEnemyStationEntities[0], esWeapon); m_admin.addComponent<WeaponComponent>(m_currentEnemyStationEntities[0], esWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
} }
{ {
@@ -323,7 +328,7 @@ void Simulation::placeEnemyStationSet(int generation)
} }
m_currentEnemyStationEntities[1] = m_admin.spawnStation( m_currentEnemyStationEntities[1] = m_admin.spawnStation(
anchor, esParsed.footprint, absCells, esHp, esHp, true); anchor, esParsed.footprint, absCells, esHp, esHp, true);
m_admin.addComponent<Weapon>(m_currentEnemyStationEntities[1], esWeapon); m_admin.addComponent<WeaponComponent>(m_currentEnemyStationEntities[1], esWeapon);
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId()); m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
} }
} }
@@ -336,8 +341,9 @@ void Simulation::tickDeathsAndLoot()
{ {
// --- Dead ships --- // --- Dead ships ---
std::vector<entt::entity> deadShips; std::vector<entt::entity> deadShips;
m_admin.forEach<ShipIdentity, Health>( m_admin.forEach<ShipIdentityComponent, HealthComponent>(
[&deadShips](entt::entity e, const ShipIdentity& /*si*/, const Health& h) [&deadShips](entt::entity e, const ShipIdentityComponent& /*si*/,
const HealthComponent& h)
{ {
if (h.hp <= 0.0f) if (h.hp <= 0.0f)
{ {
@@ -347,8 +353,8 @@ void Simulation::tickDeathsAndLoot()
for (entt::entity deadEntity : deadShips) for (entt::entity deadEntity : deadShips)
{ {
const ShipIdentity& si = m_admin.get<ShipIdentity>(deadEntity); const ShipIdentityComponent& si = m_admin.get<ShipIdentityComponent>(deadEntity);
const Position& pos = m_admin.get<Position>(deadEntity); const PositionComponent& pos = m_admin.get<PositionComponent>(deadEntity);
for (const ShipDef& def : m_config.ships.ships) for (const ShipDef& def : m_config.ships.ships)
{ {
if (def.id == si.schematicId && def.loot.scrapDrop > 0) if (def.id == si.schematicId && def.loot.scrapDrop > 0)
@@ -364,8 +370,9 @@ void Simulation::tickDeathsAndLoot()
// --- Dead stations --- // --- Dead stations ---
std::vector<entt::entity> deadStations; std::vector<entt::entity> deadStations;
m_admin.forEach<StationBody, Health>( m_admin.forEach<StationBodyComponent, HealthComponent>(
[&deadStations](entt::entity e, const StationBody& /*sb*/, const Health& h) [&deadStations](entt::entity e, const StationBodyComponent& /*sb*/,
const HealthComponent& h)
{ {
if (h.hp <= 0.0f) if (h.hp <= 0.0f)
{ {
@@ -375,9 +382,9 @@ void Simulation::tickDeathsAndLoot()
for (entt::entity deadEntity : deadStations) for (entt::entity deadEntity : deadStations)
{ {
const StationBody& sb = m_admin.get<StationBody>(deadEntity); const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
const Position& pos = m_admin.get<Position>(deadEntity); const PositionComponent& pos = m_admin.get<PositionComponent>(deadEntity);
const Faction& fac = m_admin.get<Faction>(deadEntity); const FactionComponent& fac = m_admin.get<FactionComponent>(deadEntity);
const Tick despawnAt = m_currentTick const Tick despawnAt = m_currentTick
+ secondsToTicks(m_config.world.scrapDespawnSeconds); + secondsToTicks(m_config.world.scrapDespawnSeconds);
@@ -406,7 +413,7 @@ void Simulation::tickDeathsAndLoot()
// --- HQ death check --- // --- HQ death check ---
if (m_admin.isValid(m_hqProxyEntity)) if (m_admin.isValid(m_hqProxyEntity))
{ {
const Health& hqHealth = m_admin.get<Health>(m_hqProxyEntity); const HealthComponent& hqHealth = m_admin.get<HealthComponent>(m_hqProxyEntity);
if (hqHealth.hp <= 0.0f) if (hqHealth.hp <= 0.0f)
{ {
m_gameOver = true; m_gameOver = true;
@@ -415,9 +422,9 @@ void Simulation::tickDeathsAndLoot()
// --- Push check: if both current enemy stations are gone, trigger push --- // --- Push check: if both current enemy stations are gone, trigger push ---
const bool es0Gone = !m_admin.isValid(m_currentEnemyStationEntities[0]) const bool es0Gone = !m_admin.isValid(m_currentEnemyStationEntities[0])
|| m_admin.get<Health>(m_currentEnemyStationEntities[0]).hp <= 0.0f; || m_admin.get<HealthComponent>(m_currentEnemyStationEntities[0]).hp <= 0.0f;
const bool es1Gone = !m_admin.isValid(m_currentEnemyStationEntities[1]) const bool es1Gone = !m_admin.isValid(m_currentEnemyStationEntities[1])
|| m_admin.get<Health>(m_currentEnemyStationEntities[1]).hp <= 0.0f; || m_admin.get<HealthComponent>(m_currentEnemyStationEntities[1]).hp <= 0.0f;
if (es0Gone && es1Gone && if (es0Gone && es1Gone &&
m_currentEnemyStationEntities[0] != entt::null) m_currentEnemyStationEntities[0] != entt::null)

View File

@@ -12,14 +12,25 @@
#include "ConfigLoader.h" #include "ConfigLoader.h"
#include "DynamicBodyComponent.h" #include "DynamicBodyComponent.h"
#include "DynamicBodySystem.h" #include "DynamicBodySystem.h"
#include "EcsComponents.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "HomeReturnBehaviorComponent.h"
#include "MovementIntentComponent.h"
#include "MovementIntentSystem.h" #include "MovementIntentSystem.h"
#include "PositionComponent.h"
#include "RallyBehaviorComponent.h"
#include "RepairBehaviorComponent.h"
#include "RepairToolComponent.h"
#include "Rotation.h" #include "Rotation.h"
#include "SalvageBehaviorComponent.h"
#include "SalvageCargoComponent.h"
#include "ScrapSystem.h" #include "ScrapSystem.h"
#include "Ship.h" #include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "Tick.h" #include "Tick.h"
#include "ThreatResponseBehaviorComponent.h"
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Fixture // Fixture
@@ -78,19 +89,19 @@ struct Fixture
}; };
// Helpers to read ECS data for a ship entity. // Helpers to read ECS data for a ship entity.
static const MovementIntent& intent(EntityAdmin& a, entt::entity e) static const MovementIntentComponent& intent(EntityAdmin& a, entt::entity e)
{ {
return a.get<MovementIntent>(e); return a.get<MovementIntentComponent>(e);
} }
static const Health& health(EntityAdmin& a, entt::entity e) static const HealthComponent& health(EntityAdmin& a, entt::entity e)
{ {
return a.get<Health>(e); return a.get<HealthComponent>(e);
} }
static const Position& pos(EntityAdmin& a, entt::entity e) static const PositionComponent& pos(EntityAdmin& a, entt::entity e)
{ {
return a.get<Position>(e); return a.get<PositionComponent>(e);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -103,7 +114,7 @@ TEST_CASE("BehaviorSystem: clearMovementIntents resets all ships to priority 0",
Fixture f; Fixture f;
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
f.admin.get<MovementIntent>(e) = MovementIntent{3, QVector2D(10.0f, 0.0f)}; f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{3, QVector2D(10.0f, 0.0f)};
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
REQUIRE(intent(f.admin, e).priority == 0); REQUIRE(intent(f.admin, e).priority == 0);
@@ -120,7 +131,7 @@ TEST_CASE("BehaviorSystem: tickMovement advances ship by maxSpeedPerTick toward
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeedPerTick; const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeedPerTick;
f.admin.get<MovementIntent>(e) = MovementIntent{1, QVector2D(100.0f, 0.0f)}; f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{1, QVector2D(100.0f, 0.0f)};
f.movementIntent.tick(f.admin); f.movementIntent.tick(f.admin);
f.dynamicBody.tick(f.admin); f.dynamicBody.tick(f.admin);
@@ -136,7 +147,7 @@ TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoo
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeedPerTick; const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeedPerTick;
const QVector2D target(speed * 0.5f, 0.0f); const QVector2D target(speed * 0.5f, 0.0f);
f.admin.get<MovementIntent>(e) = MovementIntent{1, target}; f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{1, target};
f.movementIntent.tick(f.admin); f.movementIntent.tick(f.admin);
f.dynamicBody.tick(f.admin); f.dynamicBody.tick(f.admin);
@@ -153,8 +164,8 @@ TEST_CASE("BehaviorSystem: tickHomeReturnBehavior does nothing when HP is above
{ {
Fixture f; Fixture f;
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
f.admin.addComponent<HomeReturnBehavior>(e, HomeReturnBehavior{0.3f, QVector2D(-10.0f, 0.0f)}); f.admin.addComponent<HomeReturnBehaviorComponent>(e, HomeReturnBehaviorComponent{0.3f, QVector2D(-10.0f, 0.0f)});
f.admin.get<Health>(e).hp = f.admin.get<Health>(e).maxHp; // full HP f.admin.get<HealthComponent>(e).hp = f.admin.get<HealthComponent>(e).maxHp; // full HP
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickHomeReturnBehavior(f.admin); f.ai.tickHomeReturnBehavior(f.admin);
@@ -168,8 +179,8 @@ TEST_CASE("BehaviorSystem: tickHomeReturnBehavior writes priority-4 intent towar
Fixture f; Fixture f;
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
const QVector2D homePos(-10.0f, 0.0f); const QVector2D homePos(-10.0f, 0.0f);
f.admin.addComponent<HomeReturnBehavior>(e, HomeReturnBehavior{0.5f, homePos}); f.admin.addComponent<HomeReturnBehaviorComponent>(e, HomeReturnBehaviorComponent{0.5f, homePos});
f.admin.get<Health>(e).hp = f.admin.get<Health>(e).maxHp * 0.2f; // below threshold f.admin.get<HealthComponent>(e).hp = f.admin.get<HealthComponent>(e).maxHp * 0.2f; // below threshold
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickHomeReturnBehavior(f.admin); f.ai.tickHomeReturnBehavior(f.admin);
@@ -186,8 +197,8 @@ TEST_CASE("BehaviorSystem: tickHomeReturnBehavior priority-4 beats tickThreatRes
f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f), /*isEnemy=*/true); f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f), /*isEnemy=*/true);
const QVector2D homePos(-50.0f, 0.0f); const QVector2D homePos(-50.0f, 0.0f);
f.admin.addComponent<HomeReturnBehavior>(player, HomeReturnBehavior{0.5f, homePos}); f.admin.addComponent<HomeReturnBehaviorComponent>(player, HomeReturnBehaviorComponent{0.5f, homePos});
f.admin.get<Health>(player).hp = f.admin.get<Health>(player).maxHp * 0.1f; f.admin.get<HealthComponent>(player).hp = f.admin.get<HealthComponent>(player).maxHp * 0.1f;
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickHomeReturnBehavior(f.admin); f.ai.tickHomeReturnBehavior(f.admin);
@@ -212,8 +223,8 @@ TEST_CASE("BehaviorSystem: player combat ship acquires nearest enemy ship in ran
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickThreatResponseBehavior(f.admin, f.buildings); f.ai.tickThreatResponseBehavior(f.admin, f.buildings);
REQUIRE(f.admin.hasAll<ThreatResponseBehavior>(player)); REQUIRE(f.admin.hasAll<ThreatResponseBehaviorComponent>(player));
const ThreatResponseBehavior& threatResponseBehavior = f.admin.get<ThreatResponseBehavior>(player); const ThreatResponseBehaviorComponent& threatResponseBehavior = f.admin.get<ThreatResponseBehaviorComponent>(player);
REQUIRE(threatResponseBehavior.currentTarget.has_value()); REQUIRE(threatResponseBehavior.currentTarget.has_value());
REQUIRE(*threatResponseBehavior.currentTarget == enemy); REQUIRE(*threatResponseBehavior.currentTarget == enemy);
} }
@@ -228,8 +239,8 @@ TEST_CASE("BehaviorSystem: player combat ship does not target friendly ships",
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickThreatResponseBehavior(f.admin, f.buildings); f.ai.tickThreatResponseBehavior(f.admin, f.buildings);
REQUIRE(f.admin.hasAll<ThreatResponseBehavior>(e1)); REQUIRE(f.admin.hasAll<ThreatResponseBehaviorComponent>(e1));
REQUIRE_FALSE(f.admin.get<ThreatResponseBehavior>(e1).currentTarget.has_value()); REQUIRE_FALSE(f.admin.get<ThreatResponseBehaviorComponent>(e1).currentTarget.has_value());
} }
TEST_CASE("BehaviorSystem: player combat ship ignores enemy beyond engagement range", TEST_CASE("BehaviorSystem: player combat ship ignores enemy beyond engagement range",
@@ -242,7 +253,7 @@ TEST_CASE("BehaviorSystem: player combat ship ignores enemy beyond engagement ra
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickThreatResponseBehavior(f.admin, f.buildings); f.ai.tickThreatResponseBehavior(f.admin, f.buildings);
REQUIRE_FALSE(f.admin.get<ThreatResponseBehavior>(player).currentTarget.has_value()); REQUIRE_FALSE(f.admin.get<ThreatResponseBehaviorComponent>(player).currentTarget.has_value());
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -260,8 +271,8 @@ TEST_CASE("BehaviorSystem: enemy ship acquires nearest player ship in range",
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickThreatResponseBehavior(f.admin, f.buildings); f.ai.tickThreatResponseBehavior(f.admin, f.buildings);
REQUIRE(f.admin.hasAll<ThreatResponseBehavior>(enemy)); REQUIRE(f.admin.hasAll<ThreatResponseBehaviorComponent>(enemy));
const ThreatResponseBehavior& threatResponseBehavior = f.admin.get<ThreatResponseBehavior>(enemy); const ThreatResponseBehaviorComponent& threatResponseBehavior = f.admin.get<ThreatResponseBehaviorComponent>(enemy);
REQUIRE(threatResponseBehavior.currentTarget.has_value()); REQUIRE(threatResponseBehavior.currentTarget.has_value());
REQUIRE(*threatResponseBehavior.currentTarget == player); REQUIRE(*threatResponseBehavior.currentTarget == player);
} }
@@ -291,7 +302,7 @@ TEST_CASE("BehaviorSystem: repair ship writes intent toward damaged friendly shi
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f)); const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f)); const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f));
f.admin.get<Health>(friendly).hp = f.admin.get<Health>(friendly).maxHp * 0.5f; f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickRepairBehavior(f.admin, f.buildings); f.ai.tickRepairBehavior(f.admin, f.buildings);
@@ -307,8 +318,8 @@ TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f)); const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f)); const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
const float initialHp = f.admin.get<Health>(friendly).maxHp * 0.5f; const float initialHp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
f.admin.get<Health>(friendly).hp = initialHp; f.admin.get<HealthComponent>(friendly).hp = initialHp;
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickRepairBehavior(f.admin, f.buildings); f.ai.tickRepairBehavior(f.admin, f.buildings);
@@ -322,7 +333,7 @@ TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f)); f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f)); const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
f.admin.get<Health>(friendly).hp = f.admin.get<Health>(friendly).maxHp - 0.001f; f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp - 0.001f;
for (int i = 0; i < 5; ++i) for (int i = 0; i < 5; ++i)
{ {
@@ -330,7 +341,7 @@ TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
f.ai.tickRepairBehavior(f.admin, f.buildings); f.ai.tickRepairBehavior(f.admin, f.buildings);
} }
const Health& h = health(f.admin, friendly); const HealthComponent& h = health(f.admin, friendly);
REQUIRE(h.hp <= h.maxHp); REQUIRE(h.hp <= h.maxHp);
REQUIRE(h.hp == Approx(h.maxHp)); REQUIRE(h.hp == Approx(h.maxHp));
} }
@@ -363,7 +374,7 @@ TEST_CASE("BehaviorSystem: salvage ship collects scrap on arrival", "[behavior]"
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings); f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings);
REQUIRE(f.admin.get<SalvageCargo>(ship).current == 1); REQUIRE(f.admin.get<SalvageCargoComponent>(ship).current == 1);
REQUIRE_FALSE(f.admin.isValid(scrapEntity)); REQUIRE_FALSE(f.admin.isValid(scrapEntity));
} }
@@ -385,13 +396,13 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
REQUIRE(f.buildings.findBuilding(bayId) != nullptr); REQUIRE(f.buildings.findBuilding(bayId) != nullptr);
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(5.0f, 0.0f)); const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(5.0f, 0.0f));
SalvageCargo& cargo = f.admin.get<SalvageCargo>(ship); SalvageCargoComponent& cargo = f.admin.get<SalvageCargoComponent>(ship);
cargo.current = cargo.capacity; // full cargo cargo.current = cargo.capacity; // full cargo
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings); f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings);
const MovementIntent& i = intent(f.admin, ship); const MovementIntentComponent& i = intent(f.admin, ship);
REQUIRE(i.priority == 1); REQUIRE(i.priority == 1);
REQUIRE(i.target.x() < pos(f.admin, ship).value.x()); REQUIRE(i.target.x() < pos(f.admin, ship).value.x());
} }
@@ -404,7 +415,7 @@ TEST_CASE("SensorRange: sensorRange is populated from config formula at spawn",
{ {
Fixture f; Fixture f;
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
REQUIRE(f.admin.get<SensorRange>(e).value == Approx(200.0f)); REQUIRE(f.admin.get<SensorRangeComponent>(e).value == Approx(200.0f));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -421,7 +432,7 @@ TEST_CASE("SensorRange: player combat ship acquires enemy just inside sensor ran
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickThreatResponseBehavior(f.admin, f.buildings); f.ai.tickThreatResponseBehavior(f.admin, f.buildings);
REQUIRE(f.admin.get<ThreatResponseBehavior>(player).currentTarget == enemy); REQUIRE(f.admin.get<ThreatResponseBehaviorComponent>(player).currentTarget == enemy);
} }
TEST_CASE("SensorRange: player combat ship ignores enemy just outside sensor range", "[sensor]") TEST_CASE("SensorRange: player combat ship ignores enemy just outside sensor range", "[sensor]")
@@ -433,7 +444,7 @@ TEST_CASE("SensorRange: player combat ship ignores enemy just outside sensor ran
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickThreatResponseBehavior(f.admin, f.buildings); f.ai.tickThreatResponseBehavior(f.admin, f.buildings);
REQUIRE_FALSE(f.admin.get<ThreatResponseBehavior>(player).currentTarget.has_value()); REQUIRE_FALSE(f.admin.get<ThreatResponseBehaviorComponent>(player).currentTarget.has_value());
} }
TEST_CASE("SensorRange: enemy ship ignores player just outside sensor range", "[sensor]") TEST_CASE("SensorRange: enemy ship ignores player just outside sensor range", "[sensor]")
@@ -446,7 +457,7 @@ TEST_CASE("SensorRange: enemy ship ignores player just outside sensor range", "[
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickThreatResponseBehavior(f.admin, f.buildings); f.ai.tickThreatResponseBehavior(f.admin, f.buildings);
REQUIRE_FALSE(f.admin.get<ThreatResponseBehavior>(enemy).currentTarget.has_value()); REQUIRE_FALSE(f.admin.get<ThreatResponseBehaviorComponent>(enemy).currentTarget.has_value());
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -483,12 +494,12 @@ TEST_CASE("SensorRange: repair ship does not acquire damaged ally beyond sensor
Fixture f; Fixture f;
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f)); const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f)); const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f));
f.admin.get<Health>(friendly).hp = f.admin.get<Health>(friendly).maxHp * 0.5f; f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickRepairBehavior(f.admin, f.buildings); f.ai.tickRepairBehavior(f.admin, f.buildings);
REQUIRE_FALSE(f.admin.get<RepairBehavior>(repairShip).currentTarget.has_value()); REQUIRE_FALSE(f.admin.get<RepairBehaviorComponent>(repairShip).currentTarget.has_value());
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -504,6 +515,6 @@ TEST_CASE("SensorRange: salvage ship ignores scrap beyond sensor range", "[senso
f.ships.clearMovementIntents(); f.ships.clearMovementIntents();
f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings); f.ai.tickSalvageBehavior(f.admin, f.scraps, f.buildings);
REQUIRE_FALSE(f.admin.get<SalvageBehavior>(ship).scrapTarget.has_value()); REQUIRE_FALSE(f.admin.get<SalvageBehaviorComponent>(ship).scrapTarget.has_value());
REQUIRE(intent(f.admin, ship).target.x() > pos(f.admin, ship).value.x()); REQUIRE(intent(f.admin, ship).target.x() > pos(f.admin, ship).value.x());
} }

View File

@@ -8,14 +8,18 @@
#include "BuildingType.h" #include "BuildingType.h"
#include "CombatSystem.h" #include "CombatSystem.h"
#include "ConfigLoader.h" #include "ConfigLoader.h"
#include "EcsComponents.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "FactionComponent.h"
#include "FireEvent.h" #include "FireEvent.h"
#include "HealthComponent.h"
#include "HqProxyComponent.h"
#include "ScrapSystem.h" #include "ScrapSystem.h"
#include "Ship.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "Simulation.h" #include "Simulation.h"
#include "StationBodyComponent.h"
#include "Tick.h" #include "Tick.h"
#include "ThreatResponseBehaviorComponent.h"
#include "WeaponComponent.h"
static GameConfig loadConfig() static GameConfig loadConfig()
{ {
@@ -63,14 +67,14 @@ struct CombatFixture
void wireEnemyTarget(entt::entity enemy, entt::entity playerTarget) void wireEnemyTarget(entt::entity enemy, entt::entity playerTarget)
{ {
if (admin.hasAll<Weapon>(enemy)) if (admin.hasAll<WeaponComponent>(enemy))
{ {
admin.get<Weapon>(enemy).currentTarget = playerTarget; admin.get<WeaponComponent>(enemy).currentTarget = playerTarget;
admin.get<Weapon>(enemy).cooldownTicks = 0.0f; admin.get<WeaponComponent>(enemy).cooldownTicks = 0.0f;
} }
if (admin.hasAll<ThreatResponseBehavior>(enemy)) if (admin.hasAll<ThreatResponseBehaviorComponent>(enemy))
{ {
admin.get<ThreatResponseBehavior>(enemy).currentTarget = playerTarget; admin.get<ThreatResponseBehaviorComponent>(enemy).currentTarget = playerTarget;
} }
} }
}; };
@@ -89,13 +93,13 @@ TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[comb
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false); const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
f.wireEnemyTarget(enemy, player); f.wireEnemyTarget(enemy, player);
const float hpBefore = f.admin.get<Health>(player).hp; const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<FireEvent> events; std::vector<FireEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
f.combat.applyPendingDamage(5, f.admin); f.combat.applyPendingDamage(5, f.admin);
REQUIRE(f.admin.get<Health>(player).hp < hpBefore); REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
REQUIRE(events.size() >= 1); REQUIRE(events.size() >= 1);
} }
@@ -109,7 +113,7 @@ 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); const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
f.wireEnemyTarget(enemy, player); f.wireEnemyTarget(enemy, player);
f.admin.get<Weapon>(enemy).cooldownTicks = 3.0f; // override to 3 f.admin.get<WeaponComponent>(enemy).cooldownTicks = 3.0f; // override to 3
std::vector<FireEvent> events; std::vector<FireEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
@@ -146,8 +150,8 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
// Find the player station entity via ECS. // Find the player station entity via ECS.
entt::entity stationEntity = entt::null; entt::entity stationEntity = entt::null;
QVector2D stationCenter; QVector2D stationCenter;
sim.admin().forEach<StationBody, Faction>( sim.admin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity e, const StationBody& sb, const Faction& f) [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (!f.isEnemy && stationEntity == entt::null) if (!f.isEnemy && stationEntity == entt::null)
{ {
@@ -184,8 +188,8 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
entt::entity stationEntity = entt::null; entt::entity stationEntity = entt::null;
QVector2D stationCenter; QVector2D stationCenter;
sim.admin().forEach<StationBody, Faction>( sim.admin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity e, const StationBody& sb, const Faction& f) [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (f.isEnemy && stationEntity == entt::null) if (f.isEnemy && stationEntity == entt::null)
{ {
@@ -222,8 +226,8 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
entt::entity stationEntity = entt::null; entt::entity stationEntity = entt::null;
QVector2D stationCenter; QVector2D stationCenter;
sim.admin().forEach<StationBody, Faction>( sim.admin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity e, const StationBody& sb, const Faction& f) [&](entt::entity e, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (f.isEnemy && stationEntity == entt::null) if (f.isEnemy && stationEntity == entt::null)
{ {
@@ -271,7 +275,7 @@ TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false); const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
f.wireEnemyTarget(enemy, player); f.wireEnemyTarget(enemy, player);
const float hpBefore = f.admin.get<Health>(player).hp; const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<FireEvent> events; std::vector<FireEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
@@ -279,7 +283,7 @@ TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
for (Tick t = 1; t < 5; ++t) for (Tick t = 1; t < 5; ++t)
{ {
f.combat.applyPendingDamage(t, f.admin); f.combat.applyPendingDamage(t, f.admin);
REQUIRE(f.admin.get<Health>(player).hp == Approx(hpBefore)); REQUIRE(f.admin.get<HealthComponent>(player).hp == Approx(hpBefore));
} }
} }
@@ -293,13 +297,13 @@ TEST_CASE("CombatSystem: damage applied exactly at impact tick", "[combat]")
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false); const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
f.wireEnemyTarget(enemy, player); f.wireEnemyTarget(enemy, player);
const float hpBefore = f.admin.get<Health>(player).hp; const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<FireEvent> events; std::vector<FireEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
f.combat.applyPendingDamage(5, f.admin); f.combat.applyPendingDamage(5, f.admin);
REQUIRE(f.admin.get<Health>(player).hp < hpBefore); REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
} }
TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[combat]") TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[combat]")
@@ -333,7 +337,7 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false); const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
f.wireEnemyTarget(enemy, player); f.wireEnemyTarget(enemy, player);
const float hpBefore = f.admin.get<Health>(player).hp; const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<FireEvent> events; std::vector<FireEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
@@ -342,7 +346,7 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
f.combat.applyPendingDamage(5, f.admin); f.combat.applyPendingDamage(5, f.admin);
REQUIRE(f.admin.get<Health>(player).hp < hpBefore); REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -359,7 +363,7 @@ TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
const entt::entity ship = sim.ships().spawn(combatDef->id, 1, const entt::entity ship = sim.ships().spawn(combatDef->id, 1,
QVector2D(10.0f, 10.0f)); QVector2D(10.0f, 10.0f));
sim.admin().get<Health>(ship).hp = -1.0f; sim.admin().get<HealthComponent>(ship).hp = -1.0f;
sim.tick(); sim.tick();
@@ -383,7 +387,7 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
const entt::entity ship = sim.ships().spawn(droppingDef->id, 1, const entt::entity ship = sim.ships().spawn(droppingDef->id, 1,
QVector2D(10.0f, 10.0f)); QVector2D(10.0f, 10.0f));
sim.admin().get<Health>(ship).hp = -1.0f; sim.admin().get<HealthComponent>(ship).hp = -1.0f;
sim.tick(); sim.tick();
@@ -395,8 +399,8 @@ TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
// Damage the HQ proxy entity (has HqProxy + Health). // Damage the HQ proxy entity (has HqProxy + Health).
sim.admin().forEach<HqProxy, Health>( sim.admin().forEach<HqProxyComponent, HealthComponent>(
[](entt::entity /*e*/, const HqProxy& /*hq*/, Health& h) [](entt::entity /*e*/, const HqProxyComponent& /*hq*/, HealthComponent& h)
{ {
h.hp = -1.0f; h.hp = -1.0f;
}); });

View File

@@ -2,7 +2,9 @@
#include <QVector2D> #include <QVector2D>
#include "DespawnAtComponent.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "ScrapDataComponent.h"
#include "ScrapSystem.h" #include "ScrapSystem.h"
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -17,8 +19,8 @@ TEST_CASE("ScrapSystem: spawn returns a valid entity with correct scrap data", "
const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100); const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100);
REQUIRE(admin.isValid(e)); REQUIRE(admin.isValid(e));
REQUIRE(admin.get<ScrapData>(e).amount == 5); REQUIRE(admin.get<ScrapDataComponent>(e).amount == 5);
REQUIRE(admin.get<DespawnAt>(e).tick == 100); REQUIRE(admin.get<DespawnAtComponent>(e).tick == 100);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -4,13 +4,13 @@
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "ConfigLoader.h" #include "ConfigLoader.h"
#include "EcsComponents.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "HealthComponent.h"
#include "ItemType.h" #include "ItemType.h"
#include "ModulesConfig.h" #include "ModulesConfig.h"
#include "Rotation.h" #include "Rotation.h"
#include "Ship.h" #include "SensorRangeComponent.h"
#include "ShipLayout.h" #include "ShipLayout.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "Simulation.h" #include "Simulation.h"
@@ -102,7 +102,7 @@ TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]")
QVector2D(5.0f, 5.0f), false, std::nullopt); QVector2D(5.0f, 5.0f), false, std::nullopt);
REQUIRE(sim.admin().isValid(e)); REQUIRE(sim.admin().isValid(e));
CHECK(sim.admin().get<Health>(e).maxHp == Approx(expectedHp)); CHECK(sim.admin().get<HealthComponent>(e).maxHp == Approx(expectedHp));
} }
TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]") TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
@@ -128,8 +128,8 @@ TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
REQUIRE(sim.admin().isValid(e)); REQUIRE(sim.admin().isValid(e));
// armor_plate has multiplied_hp_formula = "1.5" // armor_plate has multiplied_hp_formula = "1.5"
// final = base * (1 + (1.5 - 1)) + 0 = base * 1.5 // final = base * (1 + (1.5 - 1)) + 0 = base * 1.5
CHECK(sim.admin().get<Health>(e).maxHp == Approx(baseHp * 1.5f)); CHECK(sim.admin().get<HealthComponent>(e).maxHp == Approx(baseHp * 1.5f));
CHECK(sim.admin().get<Health>(e).hp == sim.admin().get<Health>(e).maxHp); CHECK(sim.admin().get<HealthComponent>(e).hp == sim.admin().get<HealthComponent>(e).maxHp);
} }
TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]") TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
@@ -155,7 +155,7 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
REQUIRE(sim.admin().isValid(e)); REQUIRE(sim.admin().isValid(e));
// sensor_booster has added_sensor_range_formula = "10" // sensor_booster has added_sensor_range_formula = "10"
// final = base * 1.0 + 10 = base + 10 // final = base * 1.0 + 10 = base + 10
CHECK(sim.admin().get<SensorRange>(e).value == Approx(baseRange + 10.0f)); CHECK(sim.admin().get<SensorRangeComponent>(e).value == Approx(baseRange + 10.0f));
} }
TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]") TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
@@ -185,7 +185,7 @@ TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
// Two armor_plates: each 1.5 multiplier // Two armor_plates: each 1.5 multiplier
// total_mult = 1 + (1.5 - 1) + (1.5 - 1) = 2.0 // total_mult = 1 + (1.5 - 1) + (1.5 - 1) = 2.0
// final = base * 2.0 // final = base * 2.0
CHECK(sim.admin().get<Health>(e).maxHp == Approx(baseHp * 2.0f)); CHECK(sim.admin().get<HealthComponent>(e).maxHp == Approx(baseHp * 2.0f));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -5,14 +5,20 @@
#include <QVector2D> #include <QVector2D>
#include "BuildingId.h"
#include "ConfigLoader.h" #include "ConfigLoader.h"
#include "DynamicBodyComponent.h" #include "DynamicBodyComponent.h"
#include "EcsComponents.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "BuildingId.h" #include "HealthComponent.h"
#include "Ship.h" #include "RepairBehaviorComponent.h"
#include "RepairToolComponent.h"
#include "SalvageBehaviorComponent.h"
#include "SalvageCargoComponent.h"
#include "SensorRangeComponent.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "Tick.h" #include "Tick.h"
#include "ThreatResponseBehaviorComponent.h"
#include "WeaponComponent.h"
static GameConfig loadConfig() static GameConfig loadConfig()
{ {
@@ -33,12 +39,12 @@ TEST_CASE("ShipSystem: interceptor spawn has weapon and threatResponse, no cargo
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));
REQUIRE(admin.isValid(e)); REQUIRE(admin.isValid(e));
REQUIRE(admin.hasAll<Weapon>(e)); REQUIRE(admin.hasAll<WeaponComponent>(e));
REQUIRE(admin.hasAll<ThreatResponseBehavior>(e)); REQUIRE(admin.hasAll<ThreatResponseBehaviorComponent>(e));
REQUIRE_FALSE(admin.hasAll<SalvageCargo>(e)); REQUIRE_FALSE(admin.hasAll<SalvageCargoComponent>(e));
REQUIRE_FALSE(admin.hasAll<RepairTool>(e)); REQUIRE_FALSE(admin.hasAll<RepairToolComponent>(e));
REQUIRE_FALSE(admin.hasAll<RepairBehavior>(e)); REQUIRE_FALSE(admin.hasAll<RepairBehaviorComponent>(e));
REQUIRE_FALSE(admin.hasAll<SalvageBehavior>(e)); REQUIRE_FALSE(admin.hasAll<SalvageBehaviorComponent>(e));
} }
TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]") TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]")
@@ -50,16 +56,16 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]
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));
// hp_formula = "40 + 5*x" at x=1 → 45 // hp_formula = "40 + 5*x" at x=1 → 45
REQUIRE(admin.get<Health>(e).maxHp == Approx(45.0f)); REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
REQUIRE(admin.get<Health>(e).hp == Approx(45.0f)); REQUIRE(admin.get<HealthComponent>(e).hp == Approx(45.0f));
// damage_formula = "10 + 2*x" at x=1 → 12 // damage_formula = "10 + 2*x" at x=1 → 12
REQUIRE(admin.get<Weapon>(e).damage == Approx(12.0f)); REQUIRE(admin.get<WeaponComponent>(e).damage == Approx(12.0f));
// attack_range_formula = "150" // attack_range_formula = "150"
REQUIRE(admin.get<Weapon>(e).range == Approx(150.0f)); REQUIRE(admin.get<WeaponComponent>(e).range == Approx(150.0f));
// sensor_range_formula = "200" // sensor_range_formula = "200"
REQUIRE(admin.get<SensorRange>(e).value == Approx(200.0f)); REQUIRE(admin.get<SensorRangeComponent>(e).value == Approx(200.0f));
// cooldownTicks starts at 0 // cooldownTicks starts at 0
REQUIRE(admin.get<Weapon>(e).cooldownTicks == Approx(0.0f)); REQUIRE(admin.get<WeaponComponent>(e).cooldownTicks == Approx(0.0f));
} }
TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]") TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]")
@@ -71,7 +77,7 @@ TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]")
const entt::entity e = ss.spawn("interceptor", 5, QVector2D(0.0f, 0.0f)); const entt::entity e = ss.spawn("interceptor", 5, QVector2D(0.0f, 0.0f));
// hp_formula = "40 + 5*x" at x=5 → 65 // hp_formula = "40 + 5*x" at x=5 → 65
REQUIRE(admin.get<Health>(e).maxHp == Approx(65.0f)); REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(65.0f));
} }
TEST_CASE("ShipSystem: interceptor level 0 maxSpeedPerTick matches formula / kTickRateHz", "[ship]") TEST_CASE("ShipSystem: interceptor level 0 maxSpeedPerTick matches formula / kTickRateHz", "[ship]")
@@ -100,10 +106,10 @@ TEST_CASE("ShipSystem: salvage_ship spawn has cargo and scrapCollector, no weapo
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
REQUIRE(admin.hasAll<SalvageCargo>(e)); REQUIRE(admin.hasAll<SalvageCargoComponent>(e));
REQUIRE(admin.hasAll<SalvageBehavior>(e)); REQUIRE(admin.hasAll<SalvageBehaviorComponent>(e));
REQUIRE_FALSE(admin.hasAll<Weapon>(e)); REQUIRE_FALSE(admin.hasAll<WeaponComponent>(e));
REQUIRE_FALSE(admin.hasAll<RepairTool>(e)); REQUIRE_FALSE(admin.hasAll<RepairToolComponent>(e));
} }
TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]") TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
@@ -115,10 +121,10 @@ TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
// cargo_capacity = 10 // cargo_capacity = 10
REQUIRE(admin.get<SalvageCargo>(e).capacity == 10); REQUIRE(admin.get<SalvageCargoComponent>(e).capacity == 10);
REQUIRE(admin.get<SalvageCargo>(e).current == 0); REQUIRE(admin.get<SalvageCargoComponent>(e).current == 0);
REQUIRE(admin.get<SalvageBehavior>(e).deliveryBay == kInvalidBuildingId); REQUIRE(admin.get<SalvageBehaviorComponent>(e).deliveryBay == kInvalidBuildingId);
REQUIRE_FALSE(admin.get<SalvageBehavior>(e).scrapTarget.has_value()); REQUIRE_FALSE(admin.get<SalvageBehaviorComponent>(e).scrapTarget.has_value());
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -134,10 +140,10 @@ TEST_CASE("ShipSystem: repair_ship spawn has repairTool and repairBehavior, no w
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
REQUIRE(admin.hasAll<RepairTool>(e)); REQUIRE(admin.hasAll<RepairToolComponent>(e));
REQUIRE(admin.hasAll<RepairBehavior>(e)); REQUIRE(admin.hasAll<RepairBehaviorComponent>(e));
REQUIRE_FALSE(admin.hasAll<Weapon>(e)); REQUIRE_FALSE(admin.hasAll<WeaponComponent>(e));
REQUIRE_FALSE(admin.hasAll<SalvageCargo>(e)); REQUIRE_FALSE(admin.hasAll<SalvageCargoComponent>(e));
} }
TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]") TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]")
@@ -149,9 +155,9 @@ TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas",
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
// repair_rate_formula = "5 + x" at x=1 → 6 // repair_rate_formula = "5 + x" at x=1 → 6
REQUIRE(admin.get<RepairTool>(e).ratePerTick == Approx(6.0f)); REQUIRE(admin.get<RepairToolComponent>(e).ratePerTick == Approx(6.0f));
// repair_range_formula = "80" // repair_range_formula = "80"
REQUIRE(admin.get<RepairTool>(e).range == Approx(80.0f)); REQUIRE(admin.get<RepairToolComponent>(e).range == Approx(80.0f));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -4,12 +4,12 @@
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "ConfigLoader.h" #include "ConfigLoader.h"
#include "EcsComponents.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "FactionComponent.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "ItemType.h" #include "ItemType.h"
#include "Rotation.h" #include "Rotation.h"
#include "Ship.h" #include "ShipIdentityComponent.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "Simulation.h" #include "Simulation.h"
#include "Tick.h" #include "Tick.h"
@@ -55,8 +55,8 @@ static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
static int countShips(Simulation& sim) static int countShips(Simulation& sim)
{ {
int n = 0; int n = 0;
sim.admin().forEach<ShipIdentity>( sim.admin().forEach<ShipIdentityComponent>(
[&n](entt::entity /*e*/, const ShipIdentity& /*si*/) { ++n; }); [&n](entt::entity /*e*/, const ShipIdentityComponent& /*si*/) { ++n; });
return n; return n;
} }
@@ -114,8 +114,8 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
REQUIRE(countShips(sim) == shipsBefore + 1); REQUIRE(countShips(sim) == shipsBefore + 1);
bool foundPlayerShip = false; bool foundPlayerShip = false;
sim.admin().forEach<ShipIdentity, Faction>( sim.admin().forEach<ShipIdentityComponent, FactionComponent>(
[&](entt::entity /*e*/, const ShipIdentity& si, const Faction& f) [&](entt::entity /*e*/, const ShipIdentityComponent& si, const FactionComponent& f)
{ {
if (!f.isEnemy && si.schematicId == def->id) if (!f.isEnemy && si.schematicId == def->id)
{ {

View File

@@ -6,11 +6,15 @@
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "ConfigLoader.h" #include "ConfigLoader.h"
#include "EcsComponents.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "HqProxyComponent.h"
#include "Rotation.h" #include "Rotation.h"
#include "Ship.h" #include "ShipIdentityComponent.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "StationBodyComponent.h"
#include "WeaponComponent.h"
#include "Simulation.h" #include "Simulation.h"
#include "Tick.h" #include "Tick.h"
#include "WaveSystem.h" #include "WaveSystem.h"
@@ -112,8 +116,8 @@ TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations",
// Stations are ECS entities. // Stations are ECS entities.
int playerCount = 0; int playerCount = 0;
int enemyCount = 0; int enemyCount = 0;
sim.admin().forEach<StationBody, Faction>( sim.admin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f) [&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f)
{ {
if (f.isEnemy) { ++enemyCount; } if (f.isEnemy) { ++enemyCount; }
else { ++playerCount; } else { ++playerCount; }
@@ -132,8 +136,8 @@ TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]")
static_cast<float>(sim.config().stations.hq.hpFormula.evaluate(0.0)); static_cast<float>(sim.config().stations.hq.hpFormula.evaluate(0.0));
bool found = false; bool found = false;
float actualHp = 0.0f; float actualHp = 0.0f;
sim.admin().forEach<HqProxy, Health>( sim.admin().forEach<HqProxyComponent, HealthComponent>(
[&](entt::entity /*e*/, const HqProxy& /*hq*/, const Health& h) [&](entt::entity /*e*/, const HqProxyComponent& /*hq*/, const HealthComponent& h)
{ {
found = true; found = true;
actualHp = h.hp; actualHp = h.hp;
@@ -165,9 +169,9 @@ TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
const Simulation sim(loadConfig(), 42); const Simulation sim(loadConfig(), 42);
int armedPlayerStations = 0; int armedPlayerStations = 0;
sim.admin().forEach<StationBody, Faction, Weapon>( sim.admin().forEach<StationBodyComponent, FactionComponent, WeaponComponent>(
[&](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f, [&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f,
const Weapon& w) const WeaponComponent& w)
{ {
if (!f.isEnemy) if (!f.isEnemy)
{ {
@@ -185,9 +189,9 @@ TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
const Simulation sim(loadConfig(), 42); const Simulation sim(loadConfig(), 42);
int armedEnemyStations = 0; int armedEnemyStations = 0;
sim.admin().forEach<StationBody, Faction, Weapon>( sim.admin().forEach<StationBodyComponent, FactionComponent, WeaponComponent>(
[&](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f, [&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f,
const Weapon& w) const WeaponComponent& w)
{ {
if (f.isEnemy) if (f.isEnemy)
{ {
@@ -217,8 +221,8 @@ TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]
} }
bool foundEnemyShip = false; bool foundEnemyShip = false;
sim.admin().forEach<ShipIdentity, Faction>( sim.admin().forEach<ShipIdentityComponent, FactionComponent>(
[&](entt::entity /*e*/, const ShipIdentity& /*si*/, const Faction& f) [&](entt::entity /*e*/, const ShipIdentityComponent& /*si*/, const FactionComponent& f)
{ {
if (f.isEnemy) { foundEnemyShip = true; } if (f.isEnemy) { foundEnemyShip = true; }
}); });
@@ -236,8 +240,8 @@ TEST_CASE("WaveSystem: only eligible ships (cost > 0) appear in waves", "[wave]"
sim.tick(); sim.tick();
} }
sim.admin().forEach<ShipIdentity, Faction>( sim.admin().forEach<ShipIdentityComponent, FactionComponent>(
[&](entt::entity /*e*/, const ShipIdentity& si, const Faction& f) [&](entt::entity /*e*/, const ShipIdentityComponent& si, const FactionComponent& f)
{ {
if (!f.isEnemy) { return; } if (!f.isEnemy) { return; }
// salvage_ship and repair_ship have cost_formula = "0" and must not spawn. // salvage_ship and repair_ship have cost_formula = "0" and must not spawn.
@@ -255,8 +259,8 @@ TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]"
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
// Damage both enemy stations to 0. // Damage both enemy stations to 0.
sim.admin().forEach<StationBody, Faction, Health>( sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f, Health& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
{ {
if (f.isEnemy) { h.hp = -1.0f; } if (f.isEnemy) { h.hp = -1.0f; }
}); });
@@ -265,8 +269,8 @@ TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]"
// After push: should have 2 new enemy stations. // After push: should have 2 new enemy stations.
int enemyCount = 0; int enemyCount = 0;
sim.admin().forEach<StationBody, Faction>( sim.admin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f) [&](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f)
{ {
if (f.isEnemy) { ++enemyCount; } if (f.isEnemy) { ++enemyCount; }
}); });
@@ -277,8 +281,8 @@ TEST_CASE("WaveSystem: push emits exactly one SchematicDropEvent", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
sim.admin().forEach<StationBody, Faction, Health>( sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f, Health& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
{ {
if (f.isEnemy) { h.hp = -1.0f; } if (f.isEnemy) { h.hp = -1.0f; }
}); });
@@ -293,8 +297,8 @@ TEST_CASE("WaveSystem: push schematic drop awards a known ship id", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadConfig(), 42);
sim.admin().forEach<StationBody, Faction, Health>( sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f, Health& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
{ {
if (f.isEnemy) { h.hp = -1.0f; } if (f.isEnemy) { h.hp = -1.0f; }
}); });
@@ -322,8 +326,8 @@ TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
// Record the X position of the initial enemy stations. // Record the X position of the initial enemy stations.
int initialX = std::numeric_limits<int>::min(); int initialX = std::numeric_limits<int>::min();
sim.admin().forEach<StationBody, Faction>( sim.admin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity /*e*/, const StationBody& sb, const Faction& f) [&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (f.isEnemy && sb.anchor.x() > initialX) if (f.isEnemy && sb.anchor.x() > initialX)
{ {
@@ -331,16 +335,16 @@ TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
} }
}); });
sim.admin().forEach<StationBody, Faction, Health>( sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBody& /*sb*/, const Faction& f, Health& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
{ {
if (f.isEnemy) { h.hp = -1.0f; } if (f.isEnemy) { h.hp = -1.0f; }
}); });
sim.tick(); sim.tick();
int newX = std::numeric_limits<int>::min(); int newX = std::numeric_limits<int>::min();
sim.admin().forEach<StationBody, Faction>( sim.admin().forEach<StationBodyComponent, FactionComponent>(
[&](entt::entity /*e*/, const StationBody& sb, const Faction& f) [&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (f.isEnemy && sb.anchor.x() > newX) if (f.isEnemy && sb.anchor.x() > newX)
{ {

View File

@@ -17,14 +17,21 @@
#include <QPolygonF> #include <QPolygonF>
#include <QTimer> #include <QTimer>
#include "BeltSystem.h"
#include "Building.h" #include "Building.h"
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "BeltSystem.h" #include "FacingComponent.h"
#include "EcsComponents.h" #include "FactionComponent.h"
#include "HealthComponent.h"
#include "PositionComponent.h"
#include "RepairToolComponent.h"
#include "SalvageCargoComponent.h"
#include "ScrapSystem.h" #include "ScrapSystem.h"
#include "Ship.h" #include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "Simulation.h" #include "Simulation.h"
#include "StationBodyComponent.h"
#include "SurfaceMask.h" #include "SurfaceMask.h"
#include "Tick.h" #include "Tick.h"
@@ -161,9 +168,9 @@ void GameWorldView::onFrame()
{ {
float maxRadius = 0.125f; float maxRadius = 0.125f;
if (m_sim->admin().isValid(fe.target) if (m_sim->admin().isValid(fe.target)
&& m_sim->admin().hasAll<StationBody>(fe.target)) && m_sim->admin().hasAll<StationBodyComponent>(fe.target))
{ {
const StationBody& sb = m_sim->admin().get<StationBody>(fe.target); const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(fe.target);
const int shorter = std::min(sb.footprint.width(), sb.footprint.height()); const int shorter = std::min(sb.footprint.width(), sb.footprint.height());
maxRadius = shorter / 2.0f; maxRadius = shorter / 2.0f;
} }
@@ -343,8 +350,8 @@ float GameWorldView::enemyStationRightEdge() const
{ {
float rightX = static_cast<float>(m_config->world.regions.playerBufferWidth float rightX = static_cast<float>(m_config->world.regions.playerBufferWidth
+ m_config->world.regions.contestZoneWidth); + m_config->world.regions.contestZoneWidth);
m_sim->admin().forEach<StationBody, Faction>( m_sim->admin().forEach<StationBodyComponent, FactionComponent>(
[&rightX](entt::entity /*e*/, const StationBody& sb, const Faction& f) [&rightX](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f)
{ {
if (!f.isEnemy) { return; } if (!f.isEnemy) { return; }
for (const QPoint& cell : sb.bodyCells) for (const QPoint& cell : sb.bodyCells)
@@ -445,11 +452,11 @@ BuildingId GameWorldView::siteAtTile(QPoint tile) const
std::optional<QVector2D> GameWorldView::entityPosition(entt::entity entity) const std::optional<QVector2D> GameWorldView::entityPosition(entt::entity entity) const
{ {
if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll<Position>(entity)) if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll<PositionComponent>(entity))
{ {
return std::nullopt; return std::nullopt;
} }
return m_sim->admin().get<Position>(entity).value; return m_sim->admin().get<PositionComponent>(entity).value;
} }
void GameWorldView::stepSpeed(int delta) void GameWorldView::stepSpeed(int delta)
@@ -794,8 +801,9 @@ void GameWorldView::drawScrap(QPainter& painter)
void GameWorldView::drawStations(QPainter& painter) void GameWorldView::drawStations(QPainter& painter)
{ {
m_sim->admin().forEach<StationBody, Faction, Health>( m_sim->admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[&](entt::entity /*e*/, const StationBody& sb, const Faction& f, const Health& h) [&](entt::entity /*e*/, const StationBodyComponent& sb, const FactionComponent& f,
const HealthComponent& h)
{ {
const BuildingType visType = f.isEnemy const BuildingType visType = f.isEnemy
? BuildingType::EnemyDefenceStation ? BuildingType::EnemyDefenceStation
@@ -837,12 +845,14 @@ void GameWorldView::drawStations(QPainter& painter)
void GameWorldView::drawShips(QPainter& painter) void GameWorldView::drawShips(QPainter& painter)
{ {
m_sim->admin().forEach<ShipIdentity, Position, Facing, Faction>( m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
[&](entt::entity e, const ShipIdentity& /*si*/, const Position& pos, FactionComponent>(
const Facing& facing, const Faction& fac) [&](entt::entity e, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FacingComponent& facing,
const FactionComponent& fac)
{ {
const bool hasCargo = m_sim->admin().hasAll<SalvageCargo>(e); const bool hasCargo = m_sim->admin().hasAll<SalvageCargoComponent>(e);
const bool hasRepair = m_sim->admin().hasAll<RepairTool>(e); const bool hasRepair = m_sim->admin().hasAll<RepairToolComponent>(e);
const ShipRole role = shipRoleFromComponents(fac.isEnemy, hasCargo, hasRepair); const ShipRole role = shipRoleFromComponents(fac.isEnemy, hasCargo, hasRepair);
const std::map<ShipRole, ShipVisuals>::const_iterator it = const std::map<ShipRole, ShipVisuals>::const_iterator it =
m_visuals->ships.find(role); m_visuals->ships.find(role);
@@ -872,12 +882,14 @@ void GameWorldView::drawShips(QPainter& painter)
void GameWorldView::drawDebugSensorRanges(QPainter& painter) void GameWorldView::drawDebugSensorRanges(QPainter& painter)
{ {
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
m_sim->admin().forEach<ShipIdentity, Position, Facing, Faction, SensorRange>( m_sim->admin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
[&](entt::entity e, const ShipIdentity& /*si*/, const Position& pos, FactionComponent, SensorRangeComponent>(
const Facing& /*facing*/, const Faction& fac, const SensorRange& sensor) [&](entt::entity e, const ShipIdentityComponent& /*si*/,
const PositionComponent& pos, const FacingComponent& /*facing*/,
const FactionComponent& fac, const SensorRangeComponent& sensor)
{ {
const bool hasCargo = m_sim->admin().hasAll<SalvageCargo>(e); const bool hasCargo = m_sim->admin().hasAll<SalvageCargoComponent>(e);
const bool hasRepair = m_sim->admin().hasAll<RepairTool>(e); const bool hasRepair = m_sim->admin().hasAll<RepairToolComponent>(e);
const ShipRole role = shipRoleFromComponents(fac.isEnemy, hasCargo, hasRepair); const ShipRole role = shipRoleFromComponents(fac.isEnemy, hasCargo, hasRepair);
const std::map<ShipRole, ShipVisuals>::const_iterator it = const std::map<ShipRole, ShipVisuals>::const_iterator it =
m_visuals->ships.find(role); m_visuals->ships.find(role);