// ==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) {
const uniqueID = equip.equipid;
const existingItem = breakableEquipment.get(uniqueID);
if (existingItem) {
// Multi-slot item, add slot to existing entry
existingItem.slot += `,${equip.slotid}`;
} else {
let breakTime = equip.breakTime;
if (breakTime && breakTime !== "NULL") {
// 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);
}
})();