A summon at your own feet earns a shot of yourself

Two A9WK summons — fire imp, shadow — landed on their casters' OWN
squares: no turn can face your own feet, and a creature in your own
cell is closer than the near plane, so both materialized invisibly.
The aim now reads creatureCreated (the summon's true landing, whatever
the cast's target says), and a deed on the wizard's own square takes
the CUTAWAY: the camera steps outside, stands down the deepest
corridor, and — for cutaway frames only — the reel's own wizard
renders too, so you watch the imp grow at your own feet with yourself
in the shot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-23 20:41:39 -04:00
co-authored by Claude Fable 5
parent ccb5346a31
commit 698e975b6d
+32 -15
View File
@@ -122,6 +122,8 @@
* turn to face the recipient. The fx and the view reveal together. */
let camPlan = $state<{ idx: number; holdMs: number } | null>(null);
let heldView = $state<GameView | null>(null);
/** A cutaway frame shows the reel's own wizard too. */
let cutawayShot = $state(false);
/** In first person the same events become projectiles, impacts,
* flashes, and shakes — scheduled on this step's beat, after the
@@ -252,7 +254,9 @@
const aim = (() => {
for (const e of step.events) {
let cell: { x: number; y: number } | null = null;
if (e.type === "spellCast" && e.caster === povId) {
if (e.type === "creatureCreated" && "controller" in e && e.controller === povId) {
cell = (e as { at: { x: number; y: number } }).at;
} else 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;
@@ -275,25 +279,22 @@
continue;
}
if (cell && (cell.x !== me.position.x || cell.y !== me.position.y)) return cell;
// The deed lands on the wizard's OWN square — a summon at their
// feet, invisible from inside their own eyes. Mark it for the
// cutaway: step outside yourself and watch.
if (cell) return { ...cell, self: true };
}
return null;
})();
})() as ({ x: number; y: number; self?: boolean } | null);
// A leap the legs cannot explain (first frame, teleport, the return
// from a cutaway) is a CUT, and a cut is not a stride: the direction
// of travel means nothing at the far end.
const willCut = !camReady || (dist > 1.6 && !hurled);
let targetFacing = camF;
let aimed = false;
if (camReady && !willCut && dist > 0.05 && !hurled) { targetFacing = Math.atan2(dy, dx); aimed = true; }
else if (aim) {
const ax = aim.x + 0.5, ay = aim.y + 0.5;
const toAim = Math.hypot(ax - tx, ay - ty);
const sight = castRay(v, tx, ty, Math.atan2(ay - ty, ax - tx));
if (sight.warped || sight.dist < toAim - 0.4) {
// The deed lands beyond these eyes — a summon across the maze, a
// spell through walls. Cut away to the spot like a broadcast
// camera: stand back down its deepest corridor, facing it, and
// hold a beat of the world-before so the deed happens ON screen.
cutawayShot = false;
/** The broadcast cutaway: stand back down the target cell's deepest
* corridor, facing it, holding a beat of the world-before so the
* deed happens ON screen — with the reel's own wizard visible. */
const takeCutaway = (ax: number, ay: number) => {
let best = { d: -1, a: 0 };
for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) {
const h = castRay(v, ax, ay, a);
@@ -304,6 +305,7 @@
cam.y = ay + Math.sin(best.a) * back;
cam.facing = best.a + Math.PI;
camReady = true;
cutawayShot = true;
const i2 = Math.min(idx, steps.length - 1);
const pv = i2 > 0 ? steps[i2 - 1]!.view : null;
heldView = pv;
@@ -313,6 +315,21 @@
return () => clearTimeout(t);
}
return;
};
let targetFacing = camF;
let aimed = false;
if (camReady && !willCut && dist > 0.05 && !hurled) { targetFacing = Math.atan2(dy, dx); aimed = true; }
else if (aim?.self) {
// Summoned at your own feet: no turn can show it — step outside
// yourself and watch it grow beside you.
return takeCutaway(me.position.x + 0.5, me.position.y + 0.5);
}
else if (aim) {
const ax = aim.x + 0.5, ay = aim.y + 0.5;
const toAim = Math.hypot(ax - tx, ay - ty);
const sight = castRay(v, tx, ty, Math.atan2(ay - ty, ax - tx));
if (sight.warped || sight.dist < toAim - 0.4) {
return takeCutaway(ax, ay);
}
targetFacing = Math.atan2(ay - ty, ax - tx);
aimed = true;
@@ -551,7 +568,7 @@
</header>
<div class="replay-board" bind:this={stageEl}>
{#if fp}
<FirstPerson view={heldView ?? step.view} {povId}
<FirstPerson view={heldView ?? step.view} povId={cutawayShot ? "" : povId}
x={cam.x} y={cam.y} facing={cam.facing} width={640} height={360}
fx={fpFx} posOverride={actorPos} rubble={rubbleSpots} />
{:else}