Walls take on thickness: every blocked edge stands as a slab
Zero-width planes vanish when seen end-on. Each wall edge now rises as a thin box — masonry thickest, door joinery slimmer, fire a mere sheet — so the ray march tests the slabs of every cell it passes through and wall ENDS present a cap face. Corners, jambs, and junctions read as stone with depth instead of paper. Warp traversal, stone cells, and the rim keep their crossings; the frame budget doesn't notice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0f01f8c356
commit
3ff7d164c0
@@ -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<string, number> = { 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) {
|
||||
|
||||
Reference in New Issue
Block a user