// ==UserScript==
// @name ZedHelper
// @description Misc helper tools for Zed City
// @version 0.4.14
// @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.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);
}
}
});
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");
showTradeValues();
}
else if (page.includes("/store/junk")) {
module = "store";
log("Setting up auto input for junk store - 360 items");
autoPopulate360Items();
}
else if (page.includes("/zedhelper")) {
showSettingsPage();
}
else {
module = "unknown";
log(`Unknown subpage: ${page}`);
}
navigationTimeout = setTimeout(() => {
clearTimeout(navigationTimeout);
navigationTimeout = null;
}, 250);
}
}
// try {
// window.addEventListener('urlchange', async (event) => {
// urlChangeHandler();
// });
// } catch (error) {
// log("FATAL ERROR: Could not add EventListener for window urlchange: " + JSON.stringify(error));
// }
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">`;
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">Raid: <a href="/raids">${timeLeft > 0 ? `<span class="red">${timeLeftFormatted}</span>` : `<span class="green">Ready</span>`}</a></span>`;
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">Junk Store: <a href="/store/junk">${timeLeft > 0 ? `<span class="red">${timeLeftFormatted}</span>` : `<span class="green">Ready</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);
});
}
// 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");
}
}
/** 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++;
}
});
} 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 }));
});
}
/** Functions related to market/inventory */
// async function fetchMarketPrice(itemCodename) {
// try {
// const response = await fetch(`${baseApiUrl}/getOffers?item=${itemCodename}`, {
// "headers": {
// "accept": "application/json, text/plain, */*",
// "accept-language": "en-US,en;q=0.9,no;q=0.8",
// "content-type": "application/json",
// "cookie": document.cookie, // Use current cookies from the browser
// "Referer": "https://www.zed.city/"
// },
// "body": "{\"page\":1,\"descending\":false}",
// "method": "POST"
// });
// if (!response.ok) {
// log(`Error fetching price for ${itemCodename}: ${response.status}`);
// return null;
// }
// const data = await response.json();
// if (data && data.length > 0) { // Check if there are any offers
// return data[0].market_price; // Return the first offer's price
// } else {
// return null; // No offers found
// }
// } catch (error) {
// log(`Error fetching price for ${itemCodename}: ${error}`);
// return null;
// }
// }
// 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
},
]
}
*/