#include "CombatSystem.h" #include "EntityAdmin.h" #include "FactionComponent.h" #include "HealthComponent.h" #include "ModuleOwnerComponent.h" #include "PositionComponent.h" #include "SensorRangeComponent.h" #include "ShipIdentityComponent.h" #include "tracing.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& outWeaponFiredEvents) { TRACE(); // All weapons (ships and stations) are child entities linked via ModuleOwnerComponent. // AttackExecutor has already set each weapon's preferred (in-range) target; here we // validate it, fall back to nearest-target acquisition, and fire. admin.forEach( [&](entt::entity /*e*/, WeaponComponent& weapon, const ModuleOwnerComponent& owner) { const PositionComponent& pos = admin.get(owner.owner); const FactionComponent& faction = admin.get(owner.owner); resolveWeapon(owner.owner, weapon, pos, faction, currentTick, admin, outWeaponFiredEvents); }); } void CombatSystem::resolveWeapon( entt::entity shipEntity, WeaponComponent& weapon, const PositionComponent& ownPos, const FactionComponent& ownFaction, Tick currentTick, EntityAdmin& admin, std::vector& 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(t)) { weapon.currentTarget = std::nullopt; } else { const float distanceSquared = (ownPos.value - admin.get(t).value).lengthSquared(); if (distanceSquared > weapon.range_tiles * weapon.range_tiles) { weapon.currentTarget = std::nullopt; } } } // Acquire a new target if needed. // Ships use their sensor range; stations fall back to weapon range. if (!weapon.currentTarget) { const float acquisitionRange = admin.hasAll(shipEntity) ? admin.get(shipEntity).value_tiles : weapon.range_tiles; float bestDistanceSquared = acquisitionRange * acquisitionRange; admin.forEach( [&](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}); WeaponFiredEvent evt; evt.shooter = shipEntity; evt.target = targetEntity; evt.emittedAt = currentTick; out.push_back(evt); weapon.cooldownTicks = static_cast(kTickRateHz) / weapon.fireRateHz; } void CombatSystem::applyPendingDamage(Tick currentTick, EntityAdmin& admin) { TRACE(); std::vector::iterator it = m_pendingDamage.begin(); while (it != m_pendingDamage.end()) { if (it->appliesAt <= currentTick) { if (admin.isValid(it->target) && admin.hasAll(it->target)) { admin.get(it->target).hp -= it->amount; } it = m_pendingDamage.erase(it); } else { ++it; } } }