diff --git a/packages/web/src/fpv/raycast.ts b/packages/web/src/fpv/raycast.ts index 080bcc7..6db2a25 100644 --- a/packages/web/src/fpv/raycast.ts +++ b/packages/web/src/fpv/raycast.ts @@ -43,6 +43,29 @@ function edgeObstacle(view: GameView, key: string): Hit["kind"] | null { * stops the eye. Off-board is the maze's rim: a wall, unless the crossing * is a warp mouth (then a shimmering opening). */ +/** Half-thickness per material: walls are masonry, doors joinery, fire a + * sheet. The slab gives every wall END a visible cap face, so corners and + * doorways read as three-dimensional stone rather than paper. */ +const HALF_THICK: Record = { wall: 0.07, rim: 0.07, door: 0.05, firewall: 0.025 }; + +/** Ray vs axis-aligned box; returns entry distance and the axis of the + * face struck, or null for a miss. */ +function slabHit( + ox: number, oy: number, dx: number, dy: number, + minX: number, maxX: number, minY: number, maxY: number, +): { t: number; axis: "x" | "y" } | null { + const ix = 1 / (dx || 1e-9); + const iy = 1 / (dy || 1e-9); + let tx1 = (minX - ox) * ix, tx2 = (maxX - ox) * ix; + if (tx1 > tx2) [tx1, tx2] = [tx2, tx1]; + let ty1 = (minY - oy) * iy, ty2 = (maxY - oy) * iy; + if (ty1 > ty2) [ty1, ty2] = [ty2, ty1]; + const tNear = Math.max(tx1, ty1); + const tFar = Math.min(tx2, ty2); + if (tNear > tFar || tFar < 0) return null; + return { t: Math.max(tNear, 0), axis: tx1 > ty1 ? "x" : "y" }; +} + export function castRay(view: GameView, ox: number, oy: number, angle: number): Hit { // Sight runs THROUGH warps as the rules say it does: a ray that reaches // a mouth re-enters at the paired mouth and marches on, its hits hazed. @@ -55,33 +78,44 @@ export function castRay(view: GameView, ox: number, oy: number, angle: number): let cy = Math.floor(oy); const stepX = dx > 0 ? 1 : -1; const stepY = dy > 0 ? 1 : -1; - // Distance along the ray between successive x / y grid lines. const dDistX = Math.abs(1 / (dx || 1e-9)); const dDistY = Math.abs(1 / (dy || 1e-9)); let sideDistX = (dx > 0 ? cx + 1 - ox : ox - cx) * dDistX; let sideDistY = (dy > 0 ? cy + 1 - oy : oy - cy) * dDistY; for (let i = 0; i < 64; i++) { + // Every blocked edge of the current cell stands as a slab; the + // nearest strike inside this cell's span wins. + const exit = Math.min(sideDistX, sideDistY); + let best: { t: number; axis: "x" | "y"; kind: Hit["kind"] } | null = null; + const trySide = (side: Side, minX: number, maxX: number, minY: number, maxY: number) => { + const kind = edgeObstacle(view, edgeKey({ x: cx, y: cy }, side)); + if (!kind) return; + const h = slabHit(ox, oy, dx, dy, minX, maxX, minY, maxY); + if (h && h.t <= exit + 0.15 && (!best || h.t < best.t)) best = { ...h, kind }; + }; + const hw = (side: Side) => + HALF_THICK[edgeObstacle(view, edgeKey({ x: cx, y: cy }, side)) ?? "wall"] ?? 0.07; + trySide("E", cx + 1 - hw("E"), cx + 1 + hw("E"), cy, cy + 1); + trySide("W", cx - hw("W"), cx + hw("W"), cy, cy + 1); + trySide("S", cx, cx + 1, cy + 1 - hw("S"), cy + 1 + hw("S")); + trySide("N", cx, cx + 1, cy - hw("N"), cy + hw("N")); + if (best !== null) { + const b = best as { t: number; axis: "x" | "y"; kind: Hit["kind"] }; + const along = b.axis === "x" ? oy + b.t * dy : ox + b.t * dx; + const u = along - Math.floor(along); + return { dist: baseDist + b.t, kind: b.kind, u, axis: b.axis, warped }; + } + + // No slab in this cell: advance through the open boundary. const crossingX = sideDistX < sideDistY; const dist = crossingX ? sideDistX : sideDistY; - // The edge being crossed lives between the current cell and the next. - let key: string; let nx = cx, ny = cy; - if (crossingX) { - key = edgeKey({ x: stepX > 0 ? cx : cx - 1, y: cy }, "E"); - nx = cx + stepX; - sideDistX += dDistX; - } else { - key = edgeKey({ x: cx, y: stepY > 0 ? cy : cy - 1 }, "S"); - ny = cy + stepY; - sideDistY += dDistY; - } + if (crossingX) { nx = cx + stepX; sideDistX += dDistX; } + else { ny = cy + stepY; sideDistY += dDistY; } const axis: Hit["axis"] = crossingX ? "x" : "y"; - const u = crossingX ? (oy + dist * dy) % 1 : (ox + dist * dx) % 1; - const texU = u < 0 ? u + 1 : u; - - const blocked = edgeObstacle(view, key); - if (blocked) return { dist: baseDist + dist, kind: blocked, u: texU, axis, warped }; + const u0 = crossingX ? (oy + dist * dy) % 1 : (ox + dist * dx) % 1; + const texU = u0 < 0 ? u0 + 1 : u0; const offBoard = !view.board.cells[cellKey({ x: nx, y: ny })]; if (offBoard) {