您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Could I *BE* any more scripts!? (Market Favs, Profit Helper, Inventory Net Worth, Timer Bar, Exploration Data, Auto fill for selling, Store Remaining Amount + Rad ROI Tracker + Durability converter + Market Buy Tracker). Each feature can be toggled from the cog in the header.
// ==UserScript== // @name Zed.City – QOL Update By MathewPerry // @namespace zed.city.aio // @version 1.6.2 // @description Could I *BE* any more scripts!? (Market Favs, Profit Helper, Inventory Net Worth, Timer Bar, Exploration Data, Auto fill for selling, Store Remaining Amount + Rad ROI Tracker + Durability converter + Market Buy Tracker). Each feature can be toggled from the cog in the header. // @author MathewPerry // @match https://www.zed.city/* // @run-at document-idle // @icon https://www.google.com/s2/favicons?sz=64&domain=zed.city // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @license MIT // ==/UserScript== (function() { 'use strict'; const SETTINGS_KEY = 'zed-aio-settings'; const DefaultSettings = { marketFavs: true, profitHelper: true, networth: true, timerBar: true, explorationData: true, marketSelling: true, storeRemainingAmounts: true, radTracker: true, durability: true, marketBuyTracker: true }; function readSettings(){ try { const raw = localStorage.getItem(SETTINGS_KEY); return Object.assign({}, DefaultSettings, raw ? JSON.parse(raw) : {}); } catch(e) { return Object.assign({}, DefaultSettings); } } function writeSettings(s){ localStorage.setItem(SETTINGS_KEY, JSON.stringify(s)); } let SETTINGS = readSettings(); // Find the exact toolbar row and insert Options button as FIRST child (left) function findToolbarRow(){ const exact = document.querySelector('div.q-gutter-xs.row.items-center.no-wrap.col-xs-4.order-xs-first.order-sm-none.col-sm-auto.justify-end'); if (exact) return exact; return document.querySelector('.q-toolbar .row.justify-end') || document.querySelector('.q-header .row.justify-end'); } function mountOptions(){ const row = findToolbarRow(); if (!row) return; if (row.querySelector('.zed-aio-opts-btn')) return; const btn = document.createElement('button'); btn.className = 'zed-aio-opts-btn q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--round text-grey-7 q-btn--actionable q-focusable q-hoverable'; btn.type = 'button'; btn.setAttribute('aria-label', 'Options'); btn.innerHTML = '<span class="q-focus-helper"></span><span class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><i class="q-icon fal fa-cog" aria-hidden="true"></i></span>'; row.insertBefore(btn, row.firstChild); const panel = document.createElement('div'); panel.className = 'zed-aio-opts-panel'; panel.style.cssText = 'position:absolute;z-index:9999;margin-top:8px;padding:10px 12px;border-radius:8px;background:rgba(20,20,20,.98);border:1px solid rgba(255,255,255,.12);box-shadow:0 6px 20px rgba(0,0,0,.35);color:#ddd;font-size:10px;display:none;left:0;transform:translateX(-24px);width:200px;'; panel.innerHTML = [ '<div style="font-weight:200;margin-bottom:8px">Options</div>', '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="marketFavs">Market Favs</label>', '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="profitHelper">Profit Helper</label>', '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="networth">Networth</label>', '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="timerBar">Timer Bar</label>', '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="explorationData">Exploration Data</label>', '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="marketSelling">Auto Fill sell price</label>', '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="storeRemainingAmounts">Shows Store amounts</label>', '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="radTracker">Shows ROI on Rad Spent</label>', '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="durability">Shows durability on items</label>', '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="marketBuyTracker">Shows Market buy history</label>', '<div style="opacity:.75;margin-top:8px">Page will reload to apply.</div>' ].join(''); btn.style.position = 'relative'; btn.appendChild(panel); panel.querySelectorAll('input[type=checkbox][data-k]').forEach(cb => { const k = cb.getAttribute('data-k'); cb.checked = !!SETTINGS[k]; cb.addEventListener('change', () => { SETTINGS[k] = cb.checked; writeSettings(SETTINGS); location.reload(); }); }); let open = false; function show(){ panel.style.display = 'block'; open = true; } function hide(){ panel.style.display = 'none'; open = false; } btn.addEventListener('click', (e) => { e.stopPropagation(); open ? hide() : show(); }); document.addEventListener('click', (e) => { if (open && !btn.contains(e.target)) hide(); }); document.addEventListener('keydown', (e) => { if (open && e.key === 'Escape') hide(); }); } // Retry mount in case SPA header loads late (function retryMount(){ let tries = 0; const t = setInterval(() => { tries++; mountOptions(); if (findToolbarRow() && tries > 1 || tries > 60) clearInterval(t); }, 250); })(); // ---------- Module gates ---------- const RUN_MARKET = !!SETTINGS.marketFavs; const RUN_PROFIT = !!SETTINGS.profitHelper; const RUN_NETWORTH = !!SETTINGS.networth; const RUN_TIMERS = !!SETTINGS.timerBar; const RUN_EXPLORATION = !!SETTINGS.explorationData; const RUN_MARKETSELLING = !!SETTINGS.marketSelling; const RUN_STOREREMAININGAMOUNTS = !!SETTINGS.storeRemainingAmounts; const RUN_RADTRACKER = !!SETTINGS.radTracker; const RUN_DURABILITY = !!SETTINGS.durability; const RUN_MARKETBUYTRACKER = !!SETTINGS.marketBuyTracker; // =============================== // NetBus (hardened, private, no CSRF exposure) // =============================== (function NetBus(){ const FLAG_XHR = '__zedNetBusXHR'; const FLAG_FETCH = '__zedNetBusFetch'; // Private in-closure bus (not on window) const Bus = new EventTarget(); // Compat shim: same API name your modules already use // Returns an unsubscribe function. window.addNetListener = function addNetListener(pattern, handler){ const re = pattern instanceof RegExp ? pattern : new RegExp(String(pattern), 'i'); const listener = (e) => { const { url = '', response, source } = e.detail || {}; if (re.test(url)) handler(url, response, source); }; Bus.addEventListener('net', listener); return () => Bus.removeEventListener('net', listener); }; // Scrub likely secrets before emitting to modules (belt & braces) function scrub(obj){ try { return JSON.parse(JSON.stringify(obj, (k, v) => ( /csrf|token|auth|session|secret|cookie/i.test(k) ? undefined : v ))); } catch { return null; } } // ---- XHR wrapper (guarded) ---- if (!XMLHttpRequest.prototype[FLAG_XHR]) { const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this._zedURL = url; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { this.addEventListener('readystatechange', function(){ if (this.readyState === 4) { try { if (this.responseType && this.responseType !== '' && this.responseType !== 'text') return; const text = this.responseText || ''; const first = text[0]; if (!text || (first !== '{' && first !== '[')) return; // quick JSON gate const json = JSON.parse(text); const clean = scrub(json); // Emit ONLY on the private bus Bus.dispatchEvent(new CustomEvent('net', { detail: { url: this._zedURL || '', response: clean, source: 'xhr' } })); } catch(e){} } }); return originalSend.apply(this, arguments); }; Object.defineProperty(XMLHttpRequest.prototype, FLAG_XHR, { value: true, configurable: false }); } // ---- fetch wrapper (guarded) ---- if (!window[FLAG_FETCH]) { const origFetch = window.fetch; window.fetch = async function(){ const res = await origFetch.apply(this, arguments); try { const clone = res.clone(); const ct = clone.headers.get('content-type') || ''; if (ct.includes('application/json')) { const json = await clone.json(); const clean = scrub(json); const req = arguments[0]; const url = typeof req === 'string' ? req : (req && req.url) || ''; Bus.dispatchEvent(new CustomEvent('net', { detail: { url, response: clean, source: 'fetch' } })); } } catch(e){} return res; }; Object.defineProperty(window, FLAG_FETCH, { value: true, configurable: false }); } })(); //-----Marketdata, always run ------- // =============================== // Config // =============================== const PINNED_ITEM_LIMIT = 22; const MARKET_KEY = "Zed-market-data"; const HISTORY_KEY = "Zed-market-data-history"; const PINNED_KEY = "Zed-pinned-items"; const COLLAPSE_KEY = "Zed-pinned-collapsed"; const LAST_SUCCESS_KEY = "Zed-market-last-success";// last successful save (ms) const NEXT_ALLOWED_KEY = "Zed-market-next-allowed";// earliest next attempt (ms) const HISTORY_LIMIT = 50; const STALE_MS = 10 * 60 * 1000; const POLL_MS = 60 * 1000; const RETRY_MIN_MS = 5 * 1000;// at least 5s between retries const RETRY_MAX_MS = 5 * 60 * 1000;// cap at 5m const DEBUG = false; // Compact UI tuning const SPARK_W = 100; const SPARK_H = 22; const isCollapsed = () => localStorage.getItem(COLLAPSE_KEY) === "1"; const setCollapsed = (v) => localStorage.setItem(COLLAPSE_KEY, v ? "1" : "0"); const log = (...a) => DEBUG && console.log("[zed-market-data]", ...a); // Init stores if missing for (const k of [MARKET_KEY, HISTORY_KEY]) { if (!localStorage.getItem(k)) localStorage.setItem(k, JSON.stringify({})); } // =============================== // Utils // =============================== const normalizeName = (name) => (name || "").replace(/[★☆]/g, "").trim().toLowerCase(); const getPinnedItems = () => [...new Set(JSON.parse(localStorage.getItem(PINNED_KEY) || "[]").map(normalizeName))]; const setPinnedItems = (items) => localStorage.setItem(PINNED_KEY, JSON.stringify([...new Set(items.map(normalizeName))])); const getMarket = () => JSON.parse(localStorage.getItem(MARKET_KEY) || "{}"); const setMarket = (obj) => localStorage.setItem(MARKET_KEY, JSON.stringify(obj || {})); const getHistory = () => JSON.parse(localStorage.getItem(HISTORY_KEY) || "{}"); const setHistory = (obj) => localStorage.setItem(HISTORY_KEY, JSON.stringify(obj || {})); function appendHistory(name, price) { try { const now = Date.now(); const bag = getHistory(); const k = normalizeName(name); const cur = bag[k] || { v: 2, h: [] }; const last = cur.h.at?.(-1); const lastTs = Number(last?.[0] || 0); const lastVal = Number(last?.[1]); // append if price changed OR last update was > 60 minutes ago if (!last || lastVal !== Number(price) || (now - lastTs) > 60 * 60 * 1000) { cur.h.push([now, Number(price)]); if (cur.h.length > HISTORY_LIMIT) cur.h = cur.h.slice(-HISTORY_LIMIT); bag[k] = cur; setHistory(bag); } } catch (e) { console.error("[zed-market-data] appendHistory failed:", e); } } function extractItems(apiResponse) { if (!apiResponse) return null; if (Array.isArray(apiResponse.items)) return apiResponse.items; if (Array.isArray(apiResponse)) return apiResponse; // tolerate bare array if (apiResponse.data && Array.isArray(apiResponse.data.items)) return apiResponse.data.items; return null; } // =============================== // Capture market prices (XHR + fetch hooks) // =============================== const _open = XMLHttpRequest.prototype.open; const _send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this._zed_url = url; return _open.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { this.addEventListener("readystatechange", function () { if (this.readyState === 4) { try { const url = this._zed_url || ""; if (typeof url === "string" && url.includes("getMarket") && !url.includes("getMarketUser")) { const resp = JSON.parse(this.responseText); saveMarketPrices(resp); } // Also capture stats for instant timers if (typeof url === "string" && url.includes("getStats")) { try { const stats = JSON.parse(this.responseText); if (window.zedApplyStats) window.zedApplyStats(stats); } catch(_) {} } } catch (_) {} } }); return _send.apply(this, arguments); }; const _origFetch = window.fetch; window.fetch = async function (...args) { const res = await _origFetch.apply(this, args); try { const req = args[0]; const url = typeof req === "string" ? req : (req && req.url) || ""; if (url.includes("getMarket") && !url.includes("getMarketUser")) { const clone = res.clone(); const json = await clone.json(); saveMarketPrices(json); } } catch (_) {} return res; }; // =============================== // Active poller with backoff // =============================== let pollInFlight = false; let backoffMs = RETRY_MIN_MS; function scheduleNextAllowed(delayMs) { const when = Date.now() + delayMs; localStorage.setItem(NEXT_ALLOWED_KEY, String(when)); return when; } async function pollMarketOnce() { if (pollInFlight) return; pollInFlight = true; try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); const res = await _origFetch("https://api.zed.city/getMarket", { signal: controller.signal, cache: "no-store", mode: "cors", credentials: "include", }); clearTimeout(timeout); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); saveMarketPrices(json); // success → reset backoff & schedule next at POLL_MS backoffMs = RETRY_MIN_MS; scheduleNextAllowed(POLL_MS); } catch (e) { log("poll error:", e?.message || e); // failure → exponential backoff (min 5s) + jitter const jitter = Math.floor(Math.random() * 1000); backoffMs = Math.min(Math.max(backoffMs * 2, RETRY_MIN_MS), RETRY_MAX_MS); scheduleNextAllowed(backoffMs + jitter); } finally { pollInFlight = false; } } function startMarketPoller() { if (!localStorage.getItem(NEXT_ALLOWED_KEY)) { localStorage.setItem(NEXT_ALLOWED_KEY, "0"); } const now = Date.now(); const nextAllowed = Number(localStorage.getItem(NEXT_ALLOWED_KEY) || 0); const lastSuccess = Number(localStorage.getItem(LAST_SUCCESS_KEY) || 0); if (now >= nextAllowed && (now - lastSuccess >= POLL_MS)) { pollMarketOnce(); } // 1s tick to decide whether we can poll setInterval(() => { const now = Date.now(); const nextAllowed = Number(localStorage.getItem(NEXT_ALLOWED_KEY) || 0); const lastSuccess = Number(localStorage.getItem(LAST_SUCCESS_KEY) || 0); if (now >= nextAllowed && (now - lastSuccess >= POLL_MS)) { pollMarketOnce(); } }, 1000); document.addEventListener("visibilitychange", () => { if (!document.hidden) { const now = Date.now(); const nextAllowed = Number(localStorage.getItem(NEXT_ALLOWED_KEY) || 0); const lastSuccess = Number(localStorage.getItem(LAST_SUCCESS_KEY) || 0); if (now >= nextAllowed && (now - lastSuccess >= POLL_MS)) { pollMarketOnce(); } } }); } function saveMarketPrices(apiResponse) { try { const items = extractItems(apiResponse); if (!items) return; const market = getMarket(); for (const item of items) { if (!item || typeof item.name !== "string") continue; const name = item.name; const price = Number(item.market_price); if (!Number.isFinite(price)) continue; market[name] = price; appendHistory(name, price); } setMarket(market); const now = Date.now(); localStorage.setItem(LAST_SUCCESS_KEY, String(now)); // After success, next attempt is after POLL_MS localStorage.setItem(NEXT_ALLOWED_KEY, String(now + POLL_MS)); window.dispatchEvent(new CustomEvent("zed:marketDataUpdated", { detail: now })); } catch (e) { console.error("[zed-market-data] saveMarketPrices failed:", e); } } // ---------- Market Favs ---------- function run_MarketFavs(){ (function () { // =============================== // UI helpers // =============================== function getMarketHost() { return ( document.querySelector(".zed-grid.has-title.has-content") || document.querySelector(".zed-grid.has-content") || document.querySelector(".zed-grid") || document.querySelector(".q-page-container") || document.querySelector(".q-px-xs") ); } function findNavShell() { // inner row with the <a> tabs const tabsContent = document.querySelector( ".q-tabs__content.scroll--mobile.row.no-wrap.items-center.self-stretch.hide-scrollbar.relative-position.q-tabs__content--align-center" ); if (!tabsContent) return null; // the q-tabs component const qtabs = tabsContent.closest(".q-tabs"); // the outer bar wrapper you showed (preferred anchor) const shell = qtabs?.closest(".gt-xs.bg-grey-3.text-grey-5.text-h6") || qtabs?.parentElement || // fallback: parent of .q-tabs tabsContent; return shell; } function findStickyAncestor(el) { let cur = el; while (cur && cur !== document.body) { const cs = getComputedStyle(cur); if (cs.position === "sticky" || cs.position === "fixed") return cur; cur = cur.parentElement; } return null; } function ensurePinnedBar() { // the inner row with the <a> tabs const tabsContent = document.querySelector( ".q-tabs__content.scroll--mobile.row.no-wrap.items-center.self-stretch.hide-scrollbar.relative-position.q-tabs__content--align-center" ); if (!tabsContent) return null; // (re)use/create bar let pinnedDiv = document.getElementById("pinnedItems"); if (!pinnedDiv) { pinnedDiv = document.createElement("div"); pinnedDiv.id = "pinnedItems"; pinnedDiv.style = ` background: rgba(0,0,0,0.5); border: 1px solid #666; padding: 5px 0px; margin: 0px 0 0px; border-radius: 0px; color: #fff; font-size: 12px; width: 100%; box-sizing: border-box; display: block; flex: 0 0 100%; align-self: stretch; position: relative; /* normal flow so it scrolls away */ `; } // Find the sticky/fixed ancestor (likely the header) const sticky = findStickyAncestor(tabsContent) || tabsContent.closest(".q-header, .q-layout__header"); // Prefer to insert as the FIRST child of the main scrolling container const pageContainer = (sticky && sticky.nextElementSibling && sticky.nextElementSibling.matches(".q-page-container")) ? sticky.nextElementSibling : document.querySelector(".q-page-container") || document.querySelector(".zed-grid.has-title.has-content, .zed-grid.has-content, .zed-grid") || document.querySelector(".q-page"); // fallbacks if (pageContainer) { // Put our bar at the very top of the scrolled page content (below the header) pageContainer.insertBefore(pinnedDiv, pageContainer.firstChild); pinnedDiv.dataset.anchor = "below-sticky-header"; return pinnedDiv; } // Last resort: place right after the sticky header element itself if (sticky && sticky.parentNode) { sticky.insertAdjacentElement("afterend", pinnedDiv); pinnedDiv.dataset.anchor = "below-sticky-header"; return pinnedDiv; } // If we got here, just append to body (still scrolls, but not ideal layout-wise) if (!pinnedDiv.parentElement) document.body.appendChild(pinnedDiv); return pinnedDiv; } function renderSparkline(el, series, w = SPARK_W, h = SPARK_H, pad = 2) { el.innerHTML = ""; if (!series || series.length < 2) return; const c = document.createElement("canvas"); c.width = w; c.height = h; el.appendChild(c); const ctx = c.getContext("2d"); const ys = series.map(p => Number(p[1])).filter(Number.isFinite); const n = ys.length; if (n < 2) return; const min = Math.min(...ys), max = Math.max(...ys); const span = Math.max(1, max - min); const x = (i) => pad + (i * (w - pad * 2)) / (n - 1); const y = (val) => h - pad - ((val - min) * (h - pad * 2)) / span; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x(0), y(ys[0])); for (let i = 1; i < n; i++) ctx.lineTo(x(i), y(ys[i])); ctx.stroke(); ctx.beginPath(); ctx.arc(x(n - 1), y(ys[n - 1]), 1.5, 0, Math.PI * 2); ctx.fill(); } // =============================== // UI: Pinned bar + toggle // =============================== function renderPinnedItems() { if (document.documentElement.hasAttribute('data-mail')) return; try { const host = getMarketHost(); if (!host) { setTimeout(renderPinnedItems, 500); return; } const pinnedDiv = ensurePinnedBar(host); const pinned = getPinnedItems(); const market = getMarket(); const history = getHistory(); pinnedDiv.innerHTML = ""; // Header const header = document.createElement("div"); header.style = "display:flex;align-items:center;gap:10px;justify-content:space-between;flex-wrap:wrap;"; const leftHdr = document.createElement("div"); leftHdr.style = "display:flex;align-items:center;gap:8px;"; const title = document.createElement("strong"); title.textContent = "⭐ Pinned Items"; const tsSpan = document.createElement("span"); tsSpan.id = "pinned-last-updated"; tsSpan.style = "opacity:.8;font-size:12px;"; leftHdr.appendChild(title); leftHdr.appendChild(tsSpan); const rightHdr = document.createElement("div"); rightHdr.style = "display:flex;align-items:center;gap:8px;"; const tip = document.createElement("div"); tip.textContent = ""; tip.style = "opacity:.8;font-size:12px;"; const toggleBtn = document.createElement("button"); toggleBtn.textContent = isCollapsed() ? "▸" : "▾"; toggleBtn.title = (isCollapsed() ? "Show" : "Hide") + " market ticker"; toggleBtn.style = ` cursor:pointer;border:1px solid #666;border-radius:6px;background:rgba(255,255,255,0.08); color:#fff;font-size:12px;line-height:1;padding:4px 8px; `; toggleBtn.onclick = () => { setCollapsed(!isCollapsed()); renderPinnedItems(); }; rightHdr.appendChild(tip); rightHdr.appendChild(toggleBtn); header.appendChild(leftHdr); header.appendChild(rightHdr); pinnedDiv.appendChild(header); if (pinned.length === 0) { pinnedDiv.appendChild(Object.assign(document.createElement("i"), { textContent: "No pinned items" })); return; } const lastTs = Number(localStorage.getItem(LAST_SUCCESS_KEY) || 0) || Object.values(history).map(v => v?.h?.at?.(-1)?.[0]).filter(Boolean).sort((a, b) => b - a)[0]; if (lastTs) { const age = Date.now() - lastTs; const mins = Math.floor(age / 60000); tsSpan.textContent = `Last updated: ${mins ? `${mins}m` : "just now"} ago`; } else { tsSpan.textContent = `(waiting for market data…)`; } // Collapsed? keep header only. if (isCollapsed()) return; // Rows container (compact) const rows = document.createElement("div"); rows.style = ` display:grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); /* was 260px */ gap: 4px 8px; /* tighter spacing */ margin-top: 6px; /* compact */ width: 100%; `; pinnedDiv.appendChild(rows); for (const pinnedName of pinned) { const displayKey = Object.keys(market).find(k => normalizeName(k) === pinnedName); const displayName = displayKey || pinnedName; const price = displayKey ? market[displayKey] : null; const hist = history[pinnedName] || history[normalizeName(displayName)] || history[normalizeName(pinnedName)]; const series = hist?.h || []; const first = series.length ? series[0][1] : null; const last = series.length ? series.at?.(-1)?.[1] : price; const pct = (first && last) ? ((Number(last) - Number(first)) / Number(first)) * 100 : null; const isUp = pct != null && pct >= 0; const isStale = series.length ? (Date.now() - (series.at?.(-1)?.[0] || 0) > STALE_MS) : true; const row = document.createElement("div"); row.style = ` display:flex; align-items:center; justify-content:space-between; column-gap: 8px; padding:4px 6px; /* compact */ border-radius:6px; border:1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.04); ${isStale ? "opacity:.85;" : ""} `; const left = document.createElement("div"); left.style = "display:flex;align-items:center;gap:6px;min-width:0;flex:1;"; const nameSpan = document.createElement("span"); nameSpan.textContent = displayName; nameSpan.style = ` cursor:pointer; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex: 1 1 50%; /* ensure ~1/2+ width before truncation */ min-width: 45%; `; nameSpan.title = "Click to search this item"; nameSpan.onclick = () => { const input = document.querySelector("input[type='text']"); if (input) { input.value = displayName; input.dispatchEvent(new Event("input", { bubbles: true })); } }; const spark = document.createElement("span"); spark.className = "spark"; spark.style = ` display:inline-block; width:${SPARK_W}px; height:${SPARK_H}px; flex: 0 0 ${SPARK_W}px; `; left.appendChild(nameSpan); left.appendChild(spark); const right = document.createElement("div"); right.style = "display:flex;align-items:center;gap:6px;flex:0 0 auto;"; const priceSpan = document.createElement("span"); priceSpan.textContent = price != null ? `$${Number(price).toLocaleString()}` : "N/A"; priceSpan.style = "font-variant-numeric: tabular-nums;"; const pctSpan = document.createElement("span"); if (pct != null && isFinite(pct)) { pctSpan.textContent = `${isUp ? "+" : ""}${pct.toFixed(1)}%`; pctSpan.style = ` font-size:12px;padding:1px 6px;border-radius:999px; background:${isUp ? "rgba(0,200,0,.15)" : "rgba(220,0,0,.15)"}; color:${isUp ? "#7CFC9A" : "#FFAAAA"}; white-space:nowrap; `; } right.appendChild(priceSpan); if (pctSpan.textContent) right.appendChild(pctSpan); row.appendChild(left); row.appendChild(right); rows.appendChild(row); try { renderSparkline(spark, series); } catch (_) {} } } catch (e) { console.error("[zed-market-data] renderPinnedItems failed:", e); } } // =============================== // Stars in market list // =============================== function injectStarsIntoMarketItems() { const items = document.querySelectorAll(".q-item"); if (!items.length) return; const pinned = getPinnedItems(); items.forEach((item) => { const label = item.querySelector(".q-item__label"); if (!label) return; const rawText = label.textContent.trim(); const itemName = rawText.split("\n")[0].trim(); if (!itemName) return; const normName = normalizeName(itemName); let star = label.querySelector(".market-fav-star"); const storedName = star?.dataset?.itemName; if (!star || normalizeName(storedName) !== normName) { if (star) star.remove(); label.style.position = "relative"; star = document.createElement("span"); star.className = "market-fav-star"; star.innerHTML = pinned.includes(normName) ? "★" : "☆"; star.dataset.itemName = itemName; star.title = "Click to pin/unpin"; star.style.cssText = ` position:absolute; top:2px; right:5px; font-size:1.2em; cursor:pointer; color:gold; z-index:10; user-select:none; `; star.onclick = (e) => { e.stopPropagation(); e.preventDefault(); let list = getPinnedItems(); const index = list.findIndex((p) => p === normName); if (index >= 0) { list.splice(index, 1); star.innerHTML = "☆"; } else { if (list.length >= PINNED_ITEM_LIMIT) { alert(`You can only pin up to ${PINNED_ITEM_LIMIT} items.`); return; } list.push(normName); star.innerHTML = "★"; } setPinnedItems(list); renderPinnedItems(); }; label.appendChild(star); log("Injected/Updated star:", itemName); } else { star.innerHTML = pinned.includes(normName) ? "★" : "☆"; } }); } function hookSearchBar() { const input = document.querySelector("input[type='text']"); if (!input) { setTimeout(hookSearchBar, 500); return; } input.addEventListener("input", () => setTimeout(injectStarsIntoMarketItems, 300)); } function observeMarket() { if (document.documentElement.hasAttribute('data-mail')) return; const container = document.querySelector(".zed-grid.has-title.has-content") || document.querySelector(".zed-grid.has-content") || document.querySelector(".zed-grid"); if (!container) { setTimeout(observeMarket, 500); return; } const observer = new MutationObserver(() => { debounceInjectStars(); }); observer.observe(container, { childList: true, subtree: true }); injectStarsIntoMarketItems(); renderPinnedItems(); hookSearchBar(); } let lastInjectTime = 0; function debounceInjectStars() { const now = Date.now(); if (now - lastInjectTime > 300) { lastInjectTime = now; injectStarsIntoMarketItems(); } } // =============================== // URL handling // =============================== function updateMailFlag() { document.documentElement.toggleAttribute('data-mail', location.pathname.startsWith('/mail')); } // rAF-throttled renderer to avoid bursty reflows let _renderScheduled = false; function scheduleRender() { if (_renderScheduled) return; _renderScheduled = true; requestAnimationFrame(() => { _renderScheduled = false; renderPinnedItems(); }); } function handleURL() { // set/clear the mail flag every time we "navigate" updateMailFlag(); // Render the bar anywhere a suitable host exists (not just /market) setTimeout(() => { if (getMarketHost()) scheduleRender(); }, 300); // Stars only make sense on the market page if (location.pathname.includes("/market")) setTimeout(observeMarket, 500); } // =============================== // Boot // =============================== // Hide pinned ticker completely on /mail via attribute on <html> (() => { const hideTickerCSS = document.createElement('style'); hideTickerCSS.textContent = `html[data-mail] #pinnedItems{display:none!important;}`; document.head.appendChild(hideTickerCSS); })(); window.addEventListener("zed:marketDataUpdated", scheduleRender); window.addEventListener("hashchange", handleURL); window.addEventListener("popstate", handleURL); // Wrap history once (don’t re-wrap elsewhere) const _pushState = history.pushState; history.pushState = function (...args) { const r = _pushState.apply(this, args); handleURL(); return r; }; const _replaceState = history.replaceState; history.replaceState = function (...args) { const r = _replaceState.apply(this, args); handleURL(); return r; }; // Lightweight route watcher: history hooks + mild pathname poll (() => { document.addEventListener('visibilitychange', () => { if (!document.hidden) handleURL(); }); let last = location.pathname; setInterval(() => { const cur = location.pathname; if (cur !== last) { last = cur; handleURL(); } }, 1500); // was 250ms; reduce wakeups })(); console.log("[zed-market-data] loaded"); startMarketPoller(); handleURL(); // initial run })(); } // ---------- Profit Helper ---------- function run_ProfitHelper(){ (() => { // ====== DEBUG ====== const DEBUG_NUMBER_BADGES = false; const DEBUG_CONSOLE = false; // ====== PRICING CONFIG ====== const MARKET_KEY = "Zed-market-data"; const PRICE_FALLBACK = "buy"; // fallback to vars.buy (else vars.sell) per unit // ====== BENCH RECIPE CACHE (1 hour) ====== const RECIPES_CACHE_KEY = "Zed-recipes-cache-v1"; const RECIPES_CACHE_TTL = 3600_000; // 1 hour // ====== ROUTING ====== const STRONGHOLD_RE = /\/stronghold\/\d+/; const isOnStronghold = () => STRONGHOLD_RE.test(location.pathname) || location.href.includes("/stronghold/"); // ====== BENCH TYPES ====== const BENCH_TYPES = [ "crafting_bench","medical_bay","tech_lab","materials_bench", "armour_bench","ammo_bench","chem_bench","kitchen","furnace","weapon_bench" ]; // ====== ACTION WORDS ====== const ACTIONS = ["Craft","Salvage","Combine","Recycle","Bulk Recycle","Smelt","Forge","Burn","Purify","Create"]; const ACTION_RE = new RegExp(`^(?:${ACTIONS.map(a => a.replace(/\s+/g,"\\s+")).join("|")})\\b`, "i"); // ====== RADIO TOWER (card detection) ====== function getRadioCardButtons(root=document){ const spans = root.querySelectorAll('.q-btn .q-btn__content .block'); return [...spans].filter(s => (s.textContent || "").trim().toLowerCase() === "trade") .map(s => s.closest('.q-btn')); } const RADIO_CARD_CONTAINER = (btn) => btn?.closest('.q-pa-md') || btn?.closest('.grid-cont') || btn?.closest('.zed-grid'); // ====== STATE ====== const processedBenchNodes = new WeakSet(); const processedRadio = new WeakSet(); const lastVals = new WeakMap(); let recipeIndexes = {}; let recipeIndexesNorm = {}; let radioTrades = null; let rerenderQueued = false; let mo = null; let globalBadgeSeq = 0; // ====== UTILS ====== const fmt = (n) => (Number.isFinite(n) ? `$${Math.round(n).toLocaleString()}` : "N/A"); const safeNum = (v) => (Number.isFinite(+v) ? +v : 0); const collapseSpaces = (s) => String(s || "").replace(/\s+/g," ").trim(); const normalizeTitle = (s) => collapseSpaces(s).replace(/\s*blueprint\s*$/i,"").toLowerCase(); const log = (...a) => { if (DEBUG_CONSOLE) console.log("[ZedProfit]", ...a); }; const getMarket = () => { try { return JSON.parse(localStorage.getItem(MARKET_KEY) || "{}"); } catch { return {}; } }; const bestPriceForName = (name, vars = {}) => { const m = getMarket(); if (m && m[name] != null) return +m[name]; // unit price from market cache const fb = PRICE_FALLBACK === "buy" ? vars?.buy : vars?.sell; // unit fallback if (fb != null) return +fb; return null; }; const getQty = (obj) => { const v = obj?.qty ?? obj?.quantity ?? obj?.count ?? obj?.amount ?? obj?.vars?.qty; const n = Number(v); return Number.isFinite(n) && n > 0 ? n : 1; }; // ====== CSS ====== (function injectCSS(){ if (document.getElementById("zed-craft-css")) return; const css = ` .zed-craft-badge{ margin-left:6px; font-size:12px; padding:1px 6px; border-radius:999px; display:inline-block; font-weight:bold; white-space:nowrap; } .zed-pos{ background: rgba(0,200,0,.15); color:#7CFC9A; } /* Market Favs positive */ .zed-neg{ background: rgba(220,0,0,.15); color:#FFAAAA; } /* Market Favs negative */ .zed-dim{ opacity:.85 } `; const style = document.createElement("style"); style.id = "zed-craft-css"; style.textContent = css; document.head.appendChild(style); })(); // ====== CACHE HELPERS ====== function readRecipesCache() { try { return JSON.parse(localStorage.getItem(RECIPES_CACHE_KEY) || "{}"); } catch { return {}; } } function writeRecipesCache(cacheObj) { try { localStorage.setItem(RECIPES_CACHE_KEY, JSON.stringify(cacheObj)); } catch {} } function getCachedBenchJson(type) { const cache = readRecipesCache(); const entry = cache[type]; if (!entry) return null; if ((Date.now() - (entry.ts || 0)) > RECIPES_CACHE_TTL) return null; return entry.json || null; } function setCachedBenchJson(type, json) { const cache = readRecipesCache(); cache[type] = { ts: Date.now(), json }; writeRecipesCache(cache); } // ====== FETCHERS: BENCHES ====== async function fetchRecipesForBench(type) { const cached = getCachedBenchJson(type); if (cached) return indexRecipes(cached); const res = await fetch(`https://api.zed.city/getRecipes?type=${encodeURIComponent(type)}`, { credentials:"include" }); if (!res.ok) throw new Error(`getRecipes failed for ${type}`); const json = await res.json(); setCachedBenchJson(type, json); return indexRecipes(json); } function indexRecipes(recipesJson) { const orig = new Map(); const norm = new Map(); const list = Array.isArray(recipesJson?.items) ? recipesJson.items : []; for (const r of list) { const displayName = collapseSpaces(r?.name || ""); const reqs = r?.vars?.items || {}; const outObj = r?.vars?.output || {}; const outputs = []; for (const k of Object.keys(outObj)) { const item = outObj[k]; if (!item) continue; outputs.push({ name: item.name || k, item, qty: getQty(item) }); } const rec = { reqs, outputs, displayName }; orig.set(displayName, rec); const keyNorm = normalizeTitle(displayName); if (keyNorm) norm.set(keyNorm, rec); } return { orig, norm }; } async function loadAllRecipes() { recipeIndexes = {}; recipeIndexesNorm = {}; await Promise.all(BENCH_TYPES.map(async type => { try { const { orig, norm } = await fetchRecipesForBench(type); recipeIndexes[type] = orig; recipeIndexesNorm[type] = norm; } catch { recipeIndexes[type] = new Map(); recipeIndexesNorm[type] = new Map(); } })); } // ====== FETCHERS: RADIO TOWER (no cache on purpose) ====== async function getStrongholdJson() { const res = await fetch(`https://api.zed.city/getStronghold`, { credentials:"include" }); if (!res.ok) throw new Error("getStronghold failed"); return res.json(); } function findRadioTowerId(strongholdJson) { const buildings = strongholdJson?.stronghold || strongholdJson || {}; for (const k of Object.keys(buildings)) { const b = buildings[k]; if (b?.codename === "radio_tower") return Number(b.id || k); } return null; } async function fetchRadioTrades(radioId) { let res = await fetch(`https://api.zed.city/getRadioTower?id=${encodeURIComponent(radioId)}`, { credentials:"include" }); if (!res.ok) { res = await fetch(`https://api.zed.city/getRadioTower`, { credentials:"include" }); if (!res.ok) throw new Error("getRadioTower failed"); } const json = await res.json(); return indexRadioTrades(json); } function indexRadioTrades(json) { const orig = new Map(); const norm = new Map(); const list = []; const items = Array.isArray(json?.items) ? json.items : (Array.isArray(json) ? json : []); for (const t of items) { const displayName = collapseSpaces(t?.name || ""); const reqs = t?.vars?.items || {}; const outObj = t?.vars?.output || {}; const outputs = []; for (const k of Object.keys(outObj)) { const item = outObj[k]; if (!item) continue; outputs.push({ name: item.name || k, item, qty: getQty(item) }); } const rec = { reqs, outputs, displayName, isRadio:true }; list.push(rec); if (displayName) { orig.set(displayName, rec); norm.set(normalizeTitle(displayName), rec); } } return { list, orig, norm }; } // ====== COMPUTE ====== function sumIngredientCost(itemReqs) { let total = 0, missing = []; for (const key of Object.keys(itemReqs || {})) { const req = itemReqs[key]; if (!req?.name) continue; const unit = bestPriceForName(req.name, req.vars || {}); const qty = safeNum(req.req_qty ?? req.qty ?? 0); if (unit == null) missing.push(req.name); total += (unit ?? 0) * qty; } return { total, missing }; } function lookupRecipeOrTrade(displayName) { const text = collapseSpaces(displayName); const keyNorm = normalizeTitle(text); // benches for (const type of BENCH_TYPES) { const byOrig = recipeIndexes[type]; if (byOrig?.has(text)) return byOrig.get(text); const byNorm = recipeIndexesNorm[type]; if (keyNorm && byNorm?.has(keyNorm)) return byNorm.get(keyNorm); } // radio if (radioTrades?.orig?.has(text)) return radioTrades.orig.get(text); if (keyNorm && radioTrades?.norm?.has(keyNorm)) return radioTrades.norm.get(keyNorm); return null; } function computeRec(rec) { if (!rec) return null; const { total: cost, missing } = sumIngredientCost(rec.reqs); let valueTotal = 0; let anyVal = false; const outQtyNotes = []; for (const out of rec.outputs || []) { const unitFromMarket = bestPriceForName(out.name, out.item?.vars || {}); let unitVal = unitFromMarket; if (unitVal == null && Number.isFinite(out.item?.value)) unitVal = +out.item.value; // per-unit fallback if (unitVal == null) continue; const qty = getQty(out.item) || out.qty || 1; valueTotal += unitVal * qty; anyVal = true; if (qty > 1) outQtyNotes.push(`${out.name}×${qty}`); } const value = anyVal ? valueTotal : null; const profit = (Number.isFinite(cost) && Number.isFinite(value)) ? (value - cost) : null; return { cost, value, profit, missing, outQtyNotes }; } // ====== BENCH MATCHER (permissive) ====== function isBenchRow(el) { if (!el || el.nodeType !== 1) return false; const hasClassCol = el.classList?.contains("col"); const likelyContainer = hasClassCol || /^(DIV|SPAN|A|BUTTON)$/i.test(el.tagName); if (!likelyContainer) return false; const t = collapseSpaces(el.textContent || ""); if (!t) return false; if (ACTION_RE.test(t)) return true; if (/\bBlueprint$/i.test(t)) return true; return false; } // ====== DOM ====== function makeOrUpdateBadge(el, data, debugId=null) { const prev = lastVals.get(el) || {}; const round = (x) => Number.isFinite(x) ? Math.round(x) : NaN; if (round(prev.cost) === round(data.cost) && round(prev.value) === round(data.value) && round(prev.profit) === round(data.profit)) return; let badge = el.querySelector(":scope > .zed-craft-badge"); let txtCore = `${fmt(data.cost)} → ${fmt(data.value)} (${data.profit > 0 ? "+" : ""}${fmt(data.profit).replace("$","")})`; if (DEBUG_NUMBER_BADGES && debugId != null) txtCore += ` [#${debugId}]`; if (!badge) { badge = document.createElement("span"); badge.className = "zed-craft-badge"; el.appendChild(badge); } badge.textContent = txtCore; // Market Favs logic: zero treated as positive badge.classList.remove("zed-pos", "zed-neg"); if (!Number.isFinite(data.profit) || data.profit >= 0) badge.classList.add("zed-pos"); else badge.classList.add("zed-neg"); const missingNames = (data.missing || []); badge.classList.toggle("zed-dim", missingNames.length > 0); const parts = []; if (missingNames.length) parts.push(`Missing prices: ${missingNames.join(", ")}`); if (data.outQtyNotes?.length) parts.push(`Outputs: ${data.outQtyNotes.join(", ")}`); badge.title = parts.join(" • "); lastVals.set(el, { cost: data.cost, value: data.value, profit: data.profit }); } // ====== RENDER ====== function renderBenches(root=document){ if (!Object.keys(recipeIndexes).length) return; const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, { acceptNode(node) { return (!processedBenchNodes.has(node) && isBenchRow(node)) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; } }); let node; while ((node = walker.nextNode())) { // If this match is inside an .item-row that already has *any* badge, skip. const row = node.closest?.('.item-row'); if (row && (row.dataset.zedBadgeDone === "1" || row.querySelector('.zed-craft-badge'))) { processedBenchNodes.add(node); continue; } const title = collapseSpaces(node.textContent || ""); const rec = lookupRecipeOrTrade(title); const data = computeRec(rec); if (!data) { processedBenchNodes.add(node); continue; } // Place badge here (first hit in this row wins) const host = document.createElement("span"); node.appendChild(host); const id = ++globalBadgeSeq; makeOrUpdateBadge(host, data, id); // Mark the entire row as done so later matches inside it are ignored if (row) row.dataset.zedBadgeDone = "1"; processedBenchNodes.add(node); } } function renderRadio(root=document){ if (!radioTrades?.list?.length) return; const buttons = getRadioCardButtons(root); buttons.forEach((btn, i) => { const container = RADIO_CARD_CONTAINER(btn) || btn.parentElement; if (!container || processedRadio.has(container) || container.querySelector(".zed-craft-badge")) return; const rec = radioTrades.list[i]; const data = computeRec(rec); if (!data) { processedRadio.add(container); return; } const hostWrap = document.createElement("div"); hostWrap.style.marginTop = "6px"; (btn.parentElement || container).appendChild(hostWrap); const id = ++globalBadgeSeq; makeOrUpdateBadge(hostWrap, data, id); processedRadio.add(container); }); } function renderPass(root=document){ renderBenches(root); renderRadio(root); } function queueRender(root=document){ if (rerenderQueued) return; rerenderQueued = true; requestAnimationFrame(() => { rerenderQueued = false; renderPass(root); }); } // ====== OBSERVERS & ROUTING ====== function connectObserver() { if (mo) mo.disconnect(); mo = new MutationObserver((muts) => { for (const m of muts) for (const n of m.addedNodes || []) { if (n.nodeType === 1) queueRender(n); } }); mo.observe(document.body, { childList:true, subtree:true }); } function disconnectObserver(){ if (mo) { mo.disconnect(); mo = null; } } async function initForPage() { try { if (!isOnStronghold()) { recipeIndexes = {}; recipeIndexesNorm = {}; radioTrades = null; connectObserver(); return; } // Benches (cached) await loadAllRecipes(); // Radio Tower (live) try { const sh = await getStrongholdJson(); const radioId = findRadioTowerId(sh); radioTrades = radioId ? await fetchRadioTrades(radioId) : null; } catch { radioTrades = null; } connectObserver(); queueRender(document); } catch (e) { if (DEBUG_CONSOLE) console.warn(e); } } let lastURL = location.href; function onRouteChange() { if (location.href === lastURL) return; lastURL = location.href; processedBenchNodes.clear?.(); processedRadio.clear?.(); lastVals.clear?.(); disconnectObserver(); // Keep localStorage cache; just clear in-memory recipeIndexes = {}; recipeIndexesNorm = {}; radioTrades = null; requestAnimationFrame(() => { initForPage(); setTimeout(() => queueRender(document), 150); }); } // SPA hooks const _ps = history.pushState; history.pushState = function (...a) { const r = _ps.apply(this, a); onRouteChange(); return r; }; const _rs = history.replaceState; history.replaceState = function (...a) { const r = _rs.apply(this, a); onRouteChange(); return r; }; window.addEventListener("popstate", onRouteChange); document.addEventListener("click", (e) => { const a = e.target.closest?.("a[href]"); if (!a) return; setTimeout(onRouteChange, 0); }, { capture:true }); // Safety net const hintObserver = new MutationObserver((muts) => { for (const m of muts) for (const n of m.addedNodes || []) { if (n.nodeType !== 1) continue; if (/\b(Craft|Salvage|Combine|Recycle|Blueprint|Trade|Smelt|Forge|Burn|Purify|Create)\b/i.test(n.textContent || "")) { initForPage(); hintObserver.disconnect(); return; } } }); hintObserver.observe(document.documentElement, { childList:true, subtree:true }); // Re-render when market cache changes window.addEventListener("zed:marketDataUpdated", () => queueRender(document)); window.addEventListener("storage", (e) => { if (e.key === MARKET_KEY) queueRender(document); }); // Kick off initForPage(); })(); } /* ===== Inventory Net Worth ===== */ function run_networth(){ (() => { const MARKET_KEY = "Zed-market-data"; const API_ITEMS = "https://api.zed.city/loadItems"; const API_STATS = "https://api.zed.city/getStats"; const INVENTORY_GRID_SEL = 'div.zed-grid.has-title.has-content'; const PANEL_ID = 'inventory-networth-api'; const INVENTORY_RE = /\/inventory(?:$|[?#])/; // --- state let panel = null; let itemsCache = null; let cashBalance = null; let lastMarketStr = null; // --- utils const onInventory = () => INVENTORY_RE.test(location.pathname) || location.href.includes('/inventory'); const $ = (sel, root=document) => root.querySelector(sel); const money = n => Number.isFinite(n) ? ('$' + Math.round(n).toLocaleString()) : 'N/A'; const parseCash = (obj) => { // Try a few common shapes safely const cands = [ obj?.money, obj?.cash, obj?.balance, obj?.stats?.money, obj?.user?.money, obj?.data?.money ].filter(v => Number.isFinite(+v)); return cands.length ? +cands[0] : null; }; const readMarket = () => { const s = localStorage.getItem(MARKET_KEY) || "{}"; if (s === lastMarketStr) return null; lastMarketStr = s; try { return JSON.parse(s); } catch { return {}; } }; // Find the inventory grid by structure (hash-proof) function findInventoryGrid() { try { const q = document.querySelector(`${INVENTORY_GRID_SEL}:has(.grid-cont .q-list)`); if (q) return q; } catch {} // Fallback: scan candidates for the expected inner structure const grids = document.querySelectorAll(INVENTORY_GRID_SEL); for (const el of grids) { if (el.querySelector('.grid-cont .q-list')) return el; } return null; } function ensurePanel() { if (panel && panel.isConnected) return panel; const bottom = findInventoryGrid(); if (!bottom || !bottom.parentNode) return null; const p = document.createElement('div'); p.id = PANEL_ID; p.style.margin = '8px 0 10px'; p.style.padding = '4px 4px'; p.style.borderRadius = '4px'; p.style.background = 'rgba(255,255,255,0.06)'; p.style.border = '1px solid rgba(255,255,255,0.08)'; p.style.display = 'flex'; p.style.flexWrap = 'wrap'; p.style.alignItems = 'center'; p.style.gap = '4px'; p.style.fontSize = '12px'; const h = document.createElement('div'); h.textContent = 'Net Worth'; h.style.fontWeight = '700'; h.style.marginRight = '8px'; p.appendChild(h); const v = document.createElement('div'); v.className = 'zed-nw-total'; v.style.fontWeight = '700'; v.style.padding = '2px 8px'; v.style.borderRadius = '999px'; v.style.background = 'rgba(0,200,0,.15)'; v.style.color = '#7CFC9A'; v.textContent = '$…'; p.appendChild(v); const b = document.createElement('div'); b.className = 'zed-nw-breakdown'; b.style.opacity = '0.85'; b.textContent = 'items: $0 + cash: $0'; p.appendChild(b); bottom.parentNode.insertBefore(p, bottom); panel = p; return panel; } function setPanel(itemsVal, cashVal, priced, unpriced) { if (!panel) return; const total = (Number(itemsVal)||0) + (Number(cashVal)||0); const v = panel.querySelector('.zed-nw-total'); const b = panel.querySelector('.zed-nw-breakdown'); if (v) v.textContent = money(total); if (b) b.textContent = `items: ${money(itemsVal||0)} + cash: ${money(cashVal||0)} · unpriced: ${unpriced}`; } function computeFromCaches() { const market = readMarket() ?? JSON.parse(lastMarketStr || "{}"); // items let itemsVal = 0, priced = 0, unpriced = 0; if (Array.isArray(itemsCache)) { for (const it of itemsCache) { const name = it?.name; const qty = Number(it?.quantity) || 0; const unit = (name && market[name] != null) ? Number(market[name]) : null; if (unit != null && Number.isFinite(unit)) { itemsVal += unit * qty; priced++; } else { unpriced++; } } } setPanel(itemsVal, cashBalance||0, priced, unpriced); } async function fetchOnce(url, postBodyIfNoGet=false) { try { const r = await fetch(url, { credentials: 'include' }); if (!r.ok) throw new Error('GET failed'); return await r.json(); } catch {} } async function initInventoryNW() { if (!onInventory()) return; // Wait briefly for the inventory grid (debounced observer, short timeout) const waitForGrid = () => new Promise(res => { const now = findInventoryGrid(); if (now) return res(true); let done = false, rafId = 0, timer = 0; const debounce = (() => { let queued = false; return () => { if (queued) return; queued = true; rafId = requestAnimationFrame(() => { queued = false; check(); }); }; })(); const mo = new MutationObserver(debounce); function check() { if (done) return; if (findInventoryGrid()) { done = true; mo.disconnect(); cancelAnimationFrame(rafId); clearTimeout(timer); res(true); } } mo.observe(document.documentElement, { childList: true, subtree: true }); timer = setTimeout(() => { if (!done) { done = true; mo.disconnect(); res(false); } }, 1500); // kick once debounce(); }); const ok = await waitForGrid(); if (!ok) return; if (!ensurePanel()) return; // Fetch once if we haven't intercepted yet if (!Array.isArray(itemsCache)) { const ji = await fetchOnce(API_ITEMS, true); if (ji && Array.isArray(ji.items)) itemsCache = ji.items; } if (!Number.isFinite(cashBalance)) { const js = await fetchOnce(API_STATS, true); const cash = js ? parseCash(js) : null; if (Number.isFinite(cash)) cashBalance = cash; } computeFromCaches(); // tiny self-heal: reattach if grid/panel moves (debounced) (function liteReattach(){ let queued = false; const reattach = () => { if (queued) return; queued = true; requestAnimationFrame(() => { queued = false; const grid = findInventoryGrid(); if (!grid) return; if (!panel || !panel.isConnected || panel.nextSibling !== grid) { if (panel && panel.isConnected) panel.remove(); panel = null; ensurePanel(); computeFromCaches(); } }); }; const mo = new MutationObserver(reattach); mo.observe(document.documentElement, { childList: true, subtree: true }); // hook route changes const _ps = history.pushState; history.pushState = function(...a){ const r = _ps.apply(this, a); reattach(); return r; }; const _rs = history.replaceState; history.replaceState = function(...a){ const r = _rs.apply(this, a); reattach(); return r; }; window.addEventListener('popstate', reattach); window.addEventListener('hashchange', reattach); })(); } // Market Favs updates → recompute only window.addEventListener('storage', (e) => { if (e.key === MARKET_KEY) computeFromCaches(); }); window.addEventListener('zed:marketDataUpdated', computeFromCaches); // Minimal network hooks (ONLY intercept our two endpoints) ;(function hookFetchOnce(){ if (window.__nw2_fetch_hooked__) return; window.__nw2_fetch_hooked__ = true; const orig = window.fetch; window.fetch = async function(input, init) { const resp = await orig.apply(this, arguments); try { const url = typeof input === 'string' ? input : (input?.url || ''); if (url.includes('loadItems') || url.includes('getStats')) { const clone = resp.clone(); clone.json().then(j => { if (url.includes('loadItems') && Array.isArray(j?.items)) { itemsCache = j.items; } if (url.includes('getStats')) { const cash = parseCash(j); if (Number.isFinite(cash)) cashBalance = cash; } if (onInventory() && panel) computeFromCaches(); }).catch(()=>{}); } } catch {} return resp; }; })(); ;(function hookXHROnce(){ if (window.__nw2_xhr_hooked__) return; window.__nw2_xhr_hooked__ = true; const open = XMLHttpRequest.prototype.open; const send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this.__nw2_url = url || ''; return open.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { const url = this.__nw2_url || ''; if (url.includes('loadItems') || url.includes('getStats')) { this.addEventListener('readystatechange', function() { if (this.readyState === 4) { try { const j = JSON.parse(this.responseText); if (url.includes('loadItems') && Array.isArray(j?.items)) { itemsCache = j.items; } if (url.includes('getStats')) { const cash = parseCash(j); if (Number.isFinite(cash)) cashBalance = cash; } if (onInventory() && panel) computeFromCaches(); } catch {} } }); } return send.apply(this, arguments); }; })(); // SPA routing trigger + first load function onRouteChange(){ if (onInventory()) initInventoryNW(); } const _ps = history.pushState; history.pushState = function(...a){ const r = _ps.apply(this, a); onRouteChange(); return r; }; const _rs = history.replaceState; history.replaceState = function(...a){ const r = _rs.apply(this, a); onRouteChange(); return r; }; window.addEventListener('popstate', onRouteChange); window.addEventListener('hashchange', onRouteChange); // First load initInventoryNW(); })(); } // ---------- Timers ---------- function run_Timers(){ (function () { 'use strict'; /** CONFIG **/ const API = { stats: 'https://api.zed.city/getStats', stronghold: 'https://api.zed.city/getStronghold', }; const ICON = { energy: '⚡', rads: '☢', xp: '🏆', raid: '🎯', furnace: '🔥', mine_iron: '⛏️', mine_coal: '⛏️', junk: '🛒', radio: '🗼', }; const TEN_MIN = 10 * 60; // seconds const POLL_STATS_EVERY = 60 * 1000; const POLL_STRONGHOLD_EVERY = 60 * 1000; const SAVED_KEY = 'zed-timerbar-saved'; // junk/radio const SH_CACHE_KEY = 'zed-timerbar-stronghold-cache'; // furnace cache for travel mode const saved = () => JSON.parse(localStorage.getItem(SAVED_KEY) || '{}'); const save = (obj) => localStorage.setItem(SAVED_KEY, JSON.stringify(obj)); let __zed_started = false; const __zed_intervals = { stats: null, stronghold: null, saved: null, tick: null }; function setIntervalSafe(key, fn, ms) { if (__zed_intervals[key]) clearInterval(__zed_intervals[key]); __zed_intervals[key] = setInterval(fn, ms); } /** Cross-origin friendly JSON fetch (falls back to GM_xmlhttpRequest) **/ function fetchJSON(url) { return new Promise(async (resolve) => { try { const r = await fetch(url, { method: 'GET', mode: 'cors', credentials: 'include' }); if (r && r.ok) { resolve(await r.json()); return; } } catch { /* try GM below */ } if (typeof GM_xmlhttpRequest === 'function') { try { GM_xmlhttpRequest({ method: 'GET', url, onload: (res) => { try { resolve(JSON.parse(res.responseText)); } catch { resolve(null); } }, onerror: () => resolve(null), ontimeout: () => resolve(null), }); return; } catch { /* noop */ } } resolve(null); }); } /** DOM: create bar just under the main toolbar **/ function ensureBar() { const toolbar = document.querySelector('div.q-toolbar.row.no-wrap.items-center[role="toolbar"]') || document.querySelector('div.q-toolbar[role="toolbar"]') || document.querySelector('.q-header .q-toolbar') || document.querySelector('header .q-toolbar'); if (!toolbar) return null; let holder = document.getElementById('zed-timerbar'); if (holder) return holder; holder = document.createElement('div'); holder.id = 'zed-timerbar'; holder.setAttribute('role', 'group'); holder.innerHTML = ` <div class="zed-timerbar-inner"> ${tileHTML('energy')} ${tileHTML('rads')} ${tileHTML('xp')} ${tileHTML('raid')} <span id="zed-furnace-mount" style="display:none"></span> ${tileHTML('mine_iron')} ${tileHTML('mine_coal')} ${tileHTML('junk')} ${tileHTML('radio')} </div> `; if (toolbar.parentElement) { toolbar.parentElement.insertBefore(holder, toolbar.nextSibling); } else { document.body.insertBefore(holder, document.body.firstChild); } injectStyles(); // Hide mines until confirmed setTileVisible('mine_iron', false); setTileVisible('mine_coal', false); // Remove any legacy single furnace tile holder.querySelectorAll('.zed-tile[data-key="furnace"]').forEach(el => el.remove()); return holder; } function tileHTML(key) { return ` <a class="zed-tile" data-key="${key}" href="#" title="${key.toUpperCase()}"> <div class="zed-icon">${ICON[key] || '•'}</div> <div class="zed-time" data-seconds="-1">--:--</div> </a> `; } function injectStyles() { if (document.getElementById('zed-timerbar-style')) return; const style = document.createElement('style'); style.id = 'zed-timerbar-style'; style.textContent = ` #zed-timerbar { display: flex; justify-content: center; padding: 2px 8px; background: rgba(9,10,11,0.9); border-top: 1px solid rgba(255,255,255,0.06); } .zed-timerbar-inner { display: flex; flex-wrap: wrap; gap: 0px; justify-content: center; align-items: flex-start; } .zed-tile { display: flex; flex-direction: row; align-items: center; gap: 6px; text-decoration: none; padding: 2px 8px; } .zed-tile.hidden { display: none !important; } .zed-icon { font-size: 12px; line-height: 1; } .zed-time { padding: 2px 6px; font-size: 12px; font-weight: 600; color: #fff; background: rgba(255,255,255,0.08); border-radius: 8px; min-width: 44px; text-align: center; } .zed-time.alert { color: #ff4d4f; background: rgba(255,77,79,0.12); } #zed-furnace-mount { display:none; } `; document.head.appendChild(style); } /** Formatting **/ function fmt(seconds) { // Show D:HH:MM:SS (days shown only when needed) if (seconds == null || seconds <= 0) return '00:00'; let s = Math.floor(seconds); const d = Math.floor(s / 86400); s -= d * 86400; const h = Math.floor(s / 3600); s -= h * 3600; const m = Math.floor(s / 60); s -= m * 60; const pad = (n) => (n < 10 ? '0' + n : '' + n); const parts = []; if (d) parts.push(pad(d)); if (h || parts.length) parts.push(pad(h)); parts.push(pad(m)); parts.push(pad(s)); return parts.join(':'); } function applyAlertClass(el, seconds) { if (!el) return; if (seconds <= TEN_MIN) el.classList.add('alert'); else el.classList.remove('alert'); } function setTileVisible(key, visible) { const el = document.querySelector(`.zed-tile[data-key="${key}"]`); if (!el) return; el.classList.toggle('hidden', !visible); } /** XHR intercept **/ (function interceptXHR() { const openOrig = XMLHttpRequest.prototype.open; const sendOrig = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this._zedURL = url; return openOrig.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { this.addEventListener('readystatechange', function () { if (this.readyState !== 4) return; try { const url = String(this._zedURL || ''); const res = JSON.parse(this.responseText || 'null'); if (!res) return; if (url.includes('store_id') && url.includes('junk')) { const reset = res?.limits?.reset_time; if (reset) { const until = Date.now() + reset * 1000; const s = saved(); s.junk = { until, href: '/store/junk' }; save(s); } } if (url.includes('getRadioTower')) { const expire = res?.expire; if (expire) { const until = Date.now() + expire * 1000; const s = saved(); s.radio = { until, href: '/stronghold' }; save(s); } } } catch {} }); return sendOrig.apply(this, arguments); }; })(); function writeStrongholdCache(entries) { const payload = { saved_at: Date.now(), furnaces: entries.map(e => ({ id: e.id, until: e.until })), }; localStorage.setItem(SH_CACHE_KEY, JSON.stringify(payload)); } function readStrongholdCache() { try { const raw = localStorage.getItem(SH_CACHE_KEY); if (!raw) return null; const data = JSON.parse(raw); const now = Date.now(); return (data.furnaces || []).map(f => ({ id: f.id, seconds: Math.max(0, Math.floor((f.until - now) / 1000)), })); } catch { return null; } } function zedApplyStats(data) { // --- Base energy max --- let energyMax = data?.membership ? 150 : (data?.stats?.max_energy || 100); // --- Add +50 if "Feeling Relaxed" effect is active --- const hasFeelingRelaxed = Object.values(data?.effects || {}).some( eff => eff.codename === 'player_effect_feeling_relaxed' ); if (hasFeelingRelaxed) { energyMax += 50; } // --- Energy calculations --- const energyMissing = Math.max(0, energyMax - (data?.energy ?? 0)); const tickSeconds = data?.energy_tick ?? (data?.membership ? 600 : 900); const toNextEnergy = Math.max(0, data?.energy_regen ?? 0); const ticksNeeded = energyMissing > 0 ? Math.ceil(energyMissing / 5) : 0; const energySeconds = ticksNeeded > 0 ? toNextEnergy + (ticksNeeded - 1) * tickSeconds : 0; setTile('energy', '/stronghold', energySeconds); // --- Radiation calculations --- const radBaseMax = (data?.stats?.max_rad ?? 60); const radMax = radBaseMax + (data?.membership ? 20 : 0); const radMissing = Math.max(0, radMax - (data?.rad ?? 0)); const baseTick = 300; const toNext = Math.max(0, data?.rad_regen || 0); const radSeconds = radMissing > 0 ? Math.max(0, (radMissing - 1) * baseTick) + toNext : 0; setTile('rads', '/scavenge', radSeconds); // --- XP tile --- const xpToGo = Math.max(0, Math.round((data?.xp_end || 0)) - Math.round((data?.experience || 0))); setTile('xp', '/profile', null, xpToGo.toLocaleString()); // --- Raid cooldown --- const raid = Math.max(0, data?.raid_cooldown ?? 0); setTile('raid', '/raids', raid); } window.zedApplyStats = zedApplyStats; async function refreshStats() { const data = await fetchJSON(API.stats); if (!data) return; if (window.zedApplyStats) window.zedApplyStats(data); } async function refreshStronghold() { function ensureFurnaceMount() { let mount = document.getElementById('zed-furnace-mount'); if (!mount) { const fallback = document.querySelector('.zed-timerbar-inner'); mount = document.createElement('span'); mount.id = 'zed-furnace-mount'; (fallback || document.body).appendChild(mount); } return mount; } function createFurnaceTile(id, seconds) { const a = document.createElement('a'); a.className = 'zed-tile'; a.href = `/stronghold/${id}`; a.title = `FURNACE ${id}`; const icon = document.createElement('div'); icon.className = 'zed-icon'; icon.textContent = '🔥'; const time = document.createElement('div'); time.className = 'zed-time'; time.dataset.seconds = String(Math.max(0, seconds || 0)); time.textContent = fmt(seconds || 0); a.appendChild(icon); a.appendChild(time); return a; } function renderFurnaces(list) { const mount = ensureFurnaceMount(); if (!mount) return; const container = mount.parentElement || document; if (!list.length) { container.querySelectorAll('.zed-tile[data-furnace-id]').forEach(el => el.remove()); return; } const existing = new Set(list.map(r => String(r.id))); container.querySelectorAll('.zed-tile[data-furnace-id]').forEach(el => { if (!existing.has(el.dataset.furnaceId)) el.remove(); }); list.forEach(({ id, seconds }) => { let tile = container.querySelector(`.zed-tile[data-furnace-id="${id}"]`); if (!tile) { tile = createFurnaceTile(id, seconds || 0); tile.dataset.furnaceId = String(id); mount.insertAdjacentElement('afterend', tile); } else { const timeEl = tile.querySelector('.zed-time'); const s = Math.max(0, Number(seconds || 0)); timeEl.dataset.seconds = String(s); timeEl.textContent = fmt(s); applyAlertClass(timeEl, s); } }); } const data = await fetchJSON(API.stronghold); if (data && data.stronghold) { const now = Date.now(); const buildings = Object.values(data.stronghold) || []; const furnacesRaw = buildings.filter(b => b?.codename === 'furnace'); const computed = furnacesRaw.map((f) => { let seconds = 0; const bpWait = f?.items?.['item_requirement-bp']?.vars?.wait_time; const qtyEach = f?.items?.['item_requirement-bp']?.vars?.items?.['item_requirement-1']?.qty; const total = f?.items?.['item_requirement-1']?.quantity; if (f?.in_progress && typeof f?.timeLeft === 'number' && bpWait && qtyEach && total) { const done = f?.iterationsPassed || 0; const remainingIters = Math.max(0, Math.floor(total / qtyEach) - done - 1); seconds = Math.max(0, (remainingIters * bpWait) + (f.timeLeft || 0)); } else if (typeof f?.wait === 'number') { seconds = Math.max(0, f.wait); } return { id: f.id, seconds, until: now + seconds * 1000 }; }); writeStrongholdCache(computed.map(({ id, until }) => ({ id, until }))); const ironMine = buildings.find(b => b?.codename === 'iron_automine'); if (ironMine) { const secsIron = Math.max(0, typeof ironMine?.timeLeft === 'number' ? ironMine.timeLeft : (ironMine?.wait || 0) ); setTile('mine_iron', ironMine?.id ? `/stronghold/${ironMine.id}` : '#', secsIron); setTileVisible('mine_iron', true); } else setTileVisible('mine_iron', false); const coalMine = buildings.find(b => b?.codename === 'coal_automine'); if (coalMine) { const secsCoal = Math.max(0, typeof coalMine?.timeLeft === 'number' ? coalMine.timeLeft : (coalMine?.wait || 0) ); setTile('mine_coal', coalMine?.id ? `/stronghold/${coalMine.id}` : '#', secsCoal); setTileVisible('mine_coal', true); } else setTileVisible('mine_coal', false); renderFurnaces(computed.map(({ id, seconds }) => ({ id, seconds }))); return; } const cached = readStrongholdCache(); if (cached && cached.length) renderFurnaces(cached); } function setTile(key, href, seconds, overrideLabel) { const tile = document.querySelector(`.zed-tile[data-key="${key}"]`); if (!tile) return; tile.href = href || '#'; const timeEl = tile.querySelector('.zed-time'); if (!timeEl) return; if (overrideLabel != null) { timeEl.textContent = String(overrideLabel); timeEl.dataset.seconds = '-1'; timeEl.classList.remove('alert'); return; } const s = Math.max(0, Number(seconds || 0)); timeEl.textContent = fmt(s); timeEl.dataset.seconds = String(s); applyAlertClass(timeEl, s); } function refreshSavedTimers() { const now = Date.now(); const s = saved(); if (s.junk) { const secs = Math.max(0, Math.floor((s.junk.until - now) / 1000)); setTile('junk', s.junk.href || '/store/junk', secs); } if (s.radio) { const secs = Math.max(0, Math.floor((s.radio.until - now) / 1000)); setTile('radio', s.radio.href || '/stronghold', secs); } } function tick() { document.querySelectorAll('#zed-timerbar [data-seconds]').forEach(el => { const s = parseInt(el.dataset.seconds || '-1', 10); if (isNaN(s) || s < 0) return; const next = Math.max(0, s - 1); el.dataset.seconds = String(next); el.textContent = fmt(next); applyAlertClass(el, next); }); } function startIntervals() { if (__zed_started) return; __zed_started = true; refreshStats(); refreshStronghold(); refreshSavedTimers(); setIntervalSafe('stats', refreshStats, POLL_STATS_EVERY); setIntervalSafe('stronghold', refreshStronghold, POLL_STRONGHOLD_EVERY); setIntervalSafe('saved', refreshSavedTimers, 5 * 1000); setIntervalSafe('tick', tick, 1000); } function boot() { if (ensureBar()) { startIntervals(); return; } const obs = new MutationObserver(() => { if (ensureBar()) { obs.disconnect(); startIntervals(); } }); obs.observe(document.documentElement, { childList: true, subtree: true }); setTimeout(() => { obs.disconnect(); (function retryUntilReady(tries = 40) { if (ensureBar()) startIntervals(); else if (tries > 0) setTimeout(() => retryUntilReady(tries - 1), 500); })(); }, 20000); } boot(); })(); } // ========================================================== // Exploration Data with arrival-based pruning (no legacy migration) // ========================================================== function run_ExplorationData(){ (function(){ // --------------------------------- // Storage (data + UI state) // --------------------------------- const DATA_KEY = "Zed-exploration-data"; const UI_KEY = "Zed-exploration-ui"; // { [location]: { NPCs: true/false, Gates: true/false } } // Initialize fresh keys — no legacy migration if (!localStorage.getItem(DATA_KEY)) { localStorage.setItem(DATA_KEY, JSON.stringify({})); } if (!localStorage.getItem(UI_KEY)) { localStorage.setItem(UI_KEY, JSON.stringify({})); } // helpers: read/write function readData() { try { return JSON.parse(localStorage.getItem(DATA_KEY)) || {}; } catch { return {}; } } function writeData(all) { localStorage.setItem(DATA_KEY, JSON.stringify(all)); } function readUI() { try { return JSON.parse(localStorage.getItem(UI_KEY)) || {}; } catch { return {}; } } function writeUI(ui) { localStorage.setItem(UI_KEY, JSON.stringify(ui)); } function getSectionState(location, sectionTitle, defaultOpen = true) { const ui = readUI(); return ui[location]?.[sectionTitle] ?? defaultOpen; } function setSectionState(location, sectionTitle, isOpen) { const ui = readUI(); if (!ui[location]) ui[location] = {}; ui[location][sectionTitle] = isOpen; writeUI(ui); } // --------------------------------- // Minimal state / other helpers // --------------------------------- let current_location = "City"; const TIMER_THRESHOLD = 10 * 60; function slugify(s) { return String(s || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } function getDayHourMinute(seconds) { seconds = Math.max(0, Math.floor(Number(seconds) || 0)); let days = Math.floor(seconds / 86400); seconds -= days * 86400; let hours = Math.floor(seconds / 3600); seconds -= hours * 3600; let minutes = Math.floor(seconds / 60); seconds -= minutes * 60; const pad = (n) => (n < 10 ? "0" + n : String(n)); const time = []; if (days > 0) time.push(pad(days)); if (hours > 0 || time.length) time.push(pad(hours)); if (minutes > 0 || time.length) time.push(pad(minutes)); time.push(pad(seconds)); return time.join(":"); } function updateTimers() { document.querySelectorAll(".explore-ui .timer").forEach((el) => { let t = Number(el.timeLeft); if (isNaN(t)) return; // already finished? if (t <= -1) { el.classList.add("alert"); el.textContent = getDayHourMinute(0); const row = el.closest('.rowline'); const kind = row?.dataset?.kind || ''; const state = row?.querySelector('.state'); if (state) { if (kind === 'scavenge') { state.className = 'state ok'; state.textContent = 'Ready'; } else if (kind === 'gate') { state.className = 'state warn'; state.textContent = 'Locked'; } } return; } const next = t - 1; el.timeLeft = next; if (next <= TIMER_THRESHOLD) el.classList.add("alert"); else el.classList.remove("alert"); // when it hits zero, set the right state and freeze if (next <= 0) { el.timeLeft = -1; el.textContent = getDayHourMinute(0); const row = el.closest('.rowline'); const kind = row?.dataset?.kind || ''; const state = row?.querySelector('.state'); if (state) { if (kind === 'scavenge') { state.className = 'state ok'; state.textContent = 'Ready'; } else if (kind === 'gate') { state.className = 'state warn'; state.textContent = 'Locked'; } } return; } // normal ticking el.textContent = getDayHourMinute(next); }); } // --------------------------------- // Safe XHR intercept (guarded so we don’t double-wrap prototype) // --------------------------------- (function () { if (XMLHttpRequest.prototype.__zedExplorationWrapped) return; const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this._interceptedURL = url; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { this.addEventListener("readystatechange", function () { if (this.readyState === 4) { try { const response = JSON.parse(this.responseText); const eventData = { url: this._interceptedURL, response }; window.dispatchEvent(new CustomEvent("xhrIntercepted", { detail: eventData })); } catch (e) {} } }); return originalSend.apply(this, arguments); }; Object.defineProperty(XMLHttpRequest.prototype, '__zedExplorationWrapped', { value: true, configurable: false }); })(); // --------------------------------- // Data collectors // --------------------------------- // When we receive getLocation, we treat that as "arriving" at a top-level location. // We store an arrival timestamp and mark that this location needs a one-time prune. function handleLocation(response) { current_location = response.name || current_location; const all = readData(); const cur = all[current_location] || {}; const meta = cur.meta || {}; meta.lastArrival = Date.now(); meta.needsArrivalPrune = true; // first getRoom after arrival will prune stale stuff all[current_location] = { npcs: cur.npcs || {}, gates: cur.gates || {}, scavenge: cur.scavenge || {}, meta }; writeData(all); } // Helper: prune entries whose timers already hit 0 *before arrival*. function pruneOnArrival(cur) { const now = Date.now(); const arrivalTs = cur?.meta?.lastArrival || now; const result = { npcs: {}, gates: {}, scavenge: cur.scavenge || {}, meta: { ...(cur.meta || {}), needsArrivalPrune: false, prunedAt: now } }; // NPCs: respawn_time is an absolute ms timestamp we saved previously. // If respawn_time <= arrivalTs (i.e., timer would have been 0 or negative BEFORE we arrived), // drop it so new spawns/renames won't duplicate. for (const id in (cur.npcs || {})) { const npc = cur.npcs[id]; const respawn = Number(npc.respawn_time || 0); if (respawn > arrivalTs) { result.npcs[id] = npc; } } // Gates: gate_next_change_time/gate_unlock_time are absolute ms timestamps when state changes. // If that time <= arrivalTs, drop it so we accept whatever the room now tells us. for (const id in (cur.gates || {})) { const g = cur.gates[id]; const changeAt = Number(g.gate_next_change_time || g.gate_unlock_time || 0); if (changeAt > arrivalTs) { result.gates[id] = g; } } return result; } // Merge helper: add/replace by id without deleting other sub-area items function mergeById(target, incoming) { for (const id in incoming) { target[id] = incoming[id]; } return target; } function saveExplorationData(response) { const all = readData(); const now = Date.now(); const cur = all[current_location] || { npcs: {}, gates: {}, scavenge: {}, meta: {} }; // If this is the first room load after arriving, prune stale entries once. let working = cur; if (cur.meta?.needsArrivalPrune) { working = pruneOnArrival(cur); } else { // ensure meta object exists working.meta = working.meta || {}; } // Build fresh payloads from this getRoom const npcsIncoming = {}; if (Array.isArray(response.npcs)) { const timeNow = new Date(); for (const npc of response.npcs) { const id = npc.id; const life = npc.vars?.life; const max_life = npc.vars?.max_life; const respawn_left = npc.vars?.respawn_time || 0; // seconds until respawn const respawn_time = timeNow.getTime() + respawn_left * 1000; // absolute ms npcsIncoming[id] = { name: npc.name, life, max_life, respawn_time }; } } const gatesIncoming = {}; if (Array.isArray(response.gates)) { const timeNow = new Date().getTime(); for (const gate of response.gates) { const id = gate.id; const unlocked = !!(gate.vars?.unlocked); const next_secs = gate.vars?.unlock_time || 0; const gate_next_change_time = next_secs ? timeNow + next_secs * 1000 : 0; const required_items = {}; if (gate.items) { for (const key in gate.items) { const it = gate.items[key]; required_items[it.name] = it.req_qty; } } const gate_unlock_time = gate_next_change_time; // legacy fallback (renderer uses next_change) gatesIncoming[id] = { name: gate.name, unlocked, gate_next_change_time, gate_unlock_time, unlock_time: next_secs, required_items }; } } // Scavenge only updated when we see it; we don't want to erase existing cooldowns from other sub-areas. const scavengeIncoming = {}; const scavenge_invalid = ["Fuel Pumps","Foundation Pit","Bulk Goods Lockup","Scrap Pile","Warm Springs","Red River","Grand Lake"]; if (Array.isArray(response.scavenge)) { const tNow = new Date().getTime(); for (const s of response.scavenge) { if (scavenge_invalid.includes(s.name)) continue; const cooldown = s.vars?.cooldown || 0; scavengeIncoming[s.id] = { name: s.name, cooldown, cooldown_end: cooldown ? tNow + cooldown * 1000 : -1 }; } } // Merge the new data without wiping other entries (sub-areas) working.npcs = mergeById(working.npcs || {}, npcsIncoming); working.gates = mergeById(working.gates || {}, gatesIncoming); working.scavenge = mergeById(working.scavenge || {}, scavengeIncoming); // Bookkeeping working.meta.lastRoomUpdate = now; all[current_location] = working; writeData(all); } function handleExplorationTrade(response) { const all = readData(); const cur = all[current_location] || { npcs: {}, gates: {}, scavenge: {}, meta: {} }; const now = new Date(); const job = response.job; const wait = job?.vars?.cooldown || 0; cur.scavenge[job.id] = { name: job.name, cooldown: wait, cooldown_end: now.getTime() + wait * 1000 }; all[current_location] = cur; writeData(all); } function handleStartJob(response) { if (response.error) return; const code = response.job?.codename || ""; if (code.startsWith("job_fuel_depot_fuel_trader") || code.startsWith("job_vault_lockbox") || code.startsWith("job_demolition_site")) { handleExplorationTrade(response); } } // --------------------------------- // Collapsible UI (per-location persistent) // --------------------------------- function preventNav(e) { e.preventDefault(); e.stopPropagation(); } function makeCollapsibleSection(title, contentElement, initiallyOpen, locationName) { const slug = `${slugify(locationName)}-${slugify(title)}`; const wrapper = document.createElement("div"); wrapper.className = "collapsible-section explore-ui"; wrapper.setAttribute("data-exploration-ui", "1"); const header = document.createElement("button"); header.type = "button"; header.className = "collapsible-header"; header.setAttribute("aria-expanded", initiallyOpen ? "true" : "false"); header.setAttribute("aria-controls", `${slug}-section`); header.innerHTML = `<span class="arrow">${initiallyOpen ? "▼" : "►"}</span> ${title}`; ["pointerdown","mousedown","touchstart"].forEach(evt => { header.addEventListener(evt, preventNav, true); }); const body = document.createElement("div"); body.id = `${slug}-section`; body.className = "collapsible-body"; body.style.display = initiallyOpen ? "block" : "none"; body.appendChild(contentElement); function toggle(open) { const isOpen = open !== undefined ? open : body.style.display === "none"; body.style.display = isOpen ? "block" : "none"; header.querySelector(".arrow").textContent = isOpen ? "▼" : "►"; header.setAttribute("aria-expanded", isOpen ? "true" : "false"); body.toggleAttribute("hidden", !isOpen); setSectionState(locationName, title, isOpen); // persist } header.addEventListener("click", () => toggle()); header.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); } }); wrapper.appendChild(header); wrapper.appendChild(body); return wrapper; } // compact renderers (3-up, with pipes) function makeSep(){ const s=document.createElement('span'); s.className='sep'; s.textContent=' | '; return s; } function createNPCSection(locationName, npcs){ const content = document.createElement('div'); content.className = 'grid three-up'; if (!npcs || Object.keys(npcs).length === 0){ const empty = document.createElement('div'); empty.className = 'rowline empty'; empty.textContent = 'No NPCs available'; content.appendChild(empty); const open = getSectionState(locationName, 'NPCs', true); return makeCollapsibleSection('NPCs', content, open, locationName); } const now = Date.now(); for (const id in npcs){ const npc = npcs[id]; const item = document.createElement('div'); item.className = 'rowline'; item.title = npc.name; // HP column const life = npc.life ?? '—'; const maxLife = npc.max_life ?? '—'; const nameEl = document.createElement('span'); nameEl.className = 'name'; nameEl.textContent = npc.name; const hpEl = document.createElement('span'); hpEl.className = 'hp'; hpEl.textContent = `${life}/${maxLife}`; // Timer column const respawnMs = npc.respawn_time ? npc.respawn_time - now : 0; const secs = Math.max(0, Math.floor(respawnMs / 1000)); const timerEl = document.createElement('span'); timerEl.className = 'timer'; timerEl.timeLeft = secs; // updateTimers() decrements this timerEl.textContent = getDayHourMinute(secs); // always DD:HH:MM:SS // Compose row item.appendChild(nameEl); item.appendChild(makeSep()); item.appendChild(hpEl); item.appendChild(makeSep()); item.appendChild(document.createTextNode('{')); item.appendChild(timerEl); item.appendChild(document.createTextNode('}')); content.appendChild(item); } const open = getSectionState(locationName, 'NPCs', true); return makeCollapsibleSection('NPCs', content, open, locationName); } function createGatesSection(locationName, gates){ const content = document.createElement('div'); content.className = 'grid three-up'; if (!gates || Object.keys(gates).length === 0){ const empty = document.createElement('div'); empty.className = 'rowline empty'; empty.textContent = 'No gates available'; content.appendChild(empty); const open = getSectionState(locationName, 'Gates', true); return makeCollapsibleSection('Gates', content, open, locationName); } const now = Date.now(); for (const id in gates){ const gate = gates[id]; const item = document.createElement('div'); item.className = 'rowline'; item.dataset.kind = 'gate'; // Tooltip: full requirement breakdown if present if (gate.required_items && Object.keys(gate.required_items).length){ const list = Object.entries(gate.required_items).map(([n,q]) => `${n} (${q})`).join(', '); item.title = `Items: ${list}`; } // Compute time left (if any) let changeMillis = 0; if (gate.gate_next_change_time) changeMillis = gate.gate_next_change_time - now; else if (gate.gate_unlock_time) changeMillis = gate.gate_unlock_time - now; let secs = Math.max(0, Math.floor(changeMillis / 1000)); const isUnlockedNow = !!gate.unlocked && secs > 0; // only unlocked while timer is running // Left + middle columns const nameEl = document.createElement('span'); nameEl.className = 'name'; nameEl.textContent = gate.name; const stateEl = document.createElement('span'); stateEl.className = isUnlockedNow ? 'state ok' : 'state warn'; stateEl.textContent = isUnlockedNow ? 'Unlocked' : 'Locked'; // Right column let thirdEl; if (secs > 0){ const timerEl = document.createElement('span'); timerEl.className = 'timer'; timerEl.timeLeft = secs; // ticking handled by updateTimers() timerEl.textContent = getDayHourMinute(secs); // always DD:HH:MM:SS thirdEl = timerEl; } else { const needEl = document.createElement('span'); needEl.className = 'needs'; if (gate.required_items && Object.keys(gate.required_items).length){ needEl.textContent = Object.entries(gate.required_items) .map(([n,q]) => `${n} (${q})`) .join(', '); } else { needEl.textContent = '—'; } thirdEl = needEl; } // Compose row item.appendChild(nameEl); item.appendChild(makeSep()); item.appendChild(stateEl); item.appendChild(makeSep()); item.appendChild(document.createTextNode('{')); item.appendChild(thirdEl); item.appendChild(document.createTextNode('}')); content.appendChild(item); } const open = getSectionState(locationName, 'Gates', true); return makeCollapsibleSection('Gates', content, open, locationName); } function createScavengeSection(locationName, scavenge){ const content = document.createElement('div'); content.className = 'grid three-up'; if (!scavenge || Object.keys(scavenge).length === 0){ const empty = document.createElement('div'); empty.className = 'rowline empty'; empty.textContent = 'No loot crates found'; content.appendChild(empty); const open = getSectionState(locationName, 'Loot Crates', true); return makeCollapsibleSection('Loot Crates', content, open, locationName); } const now = Date.now(); for (const id in scavenge){ const s = scavenge[id]; const item = document.createElement('div'); item.className = 'rowline'; item.dataset.kind = 'scavenge'; // so updateTimers knows how to flip state const nameEl = document.createElement('span'); nameEl.className = 'name'; nameEl.textContent = s.name || 'Loot'; // time remaining (we persist cooldown_end in ms) let secs = 0; if (typeof s.cooldown_end === 'number' && s.cooldown_end > 0){ secs = Math.max(0, Math.floor((s.cooldown_end - now) / 1000)); } // state badge const stateEl = document.createElement('span'); if (secs === 0){ stateEl.className = 'state ok'; stateEl.textContent = 'Ready'; } else { stateEl.className = 'state warn'; stateEl.textContent = 'Cooling'; } // right column: the live timer (always show; freezes at 00:00:00:00) const timerEl = document.createElement('span'); timerEl.className = 'timer'; timerEl.timeLeft = secs === 0 ? -1 : secs; // -1 means “finished” to our updater timerEl.textContent = getDayHourMinute(secs); // compose row item.appendChild(nameEl); item.appendChild(makeSep()); item.appendChild(stateEl); item.appendChild(makeSep()); item.appendChild(document.createTextNode('{')); item.appendChild(timerEl); item.appendChild(document.createTextNode('}')); content.appendChild(item); } const open = getSectionState(locationName, 'Loot Crates', true); return makeCollapsibleSection('Loot Crates', content, open, locationName); } // --------------------------------- // Render on Explore screen // --------------------------------- function safeInsertAfter(card, node){ try { if (card && card.parentNode) { card.parentNode.insertBefore(node, card.nextSibling); return true; } } catch(e) {} return false; } function display_exploration_data(){ try { const all = readData(); if (!location.href.includes('/explore')) return; const titles = Array.from(document.querySelectorAll('.job-name')); if (titles.length === 0) return; for (const titleEl of titles){ if (titleEl.hasAttribute('data-exploration-processed')) continue; const locationName = (titleEl.textContent || '').trim(); const locationData = all[locationName]; if (!locationData) { titleEl.setAttribute('data-exploration-processed','1'); continue; } const container = document.createElement('div'); container.className = 'exploration-data-container explore-ui'; container.setAttribute('data-exploration-ui', '1'); container.appendChild(createNPCSection(locationName, locationData.npcs)); container.appendChild(createGatesSection(locationName, locationData.gates)); container.appendChild(createScavengeSection(locationName, locationData.scavenge)); let card = titleEl; let p = titleEl.parentElement; while (p){ const classes = [...p.classList]; if (classes.some(c => c.startsWith('job-cont'))) { card = p; break; } p = p.parentElement; } if (!safeInsertAfter(card, container)) { card.insertAdjacentElement('afterend', container); } titleEl.setAttribute('data-exploration-processed','1'); } } catch(err){ console.error('[Exploration UI] render error:', err); } } // --------------------------------- // Wiring (guarded push/replace wrappers) // --------------------------------- window.addEventListener('xhrIntercepted', (e) => { const { url, response } = e.detail; if (String(url).includes('getLocation')) { handleLocation(response); } else if (String(url).includes('getRoom')) { saveExplorationData(response); display_exploration_data(); } else if (String(url).includes('startJob')) { handleStartJob(response); } }); function onRouteChange(){ setTimeout(() => { if (location.href.includes('/explore')) display_exploration_data(); }, 300); } if (!history.__zedExploreWrapped){ const origPush = history.pushState; history.pushState = function(){ const r = origPush.apply(this, arguments); onRouteChange(); return r; }; const origReplace = history.replaceState; history.replaceState = function(){ const r = origReplace.apply(this, arguments); onRouteChange(); return r; }; Object.defineProperty(history, '__zedExploreWrapped', { value: true, configurable: false }); } window.addEventListener('hashchange', onRouteChange); window.addEventListener('popstate', onRouteChange); const mo = new MutationObserver(() => { if (location.href.includes('/explore')) display_exploration_data(); }); mo.observe(document.documentElement, { childList: true, subtree: true }); const timerId = setInterval(updateTimers, 1000); // Stop timers if user turns the toggle off without reload (defensive) window.addEventListener('beforeunload', () => clearInterval(timerId)); // --------------------------------- // Styling // --------------------------------- const style = document.createElement('style'); style.textContent = ` .exploration-data-container { margin-top:4px; padding:4px; border-radius:4px; background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.02)); backdrop-filter: blur(2px); position: relative; z-index: 1; font-size:10px; box-shadow: 0 1px 8px rgba(0,0,0,.12); } .collapsible-header { cursor:pointer; font-weight:700; letter-spacing:.2px; margin:4px 0 4px; user-select:none; display:flex; align-items:center; gap:4px; background:transparent; border:0; padding:0; color:inherit; font:inherit; text-align:left; } .collapsible-header .arrow { width:1em; text-align:center; } .collapsible-body { margin-left:0; } .grid.three-up { display:grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap:0; border:1px solid rgba(255,255,255,0.08); border-radius:8px; overflow:hidden; } .grid.three-up .rowline { padding:4px 4px; display:flex; align-items:baseline; gap:4px; min-width:0; white-space:nowrap; overflow:hidden; background: rgba(255,255,255,0.02); transition: background .15s ease, transform .05s ease; } .grid.three-up .rowline:nth-child(odd) { background: rgba(255,255,255,0.015); } .grid.three-up .rowline:hover { background: rgba(255,255,255,0.06); } .grid.three-up .rowline .name { font-weight:600; overflow:hidden; text-overflow:ellipsis; } .grid.three-up .sep { opacity:.5; } .grid.three-up .timer.alert { color:#ff4d4f; } .rowline.empty { padding:6px; opacity:.75; } .state.ok { color:#9cff9c; } .state.warn { color:#ffae7a; } `; style.textContent += ` .grid.three-up .needs { opacity:.9; font-style:italic; } `; document.head.appendChild(style); })(); } // ---------- Market selling ---------- function run_marketSelling(){ (function(){ 'use strict'; const MARKET_KEY = "Zed-market-data"; let modalActive = false; function getMarketMap(){ try { return JSON.parse(localStorage.getItem(MARKET_KEY) || "{}"); } catch { return {}; } } function fillSellPriceRespectfully(){ const titleDiv = document.querySelector(".title div"); if (!titleDiv || !/Create Listing/i.test(titleDiv.textContent || "")) return false; const nameEl = document.querySelector(".q-py-sm > div:nth-child(1)"); if (!nameEl) return false; const itemName = (nameEl.textContent || "").trim(); if (!itemName) return false; const market = getMarketMap(); const price = Number(market[itemName]); if (!Number.isFinite(price)) return false; const moneyWrap = document.querySelector(".zed-money-input"); const input = moneyWrap && moneyWrap.querySelector("input"); if (!input) return false; // Respect the user: only prefill if empty or previously autofilled const alreadyAuto = input.dataset.autofilled === "1"; const emptyField = (input.value || "").trim() === ""; if (!alreadyAuto && !emptyField) return true; // user already typed something // Don’t change while the user is actively typing if (document.activeElement === input && !alreadyAuto) return true; input.value = String(price - 1); input.dataset.autofilled = "1"; input.dispatchEvent(new Event("input", { bubbles: true })); // First user edit disables further autofill for this modal const onUserEdit = () => { delete input.dataset.autofilled; input.removeEventListener("input", onUserEdit); }; input.addEventListener("input", onUserEdit); return true; } // Watch for the Create Listing modal appearing/disappearing const mo = new MutationObserver(() => { const isOpen = !!document.querySelector(".title div") && /Create Listing/i.test((document.querySelector(".title div")?.textContent || "")); if (isOpen && !modalActive) { modalActive = true; // Try a few times to catch late-drawn inputs, then stop let tries = 8; const tryFill = () => { if (fillSellPriceRespectfully() || --tries <= 0 || !modalActive) return; setTimeout(tryFill, 60); }; tryFill(); } else if (!isOpen && modalActive) { // Modal closed: reset state for next time modalActive = false; } }); mo.observe(document.documentElement, { childList: true, subtree: true }); // Also try once shortly in case the modal is already open setTimeout(() => { if (!modalActive) fillSellPriceRespectfully(); }, 200); })(); } // ---------- Store Remaining ---------- function run_storeRemainingAmounts() { if (window.__SRA_INIT__) return; window.__SRA_INIT__ = true; const ITEM_LIMIT = 360; let used = 0; const stock = Object.create(null); const inv = Object.create(null); function handleStore(resp){ const total = +resp?.limits?.limit || 0; const left = +resp?.limits?.limit_left || 0; used = Math.max(0, total - left); for (const it of (resp?.storeItems || [])) stock[it.name] = +it.quantity || 0; for (const it of (resp?.userItems || [])) inv[it.name] = +it.quantity || 0; microTry(insertBadge); //microTry(autofillQty); -- BUG } function microTry(fn, tries=10){ (function loop(){ if (fn() === true || --tries <= 0) return; setTimeout(loop, 50); })(); } function insertBadge(){ const host = document.querySelector('.text-h4'); if (!host) return false; let badge = document.getElementById('zed-item-limit'); if (!badge){ badge = document.createElement('div'); badge.id = 'zed-item-limit'; host.appendChild(badge); } badge.textContent = ` [ ${used} / ${ITEM_LIMIT} ]`; host.style.color = used < ITEM_LIMIT ? '#6fcf97' : '#e76f51'; return true; } /*function autofillQty(){ ///TODO FIX BUG CAUSING MASS DELITION OF ITEMS BY ACCIDENT - NOT GOOD!! BAD CODE. Hello, if you are reading this :) const nameEl = document.querySelector('.small-modal > div:nth-child(1)'); const qtyInput = document.querySelector('.small-modal div.grid-cont input'); const actionEl = document.querySelector('div.text-center:nth-child(2) > button span span'); if (!nameEl || !qtyInput || !actionEl) return false; const item = (nameEl.textContent || '').trim(); const buying = /Buy/i.test(actionEl.textContent || ''); let qty = 0; if (buying){ const remaining = Math.max(0, ITEM_LIMIT - used); qty = Math.min(remaining, +stock[item] || 0); } else { qty = +inv[item] || 0; } qtyInput.value = String(qty); qtyInput.dispatchEvent(new Event('input', { bubbles:true })); return true; }*/ // ---- Chain-safe XHR hook: tap current impl, don't clobber it ---- (function hookXHROnce(){ if (XMLHttpRequest.prototype.__sraHooked__) return; const prevOpen = XMLHttpRequest.prototype.open; const prevSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url){ this.__sra_url = url || ''; return prevOpen.apply(this, arguments); // chain }; XMLHttpRequest.prototype.send = function(body){ // read after completion this.addEventListener('readystatechange', function(){ if (this.readyState !== 4) return; try { const url = String(this.__sra_url || ''); if (url.includes('store_id')) { const json = JSON.parse(this.responseText || 'null'); if (json) handleStore(json); } } catch {} }); return prevSend.apply(this, arguments); // chain }; Object.defineProperty(XMLHttpRequest.prototype, '__sraHooked__', { value: true }); })(); // ---- Chain-safe fetch hook (tiny) ---- (function hookFetchOnce(){ if (window.__sraFetchHooked__) return; window.__sraFetchHooked__ = true; const prevFetch = window.fetch; window.fetch = async function(){ const res = await prevFetch.apply(this, arguments); try { const req = arguments[0]; const url = typeof req === 'string' ? req : (req && req.url) || ''; if (url.includes('store_id')) { const clone = res.clone(); const json = await clone.json(); if (json) handleStore(json); } } catch {} return res; }; })(); // also try filling when user opens the modal ---- BUG // document.addEventListener('click', () => microTry(autofillQty), true); } // =============================== // Rad Tracker (uses Zed-market-data + codename index) // =============================== function run_radTracker(){ (function(){ // --- keys from your market module const MARKET_KEY = "Zed-market-data"; const CODEMAP_KEY = "Zed-market-codemap"; const ROI_KEY = "Zed-explore-roi"; // init ROI store if (!localStorage.getItem(ROI_KEY)) { localStorage.setItem(ROI_KEY, JSON.stringify({ trip:{ rads:0, value:0 }, lastSummaryAt:0 })); } // ---------- small storage helpers ---------- const readROI = () => { try { return JSON.parse(localStorage.getItem(ROI_KEY)) || { trip:{rads:0,value:0}, lastSummaryAt:0 }; } catch { return { trip:{rads:0,value:0}, lastSummaryAt:0 }; } }; const writeROI = (roi) => localStorage.setItem(ROI_KEY, JSON.stringify(roi)); const readMarket = () => { try { return JSON.parse(localStorage.getItem(MARKET_KEY) || "{}"); } catch { return {}; } }; const readCodeMap = () => { try { return JSON.parse(localStorage.getItem(CODEMAP_KEY) || "{}"); } catch { return {}; } }; const writeCodeMap = (m) => localStorage.setItem(CODEMAP_KEY, JSON.stringify(m || {})); // ---------- UI helpers ---------- function roiGet(){ const r = readROI(); return { ...r.trip, roi: r.trip.rads > 0 ? (r.trip.value / r.trip.rads) : 0 }; } function roiAddRads(n){ if (!Number.isFinite(+n) || !+n) return; const r=readROI(); r.trip.rads = Math.max(0,(r.trip.rads||0)+(+n)); writeROI(r); refreshRoiPanel(); } function roiAddValue(n){ if (!Number.isFinite(+n) || !+n) return; const r=readROI(); r.trip.value= Math.max(0,(r.trip.value||0)+(+n)); writeROI(r); refreshRoiPanel(); } function roiClearAll(){ writeROI({ trip:{rads:0,value:0}, lastSummaryAt:0 }); refreshRoiPanel(); } // ---------- build a codename→name/price index whenever getMarket is seen ---------- // getMarket responses have items with { name, codename, market_price }. // We'll mirror them into CODEMAP_KEY for fast codename lookups. if (typeof addNetListener === "function") { addNetListener(/getMarket(?!User)/i, (_url, resp) => { try { const items = (resp && (resp.items || (resp.data && resp.data.items) || Array.isArray(resp) && resp)) || []; if (!Array.isArray(items) || !items.length) return; const m = readCodeMap(); const now = Date.now(); for (const it of items) { const code = it?.codename; const name = it?.name; const price = Number(it?.market_price); if (!code || !name) continue; const entry = m[code] || {}; m[code] = { name, // prefer latest numeric price if present price: Number.isFinite(price) ? price : (Number.isFinite(entry.price) ? entry.price : undefined), ts: now }; } writeCodeMap(m); } catch {} }); } // ---------- pricing helper (codename → unit price) ---------- function unitPriceForCodename(code, rewardVars){ if (!code) return null; // 1) Try codemap direct (latest market_price we saw with this codename) const cm = readCodeMap()[code]; if (cm && Number.isFinite(cm.price)) return cm.price; // 2) Try codemap name → market cache (name -> price in MARKET_KEY) // This covers cases where our codemap has name but not price yet. if (cm && typeof cm.name === "string") { const named = readMarket()[cm.name]; if (Number.isFinite(+named)) return +named; } // 3) Last resort: if reward payload has hints const fallbacks = [ Number(rewardVars?.value), Number(rewardVars?.vars?.sell), Number(rewardVars?.vars?.buy) ]; for (const v of fallbacks) if (Number.isFinite(v)) return v; // 4) Also try a naive name lookup if the reward included a readable name // (some APIs send both name and codename) const nameFromReward = rewardVars?.name; if (nameFromReward) { const byName = readMarket()[nameFromReward]; if (Number.isFinite(+byName)) return +byName; } return null; } // ---------- Start_job valuation ---------- function valueStartJob(resp){ const outcome = resp?.outcome || {}; const job = resp?.job || {}; const iters = Number.isFinite(outcome.iterations) ? outcome.iterations : 1; // Rads from requirement "rad" in job.items (req_qty * iterations) let radSpent = 0; if (job.items) { for (const k of Object.keys(job.items)) { const req = job.items[k]; if (req?.codename === "rad") radSpent += (Number(req.req_qty) || 0) * iters; } } // Loot value = Σ (posted_qty × unitPriceForCodename) let valueAdd = 0; for (const r of Array.isArray(outcome.rewards) ? outcome.rewards : []) { const code = r.codename || r.name || "unknown"; const qty = Number.isFinite(r.posted_qty) ? r.posted_qty : 0; if (qty <= 0) continue; const unit = unitPriceForCodename(code, r); if (Number.isFinite(+unit)) valueAdd += (+unit) * qty; } return { radSpent, valueAdd }; } // ---------- scan network responses ---------- function scanResponse(url, response){ try{ if (!/start_job|startJob/i.test(url)) return; if (!response?.outcome) return; const { radSpent, valueAdd } = valueStartJob(response); if (radSpent > 0) roiAddRads(radSpent); if (valueAdd > 0) roiAddValue(valueAdd); // console.debug("[radTracker] Δ", { radSpent, valueAdd }); } catch {} } // ---------- wire to XHR/fetch (guarded) ---------- (function (){ const FLAG = '__radTrackerWrapped'; if (XMLHttpRequest.prototype[FLAG]) return; const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this._url = url; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { this.addEventListener("readystatechange", function () { if (this.readyState === 4) { try { const text = this.responseText || ""; const first = text[0]; if (!text || (first !== '{' && first !== '[')) return; const response = JSON.parse(text); scanResponse(this._url || "", response); } catch {} } }); return originalSend.apply(this, arguments); }; Object.defineProperty(XMLHttpRequest.prototype, FLAG, { value: true, configurable: false }); })(); // ---------- mini UI ---------- let roiPanel, roiBtn; function refreshRoiPanel(){ if (!roiPanel) return; const m = roiGet(); roiPanel.querySelector(".erp-rads").textContent = m.rads.toFixed(0); roiPanel.querySelector(".erp-value").textContent = m.value.toFixed(0); roiPanel.querySelector(".erp-vpr").textContent = (m.rads > 0 ? m.roi : 0).toFixed(2); } function toggleRoiPanel(){ if (!roiPanel) return; roiPanel.classList.toggle('open'); if (roiPanel.classList.contains('open')) refreshRoiPanel(); } function buildRoiPanel(){ if (roiPanel) return roiPanel; roiPanel = document.createElement('div'); roiPanel.className = 'explore-roi-panel'; roiPanel.innerHTML = ` <div class="erp-head"><strong>RAD ROI</strong><button class="erp-close" aria-label="Close">✕</button></div> <div class="erp-body"> <div class="erp-row"><span>Rads spent:</span><b class="erp-rads">0</b></div> <div class="erp-row"><span>Loot value:</span><b class="erp-value">0</b></div> <div class="erp-row"><span>Value / Rad:</span><b class="erp-vpr">0</b></div> </div> <div class="erp-actions"><button class="erp-reset">Reset</button></div> `; roiPanel.querySelector(".erp-close").addEventListener("click", toggleRoiPanel); roiPanel.querySelector(".erp-reset").addEventListener("click", () => roiClearAll()); document.body.appendChild(roiPanel); refreshRoiPanel(); return roiPanel; } function ensureRoiButton(){ if (roiBtn) return roiBtn; roiBtn = document.createElement('button'); roiBtn.className = 'explore-roi-btn'; roiBtn.type = 'button'; roiBtn.title = 'Show ROI'; roiBtn.textContent = 'ROI'; roiBtn.addEventListener('click', () => { buildRoiPanel(); toggleRoiPanel(); }); const gearLike = document.querySelector('.gear, .settings, .zed-gear, .zed-options, [data-gear], [data-zed-gear]'); if (gearLike?.parentElement) gearLike.parentElement.appendChild(roiBtn); else document.body.appendChild(roiBtn); return roiBtn; } const style = document.createElement('style'); style.textContent = ` .explore-roi-btn { position:fixed; right:12px; bottom:58px; z-index:9999; padding:6px 12px; font-weight:600; font-size:12px; border-radius:8px; border:1px solid rgba(255,255,255,.15); background:rgba(20,20,28,.75); color:#fff; cursor:pointer; box-shadow: 0 3px 12px rgba(0,0,0,.4); backdrop-filter: blur(5px); transition: all .15s ease; } .explore-roi-btn:hover { background:rgba(32,34,44,.9); transform: translateY(-1px); } .explore-roi-panel { position:fixed; right:12px; bottom:110px; z-index:10000; width:240px; display:none; padding:12px; border-radius:12px; background:rgba(16,18,22,.95); color:#fff; border:1px solid rgba(255,255,255,.12); box-shadow:0 8px 28px rgba(0,0,0,.55); backdrop-filter: blur(8px); font-size:13px; } .explore-roi-panel.open { display:block; } .erp-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; font-weight:600; font-size:14px; } .erp-body { display:grid; grid-template-columns: 1fr auto; gap:8px 6px; margin-bottom:10px; } .erp-row { grid-column: 1 / -1; display:flex; align-items:center; justify-content:space-between; padding:6px 8px; border-radius:8px; background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.08); } .erp-label { font-weight:500; color:#e2e2e2; } .erp-value { font-weight:600; color:#fff; font-variant-numeric: tabular-nums; } .erp-actions { display:flex; gap:6px; margin-top:8px; } .erp-actions button { flex:1 1 auto; padding:6px 10px; border-radius:6px; border:1px solid rgba(255,255,255,.18); background:rgba(32,36,42,.85); color:#fff; cursor:pointer; font-size:12px; transition: all .15s ease; } .erp-actions button:hover { background:rgba(45,50,60,.9); } `; document.head.appendChild(style); ensureRoiButton(); buildRoiPanel(); })(); } // ---------- Market Favs ---------- function run_Durability(){ // Lite Durability/Condition annotator — FIXED (plain object) (function(){ const TIER_USES = { brittle:10,"very weak":25,poor:50,weak:100,moderate:150,tempered:200, resilient:250,durable:300,reinforced:350,robust:500,"long lasting":1000, enduring:2000,pristine:3000,embued:4000,imbued:4000,immaculate:5000 }; const TIERS = Object.keys(TIER_USES); const norm = s => String(s||'').toLowerCase().replace(/\s+/g,' ').trim(); function readTierWord(el){ const txt = norm(el.textContent||''); return TIERS.find(t => txt.includes(t)) || null; } function findConditionPercent(scope){ const condEl = scope.querySelector('.stat-condition') || (() => { const blocks = scope.querySelectorAll('.stat-block'); for (const b of blocks) { const l = b.querySelector('.stat-label'); if (l && norm(l.textContent)==='condition') return b; } return null; })(); if (!condEl) return null; const m = (condEl.textContent||'').match(/(\d{1,3})\s*%/); return m ? Math.min(100, Math.max(0, parseInt(m[1],10))) / 100 : null; } function update(root=document){ root.querySelectorAll('.stat-block').forEach(block=>{ const lbl = block.querySelector('.stat-label'); if (!lbl || norm(lbl.textContent) !== 'durability') return; const val = block.querySelector('.stat-durability') || block.querySelector('.stat-value'); if (!val) return; const tierWord = readTierWord(val); if (!tierWord) return; const max = TIER_USES[tierWord]; if (!max) return; const grid = block.closest('.item-stats-grid') || document; const pct = findConditionPercent(grid); const text = (pct!=null) ? `${Math.round(max*pct)}/${max}` : `${max}`; let badge = val.querySelector('.zed-dur-uses-badge'); if (!badge) { badge = document.createElement('span'); badge.className = 'zed-dur-uses-badge'; badge.style.cssText = 'margin-left:.5rem;font-size:12px;opacity:.9;'; val.appendChild(badge); } badge.textContent = text; }); } function scheduleBurst(){ requestAnimationFrame(update); setTimeout(update, 80); setTimeout(update, 200); setTimeout(update, 450); } document.addEventListener('click', scheduleBurst, true); document.addEventListener('keyup', e => { if (e.key==='Enter'||e.key===' ') scheduleBurst(); }, true); new MutationObserver(m=>{ for (const x of m) for (const n of x.addedNodes||[]) if (n.nodeType===1 && n.querySelector?.('.stat-durability,.stat-condition,.item-stats-grid,.stat-block')) update(n); }) .observe(document.body,{childList:true,subtree:true}); setTimeout(update, 200); })(); } // =============================== // Market Buy Tracker // =============================== function run_marketBuyTracker(){ (function(){ const API_ITEMS = "https://api.zed.city/loadItems"; const MARKET_KEY = "Zed-market-data"; const MBT_LOG_KEY = "Zed-market-buys-log"; // array of entries to persist const POLL_MS = 30_000; let lastSnapshot = new Map(); let lastSnapshotAt = 0; let pollTimer = null; let inFlightBuy = false; let panel, btn, list; const nowISO = () => new Date().toLocaleString(); const readMarket = () => { try { return JSON.parse(localStorage.getItem(MARKET_KEY)||"{}"); } catch { return {}; } }; const readLog = () => { try { return JSON.parse(localStorage.getItem(MBT_LOG_KEY)||"[]"); } catch { return []; } }; const writeLog = (arr) => localStorage.setItem(MBT_LOG_KEY, JSON.stringify(arr.slice(-500))); // keep last 500 function indexInv(json){ const map = new Map(); const items = Array.isArray(json?.items) ? json.items : []; for (const it of items){ const key = it.codename || it.id || it.name; const qty = Number(it.quantity)||0; map.set(key, qty); } return map; } async function fetchInventory(){ try{ const r = await fetch(API_ITEMS, {credentials:"include"}); if (!r.ok) return; const j = await r.json(); lastSnapshot = indexInv(j); lastSnapshotAt = Date.now(); }catch{} } function startPoll(){ clearInterval(pollTimer); pollTimer = setInterval(fetchInventory, POLL_MS); fetchInventory(); } function ensureStyles(){ if (document.getElementById("mbt-style")) return; const css = document.createElement("style"); css.id = "mbt-style"; css.textContent = ` .mbt-btn { position:fixed; right:12px; bottom:140px; z-index:9999; padding:6px 12px; font-weight:600; font-size:12px; border-radius:8px; border:1px solid rgba(255,255,255,.15); background:rgba(20,20,28,.75); color:#fff; cursor:pointer; box-shadow:0 3px 12px rgba(0,0,0,.4); backdrop-filter: blur(5px); transition:all .15s ease; } .mbt-btn:hover { background:rgba(32,34,44,.9); transform: translateY(-1px); } .mbt-panel { position:fixed; right:12px; bottom:164px; z-index:10000; width:300px; display:none; padding:12px; border-radius:12px; background:rgba(16,18,22,.95); color:#fff; border:1px solid rgba(255,255,255,.12); box-shadow:0 8px 28px rgba(0,0,0,.55); backdrop-filter: blur(8px); font-size:12px; } .mbt-panel.open { display:block; } .mbt-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; font-weight:700; } .mbt-list { max-height:260px; overflow:auto; display:flex; flex-direction:column; gap:6px; padding-right:2px; } .mbt-row { display:grid; grid-template-columns: 1fr auto; gap:4px 8px; padding:6px 8px; border-radius:8px; background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.08); } .mbt-row .meta { grid-column:1 / -1; opacity:.85; font-size:11px; display:flex; gap:6px; flex-wrap:wrap; } .mbt-row .name { font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .mbt-row .qty { font-variant-numeric: tabular-nums; } .mbt-row .price { justify-self:end; font-variant-numeric: tabular-nums; } .mbt-actions { display:flex; gap:6px; margin-top:8px; } .mbt-actions button { flex:1 1 auto; padding:6px 10px; border-radius:6px; border:1px solid rgba(255,255,255,.18); background:rgba(32,36,42,.85); color:#fff; cursor:pointer; font-size:12px; transition:.15s ease; } .mbt-actions button:hover { background:rgba(45,50,60,.9); } `; document.head.appendChild(css); } function ensureUI(){ ensureStyles(); if (!btn){ btn = document.createElement("button"); btn.className = "mbt-btn"; btn.textContent = "Buys"; btn.title = "Show Market Buys"; btn.addEventListener("click", () => { ensurePanel(); panel.classList.toggle("open"); }); document.body.appendChild(btn); } ensurePanel(); } function ensurePanel(){ if (panel) return panel; panel = document.createElement("div"); panel.className = "mbt-panel"; panel.innerHTML = ` <div class="mbt-head"><span>MARKET BUYS</span><button class="mbt-close" aria-label="Close">✕</button></div> <div class="mbt-list"></div> <div class="mbt-actions"> <button class="mbt-clear">Clear</button> </div> `; panel.querySelector(".mbt-close").onclick = () => panel.classList.toggle("open"); panel.querySelector(".mbt-clear").onclick = () => { writeLog([]); renderFromStorage(); }; list = panel.querySelector(".mbt-list"); document.body.appendChild(panel); renderFromStorage(); return panel; } function renderFromStorage(){ if (!list) return; const data = readLog().slice().reverse(); list.innerHTML = ""; for (const entry of data){ const row = document.createElement("div"); row.className = "mbt-row"; const spend = Number(entry.cost)||0; const unitTxt = Number.isFinite(entry.unit) ? `$${Math.round(entry.unit).toLocaleString()}` : "—"; row.innerHTML = ` <div class="name">${entry.name} <span class="qty">×${entry.qty}</span></div> <div class="price">$${Math.round(spend).toLocaleString()}</div> <div class="meta"> <span>${entry.time}</span> <span>Unit: ${unitTxt}</span> </div> `; list.appendChild(row); } } function pushLog(entry){ const arr = readLog(); arr.push(entry); writeLog(arr); ensureUI(); const row = document.createElement("div"); row.className = "mbt-row"; const unitTxt = Number.isFinite(entry.unit) ? `$${Math.round(entry.unit).toLocaleString()}` : "—"; row.innerHTML = ` <div class="name">${entry.name} <span class="qty">×${entry.qty}</span></div> <div class="price">$${Math.round(entry.cost||0).toLocaleString()}</div> <div class="meta"> <span>${entry.time}</span> <span>Unit: ${unitTxt}</span> </div> `; list?.insertBefore(row, list.firstChild || null); } function computeDiff(prev, next, nameByKey){ const diffs = []; const keys = new Set([...prev.keys(), ...next.keys()]); for (const k of keys){ const a = Number(prev.get(k)||0); const b = Number(next.get(k)||0); const d = b - a; if (d > 0){ const name = nameByKey?.get(k) || String(k); diffs.push({ key:k, name, qty:d }); } } return diffs; } function buildNameMap(json){ const map = new Map(); const items = Array.isArray(json?.items) ? json.items : []; for (const it of items){ const key = it.codename || it.id || it.name; map.set(key, it.name || it.codename || String(it.id)); } return map; } async function handleBuy(url, resp){ if (inFlightBuy) return; inFlightBuy = true; try{ const bag = Array.isArray(resp?.reactive_items_qty) ? resp.reactive_items_qty : []; if (!bag.length) { inFlightBuy = false; return; } clearInterval(pollTimer); const before = new Map(lastSnapshot); const r = await fetch(API_ITEMS, {credentials:"include"}); if (!r.ok) { startPoll(); inFlightBuy = false; return; } const inv = await r.json(); const after = indexInv(inv); const names = buildNameMap(inv); const changes = computeDiff(before, after, names); const unitPrices = readMarket(); const time = nowISO(); for (const ch of changes){ const unit = Number(unitPrices[ch.name]); const cost = Number.isFinite(unit) ? unit * ch.qty : 0; pushLog({ time, name: ch.name, qty: ch.qty, unit: Number.isFinite(unit) ? unit : null, cost, method: 'marketBuyItem' }); } lastSnapshot = after; lastSnapshotAt = Date.now(); } catch (e) { } finally { startPoll(); inFlightBuy = false; } } if (typeof addNetListener === "function"){ addNetListener(/marketBuyItem/i, handleBuy); } startPoll(); document.addEventListener("visibilitychange", () => { if (!document.hidden) fetchInventory(); }); ensureUI(); renderFromStorage(); })(); } // ---------- Boot toggles ---------- if (RUN_MARKET) run_MarketFavs(); if (RUN_PROFIT) run_ProfitHelper(); if (RUN_NETWORTH) run_networth(); if (RUN_TIMERS) run_Timers(); if (RUN_EXPLORATION) run_ExplorationData(); if (RUN_MARKETSELLING) run_marketSelling(); if (RUN_STOREREMAININGAMOUNTS) run_storeRemainingAmounts(); if (RUN_RADTRACKER) run_radTracker(); if (RUN_DURABILITY) run_Durability(); if (RUN_MARKETBUYTRACKER) run_marketBuyTracker(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址