Stage 2.5: the maze gets its masonry and the walk gets its gait
Walls wear baked 64-texel textures sampled per column — running-bond brick for the maze, darker courses for the rim, rough blocks for filled stone, planked doors with an iron band and handle, flame-streaked fire, swirling violet for the warps — drawn crisp with smoothing off, shaded by distance and animated by overlay. The floor shows its tile seams: grid lines projected onto the ground plane, occluded by the walls that stand on them. And the workshop walks like the game plays: a tap glides exactly one square with an eased stride, a turn swings a clean quarter, held keys queueing the next move as each settles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
65092f50af
commit
cf6349aca8
@@ -29,8 +29,9 @@
|
||||
let y = $state((q.has("y") ? Number(q.get("y")) : start.y) + 0.5);
|
||||
let facing = $state(DIR_ANGLE[q.get("dir") ?? ""] ?? 0);
|
||||
|
||||
// Held keys drive per-frame motion: turning is free, walking asks the
|
||||
// board (workshop collision — walls and shut doors stop you).
|
||||
// Dungeon-crawler locomotion: the maze is walked cell by cell, so the
|
||||
// camera moves the same way — a tap glides one square, a turn swings a
|
||||
// clean quarter. Held keys queue the next move as each one settles.
|
||||
const held = new Set<string>();
|
||||
function onKey(e: KeyboardEvent, down: boolean) {
|
||||
const k = e.key.toLowerCase();
|
||||
@@ -39,29 +40,46 @@
|
||||
if (down) held.add(k); else held.delete(k);
|
||||
}
|
||||
}
|
||||
const SIDES4 = ["E", "S", "W", "N"] as const;
|
||||
type Anim =
|
||||
| { kind: "glide"; fx: number; fy: number; tx: number; ty: number; t0: number; dur: number }
|
||||
| { kind: "turn"; from: number; to: number; t0: number; dur: number }
|
||||
| null;
|
||||
let anim: Anim = null;
|
||||
const ease = (w: number) => w * w * (3 - 2 * w);
|
||||
function startNext(now: number) {
|
||||
const turn = (held.has("arrowleft") || held.has("a") ? -1 : 0) +
|
||||
(held.has("arrowright") || held.has("d") ? 1 : 0);
|
||||
if (turn !== 0) {
|
||||
anim = { kind: "turn", from: facing, to: facing + turn * (Math.PI / 2), t0: now, dur: 210 };
|
||||
return;
|
||||
}
|
||||
const fwd = (held.has("arrowup") || held.has("w") ? 1 : 0) +
|
||||
(held.has("arrowdown") || held.has("s") ? -1 : 0);
|
||||
if (fwd === 0) return;
|
||||
// The stride goes along the nearest cardinal (backpedal reverses it).
|
||||
const q = Math.round(facing / (Math.PI / 2));
|
||||
const idx = ((q % 4) + 4 + (fwd < 0 ? 2 : 0)) % 4;
|
||||
const side = SIDES4[idx]!;
|
||||
const cell = { x: Math.floor(x), y: Math.floor(y) };
|
||||
if (!canWalk(view, cell, side)) return;
|
||||
const tx = x + (side === "E" ? 1 : side === "W" ? -1 : 0);
|
||||
const ty = y + (side === "S" ? 1 : side === "N" ? -1 : 0);
|
||||
anim = { kind: "glide", fx: x, fy: y, tx, ty, t0: now, dur: 280 };
|
||||
}
|
||||
$effect(() => {
|
||||
let raf = 0;
|
||||
let last = performance.now();
|
||||
const tick = (now: number) => {
|
||||
const dt = Math.min(0.05, (now - last) / 1000);
|
||||
last = now;
|
||||
const turn = (held.has("arrowleft") || held.has("a") ? -1 : 0) +
|
||||
(held.has("arrowright") || held.has("d") ? 1 : 0);
|
||||
facing += turn * dt * 2.6;
|
||||
const fwd = (held.has("arrowup") || held.has("w") ? 1 : 0) +
|
||||
(held.has("arrowdown") || held.has("s") ? -1 : 0);
|
||||
if (fwd !== 0) {
|
||||
const speed = fwd * dt * 2.2;
|
||||
const nx = x + Math.cos(facing) * speed;
|
||||
const ny = y + Math.sin(facing) * speed;
|
||||
const cell = { x: Math.floor(x), y: Math.floor(y) };
|
||||
// Crossing a cell boundary asks the board's permission.
|
||||
const crossX = Math.floor(nx) !== cell.x;
|
||||
const crossY = Math.floor(ny) !== cell.y;
|
||||
const okX = !crossX || canWalk(view, cell, nx > x ? "E" : "W");
|
||||
const okY = !crossY || canWalk(view, cell, ny > y ? "S" : "N");
|
||||
if (okX) x = nx;
|
||||
if (okY) y = ny;
|
||||
if (!anim) startNext(now);
|
||||
if (anim) {
|
||||
const w = Math.min(1, (now - anim.t0) / anim.dur);
|
||||
if (anim.kind === "glide") {
|
||||
x = anim.fx + (anim.tx - anim.fx) * ease(w);
|
||||
y = anim.fy + (anim.ty - anim.fy) * ease(w);
|
||||
} else {
|
||||
facing = anim.from + (anim.to - anim.from) * ease(w);
|
||||
}
|
||||
if (w >= 1) anim = null;
|
||||
}
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
@@ -121,7 +139,8 @@
|
||||
<div class="fpv-shop">
|
||||
<h1>The maze, through {povId}'s eyes</h1>
|
||||
<p class="hint">
|
||||
Arrows / WASD walk and turn. Walls and shut doors refuse you; fire and
|
||||
Arrows / WASD stride the maze square by square and swing in quarter
|
||||
turns, as a wizard walks it. Walls and shut doors refuse you; fire and
|
||||
stranger things do not — this workshop has eyes, not rules. Seed with
|
||||
<code>?fpv&seed=N</code>, stand anywhere with <code>&x=&y=&dir=</code>,
|
||||
or watch the clockwork's reel with <code>&demo=1</code> (toggle 👁).
|
||||
|
||||
Reference in New Issue
Block a user