320 lines
13 KiB
C++
320 lines
13 KiB
C++
#include "ShipStatsCalculator.h"
|
|
|
|
#include <map>
|
|
#include <stdexcept>
|
|
#include <utility>
|
|
|
|
#include "DynamicBodyComponent.h"
|
|
#include "EntityAdmin.h"
|
|
#include "HealthComponent.h"
|
|
#include "ModuleOwnerComponent.h"
|
|
#include "RepairToolComponent.h"
|
|
#include "SalvageCargoComponent.h"
|
|
#include "SensorRangeComponent.h"
|
|
#include "Tick.h"
|
|
#include "WeaponComponent.h"
|
|
|
|
ShipStats calculateShipStats(const GameConfig& config,
|
|
const std::string& shipId,
|
|
int level,
|
|
const std::vector<PlacedModule>& modules,
|
|
const std::map<std::string, int>& moduleLevelOverrides)
|
|
{
|
|
ShipStats result{};
|
|
|
|
const ShipDef* shipDef = nullptr;
|
|
for (const ShipDef& d : config.ships.ships)
|
|
{
|
|
if (d.id == shipId) { shipDef = &d; break; }
|
|
}
|
|
if (!shipDef) { return result; }
|
|
|
|
auto findModuleDef = [&](const std::string& id) -> const ModuleDef*
|
|
{
|
|
for (const ModuleDef& d : config.modules.modules)
|
|
{
|
|
if (d.id == id) { return &d; }
|
|
}
|
|
return nullptr;
|
|
};
|
|
|
|
const double x = static_cast<double>(level);
|
|
const double tileSize = config.world.tileSize_m;
|
|
|
|
// --- Base hull stats (convert from SI to display units) ------------------
|
|
result.hp = static_cast<float>(
|
|
shipDef->health.hpFormula.evaluate(x));
|
|
result.maxSpeed_tps = static_cast<float>(
|
|
shipDef->movement.speedFormula.evaluate(x) / tileSize);
|
|
result.sensorRange_tiles = static_cast<float>(
|
|
shipDef->sensor.sensorRangeFormula.evaluate(x) / tileSize);
|
|
result.mainAcceleration_tpss = static_cast<float>(
|
|
shipDef->movement.mainAccelerationFormula.evaluate(x) / tileSize);
|
|
result.maneuveringAcceleration_tpss = static_cast<float>(
|
|
shipDef->movement.maneuveringAccelerationFormula.evaluate(x) / tileSize);
|
|
result.angularAcceleration_radpss = static_cast<float>(
|
|
shipDef->movement.angularAccelerationFormula.evaluate(x));
|
|
result.maxRotationSpeed_radps = static_cast<float>(
|
|
shipDef->movement.maxRotationSpeedFormula.evaluate(x));
|
|
|
|
// --- Pass 1: base capability stats per module instance -------------------
|
|
struct WeaponInstance { float damage; float range_tiles; float rate_hz; };
|
|
struct SalvageInstance { float range_tiles; float rate; };
|
|
struct RepairInstance { float rate_hz; float amount_hp; float range_tiles; };
|
|
|
|
std::vector<WeaponInstance> weaponInstances;
|
|
std::vector<SalvageInstance> salvageInstances;
|
|
std::vector<RepairInstance> repairInstances;
|
|
|
|
for (const PlacedModule& pm : modules)
|
|
{
|
|
const ModuleDef* def = findModuleDef(pm.moduleId);
|
|
if (!def) { 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 : def->playerProductionLevel);
|
|
|
|
if (def->weaponCapability)
|
|
{
|
|
WeaponInstance wi;
|
|
wi.damage = static_cast<float>(def->weaponCapability->damageFormula.evaluate(mx));
|
|
wi.range_tiles = static_cast<float>(def->weaponCapability->attackRangeFormula.evaluate(mx) / tileSize);
|
|
wi.rate_hz = static_cast<float>(def->weaponCapability->attackRateFormula.evaluate(mx));
|
|
weaponInstances.push_back(wi);
|
|
}
|
|
if (def->salvageCapability)
|
|
{
|
|
SalvageInstance si;
|
|
si.range_tiles = static_cast<float>(def->salvageCapability->collectionRangeFormula.evaluate(mx) / tileSize);
|
|
si.rate = static_cast<float>(def->salvageCapability->collectionRateFormula.evaluate(mx));
|
|
salvageInstances.push_back(si);
|
|
}
|
|
if (def->repairCapability)
|
|
{
|
|
RepairInstance ri;
|
|
ri.rate_hz = static_cast<float>(def->repairCapability->repairRateFormula.evaluate(mx));
|
|
ri.amount_hp = static_cast<float>(def->repairCapability->repairAmountHpFormula.evaluate(mx));
|
|
ri.range_tiles = static_cast<float>(def->repairCapability->repairRangeFormula.evaluate(mx) / tileSize);
|
|
repairInstances.push_back(ri);
|
|
}
|
|
}
|
|
|
|
// --- Pass 2: accumulate passive stat modifiers ---------------------------
|
|
// Mirrors ShipSystem::spawn() routing logic exactly.
|
|
std::map<std::string, std::pair<double, double>> hullMods;
|
|
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* def = findModuleDef(pm.moduleId);
|
|
if (!def) { 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 : def->playerProductionLevel);
|
|
|
|
for (const ModuleStatModifier& sm : def->statModifiers)
|
|
{
|
|
const double val = sm.formula.evaluate(mx);
|
|
|
|
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 additive modifiers are in metres in config; convert to tiles.
|
|
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 /= tileSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Acceleration additive modifiers are in m/s² in config; convert to tiles/s².
|
|
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 /= tileSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto applyMod = [](float& stat, const std::string& name,
|
|
const std::map<std::string, std::pair<double, double>>& mods)
|
|
{
|
|
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);
|
|
}
|
|
};
|
|
|
|
// Apply hull modifiers.
|
|
applyMod(result.hp, "hp", hullMods);
|
|
applyMod(result.maxSpeed_tps, "speed", hullMods);
|
|
applyMod(result.mainAcceleration_tpss, "main_acceleration", hullMods);
|
|
applyMod(result.maneuveringAcceleration_tpss, "maneuvering_acceleration", hullMods);
|
|
applyMod(result.angularAcceleration_radpss, "angular_acceleration", hullMods);
|
|
applyMod(result.maxRotationSpeed_radps, "max_rotation_speed", hullMods);
|
|
applyMod(result.sensorRange_tiles, "sensor_range", hullMods);
|
|
|
|
// Apply weapon modifiers and compute combined stats.
|
|
if (!weaponInstances.empty())
|
|
{
|
|
float combinedDps = 0.0f;
|
|
float maxRange = 0.0f;
|
|
for (WeaponInstance& wi : weaponInstances)
|
|
{
|
|
applyMod(wi.damage, "damage", weaponMods);
|
|
applyMod(wi.range_tiles, "attack_range", weaponMods);
|
|
applyMod(wi.rate_hz, "attack_rate", weaponMods);
|
|
combinedDps += wi.damage * wi.rate_hz;
|
|
if (wi.range_tiles > maxRange) { maxRange = wi.range_tiles; }
|
|
}
|
|
result.weapons = ShipStats::WeaponStats{combinedDps, maxRange};
|
|
}
|
|
|
|
// Apply salvage modifiers and compute combined stats.
|
|
if (!salvageInstances.empty())
|
|
{
|
|
float combinedRate = 0.0f;
|
|
float maxRange = 0.0f;
|
|
for (SalvageInstance& si : salvageInstances)
|
|
{
|
|
applyMod(si.range_tiles, "collection_range", salvageMods);
|
|
applyMod(si.rate, "collection_rate", salvageMods);
|
|
combinedRate += si.rate;
|
|
if (si.range_tiles > maxRange) { maxRange = si.range_tiles; }
|
|
}
|
|
result.salvage = ShipStats::SalvageStats{combinedRate, maxRange};
|
|
}
|
|
|
|
// Apply repair modifiers and compute combined stats.
|
|
if (!repairInstances.empty())
|
|
{
|
|
float combinedRate = 0.0f;
|
|
float maxRange = 0.0f;
|
|
for (RepairInstance& ri : repairInstances)
|
|
{
|
|
applyMod(ri.rate_hz, "repair_rate", repairMods);
|
|
applyMod(ri.range_tiles, "repair_range", repairMods);
|
|
combinedRate += ri.rate_hz * ri.amount_hp;
|
|
if (ri.range_tiles > maxRange) { maxRange = ri.range_tiles; }
|
|
}
|
|
result.repair = ShipStats::RepairStats{combinedRate, maxRange};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
ShipStats buildShipStatsFromEntity(const EntityAdmin& admin, entt::entity shipEntity)
|
|
{
|
|
ShipStats result{};
|
|
|
|
const HealthComponent& health = admin.get<HealthComponent>(shipEntity);
|
|
const DynamicBodyComponent& body = admin.get<DynamicBodyComponent>(shipEntity);
|
|
const SensorRangeComponent& sensor = admin.get<SensorRangeComponent>(shipEntity);
|
|
|
|
result.hp = health.maxHp;
|
|
result.maxSpeed_tps = body.maxSpeed_tpt * kTickRateHz;
|
|
result.sensorRange_tiles = sensor.value_tiles;
|
|
result.mainAcceleration_tpss = body.mainAcceleration_tptt * kTickRateHz * kTickRateHz;
|
|
result.maneuveringAcceleration_tpss = body.maneuveringAcceleration_tptt * kTickRateHz * kTickRateHz;
|
|
result.angularAcceleration_radpss = body.maxAngularAcceleration_rptt * kTickRateHz * kTickRateHz;
|
|
result.maxRotationSpeed_radps = body.maxRotationSpeed_rpt * kTickRateHz;
|
|
|
|
float weaponDps = 0.0f;
|
|
float weaponMaxRange = 0.0f;
|
|
bool hasWeapons = false;
|
|
|
|
float salvageRate = 0.0f;
|
|
float salvageMaxRange = 0.0f;
|
|
bool hasSalvage = false;
|
|
|
|
float repairRate = 0.0f;
|
|
float repairMaxRange = 0.0f;
|
|
bool hasRepair = false;
|
|
|
|
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
|
|
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const WeaponComponent& w)
|
|
{
|
|
if (owner.owner != shipEntity) { return; }
|
|
hasWeapons = true;
|
|
weaponDps += w.damage * w.fireRateHz;
|
|
if (w.range_tiles > weaponMaxRange) { weaponMaxRange = w.range_tiles; }
|
|
});
|
|
|
|
admin.forEach<ModuleOwnerComponent, SalvageCargoComponent>(
|
|
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const SalvageCargoComponent& s)
|
|
{
|
|
if (owner.owner != shipEntity) { return; }
|
|
hasSalvage = true;
|
|
const float rate = (s.collectionIntervalTicks > 0)
|
|
? static_cast<float>(kTickRateHz) / static_cast<float>(s.collectionIntervalTicks)
|
|
: 0.0f;
|
|
salvageRate += rate;
|
|
if (s.collectionRange_tiles > salvageMaxRange) { salvageMaxRange = s.collectionRange_tiles; }
|
|
});
|
|
|
|
admin.forEach<ModuleOwnerComponent, RepairToolComponent>(
|
|
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const RepairToolComponent& r)
|
|
{
|
|
if (owner.owner != shipEntity) { return; }
|
|
hasRepair = true;
|
|
const float cyclesPerSec = (r.repairIntervalTicks > 0)
|
|
? static_cast<float>(kTickRateHz) / static_cast<float>(r.repairIntervalTicks)
|
|
: 0.0f;
|
|
repairRate += cyclesPerSec * r.repairAmountHp;
|
|
if (r.range_tiles > repairMaxRange) { repairMaxRange = r.range_tiles; }
|
|
});
|
|
|
|
if (hasWeapons) { result.weapons = ShipStats::WeaponStats{weaponDps, weaponMaxRange}; }
|
|
if (hasSalvage) { result.salvage = ShipStats::SalvageStats{salvageRate, salvageMaxRange}; }
|
|
if (hasRepair) { result.repair = ShipStats::RepairStats{repairRate, repairMaxRange}; }
|
|
|
|
return result;
|
|
}
|