Compare commits
6 Commits
9ddd622870
...
progressio
| Author | SHA1 | Date | |
|---|---|---|---|
| a93f7c3897 | |||
| f8028a6ca2 | |||
| b4fa3e6dff | |||
| dcc6af123f | |||
| f62b7bb78e | |||
| 27c3a7b65b |
@@ -47,7 +47,7 @@ attack_rate_hz = 2.0
|
||||
|
||||
[[module]]
|
||||
id = "railgun_m"
|
||||
unlock_at_station_level = 1
|
||||
unlock_at_station_level = 2
|
||||
surface_mask = [
|
||||
"OO",
|
||||
"OO"]
|
||||
@@ -56,15 +56,18 @@ production_time_seconds = 3
|
||||
fill_color = "#FF8040"
|
||||
glyph = "Rm"
|
||||
|
||||
# damage 14 keeps a 60 HP drone at 5 hits — 15+ crosses a breakpoint that
|
||||
# silently adds ~25% effective DPS vs drones (docs/balancing/history.md,
|
||||
# round 6).
|
||||
[module.weapon]
|
||||
damage = 14
|
||||
attack_range_m = 70
|
||||
attack_range_m = 80
|
||||
attack_rate_hz = 1.5
|
||||
|
||||
|
||||
[[module]]
|
||||
id = "railgun_l"
|
||||
unlock_at_station_level = 1
|
||||
unlock_at_station_level = 6
|
||||
unlock_requires = ["railgun_m"]
|
||||
surface_mask = [
|
||||
"OOO",
|
||||
@@ -75,9 +78,12 @@ production_time_seconds = 4
|
||||
fill_color = "#FF8040"
|
||||
glyph = "Rl"
|
||||
|
||||
# attack_range_m 130 deliberately exceeds the station range of 120
|
||||
# (stations.toml) — the l gun is the only weapon that can besiege stations
|
||||
# without tanking their fire (docs/balancing/history.md, playtest 1).
|
||||
[module.weapon]
|
||||
damage = 52
|
||||
attack_range_m = 100
|
||||
attack_range_m = 130
|
||||
attack_rate_hz = 0.8
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -101,7 +107,7 @@ collection_rate_hz = 0.5
|
||||
|
||||
[[module]]
|
||||
id = "repair_tool"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 0
|
||||
surface_mask = ["O"]
|
||||
materials = [{item = "repair_tool_module", amount = 1}]
|
||||
production_time_seconds = 1
|
||||
@@ -110,7 +116,7 @@ glyph = "Rp"
|
||||
|
||||
[module.repair]
|
||||
repair_rate_hz = 1
|
||||
repair_amount_hp = 12
|
||||
repair_amount_hp = 4
|
||||
repair_range_m = 80
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -119,7 +125,7 @@ repair_range_m = 80
|
||||
|
||||
[[module]]
|
||||
id = "afterburner"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 2
|
||||
surface_mask = ["OOO"]
|
||||
materials = [{item = "afterburner_module", amount = 1}]
|
||||
production_time_seconds = 1
|
||||
@@ -133,7 +139,7 @@ added_main_acceleration_mpss = 60
|
||||
|
||||
[[module]]
|
||||
id = "maneuvering_thrusters"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 1
|
||||
surface_mask = ["OO"]
|
||||
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
||||
production_time_seconds = 1
|
||||
@@ -150,7 +156,7 @@ added_maneuvering_acceleration_mpss = 10
|
||||
|
||||
[[module]]
|
||||
id = "armor_plates"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 0
|
||||
surface_mask = ["OO"]
|
||||
materials = [{item = "armor_plates_module", amount = 1}]
|
||||
production_time_seconds = 1
|
||||
@@ -163,7 +169,7 @@ added_hp = 1200
|
||||
|
||||
[[module]]
|
||||
id = "sensor_booster"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 1
|
||||
surface_mask = ["OO"]
|
||||
materials = [{item = "sensor_booster_module", amount = 1}]
|
||||
production_time_seconds = 1
|
||||
@@ -179,7 +185,7 @@ added_sensor_range_m = 50
|
||||
|
||||
[[module]]
|
||||
id = "weapon_upgrade"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 4
|
||||
surface_mask = [
|
||||
"OO",
|
||||
"OX",
|
||||
@@ -195,7 +201,7 @@ multiplied_damage = 1.2
|
||||
|
||||
[[module]]
|
||||
id = "weapon_primer"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 4
|
||||
surface_mask = [
|
||||
"OO",
|
||||
"OX",
|
||||
@@ -211,7 +217,7 @@ multiplied_attack_rate_hz = 1.2
|
||||
|
||||
[[module]]
|
||||
id = "weapon_stabilizer"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 3
|
||||
surface_mask = [
|
||||
"OO",
|
||||
"OX",
|
||||
@@ -234,7 +240,7 @@ multiplied_attack_rate_hz = 0.8
|
||||
|
||||
[[module]]
|
||||
id = "drone_bay"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 5
|
||||
surface_mask = [
|
||||
"OO",
|
||||
"OO"]
|
||||
@@ -246,7 +252,7 @@ glyph = "Db"
|
||||
|
||||
[[module]]
|
||||
id = "drone_hangar"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 9
|
||||
surface_mask = [
|
||||
"OOOOOO",
|
||||
"OOOOOO"]
|
||||
|
||||
@@ -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,9 +141,12 @@ 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]]
|
||||
id = "building_block"
|
||||
unlock_at_station_level = -1
|
||||
building = "assembler"
|
||||
inputs = [{item = "steel_plate", amount = 2}]
|
||||
outputs = [{item = "building_block", amount = 4}]
|
||||
|
||||
@@ -84,7 +84,7 @@ sensor_range_m = 200
|
||||
# mount medium hardware.
|
||||
[[ship]]
|
||||
id = "destroyer"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 0
|
||||
layout = [
|
||||
"OXOXO",
|
||||
"OOOOO",
|
||||
@@ -120,7 +120,7 @@ sensor_range_m = 220
|
||||
# supports; no 3x3 area exists for an l gun.
|
||||
[[ship]]
|
||||
id = "cruiser"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 2
|
||||
layout = [
|
||||
"XOOX",
|
||||
"OOOO",
|
||||
@@ -158,7 +158,8 @@ sensor_range_m = 250
|
||||
# stern leave no 3x3 area for an l gun and no 2x6 area for a drone hangar.
|
||||
[[ship]]
|
||||
id = "battlecruiser"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 4
|
||||
unlock_requires = ["cruiser"]
|
||||
layout = [
|
||||
"OOXXOO",
|
||||
"OOOOOO",
|
||||
@@ -179,7 +180,7 @@ materials = [{item = "battlecruiser_hull", amount = 1}]
|
||||
production_time_seconds = 5
|
||||
|
||||
[ship.health]
|
||||
hp = 2200
|
||||
hp = 2400
|
||||
|
||||
[ship.movement]
|
||||
speed_mps = 20
|
||||
@@ -200,7 +201,8 @@ sensor_range_m = 260
|
||||
# so no 2x6 drone hangar fits.
|
||||
[[ship]]
|
||||
id = "battleship"
|
||||
unlock_at_station_level = 1
|
||||
unlock_at_station_level = 6
|
||||
unlock_requires = ["battlecruiser"]
|
||||
layout = [
|
||||
"XOOOOX",
|
||||
"OOOOOO",
|
||||
@@ -242,7 +244,7 @@ sensor_range_m = 280
|
||||
# stay the only hangar hull. Bow and stern strips hold supports.
|
||||
[[ship]]
|
||||
id = "dreadnought"
|
||||
unlock_at_station_level = 2
|
||||
unlock_at_station_level = 8
|
||||
unlock_requires = ["battleship"]
|
||||
layout = [
|
||||
"XXXOOOOOXXX",
|
||||
@@ -286,7 +288,8 @@ sensor_range_m = 300
|
||||
# the lower decks hold supports and 2x2 point-defense m guns.
|
||||
[[ship]]
|
||||
id = "carrier"
|
||||
unlock_at_station_level = -1
|
||||
unlock_at_station_level = 9
|
||||
unlock_requires = ["battleship"]
|
||||
layout = [
|
||||
"XOOOOOOOOX",
|
||||
"OOOOOOOOOO",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[world]
|
||||
height_tiles = 40
|
||||
refund_percentage = 100
|
||||
starting_building_blocks = 1000
|
||||
starting_building_blocks = 200
|
||||
scrap_despawn_seconds = 120
|
||||
scrap_per_threat = 0.25
|
||||
tile_size_m = 10
|
||||
@@ -19,7 +19,9 @@ enemy_buffer_width_tiles = 20
|
||||
|
||||
[expansion]
|
||||
columns_per_expansion_tiles = 10
|
||||
cost_building_blocks = 200
|
||||
# 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]
|
||||
push_expand_columns_tiles = 10
|
||||
@@ -32,10 +34,13 @@ target_hysteresis = 0.40 # keep current target unless
|
||||
|
||||
[artifacts]
|
||||
artifact_chance_formula = "0.05 * x" # 5% chance per station level
|
||||
artifact_win_count = 3
|
||||
artifact_win_count = 5
|
||||
|
||||
[waves]
|
||||
threat_rate_formula = "x"
|
||||
# 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"
|
||||
gap_min_seconds = 15
|
||||
gap_max_seconds = 45
|
||||
spawn_duration_seconds = 10
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
# using the verified fitted threat values from tools/threat_report.py:
|
||||
# drone 10.5, frigate 47, destroyer 99, cruiser 233.5,
|
||||
# battlecruiser 354.5, battleship 722.5, dreadnought 1491.5,
|
||||
# carrier 1436.5, glass destroyer (8 small guns) 92, repair drone 17.
|
||||
# carrier 1436.5, glass destroyer (8 small guns) 92, repair drone 17,
|
||||
# railgun_s-spam cruiser (12 small guns) 178.
|
||||
# Module arrays mirror the ships' default_modules loadouts unless a
|
||||
# doctrine variant is the point of the arena.
|
||||
#
|
||||
@@ -348,6 +349,50 @@ enemy_buffer_width_tiles = 10
|
||||
{type = "railgun_s", x = 4, y = 1, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena]]
|
||||
name = "Railgun_s-spam cruisers vs default cruisers (1424 vs 1401)"
|
||||
# Tracks the playtest-1 meta: cruiser hulls filled with 12 small guns
|
||||
# (max DPS/threat, no armor, range 50) against the default m-gun fit.
|
||||
# The concentration tax means the spam side SHOULD win a brawl somewhat;
|
||||
# this arena bounds its margin — a blowout here means the small-gun
|
||||
# premium or the m-gun range edge needs retuning.
|
||||
height_tiles = 10
|
||||
player_buffer_width_tiles = 10
|
||||
contest_zone_width_tiles = 50
|
||||
enemy_buffer_width_tiles = 10
|
||||
|
||||
[[arena.team]]
|
||||
name = "Spam"
|
||||
[[arena.team.ship]]
|
||||
schematic = "cruiser"
|
||||
count = 8
|
||||
modules = [
|
||||
{type = "railgun_s", x = 1, y = 0, rotation = "east"},
|
||||
{type = "railgun_s", x = 2, y = 0, rotation = "east"},
|
||||
{type = "railgun_s", x = 0, y = 1, rotation = "east"},
|
||||
{type = "railgun_s", x = 1, y = 1, rotation = "east"},
|
||||
{type = "railgun_s", x = 2, y = 1, rotation = "east"},
|
||||
{type = "railgun_s", x = 3, y = 1, rotation = "east"},
|
||||
{type = "railgun_s", x = 0, y = 2, rotation = "east"},
|
||||
{type = "railgun_s", x = 1, y = 2, rotation = "east"},
|
||||
{type = "railgun_s", x = 2, y = 2, rotation = "east"},
|
||||
{type = "railgun_s", x = 3, y = 2, rotation = "east"},
|
||||
{type = "railgun_s", x = 1, y = 3, rotation = "east"},
|
||||
{type = "railgun_s", x = 2, y = 3, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena.team]]
|
||||
name = "Default"
|
||||
[[arena.team.ship]]
|
||||
schematic = "cruiser"
|
||||
count = 6
|
||||
modules = [
|
||||
{type = "railgun_m", x = 0, y = 1, rotation = "east"},
|
||||
{type = "railgun_m", x = 2, y = 1, rotation = "east"},
|
||||
{type = "armor_plates", x = 1, y = 0, rotation = "east"},
|
||||
{type = "maneuvering_thrusters", x = 1, y = 3, rotation = "east"},
|
||||
]
|
||||
|
||||
[[arena]]
|
||||
name = "Repair escort vs raw numbers (444 vs 444)"
|
||||
height_tiles = 10
|
||||
|
||||
@@ -19,7 +19,7 @@ enemy_buffer_width_tiles = 15
|
||||
|
||||
[expansion]
|
||||
columns_per_expansion_tiles = 10
|
||||
cost_building_blocks = 200
|
||||
cost_building_blocks_formula = "400 * 2^x"
|
||||
|
||||
[push]
|
||||
push_expand_columns_tiles = 20
|
||||
|
||||
60
docs/balancing/README.md
Normal file
60
docs/balancing/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 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. Playtesting in progress: playtest 1 (two full ~40-min wins,
|
||||
cruisers only) found the railgun_s-spam-cruiser meta, confirmed repair
|
||||
as overpowered, and exposed a 2.5–3× run-length gap; combat stats
|
||||
adjusted and re-checked in arena round 6 (see `history.md`). Next:
|
||||
playtest 2 with the new stats — pacing knobs (station scaling, threat
|
||||
rate, win pacing) wait for its result.
|
||||
|
||||
## 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.
|
||||
171
docs/balancing/derived.md
Normal file
171
docs/balancing/derived.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# 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, rounds 1–5; playtest-1 adjustments 2026-07-06 —
|
||||
see `history.md`)
|
||||
|
||||
**Weapons:** railgun_s 2 dmg × 2.0 Hz (4.0 DPS), range 50 m;
|
||||
railgun_m 14 × 1.5 (21), range 80; railgun_l 52 × 0.8 (41.6), range 130.
|
||||
DPS per threat: s 0.62, m 0.48, l 0.41 — the concentration tax stands;
|
||||
reach is the bigger guns' compensation (ranges raised after playtest 1,
|
||||
which alone priced out the small-gun-spam meta). railgun_l deliberately
|
||||
outranges stations (120 m) to buy the siege role. railgun_m damage is
|
||||
breakpoint-sensitive: 15+ drops a 60 HP drone from 5 hits to 4.
|
||||
|
||||
**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 4 HP × 1 Hz,
|
||||
range 80 (halved after playtest 1 — free between-wave top-offs were never
|
||||
priced by the arena escort test); 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.
|
||||
161
docs/balancing/history.md
Normal file
161
docs/balancing/history.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 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.
|
||||
|
||||
## 2026-07-06 — playtest 1 (two full playthroughs)
|
||||
|
||||
Two complete runs, WON in ~40 min each with cruiser fleets only — never
|
||||
needing capitals. (Initially misread as "two pushes in 40 min, pacing on
|
||||
target"; corrected in round 6.) That is roughly cycle 8 against the
|
||||
win-cycle target of 20–24: a 2.5–3× pacing gap. Pacing knobs deliberately
|
||||
untouched this round — the runs rode 9 HP/s repair and stations nothing
|
||||
outranged, both nerfed below; playtest 2 measures the remaining gap.
|
||||
|
||||
- **Meta finding: cruisers filled with 12× railgun_s dominate.** Predicted
|
||||
by the numbers in hindsight: the concentration tax makes railgun_s the
|
||||
best DPS/threat (0.62 vs m 0.48, l 0.41), range is the big guns' only
|
||||
mechanical edge (armor is added HP, not damage reduction — no anti-swarm
|
||||
mechanic), repair sustain covers the closing distance, and stations
|
||||
outranged every ship gun (120 vs railgun_l's 100), so even capitals had
|
||||
to tank-and-brawl. The cruiser compounds it: first hull with a large
|
||||
1×1 canvas (12 cells) and a nearly quartz-free chain.
|
||||
- **Repair tool confirmed overpowered** (second signal after the
|
||||
persistent +24% arena escort margin): the arena only prices in-fight
|
||||
sustain; real runs add free full top-offs in every 15–45 s wave gap
|
||||
across the whole swarm. The 0.7 HP/s-per-threat prior is wrong for
|
||||
wave defence.
|
||||
- **Changes:** repair_tool 9→4 HP/s; railgun_l range 100→130 (now
|
||||
outranges stations — buys the siege role the capital ladder promises);
|
||||
railgun_m 14→16 dmg and range 70→80 (tax softened: m sits at 0.55
|
||||
DPS/threat, between s and l). Module threats unchanged (costs
|
||||
untouched), so no ladder recalculation needed.
|
||||
- New tracked arena added: railgun_s-spam cruisers (8× 178) vs default
|
||||
cruisers (6× 233.5) — the spam side should win a brawl somewhat, but a
|
||||
blowout means the small-gun premium needs retuning.
|
||||
- **Open:** re-run the arena suite to check the range/damage changes
|
||||
against the round 1–5 results; next playtest should verify big guns now
|
||||
feel worth climbing to and repair is merely good.
|
||||
|
||||
## 2026-07-06 — arena round 6 (checking the playtest-1 adjustments)
|
||||
|
||||
Mirrors healthy (5–8% margins, durations 24/66/91 s). Results:
|
||||
|
||||
- **Spam-cruiser arena: default cruisers +9% — the meta is priced out**,
|
||||
and the range buff alone did the work.
|
||||
- **Regression: drone swarm vs cruisers +47% for cruisers** (was +14%
|
||||
swarm in round 3). Besides the wider range gap, the damage buff crossed
|
||||
a breakpoint: 14 dmg kills a 60 HP drone in 5 hits, 16 in 4 — a hidden
|
||||
~25% effective-DPS gain vs drones. Change: **railgun_m damage 16→14**
|
||||
(range stays 80); the tax stands, reach is the compensation.
|
||||
- Battleship +30% and dreadnought +29% vs pure railgun_s fleets:
|
||||
**accepted as reach-doctrine texture** (BS was already accepted at
|
||||
+23%) — the l gun's 130 m standoff is exactly what the range buff
|
||||
bought; the counter is your own reach or 2:1 numbers, not equal-threat
|
||||
small guns. Watch, don't tune.
|
||||
- Repair escort flipped to raw +16%: **kept at 4 HP/s deliberately** —
|
||||
the arena cannot price the free between-wave top-offs, so slightly
|
||||
below par in-fight is the correct price for a module whose run-value
|
||||
includes them. Playtest 2 decides; 6 is the fallback if repair feels
|
||||
dead.
|
||||
- Station assault: the 3× swarm cracked the fortified position keeping
|
||||
45% EHP. No knob this round touched it; together with playtest 1's
|
||||
trivially easy pushes it flags **station strength as the first pacing
|
||||
lever** for the next pass.
|
||||
|
||||
**Pacing deferred:** playtest 1's 40-min wins predate the repair nerf
|
||||
and the l-gun siege range. If playtest 2 still wins by ~cycle 10, the
|
||||
levers are enemy station scaling (`3000 + 1500*x` likely too shallow),
|
||||
the threat rate, and possibly `artifact_win_count`.
|
||||
98
docs/balancing/process.md
Normal file
98
docs/balancing/process.md
Normal 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 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).
|
||||
@@ -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,45 +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`.
|
||||
6. **Verify the balancing arena end condition.** Arena round 1's
|
||||
station-assault log showed the arena "(running)" with every ship on
|
||||
both sides destroyed — per REQ-BAL-SIM-END the station-less team
|
||||
should have lost the moment its last ship died. Round 2 ended
|
||||
correctly, so it may only be a log-snapshot artifact; confirm, and
|
||||
fix the end condition if it can genuinely idle in that state.
|
||||
104
docs/balancing/targets.md
Normal file
104
docs/balancing/targets.md
Normal 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 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.
|
||||
@@ -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,321 +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-4 trims: drone 60, frigate 300, destroyer 550,
|
||||
cruiser 1500, battlecruiser 2200, 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 ≈ 1 HP/s per threat:** repair_tool heals 12 HP × 1 Hz at 13
|
||||
threat (arena round 1: at 2 HP/s-per-threat the escorted team won
|
||||
flawlessly — in-combat sustain effectively removes enemy DPS and must
|
||||
be priced like DPS, not like HP). 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 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.
|
||||
|
||||
## 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).
|
||||
|
||||
@@ -367,7 +367,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
## Asteroid Expansion
|
||||
|
||||
- REQ-EXP-UNLOCK: The player can unlock additional asteroid tile columns to the left of the existing asteroid by spending building blocks from the global stock.
|
||||
- REQ-EXP-COST: Each expansion adds `world.toml [expansion].columns_per_expansion` columns and costs `[expansion].cost_building_blocks` building blocks.
|
||||
- REQ-EXP-COST: Each expansion adds `world.toml [expansion].columns_per_expansion` columns. The building block cost of an expansion is defined by the formula `world.toml [expansion].cost_building_blocks_formula`, where `x` is the number of expansions already purchased (0 for the first expansion, incrementing by 1 for each subsequent expansion). The formula is evaluated at purchase time and its result is floored to an integer number of building blocks.
|
||||
|
||||
## UI
|
||||
|
||||
@@ -392,9 +392,10 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
(75% width) (25% width)
|
||||
```
|
||||
|
||||
- REQ-UI-HEADER: The header bar spans the width of the game world column (75% of the screen width) and always shows the elapsed survival time, the current global building blocks stock, and the artifact count (REQ-WIN-ARTIFACT-COUNT) displayed as `Artifacts: x/y` (where `x` is the current artifact count and `y` is `world.toml [world].artifact_win_count`) on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) to the left of the speed buttons, and game speed controls on the right.
|
||||
- REQ-UI-HEADER: The header bar spans the width of the game world column (75% of the screen width) and always shows the elapsed survival time, the current global building blocks stock, and the artifact count (REQ-WIN-ARTIFACT-COUNT) displayed as `Artifacts: x/y` (where `x` is the current artifact count and `y` is `world.toml [world].artifact_win_count`) on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) and an asteroid expansion button (REQ-UI-EXPAND-BUTTON) to the left of the speed buttons, and game speed controls on the right.
|
||||
- REQ-UI-BOSS-STATUS: The header bar displays, to the left of the speed buttons, the current boss wave counter (REQ-WAV-BOSS-COUNTER) and the time remaining on the boss countdown (REQ-WAV-BOSS-COUNTDOWN). The boss wave counter is shown as `Boss Wave #<x>` and the countdown as `Next boss: <M:SS>`, where `<M:SS>` is the remaining seconds formatted as whole minutes and two-digit seconds. Both values update continuously as the simulation runs.
|
||||
- REQ-UI-SPEED: The game speed controls in the header bar are buttons for 0×, 0.5×, 1×, 2×, and 4× speed. The currently active speed is shown as selected. All game simulation (production, movement, threat accumulation, wave timing) scales with the selected speed. 0× pauses the game.
|
||||
- REQ-UI-EXPAND-BUTTON: The header bar shows an asteroid expansion button captioned `Expand: <x> Blocks`, where `<x>` is the current expansion cost computed from `world.toml [expansion].cost_building_blocks_formula` at the current number of purchased expansions (REQ-EXP-COST). Clicking the button unlocks the next asteroid expansion (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND), spending that many building blocks from the global stock. The button is disabled when the player cannot currently afford the cost (consistent with REQ-UI-BUILD-DISABLED). The caption updates as the cost changes with each purchased expansion.
|
||||
- REQ-UI-WORLD-SIZE: The game world view occupies the full height below the header bar in the main column (75% of the screen width).
|
||||
- REQ-UI-PANEL-COLUMN: The side panel column occupies 25% of the screen width and the full screen height. It is divided into three equal-height panels stacked top to bottom: selected building panel (top), build button grid (middle), and blueprint panel (bottom).
|
||||
|
||||
|
||||
@@ -278,8 +278,8 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
||||
cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
|
||||
cfg.regions.enemyBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles"));
|
||||
|
||||
cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles"));
|
||||
cfg.expansion.costBuildingBlocks = static_cast<int>(requireInt(tbl["expansion"]["cost_building_blocks"], file, "expansion.cost_building_blocks"));
|
||||
cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles"));
|
||||
cfg.expansion.costBuildingBlocksFormula = requireFormula(tbl["expansion"]["cost_building_blocks_formula"], file, "expansion.cost_building_blocks_formula");
|
||||
|
||||
cfg.push.pushExpandColumns_tiles = static_cast<int>(requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles"));
|
||||
cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
|
||||
|
||||
@@ -14,8 +14,8 @@ struct WorldRegions
|
||||
// Asteroid expansion (REQ-EXP-UNLOCK, REQ-EXP-COST).
|
||||
struct WorldExpansion
|
||||
{
|
||||
int columnsPerExpansion_tiles;
|
||||
int costBuildingBlocks;
|
||||
int columnsPerExpansion_tiles;
|
||||
Formula costBuildingBlocksFormula; // cost in building blocks; x = expansions already purchased
|
||||
};
|
||||
|
||||
// Push effects (REQ-PSH-*, REQ-WAV-BOSS-ADVANCE).
|
||||
|
||||
@@ -3,6 +3,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TracePrintRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TickAdvancedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBlocksChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ExpansionCostChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
||||
|
||||
17
src/lib/eventsystem/event/ExpansionCostChangedEvent.h
Normal file
17
src/lib/eventsystem/event/ExpansionCostChangedEvent.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef EXPANSION_COST_CHANGED_EVENT_H
|
||||
#define EXPANSION_COST_CHANGED_EVENT_H
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
// Fired when the current asteroid-expansion cost changes (REQ-EXP-COST): once at
|
||||
// startup and again after each expansion is purchased. Carries the cost in
|
||||
// building blocks so the header Expand button can update its caption/enabled
|
||||
// state (REQ-UI-EXPAND-BUTTON).
|
||||
class ExpansionCostChangedEvent : public Event
|
||||
{
|
||||
public:
|
||||
explicit ExpansionCostChangedEvent(int cost) : cost(cost) {}
|
||||
const int cost;
|
||||
};
|
||||
|
||||
#endif // EXPANSION_COST_CHANGED_EVENT_H
|
||||
@@ -24,6 +24,7 @@ BuildingSystem::BuildingSystem(const GameConfig& config,
|
||||
, m_spawnShip(std::move(spawnShip))
|
||||
, m_isItemUnlocked(std::move(isItemUnlocked))
|
||||
, m_rng(rng)
|
||||
, m_asteroidWidth_tiles(config.world.regions.asteroidWidth_tiles)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -281,7 +282,7 @@ bool BuildingSystem::bodyCellsWithinWorldBounds(const std::vector<QPoint>& bodyC
|
||||
QPoint anchor) const
|
||||
{
|
||||
const int heightTiles = m_config.world.heightTiles;
|
||||
const int leftEdgeX = -m_config.world.regions.asteroidWidth_tiles;
|
||||
const int leftEdgeX = -m_asteroidWidth_tiles;
|
||||
for (const QPoint& cell : bodyCells)
|
||||
{
|
||||
const QPoint worldCell = anchor + cell;
|
||||
|
||||
@@ -59,6 +59,11 @@ public:
|
||||
bool isPlacementValid(BuildingType type, QPoint anchor,
|
||||
Rotation rotation) const;
|
||||
|
||||
// Sets the current buildable asteroid width in tiles. Grows the left
|
||||
// placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
|
||||
// Defaults to world.regions.asteroid_width_tiles at construction.
|
||||
void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; }
|
||||
|
||||
// Remove a building or construction site by id. Returns the refund in
|
||||
// building blocks (floor(cost * refundPercentage / 100)). Returns 0 for
|
||||
// unknown ids.
|
||||
@@ -179,6 +184,7 @@ private:
|
||||
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
||||
std::function<bool(const std::string&)> m_isItemUnlocked;
|
||||
std::mt19937& m_rng;
|
||||
int m_asteroidWidth_tiles;
|
||||
|
||||
std::vector<Building> m_buildings;
|
||||
std::deque<ConstructionSite> m_constructionQueue;
|
||||
|
||||
@@ -35,6 +35,7 @@ enum class CommandKind
|
||||
SetSplitterFilters,
|
||||
ClearBeltTiles,
|
||||
ApplySchematicChoice,
|
||||
ExpandAsteroid,
|
||||
Reset
|
||||
};
|
||||
|
||||
@@ -129,6 +130,14 @@ struct ApplySchematicChoiceCommand : Command
|
||||
int choiceIndex = 0;
|
||||
};
|
||||
|
||||
// Unlocks the next asteroid expansion (REQ-EXP-UNLOCK). Carries no payload: the
|
||||
// simulation derives the cost and column count from its own expansion counter
|
||||
// and config, so a recorded command replays identically.
|
||||
struct ExpandAsteroidCommand : Command
|
||||
{
|
||||
ExpandAsteroidCommand() : Command(CommandKind::ExpandAsteroid) {}
|
||||
};
|
||||
|
||||
// Restart boundary: reinitializes the simulation with a fresh seed and, if
|
||||
// config is set, a reloaded config (GameConfig is move-only, so it is carried by
|
||||
// shared_ptr and moved into the sim on apply). A null config keeps the current
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
|
||||
#include "AiSystem.h"
|
||||
#include "Command.h"
|
||||
@@ -132,6 +133,7 @@ void Simulation::reset(unsigned int seed)
|
||||
m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds);
|
||||
m_nextBuildingId = 1;
|
||||
m_buildingBlocksStock = m_config.world.startingBuildingBlocks;
|
||||
m_expansionsPurchased = 0;
|
||||
m_gameOver = false;
|
||||
m_isWon = false;
|
||||
m_artifactCount = 0;
|
||||
@@ -275,6 +277,9 @@ void Simulation::apply(const Command& command)
|
||||
case CommandKind::ApplySchematicChoice:
|
||||
applySchematicChoice(static_cast<const ApplySchematicChoiceCommand&>(command).choiceIndex);
|
||||
break;
|
||||
case CommandKind::ExpandAsteroid:
|
||||
tryExpandAsteroid();
|
||||
break;
|
||||
case CommandKind::Reset:
|
||||
{
|
||||
const ResetCommand& c = static_cast<const ResetCommand&>(command);
|
||||
@@ -993,6 +998,7 @@ unsigned long long Simulation::computeStateChecksum() const
|
||||
hasher.append(m_gameOver);
|
||||
hasher.append(m_isWon);
|
||||
hasher.append(m_artifactCount);
|
||||
hasher.append(m_expansionsPurchased);
|
||||
|
||||
// WaveSystem scalar state, reached through existing accessors.
|
||||
hasher.append(threatLevel());
|
||||
@@ -1100,6 +1106,31 @@ int Simulation::buildingBlocksStock() const
|
||||
return m_buildingBlocksStock;
|
||||
}
|
||||
|
||||
int Simulation::currentAsteroidWidth_tiles() const
|
||||
{
|
||||
return m_config.world.regions.asteroidWidth_tiles
|
||||
+ m_expansionsPurchased * m_config.world.expansion.columnsPerExpansion_tiles;
|
||||
}
|
||||
|
||||
int Simulation::currentExpansionCost() const
|
||||
{
|
||||
const double cost = m_config.world.expansion.costBuildingBlocksFormula.evaluate(
|
||||
static_cast<double>(m_expansionsPurchased));
|
||||
return static_cast<int>(std::floor(cost));
|
||||
}
|
||||
|
||||
void Simulation::tryExpandAsteroid()
|
||||
{
|
||||
const int cost = currentExpansionCost();
|
||||
if (m_buildingBlocksStock < cost)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_buildingBlocksStock -= cost;
|
||||
++m_expansionsPurchased;
|
||||
m_buildingSystem->setAsteroidWidth_tiles(currentAsteroidWidth_tiles());
|
||||
}
|
||||
|
||||
bool Simulation::isGameOver() const
|
||||
{
|
||||
return m_gameOver;
|
||||
|
||||
@@ -72,6 +72,12 @@ public:
|
||||
// The seed this run was (re)initialized with; written to the replay header.
|
||||
unsigned int getSeed() const;
|
||||
int buildingBlocksStock() const;
|
||||
// Current asteroid width in tiles = base width + purchased expansions
|
||||
// (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND).
|
||||
int currentAsteroidWidth_tiles() const;
|
||||
// Building block cost of the next expansion, floored to an integer
|
||||
// (REQ-EXP-COST); x = number of expansions already purchased.
|
||||
int currentExpansionCost() const;
|
||||
bool isGameOver() const;
|
||||
bool isWon() const;
|
||||
int artifactCount() const;
|
||||
@@ -136,6 +142,11 @@ private:
|
||||
// Clears the pending choices after application.
|
||||
void applySchematicChoice(int choiceIndex);
|
||||
|
||||
// Unlocks one asteroid expansion if affordable (REQ-EXP-UNLOCK): checks the
|
||||
// current cost against the stock, deducts it, increments the expansion
|
||||
// counter, and widens the buildable asteroid. No-op if blocks are short.
|
||||
void tryExpandAsteroid();
|
||||
|
||||
// Mutable subsystem accessors; same chokepoint rule as the mutators above.
|
||||
BuildingSystem& buildingsMutable();
|
||||
BeltSystem& beltsMutable();
|
||||
@@ -165,6 +176,7 @@ private:
|
||||
Tick m_nextDepartureTick;
|
||||
BuildingId m_nextBuildingId;
|
||||
int m_buildingBlocksStock;
|
||||
int m_expansionsPurchased = 0; // REQ-EXP-COST formula variable x
|
||||
bool m_gameOver = false;
|
||||
bool m_isWon = false;
|
||||
int m_artifactCount = 0;
|
||||
|
||||
@@ -76,6 +76,8 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
|
||||
REQUIRE(cfg.world.regions.playerBufferWidth_tiles == 10);
|
||||
REQUIRE(cfg.world.regions.enemyBufferWidth_tiles == 15);
|
||||
REQUIRE(cfg.world.expansion.columnsPerExpansion_tiles == 10);
|
||||
REQUIRE(cfg.world.expansion.costBuildingBlocksFormula.evaluate(0) == Approx(400.0));
|
||||
REQUIRE(cfg.world.expansion.costBuildingBlocksFormula.evaluate(1) == Approx(800.0));
|
||||
REQUIRE(cfg.world.push.bossAdvanceSeconds == Approx(60.0));
|
||||
REQUIRE(cfg.world.orbitFactor == Approx(0.8));
|
||||
REQUIRE(cfg.world.rallyOrbitRadius_tiles == Approx(5.0));
|
||||
@@ -186,7 +188,7 @@ contest_zone_width_tiles = 30
|
||||
|
||||
[expansion]
|
||||
columns_per_expansion_tiles = 10
|
||||
cost_building_blocks = 200
|
||||
cost_building_blocks_formula = "400 * 2^x"
|
||||
|
||||
[push]
|
||||
push_expand_columns_tiles = 20
|
||||
@@ -237,7 +239,7 @@ enemy_buffer_width_tiles = 15
|
||||
|
||||
[expansion]
|
||||
columns_per_expansion_tiles = 10
|
||||
cost_building_blocks = 200
|
||||
cost_building_blocks_formula = "400 * 2^x"
|
||||
|
||||
[push]
|
||||
push_expand_columns_tiles = 20
|
||||
@@ -284,7 +286,7 @@ enemy_buffer_width_tiles = 15
|
||||
|
||||
[expansion]
|
||||
columns_per_expansion_tiles = 10
|
||||
cost_building_blocks = 200
|
||||
cost_building_blocks_formula = "400 * 2^x"
|
||||
|
||||
[push]
|
||||
push_expand_columns_tiles = 20
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
#include "BuilderModeExitedEvent.h"
|
||||
#include "BlueprintModeExitedEvent.h"
|
||||
#include "BuildingBlocksChangedEvent.h"
|
||||
#include "ExpansionCostChangedEvent.h"
|
||||
#include "GameSpeedChangedEvent.h"
|
||||
#include "SchematicChoicesAvailableEvent.h"
|
||||
#include "TickAdvancedEvent.h"
|
||||
@@ -276,6 +277,7 @@ void GameWorldView::onFrame()
|
||||
{
|
||||
const Tick newTick = m_sim->currentTick();
|
||||
const int newBlocks = m_sim->buildingBlocksStock();
|
||||
const int newExpCost = m_sim->currentExpansionCost();
|
||||
const int newBoss = m_sim->bossWaveCounter();
|
||||
const Tick newCountdown = m_sim->bossCountdownTicks();
|
||||
|
||||
@@ -291,6 +293,12 @@ void GameWorldView::onFrame()
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BuildingBlocksChangedEvent>(newBlocks));
|
||||
}
|
||||
if (newExpCost != m_lastExpansionCost)
|
||||
{
|
||||
m_lastExpansionCost = newExpCost;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ExpansionCostChangedEvent>(newExpCost));
|
||||
}
|
||||
if (newBoss != m_lastBossCounter || newCountdown != m_lastBossCountdown)
|
||||
{
|
||||
m_lastBossCounter = newBoss;
|
||||
@@ -435,7 +443,7 @@ QRect GameWorldView::viewportRect() const
|
||||
|
||||
float GameWorldView::asteroidLeftEdge() const
|
||||
{
|
||||
float leftX = -static_cast<float>(m_config->world.regions.asteroidWidth_tiles);
|
||||
float leftX = -static_cast<float>(m_sim->currentAsteroidWidth_tiles());
|
||||
for (const Building& b : m_sim->buildings().allBuildings())
|
||||
{
|
||||
for (const QPoint& cell : b.bodyCells)
|
||||
@@ -1827,6 +1835,7 @@ void GameWorldView::resetForNewGame()
|
||||
m_prevNonZeroSpeed = 1.0;
|
||||
m_lastTick = Tick(-1);
|
||||
m_lastBlocks = -1;
|
||||
m_lastExpansionCost = -1;
|
||||
m_lastBossCounter = -1;
|
||||
m_lastBossCountdown = Tick(-1);
|
||||
m_lastArtifactCount = -1;
|
||||
|
||||
@@ -226,6 +226,7 @@ private:
|
||||
|
||||
Tick m_lastTick = Tick(-1);
|
||||
int m_lastBlocks = -1;
|
||||
int m_lastExpansionCost = -1;
|
||||
int m_lastBossCounter = -1;
|
||||
Tick m_lastBossCountdown = Tick(-1);
|
||||
int m_lastArtifactCount = -1;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <QPushButton>
|
||||
#include <QSignalMapper>
|
||||
|
||||
#include "Command.h"
|
||||
#include "CommandRequestedEvent.h"
|
||||
#include "EventManager.h"
|
||||
#include "SpeedChangeRequestedEvent.h"
|
||||
#include "Tick.h"
|
||||
@@ -32,6 +34,17 @@ HeaderBar::HeaderBar(QWidget* parent)
|
||||
layout->addStretch();
|
||||
layout->addWidget(m_bossLabel);
|
||||
|
||||
// Asteroid expansion button, to the left of the speed buttons (REQ-UI-HEADER,
|
||||
// REQ-UI-EXPAND-BUTTON). Caption/enabled state are set on the first
|
||||
// ExpansionCostChangedEvent; clicking requests an ExpandAsteroidCommand.
|
||||
m_expandButton = new QPushButton(tr("Expand"), this);
|
||||
layout->addWidget(m_expandButton);
|
||||
connect(m_expandButton, &QPushButton::clicked, this, []() {
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(
|
||||
std::make_shared<ExpandAsteroidCommand>()));
|
||||
});
|
||||
|
||||
const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" };
|
||||
QSignalMapper* mapper = new QSignalMapper(this);
|
||||
for (int i = 0; i < kSpeedCount; ++i)
|
||||
@@ -67,7 +80,21 @@ void HeaderBar::handleEvent(std::shared_ptr<const TickAdvancedEvent> event)
|
||||
|
||||
void HeaderBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
|
||||
{
|
||||
m_blocks = event->blocks;
|
||||
m_blocksLabel->setText(tr("Blocks: %1").arg(event->blocks));
|
||||
updateExpandButton();
|
||||
}
|
||||
|
||||
void HeaderBar::handleEvent(std::shared_ptr<const ExpansionCostChangedEvent> event)
|
||||
{
|
||||
m_expansionCost = event->cost;
|
||||
updateExpandButton();
|
||||
}
|
||||
|
||||
void HeaderBar::updateExpandButton()
|
||||
{
|
||||
m_expandButton->setText(tr("Expand: %1 Blocks").arg(m_expansionCost));
|
||||
m_expandButton->setEnabled(m_blocks >= m_expansionCost);
|
||||
}
|
||||
|
||||
void HeaderBar::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "BossWaveUpdatedEvent.h"
|
||||
#include "BuildingBlocksChangedEvent.h"
|
||||
#include "EventHandler.h"
|
||||
#include "ExpansionCostChangedEvent.h"
|
||||
#include "GameSpeedChangedEvent.h"
|
||||
#include "Tick.h"
|
||||
#include "TickAdvancedEvent.h"
|
||||
@@ -18,6 +19,7 @@ class QPushButton;
|
||||
class HeaderBar : public QWidget,
|
||||
public CombinedEventHandler<TickAdvancedEvent,
|
||||
BuildingBlocksChangedEvent,
|
||||
ExpansionCostChangedEvent,
|
||||
GameSpeedChangedEvent,
|
||||
BossWaveUpdatedEvent,
|
||||
ArtifactCountChangedEvent>
|
||||
@@ -34,16 +36,25 @@ private slots:
|
||||
private:
|
||||
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const ExpansionCostChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const BossWaveUpdatedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const ArtifactCountChangedEvent> event) override;
|
||||
|
||||
// Refreshes the Expand button caption and enabled state from the current
|
||||
// expansion cost and building block stock (REQ-UI-EXPAND-BUTTON).
|
||||
void updateExpandButton();
|
||||
|
||||
QLabel* m_timeLabel;
|
||||
QLabel* m_blocksLabel;
|
||||
QLabel* m_artifactsLabel;
|
||||
QLabel* m_bossLabel;
|
||||
QPushButton* m_expandButton;
|
||||
std::vector<QPushButton*> m_speedButtons;
|
||||
|
||||
int m_blocks = 0;
|
||||
int m_expansionCost = 0;
|
||||
|
||||
static const double kSpeeds[];
|
||||
static const int kSpeedCount;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user