split off MovementSystem and AiSystem from ShipSystem
This commit is contained in:
463
src/lib/sim/AiSystem.cpp
Normal file
463
src/lib/sim/AiSystem.cpp
Normal 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())};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user