update
This commit is contained in:
parent
640505bc1f
commit
83127a7c14
4
.env
4
.env
@ -23,6 +23,6 @@ PTERO_SERVER_SFTP_URL=sftp://panel.ironieopen.de:2022
|
||||
PTERO_SERVER_SFTP_USER=army.37a11489
|
||||
PTERO_SERVER_SFTP_PASSWORD=IJHoYHTXQvJkCxkycTYM
|
||||
PTERO_SERVER_ID=37a11489
|
||||
NEXT_PUBLIC_CS2_WS_URL=wss://cs2.ironieopen.de:8081/telemetry
|
||||
NEXT_PUBLIC_CS2_WS_HOST=cs2.ironieopen.de
|
||||
NEXT_PUBLIC_CS2_WS_URL=wss://ws.ironieopen.de:8081/telemetry
|
||||
NEXT_PUBLIC_CS2_WS_HOST=ws.ironieopen.de
|
||||
NEXT_PUBLIC_CS2_WS_PORT=8081
|
||||
@ -1,8 +1,7 @@
|
||||
// /app/components/LiveRadar.tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Button from './Button'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import LoadingSpinner from './LoadingSpinner'
|
||||
|
||||
type Props = { matchId: string }
|
||||
@ -13,15 +12,45 @@ type ApiResponse = {
|
||||
mapVisuals?: Record<string, { label: string; bg: string }>
|
||||
}
|
||||
|
||||
type PlayerState = {
|
||||
id: string
|
||||
name?: string | null
|
||||
team?: 'T' | 'CT' | string
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
yaw: number
|
||||
alive?: boolean
|
||||
}
|
||||
|
||||
export default function LiveRadar({ matchId }: Props) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [data, setData] = useState<ApiResponse | null>(null)
|
||||
|
||||
// --- WS Status für Header ---
|
||||
// WS-Status + von WS gemeldete Map
|
||||
const [wsStatus, setWsStatus] = useState<'idle'|'connecting'|'open'|'closed'|'error'>('idle')
|
||||
const [wsMapKey, setWsMapKey] = useState<string | null>(null) // <<— NEU
|
||||
|
||||
// 1) MapVote laden (welche Map wurde gewählt?)
|
||||
// Headerhöhe → max Bildhöhe
|
||||
const headerRef = useRef<HTMLDivElement | null>(null)
|
||||
const [maxImgHeight, setMaxImgHeight] = useState<number | null>(null)
|
||||
useEffect(() => {
|
||||
const updateMax = () => {
|
||||
const bottom = headerRef.current?.getBoundingClientRect().bottom ?? 0
|
||||
const h = Math.max(120, Math.floor(window.innerHeight - bottom - 16))
|
||||
setMaxImgHeight(h)
|
||||
}
|
||||
updateMax()
|
||||
window.addEventListener('resize', updateMax)
|
||||
window.addEventListener('scroll', updateMax, { passive: true })
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateMax)
|
||||
window.removeEventListener('scroll', updateMax)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 1) MapVote laden (Fallback, falls WS-Map noch nicht kam)
|
||||
useEffect(() => {
|
||||
let canceled = false
|
||||
const load = async () => {
|
||||
@ -45,16 +74,39 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
return () => { canceled = true }
|
||||
}, [matchId])
|
||||
|
||||
// 2) Beim Betreten des Radars mit dem CS2-WS-Server verbinden und alles loggen
|
||||
// ========= Spieler-Overlay =========
|
||||
const playersRef = useRef<Map<string, PlayerState>>(new Map())
|
||||
const [players, setPlayers] = useState<PlayerState[]>([])
|
||||
const flushTimer = useRef<number | null>(null)
|
||||
const scheduleFlush = () => {
|
||||
if (flushTimer.current != null) return
|
||||
flushTimer.current = window.setTimeout(() => {
|
||||
flushTimer.current = null
|
||||
setPlayers(Array.from(playersRef.current.values()))
|
||||
}, 66)
|
||||
}
|
||||
function parsePlayer(p: any): PlayerState | null {
|
||||
if (!p) return null
|
||||
const id = p.steamId || p.steam_id || p.userId || p.playerId || p.id || p.name
|
||||
if (!id) return null
|
||||
const pos = p.pos || p.position || p.location || p.coordinates
|
||||
const x = Number(p.x ?? pos?.x ?? (Array.isArray(pos) ? pos[0] : undefined))
|
||||
const y = Number(p.y ?? pos?.y ?? (Array.isArray(pos) ? pos[1] : undefined))
|
||||
const z = Number(p.z ?? pos?.z ?? (Array.isArray(pos) ? pos[2] : undefined))
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
|
||||
const yaw = Number(p.yaw ?? p.ang?.y ?? p.angles?.y ?? p.rotation?.yaw ?? p.view?.yaw ?? 0)
|
||||
return { id: String(id), name: p.name, team: p.team, x, y, z: Number.isFinite(z) ? z : 0, yaw, alive: p.alive }
|
||||
}
|
||||
|
||||
// 2) WS verbinden — zusätzlich Map-Meldung verarbeiten
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
// Priorität: explizite URL > Host/Port > Fallback auf aktuelle Hostname:8081
|
||||
const explicit = process.env.NEXT_PUBLIC_CS2_WS_URL
|
||||
const host = process.env.NEXT_PUBLIC_CS2_WS_HOST || window.location.hostname
|
||||
const port = process.env.NEXT_PUBLIC_CS2_WS_PORT || '8081'
|
||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const url = explicit || `${proto}://${host}:${port}`
|
||||
const path = process.env.NEXT_PUBLIC_CS2_WS_PATH || '/telemetry'
|
||||
const url = explicit || `${proto}://${host}:${port}${path}`
|
||||
|
||||
let alive = true
|
||||
let ws: WebSocket | null = null
|
||||
@ -71,13 +123,33 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
}
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
// Rohdaten + JSON-parsed in der Konsole anzeigen
|
||||
const raw = ev.data
|
||||
try {
|
||||
const parsed = JSON.parse(raw as string)
|
||||
console.log('[cs2-ws] message (json):', parsed)
|
||||
} catch {
|
||||
console.log('[cs2-ws] message (raw):', raw)
|
||||
let msg: any = null
|
||||
try { msg = JSON.parse(ev.data as string) } catch { /* ignore raw */ }
|
||||
|
||||
// --- NEU: Map-Meldung direkt auswerten ---
|
||||
if (msg && msg.type === 'map' && typeof msg.name === 'string') {
|
||||
// msg.name z.B. "de_dust2"
|
||||
setWsMapKey(msg.name)
|
||||
return
|
||||
}
|
||||
|
||||
// Spieler-Formate
|
||||
const candidates: any[] = []
|
||||
if (Array.isArray(msg)) candidates.push(...msg)
|
||||
else if (msg?.type === 'players' && Array.isArray(msg.players)) candidates.push(...msg.players)
|
||||
else if (Array.isArray(msg?.players)) candidates.push(...msg.players)
|
||||
else if (Array.isArray(msg?.telemetry?.players)) candidates.push(...msg.telemetry.players)
|
||||
else if (msg?.type === 'player' || msg?.steamId || msg?.pos || msg?.position) candidates.push(msg)
|
||||
|
||||
if (candidates.length) {
|
||||
let changed = false
|
||||
for (const raw of candidates) {
|
||||
const p = parsePlayer(raw)
|
||||
if (!p) continue
|
||||
playersRef.current.set(p.id, p)
|
||||
changed = true
|
||||
}
|
||||
if (changed) scheduleFlush()
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,52 +161,67 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
ws.onclose = (ev) => {
|
||||
console.warn('[cs2-ws] closed:', ev.code, ev.reason)
|
||||
setWsStatus('closed')
|
||||
if (alive) {
|
||||
// simpler Reconnect nach 2s
|
||||
retryTimer = window.setTimeout(connect, 2000)
|
||||
}
|
||||
if (alive) retryTimer = window.setTimeout(connect, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
connect()
|
||||
|
||||
return () => {
|
||||
alive = false
|
||||
if (retryTimer) window.clearTimeout(retryTimer)
|
||||
try { ws?.close(1000, 'radar unmounted') } catch {}
|
||||
}
|
||||
}, []) // nur einmal beim Mount auf /radar verbinden
|
||||
}, [])
|
||||
|
||||
// Erste gespielte Map (Pick/Decider in Anzeige-Reihenfolge)
|
||||
const firstMapKey = useMemo(() => {
|
||||
// Fallback-Map aus dem Voting (erste gespielte)
|
||||
const votedFirstMapKey = useMemo(() => {
|
||||
const chosen = (data?.steps ?? []).filter(s => (s.action === 'pick' || s.action === 'decider') && s.map)
|
||||
return chosen[0]?.map ?? null
|
||||
}, [data])
|
||||
|
||||
const mapLabel = useMemo(() => {
|
||||
if (!firstMapKey) return 'Unbekannte Map'
|
||||
return data?.mapVisuals?.[firstMapKey]?.label ?? firstMapKey
|
||||
}, [data, firstMapKey])
|
||||
// Aktive Map: WS > Voting
|
||||
const activeMapKey = wsMapKey ?? votedFirstMapKey
|
||||
|
||||
// Radar-Datei(en)
|
||||
// Label
|
||||
const mapLabel = useMemo(() => {
|
||||
if (!activeMapKey) return 'Unbekannte Map'
|
||||
return data?.mapVisuals?.[activeMapKey]?.label ?? activeMapKey
|
||||
}, [data, activeMapKey])
|
||||
|
||||
// Radar-Datei(en) für aktive Map
|
||||
const { folderKey, candidates } = useMemo(() => {
|
||||
if (!firstMapKey) return { folderKey: null as string | null, candidates: [] as string[] }
|
||||
const key = firstMapKey.startsWith('de_') ? firstMapKey.slice(3) : firstMapKey
|
||||
const base = `/assets/img/radar/${key}`
|
||||
if (!activeMapKey) return { folderKey: null as string | null, candidates: [] as string[] }
|
||||
const key = activeMapKey.startsWith('de_') ? activeMapKey.slice(3) : activeMapKey
|
||||
const base = `/assets/img/radar/${activeMapKey}` // Ordner ist [mapKey], z.B. de_dust2
|
||||
const list = [
|
||||
`${base}/de_${key}_radar_psd.png`,
|
||||
`${base}/de_${key}_lower_radar_psd.png`,
|
||||
`${base}/de_${key}_v1_radar_psd.png`,
|
||||
`${base}/de_${key}_radar.png`, // optionaler Fallback
|
||||
`${base}/de_${key}_radar.png`,
|
||||
]
|
||||
return { folderKey: key, candidates: list }
|
||||
}, [firstMapKey])
|
||||
}, [activeMapKey])
|
||||
|
||||
const [srcIdx, setSrcIdx] = useState(0)
|
||||
useEffect(() => { setSrcIdx(0) }, [folderKey])
|
||||
const currentSrc = candidates[srcIdx]
|
||||
|
||||
// kleines Badge für WS-Status
|
||||
// Bildgröße für SVG-Overlay
|
||||
const [imgSize, setImgSize] = useState<{w: number, h: number} | null>(null)
|
||||
|
||||
// Welt→Bild (0/0/0 = Bildmitte)
|
||||
const DEFAULT_WORLD_RADIUS = 4096
|
||||
const pxPerUnit = useMemo(() => {
|
||||
if (!imgSize) return 0.1
|
||||
const spanPx = Math.min(imgSize.w, imgSize.h)
|
||||
return spanPx / (2 * DEFAULT_WORLD_RADIUS)
|
||||
}, [imgSize])
|
||||
const worldToPx = (x: number, y: number) => {
|
||||
if (!imgSize) return { x: 0, y: 0 }
|
||||
const cx = imgSize.w / 2, cy = imgSize.h / 2
|
||||
return { x: cx + x * pxPerUnit, y: cy - y * pxPerUnit }
|
||||
}
|
||||
|
||||
const WsDot = ({ status }: { status: typeof wsStatus }) => {
|
||||
const color =
|
||||
status === 'open' ? 'bg-green-500' :
|
||||
@ -156,7 +243,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div ref={headerRef} className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">Live Radar</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm opacity-80">{mapLabel}</div>
|
||||
@ -168,25 +255,59 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
<div className="p-8 flex justify-center"><LoadingSpinner /></div>
|
||||
) : error ? (
|
||||
<div className="p-4 text-red-600">{error}</div>
|
||||
) : !firstMapKey ? (
|
||||
) : !activeMapKey ? (
|
||||
<div className="p-4 rounded bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-100">
|
||||
Noch keine gespielte Map gefunden.
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
{/* großes Radar-Bild */}
|
||||
<div className="relative w-full max-w-5xl mx-auto rounded-lg overflow-hidden border border-neutral-300 dark:border-neutral-700 bg-neutral-100 dark:bg-neutral-800">
|
||||
<div
|
||||
className="relative mx-auto rounded-lg overflow-hidden border border-neutral-300 dark:border-neutral-700 bg-neutral-100 dark:bg-neutral-800 inline-block"
|
||||
style={{ maxHeight: maxImgHeight ?? undefined }}
|
||||
>
|
||||
{currentSrc ? (
|
||||
<>
|
||||
<img
|
||||
key={currentSrc}
|
||||
src={currentSrc}
|
||||
alt={mapLabel}
|
||||
className="w-full h-auto block"
|
||||
className="block h-auto max-w-full"
|
||||
style={{ maxHeight: maxImgHeight ?? undefined }}
|
||||
onLoad={(e) => {
|
||||
const img = e.currentTarget
|
||||
setImgSize({ w: img.naturalWidth, h: img.naturalHeight })
|
||||
}}
|
||||
onError={() => {
|
||||
if (srcIdx < candidates.length - 1) setSrcIdx(i => i + 1)
|
||||
else setError('Radar-Grafik nicht gefunden.')
|
||||
}}
|
||||
/>
|
||||
{imgSize && (
|
||||
<svg
|
||||
className="absolute inset-0 w-full h-full pointer-events-none"
|
||||
viewBox={`0 0 ${imgSize.w} ${imgSize.h}`}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
{players.map((p) => {
|
||||
const { x, y } = worldToPx(p.x, p.y)
|
||||
const dirLen = Math.max(18, Math.min(imgSize.w, imgSize.h) * 0.025)
|
||||
const rad = (p.yaw * Math.PI) / 180
|
||||
const dx = Math.cos(rad) * dirLen
|
||||
const dy = -Math.sin(rad) * dirLen
|
||||
const r = Math.max(4, Math.min(imgSize.w, imgSize.h) * 0.008)
|
||||
const color = p.team === 'CT' ? '#3b82f6' : p.team === 'T' ? '#f59e0b' : '#10b981'
|
||||
const strokeW = Math.max(1.5, r * 0.35)
|
||||
|
||||
return (
|
||||
<g key={p.id}>
|
||||
<line x1={x} y1={y} x2={x + dx} y2={y + dy} stroke={color} strokeWidth={strokeW} strokeLinecap="round" opacity={p.alive === false ? 0.5 : 1} />
|
||||
<circle cx={x} cy={y} r={r} fill={color} stroke="#ffffff" strokeWidth={Math.max(1, r * 0.3)} opacity={p.alive === false ? 0.6 : 1} />
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="p-6 text-center">Keine Radar-Grafik gefunden.</div>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user