// ==UserScript==
// @name GazelleGames pet leveling info
// @namespace v3rrrr82xk1c96vvo1c6
// @match https://gazellegames.net/user.php?id=*
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @version 1.3.4
// @description Adds pet leveling info to your own profile page
// @author lunboks
// @run-at document-start
// @inject-into content
// @license MIT
// ==/UserScript==
(async function () {
"use strict";
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 toInt(value) {
return (typeof value === "number") ? value : parseInt(value, 10);
}
// Hardcoded IDs of XP-gaining pets.
// We display these even if they have 0 XP, which we otherwise
// cannot distinguish from non-leveling pets.
const levelingPetIDs = new Set(["2509","2510","2511","2512","2513","2514","2515","2521","2522","2523","2524","2525","2529","2583","2927","2928","2929","2933","3215","3216","3237","3322","3323","3324","3369","3370","3371","3373"]);
const pets = [];
for (const equip of equipment.response) {
const type = equip.item.equipType;
if (type && String(type) === "18" && (levelingPetIDs.has(equip.itemid) || equip.experience > 0)) {
pets.push({
name: equip.item.name,
xp: toInt(equip.experience),
lv: toInt(equip.level),
id: String(equip.itemid),
slot: toInt(equip.slotid)
});
}
}
if (!pets.length) return;
// Sort by slot ID for consistent ordering.
pets.sort((first, second) => first.slot - second.slot);
// Build our HTML structure.
// CSS classes copied from the "Personal" box
const box = document.createElement("div");
const innerBox = document.createElement("div");
const list = document.createElement("ul");
const heading = document.createElement("div");
box.className = "box_personal_history";
innerBox.className = "box";
heading.className = "head colhead_dark";
list.className = "stats nobullet";
list.style.lineHeight = "1.5";
heading.append("Pet Leveling");
innerBox.append(heading, list);
box.append(innerBox);
function totalXP(lv) {
return Math.ceil((lv * lv * 625) / 9);
}
function xpToTimeString(xp) {
const days = Math.floor(xp / 24);
const hours = xp % 24;
let timeString = "";
if (days) {
const s = (days === 1) ? "" : "s";
timeString = `${days} day${s}`;
}
if (hours) {
if (timeString) {
timeString += " ";
}
const s = (hours === 1) ? "" : "s";
timeString += `${hours} hour${s}`;
} else if (!timeString) {
timeString = "0 hours"; // ???
}
return timeString;
}
const listItems = [];
pets.forEach((pet, index) => {
const liItem = document.createElement("li");
const liLevelInput = document.createElement("li");
const liTimeOutput = document.createElement("li");
const shopLink = document.createElement("a");
// Spacing between pets
if (index > 0) {
liItem.style.marginTop = "0.6em";
}
// Indent the level/time lines
liLevelInput.style.paddingLeft = "10px";
liTimeOutput.style.paddingLeft = "10px";
shopLink.style.fontWeight = "bold";
shopLink.href = `/shop.php?ItemID=${pet.id}`;
shopLink.referrerPolicy = "no-referrer";
shopLink.title = "Shop for this pet";
const nextLevel = pet.lv + 1;
const targetLevelInput = document.createElement("input");
targetLevelInput.type = "number";
targetLevelInput.required = true;
targetLevelInput.inputmode = "numeric";
targetLevelInput.style.width = "3em";
targetLevelInput.min = nextLevel;
targetLevelInput.max = Math.max(999, nextLevel);
targetLevelInput.value = nextLevel;
const displayTimeDifference = (toLevel) => {
const missingXP = totalXP(toLevel) - pet.xp;
liTimeOutput.textContent = xpToTimeString(missingXP);
};
// Display initial info
displayTimeDifference(nextLevel);
// When the user types anything, update if it's valid.
targetLevelInput.addEventListener("input", function () {
if (this.checkValidity()) {
displayTimeDifference(parseInt(this.value, 10));
}
});
// When the user commits a new value, complain if it's invalid.
// No idea why setTimeout is needed tbh
targetLevelInput.addEventListener("change", function () {
setTimeout(() => {
if (!this.reportValidity()) {
liTimeOutput.textContent = "";
}
});
});
shopLink.append(pet.name);
liItem.append(shopLink);
liLevelInput.append(`Level ${pet.lv} → `, targetLevelInput);
listItems.push(liItem, liLevelInput, liTimeOutput);
});
list.append(...listItems);
// Try to insert our UI
function insert() {
document.getElementsByName("user_info")[0]?.after(box);
return box.isConnected;
}
if (!insert()) {
window.addEventListener("DOMContentLoaded", insert);
}
})();