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.
63 lines
2.8 KiB
GDScript
63 lines
2.8 KiB
GDScript
extends GutTest
|
|
## Movement is the one function the client is allowed to run ahead of the
|
|
## server, so client prediction is only correct while these hold.
|
|
|
|
var map: MapGrid
|
|
|
|
|
|
func before_each() -> void:
|
|
# A plain walled room, big enough that these tests are about movement
|
|
# rather than about walls.
|
|
map = MapGrid.new(42, 24, MapGrid.Kind.WALL)
|
|
map.fill_rect(Rect2i(1, 1, 40, 22), MapGrid.Kind.FLOOR)
|
|
map.centre_on_origin()
|
|
|
|
|
|
func test_diagonal_is_not_faster_than_cardinal() -> void:
|
|
var straight := Movement.step_player(Vector2.ZERO, Vector2(1, 0), SimConfig.PLAYER_SPEED, map)
|
|
var diagonal := Movement.step_player(Vector2.ZERO, Vector2(1, 1), SimConfig.PLAYER_SPEED, map)
|
|
assert_almost_eq(diagonal.length(), straight.length(), 0.001,
|
|
"a diagonal must cover the same distance as a cardinal move")
|
|
|
|
|
|
func test_oversized_input_vector_is_clamped() -> void:
|
|
# The wire format cannot express this, but a patched client could try.
|
|
var cheated := Movement.step_player(Vector2.ZERO, Vector2(1000, 0), SimConfig.PLAYER_SPEED, map)
|
|
var honest := Movement.step_player(Vector2.ZERO, Vector2(1, 0), SimConfig.PLAYER_SPEED, map)
|
|
assert_eq(cheated, honest, "an over-long move vector must buy no extra speed")
|
|
|
|
|
|
func test_speed_matches_config() -> void:
|
|
var moved := Movement.step_player(Vector2.ZERO, Vector2.RIGHT, SimConfig.PLAYER_SPEED, map)
|
|
assert_almost_eq(moved.x, SimConfig.PLAYER_SPEED * SimConfig.TICK_DELTA, 0.001)
|
|
|
|
|
|
func test_a_player_cannot_walk_through_the_map_edge() -> void:
|
|
var far := Vector2(map.world_rect().end.x - MapGrid.TILE * 1.5, 0.0)
|
|
var out := Movement.step_player(far, Vector2.RIGHT, 10000.0, map)
|
|
assert_lt(out.x, map.world_rect().end.x - MapGrid.TILE,
|
|
"the border wall has to stop even an absurd step")
|
|
|
|
|
|
func test_circles_overlap_at_the_boundary() -> void:
|
|
assert_true(Movement.circles_overlap(Vector2.ZERO, 5.0, Vector2(9.9, 0.0), 5.0))
|
|
assert_false(Movement.circles_overlap(Vector2.ZERO, 5.0, Vector2(10.1, 0.0), 5.0))
|
|
|
|
|
|
func test_outside_map_respects_the_cull_margin() -> void:
|
|
var edge := map.world_rect().end.x + SimConfig.BULLET_CULL_MARGIN
|
|
assert_false(Movement.outside_map(Vector2(edge - 1.0, 0.0), map))
|
|
assert_true(Movement.outside_map(Vector2(edge + 1.0, 0.0), map))
|
|
|
|
|
|
## Out-of-map death is derivable from the map's dimensions, which every client
|
|
## is told; wall death is not, because tiles are streamed. Only the second one
|
|
## needs announcing, and conflating them would either desync bullets or double
|
|
## the despawn traffic.
|
|
func test_map_bounds_and_wall_deaths_are_distinguishable() -> void:
|
|
var outside := Vector2(map.world_rect().end.x + 100.0, 0.0)
|
|
assert_true(Movement.outside_map(outside, map))
|
|
var in_wall := map.tile_centre(0, 12)
|
|
assert_false(Movement.outside_map(in_wall, map), "a border wall is inside the map")
|
|
assert_true(Movement.bullet_stopped(in_wall, map))
|