Restructure balancing docs into docs/balancing/

Split the cluttered progression_design.md and content_design.md into
separated documents by role:

- docs/balancing/rules.md    - design rules and principles (moved from
                               progression_design.md, which is removed)
- docs/balancing/targets.md  - the chosen base numbers: run shape,
                               factory curve, threat ladder (achieved
                               values adopted), combat anchors, pacing
                               anchors
- docs/balancing/derived.md  - current tuned state of all derived
                               numbers, mirroring the configs
- docs/balancing/process.md  - pass order, tuning discipline learned in
                               arena rounds 1-5, tools, next-round
                               checklist
- docs/balancing/history.md  - chronological record: decisions, numbers
                               pass, calculator bugs, arena rounds,
                               pacing pass
- docs/balancing/README.md   - index, status, open action items (moved
                               from progression_design.md)

content_design.md slims back down to actual content: footprint gating,
hull grids, gating matrix (module names updated to railguns), and the
production tree structure with its fiction - numbers, anchors, arena
logs, and pacing all moved to docs/balancing/. Stale references in the
config comments updated to the new locations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyCu8vwChKMbLJQ3xosYEN
This commit is contained in:
2026-07-06 17:12:25 +02:00
parent dcc6af123f
commit b4fa3e6dff
10 changed files with 604 additions and 543 deletions

56
docs/balancing/README.md Normal file
View File

@@ -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.

164
docs/balancing/derived.md Normal file
View File

@@ -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 15)
**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 23 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.

96
docs/balancing/history.md Normal file
View File

@@ -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 15/614/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 96124% 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 15
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 89 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, 23 cycles apart late.
- **First full balancing round complete.** Next: full-game playtests
against the run-shape targets.

98
docs/balancing/process.md Normal file
View File

@@ -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 15)
- **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).

View File

@@ -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`.

104
docs/balancing/targets.md Normal file
View File

@@ -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 2024. Losing runs end earlier.
2. **Phase boundaries** — early = cycles 15 (iron/copper, small hulls),
mid = cycles 614 (quartz, medium hulls), late = cycles 15+
(voidsteel, capitals). Push cadence: first station set around cycle
23, 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 ~1020% 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 23
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 3060 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 (~1525%) 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 ~1518 pushes (~7 cumulative artifact offers at
the current chance formula), the player must choose the artifact over
a schematic about five times.

View File

@@ -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 `<ship>_hull`; module items `<module>_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** (`<ship>_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 ~2030).
- *Small-end deviation:* frigatecruiser land 624% 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 3060 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
(4050% of the winner survives) but team 1 won all three mirrors —
possible sim-side bias, under investigation (progression_design.md
action items 67). 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 2272 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 510%, 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 03%; 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 (17%);
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 57% 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 75150 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 23, 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 89 ≈ 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`: ~1518 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 2024 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 2024. Losing runs end earlier.
2. **Phase boundaries** — early = cycles 15 (iron/copper, small hulls),
mid = cycles 614 (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).