143 lines
4.6 KiB
C++
143 lines
4.6 KiB
C++
#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<WeaponFiredEvent>& 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<WeaponComponent, ModuleOwnerComponent>(
|
|
[&](entt::entity /*e*/, WeaponComponent& weapon, const ModuleOwnerComponent& owner)
|
|
{
|
|
const PositionComponent& pos = admin.get<PositionComponent>(owner.owner);
|
|
const FactionComponent& faction = admin.get<FactionComponent>(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<WeaponFiredEvent>& 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_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<SensorRangeComponent>(shipEntity)
|
|
? admin.get<SensorRangeComponent>(shipEntity).value_tiles
|
|
: weapon.range_tiles;
|
|
float bestDistanceSquared = acquisitionRange * acquisitionRange;
|
|
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});
|
|
|
|
WeaponFiredEvent 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)
|
|
{
|
|
TRACE();
|
|
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;
|
|
}
|
|
}
|
|
}
|