Files
dota_factory/src/lib/ecs/system/ShipSystem.cpp

480 lines
18 KiB
C++

#include "ShipSystem.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <map>
#include <stdexcept>
#include <utility>
#include <vector>
#include "AdvanceBehavior.h"
#include "AttackBehavior.h"
#include "BehaviorScores.h"
#include "CargoComponent.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 "SalvageScrapBehavior.h"
#include "SalvagerComponent.h"
#include "SelectedBehaviorComponent.h"
#include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h"
#include "StandbyBehavior.h"
#include "ThreatCostCalculator.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,
QVector2D position, bool isEnemy,
const std::optional<ShipLayoutConfig>& layout)
{
const ShipDef* def = findShipDef(schematicId);
assert(def != nullptr);
const float tickRate = static_cast<float>(kTickRateHz);
const float tileSize = static_cast<float>(m_config.world.tileSize_m);
float hp = def->health.hp;
float maxHp = hp;
float maxSpeed_tpt = def->movement.speed_mps / tileSize / tickRate;
float mainAcceleration_tptt = def->movement.mainAcceleration_mpss / tileSize / tickRate;
float maneuveringAcceleration_tptt = def->movement.maneuveringAcceleration_mpss
/ tileSize / tickRate;
float maxAngularAcceleration_rptt = def->movement.angularAcceleration_radpss / tickRate;
float maxRotationSpeed_rpt = def->movement.maxRotationSpeed_radps / tickRate;
float sensorRange_tiles = def->sensor.sensorRange_m / tileSize;
entt::entity entity = m_admin.spawnShip(
position, hp, maxHp,
maxSpeed_tpt, mainAcceleration_tptt, maneuveringAcceleration_tptt,
maxAngularAcceleration_rptt, maxRotationSpeed_rpt, sensorRange_tiles,
schematicId, isEnemy);
// Determine module list: configured layout takes precedence over default.
const std::vector<PlacedModule>& modules =
layout.has_value() ? layout->placedModules : def->defaultModules;
// Derive the scrap dropped on destruction from the ship's as-built threat cost
// (REQ-RES-SCRAP-DROP): round(threat * scrap_per_threat), floored at 1 for any
// ship with threat > 0. Computed once here since threat is level-independent.
const double threatCost = calculateShipThreatCost(m_config.threatCosts, m_config,
schematicId, modules);
const int scrapDrop = threatCost > 0.0
? std::max(1, static_cast<int>(std::lround(threatCost * m_config.world.scrapPerThreat)))
: 0;
m_admin.get<ShipIdentityComponent>(entity).scrapDrop = scrapDrop;
// --- Pass 1: create capability child entities ----------------------------
std::vector<entt::entity> weaponChildren;
std::vector<entt::entity> salvageChildren;
std::vector<entt::entity> repairChildren;
// Cargo capacity is a ship-level stat (REQ-MOD-CARGO-CAPACITY): its base is the
// sum of every cargo-providing module's contribution, accumulated here.
double cargoCapacityBase = 0.0;
for (const PlacedModule& pm : modules)
{
const ModuleDef* modDef = findModuleDef(pm.moduleId);
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
if (modDef->weaponCapability)
{
WeaponComponent w;
w.damage = modDef->weaponCapability->damage;
w.range_tiles = modDef->weaponCapability->attackRange_m / tileSize;
w.fireRateHz = modDef->weaponCapability->attackRate_hz;
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)
{
cargoCapacityBase += modDef->salvageCapability->cargoCapacity;
SalvagerComponent salvager;
salvager.collectionRange_tiles = modDef->salvageCapability->collectionRange_m / tileSize;
const double rate = modDef->salvageCapability->collectionRate_hz;
salvager.collectionIntervalTicks = (rate > 0.0)
? static_cast<int>(kTickRateHz / rate + 0.5)
: 0;
salvager.cooldownTicksRemaining = 0;
entt::entity child = m_admin.createModuleEntity();
m_admin.addComponent<SalvagerComponent>(child, salvager);
m_admin.addComponent<ModuleOwnerComponent>(child, ModuleOwnerComponent{entity});
salvageChildren.push_back(child);
}
if (modDef->repairCapability)
{
RepairToolComponent rt;
const double repairRateHz = modDef->repairCapability->repairRate_hz;
rt.repairIntervalTicks = (repairRateHz > 0.0)
? static_cast<int>(kTickRateHz / repairRateHz + 0.5)
: 0;
rt.repairAmountHp = modDef->repairCapability->repairAmountHp;
rt.cooldownTicksRemaining = 0;
rt.range_tiles = modDef->repairCapability->repairRange_m / 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;
// Ship-level cargo capacity modifiers ([module.cargo]); applied to the pool.
std::map<std::string, std::pair<double, double>> cargoMods;
for (const PlacedModule& pm : modules)
{
const ModuleDef* modDef = findModuleDef(pm.moduleId);
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
for (const ModuleStatModifier& sm : modDef->statModifiers)
{
const double val = sm.value;
// 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");
const bool isRepairStat = (sm.stat == "repair_rate"
|| sm.stat == "repair_range");
const bool isCargoStat = (sm.stat == "cargo_capacity");
std::map<std::string, std::pair<double, double>>* target = &hullMods;
if (isWeaponStat) { target = &weaponMods; }
if (isSalvageStat) { target = &salvageMods; }
if (isRepairStat) { target = &repairMods; }
if (isCargoStat) { target = &cargoMods; }
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)
{
SalvagerComponent& c = m_admin.get<SalvagerComponent>(child);
float fRange = c.collectionRange_tiles;
// 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(fRate, "collection_rate", salvageMods);
c.collectionRange_tiles = fRange;
c.collectionIntervalTicks = (fRate > 0.0f)
? static_cast<int>(static_cast<float>(kTickRateHz) / fRate + 0.5f)
: 0;
}
// Cargo capacity is a ship-level stat: apply [module.cargo] modifiers to the
// summed base, then attach the shared cargo pool when the ship can hold anything
// (REQ-MOD-CARGO-CAPACITY).
{
float fCapacity = static_cast<float>(cargoCapacityBase);
applyMod(fCapacity, "cargo_capacity", cargoMods);
const int maxCapacity = static_cast<int>(fCapacity + 0.5f);
if (maxCapacity > 0)
{
m_admin.addComponent<CargoComponent>(entity, CargoComponent{maxCapacity, 0});
}
}
// Apply repair modifiers to each repair child.
for (entt::entity child : repairChildren)
{
RepairToolComponent& rt = m_admin.get<RepairToolComponent>(child);
// Apply rate modifier: compute cycles/s from interval, apply, convert back.
float fRate = (rt.repairIntervalTicks > 0)
? static_cast<float>(kTickRateHz) / static_cast<float>(rt.repairIntervalTicks)
: 0.0f;
applyMod(fRate, "repair_rate", repairMods);
applyMod(rt.range_tiles, "repair_range", repairMods);
rt.repairIntervalTicks = (fRate > 0.0f)
? static_cast<int>(static_cast<float>(kTickRateHz) / fRate + 0.5f)
: 0;
}
// --- 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<SalvagerComponent>(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; // deliveryBay starts unassigned (nullopt)
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);
}
}