diff --git a/html/shop.css b/html/shop.css new file mode 100644 index 0000000..f9da4d3 --- /dev/null +++ b/html/shop.css @@ -0,0 +1,165 @@ +/* html/shop.css */ + +/* Hide helpers */ +#shop-wrap.hidden { display: none; } +.hidden { display: none !important; } + +/* Kill any “black frame” / overlay behind the card */ +html, body { + background: transparent !important; +} + +#shop-wrap { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + pointer-events: auto; + background: transparent !important; + z-index: 50; +} + +/* If shop is open, hide ATM vignette even if wrap accidentally exists */ +#shop-wrap:not(.hidden) ~ #wrap::before { display: none !important; } +#wrap::before { display: none !important; } /* temporary test */ + +/* Card (gang-tinted glass) */ +.shop-card { + width: 520px; + border-radius: 16px; + padding: 18px; + + /* Gang-tinted glass */ + background: linear-gradient( + 180deg, + rgba(var(--gang-rgb, 10, 10, 10), 0.45), + rgba(10,10,10,0.60) + ); + + /* ✅ remove the frame */ + border: none; + outline: none; + box-shadow: 0 10px 26px rgba(var(--gang-rgb, 10, 10, 10), 0.18); + + color: #fff; + backdrop-filter: blur(12px); +} + + +/* Header */ +.shop-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.shop-title { + font-size: 20px; + font-weight: 800; + margin: 0; + line-height: 1.2; +} + +.shop-sub { + font-size: 12px; + opacity: 0.85; + margin-top: 6px; +} + +.shop-balance { + text-align: right; + font-size: 12px; + opacity: 0.9; +} +.shop-balance .num { + font-size: 18px; + font-weight: 800; + margin-top: 2px; +} + +/* Tabs */ +.shop-tabs { + display: flex; + gap: 8px; + margin: 12px 0 10px; +} + +.shop-tab { + border: 1px solid rgba(255,255,255,0.14); + background: rgba(255,255,255,0.06); + color: #fff; + padding: 8px 10px; + border-radius: 10px; + cursor: pointer; + font-size: 13px; + user-select: none; +} + +.shop-tab.active { + background: rgba(var(--gang-rgb, 10, 10, 10), 0.28); + border-color: rgba(var(--gang-rgb, 10, 10, 10), 0.55); +} + +/* List */ +.shop-list { + max-height: 360px; + overflow: auto; + border-top: 1px solid rgba(255,255,255,0.10); + padding-top: 10px; +} + +.shop-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid rgba(255,255,255,0.08); +} + +.shop-item-name { font-weight: 700; } + +.shop-item-sub { + font-size: 12px; + opacity: 0.85; + margin-top: 2px; +} + +.shop-price { + font-weight: 800; + margin-right: 10px; + white-space: nowrap; +} + +/* Buttons */ +.shop-buy { + border: 1px solid rgba(255,255,255,0.14); + background: rgba(255,255,255,0.08); + color: #fff; + padding: 8px 12px; + border-radius: 10px; + cursor: pointer; + font-weight: 700; +} +.shop-buy:hover { + background: rgba(255,255,255,0.12); +} + +/* Footer */ +.shop-footer { + display: flex; + justify-content: space-between; + margin-top: 12px; + gap: 10px; + font-size: 12px; + opacity: 0.85; +} + +.shop-close { + cursor: pointer; + text-decoration: underline; + opacity: 0.9; +} diff --git a/html/shop.js b/html/shop.js new file mode 100644 index 0000000..e8ae769 --- /dev/null +++ b/html/shop.js @@ -0,0 +1,249 @@ +// html/shop.js +(() => { + const wrap = document.getElementById("shop-wrap"); + const title = document.getElementById("shop-title"); + const balanceNum = document.getElementById("shop-balance-num"); + + const tabBtns = { + weapons: document.getElementById("shop-tab-weapons"), + ammo: document.getElementById("shop-tab-ammo"), + vehicles: document.getElementById("shop-tab-vehicles"), + }; + + const list = document.getElementById("shop-list"); + const closeBtn = document.getElementById("shop-close"); + + console.log("[turfwar] shop.js loaded"); + + // Guard against missing markup (prevents silent failures) + if ( + !wrap || !title || !balanceNum || !list || !closeBtn || + !tabBtns.weapons || !tabBtns.ammo || !tabBtns.vehicles + ) { + console.error("[turfwar] shop.js missing DOM elements. Check atm.html includes #shop-wrap and tab ids."); + return; + } + + const ALL_TABS = ["weapons", "ammo", "vehicles"]; + + let state = { + open: false, + tab: "weapons", + gangId: 0, + balance: 0, + payload: { weapons: {}, ammo: {}, vehicles: {}, gangs: {} }, + + // NEW: which tabs are allowed for this open instance + allowedTabs: ["weapons", "ammo", "vehicles"], + }; + + function fmtMoney(n) { + n = Number(n) || 0; + return "$" + n.toLocaleString(); + } + + function normalizeAllowedTabs(arr) { + // If not provided, allow all + if (!Array.isArray(arr) || arr.length === 0) return [...ALL_TABS]; + + // Keep only known tabs, unique, in ALL_TABS order + const set = new Set(arr.map(String)); + return ALL_TABS.filter(t => set.has(t)); + } + + function firstAllowedTab() { + return (state.allowedTabs && state.allowedTabs[0]) ? state.allowedTabs[0] : "weapons"; + } + + function applyAllowedTabsUI() { + // Hide / show tab buttons + for (const t of ALL_TABS) { + const btn = tabBtns[t]; + const ok = state.allowedTabs.includes(t); + btn.classList.toggle("hidden", !ok); + // Also prevent focusing hidden buttons + btn.tabIndex = ok ? 0 : -1; + } + + // If current tab not allowed, force to first allowed + if (!state.allowedTabs.includes(state.tab)) { + state.tab = firstAllowedTab(); + } + } + + function setActiveTab(tab) { + tab = String(tab || ""); + + // Enforce allowed tabs + if (!state.allowedTabs.includes(tab)) { + tab = firstAllowedTab(); + } + + state.tab = tab; + Object.keys(tabBtns).forEach((k) => tabBtns[k].classList.toggle("active", k === tab)); + renderList(); + } + + function renderList() { + list.innerHTML = ""; + + // Enforce allowed tab again (in case something changed) + if (!state.allowedTabs.includes(state.tab)) { + setActiveTab(firstAllowedTab()); + return; + } + + const catalog = state.payload[state.tab] || {}; + const entries = Object.entries(catalog); + + if (entries.length === 0) { + const empty = document.createElement("div"); + empty.style.padding = "10px 0"; + empty.style.opacity = "0.8"; + empty.textContent = "No items configured."; + list.appendChild(empty); + return; + } + + for (const [itemId, it] of entries) { + const row = document.createElement("div"); + row.className = "shop-row"; + + const left = document.createElement("div"); + + const name = document.createElement("div"); + name.className = "shop-item-name"; + name.textContent = it.label || itemId; + + const sub = document.createElement("div"); + sub.className = "shop-item-sub"; + + if (state.tab === "vehicles" && itemId === "gangveh") { + const gName = + (state.payload.gangs && + state.payload.gangs[state.gangId] && + state.payload.gangs[state.gangId].name) || + "Your Gang"; + sub.textContent = `Free gang vehicle for ${gName}`; + } else { + sub.textContent = `ID: ${itemId}`; + } + + left.appendChild(name); + left.appendChild(sub); + + const right = document.createElement("div"); + right.style.display = "flex"; + right.style.alignItems = "center"; + + const price = document.createElement("div"); + price.className = "shop-price"; + price.textContent = fmtMoney(it.price || 0); + + const buy = document.createElement("button"); + buy.className = "shop-buy"; + buy.type = "button"; + buy.textContent = "Buy"; + buy.onclick = () => doBuy(state.tab, itemId); + + right.appendChild(price); + right.appendChild(buy); + + row.appendChild(left); + row.appendChild(right); + + list.appendChild(row); + } + } + + function post(name, data) { + return fetch(`https://${GetParentResourceName()}/${name}`, { + method: "POST", + headers: { "Content-Type": "application/json; charset=UTF-8" }, + body: JSON.stringify(data || {}), + }).catch((err) => { + console.error("[turfwar] NUI post failed:", name, err); + }); + } + + function doBuy(tab, itemId) { + // Hard enforce allowed tabs (security-ish) + if (!state.allowedTabs.includes(tab)) { + console.warn("[turfwar] blocked buy from disallowed tab:", tab, itemId); + return; + } + + if (tab === "weapons") return post("shop:buyWeapon", { itemId }); + if (tab === "ammo") return post("shop:buyAmmo", { itemId }); + if (tab === "vehicles") return post("shop:buyVehicle", { itemId }); + } + + function open(msg) { + state.open = true; + + state.gangId = Number(msg.gangId) || 0; + state.payload = msg.payload || state.payload; + + // NEW: allowed tabs + state.allowedTabs = normalizeAllowedTabs(msg.allowedTabs); + + // requested tab (will be forced to allowed) + state.tab = String(msg.tab || "weapons"); + + title.textContent = "Gang Shop"; + balanceNum.textContent = fmtMoney(state.balance); + + // apply allowed tab UI before setting active tab + applyAllowedTabsUI(); + + wrap.classList.remove("hidden"); + setActiveTab(state.tab); + } + + function close() { + state.open = false; + wrap.classList.add("hidden"); + post("shop:close", {}); + } + + // Buttons (they’ll be hidden when not allowed) + tabBtns.weapons.addEventListener("click", () => setActiveTab("weapons")); + tabBtns.ammo.addEventListener("click", () => setActiveTab("ammo")); + tabBtns.vehicles.addEventListener("click", () => setActiveTab("vehicles")); + closeBtn.addEventListener("click", close); + + // ESC closes +window.addEventListener("message", (e) => { + if (e.data.type === "shop:open") { + const rgb = e.data.gangRGB || { r: 10, g: 10, b: 10 } + document.documentElement.style.setProperty( + "--gang-rgb", + `${rgb.r}, ${rgb.g}, ${rgb.b}` + ) + } +}) + + // Receive messages from Lua + window.addEventListener("message", (event) => { + const msg = event.data; + if (!msg || !msg.type) return; + + if (msg.type === "shop:open") { + console.log("[turfwar] shop:open", msg); + open(msg); + + } else if (msg.type === "shop:close") { + wrap.classList.add("hidden"); + state.open = false; + + } else if (msg.type === "shop:balance") { + state.balance = Number(msg.balance) || 0; + state.gangId = Number(msg.gangId) || state.gangId; + balanceNum.textContent = fmtMoney(state.balance); + + } else if (msg.type === "shop:gang") { + state.gangId = Number(msg.gangId) || 0; + renderList(); + } + }); +})();