import { useEffect, useMemo, useState } from 'react'; import { Mapper, Overview } from '@/lib/types'; import { defaultWorldToPx, parseOverviewJson, parseValveKvOverview } from '@/lib/helpers'; export function useOverview(activeMapKey: string | null, playersForAutoFit: {x:number;y:number}[]) { const [overview, setOverview] = useState(null); const [imgSize, setImgSize] = useState<{ w: number; h: number } | null>(null); const [srcIdx, setSrcIdx] = useState(0); const overviewCandidates = (mapKey: string) => { const base = mapKey; return [ `/assets/resource/overviews/${base}.json`, `/assets/resource/overviews/${base}_lower.json`, `/assets/resource/overviews/${base}_v1.json`, `/assets/resource/overviews/${base}_v2.json`, `/assets/resource/overviews/${base}_s2.json`, ]; }; useEffect(() => { setSrcIdx(0); }, [activeMapKey]); useEffect(() => { let cancel = false; (async () => { if (!activeMapKey) { setOverview(null); return; } for (const path of overviewCandidates(activeMapKey)) { try { const res = await fetch(path, { cache: 'no-store' }); if (!res.ok) continue; const txt = await res.text(); let ov: Overview | null = null; try { ov = parseOverviewJson(JSON.parse(txt)); } catch { ov = parseValveKvOverview(txt); } if (ov && !cancel) { setOverview(ov); return; } } catch {} } if (!cancel) setOverview(null); })(); return () => { cancel = true; }; }, [activeMapKey]); const { folderKey, imageCandidates } = useMemo(() => { if (!activeMapKey) return { folderKey: null as string | null, imageCandidates: [] as string[] }; const short = activeMapKey.startsWith('de_') ? activeMapKey.slice(3) : activeMapKey; const base = `/assets/img/radar/${activeMapKey}`; return { folderKey: short, imageCandidates: [ `${base}/de_${short}_radar_psd.png`, `${base}/de_${short}_lower_radar_psd.png`, `${base}/de_${short}_v1_radar_psd.png`, `${base}/de_${short}_radar.png`, ], }; }, [activeMapKey]); const currentSrc = imageCandidates[srcIdx]; const worldToPx: Mapper = useMemo(() => { if (!imgSize || !overview) return defaultWorldToPx(imgSize); const { posX, posY, scale, rotate = 0 } = overview; const w = imgSize.w, h = imgSize.h; const cx = w/2, cy = h/2; const bases: ((xw: number, yw: number) => { x: number; y: number })[] = [ (xw, yw) => ({ x: (xw - posX) / scale, y: (posY - yw) / scale }), (xw, yw) => ({ x: (posX - xw) / scale, y: (posY - yw) / scale }), (xw, yw) => ({ x: (xw - posX) / scale, y: (yw - posY) / scale }), (xw, yw) => ({ x: (posX - xw) / scale, y: (yw - posY) / scale }), ]; const rotSigns = [1, -1]; const candidates: Mapper[] = []; for (const base of bases) { for (const s of rotSigns) { const theta = (rotate * s * Math.PI) / 180; candidates.push((xw, yw) => { const p = base(xw, yw); if (rotate === 0) return p; const dx = p.x - cx, dy = p.y - cy; const xr = dx * Math.cos(theta) - dy * Math.sin(theta); const yr = dx * Math.sin(theta) + dy * Math.cos(theta); return { x: cx + xr, y: cy + yr }; }); } } if (!playersForAutoFit?.length) return candidates[0]; const score = (mapFn: Mapper) => { let inside = 0; for (const p of playersForAutoFit) { const { x, y } = mapFn(p.x, p.y); if (Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0 && x <= w && y <= h) inside++; } return inside; }; let best = candidates[0], bestScore = -1; for (const m of candidates) { const s = score(m); if (s > bestScore) { bestScore = s; best = m; } } return best; }, [imgSize, overview, playersForAutoFit]); const unitsToPx = useMemo(() => { if (!imgSize) return (u: number) => u; if (overview) { const scale = overview.scale; return (u: number) => u / scale; } const R = 4096; const span = Math.min(imgSize.w, imgSize.h); const k = span / (2 * R); return (u: number) => u * k; }, [imgSize, overview]); return { overview, imgSize, setImgSize, currentSrc, srcIdx, setSrcIdx, worldToPx, unitsToPx, }; }