From cdf89ce0dd8fee6872925152b287d92375d85080 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Wed, 8 Jul 2026 20:33:51 +0200 Subject: [PATCH] Restructure balancing docs into docs/balancing/ --- bin/app/data/config/recipes.toml | 8 +- bin/app/data/config/stations.toml | 2 +- bin/app/data/config/world.toml | 4 +- docs/balancing/README.md | 56 ++ docs/balancing/derived.md | 164 ++++++ docs/balancing/history.md | 96 ++++ docs/balancing/process.md | 98 ++++ .../rules.md} | 82 +-- docs/balancing/targets.md | 104 ++++ docs/content_design.md | 533 ++---------------- 10 files changed, 604 insertions(+), 543 deletions(-) create mode 100644 docs/balancing/README.md create mode 100644 docs/balancing/derived.md create mode 100644 docs/balancing/history.md create mode 100644 docs/balancing/process.md rename docs/{progression_design.md => balancing/rules.md} (82%) create mode 100644 docs/balancing/targets.md diff --git a/bin/app/data/config/recipes.toml b/bin/app/data/config/recipes.toml index cea8dcb..8558826 100644 --- a/bin/app/data/config/recipes.toml +++ b/bin/app/data/config/recipes.toml @@ -1,7 +1,7 @@ # recipes.toml # -# Production tree v2 (see docs/content_design.md, "Production tree v2" and -# "Numbers — first pass"). Quantities and durations are tuned so that every +# 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. @@ -14,7 +14,7 @@ # rock IS ore) # mid + quartz geode deposits in expansion territory # (deposit gating pending — see action -# item 5 in docs/progression_design.md; +# item 5 in docs/balancing/README.md; # until then quartz mines anywhere) # late + voidsteel battle-forged: ONLY from reprocessing # salvaged scrap, so capital production @@ -141,7 +141,7 @@ 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 progression_design.md. +# 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. [[recipe]] diff --git a/bin/app/data/config/stations.toml b/bin/app/data/config/stations.toml index 15af5a8..6a13ec9 100644 --- a/bin/app/data/config/stations.toml +++ b/bin/app/data/config/stations.toml @@ -1,6 +1,6 @@ # stations.toml # -# Combat-pass anchors (see docs/content_design.md, "Combat stat anchors"): +# Combat-pass anchors (see docs/balancing/targets.md, "Combat anchors"): # a fresh player defence station holds one early parity wave unaided; the # enemy station at level 0 matches the player station exactly and scales # with the push level x. Station scrap drops stay authored (pushing rewards diff --git a/bin/app/data/config/world.toml b/bin/app/data/config/world.toml index 539cce0..9c8c937 100644 --- a/bin/app/data/config/world.toml +++ b/bin/app/data/config/world.toml @@ -19,6 +19,8 @@ enemy_buffer_width_tiles = 20 [expansion] columns_per_expansion_tiles = 10 +# x = expansions already purchased; ~1 per cycle mid-game, decelerating +# to 2-3 cycles late (docs/balancing/derived.md). cost_building_blocks_formula = "300 + 50*x + 10*x*x" [push] @@ -35,7 +37,7 @@ artifact_chance_formula = "0.05 * x" # 5% chance per station level artifact_win_count = 5 [waves] -# Tuned against the factory-size curve (content_design.md, balancing +# Tuned against the factory-size curve (docs/balancing/targets.md, balancing # targets): stays below the player's achievable military output early, # crosses it around the late boundary (~cycle 15), overwhelms by ~24. threat_rate_formula = "2*x + 0.15*x*x" diff --git a/docs/balancing/README.md b/docs/balancing/README.md new file mode 100644 index 0000000..26a809e --- /dev/null +++ b/docs/balancing/README.md @@ -0,0 +1,56 @@ +# Balancing Documentation + +Everything about balancing Dota Factory, separated by role: + +- **[rules.md](rules.md)** — the design rules and principles. Timeless; + changes only when the design changes. +- **[targets.md](targets.md)** — the base numbers (roots/anchors) chosen + by design. Change these first; everything else re-derives. +- **[derived.md](derived.md)** — the current tuned state of all derived + numbers, mirroring the configs. Updated whenever configs change. +- **[process.md](process.md)** — how balancing is done: the pass order, + tuning discipline, tools, and the checklist for the next round. +- **[history.md](history.md)** — chronological record of decisions, + findings, bugs, and arena rounds. + +Related: game content (hull grids, footprint gating, tree design and +fiction) in [../content_design.md](../content_design.md); rules with +REQ-* ids in [../requirements.md](../requirements.md). + +## Status + +First full balancing round complete (2026-07-06): targets → tree → +numbers → threat-calculator parity → combat stats (arena-converged) → +pacing. Next step: full-game playtests against the run-shape targets in +`targets.md`. + +## Open action items + +Agreed changes that require edits to `requirements.md`, the code, or the +configs. Completed items are removed (their outcomes live in +`requirements.md`, `history.md`, and the git history). + +1. **Fill unfillable schematic slots with artifacts.** With duplicates + removed, the schematic drop pool can run dry — previously unreachable. + Decision: every slot in the choice dialog that cannot be filled with a + schematic because the eligible pool is exhausted is filled with an + artifact option instead (in addition to any artifact option granted by + the regular artifact roll). A push therefore always awards a full + dialog. Update REQ-DEF-SCHEMATIC-DROP. +2. **Confirm wave scaling in playtests.** `threat_rate_formula` is the + only time-scaling axis; verify the tuned curve (see `derived.md`) + produces the intended difficulty race in real runs. +3. **Gate shortcut-recipe drops on their inputs.** Extend the assembler + recipe schematic pool eligibility in REQ-DEF-SCHEMATIC-DROP: in + addition to the existing station-level and output-item checks, all of + the recipe's input item types must be implicitly unlocked as well. +4. **Resource deposits.** Add a terrain deposit layer per the Resource + deposits rules (`rules.md`): deposit patches generated in expansion + columns (deterministic content per expansion, randomized placement + within the new columns), deposit rendering, and a miner condition (a + resource recipe is selectable only if the miner's footprint overlaps + at least one matching deposit tile). Touches REQ-BLD-MINER ("every + asteroid tile is equivalent" no longer holds), + REQ-GW-ASTEROID-EXPAND / REQ-EXP-*, `world.toml`, and `visuals.toml`. + Until this lands, quartz mines anywhere and the mid-game is + knowledge-gated only. diff --git a/docs/balancing/derived.md b/docs/balancing/derived.md new file mode 100644 index 0000000..4ccefad --- /dev/null +++ b/docs/balancing/derived.md @@ -0,0 +1,164 @@ +# Derived Values (current tuned state) + +Everything here is derived from `targets.md` under the rules in +`rules.md`, and mirrors the config files. Item threats, ship threats, +ratios, and belt checks are verified by `tools/threat_report.py` — re-run +it after any recipe or material change and update this file when values +move. Combat stats were tuned empirically against the arena suite in +`bin/balancing/data/balancing.toml` (round-by-round record in +`history.md`). + +## Economy constants + +- `scrap_per_threat = 0.25` — 1 scrap per 4 threat destroyed (a cruiser + kill drops ~59 scrap); threat(scrap) = 4. +- Scrap smelting: 1 scrap → 1 iron_ingot, 1 s — deliberately + value-losing; reprocessing is the value-preserving path. +- Reprocessing: 4 scrap per cycle, 4 s; full-pool weights iron_ingot 30 / + copper_ingot 30 / silicon 20 / voidsteel 20 → threat(voidsteel) + = (4·4 + 4)/0.2 = 100. +- `scrap_despawn_seconds = 120` (a capital kill drops hundreds of scrap, + collected one per salvage cycle). + +## Recipes and item threats + +(dur in seconds; threat is per output unit) + +| item | recipe | dur | out | threat | +|---|---|---|---|---| +| iron_ore / copper_ore | miner | 1 | 1 | 1 | +| quartz | miner (deposit) | 2 | 1 | 2 | +| iron_ingot | 1 iron_ore | 1 | 1 | 2 | +| copper_ingot | 1 copper_ore | 1 | 1 | 2 | +| silicon | 1 quartz | 2 | 1 | 4 | +| steel_plate | 2 iron_ingot | 3 | 1 | 7 | +| copper_wire | 1 copper_ingot | 1 | 2 | 1.5 | +| copper_coil | 2 copper_wire | 1.5 | 1 | 4.5 | +| building_block | 2 steel_plate | 2 | 4 | 4 | +| control_chip | 1 silicon + 2 copper_wire | 5 | 1 | 12 | +| capacitor_bank | 2 copper_coil + 1 silicon | 5 | 1 | 18 | +| hardened_steel | 3 steel_plate | 12 | 1 | 33 | +| ceramic_plate | 2 quartz | 4 | 1 | 8 | +| drive_unit | 2 steel_plate + 2 copper_coil + 1 control_chip | 8 | 1 | 43 | +| voidsteel_plate | 1 voidsteel + 1 hardened_steel | 8 | 1 | 141 | +| capital_core | 2 voidsteel + 1 capacitor_bank + 1 control_chip | 10 | 1 | 240 | + +Shortcut recipes (drop-only; item threat stays defined by the base path +via the max rule): `shortcut_steel_plate` 3 iron_ore → 1 plate (2 s, +level 1), `shortcut_control_chip` 2 quartz → 1 chip (4 s, level 2), +`shortcut_hardened_steel` 4 iron_ingot → 1 hardened (8 s, level 2). + +Ratio curve realized: t1 all 1:1 (miner:smelter); t2 clean 2:3 +(ingot→plate, wire→coil); t3 strange — 2:5 (silicon→chip), 3:5 +(coil→capacitor), 3:4 (plate→hardened, plate→drive); t4 inverted 3:2 +(hardened→voidsteel_plate). Belt check: worst input demand 1.33 items/s, +under the ~2/s single-belt cap everywhere. + +## Module prefabs + +(contribution = item threat + module production time) + +| module | recipe | dur | mod. time | contribution | +|---|---|---|---|---| +| railgun_s | 1 copper_coil | 1 | 1 | 6.5 | +| salvager | 1 steel_plate + 2 copper_wire | 2 | 1 | 13 | +| repair_tool | 1 steel_plate + 2 copper_wire | 2 | 1 | 13 | +| armor_plates | 4 steel_plate | 3 | 1 | 32 | +| maneuvering_thrusters | 1 steel_plate + 1 copper_coil | 2 | 1 | 14.5 | +| sensor_booster | 2 copper_wire + 1 copper_coil | 2 | 1 | 10.5 | +| afterburner | 2 copper_coil + 1 steel_plate | 3 | 1 | 20 | +| weapon_stabilizer | 1 steel_plate + 1 copper_coil | 2 | 1 | 14.5 | +| weapon_primer | 1 capacitor_bank + 1 copper_coil | 4 | 2 | 28.5 | +| weapon_upgrade | 1 control_chip + 1 copper_coil | 4 | 2 | 22.5 | +| railgun_m | 1 capacitor_bank + 2 steel_plate + 1 copper_coil | 4 | 3 | 43.5 | +| drone_bay | 1 control_chip + 2 steel_plate + 1 copper_coil | 4 | 3 | 37.5 | +| railgun_l | 1 capacitor_bank + 2 hardened_steel + 1 ceramic_plate | 6 | 4 | 102 | +| drone_hangar | 1 voidsteel_plate + 2 control_chip + 1 drive_unit | 10 | 6 | 224 | + +## Ships + +(fitted = hull item + ship base time + default loadout; the default +loadouts are the `default_modules` used by enemy waves and are +geometry-validated against the hull grids) + +| ship | hull recipe | dur | base | default loadout | fitted | +|---|---|---|---|---|---| +| drone | 1 iron_ingot | 1 | 1 | railgun_s | 10.5 | +| frigate | 2 steel_plate + 1 copper_wire | 2 | 2 | 2× railgun_s, maneuvering_thrusters | 47 | +| destroyer | 3 steel_plate + 2 copper_coil | 4 | 3 | 3× railgun_s, armor_plates, sensor_booster | 99 | +| cruiser | 2 hardened_steel + 2 control_chip | 6 | 4 | 2× railgun_m, armor_plates, maneuvering_thrusters | 233.5 | +| battlecruiser | 3 hardened_steel + 2 control_chip + 1 drive_unit | 8 | 5 | 3× railgun_m, armor_plates, 2× railgun_s | 354.5 | +| battleship | 3 voidsteel_plate + 1 drive_unit + 2 control_chip | 10 | 6 | railgun_l, 2× railgun_m, weapon_stabilizer, 2× railgun_s | 722.5 | +| dreadnought | 5 voidsteel_plate + 1 capital_core + 2 drive_unit | 12 | 8 | 3× railgun_l, 4× armor_plates, railgun_s | 1491.5 | +| carrier | 5 voidsteel_plate + 1 capital_core + 2 drive_unit | 12 | 8 | drone_hangar, 2× railgun_m, 2× armor_plates, sensor_booster | 1436.5 | + +## Combat stats + +(arena-converged, 2026-07; see `history.md` rounds 1–5) + +**Weapons:** railgun_s 2 dmg × 2.0 Hz (4.0 DPS), range 50 m; +railgun_m 14 × 1.5 (21), range 70; railgun_l 52 × 0.8 (41.6), range 100. + +**Hull HP** (15/threat prior + empirical trims): drone 60, frigate 300, +destroyer 550, cruiser 1500, battlecruiser 2400, battleship 6300, +dreadnought/carrier 24000. + +**Mobility ladder** (speed m/s | main accel | maneuvering | angular | +max rot): drone 45|60|30|12|6, frigate 35|45|22|8|4, +destroyer 30|35|18|6|3, cruiser 24|25|12|4|2, battlecruiser 20|20|10|3|1.5, +battleship 15|14|7|2|1, dreadnought/carrier 10|8|4|1|0.5. +Sensors: 150/200/220/250/260/280/300/350 m. + +**Other modules:** armor_plates +1200 HP; repair_tool 9 HP × 1 Hz, +range 80; salvager range 60, cargo 20, 0.5 collections/s; afterburner +×1.6 speed +60 accel; maneuvering_thrusters ×1.2 speed +10 maneuvering; +sensor_booster +50 m; weapon_upgrade ×1.2 damage; weapon_primer ×1.2 +rate; weapon_stabilizer ×1.3 range ×0.8 rate. + +**Stations:** HQ 5000 HP. Player station 3000 HP, 25 dmg × 1 Hz, +range 120, scrap 40. Enemy station: 3000+1500x HP, 25+12x dmg, +1.0+0.1x Hz, range 120, scrap 40+30x (x = push level). + +## Pacing + +**Unlock ladder** (level → unlocks; ← marks `unlock_requires`; starting +set at −1: drone, frigate, railgun_s, salvager, building_block recipe): + +| level | ships | modules | recipes | +|---|---|---|---| +| 0 | destroyer | repair_tool, armor_plates | | +| 1 | | maneuvering_thrusters, sensor_booster | shortcut_steel_plate | +| 2 | cruiser | railgun_m, afterburner | shortcut_control_chip, shortcut_hardened_steel | +| 3 | | weapon_stabilizer | | +| 4 | battlecruiser ← cruiser | weapon_primer, weapon_upgrade | | +| 5 | | drone_bay | | +| 6 | battleship ← battlecruiser | railgun_l ← railgun_m | | +| 8 | dreadnought ← battleship | | | +| 9 | carrier ← battleship | drone_hangar | | + +Level 0's pool has exactly three entries (a full first dialog). Level 2 +is the quartz gate: cruiser and railgun_m are the first schematics whose +chains reach quartz; the shortcut outputs only become implicitly +unlocked alongside them, so shortcuts cannot drop early. + +**Threat rate** `2*x + 0.15*x*x` (x = boss cycle counter), against the +factory-size curve with ~half the player's output assumed military: + +| cycle x | rate (threat/s) | player military (≈ curve/2) | +|---|---|---| +| 2 | 4.6 | ~12 | +| 6 | 17.4 | ~30 | +| 15 | 63.8 | ~60 | +| 20 | 100 | ~75 | +| 24 | 134 | — | + +**Economy:** `starting_building_blocks = 200`; expansion cost formula +`300 + 50*x + 10*x*x` (x = expansions already purchased: ~1 affordable +per cycle mid-game at ~1/3 of block income, stretching to 2–3 cycles +late — quadratic so costs outrun the roughly linear block income +gradually, never with a hard wall); `artifact_win_count = 5` with +`artifact_chance_formula = 0.05*x`. Building costs: belt 2, splitter 3, +tunnels 5, miner 15, smelter 20, assembler 35, reprocessing plant 40, +salvage bay 25, shipyard 60 — averaging ≈18 blocks per placed building +(belts included), which meets the 4-minute doubling target at block +threat 4. diff --git a/docs/balancing/history.md b/docs/balancing/history.md new file mode 100644 index 0000000..690d390 --- /dev/null +++ b/docs/balancing/history.md @@ -0,0 +1,96 @@ +# Balancing History + +Chronological record of the balancing work: what was decided, what was +found, what changed. Current values live in `derived.md`; this file +explains how they got there. + +## 2026-07-02/03 — rules and structural decisions + +- Rules document written (now `rules.md`): ratio curve, shortcut + recipes, refactorability, cost archetypes, threat model, growth curve. +- Scrap derived from threat (`scrap_per_threat`), replacing authored + per-ship scrap drops; scrap threat became the constant + `1/scrap_per_threat`, removing the old min-scrap_drop derivation and + its circularity. +- Duplicate schematic drops removed (no level-ups); ship/module levels + removed entirely — all time scaling lives in the threat rate, push + scaling stays on stations. Mk2 upgrade recipes noted as the future + per-item progression. +- Growth-curve rules added: escalating expansion costs, designed + doubling time, growth limited by economy not waiting; resource + deposits designed (deposit-gated mid resource in expansion territory). +- Production tree v2 decided: iron/copper everywhere (M-type asteroid), + quartz in geodes (mid), voidsteel battle-forged from scrap (late); + titanium dropped; lasers renamed to railguns, lasers reserved as a + future weapon type. + +## 2026-07-03 — targets, tree, numbers + +- Balancing targets fixed: ≤2 h run, phases 1–5/6–14/15+, factory curve + 25/60/120/150, threat ladder, 25-ship swarm, block roots. +- Tree structure drafted and numbers computed (recursive threat + calculator); ratio curve realized; fitted ships within 96–124% of the + strawman ladder (small end hot from fixed chain overhead — ladder + later adopted the achieved values). +- **Rule bugs found by the numbers work:** the scrap→ingot smelter + recipe would inflate basic materials via the max rule (fixed: + scrap-consuming recipes are threat fallback only); recipe output + amounts were ignored (fixed: per-unit division); items downstream of + reprocessing-only items never resolved (fixed: fixpoint resolution); + a shortcut recipe resolving earlier than the base path silently + underpriced items (fixed: commit only when all eligible recipes are + computable). All four fixed in `ThreatCostCalculator` with tests, and + implemented in `tools/threat_report.py`. +- v2 tree written into the configs; `default_modules` loadouts + geometry-validated (the numbers-pass loadouts for battlecruiser and + dreadnought were geometrically impossible — L-modifiers don't fit + beside full gun complements; corrected loadouts landed closer to the + ladder). + +## 2026-07-04 — combat stats, arena rounds 1–5 + +Initial stats derived from the anchors (weapon DPS ≈0.6/threat flat, +hull 15 HP/threat, armor 20/threat, repair 2 HP/s/threat, station +range 200). + +- **Round 1:** concentrated fleets won all equal-threat cross-tier + matchups flawlessly; glass beat armored; repair escort flawless; two + stations shrugged off a 3× swarm. Changes: concentration tax on m/l + gun damage (railgun_m 17→14, railgun_l 70→52), armor 640→1000, + repair 25→12, station range 200→120. (Team-1 "bias" in mirrors later + shown to be noise.) +- **Round 2 (EHP-margin logging added):** battleship +33% while + dreadnought −37% (stabilizer range + opposing armor); glass still + +11%. Changes: stabilizer range ×1.5→×1.3; per-hull trims introduced + (BC 2700→2500, BS 7500→7000, DN/CV 15500→19000). +- **Round 3 (narrow lanes — geometry fixed into the fixture):** + DN closed to −11%, BS +22%, swarm flipped to +14% over cruisers, + glass +12% third time. Changes: armor 1000→1200, BC 2500→2000, + BS 7000→6300, DN/CV 19000→22500. +- **Round 4:** glass-vs-armored resolved (+3% armored); noise floor + established (~±10%/run: BS ignored a −10% EHP cut; repair drifted + 14→24% untouched). Convergence policy adopted: two-round signals only, + ±20% converged. Changes: BC 2000→2200, DN/CV 22500→24000; BS +23% + accepted as doctrine texture (mechanical range edge vs. pure small + fleets). +- **Round 5 (durations logged; end-condition bug fixed upstream):** + TTK anchor validated (mirrors 23/71/95 s; DN-vs-swarm 214 s outlier + accepted); dreadnought +3%, everything else inside band. Final + changes: BC 2200→2400, repair 12→9 (persistent +24% escort margin). + **Combat pass declared converged.** + +## 2026-07-05/06 — pacing pass + +- Unlock ladder set (starting set drone/frigate/railgun_s/salvager; + quartz gate at level 2; capitals at 8–9 with `unlock_requires` + chains); threat rate `2*x + 0.15*x*x`; starting blocks 1000→200; + expansion 400 flat pending the cost formula; artifacts 3→5. +- **Bug found:** the building_block recipe was silently locked at game + start (building blocks appear in no schematic's materials, so implicit + unlocking could never reach the recipe) — fixed with an explicit + `unlock_at_station_level = -1`. +- Expansion cost formula implemented and set (`300 + 50*x + 10*x*x`): + quadratic, so costs outrun the roughly linear block income gradually + — ~1 expansion per cycle mid-game, 2–3 cycles apart late. +- **First full balancing round complete.** Next: full-game playtests + against the run-shape targets. diff --git a/docs/balancing/process.md b/docs/balancing/process.md new file mode 100644 index 0000000..22d5e5e --- /dev/null +++ b/docs/balancing/process.md @@ -0,0 +1,98 @@ +# Balancing Process + +How balancing is done in this project: the pass order, the tuning +discipline, and the tools. Refer to this when starting the next +balancing round. + +## The pass order + +Each pass depends on the ones before it; a change in an earlier pass +invalidates the later ones (but not vice versa). Redo from the earliest +pass whose inputs changed. + +1. **Targets** (`targets.md`) — choose the root numbers: run shape, + factory curve, threat-cost ladder, fleet size, block roots, combat + anchors, pacing anchors. These are design decisions, not + measurements. Everything else is derived from them. +2. **Tree structure** (`../content_design.md`) — items, chains, + what-consumes-what, per the production tree rules (one input per + phase transition, generic parts, archetypes, refactorability). + Structure only, no quantities. +3. **Numbers** (`derived.md`, recipes/materials in the configs) — + quantities and durations so every fitted ship sums to its ladder + value, the ratio curve is realized, and the belt/buffer guardrails + hold. Verified computationally by `tools/threat_report.py`. +4. **Calculator/tooling parity** — the game's `ThreatCostCalculator` + and `tools/threat_report.py` must produce identical values; the + Python tool is the design reference. Any semantic change to + REQ-THREAT-* needs both updated plus tests. +5. **Combat stats** (arena-driven) — derive stats from the combat + anchors, then iterate against the arena suite + (`bin/balancing/data/balancing.toml`) until equal-threat matchups are + near-draws. Threat costs are stat-independent, so arena ship counts + stay valid across stat changes. +6. **Pacing** — unlock ladder, `unlock_requires` edges, threat rate, + block/artifact/expansion values, per the pacing anchors. + +Then: **full-game playtests**, which are the only check for the pacing +pass and feed back into targets. + +## Tuning discipline (learned in arena rounds 1–5) + +- **Change anchors, not symptoms.** When a class of results is off, + adjust the anchor that explains all of them (e.g. the concentration + tax) rather than individual stats. +- **Fewest knobs per round.** Attribution dies when many knobs move at + once. Prefer one anchor change plus its mechanical compensations. +- **Shared vs. local knobs.** Guns and module stats are shared across + many hulls — changing them moves many matchups. Per-hull HP moves + exactly one matchup; it is the designated per-ship trim knob on top of + the HP-per-threat prior. +- **Mind the ride-alongs.** A module buff lands on every default loadout + containing it (e.g. an armor buff strengthens the destroyer swarm that + opposes the dreadnought). Compute the net effect per matchup before + choosing step sizes. +- **Two-round signal policy.** Single arena runs re-roll by ~±10% EHP + margin; a margin inside ±20% counts as converged for v1. Only act on + signals that persist across two rounds. +- **Arena geometry is part of the fixture.** Lane width/height changes + the results (full engagement vs. fleets slipping past); margins are + only comparable within the same geometry. +- **Accept mechanical texture.** Not every deviation is a bug: a margin + that survives a stat change is mechanical (usually range/kiting under + the orbit AI) and may be desirable doctrine texture. Document the + acceptance in `targets.md` instead of chasing it. +- **Range is the strongest stat** under the orbit AI — free approach + fire. Price range modifiers conservatively; station dominance is + controlled via range, not HP. + +## Tools + +- `tools/threat_report.py` — item threats, module contributions, + hull/fitted ship threats, producer:consumer ratios, belt feasibility; + reads the real configs. The design reference for threat semantics. +- `tools/verify_recipes.py` — recipe tree closure, visuals coverage, + orphans, reprocessing-only items. +- `tools/verify_layouts.py` — module footprint gating matrix per hull. +- **Balancing tool** (`balancing` target) — parallel arena simulation of + `bin/balancing/data/balancing.toml`; logs winner, surviving counts, + team EHP %, and fight duration per arena. The suite covers: class + mirrors (expect near-mutual annihilation, symmetric winners), + equal-threat cross-tier matchups (expect near-draws — power-per-threat + made empirical), a 2:1 decisiveness check, doctrine matchups + (armored-vs-glass, repair-escort), and station assault. + +## Checklist for the next balancing round + +1. Pull; run `verify_recipes.py`, `verify_layouts.py`, + `threat_report.py`; compare against the tables in `derived.md`. +2. If recipes/materials changed: re-check fitted threats vs. the ladder + in `targets.md`; update arena suite ship counts if fitted values + moved. +3. Run the arena suite; read EHP margins and durations against the + expectations noted in `balancing.toml` and the anchors. +4. Apply changes per the tuning discipline (two-round signals only); + record the round and its knob changes in `history.md`. +5. Update `derived.md` where values moved; if an anchor moved, update + `targets.md` and state why. +6. Commit and push (the review workflow reads the remote). diff --git a/docs/progression_design.md b/docs/balancing/rules.md similarity index 82% rename from docs/progression_design.md rename to docs/balancing/rules.md index 6e9fb17..57a9870 100644 --- a/docs/progression_design.md +++ b/docs/balancing/rules.md @@ -1,9 +1,10 @@ -# Progression & Balancing Design +# Balancing & Progression Rules Rules and principles that govern the production tree, progression pacing, -and balancing. This document contains **rules only** — concrete content -(item lists, recipes, unlock levels, stat numbers) lives in the config files -and `content_design.md`; those numbers must follow the rules stated here. +and balancing. This document contains **rules only** — the chosen base +numbers live in `targets.md`, everything derived from them in +`derived.md`, and the concrete content in the config files and +`../content_design.md`. All of those must follow the rules stated here. ## Player-experience goals @@ -36,7 +37,7 @@ adapts later (see Refactorability). - The fourth input is the core loop hook: capital ship production requires fighting (salvaging and reprocessing), not just mining. - Every gating has a fictional reason (concrete fiction in - `content_design.md`): the asteroid is a metal-rich body, so its bulk + `../content_design.md`): the asteroid is a metal-rich body, so its bulk rock is minable anywhere; the mid resource sits in rare pockets; the late input is battle-forged — created only in the violence of ship destruction, which is why any wreck (including the player's own) @@ -122,7 +123,8 @@ adapts later (see Refactorability). already unlocked (in addition to the station level check). The player is never offered a shortcut for a chain they have not built yet. The output-item half of this check already exists in - REQ-DEF-SCHEMATIC-DROP; the input half is new (see Action items). + REQ-DEF-SCHEMATIC-DROP; the input half is an open action item (see + `README.md`). - **Shortcuts are pure rewards, never balance factors.** An item's threat value is the *maximum* across its producing recipes (REQ-THREAT-ITEM), so unlocking a cheaper recipe does not lower the item's threat accounting — @@ -178,8 +180,8 @@ supporting different fleet doctrines feel structurally different to build. modules, and tiers. Higher tiers are better per *ship* and per *module slot*, not per invested factory-second — their advantage is concentration (fewer, bigger things; slot geometry per - `content_design.md`) and qualitative capabilities, not a better exchange - rate. Deviations from this rule are deliberate and documented. + `../content_design.md`) and qualitative capabilities, not a better + exchange rate. Deviations from this rule are deliberate and documented. - **Difficulty race:** the enemy threat rate (`threat_rate_formula`) is tuned against the factory output (threat/s) achievable by a competent player — slightly below it early, crossing above it eventually. The game @@ -188,9 +190,8 @@ supporting different fleet doctrines feel structurally different to build. - **All time scaling lives in the threat rate** — waves get bigger, ships of a given schematic never get individually stronger. There is no ship level dimension: stat formulas are plain values, and per-ship level - scaling does not exist. Push - scaling on enemy defence stations is the separate, player-triggered - difficulty axis and keeps its level formulas. + scaling does not exist. Push scaling on enemy defence stations is the + separate, player-triggered difficulty axis and keeps its level formulas. ## Unlock & drop pacing @@ -206,18 +207,18 @@ supporting different fleet doctrines feel structurally different to build. gate, a schematic (ship, module, or assembler recipe) may list prerequisite schematics (`unlock_requires`, REQ-LOCK-PREREQ) that must already be unlocked before it enters the drop pool — e.g. the medium - laser requires the small laser; a future Mk2 requires its base version. - Station level gates the earliest - *when*; prerequisites gate the *order*, keeping drop offers coherent - with what the player already owns. + gun requires the small gun; a future Mk2 requires its base version. + Station level gates the earliest *when*; prerequisites gate the + *order*, keeping drop offers coherent with what the player already + owns. - **No duplicate drops.** Ship and module schematics leave the drop pool once owned, exactly as assembler recipe schematics already do. There are no schematic level-ups; player power grows through unlock breadth and factory scale only, which keeps power-per-threat exact on both sides. The pool therefore shrinks over a run and late pushes increasingly offer artifacts — intended: the late game is a race for the win condition. - Per-item progression may return later as Mk2 upgrade recipes (see Open - tasks), never as free level-ups. + Per-item progression may return later as Mk2 upgrade recipes (see Future + work), never as free level-ups. - **Artifacts trade power for progress.** Artifact options compete with schematic picks in the same choice dialog; the artifact chance must be tuned so that taking one is a real decision (giving up an unlock), not @@ -295,7 +296,7 @@ supporting different fleet doctrines feel structurally different to build. through the HQ's single belt port, so income is capped at belt throughput regardless of assembler count. Per-building costs should be high enough that this cap can bind late-game (see the condensed-block - idea under Open tasks). + idea under Future work). ## Numeric guardrails @@ -314,15 +315,8 @@ Constraints that every recipe must respect, independent of tuning: - **Cycle times scale with tier** monotonically — a higher-tier item never has a shorter total chain time than a lower-tier item of the same role. -## Open tasks / future work +## Future work -- **Rework `recipes.toml`** once the rules in this document are fixed: the - current tree feels too close to Factorio; apply the thematic-naming rule - and the ratio curve to it (renames and quantity changes, not new items). -- **Balancing pass** (see placeholders in `content_design.md`): set the - `unlock_at_station_level` ladder, real threat costs and - `default_modules`, reprocessing weights, and the threat-rate formulas - according to the rules above. - **Condensed building blocks** — a drop-unlockable shortcut-style recipe that packs several blocks' worth of value into one belt item, relieving the HQ intake ceiling (see Building block economy) as a @@ -337,39 +331,3 @@ Constraints that every recipe must respect, independent of tuning: assembler), and keeps balancing one-dimensional (no level variable anywhere). Enemy-side progression happens via `default_modules` variants per era instead of a level formula. - -## Action items — changes beyond this document - -Agreed changes that require edits to `requirements.md`, the code, and the -configs. Completed items are removed from this list (their outcomes live -in `requirements.md` and the git history). Still open: - -1. **Fill unfillable schematic slots with artifacts.** With duplicates - removed, the schematic drop pool can run dry — previously unreachable. - Decision: every slot in the choice dialog that cannot be filled with a - schematic because the eligible pool is exhausted is filled with an - artifact option instead (in addition to any artifact option granted by - the regular artifact roll). A push therefore always awards a full - dialog. Update REQ-DEF-SCHEMATIC-DROP. -2. **Confirm wave scaling carries alone.** With per-ship level scaling - removed, `threat_rate_formula` is the only time-scaling axis; verify - the current `world.toml` values still produce the intended difficulty - curve (likely a review, not a retune, since the config already ran - with a flat ship level before the removal). -3. **Gate shortcut-recipe drops on their inputs.** Extend the assembler - recipe schematic pool eligibility in REQ-DEF-SCHEMATIC-DROP: in - addition to the existing station-level and output-item checks, all of - the recipe's input item types must be implicitly unlocked as well. -4. **Expansion cost formula.** Replace the flat - `world.toml [expansion].cost_building_blocks` with a formula of the - number of expansions already purchased (x = expansions bought so - far), so expansion costs can escalate per the block economy rules. - Update REQ-EXP-COST and wherever the UI displays the expansion cost. -5. **Resource deposits.** Add a terrain deposit layer per the Resource - deposits rules: deposit patches generated in expansion columns - (deterministic content per expansion, randomized placement within the - new columns), deposit rendering, and a miner condition (a resource - recipe is selectable only if the miner's footprint overlaps at least - one matching deposit tile). Touches REQ-BLD-MINER ("every asteroid - tile is equivalent" no longer holds), REQ-GW-ASTEROID-EXPAND / - REQ-EXP-*, `world.toml`, and `visuals.toml`. diff --git a/docs/balancing/targets.md b/docs/balancing/targets.md new file mode 100644 index 0000000..1b443a1 --- /dev/null +++ b/docs/balancing/targets.md @@ -0,0 +1,104 @@ +# Balancing Targets (base numbers) + +The root numbers of the balancing. Everything in `derived.md` is tuned to +hit these; when rebalancing, **change these first and re-derive — never +patch derived values directly**. The rules these numbers follow live in +`rules.md`. + +All time targets are in **game time**. The player can pause and +accelerate, so real session length differs; playtests measure both. The +time unit is the boss cycle (`world.toml boss_countdown_seconds`, 300 s). +Destroying a station set advances the boss countdown by +`boss_advance_seconds` (60 s), so cycles run shorter than nominal when +pushing actively — targets deliberately ignore that. + +## Run shape + +1. **Run length** — a winning run takes up to 2 hours of game time: win + around boss cycle 20–24. Losing runs end earlier. +2. **Phase boundaries** — early = cycles 1–5 (iron/copper, small hulls), + mid = cycles 6–14 (quartz, medium hulls), late = cycles 15+ + (voidsteel, capitals). Push cadence: first station set around cycle + 2–3, roughly one per cycle from mid onward — so the destroyed set's + level ℓ is reached around cycle ℓ+2. +3. **Factory size curve** — producing buildings over time; when + saturated, output threat/s equals this count, so this curve IS the + player power curve: ~25 when the starting asteroid is full (end of + cycle 2), ~60 at the start of mid (cycle 6), ~120 at the start of + late (cycle 15), ~150 near the win. `threat_rate_formula` must remain + a fraction of this curve; buildings plus belts must physically fit + the asteroid plus affordable expansions. +4. **Threat-cost ladder** — total production-seconds per *fitted* hull + (including the typical/default module loadout): drone 10.5, + frigate 47, destroyer 99, cruiser 233.5, battlecruiser 354.5, + battleship 722.5, dreadnought 1491.5, carrier 1436.5. Every + production chain must sum to its ladder value. (The original strawman + was 10/40/80/200/350/700/1500; the small end runs ~10–20% hot because + fixed chain overhead dominates small hulls — accepted, and the + achieved values adopted as the ladder. The ~×2-per-class curve shape + is the invariant.) +5. **Fleet size** — swarm-leaning: ~25 player combat ships as the + standing mid-game fleet. Standing fleet = build cadence (4) × average + ship lifetime, so this target drives time-to-kill and therefore all + combat stat magnitudes. +6. **Block economy roots** — bootstrap complete (starting asteroid full) + by the end of cycle 2; a factory spending ~30% of its capacity on + blocks doubles in ~4 minutes early game; one expansion affordable per + cycle at ~1/3 of block income mid-game, decelerating to one per 2–3 + cycles late as escalating costs outrun income. + +## Combat anchors + +All combat stats derive from these; per-hull HP additionally carries +empirical trims from arena rounds (values in `derived.md`). + +- **Weapon DPS per threat pays a concentration tax that grows with gun + size**: small ≈ 0.62, medium ≈ 0.48, large ≈ 0.41 DPS per threat of + weapon contribution, compensated by the range ladder 50/70/100 m. + Rationale: concentration itself (focus fire, no DPS loss to attrition, + range) is worth paying for — with a flat curve, concentrated fleets + win equal-threat fights outright (arena round 1). +- **Hull HP = 15 per threat of hull contribution** as the prior; per-hull + HP is the empirical trim knob (guns are shared across hulls, hull HP + moves exactly one matchup). The arena consistently prices capitals as + *tanks with taxed guns* — capital hulls sit well above the prior. +- **Armor HP ≈ 37 per threat** — a strong premium over hull HP because + armor is pure HP with no capability, and fights snowball: killing + removes enemy DPS, surviving merely delays — HP must be cheaper than + DPS. +- **Repair ≈ 0.7 HP/s per threat** — in-combat sustain effectively + removes enemy DPS and must be priced like DPS, not like HP. +- **TTK / fight duration**: parity fights in the 30–60 s band at + mid-game scale; capital mirrors ~90 s deliberately; the extreme + tank-vs-chip-damage matchup (dreadnought vs destroyer swarm, ~3.5 min) + is an accepted outlier. +- **Mobility is monotone in size** — the smallest hulls are the fastest + and nimblest. Sensor ranges (150→350 m) always exceed weapon ranges. +- **Weapon modifiers are capital economy**: a ×1.2 damage modifier at + ~22.5 threat beats adding a gun once a ship carries more than ~68 + threat of weapons — modifiers pay off on gun-heavy big hulls, waste on + small ones. Range modifiers are the strongest and are priced/kept + small (×1.3): range is the dominant stat under the orbit AI (free + approach fire). +- **Stations**: a fresh player station holds one early parity wave + unaided; the enemy station at level 0 matches the player station + exactly and scales per push level. Station range is the dominance + lever, not HP (at 4× a small gun's range, two stations annihilated a + 3× threat swarm through approach fire alone). +- **Accepted imbalances**: the carrier loses its equal-threat fights + until the drone-launching capability exists (the hangar is dead + threat) — fix by implementing drones, not stats. A pure smallest-ship + fleet modestly loses (~15–25%) to a range-fitted capital — desirable + doctrine texture; the fair anti-capital answer is the mixed fleet. + +## Pacing anchors + +- **Starting set** is the rule-minimum: drone, frigate, small gun, + salvager (plus the explicitly unlocked building-block recipe). +- **Threat rate shape**: below the player's achievable military output + (≈ half the factory curve) early, crossing at the late boundary + (~cycle 15), overwhelming by ~cycle 24. +- **Winning = five real decisions**: `artifact_win_count` is set so that + across a winning run's ~15–18 pushes (~7 cumulative artifact offers at + the current chance formula), the player must choose the artifact over + a schematic about five times. diff --git a/docs/content_design.md b/docs/content_design.md index d0f4609..02ad634 100644 --- a/docs/content_design.md +++ b/docs/content_design.md @@ -1,9 +1,10 @@ -# Content Design — Ships & Modules +# Content Design — Ships, Modules & Production Tree -First real-content iterations (June 2026). Pass 1 defined ship hull grids and -module surface masks; pass 2 defined the production tree (recipes). Stats and -threat costs in the config files are still placeholders for the balancing -pass. +The designed game content: hull layout grids, module footprints and the +gating between them, and the production tree (items, chains, fiction). +All numbers — quantities, durations, threat values, stats, unlock levels +— live in the config files and are documented with their derivations in +`docs/balancing/` (see `balancing/README.md` for the index). ## Design principle: footprint gating @@ -17,12 +18,12 @@ and makes them trivially moddable through the config files alone. | Footprint | Modules | Smallest hull that fits it | |-----------|---------|----------------------------| -| 1x1 | laser_cannon_s, salvager, repair_tool | drone | +| 1x1 | railgun_s, salvager, repair_tool | drone | | 1x2 | maneuvering_thrusters, sensor_booster, armor_plates | frigate | | 1x3 | afterburner | frigate (eats most of it) | | L-shape (3 cells) | weapon_stabilizer, weapon_primer, weapon_upgrade | frigate | -| 2x2 | laser_cannon_m, drone_bay | cruiser | -| 3x3 | laser_cannon_l | battleship | +| 2x2 | railgun_m, drone_bay | cruiser | +| 3x3 | railgun_l | battleship | | 2x6 | drone_hangar | carrier (only) | ### Hull grids @@ -129,58 +130,11 @@ Maximum simultaneous (disjoint) placements: m guns — cruiser 2, battlecruiser 3, battleship 4; l guns — battleship 1, dreadnought 3; drone hangar — carrier 1. -## Production tree (first pass — superseded) +## Production tree -**Superseded (July 2026):** this first-pass tree predates the rules in -`docs/progression_design.md` and will be replaced. The decisions for the -redesign are recorded in "Production tree v2 — decisions" below; the text -of this section is kept for reference until the new tree lands. - -Design principle: each game phase adds exactly one new base input chain, so -factory complexity ramps alongside ship size. - -| Phase | New input | How acquired | Unlocks | -|-------|-----------|--------------|---------| -| early | iron_ore, copper_ore | mined | drone, frigate, destroyer; small guns and basic supports | -| mid | titanium_ore | mined (3x slower than iron) | cruiser, battlecruiser; m guns, drone bay, weapon modifiers | -| late | advanced_alloy | ONLY from reprocessing salvaged scrap | battleship, dreadnought, carrier; l guns, drone hangar | - -The advanced_alloy gate is the core loop hook: capital ship production -requires fighting (salvaging scrap from kills and reprocessing it), not just -mining. The reprocessing plant turns 5 scrap into iron/copper/titanium ingots -or advanced_alloy probabilistically. - -Intermediate components, by tier: - -- **Tier 2 (early):** copper_wire (copper), steel_plate (iron), circuit_board - (iron + wire), building_block (iron). -- **Tier 3 (mid):** mechanical_parts (steel + iron), targeting_unit (circuits - + wire), drive_unit (steel + mechanical_parts + circuit), titanium_frame - (titanium + steel). -- **Tier 4 (late):** reinforced_plating (steel + advanced_alloy), - capital_core (targeting_unit + drive_unit + 2 advanced_alloy). - -Hulls and modules consume intermediates of their tier: early items are built -from tier-2 parts, midgame items require tier-3 parts (deeper chains, more -assemblers), capital items require tier-4 parts (and therefore combat). Hull -items are named `_hull`; module items `_module`. Every item has -an `[items.*]` entry in visuals.toml; hull item outlines match the ship's -fleet color from `[ships.*]`. - -Consistency is checked by `tools/verify_recipes.py` — re-run it after editing -recipes, ship/module materials, or visuals: - - python dota_factory/tools/verify_recipes.py - -It verifies every consumed item has a producer, every item has a visuals -entry, flags orphaned items, and prints which items are reprocessing-only -(currently exactly advanced_alloy). - -## Production tree v2 — decisions (July 2026) - -The old tree is discarded; the new one is designed against the rules in -`docs/progression_design.md` (ratio curve, cost ladder, cost archetypes, -refactorability). Decisions fixed so far: +Designed against the rules in `docs/balancing/rules.md` (ratio curve, +cost ladder, cost archetypes, refactorability). Quantities, durations, +and threat values live in `docs/balancing/derived.md`. ### Base inputs (4) and fiction @@ -189,28 +143,27 @@ refactorability). Decisions fixed so far: *is* ore, which is why the shipyard operation was built here at all. - **quartz** — mid-game, minable only on geode deposit patches in expansion territory (see the Resource deposits rules in - `progression_design.md`). Fiction: ordinary silicate dust is everywhere - and worthless; chips and optics need rare, pocket-bound optical-grade - crystal. + `docs/balancing/rules.md`; the deposit mechanic itself is an open + action item — until it lands, quartz mines anywhere). Fiction: + ordinary silicate dust is everywhere and worthless; chips and optics + need rare, pocket-bound optical-grade crystal. - **voidsteel** — late-game, obtained only by reprocessing scrap. Fiction: battle-forged — formed when weapon plasma anneals hull metal in the violence of ship destruction. Any wreck yields it, including the player's own; no foundry can replicate it. -- **titanium is dropped.** Its hull-gating role moves to quartz-era - control systems ("you can smelt all the steel you want, but you cannot - steer a battlecruiser without electronics") and possibly a *quality* - steel step — e.g. a long-running hardened-steel recipe (time-heavy - archetype). Open question for the tree draft: quality step vs. - electronics-only gating; explicitly **not** sheer steel quantity alone. +- **titanium was dropped** (v1 tree). Its hull-gating role moved to + quartz-era control systems ("you can smelt all the steel you want, but + you cannot steer a battlecruiser without electronics") plus the + hardened-steel quality step (a deliberately long-running, time-heavy + recipe) — explicitly not sheer steel quantity alone. ### Material palette (fingerprints per family) - **iron/steel** — structure. - **copper** — conduction and heat: wiring, coils, heat sinks. - **silicon family** (all derived from quartz): silicon (logic, - sensors), glass/optics (lenses, focusing crystals), ceramics (heat - shielding, insulators). Carries the non-metal variety without extra - base inputs. + sensors), ceramics (heat shielding, insulators); glass/optics are cut + from v1 — their only consumers would be lasers, which are deferred. - **voidsteel** — capital-tier structure and exotics. - Deliberately skipped: carbon (mostly redundant with copper/ceramics), plastics (drags in Factorio-style chemical chains; ceramics read more @@ -219,23 +172,25 @@ refactorability). Decisions fixed so far: ### Weapons -- All current lasers are renamed to **railguns** (`laser_cannon_s/m/l` → - `railgun_s/m/l`); footprints and the gating matrix are unchanged. - Implementation stays as-is (instant damage application, no projectile, - no ammunition) — the beam visual reads as a tracer round. Materials: - iron slugs, copper coils, steel rails — the starting-metal fingerprint. +- All v1 weapons are **railguns** (`railgun_s/m/l`, renamed from the + laser placeholders; footprints and the gating matrix unchanged). + Implementation is instant damage application with no projectile and no + ammunition — the beam visual reads as a tracer round. Materials: iron + slugs, copper coils, steel rails — the starting-metal fingerprint. - **Lasers are reserved for later** as a genuinely distinct weapon type (e.g. once projectile/ammunition mechanics exist for other families), arriving with quartz optics. More weapon types are planned; railguns are simply the baseline tech that ships with v1. +- `drone_bay` and `drone_hangar` are footprint-only placeholders: the + drone-launching capability does not exist in the simulation yet, so + they define no capability section. The carrier is deliberately weak + until that capability lands (see the accepted imbalances in + `docs/balancing/targets.md`). -### Tree structure — draft +### Tree structure -Recipes are sketched as input lists only; quantities and durations come -in the numbers pass, tuned so every chain sums to its threat-ladder -value and follows the ratio curve (t1 nice → t4 strange). Glass/optics -are cut from v1 — their only consumers would be lasers, which are -deferred; the silicon family ships as silicon + ceramics. +Input lists only — quantities, durations, and per-item threat values are +in `docs/balancing/derived.md` and the configs. **Mined (miner):** `iron_ore`, `copper_ore` (every tile), `quartz` (geode deposits in expansion territory). @@ -244,7 +199,7 @@ deferred; the silicon family ships as silicon + ceramics. | output | input | ratio class | |---|---|---| -| iron_ingot | iron_ore | nice (1:1 or 1:2) | +| iron_ingot | iron_ore | nice (1:1) | | copper_ingot | copper_ore | nice | | silicon | quartz | mid entry | | iron_ingot | scrap | the safe, boring scrap sink | @@ -280,14 +235,13 @@ fully unlocked pool state. | capital_core | voidsteel + capacitor_bank + control_chip | capital heart | **Hull items** (`_hull`, assembler-made; the shipyard consumes the -hull item plus module materials). The m+ hull gate is resolved as -**both** of the open-question options: hardened_steel (quality steel, a -deliberately long-running recipe — the time-heavy step) *and* -control_chip (electronics): +hull item plus module materials). The m+ hull gate is **both** +hardened_steel (quality steel, the time-heavy step) *and* control_chip +(electronics): | hull | inputs | |---|---| -| drone_hull | steel_plate | +| drone_hull | iron_ingot | | frigate_hull | steel_plate + copper_wire | | destroyer_hull | steel_plate + copper_coil | | cruiser_hull | hardened_steel + control_chip | @@ -302,7 +256,7 @@ stockpiled): | module | inputs | archetype | |---|---|---| -| railgun_s | steel_plate + copper_coil | balanced | +| railgun_s | copper_coil | lean | | salvager | steel_plate + copper_wire | balanced | | repair_tool | steel_plate + copper_wire | balanced | | armor_plates | steel_plate (many) | material-heavy, fast | @@ -317,395 +271,24 @@ stockpiled): | railgun_l | capacitor_bank + hardened_steel + ceramic_plate | late | | drone_hangar | voidsteel_plate + control_chip + drive_unit | late (carrier only) | -Refactorability check (the default technique holds): railgun_s → m -introduces capacitor_bank, built from a subset of the small gun's -inputs (copper_coil) plus the new base resource (silicon); the m gun -otherwise reuses the small gun's inputs. Hulls likewise: cruiser adds +**Refactorability check** (the default technique holds): railgun_s → m +introduces capacitor_bank, built from a subset of the small gun's inputs +(copper_coil) plus the new base resource (silicon); the m gun otherwise +reuses the small gun's inputs. Hulls likewise: the cruiser adds hardening (fed by the existing steel line) and chips (fed by the new quartz territory) without touching the iron/copper core. -**Shortcut recipe candidates** (drop-only assembler schematics; not -every strange chain gets one): +**Shortcut recipes** (drop-only assembler schematics; not every strange +chain gets one): `iron_ore → steel_plate` (skips the ingot step on the +highest-volume chain), `quartz → control_chip` (skips silicon), +`iron_ingot → hardened_steel` (a nicer-ratio route past the deliberately +awkward hardening step). -- `iron_ore → steel_plate` — skips the ingot step on the highest-volume - chain in the game. -- `quartz → control_chip` — skips silicon on the electronics chain. -- `iron_ingot → hardened_steel` — a nicer-ratio route past the - deliberately awkward hardening step. +Consistency is checked by `tools/verify_recipes.py` — re-run it after +editing recipes, ship/module materials, or visuals: -Item count: 3 mined + scrap + 3 smelted + 4 t2 + 5 t3 + 3 t4 (incl. -voidsteel) + 8 hulls + 14 modules ≈ 41 item types — same scale as the -first-pass tree. `verify_recipes.py` must be re-run once this lands in -recipes.toml; the numbers pass should add a threat-report tool that -prints per-item threat values and producer:consumer ratio tables. + python dota_factory/tools/verify_recipes.py -### Numbers — first pass - -These numbers are live in the config files and verified by -`tools/threat_report.py`; quantities and durations are tuned so fitted -ships land on the threat-cost ladder and the ratio curve is realized. -The typical loadouts are geometry-validated against the hull grids and -configured as each ship's `default_modules`: the L-shaped weapon -modifiers turned out not to fit alongside the full gun complement on -the battlecruiser and dreadnought, so their loadouts use armor and -small-railgun fillers instead — which moved all three heavy ships -closer to their ladder targets. - -**Economy constants:** `scrap_per_threat = 0.25` (1 scrap per 4 threat -destroyed — a cruiser kill drops ~59 scrap). Reprocessing: 4 scrap per -cycle, 4 s, full-pool weights iron_ingot 30 / copper_ingot 30 / -silicon 20 / voidsteel 20 → threat(voidsteel) = (4·4 + 4)/0.2 = 100. -Scrap smelting: 1 scrap → 1 iron_ingot, 1 s — deliberately -value-losing (4 threat of scrap becomes a 2-threat ingot); reprocessing -is the value-preserving path. - -**Recipes** (dur in seconds; threat is per output item): - -| item | recipe | dur | out | threat | -|---|---|---|---|---| -| iron_ore / copper_ore | miner | 1 | 1 | 1 | -| quartz | miner (deposit) | 2 | 1 | 2 | -| iron_ingot | 1 iron_ore | 1 | 1 | 2 | -| copper_ingot | 1 copper_ore | 1 | 1 | 2 | -| silicon | 1 quartz | 2 | 1 | 4 | -| steel_plate | 2 iron_ingot | 3 | 1 | 7 | -| copper_wire | 1 copper_ingot | 1 | 2 | 1.5 | -| copper_coil | 2 copper_wire | 1.5 | 1 | 4.5 | -| building_block | 2 steel_plate | 2 | 4 | 4 | -| control_chip | 1 silicon + 2 copper_wire | 5 | 1 | 12 | -| capacitor_bank | 2 copper_coil + 1 silicon | 5 | 1 | 18 | -| hardened_steel | 3 steel_plate | 12 | 1 | 33 | -| ceramic_plate | 2 quartz | 4 | 1 | 8 | -| drive_unit | 2 steel_plate + 2 copper_coil + 1 control_chip | 8 | 1 | 43 | -| voidsteel_plate | 1 voidsteel + 1 hardened_steel | 8 | 1 | 141 | -| capital_core | 2 voidsteel + 1 capacitor_bank + 1 control_chip | 10 | 1 | 240 | - -**Module prefabs** (contribution = item threat + module production -time): - -| module | recipe | dur | mod. time | contribution | -|---|---|---|---|---| -| railgun_s | 1 copper_coil | 1 | 1 | 6.5 | -| salvager | 1 steel_plate + 2 copper_wire | 2 | 1 | 13 | -| repair_tool | 1 steel_plate + 2 copper_wire | 2 | 1 | 13 | -| armor_plates | 4 steel_plate | 3 | 1 | 32 | -| maneuvering_thrusters | 1 steel_plate + 1 copper_coil | 2 | 1 | 14.5 | -| sensor_booster | 2 copper_wire + 1 copper_coil | 2 | 1 | 10.5 | -| afterburner | 2 copper_coil + 1 steel_plate | 3 | 1 | 20 | -| weapon_stabilizer | 1 steel_plate + 1 copper_coil | 2 | 1 | 14.5 | -| weapon_primer | 1 capacitor_bank + 1 copper_coil | 4 | 2 | 28.5 | -| weapon_upgrade | 1 control_chip + 1 copper_coil | 4 | 2 | 22.5 | -| railgun_m | 1 capacitor_bank + 2 steel_plate + 1 copper_coil | 4 | 3 | 43.5 | -| drone_bay | 1 control_chip + 2 steel_plate + 1 copper_coil | 4 | 3 | 37.5 | -| railgun_l | 1 capacitor_bank + 2 hardened_steel + 1 ceramic_plate | 6 | 4 | 102 | -| drone_hangar | 1 voidsteel_plate + 2 control_chip + 1 drive_unit | 10 | 6 | 224 | - -**Ships** (fitted = hull item + ship base time + typical loadout; the -typical loadouts double as the `default_modules` for enemy waves): - -| ship | hull recipe | dur | base | typical loadout | fitted (target) | -|---|---|---|---|---|---| -| drone | 1 iron_ingot | 1 | 1 | railgun_s | 10 (10) | -| frigate | 2 steel_plate + 1 copper_wire | 2 | 2 | 2× railgun_s, maneuvering_thrusters | 47 (40) | -| destroyer | 3 steel_plate + 2 copper_coil | 4 | 3 | 3× railgun_s, armor_plates, sensor_booster | 99 (80) | -| cruiser | 2 hardened_steel + 2 control_chip | 6 | 4 | 2× railgun_m, armor_plates, maneuvering_thrusters | 234 (200) | -| battlecruiser | 3 hardened_steel + 2 control_chip + 1 drive_unit | 8 | 5 | 3× railgun_m, armor_plates, 2× railgun_s | 355 (350) | -| battleship | 3 voidsteel_plate + 1 drive_unit + 2 control_chip | 10 | 6 | railgun_l, 2× railgun_m, weapon_stabilizer, 2× railgun_s | 723 (700) | -| dreadnought | 5 voidsteel_plate + 1 capital_core + 2 drive_unit | 12 | 8 | 3× railgun_l, 4× armor_plates, railgun_s | 1492 (1500) | -| carrier | 5 voidsteel_plate + 1 capital_core + 2 drive_unit | 12 | 8 | drone_hangar, 2× railgun_m, 2× armor_plates, sensor_booster | 1436 (1500) | - -**Checks:** - -- *Ratio curve realized:* t1 all 1:1 (miner:smelter); t2 clean 2:3 - (ingot→plate, wire→coil); t3 strange — 2:5 (silicon→chip), 3:5 - (coil→capacitor), 3:4 (plate→hardened, plate→drive); t4 inverted 3:2 - (hardened→voidsteel_plate). -- *Belt feasibility:* worst input demand is 1.33 items/s (wire→coil, - plate→armor) — under the ~2/s single-belt cap everywhere; no - accidental multi-belt recipes. -- *Block economy:* building_block = 4 threat ⇒ for the 4-minute - doubling at 30% capacity, the average building must cost ≈ 18 blocks - (0.3 × 240 / 4). buildings.toml costs should be set around that mean - (belts cheap, producers ~20–30). -- *Small-end deviation:* frigate–cruiser land 6–24% hot because the - fixed chain overhead dominates small hulls. Recommendation: accept - and adjust the ladder targets to the achieved values (the ~×2-per- - class curve shape is preserved) rather than thinning the early - chains below readability. -- *Rule bug discovered:* the scrap→iron_ingot smelter recipe combined - with REQ-THREAT-ITEM's max-across-recipes rule would set - threat(iron_ingot) to the scrap path (1 + 4 = 5, or more at other - scrap values) instead of 2, inflating every downstream item. Fix: - scrap-consuming recipes count toward an item's threat only when no - scrap-free recipe produces that item, mirroring the reprocessing-path - rule (see progression_design.md action items). - -### Combat stat anchors — first pass - -All combat stats derive from a few anchors so that power-per-threat holds -by construction; the arena suite in `bin/balancing/data/balancing.toml` -verifies it empirically. Change the anchors first, re-derive stats second. - -- **Weapon DPS per threat pays a concentration tax that grows with gun - size**: railgun_s 2 dmg × 2.0 Hz = 4.0 DPS at 6.5 threat (0.62), - range 50; railgun_m 14 × 1.5 = 21 at 43.5 (0.48), range 70; - railgun_l 52 × 0.8 = 41.6 at 102 (0.41), range 100. Arena round 1 - (see below) showed that at equal threat, concentrated fleets win - flawlessly with a flat-ish DPS curve — concentration itself (focus - survivability, range, no losses-to-attrition of own DPS) is worth - paying for, so bigger guns get less raw DPS per threat. -- **Hull HP = 15 per threat of hull contribution** (incl. ship base - time) as the *prior*; per-hull HP is the empirical trim knob for - arena results (guns are shared across many hulls, so gun changes move - many matchups at once — hull HP moves exactly one). Current values - after round-5 trims: drone 60, frigate 300, destroyer 550, - cruiser 1500, battlecruiser 2400, battleship 6300, - dreadnought/carrier 24000. (The capital hulls sit well above the - 15/threat prior while their guns sit below the DPS prior — the arena - consistently prices big ships as tanks with taxed guns.) -- **Armor HP ≈ 37 per threat** — a strong premium over hull HP (15) - because armor is pure HP with no capability, and fights snowball: - killing enemies removes their DPS, while surviving merely delays — - so a point of HP must be cheaper than a point of DPS. - armor_plates adds 1200 HP at 32 threat (raised from 640 → 1000 → - 1200: the all-guns destroyer beat the armored one in three - consecutive arena rounds). -- **TTK check:** a mirror cruiser duel (51 DPS vs 2140 EHP) lasts ~42 s — - inside the 30–60 s parity-fight band implied by the 25-ship fleet - target. Capital mirrors run longer (~90 s), deliberately. -- **Repair ≈ 0.7 HP/s per threat:** repair_tool heals 9 HP × 1 Hz at 13 - threat (nerfed 25 → 12 → 9: in-combat sustain effectively removes - enemy DPS and must be priced like DPS, not like HP — the escorted - team held a persistent +24% at 12). Salvager: collection range 60 (was - a nonsensical 500, beyond any sensor), cargo 20, 0.5 collections/s. - `scrap_despawn_seconds` raised 30 → 120: a capital kill drops - hundreds of scrap collected one per cycle, so 30 s despawned nearly - everything. -- **Mobility ladder is monotone in size** — small = fast and nimble - (drone 45 m/s, 60 accel) down to capitals (10 m/s, 8 accel); the old - placeholder had the drone as the least agile ship. Sensor ranges keep - the existing 150→350 ladder; all weapon ranges stay below sensor - ranges. -- **Weapon modifiers are capital economy:** ×1.2 damage at 22.5 threat - beats adding a gun once a ship carries more than ~68 threat of weapons - — so modifiers pay off on gun-heavy big hulls and are a waste on small - ones. Damage/rate kept at ×1.2; the stabilizer's range multiplier was - cut ×1.5 → ×1.3 after round 2 — range is the strongest stat in the - orbit-AI's hands (free approach fire), and the stabilized battleship - was the one loadout still overperforming. -- **Stations:** a fresh player station (3000 HP, 25 dmg × 1 Hz, - range 120) holds one early parity wave unaided; the enemy station at - level 0 matches it exactly and scales per push level - (+1500 HP, +12 dmg, +0.1 Hz per level). HQ 5000 HP. Range reduced - from 200 in round 1: at 200 (4× a small gun's range) two stations - annihilated a 3× threat swarm through approach fire alone — range, - not HP, is the station dominance lever. -- **Known imbalance, accepted:** the carrier loses its equal-threat - arena until the drone-launching capability exists — the hangar is 224 - threat of dead weight. Do not compensate with stats; fix by - implementing drones. - -**Arena round 1 results (2026-07-04):** mirrors snowball as expected -(40–50% of the winner survives) but team 1 won all three mirrors — -possible sim-side bias, under investigation (progression_design.md -action items 6–7). All four equal-threat cross-tier matchups were -flawless wins for the concentrated side; glass destroyers beat armored; -repair escort beat raw numbers flawlessly; two stations shrugged off a -3× threat swarm. Round-2 knob changes: concentration tax on m/l gun -DPS (17→14, 70→52 damage), armor 640→1000, repair 25→12, station range -200→120. Gun ranges deliberately untouched — if concentrated fleets -still win flawlessly in round 2, the range ladder (kiting) is the next -suspect, ahead of further DPS cuts. - -**Arena round 5 results (2026-07-04, with fight durations) — combat -pass converged:** durations validate the TTK anchor: drone mirror -22.7 s, cruiser mirror 71.2 s, battleship mirror 94.6 s, most fights -in the 22–72 s band; the destroyers-vs-dreadnought slugfest is the -outlier at 214 s (extreme tank vs. chip damage — acceptable for the -extreme case). Converged: mirrors 5–10%, swarm vs cruisers +15%, -frigates vs battleship dropped to +13% on its own (confirming the -noise-floor read — now inside band anyway), dreadnought at +3% knife -edge, glass vs armored +10%, two-to-one 78%, station assault stable at -44%. Two two-round signals actioned in round 6: mixed beat -battlecruisers again (+20%, +18%) → battlecruiser hull 2200→2400; -repair escort held +24% twice with all frigates surviving → -repair_amount 12→9. Everything else is frozen; further refinement -moves to real-game playtests. - -**Arena round 4 results (2026-07-04) and convergence policy:** mirrors -at 0–3%; swarm vs cruisers +7%; glass vs armored resolved at +3% for -armored (armor 1200 confirmed after four rounds of signal). Noise -floor established: the battleship held +23% straight through a −10% -EHP cut, and repair drifted 14→24% with no repair changes — single-run -margins move ~±10% on a re-roll. Policy from here: only act on signals -that persist two rounds; treat anything inside ±20% as converged for -v1 (real-game playtests refine further). Armor-coupling overshoots -corrected in round 5: battlecruiser 2000→2200, dreadnought/carrier -22500→24000 (two-round persistent deficit vs destroyers). **Accepted, -not chased:** the stabilized battleship beats a pure frigate fleet by -~23% — the edge survived a stat cut, so it is mechanical (range), and -"the extreme smallest-ship fleet modestly loses to a range-fitted -capital" is desirable doctrine texture; the fair anti-capital answer -is the mixed fleet, whose arena is balanced. - -**Arena round 3 results (2026-07-04, narrow lanes):** arenas were -narrowed (height 10) so fleets can no longer pass each other — arena -geometry is part of the fixture from here on; margins are only -comparable within the same geometry. Results: mirrors healthy (1–7%); -dreadnought closed to −11%; battleship improved to +22% (still over); -drone swarm now beats cruisers by 14% (full engagement in the narrow -lane favors numbers); mixed vs battlecruisers +18%; glass beat armored -for the third consecutive round (+12%); repair 14%, two-to-one 78%, -station set cracked at 51% by the 3× swarm (geometry changed under the -stations — watch before touching stats). Round-4 knob changes: -armor_plates 1000→1200 (the persistent glass signal; also lifts the -armor-carrying cruiser/BC/destroyer/DN loadouts), battlecruiser hull -2500→2000 (net −300 EHP after its armor gain), battleship 7000→6300 -(no armor in its loadout, clean −10%), dreadnought/carrier -19000→22500 (the opposing destroyers gain armor too, so the DN needs -the larger step). - -**Arena round 2 results (2026-07-04, with EHP-margin logging):** the -mirror "bias" was noise — repeated runs split randomly, and winners -finish at 5–7% EHP (near-mutual annihilation, healthy). Drones vs -cruisers 10% and mixed vs battlecruisers 14% — near parity. The two -capital fights diverged: battleship beat frigates at +33% while the -dreadnought lost to destroyers at −37% — same guns, so the difference -is the destroyers carrying the buffed armor plus the battleship's -stabilizer putting all its guns at 75–150 m against 50 m opponents. -Glass vs armored narrowed to 11% (watch; if it persists, armor -1000→1200). Repair escort 11% — fair. Two-to-one decisive at 80%; -station assault cracked by the 3× swarm at 13% (stations strong but -breakable). Round-3 knob changes: stabilizer range ×1.5→×1.3, and -per-hull HP trims — battlecruiser 2700→2500, battleship 7500→7000, -dreadnought/carrier 15500→19000. - -### Pacing pass — first values - -The mapping between station levels and boss cycles: at the intended -push cadence (first set around cycle 2–3, roughly one per cycle from -mid on), the destroyed set's level ℓ is reached around cycle ℓ+2. So -level 2 ≈ the mid boundary, level 6 ≈ mid-late, levels 8–9 ≈ capitals -with enough cycles left to ramp voidsteel production before late. - -**Starting set** (`unlock_at_station_level = -1`), per the starting-set -rule (exactly enough to reach the first push): drone, frigate, -railgun_s, salvager. Note: the building_block recipe must carry an -explicit `-1` — building blocks appear in no schematic's materials, so -implicit unlocking can never reach it (fixed in this pass; it was -silently locked). - -**Unlock ladder** (level → unlocks; ← marks `unlock_requires`): - -| level | ships | modules | recipes | -|---|---|---|---| -| 0 | destroyer | repair_tool, armor_plates | | -| 1 | | maneuvering_thrusters, sensor_booster | shortcut_steel_plate | -| 2 | cruiser | railgun_m, afterburner | shortcut_control_chip, shortcut_hardened_steel | -| 3 | | weapon_stabilizer | | -| 4 | battlecruiser ← cruiser | weapon_primer, weapon_upgrade | | -| 5 | | drone_bay | | -| 6 | battleship ← battlecruiser | railgun_l ← railgun_m | | -| 8 | dreadnought ← battleship | | | -| 9 | carrier ← battleship | drone_hangar | | - -Level 0's pool has exactly three entries, so the first push offers a -full dialog with no artifacts competing. Level 2 is the quartz gate: -cruiser and railgun_m are the first schematics whose chains reach -quartz (the shortcut recipes' outputs only become implicitly unlocked -alongside them, so they cannot drop early). - -**Threat rate** `2*x + 0.15*x*x`, tuned against the factory-size curve -with roughly half the player's output assumed military: - -| cycle x | rate (threat/s) | player military (≈ curve/2) | -|---|---|---| -| 2 | 4.6 | ~12 | -| 6 | 17.4 | ~30 | -| 15 | 63.8 | ~60 | -| 20 | 100 | ~75 | -| 24 | 134 | — | - -Below the player early, crossing at the late boundary, overwhelming by -~24 — the endless-race shape from the progression rules. Typical wave -at x=6 (30 s gap): ~520 threat ≈ two cruisers plus escorts; boss at -x=15: ~3800 threat ≈ two and a half dreadnoughts. - -**Economy**: `starting_building_blocks` 1000 → 200 (minimal block loop -+ first shipyard ≈ 150, plus beginner slack); expansion cost 200 → 400 -flat (placeholder until the escalating formula, action item 4); -`artifact_win_count` 3 → 5 with chance `0.05*x`: ~15–18 pushes in a -winning run offer ~7 artifact options cumulatively, so winning takes -choosing the artifact over a schematic five times — a real decision -each time, and the win lands in the cycle 20–24 target window. - -## Balancing targets (first pass, July 2026) - -The six root numbers for the balancing pass. Every derived value (threat -rate, recipe quantities, block costs, scrap rates, unlock ladder) is tuned -to hit these; when rebalancing later, change these first and re-derive, -never the other way around. The rules they follow live in -`docs/progression_design.md`. - -All targets are in **game time**. The player can pause and accelerate, so -real session length differs; playtests should measure both. The time unit -is the boss cycle (`world.toml boss_countdown_seconds`, 300 s). Destroying -a station set advances the boss countdown by `boss_advance_seconds` -(60 s), so cycles run shorter than nominal when pushing actively — these -targets deliberately ignore that; playtesting will show real run lengths. - -1. **Run length** — a winning run takes up to 2 hours of game time: win - around boss cycle 20–24. Losing runs end earlier. -2. **Phase boundaries** — early = cycles 1–5 (iron/copper, small hulls), - mid = cycles 6–14 (quartz, medium hulls), late = cycles 15+ - (voidsteel, capitals). Push cadence roughly one station set per - cycle from mid onward. This fixes the `unlock_at_station_level` - ladder and, with the artifact win count, the `artifact_chance_formula` - pacing. -3. **Factory size curve** — producing buildings over time; when - saturated, output threat/s equals this count, so this curve IS the - player power curve. Targets: ~25 when the starting asteroid is full - (end of cycle 2), ~60 at the start of mid (cycle 6), ~120 at the - start of late (cycle 15), ~150 near the win. `threat_rate_formula` - must remain a fraction of this curve, and buildings plus belts must - physically fit the asteroid plus affordable expansions. -4. **Threat-cost ladder** — total production-seconds per *fitted* hull - (including a typical module loadout): drone ~10, frigate ~40, - destroyer ~80, cruiser ~200, battlecruiser ~350, battleship ~700, - dreadnought/carrier ~1500. Every production chain must sum to its - ladder value. Cross-check against (3): fitted-ship cadence = - ladder value / (factory threat/s devoted to military) — e.g. a mid - factory spending half of 60 threat/s on ships fields a fitted - cruiser roughly every 7 s. -5. **Fleet size** — swarm-leaning: ~25 player combat ships as the - standing mid-game fleet. Standing fleet = build cadence (4) × average - ship lifetime, so this target drives time-to-kill and therefore the - combat stat magnitudes tuned in the arena pass. -6. **Block economy roots** — bootstrap complete (starting asteroid full) - by the end of cycle 2; a factory spending ~30% of its capacity on - blocks doubles in ~4 minutes early game; one expansion affordable per - cycle at ~1/3 of block income, with escalating costs that eventually - outrun any income (see the growth curve rules in - `progression_design.md`). - -## Deliberate placeholders / open questions for later passes - -- All new hulls have `threat.cost_formula = "0"` so enemy waves do not spawn - them yet (WaveSystem treats any ship with positive threat cost as wave- - eligible, regardless of unlock level). The balancing pass should set real - threat costs together with `default_modules` loadouts so waves spawn them - armed. -- All new hulls and all assembler recipes are `unlock_at_station_level = -1` - (available from the start) to make testing easy; the balancing pass should - stagger these so mid/lategame recipes drop as schematics from enemy defence - stations. -- Recipe quantities and durations are a first guess, deliberately roughly - tiered (capital hulls ~60 s, drones 4 s); the balancing pass tunes them. -- `drone_bay` and `drone_hangar` are footprint-only placeholders: the drone - launching capability does not exist in the simulation yet, so they define - no capability section. -- Renames in this pass: `laser_cannon_xs` → `laser_cannon_s` (the old 2x2 - `laser_cannon_s` became `laser_cannon_m`), `armor_plate` → `armor_plates`, - `manuvering_thrusters` → `maneuvering_thrusters` (typo fix). Test data - under `bin/test/data/config` intentionally still uses the old ids — it is - an independent fixture set. +It verifies every consumed item has a producer, every item has a visuals +entry, flags orphaned items, and prints which items are +reprocessing-only (currently exactly voidsteel).