updated player angle
This commit is contained in:
parent
c10bd74b70
commit
02b2a8efc8
@ -4,28 +4,69 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import LoadingSpinner from './LoadingSpinner'
|
import LoadingSpinner from './LoadingSpinner'
|
||||||
|
|
||||||
const RAD2DEG = 180 / Math.PI;
|
// ---------- Konfiguration (UI & Verhalten) ----------
|
||||||
function normalizeDeg(d: number) { d = d % 360; return d < 0 ? d + 360 : d; }
|
const UI = {
|
||||||
|
player: {
|
||||||
|
minRadiusPx: 4,
|
||||||
|
radiusRel: 0.008, // Radius relativ zur kleineren Bildkante
|
||||||
|
dirLenRel: 0.70, // Anteil des Radius, Linie bleibt im Kreis
|
||||||
|
dirMinLenPx: 6,
|
||||||
|
lineWidthRel: 0.25, // Linienbreite relativ zum Radius
|
||||||
|
stroke: '#ffffff',
|
||||||
|
fillCT: '#3b82f6',
|
||||||
|
fillT: '#f59e0b',
|
||||||
|
// 'auto' = automatisch kontrastierend zum Kreis, sonst fixe Farbe wie '#fff'
|
||||||
|
dirColor: 'auto' as 'auto' | string,
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
smokeIconScale: 1.6,
|
||||||
|
fireIconScale: 1.45,
|
||||||
|
smokeOpacity: 0.95,
|
||||||
|
smokeFillOpacity: 0.70,
|
||||||
|
fireOpacity: 1,
|
||||||
|
smokeFadeMs: 3000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RAD2DEG = 180 / Math.PI
|
||||||
|
|
||||||
|
function normalizeDeg(d: number) {
|
||||||
|
d = d % 360
|
||||||
|
return d < 0 ? d + 360 : d
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortestAngleDeltaDeg(a: number, b: number) {
|
||||||
|
// delta in [-180, 180)
|
||||||
|
return ((b - a + 540) % 360) - 180
|
||||||
|
}
|
||||||
|
|
||||||
|
function lerpAngleDeg(a: number, b: number, t: number) {
|
||||||
|
return normalizeDeg(a + shortestAngleDeltaDeg(a, b) * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yaw-Quelle parsen (Server liefert Grad)
|
||||||
function toYawDegMaybe(raw: any): number | null {
|
function toYawDegMaybe(raw: any): number | null {
|
||||||
const v = Number(raw);
|
const v = Number(raw)
|
||||||
if (!Number.isFinite(v)) return null;
|
return Number.isFinite(v) ? v : null
|
||||||
if (Math.abs(v) <= 2 * Math.PI + 1e-3) return v * RAD2DEG; // radians
|
|
||||||
return v; // degrees
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback, wenn keine yaw übermittelt wird
|
||||||
function deriveYawDeg(raw: any, prev: PlayerState | undefined, x: number, y: number): number {
|
function deriveYawDeg(raw: any, prev: PlayerState | undefined, x: number, y: number): number {
|
||||||
const fromRaw = toYawDegMaybe(raw);
|
const fromRaw = toYawDegMaybe(raw)
|
||||||
if (fromRaw != null && Math.abs(fromRaw) > 1e-6 && Math.abs(fromRaw) < 1e6) return normalizeDeg(fromRaw);
|
if (fromRaw != null) return normalizeDeg(fromRaw)
|
||||||
|
|
||||||
if (prev) {
|
if (prev) {
|
||||||
const dx = x - prev.x, dy = y - prev.y;
|
const dx = x - prev.x, dy = y - prev.y
|
||||||
if (Math.hypot(dx, dy) > 1) return normalizeDeg(Math.atan2(dy, dx) * RAD2DEG);
|
if (Math.hypot(dx, dy) > 1) return normalizeDeg(Math.atan2(dy, dx) * RAD2DEG)
|
||||||
if (Number.isFinite(prev.yaw)) return prev.yaw;
|
if (Number.isFinite(prev.yaw)) return prev.yaw
|
||||||
}
|
}
|
||||||
return 0;
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapTeam(t: any): 'T' | 'CT' | string {
|
function mapTeam(t: any): 'T' | 'CT' | string {
|
||||||
if (t === 2 || t === 'T' || t === 't') return 'T';
|
if (t === 2 || t === 'T' || t === 't') return 'T'
|
||||||
if (t === 3 || t === 'CT' || t === 'ct') return 'CT';
|
if (t === 3 || t === 'CT' || t === 'ct') return 'CT'
|
||||||
return String(t ?? '');
|
return String(t ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = { matchId: string }
|
type Props = { matchId: string }
|
||||||
@ -62,16 +103,15 @@ type Effect = {
|
|||||||
z: number
|
z: number
|
||||||
startMs: number
|
startMs: number
|
||||||
ttlMs: number
|
ttlMs: number
|
||||||
// neu:
|
|
||||||
ending?: boolean
|
ending?: boolean
|
||||||
fadeUntil?: number
|
fadeUntil?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Nade-Pfade -------------------------------------------------------------
|
// --- Nade-Pfade --------------------------------------------------------------
|
||||||
type NadePoint = { x: number; y: number; z?: number; t?: number; s?: number }
|
type NadePoint = { x: number; y: number; z?: number; t?: number; s?: number }
|
||||||
type NadePath = {
|
type NadePath = {
|
||||||
id: string
|
id: string
|
||||||
kind: string // 'smoke' | 'flash' | 'he' | 'molotov' | ...
|
kind: string
|
||||||
points: NadePoint[]
|
points: NadePoint[]
|
||||||
startedMs: number
|
startedMs: number
|
||||||
endedMs?: number
|
endedMs?: number
|
||||||
@ -81,14 +121,24 @@ const NADE_PATH_TTL = 6000 // Pfad noch 6s nach Detonation zeigen
|
|||||||
|
|
||||||
function nadeColor(kind: string) {
|
function nadeColor(kind: string) {
|
||||||
const k = String(kind || '').toLowerCase()
|
const k = String(kind || '').toLowerCase()
|
||||||
if (k.includes('smoke')) return '#94a3b8' // smoke: slate-grau
|
if (k.includes('smoke')) return '#94a3b8'
|
||||||
if (k.includes('flash')) return '#fbbf24' // flash: gelb
|
if (k.includes('flash')) return '#fbbf24'
|
||||||
if (k.includes('molotov') || k.includes('incen') || k === 'fire') return '#f97316' // molly
|
if (k.includes('molotov') || k.includes('incen') || k === 'fire') return '#f97316'
|
||||||
if (k.includes('he') || k.includes('frag')) return '#ef4444' // HE
|
if (k.includes('he') || k.includes('frag')) return '#ef4444'
|
||||||
if (k.includes('decoy')) return '#22c55e' // decoy
|
if (k.includes('decoy')) return '#22c55e'
|
||||||
return '#a3a3a3'
|
return '#a3a3a3'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
export default function LiveRadar({ matchId }: Props) {
|
export default function LiveRadar({ matchId }: Props) {
|
||||||
// ---- MapVote (Backup)
|
// ---- MapVote (Backup)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@ -116,29 +166,32 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// SMOKE + FIRE:
|
// ---- Effekt-SVG-Paths
|
||||||
const SMOKE_PATH = "M32 400C32 479.5 96.5 544 176 544L480 544C550.7 544 608 486.7 608 416C608 364.4 577.5 319.9 533.5 299.7C540.2 286.6 544 271.7 544 256C544 203 501 160 448 160C430.3 160 413.8 164.8 399.6 173.1C375.5 127.3 327.4 96 272 96C192.5 96 128 160.5 128 240C128 248 128.7 255.9 129.9 263.5C73 282.7 32 336.6 32 400z";
|
const SMOKE_PATH = "M32 400C32 479.5 96.5 544 176 544L480 544C550.7 544 608 486.7 608 416C608 364.4 577.5 319.9 533.5 299.7C540.2 286.6 544 271.7 544 256C544 203 501 160 448 160C430.3 160 413.8 164.8 399.6 173.1C375.5 127.3 327.4 96 272 96C192.5 96 128 160.5 128 240C128 248 128.7 255.9 129.9 263.5C73 282.7 32 336.6 32 400z"
|
||||||
const FIRE_PATH = "M256.5 37.6C265.8 29.8 279.5 30.1 288.4 38.5C300.7 50.1 311.7 62.9 322.3 75.9C335.8 92.4 352 114.2 367.6 140.1C372.8 133.3 377.6 127.3 381.8 122.2C382.9 120.9 384 119.5 385.1 118.1C393 108.3 402.8 96 415.9 96C429.3 96 438.7 107.9 446.7 118.1C448 119.8 449.3 121.4 450.6 122.9C460.9 135.3 474.6 153.2 488.3 175.3C515.5 219.2 543.9 281.7 543.9 351.9C543.9 475.6 443.6 575.9 319.9 575.9C196.2 575.9 96 475.7 96 352C96 260.9 137.1 182 176.5 127C196.4 99.3 216.2 77.1 231.1 61.9C239.3 53.5 247.6 45.2 256.6 37.7zM321.7 480C347 480 369.4 473 390.5 459C432.6 429.6 443.9 370.8 418.6 324.6C414.1 315.6 402.6 315 396.1 322.6L370.9 351.9C364.3 359.5 352.4 359.3 346.2 351.4C328.9 329.3 297.1 289 280.9 268.4C275.5 261.5 265.7 260.4 259.4 266.5C241.1 284.3 207.9 323.3 207.9 370.8C207.9 439.4 258.5 480 321.6 480z";
|
const FIRE_PATH = "M256.5 37.6C265.8 29.8 279.5 30.1 288.4 38.5C300.7 50.1 311.7 62.9 322.3 75.9C335.8 92.4 352 114.2 367.6 140.1C372.8 133.3 377.6 127.3 381.8 122.2C382.9 120.9 384 119.5 385.1 118.1C393 108.3 402.8 96 415.9 96C429.3 96 438.7 107.9 446.7 118.1C448 119.8 449.3 121.4 450.6 122.9C460.9 135.3 474.6 153.2 488.3 175.3C515.5 219.2 543.9 281.7 543.9 351.9C543.9 475.6 443.6 575.9 319.9 575.9C196.2 575.9 96 475.7 96 352C96 260.9 137.1 182 176.5 127C196.4 99.3 216.2 77.1 231.1 61.9C239.3 53.5 247.6 45.2 256.6 37.7zM321.7 480C347 480 369.4 473 390.5 459C432.6 429.6 443.9 370.8 418.6 324.6C414.1 315.6 402.6 315 396.1 322.6L370.9 351.9C364.3 359.5 352.4 359.3 346.2 351.4C328.9 329.3 297.1 289 280.9 268.4C275.5 261.5 265.7 260.4 259.4 266.5C241.1 284.3 207.9 323.3 207.9 370.8C207.9 439.4 258.5 480 321.6 480z"
|
||||||
|
|
||||||
const SMOKE_FADE_MS = 3000; // 3s ausfaden
|
// ---- Nade-Pfade & Effekte -------------------------------------------------
|
||||||
|
|
||||||
// NEU – Größe & Opazität der Icons
|
|
||||||
const SMOKE_ICON_SCALE = 1.6; // >1 = größer
|
|
||||||
const FIRE_ICON_SCALE = 1.45;
|
|
||||||
|
|
||||||
const SMOKE_GROUP_OPACITY = 0.95; // weniger transparent
|
|
||||||
const SMOKE_FILL_OPACITY = 0.70;
|
|
||||||
|
|
||||||
const FIRE_GROUP_OPACITY = 1.0; // fast/komplett deckend
|
|
||||||
|
|
||||||
// ---- Nade-Pfade ------------------------------------------------------------
|
|
||||||
const nadePathsRef = useRef<Map<string, NadePath>>(new Map())
|
const nadePathsRef = useRef<Map<string, NadePath>>(new Map())
|
||||||
const [nadePaths, setNadePaths] = useState<NadePath[]>([])
|
const [nadePaths, setNadePaths] = useState<NadePath[]>([])
|
||||||
const syncNadePaths = () => setNadePaths(Array.from(nadePathsRef.current.values()))
|
const syncNadePaths = () => setNadePaths(Array.from(nadePathsRef.current.values()))
|
||||||
|
|
||||||
|
const effectsRef = useRef<Map<string, Effect>>(new Map())
|
||||||
|
const [effects, setEffects] = useState<Effect[]>([])
|
||||||
|
const syncEffects = () => setEffects(Array.from(effectsRef.current.values()))
|
||||||
|
|
||||||
|
// ---- Spieler (throttled) --------------------------------------------------
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 1) MapVote laden
|
// ---- MapVote laden --------------------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancel = false
|
let cancel = false
|
||||||
;(async () => {
|
;(async () => {
|
||||||
@ -161,37 +214,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
return () => { cancel = true }
|
return () => { cancel = true }
|
||||||
}, [matchId])
|
}, [matchId])
|
||||||
|
|
||||||
// ---- Spieler-Overlay (ge-throttled)
|
// ---- Aktive Map bestimmen -------------------------------------------------
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Effekte-State (seltenere Updates, kein Throttle nötig – aber Map + Sync)
|
|
||||||
const effectsRef = useRef<Map<string, Effect>>(new Map())
|
|
||||||
const [effects, setEffects] = useState<Effect[]>([])
|
|
||||||
const syncEffects = () => setEffects(Array.from(effectsRef.current.values()))
|
|
||||||
|
|
||||||
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] : 0))
|
|
||||||
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, yaw, alive: p.alive }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Aktive Map (WS hat Vorrang)
|
|
||||||
const voteMapKey = useMemo(() => {
|
const voteMapKey = useMemo(() => {
|
||||||
const chosen = (voteData?.steps ?? []).filter(s => (s.action === 'pick' || s.action === 'decider') && s.map)
|
const chosen = (voteData?.steps ?? []).filter(s => (s.action === 'pick' || s.action === 'decider') && s.map)
|
||||||
return chosen[0]?.map ?? null
|
return chosen[0]?.map ?? null
|
||||||
@ -202,7 +225,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
if (!activeMapKey && voteMapKey) setActiveMapKey(voteMapKey)
|
if (!activeMapKey && voteMapKey) setActiveMapKey(voteMapKey)
|
||||||
}, [voteMapKey, activeMapKey])
|
}, [voteMapKey, activeMapKey])
|
||||||
|
|
||||||
// ---- 2) WS verbinden: Map + Players + Effekte
|
// ---- WebSocket verbinden --------------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
const explicit = process.env.NEXT_PUBLIC_CS2_WS_URL
|
const explicit = process.env.NEXT_PUBLIC_CS2_WS_URL
|
||||||
@ -216,14 +239,14 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
let ws: WebSocket | null = null
|
let ws: WebSocket | null = null
|
||||||
let retry: number | null = null
|
let retry: number | null = null
|
||||||
|
|
||||||
// Hilfsfunktionen für Effekte
|
// Effekte hinzufügen/entfernen
|
||||||
const addEffect = (type: EffectType, m: any) => {
|
const addEffect = (type: EffectType, m: any) => {
|
||||||
const pos = m.pos ?? m.position
|
const pos = m.pos ?? m.position
|
||||||
const x = Number(m.x ?? pos?.x), y = Number(m.y ?? pos?.y), z = Number(m.z ?? pos?.z ?? 0)
|
const x = Number(m.x ?? pos?.x), y = Number(m.y ?? pos?.y), z = Number(m.z ?? pos?.z ?? 0)
|
||||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return
|
||||||
const serverId = m.id ?? m.entityId ?? m.grenadeId ?? m.guid
|
const serverId = m.id ?? m.entityId ?? m.grenadeId ?? m.guid
|
||||||
const id = String(serverId ?? `${type}:${Math.round(x)}:${Math.round(y)}:${Math.round(m.t ?? Date.now())}`)
|
const id = String(serverId ?? `${type}:${Math.round(x)}:${Math.round(y)}:${Math.round(m.t ?? Date.now())}`)
|
||||||
const ttlMs = type === 'smoke' ? 19000 : 7000 // Smoke ~19s, Molotov ~7s (Fallback)
|
const ttlMs = type === 'smoke' ? 19000 : 7000
|
||||||
effectsRef.current.set(id, { id, type, x, y, z, startMs: Date.now(), ttlMs })
|
effectsRef.current.set(id, { id, type, x, y, z, startMs: Date.now(), ttlMs })
|
||||||
syncEffects()
|
syncEffects()
|
||||||
}
|
}
|
||||||
@ -235,12 +258,10 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
const e = effectsRef.current.get(key)
|
const e = effectsRef.current.get(key)
|
||||||
if (e) {
|
if (e) {
|
||||||
if (type === 'smoke') {
|
if (type === 'smoke') {
|
||||||
// statt löschen: Fade markieren
|
|
||||||
e.ending = true
|
e.ending = true
|
||||||
e.fadeUntil = Date.now() + SMOKE_FADE_MS
|
e.fadeUntil = Date.now() + UI.effects.smokeFadeMs
|
||||||
effectsRef.current.set(key, e)
|
effectsRef.current.set(key, e)
|
||||||
} else {
|
} else {
|
||||||
// fire weiterhin sofort entfernen
|
|
||||||
effectsRef.current.delete(key)
|
effectsRef.current.delete(key)
|
||||||
}
|
}
|
||||||
syncEffects()
|
syncEffects()
|
||||||
@ -248,9 +269,8 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: nächster gleicher Typ in der Nähe
|
// Fallback: nächster gleicher Typ in der Nähe (ohne serverId)
|
||||||
const pos = m.pos ?? m.position
|
const pos = m.pos ?? m.position
|
||||||
|
|
||||||
const x = Number(m.x ?? pos?.x), y = Number(m.y ?? pos?.y)
|
const x = Number(m.x ?? pos?.x), y = Number(m.y ?? pos?.y)
|
||||||
if (Number.isFinite(x) && Number.isFinite(y)) {
|
if (Number.isFinite(x) && Number.isFinite(y)) {
|
||||||
let bestKey: string | null = null, bestD = Infinity
|
let bestKey: string | null = null, bestD = Infinity
|
||||||
@ -263,7 +283,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
if (type === 'smoke') {
|
if (type === 'smoke') {
|
||||||
const e = effectsRef.current.get(bestKey)!
|
const e = effectsRef.current.get(bestKey)!
|
||||||
e.ending = true
|
e.ending = true
|
||||||
e.fadeUntil = Date.now() + SMOKE_FADE_MS
|
e.fadeUntil = Date.now() + UI.effects.smokeFadeMs
|
||||||
effectsRef.current.set(bestKey, e)
|
effectsRef.current.set(bestKey, e)
|
||||||
} else {
|
} else {
|
||||||
effectsRef.current.delete(bestKey)
|
effectsRef.current.delete(bestKey)
|
||||||
@ -273,12 +293,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const connect = () => {
|
// Nade-Trace verarbeiten
|
||||||
if (!alive) return
|
|
||||||
setWsStatus('connecting')
|
|
||||||
ws = new WebSocket(url)
|
|
||||||
|
|
||||||
// ---- helper: trace-objekt in NadePath mergen + Effekte erzeugen/entfernen
|
|
||||||
const upsertNadeTrace = (tr: any) => {
|
const upsertNadeTrace = (tr: any) => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const id = String(tr?.id ?? tr?.guid ?? tr?.entityId ?? tr?.grenadeId ?? '')
|
const id = String(tr?.id ?? tr?.guid ?? tr?.entityId ?? tr?.grenadeId ?? '')
|
||||||
@ -300,7 +315,6 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
const last = cur.points[cur.points.length - 1]
|
const last = cur.points[cur.points.length - 1]
|
||||||
const seg = Number.isFinite(s0) ? s0 : (last?.s ?? 0)
|
const seg = Number.isFinite(s0) ? s0 : (last?.s ?? 0)
|
||||||
|
|
||||||
// einfache Dedupe (Distanz + Zeit)
|
|
||||||
const tooClose = last && Math.hypot(last.x - px, last.y - py) <= 1
|
const tooClose = last && Math.hypot(last.x - px, last.y - py) <= 1
|
||||||
const sameTime = last && (last.t ?? 0) === t
|
const sameTime = last && (last.t ?? 0) === t
|
||||||
if (tooClose && sameTime) return
|
if (tooClose && sameTime) return
|
||||||
@ -309,18 +323,15 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
nadePathsRef.current.set(id, cur)
|
nadePathsRef.current.set(id, cur)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Punkte anhängen (einzeln oder als Liste)
|
|
||||||
if (Array.isArray(tr?.points)) tr.points.forEach(addPoint)
|
if (Array.isArray(tr?.points)) tr.points.forEach(addPoint)
|
||||||
else if (tr?.pos || (tr?.x != null && tr?.y != null)) addPoint(tr.pos ?? tr)
|
else if (tr?.pos || (tr?.x != null && tr?.y != null)) addPoint(tr.pos ?? tr)
|
||||||
|
|
||||||
// Status auswerten
|
|
||||||
const state = String(tr?.state ?? tr?.sub ?? tr?.phase ?? '').toLowerCase()
|
const state = String(tr?.state ?? tr?.sub ?? tr?.phase ?? '').toLowerCase()
|
||||||
const markEnded = () => {
|
const markEnded = () => {
|
||||||
const cur = nadePathsRef.current.get(id)
|
const cur = nadePathsRef.current.get(id)
|
||||||
if (cur && !cur.endedMs) { cur.endedMs = now; nadePathsRef.current.set(id, cur) }
|
if (cur && !cur.endedMs) { cur.endedMs = now; nadePathsRef.current.set(id, cur) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detonation -> Pfad beenden + Effekt starten
|
|
||||||
if (state === 'detonate' || state === 'detonated' || tr?.detonate || tr?.done) {
|
if (state === 'detonate' || state === 'detonated' || tr?.detonate || tr?.done) {
|
||||||
markEnded()
|
markEnded()
|
||||||
const pos = tr?.pos ?? tr
|
const pos = tr?.pos ?? tr
|
||||||
@ -328,7 +339,6 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
if (kind.includes('molotov') || kind.includes('incen') || kind.includes('fire')) addEffect('fire', { ...pos, id })
|
if (kind.includes('molotov') || kind.includes('incen') || kind.includes('fire')) addEffect('fire', { ...pos, id })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ende (explizit) -> Effekt entfernen (Smoke mit Fade, Fire sofort wie gehabt)
|
|
||||||
if (state === 'end' || state === 'expired') {
|
if (state === 'end' || state === 'expired') {
|
||||||
if (kind.includes('smoke')) removeEffect('smoke', { id })
|
if (kind.includes('smoke')) removeEffect('smoke', { id })
|
||||||
if (kind.includes('molotov') || kind.includes('incen') || kind.includes('fire')) removeEffect('fire', { id })
|
if (kind.includes('molotov') || kind.includes('incen') || kind.includes('fire')) removeEffect('fire', { id })
|
||||||
@ -336,25 +346,28 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onopen = () => { setWsStatus('open'); console.info('[cs2-ws] connected →', url) }
|
const connect = () => {
|
||||||
|
if (!alive) return
|
||||||
|
setWsStatus('connecting')
|
||||||
|
ws = new WebSocket(url)
|
||||||
|
|
||||||
|
ws.onopen = () => setWsStatus('open')
|
||||||
|
|
||||||
ws.onmessage = (ev) => {
|
ws.onmessage = (ev) => {
|
||||||
let msg: any = null
|
let msg: any = null
|
||||||
try { msg = JSON.parse(ev.data as string) } catch {}
|
try { msg = JSON.parse(ev.data as string) } catch {}
|
||||||
|
|
||||||
const tickTs = Number(msg?.t ?? Date.now())
|
|
||||||
|
|
||||||
const handleEvent = (e: any) => {
|
const handleEvent = (e: any) => {
|
||||||
if (!e) return
|
if (!e) return
|
||||||
|
|
||||||
// 1) Map-Event
|
// Map wechseln
|
||||||
if (e.type === 'map' || e.type === 'level' || e.map) {
|
if (e.type === 'map' || e.type === 'level' || e.map) {
|
||||||
const key = e.name || e.map || e.level
|
const key = e.name || e.map || e.level
|
||||||
if (typeof key === 'string' && key) setActiveMapKey(key)
|
if (typeof key === 'string' && key) setActiveMapKey(key)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Effekte -> NICHT als Player behandeln
|
// Effekte
|
||||||
if (e.type === 'smoke' || e.type === 'fire') {
|
if (e.type === 'smoke' || e.type === 'fire') {
|
||||||
const t = e.type as EffectType
|
const t = e.type as EffectType
|
||||||
if (e.state === 'start') addEffect(t, e)
|
if (e.state === 'start') addEffect(t, e)
|
||||||
@ -362,64 +375,12 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.type === 'nade') {
|
// Grenade-Traces
|
||||||
const id = String(e.id ?? '')
|
if (e.type === 'nade' || e.kind || e.nade || e.weapon) {
|
||||||
const kind = String(e.nade ?? e.weapon ?? '')
|
upsertNadeTrace(e)
|
||||||
if (!id) return
|
|
||||||
|
|
||||||
if (e.sub === 'thrown') {
|
|
||||||
const p = e.throwPos ?? e.pos ?? {}
|
|
||||||
const x = Number(p.x), y = Number(p.y), z = Number(p.z ?? 0)
|
|
||||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return
|
|
||||||
nadePathsRef.current.set(id, {
|
|
||||||
id, kind,
|
|
||||||
points: [{ x, y, z, t: e.t }],
|
|
||||||
startedMs: Date.now()
|
|
||||||
})
|
|
||||||
syncNadePaths()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.sub === 'path') {
|
// Spieler
|
||||||
const cur = nadePathsRef.current.get(id)
|
|
||||||
const pts = Array.isArray(e.points) ? e.points as NadePoint[] : []
|
|
||||||
if (!cur) {
|
|
||||||
if (pts.length) {
|
|
||||||
nadePathsRef.current.set(id, { id, kind, points: pts.slice(0, 200), startedMs: Date.now() })
|
|
||||||
syncNadePaths()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Punkte anhängen (ein wenig deduplizieren)
|
|
||||||
for (const pt of pts) {
|
|
||||||
const x = Number(pt.x), y = Number(pt.y)
|
|
||||||
if (!Number.isFinite(x) || !Number.isFinite(y)) continue
|
|
||||||
const last = cur.points[cur.points.length - 1]
|
|
||||||
if (!last || Math.hypot(last.x - x, last.y - y) > 1) cur.points.push({ x, y, z: pt.z, t: pt.t })
|
|
||||||
}
|
|
||||||
// Länge begrenzen
|
|
||||||
if (cur.points.length > 300) cur.points.splice(0, cur.points.length - 300)
|
|
||||||
nadePathsRef.current.set(id, cur)
|
|
||||||
syncNadePaths()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.sub === 'detonate') {
|
|
||||||
const cur = nadePathsRef.current.get(id)
|
|
||||||
if (cur && !cur.endedMs) {
|
|
||||||
// Endpunkt (falls vorhanden) hinzufügen
|
|
||||||
const p = e.pos ?? {}
|
|
||||||
const x = Number(p.x), y = Number(p.y)
|
|
||||||
if (Number.isFinite(x) && Number.isFinite(y)) cur.points.push({ x, y, z: Number(p.z ?? 0), t: e.t })
|
|
||||||
cur.endedMs = Date.now()
|
|
||||||
nadePathsRef.current.set(id, cur)
|
|
||||||
syncNadePaths()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Player-ähnliche Events
|
|
||||||
if (!(e.steamId || e.steam_id || e.pos || e.position)) return
|
if (!(e.steamId || e.steam_id || e.pos || e.position)) return
|
||||||
|
|
||||||
const id = String(e.steamId ?? e.steam_id ?? e.userId ?? e.playerId ?? e.id ?? e.name ?? '')
|
const id = String(e.steamId ?? e.steam_id ?? e.userId ?? e.playerId ?? e.id ?? e.name ?? '')
|
||||||
@ -432,8 +393,20 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return
|
||||||
|
|
||||||
const prev = playersRef.current.get(id)
|
const prev = playersRef.current.get(id)
|
||||||
const rawYaw = e.yaw ?? e.ang?.y ?? e.angles?.y ?? e.rotation?.yaw ?? e.view?.yaw
|
|
||||||
const yawDeg = deriveYawDeg(rawYaw, prev, x, y)
|
const rawYaw =
|
||||||
|
e.viewAngle?.yaw ??
|
||||||
|
e.view?.yaw ??
|
||||||
|
e.aim?.yaw ??
|
||||||
|
e.yaw ??
|
||||||
|
e.ang?.y ??
|
||||||
|
e.angles?.y ??
|
||||||
|
e.rotation?.yaw
|
||||||
|
|
||||||
|
const yawDegRaw = deriveYawDeg(rawYaw, prev, x, y)
|
||||||
|
// Sanftes Smoothing, damit kleine Änderungen sichtbar & stabil sind
|
||||||
|
const YAW_SMOOTH = 0.01
|
||||||
|
const yawDeg = prev ? lerpAngleDeg(prev.yaw, yawDegRaw, YAW_SMOOTH) : yawDegRaw
|
||||||
|
|
||||||
const p: PlayerState = {
|
const p: PlayerState = {
|
||||||
id,
|
id,
|
||||||
@ -454,39 +427,10 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
handleEvent(msg)
|
handleEvent(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg?.nades?.trace && Array.isArray(msg.nades.trace)) {
|
|
||||||
for (const tr of msg.nades.trace) upsertNadeTrace(tr)
|
|
||||||
syncNadePaths() // sichtbaren State aktualisieren
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(msg?.nades?.active)) {
|
|
||||||
for (const a of msg.nades.active) {
|
|
||||||
const id = String(a?.id ?? '')
|
|
||||||
if (!id) continue
|
|
||||||
const kind = String(a?.nade ?? a?.kind ?? '').toLowerCase()
|
|
||||||
const p = a.pos ?? {}
|
|
||||||
const x = Number(p.x), y = Number(p.y), z = Number(p.z ?? 0)
|
|
||||||
if (!Number.isFinite(x) || !Number.isFinite(y)) continue
|
|
||||||
|
|
||||||
let cur = nadePathsRef.current.get(id)
|
|
||||||
if (!cur) cur = { id, kind, points: [], startedMs: Date.now() }
|
|
||||||
else if (!cur.kind && kind) cur.kind = kind
|
|
||||||
|
|
||||||
const last = cur.points[cur.points.length - 1]
|
|
||||||
const seg = last?.s ?? 0
|
|
||||||
// IMMER anhängen (pro Tick ein Punkt)
|
|
||||||
cur.points.push({ x, y, z, t: tickTs, s: seg })
|
|
||||||
if (cur.points.length > 500) cur.points.splice(0, cur.points.length - 500)
|
|
||||||
|
|
||||||
nadePathsRef.current.set(id, cur)
|
|
||||||
}
|
|
||||||
syncNadePaths() // sofort zeichnen
|
|
||||||
}
|
|
||||||
// Spieler-Flush (wenn sich etwas geändert hat)
|
|
||||||
scheduleFlush()
|
scheduleFlush()
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onerror = (e) => { setWsStatus('error'); console.error('[cs2-ws] error:', e) }
|
ws.onerror = () => setWsStatus('error')
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
setWsStatus('closed')
|
setWsStatus('closed')
|
||||||
if (alive) retry = window.setTimeout(connect, 2000)
|
if (alive) retry = window.setTimeout(connect, 2000)
|
||||||
@ -501,7 +445,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Automatisches Aufräumen von abgelaufenen Effekten (TTL)
|
// ---- Aufräumen (TTL) ------------------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const iv = window.setInterval(() => {
|
const iv = window.setInterval(() => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
@ -510,40 +454,29 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
|
|
||||||
for (const [k, e] of effectsRef.current) {
|
for (const [k, e] of effectsRef.current) {
|
||||||
haveAny = true
|
haveAny = true
|
||||||
// wenn beendet & Fade vorbei -> löschen
|
|
||||||
if (e.ending && e.fadeUntil && now >= e.fadeUntil) {
|
if (e.ending && e.fadeUntil && now >= e.fadeUntil) {
|
||||||
effectsRef.current.delete(k)
|
effectsRef.current.delete(k); changed = true; continue
|
||||||
changed = true
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
// Sicherheit: TTL nur anwenden, wenn nicht bereits im Fade
|
|
||||||
if (!e.ending && now - e.startMs > e.ttlMs) {
|
if (!e.ending && now - e.startMs > e.ttlMs) {
|
||||||
effectsRef.current.delete(k)
|
effectsRef.current.delete(k); changed = true
|
||||||
changed = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Nade-Pfade nach TTL entfernen ---
|
|
||||||
for (const [k, np] of nadePathsRef.current) {
|
for (const [k, np] of nadePathsRef.current) {
|
||||||
if (np.endedMs && Date.now() - np.endedMs > NADE_PATH_TTL) {
|
if (np.endedMs && now - np.endedMs > NADE_PATH_TTL) {
|
||||||
nadePathsRef.current.delete(k)
|
nadePathsRef.current.delete(k); changed = true
|
||||||
changed = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changed) setNadePaths(Array.from(nadePathsRef.current.values()))
|
if (changed) setNadePaths(Array.from(nadePathsRef.current.values()))
|
||||||
|
if (changed || haveAny) setEffects(Array.from(effectsRef.current.values()))
|
||||||
// Repaint erzwingen, damit Fade-Opacity sichtbar animiert
|
}, 100)
|
||||||
if (changed || haveAny) {
|
|
||||||
setEffects(Array.from(effectsRef.current.values()))
|
|
||||||
}
|
|
||||||
}, 100) // ~10 FPS reicht für sanftes Fade
|
|
||||||
return () => window.clearInterval(iv)
|
return () => window.clearInterval(iv)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// ---- 3) Overview laden (JSON ODER Valve-KV)
|
// ---- Overview laden -------------------------------------------------------
|
||||||
const [overview, setOverview] = useState<Overview | null>(null)
|
const [overview, setOverview] = useState<Overview | null>(null)
|
||||||
|
|
||||||
function overviewCandidates(mapKey: string) {
|
const overviewCandidates = (mapKey: string) => {
|
||||||
const base = mapKey
|
const base = mapKey
|
||||||
return [
|
return [
|
||||||
`/assets/resource/overviews/${base}.json`,
|
`/assets/resource/overviews/${base}.json`,
|
||||||
@ -553,6 +486,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
`/assets/resource/overviews/${base}_s2.json`,
|
`/assets/resource/overviews/${base}_s2.json`,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseOverviewJson = (j: any): Overview | null => {
|
const parseOverviewJson = (j: any): Overview | null => {
|
||||||
const posX = Number(j?.posX ?? j?.pos_x)
|
const posX = Number(j?.posX ?? j?.pos_x)
|
||||||
const posY = Number(j?.posY ?? j?.pos_y)
|
const posY = Number(j?.posY ?? j?.pos_y)
|
||||||
@ -561,6 +495,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
if (![posX, posY, scale].every(Number.isFinite)) return null
|
if (![posX, posY, scale].every(Number.isFinite)) return null
|
||||||
return { posX, posY, scale, rotate }
|
return { posX, posY, scale, rotate }
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseValveKvOverview = (txt: string): Overview | null => {
|
const parseValveKvOverview = (txt: string): Overview | null => {
|
||||||
const clean = txt.replace(/\/\/.*$/gm, '')
|
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 pick = (k: string) => { const m = clean.match(new RegExp(`"\\s*${k}\\s*"\\s*"([^"]+)"`)); return m ? Number(m[1]) : NaN }
|
||||||
@ -569,6 +504,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
if (![posX, posY, scale].every(Number.isFinite)) return null
|
if (![posX, posY, scale].every(Number.isFinite)) return null
|
||||||
return { posX, posY, scale, rotate }
|
return { posX, posY, scale, rotate }
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancel = false
|
let cancel = false
|
||||||
;(async () => {
|
;(async () => {
|
||||||
@ -588,14 +524,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
return () => { cancel = true }
|
return () => { cancel = true }
|
||||||
}, [activeMapKey])
|
}, [activeMapKey])
|
||||||
|
|
||||||
// ---- 4) Radarbild-Pfade
|
// ---- Radarbild-Pfade ------------------------------------------------------
|
||||||
const mapLabel = useMemo(() => {
|
|
||||||
if (activeMapKey && voteData?.mapVisuals?.[activeMapKey]?.label)
|
|
||||||
return voteData.mapVisuals[activeMapKey].label
|
|
||||||
if (activeMapKey) return activeMapKey.replace(/^de_/, '').replace(/_/g, ' ').toUpperCase()
|
|
||||||
return 'Unbekannte Map'
|
|
||||||
}, [activeMapKey, voteData?.mapVisuals])
|
|
||||||
|
|
||||||
const { folderKey, imageCandidates } = useMemo(() => {
|
const { folderKey, imageCandidates } = useMemo(() => {
|
||||||
if (!activeMapKey) return { folderKey: null as string | null, imageCandidates: [] as string[] }
|
if (!activeMapKey) return { folderKey: null as string | null, imageCandidates: [] as string[] }
|
||||||
const short = activeMapKey.startsWith('de_') ? activeMapKey.slice(3) : activeMapKey
|
const short = activeMapKey.startsWith('de_') ? activeMapKey.slice(3) : activeMapKey
|
||||||
@ -615,10 +544,10 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
useEffect(() => { setSrcIdx(0) }, [folderKey])
|
useEffect(() => { setSrcIdx(0) }, [folderKey])
|
||||||
const currentSrc = imageCandidates[srcIdx]
|
const currentSrc = imageCandidates[srcIdx]
|
||||||
|
|
||||||
// ---- 5) Bildgröße
|
// ---- Bildgröße ------------------------------------------------------------
|
||||||
const [imgSize, setImgSize] = useState<{ w: number; h: number } | null>(null)
|
const [imgSize, setImgSize] = useState<{ w: number; h: number } | null>(null)
|
||||||
|
|
||||||
// ---- 6) Welt→Pixel + Einheiten→Pixel -------------------------------------
|
// ---- Welt→Pixel & Einheiten→Pixel ----------------------------------------
|
||||||
type Mapper = (xw: number, yw: number) => { x: number; y: number }
|
type Mapper = (xw: number, yw: number) => { x: number; y: number }
|
||||||
|
|
||||||
const worldToPx: Mapper = useMemo(() => {
|
const worldToPx: Mapper = useMemo(() => {
|
||||||
@ -631,8 +560,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
return { x: imgSize.w / 2 + xw * k, y: imgSize.h / 2 - yw * k }
|
return { x: imgSize.w / 2 + xw * k, y: imgSize.h / 2 - yw * k }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { posX, posY, scale } = overview
|
const { posX, posY, scale, rotate = 0 } = overview
|
||||||
const rotDeg = overview.rotate ?? 0
|
|
||||||
const w = imgSize.w, h = imgSize.h
|
const w = imgSize.w, h = imgSize.h
|
||||||
const cx = w / 2, cy = h / 2
|
const cx = w / 2, cy = h / 2
|
||||||
|
|
||||||
@ -646,10 +574,10 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
const candidates: Mapper[] = []
|
const candidates: Mapper[] = []
|
||||||
for (const base of bases) {
|
for (const base of bases) {
|
||||||
for (const s of rotSigns) {
|
for (const s of rotSigns) {
|
||||||
const theta = (rotDeg * s * Math.PI) / 180
|
const theta = (rotate * s * Math.PI) / 180
|
||||||
candidates.push((xw, yw) => {
|
candidates.push((xw, yw) => {
|
||||||
const p = base(xw, yw)
|
const p = base(xw, yw)
|
||||||
if (rotDeg === 0) return p
|
if (rotate === 0) return p
|
||||||
const dx = p.x - cx, dy = p.y - cy
|
const dx = p.x - cx, dy = p.y - cy
|
||||||
const xr = dx * Math.cos(theta) - dy * Math.sin(theta)
|
const xr = dx * Math.cos(theta) - dy * Math.sin(theta)
|
||||||
const yr = dx * Math.sin(theta) + dy * Math.cos(theta)
|
const yr = dx * Math.sin(theta) + dy * Math.cos(theta)
|
||||||
@ -686,7 +614,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
return (u: number) => u * k
|
return (u: number) => u * k
|
||||||
}, [imgSize, overview])
|
}, [imgSize, overview])
|
||||||
|
|
||||||
// ---- Status-Badge
|
// ---- Status-Badge ---------------------------------------------------------
|
||||||
const WsDot = ({ status }: { status: typeof wsStatus }) => {
|
const WsDot = ({ status }: { status: typeof wsStatus }) => {
|
||||||
const color =
|
const color =
|
||||||
status === 'open' ? 'bg-green-500' :
|
status === 'open' ? 'bg-green-500' :
|
||||||
@ -706,32 +634,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitSegments(points: NadePoint[], jumpWorld = 64): NadePoint[][] {
|
// ---- Render ---------------------------------------------------------------
|
||||||
const out: NadePoint[][] = []
|
|
||||||
let cur: NadePoint[] = []
|
|
||||||
let lastS = points.length ? (points[0].s ?? 0) : 0
|
|
||||||
let last: NadePoint | null = null
|
|
||||||
|
|
||||||
for (const p of points) {
|
|
||||||
const s = p.s ?? lastS
|
|
||||||
const jump = last ? Math.hypot(p.x - last.x, p.y - last.y) > jumpWorld : false
|
|
||||||
const breakHere = (s !== lastS) || jump
|
|
||||||
|
|
||||||
if (breakHere) {
|
|
||||||
if (cur.length >= 2) out.push(cur)
|
|
||||||
cur = [p]
|
|
||||||
} else {
|
|
||||||
cur.push(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
last = p
|
|
||||||
lastS = s
|
|
||||||
}
|
|
||||||
if (cur.length >= 2) out.push(cur)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Render
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div ref={headerRef} className="mb-4 flex items-center justify-between">
|
<div ref={headerRef} className="mb-4 flex items-center justify-between">
|
||||||
@ -785,7 +688,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
viewBox={`0 0 ${imgSize.w} ${imgSize.h}`}
|
viewBox={`0 0 ${imgSize.w} ${imgSize.h}`}
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
>
|
>
|
||||||
{/* SVG-Defs für Filter */}
|
{/* SVG-Defs */}
|
||||||
<defs>
|
<defs>
|
||||||
<filter id="smoke-blur" x="-50%" y="-50%" width="200%" height="200%">
|
<filter id="smoke-blur" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
<feGaussianBlur stdDeviation="8" />
|
<feGaussianBlur stdDeviation="8" />
|
||||||
@ -800,7 +703,7 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
</radialGradient>
|
</radialGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
{/* --- Effekte unter den Spielern (SVG-Icon-basiert) --- */}
|
{/* Effekte */}
|
||||||
{effects.map(e => {
|
{effects.map(e => {
|
||||||
const { x, y } = worldToPx(e.x, e.y)
|
const { x, y } = worldToPx(e.x, e.y)
|
||||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
|
||||||
@ -808,81 +711,91 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
const R_WORLD = e.type === 'smoke' ? 170 : 110
|
const R_WORLD = e.type === 'smoke' ? 170 : 110
|
||||||
const halfPx = Math.max(12, unitsToPx(R_WORLD))
|
const halfPx = Math.max(12, unitsToPx(R_WORLD))
|
||||||
const sBase = (halfPx * 2) / 640
|
const sBase = (halfPx * 2) / 640
|
||||||
const s = sBase * (e.type === 'smoke' ? SMOKE_ICON_SCALE : FIRE_ICON_SCALE)
|
const s = sBase * (e.type === 'smoke' ? UI.effects.smokeIconScale : UI.effects.fireIconScale)
|
||||||
const baseT = `translate(${x},${y}) scale(${s}) translate(-320,-320)`
|
const baseT = `translate(${x},${y}) scale(${s}) translate(-320,-320)`
|
||||||
|
|
||||||
let fadeAlpha = 1
|
let fadeAlpha = 1
|
||||||
if (e.type === 'smoke' && e.fadeUntil) {
|
if (e.type === 'smoke' && e.fadeUntil) {
|
||||||
const remain = Math.max(0, e.fadeUntil - Date.now())
|
const remain = Math.max(0, e.fadeUntil - Date.now())
|
||||||
fadeAlpha = Math.min(1, remain / SMOKE_FADE_MS)
|
fadeAlpha = Math.min(1, remain / UI.effects.smokeFadeMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.type === 'smoke') {
|
if (e.type === 'smoke') {
|
||||||
return (
|
return (
|
||||||
<g filter="url(#smoke-blur)" opacity={SMOKE_GROUP_OPACITY * fadeAlpha} transform={baseT}>
|
<g filter="url(#smoke-blur)" opacity={UI.effects.smokeOpacity * fadeAlpha} transform={baseT}>
|
||||||
<path d={SMOKE_PATH} fill="#949494" fillOpacity={SMOKE_FILL_OPACITY} />
|
<path d={SMOKE_PATH} fill="#949494" fillOpacity={UI.effects.smokeFillOpacity} />
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g filter="url(#fire-blur)" opacity={FIRE_GROUP_OPACITY} transform={baseT}>
|
<g filter="url(#fire-blur)" opacity={UI.effects.fireOpacity} transform={baseT}>
|
||||||
<path d={FIRE_PATH} fill="url(#fire-grad)" />
|
<path d={FIRE_PATH} fill="url(#fire-grad)" />
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* --- Spieler darüber --- */}
|
{/* Spieler */}
|
||||||
{players
|
{players
|
||||||
.filter(p => p.team === 'CT' || p.team === 'T') // <— nur CT/T
|
.filter(p => p.team === 'CT' || p.team === 'T')
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
const A = worldToPx(p.x, p.y)
|
const A = worldToPx(p.x, p.y)
|
||||||
const base = Math.min(imgSize.w, imgSize.h)
|
const base = Math.min(imgSize.w, imgSize.h)
|
||||||
const r = Math.max(4, base * 0.008)
|
const r = Math.max(UI.player.minRadiusPx, base * UI.player.radiusRel)
|
||||||
const dirLenPx = Math.max(18, base * 0.025)
|
|
||||||
const stroke = '#fff'
|
|
||||||
const strokeW = Math.max(1.5, r * 0.35)
|
|
||||||
const color = p.team === 'CT' ? '#3b82f6' : '#f59e0b'
|
|
||||||
|
|
||||||
const yawRad = (p.yaw * Math.PI) / 180
|
const dirLenPx = Math.max(UI.player.dirMinLenPx, r * UI.player.dirLenRel)
|
||||||
|
const stroke = 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
|
||||||
|
|
||||||
|
const yawRad = Number.isFinite(p.yaw) ? (p.yaw * Math.PI) / 180 : 0
|
||||||
|
|
||||||
|
// Richtung als Welt-Schritt (respektiert Overview-Rotation)
|
||||||
const STEP_WORLD = 200
|
const STEP_WORLD = 200
|
||||||
const B = worldToPx(
|
const B = worldToPx(
|
||||||
p.x + Math.cos(yawRad) * STEP_WORLD,
|
p.x + Math.cos(yawRad) * STEP_WORLD,
|
||||||
p.y + Math.sin(yawRad) * STEP_WORLD
|
p.y + Math.sin(yawRad) * STEP_WORLD
|
||||||
)
|
)
|
||||||
|
|
||||||
let dxp = B.x - A.x, dyp = B.y - A.y
|
let dxp = B.x - A.x, dyp = B.y - A.y
|
||||||
|
if (!Number.isFinite(dxp) || !Number.isFinite(dyp)) { dxp = STEP_WORLD; dyp = 0 }
|
||||||
|
|
||||||
const cur = Math.hypot(dxp, dyp) || 1
|
const cur = Math.hypot(dxp, dyp) || 1
|
||||||
dxp *= dirLenPx / cur
|
dxp *= dirLenPx / cur
|
||||||
dyp *= dirLenPx / cur
|
dyp *= dirLenPx / cur
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g key={p.id}>
|
<g key={p.id}>
|
||||||
<line x1={A.x} y1={A.y} x2={A.x + dxp} y2={A.y + dyp}
|
{/* Kreis zuerst */}
|
||||||
stroke={color} strokeWidth={strokeW} strokeLinecap="round"
|
<circle
|
||||||
opacity={p.alive === false ? 0.5 : 1}/>
|
cx={A.x} cy={A.y} r={r}
|
||||||
<circle cx={A.x} cy={A.y} r={r}
|
fill={fillColor} stroke={stroke}
|
||||||
fill={color} stroke={stroke}
|
|
||||||
strokeWidth={Math.max(1, r*0.3)}
|
strokeWidth={Math.max(1, r*0.3)}
|
||||||
opacity={p.alive === false ? 0.6 : 1}/>
|
opacity={p.alive === false ? 0.6 : 1}
|
||||||
|
/>
|
||||||
|
{/* Linie darüber (sichtbar) */}
|
||||||
|
<line
|
||||||
|
x1={A.x} y1={A.y} x2={A.x + dxp} y2={A.y + dyp}
|
||||||
|
stroke={dirColor} strokeWidth={strokeW} strokeLinecap="round"
|
||||||
|
opacity={p.alive === false ? 0.5 : 1}
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* --- Nade-Pfade (unter Effekten & Spielern) --- */}
|
{/* Nade-Pfade */}
|
||||||
<g opacity={0.95}>
|
<g opacity={0.95}>
|
||||||
{nadePaths.map(np => {
|
{nadePaths.map(np => {
|
||||||
const col = nadeColor(np.kind)
|
const col = nadeColor(np.kind)
|
||||||
const wBase = Math.min(imgSize.w, imgSize.h)
|
const wBase = Math.min(imgSize.w, imgSize.h)
|
||||||
const strokeW = Math.max(2, wBase * 0.004)
|
const strokeW = Math.max(2, wBase * 0.004)
|
||||||
const dotR = Math.max(1.5, wBase * 0.0025)
|
const dotR = Math.max(1.5, wBase * 0.0025)
|
||||||
|
|
||||||
let alpha = 1
|
let alpha = 1
|
||||||
if (np.endedMs) alpha = Math.max(0, 1 - (Date.now() - np.endedMs) / NADE_PATH_TTL)
|
if (np.endedMs) alpha = Math.max(0, 1 - (Date.now() - np.endedMs) / NADE_PATH_TTL)
|
||||||
|
|
||||||
const pts = np.points
|
const pts = np.points
|
||||||
.map(p => worldToPx(p.x, p.y))
|
.map(p => worldToPx(p.x, p.y))
|
||||||
.filter(p => Number.isFinite(p.x) && Number.isFinite(p.y))
|
.filter(p => Number.isFinite(p.x) && Number.isFinite(p.y))
|
||||||
|
|
||||||
if (pts.length === 0) return null
|
if (pts.length === 0) return null
|
||||||
|
|
||||||
const d = pts.length >= 2
|
const d = pts.length >= 2
|
||||||
@ -901,7 +814,6 @@ export default function LiveRadar({ matchId }: Props) {
|
|||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Punkte anzeigen, damit sofort etwas sichtbar ist */}
|
|
||||||
{pts.map((p, i) => (
|
{pts.map((p, i) => (
|
||||||
<circle key={`${np.id}:pt:${i}`} cx={p.x} cy={p.y} r={dotR} fill={col} opacity={0.9} />
|
<circle key={`${np.id}:pt:${i}`} cx={p.x} cy={p.y} r={dotR} fill={col} opacity={0.9} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user