This commit is contained in:
Linrador 2025-09-09 23:32:22 +02:00
parent 4e0acf9e7c
commit 02fc0aa4ee

115
server.js
View File

@ -101,6 +101,81 @@ function parseVec3(str) {
return { x, y, z };
}
function pickVec3Any(pos) {
if (!pos) return { x: 0, y: 0, z: 0 };
if (Array.isArray(pos)) return { x: +pos[0]||0, y: +pos[1]||0, z: +pos[2]||0 };
if (typeof pos === 'string') return parseVec3(pos);
return { x: +pos.x||0, y: +pos.y||0, z: +pos.z||0 };
}
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 },
};
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';
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 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);
}
return out;
}
function forwardToYawPitch(fwd) {
const yaw = Math.atan2(fwd.y || 0, fwd.x || 0) * 180 / Math.PI;
const z = Math.max(-1, Math.min(1, fwd.z || 0));
@ -141,20 +216,27 @@ function normalizeBombFromGSI(body) {
: { x: Number(b?.x) || 0, y: Number(b?.y) || 0, z: Number(b?.z) || 0 };
let raw = String(b?.state || b?.status || "").toLowerCase();
let status = "unknown"; // 'planted'|'dropped'|'carried'|'unknown'
if (raw.includes("plant")) status = "planted";
else if (raw.includes("drop")) status = "dropped";
else if (raw.includes("carry")) status = "carried";
let status = "unknown";
if (raw.includes("planted")) status = "planted";
else if (raw.includes("defusing")) status = "defusing";
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?.dropped === true) status = "dropped";
if (b?.carried === true) status = "carried";
if (b?.planted === true) status = "planted";
if (b?.defusing === true) status = "defusing";
if (b?.defused === true) status = "defused";
if (b?.dropped === true) status = "dropped";
if (b?.carried === true) status = "carried";
if (!Number.isFinite(pos.x) || !Number.isFinite(pos.y)) return null;
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;
@ -253,17 +335,29 @@ app.post(GSI_PATH, (req, res) => {
}
// 4) Grenades roh (du kannst bei Bedarf normalisieren)
const grenades = body?.grenades || {};
const grenades = normalizeGrenadesFromGSI(body?.grenades || {}, Date.now());
// 5) Bombe normalisieren + Events bei Statuswechsel
const bomb = normalizeBombFromGSI(body);
if (!sameBomb(bomb, lastBomb)) {
if (bomb) {
if (bomb.status === "planted" && (!lastBomb || lastBomb.status !== "planted")) {
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" && lastBomb && lastBomb.status === "dropped") {
} else if (bomb.status === "carried" && prev && prev.status === "dropped") {
broadcast({ type: "bomb_pickup", bomb });
} else {
broadcast({ type: "bomb", bomb });
@ -274,6 +368,7 @@ app.post(GSI_PATH, (req, res) => {
lastBomb = bomb || null;
}
// 6) Tick mit kompletter Momentaufnahme (inkl. bomb)
broadcast({
type: "tick",