Greasy Fork is available in English.

GazelleGames equipment durability info

Adds equipment durability info to your own profile page

// ==UserScript==
// @name        GazelleGames equipment durability info
// @namespace   8n9ve1ockfqbr82qlwc12i
// @match       https://gazellegames.net/user.php?id=*
// @grant       GM.getValue
// @grant       GM.setValue
// @grant       GM.deleteValue
// @version     1.0.1
// @description Adds equipment durability info to your own profile page
// @author      lunboks
// @run-at      document-start
// @inject-into content
// @license     MIT
// ==/UserScript==

(async function () {
	"use strict";

	// Mark items with less than x hours remaining
	const HOURS_WARNING = 24;

	// Log API response
	const DEBUG = false;


	const theirUserID = new URLSearchParams(location.search).get("id");
	const ownUserID = await GM.getValue("you").then((yourID) => {
		// Own ID is cached
		if (yourID) {
			return yourID;
		}

		// Not cached, get it from page once it's loaded
		return new Promise((resolve) => {
			window.addEventListener("DOMContentLoaded", () => {
				yourID = new URLSearchParams(document.body.querySelector("#nav_userinfo a.username").search).get("id");
				GM.setValue("you", yourID);
				resolve(yourID);
			});
		});
	});


	// Only runs on your own user page
	if (theirUserID !== ownUserID) {
		return;
	}


	let apiKey = await GM.getValue("apiKey");

	if (!apiKey) {
		if (!(apiKey = prompt("Please enter an API key with the 'Items' permission to use this script.")?.trim())) {
			return;
		}
		GM.setValue("apiKey", apiKey);
	}


	const endpoint = "https://gazellegames.net/api.php?request=items&type=users_equipped&include_info=true";
	const options = {
		method: "GET",
		mode: "same-origin",
		credentials: "omit",
		redirect: "error",
		referrerPolicy: "no-referrer",
		headers: {
			"X-API-Key": apiKey
		}
	};


	const equipment = await (await fetch(endpoint, options)).json();
	
	if (equipment.status !== "success") {
		if (equipment.status === 401) {
			GM.deleteValue("apiKey");
		}
		return;
	}


	function plural(n) {
		return (n === 1) ? "" : "s";
	}


	const tsUnits = new Map();
	tsUnits.set("day", 86_400_000)
		.set("hour",    3_600_000)
		.set("minute",     60_000)
		.set("second",      1_000)
		.set("millisecond",     1);


	// Converts milliseconds to something like "4 days 3 hours"
	function millisToTimeString(ms) {
		let unit;

		// Find highest applicable unit
		for (const [key, value] of tsUnits) {
			if (ms >= value) {
				unit = key;
				break;
			}
		}

		const amount = Math.floor(ms / tsUnits.get(unit));
		let timeString = `${amount} ${unit}${plural(amount)}`;

		// Special case - add hours after days
		if (unit === "day") {
			const hours = Math.floor(ms / tsUnits.get("hour")) % 24;
			if (hours) {
				timeString += ` ${hours} hour${plural(hours)}`;
			}
		}

		return timeString;
	}


	if (DEBUG) {
		console.log("Equipment API response", JSON.stringify(equipment));
	}


	const slotNames = {
		"1":   "Helmet",
		"2":   "Upper Body",
		"3":   "Arms",
		"4":   "Legs",
		"5":   "Hands",
		"6":   "Foot",
		"7":   "Left-Hand Weapon",
		"8":   "Right-Hand Weapon",
		// special cases for 2-handed weapons
		"7,8": "Two-Handed Weapon",
		"8,7": "Two-Handed Weapon",
		"9":   "Necklace",
		"10":  "Left-Hand Ring",
		"11":  "Right-Hand Ring",
		"12":  "Back",
		"13":  "Clothes",
		"14":  "Right-Side Pet",
		"15":  "Left-Side Pet"
	};

	// Map equipment by unique ID because the same item can
	// show up multiple times if it occupies multiple slots.
	let breakableEquipment = new Map();
	const now = Date.now();

	for (const equip of equipment.response) {
		let breakTime = equip.breakTime;
		if (breakTime && breakTime !== "NULL") {
			const uniqueID = equip.equipid;
			const existingItem = breakableEquipment.get(uniqueID);

			if (existingItem) {
				// Multi-slot item, add slot to existing entry
				existingItem.slot += `,${equip.slotid}`;
			} else {
				// The API doesn't return a proper date with a time zone so try to force UTC idk
				breakTime = Date.parse(`${breakTime}Z`) || Date.parse(breakTime);

				if (breakTime) {
					breakableEquipment.set(uniqueID, {
						name: equip.item.name,
						id: String(equip.itemid),
						breakTime: new Date(breakTime).toLocaleString(),
						timeLeft: breakTime - now,
						slot: String(equip.slotid)
					});
				}
			}
		}
	}

	if (breakableEquipment.size < 1) return;

	// Convert to array and sort by break time ascending
	breakableEquipment = Array.from(breakableEquipment.values());
	breakableEquipment.sort((first, second) => first.timeLeft - second.timeLeft);


	const box = document.createElement("div");
	box.className = "box";

	const heading = document.createElement("div");
	heading.className = "head colhead_dark";

	const headerLink = document.createElement("a");
	headerLink.href = "/user.php?action=equipment";
	headerLink.referrerPolicy = "no-referrer";
	headerLink.append("Equipment Durability");
	headerLink.title = "Your active equipment that's breakable";

	const list = document.createElement("ul");
	list.className = "stats nobullet";

	heading.append(headerLink);
	box.append(heading, list);


	const cutoffWarn = HOURS_WARNING * tsUnits.get("hour");
	const warningMsg = `Item durability less than ${HOURS_WARNING} hour${plural(HOURS_WARNING)}`;

	const listItems = [];

	breakableEquipment.forEach((equip, index) => {
		const itemName = document.createElement("li");
		const itemStatus = document.createElement("li");

		itemStatus.style.paddingLeft = "10px";

		// Don't add margin spacing to the first item
		if (index > 0) {
			itemName.style.marginTop = "0.6em";
		}

		const itemLink = document.createElement("a");
		itemLink.style.fontWeight = "bold";
		itemLink.href = `/shop.php?ItemID=${equip.id}`;
		itemLink.referrerPolicy = "no-referrer";
		itemLink.title = "Shop for this item";
		itemLink.append(equip.name);

		itemName.append(itemLink);

		listItems.push(itemName);

		const slotName = slotNames[equip.slot];
		if (slotName) {
			const slotDesc = document.createElement("li");
			slotDesc.title = `Slot ID ${equip.slot}`;
			slotDesc.style.fontSize = "smaller";
			slotDesc.append(slotName);
			listItems.push(slotDesc);
		}


		const timeSpan = document.createElement("span");
		timeSpan.title = equip.breakTime;

		if (equip.timeLeft > 0) {
			timeSpan.append(millisToTimeString(equip.timeLeft));
			itemStatus.append(timeSpan, " left");
		} else {
			timeSpan.append("already broken!");
			itemStatus.append(timeSpan);
		}

		if (equip.timeLeft < cutoffWarn) {
			const warningSpan = document.createElement("span");
			warningSpan.title = warningMsg;
			warningSpan.append("⚠️");
			itemStatus.append(" ", warningSpan);
		}

		listItems.push(itemStatus);
	});

	list.append(...listItems);


	function insert() {
		document.getElementsByName("user_info")[0]?.after(box);
		return box.isConnected;
	}

	if (!insert()) {
		window.addEventListener("DOMContentLoaded", insert);
	}
})();