'use client' import { useEffect, useMemo, useRef, useState } from 'react' import MetaSocket from './MetaSocket' import PositionsSocket from './PositionsSocket' import TeamSidebar from './TeamSidebar' /* ───────── UI config ───────── */ const UI = { player: { minRadiusPx: 4, radiusRel: 0.008, dirLenRel: 0.70, dirMinLenPx: 6, lineWidthRel: 0.25, stroke: '#ffffff', bombStroke: '#ef4444', fillCT: '#3b82f6', fillT: '#f59e0b', dirColor: 'auto' as 'auto' | string, }, nade: { stroke: '#111111', smokeFill: 'rgba(160,160,160,0.35)', fireFill: 'rgba(255,128,0,0.35)', heFill: 'rgba(90,160,90,0.9)', flashFill: 'rgba(255,255,255,0.95)', decoyFill: 'rgba(140,140,255,0.25)', teamStrokeCT: '#3b82f6', teamStrokeT: '#f59e0b', minRadiusPx: 6, }, death: { stroke: '#9ca3af', lineWidthPx: 2, sizePx: 10, }, trail: { maxPoints: 60, fadeMs: 1500, stroke: 'rgba(60,60,60,0.7)', widthPx: 2, } } /* ───────── helpers ───────── */ function contrastStroke(hex: string) { const h = hex.replace('#','') const r = parseInt(h.slice(0,2),16)/255 const g = parseInt(h.slice(2,4),16)/255 const b = parseInt(h.slice(4,6),16)/255 const toL = (c:number) => (c<=0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4)) const L = 0.2126*toL(r) + 0.7152*toL(g) + 0.0722*toL(b) return L > 0.6 ? '#111111' : '#ffffff' } function mapTeam(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 ?? '') } function detectHasBomb(src: any): boolean { const flags = [ 'hasBomb','has_bomb','bomb','c4','hasC4','carryingBomb','bombCarrier','isBombCarrier' ] for (const k of flags) { if (typeof src?.[k] === 'boolean') return !!src[k] if (typeof src?.[k] === 'string') { const s = String(src[k]).toLowerCase() if (s === 'true' || s === '1' || s === 'c4' || s.includes('bomb')) return true } } const arrays = [src?.weapons, src?.inventory, src?.items] for (const arr of arrays) { if (!arr) continue if (Array.isArray(arr)) { if (arr.some((w:any)=> typeof w === 'string' ? w.toLowerCase().includes('c4') || w.toLowerCase().includes('bomb') : (w?.name||w?.type||w?.weapon||'').toLowerCase().includes('c4') || (w?.name||w?.type||w?.weapon||'').toLowerCase().includes('bomb') )) return true } else if (typeof arr === 'object') { const vals = Object.values(arr) if (vals.some((w:any)=> typeof w === 'string' ? w.toLowerCase().includes('c4') || w.toLowerCase().includes('bomb') : (w?.name||w?.type||w?.weapon||'').toLowerCase().includes('c4') || (w?.name||w?.type||w?.weapon||'').toLowerCase().includes('bomb') )) return true } } return false } 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 metaUrl = makeWsUrl( process.env.NEXT_PUBLIC_CS2_META_WS_HOST, process.env.NEXT_PUBLIC_CS2_META_WS_PORT, process.env.NEXT_PUBLIC_CS2_META_WS_PATH, process.env.NEXT_PUBLIC_CS2_META_WS_SCHEME ) const posUrl = makeWsUrl( process.env.NEXT_PUBLIC_CS2_POS_WS_HOST, process.env.NEXT_PUBLIC_CS2_POS_WS_PORT, process.env.NEXT_PUBLIC_CS2_POS_WS_PATH, process.env.NEXT_PUBLIC_CS2_POS_WS_SCHEME ) const RAD2DEG = 180 / Math.PI const normalizeDeg = (d: number) => (d % 360 + 360) % 360 const parseVec3String = (str?: string) => { if (!str || typeof str !== 'string') return { x: 0, y: 0, z: 0 } const [x, y, z] = str.split(',').map(s => Number(s.trim())) return { x: Number.isFinite(x) ? x : 0, y: Number.isFinite(y) ? y : 0, z: Number.isFinite(z) ? z : 0 } } const asNum = (n: any, def=0) => { const v = Number(n); return Number.isFinite(v) ? v : def } /* ───────── types ───────── */ type PlayerState = { id: string name?: string | null team?: 'T' | 'CT' | string x: number y: number z: number yaw?: number | null alive?: boolean hasBomb?: boolean hp?: number | null armor?: number | null helmet?: boolean | null defuse?: boolean | null } type Grenade = { id: string kind: 'smoke' | 'molotov' | 'he' | 'flash' | 'decoy' | 'unknown' x: number y: number z: number radius?: number | null expiresAt?: number | null team?: 'T' | 'CT' | string | null } type DeathMarker = { id: string; x: number; y: number; t: number } type Trail = { id: string; kind: Grenade['kind']; pts: {x:number,y:number}[]; lastSeen: number } type Overview = { posX: number; posY: number; scale: number; rotate?: number } type Mapper = (xw: number, yw: number) => { x: number; y: number } type WsStatus = 'idle'|'connecting'|'open'|'closed'|'error' /* ───────── Komponente ───────── */ export default function LiveRadar() { // WS-Status const [metaWsStatus, setMetaWsStatus] = useState('idle') const [posWsStatus, setPosWsStatus] = useState('idle') // Map const [activeMapKey, setActiveMapKey] = useState(null) // Spieler const playersRef = useRef>(new Map()) const [players, setPlayers] = useState([]) // Grenaden + Trails const grenadesRef = useRef>(new Map()) const [grenades, setGrenades] = useState([]) const trailsRef = useRef>(new Map()) const [trails, setTrails] = useState([]) // Death-Marker const deathMarkersRef = useRef([]) const [deathMarkers, setDeathMarkers] = useState([]) // Flush const flushTimer = useRef(null) const scheduleFlush = () => { if (flushTimer.current != null) return flushTimer.current = window.setTimeout(() => { flushTimer.current = null setPlayers(Array.from(playersRef.current.values())) setGrenades(Array.from(grenadesRef.current.values())) setTrails(Array.from(trailsRef.current.values())) setDeathMarkers([...deathMarkersRef.current]) }, 66) } useEffect(() => { return () => { if (flushTimer.current != null) { window.clearTimeout(flushTimer.current) flushTimer.current = null } } }, []) // Runden-/Map-Reset const clearRoundArtifacts = () => { deathMarkersRef.current = [] trailsRef.current.clear() grenadesRef.current.clear() scheduleFlush() } useEffect(() => { if (activeMapKey) clearRoundArtifacts() // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeMapKey]) /* ───────── Meta-Callbacks ───────── */ const handleMetaMap = (key: string) => setActiveMapKey(key.toLowerCase()) const handleMetaPlayersSnapshot = (list: Array<{ steamId: string|number; name?: string; team?: any }>) => { for (const p of list) { const id = String(p.steamId ?? '') if (!id) continue const old = playersRef.current.get(id) playersRef.current.set(id, { id, name: p.name ?? old?.name ?? null, team: mapTeam(p.team ?? old?.team), x: old?.x ?? 0, y: old?.y ?? 0, z: old?.z ?? 0, yaw: old?.yaw ?? null, alive: old?.alive, hasBomb: old?.hasBomb ?? false, }) } scheduleFlush() } const handleMetaPlayerJoin = (p: any) => { const id = String(p?.steamId ?? p?.id ?? p?.name ?? '') if (!id) return const old = playersRef.current.get(id) playersRef.current.set(id, { id, name: p?.name ?? old?.name ?? null, team: mapTeam(p?.team ?? old?.team), x: old?.x ?? 0, y: old?.y ?? 0, z: old?.z ?? 0, yaw: old?.yaw ?? null, alive: true, hasBomb: old?.hasBomb ?? false, }) scheduleFlush() } const handleMetaPlayerLeave = (steamId: string | number) => { const id = String(steamId) const old = playersRef.current.get(id) if (old) { playersRef.current.set(id, { ...old, alive: false }) scheduleFlush() } } /* ───────── Positions-Callbacks ───────── */ const addDeathMarker = (x:number, y:number, idHint?: string) => { deathMarkersRef.current.push({ id: idHint ?? `d#${Date.now()}`, x, y, t: Date.now() }) } const upsertPlayer = (e: any) => { const id = String(e.steamId ?? e.steam_id ?? e.userId ?? e.playerId ?? e.id ?? e.name ?? '') if (!id) return const pos = e.pos ?? e.position ?? e.location ?? e.coordinates const x = Number(e.x ?? pos?.x ?? (Array.isArray(pos) ? pos?.[0] : undefined)) const y = Number(e.y ?? pos?.y ?? (Array.isArray(pos) ? pos?.[1] : undefined)) const z = Number(e.z ?? pos?.z ?? (Array.isArray(pos) ? pos?.[2] : 0)) if (!Number.isFinite(x) || !Number.isFinite(y)) return const yaw = Number( e.yaw ?? e.viewAngle?.yaw ?? e.view?.yaw ?? e.aim?.yaw ?? e.ang?.y ?? e.angles?.y ?? e.rotation?.yaw ) const old = playersRef.current.get(id) const nextAlive = (e.alive !== undefined) ? !!e.alive : old?.alive const hp = Number( e.hp ?? e.health ?? e.state?.health ) const armor = Number( e.armor ?? e.state?.armor ) const helmet = Boolean( e.helmet ?? e.hasHelmet ?? e.state?.helmet ) const defuse = Boolean( e.defuse ?? e.hasDefuse ?? e.hasDefuser ?? e.state?.defusekit ) const hasBomb = detectHasBomb(e) || old?.hasBomb if (old?.alive !== false && nextAlive === false) addDeathMarker(x, y, id) playersRef.current.set(id, { id, name: e.name ?? old?.name ?? null, team: mapTeam(e.team ?? old?.team), x, y, z, yaw: Number.isFinite(yaw) ? yaw : old?.yaw ?? null, alive: nextAlive, hasBomb: !!hasBomb, hp: Number.isFinite(hp) ? hp : old?.hp ?? null, armor: Number.isFinite(armor) ? armor : old?.armor ?? null, helmet: helmet ?? old?.helmet ?? null, defuse: defuse ?? old?.defuse ?? null, }) } const handlePlayersAll = (msg: any) => { const ap = msg?.allplayers if (!ap || typeof ap !== 'object') return let total = 0, aliveCount = 0 for (const key of Object.keys(ap)) { const p = ap[key] const pos = parseVec3String(p.position) const fwd = parseVec3String(p.forward) const yaw = normalizeDeg(Math.atan2(fwd.y, fwd.x) * RAD2DEG) const id = String(key) const old = playersRef.current.get(id) const isAlive = p.state?.health > 0 || p.state?.health == null const hp = Number(p.state?.health) const armor = Number(p.state?.armor) const helmet = !!p.state?.helmet const defuse = !!p.state?.defusekit const hasBomb = detectHasBomb(p) || old?.hasBomb if ((old?.alive ?? true) && !isAlive) addDeathMarker(pos.x, pos.y, id) playersRef.current.set(id, { id, name: p.name ?? old?.name ?? null, team: mapTeam(p.team ?? old?.team), x: pos.x, y: pos.y, z: pos.z, yaw, alive: isAlive, hasBomb: !!hasBomb, hp: Number.isFinite(hp) ? hp : old?.hp ?? null, armor: Number.isFinite(armor) ? armor : old?.armor ?? null, helmet: helmet ?? old?.helmet ?? null, defuse: defuse ?? old?.defuse ?? null, }) total++ if (isAlive) aliveCount++ } if (total > 0 && aliveCount === total && (deathMarkersRef.current.length > 0 || trailsRef.current.size > 0 || grenadesRef.current.size > 0)) { clearRoundArtifacts() } scheduleFlush() } const normalizeGrenades = (raw: any): Grenade[] => { if (!raw) return [] const pickTeam = (t: any): 'T' | 'CT' | string | null => { const s = mapTeam(t) return s === 'T' || s === 'CT' ? s : (typeof t === 'string' ? t : null) } if (Array.isArray(raw)) { return raw.map((g: any, i: number) => { const pos = g.pos ?? g.position ?? g.location ?? {} const xyz = Array.isArray(pos) ? { x: pos[0], y: pos[1], z: pos[2] } : typeof pos === 'string' ? parseVec3String(pos) : pos return { id: String(g.id ?? `${g.type ?? 'nade'}#${i}`), kind: (String(g.type ?? g.kind ?? 'unknown').toLowerCase() as Grenade['kind']), x: asNum(g.x ?? xyz?.x), y: asNum(g.y ?? xyz?.y), z: asNum(g.z ?? xyz?.z), radius: Number.isFinite(Number(g.radius)) ? Number(g.radius) : null, expiresAt: Number.isFinite(Number(g.expiresAt)) ? Number(g.expiresAt) : null, team: pickTeam(g.team ?? g.owner_team ?? g.side ?? null), } }) } const buckets: Record = { smoke: ['smokes', 'smoke', 'smokegrenade', 'smokegrenades'], molotov: ['molotov', 'molotovs', 'inferno', 'fire', 'incendiary'], he: ['he', 'hegrenade', 'hegrenades', 'explosive'], flash: ['flash', 'flashbang', 'flashbangs'], decoy: ['decoy', 'decoys'], } const out: Grenade[] = [] const push = (kind: Grenade['kind'], list: any) => { if (!list) return const arr = Array.isArray(list) ? list : Object.values(list) let i = 0 for (const g of arr) { const pos = g?.pos ?? g?.position ?? g?.location const xyz = Array.isArray(pos) ? { x: pos[0], y: pos[1], z: pos[2] } : typeof pos === 'string' ? parseVec3String(pos) : (pos || { x: g?.x, y: g?.y, z: g?.z }) const id = String( g?.id ?? g?.entityid ?? g?.entindex ?? `${kind}#${asNum(xyz?.x)}:${asNum(xyz?.y)}:${asNum(xyz?.z)}:${i++}` ) out.push({ id, kind, x: asNum(xyz?.x), y: asNum(xyz?.y), z: asNum(xyz?.z), radius: Number.isFinite(Number(g?.radius)) ? Number(g?.radius) : null, expiresAt: Number.isFinite(Number(g?.expiresAt)) ? Number(g?.expiresAt) : null, team: pickTeam(g?.team ?? g?.owner_team ?? g?.side ?? null), }) } } for (const [kind, keys] of Object.entries(buckets)) { for (const k of keys) if ((raw as any)[k]) push(kind as Grenade['kind'], (raw as any)[k]) } if (out.length === 0 && typeof raw === 'object') { for (const [k, v] of Object.entries(raw)) { const kk = k.toLowerCase() const kind = kk.includes('smoke') ? 'smoke' : kk.includes('flash') ? 'flash' : kk.includes('molotov') || kk.includes('inferno') || kk.includes('fire') ? 'molotov' : kk.includes('decoy') ? 'decoy' : kk.includes('he') ? 'he' : 'unknown' push(kind as Grenade['kind'], v) } } return out } const handleGrenades = (g: any) => { const list = normalizeGrenades(g) // Trails updaten const seen = new Set() const now = Date.now() for (const it of list) { seen.add(it.id) const prev = trailsRef.current.get(it.id) ?? { id: it.id, kind: it.kind, pts: [], lastSeen: 0 } const last = prev.pts[prev.pts.length - 1] if (!last || Math.hypot(it.x - last.x, it.y - last.y) > 1) { prev.pts.push({ x: it.x, y: it.y }) if (prev.pts.length > UI.trail.maxPoints) prev.pts = prev.pts.slice(-UI.trail.maxPoints) } prev.kind = it.kind prev.lastSeen = now trailsRef.current.set(it.id, prev) } // Trails ausdünnen for (const [id, tr] of trailsRef.current) { if (!seen.has(id) && now - tr.lastSeen > UI.trail.fadeMs) { trailsRef.current.delete(id) } } // aktuelle Nades übernehmen const next = new Map() for (const it of list) next.set(it.id, it) grenadesRef.current = next scheduleFlush() } // erster Flush useEffect(() => { if (!playersRef.current && !grenadesRef.current) return scheduleFlush() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) /* ───────── Overview + Radarbild ───────── */ const [overview, setOverview] = useState(null) const overviewCandidates = (mapKey: string) => { const base = mapKey return [ `/assets/resource/overviews/${base}.json`, `/assets/resource/overviews/${base}_lower.json`, `/assets/resource/overviews/${base}_v1.json`, `/assets/resource/overviews/${base}_v2.json`, `/assets/resource/overviews/${base}_s2.json`, ] } const parseOverviewJson = (j: any): Overview | null => { const posX = Number(j?.posX ?? j?.pos_x) const posY = Number(j?.posY ?? j?.pos_y) const scale = Number(j?.scale) const rotate = Number(j?.rotate ?? 0) if (![posX, posY, scale].every(Number.isFinite)) return null return { posX, posY, scale, rotate } } const parseValveKvOverview = (txt: string): Overview | null => { const clean = txt.replace(/\/\/.*$/gm, '') const pick = (k: string) => { const m = clean.match(new RegExp(`"\\s*${k}\\s*"\\s*"([^"]+)"`)); return m ? Number(m[1]) : NaN } const posX = pick('pos_x'), posY = pick('pos_y'), scale = pick('scale') const r = pick('rotate'); const rotate = Number.isFinite(r) ? r : 0 if (![posX, posY, scale].every(Number.isFinite)) return null return { posX, posY, scale, rotate } } useEffect(() => { let cancel = false ;(async () => { if (!activeMapKey) { setOverview(null); return } for (const path of overviewCandidates(activeMapKey)) { try { const res = await fetch(path, { cache: 'no-store' }) if (!res.ok) continue const txt = await res.text() let ov: Overview | null = null try { ov = parseOverviewJson(JSON.parse(txt)) } catch { ov = parseValveKvOverview(txt) } if (ov && !cancel) { setOverview(ov); return } } catch {} } if (!cancel) setOverview(null) })() return () => { cancel = true } }, [activeMapKey]) const { folderKey, imageCandidates } = useMemo(() => { if (!activeMapKey) return { folderKey: null as string | null, imageCandidates: [] as string[] } const short = activeMapKey.startsWith('de_') ? activeMapKey.slice(3) : activeMapKey const base = `/assets/img/radar/${activeMapKey}` return { folderKey: short, imageCandidates: [ `${base}/de_${short}_radar_psd.png`, `${base}/de_${short}_lower_radar_psd.png`, `${base}/de_${short}_v1_radar_psd.png`, `${base}/de_${short}_radar.png`, ], } }, [activeMapKey]) const [srcIdx, setSrcIdx] = useState(0) useEffect(() => { setSrcIdx(0) }, [folderKey]) const currentSrc = imageCandidates[srcIdx] const [imgSize, setImgSize] = useState<{ w: number; h: number } | null>(null) /* ───────── Welt → Pixel ───────── */ const worldToPx: Mapper = useMemo(() => { if (!imgSize || !overview) { return (xw, yw) => { if (!imgSize) return { x: 0, y: 0 } const R = 4096 const span = Math.min(imgSize.w, imgSize.h) const k = span / (2 * R) return { x: imgSize.w / 2 + xw * k, y: imgSize.h / 2 - yw * k } } } const { posX, posY, scale, rotate = 0 } = overview const w = imgSize.w, h = imgSize.h const cx = w / 2, cy = h / 2 const bases: ((xw: number, yw: number) => { x: number; y: number })[] = [ (xw, yw) => ({ x: (xw - posX) / scale, y: (posY - yw) / scale }), (xw, yw) => ({ x: (posX - xw) / scale, y: (posY - yw) / scale }), (xw, yw) => ({ x: (xw - posX) / scale, y: (yw - posY) / scale }), (xw, yw) => ({ x: (posX - xw) / scale, y: (yw - posY) / scale }), ] const rotSigns = [1, -1] const candidates: Mapper[] = [] for (const base of bases) { for (const s of rotSigns) { const theta = (rotate * s * Math.PI) / 180 candidates.push((xw, yw) => { const p = base(xw, yw) if (rotate === 0) return p const dx = p.x - cx, dy = p.y - cy const xr = dx * Math.cos(theta) - dy * Math.sin(theta) const yr = dx * Math.sin(theta) + dy * Math.cos(theta) return { x: cx + xr, y: cy + yr } }) } } if (players.length === 0) return candidates[0] const score = (mapFn: Mapper) => { let inside = 0 for (const p of players) { const { x, y } = mapFn(p.x, p.y) if (Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0 && x <= w && y <= h) inside++ } return inside } let best = candidates[0], bestScore = -1 for (const m of candidates) { const s = score(m) if (s > bestScore) { bestScore = s; best = m } } return best }, [imgSize, overview, players]) const unitsToPx = useMemo(() => { if (!imgSize) return (u: number) => u if (overview) { const scale = overview.scale return (u: number) => u / scale } const R = 4096 const span = Math.min(imgSize.w, imgSize.h) const k = span / (2 * R) return (u: number) => u * k }, [imgSize, overview]) /* ───────── Status-Badge ───────── */ const WsDot = ({ status, label }: { status: WsStatus, label: string }) => { const color = status === 'open' ? 'bg-green-500' : status === 'connecting' ? 'bg-amber-500' : status === 'error' ? 'bg-red-500' : 'bg-neutral-400' const txt = status === 'open' ? 'verbunden' : status === 'connecting' ? 'verbinde…' : status === 'error' ? 'Fehler' : status === 'closed' ? 'getrennt' : '—' return ( {label} {txt} ) } /* ───────── Render (Fix A: reines Flex-Layout) ───────── */ return (
{/* Header */}

Live Radar

{activeMapKey ? activeMapKey.replace(/^de_/,'').replace(/_/g,' ').toUpperCase() : '—'}
{/* Unsichtbare WS-Clients */} setActiveMapKey(k.toLowerCase())} onPlayersSnapshot={handleMetaPlayersSnapshot} onPlayerJoin={handleMetaPlayerJoin} onPlayerLeave={handleMetaPlayerLeave} /> setActiveMapKey(String(k).toLowerCase())} onPlayerUpdate={(p)=> { upsertPlayer(p); scheduleFlush() }} onPlayersAll={(m)=> { handlePlayersAll(m); scheduleFlush() }} onGrenades={(g)=> { handleGrenades(g); scheduleFlush() }} /> {/* Inhalt: 3-Spalten-Layout (T | Radar | CT) */}
{!activeMapKey ? (
Keine Map erkannt.
) : (
{/* Left: T */} p.team === 'T') .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 })) } /> {/* Center: Radar */}
{currentSrc ? (
{/* Bild füllt Container (Letterboxing via object-contain) */} {activeMapKey { const img = e.currentTarget setImgSize({ w: img.naturalWidth, h: img.naturalHeight }) }} onError={() => { if (srcIdx < imageCandidates.length - 1) setSrcIdx(i => i + 1) }} /> {/* Overlay skaliert deckungsgleich zum Bild */} {imgSize && ( {/* Trails */} {trails.map(tr => { const pts = tr.pts.map(p => { const q = worldToPx(p.x, p.y) return `${q.x},${q.y}` }).join(' ') if (!pts) return null return ( ) })} {/* Grenades */} {grenades.map((g) => { const P = worldToPx(g.x, g.y) const defR = g.kind === 'smoke' ? 150 : g.kind === 'molotov'? 120 : g.kind === 'he' ? 40 : g.kind === 'flash' ? 36 : g.kind === 'decoy' ? 80 : 60 const rPx = Math.max(UI.nade.minRadiusPx, unitsToPx(g.radius ?? defR)) const stroke = g.team === 'CT' ? UI.nade.teamStrokeCT : g.team === 'T' ? UI.nade.teamStrokeT : UI.nade.stroke const sw = Math.max(1, Math.sqrt(rPx) * 0.6) if (!Number.isFinite(P.x) || !Number.isFinite(P.y)) return null if (g.kind === 'smoke') { return } if (g.kind === 'molotov') { return } if (g.kind === 'decoy') { return } if (g.kind === 'flash') { return ( ) } return })} {/* Spieler */} {players .filter(p => (p.team === 'CT' || p.team === 'T') && p.alive !== false) .map((p) => { const A = worldToPx(p.x, p.y) const base = Math.min(imgSize.w, imgSize.h) const r = Math.max(UI.player.minRadiusPx, base * UI.player.radiusRel) const dirLenPx = Math.max(UI.player.dirMinLenPx, r * UI.player.dirLenRel) const stroke = p.hasBomb ? UI.player.bombStroke : UI.player.stroke const strokeW = Math.max(1, r * UI.player.lineWidthRel) const fillColor = p.team === 'CT' ? UI.player.fillCT : UI.player.fillT const dirColor = UI.player.dirColor === 'auto' ? contrastStroke(fillColor) : UI.player.dirColor let dxp = 0, dyp = 0 if (Number.isFinite(p.yaw as number)) { const yawRad = (Number(p.yaw) * Math.PI) / 180 const STEP_WORLD = 200 const B = worldToPx( p.x + Math.cos(yawRad) * STEP_WORLD, p.y + Math.sin(yawRad) * STEP_WORLD ) dxp = B.x - A.x dyp = B.y - A.y const cur = Math.hypot(dxp, dyp) || 1 dxp *= dirLenPx / cur dyp *= dirLenPx / cur } return ( {Number.isFinite(p.yaw as number) && ( )} ) })} {/* Death-Marker */} {deathMarkers.map(dm => { const P = worldToPx(dm.x, dm.y) const s = UI.death.sizePx return ( ) })} )}
) : (
Keine Radar-Grafik gefunden.
)}
{/* Right: CT */} p.team === 'CT') .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 })) } />
)}
) }