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:
parent
a336376a93
commit
f6081de0a8
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue