Files
dota_factory/src/lib/ecs/system/CombatSystem.cpp
2026-06-05 20:09:20 +02:00

147 lines
4.7 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 "ThreatResponseBehaviorComponent.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<FireEvent>& outFireEvents)
{
TRACE();
// All weapons (ships and stations) are child entities linked via ModuleOwnerComponent.
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
[&](entt::entity /*e*/, WeaponComponent& weapon, const ModuleOwnerComponent& owner)
{
if (admin.hasAll<ThreatResponseBehaviorComponent>(owner.owner))
{
weapon.currentTarget =
admin.get<ThreatResponseBehaviorComponent>(owner.owner).currentTarget;
}
const PositionComponent& pos = admin.get<PositionComponent>(owner.owner);
const FactionComponent& faction = admin.get<FactionComponent>(owner.owner);
resolveWeapon(owner.owner, 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_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});
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)
{
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;
}
}
}