Credibility pass: the session seams sanded from the clips-and-camera batch
Two blind reviews of everything since the last pass (afa0e17), every
finding checked against the code, no behavior changed: all thirty
scene goldens match without a re-bless and the engine suite is
untouched.
Reel and renderer: the camera's look-down rule is stated once, beside
LOOK_DOWN, instead of twice in the effect; the empty aim branch that
stood where a cutaway used to be is gone (the guard it implied is now
explicit); the pit events and the punch are handled by their own
types, not through "in" casts; smoothstep is one export used by every
tween instead of eleven inline copies; the two floor rings share one
painter; project() takes a Billboard instead of a third hand-typed
copy of its fields; the strides-left figure and the web rim no longer
shadow the reel's steps and the pane's fx; the die card's verdict is
built from events, not by matching an emoji; the workshop asks for the
hover cue by name instead of passing an empty click handler.
Server and engine: one requestBase() for the origin, one slug pattern
in store.ts gating both the clip page and its files, one 404 for both;
LOOPBACK sits above its only caller; doCounteract names what a counter
is played against once; fearCells sits beside its own docblock rather
than between sightedCellsFor and its.
Deploy: chromiumExe, the private server, ffmpeg, and the reel rewind
live in deploy/lib/harness.mjs, shared by the gate, the recorder, and
the card cutter instead of pasted three times; the recorder drops its
duplicate frame counters and names its poster settle; the card uses the
gallery's exact gold; the one-time Sentry URL bootstrap leaves
deploy.sh; the backup comment states the rule rather than the incident.
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
ddd2e347bd
commit
e1a740119c
@@ -5,9 +5,9 @@
|
||||
import { humanize, spellName } from "./net.svelte";
|
||||
import { tokenArt } from "./art";
|
||||
import { scheduleFx, type BoardFx } from "./fx";
|
||||
import { fpFxForEvents, type FpFx } from "./fpv/fx3d";
|
||||
import { fpFxForEvents, smoothstep, type FpFx } from "./fpv/fx3d";
|
||||
import { castRay, edgeMid } from "./fpv/raycast";
|
||||
import { cutawayStand, deepestFacing, aimOfEvents, gatherGlides, hurledIn, shortestArc, sightline } from "./fpv/director";
|
||||
import { cutawayStand, aimOfEvents, gatherGlides, hurledIn, shortestArc, sightline } from "./fpv/director";
|
||||
import { prefs } from "./prefs.svelte";
|
||||
import { cardDef, isPermanentDuration, stackSightTrace } from "@wizwar/engine";
|
||||
import type { GameEvent, GameView } from "@wizwar/engine";
|
||||
@@ -99,7 +99,9 @@
|
||||
const t0 = performance.now();
|
||||
const dur = DIE_MS / speed;
|
||||
const who = d.player ?? "The maze";
|
||||
die = { idx: i, player: who, roll: d.roll, purpose: d.purpose, face: 1, landed: false };
|
||||
const show = (face: number, landed: boolean) =>
|
||||
(die = { idx: i, player: who, roll: d.roll, purpose: d.purpose, face, landed });
|
||||
show(1, false);
|
||||
let raf = 0;
|
||||
const tick = (now: number) => {
|
||||
const p = Math.max(0, now - t0) / dur;
|
||||
@@ -108,9 +110,9 @@
|
||||
// The tumble: faces flick past, slowing as the die settles. A
|
||||
// fixed sequence, so the harness films the same roll every time.
|
||||
const n = Math.floor(p * 26 - p * p * 18);
|
||||
die = { idx: i, player: who, roll: d.roll, purpose: d.purpose, face: ((n * 3 + 1) % 4) + 1, landed: false };
|
||||
show(((n * 3 + 1) % 4) + 1, false);
|
||||
} else if (!die?.landed) {
|
||||
die = { idx: i, player: who, roll: d.roll, purpose: d.purpose, face: d.roll, landed: true };
|
||||
show(d.roll, true);
|
||||
}
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
@@ -122,15 +124,14 @@
|
||||
/** What you drew belongs in the chronicle, not on a reel that may be
|
||||
* recorded and passed around — captions keep the count, not the cards. */
|
||||
const CAPTION_SILENT = new Set(["cardsDrawnPrivate", "cardsDealtPrivate", "cardsStolenPrivate"]);
|
||||
const lines = $derived(
|
||||
step.events
|
||||
.filter((e) => !CAPTION_SILENT.has(e.type))
|
||||
.map(humanize)
|
||||
.filter((l): l is string => l !== null),
|
||||
);
|
||||
const captionLines = (events: GameEvent[], silent: Set<string>) =>
|
||||
events.filter((e) => !silent.has(e.type)).map(humanize).filter((l): l is string => l !== null);
|
||||
const lines = $derived(captionLines(step.events, CAPTION_SILENT));
|
||||
const atEnd = $derived(idx >= steps.length - 1);
|
||||
/** What the roll brought about, in the caption's own words. */
|
||||
const dieVerdict = $derived(lines.filter((l) => !l.startsWith("\u{1F3B2}")).slice(0, 2).join(" "));
|
||||
/** What the roll brought about, in the caption's own words — the roll
|
||||
* itself is on the die card, so its line is left out. */
|
||||
const VERDICT_SILENT = new Set([...CAPTION_SILENT, "dieRolled"]);
|
||||
const dieVerdict = $derived(captionLines(step.events, VERDICT_SILENT).slice(0, 2).join(" "));
|
||||
|
||||
/** The acting wizard's standee, shown beside their words. */
|
||||
const actorArt = $derived.by(() => {
|
||||
@@ -147,7 +148,7 @@
|
||||
const p = v.players.find((x) => x.id === step.actor);
|
||||
if (!p) return null;
|
||||
const acting = v.activePlayerId === p.id;
|
||||
const steps = acting ? Math.max(0, v.turn.movementAllowance - v.turn.movementUsed) : null;
|
||||
const strides = acting ? Math.max(0, v.turn.movementAllowance - v.turn.movementUsed) : null;
|
||||
const chips: string[] = [`♥ ${p.life}`];
|
||||
if (p.carriedTreasureId) chips.push("carrying a treasure");
|
||||
for (const c of p.displayed) chips.push(cardDef(c.cardId).name.toLowerCase());
|
||||
@@ -158,7 +159,7 @@
|
||||
if (p.lostTurns > 0) chips.push(`loses ${p.lostTurns} turn${p.lostTurns === 1 ? "" : "s"}`);
|
||||
if (p.passWallCharges > 0) chips.push(`pass wall × ${p.passWallCharges}`);
|
||||
if (acting && v.turn.attackUsed) chips.push("attack spent");
|
||||
return { steps, chips };
|
||||
return { strides, chips };
|
||||
});
|
||||
|
||||
/** A change of eyes wipes a slate across the pane: the cut to another
|
||||
@@ -257,8 +258,8 @@
|
||||
$effect(() => {
|
||||
if (!playing) return;
|
||||
// Each step gets the reel's beat, plus the die's interlude when
|
||||
// one decided it. Read the step here so every advance re-arms the
|
||||
// timer — a derived that stays 0 across steps would not.
|
||||
// one decided it. Reading idx here is what re-arms the timer on
|
||||
// every advance.
|
||||
const from = idx;
|
||||
const t = setTimeout(() => {
|
||||
if (idx !== from) return;
|
||||
@@ -287,7 +288,7 @@
|
||||
let raf = 0;
|
||||
const tick = (now: number) => {
|
||||
const w = Math.min(1, Math.max(0, (now - t0) / dur));
|
||||
const ease = w * w * (3 - 2 * w);
|
||||
const ease = smoothstep(w);
|
||||
const next: Record<string, { x: number; y: number }> = {};
|
||||
for (const m of moves) {
|
||||
next[m.id] = { x: m.fx + (m.tx - m.fx) * ease, y: m.fy + (m.ty - m.fy) * ease };
|
||||
@@ -308,6 +309,11 @@
|
||||
/** How far the eyes drop to watch their own feet: the horizon rises
|
||||
* to the pane's top third and the ground underfoot fills the rest. */
|
||||
const LOOK_DOWN = 0.36;
|
||||
/** Deeds on the ground of your own square, watched by looking down.
|
||||
* A punch traded with a square-mate is not one: they stand in front
|
||||
* of the eyes wherever those point. */
|
||||
const GROUNDWORK = new Set(["spellCast", "squareFilled", "creatureCreated",
|
||||
"treasurePickedUp", "treasureDropped", "objectDropped"]);
|
||||
let camReady = false;
|
||||
$effect(() => {
|
||||
if (!fp) { camReady = false; return; }
|
||||
@@ -364,18 +370,11 @@
|
||||
};
|
||||
let targetFacing = camF;
|
||||
let aimed = false;
|
||||
// Something at your own feet — a conjuration on your square, a
|
||||
// treasure taken up, the pit you just fell down — is watched by
|
||||
// looking down; the eyes come back level with the next step.
|
||||
// The pit you fell down, or climbed from, is at your feet too; the
|
||||
// eyes come back level with the next step.
|
||||
const underfoot = step.events.some((e) =>
|
||||
(e.type === "fellInPit" || e.type === "climbedFromPit") && "player" in e && (e as { player: string }).player === povId);
|
||||
(e.type === "fellInPit" || e.type === "climbedFromPit") && e.player === povId);
|
||||
let targetPitch = 0;
|
||||
// Only GROUND deeds earn the look-down: a conjuration on this
|
||||
// square, a treasure taken up or set down. A punch traded with
|
||||
// someone sharing the square aims at nobody's feet — they stand
|
||||
// right in front of the eyes, wherever those point.
|
||||
const GROUNDWORK = new Set(["spellCast", "squareFilled", "creatureCreated",
|
||||
"treasurePickedUp", "treasureDropped", "objectDropped"]);
|
||||
const groundwork = aim?.self && step.events.some((e) => GROUNDWORK.has(e.type));
|
||||
/** A stride that ends with something to watch — an ambush that
|
||||
* fired as the step landed — turns once more after the walk. */
|
||||
@@ -392,10 +391,7 @@
|
||||
targetPitch = LOOK_DOWN;
|
||||
aimed = true;
|
||||
}
|
||||
else if (aim?.self) {
|
||||
// Nothing to turn toward: keep the view, unless it is a wall.
|
||||
}
|
||||
else if (aim) {
|
||||
else if (aim && !aim.self) {
|
||||
const ax = aim.x + 0.5, ay = aim.y + 0.5;
|
||||
// The eyes watch their own magic land wherever they can see it —
|
||||
// straight down a corridor or through a warp mouth. Only a target
|
||||
@@ -523,20 +519,20 @@
|
||||
const t = Math.max(0, now - t0);
|
||||
if (tiltMs > 0) {
|
||||
const w = Math.min(1, t / tiltMs);
|
||||
cam.pitch = fromP + (targetPitch - fromP) * (w * w * (3 - 2 * w));
|
||||
cam.pitch = fromP + (targetPitch - fromP) * (smoothstep(w));
|
||||
}
|
||||
if (turnMs > 0 && t < turnMs) {
|
||||
cam.facing = fromF + arc * (t / turnMs);
|
||||
} else if (walkMs > 0 && t < turnMs + walkMs) {
|
||||
cam.facing = fromF + arc;
|
||||
const w = (t - turnMs) / walkMs;
|
||||
const ease = w * w * (3 - 2 * w);
|
||||
const ease = smoothstep(w);
|
||||
cam.x = fromX + (tx - fromX) * ease;
|
||||
cam.y = fromY + (ty - fromY) * ease;
|
||||
} else if (closingArc !== 0 && t < turnMs + walkMs + closingMs) {
|
||||
cam.x = tx; cam.y = ty;
|
||||
const w = (t - turnMs - walkMs) / closingMs;
|
||||
cam.facing = fromF + arc + closingArc * w * w * (3 - 2 * w);
|
||||
cam.facing = fromF + arc + closingArc * smoothstep(w);
|
||||
} else if (tiltMs > 0 && t < tiltMs) {
|
||||
cam.facing = fromF + arc + closingArc;
|
||||
cam.x = tx; cam.y = ty;
|
||||
@@ -606,13 +602,13 @@
|
||||
said.forEach((line, i) => g.fillText(line, textX, 720 - 32 + i * 26, 760));
|
||||
if (status) {
|
||||
g.textAlign = "right";
|
||||
if (status.steps !== null) {
|
||||
if (status.strides !== null) {
|
||||
g.fillStyle = "#efe8d4";
|
||||
g.font = "600 34px Oswald, sans-serif";
|
||||
g.fillText(String(status.steps), 1280 - 28, 720 - 52);
|
||||
g.fillText(String(status.strides), 1280 - 28, 720 - 52);
|
||||
g.fillStyle = "#a49c86";
|
||||
g.font = "600 14px Oswald, sans-serif";
|
||||
g.fillText(status.steps === 1 ? "STEP LEFT" : "STEPS LEFT", 1280 - 28, 720 - 34);
|
||||
g.fillText(status.strides === 1 ? "STEP LEFT" : "STEPS LEFT", 1280 - 28, 720 - 34);
|
||||
}
|
||||
g.fillStyle = "#d8d2c0";
|
||||
g.font = "16px 'Courier Prime', monospace";
|
||||
@@ -720,8 +716,8 @@
|
||||
</div>
|
||||
{#if status}
|
||||
<div class="caption-status">
|
||||
{#if status.steps !== null}
|
||||
<div class="caption-steps"><b>{status.steps}</b><span>{status.steps === 1 ? "step" : "steps"} left</span></div>
|
||||
{#if status.strides !== null}
|
||||
<div class="caption-steps"><b>{status.strides}</b><span>{status.strides === 1 ? "step" : "steps"} left</span></div>
|
||||
{/if}
|
||||
<div class="caption-chips">
|
||||
{#each status.chips as chip, i (i)}<span class="chip">{chip}</span>{/each}
|
||||
|
||||
Reference in New Issue
Block a user