extends GutTest ## The fog's geometry. Nothing here can look at the screen -- screenshot.tscn is ## for that -- but every property that makes the fog fair or unfair is a ## question about which points are lit, and those can be asked directly. const T := MapGrid.TILE const VIEW := 460.0 var sight: Visibility func before_each() -> void: sight = Visibility.new() ## An open floor, walled at the border, with the origin at a tile corner so ## world positions are easy to reason about. func _room(w: int = 40, h: int = 40) -> MapGrid: var g := MapGrid.new(w, h, MapGrid.Kind.WALL) g.fill_rect(Rect2i(1, 1, w - 2, h - 2), MapGrid.Kind.FLOOR) return g func _centre(tx: int, ty: int) -> Vector2: return Vector2(float(tx) + 0.5, float(ty) + 0.5) * T func test_open_ground_is_lit_and_beyond_sight_is_not() -> void: var g := _room() var eye := _centre(20, 20) sight.compute(g, eye, VIEW) assert_eq(sight.light_at(eye + Vector2(100.0, 0.0)), 1.0) assert_eq(sight.light_at(eye + Vector2(0.0, -(VIEW - Visibility.FADE - 1.0))), 1.0, "lit all the way to where the fade begins") assert_eq(sight.light_at(eye + Vector2(VIEW + 5.0, 0.0)), 0.0, "nothing past the radius") var mid := sight.light_at(eye + Vector2(VIEW - Visibility.FADE * 0.5, 0.0)) assert_between(mid, 0.2, 0.8, "the edge of sight fades instead of cutting") func test_a_wall_hides_what_is_behind_it() -> void: var g := _room() for ty in range(1, 39): g.set_tile(24, ty, MapGrid.Kind.WALL) var eye := _centre(20, 20) sight.compute(g, eye, VIEW) assert_eq(sight.light_at(_centre(28, 20)), 0.0) assert_eq(sight.light_at(_centre(22, 20)), 1.0, "and not what is in front") ## The feature asked for: a shadow edge that runs THROUGH a tile, so part of it ## is lit and part is not. The old fog could only light or hide a tile whole. func test_one_tile_can_be_part_lit() -> void: var g := _room() g.set_tile(22, 20, MapGrid.Kind.PILLAR) var eye := _centre(20, 20) + Vector2(0.0, 7.0) sight.compute(g, eye, VIEW) # The pillar's shadow is the wedge between its top-left and bottom-left # corners as seen from the eye: -25.6 to +10.6 degrees. Tile (25, 21) spans # about +2.9 to +15.9, so the lower edge of the wedge cuts straight across # it. Sample a grid inside it and require both answers. var lit := 0 var dark := 0 for sy in 8: for sx in 8: var p := Vector2(25.0 * T + 2.0 + float(sx) * 4.0, 21.0 * T + 2.0 + float(sy) * 4.0) var l := sight.light_at(p) if l >= 0.99: lit += 1 elif l <= 0.01: dark += 1 assert_gt(lit, 0, "some of the tile is in the light") assert_gt(dark, 0, "and some of the same tile is in shadow") func test_a_shadow_leaves_a_corner_as_a_straight_line() -> void: var g := _room() g.set_tile(22, 18, MapGrid.Kind.PILLAR) var eye := _centre(20, 20) sight.compute(g, eye, VIEW) # The pillar's lower-right corner, and the line from the eye through it. # orthogonal() turns towards the pillar's other silhouette corner, which is # the shadowed side; its negation points away from the pillar, into light. var corner := Vector2(23.0, 19.0) * T var away := (corner - eye).normalized() var side := -away.orthogonal() for dist in [140.0, 220.0, 300.0]: var on_line: Vector2 = eye + away * dist assert_eq(sight.light_at(on_line + side * 3.0), 1.0, "%d units out, just on the open side of the edge" % dist) assert_eq(sight.light_at(on_line - side * 3.0), 0.0, "%d units out, just on the shadowed side" % dist) ## "A bit less strict": the face of the wall in front of you is lit, rather than ## being exactly where the dark begins and so never drawn at all. func test_the_wall_you_are_facing_is_visible() -> void: var g := _room() for ty in range(1, 39): g.set_tile(24, ty, MapGrid.Kind.WALL) var eye := _centre(20, 20) sight.compute(g, eye, VIEW) var face := 24.0 * T # A fixed depth, not one computed from the constant: a test that samples at # WALL_DEPTH - 2 passes with the depth at zero, sampling the floor in front. assert_eq(sight.light_at(Vector2(face + 12.0, eye.y)), 1.0, "the first stretch into the wall is fully lit") assert_lt(sight.light_at(Vector2(face + Visibility.WALL_DEPTH + 1.0, eye.y)), 0.01, "and it is dark again before the far side") ## The regression the first version of this shipped with, caught by a ## screenshot: light carried a fixed distance ALONG each ray, so a wall seen at a ## grazing angle -- the one right beside you in a tunnel -- got a sliver and ## looked missing. Depth is measured straight in from the face now. func test_a_wall_you_are_looking_along_is_lit_its_whole_length() -> void: var g := _room() for ty in range(1, 39): g.set_tile(24, ty, MapGrid.Kind.WALL) var eye := Vector2(24.0 * T - 40.0, 20.5 * T) # just over a tile from the face sight.compute(g, eye, VIEW) var dim := 0 for along in [0.0, 60.0, 120.0, 200.0, 280.0, 350.0]: for side in [-1.0, 1.0]: var p := Vector2(24.0 * T + 10.0, eye.y + along * side) if sight.light_at(p) < 0.99: dim += 1 assert_eq(dim, 0, "ten units into the wall, all the way along it, is lit") func test_light_does_not_round_a_corner() -> void: # A wall ending in a corner, seen at a grazing angle. A ray that enters the # wall near its end would, if its light ran on regardless, come out through # the end face into the open space round the corner. It must stop there. var g := _room() for ty in range(1, 21): g.set_tile(24, ty, MapGrid.Kind.WALL) # ends at row 20 var eye := Vector2(24.0 * T - 40.0, 6.0 * T) sight.compute(g, eye, VIEW) # Round the end of the wall: past its far face, below its last tile. for p in [Vector2(25.0 * T + 6.0, 21.0 * T + 6.0), Vector2(26.0 * T, 21.0 * T + 20.0), Vector2(25.0 * T + 20.0, 22.0 * T)]: assert_eq(sight.light_at(p), 0.0, "round the corner at %s is dark" % p) func test_light_never_reaches_the_far_side_of_a_one_tile_wall() -> void: assert_lt(Visibility.WALL_DEPTH, T, "the lit band inside a wall must stop inside a single tile of it") var g := _room() for ty in range(1, 39): g.set_tile(24, ty, MapGrid.Kind.WALL) var eye := _centre(20, 20) sight.compute(g, eye, VIEW) # Just past the wall, at every height the eye can see the wall at -- steep # and grazing alike. var leaks := 0 for ty in range(2, 38): for fx in [0.5, 4.0, 12.0]: if sight.light_at(Vector2(25.0 * T + fx, float(ty) * T + 16.0)) > 0.0: leaks += 1 assert_eq(leaks, 0, "nothing beyond a one-tile wall may be lit") func test_a_staircase_wall_hides_its_far_side() -> void: # A wall one tile thick that runs diagonally, as steps. A band of light # measured straight in from the face it entered could leave a step through # its other side -- which is the far side of the wall. var g := _room() for i in 14: g.set_tile(18 + i, 6 + i, MapGrid.Kind.WALL) g.set_tile(19 + i, 6 + i, MapGrid.Kind.WALL) var eye := _centre(16, 16) # below-left of the stairs sight.compute(g, eye, VIEW) var leaks := 0 for i in range(1, 12): # The tile immediately above-right of each step: the far side. var far := _centre(20 + i, 5 + i) if g.at(20 + i, 5 + i) == MapGrid.Kind.FLOOR and sight.light_at(far) > 0.0: leaks += 1 assert_eq(leaks, 0, "the far side of a stepped wall stays dark") func test_two_tiles_touching_at_a_corner_leave_no_gap_to_see_through() -> void: var g := _room() g.set_tile(22, 21, MapGrid.Kind.PILLAR) g.set_tile(21, 22, MapGrid.Kind.PILLAR) var eye := _centre(20, 20) + Vector2(16.0, 16.0) # the shared corner's diagonal sight.compute(g, eye, VIEW) # Straight along the diagonal, through the point where the two pillars meet. var through := eye + Vector2(1.0, 1.0).normalized() * 120.0 assert_eq(sight.light_at(through), 0.0, "two walls meeting at a point are closed, not a crack") # Vector2 is single precision and the cast is double, so "exactly" is to a # thousandth of a unit. assert_almost_eq(Visibility.cast(g, eye, Vector2(1.0, 1.0).normalized(), VIEW), Vector2(22.0 * T, 22.0 * T).distance_to(eye), 0.001, "the ray stops exactly at the shared corner") func test_a_ray_stops_exactly_on_the_face_it_hits() -> void: var g := _room() g.set_tile(25, 20, MapGrid.Kind.WALL) var eye := _centre(20, 20) assert_almost_eq(Visibility.cast(g, eye, Vector2.RIGHT, VIEW), 25.0 * T - eye.x, 0.0001) assert_eq(Visibility.cast(g, eye, Vector2.UP, 100.0), 100.0, "nothing within reach means the full reach") func test_only_what_blocks_sight_casts_a_shadow() -> void: var g := _room() var eye := _centre(20, 20) for kind in [MapGrid.Kind.PIT, MapGrid.Kind.BARRICADE]: g.set_tile(23, 20, kind) sight.compute(g, eye, VIEW) assert_eq(sight.light_at(_centre(26, 20)), 1.0, "you can see across a %s" % MapGrid.Kind.keys()[kind]) g.set_tile(23, 20, MapGrid.Kind.PILLAR) sight.compute(g, eye, VIEW) assert_eq(sight.light_at(_centre(26, 20)), 0.0, "but not past a pillar") func test_a_streamed_wall_is_noticed() -> void: # The corner list is cached between frames, so a chunk arriving with a new # wall in it has to invalidate it. A stale list does not lose the shadow -- # rays read the tiles live -- it loses the EDGE: the new pillar's corners get # no rays of their own, and its silhouette is drawn only as finely as the # sweep, two degrees at a time. So the check is the edge, not the shadow. var g := _room() var eye := _centre(20, 20) sight.compute(g, eye, VIEW) g.set_tile(22, 18, MapGrid.Kind.PILLAR) sight.compute(g, eye, VIEW) var corner := Vector2(23.0, 19.0) * T var away := (corner - eye).normalized() var side := -away.orthogonal() var on_line := eye + away * 300.0 assert_eq(sight.light_at(on_line + side * 2.0), 1.0, "the new pillar's edge is as sharp as one that was always there") assert_eq(sight.light_at(on_line - side * 2.0), 0.0) func test_light_falls_off_monotonically_along_every_ray() -> void: var g := _room() g.set_tile(23, 18, MapGrid.Kind.PILLAR) g.set_tile(17, 23, MapGrid.Kind.WALL) var eye := _centre(20, 20) sight.compute(g, eye, VIEW) for i in 64: var dir := Vector2.from_angle(TAU * float(i) / 64.0) var last := 1.0 var r := 4.0 while r < VIEW + 20.0: var l := sight.light_at(eye + dir * r) assert_lte(l, last + 0.0001, "light never comes back along a ray") last = l r += 6.0 func test_the_shadow_mesh_is_well_formed() -> void: var g := _room() g.set_tile(23, 18, MapGrid.Kind.PILLAR) sight.compute(g, _centre(20, 20), VIEW) var mesh := sight.shadow_mesh(VIEW + 64.0, Color(0.05, 0.05, 0.08)) var points: PackedVector2Array = mesh["points"] var colors: PackedColorArray = mesh["colors"] var indices: PackedInt32Array = mesh["indices"] assert_eq(points.size(), sight.angles.size() * 3) assert_eq(colors.size(), points.size(), "a colour per vertex") assert_eq(indices.size() % 3, 0, "whole triangles") for k in indices: assert_between(k, 0, points.size() - 1) for c in colors: assert_true(c.a == 0.0 or c.a == 1.0, "clear at the fade's start, dark beyond") func test_neighbouring_rays_are_never_far_apart() -> void: # The outline between two rays is a straight chord. If the gap were wide, the # chord would cut inside the circle of sight on open ground, and a wedge of # darkness would appear where nothing is in the way. var g := _room() sight.compute(g, _centre(20, 20), VIEW) var widest := 0.0 var n := sight.angles.size() for i in n: var gap := wrapf(sight.angles[(i + 1) % n] - sight.angles[i], 0.0, TAU) widest = maxf(widest, gap) var dip := (VIEW - Visibility.FADE) * (1.0 - cos(widest * 0.5)) assert_lt(dip, 1.0, "the chord dips %.2f units into open ground; under a pixel is invisible" % dip) # And the behaviour that follows from it: halfway between two rays, right at # the start of the fade, open ground is still fully lit. var mid := (sight.angles[0] + sight.angles[1]) * 0.5 assert_eq(sight.light_at(_centre(20, 20) + Vector2.from_angle(mid) * (VIEW - Visibility.FADE - 1.0)), 1.0) func test_a_real_dungeon_is_cheap_enough_to_do_every_frame() -> void: # Not a benchmark -- the machine running the suite varies -- but a guard # against the ray count exploding: corners are the variable part, and a # map with thousands of them in view would be a regression worth knowing. var inst := Instance.make_dungeon(2, 4242, 1, Dungeons.STANDARD) sight.compute(inst.world.map, inst.world.spawn_point, VIEW) assert_lt(sight.angles.size(), 1200, "%d rays for one frame of a depth-1 dungeon" % sight.angles.size())