Hedges stay in their squares; the eyes turn to the lock they pick

Eric's rosebush-through-a-wall, hunted to two roots. A camera-facing
billboard viewed obliquely rotates out of its square, so cell-bound
volumes are now windowed to the exact segment where their plane
crosses their own cell — no end may paint beyond it. And the real
smoking gun: the near-bias that lets a hedge win the tie against the
wizard standing in it was applied to the OCCLUSION depth too, letting
the hedge cheat a fifth of a cell through any neighboring wall. The
bias now orders sprites among themselves only; walls always judge the
true depth.

Also answered: edge-deeds turn the head. Picking or jamming a lock,
unlocking with the Master Key, raising or razing or drowning a wall —
any edge-bearing deed by the reel's own wizard now aims the camera at
that edge's midpoint before the outcome shows, same as any other
target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-23 20:28:24 -04:00
co-authored by Claude Fable 5
parent 285c91b2c8
commit ccb5346a31
3 changed files with 66 additions and 4 deletions
+43 -4
View File
@@ -373,7 +373,7 @@
}, ex, ey);
if (s) sprites.push({ ...s, alpha: f.kind === "impact" ? 1 - p : 1, fallback: f.art });
}
sprites.sort((a, b) => b.depth - a.depth);
sprites.sort((a, b) => b.sort - a.sort);
for (const s of sprites) {
const img = imageFor(s.src) ??
(s.fallback
@@ -391,6 +391,7 @@
if (s.warped) {
if (warpIdCol[col] !== (s.warpId ?? -2) || s.depth <= warpDistCol[col]!) continue;
} else if (s.depth >= warpDistCol[col]!) continue;
if (s.clampL !== undefined && (col < s.clampL || col > s.clampR!)) continue;
const texX = ((col - s.left) / (s.right - s.left));
if (img) {
ctx.drawImage(
@@ -488,11 +489,18 @@
glow?: boolean;
alpha?: number;
fallback?: string;
clampL?: number;
clampR?: number;
/** Sort key only: the near-bias orders sprites among THEMSELVES (a
* hedge over the wizard standing in it) but must never let one
* cheat past a wall — occlusion always uses the true depth. */
sort: number;
}
function project(
b: { x: number; y: number; src: string; scale: number; rise: number;
aspect?: number; alpha?: number; glow?: boolean; bias?: number;
fallback?: string; warped?: boolean; warpId?: number },
fallback?: string; warped?: boolean; warpId?: number;
clip?: { x: number; y: number } },
ex: number, ey: number,
): Projected | null {
const relX = b.x - ex, relY = b.y - ey;
@@ -511,9 +519,40 @@
? size * b.aspect * (((W / 2) / Math.tan(FOV / 2)) / H)
: size;
const bottom = half + wallH / 2 - b.rise * wallH;
// A cell-bound volume is windowed to its own square: the billboard's
// camera-facing plane is intersected with the cell, and only that
// lateral segment may paint — an obliquely-viewed hedge can no longer
// poke its ends through the neighboring walls. (Virtual warp copies
// skip this — their clip cell lives in another frame.)
let clampL: number | undefined;
let clampR: number | undefined;
if (b.clip && !b.warped) {
const sinF = Math.sin(facing), cosF = Math.cos(facing);
let sMin = -Infinity, sMax = Infinity;
if (Math.abs(sinF) > 1e-6) {
const a1 = (b.x - b.clip.x) / sinF;
const a2 = (b.x - (b.clip.x + 1)) / sinF;
sMin = Math.max(sMin, Math.min(a1, a2));
sMax = Math.min(sMax, Math.max(a1, a2));
}
if (Math.abs(cosF) > 1e-6) {
const a1 = (b.clip.y - b.y) / cosF;
const a2 = (b.clip.y + 1 - b.y) / cosF;
sMin = Math.max(sMin, Math.min(a1, a2));
sMax = Math.min(sMax, Math.max(a1, a2));
}
if (sMin <= sMax && Number.isFinite(sMin) && Number.isFinite(sMax)) {
const sideC = -relX * sinF + relY * cosF;
const fl = (W / 2) / Math.tan(FOV / 2);
const e1 = W / 2 + ((sideC + sMin) / depth) * fl;
const e2 = W / 2 + ((sideC + sMax) / depth) * fl;
clampL = Math.min(e1, e2);
clampR = Math.max(e1, e2);
}
}
return {
src: b.src, depth: depth - (b.bias ?? 0), warped: b.warped, warpId: b.warpId,
alpha: b.alpha, glow: b.glow, fallback: b.fallback,
src: b.src, depth, sort: depth - (b.bias ?? 0), warped: b.warped, warpId: b.warpId,
alpha: b.alpha, glow: b.glow, fallback: b.fallback, clampL, clampR,
left: screenX - wide / 2, right: screenX + wide / 2,
top: bottom - size, bottom,
};