Fog lit by rays: shadows cut through tiles, and walls stay visible
ci / verify (push) Successful in 52s

The fog asked one question per tile -- can I see its centre? -- and drew or
skipped the whole tile on the answer. Every shadow was a staircase, tiles
snapped in and out as you walked, and a wall beside you vanished whenever
its centre happened to sit behind its neighbour.

Visibility casts rays from the eye instead: a fixed sweep, plus two aimed
either side of every wall corner, which is what makes each shadow edge a
straight line rather than a smear between rays. The view now draws terrain
and actors first and then darkness over whatever the rays did not reach,
so a shadow runs straight through a tile and an enemy rounding a corner
appears a slice at a time. Bullets are a child node and are still drawn
over the dark; telegraphs are drawn after it, as they must be.

"A bit less strict" is three specific things. A wall face you can see is
lit 26 units in, measured perpendicular to the face; shadows end in a
short fade rather than a line; and sight fades over its last 64 units
rather than stopping at a circle. Still hard fog -- nothing remembered.

The first version measured that depth along the ray, and a screenshot
caught what no test had: a wall you look along got a sliver, so the wall
beside you in a tunnel looked missing -- the old fog's own complaint.
Perpendicular depth fixes it, and light stops the instant a ray steps
back into the open, so it never rounds a corner, never reaches the far
side of a one-tile wall, and never leaves a stepped wall through its
other face. Two tiles touching only at a corner are closed.

About half a millisecond a frame. Measured against the old fog in the
same scene: hub 68 fps against 62, dungeon 240 against 240. The hub's
own low rate predates this.

MapGrid gains a revision counter so the corner list is rebuilt only when
the map changes or the eye crosses a tile. screenshot.tscn now captures a
dungeon, a tunnel and the tunnel with F1's overlay, which draws where
each ray stopped -- and makes SHOT_DIR rather than failing every capture
when it does not exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-05 10:07:33 +02:00
1 parent 83c3b920fb
commit 855669a665
12 files changed
+871 -45

No files matched your search

+17 -2
View File
@@ -54,6 +54,11 @@ var tiles := PackedByteArray()
## existing coordinate (spawn points, portal, boss placement) meaningful and
## avoids an all-positive coordinate space where "0" is a corner.
var origin := Vector2.ZERO
## Bumped by every write, so something that derives data from the tiles can tell
## whether it is stale without comparing them. The client's fog extracts wall
## corners from the map and would otherwise have to redo it every frame, or miss
## a chunk arriving.
var revision: int = 0
func _init(w: int = 1, h: int = 1, fill: Kind = Kind.WALL) -> void:
@@ -65,6 +70,7 @@ func resize(w: int, h: int, fill: Kind = Kind.WALL) -> void:
height = maxi(h, 1)
tiles.resize(width * height)
tiles.fill(fill)
revision += 1
func in_bounds(tx: int, ty: int) -> bool:
@@ -82,6 +88,7 @@ func at(tx: int, ty: int) -> Kind:
func set_tile(tx: int, ty: int, kind: Kind) -> void:
if in_bounds(tx, ty):
tiles[ty * width + tx] = kind
revision += 1
func fill_rect(rect: Rect2i, kind: Kind) -> void:
@@ -111,6 +118,7 @@ func to_tile(world: Vector2) -> Vector2i:
## Centre the map on the world origin.
func centre_on_origin() -> void:
origin = -world_size() * 0.5
revision += 1
## World-space rectangle the map occupies.
@@ -175,8 +183,14 @@ func slide_circle(pos: Vector2, delta: Vector2, radius: float) -> Vector2:
return out
## Bresenham-style sight test between two world points. Used for fog on the
## client and for aggro on the server, so it has to agree on both.
## Bresenham-style sight test between two world points, from tile to tile.
## Used for aggro on the server.
##
## It used to drive the client's fog as well, which is why the two once had to
## agree. The fog is exact geometry now (see Visibility), so this is the
## server's question alone -- and it is coarser than what the player sees: its
## diagonal steps pass between two tiles that touch only at a corner, which
## the fog treats as closed.
##
## [param include_end] is false here: the endpoints themselves never block, so
## standing in a doorway, or shooting at something embedded in a wall, still
@@ -303,3 +317,4 @@ func apply_chunk(chunk_id: int, data: PackedByteArray) -> void:
for tx in range(r.position.x, r.end.x):
tiles[ty * width + tx] = data[i]
i += 1
revision += 1