Sprites respect the window they are seen through
DUKK's floor-to-ceiling white sliver, decoded: the sprite gate only asked "did this column's ray warp?" — so a REAL body standing in front of a warp mouth was clipped out of the mouth's columns (leaving a baffling sliver of a huge close sprite), and a body virtualized through warp A could paint columns bent by warp B. Every column now remembers WHICH warp bent its ray and how far away that mouth stood; a real body paints a column only nearer than its mouth (in front of the window), and a virtual one only through its own warp's columns, beyond the mouth. Projectile legs carry the same identity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
045e868190
commit
cd9ce9fddf
@@ -238,7 +238,12 @@
|
|||||||
// side must MATCH its column's, or bodies near a far mouth would
|
// side must MATCH its column's, or bodies near a far mouth would
|
||||||
// ghost into real corridors the unrolled space happens to overlap.
|
// ghost into real corridors the unrolled space happens to overlap.
|
||||||
const zbuf = new Float64Array(W);
|
const zbuf = new Float64Array(W);
|
||||||
const warpedCol = new Uint8Array(W);
|
// Per column: which warp (if any) the ray bent through, and how far
|
||||||
|
// away that mouth stood. A REAL body paints a column only if it is
|
||||||
|
// nearer than the mouth (it stands in front of the window); a
|
||||||
|
// VIRTUAL one only through its OWN warp's columns, beyond the mouth.
|
||||||
|
const warpIdCol = new Int32Array(W).fill(-1);
|
||||||
|
const warpDistCol = new Float64Array(W).fill(Infinity);
|
||||||
// Known illusions: the ray passes, but a translucent ghost of a wall
|
// Known illusions: the ray passes, but a translucent ghost of a wall
|
||||||
// stands at the crossing — drawn after the sprites so bodies show
|
// stands at the crossing — drawn after the sprites so bodies show
|
||||||
// through it, shimmering so nobody mistakes it for stone.
|
// through it, shimmering so nobody mistakes it for stone.
|
||||||
@@ -250,7 +255,10 @@
|
|||||||
const hit = castRay(view, ex, ey, rayAngle, doors);
|
const hit = castRay(view, ex, ey, rayAngle, doors);
|
||||||
const depth = hit.dist * Math.cos(rayAngle - facing); // no fisheye
|
const depth = hit.dist * Math.cos(rayAngle - facing); // no fisheye
|
||||||
zbuf[col] = depth;
|
zbuf[col] = depth;
|
||||||
warpedCol[col] = hit.warped ? 1 : 0;
|
if (hit.warpId !== undefined) {
|
||||||
|
warpIdCol[col] = hit.warpId;
|
||||||
|
warpDistCol[col] = (hit.warpDist ?? 0) * Math.cos(rayAngle - facing);
|
||||||
|
}
|
||||||
if (hit.ghost) {
|
if (hit.ghost) {
|
||||||
ghosts.push({
|
ghosts.push({
|
||||||
col, depth: hit.ghost.dist * Math.cos(rayAngle - facing),
|
col, depth: hit.ghost.dist * Math.cos(rayAngle - facing),
|
||||||
@@ -341,6 +349,7 @@
|
|||||||
scale: f.kind === "projectile" ? 0.3 : 0.25 + 0.6 * p,
|
scale: f.kind === "projectile" ? 0.3 : 0.25 + 0.6 * p,
|
||||||
rise: f.kind === "impact" ? (f.rise ?? 0.3) : 0.3,
|
rise: f.kind === "impact" ? (f.rise ?? 0.3) : 0.3,
|
||||||
warped: f.kind === "projectile" ? f.warped : undefined,
|
warped: f.kind === "projectile" ? f.warped : undefined,
|
||||||
|
warpId: f.kind === "projectile" ? f.warpId : undefined,
|
||||||
}, ex, ey);
|
}, ex, ey);
|
||||||
if (s) sprites.push({ ...s, alpha: f.kind === "impact" ? 1 - p : 1, fallback: f.art });
|
if (s) sprites.push({ ...s, alpha: f.kind === "impact" ? 1 - p : 1, fallback: f.art });
|
||||||
}
|
}
|
||||||
@@ -359,7 +368,9 @@
|
|||||||
if (s.alpha !== undefined || s.glow) ctx.globalAlpha = s.alpha ?? 1;
|
if (s.alpha !== undefined || s.glow) ctx.globalAlpha = s.alpha ?? 1;
|
||||||
for (let col = Math.max(0, s.left | 0); col < Math.min(W, s.right); col++) {
|
for (let col = Math.max(0, s.left | 0); col < Math.min(W, s.right); col++) {
|
||||||
if (s.depth >= zbuf[col]!) continue;
|
if (s.depth >= zbuf[col]!) continue;
|
||||||
if ((s.warped ? 1 : 0) !== warpedCol[col]!) continue;
|
if (s.warped) {
|
||||||
|
if (warpIdCol[col] !== (s.warpId ?? -2) || s.depth <= warpDistCol[col]!) continue;
|
||||||
|
} else if (s.depth >= warpDistCol[col]!) continue;
|
||||||
const texX = ((col - s.left) / (s.right - s.left));
|
const texX = ((col - s.left) / (s.right - s.left));
|
||||||
if (img) {
|
if (img) {
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
@@ -437,6 +448,7 @@
|
|||||||
top: number;
|
top: number;
|
||||||
bottom: number;
|
bottom: number;
|
||||||
warped?: boolean;
|
warped?: boolean;
|
||||||
|
warpId?: number;
|
||||||
glow?: boolean;
|
glow?: boolean;
|
||||||
alpha?: number;
|
alpha?: number;
|
||||||
fallback?: string;
|
fallback?: string;
|
||||||
@@ -444,7 +456,7 @@
|
|||||||
function project(
|
function project(
|
||||||
b: { x: number; y: number; src: string; scale: number; rise: number;
|
b: { x: number; y: number; src: string; scale: number; rise: number;
|
||||||
aspect?: number; alpha?: number; glow?: boolean; bias?: number;
|
aspect?: number; alpha?: number; glow?: boolean; bias?: number;
|
||||||
fallback?: string; warped?: boolean },
|
fallback?: string; warped?: boolean; warpId?: number },
|
||||||
ex: number, ey: number,
|
ex: number, ey: number,
|
||||||
): Projected | null {
|
): Projected | null {
|
||||||
const relX = b.x - ex, relY = b.y - ey;
|
const relX = b.x - ex, relY = b.y - ey;
|
||||||
@@ -464,7 +476,7 @@
|
|||||||
: size;
|
: size;
|
||||||
const bottom = half + wallH / 2 - b.rise * wallH;
|
const bottom = half + wallH / 2 - b.rise * wallH;
|
||||||
return {
|
return {
|
||||||
src: b.src, depth: depth - (b.bias ?? 0), warped: b.warped,
|
src: b.src, depth: depth - (b.bias ?? 0), warped: b.warped, warpId: b.warpId,
|
||||||
alpha: b.alpha, glow: b.glow, fallback: b.fallback,
|
alpha: b.alpha, glow: b.glow, fallback: b.fallback,
|
||||||
left: screenX - wide / 2, right: screenX + wide / 2,
|
left: screenX - wide / 2, right: screenX + wide / 2,
|
||||||
top: bottom - size, bottom,
|
top: bottom - size, bottom,
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import type { GameEvent, GameView } from "@wizwar/engine";
|
|||||||
export type FpFx =
|
export type FpFx =
|
||||||
| { id: number; kind: "projectile"; art: string; from: { x: number; y: number }; to: { x: number; y: number }; t0: number; dur: number;
|
| { id: number; kind: "projectile"; art: string; from: { x: number; y: number }; to: { x: number; y: number }; t0: number; dur: number;
|
||||||
/** This leg flies in a warp mouth's virtual space: visible only
|
/** This leg flies in a warp mouth's virtual space: visible only
|
||||||
* through the opening, on columns whose rays bent. */
|
* through THAT warp's opening, beyond its mouth. */
|
||||||
warped?: boolean }
|
warped?: boolean; warpId?: number }
|
||||||
| { id: number; kind: "impact"; art: string; at: { x: number; y: number }; t0: number; dur: number;
|
| { id: number; kind: "impact"; art: string; at: { x: number; y: number }; t0: number; dur: number;
|
||||||
/** Lifted off the floor (fireworks burst overhead); default 0.3. */
|
/** Lifted off the floor (fireworks burst overhead); default 0.3. */
|
||||||
rise?: number }
|
rise?: number }
|
||||||
@@ -62,19 +62,20 @@ const IMPACT_ART: Record<string, string> = {
|
|||||||
* clipped by the depth buffer to the side the camera stands on. */
|
* clipped by the depth buffer to the side the camera stands on. */
|
||||||
function projectileLegs(
|
function projectileLegs(
|
||||||
view: GameView, a: { x: number; y: number }, b: { x: number; y: number },
|
view: GameView, a: { x: number; y: number }, b: { x: number; y: number },
|
||||||
): { from: { x: number; y: number }; to: { x: number; y: number }; warped?: boolean }[] {
|
): { from: { x: number; y: number }; to: { x: number; y: number }; warped?: boolean; warpId?: number }[] {
|
||||||
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
||||||
if (len < 0.05) return [{ from: a, to: b }];
|
if (len < 0.05) return [{ from: a, to: b }];
|
||||||
const direct = castRay(view, a.x, a.y, Math.atan2(b.y - a.y, b.x - a.x));
|
const direct = castRay(view, a.x, a.y, Math.atan2(b.y - a.y, b.x - a.x));
|
||||||
if (!direct.warped && direct.dist >= len - 0.4) return [{ from: a, to: b }];
|
if (!direct.warped && direct.dist >= len - 0.4) return [{ from: a, to: b }];
|
||||||
for (const w of view.board.warps) {
|
for (let wi = 0; wi < view.board.warps.length; wi++) {
|
||||||
|
const w = view.board.warps[wi]!;
|
||||||
const motion = warpMotion(w);
|
const motion = warpMotion(w);
|
||||||
const vt = motion.toVirtual(b);
|
const vt = motion.toVirtual(b);
|
||||||
const vlen = Math.hypot(vt.x - a.x, vt.y - a.y);
|
const vlen = Math.hypot(vt.x - a.x, vt.y - a.y);
|
||||||
const hit = castRay(view, a.x, a.y, Math.atan2(vt.y - a.y, vt.x - a.x));
|
const hit = castRay(view, a.x, a.y, Math.atan2(vt.y - a.y, vt.x - a.x));
|
||||||
if (hit.warped && hit.dist >= vlen - 0.4) {
|
if (hit.warped && hit.dist >= vlen - 0.4) {
|
||||||
return [
|
return [
|
||||||
{ from: a, to: vt, warped: true },
|
{ from: a, to: vt, warped: true, warpId: wi },
|
||||||
{ from: motion.toReal(a), to: b },
|
{ from: motion.toReal(a), to: b },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,7 @@ export function fpFxForEvents(
|
|||||||
out.push({
|
out.push({
|
||||||
fx: {
|
fx: {
|
||||||
id: nextId++, kind: "projectile", art,
|
id: nextId++, kind: "projectile", art,
|
||||||
from: leg.from, to: leg.to, warped: leg.warped,
|
from: leg.from, to: leg.to, warped: leg.warped, warpId: leg.warpId,
|
||||||
t0: 0, dur: PROJECTILE_DUR[art]!,
|
t0: 0, dur: PROJECTILE_DUR[art]!,
|
||||||
},
|
},
|
||||||
delay,
|
delay,
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export interface Hit {
|
|||||||
edge?: string;
|
edge?: string;
|
||||||
/** The eye reached this through a warp: haze it other-worldly. */
|
/** The eye reached this through a warp: haze it other-worldly. */
|
||||||
warped?: boolean;
|
warped?: boolean;
|
||||||
|
/** Which warp (index in board.warps) the ray first bent through, and
|
||||||
|
* how far along the ray that mouth stood — the gate a sprite must be
|
||||||
|
* beyond (its own warp) or in front of (any) to paint this column. */
|
||||||
|
warpId?: number;
|
||||||
|
warpDist?: number;
|
||||||
/** The first known-illusion edge the ray crossed before its solid hit:
|
/** The first known-illusion edge the ray crossed before its solid hit:
|
||||||
* the eye passes, but a translucent ghost of a wall stands there. */
|
* the eye passes, but a translucent ghost of a wall stands there. */
|
||||||
ghost?: { dist: number; u: number; worldU: number; axis: "x" | "y" };
|
ghost?: { dist: number; u: number; worldU: number; axis: "x" | "y" };
|
||||||
@@ -108,6 +113,8 @@ export function castRay(
|
|||||||
// a mouth re-enters at the paired mouth and marches on, its hits hazed.
|
// a mouth re-enters at the paired mouth and marches on, its hits hazed.
|
||||||
let baseDist = 0;
|
let baseDist = 0;
|
||||||
let warped = false;
|
let warped = false;
|
||||||
|
let warpId: number | undefined;
|
||||||
|
let warpDist: number | undefined;
|
||||||
let ghost: Hit["ghost"];
|
let ghost: Hit["ghost"];
|
||||||
let doorway: Hit["doorway"];
|
let doorway: Hit["doorway"];
|
||||||
for (let traversal = 0; traversal < 3; traversal++) {
|
for (let traversal = 0; traversal < 3; traversal++) {
|
||||||
@@ -171,7 +178,7 @@ export function castRay(
|
|||||||
// A sliding door carries its texture with it: sample past the gap.
|
// A sliding door carries its texture with it: sample past the gap.
|
||||||
return {
|
return {
|
||||||
dist: baseDist + b.t, kind: b.kind, u: Math.max(0, u - b.slide),
|
dist: baseDist + b.t, kind: b.kind, u: Math.max(0, u - b.slide),
|
||||||
axis: b.axis, worldU: along, edge: b.edge, warped, ghost, doorway,
|
axis: b.axis, worldU: along, edge: b.edge, warped, warpId, warpDist, ghost, doorway,
|
||||||
...(b.frame ? { frame: true } : {}),
|
...(b.frame ? { frame: true } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -194,10 +201,12 @@ export function castRay(
|
|||||||
const offBoard = !view.board.cells[cellKey({ x: nx, y: ny })];
|
const offBoard = !view.board.cells[cellKey({ x: nx, y: ny })];
|
||||||
if (offBoard) {
|
if (offBoard) {
|
||||||
const side: Side = crossSide;
|
const side: Side = crossSide;
|
||||||
const warp = view.board.warps.find(
|
const wIdx = view.board.warps.findIndex(
|
||||||
(w) => cellKey(w.from.cell) === cellKey({ x: cx, y: cy }) && w.from.side === side,
|
(w) => cellKey(w.from.cell) === cellKey({ x: cx, y: cy }) && w.from.side === side,
|
||||||
);
|
);
|
||||||
if (!warp) return { dist: baseDist + dist, kind: "rim", u: texU, axis, worldU: along, warped, ghost, doorway };
|
const warp = wIdx >= 0 ? view.board.warps[wIdx]! : undefined;
|
||||||
|
if (!warp) return { dist: baseDist + dist, kind: "rim", u: texU, axis, worldU: along, warped, warpId, warpDist, ghost, doorway };
|
||||||
|
if (warpId === undefined) { warpId = wIdx; warpDist = baseDist + dist; }
|
||||||
// Step through: re-enter at the paired mouth, heading inward, the
|
// Step through: re-enter at the paired mouth, heading inward, the
|
||||||
// lane carried by the same proper rotation the heading turns by —
|
// lane carried by the same proper rotation the heading turns by —
|
||||||
// mirrored for the pairings whose axes land head-to-head.
|
// mirrored for the pairings whose axes land head-to-head.
|
||||||
@@ -215,14 +224,14 @@ export function castRay(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (view.squareContents[cellKey({ x: nx, y: ny })]?.kind === "stone") {
|
if (view.squareContents[cellKey({ x: nx, y: ny })]?.kind === "stone") {
|
||||||
return { dist: baseDist + dist, kind: "stone", u: texU, axis, worldU: along, warped, ghost, doorway };
|
return { dist: baseDist + dist, kind: "stone", u: texU, axis, worldU: along, warped, warpId, warpDist, ghost, doorway };
|
||||||
}
|
}
|
||||||
cx = nx;
|
cx = nx;
|
||||||
cy = ny;
|
cy = ny;
|
||||||
}
|
}
|
||||||
if (!warped || traversal === 2) break;
|
if (!warped || traversal === 2) break;
|
||||||
}
|
}
|
||||||
return { dist: baseDist + 64, kind: "rim", u: 0, axis: "x", worldU: 0, warped, ghost, doorway };
|
return { dist: baseDist + 64, kind: "rim", u: 0, axis: "x", worldU: 0, warped, warpId, warpDist, ghost, doorway };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Can a wizard's body (not just their eye) cross this edge? Workshop
|
/** Can a wizard's body (not just their eye) cross this edge? Workshop
|
||||||
@@ -277,8 +286,10 @@ export interface Billboard {
|
|||||||
bias?: number;
|
bias?: number;
|
||||||
/** Procedural stand-in name while the painted file loads. */
|
/** Procedural stand-in name while the painted file loads. */
|
||||||
fallback?: string;
|
fallback?: string;
|
||||||
/** A reflection seen through a warp mouth, not the thing itself. */
|
/** A reflection seen through a warp mouth, not the thing itself —
|
||||||
|
* visible only through THAT warp's columns, beyond its mouth. */
|
||||||
warped?: boolean;
|
warped?: boolean;
|
||||||
|
warpId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The along=0 corner of a warp mouth — the anchor castRay's lane
|
/** The along=0 corner of a warp mouth — the anchor castRay's lane
|
||||||
@@ -407,7 +418,8 @@ export function billboards(
|
|||||||
// the same rigid motion the rays use — the zbuffer clips it to the
|
// the same rigid motion the rays use — the zbuffer clips it to the
|
||||||
// opening, since everything around the mouth is nearer wall.
|
// opening, since everything around the mouth is nearer wall.
|
||||||
const real = out.slice();
|
const real = out.slice();
|
||||||
for (const w of view.board.warps) {
|
for (let wi = 0; wi < view.board.warps.length; wi++) {
|
||||||
|
const w = view.board.warps[wi]!;
|
||||||
const motion = warpMotion(w);
|
const motion = warpMotion(w);
|
||||||
const a2 = mouthAnchor(w.to);
|
const a2 = mouthAnchor(w.to);
|
||||||
const inward = SIDE_ANGLE[OPPOSITE[w.to.side]];
|
const inward = SIDE_ANGLE[OPPOSITE[w.to.side]];
|
||||||
@@ -415,7 +427,7 @@ export function billboards(
|
|||||||
for (const b of real) {
|
for (const b of real) {
|
||||||
const rx = b.x - a2.x, ry = b.y - a2.y;
|
const rx = b.x - a2.x, ry = b.y - a2.y;
|
||||||
if (rx * inX + ry * inY < -0.2 || Math.hypot(rx, ry) > 8) continue;
|
if (rx * inX + ry * inY < -0.2 || Math.hypot(rx, ry) > 8) continue;
|
||||||
out.push({ ...b, ...motion.toVirtual(b), warped: true });
|
out.push({ ...b, ...motion.toVirtual(b), warped: true, warpId: wi });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
Reference in New Issue
Block a user