122 lines
4.3 KiB
TypeScript
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,
|
|
};
|
|
}
|