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;
}
});