70 lines
2.6 KiB
TypeScript
70 lines
2.6 KiB
TypeScript
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<string>, ...candidates: readonly string[]): number {
|
|
return Number(candidates.some((code) => codes.has(code)));
|
|
}
|
|
|
|
export function spectatorKeyboardAxes(codes: ReadonlySet<string>): 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<string>): 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<string>,
|
|
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];
|
|
}
|