diff --git a/packages/web/src/LiveFirstPerson.svelte b/packages/web/src/LiveFirstPerson.svelte index c3c5a8f..2472c9f 100644 --- a/packages/web/src/LiveFirstPerson.svelte +++ b/packages/web/src/LiveFirstPerson.svelte @@ -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; diff --git a/packages/web/src/Replay.svelte b/packages/web/src/Replay.svelte index a3307d8..581d482 100644 --- a/packages/web/src/Replay.svelte +++ b/packages/web/src/Replay.svelte @@ -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; diff --git a/packages/web/src/fpv/director.ts b/packages/web/src/fpv/director.ts index 03b9202..33777fd 100644 --- a/packages/web/src/fpv/director.ts +++ b/packages/web/src/fpv/director.ts @@ -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 }; +}