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())};
}
}
}
});
}