A warp crossing surges the veil itself, not a billboard over the wizard
Every board effect the cockpit had no art for landed as the same purple lightning-ring sprite, so a body stepping through a warp mouth wore the ring over its own portrait. Eric wanted the purple wall to do the animating. The portal effect now becomes a "surge" keyed by the mouth's index in board.warps: the veil hung in that opening floods with its texture, the swirl quickens and pales, and a ripple runs out to the jambs before it settles back — nothing drawn over whoever stands in it. The director used to steer the eyes toward that ring's cell on the pov wizard's own crossing; it now aims at a surging mouth the same way, so the heist's pinned frames render as before. Illusion and Pass Wall crossings keep the shimmer sprite — stone has no veil to surge. Scene gate: 29 of 30 frames unchanged. stone-gaze-s10-board drifts 0.21% with or without this change (the dashed sight line's phase), a pre-existing flake left unblessed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
8488e9a8d1
commit
a7f77367a6
@@ -5,7 +5,7 @@
|
|||||||
// by the same depth buffer the walls wrote.
|
// by the same depth buffer the walls wrote.
|
||||||
import { billboards, castRay, warpMotion, type FpvTarget } from "./raycast";
|
import { billboards, castRay, warpMotion, type FpvTarget } from "./raycast";
|
||||||
import { materialTextures } from "./textures";
|
import { materialTextures } from "./textures";
|
||||||
import { doorOpenness, fxFallback, growProgress, type FpFx } from "./fx3d";
|
import { doorOpenness, fxFallback, growProgress, surgeIntensity, type FpFx } from "./fx3d";
|
||||||
import { terrainFallback, TERRAIN3D } from "./terrain3d";
|
import { terrainFallback, TERRAIN3D } from "./terrain3d";
|
||||||
import { tokenArt } from "../art";
|
import { tokenArt } from "../art";
|
||||||
import { PLAYER_COLORS } from "../colors";
|
import { PLAYER_COLORS } from "../colors";
|
||||||
@@ -201,14 +201,19 @@
|
|||||||
ey += f.mag * (1 - p) * Math.cos(time * 0.087);
|
ey += f.mag * (1 - p) * Math.cos(time * 0.087);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Doors mid-swing and conjurations mid-growth this frame.
|
// Doors mid-swing, conjurations mid-growth, and warp veils
|
||||||
|
// mid-surge this frame.
|
||||||
let doors: Record<string, number> | undefined;
|
let doors: Record<string, number> | undefined;
|
||||||
let growing: Record<string, number> | undefined;
|
let growing: Record<string, number> | undefined;
|
||||||
const spawn: Record<string, number> = {};
|
const spawn: Record<string, number> = {};
|
||||||
|
const surging: Record<number, number> = {};
|
||||||
for (const f of fx) {
|
for (const f of fx) {
|
||||||
if (f.kind === "door") {
|
if (f.kind === "door") {
|
||||||
const a = doorOpenness((time - f.t0) / f.dur);
|
const a = doorOpenness((time - f.t0) / f.dur);
|
||||||
if (a > 0) (doors ??= {})[f.edge] = Math.max(doors?.[f.edge] ?? 0, a);
|
if (a > 0) (doors ??= {})[f.edge] = Math.max(doors?.[f.edge] ?? 0, a);
|
||||||
|
} else if (f.kind === "surge") {
|
||||||
|
const s = surgeIntensity((time - f.t0) / f.dur);
|
||||||
|
if (s > 0) surging[f.warp] = Math.max(surging[f.warp] ?? 0, s);
|
||||||
} else if (f.kind === "grow") {
|
} else if (f.kind === "grow") {
|
||||||
const p = (time - f.t0) / f.dur;
|
const p = (time - f.t0) / f.dur;
|
||||||
if (p < 0 || p >= 1) continue;
|
if (p < 0 || p >= 1) continue;
|
||||||
@@ -335,7 +340,7 @@
|
|||||||
const warpDistCol = new Float64Array(W).fill(Infinity);
|
const warpDistCol = new Float64Array(W).fill(Infinity);
|
||||||
// Each mouth a ray passed through hangs a translucent veil of the
|
// Each mouth a ray passed through hangs a translucent veil of the
|
||||||
// warp texture at its own depth, drawn with the other overlays.
|
// warp texture at its own depth, drawn with the other overlays.
|
||||||
const veils: { col: number; depth: number; u: number }[] = [];
|
const veils: { col: number; depth: number; u: number; warp: number }[] = [];
|
||||||
// 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.
|
||||||
@@ -352,7 +357,7 @@
|
|||||||
if (hit.warpId !== undefined) {
|
if (hit.warpId !== undefined) {
|
||||||
warpIdCol[col] = hit.warpId;
|
warpIdCol[col] = hit.warpId;
|
||||||
warpDistCol[col] = (hit.warpDist ?? 0) * Math.cos(rayAngle - facing);
|
warpDistCol[col] = (hit.warpDist ?? 0) * Math.cos(rayAngle - facing);
|
||||||
veils.push({ col, depth: warpDistCol[col]!, u: hit.warpU ?? 0 });
|
veils.push({ col, depth: warpDistCol[col]!, u: hit.warpU ?? 0, warp: hit.warpId });
|
||||||
}
|
}
|
||||||
if (hit.ghost) {
|
if (hit.ghost) {
|
||||||
ghosts.push({
|
ghosts.push({
|
||||||
@@ -598,14 +603,21 @@
|
|||||||
|
|
||||||
// Warp mouths wear their veil: the warp texture at the opening,
|
// Warp mouths wear their veil: the warp texture at the opening,
|
||||||
// translucent, swirling — the doorway between rooms that are not
|
// translucent, swirling — the doorway between rooms that are not
|
||||||
// neighbors announces itself.
|
// neighbors announces itself. A mouth something just passed through
|
||||||
|
// surges: the texture floods in, the swirl quickens and pales, and
|
||||||
|
// a bright ripple runs out from the crossing point to the jambs.
|
||||||
for (const vl of veils) {
|
for (const vl of veils) {
|
||||||
const vh = Math.min(H * 2.5, H / Math.max(vl.depth, 0.05));
|
const vh = Math.min(H * 2.5, H / Math.max(vl.depth, 0.05));
|
||||||
const vTop = half - vh / 2;
|
const vTop = half - vh / 2;
|
||||||
const wt = textures.warp!;
|
const wt = textures.warp!;
|
||||||
const swirl = 0.1 + 0.1 * Math.sin(time / 240 + vl.u * 9 + vl.col * 0.02);
|
const s = surging[vl.warp] ?? 0;
|
||||||
|
const swirl = 0.1 + 0.1 * Math.sin(time / (240 - 170 * s) + vl.u * 9 + vl.col * 0.02);
|
||||||
|
// The ripple: a ring of light widening from the veil's center over
|
||||||
|
// the surge, one full sweep to the edges; ~a fifth of the veil wide.
|
||||||
|
const ring = s > 0 ? Math.max(0, 1 - Math.abs(Math.abs(vl.u - 0.5) * 2 - (1 - s)) / 0.2) : 0;
|
||||||
|
const glow = s * (0.45 * ring + 0.22);
|
||||||
for (const [segTop, segH] of maskedSegs(vl.col, vl.depth, vTop, vh)) {
|
for (const [segTop, segH] of maskedSegs(vl.col, vl.depth, vTop, vh)) {
|
||||||
ctx.globalAlpha = 0.3;
|
ctx.globalAlpha = 0.3 + 0.5 * s;
|
||||||
ctx.drawImage(wt, Math.min(wt.width - 1, vl.u * wt.width),
|
ctx.drawImage(wt, Math.min(wt.width - 1, vl.u * wt.width),
|
||||||
((segTop - vTop) / vh) * wt.height,
|
((segTop - vTop) / vh) * wt.height,
|
||||||
Math.max(1, wt.width / 96), (segH / vh) * wt.height,
|
Math.max(1, wt.width / 96), (segH / vh) * wt.height,
|
||||||
@@ -613,6 +625,10 @@
|
|||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
ctx.fillStyle = `rgba(190,150,255,${Math.max(0, swirl)})`;
|
ctx.fillStyle = `rgba(190,150,255,${Math.max(0, swirl)})`;
|
||||||
ctx.fillRect(vl.col, segTop, 1, segH);
|
ctx.fillRect(vl.col, segTop, 1, segH);
|
||||||
|
if (glow > 0) {
|
||||||
|
ctx.fillStyle = `rgba(235,220,255,${Math.min(0.85, glow)})`;
|
||||||
|
ctx.fillRect(vl.col, segTop, 1, segH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,12 @@ export function aimOfEvents(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const { fx } of fpFxForEvents(events, view, povId)) {
|
for (const { fx } of fpFxForEvents(events, view, povId)) {
|
||||||
const spot = fx.kind === "impact" ? fx.at : fx.kind === "projectile" ? fx.to : null;
|
// A surging veil is watched from the mouth's own cell — through
|
||||||
|
// the warp, if that is where the eyes just came from.
|
||||||
|
const spot = fx.kind === "impact" ? fx.at
|
||||||
|
: fx.kind === "projectile" ? fx.to
|
||||||
|
: fx.kind === "surge" ? { x: view.board.warps[fx.warp]!.from.cell.x + 0.5, y: view.board.warps[fx.warp]!.from.cell.y + 0.5 }
|
||||||
|
: null;
|
||||||
if (!spot) continue;
|
if (!spot) continue;
|
||||||
const cx = Math.floor(spot.x), cy = Math.floor(spot.y);
|
const cx = Math.floor(spot.x), cy = Math.floor(spot.y);
|
||||||
if (cx === myCell.x && cy === myCell.y) return { x: cx, y: cy, self: true };
|
if (cx === myCell.x && cy === myCell.y) return { x: cx, y: cy, self: true };
|
||||||
|
|||||||
@@ -21,8 +21,21 @@ export type FpFx =
|
|||||||
| { id: number; kind: "flash"; color: string; peak: number; t0: number; dur: number }
|
| { id: number; kind: "flash"; color: string; peak: number; t0: number; dur: number }
|
||||||
| { id: number; kind: "shake"; mag: number; t0: number; dur: number }
|
| { id: number; kind: "shake"; mag: number; t0: number; dur: number }
|
||||||
| { id: number; kind: "door"; edge: string; t0: number; dur: number }
|
| { id: number; kind: "door"; edge: string; t0: number; dur: number }
|
||||||
|
/** A body passed through a warp mouth (or the mouth just opened): the
|
||||||
|
* veil hung in that opening surges — the warp's own face animates,
|
||||||
|
* no billboard over whoever stands in it. */
|
||||||
|
| { id: number; kind: "surge"; warp: number; t0: number; dur: number }
|
||||||
| { id: number; kind: "grow"; slot: "solid" | "sprite"; key: string; t0: number; dur: number };
|
| { id: number; kind: "grow"; slot: "solid" | "sprite"; key: string; t0: number; dur: number };
|
||||||
|
|
||||||
|
/** How hard a surging veil glows at progress p: a snap to full as the
|
||||||
|
* body breaks the surface, then a long settle back to the idle swirl. */
|
||||||
|
export function surgeIntensity(p: number): number {
|
||||||
|
if (p < 0 || p >= 1) return 0;
|
||||||
|
if (p < 0.12) return p / 0.12;
|
||||||
|
const q = (p - 0.12) / 0.88;
|
||||||
|
return (1 - q) * (1 - q);
|
||||||
|
}
|
||||||
|
|
||||||
/** Eased growth 0..1 for a conjuration's rise. */
|
/** Eased growth 0..1 for a conjuration's rise. */
|
||||||
export function growProgress(p: number): number {
|
export function growProgress(p: number): number {
|
||||||
if (p <= 0) return 0.01;
|
if (p <= 0) return 0.01;
|
||||||
@@ -112,10 +125,20 @@ export function fpFxForEvents(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (fx.kind === "portal") {
|
} else if (fx.kind === "portal") {
|
||||||
|
// The board ripples the mouth; here the mouth's own veil surges.
|
||||||
|
// Every mouth heads one entry of board.warps, the index the
|
||||||
|
// raycaster tags its columns with.
|
||||||
|
const warp = view.board.warps.findIndex(
|
||||||
|
(w) => w.from.cell.x === fx.cell.x && w.from.cell.y === fx.cell.y && w.from.side === fx.side,
|
||||||
|
);
|
||||||
|
if (warp >= 0) {
|
||||||
|
out.push({ fx: { id: nextId++, kind: "surge", warp, t0: 0, dur: 900 }, delay });
|
||||||
|
} else {
|
||||||
out.push({
|
out.push({
|
||||||
fx: { id: nextId++, kind: "impact", art: "shimmer", at: { x: fx.cell.x + 0.5, y: fx.cell.y + 0.5 }, t0: 0, dur: 550 },
|
fx: { id: nextId++, kind: "impact", art: "shimmer", at: { x: fx.cell.x + 0.5, y: fx.cell.y + 0.5 }, t0: 0, dur: 550 },
|
||||||
delay,
|
delay,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} else if ("at" in fx && IMPACT_ART[fx.kind]) {
|
} else if ("at" in fx && IMPACT_ART[fx.kind]) {
|
||||||
out.push({
|
out.push({
|
||||||
fx: { id: nextId++, kind: "impact", art: IMPACT_ART[fx.kind]!, at: { x: fx.at.x + 0.5, y: fx.at.y + 0.5 }, t0: 0, dur: 550 },
|
fx: { id: nextId++, kind: "impact", art: IMPACT_ART[fx.kind]!, at: { x: fx.at.x + 0.5, y: fx.at.y + 0.5 }, t0: 0, dur: 550 },
|
||||||
|
|||||||
Reference in New Issue
Block a user