@mwm/addon

Market addon using mwi-moonitoring library for WebSocket events

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name               @mwm/addon
// @name:en            MWI Market Addon
// @name:zh-CN         MWI 市场插件
// @namespace          https://milkyway.market/
// @version            1.5.1
// @author             mathewcst
// @description:en     Sync character data between MWI game and MilkyWay Market
// @description:zh-CN  同步 MWI 游戏和 MilkyWay Market 之间的角色数据
// @license            MIT
// @icon               https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com
// @match              https://www.milkywayidle.com/*
// @match              https://milkywayidle.com/*
// @match              https://test.milkywayidle.com/*
// @match              https://milkyway.market/*
// @match              https://www.milkyway.market/*
// @match              https://v2.milkyway.market/*
// @connect            api.milkyway.market
// @grant              GM_addStyle
// @grant              GM_addValueChangeListener
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_xmlhttpRequest
// @description Market addon using mwi-moonitoring library for WebSocket events
// ==/UserScript==

(function () {
  'use strict';

  const d=new Set;const importCSS = async e=>{d.has(e)||(d.add(e),(t=>{typeof GM_addStyle=="function"?GM_addStyle(t):(document.head||document.documentElement).appendChild(document.createElement("style")).append(t);})(e));};

  var _GM_addValueChangeListener = (() => typeof GM_addValueChangeListener != "undefined" ? GM_addValueChangeListener : void 0)();
  var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
  var _GM_xmlhttpRequest = (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();
  const version = "1.5.1";
  const VERSION = version;
  const STORAGE_KEY$1 = "@mwm/character-data";
  const SYNC_THROTTLE_MS = 3e4;
  const WS_ENDPOINTS = [
    "api.milkywayidle.com/ws",
    "api-test.milkywayidle.com/ws"
  ];
  const MARKET_API = "https://api.milkyway.market";
  const EVENTS = {
    REQUEST: "mwi-request-character-data",
    RESPONSE: "mwi-character-data-response",
    UPDATED: "mwi-character-data-updated",
    ADDON_READY: "mwm-addon-ready"
  };
  const isGameSite = location.hostname.includes("milkywayidle.com");
  const isMarketSite = location.hostname.includes("milkyway.market") || location.hostname.includes("localhost");
  const ADDON_COLOR = "#8B5CF6";
  const WARN_COLOR = "#F59E0B";
  const formatMessage = (msg) => {
    const prefix = `%c[MWM] [Addon v${VERSION}]%c`;
    const prefixStyle = `color: ${ADDON_COLOR}; font-weight: bold;`;
    const messageStyle = "color: inherit; padding-left: 4px;";
    return [`${prefix} ${msg}`, prefixStyle, messageStyle];
  };
  const formatWarnMessage = (msg) => {
    const prefix = `%c[MWM] [Addon v${VERSION}]%c`;
    const prefixStyle = `color: ${WARN_COLOR}; font-weight: bold;`;
    const messageStyle = "color: inherit; padding-left: 4px;";
    return [`${prefix} ${msg}`, prefixStyle, messageStyle];
  };
  const log = (msg, ...args) => {
    const [formatted, style1, style2] = formatMessage(msg);
    console.log(formatted, style1, style2, ...args);
  };
  const warn = (msg, ...args) => {
    const [formatted, style1, style2] = formatWarnMessage(msg);
    console.warn(formatted, style1, style2, ...args);
  };
  let lastSyncTime = 0;
  function mergeItems(target, newItems) {
    if (!newItems?.length) return;
    const itemMap = new Map();
    if (target.characterItems) {
      for (const item of target.characterItems) {
        const key = `${item.itemHrid}:${item.enhancementLevel || 0}:${item.itemLocationHrid || ""}`;
        itemMap.set(key, item);
      }
    }
    for (const item of newItems) {
      const key = `${item.itemHrid}:${item.enhancementLevel || 0}:${item.itemLocationHrid || ""}`;
      if (item.count > 0) {
        itemMap.set(key, item);
      } else {
        itemMap.delete(key);
      }
    }
    target.characterItems = Array.from(itemMap.values());
  }
  function mergeSkills(target, newSkills) {
    if (!newSkills?.length) return;
    const skillMap = new Map();
    if (target.characterSkills) {
      for (const skill of target.characterSkills) {
        skillMap.set(skill.skillHrid, skill);
      }
    }
    for (const skill of newSkills) {
      skillMap.set(skill.skillHrid, skill);
    }
    target.characterSkills = Array.from(skillMap.values());
  }
  function syncToStorage(characterData2, force = false) {
    if (!characterData2) return;
    const now = Date.now();
    if (!force && now - lastSyncTime < SYNC_THROTTLE_MS) return;
    lastSyncTime = now;
    characterData2.currentTimestamp = ( new Date()).toISOString();
    const storedData = { data: characterData2, timestamp: now };
    _GM_setValue(STORAGE_KEY$1, storedData);
    log("Synced:", characterData2.character?.name);
  }
  function loadFromStorage() {
    const stored = _GM_getValue(STORAGE_KEY$1, null);
    if (stored?.data) {
      log("Loaded from storage:", stored.data.character?.name);
      return stored.data;
    }
    return null;
  }
  class DOMObserver {
    observer = null;
    handlers = [];
    isObserving = false;
start() {
      if (this.isObserving) return;
      this.observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
          for (const node of mutation.addedNodes) {
            if (!(node instanceof Element)) continue;
            this.processNode(node);
          }
        }
      });
      this.observer.observe(document.body, {
        childList: true,
        subtree: true
      });
      this.isObserving = true;
      log("DOM observer started");
    }
stop() {
      if (this.observer) {
        this.observer.disconnect();
        this.observer = null;
      }
      this.isObserving = false;
      log("DOM observer stopped");
    }
onClass(name, className, callback) {
      const handler = { name, className, callback };
      this.handlers.push(handler);
      return () => {
        const index = this.handlers.indexOf(handler);
        if (index > -1) {
          this.handlers.splice(index, 1);
        }
      };
    }
processNode(node) {
      for (const handler of this.handlers) {
        if (node.classList.contains(handler.className)) {
          try {
            handler.callback(node);
          } catch (err) {
            console.error(`[MWM] Handler error (${handler.name}):`, err);
          }
        }
        const matches = node.querySelectorAll(`.${handler.className}`);
        for (const match of matches) {
          try {
            handler.callback(match);
          } catch (err) {
            console.error(`[MWM] Handler error (${handler.name}):`, err);
          }
        }
      }
    }
  }
  const domObserver = new DOMObserver();
  const NON_TRADEABLE_ITEMS = new Set([
"coin",
    "cowbell",
    "task_token",
    "chimerical_token",
    "sinister_token",
    "enchanted_token",
    "pirate_token",
"purples_gift",
    "small_meteorite_cache",
    "medium_meteorite_cache",
    "large_meteorite_cache",
    "small_artisans_crate",
    "medium_artisans_crate",
    "large_artisans_crate",
    "small_treasure_chest",
    "medium_treasure_chest",
    "large_treasure_chest",
    "chimerical_chest",
    "chimerical_refinement_chest",
    "sinister_chest",
    "sinister_refinement_chest",
    "enchanted_chest",
    "enchanted_refinement_chest",
    "pirate_chest",
    "pirate_refinement_chest",
"sinister_cape",
    "sinister_cape_refined",
    "chimerical_quiver",
    "chimerical_quiver_refined",
    "enchanted_cloak",
    "enchanted_cloak_refined",
"trainee_milking_charm",
    "trainee_foraging_charm",
    "trainee_woodcutting_charm",
    "trainee_cheesesmithing_charm",
    "trainee_crafting_charm",
    "trainee_tailoring_charm",
    "trainee_cooking_charm",
    "trainee_brewing_charm",
    "trainee_alchemy_charm",
    "trainee_enhancing_charm",
    "trainee_stamina_charm",
    "trainee_intelligence_charm",
    "trainee_attack_charm",
    "trainee_defense_charm",
    "trainee_melee_charm",
    "trainee_ranged_charm",
    "trainee_magic_charm",
"basic_task_badge",
    "advanced_task_badge",
    "expert_task_badge",
    "task_crystal"
  ]);
  function isNonTradeable(itemHrid) {
    return NON_TRADEABLE_ITEMS.has(itemHrid.toLowerCase());
  }
  const GREASYFORK_URL = "https://greasyfork.org/en/scripts/540058-mwi-market-addon";
  let updateNotificationShown = false;
  function showUpdateNotification(expectedVersion) {
    if (updateNotificationShown) return;
    updateNotificationShown = true;
    const banner = document.createElement("div");
    banner.className = "mwm-update-banner";
    banner.innerHTML = `
		<div class="mwm-update-content">
			<span class="mwm-update-icon">⚠️</span>
			<span class="mwm-update-text">
				MWM Addon update required: v${VERSION} → v${expectedVersion}
			</span>
			<a href="${GREASYFORK_URL}" target="_blank" class="mwm-update-link">
				Update Now
			</a>
			<button class="mwm-update-dismiss" aria-label="Dismiss">×</button>
		</div>
	`;
    banner.querySelector(".mwm-update-dismiss")?.addEventListener("click", () => {
      banner.remove();
    });
    banner.querySelector(".mwm-update-link")?.addEventListener("click", () => {
      banner.remove();
    });
    document.body.appendChild(banner);
    warn(`Update required: v${VERSION} → v${expectedVersion}`);
  }
  function getCacheKey(itemHrid, enhancement) {
    return `${itemHrid}:${enhancement}`;
  }
  const priceCache = new Map();
  const CACHE_TTL = 5 * 60 * 1e3;
  async function fetchPriceChart(itemHrid, enhancement = 0) {
    if (isNonTradeable(itemHrid)) {
      return [];
    }
    const cacheKey = getCacheKey(itemHrid, enhancement);
    const cached = priceCache.get(cacheKey);
    if (cached && cached.expires > Date.now()) {
      log(`Cache hit for ${itemHrid} +${enhancement}`);
      return cached.data;
    }
    const url = enhancement > 0 ? `${MARKET_API}/addon/prices/${itemHrid}?enhancement=${enhancement}` : `${MARKET_API}/addon/prices/${itemHrid}`;
    return new Promise((resolve, reject) => {
      _GM_xmlhttpRequest({
        method: "GET",
        url,
        headers: {
          "Content-Type": "application/json",
          "X-MWM-Addon": VERSION
        },
        onload: (response) => {
          if (response.status === 426) {
            try {
              const errorData = JSON.parse(response.responseText);
              const match = errorData.error?.message?.match(/Expected ([^\s,]+)/);
              const expectedVersion = match?.[1] || "latest";
              showUpdateNotification(expectedVersion);
            } catch {
              showUpdateNotification("latest");
            }
            resolve([]);
            return;
          }
          try {
            const parsed = JSON.parse(response.responseText);
            if (parsed.success) {
              log(
                `Loaded ${parsed.data.length} price points for ${itemHrid} +${enhancement}`
              );
              priceCache.set(cacheKey, {
                data: parsed.data,
                expires: Date.now() + CACHE_TTL
              });
              resolve(parsed.data);
            } else {
              warn(`API error for ${itemHrid} +${enhancement}`);
              resolve([]);
            }
          } catch (err) {
            warn(`Parse error for ${itemHrid} +${enhancement}:`, err);
            reject(err);
          }
        },
        onerror: (err) => {
          warn(`Network error for ${itemHrid} +${enhancement}:`, err);
          reject(err);
        }
      });
    });
  }
  const INJECTED_CLASS$2 = "mwm-modal-injected";
  const ITEM_DICTIONARY_CLASS = "ItemDictionary_modalContent__WvEBY";
  const ITEM_TITLE_CLASS = "ItemDictionary_title__27cTd";
  function initItemModal() {
    log("Initializing item modal prices");
    return domObserver.onClass(
      "item-modal-prices",
      ITEM_DICTIONARY_CLASS,
      handleItemModal
    );
  }
  function formatPrice$2(price) {
    if (price === null) return "—";
    if (price >= 1e6) return `${(price / 1e6).toFixed(1)}M`;
    if (price >= 1e3) return `${(price / 1e3).toFixed(1)}K`;
    return price.toLocaleString();
  }
  function formatDate$1(timestamp) {
    const date = new Date(timestamp);
    return date.toLocaleDateString("en-US", {
      month: "short",
      day: "numeric",
      hour: "2-digit",
      minute: "2-digit"
    });
  }
  function calculateStats$1(data) {
    const bidPrices = data.map((d) => d.bidPrice).filter((p) => p !== null);
    const askPrices = data.map((d) => d.askPrice).filter((p) => p !== null);
    const currentBid = bidPrices[bidPrices.length - 1] ?? null;
    const currentAsk = askPrices[askPrices.length - 1] ?? null;
    const highBid = bidPrices.length > 0 ? Math.max(...bidPrices) : null;
    const lowBid = bidPrices.length > 0 ? Math.min(...bidPrices) : null;
    let change = 0;
    if (bidPrices.length >= 2) {
      const first = bidPrices[0];
      const last = bidPrices[bidPrices.length - 1];
      change = (last - first) / first * 100;
    }
    return { currentBid, currentAsk, highBid, lowBid, change };
  }
  function renderChart$1(canvas, data, tooltipEl) {
    const ctx = canvas.getContext("2d");
    if (!ctx) return;
    const rect = canvas.getBoundingClientRect();
    const dpr = window.devicePixelRatio || 1;
    canvas.width = rect.width * dpr;
    canvas.height = rect.height * dpr;
    ctx.scale(dpr, dpr);
    const width = rect.width;
    const height = rect.height;
    const padding = { top: 15, right: 10, bottom: 20, left: 45 };
    const chartWidth = width - padding.left - padding.right;
    const chartHeight = height - padding.top - padding.bottom;
    ctx.clearRect(0, 0, width, height);
    if (data.length < 2) {
      ctx.fillStyle = "rgba(226, 232, 240, 0.4)";
      ctx.font = "11px monospace";
      ctx.textAlign = "center";
      ctx.fillText("No data available", width / 2, height / 2);
      return;
    }
    const bidPrices = data.map((d) => d.bidPrice).filter((p) => p !== null);
    const askPrices = data.map((d) => d.askPrice).filter((p) => p !== null);
    const allPrices = [...bidPrices, ...askPrices];
    const minPrice = Math.min(...allPrices);
    const maxPrice = Math.max(...allPrices);
    const priceRange = maxPrice - minPrice || 1;
    const getX = (i) => padding.left + i / (data.length - 1) * chartWidth;
    const getY = (price) => padding.top + chartHeight - (price - minPrice) / priceRange * chartHeight;
    ctx.strokeStyle = "rgba(59, 89, 152, 0.2)";
    ctx.lineWidth = 1;
    for (let i = 0; i <= 4; i++) {
      const y = padding.top + i / 4 * chartHeight;
      ctx.beginPath();
      ctx.moveTo(padding.left, y);
      ctx.lineTo(width - padding.right, y);
      ctx.stroke();
      const price = maxPrice - i / 4 * priceRange;
      ctx.fillStyle = "rgba(226, 232, 240, 0.4)";
      ctx.font = "9px monospace";
      ctx.textAlign = "right";
      ctx.fillText(formatPrice$2(price), padding.left - 6, y + 3);
    }
    ctx.beginPath();
    let started = false;
    for (let i = 0; i < data.length; i++) {
      const price = data[i].askPrice;
      if (price !== null) {
        const x = getX(i);
        const y = getY(price);
        if (!started) {
          ctx.moveTo(x, y);
          started = true;
        } else {
          ctx.lineTo(x, y);
        }
      }
    }
    ctx.strokeStyle = "#5ee9c5";
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.beginPath();
    started = false;
    for (let i = 0; i < data.length; i++) {
      const price = data[i].bidPrice;
      if (price !== null) {
        const x = getX(i);
        const y = getY(price);
        if (!started) {
          ctx.moveTo(x, y);
          started = true;
        } else {
          ctx.lineTo(x, y);
        }
      }
    }
    ctx.strokeStyle = "#e879a7";
    ctx.lineWidth = 2;
    ctx.stroke();
    canvas.onmousemove = (e) => {
      const bounds = canvas.getBoundingClientRect();
      const mouseX = e.clientX - bounds.left;
      const dataIndex = Math.round(
        (mouseX - padding.left) / chartWidth * (data.length - 1)
      );
      if (dataIndex >= 0 && dataIndex < data.length) {
        const point = data[dataIndex];
        const dateEl = tooltipEl.querySelector(".mwm-tooltip-date");
        const bidEl = tooltipEl.querySelector(".mwm-tooltip-price.bid");
        const askEl = tooltipEl.querySelector(".mwm-tooltip-price.ask");
        if (dateEl) dateEl.textContent = formatDate$1(point.timestamp);
        if (bidEl) bidEl.textContent = `Bid: ${formatPrice$2(point.bidPrice)}`;
        if (askEl) askEl.textContent = `Ask: ${formatPrice$2(point.askPrice)}`;
        tooltipEl.classList.add("visible");
      }
    };
    canvas.onmouseleave = () => {
      tooltipEl.classList.remove("visible");
    };
  }
  function createPanelHTML$1() {
    return `
		<div class="mwm-modal-panel ${INJECTED_CLASS$2}">
			<div class="mwm-panel-header" style="cursor: default;">
				<div class="mwm-panel-title">
					<div class="mwm-panel-icon">📊</div>
					<span class="mwm-panel-name">Market Prices (14d)</span>
				</div>
			</div>
			<div class="mwm-panel-body">
				<div class="mwm-stats-row">
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Bid</div>
						<div class="mwm-stat-value bid" data-stat="bid">—</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Ask</div>
						<div class="mwm-stat-value ask" data-stat="ask">—</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">High</div>
						<div class="mwm-stat-value" data-stat="high">—</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Low</div>
						<div class="mwm-stat-value" data-stat="low">—</div>
					</div>
				</div>
				<div class="mwm-chart-container">
					<canvas class="mwm-chart-canvas"></canvas>
					<div class="mwm-chart-tooltip">
						<div class="mwm-tooltip-date"></div>
						<div class="mwm-tooltip-prices">
							<span class="mwm-tooltip-price bid"></span>
							<span class="mwm-tooltip-price ask"></span>
						</div>
					</div>
				</div>
				<div class="mwm-legend">
					<div class="mwm-legend-item">
						<span class="mwm-legend-color bid"></span>
						<span>Bid</span>
					</div>
					<div class="mwm-legend-item">
						<span class="mwm-legend-color ask"></span>
						<span>Ask</span>
					</div>
				</div>
			</div>
			<div class="mwm-panel-footer">
				<a class="mwm-panel-link" href="https://milkyway.market" target="_blank">
					View on MilkyWay Market →
				</a>
				<span class="mwm-panel-brand">MWM</span>
			</div>
		</div>
	`;
  }
  async function handleItemModal(modalContent) {
    if (modalContent.querySelector(`.${INJECTED_CLASS$2}`)) return;
    const titleEl = modalContent.querySelector(`h1.${ITEM_TITLE_CLASS}`);
    if (!titleEl) return;
    const itemName = titleEl.textContent?.trim();
    if (!itemName) return;
    const itemHrid = nameToHrid$1(itemName);
    log(`Item modal detected: ${itemName} -> ${itemHrid}`);
    const panelContainer = document.createElement("div");
    panelContainer.innerHTML = createPanelHTML$1();
    const panel = panelContainer.firstElementChild;
    const insertTarget = titleEl.parentElement || modalContent;
    insertTarget.insertAdjacentElement("afterend", panel);
    const statsRow = panel.querySelector(".mwm-stats-row");
    if (statsRow) {
      statsRow.innerHTML = `
			<div class="mwm-loading" style="grid-column: 1 / -1;">
				<div class="mwm-loading-spinner"></div>
				<span>Loading market data...</span>
			</div>
		`;
    }
    try {
      const priceData = await fetchPriceChart(itemHrid);
      if (priceData.length > 0) {
        const stats = calculateStats$1(priceData);
        if (statsRow) {
          statsRow.innerHTML = `
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Bid</div>
						<div class="mwm-stat-value bid">${formatPrice$2(stats.currentBid)}</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Ask</div>
						<div class="mwm-stat-value ask">${formatPrice$2(stats.currentAsk)}</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">High</div>
						<div class="mwm-stat-value">${formatPrice$2(stats.highBid)}</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Low</div>
						<div class="mwm-stat-value">${formatPrice$2(stats.lowBid)}</div>
					</div>
				`;
        }
        const canvas = panel.querySelector(
          ".mwm-chart-canvas"
        );
        const tooltip = panel.querySelector(".mwm-chart-tooltip");
        if (canvas && tooltip) {
          renderChart$1(canvas, priceData, tooltip);
        }
      } else {
        const body = panel.querySelector(".mwm-panel-body");
        if (body) {
          body.innerHTML = `
					<div class="mwm-no-data">
						<span>No market data available for this item</span>
					</div>
				`;
        }
      }
    } catch (err) {
      warn(`Failed to load prices for modal: ${err}`);
      const body = panel.querySelector(".mwm-panel-body");
      if (body) {
        body.innerHTML = `
				<div class="mwm-error">
					<span>Failed to load market data</span>
				</div>
			`;
      }
    }
  }
  function nameToHrid$1(name) {
    return name.toLowerCase().replace(/['']/g, "").replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "");
  }
  const SETTINGS_STORAGE_KEY = "@mwm/settings";
  const SETTINGS_PANEL_CLASS = "SettingsPanel_profileTab__214Bj";
  const DEFAULT_SETTINGS = {
    tooltipGraphEnabled: true,
    marketGraphEnabled: true,
    fetchDelayMs: 2e3
  };
  let currentSettings = { ...DEFAULT_SETTINGS };
  function loadSettings() {
    try {
      const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
      if (stored) {
        const parsed = JSON.parse(stored);
        currentSettings = {
          ...DEFAULT_SETTINGS,
          ...parsed,
fetchDelayMs: Math.max(
            2e3,
            parsed.fetchDelayMs ?? DEFAULT_SETTINGS.fetchDelayMs
          )
        };
      }
    } catch {
      currentSettings = { ...DEFAULT_SETTINGS };
    }
    return currentSettings;
  }
  function saveSettings() {
    try {
      localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(currentSettings));
      log("Settings saved:", currentSettings);
    } catch (err) {
      console.error("Failed to save settings:", err);
    }
  }
  function getSettings() {
    return currentSettings;
  }
  function createSettingsPanelHTML() {
    return `
		<div id="mwm-settings" class="mwm-settings-panel">
			<div class="mwm-settings-header">
				<span class="mwm-settings-title">MWM Addon Settings</span>
				<span class="mwm-settings-note">(changes apply immediately)</span>
			</div>

			<div class="mwm-settings-section">
				<div class="mwm-setting-item">
					<label class="mwm-setting-label">
						<input type="checkbox" id="mwm-tooltip-graph" ${currentSettings.tooltipGraphEnabled ? "checked" : ""} />
						<span class="mwm-setting-text">Show prices in tooltips</span>
					</label>
				</div>

				<div class="mwm-setting-item">
					<label class="mwm-setting-label">
						<input type="checkbox" id="mwm-market-graph" ${currentSettings.marketGraphEnabled ? "checked" : ""} />
						<span class="mwm-setting-text">Show price chart in marketplace</span>
					</label>
				</div>

				<div class="mwm-setting-item">
					<label class="mwm-setting-label">
						<span class="mwm-setting-text">Fetch delay (seconds):</span>
						<input
							type="number"
							id="mwm-fetch-delay"
							value="${currentSettings.fetchDelayMs / 1e3}"
							min="2"
							step="0.5"
							class="mwm-setting-input"
						/>
					</label>
					<span class="mwm-setting-hint">Minimum 2s. Higher = fewer API calls on rapid hovers</span>
				</div>
			</div>
		</div>
	`;
  }
  function injectSettingsUI(settingsPanel) {
    if (settingsPanel.querySelector("#mwm-settings")) return;
    const container = document.createElement("div");
    container.innerHTML = createSettingsPanelHTML();
    settingsPanel.appendChild(container.firstElementChild);
    const tooltipCheckbox = document.getElementById(
      "mwm-tooltip-graph"
    );
    const marketCheckbox = document.getElementById(
      "mwm-market-graph"
    );
    const delayInput = document.getElementById(
      "mwm-fetch-delay"
    );
    if (tooltipCheckbox) {
      tooltipCheckbox.addEventListener("change", (e) => {
        const target = e.target;
        currentSettings.tooltipGraphEnabled = target.checked;
        log("Tooltip graph setting changed:", target.checked);
        saveSettings();
        log("Current settings:", { ...currentSettings });
      });
    }
    if (marketCheckbox) {
      marketCheckbox.addEventListener("change", (e) => {
        const target = e.target;
        currentSettings.marketGraphEnabled = target.checked;
        log("Market graph setting changed:", target.checked);
        saveSettings();
        log("Current settings:", { ...currentSettings });
      });
    }
    if (delayInput) {
      delayInput.addEventListener("input", (e) => {
        const target = e.target;
        const valueInSeconds = parseFloat(target.value);
        const valueInMs = isNaN(valueInSeconds) ? 2e3 : valueInSeconds * 1e3;
        currentSettings.fetchDelayMs = Math.max(2e3, valueInMs);
        target.value = String(currentSettings.fetchDelayMs / 1e3);
        log("Fetch delay setting changed:", currentSettings.fetchDelayMs);
        saveSettings();
      });
    }
    log("Settings UI injected");
  }
  function initSettings() {
    log("Settings initialized:", currentSettings);
    return domObserver.onClass(
      "settings-panel",
      SETTINGS_PANEL_CLASS,
      injectSettingsUI
    );
  }
  const INJECTED_CLASS$1 = "mwm-marketplace-injected";
  const CURRENT_ITEM_CLASS = "MarketplacePanel_currentItem__3ercC";
  const STORAGE_KEY = "@mwm/panel-visible";
  function getPanelVisible() {
    try {
      const stored = localStorage.getItem(STORAGE_KEY);
      if (stored === null) {
        return false;
      }
      return stored === "true";
    } catch {
      return false;
    }
  }
  function setPanelVisible(visible) {
    try {
      localStorage.setItem(STORAGE_KEY, String(visible));
    } catch {
    }
  }
  function initMarketplacePrices() {
    log("Initializing marketplace prices");
    return domObserver.onClass(
      "marketplace-prices",
      CURRENT_ITEM_CLASS,
      handleMarketplaceItem
    );
  }
  function formatPrice$1(price) {
    if (price === null) return "—";
    if (price >= 1e6) return `${(price / 1e6).toFixed(1)}M`;
    if (price >= 1e3) return `${(price / 1e3).toFixed(1)}K`;
    return price.toLocaleString();
  }
  function formatDate(timestamp) {
    const date = new Date(timestamp);
    return date.toLocaleDateString("en-US", {
      month: "short",
      day: "numeric",
      hour: "2-digit",
      minute: "2-digit"
    });
  }
  function calculateStats(data) {
    const bidPrices = data.map((d) => d.bidPrice).filter((p) => p !== null);
    const askPrices = data.map((d) => d.askPrice).filter((p) => p !== null);
    const currentBid = bidPrices[bidPrices.length - 1] ?? null;
    const currentAsk = askPrices[askPrices.length - 1] ?? null;
    const highBid = bidPrices.length > 0 ? Math.max(...bidPrices) : null;
    const lowBid = bidPrices.length > 0 ? Math.min(...bidPrices) : null;
    let change = 0;
    if (bidPrices.length >= 2) {
      const first = bidPrices[0];
      const last = bidPrices[bidPrices.length - 1];
      change = (last - first) / first * 100;
    }
    return { currentBid, currentAsk, highBid, lowBid, change };
  }
  function renderChart(canvas, data, tooltipEl) {
    const ctx = canvas.getContext("2d");
    if (!ctx) return;
    const rect = canvas.getBoundingClientRect();
    const dpr = window.devicePixelRatio || 1;
    canvas.width = rect.width * dpr;
    canvas.height = rect.height * dpr;
    ctx.scale(dpr, dpr);
    const width = rect.width;
    const height = rect.height;
    const padding = { top: 15, right: 10, bottom: 20, left: 45 };
    const chartWidth = width - padding.left - padding.right;
    const chartHeight = height - padding.top - padding.bottom;
    let hoverIndex = null;
    const render = () => {
      ctx.clearRect(0, 0, width, height);
      if (data.length < 2) {
        ctx.fillStyle = "rgba(226, 232, 240, 0.4)";
        ctx.font = "11px monospace";
        ctx.textAlign = "center";
        ctx.fillText("No data available", width / 2, height / 2);
        return;
      }
      const bidPrices = data.map((d) => d.bidPrice).filter((p) => p !== null);
      const askPrices = data.map((d) => d.askPrice).filter((p) => p !== null);
      const allPrices = [...bidPrices, ...askPrices];
      const minPrice = Math.min(...allPrices);
      const maxPrice = Math.max(...allPrices);
      const priceRange = maxPrice - minPrice || 1;
      const getX = (i) => padding.left + (data.length - 1 - i) / (data.length - 1) * chartWidth;
      const getY = (price) => padding.top + chartHeight - (price - minPrice) / priceRange * chartHeight;
      ctx.strokeStyle = "rgba(59, 89, 152, 0.2)";
      ctx.lineWidth = 1;
      for (let i = 0; i <= 4; i++) {
        const y = padding.top + i / 4 * chartHeight;
        ctx.beginPath();
        ctx.moveTo(padding.left, y);
        ctx.lineTo(width - padding.right, y);
        ctx.stroke();
        const price = maxPrice - i / 4 * priceRange;
        ctx.fillStyle = "rgba(226, 232, 240, 0.4)";
        ctx.font = "10px monospace";
        ctx.textAlign = "right";
        ctx.fillText(formatPrice$1(price), padding.left - 6, y + 3);
      }
      const dateCount = Math.min(5, data.length);
      ctx.fillStyle = "rgba(226, 232, 240, 0.4)";
      ctx.font = "10px monospace";
      ctx.textAlign = "center";
      for (let i = 0; i < dateCount; i++) {
        const dataIdx = dateCount === 1 ? 0 : Math.round(
          (dateCount - 1 - i) / (dateCount - 1) * (data.length - 1)
        );
        const x = getX(dataIdx);
        const point = data[dataIdx];
        if (!point) continue;
        const date = new Date(point.timestamp);
        const label = `${(date.getMonth() + 1).toString().padStart(2, "0")}/${date.getDate().toString().padStart(2, "0")}`;
        ctx.fillText(label, x, height - 4);
      }
      ctx.beginPath();
      let started = false;
      for (let i = 0; i < data.length; i++) {
        const price = data[i]?.askPrice;
        if (price !== null && price !== void 0) {
          const x = getX(i);
          const y = getY(price);
          if (!started) {
            ctx.moveTo(x, y);
            started = true;
          } else {
            ctx.lineTo(x, y);
          }
        }
      }
      ctx.strokeStyle = "#5ee9c5";
      ctx.lineWidth = 2;
      ctx.stroke();
      ctx.beginPath();
      started = false;
      for (let i = 0; i < data.length; i++) {
        const price = data[i]?.bidPrice;
        if (price !== null && price !== void 0) {
          const x = getX(i);
          const y = getY(price);
          if (!started) {
            ctx.moveTo(x, y);
            started = true;
          } else {
            ctx.lineTo(x, y);
          }
        }
      }
      ctx.strokeStyle = "#e879a7";
      ctx.lineWidth = 2;
      ctx.stroke();
      if (hoverIndex !== null && hoverIndex >= 0 && hoverIndex < data.length) {
        const point = data[hoverIndex];
        if (!point) return;
        const x = getX(hoverIndex);
        if (point.askPrice !== null) {
          const y = getY(point.askPrice);
          ctx.beginPath();
          ctx.arc(x, y, 4, 0, Math.PI * 2);
          ctx.fillStyle = "#5ee9c5";
          ctx.fill();
          ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
          ctx.lineWidth = 1.5;
          ctx.stroke();
        }
        if (point.bidPrice !== null) {
          const y = getY(point.bidPrice);
          ctx.beginPath();
          ctx.arc(x, y, 4, 0, Math.PI * 2);
          ctx.fillStyle = "#e879a7";
          ctx.fill();
          ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
          ctx.lineWidth = 1.5;
          ctx.stroke();
        }
      }
    };
    render();
    canvas.onmousemove = (e) => {
      const bounds = canvas.getBoundingClientRect();
      const mouseX = e.clientX - bounds.left;
      const dataIndex = Math.round(
        (1 - (mouseX - padding.left) / chartWidth) * (data.length - 1)
      );
      if (dataIndex >= 0 && dataIndex < data.length) {
        const point = data[dataIndex];
        if (!point) return;
        hoverIndex = dataIndex;
        render();
        const dateEl = tooltipEl.querySelector(".mwm-tooltip-date");
        const bidEl = tooltipEl.querySelector(".mwm-tooltip-price.bid");
        const askEl = tooltipEl.querySelector(".mwm-tooltip-price.ask");
        if (dateEl) dateEl.textContent = formatDate(point.timestamp);
        if (bidEl) bidEl.textContent = `Bid: ${formatPrice$1(point.bidPrice)}`;
        if (askEl) askEl.textContent = `Ask: ${formatPrice$1(point.askPrice)}`;
        tooltipEl.classList.add("visible");
      }
    };
    canvas.onmouseleave = () => {
      hoverIndex = null;
      render();
      tooltipEl.classList.remove("visible");
    };
  }
  function createPanelHTML(isVisible) {
    const collapsedClass = isVisible ? "" : "collapsed";
    const toggleIcon = isVisible ? "−" : "+";
    const toggleTitle = isVisible ? "Hide panel" : "Show panel";
    return `
		<div class="mwm-marketplace-panel ${INJECTED_CLASS$1} ${collapsedClass}">
			<div class="mwm-panel-header" style="cursor: default;">
				<div class="mwm-panel-title">
					<span class="mwm-panel-name">MWM Prices (14d)</span>
				</div>
				<button class="mwm-panel-toggle" title="${toggleTitle}">${toggleIcon}</button>
			</div>
			<div class="mwm-panel-body">
				<div class="mwm-stats-row">
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Ask</div>
						<div class="mwm-stat-value ask" data-stat="ask">—</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Bid</div>
						<div class="mwm-stat-value bid" data-stat="bid">—</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">High</div>
						<div class="mwm-stat-value" data-stat="high">—</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Low</div>
						<div class="mwm-stat-value" data-stat="low">—</div>
					</div>
				</div>
				<div class="mwm-chart-container">
					<canvas class="mwm-chart-canvas"></canvas>
					<div class="mwm-chart-tooltip">
						<div class="mwm-tooltip-date"></div>
						<div class="mwm-tooltip-prices">
						<span class="mwm-tooltip-price ask"></span>
							<span class="mwm-tooltip-price bid"></span>
						</div>
					</div>
				</div>
				<div class="mwm-legend">
					<div class="mwm-legend-item">
						<span class="mwm-legend-color ask"></span>
						<span>Ask</span>
					</div>
					<div class="mwm-legend-item">
						<span class="mwm-legend-color bid"></span>
						<span>Bid</span>
					</div>
				</div>
			</div>
			<div class="mwm-panel-footer">
				<a class="mwm-panel-link" href="https://milkyway.market" target="_blank">
					View on MilkyWay Market →
				</a>
				<span class="mwm-panel-brand">MWM</span>
			</div>
		</div>
	`;
  }
  function extractItemHrid(currentItemEl) {
    const useElement = currentItemEl.querySelector("use");
    if (useElement && useElement.href && useElement.href.baseVal) {
      const itemId = useElement.href.baseVal.split("#")[1];
      if (itemId) {
        return itemId;
      }
    }
    return null;
  }
  function extractEnhancementLevel(currentItemEl) {
    const infoContainer = currentItemEl.closest(
      '[class*="MarketplacePanel_infoContainer"]'
    );
    if (!infoContainer) return 0;
    const nameSelectors = [
      '[class*="Item_name"]',
      '[class*="itemName"]',
      "h2",
      "h3",
      '[class*="header"]'
    ];
    for (const selector of nameSelectors) {
      const nameEl = infoContainer.querySelector(selector);
      if (nameEl?.textContent) {
        const match2 = nameEl.textContent.match(/\+(\d+)\s*$/);
        if (match2?.[1]) {
          return parseInt(match2[1], 10);
        }
      }
    }
    const allText = infoContainer.textContent || "";
    const match = allText.match(/\+(\d{1,2})(?:\s|$)/);
    if (match?.[1]) {
      const level = parseInt(match[1], 10);
      if (level >= 0 && level <= 20) {
        return level;
      }
    }
    return 0;
  }
  async function handleMarketplaceItem(currentItemEl) {
    if (!getSettings().marketGraphEnabled) {
      const existingPanel2 = document.querySelector(
        `.mwm-marketplace-panel.${INJECTED_CLASS$1}`
      );
      existingPanel2?.remove();
      return;
    }
    const priceInputsContainer = document.querySelector(
      '[class*="MarketplacePanel_orderBooksContainer"]'
    );
    const infoContainer = currentItemEl.closest(
      '[class*="MarketplacePanel_infoContainer"]'
    );
    const insertTarget = priceInputsContainer || infoContainer;
    if (!insertTarget) {
      warn("Could not find marketplace insert target");
      return;
    }
    const existingPanel = document.querySelector(
      `.mwm-marketplace-panel.${INJECTED_CLASS$1}`
    );
    const itemHrid = extractItemHrid(currentItemEl);
    if (!itemHrid) {
      warn("Could not extract item HRID from marketplace");
      return;
    }
    const enhancement = extractEnhancementLevel(currentItemEl);
    if (isNonTradeable(itemHrid)) {
      log(`Skipping non-tradeable item: ${itemHrid}`);
      existingPanel?.remove();
      return;
    }
    const cacheKey = `${itemHrid}:${enhancement}`;
    if (existingPanel) {
      const existingKey = existingPanel.dataset.itemKey;
      if (existingKey === cacheKey) {
        return;
      }
      existingPanel.remove();
    }
    const isVisible = getPanelVisible();
    const panelContainer = document.createElement("div");
    panelContainer.innerHTML = createPanelHTML(isVisible);
    const panel = panelContainer.firstElementChild;
    panel.dataset.itemHrid = itemHrid;
    panel.dataset.itemKey = cacheKey;
    panel.dataset.enhancement = String(enhancement);
    if (priceInputsContainer) {
      priceInputsContainer.insertAdjacentElement("beforebegin", panel);
    } else {
      insertTarget.appendChild(panel);
    }
    const toggleBtn = panel.querySelector(".mwm-panel-toggle");
    let dataLoaded = false;
    if (toggleBtn) {
      toggleBtn.addEventListener("click", async () => {
        const isNowCollapsed = panel.classList.toggle("collapsed");
        const newVisible = !isNowCollapsed;
        setPanelVisible(newVisible);
        toggleBtn.textContent = newVisible ? "−" : "+";
        toggleBtn.setAttribute("title", newVisible ? "Hide panel" : "Show panel");
        if (newVisible && !dataLoaded) {
          dataLoaded = true;
          await loadPanelData(panel, itemHrid, enhancement);
        }
      });
    }
    if (!isVisible) {
      return;
    }
    dataLoaded = true;
    await loadPanelData(panel, itemHrid, enhancement);
  }
  async function loadPanelData(panel, itemHrid, enhancement = 0) {
    const statsRow = panel.querySelector(".mwm-stats-row");
    if (statsRow) {
      statsRow.innerHTML = `
			<div class="mwm-loading" style="grid-column: 1 / -1;">
				<div class="mwm-loading-spinner"></div>
				<span>Loading market data...</span>
			</div>
		`;
    }
    try {
      const priceData = await fetchPriceChart(itemHrid, enhancement);
      if (priceData.length > 0) {
        const stats = calculateStats(priceData);
        if (statsRow) {
          statsRow.innerHTML = `
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Ask</div>
						<div class="mwm-stat-value ask">${formatPrice$1(stats.currentAsk)}</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Bid</div>
						<div class="mwm-stat-value bid">${formatPrice$1(stats.currentBid)}</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">High</div>
						<div class="mwm-stat-value">${formatPrice$1(stats.highBid)}</div>
					</div>
					<div class="mwm-stat-item">
						<div class="mwm-stat-label">Low</div>
						<div class="mwm-stat-value">${formatPrice$1(stats.lowBid)}</div>
					</div>
				`;
        }
        const canvas = panel.querySelector(
          ".mwm-chart-canvas"
        );
        const tooltip = panel.querySelector(".mwm-chart-tooltip");
        if (canvas && tooltip) {
          renderChart(canvas, priceData, tooltip);
        }
      } else {
        const body = panel.querySelector(".mwm-panel-body");
        if (body) {
          body.innerHTML = `
					<div class="mwm-no-data">
						<span>No market data available for this item</span>
					</div>
				`;
        }
      }
    } catch (err) {
      warn(`Failed to load prices for marketplace: ${err}`);
      const body = panel.querySelector(".mwm-panel-body");
      if (body) {
        body.innerHTML = `
				<div class="mwm-error">
					<span>Failed to load market data</span>
				</div>
			`;
      }
    }
  }
  const DEFAULT_OPTIONS = {
    width: 200,
    height: 36,
    bidColor: "#e879a7",
askColor: "#5ee9c5",
fillOpacity: 0.15,
    lineWidth: 1.5
  };
  function renderSparkline(canvas, data, options) {
    const opts = { ...DEFAULT_OPTIONS, ...options };
    const ctx = canvas.getContext("2d");
    if (!ctx) return;
    canvas.width = opts.width;
    canvas.height = opts.height;
    ctx.clearRect(0, 0, opts.width, opts.height);
    if (data.length < 2) return;
    const bidPrices = data.map((d) => d.bidPrice).filter((p) => p !== null);
    const askPrices = data.map((d) => d.askPrice).filter((p) => p !== null);
    const allPrices = [...bidPrices, ...askPrices];
    const minPrice = Math.min(...allPrices);
    const maxPrice = Math.max(...allPrices);
    const priceRange = maxPrice - minPrice || 1;
    const normalizeY = (price) => {
      const padding = 2;
      const height = opts.height - padding * 2;
      return padding + height - (price - minPrice) / priceRange * height;
    };
    const getX = (index, total) => {
      return index / (total - 1) * opts.width;
    };
    if (askPrices.length >= 2) {
      drawLine(ctx, data, "askPrice", opts.askColor, opts, normalizeY, getX);
    }
    if (bidPrices.length >= 2) {
      drawLine(ctx, data, "bidPrice", opts.bidColor, opts, normalizeY, getX);
    }
  }
  function drawLine(ctx, data, priceKey, color, opts, normalizeY, getX) {
    const points = [];
    for (let i = 0; i < data.length; i++) {
      const price = data[i][priceKey];
      if (price !== null) {
        points.push({
          x: getX(i, data.length),
          y: normalizeY(price)
        });
      }
    }
    if (points.length < 2) return;
    ctx.beginPath();
    ctx.moveTo(points[0].x, opts.height);
    for (const point of points) {
      ctx.lineTo(point.x, point.y);
    }
    ctx.lineTo(points[points.length - 1].x, opts.height);
    ctx.closePath();
    const gradient = ctx.createLinearGradient(0, 0, 0, opts.height);
    gradient.addColorStop(0, hexToRgba(color, opts.fillOpacity));
    gradient.addColorStop(1, hexToRgba(color, 0));
    ctx.fillStyle = gradient;
    ctx.fill();
    ctx.beginPath();
    ctx.moveTo(points[0].x, points[0].y);
    for (let i = 1; i < points.length; i++) {
      ctx.lineTo(points[i].x, points[i].y);
    }
    ctx.strokeStyle = color;
    ctx.lineWidth = opts.lineWidth;
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    ctx.stroke();
  }
  function hexToRgba(hex, alpha) {
    const r = parseInt(hex.slice(1, 3), 16);
    const g = parseInt(hex.slice(3, 5), 16);
    const b = parseInt(hex.slice(5, 7), 16);
    return `rgba(${r}, ${g}, ${b}, ${alpha})`;
  }
  function createSparklineCanvas(options) {
    const opts = { ...DEFAULT_OPTIONS, ...options };
    const canvas = document.createElement("canvas");
    canvas.width = opts.width;
    canvas.height = opts.height;
    canvas.className = "mwm-sparkline";
    return canvas;
  }
  const INJECTED_CLASS = "mwm-price-injected";
  const TOOLTIP_POPPER_CLASS = "MuiTooltip-popper";
  const ITEM_NAME_CLASS = "ItemTooltipText_name__2JAHA";
  const TOOLTIP_CONTENT_CLASS = "ItemTooltipText_itemTooltipText__zFq3A";
  function formatPrice(price) {
    if (price === null) return "—";
    if (price >= 1e6) return `${(price / 1e6).toFixed(1)}M`;
    if (price >= 1e3) return `${(price / 1e3).toFixed(1)}K`;
    return price.toLocaleString();
  }
  function calculateChange(data) {
    const bidPrices = data.map((d) => d.bidPrice).filter((p) => p !== null);
    if (bidPrices.length < 2) return 0;
    const first = bidPrices[0];
    const last = bidPrices[bidPrices.length - 1];
    return (last - first) / first * 100;
  }
  function initTooltipPrices() {
    log("Initializing tooltip prices");
    return domObserver.onClass(
      "tooltip-prices",
      TOOLTIP_POPPER_CLASS,
      handleTooltip
    );
  }
  function createLoadingHTML() {
    return `
		<div class="mwm-loading">
			<div class="mwm-loading-spinner"></div>
			<span>Loading prices...</span>
		</div>
	`;
  }
  function createPriceHTML(data) {
    const currentBid = data[data.length - 1]?.bidPrice ?? null;
    const currentAsk = data[data.length - 1]?.askPrice ?? null;
    const change = calculateChange(data);
    const changeClass = change > 0 ? "positive" : change < 0 ? "negative" : "neutral";
    const changeText = change > 0 ? `+${change.toFixed(1)}%` : `${change.toFixed(1)}%`;
    return `
		<div class="mwm-price-header">
			<span class="mwm-price-label">MWM Prices</span>
			<span class="mwm-price-change ${changeClass}">${changeText}</span>
		</div>
		<div class="mwm-price-row">
			<div class="mwm-price-item">
				<span class="mwm-price-type bid">Bid</span>
				<span class="mwm-price-value">${formatPrice(currentBid)}</span>
			</div>
			<div class="mwm-price-item">
				<span class="mwm-price-type ask">Ask</span>
				<span class="mwm-price-value">${formatPrice(currentAsk)}</span>
			</div>
		</div>
	`;
  }
  function createNoDataHTML() {
    return `
		<div class="mwm-no-data">
			<span>No market data available</span>
		</div>
	`;
  }
  function createErrorHTML() {
    return `
		<div class="mwm-error">
			<span>Failed to load prices</span>
		</div>
	`;
  }
  function getFetchDelay() {
    return getSettings().fetchDelayMs;
  }
  async function handleTooltip(tooltip) {
    if (!getSettings().tooltipGraphEnabled) {
      const existingContainer = tooltip.querySelector(`.${INJECTED_CLASS}`);
      if (existingContainer) existingContainer.remove();
      return;
    }
    const nameElement = tooltip.querySelector(`.${ITEM_NAME_CLASS}`);
    if (!nameElement) return;
    const tooltipContent = tooltip.querySelector(`.${TOOLTIP_CONTENT_CLASS}`);
    if (!tooltipContent) return;
    if (tooltipContent.querySelector(`.${INJECTED_CLASS}`)) return;
    const itemName = nameElement.textContent?.trim();
    if (!itemName) return;
    const enhancementMatch = itemName.match(/\s*\+(\d+)$/);
    const enhancement = enhancementMatch ? parseInt(enhancementMatch[1], 10) : 0;
    const cleanName = itemName.replace(/\s*\+\d+$/, "");
    const itemHrid = nameToHrid(cleanName);
    if (isNonTradeable(itemHrid)) return;
    const container = document.createElement("div");
    container.className = `mwm-price-container ${INJECTED_CLASS}`;
    container.innerHTML = createLoadingHTML();
    tooltipContent.appendChild(container);
    await new Promise((resolve) => setTimeout(resolve, getFetchDelay()));
    if (!document.body.contains(tooltip)) {
      return;
    }
    try {
      const priceData = await fetchPriceChart(itemHrid, enhancement);
      if (!document.body.contains(tooltip)) {
        return;
      }
      if (priceData.length > 0) {
        container.innerHTML = createPriceHTML(priceData);
        const canvas = createSparklineCanvas();
        container.appendChild(canvas);
        renderSparkline(canvas, priceData);
      } else {
        container.innerHTML = createNoDataHTML();
      }
    } catch (err) {
      warn(`Failed to fetch prices for ${itemHrid}:`, err);
      container.innerHTML = createErrorHTML();
    }
  }
  function nameToHrid(name) {
    return name.toLowerCase().replace(/['']/g, "").replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "");
  }
  function hookWebSocket(onMessage) {
    const dataDesc = Object.getOwnPropertyDescriptor(
      MessageEvent.prototype,
      "data"
    );
    if (!dataDesc?.get) {
      warn("Cannot hook WebSocket - MessageEvent.prototype.data not found");
      return;
    }
    const originalGet = dataDesc.get;
    dataDesc.get = function hookedGet() {
      const socket = this.currentTarget;
      if (!(socket instanceof WebSocket)) {
        return originalGet.call(this);
      }
      const isMWI = WS_ENDPOINTS.some((ep) => socket.url?.includes(ep));
      if (!isMWI) {
        return originalGet.call(this);
      }
      const message = originalGet.call(this);
      Object.defineProperty(this, "data", { value: message });
      if (typeof message === "string") {
        try {
          const data = JSON.parse(message);
          if (data?.type) {
            setTimeout(() => onMessage(data.type, data), 0);
          }
        } catch {
        }
      }
      return message;
    };
    Object.defineProperty(MessageEvent.prototype, "data", dataDesc);
    log("WebSocket hook installed");
  }
  const styleCss = ':root{--mwm-bg-deep: #0a0e1a;--mwm-bg-panel: rgba(18, 22, 42, .95);--mwm-bg-surface: rgba(30, 36, 60, .8);--mwm-border-glow: #3b5998;--mwm-border-dim: rgba(59, 89, 152, .3);--mwm-bid-color: #e879a7;--mwm-bid-color-dim: rgba(232, 121, 167, .2);--mwm-ask-color: #5ee9c5;--mwm-ask-color-dim: rgba(94, 233, 197, .2);--mwm-accent-purple: #8b5cf6;--mwm-accent-purple-dim: rgba(139, 92, 246, .15);--mwm-text-primary: #e2e8f0;--mwm-text-secondary: rgba(226, 232, 240, .6);--mwm-text-muted: rgba(226, 232, 240, .4);--mwm-font-mono: "SF Mono", "Fira Code", "Consolas", "Monaco", monospace}.mwm-price-container{margin-top:10px;padding:10px 12px;background:linear-gradient(135deg,var(--mwm-bg-panel) 0%,rgba(12,16,32,.95) 100%);border-radius:6px;border:1px solid var(--mwm-border-dim);box-shadow:0 0 20px #3b599826,inset 0 1px #ffffff0d;position:relative;overflow:hidden}.mwm-price-container:before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.03) 2px,rgba(0,0,0,.03) 4px);pointer-events:none}.mwm-price-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;position:relative;z-index:1}.mwm-price-label{font-family:var(--mwm-font-mono);font-size:9px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--mwm-accent-purple);display:flex;align-items:center;gap:6px}.mwm-price-label:before{content:"";width:6px;height:6px;background:var(--mwm-accent-purple);border-radius:50%;box-shadow:0 0 6px var(--mwm-accent-purple);animation:mwm-pulse 2s ease-in-out infinite}@keyframes mwm-pulse{0%,to{opacity:1}50%{opacity:.5}}.mwm-click-hint{font-family:var(--mwm-font-mono);font-size:8px;color:var(--mwm-text-muted);cursor:pointer;transition:color .2s}.mwm-click-hint:hover{color:var(--mwm-text-secondary)}.mwm-price-row{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px;position:relative;z-index:1}.mwm-price-item{display:flex;flex-direction:column;gap:2px}.mwm-price-type{font-family:var(--mwm-font-mono);font-size:8px;text-transform:uppercase;letter-spacing:.05em}.mwm-price-type.bid{color:var(--mwm-bid-color)}.mwm-price-type.ask{color:var(--mwm-ask-color)}.mwm-price-value{font-family:var(--mwm-font-mono);font-size:13px;font-weight:600;color:var(--mwm-text-primary)}.mwm-price-change{font-family:var(--mwm-font-mono);font-size:10px;padding:2px 6px;border-radius:3px}.mwm-price-change.positive{color:var(--mwm-bid-color);background:var(--mwm-bid-color-dim)}.mwm-price-change.negative{color:var(--mwm-ask-color);background:var(--mwm-ask-color-dim)}.mwm-price-change.neutral{color:var(--mwm-text-secondary);background:var(--mwm-bg-surface)}.mwm-sparkline{display:block;width:100%;height:32px;border-radius:4px;background:var(--mwm-bg-surface);position:relative;z-index:1}.mwm-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:16px 12px;color:var(--mwm-text-secondary);font-family:var(--mwm-font-mono);font-size:10px}.mwm-loading-spinner{width:14px;height:14px;border:2px solid var(--mwm-border-dim);border-top-color:var(--mwm-accent-purple);border-radius:50%;animation:mwm-spin .8s linear infinite}@keyframes mwm-spin{to{transform:rotate(360deg)}}.mwm-error{display:flex;align-items:center;gap:6px;padding:8px;color:var(--mwm-text-muted);font-family:var(--mwm-font-mono);font-size:9px}.mwm-panel-overlay{position:fixed;inset:0;background:#0006;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:999998;opacity:0;pointer-events:none;transition:opacity .2s ease}.mwm-panel-overlay.visible{opacity:1;pointer-events:auto}.mwm-detail-panel{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(.95);width:420px;max-width:90vw;background:linear-gradient(180deg,var(--mwm-bg-panel) 0%,var(--mwm-bg-deep) 100%);border-radius:12px;border:1px solid var(--mwm-border-glow);box-shadow:0 0 40px #3b59984d,0 0 80px #8b5cf61a,inset 0 1px #ffffff1a;z-index:999999;opacity:0;pointer-events:none;transition:all .25s cubic-bezier(.34,1.56,.64,1);overflow:hidden}.mwm-detail-panel.visible{opacity:1;pointer-events:auto;transform:translate(-50%,-50%) scale(1)}.mwm-detail-panel:before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.02) 2px,rgba(0,0,0,.02) 4px);pointer-events:none;z-index:1}.mwm-panel-header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;background:linear-gradient(90deg,var(--mwm-accent-purple-dim) 0%,transparent 100%);border-bottom:1px solid var(--mwm-border-dim);cursor:move;-webkit-user-select:none;user-select:none;position:relative;z-index:2}.mwm-panel-title{display:flex;align-items:center;gap:10px}.mwm-panel-icon{width:28px;height:28px;border-radius:4px;background:var(--mwm-bg-surface);display:flex;align-items:center;justify-content:center;font-size:16px;border:1px solid var(--mwm-border-dim)}.mwm-panel-name{font-family:var(--mwm-font-mono);font-size:14px;font-weight:600;color:var(--mwm-text-primary)}.mwm-panel-close{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:none;background:transparent;color:var(--mwm-text-secondary);cursor:pointer;border-radius:4px;transition:all .15s;font-size:18px}.mwm-panel-close:hover{background:#ffffff1a;color:var(--mwm-text-primary)}.mwm-panel-body{padding:16px;position:relative;z-index:2}.mwm-stats-row{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}.mwm-stat-item{background:var(--mwm-bg-surface);border-radius:6px;padding:10px 8px;border:1px solid var(--mwm-border-dim);text-align:center}.mwm-stat-label{font-family:var(--mwm-font-mono);font-size:8px;text-transform:uppercase;letter-spacing:.1em;color:var(--mwm-text-muted);margin-bottom:4px}.mwm-stat-value{font-family:var(--mwm-font-mono);font-size:13px;font-weight:600;color:var(--mwm-text-primary)}.mwm-stat-value.bid{color:var(--mwm-bid-color)}.mwm-stat-value.ask{color:var(--mwm-ask-color)}.mwm-period-selector{display:flex;gap:4px;margin-bottom:12px}.mwm-period-btn{flex:1;padding:6px 12px;border:1px solid var(--mwm-border-dim);background:transparent;color:var(--mwm-text-secondary);font-family:var(--mwm-font-mono);font-size:10px;font-weight:500;border-radius:4px;cursor:pointer;transition:all .15s}.mwm-period-btn:hover{background:var(--mwm-bg-surface);color:var(--mwm-text-primary)}.mwm-period-btn.active{background:var(--mwm-accent-purple-dim);border-color:var(--mwm-accent-purple);color:var(--mwm-accent-purple)}.mwm-chart-container{position:relative;background:var(--mwm-bg-surface);border-radius:8px;border:1px solid var(--mwm-border-dim);overflow:hidden}.mwm-chart-canvas{display:block;width:100%;height:180px;cursor:crosshair}.mwm-chart-tooltip{position:absolute;top:8px;right:8px;background:var(--mwm-bg-panel);border:1px solid var(--mwm-border-dim);border-radius:4px;padding:8px 10px;font-family:var(--mwm-font-mono);pointer-events:none;opacity:0;transition:opacity .15s;z-index:10}.mwm-chart-tooltip.visible{opacity:1}.mwm-tooltip-date{font-size:10px;color:var(--mwm-text-muted);margin-bottom:4px}.mwm-tooltip-prices{display:flex;gap:12px}.mwm-tooltip-price{font-size:11px}.mwm-tooltip-price.bid{color:var(--mwm-bid-color)}.mwm-tooltip-price.ask{color:var(--mwm-ask-color)}.mwm-panel-footer{padding:12px 16px;border-top:1px solid var(--mwm-border-dim);display:flex;align-items:center;justify-content:space-between;position:relative;z-index:2}.mwm-panel-link{font-family:var(--mwm-font-mono);font-size:10px;color:var(--mwm-accent-purple);text-decoration:none;display:flex;align-items:center;gap:4px;transition:opacity .15s}.mwm-panel-link:hover{opacity:.8}.mwm-panel-brand{font-family:var(--mwm-font-mono);font-size:9px;color:var(--mwm-text-muted)}.mwm-legend{display:flex;gap:16px;margin-top:10px;justify-content:center}.mwm-legend-item{display:flex;align-items:center;gap:6px;font-family:var(--mwm-font-mono);font-size:9px;color:var(--mwm-text-secondary)}.mwm-legend-color{width:12px;height:3px;border-radius:2px}.mwm-legend-color.bid{background:var(--mwm-bid-color)}.mwm-legend-color.ask{background:var(--mwm-ask-color)}.mwm-no-data{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;color:var(--mwm-text-muted);font-family:var(--mwm-font-mono);font-size:11px;text-align:center;gap:4px}.mwm-modal-panel{margin:16px 0;background:linear-gradient(180deg,var(--mwm-bg-panel) 0%,var(--mwm-bg-deep) 100%);border-radius:8px;border:1px solid var(--mwm-border-glow);box-shadow:0 0 20px #3b599833,inset 0 1px #ffffff0d;overflow:hidden}.mwm-modal-panel .mwm-panel-header{padding:10px 14px;background:linear-gradient(90deg,var(--mwm-accent-purple-dim) 0%,transparent 100%);border-bottom:1px solid var(--mwm-border-dim)}.mwm-modal-panel .mwm-panel-body{padding:14px}.mwm-modal-panel .mwm-panel-footer{padding:10px 14px}.mwm-modal-panel .mwm-stats-row{grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:12px}.mwm-modal-panel .mwm-chart-canvas{height:140px}.mwm-marketplace-panel{margin:12px 0;background:linear-gradient(180deg,var(--mwm-bg-panel) 0%,var(--mwm-bg-deep) 100%);border-radius:8px;border:1px solid var(--mwm-border-glow);box-shadow:0 0 20px #3b599833,inset 0 1px #ffffff0d;overflow:visible;width:100%}.mwm-marketplace-panel .mwm-panel-header{padding:8px 12px;background:linear-gradient(90deg,var(--mwm-accent-purple-dim) 0%,transparent 100%);border-bottom:1px solid var(--mwm-border-dim)}.mwm-marketplace-panel .mwm-panel-name{font-size:11px}.mwm-marketplace-panel .mwm-panel-body{padding:10px 12px}.mwm-marketplace-panel .mwm-panel-footer{padding:8px 12px}.mwm-marketplace-panel .mwm-stats-row{grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:10px}.mwm-marketplace-panel .mwm-stat-item{padding:6px 4px}.mwm-marketplace-panel .mwm-stat-label{font-size:7px}.mwm-marketplace-panel .mwm-stat-value{font-size:11px}.mwm-marketplace-panel .mwm-chart-canvas{height:120px}.mwm-marketplace-panel .mwm-legend{margin-top:8px}.mwm-marketplace-panel .mwm-panel-link{font-size:9px}.mwm-marketplace-panel .mwm-panel-brand{font-size:8px}.mwm-panel-toggle{width:24px;height:24px;display:flex;align-items:center;justify-content:center;border:1px solid var(--mwm-border-dim);background:var(--mwm-bg-surface);color:var(--mwm-text-secondary);cursor:pointer;border-radius:4px;transition:all .15s;font-size:16px;font-family:var(--mwm-font-mono);line-height:1}.mwm-panel-toggle:hover{background:#ffffff1a;color:var(--mwm-text-primary);border-color:var(--mwm-accent-purple)}.mwm-marketplace-panel.collapsed .mwm-panel-body,.mwm-marketplace-panel.collapsed .mwm-panel-footer{display:none}.mwm-marketplace-panel.collapsed{border-color:var(--mwm-border-dim)}.mwm-marketplace-panel.collapsed .mwm-panel-header{border-bottom:none}.mwm-settings-panel{margin:20px 0;padding:16px 32px;background:linear-gradient(135deg,var(--mwm-bg-panel) 0%,rgba(12,16,32,.95) 100%);border-radius:8px;border:1px solid var(--mwm-border-glow);box-shadow:0 0 20px #3b599833,inset 0 1px #ffffff0d;position:relative;overflow:hidden;width:100%}.mwm-settings-header{display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid var(--mwm-border-dim);position:relative;z-index:1}.mwm-settings-title{font-family:var(--mwm-font-mono);font-size:13px;font-weight:600;color:var(--mwm-accent-purple);text-transform:uppercase;letter-spacing:.05em;display:flex;align-items:center;gap:8px}.mwm-settings-title:before{content:"";width:8px;height:8px;background:var(--mwm-accent-purple);border-radius:50%;box-shadow:0 0 8px var(--mwm-accent-purple);animation:mwm-pulse 2s ease-in-out infinite}.mwm-settings-note{font-family:var(--mwm-font-mono);font-size:9px;color:var(--mwm-text-muted)}.mwm-settings-section{display:flex;flex-direction:column;gap:14px;position:relative;z-index:1}.mwm-setting-item{display:flex;flex-direction:column;gap:8px}.mwm-setting-label{display:flex;align-items:center;gap:10px;cursor:pointer}.mwm-setting-label input[type=checkbox]{appearance:none;-webkit-appearance:none;width:18px;height:18px;border:2px solid var(--mwm-border-dim);border-radius:4px;background:var(--mwm-bg-surface);cursor:pointer;position:relative;transition:all .15s;flex-shrink:0}.mwm-setting-label input[type=checkbox]:hover{border-color:var(--mwm-accent-purple);background:var(--mwm-accent-purple-dim)}.mwm-setting-label input[type=checkbox]:checked{background:var(--mwm-accent-purple);border-color:var(--mwm-accent-purple)}.mwm-setting-label input[type=checkbox]:checked:after{content:"✓";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;font-size:11px;font-weight:700}.mwm-setting-text{font-family:var(--mwm-font-mono);font-size:11px;color:var(--mwm-text-primary)}.mwm-setting-input{width:80px;padding:6px 10px;background:var(--mwm-bg-surface);border:1px solid var(--mwm-border-dim);border-radius:4px;color:var(--mwm-text-primary);font-family:var(--mwm-font-mono);font-size:11px;transition:all .15s}.mwm-setting-input:hover,.mwm-setting-input:focus{border-color:var(--mwm-accent-purple);outline:none}.mwm-setting-input:focus{box-shadow:0 0 8px var(--mwm-accent-purple-dim)}.mwm-setting-hint{font-family:var(--mwm-font-mono);font-size:9px;color:var(--mwm-text-muted);text-align:left}.mwm-setting-preview{padding:10px;background:var(--mwm-bg-surface);border-radius:6px;border:1px solid var(--mwm-border-dim);margin-left:28px;transition:opacity .3s}.mwm-setting-preview canvas{display:block;border-radius:4px}.mwm-setting-input::-webkit-inner-spin-button,.mwm-setting-input::-webkit-outer-spin-button{opacity:1;height:20px}.mwm-update-banner{position:fixed;top:0;left:0;right:0;z-index:999999;background:linear-gradient(90deg,#dc2626f2,#b91c1cf2);box-shadow:0 4px 20px #0000004d,0 0 40px #dc262633;animation:mwm-slide-down .3s ease-out}@keyframes mwm-slide-down{0%{transform:translateY(-100%);opacity:0}to{transform:translateY(0);opacity:1}}.mwm-update-content{display:flex;align-items:center;justify-content:center;gap:12px;padding:10px 16px;max-width:100%}.mwm-update-icon{font-size:18px;flex-shrink:0}.mwm-update-text{font-family:var(--mwm-font-mono);font-size:12px;font-weight:500;color:#fff;letter-spacing:.02em}.mwm-update-link{display:inline-flex;align-items:center;gap:4px;padding:6px 14px;background:#fff3;border:1px solid rgba(255,255,255,.3);border-radius:4px;color:#fff;font-family:var(--mwm-font-mono);font-size:11px;font-weight:600;text-decoration:none;text-transform:uppercase;letter-spacing:.05em;transition:all .15s;flex-shrink:0}.mwm-update-link:hover{background:#ffffff4d;border-color:#ffffff80;transform:translateY(-1px)}.mwm-update-dismiss{width:24px;height:24px;display:flex;align-items:center;justify-content:center;border:none;background:#ffffff1a;color:#ffffffb3;cursor:pointer;border-radius:4px;font-size:18px;line-height:1;transition:all .15s;flex-shrink:0;margin-left:8px}.mwm-update-dismiss:hover{background:#fff3;color:#fff}';
  importCSS(styleCss);
  function signalPresence() {
    document.documentElement.setAttribute("data-mwm-addon", VERSION);
    window.dispatchEvent(
      new CustomEvent(EVENTS.ADDON_READY, { detail: { version: VERSION } })
    );
    log("Addon ready");
  }
  let characterData = null;
  async function initGameSite() {
    hookWebSocket((type, data) => {
      switch (type) {
case "init_character_data":
          characterData = data;
          log("Character initialized:", characterData.character?.name);
          syncToStorage(characterData, true);
          break;
case "items_updated": {
          if (!characterData) return;
          const itemsData = data;
          const items = itemsData.characterItems || itemsData.items;
          if (items) mergeItems(characterData, items);
          syncToStorage(characterData);
          break;
        }
case "action_completed": {
          if (!characterData) return;
          const actionData = data;
          const items = actionData.characterItems || actionData.endCharacterItems;
          const skills = actionData.characterSkills || actionData.endCharacterSkills;
          if (items) mergeItems(characterData, items);
          if (skills) mergeSkills(characterData, skills);
          syncToStorage(characterData);
          break;
        }
      }
    });
    window.addEventListener(EVENTS.REQUEST, (event) => {
      log("Data request received");
      const customEvent = event;
      if (characterData) {
        syncToStorage(characterData, true);
        const detail = {
          requestId: customEvent.detail?.requestId,
          data: characterData,
          source: "game_site"
        };
        window.dispatchEvent(new CustomEvent(EVENTS.RESPONSE, { detail }));
      } else {
        warn("No character data available");
      }
    });
    loadSettings();
    domObserver.start();
    initSettings();
    initTooltipPrices();
    initItemModal();
    initMarketplacePrices();
    log("Game site initialized");
  }
  function initMarketSite() {
    characterData = loadFromStorage();
    _GM_addValueChangeListener(
      STORAGE_KEY$1,
      (_name, _oldValue, newValue, remote) => {
        if (!remote || !newValue?.data) return;
        const storedData = newValue;
        characterData = storedData.data;
        log("Cross-tab update:", characterData.character?.name);
        const detail = {
          data: characterData,
          source: "cross_tab",
          timestamp: storedData.timestamp
        };
        window.dispatchEvent(new CustomEvent(EVENTS.UPDATED, { detail }));
      }
    );
    window.addEventListener(EVENTS.REQUEST, (event) => {
      log("Pull request received");
      const customEvent = event;
      if (characterData) {
        const detail = {
          requestId: customEvent.detail?.requestId,
          data: characterData,
          source: "storage"
        };
        window.dispatchEvent(new CustomEvent(EVENTS.RESPONSE, { detail }));
      } else {
        warn("No character data in storage");
      }
    });
    if (characterData) {
      setTimeout(() => {
        const detail = {
          data: characterData,
          source: "initial_load",
          timestamp: Date.now()
        };
        window.dispatchEvent(new CustomEvent(EVENTS.UPDATED, { detail }));
      }, 100);
    }
    log("Market site initialized");
  }
  function init() {
    signalPresence();
    if (isGameSite) {
      initGameSite();
    } else if (isMarketSite) {
      initMarketSite();
    }
  }
  init();

})();