7af439341d
ci / verify (push) Successful in 46s
Replaces the fixed centred arena with per-world tile geometry, which is the foundation the remaining features sit on. - MapGrid: tile grid with three independent flags -- blocks movement, bullets, sight -- so a pit stops feet but not bullets or eyes, and a barricade stops feet and bullets but not eyes. Circle collision with per-axis sliding, and Bresenham line of sight shared by fog and aggro. - MapGen: rooms and corridors generated from (seed, depth), with hand-authored boss arenas from Rooms stamped in first so a corridor can never carve through a designed fight. Reachability from spawn to boss is asserted over 40 seeds -- "usually connected" is the failure mode that ruins one run in twenty. - Dungeons are populated at creation, per room, instead of gating on waves. You explore and choose your fights; the run ends when the boss dies, not when the map is swept. Boss rooms have no lock, so walking out is always available. - Aggro: enemies need range AND line of sight, so a dungeon stays quiet until engaged and cover actually protects. - Hard fog, scrolling camera, and terrain rendering. Maps are streamed per peer in chunks around that peer's player, and the seed is deliberately NOT sent -- a client holding it could regenerate the whole dungeon, which is a map hack for free. Stream radius (900u) is wider than view radius (460u) because the client predicts movement against walls and simulates bullets that die on them; the accepted cost is a cheater seeing a little further than the fog, never the floor plan. Partial map knowledge means wall deaths must be announced rather than derived. A test caught the subtle half of that: out-of-bounds tiles read as WALL by design, so checking geometry before bounds reported every bullet leaving the map as a wall kill. 128 tests (was 103); check.sh, test.sh and smoke.sh pass. 0.26 ms/tick with 4 players and a boss, ~65x headroom.
105 lines
4.2 KiB
GDScript
105 lines
4.2 KiB
GDScript
extends GutTest
|
|
## The grid is the foundation for collision, bullets, fog and interest
|
|
## management, so its edge cases are worth pinning down directly rather than
|
|
## discovering them as strange behaviour three systems away.
|
|
|
|
var grid: MapGrid
|
|
|
|
|
|
func before_each() -> void:
|
|
# 10x10 room: solid border, open interior.
|
|
grid = MapGrid.new(10, 10, MapGrid.Kind.WALL)
|
|
grid.fill_rect(Rect2i(1, 1, 8, 8), MapGrid.Kind.FLOOR)
|
|
|
|
|
|
func test_out_of_bounds_reads_as_wall() -> void:
|
|
assert_eq(grid.at(-1, 5), MapGrid.Kind.WALL,
|
|
"the world has to be sealed without every caller bounds-checking")
|
|
assert_eq(grid.at(999, 999), MapGrid.Kind.WALL)
|
|
|
|
|
|
func test_tile_flags_are_independent() -> void:
|
|
# The whole reason for three flags rather than one "solid" bit.
|
|
assert_true(grid.BLOCKS_MOVE[MapGrid.Kind.PIT], "you cannot walk over a pit")
|
|
assert_false(grid.BLOCKS_BULLET[MapGrid.Kind.PIT], "but you can shoot over it")
|
|
assert_false(grid.BLOCKS_SIGHT[MapGrid.Kind.PIT], "and see over it")
|
|
|
|
assert_true(grid.BLOCKS_MOVE[MapGrid.Kind.BARRICADE])
|
|
assert_true(grid.BLOCKS_BULLET[MapGrid.Kind.BARRICADE])
|
|
assert_false(grid.BLOCKS_SIGHT[MapGrid.Kind.BARRICADE],
|
|
"chest height: you see over it but cannot shoot or walk through")
|
|
|
|
|
|
func test_world_and_tile_coordinates_round_trip() -> void:
|
|
assert_eq(grid.to_tile(Vector2(0.0, 0.0)), Vector2i(0, 0))
|
|
assert_eq(grid.to_tile(Vector2(MapGrid.TILE * 3.5, MapGrid.TILE * 2.5)), Vector2i(3, 2))
|
|
assert_eq(grid.tile_centre(3, 2), Vector2(MapGrid.TILE * 3.5, MapGrid.TILE * 2.5))
|
|
assert_eq(grid.world_size(), Vector2(10.0, 10.0) * MapGrid.TILE)
|
|
|
|
|
|
func test_negative_world_positions_map_outside_the_grid() -> void:
|
|
# floor(), not truncation -- int() would fold -0.5 onto tile 0 and let an
|
|
# actor stand half a tile outside the map.
|
|
assert_eq(grid.to_tile(Vector2(-1.0, -1.0)), Vector2i(-1, -1))
|
|
|
|
|
|
func test_circle_collision_against_a_wall() -> void:
|
|
var open := grid.tile_centre(4, 4)
|
|
assert_false(grid.circle_blocked(open, 6.0))
|
|
# Hard against the left wall: tile 0 is solid, so a circle at tile 1's
|
|
# centre minus most of a tile overlaps it.
|
|
assert_true(grid.circle_blocked(Vector2(MapGrid.TILE + 2.0, grid.tile_centre(1, 4).y), 6.0))
|
|
|
|
|
|
func test_sliding_along_a_wall_preserves_the_free_axis() -> void:
|
|
# Moving diagonally into the top wall should keep the horizontal motion.
|
|
var start := grid.tile_centre(4, 1)
|
|
var moved := grid.slide_circle(start, Vector2(8.0, -40.0), 6.0)
|
|
assert_almost_eq(moved.x, start.x + 8.0, 0.001, "x is unobstructed and must not be lost")
|
|
assert_lt(absf(moved.y - start.y), 40.0, "y is blocked by the wall")
|
|
|
|
|
|
func test_a_circle_cannot_be_pushed_through_a_wall() -> void:
|
|
var start := grid.tile_centre(4, 1)
|
|
for _i in 60:
|
|
start = grid.slide_circle(start, Vector2(0.0, -100.0), 6.0)
|
|
assert_gt(start.y, MapGrid.TILE, "no amount of shoving may cross a solid tile")
|
|
|
|
|
|
func test_bullets_stop_at_walls_but_cross_pits() -> void:
|
|
grid.set_tile(4, 4, MapGrid.Kind.PIT)
|
|
assert_false(grid.bullet_blocked(grid.tile_centre(4, 4)), "bullets fly over pits")
|
|
assert_true(grid.bullet_blocked(grid.tile_centre(0, 0)), "and stop at walls")
|
|
|
|
|
|
func test_line_of_sight_is_blocked_by_walls_and_not_by_pits() -> void:
|
|
var a := grid.tile_centre(1, 4)
|
|
var b := grid.tile_centre(8, 4)
|
|
assert_true(grid.has_line_of_sight(a, b), "clear floor between them")
|
|
|
|
grid.set_tile(4, 4, MapGrid.Kind.PIT)
|
|
assert_true(grid.has_line_of_sight(a, b), "a pit is a hole, not a screen")
|
|
|
|
grid.set_tile(4, 4, MapGrid.Kind.PILLAR)
|
|
assert_false(grid.has_line_of_sight(a, b), "a pillar blocks it")
|
|
|
|
grid.set_tile(4, 4, MapGrid.Kind.BARRICADE)
|
|
assert_true(grid.has_line_of_sight(a, b), "you can see over a barricade")
|
|
|
|
|
|
func test_line_of_sight_is_symmetric() -> void:
|
|
grid.set_tile(5, 4, MapGrid.Kind.WALL)
|
|
var a := grid.tile_centre(1, 4)
|
|
var b := grid.tile_centre(8, 4)
|
|
assert_eq(grid.has_line_of_sight(a, b), grid.has_line_of_sight(b, a),
|
|
"asymmetric sight would mean an enemy can shoot you from cover you " +
|
|
"cannot shoot back into")
|
|
|
|
|
|
func test_standing_inside_a_wall_can_still_see_out() -> void:
|
|
# Endpoints must not block, or an actor clipped into geometry goes blind
|
|
# and its aggro check silently fails.
|
|
var inside := grid.tile_centre(0, 0)
|
|
var outside := grid.tile_centre(1, 1)
|
|
assert_true(grid.has_line_of_sight(inside, outside))
|