diff --git a/packages/web/src/Replay.svelte b/packages/web/src/Replay.svelte index 9998a1b..abd512b 100644 --- a/packages/web/src/Replay.svelte +++ b/packages/web/src/Replay.svelte @@ -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(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,44 +279,57 @@ 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); + 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); + if (h.dist > best.d) best = { d: h.dist, a }; + } + const back = Math.min(1.6, Math.max(0.35, best.d - 0.4)); + cam.x = ax + Math.cos(best.a) * back; + 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; + camPlan = { idx: i2, holdMs: pv ? 420 / speed : 0 }; + if (pv) { + const t = setTimeout(() => (heldView = null), 420 / speed); + 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) { - // 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. - 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); - if (h.dist > best.d) best = { d: h.dist, a }; - } - const back = Math.min(1.6, Math.max(0.35, best.d - 0.4)); - cam.x = ax + Math.cos(best.a) * back; - cam.y = ay + Math.sin(best.a) * back; - cam.facing = best.a + Math.PI; - camReady = true; - const i2 = Math.min(idx, steps.length - 1); - const pv = i2 > 0 ? steps[i2 - 1]!.view : null; - heldView = pv; - camPlan = { idx: i2, holdMs: pv ? 420 / speed : 0 }; - if (pv) { - const t = setTimeout(() => (heldView = null), 420 / speed); - return () => clearTimeout(t); - } - return; + return takeCutaway(ax, ay); } targetFacing = Math.atan2(ay - ty, ax - tx); aimed = true; @@ -551,7 +568,7 @@
{#if fp} - {:else}