Credibility pass: comments say what the code can't, and the seams are gone

Engine: pit-rim exits live in one helper (pitRimExits) shared by the
resolver and the automaton; chargeReach names the six-stride cap; dust
sight is inDust + dustAtEnd; PushPending is exported once; the force-field
counter builds its events in one list; drawUnderSlowDeath says what it
draws under. Test groups are named for the rule they pin, not the
revision that introduced it.

Client: one smoothstep; one edgeScar per battle-scarred edge; one
board-zone rule (which also seats the caption strip on phones); the
compass names live in SIDE_NAMES; net.flash() is the one toast; MediaQuery
replaces two hand-rolled matchMedia listeners; sprites carry their sizes
as plain numbers and their CSS in one rule each; canvas helpers are
formatted like the rest of the tree. Comments that told how a cel or a
fallback used to look now say what it draws. The actions-over notice no
longer blames a pickup when slime ended the turn.

Server and ops: dataRoot() is the one data directory; headlineOf builds
its deeds without a cast; the rollup and skills read the same way the
code behaves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-16 00:21:34 -04:00
co-authored by Claude Fable 5.1
parent 85690f0e23
commit 6e49fc9020
40 changed files with 331 additions and 293 deletions
+4 -9
View File
@@ -5,7 +5,7 @@
// by the same depth buffer the walls wrote.
import { motion } from "../motion";
import { billboards, castRay, warpMotion, type Billboard, type FpvTarget } from "./raycast";
import { paintFist, paintPow, punchPose, paintWeb, paintFloorRing } from "./paintedFx";
import { paintFist, paintPow, punchPose, paintWeb, paintFloorRing as floorRing } from "./paintedFx";
import { materialTextures } from "./textures";
import { doorOpenness, fxFallback, growProgress, slateBand, surgeIntensity, type FpFx } from "./fx3d";
import { terrainFallback, TERRAIN3D } from "./terrain3d";
@@ -198,7 +198,8 @@
}
function draw(time: number) {
// Keep the final game state and static hazard/target cues visible.
// With effects off the frame still draws — walls, bodies, hazards —
// but nothing moves: no fx, and the ambient pulses freeze at t=0.
const activeFx = motion.effects ? fx : [];
const ambientTime = motion.effects ? time : 0;
const ctx = canvas?.getContext("2d");
@@ -724,7 +725,7 @@
ctx.rotate(pose.rotation);
paintFist(ctx, pose.size, f.swing);
ctx.restore();
if (pose.radius > H * 0.19 * 0.01) {
if (pose.radius > 0.5) {
ctx.save();
ctx.translate(pose.impactX, pose.impactY);
ctx.rotate(-0.14);
@@ -776,12 +777,6 @@
if ((ontarget || hover) && mouse) drawHover(ctx, W, H, half);
}
/** An ellipse of light on the floor at a body's feet: `rgb` as "r,g,b",
* 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) {
paintFloorRing(ctx, cx, cy, rx, ry, rgb, alpha, fill);
}
/** Paint the hover cue for whatever stands under the crosshair. */
function drawHover(ctx: CanvasRenderingContext2D, W: number, H: number, half: number) {
+4 -3
View File
@@ -10,7 +10,7 @@
import FirstPerson from "./FirstPerson.svelte";
import Replay from "../Replay.svelte";
import { canWalk, edgeMid, SIDE_ANGLE, OPPOSITE } from "./raycast";
import { buildScreenplaySteps, screenplayByName, SCREENPLAYS } from "./screenplays";
import { buildScreenplaySteps, screenplayByName, SCREENPLAYS, type ScreenplayStep } from "./screenplays";
const q = new URLSearchParams(location.search);
const seed = Number(q.get("seed") ?? 42);
@@ -206,7 +206,7 @@
// a refused move reports itself instead of a blank stage.
const scriptName = q.get("script");
const scriptPlay = scriptName ? screenplayByName(scriptName) : null;
const { scriptSteps, scriptError } = (() => {
function loadScript(): { scriptSteps: ScreenplayStep[]; scriptError: string | null } {
if (scriptName && !scriptPlay) {
return { scriptSteps: [], scriptError: `No screenplay named "${scriptName}". The catalog: ${SCREENPLAYS.map((sp) => sp.name).join(", ")}` };
}
@@ -215,7 +215,8 @@
} catch (e) {
return { scriptSteps: [], scriptError: e instanceof Error ? e.message : String(e) };
}
})();
}
const { scriptSteps, scriptError } = loadScript();
// Minimap geometry (top-down, one small square per cell).
const MM = 9;
+3 -5
View File
@@ -4,7 +4,8 @@
// camera shakes for the raycaster to draw. What happens ACROSS the room
// is watched; what happens to YOU is felt.
import { fallbackCels, paintCels } from "./paintedFx";
import { fallbackCels, paintCels, smoothstep } from "./paintedFx";
export { smoothstep };
import { fxForEvents } from "../fx";
import { CELL } from "../fx-sprites/geom";
import { castRay, edgeMid, warpMotion } from "./raycast";
@@ -55,9 +56,6 @@ export function slateBand(p: number): [number, number] {
return [smoothstep((p - 0.68) / 0.32), 1];
}
/** The one easing every tween here shares. */
export const smoothstep = (w: number): number => w * w * (3 - 2 * w);
/** Eased growth 0..1 for a conjuration's rise. */
export function growProgress(p: number): number {
if (p <= 0) return 0.01;
@@ -306,7 +304,7 @@ export function fpFxForEvents(
return out;
}
/** Inked stand-ins in each original palette, replaced when the PNG loads. */
/** Core and rim colors for each conjuration's inked stand-in. */
const FX_COLORS: Record<string, [string, string]> = {
fireball: ["#ffe27a", "#e0431a"],
bolt: ["#ffffff", "#7ab0ff"],
+81 -41
View File
@@ -1,10 +1,11 @@
/** Inked canvas cels. Paths are shared by the renderer and artwork previews. */
export const smoothstep = (w: number): number => w * w * (3 - 2 * w);
export type Cel = { d: string; fill?: string; stroke?: string; width?: number };
const INK = "#2b2218";
const ochre = "#dba54b", light = "#f2ce78", shade = "#95602e";
/** Poses modeled on the original right-facing and oncoming fist emoji:
* folded fingers face the viewer; the thumb stays outside their curl. */
/** Two fist poses: a punch thrown (wrist left, knuckles right) and one
* arriving (knuckles to the viewer). The thumb stays outside the curl. */
export const FISTS: Record<"out" | "in", Cel[]> = {
out: [
// Wrist at left, back of hand above, bent index at the right edge.
@@ -33,7 +34,7 @@ export const FISTS: Record<"out" | "in", Cel[]> = {
],
};
/** Deliberately uneven impact silhouette and hand-cut lettering; no font dependency. */
/** The POW starburst and its lettering, as paths: the canvas has no font to wait on. */
export const POW: Cel[] = [
{ d: "M-65-26 L-115-65 -53-53 -59-91 -18-60 8-88 17-57 63-79 53-43 119-43 82-13 126 13 78 23 98 64 45 48 27 86 4 57 -36 82 -39 48 -100 66 -76 28 -126 14 -81-5 -115-28Z", fill: "#f2d24a", width: 5 },
{ d: "M-83-38 L-60-31 M49-50 L59-60 M77 29 L88 37 M-51 46 L-64 55", stroke: "#c03020", width: 3 },
@@ -57,58 +58,97 @@ export function paintCels(ctx: CanvasRenderingContext2D, cels: readonly Cel[]) {
ctx.restore();
}
export function paintFist(ctx: CanvasRenderingContext2D, size: number, swing: "out" | "in") {
ctx.save(); ctx.scale(size / 100, size / 100); paintCels(ctx, FISTS[swing]); ctx.restore();
}
export function paintPow(ctx: CanvasRenderingContext2D, radius: number) {
ctx.save(); ctx.scale(radius / 100, radius / 100); paintCels(ctx, POW); ctx.restore();
ctx.save();
ctx.scale(size / 100, size / 100);
paintCels(ctx, FISTS[swing]);
ctx.restore();
}
/** Same endpoints and landing beats as the original animation. */
export function paintPow(ctx: CanvasRenderingContext2D, radius: number) {
ctx.save();
ctx.scale(radius / 100, radius / 100);
paintCels(ctx, POW);
ctx.restore();
}
/** Where the fist is at progress `p`: thrown, it lunges to 0.42 and
* retracts; arriving, it closes to 0.45, holds, and fades. The POW pops
* at the landing beat and shrinks away. */
export function punchPose(p: number, out: boolean, W: number, H: number) {
const ease = (v: number) => v * v * (3 - 2 * v);
// Clamp before easing: otherwise the incoming fist reverses past p=.45.
const a = out ? (p < .42 ? ease(p / .42) : ease(1 - (p - .42) / .58)) : ease(Math.min(1, p / .45));
const sx = out ? W * .86 : W * .5, sy = out ? H * 1.15 : H * .55;
const tx = W * .56, ty = H * .5;
const land = out ? .36 : .4, q = (p - land) / (1 - land);
const pop = p < land ? 0 : q < .18 ? ease(q / .18) * 1.15 : q < .7 ? 1.15 - .15 * ease((q - .18) / .52) : 1 - ease((q - .7) / .3);
const ease = smoothstep;
// Clamp before easing: past 0.45 the arriving fist would reverse.
const a = out ? (p < 0.42 ? ease(p / 0.42) : ease(1 - (p - 0.42) / 0.58)) : ease(Math.min(1, 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 land = out ? 0.36 : 0.4, q = (p - land) / (1 - land);
const pop = p < land ? 0 : 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);
return { x: sx + (tx - sx) * a, y: sy + (ty - sy) * a,
size: out ? H * (.62 - .30 * a) : H * (.18 + 1.25 * a),
alpha: !out && p > .75 ? 1 - (p - .75) / .25 : 1,
rotation: out ? -.6 : .15, radius: H * .19 * pop,
impactX: out ? tx - W * .02 : W * .5, impactY: H * .34 };
size: out ? H * (0.62 - 0.30 * a) : H * (0.18 + 1.25 * a),
alpha: !out && p > 0.75 ? 1 - (p - 0.75) / 0.25 : 1,
rotation: out ? -0.6 : 0.15, radius: H * 0.19 * pop,
impactX: out ? tx - W * 0.02 : W * 0.5, impactY: H * 0.34 };
}
export function paintWeb(ctx: CanvasRenderingContext2D, left: number, top: number, w: number, h: number) {
const hub = [left + w * .5, top + h * .45];
const rim = [[.02,.06],[.5,0],[.98,.08],[1,.5],[.96,.94],[.5,1],[.04,.92],[0,.5]].map(([u,v]) => [left + u! * w, top + v! * h]);
const at = (i: number, t: number) => { const r = rim[i % 8]!; return [hub[0]! + (r[0]! - hub[0]!) * t, hub[1]! + (r[1]! - hub[1]!) * t]; };
ctx.save(); ctx.lineJoin = "round"; ctx.lineCap = "round";
type Pt = [number, number];
const hub: Pt = [left + w * 0.5, top + h * 0.45];
const rim: Pt[] = ([[0.02, 0.06], [0.5, 0], [0.98, 0.08], [1, 0.5], [0.96, 0.94], [0.5, 1], [0.04, 0.92], [0, 0.5]] as Pt[])
.map(([u, v]) => [left + u * w, top + v * h]);
const at = (i: number, t: number): Pt => {
const r = rim[i % 8] as Pt;
return [hub[0] + (r[0] - hub[0]) * t, hub[1] + (r[1] - hub[1]) * t];
};
ctx.save();
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.beginPath();
for (const r of rim) { ctx.moveTo(hub[0]!,hub[1]!); ctx.lineTo(r[0]!,r[1]!); }
for (const t of [.27,.53,.8]) for (let i = 0; i < 8; i++) {
const a = at(i,t), b = at(i+1,t);
ctx.moveTo(a[0]!,a[1]!);
ctx.quadraticCurveTo((a[0]!+b[0]!)*.36+hub[0]!*.28,(a[1]!+b[1]!)*.36+hub[1]!*.28,b[0]!,b[1]!);
for (const r of rim) {
ctx.moveTo(hub[0], hub[1]);
ctx.lineTo(r[0], r[1]);
}
ctx.strokeStyle = "rgba(25,25,30,0.65)"; ctx.lineWidth = 3; ctx.stroke();
ctx.strokeStyle = "rgba(245,245,240,0.9)"; ctx.lineWidth = 1.3; ctx.stroke(); ctx.restore();
// Three sagging rings, each strand bowed toward the hub.
for (const t of [0.27, 0.53, 0.8]) {
for (let i = 0; i < 8; i++) {
const a = at(i, t), b = at(i + 1, t);
ctx.moveTo(a[0], a[1]);
ctx.quadraticCurveTo((a[0] + b[0]) * 0.36 + hub[0] * 0.28, (a[1] + b[1]) * 0.36 + hub[1] * 0.28, b[0], b[1]);
}
}
ctx.strokeStyle = "rgba(25,25,30,0.65)";
ctx.lineWidth = 3;
ctx.stroke();
ctx.strokeStyle = "rgba(245,245,240,0.9)";
ctx.lineWidth = 1.3;
ctx.stroke();
ctx.restore();
}
export function paintFloorRing(ctx: CanvasRenderingContext2D, cx: number, cy: number, rx: number, ry: number, rgb: string, alpha: number, fill: number) {
ctx.save(); ctx.fillStyle = `rgba(${rgb},${fill})`; ctx.strokeStyle = `rgba(${rgb},${alpha})`; ctx.lineWidth = 2;
ctx.beginPath(); ctx.ellipse(cx,cy,rx,ry,0,0,Math.PI*2); ctx.fill();
export function paintFloorRing(
ctx: CanvasRenderingContext2D, cx: number, cy: number, rx: number, ry: number, rgb: string, alpha: number, fill: number,
) {
ctx.save();
ctx.fillStyle = `rgba(${rgb},${fill})`;
ctx.strokeStyle = `rgba(${rgb},${alpha})`;
ctx.lineWidth = 2;
ctx.beginPath();
const segments = [[.06,1.75],[1.87,3.48],[3.62,5.04],[5.18,6.22]];
for (const [start,end] of segments) for (let i=0; i<=20; i++) {
const t = start! + (end!-start!) * i/20, k = 1 - .018 * (1 + Math.sin(t*5));
const x = cx + Math.cos(t)*rx*k, y = cy + Math.sin(t)*ry*k;
if (!i) ctx.moveTo(x,y); else ctx.lineTo(x,y);
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
ctx.fill();
// The rim is inked in four strokes with a slight tremor, not one perfect ellipse.
ctx.beginPath();
const segments: [number, number][] = [[0.06, 1.75], [1.87, 3.48], [3.62, 5.04], [5.18, 6.22]];
for (const [start, end] of segments) {
for (let i = 0; i <= 20; i++) {
const t = start + (end - start) * i / 20;
const k = 1 - 0.018 * (1 + Math.sin(t * 5));
const x = cx + Math.cos(t) * rx * k, y = cy + Math.sin(t) * ry * k;
if (!i) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
}
ctx.stroke(); ctx.restore();
ctx.stroke();
ctx.restore();
}
/** Small, legible loading/error sprites using the original fallback palettes. */
/** The inked silhouette drawn in a conjuration's palette until its PNG loads. */
export function fallbackCels(name: string, core: string, rim: string): Cel[] {
const outer: Record<string, string> = {
fireball: "M20 78 Q3 54 28 32 L19 10 Q42 17 48 37 Q68 11 58 3 Q96 24 85 62 Q81 83 60 88 Q33 98 20 78Z",
@@ -135,5 +175,5 @@ export function fallbackCels(name: string, core: string, rim: string): Cel[] {
rubble:"M5 85 L18 79 29 83 24 90 11 91Z M36 72 L48 66 60 73 55 81 40 80Z M69 84 L80 79 89 84 83 91 73 92Z",
};
const kind = outer[name] ? name : "spark";
return [{d:outer[kind]!,fill:rim,width:3},{d:inside[kind]!,fill:core,width:1.5}];
return [{ d: outer[kind]!, fill: rim, width: 3 }, { d: inside[kind]!, fill: core, width: 1.5 }];
}