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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
21
bin/test/data/config/unlocks.toml
Normal file
21
bin/test/data/config/unlocks.toml
Normal 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"]
|
||||
Reference in New Issue
Block a user