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

@@ -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;