2025-09-23 15:27:42 +02:00

297 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import { useEffect, useMemo, useRef, useState } from 'react';
import { BombState, DeathMarker, Grenade, PlayerState, Score, Trail, WsStatus } from '@/lib/types';
import { UI } from '@/lib/ui';
import { asNum, mapTeam, steamIdOf } from '@/lib/helpers';
import { normalizeGrenades } from '@/lib/grenades';
export function useRadarState(mySteamId: string | null) {
// WS / Map
const [radarWsStatus, setGameWsStatus] = useState<WsStatus>('idle');
const [activeMapKey, setActiveMapKey] = useState<string | null>(null);
// Spieler
const playersRef = useRef<Map<string, PlayerState>>(new Map());
const [players, setPlayers] = useState<PlayerState[]>([]);
const [hoveredPlayerId, setHoveredPlayerId] = useState<string | null>(null);
// Deaths
const deathSeqRef = useRef(0);
const deathSeenRef = useRef<Set<string>>(new Set());
const lastAlivePosRef = useRef<Map<string, {x:number,y:number}>>(new Map());
// Grenaden + Trails
const grenadesRef = useRef<Map<string, Grenade>>(new Map());
const [grenades, setGrenades] = useState<Grenade[]>([]);
const trailsRef = useRef<Map<string, Trail>>(new Map());
const [trails, setTrails] = useState<Trail[]>([]);
// Death-Marker
const deathMarkersRef = useRef<DeathMarker[]>([]);
const [deathMarkers, setDeathMarkers] = useState<DeathMarker[]>([]);
// Bomb
const bombRef = useRef<BombState | null>(null);
const [bomb, setBomb] = useState<BombState | null>(null);
// Score + Phase
const [roundPhase, setRoundPhase] =
useState<'freezetime'|'live'|'bomb'|'over'|'warmup'|'unknown'>('unknown');
const roundEndsAtRef = useRef<number|null>(null);
const bombEndsAtRef = useRef<number|null>(null);
const defuseRef = useRef<{ by: string|null; hasKit: boolean; endsAt: number|null }>({ by: null, hasKit: false, endsAt: null });
const [score, setScore] = useState<Score>({ ct: 0, t: 0, round: null });
// flush-batching
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()));
setGrenades(Array.from(grenadesRef.current.values()));
setTrails(Array.from(trailsRef.current.values()));
setDeathMarkers([...deathMarkersRef.current]);
updateBombFromPlayers();
setBomb(bombRef.current);
}, 66);
};
useEffect(() => () => {
if (flushTimer.current != null) { window.clearTimeout(flushTimer.current); flushTimer.current = null; }
}, []);
const myTeam = useMemo<'T'|'CT'|string|null>(() => {
if (!mySteamId) return null;
return playersRef.current.get(mySteamId)?.team ?? null;
}, [players, mySteamId]);
const addDeathMarker = (x:number,y:number, steamId?: string) => {
const now = Date.now();
if (steamId) {
if (deathSeenRef.current.has(steamId)) return;
deathSeenRef.current.add(steamId);
}
const uid = `${steamId ?? 'd'}#${now}#${deathSeqRef.current++}`;
deathMarkersRef.current.push({ id: uid, sid: steamId ?? null, x, y, t: now });
};
const addDeathMarkerFor = (id: string, xNow: number, yNow: number) => {
const last = lastAlivePosRef.current.get(id);
const x = Number.isFinite(last?.x) ? last!.x : xNow;
const y = Number.isFinite(last?.y) ? last!.y : yNow;
addDeathMarker(x, y, id);
};
const clearRoundArtifacts = (resetPlayers = false, hard = false) => {
deathMarkersRef.current = [];
deathSeenRef.current.clear();
trailsRef.current.clear();
grenadesRef.current.clear();
lastAlivePosRef.current.clear();
bombRef.current = null;
if (hard) {
playersRef.current.clear();
} else if (resetPlayers) {
for (const [id, p] of playersRef.current) {
playersRef.current.set(id, { ...p, alive: true, hasBomb: false });
}
}
scheduleFlush();
};
const updateBombFromPlayers = () => {
if (bombRef.current?.status === 'planted') return;
const carrier = Array.from(playersRef.current.values()).find(p => p.hasBomb);
if (carrier) {
bombRef.current = {
x: carrier.x, y: carrier.y, z: carrier.z,
status: 'carried',
changedAt: bombRef.current?.status === 'carried'
? bombRef.current.changedAt
: Date.now(),
};
}
};
// ---- Player Upsert (gekürzt Logik aus deiner Datei) --------------
function upsertPlayer(e:any) {
const id = steamIdOf(e); if (!id) return;
const pos = e.pos ?? e.position ?? e.location ?? e.coordinates;
const x = asNum(e.x ?? (Array.isArray(pos) ? pos?.[0] : pos?.x));
const y = asNum(e.y ?? (Array.isArray(pos) ? pos?.[1] : pos?.y));
const z = asNum(e.z ?? (Array.isArray(pos) ? pos?.[2] : pos?.z), 0);
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
const hpProbe = asNum(e.hp ?? e.health ?? e.state?.health, NaN);
const old = playersRef.current.get(id);
const nextAlive = Number.isFinite(hpProbe) ? hpProbe > 0 : (old?.alive ?? true);
if ((old?.alive !== false) && nextAlive === false) addDeathMarker(x, y, id);
if (nextAlive === true) lastAlivePosRef.current.set(id, { x, y });
else if (nextAlive === false && (old?.alive !== false)) addDeathMarkerFor(id, x, y);
const activeWeaponName =
(typeof e.activeWeapon === 'string' && e.activeWeapon) ||
(e.activeWeapon?.name ?? null) ||
(Array.isArray(e.weapons)
? (e.weapons.find((w:any) => (w?.state ?? '').toLowerCase() === 'active')?.name ?? null)
: null);
playersRef.current.set(id, {
id,
name: e.name ?? old?.name ?? null,
team: mapTeam(e.team ?? old?.team),
x, y, z,
yaw: Number.isFinite(Number(e.yaw)) ? Number(e.yaw) : (old?.yaw ?? null),
alive: nextAlive,
hasBomb: Boolean(e.hasBomb) || Boolean(old?.hasBomb),
hp: Number.isFinite(hpProbe) ? hpProbe : (old?.hp ?? null),
armor: Number.isFinite(asNum(e.armor ?? e.state?.armor, NaN)) ? asNum(e.armor ?? e.state?.armor, NaN) : (old?.armor ?? null),
helmet: (e.helmet ?? e.hasHelmet ?? e.state?.helmet) ?? (old?.helmet ?? null),
defuse: (e.defuse ?? e.hasDefuse ?? e.hasDefuser ?? e.state?.defusekit) ?? (old?.defuse ?? null),
activeWeapon: activeWeaponName ?? old?.activeWeapon ?? null,
weapons: Array.isArray(e.weapons) ? e.weapons : (old?.weapons ?? null),
nades: old?.nades ?? null,
});
}
// ---- Handlers für GameSocket ---------------------------------------
const handlePlayersAll = (msg:any) => {
const pcd = msg?.phase ?? msg?.phase_countdowns;
const phase = String(pcd?.phase ?? '').toLowerCase();
if (phase === 'freezetime' && (deathMarkersRef.current.length || trailsRef.current.size)) {
clearRoundArtifacts(true);
}
if (pcd?.phase_ends_in != null) {
const sec = Number(pcd.phase_ends_in);
if (Number.isFinite(sec)) {
roundEndsAtRef.current = Date.now() + sec * 1000;
setRoundPhase(String(pcd.phase ?? 'unknown').toLowerCase() as any);
}
} else if (pcd?.phase) {
setRoundPhase(String(pcd.phase).toLowerCase() as any);
}
if ((pcd?.phase ?? '').toLowerCase() === 'over') {
roundEndsAtRef.current = null;
bombEndsAtRef.current = null;
defuseRef.current = { by: null, hasKit: false, endsAt: null };
}
// Spieler (gekürzt, robust genug)
const apObj = msg?.allplayers;
const apArr = Array.isArray(msg?.players) ? msg.players : null;
const upsertFromPayload = (p:any) => {
const id = steamIdOf(p); if (!id) return;
const pos = p.position ?? p.pos ?? p.location ?? p.coordinates ?? p.eye ?? p.pos;
const xyz = Array.isArray(pos) ? { x: pos[0], y: pos[1], z: pos[2] }
: typeof pos === 'object' ? pos : { x: p.x, y: p.y, z: p.z };
const { x=0, y=0, z=0 } = xyz;
const hpNum = Number(p?.state?.health ?? p?.hp);
const isAlive = Number.isFinite(hpNum) ? hpNum > 0 : (playersRef.current.get(id)?.alive ?? true);
if ((playersRef.current.get(id)?.alive ?? true) && !isAlive) addDeathMarker(x, y, id);
playersRef.current.set(id, {
id,
name: p?.name ?? playersRef.current.get(id)?.name ?? null,
team: mapTeam(p?.team ?? playersRef.current.get(id)?.team),
x, y, z,
yaw: playersRef.current.get(id)?.yaw ?? null,
alive: isAlive,
hasBomb: Boolean(playersRef.current.get(id)?.hasBomb),
hp: Number.isFinite(hpNum) ? hpNum : (playersRef.current.get(id)?.hp ?? null),
armor: playersRef.current.get(id)?.armor ?? null,
helmet: playersRef.current.get(id)?.helmet ?? null,
defuse: playersRef.current.get(id)?.defuse ?? null,
activeWeapon: playersRef.current.get(id)?.activeWeapon ?? null,
weapons: playersRef.current.get(id)?.weapons ?? null,
nades: playersRef.current.get(id)?.nades ?? null,
});
};
if (apObj && typeof apObj === 'object') for (const k of Object.keys(apObj)) upsertFromPayload(apObj[k]);
else if (apArr) for (const p of apArr) upsertFromPayload(p);
// Scores (robust, gekürzt)
const pick = (v:any)=> Number.isFinite(Number(v)) ? Number(v) : null;
const ct = pick(msg?.score?.ct) ?? pick(msg?.scores?.ct) ?? pick(msg?.map?.team_ct?.score) ?? 0;
const t = pick(msg?.score?.t) ?? pick(msg?.scores?.t) ?? pick(msg?.map?.team_t?.score) ?? 0;
const rnd= pick(msg?.round) ?? pick(msg?.rounds?.played) ?? pick(msg?.map?.round) ?? null;
setScore({ ct, t, round: rnd });
scheduleFlush();
};
const handleGrenades = (g:any) => {
const list = normalizeGrenades(g);
const now = Date.now();
// Trails nur für eigene Projektile
const mine = mySteamId
? list.filter(n => n.ownerId === mySteamId && n.phase === 'projectile')
: [];
const seenTrailIds = new Set<string>();
for (const it of mine) {
seenTrailIds.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);
}
for (const [id, tr] of trailsRef.current) {
if (!seenTrailIds.has(id) && now - tr.lastSeen > UI.trail.fadeMs) trailsRef.current.delete(id);
}
// sanftes Mergen
const next = new Map<string, Grenade>(grenadesRef.current);
const seenIds = new Set<string>();
for (const it of list) { seenIds.add(it.id); next.set(it.id, { ...(next.get(it.id) || {}), ...it }); }
for (const [id, nade] of next) {
if (!seenIds.has(id) && nade.phase === 'projectile') next.delete(id);
if ((nade.phase === 'effect' || nade.phase === 'exploded') && nade.expiresAt != null && nade.expiresAt <= now) next.delete(id);
}
grenadesRef.current = next;
scheduleFlush();
};
const handleBomb = (normalizeBomb:(b:any)=>BombState|null) => (b:any) => {
const prev = bombRef.current;
const nb = normalizeBomb(b);
if (!nb) return;
const withPos = {
x: Number.isFinite(nb.x) ? nb.x : (prev?.x ?? 0),
y: Number.isFinite(nb.y) ? nb.y : (prev?.y ?? 0),
z: Number.isFinite(nb.z) ? nb.z : (prev?.z ?? 0),
};
const sameStatus = prev && prev.status === nb.status;
bombRef.current = { ...withPos, status: nb.status, changedAt: sameStatus ? prev!.changedAt : Date.now() };
scheduleFlush();
};
return {
// state
radarWsStatus, setGameWsStatus,
activeMapKey, setActiveMapKey,
players, playersRef, hoveredPlayerId, setHoveredPlayerId,
grenades, trails, deathMarkers,
bomb,
roundPhase, roundEndsAtRef, bombEndsAtRef, defuseRef,
score,
myTeam,
// ops
upsertPlayer,
handlePlayersAll,
handleGrenades,
handleBomb,
clearRoundArtifacts,
scheduleFlush,
addDeathMarker,
};
}