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:
co-authored by
Claude Fable 5
parent
ccb5346a31
commit
698e975b6d
@@ -122,6 +122,8 @@
|
|||||||
* turn to face the recipient. The fx and the view reveal together. */
|
* turn to face the recipient. The fx and the view reveal together. */
|
||||||
let camPlan = $state<{ idx: number; holdMs: number } | null>(null);
|
let camPlan = $state<{ idx: number; holdMs: number } | null>(null);
|
||||||
let heldView = $state<GameView | 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,
|
/** In first person the same events become projectiles, impacts,
|
||||||
* flashes, and shakes — scheduled on this step's beat, after the
|
* flashes, and shakes — scheduled on this step's beat, after the
|
||||||
@@ -252,7 +254,9 @@
|
|||||||
const aim = (() => {
|
const aim = (() => {
|
||||||
for (const e of step.events) {
|
for (const e of step.events) {
|
||||||
let cell: { x: number; y: number } | null = null;
|
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;
|
cell = e.targetCell ?? v.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 = v.players.find((p) => p.id === e.defender)?.position ?? null;
|
cell = v.players.find((p) => p.id === e.defender)?.position ?? null;
|
||||||
@@ -275,44 +279,57 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (cell && (cell.x !== me.position.x || cell.y !== me.position.y)) return cell;
|
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;
|
return null;
|
||||||
})();
|
})() as ({ x: number; y: number; self?: boolean } | null);
|
||||||
// A leap the legs cannot explain (first frame, teleport, the return
|
// 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
|
// from a cutaway) is a CUT, and a cut is not a stride: the direction
|
||||||
// of travel means nothing at the far end.
|
// of travel means nothing at the far end.
|
||||||
const willCut = !camReady || (dist > 1.6 && !hurled);
|
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 targetFacing = camF;
|
||||||
let aimed = false;
|
let aimed = false;
|
||||||
if (camReady && !willCut && dist > 0.05 && !hurled) { targetFacing = Math.atan2(dy, dx); aimed = true; }
|
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) {
|
else if (aim) {
|
||||||
const ax = aim.x + 0.5, ay = aim.y + 0.5;
|
const ax = aim.x + 0.5, ay = aim.y + 0.5;
|
||||||
const toAim = Math.hypot(ax - tx, ay - ty);
|
const toAim = Math.hypot(ax - tx, ay - ty);
|
||||||
const sight = castRay(v, tx, ty, Math.atan2(ay - ty, ax - tx));
|
const sight = castRay(v, tx, ty, Math.atan2(ay - ty, ax - tx));
|
||||||
if (sight.warped || sight.dist < toAim - 0.4) {
|
if (sight.warped || sight.dist < toAim - 0.4) {
|
||||||
// The deed lands beyond these eyes — a summon across the maze, a
|
return takeCutaway(ax, ay);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
targetFacing = Math.atan2(ay - ty, ax - tx);
|
targetFacing = Math.atan2(ay - ty, ax - tx);
|
||||||
aimed = true;
|
aimed = true;
|
||||||
@@ -551,7 +568,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="replay-board" bind:this={stageEl}>
|
<div class="replay-board" bind:this={stageEl}>
|
||||||
{#if fp}
|
{#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}
|
x={cam.x} y={cam.y} facing={cam.facing} width={640} height={360}
|
||||||
fx={fpFx} posOverride={actorPos} rubble={rubbleSpots} />
|
fx={fpFx} posOverride={actorPos} rubble={rubbleSpots} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
Reference in New Issue
Block a user