split off MovementSystem and AiSystem from ShipSystem

This commit is contained in:
2026-05-20 22:26:45 +02:00
parent 34c6dea505
commit 452c26c8b3
12 changed files with 684 additions and 703 deletions

463
src/lib/sim/AiSystem.cpp Normal file
View File

@@ -0,0 +1,463 @@
#include "AiSystem.h"
#include <optional>
#include <vector>
#include <QVector2D>
#include "Building.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "EntityId.h"
#include "MovementIntent.h"
#include "Scrap.h"
#include "ScrapSystem.h"
#include "Ship.h"
#include "ShipSystem.h"
static QVector2D buildingCenter(const Building& b)
{
return QVector2D(b.anchor.x() + b.footprint.width() / 2.0f,
b.anchor.y() + b.footprint.height() / 2.0f);
}
static bool isTargetValid(EntityId id, float range, const Ship& ship,
const ShipSystem& ships,
const BuildingSystem& buildings)
{
if (id == kInvalidEntityId) { return false; }
const Ship* target = ships.findShip(id);
if (target) { return (target->position - ship.position).length() <= range; }
const Building* bld = buildings.findBuilding(id);
if (bld) { return (buildingCenter(*bld) - ship.position).length() <= range; }
return false;
}
// ---------------------------------------------------------------------------
// tickHomeReturn (priority 4)
// ---------------------------------------------------------------------------
void AiSystem::tickHomeReturn(ShipSystem& ships)
{
ships.forEach([&](Ship& s)
{
if (!s.homeReturn) { return; }
if (s.hp / s.maxHp < s.homeReturn->retreatHpFraction)
{
if (4 > s.intent.priority)
{
s.intent = MovementIntent{4, s.homeReturn->homePos};
}
}
});
}
// ---------------------------------------------------------------------------
// tickThreatResponse (priority 3)
// ---------------------------------------------------------------------------
void AiSystem::tickThreatResponse(ShipSystem& ships, const BuildingSystem& buildings)
{
const std::vector<Building> allBuildings = buildings.allBuildings();
const std::vector<Ship> allShips = ships.allShips();
ships.forEach([&](Ship& s)
{
if (!s.threatResponse) { return; }
const float range = s.sensorRange;
if (!s.isEnemy)
{
if (!isTargetValid(s.threatResponse->currentTarget.value_or(kInvalidEntityId),
range, s, ships, buildings))
{
s.threatResponse->currentTarget = std::nullopt;
float bestDist = range;
for (const Ship& candidate : allShips)
{
if (!candidate.isEnemy) { continue; }
float dist = (candidate.position - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = candidate.id;
}
}
for (const Building& b : allBuildings)
{
if (b.type != BuildingType::EnemyDefenceStation) { continue; }
float dist = (buildingCenter(b) - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = b.id;
}
}
}
if (s.threatResponse->currentTarget)
{
QVector2D dest;
const Ship* tShip = ships.findShip(*s.threatResponse->currentTarget);
if (tShip)
{
dest = tShip->position;
}
else
{
const Building* tBld = buildings.findBuilding(
*s.threatResponse->currentTarget);
dest = tBld ? buildingCenter(*tBld) : s.position;
}
if (3 > s.intent.priority)
{
s.intent = MovementIntent{3, dest};
}
}
else
{
if (3 > s.intent.priority)
{
if (s.rallyBehavior)
{
s.intent = MovementIntent{3, s.rallyBehavior->rallyPoint};
}
else
{
s.intent = MovementIntent{3, QVector2D(s.position.x() + 1000.0f,
s.position.y())};
}
}
}
}
else
{
if (!isTargetValid(s.threatResponse->currentTarget.value_or(kInvalidEntityId),
range, s, ships, buildings))
{
s.threatResponse->currentTarget = std::nullopt;
float bestDist = range;
for (const Ship& candidate : allShips)
{
if (candidate.isEnemy) { continue; }
float dist = (candidate.position - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = candidate.id;
}
}
for (const Building& b : allBuildings)
{
if (b.type != BuildingType::PlayerDefenceStation
&& b.type != BuildingType::Hq)
{
continue;
}
float dist = (buildingCenter(b) - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = b.id;
}
}
}
if (s.threatResponse->currentTarget)
{
QVector2D dest;
const Ship* tShip = ships.findShip(*s.threatResponse->currentTarget);
if (tShip)
{
dest = tShip->position;
}
else
{
const Building* tBld = buildings.findBuilding(
*s.threatResponse->currentTarget);
dest = tBld ? buildingCenter(*tBld) : s.position;
}
if (3 > s.intent.priority)
{
s.intent = MovementIntent{3, dest};
}
}
else
{
if (3 > s.intent.priority)
{
s.intent = MovementIntent{3, QVector2D(-10000.0f, s.position.y())};
}
}
}
});
}
// ---------------------------------------------------------------------------
// tickRepairBehavior (priority 2)
// ---------------------------------------------------------------------------
void AiSystem::tickRepairBehavior(ShipSystem& ships, BuildingSystem& buildings)
{
const std::vector<Building> allBuildings = buildings.allBuildings();
const std::vector<Ship> allShips = ships.allShips();
ships.forEach([&](Ship& s)
{
if (!s.repairBehavior || !s.repairTool) { return; }
const float repairRange = s.repairTool->range;
bool enemyNearby = false;
for (const Ship& candidate : allShips)
{
if (candidate.isEnemy
&& (candidate.position - s.position).length() <= s.sensorRange)
{
enemyNearby = true;
break;
}
}
if (enemyNearby)
{
if (2 > s.intent.priority)
{
s.intent = MovementIntent{2, QVector2D(-10000.0f, s.position.y())};
}
return;
}
EntityId currentId = s.repairBehavior->currentTarget.value_or(kInvalidEntityId);
bool targetValid = false;
if (currentId != kInvalidEntityId)
{
const Ship* tShip = ships.findShip(currentId);
if (tShip && !tShip->isEnemy && tShip->hp < tShip->maxHp)
{
targetValid = true;
}
else
{
const Building* tBld = buildings.findBuilding(currentId);
if (tBld && tBld->type == BuildingType::PlayerDefenceStation
&& tBld->hp < tBld->maxHp)
{
targetValid = true;
}
}
}
if (!targetValid)
{
s.repairBehavior->currentTarget = std::nullopt;
currentId = kInvalidEntityId;
float bestDist = s.sensorRange;
for (const Ship& candidate : allShips)
{
if (candidate.isEnemy || candidate.id == s.id
|| candidate.hp >= candidate.maxHp)
{
continue;
}
float dist = (candidate.position - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.repairBehavior->currentTarget = candidate.id;
}
}
for (const Building& b : allBuildings)
{
if (b.type != BuildingType::PlayerDefenceStation
|| b.hp >= b.maxHp)
{
continue;
}
float dist = (buildingCenter(b) - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.repairBehavior->currentTarget = b.id;
}
}
currentId = s.repairBehavior->currentTarget.value_or(kInvalidEntityId);
}
if (currentId == kInvalidEntityId)
{
if (2 > s.intent.priority)
{
s.intent = MovementIntent{2, QVector2D(s.position.x() + 1000.0f,
s.position.y())};
}
return;
}
QVector2D targetPos;
bool isShipTarget = false;
const Ship* tShip = ships.findShip(currentId);
if (tShip)
{
targetPos = tShip->position;
isShipTarget = true;
}
else
{
const Building* tBld = buildings.findBuilding(currentId);
targetPos = tBld ? buildingCenter(*tBld) : s.position;
}
float distToTarget = (targetPos - s.position).length();
if (distToTarget <= repairRange)
{
if (isShipTarget)
{
ships.healShip(currentId, s.repairTool->ratePerTick);
}
else
{
buildings.healBuilding(currentId, s.repairTool->ratePerTick);
}
}
if (2 > s.intent.priority)
{
s.intent = MovementIntent{2, targetPos};
}
});
}
// ---------------------------------------------------------------------------
// tickScrapCollector (priority 1)
// ---------------------------------------------------------------------------
void AiSystem::tickScrapCollector(ShipSystem& ships, ScrapSystem& scraps,
BuildingSystem& buildings)
{
const std::vector<Ship> allShips = ships.allShips();
ships.forEach([&](Ship& s)
{
if (!s.scrapCollector || !s.cargo) { return; }
const float collectRange = s.cargo->collectionRange;
if (s.scrapCollector->deliveryBay == kInvalidEntityId)
{
const Building* bay = buildings.findNearestBuilding(s.position,
BuildingType::SalvageBay);
if (bay)
{
s.scrapCollector->deliveryBay = bay->id;
}
}
const EntityId bayId = s.scrapCollector->deliveryBay;
QVector2D bayPos = s.position;
if (bayId != kInvalidEntityId)
{
const Building* bay = buildings.findBuilding(bayId);
if (bay)
{
bayPos = buildingCenter(*bay);
}
}
const bool cargoFull = (s.cargo->current >= s.cargo->capacity);
if (cargoFull)
{
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1, bayPos};
}
if (bayId != kInvalidEntityId
&& (s.position - bayPos).length() <= 1.0f)
{
if (buildings.deliverScrapToSalvageBay(bayId))
{
--s.cargo->current;
}
}
return;
}
bool retreating = false;
if (s.cargo->current == 0)
{
for (const Ship& candidate : allShips)
{
if (candidate.isEnemy
&& (candidate.position - s.position).length() <= collectRange)
{
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1, QVector2D(-10000.0f, s.position.y())};
}
retreating = true;
break;
}
}
}
if (retreating) { return; }
for (const Scrap& sc : scraps.allScraps())
{
if ((sc.position - s.position).length() <= collectRange)
{
if (scraps.consume(sc.id))
{
++s.cargo->current;
s.scrapCollector->scrapTarget = std::nullopt;
}
break;
}
}
if (s.scrapCollector->scrapTarget)
{
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1, *s.scrapCollector->scrapTarget};
}
}
else
{
float bestDist = s.sensorRange;
std::optional<QVector2D> bestPos;
for (const Scrap& sc : scraps.allScraps())
{
float dist = (sc.position - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
bestPos = sc.position;
}
}
if (bestPos)
{
s.scrapCollector->scrapTarget = bestPos;
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1, *bestPos};
}
}
else
{
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1, QVector2D(s.position.x() + 1000.0f,
s.position.y())};
}
}
}
});
}

14
src/lib/sim/AiSystem.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
class BuildingSystem;
class ScrapSystem;
class ShipSystem;
class AiSystem
{
public:
void tickHomeReturn(ShipSystem& ships);
void tickThreatResponse(ShipSystem& ships, const BuildingSystem& buildings);
void tickRepairBehavior(ShipSystem& ships, BuildingSystem& buildings);
void tickScrapCollector(ShipSystem& ships, ScrapSystem& scraps, BuildingSystem& buildings);
};

View File

@@ -10,6 +10,8 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/MovementSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
@@ -23,6 +25,8 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/MovementSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp

View File

@@ -0,0 +1,102 @@
#include "MovementSystem.h"
#include <algorithm>
#include <cmath>
#include <QVector2D>
#include "Ship.h"
#include "ShipSystem.h"
static float wrapAngle(float a)
{
constexpr float kPi = 3.14159265f;
a = std::fmod(a, 2.0f * kPi);
if (a > kPi) { a -= 2.0f * kPi; }
if (a < -kPi) { a += 2.0f * kPi; }
return a;
}
void MovementSystem::tick(ShipSystem& ships)
{
ships.forEach([&](Ship& s)
{
if (s.intent.priority == 0)
{
s.velocity = QVector2D(0.0f, 0.0f);
s.rotationSpeed = 0.0f;
return;
}
const QVector2D delta = s.intent.target - s.position;
const float dist = delta.length();
if (dist < 0.001f)
{
s.velocity = QVector2D(0.0f, 0.0f);
return;
}
// ── Rotate toward target ──────────────────────────────────────────
const float desiredAngle = std::atan2(delta.y(), delta.x());
const float angleDiff = wrapAngle(desiredAngle - s.facing);
const float rotDelta = std::max(-s.angularAccelerationPerTick,
std::min(angleDiff, s.angularAccelerationPerTick));
s.rotationSpeed += rotDelta;
s.rotationSpeed = std::max(-s.maxRotationSpeedPerTick,
std::min(s.rotationSpeed, s.maxRotationSpeedPerTick));
const bool sameSign = (s.rotationSpeed >= 0.0f) == (angleDiff >= 0.0f);
if (sameSign && std::abs(s.rotationSpeed) > std::abs(angleDiff))
{
s.rotationSpeed = angleDiff;
}
s.facing = wrapAngle(s.facing + s.rotationSpeed);
// ── Desired velocity (with braking near target) ───────────────────
const float manAccel = s.maneuveringAccelerationPerTick;
const float stoppingDist = (s.maxSpeedPerTick * s.maxSpeedPerTick)
/ (2.0f * manAccel);
const float desiredSpeed = (dist <= stoppingDist)
? std::sqrt(2.0f * manAccel * dist)
: s.maxSpeedPerTick;
const QVector2D desiredVel = delta.normalized() * desiredSpeed;
const QVector2D velError = desiredVel - s.velocity;
// ── Main acceleration: forward only, along facing ─────────────────
const QVector2D facingVec(std::cos(s.facing), std::sin(s.facing));
const float mainAligned = std::max(0.0f,
QVector2D::dotProduct(velError, facingVec));
const float mainApplied = std::min(mainAligned, s.mainAccelerationPerTick);
const QVector2D mainDelta = facingVec * mainApplied;
// ── Maneuvering acceleration: any direction, handles the remainder ─
const QVector2D remaining = velError - mainDelta;
const float remainLen = remaining.length();
const QVector2D maneuverDelta = (remainLen > manAccel)
? remaining.normalized() * manAccel
: remaining;
s.velocity += mainDelta + maneuverDelta;
// ── Speed cap ─────────────────────────────────────────────────────
const float speed = s.velocity.length();
if (speed > s.maxSpeedPerTick)
{
s.velocity = s.velocity.normalized() * s.maxSpeedPerTick;
}
// ── Snap to target or advance ─────────────────────────────────────
if (dist <= s.velocity.length())
{
s.position = s.intent.target;
s.velocity = QVector2D(0.0f, 0.0f);
}
else
{
s.position += s.velocity;
}
});
}

View File

@@ -0,0 +1,9 @@
#pragma once
class ShipSystem;
class MovementSystem
{
public:
void tick(ShipSystem& ships);
};

View File

@@ -2,17 +2,10 @@
#include <algorithm>
#include <cassert>
#include <cmath>
#include <limits>
#include <map>
#include <utility>
#include "Building.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "ModulesConfig.h"
#include "Scrap.h"
#include "ScrapSystem.h"
#include "Tick.h"
ShipSystem::ShipSystem(const GameConfig& config,
@@ -217,38 +210,6 @@ void ShipSystem::forEach(std::function<void(Ship&)> fn)
}
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
static QVector2D buildingCenter(const Building& b)
{
return QVector2D(b.anchor.x() + b.footprint.width() / 2.0f,
b.anchor.y() + b.footprint.height() / 2.0f);
}
bool ShipSystem::isTargetValid(EntityId id, float range, const Ship& ship,
const BuildingSystem& buildings) const
{
if (id == kInvalidEntityId)
{
return false;
}
// Check ship pool first.
const Ship* target = findShip(id);
if (target)
{
return (target->position - ship.position).length() <= range;
}
// Check building pool (HQ and defence stations are targetable).
const Building* bld = buildings.findBuilding(id);
if (bld)
{
return (buildingCenter(*bld) - ship.position).length() <= range;
}
return false;
}
bool ShipSystem::healShip(EntityId id, float amount)
{
for (Ship& s : m_ships)
@@ -287,474 +248,6 @@ void ShipSystem::clearMovementIntents()
}
}
// ---------------------------------------------------------------------------
// tickHomeReturn (priority 4)
// ---------------------------------------------------------------------------
void ShipSystem::tickHomeReturn()
{
for (Ship& s : m_ships)
{
if (!s.homeReturn)
{
continue;
}
if (s.hp / s.maxHp < s.homeReturn->retreatHpFraction)
{
if (4 > s.intent.priority)
{
s.intent = MovementIntent{4, s.homeReturn->homePos};
}
}
}
}
// ---------------------------------------------------------------------------
// tickThreatResponse (priority 3)
// ---------------------------------------------------------------------------
void ShipSystem::tickThreatResponse(const BuildingSystem& buildings)
{
// Snapshot all buildings once (used for enemy targeting).
const std::vector<Building> allBuildings = buildings.allBuildings();
for (Ship& s : m_ships)
{
if (!s.threatResponse)
{
continue;
}
const float range = s.sensorRange;
if (!s.isEnemy)
{
// Player combat ship: target nearest enemy ship or enemy building.
if (!isTargetValid(s.threatResponse->currentTarget.value_or(kInvalidEntityId),
range, s, buildings))
{
s.threatResponse->currentTarget = std::nullopt;
float bestDist = range;
for (const Ship& candidate : m_ships)
{
if (!candidate.isEnemy)
{
continue;
}
float dist = (candidate.position - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = candidate.id;
}
}
for (const Building& b : allBuildings)
{
if (b.type != BuildingType::EnemyDefenceStation)
{
continue;
}
float dist = (buildingCenter(b) - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = b.id;
}
}
}
if (s.threatResponse->currentTarget)
{
QVector2D dest;
const Ship* tShip = findShip(*s.threatResponse->currentTarget);
if (tShip)
{
dest = tShip->position;
}
else
{
const Building* tBld = buildings.findBuilding(
*s.threatResponse->currentTarget);
dest = tBld ? buildingCenter(*tBld) : s.position;
}
if (3 > s.intent.priority)
{
s.intent = MovementIntent{3, dest};
}
}
else
{
// No target: gather at rally point or patrol rightward once departed.
if (3 > s.intent.priority)
{
if (s.rallyBehavior)
{
s.intent = MovementIntent{3, s.rallyBehavior->rallyPoint};
}
else
{
s.intent = MovementIntent{3, QVector2D(s.position.x() + 1000.0f,
s.position.y())};
}
}
}
}
else
{
// Enemy ship: target nearest player ship or player building.
if (!isTargetValid(s.threatResponse->currentTarget.value_or(kInvalidEntityId),
range, s, buildings))
{
s.threatResponse->currentTarget = std::nullopt;
float bestDist = range;
for (const Ship& candidate : m_ships)
{
if (candidate.isEnemy)
{
continue;
}
float dist = (candidate.position - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = candidate.id;
}
}
for (const Building& b : allBuildings)
{
if (b.type != BuildingType::PlayerDefenceStation
&& b.type != BuildingType::Hq)
{
continue;
}
float dist = (buildingCenter(b) - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.threatResponse->currentTarget = b.id;
}
}
}
if (s.threatResponse->currentTarget)
{
// Move toward target (building or ship).
QVector2D dest;
const Ship* tShip = findShip(*s.threatResponse->currentTarget);
if (tShip)
{
dest = tShip->position;
}
else
{
const Building* tBld = buildings.findBuilding(
*s.threatResponse->currentTarget);
dest = tBld ? buildingCenter(*tBld) : s.position;
}
if (3 > s.intent.priority)
{
s.intent = MovementIntent{3, dest};
}
}
else
{
// No target: move toward asteroid (leftward).
if (3 > s.intent.priority)
{
s.intent = MovementIntent{3, QVector2D(-10000.0f, s.position.y())};
}
}
}
}
}
// ---------------------------------------------------------------------------
// tickRepairBehavior (priority 2)
// ---------------------------------------------------------------------------
void ShipSystem::tickRepairBehavior(BuildingSystem& buildings)
{
const std::vector<Building> allBuildings = buildings.allBuildings();
for (Ship& s : m_ships)
{
if (!s.repairBehavior || !s.repairTool)
{
continue;
}
const float repairRange = s.repairTool->range;
// Check for nearby enemies; if present, retreat.
bool enemyNearby = false;
for (const Ship& candidate : m_ships)
{
if (candidate.isEnemy
&& (candidate.position - s.position).length() <= s.sensorRange)
{
enemyNearby = true;
break;
}
}
if (enemyNearby)
{
if (2 > s.intent.priority)
{
s.intent = MovementIntent{2, QVector2D(-10000.0f, s.position.y())};
}
continue;
}
// Validate current repair target.
EntityId currentId = s.repairBehavior->currentTarget.value_or(kInvalidEntityId);
bool targetValid = false;
if (currentId != kInvalidEntityId)
{
const Ship* tShip = findShip(currentId);
if (tShip && !tShip->isEnemy && tShip->hp < tShip->maxHp)
{
targetValid = true;
}
else
{
const Building* tBld = buildings.findBuilding(currentId);
if (tBld && tBld->type == BuildingType::PlayerDefenceStation
&& tBld->hp < tBld->maxHp)
{
targetValid = true;
}
}
}
if (!targetValid)
{
s.repairBehavior->currentTarget = std::nullopt;
currentId = kInvalidEntityId;
float bestDist = s.sensorRange;
for (const Ship& candidate : m_ships)
{
if (candidate.isEnemy || candidate.id == s.id
|| candidate.hp >= candidate.maxHp)
{
continue;
}
float dist = (candidate.position - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.repairBehavior->currentTarget = candidate.id;
}
}
for (const Building& b : allBuildings)
{
if (b.type != BuildingType::PlayerDefenceStation
|| b.hp >= b.maxHp)
{
continue;
}
float dist = (buildingCenter(b) - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
s.repairBehavior->currentTarget = b.id;
}
}
currentId = s.repairBehavior->currentTarget.value_or(kInvalidEntityId);
}
if (currentId == kInvalidEntityId)
{
// No target: patrol rightward.
if (2 > s.intent.priority)
{
s.intent = MovementIntent{2, QVector2D(s.position.x() + 1000.0f,
s.position.y())};
}
continue;
}
// Compute target position and whether we are in repair range.
QVector2D targetPos;
bool isShipTarget = false;
const Ship* tShip = findShip(currentId);
if (tShip)
{
targetPos = tShip->position;
isShipTarget = true;
}
else
{
const Building* tBld = buildings.findBuilding(currentId);
targetPos = tBld ? buildingCenter(*tBld) : s.position;
}
float distToTarget = (targetPos - s.position).length();
if (distToTarget <= repairRange)
{
if (isShipTarget)
{
healShip(currentId, s.repairTool->ratePerTick);
}
else
{
buildings.healBuilding(currentId, s.repairTool->ratePerTick);
}
}
if (2 > s.intent.priority)
{
s.intent = MovementIntent{2, targetPos};
}
}
}
// ---------------------------------------------------------------------------
// tickScrapCollector (priority 1)
// ---------------------------------------------------------------------------
void ShipSystem::tickScrapCollector(ScrapSystem& scraps, const BuildingSystem& buildings)
{
for (Ship& s : m_ships)
{
if (!s.scrapCollector || !s.cargo)
{
continue;
}
const float collectRange = s.cargo->collectionRange;
// Assign delivery bay if not yet set.
if (s.scrapCollector->deliveryBay == kInvalidEntityId)
{
const Building* bay = buildings.findNearestBuilding(s.position,
BuildingType::SalvageBay);
if (bay)
{
s.scrapCollector->deliveryBay = bay->id;
}
}
const EntityId bayId = s.scrapCollector->deliveryBay;
// Compute bay position for movement.
QVector2D bayPos = s.position;
if (bayId != kInvalidEntityId)
{
const Building* bay = buildings.findBuilding(bayId);
if (bay)
{
bayPos = buildingCenter(*bay);
}
}
const bool cargoFull = (s.cargo->current >= s.cargo->capacity);
if (cargoFull)
{
// Return to bay and deliver.
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1, bayPos};
}
if (bayId != kInvalidEntityId
&& (s.position - bayPos).length() <= 1.0f)
{
// Deliver one item per tick.
if (const_cast<BuildingSystem&>(buildings).deliverScrapToSalvageBay(bayId))
{
--s.cargo->current;
}
}
continue;
}
// Retreat from enemies when not carrying cargo.
bool retreating = false;
if (s.cargo->current == 0)
{
for (const Ship& candidate : m_ships)
{
if (candidate.isEnemy
&& (candidate.position - s.position).length() <= collectRange)
{
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1,
QVector2D(-10000.0f, s.position.y())};
}
retreating = true;
break;
}
}
}
if (retreating)
{
continue;
}
// Collect scrap if within range.
for (const Scrap& sc : scraps.allScraps())
{
if ((sc.position - s.position).length() <= collectRange)
{
if (scraps.consume(sc.id))
{
++s.cargo->current;
s.scrapCollector->scrapTarget = std::nullopt;
}
break;
}
}
if (s.scrapCollector->scrapTarget)
{
// Move toward known scrap target.
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1, *s.scrapCollector->scrapTarget};
}
}
else
{
// Scan for nearest scrap within sensor range.
float bestDist = s.sensorRange;
std::optional<QVector2D> bestPos;
for (const Scrap& sc : scraps.allScraps())
{
float dist = (sc.position - s.position).length();
if (dist < bestDist)
{
bestDist = dist;
bestPos = sc.position;
}
}
if (bestPos)
{
s.scrapCollector->scrapTarget = bestPos;
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1, *bestPos};
}
}
else
{
// No scrap in range: patrol rightward.
if (1 > s.intent.priority)
{
s.intent = MovementIntent{1, QVector2D(s.position.x() + 1000.0f,
s.position.y())};
}
}
}
}
}
// ---------------------------------------------------------------------------
// Rally point management (REQ-SHP-RALLY)
// ---------------------------------------------------------------------------
@@ -774,104 +267,3 @@ void ShipSystem::triggerRallyDeparture()
}
}
}
// ---------------------------------------------------------------------------
// tickMovement (tick-order step 10)
// ---------------------------------------------------------------------------
// Reduces angle to [-π, π].
static float wrapAngle(float a)
{
constexpr float kPi = 3.14159265f;
a = std::fmod(a, 2.0f * kPi);
if (a > kPi) { a -= 2.0f * kPi; }
if (a < -kPi) { a += 2.0f * kPi; }
return a;
}
void ShipSystem::tickMovement()
{
for (Ship& s : m_ships)
{
if (s.intent.priority == 0)
{
s.velocity = QVector2D(0.0f, 0.0f);
s.rotationSpeed = 0.0f;
continue;
}
const QVector2D delta = s.intent.target - s.position;
const float dist = delta.length();
if (dist < 0.001f)
{
s.velocity = QVector2D(0.0f, 0.0f);
continue;
}
// ── Rotate toward target ──────────────────────────────────────────
const float desiredAngle = std::atan2(delta.y(), delta.x());
const float angleDiff = wrapAngle(desiredAngle - s.facing);
// Clamp angular acceleration, accumulate rotation speed.
const float rotDelta = std::max(-s.angularAccelerationPerTick,
std::min(angleDiff, s.angularAccelerationPerTick));
s.rotationSpeed += rotDelta;
s.rotationSpeed = std::max(-s.maxRotationSpeedPerTick,
std::min(s.rotationSpeed, s.maxRotationSpeedPerTick));
// Prevent overshooting the desired angle this tick.
const bool sameSign = (s.rotationSpeed >= 0.0f) == (angleDiff >= 0.0f);
if (sameSign && std::abs(s.rotationSpeed) > std::abs(angleDiff))
{
s.rotationSpeed = angleDiff;
}
s.facing = wrapAngle(s.facing + s.rotationSpeed);
// ── Desired velocity (with braking near target) ───────────────────
// Stopping distance using maneuvering acceleration as the worst-case brake.
const float manAccel = s.maneuveringAccelerationPerTick;
const float stoppingDist = (s.maxSpeedPerTick * s.maxSpeedPerTick)
/ (2.0f * manAccel);
const float desiredSpeed = (dist <= stoppingDist)
? std::sqrt(2.0f * manAccel * dist)
: s.maxSpeedPerTick;
const QVector2D desiredVel = delta.normalized() * desiredSpeed;
const QVector2D velError = desiredVel - s.velocity;
// ── Main acceleration: forward only, along facing ─────────────────
const QVector2D facingVec(std::cos(s.facing), std::sin(s.facing));
const float mainAligned = std::max(0.0f,
QVector2D::dotProduct(velError, facingVec));
const float mainApplied = std::min(mainAligned, s.mainAccelerationPerTick);
const QVector2D mainDelta = facingVec * mainApplied;
// ── Maneuvering acceleration: any direction, handles the remainder ─
const QVector2D remaining = velError - mainDelta;
const float remainLen = remaining.length();
const QVector2D maneuverDelta = (remainLen > manAccel)
? remaining.normalized() * manAccel
: remaining;
s.velocity += mainDelta + maneuverDelta;
// ── Speed cap ─────────────────────────────────────────────────────
const float speed = s.velocity.length();
if (speed > s.maxSpeedPerTick)
{
s.velocity = s.velocity.normalized() * s.maxSpeedPerTick;
}
// ── Snap to target or advance ─────────────────────────────────────
if (dist <= s.velocity.length())
{
s.position = s.intent.target;
s.velocity = QVector2D(0.0f, 0.0f);
}
else
{
s.position += s.velocity;
}
}
}

View File

@@ -10,9 +10,6 @@
#include "Ship.h"
#include "ShipLayout.h"
class BuildingSystem;
class ScrapSystem;
class ShipSystem
{
public:
@@ -29,26 +26,9 @@ public:
std::vector<Ship> allShips() const;
void forEach(std::function<void(Ship&)> fn);
// -- Behavior tick methods (tick-order step 7) ---------------------------
// Reset all movement intents to priority 0 before behavior systems run.
void clearMovementIntents();
// Priority 4: low-HP ships retreat to homePos.
void tickHomeReturn();
// Priority 3: combat ships acquire targets and advance toward them.
void tickThreatResponse(const BuildingSystem& buildings);
// Priority 2: repair ships find and heal damaged friendly ships/stations.
void tickRepairBehavior(BuildingSystem& buildings);
// Priority 1: salvage ships collect scrap and deliver it.
void tickScrapCollector(ScrapSystem& scraps, const BuildingSystem& buildings);
// -- Movement (tick-order step 10) ---------------------------------------
void tickMovement();
// Set the rally point that newly spawned player combat ships will loiter at.
void setRallyPoint(QVector2D point);
@@ -59,19 +39,14 @@ public:
// Returns false if ship not found.
bool damageShip(EntityId id, float amount);
private:
const ShipDef* findShipDef(const std::string& schematicId) const;
const ModuleDef* findModuleDef(const std::string& id) const;
// True if the entity identified by id is alive and within range of ship.
// Searches both the ship list and (for buildings) the supplied BuildingSystem.
bool isTargetValid(EntityId id, float range, const Ship& ship,
const BuildingSystem& buildings) const;
// Heal the ship with the given id by amount, clamped to maxHp.
// Returns false if the ship is not found.
bool healShip(EntityId id, float amount);
private:
const ShipDef* findShipDef(const std::string& schematicId) const;
const ModuleDef* findModuleDef(const std::string& id) const;
const GameConfig& m_config;
std::function<EntityId()> m_allocateId;
std::vector<Ship> m_ships;

View File

@@ -2,8 +2,10 @@
#include <cassert>
#include "AiSystem.h"
#include "BuildingSystem.h"
#include "CombatSystem.h"
#include "MovementSystem.h"
#include "ScrapSystem.h"
#include "ShipSystem.h"
#include "SurfaceMask.h"
@@ -41,10 +43,12 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
},
m_rng);
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
m_combatSystem = std::make_unique<CombatSystem>(m_config);
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
m_aiSystem = std::make_unique<AiSystem>();
m_movementSystem = std::make_unique<MovementSystem>();
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
m_combatSystem = std::make_unique<CombatSystem>(m_config);
// Initialize schematic unlock state.
for (const ShipDef& def : m_config.ships.ships)
@@ -104,10 +108,12 @@ void Simulation::reset(unsigned int seed)
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
},
m_rng);
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
m_combatSystem = std::make_unique<CombatSystem>(m_config);
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
m_aiSystem = std::make_unique<AiSystem>();
m_movementSystem = std::make_unique<MovementSystem>();
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
m_combatSystem = std::make_unique<CombatSystem>(m_config);
m_schematicLevels.clear();
for (const ShipDef& def : m_config.ships.ships)
@@ -152,10 +158,10 @@ void Simulation::tick()
}
m_shipSystem->clearMovementIntents();
m_shipSystem->tickHomeReturn(); // priority 4
m_shipSystem->tickThreatResponse(*m_buildingSystem); // priority 3
m_shipSystem->tickRepairBehavior(*m_buildingSystem); // priority 2
m_shipSystem->tickScrapCollector(*m_scrapSystem, *m_buildingSystem); // priority 1
m_aiSystem->tickHomeReturn(*m_shipSystem); // priority 4
m_aiSystem->tickThreatResponse(*m_shipSystem, *m_buildingSystem); // priority 3
m_aiSystem->tickRepairBehavior(*m_shipSystem, *m_buildingSystem); // priority 2
m_aiSystem->tickScrapCollector(*m_shipSystem, *m_scrapSystem, *m_buildingSystem); // priority 1
// Step 8: combat resolution
m_combatSystem->tick(m_currentTick, *m_shipSystem,
@@ -171,7 +177,7 @@ void Simulation::tick()
}
// Step 10: advance ship positions
m_shipSystem->tickMovement();
m_movementSystem->tick(*m_shipSystem);
// Step 11: scrap despawn
m_scrapSystem->tickDespawn(m_currentTick);

View File

@@ -17,8 +17,10 @@
#include "Rotation.h"
#include "Tick.h"
class AiSystem;
class BuildingSystem;
class CombatSystem;
class MovementSystem;
class ShipSystem;
class ScrapSystem;
class WaveSystem;
@@ -111,12 +113,14 @@ private:
};
std::map<std::string, SchematicState> m_schematicLevels;
BeltSystem m_beltSystem;
std::unique_ptr<BuildingSystem> m_buildingSystem;
std::unique_ptr<ShipSystem> m_shipSystem;
std::unique_ptr<ScrapSystem> m_scrapSystem;
std::unique_ptr<WaveSystem> m_waveSystem;
std::unique_ptr<CombatSystem> m_combatSystem;
BeltSystem m_beltSystem;
std::unique_ptr<BuildingSystem> m_buildingSystem;
std::unique_ptr<ShipSystem> m_shipSystem;
std::unique_ptr<AiSystem> m_aiSystem;
std::unique_ptr<MovementSystem> m_movementSystem;
std::unique_ptr<ScrapSystem> m_scrapSystem;
std::unique_ptr<WaveSystem> m_waveSystem;
std::unique_ptr<CombatSystem> m_combatSystem;
std::vector<FireEvent> m_fireEvents;
std::vector<SchematicDropEvent> m_schematicDropEvents;