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

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);
}
}