import { Raycaster, Vector3, type Object3D } from "three"; import { clampCameraPitch, DEFAULT_CAMERA_PITCH } from "./inputMath"; import type { ArenaStageManifest, Vec3Tuple } from "./arenaTypes"; export const ARENA_CAMERA_DISTANCE = 5.8; export const ARENA_CAMERA_FOCUS_HEIGHT = 1.35; export interface ArenaCameraPose { readonly position: [number, number, number]; readonly target: [number, number, number]; } export function arenaSpawnYaw(stage: ArenaStageManifest, team: 0 | 1): number { const from = stage.teamSpawns[team]; const to = stage.preparationGates[team]; return Math.atan2(to[0] - from[0], to[2] - from[2]); } /** Follow the controlled character at their elevation, including underground arenas. */ export function arenaCameraPose( footPosition: Vec3Tuple, yaw: number, pitch = DEFAULT_CAMERA_PITCH, distance = ARENA_CAMERA_DISTANCE, ): ArenaCameraPose { const safeYaw = Number.isFinite(yaw) ? yaw : 0; const safePitch = clampCameraPitch(pitch); const radius = Number.isFinite(distance) ? Math.max(0.25, Math.min(12, distance)) : ARENA_CAMERA_DISTANCE; const target: [number, number, number] = [footPosition[0], footPosition[1] + ARENA_CAMERA_FOCUS_HEIGHT, footPosition[2]]; return { target, position: [ target[0] - Math.sin(safeYaw) * Math.cos(safePitch) * radius, target[1] + Math.sin(safePitch) * radius, target[2] - Math.cos(safeYaw) * Math.cos(safePitch) * radius, ], }; } /** Probe both directions so one-sided imported walls also obstruct the camera. */ export function arenaCameraClearDistance(target: Vector3, desired: Vector3, environment: Object3D, ray: Raycaster): number { const length = target.distanceTo(desired); if (length < 0.001) return 0; const direction = desired.clone().sub(target).normalize(); let distance = length; for (const offset of [[0, 0, 0], [0.18, 0, 0], [-0.18, 0, 0], [0, 0.18, 0], [0, -0.18, 0]]) { const origin = target.clone().add(new Vector3(...offset)); ray.near = 0.03; ray.far = length; ray.set(origin, direction); for (const hit of ray.intersectObject(environment, true)) distance = Math.min(distance, hit.distance - 0.2); ray.set(origin.addScaledVector(direction, length), direction.clone().negate()); for (const hit of ray.intersectObject(environment, true)) distance = Math.min(distance, length - hit.distance - 0.2); } return Math.max(0.15, distance); }