allow to configure when which schematic gets unlockable

This commit is contained in:
2026-06-10 20:54:16 +02:00
parent 26857e8414
commit aad094f842
9 changed files with 25 additions and 21 deletions

View File

@@ -1,6 +1,6 @@
[[ship]]
id = "drone"
available_from_start = true
unlock_at_station_level = -1
layout = ["O"]
default_modules = [{type = "laser_cannon", x = 0, y = 0, rotation = "east"}]

View File

@@ -1,6 +1,6 @@
[[ship]]
id = "interceptor"
available_from_start = true
unlock_at_station_level = -1
layout = ["XOX", "OOO", "XOX"]
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
@@ -31,7 +31,7 @@ scrap_drop = 2
[[ship]]
id = "destroyer"
available_from_start = true
unlock_at_station_level = -1
layout = ["XOOX", "OOOO", "XOOX"]
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
@@ -62,7 +62,7 @@ scrap_drop = 4
[[ship]]
id = "salvage_ship"
available_from_start = true
unlock_at_station_level = -1
layout = ["OOO", "OOO"]
[ship.schematic]
@@ -92,7 +92,7 @@ scrap_drop = 2
[[ship]]
id = "repair_ship"
available_from_start = false
unlock_at_station_level = 0
layout = ["XOX", "OOO", "XOX"]
[ship.schematic]

View File

@@ -7,7 +7,7 @@ Config files use the TOML format. The following config files drive game paramete
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, enemy ship level formula, belt speed, starting building blocks, departure interval.
- **buildings.toml** — building block cost and construction time per building type.
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities.
- **ships.toml** — per schematic: a human-readable display name (used in toasts and UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as formulas of ship level, required build materials, threat cost formula, player production level, whether the schematic is available from game start, a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
- **ships.toml** — per schematic: a human-readable display name (used in toasts and UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as formulas of ship level, required build materials, threat cost formula, player production level, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
- **modules.toml** — per module type: id, surface mask, materials list, player production level, production time, threat cost, fill color, glyph, and an optional capability section and/or stat modifier formulas. A module with a capability section (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas is a **capability module** that grants the ship a weapon, salvage bay, or repair tool per instance (see REQ-MOD-CONFIG for the full list of formulas per capability type). A module with only `added_*`/`multiplied_*` formulas is a **passive module** that modifies stats on the ship or on capability module instances (see REQ-MOD-STAT-CALC).
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship schematic, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
@@ -149,7 +149,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
## Ships
- REQ-SHP-AUTONOMOUS: Ships are produced by shipyards and are fully autonomous once produced.
- REQ-SHP-STATS: Base hull stats are defined as formulas of ship level in `ships.toml`: HP (`[ship.health].hp_formula`), max linear speed (`[ship.movement].speed_formula`), sensor range (`[ship.sensors].range_formula`), main acceleration (`[ship.movement].main_acceleration_formula`, tiles/s²), maneuvering acceleration (`[ship.movement].maneuvering_acceleration_formula`, tiles/s²), angular acceleration (`[ship.movement].angular_acceleration_formula`, rad/s²), max rotation speed (`[ship.movement].max_rotation_speed_formula`, rad/s). Required build materials (`[ship.schematic].materials`) and availability from game start (`[[ship]].available_from_start`) are also defined there. Combat, salvage, and repair capabilities are provided by modules (see REQ-MOD-CONFIG). Final hull stats incorporate passive module modifiers per REQ-MOD-STAT-CALC.
- REQ-SHP-STATS: Base hull stats are defined as formulas of ship level in `ships.toml`: HP (`[ship.health].hp_formula`), max linear speed (`[ship.movement].speed_formula`), sensor range (`[ship.sensors].range_formula`), main acceleration (`[ship.movement].main_acceleration_formula`, tiles/s²), maneuvering acceleration (`[ship.movement].maneuvering_acceleration_formula`, tiles/s²), angular acceleration (`[ship.movement].angular_acceleration_formula`, rad/s²), max rotation speed (`[ship.movement].max_rotation_speed_formula`, rad/s). Required build materials (`[ship.schematic].materials`) and the station level at which the schematic becomes available for unlock (`[[ship]].unlock_at_station_level`; -1 = player starts with the schematic already unlocked) are also defined there. Combat, salvage, and repair capabilities are provided by modules (see REQ-MOD-CONFIG). Final hull stats incorporate passive module modifiers per REQ-MOD-STAT-CALC.
- REQ-SHP-SPAWN-PLAYER: A ship produced by a shipyard spawns centered on the shipyard's output port tile.
- REQ-SHP-SPAWN-ENEMY: Enemy ships spawn at a uniformly random position within the current enemy buffer zone — random X across the buffer's width and random Y across the world height.
- REQ-SHP-MOVEMENT: Ships move using a physics-based model. Each ship has a velocity and a facing direction, both updated each tick. The main acceleration (`main_acceleration_formula`) is applied along the ship's current facing direction only. The maneuvering acceleration (`maneuvering_acceleration_formula`) can be applied in any direction independently of the facing direction, enabling lateral or braking movement without rotating. The angular acceleration (`angular_acceleration_formula`) controls how quickly the ship rotates. Linear speed is capped at the ship's `speed_formula` value; rotation rate is capped at the ship's `max_rotation_speed_formula` value. Ship position refers to the ship's center for all range, sensor, and attack checks.
@@ -269,7 +269,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
- REQ-DEF-ENEMY-FIRE: Enemy defence stations automatically fire at player ships within range.
- REQ-DEF-NO-CROSSFIRE: Enemy and player defence stations are never in each other's firing range.
- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the boss countdown is advanced (REQ-WAV-BOSS-ADVANCE), the scrollable area is extended (REQ-GW-PUSH-EXPAND), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP).
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop is automatic — no physical item to collect. A schematic is chosen uniformly at random from all schematics defined in `ships.toml`. If the player does not yet have that schematic, it is unlocked. If the player already has it, the schematic's `[ship.schematic].player_production_level` is incremented by 1 — so subsequent ships of that type are produced at a higher level. The player is notified via a toast (REQ-UI-SCHEMATIC-TOAST).
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop is automatic — no physical item to collect. A schematic is chosen uniformly at random from all schematics whose `unlock_at_station_level` is -1 or is ≤ the level of the destroyed station set. If the player does not yet have that schematic, it is unlocked. If the player already has it, the schematic's `[ship.schematic].player_production_level` is incremented by 1 — so subsequent ships of that type are produced at a higher level. The player is notified via a toast (REQ-UI-SCHEMATIC-TOAST).
## Threat Level & Enemy Waves

View File

@@ -403,7 +403,7 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
ShipDef def;
def.id = requireString(mt["id"], file, elemPath + ".id");
def.availableFromStart = requireBool(mt["available_from_start"], file, elemPath + ".available_from_start");
def.unlockAtStationLevel = static_cast<int>(requireInt(mt["unlock_at_station_level"], file, elemPath + ".unlock_at_station_level"));
def.layout = requireStringArray(mt["layout"], file, elemPath + ".layout");
// Schematic

View File

@@ -51,7 +51,7 @@ struct ShipLoot
struct ShipDef
{
std::string id;
bool availableFromStart;
int unlockAtStationLevel;
std::vector<std::string> layout;
ShipSchematic schematic;

View File

@@ -66,8 +66,8 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
for (const ShipDef& def : m_config.ships.ships)
{
SchematicState state;
state.unlocked = def.availableFromStart;
state.level = def.availableFromStart ? def.schematic.playerProductionLevel : 0;
state.unlocked = (def.unlockAtStationLevel == -1);
state.level = (def.unlockAtStationLevel == -1) ? def.schematic.playerProductionLevel : 0;
m_schematicLevels[def.id] = state;
}
@@ -139,8 +139,8 @@ void Simulation::reset(unsigned int seed)
for (const ShipDef& def : m_config.ships.ships)
{
SchematicState state;
state.unlocked = def.availableFromStart;
state.level = def.availableFromStart ? def.schematic.playerProductionLevel : 0;
state.unlocked = (def.unlockAtStationLevel == -1);
state.level = (def.unlockAtStationLevel == -1) ? def.schematic.playerProductionLevel : 0;
m_schematicLevels[def.id] = state;
}
@@ -473,19 +473,23 @@ void Simulation::tickDeathsAndLoot()
if (es0Gone && es1Gone &&
m_currentEnemyStationEntities[0] != entt::null)
{
const int destroyedLevel = m_waveSystem->generation();
m_waveSystem->onEnemyStationsDestroyed();
placeEnemyStationSet(m_waveSystem->generation());
awardSchematicDrop();
awardSchematicDrop(destroyedLevel);
}
}
void Simulation::awardSchematicDrop()
void Simulation::awardSchematicDrop(int destroyedStationLevel)
{
std::vector<std::string> ids;
ids.reserve(m_config.ships.ships.size());
for (const ShipDef& def : m_config.ships.ships)
{
ids.push_back(def.id);
if (def.unlockAtStationLevel == -1 || def.unlockAtStationLevel <= destroyedStationLevel)
{
ids.push_back(def.id);
}
}
std::uniform_int_distribution<int> dist(0, static_cast<int>(ids.size()) - 1);

View File

@@ -100,7 +100,7 @@ private:
void tickDeathsAndLoot();
// Award a random schematic drop (REQ-DEF-SCHEMATIC-DROP) and emit the event.
void awardSchematicDrop();
void awardSchematicDrop(int destroyedStationLevel);
GameConfig m_config;
std::mt19937 m_rng;

View File

@@ -654,7 +654,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
TEST_CASE("Blueprint placement: interceptor schematic is unlocked at game start", "[blueprint]")
{
// "interceptor" has available_from_start = true in the test config.
// "interceptor" has unlock_at_station_level = -1 in the test config.
// This confirms the guard in placeBlueprintAtTile passes for start-unlocked schematics.
Simulation sim(loadConfig());
REQUIRE(sim.isSchematicUnlocked("interceptor"));
@@ -662,7 +662,7 @@ TEST_CASE("Blueprint placement: interceptor schematic is unlocked at game start"
TEST_CASE("Blueprint placement: repair_ship schematic is locked at game start", "[blueprint]")
{
// "repair_ship" has available_from_start = false in the test config.
// "repair_ship" has unlock_at_station_level = 0 in the test config.
// This confirms the guard in placeBlueprintAtTile blocks locked schematics,
// leaving the shipyard's schematic unset.
Simulation sim(loadConfig());

View File

@@ -23,7 +23,7 @@ static const ShipDef* findAvailableSchematic(const GameConfig& cfg)
{
for (const ShipDef& def : cfg.ships.ships)
{
if (def.availableFromStart && !def.schematic.materials.empty())
if (def.unlockAtStationLevel == -1 && !def.schematic.materials.empty())
{
return &def;
}