Files
transcience/tests/integration/test_dungeon_flow.gd
T
claude c4beeae38f Initial commit: Transcience MVP
Top-down twin-stick bullet-hell, Godot 4.7, server-authoritative dedicated
server with client-side prediction. Clients send input only; the server
resolves every hit for both players and enemies (no PvP).

- SimWorld: whole simulation as plain RefCounted objects (no nodes, no
  physics server), ~0.24ms/tick at peak load -- runs headless for free and
  drives 78 tests in under a second
- BulletPool: struct-of-arrays bullet storage, replicated as spawn/despawn
  events rather than per-tick state
- Emitter framework (Ring/AimedSpread/WallGap/ArcSweep) shared by trash
  enemies and bosses -- a new boss is data in src/content/content.gd, no
  simulation changes
- The Warden of the Fold: stationary 4-phase boss built entirely on that
  format
- Lobby hub with a portal into on-demand dungeon instances; one process
  hosts the hub plus every concurrent dungeon
- Emergency escape: 3s server-owned channel, cancelled by damage
- tools/check.sh, test.sh (GUT), smoke.sh (real server + bot clients over
  ENet), bench.gd; git hooks wired to the same scripts
- docs/ARCHITECTURE.md, NETCODE.md, WORKFLOW.md, ROADMAP.md
2026-09-03 16:03:57 +02:00

110 lines
3.3 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)
# The exit timer has to run down, or the party would never be released.
_step(400)
assert_eq(inst.stage_delay, 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)