import { useEffect, useMemo, useRef, useState } from 'react'; import { BombState, DeathMarker, Grenade, PlayerState, Score, Trail, WsStatus } from '@/lib/types'; import { UI } from '@/lib/ui'; import { asNum, mapTeam, steamIdOf } from '@/lib/helpers'; import { normalizeGrenades } from '@/lib/grenades'; export function useRadarState(mySteamId: string | null) { // WS / Map const [radarWsStatus, setGameWsStatus] = useState('idle'); const [activeMapKey, setActiveMapKey] = useState(null); // Spieler const playersRef = useRef>(new Map()); const [players, setPlayers] = useState([]); const [hoveredPlayerId, setHoveredPlayerId] = useState(null); // Deaths const deathSeqRef = useRef(0); const deathSeenRef = useRef>(new Set()); const lastAlivePosRef = useRef>(new Map()); // Grenaden + Trails const grenadesRef = useRef>(new Map()); const [grenades, setGrenades] = useState([]); const trailsRef = useRef>(new Map()); const [trails, setTrails] = useState([]); // Death-Marker const deathMarkersRef = useRef([]); const [deathMarkers, setDeathMarkers] = useState([]); // Bomb const bombRef = useRef(null); const [bomb, setBomb] = useState(null); // Score + Phase const [roundPhase, setRoundPhase] = useState<'freezetime'|'live'|'bomb'|'over'|'warmup'|'unknown'>('unknown'); const roundEndsAtRef = useRef(null); const bombEndsAtRef = useRef(null); const defuseRef = useRef<{ by: string|null; hasKit: boolean; endsAt: number|null }>({ by: null, hasKit: false, endsAt: null }); const [score, setScore] = useState({ ct: 0, t: 0, round: null }); // flush-batching const flushTimer = useRef(null); const scheduleFlush = () => { if (flushTimer.current != null) return; flushTimer.current = window.setTimeout(() => { flushTimer.current = null; setPlayers(Array.from(playersRef.current.values())); setGrenades(Array.from(grenadesRef.current.values())); setTrails(Array.from(trailsRef.current.values())); setDeathMarkers([...deathMarkersRef.current]); updateBombFromPlayers(); setBomb(bombRef.current); }, 66); }; useEffect(() => () => { if (flushTimer.current != null) { window.clearTimeout(flushTimer.current); flushTimer.current = null; } }, []); const myTeam = useMemo<'T'|'CT'|string|null>(() => { if (!mySteamId) return null; return playersRef.current.get(mySteamId)?.team ?? null; }, [players, mySteamId]); const addDeathMarker = (x:number,y:number, steamId?: string) => { const now = Date.now(); if (steamId) { if (deathSeenRef.current.has(steamId)) return; deathSeenRef.current.add(steamId); } const uid = `${steamId ?? 'd'}#${now}#${deathSeqRef.current++}`; deathMarkersRef.current.push({ id: uid, sid: steamId ?? null, x, y, t: now }); }; const addDeathMarkerFor = (id: string, xNow: number, yNow: number) => { const last = lastAlivePosRef.current.get(id); const x = Number.isFinite(last?.x) ? last!.x : xNow; const y = Number.isFinite(last?.y) ? last!.y : yNow; addDeathMarker(x, y, id); }; const clearRoundArtifacts = (resetPlayers = false, hard = false) => { deathMarkersRef.current = []; deathSeenRef.current.clear(); trailsRef.current.clear(); grenadesRef.current.clear(); lastAlivePosRef.current.clear(); bombRef.current = null; if (hard) { playersRef.current.clear(); } else if (resetPlayers) { for (const [id, p] of playersRef.current) { playersRef.current.set(id, { ...p, alive: true, hasBomb: false }); } } scheduleFlush(); }; const updateBombFromPlayers = () => { if (bombRef.current?.status === 'planted') return; const carrier = Array.from(playersRef.current.values()).find(p => p.hasBomb); if (carrier) { bombRef.current = { x: carrier.x, y: carrier.y, z: carrier.z, status: 'carried', changedAt: bombRef.current?.status === 'carried' ? bombRef.current.changedAt : Date.now(), }; } }; // ---- Player Upsert (gekürzt – Logik aus deiner Datei) -------------- function upsertPlayer(e:any) { const id = steamIdOf(e); if (!id) return; const pos = e.pos ?? e.position ?? e.location ?? e.coordinates; const x = asNum(e.x ?? (Array.isArray(pos) ? pos?.[0] : pos?.x)); const y = asNum(e.y ?? (Array.isArray(pos) ? pos?.[1] : pos?.y)); const z = asNum(e.z ?? (Array.isArray(pos) ? pos?.[2] : pos?.z), 0); if (!Number.isFinite(x) || !Number.isFinite(y)) return; const hpProbe = asNum(e.hp ?? e.health ?? e.state?.health, NaN); const old = playersRef.current.get(id); const nextAlive = Number.isFinite(hpProbe) ? hpProbe > 0 : (old?.alive ?? true); if ((old?.alive !== false) && nextAlive === false) addDeathMarker(x, y, id); if (nextAlive === true) lastAlivePosRef.current.set(id, { x, y }); else if (nextAlive === false && (old?.alive !== false)) addDeathMarkerFor(id, x, y); const activeWeaponName = (typeof e.activeWeapon === 'string' && e.activeWeapon) || (e.activeWeapon?.name ?? null) || (Array.isArray(e.weapons) ? (e.weapons.find((w:any) => (w?.state ?? '').toLowerCase() === 'active')?.name ?? null) : null); playersRef.current.set(id, { id, name: e.name ?? old?.name ?? null, team: mapTeam(e.team ?? old?.team), x, y, z, yaw: Number.isFinite(Number(e.yaw)) ? Number(e.yaw) : (old?.yaw ?? null), alive: nextAlive, hasBomb: Boolean(e.hasBomb) || Boolean(old?.hasBomb), hp: Number.isFinite(hpProbe) ? hpProbe : (old?.hp ?? null), armor: Number.isFinite(asNum(e.armor ?? e.state?.armor, NaN)) ? asNum(e.armor ?? e.state?.armor, NaN) : (old?.armor ?? null), helmet: (e.helmet ?? e.hasHelmet ?? e.state?.helmet) ?? (old?.helmet ?? null), defuse: (e.defuse ?? e.hasDefuse ?? e.hasDefuser ?? e.state?.defusekit) ?? (old?.defuse ?? null), activeWeapon: activeWeaponName ?? old?.activeWeapon ?? null, weapons: Array.isArray(e.weapons) ? e.weapons : (old?.weapons ?? null), nades: old?.nades ?? null, }); } // ---- Handlers für GameSocket --------------------------------------- const handlePlayersAll = (msg:any) => { const pcd = msg?.phase ?? msg?.phase_countdowns; const phase = String(pcd?.phase ?? '').toLowerCase(); if (phase === 'freezetime' && (deathMarkersRef.current.length || trailsRef.current.size)) { clearRoundArtifacts(true); } if (pcd?.phase_ends_in != null) { const sec = Number(pcd.phase_ends_in); if (Number.isFinite(sec)) { roundEndsAtRef.current = Date.now() + sec * 1000; setRoundPhase(String(pcd.phase ?? 'unknown').toLowerCase() as any); } } else if (pcd?.phase) { setRoundPhase(String(pcd.phase).toLowerCase() as any); } if ((pcd?.phase ?? '').toLowerCase() === 'over') { roundEndsAtRef.current = null; bombEndsAtRef.current = null; defuseRef.current = { by: null, hasKit: false, endsAt: null }; } // Spieler (gekürzt, robust genug) const apObj = msg?.allplayers; const apArr = Array.isArray(msg?.players) ? msg.players : null; const upsertFromPayload = (p:any) => { const id = steamIdOf(p); if (!id) return; const pos = p.position ?? p.pos ?? p.location ?? p.coordinates ?? p.eye ?? p.pos; const xyz = Array.isArray(pos) ? { x: pos[0], y: pos[1], z: pos[2] } : typeof pos === 'object' ? pos : { x: p.x, y: p.y, z: p.z }; const { x=0, y=0, z=0 } = xyz; const hpNum = Number(p?.state?.health ?? p?.hp); const isAlive = Number.isFinite(hpNum) ? hpNum > 0 : (playersRef.current.get(id)?.alive ?? true); if ((playersRef.current.get(id)?.alive ?? true) && !isAlive) addDeathMarker(x, y, id); playersRef.current.set(id, { id, name: p?.name ?? playersRef.current.get(id)?.name ?? null, team: mapTeam(p?.team ?? playersRef.current.get(id)?.team), x, y, z, yaw: playersRef.current.get(id)?.yaw ?? null, alive: isAlive, hasBomb: Boolean(playersRef.current.get(id)?.hasBomb), hp: Number.isFinite(hpNum) ? hpNum : (playersRef.current.get(id)?.hp ?? null), armor: playersRef.current.get(id)?.armor ?? null, helmet: playersRef.current.get(id)?.helmet ?? null, defuse: playersRef.current.get(id)?.defuse ?? null, activeWeapon: playersRef.current.get(id)?.activeWeapon ?? null, weapons: playersRef.current.get(id)?.weapons ?? null, nades: playersRef.current.get(id)?.nades ?? null, }); }; if (apObj && typeof apObj === 'object') for (const k of Object.keys(apObj)) upsertFromPayload(apObj[k]); else if (apArr) for (const p of apArr) upsertFromPayload(p); // Scores (robust, gekürzt) const pick = (v:any)=> Number.isFinite(Number(v)) ? Number(v) : null; const ct = pick(msg?.score?.ct) ?? pick(msg?.scores?.ct) ?? pick(msg?.map?.team_ct?.score) ?? 0; const t = pick(msg?.score?.t) ?? pick(msg?.scores?.t) ?? pick(msg?.map?.team_t?.score) ?? 0; const rnd= pick(msg?.round) ?? pick(msg?.rounds?.played) ?? pick(msg?.map?.round) ?? null; setScore({ ct, t, round: rnd }); scheduleFlush(); }; const handleGrenades = (g:any) => { const list = normalizeGrenades(g); const now = Date.now(); // Trails nur für eigene Projektile const mine = mySteamId ? list.filter(n => n.ownerId === mySteamId && n.phase === 'projectile') : []; const seenTrailIds = new Set(); for (const it of mine) { seenTrailIds.add(it.id); const prev = trailsRef.current.get(it.id) ?? { id: it.id, kind: it.kind, pts: [], lastSeen: 0 }; const last = prev.pts[prev.pts.length - 1]; if (!last || Math.hypot(it.x - last.x, it.y - last.y) > 1) { prev.pts.push({ x: it.x, y: it.y }); if (prev.pts.length > UI.trail.maxPoints) prev.pts = prev.pts.slice(-UI.trail.maxPoints); } prev.kind = it.kind; prev.lastSeen = now; trailsRef.current.set(it.id, prev); } for (const [id, tr] of trailsRef.current) { if (!seenTrailIds.has(id) && now - tr.lastSeen > UI.trail.fadeMs) trailsRef.current.delete(id); } // sanftes Mergen const next = new Map(grenadesRef.current); const seenIds = new Set(); for (const it of list) { seenIds.add(it.id); next.set(it.id, { ...(next.get(it.id) || {}), ...it }); } for (const [id, nade] of next) { if (!seenIds.has(id) && nade.phase === 'projectile') next.delete(id); if ((nade.phase === 'effect' || nade.phase === 'exploded') && nade.expiresAt != null && nade.expiresAt <= now) next.delete(id); } grenadesRef.current = next; scheduleFlush(); }; const handleBomb = (normalizeBomb:(b:any)=>BombState|null) => (b:any) => { const prev = bombRef.current; const nb = normalizeBomb(b); if (!nb) return; const withPos = { x: Number.isFinite(nb.x) ? nb.x : (prev?.x ?? 0), y: Number.isFinite(nb.y) ? nb.y : (prev?.y ?? 0), z: Number.isFinite(nb.z) ? nb.z : (prev?.z ?? 0), }; const sameStatus = prev && prev.status === nb.status; bombRef.current = { ...withPos, status: nb.status, changedAt: sameStatus ? prev!.changedAt : Date.now() }; scheduleFlush(); }; return { // state radarWsStatus, setGameWsStatus, activeMapKey, setActiveMapKey, players, playersRef, hoveredPlayerId, setHoveredPlayerId, grenades, trails, deathMarkers, bomb, roundPhase, roundEndsAtRef, bombEndsAtRef, defuseRef, score, myTeam, // ops upsertPlayer, handlePlayersAll, handleGrenades, handleBomb, clearRoundArtifacts, scheduleFlush, addDeathMarker, }; }