2025-09-23 15:27:42 +02:00

122 lines
4.3 KiB
TypeScript

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<Overview | null>(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,
};
}