#include "ShipStatsCalculator.h" #include #include #include #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& modules, const std::map& 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(level); const double tileSize = config.world.tileSize_m; // --- Base hull stats (convert from SI to display units) ------------------ result.hp = static_cast( shipDef->health.hpFormula.evaluate(x)); result.maxSpeed_tps = static_cast( shipDef->movement.speedFormula.evaluate(x) / tileSize); result.sensorRange_tiles = static_cast( shipDef->sensor.sensorRangeFormula.evaluate(x) / tileSize); result.mainAcceleration_tpss = static_cast( shipDef->movement.mainAccelerationFormula.evaluate(x) / tileSize); result.maneuveringAcceleration_tpss = static_cast( shipDef->movement.maneuveringAccelerationFormula.evaluate(x) / tileSize); result.angularAcceleration_radpss = static_cast( shipDef->movement.angularAccelerationFormula.evaluate(x)); result.maxRotationSpeed_radps = static_cast( 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 weaponInstances; std::vector salvageInstances; std::vector 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( overIt != moduleLevelOverrides.end() ? overIt->second : def->playerProductionLevel); if (def->weaponCapability) { WeaponInstance wi; wi.damage = static_cast(def->weaponCapability->damageFormula.evaluate(mx)); wi.range_tiles = static_cast(def->weaponCapability->attackRangeFormula.evaluate(mx) / tileSize); wi.rate_hz = static_cast(def->weaponCapability->attackRateFormula.evaluate(mx)); weaponInstances.push_back(wi); } if (def->salvageCapability) { SalvageInstance si; si.range_tiles = static_cast(def->salvageCapability->collectionRangeFormula.evaluate(mx) / tileSize); si.rate = static_cast(def->salvageCapability->collectionRateFormula.evaluate(mx)); salvageInstances.push_back(si); } if (def->repairCapability) { RepairInstance ri; ri.rate_hz = static_cast(def->repairCapability->repairRateFormula.evaluate(mx)); ri.amount_hp = static_cast(def->repairCapability->repairAmountHpFormula.evaluate(mx)); ri.range_tiles = static_cast(def->repairCapability->repairRangeFormula.evaluate(mx) / tileSize); repairInstances.push_back(ri); } } // --- Pass 2: accumulate passive stat modifiers --------------------------- // Mirrors ShipSystem::spawn() routing logic exactly. std::map> hullMods; std::map> weaponMods; std::map> salvageMods; std::map> 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( 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>* target = &hullMods; if (isWeaponStat) { target = &weaponMods; } if (isSalvageStat) { target = &salvageMods; } if (isRepairStat) { target = &repairMods; } std::pair& 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>* allModMaps[] = { &hullMods, &weaponMods, &salvageMods, &repairMods }; for (const char* stat : kRangeStats) { for (std::map>* mods : allModMaps) { std::map>::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>* mods : allModMaps) { std::map>::iterator it = mods->find(stat); if (it != mods->end()) { it->second.second /= tileSize; } } } auto applyMod = [](float& stat, const std::string& name, const std::map>& mods) { const std::map>::const_iterator it = mods.find(name); if (it != mods.end()) { stat = static_cast( static_cast(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(shipEntity); const DynamicBodyComponent& body = admin.get(shipEntity); const SensorRangeComponent& sensor = admin.get(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( [&](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( [&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const SalvageCargoComponent& s) { if (owner.owner != shipEntity) { return; } hasSalvage = true; const float rate = (s.collectionIntervalTicks > 0) ? static_cast(kTickRateHz) / static_cast(s.collectionIntervalTicks) : 0.0f; salvageRate += rate; if (s.collectionRange_tiles > salvageMaxRange) { salvageMaxRange = s.collectionRange_tiles; } }); admin.forEach( [&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const RepairToolComponent& r) { if (owner.owner != shipEntity) { return; } hasRepair = true; const float cyclesPerSec = (r.repairIntervalTicks > 0) ? static_cast(kTickRateHz) / static_cast(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; }