Implement config-driven unlock groups
Lift unlocking into first-class unlock groups defined in unlocks.toml, so one defence-station drop can grant a bundle of ships/modules/buildings/ recipes at once (e.g. the salvager module + salvage bay), and buildings can now be locked at game start and unlocked via a drop. Config: - New UnlocksConfig + ConfigLoader::loadUnlocks + validateUnlocks (unknown/ duplicate/non-assembler grants, unknown requires, empty group all fail load). - Drop unlock_at_station_level/unlock_requires from ship/module/recipe defs; add unlocked_at_start bool to recipes for graph-unreachable base recipes. Simulation: - Track awarded groups + per-building unlock state; an item starts unlocked iff no group grants it. Rework generateSchematicChoices/applySchematicChoice to operate on groups (grant all members at once). Extend state checksum. - isBuildingUnlocked query; tryPlaceBuilding rejects locked types. UI: - Build-menu buttons for locked buildings start hidden, revealed via a new UnlockedBuildingsChangedEvent (emitted from GameWorldView's poll loop). - Schematic choice dialog lists a group's granted items. - Blueprint placement excludes locked building types (REQ-LOCK-UI-BLUEPRINT). Data: unlocks.toml (app + test) reproduces the prior per-item gates as singleton groups and adds salvage_operations (salvager + salvage_bay, lvl 1) and reprocessing (reprocessing_plant, lvl 2). Tests: rewrote unlock/config/recipe-schematic/artifact/wave/shipyard tests for the group model; added UnlockGroupTest. All 443 cases pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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}]
|
||||
|
||||
@@ -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",
|
||||
|
||||
140
bin/app/data/config/unlocks.toml
Normal file
140
bin/app/data/config/unlocks.toml
Normal 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"]
|
||||
Reference in New Issue
Block a user