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
149 lines
4.2 KiB
TOML
149 lines
4.2 KiB
TOML
[[recipe]]
|
|
id = "mine_iron_ore"
|
|
building = "miner"
|
|
inputs = []
|
|
outputs = [{item = "iron_ore", amount = 1}]
|
|
duration_seconds = 1.0
|
|
|
|
[[recipe]]
|
|
id = "mine_copper_ore"
|
|
building = "miner"
|
|
inputs = []
|
|
outputs = [{item = "copper_ore", amount = 1}]
|
|
duration_seconds = 1.5
|
|
|
|
[[recipe]]
|
|
id = "iron_ingot"
|
|
building = "smelter"
|
|
inputs = [{item = "iron_ore", amount = 2}]
|
|
outputs = [{item = "iron_ingot", amount = 1}]
|
|
duration_seconds = 2.0
|
|
|
|
[[recipe]]
|
|
id = "copper_ingot"
|
|
building = "smelter"
|
|
inputs = [{item = "copper_ore", amount = 2}]
|
|
outputs = [{item = "copper_ingot", amount = 1}]
|
|
duration_seconds = 2.5
|
|
|
|
[[recipe]]
|
|
id = "circuit_board"
|
|
building = "assembler"
|
|
inputs = [{item = "iron_ingot", amount = 3}, {item = "copper_ingot", amount = 2}]
|
|
outputs = [{item = "circuit_board", amount = 1}]
|
|
duration_seconds = 5.0
|
|
|
|
[[recipe]]
|
|
id = "building_blocks"
|
|
building = "assembler"
|
|
inputs = [{item = "iron_ingot", amount = 4}]
|
|
outputs = [{item = "building_block", amount = 10}]
|
|
duration_seconds = 4.0
|
|
|
|
[[recipe]]
|
|
id = "premium_circuit"
|
|
building = "assembler"
|
|
unlocked_at_start = true
|
|
inputs = [{item = "circuit_board", amount = 1}]
|
|
outputs = [{item = "premium_circuit", amount = 1}]
|
|
duration_seconds = 8.0
|
|
|
|
[[recipe]]
|
|
id = "quick_circuit"
|
|
building = "assembler"
|
|
inputs = [{item = "copper_ingot", amount = 3}]
|
|
outputs = [{item = "circuit_board", amount = 1}]
|
|
duration_seconds = 3.0
|
|
|
|
[[recipe]]
|
|
id = "advanced_circuit"
|
|
building = "assembler"
|
|
inputs = [{item = "iron_ingot", amount = 5}]
|
|
outputs = [{item = "circuit_board", amount = 1}]
|
|
duration_seconds = 6.0
|
|
|
|
[[recipe]]
|
|
id = "exotic_alloy"
|
|
building = "assembler"
|
|
inputs = [{item = "exotic_ore", amount = 2}]
|
|
outputs = [{item = "exotic_alloy", amount = 1}]
|
|
duration_seconds = 10.0
|
|
|
|
[[recipe]]
|
|
id = "reprocessing_cycle"
|
|
building = "reprocessing_plant"
|
|
inputs = [{item = "scrap", amount = 5}]
|
|
duration_seconds = 3.0
|
|
|
|
[[recipe.outputs]]
|
|
item = "iron_ingot"
|
|
amount = 2
|
|
probability = 0.6
|
|
|
|
[[recipe.outputs]]
|
|
item = "circuit_board"
|
|
amount = 1
|
|
probability = 0.3
|
|
|
|
[[recipe.outputs]]
|
|
item = "advanced_alloy"
|
|
amount = 1
|
|
probability = 0.1
|
|
|
|
# -------------------------------------------------------------------
|
|
# Extra recipes for ThreatCostCalculator unit tests (fixes 6-9)
|
|
# -------------------------------------------------------------------
|
|
|
|
# Fix 6: scrap-consuming smelter recipe for iron_ingot. Because iron_ingot
|
|
# already has a scrap-free smelter recipe above, this recipe must be excluded
|
|
# from iron_ingot's threat computation.
|
|
[[recipe]]
|
|
id = "scrap_iron"
|
|
building = "smelter"
|
|
inputs = [{item = "scrap", amount = 1}]
|
|
outputs = [{item = "iron_ingot", amount = 1}]
|
|
duration_seconds = 1.0
|
|
|
|
# Fix 7: a recipe that produces 2 items per cycle. Per-unit threat must
|
|
# divide by the output amount.
|
|
# dual_wire: (duration=3.0 + iron_ore(1.0)*1) / 2 = 4.0 / 2 = 2.0 per unit.
|
|
[[recipe]]
|
|
id = "dual_wire"
|
|
building = "assembler"
|
|
inputs = [{item = "iron_ore", amount = 1}]
|
|
outputs = [{item = "dual_wire", amount = 2}]
|
|
duration_seconds = 3.0
|
|
|
|
# Fix 8: an item downstream of a reprocessing-only item (advanced_alloy).
|
|
# advanced_alloy is resolved only by the reprocessing pass; downstream_product
|
|
# can only resolve in a non-reprocessing pass that runs AFTER the reprocessing
|
|
# pass, requiring proper fixpoint iteration.
|
|
# downstream_product: 2.0 + advanced_alloy(80.0)*1 = 82.0
|
|
[[recipe]]
|
|
id = "downstream_product"
|
|
building = "assembler"
|
|
inputs = [{item = "advanced_alloy", amount = 1}]
|
|
outputs = [{item = "downstream_product", amount = 1}]
|
|
duration_seconds = 2.0
|
|
|
|
# Fix 9: two recipes producing the same staggered_item. The cheap recipe
|
|
# resolves before circuit_board is known; the expensive one requires
|
|
# circuit_board. The item must be committed only once BOTH are computable,
|
|
# so the result is max(cheap, expensive).
|
|
# staggered_item_cheap: 1.0 + iron_ore(1.0)*1 = 2.0 (resolves early)
|
|
# staggered_item_expensive: 1.0 + circuit_board(28.0)*1 = 29.0 (resolves later)
|
|
# expected: max = 29.0
|
|
[[recipe]]
|
|
id = "staggered_item_cheap"
|
|
building = "assembler"
|
|
inputs = [{item = "iron_ore", amount = 1}]
|
|
outputs = [{item = "staggered_item", amount = 1}]
|
|
duration_seconds = 1.0
|
|
|
|
[[recipe]]
|
|
id = "staggered_item_expensive"
|
|
building = "assembler"
|
|
inputs = [{item = "circuit_board", amount = 1}]
|
|
outputs = [{item = "staggered_item", amount = 1}]
|
|
duration_seconds = 1.0
|