updated radar
This commit is contained in:
parent
3c68c3ad2c
commit
94bbaaa37e
18
.env
18
.env
@ -21,15 +21,15 @@ PTERO_SERVER_SFTP_PASSWORD=IJHoYHTXQvJkCxkycTYM
|
||||
PTERO_SERVER_ID=37a11489
|
||||
|
||||
# META (vom CS2-Server-Plugin)
|
||||
NEXT_PUBLIC_CS2_META_WS_HOST=ironieopen.local
|
||||
NEXT_PUBLIC_CS2_META_WS_PORT=443
|
||||
NEXT_PUBLIC_CS2_META_WS_PATH=/telemetry
|
||||
NEXT_PUBLIC_CS2_META_WS_SCHEME=wss
|
||||
NEXT_PUBLIC_CS2_TELEMETRY_WS_HOST=ironieopen.local
|
||||
NEXT_PUBLIC_CS2_TELEMETRY_WS_PORT=443
|
||||
NEXT_PUBLIC_CS2_TELEMETRY_WS_PATH=/telemetry
|
||||
NEXT_PUBLIC_CS2_TELEMETRY_WS_SCHEME=wss
|
||||
|
||||
# POS (lokaler Aggregator)
|
||||
NEXT_PUBLIC_CS2_POS_WS_HOST=ironieopen.local
|
||||
NEXT_PUBLIC_CS2_POS_WS_PORT=443
|
||||
NEXT_PUBLIC_CS2_POS_WS_PATH=/positions
|
||||
NEXT_PUBLIC_CS2_POS_WS_SCHEME=wss
|
||||
# RADAR (lokaler Aggregator)
|
||||
NEXT_PUBLIC_CS2_GAME_WS_HOST=ironieopen.local
|
||||
NEXT_PUBLIC_CS2_GAME_WS_PORT=443
|
||||
NEXT_PUBLIC_CS2_GAME_WS_PATH=/game
|
||||
NEXT_PUBLIC_CS2_GAME_WS_SCHEME=wss
|
||||
|
||||
NEXT_PUBLIC_CONNECT_HREF="steam://connect/94.130.66.149:27015/0000"
|
||||
|
||||
@ -182,6 +182,35 @@ export default function Sidebar() {
|
||||
Spielplan
|
||||
</Button>
|
||||
</li>
|
||||
|
||||
{/* Radar */}
|
||||
<li>
|
||||
<Button
|
||||
onClick={() => { router.push('/radar'); setIsOpen(false) }}
|
||||
size="sm"
|
||||
variant="link"
|
||||
className={`${navBtnBase} ${isActive('/radar') ? activeClasses : idleClasses}`}
|
||||
>
|
||||
<svg
|
||||
className="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
{/* äußerer Kreis */}
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
{/* Sweep-Linie */}
|
||||
<line x1="12" y1="12" x2="21" y2="12" />
|
||||
{/* Zusatz-Ringe */}
|
||||
<circle cx="12" cy="12" r="6" strokeDasharray="4 4" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
Radar
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
||||
34
src/app/components/StatusDot.tsx
Normal file
34
src/app/components/StatusDot.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
|
||||
type WsStatus = 'idle' | 'connecting' | 'open' | 'closed' | 'error'
|
||||
|
||||
export default function StatusDot({
|
||||
status,
|
||||
label,
|
||||
className = '',
|
||||
}: {
|
||||
status: WsStatus
|
||||
label?: string
|
||||
className?: string
|
||||
}) {
|
||||
const color =
|
||||
status === 'open' ? 'bg-green-500' :
|
||||
status === 'connecting' ? 'bg-amber-500' :
|
||||
status === 'error' ? 'bg-red-500' :
|
||||
'bg-neutral-400'
|
||||
|
||||
const text =
|
||||
status === 'open' ? 'verbunden'
|
||||
: status === 'connecting' ? 'verbinde…'
|
||||
: status === 'error' ? 'Fehler'
|
||||
: status === 'closed' ? 'getrennt'
|
||||
: '—'
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 text-xs opacity-80 ${className}`}>
|
||||
{label && <span className="font-medium">{label}</span>}
|
||||
<span className={`inline-block w-2.5 h-2.5 rounded-full ${color}`} />
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@ -21,9 +21,9 @@ export default function Switch({
|
||||
className = '',
|
||||
}: SwitchProps) {
|
||||
return (
|
||||
<div className={`flex items-center gap-x-3 ${className}`}>
|
||||
<div className={`flex items-center gap-x-3 cursor-pointer ${className}`}>
|
||||
{labelLeft && (
|
||||
<label htmlFor={id} className="text-sm text-gray-500 dark:text-neutral-400">
|
||||
<label htmlFor={id} className="text-sm text-gray-500 dark:text-neutral-400 cursor-pointer">
|
||||
{labelLeft}
|
||||
</label>
|
||||
)}
|
||||
|
||||
95
src/app/components/TelemetrySocket.tsx
Normal file
95
src/app/components/TelemetrySocket.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { usePresenceStore } from '@/app/lib/usePresenceStore'
|
||||
import { useTelemetryStore } from '@/app/lib/useTelemetryStore'
|
||||
|
||||
type Status = 'idle'|'connecting'|'open'|'closed'|'error'
|
||||
|
||||
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}`
|
||||
}
|
||||
|
||||
export default function TelemetrySocket() {
|
||||
const url = useMemo(
|
||||
() => makeWsUrl(
|
||||
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_HOST,
|
||||
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_PORT,
|
||||
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_PATH,
|
||||
process.env.NEXT_PUBLIC_CS2_TELEMETRY_WS_SCHEME
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
const setSnapshot = usePresenceStore(s => s.setSnapshot)
|
||||
const setJoin = usePresenceStore(s => s.setJoin)
|
||||
const setLeave = usePresenceStore(s => s.setLeave)
|
||||
|
||||
const setMapKey = useTelemetryStore(s => s.setMapKey)
|
||||
|
||||
// interne Refs für saubere Handler
|
||||
const aliveRef = useRef(true)
|
||||
const retryRef = useRef<number | null>(null)
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
aliveRef.current = true
|
||||
|
||||
const connect = () => {
|
||||
if (!aliveRef.current || !url) return
|
||||
const ws = new WebSocket(url)
|
||||
wsRef.current = ws
|
||||
|
||||
ws.onopen = () => {
|
||||
if (process.env.NODE_ENV !== 'production') console.debug('[TelemetrySocket] open')
|
||||
}
|
||||
ws.onerror = () => {
|
||||
if (process.env.NODE_ENV !== 'production') console.debug('[TelemetrySocket] error')
|
||||
}
|
||||
ws.onclose = () => {
|
||||
if (process.env.NODE_ENV !== 'production') console.debug('[TelemetrySocket] closed')
|
||||
if (aliveRef.current) retryRef.current = window.setTimeout(connect, 2000)
|
||||
}
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
let msg: any = null
|
||||
try { msg = JSON.parse(String(ev.data ?? '')) } catch {}
|
||||
if (!msg) return
|
||||
|
||||
if (msg.type === 'players' && Array.isArray(msg.players)) {
|
||||
// kompletter Presence-Snapshot
|
||||
setSnapshot(msg.players)
|
||||
} else if (msg.type === 'player_join' && msg.player) {
|
||||
setJoin(msg.player)
|
||||
} else if (msg.type === 'player_leave') {
|
||||
const sid = msg.steamId ?? msg.steam_id ?? msg.id
|
||||
if (sid != null) setLeave(sid)
|
||||
}
|
||||
|
||||
// ⬇️ Map-Event aus /telemetry
|
||||
if (msg.type === 'map' && typeof msg.name === 'string') {
|
||||
const key = msg.name.toLowerCase()
|
||||
if (process.env.NODE_ENV!=='production') console.debug('[TelemetrySocket] map:', key)
|
||||
setMapKey(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connect()
|
||||
return () => {
|
||||
aliveRef.current = false
|
||||
if (retryRef.current) window.clearTimeout(retryRef.current)
|
||||
try { wsRef.current?.close(1000, 'telemetry socket unmounted') } catch {}
|
||||
}
|
||||
}, [url, setSnapshot, setJoin, setLeave])
|
||||
|
||||
return null
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
// MetaSocket.tsx
|
||||
|
||||
'use client'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
type Status = 'idle'|'connecting'|'open'|'closed'|'error'
|
||||
type MetaSocketProps = {
|
||||
url?: string
|
||||
onStatus?: (s: Status) => void
|
||||
onMap?: (mapKey: string) => void
|
||||
onPlayersSnapshot?: (list: any[]) => void
|
||||
onPlayerJoin?: (p: any) => void
|
||||
onPlayerLeave?: (steamId: string | number) => void
|
||||
}
|
||||
|
||||
export default function MetaSocket({
|
||||
url,
|
||||
onStatus,
|
||||
onMap,
|
||||
onPlayersSnapshot,
|
||||
onPlayerJoin,
|
||||
onPlayerLeave,
|
||||
}: MetaSocketProps) {
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const aliveRef = useRef(true)
|
||||
const retryRef = useRef<number | null>(null)
|
||||
|
||||
// aktuelle Handler in Refs spiegeln (ändern NICHT die Effect-Dependencies)
|
||||
const onMapRef = useRef(onMap)
|
||||
const onPlayersSnapshotRef = useRef(onPlayersSnapshot)
|
||||
const onPlayerJoinRef = useRef(onPlayerJoin)
|
||||
const onPlayerLeaveRef = useRef(onPlayerLeave)
|
||||
useEffect(() => { onMapRef.current = onMap }, [onMap])
|
||||
useEffect(() => { onPlayersSnapshotRef.current = onPlayersSnapshot }, [onPlayersSnapshot])
|
||||
useEffect(() => { onPlayerJoinRef.current = onPlayerJoin }, [onPlayerJoin])
|
||||
useEffect(() => { onPlayerLeaveRef.current = onPlayerLeave }, [onPlayerLeave])
|
||||
|
||||
useEffect(() => {
|
||||
aliveRef.current = true
|
||||
|
||||
const connect = () => {
|
||||
if (!aliveRef.current || !url) return
|
||||
onStatus?.('connecting')
|
||||
|
||||
const ws = new WebSocket(url)
|
||||
wsRef.current = ws
|
||||
|
||||
ws.onopen = () => onStatus?.('open')
|
||||
ws.onerror = () => onStatus?.('error')
|
||||
ws.onclose = () => {
|
||||
onStatus?.('closed')
|
||||
// optional: Backoff oder ganz ohne Auto-Reconnect, je nach Wunsch
|
||||
if (aliveRef.current) retryRef.current = window.setTimeout(connect, 2000)
|
||||
}
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
let msg: any = null
|
||||
try { msg = JSON.parse(String(ev.data ?? '')) } catch {}
|
||||
if (!msg) return
|
||||
|
||||
if (msg.type === 'map' && typeof msg.name === 'string') {
|
||||
onMapRef.current?.(msg.name.toLowerCase())
|
||||
} else if (msg.type === 'players' && Array.isArray(msg.players)) {
|
||||
onPlayersSnapshotRef.current?.(msg.players)
|
||||
} else if (msg.type === 'player_join' && msg.player) {
|
||||
onPlayerJoinRef.current?.(msg.player)
|
||||
} else if (msg.type === 'player_leave') {
|
||||
onPlayerLeaveRef.current?.(msg.steamId ?? msg.steam_id ?? msg.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connect()
|
||||
|
||||
return () => {
|
||||
aliveRef.current = false
|
||||
if (retryRef.current) window.clearTimeout(retryRef.current)
|
||||
try { wsRef.current?.close(1000, 'meta unmounted') } catch {}
|
||||
}
|
||||
}, [url, onStatus]) // <— nur auf url/onStatus hören!
|
||||
|
||||
return null
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
// PositionsSocket.tsx
|
||||
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
type Status = 'idle'|'connecting'|'open'|'closed'|'error'
|
||||
|
||||
type PositionsSocketProps = {
|
||||
url?: string
|
||||
onStatus?: (s: Status) => void
|
||||
onMap?: (mapKey: string) => void
|
||||
onPlayerUpdate?: (p: any) => void
|
||||
onPlayersAll?: (allplayers: any) => void
|
||||
onGrenades?: (g: any) => void
|
||||
onRoundStart?: () => void
|
||||
onRoundEnd?: () => void
|
||||
onBomb?: (b:any) => void
|
||||
}
|
||||
|
||||
export default function PositionsSocket({
|
||||
url,
|
||||
onStatus,
|
||||
onMap,
|
||||
onPlayerUpdate,
|
||||
onPlayersAll,
|
||||
onGrenades,
|
||||
onRoundStart,
|
||||
onRoundEnd,
|
||||
onBomb
|
||||
}: PositionsSocketProps) {
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const aliveRef = useRef(true)
|
||||
const retryRef = useRef<number | null>(null)
|
||||
|
||||
const dispatch = (msg: any) => {
|
||||
if (!msg) return;
|
||||
|
||||
if (msg.type === 'round_start') { onRoundStart?.(); return }
|
||||
if (msg.type === 'round_end') { onRoundEnd?.(); return }
|
||||
|
||||
if (msg.type === 'tick') {
|
||||
if (typeof msg.map === 'string') onMap?.(msg.map.toLowerCase())
|
||||
if (Array.isArray(msg.players)) msg.players.forEach(onPlayerUpdate ?? (() => {}))
|
||||
if (msg.grenades) onGrenades?.(msg.grenades)
|
||||
if (msg.bomb) onBomb?.(msg.bomb)
|
||||
return
|
||||
}
|
||||
|
||||
if (msg.map && typeof msg.map.name === 'string') onMap?.(msg.map.name.toLowerCase())
|
||||
if (msg.allplayers) onPlayersAll?.(msg)
|
||||
if (msg.player || msg.steamId || msg.position || msg.pos) onPlayerUpdate?.(msg)
|
||||
if (msg.grenades && msg.type !== 'tick') onGrenades?.(msg.grenades)
|
||||
|
||||
const t = String(msg.type || '').toLowerCase();
|
||||
if (msg.bomb || msg.c4 || t.startsWith('bomb_')) {
|
||||
onBomb?.(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
aliveRef.current = true
|
||||
const connect = () => {
|
||||
if (!aliveRef.current) return
|
||||
onStatus?.('connecting')
|
||||
const ws = new WebSocket(url!)
|
||||
wsRef.current = ws
|
||||
|
||||
ws.onopen = () => onStatus?.('open')
|
||||
ws.onerror = () => onStatus?.('error')
|
||||
ws.onclose = () => {
|
||||
onStatus?.('closed')
|
||||
if (aliveRef.current) retryRef.current = window.setTimeout(connect, 2000)
|
||||
}
|
||||
ws.onmessage = (ev) => {
|
||||
let msg: any = null
|
||||
try { msg = JSON.parse(String(ev.data ?? '')) } catch {}
|
||||
if (Array.isArray(msg)) msg.forEach(dispatch)
|
||||
else dispatch(msg)
|
||||
}
|
||||
}
|
||||
|
||||
if (url) connect()
|
||||
return () => {
|
||||
aliveRef.current = false
|
||||
if (retryRef.current) window.clearTimeout(retryRef.current)
|
||||
try { wsRef.current?.close(1000, 'positions unmounted') } catch {}
|
||||
}
|
||||
}, [url, onStatus])
|
||||
|
||||
return null
|
||||
}
|
||||
@ -13,6 +13,7 @@ import SSEHandler from "./lib/SSEHandler";
|
||||
import UserActivityTracker from "./components/UserActivityTracker";
|
||||
import AudioPrimer from "./components/AudioPrimer";
|
||||
import ReadyOverlayHost from "./components/ReadyOverlayHost";
|
||||
import TelemetrySocket from "./components/TelemetrySocket";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@ -39,6 +40,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<UserActivityTracker />
|
||||
<AudioPrimer />
|
||||
<ReadyOverlayHost />
|
||||
<TelemetrySocket />
|
||||
|
||||
{/* App-Shell: Sidebar | Main */}
|
||||
<div className="min-h-dvh grid grid-cols-1 sm:grid-cols-[16rem_1fr]">
|
||||
|
||||
83
src/app/lib/usePresenceStore.ts
Normal file
83
src/app/lib/usePresenceStore.ts
Normal file
@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
import { create } from 'zustand'
|
||||
|
||||
type PresenceInfo = {
|
||||
id: string
|
||||
name?: string | null
|
||||
team?: 'T' | 'CT' | string
|
||||
joinedAt: number
|
||||
}
|
||||
|
||||
type PresenceState = {
|
||||
// online nach SteamID
|
||||
online: Record<string, PresenceInfo>
|
||||
setSnapshot: (players: Array<{ steamId: string|number; name?: string; team?: any }>) => void
|
||||
setJoin: (p: { steamId: string|number; name?: string; team?: any }) => void
|
||||
setLeave: (sid: string|number) => void
|
||||
isOnline: (sid?: string|null) => boolean
|
||||
onlineIds: () => string[]
|
||||
}
|
||||
|
||||
const normTeam = (t: any): 'T' | 'CT' | string => {
|
||||
if (t === 2 || t === 'T' || t === 't') return 'T'
|
||||
if (t === 3 || t === 'CT' || t === 'ct') return 'CT'
|
||||
return String(t ?? '')
|
||||
}
|
||||
|
||||
const sidOf = (raw: any): string | null => {
|
||||
const s = raw != null ? String(raw) : ''
|
||||
return s && s !== '0' ? s : null
|
||||
}
|
||||
|
||||
export const usePresenceStore = create<PresenceState>((set, get) => ({
|
||||
online: {},
|
||||
|
||||
setSnapshot: (players) => {
|
||||
const map: Record<string, PresenceInfo> = {}
|
||||
const now = Date.now()
|
||||
for (const p of players) {
|
||||
const id = sidOf(p.steamId)
|
||||
if (!id) continue
|
||||
map[id] = {
|
||||
id,
|
||||
name: p.name ?? null,
|
||||
team: normTeam(p.team),
|
||||
joinedAt: now,
|
||||
}
|
||||
}
|
||||
set({ online: map })
|
||||
},
|
||||
|
||||
setJoin: (p) => {
|
||||
const id = sidOf(p.steamId)
|
||||
if (!id) return
|
||||
set(s => ({
|
||||
online: {
|
||||
...s.online,
|
||||
[id]: {
|
||||
id,
|
||||
name: p.name ?? s.online[id]?.name ?? null,
|
||||
team: normTeam(p.team ?? s.online[id]?.team),
|
||||
joinedAt: s.online[id]?.joinedAt ?? Date.now(),
|
||||
}
|
||||
}
|
||||
}))
|
||||
},
|
||||
|
||||
setLeave: (sid) => {
|
||||
const id = sidOf(sid)
|
||||
if (!id) return
|
||||
set(s => {
|
||||
const next = { ...s.online }
|
||||
delete next[id]
|
||||
return { online: next }
|
||||
})
|
||||
},
|
||||
|
||||
isOnline: (sid) => !!(sid && get().online[String(sid)]),
|
||||
onlineIds: () => Object.keys(get().online),
|
||||
}))
|
||||
|
||||
// Bequemer Hook: true/false
|
||||
export const useIsPlayerOnline = (steamId?: string|null) =>
|
||||
usePresenceStore(s => !!(steamId && s.online[String(steamId)]))
|
||||
@ -1,3 +1,5 @@
|
||||
// /src/app/lib/useReadyOverlayStore.ts
|
||||
|
||||
'use client'
|
||||
|
||||
import { create } from 'zustand'
|
||||
|
||||
13
src/app/lib/useTelemetryStore.ts
Normal file
13
src/app/lib/useTelemetryStore.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// /src/app/lib/useTelemetryStore.ts
|
||||
'use client'
|
||||
import { create } from 'zustand'
|
||||
|
||||
type State = {
|
||||
mapKey: string | null
|
||||
setMapKey: (k: string | null) => void
|
||||
}
|
||||
|
||||
export const useTelemetryStore = create<State>((set) => ({
|
||||
mapKey: null,
|
||||
setMapKey: (k) => set({ mapKey: k }),
|
||||
}))
|
||||
108
src/app/radar/GameSocket.tsx
Normal file
108
src/app/radar/GameSocket.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
// /src/app/radar/GameSocket.tsx
|
||||
'use client'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
type Status = 'idle'|'connecting'|'open'|'closed'|'error'
|
||||
type GameSocketProps = {
|
||||
url?: string
|
||||
onStatus?: (s: Status) => void
|
||||
onMap?: (mapKey: string) => void
|
||||
onPlayerUpdate?: (p: any) => void
|
||||
onPlayersAll?: (payload: any) => void
|
||||
onGrenades?: (g: any) => void
|
||||
onRoundStart?: () => void
|
||||
onRoundEnd?: () => void
|
||||
onBomb?: (b:any) => void
|
||||
}
|
||||
|
||||
export default function GameSocket(props: GameSocketProps) {
|
||||
const { url, onStatus, onMap, onPlayerUpdate, onPlayersAll, onGrenades, onRoundStart, onRoundEnd, onBomb } = props
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const retryRef = useRef<number | null>(null)
|
||||
const connectTimerRef = useRef<number | null>(null) // <- NEU
|
||||
const shouldReconnectRef = useRef(true)
|
||||
|
||||
const dispatch = (msg: any) => {
|
||||
if (!msg) return
|
||||
if (msg.type === 'round_start') { onRoundStart?.(); return }
|
||||
if (msg.type === 'round_end') { onRoundEnd?.(); return }
|
||||
|
||||
if (msg.type === 'tick') {
|
||||
if (typeof msg.map === 'string') onMap?.(msg.map.toLowerCase())
|
||||
if (Array.isArray(msg.players)) msg.players.forEach(onPlayerUpdate ?? (() => {}))
|
||||
|
||||
const g = msg.grenades ?? msg.projectiles ?? msg.nades ?? msg.grenadeProjectiles
|
||||
if (g) onGrenades?.(g)
|
||||
|
||||
if (msg.bomb) onBomb?.(msg.bomb)
|
||||
onPlayersAll?.(msg)
|
||||
return
|
||||
}
|
||||
|
||||
// non-tick:
|
||||
const g2 = msg.grenades ?? msg.projectiles ?? msg.nades ?? msg.grenadeProjectiles
|
||||
if (g2 && msg.type !== 'tick') onGrenades?.(g2)
|
||||
|
||||
if (msg.map && typeof msg.map.name === 'string') onMap?.(msg.map.name.toLowerCase())
|
||||
if (msg.allplayers) onPlayersAll?.(msg)
|
||||
if (msg.player || msg.steamId || msg.position || msg.pos) onPlayerUpdate?.(msg)
|
||||
if (msg.grenades && msg.type !== 'tick') onGrenades?.(msg.grenades)
|
||||
|
||||
const t = String(msg.type || '').toLowerCase()
|
||||
if (msg.bomb || msg.c4 || t.startsWith('bomb_')) onBomb?.(msg)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!url) return
|
||||
shouldReconnectRef.current = true
|
||||
|
||||
// evtl. alte Ressourcen räumen
|
||||
try { wsRef.current?.close(1000, 'replaced by new /radar visit') } catch {}
|
||||
if (retryRef.current) { window.clearTimeout(retryRef.current); retryRef.current = null }
|
||||
if (connectTimerRef.current) { window.clearTimeout(connectTimerRef.current); connectTimerRef.current = null }
|
||||
|
||||
const connect = () => {
|
||||
if (!shouldReconnectRef.current) return
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) return
|
||||
|
||||
onStatus?.('connecting')
|
||||
const ws = new WebSocket(url)
|
||||
wsRef.current = ws
|
||||
|
||||
ws.onopen = () => onStatus?.('open')
|
||||
ws.onerror = () => onStatus?.('error')
|
||||
ws.onclose = () => {
|
||||
onStatus?.('closed')
|
||||
if (shouldReconnectRef.current) {
|
||||
retryRef.current = window.setTimeout(connect, 2000)
|
||||
}
|
||||
}
|
||||
ws.onmessage = (ev) => {
|
||||
let msg: any = null
|
||||
try { msg = JSON.parse(String(ev.data ?? '')) } catch {}
|
||||
if (Array.isArray(msg)) msg.forEach(dispatch)
|
||||
else dispatch(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// *** WICHTIG: leicht verzögert verbinden ***
|
||||
// Verhindert das Fehl-Log im React-Strict-Mode (Mount->Unmount->Mount).
|
||||
connectTimerRef.current = window.setTimeout(connect, 0)
|
||||
|
||||
return () => {
|
||||
shouldReconnectRef.current = false
|
||||
|
||||
if (connectTimerRef.current) { window.clearTimeout(connectTimerRef.current); connectTimerRef.current = null }
|
||||
if (retryRef.current) { window.clearTimeout(retryRef.current); retryRef.current = null }
|
||||
|
||||
const ws = wsRef.current
|
||||
if (ws) {
|
||||
ws.onclose = null // Reconnect nicht anstoßen
|
||||
try { ws.close(1000, 'left /radar') } catch {}
|
||||
}
|
||||
onStatus?.('closed')
|
||||
}
|
||||
}, [url])
|
||||
|
||||
return null
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
// TeamSidebar.tsx
|
||||
// /src/app/radar/TeamSidebar.tsx
|
||||
'use client'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useAvatarDirectoryStore } from '@/app/lib/useAvatarDirectoryStore'
|
||||
@ -79,7 +79,8 @@ export default function TeamSidebar({
|
||||
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
key={`player-${p.id}`}
|
||||
id={`player-${p.id}`}
|
||||
onMouseEnter={() => onHoverPlayer?.(p.id)}
|
||||
onMouseLeave={() => onHoverPlayer?.(null)}
|
||||
//className={`rounded-md px-2 py-2 bg-neutral-800/40 ${dead ? 'opacity-60' : ''}`}
|
||||
@ -1,7 +1,7 @@
|
||||
// /app/match-details/[matchId]/radar/page.tsx
|
||||
// /src/app/radar/page.tsx
|
||||
|
||||
import Card from '@/app/components/Card'
|
||||
import LiveRadar from '@/app/components/radar/LiveRadar'
|
||||
import LiveRadar from './LiveRadar';
|
||||
|
||||
export default function RadarPage({ params }: { params: { matchId: string } }) {
|
||||
return (
|
||||
Loading…
x
Reference in New Issue
Block a user