add ship stats panel to ship layout dialog

This commit is contained in:
2026-06-06 21:21:48 +02:00
parent 8dad554800
commit 37a70ea321
9 changed files with 562 additions and 24 deletions

View File

@@ -7,6 +7,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprint.h
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
PARENT_SCOPE
)
@@ -17,6 +18,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
PARENT_SCOPE
)

View File

@@ -0,0 +1,218 @@
#include "ShipStatsCalculator.h"
#include <map>
#include <utility>
ShipStats calculateShipStats(const GameConfig& config,
const std::string& shipId,
int level,
const std::vector<PlacedModule>& modules)
{
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_hps; 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) { continue; }
const double mx = static_cast<double>(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_hps = static_cast<float>(def->repairCapability->repairRateFormula.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) { continue; }
const double mx = static_cast<double>(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;
}
}
}
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_hps, "repair_rate", repairMods);
applyMod(ri.range_tiles, "repair_range", repairMods);
combinedRate += ri.rate_hps;
if (ri.range_tiles > maxRange) { maxRange = ri.range_tiles; }
}
result.repair = ShipStats::RepairStats{combinedRate, maxRange};
}
return result;
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
#include "GameConfig.h"
#include "ShipLayout.h"
// Effective stats for a ship with a given layout, after applying all passive
// module modifiers per REQ-MOD-STAT-CALC. Values are in display units:
// speeds in tiles/s, ranges in tiles, accelerations in tiles/s² or rad/s².
struct ShipStats
{
float hp;
float maxSpeed_tps;
float sensorRange_tiles;
float mainAcceleration_tpss;
float maneuveringAcceleration_tpss;
float angularAcceleration_radpss;
float maxRotationSpeed_radps;
struct WeaponStats
{
float combinedDps;
float maxRange_tiles;
};
struct SalvageStats
{
float combinedCollectionRate;
float maxRange_tiles;
};
struct RepairStats
{
float combinedRepairRate_hps;
float maxRange_tiles;
};
std::optional<WeaponStats> weapons;
std::optional<SalvageStats> salvage;
std::optional<RepairStats> repair;
};
ShipStats calculateShipStats(const GameConfig& config,
const std::string& shipId,
int level,
const std::vector<PlacedModule>& modules);