diff --git a/html/atm.css b/html/atm.css
new file mode 100644
index 0000000..86a7acb
--- /dev/null
+++ b/html/atm.css
@@ -0,0 +1,349 @@
+html, body {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ background: transparent;
+ font-family: Arial, Helvetica, sans-serif;
+ overflow: hidden;
+}
+
+.hidden { display: none !important; }
+
+/* Stop clicks leaking into the game world */
+body { pointer-events: none; }
+#wrap, #wrap * { pointer-events: auto; }
+
+/* Fullscreen wrapper that centers the ATM card */
+/* Fullscreen wrapper that centers the ATM card */
+#wrap {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* ✅ Only draw the ATM vignette when ATM is visible (not hidden) */
+#wrap:not(.hidden)::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(
+ circle at 50% 40%,
+ rgba(0,0,0,0.25),
+ rgba(0,0,0,0.65)
+ );
+}
+
+/* ATM card */
+.card {
+ position: relative;
+ z-index: 10;
+
+ width: 420px;
+ border-radius: 14px;
+ padding: 18px;
+
+ color: #e7f2ff;
+
+ background: linear-gradient(180deg, rgba(28, 40, 52, 0.95), rgba(10, 14, 18, 0.92));
+ border: 1px solid rgba(130, 190, 255, 0.25);
+
+ backdrop-filter: blur(8px);
+ box-shadow:
+ 0 0 0 1px rgba(255,255,255,0.06),
+ 0 25px 70px rgba(0,0,0,0.85);
+}
+
+/* Header */
+.title {
+ font-size: 20px;
+ font-weight: 700;
+ margin-bottom: 12px;
+ text-align: center;
+ letter-spacing: 1px;
+ color: rgba(160, 210, 255, 0.95);
+}
+
+/* LCD balance panel */
+.balances {
+ margin-bottom: 14px;
+
+ border-radius: 12px;
+ padding: 12px 14px;
+
+ background: linear-gradient(180deg, rgba(10, 18, 26, 0.88), rgba(6, 10, 14, 0.78));
+ border: 1px solid rgba(120, 200, 255, 0.18);
+ box-shadow: inset 0 0 22px rgba(0,0,0,0.65);
+}
+
+.row {
+ display: flex;
+ justify-content: space-between;
+ padding: 10px 0;
+ border-bottom: 1px solid rgba(255,255,255,0.08);
+}
+
+.row strong {
+ color: rgba(155, 231, 255, 0.95);
+ font-size: 18px;
+}
+
+/* Actions area */
+.actions {
+ margin-top: 12px;
+ display: grid;
+ gap: 10px;
+}
+
+/* Input */
+input {
+ width: 100%;
+ padding: 10px 12px;
+ border-radius: 10px;
+
+ border: 1px solid rgba(120, 200, 255, 0.14);
+ background: rgba(0,0,0,0.38);
+
+ color: #eaf4ff;
+ outline: none;
+
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.55);
+}
+
+input:focus {
+ border-color: rgba(140, 220, 255, 0.35);
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.55), 0 0 0 2px rgba(100, 180, 255, 0.15);
+}
+
+/* Buttons */
+.btnrow {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+}
+
+button {
+ padding: 10px 12px;
+ border-radius: 10px;
+ border: 0;
+ cursor: pointer;
+ font-weight: 700;
+ color: #fff;
+
+ box-shadow:
+ inset 0 1px 0 rgba(255,255,255,0.18),
+ 0 8px 18px rgba(0,0,0,0.55);
+
+ transition: transform 0.06s ease, filter 0.15s ease;
+}
+
+button:hover { filter: brightness(1.12); }
+button:active { transform: translateY(2px); }
+
+/* Primary actions */
+#btnDeposit { background: linear-gradient(180deg, rgba(0, 180, 90, 0.95), rgba(0, 120, 60, 0.9)); }
+#btnWithdraw { background: linear-gradient(180deg, rgba(0, 130, 255, 0.95), rgba(0, 90, 180, 0.9)); }
+
+/* Exit / secondary */
+.secondary {
+ background: rgba(255,255,255,0.12);
+ color: #fff;
+}
+
+/* Hint text */
+.hint {
+ margin-top: 10px;
+ opacity: 0.8;
+ font-size: 12px;
+}
+
+/* =========================
+ Gang Bank HUD (overlay)
+ ========================= */
+#gangbankHud {
+ position: absolute;
+ bottom: 170px; /* pushed up ~2x HUD height */
+ right: 22px;
+ z-index: 2;
+ pointer-events: none;
+}
+
+.gb-hidden { display: none !important; }
+
+.gb-card {
+ min-width: 190px;
+ padding: 6px 0; /* tighter HUD feel */
+ background: none; /* ✅ no background */
+ border: none; /* ✅ no border */
+ box-shadow: none; /* ✅ no shadow */
+ backdrop-filter: none;
+ color: #fff;
+}
+
+
+/* GTA-ish font stack (safe fallback) */
+.gb-card, .gb-card * {
+ font-family: "ChaletLondonNineteenSixty", "Chalet Comprime Cologne", "ChaletComprime Cologne", Arial, Helvetica, sans-serif;
+ letter-spacing: 0.4px;
+}
+
+.gb-label {
+ font-size: 12px;
+ font-weight: 800;
+ margin-bottom: 4px;
+ color: var(--gang-color, #ffffff); /* ✅ dynamic gang color */
+ text-shadow: 0 0 6px rgba(0,0,0,0.6);
+}
+
+.gb-value {
+ font-size: 20px;
+ font-weight: 800;
+ color: #3cff57; /* cash green */
+ text-shadow: 0 2px 10px rgba(0,0,0,0.35);
+}
+
+/* Pulse animation */
+@keyframes gbPulse {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.06); }
+ 100% { transform: scale(1); }
+}
+.gb-pulse { animation: gbPulse 0.25s ease; }
+
+/* =========================
+ Turf Capture HUD (overlay)
+ ========================= */
+#captureHud {
+ position: absolute;
+ left: 50%;
+ bottom: 10%;
+ transform: translateX(-50%);
+ z-index: 3;
+ pointer-events: none; /* never blocks input */
+}
+
+.cap-hidden { display: none !important; }
+
+.cap-card {
+ width: 360px;
+ padding: 12px 14px;
+ border-radius: 14px;
+
+ background: rgba(10, 10, 10, 0.60);
+ border: 1px solid rgba(255,255,255,0.12);
+ color: #fff;
+ backdrop-filter: blur(8px);
+}
+
+.cap-card, .cap-card * {
+ font-family: "ChaletLondonNineteenSixty", "Chalet Comprime Cologne", "ChaletComprime Cologne", Arial, Helvetica, sans-serif;
+ letter-spacing: 0.4px;
+}
+
+.cap-title {
+ font-size: 14px;
+ font-weight: 800;
+ opacity: 0.95;
+ margin-bottom: 8px;
+}
+
+.cap-bar {
+ height: 12px;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.14);
+ overflow: hidden;
+}
+
+.cap-fill {
+ height: 100%;
+ width: 0%;
+ background: rgba(255,255,255,0.85);
+ transition: width 80ms linear;
+}
+
+.cap-sub {
+ margin-top: 7px;
+ font-size: 12px;
+ opacity: 0.85;
+}
+
+/* =========================
+ ✅ Leaderboard HUD (overlay)
+ ========================= */
+#leaderboardHud {
+ position: absolute;
+ top: 12%;
+ right: 22px;
+ z-index: 4;
+ pointer-events: none;
+}
+
+.lb-hidden { display: none !important; }
+
+.lb-card {
+ width: 260px;
+ padding: 6px 0; /* tighter, cleaner */
+ background: none; /* ✅ fully transparent */
+ border: none; /* ✅ no border */
+ box-shadow: none; /* ✅ no shadow */
+ backdrop-filter: none;
+ color: #fff;
+}
+
+
+.lb-card, .lb-card * {
+ font-family: "ChaletLondonNineteenSixty", "Chalet Comprime Cologne", "ChaletComprime Cologne", Arial, Helvetica, sans-serif;
+ letter-spacing: 0.4px;
+}
+
+.lb-title {
+ font-size: 14px;
+ font-weight: 900;
+ opacity: 0.95;
+ margin-bottom: 10px;
+}
+
+.lb-error {
+ margin-bottom: 10px;
+ padding: 8px 10px;
+ border-radius: 10px;
+ background: rgba(255, 80, 80, 0.18);
+ border: 1px solid rgba(255, 80, 80, 0.35);
+ font-size: 12px;
+ opacity: 0.95;
+}
+
+.lb-rows .lb-row {
+ display: flex;
+ justify-content: space-between;
+ padding: 6px 0;
+ border-bottom: none; /* cleaner HUD look */
+ font-size: 13px;
+}
+
+
+.lb-rows .lb-row:last-child {
+ border-bottom: 0;
+}
+
+.lb-rank {
+ opacity: 0.8;
+ margin-right: 8px;
+}
+
+.lb-name {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-right: 10px;
+ text-shadow: 0 0 6px rgba(0,0,0,0.6);
+}
+
+.lb-val {
+ font-weight: 900;
+ opacity: 0.95;
+ text-shadow: 0 0 6px rgba(0,0,0,0.6);
+}
diff --git a/html/atm.html b/html/atm.html
new file mode 100644
index 0000000..f0b2ae2
--- /dev/null
+++ b/html/atm.html
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+ turfwar ui
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Fleeca Bank
+
+
+
+ Cash
+ $0
+
+
+ Bank
+ $0
+
+
+
+
+
+
+
+
+
+
+
+
+
Press ESC to close
+
+
+
+
+
+
+
+
+
+
+
Capturing…
+
+
+
+
0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Gang Shop
+
Weapons • Ammo • Vehicles
+
+
+
+
+
+
Weapons
+
Ammo
+
Vehicles
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/html/atm.js b/html/atm.js
new file mode 100644
index 0000000..55d38ff
--- /dev/null
+++ b/html/atm.js
@@ -0,0 +1,384 @@
+console.log("[ATM UI] atm.js loaded");
+console.log("[NUI] loaded");
+
+// =====================
+// DOM refs
+// =====================
+const wrap = document.getElementById("wrap");
+const cashEl = document.getElementById("cash");
+const bankEl = document.getElementById("bank");
+const amountEl = document.getElementById("amount");
+
+const btnDeposit = document.getElementById("btnDeposit");
+const btnWithdraw = document.getElementById("btnWithdraw");
+const btnClose = document.getElementById("btnClose");
+
+// Gang Bank HUD
+const gbHud = document.getElementById("gangbankHud");
+const gbAmount = document.getElementById("gbAmount");
+const gbLabel = document.getElementById("gbLabel");
+
+// Capture HUD
+const capHud = document.getElementById("captureHud");
+const capTitle = document.getElementById("capTitle");
+const capFill = document.getElementById("capFill");
+const capSub = document.getElementById("capSub");
+const capBar = document.getElementById("capBar");
+
+// Leaderboard HUD
+const lbHud = document.getElementById("leaderboardHud");
+const lbTitle = document.getElementById("lbTitle");
+const lbRows = document.getElementById("lbRows");
+const lbError = document.getElementById("lbError");
+
+// =====================
+// State
+// =====================
+let lastGangBank = null;
+
+// =====================
+// Helpers
+// =====================
+function fmt(n) {
+ n = Number(n || 0);
+ if (!Number.isFinite(n)) n = 0;
+ return "$" + Math.floor(n).toLocaleString();
+}
+
+function post(name, data = {}) {
+ try {
+ fetch(`https://${GetParentResourceName()}/${name}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json; charset=UTF-8" },
+ body: JSON.stringify(data)
+ }).catch(() => {});
+ } catch (e) {
+ console.log("[ATM UI] post failed:", name, e);
+ }
+}
+
+function getAmount() {
+ const v = Number(amountEl?.value || 0);
+ if (!Number.isFinite(v)) return 0;
+ return Math.floor(v);
+}
+
+// =====================
+// ATM UI
+// =====================
+function hideATM() {
+ wrap?.classList.add("hidden");
+}
+
+function showATM() {
+ wrap?.classList.remove("hidden");
+ setTimeout(() => amountEl?.focus(), 0);
+}
+
+// ========================
+// Turf Capture HUD (fixed)
+// ========================
+function capShow() {
+ capHud?.classList.remove("cap-hidden");
+}
+
+function capHide() {
+ capHud?.classList.add("cap-hidden");
+ if (capFill) capFill.style.width = "0%";
+ if (capSub) capSub.textContent = "0%";
+}
+
+function capStart(titleText) {
+ capShow();
+ if (capTitle) capTitle.textContent = titleText || "Capturing…";
+ capSet(0);
+}
+
+function capSet(t) {
+ t = Math.max(0, Math.min(1, Number(t || 0)));
+ const pct = Math.round(t * 100);
+ if (capFill) capFill.style.width = pct + "%";
+ if (capSub) capSub.textContent = pct + "%";
+}
+
+/**
+ * Capture styling:
+ * - fillCss = contesting/capturing gang
+ * - bgCss = outgoing/current owner gang
+ */
+function capStyle(fillCss, bgCss) {
+ if (capFill && fillCss) capFill.style.background = fillCss;
+ if (capBar && bgCss) capBar.style.background = bgCss;
+}
+
+function capPaused(isPaused) {
+ if (!capSub) return;
+ if (isPaused) capSub.textContent = "PAUSED";
+}
+
+// ========================
+// Leaderboard HUD
+// ========================
+function lbShow() {
+ lbHud?.classList.remove("lb-hidden");
+}
+
+function lbHide() {
+ lbHud?.classList.add("lb-hidden");
+}
+
+function lbRender(payload) {
+ if (!payload) return;
+
+ lbShow();
+
+ if (lbTitle) lbTitle.textContent = String(payload.title || "Most influence");
+
+ // Error display (optional)
+ if (payload.error) {
+ if (lbError) {
+ lbError.textContent = String(payload.error);
+ lbError.classList.remove("lb-hidden");
+ }
+ } else {
+ lbError?.classList.add("lb-hidden");
+ if (lbError) lbError.textContent = "";
+ }
+
+ if (!lbRows) return;
+ lbRows.innerHTML = "";
+
+ const rows = Array.isArray(payload.rows) ? payload.rows : [];
+
+ rows.forEach((r) => {
+ const gangId = Number(r.gangId);
+ const name = String(r.name ?? "Unknown");
+ const val = Number(r.value ?? 0);
+
+ // 🚫 Skip Police
+ if (gangId === 3) return;
+
+ // --- Color handling ---
+ const rgb = Array.isArray(r.rgb) ? r.rgb : [255, 255, 255];
+
+ let R = Number(rgb[0]);
+ let G = Number(rgb[1]);
+ let B = Number(rgb[2]);
+
+ // Fallback safety
+ if (!Number.isFinite(R)) R = 255;
+ if (!Number.isFinite(G)) G = 255;
+ if (!Number.isFinite(B)) B = 255;
+
+ // Brighten very dark colors only (Lost Gang)
+ if (R < 80 && G < 80 && B < 80) {
+ R = Math.min(255, R + 80);
+ G = Math.min(255, G + 80);
+ B = Math.min(255, B + 80);
+ }
+
+ const div = document.createElement("div");
+ div.className = "lb-row";
+
+ div.innerHTML = `
+
+ ${escapeHtml(name)}
+
+ ${Number.isFinite(val) ? val : 0}
+ `;
+
+ lbRows.appendChild(div);
+ });
+}
+
+function escapeHtml(s) {
+ return String(s)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function setGangBankColor(rgb) {
+ if (!gbLabel) return;
+
+ const arr = Array.isArray(rgb) ? rgb : [255, 255, 255];
+
+ let R = Number(arr[0]);
+ let G = Number(arr[1]);
+ let B = Number(arr[2]);
+
+ if (!Number.isFinite(R)) R = 255;
+ if (!Number.isFinite(G)) G = 255;
+ if (!Number.isFinite(B)) B = 255;
+
+ // Brighten very dark colors (e.g. Lost Gang)
+ if (R < 80 && G < 80 && B < 80) {
+ R = Math.min(255, R + 80);
+ G = Math.min(255, G + 80);
+ B = Math.min(255, B + 80);
+ }
+
+ const css = `rgb(${R}, ${G}, ${B})`;
+
+ // Apply to label (and optionally amount if you want later)
+ gbLabel.style.setProperty("--gang-color", css);
+ gbLabel.style.color = "var(--gang-color)"; // ensures it shows even if CSS missing
+}
+
+
+
+// =====================
+// Boot
+// =====================
+document.addEventListener("DOMContentLoaded", () => {
+ hideATM();
+ capHide(); // ✅ was capStop() in your file (but didn’t exist)
+ lbHide(); // start hidden until first update
+
+ console.log("[ATM UI] DOMContentLoaded -> posting atm:ready");
+ post("atm:ready");
+});
+
+// =====================
+// Button wiring
+// =====================
+btnDeposit?.addEventListener("click", () => {
+ const amount = getAmount();
+ if (amount > 0) post("atm:deposit", { amount });
+});
+
+btnWithdraw?.addEventListener("click", () => {
+ const amount = getAmount();
+ if (amount > 0) post("atm:withdraw", { amount });
+});
+
+btnClose?.addEventListener("click", () => post("atm:close"));
+
+document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") post("atm:close");
+});
+
+// =====================
+// NUI messages (single listener)
+// =====================
+window.addEventListener("message", (event) => {
+ const data = event.data || {};
+ const type = data.type;
+ if (!type) return;
+
+ // Capture debug (optional)
+ if (type.startsWith("capture:")) {
+ // console.log("[NUI] capture msg", data);
+ }
+
+ switch (type) {
+ // -------- ATM --------
+ case "atm:open":
+ showATM();
+ break;
+
+ case "atm:close":
+ hideATM();
+ break;
+
+ case "atm:balances":
+ if (cashEl) cashEl.textContent = fmt(data.cash);
+ if (bankEl) bankEl.textContent = fmt(data.bank);
+ break;
+
+// ---- Gang Bank HUD ----
+case "gangbank:show":
+ gbHud?.classList.remove("gb-hidden");
+ break;
+
+case "gangbank:hide":
+ gbHud?.classList.add("gb-hidden");
+ break;
+
+case "gangbank:label": {
+ if (gbLabel) gbLabel.textContent = String(data.label || "Gang Bank");
+
+ // ✅ set color if provided
+ if (data.rgb) setGangBankColor(data.rgb);
+
+ break;
+}
+
+case "gangbank:update": {
+ if (!gbAmount) break;
+
+ const amt = Number(data.balance ?? data.amount ?? 0);
+ gbAmount.textContent = fmt(amt);
+
+ // ✅ also accept rgb here (in case update is the only message carrying it)
+ if (data.rgb) setGangBankColor(data.rgb);
+
+ // pulse only if changed
+ if (lastGangBank !== null && amt !== lastGangBank) {
+ gbAmount.classList.remove("gb-pulse");
+ void gbAmount.offsetWidth; // restart animation
+ gbAmount.classList.add("gb-pulse");
+ }
+ lastGangBank = amt;
+ break;
+}
+
+ // ---- Turf Capture HUD ----
+ case "capture:start":
+ capStart(data.turfName || data.title || "Capturing…");
+ if (data.t !== undefined) capSet(data.t);
+ if (data.fill || data.bg) capStyle(data.fill, data.bg);
+ break;
+
+ case "capture:set":
+ capShow();
+ capSet(data.t);
+ break;
+
+ case "capture:style":
+ capStyle(data.fill, data.bg);
+ break;
+
+ case "capture:paused":
+ capPaused(!!data.paused);
+ break;
+
+ case "capture:hint":
+ if (capSub && data.text) {
+ capSub.textContent = String(data.text);
+ setTimeout(() => {
+ // only clear if still showing the same hint-like text
+ if (capSub && capSub.textContent === String(data.text)) {
+ capSub.textContent = "";
+ }
+ }, 1500);
+ }
+ break;
+
+ case "capture:stop":
+ capHide();
+ break;
+
+ // ---- ✅ Leaderboard ----
+ // We accept either name:
+ // - "leaderboard:update" (client might send this)
+ // - "turfwar:leaderboard:update" (recommended)
+ case "leaderboard:update":
+ case "turfwar:leaderboard:update":
+ lbRender(data.payload || data);
+ break;
+
+ case "leaderboard:hide":
+ lbHide();
+ break;
+
+ case "leaderboard:show":
+ lbShow();
+ break;
+
+ default:
+ break;
+ }
+});
diff --git a/html/killfeed.css b/html/killfeed.css
new file mode 100644
index 0000000..d39fb49
--- /dev/null
+++ b/html/killfeed.css
@@ -0,0 +1,108 @@
+/* =========================================================
+ Killfeed (NUI)
+ - Bottom-left (above minimap)
+ - Stacks UP
+ - Bigger, bold, white text
+ - FULLY TRANSPARENT (no background)
+ ========================================================= */
+
+html, body {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ background: transparent;
+ overflow: hidden;
+}
+
+/* Killfeed container */
+#killfeed {
+ position: absolute;
+ left: 18px;
+ bottom: 250px; /* tweak if needed */
+
+ z-index: 50;
+ pointer-events: none;
+
+ display: flex;
+ flex-direction: column-reverse; /* stack upward */
+
+ gap: 8px;
+ max-width: 460px;
+}
+
+/* Each line */
+.kf-line {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+
+ padding: 0; /* no box feel */
+ border-radius: 0;
+
+ /* ❌ NO BACKGROUND */
+ background: none;
+ backdrop-filter: none;
+ box-shadow: none;
+
+ font-family: "ChaletLondonNineteenSixty",
+ "Chalet Comprime Cologne",
+ "ChaletComprime Cologne",
+ Arial, Helvetica, sans-serif;
+
+ font-size: 16px; /* bigger */
+ font-weight: 800; /* bold */
+ line-height: 1.25;
+
+ color: #ffffff; /* white text */
+
+ /* Strong GTA-style shadow for readability */
+ text-shadow:
+ 0 1px 2px rgba(0,0,0,0.95),
+ 0 0 8px rgba(0,0,0,0.85);
+
+ opacity: 0;
+ transform: translateX(-6px);
+ animation: kfIn 140ms ease forwards;
+}
+
+/* Force all spans to white unless overridden inline */
+.kf-line span {
+ color: #ffffff;
+}
+
+/* Killer & victim names (still gang-colored inline) */
+.kf-killer,
+.kf-victim {
+ font-weight: 900;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 180px;
+
+ text-shadow:
+ 0 1px 2px rgba(0,0,0,0.98),
+ 0 0 8px rgba(0,0,0,0.90);
+}
+
+/* Weapon + extras */
+.kf-weapon,
+.kf-extra {
+ font-size: 14px;
+ font-weight: 800;
+ opacity: 0.98;
+ white-space: nowrap;
+}
+
+/* Fade out */
+.kf-out {
+ animation: kfOut 260ms ease forwards;
+}
+
+@keyframes kfIn {
+ to { opacity: 1; transform: translateX(0); }
+}
+
+@keyframes kfOut {
+ to { opacity: 0; transform: translateX(-8px); }
+}
diff --git a/html/killfeed.js b/html/killfeed.js
new file mode 100644
index 0000000..a0503b4
--- /dev/null
+++ b/html/killfeed.js
@@ -0,0 +1,102 @@
+console.log("[KILLFEED] killfeed.js loaded");
+
+const kfWrap = document.getElementById("killfeed");
+
+function safeText(s) {
+ return String(s ?? "").replace(/[&<>"']/g, (c) => ({
+ "&":"&", "<":"<", ">":">", '"':""", "'":"'"
+ }[c]));
+}
+
+function rgbCss(rgb) {
+ const a = Array.isArray(rgb) ? rgb : [255,255,255];
+ let r = Number(a[0]), g = Number(a[1]), b = Number(a[2]);
+ if (!Number.isFinite(r)) r = 255;
+ if (!Number.isFinite(g)) g = 255;
+ if (!Number.isFinite(b)) b = 255;
+
+ // brighten very dark colors slightly
+ if (r < 70 && g < 70 && b < 70) {
+ r = Math.min(255, r + 80);
+ g = Math.min(255, g + 80);
+ b = Math.min(255, b + 80);
+ }
+ return `rgb(${r},${g},${b})`;
+}
+
+function addKillfeedLine(payload) {
+ if (!kfWrap) return;
+ payload = payload || {};
+
+ // ✅ Death without a killer
+ if (payload.isDeathOnly) {
+ const victim = safeText(payload.victimName || "Unknown");
+ const vCss = rgbCss(payload.victimRgb);
+
+ const line = document.createElement("div");
+ line.className = "kf-line";
+ line.innerHTML = `
+ ${victim}
+ has died
+ `;
+
+ kfWrap.prepend(line);
+
+ const maxLines = 6;
+ while (kfWrap.children.length > maxLines) {
+ kfWrap.lastElementChild?.remove();
+ }
+
+ setTimeout(() => {
+ line.classList.add("kf-out");
+ setTimeout(() => line.remove(), 300);
+ }, 6000);
+
+ return;
+ }
+
+ // ✅ Normal PvP line
+ const killer = safeText(payload.killerName || "Unknown");
+ const victim = safeText(payload.victimName || "Unknown");
+ const weapon = safeText(payload.weapon || "Unknown");
+
+ const extras = [];
+ if (payload.headshot) extras.push("HS");
+ const dist = Number(payload.distance || 0);
+ if (Number.isFinite(dist) && dist > 0.1) extras.push(`${Math.round(dist)}m`);
+ const extraText = extras.length ? `(${extras.join(" · ")})` : "";
+
+ const line = document.createElement("div");
+ line.className = "kf-line";
+
+ const kCss = rgbCss(payload.killerRgb);
+ const vCss = rgbCss(payload.victimRgb);
+
+ line.innerHTML = `
+ ${killer}
+ [${weapon}]
+ ${victim}
+ ${extraText ? `` : ""}
+ `;
+
+ kfWrap.prepend(line);
+
+ const maxLines = 6;
+ while (kfWrap.children.length > maxLines) {
+ kfWrap.lastElementChild?.remove();
+ }
+
+ setTimeout(() => {
+ line.classList.add("kf-out");
+ setTimeout(() => line.remove(), 300);
+ }, 7000);
+}
+
+window.addEventListener("message", (event) => {
+ const data = event.data || {};
+ if (data.type === "killfeed:add") {
+ addKillfeedLine(data);
+ } else if (data.type === "killfeed:clear") {
+ if (kfWrap) kfWrap.innerHTML = "";
+ }
+});