The cutaway camera may not walk through a warp mouth

A ray threads a warp and keeps counting; a body cannot. Both cutaway
cameras (reel and live pane) backed down deepestFacing's corridor,
which meant a mouth-adjacent shot could measure its backing room
THROUGH the warp and park the eye off the board — a second of smeared
nothing whenever someone stepped a rim warp. cutawayStand now measures
standable ground, clamping warped rays at the mouth, so the shot stands
on real floor watching the veil instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-09-01 12:41:17 -04:00
co-authored by Claude Fable 5
parent c8f719f467
commit 7c201554f5
3 changed files with 17 additions and 6 deletions
+2 -3
View File
@@ -12,7 +12,7 @@
import { objectArt, tokenArt } from "./art";
import { fpFxForEvents, type FpFx } from "./fpv/fx3d";
import { castRay, SIDE_ANGLE, OPPOSITE } from "./fpv/raycast";
import { deepestFacing } from "./fpv/director";
import { cutawayStand, deepestFacing } from "./fpv/director";
import { aimOfEvents, gatherGlides, hurledIn, shortestArc } from "./fpv/director";
import { onDestroy, untrack } from "svelte";
import type { GameEvent, GameView, Side } from "@wizwar/engine";
@@ -331,8 +331,7 @@
const willCut = !camReady || (dist > 1.6 && !hurled);
cutawayShot = false;
const takeCutaway = (ax: number, ay: number) => {
const a = deepestFacing(v, ax, ay);
const d = castRay(v, ax, ay, a).dist;
const { facing: a, depth: d } = cutawayStand(v, ax, ay);
const back = Math.min(1.6, Math.max(0.35, d - 0.4));
cam.x = ax + Math.cos(a) * back;
cam.y = ay + Math.sin(a) * back;
+2 -3
View File
@@ -7,7 +7,7 @@
import { scheduleFx, type BoardFx } from "./fx";
import { fpFxForEvents, type FpFx } from "./fpv/fx3d";
import { castRay, edgeMid } from "./fpv/raycast";
import { deepestFacing, aimOfEvents, gatherGlides, hurledIn, shortestArc } from "./fpv/director";
import { cutawayStand, deepestFacing, aimOfEvents, gatherGlides, hurledIn, shortestArc } from "./fpv/director";
import { prefs } from "./prefs.svelte";
import { stackSightTrace } from "@wizwar/engine";
import type { GameEvent, GameView } from "@wizwar/engine";
@@ -249,8 +249,7 @@
* 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) => {
const a = deepestFacing(v, ax, ay);
const d = castRay(v, ax, ay, a).dist;
const { facing: a, depth: d } = cutawayStand(v, ax, ay);
const back = Math.min(1.6, Math.max(0.35, d - 0.4));
cam.x = ax + Math.cos(a) * back;
cam.y = ay + Math.sin(a) * back;
+13
View File
@@ -143,3 +143,16 @@ export function deepestFacing(view: GameView, x: number, y: number): number {
}
return face;
}
/** Where a cutaway camera may STAND back from a square: rays thread warp
* mouths but a body cannot, so a warped ray's ground ends at the mouth.
* Returns the richest heading and how far back real floor allows. */
export function cutawayStand(view: GameView, x: number, y: number): { facing: number; depth: number } {
let deepest = -1, facing = 0;
for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) {
const h = castRay(view, x, y, a);
const d = h.warped && h.warpDist !== undefined ? Math.min(h.dist, h.warpDist) : h.dist;
if (d > deepest) { deepest = d; facing = a; }
}
return { facing, depth: deepest };
}