import type { Vector3Tuple } from "./dungeonTypes"; export interface SpectatorCameraAxes { readonly forward: number; readonly right: number; readonly vertical: number; } const MAX_CAMERA_FRAME_SECONDS = 0.1; function held(codes: ReadonlySet, ...candidates: readonly string[]): number { return Number(candidates.some((code) => codes.has(code))); } export function spectatorKeyboardAxes(codes: ReadonlySet): SpectatorCameraAxes { return { forward: held(codes, "KeyW") - held(codes, "KeyS"), right: held(codes, "KeyD") - held(codes, "KeyA"), vertical: held(codes, "KeyE", "Space") - held(codes, "KeyQ", "ControlLeft", "ControlRight"), }; } export function spectatorCameraBoosted(codes: ReadonlySet): boolean { return codes.has("ShiftLeft") || codes.has("ShiftRight"); } /** Returns a bounded, normalized free-flight translation for one render frame. */ export function spectatorCameraTranslation( yaw: number, pitch: number, axes: SpectatorCameraAxes, deltaSeconds: number, boosted: boolean, ): Vector3Tuple { if (![yaw, pitch, deltaSeconds, axes.forward, axes.right, axes.vertical].every(Number.isFinite)) return [0, 0, 0]; const forwardX = Math.sin(yaw) * Math.cos(pitch); const forwardY = Math.sin(pitch); const forwardZ = Math.cos(yaw) * Math.cos(pitch); const rightX = Math.cos(yaw); const rightZ = -Math.sin(yaw); let x = forwardX * axes.forward + rightX * axes.right; let y = forwardY * axes.forward + axes.vertical; let z = forwardZ * axes.forward + rightZ * axes.right; const length = Math.hypot(x, y, z); if (length <= 1e-6) return [0, 0, 0]; const scale = (boosted ? 28 : 10) * Math.max(0, Math.min(MAX_CAMERA_FRAME_SECONDS, deltaSeconds)) / Math.max(1, length); x *= scale; y *= scale; z *= scale; return [x, y, z]; }