move ecs related code to own folder
This commit is contained in:
473
src/lib/ecs/system/AiSystem.cpp
Normal file
473
src/lib/ecs/system/AiSystem.cpp
Normal file
@@ -0,0 +1,473 @@
|
||||
#include "AiSystem.h"
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "BuildingType.h"
|
||||
#include "BuildingId.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "HomeReturnBehaviorComponent.h"
|
||||
#include "HqProxyComponent.h"
|
||||
#include "MovementIntentComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "RallyBehaviorComponent.h"
|
||||
#include "RepairBehaviorComponent.h"
|
||||
#include "RepairToolComponent.h"
|
||||
#include "SalvageBehaviorComponent.h"
|
||||
#include "SalvageCargoComponent.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "StationBodyComponent.h"
|
||||
#include "ThreatResponseBehaviorComponent.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tickHomeReturnBehavior (priority 4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void AiSystem::tickHomeReturnBehavior(EntityAdmin& admin)
|
||||
{
|
||||
admin.forEach<HomeReturnBehaviorComponent, HealthComponent, MovementIntentComponent>(
|
||||
[](entt::entity /*e*/, const HomeReturnBehaviorComponent& homeReturnBehavior,
|
||||
const HealthComponent& h, MovementIntentComponent& intent)
|
||||
{
|
||||
if (h.hp / h.maxHp < homeReturnBehavior.retreatHpFraction)
|
||||
{
|
||||
if (4 > intent.priority)
|
||||
{
|
||||
intent = MovementIntentComponent{4, homeReturnBehavior.homePos};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tickThreatResponseBehavior (priority 3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSystem& buildings)
|
||||
{
|
||||
// Snapshot all combatant entities for target acquisition.
|
||||
struct CombatantInfo
|
||||
{
|
||||
entt::entity entity;
|
||||
QVector2D position;
|
||||
bool isEnemy;
|
||||
bool isStation;
|
||||
};
|
||||
std::vector<CombatantInfo> combatants;
|
||||
|
||||
admin.forEach<PositionComponent, FactionComponent, ShipIdentityComponent>(
|
||||
[&combatants](entt::entity e, const PositionComponent& pos,
|
||||
const FactionComponent& f, const ShipIdentityComponent& /*si*/)
|
||||
{
|
||||
combatants.push_back({e, pos.value, f.isEnemy, false});
|
||||
});
|
||||
|
||||
admin.forEach<PositionComponent, FactionComponent, StationBodyComponent>(
|
||||
[&combatants](entt::entity e, const PositionComponent& pos,
|
||||
const FactionComponent& f, const StationBodyComponent& /*sb*/)
|
||||
{
|
||||
combatants.push_back({e, pos.value, f.isEnemy, true});
|
||||
});
|
||||
|
||||
admin.forEach<PositionComponent, FactionComponent, HqProxyComponent>(
|
||||
[&combatants](entt::entity e, const PositionComponent& pos,
|
||||
const FactionComponent& f, const HqProxyComponent& /*hq*/)
|
||||
{
|
||||
combatants.push_back({e, pos.value, f.isEnemy, true});
|
||||
});
|
||||
|
||||
admin.forEach<ThreatResponseBehaviorComponent, PositionComponent, FactionComponent,
|
||||
SensorRangeComponent, MovementIntentComponent>(
|
||||
[&](entt::entity e, ThreatResponseBehaviorComponent& threatResponseBehavior,
|
||||
PositionComponent& pos, FactionComponent& faction,
|
||||
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
||||
{
|
||||
const float range = sensor.value;
|
||||
|
||||
// Validate current target.
|
||||
bool targetValid = false;
|
||||
if (threatResponseBehavior.currentTarget)
|
||||
{
|
||||
const entt::entity t = *threatResponseBehavior.currentTarget;
|
||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
||||
{
|
||||
const float dist =
|
||||
(admin.get<PositionComponent>(t).value - pos.value).length();
|
||||
if (dist <= range)
|
||||
{
|
||||
targetValid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetValid)
|
||||
{
|
||||
threatResponseBehavior.currentTarget = std::nullopt;
|
||||
float bestDist = range;
|
||||
|
||||
for (const CombatantInfo& c : combatants)
|
||||
{
|
||||
if (c.entity == e) { continue; }
|
||||
|
||||
bool isValidTarget = false;
|
||||
if (!faction.isEnemy)
|
||||
{
|
||||
isValidTarget = c.isEnemy;
|
||||
}
|
||||
else
|
||||
{
|
||||
isValidTarget = !c.isEnemy;
|
||||
}
|
||||
if (!isValidTarget) { continue; }
|
||||
|
||||
const float dist = (c.position - pos.value).length();
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
threatResponseBehavior.currentTarget = c.entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (threatResponseBehavior.currentTarget)
|
||||
{
|
||||
const entt::entity t = *threatResponseBehavior.currentTarget;
|
||||
QVector2D dest = pos.value;
|
||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
||||
{
|
||||
dest = admin.get<PositionComponent>(t).value;
|
||||
}
|
||||
if (3 > intent.priority)
|
||||
{
|
||||
intent = MovementIntentComponent{3, dest};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (3 > intent.priority)
|
||||
{
|
||||
if (admin.hasAll<RallyBehaviorComponent>(e))
|
||||
{
|
||||
intent = MovementIntentComponent{
|
||||
3, admin.get<RallyBehaviorComponent>(e).rallyPoint};
|
||||
}
|
||||
else if (!faction.isEnemy)
|
||||
{
|
||||
intent = MovementIntentComponent{
|
||||
3, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
|
||||
}
|
||||
else
|
||||
{
|
||||
intent = MovementIntentComponent{
|
||||
3, QVector2D(-10000.0f, pos.value.y())};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tickRepairBehavior (priority 2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
|
||||
{
|
||||
// Snapshot all entities with health for repair targeting.
|
||||
struct RepairableInfo
|
||||
{
|
||||
entt::entity entity;
|
||||
QVector2D position;
|
||||
bool isEnemy;
|
||||
bool isShip;
|
||||
float hp;
|
||||
float maxHp;
|
||||
};
|
||||
std::vector<RepairableInfo> repairables;
|
||||
|
||||
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent, HealthComponent>(
|
||||
[&repairables](entt::entity e, const ShipIdentityComponent& /*si*/,
|
||||
const PositionComponent& pos, const FactionComponent& f,
|
||||
const HealthComponent& h)
|
||||
{
|
||||
repairables.push_back({e, pos.value, f.isEnemy, true, h.hp, h.maxHp});
|
||||
});
|
||||
|
||||
admin.forEach<StationBodyComponent, PositionComponent, FactionComponent, HealthComponent>(
|
||||
[&repairables](entt::entity e, const StationBodyComponent& /*sb*/,
|
||||
const PositionComponent& pos, const FactionComponent& f,
|
||||
const HealthComponent& h)
|
||||
{
|
||||
repairables.push_back({e, pos.value, f.isEnemy, false, h.hp, h.maxHp});
|
||||
});
|
||||
|
||||
// Snapshot enemy ships for threat detection.
|
||||
struct EnemyInfo
|
||||
{
|
||||
QVector2D position;
|
||||
};
|
||||
std::vector<EnemyInfo> enemies;
|
||||
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
|
||||
[&enemies](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
||||
const PositionComponent& pos, const FactionComponent& f)
|
||||
{
|
||||
if (f.isEnemy)
|
||||
{
|
||||
enemies.push_back({pos.value});
|
||||
}
|
||||
});
|
||||
|
||||
admin.forEach<RepairBehaviorComponent, RepairToolComponent, PositionComponent,
|
||||
FactionComponent, SensorRangeComponent, MovementIntentComponent>(
|
||||
[&](entt::entity e, RepairBehaviorComponent& rb, RepairToolComponent& rt,
|
||||
PositionComponent& pos, FactionComponent& /*faction*/,
|
||||
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
||||
{
|
||||
const float repairRange = rt.range;
|
||||
|
||||
// Flee if enemy nearby.
|
||||
bool enemyNearby = false;
|
||||
for (const EnemyInfo& enemy : enemies)
|
||||
{
|
||||
if ((enemy.position - pos.value).length() <= sensor.value)
|
||||
{
|
||||
enemyNearby = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (enemyNearby)
|
||||
{
|
||||
if (2 > intent.priority)
|
||||
{
|
||||
intent = MovementIntentComponent{
|
||||
2, QVector2D(-10000.0f, pos.value.y())};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate current target.
|
||||
bool targetValid = false;
|
||||
if (rb.currentTarget)
|
||||
{
|
||||
const entt::entity t = *rb.currentTarget;
|
||||
if (admin.isValid(t) && admin.hasAll<HealthComponent>(t))
|
||||
{
|
||||
const HealthComponent& th = admin.get<HealthComponent>(t);
|
||||
if (th.hp > 0.0f && th.hp < th.maxHp)
|
||||
{
|
||||
targetValid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetValid)
|
||||
{
|
||||
rb.currentTarget = std::nullopt;
|
||||
float bestDist = sensor.value;
|
||||
|
||||
for (const RepairableInfo& r : repairables)
|
||||
{
|
||||
if (r.entity == e) { continue; }
|
||||
if (r.isEnemy) { continue; }
|
||||
if (r.hp >= r.maxHp) { continue; }
|
||||
const float dist = (r.position - pos.value).length();
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
rb.currentTarget = r.entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!rb.currentTarget)
|
||||
{
|
||||
if (2 > intent.priority)
|
||||
{
|
||||
intent = MovementIntentComponent{
|
||||
2, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const entt::entity target = *rb.currentTarget;
|
||||
QVector2D targetPos = pos.value;
|
||||
if (admin.isValid(target) && admin.hasAll<PositionComponent>(target))
|
||||
{
|
||||
targetPos = admin.get<PositionComponent>(target).value;
|
||||
}
|
||||
|
||||
const float distToTarget = (targetPos - pos.value).length();
|
||||
if (distToTarget <= repairRange)
|
||||
{
|
||||
if (admin.isValid(target) && admin.hasAll<HealthComponent>(target))
|
||||
{
|
||||
HealthComponent& targetHealth = admin.get<HealthComponent>(target);
|
||||
targetHealth.hp = std::min(targetHealth.hp + rt.ratePerTick,
|
||||
targetHealth.maxHp);
|
||||
}
|
||||
}
|
||||
|
||||
if (2 > intent.priority)
|
||||
{
|
||||
intent = MovementIntentComponent{2, targetPos};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tickSalvageBehavior (priority 1)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
|
||||
BuildingSystem& buildings)
|
||||
{
|
||||
// Snapshot enemy ships for threat detection.
|
||||
struct EnemyShipPos
|
||||
{
|
||||
QVector2D position;
|
||||
};
|
||||
std::vector<EnemyShipPos> enemyShips;
|
||||
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
|
||||
[&enemyShips](entt::entity /*e*/, const ShipIdentityComponent& /*si*/,
|
||||
const PositionComponent& pos, const FactionComponent& f)
|
||||
{
|
||||
if (f.isEnemy)
|
||||
{
|
||||
enemyShips.push_back({pos.value});
|
||||
}
|
||||
});
|
||||
|
||||
const std::vector<ScrapInfo> allScrap = scraps.allScrapInfo();
|
||||
|
||||
admin.forEach<SalvageBehaviorComponent, SalvageCargoComponent, PositionComponent,
|
||||
SensorRangeComponent, MovementIntentComponent>(
|
||||
[&](entt::entity /*e*/, SalvageBehaviorComponent& salvageBehavior,
|
||||
SalvageCargoComponent& cargo, PositionComponent& pos,
|
||||
SensorRangeComponent& sensor, MovementIntentComponent& intent)
|
||||
{
|
||||
const float collectRange = cargo.collectionRange;
|
||||
|
||||
// Assign nearest SalvageBay if needed.
|
||||
if (salvageBehavior.deliveryBay == kInvalidBuildingId)
|
||||
{
|
||||
const Building* bay = buildings.findNearestBuilding(pos.value,
|
||||
BuildingType::SalvageBay);
|
||||
if (bay)
|
||||
{
|
||||
salvageBehavior.deliveryBay = bay->id;
|
||||
}
|
||||
}
|
||||
|
||||
const BuildingId bayId = salvageBehavior.deliveryBay;
|
||||
|
||||
QVector2D bayPos = pos.value;
|
||||
if (bayId != kInvalidBuildingId)
|
||||
{
|
||||
const Building* bay = buildings.findBuilding(bayId);
|
||||
if (bay)
|
||||
{
|
||||
bayPos = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
||||
bay->anchor.y() + bay->footprint.height() / 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
const bool cargoFull = (cargo.current >= cargo.capacity);
|
||||
|
||||
if (cargoFull)
|
||||
{
|
||||
if (1 > intent.priority)
|
||||
{
|
||||
intent = MovementIntentComponent{1, bayPos};
|
||||
}
|
||||
if (bayId != kInvalidBuildingId
|
||||
&& (pos.value - bayPos).length() <= 1.0f)
|
||||
{
|
||||
if (buildings.deliverScrapToSalvageBay(bayId))
|
||||
{
|
||||
--cargo.current;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Retreat if enemy near and cargo empty.
|
||||
bool retreating = false;
|
||||
if (cargo.current == 0)
|
||||
{
|
||||
for (const EnemyShipPos& enemy : enemyShips)
|
||||
{
|
||||
if ((enemy.position - pos.value).length() <= collectRange)
|
||||
{
|
||||
if (1 > intent.priority)
|
||||
{
|
||||
intent = MovementIntentComponent{
|
||||
1, QVector2D(-10000.0f, pos.value.y())};
|
||||
}
|
||||
retreating = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (retreating) { return; }
|
||||
|
||||
// Collect nearby scrap.
|
||||
for (const ScrapInfo& si : allScrap)
|
||||
{
|
||||
if ((si.position - pos.value).length() <= collectRange)
|
||||
{
|
||||
if (scraps.consume(si.entity))
|
||||
{
|
||||
++cargo.current;
|
||||
salvageBehavior.scrapTarget = std::nullopt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Move toward scrap target or find a new one.
|
||||
if (salvageBehavior.scrapTarget)
|
||||
{
|
||||
if (1 > intent.priority)
|
||||
{
|
||||
intent = MovementIntentComponent{1, *salvageBehavior.scrapTarget};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float bestDist = sensor.value;
|
||||
std::optional<QVector2D> bestPos;
|
||||
for (const ScrapInfo& si : allScrap)
|
||||
{
|
||||
const float dist = (si.position - pos.value).length();
|
||||
if (dist < bestDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
bestPos = si.position;
|
||||
}
|
||||
}
|
||||
if (bestPos)
|
||||
{
|
||||
salvageBehavior.scrapTarget = bestPos;
|
||||
if (1 > intent.priority)
|
||||
{
|
||||
intent = MovementIntentComponent{1, *bestPos};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (1 > intent.priority)
|
||||
{
|
||||
intent = MovementIntentComponent{
|
||||
1, QVector2D(pos.value.x() + 1000.0f, pos.value.y())};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
14
src/lib/ecs/system/AiSystem.h
Normal file
14
src/lib/ecs/system/AiSystem.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
class BuildingSystem;
|
||||
class EntityAdmin;
|
||||
class ScrapSystem;
|
||||
|
||||
class AiSystem
|
||||
{
|
||||
public:
|
||||
void tickHomeReturnBehavior(EntityAdmin& admin);
|
||||
void tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSystem& buildings);
|
||||
void tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings);
|
||||
void tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps, BuildingSystem& buildings);
|
||||
};
|
||||
27
src/lib/ecs/system/CMakeLists.txt
Normal file
27
src/lib/ecs/system/CMakeLists.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
SET(HDRS
|
||||
${HDRS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
SET(SRCS
|
||||
${SRCS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/AiSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CombatSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DynamicBodySystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MovementIntentSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
set(LIB_INCLUDE_PATH
|
||||
${LIB_INCLUDE_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PARENT_SCOPE
|
||||
)
|
||||
142
src/lib/ecs/system/CombatSystem.cpp
Normal file
142
src/lib/ecs/system/CombatSystem.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
#include "CombatSystem.h"
|
||||
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "ThreatResponseBehaviorComponent.h"
|
||||
#include "WeaponComponent.h"
|
||||
|
||||
static constexpr Tick kWeaponImpactDelayTicks = 5;
|
||||
|
||||
CombatSystem::CombatSystem(const GameConfig& config)
|
||||
: m_config(config)
|
||||
{
|
||||
}
|
||||
|
||||
void CombatSystem::tick(Tick currentTick,
|
||||
EntityAdmin& admin,
|
||||
BuildingSystem& /*buildings*/,
|
||||
std::vector<FireEvent>& outFireEvents)
|
||||
{
|
||||
// Ship weapons.
|
||||
admin.forEach<WeaponComponent, ThreatResponseBehaviorComponent,
|
||||
PositionComponent, FactionComponent>(
|
||||
[&](entt::entity e, WeaponComponent& weapon,
|
||||
ThreatResponseBehaviorComponent& threatResponseBehavior,
|
||||
PositionComponent& pos, FactionComponent& faction)
|
||||
{
|
||||
weapon.currentTarget = threatResponseBehavior.currentTarget;
|
||||
resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
|
||||
});
|
||||
|
||||
// Station weapons.
|
||||
admin.forEach<WeaponComponent, PositionComponent, FactionComponent>(
|
||||
[&](entt::entity e, WeaponComponent& weapon, PositionComponent& pos,
|
||||
FactionComponent& faction)
|
||||
{
|
||||
resolveWeapon(e, weapon, pos, faction, currentTick, admin, outFireEvents);
|
||||
});
|
||||
}
|
||||
|
||||
void CombatSystem::resolveWeapon(
|
||||
entt::entity shipEntity,
|
||||
WeaponComponent& weapon,
|
||||
const PositionComponent& ownPos,
|
||||
const FactionComponent& ownFaction,
|
||||
Tick currentTick,
|
||||
EntityAdmin& admin,
|
||||
std::vector<FireEvent>& out)
|
||||
{
|
||||
if (weapon.cooldownTicks > 0.0f)
|
||||
{
|
||||
weapon.cooldownTicks -= 1.0f;
|
||||
}
|
||||
if (weapon.cooldownTicks > 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate or clear existing target.
|
||||
if (weapon.currentTarget)
|
||||
{
|
||||
const entt::entity t = *weapon.currentTarget;
|
||||
if (!admin.isValid(t) || !admin.hasAll<PositionComponent>(t))
|
||||
{
|
||||
weapon.currentTarget = std::nullopt;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float distanceSquared =
|
||||
(ownPos.value - admin.get<PositionComponent>(t).value).lengthSquared();
|
||||
if (distanceSquared > weapon.range * weapon.range)
|
||||
{
|
||||
weapon.currentTarget = std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire a new target if needed (nearest opposing-faction ship).
|
||||
if (!weapon.currentTarget)
|
||||
{
|
||||
float bestDistanceSquared = weapon.range * weapon.range;
|
||||
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
|
||||
[&](entt::entity candidate, const ShipIdentityComponent& /*si*/,
|
||||
const PositionComponent& candidatePos,
|
||||
const FactionComponent& candidateFaction)
|
||||
{
|
||||
const bool isValidTarget = ownFaction.isEnemy
|
||||
? !candidateFaction.isEnemy
|
||||
: candidateFaction.isEnemy;
|
||||
if (!isValidTarget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float distanceSquared =
|
||||
(candidatePos.value - ownPos.value).lengthSquared();
|
||||
if (distanceSquared < bestDistanceSquared)
|
||||
{
|
||||
bestDistanceSquared = distanceSquared;
|
||||
weapon.currentTarget = candidate;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!weapon.currentTarget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const entt::entity targetEntity = *weapon.currentTarget;
|
||||
m_pendingDamage.push_back({targetEntity, weapon.damage,
|
||||
currentTick + kWeaponImpactDelayTicks});
|
||||
|
||||
FireEvent evt;
|
||||
evt.shooter = shipEntity;
|
||||
evt.target = targetEntity;
|
||||
evt.emittedAt = currentTick;
|
||||
out.push_back(evt);
|
||||
|
||||
weapon.cooldownTicks = static_cast<float>(kTickRateHz) / weapon.fireRateHz;
|
||||
}
|
||||
|
||||
void CombatSystem::applyPendingDamage(Tick currentTick, EntityAdmin& admin)
|
||||
{
|
||||
std::vector<PendingDamage>::iterator it = m_pendingDamage.begin();
|
||||
while (it != m_pendingDamage.end())
|
||||
{
|
||||
if (it->appliesAt <= currentTick)
|
||||
{
|
||||
if (admin.isValid(it->target) && admin.hasAll<HealthComponent>(it->target))
|
||||
{
|
||||
admin.get<HealthComponent>(it->target).hp -= it->amount;
|
||||
}
|
||||
it = m_pendingDamage.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/lib/ecs/system/CombatSystem.h
Normal file
53
src/lib/ecs/system/CombatSystem.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "Building.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "FireEvent.h"
|
||||
#include "GameConfig.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "Tick.h"
|
||||
#include "WeaponComponent.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
class BuildingSystem;
|
||||
class EntityAdmin;
|
||||
|
||||
class CombatSystem
|
||||
{
|
||||
public:
|
||||
explicit CombatSystem(const GameConfig& config);
|
||||
|
||||
void tick(Tick currentTick,
|
||||
EntityAdmin& admin,
|
||||
BuildingSystem& buildings,
|
||||
std::vector<FireEvent>& outFireEvents);
|
||||
|
||||
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
|
||||
|
||||
private:
|
||||
struct PendingDamage
|
||||
{
|
||||
entt::entity target;
|
||||
float amount;
|
||||
Tick appliesAt;
|
||||
};
|
||||
|
||||
std::vector<PendingDamage> m_pendingDamage;
|
||||
|
||||
void resolveWeapon(
|
||||
entt::entity shipEntity,
|
||||
WeaponComponent& weapon,
|
||||
const PositionComponent& ownPos,
|
||||
const FactionComponent& ownFaction,
|
||||
Tick currentTick,
|
||||
EntityAdmin& admin,
|
||||
std::vector<FireEvent>& out);
|
||||
|
||||
const GameConfig& m_config;
|
||||
};
|
||||
51
src/lib/ecs/system/DynamicBodySystem.cpp
Normal file
51
src/lib/ecs/system/DynamicBodySystem.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "DynamicBodySystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FacingComponent.h"
|
||||
#include "PositionComponent.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 DynamicBodySystem::tick(EntityAdmin& admin)
|
||||
{
|
||||
admin.forEach<PositionComponent, FacingComponent, DynamicBodyComponent>(
|
||||
[](entt::entity /*e*/, PositionComponent& pos, FacingComponent& facing,
|
||||
DynamicBodyComponent& body)
|
||||
{
|
||||
// Integrate angular velocity, clamp to max rotation speed, then advance facing.
|
||||
body.angularVelocity += body.angularAcceleration;
|
||||
body.angularVelocity = std::max(-body.maxRotationSpeedPerTick,
|
||||
std::min(body.angularVelocity,
|
||||
body.maxRotationSpeedPerTick));
|
||||
facing.radians = wrapAngle(facing.radians + body.angularVelocity);
|
||||
|
||||
// Integrate linear velocity and cap to max speed.
|
||||
body.velocity += body.linearAcceleration;
|
||||
const float speed = body.velocity.length();
|
||||
if (speed > body.maxSpeedPerTick)
|
||||
{
|
||||
body.velocity = body.velocity.normalized() * body.maxSpeedPerTick;
|
||||
}
|
||||
|
||||
// Advance position.
|
||||
pos.value += body.velocity;
|
||||
|
||||
// Reset per-tick fields so stale values don't linger if the intent
|
||||
// system is skipped for this entity in a future tick.
|
||||
body.linearAcceleration = QVector2D(0.0f, 0.0f);
|
||||
body.angularAcceleration = 0.0f;
|
||||
});
|
||||
}
|
||||
9
src/lib/ecs/system/DynamicBodySystem.h
Normal file
9
src/lib/ecs/system/DynamicBodySystem.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
class EntityAdmin;
|
||||
|
||||
class DynamicBodySystem
|
||||
{
|
||||
public:
|
||||
void tick(EntityAdmin& admin);
|
||||
};
|
||||
115
src/lib/ecs/system/MovementIntentSystem.cpp
Normal file
115
src/lib/ecs/system/MovementIntentSystem.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#include "MovementIntentSystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FacingComponent.h"
|
||||
#include "MovementIntentComponent.h"
|
||||
#include "PositionComponent.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 MovementIntentSystem::tick(EntityAdmin& admin)
|
||||
{
|
||||
admin.forEach<PositionComponent, FacingComponent, DynamicBodyComponent,
|
||||
MovementIntentComponent>(
|
||||
[](entt::entity /*e*/, const PositionComponent& pos, const FacingComponent& facing,
|
||||
DynamicBodyComponent& body, const MovementIntentComponent& intent)
|
||||
{
|
||||
if (intent.priority == 0)
|
||||
{
|
||||
// No movement intent: brake using available thrust.
|
||||
const float linearBraking = std::min(body.velocity.length(),
|
||||
body.maneuveringAccelerationPerTick);
|
||||
body.linearAcceleration = (body.velocity.length() > 0.0001f)
|
||||
? -body.velocity.normalized() * linearBraking
|
||||
: QVector2D(0.0f, 0.0f);
|
||||
|
||||
const float angBraking = std::min(std::abs(body.angularVelocity),
|
||||
body.angularAccelerationPerTick);
|
||||
body.angularAcceleration =
|
||||
(body.angularVelocity >= 0.0f) ? -angBraking : angBraking;
|
||||
return;
|
||||
}
|
||||
|
||||
const QVector2D delta = intent.target - pos.value;
|
||||
const float dist = delta.length();
|
||||
|
||||
if (dist < 0.001f)
|
||||
{
|
||||
// Already at target: no new thrust. The ship drifts; it will
|
||||
// re-approach next tick once it has moved away.
|
||||
body.linearAcceleration = QVector2D(0.0f, 0.0f);
|
||||
body.angularAcceleration = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Angular acceleration ---
|
||||
|
||||
const float desiredAngle = std::atan2(delta.y(), delta.x());
|
||||
const float angleDiff = wrapAngle(desiredAngle - facing.radians);
|
||||
|
||||
const float rotDelta = std::max(-body.angularAccelerationPerTick,
|
||||
std::min(angleDiff,
|
||||
body.angularAccelerationPerTick));
|
||||
|
||||
float newAngVel = body.angularVelocity + rotDelta;
|
||||
|
||||
// Overshoot prevention: if the accumulated angular velocity already
|
||||
// exceeds the remaining angle, snap it to exactly that angle so the
|
||||
// ship doesn't rotate past its heading.
|
||||
const bool sameSign = (newAngVel >= 0.0f) == (angleDiff >= 0.0f);
|
||||
if (sameSign && std::abs(newAngVel) > std::abs(angleDiff))
|
||||
{
|
||||
newAngVel = angleDiff;
|
||||
}
|
||||
|
||||
body.angularAcceleration = newAngVel - body.angularVelocity;
|
||||
// DynamicBodySystem applies the clamp to maxRotationSpeedPerTick after
|
||||
// integrating, so we do not clamp here.
|
||||
|
||||
// --- Linear acceleration ---
|
||||
// Use the projected facing (after this tick's angular integration) so
|
||||
// that the main thruster aligns with where the ship will actually be
|
||||
// pointing when DynamicBodySystem applies the forces.
|
||||
|
||||
const float projectedRadians = wrapAngle(facing.radians + newAngVel);
|
||||
const QVector2D facingVec(std::cos(projectedRadians),
|
||||
std::sin(projectedRadians));
|
||||
|
||||
const float manAccel = body.maneuveringAccelerationPerTick;
|
||||
const float stoppingDist = (body.maxSpeedPerTick * body.maxSpeedPerTick)
|
||||
/ (2.0f * manAccel);
|
||||
const float desiredSpeed = (dist <= stoppingDist)
|
||||
? std::sqrt(2.0f * manAccel * dist)
|
||||
: body.maxSpeedPerTick;
|
||||
|
||||
const QVector2D desiredVel = delta.normalized() * desiredSpeed;
|
||||
const QVector2D velError = desiredVel - body.velocity;
|
||||
|
||||
const float mainAligned = std::max(0.0f,
|
||||
QVector2D::dotProduct(velError, facingVec));
|
||||
const float mainApplied = std::min(mainAligned,
|
||||
body.mainAccelerationPerTick);
|
||||
const QVector2D mainDelta = facingVec * mainApplied;
|
||||
|
||||
const QVector2D remaining = velError - mainDelta;
|
||||
const float remainLen = remaining.length();
|
||||
const QVector2D maneuverDelta = (remainLen > manAccel)
|
||||
? remaining.normalized() * manAccel
|
||||
: remaining;
|
||||
|
||||
body.linearAcceleration = mainDelta + maneuverDelta;
|
||||
});
|
||||
}
|
||||
9
src/lib/ecs/system/MovementIntentSystem.h
Normal file
9
src/lib/ecs/system/MovementIntentSystem.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
class EntityAdmin;
|
||||
|
||||
class MovementIntentSystem
|
||||
{
|
||||
public:
|
||||
void tick(EntityAdmin& admin);
|
||||
};
|
||||
56
src/lib/ecs/system/ScrapSystem.cpp
Normal file
56
src/lib/ecs/system/ScrapSystem.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#include "ScrapSystem.h"
|
||||
|
||||
#include "DespawnAtComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "ScrapDataComponent.h"
|
||||
|
||||
ScrapSystem::ScrapSystem(EntityAdmin& admin)
|
||||
: m_admin(admin)
|
||||
{
|
||||
}
|
||||
|
||||
entt::entity ScrapSystem::spawn(QVector2D position, int amount, Tick despawnAt)
|
||||
{
|
||||
return m_admin.spawnScrap(position, amount, despawnAt);
|
||||
}
|
||||
|
||||
void ScrapSystem::tickDespawn(Tick currentTick)
|
||||
{
|
||||
std::vector<entt::entity> expired;
|
||||
m_admin.forEach<DespawnAtComponent>(
|
||||
[&expired, currentTick](entt::entity e, DespawnAtComponent& d)
|
||||
{
|
||||
if (d.tick <= currentTick)
|
||||
{
|
||||
expired.push_back(e);
|
||||
}
|
||||
});
|
||||
|
||||
for (entt::entity e : expired)
|
||||
{
|
||||
m_admin.destroy(e);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<int> ScrapSystem::consume(entt::entity entity)
|
||||
{
|
||||
if (!m_admin.isValid(entity) || !m_admin.hasAll<ScrapDataComponent>(entity))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
int amount = m_admin.get<ScrapDataComponent>(entity).amount;
|
||||
m_admin.destroy(entity);
|
||||
return amount;
|
||||
}
|
||||
|
||||
std::vector<ScrapInfo> ScrapSystem::allScrapInfo() const
|
||||
{
|
||||
std::vector<ScrapInfo> result;
|
||||
m_admin.forEach<ScrapDataComponent>(
|
||||
[&result, this](entt::entity e, const ScrapDataComponent& /*sd*/)
|
||||
{
|
||||
result.push_back(ScrapInfo{e, m_admin.get<PositionComponent>(e).value});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
36
src/lib/ecs/system/ScrapSystem.h
Normal file
36
src/lib/ecs/system/ScrapSystem.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "Tick.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
class EntityAdmin;
|
||||
|
||||
struct ScrapInfo
|
||||
{
|
||||
entt::entity entity;
|
||||
QVector2D position;
|
||||
};
|
||||
|
||||
class ScrapSystem
|
||||
{
|
||||
public:
|
||||
explicit ScrapSystem(EntityAdmin& admin);
|
||||
|
||||
entt::entity spawn(QVector2D position, int amount, Tick despawnAt);
|
||||
void tickDespawn(Tick currentTick);
|
||||
|
||||
// Removes the scrap and returns its amount, or nullopt if not found.
|
||||
std::optional<int> consume(entt::entity entity);
|
||||
|
||||
// Lightweight snapshot for callers that need to iterate all scrap.
|
||||
std::vector<ScrapInfo> allScrapInfo() const;
|
||||
|
||||
private:
|
||||
EntityAdmin& m_admin;
|
||||
};
|
||||
238
src/lib/ecs/system/ShipSystem.cpp
Normal file
238
src/lib/ecs/system/ShipSystem.cpp
Normal file
@@ -0,0 +1,238 @@
|
||||
#include "ShipSystem.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "ModulesConfig.h"
|
||||
#include "MovementIntentComponent.h"
|
||||
#include "RallyBehaviorComponent.h"
|
||||
#include "RepairBehaviorComponent.h"
|
||||
#include "RepairToolComponent.h"
|
||||
#include "SalvageBehaviorComponent.h"
|
||||
#include "SalvageCargoComponent.h"
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "Tick.h"
|
||||
#include "ThreatResponseBehaviorComponent.h"
|
||||
#include "WeaponComponent.h"
|
||||
|
||||
ShipSystem::ShipSystem(const GameConfig& config, EntityAdmin& admin)
|
||||
: m_config(config)
|
||||
, m_admin(admin)
|
||||
{
|
||||
}
|
||||
|
||||
const ShipDef* ShipSystem::findShipDef(const std::string& schematicId) const
|
||||
{
|
||||
for (const ShipDef& def : m_config.ships.ships)
|
||||
{
|
||||
if (def.id == schematicId)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
|
||||
{
|
||||
for (const ModuleDef& def : m_config.modules.modules)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
QVector2D position, bool isEnemy,
|
||||
const std::optional<ShipLayoutConfig>& layout)
|
||||
{
|
||||
const ShipDef* def = findShipDef(schematicId);
|
||||
assert(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(level);
|
||||
const float tickRate = static_cast<float>(kTickRateHz);
|
||||
|
||||
float hp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
||||
float maxHp = hp;
|
||||
float maxSpeedPerTick = static_cast<float>(def->movement.speedFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float mainAccelPerTick = static_cast<float>(
|
||||
def->movement.mainAccelerationFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float maneuveringAccelPerTick = static_cast<float>(
|
||||
def->movement.maneuveringAccelerationFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float angularAccelPerTick = static_cast<float>(
|
||||
def->movement.angularAccelerationFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float maxRotationSpeedPerTick = static_cast<float>(
|
||||
def->movement.maxRotationSpeedFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float sensorRange = static_cast<float>(
|
||||
def->sensor.sensorRangeFormula.evaluate(x));
|
||||
|
||||
entt::entity entity = m_admin.spawnShip(
|
||||
position, hp, maxHp,
|
||||
maxSpeedPerTick, mainAccelPerTick, maneuveringAccelPerTick,
|
||||
angularAccelPerTick, maxRotationSpeedPerTick, sensorRange,
|
||||
level, schematicId, isEnemy);
|
||||
|
||||
// Optional components based on ship role.
|
||||
if (def->combat)
|
||||
{
|
||||
WeaponComponent w;
|
||||
w.damage = static_cast<float>(def->combat->damageFormula.evaluate(x));
|
||||
w.range = static_cast<float>(def->combat->attackRangeFormula.evaluate(x));
|
||||
w.fireRateHz = static_cast<float>(def->combat->attackRateFormula.evaluate(x));
|
||||
w.cooldownTicks = 0.0f;
|
||||
w.currentTarget = std::nullopt;
|
||||
m_admin.addComponent<WeaponComponent>(entity, w);
|
||||
|
||||
m_admin.addComponent<ThreatResponseBehaviorComponent>(
|
||||
entity, ThreatResponseBehaviorComponent{});
|
||||
|
||||
if (!isEnemy)
|
||||
{
|
||||
m_admin.addComponent<RallyBehaviorComponent>(
|
||||
entity, RallyBehaviorComponent{m_rallyPoint});
|
||||
}
|
||||
}
|
||||
|
||||
if (def->salvage)
|
||||
{
|
||||
SalvageCargoComponent cargo;
|
||||
cargo.capacity = def->salvage->cargoCapacity;
|
||||
cargo.current = 0;
|
||||
cargo.collectionRange = static_cast<float>(def->salvage->collectionRange);
|
||||
m_admin.addComponent<SalvageCargoComponent>(entity, cargo);
|
||||
|
||||
SalvageBehaviorComponent salvageBehavior;
|
||||
salvageBehavior.scrapTarget = std::nullopt;
|
||||
salvageBehavior.deliveryBay = kInvalidBuildingId;
|
||||
m_admin.addComponent<SalvageBehaviorComponent>(entity, salvageBehavior);
|
||||
}
|
||||
|
||||
if (def->repair)
|
||||
{
|
||||
RepairToolComponent rt;
|
||||
rt.ratePerTick = static_cast<float>(def->repair->repairRateFormula.evaluate(x));
|
||||
rt.range = static_cast<float>(def->repair->repairRangeFormula.evaluate(x));
|
||||
rt.currentTarget = std::nullopt;
|
||||
m_admin.addComponent<RepairToolComponent>(entity, rt);
|
||||
|
||||
m_admin.addComponent<RepairBehaviorComponent>(entity, RepairBehaviorComponent{});
|
||||
}
|
||||
|
||||
// Apply module stat modifiers (REQ-MOD-STAT-CALC).
|
||||
if (layout.has_value() && !layout->placedModules.empty())
|
||||
{
|
||||
std::map<std::string, std::pair<double, double>> mods;
|
||||
for (const PlacedModule& pm : layout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
||||
{
|
||||
const double val = sm.formula.evaluate(
|
||||
static_cast<double>(modDef->playerProductionLevel));
|
||||
std::pair<double, double>& acc = mods[sm.stat];
|
||||
if (sm.modifierType == "multiplicative")
|
||||
{
|
||||
acc.first += (val - 1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
acc.second += val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto applyMod = [&mods](float& stat, const std::string& name) {
|
||||
const std::map<std::string, std::pair<double, double>>::const_iterator it =
|
||||
mods.find(name);
|
||||
if (it != mods.end())
|
||||
{
|
||||
stat = static_cast<float>(
|
||||
static_cast<double>(stat) * (1.0 + it->second.first)
|
||||
+ it->second.second);
|
||||
}
|
||||
};
|
||||
|
||||
HealthComponent& health = m_admin.get<HealthComponent>(entity);
|
||||
DynamicBodyComponent& dynamics = m_admin.get<DynamicBodyComponent>(entity);
|
||||
SensorRangeComponent& sensor = m_admin.get<SensorRangeComponent>(entity);
|
||||
|
||||
applyMod(health.maxHp, "hp");
|
||||
health.hp = health.maxHp;
|
||||
applyMod(dynamics.maxSpeedPerTick, "speed");
|
||||
applyMod(dynamics.mainAccelerationPerTick, "main_acceleration");
|
||||
applyMod(dynamics.maneuveringAccelerationPerTick, "maneuvering_acceleration");
|
||||
applyMod(dynamics.angularAccelerationPerTick, "angular_acceleration");
|
||||
applyMod(dynamics.maxRotationSpeedPerTick, "max_rotation_speed");
|
||||
applyMod(sensor.value, "sensor_range");
|
||||
|
||||
if (m_admin.hasAll<WeaponComponent>(entity))
|
||||
{
|
||||
WeaponComponent& weapon = m_admin.get<WeaponComponent>(entity);
|
||||
applyMod(weapon.damage, "damage");
|
||||
applyMod(weapon.range, "attack_range");
|
||||
applyMod(weapon.fireRateHz, "attack_rate");
|
||||
}
|
||||
if (m_admin.hasAll<RepairToolComponent>(entity))
|
||||
{
|
||||
RepairToolComponent& repairTool = m_admin.get<RepairToolComponent>(entity);
|
||||
applyMod(repairTool.ratePerTick, "repair_rate");
|
||||
applyMod(repairTool.range, "repair_range");
|
||||
}
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
void ShipSystem::despawn(entt::entity entity)
|
||||
{
|
||||
m_admin.destroy(entity);
|
||||
}
|
||||
|
||||
void ShipSystem::clearMovementIntents()
|
||||
{
|
||||
m_admin.forEach<MovementIntentComponent>(
|
||||
[](entt::entity /*e*/, MovementIntentComponent& i)
|
||||
{
|
||||
i = MovementIntentComponent{0, QVector2D(0.0f, 0.0f)};
|
||||
});
|
||||
}
|
||||
|
||||
void ShipSystem::setRallyPoint(QVector2D point)
|
||||
{
|
||||
m_rallyPoint = point;
|
||||
}
|
||||
|
||||
void ShipSystem::triggerRallyDeparture()
|
||||
{
|
||||
std::vector<entt::entity> toRemove;
|
||||
m_admin.forEach<RallyBehaviorComponent, FactionComponent>(
|
||||
[&toRemove](entt::entity e, const RallyBehaviorComponent& /*rb*/,
|
||||
const FactionComponent& f)
|
||||
{
|
||||
if (!f.isEnemy)
|
||||
{
|
||||
toRemove.push_back(e);
|
||||
}
|
||||
});
|
||||
for (entt::entity e : toRemove)
|
||||
{
|
||||
m_admin.removeComponent<RallyBehaviorComponent>(e);
|
||||
}
|
||||
}
|
||||
41
src/lib/ecs/system/ShipSystem.h
Normal file
41
src/lib/ecs/system/ShipSystem.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "GameConfig.h"
|
||||
#include "ShipLayout.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
class EntityAdmin;
|
||||
|
||||
class ShipSystem
|
||||
{
|
||||
public:
|
||||
ShipSystem(const GameConfig& config, EntityAdmin& admin);
|
||||
|
||||
entt::entity spawn(const std::string& schematicId, int level, QVector2D position,
|
||||
bool isEnemy = false,
|
||||
const std::optional<ShipLayoutConfig>& layout = std::nullopt);
|
||||
void despawn(entt::entity entity);
|
||||
|
||||
// Reset all movement intents to priority 0 before behavior systems run.
|
||||
void clearMovementIntents();
|
||||
|
||||
// Set the rally point that newly spawned player combat ships will loiter at.
|
||||
void setRallyPoint(QVector2D point);
|
||||
|
||||
// Release all gathered player combat ships to advance toward the enemy.
|
||||
void triggerRallyDeparture();
|
||||
|
||||
private:
|
||||
const ShipDef* findShipDef(const std::string& schematicId) const;
|
||||
const ModuleDef* findModuleDef(const std::string& id) const;
|
||||
|
||||
const GameConfig& m_config;
|
||||
EntityAdmin& m_admin;
|
||||
QVector2D m_rallyPoint;
|
||||
};
|
||||
Reference in New Issue
Block a user