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

+67 -1
View File
@@ -29,7 +29,12 @@ const OPEN_UPGRADES := 330
const AT_UPGRADES := 360
const OPEN_ROSTER := 370
const AT_ROSTER := 400
const DONE := 410
const ENTER_DUNGEON := 410
const AT_DUNGEON := 480
const TO_TUNNEL := 490
const AT_TUNNEL := 550
const AT_TUNNEL_DEBUG := 560
const DONE := 570
var _frame: int = 0
var _game: Node = null
@@ -44,6 +49,10 @@ func _ready() -> void:
var from_env := OS.get_environment("SHOT_DIR")
if not from_env.is_empty():
_dir = from_env
# Made rather than assumed. The example in CLAUDE.md points at a fresh
# /tmp/shots, and a missing directory fails every capture one at a time
# with an error that names the file rather than the folder.
DirAccess.make_dir_recursive_absolute(_dir)
get_window().size = Vector2i(1280, 720)
_menu = preload("res://src/ui/main_menu.gd").new()
add_child(_menu)
@@ -118,6 +127,23 @@ func _process(_delta: float) -> void:
_show_the_roster()
AT_ROSTER:
_grab("roster")
ENTER_DUNGEON:
# The fog is what these two are for, and the hub is the worst place to
# judge it: one big room with little in it to cast a shadow.
_game.set("_roster_open", false)
Net.server._send_to_dungeon(Net.LOCAL_PEER)
AT_DUNGEON:
_grab("dungeon")
TO_TUNNEL:
_stand_in_a_tunnel()
AT_TUNNEL:
_grab("tunnel")
# The same view with F1's overlay: the outline is where each fog ray
# stopped, which is the quickest way to tell a rendering mistake from
# a geometry one.
_game.get_node("WorldView").debug_hitboxes = true
AT_TUNNEL_DEBUG:
_grab("tunnel_debug")
DONE:
Net.shutdown()
get_tree().quit(0)
@@ -152,6 +178,46 @@ func _show_the_roster() -> void:
_game.get_node("CharacterSelect").refresh(roster, "c4")
## Move the player, on both sides, into the tunnel closest to where it spawned:
## somewhere with walls close on two sides and a corner in view, which is where
## the old fog looked worst.
func _stand_in_a_tunnel() -> void:
var inst := Net.server.instance_of(Net.LOCAL_PEER)
if inst == null or inst.rooms.is_empty():
return
var map := inst.world.map
var start: Rect2i = inst.rooms[0]
var spawn := map.to_tile(inst.world.spawn_point)
var best := Vector2i(-1, -1)
var best_d := INF
for ty in map.height:
for tx in map.width:
if map.at(tx, ty) != MapGrid.Kind.FLOOR:
continue
var here := Vector2i(tx, ty)
var in_room := inst.boss_room.has_point(here)
for r: Rect2i in inst.rooms:
if r.grow(1).has_point(here):
in_room = true
if in_room:
continue
# A couple of tiles clear of the first room, so the tunnel's walls
# are on screen and not just the doorway.
if start.grow(3).has_point(here):
continue
var d := float((here - spawn).length_squared())
if d < best_d:
best_d = d
best = here
if best.x < 0:
return
var at := map.tile_centre(best.x, best.y)
Net.client.predicted_pos = at
var p: SimPlayer = inst.world.players.get(Net.LOCAL_PEER)
if p != null:
p.pos = at
## One roster row in the shape decode_characters produces.
func _roster_entry(who: String, level: int, ceiling: int, current: int,
tint: Color) -> Dictionary: