From 949ce18a6b12999c7a5bde5c2a5a5f8b9e683c01 Mon Sep 17 00:00:00 2001 From: Linrador Date: Sat, 13 Sep 2025 15:40:46 +0200 Subject: [PATCH] updated --- server.js | 445 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 380 insertions(+), 65 deletions(-) diff --git a/server.js b/server.js index 2781ad9..fc15348 100644 --- a/server.js +++ b/server.js @@ -109,71 +109,397 @@ function pickVec3Any(pos) { } const NADE_DEFAULTS = { - smoke: { radius: 150, lifetimeMs: 18_000 }, - molotov:{ radius: 120, lifetimeMs: 7_000 }, - he: { radius: 40, lifetimeMs: 300 }, - flash: { radius: 36, lifetimeMs: 300 }, - decoy: { radius: 80, lifetimeMs: 15_000 }, + smoke: { radius: 150, lifetimeMs: 18_000 }, + molotov: { radius: 120, lifetimeMs: 7_000 }, + incendiary: { radius: 120, lifetimeMs: 7_000 }, + he: { radius: 40, lifetimeMs: 300 }, + flash: { radius: 36, lifetimeMs: 300 }, + decoy: { radius: 80, lifetimeMs: 15_000 }, }; function normalizeGrenadesFromGSI(raw, now) { + if (!raw || typeof raw !== 'object') return []; - // Dr. Weissbrot Format: buckets (smokes, infernos, hegrenades, flashes, decoys, ...), - // evtl. auch *_projectile – beides unterstützen. - const mapKind = (k) => { - const s = k.toLowerCase(); - if (s.includes('smoke')) return 'smoke'; - if (s.includes('inferno') || s.includes('molotov') || s.includes('incendiary')) return 'molotov'; - if (s.includes('flash')) return 'flash'; - if (s.includes('decoy')) return 'decoy'; - if (s.includes('he')) return 'he'; + const KNOWN_BUCKETS = new Set([ + 'smokes','smoke','smokegrenade','smokegrenades', + 'smoke_effect','smokeeffects','smokecloud','smokeclouds', + 'inferno','molotov','incendiary','firebomb', + 'he','hegrenade','hegrenades','explosive', + 'flash','flashbang','flashbangs', + 'decoy','decoys', + 'smokegrenade_projectile','hegrenade_projectile','flashbang_projectile','decoy_projectile', + 'projectiles','grenadeprojectiles','nades','flying' + ]); + + + const mapKindFromString = (s) => { + const k = String(s || '').toLowerCase(); + if (k.includes('smoke')) return 'smoke'; + if (k.includes('inferno') || k.includes('molotov') || k.includes('incendiary')) { + return k.includes('incendiary') ? 'incendiary' : 'molotov'; + } + if (k.includes('flash')) return 'flash'; + if (k.includes('decoy')) return 'decoy'; + if (k.includes('he') || k.includes('frag')) return 'he'; + if (k.includes('firebomb')) return 'molotov'; return 'unknown'; }; + - const pushList = (acc, kind, list) => { - if (!list) return; - const arr = Array.isArray(list) ? list : Object.values(list); - let i = 0; - for (const g of arr) { - const pos = pickVec3Any(g?.position || g?.pos || g?.location || g); - const id = - String(g?.id ?? g?.entityid ?? g?.entindex ?? - `${kind}#${Math.round(pos.x)}:${Math.round(pos.y)}:${i++}`); - - const ownerTeam = (g?.owner?.team || g?.team || '').toString().toUpperCase(); - const team = ownerTeam === 'T' || ownerTeam === 'CT' ? ownerTeam : null; - - const def = NADE_DEFAULTS[kind] || { radius: 60, lifetimeMs: 2_000 }; - const bornAt = - Number.isFinite(+g?.lifetime) ? (now - Math.max(0, +g.lifetime * 1000)) - : Number.isFinite(+g?.spawn_time) ? +g.spawn_time - : now; - - // Für aktive Effekte (z.B. smoke/inferno) gibt es teils direkte "expire"-Infos. - const radius = Number.isFinite(+g?.radius) ? +g.radius : def.radius; - const expiresAt = Number.isFinite(+g?.expiresAt) - ? +g.expiresAt - : bornAt + def.lifetimeMs; - - acc.push({ - id, - kind, - x: pos.x, y: pos.y, z: pos.z, - radius, - expiresAt, - team, - }); + const parsePos = (src) => { + if (!src) return null; + if (Array.isArray(src)) { + const x = +src[0], y = +src[1], z = +(src[2] ?? 0); + return (Number.isFinite(x) && Number.isFinite(y)) ? { x, y, z } : null; } + if (typeof src === 'string') { + const sp = src.split(/[\s,]+/).map(Number); + const x = sp[0], y = sp[1], z = +(sp[2] ?? 0); + return (Number.isFinite(x) && Number.isFinite(y)) ? { x, y, z } : null; + } + const x = +src.x, y = +src.y, z = +(src.z ?? 0); + return (Number.isFinite(x) && Number.isFinite(y)) ? { x, y, z } : null; + }; + + const parseVec = (src) => { + if (!src) return null; + if (typeof src === 'string') { + const sp = src.split(/[\s,]+/).map(Number); + const x = sp[0], y = sp[1], z = +(sp[2] ?? 0); + if ([x,y].every(Number.isFinite)) return { x, y, z }; + return null; + } + if (typeof src === 'object') { + const x = +src.x, y = +src.y, z = +(src.z ?? 0); + if ([x,y].every(Number.isFinite)) return { x, y, z }; + } + return null; + }; + + const circleFromFlames = (flamesObj) => { + if (!flamesObj || typeof flamesObj !== 'object') return null; + const pts = []; + for (const v of Object.values(flamesObj)) { + const p = parsePos(v); + if (p) pts.push(p); + } + if (!pts.length) return null; + const cx = pts.reduce((a,p)=>a+p.x,0) / pts.length; + const cy = pts.reduce((a,p)=>a+p.y,0) / pts.length; + let r = 0; + for (const p of pts) { + const d = Math.hypot(p.x - cx, p.y - cy); + if (d > r) r = d; + } + r = Math.max(r + 24, 60); + const cz = pts.reduce((a,p)=>a+p.z,0) / pts.length; + return { x: cx, y: cy, z: cz, r }; + }; + + // 🔸 EffectTime Reader (Sekunden) – robuster + const readEffectTimeSec = (g) => { + const v = + g?.effecttime ?? + g?.effectTime ?? + g?.EffectTime ?? + g?.effect_time ?? + g?.efftime ?? + g?.lifetime ?? + g?.time_since_detonation ?? + g?.time_since_start; + + const n = Number(v); + return Number.isFinite(n) ? n : 0; }; const out = []; + + // --- Effekt-Listen (Buckets wie 'smokes', 'inferno', ...) --- + const pushEffectList = (bucketKey, list) => { + if (!list) return; + const arr = Array.isArray(list) ? list : Object.values(list); + let seq = 0; + + for (const g of arr) { + // c) Sonstiges: frag/flash/decoy/etc. und SMOKE (numeric keys) + const pos = parsePos(g.position ?? g.pos ?? g.location); + if (!pos) continue; + + const kind = mapKindFromString(type); + + if (kind === 'unknown') continue; + + let team = null; + const ownerSid = String(g.owner ?? g.thrower ?? g.player ?? ''); + if (ownerSid && lastBySteam && lastBySteam.has(Number(ownerSid))) { + const snap = lastBySteam.get(Number(ownerSid)); + const t = String(snap?.team || '').toUpperCase(); + team = (t === 'T' || t === 'CT') ? t : null; + } + + // 🔸 NEU: smoke hier wie bei smokegrenade_projectile behandeln + if (kind === 'smoke') { + const effectTimeSec = readEffectTimeSec(g); + const state = String(g?.state ?? '').toLowerCase(); + const isEffectFlag = state === 'effect' || state === 'stopped' || g?.stopped === true || g?.detonated === true; + + const idBase = `${k}:${Math.round(pos.x)}:${Math.round(pos.y)}`; + + if (effectTimeSec > 0 || isEffectFlag) { + const lifeMs = NADE_DEFAULTS.smoke.lifetimeMs; + const bornAt = effectTimeSec > 0 ? (now - Math.max(0, effectTimeSec * 1000)) : now; + const expiresAt = bornAt + lifeMs; + const lifeElapsedMs = Math.max(0, now - bornAt); + const lifeLeftMs = Math.max(0, expiresAt - now); + + out.push({ + id: `smoke#${idBase}`, + kind: 'smoke', + x: pos.x, y: pos.y, z: pos.z, + radius: NADE_DEFAULTS.smoke.radius, + expiresAt, + team, + phase: 'effect', + effectTimeSec: Math.max(effectTimeSec, lifeElapsedMs / 1000), + lifeElapsedMs, + lifeLeftMs + }); + } else { + const vel = parseVec(g?.velocity ?? g?.vel ?? g?.dir ?? g?.forward); + const payload = { + id: `proj:smoke#${idBase}`, + kind: 'smoke', + x: pos.x, y: pos.y, z: pos.z, + team, + phase: 'projectile', + effectTimeSec: 0 + }; + if (vel) payload.vel = { x: vel.x, y: vel.y, z: vel.z }; + out.push(payload); + } + continue; // smoke erledigt – nicht in den generischen Zweig fallen + } + + // (bestehender generischer Zweig für andere Arten) + const id = `proj:${kind}#${k}:${Math.round(pos.x)}:${Math.round(pos.y)}`; + const payload = { id, kind, x: pos.x, y: pos.y, z: pos.z, team, phase: 'projectile' }; + const vel = parseVec(g.velocity); + if (vel) payload.vel = { x: vel.x, y: vel.y, z: vel.z }; + out.push(payload); + + } + }; + + // --- Spezial: 'smokegrenade_projectile' mit EffectTime-Umschaltung --- + const pushSmokeFromProjectiles = (list) => { + if (!list) return; + const arr = Array.isArray(list) ? list : Object.values(list); + let seq = 0; + + for (const g of arr) { + const pos = parsePos(g?.position ?? g?.pos ?? g?.location ?? g?.origin); + if (!pos) continue; + + const effectTimeSec = readEffectTimeSec(g); + + // zusätzliche Flags aus manchen GSIs + const state = String(g?.state ?? '').toLowerCase(); + const isEffectFlag = + state === 'effect' || state === 'stopped' || g?.stopped === true || g?.detonated === true; + + const teamRaw = String(g?.owner?.team ?? g?.team ?? g?.owner_team ?? '').toUpperCase(); + const team = (teamRaw === 'T' || teamRaw === 'CT') ? teamRaw : null; + + const givenId = g?.id ?? g?.entityid ?? g?.entindex; + const idBase = givenId ? String(givenId) : `${Math.round(pos.x)}:${Math.round(pos.y)}:${seq++}`; + const id = `smoke#${idBase}`; // ← EINHEITLICHE ID für projectile **und** effect + + if (effectTimeSec > 0 || isEffectFlag) { + const lifeMs = NADE_DEFAULTS.smoke.lifetimeMs; + const bornAt = effectTimeSec > 0 ? (now - Math.max(0, effectTimeSec * 1000)) : now; + const expiresAt = bornAt + lifeMs; + const lifeElapsedMs = Math.max(0, now - bornAt); + const lifeLeftMs = Math.max(0, expiresAt - now); + + out.push({ + id, + kind: 'smoke', + x: pos.x, y: pos.y, z: pos.z, + radius: NADE_DEFAULTS.smoke.radius, + expiresAt, + team, + phase: 'effect', // ← ab hier Effekt + effectTimeSec: Math.max(effectTimeSec, lifeElapsedMs / 1000), + lifeElapsedMs, + lifeLeftMs + }); + } else { + const vel = parseVec(g?.velocity ?? g?.vel ?? g?.dir ?? g?.forward); + const payload = { + id, // ← gleiche ID wie beim Effekt + kind: 'smoke', + x: pos.x, y: pos.y, z: pos.z, + team, + phase: 'projectile', + effectTimeSec: 0 + }; + if (vel) payload.vel = { x: vel.x, y: vel.y, z: vel.z }; + out.push(payload); + } + } + }; + + + // 1) Buckets auswerten for (const [k, v] of Object.entries(raw)) { - // z.B. "smokes", "smokegrenade_projectile", "inferno", "hegrenade_projectile" ... - const kind = mapKind(k); - pushList(out, kind, v); + const key = String(k).toLowerCase(); + + if (key === 'smokegrenade_projectile') { + pushSmokeFromProjectiles(v); + continue; + } + if (KNOWN_BUCKETS.has(key)) { + pushEffectList(key, v); + } } - return out; + + // 2) Numerische/sonstige Keys … + for (const [k, v] of Object.entries(raw)) { + const key = String(k).toLowerCase(); + if (KNOWN_BUCKETS.has(key)) continue; + const g = v; + if (!g || typeof g !== 'object') continue; + + const type = String(g.type || '').toLowerCase(); + + // a) Projektile: firebomb → molotov/incendiary + if (type === 'firebomb') { + const pos = parsePos(g.position); + if (!pos) continue; + + const vel = parseVec(g.velocity); + let team = null; + const ownerSid = String(g.owner ?? ''); + if (ownerSid && lastBySteam && lastBySteam.has(Number(ownerSid))) { + const snap = lastBySteam.get(Number(ownerSid)); + const t = String(snap?.team || '').toUpperCase(); + team = (t === 'T' || t === 'CT') ? t : null; + } + const kind = team === 'CT' ? 'incendiary' : 'molotov'; + const id = `proj:${kind}#${k}:${Math.round(pos.x)}:${Math.round(pos.y)}`; + + const payload = { id, kind, x: pos.x, y: pos.y, z: pos.z, team, phase: 'projectile' }; + if (vel) payload.vel = { x: vel.x, y: vel.y, z: vel.z }; + + out.push(payload); + continue; + } + + // b) Effekt: inferno → Kreis aus flames (Herdpunkte) + if (type === 'inferno') { + const circ = circleFromFlames(g.flames); + if (!circ) continue; + + let team = null; + const ownerSid = String(g.owner ?? ''); + if (ownerSid && lastBySteam && lastBySteam.has(Number(ownerSid))) { + const snap = lastBySteam.get(Number(ownerSid)); + const t = String(snap?.team || '').toUpperCase(); + team = (t === 'T' || t === 'CT') ? t : null; + } + + const bornAt = Number.isFinite(+g?.lifetime) ? (now - Math.max(0, +g.lifetime * 1000)) : now; + const lifeMs = NADE_DEFAULTS.molotov.lifetimeMs; + const expiresAt = bornAt + lifeMs; + + const kind = team === 'CT' ? 'incendiary' : 'molotov'; + const id = `inferno:${k}`; + + out.push({ + id, + kind, + x: circ.x, y: circ.y, z: circ.z, + radius: circ.r, + expiresAt, + team, + phase: 'effect' + }); + continue; + } + + // c) "Sonstiges": hier kommt deine smoke mit numerischer ID an + const pos = parsePos(g.position ?? g.pos ?? g.location); + if (!pos) continue; + + const kind = mapKindFromString(type); + if (kind === 'unknown') continue; + + let team = null; + const ownerSid = String(g.owner ?? g.thrower ?? g.player ?? ''); + if (ownerSid && lastBySteam && lastBySteam.has(Number(ownerSid))) { + const snap = lastBySteam.get(Number(ownerSid)); + const t = String(snap?.team || '').toUpperCase(); + team = (t === 'T' || t === 'CT') ? t : null; + } + + // 🔸 NEU: smoke wie im projectile-Bucket behandeln + if (kind === 'smoke') { + const effectTimeSec = readEffectTimeSec(g); + const state = String(g?.state ?? '').toLowerCase(); + const isEffectFlag = + state === 'effect' || state === 'stopped' || g?.stopped === true || g?.detonated === true; + + const idBase = `${k}:${Math.round(pos.x)}:${Math.round(pos.y)}`; + const id = `smoke#${idBase}`; // ← gleiche ID für beide Phasen + + if (effectTimeSec > 0 || isEffectFlag) { + const lifeMs = NADE_DEFAULTS.smoke.lifetimeMs; + const bornAt = effectTimeSec > 0 ? (now - Math.max(0, effectTimeSec * 1000)) : now; + const expiresAt = bornAt + lifeMs; + const lifeElapsedMs = Math.max(0, now - bornAt); + const lifeLeftMs = Math.max(0, expiresAt - now); + + out.push({ + id, + kind: 'smoke', + x: pos.x, y: pos.y, z: pos.z, + radius: NADE_DEFAULTS.smoke.radius, + expiresAt, + team, + phase: 'effect', // ← Effekt + effectTimeSec: Math.max(effectTimeSec, lifeElapsedMs / 1000), + lifeElapsedMs, + lifeLeftMs + }); + } else { + const vel = parseVec(g?.velocity ?? g?.vel ?? g?.dir ?? g?.forward); + const payload = { + id, // ← gleiche ID + kind: 'smoke', + x: pos.x, y: pos.y, z: pos.z, + team, + phase: 'projectile', // ← Projektil bis effecttime > 0 + effectTimeSec: 0 + }; + if (vel) payload.vel = { x: vel.x, y: vel.y, z: vel.z }; + out.push(payload); + } + continue; // smoke erledigt – nicht in generischen Zweig fallen + } + + // generischer Zweig für he/flash/decoy etc. + const id = `proj:${kind}#${k}:${Math.round(pos.x)}:${Math.round(pos.y)}`; + const payload = { id, kind, x: pos.x, y: pos.y, z: pos.z, team, phase: 'projectile' }; + const vel = parseVec(g.velocity); + if (vel) payload.vel = { x: vel.x, y: vel.y, z: vel.z }; + out.push(payload); + } + + // (Optional) einfache Dedupe per ID + const byId = new Map(); + for (const n of out) byId.set(n.id, n); + return Array.from(byId.values()); } function forwardToYawPitch(fwd) { @@ -223,9 +549,7 @@ function normalizeBombFromGSI(body) { else if (raw.includes("defused")) status = "defused"; else if (raw.includes("dropped")) status = "dropped"; else if (raw.includes("carried")) status = "carried"; - // "planting" bewusst NICHT als planted behandeln - // bool-Fallbacks if (b?.planted === true) status = "planted"; if (b?.defusing === true) status = "defusing"; if (b?.defused === true) status = "defused"; @@ -236,7 +560,6 @@ function normalizeBombFromGSI(body) { return { x: pos.x, y: pos.y, z: pos.z, status }; } - function sameBomb(a, b) { if (!a && !b) return true; if (!a || !b) return false; @@ -275,11 +598,9 @@ app.post(GSI_PATH, (req, res) => { // Round-Events anhand Phase-Transition const curPhase = (phaseInfo.phase || "").toLowerCase(); if (curPhase !== lastPhase) { - // Start zählt, sobald es "live" wird (alternativ: "freezetime" → "live") if (curPhase === "live" && lastPhase && lastPhase !== "live") { broadcast({ type: "round_start", round: phaseInfo.round ?? null }); } - // Ende, wenn "over" if (curPhase === "over" && lastPhase !== "over") { broadcast({ type: "round_end", round: phaseInfo.round ?? null }); lastBomb = null; // round-reset @@ -299,7 +620,7 @@ app.post(GSI_PATH, (req, res) => { const pos = parseVec3(p.position); const fwd = parseVec3(p.forward); const { yaw, pitch } = forwardToYawPitch(fwd); - const eye = { x: pos.x, y: pos.y, z: pos.z + 64 }; // grobe Augenhöhe + const eye = { x: pos.x, y: pos.y, z: pos.z + 64 }; const { all: weapons, active } = normalizeWeapons(p.weapons); @@ -334,7 +655,7 @@ app.post(GSI_PATH, (req, res) => { if (!presentIds.has(sid)) lastBySteam.delete(sid); } - // 4) Grenades roh (du kannst bei Bedarf normalisieren) + // 4) Grenades normalisieren (inkl. Smoke-EffectTime-Logik) const grenades = normalizeGrenadesFromGSI(body?.grenades || {}, Date.now()); // 5) Bombe normalisieren + Events bei Statuswechsel @@ -342,19 +663,14 @@ app.post(GSI_PATH, (req, res) => { if (!sameBomb(bomb, lastBomb)) { if (bomb) { const prev = lastBomb; - // echte Plant-Transition if (bomb.status === "planted" && (!prev || prev.status !== "planted")) { broadcast({ type: "bomb_planted", bomb }); - // Defuse begonnen } else if (bomb.status === "defusing" && (!prev || prev.status !== "defusing")) { broadcast({ type: "bomb_begindefuse", bomb }); - // Defuse abgebrochen (zurück zu planted) } else if (prev && prev.status === "defusing" && bomb.status === "planted") { broadcast({ type: "bomb_abortdefuse", bomb }); - // Erfolgreich entschärft } else if (bomb.status === "defused" && (!prev || prev.status !== "defused")) { broadcast({ type: "bomb_defused", bomb }); - // Drop / Pickup } else if (bomb.status === "dropped") { broadcast({ type: "bomb_dropped", bomb }); } else if (bomb.status === "carried" && prev && prev.status === "dropped") { @@ -368,7 +684,6 @@ app.post(GSI_PATH, (req, res) => { lastBomb = bomb || null; } - // 6) Tick mit kompletter Momentaufnahme (inkl. bomb) broadcast({ type: "tick", @@ -376,7 +691,7 @@ app.post(GSI_PATH, (req, res) => { map: currentMap, phase: phaseInfo, players, // vollständiger Satz aller Spieler dieses Snapshots - grenades, // roh + grenades, // normalisiert (inkl. Smoke: effectTimeSec/lifeElapsedMs/lifeLeftMs) bomb // für Client-Rendering der C4 });