extends GutTest ## A dungeon run, end to end: a generated map populated at creation, explored ## rather than survived, and cleared by killing the boss. var inst: Instance func before_each() -> void: inst = Instance.make_dungeon(2, 12345, 1) inst.add_peer(1, "tester") func _step(n: int) -> void: for _i in n: inst.step() func test_a_dungeon_is_populated_when_it_is_created() -> void: # Not spawned in waves: the map has contents before anyone walks in, which # is what makes exploring a decision instead of a countdown. assert_gt(inst.world.enemies.size(), 0, "the rooms should have occupants") assert_not_null(inst.world.boss) assert_true(inst.world.boss.alive) func test_the_boss_starts_inside_its_own_room() -> void: assert_true(inst.boss_room.size != Vector2i.ZERO, "a boss room must exist") assert_true(inst.world.boss.room.has_point(inst.world.boss.pos), "the boss has to start inside the arena it is confined to") ## Boss rooms have no door that locks, so the only thing keeping a boss fight in ## the boss room is the boss itself. Bosses are stationary today, which makes ## this the right moment to pin the invariant -- before movement arrives and ## quietly breaks it. func test_a_boss_cannot_leave_its_room() -> void: _step(SimConfig.DUNGEON_FORMING_TICKS + 5) var boss := inst.world.boss var room: Rect2 = boss.room # Shove it far outside, the way a future chase behaviour might. boss.pos = room.position + room.size + Vector2(2000.0, 2000.0) inst.step() assert_true(room.grow(1.0).has_point(boss.pos), "a boss that can be pushed out of its arena could chase a player who " + "walked away, and walking away is always allowed") func test_nothing_spawns_inside_geometry() -> void: for e in inst.world.enemies.values(): assert_false(inst.world.map.circle_blocked(e.pos, e.def.radius), "%s spawned inside a wall" % e.def.id) assert_false(inst.world.map.circle_blocked(inst.world.spawn_point, SimConfig.PLAYER_RADIUS), "the party would arrive inside a wall") 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_killing_the_boss_clears_the_instance() -> void: _step(SimConfig.DUNGEON_FORMING_TICKS + 5) 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) _step(SimConfig.DUNGEON_CLEARED_EXIT_TICKS + 10) assert_eq(inst.stage_delay, 0) assert_eq(inst.exit_countdown_seconds(), 0) ## Trash left alive must not keep the run open -- you clear a dungeon by ## beating the boss, not by sweeping every corner of the map. func test_leftover_enemies_do_not_block_clearing() -> void: _step(SimConfig.DUNGEON_FORMING_TICKS + 5) assert_gt(inst.world.enemies.size(), 0, "setup: enemies should remain") inst.world.boss.alive = false _step(5) assert_eq(inst.state, Instance.State.CLEARED) func test_deeper_dungeons_are_larger() -> void: var deep := Instance.make_dungeon(3, 12345, 5) assert_gt(deep.world.map.width, inst.world.map.width) # --- The hub ---------------------------------------------------------------- func _lobby() -> Instance: var l := Instance.make_lobby(SimConfig.LOBBY_INSTANCE_ID) l.add_peer(1, "tester") return l func test_the_lobby_has_a_portal_and_no_hostiles() -> void: var lobby := _lobby() assert_true(lobby.world.portal_enabled) assert_ne(lobby.world.portal_pos, Vector2.ZERO, "the portal comes from the map") 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 := _lobby() lobby.world.players[1].pos = lobby.world.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 := _lobby() lobby.world.players[1].pos = lobby.world.portal_pos \ + Vector2(0.0, SimConfig.PORTAL_RADIUS + 80.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)