updated
This commit is contained in:
parent
12b297dd32
commit
949ce18a6b
437
server.js
437
server.js
@ -110,70 +110,396 @@ function pickVec3Any(pos) {
|
||||
|
||||
const NADE_DEFAULTS = {
|
||||
smoke: { radius: 150, lifetimeMs: 18_000 },
|
||||
molotov:{ radius: 120, lifetimeMs: 7_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 = [];
|
||||
for (const [k, v] of Object.entries(raw)) {
|
||||
// z.B. "smokes", "smokegrenade_projectile", "inferno", "hegrenade_projectile" ...
|
||||
const kind = mapKind(k);
|
||||
pushList(out, kind, v);
|
||||
|
||||
// --- 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;
|
||||
}
|
||||
return out;
|
||||
|
||||
// 🔸 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)) {
|
||||
const key = String(k).toLowerCase();
|
||||
|
||||
if (key === 'smokegrenade_projectile') {
|
||||
pushSmokeFromProjectiles(v);
|
||||
continue;
|
||||
}
|
||||
if (KNOWN_BUCKETS.has(key)) {
|
||||
pushEffectList(key, v);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user