Remove schematic upgrades and module and ship levels

This commit is contained in:
2026-07-02 21:32:36 +02:00
parent 81b1c7a66b
commit 18e732ae99
43 changed files with 498 additions and 666 deletions

View File

@@ -284,7 +284,6 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
cfg.waves.shipLevelFormula = requireFormula(tbl["waves"]["ship_level_formula"], file, "waves.ship_level_formula");
cfg.waves.gapMinSeconds = requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds");
cfg.waves.gapMaxSeconds = requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds");
cfg.waves.spawnDurationSeconds = requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
@@ -433,8 +432,6 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
const toml::array& materials = requireArray(bpMt["materials"], file, bpPath + ".materials");
def.schematic.materials = parseIngredients(materials, file, bpPath + ".materials");
def.schematic.playerProductionLevel = static_cast<int>(requireInt(
bpMt["player_production_level"], file, bpPath + ".player_production_level"));
def.schematic.productionTimeSeconds = requireDouble(
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
}
@@ -444,7 +441,7 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
const std::string hPath = elemPath + ".health";
const toml::table& hTable = requireTable(mt["health"], file, hPath);
toml::table& hMt = const_cast<toml::table&>(hTable);
def.health.hpFormula = requireFormula(hMt["hp_formula"], file, hPath + ".hp_formula");
def.health.hp = static_cast<float>(requireDouble(hMt["hp"], file, hPath + ".hp"));
}
// Movement
@@ -452,11 +449,11 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
const std::string mPath = elemPath + ".movement";
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
toml::table& mMt = const_cast<toml::table&>(mTable);
def.movement.speedFormula = requireFormula(mMt["speed_mps_formula"], file, mPath + ".speed_mps_formula");
def.movement.mainAccelerationFormula = requireFormula(mMt["main_acceleration_mpss_formula"], file, mPath + ".main_acceleration_mpss_formula");
def.movement.maneuveringAccelerationFormula = requireFormula(mMt["maneuvering_acceleration_mpss_formula"], file, mPath + ".maneuvering_acceleration_mpss_formula");
def.movement.angularAccelerationFormula = requireFormula(mMt["angular_acceleration_radpss_formula"], file, mPath + ".angular_acceleration_radpss_formula");
def.movement.maxRotationSpeedFormula = requireFormula(mMt["max_rotation_speed_radps_formula"], file, mPath + ".max_rotation_speed_radps_formula");
def.movement.speed_mps = static_cast<float>(requireDouble(mMt["speed_mps"], file, mPath + ".speed_mps"));
def.movement.mainAcceleration_mpss = static_cast<float>(requireDouble(mMt["main_acceleration_mpss"], file, mPath + ".main_acceleration_mpss"));
def.movement.maneuveringAcceleration_mpss = static_cast<float>(requireDouble(mMt["maneuvering_acceleration_mpss"], file, mPath + ".maneuvering_acceleration_mpss"));
def.movement.angularAcceleration_radpss = static_cast<float>(requireDouble(mMt["angular_acceleration_radpss"], file, mPath + ".angular_acceleration_radpss"));
def.movement.maxRotationSpeed_radps = static_cast<float>(requireDouble(mMt["max_rotation_speed_radps"], file, mPath + ".max_rotation_speed_radps"));
}
// Sensor
@@ -464,7 +461,7 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
const std::string snsPath = elemPath + ".sensor";
const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath);
toml::table& snsMt = const_cast<toml::table&>(snsTable);
def.sensor.sensorRangeFormula = requireFormula(snsMt["sensor_range_m_formula"], file, snsPath + ".sensor_range_m_formula");
def.sensor.sensorRange_m = static_cast<float>(requireDouble(snsMt["sensor_range_m"], file, snsPath + ".sensor_range_m"));
}
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
@@ -577,8 +574,6 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
def.unlockAtStationLevel = static_cast<int>(
mt["unlock_at_station_level"].value_or<int64_t>(-1));
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
def.playerProductionLevel = static_cast<int>(requireInt(
mt["player_production_level"], file, elemPath + ".player_production_level"));
def.productionTimeSeconds = requireDouble(
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
@@ -601,15 +596,15 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
elemPath + "." + se.category);
toml::table& catMt = const_cast<toml::table&>(catTable);
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix + "_formula";
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix + "_formula";
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix;
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix;
if (catMt.contains(addedKey))
{
ModuleStatModifier mod;
mod.stat = se.stat;
mod.modifierType = "additive";
mod.formula = requireFormula(catMt[addedKey], file,
mod.value = requireDouble(catMt[addedKey], file,
elemPath + "." + se.category + "." + addedKey);
def.statModifiers.push_back(std::move(mod));
}
@@ -619,7 +614,7 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
ModuleStatModifier mod;
mod.stat = se.stat;
mod.modifierType = "multiplicative";
mod.formula = requireFormula(catMt[multipliedKey], file,
mod.value = requireDouble(catMt[multipliedKey], file,
elemPath + "." + se.category + "." + multipliedKey);
def.statModifiers.push_back(std::move(mod));
}
@@ -631,16 +626,16 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
const std::string wPath = elemPath + ".weapon";
const toml::table& wTable = requireTable(mt["weapon"], file, wPath);
toml::table& wMt = const_cast<toml::table&>(wTable);
if (wMt.contains("damage_formula") || wMt.contains("attack_range_m_formula")
|| wMt.contains("attack_rate_hz_formula"))
if (wMt.contains("damage") || wMt.contains("attack_range_m")
|| wMt.contains("attack_rate_hz"))
{
ModuleWeaponCapability cap;
cap.damageFormula = requireFormula(wMt["damage_formula"],
file, wPath + ".damage_formula");
cap.attackRangeFormula = requireFormula(wMt["attack_range_m_formula"],
file, wPath + ".attack_range_m_formula");
cap.attackRateFormula = requireFormula(wMt["attack_rate_hz_formula"],
file, wPath + ".attack_rate_hz_formula");
cap.damage = static_cast<float>(requireDouble(wMt["damage"],
file, wPath + ".damage"));
cap.attackRange_m = static_cast<float>(requireDouble(wMt["attack_range_m"],
file, wPath + ".attack_range_m"));
cap.attackRate_hz = static_cast<float>(requireDouble(wMt["attack_rate_hz"],
file, wPath + ".attack_rate_hz"));
def.weaponCapability = std::move(cap);
}
}
@@ -651,16 +646,16 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
const std::string sPath = elemPath + ".salvage";
const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
toml::table& sMt = const_cast<toml::table&>(sTable);
if (sMt.contains("collection_range_m_formula") || sMt.contains("cargo_capacity_formula")
|| sMt.contains("collection_rate_hz_formula"))
if (sMt.contains("collection_range_m") || sMt.contains("cargo_capacity")
|| sMt.contains("collection_rate_hz"))
{
ModuleSalvageCapability cap;
cap.collectionRangeFormula = requireFormula(sMt["collection_range_m_formula"],
file, sPath + ".collection_range_m_formula");
cap.cargoCapacityFormula = requireFormula(sMt["cargo_capacity_formula"],
file, sPath + ".cargo_capacity_formula");
cap.collectionRateFormula = requireFormula(sMt["collection_rate_hz_formula"],
file, sPath + ".collection_rate_hz_formula");
cap.collectionRange_m = static_cast<float>(requireDouble(sMt["collection_range_m"],
file, sPath + ".collection_range_m"));
cap.cargoCapacity = static_cast<float>(requireDouble(sMt["cargo_capacity"],
file, sPath + ".cargo_capacity"));
cap.collectionRate_hz = static_cast<float>(requireDouble(sMt["collection_rate_hz"],
file, sPath + ".collection_rate_hz"));
def.salvageCapability = std::move(cap);
}
}
@@ -671,15 +666,15 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
const std::string rPath = elemPath + ".repair";
const toml::table& rTable = requireTable(mt["repair"], file, rPath);
toml::table& rMt = const_cast<toml::table&>(rTable);
if (rMt.contains("repair_rate_hz_formula") || rMt.contains("repair_range_m_formula"))
if (rMt.contains("repair_rate_hz") || rMt.contains("repair_range_m"))
{
ModuleRepairCapability cap;
cap.repairRateFormula = requireFormula(rMt["repair_rate_hz_formula"],
file, rPath + ".repair_rate_hz_formula");
cap.repairAmountHpFormula = requireFormula(rMt["repair_amount_hp_formula"],
file, rPath + ".repair_amount_hp_formula");
cap.repairRangeFormula = requireFormula(rMt["repair_range_m_formula"],
file, rPath + ".repair_range_m_formula");
cap.repairRate_hz = static_cast<float>(requireDouble(rMt["repair_rate_hz"],
file, rPath + ".repair_rate_hz"));
cap.repairAmountHp = static_cast<float>(requireDouble(rMt["repair_amount_hp"],
file, rPath + ".repair_amount_hp"));
cap.repairRange_m = static_cast<float>(requireDouble(rMt["repair_range_m"],
file, rPath + ".repair_range_m"));
def.repairCapability = std::move(cap);
}
}

View File

@@ -4,7 +4,6 @@
#include <string>
#include <vector>
#include "Formula.h"
#include "RecipesConfig.h"
// A single stat modifier contributed by a module instance.
@@ -13,29 +12,29 @@ struct ModuleStatModifier
{
std::string stat; // e.g. "hp", "speed", "sensor_range"
std::string modifierType; // "additive" or "multiplicative"
Formula formula;
double value;
};
// Capability sections — present when the module grants that capability.
struct ModuleWeaponCapability
{
Formula damageFormula;
Formula attackRangeFormula;
Formula attackRateFormula;
float damage;
float attackRange_m;
float attackRate_hz;
};
struct ModuleSalvageCapability
{
Formula collectionRangeFormula;
Formula cargoCapacityFormula;
Formula collectionRateFormula;
float collectionRange_m;
float cargoCapacity;
float collectionRate_hz;
};
struct ModuleRepairCapability
{
Formula repairRateFormula; // repair cycles per second
Formula repairAmountHpFormula; // HP restored per cycle
Formula repairRangeFormula;
float repairRate_hz; // repair cycles per second
float repairAmountHp; // HP restored per cycle
float repairRange_m;
};
struct ModuleDef
@@ -44,7 +43,6 @@ struct ModuleDef
int unlockAtStationLevel;
std::vector<std::string> surfaceMask;
std::vector<RecipeIngredient> materials;
int playerProductionLevel;
double productionTimeSeconds;
std::string fillColor;
std::string glyph;

View File

@@ -3,36 +3,33 @@
#include <string>
#include <vector>
#include "Formula.h"
#include "RecipesConfig.h" // for RecipeIngredient
#include "ShipLayout.h" // for PlacedModule
// Build materials and initial per-schematic production level
// (REQ-BLD-SHIPYARD, REQ-DEF-SCHEMATIC-DROP).
// Build materials and base production time (REQ-BLD-SHIPYARD, REQ-DEF-SCHEMATIC-DROP).
struct ShipSchematic
{
std::vector<RecipeIngredient> materials;
int playerProductionLevel;
double productionTimeSeconds;
};
struct ShipHealth
{
Formula hpFormula; // REQ-SHP-STATS
float hp; // REQ-SHP-STATS
};
struct ShipMovement
{
Formula speedFormula; // max linear speed cap, tiles/s (REQ-SHP-STATS, REQ-SHP-MOVEMENT)
Formula mainAccelerationFormula; // forward acceleration, tiles/s²
Formula maneuveringAccelerationFormula;// omnidirectional acceleration, tiles/s²
Formula angularAccelerationFormula; // angular acceleration, rad/s²
Formula maxRotationSpeedFormula; // angular velocity cap, rad/s
float speed_mps; // max linear speed cap, m/s (REQ-SHP-STATS, REQ-SHP-MOVEMENT)
float mainAcceleration_mpss; // forward acceleration, m/s²
float maneuveringAcceleration_mpss;// omnidirectional acceleration, m/s²
float angularAcceleration_radpss; // angular acceleration, rad/s²
float maxRotationSpeed_radps; // angular velocity cap, rad/s
};
struct ShipSensor
{
Formula sensorRangeFormula; // REQ-SHP-SENSOR, REQ-SHP-STATS
float sensorRange_m; // REQ-SHP-SENSOR, REQ-SHP-STATS
};
struct ShipDef

View File

@@ -29,7 +29,6 @@ struct WorldPush
struct WorldWaves
{
Formula threatRateFormula; // threat/s as a function of boss wave counter x
Formula shipLevelFormula; // enemy ship level as a function of boss wave counter x
double gapMinSeconds;
double gapMaxSeconds;
double spawnDurationSeconds;

View File

@@ -42,7 +42,7 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
float maxSpeed_tpt, float mainAcceleration_tptt,
float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
float maxRotationSpeed_rpt, float sensorRange_tiles,
int level, const std::string& schematicId, bool isEnemy)
const std::string& schematicId, bool isEnemy)
{
entt::entity entity = createEntity();
add<PositionComponent>(entity, PositionComponent{position});
@@ -61,7 +61,7 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
0.0f // angularAcceleration_rptt
});
add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
add<ShipIdentityComponent>(entity, ShipIdentityComponent{level, schematicId});
add<ShipIdentityComponent>(entity, ShipIdentityComponent{schematicId});
add<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
return entity;
}

View File

@@ -56,7 +56,7 @@ public:
float maxSpeed_tpt, float mainAcceleration_tptt,
float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
float maxRotationSpeed_rpt, float sensorRange_tiles,
int level, const std::string& schematicId, bool isEnemy);
const std::string& schematicId, bool isEnemy);
entt::entity spawnStation(QPoint anchor, QSize footprint,
const std::vector<QPoint>& bodyCells,

View File

@@ -19,8 +19,6 @@ struct SchematicChoiceOption
std::string schematicId;
SchematicType type;
std::string displayName;
bool isNewUnlock;
int targetLevel;
// Display names of items produced by recipes that would newly become
// implicitly unlocked (REQ-LOCK-IMPLICIT) if this option is selected.

View File

@@ -4,7 +4,6 @@
struct ShipIdentityComponent
{
int level;
std::string schematicId;
// Scrap dropped on destruction, derived from the ship's as-built threat cost
// at spawn time (REQ-RES-SCRAP-DROP).

View File

@@ -65,43 +65,31 @@ const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
return nullptr;
}
entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
entt::entity ShipSystem::spawn(const std::string& schematicId,
QVector2D position, bool isEnemy,
const std::optional<ShipLayoutConfig>& layout,
const std::map<std::string, int>& moduleLevelOverrides)
const std::optional<ShipLayoutConfig>& layout)
{
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 hp = def->health.hp;
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))
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 = 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;
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,
level, schematicId, isEnemy);
schematicId, isEnemy);
// Determine module list: configured layout takes precedence over default.
const std::vector<PlacedModule>& modules =
@@ -131,19 +119,12 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
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.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;
@@ -155,12 +136,11 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
if (modDef->salvageCapability)
{
cargoCapacityBase += modDef->salvageCapability->cargoCapacityFormula.evaluate(mx);
cargoCapacityBase += modDef->salvageCapability->cargoCapacity;
SalvagerComponent salvager;
salvager.collectionRange_tiles = static_cast<float>(
modDef->salvageCapability->collectionRangeFormula.evaluate(mx)) / tileSize;
const double rate = modDef->salvageCapability->collectionRateFormula.evaluate(mx);
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;
@@ -175,16 +155,13 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
if (modDef->repairCapability)
{
RepairToolComponent rt;
const double repairRateHz =
modDef->repairCapability->repairRateFormula.evaluate(mx);
const double repairRateHz = modDef->repairCapability->repairRate_hz;
rt.repairIntervalTicks = (repairRateHz > 0.0)
? static_cast<int>(kTickRateHz / repairRateHz + 0.5)
: 0;
rt.repairAmountHp = static_cast<float>(
modDef->repairCapability->repairAmountHpFormula.evaluate(mx));
rt.repairAmountHp = modDef->repairCapability->repairAmountHp;
rt.cooldownTicksRemaining = 0;
rt.range_tiles = static_cast<float>(
modDef->repairCapability->repairRangeFormula.evaluate(mx)) / tileSize;
rt.range_tiles = modDef->repairCapability->repairRange_m / tileSize;
rt.currentTarget = std::nullopt;
entt::entity child = m_admin.createModuleEntity();
@@ -210,13 +187,9 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
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);
const double val = sm.value;
// Route modifier to the correct accumulator by stat category.
// weapon/salvage/repair stats go to the corresponding child map;

View File

@@ -18,10 +18,9 @@ class ShipSystem
public:
ShipSystem(const GameConfig& config, EntityAdmin& admin);
entt::entity spawn(const std::string& schematicId, int level, QVector2D position,
entt::entity spawn(const std::string& schematicId, QVector2D position,
bool isEnemy = false,
const std::optional<ShipLayoutConfig>& layout = std::nullopt,
const std::map<std::string, int>& moduleLevelOverrides = {});
const std::optional<ShipLayoutConfig>& layout = std::nullopt);
void despawn(entt::entity entity);
// Reset all movement intents to inactive before behavior systems run.

View File

@@ -17,9 +17,7 @@
ShipStats calculateShipStats(const GameConfig& config,
const std::string& shipId,
int level,
const std::vector<PlacedModule>& modules,
const std::map<std::string, int>& moduleLevelOverrides)
const std::vector<PlacedModule>& modules)
{
ShipStats result{};
@@ -39,24 +37,20 @@ ShipStats calculateShipStats(const GameConfig& config,
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.hp = shipDef->health.hp;
result.maxSpeed_tps = static_cast<float>(
shipDef->movement.speedFormula.evaluate(x) / tileSize);
shipDef->movement.speed_mps / tileSize);
result.sensorRange_tiles = static_cast<float>(
shipDef->sensor.sensorRangeFormula.evaluate(x) / tileSize);
shipDef->sensor.sensorRange_m / tileSize);
result.mainAcceleration_tpss = static_cast<float>(
shipDef->movement.mainAccelerationFormula.evaluate(x) / tileSize);
shipDef->movement.mainAcceleration_mpss / 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));
shipDef->movement.maneuveringAcceleration_mpss / tileSize);
result.angularAcceleration_radpss = shipDef->movement.angularAcceleration_radpss;
result.maxRotationSpeed_radps = shipDef->movement.maxRotationSpeed_radps;
// --- Pass 1: base capability stats per module instance -------------------
struct WeaponInstance { float damage; float range_tiles; float rate_hz; };
@@ -76,33 +70,29 @@ ShipStats calculateShipStats(const GameConfig& config,
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));
wi.damage = def->weaponCapability->damage;
wi.range_tiles = static_cast<float>(def->weaponCapability->attackRange_m / tileSize);
wi.rate_hz = def->weaponCapability->attackRate_hz;
weaponInstances.push_back(wi);
}
if (def->salvageCapability)
{
cargoCapacityBase += def->salvageCapability->cargoCapacityFormula.evaluate(mx);
cargoCapacityBase += def->salvageCapability->cargoCapacity;
SalvageInstance si;
si.range_tiles = static_cast<float>(def->salvageCapability->collectionRangeFormula.evaluate(mx) / tileSize);
si.rate = static_cast<float>(def->salvageCapability->collectionRateFormula.evaluate(mx));
si.range_tiles = static_cast<float>(def->salvageCapability->collectionRange_m / tileSize);
si.rate = def->salvageCapability->collectionRate_hz;
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);
ri.rate_hz = def->repairCapability->repairRate_hz;
ri.amount_hp = def->repairCapability->repairAmountHp;
ri.range_tiles = static_cast<float>(def->repairCapability->repairRange_m / tileSize);
repairInstances.push_back(ri);
}
}
@@ -120,13 +110,9 @@ ShipStats calculateShipStats(const GameConfig& config,
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 double val = sm.value;
const bool isWeaponStat = (sm.stat == "damage"
|| sm.stat == "attack_range"

View File

@@ -51,9 +51,7 @@ struct ShipStats
ShipStats calculateShipStats(const GameConfig& config,
const std::string& shipId,
int level,
const std::vector<PlacedModule>& modules,
const std::map<std::string, int>& moduleLevelOverrides = {});
const std::vector<PlacedModule>& modules);
ShipStats buildShipStatsFromEntity(const EntityAdmin& admin, entt::entity shipEntity);

View File

@@ -61,13 +61,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
{
return;
}
std::map<std::string, int> moduleLevels;
for (const auto& [mId, mState] : m_moduleSchematicLevels)
{
moduleLevels[mId] = mState.level;
}
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout,
moduleLevels);
m_shipSystem->spawn(id, pos, /*isEnemy=*/false, layout);
},
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
m_rng);
@@ -86,7 +80,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
{
SchematicState state;
state.unlocked = (def.unlockAtStationLevel == -1);
state.level = (def.unlockAtStationLevel == -1) ? def.schematic.playerProductionLevel : 0;
m_schematicLevels[def.id] = state;
}
@@ -95,7 +88,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
{
SchematicState state;
state.unlocked = (def.unlockAtStationLevel == -1);
state.level = (def.unlockAtStationLevel == -1) ? def.playerProductionLevel : 0;
m_moduleSchematicLevels[def.id] = state;
}
@@ -167,13 +159,7 @@ void Simulation::reset(unsigned int seed)
{
return;
}
std::map<std::string, int> moduleLevels;
for (const auto& [mId, mState] : m_moduleSchematicLevels)
{
moduleLevels[mId] = mState.level;
}
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout,
moduleLevels);
m_shipSystem->spawn(id, pos, /*isEnemy=*/false, layout);
},
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
m_rng);
@@ -192,7 +178,6 @@ void Simulation::reset(unsigned int seed)
{
SchematicState state;
state.unlocked = (def.unlockAtStationLevel == -1);
state.level = (def.unlockAtStationLevel == -1) ? def.schematic.playerProductionLevel : 0;
m_schematicLevels[def.id] = state;
}
@@ -201,7 +186,6 @@ void Simulation::reset(unsigned int seed)
{
SchematicState state;
state.unlocked = (def.unlockAtStationLevel == -1);
state.level = (def.unlockAtStationLevel == -1) ? def.playerProductionLevel : 0;
m_moduleSchematicLevels[def.id] = state;
}
@@ -643,19 +627,21 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
struct PoolEntry { std::string id; DropType type; };
std::vector<PoolEntry> pool;
// Owned schematics leave the pool (REQ-DEF-SCHEMATIC-DROP): only offer
// ship/module schematics that are gated to a station level in range and not
// yet unlocked. Schematics with unlock_at_station_level -1 start unlocked, so
// they are always owned and never eligible.
for (const ShipDef& def : m_config.ships.ships)
{
if (def.unlockAtStationLevel == -1 || def.unlockAtStationLevel <= destroyedStationLevel)
{
pool.push_back({def.id, DropType::Ship});
}
if (def.unlockAtStationLevel < 0 || def.unlockAtStationLevel > destroyedStationLevel) { continue; }
if (m_schematicLevels.at(def.id).unlocked) { continue; }
pool.push_back({def.id, DropType::Ship});
}
for (const ModuleDef& def : m_config.modules.modules)
{
if (def.unlockAtStationLevel == -1 || def.unlockAtStationLevel <= destroyedStationLevel)
{
pool.push_back({def.id, DropType::Module});
}
if (def.unlockAtStationLevel < 0 || def.unlockAtStationLevel > destroyedStationLevel) { continue; }
if (m_moduleSchematicLevels.at(def.id).unlocked) { continue; }
pool.push_back({def.id, DropType::Module});
}
for (const RecipeDef& def : m_config.recipes.recipes)
{
@@ -704,23 +690,15 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
{
option.type = SchematicType::Ship;
option.displayName = toDisplayName(entry.id);
const SchematicState& state = m_schematicLevels.at(entry.id);
option.isNewUnlock = !state.unlocked;
option.targetLevel = state.level + 1;
}
else if (entry.type == DropType::Module)
{
option.type = SchematicType::Module;
option.displayName = toDisplayName(entry.id);
const SchematicState& state = m_moduleSchematicLevels.at(entry.id);
option.isNewUnlock = !state.unlocked;
option.targetLevel = state.level + 1;
}
else
{
option.type = SchematicType::Recipe;
option.isNewUnlock = true;
option.targetLevel = 0;
for (const RecipeDef& def : m_config.recipes.recipes)
{
if (def.id == entry.id && !def.outputs.empty())
@@ -736,11 +714,11 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
std::set<std::string> hypotheticalModuleIds = currentModuleIds;
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
if (entry.type == DropType::Ship && option.isNewUnlock)
if (entry.type == DropType::Ship)
{
hypotheticalShipIds.insert(entry.id);
}
else if (entry.type == DropType::Module && option.isNewUnlock)
else if (entry.type == DropType::Module)
{
hypotheticalModuleIds.insert(entry.id);
}
@@ -762,8 +740,6 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
artifactOption.schematicId = "";
artifactOption.type = SchematicType::Artifact;
artifactOption.displayName = "Artifact";
artifactOption.isNewUnlock = false;
artifactOption.targetLevel = 0;
m_pendingSchematicChoices.push_back(std::move(artifactOption));
}
}
@@ -794,7 +770,6 @@ void Simulation::applySchematicChoice(int choiceIndex)
? m_moduleSchematicLevels.at(chosen.schematicId)
: m_schematicLevels.at(chosen.schematicId);
state.unlocked = true;
state.level += 1;
}
recomputeUnlocked();
@@ -957,7 +932,6 @@ void Simulation::appendSchematicMap(Hasher& hasher,
{
hasher.append(entry.first);
hasher.append(entry.second.unlocked);
hasher.append(entry.second.level);
}
}
@@ -1050,7 +1024,6 @@ unsigned long long Simulation::computeStateChecksum() const
[&hasher](entt::entity entity, const ShipIdentityComponent& c)
{
hasher.append(static_cast<std::uint32_t>(entity));
hasher.append(c.level);
hasher.append(c.schematicId);
});
@@ -1148,17 +1121,6 @@ Tick Simulation::normalGapRemainingTicks() const
return m_waveSystem->normalGapRemainingTicks();
}
int Simulation::schematicLevel(const std::string& shipId) const
{
const std::map<std::string, SchematicState>::const_iterator it =
m_schematicLevels.find(shipId);
if (it == m_schematicLevels.end())
{
return 0;
}
return it->second.level;
}
bool Simulation::isSchematicUnlocked(const std::string& shipId) const
{
const std::map<std::string, SchematicState>::const_iterator it =
@@ -1170,17 +1132,6 @@ bool Simulation::isSchematicUnlocked(const std::string& shipId) const
return it->second.unlocked;
}
int Simulation::moduleSchematicLevel(const std::string& moduleId) const
{
const std::map<std::string, SchematicState>::const_iterator it =
m_moduleSchematicLevels.find(moduleId);
if (it == m_moduleSchematicLevels.end())
{
return 0;
}
return it->second.level;
}
bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
{
const std::map<std::string, SchematicState>::const_iterator it =

View File

@@ -83,12 +83,10 @@ public:
Tick bossCountdownTicks() const;
Tick normalGapRemainingTicks() const;
// Ship schematic state queries.
int schematicLevel(const std::string& shipId) const;
// Ship schematic state query.
bool isSchematicUnlocked(const std::string& shipId) const;
// Module schematic state queries.
int moduleSchematicLevel(const std::string& moduleId) const;
// Module schematic state query.
bool isModuleSchematicUnlocked(const std::string& moduleId) const;
// Implicit recipe/item unlock queries (REQ-LOCK-IMPLICIT).
@@ -182,7 +180,6 @@ private:
struct SchematicState
{
bool unlocked;
int level;
};
std::map<std::string, SchematicState> m_schematicLevels;
std::map<std::string, SchematicState> m_moduleSchematicLevels;

View File

@@ -55,7 +55,7 @@ void WaveSystem::tickWaveScheduler(Tick currentTick, ShipSystem& ships,
{
if (currentTick >= entry.spawnAt)
{
ships.spawn(entry.schematicId, entry.level, entry.position,
ships.spawn(entry.schematicId, entry.position,
/*isEnemy=*/true, entry.layout);
}
else
@@ -174,11 +174,7 @@ std::vector<WaveSystem::SpawnEntry> WaveSystem::selectWaveShips(double& budget,
Tick currentTick,
int worldHeightTiles)
{
const int shipLevel = std::max(1, static_cast<int>(
m_config.world.waves.shipLevelFormula.evaluate(
static_cast<double>(m_bossWaveCounter))));
// Build eligible ship list with their costs at the current level.
// Build eligible ship list with their (level-independent) threat costs.
struct EligibleShip
{
std::string schematicId;
@@ -242,7 +238,6 @@ std::vector<WaveSystem::SpawnEntry> WaveSystem::selectWaveShips(double& budget,
SpawnEntry entry;
entry.schematicId = chosen.schematicId;
entry.level = shipLevel;
entry.spawnAt = 0; // set below after all picks are done
entry.position = QVector2D(xDist(m_rng),
static_cast<float>(yDist(m_rng)) + 0.5f);

View File

@@ -59,7 +59,6 @@ private:
struct SpawnEntry
{
std::string schematicId;
int level;
Tick spawnAt;
QVector2D position;
ShipLayoutConfig layout;