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. */
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user