Files
transcience/tests/unit/test_regen.gd
T
claude cb2c1e7840 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>
2026-09-06 16:16:38 +02:00

99 lines
2.9 KiB
GDScript

extends GutTest
## Passive health regeneration.
var world: SimWorld
const PEER := 4
func before_each() -> void:
world = SimWorld.new(1)
world.add_player(PEER, "tester")
func _player() -> SimPlayer:
return world.players[PEER]
func test_it_heals_the_configured_share_of_maximum_per_second() -> void:
var p := _player()
p.hp = 1
for _i in SimConfig.TICK_RATE:
world.step()
var expected := float(p.max_hp) * SimConfig.HP_REGEN_PERCENT_PER_SEC / 100.0
assert_almost_eq(float(p.hp - 1), expected, 1.0,
"one second should restore %.1f%% of maximum" % SimConfig.HP_REGEN_PERCENT_PER_SEC)
## The reason for the fractional carry: at this rate a tick heals far less than
## one hit point, so truncating every tick would heal exactly nothing forever.
func test_fractions_accumulate_rather_than_being_lost() -> void:
var p := _player()
p.hp = 1
var per_tick := float(p.max_hp) * (SimConfig.HP_REGEN_PERCENT_PER_SEC / 100.0) \
* SimConfig.TICK_DELTA
assert_lt(per_tick, 1.0, "setup: a single tick must heal less than one hp")
for _i in SimConfig.TICK_RATE * 10:
world.step()
assert_gt(p.hp, 1, "ten seconds of sub-integer healing must still add up")
func test_it_scales_with_maximum_health_so_levels_do_not_dilute_it() -> void:
var low := _player()
# Taken from the curve rather than written down, so a rescale of the health
# numbers cannot leave this test comparing two arbitrary constants.
low.max_hp = Progression.max_hp_for_level(Progression.START_LEVEL)
low.hp = 1
for _i in SimConfig.TICK_RATE:
world.step()
var low_gain := low.hp - 1
var world2 := SimWorld.new(1)
var high := world2.add_player(PEER, "tester")
high.max_hp = Progression.max_hp_for_level(Progression.MAX_LEVEL)
high.hp = 1
for _i in SimConfig.TICK_RATE:
world2.step()
assert_gt(high.hp - 1, low_gain,
"a levelled character should regain more per second, not the same")
func test_it_never_exceeds_maximum() -> void:
var p := _player()
p.hp = p.max_hp - 1
for _i in SimConfig.TICK_RATE * 30:
world.step()
assert_eq(p.hp, p.max_hp)
func test_the_dead_do_not_heal() -> void:
var p := _player()
p.alive = false
p.hp = 0
for _i in SimConfig.TICK_RATE * 10:
world.step()
assert_eq(p.hp, 0, "regeneration must not quietly revive a downed player")
## A replica never heals anyone: health, like damage, is the server's to decide.
func test_a_replica_world_does_not_regenerate() -> void:
world.authoritative = false
var p := _player()
p.hp = 1
for _i in SimConfig.TICK_RATE * 5:
world.step()
assert_eq(p.hp, 1)
## Slow enough that it cannot out-heal being shot, which is what makes an
## out-of-combat gate unnecessary.
func test_it_cannot_outpace_incoming_fire() -> void:
var p := _player()
p.pos = Vector2.ZERO
p.spawn_grace = 0
var start := p.hp
for _i in 60:
world.pool.spawn(Vector2.ZERO, Vector2.ZERO, 6.0, 60, 5,
SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB)
world.step()
assert_lt(p.hp, start, "standing in fire must still lose health")