switch to ECS architecture

This commit is contained in:
2026-05-22 20:31:39 +02:00
parent c18c4e4804
commit ca07cbaf0e
34 changed files with 1943 additions and 2074 deletions

View File

@@ -1,11 +1,9 @@
#include "CombatSystem.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "Ship.h"
#include "ShipSystem.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
static constexpr Tick kWeaponImpactDelayTicks = 5; // 0.15 s × 30 Hz, rounded to nearest
static constexpr Tick kWeaponImpactDelayTicks = 5;
CombatSystem::CombatSystem(const GameConfig& config)
: m_config(config)
@@ -13,199 +11,169 @@ CombatSystem::CombatSystem(const GameConfig& config)
}
void CombatSystem::tick(Tick currentTick,
ShipSystem& ships,
BuildingSystem& buildings,
EntityAdmin& admin,
BuildingSystem& /*buildings*/,
std::vector<FireEvent>& outFireEvents)
{
// Ships: iterate and resolve weapon for each combat ship.
ships.forEach([&](Ship& ship)
{
resolveShipWeapon(ship, currentTick, ships, buildings, outFireEvents);
});
// Defence stations: acquire targets and fire.
buildings.forEachBuilding([&](Building& building)
{
if (building.type == BuildingType::PlayerDefenceStation ||
building.type == BuildingType::EnemyDefenceStation)
// Ship weapons.
admin.forEach<Weapon, ThreatResponse, Position>(
[&](entt::entity e, Weapon& weapon, ThreatResponse& threat, Position& pos)
{
resolveStationWeapon(building, currentTick, ships, buildings, outFireEvents);
}
});
resolveShipWeapon(e, weapon, threat, pos, currentTick, admin, outFireEvents);
});
// Station weapons.
admin.forEach<StationWeapon, Position, Faction>(
[&](entt::entity e, StationWeapon& weapon, Position& pos, Faction& faction)
{
resolveStationWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
});
}
void CombatSystem::resolveShipWeapon(Ship& ship, Tick currentTick,
ShipSystem& ships,
BuildingSystem& buildings,
void CombatSystem::resolveShipWeapon(entt::entity shipEntity, Weapon& weapon,
const ThreatResponse& threat,
const Position& pos, Tick currentTick,
EntityAdmin& admin,
std::vector<FireEvent>& out)
{
if (!ship.weapon || !ship.threatResponse ||
!ship.threatResponse->currentTarget)
if (!threat.currentTarget)
{
return;
}
Weapon& w = *ship.weapon;
// Decrement cooldown toward zero.
if (w.cooldownTicks > 0.0f)
if (weapon.cooldownTicks > 0.0f)
{
w.cooldownTicks -= 1.0f;
weapon.cooldownTicks -= 1.0f;
}
if (w.cooldownTicks > 0.0f)
if (weapon.cooldownTicks > 0.0f)
{
return;
}
const EntityId targetId = *ship.threatResponse->currentTarget;
const std::optional<QVector2D> tPos = targetPosition(targetId, ships, buildings);
if (!tPos)
{
ship.threatResponse->currentTarget = std::nullopt;
return;
}
const float dist = (ship.position - *tPos).length();
if (dist > w.range)
const entt::entity targetEntity = *threat.currentTarget;
if (!admin.isValid(targetEntity) || !admin.hasAll<Position>(targetEntity))
{
return;
}
m_pendingDamage.push_back({targetId, w.damage, currentTick + kWeaponImpactDelayTicks});
const QVector2D targetPos = admin.get<Position>(targetEntity).value;
const float dist = (pos.value - targetPos).length();
if (dist > weapon.range)
{
return;
}
m_pendingDamage.push_back({targetEntity, weapon.damage,
currentTick + kWeaponImpactDelayTicks});
FireEvent evt;
evt.shooter = ship.id;
evt.target = targetId;
evt.shooter = shipEntity;
evt.target = targetEntity;
evt.emittedAt = currentTick;
out.push_back(evt);
w.cooldownTicks = static_cast<float>(kTickRateHz) / w.fireRateHz;
weapon.cooldownTicks = static_cast<float>(kTickRateHz) / weapon.fireRateHz;
}
void CombatSystem::resolveStationWeapon(Building& station, Tick currentTick,
ShipSystem& ships,
BuildingSystem& buildings,
void CombatSystem::resolveStationWeapon(entt::entity stationEntity,
StationWeapon& weapon,
const Position& stationPos,
const Faction& stationFaction,
Tick currentTick,
EntityAdmin& admin,
std::vector<FireEvent>& out)
{
if (!station.weapon)
{
return;
}
StationWeapon& w = *station.weapon;
const bool stationIsEnemy = (station.type == BuildingType::EnemyDefenceStation);
const QVector2D stationCenter(
station.anchor.x() + station.footprint.width() / 2.0f,
station.anchor.y() + station.footprint.height() / 2.0f);
// Validate or clear existing target.
if (w.currentTarget)
if (weapon.currentTarget)
{
const std::optional<QVector2D> tPos =
targetPosition(*w.currentTarget, ships, buildings);
if (!tPos || (stationCenter - *tPos).length() > w.range)
const entt::entity t = *weapon.currentTarget;
if (!admin.isValid(t) || !admin.hasAll<Position>(t))
{
w.currentTarget = std::nullopt;
weapon.currentTarget = std::nullopt;
}
else
{
const float dist = (stationPos.value - admin.get<Position>(t).value).length();
if (dist > weapon.range)
{
weapon.currentTarget = std::nullopt;
}
}
}
// Acquire a new target if needed.
if (!w.currentTarget)
// Acquire a new target if needed (nearest opposing-faction ship).
if (!weapon.currentTarget)
{
w.currentTarget = acquireStationTarget(station, stationIsEnemy, ships);
float bestDist = weapon.range;
admin.forEach<ShipIdentity, Position, Faction>(
[&](entt::entity candidate, const ShipIdentity& /*si*/,
const Position& candidatePos, const Faction& candidateFaction)
{
const bool isValidTarget = stationFaction.isEnemy
? !candidateFaction.isEnemy
: candidateFaction.isEnemy;
if (!isValidTarget)
{
return;
}
const float dist = (candidatePos.value - stationPos.value).length();
if (dist < bestDist)
{
bestDist = dist;
weapon.currentTarget = candidate;
}
});
}
if (!w.currentTarget)
if (!weapon.currentTarget)
{
return;
}
// Decrement cooldown.
if (w.cooldownTicks > 0.0f)
if (weapon.cooldownTicks > 0.0f)
{
w.cooldownTicks -= 1.0f;
weapon.cooldownTicks -= 1.0f;
}
if (w.cooldownTicks > 0.0f)
if (weapon.cooldownTicks > 0.0f)
{
return;
}
const EntityId targetId = *w.currentTarget;
const std::optional<QVector2D> tPos = targetPosition(targetId, ships, buildings);
if (!tPos)
const entt::entity targetEntity = *weapon.currentTarget;
if (!admin.isValid(targetEntity) || !admin.hasAll<Position>(targetEntity))
{
w.currentTarget = std::nullopt;
weapon.currentTarget = std::nullopt;
return;
}
if ((stationCenter - *tPos).length() > w.range)
const QVector2D targetPos = admin.get<Position>(targetEntity).value;
if ((stationPos.value - targetPos).length() > weapon.range)
{
return;
}
m_pendingDamage.push_back({targetId, w.damage, currentTick + kWeaponImpactDelayTicks});
m_pendingDamage.push_back({targetEntity, weapon.damage,
currentTick + kWeaponImpactDelayTicks});
FireEvent evt;
evt.shooter = station.id;
evt.target = targetId;
evt.shooter = stationEntity;
evt.target = targetEntity;
evt.emittedAt = currentTick;
out.push_back(evt);
w.cooldownTicks = static_cast<float>(kTickRateHz) / w.fireRateHz;
weapon.cooldownTicks = static_cast<float>(kTickRateHz) / weapon.fireRateHz;
}
std::optional<EntityId> CombatSystem::acquireStationTarget(
const Building& station, bool stationIsEnemy,
const ShipSystem& ships) const
void CombatSystem::applyPendingDamage(Tick currentTick, EntityAdmin& admin)
{
const QVector2D stationCenter(
station.anchor.x() + station.footprint.width() / 2.0f,
station.anchor.y() + station.footprint.height() / 2.0f);
const float range = station.weapon->range;
std::optional<EntityId> best;
float bestDist = range;
// Scan ships for valid targets.
for (const Ship& candidate : ships.allShips())
{
const bool isValidTarget = stationIsEnemy ? !candidate.isEnemy
: candidate.isEnemy;
if (!isValidTarget)
{
continue;
}
const float dist = (candidate.position - stationCenter).length();
if (dist < bestDist)
{
bestDist = dist;
best = candidate.id;
}
}
return best;
}
void CombatSystem::applyPendingDamage(Tick currentTick,
ShipSystem& ships,
BuildingSystem& buildings)
{
auto it = m_pendingDamage.begin();
std::vector<PendingDamage>::iterator it = m_pendingDamage.begin();
while (it != m_pendingDamage.end())
{
if (it->appliesAt <= currentTick)
{
if (ships.findShip(it->target))
if (admin.isValid(it->target) && admin.hasAll<Health>(it->target))
{
ships.damageShip(it->target, it->amount);
}
else if (buildings.findBuilding(it->target))
{
buildings.damageBuilding(it->target, it->amount);
admin.get<Health>(it->target).hp -= it->amount;
}
it = m_pendingDamage.erase(it);
}
@@ -215,22 +183,3 @@ void CombatSystem::applyPendingDamage(Tick currentTick,
}
}
}
std::optional<QVector2D> CombatSystem::targetPosition(
EntityId id,
const ShipSystem& ships,
const BuildingSystem& buildings) const
{
const Ship* ship = ships.findShip(id);
if (ship)
{
return ship->position;
}
const Building* bld = buildings.findBuilding(id);
if (bld)
{
return QVector2D(bld->anchor.x() + bld->footprint.width() / 2.0f,
bld->anchor.y() + bld->footprint.height() / 2.0f);
}
return std::nullopt;
}