Implement config-driven unlock groups so that multiple things can be unlocked at once (including buildings)

This commit is contained in:
2026-07-22 21:34:59 +02:00
parent a8a6a04f1e
commit a9082c57f3
36 changed files with 1005 additions and 543 deletions

View File

@@ -7,10 +7,9 @@
# Combat stats are placeholders until the arena balancing pass;
# production_time_seconds values come from the numbers pass.
#
# Unlock progression is mostly disabled (unlock_at_station_level = -1) pending
# the pacing pass. The railguns are the exception: railgun_m is gated to
# station level 1, and railgun_l requires railgun_m to be unlocked first
# (unlock_requires) — a demonstration of the prerequisite chain.
# Unlock gating is defined in unlocks.toml, not here (REQ-LOCK-EXPLICIT): a
# module id granted by an unlock group starts locked and is awarded via a
# defence station drop; ids absent from unlocks.toml (railgun_s) start unlocked.
#
# Surface mask footprint ladder — footprints gate which hulls can mount a
# module, purely through geometry (see ships.toml for the matching hull
@@ -33,7 +32,6 @@
[[module]]
id = "railgun_s"
tooltip = "Small railgun. Fast-firing, short range, low damage; fits any hull."
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "railgun_s_module", amount = 1}]
production_time_seconds = 1
@@ -49,7 +47,6 @@ attack_rate_hz = 2.0
[[module]]
id = "railgun_m"
tooltip = "Medium railgun. Higher damage at longer range; needs a 2x2 slot."
unlock_at_station_level = 2
surface_mask = [
"OO",
"OO"]
@@ -67,8 +64,6 @@ attack_rate_hz = 1.5
[[module]]
id = "railgun_l"
tooltip = "Large railgun. Heavy damage at long range; needs a 3x3 slot."
unlock_at_station_level = 6
unlock_requires = ["railgun_m"]
surface_mask = [
"OOO",
"OOO",
@@ -90,7 +85,6 @@ attack_rate_hz = 0.8
[[module]]
id = "salvager"
tooltip = "Collects scrap from wrecks and stores it in the ship's cargo hold."
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "salvager_module", amount = 1}]
production_time_seconds = 1
@@ -106,7 +100,6 @@ collection_rate_hz = 0.5
[[module]]
id = "repair_tool"
tooltip = "Repairs damaged friendly ships and defence stations within range."
unlock_at_station_level = 0
surface_mask = ["O"]
materials = [{item = "repair_tool_module", amount = 1}]
production_time_seconds = 1
@@ -125,7 +118,6 @@ repair_range_m = 80
[[module]]
id = "afterburner"
tooltip = "Greatly boosts top speed and forward acceleration."
unlock_at_station_level = 2
surface_mask = ["OOO"]
materials = [{item = "afterburner_module", amount = 1}]
production_time_seconds = 1
@@ -140,7 +132,6 @@ added_main_acceleration_mpss = 60
[[module]]
id = "maneuvering_thrusters"
tooltip = "Improves top speed and lateral/braking acceleration."
unlock_at_station_level = 1
surface_mask = ["OO"]
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
production_time_seconds = 1
@@ -158,7 +149,6 @@ added_maneuvering_acceleration_mpss = 10
[[module]]
id = "armor_plates"
tooltip = "Adds a large flat bonus to the ship's hit points."
unlock_at_station_level = 0
surface_mask = ["OO"]
materials = [{item = "armor_plates_module", amount = 1}]
production_time_seconds = 1
@@ -172,7 +162,6 @@ added_hp = 1200
[[module]]
id = "sensor_booster"
tooltip = "Extends the ship's sensor range."
unlock_at_station_level = 1
surface_mask = ["OO"]
materials = [{item = "sensor_booster_module", amount = 1}]
production_time_seconds = 1
@@ -189,7 +178,6 @@ added_sensor_range_m = 50
[[module]]
id = "weapon_upgrade"
tooltip = "Increases the damage of all weapons on the ship."
unlock_at_station_level = 4
surface_mask = [
"OO",
"OX",
@@ -206,7 +194,6 @@ multiplied_damage = 1.2
[[module]]
id = "weapon_primer"
tooltip = "Increases the fire rate of all weapons on the ship."
unlock_at_station_level = 4
surface_mask = [
"OO",
"OX",
@@ -223,7 +210,6 @@ multiplied_attack_rate_hz = 1.2
[[module]]
id = "weapon_stabilizer"
tooltip = "Extends weapon range at the cost of some fire rate."
unlock_at_station_level = 3
surface_mask = [
"OO",
"OX",
@@ -247,7 +233,6 @@ multiplied_attack_rate_hz = 0.8
[[module]]
id = "drone_bay"
tooltip = "Drone launch bay (capability not yet implemented)."
unlock_at_station_level = 5
surface_mask = [
"OO",
"OO"]
@@ -260,7 +245,6 @@ glyph = "Db"
[[module]]
id = "drone_hangar"
tooltip = "Large drone hangar (capability not yet implemented)."
unlock_at_station_level = 9
surface_mask = [
"OOOOOO",
"OOOOOO"]

View File

@@ -142,12 +142,12 @@ duration_seconds = 1.5
# Depth-3 chain (ore -> ingot -> plate -> block) is the factory's
# doubling-time knob; see the block economy rules in docs/balancing/rules.md.
# Explicitly unlocked at start (-1): building blocks appear in no
# schematic's materials, so implicit unlocking can never reach this recipe.
# unlocked_at_start: building blocks appear in no schematic's materials, so the
# implicit item graph can never reach this recipe (REQ-LOCK-IMPLICIT).
[[recipe]]
id = "building_block"
unlock_at_station_level = -1
building = "assembler"
unlocked_at_start = true
inputs = [{item = "steel_plate", amount = 2}]
outputs = [{item = "building_block", amount = 4}]
duration_seconds = 2.0
@@ -220,15 +220,14 @@ outputs = [{item = "capital_core", amount = 1}]
duration_seconds = 10.0
# -----------------------------------------------------------------------------
# Shortcut recipes — drop-only assembler recipe schematics
# (unlock_at_station_level >= 0). Pure rewards: item threat stays defined by
# Shortcut recipes — drop-only assembler recipes, gated by unlock groups in
# unlocks.toml (REQ-LOCK-EXPLICIT). Pure rewards: item threat stays defined by
# the base (expensive) path via the max rule, so shortcuts give real factory
# efficiency without shifting any balance.
# -----------------------------------------------------------------------------
[[recipe]]
id = "shortcut_steel_plate"
unlock_at_station_level = 1
building = "assembler"
inputs = [{item = "iron_ore", amount = 3}]
outputs = [{item = "steel_plate", amount = 1}]
@@ -236,7 +235,6 @@ duration_seconds = 2.0
[[recipe]]
id = "shortcut_control_chip"
unlock_at_station_level = 2
building = "assembler"
inputs = [{item = "quartz", amount = 2}]
outputs = [{item = "control_chip", amount = 1}]
@@ -244,7 +242,6 @@ duration_seconds = 4.0
[[recipe]]
id = "shortcut_hardened_steel"
unlock_at_station_level = 2
building = "assembler"
inputs = [{item = "iron_ingot", amount = 4}]
outputs = [{item = "hardened_steel", amount = 1}]

View File

@@ -4,10 +4,9 @@
# content; stats, materials, and production times are placeholders until the
# recipe and balancing passes.
#
# Unlock progression is mostly disabled (unlock_at_station_level = -1) pending
# the balancing pass. The capital hulls are the exception: battleship is gated
# to station level 1, and dreadnought (level 2) requires battleship to be
# unlocked first (unlock_requires) — a demonstration of the prerequisite chain.
# Unlock gating is defined in unlocks.toml, not here (REQ-LOCK-EXPLICIT): a ship
# id granted by an unlock group starts locked and is awarded via a defence
# station drop; ids absent from unlocks.toml (drone, frigate) start unlocked.
#
# Size classes:
# xs drone 1 cell — exactly one 1x1 module
@@ -22,7 +21,6 @@
[[ship]]
id = "drone"
unlock_at_station_level = -1
layout = ["O"]
default_modules = [{type = "railgun_s", x = 0, y = 0, rotation = "east"}]
@@ -49,7 +47,6 @@ sensor_range_m = 150
# L-shaped weapon modifier, or an afterburner spanning the full center line.
[[ship]]
id = "frigate"
unlock_at_station_level = -1
layout = [
"XOX",
"OOO",
@@ -84,7 +81,6 @@ sensor_range_m = 200
# mount medium hardware.
[[ship]]
id = "destroyer"
unlock_at_station_level = 0
layout = [
"OXOXO",
"OOOOO",
@@ -120,7 +116,6 @@ sensor_range_m = 220
# supports; no 3x3 area exists for an l gun.
[[ship]]
id = "cruiser"
unlock_at_station_level = 2
layout = [
"XOOX",
"OOOO",
@@ -158,8 +153,6 @@ sensor_range_m = 250
# stern leave no 3x3 area for an l gun and no 2x6 area for a drone hangar.
[[ship]]
id = "battlecruiser"
unlock_at_station_level = 4
unlock_requires = ["cruiser"]
layout = [
"OOXXOO",
"OOOOOO",
@@ -201,8 +194,6 @@ sensor_range_m = 260
# so no 2x6 drone hangar fits.
[[ship]]
id = "battleship"
unlock_at_station_level = 6
unlock_requires = ["battlecruiser"]
layout = [
"XOOOOX",
"OOOOOO",
@@ -244,8 +235,6 @@ sensor_range_m = 280
# stay the only hangar hull. Bow and stern strips hold supports.
[[ship]]
id = "dreadnought"
unlock_at_station_level = 8
unlock_requires = ["battleship"]
layout = [
"XXXOOOOOXXX",
"OOOXOOOXOOO",
@@ -288,8 +277,6 @@ sensor_range_m = 300
# the lower decks hold supports and 2x2 point-defense m guns.
[[ship]]
id = "carrier"
unlock_at_station_level = 9
unlock_requires = ["battleship"]
layout = [
"XOOOOOOOOX",
"OOOOOOOOOO",

View File

@@ -0,0 +1,140 @@
# Unlock groups (REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP).
#
# Each [[unlock]] is a group of ships/modules/buildings/recipes awarded together
# from a single defence station drop. Anything NOT granted by any group is
# available from game start. `station_level` gates when a group becomes eligible;
# `requires` lists prerequisite unlock-group ids (REQ-LOCK-PREREQ).
#
# Most entries below are single-item groups that reproduce the previous per-item
# progression. The salvage_operations and reprocessing groups are the grouped
# unlocks: they lock the salvager module + salvage bay, and the reprocessing
# plant, from game start.
# --- Grouped unlocks -------------------------------------------------------
[[unlock]]
id = "salvage_operations"
station_level = 1
modules = ["salvager"]
buildings = ["salvage_bay"]
[[unlock]]
id = "reprocessing"
station_level = 2
buildings = ["reprocessing_plant"]
# --- Ships -----------------------------------------------------------------
[[unlock]]
id = "destroyer"
station_level = 0
ships = ["destroyer"]
[[unlock]]
id = "cruiser"
station_level = 2
ships = ["cruiser"]
[[unlock]]
id = "battlecruiser"
station_level = 4
requires = ["cruiser"]
ships = ["battlecruiser"]
[[unlock]]
id = "battleship"
station_level = 6
requires = ["battlecruiser"]
ships = ["battleship"]
[[unlock]]
id = "dreadnought"
station_level = 8
requires = ["battleship"]
ships = ["dreadnought"]
[[unlock]]
id = "carrier"
station_level = 9
requires = ["battleship"]
ships = ["carrier"]
# --- Modules ---------------------------------------------------------------
[[unlock]]
id = "repair_tool"
station_level = 0
modules = ["repair_tool"]
[[unlock]]
id = "armor_plates"
station_level = 0
modules = ["armor_plates"]
[[unlock]]
id = "maneuvering_thrusters"
station_level = 1
modules = ["maneuvering_thrusters"]
[[unlock]]
id = "sensor_booster"
station_level = 1
modules = ["sensor_booster"]
[[unlock]]
id = "railgun_m"
station_level = 2
modules = ["railgun_m"]
[[unlock]]
id = "afterburner"
station_level = 2
modules = ["afterburner"]
[[unlock]]
id = "weapon_stabilizer"
station_level = 3
modules = ["weapon_stabilizer"]
[[unlock]]
id = "weapon_upgrade"
station_level = 4
modules = ["weapon_upgrade"]
[[unlock]]
id = "weapon_primer"
station_level = 4
modules = ["weapon_primer"]
[[unlock]]
id = "drone_bay"
station_level = 5
modules = ["drone_bay"]
[[unlock]]
id = "railgun_l"
station_level = 6
requires = ["railgun_m"]
modules = ["railgun_l"]
[[unlock]]
id = "drone_hangar"
station_level = 9
modules = ["drone_hangar"]
# --- Assembler recipes -----------------------------------------------------
[[unlock]]
id = "shortcut_steel_plate"
station_level = 1
recipes = ["shortcut_steel_plate"]
[[unlock]]
id = "shortcut_control_chip"
station_level = 2
recipes = ["shortcut_control_chip"]
[[unlock]]
id = "shortcut_hardened_steel"
station_level = 2
recipes = ["shortcut_hardened_steel"]

View File

@@ -1,7 +1,6 @@
[[module]]
id = "armor_plate"
tooltip = "Adds a large flat bonus to hit points."
unlock_at_station_level = -1
surface_mask = ["OO"]
materials = [{item = "iron_ingot", amount = 2}]
production_time_seconds = 3
@@ -13,7 +12,6 @@ multiplied_hp = 1.5
[[module]]
id = "sensor_booster"
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "circuit_board", amount = 1}]
production_time_seconds = 2
@@ -25,7 +23,6 @@ added_sensor_range_m = 100
[[module]]
id = "weapon_upgrade"
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "iron_ingot", amount = 1}, {item = "circuit_board", amount = 1}]
production_time_seconds = 4
@@ -37,7 +34,6 @@ multiplied_damage = 1.2
[[module]]
id = "laser_cannon"
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "iron_ingot", amount = 1}]
production_time_seconds = 5
@@ -51,7 +47,6 @@ attack_rate_hz = 2.0
[[module]]
id = "salvager"
unlock_at_station_level = -1
surface_mask = ["OO"]
materials = [{item = "iron_ingot", amount = 2}]
production_time_seconds = 5
@@ -65,7 +60,6 @@ collection_rate_hz = 0.5
[[module]]
id = "repair_tool"
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "circuit_board", amount = 2}]
production_time_seconds = 5
@@ -79,7 +73,6 @@ repair_range_m = 800
[[module]]
id = "weapon_primer"
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "iron_ingot", amount = 1}]
production_time_seconds = 4
@@ -91,7 +84,6 @@ multiplied_attack_rate_hz = 1.2
[[module]]
id = "weapon_stabilizer"
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "iron_ingot", amount = 1}]
production_time_seconds = 4
@@ -104,7 +96,6 @@ multiplied_attack_rate_hz = 0.8
[[module]]
id = "afterburner"
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "iron_ingot", amount = 1}]
production_time_seconds = 2
@@ -117,7 +108,6 @@ added_main_acceleration_mpss = 60
[[module]]
id = "maneuvering_thrusters"
unlock_at_station_level = -1
surface_mask = ["O"]
materials = [{item = "iron_ingot", amount = 1}]
production_time_seconds = 2

View File

@@ -43,7 +43,7 @@ duration_seconds = 4.0
[[recipe]]
id = "premium_circuit"
building = "assembler"
unlock_at_station_level = -1
unlocked_at_start = true
inputs = [{item = "circuit_board", amount = 1}]
outputs = [{item = "premium_circuit", amount = 1}]
duration_seconds = 8.0
@@ -51,7 +51,6 @@ duration_seconds = 8.0
[[recipe]]
id = "quick_circuit"
building = "assembler"
unlock_at_station_level = 0
inputs = [{item = "copper_ingot", amount = 3}]
outputs = [{item = "circuit_board", amount = 1}]
duration_seconds = 3.0
@@ -59,7 +58,6 @@ duration_seconds = 3.0
[[recipe]]
id = "advanced_circuit"
building = "assembler"
unlock_at_station_level = 1
inputs = [{item = "iron_ingot", amount = 5}]
outputs = [{item = "circuit_board", amount = 1}]
duration_seconds = 6.0
@@ -67,7 +65,6 @@ duration_seconds = 6.0
[[recipe]]
id = "exotic_alloy"
building = "assembler"
unlock_at_station_level = 0
inputs = [{item = "exotic_ore", amount = 2}]
outputs = [{item = "exotic_alloy", amount = 1}]
duration_seconds = 10.0

View File

@@ -1,6 +1,5 @@
[[ship]]
id = "interceptor"
unlock_at_station_level = -1
layout = ["XOX", "OOO", "XOX"]
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
@@ -24,7 +23,6 @@ sensor_range_m = 2000
[[ship]]
id = "destroyer"
unlock_at_station_level = -1
layout = ["XOOX", "OOOO", "XOOX"]
default_modules = [{type = "laser_cannon", x = 1, y = 1, rotation = "east"}]
@@ -48,7 +46,6 @@ sensor_range_m = 3000
[[ship]]
id = "salvage_ship"
unlock_at_station_level = -1
layout = ["OOO", "OOO"]
[ship.schematic]
@@ -71,7 +68,6 @@ sensor_range_m = 2500
[[ship]]
id = "repair_ship"
unlock_at_station_level = 0
layout = ["XOX", "OOO", "XOX"]
[ship.schematic]

View File

@@ -0,0 +1,21 @@
# Unlock groups for the test config (REQ-LOCK-EXPLICIT). Mirrors the previous
# per-item gating: repair_ship, quick_circuit, advanced_circuit start locked and
# are awarded via defence station drops. exotic_alloy is intentionally NOT here:
# it stays implicitly gated (its output/inputs are unreachable), so it is never
# unlocked. premium_circuit uses unlocked_at_start in recipes.toml. Everything
# else (interceptor, destroyer, salvage_ship, all modules) starts unlocked.
[[unlock]]
id = "repair_ship"
station_level = 0
ships = ["repair_ship"]
[[unlock]]
id = "quick_circuit"
station_level = 0
recipes = ["quick_circuit"]
[[unlock]]
id = "advanced_circuit"
station_level = 1
recipes = ["advanced_circuit"]