The reel camera looks where its wizard aims, never at a wall by default

A9WK's clip showed the flaw: a turn owner who casts without moving left
the camera staring into the wall its default heading happened to face,
for the whole turn. The facing ladder grows two rungs: the eyes turn
toward their OWN magic — a cast's target or cell, an attack's defender,
a thrown object's landing — and when a reel opens on a stationary
wizard with nothing to aim at, the camera faces the longest corridor
from where they stand instead of hardcoded east.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-23 16:51:08 -04:00
co-authored by Claude Fable 5
parent 4c358d157e
commit c7e245a32a
+35 -3
View File
@@ -202,16 +202,48 @@
(e.type === "knockedBack" || e.type === "shoved" || e.type === "washedBack" ||
e.type === "retreatedInHorror") && e.player === povId);
const actor = v.players.find((p) => p.id === step.actor);
// Where the eyes' own magic went this step: a wizard looks where
// they aim, so the camera turns to watch its own spells land.
const aim = (() => {
for (const e of step.events) {
let cell: { x: number; y: number } | null = null;
if (e.type === "spellCast" && e.caster === povId) {
cell = e.targetCell ?? v.players.find((p) => p.id === e.target)?.position ?? null;
} else if (e.type === "attackResolved" && e.attacker === povId) {
cell = v.players.find((p) => p.id === e.defender)?.position ?? null;
} else if (e.type === "objectThrown" && e.attacker === povId) {
cell = e.landedAt;
}
if (cell && (cell.x !== me.position.x || cell.y !== me.position.y)) return cell;
}
return null;
})();
let targetFacing = camF;
if (dist > 0.05 && !hurled) targetFacing = Math.atan2(dy, dx);
else if (actor && actor.id !== povId &&
let aimed = false;
if (dist > 0.05 && !hurled) { targetFacing = Math.atan2(dy, dx); aimed = true; }
else if (aim) {
targetFacing = Math.atan2(aim.y + 0.5 - ty, aim.x + 0.5 - tx);
aimed = true;
} else if (actor && actor.id !== povId &&
(actor.position.x !== me.position.x || actor.position.y !== me.position.y)) {
// Turn toward the actor — but only if these eyes could actually see
// them: an unbroken straight sight line, no warps bending it.
const ax = actor.position.x + 0.5, ay = actor.position.y + 0.5;
const toActor = Math.hypot(ax - tx, ay - ty);
const ray = castRay(v, tx, ty, Math.atan2(ay - ty, ax - tx));
if (!ray.warped && ray.dist > toActor - 0.2) targetFacing = Math.atan2(ay - ty, ax - tx);
if (!ray.warped && ray.dist > toActor - 0.2) {
targetFacing = Math.atan2(ay - ty, ax - tx);
aimed = true;
}
}
if (!camReady && !aimed) {
// The reel opens on a wizard standing still: face the longest
// corridor rather than whatever wall the default heading found.
let deepest = -1;
for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) {
const h = castRay(v, tx, ty, a);
if (h.dist > deepest) { deepest = h.dist; targetFacing = a; }
}
}
if (!camReady || (dist > 1.6 && !hurled)) {
// First frame, or a leap the legs cannot explain: cut.