mooket

银河奶牛历史价格(包含强化物品)history(enhancement included) price for milkywayidle

当前为 2025-04-22 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         mooket
// @namespace    http://tampermonkey.net/
// @version      20250422.2.2
// @description  银河奶牛历史价格(包含强化物品)history(enhancement included) price for milkywayidle
// @author       IOMisaka
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @icon         https://www.milkywayidle.com/favicon.svg
// @grant        none
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js
// @run-at       document-start
// @license MIT
// ==/UserScript==

(function () {
  'use strict';
  let injectSpace = "mwi";//use window.mwi to access the injected object
  if (window[injectSpace]) return;//已经注入
  let mwi = {//供外部调用的接口
    version: "0.2.0",//版本号,未改动原有接口只更新最后一个版本号,更改了接口会更改次版本号,主版本暂时不更新,等稳定之后再考虑主版本号更新
    MWICoreInitialized: false,//是否初始化完成,完成会还会通过window发送一个自定义事件 MWICoreInitialized

    /*一些可以直接用的游戏数据,欢迎大家一起来整理
    game.state.levelExperienceTable //经验表
    game.state.skillingActionTypeBuffsDict },
    game.state.characterActions //[0]是当前正在执行的动作,其余是队列中的动作
    */
    game: null,//注入游戏对象,可以直接访问游戏中的大量数据和方法以及消息事件等
    lang: null,//语言翻译, 例如中文物品lang.zh.translation.itemNames['/items/coin']
    buffCalculator: null,//注入buff计算对象buffCalculator.mergeBuffs()合并buffs,计算加成效果等
    alchemyCalculator: null,//注入炼金计算对象


    /* marketJson兼容接口 */
    get marketJson() {
      return this.MWICoreInitialized && new Proxy(this.coreMarket, {
        get(coreMarket, prop) {
          if (prop === "market") {
            return new Proxy(coreMarket, {
              get(coreMarket, itemHridOrName) {
                return coreMarket.getItemPrice(itemHridOrName);
              }
            });
          }
          return null;
        }

      });
    },
    coreMarket: null,//coreMarket.marketData 格式{"/items/apple_yogurt:0":{ask,bid,time}}
    itemNameToHridDict: null,//物品名称反查表
    ensureItemHrid: function (itemHridOrName) {
      let itemHrid = this.itemNameToHridDict[itemHridOrName];
      if (itemHrid) return itemHrid;
      if (itemHridOrName?.startsWith("/items/") && this?.game?.state?.itemDetailDict) return itemHridOrName;
      return null;
    },//各种名字转itemHrid,找不到返回原itemHrid或者null
    hookCallback: hookCallback,//hook回调,用于hook游戏事件等 例如聊天消息mwi.hookCallback(mwi.game, "handleMessageChatMessageReceived", (_,obj)=>{console.log(obj)})
    fetchWithTimeout: fetchWithTimeout,//带超时的fetch
  };
  window[injectSpace] = mwi;

  async function patchScript(node) {
    try {
      const scriptUrl = node.src;
      node.remove();
      const response = await fetch(scriptUrl);
      if (!response.ok) throw new Error(`Failed to fetch script: ${response.status}`);

      let sourceCode = await response.text();

      // Define injection points as configurable patterns
      const injectionPoints = [
        {
          pattern: "Ca.a.use",
          replacement: `window.${injectSpace}.lang=Oa;Ca.a.use`,
          description: "注入语言翻译对象"
        },
        {
          pattern: "class lp extends s.a.Component{constructor(e){var t;super(e),t=this,",
          replacement: `class lp extends s.a.Component{constructor(e){var t;super(e),t=this,window.${injectSpace}.game=this,`,
          description: "注入游戏对象"

        },
        {
          pattern: "var Q=W;",
          replacement: `window.${injectSpace}.buffCalculator=W;var Q=W;`,
          description: "注入buff计算对象"
        },
        {
          pattern: "class Dn",
          replacement: `window.${injectSpace}.alchemyCalculator=Mn;class Dn`,
          description: "注入炼金计算对象"
        },
        {
          pattern: "var z=q;",
          replacement: `window.${injectSpace}.actionManager=q;var z=q;`,
          description: "注入动作管理对象"
        }
      ];

      injectionPoints.forEach(({ pattern, replacement, description }) => {
        if (sourceCode.includes(pattern)) {
          sourceCode = sourceCode.replace(pattern, replacement);
          console.info(`MWICore injecting: ${description}`);
        } else {
          console.warn(`MWICore injecting failed: ${description}`);
        }
      });

      const newNode = document.createElement('script');
      newNode.textContent = sourceCode;
      document.body.appendChild(newNode);
      console.info('MWICore patched successfully.')
    } catch (error) {
      console.error('MWICore patching failed:', error);
    }
  }
  new MutationObserver((mutationsList, obs) => {
    mutationsList.forEach((mutationRecord) => {
      for (const node of mutationRecord.addedNodes) {
        if (node.src) {
          if (node.src.search(/.*main\..*\.chunk.js/) === 0) {
            obs.disconnect();
            patchScript(node);
          }
        }
      }
    });
  }).observe(document, { childList: true, subtree: true });

  /**
   * Hook回调函数并添加后处理
   * @param {Object} targetObj 目标对象
   * @param {string} callbackProp 回调属性名
   * @param {Function} handler 后处理函数
   */
  function hookCallback(targetObj, callbackProp, handler) {
    const originalCallback = targetObj[callbackProp];

    if (!originalCallback) {
      throw new Error(`Callback ${callbackProp} does not exist`);
    }

    targetObj[callbackProp] = function (...args) {
      const result = originalCallback.apply(this, args);

      // 异步处理
      if (result && typeof result.then === 'function') {
        return result.then(res => {
          handler(res, ...args);
          return res;
        });
      }

      // 同步处理
      handler(result, ...args);
      return result;
    };

    // 返回取消Hook的方法
    return () => {
      targetObj[callbackProp] = originalCallback;
    };
  }
  /**
   * 带超时功能的fetch封装
   * @param {string} url - 请求URL
   * @param {object} options - fetch选项
   * @param {number} timeout - 超时时间(毫秒),默认10秒
   * @returns {Promise} - 返回fetch的Promise
   */
  function fetchWithTimeout(url, options = {}, timeout = 10000) {
    // 创建AbortController实例
    const controller = new AbortController();
    const { signal } = controller;

    // 设置超时计时器
    const timeoutId = setTimeout(() => {
      controller.abort(new Error(`请求超时: ${timeout}ms`));
    }, timeout);

    // 合并选项,添加signal
    const fetchOptions = {
      ...options,
      signal
    };

    // 发起fetch请求
    return fetch(url, fetchOptions)
      .then(response => {
        // 清除超时计时器
        clearTimeout(timeoutId);

        if (!response.ok) {
          throw new Error(`HTTP错误! 状态码: ${response.status}`);
        }
        return response;
      })
      .catch(error => {
        // 清除超时计时器
        clearTimeout(timeoutId);

        // 如果是中止错误,重新抛出超时错误
        if (error.name === 'AbortError') {
          throw new Error(`请求超时: ${timeout}ms`);
        }
        throw error;
      });
  }
  class ReconnectWebSocket {
    constructor(url, options = {}) {
      this.url = url; // WebSocket 服务器地址
      this.reconnectInterval = options.reconnectInterval || 10000; // 重连间隔(默认 5 秒)
      this.heartbeatInterval = options.heartbeatInterval || 60000; // 心跳间隔(默认 60 秒)
      this.maxReconnectAttempts = options.maxReconnectAttempts || 9999999; // 最大重连次数
      this.reconnectAttempts = 0; // 当前重连次数
      this.ws = null; // WebSocket 实例
      this.heartbeatTimer = null; // 心跳定时器
      this.isManualClose = false; // 是否手动关闭连接

      // 绑定事件处理器
      this.onOpen = options.onOpen || (() => { });
      this.onMessage = options.onMessage || (() => { });
      this.onClose = options.onClose || (() => { });
      this.onError = options.onError || (() => { });

      this.connect();
    }

    // 连接 WebSocket
    connect() {
      this.ws = new WebSocket(this.url);

      // WebSocket 打开事件
      this.ws.onopen = () => {
        console.log('WebMooket connected');
        this.reconnectAttempts = 0; // 重置重连次数
        this.startHeartbeat(); // 启动心跳
        this.onOpen();
      };

      // WebSocket 消息事件
      this.ws.onmessage = (event) => {
        this.onMessage(event.data);
      };

      // WebSocket 关闭事件
      this.ws.onclose = () => {
        console.log('WebMooket disconnected');
        this.stopHeartbeat(); // 停止心跳
        this.onClose();

        if (!this.isManualClose) {
          this.reconnect();
        }
      };

      // WebSocket 错误事件
      this.ws.onerror = (error) => {
        console.error('WebMooket error:', error);
        this.onError(error);
      };
    }

    // 启动心跳
    startHeartbeat() {
      this.heartbeatTimer = setInterval(() => {
        if (this.ws.readyState === WebSocket.OPEN) {
          //this.ws.send("ping");
        }
      }, this.heartbeatInterval);
    }

    // 停止心跳
    stopHeartbeat() {
      if (this.heartbeatTimer) {
        clearInterval(this.heartbeatTimer);
        this.heartbeatTimer = null;
      }
    }

    // 自动重连
    reconnect() {
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        console.log(`Reconnecting in ${this.reconnectInterval / 1000} seconds...`);
        setTimeout(() => {
          this.reconnectAttempts++;
          this.connect();
        }, this.reconnectInterval);
      } else {
        console.error('Max reconnection attempts reached');
      }
    }

    // 发送消息
    send(data) {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send(data);
      } else {
        console.error('WebMooket is not open');
      }
    }

    // 手动关闭连接
    close() {
      this.isManualClose = true;
      this.ws.close();
    }
  }
  /*实时市场模块*/
  const HOST = "https://mooket.qi-e.top";
  const MWIAPI_URL = "https://raw.githubusercontent.com/holychikenz/MWIApi/main/milkyapi.json";

  class CoreMarket {
    marketData = {};//市场数据,带强化等级,存储格式{"/items/apple_yogurt:0":{ask,bid,time}}
    fetchTimeDict = {};//记录上次API请求时间,防止频繁请求
    ttl = 300;//缓存时间,单位秒
    trade_ws = null;
    constructor() {
      //core data
      let marketDataStr = localStorage.getItem("MWICore_marketData") || "{}";
      this.marketData = JSON.parse(marketDataStr);

      if (mwi.game?.state?.character?.gameMode === "standard") {//标准模式才连接ws服务器,铁牛模式不连接ws服务器
        this.trade_ws = new ReconnectWebSocket(`${HOST}/market/ws`);
      }

      //mwiapi data
      let mwiapiJsonStr = localStorage.getItem("MWIAPI_JSON") || localStorage.getItem("MWITools_marketAPI_json");
      let mwiapiObj = null;
      if (mwiapiJsonStr) {
        mwiapiObj = JSON.parse(mwiapiJsonStr);
        this.mergeMWIData(mwiapiObj);
      }
      if (!mwiapiObj || Date.now() / 1000 - mwiapiObj.time > 600) {//超过10分才更新
        fetch(MWIAPI_URL).then(res => {
          res.text().then(mwiapiJsonStr => {
            mwiapiObj = JSON.parse(mwiapiJsonStr);
            this.mergeMWIData(mwiapiObj);
            //更新本地缓存数据
            localStorage.setItem("MWIAPI_JSON", mwiapiJsonStr);//更新本地缓存数据
            console.info("MWIAPI_JSON updated:", new Date(mwiapiObj.time * 1000).toLocaleString());
          })
        });
      }
      (this.trade_ws ?? {}).onMessage = (data) => {
        if (data === "ping") { return; }//心跳包,忽略
        let obj = JSON.parse(data);
        if (obj && obj.type === "market_item_order_books_updated") {
          this.handleMessageMarketItemOrderBooksUpdated(obj, false);//收到市场服务器数据,不上传
        } else if (obj && obj.type === "ItemPrice") {
          this.processItemPrice(obj);
        } else {
          console.log(data);
        }


      }
      //市场数据更新
      hookCallback(mwi.game, "handleMessageMarketItemOrderBooksUpdated", (res, obj) => this.handleMessageMarketItemOrderBooksUpdated(obj, true));
      setInterval(() => { this.save(); }, 1000 * 600);//十分钟保存一次
    }
    handleMessageMarketItemOrderBooksUpdated(obj, upload = false) {
      //更新本地,游戏数据不带时间戳,市场服务器数据带时间戳
      let timestamp = obj.time || parseInt(Date.now() / 1000);
      let itemHrid = obj.marketItemOrderBooks.itemHrid;
      obj.marketItemOrderBooks?.orderBooks?.forEach((item, enhancementLevel) => {
        let bid = item.bids?.length > 0 ? item.bids[0].price : -1;
        let ask = item.asks?.length > 0 ? item.asks[0].price : -1;
        this.updateItem(itemHrid + ":" + enhancementLevel, { bid: bid, ask: ask, time: timestamp });
      });
      obj.time = timestamp;//添加时间戳
      //上报数据
      if (this.trade_ws) {//标准模式走ws
        if (!upload) return;//只在game收到消息的时候上报
        this.trade_ws.send(JSON.stringify(obj));//ws上报
      } else {//铁牛上报
        fetchWithTimeout(`${HOST}/market/upload/order`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify(obj)
        });
      }
    }
    /**
     * 合并MWIAPI数据,只包含0级物品
     *
     * @param obj 包含市场数据的对象
     */
    mergeMWIData(obj) {
      Object.entries(obj.market).forEach(([itemName, price]) => {
        let itemHrid = mwi.ensureItemHrid(itemName);
        if (itemHrid) this.updateItem(itemHrid + ":" + 0, { bid: price.bid, ask: price.ask, time: obj.time }, false);//本地更新
      });
      this.save();
    }
    mergeCoreDataBeforeSave() {
      let obj = JSON.parse(localStorage.getItem("MWICore_marketData") || "{}");
      Object.entries(obj).forEach(([itemHridLevel, priceObj]) => {
        this.updateItem(itemHridLevel, priceObj, false);//本地更新
      });
      //不保存,只合并
    }
    save() {//保存到localStorage
      this.mergeCoreDataBeforeSave();//从其他角色合并保存的数据
      localStorage.setItem("MWICore_marketData", JSON.stringify(this.marketData));
    }

    /**
     * 部分特殊物品的价格
     * 例如金币固定1,牛铃固定为牛铃袋/10的价格
     * @param {string} itemHrid - 物品hrid
     * @returns {Price|null} - 返回对应商品的价格对象,如果没有则null
     */
    getSpecialPrice(itemHrid) {
      switch (itemHrid) {
        case "/items/coin":
          return { bid: 1, ask: 1, time: Date.now() / 1000 };
        case "/items/cowbell": {
          let cowbells = this.getItemPrice("/items/bag_of_10_cowbells");
          return cowbells && { bid: cowbells.bid / 10, ask: cowbells.ask / 10, time: cowbells.time };
        }
        default:
          return null;
      }
    }
    /**
     * 获取商品的价格
     *
     * @param {string} itemHridOrName 商品HRID或名称
     * @param {number} [enhancementLevel=0] 装备强化等级,普通商品默认为0
     * @param {boolean} [peek=false] 是否只查看本地数据,不请求服务器数据
     * @returns {number|null} 返回商品的价格,如果商品不存在或无法获取价格则返回null
     */
    getItemPrice(itemHridOrName, enhancementLevel = 0, peek = false) {
      let itemHrid = mwi.ensureItemHrid(itemHridOrName);
      if (!itemHrid) return null;
      let specialPrice = this.getSpecialPrice(itemHrid);
      if (specialPrice) return specialPrice;
      let itemHridLevel = itemHrid + ":" + enhancementLevel;

      let priceObj = this.marketData[itemHridLevel];
      if (peek) return priceObj;

      if (Date.now() / 1000 - this.fetchTimeDict[itemHridLevel] < this.ttl) return priceObj;//1分钟内直接返回本地数据,防止频繁请求服务器
      this.fetchTimeDict[itemHridLevel] = Date.now() / 1000;
      this.trade_ws?.send(JSON.stringify({ type: "GetItemPrice", name: itemHrid, level: enhancementLevel }));
      return priceObj;
    }
    processItemPrice(resObj) {
      let itemHridLevel = resObj.name + ":" + resObj.level;
      let priceObj = { bid: resObj.bid, ask: resObj.ask, time: resObj.time };
      if (resObj.ttl) this.ttl = resObj.ttl;//更新ttl
      this.updateItem(itemHridLevel, priceObj);
    }
    updateItem(itemHridLevel, priceObj, isFetch = true) {
      let localItem = this.marketData[itemHridLevel];
      if (isFetch) this.fetchTimeDict[itemHridLevel] = Date.now() / 1000;//fetch时间戳
      if (!localItem || localItem.time < priceObj.time) {//服务器数据更新则更新本地数据

        let risePercent = 0;
        if (localItem) {
          let oriPrice = (localItem.ask + localItem.bid);
          let newPrice = (priceObj.ask + priceObj.bid);
          if (oriPrice != 0) risePercent = newPrice / oriPrice - 1;
        }
        this.marketData[itemHridLevel] = { rise: risePercent, ask: priceObj.ask, bid: priceObj.bid, time: priceObj.time };//更新本地数据
        dispatchEvent(new CustomEvent("MWICoreItemPriceUpdated"), priceObj);//触发事件
      }
    }
    resetRise() {
      Object.entries(this.marketData).forEach(([k, v]) => {
        v.rise = 0;
      });
    }
    save() {
      localStorage.setItem("MWICore_marketData", JSON.stringify(this.marketData));
    }
  }

  function init() {
    mwi.itemNameToHridDict = {};
    Object.entries(mwi.lang.en.translation.itemNames).forEach(([k, v]) => { mwi.itemNameToHridDict[v] = k });
    Object.entries(mwi.lang.zh.translation.itemNames).forEach(([k, v]) => { mwi.itemNameToHridDict[v] = k });
    mwi.coreMarket = new CoreMarket();

    mwi.MWICoreInitialized = true;
    window.dispatchEvent(new CustomEvent("MWICoreInitialized"))
    console.info("MWICoreInitialized event dispatched. window.mwi.MWICoreInitialized=true");
  }
  new Promise(resolve => {
    const interval = setInterval(() => {
      if (mwi.game && mwi.lang && mwi?.game?.state?.character?.gameMode) {//等待必须组件加载完毕后再初始化
        clearInterval(interval);
        resolve();
      }
    }, 200);
  }).then(() => {
    init();
    mooket();
  });

  function mooket() {

    window.mwi.hookCallback(window.mwi.game, "handleMessageMarketListingsUpdated", (_, obj) => {
      obj.endMarketListings.forEach(order => {
        if (order.filledQuantity == 0) return;//没有成交的订单不记录
        let key = order.itemHrid + "_" + order.enhancementLevel;

        let tradeItem = trade_history[key] || {}
        if (order.isSell) {
          tradeItem.sell = order.price;
        } else {
          tradeItem.buy = order.price;
        }
        trade_history[key] = tradeItem;
      });
      if (window.mwi?.game?.state?.character?.gameMode === "standard")//只记录标准模式的数据,因为铁牛不能交易
        localStorage.setItem("mooket_trade_history", JSON.stringify(trade_history));//保存挂单数据
    });

    let trade_history = JSON.parse(localStorage.getItem("mooket_trade_history") || "{}");

    let cur_day = 1;
    let curHridName = null;
    let curLevel = 0;
    let curShowItemName = null;
    let chartWidth = 500;
    let chartHeight = 280

    let configStr = localStorage.getItem("mooket_config");
    let config = configStr ? JSON.parse(configStr) : { "dayIndex": 0, "visible": true, "filter": { "bid": true, "ask": true, "mean": true } };
    cur_day = config.day;//读取设置

    window.onresize = function () {
      checkSize();
    };
    function checkSize() {
      if (window.innerWidth < window.innerHeight) {//竖屏,强制设置
        config.w = chartWidth = window.innerWidth * 0.8;
        config.h = chartHeight = chartWidth * 0.6;
      } else {
        chartWidth = 400;
        chartHeight = 250;
      }
    }
    checkSize();

    // 创建容器元素并设置样式和位置
    const container = document.createElement('div');
    container.style.border = "1px solid #ccc"; //边框样式
    container.style.backgroundColor = config.bgcolor||"#000";
    container.style.position = "fixed";
    container.style.zIndex = 10000;
    container.style.top = `${Math.max(0, Math.min(config.y || 0, window.innerHeight - 50))}px`; //距离顶部位置
    container.style.left = `${Math.max(0, Math.min(config.x || 0, window.innerWidth - 50))}px`; //距离左侧位置
    container.style.width = `${Math.max(0, Math.min(config.w || chartWidth, window.innerWidth))}px`; //容器宽度
    container.style.height = `${Math.max(0, Math.min(config.h || chartHeight, window.innerHeight))}px`; //容器高度
    container.style.resize = "both";
    container.style.overflow = "auto";
    container.style.display = "none";
    container.style.flexDirection = "column";
    container.style.flex = "1";
    container.style.minHeight = "33px";
    container.style.minWidth = "68px";
    container.style.cursor = "move";
    container.style.userSelect = "none";

    let mouseDragging = false;
    let touchDragging = false;
    let offsetX, offsetY;

    let resizeEndTimer = null;
    container.addEventListener("resize", () => {
      if (resizeEndTimer) clearTimeout(resizeEndTimer);
      resizeEndTimer = setTimeout(save_config, 1000);
    });
    container.addEventListener("mousedown", function (e) {
      if (mouseDragging || touchDragging) return;
      const rect = container.getBoundingClientRect();
      if (container.style.resize === "both" && (e.clientX > rect.right - 10 || e.clientY > rect.bottom - 10)) return;
      mouseDragging = true;
      offsetX = e.clientX - container.offsetLeft;
      offsetY = e.clientY - container.offsetTop;
    });

    document.addEventListener("mousemove", function (e) {
      if (mouseDragging) {
        var newX = e.clientX - offsetX;
        var newY = e.clientY - offsetY;

        if (newX < 0) newX = 0;
        if (newY < 0) newY = 0;
        if (newX > window.innerWidth - container.offsetWidth) newX = window.innerWidth - container.offsetWidth;
        if (newY > window.innerHeight - container.offsetHeight) newY = window.innerHeight - container.offsetHeight;

        container.style.left = newX + "px";
        container.style.top = newY + "px";
      }
    });

    document.addEventListener("mouseup", function () {
      if (mouseDragging) {
        mouseDragging = false;
        save_config();
      }
    });

    container.addEventListener("touchstart", function (e) {
      if (mouseDragging || touchDragging) return;
      const rect = container.getBoundingClientRect();
      let touch = e.touches[0];
      if (container.style.resize === "both" && (e.clientX > rect.right - 10 || e.clientY > rect.bottom - 10)) return;
      touchDragging = true;
      offsetX = touch.clientX - container.offsetLeft;
      offsetY = touch.clientY - container.offsetTop;
    });

    document.addEventListener("touchmove", function (e) {
      if (touchDragging) {
        let touch = e.touches[0];
        var newX = touch.clientX - offsetX;
        var newY = touch.clientY - offsetY;

        if (newX < 0) newX = 0;
        if (newY < 0) newY = 0;
        if (newX > window.innerWidth - container.offsetWidth) newX = window.innerWidth - container.offsetWidth;
        if (newY > window.innerHeight - container.offsetHeight) newY = window.innerHeight - container.offsetHeight;

        container.style.left = newX + "px";
        container.style.top = newY + "px";
      }
    });

    document.addEventListener("touchend", function () {
      if (touchDragging) {
        touchDragging = false;
        save_config();
      }
    });
    document.body.appendChild(container);

    const ctx = document.createElement('canvas');
    ctx.id = "myChart";
    container.appendChild(ctx);



    // 创建下拉菜单并设置样式和位置
    let wrapper = document.createElement('div');
    wrapper.style.position = 'absolute';
    wrapper.style.top = '5px';
    wrapper.style.right = '16px';
    wrapper.style.fontSize = '14px';

    //wrapper.style.backgroundColor = '#fff';
    wrapper.style.flexShrink = 0;
    container.appendChild(wrapper);

    const days = [1, 3, 7, 14, 30, 180, 360];
    const dayTitle = ['1天', '3天', '1周', '2周', '1月', '半年', '一年'];
    cur_day = days[config.dayIndex];

    let select = document.createElement('select');
    select.style.cursor = 'pointer';
    select.style.verticalAlign = 'middle';
    select.onchange = function () {
      config.dayIndex = days.indexOf(parseInt(this.value));
      if (curHridName) requestItemPrice(curHridName, this.value, curLevel);
      save_config();
    };

    for (let i = 0; i < days.length; i++) {
      let option = document.createElement('option');
      option.value = days[i];
      option.text = dayTitle[i];
      if (i === config.dayIndex) option.selected = true;
      select.appendChild(option);
    }

    wrapper.appendChild(select);

    // 创建一个容器元素并设置样式和位置
    const leftContainer = document.createElement('div');
    leftContainer.style.padding = '2px'
    leftContainer.style.display = 'flex';
    leftContainer.style.flexDirection = 'row';
    leftContainer.style.alignItems = 'center'
    container.appendChild(leftContainer);

    //添加一个btn隐藏canvas和wrapper
    let btn_close = document.createElement('input');
    btn_close.type = 'button';
    btn_close.value = '📈隐藏';
    btn_close.style.margin = 0;
    btn_close.style.cursor = 'pointer';

    leftContainer.appendChild(btn_close);

    let picker = document.createElement('input');
    picker.type = 'color';
    picker.style.cursor = 'pointer';
    picker.style.width = '20px';
    picker.style.height = '20px';
    picker.style.padding = "0"
    picker.style.verticalAlign="middle"
    picker.onchange = function () {
      container.style.backgroundColor = this.value;
      config.bgcolor = this.value;
      save_config();
    }
    wrapper.appendChild(picker);



    //一个固定的文本显示买入卖出历史价格
    let price_info = document.createElement('div');

    price_info.style.fontSize = '14px';
    price_info.title = "我的最近买/卖价格"
    price_info.style.width = "max-content";
    price_info.style.whiteSpace = "nowrap";
    price_info.style.lineHeight = '25px';
    price_info.style.display = 'none';
    price_info.style.marginLeft = '5px';

    let buy_price = document.createElement('span');
    let sell_price = document.createElement('span');
    price_info.appendChild(buy_price);
    price_info.appendChild(sell_price);
    buy_price.style.color = 'red';
    sell_price.style.color = 'lime';

    leftContainer.appendChild(price_info);

    let lastWidth;
    let lastHeight;
    btn_close.onclick = toggle;
    function toggle() {
      if (wrapper.style.display === 'none') {
        wrapper.style.display = ctx.style.display = 'block';
        container.style.resize = "both";
        btn_close.value = '📈隐藏';
        leftContainer.style.position = 'absolute'
        leftContainer.style.top = '1px';
        leftContainer.style.left = '1px';
        container.style.width = lastWidth;
        container.style.height = lastHeight;
        config.visible = true;
        save_config();
      } else {
        lastWidth = container.style.width;
        lastHeight = container.style.height;
        wrapper.style.display = ctx.style.display = 'none';
        container.style.resize = "none";
        container.style.width = "auto";
        container.style.height = "auto";


        btn_close.value = '📈显示';
        leftContainer.style.position = 'relative'
        leftContainer.style.top = 0;
        leftContainer.style.left = 0;

        config.visible = false;
        save_config();
      }
    };

    let chart = new Chart(ctx, {
      type: 'line',
      data: {
        labels: [],
        datasets: [{
          label: '市场',
          data: [],
          backgroundColor: 'rgba(255,99,132,0.2)',
          borderColor: 'rgba(255,99,132,1)',
          borderWidth: 1
        }]
      },
      options: {
        onClick: save_config,
        responsive: true,
        maintainAspectRatio: false,
        pointRadius: 0,
        pointHitRadius: 20,
        scales: {
          y: {
            beginAtZero: false,
            ticks: {
              // 自定义刻度标签格式化
              callback: showNumber
            }
          }
        },
        plugins: {
          title: {
            display: true,
            text: "",
            color: "cornflowerblue",
            font: {
              size: 15,
              weight: 'bold',
            }
          }
        }
      }
    });

    function requestItemPrice(itemHridName, day = 1, level = 0) {
      if (!itemHridName) return;
      if (curHridName === itemHridName && curLevel === level && cur_day === day) return;//防止重复请求

      curHridName = itemHridName;
      curLevel = level;
      cur_day = day;

      curShowItemName = localStorage.getItem("i18nextLng")?.startsWith("zh") ?
        window.mwi.lang.zh.translation.itemNames[itemHridName] : window.mwi.lang.en.translation.itemNames[itemHridName];
      curShowItemName += curLevel > 0 ? "+" + curLevel : "";

      let time = day * 3600 * 24;
      //const HOST = "https://mooket.qi-e.top";上面定义了
      if (curLevel > 0 || day < 2) {
        const params = new URLSearchParams();
        params.append("name", curHridName);
        params.append("level", curLevel);
        params.append("time", time);
        fetch(`${HOST}/market/item/history?${params}`).then(res => {
          res.json().then(data => updateChart(data, cur_day));
        })
      }//新api
      else {//旧api
        let itemNameEN = window.mwi.game.state.itemDetailDict[itemHridName].name;
        fetch(`${HOST}/market`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            name: itemNameEN,
            time: time
          })
        }).then(res => {
          res.json().then(data => updateChart(data, cur_day));
        })
      }
    }

    function formatTime(timestamp, range) {
      const date = new Date(timestamp * 1000);
      const pad = n => n.toString().padStart(2, '0');

      // 获取各时间组件
      const hours = pad(date.getHours());
      const minutes = pad(date.getMinutes());
      const day = pad(date.getDate());
      const month = pad(date.getMonth() + 1);
      const shortYear = date.getFullYear().toString().slice(-2);

      // 根据时间范围选择格式
      switch (parseInt(range)) {
        case 1: // 1天:只显示时间
          return `${hours}:${minutes}`;

        case 3: // 3天:日+时段
          return `${hours}:${minutes}`;

        case 7: // 7天:月/日 + 时段
          return `${day}.${hours}`;
        case 14: // 14天:月/日 + 时段
          return `${day}.${hours}`;
        case 30: // 30天:月/日
          return `${month}/${day}`;

        default: // 180天:年/月
          return `${shortYear}/${month}`;
      }
    }

    function showNumber(num) {
      if (isNaN(num)) return num;
      if (num === 0) return "0"; // 单独处理0的情况

      const absNum = Math.abs(num);

      //num保留一位小数
      if (num < 1) return num.toFixed(2);

      return absNum >= 1e10 ? `${(num / 1e9).toFixed(1)}B` :
        absNum >= 1e7 ? `${(num / 1e6).toFixed(1)}M` :
          absNum >= 1e4 ? `${Math.floor(num / 1e3)}K` :
            `${Math.floor(num)}`;
    }
    //data={'bid':[{time:1,price:1}],'ask':[{time:1,price:1}]}
    function updateChart(data, day) {
      //字段名差异
      data.bid = data.bid || data.bids
      data.ask = data.ask || data.asks;
      //过滤异常元素
      for (let i = data.bid.length - 1; i >= 0; i--) {
        if (data.bid[i].price < 0 && data.ask[i].price < 0) {//都小于0,认为是异常数据,直接删除
          data.bid.splice(i, 1);
          data.ask.splice(i, 1);
        } else {//小于0则设置为0
          data.bid[i].price = Math.max(0, data.bid[i].price);
          data.ask[i].price = Math.max(0, data.ask[i].price);
        }
      }

      //timestamp转日期时间
      //根据day输出不同的时间表示,<3天显示时分,<=7天显示日时,<=30天显示月日,>30天显示年月

      //显示历史价格
      let enhancementLevel = document.querySelector(".MarketplacePanel_infoContainer__2mCnh .Item_enhancementLevel__19g-e")?.textContent.replace("+", "") || "0";
      let tradeName = curHridName + "_" + parseInt(enhancementLevel);
      if (trade_history[tradeName]) {
        let buy = trade_history[tradeName].buy || "无";
        let sell = trade_history[tradeName].sell || "无";
        price_info.style.display = "inline-block";
        let levelStr = enhancementLevel > 0 ? "(+" + enhancementLevel + ")" : "";
        price_info.innerHTML = `<span style="color:red">${showNumber(buy)}</span>/<span style="color:green">${showNumber(sell)}</span>${levelStr}`;
        container.style.minWidth = price_info.clientWidth + 70 + "px";

      } else {
        price_info.style.display = "none";
        container.style.minWidth = "68px";
      }

      let labels = data.bid.map(x => formatTime(x.time, day));

      chart.data.labels = labels;

      let sma = [];
      let sma_size = 6;
      let sma_window = [];
      for (let i = 0; i < data.bid.length; i++) {
        sma_window.push((data.bid[i].price + data.ask[i].price) / 2);
        if (sma_window.length > sma_size) sma_window.shift();
        sma.push(sma_window.reduce((a, b) => a + b, 0) / sma_window.length);
      }
      chart.options.plugins.title.text = curShowItemName
      chart.data.datasets = [
        {
          label: '买入',
          data: data.bid.map(x => x.price),
          borderColor: '#ff3300',
          backgroundColor: '#ff3300',
          borderWidth: 1.5
        },
        {
          label: '卖出',
          data: data.ask.map(x => x.price),
          borderColor: '#00cc00',
          backgroundColor: '#00cc00',
          borderWidth: 1.5
        },
        {
          label: '均线',
          data: sma,
          borderColor: '#ff9900',
          borderWidth: 3,
          tension: 0.5,
          fill: true
        }
      ];
      chart.setDatasetVisibility(0, config.filter.ask);
      chart.setDatasetVisibility(1, config.filter.bid);
      chart.setDatasetVisibility(2, config.filter.mean);

      chart.update()
    }
    function save_config() {

      if (chart && chart.data && chart.data.datasets && chart.data.datasets.length == 3) {
        config.filter.ask = chart.getDatasetMeta(0).visible;
        config.filter.bid = chart.getDatasetMeta(1).visible;
        config.filter.mean = chart.getDatasetMeta(2).visible;
      }
      config.x = Math.max(0, Math.min(container.getBoundingClientRect().x, window.innerWidth - 50));
      config.y = Math.max(0, Math.min(container.getBoundingClientRect().y, window.innerHeight - 50));
      if (container.style.width != "auto") {
        config.w = container.clientWidth;
        config.h = container.clientHeight;
      }

      localStorage.setItem("mooket_config", JSON.stringify(config));
    }
    setInterval(() => {
      if (document.querySelector(".MarketplacePanel_marketplacePanel__21b7o")?.checkVisibility()) {
        container.style.display = "block"
        try {
          let currentItem = document.querySelector(".MarketplacePanel_currentItem__3ercC");
          let level = currentItem?.querySelector(".Item_enhancementLevel__19g-e");
          let itemHrid = mwi.ensureItemHrid(currentItem?.querySelector(".Icon_icon__2LtL_")?.ariaLabel);
          requestItemPrice(itemHrid, cur_day, parseInt(level?.textContent.replace("+", "") || "0"))
        } catch (e) {
          console.log(e)
        }
      } else {
        container.style.display = "none"
      }
    }, 500);
    //setInterval(updateInventoryStatus, 60000);
    toggle();
    console.info("mooket 初始化完成");
  }
})();