您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Misc helper tools for Zed City
// ==UserScript== // @name ZedHelper // @description Misc helper tools for Zed City // @version 0.4.17 // @namespace kvassh.zedhelper // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=zed.city // @homepage https://greasyfork.org/en/scripts/527868-zedhelper // @author Kvassh // @match https://www.zed.city/* // @run-at document-end // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant window.onurlchange // @connect api.zed.city // ==/UserScript== /** * ZedHelper * * Features: * - Displays market value for items in inventory * - Calculates your inventory networth based on current market values * - Extra nav menu with some useful shortcuts (togglable in settings) * - Autopopulates gym train input fields to use maximum energy * - Autopopulates input field for junk shop with 360 item buy qty * - Show value of trades at Radio Tower * - Show timer for various features (Raid, Junk Store limit) * * If you have any questions, feel free to reach out to Kvassh [12853] in Zed City * * Changelog: * - 0.4.17: Add Max Rad btn on scavenge page * Make icons/links smaller on the timer bar * - 0.4.16: Try to click MAX button to set input to max in stores * Add more buttons to timer bar (Gym, RadioTower, Scavenge) * Make the timer links go to the page without reloading webbrowser page * - 0.4.15: Add bulk scavenge buttons for 5 and 30 scavenges at a time. * Fix bug in Radio Tower trade value calculation. * - 0.4.14: Another fix for the duplicate timer bar issue, hopefully 100% fixed now. * - 0.4.13: Fix error with duplicate timer bar appearing. * Add link to respective functions for timers also when not ready * - 0.4.12: Fix another bug in time parsing for timer bar * - 0.4.11: Fix bug in time parsing for timer bar * - 0.4.10: Fix correct link for Raid timer shortcut * - 0.4.9: Add timer for Raid * - 0.4.8: Add timer for Junk Store limit * - 0.4.7: Fix container width for mobile in Settings page. * - 0.4.6: Add ZH icon to statusbar that points to new Settings page. * Add setting for toggling extra nav menu on or off. * Include cash on hand when calculating networth. * - 0.4.5: Avoid duplicate inventory networth elements * Less padding for item values in inventory to fit better on mobile. * - 0.4.4: Change homepage and downloadURL to use greasyfork.org + change icon to zed.city favicon. * - 0.4.3: Use navigation navigate eventlistener instead to detect page change. * - 0.4.2: Try to force window eventlistener for urlchange to work on mobile. * - 0.4.1: Show warning if market values has not been cached yet. * Show warning on Radio Tower if the cached data is old. * Indicate if the trade is good or bad with checkmark on Radio Tower. * Fixed bug on inventory page where it would potentially not update prices if changing to next page in inventorylist. * - 0.4: Add value of trades at Radio Tower. * - 0.3: Fix bug in gym autopopulate + add new autopopulate in junk store for 360 items. * - 0.2: Add feature to autopopulate gym input fields. * - 0.1: Initial release. */ (function() { 'use strict'; // Add CSS for displaying prices (optional, but makes it look nicer) GM_addStyle(` .market-price { color:#999999; float:right; position:absolute; top:18px; right:100px; } .green { color: #00cc66; } .red { color: #ff6666; } .gray { color: #888; } .zedhelper-networth { text-align: center; margin: 10px auto; color: #ccc; font-size: 1.6rem; } .zedhelper-inventory-warning { text-align: center; margin: 10px auto; color: #ccc; font-size: 0.8rem; } .radio-warning { text-align: center; } .zedhelper-timer-bar { margin-top:0px; } .zedhelper-timer-span { padding: 0 10px; } .zedhelper-timer-span a { text-decoration:none; } `); const baseApiUrl = 'https://api.zed.city'; /** Dont modify anything below this line */ let module = "index"; let checkForInventoryUpdates = null; /** Utils */ function get(key) { return localStorage.getItem(`kvassh.zedhelper.${key}`); } function set(key, value) { localStorage.setItem(`kvassh.zedhelper.${key}`, value); } function log(msg) { const spacer = " "; const ts = new Date(); console.log("ZedHelper (" + ts.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' } )+ ") " + "[" + module + "]" + ((module.length < spacer.length) ? spacer.substring(0, spacer.length - module.length) : "") + ": " + (typeof msg === 'object' ? JSON.stringify(msg) : msg)); } function waitForElement(selector) { return new Promise(resolve => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336 observer.observe(document.body, { childList: true, subtree: true }); }); } function getCodename(itemName) { let codename = itemName.toString().toLowerCase().replace(' ', '_').trim().split(/\n/)[0]; let nametable = { "arrows": "ammo_arrows", "bows": "ammo_bows", "logs": "craft_log", "nails": "craft_nails", "rope": "craft_rope", "scrap": "craft_scrap", "wire": "craft_wire", "army_helmet": "defense_army_helmet", "camo_hat": "defense_camo_hat", "camo_vest": "defense_camo_vest", "e-cola": "ecola", "lighter":"misc_lighter", "lockpick":"misc_lockpick", "pickaxe":"misc_pickaxe", "security_card":"defense_security_card", "zed_coin": "points", "baseball_bat": "weapon_baseball_bat", "bow":"weapon_bow", "chainsaw":"weapon_chainsaw", "spear":"weapon_spear", "switchblade":"weapon_switchblade", }; for (const [key, value] of Object.entries(nametable)) { if (codename === key) { return value; } } return codename; } function formatNumber(number) { const formatter = new Intl.NumberFormat('nb-NO', { maximumFractionDigits: 0, }); return formatter.format(number); } /** XHR Interceptor */ const originalXHR = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (...args) { this.addEventListener('load', function () { const url = this.responseURL; // if (url.includes("/getOffers")) { // const item = JSON.parse(this.responseText)[0]; // log(`Caching market value for: ${item['name']} (${item['codename']})`); // set(`mv_${item["codename"]}`, JSON.stringify({ "name": item["name"], "marketValue": item["market_price"], "tz": Date.now() })); // } if (url.endsWith("/getMarket")) { const items = JSON.parse(this.responseText).items; let itemsCached = 0; for (let item of items) { let codename = getCodename(item["name"]); set(`mv_${codename}`, JSON.stringify({ "name": item["name"], "marketValue": item["market_price"], "tz": Date.now() })); itemsCached++; } set(`mv_lastupdate`, Date.now()); log(`Cached market value for ${itemsCached} items.`); } else if (url.endsWith("/loadItems")) { const data = JSON.parse(this.responseText); const items = data.items; let networthVendor = 0; let networthMarket = 0; for (let item of items) { networthVendor += item.value * item.quantity; const codename = item.codename; if(get(`mv_${codename}`)) { const mv = JSON.parse(get(`mv_${codename}`)); networthMarket += mv.marketValue * item.quantity; } else { networthMarket += item.value * item.quantity; } } set(`mv_networth_vendor`, networthVendor); set(`mv_networth_market`, networthMarket); log(`cached inventory networth (vendor: ${networthVendor}, market: ${networthMarket})`); } else if (url.endsWith("/getStats")) { const data = JSON.parse(this.responseText); set(`energy`, data.energy); set(`morale`, data.morale); set(`rad`, data.rad); set(`refills`, data.refills); set(`money`, data.money); set(`xpUntilNextRank`, parseInt(data.xp_end-data.experience)); set(`raidCooldownSecondsLeft`, data.raid_cooldown); set(`raidCooldownTime`, Date.now()); } else if (url.endsWith("/getRadioTower")) { const data = JSON.parse(this.responseText); saveCurrentTradeValues(data); set(`radio_lastupdate`, Date.now()); } else if (url.endsWith("/getStore?store_id=junk")) { const data = JSON.parse(this.responseText); if (data.hasOwnProperty('limits')) { set(`junkStoreLimitSecondsLeft`, data.limits.reset_time); set(`junkStoreLimitTime`, Date.now()); } else { set(`junkStoreLimitSecondsLeft`, 0); set(`junkStoreLimitTime`, 0); } } else if (url.endsWith("/getStore?store_id=zedmart")) { const data = JSON.parse(this.responseText); if (data.hasOwnProperty('limits')) { set(`zedMartLimitSecondsLeft`, data.limits.reset_time); set(`zedMartLimitTime`, Date.now()); } else { set(`zedMartLimitSecondsLeft`, 0); set(`zedMartLimitTime`, 0); } } }); originalXHR.apply(this, args); }; /** Main script */ log("Starting up ZedHelper!"); let navigationTimeout = null; let urlChangeHandler = async () => { if (navigationTimeout === null) { const page = location.pathname; // Ensure we dont watch for inventory updates after changing subpage clearInterval(checkForInventoryUpdates); checkForInventoryUpdates = null; // Update the timer bar addZedHelperIconAndTimerBar(); if (page.includes("inventory")) { module = "inventory"; // log("Waiting for inventory list..."); waitForElement("#q-app > div > div.q-page-container > main > div > div:nth-child(4) > div > div.grid-cont.no-padding").then(() => { waitForElement(".item-row").then(() => { log("Inventory list loaded! Adding market prices..."); addMarketPrices(); }); }); waitForElement("#q-app > div > div.q-page-container > main > div").then(() => { showNetworth(); }); } else if (page.includes("market-listings")) { module = "market"; log("Navigated to Market Listings - Watching for element to add new listing..."); waitForElement("div > div > button.q-btn.q-btn-item.bg-positive").then(() => { log("Detected form for adding new market listing... showing market values for inventory!"); addMarketPrices(); }) } else if (page.includes("stronghold/2375014")) { module = "gym"; log("Navigated to Gym"); autoPopulateTrainInput(); } else if (page.includes("stronghold/2375016")) { module = "crafting"; log("Navigated to Crafting Bench"); } else if (page.includes("stronghold/2375017")) { module = "furnace"; log("Navigated to Furnace"); } else if (page.includes("stronghold/2375019")) { module = "radio"; log("Navigated to Radio Tower"); setTimeout(() => { showTradeValues(); },1000); } else if (page.includes("/store/")) { module = "store"; log("Setting up auto input for store - click max btn automatically"); // autoPopulate360Items(); autoPopulateMaxItems(); } else if (page.includes("/zedhelper")) { showSettingsPage(); } else if (/\/scavenge\/\d+$/.test(page)) { module = "scavenge"; log("Navigated to Scavenge"); addBulkScavengeButtons(); } else { module = "unknown"; log(`Unknown subpage: ${page}`); } navigationTimeout = setTimeout(() => { clearTimeout(navigationTimeout); navigationTimeout = null; }, 250); } } try { navigation.addEventListener('navigate', () => { setTimeout(() => { urlChangeHandler(); },100); }); } catch (error) { log("FATAL ERROR: Could not add EventListener for navigation navigate: " + JSON.stringify(error)); } /** Add a second nav menu with some useful shortcuts */ // document.querySelector("#q-app > div > header > div:nth-child(2) > div > div > div").app const secondNavBar = document.createElement('div'); secondNavBar.innerHTML = ` <div> <div class="gt-xs bg-grey-3 text-grey-5 text-h6"> <div class="q-tabs row no-wrap items-center q-tabs--not-scrollable q-tabs--horizontal q-tabs__arrows--inside q-tabs--mobile-with-arrows q-tabs--dense" role="tablist" inside-arrows=""> <div class="q-tabs__content scroll--mobile row no-wrap items-center self-stretch hide-scrollbar relative-position q-tabs__content--align-center"> <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/stronghold/2375017"> <div class="q-focus-helper" tabindex="-1"></div> <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column"> <div class="q-tab__label">Furnace</div> </div> <div class="q-tab__indicator absolute-bottom text-transparent"></div> </a> <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/stronghold/2375014"> <div class="q-focus-helper" tabindex="-1"></div> <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column"> <div class="q-tab__label">Gym</div> </div> <div class="q-tab__indicator absolute-bottom text-transparent"></div> </a> <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/scavenge/2"> <div class="q-focus-helper" tabindex="-1"></div> <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column"> <div class="q-tab__label">Scrapyard</div> </div> <div class="q-tab__indicator absolute-bottom text-transparent"></div> </a> <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/market"> <div class="q-focus-helper" tabindex="-1"></div> <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column"> <div class="q-tab__label">Market</div> </div> <div class="q-tab__indicator absolute-bottom text-transparent"></div> </a> <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/store/junk"> <div class="q-focus-helper" tabindex="-1"></div> <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column"> <div class="q-tab__label">Junk Store</div> </div> <div class="q-tab__indicator absolute-bottom text-transparent"></div> </a> <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/zedhelper"> <div class="q-focus-helper" tabindex="-1"></div> <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column"> <div class="q-tab__label">Settings</div> </div> <div class="q-tab__indicator absolute-bottom text-transparent"></div> </a> </div> </div> </div> `; if(get('extraNavMenu') && (get('extraNavMenu') === 'true' || get('extraNavMenu') === true)) { log("Enabling extra navigation menu"); waitForElement("#q-app > div > header").then(() => { document.querySelector("#q-app > div > header").appendChild(secondNavBar); }); } /** Add icon for ZedHelper settings + timer bar */ function addZedHelperIconAndTimerBar() { const zedHelperIcon = document.createElement('div'); zedHelperIcon.classList = 'zedhelper-icon-bar'; zedHelperIcon.innerHTML = ` <div class="row items-center"> <b><a href="/zedhelper" title="ZedHelper Settings" style="color:dodgerblue;text-decoration:none;font-weight:bold;">ZH</a></b> </div> `; const timerBar = document.createElement('div'); timerBar.classList = "row q-col-gutter-md justify-center items-center zedhelper-timer-bar"; let timeDiff = 0; let timeLeft = 0; let timeLeftFormatted = ""; let html = `<div class="q-tab__label">`; /** GYM */ // const maxEnergy = 150; // const currentEnergy = get('energy') || 0; // Retrieve current energy from storage // const energyRegenRate = 5; // Energy regenerated per interval // const regenIntervalMinutes = 10; // Interval in minutes // const missingEnergy = maxEnergy - currentEnergy; // const timeToFullEnergyMinutes = Math.ceil((missingEnergy / energyRegenRate) * regenIntervalMinutes); // if (timeToFullEnergyMinutes > 0) { // const hours = Math.floor(timeToFullEnergyMinutes / 60); // const minutes = timeToFullEnergyMinutes % 60; // timeLeftFormatted = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; // } // html += `<span class="zedhelper-timer-span">Gym: <a id="zhOpenGym" style="cursor:pointer;">${timeToFullEnergyMinutes > 0 ? `<span class="red">${timeLeftFormatted}</span>` : `<span class="green">Ready</span>`}</a></span>`; // html += `<span class="zedhelper-timer-span"><a id="zhOpenGym" style="cursor:pointer;">⚡</a></span>`; // html += `<span class="zedhelper-timer-span"><a id="zhOpenGym" style="cursor:pointer;"><img src="https://www.zed.city/assets/gym-cOgAonBN.png" style="width:20px;position:relative;top:5px;"></a></span>`; html += `<span class="zedhelper-timer-span"><a id="zhOpenGym" style="cursor:pointer;">💪</a></span>`; /** RADIO TOWER */ // const last = get("radioTower_last_visited") || 0; // const ready = parseInt(last) + (12*60*60*1000); // timeLeft = ready - Date.now(); // const h = Math.floor(timeLeft / 36e5); // const m = Math.floor((timeLeft % 36e5) / 6e4); // const s = Math.floor((timeLeft % 6e4) / 1000); // // return `${h}h ${m}m ${s}s left`; // console.log(`last: ${last} - ready: ${ready} - now: ${Date.now()} - timeLeft: ${timeLeft}`); // // console.log("timeLeft: " + timeLeft); // try { // timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5); // } catch (error) { // timeLeftFormatted = "00:00"; // } // html += `<span class="zedhelper-timer-span">Radio Tower: <a id="zhOpenRadioTower" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">${timeLeftFormatted}</span>` : `<span class="green">Ready</span>`}</a></span>`; // html += `<span class="zedhelper-timer-span"><a id="zhOpenRadioTower" style="cursor:pointer;"><img src="https://www.zed.city/assets/radio_tower-DZgBlHS5.png" style="width:20px;position:relative;top:5px;"></a></span>`; html += `<span class="zedhelper-timer-span"><a id="zhOpenRadioTower" style="cursor:pointer;">📡</a></span>`; /** SCAVENGE */ // const maxRad = 50; const currentRad = get('rad') || 0; // Retrieve current rad from storage // const radRegenRate = 1; // Rad regenerated per interval // const regenIntervalMinutesRad = 5; // Interval in minutes // const missingRad = maxRad - currentRad; // const timeToFullRadMinutes = Math.ceil((missingRad / radRegenRate) * regenIntervalMinutesRad); // let radTimeLeftFormatted = ''; // if (timeToFullRadMinutes > 0) { // const hours = Math.floor(timeToFullRadMinutes / 60); // const minutes = timeToFullRadMinutes % 60; // radTimeLeftFormatted = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; // } // html += `<span class="zedhelper-timer-span">Scavenge: <a id="zhOpenScavenge" style="cursor:pointer;">${timeToFullRadMinutes > 0 ? // `<span class="red">${radTimeLeftFormatted}</span>` // : `<span class="green">Ready</span>` // }</a></span>`; // html += `<span class="zedhelper-timer-span"><a id="zhOpenScavenge" style="cursor:pointer;"><img src="https://www.zed.city/assets/scrapyard-BrTM3-qI.jpg" style="width:20px;position:relative;top:5px;"></a></span>`; // html += `<span class="zedhelper-timer-span"><a id="zhOpenScavenge" style="cursor:pointer;">${currentRad < 10 ? `<span class="red">Scavenge</span>` : `<span class="green">Scavenge</span>`}</a></span>`; html += `<span class="zedhelper-timer-span"><a id="zhOpenScavenge" style="cursor:pointer;">${currentRad < 10 ? `<span class="red">🛢️</span>` : `<span class="green">🛢️</span>`}</a></span>`; /** RAID */ const raidCooldownSecondsLeft = get('raidCooldownSecondsLeft'); const raidCooldownTime = get('raidCooldownTime'); if (raidCooldownSecondsLeft && raidCooldownTime) { timeDiff = (Date.now() - raidCooldownTime)/1000; timeLeft = raidCooldownSecondsLeft - Math.round(timeDiff); try { timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5); } catch (error) { timeLeftFormatted = "00:00"; } } html += `<span class="zedhelper-timer-span" tooltip="${timeLeftFormatted}"><a id="zhOpenRaid" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">🪖 ${timeLeftFormatted}</span>` : `<span class="green">🪖✅</span>`}</a></span>`; /** JUNK STORE */ const junkStoreLimitSecondsLeft = get('junkStoreLimitSecondsLeft'); const junkStoreLimitTime = get('junkStoreLimitTime'); if (junkStoreLimitSecondsLeft && junkStoreLimitTime) { timeDiff = (Date.now() - junkStoreLimitTime)/1000; timeLeft = junkStoreLimitSecondsLeft - Math.round(timeDiff); try { timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5); } catch (error) { timeLeftFormatted = "00:00"; } } html += `<span class="zedhelper-timer-span"><a id="zhOpenJunkStore" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">🏪 ${timeLeftFormatted}</span>` : `<span class="green">🏪✅</span>`}</a></span>`; /** ZED MART */ const zedMartLimitSecondsLeft = get('zedMartLimitSecondsLeft'); const zedMartLimitTime = get('zedMartLimitTime'); if (zedMartLimitSecondsLeft && zedMartLimitTime) { timeDiff = (Date.now() - zedMartLimitTime)/1000; timeLeft = zedMartLimitSecondsLeft - Math.round(timeDiff); try { timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5); } catch (error) { timeLeftFormatted = "00:00"; } } html += `<span class="zedhelper-timer-span"><a id="zhOpenZedMart" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">🏬 ${timeLeftFormatted}</span>` : `<span class="green">🏬✅</span>`}</a></span>`; html += `</div>`; timerBar.innerHTML = html; const selector = ".q-col-gutter-md.justify-center.items-center.currency-stats"; log("Searching for statusbar..."); waitForElement(selector).then((el) => { try { document.querySelectorAll('.zedhelper-icon-bar').entries().forEach((entry) => { log("Remove entry of icon bar: " + entry[1]); entry[1].remove(); }); document.querySelectorAll('.zedhelper-timer-bar').entries().forEach((entry) => { log("Remove entry of timer bar: " + entry[1]); entry[1].remove(); }); } catch (error) { // eat exception } log("Appending ZedHelper icon to statusbar + adding new bar for timers!"); el.appendChild(zedHelperIcon); el.parentElement.appendChild(timerBar); // document.querySelector(selector).parentElement.appendChild(zedHelperIcon); /** Setup event listener to detect clicks on the timer buttons */ setTimeout(() => { document.querySelector('#zhOpenGym').addEventListener('click', (event) => { event.preventDefault(); openGym(); }); document.querySelector('#zhOpenRadioTower').addEventListener('click', (event) => { event.preventDefault(); openRadioTower(); }); document.querySelector('#zhOpenScavenge').addEventListener('click', (event) => { event.preventDefault(); openScavenge(); }); document.querySelector('#zhOpenRaid').addEventListener('click', (event) => { event.preventDefault(); openRaid(); }); document.querySelector('#zhOpenJunkStore').addEventListener('click', (event) => { event.preventDefault(); openJunkStore(); }); document.querySelector('#zhOpenZedMart').addEventListener('click', (event) => { event.preventDefault(); openZedMart(); }); },10); }); } function openGym() { log("Opening Gym..."); // Navigate to Faction const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "stronghold"); link.click(); waitForElement('div.building-cont').then(() => { setTimeout(() => { const gymDiv = [...document.querySelectorAll("div.building-cont")].find(el => el.textContent.trim().includes("Gym")); gymDiv.click(); },250); }); } function openRadioTower() { log("Opening Radio Tower..."); // Navigate to Faction const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "stronghold"); link.click(); waitForElement('div.building-cont').then(() => { setTimeout(() => { const link = [...document.querySelectorAll("div.building-cont")].find(el => el.textContent.trim().includes("Radio Tower")); link.click(); },250); }); } function openScavenge() { log("Opening Scavenge..."); // Navigate to Faction const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "scavenge"); link.click(); waitForElement('.job-cont').then(() => { setTimeout(() => { const link = [...document.querySelectorAll(".job-cont")].find(el => el.textContent.includes("Scrapyard")); link.click(); },250); }); } function openRaid() { log("Opening Raid..."); // Navigate to Faction const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "faction"); link.click(); // Then click on Raid waitForElement('a[href="/raids"]').then((el) => { el.click(); }); } function openJunkStore() { log("Opening Junk Store..."); // Navigate to City const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "city"); link.click(); // Then click on Junk store waitForElement('a[data-cy="citymenu-junk-store"]').then((el) => { el.click(); }); } function openZedMart() { log("Opening Zed Mart..."); // Navigate to City const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "city"); link.click(); // Then click on Zed Mart waitForElement('a[data-cy="citymenu-zed-mart"]').then((el) => { el.click(); }); } // addZedHelperIconAndTimerBar(); /** Settings page */ function showSettingsPage() { const selector = "#q-app > div > div.q-page-container > div"; waitForElement(selector).then((el) => { el.style.top = "40%"; el.style.width = "90%"; el.style.border = "2px inset #333"; el.style.padding = "10px"; el.innerHTML = ` <h3>ZedHelper Settings</h3> <p>Userscript written by <a href="https://www.zed.city/profile/12853">Kvassh</a><br> For any questions or feedback, please reach out to me in Zed City or Discord.</p> <br><br><hr><br> <div style="text-align:left;"> <label>Enable extra nav menu? <input type="checkbox" id="extraNavMenu" name="extraNavMenu" value="true" ${get('extraNavMenu') === true | get('extraNavMenu') === 'true' ? 'checked' : ''}></label> </style> <br><br> <div id="zedhelper-settings-output" style="height:50px; display:block;"> </div> `; setTimeout(() => { document.querySelector("#extraNavMenu").addEventListener('change', (event) => { set('extraNavMenu', event.target.checked); document.querySelector('#zedhelper-settings-output').innerHTML = '<b class="green">Settings saved! ✓<br>You might need to refresh page for some settings like the extra nav menu.</b>'; setTimeout(() => { document.querySelector('#zedhelper-settings-output').innerHTML = " "; },1000); }); }, 100); }); } /** Gym functions */ function autoPopulateTrainInput() { const energy = get("energy"); if (energy > 5) { const trainsAvailable = Math.floor(energy/5); log(`Current energy: ${energy} - Autopopulating ${trainsAvailable} into the input fields`); waitForElement("input.q-field__native").then(() => { const inputs = document.querySelectorAll("input.q-field__native"); for (let input of inputs) { input.value = trainsAvailable; input.dispatchEvent(new Event("input", { bubbles: true })); } }); } else { log("Current energy is 5 or lower, don't autopopulate input fields"); } } /** Scavenge functions */ function addBulkScavengeButtons() { const selector = "#q-app > div > div.q-page-container > main > div > div > div.full-width > div > div.q-mt-lg"; waitForElement(selector).then(() => { // Add buttons for 1, 5, 10, 25, 50, 100 scavenges const container = document.querySelector(selector); const doScavengeBtn = document.querySelector("#q-app > div > div.q-page-container > main > div > div > div.full-width > div > div.q-mt-lg > button:nth-child(1)"); const btn5 = document.createElement('button'); btn5.innerHTML = `<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><span class="block">x5</span></span>`; btn5.classList = "q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-positive text-white q-btn--actionable q-focusable q-hoverable"; btn5.style.margin = "5px"; btn5.addEventListener('click', () => { console.log("Clicking 5 times..."); for (let i = 0; i < 5; i++) { doScavengeBtn.click(); } }); container.append(btn5); const btn30 = document.createElement('button'); btn30.innerHTML = `<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><span class="block">x30</span></span>`; btn30.classList = "q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-positive text-white q-btn--actionable q-focusable q-hoverable"; btn30.style.margin = "5px"; btn30.addEventListener('click', () => { console.log("Clicking 30 times..."); for (let i = 0; i < 30; i++) { doScavengeBtn.click(); } }); container.append(btn30); const currentRad = get('rad') || 0; const btnMax = document.createElement('button'); btnMax.innerHTML = `<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><span class="block">Rad (x${currentRad})</span></span>`; btnMax.classList = "q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-positive text-white q-btn--actionable q-focusable q-hoverable"; btnMax.style.margin = "5px"; btnMax.addEventListener('click', () => { console.log(`Current rad: ${currentRad} - Clicking ${currentRad} times...`); for (let i = 0; i < currentRad; i++) { doScavengeBtn.click(); } }); container.append(btnMax); }); } /** Radio Tower functions */ function showTradeValues() { try { const timeDiff = get("radio_lastupdate") ? (Date.now() - get("radio_lastupdate"))/1000 : 0; log(`Trade values last updated: ${timeDiff} sec ago`); if (timeDiff > 60*60*12) { log("Trade values are old. Please visit the Radio Tower to cache new values."); const el = document.createElement('div'); el.classList.add('radio-warning'); waitForElement("div.overlay-cont").then(() => { const container = document.querySelector("div.overlay-cont"); // document.querySelector("#q-app > div > div.q-page-container > main > div > div:nth-child(11) > div.overlay-cont > div > div > div > div > div.text-center.text-no-bg-light.subtext-large.q-my-md") el.innerHTML = `Radio trades data are old - please refresh <a href="stronghold/2375019">Radio Tower</a> to cache new values.`; container.prepend(el); }); return; } const trades = JSON.parse(get(`tradeValues`)); // [{"give":96,"return":460},{"give":1425,"return":11900},{"give":3000,"return":2380}] log("Current trades to show:"); log(trades); waitForElement(".q-pa-md").then(() => { const tradeContainers = document.querySelectorAll(".q-pa-md"); let i = 0; for (let tradeContainer of tradeContainers) { const valueEl = document.createElement('div'); valueEl.classList.add('trade-value'); valueEl.innerHTML = ` <div style="float:left;"> ${trades[i].giveqty} items worth<br><span class="red">$</span> ${formatNumber(trades[i].give)} </div> <div style="float:right;"> ${trades[i].returnqty} items worth<br><span class="green">$</span> ${formatNumber(trades[i].return)} </div> <div style="clear:both;font-size:1.2rem;"> ${parseInt(trades[i].return) > parseInt(trades[i].give) ? '<span class="green">✓</span>' : '<span class="red">✗</span>'} </div> `; tradeContainer.appendChild(valueEl); i++; } // Update radiotower last purchased date set(`radioTower_last_visited`, Date.now()); }); } catch(error) { log("No trade values found"); } } function saveCurrentTradeValues(data) { try { const trades = []; for (let trade of data.items) { // trade -> vars -> items -> <item_requirement_1> -> codename/req_qty // trade -> vars -> output -> <item_list-1> -> codename/quantity let worthGive = 0; let worthReturn = 0; let qtyGive = 0; let qtyReturn = 0; const items = trade.vars.items; Object.keys(items).forEach( (key,val) => { const marketValue = JSON.parse(get(`mv_${items[key].codename}`)).marketValue; worthGive += (marketValue*items[key].req_qty); qtyGive += items[key].req_qty; }); const output = trade.vars.output; Object.keys(output).forEach( (key,val) => { const marketValue = JSON.parse(get(`mv_${output[key].codename}`)).marketValue; worthReturn += (marketValue*output[key].quantity); qtyReturn += output[key].quantity; }); log(`Trade: ${trade.name} - Give: ${worthGive} - Return: ${worthReturn}`); trades.push({ "give": worthGive, "return": worthReturn, "giveqty": qtyGive, "returnqty": qtyReturn }); } set(`tradeValues`, JSON.stringify(trades)); } catch(error) { log("Error saving trade values"); set(`tradeValues`, null); } } /** Store functions */ function autoPopulate360Items() { const selector = "input[type=number].q-placeholder"; waitForElement(selector).then(() => { const el = document.querySelector(selector); el.value = 360; el.dispatchEvent(new Event("input", { bubbles: true })); }); } function autoPopulateMaxItems() { const selector = "input[type=number].q-placeholder"; waitForElement(selector).then(() => { const maxButton = [...document.querySelectorAll("button")].find(btn => btn.textContent.toLowerCase().includes("max")); maxButton.click(); // const el = document.querySelector(selector); // el.value = 360; // el.dispatchEvent(new Event("input", { bubbles: true })); }); } /** Functions related to market/inventory */ // Function to process inventory items and add prices async function addMarketPrices() { const items = document.querySelectorAll('.item-row'); if (!items) { log("No inventory items found. Check your selectors."); return; } const mvLastUpdateEl = document.createElement('div'); mvLastUpdateEl.classList.add('zedhelper-inventory-warning'); const mvLastUpdated = get('mv_lastupdate'); if (mvLastUpdated) { const timeDiff = (Date.now() - mvLastUpdated)/1000; log(`Market values last updated: ${timeDiff} sec ago`); if (timeDiff > 60*60*24) { log("Market values are older than 24 hours. Please visit the market page to cache new values."); mvLastUpdateEl.innerHTML = `Market values are older than a day - please visit the <a href="market">Market</a> page to cache new values.`; } } else { log("Market values not cached. Please visit the market page to cache values."); mvLastUpdateEl.innerHTML = ` Market value has not been cached yet.<br> Please visit the <a href="market">Market</a> page first to calculate worth on your inventory. `; } const selector = "#q-app > div > div.q-page-container > main > div > div:nth-child(2)"; waitForElement(selector).then(() => { document.querySelector(selector).prepend(mvLastUpdateEl); }); // Delete any existing market value elements const existingMarketValues = document.querySelectorAll('.market-price'); for (let mvEl of existingMarketValues) { mvEl.remove(); } for (let item of items) { const codename = getCodename(item.querySelector('.q-item__label').innerText); let qty = 1; try { qty = item.querySelector('.item-qty').innerText; if (qty.includes("%")) { qty = 1; } else { qty = parseInt(qty.replace(/[^0-9]/g, '')); } } catch (error) { // eat exception } if (Number.isNaN(qty)) { qty = 1; } log(`Adding market value for ${codename} x ${qty}`); let data = null; if(get(`mv_${codename}`)) { data = JSON.parse(get(`mv_${codename}`)); } const priceElement = document.createElement('span'); priceElement.classList.add('market-price'); if (data !== null) { const datetime = new Date(data.tz).toISOString(); priceElement.innerHTML = `<span title="${datetime}"> <b class="green">$</b> ${formatNumber(data.marketValue * qty)} <small>(<b class="green">$</b> ${formatNumber(data.marketValue)})</small> </span>`; } else { priceElement.innerHTML = `<span class="gray">N/A</span>`; } item.querySelector('.q-item__label').appendChild(priceElement); } // Setup interval to check if inventory list changes let firstItemRowCodename = ""; try { firstItemRowCodename = getCodename(items[0].querySelector('.q-item__label').innerText); } catch (error) { // eat exception } checkForInventoryUpdates = setInterval(() => { let newItems = document.querySelectorAll('.item-row'); if (newItems.length !== items.length) { log("Inventory list has changed. Updating prices..."); clearInterval(checkForInventoryUpdates); checkForInventoryUpdates = null; addMarketPrices(); return; } let newFirstItemRowCodename = ""; try { newFirstItemRowCodename = getCodename(newItems[0].querySelector('.q-item__label').innerText); } catch (error) { // eat exception } if (firstItemRowCodename != newFirstItemRowCodename) { log("Inventory list has changed. Updating prices..."); clearInterval(checkForInventoryUpdates); checkForInventoryUpdates = null; addMarketPrices(); return; } },250); } function showNetworth() { const networthVendor = get(`mv_networth_vendor`) || 0; const networthMarket = get(`mv_networth_market`) || 0; const networthCash = get(`money`) || 0; const networth = parseInt(networthMarket) + parseInt(networthCash); const existingElement = document.querySelector('.zedhelper-networth'); if (existingElement) { existingElement.remove8(); } const el = document.createElement('div'); el.classList.add('zedhelper-networth'); el.innerHTML = ` Networth: <span title="Value of items only if sold to vendor: $ ${formatNumber(networthVendor)}"> <b class="green">$</b> ${formatNumber(networth)} </span> `; const selector = "#q-app > div > div.q-page-container > main > div > div:nth-child(2)"; waitForElement(selector).then(() => { document.querySelector(selector).prepend(el); }); } })(); /* EXAMPLE RESPONSE /getOffers: [ { "name":"Advanced Tools", "codename":"advanced_tools", "type":"resources_craft_basic", "quantity":2, "value":10, "vars":{ "buy":10,"sell":5,"desc":"","weight":"1","ash_value":"20" }, "market_id":15490, "market_price":19500, "quantity_sold":3, "user":{ "id":11703,"username":"bump","avatar":"","online":1739285402 } }, ] */ /* EXAMPLE RESPONSE /getMarket { "items": [ { "name":"Advanced Tools", "codename":"advanced_tools", "type":"resources_craft_basic", "quantity":35, "value":10, "vars": { "buy":10, "sell":5, "desc":"", "weight":"1", "ash_value":"20" }, "market_id":14020, "market_price":19500, "quantity_sold":0 }, ] } */