From cdfae39398d0250e8d173dc88d40b6efe2a1cfec Mon Sep 17 00:00:00 2001
From: Eric Wagoner
Date: Sun, 23 Aug 2026 16:30:35 -0400
Subject: [PATCH] The maze performs: doors swing, walls die loudly, homes wear
their colors
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Entertainment polish for the instant replay, all through one wizard's
eyes. Doors now slide open along their own edge — Wolf3D style — when
anyone steps through in a reel, hold for the crossing, and swing shut;
the raycaster takes a per-edge openness map and doors carry their
texture with them, leaving a sliver of jamb at the far post. A wall
that dies bursts into stone and shakes the camera, then leaves a mound
of rubble at the fallen edge for the rest of the reel (a tenth
paintable sprite, drawn opaque — debris, not light). Jammed locks
seethe: a rusty pulse across the wood, keyed off the view's own door
states. Walking THROUGH a wall shimmers on both faces and washes the
walker's own screen stone-gray. And every home base wears its owner's
emblem on the flagstones — a near-white paintable overlay tinted by
wizard color per cell in the floor cast.
Testing surfaced a real ghost: a body standing near a far warp mouth
also rendered at its virtual position, which can overlap real
corridors — the depth buffer alone cannot clip it. Every column now
remembers whether its ray bent, and a sprite draws only where its warp
side matches the column's; projectile legs carry the same flag.
Co-Authored-By: Claude Fable 5
---
packages/web/public/fx3d/rubble.png | Bin 0 -> 1556 bytes
packages/web/public/textures/home.png | Bin 0 -> 1055 bytes
packages/web/src/Replay.svelte | 22 +++++-
packages/web/src/TokenGallery.svelte | 2 +-
packages/web/src/fpv/FirstPerson.svelte | 89 ++++++++++++++++++++++--
packages/web/src/fpv/fx3d.ts | 78 +++++++++++++++++----
packages/web/src/fpv/raycast.ts | 47 +++++++++----
packages/web/src/fpv/textures.ts | 22 +++++-
research/fx3d-sprite-spec.md | 15 +++-
9 files changed, 238 insertions(+), 37 deletions(-)
create mode 100644 packages/web/public/fx3d/rubble.png
create mode 100644 packages/web/public/textures/home.png
diff --git a/packages/web/public/fx3d/rubble.png b/packages/web/public/fx3d/rubble.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd435e16cec3b78bb8516c5dee29724b158c91c6
GIT binary patch
literal 1556
zcmV+v2J88WP)oNoc!Z2@(~
zI|Qg1v77ExfZbMHov%{?YDVm)I~8EJ6<6o$RDhZhyXj5^*loqt`8pM#X2fo~Qvv_?
z6*tQ7zN>)e&Ul}Hw%2=kpw}Bf;SmXtjeJH4CF;L#2%zLJ*#PJ81p_#ZiIky4TN&BN
zM;O9J9jV?;5iro>eH|zEp=?FON48WakH!!5bbmz2p}sR|RE4dKuy0;?ac=D5P;hKy
zh$|BAUl|+yjveq2I$o#r?rIeFM
zK04^ln}5RGTp+5(-#L5h>ye?U??yhWi+99pQUV6B>=CM$Ji3+E9Kk9|A{Q*>cM`#i
zu(Pwji_D4*%e7?#wtmnfK}rCYc)v1=nF|A5ED=Q81!emT>Gh7{&}Q~L!sKmjZB1SU
zN*FI$hs|N7E4auB0B8d9e?lys516$03;{T6et3Q4!opv=0ICGpnV?62?x1Fzk@LaK
zHd2Pm$jE@g!o5&mUkB~&k3vJk0m#nIQha)sm6f}C?ywKf1ktyH9s$?Gk*2O1ER{V*
znob_E5DJB0aq$k!%-n!$*M5O}_x^?>M;?Q!stR&Tg|>mzxx*3S>zb`9CxFAn9kKdd
z5!T>b&>t}Dy>)9ICMJG_f`U9l62QzK=Ur-;lz`#O;~yY+8pWthN3d#~@LW)X%;-~7
ze%Q4uTe=<6($c`^`_*_T4w{OT5-`YK#ACH-(C>H
zi;Ih3-@Xc{tJ@EL|Ev%rGwKo$4WbgKWKmoZX(3zGFT9J+1tBmsV^&rcR8~H$4C3)T
z2&JV(u)MsY=$Z=$4TGi#&XKZ3y3HK}`fHU@ZcnW~5
ztW3zs*%eil1MASC1JK!d6g*hA<>h5?_;4E#8Tt8pVmQ4HCEwK4pa;?TGMHQQ)ovL9
zjDzy&Xl1**pM>MbJD|7sgd&u9O-;4t0}>e>9Y+nT!R=0mM3cuy?)SWBU$~>5)G`8a
zqK9ZTX=SmWP@@=%l?AP@Z^&FiJAQ5XPMv%CZIRPT0-&+6UNomETPm^f6R@Bg(gxyf
z+nQwrl$I9XrIOyBDzP5L()JNWWoAAgGK*{*>EP;oy6>eMA}84_BcQUX{2!Vi6)7t#
z5r*gH<_hfsW4d1Qgtv?UPeDN@O^}LsBwuHRg`QM~jf=I60C$EvFK(tJlWTgY*yM1DK13=qKOc;R(|bar
zS7R}7xHH+)_lm&_0szYhINf*t8~hxTC`q-YrDX^P!*6?O=Of$z0000Aab2Y^uwWBhnBeNV7vkF(H*tNo(_z#v93jPb1)*6eXc&UQbQG3EK(4-Nn$7{V9^_lYNmTxzJ@m+yKW
z6aWS=f+38x6-zEP)Y7AEolRaa00_qbMld85M-8>~pqI41ZGI2{2u3&tFw#~`HIG_)
z(2Jhcb06=$13)N(5sm>@k@TP!J?ZV*uUhXF00I$;V1!qP*}kV2J?YI1+dg%5-T)vB
zfe1x#-7uFudeWO2%yQ|a*LnefAcP?hq56R8y12M_u>5oW`f@RU^LKInj<_t>>~U7j
z*Wc5d8O&Oize(z6wrghq2tg3S5ZEMS_50`Zb$0bOO|n;+N=Hg%WqMYAO-Jl;MozhD
z{A32RlJYl6W*ZY*9;7YvYSR#a5CkEtHtA*)V@axJs%VYyxn3JGZPOG
ztN}>ObG4H+FU{Y2Xk3s>O;eID2vz_j<}tBKuU@FeCr_3hnYymg_wKHJKoA3vnn!k#
zBu{yjMB5ry2qFMB<}sbAqWpo!s(namnjq|(C<3sr*?l}00k9>Vt7I*OBdM{ynP?IL
zpuh$~QWU?al|VZ1r|u2fEG%gEJrO>?@ddAeHxpilxKIXU@PY_c=z
z+61}OH0Am*Pk(Cw6iXnCpFI0fY*Y!M=_cfg9CGPz3SZ{w?*xDbQV65`=+maizvgOF
z{$K<~UVd|I*6eW>(>(f}0nkJOfvbJ|`1H%8$@KHX$>~ReHG7=V&lEnN{C0*H05p?8
zAYU3G%%i~@0Gfq@5XwB7y#k=2ZwMjHqt`nCn)-qe+B{l=0HCoegc#=084Li;Z6U-m
zkH(+?7$AiZ(>(fu17Jv72tLd+PZCi8AZQ {
+ const spots: { x: number; y: number }[] = [];
+ const seen = new Set();
+ for (let i = 0; i <= Math.min(idx, steps.length - 1); i++) {
+ for (const e of steps[i]!.events) {
+ if (e.type !== "wallDestroyed" || !("edge" in e)) continue;
+ const edge = (e as { edge: { cell: { x: number; y: number }; side: "N" | "E" | "S" | "W" } }).edge;
+ const key = `${edge.cell.x},${edge.cell.y}:${edge.side}`;
+ if (seen.has(key)) continue;
+ seen.add(key);
+ spots.push(edgeMid(edge.cell, edge.side));
+ }
+ }
+ return spots;
+ });
+
/** Each step's spells flare on the reel exactly as they did at the table. */
let boardFx = $state([]);
$effect(() => {
@@ -320,7 +338,7 @@
{#if fp}
+ fx={fpFx} posOverride={actorPos} rubble={rubbleSpots} />
{:else}
{/if}
diff --git a/packages/web/src/TokenGallery.svelte b/packages/web/src/TokenGallery.svelte
index 5d7f45c..8b42fc5 100644
--- a/packages/web/src/TokenGallery.svelte
+++ b/packages/web/src/TokenGallery.svelte
@@ -95,7 +95,7 @@
wherever a file is missing.
- {#each ["wall", "rim", "stone", "door", "firewall", "warp", "floor", "ceiling"] as name (name)}
+ {#each ["wall", "rim", "stone", "door", "firewall", "warp", "floor", "ceiling", "home"] as name (name)}
{name}
diff --git a/packages/web/src/fpv/FirstPerson.svelte b/packages/web/src/fpv/FirstPerson.svelte
index 933ef36..e3a1fb3 100644
--- a/packages/web/src/fpv/FirstPerson.svelte
+++ b/packages/web/src/fpv/FirstPerson.svelte
@@ -5,8 +5,9 @@
// by the same depth buffer the walls wrote.
import { castRay, billboards } from "./raycast";
import { materialTextures } from "./textures";
- import { fxFallback, type FpFx } from "./fx3d";
+ import { doorOpenness, fxFallback, type FpFx } from "./fx3d";
import { tokenArt } from "../art";
+ import { PLAYER_COLORS } from "../colors";
import type { GameView } from "@wizwar/engine";
let {
@@ -19,6 +20,7 @@
height = 440,
fx = [],
posOverride,
+ rubble = [],
}: {
view: GameView;
povId: string;
@@ -33,6 +35,8 @@
fx?: FpFx[];
/** Bodies mid-glide: world positions that override the view's cells. */
posOverride?: Record;
+ /** Where walls have died: a mound of stone marks each fallen edge. */
+ rubble?: { x: number; y: number }[];
} = $props();
const FOV = Math.PI / 2.9;
@@ -84,6 +88,33 @@
}
let frame: ImageData | null = null;
+ // Home bases wear their owner's emblem on the floor: a per-view grid of
+ // owner tints, indexed by integer cell for the per-pixel pass.
+ const homeCache = new WeakMap