465 lines
18 KiB
C++
465 lines
18 KiB
C++
#include "ShipSystem.h"
|
|
|
|
#include <cassert>
|
|
#include <map>
|
|
#include <stdexcept>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "AdvanceBehavior.h"
|
|
#include "AttackBehavior.h"
|
|
#include "BehaviorScores.h"
|
|
#include "DeliverScrapBehavior.h"
|
|
#include "DynamicBodyComponent.h"
|
|
#include "EntityAdmin.h"
|
|
#include "FactionComponent.h"
|
|
#include "HealthComponent.h"
|
|
#include "ModuleOwnerComponent.h"
|
|
#include "ModulesConfig.h"
|
|
#include "MovementIntentComponent.h"
|
|
#include "RallyBehavior.h"
|
|
#include "RepairBehavior.h"
|
|
#include "RepairToolComponent.h"
|
|
#include "RetreatBehavior.h"
|
|
#include "SalvageCargoComponent.h"
|
|
#include "SalvageScrapBehavior.h"
|
|
#include "SelectedBehaviorComponent.h"
|
|
#include "SensorRangeComponent.h"
|
|
#include "StandbyBehavior.h"
|
|
#include "Tick.h"
|
|
#include "tracing.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 std::map<std::string, int>& moduleLevelOverrides)
|
|
{
|
|
const ShipDef* def = findShipDef(schematicId);
|
|
assert(def != nullptr);
|
|
|
|
const double x = static_cast<double>(level);
|
|
const float tickRate = static_cast<float>(kTickRateHz);
|
|
const float tileSize = static_cast<float>(m_config.world.tileSize_m);
|
|
|
|
float hp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
|
float maxHp = hp;
|
|
float maxSpeed_tpt = static_cast<float>(def->movement.speedFormula.evaluate(x))
|
|
/ tileSize / tickRate;
|
|
float mainAcceleration_tptt = static_cast<float>(
|
|
def->movement.mainAccelerationFormula.evaluate(x))
|
|
/ tileSize / tickRate;
|
|
float maneuveringAcceleration_tptt = static_cast<float>(
|
|
def->movement.maneuveringAccelerationFormula.evaluate(x))
|
|
/ tileSize / tickRate;
|
|
float maxAngularAcceleration_rptt = static_cast<float>(
|
|
def->movement.angularAccelerationFormula.evaluate(x))
|
|
/ tickRate;
|
|
float maxRotationSpeed_rpt = static_cast<float>(
|
|
def->movement.maxRotationSpeedFormula.evaluate(x))
|
|
/ tickRate;
|
|
float sensorRange_tiles = static_cast<float>(
|
|
def->sensor.sensorRangeFormula.evaluate(x))
|
|
/ tileSize;
|
|
|
|
entt::entity entity = m_admin.spawnShip(
|
|
position, hp, maxHp,
|
|
maxSpeed_tpt, mainAcceleration_tptt, maneuveringAcceleration_tptt,
|
|
maxAngularAcceleration_rptt, maxRotationSpeed_rpt, sensorRange_tiles,
|
|
level, schematicId, isEnemy);
|
|
|
|
// Determine module list: configured layout takes precedence over default.
|
|
const std::vector<PlacedModule>& modules =
|
|
layout.has_value() ? layout->placedModules : def->defaultModules;
|
|
|
|
// --- Pass 1: create capability child entities ----------------------------
|
|
std::vector<entt::entity> weaponChildren;
|
|
std::vector<entt::entity> salvageChildren;
|
|
std::vector<entt::entity> repairChildren;
|
|
|
|
for (const PlacedModule& pm : modules)
|
|
{
|
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
|
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
|
|
|
const auto overIt = moduleLevelOverrides.find(pm.moduleId);
|
|
const double mx = static_cast<double>(
|
|
overIt != moduleLevelOverrides.end() ? overIt->second : modDef->playerProductionLevel);
|
|
|
|
if (modDef->weaponCapability)
|
|
{
|
|
WeaponComponent w;
|
|
w.damage = static_cast<float>(
|
|
modDef->weaponCapability->damageFormula.evaluate(mx));
|
|
w.range_tiles = static_cast<float>(
|
|
modDef->weaponCapability->attackRangeFormula.evaluate(mx)) / tileSize;
|
|
w.fireRateHz = static_cast<float>(
|
|
modDef->weaponCapability->attackRateFormula.evaluate(mx));
|
|
w.cooldownTicks = 0.0f;
|
|
w.currentTarget = std::nullopt;
|
|
|
|
entt::entity child = m_admin.createModuleEntity();
|
|
m_admin.addComponent<WeaponComponent>(child, w);
|
|
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
|
|
weaponChildren.push_back(child);
|
|
}
|
|
|
|
if (modDef->salvageCapability)
|
|
{
|
|
SalvageCargoComponent cargo;
|
|
cargo.capacity = static_cast<int>(
|
|
modDef->salvageCapability->cargoCapacityFormula.evaluate(mx));
|
|
cargo.current = 0;
|
|
cargo.collectionRange_tiles = static_cast<float>(
|
|
modDef->salvageCapability->collectionRangeFormula.evaluate(mx)) / tileSize;
|
|
const double rate = modDef->salvageCapability->collectionRateFormula.evaluate(mx);
|
|
cargo.collectionIntervalTicks = (rate > 0.0)
|
|
? static_cast<int>(kTickRateHz / rate + 0.5)
|
|
: 0;
|
|
cargo.cooldownTicksRemaining = 0;
|
|
|
|
entt::entity child = m_admin.createModuleEntity();
|
|
m_admin.addComponent<SalvageCargoComponent>(child, cargo);
|
|
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
|
|
salvageChildren.push_back(child);
|
|
}
|
|
|
|
if (modDef->repairCapability)
|
|
{
|
|
RepairToolComponent rt;
|
|
rt.ratePerTick = static_cast<float>(
|
|
modDef->repairCapability->repairRateFormula.evaluate(mx))
|
|
/ static_cast<float>(kTickRateHz);
|
|
rt.range_tiles = static_cast<float>(
|
|
modDef->repairCapability->repairRangeFormula.evaluate(mx)) / tileSize;
|
|
rt.currentTarget = std::nullopt;
|
|
|
|
entt::entity child = m_admin.createModuleEntity();
|
|
m_admin.addComponent<RepairToolComponent>(child, rt);
|
|
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
|
|
repairChildren.push_back(child);
|
|
}
|
|
}
|
|
|
|
// --- Pass 2: apply passive stat modifiers --------------------------------
|
|
|
|
// Accumulate hull-level modifiers.
|
|
std::map<std::string, std::pair<double, double>> hullMods;
|
|
// Per-capability-type modifier accumulators (applied to each child).
|
|
std::map<std::string, std::pair<double, double>> weaponMods;
|
|
std::map<std::string, std::pair<double, double>> salvageMods;
|
|
std::map<std::string, std::pair<double, double>> repairMods;
|
|
|
|
for (const PlacedModule& pm : modules)
|
|
{
|
|
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
|
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
|
|
|
const auto overIt2 = moduleLevelOverrides.find(pm.moduleId);
|
|
const double mx = static_cast<double>(
|
|
overIt2 != moduleLevelOverrides.end() ? overIt2->second : modDef->playerProductionLevel);
|
|
|
|
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
|
{
|
|
const double val = sm.formula.evaluate(mx);
|
|
|
|
// Route modifier to the correct accumulator by stat category.
|
|
// weapon/salvage/repair stats go to the corresponding child map;
|
|
// hull stats (hp, speed, sensor_range, …) go to hullMods.
|
|
const bool isWeaponStat = (sm.stat == "damage"
|
|
|| sm.stat == "attack_range"
|
|
|| sm.stat == "attack_rate");
|
|
const bool isSalvageStat = (sm.stat == "collection_range"
|
|
|| sm.stat == "cargo_capacity");
|
|
const bool isRepairStat = (sm.stat == "repair_rate"
|
|
|| sm.stat == "repair_range");
|
|
|
|
std::map<std::string, std::pair<double, double>>* target = &hullMods;
|
|
if (isWeaponStat) { target = &weaponMods; }
|
|
if (isSalvageStat) { target = &salvageMods; }
|
|
if (isRepairStat) { target = &repairMods; }
|
|
|
|
std::pair<double, double>& acc = (*target)[sm.stat];
|
|
if (sm.modifierType == "multiplicative")
|
|
{
|
|
acc.first += (val - 1.0);
|
|
}
|
|
else
|
|
{
|
|
acc.second += val;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Range stat additive modifiers are expressed in metres in config; convert to tiles.
|
|
const double tileSizeD = static_cast<double>(m_config.world.tileSize_m);
|
|
const double tickRateD = static_cast<double>(kTickRateHz);
|
|
const char* const kRangeStats[] = {
|
|
"sensor_range", "attack_range", "collection_range", "repair_range"
|
|
};
|
|
std::map<std::string, std::pair<double, double>>* allModMaps[] = {
|
|
&hullMods, &weaponMods, &salvageMods, &repairMods
|
|
};
|
|
for (const char* stat : kRangeStats)
|
|
{
|
|
for (std::map<std::string, std::pair<double, double>>* mods : allModMaps)
|
|
{
|
|
std::map<std::string, std::pair<double, double>>::iterator it = mods->find(stat);
|
|
if (it != mods->end())
|
|
{
|
|
it->second.second /= tileSizeD;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Acceleration additive modifiers are in m/s² in config; convert to tiles/tick
|
|
// (same as the base spawn conversion: / tileSize / tickRate).
|
|
const char* const kAccelerationStats[] = {
|
|
"main_acceleration", "maneuvering_acceleration"
|
|
};
|
|
for (const char* stat : kAccelerationStats)
|
|
{
|
|
for (std::map<std::string, std::pair<double, double>>* mods : allModMaps)
|
|
{
|
|
std::map<std::string, std::pair<double, double>>::iterator it = mods->find(stat);
|
|
if (it != mods->end())
|
|
{
|
|
it->second.second /= tileSizeD * tickRateD;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper: apply a modifier map to a float stat.
|
|
auto applyMod = [](float& stat, const std::string& name,
|
|
const std::map<std::string, std::pair<double, double>>& mods)
|
|
{
|
|
const auto it = mods.find(name);
|
|
if (it != mods.end())
|
|
{
|
|
stat = static_cast<float>(
|
|
static_cast<double>(stat) * (1.0 + it->second.first)
|
|
+ it->second.second);
|
|
}
|
|
};
|
|
|
|
// Apply hull modifiers.
|
|
{
|
|
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", hullMods);
|
|
health.hp = health.maxHp;
|
|
applyMod(dynamics.maxSpeed_tpt, "speed", hullMods);
|
|
applyMod(dynamics.mainAcceleration_tptt, "main_acceleration", hullMods);
|
|
applyMod(dynamics.maneuveringAcceleration_tptt, "maneuvering_acceleration", hullMods);
|
|
applyMod(dynamics.maxAngularAcceleration_rptt, "angular_acceleration", hullMods);
|
|
applyMod(dynamics.maxRotationSpeed_rpt, "max_rotation_speed", hullMods);
|
|
applyMod(sensor.value_tiles, "sensor_range", hullMods);
|
|
}
|
|
|
|
// Apply weapon modifiers to each weapon child.
|
|
for (entt::entity child : weaponChildren)
|
|
{
|
|
WeaponComponent& w = m_admin.get<WeaponComponent>(child);
|
|
applyMod(w.damage, "damage", weaponMods);
|
|
applyMod(w.range_tiles, "attack_range", weaponMods);
|
|
applyMod(w.fireRateHz, "attack_rate", weaponMods);
|
|
}
|
|
|
|
// Apply salvage modifiers to each salvage child.
|
|
for (entt::entity child : salvageChildren)
|
|
{
|
|
SalvageCargoComponent& c = m_admin.get<SalvageCargoComponent>(child);
|
|
float fRange = c.collectionRange_tiles;
|
|
float fCapacity = static_cast<float>(c.capacity);
|
|
// Apply rate modifier: compute rate from interval, apply multiplier, convert back.
|
|
float fRate = (c.collectionIntervalTicks > 0)
|
|
? static_cast<float>(kTickRateHz) / static_cast<float>(c.collectionIntervalTicks)
|
|
: 0.0f;
|
|
applyMod(fRange, "collection_range", salvageMods);
|
|
applyMod(fCapacity, "cargo_capacity", salvageMods);
|
|
applyMod(fRate, "collection_rate", salvageMods);
|
|
c.collectionRange_tiles = fRange;
|
|
c.capacity = static_cast<int>(fCapacity + 0.5f);
|
|
c.collectionIntervalTicks = (fRate > 0.0f)
|
|
? static_cast<int>(static_cast<float>(kTickRateHz) / fRate + 0.5f)
|
|
: 0;
|
|
}
|
|
|
|
// Apply repair modifiers to each repair child.
|
|
for (entt::entity child : repairChildren)
|
|
{
|
|
RepairToolComponent& rt = m_admin.get<RepairToolComponent>(child);
|
|
applyMod(rt.ratePerTick, "repair_rate", repairMods);
|
|
applyMod(rt.range_tiles, "repair_range", repairMods);
|
|
}
|
|
|
|
// --- Pass 3: attach behavior components based on capability presence -----
|
|
|
|
// Baseline: every ship can always fall back to advancing, and needs a slot
|
|
// for the per-tick behavior selection result.
|
|
m_admin.addComponent<AdvanceBehavior>(entity, AdvanceBehavior{});
|
|
m_admin.addComponent<SelectedBehaviorComponent>(entity, SelectedBehaviorComponent{});
|
|
|
|
// Player ships retreat to the rally point when threatened or badly damaged
|
|
// (disabled by the balancing tool to keep arena fights symmetric).
|
|
if (!isEnemy && m_retreatEnabled)
|
|
{
|
|
RetreatBehavior retreat;
|
|
retreat.retreatHpFraction = BehaviorScores::kLowHpFraction;
|
|
retreat.retreatPoint = m_rallyPoint;
|
|
m_admin.addComponent<RetreatBehavior>(entity, retreat);
|
|
}
|
|
|
|
if (!weaponChildren.empty())
|
|
{
|
|
float maxWeaponRange = 0.0f;
|
|
for (entt::entity child : weaponChildren)
|
|
{
|
|
const float r = m_admin.get<WeaponComponent>(child).range_tiles;
|
|
if (r > maxWeaponRange) { maxWeaponRange = r; }
|
|
}
|
|
|
|
AttackBehavior attack;
|
|
attack.orbitRadius_tiles =
|
|
maxWeaponRange * static_cast<float>(m_config.world.orbitFactor);
|
|
m_admin.addComponent<AttackBehavior>(entity, attack);
|
|
|
|
if (!isEnemy)
|
|
{
|
|
RallyBehavior rally;
|
|
rally.rallyPoint = m_rallyPoint;
|
|
rally.orbitRadius_tiles =
|
|
static_cast<float>(m_config.world.rallyOrbitRadius_tiles);
|
|
m_admin.addComponent<RallyBehavior>(entity, rally);
|
|
}
|
|
}
|
|
|
|
if (!salvageChildren.empty())
|
|
{
|
|
float maxCollRange = 0.0f;
|
|
for (entt::entity child : salvageChildren)
|
|
{
|
|
const float r = m_admin.get<SalvageCargoComponent>(child).collectionRange_tiles;
|
|
if (r > maxCollRange) { maxCollRange = r; }
|
|
}
|
|
|
|
SalvageScrapBehavior salvage;
|
|
salvage.scrapTarget = std::nullopt;
|
|
salvage.maxCollectionRange_tiles = maxCollRange;
|
|
salvage.orbitRadius_tiles =
|
|
maxCollRange * static_cast<float>(m_config.world.orbitFactor);
|
|
m_admin.addComponent<SalvageScrapBehavior>(entity, salvage);
|
|
|
|
DeliverScrapBehavior deliver;
|
|
deliver.deliveryBay = kInvalidBuildingId;
|
|
m_admin.addComponent<DeliverScrapBehavior>(entity, deliver);
|
|
}
|
|
|
|
if (!repairChildren.empty())
|
|
{
|
|
float maxRepairRange = 0.0f;
|
|
for (entt::entity child : repairChildren)
|
|
{
|
|
const float r = m_admin.get<RepairToolComponent>(child).range_tiles;
|
|
if (r > maxRepairRange) { maxRepairRange = r; }
|
|
}
|
|
|
|
RepairBehavior repair;
|
|
repair.currentTarget = std::nullopt;
|
|
repair.maxRepairRange_tiles = maxRepairRange;
|
|
repair.orbitRadius_tiles =
|
|
maxRepairRange * static_cast<float>(m_config.world.orbitFactor);
|
|
m_admin.addComponent<RepairBehavior>(entity, repair);
|
|
|
|
// Repair-capable ships hold with the fleet (REQ-SHP-STANDBY) instead of
|
|
// charging the enemy when no more urgent behavior applies; this applies
|
|
// whether or not the ship also carries weapons.
|
|
m_admin.addComponent<StandbyBehavior>(entity, StandbyBehavior{});
|
|
}
|
|
|
|
return entity;
|
|
}
|
|
|
|
void ShipSystem::despawn(entt::entity entity)
|
|
{
|
|
std::vector<entt::entity> children;
|
|
m_admin.forEach<ModuleOwnerComponent>(
|
|
[&](entt::entity e, const ModuleOwnerComponent& o)
|
|
{
|
|
if (o.owner == entity) { children.push_back(e); }
|
|
});
|
|
for (entt::entity child : children) { m_admin.destroy(child); }
|
|
m_admin.destroy(entity);
|
|
}
|
|
|
|
void ShipSystem::clearMovementIntents()
|
|
{
|
|
TRACE();
|
|
m_admin.forEach<MovementIntentComponent>(
|
|
[](entt::entity /*e*/, MovementIntentComponent& i)
|
|
{
|
|
i = MovementIntentComponent{false, QVector2D(0.0f, 0.0f)};
|
|
});
|
|
}
|
|
|
|
void ShipSystem::setRallyPoint(QVector2D point)
|
|
{
|
|
m_rallyPoint = point;
|
|
}
|
|
|
|
void ShipSystem::setRetreatEnabled(bool enabled)
|
|
{
|
|
m_retreatEnabled = enabled;
|
|
}
|
|
|
|
void ShipSystem::triggerRallyDeparture()
|
|
{
|
|
TRACE();
|
|
std::vector<entt::entity> toRemove;
|
|
m_admin.forEach<RallyBehavior, FactionComponent>(
|
|
[&toRemove](entt::entity e, const RallyBehavior& /*rb*/,
|
|
const FactionComponent& f)
|
|
{
|
|
if (!f.isEnemy)
|
|
{
|
|
toRemove.push_back(e);
|
|
}
|
|
});
|
|
for (entt::entity e : toRemove)
|
|
{
|
|
m_admin.removeComponent<RallyBehavior>(e);
|
|
}
|
|
}
|