Scale health and damage by ten so percentages have somewhere to land

A shot is 60, a fresh character has 1000, the Warden has 36000. Enemy health,
boss health and every emitter's damage were scaled with them, so every ratio
is unchanged and time to kill is exactly what it was -- tests/unit/test_content
now pins shots-to-kill per enemy and hits-to-kill a player so a careless edit
to either side shows up as a number a designer recognises.

The reason is rounding rather than balance. Damage is an integer, and at a base
of 6 the +5% every upgrade carries computed to 6.3 and rounded straight back to
6: a player took their first upgrade, was told it made them stronger, and
nothing happened. diag_upgrades used to print "damage matches the formula
(6 -> 6)"; it now prints (60 -> 63). The boss's per-phase armour multiplier had
the same problem, turning 1.15 into an effective 1.17.

The practice dummy stopped relying on a huge health pool at the same time. Its
old 100000 was already past the u16 the snapshot sends enemy health in, and
once a shot did 60 a patient player could have destroyed the hub's only
practice target for everyone until the next restart. EnemyDef.indestructible
says what was actually meant, and a test asserts nothing else uses it.

check.sh clean, 364 tests, SMOKE PASS, all four diagnostics green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5 committed 2026-09-06 16:16:38 +02:00
1 parent b351bc2d55
commit cb2c1e7840
14 files changed
+185 -54

No files matched your search

+25 -19
View File
@@ -43,7 +43,7 @@ static func drifter() -> EnemyDef:
var d := EnemyDef.new()
d.id = ENEMY_DRIFTER
d.display_name = "Drifter"
d.max_hp = 40
d.max_hp = 400
d.radius = 14.0
d.move = EnemyDef.Move.DRIFT
d.speed = 55.0
@@ -57,7 +57,7 @@ static func drifter() -> EnemyDef:
fan.spread_deg = 20.0
fan.speed = 150.0
fan.radius = 6.0
fan.damage = 10
fan.damage = 100
fan.lifetime = 240
fan.kind = SimConfig.KIND_ORB
d.emitters = [fan]
@@ -70,7 +70,7 @@ static func turret() -> EnemyDef:
var d := EnemyDef.new()
d.id = ENEMY_TURRET
d.display_name = "Turret"
d.max_hp = 70
d.max_hp = 700
d.radius = 16.0
d.move = EnemyDef.Move.STATIC
d.speed = 0.0
@@ -84,7 +84,7 @@ static func turret() -> EnemyDef:
ring.spin_per_shot_deg = 18.0
ring.speed = 130.0
ring.radius = 7.0
ring.damage = 12
ring.damage = 120
ring.lifetime = 300
ring.kind = SimConfig.KIND_ORB
d.emitters = [ring]
@@ -104,7 +104,7 @@ static func stalker() -> EnemyDef:
var d := EnemyDef.new()
d.id = ENEMY_STALKER
d.display_name = "Stalker"
d.max_hp = 30
d.max_hp = 300
d.radius = 12.0
d.move = EnemyDef.Move.APPROACH
d.speed = 95.0
@@ -119,7 +119,7 @@ static func stalker() -> EnemyDef:
lunge.spread_deg = 62.0
lunge.speed = 260.0
lunge.radius = 6.0
lunge.damage = 14
lunge.damage = 140
# 18 ticks at 260 u/s is about 78px of reach -- shorter than the muzzle-to-
# player distance at any range you would call "not point blank".
lunge.lifetime = 18
@@ -130,12 +130,18 @@ static func stalker() -> EnemyDef:
return d
## Lobby target dummy: inert, tough, so players can feel out the gun.
## Lobby target dummy: inert, indestructible, so players can feel out the gun.
##
## Indestructible by flag rather than by a huge health pool. The old 100000 was
## already past the u16 the snapshot sends enemy health in, and after the x10
## rescale a determined player could have destroyed the hub's only practice
## target for everyone until the server restarted.
static func dummy() -> EnemyDef:
var d := EnemyDef.new()
d.id = ENEMY_DUMMY
d.display_name = "Target Dummy"
d.max_hp = 100000
d.max_hp = 1000
d.indestructible = true
d.radius = 20.0
d.move = EnemyDef.Move.STATIC
d.visual = 3
@@ -151,7 +157,7 @@ static func warden() -> BossDef:
var b := BossDef.new()
b.id = BOSS_WARDEN
b.display_name = "Warden of the Fold"
b.max_hp = 3600
b.max_hp = 36000
b.radius = 42.0
b.stationary = true
b.spawn_pos = Vector2(0.0, -150.0)
@@ -182,7 +188,7 @@ static func _warden_p1() -> BossPhase:
ring.spin_per_shot_deg = 9.0
ring.speed = 135.0
ring.radius = 8.0
ring.damage = 14
ring.damage = 140
ring.lifetime = 420
ring.muzzle_offset = 46.0
@@ -193,7 +199,7 @@ static func _warden_p1() -> BossPhase:
fan.spread_deg = 26.0
fan.speed = 210.0
fan.radius = 6.0
fan.damage = 12
fan.damage = 120
fan.kind = SimConfig.KIND_NEEDLE
fan.muzzle_offset = 46.0
@@ -216,7 +222,7 @@ static func _warden_p2() -> BossPhase:
spiral.spin_per_shot_deg = 23.0
spiral.speed = 120.0
spiral.radius = 7.0
spiral.damage = 12
spiral.damage = 120
spiral.lifetime = 480
spiral.muzzle_offset = 46.0
@@ -229,7 +235,7 @@ static func _warden_p2() -> BossPhase:
wall.gap_step = 7
wall.speed = 175.0
wall.radius = 8.0
wall.damage = 16
wall.damage = 160
wall.lifetime = 300
wall.kind = SimConfig.KIND_HEAVY
@@ -256,7 +262,7 @@ static func _warden_p3() -> BossPhase:
arms.sweep_period = 5.0
arms.speed = 165.0
arms.radius = 7.0
arms.damage = 13
arms.damage = 130
arms.lifetime = 400
var fan := AimedSpreadEmitter.new()
@@ -268,7 +274,7 @@ static func _warden_p3() -> BossPhase:
fan.speed = 195.0
fan.edge_speed_bonus = 0.25
fan.radius = 6.0
fan.damage = 12
fan.damage = 120
fan.kind = SimConfig.KIND_NEEDLE
var counter_ring := RingEmitter.new()
@@ -279,7 +285,7 @@ static func _warden_p3() -> BossPhase:
counter_ring.spin_per_shot_deg = -14.0
counter_ring.speed = 105.0
counter_ring.radius = 7.0
counter_ring.damage = 12
counter_ring.damage = 120
counter_ring.lifetime = 420
p.emitters = [arms, fan, counter_ring]
@@ -303,7 +309,7 @@ static func _warden_p4() -> BossPhase:
curve.speed = 130.0
curve.turn_deg = 0.55
curve.radius = 7.0
curve.damage = 15
curve.damage = 150
curve.lifetime = 400
curve.muzzle_offset = 46.0
@@ -316,7 +322,7 @@ static func _warden_p4() -> BossPhase:
walls.gap_step = 5
walls.speed = 200.0
walls.radius = 8.0
walls.damage = 18
walls.damage = 180
walls.kind = SimConfig.KIND_HEAVY
var snipe := AimedSpreadEmitter.new()
@@ -326,7 +332,7 @@ static func _warden_p4() -> BossPhase:
snipe.spread_deg = 10.0
snipe.speed = 300.0
snipe.radius = 5.0
snipe.damage = 16
snipe.damage = 160
snipe.kind = SimConfig.KIND_NEEDLE
p.emitters = [curve, walls, snipe]