Compare commits
15 Commits
84a4733ffc
...
progressio
| Author | SHA1 | Date | |
|---|---|---|---|
| a93f7c3897 | |||
| f8028a6ca2 | |||
| b4fa3e6dff | |||
| dcc6af123f | |||
| f62b7bb78e | |||
| 27c3a7b65b | |||
| 9ddd622870 | |||
| df9f0d5ccd | |||
| db7a03dc1f | |||
| 67054f432f | |||
| 7a9a7a35ad | |||
| f822047b4e | |||
| d9e7dd0fe8 | |||
| 4d5b218fec | |||
| 97bafc95c3 |
@@ -47,7 +47,7 @@ attack_rate_hz = 2.0
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "railgun_m"
|
id = "railgun_m"
|
||||||
unlock_at_station_level = 1
|
unlock_at_station_level = 2
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
"OO"]
|
"OO"]
|
||||||
@@ -56,15 +56,18 @@ production_time_seconds = 3
|
|||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "Rm"
|
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]
|
[module.weapon]
|
||||||
damage = 17
|
damage = 14
|
||||||
attack_range_m = 70
|
attack_range_m = 80
|
||||||
attack_rate_hz = 1.5
|
attack_rate_hz = 1.5
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "railgun_l"
|
id = "railgun_l"
|
||||||
unlock_at_station_level = 1
|
unlock_at_station_level = 6
|
||||||
unlock_requires = ["railgun_m"]
|
unlock_requires = ["railgun_m"]
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OOO",
|
"OOO",
|
||||||
@@ -75,9 +78,12 @@ production_time_seconds = 4
|
|||||||
fill_color = "#FF8040"
|
fill_color = "#FF8040"
|
||||||
glyph = "Rl"
|
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]
|
[module.weapon]
|
||||||
damage = 70
|
damage = 52
|
||||||
attack_range_m = 100
|
attack_range_m = 130
|
||||||
attack_rate_hz = 0.8
|
attack_rate_hz = 0.8
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -101,7 +107,7 @@ collection_rate_hz = 0.5
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "repair_tool"
|
id = "repair_tool"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 0
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "repair_tool_module", amount = 1}]
|
materials = [{item = "repair_tool_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -110,7 +116,7 @@ glyph = "Rp"
|
|||||||
|
|
||||||
[module.repair]
|
[module.repair]
|
||||||
repair_rate_hz = 1
|
repair_rate_hz = 1
|
||||||
repair_amount_hp = 25
|
repair_amount_hp = 4
|
||||||
repair_range_m = 80
|
repair_range_m = 80
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -119,7 +125,7 @@ repair_range_m = 80
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "afterburner"
|
id = "afterburner"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 2
|
||||||
surface_mask = ["OOO"]
|
surface_mask = ["OOO"]
|
||||||
materials = [{item = "afterburner_module", amount = 1}]
|
materials = [{item = "afterburner_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -133,7 +139,7 @@ added_main_acceleration_mpss = 60
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "maneuvering_thrusters"
|
id = "maneuvering_thrusters"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 1
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
materials = [{item = "maneuvering_thrusters_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -150,7 +156,7 @@ added_maneuvering_acceleration_mpss = 10
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "armor_plates"
|
id = "armor_plates"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 0
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "armor_plates_module", amount = 1}]
|
materials = [{item = "armor_plates_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -158,12 +164,12 @@ fill_color = "#808080"
|
|||||||
glyph = "A"
|
glyph = "A"
|
||||||
|
|
||||||
[module.health]
|
[module.health]
|
||||||
added_hp = 640
|
added_hp = 1200
|
||||||
|
|
||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "sensor_booster"
|
id = "sensor_booster"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 1
|
||||||
surface_mask = ["OO"]
|
surface_mask = ["OO"]
|
||||||
materials = [{item = "sensor_booster_module", amount = 1}]
|
materials = [{item = "sensor_booster_module", amount = 1}]
|
||||||
production_time_seconds = 1
|
production_time_seconds = 1
|
||||||
@@ -179,7 +185,7 @@ added_sensor_range_m = 50
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_upgrade"
|
id = "weapon_upgrade"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 4
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
"OX",
|
"OX",
|
||||||
@@ -195,7 +201,7 @@ multiplied_damage = 1.2
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_primer"
|
id = "weapon_primer"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 4
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
"OX",
|
"OX",
|
||||||
@@ -211,7 +217,7 @@ multiplied_attack_rate_hz = 1.2
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "weapon_stabilizer"
|
id = "weapon_stabilizer"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 3
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
"OX",
|
"OX",
|
||||||
@@ -222,7 +228,7 @@ fill_color = "#FF4040"
|
|||||||
glyph = "Ws"
|
glyph = "Ws"
|
||||||
|
|
||||||
[module.weapon]
|
[module.weapon]
|
||||||
multiplied_attack_range_m = 1.5
|
multiplied_attack_range_m = 1.3
|
||||||
multiplied_attack_rate_hz = 0.8
|
multiplied_attack_rate_hz = 0.8
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -234,7 +240,7 @@ multiplied_attack_rate_hz = 0.8
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "drone_bay"
|
id = "drone_bay"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 5
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OO",
|
"OO",
|
||||||
"OO"]
|
"OO"]
|
||||||
@@ -246,7 +252,7 @@ glyph = "Db"
|
|||||||
|
|
||||||
[[module]]
|
[[module]]
|
||||||
id = "drone_hangar"
|
id = "drone_hangar"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 9
|
||||||
surface_mask = [
|
surface_mask = [
|
||||||
"OOOOOO",
|
"OOOOOO",
|
||||||
"OOOOOO"]
|
"OOOOOO"]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# recipes.toml
|
# recipes.toml
|
||||||
#
|
#
|
||||||
# Production tree v2 (see docs/content_design.md, "Production tree v2" and
|
# Production tree v2 (structure in docs/content_design.md, numbers with
|
||||||
# "Numbers — first pass"). Quantities and durations are tuned so that every
|
# 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
|
# 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
|
# realized: tier 1 ratios are 1:1, tier 2 ratios are 2:3, tier 3+ ratios
|
||||||
# are deliberately strange.
|
# are deliberately strange.
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
# rock IS ore)
|
# rock IS ore)
|
||||||
# mid + quartz geode deposits in expansion territory
|
# mid + quartz geode deposits in expansion territory
|
||||||
# (deposit gating pending — see action
|
# (deposit gating pending — see action
|
||||||
# item 5 in docs/progression_design.md;
|
# item 5 in docs/balancing/README.md;
|
||||||
# until then quartz mines anywhere)
|
# until then quartz mines anywhere)
|
||||||
# late + voidsteel battle-forged: ONLY from reprocessing
|
# late + voidsteel battle-forged: ONLY from reprocessing
|
||||||
# salvaged scrap, so capital production
|
# salvaged scrap, so capital production
|
||||||
@@ -141,9 +141,12 @@ outputs = [{item = "copper_coil", amount = 1}]
|
|||||||
duration_seconds = 1.5
|
duration_seconds = 1.5
|
||||||
|
|
||||||
# Depth-3 chain (ore -> ingot -> plate -> block) is the factory's
|
# 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]]
|
[[recipe]]
|
||||||
id = "building_block"
|
id = "building_block"
|
||||||
|
unlock_at_station_level = -1
|
||||||
building = "assembler"
|
building = "assembler"
|
||||||
inputs = [{item = "steel_plate", amount = 2}]
|
inputs = [{item = "steel_plate", amount = 2}]
|
||||||
outputs = [{item = "building_block", amount = 4}]
|
outputs = [{item = "building_block", amount = 4}]
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ sensor_range_m = 200
|
|||||||
# mount medium hardware.
|
# mount medium hardware.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "destroyer"
|
id = "destroyer"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 0
|
||||||
layout = [
|
layout = [
|
||||||
"OXOXO",
|
"OXOXO",
|
||||||
"OOOOO",
|
"OOOOO",
|
||||||
@@ -120,7 +120,7 @@ sensor_range_m = 220
|
|||||||
# supports; no 3x3 area exists for an l gun.
|
# supports; no 3x3 area exists for an l gun.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "cruiser"
|
id = "cruiser"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 2
|
||||||
layout = [
|
layout = [
|
||||||
"XOOX",
|
"XOOX",
|
||||||
"OOOO",
|
"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.
|
# stern leave no 3x3 area for an l gun and no 2x6 area for a drone hangar.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "battlecruiser"
|
id = "battlecruiser"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 4
|
||||||
|
unlock_requires = ["cruiser"]
|
||||||
layout = [
|
layout = [
|
||||||
"OOXXOO",
|
"OOXXOO",
|
||||||
"OOOOOO",
|
"OOOOOO",
|
||||||
@@ -179,7 +180,7 @@ materials = [{item = "battlecruiser_hull", amount = 1}]
|
|||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp = 2700
|
hp = 2400
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps = 20
|
speed_mps = 20
|
||||||
@@ -200,7 +201,8 @@ sensor_range_m = 260
|
|||||||
# so no 2x6 drone hangar fits.
|
# so no 2x6 drone hangar fits.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "battleship"
|
id = "battleship"
|
||||||
unlock_at_station_level = 1
|
unlock_at_station_level = 6
|
||||||
|
unlock_requires = ["battlecruiser"]
|
||||||
layout = [
|
layout = [
|
||||||
"XOOOOX",
|
"XOOOOX",
|
||||||
"OOOOOO",
|
"OOOOOO",
|
||||||
@@ -222,7 +224,7 @@ materials = [{item = "battleship_hull", amount = 1}]
|
|||||||
production_time_seconds = 6
|
production_time_seconds = 6
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp = 7500
|
hp = 6300
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps = 15
|
speed_mps = 15
|
||||||
@@ -242,7 +244,7 @@ sensor_range_m = 280
|
|||||||
# stay the only hangar hull. Bow and stern strips hold supports.
|
# stay the only hangar hull. Bow and stern strips hold supports.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "dreadnought"
|
id = "dreadnought"
|
||||||
unlock_at_station_level = 2
|
unlock_at_station_level = 8
|
||||||
unlock_requires = ["battleship"]
|
unlock_requires = ["battleship"]
|
||||||
layout = [
|
layout = [
|
||||||
"XXXOOOOOXXX",
|
"XXXOOOOOXXX",
|
||||||
@@ -267,7 +269,7 @@ materials = [{item = "dreadnought_hull", amount = 1}]
|
|||||||
production_time_seconds = 8
|
production_time_seconds = 8
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp = 15500
|
hp = 24000
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps = 10
|
speed_mps = 10
|
||||||
@@ -286,7 +288,8 @@ sensor_range_m = 300
|
|||||||
# the lower decks hold supports and 2x2 point-defense m guns.
|
# the lower decks hold supports and 2x2 point-defense m guns.
|
||||||
[[ship]]
|
[[ship]]
|
||||||
id = "carrier"
|
id = "carrier"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = 9
|
||||||
|
unlock_requires = ["battleship"]
|
||||||
layout = [
|
layout = [
|
||||||
"XOOOOOOOOX",
|
"XOOOOOOOOX",
|
||||||
"OOOOOOOOOO",
|
"OOOOOOOOOO",
|
||||||
@@ -308,7 +311,7 @@ materials = [{item = "carrier_hull", amount = 1}]
|
|||||||
production_time_seconds = 8
|
production_time_seconds = 8
|
||||||
|
|
||||||
[ship.health]
|
[ship.health]
|
||||||
hp = 15500
|
hp = 24000
|
||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_mps = 10
|
speed_mps = 10
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# stations.toml
|
# 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
|
# a fresh player defence station holds one early parity wave unaided; the
|
||||||
# enemy station at level 0 matches the player station exactly and scales
|
# enemy station at level 0 matches the player station exactly and scales
|
||||||
# with the push level x. Station scrap drops stay authored (pushing rewards
|
# with the push level x. Station scrap drops stay authored (pushing rewards
|
||||||
@@ -22,7 +22,7 @@ surface_mask = [
|
|||||||
level = 1
|
level = 1
|
||||||
hp_formula = "3000"
|
hp_formula = "3000"
|
||||||
damage_formula = "25"
|
damage_formula = "25"
|
||||||
range_m_formula = "200"
|
range_m_formula = "120"
|
||||||
fire_rate_hz_formula = "1"
|
fire_rate_hz_formula = "1"
|
||||||
scrap_drop_formula = "40"
|
scrap_drop_formula = "40"
|
||||||
|
|
||||||
@@ -33,6 +33,6 @@ surface_mask = [
|
|||||||
]
|
]
|
||||||
hp_formula = "3000 + 1500*x"
|
hp_formula = "3000 + 1500*x"
|
||||||
damage_formula = "25 + 12*x"
|
damage_formula = "25 + 12*x"
|
||||||
range_m_formula = "200"
|
range_m_formula = "120"
|
||||||
fire_rate_hz_formula = "1.0 + 0.1*x"
|
fire_rate_hz_formula = "1.0 + 0.1*x"
|
||||||
scrap_drop_formula = "40 + 30*x"
|
scrap_drop_formula = "40 + 30*x"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[world]
|
[world]
|
||||||
height_tiles = 40
|
height_tiles = 40
|
||||||
refund_percentage = 100
|
refund_percentage = 100
|
||||||
starting_building_blocks = 1000
|
starting_building_blocks = 200
|
||||||
scrap_despawn_seconds = 120
|
scrap_despawn_seconds = 120
|
||||||
scrap_per_threat = 0.25
|
scrap_per_threat = 0.25
|
||||||
tile_size_m = 10
|
tile_size_m = 10
|
||||||
@@ -19,7 +19,9 @@ enemy_buffer_width_tiles = 20
|
|||||||
|
|
||||||
[expansion]
|
[expansion]
|
||||||
columns_per_expansion_tiles = 10
|
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]
|
||||||
push_expand_columns_tiles = 10
|
push_expand_columns_tiles = 10
|
||||||
@@ -32,10 +34,13 @@ target_hysteresis = 0.40 # keep current target unless
|
|||||||
|
|
||||||
[artifacts]
|
[artifacts]
|
||||||
artifact_chance_formula = "0.05 * x" # 5% chance per station level
|
artifact_chance_formula = "0.05 * x" # 5% chance per station level
|
||||||
artifact_win_count = 3
|
artifact_win_count = 5
|
||||||
|
|
||||||
[waves]
|
[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_min_seconds = 15
|
||||||
gap_max_seconds = 45
|
gap_max_seconds = 45
|
||||||
spawn_duration_seconds = 10
|
spawn_duration_seconds = 10
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
# using the verified fitted threat values from tools/threat_report.py:
|
# using the verified fitted threat values from tools/threat_report.py:
|
||||||
# drone 10.5, frigate 47, destroyer 99, cruiser 233.5,
|
# drone 10.5, frigate 47, destroyer 99, cruiser 233.5,
|
||||||
# battlecruiser 354.5, battleship 722.5, dreadnought 1491.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
|
# Module arrays mirror the ships' default_modules loadouts unless a
|
||||||
# doctrine variant is the point of the arena.
|
# doctrine variant is the point of the arena.
|
||||||
#
|
#
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Mirror: drones 20v20"
|
name = "Mirror: drones 20v20"
|
||||||
height_tiles = 30
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 10
|
player_buffer_width_tiles = 10
|
||||||
contest_zone_width_tiles = 50
|
contest_zone_width_tiles = 50
|
||||||
enemy_buffer_width_tiles = 10
|
enemy_buffer_width_tiles = 10
|
||||||
@@ -43,7 +44,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Mirror: cruisers 6v6"
|
name = "Mirror: cruisers 6v6"
|
||||||
height_tiles = 30
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 10
|
player_buffer_width_tiles = 10
|
||||||
contest_zone_width_tiles = 50
|
contest_zone_width_tiles = 50
|
||||||
enemy_buffer_width_tiles = 10
|
enemy_buffer_width_tiles = 10
|
||||||
@@ -74,7 +75,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Mirror: battleships 2v2"
|
name = "Mirror: battleships 2v2"
|
||||||
height_tiles = 30
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 10
|
player_buffer_width_tiles = 10
|
||||||
contest_zone_width_tiles = 50
|
contest_zone_width_tiles = 50
|
||||||
enemy_buffer_width_tiles = 10
|
enemy_buffer_width_tiles = 10
|
||||||
@@ -111,7 +112,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Drone swarm vs cruisers (462 vs 467)"
|
name = "Drone swarm vs cruisers (462 vs 467)"
|
||||||
height_tiles = 30
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 10
|
player_buffer_width_tiles = 10
|
||||||
contest_zone_width_tiles = 50
|
contest_zone_width_tiles = 50
|
||||||
enemy_buffer_width_tiles = 10
|
enemy_buffer_width_tiles = 10
|
||||||
@@ -139,7 +140,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Frigates vs battleship (705 vs 723)"
|
name = "Frigates vs battleship (705 vs 723)"
|
||||||
height_tiles = 30
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 10
|
player_buffer_width_tiles = 10
|
||||||
contest_zone_width_tiles = 50
|
contest_zone_width_tiles = 50
|
||||||
enemy_buffer_width_tiles = 10
|
enemy_buffer_width_tiles = 10
|
||||||
@@ -171,7 +172,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Destroyers vs dreadnought (1485 vs 1492)"
|
name = "Destroyers vs dreadnought (1485 vs 1492)"
|
||||||
height_tiles = 40
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 10
|
player_buffer_width_tiles = 10
|
||||||
contest_zone_width_tiles = 50
|
contest_zone_width_tiles = 50
|
||||||
enemy_buffer_width_tiles = 10
|
enemy_buffer_width_tiles = 10
|
||||||
@@ -207,7 +208,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Cruisers vs carrier (1401 vs 1437, carrier expected to lose)"
|
name = "Cruisers vs carrier (1401 vs 1437, carrier expected to lose)"
|
||||||
height_tiles = 40
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 10
|
player_buffer_width_tiles = 10
|
||||||
contest_zone_width_tiles = 50
|
contest_zone_width_tiles = 50
|
||||||
enemy_buffer_width_tiles = 10
|
enemy_buffer_width_tiles = 10
|
||||||
@@ -240,7 +241,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Mixed mid vs battlecruisers (1394 vs 1418)"
|
name = "Mixed mid vs battlecruisers (1394 vs 1418)"
|
||||||
height_tiles = 40
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 10
|
player_buffer_width_tiles = 10
|
||||||
contest_zone_width_tiles = 50
|
contest_zone_width_tiles = 50
|
||||||
enemy_buffer_width_tiles = 10
|
enemy_buffer_width_tiles = 10
|
||||||
@@ -285,7 +286,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Two to one (must be decisive)"
|
name = "Two to one (must be decisive)"
|
||||||
height_tiles = 30
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 10
|
player_buffer_width_tiles = 10
|
||||||
contest_zone_width_tiles = 50
|
contest_zone_width_tiles = 50
|
||||||
enemy_buffer_width_tiles = 10
|
enemy_buffer_width_tiles = 10
|
||||||
@@ -314,7 +315,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Armored vs glass destroyers (1188 vs 1196)"
|
name = "Armored vs glass destroyers (1188 vs 1196)"
|
||||||
height_tiles = 30
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 10
|
player_buffer_width_tiles = 10
|
||||||
contest_zone_width_tiles = 50
|
contest_zone_width_tiles = 50
|
||||||
enemy_buffer_width_tiles = 10
|
enemy_buffer_width_tiles = 10
|
||||||
@@ -348,9 +349,53 @@ enemy_buffer_width_tiles = 10
|
|||||||
{type = "railgun_s", x = 4, y = 1, rotation = "east"},
|
{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]]
|
[[arena]]
|
||||||
name = "Repair escort vs raw numbers (444 vs 444)"
|
name = "Repair escort vs raw numbers (444 vs 444)"
|
||||||
height_tiles = 30
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 10
|
player_buffer_width_tiles = 10
|
||||||
contest_zone_width_tiles = 50
|
contest_zone_width_tiles = 50
|
||||||
enemy_buffer_width_tiles = 10
|
enemy_buffer_width_tiles = 10
|
||||||
@@ -391,7 +436,7 @@ enemy_buffer_width_tiles = 10
|
|||||||
|
|
||||||
[[arena]]
|
[[arena]]
|
||||||
name = "Station assault (2 stations + 105 vs 315)"
|
name = "Station assault (2 stations + 105 vs 315)"
|
||||||
height_tiles = 60
|
height_tiles = 10
|
||||||
player_buffer_width_tiles = 15
|
player_buffer_width_tiles = 15
|
||||||
contest_zone_width_tiles = 40
|
contest_zone_width_tiles = 40
|
||||||
enemy_buffer_width_tiles = 15
|
enemy_buffer_width_tiles = 15
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ enemy_buffer_width_tiles = 15
|
|||||||
|
|
||||||
[expansion]
|
[expansion]
|
||||||
columns_per_expansion_tiles = 10
|
columns_per_expansion_tiles = 10
|
||||||
cost_building_blocks = 200
|
cost_building_blocks_formula = "400 * 2^x"
|
||||||
|
|
||||||
[push]
|
[push]
|
||||||
push_expand_columns_tiles = 20
|
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,
|
Rules and principles that govern the production tree, progression pacing,
|
||||||
and balancing. This document contains **rules only** — concrete content
|
and balancing. This document contains **rules only** — the chosen base
|
||||||
(item lists, recipes, unlock levels, stat numbers) lives in the config files
|
numbers live in `targets.md`, everything derived from them in
|
||||||
and `content_design.md`; those numbers must follow the rules stated here.
|
`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
|
## Player-experience goals
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ adapts later (see Refactorability).
|
|||||||
- The fourth input is the core loop hook: capital ship production requires
|
- The fourth input is the core loop hook: capital ship production requires
|
||||||
fighting (salvaging and reprocessing), not just mining.
|
fighting (salvaging and reprocessing), not just mining.
|
||||||
- Every gating has a fictional reason (concrete fiction in
|
- 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
|
rock is minable anywhere; the mid resource sits in rare pockets; the
|
||||||
late input is battle-forged — created only in the violence of ship
|
late input is battle-forged — created only in the violence of ship
|
||||||
destruction, which is why any wreck (including the player's own)
|
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
|
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
|
is never offered a shortcut for a chain they have not built yet. The
|
||||||
output-item half of this check already exists in
|
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
|
- **Shortcuts are pure rewards, never balance factors.** An item's threat
|
||||||
value is the *maximum* across its producing recipes (REQ-THREAT-ITEM), so
|
value is the *maximum* across its producing recipes (REQ-THREAT-ITEM), so
|
||||||
unlocking a cheaper recipe does not lower the item's threat accounting —
|
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
|
modules, and tiers. Higher tiers are better per *ship* and per *module
|
||||||
slot*, not per invested factory-second — their advantage is
|
slot*, not per invested factory-second — their advantage is
|
||||||
concentration (fewer, bigger things; slot geometry per
|
concentration (fewer, bigger things; slot geometry per
|
||||||
`content_design.md`) and qualitative capabilities, not a better exchange
|
`../content_design.md`) and qualitative capabilities, not a better
|
||||||
rate. Deviations from this rule are deliberate and documented.
|
exchange rate. Deviations from this rule are deliberate and documented.
|
||||||
- **Difficulty race:** the enemy threat rate (`threat_rate_formula`) is
|
- **Difficulty race:** the enemy threat rate (`threat_rate_formula`) is
|
||||||
tuned against the factory output (threat/s) achievable by a competent
|
tuned against the factory output (threat/s) achievable by a competent
|
||||||
player — slightly below it early, crossing above it eventually. The game
|
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
|
- **All time scaling lives in the threat rate** — waves get bigger, ships
|
||||||
of a given schematic never get individually stronger. There is no ship
|
of a given schematic never get individually stronger. There is no ship
|
||||||
level dimension: stat formulas are plain values, and per-ship level
|
level dimension: stat formulas are plain values, and per-ship level
|
||||||
scaling does not exist. Push
|
scaling does not exist. Push scaling on enemy defence stations is the
|
||||||
scaling on enemy defence stations is the separate, player-triggered
|
separate, player-triggered difficulty axis and keeps its level formulas.
|
||||||
difficulty axis and keeps its level formulas.
|
|
||||||
|
|
||||||
## Unlock & drop pacing
|
## 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
|
gate, a schematic (ship, module, or assembler recipe) may list
|
||||||
prerequisite schematics (`unlock_requires`, REQ-LOCK-PREREQ) that must
|
prerequisite schematics (`unlock_requires`, REQ-LOCK-PREREQ) that must
|
||||||
already be unlocked before it enters the drop pool — e.g. the medium
|
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.
|
gun requires the small gun; a future Mk2 requires its base version.
|
||||||
Station level gates the earliest
|
Station level gates the earliest *when*; prerequisites gate the
|
||||||
*when*; prerequisites gate the *order*, keeping drop offers coherent
|
*order*, keeping drop offers coherent with what the player already
|
||||||
with what the player already owns.
|
owns.
|
||||||
- **No duplicate drops.** Ship and module schematics leave the drop pool
|
- **No duplicate drops.** Ship and module schematics leave the drop pool
|
||||||
once owned, exactly as assembler recipe schematics already do. There are
|
once owned, exactly as assembler recipe schematics already do. There are
|
||||||
no schematic level-ups; player power grows through unlock breadth and
|
no schematic level-ups; player power grows through unlock breadth and
|
||||||
factory scale only, which keeps power-per-threat exact on both sides.
|
factory scale only, which keeps power-per-threat exact on both sides.
|
||||||
The pool therefore shrinks over a run and late pushes increasingly offer
|
The pool therefore shrinks over a run and late pushes increasingly offer
|
||||||
artifacts — intended: the late game is a race for the win condition.
|
artifacts — intended: the late game is a race for the win condition.
|
||||||
Per-item progression may return later as Mk2 upgrade recipes (see Open
|
Per-item progression may return later as Mk2 upgrade recipes (see Future
|
||||||
tasks), never as free level-ups.
|
work), never as free level-ups.
|
||||||
- **Artifacts trade power for progress.** Artifact options compete with
|
- **Artifacts trade power for progress.** Artifact options compete with
|
||||||
schematic picks in the same choice dialog; the artifact chance must be
|
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
|
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
|
through the HQ's single belt port, so income is capped at belt
|
||||||
throughput regardless of assembler count. Per-building costs should be
|
throughput regardless of assembler count. Per-building costs should be
|
||||||
high enough that this cap can bind late-game (see the condensed-block
|
high enough that this cap can bind late-game (see the condensed-block
|
||||||
idea under Open tasks).
|
idea under Future work).
|
||||||
|
|
||||||
## Numeric guardrails
|
## 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
|
- **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.
|
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
|
- **Condensed building blocks** — a drop-unlockable shortcut-style
|
||||||
recipe that packs several blocks' worth of value into one belt item,
|
recipe that packs several blocks' worth of value into one belt item,
|
||||||
relieving the HQ intake ceiling (see Building block economy) as a
|
relieving the HQ intake ceiling (see Building block economy) as a
|
||||||
@@ -337,39 +331,3 @@ Constraints that every recipe must respect, independent of tuning:
|
|||||||
assembler), and keeps balancing one-dimensional (no level variable
|
assembler), and keeps balancing one-dimensional (no level variable
|
||||||
anywhere). Enemy-side progression happens via `default_modules`
|
anywhere). Enemy-side progression happens via `default_modules`
|
||||||
variants per era instead of a level formula.
|
variants per era instead of a level formula.
|
||||||
|
|
||||||
## Action items — changes beyond this document
|
|
||||||
|
|
||||||
Agreed changes that require edits to `requirements.md`, the code, and the
|
|
||||||
configs. Completed items are removed from this list (their outcomes live
|
|
||||||
in `requirements.md` and the git history). Still open:
|
|
||||||
|
|
||||||
1. **Fill unfillable schematic slots with artifacts.** With duplicates
|
|
||||||
removed, the schematic drop pool can run dry — previously unreachable.
|
|
||||||
Decision: every slot in the choice dialog that cannot be filled with a
|
|
||||||
schematic because the eligible pool is exhausted is filled with an
|
|
||||||
artifact option instead (in addition to any artifact option granted by
|
|
||||||
the regular artifact roll). A push therefore always awards a full
|
|
||||||
dialog. Update REQ-DEF-SCHEMATIC-DROP.
|
|
||||||
2. **Confirm wave scaling carries alone.** With per-ship level scaling
|
|
||||||
removed, `threat_rate_formula` is the only time-scaling axis; verify
|
|
||||||
the current `world.toml` values still produce the intended difficulty
|
|
||||||
curve (likely a review, not a retune, since the config already ran
|
|
||||||
with a flat ship level before the removal).
|
|
||||||
3. **Gate shortcut-recipe drops on their inputs.** Extend the assembler
|
|
||||||
recipe schematic pool eligibility in REQ-DEF-SCHEMATIC-DROP: in
|
|
||||||
addition to the existing station-level and output-item checks, all of
|
|
||||||
the recipe's input item types must be implicitly unlocked as well.
|
|
||||||
4. **Expansion cost formula.** Replace the flat
|
|
||||||
`world.toml [expansion].cost_building_blocks` with a formula of the
|
|
||||||
number of expansions already purchased (x = expansions bought so
|
|
||||||
far), so expansion costs can escalate per the block economy rules.
|
|
||||||
Update REQ-EXP-COST and wherever the UI displays the expansion cost.
|
|
||||||
5. **Resource deposits.** Add a terrain deposit layer per the Resource
|
|
||||||
deposits rules: deposit patches generated in expansion columns
|
|
||||||
(deterministic content per expansion, randomized placement within the
|
|
||||||
new columns), deposit rendering, and a miner condition (a resource
|
|
||||||
recipe is selectable only if the miner's footprint overlaps at least
|
|
||||||
one matching deposit tile). Touches REQ-BLD-MINER ("every asteroid
|
|
||||||
tile is equivalent" no longer holds), REQ-GW-ASTEROID-EXPAND /
|
|
||||||
REQ-EXP-*, `world.toml`, and `visuals.toml`.
|
|
||||||
104
docs/balancing/targets.md
Normal file
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
|
The designed game content: hull layout grids, module footprints and the
|
||||||
module surface masks; pass 2 defined the production tree (recipes). Stats and
|
gating between them, and the production tree (items, chains, fiction).
|
||||||
threat costs in the config files are still placeholders for the balancing
|
All numbers — quantities, durations, threat values, stats, unlock levels
|
||||||
pass.
|
— 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
|
## 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 |
|
| 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 |
|
| 1x2 | maneuvering_thrusters, sensor_booster, armor_plates | frigate |
|
||||||
| 1x3 | afterburner | frigate (eats most of it) |
|
| 1x3 | afterburner | frigate (eats most of it) |
|
||||||
| L-shape (3 cells) | weapon_stabilizer, weapon_primer, weapon_upgrade | frigate |
|
| L-shape (3 cells) | weapon_stabilizer, weapon_primer, weapon_upgrade | frigate |
|
||||||
| 2x2 | laser_cannon_m, drone_bay | cruiser |
|
| 2x2 | railgun_m, drone_bay | cruiser |
|
||||||
| 3x3 | laser_cannon_l | battleship |
|
| 3x3 | railgun_l | battleship |
|
||||||
| 2x6 | drone_hangar | carrier (only) |
|
| 2x6 | drone_hangar | carrier (only) |
|
||||||
|
|
||||||
### Hull grids
|
### Hull grids
|
||||||
@@ -129,58 +130,11 @@ Maximum simultaneous (disjoint) placements: m guns — cruiser 2,
|
|||||||
battlecruiser 3, battleship 4; l guns — battleship 1, dreadnought 3;
|
battlecruiser 3, battleship 4; l guns — battleship 1, dreadnought 3;
|
||||||
drone hangar — carrier 1.
|
drone hangar — carrier 1.
|
||||||
|
|
||||||
## Production tree (first pass — superseded)
|
## Production tree
|
||||||
|
|
||||||
**Superseded (July 2026):** this first-pass tree predates the rules in
|
Designed against the rules in `docs/balancing/rules.md` (ratio curve,
|
||||||
`docs/progression_design.md` and will be replaced. The decisions for the
|
cost ladder, cost archetypes, refactorability). Quantities, durations,
|
||||||
redesign are recorded in "Production tree v2 — decisions" below; the text
|
and threat values live in `docs/balancing/derived.md`.
|
||||||
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:
|
|
||||||
|
|
||||||
### Base inputs (4) and fiction
|
### 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.
|
*is* ore, which is why the shipyard operation was built here at all.
|
||||||
- **quartz** — mid-game, minable only on geode deposit patches in
|
- **quartz** — mid-game, minable only on geode deposit patches in
|
||||||
expansion territory (see the Resource deposits rules in
|
expansion territory (see the Resource deposits rules in
|
||||||
`progression_design.md`). Fiction: ordinary silicate dust is everywhere
|
`docs/balancing/rules.md`; the deposit mechanic itself is an open
|
||||||
and worthless; chips and optics need rare, pocket-bound optical-grade
|
action item — until it lands, quartz mines anywhere). Fiction:
|
||||||
crystal.
|
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.
|
- **voidsteel** — late-game, obtained only by reprocessing scrap.
|
||||||
Fiction: battle-forged — formed when weapon plasma anneals hull metal
|
Fiction: battle-forged — formed when weapon plasma anneals hull metal
|
||||||
in the violence of ship destruction. Any wreck yields it, including the
|
in the violence of ship destruction. Any wreck yields it, including the
|
||||||
player's own; no foundry can replicate it.
|
player's own; no foundry can replicate it.
|
||||||
- **titanium is dropped.** Its hull-gating role moves to quartz-era
|
- **titanium was dropped** (v1 tree). Its hull-gating role moved to
|
||||||
control systems ("you can smelt all the steel you want, but you cannot
|
quartz-era control systems ("you can smelt all the steel you want, but
|
||||||
steer a battlecruiser without electronics") and possibly a *quality*
|
you cannot steer a battlecruiser without electronics") plus the
|
||||||
steel step — e.g. a long-running hardened-steel recipe (time-heavy
|
hardened-steel quality step (a deliberately long-running, time-heavy
|
||||||
archetype). Open question for the tree draft: quality step vs.
|
recipe) — explicitly not sheer steel quantity alone.
|
||||||
electronics-only gating; explicitly **not** sheer steel quantity alone.
|
|
||||||
|
|
||||||
### Material palette (fingerprints per family)
|
### Material palette (fingerprints per family)
|
||||||
|
|
||||||
- **iron/steel** — structure.
|
- **iron/steel** — structure.
|
||||||
- **copper** — conduction and heat: wiring, coils, heat sinks.
|
- **copper** — conduction and heat: wiring, coils, heat sinks.
|
||||||
- **silicon family** (all derived from quartz): silicon (logic,
|
- **silicon family** (all derived from quartz): silicon (logic,
|
||||||
sensors), glass/optics (lenses, focusing crystals), ceramics (heat
|
sensors), ceramics (heat shielding, insulators); glass/optics are cut
|
||||||
shielding, insulators). Carries the non-metal variety without extra
|
from v1 — their only consumers would be lasers, which are deferred.
|
||||||
base inputs.
|
|
||||||
- **voidsteel** — capital-tier structure and exotics.
|
- **voidsteel** — capital-tier structure and exotics.
|
||||||
- Deliberately skipped: carbon (mostly redundant with copper/ceramics),
|
- Deliberately skipped: carbon (mostly redundant with copper/ceramics),
|
||||||
plastics (drags in Factorio-style chemical chains; ceramics read more
|
plastics (drags in Factorio-style chemical chains; ceramics read more
|
||||||
@@ -219,23 +172,25 @@ refactorability). Decisions fixed so far:
|
|||||||
|
|
||||||
### Weapons
|
### Weapons
|
||||||
|
|
||||||
- All current lasers are renamed to **railguns** (`laser_cannon_s/m/l` →
|
- All v1 weapons are **railguns** (`railgun_s/m/l`, renamed from the
|
||||||
`railgun_s/m/l`); footprints and the gating matrix are unchanged.
|
laser placeholders; footprints and the gating matrix unchanged).
|
||||||
Implementation stays as-is (instant damage application, no projectile,
|
Implementation is instant damage application with no projectile and no
|
||||||
no ammunition) — the beam visual reads as a tracer round. Materials:
|
ammunition — the beam visual reads as a tracer round. Materials: iron
|
||||||
iron slugs, copper coils, steel rails — the starting-metal fingerprint.
|
slugs, copper coils, steel rails — the starting-metal fingerprint.
|
||||||
- **Lasers are reserved for later** as a genuinely distinct weapon type
|
- **Lasers are reserved for later** as a genuinely distinct weapon type
|
||||||
(e.g. once projectile/ammunition mechanics exist for other families),
|
(e.g. once projectile/ammunition mechanics exist for other families),
|
||||||
arriving with quartz optics. More weapon types are planned; railguns
|
arriving with quartz optics. More weapon types are planned; railguns
|
||||||
are simply the baseline tech that ships with v1.
|
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
|
Input lists only — quantities, durations, and per-item threat values are
|
||||||
in the numbers pass, tuned so every chain sums to its threat-ladder
|
in `docs/balancing/derived.md` and the configs.
|
||||||
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.
|
|
||||||
|
|
||||||
**Mined (miner):** `iron_ore`, `copper_ore` (every tile), `quartz`
|
**Mined (miner):** `iron_ore`, `copper_ore` (every tile), `quartz`
|
||||||
(geode deposits in expansion territory).
|
(geode deposits in expansion territory).
|
||||||
@@ -244,7 +199,7 @@ deferred; the silicon family ships as silicon + ceramics.
|
|||||||
|
|
||||||
| output | input | ratio class |
|
| 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 |
|
| copper_ingot | copper_ore | nice |
|
||||||
| silicon | quartz | mid entry |
|
| silicon | quartz | mid entry |
|
||||||
| iron_ingot | scrap | the safe, boring scrap sink |
|
| 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 |
|
| capital_core | voidsteel + capacitor_bank + control_chip | capital heart |
|
||||||
|
|
||||||
**Hull items** (`<ship>_hull`, assembler-made; the shipyard consumes the
|
**Hull items** (`<ship>_hull`, assembler-made; the shipyard consumes the
|
||||||
hull item plus module materials). The m+ hull gate is resolved as
|
hull item plus module materials). The m+ hull gate is **both**
|
||||||
**both** of the open-question options: hardened_steel (quality steel, a
|
hardened_steel (quality steel, the time-heavy step) *and* control_chip
|
||||||
deliberately long-running recipe — the time-heavy step) *and*
|
(electronics):
|
||||||
control_chip (electronics):
|
|
||||||
|
|
||||||
| hull | inputs |
|
| hull | inputs |
|
||||||
|---|---|
|
|---|---|
|
||||||
| drone_hull | steel_plate |
|
| drone_hull | iron_ingot |
|
||||||
| frigate_hull | steel_plate + copper_wire |
|
| frigate_hull | steel_plate + copper_wire |
|
||||||
| destroyer_hull | steel_plate + copper_coil |
|
| destroyer_hull | steel_plate + copper_coil |
|
||||||
| cruiser_hull | hardened_steel + control_chip |
|
| cruiser_hull | hardened_steel + control_chip |
|
||||||
@@ -302,7 +256,7 @@ stockpiled):
|
|||||||
|
|
||||||
| module | inputs | archetype |
|
| module | inputs | archetype |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| railgun_s | steel_plate + copper_coil | balanced |
|
| railgun_s | copper_coil | lean |
|
||||||
| salvager | steel_plate + copper_wire | balanced |
|
| salvager | steel_plate + copper_wire | balanced |
|
||||||
| repair_tool | steel_plate + copper_wire | balanced |
|
| repair_tool | steel_plate + copper_wire | balanced |
|
||||||
| armor_plates | steel_plate (many) | material-heavy, fast |
|
| armor_plates | steel_plate (many) | material-heavy, fast |
|
||||||
@@ -317,238 +271,24 @@ stockpiled):
|
|||||||
| railgun_l | capacitor_bank + hardened_steel + ceramic_plate | late |
|
| railgun_l | capacitor_bank + hardened_steel + ceramic_plate | late |
|
||||||
| drone_hangar | voidsteel_plate + control_chip + drive_unit | late (carrier only) |
|
| drone_hangar | voidsteel_plate + control_chip + drive_unit | late (carrier only) |
|
||||||
|
|
||||||
Refactorability check (the default technique holds): railgun_s → m
|
**Refactorability check** (the default technique holds): railgun_s → m
|
||||||
introduces capacitor_bank, built from a subset of the small gun's
|
introduces capacitor_bank, built from a subset of the small gun's inputs
|
||||||
inputs (copper_coil) plus the new base resource (silicon); the m gun
|
(copper_coil) plus the new base resource (silicon); the m gun otherwise
|
||||||
otherwise reuses the small gun's inputs. Hulls likewise: cruiser adds
|
reuses the small gun's inputs. Hulls likewise: the cruiser adds
|
||||||
hardening (fed by the existing steel line) and chips (fed by the new
|
hardening (fed by the existing steel line) and chips (fed by the new
|
||||||
quartz territory) without touching the iron/copper core.
|
quartz territory) without touching the iron/copper core.
|
||||||
|
|
||||||
**Shortcut recipe candidates** (drop-only assembler schematics; not
|
**Shortcut recipes** (drop-only assembler schematics; not every strange
|
||||||
every strange chain gets one):
|
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
|
Consistency is checked by `tools/verify_recipes.py` — re-run it after
|
||||||
chain in the game.
|
editing recipes, ship/module materials, or visuals:
|
||||||
- `quartz → control_chip` — skips silicon on the electronics chain.
|
|
||||||
- `iron_ingot → hardened_steel` — a nicer-ratio route past the
|
|
||||||
deliberately awkward hardening step.
|
|
||||||
|
|
||||||
Item count: 3 mined + scrap + 3 smelted + 4 t2 + 5 t3 + 3 t4 (incl.
|
python dota_factory/tools/verify_recipes.py
|
||||||
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.
|
|
||||||
|
|
||||||
### Numbers — first pass
|
It verifies every consumed item has a producer, every item has a visuals
|
||||||
|
entry, flags orphaned items, and prints which items are
|
||||||
These numbers are live in the config files and verified by
|
reprocessing-only (currently exactly voidsteel).
|
||||||
`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 ≈ 0.6 per threat of weapon contribution**, mildly
|
|
||||||
diminishing with gun size, compensated by the range ladder:
|
|
||||||
railgun_s 2 dmg × 2.0 Hz = 4.0 DPS at 6.5 threat (0.62), range 50;
|
|
||||||
railgun_m 17 × 1.5 = 25.5 at 43.5 (0.59), range 70;
|
|
||||||
railgun_l 70 × 0.8 = 56 at 102 (0.55), range 100.
|
|
||||||
- **Hull HP = 15 per threat of hull contribution** (incl. ship base
|
|
||||||
time): drone 60, frigate 300, destroyer 550, cruiser 1500,
|
|
||||||
battlecruiser 2700, battleship 7500, dreadnought/carrier 15500.
|
|
||||||
- **Armor HP = 20 per threat** — a premium over hull HP because armor is
|
|
||||||
pure HP with no capability: armor_plates adds 640 HP at 32 threat.
|
|
||||||
- **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 ≈ 2 HP/s per threat:** repair_tool heals 25 HP × 1 Hz at 13
|
|
||||||
threat. 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. Kept at ×1.2/×1.2/(×1.5 range, ×0.8 rate) deliberately.
|
|
||||||
- **Stations:** a fresh player station (3000 HP, 25 dmg × 1 Hz,
|
|
||||||
range 200) 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.
|
|
||||||
- **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.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
## Asteroid Expansion
|
## 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-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
|
## UI
|
||||||
|
|
||||||
@@ -392,9 +392,10 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
(75% width) (25% width)
|
(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-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-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-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).
|
- 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).
|
||||||
|
|
||||||
@@ -516,13 +517,13 @@ A separate executable target (`balancing`) that links against `lib` but contains
|
|||||||
- REQ-BAL-UI-RELOAD: The "Reload Config" button reloads all config files from disk (`balancing.toml`, `ships.toml`, `stations.toml`), stops any running simulations, and replaces the arena widget list with freshly created widgets from the reloaded config. The button is disabled while any arena simulation is currently running.
|
- REQ-BAL-UI-RELOAD: The "Reload Config" button reloads all config files from disk (`balancing.toml`, `ships.toml`, `stations.toml`), stops any running simulations, and replaces the arena widget list with freshly created widgets from the reloaded config. The button is disabled while any arena simulation is currently running.
|
||||||
- REQ-BAL-UI-START-ALL: The "Start All" button is placed above the scrollable arena list, to the right of the "Reload Config" button. Clicking it starts (or restarts) the simulation for every arena that is not currently running. The button is disabled when all arenas are currently running.
|
- REQ-BAL-UI-START-ALL: The "Start All" button is placed above the scrollable arena list, to the right of the "Reload Config" button. Clicking it starts (or restarts) the simulation for every arena that is not currently running. The button is disabled when all arenas are currently running.
|
||||||
- REQ-BAL-UI-LOG: The "Log" button is placed in the header to the right of the "Start All" button. It is enabled whenever the main window's controls are enabled — including while simulations are running (it captures a live snapshot) — and, like all main window controls, is disabled only while an arena is being inspected (REQ-BAL-UI-WINDOW). Clicking it writes the current state of every arena to a file named `balancing_log.md` in the tool's current working directory, replacing (overwriting) any previous content of that file. The log captures, for every arena in `balancing.toml` order, exactly the information shown in that arena's widget (REQ-BAL-UI-WIDGET) at the moment the button is clicked. Each arena is written as its own section:
|
- REQ-BAL-UI-LOG: The "Log" button is placed in the header to the right of the "Start All" button. It is enabled whenever the main window's controls are enabled — including while simulations are running (it captures a live snapshot) — and, like all main window controls, is disabled only while an arena is being inspected (REQ-BAL-UI-WINDOW). Clicking it writes the current state of every arena to a file named `balancing_log.md` in the tool's current working directory, replacing (overwriting) any previous content of that file. The log captures, for every arena in `balancing.toml` order, exactly the information shown in that arena's widget (REQ-BAL-UI-WIDGET) at the moment the button is clicked. Each arena is written as its own section:
|
||||||
- A heading with the arena name followed by the arena's current state — `not started`, `running`, or `ended` (corresponding to the widget border colors of REQ-BAL-UI-WIDGET-BORDER).
|
- A heading with the arena name followed by the arena's current state — `not started`, `running`, or `ended` (corresponding to the widget border colors of REQ-BAL-UI-WIDGET-BORDER). For an `ended` arena, the heading also includes the battle duration (REQ-BAL-UI-WIDGET), for example `ended, 42.3 s`.
|
||||||
- A markdown table with one column per team (team 1 left, team 2 right). Each team's column header shows the team name — prefixed with `[WON]` when that team won, matching REQ-BAL-UI-WIDGET — the team's accumulated threat level, and the team's remaining EHP percentage (REQ-BAL-UI-WIDGET).
|
- A markdown table with one column per team (team 1 left, team 2 right). Each team's column header shows the team name — prefixed with `[WON]` when that team won, matching REQ-BAL-UI-WIDGET — the team's accumulated threat level, and the team's remaining EHP percentage (REQ-BAL-UI-WIDGET).
|
||||||
- Below the header, each table row holds one of that team's entries, in the same order and text format as the widget (REQ-BAL-UI-WIDGET): the HQ first, then ship types, then defence stations, formatted `surviving/total TypeName` for ship entries and `surviving/total TypeName Llevel` for the HQ and defence station entries. When the two teams have different numbers of entries, the shorter column's remaining cells are left blank.
|
- Below the header, each table row holds one of that team's entries, in the same order and text format as the widget (REQ-BAL-UI-WIDGET): the HQ first, then ship types, then defence stations, formatted `surviving/total TypeName` for ship entries and `surviving/total TypeName Llevel` for the HQ and defence station entries. When the two teams have different numbers of entries, the shorter column's remaining cells are left blank.
|
||||||
- REQ-BAL-UI-WIDGET: Each arena widget displays the arena name, an "Inspect" button (to the right of the arena name), and two columns (one per team). Each column shows the team name as a header, then directly below the header the team's **accumulated threat level** — the sum, across the team's configured ship entries, of each entry's `count` multiplied by the threat cost (REQ-MOD-THREAT) of one ship of that entry computed from its level-independent module layout. Only ships contribute; the HQ and defence stations are excluded. This value is static: it is computed once from the full configured roster and does not change as ships are destroyed during the fight. Directly below the threat level, the column shows the team's **remaining EHP percentage** — the sum of the current HP of all of the team's ships and defence stations, divided by the sum of their maximum HP, expressed as a percentage rounded to a whole number. Maximum HP is the final per-entity maximum (REQ-MOD-STAT-CALC), so module HP bonuses such as armor plates are included. (The game has no damage mitigation, so effective HP equals raw HP.) The HQ is excluded from both sums. A destroyed ship or station contributes 0 to the numerator and its maximum HP to the denominator, so the value measures how much of the team's fielded durability remains: it starts at 100% and decreases as units take damage or are destroyed. Unlike the static threat level, this value updates live as the simulation progresses. If the team has neither ships nor defence stations (the denominator is 0), the percentage is shown as `n/a`. Below the EHP percentage, the column shows a list of entries. The HQ is always the first entry in each column. Below the HQ, ship types are listed, followed by defence stations (if any). Each entry uses the format `surviving/total TypeName` for ship entries and `surviving/total TypeName Llevel` for the HQ and defence station entries — for example `2/3 Fighter`, `1/1 HQ L1`, or `2/2 Enemy Station L3`. The surviving count updates live as the simulation progresses. When the fight ends, the winning team's name header is prefixed with `[WON]`.
|
- REQ-BAL-UI-WIDGET: Each arena widget displays the arena name, an "Inspect" button (to the right of the arena name), and two columns (one per team). Each column shows the team name as a header, then directly below the header the team's **accumulated threat level** — the sum, across the team's configured ship entries, of each entry's `count` multiplied by the threat cost (REQ-MOD-THREAT) of one ship of that entry computed from its level-independent module layout. Only ships contribute; the HQ and defence stations are excluded. This value is static: it is computed once from the full configured roster and does not change as ships are destroyed during the fight. Directly below the threat level, the column shows the team's **remaining EHP percentage** — the sum of the current HP of all of the team's ships and defence stations, divided by the sum of their maximum HP, expressed as a percentage rounded to a whole number. Maximum HP is the final per-entity maximum (REQ-MOD-STAT-CALC), so module HP bonuses such as armor plates are included. (The game has no damage mitigation, so effective HP equals raw HP.) The HQ is excluded from both sums. A destroyed ship or station contributes 0 to the numerator and its maximum HP to the denominator, so the value measures how much of the team's fielded durability remains: it starts at 100% and decreases as units take damage or are destroyed. Unlike the static threat level, this value updates live as the simulation progresses. If the team has neither ships nor defence stations (the denominator is 0), the percentage is shown as `n/a`. Below the EHP percentage, the column shows a list of entries. The HQ is always the first entry in each column. Below the HQ, ship types are listed, followed by defence stations (if any). Each entry uses the format `surviving/total TypeName` for ship entries and `surviving/total TypeName Llevel` for the HQ and defence station entries — for example `2/3 Fighter`, `1/1 HQ L1`, or `2/2 Enemy Station L3`. The surviving count updates live as the simulation progresses. When the fight ends, the winning team's name header is prefixed with `[WON]`. When the fight has ended, the widget also displays the arena's **battle duration** — an arena-level value (not per team) giving the game time the fight lasted, computed as the number of simulated ticks at completion multiplied by the simulation's fixed tick duration (the same tick-based simulation as the main game, REQ-BAL-SIM-ENV). This is game time, not wall-clock time, so it is independent of how fast the arena was simulated (non-inspected arenas run at maximum tick rate, REQ-BAL-SIM-SPEED). It is shown in seconds with one decimal place, for example `Duration: 42.3 s`. The battle duration is shown only for completed (ended) runs; it is not shown while the arena is not started or running.
|
||||||
- REQ-BAL-UI-WIDGET-START: Each arena widget contains a "Start" button that starts the simulation for that arena. The button is disabled while the arena's simulation is running. When a finished arena's Start button is clicked, a fresh simulation is created and started (the widget resets to initial unit counts, the border returns to blue, and the previous results are replaced).
|
- REQ-BAL-UI-WIDGET-START: Each arena widget contains a "Start" button that starts the simulation for that arena. The button is disabled while the arena's simulation is running. When a finished arena's Start button is clicked, a fresh simulation is created and started (the widget resets to initial unit counts, the border returns to blue, and the previous results are replaced).
|
||||||
- REQ-BAL-UI-WIDGET-BORDER: Each arena widget has a colored border indicating its state: grey when not yet started, blue while its simulation is running, and green when the fight has ended.
|
- REQ-BAL-UI-WIDGET-BORDER: Each arena widget has a colored border indicating its state: grey when not yet started, blue while its simulation is running, and green when the fight has ended.
|
||||||
- REQ-BAL-UI-INSPECT: Clicking an arena widget's "Inspect" button opens a new inspect window for that arena. Any previously open inspect window is closed first (its arena's simulation is aborted and its widget border returns to grey). The inspected arena is restarted with a fresh simulation that runs at controllable game speed with full rendering (REQ-BAL-SIM-SPEED). The arena widget updates live during inspection (surviving counts, border color, `[WON]` prefix) as it does for non-inspected arenas. Only one inspect window may be open at a time.
|
- REQ-BAL-UI-INSPECT: Clicking an arena widget's "Inspect" button opens a new inspect window for that arena. Any previously open inspect window is closed first (its arena's simulation is aborted and its widget border returns to grey). The inspected arena is restarted with a fresh simulation that runs at controllable game speed with full rendering (REQ-BAL-SIM-SPEED). The arena widget updates live during inspection (surviving counts, border color, `[WON]` prefix) as it does for non-inspected arenas. Only one inspect window may be open at a time.
|
||||||
- REQ-BAL-UI-INSPECT-WINDOW: The inspect window consists of three sections, top to bottom: a title bar area containing the arena name and game speed controls (same buttons as the main game: 0×, 0.5×, 1×, 2×, 4×, with Space to toggle pause — see REQ-UI-SPEED and REQ-UI-HOTKEYS), the arena view in the center, and an info panel at the bottom displaying the same team columns and entry format as the arena widget in the main window (REQ-BAL-UI-WIDGET), updated live.
|
- REQ-BAL-UI-INSPECT-WINDOW: The inspect window consists of three sections, top to bottom: a title bar area containing the arena name and game speed controls (same buttons as the main game: 0×, 0.5×, 1×, 2×, 4×, with Space to toggle pause — see REQ-UI-SPEED and REQ-UI-HOTKEYS), the arena view in the center, and an info panel at the bottom displaying the same team columns and entry format as the arena widget in the main window (REQ-BAL-UI-WIDGET), updated live, including the arena's battle duration once the fight has ended (REQ-BAL-UI-WIDGET).
|
||||||
- REQ-BAL-UI-INSPECT-VIEW: The arena view renders all tiles of the arena and displays ships, HQs, defence stations, and laser beams using the same visual elements and `visuals.toml` colors as the main game. Team 1 uses player visual styles; team 2 uses enemy visual styles. The view has a fixed zoom level — no zoom or scroll is possible. The tile size is derived so that the full arena (all tiles) fits within the view.
|
- REQ-BAL-UI-INSPECT-VIEW: The arena view renders all tiles of the arena and displays ships, HQs, defence stations, and laser beams using the same visual elements and `visuals.toml` colors as the main game. Team 1 uses player visual styles; team 2 uses enemy visual styles. The view has a fixed zoom level — no zoom or scroll is possible. The tile size is derived so that the full arena (all tiles) fits within the view.
|
||||||
- REQ-BAL-UI-INSPECT-CLOSE: Closing the inspect window (via the window's close button) aborts the inspected arena's simulation. The arena widget's border returns to grey and its surviving counts are left as they were at the moment of closing. All main window buttons and controls are re-enabled.
|
- REQ-BAL-UI-INSPECT-CLOSE: Closing the inspect window (via the window's close button) aborts the inspected arena's simulation. The arena widget's border returns to grey and its surviving counts are left as they were at the moment of closing. All main window buttons and controls are re-enabled.
|
||||||
|
|||||||
@@ -433,10 +433,13 @@ void ArenaSimulation::tickDeaths()
|
|||||||
});
|
});
|
||||||
|
|
||||||
m_admin.forEach<StationBodyComponent, FactionComponent>(
|
m_admin.forEach<StationBodyComponent, FactionComponent>(
|
||||||
[&team1HasUnits, &team2HasUnits](entt::entity /*e*/,
|
[this, &team1HasUnits, &team2HasUnits](entt::entity e,
|
||||||
const StationBodyComponent& /*sb*/,
|
const StationBodyComponent& /*sb*/,
|
||||||
const FactionComponent& f)
|
const FactionComponent& f)
|
||||||
{
|
{
|
||||||
|
// The HQ carries a StationBodyComponent but is not a defence station;
|
||||||
|
// its destruction is a separate end condition (REQ-BAL-SIM-END).
|
||||||
|
if (m_admin.hasAll<HqProxyComponent>(e)) { return; }
|
||||||
if (f.isEnemy) { team2HasUnits = true; }
|
if (f.isEnemy) { team2HasUnits = true; }
|
||||||
else { team1HasUnits = true; }
|
else { team1HasUnits = true; }
|
||||||
});
|
});
|
||||||
@@ -515,6 +518,7 @@ void ArenaSimulation::updateStatus()
|
|||||||
ArenaStatus newStatus;
|
ArenaStatus newStatus;
|
||||||
newStatus.finished = m_finished;
|
newStatus.finished = m_finished;
|
||||||
newStatus.winnerTeam = m_winnerTeam;
|
newStatus.winnerTeam = m_winnerTeam;
|
||||||
|
newStatus.durationSeconds = ticksToSeconds(m_currentTick);
|
||||||
|
|
||||||
// Live remaining HP of each team's ships and defence stations (HQ excluded);
|
// Live remaining HP of each team's ships and defence stations (HQ excluded);
|
||||||
// the EHP-percentage numerator (denominator is the fixed m_teamMaxEhp).
|
// the EHP-percentage numerator (denominator is the fixed m_teamMaxEhp).
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ struct ArenaStatus
|
|||||||
TeamStatus teams[2];
|
TeamStatus teams[2];
|
||||||
bool finished = false;
|
bool finished = false;
|
||||||
int winnerTeam = -1; // 0 or 1 when finished; -1 while running
|
int winnerTeam = -1; // 0 or 1 when finished; -1 while running
|
||||||
|
// Game time the fight has lasted (simulated ticks * fixed tick duration).
|
||||||
|
// Meaningful once finished; the battle duration shown for completed runs.
|
||||||
|
double durationSeconds = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ArenaSimulation
|
class ArenaSimulation
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ void ArenaWidget::buildLayout(const std::string& arenaName)
|
|||||||
m_titleLabel->setFont(titleFont);
|
m_titleLabel->setFont(titleFont);
|
||||||
titleRow->addWidget(m_titleLabel);
|
titleRow->addWidget(m_titleLabel);
|
||||||
|
|
||||||
|
m_durationLabel = new QLabel(this);
|
||||||
|
titleRow->addWidget(m_durationLabel);
|
||||||
|
|
||||||
titleRow->addStretch();
|
titleRow->addStretch();
|
||||||
|
|
||||||
m_inspectButton = new QPushButton(tr("Inspect"), this);
|
m_inspectButton = new QPushButton(tr("Inspect"), this);
|
||||||
@@ -159,6 +162,10 @@ void ArenaWidget::updateStatus(const ArenaStatus& status)
|
|||||||
content->setText(lines);
|
content->setText(lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_durationLabel->setText(status.finished
|
||||||
|
? tr("Duration: %1 s").arg(QString::number(status.durationSeconds, 'f', 1))
|
||||||
|
: QString());
|
||||||
|
|
||||||
if (status.finished && !m_wasFinished)
|
if (status.finished && !m_wasFinished)
|
||||||
{
|
{
|
||||||
m_wasFinished = true;
|
m_wasFinished = true;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ private:
|
|||||||
int m_arenaIndex;
|
int m_arenaIndex;
|
||||||
ArenaStatus m_lastStatus;
|
ArenaStatus m_lastStatus;
|
||||||
QLabel* m_titleLabel;
|
QLabel* m_titleLabel;
|
||||||
|
QLabel* m_durationLabel;
|
||||||
QLabel* m_team1Header;
|
QLabel* m_team1Header;
|
||||||
QLabel* m_team2Header;
|
QLabel* m_team2Header;
|
||||||
QLabel* m_team1Threat;
|
QLabel* m_team1Threat;
|
||||||
|
|||||||
@@ -373,9 +373,17 @@ void BalancingWindow::writeLog()
|
|||||||
for (const ArenaEntry& entry : m_arenas)
|
for (const ArenaEntry& entry : m_arenas)
|
||||||
{
|
{
|
||||||
const ArenaStatus& status = entry.widget->getLastStatus();
|
const ArenaStatus& status = entry.widget->getLastStatus();
|
||||||
|
const ArenaWidget::State state = entry.widget->getState();
|
||||||
|
|
||||||
|
QString stateSuffix = stateText(state);
|
||||||
|
if (state == ArenaWidget::State::Ended)
|
||||||
|
{
|
||||||
|
stateSuffix += QStringLiteral(", %1 s")
|
||||||
|
.arg(QString::number(status.durationSeconds, 'f', 1));
|
||||||
|
}
|
||||||
|
|
||||||
out << "\n## Arena: " << QString::fromStdString(entry.config.name)
|
out << "\n## Arena: " << QString::fromStdString(entry.config.name)
|
||||||
<< " (" << stateText(entry.widget->getState()) << ")\n\n";
|
<< " (" << stateSuffix << ")\n\n";
|
||||||
|
|
||||||
out << "| " << teamHeaderCell(status, 0) << " | "
|
out << "| " << teamHeaderCell(status, 0) << " | "
|
||||||
<< teamHeaderCell(status, 1) << " |\n";
|
<< teamHeaderCell(status, 1) << " |\n";
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const GameConfig* config,
|
|||||||
nameLabel->setFont(nameFont);
|
nameLabel->setFont(nameFont);
|
||||||
headerLayout->addWidget(nameLabel);
|
headerLayout->addWidget(nameLabel);
|
||||||
|
|
||||||
|
m_durationLabel = new QLabel(header);
|
||||||
|
headerLayout->addWidget(m_durationLabel);
|
||||||
|
|
||||||
headerLayout->addStretch();
|
headerLayout->addStretch();
|
||||||
|
|
||||||
const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" };
|
const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" };
|
||||||
@@ -202,6 +205,10 @@ void InspectWindow::pollStatus()
|
|||||||
|
|
||||||
void InspectWindow::updateInfoPanel(const ArenaStatus& status)
|
void InspectWindow::updateInfoPanel(const ArenaStatus& status)
|
||||||
{
|
{
|
||||||
|
m_durationLabel->setText(status.finished
|
||||||
|
? tr("Duration: %1 s").arg(QString::number(status.durationSeconds, 'f', 1))
|
||||||
|
: QString());
|
||||||
|
|
||||||
for (int ti = 0; ti < 2; ++ti)
|
for (int ti = 0; ti < 2; ++ti)
|
||||||
{
|
{
|
||||||
const ArenaStatus::TeamStatus& team = status.teams[ti];
|
const ArenaStatus::TeamStatus& team = status.teams[ti];
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ private:
|
|||||||
ArenaView* m_arenaView;
|
ArenaView* m_arenaView;
|
||||||
|
|
||||||
std::vector<QPushButton*> m_speedButtons;
|
std::vector<QPushButton*> m_speedButtons;
|
||||||
|
QLabel* m_durationLabel;
|
||||||
QLabel* m_team1Header;
|
QLabel* m_team1Header;
|
||||||
QLabel* m_team2Header;
|
QLabel* m_team2Header;
|
||||||
QLabel* m_team1Threat;
|
QLabel* m_team1Threat;
|
||||||
|
|||||||
@@ -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.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.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.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.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.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");
|
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).
|
// Asteroid expansion (REQ-EXP-UNLOCK, REQ-EXP-COST).
|
||||||
struct WorldExpansion
|
struct WorldExpansion
|
||||||
{
|
{
|
||||||
int columnsPerExpansion_tiles;
|
int columnsPerExpansion_tiles;
|
||||||
int costBuildingBlocks;
|
Formula costBuildingBlocksFormula; // cost in building blocks; x = expansions already purchased
|
||||||
};
|
};
|
||||||
|
|
||||||
// Push effects (REQ-PSH-*, REQ-WAV-BOSS-ADVANCE).
|
// Push effects (REQ-PSH-*, REQ-WAV-BOSS-ADVANCE).
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/TracePrintRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/TracePrintRequestedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/TickAdvancedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/TickAdvancedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBlocksChangedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBlocksChangedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ExpansionCostChangedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.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_spawnShip(std::move(spawnShip))
|
||||||
, m_isItemUnlocked(std::move(isItemUnlocked))
|
, m_isItemUnlocked(std::move(isItemUnlocked))
|
||||||
, m_rng(rng)
|
, 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
|
QPoint anchor) const
|
||||||
{
|
{
|
||||||
const int heightTiles = m_config.world.heightTiles;
|
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)
|
for (const QPoint& cell : bodyCells)
|
||||||
{
|
{
|
||||||
const QPoint worldCell = anchor + cell;
|
const QPoint worldCell = anchor + cell;
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ public:
|
|||||||
bool isPlacementValid(BuildingType type, QPoint anchor,
|
bool isPlacementValid(BuildingType type, QPoint anchor,
|
||||||
Rotation rotation) const;
|
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
|
// Remove a building or construction site by id. Returns the refund in
|
||||||
// building blocks (floor(cost * refundPercentage / 100)). Returns 0 for
|
// building blocks (floor(cost * refundPercentage / 100)). Returns 0 for
|
||||||
// unknown ids.
|
// unknown ids.
|
||||||
@@ -179,6 +184,7 @@ private:
|
|||||||
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
||||||
std::function<bool(const std::string&)> m_isItemUnlocked;
|
std::function<bool(const std::string&)> m_isItemUnlocked;
|
||||||
std::mt19937& m_rng;
|
std::mt19937& m_rng;
|
||||||
|
int m_asteroidWidth_tiles;
|
||||||
|
|
||||||
std::vector<Building> m_buildings;
|
std::vector<Building> m_buildings;
|
||||||
std::deque<ConstructionSite> m_constructionQueue;
|
std::deque<ConstructionSite> m_constructionQueue;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ enum class CommandKind
|
|||||||
SetSplitterFilters,
|
SetSplitterFilters,
|
||||||
ClearBeltTiles,
|
ClearBeltTiles,
|
||||||
ApplySchematicChoice,
|
ApplySchematicChoice,
|
||||||
|
ExpandAsteroid,
|
||||||
Reset
|
Reset
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,6 +130,14 @@ struct ApplySchematicChoiceCommand : Command
|
|||||||
int choiceIndex = 0;
|
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
|
// 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
|
// 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
|
// shared_ptr and moved into the sim on apply). A null config keeps the current
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
#include "AiSystem.h"
|
#include "AiSystem.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
@@ -132,6 +133,7 @@ void Simulation::reset(unsigned int seed)
|
|||||||
m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds);
|
m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds);
|
||||||
m_nextBuildingId = 1;
|
m_nextBuildingId = 1;
|
||||||
m_buildingBlocksStock = m_config.world.startingBuildingBlocks;
|
m_buildingBlocksStock = m_config.world.startingBuildingBlocks;
|
||||||
|
m_expansionsPurchased = 0;
|
||||||
m_gameOver = false;
|
m_gameOver = false;
|
||||||
m_isWon = false;
|
m_isWon = false;
|
||||||
m_artifactCount = 0;
|
m_artifactCount = 0;
|
||||||
@@ -275,6 +277,9 @@ void Simulation::apply(const Command& command)
|
|||||||
case CommandKind::ApplySchematicChoice:
|
case CommandKind::ApplySchematicChoice:
|
||||||
applySchematicChoice(static_cast<const ApplySchematicChoiceCommand&>(command).choiceIndex);
|
applySchematicChoice(static_cast<const ApplySchematicChoiceCommand&>(command).choiceIndex);
|
||||||
break;
|
break;
|
||||||
|
case CommandKind::ExpandAsteroid:
|
||||||
|
tryExpandAsteroid();
|
||||||
|
break;
|
||||||
case CommandKind::Reset:
|
case CommandKind::Reset:
|
||||||
{
|
{
|
||||||
const ResetCommand& c = static_cast<const ResetCommand&>(command);
|
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_gameOver);
|
||||||
hasher.append(m_isWon);
|
hasher.append(m_isWon);
|
||||||
hasher.append(m_artifactCount);
|
hasher.append(m_artifactCount);
|
||||||
|
hasher.append(m_expansionsPurchased);
|
||||||
|
|
||||||
// WaveSystem scalar state, reached through existing accessors.
|
// WaveSystem scalar state, reached through existing accessors.
|
||||||
hasher.append(threatLevel());
|
hasher.append(threatLevel());
|
||||||
@@ -1100,6 +1106,31 @@ int Simulation::buildingBlocksStock() const
|
|||||||
return m_buildingBlocksStock;
|
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
|
bool Simulation::isGameOver() const
|
||||||
{
|
{
|
||||||
return m_gameOver;
|
return m_gameOver;
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ public:
|
|||||||
// The seed this run was (re)initialized with; written to the replay header.
|
// The seed this run was (re)initialized with; written to the replay header.
|
||||||
unsigned int getSeed() const;
|
unsigned int getSeed() const;
|
||||||
int buildingBlocksStock() 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 isGameOver() const;
|
||||||
bool isWon() const;
|
bool isWon() const;
|
||||||
int artifactCount() const;
|
int artifactCount() const;
|
||||||
@@ -136,6 +142,11 @@ private:
|
|||||||
// Clears the pending choices after application.
|
// Clears the pending choices after application.
|
||||||
void applySchematicChoice(int choiceIndex);
|
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.
|
// Mutable subsystem accessors; same chokepoint rule as the mutators above.
|
||||||
BuildingSystem& buildingsMutable();
|
BuildingSystem& buildingsMutable();
|
||||||
BeltSystem& beltsMutable();
|
BeltSystem& beltsMutable();
|
||||||
@@ -165,6 +176,7 @@ private:
|
|||||||
Tick m_nextDepartureTick;
|
Tick m_nextDepartureTick;
|
||||||
BuildingId m_nextBuildingId;
|
BuildingId m_nextBuildingId;
|
||||||
int m_buildingBlocksStock;
|
int m_buildingBlocksStock;
|
||||||
|
int m_expansionsPurchased = 0; // REQ-EXP-COST formula variable x
|
||||||
bool m_gameOver = false;
|
bool m_gameOver = false;
|
||||||
bool m_isWon = false;
|
bool m_isWon = false;
|
||||||
int m_artifactCount = 0;
|
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.playerBufferWidth_tiles == 10);
|
||||||
REQUIRE(cfg.world.regions.enemyBufferWidth_tiles == 15);
|
REQUIRE(cfg.world.regions.enemyBufferWidth_tiles == 15);
|
||||||
REQUIRE(cfg.world.expansion.columnsPerExpansion_tiles == 10);
|
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.push.bossAdvanceSeconds == Approx(60.0));
|
||||||
REQUIRE(cfg.world.orbitFactor == Approx(0.8));
|
REQUIRE(cfg.world.orbitFactor == Approx(0.8));
|
||||||
REQUIRE(cfg.world.rallyOrbitRadius_tiles == Approx(5.0));
|
REQUIRE(cfg.world.rallyOrbitRadius_tiles == Approx(5.0));
|
||||||
@@ -186,7 +188,7 @@ contest_zone_width_tiles = 30
|
|||||||
|
|
||||||
[expansion]
|
[expansion]
|
||||||
columns_per_expansion_tiles = 10
|
columns_per_expansion_tiles = 10
|
||||||
cost_building_blocks = 200
|
cost_building_blocks_formula = "400 * 2^x"
|
||||||
|
|
||||||
[push]
|
[push]
|
||||||
push_expand_columns_tiles = 20
|
push_expand_columns_tiles = 20
|
||||||
@@ -237,7 +239,7 @@ enemy_buffer_width_tiles = 15
|
|||||||
|
|
||||||
[expansion]
|
[expansion]
|
||||||
columns_per_expansion_tiles = 10
|
columns_per_expansion_tiles = 10
|
||||||
cost_building_blocks = 200
|
cost_building_blocks_formula = "400 * 2^x"
|
||||||
|
|
||||||
[push]
|
[push]
|
||||||
push_expand_columns_tiles = 20
|
push_expand_columns_tiles = 20
|
||||||
@@ -284,7 +286,7 @@ enemy_buffer_width_tiles = 15
|
|||||||
|
|
||||||
[expansion]
|
[expansion]
|
||||||
columns_per_expansion_tiles = 10
|
columns_per_expansion_tiles = 10
|
||||||
cost_building_blocks = 200
|
cost_building_blocks_formula = "400 * 2^x"
|
||||||
|
|
||||||
[push]
|
[push]
|
||||||
push_expand_columns_tiles = 20
|
push_expand_columns_tiles = 20
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
#include "BuilderModeExitedEvent.h"
|
#include "BuilderModeExitedEvent.h"
|
||||||
#include "BlueprintModeExitedEvent.h"
|
#include "BlueprintModeExitedEvent.h"
|
||||||
#include "BuildingBlocksChangedEvent.h"
|
#include "BuildingBlocksChangedEvent.h"
|
||||||
|
#include "ExpansionCostChangedEvent.h"
|
||||||
#include "GameSpeedChangedEvent.h"
|
#include "GameSpeedChangedEvent.h"
|
||||||
#include "SchematicChoicesAvailableEvent.h"
|
#include "SchematicChoicesAvailableEvent.h"
|
||||||
#include "TickAdvancedEvent.h"
|
#include "TickAdvancedEvent.h"
|
||||||
@@ -276,6 +277,7 @@ void GameWorldView::onFrame()
|
|||||||
{
|
{
|
||||||
const Tick newTick = m_sim->currentTick();
|
const Tick newTick = m_sim->currentTick();
|
||||||
const int newBlocks = m_sim->buildingBlocksStock();
|
const int newBlocks = m_sim->buildingBlocksStock();
|
||||||
|
const int newExpCost = m_sim->currentExpansionCost();
|
||||||
const int newBoss = m_sim->bossWaveCounter();
|
const int newBoss = m_sim->bossWaveCounter();
|
||||||
const Tick newCountdown = m_sim->bossCountdownTicks();
|
const Tick newCountdown = m_sim->bossCountdownTicks();
|
||||||
|
|
||||||
@@ -291,6 +293,12 @@ void GameWorldView::onFrame()
|
|||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<BuildingBlocksChangedEvent>(newBlocks));
|
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)
|
if (newBoss != m_lastBossCounter || newCountdown != m_lastBossCountdown)
|
||||||
{
|
{
|
||||||
m_lastBossCounter = newBoss;
|
m_lastBossCounter = newBoss;
|
||||||
@@ -435,7 +443,7 @@ QRect GameWorldView::viewportRect() const
|
|||||||
|
|
||||||
float GameWorldView::asteroidLeftEdge() 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 Building& b : m_sim->buildings().allBuildings())
|
||||||
{
|
{
|
||||||
for (const QPoint& cell : b.bodyCells)
|
for (const QPoint& cell : b.bodyCells)
|
||||||
@@ -1827,6 +1835,7 @@ void GameWorldView::resetForNewGame()
|
|||||||
m_prevNonZeroSpeed = 1.0;
|
m_prevNonZeroSpeed = 1.0;
|
||||||
m_lastTick = Tick(-1);
|
m_lastTick = Tick(-1);
|
||||||
m_lastBlocks = -1;
|
m_lastBlocks = -1;
|
||||||
|
m_lastExpansionCost = -1;
|
||||||
m_lastBossCounter = -1;
|
m_lastBossCounter = -1;
|
||||||
m_lastBossCountdown = Tick(-1);
|
m_lastBossCountdown = Tick(-1);
|
||||||
m_lastArtifactCount = -1;
|
m_lastArtifactCount = -1;
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ private:
|
|||||||
|
|
||||||
Tick m_lastTick = Tick(-1);
|
Tick m_lastTick = Tick(-1);
|
||||||
int m_lastBlocks = -1;
|
int m_lastBlocks = -1;
|
||||||
|
int m_lastExpansionCost = -1;
|
||||||
int m_lastBossCounter = -1;
|
int m_lastBossCounter = -1;
|
||||||
Tick m_lastBossCountdown = Tick(-1);
|
Tick m_lastBossCountdown = Tick(-1);
|
||||||
int m_lastArtifactCount = -1;
|
int m_lastArtifactCount = -1;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QSignalMapper>
|
#include <QSignalMapper>
|
||||||
|
|
||||||
|
#include "Command.h"
|
||||||
|
#include "CommandRequestedEvent.h"
|
||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
#include "SpeedChangeRequestedEvent.h"
|
#include "SpeedChangeRequestedEvent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
@@ -32,6 +34,17 @@ HeaderBar::HeaderBar(QWidget* parent)
|
|||||||
layout->addStretch();
|
layout->addStretch();
|
||||||
layout->addWidget(m_bossLabel);
|
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" };
|
const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" };
|
||||||
QSignalMapper* mapper = new QSignalMapper(this);
|
QSignalMapper* mapper = new QSignalMapper(this);
|
||||||
for (int i = 0; i < kSpeedCount; ++i)
|
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)
|
void HeaderBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
|
||||||
{
|
{
|
||||||
|
m_blocks = event->blocks;
|
||||||
m_blocksLabel->setText(tr("Blocks: %1").arg(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)
|
void HeaderBar::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "BossWaveUpdatedEvent.h"
|
#include "BossWaveUpdatedEvent.h"
|
||||||
#include "BuildingBlocksChangedEvent.h"
|
#include "BuildingBlocksChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
|
#include "ExpansionCostChangedEvent.h"
|
||||||
#include "GameSpeedChangedEvent.h"
|
#include "GameSpeedChangedEvent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "TickAdvancedEvent.h"
|
#include "TickAdvancedEvent.h"
|
||||||
@@ -18,6 +19,7 @@ class QPushButton;
|
|||||||
class HeaderBar : public QWidget,
|
class HeaderBar : public QWidget,
|
||||||
public CombinedEventHandler<TickAdvancedEvent,
|
public CombinedEventHandler<TickAdvancedEvent,
|
||||||
BuildingBlocksChangedEvent,
|
BuildingBlocksChangedEvent,
|
||||||
|
ExpansionCostChangedEvent,
|
||||||
GameSpeedChangedEvent,
|
GameSpeedChangedEvent,
|
||||||
BossWaveUpdatedEvent,
|
BossWaveUpdatedEvent,
|
||||||
ArtifactCountChangedEvent>
|
ArtifactCountChangedEvent>
|
||||||
@@ -34,16 +36,25 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> 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 GameSpeedChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const BossWaveUpdatedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BossWaveUpdatedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const ArtifactCountChangedEvent> 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_timeLabel;
|
||||||
QLabel* m_blocksLabel;
|
QLabel* m_blocksLabel;
|
||||||
QLabel* m_artifactsLabel;
|
QLabel* m_artifactsLabel;
|
||||||
QLabel* m_bossLabel;
|
QLabel* m_bossLabel;
|
||||||
|
QPushButton* m_expandButton;
|
||||||
std::vector<QPushButton*> m_speedButtons;
|
std::vector<QPushButton*> m_speedButtons;
|
||||||
|
|
||||||
|
int m_blocks = 0;
|
||||||
|
int m_expansionCost = 0;
|
||||||
|
|
||||||
static const double kSpeeds[];
|
static const double kSpeeds[];
|
||||||
static const int kSpeedCount;
|
static const int kSpeedCount;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user