Live inventory monitoring, meal timers and more!
// ==UserScript==
// @name FRPG HUD
// @namespace AppleBottomJeans.FRPG.HUD
// @version 2026-05-05
// @description Live inventory monitoring, meal timers and more!
// @author AppleBottomJeans
// @match https://farmrpg.com/index.php
// @match https://farmrpg.com/
// @icon https://www.google.com/s2/favicons?sz=64&domain=farmrpg.com
// @license GNU GPLv3
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addValueChangeListener
// @grant GM_addStyle
// @grant GM_setClipboard
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
(function() {
"use strict";
var _a;
const STORAGE_KEYS = {
INVENTORY: "frpg.inventory",
INVENTORY_LIMIT: "frpg.inventory-limit",
RECIPES: "frpg.recipes",
RETURN_RATE: "frpg.return-rate",
LOCATION_PREFIX: "frpg.location",
QUICK_ACTIONS: "frpg.quick-actions",
TOWNSFOLK: "frpg.townsfolk",
TOWNSFOLK_GIFTS: "frpg.townsfolk-gifts",
HUD_STATUS: "frpg.hud-status",
HUD_ITEMS: "frpg.hud-items",
HUD_URL: "frpg.hud-url",
HUD_STASH: "frpg.hud-stash",
HUD_TIMERS: "frpg.hud-timers",
HUD_BUTTON: "frpg.hud-button",
SUPPLY_PACKS: "frpg.supply-packs",
NEW_ITEM: "frpg.new-item",
CRAFTWORKS: "frpg.craftworks",
SETTINGS: "frpg.settings",
PRODUCTION: "frpg.production",
PRODUCTION_LAST_UPDATE: "frpg.production-last-update",
PRODUCTION_LOCK: "frpg.production-lock",
QUESTS: "frpg.quests"
};
const HUD_DISPLAY_MODES = {
INVENTORY: "INVENTORY",
MEAL: "MEAL",
TIMER: "TIMER",
SUPPLY_PACK: "SUPPLY_PACK"
};
const seedCrop = {
"14": "13",
// Eggplant
"16": "15",
// Tomato
"20": "19",
// Carrot
"30": "29",
// Cucumber
"32": "31",
// Radish
"34": "33",
// Onion
"47": "46",
// Hops
"49": "48",
// Potato
"51": "50",
// Leek
"60": "59",
// Watermelon
"64": "65",
// Corn
"66": "67",
// Cabbage
"68": "69",
// Pumpkin
"70": "71",
// Wheat
"160": "159",
// Gold Carrot
"190": "189",
// Gold Cucumber
"255": "254",
// Cotton
"257": "256",
// Broccoli
"352": "262",
// Gold Eggplant
"374": "373",
// Sunflower
"449": "450",
// Beet
"631": "630",
// Rice
"158": "157",
// Gold Pepper
"162": "161",
// Gold Pea
"588": "450",
// Mega Beet
"741": "254",
// Mega Cotton
"589": "373",
// Mega Sunflower
"395": "43",
// Mushroom
"28": "27",
// Pea
"12": "11",
// Pepper
"410": "409",
// Pine
"1307": "1276"
// Sugar Cane
};
const staminaItems = ["Apple", "Orange Juice"];
const mealTimeExceptions = {
"Breakfast Boost": 2 * 60,
"Cabbage Stew": 2 * 60,
"Lemon Cream Pie": 2 * 60,
"Crunchy Omelette": 2 * 60,
"Hickory Omelette": 60 * 60
};
const loadouts = {
"Hourly": {
items: ["Stone", "Coal", "Wood", "Board", "Sandstone", "Straw", "Steel", "Steel Wire"]
},
"Reset": {
items: ["Apple", "Grapes", "Lemon", "Orange", "Antler", "Milk", "Eggs", "Feathers", "Black Truffle", "White Truffle", "Steak", "Steak Kabob"]
},
"Meals": {
items: ["Mushroom Stew", "Shrimp-a-Plenty", "Quandary Chowder", "Neigh", "Lemon Cream Pie", "Cabbage Stew", "Cat's Meow", "Sea Pincher Special", "Hickory Omelette", "Breakfast Boost", "Over The Moon", "Onion Soup"],
displayMode: HUD_DISPLAY_MODES.MEAL
},
"Cookies": {
items: ["Mushroom Stew", "Breakfast Boost", "Happy Cookies", "Spooky Cookies", "Lovely Cookies"],
displayMode: HUD_DISPLAY_MODES.MEAL
}
};
const mealNames = /* @__PURE__ */ new Set([...loadouts.Meals.items, ...loadouts.Cookies.items]);
const wheelItems = ["Apple", "Apple Cider", "Orange Juice", "Lemonade", "Fishing Net", "Rope", "Mushroom Paste", "Yarn"];
const unsellableItems = ["White Truffle", "Black Truffle"];
const darkModeActive = (((_a = document.querySelector("#dark_mode")) == null ? void 0 : _a.innerText.trim()) ?? "1") === "1";
const defaultSettings = {
mealTimersEnabled: {
label: "Display meal timers",
default: true
},
processCraftworks: {
label: "Run craftworks simulation",
default: true
},
hudStashEnabled: {
label: "Enable HUD stash (Restore Button)",
default: true
},
craftworksNotifications: {
label: "Notify on full craftworks inventory",
default: true
}
};
const farmProductionKeys = {
"Worms": "Worms",
"Gummy Worms": "Gummies",
"Mealworms": "Mealworms",
"Grubs": "Grubs",
"Minnows": "Minnows",
"Wood": "Wood",
"Board": "Boards",
"Oak": "Oak",
"Steel": "Steel",
"Steel Wire": "Wire",
"Straw": "Straw",
"Stone": "Stone",
"Sandstone": "Stone",
"Coal": "Coal Hourly"
};
const tenMinuteProductionItems = ["Straw", "Stone", "Sandstone"];
const hourlyProductionItems = ["Wood", "Board", "Coal", "Steel", "Steel Wire", "Oak", "Worms", "Gummy Worms", "Mealworms", "Grubs", "Minnows"];
const defaultTownsfolkGifts = {
"Baba Gec": {
"loves": ["Cabbage Stew", "Peach Juice", "Wooden Button", "Cabbage", "Frozen Cabbage"],
"likes": ["Leek", "Onion", "Rope", "Snail"]
},
"Beatrix": {
"loves": ["Black Powder", "Explosive", "Fireworks", "Iced Tea", "Bottle Rocket"],
"likes": ["Bird Egg", "Carbon Sphere", "Coal", "Hammer", "Hops", "Oak"]
},
"Borgen": {
"loves": ["Cheese", "Gold Catfish", "Wooden Box", "Ancient Coin", "Skull Coin"],
"likes": ["Glass Orb", "Gold Carrot", "Gold Cucumber", "Gold Peas", "Milk", "Slimestone"]
},
"Buddy": {
"loves": ["Pirate Bandana", "Pirate Flag", "Purple Flower", "Valentines Card", "Buddystone"],
"likes": ["Bone", "Bucket", "Giant Centipede", "Gold Peppers", "Gummy Worms", "Mushroom", "Snail", "Spider"]
},
"Captain Thomas": {
"loves": ["Fishing Net", "Gold Catfish", "Gold Drum", "Gold Trout", "Large Net"],
"likes": ["Blue Crab", "Minnows"]
},
"Cecil": {
"loves": ["Grasshopper", "Horned Beetle", "Leather", "MIAB", "Old Boot", "Shiny Beetle", "Yarn"],
"likes": ["Aquamarine", "Giant Centipede", "Grapes", "Ladder", "Slimestone", "Snail"]
},
"Charles": {
"loves": ["Apple", "Apple Cider", "Box of Chocolate 01", "Gold Carrot", "Peach", "Valentines Card", "Horseshoe", "4-leaf Clover", "Sugar Cube"],
"likes": ["3-leaf Clover", "Carrot", "Grasshopper", "Twine"]
},
"Cid": {
"loves": ["Bomb", "Diamonds", "Explosive", "Mushroom Stew", "Safety Goggles", "Spider"],
"likes": ["Black Powder", "Blue Feathers", "Shimmer Stone", "Stone"]
},
"frank": {
"loves": ["Carrot", "Gold Carrot", "Cabbage"],
"likes": ["Blue Dye", "Blue Feathers", "Bucket", "Caterpillar", "Feathers", "Grasshopper"]
},
"Gary Bearson V": {
"loves": ["Apple Cider", "Gold Trout", "Yarn", "You Rock Card"],
"likes": ["Feathers", "Oak", "Trout"]
},
"Geist": {
"loves": ["Gold Catfish", "Goldgill", "Sea Pincher Special", "Shrimp-a-Plenty"],
"likes": ["Blue Crab", "Green Chromis", "Stingray", "Yellow Perch"]
},
"George": {
"loves": ["Apple Cider", "Carbon Sphere", "Hide", "Mug of Beer", "Spider"],
"likes": ["Arrowhead", "Bird Egg", "Glass Orb", "Hops", "Mushroom Stew", "Orange Juice"]
},
"Holger": {
"loves": ["Gold Trout", "Mug of Beer", "Potato", "Wooden Table", "Marlin"],
"likes": ["Apple Cider", "Arrowhead", "Bluegill", "Carp", "Cheese", "Horn", "Largemouth Bass", "Mushroom Stew", "Peach", "Peas", "Trout"]
},
"Jill": {
"loves": ["Leather", "MIAB", "Mushroom Paste", "Peach", "Yellow Perch", "Corn", "Corn Husk Doll"],
"likes": ["Cheese", "Grapes", "Milk", "Old Boot", "Scrap Metal", "Tomato"]
},
"Lorn": {
"loves": ["Glass Orb", "Gold Peas", "Milk", "Shrimp", "Small Prawn"],
"likes": ["3-leaf Clover", "Apple Cider", "Bucket", "Green Parchment", "Iced Tea", "Iron Cup", "Peas", "Purple Parchment"]
},
"Mariya": {
"loves": ["Cat's Meow", "Leather Diary", "Mushroom Stew", "Onion Soup", "Over The Moon", "Quandary Chowder", "Sea Pincher Special", "Shrimp-a-Plenty"],
"likes": ["Cucumber", "Eggplant", "Eggs", "Iced Tea", "Milk", "Peach", "Radish"]
},
"Mummy": {
"loves": ["Bone", "Spider", "Valentines Card", "Pumpkin Spiced Milk", "Toilet Paper"],
"likes": ["Fish Bones", "Hammer", "Treat Bag 02", "Yarn"]
},
"Ric Ryph": {
"loves": ["5 Gold", "Hammer", "Mushroom Paste", "Shovel"],
"likes": ["Arrowhead", "Black Powder", "Bucket", "Carbon Sphere", "Coal", "Green Parchment", "Old Boot", "Unpolished Shimmer Stone"]
},
"ROOMBA": {
"loves": ["Carbon Sphere", "Scrap Metal"],
"likes": ["Glass Orb", "Hammer", "Scrap Wire"]
},
"Rosalie": {
"loves": ["Blue Dye", "Box of Chocolate 01", "Gold Carrot", "Green Dye", "Purple Dye", "Red Dye", "Valentines Card", "Garnet Ring", "Ruby Ring"],
"likes": ["Apple", "Apple Cider", "Aquamarine", "Carrot", "Caterpillar", "Fireworks", "Iced Tea", "Purple Flower"]
},
"Star Meerif": {
"loves": ["Blue Feathers", "Gold Feather", "Linked Lantern"],
"likes": ["Eggs", "Feathers"]
},
"Thomas": {
"loves": ["Fishing Net", "Flier", "Gold Catfish", "Gold Trout", "Goldgill", "Chum"],
"likes": ["Carp", "Drum", "Gummy Worms", "Iced Tea", "Largemouth Bass", "Mealworms", "Minnows"]
},
"Vincent": {
"loves": ["5 Gold", "Apple Cider", "Axe", "Lemonade", "Mushroom Paste", "Onion Soup", "Orange Juice"],
"likes": ["Acorn", "Apple", "Cheese", "Hops", "Horn", "Leather Diary", "Shovel", "Wooden Box"]
},
"Goostav": {
"loves": ["Slime Egg Shell", "Glowshroom", "Mini Slime Squid", "Gold Slimeback", "Swamp Gourd"],
"likes": ["Slimestone", "Essence of Slime", "Frog", "Snail", "Green Butterfly", "Swamp Algae", "Sporefly"]
}
};
const defaultLikedItems = {};
for (const [townsfolk2, gifts] of Object.entries(defaultTownsfolkGifts)) {
gifts.loves.forEach((item) => {
if (!defaultLikedItems[item]) defaultLikedItems[item] = { likes: [], loves: [] };
defaultLikedItems[item].loves.push(townsfolk2);
});
gifts.likes.forEach((item) => {
if (!defaultLikedItems[item]) defaultLikedItems[item] = { likes: [], loves: [] };
defaultLikedItems[item].likes.push(townsfolk2);
});
}
const goldImageUrl = "https://farmrpg.com/img/items/gold_17.png?1";
const parseHtml = (htmlString) => {
const tempElement = document.createElement("div");
tempElement.innerHTML = htmlString;
return tempElement;
};
function debounceHudUpdate(fn, delay) {
let timer = null;
let lastArgs = null;
let lastThis = null;
let invoked = false;
return function(...args) {
lastArgs = args;
lastThis = this;
const callNow = !invoked || args[0];
if (timer) {
clearTimeout(timer);
}
if (callNow) {
fn.apply(lastThis, lastArgs);
invoked = true;
}
timer = setTimeout(() => {
fn.apply(lastThis, lastArgs);
timer = null;
invoked = false;
}, delay);
};
}
const refreshInventory = () => {
myApp.showIndicator();
$.ajax({
url: `inventory.php`,
method: "GET"
}).done(() => {
myApp.hideIndicator();
});
};
const getDefaultTextColor = () => darkModeActive ? "white" : "black";
const capitalizeFirst = (text) => text[0].toUpperCase() + text.slice(1);
const notify = (title, subtitle) => {
myApp.addNotification({ title, subtitle });
};
const getMaterialsDelta = (recipe, amountConsumed) => {
const delta = {};
for (const [materialName, requiredPerCraft] of Object.entries(recipe)) {
if (["Iron", "Nails"].includes(materialName)) continue;
delta[materialName] = requiredPerCraft * amountConsumed * -1;
}
return delta;
};
const getMaxCraftable = (recipe, inventory) => {
return Math.min(...Object.entries(recipe).map(([materialName, requiredPerCraft]) => {
var _a2;
const materialId = itemNameIdMap.get(materialName);
return Math.floor((((_a2 = inventory[materialId]) == null ? void 0 : _a2.count) ?? 0) / requiredPerCraft);
}));
};
const getCraftResult = (remainingSlots, maxCraftable, returnRate2) => {
maxCraftable = Math.min(maxCraftable, remainingSlots);
const amountCrafted = Math.min(remainingSlots, Math.round(maxCraftable * returnRate2));
const materialsUsed = Math.round(amountCrafted / returnRate2);
return { amountCrafted, materialsUsed };
};
let returnRate = GM_getValue(STORAGE_KEYS.RETURN_RATE, 1.45);
const setReturnRate = (rate) => returnRate = rate;
let recipes = GM_getValue(STORAGE_KEYS.RECIPES, {});
const setRecipes = (newRecipes) => recipes = newRecipes;
const craftworksDependencies = /* @__PURE__ */ new Set();
const craftworksIngredients = /* @__PURE__ */ new Set();
let craftworks = GM_getValue(STORAGE_KEYS.CRAFTWORKS, []);
const setCraftworks = (value) => {
craftworks = value;
generateDependencies();
};
const generateDependencies = () => {
craftworksDependencies.clear();
craftworksIngredients.clear();
const enabledItems = new Set(craftworks.filter((entry) => entry.enabled).map((entry) => entry.item));
for (const itemId of enabledItems) {
craftworksDependencies.add(itemId);
const itemName = inventoryCache[itemId].name;
const recipe = recipes[itemName];
if (!recipe) continue;
for (const materialName of Object.keys(recipe)) {
const materialId = itemNameIdMap.get(materialName);
if (!materialId) continue;
craftworksDependencies.add(materialId);
if (enabledItems.has(materialId)) {
craftworksIngredients.add(materialId);
}
}
}
};
const numberFormatter = Intl.NumberFormat("en-GB");
const parseNumberWithCommas = (text) => {
return Number(text.replaceAll(",", ""));
};
const getFormattedNumber = (number) => {
return Number.isNaN(number) ? "0" : numberFormatter.format(number);
};
let supplyPacks = GM_getValue(STORAGE_KEYS.SUPPLY_PACKS, {});
const setSupplyPacks = (packs) => supplyPacks = packs;
const parseSupplyPack = (titleRows, itemName) => {
const supplyPackTitle = titleRows.find(
(row) => row.innerText.trim().toLowerCase() === "item contents"
);
if (!supplyPackTitle) return;
const supplyPackItems = {};
const updatedInventory = {};
const itemListElement = supplyPackTitle.nextElementSibling.nextElementSibling;
const itemList = itemListElement.querySelectorAll("a.item-link");
for (const item of itemList) {
const itemId = item.href.split("id=")[1];
const image = item.querySelector("img.itemimg").src;
const name = item.querySelector(".item-title > strong").innerText.trim();
const count = Number(item.querySelector(".item-after").innerText.replace("x", "").trim());
supplyPackItems[name] = count;
if (name !== "Gold") {
updatedInventory[itemId] = { id: itemId, name, image };
}
}
updateInventory(updatedInventory, { isDetailed: true });
const updatedSupplyPacks = { ...supplyPacks, [itemName]: supplyPackItems };
GM_setValue(STORAGE_KEYS.SUPPLY_PACKS, updatedSupplyPacks);
};
let statsData = [];
const setStatsData = (data) => statsData = data;
let statsHtml = "";
const setStatsHtml = (html) => statsHtml = html;
let hudStatus = GM_getValue(STORAGE_KEYS.HUD_STATUS, false);
const setHudStatus = (status) => hudStatus = status;
let hudItems = GM_getValue(STORAGE_KEYS.HUD_ITEMS, []);
const setHudItems = (items) => hudItems = items;
let hudTimers = GM_getValue(STORAGE_KEYS.HUD_TIMERS, {});
let hudStash = GM_getValue(STORAGE_KEYS.HUD_STASH, null);
const setHudStash = (value) => hudStash = value;
let hudButton = GM_getValue(STORAGE_KEYS.HUD_BUTTON, "Explore");
const setHudButton = (value) => hudButton = value;
const toggleHudButton = () => {
const newHudButton = hudButton === "Explore" ? "Fish" : "Explore";
GM_setValue(STORAGE_KEYS.HUD_BUTTON, newHudButton);
};
let hudTimerInterval = null;
const handleHudTimerUpdate = (value) => {
hudTimers = value;
clearInterval(hudTimerInterval);
if (Object.keys(value).length > 0 && settings.mealTimersEnabled) {
let activeMeals = false;
let now = Date.now();
for (const timestamp of Object.values(hudTimers)) {
activeMeals = activeMeals || timestamp + 30 * 1e3 > now;
}
if (activeMeals) {
hudTimerInterval = setInterval(updateHudDisplay, 1e3);
}
}
return true;
};
setTimeout(() => handleHudTimerUpdate(hudTimers));
setInterval(() => handleHudTimerUpdate(hudTimers), 30 * 1e3);
let hudUrl = GM_getValue(STORAGE_KEYS.HUD_URL, null);
const setHudUrl = (url) => hudUrl = url;
const toggleHudStatus = () => {
GM_setValue(STORAGE_KEYS.HUD_STATUS, !hudStatus);
};
unsafeWindow.toggleHudStatus = toggleHudStatus;
const addHudItems = (items) => {
const seenIds = /* @__PURE__ */ new Set();
const updatedItems = [];
for (const item of [...hudItems, ...items]) {
if (!seenIds.has(item.id)) {
seenIds.add(item.id);
updatedItems.push(item);
}
}
if (updatedItems.length > hudItems.length) GM_setValue(STORAGE_KEYS.HUD_ITEMS, updatedItems);
};
const setHudDetails = (items, url) => {
items = items.filter((item) => !["Iron", "Nails"].includes(item.name));
GM_setValue(STORAGE_KEYS.HUD_ITEMS, items);
if (url) {
GM_setValue(STORAGE_KEYS.HUD_URL, url);
}
};
const setHudItemsByName = (items, displayMode = HUD_DISPLAY_MODES.INVENTORY) => {
setHudDetails(items.map((item) => {
return { ...inventoryCache[itemNameIdMap.get(item)], displayMode };
}).filter((item) => item.id));
};
const removeHudItem = (items) => {
const updatedItems = hudItems.filter((item) => !items.includes(item.id));
GM_setValue(STORAGE_KEYS.HUD_ITEMS, updatedItems);
};
const restoreHudItems = () => {
if (hudStash === null) return;
setHudItemsByName(hudStash);
GM_setValue(STORAGE_KEYS.HUD_STASH, null);
};
const stashCurrentItems = () => {
if (hudStash !== null) return;
const currentItemNames = hudItems.map((item) => item.name);
GM_setValue(STORAGE_KEYS.HUD_STASH, currentItemNames);
};
const setSupplyItemsHud = (supplyPack, amountSelected) => {
if (supplyPack.startsWith("Void Bag")) return;
const packItems = supplyPacks[supplyPack];
if (!packItems || Object.keys(packItems).length === 0) return;
const updatedHudItems = Object.entries(packItems).map(([itemName, packQuantity]) => {
return {
...inventoryCache[itemNameIdMap.get(itemName)],
amountPerSupplyPack: packQuantity,
supplyPacksSelected: amountSelected,
displayMode: HUD_DISPLAY_MODES.SUPPLY_PACK,
...itemName === "Gold" && { name: itemName, image: goldImageUrl, count: 0 }
};
}).filter((item) => item.id || item.name === "Gold");
if (updatedHudItems.length > 0) {
stashCurrentItems();
setHudDetails(updatedHudItems);
}
};
const formatRemainingTime = (timer, currentTime) => {
const remainingSeconds = Math.max(0, Math.floor((timer - currentTime) / 1e3));
if (remainingSeconds < 60) {
return `${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`;
} else if (remainingSeconds < 3600) {
const minutes = Math.floor(remainingSeconds / 60);
const seconds = remainingSeconds % 60;
return `${minutes} mins ${seconds.toString().padStart(2, "0")} secs`;
} else {
const hours = Math.floor(remainingSeconds / 3600);
const minutes = Math.floor(remainingSeconds % 3600 / 60);
const seconds = remainingSeconds % 60;
return `${hours}h ${minutes.toString().padStart(2, "0")}m ${seconds.toString().padStart(2, "0")}s`;
}
};
const getTextColour = (count, limit) => {
if (count >= limit) return "red";
else if (count >= limit * 0.8) {
const percent = count / limit;
const green = Math.round(255 - (percent - 0.8) / 0.2 * (255 - 100));
return `rgb(255, ${green}, 50);`;
}
return getDefaultTextColor();
};
const hudHtmlCallbacks = {
[HUD_DISPLAY_MODES.INVENTORY]: ({ image, count }) => {
const textColour = getTextColour(count, inventoryLimit);
return `
<span style="color: ${textColour};">
<img src="${image}" class="itemimgmini" style="width:15px; vertical-align:middle; padding-right:1px">
${getFormattedNumber(count)} / ${getFormattedNumber(inventoryLimit)}
</span>`;
},
[HUD_DISPLAY_MODES.SUPPLY_PACK]: (item) => {
const supplyPackTotal = (item.amountPerSupplyPack ?? 0) * (item.supplyPacksSelected ?? 0);
const totalOnOpen = item.count + supplyPackTotal;
const textColour = getTextColour(totalOnOpen, inventoryLimit);
const formattedLimit = getFormattedNumber(inventoryLimit);
const limitText = item.name === "Gold" ? " Gold" : `/ ${formattedLimit}`;
return `
<span style="color: ${textColour};">
<img src="${item.image}" class="itemimgmini" style="width:15px; vertical-align:middle; padding-right:1px">
${getFormattedNumber(totalOnOpen)} ${limitText}
</span>`;
},
[HUD_DISPLAY_MODES.MEAL]: ({ name, image, count }) => {
const textColour = count === 0 ? "red" : getDefaultTextColor();
return `
<span style="color: ${textColour};">
<img src="${image}" class="itemimgmini" style="width:15px; vertical-align:middle; padding-right:1px">
${name} (${getFormattedNumber(count)})
</span>`;
},
[HUD_DISPLAY_MODES.TIMER]: ({ name, image, timer, showName }) => {
const currentTime = Date.now();
const textColour = currentTime > timer ? "orange" : getDefaultTextColor();
const remainingTime = currentTime > timer ? "Finished!" : formatRemainingTime(timer, currentTime);
const timerText = showName ? `${name} (${remainingTime})` : remainingTime;
return `
<span style="color: ${textColour};">
<img src="${image}" class="itemimgmini" style="width:15px; vertical-align:middle; padding-right:1px">
${timerText}
</span>`;
}
};
const getHudItemHtml = (item) => {
const callback = hudHtmlCallbacks[item.displayMode] ?? hudHtmlCallbacks[HUD_DISPLAY_MODES.INVENTORY];
const itemContent = callback(item);
return `<td style="padding: 0px">
<a class="frpg-hud-item" href="item.php?id=${item.id}"
data-id="${item.id}" data-count="${item.count}" data-remove="${item.removeOnQuickSell ?? false}">
${itemContent}
<span class="fill-animation"
style="position: absolute; left: 0; top: -3px; width: 0; height: 125%; background-color: rgba(255, 0, 0, 0.5);
z-index: 1; border-radius: 3px;"></span>
</a>
</td>`;
};
const getHudTable = (items, perRowItems) => {
let hudHtml = '<table style="width: 100%;">';
for (let start = 0; start < items.length; start += perRowItems) {
hudHtml += "<tr>";
items.slice(start, start + perRowItems).forEach((item) => {
hudHtml += getHudItemHtml(item);
});
hudHtml += "</tr>";
}
hudHtml += "</table>";
return hudHtml;
};
const exitEditMode = () => {
setEditMode(false);
};
unsafeWindow.refreshInventory = refreshInventory;
unsafeWindow.restoreHudItems = restoreHudItems;
unsafeWindow.exitEditMode = exitEditMode;
const getHudHtml = () => {
const hudHasItems = hudItems.length > 0;
const filteredTimers = Object.fromEntries(
Object.entries(hudTimers).filter(([, timer]) => Date.now() - timer <= 15 * 1e3)
);
const timersCount = Object.keys(filteredTimers).length;
const displayTimers = timersCount > 0 && settings.mealTimersEnabled;
const perRowItems = hudItems.length > 12 ? 3 : 2;
const timerRows = Math.ceil(timersCount / perRowItems);
const itemRows = Math.ceil(hudItems.length / perRowItems);
const totalRows = timerRows * displayTimers + itemRows + (hudHasItems && displayTimers);
const hudTranslateY = 50 + 4 * (totalRows - 1);
let hudHtml = `<div id="frpg-hud" style="
margin: 0;
position: absolute;
background: ${darkModeActive ? "#111111" : "#ffffff"};
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border: 1px solid ${darkModeActive ? "#555555" : "#dddddd"};
padding: 5px;
transform: translateY(-${hudTranslateY}%) translateX(-8px);
line-height: 18px;
z-index: 99999;
">
${statsData.join("")}
<hr>`;
const hudSegments = [];
if (displayTimers) {
const showName = timersCount === 1;
const timerItems = Object.entries(filteredTimers).map(([id, timer]) => {
return {
...inventoryCache[id],
timer,
showName,
displayMode: HUD_DISPLAY_MODES.TIMER
};
});
hudSegments.push(getHudTable(timerItems, perRowItems));
}
if (hudHasItems) {
const detailedItems = hudItems.map((item) => {
return { ...item, ...inventoryCache[item.id] };
});
hudSegments.push(getHudTable(detailedItems, perRowItems));
}
if (!displayTimers && !hudHasItems) {
hudSegments.push("<span>HUD empty!</span>");
}
hudHtml += hudSegments.join("<hr />");
const continueButton = `<a class="button" style="margin-left: 2%; height: 22px; line-height: 20px; white-space: nowrap;" href="${hudUrl}">C</a>`;
const restoreButton = `<a class="button" style="margin-left: 2%; height: 22px; line-height: 20px; white-space: nowrap;" onclick="restoreHudItems()">R</a>`;
const exitEditModeButton = `<a class="button" style="margin-left: 2%; height: 22px; line-height: 20px; white-space: nowrap;" onclick="exitEditMode()">E</a>`;
let miniButton;
if (editMode) miniButton = exitEditModeButton;
else if (hudStash !== null && settings.hudStashEnabled) miniButton = restoreButton;
else miniButton = continueButton;
let hudButtonText = hudButton === "Explore" ? "Explore" : "Fish";
let HudButtonLink = hudButton === "Explore" ? "/explore.php" : "/fish.php";
hudHtml += `<div style="display: flex; margin-top: 5px; margin-bottom: 15px;">
<a class="button" style="height: 22px; line-height: 20px; width: 42%;" onclick="refreshInventory()">Refresh</a>
<a id="frpg-hud-button" href="${HudButtonLink}" class="button" style="margin-left: 2%; height: 22px; line-height: 20px; width: 42%;">${hudButtonText}</a>
${miniButton}
</div>`;
hudHtml += `</div>`;
return hudHtml;
};
const _updateHudDisplay = (forceUpdate = false) => {
if (document.hidden && !forceUpdate) return;
const parentElement = document.querySelector("#statszone_tracker");
if (!parentElement) return;
if (!hudStatus) {
if (forceUpdate) parentElement.innerHTML = statsHtml;
return;
}
const hudElement = getHudHtml();
parentElement.innerHTML = hudElement;
};
const updateHudDisplay = debounceHudUpdate(_updateHudDisplay, 100);
const hudRemovalTimeouts = {};
const cancelHudRemoval = (itemId) => {
clearTimeout(hudRemovalTimeouts[itemId]);
};
const getCleanupCallback = (target) => {
return (removeItem = true) => {
const targetStyle = target.firstElementChild.style;
targetStyle.color = getDefaultTextColor();
if (removeItem && target.dataset.remove === "true") {
const itemId = target.dataset.id;
hudRemovalTimeouts[itemId] = setTimeout(() => {
removeHudItem([itemId]);
}, 2500);
}
return true;
};
};
const getApplicableInventory = (recipeDetails, triggerItem, applicableCount, bypassReserve) => {
var _a2;
const applicableInventory = {};
const globalReserve = getGlobalReserveAmount();
for (const materialName of Object.keys(recipeDetails)) {
const materialId = itemNameIdMap.get(materialName);
const itemCount = inventoryCache[materialId].count;
applicableInventory[materialId] = { count: itemCount };
if (["Iron", "Nails"].includes(materialName)) continue;
if (materialName === triggerItem) {
applicableInventory[materialId].count = applicableCount;
} else if (!bypassReserve) {
applicableInventory[materialId].count = Math.max(0, itemCount - (((_a2 = quickActions[materialName]) == null ? void 0 : _a2.reserve) ?? globalReserve));
}
}
return applicableInventory;
};
const handleItemCraft$1 = (itemName, applicableCount, action, cleanup) => {
const targetItemName = action.item;
if (targetItemName === "Select") {
myApp.addNotification({ title: "No item selected to craft!", subtitle: "Please go to item details and select the item to craft into" });
return cleanup(false);
}
const targetItemId = itemNameIdMap.get(targetItemName);
const targetItem = inventoryCache[targetItemId];
const recipe = recipes[targetItemName];
if (!recipe) {
myApp.addNotification({ title: "Recipe not found!", subtitle: "Please go to the workshop to refresh recipes or select another item to craft" });
return cleanup(false);
}
const inventoryLeft = inventoryLimit - targetItem.count;
if (inventoryLeft === 0) {
addHudItems([{ ...targetItem, removeOnQuickSell: true }]);
return cleanup(false) && refreshInventory();
}
cancelHudRemoval(itemNameIdMap.get(targetItemName));
const applicableInventory = getApplicableInventory(recipe, itemName, applicableCount, action.bypassReserve ?? false);
const maxCraftable = getMaxCraftable(recipe, applicableInventory);
const craftCount = Math.min(maxCraftable, inventoryLeft);
if (craftCount === 0) {
return cleanup(false) && refreshInventory();
}
$.ajax({
url: `worker.php?go=craftitem&id=${targetItemId}&qty=${craftCount}`,
method: "POST"
}).done(
function(data) {
if (data === "success") {
addHudItems([{ ...targetItem, removeOnQuickSell: true }]);
} else if (data === "cannotafford") {
myApp.addNotification({ title: "Cannot afford error", subtitle: "Inventory most likely out of sync" });
} else {
myApp.addNotification({ title: "Something went wrong...", subtitle: `Unexpected server response: ${data}` });
}
return cleanup();
}
);
};
const useMeal = (mealId, mealName) => {
$.ajax({
url: `worker.php?go=useitem&id=${mealId}`,
method: "POST"
}).done(function(data) {
switch (data) {
case "success":
myApp.addNotification({ title: "Delicious", subtitle: `${mealName} successfully used` });
break;
case "limit":
myApp.addNotification({ title: "Meal limit reached!", subtitle: "Cancel some meals or buy more slots" });
break;
case "error":
myApp.addNotification({ title: "Cannot use meals right now!", subtitle: "Try again later when reset is over" });
break;
default:
myApp.addNotification({ title: "Something went wrong...", subtitle: `Unexpected server response: ${data}` });
break;
}
});
};
const confirmMealUse = (mealId, mealName) => {
const actions = [
{
text: `Are you sure you want to eat ${mealName}?`,
label: true
},
{
text: "Yes",
onClick: () => useMeal(mealId, mealName)
},
{
text: "Cancel",
color: "red",
onClick: refreshInventory
}
];
myApp.actions(actions);
};
let townsfolk = GM_getValue(STORAGE_KEYS.TOWNSFOLK, {});
const setTownsfolk = (newTownsfolk) => townsfolk = newTownsfolk;
let townsfolkGifts = GM_getValue(STORAGE_KEYS.TOWNSFOLK_GIFTS, defaultLikedItems);
const setTownsfolkGifts = (newTownsfolkGifts) => townsfolkGifts = newTownsfolkGifts;
const addTownsfolkGifts = (townsfolkName, items) => {
var _a2;
let newFound = false;
for (const [itemId, status] of Object.entries(items)) {
const statusKey = {
like: "likes",
likes: "likes",
liked: "likes",
love: "loves",
loves: "loves",
loved: "loves"
}[status];
if (!statusKey) continue;
const itemName = (_a2 = inventoryCache[itemId]) == null ? void 0 : _a2.name;
if (!itemName) continue;
if (!townsfolkGifts[itemName]) {
townsfolkGifts[itemName] = {
likes: [],
loves: []
};
}
if (!townsfolkGifts[itemName][statusKey].includes(townsfolkName)) {
townsfolkGifts[itemName][statusKey].push(townsfolkName);
newFound = true;
}
}
if (newFound) {
GM_setValue(STORAGE_KEYS.TOWNSFOLK_GIFTS, townsfolkGifts);
}
return newFound;
};
const parseQuickSend = (panelRows) => {
const quickGiveRow = panelRows.find((row) => row.innerHTML.includes("npclevels.php"));
if (!quickGiveRow) return;
const optionsElement = quickGiveRow.querySelector(".quickgivedd");
if (!optionsElement) return;
const updatedTownsfolk = {};
const itemId = optionsElement.dataset.id;
const options = optionsElement.options;
Array.from(options).slice(1).forEach((opt) => {
const split = opt.innerText.split("(");
const townsfolkName = split[0].trim();
const townsfolkId = opt.value;
updatedTownsfolk[townsfolkName] = townsfolkId;
if (split.length > 1) {
const itemStatus = split[1].replace(")", "").toLowerCase().trim();
addTownsfolkGifts(townsfolkName, { [itemId]: itemStatus });
}
});
if (Object.keys(updatedTownsfolk).length > 5) {
GM_setValue(STORAGE_KEYS.TOWNSFOLK, updatedTownsfolk);
}
};
const confirmQuickAction = (itemName, quickAction, target, animate = true) => {
var _a2, _b, _c, _d;
const actions = [
{ text: "Item config", label: true },
{ text: `Item: ${itemName}` },
{ text: `Action: ${capitalizeFirst(quickAction.action)}`, onClick: () => promptQuickAction(itemName, quickAction, target) }
];
if (quickAction.reserve !== void 0) {
actions.push({
text: `Reserve: ${getFormattedNumber(quickAction.reserve)}`,
onClick: () => promptReserveAmount(itemName, quickAction, target)
});
}
if (quickAction.action === "send") {
const loved = (_b = (_a2 = townsfolkGifts[itemName]) == null ? void 0 : _a2.loves) == null ? void 0 : _b.includes(quickAction.townsfolk);
const liked = (_d = (_c = townsfolkGifts[itemName]) == null ? void 0 : _c.likes) == null ? void 0 : _d.includes(quickAction.townsfolk);
const townsfolkText = `${quickAction.townsfolk}${loved ? " (loves)" : ""}${liked ? " (likes)" : ""}`;
actions.push({ text: `Townsfolk: ${townsfolkText}`, onClick: () => promptQuickSend(itemName, quickAction, target) });
} else if (quickAction.action === "craft") {
const recipeDetails = inventoryCache[itemNameIdMap.get(quickAction.item)];
actions.push({
text: `Crafted Item: ${quickAction.item} (${getFormattedNumber(recipeDetails.count)})`,
onClick: () => promptQuickCraft(itemName, quickAction, target, quickAction.bypassReserve)
});
actions.push({
text: `Bypass Reserve: ${quickAction.bypassReserve ? "Yes" : "No"}`,
onClick: () => {
quickAction.bypassReserve = !quickAction.bypassReserve;
confirmQuickAction(itemName, quickAction, target, false);
}
});
}
actions.push({ text: "Actions", label: true });
if (quickAction.action !== "none") {
actions.push({ text: "Perform", onClick: () => handleQuickAction(target, quickAction) });
}
actions.push(
{ text: "Save", onClick: () => updateQuickAction(itemName, quickAction) },
{ text: "Cancel", color: "red" }
);
myApp.actions(actions, animate);
};
const setReserve = (quickAction, reserveAmount) => {
quickAction["reserve"] = reserveAmount;
return quickAction;
};
const promptReserveAmount = (itemName, quickAction, target) => {
const percent10 = parseInt(inventoryLimit * 0.1);
const percent25 = parseInt(inventoryLimit * 0.25);
const percent50 = parseInt(inventoryLimit * 0.5);
const percent90 = parseInt(inventoryLimit * 0.9);
const actions = [
{ text: "Select the reserve amount:", label: true },
{ text: "No reserve", onClick: () => confirmQuickAction(itemName, setReserve(quickAction, 0), target) },
{
text: `${getFormattedNumber(getGlobalReserveAmount())} (Global Reserve)`,
onClick: () => confirmQuickAction(itemName, setReserve(quickAction, getGlobalReserveAmount()), target)
},
{ text: `${getFormattedNumber(percent10)} (10%)`, onClick: () => confirmQuickAction(itemName, setReserve(quickAction, percent10), target) },
{ text: `${getFormattedNumber(percent25)} (25%)`, onClick: () => confirmQuickAction(itemName, setReserve(quickAction, percent25), target) },
{ text: `${getFormattedNumber(percent50)} (50%)`, onClick: () => confirmQuickAction(itemName, setReserve(quickAction, percent50), target) },
{ text: `${getFormattedNumber(percent90)} (90%)`, onClick: () => confirmQuickAction(itemName, setReserve(quickAction, percent90), target) }
];
if (quickAction.reserve !== void 0) {
actions.push({ text: `${getFormattedNumber(quickAction.reserve)} (current)`, onClick: () => confirmQuickAction(itemName, quickAction, target) });
}
actions.push({ text: "Cancel", color: "red" });
myApp.actions(actions);
};
const promptQuickSell = (itemName, quickAction, target) => {
quickAction.action = "sell";
promptReserveAmount(itemName, quickAction, target);
};
const getSendAction = (quickAction, target) => {
quickAction.action = "send";
quickAction.townsfolk = target;
return quickAction;
};
const promptQuickSend = (itemName, quickAction, target, displayAll = false) => {
var _a2, _b, _c, _d;
if (!townsfolkGifts[itemName]) {
displayAll = true;
}
const actions = [
{ text: "Select the townsfolk to send the item to: ", label: true }
];
for (const npc of Object.keys(townsfolk)) {
const loved = (_b = (_a2 = townsfolkGifts[itemName]) == null ? void 0 : _a2.loves) == null ? void 0 : _b.includes(npc);
const liked = (_d = (_c = townsfolkGifts[itemName]) == null ? void 0 : _c.likes) == null ? void 0 : _d.includes(npc);
if (!displayAll && !(liked || loved)) continue;
const targetText = `${npc}${loved ? " (loves)" : ""}${liked ? " (likes)" : ""}`;
actions.push({ text: targetText, onClick: () => promptReserveAmount(itemName, getSendAction(quickAction, npc), target) });
}
if (!displayAll) {
actions.push({ text: "Show All", onClick: () => promptQuickSend(itemName, quickAction, target, true) });
}
actions.push({ text: "Cancel", color: "red" });
myApp.actions(actions);
};
const isCraftable = (itemName) => {
for (const recipe of Object.values(recipes)) {
if (recipe[itemName]) return true;
}
return false;
};
const getCraftAction = (quickAction, recipe, bypassReserve) => {
quickAction.action = "craft";
quickAction.item = recipe;
quickAction.bypassReserve = bypassReserve;
return quickAction;
};
const promptQuickCraft = (itemName, quickAction, target, bypassReserve = false, animate = true) => {
if (!isCraftable(itemName)) {
const actions2 = [
{ text: "No recipes unlocked for this item yet", label: true },
{ text: "Cancel", color: "red" }
];
myApp.actions(actions2);
return;
}
const craftableItems = [];
for (const [recipeName, ingredients] of Object.entries(recipes)) {
if (ingredients[itemName] !== void 0) craftableItems.push(recipeName);
}
const actions = [
{ text: "Bypass other materials' reserve?", label: true },
{ text: `Enabled: ${bypassReserve ? "Yes" : "No"}`, onClick: () => promptQuickCraft(itemName, quickAction, target, !bypassReserve, false) },
{ text: "Select the item to craft", label: true }
];
for (const recipe of craftableItems) {
const recipeDetails = inventoryCache[itemNameIdMap.get(recipe)];
actions.push({
text: `${recipe} (inv: ${getFormattedNumber(recipeDetails.count)})`,
onClick: () => promptReserveAmount(itemName, getCraftAction(quickAction, recipe, bypassReserve), target)
});
}
actions.push({ text: "Cancel", color: "red" });
myApp.actions(actions, animate);
};
const promptQuickUse = (itemName, quickAction, target) => {
quickAction.action = "use";
promptReserveAmount(itemName, quickAction, target);
};
const promptNoAction = (itemName, quickAction, target) => {
quickAction.action = "none";
confirmQuickAction(itemName, quickAction, target);
};
const promptQuickAction = (itemName, quickAction, target) => {
quickAction = { ...quickAction ?? {} };
const possibleActions = [
{
display: true,
text: `Select the quick action for ${itemName}`,
label: true
},
{
display: true,
text: "Sell",
onClick: () => promptQuickSell(itemName, quickAction, target)
},
{
display: townsfolkGifts[itemName],
text: "Send",
onClick: () => promptQuickSend(itemName, quickAction, target)
},
{
display: isCraftable(itemName),
text: "Craft",
onClick: () => promptQuickCraft(itemName, quickAction, target)
},
{
display: staminaItems.includes(itemName),
text: "Use",
onClick: () => promptQuickUse(itemName, quickAction, target)
},
{
display: true,
text: "None",
onClick: () => promptNoAction(itemName, quickAction, target)
},
{
display: true,
text: "Cancel",
color: "red"
}
];
const displayedActions = possibleActions.filter((action) => action.display);
myApp.actions(displayedActions);
};
const getSellUrlParams = (itemId, itemName, count) => {
switch (itemName) {
case "Steak":
return `sellsteaks&amt=${count}`;
case "Steak Kabob":
return `sellkabobs&amt=${count}`;
default:
return `sellitem&id=${itemId}&qty=${count}`;
}
};
const handleItemSell = (itemId, itemName, count, cleanup) => {
const urlParameters = getSellUrlParams(itemId, itemName, count);
$.ajax({
url: `worker.php?go=${urlParameters}`,
method: "POST"
}).done((data) => {
if (data === "error") {
myApp.addNotification({ title: "Error selling item!", subtitle: "Inventory most likely out of sync. Craftworks?" });
}
cleanup();
});
};
const handleItemSend$1 = (itemId, count, action, cleanup) => {
const sendTarget = action.townsfolk;
const targetId = townsfolk[sendTarget];
if (!targetId) {
myApp.addNotification({
title: "Invalid townsfolk selected",
subtitle: "The townsfolk set for this item does not exist"
});
return cleanup(false);
}
$.ajax({
url: `worker.php?go=givemailitem&id=${itemId}&to=${targetId}&qty=${count}&rs=1`,
method: "POST"
}).done((data) => {
if (data === "cannotrec") {
myApp.addNotification({
title: "Error!",
subtitle: "This NPC cannot accept this item"
});
}
cleanup();
});
};
const handleItemUse = (itemName, count, cleanup) => {
if (!staminaItems.includes(itemName)) {
myApp.addNotification({ title: "This shouldn't be possible...", subtitle: "Cannot use this item" });
return cleanup();
}
const method = itemName === "Apple" ? "eatxapples" : "drinkxojs";
$.ajax({
url: `worker.php?go=${method}&amt=${count}`,
method: "POST"
}).done(function() {
return cleanup();
});
};
let quickActions = GM_getValue(STORAGE_KEYS.QUICK_ACTIONS, {});
const setQuickActions = (actions) => quickActions = actions;
const updateQuickAction = (itemName, actionDetails) => {
quickActions[itemName] = actionDetails;
GM_setValue(STORAGE_KEYS.QUICK_ACTIONS, quickActions);
};
const handleQuickAction = (target, itemAction = null) => {
const cleanup = getCleanupCallback(target);
const itemId = target.dataset.id;
const itemCount = target.dataset.count;
const itemName = inventoryCache[itemId].name;
const isMeal = mealNames.has(itemName);
if (isMeal) return confirmMealUse(itemId, itemName);
if (unsellableItems.includes(itemName)) {
myApp.addNotification({ title: "Cannot perform quick action", subtitle: "Quick actions cannot be performed on this item" });
return cleanup(false);
}
if (!itemAction) {
itemAction = quickActions[itemName];
if (editMode && (itemAction == null ? void 0 : itemAction.action)) {
return cleanup(false) && confirmQuickAction(itemName, itemAction, target);
}
}
if (!(itemAction == null ? void 0 : itemAction.action) || itemAction.action === "none") {
return cleanup(false) && promptQuickAction(itemName, itemAction, target);
}
const applicableCount = itemCount - ((itemAction == null ? void 0 : itemAction.reserve) ?? getGlobalReserveAmount());
if (applicableCount <= 0) return cleanup(false) && refreshInventory();
const action = itemAction.action;
const targetStyle = target.firstElementChild.style;
targetStyle.color = {
"send": "cyan",
"craft": "skyblue",
"sell": "green",
"use": "orange"
}[action] ?? getDefaultTextColor();
if (action === "send") {
return handleItemSend$1(itemId, applicableCount, itemAction, cleanup);
} else if (action === "craft") {
return handleItemCraft$1(itemName, applicableCount, itemAction, cleanup);
} else if (action === "use") {
return handleItemUse(itemName, applicableCount, cleanup);
} else if (action === "sell") {
return handleItemSell(itemId, itemName, applicableCount, cleanup);
}
myApp.addNotification({ title: "Invalid quickAction selected", subtitle: `"${action}" is not a valid action. Please go to item page and re-configure` });
};
let _settings = GM_getValue(STORAGE_KEYS.SETTINGS, {});
const settings = new Proxy(_settings, {
get(target, key) {
if (key in target) return target[key];
if (typeof key === "string" && key in defaultSettings) {
return defaultSettings[key].default;
}
return void 0;
}
});
const setSettings = (value) => _settings = value;
let editMode = false;
const setEditMode = (value) => {
editMode = value;
if (value) {
notify("Edit mode enabled", "Perform quick action on any item to edit its config");
}
updateHudDisplay(true);
};
const toggleSetting = (key) => {
settings[key] = !(settings[key] ?? defaultSettings[key].default);
GM_setValue(STORAGE_KEYS.SETTINGS, settings);
if (key === "mealTimersEnabled") {
handleHudTimerUpdate(hudTimers);
}
};
const exportQuickActions = () => {
const quickActionsString = JSON.stringify(quickActions);
GM_setClipboard(quickActionsString, "text");
myApp.addNotification({ title: "Successfully exported QuickActions!", subtitle: `Exported ${Object.keys(quickActions).length} entries` });
};
const importQuickActions = () => {
const input = prompt("Paste the QuickAction export:");
try {
const parsedActions = JSON.parse(input);
if (typeof parsedActions !== "object" || Array.isArray(parsedActions) || parsedActions === null) {
throw new Error("Invalid paste");
}
GM_setValue(STORAGE_KEYS.QUICK_ACTIONS, { ...quickActions, ...parsedActions });
myApp.addNotification({ title: "Succesfully imported QuickActions!", subtitle: `Imported ${Object.keys(quickActions).length} entries` });
} catch (error) {
myApp.addNotification({ title: "Error importing QuickActions!", subtitle: `Please paste the full string from the export | ${error}` });
}
};
const showSettings = () => {
const settingActions = [
...Object.keys(defaultSettings).map((key) => {
return {
text: `${defaultSettings[key].label}: ${settings[key] ?? defaultSettings[key].default ? "Yes" : "No"}`,
onClick: () => {
toggleSetting(key);
showSettings();
}
};
}),
{
text: "Export QuickActions",
onClick: exportQuickActions
},
{
text: "Import QuickActions",
onClick: importQuickActions
},
{
text: "Exit",
color: "red"
}
];
myApp.actions(settingActions);
};
let inventoryCache = GM_getValue(STORAGE_KEYS.INVENTORY, {});
const setInventory = (inventory) => inventoryCache = inventory;
let inventoryLimit = GM_getValue(STORAGE_KEYS.INVENTORY_LIMIT, 1e3);
const setInventoryLimit = (limit) => inventoryLimit = limit;
const getGlobalReserveAmount = () => {
return Math.floor(inventoryLimit / 5);
};
const itemNameIdMap = /* @__PURE__ */ new Map();
const populateItemNameIdMap = () => {
for (const [itemId, item] of Object.entries(inventoryCache)) {
itemNameIdMap.set(item.name, itemId);
}
};
populateItemNameIdMap();
const resolveItemNames = (updateBatch) => {
const resolvedBatch = {};
for (const itemName of Object.keys(updateBatch)) {
const itemId = itemNameIdMap.get(itemName);
if (!itemId) continue;
resolvedBatch[itemId] = updateBatch[itemName];
}
return resolvedBatch;
};
const applyUpdateBatch = (inventory, updateBatch, { isAbsolute = false, isDetailed = false }) => {
let newItem = false;
for (const itemId of Object.keys(updateBatch)) {
if (isDetailed) {
if (!inventory[itemId]) newItem = true;
inventory[itemId] = { ...inventory[itemId], ...updateBatch[itemId] };
} else if (isAbsolute) {
inventory[itemId].count = updateBatch[itemId];
} else {
inventory[itemId].count = Math.max(0, Math.min(
inventoryLimit,
(inventory[itemId].count ?? 0) + updateBatch[itemId]
));
}
}
return newItem;
};
generateDependencies();
const simulateCraftworks = (inventory, craftedItem) => {
const usedIngredients = new Set(craftworksIngredients);
if (craftedItem) {
const recipe = recipes[inventoryCache[craftedItem].name];
if (recipe) {
Object.keys(recipe).forEach((materialName) => {
const materialId = itemNameIdMap.get(materialName);
if (materialId) usedIngredients.add(materialId);
});
}
}
const maxedItems = [];
for (const { item: recipeId, enabled } of craftworks) {
if (!enabled) continue;
const itemDetails = inventory[recipeId];
if (!itemDetails) continue;
const recipe = recipes[itemDetails.name];
if (!recipe) continue;
const remainingSlots = inventoryLimit - itemDetails.count;
if (remainingSlots === 0) continue;
const maxCraftable = getMaxCraftable(recipe, inventory);
if (maxCraftable === 0) continue;
const { amountCrafted, materialsUsed } = getCraftResult(remainingSlots, maxCraftable, returnRate);
const materialsDelta = getMaterialsDelta(recipe, materialsUsed);
const resolvedDelta = resolveItemNames({ ...materialsDelta, [itemDetails.name]: amountCrafted });
applyUpdateBatch(inventory, resolvedDelta, { isAbsolute: false, isDetailed: false });
if (settings.craftworksNotifications && remainingSlots === amountCrafted && !usedIngredients.has(recipeId)) {
maxedItems.push(recipeId);
}
}
if (maxedItems.length > 0) {
const notificationText = maxedItems.map((itemId) => {
const itemName = inventoryCache[itemId].name;
return `<a href="item.php?id=${itemId}">${itemName}</a>`;
}).join(", ");
notify("Craftworks Inventory Full", `${notificationText} ${maxedItems.length > 1 ? "are" : "is"} no longer being crafted!`);
}
};
const updateInventory = (updateBatch, { isAbsolute = false, isDetailed = false, resolveNames = false, overwriteMissing = false, processCraftworks = false, craftedItem = null }) => {
const inventory = inventoryCache;
if (resolveNames) {
updateBatch = resolveItemNames(updateBatch);
}
const newItem = applyUpdateBatch(inventory, updateBatch, { isAbsolute, isDetailed });
if (overwriteMissing) {
for (const itemId of Object.keys(inventory)) {
if (!updateBatch[itemId]) {
inventory[itemId].count = 0;
}
}
}
if (processCraftworks && settings.processCraftworks) {
const dependencyUpdated = Object.keys(updateBatch).some((itemId) => craftworksDependencies.has(itemId));
if (dependencyUpdated) simulateCraftworks(inventory, craftedItem);
}
GM_setValue(STORAGE_KEYS.INVENTORY, inventory);
if (newItem) {
GM_setValue(STORAGE_KEYS.NEW_ITEM, Date.now());
}
};
const parseInventory = (response) => {
const parsedInventory = parseHtml(response);
const items = parsedInventory.querySelectorAll(".list-group > ul a.item-link");
const currentLimit = Number(parsedInventory.querySelector(".card-content-inner > strong").innerText.replaceAll(",", ""));
const updatedInventory = {};
for (const item of items) {
const itemId = item.href.split("?id=")[1];
const name = item.querySelector(".item-title > strong").innerText;
const image = item.querySelector(".item-media > img").src;
const count = parseNumberWithCommas(item.getElementsByClassName("item-after")[0].innerText);
updatedInventory[itemId] = {
id: itemId,
name,
image,
count
};
}
updateInventory(updatedInventory, { isDetailed: true, overwriteMissing: true });
GM_setValue(STORAGE_KEYS.INVENTORY_LIMIT, currentLimit);
GM_setValue(STORAGE_KEYS.PRODUCTION_LAST_UPDATE, Date.now());
return response;
};
const inventoryListener = {
name: "Inventory",
callback: parseInventory,
urlMatch: [/^inventory\.php/],
passive: true
};
const handleItemCraft = (response, parameters) => {
if (response !== "success") return;
const itemId = parameters.get("id");
const craftCount = Number(parameters.get("qty"));
if (Number.isNaN(craftCount) || craftCount <= 0) return;
const itemDetails = inventoryCache[itemId];
if (!itemDetails) return;
const itemName = itemDetails.name;
const recipe = recipes[itemName];
if (!recipe) return;
const inventoryLeft = inventoryLimit - itemDetails.count;
const { amountCrafted, materialsUsed } = getCraftResult(inventoryLeft, craftCount, returnRate);
const materialsDelta = getMaterialsDelta(recipe, materialsUsed);
updateInventory({ ...materialsDelta, [itemName]: amountCrafted }, { isAbsolute: false, resolveNames: true, processCraftworks: true, craftedItem: itemId });
};
const handleCraftworksReorder = (response, parameters) => {
var _a2;
if (response !== "success") return;
const newOrder = parameters.get("ords");
if (!newOrder) return;
const updatedCraftworks = [];
for (const item of newOrder.split("|")) {
if (!item) break;
const [itemId, itemIndex] = item.split(",");
updatedCraftworks[Number(itemIndex) - 1] = {
item: itemId,
enabled: ((_a2 = craftworks.find((entry) => entry.item === itemId)) == null ? void 0 : _a2.enabled) ?? false
};
}
GM_setValue(STORAGE_KEYS.CRAFTWORKS, updatedCraftworks);
};
const craftingWorkers = [
{
action: "craftitem",
listener: handleItemCraft
},
{
action: "setcwitemorder",
listener: handleCraftworksReorder
}
];
const extractNumber = (element) => parseNumberWithCommas(element.innerText.trim());
const handleExploration = (response, parameters) => {
const parsedResponse = parseHtml(response);
const foundItems = parsedResponse.querySelectorAll(`img[src^="/img/items/"]`);
const updateBatch = {};
const ciderUsed = parameters.get("cider");
const lemonadeUsed = parameters.get("go") === "drinklm";
for (const itemImage of foundItems) {
const itemName = itemImage.alt.trim();
if (!itemName) continue;
let itemCount = updateBatch[itemName] ?? 0;
if (itemImage.style.filter.includes("grayscale") || itemImage.classList.contains("ifs")) {
itemCount = inventoryLimit;
} else if (itemImage.nextSibling && (ciderUsed || lemonadeUsed)) {
itemCount += parseNumberWithCommas(itemImage.nextSibling.textContent.trim().split("x")[1].slice(0, -1));
} else {
itemCount += 1;
}
updateBatch[itemName] = itemCount;
}
const updateItemDifference = (itemName, selector) => {
const selectedItem = parsedResponse.querySelector(selector);
if (!selectedItem) return;
const count = extractNumber(selectedItem);
updateBatch[itemName] = count - inventoryCache[itemNameIdMap.get(itemName)].count;
};
if (ciderUsed || lemonadeUsed) {
const itemUsed = ciderUsed ? "Apple Cider" : parsedResponse.querySelector("#lmtyp").innerText.split("(")[0].trim();
const countSelector = ciderUsed ? "#cidercnt" : "#lmcnt";
updateItemDifference(itemUsed, countSelector);
}
updateItemDifference("Apple", "#applecnt");
if (Object.keys(updateBatch).length > 0) {
updateInventory(updateBatch, { isAbsolute: false, resolveNames: true, processCraftworks: true });
}
};
const explorationWorkers = [
{
action: "drinklm",
listener: handleExploration
},
{
action: "explore",
listener: handleExploration
}
];
const processHarvestData = (data) => {
if (!data.drops) return;
const updatedInventory = {};
for (const cropId of Object.keys(data.drops)) {
updatedInventory[cropId] = data.drops[cropId].qty;
}
updateInventory(updatedInventory, { isAbsolute: false });
};
const handleFarmHarvest = (response) => {
if (response === "") return;
const parsedResponse = JSON.parse(response);
processHarvestData(parsedResponse);
try {
const cropSelect = document.querySelector(".seedid");
if (cropSelect) {
unsafeWindow.updateCropCount({ target: cropSelect });
}
} catch (error) {
console.log("Error while updating crop counts", error);
}
};
const handleGrapeJuiceVat = (response) => {
if (response === "") return;
const parsedResponse = JSON.parse(response);
processHarvestData(parsedResponse);
};
const farmWorkers = [
{
action: "harvest",
listener: handleFarmHarvest
},
{
action: "harvestall",
listener: handleFarmHarvest
},
{
action: "grapejuicevat",
listener: handleGrapeJuiceVat
}
];
const handleManualFish = (response) => {
const parsedResponse = parseHtml(response);
const fishName = parsedResponse.querySelector("img").alt;
parsedResponse.lastElementChild.remove();
let caughtCount;
if (parsedResponse.innerText.trim() === fishName) {
caughtCount = 1;
} else {
caughtCount = parseNumberWithCommas(parsedResponse.innerText.split("x")[1].slice(0, -1));
if (Number.isNaN(caughtCount)) {
return;
}
}
updateInventory({ [fishName]: caughtCount }, { isAbsolute: false, resolveNames: true });
};
const handleNetFish = (response) => {
const parsedResponse = parseHtml(response);
const updateBatch = {};
const caughtFish = parsedResponse.querySelectorAll(`img[src^="/img/items/"]`);
for (const fishImage of caughtFish) {
let fishName = fishImage.alt;
if (!fishName) {
const fishDetails = hudItems.find((item) => item.image.includes(fishImage.src));
if (!fishDetails) continue;
fishName = fishDetails.name;
}
const quantityNode = fishImage.nextSibling;
let caughtCount = updateBatch[fishName] ?? 0;
if (fishImage.style.filter.includes("grayscale")) {
caughtCount = inventoryLimit;
} else if ((quantityNode == null ? void 0 : quantityNode.nodeType) === 3 && quantityNode.textContent.trim() !== "") {
caughtCount += Number(quantityNode.textContent.split("x")[1].trim().slice(0, -1));
} else {
caughtCount += 1;
}
updateBatch[fishName] = caughtCount;
}
const itemUsed = parsedResponse.querySelector("#nettyp").innerText.trim();
const count = parseNumberWithCommas(parsedResponse.querySelector("#netcnt").innerText.trim());
const difference = count - inventoryCache[itemNameIdMap.get(itemUsed)].count;
updateBatch[itemUsed] = difference;
updateInventory(updateBatch, { isAbsolute: false, resolveNames: true });
};
const fishingWorkers = [
{
action: "fishcaught",
listener: handleManualFish
},
{
action: "castnet",
listener: handleNetFish
}
];
const handleFishSell = () => {
const updatedInventory = {};
for (const item of hudItems) {
const itemId = item.id;
const itemDetails = inventoryCache[itemId];
if (itemDetails.locked === false) {
updatedInventory[itemId] = -itemDetails.count;
}
}
updateInventory(updatedInventory, { isAbsolute: false });
};
const handleItemSale = (response, parameters) => {
if (response === "error") return;
const itemId = parameters.get("id");
const itemCount = parameters.get("qty");
updateInventory({ [itemId]: -itemCount }, { isAbsolute: false });
};
const handleKabobSale = (response, parameters) => {
if (response === "cannotafford") return;
const amount = parameters.get("amt");
updateInventory({ "Steak Kabob": -amount }, { isAbsolute: false, resolveNames: true });
};
const handleSteakSale = (response, parameters) => {
if (response === "cannotafford") return;
const amount = parameters.get("amt");
updateInventory({ "Steak": -amount }, { isAbsolute: false, resolveNames: true });
};
const itemSellWorkers = [
{
action: "sellitem",
listener: handleItemSale
},
{
action: "sellalluserfish",
listener: handleFishSell
},
{
action: "sellkabobs",
listener: handleKabobSale
},
{
action: "sellsteaks",
listener: handleSteakSale
}
];
const handleItemSend = (response, parameters) => {
var _a2;
if (!response.includes("wk__item_max")) return;
const html = parseHtml(response);
const maxItemCountElement = html.querySelector("#wk__item_max");
if (!maxItemCountElement) return;
const maxItemCount = parseNumberWithCommas(maxItemCountElement.textContent);
const itemId = parameters.get("id");
const itemCount = parameters.get("qty");
updateInventory({ [itemId]: -itemCount }, { isAbsolute: false, processCraftworks: true });
if (((_a2 = inventoryCache[itemId]) == null ? void 0 : _a2.count) !== maxItemCount) {
updateInventory({ [itemId]: maxItemCount }, { isAbsolute: true });
}
};
const itemSendWorkers = [
{
action: "givemailitem",
listener: handleItemSend
}
];
const handleMealUse = (response, parameters) => {
if (response !== "success") return;
const itemId = parameters.get("id");
const itemDetails = inventoryCache[itemId];
if (!itemDetails) return;
const mealTimeSeconds = mealTimeExceptions[itemDetails.name] ?? 5 * 60;
const endTime = Date.now() + mealTimeSeconds * 1e3;
const updatedTimers = { ...hudTimers };
if (updatedTimers[itemId] && updatedTimers[itemId] > Date.now()) {
updatedTimers[itemId] += mealTimeSeconds * 1e3;
} else {
updatedTimers[itemId] = endTime;
}
GM_setValue(STORAGE_KEYS.HUD_TIMERS, updatedTimers);
updateInventory({ [itemId]: -1 }, { isAbsolute: false });
};
const handleLocksmithOpen = (response, parameters) => {
var _a2;
const parsedResponse = parseHtml(response);
const updatedInventory = {};
const supplyPackId = parameters.get("id");
const supplyPackName = (_a2 = inventoryCache[supplyPackId]) == null ? void 0 : _a2.name;
const supplyPackData = supplyPacks[supplyPackName] ?? {};
let updateSupplyPacks = false;
for (const itemRow of parsedResponse.querySelectorAll("img")) {
const itemDetails = itemRow.nextSibling;
const [nameText, countText] = itemDetails.textContent.split("x");
const itemName = nameText.trim();
const itemCount = parseNumberWithCommas(countText);
if (!Object.keys(supplyPackData).includes(itemName)) {
supplyPackData[itemName] = 0;
updateSupplyPacks = true;
}
updatedInventory[itemName] = itemCount;
}
updateInventory(updatedInventory, { isAbsolute: false, resolveNames: true, processCraftworks: true });
if (updateSupplyPacks && supplyPackName && !supplyPackName.startsWith("Void Bag")) {
GM_setValue(STORAGE_KEYS.SUPPLY_PACKS, { ...supplyPacks, [supplyPackName]: supplyPackData });
}
};
const handleOrangeJuiceUse = (response, parameters) => {
if (!response.toLowerCase().includes("you drank")) return;
const amount = parameters.get("amt");
updateInventory({ "Orange Juice": -amount }, { isAbsolute: false, resolveNames: true });
};
const handleAppleUse = (response, parameters) => {
if (!response.toLowerCase().includes("you ate")) return;
const amount = parameters.get("amt");
updateInventory({ Apple: -amount }, { isAbsolute: false, resolveNames: true });
};
const handleAllAppleUse = (response) => {
if (!response.toLowerCase().includes("you ate")) return;
updateInventory({ Apple: 0 }, { isAbsolute: true, resolveNames: true });
};
const handleAllOrangeJuiceUse = (response) => {
if (!response.toLowerCase().includes("you drank")) return;
updateInventory({ "Orange Juice": 0 }, { isAbsolute: true, resolveNames: true });
};
const itemUseWorkers = [
{
action: "useitem",
listener: handleMealUse
},
{
action: "openitem",
listener: handleLocksmithOpen
},
{
action: "drinkxojs",
listener: handleOrangeJuiceUse
},
{
action: "eatxapples",
listener: handleAppleUse
},
{
action: "eatapple",
listener: (response) => handleAppleUse(response, { get: () => 1 })
},
{
action: "drinkoj",
listener: (response) => handleOrangeJuiceUse(response, { get: () => 1 })
},
{
action: "eatapples",
listener: handleAllAppleUse
},
{
action: "drinkojs",
listener: handleAllOrangeJuiceUse
}
];
const parseItemCount = (itemString) => {
let [itemName, countText] = itemString.split("x");
itemName = itemName.replace("(", "").trim();
const itemCount = parseNumberWithCommas(countText.split(")")[0]);
return [itemName, itemCount];
};
const handleWheelSpin = (response) => {
const parsedResponse = parseHtml(response.split("|")[1]);
const rewardText = parsedResponse.innerText.split(":")[1];
let [itemName, itemCount] = parseItemCount(rewardText);
if (itemName === "Apples") itemName = "Apple";
updateInventory({ [itemName]: itemCount }, { isAbsolute: false, resolveNames: true });
};
const handleWishingWellThrow = (response, parameters) => {
var _a2;
if (response === "cannotafford") return;
const thrownId = parameters.get("id");
const thrownCount = Number(parameters.get("amt"));
const thrownItemName = (_a2 = inventoryCache[thrownId]) == null ? void 0 : _a2.name;
const parsedResponse = parseHtml(response);
const updateBatch = {};
const items = parsedResponse.querySelectorAll("img");
for (const item of items) {
let [itemName, itemCount] = parseItemCount(item.nextSibling.textContent);
updateBatch[itemName] = itemCount;
}
if (thrownItemName) {
updateBatch[thrownItemName] = -thrownCount;
}
updateInventory(updateBatch, { isAbsolute: false, resolveNames: true });
};
const handleRewardsClaim = (response) => {
if (response === "") return;
if (response.toLowerCase().includes("no rewards left")) return;
const parsedResponse = parseHtml(response);
const updateBatch = {};
const items = parsedResponse.querySelectorAll("img");
for (const item of items) {
let [itemName, itemCount] = parseItemCount(item.nextSibling.textContent);
updateBatch[itemName] = itemCount;
}
updateInventory(updateBatch, { isAbsolute: false, resolveNames: true });
};
const miscWorkers = [
{
action: "spinfirst",
listener: handleWheelSpin
},
{
action: "tossmanyintowell",
listener: handleWishingWellThrow
},
{
action: "collectrew",
listener: handleRewardsClaim
}
];
const handleBeachball = (response) => {
try {
const itemMatch = response.match(/<strong>(.*?)<\\\/strong>/);
const quantityMatch = response.match(/class='itemimg'> x(\d+)/);
if (itemMatch && itemMatch[1]) {
const itemName = itemMatch[1];
const quantity = quantityMatch && quantityMatch[1] ? parseInt(quantityMatch[1]) : 1;
updateInventory({ [itemName]: quantity }, { isAbsolute: false, resolveNames: true });
}
} catch (error) {
console.error("Error processing beachball response:", error);
}
};
const beachballWorkers = [
{
action: "beachball",
listener: handleBeachball
}
];
let quests = GM_getValue(STORAGE_KEYS.QUESTS, {});
const setQuests = (updatedQuests) => {
quests = updatedQuests;
};
const handleQuestClaim = (response, parameters) => {
if (response === "") return;
if (response === "already") return;
if (response !== "success") return;
const questId = parameters.get("id");
const questDetails = quests[questId];
if (!questDetails) return;
const requestedItems = { ...questDetails.request };
for (const [name, quantity] of Object.entries(requestedItems)) {
requestedItems[name] = -quantity;
}
const updateBatch = { ...questDetails.reward, ...requestedItems };
updateInventory(updateBatch, { isAbsolute: false, resolveNames: true });
};
const questWorkers = [
{
action: "collectquest",
listener: handleQuestClaim
}
];
const workers = [
...explorationWorkers,
...fishingWorkers,
...itemSellWorkers,
...craftingWorkers,
...itemUseWorkers,
...itemSendWorkers,
...miscWorkers,
...farmWorkers,
...beachballWorkers,
...questWorkers
];
const activeWorkers = /* @__PURE__ */ new Map();
const workerActions = /* @__PURE__ */ new Set();
for (const worker of workers) {
workerActions.add(worker.action);
activeWorkers.set(worker.action, worker.listener);
}
const actionString = Array.from(workerActions).join("|");
const urlMatch = new RegExp(`worker\\.php\\?.*go=(${actionString})`);
const handleWorkerEvents = (response, url) => {
const urlParameters = new URLSearchParams(url.split("?")[1]);
const action = urlParameters.get("go");
const worker = activeWorkers.get(action);
worker(response, urlParameters);
return response;
};
const workerListener = {
name: "Worker Events",
callback: handleWorkerEvents,
urlMatch: [urlMatch],
passive: true
};
const fetchLocationData = (type, id) => {
return GM_getValue(`${STORAGE_KEYS.LOCATION_PREFIX}.${type}-${id}`, null);
};
const setLocationData = (type, id, data) => {
const locationItems = [];
for (const itemId of Object.keys(data)) {
locationItems.push({
id: itemId,
name: data[itemId].name,
image: data[itemId].image
});
}
GM_setValue(`${STORAGE_KEYS.LOCATION_PREFIX}.${type}-${id}`, locationItems);
};
const parseArea = (response, url) => {
const urlParameters = new URLSearchParams(url.split("?")[1]);
const locationId = urlParameters.get("id");
const type = url.startsWith("area.php") ? "explore" : "fishing";
const locationData = fetchLocationData(type, locationId);
if (locationData === null) {
myApp.addNotification({ title: "New location detected", subtitle: "Please visit the location details to track items with HUD!" });
return response;
}
setHudDetails(locationData, url);
GM_setValue(STORAGE_KEYS.HUD_STASH, null);
return response;
};
const areaListener = {
name: "Areas",
callback: parseArea,
urlMatch: [/^area\.php.*/, /^fishing\.php.*/],
passive: true
};
const parseCraftworks = (response) => {
const parsedResponse = parseHtml(response);
const itemList = parsedResponse.querySelectorAll(".close-panel[data-id]");
const craftworksItems = [];
const inventoryUpdate = {};
for (const element of itemList) {
const itemId = element.dataset.id;
const enabled = Array.from(element.querySelectorAll("button")).some((button) => button.classList.contains("pausecwbtn"));
craftworksItems.push({ item: itemId, enabled });
const titleChildren = element.querySelector(".item-title").children;
const countText = titleChildren[titleChildren.length - 1].firstChild.textContent.split(":")[1].trim();
const itemCount = parseNumberWithCommas(countText);
inventoryUpdate[itemId] = itemCount;
}
GM_setValue(STORAGE_KEYS.CRAFTWORKS, craftworksItems);
updateInventory(inventoryUpdate, { isAbsolute: true });
return response;
};
const craftworksListener = {
name: "Craftworks",
callback: parseCraftworks,
urlMatch: [/^craftworks\.php/],
passive: true
};
const parseHome = (response) => {
const parsedResponse = parseHtml(response);
const timers = parsedResponse.querySelectorAll(`[data-countdown-to]`);
const updatedTimers = {};
for (const element of timers) {
const linkElement = element.closest(".item-link");
if (!linkElement) continue;
try {
const itemId = new URLSearchParams(linkElement.href.split("?")[1]).get("id");
const rawTime = element.dataset.countdownTo;
const parsedTime = luxon.DateTime.fromISO(rawTime, { zone: "America/Chicago" });
const time = new Date(parsedTime.toISO());
updatedTimers[itemId] = +time;
} catch {
continue;
}
}
GM_setValue(STORAGE_KEYS.HUD_TIMERS, { ...hudTimers, ...updatedTimers });
return response;
};
const homeListener = {
name: "Home Page",
callback: parseHome,
urlMatch: [/^index\.php/],
passive: true
};
const addToggleButton = (element) => {
const statsDiv = element.firstElementChild;
const toggleHtml = `<span><a id="frpg-hud-toggle" style="padding: 3px 5px 2px 5px; border: 1px solid; border-radius: 5px;" onclick="toggleHudStatus()" href="#">HUD</span>`;
const hrElement = statsDiv == null ? void 0 : statsDiv.querySelector("hr");
if (hrElement) {
hrElement.insertAdjacentHTML("beforeBegin", toggleHtml);
} else {
const spans = element.querySelectorAll("span");
if (spans.length > 0) {
spans[spans.length - 1].insertAdjacentHTML("afterEnd", toggleHtml);
return element;
} else {
statsDiv.insertAdjacentHTML("beforeEnd", toggleHtml);
}
}
return element;
};
const explorationHud = (response) => {
var _a2;
const parsedResponse = JSON.parse(response);
const mainHtml = parsedResponse.html_main;
const trackerHtml = parsedResponse.html_tracker;
const mainElement = parseHtml(mainHtml);
const trackerElement = trackerHtml ? parseHtml(trackerHtml) : trackerHtml;
addToggleButton(mainElement);
if (trackerElement) {
addToggleButton(trackerElement);
}
const statsElements = Array.from(mainElement.querySelectorAll("span"));
if (statsElements.length >= 3) {
const acElement = statsElements[2];
const acCount = parseNumberWithCommas(acElement.innerText);
if (acCount !== ((_a2 = inventoryCache[itemNameIdMap.get("Ancient Coin")]) == null ? void 0 : _a2.count)) {
updateInventory({ "Ancient Coin": acCount }, { isAbsolute: true, resolveNames: true });
}
}
setStatsData(statsElements.map((i) => i.innerHTML));
setStatsHtml(trackerElement ? trackerElement.innerHTML : "");
return JSON.stringify({
...parsedResponse,
html_main: mainElement.innerHTML,
html_tracker: hudStatus ? getHudHtml() : trackerElement ? trackerElement.innerHTML : trackerElement
});
};
const hudListener = {
name: "Exploration HUD",
callback: explorationHud,
urlMatch: [/worker\.php\?.*go=getstats/],
passive: false
};
const cloneRowAfter = (row, title, subtitle, afterValue = "", img = "/img/items/7211.png") => {
var _a2;
const newRow = row.cloneNode(true);
const titleElement = newRow.querySelector(".item-title");
const titleNode = titleElement.childNodes[0];
titleNode.textContent = title;
const subtitleNode = titleNode.nextSibling.nextSibling;
subtitleNode.style.textWrap = "wrap";
subtitleNode.textContent = subtitle;
newRow.querySelector("img.itemimg").src = img;
newRow.querySelector(".item-after").innerHTML = afterValue;
(_a2 = newRow.querySelector(".progressbar")) == null ? void 0 : _a2.remove();
row.after(newRow);
return newRow;
};
const clearRowsAfter = (settingsRow) => {
let node = settingsRow.nextElementSibling;
while (node) {
const element = node;
node = node.nextElementSibling;
element.remove();
}
};
const quickActionChangeHandler = (target, ignoreUpdate = false) => {
var _a2, _b, _c, _d;
const settingsRow = target.closest("#frpg-quick-action");
const itemName = settingsRow.dataset.name;
clearRowsAfter(settingsRow);
const itemAction = quickActions[itemName] ?? {};
const selectedAction = ((_a2 = target.value) == null ? void 0 : _a2.trim()) ?? "none";
const getListenerString = (value, itemName2) => `updateQuickActionParameter('${value}', '${itemName2}', event.target.value)`;
const reserveValue = itemAction.reserve ?? getGlobalReserveAmount();
const reserveInputHtml = `
<input type="number" class="inlineinputlg" min="-1" onclick="$(this).select()" onchange="${getListenerString("reserve", itemName)}" value="${reserveValue}" />
`;
const reserveRow = cloneRowAfter(settingsRow, "Reserve Amount", "Amount to keep in reserve while performing quick actions", reserveInputHtml);
if (selectedAction === "none") {
if (ignoreUpdate) return;
updateQuickAction(itemName, {
...itemAction,
action: "none"
});
return;
}
if (selectedAction === "send") {
let selectedTownsfolk = itemAction.townsfolk;
const quickGiveSelector = (_b = reserveRow.closest("ul")) == null ? void 0 : _b.querySelector(".quickgivedd");
const sendableTownsfolk = [];
if (quickGiveSelector) {
const options = Array.from(quickGiveSelector.options).slice(1);
const optionTownsfolk = options.map((opt) => opt.innerText.split("(")[0].trim());
sendableTownsfolk.push(...optionTownsfolk);
} else {
sendableTownsfolk.push(...((_c = townsfolkGifts[itemName]) == null ? void 0 : _c.likes) ?? []);
sendableTownsfolk.push(...((_d = townsfolkGifts[itemName]) == null ? void 0 : _d.loves) ?? []);
}
if (!selectedTownsfolk) {
const selectedOption = quickGiveSelector == null ? void 0 : quickGiveSelector.selectedOptions[0];
if (!selectedOption || selectedOption.innerText.startsWith("-")) {
selectedTownsfolk = sendableTownsfolk[0] ?? Object.keys(townsfolk)[0];
} else {
selectedTownsfolk = selectedOption.innerText.split("(")[0].trim();
}
}
const townsfolkOptions = sendableTownsfolk.map((t) => `<option value="${t}" ${selectedTownsfolk === t ? "selected" : ""}>${t}</option>`).join("");
const townsfolkSelectHtml = `
<select class="inlineinputlg" onchange="${getListenerString("townsfolk", itemName)}">${townsfolkOptions}</select>
`;
cloneRowAfter(reserveRow, "Target Townsfolk", "Who would like this gift", townsfolkSelectHtml, "/img/items/icon_mail.png?1");
if (ignoreUpdate) return;
if (!selectedTownsfolk) return;
updateQuickAction(itemName, { ...itemAction, action: selectedAction, townsfolk: selectedTownsfolk });
return;
} else if (selectedAction === "craft") {
const craftableItems = [];
for (const [recipeName, recipeDetails] of Object.entries(recipes)) {
if (Object.keys(recipeDetails).includes(itemName)) craftableItems.push(recipeName);
}
let selectedRecipe = itemAction.item;
if (!selectedRecipe && craftableItems.length > 0) selectedRecipe = craftableItems[0];
const recipeOptions = craftableItems.map((recipe) => `<option value="${recipe}" ${recipe === selectedRecipe ? "selected" : ""}>${recipe}</option>`);
const recipeSelectHtml = `<select class="inlineinputlg" onchange="${getListenerString("item", itemName)}">${recipeOptions}</select>`;
const bypassReserve = itemAction.bypassReserve ?? false;
const bypassSelectHtml = `
<select class="inlineinputlg" onchange="${getListenerString("bypassReserve", itemName)}">
<option value="false" ${bypassReserve ? "" : "selected"}>No</option>
<option value="true" ${bypassReserve ? "selected" : ""}>Yes</option>
</select>
`;
const itemRow = cloneRowAfter(reserveRow, "Item", "Which item to craft this into", recipeSelectHtml, "/img/items/5868.png");
cloneRowAfter(itemRow, "Bypass Material Reserve", "Ignore reserve values of other materials", bypassSelectHtml, "/img/items/5868.png");
if (ignoreUpdate) return;
if (!selectedRecipe) return;
updateQuickAction(itemName, { ...itemAction, action: selectedAction, item: selectedRecipe, bypassReserve });
return;
} else {
if (ignoreUpdate) return;
updateQuickAction(itemName, { ...itemAction, action: selectedAction });
return;
}
};
const updateQuickActionParameter = (updateValue, itemName, newValue) => {
const itemData = quickActions[itemName] ?? {};
if (updateValue === "reserve") {
newValue = Number(newValue);
if (Number.isNaN(newValue) || newValue < 0) newValue = getGlobalReserveAmount();
} else if (updateValue === "bypassReserve") {
newValue = newValue === "true";
}
itemData[updateValue] = newValue;
quickActions[itemName] = itemData;
GM_setValue(STORAGE_KEYS.QUICK_ACTIONS, quickActions);
};
const getPanelRows = (parsedResponse) => Array.from(parsedResponse.querySelectorAll(".list-block > ul > li.close-panel"));
const getTitleRows = (parsedResponse) => Array.from(parsedResponse.querySelectorAll(".content-block-title"));
const detectSendableItem = (panelRows) => Object.keys(townsfolk).length > 0 && panelRows.some(
(row) => Array.from(row.querySelectorAll(".item-title")).some((i) => i.firstChild.textContent.trim().toLowerCase() === "givable")
);
const detectCraftableItem = (titleRows) => titleRows.some((row) => row.innerText.trim().toLowerCase() === "crafting use");
const detectUsableItem = (itemName) => staminaItems.includes(itemName);
const detectSellableItem = (panelRows, itemName) => {
if (unsellableItems.includes(itemName)) return false;
const hasQuickSell = panelRows.some((row) => row.innerHTML.includes("market.php"));
return hasQuickSell || ["Steak", "Steak Kabob"].includes(itemName);
};
const generateQuickActionOptions = (flags, itemQuickActions) => {
const { itemSendable, itemCraftable, itemSellable, itemUsable } = flags;
const actions = {
"None": true,
"Use": itemUsable,
"Send": itemSendable,
"Craft": itemCraftable,
"Sell": itemSellable
};
return Object.entries(actions).map(([option, show]) => {
if (!show) return "";
const selected = (itemQuickActions == null ? void 0 : itemQuickActions.action) === option.toLowerCase() ? "selected" : "";
return `<option value="${option.toLowerCase()}" ${selected}>${option}</option>`;
}).join("");
};
const wrapDropdownHtml = (options) => `<select onchange="quickActionChangeHandler(event.target)" class="inlineinputlg" id="frpg-quick-action-value">
${options}
</select>`;
const appendQuickActionRow = (lastRow, itemName, itemId, dropdownHtml) => {
const row = cloneRowAfter(lastRow, "Quick Action", "Select the action on middle click or tap and hold", dropdownHtml);
row.setAttribute("id", "frpg-quick-action");
row.setAttribute("data-id", itemId);
row.setAttribute("data-name", itemName);
return row;
};
const addQuickActionDropdown = (panelRows, itemName, itemId, flags) => {
const lastRow = panelRows[panelRows.length - 1];
const itemQuickActions = quickActions[itemName];
const optionsHtml = generateQuickActionOptions(flags, itemQuickActions);
const dropdownHtml = wrapDropdownHtml(optionsHtml);
const settingRow = appendQuickActionRow(lastRow, itemName, itemId, dropdownHtml);
const selectElement = settingRow.querySelector("#frpg-quick-action-value");
quickActionChangeHandler(selectElement, true);
};
const displayItemConfig = (parsedResponse, itemName, itemId) => {
const panelRows = getPanelRows(parsedResponse);
const titleRows = getTitleRows(parsedResponse);
const itemSendable = detectSendableItem(panelRows);
const itemCraftable = detectCraftableItem(titleRows);
const itemUsable = detectUsableItem(itemName);
const itemSellable = detectSellableItem(panelRows, itemName);
parseSupplyPack(titleRows, itemName);
if (itemSendable) parseQuickSend(panelRows);
if (itemSendable || itemSellable || itemCraftable || itemUsable) {
addQuickActionDropdown(panelRows, itemName, itemId, {
itemSendable,
itemCraftable,
itemSellable,
itemUsable
});
}
};
unsafeWindow.updateQuickActionParameter = updateQuickActionParameter;
unsafeWindow.quickActionChangeHandler = quickActionChangeHandler;
const extractItemId = (url) => new URLSearchParams(url.split("?")[1]).get("id");
const extractItemName = (parsedHtml) => {
var _a2;
return (_a2 = parsedHtml.querySelector(".navbar-inner > .center.sliding")) == null ? void 0 : _a2.innerText.trim();
};
const extractItemCount = (parsedHtml) => {
var _a2;
const inventoryLink = parsedHtml.querySelector(`a[href="inventory.php"]`);
const inventoryRow = inventoryLink == null ? void 0 : inventoryLink.closest(".item-content");
const countText = ((_a2 = inventoryRow == null ? void 0 : inventoryRow.querySelector(".item-after")) == null ? void 0 : _a2.innerText.trim()) || "0";
return parseNumberWithCommas(countText);
};
const extractItemImage = (parsedHtml) => {
var _a2;
return ((_a2 = parsedHtml.querySelector(".itemimglg")) == null ? void 0 : _a2.src) || null;
};
const parseItemRow = (itemElement) => {
const itemId = new URLSearchParams(itemElement.href.split("?")[1]).get("id");
const name = itemElement.querySelector(".item-title > strong").innerText;
const image = itemElement.querySelector(".item-media > img").src;
const count = parseNumberWithCommas(itemElement.querySelector(".item-after").innerText.split("/")[0]);
return { id: itemId, name, image, count };
};
const parseIngredients = (parsedHtml) => {
const craftingSections = Array.from(parsedHtml.querySelectorAll(".content-block-title:has(.fa-wrench)"));
const ingredientsSection = craftingSections.find((section) => section.innerText.toLowerCase().includes("crafting recipe"));
if (!ingredientsSection) return {};
const ingredientDetails = ingredientsSection.nextElementSibling.nextElementSibling;
const itemList = Array.from(ingredientDetails.querySelectorAll("a.item-link"));
const ingredients = {};
for (const item of itemList) {
const itemData = parseItemRow(item);
ingredients[itemData.id] = itemData;
}
return ingredients;
};
const parseCrafts = (parsedHtml) => {
const craftingSections = Array.from(parsedHtml.querySelectorAll(".content-block-title:has(.fa-wrench)"));
const craftsSection = craftingSections.find((section) => section.innerText.toLowerCase().includes("crafting use"));
if (!craftsSection) return {};
const craftsDetails = craftsSection.nextElementSibling.nextElementSibling;
const itemList = Array.from(craftsDetails.querySelectorAll("a.item-link"));
const crafts = {};
for (const item of itemList) {
const itemData = parseItemRow(item);
crafts[itemData.id] = itemData;
}
return crafts;
};
const parseItem = (response, url) => {
const parsedResponse = parseHtml(response);
const itemId = extractItemId(url);
const itemName = extractItemName(parsedResponse);
const itemCount = extractItemCount(parsedResponse);
const itemImage = extractItemImage(parsedResponse);
const itemDetails = {
id: itemId,
name: itemName,
count: itemCount,
...itemImage && { image: itemImage }
};
const ingredientsInventory = parseIngredients(parsedResponse);
const craftsInventory = parseCrafts(parsedResponse);
updateInventory({
[itemId]: itemDetails,
...ingredientsInventory,
...craftsInventory
}, { isDetailed: true });
displayItemConfig(parsedResponse, itemName, itemId);
return parsedResponse.innerHTML;
};
const itemListener = {
name: "Items",
callback: parseItem,
urlMatch: [/^item\.php\?id=\d+/],
passive: false
};
const parseLocationDetails = (response, url) => {
const urlParameters = new URLSearchParams(url.split("?")[1]);
const type = urlParameters.get("type");
const locationId = urlParameters.get("id");
const parsedLocation = parseHtml(response);
const items = parsedLocation.querySelectorAll(".card-content-inner > .row > .col-25");
const updatedInventory = {};
for (const item of items) {
if (item.querySelector(`a[href^="item.php"]`) === null) continue;
const itemId = item.firstElementChild.href.split("=")[1];
let node = item.firstElementChild;
while (node.nodeType !== Node.TEXT_NODE) {
node = node.nextSibling;
}
const name = node.textContent.trim();
const image = item.firstElementChild.firstElementChild.src;
const count = parseNumberWithCommas(item.children[item.children.length - 1].textContent.trim().split(" ")[0]);
const itemData = {
id: itemId,
name,
image,
count
};
const lockElement = item.querySelector("span > .f7-icons");
if (lockElement) {
itemData.locked = lockElement.parentElement.title === "Item locked";
}
updatedInventory[itemId] = itemData;
}
updateInventory(updatedInventory, { isDetailed: true });
setLocationData(type, locationId, updatedInventory);
return response;
};
const locationListener = {
name: "Location",
callback: parseLocationDetails,
urlMatch: [/^location\.php.*/],
passive: true
};
const trackSupplyPack = (target) => {
const itemContainer = target.closest(".item-content");
const supplyPackName = itemContainer.querySelector(".item-title > strong").childNodes[0].textContent.trim();
const quantity = parseNumberWithCommas(itemContainer.querySelector("input.qty").value);
setSupplyItemsHud(supplyPackName, quantity);
};
unsafeWindow.trackSupplyPack = trackSupplyPack;
const parseLocksmith = (response) => {
const parsedResponse = parseHtml(response);
const itemList = parsedResponse.querySelectorAll(".close-panel");
for (const item of itemList) {
item.setAttribute("onclick", "trackSupplyPack(event.target)");
}
const qtyInputs = parsedResponse.querySelectorAll("input.qty");
for (const input of qtyInputs) {
input.setAttribute("oninput", "trackSupplyPack(event.target)");
}
return parsedResponse.innerHTML;
};
const locksmithListener = {
name: "Locksmith",
callback: parseLocksmith,
urlMatch: [/^locksmith\.php/],
passive: false
};
const parseTownsfolk = (response) => {
const parsedResponse = parseHtml(response);
const mailboxLinks = parsedResponse.querySelectorAll('a[href^="mailbox.php?id"]');
const updatedTownsfolk = {};
for (const link of mailboxLinks) {
const townsfolkName = link.nextElementSibling.nextElementSibling.innerText.trim();
const townsfolkId = new URLSearchParams(link.href.split("?")[1]).get("id");
updatedTownsfolk[townsfolkName] = townsfolkId;
}
if (!("Captain Thomas" in updatedTownsfolk)) {
updatedTownsfolk["Captain Thomas"] = updatedTownsfolk["Cpt Thomas"];
delete updatedTownsfolk["Cpt Thomas"];
}
if ("Charles" in updatedTownsfolk) {
updatedTownsfolk["Charles Horsington III"] = updatedTownsfolk["Charles"];
delete updatedTownsfolk["Charles"];
}
GM_setValue(STORAGE_KEYS.TOWNSFOLK, updatedTownsfolk);
return response;
};
const townsfolkListener = {
name: "Townsfolk",
callback: parseTownsfolk,
urlMatch: [/^npclevels\.php/],
passive: true
};
const wheelSpin = (response) => {
setHudItemsByName(wheelItems);
return response;
};
const spinListener = {
name: "Wheel Spin",
callback: wheelSpin,
urlMatch: [/^spin\.php/],
passive: true
};
const parseWorkshop = (response) => {
const parsedWorkshop = parseHtml(response);
const recipeItems = {};
const materialItems = {};
const recipes2 = {};
const recipeList = parsedWorkshop.querySelectorAll("ul > li.close-panel");
for (const recipe of recipeList) {
const recipeId = recipe.dataset.id;
const recipeName = recipe.dataset.name;
if (recipeId === void 0) continue;
recipeItems[recipeId] = {
id: recipeId,
name: recipeName,
image: recipe.querySelector(".itemimg").src,
count: Number(recipe.querySelector(".item-title > strong > span").textContent.replace(/(,|\(|\))/g, ""))
};
let materialName;
let materialCount;
let materialRequired;
const recipeMaterials = {};
const materialDetails = recipe.querySelector(".item-title > span");
let node = materialDetails.firstChild;
while (node !== null) {
if (node.nodeName === "BR") {
node = node.nextSibling;
continue;
}
if (node.nodeName === "SPAN" && node.style.color === "red") {
const parts = node.textContent.split(" ");
materialCount = parseNumberWithCommas(parts[0]);
materialRequired = parseNumberWithCommas(parts[2]);
materialName = parts.slice(3).join(" ");
} else {
const amountNode = node.nextSibling;
const nameNode = amountNode.nextSibling;
materialCount = Number(node.textContent.replace(/(,|\/)/g, "").trim());
materialRequired = Number(amountNode.dataset.amt);
materialName = nameNode.textContent.trim();
node = nameNode;
}
materialItems[materialName] = materialCount;
recipeMaterials[materialName] = materialRequired;
node = node.nextSibling;
}
recipes2[recipeName] = recipeMaterials;
}
for (const card of parsedWorkshop.querySelectorAll(".card-content-inner")) {
const cardText = card.innerText.trim();
const regex = /resource saver perk is (\d{1,2})%/;
const result = regex.exec(cardText);
if (result) {
const newRateText = result[1];
const newRate = 1 + Number(newRateText) / 100;
GM_setValue(STORAGE_KEYS.RETURN_RATE, newRate);
break;
}
}
updateInventory(recipeItems, { isAbsolute: true, isDetailed: true });
updateInventory(materialItems, { isAbsolute: true, resolveNames: true });
GM_setValue(STORAGE_KEYS.RECIPES, recipes2);
GM_setValue(STORAGE_KEYS.PRODUCTION_LAST_UPDATE, Date.now());
return response;
};
const workshopListener = {
name: "Workshop",
callback: parseWorkshop,
urlMatch: [/^workshop\.php$/],
passive: true
};
let production = GM_getValue(STORAGE_KEYS.PRODUCTION, {});
const setProduction = (value) => production = value;
let productionTimeout = null;
const acquireLock = () => {
const currentTime = Date.now();
const lockTimeout = 10 * 1e3;
const lockTime = GM_getValue(STORAGE_KEYS.PRODUCTION_LOCK, 0);
if (lockTime > currentTime - lockTimeout) return false;
GM_setValue(STORAGE_KEYS.PRODUCTION_LOCK, currentTime);
const newLockTime = GM_getValue(STORAGE_KEYS.PRODUCTION_LOCK);
return newLockTime === currentTime;
};
const releaseLock = () => {
GM_setValue(STORAGE_KEYS.PRODUCTION_LOCK, 0);
};
const tenMinuteProduction = (productionTime) => {
const updateBatch = Object.fromEntries(tenMinuteProductionItems.map((itemName) => [itemName, production[itemName] ?? 0]));
const hickoryActive = hudTimers[itemNameIdMap.get("Hickory Omelette")] > productionTime;
if (hickoryActive) {
updateBatch["Wood"] = Math.floor(production["Wood"] / 5);
updateBatch["Board"] = Math.floor(production["Board"] / 5);
updateBatch["Oak"] = Math.floor(production["Oak"] / 5);
}
updateInventory(updateBatch, { isAbsolute: false, resolveNames: true, processCraftworks: true });
};
const hourlyProduction = () => {
const updateBatch = Object.fromEntries(hourlyProductionItems.map((itemName) => [itemName, production[itemName] ?? 0]));
updateInventory(updateBatch, { isAbsolute: false, resolveNames: true, processCraftworks: true });
};
const getNextUpdate = (lastUpdate) => {
const nextUpdate = new Date(lastUpdate);
const isTenMinuteMark = nextUpdate.getUTCMinutes() % 10 === 0;
const productionNotYetRan = nextUpdate.getSeconds() < 15;
nextUpdate.setSeconds(15);
nextUpdate.setMilliseconds(0);
const shouldRunThisMinute = isTenMinuteMark && productionNotYetRan;
if (!shouldRunThisMinute) {
nextUpdate.setUTCMinutes(Math.ceil((nextUpdate.getUTCMinutes() + 1) / 10) * 10);
}
return nextUpdate;
};
const handleProduction = () => {
if (!acquireLock()) return scheduleProduction();
const lastUpdate = GM_getValue(STORAGE_KEYS.PRODUCTION_LAST_UPDATE, Date.now());
const nextUpdate = getNextUpdate(lastUpdate);
const currentTime = /* @__PURE__ */ new Date();
let finishedUpdate = null;
while (nextUpdate < currentTime) {
const updateMinute = nextUpdate.getUTCMinutes();
if (updateMinute % 10 === 0) {
tenMinuteProduction(nextUpdate);
}
if (updateMinute === 0) {
hourlyProduction();
}
finishedUpdate = +nextUpdate;
nextUpdate.setUTCMinutes(Math.ceil((updateMinute + 1) / 10) * 10);
}
if (finishedUpdate) GM_setValue(STORAGE_KEYS.PRODUCTION_LAST_UPDATE, finishedUpdate);
scheduleProduction();
releaseLock();
};
const scheduleProduction = () => {
const lastUpdate = GM_getValue(STORAGE_KEYS.PRODUCTION_LAST_UPDATE, Date.now());
const nextUpdate = getNextUpdate(lastUpdate);
const currentTime = Date.now();
const delay = Math.max(nextUpdate - currentTime, 0);
clearTimeout(productionTimeout);
productionTimeout = setTimeout(handleProduction, delay);
};
const parseProductionSection = (possibleSections, items) => {
const parsedProductions = {};
for (const itemName of items) {
const itemDetails = inventoryCache[itemNameIdMap.get(itemName)];
if (!itemDetails) continue;
const sections = Array.from(possibleSections);
const targetSection = sections.find((section) => {
var _a2;
return ((_a2 = section.querySelector("img.itemimg")) == null ? void 0 : _a2.src) === itemDetails.image;
});
if (!targetSection) continue;
const addButton = targetSection.querySelector(".item-after > button.button");
if (!addButton) continue;
parsedProductions[itemName] = Number(addButton.dataset.current);
}
return parsedProductions;
};
const parseProductionChildren = (children) => {
const output = {};
for (const child of children) {
if (child.nodeType !== 3) continue;
const productionText = child.textContent.trim();
const spaceIndex = productionText.indexOf(" ");
if (spaceIndex === -1) continue;
const itemName = productionText.slice(spaceIndex + 1).trim();
const countString = productionText.slice(0, spaceIndex);
const itemCount = parseNumberWithCommas(countString);
if (Number.isNaN(itemCount)) {
continue;
}
output[itemName] = itemCount;
}
return output;
};
const parseProductionRows = (parsedResponse) => {
var _a2;
const sections = Array.from(parsedResponse.querySelectorAll(".content-block-title"));
const targetSection = sections.find((section) => section.innerText === "Around Your Farm");
if (!targetSection) return;
let parsedProductionMap = {};
const buildingLinks = (_a2 = targetSection.nextElementSibling) == null ? void 0 : _a2.querySelectorAll(".item-link");
for (const buildingLink of buildingLinks) {
const detailsElement = buildingLink.querySelector(".item-after > span");
if (!detailsElement) continue;
const buildingProduction = parseProductionChildren(detailsElement.childNodes);
parsedProductionMap = { ...parsedProductionMap, ...buildingProduction };
}
const updatedProduction = { ...production };
for (const [itemName, key] of Object.entries(farmProductionKeys)) {
updatedProduction[itemName] = parsedProductionMap[key] ?? 0;
}
GM_setValue(STORAGE_KEYS.PRODUCTION, updatedProduction);
};
const updateCropCount = (event) => {
var _a2, _b;
const selectElement = event.target;
const targetElement = selectElement.parentElement.parentElement.firstElementChild;
const seedId = selectElement.value;
const cropName = (_a2 = selectElement.selectedOptions[0].dataset.name) == null ? void 0 : _a2.slice(0, -6);
if (!cropName) {
targetElement.innerText = "No crop selected";
return;
}
const cropId = seedCrop[seedId] || null;
const cropInventory = cropId === null ? "??" : ((_b = inventoryCache[cropId]) == null ? void 0 : _b.count) ?? "??";
targetElement.innerText = `${cropInventory} ${cropName} in inventory`;
};
unsafeWindow.updateCropCount = updateCropCount;
const parseFarm = (response) => {
const parsedResponse = parseHtml(response);
const cropSelect = parsedResponse.querySelector("select.seedid");
cropSelect.setAttribute("onchange", "updateCropCount(event)");
updateCropCount({ target: cropSelect });
parseProductionRows(parsedResponse);
return parsedResponse.innerHTML;
};
const xfarmListener = {
name: "Farm",
callback: parseFarm,
urlMatch: [/^xfarm\.php\?id=/],
passive: false
};
const parseSawmill = (response) => {
const parsedResponse = parseHtml(response);
const sections = Array.from(parsedResponse.querySelectorAll("li > .item-content"));
const parsedProductions = parseProductionSection(sections, ["Wood", "Board", "Oak"]);
const updatedProduction = { ...production, ...parsedProductions };
GM_setValue(STORAGE_KEYS.PRODUCTION, updatedProduction);
return response;
};
const sawmillListener = {
name: "Sawmill",
callback: parseSawmill,
urlMatch: [/^sawmill\.php/],
passive: true
};
const parseQuarry = (response) => {
const parsedResponse = parseHtml(response);
const sections = Array.from(parsedResponse.querySelectorAll("li > .item-content"));
const parsedProductions = parseProductionSection(sections, ["Stone", "Coal"]);
if (parsedProductions["Stone"]) {
parsedProductions["Sandstone"] = parsedProductions["Stone"];
}
const updatedProduction = { ...production, ...parsedProductions };
GM_setValue(STORAGE_KEYS.PRODUCTION, updatedProduction);
return response;
};
const quarryListener = {
name: "Quarry",
callback: parseQuarry,
urlMatch: [/^quarry\.php/],
passive: true
};
const parseHayfield = (response) => {
const parsedResponse = parseHtml(response);
const sections = Array.from(parsedResponse.querySelectorAll("li > .item-content"));
const parsedProductions = parseProductionSection(sections, ["Straw"]);
const updatedProduction = { ...production, ...parsedProductions };
GM_setValue(STORAGE_KEYS.PRODUCTION, updatedProduction);
return response;
};
const hayfieldListener = {
name: "Hayfield",
callback: parseHayfield,
urlMatch: [/^hayfield\.php/],
passive: true
};
const parseSteelworks = (response) => {
const parsedResponse = parseHtml(response);
const sections = Array.from(parsedResponse.querySelectorAll("li > .item-content"));
const parsedProductions = parseProductionSection(sections, ["Steel"]);
if (parsedProductions["Steel"]) {
parsedProductions["Steel Wire"] = Math.round(parsedProductions["Steel"] * 1 / 3);
}
const updatedProduction = { ...production, ...parsedProductions };
GM_setValue(STORAGE_KEYS.PRODUCTION, updatedProduction);
return response;
};
const steelworksListener = {
name: "Steelworks",
callback: parseSteelworks,
urlMatch: [/^steelworks\.php/],
passive: true
};
const parseTroutFarm = (response) => {
const parsedResponse = parseHtml(response);
const sections = Array.from(parsedResponse.querySelectorAll("li > .item-content"));
const parsedProductions = parseProductionSection(sections, ["Grubs", "Minnows"]);
const updatedProduction = { ...production, ...parsedProductions };
GM_setValue(STORAGE_KEYS.PRODUCTION, updatedProduction);
return response;
};
const troutFarmListener = {
name: "Trout Farm",
callback: parseTroutFarm,
urlMatch: [/^troutfarm\.php/],
passive: true
};
const parseWormHabitat = (response) => {
const parsedResponse = parseHtml(response);
const sections = Array.from(parsedResponse.querySelectorAll("li > .item-content"));
const parsedProductions = parseProductionSection(sections, ["Worms", "Gummy Worms", "Mealworms"]);
const updatedProduction = { ...production, ...parsedProductions };
GM_setValue(STORAGE_KEYS.PRODUCTION, updatedProduction);
return response;
};
const wormHabitatListener = {
name: "Worm Habitat",
callback: parseWormHabitat,
urlMatch: [/^hab\.php/],
passive: true
};
const parseQuest = (response, url) => {
const urlParameters = new URLSearchParams(url.split("?")[1]);
const questId = urlParameters.get("id");
const parsedResponse = parseHtml(response);
const questDetails = {};
const updateBatch = {};
const sections = parsedResponse.querySelectorAll(".content-block-title");
for (const section of sections) {
const sectionTitle = section.innerText;
if (!["Items Requested", "Rewards"].includes(sectionTitle)) continue;
const sectionItems = section.nextElementSibling.querySelectorAll("a.item-link");
const sectionDetails = {};
for (const sectionItem of sectionItems) {
const urlMatch2 = new URLSearchParams(sectionItem.href.split("?")[1]);
const itemId = urlMatch2.get("id");
const itemName = sectionItem.querySelector(".item-title > strong").innerText.trim();
const itemImage = sectionItem.querySelector("img.itemimg").src;
const itemRequirementText = sectionItem.querySelector(".item-after").innerText;
const itemRequirement = parseNumberWithCommas(itemRequirementText.replace("x", "").trim());
const itemDetails = { id: itemId, name: itemName, image: itemImage };
const progressbar = sectionItem.querySelector(".progressbar");
if (progressbar) {
const quantityElement = progressbar.previousElementSibling;
const quantityWords = quantityElement.textContent.trim().split(" ");
const quantity = parseNumberWithCommas(quantityWords[quantityWords.length - 1]);
itemDetails.count = quantity;
}
updateBatch[itemId] = itemDetails;
sectionDetails[itemName] = itemRequirement;
}
const key = sectionTitle === "Items Requested" ? "request" : "reward";
questDetails[key] = sectionDetails;
}
const updatedQuests = { ...quests, [questId]: questDetails };
const pjButton = parsedResponse.querySelector(".drinkpj");
if (pjButton) {
const pjDetails = inventoryCache[itemNameIdMap.get("Peach Juice")];
if (pjDetails) {
const pjText = pjButton.innerText;
const pjCount = parseNumberWithCommas(pjText.split("(")[1].trim().slice(0, -1));
pjDetails.count = pjCount;
updateBatch[pjDetails.id] = pjDetails;
}
}
updateInventory(updateBatch, { isDetailed: true });
GM_setValue(STORAGE_KEYS.QUESTS, updatedQuests);
if (questDetails.reward) {
const rewardItems = Object.keys(questDetails.reward).filter((itemName) => itemNameIdMap.has(itemName)).map((itemName) => inventoryCache[itemNameIdMap.get(itemName)]).filter((item) => item);
if (rewardItems.length > 0) {
setHudDetails(rewardItems, url);
}
}
};
const questListener = {
name: "Quest",
callback: parseQuest,
urlMatch: [/^quest\.php/],
passive: true
};
const getQuestId = (questUrl) => {
const urlParameters = new URLSearchParams(questUrl.split("?")[1]);
return urlParameters.get("id");
};
const parseQuests = (response) => {
const parsedResponse = parseHtml(response);
const currentQuests = parsedResponse.querySelectorAll('a.item-link[href^="quest.php"]');
const questIds = Array.from(currentQuests).map((quest) => getQuestId(quest.href));
const updatedQuests = { ...quests };
for (const questId of Object.keys(quests)) {
if (!questIds.includes(questId)) {
delete updatedQuests[questId];
}
}
GM_setValue(STORAGE_KEYS.QUESTS, updatedQuests);
};
const questsListener = {
name: "Quests",
callback: parseQuests,
urlMatch: [/^quests\.php/],
passive: true
};
const parseMailbox = (response, url) => {
var _a2;
const parsedResponse = parseHtml(response);
const mailboxBlock = parsedResponse.querySelector(".mailboxcb");
if (!mailboxBlock) return response;
const mailItems = mailboxBlock.querySelectorAll(".list-block .item-content");
const updatedInventory = {};
for (const item of mailItems) {
const link = item.querySelector('a[href^="item.php"]');
if (!link) continue;
const itemId = new URLSearchParams(link.href.split("?")[1]).get("id");
const count = item.querySelector("input.qty").dataset.numLeft;
updatedInventory[itemId] = parseNumberWithCommas(count);
}
updateInventory(updatedInventory, { isDetailed: false, isAbsolute: true });
const playerId = new URLSearchParams(url.split("?")[1]).get("id");
const townsfolkName = Object.keys(townsfolk).find((name) => townsfolk[name] === playerId);
if (!townsfolkName) return response;
const sectionTitles = mailboxBlock.querySelectorAll(".content-block-title");
const sections = Array.from(sectionTitles);
if (sections.some((section) => section.textContent.toLowerCase().includes("discovered"))) {
const discoveredSection = sections.find((section) => section.textContent.toLowerCase().includes("discovered"));
if (!discoveredSection) return response;
const discoveredGifts = {};
const discoveredItems = discoveredSection.nextElementSibling.querySelectorAll(".item-content");
for (const item of discoveredItems) {
const link = item.querySelector('a[href^="item.php"]');
if (!link) continue;
const itemId = new URLSearchParams(link.href.split("?")[1]).get("id");
let statusImage = (_a2 = item.querySelector(".item-title > strong > img")) == null ? void 0 : _a2.src;
if (!statusImage) continue;
let status = null;
if (statusImage.includes("love")) {
status = "love";
} else if (statusImage.includes("like")) {
status = "like";
}
if (!status) continue;
discoveredGifts[itemId] = status;
}
addTownsfolkGifts(townsfolkName, discoveredGifts);
}
return response;
};
const mailboxListener = {
name: "Mailbox",
callback: parseMailbox,
urlMatch: [/^mailbox\.php/],
passive: true
};
const parseCropTile = (crop) => {
const anchor = crop.firstElementChild;
const id = anchor.href.split("?id=")[1];
const image = anchor.firstElementChild.src;
const nameSpan = crop.querySelector("span:nth-of-type(1)");
const name = nameSpan.innerText.trim();
const countSpan = crop.querySelector("span:nth-of-type(2)");
const countText = countSpan.innerText.split("/")[0].trim();
const count = parseNumberWithCommas(countText);
return { id, name, image, count };
};
const parseHarvestLog = (harvest) => {
const anchor = harvest.querySelector(".item-media > a");
const id = anchor.href.split("?id=")[1];
const time = harvest.querySelector(".item-title > span").innerText.trim();
const date = time.split(" ")[0];
const harvestCountText = harvest.querySelector(".item-after").innerText.trim();
const count = parseNumberWithCommas(harvestCountText);
return { id, date, count };
};
const getCropStatsTile = (id, name, image, count) => {
return `
<div class="col-25">
<a href="item.php?id=${id}">
<img src="${image}" class="itemimg" />
</a>
<br />
<span style="font-weight: bold">${name}</span>
<br />
<span style="font-size: 11px">${getFormattedNumber(count)}</span>
</div>
`;
};
const getCropStatsRow = (content) => {
return `
<div class="row no-gutter" style="margin-bottom:15px">
${content.join("")}
</div>
`;
};
const getCropStatsTab = (id, content, active) => {
return `
<div id="${id}" class="tab ${active ? "active" : ""}">
<div class="content-block">
<div class="card">
<div class="card-content">
<div class="card-content-inner">
${content.join("")}
</div>
</div>
</div>
</div>
</div>
`;
};
const getHarvestStatsCard = (harvestedItems, tabPrefix = "tab1") => {
const tabs = [];
const tabButtons = [];
let firstTab = true;
for (const [date, harvests] of Object.entries(harvestedItems)) {
const rows = [];
const dateEntries = Object.entries(harvests).sort((a, b) => a[1] < b[1]);
for (let i = 0; i < dateEntries.length; i += 4) {
const rowItems = dateEntries.slice(i, i + 4);
const rowContent = [];
for (const [itemId, harvestCount] of rowItems) {
const item = inventoryCache[itemId];
const itemHtml = getCropStatsTile(itemId, item.name, item.image, harvestCount);
rowContent.push(itemHtml);
}
const rowHtml = getCropStatsRow(rowContent);
rows.push(rowHtml);
}
const tabId = `${tabPrefix}-harvesttotals-${date.replaceAll("-", "").trim()}`;
const tabHtml = getCropStatsTab(tabId, rows, firstTab);
tabs.push(tabHtml);
const buttonHtml = `<a href="#${tabId}" class="tab-link button ${firstTab ? "active" : ""}">${date}</a>`;
tabButtons.push(buttonHtml);
firstTab = false;
}
const cardHtml = `
<div class="card">
<div class="card-content">
<div class="content-block">
<div class="buttons-row">
${tabButtons.join("")}
</div>
</div>
<div class="tabs">
${tabs.join("")}
</div>
</div>
</div>
`;
return cardHtml;
};
const parseFarmInfo = (response) => {
var _a2;
const parsed = parseHtml(response);
const cropTab = parsed.querySelector(".page-content > .content-block > .tabs > .tab");
if (cropTab) {
const crops = cropTab.querySelectorAll(".card-content-inner .col-25:has(a)");
const updatedBatch = {};
for (const crop of crops) {
const item = parseCropTile(crop);
updatedBatch[item.id] = item;
}
updateInventory(updatedBatch, { isDetailed: true });
}
const harvestLog = parsed.querySelector("#harvestlog");
const logsContainer = (_a2 = harvestLog == null ? void 0 : harvestLog.nextElementSibling) == null ? void 0 : _a2.nextElementSibling;
if (logsContainer) {
const harvests = logsContainer.querySelectorAll("ul > li.close-panel > .item-content");
const harvestedItems = {};
for (const harvest of harvests) {
const log = parseHarvestLog(harvest);
if (!harvestedItems[log.date]) {
harvestedItems[log.date] = {};
}
if (!harvestedItems[log.date][log.id]) {
harvestedItems[log.date][log.id] = 0;
}
harvestedItems[log.date][log.id] += log.count;
}
if (Object.keys(harvestedItems).length > 0) {
const defaultActiveTab = parsed.querySelector(".tabs > .tab.active").id;
const statsCard = getHarvestStatsCard(harvestedItems, defaultActiveTab);
harvestLog.insertAdjacentHTML("afterend", statsCard);
}
}
return parsed.innerHTML;
};
const farmInfoListener = {
name: "Farm Info",
callback: parseFarmInfo,
urlMatch: [/^farminfo\.php/],
passive: false
};
const interceptXHR = (handler) => {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this._interceptedUrl = url;
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
this.addEventListener("readystatechange", () => {
if (this.readyState === 4 && this.status === 200) {
const originalResponse = this.responseText;
const modified = handler(originalResponse, this._interceptedUrl, "ajax");
Object.defineProperty(this, "responseText", {
get: () => modified
});
}
}, false);
return originalSend.apply(this, arguments);
};
};
const interceptFetch = (handler) => {
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = (input, init) => {
const req = new Request(input, init);
return originalFetch(input, init).then((response) => {
const cloned = response.clone();
return cloned.text().then((text) => {
const modifiedText = handler(text, req.url, "fetch");
return new Response(modifiedText, {
status: cloned.status,
statusText: cloned.statusText,
headers: cloned.headers
});
});
});
};
};
const showLoadout = (loadout) => {
if (hudStash === null) {
const currentItemNames = hudItems.map((item) => item.name);
GM_setValue(STORAGE_KEYS.HUD_STASH, currentItemNames);
}
setHudItemsByName(
loadout.items,
loadout.displayMode ?? HUD_DISPLAY_MODES.INVENTORY
);
if (!hudStatus) toggleHudStatus();
};
const showLoadouts = () => {
const loadoutActions = [
{
text: "Change script settings",
onClick: showSettings
},
{
text: `${editMode ? "Disable" : "Enable"} edit mode`,
onClick: () => setEditMode(!editMode)
},
{
text: "Select the loadout to activate",
label: true
},
...Object.keys(loadouts).map((loadoutName) => {
return {
text: loadoutName,
onClick: () => showLoadout(loadouts[loadoutName])
};
}),
{
text: "Craftworks",
onClick: () => showLoadout({ items: craftworks.map((entry) => inventoryCache[entry.item].name) })
},
{
text: "Cancel",
color: "red",
onClick: refreshInventory
}
];
myApp.actions(loadoutActions);
};
const preventDefaultContextMenu = () => {
document.addEventListener("contextmenu", function(e) {
if (e.target.id === "frpg-hud-toggle") e.preventDefault();
if (e.target.closest("a.frpg-hud-item")) e.preventDefault();
if (e.target.id === "frpg-hud-button") e.preventDefault();
}, false);
};
const setupDOMContentLoadedHandlers = () => {
document.addEventListener("DOMContentLoaded", () => {
setupAuxClickHandler();
setupTouchHandlers();
});
};
const setupAuxClickHandler = () => {
document.body.addEventListener("auxclick", (event) => {
if (event.target.id === "frpg-hud-toggle") {
event.preventDefault();
event.stopPropagation();
return showLoadouts();
}
if (event.target.id === "frpg-hud-button") {
event.preventDefault();
event.stopPropagation();
return toggleHudButton();
}
const target = event.target.closest("a.frpg-hud-item");
if (!target) return;
event.preventDefault();
event.stopPropagation();
handleQuickAction(target);
});
};
const setupTouchHandlers = () => {
let quickActionTimeout;
let animationElement;
const clearAnimation = () => {
if (animationElement) {
animationElement.classList.remove("active");
animationElement = null;
}
};
document.body.addEventListener("touchstart", (event) => {
if (event.target.id === "frpg-hud-toggle") {
clearTimeout(quickActionTimeout);
quickActionTimeout = setTimeout(() => {
showLoadouts();
}, 500);
return;
}
if (event.target.id === "frpg-hud-button") {
clearTimeout(quickActionTimeout);
quickActionTimeout = setTimeout(() => {
toggleHudButton();
}, 500);
return;
}
const target = event.target.closest("a.frpg-hud-item");
if (!target) return;
animationElement = target.querySelector(".fill-animation");
if (animationElement) animationElement.classList.add("active");
clearTimeout(quickActionTimeout);
quickActionTimeout = setTimeout(() => {
clearAnimation();
handleQuickAction(target);
}, 500);
}, { passive: true });
const cancelTouch = () => {
if (quickActionTimeout) {
clearTimeout(quickActionTimeout);
quickActionTimeout = null;
}
clearAnimation();
};
document.body.addEventListener("touchend", cancelTouch);
document.body.addEventListener("touchmove", cancelTouch);
};
const addScriptStyles = () => {
GM_addStyle(`
.frpg-hud-item .fill-animation.active {
animation: fillUp 500ms forwards;
}
@keyframes fillUp {
from { width: 0; }
to { width: 95%; }
}
#statszone_tracker {
position: unset !important;
}
`);
};
const setupVisibilityChangeListener = () => {
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
updateHudDisplay(true);
}
});
};
const setupEventListeners = () => {
addScriptStyles();
preventDefaultContextMenu();
setupDOMContentLoadedHandlers();
setupVisibilityChangeListener();
};
const storageListeners = {
[STORAGE_KEYS.HUD_URL]: (value) => {
setHudUrl(value);
return true;
},
[STORAGE_KEYS.HUD_ITEMS]: (value) => {
setHudItems(value);
return true;
},
[STORAGE_KEYS.HUD_TIMERS]: handleHudTimerUpdate,
[STORAGE_KEYS.HUD_STATUS]: (value) => {
setHudStatus(value);
return true;
},
[STORAGE_KEYS.HUD_STASH]: (value) => {
setHudStash(value);
return true;
},
[STORAGE_KEYS.HUD_BUTTON]: (value) => {
setHudButton(value);
return true;
},
[STORAGE_KEYS.INVENTORY]: (value) => {
setInventory(value);
return true;
},
[STORAGE_KEYS.INVENTORY_LIMIT]: (value) => {
setInventoryLimit(value);
return true;
},
[STORAGE_KEYS.SETTINGS]: (value) => {
setSettings(value);
return true;
},
[STORAGE_KEYS.QUICK_ACTIONS]: (value) => {
setQuickActions(value);
return false;
},
[STORAGE_KEYS.TOWNSFOLK]: (value) => {
setTownsfolk(value);
return false;
},
[STORAGE_KEYS.TOWNSFOLK_GIFTS]: (value) => {
setTownsfolkGifts(value);
return false;
},
[STORAGE_KEYS.RECIPES]: (value) => {
setRecipes(value);
return false;
},
[STORAGE_KEYS.RETURN_RATE]: (value) => {
setReturnRate(value);
return false;
},
[STORAGE_KEYS.SUPPLY_PACKS]: (value) => {
setSupplyPacks(value);
return false;
},
[STORAGE_KEYS.NEW_ITEM]: () => {
populateItemNameIdMap();
return false;
},
[STORAGE_KEYS.CRAFTWORKS]: (value) => {
setCraftworks(value);
return false;
},
[STORAGE_KEYS.PRODUCTION]: (value) => {
setProduction(value);
return false;
},
[STORAGE_KEYS.QUESTS]: (value) => {
setQuests(value);
return false;
}
};
const setupStorageListeners = () => {
for (const [key, handler] of Object.entries(storageListeners)) {
GM_addValueChangeListener(key, (k, _oldVal, newVal) => {
if (handler(newVal)) updateHudDisplay(k === STORAGE_KEYS.HUD_STATUS);
});
}
};
const listeners = [
workerListener,
hudListener,
xfarmListener,
areaListener,
homeListener,
itemListener,
inventoryListener,
workshopListener,
locationListener,
craftworksListener,
locksmithListener,
spinListener,
townsfolkListener,
sawmillListener,
quarryListener,
hayfieldListener,
steelworksListener,
troutFarmListener,
wormHabitatListener,
questListener,
questsListener,
mailboxListener,
farmInfoListener
];
const responseHandler = (response, url, type) => {
for (const listener of listeners) {
for (const regex of listener.urlMatch) {
if (!regex.test(url)) continue;
if (listener.passive) {
setTimeout(listener.callback, null, response, url, type);
return response;
}
try {
const modifiedResponse = listener.callback(response, url, type);
return modifiedResponse ?? response;
} catch (error) {
console.error("Error while calling callback for", listener, error);
return response;
}
}
}
return response;
};
scheduleProduction();
setupEventListeners();
setupStorageListeners();
interceptXHR(responseHandler);
interceptFetch(responseHandler);
})();