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:
co-authored by
Claude Fable 5.1
parent
85690f0e23
commit
6e49fc9020
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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 }];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user