Files
dota_factory/src/lib/sim/CombatSystem.cpp

237 lines
6.3 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "CombatSystem.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "Ship.h"
#include "ShipSystem.h"
static constexpr Tick kWeaponImpactDelayTicks = 5; // 0.15 s × 30 Hz, rounded to nearest
CombatSystem::CombatSystem(const GameConfig& config)
: m_config(config)
{
}
void CombatSystem::tick(Tick currentTick,
ShipSystem& ships,
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)
{
resolveStationWeapon(building, currentTick, ships, buildings, outFireEvents);
}
});
}
void CombatSystem::resolveShipWeapon(Ship& ship, Tick currentTick,
ShipSystem& ships,
BuildingSystem& buildings,
std::vector<FireEvent>& out)
{
if (!ship.weapon || !ship.threatResponse ||
!ship.threatResponse->currentTarget)
{
return;
}
Weapon& w = *ship.weapon;
// Decrement cooldown toward zero.
if (w.cooldownTicks > 0.0f)
{
w.cooldownTicks -= 1.0f;
}
if (w.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)
{
return;
}
m_pendingDamage.push_back({targetId, w.damage, currentTick + kWeaponImpactDelayTicks});
FireEvent evt;
evt.shooter = ship.id;
evt.target = targetId;
evt.emittedAt = currentTick;
out.push_back(evt);
w.cooldownTicks = static_cast<float>(kTickRateHz) / w.fireRateHz;
}
void CombatSystem::resolveStationWeapon(Building& station, Tick currentTick,
ShipSystem& ships,
BuildingSystem& buildings,
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)
{
const std::optional<QVector2D> tPos =
targetPosition(*w.currentTarget, ships, buildings);
if (!tPos || (stationCenter - *tPos).length() > w.range)
{
w.currentTarget = std::nullopt;
}
}
// Acquire a new target if needed.
if (!w.currentTarget)
{
w.currentTarget = acquireStationTarget(station, stationIsEnemy, ships);
}
if (!w.currentTarget)
{
return;
}
// Decrement cooldown.
if (w.cooldownTicks > 0.0f)
{
w.cooldownTicks -= 1.0f;
}
if (w.cooldownTicks > 0.0f)
{
return;
}
const EntityId targetId = *w.currentTarget;
const std::optional<QVector2D> tPos = targetPosition(targetId, ships, buildings);
if (!tPos)
{
w.currentTarget = std::nullopt;
return;
}
if ((stationCenter - *tPos).length() > w.range)
{
return;
}
m_pendingDamage.push_back({targetId, w.damage, currentTick + kWeaponImpactDelayTicks});
FireEvent evt;
evt.shooter = station.id;
evt.target = targetId;
evt.emittedAt = currentTick;
out.push_back(evt);
w.cooldownTicks = static_cast<float>(kTickRateHz) / w.fireRateHz;
}
std::optional<EntityId> CombatSystem::acquireStationTarget(
const Building& station, bool stationIsEnemy,
const ShipSystem& ships) const
{
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();
while (it != m_pendingDamage.end())
{
if (it->appliesAt <= currentTick)
{
if (ships.findShip(it->target))
{
ships.damageShip(it->target, it->amount);
}
else if (buildings.findBuilding(it->target))
{
buildings.damageBuilding(it->target, it->amount);
}
it = m_pendingDamage.erase(it);
}
else
{
++it;
}
}
}
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;
}