allow to unlock modules when destroying defence stations

This commit is contained in:
2026-06-10 21:43:48 +02:00
parent aad094f842
commit af96b95f61
19 changed files with 203 additions and 51 deletions

View File

@@ -570,8 +570,10 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
toml::table& mt = const_cast<toml::table&>(*st);
ModuleDef def;
def.id = requireString(mt["id"], file, elemPath + ".id");
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
def.id = requireString(mt["id"], file, elemPath + ".id");
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(

View File

@@ -40,6 +40,7 @@ struct ModuleRepairCapability
struct ModuleDef
{
std::string id;
int unlockAtStationLevel;
std::vector<std::string> surfaceMask;
std::vector<RecipeIngredient> materials;
int playerProductionLevel;

View File

@@ -5,10 +5,11 @@
// Emitted in tick step 9 (Deaths & loot) when a destroyed enemy-defence-station
// set awards a schematic (REQ-DEF-SCHEMATIC-DROP). The UI renders a toast
// (REQ-UI-SCHEMATIC-TOAST); wasNewUnlock chooses between the "unlocked" and
// "level -> N" wording.
// "level -> N" wording. isModuleSchematic selects ship vs. module toast text.
struct SchematicDropEvent
{
std::string schematicId; // matches ShipDef::id in the config.
std::string schematicId; // matches ShipDef::id or ModuleDef::id in the config.
int newLevel;
bool wasNewUnlock;
bool isModuleSchematic;
};

View File

@@ -56,7 +56,8 @@ const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
QVector2D position, bool isEnemy,
const std::optional<ShipLayoutConfig>& layout)
const std::optional<ShipLayoutConfig>& layout,
const std::map<std::string, int>& moduleLevelOverrides)
{
const ShipDef* def = findShipDef(schematicId);
assert(def != nullptr);
@@ -105,7 +106,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 double mx = static_cast<double>(modDef->playerProductionLevel);
const auto overIt = moduleLevelOverrides.find(pm.moduleId);
const double mx = static_cast<double>(
overIt != moduleLevelOverrides.end() ? overIt->second : modDef->playerProductionLevel);
if (modDef->weaponCapability)
{
@@ -176,7 +179,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 double mx = static_cast<double>(modDef->playerProductionLevel);
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)
{

View File

@@ -1,5 +1,6 @@
#pragma once
#include <map>
#include <optional>
#include <string>
@@ -19,7 +20,8 @@ public:
entt::entity spawn(const std::string& schematicId, int level, QVector2D position,
bool isEnemy = false,
const std::optional<ShipLayoutConfig>& layout = std::nullopt);
const std::optional<ShipLayoutConfig>& layout = std::nullopt,
const std::map<std::string, int>& moduleLevelOverrides = {});
void despawn(entt::entity entity);
// Reset all movement intents to priority 0 before behavior systems run.

View File

@@ -17,7 +17,8 @@
ShipStats calculateShipStats(const GameConfig& config,
const std::string& shipId,
int level,
const std::vector<PlacedModule>& modules)
const std::vector<PlacedModule>& modules,
const std::map<std::string, int>& moduleLevelOverrides)
{
ShipStats result{};
@@ -70,7 +71,9 @@ ShipStats calculateShipStats(const GameConfig& config,
const ModuleDef* def = findModuleDef(pm.moduleId);
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
const double mx = static_cast<double>(def->playerProductionLevel);
const auto overIt = moduleLevelOverrides.find(pm.moduleId);
const double mx = static_cast<double>(
overIt != moduleLevelOverrides.end() ? overIt->second : def->playerProductionLevel);
if (def->weaponCapability)
{
@@ -108,7 +111,9 @@ ShipStats calculateShipStats(const GameConfig& config,
const ModuleDef* def = findModuleDef(pm.moduleId);
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
const double mx = static_cast<double>(def->playerProductionLevel);
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)
{

View File

@@ -1,5 +1,6 @@
#pragma once
#include <map>
#include <optional>
#include <string>
#include <vector>
@@ -50,7 +51,8 @@ struct ShipStats
ShipStats calculateShipStats(const GameConfig& config,
const std::string& shipId,
int level,
const std::vector<PlacedModule>& modules);
const std::vector<PlacedModule>& modules,
const std::map<std::string, int>& moduleLevelOverrides = {});
ShipStats buildShipStatsFromEntity(const EntityAdmin& admin, entt::entity shipEntity);

View File

@@ -51,7 +51,13 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
{
return;
}
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
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_rng);
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
@@ -62,7 +68,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
m_combatSystem = std::make_unique<CombatSystem>(m_config);
// Initialize schematic unlock state.
// Initialize ship schematic unlock state.
for (const ShipDef& def : m_config.ships.ships)
{
SchematicState state;
@@ -71,6 +77,15 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
m_schematicLevels[def.id] = state;
}
// Initialize module schematic unlock state.
for (const ModuleDef& def : m_config.modules.modules)
{
SchematicState state;
state.unlocked = (def.unlockAtStationLevel == -1);
state.level = (def.unlockAtStationLevel == -1) ? def.playerProductionLevel : 0;
m_moduleSchematicLevels[def.id] = state;
}
placeInitialStructures();
registerForEvents();
}
@@ -124,7 +139,13 @@ void Simulation::reset(unsigned int seed)
{
return;
}
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
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_rng);
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
@@ -144,6 +165,15 @@ void Simulation::reset(unsigned int seed)
m_schematicLevels[def.id] = state;
}
m_moduleSchematicLevels.clear();
for (const ModuleDef& def : m_config.modules.modules)
{
SchematicState state;
state.unlocked = (def.unlockAtStationLevel == -1);
state.level = (def.unlockAtStationLevel == -1) ? def.playerProductionLevel : 0;
m_moduleSchematicLevels[def.id] = state;
}
placeInitialStructures();
}
@@ -482,28 +512,37 @@ void Simulation::tickDeathsAndLoot()
void Simulation::awardSchematicDrop(int destroyedStationLevel)
{
std::vector<std::string> ids;
ids.reserve(m_config.ships.ships.size());
std::vector<std::pair<std::string, bool>> pool; // (id, isModule)
for (const ShipDef& def : m_config.ships.ships)
{
if (def.unlockAtStationLevel == -1 || def.unlockAtStationLevel <= destroyedStationLevel)
{
ids.push_back(def.id);
pool.push_back({def.id, false});
}
}
for (const ModuleDef& def : m_config.modules.modules)
{
if (def.unlockAtStationLevel == -1 || def.unlockAtStationLevel <= destroyedStationLevel)
{
pool.push_back({def.id, true});
}
}
std::uniform_int_distribution<int> dist(0, static_cast<int>(ids.size()) - 1);
const std::string chosen = ids[static_cast<std::size_t>(dist(m_rng))];
std::uniform_int_distribution<int> dist(0, static_cast<int>(pool.size()) - 1);
const auto& [chosen, isModule] = pool[static_cast<std::size_t>(dist(m_rng))];
SchematicState& state = m_schematicLevels.at(chosen);
SchematicState& state = isModule
? m_moduleSchematicLevels.at(chosen)
: m_schematicLevels.at(chosen);
const bool wasNew = !state.unlocked;
state.unlocked = true;
state.level += 1;
SchematicDropEvent evt;
evt.schematicId = chosen;
evt.newLevel = state.level;
evt.wasNewUnlock = wasNew;
evt.schematicId = chosen;
evt.newLevel = state.level;
evt.wasNewUnlock = wasNew;
evt.isModuleSchematic = isModule;
m_schematicDropEvents.push_back(evt);
}
@@ -586,6 +625,28 @@ 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 =
m_moduleSchematicLevels.find(moduleId);
if (it == m_moduleSchematicLevels.end())
{
return false;
}
return it->second.unlocked;
}
BuildingId Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
{
int cost = 0;

View File

@@ -62,10 +62,14 @@ public:
Tick bossCountdownTicks() const;
Tick normalGapRemainingTicks() const;
// Schematic state queries.
// Ship schematic state queries.
int schematicLevel(const std::string& shipId) const;
bool isSchematicUnlocked(const std::string& shipId) const;
// Module schematic state queries.
int moduleSchematicLevel(const std::string& moduleId) const;
bool isModuleSchematicUnlocked(const std::string& moduleId) const;
// Checks affordability, deducts building blocks, and places the building.
// Returns the new entity id, or kInvalidBuildingId if blocks are insufficient.
BuildingId tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
@@ -125,6 +129,7 @@ private:
int level;
};
std::map<std::string, SchematicState> m_schematicLevels;
std::map<std::string, SchematicState> m_moduleSchematicLevels;
EntityAdmin m_admin;
BeltSystem m_beltSystem;