The eyes turn to meet magic thrown at them, even after a stride

Springing the Trap never showed its fireball: Rival stepped into the
watched corridor facing the way he walked, and the ambush's fireball
flew in from the mouth behind him while only the caption told of it.
The director now aims at incoming magic and punches — the caster's or
attacker's square, straight or through a mouth — and a stride that
ends with something to watch turns once more after the walk, the
slate holding the outcome until the eyes face it. The fireball comes
through the wormhole at the camera, then the blow lands.

The trap's first beat re-blessed for it.

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-02 21:28:52 -04:00
co-authored by Claude Fable 5.1
parent 86856781df
commit 86070b42a3
3 changed files with 24 additions and 4 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 321 KiB

+18 -4
View File
@@ -377,7 +377,17 @@
const GROUNDWORK = new Set(["spellCast", "squareFilled", "creatureCreated", const GROUNDWORK = new Set(["spellCast", "squareFilled", "creatureCreated",
"treasurePickedUp", "treasureDropped", "objectDropped"]); "treasurePickedUp", "treasureDropped", "objectDropped"]);
const groundwork = aim?.self && step.events.some((e) => GROUNDWORK.has(e.type)); const groundwork = aim?.self && step.events.some((e) => GROUNDWORK.has(e.type));
if (camReady && !willCut && dist > 0.05 && !hurled) { targetFacing = Math.atan2(dy, dx); aimed = true; } /** A stride that ends with something to watch — an ambush that
* fired as the step landed — turns once more after the walk. */
let afterArc = 0;
if (camReady && !willCut && dist > 0.05 && !hurled) {
targetFacing = Math.atan2(dy, dx);
aimed = true;
if (aim && !aim.self) {
const h = sightline(v, tx, ty, aim.x + 0.5, aim.y + 0.5);
if (h !== null) afterArc = shortestArc(targetFacing, h);
}
}
else if (groundwork || underfoot) { else if (groundwork || underfoot) {
targetPitch = LOOK_DOWN; targetPitch = LOOK_DOWN;
aimed = true; aimed = true;
@@ -448,8 +458,8 @@
// The reel's LAST step may be a stride into a corner: the walk keeps // The reel's LAST step may be a stride into a corner: the walk keeps
// its own facing, then the camera turns once more to a view worth // its own facing, then the camera turns once more to a view worth
// ending on. // ending on.
let closingArc = 0; let closingArc = afterArc;
if (atEnd && aimed && castRay(v, tx, ty, targetFacing).dist < 1.2) { if (closingArc === 0 && atEnd && aimed && castRay(v, tx, ty, targetFacing).dist < 1.2) {
const current = viewScoreAt(tx, ty, targetFacing); const current = viewScoreAt(tx, ty, targetFacing);
let bestA = targetFacing, bestScore = current; let bestA = targetFacing, bestScore = current;
for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) { for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) {
@@ -498,8 +508,12 @@
const turnMs = (hurled ? 0 : Math.min(260, Math.abs(arc) * 180)) / speed; const turnMs = (hurled ? 0 : Math.min(260, Math.abs(arc) * 180)) / speed;
const walkMs = (dist > 0.05 ? (hurled ? 260 : 420) : 0) / speed; const walkMs = (dist > 0.05 ? (hurled ? 260 : 420) : 0) / speed;
const tiltMs = (Math.abs(targetPitch - fromP) > 0.01 ? 340 : 0) / speed; const tiltMs = (Math.abs(targetPitch - fromP) > 0.01 ? 340 : 0) / speed;
const clearSlate = setSlate(aimed && (aim || underfoot) ? Math.max(turnMs, tiltMs) + 120 / speed : 0);
const closingMs = Math.min(300, Math.abs(closingArc) * 200) / speed; const closingMs = Math.min(300, Math.abs(closingArc) * 200) / speed;
// The outcome waits until the eyes face it: after the turn (and,
// for a stride that turns at its end, after the walk and that turn).
const clearSlate = setSlate(aimed && (aim || underfoot)
? Math.max(turnMs, tiltMs) + (afterArc !== 0 ? walkMs + closingMs : 0) + 120 / speed
: 0);
const t0 = performance.now() + dieMs; const t0 = performance.now() + dieMs;
let raf = 0; let raf = 0;
const tick = (now: number) => { const tick = (now: number) => {
+6
View File
@@ -41,6 +41,12 @@ export function aimOfEvents(
cell = e.targetCell ?? view.players.find((p) => p.id === e.target)?.position ?? null; cell = e.targetCell ?? view.players.find((p) => p.id === e.target)?.position ?? null;
} else if (e.type === "attackResolved" && e.attacker === povId) { } else if (e.type === "attackResolved" && e.attacker === povId) {
cell = view.players.find((p) => p.id === e.defender)?.position ?? null; cell = view.players.find((p) => p.id === e.defender)?.position ?? null;
} else if (e.type === "spellCast" && e.target === povId && e.caster !== povId) {
// Magic thrown AT these eyes: they turn to where it came from —
// the ambusher down the corridor, or through the mouth.
cell = view.players.find((p) => p.id === e.caster)?.position ?? null;
} else if (e.type === "punched" && e.target === povId) {
cell = view.players.find((p) => p.id === e.attacker)?.position ?? null;
} else if (e.type === "objectThrown" && e.attacker === povId) { } else if (e.type === "objectThrown" && e.attacker === povId) {
cell = e.landedAt; cell = e.landedAt;
} else if ( } else if (