Files
transcience/tests/unit/test_content.gd
T
claude e1f9fa8096
ci / verify (push) Successful in 45s
Unlock menu after failed join; remove contact damage; cover clean disconnects
A failed connection left the connect buttons disabled forever. _show_menu()
returned early when a menu already existed, so the set_busy(false) that
re-enables them never ran on the way back from _abort_connect -- the player was
locked out of both joining and hosting with no way out but a restart. Also
guards against a second attempt stacking on an in-flight one.

Contact damage is gone as a concept: nothing in this game hurts you by touching
it, because every threat should be a bullet you can see and dodge. The Stalker
walked at you and dealt contact damage and nothing else, so it now carries a
point-blank shotgun instead -- five pellets, 62 degrees, 18-tick lifetime for
about 78px of reach, so it still has to close and leaves nothing lingering.
tests/unit/test_content.gd asserts every hostile enemy has an emitter, so a new
enemy cannot quietly reintroduce the mechanic.

The menu's "Disconnect" DOES get the anti-cheese protection -- verified, not
assumed. It calls Net.shutdown(), the socket closes, and the server takes the
same linkdead path as a SIGKILL, because the channel is keyed on the socket
closing rather than on how it closed. Made permanent: a --leave-after bot flag
and two smoke assertions covering the polite exit alongside the hard kill, so a
future "clean leave" message that skipped the channel would fail a test. The
menu now says so out loud in a dungeon -- the mechanic only reads as fair if
the cost is known before clicking.

103 tests, 12 smoke assertions; check.sh, test.sh and smoke.sh all pass.
2026-09-03 19:37:26 +02:00

79 lines
2.7 KiB
GDScript

extends GutTest
## Invariants every piece of content has to satisfy. These are cheap to write
## and they are what stops a new enemy from quietly reintroducing a mechanic the
## game has decided against.
const HOSTILES := [
Content.ENEMY_DRIFTER,
Content.ENEMY_TURRET,
Content.ENEMY_STALKER,
]
## The rule: nothing in this game hurts you by touching you. Every threat is a
## bullet you can see and dodge, so an enemy with no emitters is either inert
## scenery or a design mistake.
func test_every_hostile_enemy_actually_fires_something() -> void:
for id in HOSTILES:
var def := Content.enemy(id)
assert_gt(def.emitters.size(), 0,
"%s has no emitters -- it would be a harmless obstacle" % id)
func test_the_practice_dummy_is_inert() -> void:
var def := Content.enemy(Content.ENEMY_DUMMY)
assert_eq(def.emitters.size(), 0, "the hub target must never shoot back")
## Standing inside an enemy costs nothing by itself. Uses the dummy so the test
## measures contact alone, with no bullets in play to confuse the result.
func test_touching_an_enemy_deals_no_damage() -> void:
var world := SimWorld.new(1)
var p := world.add_player(1, "tester")
var e := world.spawn_enemy(Content.dummy(), Vector2.ZERO)
p.pos = e.pos
p.spawn_grace = 0
for _i in 120:
world.step()
assert_eq(p.hp, SimConfig.PLAYER_MAX_HP,
"contact damage is not a damage type in this game")
## The Stalker's burst replaces its old contact damage, so it has to be a real
## threat once it closes -- and harmless from across the arena.
func test_the_stalker_threatens_at_point_blank_only() -> void:
var near := SimWorld.new(1)
var pn := near.add_player(1, "tester")
pn.pos = Vector2.ZERO
pn.spawn_grace = 0
near.spawn_enemy(Content.stalker(), Vector2(40.0, 0.0))
for _i in 90:
near.step()
assert_lt(pn.hp, SimConfig.PLAYER_MAX_HP, "point blank has to hurt")
var far := SimWorld.new(1)
var pf := far.add_player(1, "tester")
pf.pos = Vector2(0.0, 300.0)
pf.spawn_grace = 0
# Pinned in place so it cannot close the distance during the test.
var e := far.spawn_enemy(Content.stalker(), Vector2(0.0, -300.0))
e.def = Content.stalker()
e.def.speed = 0.0
for _i in 90:
far.step()
assert_eq(pf.hp, SimConfig.PLAYER_MAX_HP,
"its bullets must expire long before they cross the arena")
func test_enemy_bullets_from_the_stalker_do_not_litter_the_arena() -> void:
var world := SimWorld.new(1)
var p := world.add_player(1, "tester")
p.pos = Vector2(0.0, 300.0)
var e := world.spawn_enemy(Content.stalker(), Vector2(0.0, -300.0))
e.def = Content.stalker()
e.def.speed = 0.0
for _i in 300:
world.step()
assert_lt(world.pool.live_count, 12,
"a short lifetime should keep spent point-blank shots from accumulating")