297 lines
12 KiB
TypeScript
297 lines
12 KiB
TypeScript
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,
|
||
};
|
||
}
|