261 lines
10 KiB
TypeScript
261 lines
10 KiB
TypeScript
'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 (
|
||
<div className="h-full w-full grid place-items-center">
|
||
<div className="text-center max-w-sm">
|
||
<h2 className="text-xl font-semibold mb-2">Live Radar</h2>
|
||
<p className="opacity-80">Bitte einloggen, um das Live-Radar zu sehen.</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="h-full min-h-0 w-full flex flex-col overflow-hidden">
|
||
{/* Header */}
|
||
<RadarHeader
|
||
useAvatars={useAvatars}
|
||
setUseAvatars={setUseAvatars}
|
||
radarWsStatus={radarWsStatus}
|
||
/>
|
||
|
||
{/* Unsichtbare WS-Clients */}
|
||
<GameSocket
|
||
url={gameUrl}
|
||
onStatus={setGameWsStatus}
|
||
onMap={(k)=> 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 */}
|
||
<div className="flex-1 min-h-0 grid grid-cols-[minmax(240px,340px)_1fr_minmax(240px,340px)] gap-4">
|
||
{/* Left: T */}
|
||
{myTeam !== 'CT' && (
|
||
<TeamSidebar
|
||
team="T"
|
||
teamId={teamIdT}
|
||
players={players
|
||
.filter(p => 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 */}
|
||
<RadarCanvas
|
||
activeMapKey={activeMapKey}
|
||
currentSrc={currentSrc}
|
||
onImgLoad={(img)=> 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' && (
|
||
<TeamSidebar
|
||
team="CT"
|
||
align="right"
|
||
teamId={teamIdCT}
|
||
players={players
|
||
.filter(p => 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}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|