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
+18 -4
View File
@@ -377,7 +377,17 @@
const GROUNDWORK = new Set(["spellCast", "squareFilled", "creatureCreated",
"treasurePickedUp", "treasureDropped", "objectDropped"]);
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) {
targetPitch = LOOK_DOWN;
aimed = true;
@@ -448,8 +458,8 @@
// 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
// ending on.
let closingArc = 0;
if (atEnd && aimed && castRay(v, tx, ty, targetFacing).dist < 1.2) {
let closingArc = afterArc;
if (closingArc === 0 && atEnd && aimed && castRay(v, tx, ty, targetFacing).dist < 1.2) {
const current = viewScoreAt(tx, ty, targetFacing);
let bestA = targetFacing, bestScore = current;
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 walkMs = (dist > 0.05 ? (hurled ? 260 : 420) : 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;
// 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;
let raf = 0;
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;
} else if (e.type === "attackResolved" && e.attacker === povId) {
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) {
cell = e.landedAt;
} else if (