GazelleGames equipment durability info

Adds equipment durability info to your own profile page

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);
	}
})();