export type AimTrainerCrosshairStyle = "classic" | "dot" | "cross" | "circle" | "chevron"; export type AimTrainerCrosshairPrefs = { style: AimTrainerCrosshairStyle; color: string; scale: number; }; export const AIM_TRAINER_CROSSHAIR_STORAGE_KEY = "ptf.aimTrainer.crosshair"; export const AIM_TRAINER_CROSSHAIR_SCALE_MIN = 1; export const AIM_TRAINER_CROSSHAIR_SCALE_MAX = 2.5; export const AIM_TRAINER_CROSSHAIR_SCALE_STEP = 0.1; export const AIM_TRAINER_CROSSHAIR_STYLES = [ "classic", "dot", "cross", "circle", "chevron", ] as const satisfies readonly AimTrainerCrosshairStyle[]; export const DEFAULT_AIM_TRAINER_CROSSHAIR_PREFS: AimTrainerCrosshairPrefs = { style: "classic", color: "#d8d3b6", scale: AIM_TRAINER_CROSSHAIR_SCALE_MIN, }; function createDefaultAimTrainerCrosshairPrefs(): AimTrainerCrosshairPrefs { return { ...DEFAULT_AIM_TRAINER_CROSSHAIR_PREFS }; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function isAimTrainerCrosshairStyle(value: unknown): value is AimTrainerCrosshairStyle { return ( typeof value === "string" && AIM_TRAINER_CROSSHAIR_STYLES.some((style) => style === value) ); } function isHexColor(value: unknown): value is string { return typeof value === "string" && /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(value); } export function normalizeAimTrainerCrosshairScale(value: unknown): number { if (typeof value !== "number" || !Number.isFinite(value)) { return AIM_TRAINER_CROSSHAIR_SCALE_MIN; } const clamped = Math.min( AIM_TRAINER_CROSSHAIR_SCALE_MAX, Math.max(AIM_TRAINER_CROSSHAIR_SCALE_MIN, value), ); return Number( (Math.round(clamped / AIM_TRAINER_CROSSHAIR_SCALE_STEP) * AIM_TRAINER_CROSSHAIR_SCALE_STEP).toFixed( 1, ), ); } export function loadAimTrainerCrosshairPrefs(): AimTrainerCrosshairPrefs { const defaults = createDefaultAimTrainerCrosshairPrefs(); if (typeof window === "undefined") return defaults; try { const stored = window.localStorage.getItem(AIM_TRAINER_CROSSHAIR_STORAGE_KEY); if (!stored) return defaults; const parsed: unknown = JSON.parse(stored); if (!isRecord(parsed) || !isAimTrainerCrosshairStyle(parsed.style)) return defaults; if (!isHexColor(parsed.color)) return defaults; return { style: parsed.style, color: parsed.color, scale: normalizeAimTrainerCrosshairScale(parsed.scale), }; } catch { return defaults; } } export function saveAimTrainerCrosshairPrefs(prefs: AimTrainerCrosshairPrefs): void { if (typeof window === "undefined") return; try { window.localStorage.setItem(AIM_TRAINER_CROSSHAIR_STORAGE_KEY, JSON.stringify(prefs)); } catch { return; } }