2025-09-21 22:33:16 +02:00

261 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
);
}