import type { Vector3Tuple } from "./dungeonTypes"; export interface SpectatorCameraAxes { readonly forward: number; readonly right: number; readonly vertical: number; } const MAX_CAMERA_FRAME_SECONDS = 0.1; const KEYBOARD_YAW_RADIANS_PER_SECOND = 2.25; const KEYBOARD_PITCH_RADIANS_PER_SECOND = 1.55; 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 frame-bounded arrow-key look deltas in the same orientation as mouse look. */ export function spectatorKeyboardLookDelta( codes: ReadonlySet, deltaSeconds: number, ): readonly [horizontal: number, vertical: number] { const seconds = Number.isFinite(deltaSeconds) ? Math.max(0, Math.min(MAX_CAMERA_FRAME_SECONDS, deltaSeconds)) : 0; return [ (held(codes, "ArrowRight") - held(codes, "ArrowLeft")) * seconds * KEYBOARD_YAW_RADIANS_PER_SECOND, (held(codes, "ArrowDown") - held(codes, "ArrowUp")) * seconds * KEYBOARD_PITCH_RADIANS_PER_SECOND, ]; } /** 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); // With Three's +Z-forward camera convention, screen-right is -X at zero // yaw. Keep this basis aligned with cameraRelativeMovement. 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]; }