Add real door textures with sliding-see-through rendering

The raycaster's door geometry now takes the live `doors[]` state instead of a boolean flag, so partially-open door cells let render rays pass through the already-slid portion to show what's genuinely beyond, rather than painting a flat "socket" fill. `intersectDoorMidplane` returns a slide-shifted `textureX` so the door texture visibly translates into the wall as it opens.

`WolfensteinView` samples the new `wolfenstein-doors` sheet the same way it does walls (source-texel column, vertical crop, multiply shading), falling back to the flat tan placeholder. Added the doors.png/.psd assets and a `sheets.doors` entry in the art manifest. Updated sprites.md to document the new sheet, the see-through behavior, and that this stays render-only (gameplay raycasts are unaffected).
This commit is contained in:
Brian Fertig 2026-08-21 17:54:36 -06:00
parent a336376a93
commit f6081de0a8
6 changed files with 150 additions and 101 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

View File

@ -2,7 +2,8 @@
"_readme": "Drop-in art manifest. Every entry has path: null until painted — the game renders procedurally via WolfensteinArt.js until then. Fill in a path and it lazy-loads automatically via assetManifest.js's wolfenstein entry; no code changes needed. sheets.walls expects a 4-column strip (one column per wall type in WolfensteinArt.WALL_COLORS) so WolfensteinView's per-column drawImage can sample a texture-x slice per wall type.", "_readme": "Drop-in art manifest. Every entry has path: null until painted — the game renders procedurally via WolfensteinArt.js until then. Fill in a path and it lazy-loads automatically via assetManifest.js's wolfenstein entry; no code changes needed. sheets.walls expects a 4-column strip (one column per wall type in WolfensteinArt.WALL_COLORS) so WolfensteinView's per-column drawImage can sample a texture-x slice per wall type.",
"sheets": { "sheets": {
"walls": { "key": "wolfenstein-walls", "path": "assets/images/wolfenstein/walls.png", "frameWidth": 64, "frameHeight": 64 }, "walls": { "key": "wolfenstein-walls", "path": "assets/images/wolfenstein/walls.png", "frameWidth": 64, "frameHeight": 64 },
"guard": { "key": "wolfenstein-guard-sheet", "path": null, "frameWidth": 128, "frameHeight": 128 } "guard": { "key": "wolfenstein-guard-sheet", "path": null, "frameWidth": 128, "frameHeight": 128 },
"doors": { "key": "wolfenstein-doors", "path": "assets/images/wolfenstein/doors.png", "frameWidth": 64, "frameHeight": 64 }
}, },
"artwork": [ "artwork": [
{ "key": "wolfenstein-item-pistol", "path": null }, { "key": "wolfenstein-item-pistol", "path": null },

View File

@ -13,11 +13,14 @@
// "exactly at the point dir was aimed at"). // "exactly at the point dir was aimed at").
// //
// Door cells get real recessed-mid-plane geometry (see intersectDoorMidplane) // Door cells get real recessed-mid-plane geometry (see intersectDoorMidplane)
// when a caller opts in via `doorAware` — WolfensteinView's render raycast // when a caller opts in by passing `doors` (WolfensteinView's render raycast
// does; collision/line-of-sight/bullet callers in WolfensteinLogic don't, so // does, with state.doors — the slide-open portion genuinely isn't a hit, so
// a door stays a simple full-cell solid for gameplay purposes exactly as // the DDA carries on through to render whatever's actually beyond); a
// before (see stepDoors' note on why passability only flips at slide===1). // collision/line-of-sight/bullet caller in WolfensteinLogic omits it, so a
// This is a rendering-only enhancement, not a change to what's walkable. // door stays a simple full-cell solid for gameplay purposes exactly as
// before (see stepDoors' note on why passability only flips at slide===1) —
// this is rendering-only, not a change to what's walkable/shootable/visible
// to AI, even though the player can now visually see further than that.
const DEFAULT_MAX_STEPS = 256; const DEFAULT_MAX_STEPS = 256;
@ -41,31 +44,41 @@ export function isWallCell(map, cellX, cellY) {
* walled off), not the doors[] `orientation` field, which nothing actually * walled off), not the doors[] `orientation` field, which nothing actually
* sets to anything but 'vertical'. * sets to anything but 'vertical'.
* *
* Returns null when the ray's own trajectory would clear the cell sideways * Returns null in two cases the caller treats identically keep stepping
* (cross into a neighboring cell) before ever reaching that mid-plane the * the DDA as if this cell were transparent, rather than a miss:
* caller should then keep stepping the DDA as if this cell were transparent * - the ray's own trajectory clears the cell sideways (crosses into a
* rather than treat it as a miss; stepping on lets the DDA reach the actual * neighboring cell) before ever reaching the mid-plane at all letting
* flanking wall cell and hit it at its own true (nearer, unrecessed) * the DDA carry on reaches the actual flanking wall cell and hits it at
* distance, which is what makes that wall's inward face visible at grazing * its own true (nearer, unrecessed) distance, which is what makes that
* angles the two verticals of the "H," rendered as ordinary wall hits, * wall's inward face visible at grazing angles: the two verticals of the
* not anything faked here. * "H," rendered as ordinary wall hits, not anything faked here;
* - the plane crossing falls in the portion of the door that has already
* slid open (`along >= slide`, the panel having translated `slide`
* cell-widths into its pocket) there's genuinely nothing solid there
* now, so the DDA carries on to whatever's actually beyond the doorway.
*
* For the still-covered portion, `textureX` comes back pre-shifted by
* `-slide` the same panel-translation math, so sampling straight from it
* (see WolfensteinView._drawDoorColumn) makes the door texture visibly
* slide sideways into the wall as it opens, not just get cropped in place.
*/ */
function intersectDoorMidplane(map, x, y, dirX, dirY, cellX, cellY) { function intersectDoorMidplane(map, x, y, dirX, dirY, cellX, cellY, slide) {
const blocksEW = isWallCell(map, cellX, cellY - 1) && isWallCell(map, cellX, cellY + 1); const blocksEW = isWallCell(map, cellX, cellY - 1) && isWallCell(map, cellX, cellY + 1);
let t, along;
if (blocksEW) { if (blocksEW) {
if (Math.abs(dirX) < 1e-9) return null; if (Math.abs(dirX) < 1e-9) return null;
const t = (cellX + 0.5 - x) / dirX; t = (cellX + 0.5 - x) / dirX;
if (t <= 0) return null; if (t <= 0) return null;
const along = y + t * dirY - cellY; along = y + t * dirY - cellY;
if (along < 0 || along > 1) return null; } else {
return { perpDist: t, side: 0, textureX: along }; if (Math.abs(dirY) < 1e-9) return null;
t = (cellY + 0.5 - y) / dirY;
if (t <= 0) return null;
along = x + t * dirX - cellX;
} }
if (Math.abs(dirY) < 1e-9) return null; if (along < 0 || along > 1) return null; // clears the cell sideways
const t = (cellY + 0.5 - y) / dirY; if (along < slide) return null; // this point has already slid open (opens from along=0 outward)
if (t <= 0) return null; return { perpDist: t, side: blocksEW ? 0 : 1, textureX: along - slide };
const along = x + t * dirX - cellX;
if (along < 0 || along > 1) return null;
return { perpDist: t, side: 1, textureX: along };
} }
/** /**
@ -73,11 +86,12 @@ function intersectDoorMidplane(map, x, y, dirX, dirY, cellX, cellY) {
* length; distances/`perpDist` come out scaled to that vector's own length * length; distances/`perpDist` come out scaled to that vector's own length
* until it exits the first solid cell. Returns null if it runs off the map * until it exits the first solid cell. Returns null if it runs off the map
* edge or exceeds maxSteps without a hit (should never happen inside a * edge or exceeds maxSteps without a hit (should never happen inside a
* validated closed level). `doorAware` opts into recessed door-plane * validated closed level). Passing `doors` (an array of `{x, y, slide}`, as
* geometry (see intersectDoorMidplane) instead of treating a door cell as an * on WolfensteinLogic's sim state) opts a door cell into recessed,
* ordinary flush solid. * partially-passable mid-plane geometry (see intersectDoorMidplane) instead
* of treating it as an ordinary flush full-cell solid.
*/ */
export function castRay(map, x, y, dirX, dirY, maxSteps = DEFAULT_MAX_STEPS, doorAware = false) { export function castRay(map, x, y, dirX, dirY, maxSteps = DEFAULT_MAX_STEPS, doors = null) {
let mapX = Math.floor(x); let mapX = Math.floor(x);
let mapY = Math.floor(y); let mapY = Math.floor(y);
@ -100,10 +114,11 @@ export function castRay(map, x, y, dirX, dirY, maxSteps = DEFAULT_MAX_STEPS, doo
const wallType = map.walls[mapY][mapX]; const wallType = map.walls[mapY][mapX];
if (wallType === 0) continue; if (wallType === 0) continue;
if (doorAware && wallType === DOOR_WALL_TYPE) { if (doors && wallType === DOOR_WALL_TYPE) {
const doorHit = intersectDoorMidplane(map, x, y, dirX, dirY, mapX, mapY); const door = doors.find((d) => d.x === mapX && d.y === mapY);
const doorHit = intersectDoorMidplane(map, x, y, dirX, dirY, mapX, mapY, door?.slide ?? 0);
if (doorHit) return { ...doorHit, mapX, mapY, wallType }; if (doorHit) return { ...doorHit, mapX, mapY, wallType };
continue; // ray grazes past this door cell without reaching its mid-plane continue; // ray grazes past this door cell, or passes through its already-open portion
} }
const perpDist = side === 0 ? (sideDistX - deltaDistX) : (sideDistY - deltaDistY); const perpDist = side === 0 ? (sideDistX - deltaDistX) : (sideDistY - deltaDistY);
@ -125,8 +140,8 @@ export function makeCamera(x, y, angle, fov) {
return { x, y, angle, dirX, dirY, planeX: -dirY * planeScale, planeY: dirX * planeScale }; return { x, y, angle, dirX, dirY, planeX: -dirY * planeScale, planeY: dirX * planeScale };
} }
/** One castRay per screen column, camera-space (fisheye-free). `doorAware` — see castRay — is what WolfensteinView passes to get recessed door geometry. */ /** One castRay per screen column, camera-space (fisheye-free). `doors` — see castRay — is what WolfensteinView passes (state.doors) to get recessed, slide-aware door geometry. */
export function castColumns(map, camera, numColumns, doorAware = false) { export function castColumns(map, camera, numColumns, doors = null) {
const { x, y, dirX, dirY, planeX, planeY } = camera; const { x, y, dirX, dirY, planeX, planeY } = camera;
const maxSteps = fullMapSteps(map); const maxSteps = fullMapSteps(map);
const out = new Array(numColumns); const out = new Array(numColumns);
@ -134,7 +149,7 @@ export function castColumns(map, camera, numColumns, doorAware = false) {
const cameraX = (2 * col) / numColumns - 1; const cameraX = (2 * col) / numColumns - 1;
const rdx = dirX + planeX * cameraX; const rdx = dirX + planeX * cameraX;
const rdy = dirY + planeY * cameraX; const rdy = dirY + planeY * cameraX;
out[col] = castRay(map, x, y, rdx, rdy, maxSteps, doorAware); out[col] = castRay(map, x, y, rdx, rdy, maxSteps, doors);
} }
return out; return out;
} }

View File

@ -27,17 +27,12 @@ export const VIEW_H = 940;
export const NUM_COLUMNS = 480; export const NUM_COLUMNS = 480;
// wallType -> frame index in the `wolfenstein-walls` sheet, matching // wallType -> frame index in the `wolfenstein-walls` sheet, matching
// WolfensteinArt.WALL_COLORS' order. No entry for type 9 (door) — doors are // WolfensteinArt.WALL_COLORS' order. No entry for type 9 (door) — doors
// never texture-sampled, they get their own recessed-slide treatment in // sample their own `wolfenstein-doors` sheet (frame 0) via _drawDoorColumn,
// _drawDoorColumn regardless of whether painted wall art exists. // which also handles their recessed/sliding geometry — not part of the
// generic per-column wall-texture path below at all.
const WALL_FRAME = { 1: 0, 2: 1, 3: 2, 4: 3 }; const WALL_FRAME = { 1: 0, 2: 1, 3: 2, 4: 3 };
// A dark, unshaded fill for the portion of a door that's already slid past —
// meant to read as a shadowed pocket the panel has receded into, not more
// wall. Kept flat (no side/fog shading) so it reads as a distinct material,
// not just a darker version of the door itself.
const DOOR_SOCKET_COLOR = '#141018';
export default class WolfensteinView { export default class WolfensteinView {
constructor(scene, rules) { constructor(scene, rules) {
this.scene = scene; this.scene = scene;
@ -57,6 +52,8 @@ export default class WolfensteinView {
this.wallTexture = scene.textures.exists('wolfenstein-walls') this.wallTexture = scene.textures.exists('wolfenstein-walls')
? scene.textures.get('wolfenstein-walls') : null; ? scene.textures.get('wolfenstein-walls') : null;
this.doorTexture = scene.textures.exists('wolfenstein-doors')
? scene.textures.get('wolfenstein-doors') : null;
} }
render(state, camera) { render(state, camera) {
@ -71,7 +68,7 @@ export default class WolfensteinView {
ctx.fillStyle = '#4a4a4a'; ctx.fillStyle = '#4a4a4a';
ctx.fillRect(0, VIEW_H / 2, VIEW_W, VIEW_H / 2); ctx.fillRect(0, VIEW_H / 2, VIEW_W, VIEW_H / 2);
const cols = castColumns(map, camera, NUM_COLUMNS, true); const cols = castColumns(map, camera, NUM_COLUMNS, doors);
for (let i = 0; i < NUM_COLUMNS; i++) { for (let i = 0; i < NUM_COLUMNS; i++) {
const hit = cols[i]; const hit = cols[i];
this.depthBuffer[i] = hit ? hit.perpDist : Infinity; this.depthBuffer[i] = hit ? hit.perpDist : Infinity;
@ -88,7 +85,7 @@ export default class WolfensteinView {
const h = Math.ceil(drawEnd - drawStart); const h = Math.ceil(drawEnd - drawStart);
if (hit.wallType === DOOR_WALL_TYPE) { if (hit.wallType === DOOR_WALL_TYPE) {
this._drawDoorColumn(ctx, x, y, w, h, hit, doors); this._drawDoorColumn(ctx, x, y, w, h, hit);
continue; continue;
} }
@ -117,29 +114,43 @@ export default class WolfensteinView {
} }
/** /**
* hit.perpDist already IS the recessed mid-plane distance here the * By the time a column gets here, the raycaster (see castColumns(...,
* render raycast opts into door-aware geometry (see castColumns(..., * doors) above and WolfensteinRaycaster's intersectDoorMidplane) has
* true) above and WolfensteinRaycaster's intersectDoorMidplane), so this * already resolved everything door-specific: this hit only exists because
* column's (x,y,w,h), computed by the caller exactly like any other wall, * that particular point on the door is still covering the doorway, at its
* is already correctly smaller/farther than a flush wall would be. A * true recessed mid-plane distance an already-open point along the same
* grazing column whose ray clears the door cell sideways instead hits the * ray instead passes straight through to whatever's actually beyond
* real flanking wall cell at ITS true distance via the normal wall path * (rendered by the ordinary wall path, not this method at all), which is
* above that's what makes the wall's inward face visible next to the * how you end up able to see past the door as it slides open. `textureX`
* door, the two verticals of the "H." Nothing extra to fake here. * comes back already shifted by `-slide`, so sampling it straight makes
* * the door texture visibly slide sideways into the wall, not just crop in
* `textureX` is where this column's ray crosses the door's face (0..1 * place. So this is really just "draw a wall column, but from the
* along it, computed at the mid-plane); a door slides such that the low- * `wolfenstein-doors` sheet" same technique as the main wall path
* textureX edge stays put and the far edge recedes first, so as `slide` * (source-texel column, vertical crop for close-up magnification,
* climbs, columns flip from "still there" to "already receded" in * multiply-shaded), falling back to the flat placeholder tan if no
* textureX order the door visibly sliding sideways into its socket. * sheet is loaded.
*/ */
_drawDoorColumn(ctx, x, y, w, h, hit, doors) { _drawDoorColumn(ctx, x, y, w, h, hit) {
const door = doors.find((d) => d.x === hit.mapX && d.y === hit.mapY); const frame = this.doorTexture?.frames[0];
const visibleFrac = 1 - (door?.slide ?? 0); if (!frame) {
ctx.fillStyle = hit.textureX < visibleFrac ctx.fillStyle = shadeColor(WALL_COLORS[DOOR_WALL_TYPE] ?? 0xb08040, hit);
? shadeColor(WALL_COLORS[DOOR_WALL_TYPE] ?? 0xb08040, hit) ctx.fillRect(x, y, w, h);
: DOOR_SOCKET_COLOR; return;
}
const lineHeight = VIEW_H / hit.perpDist;
const rawStart = -lineHeight / 2 + VIEW_H / 2;
const rawEnd = lineHeight / 2 + VIEW_H / 2;
const drawStart = Math.max(0, rawStart);
const drawEnd = Math.min(VIEW_H, rawEnd);
const srcX = frame.cutX + Math.min(frame.width - 1, Math.floor(hit.textureX * frame.width));
const srcY = frame.cutY + ((drawStart - rawStart) / lineHeight) * frame.height;
const srcH = ((drawEnd - drawStart) / lineHeight) * frame.height;
ctx.drawImage(frame.source.image, srcX, srcY, 1, srcH, x, y, w, h);
ctx.globalCompositeOperation = 'multiply';
ctx.fillStyle = shadeGrey(hit);
ctx.fillRect(x, y, w, h); ctx.fillRect(x, y, w, h);
ctx.globalCompositeOperation = 'source-over';
} }
_drawSprites(state, camera) { _drawSprites(state, camera) {

View File

@ -13,28 +13,41 @@ loaded as a plain Phaser spritesheet (frames read left-to-right, top-to-bottom
one 64×64 tile per wall type**, in `WolfensteinArt.WALL_COLORS` insertion one 64×64 tile per wall type**, in `WolfensteinArt.WALL_COLORS` insertion
order: frame 0 = type 1 (stone), frame 1 = type 2 (wood), frame 2 = type 3 order: frame 0 = type 1 (stone), frame 1 = type 2 (wood), frame 2 = type 3
(blue-tile), frame 3 = type 4 (green-tile). Doors (`DOOR_WALL_TYPE`, type 9) (blue-tile), frame 3 = type 4 (green-tile). Doors (`DOOR_WALL_TYPE`, type 9)
are **not** part of this sheet at all — `WolfensteinView._drawDoorColumn` are **not** part of this sheet at all — see `sheets.doors` below.
(as of 2026-08-21) handles type 9 as a special case before the texture path
even runs, rendering the flat placeholder tan color (`WALL_COLORS[9]`) for
whatever fraction of the door hasn't yet slid open, and a flat dark "socket"
fill for the rest (see the sliding-door doc below) — so a 5th sheet frame
wouldn't currently be read even if added. Painting a real door texture means
teaching `_drawDoorColumn` to sample a frame the same way the wall path
does, not just adding a 320×64 sheet.
### Sliding, recessed doors ## Door textures — `sheets.doors`
`frameWidth: 64, frameHeight: 64`, same spritesheet convention as walls.
**Wired up as of 2026-08-21: frame 0 is the one and only door type ("Normal"
— the editor's Door category has no other option yet) and is fully live.**
The current painted sheet is 256×256 (a 4×4 grid, 16 frames), of which only
frame 0 is read; the other 15 sit unused until a second door type exists
(matching how the editor's Door dropdown would need a second `option` in
`WolfensteinEditor.js`'s `CATEGORIES`, with the new tool id mapped to a
frame index the same way `WALL_FRAME` maps wall types today — no such
mapping exists yet since there's only the one type). `WolfensteinView.js`
grabs the texture once in the constructor (`this.doorTexture`) and
`_drawDoorColumn` samples it exactly like the wall path samples
`sheets.walls` (1px source-texel column, drawImage-stretched, same vertical
source-crop for close-up magnification, same `multiply`-composited
side/fog shading) — falls back to the flat placeholder tan if the sheet
isn't loaded. See the sliding/recessed-door notes below for how that
texture actually gets positioned in the scene (mid-cell plane, slide-driven
visible fraction).
### Sliding, recessed, see-through doors
Doors animate open/closed (`WolfensteinLogic.js`'s `stepDoors`, `slide` field Doors animate open/closed (`WolfensteinLogic.js`'s `stepDoors`, `slide` field
0→1 over `DOOR_SLIDE_MS`) rather than popping instantly, and the cell stays 0→1 over `DOOR_SLIDE_MS`) rather than popping instantly, and the cell stays
solid for *gameplay* (collision/LOS/bullets) through the entire animation — solid for *gameplay* (collision/LOS/bullets) through the entire animation —
it only becomes passable once `slide` reaches exactly 1 (see stepDoors' it only becomes passable once `slide` reaches exactly 1 (see stepDoors'
comment for why). That's unrelated to the visual, though: doors also render comment for why: no squeezing through a half-open door). That's unrelated to
recessed to the middle of the wall's depth — real geometry, not a flat-color the visual, though, which is real mid-cell geometry, not a projection trick:
trick — via `WolfensteinRaycaster.js`'s `intersectDoorMidplane`, which `WolfensteinRaycaster.js`'s `intersectDoorMidplane`, opted into by passing
`WolfensteinView`'s render-only raycast opts into with `castColumns(..., `state.doors` to `castColumns`/`castRay` (every gameplay raycast in
true)` (the `doorAware` flag; every gameplay raycast in `WolfensteinLogic.js` `WolfensteinLogic.js` omits it and still sees a door as an ordinary flush
omits it and still sees a door as an ordinary flush full-cell solid, so this full-cell solid — this never touches what's actually walkable/shootable/
never touches what's actually walkable/shootable). visible to AI, only what the player's screen shows).
A door's blocking axis is inferred from geometry (which pair of neighbor A door's blocking axis is inferred from geometry (which pair of neighbor
cells is walled off) rather than the `doors[]` `orientation` field, which cells is walled off) rather than the `doors[]` `orientation` field, which
@ -42,25 +55,34 @@ nothing in the codebase actually sets to anything but `'vertical'`. Given
that axis, the door plane sits at the cell's exact midpoint (e.g. `x = that axis, the door plane sits at the cell's exact midpoint (e.g. `x =
doorCellX + 0.5` for a door blocking east-west travel) instead of at the doorCellX + 0.5` for a door blocking east-west travel) instead of at the
near face like a normal wall — a straight-on ray is `perpDist`-recessed by near face like a normal wall — a straight-on ray is `perpDist`-recessed by
0.5 relative to where a flush wall would sit. Critically, a ray whose 0.5 relative to where a flush wall would sit.
trajectory would clear the door cell sideways (cross into a neighboring
cell) *before* reaching that mid-plane isn't given a fake hit at all —
`castRay` just keeps stepping the DDA, so the ray goes on to hit the actual
flanking wall cell at *its own* true, unrecessed distance. That's what
produces the "H" shape: the door is the crossbar, visibly set back between
two verticals that are ordinary wall hits, not anything faked in
`_drawDoorColumn` — confirmed with a sweep of rays across a doorway (see
git history around 2026-08-21 for the script) showing a symmetric run of
door hits at the recessed distance, flanked on both sides by wall hits at
their own, nearer distances, with no discontinuity at the transition.
`_drawDoorColumn` itself is now simple: `hit.perpDist`/`(x,y,w,h)` already Two things make a ray pass straight through a door cell instead of hitting
reflect the recessed mid-plane (computed by the caller exactly like any it — both handled by `intersectDoorMidplane` returning `null`, which
other wall), so it only has to pick door-texture-tan vs. dark socket color `castRay` treats as "keep stepping the DDA," not a miss:
per column, using `hit.textureX` (now measured at the mid-plane, 0..1 along - the ray's own trajectory clears the cell sideways (crosses into a
the door's face) against `1 - slide` — the same left-to-right slide-open neighboring cell) before ever reaching the mid-plane — the DDA carries on
wipe as before, just riding on correct geometry instead of a projection to the real flanking wall cell, hit at *its own* true, unrecessed
trick. distance, which is what produces the "H" shape (door as the crossbar,
visibly set back between two verticals that are ordinary wall hits, not
anything faked in `_drawDoorColumn`) — confirmed with a sweep of rays
across a doorway showing a symmetric run of door hits at the recessed
distance flanked by wall hits at their own nearer distances, no
discontinuity at the transition;
- the crossing point has already slid open (`along < slide`, where `along`
is the 0..1 position along the door's face and the panel is modeled as
having physically translated `slide` cell-widths into its pocket, opening
from `along = 0` outward) — the DDA carries on *past the door entirely*,
so a column here renders whatever's genuinely beyond the doorway (through
the ordinary wall path, at that room's own true distance), not a stand-in
color — you can see into the next room as the door opens, exactly as far
as the geometry actually allows.
For a still-covered point, the returned `textureX` is `along - slide`, not
raw `along` — sampling that directly (`_drawDoorColumn`, same source-texel
technique the wall path uses) is what makes the door *texture* visibly
translate sideways into the wall as it opens, rather than the closed
portion just being a static crop that shrinks in place.
**Now wired up** (as of 2026-08-21): `WolfensteinView._drawWalls` reads **Now wired up** (as of 2026-08-21): `WolfensteinView._drawWalls` reads
`hit.textureX` (the raycaster's fractional wall-face position) and `hit.textureX` (the raycaster's fractional wall-face position) and