The board's furniture and the reel's fists, painted as cels
The last of the inked look. Walls carry stone joints and highlights, doors their grain, hinges, and a brass lock plate whose keyhole turns to a cross when jammed; battle scars fracture and branch with the damage; illusion walls drift as tapered wisps with breathing glints; the wall of fire burns in flat three-tone flames on a stepped flicker; warp mouths and destinations are painted curls. Through a wizard's eyes the punch is a three-tone hand with an ink contour and a hand-cut POW, the same on every platform where the emoji glyph was not, and the incoming fist no longer reverses late in its travel. The inked stand-ins for missing conjuration files match. Goldens re-blessed: every board frame, and the web's third ring. 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
8dfd0e0c29
commit
34b621b5cb
@@ -4,8 +4,9 @@
|
||||
// billboarded for whatever stands in the corridors, occluded per column
|
||||
// by the same depth buffer the walls wrote.
|
||||
import { billboards, castRay, warpMotion, type Billboard, type FpvTarget } from "./raycast";
|
||||
import { paintFist, paintPow, punchPose, paintWeb, paintFloorRing } from "./paintedFx";
|
||||
import { materialTextures } from "./textures";
|
||||
import { doorOpenness, fxFallback, growProgress, slateBand, smoothstep, surgeIntensity, type FpFx } from "./fx3d";
|
||||
import { doorOpenness, fxFallback, growProgress, slateBand, surgeIntensity, type FpFx } from "./fx3d";
|
||||
import { terrainFallback, TERRAIN3D } from "./terrain3d";
|
||||
import { tokenArt } from "../art";
|
||||
import { PLAYER_COLORS } from "../colors";
|
||||
@@ -589,40 +590,10 @@
|
||||
if (s.glow) ctx.globalCompositeOperation = "source-over";
|
||||
if (s.alpha !== undefined || s.glow) ctx.globalAlpha = 1;
|
||||
// STICKY WAND's webs cling over the caught body: spokes from a hub
|
||||
// and two sagging rings, each strand shadowed so the silk reads
|
||||
// and three sagging rings, each strand shadowed so the silk reads
|
||||
// against busy token art. Only when the walls show the sprite.
|
||||
if (s.webbed && s.hit && seen) {
|
||||
const w = s.right - s.left, h = s.bottom - s.top;
|
||||
const hubX = s.left + w * 0.5, hubY = s.top + h * 0.45;
|
||||
const rimU: [number, number][] = [
|
||||
[0.02, 0.06], [0.5, 0.0], [0.98, 0.08], [1.0, 0.5],
|
||||
[0.96, 0.94], [0.5, 1.0], [0.04, 0.92], [0.0, 0.5],
|
||||
];
|
||||
const rim: [number, number][] = rimU.map(([u, v]) => [s.left + u * w, s.top + v * h]);
|
||||
const web = () => {
|
||||
ctx.beginPath();
|
||||
for (const [rx, ry] of rim) {
|
||||
ctx.moveTo(hubX, hubY);
|
||||
ctx.lineTo(rx, ry);
|
||||
}
|
||||
for (const t of [0.45, 0.75]) {
|
||||
for (let i = 0; i <= rim.length; i++) {
|
||||
const [rx, ry] = rim[i % rim.length]!;
|
||||
const px = hubX + (rx - hubX) * t, py = hubY + (ry - hubY) * t + h * 0.02;
|
||||
if (i === 0) ctx.moveTo(px, py);
|
||||
else ctx.lineTo(px, py);
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
};
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(25,25,30,0.45)";
|
||||
ctx.lineWidth = 2.5;
|
||||
web();
|
||||
ctx.strokeStyle = "rgba(245,245,240,0.8)";
|
||||
ctx.lineWidth = 1;
|
||||
web();
|
||||
ctx.restore();
|
||||
paintWeb(ctx, s.left, s.top, s.right - s.left, s.bottom - s.top);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -742,60 +713,19 @@
|
||||
if (f.kind !== "fist") continue;
|
||||
const p = (time - f.t0) / f.dur;
|
||||
if (p < 0 || p >= 1) continue;
|
||||
const ease = smoothstep;
|
||||
const out = f.swing === "out";
|
||||
// Travel: out 0→0.42 lunge, 0.42→1 retract; in 0→0.45 approach, then hold and fade.
|
||||
const a = out ? (p < 0.42 ? ease(p / 0.42) : ease(1 - (p - 0.42) / 0.58)) : Math.min(1, ease(p / 0.45));
|
||||
const sx = out ? W * 0.86 : W * 0.5, sy = out ? H * 1.15 : H * 0.55;
|
||||
const tx = W * 0.56, ty = H * 0.5;
|
||||
const fx0 = sx + (tx - sx) * a, fy0 = sy + (ty - sy) * a;
|
||||
const size = out ? H * (0.62 - 0.30 * a) : H * (0.18 + 1.25 * a);
|
||||
const glyph = out ? "\u{1F91C}" : "\u{1F44A}";
|
||||
const pose = punchPose(p, f.swing === "out", W, H);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = !out && p > 0.75 ? 1 - (p - 0.75) / 0.25 : 1;
|
||||
ctx.translate(fx0, fy0);
|
||||
ctx.rotate(out ? -0.6 : 0.15);
|
||||
ctx.font = `${Math.round(size)}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.shadowColor = "rgba(0,0,0,0.55)";
|
||||
ctx.shadowBlur = 12;
|
||||
ctx.shadowOffsetY = 6;
|
||||
ctx.fillText(glyph, 0, 0);
|
||||
ctx.globalAlpha = pose.alpha;
|
||||
ctx.translate(pose.x, pose.y);
|
||||
ctx.rotate(pose.rotation);
|
||||
paintFist(ctx, pose.size, f.swing);
|
||||
ctx.restore();
|
||||
// POW: pops at the landing, holds, and shrinks away.
|
||||
const land = out ? 0.36 : 0.4;
|
||||
if (p >= land) {
|
||||
const q = (p - land) / (1 - land);
|
||||
const pop = q < 0.18 ? ease(q / 0.18) * 1.15 : q < 0.7 ? 1.15 - 0.15 * ease((q - 0.18) / 0.52) : 1 - ease((q - 0.7) / 0.3);
|
||||
if (pop > 0.01) {
|
||||
const R = H * 0.19 * pop;
|
||||
const cx = out ? tx - W * 0.02 : W * 0.5, cy = out ? ty - H * 0.16 : H * 0.34;
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy);
|
||||
ctx.rotate(-0.14);
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const r = i % 2 === 0 ? R : R * 0.68;
|
||||
const t = (i / 24) * Math.PI * 2;
|
||||
ctx.lineTo(Math.cos(t) * r * 1.35, Math.sin(t) * r);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = "#f2d24a";
|
||||
ctx.strokeStyle = "#2b2218";
|
||||
ctx.lineWidth = Math.max(2, R * 0.08);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#c03020";
|
||||
ctx.strokeStyle = "#2b2218";
|
||||
ctx.lineWidth = Math.max(1.5, R * 0.06);
|
||||
ctx.font = `600 ${Math.round(R * 0.95)}px Oswald, "Archivo Narrow", sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.strokeText("POW!", 0, R * 0.04);
|
||||
ctx.fillText("POW!", 0, R * 0.04);
|
||||
ctx.restore();
|
||||
}
|
||||
if (pose.radius > H * 0.19 * 0.01) {
|
||||
ctx.save();
|
||||
ctx.translate(pose.impactX, pose.impactY);
|
||||
ctx.rotate(-0.14);
|
||||
paintPow(ctx, pose.radius);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,15 +776,7 @@
|
||||
* the stroke at `alpha`, the fill fainter. */
|
||||
function floorRing(ctx: CanvasRenderingContext2D, cx: number, cy: number, rx: number, ry: number,
|
||||
rgb: string, alpha: number, fill: number) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = `rgba(${rgb},${alpha})`;
|
||||
ctx.fillStyle = `rgba(${rgb},${fill})`;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
paintFloorRing(ctx, cx, cy, rx, ry, rgb, alpha, fill);
|
||||
}
|
||||
|
||||
/** Paint the hover cue for whatever stands under the crosshair. */
|
||||
|
||||
Reference in New Issue
Block a user