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

@@ -1,446 +0,0 @@
#include "AiSystem.h"
#include <optional>
#include <vector>
#include <QVector2D>
#include "Building.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "BuildingId.h"
#include "MovementIntent.h"
#include "ScrapSystem.h"
#include "Ship.h"
// ---------------------------------------------------------------------------
// tickHomeReturnBehavior (priority 4)
// ---------------------------------------------------------------------------
void AiSystem::tickHomeReturnBehavior(EntityAdmin& admin)
{
admin.forEach<HomeReturnBehavior, Health, MovementIntent>(
[](entt::entity /*e*/, const HomeReturnBehavior& homeReturnBehavior, const Health& h, MovementIntent& intent)
{
if (h.hp / h.maxHp < homeReturnBehavior.retreatHpFraction)
{
if (4 > intent.priority)
{
intent = MovementIntent{4, homeReturnBehavior.homePos};
}
}
});
}
// ---------------------------------------------------------------------------
// tickThreatResponseBehavior (priority 3)
// ---------------------------------------------------------------------------
void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSystem& buildings)
{
// Snapshot all combatant entities for target acquisition.
struct CombatantInfo
{
entt::entity entity;
QVector2D position;
bool isEnemy;
bool isStation;
};
std::vector<CombatantInfo> combatants;
admin.forEach<Position, Faction, ShipIdentity>(
[&combatants](entt::entity e, const Position& pos, const Faction& f, const ShipIdentity& /*si*/)
{
combatants.push_back({e, pos.value, f.isEnemy, false});
});
admin.forEach<Position, Faction, StationBody>(
[&combatants](entt::entity e, const Position& pos, const Faction& f, const StationBody& /*sb*/)
{
combatants.push_back({e, pos.value, f.isEnemy, true});
});
admin.forEach<Position, Faction, HqProxy>(
[&combatants](entt::entity e, const Position& pos, const Faction& f, const HqProxy& /*hq*/)
{
combatants.push_back({e, pos.value, f.isEnemy, true});
});
admin.forEach<ThreatResponseBehavior, Position, Faction, SensorRange, MovementIntent>(
[&](entt::entity e, ThreatResponseBehavior& threatResponseBehavior, Position& pos, Faction& faction,
SensorRange& sensor, MovementIntent& intent)
{
const float range = sensor.value;
// Validate current target.
bool targetValid = false;
if (threatResponseBehavior.currentTarget)
{
const entt::entity t = *threatResponseBehavior.currentTarget;
if (admin.isValid(t) && admin.hasAll<Position>(t))
{
const float dist = (admin.get<Position>(t).value - pos.value).length();
if (dist <= range)
{
targetValid = true;
}
}
}
if (!targetValid)
{
threatResponseBehavior.currentTarget = std::nullopt;
float bestDist = range;
for (const CombatantInfo& c : combatants)
{
if (c.entity == e) { continue; }
bool isValidTarget = false;
if (!faction.isEnemy)
{
isValidTarget = c.isEnemy;
}
else
{
isValidTarget = !c.isEnemy;
}
if (!isValidTarget) { continue; }
const float dist = (c.position - pos.value).length();
if (dist < bestDist)
{
bestDist = dist;
threatResponseBehavior.currentTarget = c.entity;
}
}
}
if (threatResponseBehavior.currentTarget)
{
const entt::entity t = *threatResponseBehavior.currentTarget;
QVector2D dest = pos.value;
if (admin.isValid(t) && admin.hasAll<Position>(t))
{
dest = admin.get<Position>(t).value;
}
if (3 > intent.priority)
{
intent = MovementIntent{3, dest};
}
}
else
{
if (3 > intent.priority)
{
if (admin.hasAll<RallyBehavior>(e))
{
intent = MovementIntent{3, admin.get<RallyBehavior>(e).rallyPoint};
}
else if (!faction.isEnemy)
{
intent = MovementIntent{3, QVector2D(pos.value.x() + 1000.0f,
pos.value.y())};
}
else
{
intent = MovementIntent{3, QVector2D(-10000.0f, pos.value.y())};
}
}
}
});
}
// ---------------------------------------------------------------------------
// tickRepairBehavior (priority 2)
// ---------------------------------------------------------------------------
void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
{
// Snapshot all entities with health for repair targeting.
struct RepairableInfo
{
entt::entity entity;
QVector2D position;
bool isEnemy;
bool isShip;
float hp;
float maxHp;
};
std::vector<RepairableInfo> repairables;
admin.forEach<ShipIdentity, Position, Faction, Health>(
[&repairables](entt::entity e, const ShipIdentity& /*si*/,
const Position& pos, const Faction& f, const Health& h)
{
repairables.push_back({e, pos.value, f.isEnemy, true, h.hp, h.maxHp});
});
admin.forEach<StationBody, Position, Faction, Health>(
[&repairables](entt::entity e, const StationBody& /*sb*/,
const Position& pos, const Faction& f, const Health& h)
{
repairables.push_back({e, pos.value, f.isEnemy, false, h.hp, h.maxHp});
});
// Snapshot enemy ships for threat detection.
struct EnemyInfo
{
QVector2D position;
};
std::vector<EnemyInfo> enemies;
admin.forEach<ShipIdentity, Position, Faction>(
[&enemies](entt::entity /*e*/, const ShipIdentity& /*si*/,
const Position& pos, const Faction& f)
{
if (f.isEnemy)
{
enemies.push_back({pos.value});
}
});
admin.forEach<RepairBehavior, RepairTool, Position, Faction, SensorRange, MovementIntent>(
[&](entt::entity e, RepairBehavior& rb, RepairTool& rt, Position& pos,
Faction& /*faction*/, SensorRange& sensor, MovementIntent& intent)
{
const float repairRange = rt.range;
// Flee if enemy nearby.
bool enemyNearby = false;
for (const EnemyInfo& enemy : enemies)
{
if ((enemy.position - pos.value).length() <= sensor.value)
{
enemyNearby = true;
break;
}
}
if (enemyNearby)
{
if (2 > intent.priority)
{
intent = MovementIntent{2, QVector2D(-10000.0f, pos.value.y())};
}
return;
}
// Validate current target.
bool targetValid = false;
if (rb.currentTarget)
{
const entt::entity t = *rb.currentTarget;
if (admin.isValid(t) && admin.hasAll<Health>(t))
{
const Health& th = admin.get<Health>(t);
if (th.hp > 0.0f && th.hp < th.maxHp)
{
targetValid = true;
}
}
}
if (!targetValid)
{
rb.currentTarget = std::nullopt;
float bestDist = sensor.value;
for (const RepairableInfo& r : repairables)
{
if (r.entity == e) { continue; }
if (r.isEnemy) { continue; }
if (r.hp >= r.maxHp) { continue; }
const float dist = (r.position - pos.value).length();
if (dist < bestDist)
{
bestDist = dist;
rb.currentTarget = r.entity;
}
}
}
if (!rb.currentTarget)
{
if (2 > intent.priority)
{
intent = MovementIntent{2, QVector2D(pos.value.x() + 1000.0f,
pos.value.y())};
}
return;
}
const entt::entity target = *rb.currentTarget;
QVector2D targetPos = pos.value;
bool isShipTarget = false;
if (admin.isValid(target) && admin.hasAll<Position>(target))
{
targetPos = admin.get<Position>(target).value;
isShipTarget = admin.hasAll<ShipIdentity>(target);
}
const float distToTarget = (targetPos - pos.value).length();
if (distToTarget <= repairRange)
{
if (admin.isValid(target) && admin.hasAll<Health>(target))
{
Health& targetHealth = admin.get<Health>(target);
targetHealth.hp = std::min(targetHealth.hp + rt.ratePerTick,
targetHealth.maxHp);
}
}
if (2 > intent.priority)
{
intent = MovementIntent{2, targetPos};
}
});
}
// ---------------------------------------------------------------------------
// tickSalvageBehavior (priority 1)
// ---------------------------------------------------------------------------
void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
BuildingSystem& buildings)
{
// Snapshot enemy ships for threat detection.
struct EnemyShipPos
{
QVector2D position;
};
std::vector<EnemyShipPos> enemyShips;
admin.forEach<ShipIdentity, Position, Faction>(
[&enemyShips](entt::entity /*e*/, const ShipIdentity& /*si*/,
const Position& pos, const Faction& f)
{
if (f.isEnemy)
{
enemyShips.push_back({pos.value});
}
});
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
admin.forEach<SalvageBehavior, SalvageCargo, Position, SensorRange, MovementIntent>(
[&](entt::entity /*e*/, SalvageBehavior& salvageBehavior, SalvageCargo& cargo,
Position& pos, SensorRange& sensor, MovementIntent& intent)
{
const float collectRange = cargo.collectionRange;
// Assign nearest SalvageBay if needed.
if (salvageBehavior.deliveryBay == kInvalidBuildingId)
{
const Building* bay = buildings.findNearestBuilding(pos.value,
BuildingType::SalvageBay);
if (bay)
{
salvageBehavior.deliveryBay = bay->id;
}
}
const BuildingId bayId = salvageBehavior.deliveryBay;
QVector2D bayPos = pos.value;
if (bayId != kInvalidBuildingId)
{
const Building* bay = buildings.findBuilding(bayId);
if (bay)
{
bayPos = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,
bay->anchor.y() + bay->footprint.height() / 2.0f);
}
}
const bool cargoFull = (cargo.current >= cargo.capacity);
if (cargoFull)
{
if (1 > intent.priority)
{
intent = MovementIntent{1, bayPos};
}
if (bayId != kInvalidBuildingId
&& (pos.value - bayPos).length() <= 1.0f)
{
if (buildings.deliverScrapToSalvageBay(bayId))
{
--cargo.current;
}
}
return;
}
// Retreat if enemy near and cargo empty.
bool retreating = false;
if (cargo.current == 0)
{
for (const EnemyShipPos& enemy : enemyShips)
{
if ((enemy.position - pos.value).length() <= collectRange)
{
if (1 > intent.priority)
{
intent = MovementIntent{1, QVector2D(-10000.0f, pos.value.y())};
}
retreating = true;
break;
}
}
}
if (retreating) { return; }
// Collect nearby scrap.
for (const ScrapInfo& si : allScrap)
{
if ((si.position - pos.value).length() <= collectRange)
{
if (scraps.consume(si.entity))
{
++cargo.current;
salvageBehavior.scrapTarget = std::nullopt;
}
break;
}
}
// Move toward scrap target or find a new one.
if (salvageBehavior.scrapTarget)
{
if (1 > intent.priority)
{
intent = MovementIntent{1, *salvageBehavior.scrapTarget};
}
}
else
{
float bestDist = sensor.value;
std::optional<QVector2D> bestPos;
for (const ScrapInfo& si : allScrap)
{
const float dist = (si.position - pos.value).length();
if (dist < bestDist)
{
bestDist = dist;
bestPos = si.position;
}
}
if (bestPos)
{
salvageBehavior.scrapTarget = bestPos;
if (1 > intent.priority)
{
intent = MovementIntent{1, *bestPos};
}
}
else
{
if (1 > intent.priority)
{
intent = MovementIntent{1, QVector2D(pos.value.x() + 1000.0f,
pos.value.y())};
}
}
}
});
}

View File

@@ -1,14 +0,0 @@
#pragma once
class BuildingSystem;
class EntityAdmin;
class ScrapSystem;
class AiSystem
{
public:
void tickHomeReturnBehavior(EntityAdmin& admin);
void tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSystem& buildings);
void tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings);
void tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps, BuildingSystem& buildings);
};

View File

@@ -5,16 +5,9 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/Ship.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.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}/CombatSystem.h
PARENT_SCOPE
)
@@ -24,13 +17,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/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}/CombatSystem.cpp
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,53 +0,0 @@
#pragma once
#include <optional>
#include <vector>
#include <QVector2D>
#include "Building.h"
#include "EcsComponents.h"
#include "FireEvent.h"
#include "GameConfig.h"
#include "Ship.h"
#include "Tick.h"
#include "entt/entity/entity.hpp"
class BuildingSystem;
class EntityAdmin;
class CombatSystem
{
public:
explicit CombatSystem(const GameConfig& config);
void tick(Tick currentTick,
EntityAdmin& admin,
BuildingSystem& buildings,
std::vector<FireEvent>& outFireEvents);
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
private:
struct PendingDamage
{
entt::entity target;
float amount;
Tick appliesAt;
};
std::vector<PendingDamage> m_pendingDamage;
void resolveWeapon(
entt::entity shipEntity,
Weapon& weapon,
const Position& ownPos,
const Faction& ownFaction,
Tick currentTick,
EntityAdmin& admin,
std::vector<FireEvent>& out);
const GameConfig& m_config;
};

View File

@@ -1,49 +0,0 @@
#include "DynamicBodySystem.h"
#include <algorithm>
#include <cmath>
#include <QVector2D>
#include "DynamicBodyComponent.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
static float wrapAngle(float a)
{
constexpr float kPi = 3.14159265f;
a = std::fmod(a, 2.0f * kPi);
if (a > kPi) { a -= 2.0f * kPi; }
if (a < -kPi) { a += 2.0f * kPi; }
return a;
}
void DynamicBodySystem::tick(EntityAdmin& admin)
{
admin.forEach<Position, Facing, DynamicBodyComponent>(
[](entt::entity /*e*/, Position& pos, Facing& facing, DynamicBodyComponent& body)
{
// Integrate angular velocity, clamp to max rotation speed, then advance facing.
body.angularVelocity += body.angularAcceleration;
body.angularVelocity = std::max(-body.maxRotationSpeedPerTick,
std::min(body.angularVelocity,
body.maxRotationSpeedPerTick));
facing.radians = wrapAngle(facing.radians + body.angularVelocity);
// Integrate linear velocity and cap to max speed.
body.velocity += body.linearAcceleration;
const float speed = body.velocity.length();
if (speed > body.maxSpeedPerTick)
{
body.velocity = body.velocity.normalized() * body.maxSpeedPerTick;
}
// Advance position.
pos.value += body.velocity;
// Reset per-tick fields so stale values don't linger if the intent
// system is skipped for this entity in a future tick.
body.linearAcceleration = QVector2D(0.0f, 0.0f);
body.angularAcceleration = 0.0f;
});
}

View File

@@ -1,9 +0,0 @@
#pragma once
class EntityAdmin;
class DynamicBodySystem
{
public:
void tick(EntityAdmin& admin);
};

View File

@@ -1,109 +0,0 @@
#include "MovementIntentSystem.h"
#include <algorithm>
#include <cmath>
#include <QVector2D>
#include "DynamicBodyComponent.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "MovementIntent.h"
static float wrapAngle(float a)
{
constexpr float kPi = 3.14159265f;
a = std::fmod(a, 2.0f * kPi);
if (a > kPi) { a -= 2.0f * kPi; }
if (a < -kPi) { a += 2.0f * kPi; }
return a;
}
void MovementIntentSystem::tick(EntityAdmin& admin)
{
admin.forEach<Position, Facing, DynamicBodyComponent, MovementIntent>(
[](entt::entity /*e*/, const Position& pos, const Facing& facing,
DynamicBodyComponent& body, const MovementIntent& intent)
{
if (intent.priority == 0)
{
// No movement intent: brake using available thrust.
const float linearBraking = std::min(body.velocity.length(),
body.maneuveringAccelerationPerTick);
body.linearAcceleration = (body.velocity.length() > 0.0001f)
? -body.velocity.normalized() * linearBraking
: QVector2D(0.0f, 0.0f);
const float angBraking = std::min(std::abs(body.angularVelocity),
body.angularAccelerationPerTick);
body.angularAcceleration = (body.angularVelocity >= 0.0f) ? -angBraking : angBraking;
return;
}
const QVector2D delta = intent.target - pos.value;
const float dist = delta.length();
if (dist < 0.001f)
{
// Already at target: no new thrust. The ship drifts; it will
// re-approach next tick once it has moved away.
body.linearAcceleration = QVector2D(0.0f, 0.0f);
body.angularAcceleration = 0.0f;
return;
}
// --- Angular acceleration ---
const float desiredAngle = std::atan2(delta.y(), delta.x());
const float angleDiff = wrapAngle(desiredAngle - facing.radians);
const float rotDelta = std::max(-body.angularAccelerationPerTick,
std::min(angleDiff, body.angularAccelerationPerTick));
float newAngVel = body.angularVelocity + rotDelta;
// Overshoot prevention: if the accumulated angular velocity already
// exceeds the remaining angle, snap it to exactly that angle so the
// ship doesn't rotate past its heading.
const bool sameSign = (newAngVel >= 0.0f) == (angleDiff >= 0.0f);
if (sameSign && std::abs(newAngVel) > std::abs(angleDiff))
{
newAngVel = angleDiff;
}
body.angularAcceleration = newAngVel - body.angularVelocity;
// DynamicBodySystem applies the clamp to maxRotationSpeedPerTick after
// integrating, so we do not clamp here.
// --- Linear acceleration ---
// Use the projected facing (after this tick's angular integration) so
// that the main thruster aligns with where the ship will actually be
// pointing when DynamicBodySystem applies the forces.
const float projectedRadians = wrapAngle(facing.radians + newAngVel);
const QVector2D facingVec(std::cos(projectedRadians), std::sin(projectedRadians));
const float manAccel = body.maneuveringAccelerationPerTick;
const float stoppingDist = (body.maxSpeedPerTick * body.maxSpeedPerTick)
/ (2.0f * manAccel);
const float desiredSpeed = (dist <= stoppingDist)
? std::sqrt(2.0f * manAccel * dist)
: body.maxSpeedPerTick;
const QVector2D desiredVel = delta.normalized() * desiredSpeed;
const QVector2D velError = desiredVel - body.velocity;
const float mainAligned = std::max(0.0f,
QVector2D::dotProduct(velError, facingVec));
const float mainApplied = std::min(mainAligned, body.mainAccelerationPerTick);
const QVector2D mainDelta = facingVec * mainApplied;
const QVector2D remaining = velError - mainDelta;
const float remainLen = remaining.length();
const QVector2D maneuverDelta = (remainLen > manAccel)
? remaining.normalized() * manAccel
: remaining;
body.linearAcceleration = mainDelta + maneuverDelta;
});
}

View File

@@ -1,9 +0,0 @@
#pragma once
class EntityAdmin;
class MovementIntentSystem
{
public:
void tick(EntityAdmin& admin);
};

View File

@@ -1,53 +0,0 @@
#include "ScrapSystem.h"
#include "EntityAdmin.h"
ScrapSystem::ScrapSystem(EntityAdmin& admin)
: m_admin(admin)
{
}
entt::entity ScrapSystem::spawn(QVector2D position, int amount, Tick despawnAt)
{
return m_admin.spawnScrap(position, amount, despawnAt);
}
void ScrapSystem::tickDespawn(Tick currentTick)
{
std::vector<entt::entity> expired;
m_admin.forEach<DespawnAt>(
[&expired, currentTick](entt::entity e, DespawnAt& d)
{
if (d.tick <= currentTick)
{
expired.push_back(e);
}
});
for (entt::entity e : expired)
{
m_admin.destroy(e);
}
}
std::optional<int> ScrapSystem::consume(entt::entity entity)
{
if (!m_admin.isValid(entity) || !m_admin.hasAll<ScrapData>(entity))
{
return std::nullopt;
}
int amount = m_admin.get<ScrapData>(entity).amount;
m_admin.destroy(entity);
return amount;
}
std::vector<ScrapInfo> ScrapSystem::allScrapInfo() const
{
std::vector<ScrapInfo> result;
m_admin.forEach<ScrapData>(
[&result, this](entt::entity e, const ScrapData& /*sd*/)
{
result.push_back(ScrapInfo{e, m_admin.get<Position>(e).value});
});
return result;
}

View File

@@ -1,37 +0,0 @@
#pragma once
#include <optional>
#include <vector>
#include <QVector2D>
#include "EcsComponents.h"
#include "Tick.h"
#include "entt/entity/entity.hpp"
class EntityAdmin;
struct ScrapInfo
{
entt::entity entity;
QVector2D position;
};
class ScrapSystem
{
public:
explicit ScrapSystem(EntityAdmin& admin);
entt::entity spawn(QVector2D position, int amount, Tick despawnAt);
void tickDespawn(Tick currentTick);
// Removes the scrap and returns its amount, or nullopt if not found.
std::optional<int> consume(entt::entity entity);
// Lightweight snapshot for callers that need to iterate all scrap.
std::vector<ScrapInfo> allScrapInfo() const;
private:
EntityAdmin& m_admin;
};

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

@@ -1,213 +0,0 @@
#include "ShipSystem.h"
#include <cassert>
#include <map>
#include <utility>
#include "DynamicBodyComponent.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "ModulesConfig.h"
#include "Tick.h"
ShipSystem::ShipSystem(const GameConfig& config, EntityAdmin& admin)
: m_config(config)
, m_admin(admin)
{
}
const ShipDef* ShipSystem::findShipDef(const std::string& schematicId) const
{
for (const ShipDef& def : m_config.ships.ships)
{
if (def.id == schematicId)
{
return &def;
}
}
return nullptr;
}
const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
{
for (const ModuleDef& def : m_config.modules.modules)
{
if (def.id == id)
{
return &def;
}
}
return nullptr;
}
entt::entity ShipSystem::spawn(const std::string& schematicId, int level, QVector2D position,
bool isEnemy,
const std::optional<ShipLayoutConfig>& layout)
{
const ShipDef* def = findShipDef(schematicId);
assert(def != nullptr);
const double x = static_cast<double>(level);
const float tickRate = static_cast<float>(kTickRateHz);
float hp = static_cast<float>(def->health.hpFormula.evaluate(x));
float maxHp = hp;
float maxSpeedPerTick = static_cast<float>(def->movement.speedFormula.evaluate(x)) / tickRate;
float mainAccelPerTick = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x)) / tickRate;
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(
position, hp, maxHp,
maxSpeedPerTick, mainAccelPerTick, maneuveringAccelPerTick,
angularAccelPerTick, maxRotationSpeedPerTick, sensorRange,
level, schematicId, isEnemy);
// Optional components based on ship role.
if (def->combat)
{
Weapon w;
w.damage = static_cast<float>(def->combat->damageFormula.evaluate(x));
w.range = static_cast<float>(def->combat->attackRangeFormula.evaluate(x));
w.fireRateHz = static_cast<float>(def->combat->attackRateFormula.evaluate(x));
w.cooldownTicks = 0.0f;
w.currentTarget = std::nullopt;
m_admin.addComponent<Weapon>(entity, w);
m_admin.addComponent<ThreatResponseBehavior>(entity, ThreatResponseBehavior{});
if (!isEnemy)
{
m_admin.addComponent<RallyBehavior>(entity, RallyBehavior{m_rallyPoint});
}
}
if (def->salvage)
{
SalvageCargo cargo;
cargo.capacity = def->salvage->cargoCapacity;
cargo.current = 0;
cargo.collectionRange = static_cast<float>(def->salvage->collectionRange);
m_admin.addComponent<SalvageCargo>(entity, cargo);
SalvageBehavior salvageBehavior;
salvageBehavior.scrapTarget = std::nullopt;
salvageBehavior.deliveryBay = kInvalidBuildingId;
m_admin.addComponent<SalvageBehavior>(entity, salvageBehavior);
}
if (def->repair)
{
RepairTool rt;
rt.ratePerTick = static_cast<float>(def->repair->repairRateFormula.evaluate(x));
rt.range = static_cast<float>(def->repair->repairRangeFormula.evaluate(x));
rt.currentTarget = std::nullopt;
m_admin.addComponent<RepairTool>(entity, rt);
m_admin.addComponent<RepairBehavior>(entity, RepairBehavior{});
}
// Apply module stat modifiers (REQ-MOD-STAT-CALC).
if (layout.has_value() && !layout->placedModules.empty())
{
std::map<std::string, std::pair<double, double>> mods;
for (const PlacedModule& pm : layout->placedModules)
{
const ModuleDef* modDef = findModuleDef(pm.moduleId);
if (!modDef)
{
continue;
}
for (const ModuleStatModifier& sm : modDef->statModifiers)
{
const double val = sm.formula.evaluate(
static_cast<double>(modDef->playerProductionLevel));
std::pair<double, double>& acc = mods[sm.stat];
if (sm.modifierType == "multiplicative")
{
acc.first += (val - 1.0);
}
else
{
acc.second += val;
}
}
}
auto applyMod = [&mods](float& stat, const std::string& name) {
const std::map<std::string, std::pair<double, double>>::const_iterator it =
mods.find(name);
if (it != mods.end())
{
stat = static_cast<float>(
static_cast<double>(stat) * (1.0 + it->second.first) + it->second.second);
}
};
Health& health = m_admin.get<Health>(entity);
DynamicBodyComponent& dynamics = m_admin.get<DynamicBodyComponent>(entity);
SensorRange& sensor = m_admin.get<SensorRange>(entity);
applyMod(health.maxHp, "hp");
health.hp = health.maxHp;
applyMod(dynamics.maxSpeedPerTick, "speed");
applyMod(dynamics.mainAccelerationPerTick, "main_acceleration");
applyMod(dynamics.maneuveringAccelerationPerTick, "maneuvering_acceleration");
applyMod(dynamics.angularAccelerationPerTick, "angular_acceleration");
applyMod(dynamics.maxRotationSpeedPerTick, "max_rotation_speed");
applyMod(sensor.value, "sensor_range");
if (m_admin.hasAll<Weapon>(entity))
{
Weapon& weapon = m_admin.get<Weapon>(entity);
applyMod(weapon.damage, "damage");
applyMod(weapon.range, "attack_range");
applyMod(weapon.fireRateHz, "attack_rate");
}
if (m_admin.hasAll<RepairTool>(entity))
{
RepairTool& repairTool = m_admin.get<RepairTool>(entity);
applyMod(repairTool.ratePerTick, "repair_rate");
applyMod(repairTool.range, "repair_range");
}
}
return entity;
}
void ShipSystem::despawn(entt::entity entity)
{
m_admin.destroy(entity);
}
void ShipSystem::clearMovementIntents()
{
m_admin.forEach<MovementIntent>([](entt::entity /*e*/, MovementIntent& i)
{
i = MovementIntent{0, QVector2D(0.0f, 0.0f)};
});
}
void ShipSystem::setRallyPoint(QVector2D point)
{
m_rallyPoint = point;
}
void ShipSystem::triggerRallyDeparture()
{
std::vector<entt::entity> toRemove;
m_admin.forEach<RallyBehavior, Faction>(
[&toRemove](entt::entity e, const RallyBehavior& /*rb*/, const Faction& f)
{
if (!f.isEnemy)
{
toRemove.push_back(e);
}
});
for (entt::entity e : toRemove)
{
m_admin.removeComponent<RallyBehavior>(e);
}
}

View File

@@ -1,42 +0,0 @@
#pragma once
#include <optional>
#include <string>
#include <QVector2D>
#include "GameConfig.h"
#include "Ship.h"
#include "ShipLayout.h"
#include "entt/entity/entity.hpp"
class EntityAdmin;
class ShipSystem
{
public:
ShipSystem(const GameConfig& config, EntityAdmin& admin);
entt::entity spawn(const std::string& schematicId, int level, QVector2D position,
bool isEnemy = false,
const std::optional<ShipLayoutConfig>& layout = std::nullopt);
void despawn(entt::entity entity);
// Reset all movement intents to priority 0 before behavior systems run.
void clearMovementIntents();
// Set the rally point that newly spawned player combat ships will loiter at.
void setRallyPoint(QVector2D point);
// Release all gathered player combat ships to advance toward the enemy.
void triggerRallyDeparture();
private:
const ShipDef* findShipDef(const std::string& schematicId) const;
const ModuleDef* findModuleDef(const std::string& id) const;
const GameConfig& m_config;
EntityAdmin& m_admin;
QVector2D m_rallyPoint;
};

View File

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