diff --git a/packages/web/src/Replay.svelte b/packages/web/src/Replay.svelte index b2d7787..e973830 100644 --- a/packages/web/src/Replay.svelte +++ b/packages/web/src/Replay.svelte @@ -294,14 +294,39 @@ aimed = true; } } - if (!aimed && castRay(v, tx, ty, targetFacing).dist < 0.8) { - // Nothing aims the eyes and they'd idle nose-to-brick (the opening - // cut, or a beat after pacing into a dead end): face the deepest - // corridor from where the wizard stands instead. - let deepest = -1; - for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) { - const h = castRay(v, tx, ty, a); - if (h.dist > deepest) { deepest = h.dist; targetFacing = a; } + if (!aimed) { + // Nothing aims the eyes. If they'd idle nose-to-brick (the opening + // cut, a dead-end beat — or the reel's CLOSING shot, which gets a + // higher standard), find a better view: the deepest corridor, and + // on the final frame, preferably one with something in it. + const viewScore = (a: number): number => { + let score = Math.min(castRay(v, tx, ty, a).dist, 8); + if (!atEnd) return score; + const bonus = (cell: { x: number; y: number }, worth: number) => { + const bx = cell.x + 0.5, by = cell.y + 0.5; + const bearing = Math.atan2(by - ty, bx - tx); + if (Math.abs(shortestArc(a, bearing)) > 0.55) return 0; + const d = Math.hypot(bx - tx, by - ty); + if (d < 0.1) return 0; + const sight = castRay(v, tx, ty, bearing); + return !sight.warped && sight.dist >= d - 0.4 ? worth / (1 + d * 0.3) : 0; + }; + for (const p of v.players) if (p.alive && p.id !== povId) score += bonus(p.position, 6); + for (const c of v.creatures) score += bonus(c.position, 5); + for (const t of v.treasures) if (t.position && !t.carriedBy) score += bonus(t.position, 3); + return score; + }; + const staring = castRay(v, tx, ty, targetFacing).dist < (atEnd ? 1.2 : 0.8); + if (staring) { + const current = viewScore(targetFacing); + let bestA = targetFacing, bestScore = current; + for (const a of [0, Math.PI / 2, Math.PI, -Math.PI / 2]) { + const sc = viewScore(a); + if (sc > bestScore + 0.5) { bestScore = sc; bestA = a; } + } + // One evaluation, one turn — a wizard walled in on all sides + // keeps whatever view it has rather than hunting forever. + targetFacing = bestA; } } if (!camReady || (dist > 1.6 && !hurled)) {