'use client' import { useEffect, useMemo, useState } from 'react'; import { useSession } from 'next-auth/react'; import GameSocket from './GameSocket'; import TeamSidebar from './TeamSidebar'; import StaticEffects from './StaticEffects'; import RadarHeader from './RadarHeader'; import RadarCanvas from './RadarCanvas'; import { useAvatarDirectoryStore } from '@/app/lib/useAvatarDirectoryStore'; import { useTelemetryStore } from '@/app/lib/useTelemetryStore'; import { useBombBeep } from './hooks/useBombBeep'; import { useOverview } from './hooks/useOverview'; import { useRadarState } from './hooks/useRadarState'; import { Grenade } from './lib/types'; import { UI } from './lib/ui'; import { teamOfGrenade } from './lib/grenades'; import { BombState } from './lib/types'; const teamIdT = undefined as string | undefined; const teamIdCT = undefined as string | undefined; function makeWsUrl(host?: string, port?: string, path?: string, scheme?: string) { const h = (host ?? '').trim() || '127.0.0.1'; const p = (port ?? '').trim() || '8081'; const pa = (path ?? '').trim() || '/telemetry'; const sch = (scheme ?? '').toLowerCase(); const pageHttps = typeof window !== 'undefined' && window.location.protocol === 'https:'; const useWss = sch === 'wss' || (sch !== 'ws' && (p === '443' || pageHttps)); const proto = useWss ? 'wss' : 'ws'; const portPart = (p === '80' || p === '443') ? '' : `:${p}`; return `${proto}://${h}${portPart}${pa}`; } const gameUrl = makeWsUrl( process.env.NEXT_PUBLIC_CS2_GAME_WS_HOST, process.env.NEXT_PUBLIC_CS2_GAME_WS_PORT, process.env.NEXT_PUBLIC_CS2_GAME_WS_PATH, process.env.NEXT_PUBLIC_CS2_GAME_WS_SCHEME ); export default function LiveRadar() { // Session / User const { data: session, status } = useSession(); const isAuthed = status === 'authenticated'; const mySteamId: string | null = useMemo(() => { const u: any = session?.user; const cands = [u?.steamId, u?.steamid, u?.steam_id, u?.id]; const first = cands.find(Boolean); return first ? String(first) : null; }, [session?.user]); // Avatar store const ensureTeamsLoaded = useAvatarDirectoryStore(s => s.ensureTeamsLoaded); const avatarVersion = useAvatarDirectoryStore(s => s.version); const avatarById = useAvatarDirectoryStore(s => s.byId); // Radar state (alles zentrale Zeug) const { radarWsStatus, setGameWsStatus, activeMapKey, setActiveMapKey, players, playersRef, hoveredPlayerId, setHoveredPlayerId, grenades, trails, deathMarkers, bomb, roundPhase, roundEndsAtRef, bombEndsAtRef, defuseRef, score, myTeam, upsertPlayer, handlePlayersAll, handleGrenades, handleBomb, clearRoundArtifacts, scheduleFlush, } = useRadarState(mySteamId); // Avatare toggle (persist) const [useAvatars, setUseAvatars] = useState(false); useEffect(() => { try { setUseAvatars(localStorage.getItem('radar.useAvatars') === '1'); } catch {} }, []); useEffect(() => { try { localStorage.setItem('radar.useAvatars', useAvatars ? '1' : '0'); } catch {} }, [useAvatars]); // Teams preload useEffect(() => { const ids = [teamIdT, teamIdCT].filter(Boolean) as string[]; if (ids.length) ensureTeamsLoaded(ids); }, [ensureTeamsLoaded]); // Map-Key aus Telemetry übernehmen const mapKeyFromTelemetry = useTelemetryStore(s => s.mapKey); useEffect(() => { if (mapKeyFromTelemetry) setActiveMapKey(mapKeyFromTelemetry); }, [mapKeyFromTelemetry, setActiveMapKey]); // overview + mapping const { overview, imgSize, setImgSize, currentSrc, srcIdx, setSrcIdx, worldToPx, unitsToPx } = useOverview(activeMapKey, players.map(p=>({x:p.x,y:p.y}))); void overview; void srcIdx; // kept if you want to expose choice UI later // Bomb beep state const { beepState } = useBombBeep(bomb); const bombSecLeft = bombEndsAtRef.current == null ? null : Math.max(0, Math.ceil((bombEndsAtRef.current - Date.now())/1000)); const bombFinal10 = bombSecLeft != null && bombSecLeft <= 10; // helper: grenade filter by team const teamOfPlayer = (sid?: string | null): 'T' | 'CT' | string | null => { if (!sid) return null; return playersRef.current.get(sid)?.team ?? null; }; const shouldShowGrenade = (g: Grenade): boolean => { if (myTeam !== 'T' && myTeam !== 'CT') return true; const gt = teamOfGrenade(g, teamOfPlayer); return gt === myTeam; }; if (!isAuthed) { return (

Live Radar

Bitte einloggen, um das Live-Radar zu sehen.

); } return (
{/* Header */} {/* Unsichtbare WS-Clients */} setActiveMapKey(String(k).toLowerCase())} onPlayerUpdate={(p)=> { upsertPlayer(p); scheduleFlush() }} onPlayersAll={(m)=> { handlePlayersAll(m); scheduleFlush() }} onGrenades={(g)=> { handleGrenades(g); scheduleFlush() }} onRoundStart={() => { clearRoundArtifacts(true) }} onRoundEnd={() => { for (const [id, p] of playersRef.current) playersRef.current.set(id, { ...p, hasBomb: false }); if (bomb?.status === 'planted') { /* visual cleanup handled in state */ } clearRoundArtifacts(true); }} onBomb={handleBomb((raw:any) => { // lokal: normalizeBomb (aus alter Datei) – du kannst es ebenfalls auslagern, falls gewünscht const pickVec3 = (src:any) => { const p = src?.pos ?? src?.position ?? src?.location ?? src?.coordinates; if (Array.isArray(p)) return { x: +p[0]||0, y: +p[1]||0, z: +p[2]||0 }; if (typeof p === 'string') { const [x, y, z] = p.split(',').map(s=>Number(s.trim())); return { x:x||0, y:y||0, z:z||0 }; } return { x: +src?.x||0, y: +src?.y||0, z: +src?.z||0 }; }; if (!raw) return null; const payload = raw.bomb ?? raw.c4 ?? raw; const pos = pickVec3(payload); const t = String(raw?.type ?? '').toLowerCase(); if (t === 'bomb_beginplant' || t === 'bomb_abortplant') return null; let status: BombState['status'] = 'unknown'; const s = String(payload?.status ?? payload?.state ?? '').toLowerCase(); if (s.includes('planted')) status = 'planted'; else if (s.includes('drop')) status = 'dropped'; else if (s.includes('carry')) status = 'carried'; else if (s.includes('defus')) status = 'defusing'; if (payload?.planted) status = 'planted'; if (payload?.dropped) status = 'dropped'; if (payload?.carried) status = 'carried'; if (payload?.defusing) status = 'defusing'; if (payload?.defused) status = 'defused'; if (t === 'bomb_planted') status = 'planted'; if (t === 'bomb_dropped') status = 'dropped'; if (t === 'bomb_pickup') status = 'carried'; if (t === 'bomb_begindefuse') status = 'defusing'; if (t === 'bomb_abortdefuse') status = 'planted'; if (t === 'bomb_defused') status = 'defused'; const x = Number.isFinite(pos.x) ? pos.x : NaN; const y = Number.isFinite(pos.y) ? pos.y : NaN; const z = Number.isFinite(pos.z) ? pos.z : NaN; return { x, y, z, status, changedAt: Date.now() }; })} /> {/* Inhalt */}
{/* Left: T */} {myTeam !== 'CT' && ( p.team === 'T' && (!myTeam || p.team === myTeam)) .map(p => ({ id: p.id, name: p.name, hp: p.hp, armor: p.armor, helmet: p.helmet, defuse: p.defuse, hasBomb: p.hasBomb, alive: p.alive, activeWeapon: p.activeWeapon || null, weapons: p.weapons || null, grenades: p.nades || null, })) } // @ts-ignore showAvatars={useAvatars} // @ts-ignore avatarsById={avatarById} onHoverPlayer={setHoveredPlayerId} /> )} {/* Center: Radar */} setImgSize({ w: img.naturalWidth, h: img.naturalHeight })} onImgError={() => {}} imgSize={imgSize} worldToPx={worldToPx} unitsToPx={unitsToPx} players={players} grenades={grenades} trails={trails} deathMarkers={deathMarkers} useAvatars={useAvatars} avatarById={avatarById} hoveredPlayerId={hoveredPlayerId} setHoveredPlayerId={setHoveredPlayerId} myTeam={myTeam} beepState={beepState} bombFinal10={bombFinal10} bomb={bomb} shouldShowGrenade={shouldShowGrenade} /> {/* Right: CT */} {myTeam !== 'T' && ( p.team === 'CT' && (!myTeam || p.team === myTeam)) .map(p => ({ id: p.id, name: p.name, hp: p.hp, armor: p.armor, helmet: p.helmet, defuse: p.defuse, hasBomb: p.hasBomb, alive: p.alive, activeWeapon: p.activeWeapon || null, weapons: p.weapons || null, grenades: p.nades || null, })) } // @ts-ignore showAvatars={useAvatars} // @ts-ignore avatarsById={avatarById} onHoverPlayer={setHoveredPlayerId} /> )}
); }