1dc1952a3c
ci / verify (push) Successful in 45s
(1) Bullets appeared to trail the ship. Two independent causes, measured with
the new tools/diag_prediction.gd rather than guessed at:
- ServerRuntime ticked before ClientRuntime, so input sampled on frame N was
not consumed until frame N+1, leaving the drawn ship a constant one tick
(4.00px at 240 u/s) ahead of the authoritative one that bullets spawn from.
ClientRuntime now sets process_physics_priority = -10. Gap on a listen
server: 4.00px -> 0.10px mean, 0.30px worst.
- PLAYER_MUZZLE_OFFSET was PLAYER_RADIUS + 6 = 12px against a 13px drawn
ship, so bullets were born inside the sprite. Regression from the previous
commit's hitbox shrink; it now derives from PLAYER_VISUAL_RADIUS.
(2) No more timed respawn. A downed player stays down until they ask for the
hub (E), which is an ordinary input -- the server has no "revive me" message.
(3) Escape channel 3s -> 1s, and damage no longer cancels it. An interruptible
channel makes killing the process strictly better than using the button, so a
dropped connection now runs the same channel: the player stays in the world as
linkdead, still killable, and is only released once it completes. Instances
refuse to close while a linkdead body is resolving, or a solo drop would delete
it on the next tick and hand the exploit straight back.
(4) Escape opens an in-game menu: return to hub (routed through the same held-
escape channel, not a new message), disconnect, quit.
(5) Server pushes a roster so the hub shows who is online and which dungeon
they are in. Entering a dungeon grants 2s arrival protection -- invulnerable
AND weapons-cold, since invulnerability alone would make the spawn a free
firing position -- flagged in the snapshot and drawn on every protected ship.
(6) Cleared dungeons hold the party 30s (was 5s) with a visible countdown.
(7) The hub's grey circle was a 100k-HP target dummy that read as scenery. Now
drawn as a bullseye so its purpose is legible.
Protocol version 1 -> 2. 91 tests (was 78); smoke.sh gains a bot that is
SIGKILLed mid-dungeon to prove the disconnect path end to end. check.sh,
test.sh and smoke.sh all pass.
114 lines
3.6 KiB
GDScript
114 lines
3.6 KiB
GDScript
extends GutTest
|
|
## Progression through a whole dungeon instance: forming, two trash waves, the
|
|
## boss, and the cleared state that sends the party home.
|
|
|
|
var inst: Instance
|
|
|
|
|
|
func before_each() -> void:
|
|
inst = Instance.make_dungeon(2, 12345)
|
|
inst.add_peer(1, "tester")
|
|
|
|
|
|
func _step(n: int) -> void:
|
|
for _i in n:
|
|
inst.step()
|
|
|
|
|
|
func _kill_all_enemies() -> void:
|
|
for e in inst.world.enemies.values():
|
|
e.alive = false
|
|
|
|
|
|
func test_a_forming_dungeon_locks_after_the_window() -> void:
|
|
assert_eq(inst.state, Instance.State.FORMING)
|
|
_step(SimConfig.DUNGEON_FORMING_TICKS + 2)
|
|
assert_eq(inst.state, Instance.State.ACTIVE)
|
|
|
|
|
|
func test_a_full_party_locks_the_dungeon_immediately() -> void:
|
|
for peer in range(2, 2 + SimConfig.DUNGEON_PARTY_MAX - 1):
|
|
inst.add_peer(peer, "p%d" % peer)
|
|
_step(2)
|
|
assert_eq(inst.state, Instance.State.ACTIVE)
|
|
assert_false(inst.accepts_new_party_member())
|
|
|
|
|
|
func test_waves_gate_on_clearing_the_previous_one() -> void:
|
|
_step(SimConfig.DUNGEON_FORMING_TICKS + 200)
|
|
assert_eq(inst.stage, 0)
|
|
var wave_one := inst.world.enemies.size()
|
|
assert_gt(wave_one, 0)
|
|
|
|
_step(200)
|
|
assert_eq(inst.stage, 0, "a slow party must never be overrun by the next wave")
|
|
|
|
_kill_all_enemies()
|
|
_step(120)
|
|
assert_eq(inst.stage, 1)
|
|
assert_gt(inst.world.enemies.size(), wave_one)
|
|
|
|
|
|
func test_the_boss_arrives_after_the_last_wave() -> void:
|
|
_step(SimConfig.DUNGEON_FORMING_TICKS + 200)
|
|
for _wave in 2:
|
|
_kill_all_enemies()
|
|
_step(120)
|
|
assert_eq(inst.stage, 2)
|
|
assert_not_null(inst.world.boss)
|
|
assert_eq(inst.world.boss.def.id, Content.BOSS_WARDEN)
|
|
|
|
|
|
func test_killing_the_boss_clears_the_instance() -> void:
|
|
_step(SimConfig.DUNGEON_FORMING_TICKS + 200)
|
|
for _wave in 2:
|
|
_kill_all_enemies()
|
|
_step(120)
|
|
inst.world.boss.alive = false
|
|
_step(5)
|
|
assert_eq(inst.state, Instance.State.CLEARED)
|
|
assert_almost_eq(inst.exit_countdown_seconds(),
|
|
SimConfig.DUNGEON_CLEARED_EXIT_TICKS / SimConfig.TICK_RATE, 1,
|
|
"the party gets a visible countdown, not an instant boot")
|
|
# The exit timer has to run down, or the party would never be released.
|
|
_step(SimConfig.DUNGEON_CLEARED_EXIT_TICKS + 10)
|
|
assert_eq(inst.stage_delay, 0)
|
|
assert_eq(inst.exit_countdown_seconds(), 0)
|
|
|
|
|
|
func test_the_lobby_has_a_portal_and_no_hostiles() -> void:
|
|
var lobby := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID)
|
|
lobby.add_peer(1, "tester")
|
|
assert_true(lobby.world.portal_enabled)
|
|
_step(1)
|
|
for _i in 600:
|
|
lobby.step()
|
|
assert_eq(lobby.world.pool.live_count, 0, "nothing in the hub may shoot at you")
|
|
assert_eq(lobby.world.players[1].hp, SimConfig.PLAYER_MAX_HP)
|
|
|
|
|
|
func test_interacting_on_the_portal_asks_for_a_dungeon() -> void:
|
|
var lobby := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID)
|
|
lobby.add_peer(1, "tester")
|
|
lobby.world.players[1].pos = SimConfig.PORTAL_POS
|
|
var frames: Array[InputFrame] = [
|
|
InputFrame.make(lobby.world.tick + 1, Vector2.ZERO, 0.0, InputFrame.BTN_INTERACT)]
|
|
lobby.world.queue_input(1, frames)
|
|
lobby.step()
|
|
var used := lobby.world.events.filter(
|
|
func(e: Dictionary) -> bool: return int(e["t"]) == SimEvent.Type.PORTAL_USED)
|
|
assert_eq(used.size(), 1)
|
|
|
|
|
|
func test_interacting_away_from_the_portal_does_nothing() -> void:
|
|
var lobby := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID)
|
|
lobby.add_peer(1, "tester")
|
|
lobby.world.players[1].pos = SimConfig.PORTAL_POS + Vector2(0.0, SimConfig.PORTAL_RADIUS + 50.0)
|
|
var frames: Array[InputFrame] = [
|
|
InputFrame.make(lobby.world.tick + 1, Vector2.ZERO, 0.0, InputFrame.BTN_INTERACT)]
|
|
lobby.world.queue_input(1, frames)
|
|
lobby.step()
|
|
var used := lobby.world.events.filter(
|
|
func(e: Dictionary) -> bool: return int(e["t"]) == SimEvent.Type.PORTAL_USED)
|
|
assert_eq(used.size(), 0)
|