247 lines
6.4 KiB
C++
247 lines
6.4 KiB
C++
#include "CombatSystem.h"
|
|
|
|
#include "BuildingSystem.h"
|
|
#include "BuildingType.h"
|
|
#include "Ship.h"
|
|
#include "ShipSystem.h"
|
|
|
|
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;
|
|
}
|
|
|
|
// Apply damage to the correct pool.
|
|
if (ships.findShip(targetId))
|
|
{
|
|
ships.damageShip(targetId, w.damage);
|
|
}
|
|
else
|
|
{
|
|
buildings.damageBuilding(targetId, w.damage);
|
|
}
|
|
|
|
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, buildings);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (ships.findShip(targetId))
|
|
{
|
|
ships.damageShip(targetId, w.damage);
|
|
}
|
|
else
|
|
{
|
|
buildings.damageBuilding(targetId, w.damage);
|
|
}
|
|
|
|
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 BuildingSystem& buildings) 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;
|
|
}
|
|
}
|
|
|
|
// Enemy stations also target player buildings (HQ, PlayerDefenceStation).
|
|
if (stationIsEnemy)
|
|
{
|
|
for (const Building& b : buildings.allBuildings())
|
|
{
|
|
if (b.type != BuildingType::Hq &&
|
|
b.type != BuildingType::PlayerDefenceStation)
|
|
{
|
|
continue;
|
|
}
|
|
const QVector2D bCenter(b.anchor.x() + b.footprint.width() / 2.0f,
|
|
b.anchor.y() + b.footprint.height() / 2.0f);
|
|
const float dist = (bCenter - stationCenter).length();
|
|
if (dist < bestDist)
|
|
{
|
|
bestDist = dist;
|
|
best = b.id;
|
|
}
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
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;
|
|
}
|