FRPG HUD

Live inventory monitoring, meal timers and more!

// ==UserScript==
// @name         FRPG HUD
// @namespace    AppleBottomJeans.FRPG.HUD
// @version      2025-06-16
// @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",
    HUD_STATUS: "frpg.hud-status",
    HUD_ITEMS: "frpg.hud-items",
    HUD_URL: "frpg.hud-url",
    HUD_TIMERS: "frpg.hud-timers",
    SUPPLY_PACKS: "frpg.supply-packs",
    NEW_ITEM: "frpg.new-item",
    CRAFTWORKS: "frpg.craftworks",
    SETTINGS: "frpg.settings"
  };
  const HUD_DISPLAY_MODES = {
    INVENTORY: "INVENTORY",
    MEAL: "MEAL",
    TIMER: "TIMER"
  };
  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
  };
  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
    }
  };
  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 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();
  let craftworks = GM_getValue(STORAGE_KEYS.CRAFTWORKS, []);
  const setCraftworks = (value) => {
    craftworks = value;
    generateDependencies();
  };
  const generateDependencies = () => {
    craftworksDependencies.clear();
    for (const { item, enabled } of craftworks) {
      if (!enabled) continue;
      craftworksDependencies.add(item);
      const itemName = inventoryCache[item].name;
      const recipe = recipes[itemName];
      if (!recipe) continue;
      Object.keys(recipe).forEach((materialName) => craftworksDependencies.add(itemNameIdMap.get(materialName)));
    }
  };
  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 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 hudTimerInterval = null;
  const handleHudTimerUpdate = (value) => {
    hudTimers = value;
    clearInterval(hudTimerInterval);
    if (Object.keys(value).length > 0 && settings.mealTimersEnabled) {
      hudTimerInterval = setInterval(updateHudDisplay, 1e3);
    }
    return true;
  };
  setTimeout(() => handleHudTimerUpdate(hudTimers));
  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 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 hudHtmlCallbacks = {
    [HUD_DISPLAY_MODES.INVENTORY]: ({ image, count }) => {
      let textColour = getDefaultTextColor();
      if (count >= inventoryLimit) textColour = "red";
      else if (count >= inventoryLimit * 0.8) {
        const percent = count / inventoryLimit;
        const green = Math.round(255 - (percent - 0.8) / 0.2 * (255 - 100));
        textColour = `rgb(255, ${green}, 50);`;
      }
      return `
            <span style="color: ${textColour};">
                <img src="${image}" class="itemimgmini" style="width:15px; vertical-align:middle; padding-right:1px">
                ${getFormattedNumber(count)} / ${getFormattedNumber(inventoryLimit)} &nbsp;
            </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)}) &nbsp;
            </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} &nbsp;
            </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;
  };
  unsafeWindow.refreshInventory = refreshInventory;
  const getHudHtml = () => {
    const hudHasItems = hudItems.length > 0;
    const timersCount = Object.keys(hudTimers).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(hudTimers).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 />");
    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 href="explore.php" class="button" style="margin-left: 2%; height: 22px; line-height: 20px; width: 42%;">Explore</a>
                    <a href="${hudUrl}" class="button" style="margin-left: 2%; height: 22px; line-height: 20px; white-space: nowrap;">C</a>
                </div>`;
    hudHtml += `</div>`;
    return hudHtml;
  };
  const filterTimers = () => {
    const filteredTimers = Object.fromEntries(
      Object.entries(hudTimers).filter(([, timer]) => Date.now() - timer <= 30 * 1e3)
    );
    if (Object.keys(filteredTimers).length !== Object.keys(hudTimers).length) {
      GM_setValue(STORAGE_KEYS.HUD_TIMERS, filteredTimers);
    }
  };
  const _updateHudDisplay = (forceUpdate = false) => {
    if (document.hidden && !forceUpdate) return;
    const parentElement = document.querySelector("#statszone");
    if (!parentElement) return;
    if (!hudStatus) {
      if (forceUpdate) parentElement.innerHTML = statsHtml;
      return;
    }
    const hudElement = getHudHtml();
    parentElement.innerHTML = hudElement;
    filterTimers();
  };
  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) => {
    var _a2;
    const applicableInventory = {};
    const bypassReserve = quickActions[triggerItem].bypassReserve ?? false;
    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 || !bypassReserve) {
        applicableInventory[materialId].count = Math.max(0, itemCount - (((_a2 = quickActions[materialName]) == null ? void 0 : _a2.reserve) ?? globalReserve));
      }
    }
    return applicableInventory;
  };
  const handleItemCraft$1 = (itemName, 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);
    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);
  };
  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();
    });
  };
  let townsfolk = GM_getValue(STORAGE_KEYS.TOWNSFOLK, {});
  const setTownsfolk = (newTownsfolk) => townsfolk = newTownsfolk;
  const parseQuickSend = (panelRows) => {
    const quickGiveRow = panelRows.find((row) => row.innerHTML.includes("npclevels.php"));
    if (!quickGiveRow) return;
    const updatedTownsfolk = {};
    const options = quickGiveRow.querySelector(".quickgivedd").options;
    Array.from(options).slice(1).forEach((opt) => {
      const name = opt.innerText.split("(")[0].trim();
      updatedTownsfolk[name] = opt.value;
    });
    GM_setValue(STORAGE_KEYS.TOWNSFOLK, updatedTownsfolk);
  };
  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 confirmSell = (itemName, count, target, cleanup) => {
    const confirmationTitle = `Sell ${getFormattedNumber(count)}x ${itemName}?`;
    const confirmationSubtitle = `Global reserve value of ${getFormattedNumber(getGlobalReserveAmount())} applied`;
    const callbackAccept = () => {
      updateQuickAction(itemName, { action: "sell" });
      handleQuickAction(target);
    };
    myApp.confirm(confirmationSubtitle, confirmationTitle, callbackAccept, refreshInventory);
    return cleanup(false);
  };
  const handleQuickAction = (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);
    const itemAction = quickActions[itemName];
    const applicableCount = itemCount - ((itemAction == null ? void 0 : itemAction.reserve) ?? getGlobalReserveAmount());
    const targetStyle = target.firstElementChild.style;
    const cleanup = getCleanupCallback(target);
    if (applicableCount <= 0 || (itemAction == null ? void 0 : itemAction.action) === "none") return cleanup(false) && refreshInventory();
    if (itemAction == null ? void 0 : itemAction.action) {
      const action = itemAction.action;
      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, itemAction, cleanup);
      } else if (action === "use") {
        return handleItemUse(itemName, applicableCount, cleanup);
      } else if (action === "sell") {
        return handleItemSell(itemId, itemName, applicableCount, cleanup);
      }
      return;
    }
    if (unsellableItems.includes(itemName)) {
      myApp.addNotification({ title: "Cannot sell this item", subtitle: "Please sell it manually" });
    }
    return confirmSell(itemName, applicableCount, target, cleanup);
  };
  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;
  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) => {
    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 });
    }
  };
  const updateInventory = (updateBatch, { isAbsolute = false, isDetailed = false, resolveNames = false, overwriteMissing = false, processCraftworks = false }) => {
    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);
    }
    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);
    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 });
  };
  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();
      let itemCount = updateBatch[itemName] ?? 0;
      if (itemImage.style.filter.includes("grayscale")) {
        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 count = extractNumber(parsedResponse.querySelector(selector));
      const difference = count - inventoryCache[itemNameIdMap.get(itemName)].count;
      updateBatch[itemName] = difference;
    };
    if (ciderUsed || lemonadeUsed) {
      const itemUsed = ciderUsed ? "Apple Cider" : parsedResponse.querySelector("#lmtyp").innerText.trim();
      const countSelector = ciderUsed ? "#cidercnt" : "#lmcnt";
      updateItemDifference(itemUsed, countSelector);
    }
    updateItemDifference("Apple", "#applecnt");
    updateInventory(updateBatch, { isAbsolute: false, resolveNames: true, processCraftworks: true });
  };
  const explorationWorkers = [
    {
      action: "drinklm",
      listener: handleExploration
    },
    {
      action: "explore",
      listener: handleExploration
    }
  ];
  const handleFarmHarvest = (response) => {
    const parsedResponse = JSON.parse(response);
    const updatedInventory = {};
    for (const cropId of Object.keys(parsedResponse.drops)) {
      updatedInventory[cropId] = parsedResponse.drops[cropId].qty;
    }
    updateInventory(updatedInventory, { isAbsolute: false });
    try {
      unsafeWindow.updateCropCount({ target: document.querySelector(".seedid") });
    } catch (error) {
      console.log("Error while updating crop counts", error);
    }
  };
  const farmWorkers = [
    {
      action: "harvestall",
      listener: handleFarmHarvest
    }
  ];
  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) => {
    if (response !== "success") return;
    const itemId = parameters.get("id");
    const itemCount = parameters.get("qty");
    updateInventory({ [itemId]: -itemCount }, { isAbsolute: false });
  };
  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;
    hudTimers[itemId] = endTime;
    GM_setValue(STORAGE_KEYS.HUD_TIMERS, updatedTimers);
    updateInventory({ [itemId]: -1 }, { isAbsolute: false });
  };
  const handleLocksmithOpen = (response) => {
    const parsedResponse = parseHtml(response);
    const updatedInventory = {};
    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);
      updatedInventory[itemName] = itemCount;
    }
    updateInventory(updatedInventory, { isAbsolute: false, resolveNames: true, processCraftworks: true });
  };
  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 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 })
    }
  ];
  const handleWheelSpin = (response) => {
    const parsedResponse = parseHtml(response.split("|")[1]);
    const rewardText = parsedResponse.innerText.split(":")[1];
    let [itemName, countText] = rewardText.split("(x");
    itemName = itemName.trim();
    if (itemName === "Apples") itemName = "Apple";
    const itemCount = Number(countText.split(")")[0]);
    updateInventory({ [itemName]: itemCount }, { isAbsolute: false, resolveNames: true });
  };
  const miscWorkers = [
    {
      action: "spinfirst",
      listener: handleWheelSpin
    }
  ];
  const workers = [
    ...explorationWorkers,
    ...fishingWorkers,
    ...itemSellWorkers,
    ...craftingWorkers,
    ...itemUseWorkers,
    ...itemSendWorkers,
    ...miscWorkers,
    ...farmWorkers
  ];
  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);
    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, updatedTimers);
    return response;
  };
  const homeListener = {
    name: "Home Page",
    callback: parseHome,
    urlMatch: [/^index\.php/],
    passive: true
  };
  const explorationHud = (response) => {
    const parsedHud = parseHtml(response);
    const statsDiv = parsedHud.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.querySelector("hr");
    if (hrElement) {
      hrElement.insertAdjacentHTML("beforeBegin", toggleHtml);
    } else {
      statsDiv.insertAdjacentHTML("beforeEnd", toggleHtml);
    }
    setStatsData(Array.from(parsedHud.firstElementChild.children).filter((element) => element.tagName === "SPAN").slice(0, 4).map((i) => i.innerHTML));
    setStatsHtml(parsedHud.innerHTML);
    if (hudStatus) {
      return getHudHtml();
    }
    return parsedHud.innerHTML;
  };
  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;
    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;
      if (!selectedTownsfolk) {
        const quickGiveSelector = (_b = reserveRow.closest("ul")) == null ? void 0 : _b.querySelector(".quickgivedd");
        const selectedOption = quickGiveSelector == null ? void 0 : quickGiveSelector.selectedOptions[0];
        if (!selectedOption || selectedOption.innerText.startsWith("-")) {
          selectedTownsfolk = Object.keys(townsfolk)[0];
        } else {
          selectedTownsfolk = selectedOption.innerText.split("(")[0].trim();
        }
      }
      const townsfolkOptions = Object.keys(townsfolk).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);
  };
  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.trim().slice(0, -1));
      if (name === "Gold") continue;
      supplyPackItems[name] = count;
      updatedInventory[itemId] = { id: itemId, name, image };
    }
    updateInventory(updatedInventory, { isDetailed: true });
    const updatedSupplyPacks = { ...supplyPacks, [itemName]: supplyPackItems };
    GM_setValue(STORAGE_KEYS.SUPPLY_PACKS, updatedSupplyPacks);
  };
  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) => panelRows.some((row) => row.innerHTML.includes("/img/items/icon_mail.png?")) && Object.keys(townsfolk).length !== 0;
  const detectCraftableItem = (titleRows) => titleRows.some((row) => row.innerText.trim().toLowerCase() === "crafting use");
  const detectUsableItem = (itemName) => mealNames.has(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 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 }
    };
    updateInventory({ [itemId]: itemDetails }, { 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 supplyPackDetails = supplyPacks[supplyPackName];
    if (!supplyPackDetails) return;
    setHudItemsByName(Object.keys(supplyPackDetails));
  };
  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)");
    }
    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"];
    }
    GM_setValue(STORAGE_KEYS.TOWNSFOLK, updatedTownsfolk);
    return response;
  };
  const townsfolkListener = {
    name: "Townsfolk",
    callback: parseTownsfolk,
    urlMatch: [/^npclevels\.php/],
    passive: true
  };
  const wheelSpin = (response) => {
    setHudItemsByName(wheelItems);
    const parsedResponse = parseHtml(response);
    const spinElement = parsedResponse.querySelector(`.card-content-inner > a[href="wheelhistory.php"]`).parentElement;
    const spinCount = Number(spinElement.children[3].innerText);
    const spinCost = 5 / 2 * spinCount * Math.max(0, spinCount - 1);
    spinElement.innerHTML = spinElement.innerHTML.replace("</strong> time(s)", `</strong> time(s) for a total of <strong>${getFormattedNumber(spinCost)}</strong> coins`);
    return parsedResponse.innerHTML;
  };
  const spinListener = {
    name: "Wheel Spin",
    callback: wheelSpin,
    urlMatch: [/^spin\.php/]
  };
  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;
      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);
    return response;
  };
  const workshopListener = {
    name: "Workshop",
    callback: parseWorkshop,
    urlMatch: [/^workshop\.php$/],
    passive: true
  };
  const updateCropCount = (event) => {
    var _a2;
    const selectElement = event.target;
    const targetElement = selectElement.parentElement.parentElement.firstElementChild;
    const seedId = selectElement.value;
    const cropName = selectElement.selectedOptions[0].dataset.name.slice(0, -6);
    const cropId = seedCrop[seedId] || null;
    const cropInventory = cropId === null ? "??" : ((_a2 = inventoryCache[cropId]) == null ? void 0 : _a2.count) ?? "??";
    targetElement.innerText = `${cropInventory} ${cropName} in inventory`;
  };
  unsafeWindow.updateCropCount = updateCropCount;
  const cropCount = (response) => {
    const parsedResponse = parseHtml(response);
    const cropSelect = parsedResponse.querySelector("select.seedid");
    cropSelect.setAttribute("onchange", "updateCropCount(event)");
    updateCropCount({ target: cropSelect });
    return parsedResponse.innerHTML;
  };
  const xfarmListener = {
    name: "Crop Count",
    callback: cropCount,
    urlMatch: [/^xfarm\.php\?id=/],
    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 showLoadouts = () => {
    const loadoutActions = [
      {
        text: "Change script settings",
        onClick: showSettings
      },
      {
        text: "Select the loadout to activate",
        label: true
      },
      ...Object.keys(loadouts).map((loadoutName) => {
        return {
          text: loadoutName,
          onClick: () => {
            setHudItemsByName(
              loadouts[loadoutName].items,
              loadouts[loadoutName].displayMode ?? HUD_DISPLAY_MODES.INVENTORY
            );
            if (!hudStatus) toggleHudStatus();
          }
        };
      }),
      {
        text: "Craftworks",
        onClick: () => {
          if (craftworks.length === 0) return;
          setHudItemsByName(craftworks.map((entry) => inventoryCache[entry.item].name));
          if (!hudStatus) toggleHudStatus();
        }
      },
      {
        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();
    }, false);
  };
  const setupDOMContentLoadedHandlers = () => {
    document.addEventListener("DOMContentLoaded", () => {
      setupAuxClickHandler();
      setupTouchHandlers();
    });
  };
  const setupAuxClickHandler = () => {
    document.body.addEventListener("auxclick", (event) => {
      if (event.button !== 1) return;
      if (event.target.id === "frpg-hud-toggle") {
        event.preventDefault();
        event.stopPropagation();
        return showLoadouts();
      }
      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);
      }
      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 addAnimationStyle = () => {
    GM_addStyle(`
        .frpg-hud-item .fill-animation.active {
            animation: fillUp 500ms forwards;
        }
        
        @keyframes fillUp {
            from { width: 0; }
            to { width: 95%; }
        }
    `);
  };
  const setupVisibilityChangeListener = () => {
    document.addEventListener("visibilitychange", () => {
      if (!document.hidden) {
        updateHudDisplay(true);
      }
    });
  };
  const setupEventListeners = () => {
    addAnimationStyle();
    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.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.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;
    }
  };
  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
  ];
  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;
  };
  setupEventListeners();
  setupStorageListeners();
  interceptXHR(responseHandler);
  interceptFetch(responseHandler);
})();