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

+6 -3
View File
@@ -48,8 +48,10 @@ This project deliberately does not, for four reasons:
The trade is that there is no physics engine, so collision is hand-written.
Since Stage 1 that means a tile grid ([MapGrid](../src/sim/map_grid.gd)):
circle-vs-tile for actors, a point test for bullets, and Bresenham line of sight
shared by fog and aggro. Actor-vs-actor hits stay circle-vs-circle, which is the
genre convention anyway.
for aggro. Actor-vs-actor hits stay circle-vs-circle, which is the genre
convention anyway. The client's fog no longer shares that line test: it casts
exact rays through the same grid ([Visibility](../src/view/visibility.gd)), so
shadows cut through tiles rather than snapping to them.
That was the right call rather than a compromise: the same grid answers
collision, sight, and per-peer interest management as array lookups. On arbitrary
@@ -186,7 +188,8 @@ nothing from `net`, `view` or `ui`, and never touches `Net`.**
| File | Role |
| --- | --- |
| `src/view/world_view.gd` | Everything that is not a bullet. Fog, actors, loot, telegraphs. |
| `src/view/world_view.gd` | Everything that is not a bullet. Fog, actors, loot, telegraphs. Draw order is the fog's mechanism: everything it may hide, then the darkness, then what it must not. |
| `src/view/visibility.gd` | The fog's geometry: rays from the eye, wall corners, the shadow mesh. Pure, so `test_visibility.gd` asks it which points are lit. |
| `src/view/bullet_renderer.gd` | The bullet field: one MultiMesh per bullet kind. |
| `src/view/art.gd` | Every atlas rect and sound path, in one table. |
| `src/view/ui_theme.gd` | The control theme, built in code from the UI pack. |
+22
View File
@@ -89,6 +89,28 @@ about as often as a good one.
**Hard fog.** *(User, this session.)* No remembered terrain — anything outside
current line of sight is not drawn, including ground already walked over.
**The fog is geometry, not a verdict per tile — and a little less strict.**
*(User asked: "no longer grid based ... able to light up cells partially", and
"a bit less strict". How strict was left to me.)* `Visibility` casts rays from
the eye — a fixed sweep plus two aimed either side of every wall corner, which
is what makes each shadow edge a straight line — and the view draws terrain and
actors first, then darkness over everything the rays did not reach. A shadow
edge runs straight through a tile; an enemy rounding a corner appears a slice
at a time instead of popping in whole.
"Less strict" is three specific things. The face of a wall you can see is lit
26 units deep, measured perpendicular to the face — along the ray it was a
sliver for any wall seen at a grazing angle, and the wall beside you vanished,
which is the complaint the old fog had. Shadows end in an 8-unit fade instead
of a hard line. And the edge of sight fades over its last 64 units instead of
stopping at a circle. Nothing is remembered: still hard fog.
Two lines this deliberately does not cross: light stops the moment a ray steps
back out of the wall it was in, so it never rounds a corner or reaches the far
side of a one-tile wall; and two blocking tiles that touch only at a corner are
closed, not a crack. Bullets are drawn over the darkness, as they always were —
a shot out of the black is still one you can dodge.
**Dungeon size scales with depth.** *(User, this session.)* `--depth N` is a dev
flag; what raises depth in actual play is still open.
+10 -7
View File
@@ -40,11 +40,14 @@ and more specific than the table above.
### Known flake
`tools/smoke.sh` failed once in a pre-push hook and then passed on four
consecutive runs with no stray processes. It was not reproduced and is not
diagnosed. The suspicion is machine load — the smoke test has fixed tick budgets
and was competing with a full suite run — but that is a guess. If it recurs,
that is the first thing to check.
`tools/smoke.sh` has failed twice, **both times inside the pre-push hook**, and
then passed on every direct run after: four in a row the first time, eight the
second (six in a background loop plus two more, one of them the hook itself).
Neither failure's logs survived, because the hook runs smoke without `KEEP`.
The first suspicion was machine load, but the second failure had nothing else
running. What is different about the hook is its context — stdin is git's
pipe of ref lines, which every bot inherits. Unconfirmed. Push with
`KEEP=1 git push` so the logs of the next one survive.
---
@@ -90,7 +93,7 @@ against itself.
| Walls, pillars, pits, barricades | done | `MapGrid.Kind` + the three `BLOCKS_*` tables |
| Enemies placed per room at creation | done | `Instance._populate()` |
| Scrolling camera | done | [src/view/game_scene.gd](../src/view/game_scene.gd) |
| Hard fog of war | done | `WorldView._draw_terrain` / `_visible` |
| Hard fog of war, lit by rays rather than per tile | done | [src/view/visibility.gd](../src/view/visibility.gd), `WorldView._draw_darkness` |
| Per-peer map streaming (anti map-hack) | done | `ServerRuntime._stream_map` |
| Aggro: range **and** line of sight | done | `SimWorld._aggro_target` |
| Cursor-to-world aiming under a scrolling camera | done | `ClientRuntime.screen_to_world` |
@@ -427,7 +430,7 @@ feet is a coin flip, the same burst with a second of notice is a question.
- **`EmitContext` gained an `events` list**, the only thing an emitter can do
besides spawn bullets.
- **Markers are drawn through fog and through walls.** Everything else in the
view respects line of sight; a warning you cannot see is not a warning. See
view is covered by the darkness; a warning you cannot see is not a warning. See
`WorldView._draw_telegraphs`.
### The Cantor of the Vault