Files
dota_factory/bin/app/data/config/recipes.toml
Malte Langkabel 1f339bd8d7 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
2026-07-21 22:38:38 +02:00

444 lines
13 KiB
TOML

# recipes.toml
#
# Production tree v2 (structure in docs/content_design.md, numbers with
# derivations in docs/balancing/derived.md). Quantities and durations are tuned so that every
# fitted ship lands on the threat-cost ladder and the ratio curve is
# realized: tier 1 ratios are 1:1, tier 2 ratios are 2:3, tier 3+ ratios
# are deliberately strange.
#
# Input chain per game phase — each phase transition adds exactly one new
# base input:
#
# early iron_ore + copper_ore minable on every asteroid tile (the
# asteroid is an M-type body — its bulk
# rock IS ore)
# mid + quartz geode deposits in expansion territory
# (deposit gating pending — see action
# item 5 in docs/balancing/README.md;
# until then quartz mines anywhere)
# late + voidsteel battle-forged: ONLY from reprocessing
# salvaged scrap, so capital production
# requires combat
#
# Run tools/verify_recipes.py and tools/threat_report.py after editing.
# -----------------------------------------------------------------------------
# Mining (tier 0)
# -----------------------------------------------------------------------------
[[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.0
[[recipe]]
id = "mine_quartz"
building = "miner"
inputs = []
outputs = [{item = "quartz", amount = 1}]
duration_seconds = 2.0
# -----------------------------------------------------------------------------
# Smelting (tier 1) — one recipe per input item; ratios are 1:1 with miners.
# -----------------------------------------------------------------------------
[[recipe]]
id = "iron_ingot"
building = "smelter"
inputs = [{item = "iron_ore", amount = 1}]
outputs = [{item = "iron_ingot", amount = 1}]
duration_seconds = 1.0
[[recipe]]
id = "copper_ingot"
building = "smelter"
inputs = [{item = "copper_ore", amount = 1}]
outputs = [{item = "copper_ingot", amount = 1}]
duration_seconds = 1.0
[[recipe]]
id = "silicon"
building = "smelter"
inputs = [{item = "quartz", amount = 1}]
outputs = [{item = "silicon", amount = 1}]
duration_seconds = 2.0
# Scrap smelting: the safe, boring sink. Deliberately value-losing (4 threat
# of scrap becomes a 2-threat ingot) — reprocessing is the value-preserving
# path.
[[recipe]]
id = "scrap_smelting"
building = "smelter"
inputs = [{item = "scrap", amount = 1}]
outputs = [{item = "iron_ingot", amount = 1}]
duration_seconds = 1.0
# -----------------------------------------------------------------------------
# Reprocessing — the only source of voidsteel (battle-forged; formed when
# weapon plasma anneals hull metal in the violence of ship destruction).
# Weights are authored for the fully unlocked pool state; the pool
# renormalizes over implicitly unlocked items early game.
# -----------------------------------------------------------------------------
[[recipe]]
id = "reprocessing_cycle"
building = "reprocessing_plant"
inputs = [{item = "scrap", amount = 4}]
duration_seconds = 4.0
[[recipe.outputs]]
item = "iron_ingot"
amount = 1
probability = 0.3
[[recipe.outputs]]
item = "copper_ingot"
amount = 1
probability = 0.3
[[recipe.outputs]]
item = "silicon"
amount = 1
probability = 0.2
[[recipe.outputs]]
item = "voidsteel"
amount = 1
probability = 0.2
# -----------------------------------------------------------------------------
# Tier 2 — early intermediates (clean ratios, ~2:3)
# -----------------------------------------------------------------------------
[[recipe]]
id = "steel_plate"
building = "assembler"
inputs = [{item = "iron_ingot", amount = 2}]
outputs = [{item = "steel_plate", amount = 1}]
duration_seconds = 3.0
[[recipe]]
id = "copper_wire"
building = "assembler"
inputs = [{item = "copper_ingot", amount = 1}]
outputs = [{item = "copper_wire", amount = 2}]
duration_seconds = 1.0
[[recipe]]
id = "copper_coil"
building = "assembler"
inputs = [{item = "copper_wire", amount = 2}]
outputs = [{item = "copper_coil", amount = 1}]
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.
# 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"
building = "assembler"
unlocked_at_start = true
inputs = [{item = "steel_plate", amount = 2}]
outputs = [{item = "building_block", amount = 4}]
duration_seconds = 2.0
# -----------------------------------------------------------------------------
# Tier 3 — mid intermediates (strange ratios begin; need quartz)
# -----------------------------------------------------------------------------
[[recipe]]
id = "control_chip"
building = "assembler"
inputs = [{item = "silicon", amount = 1}, {item = "copper_wire", amount = 2}]
outputs = [{item = "control_chip", amount = 1}]
duration_seconds = 5.0
[[recipe]]
id = "capacitor_bank"
building = "assembler"
inputs = [{item = "copper_coil", amount = 2}, {item = "silicon", amount = 1}]
outputs = [{item = "capacitor_bank", amount = 1}]
duration_seconds = 5.0
# The quality gate for m+ hulls: a deliberately long-running recipe
# (time-heavy archetype).
[[recipe]]
id = "hardened_steel"
building = "assembler"
inputs = [{item = "steel_plate", amount = 3}]
outputs = [{item = "hardened_steel", amount = 1}]
duration_seconds = 12.0
[[recipe]]
id = "ceramic_plate"
building = "assembler"
inputs = [{item = "quartz", amount = 2}]
outputs = [{item = "ceramic_plate", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "drive_unit"
building = "assembler"
inputs = [
{item = "steel_plate", amount = 2},
{item = "copper_coil", amount = 2},
{item = "control_chip", amount = 1},
]
outputs = [{item = "drive_unit", amount = 1}]
duration_seconds = 8.0
# -----------------------------------------------------------------------------
# Tier 4 — late intermediates (need voidsteel)
# -----------------------------------------------------------------------------
[[recipe]]
id = "voidsteel_plate"
building = "assembler"
inputs = [{item = "voidsteel", amount = 1}, {item = "hardened_steel", amount = 1}]
outputs = [{item = "voidsteel_plate", amount = 1}]
duration_seconds = 8.0
[[recipe]]
id = "capital_core"
building = "assembler"
inputs = [
{item = "voidsteel", amount = 2},
{item = "capacitor_bank", amount = 1},
{item = "control_chip", amount = 1},
]
outputs = [{item = "capital_core", amount = 1}]
duration_seconds = 10.0
# -----------------------------------------------------------------------------
# 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"
building = "assembler"
inputs = [{item = "iron_ore", amount = 3}]
outputs = [{item = "steel_plate", amount = 1}]
duration_seconds = 2.0
[[recipe]]
id = "shortcut_control_chip"
building = "assembler"
inputs = [{item = "quartz", amount = 2}]
outputs = [{item = "control_chip", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "shortcut_hardened_steel"
building = "assembler"
inputs = [{item = "iron_ingot", amount = 4}]
outputs = [{item = "hardened_steel", amount = 1}]
duration_seconds = 8.0
# -----------------------------------------------------------------------------
# Ship hulls
# -----------------------------------------------------------------------------
[[recipe]]
id = "drone_hull"
building = "assembler"
inputs = [{item = "iron_ingot", amount = 1}]
outputs = [{item = "drone_hull", amount = 1}]
duration_seconds = 1.0
[[recipe]]
id = "frigate_hull"
building = "assembler"
inputs = [{item = "steel_plate", amount = 2}, {item = "copper_wire", amount = 1}]
outputs = [{item = "frigate_hull", amount = 1}]
duration_seconds = 2.0
[[recipe]]
id = "destroyer_hull"
building = "assembler"
inputs = [{item = "steel_plate", amount = 3}, {item = "copper_coil", amount = 2}]
outputs = [{item = "destroyer_hull", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "cruiser_hull"
building = "assembler"
inputs = [{item = "hardened_steel", amount = 2}, {item = "control_chip", amount = 2}]
outputs = [{item = "cruiser_hull", amount = 1}]
duration_seconds = 6.0
[[recipe]]
id = "battlecruiser_hull"
building = "assembler"
inputs = [
{item = "hardened_steel", amount = 3},
{item = "control_chip", amount = 2},
{item = "drive_unit", amount = 1},
]
outputs = [{item = "battlecruiser_hull", amount = 1}]
duration_seconds = 8.0
[[recipe]]
id = "battleship_hull"
building = "assembler"
inputs = [
{item = "voidsteel_plate", amount = 3},
{item = "drive_unit", amount = 1},
{item = "control_chip", amount = 2},
]
outputs = [{item = "battleship_hull", amount = 1}]
duration_seconds = 10.0
[[recipe]]
id = "dreadnought_hull"
building = "assembler"
inputs = [
{item = "voidsteel_plate", amount = 5},
{item = "capital_core", amount = 1},
{item = "drive_unit", amount = 2},
]
outputs = [{item = "dreadnought_hull", amount = 1}]
duration_seconds = 12.0
[[recipe]]
id = "carrier_hull"
building = "assembler"
inputs = [
{item = "voidsteel_plate", amount = 5},
{item = "capital_core", amount = 1},
{item = "drive_unit", amount = 2},
]
outputs = [{item = "carrier_hull", amount = 1}]
duration_seconds = 12.0
# -----------------------------------------------------------------------------
# Module prefabs
# -----------------------------------------------------------------------------
[[recipe]]
id = "railgun_s_module"
building = "assembler"
inputs = [{item = "copper_coil", amount = 1}]
outputs = [{item = "railgun_s_module", amount = 1}]
duration_seconds = 1.0
[[recipe]]
id = "salvager_module"
building = "assembler"
inputs = [{item = "steel_plate", amount = 1}, {item = "copper_wire", amount = 2}]
outputs = [{item = "salvager_module", amount = 1}]
duration_seconds = 2.0
[[recipe]]
id = "repair_tool_module"
building = "assembler"
inputs = [{item = "steel_plate", amount = 1}, {item = "copper_wire", amount = 2}]
outputs = [{item = "repair_tool_module", amount = 1}]
duration_seconds = 2.0
# Material-heavy, fast: the armor archetype.
[[recipe]]
id = "armor_plates_module"
building = "assembler"
inputs = [{item = "steel_plate", amount = 4}]
outputs = [{item = "armor_plates_module", amount = 1}]
duration_seconds = 3.0
[[recipe]]
id = "maneuvering_thrusters_module"
building = "assembler"
inputs = [{item = "steel_plate", amount = 1}, {item = "copper_coil", amount = 1}]
outputs = [{item = "maneuvering_thrusters_module", amount = 1}]
duration_seconds = 2.0
[[recipe]]
id = "sensor_booster_module"
building = "assembler"
inputs = [{item = "copper_wire", amount = 2}, {item = "copper_coil", amount = 1}]
outputs = [{item = "sensor_booster_module", amount = 1}]
duration_seconds = 2.0
[[recipe]]
id = "afterburner_module"
building = "assembler"
inputs = [{item = "copper_coil", amount = 2}, {item = "steel_plate", amount = 1}]
outputs = [{item = "afterburner_module", amount = 1}]
duration_seconds = 3.0
[[recipe]]
id = "weapon_stabilizer_module"
building = "assembler"
inputs = [{item = "steel_plate", amount = 1}, {item = "copper_coil", amount = 1}]
outputs = [{item = "weapon_stabilizer_module", amount = 1}]
duration_seconds = 2.0
[[recipe]]
id = "weapon_primer_module"
building = "assembler"
inputs = [{item = "capacitor_bank", amount = 1}, {item = "copper_coil", amount = 1}]
outputs = [{item = "weapon_primer_module", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "weapon_upgrade_module"
building = "assembler"
inputs = [{item = "control_chip", amount = 1}, {item = "copper_coil", amount = 1}]
outputs = [{item = "weapon_upgrade_module", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "railgun_m_module"
building = "assembler"
inputs = [
{item = "capacitor_bank", amount = 1},
{item = "steel_plate", amount = 2},
{item = "copper_coil", amount = 1},
]
outputs = [{item = "railgun_m_module", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "drone_bay_module"
building = "assembler"
inputs = [
{item = "control_chip", amount = 1},
{item = "steel_plate", amount = 2},
{item = "copper_coil", amount = 1},
]
outputs = [{item = "drone_bay_module", amount = 1}]
duration_seconds = 4.0
[[recipe]]
id = "railgun_l_module"
building = "assembler"
inputs = [
{item = "capacitor_bank", amount = 1},
{item = "hardened_steel", amount = 2},
{item = "ceramic_plate", amount = 1},
]
outputs = [{item = "railgun_l_module", amount = 1}]
duration_seconds = 6.0
[[recipe]]
id = "drone_hangar_module"
building = "assembler"
inputs = [
{item = "voidsteel_plate", amount = 1},
{item = "control_chip", amount = 2},
{item = "drive_unit", amount = 1},
]
outputs = [{item = "drone_hangar_module", amount = 1}]
duration_seconds = 10.0