split off MovementSystem and AiSystem from ShipSystem
This commit is contained in:
@@ -5,10 +5,12 @@
|
|||||||
|
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
|
#include "AiSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "CombatSystem.h"
|
#include "CombatSystem.h"
|
||||||
|
#include "MovementSystem.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "Ship.h"
|
#include "Ship.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
@@ -42,8 +44,10 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
|||||||
m_shipSystem = std::make_unique<ShipSystem>(
|
m_shipSystem = std::make_unique<ShipSystem>(
|
||||||
m_gameConfig, [this]() { return allocateId(); });
|
m_gameConfig, [this]() { return allocateId(); });
|
||||||
|
|
||||||
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
|
m_aiSystem = std::make_unique<AiSystem>();
|
||||||
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
|
m_movementSystem = std::make_unique<MovementSystem>();
|
||||||
|
m_combatSystem = std::make_unique<CombatSystem>(m_gameConfig);
|
||||||
|
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
|
||||||
|
|
||||||
placeStructures();
|
placeStructures();
|
||||||
spawnShips();
|
spawnShips();
|
||||||
@@ -250,10 +254,10 @@ void ArenaSimulation::tick()
|
|||||||
{
|
{
|
||||||
// Ship behavior systems (tick step 7).
|
// Ship behavior systems (tick step 7).
|
||||||
m_shipSystem->clearMovementIntents();
|
m_shipSystem->clearMovementIntents();
|
||||||
m_shipSystem->tickHomeReturn();
|
m_aiSystem->tickHomeReturn(*m_shipSystem);
|
||||||
m_shipSystem->tickThreatResponse(*m_buildingSystem);
|
m_aiSystem->tickThreatResponse(*m_shipSystem, *m_buildingSystem);
|
||||||
m_shipSystem->tickRepairBehavior(*m_buildingSystem);
|
m_aiSystem->tickRepairBehavior(*m_shipSystem, *m_buildingSystem);
|
||||||
m_shipSystem->tickScrapCollector(*m_scrapSystem, *m_buildingSystem);
|
m_aiSystem->tickScrapCollector(*m_shipSystem, *m_scrapSystem, *m_buildingSystem);
|
||||||
|
|
||||||
// Combat resolution (tick step 8).
|
// Combat resolution (tick step 8).
|
||||||
std::vector<FireEvent> fireEvents;
|
std::vector<FireEvent> fireEvents;
|
||||||
@@ -265,7 +269,7 @@ void ArenaSimulation::tick()
|
|||||||
tickDeaths();
|
tickDeaths();
|
||||||
|
|
||||||
// Movement (tick step 10).
|
// Movement (tick step 10).
|
||||||
m_shipSystem->tickMovement();
|
m_movementSystem->tick(*m_shipSystem);
|
||||||
|
|
||||||
// Scrap despawn (tick step 11).
|
// Scrap despawn (tick step 11).
|
||||||
m_scrapSystem->tickDespawn(m_currentTick);
|
m_scrapSystem->tickDespawn(m_currentTick);
|
||||||
|
|||||||
@@ -14,8 +14,10 @@
|
|||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
|
class AiSystem;
|
||||||
class BuildingSystem;
|
class BuildingSystem;
|
||||||
class CombatSystem;
|
class CombatSystem;
|
||||||
|
class MovementSystem;
|
||||||
class ShipSystem;
|
class ShipSystem;
|
||||||
class ScrapSystem;
|
class ScrapSystem;
|
||||||
|
|
||||||
@@ -81,9 +83,11 @@ private:
|
|||||||
|
|
||||||
BeltSystem m_beltSystem;
|
BeltSystem m_beltSystem;
|
||||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||||
std::unique_ptr<CombatSystem> m_combatSystem;
|
std::unique_ptr<AiSystem> m_aiSystem;
|
||||||
std::unique_ptr<ScrapSystem> m_scrapSystem;
|
std::unique_ptr<MovementSystem> m_movementSystem;
|
||||||
|
std::unique_ptr<CombatSystem> m_combatSystem;
|
||||||
|
std::unique_ptr<ScrapSystem> m_scrapSystem;
|
||||||
|
|
||||||
EntityId m_team1HqId;
|
EntityId m_team1HqId;
|
||||||
EntityId m_team2HqId;
|
EntityId m_team2HqId;
|
||||||
|
|||||||
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())};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
14
src/lib/sim/AiSystem.h
Normal file
14
src/lib/sim/AiSystem.h
Normal 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);
|
||||||
|
};
|
||||||
@@ -10,6 +10,8 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.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}/ScrapSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
|
||||||
@@ -23,6 +25,8 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.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}/ScrapSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp
|
||||||
|
|||||||
102
src/lib/sim/MovementSystem.cpp
Normal file
102
src/lib/sim/MovementSystem.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
9
src/lib/sim/MovementSystem.h
Normal file
9
src/lib/sim/MovementSystem.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class ShipSystem;
|
||||||
|
|
||||||
|
class MovementSystem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void tick(ShipSystem& ships);
|
||||||
|
};
|
||||||
@@ -2,17 +2,10 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cmath>
|
|
||||||
#include <limits>
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "Building.h"
|
|
||||||
#include "BuildingSystem.h"
|
|
||||||
#include "BuildingType.h"
|
|
||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
#include "Scrap.h"
|
|
||||||
#include "ScrapSystem.h"
|
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
ShipSystem::ShipSystem(const GameConfig& config,
|
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)
|
bool ShipSystem::healShip(EntityId id, float amount)
|
||||||
{
|
{
|
||||||
for (Ship& s : m_ships)
|
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)
|
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,9 +10,6 @@
|
|||||||
#include "Ship.h"
|
#include "Ship.h"
|
||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
class BuildingSystem;
|
|
||||||
class ScrapSystem;
|
|
||||||
|
|
||||||
class ShipSystem
|
class ShipSystem
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -29,26 +26,9 @@ public:
|
|||||||
std::vector<Ship> allShips() const;
|
std::vector<Ship> allShips() const;
|
||||||
void forEach(std::function<void(Ship&)> fn);
|
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.
|
// Reset all movement intents to priority 0 before behavior systems run.
|
||||||
void clearMovementIntents();
|
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.
|
// Set the rally point that newly spawned player combat ships will loiter at.
|
||||||
void setRallyPoint(QVector2D point);
|
void setRallyPoint(QVector2D point);
|
||||||
|
|
||||||
@@ -59,19 +39,14 @@ public:
|
|||||||
// Returns false if ship not found.
|
// Returns false if ship not found.
|
||||||
bool damageShip(EntityId id, float amount);
|
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.
|
// Heal the ship with the given id by amount, clamped to maxHp.
|
||||||
// Returns false if the ship is not found.
|
// Returns false if the ship is not found.
|
||||||
bool healShip(EntityId id, float amount);
|
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;
|
const GameConfig& m_config;
|
||||||
std::function<EntityId()> m_allocateId;
|
std::function<EntityId()> m_allocateId;
|
||||||
std::vector<Ship> m_ships;
|
std::vector<Ship> m_ships;
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
|
#include "AiSystem.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "CombatSystem.h"
|
#include "CombatSystem.h"
|
||||||
|
#include "MovementSystem.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
#include "SurfaceMask.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_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
|
||||||
},
|
},
|
||||||
m_rng);
|
m_rng);
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
||||||
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
|
m_aiSystem = std::make_unique<AiSystem>();
|
||||||
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
m_movementSystem = std::make_unique<MovementSystem>();
|
||||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
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.
|
// Initialize schematic unlock state.
|
||||||
for (const ShipDef& def : m_config.ships.ships)
|
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_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
|
||||||
},
|
},
|
||||||
m_rng);
|
m_rng);
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
||||||
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
|
m_aiSystem = std::make_unique<AiSystem>();
|
||||||
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
m_movementSystem = std::make_unique<MovementSystem>();
|
||||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
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();
|
m_schematicLevels.clear();
|
||||||
for (const ShipDef& def : m_config.ships.ships)
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
@@ -152,10 +158,10 @@ void Simulation::tick()
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_shipSystem->clearMovementIntents();
|
m_shipSystem->clearMovementIntents();
|
||||||
m_shipSystem->tickHomeReturn(); // priority 4
|
m_aiSystem->tickHomeReturn(*m_shipSystem); // priority 4
|
||||||
m_shipSystem->tickThreatResponse(*m_buildingSystem); // priority 3
|
m_aiSystem->tickThreatResponse(*m_shipSystem, *m_buildingSystem); // priority 3
|
||||||
m_shipSystem->tickRepairBehavior(*m_buildingSystem); // priority 2
|
m_aiSystem->tickRepairBehavior(*m_shipSystem, *m_buildingSystem); // priority 2
|
||||||
m_shipSystem->tickScrapCollector(*m_scrapSystem, *m_buildingSystem); // priority 1
|
m_aiSystem->tickScrapCollector(*m_shipSystem, *m_scrapSystem, *m_buildingSystem); // priority 1
|
||||||
|
|
||||||
// Step 8: combat resolution
|
// Step 8: combat resolution
|
||||||
m_combatSystem->tick(m_currentTick, *m_shipSystem,
|
m_combatSystem->tick(m_currentTick, *m_shipSystem,
|
||||||
@@ -171,7 +177,7 @@ void Simulation::tick()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 10: advance ship positions
|
// Step 10: advance ship positions
|
||||||
m_shipSystem->tickMovement();
|
m_movementSystem->tick(*m_shipSystem);
|
||||||
|
|
||||||
// Step 11: scrap despawn
|
// Step 11: scrap despawn
|
||||||
m_scrapSystem->tickDespawn(m_currentTick);
|
m_scrapSystem->tickDespawn(m_currentTick);
|
||||||
|
|||||||
@@ -17,8 +17,10 @@
|
|||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
|
class AiSystem;
|
||||||
class BuildingSystem;
|
class BuildingSystem;
|
||||||
class CombatSystem;
|
class CombatSystem;
|
||||||
|
class MovementSystem;
|
||||||
class ShipSystem;
|
class ShipSystem;
|
||||||
class ScrapSystem;
|
class ScrapSystem;
|
||||||
class WaveSystem;
|
class WaveSystem;
|
||||||
@@ -111,12 +113,14 @@ private:
|
|||||||
};
|
};
|
||||||
std::map<std::string, SchematicState> m_schematicLevels;
|
std::map<std::string, SchematicState> m_schematicLevels;
|
||||||
|
|
||||||
BeltSystem m_beltSystem;
|
BeltSystem m_beltSystem;
|
||||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||||
std::unique_ptr<ScrapSystem> m_scrapSystem;
|
std::unique_ptr<AiSystem> m_aiSystem;
|
||||||
std::unique_ptr<WaveSystem> m_waveSystem;
|
std::unique_ptr<MovementSystem> m_movementSystem;
|
||||||
std::unique_ptr<CombatSystem> m_combatSystem;
|
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<FireEvent> m_fireEvents;
|
||||||
std::vector<SchematicDropEvent> m_schematicDropEvents;
|
std::vector<SchematicDropEvent> m_schematicDropEvents;
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
|
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
|
#include "AiSystem.h"
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
|
#include "MovementSystem.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "Scrap.h"
|
#include "Scrap.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
@@ -27,15 +29,17 @@ static GameConfig loadConfig()
|
|||||||
|
|
||||||
struct Fixture
|
struct Fixture
|
||||||
{
|
{
|
||||||
GameConfig cfg;
|
GameConfig cfg;
|
||||||
BeltSystem belts;
|
BeltSystem belts;
|
||||||
EntityId nextId;
|
EntityId nextId;
|
||||||
int stock;
|
int stock;
|
||||||
std::mt19937 rng;
|
std::mt19937 rng;
|
||||||
BuildingSystem buildings;
|
BuildingSystem buildings;
|
||||||
ShipSystem ships;
|
ShipSystem ships;
|
||||||
ScrapSystem scraps;
|
AiSystem ai;
|
||||||
Tick tick;
|
MovementSystem movement;
|
||||||
|
ScrapSystem scraps;
|
||||||
|
Tick tick;
|
||||||
|
|
||||||
explicit Fixture()
|
explicit Fixture()
|
||||||
: cfg(loadConfig())
|
: cfg(loadConfig())
|
||||||
@@ -58,11 +62,11 @@ struct Fixture
|
|||||||
void runBehaviorTick()
|
void runBehaviorTick()
|
||||||
{
|
{
|
||||||
ships.clearMovementIntents();
|
ships.clearMovementIntents();
|
||||||
ships.tickHomeReturn();
|
ai.tickHomeReturn(ships);
|
||||||
ships.tickThreatResponse(buildings);
|
ai.tickThreatResponse(ships, buildings);
|
||||||
ships.tickRepairBehavior(buildings);
|
ai.tickRepairBehavior(ships, buildings);
|
||||||
ships.tickScrapCollector(scraps, buildings);
|
ai.tickScrapCollector(ships, scraps, buildings);
|
||||||
ships.tickMovement();
|
movement.tick(ships);
|
||||||
++tick;
|
++tick;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -107,7 +111,7 @@ TEST_CASE("BehaviorSystem: tickMovement advances ship by maxSpeedPerTick toward
|
|||||||
f.ships.forEach([&target](Ship& s) {
|
f.ships.forEach([&target](Ship& s) {
|
||||||
s.intent = MovementIntent{1, target};
|
s.intent = MovementIntent{1, target};
|
||||||
});
|
});
|
||||||
f.ships.tickMovement();
|
f.movement.tick(f.ships);
|
||||||
|
|
||||||
const Ship* s = f.ships.findShip(id);
|
const Ship* s = f.ships.findShip(id);
|
||||||
REQUIRE(s->position.x() == Approx(speed));
|
REQUIRE(s->position.x() == Approx(speed));
|
||||||
@@ -129,7 +133,7 @@ TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoo
|
|||||||
f.ships.forEach([&target](Ship& s) {
|
f.ships.forEach([&target](Ship& s) {
|
||||||
s.intent = MovementIntent{1, target};
|
s.intent = MovementIntent{1, target};
|
||||||
});
|
});
|
||||||
f.ships.tickMovement();
|
f.movement.tick(f.ships);
|
||||||
|
|
||||||
const Ship* s = f.ships.findShip(id);
|
const Ship* s = f.ships.findShip(id);
|
||||||
REQUIRE(s->position.x() == Approx(target.x()));
|
REQUIRE(s->position.x() == Approx(target.x()));
|
||||||
@@ -152,7 +156,7 @@ TEST_CASE("BehaviorSystem: tickHomeReturn does nothing when HP is above threshol
|
|||||||
});
|
});
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickHomeReturn();
|
f.ai.tickHomeReturn(f.ships);
|
||||||
|
|
||||||
REQUIRE(f.ships.findShip(id)->intent.priority == 0);
|
REQUIRE(f.ships.findShip(id)->intent.priority == 0);
|
||||||
}
|
}
|
||||||
@@ -170,7 +174,7 @@ TEST_CASE("BehaviorSystem: tickHomeReturn writes priority-4 intent toward homePo
|
|||||||
});
|
});
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickHomeReturn();
|
f.ai.tickHomeReturn(f.ships);
|
||||||
|
|
||||||
const Ship* s = f.ships.findShip(id);
|
const Ship* s = f.ships.findShip(id);
|
||||||
REQUIRE(s->intent.priority == 4);
|
REQUIRE(s->intent.priority == 4);
|
||||||
@@ -195,8 +199,8 @@ TEST_CASE("BehaviorSystem: tickHomeReturn priority-4 beats tickThreatResponse pr
|
|||||||
});
|
});
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickHomeReturn();
|
f.ai.tickHomeReturn(f.ships);
|
||||||
f.ships.tickThreatResponse(f.buildings);
|
f.ai.tickThreatResponse(f.ships, f.buildings);
|
||||||
|
|
||||||
const Ship* s = f.ships.findShip(playerId);
|
const Ship* s = f.ships.findShip(playerId);
|
||||||
REQUIRE(s->intent.priority == 4);
|
REQUIRE(s->intent.priority == 4);
|
||||||
@@ -217,7 +221,7 @@ TEST_CASE("BehaviorSystem: player combat ship acquires nearest enemy ship in ran
|
|||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickThreatResponse(f.buildings);
|
f.ai.tickThreatResponse(f.ships, f.buildings);
|
||||||
|
|
||||||
const Ship* player = f.ships.findShip(playerId);
|
const Ship* player = f.ships.findShip(playerId);
|
||||||
REQUIRE(player->threatResponse.has_value());
|
REQUIRE(player->threatResponse.has_value());
|
||||||
@@ -233,7 +237,7 @@ TEST_CASE("BehaviorSystem: player combat ship does not target friendly ships",
|
|||||||
f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f)); // also player (isEnemy=false)
|
f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f)); // also player (isEnemy=false)
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickThreatResponse(f.buildings);
|
f.ai.tickThreatResponse(f.ships, f.buildings);
|
||||||
|
|
||||||
const Ship* s = f.ships.findShip(id1);
|
const Ship* s = f.ships.findShip(id1);
|
||||||
REQUIRE(s->threatResponse.has_value());
|
REQUIRE(s->threatResponse.has_value());
|
||||||
@@ -249,7 +253,7 @@ TEST_CASE("BehaviorSystem: player combat ship ignores enemy beyond engagement ra
|
|||||||
f.ships.spawn("interceptor", 1, QVector2D(500.0f, 0.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", 1, QVector2D(500.0f, 0.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickThreatResponse(f.buildings);
|
f.ai.tickThreatResponse(f.ships, f.buildings);
|
||||||
|
|
||||||
const Ship* s = f.ships.findShip(playerId);
|
const Ship* s = f.ships.findShip(playerId);
|
||||||
REQUIRE_FALSE(s->threatResponse->currentTarget.has_value());
|
REQUIRE_FALSE(s->threatResponse->currentTarget.has_value());
|
||||||
@@ -268,7 +272,7 @@ TEST_CASE("BehaviorSystem: enemy ship acquires nearest player ship in range",
|
|||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickThreatResponse(f.buildings);
|
f.ai.tickThreatResponse(f.ships, f.buildings);
|
||||||
|
|
||||||
const Ship* enemy = f.ships.findShip(enemyId);
|
const Ship* enemy = f.ships.findShip(enemyId);
|
||||||
REQUIRE(enemy->threatResponse.has_value());
|
REQUIRE(enemy->threatResponse.has_value());
|
||||||
@@ -284,7 +288,7 @@ TEST_CASE("BehaviorSystem: enemy ship with no target writes leftward movement in
|
|||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickThreatResponse(f.buildings);
|
f.ai.tickThreatResponse(f.ships, f.buildings);
|
||||||
|
|
||||||
const Ship* enemy = f.ships.findShip(enemyId);
|
const Ship* enemy = f.ships.findShip(enemyId);
|
||||||
REQUIRE(enemy->intent.priority == 3);
|
REQUIRE(enemy->intent.priority == 3);
|
||||||
@@ -311,7 +315,7 @@ TEST_CASE("BehaviorSystem: repair ship writes intent toward damaged friendly shi
|
|||||||
});
|
});
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickRepairBehavior(f.buildings);
|
f.ai.tickRepairBehavior(f.ships, f.buildings);
|
||||||
|
|
||||||
const Ship* repair = f.ships.findShip(repairId);
|
const Ship* repair = f.ships.findShip(repairId);
|
||||||
REQUIRE(repair->intent.priority == 2);
|
REQUIRE(repair->intent.priority == 2);
|
||||||
@@ -335,7 +339,7 @@ TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
|
|||||||
});
|
});
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickRepairBehavior(f.buildings);
|
f.ai.tickRepairBehavior(f.ships, f.buildings);
|
||||||
|
|
||||||
// repair_rate_formula = "5 + x" at x=1 → 6; hp should have increased.
|
// repair_rate_formula = "5 + x" at x=1 → 6; hp should have increased.
|
||||||
const Ship* friendly = f.ships.findShip(friendlyId);
|
const Ship* friendly = f.ships.findShip(friendlyId);
|
||||||
@@ -359,7 +363,7 @@ TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
|
|||||||
for (int i = 0; i < 5; ++i)
|
for (int i = 0; i < 5; ++i)
|
||||||
{
|
{
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickRepairBehavior(f.buildings);
|
f.ai.tickRepairBehavior(f.ships, f.buildings);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Ship* friendly = f.ships.findShip(friendlyId);
|
const Ship* friendly = f.ships.findShip(friendlyId);
|
||||||
@@ -382,7 +386,7 @@ TEST_CASE("BehaviorSystem: salvage ship writes intent toward nearest scrap", "[b
|
|||||||
f.scraps.spawn(scrapPos, 1, farFuture);
|
f.scraps.spawn(scrapPos, 1, farFuture);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickScrapCollector(f.scraps, f.buildings);
|
f.ai.tickScrapCollector(f.ships, f.scraps, f.buildings);
|
||||||
|
|
||||||
const Ship* s = f.ships.findShip(shipId);
|
const Ship* s = f.ships.findShip(shipId);
|
||||||
REQUIRE(s->intent.priority == 1);
|
REQUIRE(s->intent.priority == 1);
|
||||||
@@ -398,7 +402,7 @@ TEST_CASE("BehaviorSystem: salvage ship collects scrap on arrival", "[behavior]"
|
|||||||
const EntityId scrapId = f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, farFuture);
|
const EntityId scrapId = f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, farFuture);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickScrapCollector(f.scraps, f.buildings);
|
f.ai.tickScrapCollector(f.ships, f.scraps, f.buildings);
|
||||||
|
|
||||||
const Ship* s = f.ships.findShip(shipId);
|
const Ship* s = f.ships.findShip(shipId);
|
||||||
REQUIRE(s->cargo->current == 1);
|
REQUIRE(s->cargo->current == 1);
|
||||||
@@ -436,7 +440,7 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
|||||||
});
|
});
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickScrapCollector(f.scraps, f.buildings);
|
f.ai.tickScrapCollector(f.ships, f.scraps, f.buildings);
|
||||||
|
|
||||||
// Intent should point toward the bay (x < 0 area), not rightward.
|
// Intent should point toward the bay (x < 0 area), not rightward.
|
||||||
const Ship* s = f.ships.findShip(shipId);
|
const Ship* s = f.ships.findShip(shipId);
|
||||||
@@ -469,7 +473,7 @@ TEST_CASE("SensorRange: player combat ship acquires enemy just inside sensor ran
|
|||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickThreatResponse(f.buildings);
|
f.ai.tickThreatResponse(f.ships, f.buildings);
|
||||||
|
|
||||||
const Ship* player = f.ships.findShip(playerId);
|
const Ship* player = f.ships.findShip(playerId);
|
||||||
REQUIRE(player->threatResponse->currentTarget == enemyId);
|
REQUIRE(player->threatResponse->currentTarget == enemyId);
|
||||||
@@ -483,7 +487,7 @@ TEST_CASE("SensorRange: player combat ship ignores enemy just outside sensor ran
|
|||||||
f.ships.spawn("interceptor", 1, QVector2D(210.0f, 0.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", 1, QVector2D(210.0f, 0.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickThreatResponse(f.buildings);
|
f.ai.tickThreatResponse(f.ships, f.buildings);
|
||||||
|
|
||||||
const Ship* player = f.ships.findShip(playerId);
|
const Ship* player = f.ships.findShip(playerId);
|
||||||
REQUIRE_FALSE(player->threatResponse->currentTarget.has_value());
|
REQUIRE_FALSE(player->threatResponse->currentTarget.has_value());
|
||||||
@@ -498,7 +502,7 @@ TEST_CASE("SensorRange: enemy ship ignores player just outside sensor range", "[
|
|||||||
/*isEnemy=*/true);
|
/*isEnemy=*/true);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickThreatResponse(f.buildings);
|
f.ai.tickThreatResponse(f.ships, f.buildings);
|
||||||
|
|
||||||
const Ship* enemy = f.ships.findShip(enemyId);
|
const Ship* enemy = f.ships.findShip(enemyId);
|
||||||
REQUIRE_FALSE(enemy->threatResponse->currentTarget.has_value());
|
REQUIRE_FALSE(enemy->threatResponse->currentTarget.has_value());
|
||||||
@@ -516,7 +520,7 @@ TEST_CASE("SensorRange: repair ship retreats from enemy within sensor range", "[
|
|||||||
f.ships.spawn("interceptor", 1, QVector2D(200.0f, 0.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", 1, QVector2D(200.0f, 0.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickRepairBehavior(f.buildings);
|
f.ai.tickRepairBehavior(f.ships, f.buildings);
|
||||||
|
|
||||||
const Ship* repair = f.ships.findShip(repairId);
|
const Ship* repair = f.ships.findShip(repairId);
|
||||||
REQUIRE(repair->intent.priority == 2);
|
REQUIRE(repair->intent.priority == 2);
|
||||||
@@ -531,7 +535,7 @@ TEST_CASE("SensorRange: repair ship does not retreat from enemy beyond sensor ra
|
|||||||
f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f), /*isEnemy=*/true);
|
f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f), /*isEnemy=*/true);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickRepairBehavior(f.buildings);
|
f.ai.tickRepairBehavior(f.ships, f.buildings);
|
||||||
|
|
||||||
// Enemy outside sensor range → repair ship patrols rightward instead of retreating.
|
// Enemy outside sensor range → repair ship patrols rightward instead of retreating.
|
||||||
const Ship* repair = f.ships.findShip(repairId);
|
const Ship* repair = f.ships.findShip(repairId);
|
||||||
@@ -549,7 +553,7 @@ TEST_CASE("SensorRange: repair ship does not acquire damaged ally beyond sensor
|
|||||||
});
|
});
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickRepairBehavior(f.buildings);
|
f.ai.tickRepairBehavior(f.ships, f.buildings);
|
||||||
|
|
||||||
REQUIRE_FALSE(f.ships.findShip(repairId)->repairBehavior->currentTarget.has_value());
|
REQUIRE_FALSE(f.ships.findShip(repairId)->repairBehavior->currentTarget.has_value());
|
||||||
}
|
}
|
||||||
@@ -566,7 +570,7 @@ TEST_CASE("SensorRange: salvage ship ignores scrap beyond sensor range", "[senso
|
|||||||
f.scraps.spawn(QVector2D(300.0f, 0.0f), 1, 100000);
|
f.scraps.spawn(QVector2D(300.0f, 0.0f), 1, 100000);
|
||||||
|
|
||||||
f.ships.clearMovementIntents();
|
f.ships.clearMovementIntents();
|
||||||
f.ships.tickScrapCollector(f.scraps, f.buildings);
|
f.ai.tickScrapCollector(f.ships, f.scraps, f.buildings);
|
||||||
|
|
||||||
const Ship* s = f.ships.findShip(shipId);
|
const Ship* s = f.ships.findShip(shipId);
|
||||||
REQUIRE(s->scrapCollector->scrapTarget == std::nullopt);
|
REQUIRE(s->scrapCollector->scrapTarget == std::nullopt);
|
||||||
|
|||||||
Reference in New Issue
Block a user