MWITools

Tools for MilkyWayIdle. Includes a feedback center, action projections, market insights, asset history, DPS/HPS statistics, inventory tools, tasks, and guild utilities.

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 or Violentmonkey 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.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

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         MWITools
// @namespace    http://tampermonkey.net/
// @version      26.4.14
// @description  Tools for MilkyWayIdle. Includes a feedback center, action projections, market insights, asset history, DPS/HPS statistics, inventory tools, tasks, and guild utilities.
// @author       bot7420, shykai, Stella
// @license      CC-BY-NC-SA-4.0
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @match        https://www.milkywayidlecn.com/*
// @match        https://milkywayidlecn.com/*
// @match        https://amvoidguy.github.io/MWICombatSimulatorTest/*
// @match        https://shykai.github.io/MWICombatSimulatorTest/dist/*
// @match        https://mooneycalc.netlify.app/*
// @match        https://mooneycalc.vercel.app/*
// @grant        GM_addStyle
// @grant        GM.xmlHttpRequest
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_info
// @grant        unsafeWindow
// @connect      raw.githubusercontent.com
// @connect      feedback.43.167.210.211.sslip.io
// @connect      mwi-guild.43.167.210.211.sslip.io
// @require      https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.3.3/chart.umd.min.js#sha256-AaB6aVBgu9b1y80d/HEgMq4AnFJ7K/Y+9tzK1/MrvF4=
// @require      https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/2.0.1/chartjs-plugin-zoom.min.js#sha256-UDxwmAK+KFxnav4Dab9fcgZtCwwjkpGIwxWPNcAyepw=
// @require      https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js#sha256-eVNjHw5UeU0jUqPPpZHAkU1z4U+QFBBY488WvueTm88=
// ==/UserScript==

// 特别感谢:ColaCola、Zhulimoon、400badrequest、Q7

/*
    【遇到MWITools插件有问题时的解决方法】

    请先务必排查以下问题:
    1. 你的MWITools插件已更新至最新版(greasyfork网站有可能被墙,请开梯子更新;或者到QQ群文件里下载后手动导入或复制粘贴代码);
    2. 你没有重复安装插件(有的人装了新版本插件,但还有个旧版本的没有删除,在同时运行;或者有的人在同一个浏览器里装了两个油猴类浏览器插件);
    3. 安装或更新完插件后,以及在游戏设置里切换过语言后,必须刷新游戏网页;
    4. 请在电脑上、使用最新版本Chrome浏览器、使用最新版本TamperMonkey(油猴)插件尝试(作者精力有限,做不到逐个适配各种环境、为每个人定位环境问题,
       遇到问题时请优先使用上述主流环境。如果你一定要使用旧版本或其它品牌的浏览器或油猴插件,遇到问题请优先自行摸索如何解决,作者很可能无法解决你的问题。
       手机使用问题很多,作者不定位手机上问题。问问群友用什么浏览器好使,多换几个浏览器试试。苹果手机建议尝试focus浏览器。)。

    如果仍有问题,请私聊作者具体问题是什么、复现问题的具体步骤、最好附带截图;
    报错日志是定位问题的快速甚至唯一方法,请打开浏览器开发者工具查看终端,刷新游戏网页,复现遇到的问题,截图发给作者。
*/
(() => {
  var __create = Object.create;
  var __defProp = Object.defineProperty;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __getProtoOf = Object.getPrototypeOf;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __commonJS = (cb, mod) => function __require() {
    try {
      return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
    } catch (e) {
      throw mod = 0, e;
    }
  };
  var __copyProps = (to, from, except, desc) => {
    if (from && typeof from === "object" || typeof from === "function") {
      for (let key of __getOwnPropNames(from))
        if (!__hasOwnProp.call(to, key) && key !== except)
          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
    }
    return to;
  };
  var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
    // If the importer is in node compatibility mode or this is not an ESM
    // file that has been converted to a CommonJS file using a Babel-
    // compatible transform (i.e. "__esModule" has not been set), then set
    // "default" to the CommonJS "module.exports" for node compatibility.
    isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
    mod
  ));

  // node_modules/lz-string/libs/lz-string.js
  var require_lz_string = __commonJS({
    "node_modules/lz-string/libs/lz-string.js"(exports, module) {
      var LZString2 = (function() {
        var f = String.fromCharCode;
        var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
        var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
        var baseReverseDic = {};
        function getBaseValue(alphabet, character) {
          if (!baseReverseDic[alphabet]) {
            baseReverseDic[alphabet] = {};
            for (var i = 0; i < alphabet.length; i++) {
              baseReverseDic[alphabet][alphabet.charAt(i)] = i;
            }
          }
          return baseReverseDic[alphabet][character];
        }
        var LZString3 = {
          compressToBase64: function(input) {
            if (input == null) return "";
            var res = LZString3._compress(input, 6, function(a) {
              return keyStrBase64.charAt(a);
            });
            switch (res.length % 4) {
              // To produce valid Base64
              default:
              // When could this happen ?
              case 0:
                return res;
              case 1:
                return res + "===";
              case 2:
                return res + "==";
              case 3:
                return res + "=";
            }
          },
          decompressFromBase64: function(input) {
            if (input == null) return "";
            if (input == "") return null;
            return LZString3._decompress(input.length, 32, function(index) {
              return getBaseValue(keyStrBase64, input.charAt(index));
            });
          },
          compressToUTF16: function(input) {
            if (input == null) return "";
            return LZString3._compress(input, 15, function(a) {
              return f(a + 32);
            }) + " ";
          },
          decompressFromUTF16: function(compressed) {
            if (compressed == null) return "";
            if (compressed == "") return null;
            return LZString3._decompress(compressed.length, 16384, function(index) {
              return compressed.charCodeAt(index) - 32;
            });
          },
          //compress into uint8array (UCS-2 big endian format)
          compressToUint8Array: function(uncompressed) {
            var compressed = LZString3.compress(uncompressed);
            var buf = new Uint8Array(compressed.length * 2);
            for (var i = 0, TotalLen = compressed.length; i < TotalLen; i++) {
              var current_value = compressed.charCodeAt(i);
              buf[i * 2] = current_value >>> 8;
              buf[i * 2 + 1] = current_value % 256;
            }
            return buf;
          },
          //decompress from uint8array (UCS-2 big endian format)
          decompressFromUint8Array: function(compressed) {
            if (compressed === null || compressed === void 0) {
              return LZString3.decompress(compressed);
            } else {
              var buf = new Array(compressed.length / 2);
              for (var i = 0, TotalLen = buf.length; i < TotalLen; i++) {
                buf[i] = compressed[i * 2] * 256 + compressed[i * 2 + 1];
              }
              var result = [];
              buf.forEach(function(c) {
                result.push(f(c));
              });
              return LZString3.decompress(result.join(""));
            }
          },
          //compress into a string that is already URI encoded
          compressToEncodedURIComponent: function(input) {
            if (input == null) return "";
            return LZString3._compress(input, 6, function(a) {
              return keyStrUriSafe.charAt(a);
            });
          },
          //decompress from an output of compressToEncodedURIComponent
          decompressFromEncodedURIComponent: function(input) {
            if (input == null) return "";
            if (input == "") return null;
            input = input.replace(/ /g, "+");
            return LZString3._decompress(input.length, 32, function(index) {
              return getBaseValue(keyStrUriSafe, input.charAt(index));
            });
          },
          compress: function(uncompressed) {
            return LZString3._compress(uncompressed, 16, function(a) {
              return f(a);
            });
          },
          _compress: function(uncompressed, bitsPerChar, getCharFromInt) {
            if (uncompressed == null) return "";
            var i, value, context_dictionary = {}, context_dictionaryToCreate = {}, context_c = "", context_wc = "", context_w = "", context_enlargeIn = 2, context_dictSize = 3, context_numBits = 2, context_data = [], context_data_val = 0, context_data_position = 0, ii;
            for (ii = 0; ii < uncompressed.length; ii += 1) {
              context_c = uncompressed.charAt(ii);
              if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) {
                context_dictionary[context_c] = context_dictSize++;
                context_dictionaryToCreate[context_c] = true;
              }
              context_wc = context_w + context_c;
              if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) {
                context_w = context_wc;
              } else {
                if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
                  if (context_w.charCodeAt(0) < 256) {
                    for (i = 0; i < context_numBits; i++) {
                      context_data_val = context_data_val << 1;
                      if (context_data_position == bitsPerChar - 1) {
                        context_data_position = 0;
                        context_data.push(getCharFromInt(context_data_val));
                        context_data_val = 0;
                      } else {
                        context_data_position++;
                      }
                    }
                    value = context_w.charCodeAt(0);
                    for (i = 0; i < 8; i++) {
                      context_data_val = context_data_val << 1 | value & 1;
                      if (context_data_position == bitsPerChar - 1) {
                        context_data_position = 0;
                        context_data.push(getCharFromInt(context_data_val));
                        context_data_val = 0;
                      } else {
                        context_data_position++;
                      }
                      value = value >> 1;
                    }
                  } else {
                    value = 1;
                    for (i = 0; i < context_numBits; i++) {
                      context_data_val = context_data_val << 1 | value;
                      if (context_data_position == bitsPerChar - 1) {
                        context_data_position = 0;
                        context_data.push(getCharFromInt(context_data_val));
                        context_data_val = 0;
                      } else {
                        context_data_position++;
                      }
                      value = 0;
                    }
                    value = context_w.charCodeAt(0);
                    for (i = 0; i < 16; i++) {
                      context_data_val = context_data_val << 1 | value & 1;
                      if (context_data_position == bitsPerChar - 1) {
                        context_data_position = 0;
                        context_data.push(getCharFromInt(context_data_val));
                        context_data_val = 0;
                      } else {
                        context_data_position++;
                      }
                      value = value >> 1;
                    }
                  }
                  context_enlargeIn--;
                  if (context_enlargeIn == 0) {
                    context_enlargeIn = Math.pow(2, context_numBits);
                    context_numBits++;
                  }
                  delete context_dictionaryToCreate[context_w];
                } else {
                  value = context_dictionary[context_w];
                  for (i = 0; i < context_numBits; i++) {
                    context_data_val = context_data_val << 1 | value & 1;
                    if (context_data_position == bitsPerChar - 1) {
                      context_data_position = 0;
                      context_data.push(getCharFromInt(context_data_val));
                      context_data_val = 0;
                    } else {
                      context_data_position++;
                    }
                    value = value >> 1;
                  }
                }
                context_enlargeIn--;
                if (context_enlargeIn == 0) {
                  context_enlargeIn = Math.pow(2, context_numBits);
                  context_numBits++;
                }
                context_dictionary[context_wc] = context_dictSize++;
                context_w = String(context_c);
              }
            }
            if (context_w !== "") {
              if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
                if (context_w.charCodeAt(0) < 256) {
                  for (i = 0; i < context_numBits; i++) {
                    context_data_val = context_data_val << 1;
                    if (context_data_position == bitsPerChar - 1) {
                      context_data_position = 0;
                      context_data.push(getCharFromInt(context_data_val));
                      context_data_val = 0;
                    } else {
                      context_data_position++;
                    }
                  }
                  value = context_w.charCodeAt(0);
                  for (i = 0; i < 8; i++) {
                    context_data_val = context_data_val << 1 | value & 1;
                    if (context_data_position == bitsPerChar - 1) {
                      context_data_position = 0;
                      context_data.push(getCharFromInt(context_data_val));
                      context_data_val = 0;
                    } else {
                      context_data_position++;
                    }
                    value = value >> 1;
                  }
                } else {
                  value = 1;
                  for (i = 0; i < context_numBits; i++) {
                    context_data_val = context_data_val << 1 | value;
                    if (context_data_position == bitsPerChar - 1) {
                      context_data_position = 0;
                      context_data.push(getCharFromInt(context_data_val));
                      context_data_val = 0;
                    } else {
                      context_data_position++;
                    }
                    value = 0;
                  }
                  value = context_w.charCodeAt(0);
                  for (i = 0; i < 16; i++) {
                    context_data_val = context_data_val << 1 | value & 1;
                    if (context_data_position == bitsPerChar - 1) {
                      context_data_position = 0;
                      context_data.push(getCharFromInt(context_data_val));
                      context_data_val = 0;
                    } else {
                      context_data_position++;
                    }
                    value = value >> 1;
                  }
                }
                context_enlargeIn--;
                if (context_enlargeIn == 0) {
                  context_enlargeIn = Math.pow(2, context_numBits);
                  context_numBits++;
                }
                delete context_dictionaryToCreate[context_w];
              } else {
                value = context_dictionary[context_w];
                for (i = 0; i < context_numBits; i++) {
                  context_data_val = context_data_val << 1 | value & 1;
                  if (context_data_position == bitsPerChar - 1) {
                    context_data_position = 0;
                    context_data.push(getCharFromInt(context_data_val));
                    context_data_val = 0;
                  } else {
                    context_data_position++;
                  }
                  value = value >> 1;
                }
              }
              context_enlargeIn--;
              if (context_enlargeIn == 0) {
                context_enlargeIn = Math.pow(2, context_numBits);
                context_numBits++;
              }
            }
            value = 2;
            for (i = 0; i < context_numBits; i++) {
              context_data_val = context_data_val << 1 | value & 1;
              if (context_data_position == bitsPerChar - 1) {
                context_data_position = 0;
                context_data.push(getCharFromInt(context_data_val));
                context_data_val = 0;
              } else {
                context_data_position++;
              }
              value = value >> 1;
            }
            while (true) {
              context_data_val = context_data_val << 1;
              if (context_data_position == bitsPerChar - 1) {
                context_data.push(getCharFromInt(context_data_val));
                break;
              } else context_data_position++;
            }
            return context_data.join("");
          },
          decompress: function(compressed) {
            if (compressed == null) return "";
            if (compressed == "") return null;
            return LZString3._decompress(compressed.length, 32768, function(index) {
              return compressed.charCodeAt(index);
            });
          },
          _decompress: function(length, resetValue, getNextValue) {
            var dictionary = [], next, enlargeIn = 4, dictSize = 4, numBits = 3, entry = "", result = [], i, w, bits, resb, maxpower, power, c, data = { val: getNextValue(0), position: resetValue, index: 1 };
            for (i = 0; i < 3; i += 1) {
              dictionary[i] = i;
            }
            bits = 0;
            maxpower = Math.pow(2, 2);
            power = 1;
            while (power != maxpower) {
              resb = data.val & data.position;
              data.position >>= 1;
              if (data.position == 0) {
                data.position = resetValue;
                data.val = getNextValue(data.index++);
              }
              bits |= (resb > 0 ? 1 : 0) * power;
              power <<= 1;
            }
            switch (next = bits) {
              case 0:
                bits = 0;
                maxpower = Math.pow(2, 8);
                power = 1;
                while (power != maxpower) {
                  resb = data.val & data.position;
                  data.position >>= 1;
                  if (data.position == 0) {
                    data.position = resetValue;
                    data.val = getNextValue(data.index++);
                  }
                  bits |= (resb > 0 ? 1 : 0) * power;
                  power <<= 1;
                }
                c = f(bits);
                break;
              case 1:
                bits = 0;
                maxpower = Math.pow(2, 16);
                power = 1;
                while (power != maxpower) {
                  resb = data.val & data.position;
                  data.position >>= 1;
                  if (data.position == 0) {
                    data.position = resetValue;
                    data.val = getNextValue(data.index++);
                  }
                  bits |= (resb > 0 ? 1 : 0) * power;
                  power <<= 1;
                }
                c = f(bits);
                break;
              case 2:
                return "";
            }
            dictionary[3] = c;
            w = c;
            result.push(c);
            while (true) {
              if (data.index > length) {
                return "";
              }
              bits = 0;
              maxpower = Math.pow(2, numBits);
              power = 1;
              while (power != maxpower) {
                resb = data.val & data.position;
                data.position >>= 1;
                if (data.position == 0) {
                  data.position = resetValue;
                  data.val = getNextValue(data.index++);
                }
                bits |= (resb > 0 ? 1 : 0) * power;
                power <<= 1;
              }
              switch (c = bits) {
                case 0:
                  bits = 0;
                  maxpower = Math.pow(2, 8);
                  power = 1;
                  while (power != maxpower) {
                    resb = data.val & data.position;
                    data.position >>= 1;
                    if (data.position == 0) {
                      data.position = resetValue;
                      data.val = getNextValue(data.index++);
                    }
                    bits |= (resb > 0 ? 1 : 0) * power;
                    power <<= 1;
                  }
                  dictionary[dictSize++] = f(bits);
                  c = dictSize - 1;
                  enlargeIn--;
                  break;
                case 1:
                  bits = 0;
                  maxpower = Math.pow(2, 16);
                  power = 1;
                  while (power != maxpower) {
                    resb = data.val & data.position;
                    data.position >>= 1;
                    if (data.position == 0) {
                      data.position = resetValue;
                      data.val = getNextValue(data.index++);
                    }
                    bits |= (resb > 0 ? 1 : 0) * power;
                    power <<= 1;
                  }
                  dictionary[dictSize++] = f(bits);
                  c = dictSize - 1;
                  enlargeIn--;
                  break;
                case 2:
                  return result.join("");
              }
              if (enlargeIn == 0) {
                enlargeIn = Math.pow(2, numBits);
                numBits++;
              }
              if (dictionary[c]) {
                entry = dictionary[c];
              } else {
                if (c === dictSize) {
                  entry = w + w.charAt(0);
                } else {
                  return null;
                }
              }
              result.push(entry);
              dictionary[dictSize++] = w + entry.charAt(0);
              enlargeIn--;
              w = entry;
              if (enlargeIn == 0) {
                enlargeIn = Math.pow(2, numBits);
                numBits++;
              }
            }
          }
        };
        return LZString3;
      })();
      if (typeof define === "function" && define.amd) {
        define(function() {
          return LZString2;
        });
      } else if (typeof module !== "undefined" && module != null) {
        module.exports = LZString2;
      } else if (typeof angular !== "undefined" && angular != null) {
        angular.module("LZString", []).factory("LZString", function() {
          return LZString2;
        });
      }
    }
  });

  // src/core/runtime.js
  function resolveCharacterId(payload) {
    return String(
      payload?.character?.id ?? payload?.character?.characterID ?? payload?.characterID ?? payload?.characterSkills?.[0]?.characterID ?? ""
    );
  }
  function createCleanupScope() {
    const callbacks = /* @__PURE__ */ new Set();
    let cleaned = false;
    const add = (callback) => {
      if (typeof callback !== "function") return callback;
      if (cleaned) callback();
      else callbacks.add(callback);
      return callback;
    };
    return {
      add,
      event(target, type, listener, options) {
        target?.addEventListener?.(type, listener, options);
        add(() => target?.removeEventListener?.(type, listener, options));
        return listener;
      },
      observer(observer, target, options) {
        observer.observe(target, options);
        add(() => observer.disconnect());
        return observer;
      },
      interval(callback, delay) {
        const id = setInterval(callback, delay);
        add(() => clearInterval(id));
        return id;
      },
      timeout(callback, delay) {
        const id = setTimeout(() => {
          callbacks.delete(cancel);
          callback();
        }, delay);
        const cancel = () => clearTimeout(id);
        add(cancel);
        return id;
      },
      cleanup() {
        if (cleaned) return;
        cleaned = true;
        for (const callback of [...callbacks].reverse()) {
          try {
            callback();
          } catch (error) {
            console.error(
              runtime.config.isZH ? "[MWITools] 功能清理失败" : "[MWITools] Feature cleanup failed",
              error
            );
          }
        }
        callbacks.clear();
      }
    };
  }
  var featureDefinitions = /* @__PURE__ */ new Map();
  var featureStates = /* @__PURE__ */ new Map();
  var featureStatusListeners = /* @__PURE__ */ new Set();
  var runtimeStarted = false;
  var featureInitializationPaused = false;
  var activeCharacterId = "";
  function emitFeatureStatus(id) {
    const snapshot = runtime.features.getStatus(id);
    for (const listener of featureStatusListeners) {
      try {
        listener(id, snapshot);
      } catch (error) {
        console.error(
          runtime.config.isZH ? "[MWITools] 功能状态监听器执行失败" : "[MWITools] Feature status listener failed",
          error
        );
      }
    }
  }
  function setFeatureStatus(id, status, error = null) {
    const previous = featureStates.get(id) ?? {};
    featureStates.set(id, { ...previous, status, error });
    emitFeatureStatus(id);
  }
  function isFeatureEnabled(definition) {
    if (!definition.setting) return definition.defaultEnabled !== false;
    const configured = runtime.settings.get?.(definition.setting);
    return configured ?? definition.defaultEnabled !== false;
  }
  async function initializeFeature(id) {
    const definition = featureDefinitions.get(id);
    if (!definition) return false;
    const current = featureStates.get(id);
    if (current?.status === "active" || current?.status === "initializing") {
      return true;
    }
    if (!isFeatureEnabled(definition)) {
      setFeatureStatus(id, "disabled");
      return false;
    }
    if (definition.scope === "character" && !activeCharacterId) {
      setFeatureStatus(id, "waiting");
      return false;
    }
    for (const dependencyId of definition.dependsOn ?? []) {
      const dependencyReady = await initializeFeature(dependencyId);
      if (!dependencyReady) {
        setFeatureStatus(id, "waiting");
        return false;
      }
    }
    setFeatureStatus(id, "initializing");
    const scope = createCleanupScope();
    featureStates.set(id, {
      ...featureStates.get(id),
      status: "initializing",
      scope,
      instanceCleanup: null
    });
    try {
      const instanceCleanup = await definition.initialize?.({
        runtime,
        scope,
        characterId: activeCharacterId || null
      });
      const state = featureStates.get(id) ?? {};
      featureStates.set(id, {
        ...state,
        status: "active",
        error: null,
        scope,
        instanceCleanup: typeof instanceCleanup === "function" ? instanceCleanup : null
      });
      emitFeatureStatus(id);
      return true;
    } catch (error) {
      scope.cleanup();
      featureStates.set(id, {
        status: "failed",
        error,
        scope: null,
        instanceCleanup: null
      });
      console.error(
        runtime.config.isZH ? `[MWITools] 功能 ${id} 启动失败` : `[MWITools] Failed to initialize feature ${id}`,
        error
      );
      emitFeatureStatus(id);
      return false;
    }
  }
  async function disableFeature(id) {
    for (const [dependentId, dependent] of featureDefinitions) {
      if (dependent.dependsOn?.includes(id)) await disableFeature(dependentId);
    }
    const definition = featureDefinitions.get(id);
    const state = featureStates.get(id);
    if (state?.status === "active" || state?.status === "failed") {
      try {
        await state.instanceCleanup?.();
        await definition?.cleanup?.({ runtime, characterId: activeCharacterId });
      } catch (error) {
        console.error(
          runtime.config.isZH ? `[MWITools] 功能 ${id} 清理失败` : `[MWITools] Failed to clean up feature ${id}`,
          error
        );
      }
      state.scope?.cleanup();
    }
    featureStates.set(id, {
      status: "disabled",
      error: null,
      scope: null,
      instanceCleanup: null
    });
    emitFeatureStatus(id);
    return true;
  }
  var runtime = {
    api: {},
    config: {},
    settings: {},
    data: {},
    state: {},
    starts: [],
    messageHandlers: /* @__PURE__ */ new Map(),
    createCleanupScope,
    registerStart(name, start2) {
      this.starts.push({ name, start: start2 });
    },
    start() {
      featureInitializationPaused = false;
      for (const feature of this.starts) {
        try {
          const result = feature.start();
          result?.catch?.(
            (error) => console.error(
              runtime.config.isZH ? `[MWITools] 启动钩子执行失败:${feature.name}` : `[MWITools] Startup hook failed: ${feature.name}`,
              error
            )
          );
        } catch (error) {
          console.error(
            runtime.config.isZH ? `[MWITools] 启动钩子执行失败:${feature.name}` : `[MWITools] Startup hook failed: ${feature.name}`,
            error
          );
        }
      }
      runtimeStarted = true;
      return this.features.initializeAll();
    },
    onMessage(type, handler) {
      const handlers = this.messageHandlers.get(type) ?? [];
      handlers.push(handler);
      this.messageHandlers.set(type, handlers);
      return () => {
        const current = this.messageHandlers.get(type) ?? [];
        this.messageHandlers.set(
          type,
          current.filter((candidate) => candidate !== handler)
        );
      };
    },
    dispatchMessage(payload, rawMessage) {
      const handlers = [
        ...this.messageHandlers.get(payload.type) ?? [],
        ...this.messageHandlers.get("*") ?? []
      ];
      for (const handler of handlers) {
        try {
          handler(payload, rawMessage);
        } catch (error) {
          console.error(
            runtime.config.isZH ? `[MWITools] 消息 ${payload.type} 的处理器执行失败` : `[MWITools] Message handler failed for ${payload.type}`,
            error
          );
        }
      }
    },
    features: {
      register(definition) {
        if (!definition?.id || typeof definition.initialize !== "function") {
          throw new TypeError("Feature definitions need an id and initialize()");
        }
        featureDefinitions.set(definition.id, {
          scope: "global",
          defaultEnabled: true,
          ...definition
        });
        if (!featureStates.has(definition.id)) {
          featureStates.set(definition.id, {
            status: definition.scope === "character" ? "waiting" : "disabled",
            error: null
          });
        }
        if (runtimeStarted && !featureInitializationPaused) {
          void initializeFeature(definition.id);
        }
        return definition.id;
      },
      async initializeAll(scope = null) {
        for (const [id, definition] of featureDefinitions) {
          if (scope && definition.scope !== scope) continue;
          await initializeFeature(id);
        }
      },
      enable: initializeFeature,
      disable: disableFeature,
      async restart(id) {
        await disableFeature(id);
        return initializeFeature(id);
      },
      async syncSetting(settingId) {
        return this.syncSettings([settingId]);
      },
      async syncSettings(settingIds) {
        const changed = new Set(settingIds ?? []);
        const affected = /* @__PURE__ */ new Set();
        for (const [id, definition] of featureDefinitions) {
          if (changed.has(definition.setting)) affected.add(id);
        }
        let expanded = true;
        while (expanded) {
          expanded = false;
          for (const [id, definition] of featureDefinitions) {
            if (!affected.has(id) && definition.dependsOn?.some(
              (dependencyId) => affected.has(dependencyId)
            )) {
              affected.add(id);
              expanded = true;
            }
          }
        }
        if (featureInitializationPaused) return [...affected];
        for (const [id, definition] of [...featureDefinitions].reverse()) {
          if (!affected.has(id) || isFeatureEnabled(definition)) continue;
          await disableFeature(id);
        }
        for (const [id, definition] of featureDefinitions) {
          if (!affected.has(id) || !isFeatureEnabled(definition)) continue;
          await initializeFeature(id);
        }
        return [...affected];
      },
      async handleCharacterData(payload) {
        const nextCharacterId = resolveCharacterId(payload);
        if (!nextCharacterId) return;
        if (activeCharacterId && activeCharacterId !== nextCharacterId) {
          for (const [id, definition] of featureDefinitions) {
            if (definition.scope === "character") await disableFeature(id);
          }
        }
        const changed = activeCharacterId !== nextCharacterId;
        activeCharacterId = nextCharacterId;
        if (changed && !featureInitializationPaused) {
          await this.initializeAll("character");
        }
      },
      pauseInitialization() {
        featureInitializationPaused = true;
      },
      resumeInitialization() {
        featureInitializationPaused = false;
      },
      getStatus(id) {
        const state = featureStates.get(id) ?? {
          status: "unregistered",
          error: null
        };
        return {
          id,
          status: state.status,
          error: state.error?.message ?? null
        };
      },
      list() {
        return [...featureDefinitions.keys()].map((id) => this.getStatus(id));
      },
      onStatusChange(listener) {
        featureStatusListeners.add(listener);
        return () => featureStatusListeners.delete(listener);
      }
    }
  };

  // src/core/game-data.js
  var CLIENT_DATA_STATE_FIELDS = Object.freeze({
    actionDetailMap: "initData_actionDetailMap",
    levelExperienceTable: "initData_levelExperienceTable",
    enhancementLevelSuccessRateTable: "initData_enhancementLevelSuccessRateTable",
    enhancementLevelTotalBonusMultiplierTable: "initData_enhancementLevelTotalBonusMultiplierTable",
    itemDetailMap: "initData_itemDetailMap",
    itemLocationDetailMap: "initData_itemLocationDetailMap",
    houseRoomDetailMap: "initData_houseRoomDetailMap",
    actionCategoryDetailMap: "initData_actionCategoryDetailMap",
    abilityDetailMap: "initData_abilityDetailMap",
    shopItemDetailMap: "initData_shopItemDetailMap",
    taskShopItemDetailMap: "initData_taskShopItemDetailMap",
    labyrinthShopItemDetailMap: "initData_labyrinthShopItemDetailMap",
    openableLootDropMap: "initData_openableLootDropMap",
    guildBuffDetailMap: "initData_guildBuffDetailMap",
    skillDetailMap: "initData_skillDetailMap",
    buffTypeDetailMap: "initData_buffTypeDetailMap",
    combatMonsterDetailMap: "initData_combatMonsterDetailMap"
  });
  var clientData = null;
  var readyResolve;
  var warnedReadFailure = false;
  runtime.state.itemEnNameToHridMap ??= {};
  var readyPromise = new Promise((resolve) => {
    readyResolve = resolve;
  });
  function pageGlobal() {
    return globalThis.unsafeWindow ?? globalThis.window ?? globalThis;
  }
  function validClientData(value) {
    return Boolean(value?.actionDetailMap && value?.itemDetailMap);
  }
  function rebuildEnglishItemIndex(data) {
    const index = {};
    for (const [hrid, detail] of Object.entries(data?.itemDetailMap ?? {})) {
      const name = String(detail?.name ?? "").trim();
      if (name) index[name] = hrid;
    }
    runtime.state.itemEnNameToHridMap = index;
  }
  function publishClientData(data) {
    clientData = data;
    runtime.state.clientData = data;
    for (const [sourceKey, stateKey] of Object.entries(
      CLIENT_DATA_STATE_FIELDS
    )) {
      runtime.state[stateKey] = data[sourceKey] ?? null;
    }
    runtime.state.initData_monsterDetailMap = data.combatMonsterDetailMap ?? null;
    rebuildEnglishItemIndex(data);
    runtime.api.invalidateAssetValueCache?.();
    runtime.api.resetGameLocalizationCache?.();
    if (typeof globalThis.GM_setValue === "function") {
      globalThis.GM_setValue("init_client_data", JSON.stringify(data));
    }
    readyResolve?.(data);
    readyResolve = null;
    return data;
  }
  function refreshGameClientData() {
    const localStorageUtil = pageGlobal().localStorageUtil;
    if (typeof localStorageUtil?.getInitClientData !== "function") {
      return null;
    }
    let next;
    try {
      next = localStorageUtil.getInitClientData();
    } catch (error) {
      if (!warnedReadFailure) {
        warnedReadFailure = true;
        console.warn(
          "[MWITools] Could not read the game's client data cache.",
          error
        );
      }
      return null;
    }
    return validClientData(next) ? publishClientData(next) : null;
  }
  function getGameClientData() {
    return clientData;
  }
  function whenGameClientDataReady() {
    return clientData ? Promise.resolve(clientData) : readyPromise;
  }
  Object.assign(runtime.api, {
    getGameClientData,
    refreshGameClientData,
    whenGameClientDataReady
  });

  // src/core/game-assets.js
  var SPRITE_PATTERN = /((?:https?:\/\/[^/]+)?[^?#]*\/(abilities|actions|avatars|combat_monsters|items|misc|skills)_sprite(?:\.[^/#?]+)?\.svg(?:\?[^#]*)?)/i;
  var SPRITE_SOURCE_PRIORITY = Object.freeze({
    dom: 1,
    resource: 2,
    manifest: 3
  });
  var spriteBases = /* @__PURE__ */ new Map();
  var lastScanAt = Number.NEGATIVE_INFINITY;
  var spriteManifestPromise = null;
  function normalizeKind(kind) {
    const value = String(kind ?? "").toLowerCase();
    if (value === "ability") return "abilities";
    if (value === "action") return "actions";
    if (value === "avatar") return "avatars";
    if (value === "monster" || value === "monsters") return "combat_monsters";
    if (value === "item") return "items";
    if (value === "skill") return "skills";
    return value;
  }
  function registerGameSpriteSource(rawValue, options = {}) {
    const value = String(rawValue ?? "").split("#")[0];
    const match = value.match(SPRITE_PATTERN);
    if (!match) return false;
    const kind = normalizeKind(match[2]);
    const source = String(options.source ?? "dom");
    const priority = SPRITE_SOURCE_PRIORITY[source] ?? SPRITE_SOURCE_PRIORITY.dom;
    const existing = spriteBases.get(kind);
    if (!existing || priority > existing.priority || priority === existing.priority && options.replaceSamePriority === true) {
      spriteBases.set(kind, { base: match[1], priority });
    }
    return true;
  }
  function scanGameSpriteSources({ force = false } = {}) {
    const now = globalThis.performance?.now?.() ?? Date.now();
    if (!force && now - lastScanAt < 2e3) return spriteBases.size;
    lastScanAt = now;
    try {
      for (const entry of globalThis.performance?.getEntriesByType?.(
        "resource"
      ) ?? []) {
        registerGameSpriteSource(entry?.name, {
          source: "resource",
          replaceSamePriority: true
        });
      }
    } catch {
    }
    try {
      for (const node of globalThis.document?.querySelectorAll?.(
        "svg use,img[src],link[href]"
      ) ?? []) {
        registerGameSpriteSource(
          node.getAttribute?.("href") ?? node.getAttribute?.("xlink:href") ?? node.getAttribute?.("src") ?? node.currentSrc ?? node.src,
          { source: "dom" }
        );
      }
    } catch {
    }
    return spriteBases.size;
  }
  function loadGameSpriteManifest() {
    if (spriteManifestPromise) return spriteManifestPromise;
    spriteManifestPromise = (async () => {
      scanGameSpriteSources({ force: true });
      try {
        const origin = globalThis.location?.origin ?? globalThis.document?.location?.origin ?? "";
        if (!origin || typeof globalThis.fetch !== "function") {
          return spriteBases.size;
        }
        const response = await globalThis.fetch(
          new URL("/asset-manifest.json", origin).href
        );
        if (!response.ok) return spriteBases.size;
        const manifest = await response.json();
        for (const value of Object.values(manifest?.files ?? {})) {
          registerGameSpriteSource(value, {
            source: "manifest",
            replaceSamePriority: true
          });
        }
      } catch {
      }
      return spriteBases.size;
    })();
    return spriteManifestPromise;
  }
  function getGameSpriteBase(kind) {
    const normalizedKind = normalizeKind(kind);
    if (!spriteBases.has(normalizedKind)) scanGameSpriteSources();
    return spriteBases.get(normalizedKind)?.base ?? "";
  }
  function getGameSpriteHref(kind, hrid) {
    const base = getGameSpriteBase(kind);
    const symbol = String(hrid ?? "").split("/").at(-1);
    return base && symbol ? `${base}#${symbol}` : "";
  }
  function resetGameSpriteSources() {
    spriteBases.clear();
    lastScanAt = Number.NEGATIVE_INFINITY;
    spriteManifestPromise = null;
  }
  Object.assign(runtime.api, {
    getGameSpriteBase,
    getGameSpriteHref,
    registerGameSpriteSource,
    scanGameSpriteSources
  });

  // src/core/config.js
  function getGameLanguage() {
    const storedLanguage = localStorage.getItem("i18nextLng")?.trim();
    if (storedLanguage) return storedLanguage;
    return globalThis.document?.documentElement?.lang || globalThis.navigator?.language || "en-US";
  }
  function getGameNumberLocale() {
    const candidate = getGameLanguage().replaceAll("_", "-");
    try {
      return Intl.NumberFormat.supportedLocalesOf([candidate])[0] ?? "en-US";
    } catch {
      return "en-US";
    }
  }
  function getGameNumberSeparators() {
    const parts = new Intl.NumberFormat(getGameNumberLocale()).formatToParts(
      1111.1
    );
    return {
      thousand: parts.find((part) => part.type === "group")?.value ?? "",
      decimal: parts.find((part) => part.type === "decimal")?.value ?? "."
    };
  }
  function isGameLanguageZH() {
    return getGameLanguage().toLowerCase().startsWith("zh");
  }
  var isZH = isGameLanguageZH();
  var SCRIPT_COLOR_MAIN = "green";
  var SCRIPT_COLOR_TOOLTIP = "darkgreen";
  var settingsMap = {
    useOrangeAsMainColor: {
      id: "useOrangeAsMainColor",
      desc: isZH ? "使用橙色字体" : "Use orange as the main color for the script.",
      isTrue: true
    },
    displayCapMM: {
      id: "displayCapMM",
      desc: isZH ? "大数统一使用 M,不显示 B 或 T" : "Keep large values in millions instead of using B or T.",
      isTrue: false
    },
    totalActionTime: {
      id: "totalActionTime",
      desc: isZH ? "左上角显示:当前动作剩余时长和预计结束时间" : "Top left: Time remaining and expected finish time for the current action.",
      isTrue: true
    },
    actionPanel_totalTime: {
      id: "actionPanel_totalTime",
      desc: isZH ? "动作面板显示:目标等级所需次数、预计耗时和每小时经验" : "Action panel: Actions and time needed for a target level, plus XP/hour.",
      isTrue: true
    },
    actionPanel_totalTime_quickInputs: {
      id: "actionPanel_totalTime_quickInputs",
      desc: isZH ? "动作面板显示:快速输入次数 [依赖上一项]" : "Action panel: Quick input numbers. [Depends on the previous selection]",
      isTrue: true
    },
    actionPanel_foragingTotal: {
      id: "actionPanel_foragingTotal",
      desc: isZH ? "动作面板显示:采摘综合图显示综合收益 [依赖上一项]" : "Action panel: Overall profit of the foraging maps with multiple outcomes. [Depends on the previous selection]",
      isTrue: true
    },
    assetHistory: {
      id: "assetHistory",
      desc: isZH ? "库存页签显示:每日资产盈亏、历史图表和数据管理" : "Inventory tabs: Daily asset P/L, history charts, and data management.",
      isTrue: true
    },
    feedback: {
      id: "feedback",
      desc: isZH ? "总等级下方显示意见中心入口" : "Show the Feedback Center below total level.",
      isTrue: true
    },
    invWorth: {
      id: "invWorth",
      desc: isZH ? "库存搜索栏下方显示:库存和着装评分总结" : "Below inventory search bar: Inventory and gear score summary.",
      isTrue: true
    },
    includeCowbellsInAssets: {
      id: "includeCowbellsInAssets",
      desc: isZH ? "总资产计入牛铃" : "Include cowbells in total assets.",
      isTrue: false
    },
    includeGuildDungeonTokensInAssets: {
      id: "includeGuildDungeonTokensInAssets",
      desc: isZH ? "总资产计入公会与地下城代币" : "Include guild and dungeon tokens in total assets.",
      isTrue: true
    },
    valueBackEquipmentWithProtectionMirror: {
      id: "valueBackEquipmentWithProtectionMirror",
      desc: isZH ? "普通未强化背部装备按保护之镜价值估值" : "Value ordinary unenhanced back equipment using Mirrors of Protection.",
      isTrue: false
    },
    invSort: {
      id: "invSort",
      desc: isZH ? "库存显示:库存物品排序" : "Inventory: Sort inventory items.",
      isTrue: true
    },
    guildCreditConversionsSort: {
      id: "guildCreditConversionsSort",
      desc: isZH ? "公会信用兑换:显示性价比推荐" : "Guild Credit Exchange: Show value recommendations.",
      isTrue: true
    },
    profileBuildScore: {
      id: "profileBuildScore",
      desc: isZH ? "人物面板显示:战斗和生活着装评分" : "Profile panel: Combat and skilling gear scores.",
      isTrue: true
    },
    itemTooltip_prices: {
      id: "itemTooltip_prices",
      desc: isZH ? "物品悬浮窗显示:市场价值和订单簿价格" : "Item tooltip: Market value and order book prices.",
      isTrue: true
    },
    showConsumTips: {
      id: "showConsumTips",
      desc: isZH ? "物品悬浮窗显示:按市场价值计算消耗品回复性价比" : "Item tooltip: Consumable recovery efficiency based on market value.",
      isTrue: true
    },
    itemTooltip_profit: {
      id: "itemTooltip_profit",
      desc: isZH ? "物品悬浮窗显示:生产成本和利润计算 [依赖上一项]" : "Item tooltip: Production cost and profit. [Depends on the previous selection]",
      isTrue: true
    },
    itemTooltip_profitRequireKey: {
      id: "itemTooltip_profitRequireKey",
      desc: isZH ? "生产利润、宝箱估算和强化成本需要同时按住自定义按键" : "Require a shared custom held key for production profit, loot chest, and enhancement cost estimates.",
      isTrue: true
    },
    lootChestEstimate: {
      id: "lootChestEstimate",
      desc: isZH ? "战利品宝箱悬浮窗显示开箱期望价值" : "Show expected opening value for loot chests.",
      isTrue: true
    },
    lootSellAtAsk: {
      id: "lootSellAtAsk",
      desc: isZH ? "宝箱产物按卖单挂单价格估值;关闭则按买单立即卖出" : "Value chest drops at ask; off values immediate sales at bid.",
      isTrue: false
    },
    lootBuyAtAsk: {
      id: "lootBuyAtAsk",
      desc: isZH ? "钥匙和碎片按卖单立即买入;关闭则按买单挂单买入" : "Buy keys and fragments at ask; off values bid buy orders.",
      isTrue: true
    },
    lootKeyFromFragments: {
      id: "lootKeyFromFragments",
      desc: isZH ? "钥匙按碎片自制成本计算;关闭则按成品钥匙买入价" : "Use fragment crafting cost for keys; off buys finished keys.",
      isTrue: false
    },
    lootIgnoreCowbells: {
      id: "lootIgnoreCowbells",
      desc: isZH ? "宝箱估值忽略牛铃及牛铃袋的价值" : "Ignore Cowbell and Cowbell Bag value in loot estimates.",
      isTrue: false
    },
    expPercentage: {
      id: "expPercentage",
      desc: isZH ? "左侧栏显示:技能经验百分比" : "Left sidebar: Percentages of exp of the skill levels.",
      isTrue: true
    },
    battlePanel: {
      id: "battlePanel",
      desc: isZH ? "战斗总结面板(战斗时点击玩家头像)显示:平均每小时战斗次数、收入、经验" : "Battle info panel(click on player avatar during combat): Encounters/hour, revenue, exp.",
      isTrue: true
    },
    itemIconLevel: {
      id: "itemIconLevel",
      desc: isZH ? "装备图标右上角显示:装备等级" : "Top right corner of equipment icons: Equipment level.",
      isTrue: true
    },
    showsKeyInfoInIcon: {
      id: "showsKeyInfoInIcon",
      desc: isZH ? "钥匙和钥匙碎片图标右上角显示:对应的地图序号 [依赖上一项]" : "Top right corner of key/fragment icons: Corresponding combat zone index number. [Depends on the previous selection]",
      isTrue: true
    },
    marketFilter: {
      id: "marketFilter",
      desc: isZH ? "市场页面显示:装备按等级、职业、部位筛选" : "Marketplace: Filter by equipment level, class, slot.",
      isTrue: true
    },
    taskMapIndex: {
      id: "taskMapIndex",
      desc: isZH ? "任务页面显示:目标战斗地图序号" : "Tasks page: Combat zone index number.",
      isTrue: true
    },
    mapIndex: {
      id: "mapIndex",
      desc: isZH ? "战斗地图选择页面显示:地图序号" : "Combat zones page: Combat zone index number.",
      isTrue: true
    },
    skillbook: {
      id: "skillbook",
      desc: isZH ? "在物品词典显示实时技能书升级需求" : "Show live ability-book requirements in the item dictionary.",
      isTrue: true
    },
    ThirdPartyLinks: {
      id: "ThirdPartyLinks",
      desc: isZH ? "左侧菜单栏显示:第三方工具网站链接" : "Left sidebar: Links to 3rd-party websites.",
      isTrue: true
    },
    actionQueue: {
      id: "actionQueue",
      desc: isZH ? "上方动作队列菜单显示:队列中每个动作预计总时间、到何时完成" : "Queued actions panel at the top: Estimated total time and complete time of each queued action.",
      isTrue: true
    },
    enhanceSim: {
      id: "enhanceSim",
      desc: isZH ? "带强化等级的装备的悬浮菜单显示:强化模拟计算" : "Tooltip of equipment with enhancement level: Enhancing simulator calculations.",
      isTrue: true
    },
    checkEquipment: {
      id: "checkEquipment",
      desc: isZH ? "页面上方显示:战斗时穿了生产装备,或者生产时没有穿对应的生产装备而库存里有,红字警告" : "Top: Alert when fighting with production equipment equipped, or producing while matching equipment is unequipped in inventory.",
      isTrue: true
    },
    notifiEmptyAction: {
      id: "notifiEmptyAction",
      desc: isZH ? "弹窗通知:正在空闲(游戏网页打开时才有效)" : "Browser notification: Action queue is empty. (Works only when the game page is open.)",
      isTrue: false
    },
    fillMarketOrderPrice: {
      id: "fillMarketOrderPrice",
      desc: isZH ? "发布市场订单时自动填写为最小压价" : "Automatically adjust to the smallest price increase or decrease when posting marketplace orders.",
      isTrue: true
    },
    showDamage: {
      id: "showDamage",
      desc: isZH ? "启用新版 DPS、HPS、承伤、战斗片段与历史统计" : "Enable DPS, HPS, damage-taken, segment, and combat history tracking.",
      isTrue: true
    },
    battleBuffs: {
      id: "battleBuffs",
      desc: isZH ? "在战斗单位下方显示 Buff / Debuff 图标和倒计时" : "Show buff and debuff icons with countdowns below combat units.",
      isTrue: true
    },
    actionBarProfit: {
      id: "actionBarProfit",
      desc: isZH ? "动作栏显示净利润" : "Show net profit in the action bar.",
      isTrue: true
    },
    productionSummary: {
      id: "productionSummary",
      desc: isZH ? "生产面板显示产出、库存和最大可做次数" : "Show output, inventory, and maximum craftable count.",
      isTrue: true
    },
    productionProfit: {
      id: "productionProfit",
      desc: isZH ? "生产面板显示净利润" : "Show net profit in production panels.",
      isTrue: true
    },
    procurementAssistant: {
      id: "procurementAssistant",
      desc: isZH ? "启用购物车、采购计划和生产缺料加购" : "Enable the shopping cart, procurement plans, and production shortage actions.",
      isTrue: true
    },
    planningPage: {
      id: "planningPage",
      desc: isZH ? "在角色页显示独立规划计算器" : "Show the standalone Planning calculator on the character page.",
      isTrue: true
    },
    hideReadyProductionShortage: {
      id: "hideReadyProductionShortage",
      desc: isZH ? "材料充足时隐藏生产缺料提示" : "Hide the production shortage hint when materials are ready.",
      isTrue: false
    },
    taskInsights: {
      id: "taskInsights",
      desc: isZH ? "启用平铺任务布局、排序和筛选" : "Enable the flat task layout, sorting, and filters.",
      isTrue: true
    },
    semiAutoTrain: {
      id: "semiAutoTrain",
      desc: isZH ? "生产升级链显示半自动火车、逐站预填和本步加购" : "Show semi-automatic train navigation, step prefilling, and step shopping.",
      isTrue: true
    },
    taskTrainPlanner: {
      id: "taskTrainPlanner",
      desc: isZH ? "任务页按升级链合并任务并规划火车(依赖半自动火车)" : "Plan task upgrade trains by chain (requires the semi-automatic train).",
      isTrue: true
    },
    taskNewBadge: {
      id: "taskNewBadge",
      desc: isZH ? "新领取任务置顶并显示高亮和新角标" : "Pin and highlight newly received tasks.",
      isTrue: true
    },
    taskAutoReturn: {
      id: "taskAutoReturn",
      desc: isZH ? "从任务前往动作后自动返回原任务位置" : "Return to the originating task after leaving its action.",
      isTrue: true
    },
    inventoryMarketDoubleClick: {
      id: "inventoryMarketDoubleClick",
      desc: isZH ? "双击库存物品打开对应市场窗口(货币、战利品除外)" : "Double-click inventory items to open the market (except currencies and loot).",
      isTrue: true
    },
    taskMaterials: {
      id: "taskMaterials",
      desc: isZH ? "任务显示现有材料可完成数量" : "Show how much of a task your materials can complete.",
      isTrue: true
    },
    taskQueueProgress: {
      id: "taskQueueProgress",
      desc: isZH ? "任务显示队列进度" : "Show queued task progress.",
      isTrue: true
    },
    taskAutoSort: {
      id: "taskAutoSort",
      desc: isZH ? "自动整理任务顺序" : "Automatically organize tasks.",
      isTrue: true
    },
    taskIcons: {
      id: "taskIcons",
      desc: isZH ? "任务卡显示物品或怪物图标" : "Show item or monster task art.",
      isTrue: true
    },
    taskStatistics: {
      id: "taskStatistics",
      desc: isZH ? "任务页显示专业、战斗和副本统计筛选栏" : "Show profession, combat, and dungeon task filters.",
      isTrue: true
    },
    taskClaimCollector: {
      id: "taskClaimCollector",
      desc: isZH ? "集中显示可领取奖励" : "Collect claim buttons at the top.",
      isTrue: true
    },
    taskMergeActions: {
      id: "taskMergeActions",
      desc: isZH ? "合并相同动作任务数量" : "Merge matching task quantities.",
      isTrue: true
    },
    guildXpTracking: {
      id: "guildXpTracking",
      desc: isZH ? "在本机记录公会经验" : "Track guild XP locally.",
      isTrue: true
    },
    guildOverview: {
      id: "guildOverview",
      desc: isZH ? "公会总览显示经验趋势" : "Show guild XP trends.",
      isTrue: true
    },
    guildMemberXp: {
      id: "guildMemberXp",
      desc: isZH ? "成员表显示每小时经验" : "Show XP rates for guild members.",
      isTrue: true
    },
    guildLeaderboardXp: {
      id: "guildLeaderboardXp",
      desc: isZH ? "公会榜显示每小时经验" : "Show XP rates on the guild leaderboard.",
      isTrue: true
    },
    guildIdleMembers: {
      id: "guildIdleMembers",
      desc: isZH ? "公会总览显示闲置成员" : "Show idle guild members.",
      isTrue: true
    },
    leaderboardOverlay: {
      id: "leaderboardOverlay",
      desc: isZH ? "角色名字旁显示排行榜名次徽章" : "Show leaderboard rank badges beside character names.",
      isTrue: true
    },
    leaderboardXpRate: {
      id: "leaderboardXpRate",
      desc: isZH ? "排行榜显示每小时经验速率" : "Show XP rates on standard leaderboards.",
      isTrue: true
    },
    leaderboardBadgeGlint: {
      id: "leaderboardBadgeGlint",
      desc: isZH ? "前五名排行榜徽章显示扫光与闪光" : "Animate top-five leaderboard badges with a sweep and glint.",
      isTrue: true
    },
    forceMWIToolsDisplayZH: {
      id: "forceMWIToolsDisplayZH",
      desc: isZH ? "MWITools 强制显示中文" : "Always display MWITools in Chinese",
      isTrue: false
    },
    adaptIronCowMarketFeatures: {
      id: "adaptIronCowMarketFeatures",
      desc: isZH ? "铁牛角色隐藏不可用的市场与利润功能" : "Hide unavailable marketplace and profit features for Iron Cow characters.",
      isTrue: false
    }
  };
  var settingsGroups = {
    general: {
      title: { zh: "通用", en: "General" },
      summary: {
        zh: "控制 MWITools 的语言、外观、通知和常用入口。",
        en: "Control MWITools language, appearance, notifications, and shortcuts."
      }
    },
    actionBar: {
      title: { zh: "动作栏", en: "Action Bar" },
      summary: {
        zh: "在顶部查看当前动作剩余时长和预计结束时间。",
        en: "See the current action's time remaining and expected finish time."
      }
    },
    production: {
      title: { zh: "生产面板", en: "Production Panel" },
      summary: {
        zh: "输入次数后立即查看耗时、产出、库存上限和利润。",
        en: "Preview time, output, inventory limits, and profit as you enter a quantity."
      }
    },
    inventory: {
      title: { zh: "库存与资产", en: "Inventory & Assets" },
      summary: {
        zh: "整理库存,并按统一价格口径查看装备和总资产。",
        en: "Organize inventory and value gear and assets with consistent pricing."
      }
    },
    market: {
      title: { zh: "市场", en: "Marketplace" },
      summary: {
        zh: "显示市场价格、筛选装备,并减少重复下单操作。",
        en: "Show market prices, filter equipment, and streamline order entry."
      }
    },
    tasks: {
      title: { zh: "任务", en: "Tasks" },
      summary: {
        zh: "按专业整理任务;已完成任务置顶,战斗任务再按地图和地牢归类。",
        en: "Group tasks by profession, pin completed tasks, and organize combat by zone or dungeon."
      }
    },
    combat: {
      title: { zh: "战斗", en: "Combat" },
      summary: {
        zh: "查看战斗收益、实时伤害、地图编号和装备提醒。",
        en: "Review combat rewards, live damage, zone numbers, and equipment warnings."
      }
    },
    guild: {
      title: { zh: "公会", en: "Guild" },
      summary: {
        zh: "只在本机记录经验快照,展示公会进度和成员速率。",
        en: "Store XP snapshots locally to show guild progress and member rates."
      }
    },
    leaderboard: {
      title: { zh: "排行榜与排名", en: "Leaderboards & Rankings" },
      summary: {
        zh: "在角色名字旁展示技能排名,并增强排行榜数据。",
        en: "Show skill ranks beside character names and enhance leaderboard data."
      }
    },
    tools: {
      title: { zh: "外部工具", en: "External Tools" },
      summary: {
        zh: "连接战斗模拟器、计算器和第三方数据页面。",
        en: "Connect combat simulators, calculators, and third-party data tools."
      }
    }
  };
  var catalogRows = [
    [
      "feedback",
      "general",
      "意见中心",
      "Feedback Center",
      "查看版本公告、提交意见,并通过红点关注反馈回复。",
      "Read release announcements, submit feedback, and follow replies with a notification dot."
    ],
    [
      "forceMWIToolsDisplayZH",
      "general",
      "强制使用中文",
      "Always use Chinese",
      "无论游戏语言如何,都用中文显示 MWITools。",
      "Display MWITools in Chinese regardless of the game language."
    ],
    [
      "uiFontScale",
      "general",
      "插件字号",
      "MWITools font size",
      "调整 MWITools 面板、提示和角标的字号,不影响游戏原生界面。",
      "Adjust text in MWITools panels, hints, and badges without changing the game UI."
    ],
    [
      "hoverFontScale",
      "general",
      "悬浮窗口字号",
      "Tooltip panel font size",
      "调整生产利润、宝箱估值与强化成本悬浮窗口的字号,不影响游戏原生提示和页面布局。",
      "Adjust production profit, loot valuation, and enhancement cost tooltip panels without changing native game tooltips or page layout."
    ],
    [
      "useOrangeAsMainColor",
      "general",
      "使用橙色强调色",
      "Use orange accents",
      "让辅助信息更贴近游戏的暖色主题。",
      "Use a warm accent color for MWITools information."
    ],
    [
      "displayCapMM",
      "general",
      "大数只显示 M",
      "Show large values in M",
      "开启后,十亿和万亿数值继续换算为 M,例如 1.2B 显示为 1,200M;关闭时正常使用 K/M/B/T。",
      "When enabled, billions and trillions stay in millions; for example, 1.2B is shown as 1,200M. When disabled, K/M/B/T are used normally."
    ],
    [
      "notifiEmptyAction",
      "general",
      "空闲提醒",
      "Idle notification",
      "动作队列清空时发送浏览器通知;游戏页面需要保持打开。",
      "Send a browser notification when the action queue becomes empty while the game is open."
    ],
    [
      "adaptIronCowMarketFeatures",
      "general",
      "铁牛模式适配",
      "Iron Cow mode adaptation",
      "铁牛角色隐藏市场价格、交易利润和市场采购操作;资产与宝箱估值仍保留。",
      "Hide marketplace prices, trading profit, and marketplace procurement for Iron Cow characters while retaining asset and loot valuations."
    ],
    [
      "expPercentage",
      "general",
      "技能经验百分比",
      "Skill XP percentage",
      "在左侧技能进度条上显示当前等级的经验百分比。",
      "Show progress through the current level on skill bars."
    ],
    [
      "totalActionTime",
      "actionBar",
      "当前动作时间",
      "Current action timing",
      "在顶部显示剩余时长和预计结束时间。",
      "Show time remaining and the expected finish time."
    ],
    [
      "actionBarProfit",
      "actionBar",
      "当前动作利润",
      "Current action profit",
      "在顶部动作栏显示当前生产动作的市价净利润。",
      "Show market-value net profit for the current production action in the action bar."
    ],
    [
      "actionQueue",
      "actionBar",
      "完整队列时间",
      "Full queue timing",
      "计算队列中每项动作的耗时、累计完成时刻和最终结束时间。",
      "Calculate each queued action, cumulative completion times, and the final queue end time."
    ],
    [
      "actionPanel_totalTime",
      "production",
      "目标等级与经验",
      "Target level & XP",
      "输入目标等级,查看还需多少次、预计耗时和每小时经验。",
      "Enter a target level to see required actions, estimated time, and XP/hour."
    ],
    [
      "actionPanel_totalTime_quickInputs",
      "production",
      "生产次数快捷输入",
      "Production quick inputs",
      "在生产次数输入框旁提供常用次数和时长快捷按钮;依赖目标等级与经验功能。",
      "Add common count and duration shortcuts beside the production input; depends on Target level & XP."
    ],
    [
      "productionSummary",
      "production",
      "本次生产摘要",
      "Production summary",
      "选择默认折叠、始终展开或完全关闭,并实时显示产出、库存与最大可做次数。",
      "Choose collapsed, always expanded, or off while showing output, inventory, and maximum craftable count."
    ],
    [
      "productionProfit",
      "production",
      "生产净利润",
      "Production net profit",
      "同时显示市价、效率(高买低卖)和贪心(低买高卖)三种税后收入、成本与净利润。",
      "Show after-tax revenue, costs, and net profit for market, efficiency (buy high/sell low), and greedy (buy low/sell high) valuations."
    ],
    [
      "procurementAssistant",
      "production",
      "购物车与采购",
      "Shopping cart & procurement",
      "控制购物车入口、采购计划、生产缺料提示和市场采购导航;关闭后相关界面会立即移除,已有清单数据仍保留。",
      "Control the shopping cart, procurement plans, production shortage hints, and marketplace navigation. Turning it off removes the related UI while preserving saved cart data."
    ],
    [
      "planningPage",
      "production",
      "规划计算器",
      "Planning calculator",
      "控制角色页“盈亏”旁的规划入口;可独立于购物车与采购功能开启或关闭,已有规划数据会保留。",
      "Control the Planning tab beside P/L independently from shopping cart and procurement features while preserving saved planning data."
    ],
    [
      "hideReadyProductionShortage",
      "production",
      "材料充足时隐藏提示",
      "Hide ready-material hint",
      "开启后仅在材料充足时隐藏生产缺料提示;未填写数量和存在缺料时仍显示。",
      "Hide the production shortage hint only when materials are ready; keep it visible while waiting for a quantity or when materials are missing."
    ],
    [
      "actionPanel_foragingTotal",
      "production",
      "多产物采集收益",
      "Multi-output gathering value",
      "把同一采集动作的多个可能产物合并成期望收益。",
      "Combine multiple possible gathering outputs into an expected value."
    ],
    [
      "assetHistory",
      "inventory",
      "每日资产盈亏",
      "Daily asset P/L",
      "在配装右侧显示资产摘要、分项变化、历史图表和数据管理。",
      "Show asset summary, component changes, history charts, and data management beside Loadouts."
    ],
    [
      "inventoryMarketDoubleClick",
      "inventory",
      "双击打开市场",
      "Double-click to market",
      "双击库存物品打开对应市场窗口;货币和战利品不响应。",
      "Double-click an inventory item to open it in the marketplace; currencies and loot are excluded."
    ],
    [
      "invWorth",
      "inventory",
      "总资产与着装评分",
      "Assets & gear scores",
      "在库存上方显示战斗评分、生活评分以及流动和固定资产明细。",
      "Show combat and skilling scores plus current and fixed asset details above inventory."
    ],
    [
      "includeCowbellsInAssets",
      "inventory",
      "牛铃计入总资产",
      "Include cowbells in assets",
      "开启后,牛铃按市场折算价值计入不可交易代币和总资产;默认关闭。",
      "Include cowbells at their market-derived value under non-tradable tokens and total assets. Off by default."
    ],
    [
      "includeGuildDungeonTokensInAssets",
      "inventory",
      "公会与地下城代币计入总资产",
      "Include guild & dungeon tokens",
      "开启后,公会代币以及奇幻、阴森、秘法、海盗代币计入不可交易代币和总资产;默认开启。",
      "Include Guild, Chimerical, Sinister, Enchanted, and Pirate Tokens under non-tradable tokens and total assets. On by default."
    ],
    [
      "valueBackEquipmentWithProtectionMirror",
      "inventory",
      "普通背部装备按保护之镜估值",
      "Value ordinary back equipment by protection mirrors",
      "开启后,未强化、未精炼的普通背部装备(包括披风、箭袋等)按保护之镜的当前价值估值;默认关闭。所有强化背部装备(包括精炼后的背部装备)在计算强化成本时始终使用保护之镜,不受此开关影响。",
      "When enabled, ordinary unenhanced and unrefined back-slot equipment, including capes and quivers, uses the current value of a Mirror of Protection. Off by default. All enhanced back equipment, including refined back equipment, always uses Mirrors of Protection for enhancement costs regardless of this option."
    ],
    [
      "invSort",
      "inventory",
      "按价值整理库存",
      "Sort inventory by value",
      "可按市场价值、出售价或收购价整理库存,并显示整堆价值。",
      "Sort inventory by server value, ask, or bid and show each stack value."
    ],
    [
      "profileBuildScore",
      "inventory",
      "人物着装评分",
      "Profile gear scores",
      "查看自己或他人的战斗与生活着装评分。",
      "Show combat and skilling gear scores on character profiles."
    ],
    [
      "itemIconLevel",
      "inventory",
      "装备等级角标",
      "Equipment level badges",
      "在装备图标角落显示物品等级。",
      "Show item level on equipment icons."
    ],
    [
      "showsKeyInfoInIcon",
      "inventory",
      "钥匙地图编号",
      "Key zone numbers",
      "在钥匙和碎片图标上显示对应战斗地图编号。",
      "Show the related combat zone number on keys and fragments."
    ],
    [
      "itemTooltip_prices",
      "market",
      "悬浮价格",
      "Tooltip prices",
      "在物品悬浮窗显示市场价值和当前出售价、收购价。",
      "Show server value and current ask and bid prices in item tooltips."
    ],
    [
      "showConsumTips",
      "market",
      "消耗品性价比",
      "Consumable efficiency",
      "按市场价值显示回复 100 血或蓝所需的金币。",
      "Show the market-value cost to restore 100 HP or MP."
    ],
    [
      "itemTooltip_profit",
      "market",
      "悬浮生产利润",
      "Tooltip production profit",
      "在可生产物品的悬浮窗显示材料成本和预计利润。",
      "Show material cost and estimated profit for craftable items."
    ],
    [
      "itemTooltip_profitRequireKey",
      "market",
      "悬浮扩展面板需要按键",
      "Require key for tooltip panels",
      "生产利润、宝箱估算和强化成本在桌面端共用一个自定义单键;移动端均需长按。",
      "Use one shared custom held key for production profit, loot chest, and enhancement cost estimates on desktop; use a long press on touch devices."
    ],
    [
      "lootChestEstimate",
      "market",
      "宝箱价值估算",
      "Loot chest estimate",
      "在战利品宝箱悬浮窗显示概率加权的开箱期望,并可固定面板调整估值方式。",
      "Show probability-weighted opening value for loot chests and pin the panel to adjust valuation."
    ],
    [
      "lootSellAtAsk",
      "market",
      "宝箱产物挂单卖出",
      "List chest drops at ask",
      "开启:按卖单挂单价估值;关闭:按买单立即卖出价估值。",
      "On: value drops at ask; off: value immediate sales at bid."
    ],
    [
      "lootBuyAtAsk",
      "market",
      "钥匙材料立即买入",
      "Buy key materials immediately",
      "开启:按卖单立即买入钥匙或碎片;关闭:按买单挂单买入。",
      "On: buy keys or fragments at ask; off: place buy orders at bid."
    ],
    [
      "lootKeyFromFragments",
      "market",
      "钥匙碎片自制",
      "Craft keys from fragments",
      "开启:按实际配方、工匠减耗、浓缩倍率和泡饮成本自制钥匙;关闭:购买成品钥匙。",
      "On: craft keys using the recipe, Artisan reduction, concentration, and drink costs; off: buy finished keys."
    ],
    [
      "lootIgnoreCowbells",
      "market",
      "宝箱估值忽略牛铃",
      "Ignore Cowbells in loot",
      "开启后,所有直接或嵌套宝箱中的牛铃和牛铃袋均保留掉落显示,但价值按零计算。",
      "Keep Cowbell and Cowbell Bag drops visible but value them at zero in direct and nested loot estimates."
    ],
    [
      "marketFilter",
      "market",
      "装备筛选",
      "Equipment filters",
      "在市场按等级、战斗职业和装备部位筛选物品。",
      "Filter marketplace equipment by level, combat class, and slot."
    ],
    [
      "fillMarketOrderPrice",
      "market",
      "自动填写订单价格",
      "Auto-fill order prices",
      "创建订单时按最小有效档位匹配或压过当前最优价格。",
      "Fill the smallest valid price step that matches or improves the current best order."
    ],
    [
      "taskInsights",
      "tasks",
      "平铺任务布局",
      "Flat task layout",
      "按新任务、已完成、普通任务和专业顺序平铺显示;战斗任务统一置底并按地图排序。",
      "Show a flat list ordered by new, completed, and normal tasks, with combat last and sorted by zone."
    ],
    [
      "semiAutoTrain",
      "tasks",
      "半自动火车",
      "Semi-automatic train",
      "沿制造与升级链逐站跳转、预填次数;点击游戏原生加入队列后前往下一站,并可把本步净缺料加入购物车。",
      "Navigate production and upgrade chains, prefill counts, advance after the native queue action, and add each stop's net shortages to the cart."
    ],
    [
      "taskTrainPlanner",
      "tasks",
      "任务火车规划",
      "Task train planning",
      "聚合同一升级链的未完成任务,在最高级任务卡生成火车计划;依赖半自动火车。",
      "Combine unfinished tasks from the same upgrade chain and start the plan from its highest-level task; requires the semi-automatic train."
    ],
    [
      "taskNewBadge",
      "tasks",
      "新任务置顶与标记",
      "New task group and badges",
      "本次进入任务页期间,将新领取任务置于最顶部的新任务分类并显示黄色角标;再次进入任务页时恢复正常分类。",
      "Place newly received tasks in a highlighted top group for the current task-page visit, then restore their normal groups on the next visit."
    ],
    [
      "taskAutoReturn",
      "tasks",
      "任务自动返回",
      "Task auto-return",
      "从任务卡前往动作后,在提交或关闭动作时返回原任务分类和位置。",
      "Return to the originating task group and position after submitting or closing its action."
    ],
    [
      "taskMaterials",
      "tasks",
      "任务材料完成量",
      "Task material capacity",
      "根据库存中的直接材料显示每项任务当前可完成的数量。",
      "Show how much of each task can be completed from current direct-material inventory."
    ],
    [
      "taskQueueProgress",
      "tasks",
      "任务队列进度",
      "Task queue progress",
      "在任务卡上显示已经进入动作队列的数量和进度。",
      "Show the quantity and progress already placed in the action queue on task cards."
    ],
    [
      "taskAutoSort",
      "tasks",
      "自动整理任务",
      "Automatically organize tasks",
      "在每个专业分组内部按等级和生产链整理任务。",
      "Sort tasks within each profession by level and production chain."
    ],
    [
      "taskIcons",
      "tasks",
      "任务背景图标",
      "Task artwork",
      "用低透明度原生图标标识任务物品、怪物和副本。",
      "Use subtle native item, monster, and dungeon artwork on task cards."
    ],
    [
      "taskStatistics",
      "tasks",
      "任务统计筛选栏",
      "Task statistics filters",
      "显示全部任务、十个生活专业、战斗和四个副本的数量,并可按图标筛选。",
      "Show counts for all tasks, ten professions, combat, and four dungeons, with icon filters."
    ],
    [
      "taskClaimCollector",
      "tasks",
      "集中领取任务奖励",
      "Collect task rewards",
      "把当前可领取任务集中显示在任务页顶部,减少逐项查找。",
      "Collect currently claimable tasks at the top of the task page for faster claiming."
    ],
    [
      "taskMergeActions",
      "tasks",
      "合并相同任务动作",
      "Merge matching task actions",
      "打开动作时自动把多个相同任务的剩余数量合并到输入框。",
      "Pre-fill the combined remaining quantity for matching active tasks."
    ],
    [
      "taskMapIndex",
      "tasks",
      "战斗任务地图编号",
      "Combat task zone number",
      "在战斗任务标题旁显示目标怪物所在地图编号。",
      "Show the target monster's zone number on combat tasks."
    ],
    [
      "battlePanel",
      "combat",
      "战斗总结",
      "Combat summary",
      "战斗结束后查看遭遇次数、收益和经验速率。",
      "Review encounter, revenue, and XP rates after combat."
    ],
    [
      "showDamage",
      "combat",
      "DPS / HPS / 承伤统计",
      "DPS / HPS / Damage Taken",
      "记录实时伤害、治疗、承伤、战斗片段和历史;详细显示选项在 DPS 面板内设置。",
      "Track damage, healing, damage taken, segments, and history; configure display details in the DPS panel."
    ],
    [
      "battleBuffs",
      "combat",
      "战斗 Buff 显示",
      "Battle buff display",
      "在每个战斗单位下方显示增益和减益图标,带剩余时间倒计时环。",
      "Show buff and debuff icons with countdown rings below each combat unit."
    ],
    [
      "mapIndex",
      "combat",
      "战斗地图编号",
      "Combat zone numbers",
      "在战斗地图选择页显示连续编号。",
      "Show sequential numbers in the combat zone selector."
    ],
    [
      "checkEquipment",
      "combat",
      "错装提醒",
      "Equipment warning",
      "战斗穿生产装或生产漏穿库存中的对应装备时发出提醒。",
      "Warn about skilling gear in combat or useful unequipped gear while skilling."
    ],
    [
      "enhanceSim",
      "combat",
      "强化模拟",
      "Enhancement simulator",
      "在强化装备悬浮窗中估算成功次数、保护策略和总成本。",
      "Estimate attempts, protection strategy, and total cost for enhanced equipment."
    ],
    [
      "guildXpTracking",
      "guild",
      "本地记录公会经验",
      "Local guild XP tracking",
      "被动保存服务器发来的经验快照;数据只留在本机,保留 30 天。",
      "Passively store server XP snapshots on this device for 30 days."
    ],
    [
      "guildOverview",
      "guild",
      "公会经验总览",
      "Guild XP overview",
      "显示 24 小时速率、升级时间与 6 小时滚动平均的 7 天趋势。",
      "Show the 24-hour rate, time to level, and a seven-day trend using a 6-hour rolling average."
    ],
    [
      "guildMemberXp",
      "guild",
      "成员经验速率",
      "Member XP rates",
      "在成员表增加近 6 小时、24 小时和本周平均 XP/h。",
      "Add 6-hour, 24-hour, and this-week average XP/h columns to the member table."
    ],
    [
      "guildLeaderboardXp",
      "guild",
      "公会榜经验速率",
      "Guild leaderboard XP rates",
      "在全服公会榜显示本机采样得到的近 6 小时和 24 小时 XP/h。",
      "Show locally sampled 6-hour and 24-hour XP/h on the guild leaderboard."
    ],
    [
      "guildIdleMembers",
      "guild",
      "闲置成员",
      "Idle members",
      "在公会总览常显当前未进行动作的可见成员。",
      "Always show visible guild members who are not currently performing an action."
    ],
    [
      "leaderboardOverlay",
      "leaderboard",
      "排行榜名次徽章",
      "Leaderboard rank badges",
      "在角色名字旁显示信息采集助手提供的技能与名望排行榜名次徽章。",
      "Show skill and fame leaderboard rank badges supplied by the data collector beside character names."
    ],
    [
      "leaderboardXpRate",
      "leaderboard",
      "排行榜经验速率",
      "Leaderboard XP rates",
      "在全服技能排行榜增加只读的经验/小时列;此开关不影响名次徽章或排行顺序。",
      "Add a read-only XP/hour column to standard skill leaderboards without affecting rank badges or row order."
    ],
    [
      "leaderboardBadgeGlint",
      "leaderboard",
      "前五名徽章闪光",
      "Top-five badge glint",
      "前五名彩色徽章默认用一秒横扫白光、一秒右上角呼吸闪光和三秒停顿组成循环;可在此关闭。",
      "Top-five rainbow badges default to a one-second white sweep, a one-second upper-right breathing glint, and a three-second pause. Turn this off to disable the effect."
    ],
    [
      "guildCreditConversionsSort",
      "guild",
      "公会信用兑换性价比推荐",
      "Guild credit exchange recommendations",
      "按市场出售价计算兑换相同信用点的真实材料成本,并比较当前方案与最优方案。",
      "Compare the selected conversion with the cheapest way to obtain the same credits using market asks."
    ],
    [
      "ThirdPartyLinks",
      "tools",
      "第三方工具入口",
      "External tool shortcuts",
      "在左侧菜单提供模拟器、计算器和第三方数据入口。",
      "Add sidebar shortcuts for simulators, calculators, and third-party data."
    ],
    [
      "skillbook",
      "tools",
      "技能书计算器",
      "Ability book calculator",
      "在物品词典实时计算技能书解锁、升级所需本数与参考购买成本。",
      "Calculate live ability-book unlock, leveling, and reference purchase requirements in the item dictionary."
    ]
  ];
  var settingsCatalog = Object.fromEntries(
    catalogRows.map(([id, group, zhTitle, enTitle, zhSummary, enSummary]) => [
      id,
      {
        id,
        group,
        title: { zh: zhTitle, en: enTitle },
        summary: { zh: zhSummary, en: enSummary },
        details: { zh: zhSummary, en: enSummary }
      }
    ])
  );
  settingsCatalog.productionSummary.control = {
    type: "select",
    preference: "productionSummaryMode",
    options: [
      ["collapsed", { zh: "默认折叠", en: "Collapsed" }],
      ["expanded", { zh: "始终展开", en: "Always expanded" }],
      ["off", { zh: "关闭", en: "Off" }]
    ]
  };
  settingsCatalog.uiFontScale.control = {
    type: "select",
    preference: "uiFontScale",
    options: [
      ["standard", { zh: "标准", en: "Standard" }],
      ["large", { zh: "较大", en: "Large" }],
      ["largest", { zh: "最大", en: "Largest" }]
    ]
  };
  settingsCatalog.hoverFontScale.control = {
    type: "select",
    preference: "hoverFontScale",
    options: [
      ["standard", { zh: "标准", en: "Standard" }],
      ["large", { zh: "较大", en: "Large" }],
      ["largest", { zh: "最大", en: "Largest" }]
    ]
  };
  settingsCatalog.productionQuickHours = Object.freeze({
    id: "productionQuickHours",
    group: "production",
    hidden: true,
    control: Object.freeze({
      type: "text",
      preference: "productionQuickHours"
    })
  });
  settingsCatalog.productionQuickCounts = Object.freeze({
    id: "productionQuickCounts",
    group: "production",
    hidden: true,
    control: Object.freeze({
      type: "text",
      preference: "productionQuickCounts"
    })
  });
  var settingParents = {
    actionBarProfit: "totalActionTime",
    actionQueue: "totalActionTime",
    actionPanel_foragingTotal: "actionPanel_totalTime",
    productionSummary: "actionPanel_totalTime",
    productionProfit: "actionPanel_totalTime",
    hideReadyProductionShortage: "procurementAssistant",
    showsKeyInfoInIcon: "itemIconLevel",
    showConsumTips: "itemTooltip_prices",
    itemTooltip_profit: "itemTooltip_prices",
    itemTooltip_profitRequireKey: "itemTooltip_prices",
    lootChestEstimate: "itemTooltip_prices",
    lootSellAtAsk: "lootChestEstimate",
    lootBuyAtAsk: "lootChestEstimate",
    lootKeyFromFragments: "lootChestEstimate",
    lootIgnoreCowbells: "lootChestEstimate",
    taskMaterials: "taskInsights",
    taskQueueProgress: "taskInsights",
    taskAutoSort: "taskInsights",
    taskIcons: "taskInsights",
    taskStatistics: "taskInsights",
    taskClaimCollector: "taskInsights",
    taskMergeActions: "taskInsights",
    taskNewBadge: "taskInsights",
    taskTrainPlanner: "semiAutoTrain",
    guildOverview: "guildXpTracking",
    guildMemberXp: "guildXpTracking",
    guildLeaderboardXp: "guildXpTracking",
    guildIdleMembers: "guildOverview",
    leaderboardBadgeGlint: "leaderboardOverlay"
  };
  for (const [id, parent] of Object.entries(settingParents)) {
    if (settingsCatalog[id]) settingsCatalog[id].parent = parent;
  }
  var settingListeners = /* @__PURE__ */ new Map();
  var preferenceListeners = /* @__PURE__ */ new Map();
  function normalizeTextPreference(value, fallback) {
    if (typeof value !== "string" && typeof value !== "number") return fallback;
    const normalized = String(value).trim().slice(0, 256);
    return normalized || fallback;
  }
  var preferenceDefinitions = Object.freeze({
    productionSummaryMode: Object.freeze({
      defaultValue: "collapsed",
      values: Object.freeze(["collapsed", "expanded", "off"])
    }),
    uiFontScale: Object.freeze({
      defaultValue: "standard",
      values: Object.freeze(["standard", "large", "largest"])
    }),
    hoverFontScale: Object.freeze({
      defaultValue: "standard",
      values: Object.freeze(["standard", "large", "largest"])
    }),
    productionQuickHours: Object.freeze({
      defaultValue: "0.5,1,2,3,4,5,6,10,12,24",
      normalize(value) {
        return normalizeTextPreference(value, this.defaultValue);
      }
    }),
    productionQuickCounts: Object.freeze({
      defaultValue: "10,100,300,500,1000,2000",
      normalize(value) {
        return normalizeTextPreference(value, this.defaultValue);
      }
    })
  });
  var preferenceValues = Object.fromEntries(
    Object.entries(preferenceDefinitions).map(([id, definition]) => [
      id,
      definition.defaultValue
    ])
  );
  function getSetting(id) {
    return settingsMap[id]?.isTrue;
  }
  function normalizePreference(id, value) {
    const definition = preferenceDefinitions[id];
    if (!definition) return void 0;
    if (typeof definition.normalize === "function") {
      return definition.normalize(value);
    }
    return definition.values.includes(value) ? value : definition.defaultValue;
  }
  function getPreference(id) {
    return preferenceValues[id];
  }
  async function setPreference(id, value, options = {}) {
    const normalized = normalizePreference(id, value);
    if (normalized === void 0) return false;
    const previous = preferenceValues[id];
    preferenceValues[id] = normalized;
    if (previous === normalized && !options.force) return true;
    if (options.persist !== false) runtime.api.persistSettings?.();
    for (const listener of preferenceListeners.get(id) ?? []) {
      try {
        listener(normalized, previous);
      } catch (error) {
        console.error(
          isZH ? `[MWITools] 偏好设置 ${id} 的监听器执行失败` : `[MWITools] Preference listener failed for ${id}`,
          error
        );
      }
    }
    return true;
  }
  function onPreferenceChange(id, listener) {
    const listeners2 = preferenceListeners.get(id) ?? /* @__PURE__ */ new Set();
    listeners2.add(listener);
    preferenceListeners.set(id, listeners2);
    return () => listeners2.delete(listener);
  }
  function isIronCowCharacter() {
    return ["ironcow", "legacy_ironcow"].includes(
      String(runtime.state.currentCharacterGameMode ?? "").toLowerCase()
    );
  }
  function shouldSuppressMarketFeatures() {
    return Boolean(
      settingsMap.adaptIronCowMarketFeatures?.isTrue && isIronCowCharacter()
    );
  }
  async function setSetting(id, value, options = {}) {
    if (!settingsMap[id]) return false;
    const normalized = Boolean(value);
    const previous = settingsMap[id].isTrue;
    settingsMap[id].isTrue = normalized;
    if (previous === normalized && !options.force) return true;
    if (options.persist !== false) runtime.api.persistSettings?.();
    for (const listener of settingListeners.get(id) ?? []) {
      try {
        listener(normalized, previous);
      } catch (error) {
        console.error(
          isZH ? `[MWITools] 设置 ${id} 的监听器执行失败` : `[MWITools] Setting listener failed for ${id}`,
          error
        );
      }
    }
    await runtime.features.syncSetting(id);
    return true;
  }
  async function applySettingsBatch({ values = {}, preferences = {} } = {}, options = {}) {
    const settingChanges = [];
    const preferenceChanges = [];
    const nextValues = { ...values };
    if (Object.hasOwn(preferences, "productionSummaryMode") && !Object.hasOwn(nextValues, "productionSummary")) {
      nextValues.productionSummary = normalizePreference(
        "productionSummaryMode",
        preferences.productionSummaryMode
      ) !== "off";
    }
    for (const [id, value] of Object.entries(nextValues)) {
      if (!settingsMap[id]) {
        throw new TypeError(`Unknown MWITools setting: ${id}`);
      }
      if (typeof value !== "boolean") {
        throw new TypeError(`MWITools setting ${id} must be boolean`);
      }
    }
    for (const [id, value] of Object.entries(preferences)) {
      if (!preferenceDefinitions[id]) {
        throw new TypeError(`Unknown MWITools preference: ${id}`);
      }
      const definition = preferenceDefinitions[id];
      if (definition.values && !definition.values.includes(value) || definition.normalize && typeof value !== "string") {
        throw new TypeError(`Invalid MWITools preference value for ${id}`);
      }
    }
    for (const [id, value] of Object.entries(nextValues)) {
      const normalized = value;
      const previous = settingsMap[id].isTrue;
      settingsMap[id].isTrue = normalized;
      if (previous !== normalized || options.force) {
        settingChanges.push({ id, value: normalized, previous });
      }
    }
    for (const [id, value] of Object.entries(preferences)) {
      const normalized = normalizePreference(id, value);
      if (normalized === void 0) continue;
      const previous = preferenceValues[id];
      preferenceValues[id] = normalized;
      if (previous !== normalized || options.force) {
        preferenceChanges.push({ id, value: normalized, previous });
      }
    }
    if (options.persist !== false) runtime.api.persistSettings?.();
    for (const change of preferenceChanges) {
      for (const listener of preferenceListeners.get(change.id) ?? []) {
        try {
          listener(change.value, change.previous);
        } catch (error) {
          console.error(
            isZH ? `[MWITools] 偏好设置 ${change.id} 的监听器执行失败` : `[MWITools] Preference listener failed for ${change.id}`,
            error
          );
        }
      }
    }
    for (const change of settingChanges) {
      for (const listener of settingListeners.get(change.id) ?? []) {
        try {
          listener(change.value, change.previous);
        } catch (error) {
          console.error(
            isZH ? `[MWITools] 设置 ${change.id} 的监听器执行失败` : `[MWITools] Setting listener failed for ${change.id}`,
            error
          );
        }
      }
    }
    await runtime.features.syncSettings(settingChanges.map(({ id }) => id));
    return {
      settings: settingChanges.map(({ id }) => id),
      preferences: preferenceChanges.map(({ id }) => id)
    };
  }
  function onSettingChange(id, listener) {
    const listeners2 = settingListeners.get(id) ?? /* @__PURE__ */ new Set();
    listeners2.add(listener);
    settingListeners.set(id, listeners2);
    return () => listeners2.delete(listener);
  }
  Object.defineProperties(runtime.config, {
    THOUSAND_SEPERATOR: {
      enumerable: true,
      get() {
        return getGameNumberSeparators().thousand;
      }
    },
    DECIMAL_SEPERATOR: {
      enumerable: true,
      get() {
        return getGameNumberSeparators().decimal;
      }
    },
    NUMBER_LOCALE: {
      enumerable: true,
      get() {
        return getGameNumberLocale();
      }
    },
    isZHInGameSetting: {
      enumerable: true,
      get() {
        return isGameLanguageZH();
      }
    },
    gameLanguage: {
      enumerable: true,
      get() {
        return getGameLanguage();
      }
    },
    isZH: {
      enumerable: true,
      get() {
        return isZH;
      },
      set(value) {
        isZH = value;
      }
    },
    SCRIPT_COLOR_MAIN: {
      enumerable: true,
      get() {
        return SCRIPT_COLOR_MAIN;
      },
      set(value) {
        SCRIPT_COLOR_MAIN = value;
      }
    },
    SCRIPT_COLOR_TOOLTIP: {
      enumerable: true,
      get() {
        return SCRIPT_COLOR_TOOLTIP;
      },
      set(value) {
        SCRIPT_COLOR_TOOLTIP = value;
      }
    },
    MARKET_API_URL: {
      enumerable: true,
      get() {
        return runtime.api.getMarketApiUrl?.() ?? "";
      }
    }
  });
  Object.defineProperties(runtime.settings, {
    settingsMap: {
      enumerable: true,
      get() {
        return settingsMap;
      },
      set(value) {
        settingsMap = value;
      }
    },
    groups: {
      enumerable: true,
      get() {
        return settingsGroups;
      }
    },
    catalog: {
      enumerable: true,
      get() {
        return settingsCatalog;
      }
    },
    preferenceDefinitions: {
      enumerable: true,
      get() {
        return preferenceDefinitions;
      }
    }
  });
  Object.assign(runtime.settings, {
    get: getSetting,
    set: setSetting,
    applyBatch: applySettingsBatch,
    onChange: onSettingChange,
    getPreference,
    setPreference,
    onPreferenceChange
  });
  Object.assign(runtime.api, {
    isIronCowCharacter,
    shouldSuppressMarketFeatures
  });
  runtime.registerStart("core/config.js", () => {
    console.log(window.location.href);
  });

  // src/data/market-backup.json
  var market_backup_default = "N4IgtghgTg1gpgFwCIQREAuUB6AlguMAZ2wgCMBPIoiAGwH05q4A7AYzk1AAYuR0MAJgBs3ADQgymQQFYZAX3kS8BYqTZQA9mVS429ABabNAEz68s/TAHZud++MmZhADgd2lIQXwEBaAIwSUhgybu6eAMw+mAFBmKHu3J4ALNEYsU4hYQ6eMmnWyYlxGMKi4RLCaRnBMmU5EtZpAJyJjsEuheUgLmn+yTJFmf7+rZ5NVYFDA4meI2mC2Q7FgtbT9SDDE8URnV3+3pZ+k8FNdTmeKoQkEBrauvpGpvRQcABmuCxwZpZ5hzHHmAiZySEjmfyyrWKMl26024OqgOBimU+Cu6i0OgQRGeuDIZE0LHMaVc7mKpXCF1RaggJgAbhB2F96HQ2AZCBR6KzoGAieCIi1SZkArM0slFnZisKJAdQAJoYNglKQFFwTJBArASMuqlwQUNel/Lleur9cLKaprnSGRwTMyEGg2DBOQZubzZTYYW1nJ6ReDhvrbF0ZVYMP5gV6ME1xZFek01hKhuKQSAde6MPzTYaJL807V9clw8iQJdqVbGbayC8AO4fADmztdlgsabD+r6M2l8zkgsVWeVaQiWqWQr7qZD5J7gKTRr93HjCd7RZLlvp5edcCYTDA+AMdYbUB5TfmEXnxX285j4OSJuHi4kY4E4YjSpzIaTEbVM3NaJpq5tnOMGA9y5A83RDBEhDnPYB0hEdPGDAQM0nA1LzTCIk0lPtXwEFwb3sYo9QpFELVIMt/w0CBXgQYCXVAo9wRWU9MkET1k38eY42QpUEMRNsBXWFU0wLTMUjSFwMLg79Sz/JkTDeVgiDgfdDx4fJ9QnexfRbId8MyJo8OTHiDWKXCukEkN/E429MH8FwL2zMSDIjEkiOLKkV2tJlWBddgaMbVTVQk4IIicrSQ0I6yUIkcyBFOESHL9ecFxs1okikjy11eTQoAgWs/LogK00EHTktDOzoIYoL/lQkN0PilNiSczCZ20pKTNGBotiGJF0tImTbQ+AhaFoXBa1YDhlLAgQItK4QfVBXoIgDDt+wY/wmOCWz7Ia+EAXTQN1mwmwqpKULet/TzbUgPL9BAlSQGbCz1uQkqPAWxK22egT5iaoZ+M0+9RV+4Kkpa8C9pc85iJ/MimTAOBaA3Sb6LTSHZqRd603+0r5XWIz/BPZDTO+v1ysi/xVm1UVsYjQQabBuUTqStLoeky76G3WggJYes7qm6Ru0i9VtvYyr6qMhZ6piwElq40diXDKEOpAI7IxOs1WYy/8cp5pk+ZR8dxTJeaNl6WC7y8Rb9WnaL5nNmzhG2h9ARp7ZCwStMZtKiJZahtySIutciDQbcWAgZHCos/oXtYsKjjPULOz9KzdM22QzJg/URCdtI82Q5JY49t9ge9L9Nb69m0FwWhsvy+7HoEYq21BzGQ0luX4IHdW+2ljBWNp+nAfBNHSpfZpFcyOay/9mH+voKtjBMNgAFd7Tr/mhBL0MW9Nvlu5qgR++a87aFZQhcGD7F8XtTRiA3iCvq6VXZDa5j066SpVVS5rOoYx2s+Zp4HofJXq0wOrCBumAxT2z7v3Jc7lSCnzZNuS+9AECaAAA73z2o/WEXVgrRltrtbY21n6gLPO+Twn8iq+0ipQ3+RUj7MVAUA+Y4DU7SDgaCSBsCYF1WnsuRBZ8wAciYIpRkG9OjbBaPAgOSD2RoLgAISOOFRAmXVLIn8YAyBwCgBvYq0xlhDk0dSeGCADBUAQBvHYHD0y+xMdcdgRgoD4mXiwW0GDaCoCUviEwFAN5xWQvpFaos0wQQ6CtIy4TC6rTCXtCJVNiGZBcO7FWvRyFDFSlQ/BmAabJkaH6d+5MnLJmAS2dhpU+g73GEkza9CNg8OSBk4KeTZj41oZFQJrlBHWmca49x9BPHePoL4jkLx3ifG+KAMEcTijRz9j0px2V+keK8QQegiNaxEA3tYCewRrCJ13rMyeNtLa1JsMrZ2xlJ6pNVk0ViJkYnUPBgRQ5BSWxQSCacspFlmYJ2VjM2qFTnyGnOkslxmg3GrOGZs7E4yPhfC4OdDBnjOAG0bhEIxDjSAosRvQWsy8wAiI3pZM81hsUQFxUpCgmgCVQCsei+IpUC4sxntSAAjsvcO0AtBVmsb7bYN4KWcvDmI8aaKVGAlpgoc6OUyB6HDmwTQrxXhIx0Qyax6ptgjApVANgDIfGaD5Yy0M+oox4MKfGH+ZyWyemtb3AmHch4tkFqPLCvR/62KKcmZ5AhHVOpAO835IlZX6s+JyLQ1B8TGslRgXZkUZpx0wEbOCSc0zE29v9A+yaYRzMSR8/OeEGY2Xmcys42SrzsLmeWhhFkYFKkBdNLeGs2XXD1Qa+g7wXj0GDpRV4OyzgERhEm65wUa02osgZe1YlkKEwBjtdNubMjJCzUXWKeE5nDtDR2mutZJFLTmTq7d4baCEp0Xok1ggTRGIGLqsNSkw4IGXt23tKqdmLAIuO0Jb4P3LvjJ3cE4lIpKl7kB2xSormvWrYdRaVrMldCDaoom46alYzNd0hB0B709t3AjKZD1c4buXau2JdaA1XPNeB91lU4PtCLRUHJIQt3lywx2qs3ioA9rQG+k11hf37K/Y5YDfYjLxqo9mjAYG3XFtDJ6nGGGA70ovgyRRyiCN+kpmSjw517Q3CdEqlVG4AmlSAxSgAXs+pSdBCDyrYBvXGEYWVvSOU9V+Fte6CFocfFjFnu34k0FifRZNlh1BHUCcTzqLIp28620gvmfFQGXkQAwkihzavnd+x8uxljpwA+moWuWiEFoiyAVD4UiOLnOvFkZy9HSIwvbGpCEYQoZbUiVoyKSSsOp0ta1WuCYs9Oq2QZetB2OwHs5FUtMmNrVSq5Z9cm4AmFGKME8z83WQXwRvZuogrWt+kfta/GFXqpFbbnOkFa3u3eU8g19TqNsjSPnb3f61rINFIG5h6rtYIBuIQIjQLl7PXLHsa3Q+B1DuMc87e07jdPkXci43Wjs2fPzZdES3R9nsuZAiAdEd3rlhQTyy85ihOYdQOQsLfJucO5ze7S6BAZ8GWxugd7ULZP0wvdTWkvewnLtKTZLQMx1i1i7ec5lqBIuudRL2nTbTKPu2QA4Jj2xK69utS68aXntOlKcxgCSuMFCZXy4fRuEwiNJGS+CE0tXIZKPw9Ixiy3J3udoS85JY3gy1mGr8cr5luPQfxCxxbfGcOCLZFEgxANz9KbSe157mFcAtmSJ28u3YePyOZxK6rZIbvKse4wQFpbKuQcucbnJiHGmndRQdwLLXHvktKKgNsk1wlmVCoD+mZtImga8/Z3x8TceiAYNQCNtTPCHNzLWETv1SPq8efOwT6HC7ifBEpxW45q/CeD4wY3+zN4lY279fW7vs4Q314XlAfD4+U81Hb6XmyFSK9FQz3yKPvQY8Xd6joesyr6AjAAlWDosNM3rGg5h+ApmiDoNosYCMsYADrGi/KVJTmxKKDfs7kZBpNOn6IOlzpBgdlzncvvlzr6s4Pxs7ohhcr3t0JrnXrFlAf5iMjrKyPojIMsMkNivQTAa8OQFAHoJqgPKyoIpwZoPijXLSEwPokHjZGTCOjgcHmJGgXPrGFQW9lQarPwtJgxsPEunnnQRANASIfTtYkLPMiOiVE/hZAvlzr3IOLQUIfoQwRiIajfHfCajjrYjYs5vjHIc7jYTTuXMIc8NoEpOglgi3lIX3Hft4VQR5sdihF/hAEQHoMyPIiIhHHdiGA8tjibOLqGN6p/gEYkckagA6PprRPXGJDocmvRvfpGD4dXkZKrjEfkHagQfMGTAPoUUkfoJWHADWDzOkTwrZC9HJoZPkCmrofYd0QtopEQNuOYuvJeh0bTKMRwUUbdIBIsY1lnBjBOjPmfnodMRRFRFsRkYhK0ZMQgjoNMXJKqiwIpIMQoXQsOh3ueP4Ycckddr5AMfrLGhmqVAchVFjMCFgTQgcVMckVlDlHlD8eUUFhTh0msdMYNAjCNGNIyI8YBlUZJkWgkdMddMkb8WcTZB0gnP+mmrbhMb4c0Bccjh8foPDIjEpESTwgkiZjUbketLPiBsSN/O7vSRzNXNzLzHCZeqWhGMMRai2MsQUQKTrGNLaCyZUXQhyV3AGqJrSUobqOCVcesVxvoR8IqqKbGrghKSbKJlSfEV0ckVXDXLwbCf5MSUIK6hKR/igTRnYbqdMQvKYCvGvA6QVE6fkWSuSbUS1p6SROqpxmKhIiauJCZMIBwUosHLAQFiAU6ZgZPHfh5vUWPGLCVhQXkQGj8n6inLKUIcmQgIwTcLoumTwl0itnfrkSxFQfjB/qCW3HERBl2GoaKOAmeB/DOlQYWVye8RWYkVWVyBgnWb0KWmeC8bUWGFXtxB6suT3D9L2VeHESVEmROYYLgHJPooIATruSmY8FfvkEQcEJDNPsdBrpaiVlcrLlnqKGQaagslcZWRskouYhjiasMAMGeFqKeVWU+iwHwf+W+RTFPhSYhAGg6pzhbFcrYS+TzrYjudwoxh0oIZ+U3ronrv+dFn9B+ZGbgHqgYLATGkGXOhQvOvjHOW0VeAGiQXGmOVcWRayBGpoFGkatYhEf6u6bmCCdYbOWuRHs/lQSxe2BGZARxRRV2kpK+v2oRehTHiOk0fbo0fxB2X6gxRbKrFWvyUIXJRsrSjss5C4BwSZaeton+aAWopkPKFZeRfQI+vNkpfoooVtF4bnIoUqCWTZP1lzo2tIF3s5ZxclrgHhiSgKsusBV/iZexgQJxh5ZemlkMGpR3skODlLu0c0XyNiS2sZclpgtiEqnJAAB4BInRdKH7/DtSRKMb1K9zhKXJNXKx9Z8knCpIsUQStK1p+r5ESmmmlIeowLSVdBlbxyJgAo8Lhb6jnawi5EQROZ7BHZdUpSAKggeapQSl5I4WkUlXTkASVXPBvAIr4YhWsUwIjxy50EXxGDHVEC3DDQbwnT94kWQFeKOgjKN6dqno8YmnvZDC4kBHfVOhRk4aaASG8ZxHsK3lxqFXrngj3KbkthTrEHv4BqFnhklYBV9wzbV5TW5L1RXUCXoX5GzDNlZEWz7B9n5jYWzDPbH6zCQajGVIuli7qElLWphiNQs1f7g2qbfmUQkrWBniJmC16bnpQD+ImpRgraS1g3S26Ky2cgQDwBvUAluAcHg0y0cgmAEirzmUWUHVfUQBmZ7gIC8FyQsBM5Bl/LMRcK1EQTIF4yQ5w5PaMaWQ7xXKu0sJrq15ZwB0gC9Uy4h2FntwU7s2jVoWzoh3E2jq8QhI8LCA82TxZKgickjURidYhJtmO2bSc1i5+17T9x3VCFeKW0DHW0HmsBVnwqTLYIH4QJY3jVPhsTS5AUApXJ506mRmIzMC2gEBj7v6AXdS3qC3GChz1i1223208IrAwLZwhIe07xd0k47wtUy6e3Oal1GIdWQ5bWh1H07yR1llIEymx0b6AjO2J0QSLUZap1PhkglJU2Ma3Wd29BX2unH0EwDigL259DEjl0fYD3T1W02311nUTKIqWBXUPy70l2XkbVWl0GnpKTwAcivDQnwx207LYmAmaSC3LxwD607LHmZC2C62kP63q2a2w3OSiA0NkOq0G1G0L381kgV1XFMEUW/4fBJG1gGCcNrRZy70cHVi1mwEuEznnJ2IrTPzp0WyR1MKr5v0SD42MQALKz30y6P1sSNLNIuwAob3XlZJf5SNN5oKYLwn1R9ZX1njF3r5Rbt1n3HjjX9XUEMQGPGwrR6PA47xXUaGRTuH53pKF10lCHVh7gxlK6XqVJxiSN9FW1KJ76ObUOWNGosD0BYOdq4P10BL1Go3EMBHPpkB4rWbaIQWxp24rZp7s5Nb26qwrUQFqDDYuJ4r+bwEZlXnxBNn82aF7HSBvk8l8hdmgplOdMJZJYpby0PZ6RSlRxiWwXSDwUDjcnUa5jqk8LhLh5f7lNdO1YwD1YDq2KCZ+hI2eCFnMzllXGHM+IjZjYEVA1V7+h0Wiial5loRsWRkPMzESpOlvH/Jm3tP/MbaKS0BFO2KLBmEdFQgmgI0QSfjJg2H9mOWIsHPTOMAsA+QcC3b1kLPtBs6Lk9a5WFL5W2orPL5+rtmMUti5l9hSVBUqODOdF0H/Pfa/b/ZyOox9MQhi7NBCwCgI2theoiuNMU4Ss0v1XMTSuqyE1KgsWGWr4itYsVP876HwwEvpKdIDMPlDNGSkkiV8iIVRP3PYv06M7VUwskucl6XoFt1DM2FXOaM9mtm7N7SeE8N/OWsIxC68YRHgJmH4GXG+sascw1kkrbk6QSbevNPU6SUoPstCH/O64sGsFO2gskAdMRufBfDm7zO2LBI+UMTik6UmMpsWsRtDLrKjIkpkubRw4huUu1Rv4o1XPqt4q1tKSwrQulQlOCUWTwsmtoQTPTausNL5DCVhuQH/MF6iMfJV5slmHqnzCTs2GjOTMcvYsN7QC8u26KEka5E+ytnUytuIRbviXhSMsyYQRTylM7sRtD4j5eLRudLh6rNCC57Ulx13Pht4pD677/mhuBUkwSWoVFREUWwsV5yx5TPPsX4XkUvoWPxmFxETiisYdlASaSnozObZ7MW+UyVgvYsj0i3KUIHCBsEcHPrgUDF5M4O5R4OLttye0E5tM5urzJX0C/4YJaDvBsBRXsBy0mnYVkmcfYD6poC0CWK8evAAQfC4CCe6AEiSLXpxU+tojSd0Bye/5yRKpgAF5JHURqct5lCT5adqA6eycpm/7W0MhzGryqeEhxkaftCPuCI2cUDUT6Aj0kr/wS1WckA2jQCUUbyZm35e1fxo21TGuzvWdfBhe3A8VUU8JEMfhRFDnOvNBEvO7qGbPXOoG/PadJecYKX6mA1OkT7Lqfu1EZoWF+o/vV5XIaUJuv4kchdlemV7oJN7XZtSfdc2XnokpQQDnBeDcmBhduUvrcaUfVd5chB2u5ftYrdDOtdmvV6FkzvmskShcpW4a0DIdCShPGId6FdftVL3lCWPmEadeTdhdJW6KVfzfj6Lehn2tnvYGxeIQXeBqJtVt7cIxMDUR0ApE2Y1NOmu2HJh2foIafMwLW4rT40ov5zAqzCsnKPJqZ1eCest2ecIIcCD2g8MCVizPC7L3YXJgtMy4aMn3yNXruNXjGPpj1L43ZWI909XVewAk4903yP7B8/73MS3PwR49TCjC9RE8g+4Bg8QtbYmoPwjV3tetPhFdbl8Lo9utXh5JzJa9Tu6hqM2Bv1S/A/Byy8MBfH4sW6I989DG7UrZ8+cm3O80Swu/BW9wc8s2ggUYlK0ymmzA0/4+SfS/m9g9o7avN0ZVBOn1Bje1unU+Q7O2w/Y44+Fk56a/Kzs+6/Y71Lc8v2acCKE9m8k+GCoDWuK+0+H0M/OMDXk58JeP42tO6NsvkzO3c9G/XVF9A/E8W+uWm6FvM4s/6/LUy4J8q8ccrQp/BTO3p+oNhNdDs/Ap5qzXJvISf1i+Mao8Teh+l8LtR81CEL09oQF/BRp+ijz/H3s+n9QJU8Y9r+RQaSwhmNQJwKm+99g97tN4H/SAj/e3r0e1lYNhHPvpSwrH10+LPBfusGv629V+hvG6nn05J+8oQefF/qGCp6GRxeNQbvqVw/4MAX2T6N9pX3nJtV5G/xRPvI3jbzoWKoDbHGrzr6RE+EbPV8rAJTotEYEGXWEML2vJ+9sUHAO2j9k4zdMD2sUZDEs2mr7I4+KNZCPDUDpFlyYuCFxn6h27bx4ef8CnNtHxpSZmU/jbLiVjJqP0zwjNLOuuzbAxJ+eQkNzFAkOT/1AMJ0PPpBnfBaYQk6hTvg2hYr7UTIsLU3gIOfSGBjAr3b+uTA/x8CfInwEaD8WVSqpAWc1LUNsAJ57cXQAWQki6A+A2tOkPUcuFyGSH6BXgXiauPoip45YAU3tegcMyTqyYt6pQ32qUOr5FRi6OWHeNPwfzKxI6MdZYLX28ZoQWe2jSalhTt65wnBjldvt4UibbxAEUvJIb53+oQBq4MDC6j/yYx7AsBYHUxoxmBRsRuBpcSXlkLZCLZwijmJaKEI3APEqmtmCbMyllgjoS2jXEmlLFnIlYaB47bXqjBK7Wc9hDxYQdYkDBuxBWleL7i6hfxoQr2mFJigGgsGZFFWfYWwcVnu5nxNwjBcni3g8JhAR0CQTSgDwrYlALOuBITO13TRhBrUUlZrg2kXrlsjKhPD4Y8zqx2UnSGfZrKiKyp+UT8nsRbt8zIzDk8qeNd1iVkTricKRiQk4Y81GzQAXmGZKRLVz+FoZVufIalr3WZEyYpMrvNUm8JC5UjnQm2KFiairws4ZMV6OEeqKt60jx87nKBIyLDI/dnAnI4eOCKGJQjjhCIrlnbR5bfDWcUotuNSy0q3dVQ/LdkVlkeFmwqCidMVlQQhGHxSaoGDGlbmjCTChRZfdHDqx9Flp3RcoGUbakB7ad1RVrNkKx0Pg4ircwFc7t73KE5VaajSe0bGIREC4A2zOfMbfxTEpQL2gVMSpWIeKK4YhmI5dIiyyqWkVyp+SDhyPg6xZ4RDxfNmbg7ExdHM0wPHA62rwGVVRUndUT21gI+4TUE4OZOaOzoDij8to3kguJHFKRlxfbNcSrjVa7C4x+/FvISOXQ4cO8JIrZk9ANFxiv+IgqBAKEPQNiluOXFDviMpIZj3hz44fIQNHq6hTRBNUtts3/FqjAJwHUAmBJgq1FqWO1J8QiKIBId0mjmTcZ2PcyihYqSFIVtuLuFDMRy9UfGlAKAZzUrB1eMMbkl7HQjs8VEoqpSKFGQB4A1ja+OglcKxoH44pAjh43qiR0GhzCLPhxEYnQjGky/ZdBY3PGbhWJ0jUIosPqTPxUGIwJofMBZ7k150kdFnv5R5HPFlhuEgWjJNmLzFdwAxOJhOLQjLZscRw1sVuB3CpMQJnsaydeUKDHDXgV8ALJxNfEVCJqMGMtp42VjND5BagxhFRKbjI92i4UsoVdVbyzpWksYjyTYzCLcSvWdQqLGJKUGNiHe2ObaNpOLFaMxhbtJ+j3gWoJTdhuAbVgqgYBnwUyWDOxl6jdKhDKpuiaqTi2toch6pbhN8vNR367gqp+qS3swFjJ/EXJ1RAbhtgGlg9G6hAaBslmgDHc2OL0ZYqEK0AsARCtYMQjDVjQjw/G4HFfMnXnRbD9oT8MSJaTEzUC1uxbE2COSomC9F+dsQmnTwCbMQKk9/VUHskDwhJR+A5IJhLB6FIMmaCPUmr3SGEecAUfWBPkMz5oFVUGTExIWtJEJD42QLwN6nERjFZDEYEAcQnxQ8JCopemgJVGZ0axapU+E0zQMNDgBsB0E7EuAj5JCbNZtoaAumKGU96akqB17CQVOFOmqhNSXvS6aBIpyhTD2xZMakEl0H5lQxc1V2HQPCYGtyYgM7amvRcFYUQ66oRjKtU+rWcKZ3FIgElgAioz5acRbGHwJ1nUB9Zy4+tkbKJg5F2qUg6UXQhWjb0Gq/k4EkTAMhZS8i9ohgT7Rejj8XhbcC+taj5H7xQRrw/MK9DaQANxqOw4cWbL1nYgjxieHyV7D2nzoDKKgnqjQWtHSkzBmgzPMUjYFJjZ0NgtASwgJmaBhSjAYafEwQItBlgdkU2ZXMckENnIGiAmU3lmGcYNAIPN6pqXIGmzO5ZFQwP60QD9tOkMSB1EUOXSIk5Bqks1FnP2wwJtBzmEclj1DDK8A5yg8arxOTCJ19RwdOAWhEkljoAUuRB9shFF6gh1q41QBoPJoDDzqxiAeYU3Xgbpd15G/a+f/yF5gCQ+Xk4mU6VsDGxTZN8HJl8LcJ1j0w0487hET7E/MpZDw0MbkSR76DRMKg7iCsJKDsECZoCztDwUh6NJSZBY8mbgs2maBxC9M98W/DFzMzZBuPLCqiJwXoIcmRhbqd7CuHs4WchrTBTrSYUEggiOiGRt5IuEZMIJxca7j+kIkCtuF3tdaCAuYUCKQitjNcfBM6AyZFqvNe3uLEwWOo+BcqQZB8Gt7Wzc6jCrITlCojSMOJt8emUVPZp6jO+xU1eV2HCkGNWEHXHYkE0aQ38+45csxZRGSrYgFJPwWMHdP14sUWIVEtkk4rWjhScehU2JUfMyLrzepWskLuYuojmSa5lkuLs1nbl+KTiNdNJrxjcB+M9FlUogPwrOEEKghqlcQSM09EDhNuYzMdpaJxJCxY2UvXgsQH4XgKTSoHNWHVVDCTt8Yf3B1Oi1AGyik2w8Z4V0Lbb7iulFSsBYlmSwkpsSbxMRWXibHgTvR6Ndtgy1nx0KUe6C7dl5wWU9Ljmpzf8rOL8lmFxljrDTFsssitLRyUEqTmcrAVPNRR+uN5sGw7wHzvxruFQm1leUaByl/Cg8dG3Hppwd+7yjUZC1WXkxzsCNU9hIsPj3jOZ40qRU8qFi/Lj+B0yCFPk6VgqcmRoxMeUjeYXNc5aYzslst6H24rkadVpdoOWApxPZUdWaLCwYHoj9BqdOiWUu6U5MnRf2RAD5PMJ6QSWRrdFY00nZXJvZLuEMBWLMXEr4xkfNKm3wQkns12ky1FfWJ2WDisVLpeIesGCnXhUlby5VdmLHn/l6iBMDVd/SQkX89ltuZaMat1a7ji5cI2FU/NzEklC5HzUmA6r5BSr5VpZUWTaP7radYV7Y/RC9FZWNNCuRKgVYKS5gU9tgbkxNYsv74FtsltLN5guWbJ3KGipUgFeOCBHiKsV7HYKjosRIZr+Fls0wKJyBabdvKn458utw2atLNSO5WtTkyTlJ5/ydLTaMGx7WDJC8/5WfFd0/F4cMRPjLZaaqkWu1y0I6l8QF3JgfcBwMysuWdxrxd9Kk265+CoKhwCyb6SwhIZGuVUEDR8YtRFXaqvByq+smpBYISqVVJqgO0AGNeTDjW1EIl1K8MVsuZVUNx0UlCNdZ1hVoTsoi0xuP0o9l/LN1/EypA0x3UaLcRgGW9nPNIkKx5luAXFHuCIBeIVlV4lXLjiJW+cweggpyUtM/X/oSNbUgzNENdHNYcOnS2ZkyAcLGjYMEpK4cxoby2h9CiACxMHH0TPQD6E05ZWyFtDfYoAnwH1UIGE3CTRNLG20AACsaQOa6QHJtXzxUzFimjmMYHuLMK1N6YOIWTIU08bBkjeLUX8QcrtB012mszRgl3A1wKlDm6RsHAJCGaIpXqT2nop016z9N7ms5hGEIh8Dl4GCfVAwwQIc0jcw459IpArB0BLNgClQV7BC14UKwvFSCqEpj4C8yhZcmOhJgfj5aEcsrTaGUKD4ZUgpj0tsPr0jrTzNoF9a+rVAcWKzSssfAybzM55rDEoYw95jgOs6xamQ0aF+XA1Skr92t6NMYYWF6hTcyKFAHyCYE8myNFhd8jvPeyAEayMKIa6QHVrWatD12sSneFo0cZO1PFooMYeXSjnyMNh2KGbbLXm2BLlFo2oYHfznkgCbInQqSg1ok7CzYcd0xvqYPEaJLD4kAyhNNv0K5RWN4KLWjIIxmxYpusAXJnAGwYFN8GA6idfDWm3yR7izJKIUZjjIlLkktmuHYQAJCElsohmktgO0k5yQwApO/QPWtXF/FzpR/Z7CoJiTHT516c7Oba0HKXNCag7aJRB3Jh2Kt5JW3beNvxWcCJuNOunfHnWTHidplpVJBzsUaDD84JsrQrCNsTb8qc33GOJFIYic0nGIsVkjdXMEeYxh8MtEIbUNr6pGS+KH7M6JFUiLJsyupqjBq20hAt4XC49aWoEkf0RJ0g62DEhelNsgdnCSnn9OaDryV50u0wITL40MAhVPLYbZdUaQ7afFXW9XJ9B/mUC+tJAG3Qnvt2eJCUhmx/Ccmp0cMAumbTaP0Bu3Ql+FODZePgGsT1zc+A3EwA3pyZN78A+KQlMSn/Jjd4MHervfk2b1VkaUdKGTdJSMGT1y4jee0lsnk7MgDw2UCLpnNtlB6ARdaOVd1nqhPkZZMHbna8oX11hsQv+UFaRvwG10ItQLZeR7p+lUMj+Zc9bZHkJrTq96jq+OqrvgEyCYe6wqbfPp5Rn7l9FkztB8Eg3OAN9P2h/JlN6in6eY5+hTt9l/KL6Taj/XYvRV312x7GW/U5AgzLrSTYsCBpfb/hyjdoJkkBuNMltXoo1jlSLPaIPB3WOLaK+aJrQtUOSqwn+s0J5IgvdUnrypxB4A4geX0vB0SuiFzuvvX60HPYUYq0fbP1UAldiDqT6UMpWjIVl6Iuz3d4rALOZmWHAvbf8JGKHbGlRkoQ1AEX1IGzqSRXtCNIzJK7MDV0ktbFD5XFbZN96+mguLeCCdhObADkP53HUIa59xB2sL11AIuAoQStYg4jBY5kaVUSnBAI2qGJQUQh8B09EkXEKfBqAEXRbhpGxSEAMEwtLGYENnA17Gx+RpxAyAICKk2QdUpHRhO1357sA40HyNUfalq0upO07EoyrPVqAWjVRryFkur0SkmN8+yo3bSZBkKKFb1FQacjQEpKKBWMS0lEp9TGhCapyEcjKRBbQC2EfEbaKHs2oS6m06kcwRLF3kibJO/RiY7aBmksccMC06xCoMfoVG8Wli/+VxKh4y4d4BXZekzysmaGTDYIrOHnze4wI4OZq0lQ9pSme6qBSBHHuEvaFc5z6B2g3YCp0ZFzcw68q+RsDLkXb4DuLa0LEyGP47c6zDfE3ixbkgcoVqwio2gB5iD9qKhiZiPYngMVUieNAACIZh8RKJXOfxIhdjwG5wAKqO+elCkWEQcgiSHmMGflwHAOKJh8+4U7oirIlE9MmJISOXqtzGDyhzfDOIBg4GHJIMvjelsO1CWVaQEZhwREKZFNVlei/REUo6Xt6ZS3DLZXAwFL4ixzLTip0UweLmIOSAyFRMtlRK1PPwvtAotEFaaVMARm5/pvgCoeLHPxjtCXEgBGe9PpLTiQxRE1bhsHKF7hbpp1YhB0knKEEKZqsrcQUjMljSdIk6ChTqVxpQ52pvaLodRbTt1IpySDJ3xW2e7fZfs0GKyetM4sKTMZtwm9rnF3cQNyZr01WShK5R0zq5BarPOYPry/Rv/aKX8fxXgnBTk5+gCiWGijRxUaphVXwkjlfsVqk8j+m2e9o49ytQ6j08Wa3MElbolZ9LqoZinKktVBI3M0sbNTs7vaztMhMvXhOynl6V9fI/eeB4VmHTLA/OFqd7gfUE06Uv1ImZaFNGSzyaquUqT13gixZ6zZOE6c910qeV7u28yRFQvyk9YT54rvnBKHDxVDzS8rK2fUNWw2wV5mpX7MMMncLTd5/s72lDhGlIL8jbg0OxwjFijItVAEmeZ/GtLECQs3swqf7O2la4Q5p7VFx5n1DOLJFrcz6SXjcdZz8snVd+xjk/6ioUMsBuGYoBKR7ObIeeOXxRnvoE0S6+feZessM45msaVOVQxkP4qazGy3JE8c23M1pDbB9dJfKP53JZjR/KSsGQzowHZMbYSmqLqEAanxd6wROnFI8KpJueCAuWSeoZlAyBeRBy0+ZeJONZxadA7FO8F0QTRyNR5CnIoN6gVXeC3FRVLju5Map5aY0yMHktizvBhGPHT4HVijZXLPoIsO2e7QF4SWio7NRfKpfxUiAmjClHQK9TXGQKH25VsiiD2ygBmqVlSVI+XDyF8aV9vKflHjN6MkB9r8MEZPhpk0orclp17AOdeAg1xzEEXfltyvKteIZ6iioRdYqE1CwFDmy/S3IdHMMRC1SrPQfbhImz5t+bilsNjGDl6TnIRxqVAiTWoA77uD1gYk4WSmeUhYrqPHIOvuXwKvUsbdnHStewdqpFnO2mAuWVbY1xikUcEzDYshukUBKVim+cyRt9w/Lknfa1VTjLJAw8715eCqjGRMhWFoBTUjrumwqCpbmu8cJnPHQkTg9D07rS9GPOtbJZ6FTmu9KEje7iLaIac2NFplLbiBJOBC9tuXoh1wlRp1lobuXq7ymbjcEc9Jdbp3qG+Z8yHGr3qvZQId1jIJSaSQvvlPLXM9MObdDAjmXlrqsRv7ujuxrA9YUrOFzyMZMD9bagQ20SfES1ygybe1fI3O9vQlKTXRtuQNyY7Uzn0e4SqcPmpm+5Mud1nBqwDMxJGBw9RRauVcjRVlkZuiQzeCahDMZur7dl7g22YukI3VCs7aFJRprkFRKQSE2PjRDG8jjw7+8wY0knuh35rA9vWVADGj8EPCHSva+XchTYg31ZKs7C4uosnrsV/1sXWVCqEC8oZGK3yUpOcWJ21JDPc/gxFDOr59eTfL1iHSuo9GQrWe/FZHa4Ef1Mh3Vg+/rOPup6+ABByI3sGvNMpJOkm6TRT29hp5eoKBlGYXfsOI2BuKB7KPQAoXT7Fu0FVtTHiC4+WhAXlEljYX5YhiH7/Myh4sfYPew78yrRQnB3V4tgrCTbBCWRMbZB1nMidOG45U5tQY9I3g8uHlCqOjrasrljMtAdrNS605VDlR5PBNiwWlHR0giSqRmt+oJHYe3nc/nf3PGGBPEk3bIuQcfAYAtYEQtGmsSCO170XG7lIrRhEjeghqsM2oBhK2ORCKXIgA48vTbkv1J7JpQ+N0pEcrwGeq3T45sd2PwDs3PtCwQKxjXjLga7oZ2pBF4qcI2TiAQuN8cJPd0hQ/3vg/iciFhu7GvXcbrKcsA/HrlEfEk6q5zUKcRSCTJrNMtxO6nCTyKtFSGvoUkV7OXrfSpCVSLkNR+mi/d0KciEnuKVObrjK9Q1FWuHhyca8s2nccCQh9nFrolrBN3sCS9i8Jg9PSgKtnBeeR0JpROuy24mZzhLJdix2P8QqZYgL5zFGm7bEcFkdEqPJZfmhmfWJx1HTZXA1bbmJqgnPZZbO5Ypiq+59oHsd0zrEi3F00JekDQcZTS8qZzC+rL4sfJ9I5rP7nKFpWsR/M38YhHRWYOMXU5HyRfO4aDKxV8hNF84dCrUtOqUT3Zei8edln3EVT3W5NhJsu0vWtCBGji9usuPMizXbyw/Z1N8Srw9RFBasY60q5eX23N8vnPpdU7XbaEUR6vhdJ5WT13B7FA85EIWTjMXg/Vxi93CHlCKZKU148/PI29vYdXTku+cDm0q/uGhtlxU5/K2X+budCbga+eC5RHjoTajdI4xclUAsXLt8Amgdd4iZ1bshl3USkWU6G5vu3StSxxqTt8aT/TRezaHGCI/XeGxIl67+JRvBl2T0S76ORpYxYF8sfsb8+53jcU39fLzesG24zKtGOe9V5CKot7BHX+oKW2S8edzFC3Eb4S+8+jcg3t9wOnC1rt6xrGvUdXDwXTdrdDNM39BsOYbHBFs11LaIfN2gDxAjuBT2tUt9W9FbTu/x7anXs8pZc0qCyAPFd6M8IuXvQxEsbdz44xdgVql2pc5ohsdcNLplWyi+SM70uzvw1az2lJtO2nVc4ihzkNzEermZ2PN1mgWNFrzeQoDwz8gI25f5MlBeF0jnWCYEgDBxnuLIZBBKeNKYP8PhHnjiqZ+pElEHu6nYzXwKsIJawlHics91tNbEKPDIAj+x+7lUjfTCxJS9x/cRUfnuRM9CxRYEtnQ8PPHsT93LTPCfZPonvj/QA5cPE6P48V9yQFY9yfVPVvLj8p949EfyuPtmEvacDI6LeJE/bqCtELKINUTwD/ajra8u7VN+41mD/c7Y8mftzEx3c+iQmi/ERPxnnjg+YPMGO4ZfYVRpF5c8h2O6+D7z6F/Avhfr789rnfleY8kRdPKnnz7rkM9ee9PPnsizUf4snrEXVzkO4z0iSYK3+Rn+T/qV4sRxgv9XhS4vpS++T1b9HwBgl8K88ctLfpDJRZ62vAOw7Dnpo9l9RCXXl4JgMaAAozP87QrTFqZYCKXdC6KajnxuCdA/2GNW+zKJgzRM3iW2RYsq6U1+LuuseNwOTRjijpk3cqPwDlrz5d6KNKISjtqIwfq94LxGPlIo8bJluq2jecE9SYKZfZbcI2kCm82ZZV4dvrvD4p3olxllyLkCOVqNv0BgN5rHTbmCX5ThMhqw/enQM0tPbIqj24WwT43z7zj4peBb3nRl89zCw93EiY7PDuVXPbwvBMToXPG+QU/J8fARaqBoe2PbNWsfsfPPmTnN8w2P8fQFH4Xzkw/d2ZB97+8+/ipWMMHHkupk9cr7nlbwB5ctyJ6t4si0W+wWjXidm6DWk1OfUz3gmZjMyydfqYXPIcLde+25+WWaKX1b5t+Q0SqkHk0ZNmPZq69Vo7rFUDYis4G/VWkxe5+pVscXQmC5smqaV+k9vvapc4GUG5CSwX6zwDD6KTRDPL0R+LFDup0509cpL8svHJtVZA4vRvNmD/vXs54fDUUPLH5eG79iafAt7NftuHH/k2C/G/1vvcGc+YKXoT50gV8zlsaoM8itO6xdTUIZ4vb8LztsJRuqPPsXaop38ic5hR5YmceBx7EeHtNQ9a3PX89xR4sk5sg6Ax1th3daMA2+qlcvuCQzaBIKrsSnmsYrqHqL/LmzepoWHJgfsd+VLUrtb/1yjshIVw3+4P/DA0j9mbd/RsEJJBmmyt9fOA3LhL/DkF6UgyR/zac/lV1FuEjNLZSgFybf90Tc4ib1E9kfdTCTNVEAxEQI1b/Ro1rNt+BtzUcK/faSPxXXDjSkUt4bsz0NDJUJjs96bMAMY8VvRFQlljLUmkaRD9awVgCQ7YgIG4yA4bBpET7VMR99azIl0wCe7Udi7csVWl2Ss5XDVwXEpAz5V+9amImHv8cIYZUDFHlLtQXJIMTVxsgSMSGR7NG3PuEtIfdbFDICIVYJ3B9gjQRGcDdweFW1FoPQwKQcvUQZ3KFLSH9WcwHULOHMC53FgPi5pCYxySUpWGK3IFqXXgOWYXoLU33kkraiWED8wN6V6gyA0lRrtwCagLQ0SxM9xwgZlVQmeUyyM8A10cnStnQoagwsjgtkfZIO3lUg/Yw4ghAzw0mwgmRH1XshXGhXtU2wfLVyCKZDkGT1ndNwiFkigyd3iBigz3mKDQZZ5UJoFzGgUfVCAmH1S8mjMgIj4D3L3Tv9azQD0aF/VZyQKwEJNPxMIEJdsw0EEJZSTjtAAuIL+sEg9SF2IWfNsDSC2EOVVikRArmzEDspbQNGCy+FyytVKA+TGoDH/K+2oNP/M4KeIPCBcj6xLAm+3uCwcCnBWkGBJoKC0PdV4LaC+hN+wxNMiE6Hh9JAgEO9V90SbBqJGiYNU95QbGt3DkpFMAg8cNeMkNICAQ6NTjJ6iTgRHQmgo1WODZrAV3Zx3HFDQdk63YD0xpI8KYIv8WQoUmTwpxIkJt8xxBk3Hx84SlXHAZlMIIOUlnSIOFD//VgzsCqbCGxbNH+DbxLQ5VdIKel4rA3iEg+goBx3FxzbADICGdNv0fB6iQD0+djA0APjd/iUXE/1UNJYMCtdQwmnFInAgEL7UfJO73gdBlDp2+dxwDdjHMkCL9Ugw2wTlS7MvHEGCRCoEFVnTDmQm30vEQQxzF99AoLZQF1k3HR1R9Z8JenS9tQv6FDIaBZYK4DAMIGwAc1/Oc34CcQsrx38JA6DAT86DBakjkRgm3xXVtRbEmhtzuV/z5Cggt8k8wGAmyHxwRedYHbM/uW4Ko00w+wNnQ6w1kXX4j+LEJvVWwtuAyDSRWMJVx8DNAUEMPAgEMvUiBCWwZtVSD6TXUEJDAgnCQhdnCiUb0CsMo16g2IMPh2ZNcLfBNSKXUdt3tN4IEDl/dSCbJoA6Cy2CzwnfHfVGGan2UcnXaaDmD2BDwjHC8CfM2NC9WZcKolLHXEOP8zw9CUHDXdaYN/VaJSbAXJYLMtWUFZ8WVxkxJ1HGCA1i1OiNB8MvTtxDsMdcuD0BFKKCNkCZYE6wm49ACUyesFHUE0Yi+Ivwy+srFD40XonHEH3TwFBNJ2AdeXZCgXDKLIZhoFwnbhyd8FZFoJLR+DTIiGDg3WLH4ivrf2yDJy8EXlvVUYLtTV85ldCiCtAQZSL/YWVBCWVZsSG6111s9b2FxU57BEP3DVnJAhrV2Iozmswz/YVwG4PgVVCk0weTEGFVUqPpWHs7IjeRWdtIeKOXD7pLSJitNJOv3ADaWOKzxDEIb4IWMv6B5VtDwo3RDDhLeWym4jT1HGFBpDI+4gZB8AR0JzQ9HO6x3M0Sfczo08dNyw6tAwbFA+BaQQxVxAFQmNx9czVFTUdA6ATBCswndKqKf9aYSv3LhxomAEmid8Qe2REFqGJEI4NoqfgYjvYNczlBvdZe2P0TMacF6gVNC10awq8b1mxQlNNxAg94PcVH0RSrVfCiNBEGAHAperbEAgBE8C+HSEMhYO2vtmqUawq8/g/6I/Dr7LxjXk7pOnhZ87pVxQkAt/EBzFw5qbxVhM2IR/Stw+eI7D39qvAPUk53o0aBEYvon6LhRzqV+WmReVAwxCQVdfGI+iiY3zzscZNdELh4zVGAByh4AHJkp9ilAMHMFzzUf3TEXVayLi8LzAS1uQRo5oKYjko5i3jtg0OK2qRfzHf1X9GND23kY9QgYIF4c6bFDZiNaVgAbBjqAn1gdMFbEwO9CtPnno9ReXqG1iOYvBR5h+fAX1Zj2Y3WKxk+fb1xai7rK2N1jZfP6OukAYioQukeQyiOVshYwGJ/MyBTaKcMTonaNVdi2N5E1DUOQE3KRxqY+j5EVJe+w2C+4efi8Y4Qe7DBMYpdpB60MBLWMdiZfNxGSIDYt+Qf5zmPOI1kRYc2I9leoLxC3tDxSFH783LN8kTRXicWCW8cYMiL4ML3fZSksWbIYEDCG43EFal9UTk2iFbfXk3FFi7bFBGhVUNTxyg0hHwNPE7rCIV1jkA+b0qRPkCTC+ckzbAA3ju9fBRv9Z41+gG4j40QnIUvfMekqQS8XIkHZMA4ZxA9LQ+7kvjxbIFmCwJ6T8T7on4tdxhME1cuEvisbCSJ8lEXYoXYDBZN+IRQcmLGxMjF6BFxLx8YfpTosmuNCIY8TfC+yaMRoAaIGIJvAgHsxlXB70ERinJYhCx5445glMWrFJ2pt3AhBEqcqolrCNUKEyqKWjFtYRUvQbbVFzBJ3Tb8MbhmtGWMbhB/IQHb5yxVgUk4GEthOxtglZOHGogfB9xzk3w4IQTi24NwXElRQSAXR4G4txHDgQE94zASgbEHyRd3DGYMO8gVc01aUFjELHBiM4vX0fAgbFRMYCDAncNLJ39APnTjywz/1+CBlBcVPQw4YyMe1TIlG3ki4KUoORcmA2GU7UEQjmR18vpRROmgMNaOKC0j+YMSZgOSRem+Cn1LsJPV+goqKXZrYQA1iw/E8ODtMgsKvHZVsUSAARhg4LQDAA5db3Cajd/M1HwMmqVJDQE+6EVxDsOk70PV9TkPrCGpaA5cPCRUkKGNyijQ0MBMth4+WIF4QTXaJ0FrQkkh61eBXqCqTaAGpNvh6klcVFtYGS6g1kj+bnlO9rtA/wvshqTYRrjJOVZPWS6kkML7kAwDK3WFDkNASl13/E9WeSJXPaH15CCAMFOQPBd6hiQRyDSRa0twlsJcTUvEfjmoV/EfnN85IgFA8xaBA+MuTraDZJDCYHCuJf8qY7xMldkGVWKaNEU2pLl06wW5LdiJuML06jDNe6SAo6EkiAfMS/MqkEicjdfnAdBEGlMc5AQ1uT/1fY5FjmM8DJ2S343deRju8//G9hOMo43Vw2NdHOhFSQ57deU7NgxCO3BSC5YVh6C9khZI3lUGWPTOTGQhak89mUmcyE5WU+nFRSKYvcT7dq40sPGpTkypNyhUHAfyhC7ra6Gk1kiKYxvj1BVJ1rM21eN0QZQydQnUChAYPzdt4kygkf5MQ5gJhYTYfeV3pME2qC3gCLPJNcxbQh1OU4KAPcE2lEiUVUtIyHe1NmFhvWKCPYMHcuEgB/EofDQNA2d53xkC0qoz0BydHDCiojuX6xksIQpXl5T8rEe1nVarUVN3CnpJfwLMs4C+n/DpFDwhb47BPtx39MoiUg59k/Hl1TsSAQtN85q083ltjOE1pwMjdU+0ggtpySRGxJzSYkGyd23cUJJToABFB7RBpQzSpcsyLNLXS0EUuNPjvfKgM6TnAcoINDc3BBEgBYAIdyWUayb/iLYZ7GnxtD3ZDtJDsBdQXXjS1bQRIAjAIoCJDs0vZGIX8ew1VLG9cU0UXfTMXaRnLjjU4eFO9APBH1VkUfE5L/o64i5KQzC0xgmMBW4p0hWN1EQjKbxyGf8hPAySK1Ooy2GehnJTCYIwQPTGMiw3YYWAY2kH0B2XD1ix6vEjwURNPZiMQyfPGjzKJSvD0Tulx/BiU60AMwGMKTdUnz048lPATNU8fTUyV0stde7nq8JPfL2UyeOY4kG92vL1LEyeOdTwgtAyOTITT9PAk2+Ihva9W09sAer3TslLT3RWoyhYKR11gMuL3MznuNqL3MMSETNzBTvWJxnTVPUlMrMFWFfy8YXI3OOnSXMyLOS8MLC+xn8bM7t38zOMPL3cydFToTgczbRLPq9ivdrw9T0KcfxPYlzCJ11VMswjJ88eLQ0nSIuDVBjxMK0nz1a9tM4Bxk91Mnz368dLXLI/oH9S81xSvgXAEJQ5HMjLtE81EJNvotlMMO8cZ0lLOoSqTYahJSpQ+Wmo49IQQEqShSDO0ejAcMo2ocBuHLNnoilS8M3Rjsj4EIcT/RLQySkCSmB2yLDQhz44tAAgGplcAMXyST4/M1Vp1giTjJJQCdIdXr9qU4wDoZwtFjOpN5BSpNBymMw2m4yZNIDATJjs4wBrhClcOGKM+KQVGhyCQNzU+AGjfHkqTLEYomfQKNUQToQpHWLBm4mAIhzgAChAiO11aowRH6sTmGslAN4jITnGhGktKNQ5fYqrIK1w6BTN8lMvNEGZzvqCywU4gOEbSBZjfZ7RGtio7cJmyDLWwKnCFGb/QSisM2u30dmxDtxsTkBCXj4SwUu5yZyqZFnImhf8GsCIBDaHNNWF0o2sz8kd4/mNcZ9dYOKwCKcf2RlYB00EK1ymkr5EFy+qWOLvsh03ME75C49OIfpavWLB9tdYAHKtdeoKPLGg+9IlEaSUkdRGxR48zBiR18mZjkKZ5fW3Im508+gEn1n0W7yQ8lhbFGHx3EcOHt9mnNliSCBuCvKm4r46YxbxC1FJUFdKQs7XMSgA1pXmzMY1KMMTQyEclGJKU7SNsTugtm0oEv0a/ELRsk5mzN8c3Z9JIgoIybJCVqg+vKUROKAlCTzW5AiAm5l8iiiLz6UUbkhzjEXqC4jbsvg1n0981gEQAcoEzJKprrKVmfVYsHfGk0783DRv1yU5tQ6Jy8mXkRg7aKGhk0NKPXjusHNauG4pMEFGWxASDSl28UO6EdGRZctPGObTPYbxWeTkwY6U/lPdO3DBibEptO7SHYFSWLpGtKDR614CsPK9Z3wevMc0IClzWsZRcwa0V0zdRXxDtBUqh3vY6eSMQ4F6kY6XRChUiLxjlj6YH01ifZH+h1Dso1L2c904+L1ZtlYdWQF5uswRDAKnNSAtQzCU5axuo+eLfmdo0BNgpeThUgMDp5eCp+xRpTvMS0gSJtW+XfsGWIgqv51JHrSoLYva+wZlQCmguc0oCrjAC0TxME1B1y4DBEMVnuZ1LATSxW50/E3Q1+PjcIk9NAVFNfalhYp42a1BR5dIxCAzD0BWfMZhwREZQV9pxM/LIphkWqSrJOjBbkvkmUhBH8K78pSHroOjeozXFujRnLKK8i9ZCNdCKE/LPEX8xoqUhbjOaVogqDKxNylJOEvWtyvdJWHLybKRPIH1GsHO0BBMmPwrGLD8pmJ6i98yuQ81OIFlSWLG4233K4AaR30GoFBZ/KUKKZMLg98jAF1PTQ4aZUJcNHlGcImUTkvApwQbpRVNoTR8vC0ToV5FmJKlzUkFOwyP7T4MaJVDXJJ1dNI5xL2AnyFSTz5s8NRPLyXgITiSJPs1xw/B80l/K6VmSVAF05BNAdWF0zVfjkJkxEHBzJFIcl0yhLyFZYoZT8OPfLcQy7Q2VqY800AufRUURHWR1s81HW6iE0eoqXy6SxGBP9RTIJxNIzIzaECCFWegzPyOSuAC5LJySNECcMtE0gnVm2V4gwCow1N0BsgXZ3CuD0EsKgYFukgv2wAMEEUrFLEnRSnmdaMvYoDjwM+NyBtlzIO1vdJLf9jRAdSqAFRQ9S0hKo5Dsua3LzdSxvCrIGEmKhPyuNPwvdLoAKsipy1ok0muVrwtCD+4JYYoOzx1I4UvtLOSj0prS+nV5hhZx3Vl3jcQfXAIiKtQ2qHUjYk06Hu47Sh0oTLZnYMqBZmuACkGVzSlkUjcKI0KnQS2ue3Hxo9wosxIhOUPQCdBZhKg25yR8ibjbKfqB/LsZlgB7N6gcoEwBL8bTOF0gplEk0pESMnD0S7yFVKMtDSgtXXOCCkwgrM3wmjUcvHK8FSsE/d0nMBAm5ty2R3eARubUUBzZggbmPKACoIoByflCyOHZTEqsrjZ0EynXu9lwlmSVS7ra8qrIP4jJPKT8w4+XaVoQ5nlaVGw4PjZUM0g3IqEtTYJl+MdUhBB/LxI/RIaNCgwZSzcFSh9IA9H00wqktnyjUr5URymkB3K4EwJOEiRIz8XArMKjBJUDpobJwox0E4Z1zoaiGgWiC+4AVyIqefX/B5QjUUVRRdw7M1WXiWAIbWv9jCLxMGUaA6iqpssRVIs215RVpSLDRQk9TAJsUISqG0t4hRLXzKy5Upa5TDMZ1vZOK4SqNRyAoSL0kLjYxPJE6XISEQjQPB3JLCsYGZVVhPQ16UwjDEzbSVsFxNSuMrpAk5l2CXbF8M/E3IzAI9TMylUIUrJ2ZlkATYsLyqrBcfZ5jQd0HWs28slA7J17h6QwUMUMX4qOE8rZhIytiqXAgOx2sjy3KqG15eC/LzNaYNAL5c2Mmcv8rZc+9KFy5w4FWzKDHP7nCU5YwSpKrjK/ILcJ/nfNXPZiIvYP0sEMjArfMWq3y1CYWK2+IGcYreAOiquq2KvGCemIYn4raIvHGDUX3bAIciHK5b3nL7uGKpVU/KvktCLAq3MvKFxnX9lQK88miKD8bE3Qx7LfM1LwCj5qj4CG1LVGTU/KkCC4sBA4NANPjcBQoxx6TMq4032JHZZcNUNNJDSIEL9qhapHlBcYEMZNFnd1NGUBwakMYdJ2ecRhrXq4ytZDaxUJnXKc3QlzRq3DdKoPiDq9NjcIgFWWVUrYa+UI80yw3uNEzRqvyKyrWq3aqgzJfcuAOqHQhZ3YUkqs6qMhJKnCVsqgPH53GqJkssKsgaa7Gtiqbky9FWrcVNKoWDO8ztTlU8/Qiq5rYanMKDIR2L+ySrfq7l30snjb1L7jkkzCK7UFzG5kSTaQ15QOqBw7YmFYFA+CKgQbKoUIlIEo21WeVfUsGw9UgjSWIgCXodYMh9fVHKplqe0ICSvUW8ZrgqS7xMtz0rIim1QSiOk1mu1yAxK8CV0+7QRDtquI1vXxq4I0xNSLUEw4wXUPkzOsQrYa8DUvx1OUJhIwjWVKo4DVIkBkDS2lW2oZAFSSeK6igWMCXeYaa3WE4wGCrOyGJWA9xMXJIlR3Jyj6Al3JCDys6CofgTeLmr1gRsQ0jVo/y3OElsPdDrCD9x0Y6RAcZMb4JHhPZeeWbrMorwQkLIIRMMgzAY/ZMVD8wI8JzMXofDM8cVJYXLUAXgDxB48q87YoKCEWQSqZByODHO1FMJVSq+AtLcLm1F6iFSqGd8K7Q2ydwlQupbK0QN+tAaAnHkrpEZXXl2QVnakIDCSzvcWowqJnNS2bqsM7kPnQ57Ve18iTHTypAbF4fUtLLGkeKUVyfIyt2dUFy3YueUvmQ3xNTba6hqeAnSp0jqhtgGYuiqeG20C9K/vVg2AaTAUBqDLYoukS3S/AoasNYn0+3FAxGKyyt25EGkRsTK60g7K9R11Cd0GrPElRpUjiXQ90bqUaGZSuo5om0tfqtGkstkbx8VyMAqH/UxKFrLqt8GVqRalOrVTm69UotDFDGxpIAxDMHiU0+iBXhNJI04fV/rvsfAXwiECGVJ4KNyV0yKgBk5hH2ih/cKWh9g61XPiDL63hE6CRa7XTNTyVZzOCbYmiDSNTPjH+trNECxtIhgPdZ2XPSWIh2DGENzS7QPK+3WHSzq4AW6MyNvEd9njjvyuAAwRqILpknLCqvdUrKrILUpeARm6uASxP00VXmRaA2QlMTf42iomrF8zRrma8ULmIQI8JdTVqqoq7pp2b+cOukkJgcYBtOb/BUwFXVKkctOEbrm52KLdii+ByubRmkImvSM2fyNLdTEimH2rb83EFXgPsmeNvS0Kq8rgA+mgAoajIAAJGNlJOF4ChbJyF0A3SYIquO9z1Ha8l2JgNRn1+Ql7cZKq811PJst11Er93AiEWyFtYBZHL2LZD/0lXIfoPdMKyCR6fY6O7oMoiGpiQtGQwX1yJ8ywQKbUYVQ1X9gGgaJxk3OMGu/K9wbitX0qoxsPfKDgjhoRocC4xq+yMq0JM8rJWhTkv02pGpNwBb9eslmMhsq7Wf1hsqesia6kRiyBNJq39I9BBY1KOH5Y4Eco1aHojEkoN2U79wRanWrB1alF0naQcT9k0e1tqnW8gyUhXW0tPgsFG74LekTzRg00cQ/YIQtao/L8pBiGPMktSjIS8xw+T64rmqDbE8G/Pfy4S+Wz9DBlTFu2EZy7nJMxY2zC3QprPNwwz0jG/gqgN1IM0yliZBAgoEq85PKKIl1WgYjIMmAC+DpNB6v3yKaI2z80sJkmzIm+Dws7AF4IcNGuigZmS6imXoueQAXHrr7XgXZxXaFrR3qvGdQlQY3/OwOh422ua3CDHEniOBNpknKyTtdvHGFQEnWGOD/oGVfPwWzp27DU8RIGOugAK0M/xonqOq7xKySQZZqqLaEW7QCSw0ADutatQWhHjmQurLOshRhGVMmWrRJf3iXxoiLZtfq4OiikrBFmwN0PLjEiaifi2RBBvQ7l4eDr2aMyNeP0LNvW2ow79yC6KS1gFEcpo7bXFvCoUZ+Kh0crIcGRAoK1i1Spo7nm3YOjgLs3jpI6KKGlrcttdNRS/YNCGSoVFGOigDl56UuMm+TgO+ToYAKuPRJNs3LF8zFjdQE6FuooazZs6QwM2Kx1zeW4BzTjv26Yr4Q/6bGNvl5TaKs0BVO2ho07vJSpsytl5RLK0AnO9TuCJpEpnWthDkRpvBlvc6JEFyj6ozugqKy0DPGSlyMwSDynPdsPn5b2z4sDqX6oJsc6weHzsEVQiNzswV0C3jqc6gylzp+tuYjcOta6zW1rD9uw5lpi7zjIYAh9gmYRMKi40qDM1i5OsHiK7fO0BNy7AO9FsxT7i+ztg7Cuxp06LfO+BIlSUy8rqAyTFMLvVSRkiJlM7SG7CwVyzOxHASVO2zPTKlVUsdNWzPOjLoYBOu7LswQeu9FIYt+umpy3L9u5y2e5iuySOUaJY18ISSRUmxPC73WmK2m7GyWrqsK8mh+Hyy5qJrq9suaq7pLLbu0mJ2SkUIHqc6QesbrIqJuitqm7VDJH0PqkGVXxit8Cs+ugyzSeLsq8BhS1rzrvE+3Oo6oejjACTCjL9o3Kg03DOAd4C3qGeo78lfNN9hXbFHmlL8ZfWxK3s0zkg7bK2QoG5enI7hGRC3SREW5+ZZnpOL5UYaE7RPXXYOfjMkfYoQRPfcXvwETinyTfLHeO3N9ThnCTBwLvs5moHjalR7sWTQ/CwuAcHikGzLCFzLQW1TQUyTFmMp81lqibvii+2J8tAtXNAdM/VIIhkNZF33LghGERkG0coLFwBy2LGppwRZS8oQ/0de/nMj7nTFKJYdc1PPM0CTTe2PcjT7U4KW7XUxZwQlUrBEOHDLO3Bp7j0inSOBKMsI1kMcNGtQCSJ/8oTgGIByjbJ9iJuSvohpxmpLTvTjEtkifidKi0oV9lwn3S1LG+3ct4Ib0mpRDJeeoUibzTijd1TbrhGIvD6O8hiA47Ik+5swj6iOrkjpbQ/vtXrl3F2Tw6DaydDnKmuOUTjbTGqBWuraesftB6P1dCh/zpVHvOD6G+8/ph7oTKSMeDaq12t3Db+oWDHCETP7m24hSn3qU4fPAooZLUsWrPv7wKHzyqLOpGotAJ1dM1SSJwBnjmaKgaPdSpS0QeAf7abusmNmkAClnqoN/2y+kk4h3R0EHKXK3nsgAJevv1Mq7bHa2gUwyA5rQZBEOYmWKasJvCZjFCCKGZ6w4OYQbwlrakoHZMWH3q4Gle1Ejhqaxfhv5Y21WQgrdFWmfodRZxbsmq7xaz2rTqoOOxI0DAm7ACIAw4eABtiwjarkgVcYTgZ1jdBtTzIpXnFUVw6LK/ik9RkVXI0k6d1WcUw43DCKBCxaB/pPKSF3aOV3squgVrtSSC1OolixgU2u+0dvS5n3T0Y3rsGSWuw3qN62IDzB/8IXZChjSVZPyJMwmjCpT1Qu7IQWb63nMdxD6w8RXIggmgqPqf0mq0oxLcDe9AQnUVg++sqQj+HGjYrcaJsILD9g1bpIjiU0Ib16tbb6VgzP+7xKbNqm2ED31rYHunUkALIJgyz8evYBYoNUvvuyh8WZ7iQGu6w7MF5me+YayGj7CmXJTC1WLob7IUNTpu9v6jFjgH9hwwF+xxPCUtQah65pJdymK4/vLb3asLu5bgXPFvvaz6lgyrUG6lXESyX2DgBoyECDqyhxmequ1YZOM5jKD7f6XnpBG6GOHJ4yQyu+NQGK+qu1l5wO6eP594/YEdNxmQEnJoSvUTmtiwh8XVsUoBccqs9hH+fZh97/C+AArBdA8wf2wFvBRtao6m7ftCDgYhtqGVu+u4tR6fBvaqXDR86evB8NulakUL5emThuBqIdZFwHtsBmzLqSIXtG+oJRziNgkyym4bty/Gh1DwsH1MZKGScELUxHIg5GaneGtvfA3S5V7GKQx9CBkOEayyUhlPPjmetAHK5Dho0vuacin3odGx9FvQIif6+0bt9EsXvS3yJilUfHTERkgF7RHR8fULzaUYvLFoT83qNp6R6PxEALx5epjutccjPMZLawFjjuatbFdNFGcoQAjYYEVWfXtH8xsHJ1j0R4fRLGIAAsbBHYRhYoxDeesCno56wCpilKnSBsj0hESxgZI7oIoGiApmeqFG9beYS4bbGyRJduy0L7epDLkQLDdsB9SBScbDsXbAINfsyvIagM6hAT+wybxkh+k38sKGKVToP5J3gANtuqgpqrqY/oTNit+dIcHG2vFBuMqv2o2JDSBUnf1+6sYon1hS+UkEuQKMsc2P2TvMzAyNj8R7sc5c2vUJqrBwmu/U5G4BtxDTHUKiCtp7QtYj1SJ/DM7LpEPCNou7GRTO0FKJwOhjV+FeexCaEEYmNHLP90sAiawnNMv01OzScqBGM0WkBvsImozKuUw9xBuMJDHNBxieMy8S4GTG0BxrCcszcJymrdFyJxYfszq+6iaF6/cBiawm3MiSd4xIFFej4mAsvz3ajgs5bIQJrxL+3Ym9ZLCdJT1J6rgo6lJ7LKWyuTSRFY6zRaSee4Ts1TCPIT88qCMn/XXWEVJ9JrIJICRJuZwNJ/Em0YH8CSuMZ97GJjrJImDhRzAEGCRxib6z/SOSYQIfhWcN572MIzmdbB2r+D15me8CeGaQor0NSnlOBACyMyqFyczwnIgbkCn6wJYcXpZAG9GxRipmydLTd8oqdmE7SdhJK7ItX43K6j1EYiPaOzb+w+Dwg9sOSVim/FUEs6p8Ar9tYeygW2gxlTUdqGZ66CscUJSZgWoHi+mDLx7kIxLLQBfsaUYL6JucxBeAaQUA0nNNsY/MnqzVTiXFyR5OgHULGsRbgX4wsbSizYqHR/0Q1PedKi1d7K0V3FZKhuDg3E7A9vor1HqiZKJb0+4EWj9mmtQyQUug6GQ6ws/cG2hkI0mL16gTp5fV/JGAGI3rowE8F1k1GRgbsVz92w3BVyorc1rKGL7VYjkEDEZPsT7G4a4piD/ajFFjVQyLRlkrdiPkTYrM0pafhLmUfZK3EK/D7jd5FuwPkeKXR46Yw7cpjaYRK7rdBAqpkiAvEpkuelJuAqxZnaYAKdAPQP4ak2yqYVmO7Q7i7K1lQIPiHWG5CyUHm1eiIF5B8s2D+4O+BcSfR6UQDhJGLnRJjFni862ZEGrZYtzyGqHb2uYbSC/3zrKsVAyvLhLZ4VRwwRBhXV6ZW+9pzOqnK8dnhmawNEhEZEpxDxdKyTcuBGwHOMU1I9qplkpXKJuZOZyhsJ1U28mImpk0BrsUbOfDhVMqKcAVzmUxViwS5gFkE8zJcudZIJRYLuLnaAFOf0ygprTt54s51uZzmuJjufIym5zFRbmU5gSfznIJuyoG4a5gz37mhiZ6eLrh5nOdkn05xR1mhQpwRBrnAsgLxx1TJ+WjLTu5lOb0md55AZQH95nOcZIkYMeeuGT5yeZ7nw4ayZYmDx2aDusa5krMvns5HsoXnw4BrK8n8pjtn4HT58OCqmH5vERdlP5+eEXgBvbibwDZC4ue0QcoUnhyHp7O+LtzeHQm0fLAbJxwtL4fL0JT65AsNRziFxM9HgWUMr9IBHP+h8rValGw/2VagZr2Z9zxat4s+GFp7ock4iFuXhRbsXWdAJqyWqhbTL0yqoNVahHcWoZkGQvlkIW4F8PnOaxSWmBPBYFrDoYBmOiZpoHaqs6psJg1BlXQTnKzfGcih2tDpIA2FhgH47ZoofU3LJ5iRYYAxOtBvHyZy1Ra09KbSruxR6QIznWtdBkWdFm7rJxfKKQ2tutMHlZq+Y/Zvc9ZVsjx7ZbuIlWLaaf5GDR9Rh+6d6DbrYKvpyTk8WyKZIiuHu4iebLaprDZpMS9ZglTVKDfftPK9oZXIfu4kl3gjmJSSphg8XdEKbmhaYjc4T64otKh2k7n28QkvxZHDSp/EP5jvEhgtS1pZqWbTMTTvK+MwKr/dfB/6sI6H7FwZaXql9pYuU/Kxbjzo8cd+CxF3Z7/wX6Xe8xssjm6rkOmW2lxWdpHChQuekJaBzavTLnMvpdkcCqledKVeoC5YAKyqoZbNJXR2LDuWqyHqpNJbp0rSOajl3SowysVVBYYGEEV5Yd1uWCYOPmso4xLqZqK9ZZrK9F7AGBWdgqqO7MLuxxZmWAC96pjHhl3XvCgSltFd/LR5afUFgSBN2YXFgV3GqDJPkaoNoGMYm1u/8QiuNA0Q5BNiqnayV9bLE4Vh2yXLhgVumohGRjZ5cERgVnms4Sfl0mYG5BVr3G/J+1D5Z+WW1BGlSabix8VxW9lqsm1rF6cmFTL9fUxOZWiOkgGBX7ailZlWbB87gmXblvFfDrX2GifPqJ5iTGUCagROZeWzV4+xjUZVqWqLFBqlGvlz7hzvp1X4Vx1biaDVu2c/ENzTAJjDqF0Kthwr3EpYvgQW0QYRqwW8FuMT/43uEODBF+hfdqbguOOYdyZ00u9gYrfkSzNR8jIN3qEYo+kSXo1/hT56uyhmeZHbPQmZG9k2/NdBjPZEavXGW1hK2ZiHe7FCSpWQV4BGwzqEaCH7Tuvefpbf7QJeLE15aWNHzoM7Y3nQt/bCKwTJOexs/zAtIdAm5wJ6sAplNWrxDS5iOCTrNV11vok3We0fDReapso6cqHlBnnN1zvu6me9nMSs+rraL2+tc5thEx+oEsI8wRCrAT/GTTTpuGLtd3B1ka7yZKZNbXvFV91i+CtzBJiZpH6u1iDY2TgF3CsVoBuGsDMwFpfvgGskpywrJm8OvC3+l39W+0mss4Mxw9yBh5dCjauzFHq2yhRmXF8LYsCKZMzSp+YFLyX4LtYgX+shuZQY3m3qC0tN40cewk3xH+ImXLuD1ZndU1npZBqhF0RcPMRLI2LuseNzmJHGd1p9w/FIV+CVqqJ5IGoyKfZ+7nk3aGhxrHMQC5/0Tbw19CJ03F4XWJkbDSygM7CwsNZpPd2cKFeDwrPYHLRBdNytdzrmsewd/csGt8rCXh0x9xystNOjfM2cmJdeSc1xTSczDjE0VdFq24K9l6h5i1NRcrFATwGoh4YHizCIKYUQBzwFgAsHkAgAA";

  // src/core/state.js
  var MARKET_JSON_LOCAL_BACKUP = typeof market_backup_default === "string" ? market_backup_default : JSON.stringify(market_backup_default);
  var MARKET_JSON_LOCAL_BACKUP_IS_COMPRESSED = typeof market_backup_default === "string";
  var initData_characterSkills = null;
  var initData_characterItems = null;
  var initData_combatAbilities = null;
  var initData_characterHouseRoomMap = null;
  var initData_actionTypeDrinkSlotsMap = null;
  var initData_actionDetailMap = null;
  var initData_levelExperienceTable = null;
  var initData_enhancementLevelSuccessRateTable = null;
  var initData_enhancementLevelTotalBonusMultiplierTable = null;
  var initData_itemDetailMap = null;
  var initData_itemLocationDetailMap = null;
  var initData_houseRoomDetailMap = null;
  var initData_actionCategoryDetailMap = null;
  var initData_abilityDetailMap = null;
  var initData_shopItemDetailMap = null;
  var initData_taskShopItemDetailMap = null;
  var initData_labyrinthShopItemDetailMap = null;
  var initData_openableLootDropMap = null;
  var initData_guildBuffDetailMap = null;
  var initData_characterAbilities = null;
  var initData_myMarketListings = null;
  var marketApiJson = null;
  var marketValuesVersion = null;
  var marketItemValues = {};
  var marketOrderBooks = {};
  var marketPriceBands = {};
  var currentActionsHridList = [];
  var currentEquipmentMap = {};
  var guildBuffLevels = {};
  var guildDataLoaded = false;
  var currentCharacterId = "";
  var currentCharacterName = "";
  var currentCharacterGameMode = "standard";
  var labyrinthActive = false;
  var characterQuests = [];
  var guild = null;
  var guildCharacters = [];
  var guildLeaderboard = [];
  var guildStateUpdatedAt = 0;
  var actionTypeBuffSources = null;
  var equipmentTaskActionBuffs = [];
  Object.defineProperties(runtime.data, {
    MARKET_JSON_LOCAL_BACKUP: {
      enumerable: true,
      get() {
        return MARKET_JSON_LOCAL_BACKUP;
      }
    },
    MARKET_JSON_LOCAL_BACKUP_IS_COMPRESSED: {
      enumerable: true,
      get() {
        return MARKET_JSON_LOCAL_BACKUP_IS_COMPRESSED;
      }
    }
  });
  Object.defineProperties(runtime.state, {
    initData_characterSkills: {
      enumerable: true,
      get() {
        return initData_characterSkills;
      },
      set(value) {
        initData_characterSkills = value;
      }
    },
    initData_characterItems: {
      enumerable: true,
      get() {
        return initData_characterItems;
      },
      set(value) {
        initData_characterItems = value;
      }
    },
    initData_combatAbilities: {
      enumerable: true,
      get() {
        return initData_combatAbilities;
      },
      set(value) {
        initData_combatAbilities = value;
      }
    },
    initData_characterHouseRoomMap: {
      enumerable: true,
      get() {
        return initData_characterHouseRoomMap;
      },
      set(value) {
        initData_characterHouseRoomMap = value;
      }
    },
    initData_actionTypeDrinkSlotsMap: {
      enumerable: true,
      get() {
        return initData_actionTypeDrinkSlotsMap;
      },
      set(value) {
        initData_actionTypeDrinkSlotsMap = value;
      }
    },
    initData_actionDetailMap: {
      enumerable: true,
      get() {
        return initData_actionDetailMap;
      },
      set(value) {
        initData_actionDetailMap = value;
      }
    },
    initData_levelExperienceTable: {
      enumerable: true,
      get() {
        return initData_levelExperienceTable;
      },
      set(value) {
        initData_levelExperienceTable = value;
      }
    },
    initData_enhancementLevelSuccessRateTable: {
      enumerable: true,
      get() {
        return initData_enhancementLevelSuccessRateTable;
      },
      set(value) {
        initData_enhancementLevelSuccessRateTable = value;
      }
    },
    initData_enhancementLevelTotalBonusMultiplierTable: {
      enumerable: true,
      get() {
        return initData_enhancementLevelTotalBonusMultiplierTable;
      },
      set(value) {
        initData_enhancementLevelTotalBonusMultiplierTable = value;
      }
    },
    initData_itemDetailMap: {
      enumerable: true,
      get() {
        return initData_itemDetailMap;
      },
      set(value) {
        initData_itemDetailMap = value;
      }
    },
    initData_itemLocationDetailMap: {
      enumerable: true,
      get() {
        return initData_itemLocationDetailMap;
      },
      set(value) {
        initData_itemLocationDetailMap = value;
      }
    },
    initData_houseRoomDetailMap: {
      enumerable: true,
      get() {
        return initData_houseRoomDetailMap;
      },
      set(value) {
        initData_houseRoomDetailMap = value;
      }
    },
    initData_actionCategoryDetailMap: {
      enumerable: true,
      get() {
        return initData_actionCategoryDetailMap;
      },
      set(value) {
        initData_actionCategoryDetailMap = value;
      }
    },
    initData_abilityDetailMap: {
      enumerable: true,
      get() {
        return initData_abilityDetailMap;
      },
      set(value) {
        initData_abilityDetailMap = value;
      }
    },
    initData_shopItemDetailMap: {
      enumerable: true,
      get() {
        return initData_shopItemDetailMap;
      },
      set(value) {
        initData_shopItemDetailMap = value;
      }
    },
    initData_taskShopItemDetailMap: {
      enumerable: true,
      get() {
        return initData_taskShopItemDetailMap;
      },
      set(value) {
        initData_taskShopItemDetailMap = value;
      }
    },
    initData_labyrinthShopItemDetailMap: {
      enumerable: true,
      get() {
        return initData_labyrinthShopItemDetailMap;
      },
      set(value) {
        initData_labyrinthShopItemDetailMap = value;
      }
    },
    initData_openableLootDropMap: {
      enumerable: true,
      get() {
        return initData_openableLootDropMap;
      },
      set(value) {
        initData_openableLootDropMap = value;
      }
    },
    initData_guildBuffDetailMap: {
      enumerable: true,
      get() {
        return initData_guildBuffDetailMap;
      },
      set(value) {
        initData_guildBuffDetailMap = value;
      }
    },
    initData_characterAbilities: {
      enumerable: true,
      get() {
        return initData_characterAbilities;
      },
      set(value) {
        initData_characterAbilities = value;
      }
    },
    initData_myMarketListings: {
      enumerable: true,
      get() {
        return initData_myMarketListings;
      },
      set(value) {
        initData_myMarketListings = value;
      }
    },
    marketApiJson: {
      enumerable: true,
      get() {
        return marketApiJson;
      },
      set(value) {
        marketApiJson = value;
      }
    },
    marketValuesVersion: {
      enumerable: true,
      get() {
        return marketValuesVersion;
      },
      set(value) {
        marketValuesVersion = value;
      }
    },
    marketItemValues: {
      enumerable: true,
      get() {
        return marketItemValues;
      },
      set(value) {
        marketItemValues = value ?? {};
      }
    },
    marketOrderBooks: {
      enumerable: true,
      get() {
        return marketOrderBooks;
      }
    },
    marketPriceBands: {
      enumerable: true,
      get() {
        return marketPriceBands;
      }
    },
    currentActionsHridList: {
      enumerable: true,
      get() {
        return currentActionsHridList;
      },
      set(value) {
        currentActionsHridList = value;
      }
    },
    currentEquipmentMap: {
      enumerable: true,
      get() {
        return currentEquipmentMap;
      },
      set(value) {
        currentEquipmentMap = value;
      }
    },
    guildBuffLevels: {
      enumerable: true,
      get() {
        return guildBuffLevels;
      },
      set(value) {
        guildBuffLevels = value ?? {};
      }
    },
    guildDataLoaded: {
      enumerable: true,
      get() {
        return guildDataLoaded;
      },
      set(value) {
        guildDataLoaded = Boolean(value);
      }
    },
    currentCharacterId: {
      enumerable: true,
      get() {
        return currentCharacterId;
      },
      set(value) {
        currentCharacterId = String(value ?? "");
      }
    },
    currentCharacterName: {
      enumerable: true,
      get() {
        return currentCharacterName;
      },
      set(value) {
        currentCharacterName = String(value ?? "");
      }
    },
    currentCharacterGameMode: {
      enumerable: true,
      get() {
        return currentCharacterGameMode;
      },
      set(value) {
        currentCharacterGameMode = String(value ?? "standard").toLowerCase();
      }
    },
    labyrinthActive: {
      enumerable: true,
      get() {
        return labyrinthActive;
      },
      set(value) {
        labyrinthActive = Boolean(value);
      }
    },
    characterQuests: {
      enumerable: true,
      get() {
        return characterQuests;
      },
      set(value) {
        characterQuests = Array.isArray(value) ? value : [];
      }
    },
    guild: {
      enumerable: true,
      get() {
        return guild;
      },
      set(value) {
        guild = value ?? null;
      }
    },
    guildCharacters: {
      enumerable: true,
      get() {
        return guildCharacters;
      },
      set(value) {
        guildCharacters = Array.isArray(value) ? value : [];
      }
    },
    guildLeaderboard: {
      enumerable: true,
      get() {
        return guildLeaderboard;
      },
      set(value) {
        guildLeaderboard = Array.isArray(value) ? value : [];
      }
    },
    guildStateUpdatedAt: {
      enumerable: true,
      get() {
        return guildStateUpdatedAt;
      },
      set(value) {
        guildStateUpdatedAt = Number(value) || Date.now();
      }
    },
    actionTypeBuffSources: {
      enumerable: true,
      get() {
        return actionTypeBuffSources;
      },
      set(value) {
        actionTypeBuffSources = value && typeof value === "object" ? value : null;
      }
    },
    equipmentTaskActionBuffs: {
      enumerable: true,
      get() {
        return equipmentTaskActionBuffs;
      },
      set(value) {
        equipmentTaskActionBuffs = Array.isArray(value) ? value : [];
      }
    }
  });

  // src/core/game-localization.js
  var SUPPORTED_GAME_LOCALES = Object.freeze([
    "en",
    "es",
    "fr",
    "pt",
    "zh",
    "zh-TW",
    "ja",
    "ko",
    "ru"
  ]);
  var LOCALE_SET = new Set(SUPPORTED_GAME_LOCALES);
  var LOCALE_CACHE_SCHEMA = 1;
  var LOCALE_CACHE_PREFIX = "MWITools_game_locale_v1";
  var ENTITY_TYPES = Object.freeze({
    item: {
      resourceKey: "itemNames",
      clientDataKey: "itemDetailMap",
      stateKey: "initData_itemDetailMap",
      prefix: "/items/",
      sprite: "items_sprite"
    },
    action: {
      resourceKey: "actionNames",
      clientDataKey: "actionDetailMap",
      stateKey: "initData_actionDetailMap",
      prefix: "/actions/",
      sprite: "actions_sprite"
    },
    monster: {
      resourceKey: "monsterNames",
      clientDataKey: "combatMonsterDetailMap",
      stateKey: "initData_combatMonsterDetailMap",
      prefix: "/monsters/",
      sprite: "combat_monsters_sprite"
    },
    ability: {
      resourceKey: "abilityNames",
      clientDataKey: "abilityDetailMap",
      stateKey: "initData_abilityDetailMap",
      prefix: "/abilities/",
      sprite: "abilities_sprite"
    },
    skill: {
      resourceKey: "skillNames",
      clientDataKey: "skillDetailMap",
      stateKey: "initData_skillDetailMap",
      prefix: "/skills/",
      sprite: "skills_sprite"
    },
    houseRoom: {
      resourceKey: "houseRoomNames",
      clientDataKey: "houseRoomDetailMap",
      stateKey: "initData_houseRoomDetailMap",
      prefix: "/house_rooms/"
    },
    buffType: {
      resourceKey: "buffTypeNames",
      clientDataKey: "buffTypeDetailMap",
      stateKey: "initData_buffTypeDetailMap",
      prefix: "/buff_types/"
    },
    itemCategory: {
      resourceKey: "itemCategoryNames",
      clientDataKey: "itemCategoryDetailMap",
      stateKey: "initData_itemCategoryDetailMap",
      prefix: "/item_categories/"
    },
    actionCategory: {
      resourceKey: "actionCategoryNames",
      clientDataKey: "actionCategoryDetailMap",
      stateKey: "initData_actionCategoryDetailMap",
      prefix: "/action_categories/"
    },
    shopCategory: {
      resourceKey: "shopCategoryNames",
      clientDataKey: "shopCategoryDetailMap",
      stateKey: "initData_shopCategoryDetailMap",
      prefix: "/shop_categories/"
    },
    achievement: {
      resourceKey: "achievementNames",
      clientDataKey: "achievementDetailMap",
      stateKey: "initData_achievementDetailMap",
      prefix: "/achievements/"
    }
  });
  var TYPE_ALIASES = Object.freeze({
    items: "item",
    actions: "action",
    monsters: "monster",
    abilities: "ability",
    skills: "skill",
    houseRooms: "houseRoom",
    buffTypes: "buffType",
    itemCategories: "itemCategory",
    actionCategories: "actionCategory",
    shopCategories: "shopCategory",
    achievements: "achievement"
  });
  var localeResources = /* @__PURE__ */ new Map();
  var reverseIndexes = /* @__PURE__ */ new WeakMap();
  var warnedLocales = /* @__PURE__ */ new Set();
  var englishResourceCache = null;
  var englishResourceSignature = [];
  function localizedText(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function pageGlobal2() {
    return globalThis.unsafeWindow ?? globalThis.window ?? globalThis;
  }
  function normalizeGameLocale(value) {
    const raw = String(value ?? "").trim().replaceAll("_", "-");
    if (!raw) return "en";
    const lower = raw.toLowerCase();
    if (lower === "zh-tw" || lower === "zh-hant" || lower.startsWith("zh-hant-") || lower === "zh-hk" || lower === "zh-mo") {
      return "zh-TW";
    }
    if (lower === "zh" || lower.startsWith("zh-")) return "zh";
    const base = lower.split("-")[0];
    return LOCALE_SET.has(base) ? base : "en";
  }
  function getGameLocale() {
    return normalizeGameLocale(
      runtime.config.gameLanguage ?? globalThis.localStorage?.getItem?.("i18nextLng") ?? globalThis.document?.documentElement?.lang ?? globalThis.navigator?.language
    );
  }
  function webpackQueues(target = pageGlobal2()) {
    const queues = [];
    for (const key of Object.getOwnPropertyNames(target ?? {})) {
      if (!/^webpack(?:Jsonp|Chunk)/i.test(key)) continue;
      try {
        if (Array.isArray(target[key])) queues.push(target[key]);
      } catch {
      }
    }
    return queues;
  }
  function chunkEntries(target) {
    return webpackQueues(target).flatMap(
      (queue) => queue.filter((entry) => entry?.[1] && typeof entry[1] === "object")
    );
  }
  function localeModuleMap(entries) {
    const result = /* @__PURE__ */ new Map();
    const pattern = /["']\.\/([^"'\\]+)\/index\.js["']\s*:\s*\[\s*(\d+)\s*,\s*(\d+)\s*\]/g;
    for (const entry of entries) {
      for (const factory of Object.values(entry[1])) {
        if (typeof factory !== "function") continue;
        const source = Function.prototype.toString.call(factory);
        if (!source.includes("./zh-TW/index.js")) continue;
        for (const match of source.matchAll(pattern)) {
          result.set(normalizeGameLocale(match[1]), String(match[2]));
        }
      }
    }
    return result;
  }
  function runLocaleFactory(factory) {
    const module = { exports: {} };
    const exports = module.exports;
    const webpackRequire = () => {
      throw new Error();
    };
    webpackRequire.r = (target) => {
      Object.defineProperty(target, "__esModule", { value: true });
    };
    webpackRequire.d = (target, nameOrDefinition, getter) => {
      const definition = typeof nameOrDefinition === "object" ? nameOrDefinition : { [nameOrDefinition]: getter };
      for (const [name, get] of Object.entries(definition)) {
        if (typeof get !== "function" || Object.hasOwn(target, name)) continue;
        Object.defineProperty(target, name, { enumerable: true, get });
      }
    };
    factory.call(exports, module, exports, webpackRequire);
    return module.exports?.default ?? exports.default ?? module.exports;
  }
  function validResourceMap(value, prefix) {
    if (!value || typeof value !== "object" || Array.isArray(value)) return false;
    return Object.keys(value).some((key) => key.startsWith(prefix));
  }
  function validateGameLocaleResources(value) {
    return Boolean(
      value && typeof value === "object" && validResourceMap(value.itemNames, "/items/") && validResourceMap(value.actionNames, "/actions/") && validResourceMap(value.monsterNames, "/monsters/") && validResourceMap(value.abilityNames, "/abilities/")
    );
  }
  function extractGameLocaleResources(locale = getGameLocale(), target = pageGlobal2()) {
    const normalizedLocale = normalizeGameLocale(locale);
    const entries = chunkEntries(target);
    const expectedModuleId = localeModuleMap(entries).get(normalizedLocale);
    if (!expectedModuleId) return null;
    const candidates = [];
    for (const entry of entries) {
      for (const [moduleId, factory] of Object.entries(entry[1])) {
        if (typeof factory !== "function") continue;
        if (moduleId !== expectedModuleId) continue;
        candidates.push(factory);
      }
    }
    for (const factory of candidates) {
      try {
        const resources = runLocaleFactory(factory);
        if (validateGameLocaleResources(resources)) return resources;
      } catch {
      }
    }
    return null;
  }
  function i18nFromFiber(element) {
    const fiberKey2 = Reflect.ownKeys(element ?? {}).find(
      (key) => String(key).startsWith("__reactFiber$")
    );
    for (let fiber = fiberKey2 ? element[fiberKey2] : null, depth = 0; fiber && depth < 80; fiber = fiber.return, depth += 1) {
      for (const candidate of [
        fiber.memoizedProps?.i18n,
        fiber.pendingProps?.i18n,
        fiber.stateNode?.props?.i18n,
        fiber.stateNode?.i18n
      ]) {
        if (candidate?.options?.resources) return candidate;
      }
    }
    return null;
  }
  function translationForLocale(i18n, locale) {
    const normalized = normalizeGameLocale(locale);
    const match = Object.entries(i18n?.options?.resources ?? {}).find(
      ([key]) => normalizeGameLocale(key) === normalized
    );
    return match?.[1]?.translation ?? null;
  }
  function extractReactGameLocaleResources(locale = getGameLocale(), documentTarget = globalThis.document) {
    if (!documentTarget?.querySelector) return null;
    const candidates = /* @__PURE__ */ new Set([
      documentTarget.querySelector("#root"),
      documentTarget.querySelector('[class^="GamePage"]'),
      documentTarget.body
    ]);
    if (![...candidates].some((element) => i18nFromFiber(element))) {
      let inspected = 0;
      for (const element of documentTarget.querySelectorAll("*")) {
        if (inspected >= 300) break;
        if (Reflect.ownKeys(element).some(
          (key) => String(key).startsWith("__reactFiber$")
        )) {
          candidates.add(element);
          inspected += 1;
        }
      }
    }
    for (const element of candidates) {
      const resources = translationForLocale(i18nFromFiber(element), locale);
      if (validateGameLocaleResources(resources)) return resources;
    }
    return null;
  }
  function clientData2() {
    return runtime.state.clientData ?? runtime.api.getGameClientData?.() ?? null;
  }
  function englishResourceDetails(type) {
    const definition = ENTITY_TYPES[type];
    return clientData2()?.[definition.clientDataKey] ?? runtime.state[definition.stateKey] ?? (type === "monster" ? runtime.state.initData_monsterDetailMap : null);
  }
  function englishResourceMap(type, details = englishResourceDetails(type)) {
    const result = Object.fromEntries(
      Object.entries(details ?? {}).map(([hrid, detail]) => [hrid, String(detail?.name ?? "").trim()]).filter(([, name]) => name)
    );
    if (type === "item") {
      for (const [name, hrid] of Object.entries(
        runtime.state.itemEnNameToHridMap ?? {}
      )) {
        if (!result[hrid] && name) result[hrid] = name;
      }
    }
    return result;
  }
  function englishResourceName(type, hrid) {
    const details = englishResourceDetails(type);
    const detail = details instanceof Map ? details.get(hrid) : details?.[hrid];
    return String(detail?.name ?? "").trim();
  }
  function englishResources() {
    const entries = Object.entries(ENTITY_TYPES).map(([type, definition]) => [
      type,
      definition,
      englishResourceDetails(type)
    ]);
    const signature = [
      gameVersionKey(),
      clientData2(),
      runtime.state.itemEnNameToHridMap,
      ...entries.map(([, , details]) => details)
    ];
    if (englishResourceCache && signature.length === englishResourceSignature.length && signature.every((value, index) => value === englishResourceSignature[index])) {
      return englishResourceCache;
    }
    englishResourceSignature = signature;
    englishResourceCache = Object.fromEntries(
      entries.map(([type, definition, details]) => [
        definition.resourceKey,
        englishResourceMap(type, details)
      ])
    );
    return englishResourceCache;
  }
  function gameVersionKey() {
    const data = clientData2();
    return String(data?.versionTimestamp ?? data?.gameVersion ?? "").trim();
  }
  function localeCacheKey(locale) {
    const version = gameVersionKey();
    return version ? `${LOCALE_CACHE_PREFIX}:${encodeURIComponent(version)}:${normalizeGameLocale(locale)}` : "";
  }
  function readCachedResources(locale) {
    const key = localeCacheKey(locale);
    if (!key) return null;
    try {
      const cached = JSON.parse(
        globalThis.localStorage?.getItem?.(key) || "null"
      );
      return cached?.schemaVersion === LOCALE_CACHE_SCHEMA && validateGameLocaleResources(cached.resources) ? cached.resources : null;
    } catch {
      return null;
    }
  }
  function writeCachedResources(locale, resources) {
    const key = localeCacheKey(locale);
    if (!key || !validateGameLocaleResources(resources)) return;
    try {
      globalThis.localStorage?.setItem?.(
        key,
        JSON.stringify({ schemaVersion: LOCALE_CACHE_SCHEMA, resources })
      );
    } catch {
    }
  }
  function getGameLocaleResources(locale = getGameLocale()) {
    const normalizedLocale = normalizeGameLocale(locale);
    if (localeResources.has(normalizedLocale)) {
      return localeResources.get(normalizedLocale);
    }
    const official = extractReactGameLocaleResources(normalizedLocale) ?? extractGameLocaleResources(normalizedLocale) ?? readCachedResources(normalizedLocale);
    if (official) {
      localeResources.set(normalizedLocale, official);
      writeCachedResources(normalizedLocale, official);
      return official;
    }
    if (normalizedLocale === "en") return englishResources();
    if (!warnedLocales.has(normalizedLocale)) {
      warnedLocales.add(normalizedLocale);
      console.warn(
        localizedText(
          `[MWITools] 官方 ${normalizedLocale} 语言资源尚未就绪,将暂时使用英文名称。`,
          `[MWITools] Official ${normalizedLocale} resources are not ready; English names will be used temporarily.`
        )
      );
    }
    return null;
  }
  function registerGameLocaleResources(locale, resources) {
    const normalizedLocale = normalizeGameLocale(locale);
    if (!validateGameLocaleResources(resources)) return false;
    localeResources.set(normalizedLocale, resources);
    writeCachedResources(normalizedLocale, resources);
    return true;
  }
  function refreshGameLocaleResources(locale = getGameLocale()) {
    const normalizedLocale = normalizeGameLocale(locale);
    localeResources.delete(normalizedLocale);
    warnedLocales.delete(normalizedLocale);
    if (normalizedLocale === "en") {
      englishResourceCache = null;
      englishResourceSignature = [];
    }
    return getGameLocaleResources(normalizedLocale);
  }
  function entityType(kind) {
    const normalized = TYPE_ALIASES[kind] ?? kind;
    return ENTITY_TYPES[normalized] ? normalized : null;
  }
  function normalizedName(value, type = "") {
    let result = String(value ?? "").normalize("NFKC").replaceAll(/\s+/g, " ").trim();
    if (type === "item") result = result.replace(/\s+\+\d+\s*$/, "").trim();
    return result;
  }
  function reverseIndex(resources, type) {
    let indexes = reverseIndexes.get(resources);
    if (!indexes) {
      indexes = /* @__PURE__ */ new Map();
      reverseIndexes.set(resources, indexes);
    }
    if (indexes.has(type)) return indexes.get(type);
    const definition = ENTITY_TYPES[type];
    const index = /* @__PURE__ */ new Map();
    for (const [hrid, name] of Object.entries(
      resources?.[definition.resourceKey] ?? {}
    )) {
      const key = normalizedName(name, type);
      if (!key) continue;
      if (index.has(key) && index.get(key) !== hrid) index.set(key, null);
      else index.set(key, hrid);
    }
    indexes.set(type, index);
    return index;
  }
  function directHrid(type, value) {
    const candidate = String(value ?? "").trim();
    return candidate.startsWith(ENTITY_TYPES[type].prefix) ? candidate : "";
  }
  function resolveLocalizedEntity(kind, name, { locale = getGameLocale() } = {}) {
    const type = entityType(kind);
    if (!type) return "";
    const direct = directHrid(type, name);
    if (direct) return direct;
    const key = normalizedName(name, type);
    if (!key) return "";
    const localizedResources = getGameLocaleResources(locale);
    if (localizedResources) {
      const match = reverseIndex(localizedResources, type).get(key);
      if (match) return match;
    }
    const fallbackResources = englishResources();
    if (fallbackResources !== localizedResources) {
      const match = reverseIndex(fallbackResources, type).get(key);
      if (match) return match;
    }
    if (type === "item") {
      for (const [englishName, hrid] of Object.entries(
        runtime.state.itemEnNameToHridMap ?? {}
      )) {
        if (normalizedName(englishName, type) === key) return hrid;
      }
    }
    return "";
  }
  function getLocalizedEntityName(kind, hrid, { locale = getGameLocale(), fallback = "" } = {}) {
    const type = entityType(kind);
    if (!type) return fallback;
    const definition = ENTITY_TYPES[type];
    const localizedName = getGameLocaleResources(locale)?.[definition.resourceKey]?.[hrid];
    return localizedName ?? (englishResourceName(type, hrid) || englishResources()[definition.resourceKey]?.[hrid] || fallback);
  }
  function elementCandidates(element) {
    const result = [];
    for (let current = element; current && result.length < 24; ) {
      result.push(current);
      current = current.parentElement;
    }
    for (const child of element?.querySelectorAll?.("[data-hrid],svg,use") ?? []) {
      if (!result.includes(child)) result.push(child);
    }
    return result;
  }
  function datasetHrid(type, element) {
    const names = [
      `${type}Hrid`,
      "hrid",
      "itemHrid",
      "actionHrid",
      "monsterHrid",
      "abilityHrid",
      "skillHrid"
    ];
    for (const name of names) {
      const value = element?.dataset?.[name];
      const direct = directHrid(type, value);
      if (direct) return direct;
    }
    return "";
  }
  function spriteHrid(type, element) {
    const definition = ENTITY_TYPES[type];
    if (!definition.sprite) return "";
    const href = String(
      element?.getAttribute?.("href") ?? element?.getAttribute?.("xlink:href") ?? element?.querySelector?.("use")?.getAttribute?.("href") ?? ""
    );
    const [base, fragment = ""] = href.split("#");
    return fragment && base.includes(definition.sprite) ? `${definition.prefix}${fragment}` : "";
  }
  function resolveEntityFromElement(kind, element, { locale = getGameLocale() } = {}) {
    const type = entityType(kind);
    if (!type || !element) return "";
    const candidates = elementCandidates(element);
    for (const candidate of candidates) {
      const hrid = datasetHrid(type, candidate) || spriteHrid(type, candidate);
      if (hrid) return hrid;
    }
    for (const candidate of candidates) {
      for (const value of [
        candidate?.getAttribute?.("aria-label"),
        candidate?.getAttribute?.("title"),
        candidate?.textContent
      ]) {
        const hrid = resolveLocalizedEntity(type, value, { locale });
        if (hrid) return hrid;
      }
    }
    return "";
  }
  function getGameTranslation(path, { locale = getGameLocale() } = {}) {
    let value = getGameLocaleResources(locale);
    for (const key of String(path ?? "").replace(/^translation\./, "").split(".")) {
      if (!key || !value || typeof value !== "object") return "";
      value = value[key];
    }
    return typeof value === "string" ? value : "";
  }
  function escapeRegularExpression(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }
  function matchesGameTranslation(path, text, { locale = getGameLocale() } = {}) {
    const template = getGameTranslation(path, { locale });
    if (!template) return false;
    const parts = template.split(/(<[^>]+\/>|{{[^}]+}}|\$t\([^)]*\))/g);
    const pattern = parts.map(
      (part, index) => index % 2 ? ".*?" : escapeRegularExpression(part).replace(/\s+/g, "\\s+")
    ).join("");
    return new RegExp(`^${pattern}$`, "iu").test(String(text ?? "").trim());
  }
  function matchesGameTranslations(paths, text, { locale = getGameLocale(), fallbackPatterns = [] } = {}) {
    const value = String(text ?? "").trim();
    if ([...Array.isArray(paths) ? paths : [paths]].some(
      (path) => matchesGameTranslation(path, value, { locale })
    )) {
      return true;
    }
    return fallbackPatterns.some(
      (pattern) => pattern instanceof RegExp ? pattern.test(value) : String(pattern ?? "").trim().toLocaleLowerCase() === value.toLocaleLowerCase()
    );
  }
  function resetGameLocalizationCache() {
    localeResources.clear();
    warnedLocales.clear();
    englishResourceCache = null;
    englishResourceSignature = [];
  }
  Object.assign(runtime.api, {
    getGameLocale,
    getGameLocaleResources,
    registerGameLocaleResources,
    refreshGameLocaleResources,
    resetGameLocalizationCache,
    getGameTranslation,
    matchesGameTranslation,
    resolveLocalizedEntity,
    resolveEntityFromElement,
    getLocalizedEntityName
  });

  // src/core/localization.js
  function localize(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function tailLabel(hrid) {
    return String(hrid ?? "").split("/").at(-1)?.replaceAll("_", " ") ?? "";
  }
  function entityName(kind, hrid, { fallbackZh = "", fallbackEn = "", detail = null, fallback = "" } = {}) {
    const official = getLocalizedEntityName(kind, hrid);
    const detailName = String(detail?.name ?? "").trim();
    const diagnostic = fallback || tailLabel(hrid) || "—";
    return official || (runtime.config.isZH ? fallbackZh : fallbackEn) || detailName || (runtime.config.isZH ? fallbackEn : fallbackZh) || diagnostic;
  }
  var itemName = (hrid, options) => entityName("item", hrid, options);
  var actionName = (hrid, options) => entityName("action", hrid, options);
  var abilityName = (hrid, options) => entityName("ability", hrid, options);
  var monsterName = (hrid, options) => entityName("monster", hrid, options);

  // src/core/market.js
  var import_lz_string = __toESM(require_lz_string(), 1);
  var MARKET_TAX_RATE = 0.05;
  var COWBELL_TAX_RATE = 0.18;
  var MARKET_MAX_PRICE = 1e12;
  var TEST_MARKET_REFRESH_MS = 10 * 60 * 1e3;
  var PRODUCTION_MARKET_REFRESH_MS = 6 * 60 * 60 * 1e3;
  var assetValuationMarketSnapshot = null;
  var assetValuationMarketDirty = false;
  var decodedLocalMarketBackup;
  function getLocalMarketBackup() {
    const backup = runtime.data.MARKET_JSON_LOCAL_BACKUP;
    if (!runtime.data.MARKET_JSON_LOCAL_BACKUP_IS_COMPRESSED) return backup;
    if (decodedLocalMarketBackup !== void 0) return decodedLocalMarketBackup;
    decodedLocalMarketBackup = import_lz_string.default.decompressFromBase64(backup) || null;
    return decodedLocalMarketBackup;
  }
  function getMarketEnvironment(hostname = globalThis.location?.hostname ?? "") {
    if (hostname.startsWith("test.")) return "test";
    if (hostname.endsWith("milkywayidlecn.com")) return "china";
    return "production";
  }
  function getMarketApiUrl(hostname = globalThis.location?.hostname ?? "") {
    switch (getMarketEnvironment(hostname)) {
      case "test":
        return "https://test.milkywayidle.com/game_data/marketplace.json";
      case "china":
        return "https://milkywayidlecn.com/game_data/marketplace.json";
      default:
        return "https://www.milkywayidle.com/game_data/marketplace.json";
    }
  }
  function getMarketRefreshInterval(hostname = globalThis.location?.hostname ?? "") {
    return getMarketEnvironment(hostname) === "test" ? TEST_MARKET_REFRESH_MS : PRODUCTION_MARKET_REFRESH_MS;
  }
  function getLevelValue(map, itemHrid, enhancementLevel = 0) {
    const itemValues = map?.[itemHrid];
    if (!itemValues) return null;
    const value = itemValues[enhancementLevel] ?? itemValues[String(enhancementLevel)];
    return Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value) : null;
  }
  function getMarketRecordFrom(marketApiJson2, itemHrid, enhancementLevel = 0) {
    return marketApiJson2?.marketData?.[itemHrid]?.[enhancementLevel] ?? marketApiJson2?.marketData?.[itemHrid]?.[String(enhancementLevel)] ?? null;
  }
  function getMarketRecord(itemHrid, enhancementLevel = 0) {
    return getMarketRecordFrom(
      runtime.state.marketApiJson,
      itemHrid,
      enhancementLevel
    );
  }
  function cloneMarketItemValues(source) {
    return Object.fromEntries(
      Object.entries(source ?? {}).map(([itemHrid, levels]) => [
        itemHrid,
        { ...levels ?? {} }
      ])
    );
  }
  function ensureAssetValuationMarketSnapshot() {
    assetValuationMarketSnapshot ??= {
      marketApiJson: runtime.state.marketApiJson,
      marketItemValues: cloneMarketItemValues(runtime.state.marketItemValues)
    };
    return assetValuationMarketSnapshot;
  }
  function markAssetValuationMarketDirty() {
    if (assetValuationMarketSnapshot) assetValuationMarketDirty = true;
  }
  function resetAssetValuationMarketSnapshot() {
    assetValuationMarketSnapshot = null;
    assetValuationMarketDirty = false;
  }
  function isAssetValuationMarketDirty() {
    return assetValuationMarketDirty;
  }
  function getAskPrice(itemHrid, enhancementLevel = 0) {
    const price = Number(getMarketRecord(itemHrid, enhancementLevel)?.a);
    return price > 0 ? price : 0;
  }
  function getBidPrice(itemHrid, enhancementLevel = 0) {
    const price = Number(getMarketRecord(itemHrid, enhancementLevel)?.b);
    return price > 0 ? price : 0;
  }
  function getFairValue(itemHrid, enhancementLevel = 0) {
    const serverValue = getLevelValue(
      runtime.state.marketItemValues,
      itemHrid,
      enhancementLevel
    );
    if (serverValue !== null && serverValue > 0) return serverValue;
    const ask = getAskPrice(itemHrid, enhancementLevel);
    const bid = getBidPrice(itemHrid, enhancementLevel);
    if (ask > 0 && bid > 0) return (ask + bid) / 2;
    return ask || bid || 0;
  }
  function getAssetMarketRecord(itemHrid, enhancementLevel = 0) {
    return getMarketRecordFrom(
      ensureAssetValuationMarketSnapshot().marketApiJson,
      itemHrid,
      enhancementLevel
    );
  }
  function getAssetAskPrice(itemHrid, enhancementLevel = 0) {
    const price = Number(getAssetMarketRecord(itemHrid, enhancementLevel)?.a);
    return price > 0 ? price : 0;
  }
  function getAssetBidPrice(itemHrid, enhancementLevel = 0) {
    const price = Number(getAssetMarketRecord(itemHrid, enhancementLevel)?.b);
    return price > 0 ? price : 0;
  }
  function getAssetFairValue(itemHrid, enhancementLevel = 0) {
    const snapshot = ensureAssetValuationMarketSnapshot();
    const serverValue = getLevelValue(
      snapshot.marketItemValues,
      itemHrid,
      enhancementLevel
    );
    if (serverValue !== null && serverValue > 0) return serverValue;
    const ask = getAssetAskPrice(itemHrid, enhancementLevel);
    const bid = getAssetBidPrice(itemHrid, enhancementLevel);
    if (ask > 0 && bid > 0) return (ask + bid) / 2;
    return ask || bid || 0;
  }
  function getAssetNetSellPrice(itemHrid, enhancementLevel = 0) {
    return getAssetBidPrice(itemHrid, enhancementLevel) * (1 - getMarketTaxRate(itemHrid));
  }
  function getAssetNetSellPriceAtAsk(itemHrid, enhancementLevel = 0) {
    return getAssetAskPrice(itemHrid, enhancementLevel) * (1 - getMarketTaxRate(itemHrid));
  }
  function getMarketTaxRate(itemHrid) {
    return itemHrid === "/items/bag_of_10_cowbells" ? COWBELL_TAX_RATE : MARKET_TAX_RATE;
  }
  function getNetSellPrice(itemHrid, enhancementLevel = 0) {
    return getBidPrice(itemHrid, enhancementLevel) * (1 - getMarketTaxRate(itemHrid));
  }
  function getNetSellPriceAtAsk(itemHrid, enhancementLevel = 0) {
    return getAskPrice(itemHrid, enhancementLevel) * (1 - getMarketTaxRate(itemHrid));
  }
  function getMarketPriceIncrement(price) {
    const integerPrice = Math.max(1, Math.floor(Math.abs(Number(price) || 0)));
    const priceText = String(integerPrice);
    const firstDigit = Number(priceText[0]);
    const digits = priceText.length;
    if (firstDigit <= 2 && digits >= 4) return 5 * 10 ** (digits - 4);
    if (firstDigit <= 4 && digits >= 3) return 10 ** (digits - 3);
    if (digits >= 3) return 2 * 10 ** (digits - 3);
    return 1;
  }
  function normalizeMarketPrice(price, minimum = 1, maximum = MARKET_MAX_PRICE) {
    const numericPrice = Math.min(
      Math.max(Number(price) || minimum, minimum),
      maximum
    );
    const increment = getMarketPriceIncrement(numericPrice);
    const normalized = Math.round(numericPrice / increment) * increment;
    return Math.min(Math.max(normalized, minimum), maximum);
  }
  function parseCompactNumber(value) {
    if (typeof value === "number") return value;
    const thousandSeparator = runtime.config.THOUSAND_SEPERATOR ?? ",";
    const decimalSeparator = runtime.config.DECIMAL_SEPERATOR || ".";
    let normalized = String(value ?? "").trim().toLowerCase();
    if (thousandSeparator) {
      normalized = normalized.replaceAll(thousandSeparator, "");
    }
    normalized = normalized.replaceAll(/[\s\u00a0\u202f]/g, "").replace(decimalSeparator, ".");
    const match = normalized.match(/^([+-]?(?:\d+\.?\d*|\.\d+))\s*([kmbt])?$/i);
    if (!match) return Number.NaN;
    const multipliers = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 };
    return Number(match[1]) * (multipliers[match[2]] ?? 1);
  }
  function getNumberLocale() {
    return runtime.config.NUMBER_LOCALE || "en-US";
  }
  function formatExactNumber(value, fractionDigits = 20) {
    if (value === null || value === void 0 || value === "") return "—";
    const number3 = Number(value);
    if (!Number.isFinite(number3)) return "—";
    const maximumFractionDigits = Math.min(
      20,
      Math.max(0, Math.floor(Number(fractionDigits) || 0))
    );
    return new Intl.NumberFormat(getNumberLocale(), {
      maximumFractionDigits,
      useGrouping: true
    }).format(number3);
  }
  function numberFormatter(value, digits = 2) {
    if (value === null || value === void 0 || value === "") return "—";
    const number3 = Number(value);
    if (!Number.isFinite(number3)) return "—";
    const absolute = Math.abs(number3);
    const maximumFractionDigits = Math.min(2, Math.max(0, Number(digits) || 0));
    if (absolute < 1e3) {
      return new Intl.NumberFormat(getNumberLocale(), {
        maximumFractionDigits,
        useGrouping: true
      }).format(number3);
    }
    const units = [
      { value: 1e3, symbol: "K" },
      { value: 1e6, symbol: "M" },
      { value: 1e9, symbol: "B" },
      { value: 1e12, symbol: "T" }
    ];
    const capAtMillions = absolute >= 1e6 && runtime.settings.get?.("displayCapMM");
    let unit = capAtMillions ? units[1] : [...units].reverse().find(({ value: size }) => absolute >= size);
    let scaled = number3 / unit.value;
    let rounded = Number(scaled.toFixed(maximumFractionDigits));
    const index = units.indexOf(unit);
    if (!capAtMillions && Math.abs(rounded) >= 1e3 && index < units.length - 1) {
      unit = units[index + 1];
      scaled = number3 / unit.value;
      rounded = Number(scaled.toFixed(maximumFractionDigits));
    }
    return `${rounded.toLocaleString(getNumberLocale(), {
      maximumFractionDigits,
      useGrouping: true
    })}${unit.symbol}`;
  }
  function createFormattedNumber(value, options = {}) {
    const element = document.createElement(options.tagName ?? "span");
    element.className = options.className ?? "mwi-number";
    element.textContent = numberFormatter(value, options.digits ?? 2);
    element.title = formatExactNumber(value);
    if (options.label) element.setAttribute("aria-label", options.label);
    return element;
  }
  function formatScore(value) {
    const numericValue = Number(value);
    if (!Number.isFinite(numericValue)) return "-";
    const fixedValue = numericValue > 100 ? Math.round(numericValue).toString() : numericValue.toFixed(1);
    const [integerPart, decimalPart] = fixedValue.split(".");
    const groupedInteger = integerPart.replace(
      /\B(?=(\d{3})+(?!\d))/g,
      runtime.config.THOUSAND_SEPERATOR || ","
    );
    if (decimalPart === void 0) return groupedInteger;
    return `${groupedInteger}${runtime.config.DECIMAL_SEPERATOR || "."}${decimalPart}`;
  }
  function getPriceBand(itemHrid, enhancementLevel = 0) {
    const storedBand = runtime.state.marketPriceBands?.[itemHrid]?.[enhancementLevel];
    if (storedBand) return storedBand;
    const fairValue = getFairValue(itemHrid, enhancementLevel);
    if (!fairValue) return null;
    return {
      minimum: normalizeMarketPrice(fairValue * 0.9),
      maximum: normalizeMarketPrice(fairValue * 1.1)
    };
  }
  function parseStoredMarketItemValues(rawValue) {
    if (!rawValue) return null;
    const candidates = [
      rawValue,
      import_lz_string.default.decompressFromUTF16(rawValue),
      import_lz_string.default.decompress(rawValue),
      import_lz_string.default.decompressFromBase64(rawValue)
    ];
    for (const candidate of candidates) {
      if (!candidate) continue;
      try {
        const parsed = JSON.parse(candidate);
        if (parsed?.marketItemValues) return parsed;
      } catch {
      }
    }
    return null;
  }
  function loadMarketItemValuesFromStorage() {
    let parsed = null;
    try {
      const pageGlobal5 = globalThis.unsafeWindow ?? globalThis;
      parsed = pageGlobal5.localStorageUtil?.getMarketItemValues?.() ?? null;
    } catch (error) {
      console.error(
        runtime.config.isZH ? "[MWITools] 无法从游戏缓存读取市场价值" : "[MWITools] Unable to read market values from the game cache",
        error
      );
    }
    parsed ??= parseStoredMarketItemValues(
      globalThis.localStorage?.getItem("marketItemValues")
    );
    if (!parsed) return false;
    runtime.state.marketValuesVersion = parsed.marketValuesVersion ?? null;
    runtime.state.marketItemValues = parsed.marketItemValues;
    markAssetValuationMarketDirty();
    return true;
  }
  function validateMarketJsonFetch(jsonValue, isSave = false) {
    if (!jsonValue) return null;
    let jsonObj = jsonValue;
    try {
      if (typeof jsonValue === "string") jsonObj = JSON.parse(jsonValue);
    } catch (error) {
      console.error(
        runtime.config.isZH ? "[MWITools] 市场数据 JSON 解析失败:" : "[MWITools] Failed to parse market data JSON:",
        error.message
      );
      return null;
    }
    if (!jsonObj?.timestamp || !jsonObj?.marketData) return null;
    const fixedPrices = {
      "/items/coin": { a: 1, b: 1 },
      "/items/task_token": { a: 0, b: 0 },
      "/items/cowbell": { a: 0, b: 0 },
      "/items/small_treasure_chest": { a: 0, b: 0 },
      "/items/medium_treasure_chest": { a: 0, b: 0 },
      "/items/large_treasure_chest": { a: 0, b: 0 },
      "/items/basic_task_badge": { a: 0, b: 0 },
      "/items/advanced_task_badge": { a: 0, b: 0 },
      "/items/expert_task_badge": { a: 0, b: 0 }
    };
    for (const [itemHrid, prices] of Object.entries(fixedPrices)) {
      jsonObj.marketData[itemHrid] = { 0: prices };
    }
    runtime.state.marketApiJson = jsonObj;
    markAssetValuationMarketDirty();
    if (isSave) {
      localStorage.setItem("MWITools_marketAPI_timestamp", String(Date.now()));
      localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(jsonObj));
    }
    return jsonObj;
  }
  function setMarketFetchFailure(reasonZh, reasonEn) {
    console.warn(
      runtime.config.isZH ? `[MWITools] ${reasonZh};将优先使用可用的市场缓存。` : `[MWITools] ${reasonEn}; using cached market data when available.`
    );
  }
  function requestMarketJson() {
    const sendRequest = typeof GM !== "undefined" && typeof GM.xmlHttpRequest === "function" ? GM.xmlHttpRequest : typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : null;
    if (!sendRequest) return Promise.resolve(null);
    return new Promise((resolve) => {
      let settled = false;
      let watchdog;
      const finish = (response) => {
        if (settled) return;
        settled = true;
        clearTimeout(watchdog);
        resolve(response);
      };
      watchdog = setTimeout(() => finish(null), 5500);
      const options = {
        url: getMarketApiUrl(),
        method: "GET",
        timeout: 5e3,
        onload: finish,
        onabort: () => finish(null),
        onerror: () => finish(null),
        ontimeout: () => finish(null)
      };
      try {
        const result = sendRequest(options);
        if (result?.then) result.then(finish).catch(() => finish(null));
      } catch (error) {
        console.error(
          runtime.config.isZH ? "[MWITools] 市场数据请求失败" : "[MWITools] Market data request failed",
          error
        );
        finish(null);
      }
    });
  }
  function hasMarketValueSource() {
    return Boolean(
      runtime.state.marketApiJson || Object.keys(runtime.state.marketItemValues ?? {}).length
    );
  }
  async function ensureMarketValueSource() {
    if (hasMarketValueSource()) return true;
    return Boolean(await fetchMarketJSON());
  }
  async function fetchMarketJSON(forceFetch = false) {
    const cacheTimestamp = Number(
      localStorage.getItem("MWITools_marketAPI_timestamp")
    );
    const cachedJson = localStorage.getItem("MWITools_marketAPI_json");
    if (!forceFetch && cachedJson && cacheTimestamp && Date.now() - cacheTimestamp < getMarketRefreshInterval()) {
      return validateMarketJsonFetch(cachedJson, false);
    }
    const response = await requestMarketJson();
    const jsonObj = validateMarketJsonFetch(
      response?.status === 200 ? response.responseText : null,
      true
    );
    if (jsonObj) {
      return jsonObj;
    }
    setMarketFetchFailure("市场 API 请求失败", "Market API request failed");
    if (cachedJson) {
      const cached = validateMarketJsonFetch(cachedJson, false);
      if (cached) return cached;
    }
    if (getMarketEnvironment() === "test") return null;
    return validateMarketJsonFetch(getLocalMarketBackup(), false);
  }
  function applyMarketItemValues(payload) {
    if (!payload.marketItemValues) return;
    runtime.state.marketValuesVersion = payload.marketValuesVersion ?? null;
    runtime.state.marketItemValues = payload.marketItemValues;
    markAssetValuationMarketDirty();
  }
  function applyMarketOrderBooks(payload) {
    const orderBookPayload = payload.marketItemOrderBooks ?? payload;
    const itemHrid = orderBookPayload.itemHrid;
    if (!itemHrid) return;
    runtime.state.marketOrderBooks[itemHrid] = orderBookPayload.orderBooks ?? {};
    if (orderBookPayload.marketValues) {
      runtime.state.marketItemValues[itemHrid] = {
        ...runtime.state.marketItemValues[itemHrid] ?? {},
        ...orderBookPayload.marketValues
      };
      markAssetValuationMarketDirty();
    }
    const minimums = orderBookPayload.priceBandMins ?? {};
    const maximums = orderBookPayload.priceBandMaxs ?? {};
    const levels = /* @__PURE__ */ new Set([...Object.keys(minimums), ...Object.keys(maximums)]);
    runtime.state.marketPriceBands[itemHrid] ??= {};
    for (const level of levels) {
      runtime.state.marketPriceBands[itemHrid][level] = {
        minimum: Number(minimums[level]) || 0,
        maximum: Number(maximums[level]) || MARKET_MAX_PRICE
      };
    }
  }
  function applyMarketListings(payload) {
    const listings = payload.myMarketListings ?? payload.marketListings ?? payload.endMarketListings ?? payload.listings;
    if (!Array.isArray(listings)) return;
    if (payload.myMarketListings || payload.marketListings || payload.listings) {
      runtime.state.initData_myMarketListings = listings;
      return;
    }
    const current = [...runtime.state.initData_myMarketListings ?? []];
    for (const listing of listings) {
      const listingId = listing.id ?? listing.marketListingId;
      const index = current.findIndex(
        (candidate) => (candidate.id ?? candidate.marketListingId) === listingId
      );
      if (listing.isDone || listing.isCancelled) {
        if (index >= 0) current.splice(index, 1);
      } else if (index >= 0) {
        current[index] = listing;
      } else {
        current.push(listing);
      }
    }
    runtime.state.initData_myMarketListings = current;
  }
  function getListingWorkingPrice(listing) {
    return Number(listing?.workingPrice) > 0 ? Number(listing.workingPrice) : Number(listing?.price) || 0;
  }
  Object.assign(runtime.api, {
    getMarketEnvironment,
    getMarketApiUrl,
    getMarketRefreshInterval,
    getAskPrice,
    getBidPrice,
    getFairValue,
    getAssetAskPrice,
    getAssetBidPrice,
    getAssetFairValue,
    getAssetNetSellPrice,
    getAssetNetSellPriceAtAsk,
    resetAssetValuationMarketSnapshot,
    isAssetValuationMarketDirty,
    getMarketTaxRate,
    getNetSellPrice,
    getNetSellPriceAtAsk,
    getMarketPriceIncrement,
    normalizeMarketPrice,
    parseCompactNumber,
    numberFormatter,
    formatExactNumber,
    createFormattedNumber,
    formatScore,
    getPriceBand,
    parseStoredMarketItemValues,
    loadMarketItemValuesFromStorage,
    validateMarketJsonFetch,
    fetchMarketJSON,
    hasMarketValueSource,
    ensureMarketValueSource,
    applyMarketItemValues,
    applyMarketOrderBooks,
    applyMarketListings,
    getListingWorkingPrice
  });

  // src/core/action-projection.js
  var MIN_ACTION_SECONDS = 3;
  var DRINKS_PER_HOUR = 12;
  var ENHANCEMENT_BONUSES = Object.freeze([
    0,
    0.02,
    0.042,
    0.066,
    0.092,
    0.12,
    0.15,
    0.182,
    0.216,
    0.255,
    0.29,
    0.33,
    0.372,
    0.416,
    0.462,
    0.51,
    0.56,
    0.612,
    0.666,
    0.722,
    0.78
  ]);
  var BUFF_TYPES = Object.freeze({
    actionSpeed: "/buff_types/action_speed",
    artisan: "/buff_types/artisan",
    essenceFind: "/buff_types/essence_find",
    gathering: "/buff_types/gathering",
    gourmet: "/buff_types/gourmet",
    rareFind: "/buff_types/rare_find"
  });
  var ALCHEMY_ACTION_HRIDS = Object.freeze({
    coinify: "/actions/alchemy/coinify",
    decompose: "/actions/alchemy/decompose",
    transmute: "/actions/alchemy/transmute",
    unrefine: "/actions/alchemy/unrefine"
  });
  var COIN_ITEM_HRID = "/items/coin";
  function asArray(value) {
    if (Array.isArray(value)) return value;
    if (!value) return [];
    try {
      const parsed = JSON.parse(value);
      return Array.isArray(parsed) ? parsed : [];
    } catch {
      return [];
    }
  }
  function getInventoryCount(itemHrid) {
    return (runtime.state.initData_characterItems ?? []).filter(
      (item) => item.itemHrid === itemHrid && item.itemLocationHrid === "/item_locations/inventory"
    ).reduce((sum, item) => sum + Number(item.count || 0), 0);
  }
  function getInventoryItemByHash(hash) {
    if (!hash) return null;
    return (runtime.state.initData_characterItems ?? []).find(
      (item) => item.hash === hash && item.itemLocationHrid === "/item_locations/inventory"
    ) ?? null;
  }
  function getAlchemyCoinCost(actionHrid, itemDetail) {
    const itemLevel = Math.max(0, Number(itemDetail?.itemLevel) || 0);
    if (actionHrid === ALCHEMY_ACTION_HRIDS.coinify) return 0;
    if (actionHrid === ALCHEMY_ACTION_HRIDS.decompose || actionHrid === ALCHEMY_ACTION_HRIDS.unrefine) {
      return Math.floor(5 * (10 + itemLevel));
    }
    if (actionHrid === ALCHEMY_ACTION_HRIDS.transmute) {
      return Math.max(50, Math.floor((Number(itemDetail?.sellPrice) || 0) / 5));
    }
    return 0;
  }
  function getAlchemyCapacity(action, detail, lessResource) {
    if (detail?.function !== "/action_functions/alchemy") return null;
    const primary = getInventoryItemByHash(action?.primaryItemHash);
    if (!primary) {
      return { known: false, maxCraftable: null, missing: ["primaryItem"] };
    }
    const primaryDetail = runtime.state.initData_itemDetailMap?.[primary.itemHrid];
    const bulkMultiplier = Math.max(
      1,
      Math.floor(Number(primaryDetail?.alchemyDetail?.bulkMultiplier) || 1)
    );
    let maxCraftable = Math.floor(
      Math.max(0, Number(primary.count) || 0) / bulkMultiplier
    );
    if (action?.secondaryItemHash) {
      const secondary = getInventoryItemByHash(action.secondaryItemHash);
      if (!secondary) {
        return { known: false, maxCraftable: null, missing: ["secondaryItem"] };
      }
      maxCraftable = primary.hash === secondary.hash ? Math.floor(
        Math.max(0, Number(primary.count) || 0) / (bulkMultiplier + 1)
      ) : Math.min(maxCraftable, Math.max(0, Number(secondary.count) || 0));
    }
    const coinCost = getAlchemyCoinCost(action.actionHrid, primaryDetail);
    if (coinCost > 0) {
      const reduction = Math.min(1, Math.max(0, Number(lessResource) || 0));
      const effectiveCoinCost = coinCost * bulkMultiplier * (1 - reduction);
      if (effectiveCoinCost > 0) {
        maxCraftable = Math.min(
          maxCraftable,
          Math.floor(getInventoryCount(COIN_ITEM_HRID) / effectiveCoinCost)
        );
      }
    }
    return {
      known: true,
      maxCraftable: Math.max(0, maxCraftable),
      bulkMultiplier,
      coinCost,
      primary
    };
  }
  function getExpectedOutputs(detail) {
    if (asArray(detail?.outputItems).length) {
      return asArray(detail.outputItems).map((item) => ({
        itemHrid: item.itemHrid,
        count: Number(item.count) || 0
      }));
    }
    return asArray(detail?.dropTable).map((drop) => ({
      itemHrid: drop.itemHrid,
      count: (Number(drop.dropRate ?? 1) || 0) * ((Number(drop.minCount ?? drop.count ?? 0) + Number(drop.maxCount ?? drop.count ?? 0)) / 2)
    }));
  }
  function getProcessingProductMap() {
    const result = /* @__PURE__ */ new Map();
    for (const [actionHrid, detail] of Object.entries(
      runtime.state.initData_actionDetailMap ?? {}
    )) {
      if (!/(?:fabric|lumber|cheese)$/.test(actionHrid)) continue;
      const input = asArray(detail?.inputItems)[0];
      const output = asArray(detail?.outputItems)[0];
      const inputCount = Number(input?.count) || 0;
      if (!input?.itemHrid || !output?.itemHrid || inputCount <= 0) continue;
      result.set(input.itemHrid, {
        itemHrid: output.itemHrid,
        inputCount
      });
    }
    if (!result.has("/items/rainbow_milk")) {
      result.set("/items/rainbow_milk", {
        itemHrid: "/items/rainbow_cheese",
        inputCount: 2
      });
    }
    return result;
  }
  function getEffectiveOutputs(detail, teaEffects) {
    const quantityMultiplier = 1 + Math.max(0, teaEffects?.quantity ?? 0);
    const processingRate = Math.min(
      1,
      Math.max(0, teaEffects?.upgradedProduct ?? 0)
    );
    const processingMap = processingRate > 0 ? getProcessingProductMap() : null;
    const result = [];
    for (const output of getExpectedOutputs(detail)) {
      const processing = processingMap?.get(output.itemHrid);
      const baseCount = Number(output.count) || 0;
      const rawCount = baseCount * quantityMultiplier * (processing ? 1 - processingRate : 1);
      if (rawCount > 0) {
        result.push({ ...output, baseCount, count: rawCount });
      }
      if (processing) {
        const processedCount = baseCount * quantityMultiplier * processingRate / processing.inputCount;
        if (processedCount > 0) {
          result.push({
            itemHrid: processing.itemHrid,
            baseCount: 0,
            count: processedCount,
            kind: "processed",
            processedFromItemHrid: output.itemHrid
          });
        }
      }
    }
    return result;
  }
  function getDirectInputs(detail) {
    const inputs = asArray(detail?.inputItems).map((item) => ({
      itemHrid: item.itemHrid,
      enhancementLevel: Number(item.enhancementLevel ?? 0) || 0,
      count: Number(item.count) || 0,
      isUpgradeItem: false,
      upgradeItemCount: 0
    }));
    if (detail?.upgradeItemHrid) {
      const matchingIndex = inputs.findIndex(
        (input) => input.itemHrid === detail.upgradeItemHrid
      );
      if (matchingIndex >= 0) {
        inputs[matchingIndex] = {
          ...inputs[matchingIndex],
          count: inputs[matchingIndex].count + 1,
          isUpgradeItem: true,
          upgradeItemCount: 1
        };
      } else {
        inputs.push({
          itemHrid: detail.upgradeItemHrid,
          enhancementLevel: 0,
          count: 1,
          isUpgradeItem: true,
          upgradeItemCount: 1
        });
      }
    }
    return inputs;
  }
  function getEffectiveInputCount(input, lessResource) {
    const count = Math.max(0, Number(input?.count) || 0);
    const reduction = Math.min(1, Math.max(0, Number(lessResource) || 0));
    if (!input?.isUpgradeItem) return count * (1 - reduction);
    const retainedCount = Math.min(
      count,
      Math.max(0, Number(input?.upgradeItemCount) || 1)
    );
    return retainedCount + (count - retainedCount) * (1 - reduction);
  }
  function getActionBuffs(actionHrid) {
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    const sources = runtime.state.actionTypeBuffSources;
    if (!detail || !sources) return null;
    const buffs = [];
    for (const sourceMap of Object.values(sources)) {
      const actionBuffs = sourceMap?.[detail.type];
      if (Array.isArray(actionBuffs)) buffs.push(...actionBuffs);
    }
    return buffs;
  }
  function getActionBuffRatio(actionHrid, typeHrid) {
    const buffs = getActionBuffs(actionHrid);
    if (!buffs) return null;
    return buffs.reduce((total, buff) => {
      if (buff?.typeHrid !== typeHrid) return total;
      return total + (Number(buff.flatBoost) || 0) + (Number(buff.ratioBoost) || 0);
    }, 0);
  }
  function getEquippedItem(itemHrid) {
    const candidates = Object.values(runtime.state.currentEquipmentMap ?? {});
    if (!candidates.length) {
      candidates.push(
        ...(runtime.state.initData_characterItems ?? []).filter(
          (item) => item?.itemLocationHrid !== "/item_locations/inventory"
        )
      );
    }
    return candidates.filter(
      (item) => item?.itemHrid === itemHrid && Number(item.count ?? 1) > 0
    ).sort(
      (left, right) => (Number(right.enhancementLevel) || 0) - (Number(left.enhancementLevel) || 0)
    )[0];
  }
  function getDrinkConcentrationMultiplier(options = {}) {
    const overrideLevel = Number(options.guzzlingPouchLevel);
    const hasOverride = Number.isFinite(overrideLevel) && overrideLevel >= 0;
    const pouch = hasOverride ? null : getEquippedItem("/items/guzzling_pouch");
    if (!hasOverride && !pouch) return 1;
    const detail = runtime.state.initData_itemDetailMap?.["/items/guzzling_pouch"];
    const fallback = hasOverride ? 0.1 : 0;
    const base = Number(
      detail?.equipmentDetail?.noncombatStats?.drinkConcentration ?? fallback
    );
    const enhancement = Number(
      detail?.equipmentDetail?.noncombatEnhancementBonuses?.drinkConcentration ?? base
    );
    const level = Math.max(
      0,
      Math.floor(
        hasOverride ? overrideLevel : Number(pouch.enhancementLevel) || 0
      )
    );
    return Math.max(
      1,
      1 + base + enhancement * (ENHANCEMENT_BONUSES[level] ?? 0)
    );
  }
  function getSelectedDrinks(detail) {
    const slots = runtime.state.initData_actionTypeDrinkSlotsMap;
    if (!slots || typeof slots !== "object") return null;
    return asArray(slots[detail.type]).filter((drink) => drink?.itemHrid);
  }
  function getEffectiveTeaEffects(actionHrid, options = {}) {
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    const drinks = detail ? getSelectedDrinks(detail) : null;
    if (!detail || drinks === null) return null;
    const concentrationMultiplier = getDrinkConcentrationMultiplier(options);
    const base = {
      efficiency: 0,
      extraExp: 0,
      lessResource: 0,
      quantity: 0,
      upgradedProduct: 0
    };
    const drinkDetails = [];
    for (const drink of drinks) {
      const item = runtime.state.initData_itemDetailMap?.[drink.itemHrid];
      if (!item) return null;
      for (const buff of asArray(item.consumableDetail?.buffs)) {
        const flat = Number(buff.flatBoost) || 0;
        if (buff.typeHrid === BUFF_TYPES.artisan) {
          base.lessResource += flat;
        } else if (buff.typeHrid === BUFF_TYPES.gathering || buff.typeHrid === BUFF_TYPES.gourmet) {
          base.quantity += flat;
        } else if (buff.typeHrid === "/buff_types/wisdom") {
          base.extraExp += flat;
        } else if (buff.typeHrid === "/buff_types/processing") {
          base.upgradedProduct += flat;
        } else if (buff.typeHrid === "/buff_types/efficiency") {
          base.efficiency += flat;
        } else if (buff.typeHrid === "/buff_types/action_level") {
          base.efficiency -= flat / 100;
        } else if (buff.typeHrid === `/buff_types/${detail.type.replace("/action_types/", "")}_level`) {
          base.efficiency += flat / 100;
        }
      }
      drinkDetails.push({ itemHrid: drink.itemHrid });
    }
    return {
      concentrationMultiplier,
      drinks: drinkDetails,
      efficiency: base.efficiency * concentrationMultiplier,
      extraExp: base.extraExp * concentrationMultiplier,
      lessResource: Math.min(
        1,
        Math.max(0, base.lessResource * concentrationMultiplier)
      ),
      quantity: Math.max(0, base.quantity * concentrationMultiplier),
      upgradedProduct: Math.max(
        0,
        base.upgradedProduct * concentrationMultiplier
      )
    };
  }
  function projectActionCraftingCost(actionOrHrid, options = {}) {
    const action = typeof actionOrHrid === "string" ? { actionHrid: actionOrHrid } : actionOrHrid ?? {};
    const actionHrid = action.actionHrid ?? action.hrid;
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    if (!actionHrid || !detail) {
      return {
        status: "waiting",
        complete: false,
        actionHrid,
        missing: ["actionData"],
        missingPrices: []
      };
    }
    if (!isPlayerDataReady()) {
      return {
        status: "waiting",
        complete: false,
        actionHrid,
        detail,
        missing: ["playerData"],
        missingPrices: []
      };
    }
    const profile = getActionProductionProfile(action, options);
    if (profile.status !== "complete") {
      return {
        status: "waiting",
        complete: false,
        actionHrid,
        detail,
        missing: ["playerData"],
        missingPrices: []
      };
    }
    const { teaEffects } = profile;
    const getUnitPrice = typeof options.getUnitPrice === "function" ? options.getUnitPrice : () => null;
    const missingPrices = [];
    const inputs = profile.inputs.map((input) => {
      const effectiveCount = input.effectiveCount;
      const unitPrice = Number(
        getUnitPrice(input.itemHrid, input.enhancementLevel ?? 0)
      );
      if (!(unitPrice > 0)) missingPrices.push(input.itemHrid);
      return {
        ...input,
        effectiveCount,
        unitPrice: unitPrice > 0 ? unitPrice : null,
        valuePerAction: unitPrice > 0 ? effectiveCount * unitPrice : null
      };
    });
    const materialCostPerAction = inputs.reduce(
      (total, input) => total + (input.valuePerAction ?? 0),
      0
    );
    const timing = profile.timing;
    const secondsPerAction = timing?.secondsPerAction ?? null;
    const actionsPerHour = secondsPerAction ? 3600 / secondsPerAction : null;
    const drinks = teaEffects.drinks.map((drink) => {
      const unitPrice = Number(getUnitPrice(drink.itemHrid, 0));
      if (!(unitPrice > 0)) missingPrices.push(drink.itemHrid);
      const countPerHour = DRINKS_PER_HOUR * teaEffects.concentrationMultiplier;
      const costPerHour = unitPrice > 0 ? unitPrice * countPerHour : null;
      return {
        ...drink,
        unitPrice: unitPrice > 0 ? unitPrice : null,
        countPerHour,
        costPerHour
      };
    });
    const requiresTiming = drinks.length > 0;
    const teaCostPerHour = drinks.reduce(
      (total, drink) => total + (drink.costPerHour ?? 0),
      0
    );
    const teaCostPerAction = actionsPerHour ? teaCostPerHour / actionsPerHour : requiresTiming ? null : 0;
    const complete = missingPrices.length === 0 && (!requiresTiming || actionsPerHour !== null);
    const totalCostPerAction = complete ? materialCostPerAction + teaCostPerAction : null;
    return {
      status: complete ? "complete" : "incomplete",
      complete,
      actionHrid,
      detail,
      inputs,
      outputs: getExpectedOutputs(detail),
      drinks,
      teaEffects: { ...teaEffects, drinks },
      secondsPerAction,
      actionsPerHour,
      materialCostPerAction,
      teaCostPerHour,
      teaCostPerAction,
      totalCostPerAction,
      missing: requiresTiming && !actionsPerHour ? ["actionTiming"] : [],
      missingPrices: [...new Set(missingPrices)]
    };
  }
  function isPlayerDataReady() {
    return Array.isArray(runtime.state.initData_characterSkills) && Array.isArray(runtime.state.initData_characterItems) && runtime.state.initData_actionTypeDrinkSlotsMap !== null && runtime.state.initData_actionTypeDrinkSlotsMap !== void 0;
  }
  function getActionProductionProfile(actionOrHrid, context = {}) {
    const action = typeof actionOrHrid === "string" ? { actionHrid: actionOrHrid } : actionOrHrid ?? {};
    const actionHrid = action.actionHrid ?? action.hrid;
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    if (!actionHrid || !detail) {
      return { status: "waiting", actionHrid, missing: ["actionData"] };
    }
    if (!isPlayerDataReady()) {
      return {
        status: "waiting",
        actionHrid,
        detail,
        missing: ["playerData"]
      };
    }
    const teaEffects = getEffectiveTeaEffects(actionHrid, context);
    if (!teaEffects) {
      return {
        status: "waiting",
        actionHrid,
        detail,
        missing: ["playerData"]
      };
    }
    const timing = getEffectiveSeconds(actionHrid, detail, context);
    const inputs = getDirectInputs(detail).map((input) => ({
      ...input,
      effectiveCount: getEffectiveInputCount(input, teaEffects.lessResource)
    }));
    const outputs = getEffectiveOutputs(detail, teaEffects);
    return {
      status: "complete",
      actionHrid,
      detail,
      inputs,
      outputs,
      teaEffects,
      timing,
      secondsPerAction: timing?.secondsPerAction ?? null,
      actionsPerHour: timing?.secondsPerAction ? 3600 / timing.secondsPerAction : null,
      baseSeconds: timing?.baseSeconds ?? null,
      cycleSeconds: timing?.cycleSeconds ?? null,
      efficiencyPercent: timing?.efficiencyPercent ?? 0,
      speedPercent: timing?.speedPercent ?? 0,
      timingSource: timing?.timingSource ?? null,
      missing: timing ? [] : ["actionTiming"]
    };
  }
  function getActionCount(action) {
    if (action?.hasMaxCount === false) return Infinity;
    const target = Number(
      action?.targetCount ?? action?.maxCount ?? action?.count ?? action?.actionCount
    );
    if (!Number.isFinite(target) || target < 0) return Infinity;
    const current = Number(action?.currentCount ?? action?.completedCount ?? 0);
    return Math.max(0, target - (Number.isFinite(current) ? current : 0));
  }
  function getEfficiencyMultiplier(actionHrid) {
    const efficiency = Number(runtime.api.getTotalEffiPercentage?.(actionHrid)) || 0;
    return 1 + efficiency / 100;
  }
  function getActionSpeedPercent(actionHrid) {
    const authoritative = getActionBuffRatio(actionHrid, BUFF_TYPES.actionSpeed);
    if (authoritative !== null) return authoritative * 100;
    return Number(runtime.api.getToolsSpeedBuffByActionHrid?.(actionHrid)) || 0;
  }
  function getEffectiveSeconds(actionHrid, detail, context = {}) {
    const efficiencyPercent = (getEfficiencyMultiplier(actionHrid) - 1) * 100;
    const speedPercent = getActionSpeedPercent(actionHrid);
    const liveDuration = Number(context.durationPerAction);
    if (Number.isFinite(liveDuration) && liveDuration > 0) {
      return {
        cycleSeconds: liveDuration,
        efficiencyPercent,
        secondsPerAction: liveDuration / getEfficiencyMultiplier(actionHrid),
        speedPercent,
        timingSource: "live"
      };
    }
    const baseSeconds = Number(detail?.baseTimeCost) / 1e9;
    if (!Number.isFinite(baseSeconds) || baseSeconds <= 0) return null;
    const cycleSeconds = Math.max(
      MIN_ACTION_SECONDS,
      baseSeconds / (1 + speedPercent / 100)
    );
    return {
      baseSeconds,
      cycleSeconds,
      efficiencyPercent,
      secondsPerAction: cycleSeconds / getEfficiencyMultiplier(actionHrid),
      speedPercent,
      timingSource: "calculated"
    };
  }
  function getProjectedTotalSeconds(actionHrid, count, timing, context = {}) {
    if (!Number.isFinite(count)) return Infinity;
    if (!timing?.secondsPerAction) return null;
    const liveDuration = Number(context.durationPerAction);
    if (Number.isFinite(liveDuration) && liveDuration > 0) {
      const cycles = Math.max(
        count > 0 ? 1 : 0,
        Math.round(count / getEfficiencyMultiplier(actionHrid))
      );
      const currentCycleRemaining = Number(context.currentCycleRemainingSeconds);
      return cycles > 0 && Number.isFinite(currentCycleRemaining) && currentCycleRemaining >= 0 ? Math.min(liveDuration, currentCycleRemaining) + Math.max(0, cycles - 1) * liveDuration : cycles * liveDuration;
    }
    return count * timing.secondsPerAction;
  }
  function projectActionTiming(actionOrHrid, requestedCount, context = {}) {
    const action = typeof actionOrHrid === "string" ? { actionHrid: actionOrHrid } : actionOrHrid ?? {};
    const actionHrid = action.actionHrid ?? action.hrid;
    const profile = getActionProductionProfile(action, context);
    const count = requestedCount === void 0 ? getActionCount(action) : Number(requestedCount);
    const infinite = count === Infinity || !Number.isFinite(count);
    const normalizedCount = infinite ? Infinity : Math.max(0, count);
    if (profile.status !== "complete") {
      return {
        ...profile,
        count: normalizedCount,
        infinite,
        totalSeconds: null,
        finishAt: null
      };
    }
    const totalSeconds = getProjectedTotalSeconds(
      actionHrid,
      normalizedCount,
      profile.timing,
      context
    );
    const now = Number(context.now ?? Date.now());
    return {
      ...profile,
      count: normalizedCount,
      infinite,
      totalSeconds,
      finishAt: Number.isFinite(totalSeconds) && totalSeconds !== null ? now + totalSeconds * 1e3 : null
    };
  }
  var PROFIT_VALUATION_MODES = /* @__PURE__ */ new Set(["conservative", "fair", "aggressive"]);
  var PROFIT_ACTION_TYPES = /* @__PURE__ */ new Set([
    "/action_types/alchemy",
    "/action_types/brewing",
    "/action_types/cheesesmithing",
    "/action_types/cooking",
    "/action_types/crafting",
    "/action_types/foraging",
    "/action_types/milking",
    "/action_types/tailoring",
    "/action_types/woodcutting"
  ]);
  var productionActionIndexSource = null;
  var productionActionIndex = null;
  function getProductionActionIndex() {
    const source = runtime.state.initData_actionDetailMap;
    if (source === productionActionIndexSource && productionActionIndex) {
      return productionActionIndex;
    }
    const candidatesByItem = /* @__PURE__ */ new Map();
    for (const [actionHrid, detail] of Object.entries(source ?? {})) {
      if (!PROFIT_ACTION_TYPES.has(detail?.type)) continue;
      for (const output of getExpectedOutputs(detail)) {
        const itemHrid = String(output?.itemHrid ?? "");
        if (!itemHrid || !(Number(output.count) > 0)) continue;
        const candidates = candidatesByItem.get(itemHrid) ?? [];
        candidates.push([actionHrid, detail]);
        candidatesByItem.set(itemHrid, candidates);
      }
    }
    const index = /* @__PURE__ */ new Map();
    for (const [itemHrid, candidates] of candidatesByItem) {
      const slug = itemHrid.split("/").at(-1);
      const exact = candidates.find(
        ([actionHrid]) => actionHrid.endsWith(`/${slug}`)
      );
      if (exact) {
        index.set(itemHrid, exact[0]);
        continue;
      }
      candidates.sort(
        ([leftHrid, left], [rightHrid, right]) => (Number(left?.sortIndex) || 0) - (Number(right?.sortIndex) || 0) || leftHrid.localeCompare(rightHrid)
      );
      index.set(itemHrid, candidates[0][0]);
    }
    productionActionIndexSource = source;
    productionActionIndex = index;
    return productionActionIndex;
  }
  function getDirectPrice(itemHrid, kind, mode) {
    let value = 0;
    if (mode === "conservative") {
      value = kind === "sell" ? runtime.api.getNetSellPrice?.(itemHrid, 0) : runtime.api.getAskPrice?.(itemHrid, 0);
    } else if (mode === "aggressive") {
      value = kind === "sell" ? runtime.api.getNetSellPriceAtAsk?.(itemHrid, 0) : runtime.api.getBidPrice?.(itemHrid, 0);
    } else {
      value = runtime.api.getFairValue?.(itemHrid, 0);
      if (kind === "sell" && Number(value) > 0) {
        value *= 1 - (Number(runtime.api.getMarketTaxRate?.(itemHrid)) || 0);
      }
    }
    return Number(value) > 0 ? Number(value) : null;
  }
  function getPriceInfo(itemHrid, kind, mode) {
    const direct = getDirectPrice(itemHrid, kind, mode);
    if (direct !== null) {
      return {
        value: direct,
        source: "market",
        complete: true,
        fallback: false,
        missingItemHrids: []
      };
    }
    if (kind === "sell") {
      const derived = runtime.api.getAssetLiquidationValue?.(itemHrid, 0, mode);
      if (Number(derived?.value) > 0) {
        return {
          value: Number(derived.value),
          source: derived.source === "market" ? "market" : "derived",
          complete: Boolean(derived.complete),
          fallback: false,
          missingItemHrids: derived.missingItemHrids ?? []
        };
      }
    }
    const marketValue = getDirectPrice(itemHrid, kind, "fair");
    if (marketValue !== null) {
      return {
        value: marketValue,
        source: "market-value-fallback",
        complete: true,
        fallback: true,
        missingItemHrids: []
      };
    }
    return {
      value: null,
      source: "missing",
      complete: false,
      fallback: false,
      missingItemHrids: [itemHrid]
    };
  }
  function getPrice(itemHrid, kind, mode) {
    return getPriceInfo(itemHrid, kind, mode).value;
  }
  function expectedDropCount(drop) {
    const minimum = Number(drop?.minCount ?? drop?.count ?? 0) || 0;
    const maximum = Number(drop?.maxCount ?? drop?.count ?? minimum) || 0;
    const rate = Number(drop?.dropRate ?? 1) || 0;
    return rate * ((minimum + maximum) / 2);
  }
  function getOptionalOutputs(actionHrid, detail) {
    const essenceFind = Math.max(
      0,
      Number(getActionBuffRatio(actionHrid, BUFF_TYPES.essenceFind)) || 0
    );
    const rareFind = Math.max(
      0,
      Number(getActionBuffRatio(actionHrid, BUFF_TYPES.rareFind)) || 0
    );
    return [
      ...asArray(detail?.essenceDropTable).map((drop) => ({
        itemHrid: drop.itemHrid,
        count: expectedDropCount(drop) * (1 + essenceFind),
        baseCount: expectedDropCount(drop),
        findBonus: essenceFind,
        kind: "essence"
      })),
      ...asArray(detail?.rareDropTable).map((drop) => ({
        itemHrid: drop.itemHrid,
        count: expectedDropCount(drop) * (1 + rareFind),
        baseCount: expectedDropCount(drop),
        findBonus: rareFind,
        kind: "rare"
      }))
    ];
  }
  function resolveProductionActionByItemHrid(itemHrid) {
    const target = String(itemHrid ?? "");
    if (!target) return null;
    return getProductionActionIndex().get(target) ?? null;
  }
  function projectAction(actionOrHrid, requestedCount, context = {}) {
    const action = typeof actionOrHrid === "string" ? { actionHrid: actionOrHrid } : actionOrHrid ?? {};
    const actionHrid = action.actionHrid ?? action.hrid;
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    if (!actionHrid || !detail) {
      return { status: "waiting", actionHrid, missing: ["actionData"] };
    }
    const count = requestedCount === void 0 ? getActionCount(action) : Number(requestedCount);
    const infinite = count === Infinity || !Number.isFinite(count);
    const normalizedCount = infinite ? Infinity : Math.max(0, count);
    const respectInventoryLimit = context.respectInventoryLimit ?? (requestedCount === void 0 && typeof actionOrHrid !== "string");
    if (!isPlayerDataReady()) {
      return {
        status: "waiting",
        actionHrid,
        detail,
        count: normalizedCount,
        infinite,
        effectiveCount: normalizedCount,
        effectivelyInfinite: infinite,
        materialLimited: false,
        missing: ["playerData"],
        missingPrices: [],
        netProfitPerAction: null,
        profitPerHour: null,
        totalProfit: null,
        totalSeconds: null
      };
    }
    const profile = getActionProductionProfile(action, context);
    if (profile.status !== "complete") {
      return {
        status: "waiting",
        actionHrid,
        detail,
        count: normalizedCount,
        infinite,
        effectiveCount: normalizedCount,
        effectivelyInfinite: infinite,
        materialLimited: false,
        missing: ["playerData"],
        missingPrices: [],
        netProfitPerAction: null,
        profitPerHour: null,
        totalProfit: null,
        totalSeconds: null
      };
    }
    const { timing, teaEffects, outputs } = profile;
    const secondsPerAction = profile.secondsPerAction;
    const inputs = profile.inputs;
    const lessResource = teaEffects.lessResource;
    const alchemyCapacity = getAlchemyCapacity(action, detail, lessResource);
    if (alchemyCapacity && !alchemyCapacity.known) {
      return {
        status: "waiting",
        actionHrid,
        detail,
        count: normalizedCount,
        infinite,
        effectiveCount: null,
        effectivelyInfinite: false,
        materialLimited: false,
        maxCraftable: null,
        missing: alchemyCapacity.missing,
        missingPrices: [],
        netProfitPerAction: null,
        profitPerHour: null,
        totalProfit: null,
        secondsPerAction,
        totalSeconds: null,
        finishAt: null
      };
    }
    let maxCraftable = alchemyCapacity?.maxCraftable ?? Infinity;
    if (!alchemyCapacity) {
      for (const input of inputs) {
        const effectiveCount = getEffectiveInputCount(input, lessResource);
        if (effectiveCount > 0) {
          maxCraftable = Math.min(
            maxCraftable,
            Math.floor(getInventoryCount(input.itemHrid) / effectiveCount)
          );
        }
      }
      if (!inputs.length) maxCraftable = Infinity;
    }
    const canApplyInventoryLimit = respectInventoryLimit && (Boolean(alchemyCapacity) || !(infinite && maxCraftable === 0 && context.respectInventoryLimit !== true));
    const executableCount = canApplyInventoryLimit ? Math.min(normalizedCount, maxCraftable) : normalizedCount;
    const effectivelyInfinite = !Number.isFinite(executableCount);
    const materialLimited = respectInventoryLimit && (inputs.length > 0 || Boolean(alchemyCapacity)) && Number.isFinite(maxCraftable) && maxCraftable > 0 && (infinite || maxCraftable < normalizedCount);
    const valuationMode = "fair";
    const optionalOutputs = getOptionalOutputs(actionHrid, detail);
    const actionsPerHour = secondsPerAction ? 3600 / secondsPerAction : null;
    function calculateValuation(mode) {
      const missingPrices2 = [];
      const costMissingPrices = [];
      const fallbackItemHrids = [];
      const unpricedByproducts2 = [];
      const derivedMissingPrices = [];
      const materialCostPerAction = inputs.reduce((total, input) => {
        const effectiveCount = getEffectiveInputCount(input, lessResource);
        const priceInfo = getPriceInfo(input.itemHrid, "buy", mode);
        if (priceInfo.value === null) {
          missingPrices2.push(input.itemHrid);
          costMissingPrices.push(input.itemHrid);
        }
        if (priceInfo.fallback) fallbackItemHrids.push(input.itemHrid);
        return total + (priceInfo.value === null ? 0 : effectiveCount * priceInfo.value);
      }, 0);
      const primaryRevenuePerAction = outputs.reduce((total, output) => {
        const effectiveCount = output.count;
        const priceInfo = getPriceInfo(output.itemHrid, "sell", mode);
        if (priceInfo.value === null) missingPrices2.push(output.itemHrid);
        if (priceInfo.fallback) fallbackItemHrids.push(output.itemHrid);
        if (!priceInfo.complete && priceInfo.value !== null) {
          derivedMissingPrices.push(...priceInfo.missingItemHrids);
        }
        return total + (priceInfo.value === null ? 0 : effectiveCount * priceInfo.value);
      }, 0);
      const byproductRevenuePerAction = optionalOutputs.reduce(
        (total, output) => {
          const priceInfo = getPriceInfo(output.itemHrid, "sell", mode);
          if (priceInfo.value === null) unpricedByproducts2.push(output.itemHrid);
          if (priceInfo.fallback) fallbackItemHrids.push(output.itemHrid);
          if (!priceInfo.complete && priceInfo.value !== null) {
            derivedMissingPrices.push(...priceInfo.missingItemHrids);
          }
          return total + (priceInfo.value === null ? 0 : output.count * priceInfo.value);
        },
        0
      );
      let teaCostPerHour = 0;
      for (const drink of teaEffects.drinks) {
        const priceInfo = getPriceInfo(drink.itemHrid, "buy", mode);
        const price = priceInfo.value;
        if (price === null) {
          missingPrices2.push(drink.itemHrid);
          costMissingPrices.push(drink.itemHrid);
        }
        if (priceInfo.fallback) fallbackItemHrids.push(drink.itemHrid);
        const countPerHour = DRINKS_PER_HOUR * teaEffects.concentrationMultiplier;
        teaCostPerHour += price === null ? 0 : price * countPerHour;
      }
      const teaCostPerAction = actionsPerHour ? teaCostPerHour / actionsPerHour : 0;
      const revenuePerAction = primaryRevenuePerAction + byproductRevenuePerAction;
      const complete2 = missingPrices2.length === 0 && secondsPerAction !== null;
      const netProfitPerAction = complete2 ? revenuePerAction - materialCostPerAction - teaCostPerAction : null;
      const profitPerHour = netProfitPerAction === null || !actionsPerHour ? null : netProfitPerAction * actionsPerHour;
      const totalProfit = netProfitPerAction === null || effectivelyInfinite ? null : netProfitPerAction * executableCount;
      return {
        mode,
        complete: complete2,
        materialCostPerAction,
        teaCostPerHour,
        teaCostPerAction,
        primaryRevenuePerAction,
        byproductRevenuePerAction,
        revenuePerAction,
        netProfitPerAction,
        profitPerHour,
        totalProfit,
        costComplete: costMissingPrices.length === 0,
        costMissingPrices: [...new Set(costMissingPrices)],
        missingPrices: [...new Set(missingPrices2)],
        fallbackItemHrids: [...new Set(fallbackItemHrids)],
        unpricedByproducts: [...new Set(unpricedByproducts2)],
        derivedMissingPrices: [...new Set(derivedMissingPrices)]
      };
    }
    const valuations = Object.fromEntries(
      [...PROFIT_VALUATION_MODES].map((mode) => [mode, calculateValuation(mode)])
    );
    const selectedValuation = valuations[valuationMode];
    const missingPrices = selectedValuation.missingPrices;
    const inputDetails = inputs.map((input) => {
      const effectiveCount = getEffectiveInputCount(input, lessResource);
      const unitPrice = getPrice(input.itemHrid, "buy", valuationMode);
      return {
        ...input,
        effectiveCount,
        owned: getInventoryCount(input.itemHrid),
        unitPrice,
        valuePerAction: unitPrice === null ? null : effectiveCount * unitPrice
      };
    });
    const outputDetails = outputs.map((output) => {
      const effectiveCount = output.count;
      const priceInfo = getPriceInfo(output.itemHrid, "sell", valuationMode);
      const unitPrice = priceInfo.value;
      return {
        ...output,
        baseCount: output.baseCount ?? output.count,
        effectiveCount,
        expectedCount: effectiveCount * (effectivelyInfinite ? 1 : executableCount),
        kind: "primary",
        owned: getInventoryCount(output.itemHrid),
        unitPrice,
        valueSource: priceInfo.source,
        derivedMissingPrices: priceInfo.missingItemHrids,
        valuePerAction: unitPrice === null ? null : effectiveCount * unitPrice
      };
    });
    const unpricedByproducts = selectedValuation.unpricedByproducts;
    const byproductOutputs = optionalOutputs.map((output) => {
      const priceInfo = getPriceInfo(output.itemHrid, "sell", valuationMode);
      const unitPrice = priceInfo.value;
      return {
        ...output,
        effectiveCount: output.count,
        expectedCount: output.count * (effectivelyInfinite ? 1 : executableCount),
        owned: getInventoryCount(output.itemHrid),
        unitPrice,
        valueSource: priceInfo.source,
        derivedMissingPrices: priceInfo.missingItemHrids,
        valuePerAction: unitPrice === null ? null : output.count * unitPrice
      };
    });
    const drinks = teaEffects.drinks.map((drink) => {
      const price = getPrice(drink.itemHrid, "buy", valuationMode);
      const countPerHour = DRINKS_PER_HOUR * teaEffects.concentrationMultiplier;
      const costPerHour = price === null ? null : price * countPerHour;
      return {
        ...drink,
        countPerHour,
        costPerHour,
        unitPrice: price
      };
    });
    const complete = selectedValuation.complete;
    const totalSeconds = getProjectedTotalSeconds(
      actionHrid,
      executableCount,
      timing,
      context
    );
    const now = Number(context.now ?? Date.now());
    return {
      status: complete ? "complete" : "incomplete",
      isPartial: complete && (unpricedByproducts.length > 0 || selectedValuation.derivedMissingPrices.length > 0),
      actionHrid,
      detail,
      count: normalizedCount,
      infinite,
      effectiveCount: executableCount,
      effectivelyInfinite,
      materialLimited,
      respectsInventoryLimit: Boolean(respectInventoryLimit),
      secondsPerAction,
      totalSeconds,
      finishAt: Number.isFinite(totalSeconds) && totalSeconds !== null ? now + totalSeconds * 1e3 : null,
      inputs: inputDetails,
      outputs: outputDetails,
      byproductOutputs,
      actionsPerHour,
      baseSeconds: timing?.baseSeconds ?? null,
      cycleSeconds: timing?.cycleSeconds ?? null,
      efficiencyPercent: timing?.efficiencyPercent ?? 0,
      speedPercent: timing?.speedPercent ?? 0,
      timingSource: timing?.timingSource ?? null,
      teaEffects: { ...teaEffects, drinks },
      maxCraftable,
      valuationMode,
      valuations,
      materialCostPerAction: selectedValuation.materialCostPerAction,
      teaCostPerHour: selectedValuation.teaCostPerHour,
      teaCostPerAction: selectedValuation.teaCostPerAction,
      primaryRevenuePerAction: selectedValuation.primaryRevenuePerAction,
      byproductRevenuePerAction: selectedValuation.byproductRevenuePerAction,
      revenuePerAction: selectedValuation.revenuePerAction,
      netProfitPerAction: selectedValuation.netProfitPerAction,
      profitPerHour: selectedValuation.profitPerHour,
      totalProfit: selectedValuation.totalProfit,
      missingPrices,
      fallbackItemHrids: selectedValuation.fallbackItemHrids,
      unpricedByproducts,
      derivedMissingPrices: selectedValuation.derivedMissingPrices
    };
  }
  function projectQueue(actions = runtime.state.currentActionsHridList, context = {}) {
    const now = Number(context.now ?? Date.now());
    let elapsed = 0;
    let hasInfinite = false;
    const items = [];
    for (const action of actions ?? []) {
      const projection = projectAction(action, void 0, { ...context, now });
      const startsAt = hasInfinite ? null : now + elapsed * 1e3;
      if (Number.isFinite(projection.totalSeconds) && !hasInfinite) {
        elapsed += projection.totalSeconds;
      } else {
        hasInfinite = true;
      }
      items.push({
        ...projection,
        action,
        startsAt,
        cumulativeFinishAt: hasInfinite ? null : now + elapsed * 1e3
      });
    }
    return {
      items,
      totalSeconds: hasInfinite ? Infinity : elapsed,
      finishAt: hasInfinite ? null : now + elapsed * 1e3,
      hasInfinite
    };
  }
  Object.assign(runtime.api, {
    getActionBuffRatio,
    getActionSpeedPercent,
    getDrinkConcentrationMultiplier,
    getEffectiveTeaEffects,
    getActionProductionProfile,
    getInventoryCount,
    getExpectedOutputs,
    getDirectInputs,
    getActionRemainingCount: getActionCount,
    isPlayerProjectionDataReady: isPlayerDataReady,
    projectActionCraftingCost,
    projectActionTiming,
    projectAction,
    projectQueue,
    resolveProductionActionByItemHrid
  });

  // src/core/procurement.js
  var DATA_VERSION = 3;
  var SETTINGS_KEY = "MWITools_procurement_settings_v1";
  var DATA_PREFIX = "MWITools_procurement_v1";
  var MANUAL_OVERRIDE_MS = 5 * 60 * 1e3;
  var PURCHASE_SUPPRESSION_MS = 30 * 1e3;
  var MAX_CHAIN_DEPTH = 25;
  var DEFAULT_SETTINGS = Object.freeze({
    badgesEnabled: true,
    upgradeChainEnabled: true,
    createPlansByDefault: true,
    inventorySyncEnabled: true,
    autoCollapseEnabled: true,
    autoExpandOnAddEnabled: false,
    locateEnabled: true,
    autoPrefillEnabled: true,
    purchaseNavEnabled: true,
    pricesEnabled: true,
    cartTotalEnabled: true,
    autoRestockEnabled: true,
    safetyLevel: "95",
    safetyThreshold: 10,
    guzzlingPouchLevel: -1,
    nextItemShortcut: null,
    edgeZoneWidth: 10,
    handleY: 180,
    drawerWidth: 360
  });
  var Z_SCORES = Object.freeze({
    off: 0,
    95: 1.645,
    99: 2.326,
    99.9: 3.09
  });
  var listeners = /* @__PURE__ */ new Map();
  var cart = /* @__PURE__ */ new Map();
  var plans = /* @__PURE__ */ new Map();
  var inventoryEntries = /* @__PURE__ */ new Map();
  var purchaseSuppressions = /* @__PURE__ */ new Map();
  var planningData = {
    goals: [],
    overrides: {},
    defaults: { item: "chain", house: "chain" }
  };
  var activeCharacterId2 = "";
  var activeStorageKey = "";
  var ready = false;
  var settings = loadSettings();
  var producerActionSource = null;
  var producerActionIndex = null;
  function clone(value) {
    return value == null ? value : globalThis.structuredClone(value);
  }
  function emit(type, detail = {}) {
    for (const listener of listeners.get(type) ?? []) {
      try {
        listener(clone(detail));
      } catch (error) {
        console.error(
          runtime.config.isZH ? `[MWITools] 采购事件 ${type} 的监听器执行失败` : `[MWITools] Procurement ${type} listener failed`,
          error
        );
      }
    }
  }
  function on(type, listener) {
    const registered = listeners.get(type) ?? /* @__PURE__ */ new Set();
    registered.add(listener);
    listeners.set(type, registered);
    if (type === "ready" && ready) {
      globalThis.queueMicrotask(
        () => listener({ characterId: activeCharacterId2 })
      );
    }
    return () => registered.delete(listener);
  }
  function off(type, listener) {
    listeners.get(type)?.delete(listener);
  }
  function normalizeItemHrid(value) {
    const raw = String(value ?? "").trim();
    if (!raw) return "";
    const bare = raw.replace(/^#/, "").replace(/^\/items\//, "");
    return bare ? `/items/${bare}` : "";
  }
  function normalizeEnhancementLevel(value) {
    const numeric = Math.floor(Number(value) || 0);
    return Math.max(0, numeric);
  }
  function itemKey(itemHrid, enhancementLevel = 0) {
    const normalized = normalizeItemHrid(itemHrid);
    return normalized ? `${normalized}#${normalizeEnhancementLevel(enhancementLevel)}` : "";
  }
  function parseItemKey(key) {
    const index = String(key).lastIndexOf("#");
    return {
      itemHrid: index >= 0 ? key.slice(0, index) : normalizeItemHrid(key),
      enhancementLevel: index >= 0 ? normalizeEnhancementLevel(key.slice(index + 1)) : 0
    };
  }
  function emptyAllocations() {
    return { manual: 0, planning: 0, projects: {} };
  }
  function normalizeAllocations(value, fallbackQuantity = 0) {
    const projects = Object.fromEntries(
      Object.entries(value?.projects ?? {}).map(([id, quantity]) => [
        id,
        Math.max(0, Math.ceil(Number(quantity) || 0))
      ]).filter(([, quantity]) => quantity > 0)
    );
    const normalized = {
      manual: Math.max(0, Math.ceil(Number(value?.manual) || 0)),
      planning: Math.max(0, Math.ceil(Number(value?.planning) || 0)),
      projects
    };
    const allocated = allocationTotal(normalized);
    const fallback = Math.max(0, Math.ceil(Number(fallbackQuantity) || 0));
    if (allocated < fallback) normalized.manual += fallback - allocated;
    return normalized;
  }
  function allocationTotal(allocations) {
    return Math.max(0, Number(allocations?.manual) || 0) + Math.max(0, Number(allocations?.planning) || 0) + Object.values(allocations?.projects ?? {}).reduce(
      (sum, quantity) => sum + Math.max(0, Number(quantity) || 0),
      0
    );
  }
  function allocationOwner(value = {}) {
    if (value?.allocation?.kind === "planning" || value?.source === "planning") {
      return { kind: "planning", id: "planning" };
    }
    const projectId = value?.allocation?.projectId ?? value?.projectId;
    if (value?.allocation?.kind === "project" && projectId) {
      return { kind: "project", id: String(projectId) };
    }
    return { kind: "manual", id: "manual" };
  }
  function getAllocationQuantity(row, owner) {
    const allocations = row?.allocations ?? emptyAllocations();
    if (owner?.kind === "planning") return allocations.planning ?? 0;
    if (owner?.kind === "project") {
      return allocations.projects?.[owner.id] ?? 0;
    }
    return allocations.manual ?? 0;
  }
  function changeAllocation(row, owner, delta) {
    row.allocations = normalizeAllocations(row.allocations);
    if (owner.kind === "planning") {
      row.allocations.planning = Math.max(
        0,
        (row.allocations.planning ?? 0) + delta
      );
    } else if (owner.kind === "project") {
      const next = Math.max(
        0,
        (row.allocations.projects?.[owner.id] ?? 0) + delta
      );
      if (next) row.allocations.projects[owner.id] = next;
      else delete row.allocations.projects[owner.id];
    } else {
      row.allocations.manual = Math.max(0, (row.allocations.manual ?? 0) + delta);
    }
    row.quantity = allocationTotal(row.allocations);
  }
  function getEnvironment() {
    return runtime.api.getMarketEnvironment?.() ?? "production";
  }
  function getCharacterStorageKey(characterId = activeCharacterId2) {
    return `${DATA_PREFIX}:${getEnvironment()}:${characterId}`;
  }
  function loadSettings() {
    try {
      const stored = JSON.parse(localStorage.getItem(SETTINGS_KEY) || "null");
      return { ...DEFAULT_SETTINGS, ...stored?.values ?? {} };
    } catch {
      return { ...DEFAULT_SETTINGS };
    }
  }
  function saveSettings() {
    localStorage.setItem(
      SETTINGS_KEY,
      JSON.stringify({ version: DATA_VERSION, values: settings })
    );
  }
  function normalizeSettings(next) {
    const normalized = { ...DEFAULT_SETTINGS, ...next };
    normalized.safetyLevel = Object.hasOwn(Z_SCORES, normalized.safetyLevel) ? normalized.safetyLevel : "95";
    normalized.safetyThreshold = Math.max(
      0,
      Math.floor(Number(normalized.safetyThreshold) || 0)
    );
    normalized.guzzlingPouchLevel = Math.min(
      20,
      Math.max(-1, Math.floor(Number(normalized.guzzlingPouchLevel) || 0))
    );
    normalized.edgeZoneWidth = Math.min(
      32,
      Math.max(0, Math.floor(Number(normalized.edgeZoneWidth) || 0))
    );
    normalized.handleY = Math.max(8, Number(normalized.handleY) || 180);
    normalized.drawerWidth = Math.min(
      560,
      Math.max(300, Number(normalized.drawerWidth) || 360)
    );
    return normalized;
  }
  function getSettings() {
    return clone(settings);
  }
  function setSetting2(id, value) {
    if (!Object.hasOwn(DEFAULT_SETTINGS, id)) return false;
    const previous = settings[id];
    settings = normalizeSettings({ ...settings, [id]: value });
    saveSettings();
    emit("settings:change", { id, value: settings[id], previous });
    return true;
  }
  function serializeData() {
    return {
      version: DATA_VERSION,
      cart: [...cart.values()],
      plans: [...plans.values()],
      planning: planningData
    };
  }
  function persistData() {
    if (!activeStorageKey) return;
    localStorage.setItem(activeStorageKey, JSON.stringify(serializeData()));
  }
  function normalizePlanningData(next) {
    return {
      goals: Array.isArray(next?.goals) ? clone(next.goals) : [],
      overrides: next?.overrides && typeof next.overrides === "object" ? clone(next.overrides) : {},
      defaults: {
        item: String(next?.defaults?.item ?? "chain"),
        house: String(next?.defaults?.house ?? "chain")
      }
    };
  }
  function migratePlanningData(value) {
    const migrated = normalizePlanningData(value);
    const legacyPolicies = value?.policies && typeof value.policies === "object" ? value.policies : {};
    migrated.goals = migrated.goals.map((goal) => ({
      ...goal,
      policy: String(goal?.policy ?? "chain")
    }));
    for (const goal of migrated.goals) {
      const goalId = String(
        goal?.id ?? `${goal?.kind === "house" ? "house" : "item"}:${goal?.targetHrid ?? goal?.hrid ?? ""}`
      );
      if (!goalId) continue;
      const overrides = { ...migrated.overrides[goalId] ?? {} };
      for (const [itemHrid, policy] of Object.entries(legacyPolicies)) {
        if (policy === "acquire") overrides[itemHrid] = "buy";
      }
      if (Object.keys(overrides).length) migrated.overrides[goalId] = overrides;
    }
    return migrated;
  }
  function loadCharacterData(characterId, characterItems = runtime.state.initData_characterItems ?? []) {
    activeCharacterId2 = String(characterId ?? "");
    activeStorageKey = activeCharacterId2 ? getCharacterStorageKey(activeCharacterId2) : "";
    cart.clear();
    plans.clear();
    planningData = normalizePlanningData(null);
    let storedVersion = 0;
    if (activeStorageKey) {
      try {
        const stored = JSON.parse(
          localStorage.getItem(activeStorageKey) || "null"
        );
        storedVersion = Math.max(0, Number(stored?.version) || 0);
        for (const row of stored?.cart ?? []) {
          const key = itemKey(row.itemHrid ?? row.itemId, row.enhancementLevel);
          if (!key) continue;
          cart.set(key, {
            ...row,
            itemHrid: parseItemKey(key).itemHrid,
            enhancementLevel: parseItemKey(key).enhancementLevel,
            quantity: Math.max(0, Math.ceil(Number(row.quantity) || 0)),
            allocations: normalizeAllocations(row.allocations, row.quantity)
          });
        }
        for (const plan of stored?.plans ?? []) {
          if (plan?.id) plans.set(plan.id, plan);
        }
        planningData = storedVersion < 3 ? migratePlanningData(stored?.planning) : normalizePlanningData(stored?.planning);
      } catch (error) {
        console.warn(
          runtime.config.isZH ? "[MWITools] 无法加载采购数据" : "[MWITools] Could not load procurement data",
          error
        );
      }
    }
    rebuildInventorySnapshot(characterItems);
    if (storedVersion < 3) {
      if (storedVersion < 2) migrateLegacyCartAllocations();
      persistData();
    }
    refreshPlanProgress();
    ready = Boolean(activeCharacterId2);
    emit("character:change", { characterId: activeCharacterId2 });
    emit("cart:change", { reason: "load", added: 0, items: getCartItems() });
    emit("plan:change", { plans: getPlans() });
    emit("planning:change", { planning: getPlanningData() });
    if (ready) emit("ready", { characterId: activeCharacterId2 });
  }
  function loadCharacterDataFromMessage(payload) {
    const characterId = payload?.character?.id ?? payload?.character?.characterID ?? payload?.characterID ?? payload?.characterSkills?.[0]?.characterID ?? runtime.state.currentCharacterId;
    loadCharacterData(
      characterId,
      Array.isArray(payload?.characterItems) ? payload.characterItems : runtime.state.initData_characterItems
    );
  }
  function migrateLegacyCartAllocations() {
    const availableInventory = inventoryCounts();
    const orderedPlans = [...plans.values()].filter((plan) => plan.status !== "completed").sort(
      (left, right) => String(left.createdAt ?? "").localeCompare(
        String(right.createdAt ?? "")
      ) || String(left.id).localeCompare(String(right.id))
    );
    for (const plan of orderedPlans) {
      for (const [key, rawQuantity] of Object.entries(plan.materials ?? {})) {
        const demand = Math.max(0, Math.ceil(Number(rawQuantity) || 0));
        const inventory = Math.min(
          demand,
          Math.max(0, availableInventory.get(key) ?? 0)
        );
        availableInventory.set(
          key,
          Math.max(0, (availableInventory.get(key) ?? 0) - inventory)
        );
        const row = cart.get(key);
        if (!row) continue;
        const claim = Math.min(
          Math.max(0, demand - inventory),
          Math.max(0, row.allocations?.manual ?? 0)
        );
        if (!claim) continue;
        changeAllocation(row, { kind: "manual" }, -claim);
        changeAllocation(row, { kind: "project", id: String(plan.id) }, claim);
      }
    }
  }
  function getPlanningData() {
    return clone(planningData);
  }
  function setPlanningData(next, reason = "update") {
    planningData = normalizePlanningData(next);
    persistData();
    emit("planning:change", { reason, planning: getPlanningData() });
    return getPlanningData();
  }
  function resolveItemName(rawItemHrid) {
    const itemHrid = normalizeItemHrid(rawItemHrid);
    return itemName(itemHrid, { fallback: itemHrid });
  }
  function resolveActionName(actionHrid) {
    const normalized = String(actionHrid ?? "").trim();
    if (!normalized) return "";
    return actionName(normalized, { fallback: normalized });
  }
  function inventoryEntryKey(item) {
    return String(
      item?.hash ?? item?.id ?? `${item?.itemLocationHrid ?? ""}:${itemKey(item?.itemHrid, item?.enhancementLevel)}`
    );
  }
  function sameInventorySlot(left, right) {
    return normalizeItemHrid(left?.itemHrid) === normalizeItemHrid(right?.itemHrid) && String(left?.itemLocationHrid ?? "") === String(right?.itemLocationHrid ?? "") && normalizeEnhancementLevel(left?.enhancementLevel) === normalizeEnhancementLevel(right?.enhancementLevel);
  }
  function rebuildInventorySnapshot(items) {
    inventoryEntries.clear();
    for (const item of items ?? []) {
      if (!item?.itemHrid) continue;
      inventoryEntries.set(inventoryEntryKey(item), { ...item });
    }
  }
  function inventoryCounts() {
    const counts = /* @__PURE__ */ new Map();
    for (const item of inventoryEntries.values()) {
      if (item.itemLocationHrid && item.itemLocationHrid !== "/item_locations/inventory") {
        continue;
      }
      const key = itemKey(item.itemHrid, item.enhancementLevel);
      counts.set(
        key,
        (counts.get(key) ?? 0) + Math.max(0, Number(item.count) || 0)
      );
    }
    return counts;
  }
  function getInventoryCount2(itemHrid, enhancementLevel = 0) {
    return inventoryCounts().get(itemKey(itemHrid, enhancementLevel)) ?? 0;
  }
  function getLockedDetails(itemHrid, enhancementLevel = 0, excludePlanId = null, excludeActionHrids = null) {
    const key = itemKey(itemHrid, enhancementLevel);
    const byPlan = [];
    let total = 0;
    for (const plan of plans.values()) {
      if (plan.id === excludePlanId || plan.status === "completed" || excludeActionHrids?.has?.(plan.actionHrid)) {
        continue;
      }
      const quantity = Math.max(0, Number(plan.materials?.[key]) || 0);
      if (!quantity) continue;
      total += quantity;
      byPlan.push({
        id: plan.id,
        name: resolveActionName(plan.actionHrid) || plan.name,
        quantity
      });
    }
    return { total, byPlan };
  }
  function getEffectiveInventory(itemHrid, enhancementLevel = 0, excludePlanId = null) {
    const owned = getInventoryCount2(itemHrid, enhancementLevel);
    return Math.max(
      0,
      owned - getLockedDetails(itemHrid, enhancementLevel, excludePlanId).total
    );
  }
  function getProjectReservedInventory(itemHrid, enhancementLevel = 0) {
    const owned = getInventoryCount2(itemHrid, enhancementLevel);
    return Math.min(owned, getLockedDetails(itemHrid, enhancementLevel).total);
  }
  function getInventoryAllocationSnapshot() {
    const snapshot = /* @__PURE__ */ new Map();
    for (const [key, owned] of inventoryCounts()) {
      const { itemHrid, enhancementLevel } = parseItemKey(key);
      snapshot.set(key, {
        owned,
        projectInventory: Math.min(
          owned,
          getLockedDetails(itemHrid, enhancementLevel).total
        )
      });
    }
    return snapshot;
  }
  function isCoin(itemHrid) {
    return normalizeItemHrid(itemHrid) === "/items/coin";
  }
  function getSafetyScore() {
    return Z_SCORES[settings.safetyLevel] ?? 0;
  }
  function suggestedMaterialCount(baseCount, actionCount, savingsProbability, options = {}) {
    const base = Math.max(0, Number(baseCount) || 0);
    const actions = Math.max(0, Math.ceil(Number(actionCount) || 0));
    const raw = base * actions;
    const probability = Math.min(1, Math.max(0, Number(savingsProbability) || 0));
    const adjustedPerAction = base * (1 - probability);
    const expected = adjustedPerAction * actions;
    const threshold = options.threshold ?? settings.safetyThreshold;
    const score = actions > threshold && options.bufferable !== false ? getSafetyScore() : 0;
    const fractional = adjustedPerAction - Math.floor(adjustedPerAction);
    const deviation = Math.sqrt(actions * fractional * (1 - fractional));
    const cap = actions * Math.ceil(adjustedPerAction);
    const suggested = Math.min(
      raw,
      cap,
      Math.ceil(expected + score * deviation - 1e-9)
    );
    return {
      raw,
      expected,
      buffer: score * deviation,
      suggested
    };
  }
  function getGuzzlingPouchLevel() {
    if (settings.guzzlingPouchLevel >= 0) return settings.guzzlingPouchLevel;
    let level = -1;
    for (const item of inventoryEntries.values()) {
      if (normalizeItemHrid(item.itemHrid) !== "/items/guzzling_pouch") continue;
      if (item.itemLocationHrid === "/item_locations/inventory") continue;
      level = Math.max(level, normalizeEnhancementLevel(item.enhancementLevel));
    }
    return level;
  }
  function getProductionProfile(actionHrid) {
    const level = getGuzzlingPouchLevel();
    return runtime.api.getActionProductionProfile?.(
      actionHrid,
      level >= 0 ? { guzzlingPouchLevel: level } : {}
    );
  }
  function getTeaSavings(actionHrid) {
    const profile = getProductionProfile(actionHrid);
    if (profile?.status === "complete") {
      return Math.min(
        1,
        Math.max(0, Number(profile.teaEffects?.lessResource) || 0)
      );
    }
    const buffs = runtime.api.getTeaBuffsByActionHrid?.(actionHrid) ?? {};
    const level = getGuzzlingPouchLevel();
    const concentration = runtime.api.getDrinkConcentrationMultiplier?.(
      level >= 0 ? { guzzlingPouchLevel: level } : {}
    ) ?? 1;
    return Math.min(
      1,
      Math.max(0, Number(buffs.lessResource) || 0) / 100 * concentration
    );
  }
  function isBackEquipmentForProcurement(itemHrid) {
    const detail = runtime.state.initData_itemDetailMap?.[normalizeItemHrid(itemHrid)];
    const equipment = detail?.equipmentDetail;
    return [
      detail?.itemLocationHrid,
      detail?.equipmentSlotHrid,
      detail?.slotHrid,
      equipment?.itemLocationHrid,
      equipment?.equipmentSlotHrid,
      equipment?.slotHrid,
      equipment?.equipmentTypeHrid,
      equipment?.typeHrid,
      equipment?.type
    ].some((value) => /(?:^|[/_])back(?:$|[/_])/.test(String(value ?? "")));
  }
  function isRefinedBackUpgrade(detail) {
    if (!normalizeItemHrid(detail?.upgradeItemHrid)) return false;
    return (runtime.api.getExpectedOutputs?.(detail) ?? []).some((output) => {
      const outputHrid = normalizeItemHrid(output?.itemHrid);
      return outputHrid.endsWith("_refined") && isBackEquipmentForProcurement(outputHrid);
    });
  }
  function materialRequirement(input, actionHrid, actionCount, options = {}) {
    const itemHrid = normalizeItemHrid(input.itemHrid);
    const enhancementLevel = normalizeEnhancementLevel(input.enhancementLevel);
    const baseCount = Math.max(0, Number(input.count) || 0);
    const protectedCount = options.isUpgradeItem ? Math.min(baseCount, Math.max(0, Number(input.upgradeItemCount) || 1)) : 0;
    const reducibleCount = Math.max(0, baseCount - protectedCount);
    const bufferable = !isCoin(itemHrid) && reducibleCount > 0 && options.bufferable !== false;
    const protectedCalculation = suggestedMaterialCount(
      protectedCount,
      actionCount,
      0,
      { bufferable: false }
    );
    const reducibleCalculation = suggestedMaterialCount(
      reducibleCount,
      actionCount,
      bufferable ? getTeaSavings(actionHrid) : 0,
      { bufferable }
    );
    const calculated = Object.fromEntries(
      ["raw", "expected", "buffer", "suggested"].map((key) => [
        key,
        protectedCalculation[key] + reducibleCalculation[key]
      ])
    );
    const owned = getInventoryCount2(itemHrid, enhancementLevel);
    const locked = getLockedDetails(
      itemHrid,
      enhancementLevel,
      options.excludePlanId,
      options.excludeActionHrids
    );
    const effectiveOwned = Math.max(0, owned - locked.total);
    const cartRow = cart.get(itemKey(itemHrid, enhancementLevel));
    const cartQuantity = getAllocationQuantity(
      cartRow,
      options.cartOwner ?? { kind: "manual" }
    );
    return {
      itemHrid,
      enhancementLevel,
      name: resolveItemName(itemHrid),
      ...calculated,
      owned,
      locked: locked.total,
      lockedByPlans: locked.byPlan,
      effectiveOwned,
      cartQuantity,
      shortage: Math.max(0, calculated.suggested - effectiveOwned),
      addableShortage: Math.max(
        0,
        calculated.suggested - effectiveOwned - cartQuantity
      ),
      purchasable: options.purchasable !== false && !isCoin(itemHrid)
    };
  }
  function calculateRequirements(actionHrid, count, options = {}) {
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    const actionCount = Math.max(0, Math.ceil(Number(count) || 0));
    if (!detail || !actionCount) {
      return { status: "waiting", actionHrid, count: actionCount, materials: [] };
    }
    const inputs = runtime.api.getDirectInputs?.(detail) ?? [];
    const calculationOptions = {
      ...options
    };
    const excludedUpgradeHrid = isRefinedBackUpgrade(detail) ? normalizeItemHrid(detail.upgradeItemHrid) : "";
    const materials = inputs.map(
      (input) => materialRequirement(input, actionHrid, actionCount, {
        ...calculationOptions,
        isUpgradeItem: normalizeItemHrid(input.itemHrid) === normalizeItemHrid(detail.upgradeItemHrid),
        purchasable: normalizeItemHrid(input.itemHrid) !== excludedUpgradeHrid
      })
    );
    return {
      status: "complete",
      actionHrid,
      count: actionCount,
      detail,
      materials,
      missingTypes: materials.filter(
        (material) => material.purchasable && material.shortage > 0
      ).length,
      missingQuantity: materials.reduce(
        (sum, material) => sum + (material.purchasable ? material.shortage : 0),
        0
      )
    };
  }
  function getProducerAction(itemHrid) {
    const target = normalizeItemHrid(itemHrid);
    const actionDetails = runtime.state.initData_actionDetailMap ?? {};
    if (producerActionSource !== actionDetails || !producerActionIndex) {
      producerActionSource = actionDetails;
      producerActionIndex = /* @__PURE__ */ new Map();
      for (const [actionHrid, detail] of Object.entries(actionDetails)) {
        for (const output of runtime.api.getExpectedOutputs?.(detail) ?? []) {
          const outputHrid = normalizeItemHrid(output.itemHrid);
          if (!outputHrid || producerActionIndex.has(outputHrid)) continue;
          producerActionIndex.set(outputHrid, {
            actionHrid,
            detail,
            outputCount: Number(output.count) || 1
          });
        }
      }
    }
    return producerActionIndex.get(target) ?? null;
  }
  function getEffectiveProducerOutput(producer, itemHrid) {
    const profile = getProductionProfile(producer?.actionHrid);
    if (profile?.status !== "complete") {
      return {
        known: false,
        count: Math.max(0, Number(producer?.outputCount) || 0)
      };
    }
    const target = normalizeItemHrid(itemHrid);
    return {
      known: true,
      count: (profile.outputs ?? []).reduce(
        (total, output) => normalizeItemHrid(output?.itemHrid) === target ? total + Math.max(0, Number(output?.count) || 0) : total,
        0
      )
    };
  }
  function mergeMaterial(target, material) {
    const key = itemKey(material.itemHrid, material.enhancementLevel);
    const existing = target.get(key);
    if (!existing) {
      target.set(key, { ...material });
      return;
    }
    const suggested = existing.suggested + material.suggested;
    const owned = existing.owned;
    const locked = existing.locked;
    const effectiveOwned = Math.max(0, owned - locked);
    target.set(key, {
      ...existing,
      raw: existing.raw + material.raw,
      expected: existing.expected + material.expected,
      buffer: existing.buffer + material.buffer,
      suggested,
      shortage: Math.max(0, suggested - effectiveOwned),
      addableShortage: Math.max(
        0,
        suggested - effectiveOwned - existing.cartQuantity
      )
    });
  }
  function calculateUpgradeChain(actionHrid, count, options = {}) {
    const stages = [];
    const leaves = /* @__PURE__ */ new Map();
    const visited = /* @__PURE__ */ new Set();
    let cycle = false;
    let truncated = false;
    const unavailableOutputs = /* @__PURE__ */ new Set();
    const calculationOptions = {
      ...options
    };
    const visit = (currentHrid, currentCount, depth) => {
      if (depth >= MAX_CHAIN_DEPTH) {
        truncated = true;
        return;
      }
      if (visited.has(currentHrid)) {
        cycle = true;
        return;
      }
      const detail = runtime.state.initData_actionDetailMap?.[currentHrid];
      if (!detail) return;
      visited.add(currentHrid);
      const projection = calculateRequirements(
        currentHrid,
        currentCount,
        calculationOptions
      );
      const stage = {
        actionHrid: currentHrid,
        name: resolveActionName(currentHrid),
        count: currentCount,
        depth,
        materials: projection.materials
      };
      stages.push(stage);
      const upgradeHrid = normalizeItemHrid(detail.upgradeItemHrid);
      for (const material of projection.materials) {
        if (upgradeHrid && material.itemHrid === upgradeHrid) continue;
        mergeMaterial(leaves, material);
      }
      if (upgradeHrid) {
        const producer = getProducerAction(upgradeHrid);
        const upgradeMaterial = projection.materials.find(
          (material) => material.itemHrid === upgradeHrid
        );
        if (upgradeMaterial?.purchasable !== false) {
          if (producer) {
            const effectiveOutput = getEffectiveProducerOutput(
              producer,
              upgradeHrid
            );
            if (!effectiveOutput.known || effectiveOutput.count > 0) {
              visit(
                producer.actionHrid,
                Math.ceil(
                  Math.max(0, Number(upgradeMaterial?.suggested) || 0) / effectiveOutput.count
                ),
                depth + 1
              );
            } else {
              unavailableOutputs.add(upgradeHrid);
              mergeMaterial(leaves, upgradeMaterial);
            }
          } else if (upgradeMaterial) {
            mergeMaterial(leaves, upgradeMaterial);
          }
        }
      }
      visited.delete(currentHrid);
    };
    visit(actionHrid, Math.max(0, Math.ceil(Number(count) || 0)), 0);
    return {
      status: stages.length ? "complete" : "waiting",
      stages,
      leaves: [...leaves.values()],
      cycle,
      truncated,
      unavailableOutputs: [...unavailableOutputs]
    };
  }
  function selectUpgradeChainMaterials(chain, selectedActionHrids) {
    const selected = new Set(selectedActionHrids ?? []);
    const materials = /* @__PURE__ */ new Map();
    for (const stage of chain?.stages ?? []) {
      if (!selected.has(stage.actionHrid)) continue;
      const detail = runtime.state.initData_actionDetailMap?.[stage.actionHrid];
      const upgradeHrid = normalizeItemHrid(detail?.upgradeItemHrid);
      const producer = upgradeHrid ? getProducerAction(upgradeHrid) : null;
      for (const material of stage.materials ?? []) {
        if (upgradeHrid && material.itemHrid === upgradeHrid && producer && selected.has(producer.actionHrid)) {
          continue;
        }
        mergeMaterial(materials, material);
      }
    }
    return [...materials.values()];
  }
  function getCartItems() {
    return clone(
      [...cart.values()].map((item) => ({
        ...item,
        name: resolveItemName(item.itemHrid) || item.name
      }))
    );
  }
  function getCartItem(itemHrid, enhancementLevel = 0) {
    const item = cart.get(itemKey(itemHrid, enhancementLevel));
    return clone(
      item ? { ...item, name: resolveItemName(item.itemHrid) || item.name } : null
    );
  }
  function setCartOrder(keys) {
    if (!Array.isArray(keys)) return false;
    const orderedKeys = [];
    const seen = /* @__PURE__ */ new Set();
    for (const value of keys) {
      const key = String(value ?? "");
      if (!key || seen.has(key) || !cart.has(key)) continue;
      seen.add(key);
      orderedKeys.push(key);
    }
    for (const key of cart.keys()) {
      if (seen.has(key)) continue;
      seen.add(key);
      orderedKeys.push(key);
    }
    const currentKeys = [...cart.keys()];
    if (currentKeys.length === orderedKeys.length && currentKeys.every((key, index) => key === orderedKeys[index])) {
      return false;
    }
    const reordered = orderedKeys.map((key) => [key, cart.get(key)]);
    cart.clear();
    for (const [key, item] of reordered) cart.set(key, item);
    saveCartAndEmit({ reason: "reorder" });
    return true;
  }
  function saveCartAndEmit({ reason = "update", added = 0 } = {}) {
    persistData();
    emit("cart:change", { reason, added, items: getCartItems() });
  }
  function resizeCartAllocations(row, quantity) {
    const target = Math.max(0, Math.ceil(Number(quantity) || 0));
    row.allocations = normalizeAllocations(row.allocations, row.quantity);
    let delta = target - allocationTotal(row.allocations);
    if (delta > 0) {
      changeAllocation(row, { kind: "manual" }, delta);
      return;
    }
    let remove = -delta;
    const take = (owner) => {
      if (remove <= 0) return;
      const amount = Math.min(remove, getAllocationQuantity(row, owner));
      if (!amount) return;
      changeAllocation(row, owner, -amount);
      remove -= amount;
    };
    take({ kind: "manual" });
    take({ kind: "planning" });
    const newestProjects = [...plans.values()].sort(
      (left, right) => String(right.createdAt ?? "").localeCompare(
        String(left.createdAt ?? "")
      ) || String(right.id).localeCompare(String(left.id))
    ).map((plan) => String(plan.id));
    for (const id of newestProjects) take({ kind: "project", id });
    for (const id of Object.keys(row.allocations.projects ?? {})) {
      take({ kind: "project", id });
    }
    row.quantity = allocationTotal(row.allocations);
  }
  function consumeCartAllocations(row, quantity) {
    let remaining = Math.max(0, Math.floor(Number(quantity) || 0));
    const take = (owner) => {
      if (remaining <= 0) return;
      const amount = Math.min(remaining, getAllocationQuantity(row, owner));
      if (!amount) return;
      changeAllocation(row, owner, -amount);
      remaining -= amount;
    };
    const oldestProjects = [...plans.values()].sort(
      (left, right) => String(left.createdAt ?? "").localeCompare(
        String(right.createdAt ?? "")
      ) || String(left.id).localeCompare(String(right.id))
    ).map((plan) => String(plan.id));
    for (const id of oldestProjects) take({ kind: "project", id });
    take({ kind: "planning" });
    take({ kind: "manual" });
    for (const id of Object.keys(row.allocations.projects ?? {})) {
      take({ kind: "project", id });
    }
    row.quantity = allocationTotal(row.allocations);
  }
  function getCartAllocationSummary(itemHrid, enhancementLevel = 0) {
    const row = cart.get(itemKey(itemHrid, enhancementLevel));
    const allocations = normalizeAllocations(row?.allocations, row?.quantity);
    return clone({
      total: allocationTotal(allocations),
      manual: allocations.manual,
      planning: allocations.planning,
      project: Object.values(allocations.projects).reduce(
        (sum, quantity) => sum + quantity,
        0
      ),
      projects: allocations.projects
    });
  }
  function projectIdsFromCartRow(row) {
    return Object.keys(row?.allocations?.projects ?? {});
  }
  function deleteProjectsWithoutCartAllocations(candidateIds) {
    let changed = false;
    for (const id of new Set(candidateIds ?? [])) {
      const stillListed = [...cart.values()].some(
        (row) => Number(row.allocations?.projects?.[id]) > 0
      );
      if (!stillListed && plans.delete(id)) changed = true;
    }
    if (changed) savePlansAndEmit();
    return changed;
  }
  function releaseCartAllocation(owner, itemHrid = null, enhancementLevel = 0) {
    let changed = false;
    const rows2 = itemHrid ? [cart.get(itemKey(itemHrid, enhancementLevel))].filter(Boolean) : [...cart.values()];
    for (const row of rows2) {
      const quantity = getAllocationQuantity(row, owner);
      if (!quantity) continue;
      changeAllocation(row, owner, -quantity);
      changeAllocation(row, { kind: "manual" }, quantity);
      changed = true;
    }
    if (changed) saveCartAndEmit({ reason: "allocation" });
    return changed;
  }
  function moveCartAllocationToManual(itemHrid, enhancementLevel, owner, quantity) {
    const row = cart.get(itemKey(itemHrid, enhancementLevel));
    if (!row) return 0;
    const moved = Math.min(
      Math.max(0, Math.ceil(Number(quantity) || 0)),
      getAllocationQuantity(row, owner)
    );
    if (!moved) return 0;
    changeAllocation(row, owner, -moved);
    changeAllocation(row, { kind: "manual" }, moved);
    saveCartAndEmit({ reason: "allocation" });
    return moved;
  }
  function addToCart(input) {
    const rows2 = Array.isArray(input) ? input : [input];
    let added = 0;
    let skipped = 0;
    for (const value of rows2) {
      const itemHrid = normalizeItemHrid(value?.itemHrid ?? value?.itemId);
      const enhancementLevel = normalizeEnhancementLevel(value?.enhancementLevel);
      const quantity = Math.ceil(Number(value?.quantity) || 0);
      if (!itemHrid || quantity <= 0 || isCoin(itemHrid)) {
        skipped += 1;
        continue;
      }
      const key = itemKey(itemHrid, enhancementLevel);
      const existing = cart.get(key);
      const owner = allocationOwner(value);
      const row = {
        itemHrid,
        enhancementLevel,
        name: value.name || existing?.name || resolveItemName(itemHrid),
        quantity: existing?.quantity ?? 0,
        allocations: normalizeAllocations(
          existing?.allocations,
          existing?.quantity
        ),
        starred: Boolean(existing?.starred ?? value.starred),
        threshold: existing?.threshold ?? value.threshold ?? null,
        baselineStock: getInventoryCount2(itemHrid, enhancementLevel),
        source: value.source ?? existing?.source ?? "manual",
        updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
        manualOverrideUntil: existing?.manualOverrideUntil ?? 0
      };
      changeAllocation(row, owner, quantity);
      cart.set(key, row);
      added += 1;
    }
    if (added) saveCartAndEmit({ reason: "add", added });
    return { ok: added > 0, added, skipped };
  }
  function addRequirementsToCart(materials, source = "material", allocation = null) {
    return addToCart(
      (materials ?? []).filter(
        (material) => material.purchasable && material.addableShortage > 0
      ).map((material) => ({
        itemHrid: material.itemHrid,
        enhancementLevel: material.enhancementLevel,
        name: material.name,
        quantity: material.addableShortage,
        source,
        allocation
      }))
    );
  }
  function aggregateRequirements(requirementGroups) {
    const materials = /* @__PURE__ */ new Map();
    for (const group of requirementGroups ?? []) {
      for (const material of group ?? []) mergeMaterial(materials, material);
    }
    return [...materials.values()];
  }
  function setCartItemQuantity(itemHrid, quantity, enhancementLevel = 0) {
    const key = itemKey(itemHrid, enhancementLevel);
    const row = cart.get(key);
    if (!row) return { ok: false };
    const affectedProjects = projectIdsFromCartRow(row);
    const normalized = Math.max(0, Math.ceil(Number(quantity) || 0));
    if (!normalized && !row.starred) cart.delete(key);
    else {
      resizeCartAllocations(row, normalized);
      row.baselineStock = getInventoryCount2(row.itemHrid, row.enhancementLevel);
      row.manualOverrideUntil = Date.now() + MANUAL_OVERRIDE_MS;
      row.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
    }
    saveCartAndEmit();
    deleteProjectsWithoutCartAllocations(affectedProjects);
    return { ok: true };
  }
  function updateCartItem(itemHrid, enhancementLevel, patch) {
    const row = cart.get(itemKey(itemHrid, enhancementLevel));
    if (!row) return false;
    const affectedProjects = projectIdsFromCartRow(row);
    Object.assign(row, patch, { updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
    if (Object.hasOwn(patch, "quantity")) {
      resizeCartAllocations(row, patch.quantity);
      row.manualOverrideUntil = Date.now() + MANUAL_OVERRIDE_MS;
    }
    if (Object.hasOwn(patch, "threshold")) {
      row.threshold = patch.threshold == null ? null : Math.max(0, Math.ceil(Number(patch.threshold) || 0));
    }
    saveCartAndEmit();
    deleteProjectsWithoutCartAllocations(affectedProjects);
    return true;
  }
  function removeFromCart(itemHrid, enhancementLevel = 0) {
    const key = itemKey(itemHrid, enhancementLevel);
    const affectedProjects = projectIdsFromCartRow(cart.get(key));
    const removed = cart.delete(key);
    if (removed) {
      saveCartAndEmit();
      deleteProjectsWithoutCartAllocations(affectedProjects);
    }
    return { ok: removed };
  }
  function clearCart({ includeStarred = false } = {}) {
    const affectedProjects = /* @__PURE__ */ new Set();
    for (const [key, row] of cart) {
      if (includeStarred || !row.starred) {
        projectIdsFromCartRow(row).forEach((id) => affectedProjects.add(id));
        cart.delete(key);
      }
    }
    saveCartAndEmit();
    deleteProjectsWithoutCartAllocations(affectedProjects);
    return { ok: true };
  }
  function applyAcquisition(itemHrid, enhancementLevel, quantity, options = {}) {
    const key = itemKey(itemHrid, enhancementLevel);
    const row = cart.get(key);
    const acquired = Math.max(0, Math.floor(Number(quantity) || 0));
    if (!row || !acquired || !settings.inventorySyncEnabled) return false;
    const before = row.quantity;
    const affectedProjects = projectIdsFromCartRow(row);
    consumeCartAllocations(row, acquired);
    row.baselineStock = getInventoryCount2(row.itemHrid, row.enhancementLevel);
    row.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
    let fulfilled = false;
    if (row.quantity <= 0) {
      fulfilled = true;
      if (row.starred) row.quantity = 0;
      else cart.delete(key);
    }
    saveCartAndEmit();
    deleteProjectsWithoutCartAllocations(affectedProjects);
    if (fulfilled) {
      emit("item:fulfilled", { item: clone(row), source: options.source });
      if (![...cart.values()].some((candidate) => candidate.quantity > 0)) {
        emit("all:fulfilled", {});
      }
    }
    return row.quantity !== before || fulfilled;
  }
  function confirmMarketPurchase(itemHrid, quantity, enhancementLevel = 0) {
    const key = itemKey(itemHrid, enhancementLevel);
    const acquired = Math.max(0, Math.floor(Number(quantity) || 0));
    if (!acquired) return false;
    const applied = applyAcquisition(itemHrid, enhancementLevel, acquired, {
      source: "market-confirmation"
    });
    if (applied) {
      purchaseSuppressions.set(key, {
        quantity: acquired,
        expiresAt: Date.now() + PURCHASE_SUPPRESSION_MS
      });
    }
    return applied;
  }
  function consumePurchaseSuppression(key, delta) {
    const suppression = purchaseSuppressions.get(key);
    if (!suppression || Date.now() > suppression.expiresAt) {
      purchaseSuppressions.delete(key);
      return delta;
    }
    const remaining = Math.max(0, delta - suppression.quantity);
    suppression.quantity = Math.max(0, suppression.quantity - delta);
    if (!suppression.quantity) purchaseSuppressions.delete(key);
    return remaining;
  }
  function reconcileProjectCartAllocations() {
    const desiredByKey = /* @__PURE__ */ new Map();
    const activePlans = [...plans.values()].filter((plan) => plan.status !== "completed").sort(
      (left, right) => String(left.createdAt ?? "").localeCompare(
        String(right.createdAt ?? "")
      ) || String(left.id).localeCompare(String(right.id))
    );
    const demandsByKey = /* @__PURE__ */ new Map();
    for (const plan of activePlans) {
      for (const [key, quantity] of Object.entries(plan.materials ?? {})) {
        const demand = Math.max(0, Math.ceil(Number(quantity) || 0));
        if (!demand || isCoin(parseItemKey(key).itemHrid)) continue;
        if (!demandsByKey.has(key)) demandsByKey.set(key, []);
        demandsByKey.get(key).push({ planId: String(plan.id), demand });
      }
    }
    for (const [key, demands] of demandsByKey) {
      const parsed = parseItemKey(key);
      let available = getInventoryCount2(parsed.itemHrid, parsed.enhancementLevel);
      const desired = {};
      for (const { planId, demand } of demands) {
        const covered = Math.min(available, demand);
        available -= covered;
        const shortage = demand - covered;
        if (shortage > 0) desired[planId] = shortage;
      }
      desiredByKey.set(key, desired);
    }
    let changed = false;
    for (const [key, row] of [...cart]) {
      let rowChanged = false;
      const current = normalizeAllocations(
        row.allocations,
        row.quantity
      ).projects;
      const desired = desiredByKey.get(key) ?? {};
      const projectIds = /* @__PURE__ */ new Set([
        ...Object.keys(current),
        ...Object.keys(desired)
      ]);
      for (const id of projectIds) {
        const delta = (desired[id] ?? 0) - (current[id] ?? 0);
        if (!delta) continue;
        changeAllocation(row, { kind: "project", id }, delta);
        changed = true;
        rowChanged = true;
      }
      if (!rowChanged) continue;
      row.baselineStock = getInventoryCount2(row.itemHrid, row.enhancementLevel);
      row.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
      if (row.quantity <= 0 && !row.starred) cart.delete(key);
    }
    const additions = [];
    for (const [key, desired] of desiredByKey) {
      if (cart.has(key)) continue;
      const parsed = parseItemKey(key);
      for (const [projectId, quantity] of Object.entries(desired)) {
        additions.push({
          itemHrid: parsed.itemHrid,
          enhancementLevel: parsed.enhancementLevel,
          name: resolveItemName(parsed.itemHrid),
          quantity,
          source: "project",
          allocation: { kind: "project", projectId }
        });
      }
    }
    if (changed) saveCartAndEmit({ reason: "allocation" });
    const added = additions.length ? addToCart(additions).added : 0;
    return changed || added > 0;
  }
  function applyInventoryUpdates(items) {
    const before = inventoryCounts();
    for (const item of items ?? []) {
      if (!item?.itemHrid) continue;
      const key = inventoryEntryKey(item);
      const lacksStableId = item.hash == null && item.id == null;
      const matchingKeys = [...inventoryEntries].filter(
        ([currentKey, current]) => currentKey === key || (Number(item.count) <= 0 || lacksStableId) && sameInventorySlot(current, item)
      ).map(([currentKey]) => currentKey);
      for (const currentKey of matchingKeys) inventoryEntries.delete(currentKey);
      if (Number(item.count) > 0) inventoryEntries.set(key, { ...item });
    }
    const after = inventoryCounts();
    const keys = /* @__PURE__ */ new Set([...before.keys(), ...after.keys()]);
    const changes = [...keys].map((key) => ({
      key,
      ...parseItemKey(key),
      before: before.get(key) ?? 0,
      after: after.get(key) ?? 0,
      delta: (after.get(key) ?? 0) - (before.get(key) ?? 0)
    })).filter((change) => change.delta !== 0);
    if (!changes.length) return;
    for (const key of keys) {
      const delta = (after.get(key) ?? 0) - (before.get(key) ?? 0);
      const row = cart.get(key);
      if (row) row.baselineStock = after.get(key) ?? 0;
      if (delta > 0) {
        const unsuppressed = consumePurchaseSuppression(key, delta);
        if (unsuppressed > 0) {
          const parsed = parseItemKey(key);
          applyAcquisition(
            parsed.itemHrid,
            parsed.enhancementLevel,
            unsuppressed,
            {
              source: "inventory"
            }
          );
        }
      }
    }
    applyRestockThresholds();
    reconcileProjectCartAllocations();
    refreshPlanProgress();
    persistData();
    emit("inventory:change", {
      changes: changes.map(({ key: _key, ...change }) => change)
    });
  }
  function replaceInventorySnapshot(items) {
    const snapshot = (items ?? []).filter((item) => item?.itemHrid && Number(item.count) > 0).map((item) => ({ ...item }));
    const before = inventoryCounts();
    const removals = [...inventoryEntries.values()].map((item) => ({
      ...item,
      count: 0
    }));
    applyInventoryUpdates([...removals, ...snapshot]);
    applyRestockThresholds();
    reconcileProjectCartAllocations();
    const after = inventoryCounts();
    const keys = /* @__PURE__ */ new Set([...before.keys(), ...after.keys()]);
    const changedItemCount = [...keys].filter(
      (key) => (before.get(key) ?? 0) !== (after.get(key) ?? 0)
    ).length;
    return {
      stackCount: snapshot.length,
      changedItemCount,
      cartItemCount: [...cart.values()].filter((row) => row.quantity > 0).length,
      projectCount: [...plans.values()].filter(
        (plan) => plan.status !== "completed"
      ).length
    };
  }
  function applyRestockThresholds() {
    if (!settings.autoRestockEnabled) return;
    const now = Date.now();
    let changed = false;
    let added = 0;
    for (const row of cart.values()) {
      if (!row.starred || !row.threshold || row.manualOverrideUntil > now)
        continue;
      const owned = getInventoryCount2(row.itemHrid, row.enhancementLevel);
      const required = Math.max(0, row.threshold - owned);
      if (required !== row.quantity) {
        if (required > row.quantity) added += 1;
        resizeCartAllocations(row, required);
        row.baselineStock = owned;
        changed = true;
      }
    }
    if (changed) {
      saveCartAndEmit({
        reason: added ? "add" : "update",
        added
      });
    }
  }
  function getPlans() {
    refreshPlanProgress();
    return clone(
      [...plans.values()].map((plan) => ({
        ...plan,
        name: resolveActionName(plan.actionHrid) || plan.name
      }))
    );
  }
  function savePlansAndEmit() {
    persistData();
    emit("plan:change", { plans: getPlans() });
  }
  function createPlan(actionHrid, count, materials = null) {
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    if (!detail) return null;
    const targetCount = Math.max(1, Math.ceil(Number(count) || 1));
    const calculated = materials ?? (settings.upgradeChainEnabled ? calculateUpgradeChain(actionHrid, targetCount).leaves : calculateRequirements(actionHrid, targetCount).materials);
    const lockedMaterials = Object.fromEntries(
      calculated.filter((material) => material.purchasable).map((material) => [
        itemKey(material.itemHrid, material.enhancementLevel),
        material.suggested
      ])
    );
    const matching = [...plans.values()].find(
      (plan2) => plan2.status !== "completed" && plan2.actionHrid === actionHrid && plan2.targetCount === targetCount && JSON.stringify(plan2.materials ?? {}) === JSON.stringify(lockedMaterials)
    );
    if (matching) return clone({ ...matching, reused: true });
    const output = runtime.api.getExpectedOutputs?.(detail)?.[0] ?? null;
    const id = globalThis.crypto?.randomUUID?.() ?? `plan-${Date.now()}-${Math.random()}`;
    const plan = {
      id,
      actionHrid,
      name: detail.name ?? actionHrid.split("/").at(-1),
      targetCount,
      materials: lockedMaterials,
      outputItemHrid: output?.itemHrid ?? null,
      outputPerAction: Number(output?.count) || 0,
      baselineOutput: output ? getInventoryCount2(output.itemHrid, 0) : 0,
      onlineProgress: 0,
      progress: 0,
      status: "active",
      createdAt: (/* @__PURE__ */ new Date()).toISOString(),
      updatedAt: (/* @__PURE__ */ new Date()).toISOString()
    };
    plans.set(id, plan);
    savePlansAndEmit();
    return clone(plan);
  }
  function addProjectRequirementsToCart(planId) {
    const plan = plans.get(planId);
    if (!plan || plan.status === "completed") {
      return { ok: false, added: 0, skipped: 0 };
    }
    const demandByKey = /* @__PURE__ */ new Map();
    for (const candidate of plans.values()) {
      if (candidate.status === "completed") continue;
      for (const [key, quantity] of Object.entries(candidate.materials ?? {})) {
        demandByKey.set(
          key,
          (demandByKey.get(key) ?? 0) + Math.max(0, Math.ceil(Number(quantity) || 0))
        );
      }
    }
    const rows2 = [];
    for (const [key, demand] of demandByKey) {
      const parsed = parseItemKey(key);
      const desired = Math.max(
        0,
        demand - getInventoryCount2(parsed.itemHrid, parsed.enhancementLevel)
      );
      const summary = getCartAllocationSummary(
        parsed.itemHrid,
        parsed.enhancementLevel
      );
      const addable = Math.max(0, desired - summary.project);
      if (!addable || isCoin(parsed.itemHrid)) continue;
      rows2.push({
        itemHrid: parsed.itemHrid,
        enhancementLevel: parsed.enhancementLevel,
        name: resolveItemName(parsed.itemHrid),
        quantity: addable,
        source: "project",
        allocation: { kind: "project", projectId: String(planId) }
      });
    }
    return addToCart(rows2);
  }
  function releaseExcessProjectAllocations(plan) {
    let changed = false;
    for (const row of cart.values()) {
      const owner = { kind: "project", id: String(plan.id) };
      const allocated = getAllocationQuantity(row, owner);
      const allowed = Math.max(
        0,
        Math.ceil(
          Number(plan.materials?.[itemKey(row.itemHrid, row.enhancementLevel)]) || 0
        )
      );
      const excess = Math.max(0, allocated - allowed);
      if (!excess) continue;
      changeAllocation(row, owner, -excess);
      changeAllocation(row, { kind: "manual" }, excess);
      changed = true;
    }
    if (changed) saveCartAndEmit({ reason: "allocation" });
  }
  function refreshPlanProgress() {
    for (const plan of plans.values()) {
      if (plan.status === "completed") continue;
      const inventoryProgress = plan.outputItemHrid && plan.outputPerAction > 0 ? Math.floor(
        Math.max(
          0,
          getInventoryCount2(plan.outputItemHrid, 0) - plan.baselineOutput
        ) / plan.outputPerAction
      ) : 0;
      plan.progress = Math.min(
        plan.targetCount,
        Math.max(plan.progress ?? 0, plan.onlineProgress ?? 0, inventoryProgress)
      );
    }
  }
  function recordActionCompletion(payload) {
    const action = payload?.endCharacterAction ?? payload?.characterAction ?? payload;
    const actionHrid = action?.actionHrid ?? action?.hrid ?? runtime.state.currentActionsHridList?.find(
      (candidate) => candidate.id === action?.id
    )?.actionHrid;
    if (!actionHrid) return;
    let changed = false;
    for (const plan of plans.values()) {
      if (plan.status === "completed" || plan.actionHrid !== actionHrid) continue;
      plan.onlineProgress = Math.min(
        plan.targetCount,
        (plan.onlineProgress ?? 0) + 1
      );
      plan.progress = Math.max(plan.progress ?? 0, plan.onlineProgress);
      plan.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
      changed = true;
    }
    if (changed) savePlansAndEmit();
  }
  function updatePlan(id, patch) {
    const plan = plans.get(id);
    if (!plan) return false;
    if (Object.hasOwn(patch, "targetCount")) {
      plan.targetCount = Math.max(1, Math.ceil(Number(patch.targetCount) || 1));
      const chain = settings.upgradeChainEnabled ? calculateUpgradeChain(plan.actionHrid, plan.targetCount).leaves : calculateRequirements(plan.actionHrid, plan.targetCount).materials;
      plan.materials = Object.fromEntries(
        chain.filter((material) => material.purchasable).map((material) => [
          itemKey(material.itemHrid, material.enhancementLevel),
          material.suggested
        ])
      );
      releaseExcessProjectAllocations(plan);
    }
    if (patch.status === "completed" || patch.status === "active") {
      if (patch.status === "completed" && plan.status !== "completed") {
        releaseCartAllocation({ kind: "project", id: String(plan.id) });
      }
      plan.status = patch.status;
    }
    plan.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
    savePlansAndEmit();
    runtime.api.planning?.reconcilePlanningCart?.();
    return true;
  }
  function removePlan(id) {
    releaseCartAllocation({ kind: "project", id: String(id) });
    const removed = plans.delete(id);
    if (removed) {
      savePlansAndEmit();
      runtime.api.planning?.reconcilePlanningCart?.();
    }
    return removed;
  }
  function parsePurchaseConfirmation(payload) {
    if (payload?.type !== "info" || payload?.message !== "infoNotification.buyOrderCompleted") {
      return null;
    }
    const variables = new Map(
      (Array.isArray(payload.variables) ? payload.variables : []).filter((entry) => entry?.name).map((entry) => [entry.name, entry.data])
    );
    return {
      itemHrid: normalizeItemHrid(variables.get("itemHrid")),
      quantity: Math.max(0, Math.floor(Number(variables.get("count")) || 0)),
      enhancementLevel: normalizeEnhancementLevel(
        variables.get("enhancementLevel")
      )
    };
  }
  function installMessageHandlers() {
    runtime.onMessage("init_character_data", (payload) => {
      loadCharacterDataFromMessage(payload);
    });
    runtime.onMessage("items_updated", (payload) => {
      applyInventoryUpdates(payload.endCharacterItems ?? []);
    });
    runtime.onMessage("action_completed", recordActionCompletion);
    runtime.onMessage("info", (payload) => {
      const purchase = parsePurchaseConfirmation(payload);
      if (purchase?.itemHrid && purchase.quantity) {
        confirmMarketPurchase(
          purchase.itemHrid,
          purchase.quantity,
          purchase.enhancementLevel
        );
      }
    });
  }
  function exposePublicApi() {
    const pageWindow3 = globalThis.unsafeWindow ?? globalThis.window ?? globalThis;
    const root = pageWindow3.MWITools ?? {};
    const publicApi3 = {
      version: "1.0.0",
      apiVersion: 1,
      get ready() {
        return ready;
      },
      getCartItems,
      getCartItem,
      hasCartItem: (itemHrid, enhancementLevel = 0) => cart.has(itemKey(itemHrid, enhancementLevel)),
      getCartCount: () => cart.size,
      addToCart,
      setCartItemQuantity,
      removeFromCart,
      clearCart,
      resolveItemName,
      resolveActionName,
      normalizeItemId: (value) => normalizeItemHrid(value).replace("/items/", ""),
      openMarketplace: (itemHrid, enhancementLevel = 0) => runtime.api.openProcurementMarketplace?.(itemHrid, enhancementLevel) ?? false,
      on,
      off
    };
    root.shopping = publicApi3;
    pageWindow3.MWITools = root;
    return publicApi3;
  }
  installMessageHandlers();
  var publicApi = exposePublicApi();
  Object.assign(runtime.api, {
    procurement: {
      defaults: DEFAULT_SETTINGS,
      on,
      off,
      emit,
      get ready() {
        return ready;
      },
      get activeCharacterId() {
        return activeCharacterId2;
      },
      normalizeItemHrid,
      itemKey,
      parseItemKey,
      resolveItemName,
      resolveActionName,
      getSettings,
      setSetting: setSetting2,
      loadCharacterData,
      getInventoryCount: getInventoryCount2,
      getEffectiveInventory,
      getLockedDetails,
      getProjectReservedInventory,
      getInventoryAllocationSnapshot,
      suggestedMaterialCount,
      calculateRequirements,
      getProducerAction,
      calculateUpgradeChain,
      selectUpgradeChainMaterials,
      aggregateRequirements,
      getCartItems,
      getCartItem,
      setCartOrder,
      getCartAllocationSummary,
      addToCart,
      addRequirementsToCart,
      releaseCartAllocation,
      moveCartAllocationToManual,
      setCartItemQuantity,
      updateCartItem,
      removeFromCart,
      clearCart,
      getPlans,
      createPlan,
      addProjectRequirementsToCart,
      updatePlan,
      removePlan,
      getPlanningData,
      setPlanningData,
      confirmMarketPurchase,
      applyInventoryUpdates,
      replaceInventorySnapshot,
      applyRestockThresholds,
      recordActionCompletion,
      parsePurchaseConfirmation,
      publicApi
    }
  });

  // src/core/planning.js
  var MAX_DEPTH = 40;
  var EPSILON = 1e-9;
  var POLICIES = /* @__PURE__ */ new Set(["chain", "single", "buy"]);
  var procurement = runtime.api.procurement;
  var calculationRevision = 0;
  var calculationCount = 0;
  var cachedRevision = -1;
  var cachedResult = null;
  var lastResult = null;
  var lastCalculatedAt = null;
  var decisionResult = null;
  var decisionCalculatedAt = null;
  var decisionInventorySnapshot = null;
  var recipeCache = /* @__PURE__ */ new Map();
  function positiveInteger(value, fallback = 1) {
    const number3 = Math.ceil(Number(value) || 0);
    return number3 > 0 ? number3 : fallback;
  }
  function normalizePolicy(value, fallback = "chain") {
    if (value === "produce") return "chain";
    if (value === "acquire") return "buy";
    return POLICIES.has(value) ? value : fallback;
  }
  function maxHouseLevel(houseRoomHrid) {
    const levels = Object.keys(
      runtime.state.initData_houseRoomDetailMap?.[houseRoomHrid]?.upgradeCostsMap ?? {}
    ).map(Number).filter((level) => Number.isFinite(level));
    return levels.length ? Math.max(...levels) : 0;
  }
  function normalizeGoal(value) {
    const kind = value?.kind === "house" ? "house" : "item";
    const targetHrid = String(value?.targetHrid ?? value?.hrid ?? "").trim();
    if (!targetHrid) return null;
    const maximum = kind === "house" ? maxHouseLevel(targetHrid) : Infinity;
    if (kind === "house" && maximum <= 0) return null;
    return {
      id: value?.id ?? `${kind}:${targetHrid}`,
      kind,
      targetHrid,
      target: Math.min(positiveInteger(value?.target), maximum),
      policy: normalizePolicy(value?.policy),
      enabled: value?.enabled !== false,
      createdAt: value?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
      updatedAt: value?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
    };
  }
  function normalizeOverrides(value) {
    const result = {};
    for (const [goalId, entries] of Object.entries(value ?? {})) {
      if (!entries || typeof entries !== "object") continue;
      const normalized = {};
      for (const [itemHrid, policy] of Object.entries(entries)) {
        const item = procurement.normalizeItemHrid(itemHrid);
        if (item) normalized[item] = normalizePolicy(policy);
      }
      if (Object.keys(normalized).length) result[goalId] = normalized;
    }
    return result;
  }
  function getState() {
    const value = procurement.getPlanningData();
    return {
      goals: (value.goals ?? []).map(normalizeGoal).filter(Boolean),
      overrides: normalizeOverrides(value.overrides),
      defaults: {
        item: normalizePolicy(value.defaults?.item),
        house: normalizePolicy(value.defaults?.house)
      }
    };
  }
  function invalidate() {
    calculationRevision += 1;
    cachedResult = null;
    recipeCache = /* @__PURE__ */ new Map();
  }
  function clearDecisionResult() {
    decisionResult = null;
    decisionCalculatedAt = null;
    decisionInventorySnapshot = null;
  }
  function saveState(state, reason) {
    invalidate();
    if (reason === "goal") clearDecisionResult();
    return procurement.setPlanningData(state, reason);
  }
  function getGoals() {
    return getState().goals;
  }
  function upsertGoal(value) {
    const state = getState();
    const goal = normalizeGoal({
      ...value,
      policy: value?.policy ?? state.defaults[value?.kind === "house" ? "house" : "item"]
    });
    if (!goal) return null;
    const index = state.goals.findIndex(
      (entry) => entry.kind === goal.kind && entry.targetHrid === goal.targetHrid
    );
    if (index >= 0) {
      goal.id = state.goals[index].id;
      goal.createdAt = state.goals[index].createdAt;
      state.goals[index] = goal;
      delete state.overrides[goal.id];
    } else {
      state.goals.push(goal);
    }
    saveState(state, "goal");
    return goal;
  }
  function updateGoal(id, patch) {
    const state = getState();
    const index = state.goals.findIndex((goal) => goal.id === id);
    if (index < 0) return false;
    const previous = state.goals[index];
    const next = normalizeGoal({
      ...previous,
      ...patch,
      id,
      updatedAt: (/* @__PURE__ */ new Date()).toISOString()
    });
    if (!next) return false;
    state.goals[index] = next;
    if (Object.hasOwn(patch ?? {}, "policy")) {
      delete state.overrides[id];
    }
    saveState(state, "goal");
    return true;
  }
  function removeGoal(id) {
    const state = getState();
    const next = state.goals.filter((goal) => goal.id !== id);
    if (next.length === state.goals.length) return false;
    state.goals = next;
    delete state.overrides[id];
    saveState(state, "goal");
    return true;
  }
  function getDefaultPolicy(kind) {
    return getState().defaults[kind === "house" ? "house" : "item"];
  }
  function setDefaultPolicy(kind, policy) {
    const normalizedKind = kind === "house" ? "house" : "item";
    const state = getState();
    state.defaults[normalizedKind] = normalizePolicy(policy);
    saveState(state, "default-policy");
    return true;
  }
  function getGoalPolicy(goalId) {
    return getState().goals.find((goal) => goal.id === goalId)?.policy ?? "chain";
  }
  function setGoalPolicy(goalId, policy) {
    return updateGoal(goalId, { policy: normalizePolicy(policy) });
  }
  function getNodePolicy(goalId, itemHrid) {
    const state = getState();
    const item = procurement.normalizeItemHrid(itemHrid);
    return state.overrides[goalId]?.[item] ?? state.goals.find((goal) => goal.id === goalId)?.policy ?? "chain";
  }
  function setNodePolicy(goalId, itemHrid, policy) {
    const item = procurement.normalizeItemHrid(itemHrid);
    const state = getState();
    if (!item || !state.goals.some((goal) => goal.id === goalId)) return false;
    state.overrides[goalId] = { ...state.overrides[goalId] ?? {} };
    if (policy == null || policy === "inherit") {
      delete state.overrides[goalId][item];
      if (!Object.keys(state.overrides[goalId]).length) {
        delete state.overrides[goalId];
      }
    } else {
      state.overrides[goalId][item] = normalizePolicy(policy);
    }
    saveState(state, "node-policy");
    updateDecisionPolicy(goalId, item, getNodePolicy(goalId, item));
    return true;
  }
  function rows(value) {
    if (Array.isArray(value)) return value;
    if (value?.itemHrid || value?.hrid) return [value];
    return Object.entries(value ?? {}).map(([itemHrid, count]) => ({
      itemHrid,
      count: count?.count ?? count
    }));
  }
  function shopRewards(detail) {
    const explicit = detail?.itemRewards ?? detail?.rewards ?? detail?.rewardItems;
    if (explicit) return rows(explicit);
    const itemHrid = detail?.itemHrid ?? detail?.rewardItemHrid ?? detail?.item?.itemHrid;
    return itemHrid ? [
      {
        itemHrid,
        count: detail?.outputCount ?? detail?.itemCount ?? detail?.rewardCount ?? 1
      }
    ] : [];
  }
  function findShopRecipe(itemHrid) {
    const target = procurement.normalizeItemHrid(itemHrid);
    const matches = [];
    for (const [fallbackHrid, detail] of Object.entries(
      runtime.state.initData_shopItemDetailMap ?? {}
    )) {
      const outputCount = shopRewards(detail).reduce((sum, reward) => {
        const hrid = procurement.normalizeItemHrid(
          reward?.itemHrid ?? reward?.hrid
        );
        return hrid === target ? sum + Math.max(0, Number(reward?.count) || 0) : sum;
      }, 0);
      const costs = rows(detail?.costs ?? detail?.costItems ?? detail?.cost).map((cost) => ({
        itemHrid: procurement.normalizeItemHrid(cost?.itemHrid ?? cost?.hrid),
        count: Math.max(0, Number(cost?.count) || 0)
      })).filter((cost) => cost.itemHrid && cost.count > 0);
      if (outputCount > 0 && costs.length) {
        matches.push({
          kind: "exchange",
          id: detail?.hrid ?? detail?.shopItemHrid ?? fallbackHrid,
          outputCount,
          costs
        });
      }
    }
    matches.sort((left, right) => left.id.localeCompare(right.id));
    return matches[0] ?? null;
  }
  function findProductionRecipe(itemHrid) {
    const actionHrid = runtime.api.resolveProductionActionByItemHrid?.(itemHrid);
    if (!actionHrid) return null;
    const profile = runtime.api.getActionProductionProfile?.(actionHrid);
    if (profile?.status !== "complete") return null;
    const target = procurement.normalizeItemHrid(itemHrid);
    const outputCount = (profile.outputs ?? []).reduce(
      (sum, output) => procurement.normalizeItemHrid(output?.itemHrid) === target ? sum + Math.max(0, Number(output?.count) || 0) : sum,
      0
    );
    if (outputCount <= 0) return null;
    const requirements = procurement.calculateRequirements(actionHrid, 1);
    return requirements.materials?.length ? {
      kind: "production",
      id: actionHrid,
      outputCount,
      profile
    } : null;
  }
  function recipeFor(itemHrid) {
    const item = procurement.normalizeItemHrid(itemHrid);
    if (!recipeCache.has(item)) {
      recipeCache.set(item, findProductionRecipe(item) ?? findShopRecipe(item));
    }
    return recipeCache.get(item) ?? null;
  }
  function isCraftableItem(itemHrid) {
    const item = procurement.normalizeItemHrid(itemHrid);
    const actionHrid = runtime.api.resolveProductionActionByItemHrid?.(item);
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    return Boolean(actionHrid && runtime.api.getDirectInputs?.(detail)?.length);
  }
  function currentHouseLevel(houseRoomHrid) {
    const map = runtime.state.initData_characterHouseRoomMap ?? {};
    const direct = map[houseRoomHrid];
    if (direct) return Math.max(0, Number(direct.level) || 0);
    return Math.max(
      0,
      Number(
        Object.values(map).find((house) => house?.houseRoomHrid === houseRoomHrid)?.level
      ) || 0
    );
  }
  function allocateProportionally(entries, available, field) {
    const total = entries.reduce((sum, entry) => sum + entry.remaining, 0);
    const used = Math.min(Math.max(0, available), total);
    if (used <= EPSILON || total <= EPSILON) return 0;
    let distributed = 0;
    entries.forEach((entry, index) => {
      const share = index === entries.length - 1 ? used - distributed : Math.min(entry.remaining, used * entry.remaining / total);
      const applied = Math.max(0, Math.min(entry.remaining, share));
      entry[field] += applied;
      entry.remaining -= applied;
      distributed += applied;
    });
    return distributed;
  }
  function allocatePool(entries, available, field) {
    const buy = entries.filter(
      (entry) => entry.policy === "buy" && entry.remaining > EPSILON
    );
    const other = entries.filter(
      (entry) => entry.policy !== "buy" && entry.remaining > EPSILON
    );
    const buyUsed = allocateProportionally(buy, available, field);
    const otherUsed = allocateProportionally(other, available - buyUsed, field);
    return buyUsed + otherUsed;
  }
  function calculateFresh({ inventorySnapshot = null } = {}) {
    calculationCount += 1;
    const state = getState();
    const goals = state.goals.filter((goal) => goal.enabled).sort(
      (left, right) => left.kind.localeCompare(right.kind) || left.targetHrid.localeCompare(right.targetHrid)
    );
    const inventoryPool = /* @__PURE__ */ new Map();
    const virtualPool = /* @__PURE__ */ new Map();
    const decisions = /* @__PURE__ */ new Map();
    const steps = /* @__PURE__ */ new Map();
    const materials = /* @__PURE__ */ new Map();
    const warnings = [];
    let pending = [];
    const supplyFor = (itemHrid) => {
      const key = procurement.itemKey(itemHrid, 0);
      if (inventorySnapshot) {
        return inventorySnapshot.get(key) ?? { owned: 0, projectInventory: 0 };
      }
      const owned = procurement.getInventoryCount(itemHrid, 0);
      return {
        owned,
        projectInventory: procurement.getProjectReservedInventory(itemHrid, 0)
      };
    };
    const inventoryFor = (itemHrid) => {
      const key = procurement.itemKey(itemHrid, 0);
      if (!inventoryPool.has(key)) {
        const supply = supplyFor(itemHrid);
        inventoryPool.set(
          key,
          Math.max(0, supply.owned - supply.projectInventory)
        );
      }
      return inventoryPool.get(key) ?? 0;
    };
    const addPending = ({
      itemHrid,
      amount,
      goalId,
      inheritedPolicy,
      lineage,
      depth
    }) => {
      const item = procurement.normalizeItemHrid(itemHrid);
      const count = Math.max(0, Number(amount) || 0);
      if (!item || count <= EPSILON) return;
      pending.push({
        itemHrid: item,
        amount: count,
        goalId,
        inheritedPolicy: normalizePolicy(inheritedPolicy),
        lineage: [...lineage ?? []],
        depth: Math.max(0, Number(depth) || 0)
      });
    };
    const recordDecision = (entry, recipe) => {
      if (!recipe) return;
      const decision = decisions.get(entry.itemHrid) ?? {
        itemHrid: entry.itemHrid,
        outputCount: recipe.outputCount,
        policies: /* @__PURE__ */ new Set(),
        branches: /* @__PURE__ */ new Map()
      };
      const branchKey = `${entry.goalId}\0${entry.policy}`;
      const branch = decision.branches.get(branchKey) ?? {
        goalId: entry.goalId,
        policy: entry.policy,
        requiredOutput: 0,
        inventoryUsed: 0,
        virtualUsed: 0,
        remaining: 0
      };
      branch.requiredOutput += entry.amount;
      branch.inventoryUsed += entry.inventoryUsed;
      branch.virtualUsed += entry.virtualUsed;
      branch.remaining += entry.remaining;
      decision.policies.add(entry.policy);
      decision.branches.set(branchKey, branch);
      decisions.set(entry.itemHrid, decision);
    };
    const recordLeaf = (entry, reason) => {
      const material = materials.get(entry.itemHrid) ?? {
        itemHrid: entry.itemHrid,
        required: 0,
        inventoryUsed: 0,
        virtualUsed: 0,
        sourceIds: /* @__PURE__ */ new Set(),
        lineages: /* @__PURE__ */ new Set(),
        reasons: /* @__PURE__ */ new Set(),
        branches: /* @__PURE__ */ new Map()
      };
      material.required += entry.amount;
      material.inventoryUsed += entry.inventoryUsed;
      material.virtualUsed += entry.virtualUsed;
      material.sourceIds.add(entry.goalId);
      material.lineages.add([...entry.lineage, entry.itemHrid].join(" > "));
      material.reasons.add(reason);
      const branch = material.branches.get(entry.goalId) ?? {
        goalId: entry.goalId,
        required: 0,
        inventoryUsed: 0,
        virtualUsed: 0,
        requiredAfterSupply: 0,
        policies: /* @__PURE__ */ new Set()
      };
      branch.required += entry.amount;
      branch.inventoryUsed += entry.inventoryUsed;
      branch.virtualUsed += entry.virtualUsed;
      branch.requiredAfterSupply += entry.remaining;
      branch.policies.add(entry.policy);
      material.branches.set(entry.goalId, branch);
      materials.set(entry.itemHrid, material);
    };
    const addStep = (recipe, itemHrid, amount, actionCount, entries) => {
      const key = `${recipe.kind}:${recipe.id}`;
      const step = steps.get(key) ?? {
        key,
        kind: recipe.kind,
        id: recipe.id,
        itemHrid,
        requiredOutput: 0,
        actionCount: 0,
        outputCount: recipe.outputCount,
        sourceIds: /* @__PURE__ */ new Set()
      };
      step.requiredOutput += amount;
      step.actionCount += actionCount;
      entries.forEach((entry) => step.sourceIds.add(entry.goalId));
      steps.set(key, step);
    };
    for (const goal of goals) {
      if (goal.kind === "item") {
        addPending({
          itemHrid: goal.targetHrid,
          amount: goal.target,
          goalId: goal.id,
          inheritedPolicy: goal.policy,
          lineage: [],
          depth: 0
        });
        continue;
      }
      const detail = runtime.state.initData_houseRoomDetailMap?.[goal.targetHrid];
      const current = currentHouseLevel(goal.targetHrid);
      for (let level = current + 1; level <= goal.target; level += 1) {
        for (const cost of rows(detail?.upgradeCostsMap?.[level])) {
          addPending({
            itemHrid: cost.itemHrid,
            amount: cost.count,
            goalId: goal.id,
            inheritedPolicy: goal.policy,
            lineage: [goal.targetHrid],
            depth: 0
          });
        }
      }
    }
    while (pending.length) {
      const grouped = /* @__PURE__ */ new Map();
      for (const entry of pending) {
        const entries = grouped.get(entry.itemHrid) ?? [];
        entries.push(entry);
        grouped.set(entry.itemHrid, entries);
      }
      pending = [];
      for (const itemHrid of [...grouped.keys()].sort()) {
        const entries = grouped.get(itemHrid);
        for (const entry of entries) {
          entry.policy = normalizePolicy(
            state.overrides[entry.goalId]?.[itemHrid] ?? entry.inheritedPolicy
          );
          entry.remaining = entry.amount;
          entry.inventoryUsed = 0;
          entry.virtualUsed = 0;
          entry.forcedLeaf = entry.depth >= MAX_DEPTH || entry.lineage.includes(itemHrid);
          if (entry.forcedLeaf) {
            warnings.push({
              type: entry.depth >= MAX_DEPTH ? "truncated" : "cycle",
              itemHrid,
              goalId: entry.goalId,
              lineage: [...entry.lineage, itemHrid]
            });
          }
        }
        const key = procurement.itemKey(itemHrid, 0);
        const inventoryUsed = allocatePool(
          entries,
          inventoryFor(itemHrid),
          "inventoryUsed"
        );
        inventoryPool.set(
          key,
          Math.max(0, inventoryFor(itemHrid) - inventoryUsed)
        );
        const virtualUsed = allocatePool(
          entries,
          Math.max(0, virtualPool.get(itemHrid) ?? 0),
          "virtualUsed"
        );
        virtualPool.set(
          itemHrid,
          Math.max(0, (virtualPool.get(itemHrid) ?? 0) - virtualUsed)
        );
        const recipe = recipeFor(itemHrid);
        entries.forEach((entry) => recordDecision(entry, recipe));
        const produced = entries.filter(
          (entry) => !entry.forcedLeaf && entry.policy !== "buy" && recipe && entry.remaining > EPSILON
        );
        for (const entry of entries) {
          if (entry.forcedLeaf) recordLeaf(entry, "cycle");
          else if (entry.policy === "buy") recordLeaf(entry, "buy");
          else if (!recipe) recordLeaf(entry, "leaf");
        }
        if (!produced.length) continue;
        const totalRemaining = produced.reduce(
          (sum, entry) => sum + entry.remaining,
          0
        );
        const actionCount = Math.max(
          1,
          Math.ceil(totalRemaining / recipe.outputCount - EPSILON)
        );
        addStep(recipe, itemHrid, totalRemaining, actionCount, produced);
        const producedTarget = actionCount * recipe.outputCount;
        virtualPool.set(
          itemHrid,
          (virtualPool.get(itemHrid) ?? 0) + Math.max(0, producedTarget - totalRemaining)
        );
        const requirements = recipe.kind === "exchange" ? recipe.costs.map((cost) => ({
          itemHrid: cost.itemHrid,
          suggested: cost.count * actionCount
        })) : procurement.calculateRequirements(recipe.id, actionCount).materials;
        if (recipe.kind === "production") {
          for (const output of recipe.profile.outputs ?? []) {
            const outputHrid = procurement.normalizeItemHrid(output.itemHrid);
            if (!outputHrid || outputHrid === itemHrid) continue;
            virtualPool.set(
              outputHrid,
              (virtualPool.get(outputHrid) ?? 0) + Math.max(0, Number(output.count) || 0) * actionCount
            );
          }
        }
        for (const material of requirements ?? []) {
          for (const entry of produced) {
            addPending({
              itemHrid: material.itemHrid,
              amount: Math.max(0, Number(material.suggested) || 0) * (entry.remaining / totalRemaining),
              goalId: entry.goalId,
              inheritedPolicy: entry.policy === "single" ? "buy" : "chain",
              lineage: [...entry.lineage, itemHrid],
              depth: entry.depth + 1
            });
          }
        }
      }
    }
    const resultSteps = [...steps.values()].map((step) => ({ ...step, sourceIds: [...step.sourceIds] })).sort((left, right) => left.itemHrid.localeCompare(right.itemHrid));
    const actionsByItem = /* @__PURE__ */ new Map();
    for (const step of resultSteps) {
      actionsByItem.set(
        step.itemHrid,
        (actionsByItem.get(step.itemHrid) ?? 0) + step.actionCount
      );
    }
    const nodes = [...decisions.values()].map((decision) => {
      const branches = [...decision.branches.values()].map((branch) => ({
        ...branch,
        policies: [branch.policy]
      })).sort(
        (left, right) => left.goalId.localeCompare(right.goalId) || left.policy.localeCompare(right.policy)
      );
      return {
        itemHrid: decision.itemHrid,
        name: itemName(decision.itemHrid),
        requiredOutput: branches.reduce(
          (sum, branch) => sum + branch.requiredOutput,
          0
        ),
        requiredAfterSupply: branches.reduce(
          (sum, branch) => sum + branch.remaining,
          0
        ),
        outputCount: decision.outputCount,
        actionCount: actionsByItem.get(decision.itemHrid) ?? null,
        sourceIds: branches.map((branch) => branch.goalId),
        policies: [...decision.policies],
        policy: decision.policies.size === 1 ? [...decision.policies][0] : "mixed",
        branches
      };
    }).sort((left, right) => left.name.localeCompare(right.name));
    const resultMaterials = [...materials.values()].map((entry) => {
      const { owned, projectInventory } = supplyFor(entry.itemHrid);
      const cart2 = procurement.getCartAllocationSummary(entry.itemHrid, 0);
      const branches = [...entry.branches.values()].map((branch) => ({
        ...branch,
        policies: [...branch.policies],
        policy: branch.policies.size === 1 ? [...branch.policies][0] : "mixed"
      })).sort((left, right) => left.goalId.localeCompare(right.goalId));
      const requiredAfterSupply = branches.reduce(
        (sum, branch) => sum + branch.requiredAfterSupply,
        0
      );
      const remainingShortage = Math.max(
        0,
        Math.ceil(requiredAfterSupply - EPSILON)
      );
      const addableShortage = Math.max(
        0,
        Math.ceil(requiredAfterSupply - cart2.planning - EPSILON)
      );
      const detail = runtime.state.initData_itemDetailMap?.[entry.itemHrid];
      return {
        ...entry,
        sourceIds: [...entry.sourceIds],
        lineages: [...entry.lineages].filter(Boolean),
        reasons: [...entry.reasons],
        branches,
        name: itemName(entry.itemHrid),
        owned,
        projectInventory,
        availableInventory: Math.max(0, owned - projectInventory),
        requiredAfterSupply,
        remainingShortage,
        cart: cart2,
        addableShortage,
        purchasable: detail?.isTradable === true && entry.itemHrid !== "/items/coin"
      };
    }).sort((left, right) => left.name.localeCompare(right.name));
    return {
      status: goals.length ? "complete" : "empty",
      goals,
      nodes,
      steps: resultSteps,
      materials: resultMaterials,
      warnings
    };
  }
  function calculate() {
    if (cachedResult && cachedRevision === calculationRevision) {
      return cachedResult;
    }
    cachedResult = calculateFresh();
    cachedRevision = calculationRevision;
    lastResult = cachedResult;
    lastCalculatedAt = (/* @__PURE__ */ new Date()).toISOString();
    return cachedResult;
  }
  function calculateDecisions() {
    decisionInventorySnapshot = procurement.getInventoryAllocationSnapshot();
    decisionResult = calculateFresh({
      inventorySnapshot: decisionInventorySnapshot
    });
    decisionCalculatedAt = (/* @__PURE__ */ new Date()).toISOString();
    return decisionResult;
  }
  function getDecisionResult() {
    return decisionResult;
  }
  function updateDecisionPolicy(goalId, itemHrid, policy) {
    const node = decisionResult?.nodes?.find(
      (entry) => entry.itemHrid === itemHrid
    );
    if (!node) return;
    const merged = /* @__PURE__ */ new Map();
    for (const branch of node.branches) {
      const next = branch.goalId === goalId ? { ...branch, policy, policies: [policy] } : branch;
      const key = `${next.goalId}\0${next.policy}`;
      const current = merged.get(key);
      if (!current) {
        merged.set(key, { ...next });
        continue;
      }
      for (const field of [
        "requiredOutput",
        "inventoryUsed",
        "virtualUsed",
        "remaining"
      ]) {
        current[field] += next[field];
      }
    }
    node.branches = [...merged.values()].sort(
      (left, right) => left.goalId.localeCompare(right.goalId) || left.policy.localeCompare(right.policy)
    );
    node.policies = [...new Set(node.branches.map((branch) => branch.policy))];
    node.policy = node.policies.length === 1 ? node.policies[0] : "mixed";
  }
  function getPolicy(itemHrid) {
    const item = procurement.normalizeItemHrid(itemHrid);
    const state = getState();
    return state.goals.length > 0 && state.goals.every(
      (goal) => (state.overrides[goal.id]?.[item] ?? goal.policy) === "buy"
    ) ? "acquire" : "produce";
  }
  function setPolicy(itemHrid, policy) {
    const item = procurement.normalizeItemHrid(itemHrid);
    const state = getState();
    if (!item || !state.goals.length) return false;
    const mapped = normalizePolicy(policy);
    for (const goal of state.goals) {
      state.overrides[goal.id] = { ...state.overrides[goal.id] ?? {} };
      state.overrides[goal.id][item] = mapped;
    }
    saveState(state, "legacy-policy");
    for (const goal of state.goals) {
      updateDecisionPolicy(goal.id, item, mapped);
    }
    return true;
  }
  function reconcilePlanningCart(result = lastResult) {
    if (!result) return null;
    const allowed = new Map(
      result.materials.map((material) => [
        procurement.itemKey(material.itemHrid, 0),
        Math.ceil(material.requiredAfterSupply)
      ])
    );
    for (const row of procurement.getCartItems()) {
      const summary = procurement.getCartAllocationSummary(
        row.itemHrid,
        row.enhancementLevel
      );
      if (!summary.planning) continue;
      const excess = Math.max(
        0,
        summary.planning - (allowed.get(procurement.itemKey(row.itemHrid, row.enhancementLevel)) ?? 0)
      );
      if (excess > 0) {
        procurement.moveCartAllocationToManual(
          row.itemHrid,
          row.enhancementLevel,
          { kind: "planning" },
          excess
        );
      }
    }
    return result;
  }
  function refreshResultCartState(result) {
    if (!result) return result;
    for (const material of result.materials) {
      const cart2 = procurement.getCartAllocationSummary(material.itemHrid, 0);
      material.cart = cart2;
      material.addableShortage = Math.max(
        0,
        Math.ceil(material.requiredAfterSupply - cart2.planning - EPSILON)
      );
      material.remainingShortage = Math.max(
        0,
        Math.ceil(material.requiredAfterSupply - EPSILON)
      );
    }
    return result;
  }
  function recalculate({ inventorySnapshot = null } = {}) {
    const result = inventorySnapshot ? calculateFresh({ inventorySnapshot }) : calculate();
    reconcilePlanningCart(result);
    refreshResultCartState(result);
    cachedResult = result;
    lastResult = result;
    cachedRevision = calculationRevision;
    return result;
  }
  function calculateMaterials() {
    cachedResult = null;
    return recalculate({ inventorySnapshot: decisionInventorySnapshot });
  }
  function getResult() {
    return lastResult;
  }
  function isDirty() {
    return !lastResult || cachedRevision !== calculationRevision;
  }
  function addShortagesToCart(materials = lastResult?.materials ?? []) {
    return procurement.addToCart(
      materials.map((material) => {
        const cart2 = procurement.getCartAllocationSummary(material.itemHrid, 0);
        return {
          ...material,
          currentShortage: Math.max(
            0,
            Math.ceil(material.requiredAfterSupply - cart2.planning - EPSILON)
          )
        };
      }).filter(
        (material) => material.purchasable && material.currentShortage > 0
      ).map((material) => ({
        itemHrid: material.itemHrid,
        enhancementLevel: 0,
        name: material.name,
        quantity: material.currentShortage,
        source: "planning",
        allocation: { kind: "planning" }
      }))
    );
  }
  procurement.on("planning:change", (event) => {
    invalidate();
    if (event?.reason === "goal" || event?.reason === "load") {
      clearDecisionResult();
    }
  });
  procurement.on("cart:change", invalidate);
  for (const eventName of [
    "plan:change",
    "inventory:change",
    "settings:change"
  ]) {
    procurement.on(eventName, invalidate);
  }
  procurement.on("character:change", () => {
    invalidate();
    clearDecisionResult();
    lastResult = null;
    lastCalculatedAt = null;
  });
  for (const messageType of [
    "house_rooms_updated",
    "community_buffs_updated",
    "consumable_buffs_updated",
    "action_type_consumable_slots_updated",
    "equipment_buffs_updated",
    "personal_buffs_updated",
    "guild_buffs_updated",
    "init_character_data"
  ]) {
    runtime.onMessage(messageType, invalidate);
  }
  runtime.api.planning = {
    getGoals,
    upsertGoal,
    updateGoal,
    removeGoal,
    getDefaultPolicy,
    setDefaultPolicy,
    getGoalPolicy,
    setGoalPolicy,
    getNodePolicy,
    setNodePolicy,
    getPolicy,
    setPolicy,
    isCraftableItem,
    calculateDecisions,
    getDecisionResult,
    calculateMaterials,
    calculate,
    recalculate,
    getResult,
    isDirty,
    invalidate,
    getDiagnostics: () => ({
      revision: calculationRevision,
      calculationCount,
      dirty: isDirty(),
      lastCalculatedAt,
      decisionCalculatedAt
    }),
    reconcilePlanningCart,
    addShortagesToCart,
    on(listener) {
      return procurement.on("planning:change", listener);
    }
  };

  // src/core/train-planning.js
  var MAX_TRAIN_DEPTH = 50;
  function positiveNumber(value) {
    const number3 = Number(value);
    return Number.isFinite(number3) && number3 > 0 ? number3 : 0;
  }
  function normalizeItemHrid2(value) {
    return runtime.api.procurement?.normalizeItemHrid?.(value) ?? String(value ?? "");
  }
  function primaryOutput(detail) {
    const output = runtime.api.getExpectedOutputs?.(detail)?.[0];
    const itemHrid = normalizeItemHrid2(output?.itemHrid);
    return itemHrid ? { itemHrid, count: positiveNumber(output?.count) || 1 } : null;
  }
  var cachedActionMap = null;
  var cachedUpgradeActions = /* @__PURE__ */ new Map();
  var cachedBaseActions = /* @__PURE__ */ new Map();
  var cachedUpgradeInputItems = /* @__PURE__ */ new Set();
  function ensureActionIndex() {
    const actionMap = runtime.state.initData_actionDetailMap ?? {};
    if (actionMap === cachedActionMap) return;
    cachedActionMap = actionMap;
    cachedUpgradeActions = /* @__PURE__ */ new Map();
    cachedBaseActions = /* @__PURE__ */ new Map();
    cachedUpgradeInputItems = /* @__PURE__ */ new Set();
    for (const [fallbackHrid, detail] of Object.entries(actionMap)) {
      const output = primaryOutput(detail);
      if (!output?.itemHrid) continue;
      if (detail?.upgradeItemHrid) {
        cachedUpgradeInputItems.add(normalizeItemHrid2(detail.upgradeItemHrid));
        if (cachedUpgradeActions.has(output.itemHrid)) continue;
        cachedUpgradeActions.set(output.itemHrid, {
          actionHrid: detail.hrid ?? fallbackHrid,
          detail,
          output,
          inputHrid: normalizeItemHrid2(detail.upgradeItemHrid)
        });
      } else if (!cachedBaseActions.has(output.itemHrid)) {
        cachedBaseActions.set(output.itemHrid, {
          actionHrid: detail.hrid ?? fallbackHrid,
          detail,
          output
        });
      }
    }
  }
  function findUpgradeActionToItem(itemHrid) {
    const target = normalizeItemHrid2(itemHrid);
    ensureActionIndex();
    return cachedUpgradeActions.get(target) ?? null;
  }
  function findBaseActionForItem(itemHrid) {
    const target = normalizeItemHrid2(itemHrid);
    ensureActionIndex();
    return cachedBaseActions.get(target) ?? null;
  }
  function inputCountFor(detail, itemHrid) {
    const target = normalizeItemHrid2(itemHrid);
    return (runtime.api.getDirectInputs?.(detail) ?? []).reduce(
      (total, input) => normalizeItemHrid2(input?.itemHrid) === target ? total + positiveNumber(input?.count) : total,
      0
    );
  }
  function normalizeRows(value) {
    if (Array.isArray(value)) return value;
    if (value?.itemHrid || value?.hrid) return [value];
    return Object.entries(value ?? {}).map(([itemHrid, count]) => ({
      itemHrid,
      count: count?.count ?? count
    }));
  }
  function shopRewards2(detail) {
    const explicit = detail?.itemRewards ?? detail?.rewards ?? detail?.rewardItems;
    if (explicit) return normalizeRows(explicit);
    const itemHrid = detail?.itemHrid ?? detail?.rewardItemHrid ?? detail?.item?.itemHrid;
    return itemHrid ? [
      {
        itemHrid,
        count: detail?.outputCount ?? detail?.itemCount ?? detail?.rewardCount ?? 1
      }
    ] : [];
  }
  function findCoinShopOffer(itemHrid) {
    const target = normalizeItemHrid2(itemHrid);
    let best = null;
    for (const [fallbackHrid, detail] of Object.entries(
      runtime.state.initData_shopItemDetailMap ?? {}
    )) {
      const rewardCount = shopRewards2(detail).reduce(
        (total, reward) => normalizeItemHrid2(reward?.itemHrid ?? reward?.hrid) === target ? total + positiveNumber(reward?.count ?? 1) : total,
        0
      );
      if (!rewardCount) continue;
      const costs = normalizeRows(
        detail?.costs ?? detail?.costItems ?? detail?.cost
      ).filter((cost) => positiveNumber(cost?.count) > 0);
      if (!costs.length || costs.some(
        (cost) => normalizeItemHrid2(cost?.itemHrid ?? cost?.hrid) !== "/items/coin"
      )) {
        continue;
      }
      const coinCost = costs.reduce(
        (total, cost) => total + positiveNumber(cost?.count),
        0
      );
      const unitPrice = coinCost / rewardCount;
      if (!unitPrice || best && best.unitPrice <= unitPrice) continue;
      best = {
        shopHrid: detail?.hrid ?? detail?.shopItemHrid ?? fallbackHrid,
        itemHrid: target,
        rewardCount,
        unitPrice,
        detail
      };
    }
    return best;
  }
  function buildTrainChain(topItemHrid) {
    const steps = [];
    const visited = /* @__PURE__ */ new Set();
    let current = normalizeItemHrid2(topItemHrid);
    let cycle = false;
    let truncated = false;
    while (current) {
      if (visited.has(current)) {
        cycle = true;
        break;
      }
      if (steps.length >= MAX_TRAIN_DEPTH) {
        truncated = true;
        break;
      }
      visited.add(current);
      const upgrade = findUpgradeActionToItem(current);
      if (!upgrade) break;
      steps.unshift({
        kind: "upgrade",
        actionHrid: upgrade.actionHrid,
        outputHrid: current,
        outputCount: upgrade.output.count,
        inputHrid: upgrade.inputHrid,
        inputCount: inputCountFor(upgrade.detail, upgrade.inputHrid) || 1,
        detail: upgrade.detail
      });
      current = upgrade.inputHrid;
    }
    if (current && steps.length) {
      const base = findBaseActionForItem(current);
      if (base) {
        steps.unshift({
          kind: "craft",
          actionHrid: base.actionHrid,
          outputHrid: current,
          outputCount: base.output.count,
          inputHrid: null,
          inputCount: 0,
          detail: base.detail
        });
      } else {
        const offer = findCoinShopOffer(current);
        if (offer) {
          steps.unshift({
            kind: "shop",
            actionHrid: null,
            outputHrid: current,
            outputCount: 1,
            inputHrid: null,
            inputCount: 0,
            shopHrid: offer.shopHrid,
            shopOffer: offer,
            detail: null
          });
        }
      }
    }
    return { steps, cycle, truncated };
  }
  function demandCount(taskCounts, itemHrid) {
    if (taskCounts instanceof Map)
      return positiveNumber(taskCounts.get(itemHrid));
    return positiveNumber(taskCounts?.[itemHrid]);
  }
  function effectiveStepOutput(step) {
    const fallback = positiveNumber(step.outputCount) || 1;
    if (!step.actionHrid) return { known: true, count: fallback };
    const profile = runtime.api.getActionProductionProfile?.(step.actionHrid);
    if (profile?.status !== "complete") {
      return { known: false, count: fallback };
    }
    const target = normalizeItemHrid2(step.outputHrid);
    return {
      known: true,
      count: (profile.outputs ?? []).reduce(
        (total, output) => normalizeItemHrid2(output?.itemHrid) === target ? total + positiveNumber(output?.count) : total,
        0
      )
    };
  }
  function planTrainCounts(chain, taskCounts = {}) {
    const procurement4 = runtime.api.procurement;
    const planned = chain.steps.map((step) => ({ ...step, count: 0 }));
    const unavailableOutputs = /* @__PURE__ */ new Set();
    let requiredByAbove = 0;
    for (let index = planned.length - 1; index >= 0; index -= 1) {
      const step = planned[index];
      const effectiveOutput = effectiveStepOutput(step);
      const outputCount = effectiveOutput.count;
      step.baseOutputCount = positiveNumber(step.outputCount) || 1;
      step.outputCount = outputCount;
      step.outputCountSource = effectiveOutput.known ? "player" : "recipe";
      const inventory = positiveNumber(
        procurement4?.getInventoryCount?.(step.outputHrid, 0)
      );
      const shortageUnits = Math.max(0, requiredByAbove - inventory);
      const ownActions = Math.ceil(demandCount(taskCounts, step.outputHrid));
      step.requiredByAbove = requiredByAbove;
      step.inventory = inventory;
      step.shortageUnits = shortageUnits;
      if (effectiveOutput.known && outputCount <= 0) {
        step.unavailableOutput = true;
        step.count = 0;
        step.plannedOutput = 0;
        unavailableOutputs.add(step.outputHrid);
        requiredByAbove = 0;
        continue;
      }
      step.count = step.kind === "shop" ? Math.max(ownActions, Math.ceil(shortageUnits)) : Math.max(ownActions, Math.ceil(shortageUnits / outputCount));
      step.plannedOutput = step.count * outputCount;
      requiredByAbove = step.inputHrid ? step.count * (positiveNumber(step.inputCount) || 1) : 0;
    }
    return {
      ...chain,
      steps: planned,
      unavailableOutputs: [...unavailableOutputs]
    };
  }
  function applyShopPreference(plan, taskCounts = {}) {
    const root = plan.steps[0];
    if (root?.kind !== "craft" || demandCount(taskCounts, root.outputHrid) > 0 || root.count <= 0) {
      return plan;
    }
    const offer = findCoinShopOffer(root.outputHrid);
    if (!offer) return plan;
    const askPrice = positiveNumber(
      runtime.api.getAskPrice?.(root.outputHrid, 0)
    );
    if (askPrice && offer.unitPrice > askPrice) return plan;
    return {
      ...plan,
      steps: [
        {
          ...root,
          kind: "shop",
          actionHrid: null,
          outputCount: 1,
          count: Math.ceil(root.shortageUnits),
          plannedOutput: Math.ceil(root.shortageUnits),
          shopHrid: offer.shopHrid,
          shopOffer: offer,
          detail: null
        },
        ...plan.steps.slice(1)
      ]
    };
  }
  function createTrainPlan(topItemHrid, taskCounts = {}, options = {}) {
    const chain = buildTrainChain(topItemHrid);
    const planned = planTrainCounts(chain, taskCounts);
    return options.preferShop === false ? planned : applyShopPreference(planned, taskCounts);
  }
  function trainChainRoot(itemHrid) {
    let current = normalizeItemHrid2(itemHrid);
    const visited = /* @__PURE__ */ new Set();
    while (current && !visited.has(current)) {
      visited.add(current);
      const upgrade = findUpgradeActionToItem(current);
      if (!upgrade) break;
      current = upgrade.inputHrid;
    }
    return current;
  }
  function trainChainDepth(itemHrid) {
    let current = normalizeItemHrid2(itemHrid);
    const visited = /* @__PURE__ */ new Set();
    let depth = 0;
    while (current && !visited.has(current)) {
      visited.add(current);
      const upgrade = findUpgradeActionToItem(current);
      if (!upgrade) break;
      current = upgrade.inputHrid;
      depth += 1;
    }
    if (depth) return depth;
    ensureActionIndex();
    return cachedUpgradeInputItems.has(current) ? 0 : -1;
  }
  function parseTrainCount(raw) {
    const match = String(raw ?? "").trim().toLowerCase().match(/^(\d+(?:\.\d+)?)([kmb])?$/);
    if (!match) return null;
    const multiplier = { k: 1e3, m: 1e6, b: 1e9 }[match[2]] ?? 1;
    const count = Math.floor(Number(match[1]) * multiplier);
    return Number.isFinite(count) && count > 0 ? count : null;
  }
  var trainPlanning = {
    findUpgradeActionToItem,
    findBaseActionForItem,
    findCoinShopOffer,
    buildTrainChain,
    planTrainCounts,
    applyShopPreference,
    createTrainPlan,
    trainChainRoot,
    trainChainDepth,
    parseTrainCount
  };
  runtime.api.trainPlanning = trainPlanning;

  // src/core/xp-history.js
  var DB_NAME = "MWIToolsHistory";
  var STORE_NAME = "xpSnapshots";
  var FALLBACK_KEY = "MWITools_xp_history_v1";
  var RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
  var HOUR_MS = 60 * 60 * 1e3;
  var RECENT_WINDOW_MS = 6 * HOUR_MS;
  var RECENT_MINIMUM_COVERAGE_MS = HOUR_MS;
  function openDatabase() {
    if (!globalThis.indexedDB) return Promise.resolve(null);
    return new Promise((resolve) => {
      const request2 = globalThis.indexedDB.open(DB_NAME, 1);
      request2.onupgradeneeded = () => {
        const database = request2.result;
        if (!database.objectStoreNames.contains(STORE_NAME)) {
          const store = database.createObjectStore(STORE_NAME, {
            keyPath: "key",
            autoIncrement: true
          });
          store.createIndex("objectKey", "objectKey", { unique: false });
        }
      };
      request2.onsuccess = () => resolve(request2.result);
      request2.onerror = () => resolve(null);
    });
  }
  function readFallback() {
    try {
      const value = JSON.parse(
        globalThis.localStorage?.getItem(FALLBACK_KEY) || "[]"
      );
      return Array.isArray(value) ? value : [];
    } catch {
      return [];
    }
  }
  function writeFallback(records) {
    try {
      globalThis.localStorage?.setItem(FALLBACK_KEY, JSON.stringify(records));
    } catch (error) {
      console.warn(
        runtime.config.isZH ? "[MWITools] 无法保存经验历史备用数据" : "[MWITools] Unable to save XP history fallback",
        error
      );
    }
  }
  async function readIndexed(objectKey2) {
    const database = await openDatabase();
    if (!database) return null;
    return new Promise((resolve) => {
      const transaction = database.transaction(STORE_NAME, "readonly");
      const request2 = transaction.objectStore(STORE_NAME).index("objectKey").getAll(objectKey2);
      request2.onsuccess = () => resolve(request2.result ?? []);
      request2.onerror = () => resolve(null);
    });
  }
  async function replaceIndexed(objectKey2, records) {
    const database = await openDatabase();
    if (!database) return false;
    return new Promise((resolve) => {
      const transaction = database.transaction(STORE_NAME, "readwrite");
      const store = transaction.objectStore(STORE_NAME);
      const request2 = store.index("objectKey").openKeyCursor(globalThis.IDBKeyRange.only(objectKey2));
      request2.onsuccess = () => {
        const cursor = request2.result;
        if (cursor) {
          store.delete(cursor.primaryKey);
          cursor.continue();
          return;
        }
        for (const record of records) store.add({ ...record, objectKey: objectKey2 });
      };
      transaction.oncomplete = () => resolve(true);
      transaction.onerror = () => resolve(false);
    });
  }
  async function getXpHistory(objectKey2) {
    const indexed = await readIndexed(objectKey2);
    if (indexed !== null) return indexed.sort((a, b) => a.at - b.at);
    return readFallback().filter((record) => record.objectKey === objectKey2).sort((a, b) => a.at - b.at);
  }
  function compactHistory(records, now = Date.now()) {
    const cutoff = now - RETENTION_MS;
    const recentCutoff = now - 24 * HOUR_MS;
    const hourly = /* @__PURE__ */ new Map();
    const recent = [];
    for (const record of records) {
      if (record.at < cutoff) continue;
      if (record.at >= recentCutoff) recent.push(record);
      else hourly.set(Math.floor(record.at / HOUR_MS), record);
    }
    return [...hourly.values(), ...recent].sort((a, b) => a.at - b.at);
  }
  async function saveHistory(objectKey2, records) {
    if (await replaceIndexed(objectKey2, records)) return;
    const retained = readFallback().filter(
      (record) => record.objectKey !== objectKey2
    );
    writeFallback([
      ...retained,
      ...records.map((record) => ({ ...record, objectKey: objectKey2 }))
    ]);
  }
  async function recordXpSnapshot(objectKey2, xp, at = Date.now()) {
    const numericXp = Number(xp);
    const numericAt = Number(at);
    if (!objectKey2 || !Number.isFinite(numericXp) || !Number.isFinite(numericAt))
      return false;
    const records = await getXpHistory(objectKey2);
    const last = records.at(-1);
    if (last && (last.xp === numericXp || last.at >= numericAt || last.xp > numericXp))
      return false;
    records.push({ xp: numericXp, at: numericAt });
    await saveHistory(objectKey2, compactHistory(records, numericAt));
    return true;
  }
  function calculateWindowRate(records, windowMs, minimumCoverageMs, now) {
    const latest = records.at(-1);
    if (!latest) return null;
    const candidates = records.filter(
      (record) => record.at >= now - windowMs && record.at <= latest.at
    );
    const first = candidates[0];
    if (!first || latest.at - first.at < minimumCoverageMs || latest.xp < first.xp)
      return null;
    return (latest.xp - first.xp) / (latest.at - first.at) * HOUR_MS;
  }
  function calculateXpRates(records, now = Date.now()) {
    const sorted = [...records].sort((a, b) => a.at - b.at);
    const latest = sorted.at(-1);
    return {
      recent: calculateWindowRate(
        sorted,
        RECENT_WINDOW_MS,
        RECENT_MINIMUM_COVERAGE_MS,
        now
      ),
      hour: calculateWindowRate(sorted, HOUR_MS, 30 * 60 * 1e3, now),
      day: calculateWindowRate(sorted, 24 * HOUR_MS, 12 * HOUR_MS, now),
      lastSampleAt: latest?.at ?? null,
      points: sorted
    };
  }
  Object.assign(runtime.api, {
    getXpHistory,
    compactXpHistory: compactHistory,
    recordXpSnapshot,
    calculateXpRates
  });

  // src/core/asset-values.js
  var SHOP_CURRENCY_HRIDS = /* @__PURE__ */ new Set([
    "/items/chimerical_token",
    "/items/sinister_token",
    "/items/enchanted_token",
    "/items/pirate_token",
    "/items/task_token",
    "/items/labyrinth_token"
  ]);
  var COWBELL_VALUE_HRIDS = /* @__PURE__ */ new Set([
    "/items/cowbell",
    "/items/bag_of_10_cowbells"
  ]);
  var OPTIONAL_TOKEN_ASSET_HRIDS = /* @__PURE__ */ new Set([
    "/items/guild_token",
    "/items/chimerical_token",
    "/items/sinister_token",
    "/items/enchanted_token",
    "/items/pirate_token"
  ]);
  var ENHANCED_EQUIPMENT_MAX_MARKET_DEVIATION = 0.2;
  var MAX_ACQUISITION_DEPTH = 12;
  var DUNGEON_CHEST_HRID_PATTERN = /^\/items\/(.+?)(?:_refinement)?_chest$/;
  var assetValueCache = /* @__PURE__ */ new Map();
  var assetLiquidationCache = /* @__PURE__ */ new Map();
  var guildCreditHridCache = null;
  var actionOutputIndexSource = null;
  var actionOutputIndexes = null;
  var shopRewardIndexSources = null;
  var shopRewardIndex = null;
  function positiveNumber2(value) {
    const number3 = Number(value);
    return Number.isFinite(number3) && number3 > 0 ? number3 : 0;
  }
  function entriesOfMap(value) {
    if (Array.isArray(value)) {
      return value.map((record, index) => [
        record?.hrid ?? record?.itemHrid ?? record?.guildBuffHrid ?? String(index),
        record
      ]);
    }
    return Object.entries(value ?? {});
  }
  function invalidateAssetValueCache() {
    assetValueCache.clear();
    assetLiquidationCache.clear();
    guildCreditHridCache = null;
    actionOutputIndexSource = null;
    actionOutputIndexes = null;
    shopRewardIndexSources = null;
    shopRewardIndex = null;
    runtime.api.resetAssetValuationMarketSnapshot?.();
  }
  function getItemDetails(itemHrid) {
    return runtime.state.initData_itemDetailMap?.[itemHrid] ?? null;
  }
  function getDungeonEntryKeyItemHrid(itemHrid) {
    const match = DUNGEON_CHEST_HRID_PATTERN.exec(String(itemHrid ?? ""));
    if (!match) return null;
    const entryKeyItemHrid = `/items/${match[1]}_entry_key`;
    return getItemDetails(entryKeyItemHrid) ? entryKeyItemHrid : null;
  }
  function settingEnabled(id) {
    return Boolean(
      runtime.settings.get?.(id) ?? runtime.settings.settingsMap?.[id]?.isTrue
    );
  }
  function shouldIncludeCowbellsInAssets() {
    return settingEnabled("includeCowbellsInAssets");
  }
  function shouldIncludeGuildDungeonTokensInAssets() {
    return settingEnabled("includeGuildDungeonTokensInAssets");
  }
  function isOptionalTokenAsset(itemHrid) {
    return OPTIONAL_TOKEN_ASSET_HRIDS.has(itemHrid);
  }
  function isBackEquipment(itemHrid, itemLocationHrid = "") {
    if (itemLocationHrid === "/item_locations/back") return true;
    if (/(?:^|_)cape(?:_refined)?$/.test(String(itemHrid).split("/").at(-1))) {
      return true;
    }
    const detail = getItemDetails(itemHrid);
    const equipment = detail?.equipmentDetail;
    return [
      detail?.itemLocationHrid,
      detail?.equipmentSlotHrid,
      detail?.slotHrid,
      equipment?.itemLocationHrid,
      equipment?.equipmentSlotHrid,
      equipment?.slotHrid,
      equipment?.equipmentTypeHrid,
      equipment?.typeHrid,
      equipment?.type
    ].some((value) => /(?:^|[/_])back(?:$|[/_])/.test(String(value ?? "")));
  }
  function isEquipment(itemHrid) {
    return Boolean(getItemDetails(itemHrid)?.equipmentDetail);
  }
  function getGuildCreditHrids() {
    if (guildCreditHridCache) return guildCreditHridCache;
    const result = /* @__PURE__ */ new Set();
    for (const [, detail] of entriesOfMap(runtime.state.initData_itemDetailMap)) {
      for (const conversion of detail?.guildCreditConversions ?? []) {
        if (conversion?.creditItemHrid) result.add(conversion.creditItemHrid);
      }
    }
    guildCreditHridCache = result;
    return result;
  }
  function isNonTradableTokenAsset(itemHrid) {
    return itemHrid === "/items/cowbell" || isOptionalTokenAsset(itemHrid) && shouldIncludeGuildDungeonTokensInAssets() || getGuildCreditHrids().has(itemHrid);
  }
  function getGuildCreditValue(creditItemHrid) {
    let bestValue = Number.POSITIVE_INFINITY;
    for (const [fallbackHrid, detail] of entriesOfMap(
      runtime.state.initData_itemDetailMap
    )) {
      const itemHrid = detail?.hrid ?? detail?.itemHrid ?? fallbackHrid;
      if (!itemHrid || itemHrid === "/items/guild_token") continue;
      const materialValue = runtime.api.getAssetFairValue(itemHrid, 0);
      if (!(materialValue > 0)) continue;
      for (const conversion of detail?.guildCreditConversions ?? []) {
        if (conversion?.creditItemHrid !== creditItemHrid) continue;
        const itemCount = positiveNumber2(conversion.itemCount);
        const creditCount = positiveNumber2(conversion.creditCount);
        if (!itemCount || !creditCount) continue;
        bestValue = Math.min(
          bestValue,
          materialValue * itemCount / creditCount
        );
      }
    }
    return Number.isFinite(bestValue) ? bestValue : 0;
  }
  function getGuildTokenValue(context) {
    const detail = getItemDetails("/items/guild_token");
    let bestValue = 0;
    for (const conversion of detail?.guildCreditConversions ?? []) {
      const creditItemHrid = conversion?.creditItemHrid;
      const tokenCount = positiveNumber2(
        conversion?.guildTokenCount ?? conversion?.itemCount
      );
      const creditCount = positiveNumber2(conversion?.creditCount);
      if (!creditItemHrid || !tokenCount || !creditCount) continue;
      const creditValue = getAssetValueInternal(creditItemHrid, 0, context);
      if (!(creditValue > 0)) continue;
      bestValue = Math.max(bestValue, creditValue * creditCount / tokenCount);
    }
    return bestValue;
  }
  function getDropRecords(itemHrid) {
    const dropMap = runtime.state.initData_openableLootDropMap;
    const entry = dropMap instanceof Map ? dropMap.get(itemHrid) : dropMap?.[itemHrid];
    if (Array.isArray(entry)) return entry;
    return entry?.drops ?? entry?.dropTable ?? entry?.items ?? [];
  }
  function getOpenableValue(itemHrid, context) {
    const drops = getDropRecords(itemHrid);
    if (!Array.isArray(drops) || !drops.length) return 0;
    let total = 0;
    for (const drop of drops) {
      const dropItemHrid = drop?.itemHrid ?? drop?.hrid;
      if (!dropItemHrid) continue;
      const rawDropRate = Array.isArray(drop.dropRate) ? drop.dropRate[0] : drop.dropRate;
      const dropRate = Number.isFinite(Number(rawDropRate)) ? Math.max(0, Number(rawDropRate)) : 1;
      const minimum = Number(drop.minCount ?? drop.count ?? 1);
      const maximum = Number(drop.maxCount ?? drop.count ?? minimum);
      if (!Number.isFinite(minimum) || !Number.isFinite(maximum)) continue;
      const expectedCount = dropRate * (minimum + maximum) * 0.5;
      const value = getAssetValueInternal(
        dropItemHrid,
        drop.enhancementLevel ?? 0,
        context
      );
      total += expectedCount * value;
    }
    const keyItemHrid = getItemDetails(itemHrid)?.openKeyItemHrid;
    let keyCost = 0;
    if (keyItemHrid) {
      const keyCraftingCost = getCraftedAcquisitionValue(keyItemHrid, 0, context);
      if (!(keyCraftingCost > 0)) return 0;
      keyCost += keyCraftingCost;
    }
    const entryKeyItemHrid = getDungeonEntryKeyItemHrid(itemHrid);
    if (entryKeyItemHrid) {
      const entryKeyCost = getAssetValueInternal(entryKeyItemHrid, 0, context);
      if (!(entryKeyCost > 0)) return 0;
      keyCost += entryKeyCost;
    }
    return Math.max(0, total - keyCost);
  }
  function isPersonalBuffScroll(itemHrid) {
    return Boolean(getItemDetails(itemHrid)?.scrollDetail?.personalBuffTypeHrid);
  }
  function normalizeCostRecords(detail) {
    const raw = detail?.costs ?? detail?.costItems ?? detail?.cost;
    if (Array.isArray(raw)) return raw;
    if (raw?.itemHrid || raw?.hrid) return [raw];
    return Object.entries(raw ?? {}).map(([itemHrid, value]) => ({
      itemHrid,
      count: value?.count ?? value
    }));
  }
  function normalizeRewardRecords(detail) {
    const raw = detail?.itemRewards ?? detail?.rewards ?? detail?.rewardItems;
    if (Array.isArray(raw)) return raw;
    if (raw?.itemHrid || raw?.hrid) return [raw];
    const itemHrid = detail?.itemHrid ?? detail?.rewardItemHrid ?? detail?.item?.itemHrid ?? runtime.state.itemEnNameToHridMap?.[detail?.name];
    return itemHrid ? [
      {
        itemHrid,
        count: detail?.outputCount ?? detail?.itemCount ?? detail?.rewardCount ?? 1,
        enhancementLevel: detail?.enhancementLevel ?? 0
      }
    ] : [];
  }
  function getShopDetails() {
    return [
      runtime.state.initData_shopItemDetailMap,
      runtime.state.initData_taskShopItemDetailMap,
      runtime.state.initData_labyrinthShopItemDetailMap
    ].flatMap((map) => entriesOfMap(map).map(([, detail]) => detail));
  }
  function assetItemKey(itemHrid, enhancementLevel = 0) {
    return `${itemHrid}#${Number(enhancementLevel) || 0}`;
  }
  function getActionOutputIndexes() {
    const source = runtime.state.initData_actionDetailMap;
    if (source === actionOutputIndexSource && actionOutputIndexes) {
      return actionOutputIndexes;
    }
    const byItemAndLevel = /* @__PURE__ */ new Map();
    const upgradesByItem = /* @__PURE__ */ new Map();
    for (const [, action] of entriesOfMap(source)) {
      const countsByKey = /* @__PURE__ */ new Map();
      const countsByItem = /* @__PURE__ */ new Map();
      for (const output of action?.outputItems ?? []) {
        const itemHrid = output?.itemHrid ?? output?.hrid;
        if (!itemHrid) continue;
        const count = positiveNumber2(output.count ?? 1);
        if (!count) continue;
        const key = assetItemKey(itemHrid, output.enhancementLevel);
        countsByKey.set(key, (countsByKey.get(key) ?? 0) + count);
        countsByItem.set(itemHrid, (countsByItem.get(itemHrid) ?? 0) + count);
      }
      for (const [key, outputCount] of countsByKey) {
        const candidates = byItemAndLevel.get(key) ?? [];
        candidates.push({ action, outputCount });
        byItemAndLevel.set(key, candidates);
      }
      if (action?.upgradeItemHrid) {
        for (const [itemHrid, outputCount] of countsByItem) {
          const candidates = upgradesByItem.get(itemHrid) ?? [];
          candidates.push({ action, outputCount });
          upgradesByItem.set(itemHrid, candidates);
        }
      }
    }
    actionOutputIndexSource = source;
    actionOutputIndexes = { byItemAndLevel, upgradesByItem };
    return actionOutputIndexes;
  }
  function getShopRewardIndex() {
    const sources = [
      runtime.state.initData_shopItemDetailMap,
      runtime.state.initData_taskShopItemDetailMap,
      runtime.state.initData_labyrinthShopItemDetailMap
    ];
    if (shopRewardIndex && shopRewardIndexSources?.every((source, index2) => source === sources[index2])) {
      return shopRewardIndex;
    }
    const index = /* @__PURE__ */ new Map();
    for (const detail of getShopDetails()) {
      const countsByKey = /* @__PURE__ */ new Map();
      for (const reward of normalizeRewardRecords(detail)) {
        const itemHrid = reward?.itemHrid ?? reward?.hrid;
        if (!itemHrid) continue;
        const count = positiveNumber2(reward.count ?? 1);
        if (!count) continue;
        const key = assetItemKey(itemHrid, reward.enhancementLevel);
        countsByKey.set(key, (countsByKey.get(key) ?? 0) + count);
      }
      for (const [key, rewardCount] of countsByKey) {
        const candidates = index.get(key) ?? [];
        candidates.push({ detail, rewardCount });
        index.set(key, candidates);
      }
    }
    shopRewardIndexSources = sources;
    shopRewardIndex = index;
    return shopRewardIndex;
  }
  function getLootConfig(overrides = {}) {
    const settings2 = runtime.settings.settingsMap ?? {};
    return {
      sellAtAsk: Boolean(settings2.lootSellAtAsk?.isTrue),
      buyAtAsk: settings2.lootBuyAtAsk?.isTrue !== false,
      fromFragments: Boolean(settings2.lootKeyFromFragments?.isTrue),
      ignoreCowbells: Boolean(settings2.lootIgnoreCowbells?.isTrue),
      ...overrides
    };
  }
  function lootSaleValue(itemHrid, enhancementLevel, sellAtAsk) {
    if (itemHrid === "/items/coin") return 1;
    const price = sellAtAsk ? positiveNumber2(runtime.api.getAssetAskPrice?.(itemHrid, enhancementLevel)) : positiveNumber2(
      runtime.api.getAssetBidPrice?.(itemHrid, enhancementLevel)
    );
    if (!(price > 0)) return 0;
    const taxRate = Number(runtime.api.getMarketTaxRate?.(itemHrid)) || 0;
    return price * Math.max(0, 1 - taxRate);
  }
  function lootPurchaseValue(itemHrid, enhancementLevel, buyAtAsk) {
    if (itemHrid === "/items/coin") return 1;
    return buyAtAsk ? positiveNumber2(runtime.api.getAssetAskPrice?.(itemHrid, enhancementLevel)) : positiveNumber2(
      runtime.api.getAssetBidPrice?.(itemHrid, enhancementLevel)
    );
  }
  function lootDropIdentity(itemHrid, enhancementLevel = 0) {
    return `${itemHrid}:${Number(enhancementLevel) || 0}`;
  }
  function normalizeLootDrops(itemHrid) {
    return getDropRecords(itemHrid).flatMap((drop) => {
      const dropItemHrid = drop?.itemHrid ?? drop?.hrid;
      if (!dropItemHrid) return [];
      const rawDropRate = Array.isArray(drop.dropRate) ? drop.dropRate[0] : drop.dropRate;
      const dropRate = Number.isFinite(Number(rawDropRate)) ? Math.max(0, Number(rawDropRate)) : 1;
      const minimum = Number(drop.minCount ?? drop.count ?? 1);
      const maximum = Number(drop.maxCount ?? drop.count ?? minimum);
      if (!Number.isFinite(minimum) || !Number.isFinite(maximum)) return [];
      const enhancementLevel = Number(drop.enhancementLevel ?? 0) || 0;
      return [
        {
          itemHrid: dropItemHrid,
          enhancementLevel,
          dropRate,
          minCount: minimum,
          maxCount: maximum,
          expectedCount: dropRate * (minimum + maximum) * 0.5
        }
      ];
    });
  }
  function getLootKeyCost(keyItemHrid, config) {
    if (!keyItemHrid) {
      return { value: 0, complete: true, source: "none", missing: [] };
    }
    if (!config.fromFragments) {
      const value = lootPurchaseValue(keyItemHrid, 0, config.buyAtAsk);
      return {
        value,
        complete: value > 0,
        source: "finished",
        missing: value > 0 ? [] : [keyItemHrid]
      };
    }
    let bestValue = Number.POSITIVE_INFINITY;
    let sawRecipe = false;
    const missing = /* @__PURE__ */ new Set();
    for (const [actionMapHrid, action] of entriesOfMap(
      runtime.state.initData_actionDetailMap
    )) {
      const outputCount = (action?.outputItems ?? []).reduce((total, output) => {
        const outputHrid = output?.itemHrid ?? output?.hrid;
        const outputLevel = Number(output?.enhancementLevel ?? 0) || 0;
        return outputHrid === keyItemHrid && outputLevel === 0 ? total + positiveNumber2(output.count ?? 1) : total;
      }, 0);
      if (!outputCount) continue;
      sawRecipe = true;
      const actionHrid = action?.hrid ?? action?.actionHrid ?? actionMapHrid;
      const projection = runtime.api.projectActionCraftingCost?.(actionHrid, {
        getUnitPrice: (inputHrid, enhancementLevel = 0) => lootPurchaseValue(inputHrid, enhancementLevel, config.buyAtAsk)
      });
      if (projection?.complete && Number.isFinite(projection.totalCostPerAction)) {
        bestValue = Math.min(
          bestValue,
          projection.totalCostPerAction / outputCount
        );
        continue;
      }
      for (const missingItemHrid of projection?.missingPrices ?? []) {
        missing.add(missingItemHrid);
      }
      if (!projection || projection.missing?.length) missing.add(keyItemHrid);
    }
    return {
      value: Number.isFinite(bestValue) ? bestValue : 0,
      complete: Number.isFinite(bestValue),
      source: "fragments",
      missing: Number.isFinite(bestValue) ? [] : [...missing, ...sawRecipe ? [] : [keyItemHrid]]
    };
  }
  function getBestLootRedemptions(drops, config) {
    const displayedItems = new Set(
      drops.map((drop) => lootDropIdentity(drop.itemHrid, drop.enhancementLevel))
    );
    const tokenHrids = new Set(
      drops.map((drop) => drop.itemHrid).filter((itemHrid) => SHOP_CURRENCY_HRIDS.has(itemHrid))
    );
    const bestByToken = /* @__PURE__ */ new Map();
    if (!tokenHrids.size) return bestByToken;
    for (const detail of getShopDetails()) {
      const costs = normalizeCostRecords(detail);
      const rewards = normalizeRewardRecords(detail);
      if (!costs.length || rewards.length !== 1) continue;
      const reward = rewards[0];
      const rewardItemHrid = reward?.itemHrid ?? reward?.hrid;
      const rewardLevel = Number(reward?.enhancementLevel ?? 0) || 0;
      const rewardCount = positiveNumber2(reward?.count ?? 1);
      if (!rewardItemHrid || !rewardCount || config.ignoreCowbells && COWBELL_VALUE_HRIDS.has(rewardItemHrid) || !displayedItems.has(lootDropIdentity(rewardItemHrid, rewardLevel))) {
        continue;
      }
      const rewardUnitValue = lootSaleValue(
        rewardItemHrid,
        rewardLevel,
        config.sellAtAsk
      );
      if (!(rewardUnitValue > 0)) continue;
      for (const tokenItemHrid of tokenHrids) {
        const tokenCount = costs.reduce((total, cost) => {
          const costHrid = cost?.itemHrid ?? cost?.hrid;
          return costHrid === tokenItemHrid ? total + positiveNumber2(cost.count) : total;
        }, 0);
        if (!tokenCount) continue;
        let otherCostValue = 0;
        let complete = true;
        for (const cost of costs) {
          const costHrid = cost?.itemHrid ?? cost?.hrid;
          const count = positiveNumber2(cost?.count);
          if (!costHrid || !count || costHrid === tokenItemHrid) continue;
          const unitValue = lootPurchaseValue(
            costHrid,
            Number(cost?.enhancementLevel ?? 0) || 0,
            config.buyAtAsk
          );
          if (!(unitValue > 0)) {
            complete = false;
            break;
          }
          otherCostValue += count * unitValue;
        }
        if (!complete) continue;
        const totalValue = Math.max(
          0,
          rewardUnitValue * rewardCount - otherCostValue
        );
        const valuePerToken = totalValue / tokenCount;
        if (!(valuePerToken > 0)) continue;
        const candidate = {
          tokenItemHrid,
          tokenCount,
          rewardItemHrid,
          rewardEnhancementLevel: rewardLevel,
          rewardCount,
          rewardUnitValue,
          totalValue,
          valuePerToken
        };
        const current = bestByToken.get(tokenItemHrid);
        if (!current || valuePerToken > current.valuePerToken || valuePerToken === current.valuePerToken && rewardItemHrid.localeCompare(current.rewardItemHrid) < 0) {
          bestByToken.set(tokenItemHrid, candidate);
        }
      }
    }
    return bestByToken;
  }
  function projectLootChestInternal(itemHrid, config, visited) {
    if (visited.has(itemHrid)) return null;
    const normalizedDrops = normalizeLootDrops(itemHrid);
    if (!normalizedDrops.length) return null;
    visited.add(itemHrid);
    const bestRedemptions = getBestLootRedemptions(normalizedDrops, config);
    const rows2 = [];
    let grossValue = 0;
    for (const drop of normalizedDrops) {
      const cowbellExcluded = config.ignoreCowbells && COWBELL_VALUE_HRIDS.has(drop.itemHrid);
      const nestedChest = normalizeLootDrops(drop.itemHrid).length > 0;
      const marketValue = lootSaleValue(
        drop.itemHrid,
        drop.enhancementLevel,
        config.sellAtAsk
      );
      const tokenRedemption = bestRedemptions.get(drop.itemHrid) ?? null;
      const liquidationMode = config.sellAtAsk ? "aggressive" : "conservative";
      let unitValue = 0;
      let valueSource = "missing";
      let nested = false;
      let priced = false;
      let nestedMissing = [];
      let pendingSelfReference = false;
      if (cowbellExcluded) {
        unitValue = 0;
        valueSource = "excluded";
        priced = true;
      } else if (nestedChest && drop.itemHrid === itemHrid) {
        pendingSelfReference = true;
      } else if (nestedChest) {
        const nestedProjection = projectLootChestInternal(
          drop.itemHrid,
          config,
          visited
        );
        if (nestedProjection && Number.isFinite(nestedProjection.netValue)) {
          unitValue = Math.max(0, nestedProjection.netValue);
          valueSource = "nested";
          nested = true;
          priced = true;
          nestedMissing = nestedProjection.missing;
        }
      }
      if (!priced && !pendingSelfReference && marketValue > 0) {
        unitValue = marketValue;
        valueSource = "market";
        priced = true;
      }
      if (!priced && !pendingSelfReference && tokenRedemption) {
        unitValue = tokenRedemption.valuePerToken;
        valueSource = "redemption";
        priced = true;
      }
      if (!priced && !pendingSelfReference && !nestedChest) {
        const derivedLiquidation = getAssetLiquidationValueInternal(
          drop.itemHrid,
          drop.enhancementLevel,
          liquidationMode,
          /* @__PURE__ */ new Set()
        );
        if (derivedLiquidation.value > 0) {
          unitValue = derivedLiquidation.value;
          valueSource = "derived";
          priced = true;
          nestedMissing = derivedLiquidation.missingItemHrids;
        }
      }
      if (!priced && !pendingSelfReference && isPersonalBuffScroll(drop.itemHrid)) {
        unitValue = 0;
        valueSource = "zero";
        priced = true;
        nestedMissing = [];
      }
      const value = drop.expectedCount * unitValue;
      grossValue += value;
      const redemptions = [...bestRedemptions.values()].filter(
        (route) => route.rewardItemHrid === drop.itemHrid && route.rewardEnhancementLevel === drop.enhancementLevel
      );
      rows2.push({
        ...drop,
        unitValue,
        value,
        priced,
        nested,
        nestedMissing,
        pendingSelfReference,
        valueSource,
        tokenRedemption,
        redemptions
      });
    }
    const keyItemHrid = getItemDetails(itemHrid)?.openKeyItemHrid ?? null;
    const entryKeyItemHrid = getDungeonEntryKeyItemHrid(itemHrid);
    const key = getLootKeyCost(keyItemHrid, config);
    const entryKey = getLootKeyCost(entryKeyItemHrid, {
      ...config,
      fromFragments: false
    });
    const keyCost = key.value + entryKey.value;
    const keyComplete = key.complete && entryKey.complete;
    const selfRows = rows2.filter((row) => row.pendingSelfReference);
    const selfExpectedCount = selfRows.reduce(
      (total, row) => total + row.expectedCount,
      0
    );
    if (selfRows.length && keyComplete && selfExpectedCount < 1) {
      const selfValue = Math.max(
        0,
        (grossValue - keyCost) / (1 - selfExpectedCount)
      );
      for (const row of selfRows) {
        row.unitValue = selfValue;
        row.value = row.expectedCount * selfValue;
        row.priced = true;
        row.nested = true;
        row.valueSource = "nested";
        row.pendingSelfReference = false;
        grossValue += row.value;
      }
    }
    const missing = new Set(
      rows2.filter((row) => row.expectedCount > 0 && !row.priced).map((row) => row.itemHrid)
    );
    for (const row of rows2) {
      for (const missingItemHrid of row.nestedMissing) {
        missing.add(missingItemHrid);
      }
    }
    for (const missingItemHrid of key.missing) missing.add(missingItemHrid);
    for (const missingItemHrid of entryKey.missing) {
      missing.add(missingItemHrid);
    }
    const complete = missing.size === 0 && keyComplete;
    const netValue = keyComplete ? grossValue - keyCost : null;
    visited.delete(itemHrid);
    return {
      itemHrid,
      keyItemHrid,
      entryKeyItemHrid,
      config,
      drops: rows2.sort((left, right) => right.value - left.value),
      redemptions: [...bestRedemptions.values()],
      grossValue,
      keyCost,
      chestKeyCost: key.value,
      entryKeyCost: entryKey.value,
      keySource: key.source,
      keyComplete,
      chestKeyComplete: key.complete,
      entryKeyComplete: entryKey.complete,
      netValue,
      complete,
      missing: [...missing]
    };
  }
  function projectLootChest(itemHrid, overrides = {}) {
    return projectLootChestInternal(
      itemHrid,
      getLootConfig(overrides),
      /* @__PURE__ */ new Set()
    );
  }
  function acquisitionCostValue(itemHrid, enhancementLevel, context) {
    if (itemHrid === "/items/coin") return 1;
    const acquisitionDepth = [...context].filter(
      (key) => String(key).endsWith(":acquisition")
    ).length;
    if (acquisitionDepth >= MAX_ACQUISITION_DEPTH) return 0;
    return getAssetValueInternal(itemHrid, enhancementLevel, context, {
      forceAcquisitionValue: true
    });
  }
  function getShopAcquisitionValue(itemHrid, enhancementLevel, context) {
    let bestValue = Number.POSITIVE_INFINITY;
    const candidates = getShopRewardIndex().get(assetItemKey(itemHrid, enhancementLevel)) ?? [];
    for (const { detail, rewardCount } of candidates) {
      let totalCost = 0, complete = true;
      for (const cost of normalizeCostRecords(detail)) {
        const costHrid = cost?.itemHrid ?? cost?.hrid;
        const count = positiveNumber2(cost?.count);
        if (!costHrid || !count) continue;
        const unitValue = acquisitionCostValue(
          costHrid,
          Number(cost?.enhancementLevel ?? 0) || 0,
          context
        );
        if (!(unitValue > 0)) {
          complete = false;
          break;
        }
        totalCost += count * unitValue;
      }
      if (complete && totalCost > 0) {
        bestValue = Math.min(bestValue, totalCost / rewardCount);
      }
    }
    return Number.isFinite(bestValue) ? bestValue : 0;
  }
  function getRefinedAcquisitionValue(itemHrid, enhancementLevel, context) {
    if (!String(itemHrid).endsWith("_refined")) return 0;
    let bestValue = Number.POSITIVE_INFINITY;
    const candidates = getActionOutputIndexes().upgradesByItem.get(itemHrid) ?? [];
    for (const { action, outputCount } of candidates) {
      const baseItemHrid = action?.upgradeItemHrid;
      if (!baseItemHrid) continue;
      const retainedLevel = action.retainAllEnhancement ? enhancementLevel : 0;
      let totalCost = acquisitionCostValue(baseItemHrid, retainedLevel, context), complete = totalCost > 0;
      for (const cost of action.inputItems ?? []) {
        const costHrid = cost?.itemHrid ?? cost?.hrid;
        const count = positiveNumber2(cost?.count);
        if (!costHrid || !count) continue;
        const unitValue = acquisitionCostValue(
          costHrid,
          Number(cost?.enhancementLevel ?? 0) || 0,
          context
        );
        if (!(unitValue > 0)) {
          complete = false;
          break;
        }
        totalCost += count * unitValue;
      }
      if (complete && totalCost > 0) {
        bestValue = Math.min(bestValue, totalCost / outputCount);
      }
    }
    return Number.isFinite(bestValue) ? bestValue : 0;
  }
  function getCraftedAcquisitionValue(itemHrid, enhancementLevel, context) {
    let bestValue = Number.POSITIVE_INFINITY;
    const candidates = getActionOutputIndexes().byItemAndLevel.get(
      assetItemKey(itemHrid, enhancementLevel)
    ) ?? [];
    for (const { action, outputCount } of candidates) {
      let totalCost = 0;
      let complete = true;
      const inputItems = action?.inputItems ?? [];
      const upgradeItemHrid = action?.upgradeItemHrid;
      if (upgradeItemHrid) {
        const retainedLevel = action.retainAllEnhancement ? enhancementLevel : 0;
        const upgradeValue = acquisitionCostValue(
          upgradeItemHrid,
          retainedLevel,
          context
        );
        if (!(upgradeValue > 0)) complete = false;
        else totalCost += upgradeValue;
      }
      for (const input of inputItems) {
        const inputHrid = input?.itemHrid ?? input?.hrid;
        const count = positiveNumber2(input?.count);
        if (!inputHrid || !count) continue;
        const inputValue = acquisitionCostValue(
          inputHrid,
          Number(input?.enhancementLevel ?? 0) || 0,
          context
        );
        if (!(inputValue > 0)) {
          complete = false;
          break;
        }
        totalCost += count * inputValue;
      }
      if (complete && totalCost > 0) {
        bestValue = Math.min(bestValue, totalCost / outputCount);
      }
    }
    return Number.isFinite(bestValue) ? bestValue : 0;
  }
  function getEnhancedEquipmentCost(itemHrid, enhancementLevel, context, options = {}) {
    if (!(enhancementLevel > 0) || !isEquipment(itemHrid) || typeof runtime.api.calculateEnhancementPlan !== "function") {
      return 0;
    }
    const backEquipment = isBackEquipment(itemHrid, options.itemLocationHrid);
    const plan = runtime.api.calculateEnhancementPlan({
      itemHrid,
      targetLevel: enhancementLevel,
      forcedProtectionItemHrid: backEquipment ? "/items/mirror_of_protection" : null,
      allowPhilosopherMirror: true,
      getFairValue: (hrid, level = 0) => acquisitionCostValue(hrid, level, context),
      getMarketValue: (hrid, level = 0) => runtime.api.getAssetFairValue(hrid, level)
    });
    return plan?.status === "complete" ? positiveNumber2(plan.totalCost) : 0;
  }
  function getShopCurrencyValue(currencyItemHrid, context) {
    let bestValue = 0;
    for (const detail of getShopDetails()) {
      const costs = normalizeCostRecords(detail);
      const targetCost = costs.find(
        (cost) => (cost?.itemHrid ?? cost?.hrid) === currencyItemHrid
      );
      const targetCount = positiveNumber2(targetCost?.count);
      if (!targetCount) continue;
      let rewardValue2 = 0;
      for (const reward of normalizeRewardRecords(detail)) {
        const itemHrid = reward?.itemHrid ?? reward?.hrid;
        if (!itemHrid) continue;
        rewardValue2 += positiveNumber2(reward.count ?? 1) * getAssetValueInternal(itemHrid, reward.enhancementLevel ?? 0, context);
      }
      let otherCostValue = 0;
      for (const cost of costs) {
        const itemHrid = cost?.itemHrid ?? cost?.hrid;
        if (!itemHrid || itemHrid === currencyItemHrid) continue;
        otherCostValue += positiveNumber2(cost.count) * getAssetValueInternal(itemHrid, 0, context);
      }
      bestValue = Math.max(
        bestValue,
        Math.max(0, rewardValue2 - otherCostValue) / targetCount
      );
    }
    return bestValue;
  }
  function getAssetValueInternal(itemHrid, enhancementLevel, context, options = {}) {
    if (!itemHrid) return 0;
    if (itemHrid === "/items/coin") return 1;
    const level = Number(enhancementLevel) || 0;
    const directFairValue = runtime.api.getAssetFairValue(itemHrid, level);
    const backEquipment = isBackEquipment(itemHrid, options.itemLocationHrid);
    const enhancedEquipment = level > 0 && isEquipment(itemHrid);
    const refinedBackEquipment = backEquipment && String(itemHrid).endsWith("_refined");
    const ordinaryBackMirrorValue = level === 0 && backEquipment && !refinedBackEquipment && options.forceAcquisitionValue !== true && settingEnabled("valueBackEquipmentWithProtectionMirror");
    const preferAcquisitionValue = options.forceAcquisitionValue === true || refinedBackEquipment;
    const cacheMode = enhancedEquipment ? backEquipment ? "enhancement-protected-mirror" : "enhancement-protected" : ordinaryBackMirrorValue ? "protection-mirror-value" : preferAcquisitionValue ? "acquisition" : "market";
    const cacheKey = `${itemHrid}:${level}:${cacheMode}`;
    if (assetValueCache.has(cacheKey)) return assetValueCache.get(cacheKey);
    if (context.has(cacheKey)) return 0;
    if (ordinaryBackMirrorValue) {
      context.add(cacheKey);
      const mirrorValue = getAssetValueInternal(
        "/items/mirror_of_protection",
        0,
        context
      );
      context.delete(cacheKey);
      if (mirrorValue > 0) {
        assetValueCache.set(cacheKey, mirrorValue);
        return mirrorValue;
      }
    }
    if (enhancedEquipment) {
      const enhancementCost = getEnhancedEquipmentCost(
        itemHrid,
        level,
        context,
        options
      );
      if (enhancementCost > 0) {
        const deviation = directFairValue > 0 ? Math.abs(directFairValue - enhancementCost) / enhancementCost : Number.POSITIVE_INFINITY;
        const value2 = directFairValue > 0 && deviation <= ENHANCED_EQUIPMENT_MAX_MARKET_DEVIATION ? directFairValue : enhancementCost;
        assetValueCache.set(cacheKey, value2);
        return value2;
      }
      if (directFairValue > 0) {
        assetValueCache.set(cacheKey, directFairValue);
        return directFairValue;
      }
    }
    if (!preferAcquisitionValue && directFairValue > 0) {
      assetValueCache.set(cacheKey, directFairValue);
      return directFairValue;
    }
    if (directFairValue <= 0 && isPersonalBuffScroll(itemHrid)) {
      assetValueCache.set(cacheKey, 0);
      return 0;
    }
    context.add(cacheKey);
    let value = 0;
    if (itemHrid === "/items/cowbell") {
      value = getAssetValueInternal("/items/bag_of_10_cowbells", 0, context) / 10;
    } else if (getGuildCreditHrids().has(itemHrid)) {
      value = getGuildCreditValue(itemHrid);
    } else if (itemHrid === "/items/guild_token") {
      value = getGuildTokenValue(context);
    } else if (SHOP_CURRENCY_HRIDS.has(itemHrid)) {
      value = getShopCurrencyValue(itemHrid, context);
    } else {
      if (preferAcquisitionValue) {
        const candidates = [
          directFairValue,
          getShopAcquisitionValue(itemHrid, level, context),
          getCraftedAcquisitionValue(itemHrid, level, context),
          getRefinedAcquisitionValue(itemHrid, level, context)
        ].filter((candidate) => candidate > 0);
        value = candidates.length ? Math.min(...candidates) : 0;
      } else {
        value = Math.max(
          !backEquipment ? getShopAcquisitionValue(itemHrid, level, context) : 0,
          getRefinedAcquisitionValue(itemHrid, level, context),
          getOpenableValue(itemHrid, context)
        );
      }
    }
    context.delete(cacheKey);
    const keyedOpenable = Boolean(getItemDetails(itemHrid)?.openKeyItemHrid) && getDropRecords(itemHrid).length > 0;
    if (keyedOpenable && !(value > 0)) {
      assetValueCache.set(cacheKey, 0);
      return 0;
    }
    if (!(value > 0)) value = directFairValue;
    if (!(value > 0)) {
      value = positiveNumber2(getItemDetails(itemHrid)?.sellPrice);
    }
    const normalizedValue = Number.isFinite(value) && value > 0 ? value : 0;
    assetValueCache.set(cacheKey, normalizedValue);
    return normalizedValue;
  }
  function getAssetValue(itemHrid, enhancementLevel = 0, options = {}) {
    return getAssetValueInternal(itemHrid, enhancementLevel, /* @__PURE__ */ new Set(), options);
  }
  function directLiquidationValue(itemHrid, enhancementLevel, mode) {
    if (mode === "conservative") {
      return positiveNumber2(
        runtime.api.getAssetNetSellPrice?.(itemHrid, enhancementLevel)
      );
    }
    if (mode === "aggressive") {
      return positiveNumber2(
        runtime.api.getAssetNetSellPriceAtAsk?.(itemHrid, enhancementLevel)
      );
    }
    const fairValue = positiveNumber2(
      runtime.api.getAssetFairValue?.(itemHrid, enhancementLevel)
    );
    if (!fairValue) return 0;
    const taxRate = Number(runtime.api.getMarketTaxRate?.(itemHrid)) || 0;
    return fairValue * Math.max(0, 1 - taxRate);
  }
  function liquidationResult(value, source, missingItemHrids = []) {
    const normalizedValue = positiveNumber2(value);
    const missing = [...new Set(missingItemHrids.filter(Boolean))];
    return {
      value: normalizedValue,
      complete: normalizedValue > 0 && missing.length === 0,
      source: normalizedValue > 0 ? source : "missing",
      missingItemHrids: missing
    };
  }
  function mergeLiquidationMissing(results) {
    return results.flatMap((result) => result?.missingItemHrids ?? []);
  }
  function getCraftedLiquidationValue(itemHrid, enhancementLevel, mode, context) {
    let bestValue = Number.POSITIVE_INFINITY;
    let missingItemHrids = [];
    const candidates = getActionOutputIndexes().byItemAndLevel.get(
      assetItemKey(itemHrid, enhancementLevel)
    ) ?? [];
    for (const { action, outputCount } of candidates) {
      const inputItems = [...action?.inputItems ?? []];
      const upgradeItemHrid = action?.upgradeItemHrid;
      if (upgradeItemHrid) {
        inputItems.unshift({
          itemHrid: upgradeItemHrid,
          enhancementLevel: action.retainAllEnhancement ? enhancementLevel : 0,
          count: 1
        });
      }
      let totalCost = 0;
      let complete = true;
      const results = [];
      for (const input of inputItems) {
        const inputHrid = input?.itemHrid ?? input?.hrid;
        const count = positiveNumber2(input?.count);
        if (!inputHrid || !count) continue;
        const result = inputHrid === "/items/coin" ? liquidationResult(1, "coin") : getAssetLiquidationValueInternal(
          inputHrid,
          Number(input?.enhancementLevel ?? 0) || 0,
          mode,
          context
        );
        results.push(result);
        if (!(result.value > 0)) {
          complete = false;
          continue;
        }
        totalCost += count * result.value;
      }
      if (complete && totalCost > 0) {
        const unitCost = totalCost / outputCount;
        if (unitCost < bestValue) bestValue = unitCost;
      } else {
        missingItemHrids.push(...mergeLiquidationMissing(results));
      }
    }
    if (Number.isFinite(bestValue)) {
      return liquidationResult(bestValue, "crafting");
    }
    return liquidationResult(0, "missing", [...missingItemHrids, itemHrid]);
  }
  function getGuildCreditLiquidationValue(creditItemHrid, mode, context) {
    let bestValue = Number.POSITIVE_INFINITY;
    let bestResult = null;
    for (const [fallbackHrid, detail] of entriesOfMap(
      runtime.state.initData_itemDetailMap
    )) {
      const itemHrid = detail?.hrid ?? detail?.itemHrid ?? fallbackHrid;
      if (!itemHrid || itemHrid === "/items/guild_token") continue;
      for (const conversion of detail?.guildCreditConversions ?? []) {
        if (conversion?.creditItemHrid !== creditItemHrid) continue;
        const itemCount = positiveNumber2(conversion.itemCount);
        const creditCount = positiveNumber2(conversion.creditCount);
        if (!itemCount || !creditCount) continue;
        const material = getAssetLiquidationValueInternal(
          itemHrid,
          0,
          mode,
          context
        );
        if (material.value > 0) {
          const value = material.value * itemCount / creditCount;
          if (value < bestValue) {
            bestValue = value;
            bestResult = material;
          }
        }
      }
    }
    return liquidationResult(
      Number.isFinite(bestValue) ? bestValue : 0,
      "conversion",
      bestResult?.missingItemHrids ?? []
    );
  }
  function getGuildTokenLiquidationValue(mode, context) {
    const detail = getItemDetails("/items/guild_token");
    let bestValue = 0;
    let bestResult = null;
    for (const conversion of detail?.guildCreditConversions ?? []) {
      const creditItemHrid = conversion?.creditItemHrid;
      const tokenCount = positiveNumber2(
        conversion?.guildTokenCount ?? conversion?.itemCount
      );
      const creditCount = positiveNumber2(conversion?.creditCount);
      if (!creditItemHrid || !tokenCount || !creditCount) continue;
      const credit = getAssetLiquidationValueInternal(
        creditItemHrid,
        0,
        mode,
        context
      );
      if (credit.value > 0) {
        const value = credit.value * creditCount / tokenCount;
        if (value > bestValue) {
          bestValue = value;
          bestResult = credit;
        }
      }
    }
    return liquidationResult(
      bestValue,
      "conversion",
      bestResult?.missingItemHrids ?? []
    );
  }
  function getShopCurrencyLiquidationValue(currencyItemHrid, mode, context) {
    let bestValue = 0;
    let bestResults = [];
    for (const detail of getShopDetails()) {
      const costs = normalizeCostRecords(detail);
      const targetCost = costs.find(
        (cost) => (cost?.itemHrid ?? cost?.hrid) === currencyItemHrid
      );
      const targetCount = positiveNumber2(targetCost?.count);
      if (!targetCount) continue;
      const results = [];
      let rewardValue2 = 0;
      for (const reward of normalizeRewardRecords(detail)) {
        const itemHrid = reward?.itemHrid ?? reward?.hrid;
        if (!itemHrid) continue;
        const result = getAssetLiquidationValueInternal(
          itemHrid,
          reward.enhancementLevel ?? 0,
          mode,
          context
        );
        results.push(result);
        rewardValue2 += positiveNumber2(reward.count ?? 1) * result.value;
      }
      let otherCostValue = 0;
      for (const cost of costs) {
        const itemHrid = cost?.itemHrid ?? cost?.hrid;
        if (!itemHrid || itemHrid === currencyItemHrid) continue;
        const result = getAssetLiquidationValueInternal(
          itemHrid,
          0,
          mode,
          context
        );
        results.push(result);
        otherCostValue += positiveNumber2(cost.count) * result.value;
      }
      const value = Math.max(0, rewardValue2 - otherCostValue) / targetCount;
      if (value > bestValue) {
        bestValue = value;
        bestResults = results;
      }
    }
    return liquidationResult(
      bestValue,
      "conversion",
      mergeLiquidationMissing(bestResults)
    );
  }
  function getOpenableLiquidationValue(itemHrid, mode, context) {
    const drops = getDropRecords(itemHrid);
    if (!Array.isArray(drops) || !drops.length) {
      return liquidationResult(0, "missing", [itemHrid]);
    }
    let total = 0;
    const results = [];
    for (const drop of drops) {
      const dropItemHrid = drop?.itemHrid ?? drop?.hrid;
      if (!dropItemHrid) continue;
      const rawDropRate = Array.isArray(drop.dropRate) ? drop.dropRate[0] : drop.dropRate;
      const dropRate = Number.isFinite(Number(rawDropRate)) ? Math.max(0, Number(rawDropRate)) : 1;
      const minimum = Number(drop.minCount ?? drop.count ?? 1);
      const maximum = Number(drop.maxCount ?? drop.count ?? minimum);
      if (!Number.isFinite(minimum) || !Number.isFinite(maximum)) continue;
      const result = getAssetLiquidationValueInternal(
        dropItemHrid,
        drop.enhancementLevel ?? 0,
        mode,
        context
      );
      results.push(result);
      total += dropRate * (minimum + maximum) * 0.5 * result.value;
    }
    const keyItemHrid = getItemDetails(itemHrid)?.openKeyItemHrid;
    if (keyItemHrid) {
      const keyResult = getCraftedLiquidationValue(keyItemHrid, 0, mode, context);
      results.push(keyResult);
      if (!(keyResult.value > 0)) {
        return liquidationResult(0, "missing", [
          ...mergeLiquidationMissing(results),
          keyItemHrid
        ]);
      }
      total = Math.max(0, total - keyResult.value);
    }
    const entryKeyItemHrid = getDungeonEntryKeyItemHrid(itemHrid);
    if (entryKeyItemHrid) {
      const entryKeyResult = getAssetLiquidationValueInternal(
        entryKeyItemHrid,
        0,
        mode,
        context
      );
      results.push(entryKeyResult);
      if (!(entryKeyResult.value > 0)) {
        return liquidationResult(0, "missing", [
          ...mergeLiquidationMissing(results),
          entryKeyItemHrid
        ]);
      }
      total = Math.max(0, total - entryKeyResult.value);
    }
    if (!(total > 0) && results.every((result) => result.complete)) {
      return {
        value: 0,
        complete: true,
        source: "openable",
        missingItemHrids: []
      };
    }
    return liquidationResult(total, "openable", mergeLiquidationMissing(results));
  }
  function getAssetLiquidationValueInternal(itemHrid, enhancementLevel, mode, context) {
    if (!itemHrid) return liquidationResult(0, "missing");
    const normalizedMode = ["conservative", "fair", "aggressive"].includes(mode) ? mode : "fair";
    const level = Number(enhancementLevel) || 0;
    const cacheKey = `${normalizedMode}:${itemHrid}:${level}`;
    if (assetLiquidationCache.has(cacheKey)) {
      return assetLiquidationCache.get(cacheKey);
    }
    if (context.has(cacheKey)) {
      return liquidationResult(0, "missing", [itemHrid]);
    }
    const directValue = directLiquidationValue(itemHrid, level, normalizedMode);
    if (directValue > 0) {
      const result2 = liquidationResult(directValue, "market");
      assetLiquidationCache.set(cacheKey, result2);
      return result2;
    }
    context.add(cacheKey);
    let result;
    if (itemHrid === "/items/cowbell") {
      const bag = getAssetLiquidationValueInternal(
        "/items/bag_of_10_cowbells",
        0,
        normalizedMode,
        context
      );
      result = liquidationResult(
        bag.value / 10,
        "conversion",
        bag.missingItemHrids
      );
    } else if (getGuildCreditHrids().has(itemHrid)) {
      result = getGuildCreditLiquidationValue(itemHrid, normalizedMode, context);
    } else if (itemHrid === "/items/guild_token") {
      result = getGuildTokenLiquidationValue(normalizedMode, context);
    } else if (SHOP_CURRENCY_HRIDS.has(itemHrid)) {
      result = getShopCurrencyLiquidationValue(itemHrid, normalizedMode, context);
    } else {
      result = getOpenableLiquidationValue(itemHrid, normalizedMode, context);
    }
    context.delete(cacheKey);
    const keyedOpenable = Boolean(getItemDetails(itemHrid)?.openKeyItemHrid) && getDropRecords(itemHrid).length > 0;
    if (!(result.value > 0) && result.source === "missing" && !keyedOpenable) {
      const sellPrice = positiveNumber2(getItemDetails(itemHrid)?.sellPrice);
      result = sellPrice ? liquidationResult(sellPrice, "sell-price") : liquidationResult(0, "missing", [...result.missingItemHrids, itemHrid]);
    }
    assetLiquidationCache.set(cacheKey, result);
    return result;
  }
  function getAssetLiquidationValue(itemHrid, enhancementLevel = 0, mode = "fair") {
    return getAssetLiquidationValueInternal(
      itemHrid,
      enhancementLevel,
      mode,
      /* @__PURE__ */ new Set()
    );
  }
  function getGuildBuffLevel(guildBuffHrid, levels) {
    const record = Array.isArray(levels) ? levels.find(
      (value) => (value?.guildBuffHrid ?? value?.hrid) === guildBuffHrid
    ) : levels?.[guildBuffHrid];
    const level = Number(
      typeof record === "object" ? record?.level ?? record?.currentLevel : record
    );
    return Number.isSafeInteger(level) && level > 0 ? level : 0;
  }
  function getGuildShrineValues(guildBuffLevels2) {
    const usesCurrentCharacter = guildBuffLevels2 === void 0;
    if (usesCurrentCharacter && !runtime.state.guildDataLoaded) return null;
    const levels = usesCurrentCharacter ? runtime.state.guildBuffLevels : guildBuffLevels2;
    if (!levels || typeof levels !== "object") return null;
    const details = entriesOfMap(runtime.state.initData_guildBuffDetailMap);
    if (!details.length) return null;
    const values = { battle: 0, skilling: 0 };
    const valid = { battle: true, skilling: true };
    for (const [fallbackHrid, detail] of details) {
      const guildBuffHrid = detail?.guildBuffHrid ?? detail?.hrid ?? fallbackHrid;
      if (!guildBuffHrid) continue;
      const currentLevel = getGuildBuffLevel(guildBuffHrid, levels);
      if (!currentLevel) continue;
      if (typeof detail?.isCombat !== "boolean") return null;
      const group = detail.isCombat ? "battle" : "skilling";
      if (!valid[group]) continue;
      const levelCosts = detail.levelCosts;
      if (!levelCosts) {
        valid[group] = false;
        continue;
      }
      for (let level = 1; level <= currentLevel; level += 1) {
        const cost = levelCosts[level] ?? levelCosts[String(level)];
        if (!cost) {
          valid[group] = false;
          break;
        }
        const guildTokenCount = positiveNumber2(cost.guildTokenCost);
        if (guildTokenCount) {
          const tokenValue = getAssetValue("/items/guild_token", 0);
          if (!(tokenValue > 0)) {
            valid[group] = false;
            break;
          }
          values[group] += guildTokenCount * tokenValue;
        }
        for (const creditCost of cost.creditCosts ?? []) {
          const count = positiveNumber2(creditCost?.count);
          if (!count) continue;
          const creditValue = getAssetValue(creditCost.itemHrid, 0);
          if (!(creditValue > 0)) {
            valid[group] = false;
            break;
          }
          values[group] += count * creditValue;
        }
        if (!valid[group]) break;
      }
    }
    const battle = valid.battle ? values.battle : null;
    const skilling = valid.skilling ? values.skilling : null;
    return {
      battle,
      skilling,
      total: Number.isFinite(battle) && Number.isFinite(skilling) ? battle + skilling : null
    };
  }
  function getGuildShrineValue() {
    return getGuildShrineValues()?.total ?? null;
  }
  Object.assign(runtime.api, {
    getAssetValue,
    getAssetLiquidationValue,
    getGuildShrineValues,
    getGuildShrineValue,
    projectLootChest,
    isBackEquipment,
    isNonTradableTokenAsset,
    isOptionalTokenAsset,
    invalidateAssetValueCache,
    shouldIncludeCowbellsInAssets,
    shouldIncludeGuildDungeonTokensInAssets
  });
  function refreshConfiguredAssetValues() {
    invalidateAssetValueCache();
    runtime.api.scheduleNetworthRefresh?.();
    if (runtime.settings.settingsMap.assetHistory.isTrue) {
      runtime.api.scheduleAssetSnapshotRefresh?.(0);
    }
  }
  runtime.settings.onChange?.(
    "includeCowbellsInAssets",
    refreshConfiguredAssetValues
  );
  runtime.settings.onChange?.(
    "includeGuildDungeonTokensInAssets",
    refreshConfiguredAssetValues
  );
  runtime.settings.onChange?.(
    "valueBackEquipmentWithProtectionMirror",
    refreshConfiguredAssetValues
  );

  // src/core/message-state.js
  function applyClientData(payload) {
    runtime.state.initData_actionDetailMap = payload.actionDetailMap;
    runtime.state.initData_levelExperienceTable = payload.levelExperienceTable;
    runtime.state.initData_enhancementLevelSuccessRateTable = payload.enhancementLevelSuccessRateTable;
    runtime.state.initData_enhancementLevelTotalBonusMultiplierTable = payload.enhancementLevelTotalBonusMultiplierTable;
    runtime.state.initData_itemDetailMap = payload.itemDetailMap;
    runtime.state.initData_itemLocationDetailMap = payload.itemLocationDetailMap;
    runtime.state.initData_houseRoomDetailMap = payload.houseRoomDetailMap;
    runtime.state.initData_actionCategoryDetailMap = payload.actionCategoryDetailMap;
    runtime.state.initData_abilityDetailMap = payload.abilityDetailMap;
    runtime.state.initData_shopItemDetailMap = payload.shopItemDetailMap;
    runtime.state.initData_taskShopItemDetailMap = payload.taskShopItemDetailMap;
    runtime.state.initData_labyrinthShopItemDetailMap = payload.labyrinthShopItemDetailMap;
    runtime.state.initData_openableLootDropMap = payload.openableLootDropMap;
    runtime.state.initData_guildBuffDetailMap = payload.guildBuffDetailMap;
    runtime.api.invalidateAssetValueCache?.();
    for (const [key, value] of Object.entries(
      runtime.state.initData_itemDetailMap
    )) {
      runtime.state.itemEnNameToHridMap[value.name] = key;
    }
  }
  var CHARACTER_GUILD_BUFF_KEYS = [
    "characterGuildBuffMap",
    "characterGuildBuffDict",
    "characterGuildBuffs",
    "characterGuildBuffLevelMap",
    "characterGuildBuffLevelDict"
  ];
  var FALLBACK_GUILD_BUFF_KEYS = [
    "guildBuffLevelMap",
    "guildBuffLevelDict",
    "guildBuffLevels",
    "guildBuffMap",
    "guildBuffDict"
  ];
  var ACTION_TYPE_BUFF_SOURCE_KEYS = [
    "mooPassActionTypeBuffsMap",
    "communityActionTypeBuffsMap",
    "houseActionTypeBuffsMap",
    "guildActionTypeBuffsMap",
    "achievementActionTypeBuffsMap",
    "consumableActionTypeBuffsMap",
    "equipmentActionTypeBuffsMap",
    "personalActionTypeBuffsMap"
  ];
  function applyActionTypeBuffs(payload, reset = false) {
    const nextSources = reset ? {} : { ...runtime.state.actionTypeBuffSources ?? {} };
    let receivedSource = false;
    for (const key of ACTION_TYPE_BUFF_SOURCE_KEYS) {
      if (!Object.hasOwn(payload, key)) continue;
      nextSources[key] = payload[key] ?? {};
      receivedSource = true;
    }
    if (reset) {
      runtime.state.actionTypeBuffSources = receivedSource ? nextSources : null;
    } else if (receivedSource) {
      runtime.state.actionTypeBuffSources = nextSources;
    }
    if (Object.hasOwn(payload, "equipmentTaskActionBuffs")) {
      runtime.state.equipmentTaskActionBuffs = payload.equipmentTaskActionBuffs ?? [];
    } else if (reset) {
      runtime.state.equipmentTaskActionBuffs = [];
    }
    if (Object.hasOwn(payload, "actionTypeDrinkSlotsMap")) {
      runtime.state.initData_actionTypeDrinkSlotsMap = payload.actionTypeDrinkSlotsMap ?? {};
    }
  }
  function normalizeGuildBuffLevels(candidate) {
    if (!candidate || typeof candidate !== "object") return {};
    const entries = Array.isArray(candidate) ? candidate.map((record, index) => [
      record?.guildBuffHrid ?? record?.hrid ?? String(index),
      record
    ]) : Object.entries(candidate);
    return Object.fromEntries(entries.filter(([guildBuffHrid]) => guildBuffHrid));
  }
  function applyGuildData(payload, markLoaded = false) {
    const preferredCandidates = [];
    const fallbackCandidates = [];
    const pending = [{ value: payload, depth: 0 }];
    const visited = /* @__PURE__ */ new Set();
    let scanned = 0;
    while (pending.length && scanned < 400) {
      const { value, depth } = pending.pop();
      if (!value || typeof value !== "object" || visited.has(value) || depth > 6) {
        continue;
      }
      visited.add(value);
      scanned += 1;
      for (const key of CHARACTER_GUILD_BUFF_KEYS) {
        if (value[key] && typeof value[key] === "object")
          preferredCandidates.push(value[key]);
      }
      for (const key of FALLBACK_GUILD_BUFF_KEYS) {
        if (value[key] && typeof value[key] === "object")
          fallbackCandidates.push(value[key]);
      }
      for (const child of Object.values(value)) {
        if (child && typeof child === "object")
          pending.push({ value: child, depth: depth + 1 });
      }
    }
    const candidates = [...fallbackCandidates, ...preferredCandidates];
    if (candidates.length) {
      runtime.state.guildBuffLevels = candidates.reduce(
        (levels, candidate) => ({
          ...levels,
          ...normalizeGuildBuffLevels(candidate)
        }),
        runtime.state.guildBuffLevels ?? {}
      );
      runtime.state.guildDataLoaded = true;
    } else if (markLoaded) {
      runtime.state.guildDataLoaded = true;
    }
  }
  function applyCharacterData(payload) {
    runtime.state.currentCharacterId = payload.character?.id ?? payload.character?.characterID ?? payload.characterID ?? payload.characterSkills?.[0]?.characterID ?? "";
    runtime.state.currentCharacterName = payload.character?.name ?? payload.characterName ?? payload.sharableCharacter?.name ?? payload.combatUnit?.name ?? "";
    runtime.state.currentCharacterGameMode = payload.character?.gameMode ?? payload.combatUnit?.character?.gameMode ?? payload.sharableCharacter?.gameMode ?? "standard";
    runtime.state.labyrinthActive = Boolean(payload.labyrinth?.isActive);
    runtime.state.initData_characterSkills = payload.characterSkills;
    runtime.state.initData_characterItems = payload.characterItems ?? [];
    runtime.state.initData_characterHouseRoomMap = payload.characterHouseRoomMap;
    runtime.state.initData_actionTypeDrinkSlotsMap = payload.actionTypeDrinkSlotsMap;
    runtime.state.initData_characterAbilities = payload.characterAbilities;
    runtime.state.initData_myMarketListings = payload.myMarketListings ?? [];
    runtime.state.initData_combatAbilities = payload.combatUnit?.combatAbilities ?? [];
    runtime.state.currentActionsHridList = normalizeActionList(
      payload.characterActions ?? []
    );
    runtime.state.characterQuests = (payload.characterQuests ?? []).map(
      (quest, index) => ({ ...quest, _mwitoolsOriginalIndex: index })
    );
    applyActionTypeBuffs(payload, true);
    runtime.state.currentEquipmentMap = {};
    for (const item of payload.characterItems ?? []) {
      if (item.itemLocationHrid !== "/item_locations/inventory") {
        runtime.state.currentEquipmentMap[item.itemLocationHrid] = item;
      }
    }
    applyGuildData(payload);
    applyGuildSnapshot(payload);
    applyGuildCharacters(payload);
  }
  function applySkillsUpdated(payload) {
    const updates = payload.endCharacterSkills ?? payload.characterSkills ?? [];
    if (!Array.isArray(updates) || !updates.length) return;
    const skills = [...runtime.state.initData_characterSkills ?? []];
    for (const update of updates) {
      const index = skills.findIndex(
        (skill) => skill.skillHrid === update.skillHrid
      );
      if (index >= 0) skills[index] = { ...skills[index], ...update };
      else skills.push(update);
    }
    runtime.state.initData_characterSkills = skills;
  }
  function getQuestId(quest) {
    return quest?.id ?? quest?.characterQuestID ?? quest?.characterQuestId;
  }
  function applyQuestsUpdated(payload) {
    const updates = payload.endCharacterQuests ?? payload.characterQuests ?? [];
    if (!Array.isArray(updates)) return;
    const quests = [...runtime.state.characterQuests];
    for (const update of updates) {
      const id = getQuestId(update);
      const index = quests.findIndex((quest) => getQuestId(quest) === id);
      const removed = update.isClaimed || update.claimed || update.isDeleted || update.deleted || String(update.status ?? "").includes("claimed");
      if (removed) {
        if (index >= 0) quests.splice(index, 1);
        continue;
      }
      if (index >= 0) {
        quests[index] = {
          ...quests[index],
          ...update,
          _mwitoolsOriginalIndex: quests[index]._mwitoolsOriginalIndex
        };
      } else {
        quests.push({ ...update, _mwitoolsOriginalIndex: quests.length });
      }
    }
    runtime.state.characterQuests = quests;
  }
  function findArray(payload, keys) {
    for (const key of keys) {
      if (Array.isArray(payload?.[key])) return payload[key];
    }
    for (const value of Object.values(payload ?? {})) {
      if (!value || typeof value !== "object" || Array.isArray(value)) continue;
      for (const key of keys) {
        if (Array.isArray(value[key])) return value[key];
      }
    }
    return [];
  }
  function applyGuildSnapshot(payload) {
    const guild2 = payload.guild ?? payload.endGuild ?? payload.guildData;
    if (guild2) runtime.state.guild = { ...runtime.state.guild ?? {}, ...guild2 };
    runtime.state.guildStateUpdatedAt = Date.now();
  }
  function applyGuildCharacters(payload) {
    let characters = findArray(payload, [
      "guildCharacters",
      "endGuildCharacters",
      "characters",
      "members"
    ]);
    const characterMap = payload.guildCharacterMap ?? payload.endGuildCharacterMap ?? null;
    const sharableMap = payload.guildSharableCharacterMap ?? payload.endGuildSharableCharacterMap ?? {};
    if (!characters.length && characterMap && typeof characterMap === "object") {
      characters = Object.entries(characterMap).map(([id, character]) => ({
        ...sharableMap[id],
        ...character,
        characterID: character.characterID ?? Number(id)
      }));
    }
    if (characters.length) runtime.state.guildCharacters = characters;
    runtime.state.guildStateUpdatedAt = Date.now();
  }
  function applyLeaderboard(payload) {
    const category = String(
      payload.category ?? payload.leaderboardCategory ?? payload.typeHrid ?? ""
    ).toLowerCase();
    if (!category.includes("guild")) return;
    const rows2 = findArray(payload, [
      "leaderboard",
      "leaderboardEntries",
      "entries",
      "rankings",
      "guilds"
    ]);
    if (rows2.length) runtime.state.guildLeaderboard = rows2;
  }
  function normalizeActionList(actions) {
    const keyed = /* @__PURE__ */ new Map();
    const idless = [];
    for (const [index, action] of (Array.isArray(actions) ? actions : []).entries()) {
      if (!action) continue;
      const id = action.id;
      if (id === null || id === void 0) {
        idless.push({ action, index });
        continue;
      }
      const key = String(id);
      const previous = keyed.get(key);
      keyed.set(key, {
        action: previous ? { ...previous.action, ...action } : action,
        index: previous?.index ?? index
      });
    }
    return [...keyed.values(), ...idless].sort((left, right) => {
      const leftOrdinal = Number(left.action?.ordinal);
      const rightOrdinal = Number(right.action?.ordinal);
      const leftOrder = Number.isFinite(leftOrdinal) ? leftOrdinal : Number.MAX_SAFE_INTEGER;
      const rightOrder = Number.isFinite(rightOrdinal) ? rightOrdinal : Number.MAX_SAFE_INTEGER;
      return leftOrder - rightOrder || left.index - right.index;
    }).map(({ action }) => action);
  }
  function applyActionsUpdated(payload) {
    const updates = payload.endCharacterActions;
    if (!Array.isArray(updates)) return;
    let current = normalizeActionList(runtime.state.currentActionsHridList);
    for (const update of updates) {
      if (!update) continue;
      const id = update.id;
      if (id === null || id === void 0) {
        if (update.isDone !== true) current.push(update);
        continue;
      }
      const key = String(id);
      const index = current.findIndex(
        (action) => action?.id !== null && action?.id !== void 0 && String(action.id) === key
      );
      if (update.isDone === true) {
        if (index >= 0) current.splice(index, 1);
      } else if (index >= 0) {
        current[index] = { ...current[index], ...update };
      } else {
        current.push(update);
      }
    }
    runtime.state.currentActionsHridList = normalizeActionList(current);
  }
  function applyActionCompleted(payload) {
    const action = payload.endCharacterAction;
    if (!action || action.isDone === true) return;
    const currentAction = runtime.state.currentActionsHridList.find(
      ({ id }) => String(id) === String(action.id)
    );
    if (!currentAction) return;
    const currentCount = Number(
      action.currentCount ?? action.completedCount ?? action.progressCount
    );
    if (Number.isFinite(currentCount)) {
      currentAction.currentCount = currentCount;
    }
  }
  function applyItemsUpdated(payload) {
    if (!payload.endCharacterItems) return;
    for (const item of payload.endCharacterItems) {
      const existingIndex = runtime.state.initData_characterItems?.findIndex(
        (current) => item.id != null && current.id === item.id || item.id == null && current.itemHrid === item.itemHrid && current.itemLocationHrid === item.itemLocationHrid && current.enhancementLevel === item.enhancementLevel
      );
      if (existingIndex >= 0) {
        if (item.count === 0)
          runtime.state.initData_characterItems.splice(existingIndex, 1);
        else runtime.state.initData_characterItems[existingIndex] = item;
      } else if (item.count > 0) {
        runtime.state.initData_characterItems?.push(item);
      }
      if (item.itemLocationHrid === "/item_locations/inventory") continue;
      runtime.state.currentEquipmentMap[item.itemLocationHrid] = item.count === 0 ? null : item;
    }
  }
  function applyCharacterAbilitiesUpdated(payload) {
    const updates = payload.endCharacterAbilities ?? payload.characterAbilities ?? payload.abilities;
    if (!Array.isArray(updates)) return;
    const current = [...runtime.state.initData_characterAbilities ?? []];
    for (const update of updates) {
      const id = update.id ?? update.characterAbilityID ?? update.abilityHrid;
      const index = current.findIndex(
        (ability) => (ability.id ?? ability.characterAbilityID ?? ability.abilityHrid) === id
      );
      if (update.isDeleted || update.deleted) {
        if (index >= 0) current.splice(index, 1);
      } else if (index >= 0) current[index] = { ...current[index], ...update };
      else current.push(update);
    }
    runtime.state.initData_characterAbilities = current;
  }
  function applyGameMessage(payload) {
    switch (payload.type) {
      case "init_client_data":
        applyClientData(payload);
        break;
      case "init_character_data":
        applyCharacterData(payload);
        break;
      case "actions_updated":
        applyActionsUpdated(payload);
        break;
      case "action_completed":
        applyActionCompleted(payload);
        applyItemsUpdated(payload);
        applySkillsUpdated(payload);
        applyQuestsUpdated(payload);
        applyCharacterAbilitiesUpdated(payload);
        break;
      case "items_updated":
        applyItemsUpdated(payload);
        break;
      case "skills_updated":
        applySkillsUpdated(payload);
        break;
      case "quests_updated":
        applyQuestsUpdated(payload);
        break;
      case "market_item_values_updated":
        runtime.api.applyMarketItemValues(payload);
        break;
      case "market_item_order_books_updated":
        runtime.api.applyMarketOrderBooks(payload);
        break;
      case "market_listings_updated":
        runtime.api.applyMarketListings(payload);
        break;
      case "guild_updated":
        applyGuildData(payload, true);
        applyGuildSnapshot(payload);
        break;
      case "house_rooms_updated":
        runtime.state.initData_characterHouseRoomMap = payload.characterHouseRoomMap ?? runtime.state.initData_characterHouseRoomMap;
        applyActionTypeBuffs(payload);
        break;
      case "abilities_updated":
      case "character_abilities_updated":
        applyCharacterAbilitiesUpdated(payload);
        break;
      case "achievement_buffs_updated":
      case "moo_pass_buffs_updated":
      case "community_buffs_updated":
      case "consumable_buffs_updated":
      case "action_type_consumable_slots_updated":
      case "equipment_buffs_updated":
      case "personal_buffs_updated":
      case "guild_buffs_updated":
        applyActionTypeBuffs(payload);
        break;
      case "guild_characters_updated":
        applyGuildCharacters(payload);
        break;
      case "leaderboard_updated":
        applyLeaderboard(payload);
        break;
      case "labyrinth_updated":
        if (Object.hasOwn(payload, "labyrinth")) {
          runtime.state.labyrinthActive = Boolean(payload.labyrinth?.isActive);
        } else if (Object.hasOwn(payload, "isActive")) {
          runtime.state.labyrinthActive = Boolean(payload.isActive);
        }
        break;
    }
  }
  Object.assign(runtime.api, {
    applyGameMessage,
    applyGuildData,
    applyQuestsUpdated,
    applyGuildCharacters,
    applyLeaderboard,
    applyActionTypeBuffs,
    applySkillsUpdated
  });

  // src/core/messages.js
  var GAME_SOCKET_HOSTS = [
    "api.milkywayidle.com/ws",
    "api-test.milkywayidle.com/ws",
    "api.milkywayidlecn.com/ws",
    "api-test.milkywayidlecn.com/ws"
  ];
  function hookWS() {
    const dataProperty = Object.getOwnPropertyDescriptor(
      MessageEvent.prototype,
      "data"
    );
    const originalGet = dataProperty.get;
    dataProperty.get = function hookedGet() {
      const socket = this.currentTarget;
      if (!socket || typeof socket.send !== "function" && typeof socket.addEventListener !== "function" || !GAME_SOCKET_HOSTS.some((host) => String(socket.url ?? "").includes(host))) {
        return originalGet.call(this);
      }
      const message = originalGet.call(this);
      Object.defineProperty(this, "data", { value: message });
      return handleMessage(message);
    };
    Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
  }
  function handleMessage(message) {
    let payload;
    try {
      payload = JSON.parse(message);
    } catch {
      runtime.dispatchMessage({ type: "__non_json_message__" }, message);
      return message;
    }
    if (!payload?.type) return message;
    runtime.api.applyGameMessage(payload);
    if (payload.type === "init_character_data") {
      void runtime.features.handleCharacterData(payload);
    }
    runtime.dispatchMessage(payload, message);
    return message;
  }
  Object.assign(runtime.api, { hookWS, handleMessage });

  // src/features/build-score.js
  var SCORE_UNIT = 1e6;
  function hasStats(stats) {
    return Boolean(stats && Object.keys(stats).length);
  }
  function classifyEquippedItem(item) {
    const locationDetail = runtime.state.initData_itemLocationDetailMap?.[item.itemLocationHrid];
    const equipmentDetail = runtime.state.initData_itemDetailMap?.[item.itemHrid]?.equipmentDetail;
    const isTool = locationDetail?.isTool === true;
    return {
      isTool,
      isCombat: !isTool && (hasStats(equipmentDetail?.combatStats) || hasStats(equipmentDetail?.combatEnhancementBonuses)),
      isSkilling: isTool || hasStats(equipmentDetail?.noncombatStats) || hasStats(equipmentDetail?.noncombatEnhancementBonuses)
    };
  }
  function isCombatHouse(house) {
    return Boolean(
      runtime.state.initData_houseRoomDetailMap?.[house.houseRoomHrid]?.usableInActionTypeMap?.["/action_types/combat"]
    );
  }
  function isSkillingHouse(house) {
    const usableInActionTypeMap = runtime.state.initData_houseRoomDetailMap?.[house.houseRoomHrid]?.usableInActionTypeMap;
    return Object.entries(usableInActionTypeMap ?? {}).some(
      ([actionTypeHrid, isUsable]) => actionTypeHrid !== "/action_types/combat" && Boolean(isUsable)
    );
  }
  function createEmptyGearScores() {
    return {
      combatEquipment: 0,
      skillingTools: 0,
      skillingEquipment: 0
    };
  }
  function calculateGearScores(items) {
    const scores = createEmptyGearScores();
    for (const item of items ?? []) {
      if (item.itemLocationHrid === "/item_locations/inventory") continue;
      const classification = classifyEquippedItem(item);
      if (!classification.isTool && !classification.isCombat && !classification.isSkilling) {
        continue;
      }
      const fairValue = runtime.api.getAssetValue ? runtime.api.getAssetValue(item.itemHrid, item.enhancementLevel, {
        itemLocationHrid: item.itemLocationHrid
      }) : runtime.api.getFairValue(item.itemHrid, item.enhancementLevel);
      if (!(fairValue > 0)) {
        continue;
      }
      const value = Number(item.count ?? 1) * fairValue;
      if (classification.isCombat) scores.combatEquipment += value;
      if (classification.isTool) scores.skillingTools += value;
      else if (classification.isSkilling) scores.skillingEquipment += value;
    }
    for (const key of Object.keys(scores)) scores[key] /= SCORE_UNIT;
    return scores;
  }
  async function calculateHouseScores(characterHouseRoomMap) {
    let combat = 0;
    let skilling = 0;
    let all = 0;
    for (const house of Object.values(characterHouseRoomMap ?? {})) {
      const value = await getHouseFullBuildPrice(house) / SCORE_UNIT;
      all += value;
      if (isCombatHouse(house)) combat += value;
      if (isSkillingHouse(house)) skilling += value;
    }
    return { combat, skilling, all };
  }
  function createEmptyShrineScores() {
    return { battle: null, skilling: null };
  }
  function calculateGuildShrineScores(guildBuffLevels2) {
    if (typeof runtime.api.getGuildShrineValues !== "function") {
      return createEmptyShrineScores();
    }
    const values = guildBuffLevels2 === void 0 ? runtime.api.getGuildShrineValues() : runtime.api.getGuildShrineValues(guildBuffLevels2);
    return {
      battle: Number.isFinite(values?.battle) ? values.battle / SCORE_UNIT : null,
      skilling: Number.isFinite(values?.skilling) ? values.skilling / SCORE_UNIT : null
    };
  }
  function createScoreResult({
    houseScores,
    abilityScore,
    allAbilityScore,
    gearScores,
    shrineScores = createEmptyShrineScores(),
    equipmentHidden = false
  }) {
    const battle = {
      house: houseScores.combat,
      abilities: abilityScore,
      equipment: gearScores.combatEquipment,
      shrine: shrineScores.battle
    };
    battle.total = battle.house + battle.abilities + battle.equipment + (Number.isFinite(battle.shrine) ? battle.shrine : 0);
    const skilling = {
      house: houseScores.skilling,
      tools: gearScores.skillingTools,
      equipment: gearScores.skillingEquipment,
      shrine: shrineScores.skilling,
      available: !equipmentHidden
    };
    skilling.total = skilling.house + skilling.tools + skilling.equipment + (Number.isFinite(skilling.shrine) ? skilling.shrine : 0);
    return {
      battle,
      skilling,
      assets: {
        allHouses: houseScores.all,
        allAbilities: allAbilityScore
      },
      equipmentHidden
    };
  }
  async function getSelfBuildScores() {
    const houseScores = await calculateHouseScores(
      runtime.state.initData_characterHouseRoomMap
    );
    const gearScores = calculateGearScores(runtime.state.initData_characterItems);
    const guildBuffLevels2 = runtime.state.guildBuffLevels;
    const hasGuildBuffLevels = Array.isArray(guildBuffLevels2) ? guildBuffLevels2.length > 0 : Object.keys(guildBuffLevels2 ?? {}).length > 0;
    const shrineScores = runtime.state.guildDataLoaded && (Boolean(runtime.state.guild) || hasGuildBuffLevels) ? calculateGuildShrineScores() : createEmptyShrineScores();
    let abilityScore = 0;
    try {
      abilityScore = await calculateAbilityScore();
    } catch (error) {
      console.error(
        runtime.config.isZH ? "[MWITools] 计算已装备技能评分失败。" : "[MWITools] Failed to calculate equipped ability score.",
        error
      );
    }
    let allAbilityScore = 0;
    try {
      allAbilityScore = await calculateAbilityScore(true);
    } catch (error) {
      console.error(
        runtime.config.isZH ? "[MWITools] 计算全部技能评分失败。" : "[MWITools] Failed to calculate total ability score.",
        error
      );
    }
    return createScoreResult({
      houseScores,
      abilityScore,
      allAbilityScore,
      gearScores,
      shrineScores
    });
  }
  async function getHouseFullBuildPrice(house) {
    if (!await runtime.api.ensureMarketValueSource()) return 0;
    let houseDetail = runtime.state.initData_houseRoomDetailMap?.[house.houseRoomHrid];
    if (!houseDetail) {
      try {
        houseDetail = JSON.parse(GM_getValue("init_client_data", "{}")).houseRoomDetailMap?.[house.houseRoomHrid];
      } catch {
        return 0;
      }
    }
    const upgradeCostsMap = houseDetail?.upgradeCostsMap;
    if (!upgradeCostsMap) return 0;
    const level = house.level;
    let cost = 0;
    for (let i = 1; i <= level; i++) {
      for (const item of upgradeCostsMap[i] ?? []) {
        const fairValue = runtime.api.getFairValue(item.itemHrid, 0);
        if (fairValue > 0) {
          cost += item.count * fairValue;
        }
      }
    }
    return cost;
  }
  function getWeightedMarketPrice(marketPrices, ratio = 0.5) {
    let ask = marketPrices[0].a;
    let bid = marketPrices[0].b;
    if (ask > 0 && bid < 0) {
      bid = ask;
    }
    if (bid > 0 && ask < 0) {
      ask = bid;
    }
    const weightedPrice = ask * ratio + bid * (1 - ratio);
    return weightedPrice;
  }
  async function calculateAbilityScore(isAll = false) {
    const levelExperienceTable = runtime.state.initData_levelExperienceTable;
    const abilities = isAll ? runtime.state.initData_characterAbilities : runtime.state.initData_combatAbilities;
    if (!levelExperienceTable || !Array.isArray(abilities)) return 0;
    if (!await runtime.api.ensureMarketValueSource()) return 0;
    let exp_50_skill = [
      "poke",
      "scratch",
      "smack",
      "quick_shot",
      "water_strike",
      "fireball",
      "entangle",
      "minor_heal"
    ];
    const getNeedBooksToLevel = (targetLevel, abilityPerBookExp) => {
      const needExp = levelExperienceTable[targetLevel];
      if (!Number.isFinite(needExp)) return 0;
      let needBooks = needExp / abilityPerBookExp;
      needBooks += 1;
      return needBooks.toFixed(1);
    };
    let price = 0;
    abilities.forEach((item) => {
      let numBooks = 0;
      if (exp_50_skill.some((skill) => item.abilityHrid.includes(skill))) {
        numBooks = getNeedBooksToLevel(item.level, 50);
      } else {
        numBooks = getNeedBooksToLevel(item.level, 500);
      }
      const itemHrid = item.abilityHrid.replace("/abilities/", "/items/");
      const fairValue = runtime.api.getFairValue(itemHrid, 0);
      if (fairValue > 0) {
        price += numBooks * fairValue;
      }
    });
    return price /= 1e6;
  }
  function getInfoPanel() {
    const selectedElement = document.querySelector(
      `div.SharableProfile_overviewTab__W4dCV`
    );
    if (selectedElement) {
      return selectedElement;
    } else {
      return new Promise((resolve) => {
        setTimeout(() => resolve(getInfoPanel()), 500);
      });
    }
  }
  async function showBuildScoreOnProfile(profile_shared_obj) {
    const scores = await getBuildScoreByProfile(profile_shared_obj);
    const hiddenText = scores.equipmentHidden ? runtime.config.isZH ? "(装备隐藏)" : " (Equipment hidden)" : "";
    const hiddenValue = scores.equipmentHidden ? "-" : null;
    const optionalScore = (value) => Number.isFinite(value) ? runtime.api.formatScore(value) : "—";
    const panel = await getInfoPanel();
    panel.style.height = "auto";
    panel.querySelector("#script_profile_gear_scores")?.remove();
    panel.insertAdjacentHTML(
      "beforeend",
      `<section id="script_profile_gear_scores" style="display: block !important; width: fit-content !important; max-width: 100% !important; text-align: left !important; color: ${runtime.config.SCRIPT_COLOR_MAIN} !important; font-size: 0.8125rem !important; margin-top: 8px !important; border-left: 2px solid ${runtime.config.SCRIPT_COLOR_MAIN} !important; padding-left: 8px !important;">
            <p id="toggleScores_profile" style="display: block !important; margin: 4px 0 !important; padding: 4px 0 !important; cursor: pointer !important; font-weight: 650 !important; text-align: left !important; width: fit-content !important; float: none !important;">
                    <span class="mwi-profile-toggle-icon" style="display: inline !important; font-weight: bold !important; margin-right: 4px !important; float: none !important;">+</span>
                    <span style="display: inline !important; float: none !important;">${runtime.config.isZH ? "战斗着装评分:" : "Combat Gear Score: "}</span>
                    <span style="display: inline !important; font-weight: 700 !important; color: #ffffff !important; margin-left: 2px !important; float: none !important;">${runtime.api.formatScore(scores.battle.total)}${hiddenText}</span>
            </p>
            <section id="buildScores_profile" style="display: none; width: fit-content !important; text-align: left !important; margin-left: 18px !important; margin-top: 3px !important; margin-bottom: 6px !important; font-size: 0.75rem !important;">
                    <p style="display: block !important; margin: 2px 0 !important; padding: 3px 0 !important; text-align: left !important; width: fit-content !important; float: none !important;">
                            <span style="display: inline !important; color: var(--color-text-secondary, #9da6b2) !important; float: none !important;">${runtime.config.isZH ? "房屋:" : "House: "}</span>
                            <span style="display: inline !important; font-weight: 600 !important; color: #f3f5f7 !important; margin-left: 2px !important; float: none !important;">${runtime.api.formatScore(scores.battle.house)}</span>
                    </p>
                    <p style="display: block !important; margin: 2px 0 !important; padding: 3px 0 !important; text-align: left !important; width: fit-content !important; float: none !important;">
                            <span style="display: inline !important; color: var(--color-text-secondary, #9da6b2) !important; float: none !important;">${runtime.config.isZH ? "技能:" : "Abilities: "}</span>
                            <span style="display: inline !important; font-weight: 600 !important; color: #f3f5f7 !important; margin-left: 2px !important; float: none !important;">${hiddenValue ?? runtime.api.formatScore(scores.battle.abilities)}</span>
                    </p>
                    <p style="display: block !important; margin: 2px 0 !important; padding: 3px 0 !important; text-align: left !important; width: fit-content !important; float: none !important;">
                            <span style="display: inline !important; color: var(--color-text-secondary, #9da6b2) !important; float: none !important;">${runtime.config.isZH ? "装备:" : "Equipment: "}</span>
                            <span style="display: inline !important; font-weight: 600 !important; color: #f3f5f7 !important; margin-left: 2px !important; float: none !important;">${hiddenValue ?? runtime.api.formatScore(scores.battle.equipment)}</span>
                    </p>
                    <p style="display: block !important; margin: 2px 0 !important; padding: 3px 0 !important; text-align: left !important; width: fit-content !important; float: none !important;">
                            <span style="display: inline !important; color: var(--color-text-secondary, #9da6b2) !important; float: none !important;">${runtime.config.isZH ? "战斗神龛:" : "Combat shrine: "}</span>
                            <span style="display: inline !important; font-weight: 600 !important; color: #f3f5f7 !important; margin-left: 2px !important; float: none !important;">${optionalScore(scores.battle.shrine)}</span>
                    </p>
            </section>
            <p id="toggleSkillingScores_profile" style="display: block !important; margin: 4px 0 !important; padding: 4px 0 !important; cursor: pointer !important; font-weight: 650 !important; text-align: left !important; width: fit-content !important; float: none !important;">
                    <span class="mwi-profile-toggle-icon" style="display: inline !important; font-weight: bold !important; margin-right: 4px !important; float: none !important;">+</span>
                    <span style="display: inline !important; float: none !important;">${runtime.config.isZH ? "生活着装评分:" : "Skilling Gear Score: "}</span>
                    <span style="display: inline !important; font-weight: 700 !important; color: #ffffff !important; margin-left: 2px !important; float: none !important;">${hiddenValue ?? runtime.api.formatScore(scores.skilling.total)}${hiddenText}</span>
            </p>
            <section id="skillingScores_profile" style="display: none; width: fit-content !important; text-align: left !important; margin-left: 18px !important; margin-top: 3px !important; margin-bottom: 6px !important; font-size: 0.75rem !important;">
                    <p style="display: block !important; margin: 2px 0 !important; padding: 3px 0 !important; text-align: left !important; width: fit-content !important; float: none !important;">
                            <span style="display: inline !important; color: var(--color-text-secondary, #9da6b2) !important; float: none !important;">${runtime.config.isZH ? "房屋:" : "House: "}</span>
                            <span style="display: inline !important; font-weight: 600 !important; color: #f3f5f7 !important; margin-left: 2px !important; float: none !important;">${runtime.api.formatScore(scores.skilling.house)}</span>
                    </p>
                    <p style="display: block !important; margin: 2px 0 !important; padding: 3px 0 !important; text-align: left !important; width: fit-content !important; float: none !important;">
                            <span style="display: inline !important; color: var(--color-text-secondary, #9da6b2) !important; float: none !important;">${runtime.config.isZH ? "工具:" : "Tools: "}</span>
                            <span style="display: inline !important; font-weight: 600 !important; color: #f3f5f7 !important; margin-left: 2px !important; float: none !important;">${hiddenValue ?? runtime.api.formatScore(scores.skilling.tools)}</span>
                    </p>
                    <p style="display: block !important; margin: 2px 0 !important; padding: 3px 0 !important; text-align: left !important; width: fit-content !important; float: none !important;">
                            <span style="display: inline !important; color: var(--color-text-secondary, #9da6b2) !important; float: none !important;">${runtime.config.isZH ? "装备:" : "Equipment: "}</span>
                            <span style="display: inline !important; font-weight: 600 !important; color: #f3f5f7 !important; margin-left: 2px !important; float: none !important;">${hiddenValue ?? runtime.api.formatScore(scores.skilling.equipment)}</span>
                    </p>
                    <p style="display: block !important; margin: 2px 0 !important; padding: 3px 0 !important; text-align: left !important; width: fit-content !important; float: none !important;">
                            <span style="display: inline !important; color: var(--color-text-secondary, #9da6b2) !important; float: none !important;">${runtime.config.isZH ? "生活神龛:" : "Skilling shrine: "}</span>
                            <span style="display: inline !important; font-weight: 600 !important; color: #f3f5f7 !important; margin-left: 2px !important; float: none !important;">${optionalScore(scores.skilling.shrine)}</span>
                    </p>
            </section>
        </section>`
    );
    const bindToggle = (toggleId, detailsId) => {
      const toggle = document.getElementById(toggleId);
      const details = document.getElementById(detailsId);
      if (!toggle || !details) return;
      const icon = toggle.querySelector(".mwi-profile-toggle-icon");
      toggle.addEventListener("click", () => {
        const isCollapsed = details.style.display === "none";
        details.style.display = isCollapsed ? "block" : "none";
        if (icon) {
          icon.textContent = isCollapsed ? "↓" : "+";
        }
      });
    };
    bindToggle("toggleScores_profile", "buildScores_profile");
    bindToggle("toggleSkillingScores_profile", "skillingScores_profile");
  }
  async function getBuildScoreByProfile(profile_shared_obj) {
    const profile = profile_shared_obj.profile;
    const houseScores = await calculateHouseScores(profile.characterHouseRoomMap);
    const shrineScores = profile.guildId && profile.guildBuffLevelMap && typeof profile.guildBuffLevelMap === "object" ? calculateGuildShrineScores(profile.guildBuffLevelMap) : createEmptyShrineScores();
    const equipmentHidden = profile.hideWearableItems === true;
    const emptyGearScores = createEmptyGearScores();
    if (equipmentHidden) {
      return createScoreResult({
        houseScores,
        abilityScore: 0,
        allAbilityScore: 0,
        gearScores: emptyGearScores,
        shrineScores,
        equipmentHidden: true
      });
    }
    let abilityScore = 0;
    try {
      abilityScore = await calculateSkill(profile_shared_obj);
    } catch (error) {
      console.error(
        runtime.config.isZH ? "[MWITools] 计算资料技能评分失败。" : "[MWITools] Failed to calculate profile ability score.",
        error
      );
    }
    let gearScores = emptyGearScores;
    try {
      gearScores = await calculateEquipment(profile_shared_obj);
    } catch (error) {
      console.error(
        runtime.config.isZH ? "[MWITools] 计算资料装备评分失败。" : "[MWITools] Failed to calculate profile equipment score.",
        error
      );
    }
    return createScoreResult({
      houseScores,
      abilityScore,
      allAbilityScore: 0,
      gearScores,
      shrineScores
    });
  }
  async function calculateSkill(profile_shared_obj) {
    if (!await runtime.api.ensureMarketValueSource()) return 0;
    let obj = profile_shared_obj.profile;
    let exp_50_skill = [
      "poke",
      "scratch",
      "smack",
      "quick_shot",
      "water_strike",
      "fireball",
      "entangle",
      "minor_heal"
    ];
    const getNeedBooksToLevel = (targetLevel, abilityPerBookExp) => {
      const needExp = runtime.state.initData_levelExperienceTable[targetLevel];
      let needBooks = needExp / abilityPerBookExp;
      needBooks += 1;
      return needBooks.toFixed(1);
    };
    let price = 0;
    obj.equippedAbilities.forEach((item) => {
      let numBooks = 0;
      if (exp_50_skill.some((skill) => item.abilityHrid.includes(skill))) {
        numBooks = getNeedBooksToLevel(item.level, 50);
      } else {
        numBooks = getNeedBooksToLevel(item.level, 500);
      }
      const itemHrid = item.abilityHrid.replace("/abilities/", "/items/");
      const fairValue = runtime.api.getFairValue(itemHrid, 0);
      if (fairValue > 0) {
        price += numBooks * fairValue;
      }
    });
    return price /= 1e6;
  }
  async function calculateEquipment(profile_shared_obj) {
    if (!await runtime.api.ensureMarketValueSource())
      return createEmptyGearScores();
    return calculateGearScores(
      Object.values(profile_shared_obj.profile.wearableItemMap ?? {})
    );
  }
  Object.assign(runtime.api, {
    getSelfBuildScores,
    getHouseFullBuildPrice,
    getWeightedMarketPrice,
    classifyEquippedItem,
    isCombatHouse,
    isSkillingHouse,
    calculateGearScores,
    calculateHouseScores,
    calculateGuildShrineScores,
    calculateAbilityScore,
    getInfoPanel,
    showBuildScoreOnProfile,
    getBuildScoreByProfile,
    calculateSkill,
    calculateEquipment
  });

  // src/features/duplicate-script-warning.js
  var WARNING_ID = "mwitools-duplicate-script-warning";
  var MUTED_DUPLICATES_KEY = "MWITools_muted_duplicate_scripts_v1";
  var DUPLICATE_IDS = Object.freeze({
    "MWI 市场伴侣 / MWI Market Mate": "market-mate",
    "银河奶牛 DPS 统计 / Galaxy Cow DPS": "galaxy-cow-dps",
    "Everyday Profit Plus Fixed": "everyday-profit-plus",
    "MWI TaskManager": "mwi-task-manager"
  });
  var activeDuplicateWarningMonitor = null;
  var pageWindow = globalThis.unsafeWindow ?? globalThis.window ?? globalThis;
  var dpsWasPresentAtLoad = Boolean(pageWindow.__MWI_DPS);
  function detectDuplicateScripts(options = {}) {
    const target = options.pageWindow ?? pageWindow;
    const documentRef = options.documentRef ?? globalThis.document;
    const duplicates = [];
    const taskInsightsEnabled = options.taskInsightsEnabled ?? runtime.settings.get?.("taskInsights") ?? true;
    if (target.MWIMM || documentRef?.getElementById("mwi-mm2-host")) {
      duplicates.push("MWI 市场伴侣 / MWI Market Mate");
    }
    if (options.dpsWasPresent ?? dpsWasPresentAtLoad) {
      duplicates.push("银河奶牛 DPS 统计 / Galaxy Cow DPS");
    }
    if (target.kbd_calculateTotalNetworth || target.__everyday_profit_plus_interval__ || documentRef?.querySelector(
      ".deltaNetworthDiv,#deltaNetworthChartModal,#refreshNetworthIcon,.epPrecisionHintActions,#everyday-profit-chartjs,#everyday-profit-zoom,#everyday-profit-crosshair"
    )) {
      duplicates.push("Everyday Profit Plus Fixed");
    }
    if (taskInsightsEnabled && documentRef?.querySelector("#TaskSort") && documentRef?.querySelector(
      "#taskChekerInCoin,#ActionIcon,#BattleIcon,#DungeonIcon"
    )) {
      duplicates.push("MWI TaskManager");
    }
    return duplicates;
  }
  function duplicateScriptId(name) {
    return DUPLICATE_IDS[name] ?? String(name ?? "").trim().toLowerCase();
  }
  function readMutedDuplicateScriptIds(storage = globalThis.localStorage) {
    try {
      const value = JSON.parse(storage?.getItem(MUTED_DUPLICATES_KEY) || "[]");
      return new Set(Array.isArray(value) ? value.map(String) : []);
    } catch {
      return /* @__PURE__ */ new Set();
    }
  }
  function writeMutedDuplicateScriptIds(ids, storage = globalThis.localStorage) {
    const value = [...new Set(ids ?? [])].map(String).filter(Boolean).sort();
    storage?.setItem(MUTED_DUPLICATES_KEY, JSON.stringify(value));
    return value;
  }
  function clearMutedDuplicateScriptIds(storage = globalThis.localStorage) {
    storage?.removeItem(MUTED_DUPLICATES_KEY);
    activeDuplicateWarningMonitor?.schedule();
  }
  function showDuplicateWarning(duplicates, {
    documentRef = globalThis.document,
    isZH: isZH3 = runtime.config.isZH,
    onDismiss = null,
    onMute = null
  } = {}) {
    if (!duplicates.length || !documentRef?.body) return null;
    let warning = documentRef.getElementById(WARNING_ID);
    if (!warning) {
      warning = documentRef.createElement("aside");
      warning.id = WARNING_ID;
      warning.setAttribute("role", "status");
      warning.setAttribute("aria-live", "polite");
      warning.style.cssText = "position:fixed;z-index:2147483646;top:14px;right:14px;width:min(380px,calc(100vw - 28px));box-sizing:border-box;padding:12px 38px 12px 14px;border:1px solid rgba(245,158,11,.6);border-radius:9px;background:rgba(24,27,35,.97);box-shadow:0 8px 28px rgba(0,0,0,.42);color:#f5f5f5;font:13px/1.55 system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif";
      const close = documentRef.createElement("button");
      close.type = "button";
      close.textContent = "×";
      close.setAttribute("aria-label", isZH3 ? "关闭提醒" : "Close warning");
      close.style.cssText = "position:absolute;top:5px;right:7px;width:26px;height:26px;border:0;background:transparent;color:#bbb;font:20px/26px sans-serif;cursor:pointer";
      close.addEventListener("click", () => {
        warning._mwitoolsOnDismiss?.();
        warning.remove();
      });
      const content2 = documentRef.createElement("div");
      const message2 = documentRef.createElement("div");
      const mute2 = documentRef.createElement("button");
      mute2.type = "button";
      mute2.dataset.mwitoolsDuplicateMute = "";
      mute2.style.cssText = "margin-top:8px;border:1px solid rgba(245,158,11,.55);border-radius:5px;padding:4px 9px;background:rgba(245,158,11,.12);color:#ffd58a;font:inherit;cursor:pointer";
      mute2.addEventListener("click", () => {
        warning._mwitoolsOnMute?.();
        warning.remove();
      });
      content2.append(message2, mute2);
      warning.append(close, content2);
      documentRef.body.append(warning);
    }
    warning._mwitoolsOnDismiss = onDismiss;
    warning._mwitoolsOnMute = onMute;
    const content = warning.lastElementChild;
    const names = duplicates.join(isZH3 ? "、" : ", ");
    const message = isZH3 ? `检测到与新版 MWITools 功能重复的脚本:${names}。为避免重复监听、面板冲突和重复计算,建议在脚本管理器中停用或删除。` : `Scripts overlapping with the new MWITools were detected: ${names}. Disable or remove them in your userscript manager to avoid duplicate listeners, panels, and calculations.`;
    const messageNode = content.firstElementChild;
    if (messageNode.textContent !== message) messageNode.textContent = message;
    const mute = content.querySelector("[data-mwitools-duplicate-mute]");
    mute.textContent = isZH3 ? "不再提示这些脚本" : "Don't remind me again";
    return warning;
  }
  function duplicateSignature(duplicates) {
    return [...new Set(duplicates)].sort().join("\0");
  }
  function createDuplicateWarningMonitor(options = {}) {
    const documentRef = options.documentRef ?? globalThis.document;
    const detect = options.detect ?? (() => detectDuplicateScripts(options));
    const render = options.render ?? showDuplicateWarning;
    const scheduleTask = options.scheduleTask ?? globalThis.queueMicrotask;
    const setIntervalRef = options.setIntervalRef ?? globalThis.setInterval;
    const clearIntervalRef = options.clearIntervalRef ?? globalThis.clearInterval;
    const Observer = options.MutationObserverRef ?? globalThis.MutationObserver;
    const intervalMs = options.intervalMs ?? 1e4;
    const detected = /* @__PURE__ */ new Set();
    const usesStoredMuted = !options.muted;
    const muted = options.muted ?? readMutedDuplicateScriptIds(options.storage);
    const isDuplicateEnabled = options.isDuplicateEnabled ?? ((name) => duplicateScriptId(name) !== "mwi-task-manager" || (runtime.settings.get?.("taskInsights") ?? true));
    let lastSignature = "";
    let dismissed = false;
    let pending = false;
    let destroyed = false;
    const scan2 = () => {
      pending = false;
      if (destroyed || dismissed) return;
      if (usesStoredMuted) {
        muted.clear();
        for (const id of readMutedDuplicateScriptIds(options.storage)) {
          muted.add(id);
        }
      }
      const current = detect();
      if (!current.some((name) => duplicateScriptId(name) === "mwi-task-manager")) {
        for (const name of detected) {
          if (duplicateScriptId(name) === "mwi-task-manager")
            detected.delete(name);
        }
      }
      for (const name of current) detected.add(name);
      const duplicates = [...detected].filter((name) => isDuplicateEnabled(name)).filter((name) => !muted.has(duplicateScriptId(name))).sort();
      if (!duplicates.length) {
        documentRef?.getElementById(WARNING_ID)?.remove();
        lastSignature = "";
        return;
      }
      const signature = duplicateSignature(duplicates);
      if (signature === lastSignature) return;
      lastSignature = signature;
      render(duplicates, {
        documentRef,
        isZH: options.isZH ?? runtime.config.isZH,
        onDismiss() {
          dismissed = true;
        },
        onMute() {
          for (const name of duplicates) muted.add(duplicateScriptId(name));
          writeMutedDuplicateScriptIds(muted, options.storage);
          lastSignature = "";
        }
      });
    };
    const schedule = () => {
      if (destroyed || dismissed || pending) return;
      pending = true;
      scheduleTask(() => {
        if (destroyed) return;
        scan2();
      });
    };
    scan2();
    const intervalId = setIntervalRef?.(schedule, intervalMs);
    const observer = typeof Observer === "function" ? new Observer((records) => {
      const warning = documentRef?.getElementById(WARNING_ID);
      if (warning && records?.length && records.every(
        (record) => record.target === warning || warning.contains(record.target)
      )) {
        return;
      }
      schedule();
    }) : null;
    observer?.observe(documentRef.body, { childList: true, subtree: true });
    return {
      scan: scan2,
      schedule,
      destroy() {
        if (destroyed) return;
        destroyed = true;
        pending = false;
        observer?.disconnect();
        if (intervalId !== void 0) clearIntervalRef?.(intervalId);
        documentRef?.getElementById(WARNING_ID)?.remove();
      }
    };
  }
  runtime.features.register({
    id: "duplicateScriptWarning",
    initialize({ scope }) {
      const monitor = createDuplicateWarningMonitor();
      activeDuplicateWarningMonitor = monitor;
      scope.add(() => {
        monitor.destroy();
        if (activeDuplicateWarningMonitor === monitor) {
          activeDuplicateWarningMonitor = null;
        }
      });
      scope.add(
        runtime.settings.onChange?.("taskInsights", () => monitor.schedule())
      );
    }
  });
  Object.assign(runtime.api, {
    clearMutedDuplicateScriptIds,
    getMutedDuplicateScriptIds: () => [...readMutedDuplicateScriptIds()]
  });

  // src/features/mobile-viewport-fix.js
  var STYLE_ID = "mwitools-mobile-viewport-style";
  var ACTIVE_ATTRIBUTE = "data-mwitools-mobile-viewport";
  var HEIGHT_PROPERTY = "--mwitools-visual-viewport-height";
  function resolveVisualViewportHeight(windowRef = globalThis.window ?? globalThis, documentRef = globalThis.document) {
    const candidates = [
      windowRef?.visualViewport?.height,
      windowRef?.innerHeight,
      documentRef?.documentElement?.clientHeight
    ];
    for (const candidate of candidates) {
      const height = Number(candidate);
      if (Number.isFinite(height) && height > 0) return height;
    }
    return 0;
  }
  function syncMobileViewportHeight(windowRef = globalThis.window ?? globalThis, documentRef = globalThis.document) {
    const root = documentRef?.documentElement;
    const height = resolveVisualViewportHeight(windowRef, documentRef);
    if (!root || !height) return false;
    root.setAttribute(ACTIVE_ATTRIBUTE, "true");
    root.style.setProperty(
      HEIGHT_PROPERTY,
      `${Math.round(height * 100) / 100}px`
    );
    return true;
  }
  function addStyles(documentRef = document) {
    if (documentRef.getElementById(STYLE_ID)) return;
    const style = documentRef.createElement("style");
    style.id = STYLE_ID;
    style.textContent = `
    @media (max-width:760px), (any-pointer:coarse) {
      html[${ACTIVE_ATTRIBUTE}="true"],
      html[${ACTIVE_ATTRIBUTE}="true"] body,
      html[${ACTIVE_ATTRIBUTE}="true"] #root {
        height:var(${HEIGHT_PROPERTY},100dvh)!important;
      }
      html[${ACTIVE_ATTRIBUTE}="true"] body {
        background-color:var(--color-background-game,#131419)!important;
      }
    }
  `;
    (documentRef.head ?? documentRef.documentElement).append(style);
  }
  function createViewportScheduler(windowRef, documentRef) {
    let frame = null;
    const run = () => {
      frame = null;
      syncMobileViewportHeight(windowRef, documentRef);
    };
    const schedule = () => {
      if (frame !== null) return;
      if (typeof windowRef.requestAnimationFrame === "function") {
        frame = windowRef.requestAnimationFrame(run);
      } else {
        frame = windowRef.setTimeout(run, 0);
      }
    };
    const cancel = () => {
      if (frame === null) return;
      if (typeof windowRef.cancelAnimationFrame === "function") {
        windowRef.cancelAnimationFrame(frame);
      } else {
        windowRef.clearTimeout(frame);
      }
      frame = null;
    };
    return { schedule, cancel };
  }
  runtime.features.register({
    id: "mobileViewportFix",
    scope: "global",
    initialize({ scope }) {
      addStyles();
      const scheduler = createViewportScheduler(window, document);
      const schedule = scheduler.schedule;
      syncMobileViewportHeight(window, document);
      scope.event(window, "resize", schedule, { passive: true });
      scope.event(window, "orientationchange", schedule, { passive: true });
      scope.event(window, "pageshow", schedule, { passive: true });
      scope.event(window.visualViewport, "resize", schedule, { passive: true });
      scope.event(document, "visibilitychange", () => {
        if (document.visibilityState === "visible") schedule();
      });
      scope.add(() => {
        scheduler.cancel();
        const root = document.documentElement;
        root.removeAttribute(ACTIVE_ATTRIBUTE);
        root.style.removeProperty(HEIGHT_PROPERTY);
        document.getElementById(STYLE_ID)?.remove();
      });
    }
  });

  // src/features/asset-history/00-snapshot.js
  var ASSET_COMPONENT_KEYS = [
    "equipment",
    "inventory",
    "marketListings",
    "houses",
    "abilities",
    "nonTradableTokens",
    "shrine"
  ];
  var snapshotListeners = /* @__PURE__ */ new Set();
  var latestSnapshot = null;
  var refreshPromise = null;
  var refreshTimer = null;
  function finiteOrNull(value) {
    if (value === null || value === void 0 || value === "") return null;
    const number3 = Number(value);
    return Number.isFinite(number3) ? number3 : null;
  }
  function sumKnown(values) {
    return values.reduce(
      (total, value) => total + (Number.isFinite(value) ? value : 0),
      0
    );
  }
  function isTerminalMarketListing(listing) {
    if (listing?.isDone || listing?.isCancelled || listing?.isCanceled || listing?.isExpired) {
      return true;
    }
    return /(cancel|complete|expire|closed|done)/i.test(
      String(listing?.status ?? "")
    );
  }
  function calculateMarketListingValues(listings) {
    const totals = { fair: 0, ask: 0, bid: 0 };
    for (const listing of listings ?? []) {
      const enhancementLevel = listing.enhancementLevel ?? 0;
      const directMarketValue = Number(
        runtime.api.getFairValue?.(listing.itemHrid, enhancementLevel)
      );
      const assetValue = Number.isFinite(directMarketValue) && directMarketValue > 0 ? directMarketValue : runtime.api.getAssetValue(listing.itemHrid, enhancementLevel);
      const askPrice = runtime.api.getAskPrice(
        listing.itemHrid,
        enhancementLevel
      );
      const bidPrice = runtime.api.getBidPrice(
        listing.itemHrid,
        enhancementLevel
      );
      const availableCoins = Math.max(0, Number(listing.coinsAvailable ?? 0));
      const unclaimedCoins = Math.max(0, Number(listing.unclaimedCoinCount ?? 0));
      const explicitCoins = availableCoins + unclaimedCoins;
      totals.fair += explicitCoins;
      totals.ask += explicitCoins;
      totals.bid += explicitCoins;
      const unclaimedItems = Math.max(0, Number(listing.unclaimedItemCount ?? 0));
      totals.fair += unclaimedItems * assetValue;
      totals.ask += unclaimedItems * askPrice;
      totals.bid += unclaimedItems * bidPrice;
      if (!listing.isSell || isTerminalMarketListing(listing)) continue;
      const remainingQuantity = Math.max(
        0,
        Number(listing.orderQuantity ?? 0) - Number(listing.filledQuantity ?? 0)
      );
      const taxMultiplier = 1 - runtime.api.getMarketTaxRate(listing.itemHrid);
      totals.fair += remainingQuantity * assetValue * taxMultiplier;
      totals.ask += remainingQuantity * askPrice * taxMultiplier;
      totals.bid += remainingQuantity * bidPrice * taxMultiplier;
    }
    return totals;
  }
  async function getAssetSnapshot() {
    if (!Array.isArray(runtime.state.initData_characterItems)) return null;
    if (!await runtime.api.ensureMarketValueSource()) return null;
    let equipment = 0;
    let inventory = 0;
    let nonTradableTokens = 0;
    let equipmentAsk = 0;
    let equipmentBid = 0;
    let inventoryAsk = 0;
    let inventoryBid = 0;
    for (const item of runtime.state.initData_characterItems) {
      if (item.itemHrid === "/items/cowbell" && !runtime.api.shouldIncludeCowbellsInAssets()) {
        continue;
      }
      if (runtime.api.isOptionalTokenAsset?.(item.itemHrid) && !runtime.api.shouldIncludeGuildDungeonTokensInAssets?.()) {
        continue;
      }
      const count = Math.max(0, Number(item.count ?? 0));
      const enhancementLevel = item.enhancementLevel ?? 0;
      const fairValue = runtime.api.getAssetValue(
        item.itemHrid,
        enhancementLevel,
        { itemLocationHrid: item.itemLocationHrid }
      );
      const askPrice = runtime.api.getAskPrice(item.itemHrid, enhancementLevel);
      const bidPrice = runtime.api.getBidPrice(item.itemHrid, enhancementLevel);
      if (item.itemLocationHrid !== "/item_locations/inventory") {
        equipment += count * fairValue;
        equipmentAsk += count * askPrice;
        equipmentBid += count * bidPrice;
      } else if (runtime.api.isNonTradableTokenAsset(item.itemHrid)) {
        nonTradableTokens += count * fairValue;
      } else {
        inventory += count * fairValue;
        inventoryAsk += count * askPrice;
        inventoryBid += count * bidPrice;
      }
    }
    const listingValues = calculateMarketListingValues(
      runtime.state.initData_myMarketListings
    );
    const scores = await runtime.api.getSelfBuildScores();
    const shrine = finiteOrNull(runtime.api.getGuildShrineValue());
    const values = {
      equipment,
      inventory,
      marketListings: listingValues.fair,
      houses: finiteOrNull(scores?.assets?.allHouses * 1e6),
      abilities: finiteOrNull(scores?.assets?.allAbilities * 1e6),
      nonTradableTokens,
      shrine
    };
    values.liquid = sumKnown([
      values.equipment,
      values.inventory,
      values.marketListings
    ]);
    values.fixed = sumKnown([
      values.houses,
      values.abilities,
      values.nonTradableTokens,
      values.shrine
    ]);
    values.total = values.liquid + values.fixed;
    return {
      schema: 1,
      recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
      server: runtime.api.getMarketEnvironment?.() ?? "production",
      characterId: String(runtime.state.currentCharacterId ?? ""),
      complete: ASSET_COMPONENT_KEYS.every((key) => Number.isFinite(values[key])),
      values,
      liquidation: {
        ask: equipmentAsk + inventoryAsk + listingValues.ask,
        bid: equipmentBid + inventoryBid + listingValues.bid
      },
      scores
    };
  }
  function getLatestAssetSnapshot() {
    return latestSnapshot;
  }
  function onAssetSnapshot(listener) {
    snapshotListeners.add(listener);
    return () => snapshotListeners.delete(listener);
  }
  async function refreshAssetSnapshot() {
    if (refreshPromise) return refreshPromise;
    refreshPromise = getAssetSnapshot().then((snapshot) => {
      if (!snapshot) return null;
      latestSnapshot = snapshot;
      for (const listener of snapshotListeners) {
        try {
          listener(snapshot);
        } catch (error) {
          console.error(
            runtime.config.isZH ? "[MWITools] 资产快照监听器执行失败。" : "[MWITools] Asset snapshot listener failed.",
            error
          );
        }
      }
      return snapshot;
    }).finally(() => {
      refreshPromise = null;
    });
    return refreshPromise;
  }
  function scheduleAssetSnapshotRefresh(delay = 120) {
    clearTimeout(refreshTimer);
    refreshTimer = setTimeout(() => void refreshAssetSnapshot(), delay);
  }
  Object.assign(runtime.api, {
    ASSET_COMPONENT_KEYS,
    calculateMarketListingValues,
    getAssetSnapshot,
    getLatestAssetSnapshot,
    onAssetSnapshot,
    refreshAssetSnapshot,
    scheduleAssetSnapshotRefresh
  });

  // src/features/asset-history/10-store.js
  var ASSET_HISTORY_STORAGE_KEY = "MWITools_asset_history_v1";
  var ASSET_HISTORY_BACKUP_MARKER = "__mwitools_asset_history_backup__";
  var LEGACY_KEYS = {
    total: "kbd_calc_data",
    breakdown: "kbd_calc_breakdown_data",
    tags: "kbd_calc_tags",
    tagPrefs: "kbd_calc_tag_prefs",
    tagPanel: "kbd_calc_tag_panel",
    dataPanel: "kbd_calc_data_panel",
    lastUpdate: "kbd_calc_last_update_at",
    goalTarget: "ep_goal_target",
    achievements: "ep_achievements_data",
    language: "ep_lang",
    tagColors: "ep_tag_colors",
    windowSize: "ep_window_size",
    chartSettings: "ep_chart_settings",
    heatmapStyle: "ep_heatmap_style",
    glassHeartMode: "ep_glass_heart_mode",
    themeMode: "ep_theme_mode",
    lightBg: "ep_light_bg"
  };
  var ASSET_HISTORY_SCHEMA_VERSION = 2;
  var DEFAULT_ASSET_HISTORY_PREFERENCES = Object.freeze({
    language: null,
    themeMode: "dark",
    lightBg: { h: 38, s: 44 },
    glassHeartMode: false,
    windowSize: null,
    heatmapStyle: "B",
    tagColors: {},
    chart: {
      defaultView: "networth",
      defaultRange: 30,
      maWindow: 7,
      lineTension: 0.25
    }
  });
  function safeParse(raw, fallback) {
    try {
      return raw ? JSON.parse(raw) : fallback;
    } catch {
      return fallback;
    }
  }
  function createEmptyData() {
    return {
      version: ASSET_HISTORY_SCHEMA_VERSION,
      roles: {},
      migrations: { everydayProfit: {} },
      legacy: {},
      preferences: structuredClonePreferences()
    };
  }
  function structuredClonePreferences(value = DEFAULT_ASSET_HISTORY_PREFERENCES) {
    return JSON.parse(JSON.stringify(value));
  }
  function normalizePreferences(input = {}) {
    const defaults = structuredClonePreferences();
    const chart = { ...defaults.chart, ...input.chart ?? {} };
    const range = chart.defaultRange;
    chart.defaultView = [
      "networth",
      "profit",
      "breakdown",
      "statsReport"
    ].includes(chart.defaultView) ? chart.defaultView : defaults.chart.defaultView;
    chart.defaultRange = [7, 15, 30, null].includes(range) ? range : 30;
    chart.maWindow = Math.min(90, Math.max(2, Number(chart.maWindow) || 7));
    chart.lineTension = [0, 0.15, 0.25, 0.4].includes(Number(chart.lineTension)) ? Number(chart.lineTension) : 0.25;
    const lightBg = { ...defaults.lightBg, ...input.lightBg ?? {} };
    lightBg.h = Math.min(360, Math.max(0, Number(lightBg.h) || 0));
    lightBg.s = Math.min(60, Math.max(0, Number(lightBg.s) || 0));
    return {
      ...defaults,
      ...input,
      language: ["zh", "en"].includes(input.language) ? input.language : null,
      themeMode: input.themeMode === "light" ? "light" : "dark",
      lightBg,
      glassHeartMode: Boolean(input.glassHeartMode),
      windowSize: Number(input.windowSize?.w) > 0 && Number(input.windowSize?.h) > 0 ? { w: Number(input.windowSize.w), h: Number(input.windowSize.h) } : null,
      heatmapStyle: ["A", "B", "C", "D"].includes(input.heatmapStyle) ? input.heatmapStyle : "B",
      tagColors: input.tagColors && typeof input.tagColors === "object" ? { ...input.tagColors } : {},
      chart
    };
  }
  function normalizeRole(role = {}) {
    return {
      ...role,
      days: role.days && typeof role.days === "object" ? role.days : {},
      tags: role.tags && typeof role.tags === "object" ? role.tags : {},
      achievements: role.achievements && typeof role.achievements === "object" ? role.achievements : {},
      goalTarget: Number.isFinite(Number(role.goalTarget)) && Number(role.goalTarget) > 0 ? Number(role.goalTarget) : null,
      tagVisibility: role.tagVisibility !== false,
      lastUpdate: role.lastUpdate ?? null
    };
  }
  function migrateStoredData(loaded) {
    if (!loaded || typeof loaded !== "object") return createEmptyData();
    if (loaded.version !== 1 && loaded.version !== ASSET_HISTORY_SCHEMA_VERSION) {
      return createEmptyData();
    }
    return {
      ...loaded,
      version: ASSET_HISTORY_SCHEMA_VERSION,
      roles: Object.fromEntries(
        Object.entries(loaded.roles ?? {}).map(([key, role]) => [
          key,
          normalizeRole(role)
        ])
      ),
      migrations: {
        ...loaded.migrations ?? {},
        everydayProfit: loaded.migrations?.everydayProfit ?? {}
      },
      legacy: loaded.legacy ?? {},
      preferences: normalizePreferences(loaded.preferences)
    };
  }
  function finiteOrNull2(value) {
    if (value === null || value === void 0 || value === "") return null;
    const number3 = Number(value);
    return Number.isFinite(number3) ? number3 : null;
  }
  function sumIfComplete(values, keys) {
    const parts = keys.map((key) => finiteOrNull2(values[key]));
    return parts.every(Number.isFinite) ? parts.reduce((total, value) => total + value, 0) : null;
  }
  function normalizeAssetValues(input = {}, options = {}) {
    const values = Object.fromEntries(
      ASSET_COMPONENT_KEYS.map((key) => [key, finiteOrNull2(input[key])])
    );
    values.liquid = sumIfComplete(values, [
      "equipment",
      "inventory",
      "marketListings"
    ]);
    values.fixed = sumIfComplete(values, [
      "houses",
      "abilities",
      "nonTradableTokens",
      "shrine"
    ]);
    values.total = Number.isFinite(values.liquid) && Number.isFinite(values.fixed) ? values.liquid + values.fixed : options.preserveTotal ? finiteOrNull2(input.total) : null;
    return values;
  }
  function getUtc8DayKey(date = /* @__PURE__ */ new Date()) {
    return new Date(date.getTime() + 8 * 36e5).toISOString().slice(0, 10);
  }
  function parseDayKey(dayKey) {
    const [year, month, day] = String(dayKey).split("-").map(Number);
    return Date.UTC(year, month - 1, day);
  }
  function isValidDayKey(dayKey) {
    const value = String(dayKey ?? "");
    if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
    const timestamp = parseDayKey(value);
    return Number.isFinite(timestamp) && new Date(timestamp).toISOString().slice(0, 10) === value;
  }
  function dayGap(left, right) {
    return Math.round((parseDayKey(right) - parseDayKey(left)) / 864e5);
  }
  function roleBucketFromLegacy(data, roleName, characterId) {
    if (!data || typeof data !== "object") return {};
    if (roleName && data[roleName]) return data[roleName];
    if (characterId && data[characterId]) return data[characterId];
    const roles = Object.keys(data);
    return roles.length === 1 ? data[roles[0]] : {};
  }
  function legacyValues(total, breakdown = {}) {
    return normalizeAssetValues(
      {
        equipment: breakdown.equip,
        inventory: breakdown.inventory,
        marketListings: breakdown.orders,
        houses: breakdown.house,
        abilities: breakdown.skill,
        nonTradableTokens: null,
        shrine: null,
        total
      },
      { preserveTotal: true }
    );
  }
  function mergeDays(base = {}, incoming = {}) {
    return { ...base, ...incoming };
  }
  function normalizeLegacyTags(input = {}) {
    return Object.fromEntries(
      Object.entries(input ?? {}).map(([date, tags]) => [
        date,
        (Array.isArray(tags) ? tags : []).map((tag, index) => ({
          id: String(tag?.id ?? "").trim() || `legacy-${date}-${String(index).padStart(3, "0")}`,
          text: String(tag?.text ?? "").trim().slice(0, 60),
          type: String(tag?.type ?? "")
        })).filter((tag) => tag.text)
      ]).filter(([, tags]) => tags.length)
    );
  }
  function mergeTagBuckets(base = {}, incoming = {}) {
    const result = { ...base };
    for (const [date, tags] of Object.entries(incoming)) {
      const known = new Set((result[date] ?? []).map((tag) => tag.id));
      result[date] = [
        ...result[date] ?? [],
        ...tags.filter((tag) => !known.has(tag.id))
      ];
    }
    return result;
  }
  function readLegacyPreferencePayload(storage, payload = null) {
    const read = (name, fallback = null) => payload ? payload[LEGACY_KEYS[name]] ?? fallback : safeParse(storage?.getItem(LEGACY_KEYS[name]), fallback);
    const readText = (name) => payload ? payload[LEGACY_KEYS[name]] ?? null : storage?.getItem(LEGACY_KEYS[name]);
    const result = {};
    const textFields = ["language", "themeMode", "heatmapStyle"];
    for (const field of textFields) {
      const value = readText(field);
      if (value !== null && value !== void 0) result[field] = value;
    }
    const objectFields = ["lightBg", "windowSize", "tagColors"];
    for (const field of objectFields) {
      const value = read(field, null);
      if (value !== null && value !== void 0) result[field] = value;
    }
    const glassHeartMode = readText("glassHeartMode");
    if (glassHeartMode !== null && glassHeartMode !== void 0) {
      result.glassHeartMode = glassHeartMode === true || glassHeartMode === "1";
    }
    const chart = read("chartSettings", null);
    if (chart && typeof chart === "object") result.chart = chart;
    return result;
  }
  function legacyPayloadFromStorage(storage) {
    return Object.fromEntries(
      Object.values(LEGACY_KEYS).map((key) => [
        key,
        safeParse(storage?.getItem(key), storage?.getItem(key))
      ])
    );
  }
  var AssetHistoryStore = class {
    constructor(storage = globalThis.localStorage) {
      this.storage = storage;
      const loaded = safeParse(storage?.getItem(ASSET_HISTORY_STORAGE_KEY), null);
      this.data = migrateStoredData(loaded);
      if (loaded && loaded.version !== ASSET_HISTORY_SCHEMA_VERSION) this.save();
    }
    save() {
      this.storage?.setItem(ASSET_HISTORY_STORAGE_KEY, JSON.stringify(this.data));
    }
    scopeKey(characterId = runtime.state.currentCharacterId) {
      const server = runtime.api.getMarketEnvironment?.() ?? "production";
      return `${server}:${String(characterId ?? "")}`;
    }
    getRole(scopeKey = this.scopeKey()) {
      this.data.roles[scopeKey] = normalizeRole(this.data.roles[scopeKey]);
      return this.data.roles[scopeKey];
    }
    getPreferences() {
      this.data.preferences = normalizePreferences(this.data.preferences);
      return structuredClonePreferences(this.data.preferences);
    }
    setPreferences(patch = {}) {
      this.data.preferences = normalizePreferences({
        ...this.data.preferences,
        ...patch,
        chart: {
          ...this.data.preferences?.chart ?? {},
          ...patch.chart ?? {}
        },
        lightBg: {
          ...this.data.preferences?.lightBg ?? {},
          ...patch.lightBg ?? {}
        }
      });
      this.save();
      return this.getPreferences();
    }
    resetPreferences() {
      this.data.preferences = structuredClonePreferences();
      this.save();
      return this.getPreferences();
    }
    listTags(scopeKey = this.scopeKey()) {
      const tags = this.getRole(scopeKey).tags;
      return Object.entries(tags).sort(([left], [right]) => left.localeCompare(right)).flatMap(
        ([date, values]) => (Array.isArray(values) ? values : []).map((tag) => ({ ...tag, date }))
      );
    }
    addTag(date, text, type = "", scopeKey = this.scopeKey()) {
      const cleanText = String(text ?? "").replace(/\s+/g, " ").trim().slice(0, 60);
      if (!date || !cleanText) return null;
      const role = this.getRole(scopeKey);
      role.tags[date] ??= [];
      const tag = {
        id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
        text: cleanText,
        type: String(type ?? "")
      };
      role.tags[date].push(tag);
      this.save();
      return { ...tag, date };
    }
    updateTag(tagId, patch = {}, scopeKey = this.scopeKey()) {
      const role = this.getRole(scopeKey);
      for (const [date, tags] of Object.entries(role.tags)) {
        const tag = (tags ?? []).find((candidate) => candidate.id === tagId);
        if (!tag) continue;
        const cleanText = String(patch.text ?? tag.text).replace(/\s+/g, " ").trim().slice(0, 60);
        if (!cleanText) return false;
        tag.text = cleanText;
        if (patch.type !== void 0) tag.type = String(patch.type ?? "");
        this.save();
        return { ...tag, date };
      }
      return false;
    }
    deleteTag(tagId, scopeKey = this.scopeKey()) {
      const role = this.getRole(scopeKey);
      for (const [date, tags] of Object.entries(role.tags)) {
        const next = (tags ?? []).filter((tag) => tag.id !== tagId);
        if (next.length === tags.length) continue;
        if (next.length) role.tags[date] = next;
        else delete role.tags[date];
        this.save();
        return true;
      }
      return false;
    }
    setTagVisibility(visible2, scopeKey = this.scopeKey()) {
      this.getRole(scopeKey).tagVisibility = Boolean(visible2);
      this.save();
      return Boolean(visible2);
    }
    setGoalTarget(value, scopeKey = this.scopeKey()) {
      const target = Number(value);
      this.getRole(scopeKey).goalTarget = Number.isFinite(target) && target > 0 ? target : null;
      this.save();
      return this.getRole(scopeKey).goalTarget;
    }
    getGoalTarget(scopeKey = this.scopeKey()) {
      return this.getRole(scopeKey).goalTarget;
    }
    getAchievements(scopeKey = this.scopeKey()) {
      return { ...this.getRole(scopeKey).achievements };
    }
    syncAchievements(results, scopeKey = this.scopeKey()) {
      const role = this.getRole(scopeKey);
      let changed = false;
      for (const result of results ?? []) {
        if (!result?.unlocked || role.achievements[result.id]) continue;
        role.achievements[result.id] = {
          unlocked: true,
          date: result.date ?? null
        };
        changed = true;
      }
      if (changed) this.save();
      return this.getAchievements(scopeKey);
    }
    list(scopeKey = this.scopeKey()) {
      return Object.entries(this.getRole(scopeKey).days).sort(
        ([a], [b]) => a.localeCompare(b)
      );
    }
    record(snapshot, scopeKey = this.scopeKey(snapshot?.characterId)) {
      if (!snapshot?.complete) return false;
      const values = normalizeAssetValues(snapshot.values);
      if (!Number.isFinite(values.total)) return false;
      const dayKey = getUtc8DayKey(new Date(snapshot.recordedAt));
      const role = this.getRole(scopeKey);
      role.server = snapshot.server;
      role.characterId = snapshot.characterId;
      role.days[dayKey] = {
        recordedAt: snapshot.recordedAt,
        values
      };
      this.save();
      return dayKey;
    }
    comparison(dayKey = getUtc8DayKey(), scopeKey = this.scopeKey()) {
      const entries = this.list(scopeKey).filter(([date2]) => date2 < dayKey);
      if (!entries.length) return null;
      const yesterday = new Date(parseDayKey(dayKey) - 864e5).toISOString().slice(0, 10);
      const exact = entries.find(([date2]) => date2 === yesterday);
      const [date, record] = exact ?? entries.at(-1);
      return { date, record, gapDays: dayGap(date, dayKey) };
    }
    sevenDayAverage(dayKey = getUtc8DayKey(), scopeKey = this.scopeKey()) {
      const entries = this.list(scopeKey).filter(
        ([date, record]) => date <= dayKey && Number.isFinite(record?.values?.total)
      );
      if (entries.length < 2) return null;
      const currentIndex = entries.findLastIndex(([date]) => date <= dayKey);
      const current = entries[currentIndex];
      let baseline = entries[Math.max(0, currentIndex - 1)];
      for (let index = currentIndex - 1; index >= 0; index -= 1) {
        const candidate = entries[index];
        baseline = candidate;
        if (dayGap(candidate[0], current[0]) >= 7) break;
      }
      const gap = dayGap(baseline[0], current[0]);
      if (gap <= 0) return null;
      return (current[1].values.total - baseline[1].values.total) / gap;
    }
    updateDay(dayKey, componentValues, scopeKey = this.scopeKey()) {
      const values = normalizeAssetValues(componentValues);
      if (!ASSET_COMPONENT_KEYS.every((key) => Number.isFinite(values[key]))) {
        throw new TypeError("Every asset component needs a finite value");
      }
      this.getRole(scopeKey).days[dayKey] = {
        recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
        values,
        edited: true
      };
      this.save();
      return values;
    }
    insertDay(dayKey, componentValues, scopeKey = this.scopeKey()) {
      if (!isValidDayKey(dayKey)) {
        throw new TypeError("Asset history insertion requires a valid day key");
      }
      const values = normalizeAssetValues(componentValues);
      if (!ASSET_COMPONENT_KEYS.every(
        (key) => Number.isFinite(values[key]) && values[key] >= 0
      )) {
        throw new TypeError(
          "Every inserted asset component needs a non-negative finite value"
        );
      }
      const role = this.getRole(scopeKey);
      if (Object.hasOwn(role.days, dayKey)) {
        throw new RangeError(`Asset history already contains ${dayKey}`);
      }
      role.days[dayKey] = {
        recordedAt: (/* @__PURE__ */ new Date(`${dayKey}T15:59:59.999Z`)).toISOString(),
        values,
        inserted: true,
        edited: true
      };
      this.save();
      return values;
    }
    deleteDay(dayKey, scopeKey = this.scopeKey()) {
      const days = this.getRole(scopeKey).days;
      if (!Object.hasOwn(days, dayKey)) return false;
      delete days[dayKey];
      this.save();
      return true;
    }
    cleanupInvalid(scopeKey = this.scopeKey()) {
      let removed = 0;
      for (const [dayKey, record] of this.list(scopeKey)) {
        if (!Number.isFinite(record?.values?.total)) {
          delete this.getRole(scopeKey).days[dayKey];
          removed += 1;
        }
      }
      if (removed) this.save();
      return removed;
    }
    detectAnomalies(scopeKey = this.scopeKey()) {
      const entries = this.list(scopeKey).filter(
        ([, record]) => Number.isFinite(record?.values?.total)
      );
      if (entries.length < 5) return [];
      const changes = entries.slice(1).map(([date, record], index) => ({
        date,
        value: (record.values.total - entries[index][1].values.total) / Math.max(1, dayGap(entries[index][0], date))
      }));
      const mean = changes.reduce((total, change) => total + change.value, 0) / changes.length;
      const deviation = Math.sqrt(
        changes.reduce((total, change) => total + (change.value - mean) ** 2, 0) / changes.length
      );
      if (!(deviation > 0)) return [];
      const anomalies = [];
      for (let index = 0; index < changes.length - 1; index += 1) {
        const change = changes[index];
        const next = changes[index + 1];
        const zScore = (change.value - mean) / deviation;
        if (Math.abs(zScore) < 4 || !Number.isFinite(next?.value)) continue;
        const reversed = change.value > 0 && next.value < 0 || change.value < 0 && next.value > 0;
        const reversalRatio = Math.abs(next.value) / Math.abs(change.value);
        if (reversed && reversalRatio >= 0.5) {
          anomalies.push({ ...change, zScore, reversalRatio });
        }
      }
      return anomalies;
    }
    importLegacyPayload(payload, { scopeKey = this.scopeKey(), roleName = "", mode = "merge" } = {}) {
      const characterId = scopeKey.split(":").at(-1);
      const totalData = payload?.[LEGACY_KEYS.total] ?? {};
      const breakdownData = payload?.[LEGACY_KEYS.breakdown] ?? {};
      const totals = roleBucketFromLegacy(totalData, roleName, characterId);
      const breakdowns = roleBucketFromLegacy(
        breakdownData,
        roleName,
        characterId
      );
      const days = {};
      for (const dayKey of /* @__PURE__ */ new Set([
        ...Object.keys(totals ?? {}),
        ...Object.keys(breakdowns ?? {})
      ])) {
        const values = legacyValues(totals?.[dayKey], breakdowns?.[dayKey]);
        if (!Number.isFinite(values.total)) continue;
        days[dayKey] = { recordedAt: `${dayKey}T15:59:59.999Z`, values };
      }
      const role = this.getRole(scopeKey);
      role.days = mode === "replace" ? days : { ...days, ...role.days };
      role.legacyRoleName = roleName || role.legacyRoleName || null;
      const tagData = payload?.[LEGACY_KEYS.tags] ?? {};
      const tags = normalizeLegacyTags(
        roleBucketFromLegacy(tagData, roleName, characterId)
      );
      role.tags = mode === "replace" ? tags : mergeTagBuckets(role.tags, tags);
      const achievementData = payload?.[LEGACY_KEYS.achievements] ?? {};
      const achievements = roleBucketFromLegacy(
        achievementData,
        roleName,
        characterId
      );
      role.achievements = mode === "replace" ? { ...achievements } : { ...achievements, ...role.achievements };
      const goalData = payload?.[LEGACY_KEYS.goalTarget] ?? {};
      const legacyGoal = goalData?.[roleName] ?? goalData?.[characterId] ?? role.goalTarget;
      if (Number(legacyGoal) > 0) role.goalTarget = Number(legacyGoal);
      const tagPrefs = payload?.[LEGACY_KEYS.tagPrefs] ?? {};
      const legacyVisibility = tagPrefs?.[roleName] ?? tagPrefs?.[characterId];
      if (typeof legacyVisibility === "boolean") {
        role.tagVisibility = legacyVisibility;
      }
      const lastUpdates = payload?.[LEGACY_KEYS.lastUpdate] ?? {};
      role.lastUpdate = lastUpdates?.[roleName] ?? lastUpdates?.[characterId] ?? role.lastUpdate;
      this.data.preferences = normalizePreferences({
        ...this.data.preferences,
        ...readLegacyPreferencePayload(this.storage, payload)
      });
      return {
        importedDays: Object.keys(days).length,
        importedTags: Object.values(tags).reduce(
          (total, values) => total + values.length,
          0
        ),
        importedAchievements: Object.keys(achievements ?? {}).length
      };
    }
    migrateLegacy({ scopeKey = this.scopeKey(), roleName = "" } = {}) {
      if (this.data.migrations.everydayProfit[scopeKey]?.schema === 2) {
        return false;
      }
      const payload = legacyPayloadFromStorage(this.storage);
      const totalBuckets = payload?.[LEGACY_KEYS.total] ?? {};
      const characterId = scopeKey.split(":").at(-1);
      const usedBuckets = new Set(
        Object.values(this.data.migrations.everydayProfit).map((migration) => migration?.legacyRoleName).filter(Boolean)
      );
      const resolvedRoleName = roleName && Object.hasOwn(totalBuckets, roleName) && roleName || characterId && Object.hasOwn(totalBuckets, characterId) && characterId || Object.keys(totalBuckets).find((name) => !usedBuckets.has(name)) || roleName;
      const imported = this.importLegacyPayload(payload, {
        scopeKey,
        roleName: resolvedRoleName
      });
      this.data.legacy = {
        ...this.data.legacy,
        everydayProfit: payload
      };
      this.data.migrations.everydayProfit[scopeKey] = {
        schema: 2,
        migratedAt: (/* @__PURE__ */ new Date()).toISOString(),
        legacyRoleName: resolvedRoleName || null,
        ...imported
      };
      this.save();
      return imported.importedDays;
    }
    exportBackup() {
      return {
        [ASSET_HISTORY_BACKUP_MARKER]: true,
        schema: ASSET_HISTORY_SCHEMA_VERSION,
        exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
        data: this.data
      };
    }
    importBackup(backup, { mode = "merge", scopeKey = this.scopeKey() } = {}) {
      if (backup?.[ASSET_HISTORY_BACKUP_MARKER] === true) {
        const migrated = migrateStoredData(backup.data);
        if (mode === "full") {
          this.data = migrated;
          this.save();
          return Object.values(this.data.roles).reduce(
            (total, role2) => total + Object.keys(role2.days ?? {}).length,
            0
          );
        }
        const incomingRole = normalizeRole(migrated.roles?.[scopeKey]);
        const role = this.getRole(scopeKey);
        role.days = mode === "replace" ? { ...incomingRole.days } : mergeDays(role.days, incomingRole.days);
        role.tags = mode === "replace" ? { ...incomingRole.tags } : mergeTagBuckets(role.tags, incomingRole.tags);
        role.achievements = mode === "replace" ? { ...incomingRole.achievements } : { ...incomingRole.achievements, ...role.achievements };
        if (incomingRole.goalTarget) role.goalTarget = incomingRole.goalTarget;
        role.tagVisibility = incomingRole.tagVisibility;
        this.data.preferences = normalizePreferences({
          ...this.data.preferences,
          ...migrated.preferences
        });
        this.data.legacy = {
          ...this.data.legacy,
          ...migrated.legacy ?? {}
        };
        this.save();
        return Object.keys(incomingRole.days).length;
      }
      if (backup?.__everyday_profit_backup__ === true) {
        const payload = { ...backup.payload ?? {}, ...backup.settings ?? {} };
        const roleName = this.getRole(scopeKey).legacyRoleName ?? "";
        const imported = this.importLegacyPayload(payload, {
          scopeKey,
          roleName,
          mode
        });
        this.data.legacy.importedEverydayProfit = payload;
        this.save();
        return imported.importedDays;
      }
      throw new TypeError("Unsupported asset history backup");
    }
  };
  var assetHistoryStore = new AssetHistoryStore();

  // src/features/asset-history/15-analytics.js
  var ASSET_COMPONENT_META = Object.freeze({
    equipment: { zh: "装备", en: "Equipment", color: "#38bdf8" },
    inventory: { zh: "库存", en: "Inventory", color: "#fbbf24" },
    marketListings: { zh: "订单", en: "Listings", color: "#94a3b8" },
    houses: { zh: "房屋", en: "Houses", color: "#ec4899" },
    abilities: { zh: "技能", en: "Abilities", color: "#a855f7" },
    nonTradableTokens: { zh: "代币", en: "Tokens", color: "#4ade80" },
    shrine: { zh: "神龛", en: "Shrine", color: "#fb923c" }
  });
  function finiteEntries(entries, key = "total") {
    return (entries ?? []).filter(
      ([, record]) => Number.isFinite(record?.values?.[key])
    );
  }
  function normalizedDailyChanges(entries, key = "total") {
    const result = [];
    for (let index = 1; index < (entries?.length ?? 0); index += 1) {
      const [previousDate, previousRecord] = entries[index - 1];
      const [date, record] = entries[index];
      const previous = previousRecord?.values?.[key];
      const current = record?.values?.[key];
      if (!Number.isFinite(previous) || !Number.isFinite(current)) continue;
      const gapDays = Math.max(1, dayGap(previousDate, date));
      result.push({
        date,
        previousDate,
        gapDays,
        totalChange: current - previous,
        value: (current - previous) / gapDays,
        previous,
        current
      });
    }
    return result;
  }
  function calendarMovingAverage(entries, key = "total", windowDays = 7) {
    const changes = normalizedDailyChanges(entries, key);
    return changes.map((change, index) => {
      const startDate = /* @__PURE__ */ new Date(`${change.date}T00:00:00Z`);
      startDate.setUTCDate(startDate.getUTCDate() - Math.max(1, windowDays) + 1);
      const startKey = startDate.toISOString().slice(0, 10);
      const window2 = changes.slice(0, index + 1).filter(({ date }) => date >= startKey);
      const weightedDays = window2.reduce(
        (total2, item) => total2 + item.gapDays,
        0
      );
      const total = window2.reduce((sum, item) => sum + item.totalChange, 0);
      return {
        date: change.date,
        value: weightedDays > 0 ? total / weightedDays : null
      };
    });
  }
  function periodStatistics(entries, { start: start2 = null, end = null } = {}) {
    const values = finiteEntries(entries);
    const selected = values.filter(
      ([date]) => (!start2 || date >= start2) && (!end || date <= end)
    );
    if (!selected.length) return null;
    const firstDate = selected[0][0];
    const baseline = [...values].reverse().find(([date]) => date < firstDate);
    const calculationEntries = baseline ? [baseline, ...selected] : selected;
    const changes = normalizedDailyChanges(calculationEntries);
    const profits = changes.filter(({ totalChange }) => totalChange > 0);
    const losses = changes.filter(({ totalChange }) => totalChange < 0);
    const totalProfit = changes.reduce(
      (total, item) => total + item.totalChange,
      0
    );
    const elapsedDays = changes.reduce((total, item) => total + item.gapDays, 0);
    const best = changes.reduce(
      (candidate, item) => !candidate || item.totalChange > candidate.totalChange ? item : candidate,
      null
    );
    const worst = changes.reduce(
      (candidate, item) => !candidate || item.totalChange < candidate.totalChange ? item : candidate,
      null
    );
    const startValue = selected[0][1].values.total;
    const endValue = selected.at(-1)[1].values.total;
    return {
      start: firstDate,
      end: selected.at(-1)[0],
      startValue,
      endValue,
      totalProfit,
      growthPercent: startValue ? (endValue - startValue) / startValue * 100 : null,
      averagePerDay: elapsedDays > 0 ? totalProfit / elapsedDays : 0,
      profitDays: profits.length,
      lossDays: losses.length,
      flatDays: changes.length - profits.length - losses.length,
      winRate: changes.length ? profits.length / changes.length * 100 : 0,
      best,
      worst,
      changes
    };
  }
  function buildHeatmap(entries) {
    return Object.fromEntries(
      normalizedDailyChanges(finiteEntries(entries)).map((item) => [
        item.date,
        item
      ])
    );
  }
  function componentAnalysis(entries, rangeDays = null) {
    const selected = !Number.isFinite(rangeDays) ? entries : entries.slice(-(Math.max(1, rangeDays) + 1));
    const first = selected?.[0];
    const last = selected?.at(-1);
    if (!first || !last) return null;
    const gapDays = Math.max(1, dayGap(first[0], last[0]));
    const components = ASSET_COMPONENT_KEYS.map((key) => {
      const start2 = first[1]?.values?.[key];
      const end = last[1]?.values?.[key];
      const known = Number.isFinite(start2) && Number.isFinite(end);
      const change = known ? end - start2 : null;
      return {
        key,
        ...ASSET_COMPONENT_META[key],
        start: start2,
        end,
        change,
        averagePerDay: known ? change / gapDays : null,
        share: Number.isFinite(end) && Number(last[1]?.values?.total) > 0 ? end / last[1].values.total * 100 : null
      };
    }).sort(
      (left, right) => (right.change ?? -Infinity) - (left.change ?? -Infinity)
    );
    const trackedChange = components.reduce(
      (total, item) => total + (Number.isFinite(item.change) ? item.change : 0),
      0
    );
    const totalChange = last[1].values.total - first[1].values.total;
    return {
      startDate: first[0],
      endDate: last[0],
      gapDays,
      components,
      trackedChange,
      totalChange,
      untrackedChange: totalChange - trackedChange
    };
  }
  var ACHIEVEMENT_DEFINITIONS = [
    [
      "nw_1m",
      "💰",
      "初有积蓄",
      "First Million",
      "净资产达到 1M",
      "Reach 1M net worth",
      "networth",
      1e6
    ],
    [
      "nw_10m",
      "💎",
      "千万身家",
      "Ten Million",
      "净资产达到 10M",
      "Reach 10M net worth",
      "networth",
      1e7
    ],
    [
      "nw_100m",
      "🏦",
      "亿万富翁",
      "Hundred Million",
      "净资产达到 100M",
      "Reach 100M net worth",
      "networth",
      1e8
    ],
    [
      "nw_1b",
      "👑",
      "十亿俱乐部",
      "Billion Club",
      "净资产达到 1B",
      "Reach 1B net worth",
      "networth",
      1e9
    ],
    [
      "nw_10b",
      "🌌",
      "银河财阀",
      "Galactic Fortune",
      "净资产达到 10B",
      "Reach 10B net worth",
      "networth",
      1e10
    ],
    [
      "day_1m",
      "📈",
      "日进斗金",
      "Million Day",
      "单次记录盈利达到 1M",
      "Gain 1M between records",
      "profit",
      1e6
    ],
    [
      "day_10m",
      "🚀",
      "一日千万",
      "Ten Million Day",
      "单次记录盈利达到 10M",
      "Gain 10M between records",
      "profit",
      1e7
    ],
    [
      "day_100m",
      "🔥",
      "一日破亿",
      "Hundred Million Day",
      "单次记录盈利达到 100M",
      "Gain 100M between records",
      "profit",
      1e8
    ],
    [
      "day_1b",
      "🌟",
      "日赚十亿",
      "Billion Day",
      "单次记录盈利达到 1B",
      "Gain 1B between records",
      "profit",
      1e9
    ],
    [
      "growth_1",
      "🌱",
      "稳步成长",
      "One Percent",
      "单次增长达到 1%",
      "Grow 1% between records",
      "growth",
      1
    ],
    [
      "growth_3",
      "🌳",
      "快速成长",
      "Three Percent",
      "单次增长达到 3%",
      "Grow 3% between records",
      "growth",
      3
    ],
    [
      "growth_5",
      "💥",
      "财富爆发",
      "Five Percent",
      "单次增长达到 5%",
      "Grow 5% between records",
      "growth",
      5
    ],
    [
      "streak_3",
      "🥉",
      "三连胜",
      "Three-day Streak",
      "连续 3 次盈利",
      "3 profitable records in a row",
      "streak",
      3
    ],
    [
      "streak_7",
      "🥇",
      "七连胜",
      "Seven-day Streak",
      "连续 7 次盈利",
      "7 profitable records in a row",
      "streak",
      7
    ],
    [
      "streak_30",
      "🏆",
      "月度不败",
      "Thirty-day Streak",
      "连续 30 次非亏损",
      "30 non-loss records in a row",
      "streak",
      30
    ],
    [
      "veteran_30",
      "📅",
      "坚持一月",
      "One Month",
      "历史跨度达到 30 天",
      "Track assets for 30 days",
      "age",
      30
    ],
    [
      "veteran_90",
      "🗓️",
      "季度老兵",
      "Quarter Veteran",
      "历史跨度达到 90 天",
      "Track assets for 90 days",
      "age",
      90
    ],
    [
      "veteran_180",
      "🧭",
      "半年征程",
      "Half-year Journey",
      "历史跨度达到 180 天",
      "Track assets for 180 days",
      "age",
      180
    ],
    [
      "comeback",
      "🔄",
      "绝地反击",
      "Comeback",
      "亏损后下一次盈利",
      "Profit after a loss",
      "comeback",
      1
    ],
    [
      "double",
      "🎯",
      "首次翻倍",
      "First Double",
      "净资产较最初记录翻倍",
      "Double initial net worth",
      "double",
      2
    ],
    [
      "break_even",
      "🎰",
      "零和博弈",
      "Break Even",
      "某次盈亏恰好为 0",
      "A record change equals zero",
      "flat",
      0
    ],
    ...ASSET_COMPONENT_KEYS.map((key) => {
      const ids = {
        equipment: "bd_equip_1b",
        inventory: "bd_inv_1b",
        marketListings: "bd_order_1b",
        houses: "bd_house_1b",
        abilities: "bd_skill_1b",
        nonTradableTokens: "bd_tokens_1b",
        shrine: "bd_shrine_1b"
      };
      const meta = ASSET_COMPONENT_META[key];
      return [
        ids[key],
        "🧩",
        `${meta.zh}大亨`,
        `${meta.en} Billionaire`,
        `${meta.zh}价值达到 1B`,
        `${meta.en} value reaches 1B`,
        "component",
        { key, value: 1e9 }
      ];
    }),
    [
      "bd_order_500m",
      "💰",
      "订单高手",
      "Listing Master",
      "订单价值达到 500M",
      "Listings reach 500M",
      "component",
      { key: "marketListings", value: 5e8 }
    ],
    [
      "bd_equip_5b",
      "⚔️",
      "装备至上",
      "Equipment Supreme",
      "装备价值达到 5B",
      "Equipment reaches 5B",
      "component",
      { key: "equipment", value: 5e9 }
    ],
    [
      "bd_balanced",
      "⚖️",
      "均衡发展",
      "Balanced Portfolio",
      "七项中至少四项有值且无一项超过 40%",
      "At least four components, none above 40%",
      "componentBalanced",
      0.4
    ],
    [
      "bd_specialist",
      "🎯",
      "专精路线",
      "Specialist",
      "某一分项占比超过 60%",
      "One component exceeds 60%",
      "componentSpecialist",
      0.6
    ],
    [
      "bd_all_up",
      "📈",
      "全面提升",
      "All Components Up",
      "某日七项资产全部增长",
      "All seven components rise on one day",
      "componentAllUp",
      7
    ]
  ];
  function calculateAchievements(entries, persisted = {}) {
    const values = finiteEntries(entries);
    const changes = normalizedDailyChanges(values);
    let streak = 0;
    let maximumStreak = 0;
    for (const change of changes) {
      streak = change.totalChange >= 0 ? streak + 1 : 0;
      maximumStreak = Math.max(maximumStreak, streak);
    }
    const firstValue = values[0]?.[1]?.values?.total;
    const latestValue = values.at(-1)?.[1]?.values?.total;
    const age = values.length > 1 ? dayGap(values[0][0], values.at(-1)[0]) : 0;
    return ACHIEVEMENT_DEFINITIONS.map(
      ([
        id,
        icon,
        zhName,
        enName,
        zhDescription,
        enDescription,
        kind,
        target
      ]) => {
        let match = null;
        if (kind === "networth") {
          match = values.find(([, record]) => record.values.total >= target);
        } else if (kind === "profit") {
          match = changes.find((change) => change.totalChange >= target);
        } else if (kind === "growth") {
          match = changes.find(
            (change) => change.previous > 0 && change.totalChange / change.previous * 100 >= target
          );
        } else if (kind === "streak" && maximumStreak >= target) {
          let running = 0;
          match = changes.find((change) => {
            running = change.totalChange >= 0 ? running + 1 : 0;
            return running >= target;
          });
        } else if (kind === "age" && age >= target) {
          match = values.find(([date]) => dayGap(values[0][0], date) >= target);
        } else if (kind === "comeback") {
          match = changes.find(
            (change, index) => index > 0 && changes[index - 1].totalChange < 0 && change.totalChange > 0
          );
        } else if (kind === "double" && latestValue >= firstValue * target) {
          match = values.find(
            ([, record]) => record.values.total >= firstValue * target
          );
        } else if (kind === "flat") {
          match = changes.find((change) => change.totalChange === 0);
        } else if (kind === "component") {
          match = values.find(([, record]) => {
            const value = record.values[target.key];
            return Number.isFinite(value) && value >= target.value;
          });
        } else if (kind === "componentBalanced" || kind === "componentSpecialist") {
          match = values.find(([, record]) => {
            const components = ASSET_COMPONENT_KEYS.map(
              (key) => record.values[key]
            ).filter((value) => Number.isFinite(value) && value > 0);
            const total = components.reduce((sum, value) => sum + value, 0);
            if (!(total > 0)) return false;
            const maximumShare = Math.max(...components) / total;
            return kind === "componentBalanced" ? components.length >= 4 && maximumShare < target : maximumShare > target;
          });
        } else if (kind === "componentAllUp") {
          match = values.slice(1).find(
            ([, record], index) => ASSET_COMPONENT_KEYS.every((key) => {
              const previousValue = values[index][1].values[key];
              const currentValue = record.values[key];
              return Number.isFinite(previousValue) && Number.isFinite(currentValue) && currentValue > previousValue;
            })
          );
        }
        const saved = persisted[id];
        const unlocked = Boolean(match || saved?.unlocked);
        return {
          id,
          icon,
          zhName,
          enName,
          zhDescription,
          enDescription,
          unlocked,
          date: match?.date ?? match?.[0] ?? saved?.date ?? null
        };
      }
    );
  }
  function percentile(sorted, percent) {
    return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * percent))];
  }
  function simulateNetWorth(entries, {
    days = 90,
    windowSize = 60,
    runs = 2e3,
    target = null,
    random = Math.random
  } = {}) {
    const values = finiteEntries(entries);
    if (values.length < 7) return { status: "insufficient", required: 7 };
    const source = windowSize > 0 ? values.slice(-windowSize - 1) : values;
    const returns = [];
    for (let index = 1; index < source.length; index += 1) {
      const previous = source[index - 1][1].values.total;
      const current2 = source[index][1].values.total;
      if (!(previous > 0) || !(current2 > 0)) continue;
      const gap = Math.max(1, dayGap(source[index - 1][0], source[index][0]));
      returns.push(Math.log(current2 / previous) / gap);
    }
    if (returns.length < 3) return { status: "insufficient", required: 4 };
    const horizon = Math.min(365, Math.max(1, Math.floor(days)));
    const count = Math.min(1e4, Math.max(100, Math.floor(runs)));
    const current = values.at(-1)[1].values.total;
    const mu = returns.reduce((sum, value) => sum + value, 0) / returns.length;
    const variance = returns.reduce((sum, value) => sum + (value - mu) ** 2, 0) / Math.max(1, returns.length - 1);
    const sigma = Math.sqrt(variance);
    let ewmaVariance = returns[0] ** 2;
    for (let index = 1; index < returns.length; index += 1) {
      ewmaVariance = 0.94 * ewmaVariance + 0.06 * returns[index] ** 2;
    }
    const useBootstrap = returns.length >= 15;
    const paths = Array.from(
      { length: count },
      () => new Float64Array(horizon + 1)
    );
    const randomNormal = () => {
      let left = random();
      let right = random();
      if (left <= 0) left = Number.EPSILON;
      if (right <= 0) right = Number.EPSILON;
      return Math.sqrt(-2 * Math.log(left)) * Math.cos(2 * Math.PI * right);
    };
    for (let run = 0; run < count; run += 1) {
      paths[run][0] = current;
      if (useBootstrap) {
        let day = 1;
        while (day <= horizon) {
          const start2 = Math.floor(random() * returns.length);
          for (let block = 0; block < 5 && day <= horizon; block += 1) {
            paths[run][day] = paths[run][day - 1] * Math.exp(returns[(start2 + block) % returns.length]);
            day += 1;
          }
        }
      } else {
        for (let day = 1; day <= horizon; day += 1) {
          paths[run][day] = paths[run][day - 1] * Math.exp(mu - 0.5 * variance + sigma * randomNormal());
        }
      }
    }
    const series = { p10: [], p25: [], p50: [], p75: [], p90: [] };
    for (let day = 0; day <= horizon; day += 1) {
      const column = paths.map((path) => path[day]).sort((a, b) => a - b);
      series.p10.push(percentile(column, 0.1));
      series.p25.push(percentile(column, 0.25));
      series.p50.push(percentile(column, 0.5));
      series.p75.push(percentile(column, 0.75));
      series.p90.push(percentile(column, 0.9));
    }
    const resolvedTarget = Number(target) > 0 ? Number(target) : null;
    const probabilities = {};
    if (resolvedTarget) {
      for (const checkpoint of [30, 60, 90].filter((day) => day <= horizon)) {
        probabilities[checkpoint] = paths.filter((path) => path[checkpoint] >= resolvedTarget).length / count * 100;
      }
    }
    return {
      status: "complete",
      method: useBootstrap ? "block-bootstrap" : "gbm",
      current,
      days: horizon,
      runs: count,
      target: resolvedTarget,
      dailyGrowthPercent: (Math.exp(mu) - 1) * 100,
      dailyVolatilityPercent: (Math.exp(Math.sqrt(ewmaVariance)) - 1) * 100,
      doublingDays: mu > 0 ? Math.ceil(Math.LN2 / mu) : null,
      series,
      probabilities
    };
  }

  // src/core/frame-scheduler.js
  function createFrameScheduler(callback) {
    let active = true;
    let pending = false;
    let cancelPending = null;
    const run = () => {
      pending = false;
      cancelPending = null;
      if (active) callback();
    };
    return {
      schedule() {
        if (!active || pending) return false;
        pending = true;
        if (typeof globalThis.requestAnimationFrame === "function") {
          const id = globalThis.requestAnimationFrame(run);
          cancelPending = () => globalThis.cancelAnimationFrame?.(id);
        } else {
          const id = globalThis.setTimeout(run, 0);
          cancelPending = () => globalThis.clearTimeout(id);
        }
        return true;
      },
      cancel() {
        if (!active) return;
        active = false;
        cancelPending?.();
        cancelPending = null;
        pending = false;
      }
    };
  }

  // src/core/mutation-channel.js
  var channels = /* @__PURE__ */ new Map();
  var OPTION_KEYS = [
    "attributes",
    "attributeOldValue",
    "characterData",
    "characterDataOldValue",
    "childList",
    "subtree"
  ];
  function optionSignature(options = {}) {
    const normalized = Object.fromEntries(
      OPTION_KEYS.map((key) => [key, Boolean(options[key])])
    );
    normalized.attributeFilter = [...options.attributeFilter ?? []].sort();
    return JSON.stringify(normalized);
  }
  function observerConstructor(target) {
    return globalThis.MutationObserver ?? target?.ownerDocument?.defaultView?.MutationObserver ?? globalThis.document?.defaultView?.MutationObserver;
  }
  function t(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function reportSubscriberError(name, error) {
    console.error(
      `[MWITools] Mutation channel subscriber failed (${name})`,
      error
    );
  }
  function subscribeMutationChannel({ name, target, options, scope }, callback) {
    if (!name || !target || typeof callback !== "function") {
      throw new TypeError(
        "Mutation channel subscriptions need a name, target, and callback"
      );
    }
    const signature = optionSignature(options);
    let channel = channels.get(name);
    if (channel) {
      if (channel.target !== target || channel.signature !== signature) {
        throw new Error(
          t(
            `页面观察通道 ${name} 使用了不同的根节点或配置`,
            `Mutation channel ${name} was reused with a different target or options`
          )
        );
      }
    } else {
      const Observer = observerConstructor(target);
      if (typeof Observer !== "function") {
        throw new Error(
          t(
            `页面观察通道 ${name} 无法使用 MutationObserver`,
            `MutationObserver is unavailable for channel ${name}`
          )
        );
      }
      const subscribers = /* @__PURE__ */ new Set();
      const observer = new Observer((records, source) => {
        for (const subscriber of [...subscribers]) {
          try {
            subscriber(records, source);
          } catch (error) {
            reportSubscriberError(name, error);
          }
        }
      });
      channel = { name, observer, signature, subscribers, target };
      observer.observe(target, options);
      channels.set(name, channel);
    }
    channel.subscribers.add(callback);
    let active = true;
    const unsubscribe = () => {
      if (!active) return;
      active = false;
      channel.subscribers.delete(callback);
      if (channel.subscribers.size) return;
      channel.observer.disconnect();
      if (channels.get(name) === channel) channels.delete(name);
    };
    scope?.add?.(unsubscribe);
    return unsubscribe;
  }

  // src/features/asset-history/20-chart.js
  var COLORS = {
    equipment: "#5bc0eb",
    inventory: "#ffd166",
    marketListings: "#8ecae6",
    houses: "#ff6384",
    abilities: "#a78bfa",
    nonTradableTokens: "#80ed99",
    shrine: "#f4a261"
  };
  var LABELS = {
    equipment: ["装备", "Equipment"],
    inventory: ["库存", "Inventory"],
    marketListings: ["订单", "Market listings"],
    houses: ["房屋", "Houses"],
    abilities: ["技能", "Abilities"],
    nonTradableTokens: ["不可交易代币", "Non-tradable tokens"],
    shrine: ["神龛", "Shrine"]
  };
  function t2(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function filterEntries(entries, range) {
    if (!Number.isFinite(range) || !entries.length) return entries;
    const lastDate = entries.at(-1)[0];
    return entries.filter(([date]) => dayGap(date, lastDate) < range);
  }
  function normalizedChanges(entries, key) {
    return entries.map(([date, record], index) => {
      if (!index) return null;
      const previous = entries[index - 1];
      const currentValue = record?.values?.[key];
      const previousValue = previous[1]?.values?.[key];
      if (!Number.isFinite(currentValue) || !Number.isFinite(previousValue)) {
        return null;
      }
      return (currentValue - previousValue) / Math.max(1, dayGap(previous[0], date));
    });
  }
  function calendarAverage(entries, key, windowDays = 7) {
    return entries.map(([date, record], index) => {
      if (!index || !Number.isFinite(record?.values?.[key])) return null;
      let baseline = entries[index - 1];
      for (let candidate = index - 1; candidate >= 0; candidate -= 1) {
        baseline = entries[candidate];
        if (dayGap(baseline[0], date) >= windowDays) break;
      }
      const gap = dayGap(baseline[0], date);
      const baselineValue = baseline[1]?.values?.[key];
      if (!(gap > 0) || !Number.isFinite(baselineValue)) return null;
      return (record.values[key] - baselineValue) / gap;
    });
  }
  function formatTooltip(value) {
    return runtime.api.numberFormatter?.(value) ?? String(value);
  }
  var AssetHistoryChart = class {
    constructor(canvas, fallback, { hiddenDatasets = null } = {}) {
      this.canvas = canvas;
      this.fallback = fallback;
      this.instance = null;
      this.hiddenDatasets = hiddenDatasets ?? /* @__PURE__ */ new Set();
    }
    destroy() {
      this.instance?.destroy?.();
      this.instance = null;
    }
    resetZoom() {
      this.instance?.resetZoom?.();
    }
    prepareCanvas() {
      if (!this.canvas || this.canvas.isConnected === false) return null;
      if (this.canvas.style) {
        this.canvas.style.display = "block";
        this.canvas.style.width = "100%";
        this.canvas.style.height = "100%";
      }
      const bounds = this.canvas.getBoundingClientRect?.() ?? {};
      const width = Math.max(
        1,
        Math.round(
          Number(bounds.width) || Number(this.canvas.clientWidth) || Number(this.canvas.width) || 300
        )
      );
      const height = Math.max(
        1,
        Math.round(
          Number(bounds.height) || Number(this.canvas.clientHeight) || Number(this.canvas.height) || 150
        )
      );
      this.canvas.width = width;
      this.canvas.height = height;
      return this.canvas.getContext?.("2d") ?? null;
    }
    render(entries, { mode = "total", range = null } = {}) {
      return this.renderWithOptions(entries, { mode, range });
    }
    renderWithOptions(entries, {
      mode = "total",
      range = null,
      maWindow = 7,
      lineTension = 0.25,
      tags = []
    } = {}) {
      const Chart = globalThis.Chart;
      if (typeof Chart !== "function") {
        this.destroy();
        this.canvas.hidden = true;
        this.fallback.hidden = false;
        this.fallback.textContent = t2(
          "图表依赖未加载;资产数据与明细仍可正常使用。",
          "Chart dependencies did not load; asset data is still available."
        );
        return false;
      }
      const filtered = filterEntries(entries, range);
      const labels = filtered.map(([date]) => date);
      let datasets;
      let title;
      if (mode === "profit") {
        const profit = normalizedChanges(filtered, "total");
        datasets = [
          {
            type: "bar",
            label: t2("每日盈亏", "Daily P/L"),
            data: profit,
            backgroundColor: profit.map(
              (value) => value >= 0 ? "rgba(65,190,115,.58)" : "rgba(235,90,90,.58)"
            ),
            borderRadius: 3
          },
          {
            type: "line",
            label: t2(`${maWindow} 日均线`, `${maWindow}-day average`),
            data: calendarAverage(filtered, "total", maWindow),
            borderColor: "#ffd369",
            backgroundColor: "transparent",
            borderWidth: 2,
            pointRadius: 0,
            tension: lineTension,
            spanGaps: true
          }
        ];
        title = t2("每日资产盈亏", "Daily asset P/L");
      } else if (mode === "breakdown") {
        datasets = ASSET_COMPONENT_KEYS.map((key) => ({
          type: "line",
          label: t2(...LABELS[key]),
          data: filtered.map(([, record]) => record?.values?.[key] ?? null),
          mwitoolsVisibilityKey: key,
          borderColor: COLORS[key],
          backgroundColor: COLORS[key],
          borderWidth: 2,
          pointRadius: 2,
          tension: lineTension,
          spanGaps: true
        }));
        title = t2("分项资产", "Component assets");
      } else {
        datasets = [
          {
            type: "line",
            label: t2("总资产", "Total assets"),
            data: filtered.map(([, record]) => record?.values?.total ?? null),
            borderColor: "#4cc9f0",
            backgroundColor: "rgba(76,201,240,.14)",
            fill: true,
            borderWidth: 2,
            pointRadius: 2,
            tension: lineTension,
            spanGaps: true
          }
        ];
        title = t2("总资产历史", "Total asset history");
      }
      for (const dataset of datasets) {
        const key = `${mode}:${dataset.mwitoolsVisibilityKey ?? dataset.label}`;
        dataset.hidden = this.hiddenDatasets.has(key);
      }
      this.destroy();
      const context = this.prepareCanvas();
      if (!context) return false;
      this.canvas.hidden = false;
      this.fallback.hidden = true;
      const crosshairPlugin = {
        id: "mwitoolsAssetCrosshair",
        afterDraw(chart) {
          const active = chart.tooltip?.getActiveElements?.()?.[0];
          if (!active) return;
          const { ctx, chartArea } = chart;
          ctx.save();
          ctx.beginPath();
          ctx.moveTo(active.element.x, chartArea.top);
          ctx.lineTo(active.element.x, chartArea.bottom);
          ctx.lineWidth = 1;
          ctx.strokeStyle = "rgba(255,255,255,.22)";
          ctx.stroke();
          ctx.restore();
        }
      };
      const tagPlugin = {
        id: "mwitoolsAssetTags",
        afterDatasetsDraw(chart) {
          if (!tags.length) return;
          const visibleTags = tags.filter((tag) => labels.includes(tag.date));
          if (!visibleTags.length) return;
          const { ctx, chartArea, scales } = chart;
          ctx.save();
          ctx.font = "11px system-ui";
          ctx.textBaseline = "top";
          for (const tag of visibleTags) {
            const index = labels.indexOf(tag.date);
            const x = scales.x.getPixelForValue(index);
            ctx.strokeStyle = tag.color ?? "rgba(251,191,36,.4)";
            ctx.setLineDash([3, 4]);
            ctx.beginPath();
            ctx.moveTo(x, chartArea.top + 18);
            ctx.lineTo(x, chartArea.bottom);
            ctx.stroke();
            ctx.setLineDash([]);
            const text = String(tag.text ?? "").slice(0, 18);
            const width = Math.min(150, ctx.measureText(text).width + 12);
            ctx.fillStyle = "rgba(30,32,44,.94)";
            ctx.fillRect(
              Math.min(chartArea.right - width, Math.max(chartArea.left, x + 4)),
              chartArea.top + 2,
              width,
              16
            );
            ctx.fillStyle = tag.color ?? "#f8d477";
            ctx.fillText(
              text,
              Math.min(
                chartArea.right - width + 6,
                Math.max(chartArea.left + 6, x + 10)
              ),
              chartArea.top + 4
            );
          }
          ctx.restore();
        }
      };
      this.instance = new Chart(context, {
        data: { labels, datasets },
        plugins: [crosshairPlugin, tagPlugin],
        options: {
          // Chart.js responsive mode observes DOM attachment with Node.contains().
          // Firefox userscript sandboxes can reject that cross-context access when
          // the game replaces a React subtree, so MWITools sizes the canvas above.
          responsive: false,
          maintainAspectRatio: false,
          interaction: { mode: "index", intersect: false },
          animation: false,
          plugins: {
            title: { display: true, text: title, color: "#eee" },
            legend: {
              labels: { color: "#ddd", usePointStyle: true },
              onClick: (_event, legendItem, legend) => {
                const index = legendItem?.datasetIndex;
                const chart = legend?.chart;
                if (!Number.isInteger(index) || !chart) return;
                const dataset = chart.data.datasets[index];
                const key = `${mode}:${dataset.mwitoolsVisibilityKey ?? dataset.label}`;
                const visible2 = chart.isDatasetVisible(index);
                if (visible2) this.hiddenDatasets.add(key);
                else this.hiddenDatasets.delete(key);
                chart.setDatasetVisibility(index, !visible2);
                chart.update();
              }
            },
            tooltip: {
              callbacks: {
                label(context2) {
                  const value = context2.raw;
                  return `${context2.dataset.label}: ${Number.isFinite(value) ? formatTooltip(value) : "—"}`;
                }
              }
            },
            zoom: {
              pan: { enabled: true, mode: "x" },
              zoom: {
                wheel: { enabled: true },
                pinch: { enabled: true },
                drag: { enabled: true, modifierKey: "shift" },
                mode: "x"
              }
            }
          },
          scales: {
            x: {
              ticks: {
                color: "#bbb",
                maxRotation: 0,
                autoSkip: true,
                callback(value) {
                  return String(labels[value] ?? "").slice(5);
                }
              },
              grid: { color: "rgba(255,255,255,.06)" }
            },
            y: {
              ticks: {
                color: "#bbb",
                callback(value) {
                  return runtime.api.numberFormatter?.(value) ?? value;
                }
              },
              grid: { color: "rgba(255,255,255,.08)" }
            }
          }
        }
      });
      return true;
    }
  };

  // src/features/asset-history/25-center.js
  /*!
   * Asset-center interface adapted from Everyday Profit Pro (MIT License).
   * Copyright (c) 2025 VictoryWinWinWin, PaperCat, SuXingX
   * Copyright (c) 2026 ColaCola
   * Permission is hereby granted, free of charge, to use, copy, modify, merge,
   * publish, distribute, sublicense, and/or sell copies, provided that this
   * copyright and permission notice is included in substantial copies.
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
   * SOFTWARE.
   */
  var ROOT_ID = "mwitools-asset-center-modal";
  var STYLE_ID2 = "mwitools-asset-center-style";
  var EP_MIT_LICENSE = `MIT License

Copyright (c) 2025 VictoryWinWinWin, PaperCat, SuXingX
Copyright (c) 2026 ColaCola

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`;
  var TAG_TYPES = [
    ["equip", "装备", "Equipment", "#00c6ff"],
    ["skill", "技能", "Skills", "#a78bfa"],
    ["alchemy", "炼金", "Alchemy", "#ff9800"],
    ["enhance", "强化", "Enhancement", "#ef5350"],
    ["combat", "战斗", "Combat", "#f472b6"],
    ["life", "生活", "Life", "#4ade80"]
  ];
  function escapeHtml(value = "") {
    return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
  }
  function monthRange(date) {
    const year = date.getFullYear();
    const month = date.getMonth();
    const start2 = `${year}-${String(month + 1).padStart(2, "0")}-01`;
    const end = new Date(year, month + 1, 0);
    return {
      start: start2,
      end: `${year}-${String(month + 1).padStart(2, "0")}-${String(end.getDate()).padStart(2, "0")}`,
      year,
      month
    };
  }
  function weekRange(date) {
    const current = new Date(date.getFullYear(), date.getMonth(), date.getDate());
    const day = current.getDay() || 7;
    current.setDate(current.getDate() - day + 1);
    const end = new Date(current);
    end.setDate(end.getDate() + 6);
    return {
      start: current.toISOString().slice(0, 10),
      end: end.toISOString().slice(0, 10),
      year: date.getFullYear(),
      month: date.getMonth()
    };
  }
  function shiftDayKey(dayKey, amount) {
    const date = /* @__PURE__ */ new Date(`${dayKey}T00:00:00Z`);
    date.setUTCDate(date.getUTCDate() + amount);
    return date.toISOString().slice(0, 10);
  }
  function addStyles2() {
    if (document.getElementById(STYLE_ID2)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID2;
    style.textContent = `
    #${ROOT_ID}{--ep-bg:222 18% 10%;--ep-panel:222 17% 13%;--ep-card:222 16% 16%;--ep-card2:222 15% 19%;--ep-fg:210 20% 96%;--ep-muted:215 12% 66%;--ep-border:215 14% 25%;--ep-accent:191 100% 50%;position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;background:rgba(3,7,18,.72);font:13px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;color:hsl(var(--ep-fg))}
    #${ROOT_ID}[hidden]{display:none!important}#${ROOT_ID}.ep-light{--ep-bg:var(--ep-light-h,38) var(--ep-light-s,44%) 94%;--ep-panel:var(--ep-light-h,38) 35% 97%;--ep-card:0 0% 100%;--ep-card2:var(--ep-light-h,38) 25% 95%;--ep-fg:220 18% 18%;--ep-muted:220 9% 43%;--ep-border:220 12% 82%;background:rgba(15,23,42,.55)}#${ROOT_ID}.ep-glass-heart .neg{color:#f59e9e!important}
    #${ROOT_ID} *{box-sizing:border-box}#${ROOT_ID} button,#${ROOT_ID} input,#${ROOT_ID} select{font:inherit}#${ROOT_ID} button{color:inherit}
    #${ROOT_ID} .ep-shell{position:relative;display:grid;grid-template-columns:220px minmax(0,1fr);width:min(1180px,94vw);height:min(820px,92vh);min-width:720px;min-height:520px;overflow:hidden;border:1px solid hsl(var(--ep-border));border-radius:14px;background:hsl(var(--ep-bg));box-shadow:0 30px 90px rgba(0,0,0,.55);resize:both}
    #${ROOT_ID} .ep-sidebar{display:flex;min-width:0;flex-direction:column;border-right:1px solid hsl(var(--ep-border));background:linear-gradient(180deg,hsl(var(--ep-panel)),hsl(var(--ep-bg)));padding:16px 12px}
    #${ROOT_ID} .ep-brand{padding:3px 8px 17px;border-bottom:1px solid hsl(var(--ep-border));margin-bottom:14px}#${ROOT_ID} .ep-brand strong{display:block;font-size:17px;letter-spacing:.2px}#${ROOT_ID} .ep-brand small{color:hsl(var(--ep-muted))}
    #${ROOT_ID} .ep-nav-label{margin:11px 8px 5px;color:hsl(var(--ep-muted));font-size:9px;font-weight:800;letter-spacing:1.5px}#${ROOT_ID} .ep-nav-item{display:flex;width:100%;align-items:center;gap:9px;padding:8px 10px;border:0;border-radius:7px;background:transparent;text-align:left;cursor:pointer}#${ROOT_ID} .ep-nav-item:hover{background:hsl(var(--ep-card))}#${ROOT_ID} .ep-nav-item.active{background:linear-gradient(90deg,hsl(var(--ep-accent)/.2),transparent);color:hsl(var(--ep-accent));box-shadow:inset 2px 0 hsl(var(--ep-accent))}#${ROOT_ID} .ep-nav-icon{width:18px;text-align:center}
    #${ROOT_ID} .ep-nav-footer{margin-top:auto;padding-top:12px;border-top:1px solid hsl(var(--ep-border))}#${ROOT_ID} .ep-main{display:flex;min-width:0;min-height:0;flex-direction:column}#${ROOT_ID} .ep-top{display:flex;min-height:66px;align-items:center;gap:12px;padding:12px 20px;border-bottom:1px solid hsl(var(--ep-border));background:hsl(var(--ep-panel)/.85)}#${ROOT_ID} .ep-top-main{min-width:0;flex:1}#${ROOT_ID} .ep-top-title{font-size:18px;font-weight:800}#${ROOT_ID} .ep-top-sub{overflow:hidden;color:hsl(var(--ep-muted));font-size:11px;text-overflow:ellipsis;white-space:nowrap}#${ROOT_ID} .ep-close{width:34px;height:34px;border:1px solid hsl(var(--ep-border));border-radius:8px;background:hsl(var(--ep-card));cursor:pointer}
    #${ROOT_ID} .ep-page{min-height:0;flex:1;overflow:auto;padding:18px 20px 32px;scrollbar-gutter:stable}#${ROOT_ID} .ep-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}#${ROOT_ID} .ep-card{min-width:0;border:1px solid hsl(var(--ep-border));border-radius:10px;background:hsl(var(--ep-card));box-shadow:0 8px 22px rgba(0,0,0,.12)}#${ROOT_ID} .ep-metric{padding:13px 14px}#${ROOT_ID} .ep-metric span{display:block;color:hsl(var(--ep-muted));font-size:10px}#${ROOT_ID} .ep-metric strong{display:block;margin-top:4px;overflow:hidden;font:700 17px/1.25 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}#${ROOT_ID} .pos{color:#4ade80!important}#${ROOT_ID} .neg{color:#fb7185!important}
    #${ROOT_ID} .ep-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:6px;padding:10px}#${ROOT_ID} .ep-btn{padding:6px 10px;border:1px solid hsl(var(--ep-border));border-radius:6px;background:hsl(var(--ep-card2));cursor:pointer}#${ROOT_ID} .ep-btn:hover,#${ROOT_ID} .ep-btn.active{border-color:hsl(var(--ep-accent)/.65);background:hsl(var(--ep-accent)/.15)}#${ROOT_ID} .ep-btn.danger{color:#fb7185}#${ROOT_ID} .ep-spacer{flex:1}
    #${ROOT_ID} .ep-chart{height:360px;padding:4px 12px 12px}#${ROOT_ID} .ep-section{margin-top:12px}#${ROOT_ID} .ep-section-title{display:flex;align-items:center;gap:8px;padding:11px 13px;border-bottom:1px solid hsl(var(--ep-border));font-weight:800}#${ROOT_ID} .ep-section-body{padding:13px}
    #${ROOT_ID} table{width:100%;border-collapse:collapse}#${ROOT_ID} th,#${ROOT_ID} td{padding:8px 9px;border-bottom:1px solid hsl(var(--ep-border));text-align:left}#${ROOT_ID} th{color:hsl(var(--ep-muted));font-size:10px;text-transform:uppercase}#${ROOT_ID} td.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}
    #${ROOT_ID} .ep-analysis-list{display:grid;gap:8px}#${ROOT_ID} .ep-analysis-row{display:grid;grid-template-columns:110px 1fr 120px 90px;align-items:center;gap:10px}#${ROOT_ID} .ep-bar{height:9px;overflow:hidden;border-radius:99px;background:hsl(var(--ep-border))}#${ROOT_ID} .ep-bar i{display:block;height:100%;border-radius:inherit}
    #${ROOT_ID} .ep-heatmap{display:grid;grid-template-columns:repeat(7,1fr);gap:5px}#${ROOT_ID} .ep-day{position:relative;min-height:58px;padding:5px;border:1px solid hsl(var(--ep-border));border-radius:6px;background:hsl(var(--ep-card2));font-size:10px}#${ROOT_ID} .ep-day.in-period{outline:1px solid hsl(var(--ep-accent)/.55);outline-offset:-2px}#${ROOT_ID} .ep-day.empty{visibility:hidden}#${ROOT_ID} .ep-day strong{display:block;margin-top:9px;font:700 10px ui-monospace,monospace}
    #${ROOT_ID} .ep-tags,#${ROOT_ID} .ep-achievements{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}#${ROOT_ID} .ep-tag-row,#${ROOT_ID} .ep-achievement{display:flex;min-width:0;align-items:center;gap:10px;padding:10px;border:1px solid hsl(var(--ep-border));border-radius:8px;background:hsl(var(--ep-card))}#${ROOT_ID} .ep-achievement.locked{filter:grayscale(1);opacity:.45}#${ROOT_ID} .ep-achievement-icon{font-size:23px}#${ROOT_ID} .ep-grow{min-width:0;flex:1}#${ROOT_ID} .ep-grow small{display:block;color:hsl(var(--ep-muted))}
    #${ROOT_ID} .ep-form{display:flex;align-items:end;flex-wrap:wrap;gap:8px}#${ROOT_ID} label{display:grid;gap:4px;color:hsl(var(--ep-muted));font-size:10px}#${ROOT_ID} input,#${ROOT_ID} select{min-height:33px;border:1px solid hsl(var(--ep-border));border-radius:6px;background:hsl(var(--ep-card2));padding:5px 8px;color:hsl(var(--ep-fg))}#${ROOT_ID} .ep-setting{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid hsl(var(--ep-border))}#${ROOT_ID} .ep-setting>div{flex:1}#${ROOT_ID} .ep-setting small{display:block;color:hsl(var(--ep-muted))}
    #${ROOT_ID} .ep-sim-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}#${ROOT_ID} .ep-prob{padding:10px;border-radius:8px;background:hsl(var(--ep-card2));text-align:center}#${ROOT_ID} .ep-sim-band{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-top:8px}#${ROOT_ID} .ep-sim-band div{padding:7px;border:1px solid hsl(var(--ep-border));border-radius:6px;text-align:center}#${ROOT_ID} .ep-sim-band small{display:block;color:hsl(var(--ep-muted))}#${ROOT_ID} .ep-disclaimer{margin-top:10px;color:hsl(var(--ep-muted));font-size:10px}
    #${ROOT_ID} dialog{width:min(620px,90vw);border:1px solid hsl(var(--ep-border));border-radius:10px;background:hsl(var(--ep-panel));color:hsl(var(--ep-fg))}#${ROOT_ID} dialog::backdrop{background:rgba(0,0,0,.55)}#${ROOT_ID} .ep-edit-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}
    @media(max-width:820px){#${ROOT_ID}{align-items:stretch}#${ROOT_ID} .ep-shell{grid-template-columns:64px 1fr!important;width:100vw!important;height:100dvh!important;min-width:0;min-height:0;border:0;border-radius:0;resize:none}#${ROOT_ID} .ep-sidebar{padding:10px 6px}#${ROOT_ID} .ep-brand strong,#${ROOT_ID} .ep-brand small,#${ROOT_ID} .ep-nav-label,#${ROOT_ID} .ep-nav-text{display:none}#${ROOT_ID} .ep-nav-item{justify-content:center;padding:10px 4px}#${ROOT_ID} .ep-grid{grid-template-columns:repeat(2,minmax(0,1fr))}#${ROOT_ID} .ep-page{padding:12px}#${ROOT_ID} .ep-analysis-row{grid-template-columns:80px 1fr 90px}#${ROOT_ID} .ep-analysis-row>:last-child{display:none}#${ROOT_ID} .ep-tags,#${ROOT_ID} .ep-achievements{grid-template-columns:1fr}#${ROOT_ID} .ep-sim-band{grid-template-columns:repeat(2,1fr)}}
  `;
    document.head.append(style);
  }
  var AssetCenter = class {
    constructor({ store, scopeKey, onChange = null, onVisibilityChange = null }) {
      this.store = store;
      this.scopeKey = scopeKey;
      this.onChange = onChange;
      this.onVisibilityChange = onVisibilityChange;
      this.route = "chart";
      this.chartMode = this.store.getPreferences().chart.defaultView;
      if (this.chartMode === "statsReport") this.chartMode = "networth";
      this.chartRange = this.store.getPreferences().chart.defaultRange;
      this.analysisRange = 30;
      this.reportMode = "month";
      this.reportDate = /* @__PURE__ */ new Date();
      this.chart = null;
      this.hiddenChartDatasets = /* @__PURE__ */ new Set();
      this.previousFocus = null;
      this.snapshot = null;
      addStyles2();
      this.build();
    }
    isZH() {
      const language = this.store.getPreferences().language;
      return language ? language === "zh" : runtime.config.isZH;
    }
    t(zh, en) {
      return this.isZH() ? zh : en;
    }
    format(value, signed = false) {
      if (!Number.isFinite(value)) return "—";
      const text = runtime.api.numberFormatter?.(Math.abs(value)) ?? String(value);
      return signed && value !== 0 ? `${value > 0 ? "+" : "−"}${text}` : text;
    }
    tagColor(type) {
      const fallback = TAG_TYPES.find(([key]) => key === type)?.[3] ?? "#888888";
      return this.store.getPreferences().tagColors?.[type] ?? fallback;
    }
    build() {
      if (this.keydown) document.removeEventListener("keydown", this.keydown);
      this.resizeObserver?.disconnect();
      document.getElementById(ROOT_ID)?.remove();
      this.root = document.createElement("div");
      this.root.id = ROOT_ID;
      this.root.hidden = true;
      this.root.innerHTML = `<div class="ep-shell" role="dialog" aria-modal="true" aria-label="MWITools Asset Center">
      <aside class="ep-sidebar"><div class="ep-brand"><strong>MWITools</strong><small>${this.t("资产中心", "Asset Center")}</small></div>
        <div class="ep-nav-label">${this.t("分析", "ANALYSIS")}</div>
        ${this.nav("chart", "▥", this.t("图表总览", "Chart Overview"))}
        ${this.nav("analysis", "◔", this.t("分项分析", "Analysis"))}
        ${this.nav("stats", "▦", this.t("统计报表", "Statistics"))}
        <div class="ep-nav-label">${this.t("记录", "RECORD")}</div>
        ${this.nav("achievements", "♕", this.t("成就", "Achievements"))}
        <div class="ep-nav-label">${this.t("管理", "MANAGE")}</div>
        ${this.nav("data", "◫", this.t("数据管理", "Data"))}
        ${this.nav("tags", "◇", this.t("管理标签", "Tags"))}
        <div class="ep-nav-footer">${this.nav("settings", "⚙", this.t("设置/存档", "Settings"))}<button class="ep-nav-item" data-language><span class="ep-nav-icon">文</span><span class="ep-nav-text">${this.isZH() ? "EN" : "中文"}</span></button></div>
      </aside><main class="ep-main"><header class="ep-top"><div class="ep-top-main"><div class="ep-top-title"></div><div class="ep-top-sub"></div></div><button class="ep-close" data-close aria-label="${this.t("关闭", "Close")}">✕</button></header><div class="ep-page"></div></main>
      <input type="file" data-import-file accept="application/json" hidden>
      <dialog data-edit-dialog><h3 data-editor-title>${this.t("编辑分项资产", "Edit components")}</h3><label data-insert-date-wrap hidden>${this.t("插入日期", "Insert date")}<input type="date" data-insert-date></label><div class="ep-edit-grid ep-section">${ASSET_COMPONENT_KEYS.map((key) => `<label>${this.t(ASSET_COMPONENT_META[key].zh, ASSET_COMPONENT_META[key].en)}<input type="number" min="0" step="any" data-edit-component="${key}"></label>`).join("")}</div><div class="ep-toolbar"><span class="ep-spacer"></span><button class="ep-btn" data-edit-cancel>${this.t("取消", "Cancel")}</button><button class="ep-btn" data-edit-save>${this.t("保存", "Save")}</button></div></dialog>
    </div>`;
      document.body.append(this.root);
      const windowSize = this.store.getPreferences().windowSize;
      const shell2 = this.root.querySelector(".ep-shell");
      if (windowSize?.w && windowSize?.h) {
        shell2.style.width = `${windowSize.w}px`;
        shell2.style.height = `${windowSize.h}px`;
      }
      if (typeof globalThis.ResizeObserver === "function") {
        this.resizeObserver = new globalThis.ResizeObserver(() => {
          if (this.root.hidden || globalThis.innerWidth <= 820) return;
          const rect = shell2.getBoundingClientRect();
          this.pendingWindowSize = {
            w: Math.round(rect.width),
            h: Math.round(rect.height)
          };
        });
        this.resizeObserver.observe(shell2);
      }
      this.bind();
      this.applyTheme();
    }
    nav(route, icon, label) {
      return `<button class="ep-nav-item" data-route="${route}"><span class="ep-nav-icon">${icon}</span><span class="ep-nav-text">${label}</span></button>`;
    }
    bind() {
      this.root.addEventListener("click", (event) => {
        if (event.target === this.root) this.close();
        const route = event.target.closest("[data-route]")?.dataset.route;
        if (route) {
          this.route = route;
          this.render();
        }
        if (event.target.closest("[data-close]")) this.close();
        if (event.target.closest("[data-language]")) {
          const previousFocus = this.previousFocus;
          this.store.setPreferences({ language: this.isZH() ? "en" : "zh" });
          this.build();
          this.previousFocus = previousFocus;
          this.open(true);
        }
      });
      this.keydown = (event) => {
        if (event.key === "Escape" && !this.root.hidden) this.close();
      };
      document.addEventListener("keydown", this.keydown);
      this.root.querySelector("[data-edit-cancel]").addEventListener(
        "click",
        () => this.root.querySelector("[data-edit-dialog]").close()
      );
      this.root.querySelector("[data-edit-save]").addEventListener("click", () => this.saveEditor());
    }
    open(preserveFocus = false) {
      if (!preserveFocus) this.previousFocus = document.activeElement;
      this.root.hidden = false;
      document.body.dataset.mwitoolsAssetCenterOpen = "true";
      document.body.style.overflow = "hidden";
      this.onVisibilityChange?.(true);
      this.root.querySelector("[data-close]").focus();
      this.render();
    }
    close() {
      const wasOpen = !this.root.hidden;
      this.root.hidden = true;
      delete document.body.dataset.mwitoolsAssetCenterOpen;
      document.body.style.overflow = "";
      this.chart?.destroy();
      this.chart = null;
      this.hiddenChartDatasets.clear();
      if (this.pendingWindowSize) {
        this.store.setPreferences({ windowSize: this.pendingWindowSize });
        this.pendingWindowSize = null;
      }
      this.previousFocus?.focus?.();
      if (wasOpen) this.onVisibilityChange?.(false);
    }
    isOpen() {
      return !this.root.hidden;
    }
    update(snapshot) {
      this.snapshot = snapshot ?? this.snapshot;
      if (!this.root.hidden && this.route === "chart") this.updateChartSummary();
    }
    applyTheme() {
      const prefs = this.store.getPreferences();
      this.root.classList.toggle("ep-light", prefs.themeMode === "light");
      this.root.classList.toggle("ep-glass-heart", prefs.glassHeartMode);
      this.root.style.setProperty("--ep-light-h", String(prefs.lightBg.h));
      this.root.style.setProperty("--ep-light-s", `${prefs.lightBg.s}%`);
    }
    routeCopy() {
      return {
        chart: [
          this.t("图表总览", "Chart Overview"),
          this.t("净资产历史 · 盈亏分析 · 目标预测", "Net worth · P/L · goals")
        ],
        analysis: [
          this.t("分项分析", "Component Analysis"),
          this.t("占比 · 贡献 · 排名", "Allocation · contribution · ranking")
        ],
        stats: [
          this.t("统计报表", "Statistics"),
          this.t("周期统计 · 盈亏日历", "Period metrics · P/L calendar")
        ],
        achievements: [
          this.t("成就 & 里程碑", "Achievements"),
          this.t("历史进度与特殊记录", "Historical milestones")
        ],
        data: [
          this.t("数据管理", "Data Management"),
          this.t("编辑 · 清理 · 备份", "Edit · cleanup · backup")
        ],
        tags: [
          this.t("管理标签", "Tags"),
          this.t("为重要日期添加事件", "Annotate important dates")
        ],
        settings: [
          this.t("设置/存档", "Settings"),
          this.t("外观 · 图表 · 关于", "Appearance · charts · about")
        ]
      }[this.route];
    }
    render() {
      if (!this.root?.isConnected) return;
      this.chart?.destroy();
      this.chart = null;
      this.root.querySelectorAll("[data-route]").forEach(
        (button) => button.classList.toggle("active", button.dataset.route === this.route)
      );
      const [title, subtitle] = this.routeCopy();
      this.root.querySelector(".ep-top-title").textContent = title;
      this.root.querySelector(".ep-top-sub").textContent = subtitle;
      const page = this.root.querySelector(".ep-page");
      if (this.route === "chart") this.renderChartPage(page);
      else if (this.route === "analysis") this.renderAnalysisPage(page);
      else if (this.route === "stats") this.renderStatsPage(page);
      else if (this.route === "achievements") this.renderAchievementsPage(page);
      else if (this.route === "data") this.renderDataPage(page);
      else if (this.route === "tags") this.renderTagsPage(page);
      else this.renderSettingsPage(page);
    }
    summaryValues() {
      const entries = this.store.list(this.scopeKey);
      const current = this.snapshot?.values ?? entries.at(-1)?.[1]?.values ?? {};
      const previous = entries.length > 1 ? entries.at(-2)?.[1]?.values ?? {} : {};
      const change = Number.isFinite(current.total) && Number.isFinite(previous.total) ? current.total - previous.total : null;
      return { entries, current, previous, change };
    }
    metric(label, value, className = "", liveKey = "") {
      return `<div class="ep-card ep-metric"><span>${label}</span><strong class="${className}"${liveKey ? ` data-live-metric="${liveKey}"` : ""} title="${Number.isFinite(value) ? escapeHtml(runtime.api.formatExactNumber?.(value, 0) ?? value) : ""}">${this.format(value, className !== "")}</strong></div>`;
    }
    chartSummaryValues() {
      const { current, previous, change } = this.summaryValues();
      return [
        ["current", current.total, ""],
        ["change", change, change >= 0 ? "pos" : "neg"],
        ["percent", previous.total ? change / previous.total * 100 : null, ""],
        ["average", this.store.sevenDayAverage(void 0, this.scopeKey), "pos"]
      ];
    }
    updateChartSummary() {
      for (const [key, value, className] of this.chartSummaryValues()) {
        const metric4 = this.root.querySelector(`[data-live-metric="${key}"]`);
        if (!metric4) continue;
        metric4.classList.toggle("pos", className === "pos");
        metric4.classList.toggle("neg", className === "neg");
        const title = Number.isFinite(value) ? String(runtime.api.formatExactNumber?.(value, 0) ?? value) : "";
        const text = this.format(value, className !== "");
        if (metric4.title !== title) metric4.title = title;
        if (metric4.textContent !== text) metric4.textContent = text;
      }
    }
    renderChartPage(page) {
      const { entries } = this.summaryValues();
      const [current, change, percent, average] = this.chartSummaryValues();
      const prefs = this.store.getPreferences();
      const target = this.store.getGoalTarget(this.scopeKey);
      page.innerHTML = `<div class="ep-grid">${this.metric(this.t("当前净资产", "Current net worth"), current[1], current[2], current[0])}${this.metric(this.t("本期盈亏", "Current P/L"), change[1], change[2], change[0])}${this.metric(this.t("盈亏比例", "P/L percentage"), percent[1], percent[2], percent[0])}${this.metric(this.t("近 7 日平均", "7-day average"), average[1], average[2], average[0])}</div>
      <section class="ep-card ep-section"><div class="ep-toolbar"><button class="ep-btn" data-chart-mode="total">${this.t("净资产", "Net worth")}</button><button class="ep-btn" data-chart-mode="profit">${this.t("盈亏", "P/L")}</button><button class="ep-btn" data-chart-mode="breakdown">${this.t("分项资产", "Components")}</button><span class="ep-spacer"></span>${[7, 15, 30].map((range) => `<button class="ep-btn" data-chart-range="${range}">${range}${this.t("天", "d")}</button>`).join("")}<button class="ep-btn" data-chart-range="all">${this.t("全部", "All")}</button><button class="ep-btn" data-reset-zoom>${this.t("重置缩放", "Reset zoom")}</button></div><div class="ep-chart"><canvas data-center-chart></canvas><div data-chart-fallback></div></div></section>
      <section class="ep-card ep-section"><div class="ep-section-title">🎯 ${this.t("目标追踪与蒙特卡洛", "Goal & Monte Carlo")}</div><div class="ep-section-body"><div class="ep-form"><label>${this.t("目标净资产", "Target net worth")}<input data-goal type="number" min="1" value="${target ?? ""}"></label><button class="ep-btn" data-save-goal>${this.t("保存目标", "Save target")}</button><button class="ep-btn" data-simulate>${this.t("运行 90 日模拟", "Run 90-day simulation")}</button></div><div data-simulation></div></div></section>
      <p class="ep-disclaimer">${this.t("盈亏按资产估值变化计算,包含市场波动,并非已实现交易利润;预测仅供参考和娱乐。", "P/L includes valuation changes and is not realized profit. Forecasts are for reference and entertainment only.")}</p>`;
      page.querySelectorAll("[data-chart-mode]").forEach((button) => {
        button.classList.toggle(
          "active",
          button.dataset.chartMode === this.chartMode
        );
        button.addEventListener("click", () => {
          this.chartMode = button.dataset.chartMode;
          this.drawCenterChart();
        });
      });
      page.querySelectorAll("[data-chart-range]").forEach((button) => {
        const range = button.dataset.chartRange === "all" ? null : Number(button.dataset.chartRange);
        button.classList.toggle("active", range === this.chartRange);
        button.addEventListener("click", () => {
          this.chartRange = range;
          this.drawCenterChart();
        });
      });
      page.querySelector("[data-reset-zoom]").addEventListener("click", () => this.chart?.resetZoom());
      page.querySelector("[data-save-goal]").addEventListener("click", () => {
        this.store.setGoalTarget(
          page.querySelector("[data-goal]").value,
          this.scopeKey
        );
        this.render();
      });
      page.querySelector("[data-simulate]").addEventListener("click", () => {
        const result = simulateNetWorth(entries, {
          target: this.store.getGoalTarget(this.scopeKey)
        });
        this.renderSimulation(page.querySelector("[data-simulation]"), result);
      });
      this.chart = new AssetHistoryChart(
        page.querySelector("[data-center-chart]"),
        page.querySelector("[data-chart-fallback]"),
        { hiddenDatasets: this.hiddenChartDatasets }
      );
      this.drawCenterChart();
    }
    drawCenterChart() {
      const prefs = this.store.getPreferences();
      this.root.querySelectorAll("[data-chart-mode]").forEach(
        (button) => button.classList.toggle(
          "active",
          button.dataset.chartMode === this.chartMode
        )
      );
      this.root.querySelectorAll("[data-chart-range]").forEach((button) => {
        const range = button.dataset.chartRange === "all" ? null : Number(button.dataset.chartRange);
        button.classList.toggle("active", range === this.chartRange);
      });
      this.chart?.renderWithOptions(this.store.list(this.scopeKey), {
        mode: this.chartMode,
        range: this.chartRange,
        maWindow: prefs.chart.maWindow,
        lineTension: prefs.chart.lineTension,
        tags: this.store.getRole(this.scopeKey).tagVisibility ? this.store.listTags(this.scopeKey).map((tag) => ({ ...tag, color: this.tagColor(tag.type) })) : []
      });
    }
    renderSimulation(host, result) {
      if (result.status !== "complete") {
        host.innerHTML = `<p>${this.t("数据不足,至少需要 7 条有效记录。", "At least 7 valid records are required.")}</p>`;
        return;
      }
      const finalBands = ["p10", "p25", "p50", "p75", "p90"];
      host.innerHTML = `<div class="ep-sim-grid ep-section">${this.metric(this.t("日均增长率 %", "Daily growth %"), result.dailyGrowthPercent, result.dailyGrowthPercent >= 0 ? "pos" : "neg")}${this.metric(this.t("日波动率 %", "Daily volatility %"), result.dailyVolatilityPercent)}${this.metric(this.t("翻倍天数", "Doubling days"), result.doublingDays)}${this.metric(this.t("90 日中位数", "90d median"), result.series.p50.at(-1))}</div><div class="ep-sim-band">${finalBands.map((band) => `<div><small>${band.toUpperCase()}</small><strong>${this.format(result.series[band].at(-1))}</strong></div>`).join("")}</div>${result.target ? `<div class="ep-toolbar">${Object.entries(result.probabilities).map(
        ([day, probability]) => `<div class="ep-prob"><strong>${probability.toFixed(1)}%</strong><small>${day}${this.t(" 天达成", "d target")}</small></div>`
      ).join("")}</div>` : ""}`;
    }
    renderAnalysisPage(page) {
      const analysis = componentAnalysis(
        this.store.list(this.scopeKey),
        this.analysisRange
      );
      page.innerHTML = `<section class="ep-card"><div class="ep-toolbar">${[7, 30].map((range) => `<button class="ep-btn ${this.analysisRange === range ? "active" : ""}" data-analysis-range="${range}">${range}${this.t("天", "d")}</button>`).join("")}<button class="ep-btn ${this.analysisRange === null ? "active" : ""}" data-analysis-range="all">${this.t("全部", "All")}</button></div><div class="ep-section-body">${analysis ? `<div class="ep-analysis-list">${analysis.components.map((item) => `<div class="ep-analysis-row"><strong>${this.t(item.zh, item.en)}</strong><div class="ep-bar"><i style="width:${Math.max(2, Math.min(100, Math.abs(item.share ?? 0)))}%;background:${item.color}"></i></div><span class="${item.change >= 0 ? "pos" : "neg"}">${this.format(item.change, true)}</span><small>${Number.isFinite(item.share) ? item.share.toFixed(1) + "%" : "—"}</small></div>`).join("")}</div><div class="ep-grid ep-section">${this.metric(this.t("总变动", "Total change"), analysis.totalChange, analysis.totalChange >= 0 ? "pos" : "neg")}${this.metric(this.t("已追踪分项", "Tracked"), analysis.trackedChange, analysis.trackedChange >= 0 ? "pos" : "neg")}${this.metric(this.t("其他/未追踪", "Other/untracked"), analysis.untrackedChange, analysis.untrackedChange >= 0 ? "pos" : "neg")}${this.metric(this.t("跨度天数", "Calendar days"), analysis.gapDays)}</div>` : `<p>${this.t("至少需要两条记录。", "At least two records are required.")}</p>`}</div></section>`;
      page.querySelectorAll("[data-analysis-range]").forEach(
        (button) => button.addEventListener("click", () => {
          this.analysisRange = button.dataset.analysisRange === "all" ? null : Number(button.dataset.analysisRange);
          this.render();
        })
      );
    }
    renderStatsPage(page) {
      const range = this.reportMode === "week" ? weekRange(this.reportDate) : monthRange(this.reportDate);
      const entries = this.store.list(this.scopeKey);
      const stats = periodStatistics(entries, range);
      const heatmap = buildHeatmap(entries);
      const firstDay = new Date(range.year, range.month, 1).getDay();
      const offset = firstDay === 0 ? 6 : firstDay - 1;
      const count = new Date(range.year, range.month + 1, 0).getDate();
      const changes = Object.values(heatmap).filter(
        ({ date }) => date >= range.start && date <= range.end
      );
      const max = Math.max(
        1,
        ...changes.map(({ totalChange }) => Math.abs(totalChange))
      );
      const cells = Array.from(
        { length: offset },
        () => `<div class="ep-day empty"></div>`
      );
      for (let day = 1; day <= count; day += 1) {
        const date = `${range.year}-${String(range.month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
        const change = heatmap[date];
        const strength = change ? Math.abs(change.totalChange) / max : 0;
        const style = this.store.getPreferences().heatmapStyle;
        const alpha = style === "A" ? 0.28 : style === "D" ? 0.18 + 0.7 * strength : 0.12 + 0.5 * strength;
        const positive = style === "C" ? [34, 197, 94] : style === "D" ? [14, 165, 233] : [16, 185, 129];
        const negative = style === "C" ? [239, 68, 68] : style === "D" ? [168, 85, 247] : [244, 63, 94];
        const rgb = change?.totalChange >= 0 ? positive : negative;
        const color = `rgba(${rgb.join(",")},${alpha})`;
        cells.push(
          `<div class="ep-day ${date >= range.start && date <= range.end ? "in-period" : ""}" style="${change ? `background:${color}` : ""}"><span>${day}</span><strong class="${change?.totalChange >= 0 ? "pos" : "neg"}">${change ? this.format(change.totalChange, true) : ""}</strong></div>`
        );
      }
      const rangeLabel = this.reportMode === "week" ? `${range.start} — ${range.end}` : `${range.year}-${String(range.month + 1).padStart(2, "0")}`;
      page.innerHTML = `<div class="ep-toolbar"><button class="ep-btn ${this.reportMode === "week" ? "active" : ""}" data-report-mode="week">${this.t("周报", "Weekly")}</button><button class="ep-btn ${this.reportMode === "month" ? "active" : ""}" data-report-mode="month">${this.t("月报", "Monthly")}</button><span class="ep-spacer"></span><button class="ep-btn" data-period-shift="-1">‹</button><strong>${rangeLabel}</strong><button class="ep-btn" data-period-shift="1">›</button></div>${stats ? `<div class="ep-grid">${this.metric(this.t("总盈亏", "Total P/L"), stats.totalProfit, stats.totalProfit >= 0 ? "pos" : "neg")}${this.metric(this.t("日均盈亏", "Average/day"), stats.averagePerDay, stats.averagePerDay >= 0 ? "pos" : "neg")}${this.metric(this.t("胜率 %", "Win rate %"), stats.winRate)}${this.metric(this.t("增长率 %", "Growth %"), stats.growthPercent, stats.growthPercent >= 0 ? "pos" : "neg")}</div>` : ""}<section class="ep-card ep-section"><div class="ep-section-title">${this.t("盈亏日历", "P/L Calendar")}</div><div class="ep-section-body"><div class="ep-heatmap">${cells.join("")}</div></div></section>${stats ? `<section class="ep-card ep-section"><div class="ep-section-title">${this.t("每日明细", "Daily details")}</div><div class="ep-section-body"><table><thead><tr><th>${this.t("日期", "Date")}</th><th>${this.t("盈亏", "P/L")}</th><th>${this.t("跨度", "Gap")}</th></tr></thead><tbody>${stats.changes.map((item) => `<tr><td>${item.date}</td><td class="${item.totalChange >= 0 ? "pos" : "neg"}">${this.format(item.totalChange, true)}</td><td>${item.gapDays}</td></tr>`).join("")}</tbody></table></div></section>` : `<p>${this.t("所选周期暂无数据。", "No data for this period.")}</p>`}`;
      page.querySelectorAll("[data-report-mode]").forEach(
        (button) => button.addEventListener("click", () => {
          this.reportMode = button.dataset.reportMode;
          this.render();
        })
      );
      page.querySelectorAll("[data-period-shift]").forEach(
        (button) => button.addEventListener("click", () => {
          const direction = Number(button.dataset.periodShift);
          if (this.reportMode === "week") {
            this.reportDate = new Date(this.reportDate);
            this.reportDate.setDate(this.reportDate.getDate() + direction * 7);
          } else {
            this.reportDate = new Date(
              this.reportDate.getFullYear(),
              this.reportDate.getMonth() + direction,
              1
            );
          }
          this.render();
        })
      );
    }
    renderAchievementsPage(page) {
      const persisted = this.store.getAchievements(this.scopeKey);
      const achievements = calculateAchievements(
        this.store.list(this.scopeKey),
        persisted
      );
      this.store.syncAchievements(achievements, this.scopeKey);
      const unlocked = achievements.filter((item) => item.unlocked).length;
      page.innerHTML = `<div class="ep-grid">${this.metric(this.t("已解锁", "Unlocked"), unlocked)}${this.metric(this.t("全部成就", "All achievements"), achievements.length)}${this.metric(this.t("完成度 %", "Completion %"), achievements.length ? unlocked / achievements.length * 100 : 0)}${this.metric(this.t("历史天数", "History days"), this.store.list(this.scopeKey).length)}</div><div class="ep-achievements ep-section">${achievements.map((item) => `<article class="ep-achievement ${item.unlocked ? "" : "locked"}"><span class="ep-achievement-icon">${item.icon}</span><div class="ep-grow"><strong>${this.isZH() ? item.zhName : item.enName}</strong><small>${this.isZH() ? item.zhDescription : item.enDescription}</small>${item.date ? `<small>${item.date}</small>` : ""}</div><span>${item.unlocked ? "✓" : "🔒"}</span></article>`).join("")}</div>`;
    }
    renderDataPage(page) {
      const chronological = this.store.list(this.scopeKey);
      const entries = chronological.map((entry, index) => ({ entry, previous: chronological[index - 1] })).reverse();
      page.innerHTML = `<section class="ep-card"><div class="ep-toolbar"><button class="ep-btn" data-export>📤 ${this.t("导出备份", "Export")}</button><button class="ep-btn" data-import>📥 ${this.t("导入备份", "Import")}</button><select data-import-mode><option value="merge">${this.t("合并当前角色", "Merge current role")}</option><option value="replace">${this.t("替换当前角色", "Replace current role")}</option><option value="full">${this.t("完整恢复", "Full restore")}</option></select><span class="ep-spacer"></span><button class="ep-btn danger" data-clean>${this.t("清理无效", "Clean invalid")}</button><button class="ep-btn danger" data-anomalies>${this.t("删除反转异常", "Remove anomalies")}</button></div><div class="ep-section-body"><table><thead><tr><th>${this.t("日期", "Date")}</th><th>${this.t("总资产", "Total")}</th><th>${this.t("操作", "Actions")}</th></tr></thead><tbody>${entries.map(({ entry: [date, record], previous }) => {
        const insertButton = previous && shiftDayKey(previous[0], 1) < date ? `<button class="ep-btn" data-insert-after="${previous[0]}" data-insert-before="${date}">${this.t("插入", "Insert")}</button> ` : "";
        return `<tr><td>${date}</td><td class="mono">${this.format(record.values.total)}</td><td>${insertButton}<button class="ep-btn" data-edit-day="${date}">${this.t("编辑", "Edit")}</button> <button class="ep-btn danger" data-delete-day="${date}">${this.t("删除", "Delete")}</button></td></tr>`;
      }).join("")}</tbody></table></div></section>`;
      page.querySelector("[data-export]").addEventListener("click", () => this.downloadBackup());
      page.querySelector("[data-import]").addEventListener("click", () => {
        this.pendingImportMode = page.querySelector("[data-import-mode]").value;
        this.root.querySelector("[data-import-file]").click();
      });
      this.root.querySelector("[data-import-file]").onchange = async (event) => {
        const file = event.target.files?.[0];
        if (!file) return;
        try {
          this.store.importBackup(JSON.parse(await file.text()), {
            mode: this.pendingImportMode,
            scopeKey: this.scopeKey
          });
          this.changed();
        } catch (error) {
          globalThis.alert?.(
            `${this.t("导入失败", "Import failed")}: ${error.message}`
          );
        }
        event.target.value = "";
      };
      page.querySelector("[data-clean]").addEventListener("click", () => {
        this.store.cleanupInvalid(this.scopeKey);
        this.changed();
      });
      page.querySelector("[data-anomalies]").addEventListener("click", () => {
        const values = this.store.detectAnomalies(this.scopeKey);
        if (!values.length)
          return globalThis.alert?.(
            this.t("未发现反转异常。", "No reversal anomalies found.")
          );
        if (globalThis.confirm?.(
          `${this.t("删除以下异常日期?", "Delete these dates?")}
${values.map((item) => item.date).join("\n")}`
        )) {
          values.forEach(
            (item) => this.store.deleteDay(item.date, this.scopeKey)
          );
          this.changed();
        }
      });
      page.querySelectorAll("[data-insert-after]").forEach(
        (button) => button.addEventListener(
          "click",
          () => this.openInsertEditor(
            button.dataset.insertAfter,
            button.dataset.insertBefore
          )
        )
      );
      page.querySelectorAll("[data-edit-day]").forEach(
        (button) => button.addEventListener(
          "click",
          () => this.openEditor(button.dataset.editDay)
        )
      );
      page.querySelectorAll("[data-delete-day]").forEach(
        (button) => button.addEventListener("click", () => {
          if (globalThis.confirm?.(
            `${this.t("删除", "Delete")} ${button.dataset.deleteDay}?`
          )) {
            this.store.deleteDay(button.dataset.deleteDay, this.scopeKey);
            this.changed();
          }
        })
      );
    }
    openEditor(date) {
      const dialog = this.root.querySelector("[data-edit-dialog]");
      dialog.dataset.mode = "edit";
      dialog.dataset.date = date;
      delete dialog.dataset.olderDate;
      delete dialog.dataset.newerDate;
      dialog.querySelector("[data-editor-title]").textContent = this.t(
        "编辑分项资产",
        "Edit components"
      );
      dialog.querySelector("[data-insert-date-wrap]").hidden = true;
      const values = this.store.getRole(this.scopeKey).days[date]?.values ?? {};
      dialog.querySelectorAll("[data-edit-component]").forEach((input) => {
        input.value = Number.isFinite(values[input.dataset.editComponent]) ? values[input.dataset.editComponent] : "";
      });
      dialog.showModal();
    }
    openInsertEditor(olderDate, newerDate) {
      const dialog = this.root.querySelector("[data-edit-dialog]");
      const dateInput = dialog.querySelector("[data-insert-date]");
      const minimum = shiftDayKey(olderDate, 1);
      const maximum = shiftDayKey(newerDate, -1);
      dialog.dataset.mode = "insert";
      dialog.dataset.olderDate = olderDate;
      dialog.dataset.newerDate = newerDate;
      delete dialog.dataset.date;
      dialog.querySelector("[data-editor-title]").textContent = this.t(
        "插入历史资产",
        "Insert historical assets"
      );
      dialog.querySelector("[data-insert-date-wrap]").hidden = false;
      dateInput.min = minimum;
      dateInput.max = maximum;
      dateInput.value = minimum;
      const values = this.store.getRole(this.scopeKey).days[olderDate]?.values ?? {};
      dialog.querySelectorAll("[data-edit-component]").forEach((input) => {
        input.value = Number.isFinite(values[input.dataset.editComponent]) ? values[input.dataset.editComponent] : "";
      });
      dialog.showModal();
    }
    saveEditor() {
      const dialog = this.root.querySelector("[data-edit-dialog]");
      const values = Object.fromEntries(
        [...dialog.querySelectorAll("[data-edit-component]")].map((input) => [
          input.dataset.editComponent,
          input.value.trim() === "" ? null : Number(input.value)
        ])
      );
      if (!ASSET_COMPONENT_KEYS.every(
        (key) => Number.isFinite(values[key]) && values[key] >= 0
      ))
        return globalThis.alert?.(
          this.t("请填写全部七个分项。", "Enter all seven components.")
        );
      if (dialog.dataset.mode === "insert") {
        const dateInput = dialog.querySelector("[data-insert-date]");
        const dayKey = dateInput.value;
        if (!dayKey || dayKey < dateInput.min || dayKey > dateInput.max) {
          return globalThis.alert?.(
            this.t(
              "请选择两条记录之间的缺失日期。",
              "Choose a missing date between the two records."
            )
          );
        }
        try {
          this.store.insertDay(dayKey, values, this.scopeKey);
        } catch {
          return globalThis.alert?.(
            this.t(
              "无法插入:日期已存在或数据无效。",
              "Could not insert: the date already exists or the data is invalid."
            )
          );
        }
      } else {
        this.store.updateDay(dialog.dataset.date, values, this.scopeKey);
      }
      dialog.close();
      this.changed();
    }
    renderTagsPage(page) {
      const tags = this.store.listTags(this.scopeKey).slice().reverse();
      const today = new Date(Date.now() + 8 * 36e5).toISOString().slice(0, 10);
      page.innerHTML = `<section class="ep-card"><div class="ep-section-body"><div class="ep-form"><label>${this.t("日期", "Date")}<input type="date" data-tag-date value="${today}"></label><label>${this.t("分类", "Category")}<select data-tag-type><option value="">${this.t("无分类", "None")}</option>${TAG_TYPES.map(([key, zh, en]) => `<option value="${key}">${this.t(zh, en)}</option>`).join("")}</select></label><label class="ep-grow">${this.t("标签内容", "Tag text")}<input data-tag-text maxlength="60"></label><button class="ep-btn" data-add-tag>${this.t("添加标签", "Add tag")}</button><label><input type="checkbox" data-tag-visible ${this.store.getRole(this.scopeKey).tagVisibility ? "checked" : ""}>${this.t("图表显示标签", "Show on charts")}</label></div></div></section><div class="ep-tags ep-section">${tags.map((tag) => {
        const meta = TAG_TYPES.find(([key]) => key === tag.type);
        return `<article class="ep-tag-row"><i style="width:8px;height:8px;border-radius:50%;background:${this.tagColor(tag.type)}"></i><div class="ep-grow"><strong>${escapeHtml(tag.text)}</strong><small>${tag.date} · ${meta ? this.t(meta[1], meta[2]) : this.t("无分类", "None")}</small></div><button class="ep-btn" data-edit-tag="${escapeHtml(tag.id)}">✎</button><button class="ep-btn danger" data-delete-tag="${escapeHtml(tag.id)}">×</button></article>`;
      }).join("") || `<p>${this.t("暂无标签。", "No tags yet.")}</p>`}</div>`;
      page.querySelector("[data-add-tag]").addEventListener("click", () => {
        const date = page.querySelector("[data-tag-date]").value;
        const text = page.querySelector("[data-tag-text]").value;
        const type = page.querySelector("[data-tag-type]").value;
        if (this.store.addTag(date, text, type, this.scopeKey)) this.changed();
      });
      page.querySelector("[data-tag-visible]").addEventListener("change", (event) => {
        this.store.setTagVisibility(event.target.checked, this.scopeKey);
      });
      page.querySelectorAll("[data-edit-tag]").forEach(
        (button) => button.addEventListener("click", () => {
          const current = tags.find((tag) => tag.id === button.dataset.editTag);
          const text = globalThis.prompt?.(
            this.t("修改标签", "Edit tag"),
            current?.text
          );
          if (text && this.store.updateTag(button.dataset.editTag, { text }, this.scopeKey))
            this.changed();
        })
      );
      page.querySelectorAll("[data-delete-tag]").forEach(
        (button) => button.addEventListener("click", () => {
          this.store.deleteTag(button.dataset.deleteTag, this.scopeKey);
          this.changed();
        })
      );
    }
    renderSettingsPage(page) {
      const prefs = this.store.getPreferences();
      page.innerHTML = `<section class="ep-card"><div class="ep-section-title">🎨 ${this.t("外观", "Appearance")}</div><div class="ep-section-body"><div class="ep-setting"><div><strong>${this.t("主题模式", "Theme")}</strong><small>${this.t("切换深色/浅色资产中心", "Dark or light asset center")}</small></div><select data-setting="themeMode"><option value="dark" ${prefs.themeMode === "dark" ? "selected" : ""}>${this.t("深色", "Dark")}</option><option value="light" ${prefs.themeMode === "light" ? "selected" : ""}>${this.t("浅色", "Light")}</option></select></div><div class="ep-setting"><div><strong>${this.t("玻璃心模式", "Glass-heart mode")}</strong><small>${this.t("亏损使用柔和色彩", "Use softer loss colors")}</small></div><input type="checkbox" data-setting="glassHeartMode" ${prefs.glassHeartMode ? "checked" : ""}></div><div class="ep-setting"><div><strong>${this.t("热力图样式", "Heatmap style")}</strong></div><select data-setting="heatmapStyle">${["A", "B", "C", "D"].map((value) => `<option ${prefs.heatmapStyle === value ? "selected" : ""}>${value}</option>`).join("")}</select></div><div class="ep-setting"><div><strong>${this.t("浅色主题色相", "Light-theme hue")}</strong></div><input type="range" min="0" max="359" data-light-setting="h" value="${prefs.lightBg.h}"></div><div class="ep-setting"><div><strong>${this.t("浅色主题饱和度", "Light-theme saturation")}</strong></div><input type="range" min="0" max="100" data-light-setting="s" value="${prefs.lightBg.s}"></div></div></section><section class="ep-card ep-section"><div class="ep-section-title">📊 ${this.t("图表自定义", "Chart settings")}</div><div class="ep-section-body"><div class="ep-setting"><div><strong>${this.t("默认图表", "Default chart")}</strong></div><select data-chart-setting="defaultView"><option value="networth" ${prefs.chart.defaultView === "networth" ? "selected" : ""}>${this.t("净资产", "Net worth")}</option><option value="profit" ${prefs.chart.defaultView === "profit" ? "selected" : ""}>${this.t("盈亏", "P/L")}</option><option value="breakdown" ${prefs.chart.defaultView === "breakdown" ? "selected" : ""}>${this.t("分项", "Components")}</option></select></div><div class="ep-setting"><div><strong>${this.t("盈亏均线天数", "Moving-average days")}</strong></div><input type="number" min="2" max="90" data-chart-setting="maWindow" value="${prefs.chart.maWindow}"></div><div class="ep-setting"><div><strong>${this.t("默认时间范围", "Default range")}</strong></div><select data-chart-setting="defaultRange"><option value="7" ${prefs.chart.defaultRange === 7 ? "selected" : ""}>7</option><option value="15" ${prefs.chart.defaultRange === 15 ? "selected" : ""}>15</option><option value="30" ${prefs.chart.defaultRange === 30 ? "selected" : ""}>30</option><option value="all" ${prefs.chart.defaultRange === null ? "selected" : ""}>${this.t("全部", "All")}</option></select></div><div class="ep-setting"><div><strong>${this.t("曲线平滑度", "Line tension")}</strong></div><select data-chart-setting="lineTension">${[0, 0.15, 0.25, 0.4].map((value) => `<option value="${value}" ${prefs.chart.lineTension === value ? "selected" : ""}>${value}</option>`).join("")}</select></div></div></section><section class="ep-card ep-section"><div class="ep-section-title">ℹ️ ${this.t("关于与许可", "About & License")}</div><div class="ep-section-body"><p>MWITools ${this.t("资产中心界面基于 Everyday Profit Pro 合并改造。", "Asset Center UI is adapted from Everyday Profit Pro.")}</p><p>Copyright © 2025 VictoryWinWinWin, PaperCat, SuXingX<br>Copyright © 2026 ColaCola</p><p>MIT License — Permission is granted to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies with the copyright and permission notice retained.</p><button class="ep-btn danger" data-reset-settings>${this.t("恢复默认设置", "Reset settings")}</button></div></section>`;
      page.querySelectorAll(".ep-section-body").item(2)?.insertAdjacentHTML(
        "beforeend",
        `<details class="ep-section"><summary>${this.t("完整 MIT 许可", "Full MIT License")}</summary><pre style="white-space:pre-wrap;font-size:10px;color:hsl(var(--ep-muted))">${escapeHtml(EP_MIT_LICENSE)}</pre></details>`
      );
      page.querySelector(".ep-section-body").insertAdjacentHTML(
        "beforeend",
        `<div class="ep-setting"><div><strong>${this.t("标签颜色", "Tag colors")}</strong><small>${this.t("同时用于图表日期标注", "Also used for chart annotations")}</small></div><div class="ep-form">${TAG_TYPES.map(([key, zh, en, color]) => `<label>${this.t(zh, en)}<input type="color" data-tag-color="${key}" value="${prefs.tagColors[key] ?? color}"></label>`).join("")}</div></div>`
      );
      page.querySelectorAll("[data-setting]").forEach(
        (input) => input.addEventListener("change", () => {
          const key = input.dataset.setting;
          const value = input.type === "checkbox" ? input.checked : input.value;
          this.store.setPreferences({ [key]: value });
          this.applyTheme();
          this.render();
        })
      );
      page.querySelectorAll("[data-chart-setting]").forEach(
        (input) => input.addEventListener("change", () => {
          let value = input.value;
          if (input.dataset.chartSetting === "defaultRange")
            value = value === "all" ? null : Number(value);
          else if (input.dataset.chartSetting !== "defaultView")
            value = Number(value);
          this.store.setPreferences({
            chart: { [input.dataset.chartSetting]: value }
          });
          this.render();
        })
      );
      page.querySelectorAll("[data-light-setting]").forEach(
        (input) => input.addEventListener("change", () => {
          this.store.setPreferences({
            lightBg: { [input.dataset.lightSetting]: Number(input.value) }
          });
          this.applyTheme();
        })
      );
      page.querySelectorAll("[data-tag-color]").forEach(
        (input) => input.addEventListener("change", () => {
          this.store.setPreferences({
            tagColors: {
              ...this.store.getPreferences().tagColors,
              [input.dataset.tagColor]: input.value
            }
          });
        })
      );
      page.querySelector("[data-reset-settings]").addEventListener("click", () => {
        if (globalThis.confirm?.(
          this.t(
            "恢复全部资产中心设置?历史数据不受影响。",
            "Reset all asset-center settings? History is preserved."
          )
        )) {
          this.store.resetPreferences();
          this.applyTheme();
          this.render();
        }
      });
    }
    downloadBackup() {
      const blob = new Blob(
        [JSON.stringify(this.store.exportBackup(), null, 2)],
        { type: "application/json" }
      );
      const url = URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = `MWITools_asset_center_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`;
      link.click();
      URL.revokeObjectURL(url);
    }
    changed() {
      this.onChange?.();
      this.render();
    }
    destroy() {
      this.close();
      document.removeEventListener("keydown", this.keydown);
      this.resizeObserver?.disconnect();
      this.root?.remove();
    }
  };
  function createAssetCenter(options) {
    return new AssetCenter(options);
  }

  // src/features/asset-history/30-panel.js
  var TAB_ID = "mwitools-asset-history-tab";
  var PANEL_ID = "mwitools-asset-history-panel";
  var CENTER_ID = "mwitools-asset-center-modal";
  var STYLE_ID3 = "mwitools-asset-history-style";
  var ASSET_SHARE_TEMPLATE_COUNT = 12;
  var ROWS = [
    ["total", "总计", "Total"],
    ["equipment", "装备", "Equipment"],
    ["inventory", "库存", "Inventory"],
    ["marketListings", "订单", "Market listings"],
    ["houses", "房屋", "Houses"],
    ["abilities", "技能", "Abilities"],
    ["nonTradableTokens", "不可交易代币", "Non-tradable tokens"],
    ["shrine", "神龛", "Shrine"]
  ];
  function t3(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function formatNumber(value, signed = false) {
    if (!Number.isFinite(value)) return "—";
    const formatted = runtime.api.numberFormatter?.(Math.abs(value)) ?? value;
    if (!signed || value === 0) return String(formatted);
    return `${value > 0 ? "+" : "−"}${formatted}`;
  }
  function formatPercent(current, previous) {
    if (!Number.isFinite(current) || !Number.isFinite(previous) || previous === 0)
      return "—";
    const value = (current - previous) / previous * 100;
    return `${value > 0 ? "+" : ""}${value.toFixed(2)}%`;
  }
  function valueClass(value) {
    if (!Number.isFinite(value) || value === 0) return "is-neutral";
    return value > 0 ? "is-positive" : "is-negative";
  }
  function sharePeriod(gapDays) {
    if (runtime.config.isZH) {
      return gapDays === 1 ? "今天" : `近 ${gapDays} 天`;
    }
    return gapDays === 1 ? "Today" : `Over the last ${gapDays} days`;
  }
  function buildAssetShareMessage({ change, percent, gapDays = 1 }, templateIndex = Math.floor(Math.random() * ASSET_SHARE_TEMPLATE_COUNT)) {
    if (!Number.isFinite(change) || !Number.isFinite(percent)) return "";
    const period = sharePeriod(gapDays);
    const amount = formatNumber(Math.abs(change));
    const percentText = `${Math.abs(percent).toFixed(2)}%`;
    const signedAmount = formatNumber(change, true);
    const signedPercent = `${percent > 0 ? "+" : ""}${percent.toFixed(2)}%`;
    const zhProfitTemplates = [
      () => `📈 ${period}资产战报:赚了 ${amount},总资产增长 ${percentText}。`,
      () => `${period}的奶牛账本飘绿:进账 ${amount},身家上涨 ${percentText}。`,
      () => `汇报一下${period}战果:盈利 ${amount},资产增加 ${percentText}。`,
      () => `${period}收工报数:净赚 ${amount},总资产提升 ${percentText}。`,
      () => `财富成绩单更新:赚到 ${amount},资产涨幅 ${percentText}。`,
      () => `牛棚财报新鲜出炉:${period}盈利 ${amount},身家增长 ${percentText}。`,
      () => `${period}搬砖结算:收入 ${amount},资产上涨 ${percentText}。`,
      () => `小小炫耀一下:${period}赚了 ${amount},总资产 +${percentText}。`,
      () => `账本一翻,${period}多了 ${amount},身家涨了 ${percentText}。`,
      () => `🚀 财富进度向前:+${amount},涨幅 +${percentText}。`,
      () => `挤奶之余看了眼资产:${period}进账 ${amount},总计增长 ${percentText}。`,
      () => `MWITools 资产盘点:${period}盈利 +${amount},变化 +${percentText}。`
    ];
    const zhLossTemplates = [
      () => `📉 ${period}资产战报:亏了 ${amount},总资产缩水 ${percentText}。`,
      () => `${period}的奶牛账本飘红:损失 ${amount},身家下降 ${percentText}。`,
      () => `汇报一下${period}战况:亏损 ${amount},资产减少 ${percentText}。`,
      () => `${period}收工报数:净亏 ${amount},总资产回落 ${percentText}。`,
      () => `财富成绩单更新:少了 ${amount},资产跌幅 ${percentText}。`,
      () => `牛棚财报有点红:${period}亏损 ${amount},身家缩水 ${percentText}。`,
      () => `${period}搬砖结算:支出 ${amount},资产下降 ${percentText}。`,
      () => `今天不炫耀了:${period}亏了 ${amount},总资产 -${percentText}。`,
      () => `账本一翻,${period}少了 ${amount},身家跌了 ${percentText}。`,
      () => `🩹 财富进度回撤:−${amount},跌幅 −${percentText}。`,
      () => `挤奶之余看了眼资产:${period}损失 ${amount},总计下降 ${percentText}。`,
      () => `MWITools 资产盘点:${period}亏损 −${amount},变化 −${percentText}。`
    ];
    const enProfitTemplates = [
      () => `📈 Asset report: ${period} I gained ${amount}; total assets are up ${percentText}.`,
      () => `${period}'s cow ledger is green: +${amount}, net worth up ${percentText}.`,
      () => `The grind paid off: ${period} I made ${amount}, growing assets by ${percentText}.`,
      () => `Closing the books ${period}: profit ${amount}, total wealth up ${percentText}.`,
      () => `My wealth scorecard: +${amount}, with a ${percentText} gain ${period.toLowerCase()}.`,
      () => `Fresh from the cowshed: ${period} brought ${amount}, net worth up ${percentText}.`,
      () => `Tiny flex: I earned ${amount} ${period.toLowerCase()}, assets +${percentText}.`,
      () => `Checked the books: ${period} added ${amount} to the pile, up ${percentText}.`,
      () => `🚀 Wealth progress unlocked: +${amount} (+${percentText}) ${period.toLowerCase()}.`,
      () => `Milk money report: ${period} profit ${amount}, portfolio growth ${percentText}.`,
      () => `A green day in the galaxy: +${amount}, total assets climbed ${percentText}.`,
      () => `MWITools flex: ${period} P/L +${amount}, asset change +${percentText}.`
    ];
    const enLossTemplates = [
      () => `📉 Asset report: ${period} I lost ${amount}; total assets are down ${percentText}.`,
      () => `${period}'s cow ledger took a hit: -${amount}, net worth down ${percentText}.`,
      () => `Rough shift: ${period} cost me ${amount}, and assets slipped ${percentText}.`,
      () => `Closing the books ${period}: loss ${amount}, total wealth down ${percentText}.`,
      () => `My wealth scorecard: -${amount}, with a ${percentText} drop ${period.toLowerCase()}.`,
      () => `The cowshed report is red: ${period} lost ${amount}, net worth down ${percentText}.`,
      () => `Painful little update: I dropped ${amount}, and assets fell ${percentText}.`,
      () => `Checked the books twice: ${period} erased ${amount}, down ${percentText}.`,
      () => `🩹 Wealth progress setback: -${amount} (-${percentText}) ${period.toLowerCase()}.`,
      () => `Spilled milk report: ${period} loss ${amount}, portfolio down ${percentText}.`,
      () => `A red day in the galaxy: -${amount}, total assets fell ${percentText}.`,
      () => `MWITools reality check: ${period} P/L -${amount}, asset change -${percentText}.`
    ];
    const neutralTemplates = runtime.config.isZH ? [
      () => `${period}资产持平:盈亏 0,变化 ${signedPercent}。`,
      () => `${period}的奶牛账本没动:资产变化 0(${signedPercent})。`,
      () => `财富成绩单:${period}盈亏 0,涨跌 ${signedPercent}。`,
      () => `收工报数:${period}资产不增不减,变化 ${signedPercent}。`,
      () => `账本平静:${period}盈亏 0,资产变化 ${signedPercent}。`,
      () => `牛棚财报:${period}资产持平,盈亏 0(${signedPercent})。`,
      () => `${period}搬砖结算:收入支出相抵,变化 ${signedPercent}。`,
      () => `今天低调一下:${period}盈亏 0,资产持平 ${signedPercent}。`,
      () => `账本一翻:${period}没有盈亏,变化 ${signedPercent}。`,
      () => `➖ 财富进度原地踏步:${signedAmount}(${signedPercent})。`,
      () => `挤奶之余看了眼资产:${period}盈亏 0,变化 ${signedPercent}。`,
      () => `MWITools 资产盘点:${period}盈亏 ${signedAmount},变化 ${signedPercent}。`
    ] : [
      () => `${period}'s asset report is flat: P/L 0, change ${signedPercent}.`,
      () => `${period}'s cow ledger did not move: 0 P/L (${signedPercent}).`,
      () => `Wealth scorecard: ${period} finished flat at ${signedPercent}.`,
      () => `Closing the books ${period}: no gain or loss, change ${signedPercent}.`,
      () => `Quiet ledger: ${period} P/L 0, asset change ${signedPercent}.`,
      () => `Cowshed report: ${period} assets stayed flat at ${signedPercent}.`,
      () => `${period}'s grind broke even: P/L 0, change ${signedPercent}.`,
      () => `Keeping it low-key: ${period} assets stayed flat at ${signedPercent}.`,
      () => `Checked the books: ${period} had no P/L, change ${signedPercent}.`,
      () => `➖ Wealth progress held steady: ${signedAmount} (${signedPercent}).`,
      () => `Paused milking to check: ${period} P/L 0, change ${signedPercent}.`,
      () => `MWITools asset check: ${period} P/L ${signedAmount}, change ${signedPercent}.`
    ];
    const templates = change === 0 ? neutralTemplates : runtime.config.isZH ? change > 0 ? zhProfitTemplates : zhLossTemplates : change > 0 ? enProfitTemplates : enLossTemplates;
    const normalizedIndex = ((Number(templateIndex) || 0) % templates.length + templates.length) % templates.length;
    return templates[normalizedIndex]();
  }
  function pasteAssetShareToChat(message, root = document) {
    const inputs = [
      ...root.querySelectorAll(
        'input[class*="Chat_chatInput"],input[placeholder*="输入消息"],input[placeholder*="message" i]'
      )
    ];
    const input = inputs.find((candidate) => candidate.getClientRects().length > 0) ?? inputs.at(0);
    if (!input || !message) return null;
    const view = input.ownerDocument?.defaultView ?? window;
    const setter = Object.getOwnPropertyDescriptor(
      view.HTMLInputElement.prototype,
      "value"
    )?.set;
    const applyValue = (value) => {
      if (setter) setter.call(input, value);
      else input.value = value;
      input.dispatchEvent(new view.Event("input", { bubbles: true }));
    };
    applyValue("");
    applyValue(message);
    input.focus();
    input.setSelectionRange?.(message.length, message.length);
    return input;
  }
  function addStyles3() {
    if (document.getElementById(STYLE_ID3)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID3;
    style.textContent = `
    #${TAB_ID}[data-active="true"] { color:#00c6ff!important; font-weight:700; }
    [data-mwitools-asset-active="true"] button:not(#${TAB_ID}) { border-color:var(--mwi-asset-idle-border,rgba(255,255,255,.16))!important; background:var(--mwi-asset-idle-background,rgba(255,255,255,.08))!important; box-shadow:var(--mwi-asset-idle-shadow,none)!important; color:var(--mwi-asset-idle-color,var(--color-text-secondary,#aeb5c0))!important; filter:none!important; }
    #${PANEL_ID} { box-sizing:border-box; width:100%; max-width:100%; min-width:0; max-height:calc(100% - 34px); overflow-x:hidden; overflow-y:auto; overscroll-behavior:contain; scrollbar-gutter:stable; padding:12px 12px 24px; color:var(--color-text-primary,#eee); background:#111b2b; }
    .mwi-asset-disclaimer { margin:0 0 10px; color:var(--color-text-secondary,#aaa); font-size:.72rem; line-height:1.4; }
    .mwi-asset-share { display:flex; align-items:center; gap:8px; margin:-2px 0 10px; }
    .mwi-asset-share-status { min-width:0; color:var(--color-text-secondary,#aaa); font-size:.68rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    .mwi-asset-summary { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; margin-bottom:12px; }
    .mwi-asset-card { min-width:0; padding:10px 12px; border:1px solid rgba(255,255,255,.08); border-radius:8px; background:rgba(255,255,255,.06); }
    .mwi-asset-card-label { color:#9fb4d1; font-size:.68rem; }
    .mwi-asset-card-value { overflow:hidden; margin-top:4px; font-size:1rem; font-weight:700; text-overflow:ellipsis; white-space:nowrap; }
    .mwi-asset-card-meta { margin-top:3px; color:var(--color-text-secondary,#999); font-size:.63rem; }
    .is-positive { color:#65d394!important; } .is-negative { color:#ff7b75!important; } .is-neutral { color:inherit; }
    .mwi-asset-section { margin-top:10px; border:1px solid rgba(255,255,255,.08); border-radius:8px; background:#0c141f; overflow:hidden; }
    .mwi-asset-section-title { padding:9px 11px; border-bottom:1px solid rgba(255,255,255,.08); font-size:.84rem; font-weight:700; }
    .mwi-asset-table-wrap { max-width:100%; overflow-x:hidden; }
    .mwi-asset-table { width:100%; min-width:0; table-layout:fixed; border-collapse:collapse; font-size:.72rem; }
    .mwi-asset-table th,.mwi-asset-table td { overflow:hidden; padding:7px 6px; border-bottom:1px solid rgba(255,255,255,.065); text-align:right; text-overflow:ellipsis; white-space:nowrap; }
    .mwi-asset-table th { overflow-wrap:anywhere; white-space:normal; }
    .mwi-asset-table th:first-child,.mwi-asset-table td:first-child { text-align:left; }
    .mwi-asset-table:not(.mwi-asset-history-table) th:first-child { width:18%; }
    .mwi-asset-history-table th:first-child { width:28%; }
    .mwi-asset-history-table th:last-child { width:38%; }
    .mwi-asset-table tr:last-child td { border-bottom:0; }
    .mwi-asset-table tr[data-key="total"] { font-weight:700; background:rgba(255,255,255,.035); }
    .mwi-asset-chart-controls { display:flex; flex-wrap:wrap; gap:6px; padding:9px 10px 0; }
    .mwi-asset-chart-controls button,.mwi-asset-action { border:1px solid rgba(255,255,255,.13); border-radius:5px; background:rgba(255,255,255,.07); color:inherit; padding:5px 9px; cursor:pointer; font:inherit; }
    .mwi-asset-chart-controls button:hover,.mwi-asset-action:hover { background:#3f4655; transform:translateY(-1px); }
    .mwi-asset-chart-controls button[data-active="true"] { border-color:transparent; background:#00c6ff; color:#0b1522; box-shadow:0 0 10px rgba(0,198,255,.45); }
    .mwi-asset-chart-box { position:relative; height:330px; padding:8px 10px 12px; }
    .mwi-asset-chart-fallback { display:grid; height:100%; place-items:center; color:var(--color-text-secondary,#aaa); font-size:.75rem; text-align:center; }
    .mwi-asset-manager { padding:9px 11px 12px; }
    .mwi-asset-manager summary { cursor:pointer; font-size:.8rem; font-weight:700; }
    .mwi-asset-manager-actions { display:flex; flex-wrap:wrap; gap:6px; margin:9px 0; }
    .mwi-asset-action.is-danger { color:#ff938c; }
    .mwi-asset-history-table button { padding:3px 7px; font-size:.68rem; }
    .mwi-asset-edit-dialog { width:min(520px,calc(100vw - 24px)); border:1px solid rgba(255,255,255,.16); border-radius:8px; background:#182033; color:#eee; }
    .mwi-asset-edit-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px 12px; }
    .mwi-asset-edit-grid label { display:grid; gap:3px; color:#bbb; font-size:.7rem; }
    .mwi-asset-edit-grid input { box-sizing:border-box; width:100%; border:1px solid rgba(255,255,255,.18); border-radius:4px; background:#101728; color:#eee; padding:6px; }
    .mwi-asset-edit-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:12px; }
    @media(max-width:760px){
      #${PANEL_ID} { min-height:0; padding:10px 8px calc(20px + env(safe-area-inset-bottom,0px)); overflow-x:hidden; overflow-y:auto; overscroll-behavior-y:contain; -webkit-overflow-scrolling:touch; touch-action:pan-y; }
      .mwi-asset-chart-box { height:280px; }
      .mwi-asset-edit-grid { grid-template-columns:1fr; }
    }
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function buttonLabel(button) {
    return String(
      runtime.api.getOriTextFromElement?.(button) ?? button?.textContent ?? ""
    ).trim().toLowerCase();
  }
  var CHARACTER_TAB_PATTERNS = {
    inventory: /^(库存|inventory)$/i,
    equipment: /^(装备|equipment)$/i,
    skills: /^(技能|skills?|abilities)$/i,
    house: /^(房屋|house)$/i,
    loadout: /^(配装|loadouts?)(?:\s*\d+)?$/i
  };
  function findCharacterManagementLoadoutTab() {
    const groups = /* @__PURE__ */ new Map();
    for (const button of document.querySelectorAll('button[role="tab"],button')) {
      if (button.id === TAB_ID || button.dataset.mwitoolsCharacterTab === "true") {
        continue;
      }
      const parent = button.parentElement;
      if (!parent) continue;
      if (!groups.has(parent)) groups.set(parent, []);
      groups.get(parent).push(button);
    }
    const candidates = [];
    for (const [parent, buttons] of groups) {
      const inCharacterManagement = Boolean(
        parent.closest('[class*="CharacterManagement_characterManagement"]')
      );
      const nativeTabs = buttons.filter(
        (button) => button.matches('[role="tab"]')
      );
      const structuralMatch = inCharacterManagement && nativeTabs.length >= 5;
      const matched = structuralMatch ? {
        inventory: nativeTabs[0],
        equipment: nativeTabs[1],
        skills: nativeTabs[2],
        house: nativeTabs[3],
        loadout: nativeTabs.at(-1)
      } : Object.fromEntries(
        Object.entries(CHARACTER_TAB_PATTERNS).map(([key, pattern]) => [
          key,
          buttons.find((button) => pattern.test(buttonLabel(button)))
        ])
      );
      const supportingTabs = [
        matched.equipment,
        matched.skills,
        matched.house
      ].filter(Boolean).length;
      if (!matched.inventory || !matched.loadout || supportingTabs < 2) continue;
      const rect = parent.getBoundingClientRect?.();
      const visible2 = Boolean(rect && rect.width > 0 && rect.height > 0);
      candidates.push({
        button: matched.loadout,
        score: Number(visible2) * 4 + Number(inCharacterManagement) * 2 + Number(structuralMatch) * 8
      });
    }
    candidates.sort((a, b) => b.score - a.score);
    return candidates[0]?.button ?? null;
  }
  function looksLikeContent(node) {
    if (!(node instanceof Element)) return false;
    const className = String(node.className ?? "");
    return Boolean(
      node.querySelector("input,canvas") || /(Inventory|Equipment|Ability|Abilities|House|Loadout|Panel)_/i.test(
        className
      ) || node.querySelector(
        '[class*="Inventory_"],[class*="Equipment_"],[class*="Ability"],[class*="House_"],[class*="Loadout"]'
      )
    );
  }
  function findPanelShell(tab) {
    let navigationBranch = tab.parentElement;
    for (let depth = 0; navigationBranch?.parentElement && depth < 8; depth += 1) {
      const shell2 = navigationBranch.parentElement;
      const siblings = [...shell2.children].filter(
        (node) => node !== navigationBranch && node.id !== PANEL_ID
      );
      if (siblings.some(looksLikeContent)) {
        return { shell: shell2, navigationBranch };
      }
      navigationBranch = shell2;
    }
    return null;
  }
  function isCompactViewport() {
    const view = globalThis.window ?? globalThis;
    return view.matchMedia?.("(max-width: 760px)")?.matches ?? Number(view.innerWidth) <= 760;
  }
  function createCard(label, valueId, metaId = "") {
    return `<div class="mwi-asset-card"><div class="mwi-asset-card-label">${label}</div><div class="mwi-asset-card-value" id="${valueId}">—</div>${metaId ? `<div class="mwi-asset-card-meta" id="${metaId}"></div>` : ""}</div>`;
  }
  function downloadBackup(backup) {
    const blob = new Blob([JSON.stringify(backup, null, 2)], {
      type: "application/json;charset=utf-8"
    });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = `MWITools-asset-history-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`;
    document.body.appendChild(link);
    link.click();
    link.remove();
    URL.revokeObjectURL(url);
  }
  var AssetHistoryPanel = class {
    constructor(host, store, scopeKey) {
      this.host = host;
      this.store = store;
      this.scopeKey = scopeKey;
      this.snapshot = null;
      this.mode = "total";
      this.range = 30;
      this.visible = false;
      this.build();
      this.center = createAssetCenter({
        store: this.store,
        scopeKey: this.scopeKey,
        onChange: () => this.update(this.snapshot),
        onVisibilityChange: (open) => {
          if (open) this.chart.destroy();
          else if (this.visible) this.update(this.snapshot);
        }
      });
    }
    build() {
      this.host.innerHTML = `
      <p class="mwi-asset-disclaimer">${t3("盈亏按资产估值变化计算,包含市场价格波动,并非已实现交易利润。", "P/L is based on asset valuation changes, including market price movement; it is not realized trading profit.")}</p>
      <div class="mwi-asset-share"><button type="button" class="mwi-asset-action" id="mwi-asset-open-center">${t3("打开资产中心", "Open Asset Center")}</button><button type="button" class="mwi-asset-action" id="mwi-asset-share-chat" disabled>${t3("炫耀", "Flex")}</button><span class="mwi-asset-share-status">${t3("需要至少两天的资产记录", "At least two asset records are required")}</span></div>
      <div class="mwi-asset-summary">
        ${createCard(t3("当前总资产", "Current total assets"), "mwi-asset-current-total")}
        ${createCard(t3("总盈亏", "Total P/L"), "mwi-asset-total-change", "mwi-asset-compare-date")}
        ${createCard(t3("流动资产盈亏", "Liquid-asset P/L"), "mwi-asset-liquid-change")}
        ${createCard(t3("非流动资产盈亏", "Non-current-asset P/L"), "mwi-asset-fixed-change")}
        ${createCard(t3("盈亏比例", "P/L percentage"), "mwi-asset-total-percent")}
        ${createCard(t3("近 7 日平均", "7-day average"), "mwi-asset-seven-average")}
      </div>
      <section class="mwi-asset-section">
        <div class="mwi-asset-section-title">${t3("分项资产变化", "Asset changes by component")}</div>
        <div class="mwi-asset-table-wrap"><table class="mwi-asset-table"><thead><tr><th>${t3("项目", "Component")}</th><th>${t3("当前", "Current")}</th><th id="mwi-asset-change-heading">${t3("变化", "Change")}</th><th>${t3("比例", "Percentage")}</th></tr></thead><tbody id="mwi-asset-breakdown"></tbody></table></div>
      </section>
      <section class="mwi-asset-section">
        <div class="mwi-asset-chart-controls">
          <button type="button" data-mode="total">${t3("总资产", "Total assets")}</button>
          <button type="button" data-mode="profit">${t3("每日盈亏", "Daily P/L")}</button>
          <button type="button" data-mode="breakdown">${t3("分项资产", "Component assets")}</button>
          <span style="flex:1"></span>
          <button type="button" data-range="7">7${t3("天", "d")}</button>
          <button type="button" data-range="15">15${t3("天", "d")}</button>
          <button type="button" data-range="30">30${t3("天", "d")}</button>
          <button type="button" data-range="all">${t3("全部", "All")}</button>
          <button type="button" id="mwi-asset-reset-zoom">${t3("重置缩放", "Reset zoom")}</button>
        </div>
        <div class="mwi-asset-chart-box"><canvas id="mwi-asset-chart"></canvas><div class="mwi-asset-chart-fallback" hidden></div></div>
      </section>
      <section class="mwi-asset-section"><details class="mwi-asset-manager"><summary>${t3("数据管理与备份", "Data management & backup")}</summary>
        <div class="mwi-asset-manager-actions">
          <button type="button" class="mwi-asset-action" id="mwi-asset-export">${t3("导出备份", "Export backup")}</button>
          <button type="button" class="mwi-asset-action" id="mwi-asset-import">${t3("导入备份", "Import backup")}</button>
          <button type="button" class="mwi-asset-action is-danger" id="mwi-asset-cleanup">${t3("清理无效记录", "Clean invalid records")}</button>
          <button type="button" class="mwi-asset-action is-danger" id="mwi-asset-anomalies">${t3("检测并删除异常", "Detect & delete anomalies")}</button>
          <input type="file" id="mwi-asset-import-file" accept="application/json" hidden>
        </div>
        <div class="mwi-asset-table-wrap"><table class="mwi-asset-table mwi-asset-history-table"><thead><tr><th>${t3("日期", "Date")}</th><th>${t3("总资产", "Total")}</th><th>${t3("操作", "Actions")}</th></tr></thead><tbody id="mwi-asset-history-rows"></tbody></table></div>
      </details></section>
      <dialog class="mwi-asset-edit-dialog" id="mwi-asset-edit-dialog"><h3>${t3("编辑分项资产", "Edit asset components")}</h3><div class="mwi-asset-edit-grid">${ASSET_COMPONENT_KEYS.map(
        (key) => {
          const row = ROWS.find(([candidate]) => candidate === key);
          return `<label>${t3(row[1], row[2])}<input type="number" min="0" step="any" data-component="${key}"></label>`;
        }
      ).join(
        ""
      )}</div><div class="mwi-asset-edit-actions"><button type="button" class="mwi-asset-action" data-edit-cancel>${t3("取消", "Cancel")}</button><button type="button" class="mwi-asset-action" data-edit-save>${t3("保存", "Save")}</button></div></dialog>
    `;
      this.chart = new AssetHistoryChart(
        this.host.querySelector("#mwi-asset-chart"),
        this.host.querySelector(".mwi-asset-chart-fallback")
      );
      this.bind();
    }
    bind() {
      this.host.querySelector("#mwi-asset-open-center").addEventListener("click", () => this.center?.open());
      this.host.querySelector("#mwi-asset-share-chat").addEventListener("click", () => this.shareToChat());
      this.host.querySelectorAll("[data-mode]").forEach((button) => {
        button.addEventListener("click", () => {
          this.mode = button.dataset.mode;
          this.renderChart();
        });
      });
      this.host.querySelectorAll("[data-range]").forEach((button) => {
        button.addEventListener("click", () => {
          this.range = button.dataset.range === "all" ? null : Number(button.dataset.range);
          this.renderChart();
        });
      });
      this.host.querySelector("#mwi-asset-reset-zoom").addEventListener("click", () => this.chart.resetZoom());
      this.host.querySelector("#mwi-asset-export").addEventListener(
        "click",
        () => downloadBackup(this.store.exportBackup())
      );
      const fileInput = this.host.querySelector("#mwi-asset-import-file");
      this.host.querySelector("#mwi-asset-import").addEventListener("click", () => fileInput.click());
      fileInput.addEventListener("change", async () => {
        const file = fileInput.files?.[0];
        if (!file) return;
        try {
          const backup = JSON.parse(await file.text());
          const replace = globalThis.confirm?.(
            t3(
              "确定:替换当前角色历史;取消:合并导入。",
              "OK: replace this character's history; Cancel: merge it."
            )
          );
          this.store.importBackup(backup, {
            mode: replace ? "replace" : "merge",
            scopeKey: this.scopeKey
          });
          this.update(this.snapshot);
        } catch (error) {
          globalThis.alert?.(
            `${t3("导入失败", "Import failed")}: ${error.message}`
          );
        } finally {
          fileInput.value = "";
        }
      });
      this.host.querySelector("#mwi-asset-cleanup").addEventListener("click", () => {
        const removed = this.store.cleanupInvalid(this.scopeKey);
        globalThis.alert?.(
          t3(
            `已删除 ${removed} 条无效记录。`,
            `Removed ${removed} invalid records.`
          )
        );
        this.update(this.snapshot);
      });
      this.host.querySelector("#mwi-asset-anomalies").addEventListener("click", () => {
        const anomalies = this.store.detectAnomalies(this.scopeKey);
        if (!anomalies.length) {
          globalThis.alert?.(
            t3("未发现明显异常。", "No clear anomalies found.")
          );
          return;
        }
        const preview = anomalies.map(({ date, zScore }) => `${date} (Z=${zScore.toFixed(1)})`).join("\n");
        if (!globalThis.confirm?.(
          t3(
            `确认删除以下异常日期?
${preview}`,
            `Delete these anomalous dates?
${preview}`
          )
        )) {
          return;
        }
        anomalies.forEach(
          ({ date }) => this.store.deleteDay(date, this.scopeKey)
        );
        this.update(this.snapshot);
      });
      this.host.querySelector("[data-edit-cancel]").addEventListener("click", () => this.closeEditor());
      this.host.querySelector("[data-edit-save]").addEventListener("click", () => this.saveEditor());
    }
    shareToChat() {
      const status = this.host.querySelector(".mwi-asset-share-status");
      const message = buildAssetShareMessage(this.shareStats ?? {});
      if (!message) {
        status.textContent = t3(
          "暂无可对比的盈亏数据",
          "No comparable P/L data yet"
        );
        return;
      }
      const input = pasteAssetShareToChat(message);
      status.dataset.pasted = String(Boolean(input));
      status.textContent = input ? t3("已放入聊天框,按回车发送", "Pasted into chat; press Enter to send") : t3(
        "未找到聊天框,请先展开聊天",
        "Chat input not found; open chat first"
      );
    }
    openEditor(dayKey) {
      const record = this.store.getRole(this.scopeKey).days[dayKey];
      const dialog = this.host.querySelector("#mwi-asset-edit-dialog");
      dialog.dataset.dayKey = dayKey;
      for (const input of dialog.querySelectorAll("[data-component]")) {
        const value = record?.values?.[input.dataset.component];
        input.value = Number.isFinite(value) ? value : "";
      }
      if (typeof dialog.showModal === "function") dialog.showModal();
      else dialog.setAttribute("open", "");
    }
    closeEditor() {
      const dialog = this.host.querySelector("#mwi-asset-edit-dialog");
      if (typeof dialog.close === "function") dialog.close();
      else dialog.removeAttribute("open");
    }
    saveEditor() {
      const dialog = this.host.querySelector("#mwi-asset-edit-dialog");
      const values = Object.fromEntries(
        [...dialog.querySelectorAll("[data-component]")].map((input) => [
          input.dataset.component,
          Number(input.value)
        ])
      );
      if (!ASSET_COMPONENT_KEYS.every(
        (key) => Number.isFinite(values[key]) && values[key] >= 0
      )) {
        globalThis.alert?.(
          t3(
            "请为全部七个分项填写不小于零的数字。",
            "Enter a non-negative number for all seven components."
          )
        );
        return;
      }
      this.store.updateDay(dialog.dataset.dayKey, values, this.scopeKey);
      this.closeEditor();
      this.update(this.snapshot);
    }
    update(snapshot) {
      this.snapshot = snapshot ?? this.snapshot;
      this.center?.update(this.snapshot);
      if (!this.visible || !this.host.isConnected || this.center?.isOpen()) {
        if (!this.host.isConnected) this.chart.destroy();
        return;
      }
      const dayKey = getUtc8DayKey();
      const todayRecord = this.store.getRole(this.scopeKey).days[dayKey];
      const current = this.snapshot?.values ?? todayRecord?.values ?? {};
      const comparison = this.store.comparison(dayKey, this.scopeKey);
      const previous = comparison?.record?.values ?? {};
      const totalChange = Number.isFinite(current.total) && Number.isFinite(previous.total) ? current.total - previous.total : null;
      const totalPercent = Number.isFinite(totalChange) && Number.isFinite(previous.total) && previous.total !== 0 ? totalChange / previous.total * 100 : null;
      this.shareStats = comparison && Number.isFinite(totalPercent) ? {
        change: totalChange,
        percent: totalPercent,
        gapDays: comparison.gapDays
      } : null;
      const shareButton = this.host.querySelector("#mwi-asset-share-chat");
      const shareStatus = this.host.querySelector(".mwi-asset-share-status");
      shareButton.disabled = !this.shareStats;
      if (!this.shareStats) {
        shareStatus.textContent = t3(
          "需要至少两天的资产记录",
          "At least two asset records are required"
        );
      } else if (shareStatus.dataset.pasted !== "true") {
        shareStatus.textContent = t3(
          "随机生成今日战报并放入聊天框",
          "Generate a random report and paste it into chat"
        );
      }
      const compareText = comparison ? comparison.gapDays === 1 ? t3(`较昨日(${comparison.date})`, `vs yesterday (${comparison.date})`) : t3(
        `较 ${comparison.gapDays} 天前(${comparison.date})`,
        `vs ${comparison.gapDays} days ago (${comparison.date})`
      ) : t3("暂无历史对比", "No prior record");
      const setNumber = (selector, value, { signed = false, className = "" } = {}) => {
        const node = this.host.querySelector(selector);
        node.textContent = formatNumber(value, signed);
        node.title = Number.isFinite(value) ? runtime.api.formatExactNumber(value, 0) : "";
        node.className = `mwi-asset-card-value ${className}`.trim();
      };
      setNumber("#mwi-asset-current-total", current.total);
      setNumber("#mwi-asset-total-change", totalChange, {
        signed: true,
        className: valueClass(totalChange)
      });
      const liquidChange = Number.isFinite(current.liquid) && Number.isFinite(previous.liquid) ? current.liquid - previous.liquid : null;
      const fixedChange = Number.isFinite(current.fixed) && Number.isFinite(previous.fixed) ? current.fixed - previous.fixed : null;
      setNumber("#mwi-asset-liquid-change", liquidChange, {
        signed: true,
        className: valueClass(liquidChange)
      });
      setNumber("#mwi-asset-fixed-change", fixedChange, {
        signed: true,
        className: valueClass(fixedChange)
      });
      this.host.querySelector("#mwi-asset-compare-date").textContent = compareText;
      setNumber("#mwi-asset-total-percent", null, {
        className: valueClass(totalChange)
      });
      this.host.querySelector("#mwi-asset-total-percent").textContent = formatPercent(current.total, previous.total);
      const average = this.store.sevenDayAverage(dayKey, this.scopeKey);
      setNumber("#mwi-asset-seven-average", average, {
        signed: true,
        className: valueClass(average)
      });
      this.host.querySelector("#mwi-asset-change-heading").textContent = comparison ? t3(`变化(较 ${comparison.date})`, `Change (vs ${comparison.date})`) : t3("变化", "Change");
      const body = this.host.querySelector("#mwi-asset-breakdown");
      body.replaceChildren(
        ...ROWS.map(([key, zh, en]) => {
          const row = document.createElement("tr");
          row.dataset.key = key;
          const currentValue = current[key];
          const previousValue = previous[key];
          const change = Number.isFinite(currentValue) && Number.isFinite(previousValue) ? currentValue - previousValue : null;
          row.innerHTML = `<td>${t3(zh, en)}</td><td title="${Number.isFinite(currentValue) ? runtime.api.formatExactNumber(currentValue, 0) : ""}">${formatNumber(currentValue)}</td><td class="${valueClass(change)}" title="${Number.isFinite(change) ? runtime.api.formatExactNumber(change, 0) : ""}">${formatNumber(change, true)}</td><td class="${valueClass(change)}">${formatPercent(currentValue, previousValue)}</td>`;
          return row;
        })
      );
      this.renderHistoryRows();
      this.renderChart();
    }
    renderHistoryRows() {
      const body = this.host.querySelector("#mwi-asset-history-rows");
      const entries = this.store.list(this.scopeKey).slice().reverse();
      body.replaceChildren(
        ...entries.map(([dayKey, record]) => {
          const row = document.createElement("tr");
          const total = record?.values?.total;
          row.innerHTML = `<td>${dayKey}</td><td title="${Number.isFinite(total) ? runtime.api.formatExactNumber(total, 0) : ""}">${formatNumber(total)}</td><td><button type="button" class="mwi-asset-action" data-edit>${t3("编辑", "Edit")}</button> <button type="button" class="mwi-asset-action is-danger" data-delete>${t3("删除", "Delete")}</button></td>`;
          row.querySelector("[data-edit]").addEventListener("click", () => this.openEditor(dayKey));
          row.querySelector("[data-delete]").addEventListener("click", () => {
            if (globalThis.confirm?.(t3(`确认删除 ${dayKey}?`, `Delete ${dayKey}?`))) {
              this.store.deleteDay(dayKey, this.scopeKey);
              this.update(this.snapshot);
            }
          });
          return row;
        })
      );
    }
    renderChart() {
      this.host.querySelectorAll("[data-mode]").forEach((button) => {
        button.dataset.active = String(button.dataset.mode === this.mode);
      });
      this.host.querySelectorAll("[data-range]").forEach((button) => {
        const range = button.dataset.range === "all" ? null : Number(button.dataset.range);
        button.dataset.active = String(range === this.range);
      });
      this.chart.render(this.store.list(this.scopeKey), {
        mode: this.mode,
        range: this.range
      });
    }
    setVisible(visible2) {
      this.visible = Boolean(visible2);
      if (this.visible) return;
      this.chart.destroy();
      this.center?.close();
    }
    destroy() {
      this.visible = false;
      this.chart.destroy();
      this.center?.destroy();
    }
  };
  function createAssetHistoryUi({ scope, store, scopeKey }) {
    let active = false;
    let mountMode = null;
    let tab = null;
    let host = null;
    let panel = null;
    let shell2 = null;
    let navigationBranch = null;
    const hiddenNodes = /* @__PURE__ */ new Map();
    let lastActiveNativeTab = null;
    const restoreNative = () => {
      for (const [node, state] of hiddenNodes) {
        node.hidden = state.hidden;
        if (state.styleDisplay === null) node.style.removeProperty("display");
        else node.style.display = state.styleDisplay;
      }
      hiddenNodes.clear();
    };
    const captureIdleTabStyle = () => {
      if (!navigationBranch || typeof getComputedStyle !== "function") return;
      const unselectedTab = navigationBranch.querySelector(
        `button:not(#${TAB_ID}):not([aria-selected="true"]):not(.Mui-selected)`
      ) ?? navigationBranch.querySelector(`button:not(#${TAB_ID})`);
      if (!unselectedTab) return;
      const style = getComputedStyle(unselectedTab);
      const background = style.background || style.backgroundColor;
      if (background && background !== "transparent" && background !== "rgba(0, 0, 0, 0)") {
        navigationBranch.style.setProperty(
          "--mwi-asset-idle-background",
          background
        );
      }
      if (style.borderColor && style.borderColor !== "transparent") {
        navigationBranch.style.setProperty(
          "--mwi-asset-idle-border",
          style.borderColor
        );
      }
      if (style.color) {
        navigationBranch.style.setProperty("--mwi-asset-idle-color", style.color);
      }
      if (style.boxShadow && style.boxShadow !== "none") {
        navigationBranch.style.setProperty(
          "--mwi-asset-idle-shadow",
          style.boxShadow
        );
      }
    };
    const clearNativeTabOverride = () => {
      if (!navigationBranch) return;
      delete navigationBranch.dataset.mwitoolsAssetActive;
      for (const property of [
        "--mwi-asset-idle-background",
        "--mwi-asset-idle-border",
        "--mwi-asset-idle-color",
        "--mwi-asset-idle-shadow"
      ]) {
        navigationBranch.style.removeProperty(property);
      }
    };
    const syncHostViewport = () => {
      if (!host) return;
      if (!isCompactViewport() || !active) {
        host.style.removeProperty("height");
        host.style.removeProperty("max-height");
        return;
      }
      const top = Math.max(0, Math.round(host.getBoundingClientRect().top));
      const available = `calc(100dvh - ${top}px - env(safe-area-inset-bottom,0px))`;
      host.style.height = available;
      host.style.maxHeight = available;
    };
    const syncNativeVisibility = () => {
      for (const node of [...shell2?.children ?? []]) {
        if (node === navigationBranch || node === host || node.tagName === "STYLE") {
          continue;
        }
        if (!hiddenNodes.has(node)) {
          hiddenNodes.set(node, {
            hidden: node.hidden,
            styleDisplay: node.style.display || null
          });
        }
        if (!node.hidden) node.hidden = true;
        if (node.style.display !== "none") node.style.display = "none";
      }
    };
    const setActive = (next) => {
      const nextActive = Boolean(next);
      if (nextActive === active) {
        if (active) {
          syncNativeVisibility();
          syncHostViewport();
        }
        return;
      }
      if (mountMode === "native" && nextActive && !active) {
        captureIdleTabStyle();
      }
      active = nextActive;
      if (tab) {
        tab.dataset.active = String(active);
        tab.setAttribute("aria-selected", String(active));
        tab.classList.toggle("Mui-selected", active);
        if (tab.hasAttribute("data-selected")) {
          tab.dataset.selected = String(active);
        }
        if (tab.hasAttribute("data-state")) {
          tab.dataset.state = active ? "active" : "inactive";
        }
        if (!active) tab.blur();
      }
      if (navigationBranch && active) {
        const currentSelected = navigationBranch.querySelector(
          `button[aria-selected="true"]:not(#${TAB_ID}), button.Mui-selected:not(#${TAB_ID}), [role="tab"][aria-selected="true"]:not(#${TAB_ID})`
        );
        if (currentSelected) lastActiveNativeTab = currentSelected;
      }
      if (host) host.hidden = !active;
      panel?.setVisible(active);
      if (!active) {
        restoreNative();
        clearNativeTabOverride();
        syncHostViewport();
        return;
      }
      navigationBranch.dataset.mwitoolsAssetActive = "true";
      syncNativeVisibility();
      syncHostViewport();
      panel?.update(runtime.api.getLatestAssetSnapshot?.());
    };
    const teardownMount = () => {
      setActive(false);
      panel?.destroy();
      panel = null;
      tab?.remove();
      host?.remove();
      mountMode = null;
      tab = null;
      host = null;
      shell2 = null;
      navigationBranch = null;
      lastActiveNativeTab = null;
    };
    const mountNative = (loadout, found) => {
      mountMode = "native";
      ({ shell: shell2, navigationBranch } = found);
      tab = loadout.cloneNode(true);
      tab.id = TAB_ID;
      tab.dataset.mwitoolsCharacterTab = "true";
      tab.type = "button";
      const badgeText = tab.querySelector(
        ".TabsComponent_badge__1Du26, .MuiBadge-root"
      );
      if (badgeText) {
        badgeText.textContent = t3("盈亏", "P/L");
      } else {
        tab.textContent = t3("盈亏", "P/L");
      }
      for (const className of [...tab.classList]) {
        if (/(?:^|[_-])(?:active|selected)(?:[_-]|$)/i.test(className)) {
          tab.classList.remove(className);
        }
      }
      tab.dataset.active = "false";
      if (tab.hasAttribute("data-selected")) tab.dataset.selected = "false";
      if (tab.hasAttribute("data-state")) tab.dataset.state = "inactive";
      tab.setAttribute("aria-selected", "false");
      tab.setAttribute("tabindex", "-1");
      tab.addEventListener("click", (event) => {
        event.preventDefault();
        event.stopPropagation();
        setActive(!active);
      });
      loadout.insertAdjacentElement("afterend", tab);
      host = document.createElement("section");
      host.id = PANEL_ID;
      host.hidden = true;
      shell2.appendChild(host);
      panel = new AssetHistoryPanel(host, store, scopeKey);
      panel.update(runtime.api.getLatestAssetSnapshot?.());
    };
    const ensureMounted = () => {
      const loadout = findCharacterManagementLoadoutTab();
      const found = loadout && findPanelShell(loadout);
      if (loadout && found) {
        const mountedOnCurrentTabs = tab?.parentElement === loadout.parentElement && tab?.previousElementSibling === loadout && host?.parentElement === found.shell;
        if (mountMode === "native" && tab?.isConnected && host?.isConnected && mountedOnCurrentTabs) {
          const otherSelected = navigationBranch?.querySelector(
            `button[aria-selected="true"]:not(#${TAB_ID}), button.Mui-selected:not(#${TAB_ID}), [role="tab"][aria-selected="true"]:not(#${TAB_ID})`
          );
          if (otherSelected && otherSelected !== lastActiveNativeTab || tab.getAttribute("aria-selected") !== "true") {
            if (active) setActive(false);
            return;
          }
          if (active) {
            syncNativeVisibility();
            syncHostViewport();
          }
          return;
        }
        teardownMount();
        mountNative(loadout, found);
        return;
      }
      if (mountMode !== null) teardownMount();
    };
    addStyles3();
    ensureMounted();
    const mountScheduler = createFrameScheduler(ensureMounted);
    subscribeMutationChannel(
      {
        name: "character-management-mount",
        target: document.body,
        options: {
          attributes: true,
          attributeFilter: ["aria-selected", "class", "data-active", "hidden"],
          childList: true,
          subtree: true
        },
        scope
      },
      (records) => {
        const relevant = records.some((record) => {
          const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
          if (target?.closest?.(`#${TAB_ID},#${PANEL_ID},#${CENTER_ID}`)) {
            return false;
          }
          if (record.type === "attributes") {
            return Boolean(
              target?.closest?.(
                '[class*="CharacterManagement_characterManagement"]'
              )
            );
          }
          if (target?.closest?.(
            '[class*="CharacterManagement_characterManagement"]'
          )) {
            return true;
          }
          return [...record.addedNodes, ...record.removedNodes].some(
            (node) => node?.nodeType === 1 && !(node.matches?.(`#${TAB_ID},#${PANEL_ID},#${CENTER_ID}`) || node.closest?.(`#${TAB_ID},#${PANEL_ID},#${CENTER_ID}`)) && (node.matches?.(
              '[class*="CharacterManagement_characterManagement"]'
            ) || node.querySelector?.(
              '[class*="CharacterManagement_characterManagement"]'
            ))
          );
        });
        if (relevant) mountScheduler.schedule();
      }
    );
    scope.add(() => mountScheduler.cancel());
    const handleTabBranchClick = (event) => {
      if (!active || event.target.closest(`#${TAB_ID}`) || event.target.closest(`#${PANEL_ID}`)) {
        return;
      }
      if (navigationBranch?.contains(event.target)) setActive(false);
    };
    scope.event(document, "pointerdown", handleTabBranchClick, true);
    scope.event(document, "click", handleTabBranchClick, true);
    scope.event(document, "keydown", (event) => {
      if (active && isCompactViewport() && event.key === "Escape") {
        setActive(false);
      }
    });
    scope.event(window, "resize", syncHostViewport);
    return {
      update(snapshot) {
        panel?.update(snapshot);
      },
      destroy() {
        teardownMount();
        document.getElementById(STYLE_ID3)?.remove();
      }
    };
  }

  // src/features/asset-history/index.js
  function detectRoleName() {
    if (runtime.state.currentCharacterName) {
      return String(runtime.state.currentCharacterName).trim();
    }
    const candidate = [
      ".CharacterName_name__1amXp span",
      '[class*="CharacterName_name"] span',
      '[data-testid="character-name"]'
    ].map((selector) => document.querySelector(selector)).find(Boolean);
    return String(candidate?.textContent ?? "").trim();
  }
  function currentScopeKey() {
    return assetHistoryStore.scopeKey(runtime.state.currentCharacterId);
  }
  var assetHistoryApi = {
    storageKey: "MWITools_asset_history_v1",
    record(snapshot = getLatestAssetSnapshot(), scopeKey = currentScopeKey()) {
      return assetHistoryStore.record(snapshot, scopeKey);
    },
    getHistory(scopeKey = currentScopeKey()) {
      return assetHistoryStore.list(scopeKey);
    },
    getComparison(dayKey = getUtc8DayKey(), scopeKey = currentScopeKey()) {
      return assetHistoryStore.comparison(dayKey, scopeKey);
    },
    getSevenDayAverage(dayKey = getUtc8DayKey(), scopeKey = currentScopeKey()) {
      return assetHistoryStore.sevenDayAverage(dayKey, scopeKey);
    },
    updateDay(dayKey, values, scopeKey = currentScopeKey()) {
      return assetHistoryStore.updateDay(dayKey, values, scopeKey);
    },
    insertDay(dayKey, values, scopeKey = currentScopeKey()) {
      return assetHistoryStore.insertDay(dayKey, values, scopeKey);
    },
    deleteDay(dayKey, scopeKey = currentScopeKey()) {
      return assetHistoryStore.deleteDay(dayKey, scopeKey);
    },
    cleanup(scopeKey = currentScopeKey()) {
      return assetHistoryStore.cleanupInvalid(scopeKey);
    },
    detectAnomalies(scopeKey = currentScopeKey()) {
      return assetHistoryStore.detectAnomalies(scopeKey);
    },
    getTags(scopeKey = currentScopeKey()) {
      return assetHistoryStore.listTags(scopeKey);
    },
    addTag(date, text, type = "", scopeKey = currentScopeKey()) {
      return assetHistoryStore.addTag(date, text, type, scopeKey);
    },
    updateTag(tagId, patch, scopeKey = currentScopeKey()) {
      return assetHistoryStore.updateTag(tagId, patch, scopeKey);
    },
    deleteTag(tagId, scopeKey = currentScopeKey()) {
      return assetHistoryStore.deleteTag(tagId, scopeKey);
    },
    getPreferences() {
      return assetHistoryStore.getPreferences();
    },
    setPreferences(patch) {
      return assetHistoryStore.setPreferences(patch);
    },
    setGoalTarget(value, scopeKey = currentScopeKey()) {
      return assetHistoryStore.setGoalTarget(value, scopeKey);
    },
    getGoalTarget(scopeKey = currentScopeKey()) {
      return assetHistoryStore.getGoalTarget(scopeKey);
    },
    getStatistics(options = {}, scopeKey = currentScopeKey()) {
      return periodStatistics(assetHistoryStore.list(scopeKey), options);
    },
    getHeatmap(scopeKey = currentScopeKey()) {
      return buildHeatmap(assetHistoryStore.list(scopeKey));
    },
    getComponentAnalysis(rangeDays = null, scopeKey = currentScopeKey()) {
      return componentAnalysis(assetHistoryStore.list(scopeKey), rangeDays);
    },
    getAchievements(scopeKey = currentScopeKey()) {
      const results = calculateAchievements(
        assetHistoryStore.list(scopeKey),
        assetHistoryStore.getAchievements(scopeKey)
      );
      assetHistoryStore.syncAchievements(results, scopeKey);
      return results;
    },
    simulate(options = {}, scopeKey = currentScopeKey()) {
      return simulateNetWorth(assetHistoryStore.list(scopeKey), options);
    },
    exportBackup() {
      return assetHistoryStore.exportBackup();
    },
    importBackup(backup, options = {}) {
      return assetHistoryStore.importBackup(backup, {
        scopeKey: currentScopeKey(),
        ...options
      });
    },
    migrateLegacy(options = {}) {
      return assetHistoryStore.migrateLegacy({
        scopeKey: currentScopeKey(),
        roleName: detectRoleName(),
        ...options
      });
    },
    refresh: refreshAssetSnapshot,
    scheduleRefresh: scheduleAssetSnapshotRefresh
  };
  runtime.api.assetHistory = assetHistoryApi;
  runtime.features.register({
    id: "assetHistory",
    setting: "assetHistory",
    scope: "character",
    initialize({ scope, characterId }) {
      const scopeKey = assetHistoryStore.scopeKey(characterId);
      assetHistoryStore.migrateLegacy({
        scopeKey,
        roleName: detectRoleName()
      });
      const ui = createAssetHistoryUi({
        scope,
        store: assetHistoryStore,
        scopeKey
      });
      const consume = (snapshot) => {
        if (String(snapshot?.characterId ?? "") !== String(characterId)) return;
        assetHistoryStore.record(snapshot, scopeKey);
        ui.update(snapshot);
      };
      scope.add(onAssetSnapshot(consume));
      const latest = getLatestAssetSnapshot();
      if (latest) consume(latest);
      void refreshAssetSnapshot();
      return () => ui.destroy();
    }
  });

  // src/features/planning.js
  var TAB_ID2 = "mwitools-planning-tab";
  var PANEL_ID2 = "mwitools-planning-panel";
  var STYLE_ID4 = "mwitools-planning-style";
  var ASSET_TAB_ID = "mwitools-asset-history-tab";
  var procurement2 = runtime.api.procurement;
  var planning = runtime.api.planning;
  var spriteBaseCache = /* @__PURE__ */ new Map();
  function t4(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function number(value) {
    return runtime.api.numberFormatter?.(value) ?? String(value ?? "—");
  }
  function wholeNumber(value) {
    const numeric = Number(value);
    if (!Number.isFinite(numeric)) return "—";
    return new Intl.NumberFormat(runtime.config.isZH ? "zh-CN" : "en-US", {
      maximumFractionDigits: 0
    }).format(Math.max(0, Math.ceil(numeric - 1e-9)));
  }
  function escapeHtml2(value) {
    return String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
  }
  function tail(value) {
    return String(value ?? "").split("/").at(-1)?.replaceAll("_", " ") ?? "—";
  }
  function houseName(hrid) {
    const english = runtime.state.initData_houseRoomDetailMap?.[hrid]?.name;
    return getLocalizedEntityName("houseRoom", hrid, {
      fallback: english || tail(hrid)
    });
  }
  function currentHouseLevel2(hrid) {
    const map = runtime.state.initData_characterHouseRoomMap ?? {};
    return Math.max(
      0,
      Number(
        map[hrid]?.level ?? Object.values(map).find((house) => house?.houseRoomHrid === hrid)?.level
      ) || 0
    );
  }
  function maxHouseLevel2(hrid) {
    const levels = Object.keys(
      runtime.state.initData_houseRoomDetailMap?.[hrid]?.upgradeCostsMap ?? {}
    ).map(Number).filter((level) => Number.isFinite(level));
    return levels.length ? Math.max(...levels) : 0;
  }
  function goalLabel(goal) {
    return goal.kind === "house" ? houseName(goal.targetHrid) : itemName(goal.targetHrid);
  }
  function itemCandidates() {
    return Object.entries(runtime.state.initData_itemDetailMap ?? {}).filter(
      ([hrid]) => hrid.startsWith("/items/") && planning.isCraftableItem(hrid)
    ).map(([hrid, detail]) => ({
      hrid,
      name: itemName(hrid),
      english: String(detail?.name ?? ""),
      sortIndex: Number(detail?.sortIndex) || Number.MAX_SAFE_INTEGER
    })).sort(
      (left, right) => left.sortIndex - right.sortIndex || left.name.localeCompare(right.name)
    );
  }
  function houseCandidates() {
    return Object.entries(runtime.state.initData_houseRoomDetailMap ?? {}).filter(([hrid]) => maxHouseLevel2(hrid) > 0).map(([hrid, detail]) => ({
      hrid,
      name: houseName(hrid),
      skillHrid: detail?.skillHrid,
      sortIndex: Number(detail?.sortIndex) || Number.MAX_SAFE_INTEGER
    })).sort(
      (left, right) => left.sortIndex - right.sortIndex || left.name.localeCompare(right.name)
    );
  }
  function catalogSignature() {
    return JSON.stringify([
      Object.keys(runtime.state.initData_itemDetailMap ?? {}),
      Object.keys(runtime.state.initData_actionDetailMap ?? {}),
      Object.entries(runtime.state.initData_houseRoomDetailMap ?? {}).map(
        ([hrid, detail]) => [hrid, detail?.skillHrid, detail?.upgradeCostsMap]
      )
    ]);
  }
  function resolveItemInput(value, candidates) {
    const query = String(value ?? "").trim().toLowerCase();
    if (!query) return null;
    const normalized = procurement2.normalizeItemHrid(query);
    return candidates.find(
      (candidate) => candidate.hrid.toLowerCase() === query || candidate.hrid === normalized || candidate.name.toLowerCase() === query || candidate.english.toLowerCase() === query
    ) ?? null;
  }
  function findSpriteBase(kind) {
    const cached = spriteBaseCache.get(kind);
    if (cached) return cached;
    const needle = `${kind}_sprite`;
    for (const entry of globalThis.performance?.getEntriesByType?.("resource") ?? []) {
      if (!entry.name?.includes(needle) || !entry.name.endsWith(".svg")) continue;
      try {
        const pathname = new URL(entry.name).pathname;
        spriteBaseCache.set(kind, pathname);
        return pathname;
      } catch {
        spriteBaseCache.set(kind, entry.name);
        return entry.name;
      }
    }
    const use = document.querySelector(
      `svg use[href*="${needle}"],svg use[xlink\\:href*="${needle}"]`
    );
    const href = use?.getAttribute("href") ?? use?.getAttribute("xlink:href") ?? "";
    const base = href.includes("#") ? href.split("#")[0] : "";
    if (base) spriteBaseCache.set(kind, base);
    return base;
  }
  function iconMarkup(kind, hrid, label) {
    const bare = String(hrid ?? "").split("/").at(-1);
    const sprite = findSpriteBase(kind);
    if (!bare || !sprite) {
      return `<span class="planning-icon-fallback">${escapeHtml2(
        String(label || "?").trim().charAt(0) || "?"
      )}</span>`;
    }
    const href = `${sprite}#${bare}`;
    return `<svg viewBox="0 0 32 32" aria-hidden="true"><use href="${escapeHtml2(href)}" xlink:href="${escapeHtml2(href)}"></use></svg>`;
  }
  function itemIcon(itemHrid, label = itemName(itemHrid)) {
    return iconMarkup("items", itemHrid, label);
  }
  function houseIcon(candidate) {
    return iconMarkup("skills", candidate?.skillHrid, candidate?.name);
  }
  function goalIcon(goal) {
    if (goal.kind === "item") return itemIcon(goal.targetHrid, goalLabel(goal));
    const candidate = houseCandidates().find(
      (house) => house.hrid === goal.targetHrid
    );
    return houseIcon(candidate);
  }
  function goalSources(ids, goals) {
    const byId = new Map(goals.map((goal) => [goal.id, goalLabel(goal)]));
    return ids.map((id) => byId.get(id) ?? id).join(t4("、", ", "));
  }
  var POLICY_OPTIONS = Object.freeze([
    ["chain", "全链条制作", "Full chain"],
    ["single", "单步制作", "One step"],
    ["buy", "直接购买", "Buy"]
  ]);
  function policyLabel(policy) {
    if (policy === "mixed") return t4("混合", "Mixed");
    const option = POLICY_OPTIONS.find(([value]) => value === policy);
    return option ? t4(option[1], option[2]) : t4("全链条制作", "Full chain");
  }
  function createPolicyControl(value, onChange, { disabled = false } = {}) {
    if (value === "mixed") {
      const mixed = document.createElement("span");
      mixed.className = "planning-policy-mixed";
      mixed.textContent = policyLabel("mixed");
      mixed.title = t4(
        "来源目标策略不同,请展开后分别调整。",
        "Source goals use different policies; expand to edit them separately."
      );
      return mixed;
    }
    const control = document.createElement("span");
    control.className = "planning-policy-switch";
    control.setAttribute("role", "group");
    let current = value;
    const sync = () => {
      for (const button of control.querySelectorAll("button")) {
        const active = button.dataset.policy === current;
        button.dataset.active = String(active);
        button.setAttribute("aria-pressed", String(active));
      }
    };
    for (const [policy, zh, en] of POLICY_OPTIONS) {
      const button = document.createElement("button");
      button.type = "button";
      button.dataset.policy = policy;
      button.textContent = t4(zh, en);
      button.disabled = disabled;
      button.addEventListener("click", (event) => {
        event.preventDefault();
        event.stopPropagation();
        if (current === policy) return;
        current = policy;
        sync();
        onChange?.(policy);
      });
      control.append(button);
    }
    sync();
    return control;
  }
  function addStyles4() {
    if (document.getElementById(STYLE_ID4)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID4;
    style.textContent = `
    #${TAB_ID2}[data-active="true"]{color:#7dd3fc!important;font-weight:700}
    [data-mwitools-planning-active="true"] button:not(#${TAB_ID2}){border-color:var(--mwi-planning-idle-border,rgba(255,255,255,.16))!important;background:var(--mwi-planning-idle-background,rgba(255,255,255,.08))!important;box-shadow:var(--mwi-planning-idle-shadow,none)!important;color:var(--mwi-planning-idle-color,var(--color-text-secondary,#aeb5c0))!important;filter:none!important}
    #${PANEL_ID2}{box-sizing:border-box;width:100%;max-width:100%;min-width:0;max-height:calc(100% - 34px);overflow-x:hidden;overflow-y:auto;overflow-anchor:none;overscroll-behavior:contain;scrollbar-gutter:stable;padding:12px 12px 28px;color:var(--color-text-primary,#eee);background:#111b2b;font-family:"PingFang SC","Microsoft YaHei",Roboto,system-ui,sans-serif}
    #${PANEL_ID2} *{box-sizing:border-box}#${PANEL_ID2} button,#${PANEL_ID2} input,#${PANEL_ID2} select{font:inherit}
    .planning-intro{margin:0 0 10px;color:var(--color-text-secondary,#aeb5c0);font-size:.72rem;line-height:1.5}
    .planning-subtabs{display:flex;gap:4px;margin:0 0 10px;padding:3px;border:1px solid rgba(255,255,255,.09);border-radius:7px;background:#0c141f}.planning-subtabs button{flex:1;min-height:34px;border:0;border-radius:5px;background:transparent;color:#94a3b8;font-weight:700;cursor:pointer}.planning-subtabs button[data-active="true"]{background:#287fb4;color:#fff}.planning-page[hidden],.planning-stage[hidden]{display:none!important}.planning-stage-title{margin:0 0 10px;color:#dce8f5;font-size:.9rem}.planning-calculate-bar{display:flex;align-items:center;gap:10px;margin:10px 0;padding:9px 10px;border:1px solid rgba(56,189,248,.2);border-radius:8px;background:rgba(40,127,180,.08)}.planning-calculate-bar .planning-primary{margin-left:auto}.planning-dirty{color:#ffad62;font-size:.68rem}.planning-clean{color:#43d17f;font-size:.68rem}
    .planning-editor-grid{display:grid;grid-template-columns:1fr;gap:10px;margin-bottom:12px}
    .planning-add-card,.planning-section{position:relative;min-width:0;border:1px solid rgba(255,255,255,.09);border-radius:8px;background:#0c141f}
    .planning-add-title,.planning-section>h3,.planning-section-heading{min-height:38px;padding:9px 11px;border-bottom:1px solid rgba(255,255,255,.08);font-size:.82rem;font-weight:700}.planning-add-title{display:flex;align-items:center;gap:8px}.planning-add-title>span:first-child{min-width:max-content;flex:1}.planning-add-title .planning-policy-switch{width:min(100%,246px);min-width:232px}.planning-add-title .planning-policy-switch button{padding-inline:3px;font-size:clamp(.56rem,.76vw,.64rem);text-overflow:clip}
    .planning-add-body{display:flex;align-items:stretch;gap:7px;padding:9px;position:relative}
    .planning-search-wrap,.planning-house-wrap{position:relative;min-width:0;flex:1}
    .planning-search-input,.planning-count-input,.planning-level-select,.planning-picker-button{width:100%;height:34px;border:1px solid rgba(255,255,255,.16);border-radius:5px;outline:0;background:#18243a;color:#eef2f7;padding:5px 8px}
    .planning-search-input:focus,.planning-count-input:focus,.planning-level-select:focus,.planning-picker-button:focus{border-color:#38bdf8;box-shadow:0 0 0 2px rgba(56,189,248,.16)}
    .planning-count-input{width:82px;flex:0 0 82px;text-align:center}.planning-level-select{width:92px;flex:0 0 92px}
    .planning-primary{min-height:34px;border:0;border-radius:5px;background:#287fb4;color:#fff;padding:6px 12px;font-weight:700;white-space:nowrap;cursor:pointer}.planning-primary:hover{background:#3299d1}.planning-primary:disabled{opacity:.45;cursor:default}
    .planning-results{position:absolute;left:0;right:0;top:39px;z-index:20;display:none;max-height:min(390px,55vh);overflow:auto;padding:4px;border:1px solid rgba(125,211,252,.36);border-radius:6px;background:#182236;box-shadow:0 12px 28px rgba(0,0,0,.48)}.planning-results[data-open="true"]{display:block}
    .planning-option{display:flex;width:100%;min-width:0;align-items:center;gap:8px;border:0;border-bottom:1px solid rgba(152,167,233,.22);border-radius:4px;background:transparent;color:#eef2f7;padding:6px 7px;text-align:left;cursor:pointer}.planning-option:last-child{border-bottom:0}.planning-option:hover,.planning-option[data-active="true"]{background:#35425f}.planning-option-icon,.planning-picker-icon,.planning-goal-icon{display:grid;width:32px;height:32px;flex:0 0 32px;place-items:center;border-radius:5px;background:rgba(255,255,255,.05)}.planning-option-icon svg,.planning-picker-icon svg,.planning-goal-icon svg{width:28px;height:28px}.planning-icon-fallback{color:#aebbd2;font-size:.75rem;font-weight:700}.planning-option-copy{min-width:0;flex:1}.planning-option-copy strong,.planning-option-copy small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.planning-option-copy strong{font-size:.76rem}.planning-option-copy small{margin-top:2px;color:#94a3b8;font-size:.61rem}
    .planning-picker-button{display:flex;align-items:center;gap:7px;text-align:left;cursor:pointer}.planning-picker-icon{width:28px;height:28px;flex-basis:28px}.planning-picker-icon svg{width:25px;height:25px}.planning-picker-copy{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.planning-picker-arrow{color:#94a3b8}
    .planning-content-grid,.planning-results-column{display:grid;grid-template-columns:1fr;gap:10px;min-width:0}.planning-section{overflow:visible}.planning-section-heading{display:flex;align-items:center;justify-content:space-between}.planning-section-heading h3{margin:0;font-size:.82rem}.planning-empty{padding:18px 11px;color:#94a3b8;font-size:.72rem;text-align:center}
    .planning-goal{display:grid;grid-template-columns:18px 34px minmax(150px,1fr) minmax(216px,280px) 28px;align-items:center;gap:7px;padding:8px 9px;border-bottom:1px solid rgba(255,255,255,.065);font-size:.72rem;content-visibility:auto;contain-intrinsic-size:48px}.planning-goal:last-child{border-bottom:0}.planning-goal[data-enabled="false"]{opacity:.5}.planning-goal-values{display:flex;min-width:0;align-items:center;justify-content:center;gap:5px;color:#94a3b8;white-space:nowrap}.planning-goal-arrow{color:#64748b}.planning-goal input[type="number"]{width:70px;height:30px;border:1px solid rgba(255,255,255,.14);border-radius:5px;background:#18243a;color:#eef2f7;padding:4px 6px;text-align:center}.planning-remove{width:28px;height:28px;border:0;border-radius:5px;background:transparent;color:#ff8d96;font-size:1.05rem;cursor:pointer}.planning-remove:hover{background:rgba(224,90,100,.16)}
    .planning-policy-switch{display:inline-grid;grid-template-columns:repeat(3,minmax(0,1fr));width:100%;min-width:216px;padding:2px;border:1px solid rgba(255,255,255,.12);border-radius:6px;background:#111b2b}.planning-policy-switch button{min-width:0;min-height:26px;overflow:hidden;border:0;border-radius:4px;background:transparent;color:#94a3b8;padding:3px 4px;font-size:clamp(.52rem,.72vw,.61rem);line-height:1.15;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.planning-policy-switch button[data-active="true"]{background:#287fb4;color:#fff}.planning-policy-mixed{display:inline-flex;width:100%;min-width:216px;min-height:30px;align-items:center;justify-content:center;border:1px dashed rgba(255,255,255,.18);border-radius:6px;color:#ffad62;font-size:.65rem}.planning-step,.planning-material{border-bottom:1px solid rgba(255,255,255,.065);content-visibility:auto;contain-intrinsic-size:54px}.planning-step:last-child,.planning-material:last-child{border-bottom:0}.planning-step summary{display:grid;grid-template-columns:30px minmax(130px,1fr) minmax(92px,.4fr) minmax(216px,.85fr);align-items:center;gap:8px;padding:8px 9px;cursor:pointer;font-size:.72rem}.planning-material summary{display:flex;align-items:center;gap:8px;padding:8px 9px;cursor:pointer;font-size:.72rem}.planning-row-icon{display:grid;width:30px;height:30px;flex:0 0 30px;place-items:center;border-radius:5px;background:rgba(255,255,255,.05)}.planning-row-icon svg{width:27px;height:27px}.planning-step-name,.planning-material-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:700}.planning-step-count{color:#94a3b8;white-space:nowrap}.planning-source-list{display:grid;gap:5px;padding:0 9px 9px 47px}.planning-source-row{display:grid;grid-template-columns:minmax(140px,1fr) minmax(92px,.4fr) minmax(216px,.85fr);align-items:center;gap:8px;padding:6px;border-radius:5px;background:rgba(255,255,255,.035);color:#94a3b8;font-size:.64rem}.planning-source-copy{display:flex;min-width:0;align-items:center;gap:7px}.planning-source-copy strong{overflow:hidden;color:#d8e0ec;text-overflow:ellipsis;white-space:nowrap}.planning-source-icon{display:grid;width:26px;height:26px;flex:0 0 26px;place-items:center}.planning-source-icon svg{width:24px;height:24px}.planning-material-actions button{border:0;border-radius:5px;background:rgba(255,255,255,.08);color:#b8c2d3;padding:5px 8px;font-size:.66rem;font-weight:700;cursor:pointer}.planning-material[data-missing="true"] summary strong{color:#ffad62}.planning-material[data-missing="false"] summary strong{color:#43d17f}.planning-material summary strong{font-size:.67rem;white-space:nowrap}
    .planning-material-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:5px;padding:0 9px 8px}.planning-material-grid>div{min-width:0;padding:6px;border-radius:5px;background:rgba(255,255,255,.045)}.planning-material-grid span,.planning-material-grid small{display:block;overflow:hidden;color:#94a3b8;font-size:.58rem;text-overflow:ellipsis;white-space:nowrap}.planning-material-grid b{display:block;margin:2px 0;color:#e8c87f;font-size:.78rem}.planning-material-actions{display:flex;align-items:center;gap:6px;padding:0 9px 9px}.planning-material-actions span{min-width:0;flex:1;overflow:hidden;color:#94a3b8;font-size:.61rem;text-align:right;text-overflow:ellipsis;white-space:nowrap}.planning-material-actions button:disabled{opacity:.45;cursor:default}.planning-warning{margin:8px;padding:8px;border:1px solid rgba(255,173,98,.35);border-radius:6px;color:#ffad62;font-size:.67rem}.planning-footer{margin-top:10px;color:#94a3b8;font-size:.67rem;text-align:right}
    @media(max-width:900px){.planning-editor-grid,.planning-content-grid{grid-template-columns:1fr}.planning-editor-grid{gap:8px}.planning-add-body{flex-wrap:wrap}.planning-search-wrap,.planning-house-wrap{flex:1 1 calc(100% - 180px)}.planning-material-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.planning-goal{grid-template-columns:18px 34px minmax(130px,1fr) minmax(210px,260px) 28px}.planning-step summary{grid-template-columns:30px minmax(0,1fr) auto}.planning-step summary>.planning-policy-switch,.planning-step summary>.planning-policy-mixed{grid-column:2/4}.planning-source-row{grid-template-columns:minmax(0,1fr) auto}.planning-source-row>.planning-policy-switch{grid-column:1/3;min-width:0}}
    @media(max-width:760px){#${PANEL_ID2}{min-height:0;padding:10px 8px calc(22px + env(safe-area-inset-bottom,0px));overflow-y:auto;-webkit-overflow-scrolling:touch}.planning-add-title .planning-policy-switch{width:min(100%,238px);min-width:224px}.planning-count-input{width:70px;flex-basis:70px}.planning-level-select{width:84px;flex-basis:84px}.planning-goal{grid-template-columns:18px 34px minmax(0,1fr) 28px}.planning-goal>.planning-policy-switch,.planning-goal>.planning-policy-mixed{grid-column:2/5;min-width:0}.planning-goal-values{justify-content:flex-start}.planning-policy-switch button{font-size:.54rem}.planning-add-title .planning-policy-switch button{font-size:.58rem}}
    @media(max-width:420px){.planning-add-title{display:grid;grid-template-columns:1fr}.planning-add-title .planning-policy-switch{width:100%;min-width:0}}
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function createOption(candidate, kind, onSelect) {
    const button = document.createElement("button");
    button.type = "button";
    button.className = "planning-option";
    const icon = kind === "item" ? itemIcon(candidate.hrid, candidate.name) : houseIcon(candidate);
    const meta = kind === "item" ? candidate.hrid : `${t4("当前", "Current")} ${currentHouseLevel2(candidate.hrid)} · ${t4("最高", "Max")} ${maxHouseLevel2(candidate.hrid)}`;
    const iconHost = document.createElement("span");
    iconHost.className = "planning-option-icon";
    iconHost.innerHTML = icon;
    const copy = document.createElement("span");
    copy.className = "planning-option-copy";
    const name = document.createElement("strong");
    name.textContent = candidate.name;
    const detail = document.createElement("small");
    detail.textContent = meta;
    copy.append(name, detail);
    button.append(iconHost, copy);
    button.addEventListener("pointerdown", (event) => event.preventDefault());
    button.addEventListener("click", () => onSelect(candidate));
    return button;
  }
  function createItemPicker(host, onSelect, cleanup4) {
    const candidates = itemCandidates();
    const input = document.createElement("input");
    input.type = "search";
    input.className = "planning-search-input";
    input.placeholder = t4("搜索物品名称、英文名或 HRID", "Search name or HRID");
    input.setAttribute("aria-label", t4("选择规划物品", "Choose planning item"));
    const results = document.createElement("div");
    results.className = "planning-results";
    let selected = null;
    let shown = [];
    let activeIndex = 0;
    const close = () => {
      results.dataset.open = "false";
    };
    const choose = (candidate) => {
      if (!candidate) return;
      selected = candidate;
      input.value = candidate.name;
      close();
      onSelect(candidate);
    };
    const render = () => {
      const query = input.value.trim().toLowerCase();
      shown = candidates.filter(
        (candidate) => !query || candidate.name.toLowerCase().includes(query) || candidate.english.toLowerCase().includes(query) || candidate.hrid.toLowerCase().includes(query)
      ).slice(0, 80);
      activeIndex = Math.min(activeIndex, Math.max(0, shown.length - 1));
      results.replaceChildren(
        ...shown.map((candidate, index) => {
          const option = createOption(candidate, "item", choose);
          option.dataset.active = String(index === activeIndex);
          return option;
        })
      );
      results.dataset.open = String(shown.length > 0);
    };
    const syncActive = () => {
      [...results.children].forEach((option, index) => {
        option.dataset.active = String(index === activeIndex);
      });
      results.children[activeIndex]?.scrollIntoView?.({ block: "nearest" });
    };
    input.addEventListener("focus", render);
    input.addEventListener("input", () => {
      selected = null;
      activeIndex = 0;
      render();
    });
    input.addEventListener("keydown", (event) => {
      if (event.key === "ArrowDown" || event.key === "ArrowUp") {
        event.preventDefault();
        if (results.dataset.open !== "true") render();
        activeIndex = Math.max(
          0,
          Math.min(
            shown.length - 1,
            activeIndex + (event.key === "ArrowDown" ? 1 : -1)
          )
        );
        syncActive();
      } else if (event.key === "Enter") {
        event.preventDefault();
        choose(shown[activeIndex] ?? resolveItemInput(input.value, candidates));
      } else if (event.key === "Escape") {
        close();
      }
    });
    const outside = (event) => {
      if (!host.contains(event.target)) close();
    };
    document.addEventListener("pointerdown", outside, true);
    cleanup4.push(
      () => document.removeEventListener("pointerdown", outside, true)
    );
    host.append(input, results);
    return {
      input,
      getSelected: () => selected ?? resolveItemInput(input.value, candidates),
      clear() {
        selected = null;
        input.value = "";
        close();
      }
    };
  }
  function createHousePicker(host, onSelect, cleanup4) {
    const candidates = houseCandidates();
    const button = document.createElement("button");
    button.type = "button";
    button.className = "planning-picker-button";
    button.setAttribute("aria-label", t4("选择房屋", "Choose house"));
    const results = document.createElement("div");
    results.className = "planning-results";
    let selected = candidates[0] ?? null;
    const paintButton = () => {
      button.replaceChildren();
      const copy = document.createElement("span");
      copy.className = "planning-picker-copy";
      if (!selected) {
        copy.textContent = t4("暂无房屋", "No houses");
        button.append(copy);
        return;
      }
      const icon = document.createElement("span");
      icon.className = "planning-picker-icon";
      icon.innerHTML = houseIcon(selected);
      copy.textContent = selected.name;
      const arrow = document.createElement("span");
      arrow.className = "planning-picker-arrow";
      arrow.textContent = "▾";
      button.append(icon, copy, arrow);
    };
    const close = () => {
      results.dataset.open = "false";
    };
    const choose = (candidate) => {
      selected = candidate;
      paintButton();
      close();
      onSelect(candidate);
    };
    const renderOptions = () => {
      results.replaceChildren(
        ...candidates.map(
          (candidate) => createOption(candidate, "house", choose)
        )
      );
    };
    button.addEventListener("click", (event) => {
      event.stopPropagation();
      const opening = results.dataset.open !== "true";
      if (opening) renderOptions();
      results.dataset.open = String(opening);
    });
    const outside = (event) => {
      if (!host.contains(event.target)) close();
    };
    document.addEventListener("pointerdown", outside, true);
    cleanup4.push(
      () => document.removeEventListener("pointerdown", outside, true)
    );
    paintButton();
    host.append(button, results);
    return {
      getSelected: () => selected,
      refresh() {
        paintButton();
        results.querySelectorAll(".planning-option-copy small").forEach((meta, index) => {
          const candidate = candidates[index];
          meta.textContent = `${t4("当前", "Current")} ${currentHouseLevel2(candidate.hrid)} · ${t4("最高", "Max")} ${maxHouseLevel2(candidate.hrid)}`;
        });
      }
    };
  }
  function createPlanningEditor(host, cleanup4) {
    const grid = document.createElement("div");
    grid.className = "planning-editor-grid";
    const title = (zh, en, kind) => {
      const heading = document.createElement("div");
      heading.className = "planning-add-title";
      const copy = document.createElement("span");
      copy.textContent = t4(zh, en);
      let defaultPolicy = planning.getDefaultPolicy(kind);
      const policy = createPolicyControl(defaultPolicy, (next) => {
        defaultPolicy = next;
        planning.setDefaultPolicy(kind, next);
      });
      heading.append(copy, policy);
      return { heading, getPolicy: () => defaultPolicy };
    };
    const itemCard = document.createElement("section");
    itemCard.className = "planning-add-card";
    const itemTitle = title("添加物品目标", "Add item target", "item");
    itemCard.append(itemTitle.heading);
    const itemBody = document.createElement("div");
    itemBody.className = "planning-add-body";
    const itemSearch = document.createElement("div");
    itemSearch.className = "planning-search-wrap";
    let chosenItem = null;
    const itemPicker = createItemPicker(
      itemSearch,
      (candidate) => {
        chosenItem = candidate;
      },
      cleanup4
    );
    const itemCount = document.createElement("input");
    itemCount.type = "number";
    itemCount.min = "1";
    itemCount.step = "1";
    itemCount.value = "1";
    itemCount.className = "planning-count-input";
    itemCount.setAttribute("aria-label", t4("最终持有量", "Final quantity"));
    const itemAdd = document.createElement("button");
    itemAdd.type = "button";
    itemAdd.className = "planning-primary";
    itemAdd.textContent = t4("添加", "Add");
    const submitItem = () => {
      const candidate = chosenItem ?? itemPicker.getSelected();
      if (!candidate) {
        itemPicker.input.setCustomValidity?.(
          t4("请先选择有效物品", "Choose a valid item")
        );
        itemPicker.input.reportValidity?.();
        return;
      }
      itemPicker.input.setCustomValidity?.("");
      planning.upsertGoal({
        kind: "item",
        targetHrid: candidate.hrid,
        target: itemCount.value,
        policy: itemTitle.getPolicy()
      });
      chosenItem = null;
      itemPicker.clear();
      itemCount.value = "1";
    };
    itemAdd.addEventListener("click", submitItem);
    itemCount.addEventListener("keydown", (event) => {
      if (event.key === "Enter") submitItem();
    });
    itemBody.append(itemSearch, itemCount, itemAdd);
    itemCard.append(itemBody);
    const houseCard = document.createElement("section");
    houseCard.className = "planning-add-card";
    const houseTitle = title("添加房屋目标", "Add house target", "house");
    houseCard.append(houseTitle.heading);
    const houseBody = document.createElement("div");
    houseBody.className = "planning-add-body";
    const houseWrap = document.createElement("div");
    houseWrap.className = "planning-house-wrap";
    const level = document.createElement("select");
    level.className = "planning-level-select";
    level.setAttribute("aria-label", t4("房屋目标等级", "House target level"));
    let levelSignature = "";
    let levelHouseHrid = "";
    const refreshLevels = (candidate) => {
      const signature = candidate ? `${candidate.hrid}:${currentHouseLevel2(candidate.hrid)}:${maxHouseLevel2(candidate.hrid)}` : "none";
      if (signature === levelSignature) return;
      const previous = candidate?.hrid === levelHouseHrid ? String(level.value ?? "") : "";
      levelSignature = signature;
      levelHouseHrid = candidate?.hrid ?? "";
      level.replaceChildren();
      if (!candidate) {
        level.disabled = true;
        return;
      }
      const current = currentHouseLevel2(candidate.hrid);
      for (let targetLevel = current + 1; targetLevel <= maxHouseLevel2(candidate.hrid); targetLevel += 1) {
        const option = document.createElement("option");
        option.value = String(targetLevel);
        option.textContent = `${t4("等级", "Level")} ${targetLevel}`;
        level.append(option);
      }
      level.disabled = level.options.length === 0;
      if ([...level.options].some((option) => option.value === previous)) {
        level.value = previous;
      }
    };
    const housePicker = createHousePicker(houseWrap, refreshLevels, cleanup4);
    refreshLevels(housePicker.getSelected());
    const houseAdd = document.createElement("button");
    houseAdd.type = "button";
    houseAdd.className = "planning-primary";
    houseAdd.textContent = t4("添加", "Add");
    houseAdd.addEventListener("click", () => {
      const candidate = housePicker.getSelected();
      if (!candidate || !level.value) return;
      planning.upsertGoal({
        kind: "house",
        targetHrid: candidate.hrid,
        target: level.value,
        policy: houseTitle.getPolicy()
      });
    });
    houseBody.append(houseWrap, level, houseAdd);
    houseCard.append(houseBody);
    grid.append(itemCard, houseCard);
    host.append(grid);
    return {
      refresh() {
        housePicker.refresh();
        refreshLevels(housePicker.getSelected());
      }
    };
  }
  function renderGoals(host, goals) {
    host.replaceChildren();
    const section = document.createElement("section");
    section.className = "planning-section";
    const heading = document.createElement("h3");
    heading.textContent = `${t4("规划目标", "Planning goals")} · ${goals.length}`;
    section.append(heading);
    if (!goals.length) {
      const empty = document.createElement("div");
      empty.className = "planning-empty";
      empty.textContent = t4(
        "从上方选择物品或房屋开始规划。",
        "Choose an item or house above to begin."
      );
      section.append(empty);
    }
    for (const goal of goals) {
      const row = document.createElement("article");
      row.className = "planning-goal";
      row.dataset.enabled = String(goal.enabled);
      const toggle = document.createElement("input");
      toggle.type = "checkbox";
      toggle.checked = goal.enabled;
      toggle.title = t4("启用目标", "Enable goal");
      toggle.addEventListener("change", () => {
        planning.updateGoal(goal.id, { enabled: toggle.checked });
      });
      const icon = document.createElement("span");
      icon.className = "planning-goal-icon";
      icon.innerHTML = goalIcon(goal);
      icon.title = goalLabel(goal);
      icon.setAttribute("role", "img");
      icon.setAttribute("aria-label", goalLabel(goal));
      const values = document.createElement("div");
      values.className = "planning-goal-values";
      const current = document.createElement("span");
      current.className = "planning-goal-current";
      current.dataset.goalId = goal.id;
      current.textContent = `${t4("当前", "Current")} ${number(goal.kind === "house" ? currentHouseLevel2(goal.targetHrid) : procurement2.getInventoryCount(goal.targetHrid, 0))}`;
      const arrow = document.createElement("span");
      arrow.className = "planning-goal-arrow";
      arrow.textContent = "→";
      const target = document.createElement("input");
      target.type = "number";
      target.min = "1";
      target.max = goal.kind === "house" ? String(maxHouseLevel2(goal.targetHrid)) : "";
      target.step = "1";
      target.value = String(goal.target);
      target.title = goal.kind === "house" ? t4("目标等级", "Target level") : t4("最终持有量", "Final quantity");
      target.addEventListener("change", () => {
        planning.updateGoal(goal.id, { target: target.value });
      });
      values.append(current, arrow, target);
      const policy = createPolicyControl(goal.policy, (next) => {
        planning.setGoalPolicy(goal.id, next);
      });
      const remove = document.createElement("button");
      remove.type = "button";
      remove.className = "planning-remove";
      remove.textContent = "×";
      remove.title = t4("删除", "Remove");
      remove.addEventListener("click", () => planning.removeGoal(goal.id));
      row.append(toggle, icon, values, policy, remove);
      section.append(row);
    }
    host.append(section);
  }
  function updateGoalCurrentValues(host, goals) {
    const nodes = new Map(
      [...host.querySelectorAll(".planning-goal-current")].map((node) => [
        node.dataset.goalId,
        node
      ])
    );
    for (const goal of goals) {
      const node = nodes.get(goal.id);
      if (!node) continue;
      const current = goal.kind === "house" ? currentHouseLevel2(goal.targetHrid) : procurement2.getInventoryCount(goal.targetHrid, 0);
      node.textContent = `${t4("当前", "Current")} ${number(current)}`;
    }
  }
  function renderSteps(host, result) {
    host.replaceChildren();
    const section = document.createElement("section");
    section.className = "planning-section";
    const heading = document.createElement("h3");
    heading.textContent = `${t4("第 2 步:选择制作方式", "Step 2: Choose production methods")} · ${result.nodes.length}`;
    section.append(heading);
    if (!result.nodes.length) {
      const empty = document.createElement("div");
      empty.className = "planning-empty";
      empty.textContent = t4("当前没有需要制作的物品。", "Nothing to produce.");
      section.append(empty);
    }
    for (const node of result.nodes) {
      const row = document.createElement("details");
      row.className = "planning-step";
      const summary = document.createElement("summary");
      const icon = document.createElement("span");
      icon.className = "planning-row-icon";
      icon.innerHTML = itemIcon(node.itemHrid);
      const label = document.createElement("span");
      label.className = "planning-step-name";
      label.textContent = node.name;
      const required = document.createElement("span");
      required.className = "planning-step-count";
      required.textContent = `${t4("所需", "Required")} ${number(node.requiredAfterSupply ?? node.requiredOutput)}`;
      const policy = createPolicyControl(node.policy, (next) => {
        for (const goalId of new Set(
          node.branches.map((branch) => branch.goalId)
        )) {
          planning.setNodePolicy(goalId, node.itemHrid, next);
        }
      });
      summary.append(icon, label, required, policy);
      const sources = document.createElement("div");
      sources.className = "planning-source-list";
      const goals = new Map(result.goals.map((goal) => [goal.id, goal]));
      for (const branch of node.branches) {
        const source = document.createElement("div");
        source.className = "planning-source-row";
        const sourceCopy = document.createElement("span");
        sourceCopy.className = "planning-source-copy";
        const sourceIcon = document.createElement("span");
        sourceIcon.className = "planning-source-icon";
        const goal = goals.get(branch.goalId);
        if (goal) sourceIcon.innerHTML = goalIcon(goal);
        const name = document.createElement("strong");
        name.textContent = goal ? goalLabel(goal) : branch.goalId;
        name.title = name.textContent;
        sourceCopy.append(sourceIcon, name);
        const count = document.createElement("span");
        count.textContent = `${t4("所需", "Required")} ${number(branch.remaining ?? branch.requiredOutput)}`;
        const branchPolicy = createPolicyControl(branch.policy, (next) => {
          planning.setNodePolicy(branch.goalId, node.itemHrid, next);
        });
        source.append(sourceCopy, count, branchPolicy);
        sources.append(source);
      }
      row.append(summary, sources);
      section.append(row);
    }
    host.append(section);
  }
  function renderMaterials(host, result) {
    host.replaceChildren();
    const section = document.createElement("section");
    section.className = "planning-section";
    const heading = document.createElement("div");
    heading.className = "planning-section-heading";
    const title = document.createElement("h3");
    title.textContent = `${t4("基础材料", "Base materials")} · ${result.materials.length}`;
    const addAll = document.createElement("button");
    addAll.type = "button";
    addAll.className = "planning-primary";
    addAll.textContent = t4("一键补齐", "Add all");
    addAll.disabled = !result.materials.some(
      (material) => material.purchasable && material.addableShortage > 0
    );
    addAll.addEventListener("click", () => {
      planning.addShortagesToCart(result.materials);
    });
    heading.append(title, addAll);
    section.append(heading);
    if (!result.materials.length) {
      const empty = document.createElement("div");
      empty.className = "planning-empty";
      empty.textContent = t4(
        "当前没有基础材料需求。",
        "No base materials needed."
      );
      section.append(empty);
    }
    for (const material of result.materials) {
      const row = document.createElement("details");
      row.className = "planning-material";
      row.dataset.missing = String(material.remainingShortage > 0);
      const summary = document.createElement("summary");
      const icon = document.createElement("span");
      icon.className = "planning-row-icon";
      icon.innerHTML = itemIcon(material.itemHrid, material.name);
      const name = document.createElement("span");
      name.className = "planning-material-name";
      name.textContent = material.name;
      name.title = material.itemHrid;
      const missing = document.createElement("strong");
      missing.textContent = material.remainingShortage ? `${t4("还需", "Need")} ${wholeNumber(material.remainingShortage)}` : t4("已覆盖", "Covered");
      summary.append(icon, name, missing);
      const grid = document.createElement("div");
      grid.className = "planning-material-grid";
      const metric4 = (label, value, sub = "") => {
        const cell = document.createElement("div");
        cell.innerHTML = `<span>${label}</span><b>${value}</b>${sub ? `<small>${sub}</small>` : ""}`;
        return cell;
      };
      grid.append(
        metric4(t4("规划需求", "Required"), wholeNumber(material.required)),
        metric4(
          t4("库存", "Inventory"),
          wholeNumber(material.owned),
          `${t4("项目占用", "Project")} ${wholeNumber(material.projectInventory)} · ${t4("规划使用", "Planning")} ${wholeNumber(material.inventoryUsed)}`
        ),
        metric4(
          t4("购物车", "Cart"),
          wholeNumber(material.cart.total),
          `${t4("项目", "Project")} ${wholeNumber(material.cart.project)} · ${t4("规划", "Planning")} ${wholeNumber(material.cart.planning)} · ${t4("手工", "Manual")} ${wholeNumber(material.cart.manual)}`
        ),
        metric4(
          t4("材料缺口", "Shortage"),
          wholeNumber(material.remainingShortage)
        )
      );
      const actions = document.createElement("div");
      actions.className = "planning-material-actions";
      const add = document.createElement("button");
      add.type = "button";
      add.textContent = material.purchasable ? t4("加入购物车", "Add to cart") : t4("不可购买", "Not tradable");
      add.disabled = !material.purchasable || !material.addableShortage;
      add.addEventListener(
        "click",
        () => planning.addShortagesToCart([material])
      );
      const source = document.createElement("span");
      source.textContent = `${t4("来源", "Sources")}: ${goalSources(material.sourceIds, result.goals)}`;
      actions.append(add, source);
      row.append(summary, grid, actions);
      section.append(row);
    }
    host.append(section);
  }
  var PlanningPanel = class {
    constructor(host) {
      this.host = host;
      this.cleanup = [];
      this.signatures = {};
      this.route = "targets";
      this.result = planning.getResult();
      this.decisionResult = planning.getDecisionResult();
      this.catalogDirty = false;
      this.houseDirty = false;
      this.updatePending = false;
      this.updateCount = 0;
      this.build();
      this.catalogSignature = catalogSignature();
      this.unsubscribe = [
        procurement2.on("planning:change", () => this.scheduleUpdate()),
        procurement2.on("cart:change", () => this.scheduleUpdate()),
        procurement2.on("plan:change", () => this.scheduleUpdate()),
        procurement2.on("inventory:change", () => this.scheduleUpdate())
      ];
      this.updateScheduler = createFrameScheduler(() => this.update());
      this.update();
    }
    build() {
      this.host.replaceChildren();
      const tabs = document.createElement("nav");
      tabs.className = "planning-subtabs";
      const targetTab = document.createElement("button");
      targetTab.type = "button";
      targetTab.dataset.route = "targets";
      targetTab.textContent = t4("目标", "Targets");
      const listTab = document.createElement("button");
      listTab.type = "button";
      listTab.dataset.route = "list";
      listTab.textContent = t4("清单", "List");
      targetTab.addEventListener("click", () => this.setRoute("targets"));
      listTab.addEventListener("click", () => this.setRoute("list"));
      tabs.append(targetTab, listTab);
      this.targetPage = document.createElement("div");
      this.targetPage.className = "planning-page";
      this.targetPage.dataset.page = "targets";
      const stageOneTitle = document.createElement("h2");
      stageOneTitle.className = "planning-stage-title";
      stageOneTitle.textContent = t4(
        "第 1 步:选择目标",
        "Step 1: Choose targets"
      );
      const intro = document.createElement("p");
      intro.className = "planning-intro";
      intro.textContent = t4(
        "设置目标不会自动重算。房屋成本固定;计算第 2 步时,制作链才会读取当前茶饮、装备、社区 Buff、暴饮之囊、库存和安全余量。",
        "Editing targets does not recalculate automatically. House costs stay fixed; calculating Step 2 reads current buffs, inventory, and safety margins."
      );
      const calculateBar = document.createElement("div");
      calculateBar.className = "planning-calculate-bar";
      this.targetStatus = document.createElement("span");
      this.targetCalculate = document.createElement("button");
      this.targetCalculate.type = "button";
      this.targetCalculate.className = "planning-primary";
      this.targetCalculate.textContent = t4("计算第 2 步", "Calculate Step 2");
      this.targetCalculate.addEventListener(
        "click",
        () => this.calculateDecisions()
      );
      calculateBar.append(this.targetStatus, this.targetCalculate);
      this.editorHost = document.createElement("div");
      this.goalsHost = document.createElement("div");
      this.decisionStage = document.createElement("div");
      this.decisionStage.className = "planning-stage";
      this.stepsHost = document.createElement("div");
      const stageTwoBar = document.createElement("div");
      stageTwoBar.className = "planning-calculate-bar";
      this.decisionStatus = document.createElement("span");
      this.materialCalculate = document.createElement("button");
      this.materialCalculate.type = "button";
      this.materialCalculate.className = "planning-primary";
      this.materialCalculate.textContent = t4("计算第 3 步", "Calculate Step 3");
      this.materialCalculate.addEventListener(
        "click",
        () => this.calculateMaterials()
      );
      stageTwoBar.append(this.decisionStatus, this.materialCalculate);
      this.decisionStage.append(this.stepsHost, stageTwoBar);
      this.targetPage.append(
        stageOneTitle,
        intro,
        this.editorHost,
        this.goalsHost,
        calculateBar,
        this.decisionStage
      );
      this.listPage = document.createElement("div");
      this.listPage.className = "planning-page planning-results-column";
      this.listPage.dataset.page = "list";
      const listBar = document.createElement("div");
      listBar.className = "planning-calculate-bar";
      const stageThreeTitle = document.createElement("strong");
      stageThreeTitle.textContent = t4(
        "第 3 步:基础材料清单",
        "Step 3: Base-material list"
      );
      this.listStatus = document.createElement("span");
      listBar.append(stageThreeTitle, this.listStatus);
      this.materialsHost = document.createElement("div");
      this.warningHost = document.createElement("div");
      this.footer = document.createElement("div");
      this.footer.className = "planning-footer";
      this.listPage.append(
        listBar,
        this.materialsHost,
        this.warningHost,
        this.footer
      );
      this.host.append(tabs, this.targetPage, this.listPage);
      this.editor = createPlanningEditor(this.editorHost, this.cleanup);
      this.setRoute(this.route);
      this.renderDecision(true);
      this.renderResult(true);
    }
    setRoute(route) {
      this.route = route === "list" ? "list" : "targets";
      for (const button of this.host.querySelectorAll(
        ".planning-subtabs button"
      )) {
        button.dataset.active = String(button.dataset.route === this.route);
      }
      this.targetPage.hidden = this.route !== "targets";
      this.listPage.hidden = this.route !== "list";
    }
    blurCalculationControl() {
      const activeElement = document.activeElement;
      if (activeElement && this.host.contains(activeElement)) {
        activeElement.blur?.();
      }
    }
    scrollStageWithinPanel(stage) {
      if (!stage || !this.host) return;
      const panelRect = this.host.getBoundingClientRect?.();
      const stageRect = stage.getBoundingClientRect?.();
      if (!panelRect || !stageRect) return;
      const target = Number(this.host.scrollTop || 0) + stageRect.top - panelRect.top - 8;
      this.host.scrollTop = Math.max(0, target);
    }
    calculateDecisions() {
      this.blurCalculationControl();
      this.decisionResult = planning.calculateDecisions();
      this.signatures.decisions = null;
      this.renderDecision(true);
      this.updateStatus();
      this.scrollStageWithinPanel(this.decisionStage);
    }
    calculateMaterials() {
      this.blurCalculationControl();
      this.result = planning.calculateMaterials();
      this.signatures.materials = null;
      this.renderResult(true);
      this.updateStatus();
      this.setRoute("list");
      this.host.scrollTop = 0;
    }
    scheduleUpdate({ catalog = false, house = false } = {}) {
      this.catalogDirty ||= catalog;
      this.houseDirty ||= house;
      if (this.host.hidden) {
        this.updatePending = true;
        return;
      }
      this.updateScheduler?.schedule();
    }
    update() {
      if (!this.host?.isConnected) return;
      this.updatePending = false;
      this.updateCount += 1;
      if (this.catalogDirty) {
        const nextCatalogSignature = catalogSignature();
        this.catalogDirty = false;
        if (this.catalogSignature !== nextCatalogSignature) {
          const route = this.route;
          const result = this.result;
          const decisionResult2 = this.decisionResult;
          this.cleanup.forEach((dispose) => dispose());
          this.cleanup = [];
          this.signatures = {};
          this.route = route;
          this.result = result;
          this.decisionResult = decisionResult2;
          this.build();
          this.catalogSignature = nextCatalogSignature;
        }
      }
      if (this.houseDirty) {
        this.houseDirty = false;
        this.editor?.refresh();
      }
      const goals = planning.getGoals();
      const goalsSignature = JSON.stringify(goals);
      if (this.signatures.goals !== goalsSignature) {
        renderGoals(this.goalsHost, goals);
        this.signatures.goals = goalsSignature;
      } else {
        updateGoalCurrentValues(this.goalsHost, goals);
      }
      const currentDecision = planning.getDecisionResult();
      if (currentDecision !== this.decisionResult) {
        this.decisionResult = currentDecision;
        this.renderDecision(true);
      } else if (currentDecision) {
        this.renderDecision();
      }
      this.updateStatus();
    }
    updateStatus() {
      const dirty = planning.isDirty();
      const diagnostics = planning.getDiagnostics();
      const listCopy = dirty ? t4("目标已更改,等待计算", "Targets changed; calculation pending") : diagnostics.lastCalculatedAt ? t4("当前清单已计算", "List is up to date") : t4("尚未计算", "Not calculated yet");
      this.listStatus.className = dirty ? "planning-dirty" : "planning-clean";
      this.listStatus.textContent = listCopy;
      const decisionReady = Boolean(this.decisionResult);
      this.targetStatus.className = decisionReady ? "planning-clean" : "planning-dirty";
      this.targetStatus.textContent = decisionReady ? t4("第 2 步已生成", "Step 2 is ready") : t4("等待计算第 2 步", "Step 2 is pending");
      this.decisionStatus.className = "planning-dirty";
      this.decisionStatus.textContent = t4(
        "调整策略不会自动计算材料",
        "Policy edits do not calculate materials automatically"
      );
    }
    renderDecision(force = false) {
      const result = this.decisionResult;
      this.decisionStage.hidden = !result;
      if (!result) {
        this.stepsHost.replaceChildren();
        this.signatures.decisions = null;
        return;
      }
      const signature = JSON.stringify(result.nodes);
      if (force || this.signatures.decisions !== signature) {
        renderSteps(this.stepsHost, result);
        this.signatures.decisions = signature;
      }
    }
    renderResult(force = false) {
      const result = this.result;
      if (!result) {
        if (!force && this.signatures.empty) return;
        this.materialsHost.replaceChildren();
        const empty = document.createElement("div");
        empty.className = "planning-empty planning-section";
        empty.textContent = t4(
          "请在“目标”页点击开始计算。",
          "Choose Start calculation on the Targets tab."
        );
        this.materialsHost.append(empty);
        this.warningHost.replaceChildren();
        this.footer.textContent = "";
        this.signatures.empty = true;
        return;
      }
      this.signatures.empty = false;
      const materialsSignature = JSON.stringify(result.materials);
      if (force || this.signatures.materials !== materialsSignature) {
        renderMaterials(this.materialsHost, result);
        this.signatures.materials = materialsSignature;
      }
      this.warningHost.replaceChildren();
      if (result.warnings.length) {
        const warning = document.createElement("div");
        warning.className = "planning-warning";
        warning.textContent = t4(
          `有 ${result.warnings.length} 条链路出现循环或超过深度限制,已作为基础材料显示。`,
          `${result.warnings.length} paths contained a cycle or exceeded the depth limit and were shown as base materials.`
        );
        this.warningHost.append(warning);
      }
      this.footer.textContent = t4(
        `规划 ${result.goals.length} 项 · 基础材料 ${result.materials.length} 种`,
        `${result.goals.length} goals · ${result.materials.length} base materials`
      );
    }
    refreshCatalog() {
      this.scheduleUpdate({ catalog: true, house: true });
    }
    refreshHouses() {
      this.scheduleUpdate({ house: true });
    }
    destroy() {
      this.updateScheduler?.cancel();
      this.unsubscribe?.forEach((unsubscribe) => unsubscribe?.());
      this.cleanup.forEach((dispose) => dispose());
      this.cleanup = [];
    }
  };
  function isCompactViewport2() {
    return window.matchMedia?.("(max-width:760px)")?.matches ?? Number(window.innerWidth) <= 760;
  }
  function isRelevantPlanningMountAttribute(target, navigationBranch) {
    const management = target?.closest?.(
      '[class*="CharacterManagement_characterManagement"]'
    );
    if (!management) return false;
    if (target === management || target === navigationBranch || target?.parentElement === navigationBranch || navigationBranch && target?.contains?.(navigationBranch)) {
      return true;
    }
    return target?.querySelectorAll?.('button[role="tab"]')?.length >= 5;
  }
  function isRelevantPlanningMountNode(node) {
    if (node?.nodeType !== 1) return false;
    if (node.matches?.(`#${TAB_ID2},#${PANEL_ID2}`) || node.closest?.(`#${TAB_ID2},#${PANEL_ID2}`)) {
      return false;
    }
    return Boolean(
      node.matches?.(`#${ASSET_TAB_ID}`) || node.querySelector?.(`#${ASSET_TAB_ID}`) || node.matches?.('[class*="CharacterManagement_characterManagement"]') || node.querySelector?.('[class*="CharacterManagement_characterManagement"]')
    );
  }
  function createPlanningUi({ scope }) {
    let active = false;
    let tab = null;
    let host = null;
    let panel = null;
    let shell2 = null;
    let navigationBranch = null;
    let lastActiveNativeTab = null;
    const hiddenNodes = /* @__PURE__ */ new Map();
    const restoreNative = () => {
      for (const [node, state] of hiddenNodes) {
        node.hidden = state.hidden;
        if (state.styleDisplay === null) node.style.removeProperty("display");
        else node.style.display = state.styleDisplay;
      }
      hiddenNodes.clear();
    };
    const clearTabOverride = () => {
      if (!navigationBranch) return;
      delete navigationBranch.dataset.mwitoolsPlanningActive;
      for (const property of [
        "--mwi-planning-idle-background",
        "--mwi-planning-idle-border",
        "--mwi-planning-idle-color",
        "--mwi-planning-idle-shadow"
      ]) {
        navigationBranch.style.removeProperty(property);
      }
    };
    const captureIdleStyle = () => {
      if (!navigationBranch || typeof getComputedStyle !== "function") return;
      const idle = navigationBranch.querySelector(
        `button:not(#${TAB_ID2}):not([aria-selected="true"]):not(.Mui-selected)`
      );
      if (!idle) return;
      const style = getComputedStyle(idle);
      const background = style.background || style.backgroundColor;
      if (background && background !== "transparent") {
        navigationBranch.style.setProperty(
          "--mwi-planning-idle-background",
          background
        );
      }
      if (style.borderColor) {
        navigationBranch.style.setProperty(
          "--mwi-planning-idle-border",
          style.borderColor
        );
      }
      if (style.color) {
        navigationBranch.style.setProperty(
          "--mwi-planning-idle-color",
          style.color
        );
      }
      if (style.boxShadow && style.boxShadow !== "none") {
        navigationBranch.style.setProperty(
          "--mwi-planning-idle-shadow",
          style.boxShadow
        );
      }
    };
    const ensurePanel = () => {
      if (panel || !host?.isConnected) return false;
      panel = new PlanningPanel(host);
      return true;
    };
    const syncViewport = () => {
      if (!host) return;
      if (!isCompactViewport2() || !active) {
        host.style.removeProperty("height");
        host.style.removeProperty("max-height");
        return;
      }
      const top = Math.max(0, Math.round(host.getBoundingClientRect().top));
      const available = `calc(100dvh - ${top}px - env(safe-area-inset-bottom,0px))`;
      host.style.height = available;
      host.style.maxHeight = available;
    };
    const setActive = (next) => {
      const nextActive = Boolean(next);
      const activating = nextActive && !active;
      if (nextActive && !active) captureIdleStyle();
      active = nextActive;
      if (tab) {
        tab.dataset.active = String(active);
        tab.setAttribute("aria-selected", String(active));
        tab.classList.toggle("Mui-selected", active);
        if (tab.hasAttribute("data-selected")) {
          tab.dataset.selected = String(active);
        }
        if (tab.hasAttribute("data-state")) {
          tab.dataset.state = active ? "active" : "inactive";
        }
        if (!active) tab.blur();
      }
      if (navigationBranch && active) {
        const selected = navigationBranch.querySelector(
          `button[aria-selected="true"]:not(#${TAB_ID2}):not(#${ASSET_TAB_ID}),button.Mui-selected:not(#${TAB_ID2}):not(#${ASSET_TAB_ID})`
        );
        if (selected) lastActiveNativeTab = selected;
      }
      if (host) host.hidden = !active;
      if (!active) {
        restoreNative();
        clearTabOverride();
        syncViewport();
        return;
      }
      navigationBranch.dataset.mwitoolsPlanningActive = "true";
      const panelCreated = ensurePanel();
      for (const node of [...shell2?.children ?? []]) {
        if (node === navigationBranch || node === host || node.tagName === "STYLE") {
          continue;
        }
        if (!hiddenNodes.has(node)) {
          hiddenNodes.set(node, {
            hidden: node.hidden,
            styleDisplay: node.style.display || null
          });
        }
        node.hidden = true;
        node.style.display = "none";
      }
      syncViewport();
      if (!panelCreated && (activating || panel?.updatePending)) panel?.update();
    };
    const teardown = () => {
      setActive(false);
      panel?.destroy();
      panel = null;
      tab?.remove();
      host?.remove();
      tab = null;
      host = null;
      shell2 = null;
      navigationBranch = null;
      lastActiveNativeTab = null;
    };
    const mount = (anchor, found) => {
      ({ shell: shell2, navigationBranch } = found);
      tab = anchor.cloneNode(true);
      tab.id = TAB_ID2;
      tab.dataset.mwitoolsCharacterTab = "true";
      tab.type = "button";
      const badge = tab.querySelector(
        ".TabsComponent_badge__1Du26,.MuiBadge-root"
      );
      if (badge) badge.textContent = t4("规划", "Planning");
      else tab.textContent = t4("规划", "Planning");
      for (const className of [...tab.classList]) {
        if (/(?:^|[_-])(?:active|selected)(?:[_-]|$)/i.test(className)) {
          tab.classList.remove(className);
        }
      }
      tab.dataset.active = "false";
      tab.setAttribute("aria-selected", "false");
      tab.setAttribute("tabindex", "-1");
      if (tab.hasAttribute("data-selected")) tab.dataset.selected = "false";
      if (tab.hasAttribute("data-state")) tab.dataset.state = "inactive";
      tab.addEventListener("click", (event) => {
        event.preventDefault();
        event.stopPropagation();
        setActive(!active);
      });
      anchor.insertAdjacentElement("afterend", tab);
      host = document.createElement("section");
      host.id = PANEL_ID2;
      host.hidden = true;
      shell2.appendChild(host);
    };
    const ensureMounted = () => {
      const loadout = findCharacterManagementLoadoutTab();
      const assetTab = document.getElementById(ASSET_TAB_ID);
      const anchor = assetTab?.parentElement === loadout?.parentElement ? assetTab : loadout;
      const found = anchor && findPanelShell(anchor);
      if (!anchor || !found) {
        if (tab || host) teardown();
        return;
      }
      const correctlyMounted = tab?.previousElementSibling === anchor && tab?.parentElement === anchor.parentElement && host?.parentElement === found.shell;
      if (tab?.isConnected && host?.isConnected && correctlyMounted) {
        const otherSelected = navigationBranch?.querySelector(
          `button[aria-selected="true"]:not(#${TAB_ID2}):not(#${ASSET_TAB_ID}),button.Mui-selected:not(#${TAB_ID2}):not(#${ASSET_TAB_ID})`
        );
        if (otherSelected && otherSelected !== lastActiveNativeTab || tab.getAttribute("aria-selected") !== "true") {
          if (active) setActive(false);
        } else if (active) {
          syncViewport();
        }
        return;
      }
      teardown();
      mount(anchor, found);
    };
    addStyles4();
    ensureMounted();
    const mountScheduler = createFrameScheduler(ensureMounted);
    subscribeMutationChannel(
      {
        name: "character-management-mount",
        target: document.body,
        options: {
          attributes: true,
          attributeFilter: ["aria-selected", "class", "data-active", "hidden"],
          childList: true,
          subtree: true
        },
        scope
      },
      (records) => {
        const relevant = records.some((record) => {
          const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
          if (target?.closest?.(`#${TAB_ID2},#${PANEL_ID2}`)) return false;
          if (record.type === "attributes") {
            return isRelevantPlanningMountAttribute(target, navigationBranch);
          }
          return [...record.addedNodes, ...record.removedNodes].some(
            isRelevantPlanningMountNode
          );
        });
        if (relevant) mountScheduler.schedule();
      }
    );
    const closeFromOtherTab = (event) => {
      if (active && !event.target.closest(`#${TAB_ID2}`) && !event.target.closest(`#${PANEL_ID2}`) && navigationBranch?.contains(event.target)) {
        setActive(false);
      }
    };
    scope.event(document, "pointerdown", closeFromOtherTab, true);
    scope.event(document, "click", closeFromOtherTab, true);
    scope.event(window, "resize", syncViewport);
    scope.event(document, "keydown", (event) => {
      if (active && isCompactViewport2() && event.key === "Escape") {
        setActive(false);
      }
    });
    for (const messageType of [
      "community_buffs_updated",
      "consumable_buffs_updated",
      "action_type_consumable_slots_updated",
      "equipment_buffs_updated",
      "personal_buffs_updated",
      "guild_buffs_updated"
    ]) {
      scope.add(runtime.onMessage(messageType, () => panel?.scheduleUpdate()));
    }
    scope.add(
      runtime.onMessage("house_rooms_updated", () => panel?.refreshHouses())
    );
    scope.add(
      runtime.onMessage("init_character_data", () => panel?.refreshCatalog())
    );
    scope.add(() => mountScheduler.cancel());
    return {
      update() {
        panel?.scheduleUpdate();
      },
      destroy() {
        teardown();
        document.getElementById(STYLE_ID4)?.remove();
      },
      getDiagnostics() {
        return {
          panelMounted: Boolean(panel),
          updateCount: panel?.updateCount ?? 0
        };
      }
    };
  }
  runtime.features.register({
    id: "planningPage",
    setting: "planningPage",
    scope: "character",
    initialize({ scope }) {
      const ui = createPlanningUi({ scope });
      return () => ui.destroy();
    }
  });

  // src/features/public-api.js
  var PUBLIC_API_VERSION = 1;
  var SCORE_SCHEMA_VERSION = 2;
  var SCORES_UPDATED_EVENT = "mwitools:scores-updated";
  var pageGlobal3 = globalThis.unsafeWindow ?? globalThis.window ?? globalThis;
  var latestScores = null;
  function finiteOrNull3(value) {
    if (value === null || value === void 0) return null;
    const number3 = Number(value);
    return Number.isFinite(number3) ? number3 : null;
  }
  function cloneForConsumer(value) {
    if (value === null || value === void 0) return value ?? null;
    const serialized = JSON.stringify(value);
    return pageGlobal3.JSON?.parse?.(serialized) ?? JSON.parse(serialized);
  }
  function createPublicScoreSnapshot(assetSnapshot) {
    const scores = assetSnapshot?.scores;
    if (!scores?.battle || !scores?.skilling) return null;
    return {
      schemaVersion: SCORE_SCHEMA_VERSION,
      unit: "million_coins",
      server: String(assetSnapshot.server ?? "production"),
      characterId: String(assetSnapshot.characterId ?? ""),
      calculatedAt: String(assetSnapshot.recordedAt ?? (/* @__PURE__ */ new Date()).toISOString()),
      battle: {
        total: finiteOrNull3(scores.battle.total),
        house: finiteOrNull3(scores.battle.house),
        abilities: finiteOrNull3(scores.battle.abilities),
        equipment: finiteOrNull3(scores.battle.equipment),
        shrine: finiteOrNull3(scores.battle.shrine)
      },
      skilling: {
        total: finiteOrNull3(scores.skilling.total),
        house: finiteOrNull3(scores.skilling.house),
        tools: finiteOrNull3(scores.skilling.tools),
        equipment: finiteOrNull3(scores.skilling.equipment),
        shrine: finiteOrNull3(scores.skilling.shrine),
        available: scores.skilling.available !== false
      }
    };
  }
  function dispatchScoresUpdated() {
    if (!latestScores || typeof pageGlobal3.dispatchEvent !== "function") return;
    const EventConstructor = pageGlobal3.CustomEvent ?? globalThis.CustomEvent;
    if (typeof EventConstructor !== "function") return;
    pageGlobal3.dispatchEvent(
      new EventConstructor(SCORES_UPDATED_EVENT, {
        detail: cloneForConsumer(latestScores)
      })
    );
  }
  function publishScoreSnapshot(assetSnapshot) {
    const next = createPublicScoreSnapshot(assetSnapshot);
    if (!next) return null;
    latestScores = next;
    dispatchScoresUpdated();
    return cloneForConsumer(latestScores);
  }
  function getScores() {
    return cloneForConsumer(latestScores);
  }
  async function refreshScores() {
    const snapshot = await runtime.api.refreshAssetSnapshot?.();
    if (!snapshot) return null;
    if (!latestScores || latestScores.calculatedAt !== String(snapshot.recordedAt ?? "")) {
      publishScoreSnapshot(snapshot);
    }
    return getScores();
  }
  var publicApi2 = {
    name: "MWITools",
    apiVersion: PUBLIC_API_VERSION,
    events: Object.freeze({ scoresUpdated: SCORES_UPDATED_EVENT }),
    get scores() {
      return getScores();
    },
    getScores,
    refreshScores
  };
  try {
    Object.defineProperty(pageGlobal3, "MWIToolsAPI", {
      configurable: true,
      enumerable: true,
      value: publicApi2
    });
  } catch {
    pageGlobal3.MWIToolsAPI = publicApi2;
  }
  runtime.api.onAssetSnapshot?.(publishScoreSnapshot);
  var existingSnapshot = runtime.api.getLatestAssetSnapshot?.();
  if (existingSnapshot) publishScoreSnapshot(existingSnapshot);
  Object.assign(runtime.api, {
    createPublicScoreSnapshot,
    getPublishedScores: getScores,
    publishScoreSnapshot
  });

  // src/features/leaderboard-overlay.js
  var OVERLAY_VERSION = "1.3.0";
  var LEADERBOARD_API_URL = "https://mwi-guild.43.167.210.211.sslip.io/api/v1/leaderboards";
  var LEADERBOARD_CACHE_KEY = "MWITools_leaderboard_overlay_cache_v2";
  var LEADERBOARD_REFRESH_INTERVAL = 15 * 60 * 1e3;
  var STYLE_ID5 = "mwi-leaderboard-overlay-style";
  var BADGE_CONTAINER_ATTRIBUTE = "data-mwi-leaderboard-badges";
  var RATE_HEADER_ATTRIBUTE = "data-mwi-leaderboard-rate-header";
  var RATE_CELL_ATTRIBUTE = "data-mwi-leaderboard-rate-cell";
  var LEADERBOARD_TABLE_SELECTOR = 'table[class*="LeaderboardPanel_leaderboardTable"]';
  var DEFAULT_CATEGORIES = [
    ["total_level", { zh: "总等级", en: "Total Level" }],
    ["milking", { zh: "挤奶", en: "Milking" }],
    ["foraging", { zh: "采摘", en: "Foraging" }],
    ["woodcutting", { zh: "伐木", en: "Woodcutting" }],
    ["cheesesmithing", { zh: "奶酪锻造", en: "Cheesesmithing" }],
    ["crafting", { zh: "制作", en: "Crafting" }],
    ["tailoring", { zh: "缝纫", en: "Tailoring" }],
    ["cooking", { zh: "烹饪", en: "Cooking" }],
    ["brewing", { zh: "冲泡", en: "Brewing" }],
    ["alchemy", { zh: "炼金", en: "Alchemy" }],
    ["enhancing", { zh: "强化", en: "Enhancing" }],
    ["stamina", { zh: "耐力", en: "Stamina" }],
    ["intelligence", { zh: "智力", en: "Intelligence" }],
    ["attack", { zh: "攻击", en: "Attack" }],
    ["defense", { zh: "防御", en: "Defense" }],
    ["melee", { zh: "近战", en: "Melee" }],
    ["ranged", { zh: "远程", en: "Ranged" }],
    ["magic", { zh: "魔法", en: "Magic" }],
    ["task_points", { zh: "任务积分", en: "Task Points" }],
    ["labyrinth_depth", { zh: "迷宫深度", en: "Labyrinth Depth" }],
    ["fame_points", { zh: "名望", en: "Fame" }]
  ];
  var RATE_CATEGORIES = /* @__PURE__ */ new Set([
    "milking",
    "foraging",
    "woodcutting",
    "cheesesmithing",
    "crafting",
    "tailoring",
    "cooking",
    "brewing",
    "alchemy",
    "enhancing",
    "stamina",
    "intelligence",
    "attack",
    "defense",
    "melee",
    "ranged",
    "magic"
  ]);
  var MISC_CATEGORY_SYMBOLS = Object.freeze({
    total_level: "leaderboard",
    task_points: "tasks",
    labyrinth_depth: "labyrinth",
    fame_points: "experience"
  });
  var activeInstances = 0;
  var featureEnabled = false;
  var controllers = /* @__PURE__ */ new Set();
  function t5(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function categoryLabel(value, fallback) {
    if (value && typeof value === "object") {
      return value[runtime.config.isZH ? "zh" : "en"] ?? fallback;
    }
    return String(value || fallback);
  }
  function normalizedName2(value) {
    return String(value || "").normalize("NFKC").trim().toLocaleLowerCase();
  }
  function badgeTier(rank) {
    const value = Number(rank);
    if (!Number.isInteger(value) || value < 1 || value > 100) return null;
    if (value <= 20) return "rainbow";
    if (value <= 50) return "gold";
    if (value <= 80) return "silver";
    return "bronze";
  }
  function roundedCompact(value, divisor, suffix) {
    const rounded = Math.round(value / divisor * 10) / 10;
    return `${Number.isInteger(rounded) ? rounded.toFixed(0) : rounded.toFixed(1)}${suffix}`;
  }
  function validExperienceRate(value) {
    if (value == null || value === "") return null;
    const number3 = Number(value);
    return Number.isFinite(number3) && number3 >= 0 ? number3 : null;
  }
  function formatExperienceRate(value) {
    const number3 = validExperienceRate(value);
    if (number3 == null) return "—";
    if (number3 >= 1e6) return roundedCompact(number3, 1e6, "M");
    return roundedCompact(number3, 1e3, "K");
  }
  function compareRateRows(left, right, mode) {
    const leftRate = validExperienceRate(left?.xpPerHour);
    const rightRate = validExperienceRate(right?.xpPerHour);
    const leftValid = leftRate != null;
    const rightValid = rightRate != null;
    const leftRank = Number(left?.rank) || Number.MAX_SAFE_INTEGER;
    const rightRank = Number(right?.rank) || Number.MAX_SAFE_INTEGER;
    if (mode === "official") return leftRank - rightRank;
    if (leftValid !== rightValid) return leftValid ? -1 : 1;
    if (!leftValid) return leftRank - rightRank;
    const rateDifference = mode === "ascending" ? leftRate - rightRate : rightRate - leftRate;
    return rateDifference || leftRank - rightRank;
  }
  function ensureStyles(documentRef) {
    if (documentRef.getElementById(STYLE_ID5)) return;
    const mount = documentRef.head || documentRef.documentElement;
    if (!mount) {
      documentRef.addEventListener(
        "readystatechange",
        () => ensureStyles(documentRef),
        { once: true }
      );
      return;
    }
    const style = documentRef.createElement("style");
    style.id = STYLE_ID5;
    style.textContent = `
    [${BADGE_CONTAINER_ATTRIBUTE}]{display:inline-flex;align-items:center;flex-wrap:wrap;gap:2px;margin-inline-start:4px;vertical-align:middle}
    [${BADGE_CONTAINER_ATTRIBUTE}][data-mwi-leaderboard-placement="profile"]{display:flex;flex-basis:100%;width:100%;margin-block-start:4px;margin-inline-start:0}
    [${BADGE_CONTAINER_ATTRIBUTE}][data-mwi-leaderboard-placement="list"]{display:flex;width:100%;justify-content:center;margin-block-start:2px;margin-inline-start:0}
    .mwi-lb-badge{box-sizing:border-box;display:inline-flex;align-items:center;gap:1px;height:15px;min-height:15px;padding:0 3px 0 1px;border:1px solid;border-radius:999px;background:rgba(12,16,28,.78);color:#eef2ff;font:600 9px/1 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;white-space:nowrap;box-shadow:0 1px 2px rgba(0,0,0,.24);vertical-align:middle}
    .mwi-lb-badge-icon{display:block;flex:none;width:11px;height:11px;object-fit:contain}
    .mwi-lb-badge--rainbow{border-color:transparent;color:#f8fbff;background:linear-gradient(rgba(12,16,28,.9),rgba(12,16,28,.9)) padding-box,linear-gradient(105deg,#ff5f6d,#ffd166,#67e8a5,#5cb8ff,#c77dff,#ff6ec7) border-box;box-shadow:0 0 7px rgba(121,190,255,.48),0 0 3px rgba(255,103,199,.34),inset 0 0 3px rgba(255,255,255,.14)}
    .mwi-lb-badge--top-five{position:relative;overflow:hidden;isolation:isolate}
    .mwi-lb-badge--top-five::before{content:"";position:absolute;z-index:2;inset:-35% auto -35% -70%;width:42%;pointer-events:none;background:linear-gradient(105deg,transparent 0%,rgba(255,255,255,.04) 24%,rgba(255,255,255,.92) 50%,rgba(255,255,255,.08) 76%,transparent 100%);filter:blur(.35px);transform:skewX(-18deg);opacity:0;animation:mwi-lb-badge-light-sweep 5s ease-in-out infinite}
    .mwi-lb-badge--top-five::after{content:"";position:absolute;z-index:3;top:-1px;right:-1px;width:8px;height:8px;border-radius:50%;pointer-events:none;background:radial-gradient(circle at 70% 25%,rgba(255,255,255,1) 0%,rgba(255,255,255,.88) 12%,rgba(174,225,255,.42) 36%,transparent 72%);filter:blur(.25px);opacity:0;animation:mwi-lb-badge-corner-glint 5s ease-in-out infinite}
    @keyframes mwi-lb-badge-light-sweep{0%{left:-70%;opacity:0}3%{opacity:.28}18%{left:128%;opacity:.96}20%,100%{left:128%;opacity:0}}
    @keyframes mwi-lb-badge-corner-glint{0%,20%,40%,100%{opacity:0;transform:scale(.45)}30%{opacity:1;transform:scale(1.15)}}
    @media (prefers-reduced-motion:reduce){.mwi-lb-badge--top-five::before,.mwi-lb-badge--top-five::after{animation:none;opacity:0}}
    .mwi-lb-badge--gold{border-color:#d9aa38;color:#ffe8a3;box-shadow:0 0 5px rgba(217,170,56,.24)}
    .mwi-lb-badge--silver{border-color:#d8dee9;color:#f8fafc;box-shadow:0 0 4px rgba(226,232,240,.24)}
    .mwi-lb-badge--bronze{border-color:#b87333;color:#f2c49b;box-shadow:0 0 4px rgba(184,115,51,.24)}
    [${RATE_HEADER_ATTRIBUTE}]{white-space:nowrap}
    [${RATE_CELL_ATTRIBUTE}]{font-variant-numeric:tabular-nums;white-space:nowrap}
  `;
    mount.append(style);
  }
  function nativeSpriteHref(documentRef, kind, symbol) {
    for (const use of documentRef.querySelectorAll("use")) {
      registerGameSpriteSource(
        use.getAttribute("href") ?? use.getAttribute("xlink:href")
      );
    }
    return getGameSpriteHref(kind, symbol);
  }
  function createBadgeIcon(documentRef, category, customIconBaseUrl = "") {
    const miscSymbol = MISC_CATEGORY_SYMBOLS[category];
    if (customIconBaseUrl && !miscSymbol) {
      const icon2 = documentRef.createElement("img");
      icon2.className = "mwi-lb-badge-icon";
      icon2.src = `${customIconBaseUrl}/${encodeURIComponent(category)}.png`;
      icon2.alt = "";
      icon2.setAttribute("aria-hidden", "true");
      return icon2;
    }
    const spriteKind = miscSymbol ? "misc" : "skills";
    const symbol = miscSymbol || category;
    const icon = documentRef.createElementNS("http://www.w3.org/2000/svg", "svg");
    icon.classList.add("mwi-lb-badge-icon");
    icon.setAttribute("viewBox", "0 0 40 40");
    icon.setAttribute("aria-hidden", "true");
    const use = documentRef.createElementNS("http://www.w3.org/2000/svg", "use");
    const href = nativeSpriteHref(documentRef, spriteKind, symbol);
    if (href) {
      use.setAttribute("href", href);
      icon.append(use);
    }
    return icon;
  }
  function createOverlay(options = {}) {
    const documentRef = options.document ?? (typeof document !== "undefined" ? document : null);
    if (!documentRef) {
      throw new Error(
        localize(
          "排行榜浮层需要可用的页面文档。",
          "MWILeaderboardOverlay requires a document."
        )
      );
    }
    const categoryEntries = Array.isArray(options.categories) && options.categories.length ? options.categories : DEFAULT_CATEGORIES;
    const categoryOrder = categoryEntries.map(([category]) => category);
    const categoryLabels = Object.fromEntries(categoryEntries);
    const iconBaseUrl = String(options.iconBaseUrl || "").replace(/\/+$/, "");
    const state = {
      categories: {},
      nameIndex: /* @__PURE__ */ new Map(),
      currentLeaderboard: null,
      refreshPending: false,
      destroyed: false,
      showBadges: options.showBadges !== false,
      showRates: options.showRates !== false,
      showEffects: options.showEffects === true
    };
    ensureStyles(documentRef);
    function rebuildNameIndex() {
      const index = /* @__PURE__ */ new Map();
      for (const category of categoryOrder) {
        const snapshot = state.categories?.[category];
        for (const row of Array.isArray(snapshot?.rows) ? snapshot.rows : []) {
          const name = normalizedName2(row.characterName || row.name);
          const rank = Number(row.rank);
          const tier = badgeTier(rank);
          if (!name || !tier) continue;
          if (!index.has(name)) index.set(name, []);
          index.get(name).push({
            category,
            label: categoryLabels[category],
            rank,
            tier
          });
        }
      }
      const categoryIndex = new Map(
        categoryOrder.map((category, position) => [category, position])
      );
      for (const badges of index.values()) {
        badges.sort(
          (left, right) => left.rank - right.rank || (categoryIndex.get(left.category) ?? Number.MAX_SAFE_INTEGER) - (categoryIndex.get(right.category) ?? Number.MAX_SAFE_INTEGER)
        );
      }
      state.nameIndex = index;
    }
    function badgeSignature(badges) {
      return badges.map((item) => `${item.category}:${item.rank}`).join("|");
    }
    function renderNameBadges() {
      if (!state.showBadges) return;
      const nameElements = documentRef.querySelectorAll(
        '[class*="CharacterName_name"][data-name]'
      );
      for (const nameElement of nameElements) {
        const host = nameElement.parentElement;
        if (!host) continue;
        if (nameElement.closest('[class*="Header_characterInfo"]')) {
          host.closest('[class*="Header_name"]')?.querySelector(`[${BADGE_CONTAINER_ATTRIBUTE}]`)?.remove();
          host.querySelector(`[${BADGE_CONTAINER_ATTRIBUTE}]`)?.remove();
          continue;
        }
        const profileRoot = nameElement.closest(
          '[class*="CharacterProfile_"],[class*="PlayerProfile_"],[class*="ProfilePage_"],[class*="ProfilePanel_"],[data-mwi-leaderboard-profile]'
        );
        const profileNameBlock = profileRoot ? nameElement.closest('[class*="Header_name"]') : null;
        const guildNameBlock = nameElement.closest(
          '[class*="GuildPanel_characterName"]'
        );
        const friendNameBlock = nameElement.closest(
          '[class*="SocialPanel_characterName"]'
        );
        const settingsNameColor = nameElement.closest(
          '[class*="SettingsPanel_nameColor"]'
        );
        const profileFallbackMount = profileRoot ? host.parentElement : null;
        const badgeMount = profileNameBlock || profileFallbackMount || guildNameBlock || host;
        let container = badgeMount.querySelector(`:scope > [${BADGE_CONTAINER_ATTRIBUTE}]`) || (badgeMount === host ? null : host.querySelector(`:scope > [${BADGE_CONTAINER_ATTRIBUTE}]`)) || friendNameBlock?.querySelector(`[${BADGE_CONTAINER_ATTRIBUTE}]`);
        if (nameElement.closest('[class*="LeaderboardPanel_"]')) {
          container?.remove();
          continue;
        }
        const badges = state.nameIndex.get(
          normalizedName2(nameElement.getAttribute("data-name"))
        ) || [];
        const profilePlacement = Boolean(profileRoot);
        const visibleBadges = profilePlacement ? badges : badges.slice(0, 3);
        if (!visibleBadges.length) {
          container?.remove();
          continue;
        }
        const listPlacement = Boolean(guildNameBlock);
        const friendPlacement = Boolean(friendNameBlock);
        const placement = profilePlacement ? "profile" : listPlacement ? "list" : friendPlacement ? "friend" : settingsNameColor ? "settings" : "inline";
        const signature = `${badgeSignature(visibleBadges)}|effects:${state.showEffects}`;
        const previousPlacement = container?.dataset.mwiLeaderboardPlacement || "";
        if (!container) {
          container = documentRef.createElement("span");
          container.setAttribute(BADGE_CONTAINER_ATTRIBUTE, "");
        }
        if (container.dataset.mwiLeaderboardPlacement !== placement) {
          container.dataset.mwiLeaderboardPlacement = placement;
        }
        if (profilePlacement) {
          const profileName = profileNameBlock ? nameElement.closest('[class*="CharacterName_characterName"]') || nameElement : host;
          if (container.parentElement !== badgeMount || container.previousElementSibling !== profileName) {
            profileName.insertAdjacentElement("afterend", container);
          }
        } else if (listPlacement) {
          if (container.parentElement !== badgeMount)
            badgeMount.append(container);
        } else if (friendPlacement) {
          if (container.parentElement !== host) host.append(container);
        } else if (!container.isConnected || previousPlacement === "profile") {
          host.append(container);
        }
        if (container.dataset.mwiLeaderboardSignature === signature) continue;
        container.dataset.mwiLeaderboardSignature = signature;
        container.replaceChildren(
          ...visibleBadges.map((item) => {
            const badge = documentRef.createElement("span");
            badge.className = `mwi-lb-badge mwi-lb-badge--${item.tier}${state.showEffects && item.rank <= 5 ? " mwi-lb-badge--top-five" : ""}`;
            const icon = createBadgeIcon(documentRef, item.category, iconBaseUrl);
            badge.append(icon, documentRef.createTextNode(String(item.rank)));
            const label = categoryLabel(item.label, item.category);
            badge.title = runtime.config.isZH ? `${label}排行榜第 ${item.rank} 名` : `${label} leaderboard rank ${item.rank}`;
            return badge;
          })
        );
      }
    }
    function currentRowsByName() {
      return new Map(
        (state.currentLeaderboard?.rows || []).map((row) => [
          normalizedName2(row.characterName || row.name),
          row
        ])
      );
    }
    function renderLeaderboardRateColumn() {
      if (!state.showRates) return;
      const current = state.currentLeaderboard;
      if (!current) return;
      const table = documentRef.querySelector(LEADERBOARD_TABLE_SELECTOR);
      if (!table) return;
      const headingRow = table.tHead?.rows?.[0];
      const tbody = table.tBodies?.[0];
      if (!headingRow || !tbody) return;
      let header = headingRow.querySelector(`[${RATE_HEADER_ATTRIBUTE}]`);
      if (!header) {
        header = documentRef.createElement("th");
        header.setAttribute(RATE_HEADER_ATTRIBUTE, "");
        headingRow.append(header);
      }
      const headerCopy = t5("经验/小时", "XP/hour");
      if (header.textContent !== headerCopy) header.textContent = headerCopy;
      const rowsByName = currentRowsByName();
      for (const rowElement of tbody.rows) {
        const name = rowElement.querySelector('[class*="CharacterName_name"][data-name]')?.getAttribute("data-name");
        const model = rowsByName.get(normalizedName2(name));
        let cell = rowElement.querySelector(`[${RATE_CELL_ATTRIBUTE}]`);
        if (!cell) {
          cell = documentRef.createElement("td");
          cell.setAttribute(RATE_CELL_ATTRIBUTE, "");
          rowElement.append(cell);
        }
        const copy = formatExperienceRate(model?.xpPerHour);
        const rate = validExperienceRate(model?.xpPerHour);
        const title = rate != null ? `${runtime.api.formatExactNumber?.(Math.round(rate), 0) ?? Math.round(rate)} ${t5("经验/小时", "XP/hour")}` : t5("缺少可比较的历史快照", "No comparable historical snapshot");
        if (cell.textContent !== copy) cell.textContent = copy;
        if (cell.title !== title) cell.title = title;
      }
    }
    function refresh() {
      if (state.destroyed) return;
      if (state.showBadges) renderNameBadges();
      if (state.showRates) renderLeaderboardRateColumn();
    }
    function removeBadges() {
      documentRef.querySelectorAll(`[${BADGE_CONTAINER_ATTRIBUTE}]`).forEach((element) => element.remove());
    }
    function removeRateColumn() {
      documentRef.querySelectorAll(`[${RATE_HEADER_ATTRIBUTE}],[${RATE_CELL_ATTRIBUTE}]`).forEach((element) => element.remove());
    }
    function scheduleRefresh() {
      if (state.destroyed || state.refreshPending) return;
      state.refreshPending = true;
      const schedule = documentRef.defaultView?.requestAnimationFrame || ((callback) => setTimeout(callback, 25));
      schedule(() => {
        state.refreshPending = false;
        refresh();
      });
    }
    const Observer = documentRef.defaultView?.MutationObserver || (typeof MutationObserver !== "undefined" ? MutationObserver : null);
    if (!Observer) {
      throw new Error(
        localize(
          "排行榜浮层需要 MutationObserver 支持。",
          "MWILeaderboardOverlay requires MutationObserver."
        )
      );
    }
    const observer = new Observer(() => scheduleRefresh());
    const observe = () => {
      if (state.destroyed || !documentRef.documentElement) return;
      observer.observe(documentRef.documentElement, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ["data-name"]
      });
      scheduleRefresh();
    };
    if (documentRef.documentElement) observe();
    else
      documentRef.addEventListener("readystatechange", observe, { once: true });
    activeInstances += 1;
    return {
      setRankings(categories) {
        state.categories = categories && typeof categories === "object" ? categories : {};
        rebuildNameIndex();
        scheduleRefresh();
      },
      enhanceLeaderboard({ category, rows: rows2 }) {
        if (!categoryOrder.includes(category) || !RATE_CATEGORIES.has(category)) {
          removeRateColumn();
          state.currentLeaderboard = null;
          return false;
        }
        state.currentLeaderboard = {
          category,
          rows: Array.isArray(rows2) ? rows2 : []
        };
        scheduleRefresh();
        return true;
      },
      clearLeaderboard() {
        if (!state.currentLeaderboard) return;
        removeRateColumn();
        state.currentLeaderboard = null;
      },
      setDisplay({
        badges = state.showBadges,
        rates = state.showRates,
        effects = state.showEffects
      } = {}) {
        const nextBadges = Boolean(badges);
        const nextRates = Boolean(rates);
        const nextEffects = Boolean(effects);
        if (state.showBadges && !nextBadges) removeBadges();
        if (state.showRates && !nextRates) removeRateColumn();
        state.showBadges = nextBadges;
        state.showRates = nextRates;
        state.showEffects = nextEffects;
        scheduleRefresh();
      },
      destroy() {
        if (state.destroyed) return;
        state.destroyed = true;
        observer.disconnect();
        removeBadges();
        removeRateColumn();
        activeInstances = Math.max(0, activeInstances - 1);
        if (activeInstances === 0) documentRef.getElementById(STYLE_ID5)?.remove();
      }
    };
  }
  function create(options = {}) {
    let instance = null;
    let destroyed = false;
    let rankings = null;
    let leaderboard = null;
    let display = {
      badges: options.showBadges !== false,
      rates: options.showRates !== false,
      effects: options.showEffects === true
    };
    const allowedCategories = new Set(
      (Array.isArray(options.categories) && options.categories.length ? options.categories : DEFAULT_CATEGORIES).map(([category]) => category)
    );
    const mount = () => {
      if (destroyed || instance || !featureEnabled) return;
      instance = createOverlay({
        ...options,
        showBadges: display.badges,
        showRates: display.rates,
        showEffects: display.effects
      });
      if (rankings) instance.setRankings(rankings);
      if (leaderboard) instance.enhanceLeaderboard(leaderboard);
    };
    const unmount = () => {
      instance?.destroy();
      instance = null;
    };
    const controller = {
      setRankings(categories) {
        rankings = categories && typeof categories === "object" ? categories : {};
        instance?.setRankings(rankings);
      },
      enhanceLeaderboard(payload = {}) {
        if (!allowedCategories.has(payload.category) || !RATE_CATEGORIES.has(payload.category)) {
          leaderboard = null;
          instance?.clearLeaderboard();
          return false;
        }
        leaderboard = {
          category: payload.category,
          rows: Array.isArray(payload.rows) ? payload.rows : []
        };
        instance?.enhanceLeaderboard(leaderboard);
        return true;
      },
      clearLeaderboard() {
        leaderboard = null;
        instance?.clearLeaderboard();
      },
      setDisplay(next = {}) {
        display = {
          badges: next.badges ?? display.badges,
          rates: next.rates ?? display.rates,
          effects: next.effects ?? display.effects
        };
        instance?.setDisplay(display);
      },
      destroy() {
        if (destroyed) return;
        destroyed = true;
        unmount();
        controllers.delete(controller);
      }
    };
    Object.defineProperties(controller, {
      enabled: {
        enumerable: true,
        get() {
          return featureEnabled && !destroyed;
        }
      },
      _mount: { value: mount },
      _unmount: { value: unmount }
    });
    controllers.add(controller);
    mount();
    return controller;
  }
  var leaderboardOverlayApi = {
    VERSION: OVERLAY_VERSION,
    create,
    formatExperienceRate,
    badgeTier,
    compareRateRows,
    get enabled() {
      return featureEnabled;
    }
  };
  function normalizeLeaderboardPayload(payload) {
    if (payload?.type !== "leaderboard_updated" || payload?.leaderboardType !== "standard") {
      return null;
    }
    const leaderboard = payload.leaderboard;
    const category = String(
      payload.leaderboardCategory ?? leaderboard?.category ?? ""
    );
    if (leaderboard?.type !== "standard" || leaderboard?.category !== category || !DEFAULT_CATEGORIES.some(([key]) => key === category) || !Array.isArray(leaderboard?.rows)) {
      return null;
    }
    const rows2 = leaderboard.rows.map((row) => {
      const scoreOnly = category === "fame_points" || category === "task_points";
      return {
        characterId: Number(row?.characterId ?? row?.id),
        characterName: String(row?.characterName ?? row?.name ?? "").trim(),
        rank: Number(row?.rank),
        level: scoreOnly ? 0 : Number(row?.level ?? row?.value1),
        experience: Number(
          row?.experience ?? (scoreOnly ? row?.value1 : row?.value2)
        ),
        xpPerHour: validExperienceRate(row?.xpPerHour)
      };
    }).filter(
      (row) => row.characterName && Number.isInteger(row.rank) && row.rank >= 1 && row.rank <= 100
    );
    return rows2.length ? { category, rows: rows2 } : null;
  }
  function normalizeCategories(value) {
    const allowed = new Set(DEFAULT_CATEGORIES.map(([category]) => category));
    return Object.fromEntries(
      Object.entries(value ?? {}).flatMap(([category, snapshot]) => {
        if (!allowed.has(category) || !Array.isArray(snapshot?.rows)) return [];
        const rows2 = snapshot.rows.filter(
          (row) => String(row?.characterName ?? row?.name ?? "").trim() && Number.isInteger(Number(row?.rank)) && Number(row.rank) >= 1 && Number(row.rank) <= 100
        );
        return rows2.length ? [[category, { ...snapshot, rows: rows2 }]] : [];
      })
    );
  }
  function loadCachedCategories() {
    try {
      const cached = JSON.parse(
        globalThis.localStorage?.getItem(LEADERBOARD_CACHE_KEY) || "null"
      );
      return cached?.schemaVersion === 1 ? normalizeCategories(cached.categories) : {};
    } catch {
      return {};
    }
  }
  function saveCachedCategories(categories) {
    try {
      globalThis.localStorage?.setItem(
        LEADERBOARD_CACHE_KEY,
        JSON.stringify({ schemaVersion: 1, cachedAt: Date.now(), categories })
      );
    } catch (error) {
      console.warn(
        runtime.config.isZH ? "[MWITools] 无法缓存排行榜名次" : "[MWITools] Unable to cache leaderboard rankings",
        error
      );
    }
  }
  function requestLeaderboardCategories(onRequest) {
    const requestFn = typeof GM !== "undefined" && typeof GM.xmlHttpRequest === "function" ? GM.xmlHttpRequest : typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : null;
    if (!requestFn) return Promise.resolve(null);
    return new Promise((resolve) => {
      let settled = false;
      let watchdog;
      const finish = (response) => {
        if (settled) return;
        settled = true;
        clearTimeout(watchdog);
        if (Number(response?.status) < 200 || Number(response?.status) >= 300) {
          resolve(null);
          return;
        }
        try {
          const raw = response.responseText || response.response;
          const payload = typeof raw === "string" ? JSON.parse(raw) : raw;
          resolve(
            payload?.schemaVersion === 1 && payload?.leaderboardType === "standard" ? normalizeCategories(payload.categories) : null
          );
        } catch {
          resolve(null);
        }
      };
      watchdog = setTimeout(() => finish(null), 10500);
      try {
        const request2 = requestFn({
          method: "GET",
          url: LEADERBOARD_API_URL,
          timeout: 1e4,
          onload: finish,
          onabort: () => finish(null),
          onerror: () => finish(null),
          ontimeout: () => finish(null)
        });
        onRequest?.(request2);
        if (request2?.then) request2.then(finish).catch(() => finish(null));
      } catch {
        finish(null);
      }
    });
  }
  var pageGlobal4 = globalThis.unsafeWindow ?? globalThis.window ?? globalThis;
  try {
    Object.defineProperty(pageGlobal4, "MWILeaderboardOverlay", {
      configurable: true,
      enumerable: true,
      value: leaderboardOverlayApi
    });
  } catch {
    pageGlobal4.MWILeaderboardOverlay = leaderboardOverlayApi;
  }
  var integratedModes = /* @__PURE__ */ new Set();
  var integratedService = null;
  function integratedDisplay() {
    return {
      badges: integratedModes.has("badges"),
      rates: integratedModes.has("rates"),
      effects: integratedModes.has("effects")
    };
  }
  function startIntegratedService() {
    const initialDisplay = integratedDisplay();
    const controller = create({
      showBadges: initialDisplay.badges,
      showRates: initialDisplay.rates
    });
    let categories = loadCachedCategories();
    let currentLeaderboard = null;
    let active = true;
    let activeRequest = null;
    controller.setRankings(categories);
    const applyCurrentLeaderboard = () => {
      if (!currentLeaderboard) return;
      controller.enhanceLeaderboard({
        category: currentLeaderboard.category,
        rows: categories[currentLeaderboard.category]?.rows ?? currentLeaderboard.rows
      });
    };
    const refreshRankings = async () => {
      const response = await requestLeaderboardCategories((request2) => {
        activeRequest = request2;
      });
      activeRequest = null;
      if (!active || !response) return;
      categories = response;
      saveCachedCategories(categories);
      controller.setRankings(categories);
      applyCurrentLeaderboard();
    };
    const stopMessages = runtime.onMessage("leaderboard_updated", (payload) => {
      const normalized = normalizeLeaderboardPayload(payload);
      if (!normalized) {
        if (payload?.type === "leaderboard_updated" && payload?.leaderboardType === "standard") {
          currentLeaderboard = null;
          controller.clearLeaderboard();
        }
        return;
      }
      currentLeaderboard = normalized;
      if (!RATE_CATEGORIES.has(normalized.category)) {
        currentLeaderboard = null;
        controller.clearLeaderboard();
        return;
      }
      if (!categories[normalized.category]) {
        categories = {
          ...categories,
          [normalized.category]: {
            receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
            rows: normalized.rows
          }
        };
        controller.setRankings(categories);
      }
      applyCurrentLeaderboard();
    });
    const interval = setInterval(
      () => void refreshRankings(),
      LEADERBOARD_REFRESH_INTERVAL
    );
    void refreshRankings();
    return {
      controller,
      stop() {
        active = false;
        clearInterval(interval);
        stopMessages();
        activeRequest?.abort?.();
        controller.destroy();
      }
    };
  }
  function activateIntegratedMode(mode) {
    integratedModes.add(mode);
    if (!featureEnabled) {
      featureEnabled = true;
      for (const controller of controllers) controller._mount();
    }
    if (!integratedService) integratedService = startIntegratedService();
    else integratedService.controller.setDisplay(integratedDisplay());
    return () => {
      integratedModes.delete(mode);
      if (integratedModes.size) {
        integratedService?.controller.setDisplay(integratedDisplay());
        return;
      }
      integratedService?.stop();
      integratedService = null;
      featureEnabled = false;
      for (const controller of controllers) controller._unmount();
    };
  }
  runtime.features.register({
    id: "leaderboardOverlay",
    setting: "leaderboardOverlay",
    initialize() {
      return activateIntegratedMode("badges");
    }
  });
  runtime.features.register({
    id: "leaderboardXpRate",
    setting: "leaderboardXpRate",
    initialize() {
      return activateIntegratedMode("rates");
    }
  });
  runtime.features.register({
    id: "leaderboardBadgeGlint",
    setting: "leaderboardBadgeGlint",
    initialize() {
      return activateIntegratedMode("effects");
    }
  });

  // src/features/battle-buffs.js
  var STYLE_ID6 = "mwi-buff-style";
  var EXPANSION_STORAGE_KEY = "MWITools_battle_buff_expansion_v1";
  var COLLAPSED_CAPACITY = 3;
  var EXPANDED_CAPACITY = 6;
  var abilityEffectIndexSource = null;
  var abilityEffectIndex = null;
  function buildAbilityEffectIndex() {
    const source = runtime.state.initData_abilityDetailMap ?? {};
    if (source === abilityEffectIndexSource && abilityEffectIndex) {
      return abilityEffectIndex;
    }
    const index = {
      buffs: /* @__PURE__ */ new Map(),
      debuffs: /* @__PURE__ */ new Map(),
      teamBuffs: /* @__PURE__ */ new Set(),
      singleTargetDebuffs: /* @__PURE__ */ new Set()
    };
    const entries = source instanceof Map ? source.entries() : Object.entries(source);
    for (const [abilityHrid, detail] of entries) {
      for (const effect of detail?.abilityEffects ?? []) {
        const durations = (effect?.buffs ?? []).map((buff) => Number(buff?.duration) / 1e9).filter((duration2) => Number.isFinite(duration2) && duration2 > 0);
        if (!durations.length) continue;
        const duration = Math.max(...durations);
        const targetType = String(effect?.targetType ?? "").toLowerCase().replaceAll(/[^a-z]/g, "");
        const targetsEnemy = targetType.includes("enemy");
        const durationsByAbility = targetsEnemy ? index.debuffs : index.buffs;
        durationsByAbility.set(
          abilityHrid,
          Math.max(duration, durationsByAbility.get(abilityHrid) ?? 0)
        );
        if (!targetsEnemy && targetType.includes("allallies")) {
          index.teamBuffs.add(abilityHrid);
        }
        if (targetsEnemy && !targetType.includes("allenemies")) {
          index.singleTargetDebuffs.add(abilityHrid);
        }
      }
    }
    abilityEffectIndexSource = source;
    abilityEffectIndex = index;
    return index;
  }
  function dynamicCollection(key) {
    return Object.freeze({
      has(value) {
        return buildAbilityEffectIndex()[key].has(value);
      },
      get(value) {
        return buildAbilityEffectIndex()[key].get(value);
      },
      [Symbol.iterator]() {
        return buildAbilityEffectIndex()[key][Symbol.iterator]();
      }
    });
  }
  var BUFFS = dynamicCollection("buffs");
  var DEBUFFS = dynamicCollection("debuffs");
  var TEAM_BUFFS = dynamicCollection("teamBuffs");
  var SINGLE_TARGET_DEBUFFS = dynamicCollection("singleTargetDebuffs");
  function ensureBuffStyles(scope) {
    if (document.getElementById(STYLE_ID6)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID6;
    style.textContent = `
.mwi-has-buffbar{height:auto!important;min-height:0;overflow:visible!important}
.mwi-buff-shell{width:100%;box-sizing:border-box;display:grid;grid-template-rows:21px 18px;margin-top:4px}
.mwi-buff-shell[data-expanded="true"]{grid-template-rows:46px 18px}
.mwi-buffbar{width:100%;height:21px;max-height:21px;box-sizing:border-box;display:flex;flex-wrap:wrap;gap:4px;overflow:hidden;align-content:flex-start;align-items:center;justify-content:center}
.mwi-buff-shell[data-expanded="true"] .mwi-buffbar{height:46px;max-height:46px;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin}
.mwi-buff-toggle{width:100%;height:18px;box-sizing:border-box;padding:0;border:0;background:transparent;color:inherit;font:700 10px/18px "Trebuchet MS",Verdana,Arial,sans-serif;cursor:pointer;opacity:.7;text-align:center}
.mwi-buff-toggle:hover,.mwi-buff-toggle:focus-visible{opacity:1;background:rgba(127,127,127,.12);outline:none}
.mwi-chip{font:11px/1.2 "Trebuchet MS", Verdana, Arial, sans-serif;padding:2px 6px;border-radius:10px;white-space:nowrap;display:inline-flex;align-items:center;gap:4px;position:relative}
.mwi-icon-wrap{position:relative;width:15px;height:15px;display:inline-block}
.mwi-icon{width:15px;height:15px;display:block}
.mwi-progress-ring{position:absolute;inset:-3px;border-radius:14px;pointer-events:none;mask:linear-gradient(#000 0 0);-webkit-mask:linear-gradient(#000 0 0)}
.mwi-progress-ring::before{content:"";position:absolute;inset:0;border-radius:inherit;padding:3px;background:conic-gradient(var(--mwi-ring-color) 0deg var(--mwi-ring-deg), transparent var(--mwi-ring-deg) 360deg);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude}
.mwi-buff{background:#e7f4e4;color:#1e4d1a;border:1px solid #7fbf7a}
.mwi-debuff{background:#fbe3e3;color:#6b1a1a;border:1px solid #d17b7b}
`;
    (document.head || document.documentElement).appendChild(style);
    scope.add(() => style.remove());
  }
  function createBuffTracker(scope) {
    const UNIT_STATE = /* @__PURE__ */ new WeakMap();
    const BATTLE_STATE = { players: /* @__PURE__ */ new Map(), monsters: /* @__PURE__ */ new Map() };
    const PENDING_BUFFS = [];
    const PENDING_DEBUFFS = [];
    const expandedUnitKeys = (() => {
      try {
        const stored = JSON.parse(
          localStorage.getItem(EXPANSION_STORAGE_KEY) || "[]"
        );
        return new Set(
          Array.isArray(stored) ? stored.filter((value) => typeof value === "string" && value) : []
        );
      } catch {
        return /* @__PURE__ */ new Set();
      }
    })();
    function persistExpandedUnitKeys() {
      try {
        localStorage.setItem(
          EXPANSION_STORAGE_KEY,
          JSON.stringify([...expandedUnitKeys].sort())
        );
      } catch {
      }
    }
    function getUnitElements(areaClass) {
      const area = document.querySelector(`[class*="${areaClass}"]`);
      if (!area) return [];
      const grid = area.querySelector('[class*="BattlePanel_combatUnitGrid"]');
      if (!grid) return [];
      return Array.from(
        grid.querySelectorAll('[class*="CombatUnit_combatUnit"]')
      );
    }
    function getBattleUnits() {
      return {
        players: getUnitElements("BattlePanel_playersArea"),
        monsters: getUnitElements("BattlePanel_monstersArea")
      };
    }
    function unitStorageKeys(side, units) {
      const occurrences = /* @__PURE__ */ new Map();
      return units.map((unitEl, index) => {
        const name = String(
          unitEl.querySelector('[class*="CombatUnit_name"]')?.textContent ?? ""
        ).replaceAll(/\s+/g, " ").trim().toLocaleLowerCase();
        if (!name) return `${side}:slot:${index}`;
        const occurrence = occurrences.get(name) ?? 0;
        occurrences.set(name, occurrence + 1);
        return `${side}:name:${name}:${occurrence}`;
      });
    }
    function updateBuffToggle(shell2, effectCount) {
      const expanded = shell2.dataset.expanded === "true";
      const capacity = expanded ? EXPANDED_CAPACITY : COLLAPSED_CAPACITY;
      const hiddenCount = Math.max(0, effectCount - capacity);
      const toggle = shell2.querySelector(".mwi-buff-toggle");
      if (!toggle) return;
      toggle.setAttribute("aria-expanded", String(expanded));
      const direction = expanded ? "▴" : "▾";
      toggle.textContent = hiddenCount ? `+${hiddenCount} ${direction}` : direction;
      const action = expanded ? runtime.config.isZH ? "折叠 Buff" : "Collapse buffs" : runtime.config.isZH ? "展开 Buff" : "Expand buffs";
      const overflow = hiddenCount ? runtime.config.isZH ? `,另有 ${hiddenCount} 个` : `, ${hiddenCount} more` : "";
      toggle.setAttribute("aria-label", `${action}${overflow}`);
      toggle.title = `${action}${overflow}`;
    }
    function setBuffShellExpanded(shell2, expanded, { persist = false } = {}) {
      shell2.dataset.expanded = String(expanded);
      const bar = shell2.querySelector(".mwi-buffbar");
      if (!expanded && bar) bar.scrollTop = 0;
      updateBuffToggle(shell2, bar?.childElementCount ?? 0);
      if (!persist) return;
      const storageKey = shell2.dataset.storageKey;
      if (!storageKey) return;
      if (expanded) expandedUnitKeys.add(storageKey);
      else expandedUnitKeys.delete(storageKey);
      persistExpandedUnitKeys();
    }
    function ensureBuffBar(unitEl, storageKey = "") {
      let shell2 = unitEl.querySelector(".mwi-buff-shell");
      let bar = shell2?.querySelector(".mwi-buffbar");
      if (!shell2 || !bar) {
        shell2 = document.createElement("div");
        shell2.className = "mwi-buff-shell";
        bar = document.createElement("div");
        bar.className = "mwi-buffbar";
        const toggle = document.createElement("button");
        toggle.type = "button";
        toggle.className = "mwi-buff-toggle";
        toggle.addEventListener("click", () => {
          setBuffShellExpanded(shell2, shell2.dataset.expanded !== "true", {
            persist: true
          });
        });
        shell2.append(bar, toggle);
        const statusHost = unitEl.querySelector('[class*="CombatUnit_status"]') ?? unitEl;
        statusHost.classList.add("mwi-has-buffbar");
        statusHost.appendChild(shell2);
      }
      const resolvedStorageKey = storageKey || unitEl.dataset.mwiBuffStorageKey || "";
      if (resolvedStorageKey) {
        unitEl.dataset.mwiBuffStorageKey = resolvedStorageKey;
        if (shell2.dataset.storageKey !== resolvedStorageKey) {
          shell2.dataset.storageKey = resolvedStorageKey;
          setBuffShellExpanded(shell2, expandedUnitKeys.has(resolvedStorageKey));
        }
      } else if (!shell2.hasAttribute("data-expanded")) {
        setBuffShellExpanded(shell2, false);
      }
      return bar;
    }
    function ensureBattleBuffBars(units = getBattleUnits()) {
      for (const [side, unitList] of Object.entries(units)) {
        const storageKeys = unitStorageKeys(side, unitList);
        unitList.forEach((unitEl, index) => {
          if (unitEl) ensureBuffBar(unitEl, storageKeys[index]);
        });
      }
      return units;
    }
    function getState2(unitEl) {
      let state = UNIT_STATE.get(unitEl);
      if (!state) {
        state = { effects: /* @__PURE__ */ new Map() };
        UNIT_STATE.set(unitEl, state);
      }
      return state;
    }
    function getActionKey(state) {
      if (!state || typeof state !== "object") return null;
      if (typeof state.abilityHrid === "string" && state.abilityHrid.length > 0)
        return state.abilityHrid;
      if (state.isAutoAtk === true) return "auto";
      return null;
    }
    function seedStateFromCombatant(list, stateMap) {
      if (!Array.isArray(list)) return;
      for (let i = 0; i < list.length; i += 1) {
        const entry = list[i];
        if (!entry || typeof entry !== "object") continue;
        const state = stateMap.get(String(i)) || {};
        const preparing = typeof entry.preparingAbilityHrid === "string" ? entry.preparingAbilityHrid : "";
        if (preparing) {
          state.abilityHrid = preparing;
          delete state.isAutoAtk;
        } else if (entry.isPreparingAutoAttack === true || entry.isAutoAtk === true) {
          state.isAutoAtk = true;
          delete state.abilityHrid;
        }
        if (typeof entry.currentHitpoints === "number")
          state.cHP = entry.currentHitpoints;
        if (typeof entry.currentManapoints === "number")
          state.cMP = entry.currentManapoints;
        stateMap.set(String(i), state);
      }
    }
    function clearMonsterBuffs() {
      const units = getBattleUnits().monsters;
      for (const unitEl of units) {
        if (!unitEl) continue;
        const state = UNIT_STATE.get(unitEl);
        if (state) state.effects.clear();
        const bar = unitEl.querySelector(".mwi-buffbar");
        if (bar) {
          bar.replaceChildren();
          const shell2 = bar.closest(".mwi-buff-shell");
          if (shell2) updateBuffToggle(shell2, 0);
        }
      }
    }
    function resetForNewBattle() {
      BATTLE_STATE.monsters.clear();
      PENDING_BUFFS.length = 0;
      PENDING_DEBUFFS.length = 0;
    }
    function handleNewBattle(signal) {
      resetForNewBattle();
      ensureBattleBuffBars();
      clearMonsterBuffs();
      seedStateFromCombatant(signal.players, BATTLE_STATE.players);
      seedStateFromCombatant(signal.monsters, BATTLE_STATE.monsters);
    }
    function mergeState(stateMap, patchMap, mapName) {
      const actionChanges = [];
      const hpChanges = [];
      if (!patchMap || typeof patchMap !== "object")
        return { actionChanges, hpChanges };
      const keys = Object.keys(patchMap);
      for (let idx = 0; idx < keys.length; idx += 1) {
        const key = keys[idx];
        const patch = patchMap[key];
        if (!patch || typeof patch !== "object") continue;
        const prev = stateMap.get(key) || {};
        const next = { ...prev, ...patch };
        const patchPreparing = typeof patch.preparingAbilityHrid === "string" ? patch.preparingAbilityHrid : typeof patch.abilityHrid === "string" ? patch.abilityHrid : "";
        const patchAuto = patch.isPreparingAutoAttack === true || patch.isAutoAtk === true || patch.isAutoAttack === true;
        if (patchPreparing) {
          next.abilityHrid = patchPreparing;
          delete next.isAutoAtk;
        } else if (patchAuto) {
          next.isAutoAtk = true;
          delete next.abilityHrid;
        }
        const prevAction = getActionKey(prev);
        const nextAction = getActionKey(next);
        if (prevAction && nextAction && prevAction !== nextAction) {
          actionChanges.push({ mapName, key, prevAction, nextAction });
        }
        if (typeof prev.cHP === "number" && typeof next.cHP === "number" && prev.cHP !== next.cHP) {
          hpChanges.push({
            mapName,
            key,
            prevHP: prev.cHP,
            newHP: next.cHP,
            delta: next.cHP - prev.cHP
          });
        }
        stateMap.set(key, next);
      }
      return { actionChanges, hpChanges };
    }
    function updateBattleState(payload) {
      if (!payload || typeof payload !== "object") return;
      const playerResult = mergeState(BATTLE_STATE.players, payload.pMap, "pMap");
      const monsterResult = mergeState(
        BATTLE_STATE.monsters,
        payload.mMap,
        "mMap"
      );
      const monsterHits = monsterResult.hpChanges.filter((h) => h.delta < 0).map((h) => Number(h.key));
      for (const change of playerResult.actionChanges) {
        if (BUFFS.has(change.prevAction)) {
          const casterIndex2 = Number(change.key);
          if (Number.isInteger(casterIndex2))
            PENDING_BUFFS.push({
              mapName: "pMap",
              casterIndex: casterIndex2,
              abilityHrid: change.prevAction
            });
        }
        if (!DEBUFFS.has(change.prevAction)) continue;
        if (monsterHits.length === 0) continue;
        const casterIndex = Number(change.key);
        if (!Number.isInteger(casterIndex)) continue;
        PENDING_DEBUFFS.push({
          casterMap: "pMap",
          casterIndex,
          abilityHrid: change.prevAction,
          targetSide: "monsters",
          targets: monsterHits
        });
      }
      const playerHits = playerResult.hpChanges.filter((h) => h.delta < 0).map((h) => Number(h.key));
      for (const change of monsterResult.actionChanges) {
        if (BUFFS.has(change.prevAction)) {
          const casterIndex2 = Number(change.key);
          if (Number.isInteger(casterIndex2))
            PENDING_BUFFS.push({
              mapName: "mMap",
              casterIndex: casterIndex2,
              abilityHrid: change.prevAction
            });
        }
        if (!DEBUFFS.has(change.prevAction)) continue;
        if (playerHits.length === 0) continue;
        const casterIndex = Number(change.key);
        if (!Number.isInteger(casterIndex)) continue;
        PENDING_DEBUFFS.push({
          casterMap: "mMap",
          casterIndex,
          abilityHrid: change.prevAction,
          targetSide: "players",
          targets: playerHits
        });
      }
    }
    function renderUnit(unitEl) {
      const state = getState2(unitEl);
      const bar = ensureBuffBar(unitEl);
      const now = Date.now();
      const entries = Array.from(state.effects.values()).filter(
        (effect) => effect.expiresAt > now
      );
      state.effects = new Map(
        entries.map((effect) => [effect.abilityHrid, effect])
      );
      const scrollTop = bar.scrollTop;
      bar.replaceChildren();
      for (const effect of entries.sort((a, b) => a.expiresAt - b.expiresAt)) {
        const chip = document.createElement("span");
        chip.className = `mwi-chip ${effect.kind === "buff" ? "mwi-buff" : "mwi-debuff"}`;
        const iconWrap = document.createElement("span");
        iconWrap.className = "mwi-icon-wrap";
        const icon = document.createElementNS(
          "http://www.w3.org/2000/svg",
          "svg"
        );
        icon.setAttribute("role", "img");
        icon.setAttribute("aria-label", runtime.config.isZH ? "技能" : "Ability");
        icon.setAttribute("class", "Icon_icon__2LtL_ mwi-icon");
        icon.setAttribute("width", "100%");
        icon.setAttribute("height", "100%");
        const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
        const spriteRef = getGameSpriteHref("abilities", effect.abilityHrid);
        if (spriteRef) {
          use.setAttribute("href", spriteRef);
          use.setAttribute("xlink:href", spriteRef);
          icon.appendChild(use);
          iconWrap.appendChild(icon);
        } else {
          iconWrap.textContent = "?";
        }
        const total = Math.max(1, effect.durationSec);
        const elapsed = Math.max(
          0,
          Math.min(total, (now - effect.startedAt) / 1e3)
        );
        const progress = Math.min(1, Math.max(0, elapsed / total));
        const degrees = progress * 360;
        chip.appendChild(iconWrap);
        const ring = document.createElement("span");
        ring.className = "mwi-progress-ring";
        ring.style.setProperty("--mwi-ring-deg", `${degrees}deg`);
        ring.style.setProperty(
          "--mwi-ring-color",
          effect.kind === "buff" ? "rgba(60,140,60,0.7)" : "rgba(180,60,60,0.7)"
        );
        chip.appendChild(ring);
        bar.appendChild(chip);
      }
      if (bar.closest(".mwi-buff-shell")?.dataset.expanded === "true") {
        bar.scrollTop = scrollTop;
      }
      const shell2 = bar.closest(".mwi-buff-shell");
      if (shell2) updateBuffToggle(shell2, entries.length);
    }
    function updateUnitEffect(unitEl, kind, abilityHrid, durationSec) {
      const state = getState2(unitEl);
      const now = Date.now();
      state.effects.set(abilityHrid, {
        abilityHrid,
        kind,
        durationSec,
        startedAt: now,
        expiresAt: now + durationSec * 1e3
      });
      renderUnit(unitEl);
    }
    function applyBattleUpdated(payload) {
      const pMap = payload?.pMap;
      const mMap = payload?.mMap;
      const units = ensureBattleBuffBars();
      if (units.players.length === 0 && units.monsters.length === 0) return;
      ensureBuffStyles(scope);
      updateBattleState(payload);
      if (PENDING_BUFFS.length > 0) {
        const pending = PENDING_BUFFS.splice(0, PENDING_BUFFS.length);
        for (const item of pending) {
          if (!BUFFS.has(item.abilityHrid)) continue;
          const duration = BUFFS.get(item.abilityHrid);
          const isTeamBuff = TEAM_BUFFS.has(item.abilityHrid);
          const unitList = item.mapName === "pMap" ? units.players : units.monsters;
          if (isTeamBuff) {
            for (const unitEl of unitList) {
              if (unitEl)
                updateUnitEffect(unitEl, "buff", item.abilityHrid, duration);
            }
          } else {
            const unitEl = unitList[item.casterIndex];
            if (unitEl)
              updateUnitEffect(unitEl, "buff", item.abilityHrid, duration);
          }
        }
      }
      if (PENDING_DEBUFFS.length > 0) {
        const pending = PENDING_DEBUFFS.splice(0, PENDING_DEBUFFS.length);
        for (const item of pending) {
          if (!DEBUFFS.has(item.abilityHrid)) continue;
          const duration = DEBUFFS.get(item.abilityHrid);
          const applyList = item.targetSide === "monsters" ? units.monsters : units.players;
          const targets = SINGLE_TARGET_DEBUFFS.has(item.abilityHrid) ? item.targets.slice(0, 1) : item.targets;
          for (const target of targets) {
            const unitEl = applyList[target];
            if (unitEl)
              updateUnitEffect(unitEl, "debuff", item.abilityHrid, duration);
          }
        }
      }
      const applyBuffsFromMap = (map, mapName) => {
        if (!map || typeof map !== "object") return;
        const unitList = mapName === "pMap" ? units.players : units.monsters;
        const keys = Object.keys(map);
        for (let idx = 0; idx < keys.length; idx += 1) {
          const key = keys[idx];
          const entity = map[key];
          if (!entity || typeof entity !== "object") continue;
          const abilityHrid = entity.abilityHrid;
          if (typeof abilityHrid !== "string" || abilityHrid.length === 0)
            continue;
          if (!BUFFS.has(abilityHrid)) continue;
          const duration = BUFFS.get(abilityHrid);
          const keyIndex = Number.isInteger(Number(key)) ? Number(key) : idx;
          if (TEAM_BUFFS.has(abilityHrid)) {
            for (const unitEl of unitList) {
              if (unitEl) updateUnitEffect(unitEl, "buff", abilityHrid, duration);
            }
          } else {
            const unitEl = unitList[keyIndex];
            if (unitEl) updateUnitEffect(unitEl, "buff", abilityHrid, duration);
          }
        }
      };
      applyBuffsFromMap(pMap, "pMap");
      applyBuffsFromMap(mMap, "mMap");
    }
    function tickCountdowns() {
      const units = getBattleUnits();
      let active = false;
      for (const unitEl of [...units.players, ...units.monsters]) {
        if (!UNIT_STATE.has(unitEl)) continue;
        renderUnit(unitEl);
        if (UNIT_STATE.get(unitEl)?.effects?.size) active = true;
      }
      return active;
    }
    function hasActiveEffects() {
      const now = Date.now();
      const units = getBattleUnits();
      return [...units.players, ...units.monsters].some(
        (unitEl) => [...UNIT_STATE.get(unitEl)?.effects?.values?.() ?? []].some(
          (effect) => effect.expiresAt > now
        )
      );
    }
    function removeAllBuffBars() {
      const units = getBattleUnits();
      for (const unitEl of [...units.players, ...units.monsters]) {
        const shell2 = unitEl?.querySelector(".mwi-buff-shell");
        const bar = unitEl?.querySelector(".mwi-buffbar");
        if (shell2 || bar) {
          (shell2 ?? bar).closest(".mwi-has-buffbar")?.classList.remove("mwi-has-buffbar");
          (shell2 ?? bar).remove();
          delete unitEl.dataset.mwiBuffStorageKey;
        }
      }
    }
    return {
      handleNewBattle,
      applyBattleUpdated,
      hasActiveEffects,
      mountBuffBars: ensureBattleBuffBars,
      tickCountdowns,
      removeAllBuffBars
    };
  }
  runtime.features.register({
    id: "battleBuffs",
    setting: "battleBuffs",
    initialize({ scope }) {
      const tracker = createBuffTracker(scope);
      ensureBuffStyles(scope);
      tracker.mountBuffBars();
      let countdownTimer = null;
      const tick = () => {
        countdownTimer = null;
        if (tracker.tickCountdowns()) {
          countdownTimer = setTimeout(tick, 1e3);
        }
      };
      const ensureCountdown = () => {
        if (countdownTimer === null && tracker.hasActiveEffects()) {
          countdownTimer = setTimeout(tick, 1e3);
        }
      };
      scope.add(
        runtime.onMessage("new_battle", (payload) => {
          tracker.handleNewBattle(payload);
          ensureCountdown();
        })
      );
      scope.add(
        runtime.onMessage("battle_updated", (payload) => {
          tracker.applyBattleUpdated(payload);
          ensureCountdown();
        })
      );
      scope.add(() => {
        if (countdownTimer !== null) clearTimeout(countdownTimer);
        tracker.removeAllBuffBars();
      });
    }
  });
  Object.assign(runtime.api, {
    battleBuffs: { BUFFS, DEBUFFS, TEAM_BUFFS, SINGLE_TARGET_DEBUFFS }
  });

  // src/features/inventory.js
  var inventoryRefreshTimer = null;
  var inventoryDisplayVersion = 0;
  var frozenInventoryDisplays = /* @__PURE__ */ new Map();
  var frozenInventoryDisplayPromises = /* @__PURE__ */ new Map();
  var INVENTORY_SUMMARY_STYLE_ID = "mwitools-inventory-summary-style";
  function addInventorySummaryStyles() {
    if (document.getElementById(INVENTORY_SUMMARY_STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = INVENTORY_SUMMARY_STYLE_ID;
    style.textContent = `
    #script_inventory_summary {
      display: block !important;
      margin: .0625rem 0;
      color: var(--color-text-primary, #f3f5f7);
      font-family: inherit;
      font-size: calc(.875rem * var(--mwi-ui-font-scale, 1));
      line-height: 1.2;
      text-align: left;
    }
    #script_inv_sort_controls { display: block !important; }
    #script_inv_sort_controls button {
      margin: 0 2px;
      padding: 2px 8px;
      border: 1px solid rgba(255, 255, 255, .16);
      border-radius: 4px;
      background: rgba(255, 255, 255, .08);
      color: var(--color-text-secondary, #aeb5c0);
      box-shadow: none;
      font: inherit;
      font-size: .78rem;
      font-weight: 500;
      cursor: pointer;
      transition: all .15s ease-in-out;
    }
    #script_inv_sort_controls[data-sort-order="fair"] #script_sortByFair_btn,
    #script_inv_sort_controls[data-sort-order="ask"] #script_sortByAsk_btn,
    #script_inv_sort_controls[data-sort-order="bid"] #script_sortByBid_btn,
    #script_inv_sort_controls[data-sort-order="none"] #script_sortByNone_btn {
      border-color: transparent;
      background: ${runtime.config.SCRIPT_COLOR_MAIN};
      color: #0b1522;
      box-shadow: 0 0 8px rgba(0, 198, 255, .45);
      font-weight: 700;
    }
    [class*="Item_enhancementLevel"] ~ #script_stack_price {
      margin-top: 15px;
    }
    .mwi-inventory-summary-grid {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      gap: .0625rem;
    }
    .mwi-summary-card {
      --mwi-summary-accent: 120, 174, 255;
      min-width: 0;
      overflow: visible;
      border: 0;
      border-left: 2px solid rgba(var(--mwi-summary-accent), .75);
      border-radius: 0;
      background: transparent;
    }
    .mwi-summary-card--combat { --mwi-summary-accent: 238, 115, 103; }
    .mwi-summary-card--skilling { --mwi-summary-accent: 90, 200, 149; }
    .mwi-summary-card--assets { --mwi-summary-accent: 230, 181, 79; }
    .mwi-summary-toggle {
      display: flex;
      width: 100%;
      min-height: 1.375rem;
      box-sizing: border-box;
      align-items: center;
      gap: .1875rem;
      padding: .1875rem .25rem;
      border: 0;
      background: transparent;
      color: inherit;
      font: inherit;
      text-align: left;
      cursor: pointer;
      transition: none;
    }
    .mwi-summary-toggle:hover { background: rgba(var(--mwi-summary-accent), .075); }
    .mwi-summary-toggle:focus-visible {
      outline: 2px solid rgba(var(--mwi-summary-accent), .72);
      outline-offset: -3px;
    }
    .mwi-summary-heading {
      display: flex;
      min-width: 0;
      flex: 1;
      align-items: baseline;
      gap: .1875rem;
    }
    .mwi-summary-label {
      flex: 0 0 auto;
      color: var(--color-text-secondary, #aeb5c0);
      font-size: inherit;
      font-weight: 600;
      white-space: nowrap;
    }
    .mwi-summary-value {
      min-width: 0;
      color: rgb(var(--mwi-summary-accent));
      font-size: inherit;
      font-weight: 750;
      line-height: inherit;
      overflow-wrap: anywhere;
    }
    .mwi-summary-today-profit { margin-left:.22rem; font-size:.82em; font-weight:650; white-space:nowrap; }
    .mwi-summary-today-profit.is-positive { color:#5fce83; }
    .mwi-summary-today-profit.is-negative { color:#ff7474; }
    .mwi-summary-today-profit.is-neutral { color:var(--color-text-secondary,#aeb5c0); }
    .mwi-summary-chevron {
      width: .375rem;
      height: .375rem;
      margin: 0 .0625rem 0 0;
      flex: 0 0 .375rem;
      border-right: 1.5px solid rgba(255, 255, 255, .65);
      border-bottom: 1.5px solid rgba(255, 255, 255, .65);
      transform: rotate(45deg) translate(-2px, 2px);
      transition: transform .18s ease;
    }
    .mwi-summary-toggle[aria-expanded="true"] .mwi-summary-chevron {
      transform: rotate(225deg) translate(-2px, 2px);
    }
    .mwi-summary-details {
      margin: 0 .25rem 0 .5rem;
      animation: mwi-summary-reveal .16s ease-out;
    }
    .mwi-summary-stats {
      position: relative;
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      gap: 0;
      padding: 0 0 0 .5rem;
    }
    .mwi-summary-stats::before {
      position: absolute;
      top: 0;
      bottom: .5em;
      left: 0;
      width: 1px;
      background: rgba(var(--mwi-summary-accent), .34);
      content: "";
    }
    .mwi-summary-stat {
      position: relative;
      display: flex;
      min-width: 0;
      align-items: baseline;
      justify-content: flex-start;
      gap: .375rem;
      padding: .15rem .25rem;
      font-size: inherit;
      line-height: inherit;
    }
    .mwi-summary-stat::before {
      position: absolute;
      top: 50%;
      left: -.5rem;
      width: .5rem;
      border-top: 1px solid rgba(var(--mwi-summary-accent), .34);
      content: "";
    }
    .mwi-summary-stat::after {
      position: absolute;
      top: calc(50% - 2px);
      left: calc(-.5rem - 2px);
      width: 3px;
      height: 3px;
      border-radius: 50%;
      background: rgb(var(--mwi-summary-accent));
      content: "";
    }
    .mwi-summary-stat-label {
      overflow: hidden;
      color: var(--color-text-secondary, #9da6b2);
      font-size: inherit;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .mwi-summary-stat-value { color: #f3f5f7; font-size: inherit; font-weight: 650; }
    .mwi-asset-groups {
      display: grid;
      gap: 0;
      padding: 0 0 0 .5rem;
    }
    .mwi-asset-group {
      position: relative;
      overflow: visible;
    }
    .mwi-asset-group:not(:last-child)::after {
      position: absolute;
      top: .6em;
      bottom: -.6em;
      left: -.5rem;
      border-left: 1px solid rgba(var(--mwi-summary-accent), .34);
      content: "";
    }
    .mwi-asset-toggle {
      position: relative;
      display: flex;
      width: 100%;
      min-height: 0;
      align-items: center;
      gap: .25rem;
      padding: .15rem .25rem;
      border: 0;
      background: transparent;
      color: var(--color-text-primary, #e8ebef);
      font: inherit;
      font-size: inherit;
      font-weight: 600;
      line-height: inherit;
      text-align: left;
      cursor: pointer;
    }
    .mwi-asset-toggle::before {
      position: absolute;
      top: 50%;
      left: -.5rem;
      width: .5rem;
      border-top: 1px solid rgba(var(--mwi-summary-accent), .34);
      content: "";
    }
    .mwi-asset-toggle:hover { background: rgba(255, 255, 255, .04); }
    .mwi-asset-toggle:focus-visible { outline: 1px solid rgb(var(--mwi-summary-accent)); outline-offset: -2px; }
    .mwi-asset-toggle .mwi-summary-chevron { margin: 0 2px 0 0; }
    .mwi-asset-subtotal {
      min-width: 0;
      margin-left: 6px;
      color: rgb(var(--mwi-summary-accent));
      font-size: inherit;
      font-weight: 700;
      font-variant-numeric: tabular-nums;
      overflow-wrap: anywhere;
    }
    .mwi-asset-rows {
      position: relative;
      display: grid;
      gap: 0;
      margin-left: .25rem;
      padding: 0 .25rem 0 .5rem;
    }
    .mwi-asset-rows::before {
      position: absolute;
      top: 0;
      bottom: .5em;
      left: 0;
      width: 1px;
      background: rgba(var(--mwi-summary-accent), .25);
      content: "";
    }
    .mwi-asset-row {
      position: relative;
      display: flex;
      align-items: baseline;
      justify-content: flex-start;
      gap: .375rem;
      padding: .15rem 0;
      color: var(--color-text-secondary, #aeb5c0);
      font-size: inherit;
      line-height: inherit;
    }
    .mwi-asset-row::before {
      position: absolute;
      top: 50%;
      left: -.5rem;
      width: .5rem;
      border-top: 1px solid rgba(var(--mwi-summary-accent), .25);
      content: "";
    }
    .mwi-asset-row .mwi-number, .mwi-asset-row > span:last-child { color: #f3f5f7; font-weight: 600; }
    .mwi-inventory-category-heading {
      display: flex !important;
      min-width: 0;
      align-items: center;
      gap: 8px;
    }
    .mwi-inventory-category-value {
      display: inline-flex;
      max-width: min(48%, 150px);
      align-items: center;
      overflow: hidden;
      padding: 2px 7px;
      border: 1px solid rgba(230, 181, 79, .22);
      border-radius: 999px;
      background: rgba(230, 181, 79, .09);
      color: #e6c778;
      font-size: calc(.6875rem * var(--mwi-ui-font-scale, 1));
      font-weight: 650;
      line-height: 1.25;
      letter-spacing: .015em;
      pointer-events: none;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    @keyframes mwi-summary-reveal {
      from { opacity: 0; transform: translateY(-3px); }
      to { opacity: 1; transform: translateY(0); }
    }
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function numberHtml(value) {
    return `<span class="mwi-number" title="${runtime.api.formatExactNumber(value, 0)}">${runtime.api.numberFormatter(value)}</span>`;
  }
  function inventoryTodayProfitHtml(values) {
    const comparison = runtime.api.assetHistory?.getComparison?.();
    const previous = comparison?.record?.values;
    if (comparison?.gapDays !== 1) return "";
    if (!Number.isFinite(values?.total) || !Number.isFinite(previous?.total)) {
      return "";
    }
    const change = values.total - previous.total;
    const sign = change > 0 ? "+" : change < 0 ? "−" : "";
    const className = change > 0 ? "is-positive" : change < 0 ? "is-negative" : "is-neutral";
    const formatted = runtime.api.numberFormatter(Math.abs(change));
    const exact = runtime.api.formatExactNumber(change, 0);
    const [open, close] = runtime.config.isZH ? ["(", ")"] : ["(", ")"];
    return `<span class="mwi-summary-today-profit ${className}" title="${exact}">${open}${sign}${formatted}${close}</span>`;
  }
  function scheduleNetworthRefresh() {
    addInventorySummaryStyles();
    if (!Array.isArray(runtime.state.initData_characterItems)) return;
    clearTimeout(inventoryRefreshTimer);
    inventoryRefreshTimer = setTimeout(() => calculateNetworth(), 100);
  }
  function inventoryDisplayKey() {
    const characterId = String(runtime.state.currentCharacterId ?? "").trim();
    if (!characterId) return "";
    return `${runtime.api.getMarketEnvironment?.() ?? "production"}:${characterId}`;
  }
  var INVENTORY_CATEGORY_ALIASES = {
    "/item_categories/currency": ["currency", "currencies", "货币"],
    "/item_categories/loot": ["loot", "loots", "战利品"],
    "/item_categories/scroll": ["scroll", "scrolls", "卷轴"],
    "/item_categories/labyrinth": ["labyrinth", "迷宫"],
    "/item_categories/dungeon_key": [
      "dungeon key",
      "dungeon keys",
      "地牢钥匙",
      "地下城钥匙"
    ],
    "/item_categories/food": ["food", "foods", "食物"],
    "/item_categories/drink": ["drink", "drinks", "饮料"],
    "/item_categories/ability_book": ["ability book", "ability books", "技能书"],
    "/item_categories/equipment": ["equipment", "装备"],
    "/item_categories/resource": ["resource", "resources", "资源"]
  };
  function normalizeCategoryLabel(value) {
    return String(value ?? "").replace(/^[+−-]\s*/, "").replace(/\s*\(\d+\)\s*$/, "").trim().toLowerCase();
  }
  function resolveInventoryCategoryHrid(grid, heading) {
    const firstItem = grid.querySelector('div[class*="Item_itemContainer"]');
    if (firstItem) {
      const itemHrid = resolveEntityFromElement("item", firstItem);
      const categoryHrid = runtime.state.initData_itemDetailMap?.[itemHrid]?.categoryHrid;
      if (categoryHrid) return categoryHrid;
    }
    const labels = [
      runtime.api.getOriTextFromElement?.(heading),
      heading.textContent
    ].map(normalizeCategoryLabel);
    return Object.entries(INVENTORY_CATEGORY_ALIASES).find(
      ([, aliases]) => labels.some((label) => aliases.includes(label))
    )?.[0];
  }
  function calculateInventoryCategoryValues() {
    const categoryValues = /* @__PURE__ */ new Map();
    for (const item of runtime.state.initData_characterItems ?? []) {
      if (item?.itemLocationHrid !== "/item_locations/inventory") continue;
      if (item.itemHrid === "/items/cowbell" && !runtime.api.shouldIncludeCowbellsInAssets()) {
        continue;
      }
      if (runtime.api.isOptionalTokenAsset?.(item.itemHrid) && !runtime.api.shouldIncludeGuildDungeonTokensInAssets?.()) {
        continue;
      }
      const categoryHrid = runtime.state.initData_itemDetailMap?.[item.itemHrid]?.categoryHrid;
      if (!categoryHrid) continue;
      const value = Math.max(0, Number(item.count) || 0) * runtime.api.getAssetValue(item.itemHrid, item.enhancementLevel, {
        itemLocationHrid: item.itemLocationHrid
      });
      categoryValues.set(
        categoryHrid,
        (categoryValues.get(categoryHrid) ?? 0) + value
      );
    }
    return categoryValues;
  }
  function addInventoryCategoryValues(invElem, categoryValues = calculateInventoryCategoryValues()) {
    for (const category of invElem.children) {
      const grid = category.matches?.('[class*="Inventory_itemGrid"]') ? category : category.querySelector(':scope > [class*="Inventory_itemGrid"]') ?? category;
      const heading = grid.querySelector(
        ':scope > [class*="Inventory_label"],:scope > button[class*="Inventory_categoryButton"]'
      );
      if (!heading) continue;
      const categoryHrid = resolveInventoryCategoryHrid(grid, heading);
      if (!categoryHrid) continue;
      const total = categoryValues.get(categoryHrid) ?? 0;
      grid.dataset.mwitoolsInventoryCategory = "true";
      heading.classList.add("mwi-inventory-category-heading");
      heading.querySelector(":scope > .mwi-inventory-category-value")?.remove();
      const value = document.createElement("span");
      value.className = "mwi-inventory-category-value";
      value.title = `${runtime.config.isZH ? "分类价值" : "Category value"}: ${runtime.api.formatExactNumber(total, 0)}`;
      value.textContent = `${runtime.config.isZH ? "价值" : "Value"} ${runtime.api.numberFormatter(total)}`;
      heading.appendChild(value);
    }
  }
  async function getFrozenInventoryDisplay(force = false) {
    const key = inventoryDisplayKey();
    if (!key) return null;
    if (!force && frozenInventoryDisplays.has(key)) {
      return frozenInventoryDisplays.get(key);
    }
    if (!force && frozenInventoryDisplayPromises.has(key)) {
      return frozenInventoryDisplayPromises.get(key);
    }
    const pendingDisplay = runtime.api.refreshAssetSnapshot().then((snapshot) => {
      if (!snapshot) return frozenInventoryDisplays.get(key) ?? null;
      const display = {
        snapshot,
        categoryValues: calculateInventoryCategoryValues(),
        version: ++inventoryDisplayVersion
      };
      frozenInventoryDisplays.set(key, display);
      return display;
    }).finally(() => frozenInventoryDisplayPromises.delete(key));
    frozenInventoryDisplayPromises.set(key, pendingDisplay);
    return pendingDisplay;
  }
  async function calculateNetworth(options = {}) {
    if (!Array.isArray(runtime.state.initData_characterItems)) return;
    const targetNodes = document.querySelectorAll(
      'div[class*="Inventory_items"]'
    );
    if (!targetNodes.length) return;
    const showWorth = runtime.settings.settingsMap.invWorth.isTrue;
    const showSort = runtime.settings.settingsMap.invSort.isTrue;
    const display = showWorth ? await getFrozenInventoryDisplay(options.force === true) : null;
    if (showWorth && !display) return;
    const snapshot = display?.snapshot;
    addInventorySummaryStyles();
    const addInventorySummary = (invElem) => {
      const { scores, values } = snapshot;
      const previousSummary = invElem.parentElement?.querySelector(
        "#script_inventory_summary"
      );
      const wasCombatScoreOpen = previousSummary?.querySelector("#buildScores")?.style.display === "block";
      const wasSkillingScoreOpen = previousSummary?.querySelector("#skillingScores")?.style.display === "block";
      const wasNetworthOpen = previousSummary?.querySelector("#netWorthDetails")?.style.display === "block";
      previousSummary?.remove();
      const previousSortControls = invElem.parentElement?.querySelector(
        "#script_inv_sort_controls"
      );
      const summaryHTML = `<div id="script_inventory_summary">
        <div class="mwi-inventory-summary-grid">
          <section class="mwi-summary-card mwi-summary-card--combat">
            <button type="button" class="mwi-summary-toggle" id="toggleScores" aria-expanded="false" aria-controls="buildScores">
              <span class="mwi-summary-chevron" aria-hidden="true"></span>
              <span class="mwi-summary-heading">
                <span class="mwi-summary-label">${runtime.config.isZH ? "战斗着装评分:" : "Combat Gear Score: "}</span>
                <span class="mwi-summary-value">${runtime.api.formatScore(scores.battle.total)}</span>
              </span>
            </button>
            <div class="mwi-summary-details" id="buildScores" style="display: none;" hidden>
              <div class="mwi-summary-stats">
                <div class="mwi-summary-stat"><span class="mwi-summary-stat-label">${runtime.config.isZH ? "房屋:" : "House: "}</span><span class="mwi-summary-stat-value">${runtime.api.formatScore(scores.battle.house)}</span></div>
                <div class="mwi-summary-stat"><span class="mwi-summary-stat-label">${runtime.config.isZH ? "技能:" : "Abilities: "}</span><span class="mwi-summary-stat-value">${runtime.api.formatScore(scores.battle.abilities)}</span></div>
                <div class="mwi-summary-stat"><span class="mwi-summary-stat-label">${runtime.config.isZH ? "装备:" : "Equipment: "}</span><span class="mwi-summary-stat-value">${runtime.api.formatScore(scores.battle.equipment)}</span></div>
                <div class="mwi-summary-stat"><span class="mwi-summary-stat-label">${runtime.config.isZH ? "战斗神龛:" : "Combat shrine: "}</span><span class="mwi-summary-stat-value">${Number.isFinite(scores.battle.shrine) ? runtime.api.formatScore(scores.battle.shrine) : "—"}</span></div>
              </div>
            </div>
          </section>

          <section class="mwi-summary-card mwi-summary-card--skilling">
            <button type="button" class="mwi-summary-toggle" id="toggleSkillingScores" aria-expanded="false" aria-controls="skillingScores">
              <span class="mwi-summary-chevron" aria-hidden="true"></span>
              <span class="mwi-summary-heading">
                <span class="mwi-summary-label">${runtime.config.isZH ? "生活着装评分:" : "Skilling Gear Score: "}</span>
                <span class="mwi-summary-value">${runtime.api.formatScore(scores.skilling.total)}</span>
              </span>
            </button>
            <div class="mwi-summary-details" id="skillingScores" style="display: none;" hidden>
              <div class="mwi-summary-stats">
                <div class="mwi-summary-stat"><span class="mwi-summary-stat-label">${runtime.config.isZH ? "房屋:" : "House: "}</span><span class="mwi-summary-stat-value">${runtime.api.formatScore(scores.skilling.house)}</span></div>
                <div class="mwi-summary-stat"><span class="mwi-summary-stat-label">${runtime.config.isZH ? "工具:" : "Tools: "}</span><span class="mwi-summary-stat-value">${runtime.api.formatScore(scores.skilling.tools)}</span></div>
                <div class="mwi-summary-stat"><span class="mwi-summary-stat-label">${runtime.config.isZH ? "装备:" : "Equipment: "}</span><span class="mwi-summary-stat-value">${runtime.api.formatScore(scores.skilling.equipment)}</span></div>
                <div class="mwi-summary-stat"><span class="mwi-summary-stat-label">${runtime.config.isZH ? "生活神龛:" : "Skilling shrine: "}</span><span class="mwi-summary-stat-value">${Number.isFinite(scores.skilling.shrine) ? runtime.api.formatScore(scores.skilling.shrine) : "—"}</span></div>
              </div>
            </div>
          </section>

          <section class="mwi-summary-card mwi-summary-card--assets">
            <button type="button" class="mwi-summary-toggle" id="toggleNetWorth" aria-expanded="false" aria-controls="netWorthDetails">
              <span class="mwi-summary-chevron" aria-hidden="true"></span>
              <span class="mwi-summary-heading">
                <span class="mwi-summary-label">${runtime.config.isZH ? "总资产:" : "Total assets: "}</span>
                <span class="mwi-summary-value">${numberHtml(values.total)}${inventoryTodayProfitHtml(values)}</span>
              </span>
            </button>
            <div class="mwi-summary-details" id="netWorthDetails" style="display: none;" hidden>
              <div class="mwi-asset-groups">
                <section class="mwi-asset-group">
                  <button type="button" class="mwi-asset-toggle" id="toggleCurrentAssets" aria-expanded="false" aria-controls="currentAssets"><span class="mwi-summary-chevron" aria-hidden="true"></span><span>${runtime.config.isZH ? "流动资产" : "Liquid assets"}</span><span class="mwi-asset-subtotal">${numberHtml(values.liquid)}</span></button>
                  <div class="mwi-asset-rows" id="currentAssets" style="display: none;" hidden>
                    <div class="mwi-asset-row"><span>${runtime.config.isZH ? "装备:" : "Equipment: "}</span>${numberHtml(values.equipment)}</div>
                    <div class="mwi-asset-row"><span>${runtime.config.isZH ? "库存:" : "Inventory: "}</span>${numberHtml(values.inventory)}</div>
                    <div class="mwi-asset-row"><span>${runtime.config.isZH ? "市场订单:" : "Market orders: "}</span>${numberHtml(values.marketListings)}</div>
                  </div>
                </section>
                <section class="mwi-asset-group">
                  <button type="button" class="mwi-asset-toggle" id="toggleNonCurrentAssets" aria-expanded="false" aria-controls="nonCurrentAssets"><span class="mwi-summary-chevron" aria-hidden="true"></span><span>${runtime.config.isZH ? "非流动资产" : "Non-current assets"}</span><span class="mwi-asset-subtotal">${numberHtml(values.fixed)}</span></button>
                  <div class="mwi-asset-rows" id="nonCurrentAssets" style="display: none;" hidden>
                    <div class="mwi-asset-row"><span>${runtime.config.isZH ? "房屋:" : "Houses: "}</span>${numberHtml(values.houses)}</div>
                    <div class="mwi-asset-row"><span>${runtime.config.isZH ? "技能:" : "Abilities: "}</span>${numberHtml(values.abilities)}</div>
                    <div class="mwi-asset-row"><span>${runtime.config.isZH ? "不可交易代币:" : "Non-tradable tokens: "}</span>${numberHtml(values.nonTradableTokens)}</div>
                    <div class="mwi-asset-row"><span>${runtime.config.isZH ? "神龛:" : "Shrine: "}</span><span>${values.shrine === null ? "—" : numberHtml(values.shrine)}</span></div>
                  </div>
                </section>
              </div>
            </div>
          </section>
        </div>
      </div>`;
      if (previousSortControls) {
        previousSortControls.insertAdjacentHTML("afterend", summaryHTML);
      } else {
        invElem.insertAdjacentHTML("beforebegin", summaryHTML);
      }
      const summary = invElem.parentElement.querySelector(
        "#script_inventory_summary"
      );
      const toggleScores = summary.querySelector("#toggleScores");
      const ScoreDetails = summary.querySelector("#buildScores");
      const toggleSkillingScores = summary.querySelector("#toggleSkillingScores");
      const skillingScoreDetails = summary.querySelector("#skillingScores");
      const toggleButton = summary.querySelector("#toggleNetWorth");
      const netWorthDetails = summary.querySelector("#netWorthDetails");
      const toggleCurrentAssets = summary.querySelector("#toggleCurrentAssets");
      const currentAssets = summary.querySelector("#currentAssets");
      const toggleNonCurrentAssets = summary.querySelector(
        "#toggleNonCurrentAssets"
      );
      const nonCurrentAssets = summary.querySelector("#nonCurrentAssets");
      const setExpanded = (button, panel, expanded) => {
        button.setAttribute("aria-expanded", String(expanded));
        panel.hidden = !expanded;
        panel.style.display = expanded ? "block" : "none";
      };
      if (wasNetworthOpen) {
        setExpanded(toggleButton, netWorthDetails, true);
        setExpanded(toggleCurrentAssets, currentAssets, true);
        setExpanded(toggleNonCurrentAssets, nonCurrentAssets, true);
      }
      if (wasCombatScoreOpen) {
        setExpanded(toggleScores, ScoreDetails, true);
      }
      if (wasSkillingScoreOpen) {
        setExpanded(toggleSkillingScores, skillingScoreDetails, true);
      }
      toggleScores.addEventListener("click", () => {
        const isCollapsed = ScoreDetails.style.display === "none";
        setExpanded(toggleScores, ScoreDetails, isCollapsed);
      });
      toggleSkillingScores.addEventListener("click", () => {
        const isCollapsed = skillingScoreDetails.style.display === "none";
        setExpanded(toggleSkillingScores, skillingScoreDetails, isCollapsed);
      });
      toggleButton.addEventListener("click", () => {
        const isCollapsed = netWorthDetails.style.display === "none";
        setExpanded(toggleButton, netWorthDetails, isCollapsed);
        setExpanded(toggleCurrentAssets, currentAssets, isCollapsed);
        setExpanded(toggleNonCurrentAssets, nonCurrentAssets, isCollapsed);
      });
      toggleCurrentAssets.addEventListener("click", () => {
        const isCollapsed = currentAssets.style.display === "none";
        setExpanded(toggleCurrentAssets, currentAssets, isCollapsed);
      });
      toggleNonCurrentAssets.addEventListener("click", () => {
        const isCollapsed = nonCurrentAssets.style.display === "none";
        setExpanded(toggleNonCurrentAssets, nonCurrentAssets, isCollapsed);
      });
    };
    const renderInventoryPanels = () => {
      for (const node of targetNodes) {
        if (showWorth) {
          node.classList.add("script_buildScore_added");
          const renderVersion = `${display.version}:${runtime.config.isZH ? "zh" : "en"}`;
          const summary2 = node.parentElement?.querySelector(
            "#script_inventory_summary"
          );
          if (node.dataset.mwitoolsInventoryDisplayVersion !== renderVersion || !summary2) {
            addInventorySummary(node);
            addInventoryCategoryValues(node, display.categoryValues);
            node.dataset.mwitoolsInventoryDisplayVersion = renderVersion;
          }
        }
        if (showSort || showWorth) {
          if (!node.classList.contains("script_invSort_added")) {
            node.classList.add("script_invSort_added");
            addInvSortButton(node);
          }
        }
        const summary = node.parentElement?.querySelector(
          "#script_inventory_summary"
        );
        if (summary) {
          summary.style.removeProperty("display");
        }
        const sortControls = node.parentElement?.querySelector(
          "#script_inv_sort_controls"
        );
        if (sortControls) {
          sortControls.style.removeProperty("display");
        }
      }
    };
    renderInventoryPanels();
  }
  function getInventorySortUnitValue(itemHrid, enhancementLevel = 0, order = "fair") {
    const derivedValue = Number(runtime.api.getAssetValue?.(itemHrid, enhancementLevel)) || Number(runtime.api.getFairValue?.(itemHrid, enhancementLevel)) || 0;
    if (order === "ask") {
      return Number(runtime.api.getAskPrice?.(itemHrid, enhancementLevel)) || derivedValue;
    }
    if (order === "bid") {
      return Number(runtime.api.getBidPrice?.(itemHrid, enhancementLevel)) || derivedValue;
    }
    return derivedValue;
  }
  function getInventoryItemEnhancementLevel(itemElem) {
    const levelText = itemElem?.querySelector?.('[class*="Item_enhancementLevel"]')?.textContent ?? "";
    return Number.parseInt(levelText.replace(/\D/g, ""), 10) || 0;
  }
  function isSortableInventoryCategory(typeName, categoryHrid = "") {
    return Boolean(categoryHrid || String(typeName ?? "").trim());
  }
  async function addInvSortButton(invElem) {
    const showSort = runtime.settings.settingsMap.invSort.isTrue;
    const showWorth = runtime.settings.settingsMap.invWorth.isTrue;
    if (showSort) {
      const priceData = await runtime.api.fetchMarketJSON();
      if (!priceData?.marketData) {
        console.error(
          runtime.config.isZH ? "[MWITools] 市场数据不可用,无法创建库存排序按钮。" : "[MWITools] Market data is unavailable; inventory sort controls were not created."
        );
        return;
      }
    }
    const fairButton = `<button
        id="script_sortByFair_btn">
        ${runtime.config.isZH ? "市场价值" : "Market Value"}
        </button>`;
    const askButton = `<button
        id="script_sortByAsk_btn">
        ${runtime.config.isZH ? "出售价" : "Ask"}
        </button>`;
    const bidButton = `<button
        id="script_sortByBid_btn">
        ${runtime.config.isZH ? "收购价" : "Bid"}
        </button>`;
    const noneButton = `<button
        id="script_sortByNone_btn">
        ${runtime.config.isZH ? "无" : "None"}
        </button>`;
    const refreshButton = `<button
        id="script_refresh_inventory_btn">
        ${runtime.config.isZH ? "刷新价值" : "Refresh values"}
        </button>`;
    const buttonsDiv = `<div id="script_inv_sort_controls" data-sort-order="none" style="color: ${runtime.config.SCRIPT_COLOR_MAIN}; font-size: 0.875rem; text-align: left; ">${showSort ? runtime.config.isZH ? "物品排序:" : "Sort items by: " : ""}${showSort ? `${fairButton} ${askButton} ${bidButton} ${noneButton}` : ""}${showWorth ? ` ${refreshButton}` : ""}</div>`;
    if (!invElem.isConnected || !invElem.parentElement) return;
    const existingSummary = invElem.parentElement.querySelector(
      "#script_inventory_summary"
    );
    if (existingSummary) {
      existingSummary.insertAdjacentHTML("beforebegin", buttonsDiv);
    } else {
      invElem.insertAdjacentHTML("beforebegin", buttonsDiv);
    }
    const sortItemsBy = (order) => {
      const controls2 = invElem.parentElement?.querySelector(
        "#script_inv_sort_controls"
      );
      if (controls2) controls2.dataset.sortOrder = order;
      for (const typeDiv of invElem.children) {
        const categoryButton = typeDiv.querySelector(
          '[class*="Inventory_categoryButton"]'
        );
        const typeName = runtime.api.getOriTextFromElement?.(categoryButton) ?? categoryButton?.textContent ?? "";
        const categoryHrid = resolveInventoryCategoryHrid(
          typeDiv,
          categoryButton
        );
        if (!isSortableInventoryCategory(typeName, categoryHrid)) {
          continue;
        }
        const label = typeDiv.querySelector('[class*="Inventory_label"]');
        if (label) label.style.order = Number.MIN_SAFE_INTEGER;
        const itemElems = [
          ...typeDiv.querySelectorAll('[class*="Item_itemContainer"]')
        ];
        const sortableItems = itemElems.map((itemElem, originalIndex) => {
          const itemHrid = resolveEntityFromElement("item", itemElem);
          const enhancementLevel = getInventoryItemEnhancementLevel(itemElem);
          const countText = itemElem.querySelector('[class*="Item_count"]')?.textContent ?? "1";
          const parsedCount = runtime.api.parseCompactNumber?.(countText) ?? Number(countText);
          const itemCount = Number.isFinite(Number(parsedCount)) ? Number(parsedCount) : 1;
          const values = {
            ask: getInventorySortUnitValue(itemHrid, enhancementLevel, "ask") * itemCount,
            bid: getInventorySortUnitValue(itemHrid, enhancementLevel, "bid") * itemCount,
            fair: getInventorySortUnitValue(itemHrid, enhancementLevel, "fair") * itemCount
          };
          if (!itemElem.querySelector("#script_stack_price")) {
            itemElem.style.position = "relative";
            const priceElemHTML = `<div
                        id="script_stack_price"
                        style="z-index: 1; position: absolute; top: 2px; left: 2px; text-align: left;">
                    </div>`;
            const priceHost = itemElem.querySelector(
              '[class*="Item_item"][class*="Item_clickable"]'
            ) ?? itemElem;
            priceHost.insertAdjacentHTML("beforeend", priceElemHTML);
          }
          const priceElem = itemElem.querySelector("#script_stack_price");
          return { itemElem, originalIndex, priceElem, values };
        });
        if (order === "none") {
          for (const { itemElem, priceElem } of sortableItems) {
            itemElem.style.order = 0;
            priceElem.textContent = "";
          }
          continue;
        }
        sortableItems.sort(
          (left, right) => right.values[order] - left.values[order] || left.originalIndex - right.originalIndex
        );
        for (const [
          rank,
          { itemElem, priceElem, values }
        ] of sortableItems.entries()) {
          itemElem.style.order = rank;
          priceElem.textContent = runtime.api.numberFormatter(values[order]);
        }
      }
    };
    const controls = invElem.parentElement?.querySelector(
      "#script_inv_sort_controls"
    );
    if (controls) controls.mwitoolsSortItemsBy = sortItemsBy;
    if (showSort) {
      invElem.parentElement.querySelector("button#script_sortByFair_btn")?.addEventListener("click", () => sortItemsBy("fair"));
      invElem.parentElement.querySelector("button#script_sortByAsk_btn")?.addEventListener("click", () => sortItemsBy("ask"));
      invElem.parentElement.querySelector("button#script_sortByBid_btn")?.addEventListener("click", () => sortItemsBy("bid"));
      invElem.parentElement.querySelector("button#script_sortByNone_btn")?.addEventListener("click", () => sortItemsBy("none"));
    }
    if (showWorth) {
      invElem.parentElement.querySelector("button#script_refresh_inventory_btn")?.addEventListener("click", async (event) => {
        const button = event.currentTarget;
        const order = controls?.dataset.sortOrder ?? "none";
        button.disabled = true;
        button.textContent = runtime.config.isZH ? "刷新中…" : "Refreshing…";
        try {
          await calculateNetworth({ force: true });
          controls?.mwitoolsSortItemsBy?.(order);
        } finally {
          button.disabled = false;
          button.textContent = runtime.config.isZH ? "刷新价值" : "Refresh values";
        }
      });
    }
  }
  async function addGuildCreditConversionsSortButton() {
    const selectorContainer = document.querySelector(".ItemSelector_menu__12sEM");
    if (!selectorContainer) {
      return;
    }
    if (selectorContainer.querySelector("#script_itemSelector_sort_div")) {
      return;
    }
    const price_data = await runtime.api.fetchMarketJSON();
    if (!price_data || !price_data.marketData) {
      return;
    }
    const bestCreditConversionMap = {};
    for (const itemHrid in runtime.state.initData_itemDetailMap) {
      if (runtime.state.initData_itemDetailMap[itemHrid]?.guildCreditConversions) {
        const conversions = runtime.state.initData_itemDetailMap[itemHrid].guildCreditConversions;
        for (const conversion of conversions) {
          const creditHrid = conversion.creditItemHrid;
          let askPrice = 0;
          if (price_data.marketData[itemHrid] && price_data.marketData[itemHrid][0])
            askPrice = price_data.marketData[itemHrid][0].a;
          let bidPrice = 0;
          if (price_data.marketData[itemHrid] && price_data.marketData[itemHrid][0])
            bidPrice = price_data.marketData[itemHrid][0].b;
          if (askPrice === 0 && bidPrice === 0) continue;
          const creditAskPrice = askPrice * conversion.itemCount / conversion.creditCount;
          const creditBidPrice = bidPrice * conversion.itemCount / conversion.creditCount;
          const enName = runtime.state.initData_itemDetailMap[itemHrid].name;
          const displayName = getLocalizedEntityName("item", itemHrid, {
            fallback: enName
          });
          if (!bestCreditConversionMap[creditHrid]) {
            bestCreditConversionMap[creditHrid] = { ask: null, bid: null };
          }
          if (askPrice > 0 && (!bestCreditConversionMap[creditHrid].ask || creditAskPrice < bestCreditConversionMap[creditHrid].ask.price)) {
            bestCreditConversionMap[creditHrid].ask = {
              name: displayName,
              price: creditAskPrice
            };
          }
          if (bidPrice > 0 && (!bestCreditConversionMap[creditHrid].bid || creditBidPrice < bestCreditConversionMap[creditHrid].bid.price)) {
            bestCreditConversionMap[creditHrid].bid = {
              name: displayName,
              price: creditBidPrice
            };
          }
        }
      }
    }
    const inputContainer = selectorContainer.querySelector(
      ".Input_inputContainer__22GnD"
    );
    if (!inputContainer) {
      return;
    }
    const askButton = `<button
        id="script_itemSelector_sortByAsk_btn"
        style="border-radius: 3px; background-color: ${runtime.config.SCRIPT_COLOR_MAIN}; color: black; font-size: 0.875rem; padding: 2px 6px;">
        ${runtime.config.isZH ? "出售价" : "Ask"}
        </button>`;
    const bidButton = `<button
        id="script_itemSelector_sortByBid_btn"
        style="border-radius: 3px; background-color: ${runtime.config.SCRIPT_COLOR_MAIN}; color: black; font-size: 0.875rem; padding: 2px 6px;">
        ${runtime.config.isZH ? "收购价" : "Bid"}
        </button>`;
    const noneButton = `<button
        id="script_itemSelector_sortByNone_btn"
        style="border-radius: 3px; background-color: ${runtime.config.SCRIPT_COLOR_MAIN}; color: black; font-size: 0.875rem; padding: 2px 6px;">
        ${runtime.config.isZH ? "无" : "None"}
        </button>`;
    const buttonsDiv = `<div id="script_itemSelector_sort_div" style="color: ${runtime.config.SCRIPT_COLOR_MAIN}; font-size: 0.875rem; text-align: left; margin-left: 8px; display: inline;">${runtime.config.isZH ? "排序:" : "Sort: "}${askButton} ${bidButton} ${noneButton}</div>`;
    inputContainer.insertAdjacentHTML("afterend", buttonsDiv);
    const itemList = selectorContainer.querySelector(
      ".ItemSelector_itemList__Qa5lq"
    );
    if (!itemList) {
      return;
    }
    const sortItemsBy = (order) => {
      const itemContainers = itemList.querySelectorAll(
        ".ItemSelector_itemContainer__3olqe"
      );
      let targetCreditHrid = "";
      let targetCreditName = "";
      const exchangeModal = document.querySelector(
        ".GuildPanel_exchangeModalContent__aQqyL"
      );
      if (exchangeModal) {
        const creditIcon = exchangeModal.querySelector(
          ".GuildPanel_arrow__1v2a0 + .Item_itemContainer__x7kH1 svg"
        );
        if (creditIcon) {
          const creditAriaLabel = creditIcon.attributes["aria-label"]?.value;
          if (creditAriaLabel) {
            targetCreditHrid = resolveEntityFromElement("item", creditIcon);
            targetCreditName = creditAriaLabel;
          }
        }
      }
      const priceList = [];
      itemContainers.forEach((itemContainer) => {
        const itemElem = itemContainer.querySelector(
          ".Item_itemContainer__x7kH1"
        );
        if (!itemElem) return;
        const itemName4 = itemElem.querySelector("svg")?.attributes["aria-label"]?.value;
        if (!itemName4) {
          itemElem.style.order = 0;
          const priceElem2 = itemElem.querySelector("#script_itemSelector_price");
          if (priceElem2) priceElem2.remove();
          return;
        }
        const itemHrid = resolveEntityFromElement("item", itemElem);
        let itemCount = itemElem.querySelector(".Item_count__1HVvv")?.innerText;
        if (!itemCount) {
          itemElem.style.order = 0;
          const priceElem2 = itemElem.querySelector("#script_itemSelector_price");
          if (priceElem2) priceElem2.remove();
          return;
        }
        itemCount = runtime.api.parseCompactNumber(itemCount);
        let askPrice = 0;
        if (price_data.marketData[itemHrid] && price_data.marketData[itemHrid][0])
          askPrice = price_data.marketData[itemHrid][0].a;
        let bidPrice = 0;
        if (price_data.marketData[itemHrid] && price_data.marketData[itemHrid][0])
          bidPrice = price_data.marketData[itemHrid][0].b;
        let creditValue = 0;
        let creditAskPrice = 0;
        let creditBidPrice = 0;
        if (targetCreditHrid && runtime.state.initData_itemDetailMap[itemHrid]?.guildCreditConversions) {
          const conversions = runtime.state.initData_itemDetailMap[itemHrid].guildCreditConversions;
          const matchedConversion = conversions.find(
            (c) => c.creditItemHrid === targetCreditHrid
          );
          if (matchedConversion) {
            creditValue = itemCount / matchedConversion.itemCount * matchedConversion.creditCount;
            creditAskPrice = askPrice * itemCount / creditValue;
            creditBidPrice = bidPrice * itemCount / creditValue;
          }
        }
        if (targetCreditHrid && creditAskPrice > 0) {
          priceList.push({
            name: itemName4,
            ask: creditAskPrice,
            bid: creditBidPrice
          });
        }
        if (!itemElem.querySelector("#script_itemSelector_price")) {
          itemElem.style.position = "relative";
          const priceElemHTML = `<div
                    id="script_itemSelector_price"
                    style="z-index: 1; position: absolute; top: 2px; left: 2px; text-align: left; font-size: 10px;">
                </div>`;
          itemElem.querySelector(".Item_item__2De2O.Item_clickable__3viV6").insertAdjacentHTML("beforeend", priceElemHTML);
        }
        const priceElem = itemElem.querySelector("#script_itemSelector_price");
        if (!itemElem.querySelector("#script_itemSelector_credit")) {
          const creditElemHTML = `<div
                    id="script_itemSelector_credit"
                    style="z-index: 1; position: absolute; bottom: 2px; left: 2px; text-align: left; font-size: 10px;">
                </div>`;
          itemElem.querySelector(".Item_item__2De2O.Item_clickable__3viV6").insertAdjacentHTML("beforeend", creditElemHTML);
        }
        const creditElem = itemElem.querySelector("#script_itemSelector_credit");
        if (order === "ask") {
          const sortValue = creditAskPrice > 0 ? creditAskPrice : askPrice * itemCount;
          itemContainer.style.order = Math.round(sortValue);
          priceElem.textContent = runtime.api.numberFormatter(
            creditValue > 0 ? creditValue : askPrice * itemCount
          );
          creditElem.textContent = runtime.api.numberFormatter(sortValue);
        } else if (order === "bid") {
          const sortValue = creditBidPrice > 0 ? creditBidPrice : bidPrice * itemCount;
          itemContainer.style.order = Math.round(sortValue);
          priceElem.textContent = runtime.api.numberFormatter(
            creditValue > 0 ? creditValue : bidPrice * itemCount
          );
          creditElem.textContent = runtime.api.numberFormatter(sortValue);
        } else if (order === "none") {
          itemContainer.style.order = 0;
          priceElem.textContent = "";
          creditElem.textContent = "";
        }
      });
      const bestItemSpan = selectorContainer.querySelector("#script_best_item");
      if (order !== "none" && targetCreditHrid && bestCreditConversionMap[targetCreditHrid]) {
        const best = bestCreditConversionMap[targetCreditHrid][order];
        if (best) {
          if (bestItemSpan) {
            bestItemSpan.textContent = `${best.name} ${runtime.api.numberFormatter(best.price)}`;
          } else {
            const span = `<span id="script_best_item" style="color: ${runtime.config.SCRIPT_COLOR_MAIN}; font-size: 0.875rem; margin-left: 8px;">${best.name} ${runtime.api.numberFormatter(best.price)}</span>`;
            selectorContainer.querySelector("#script_itemSelector_sort_div").insertAdjacentHTML("beforeend", span);
          }
        } else if (bestItemSpan) {
          bestItemSpan.remove();
        }
      } else if (bestItemSpan) {
        bestItemSpan.remove();
      }
    };
    selectorContainer.querySelector("button#script_itemSelector_sortByAsk_btn").addEventListener("click", function(e) {
      sortItemsBy("ask");
    });
    selectorContainer.querySelector("button#script_itemSelector_sortByBid_btn").addEventListener("click", function(e) {
      sortItemsBy("bid");
    });
    selectorContainer.querySelector("button#script_itemSelector_sortByNone_btn").addEventListener("click", function(e) {
      sortItemsBy("none");
    });
  }
  Object.assign(runtime.api, {
    calculateNetworth,
    calculateInventoryCategoryValues,
    scheduleNetworthRefresh,
    addInventoryCategoryValues,
    getInventorySortUnitValue,
    getInventoryItemEnhancementLevel,
    inventoryTodayProfitHtml,
    isSortableInventoryCategory,
    addInvSortButton,
    addGuildCreditConversionsSortButton
  });

  // src/features/guild-credit-advisor.js
  var CARD_ID = "mwitools-guild-credit-advisor";
  var LEGACY_STYLE_ID = "mwitools-guild-credit-advisor-style";
  var LEGACY_SUMMARY_CLASS = "mwi-guild-credit-recommendation";
  var MODAL_SELECTOR = '[class*="GuildPanel_exchangeModalContent"]';
  var ITEM_SELECTOR = '[class*="ItemSelector_menu"]';
  var VIEWPORT_MARGIN = 12;
  var PANEL_GAP = 12;
  var CREDIT_COLORS = {
    green: "#43c4ad",
    brown: "#b8885b",
    white: "#dfe4f2",
    blue: "#6ea9ff",
    purple: "#a980e9",
    red: "#e65d68",
    silver: "#aeb9c9",
    gold: "#d8a33c"
  };
  var advisorPositionState = null;
  var advisorPositionFrame = null;
  function t6(zh, en) {
    return localize(zh, en);
  }
  function escapeHtml3(value) {
    return String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
  }
  function positiveNumber3(value) {
    const number3 = Number(value);
    return Number.isFinite(number3) && number3 > 0 ? number3 : 0;
  }
  function formatNumber2(value, digits = 2) {
    if (!Number.isFinite(Number(value))) return "—";
    return runtime.api.numberFormatter?.(Number(value), digits) ?? String(value);
  }
  function formatExact(value) {
    if (!Number.isFinite(Number(value))) return "—";
    return runtime.api.formatExactNumber?.(Number(value)) ?? String(value);
  }
  function isVisible(element) {
    if (!element?.isConnected) return false;
    for (let current = element; current; current = current.parentElement) {
      if (current.hidden || current.getAttribute?.("aria-hidden") === "true") {
        return false;
      }
      const style = current.ownerDocument?.defaultView?.getComputedStyle?.(current);
      if (style?.display === "none" || style?.visibility === "hidden") {
        return false;
      }
    }
    return true;
  }
  function findGuildExchangeModal(documentRef = document) {
    return [...documentRef.querySelectorAll(MODAL_SELECTOR)].filter(isVisible).at(-1);
  }
  function findVisibleItemSelector(documentRef = document) {
    return [...documentRef.querySelectorAll(ITEM_SELECTOR)].filter(isVisible).at(-1);
  }
  function itemHridFromIcon(icon) {
    return resolveEntityFromElement("item", icon);
  }
  function guildCreditHrids() {
    const result = /* @__PURE__ */ new Set();
    for (const detail of Object.values(
      runtime.state.initData_itemDetailMap ?? {}
    )) {
      for (const conversion of detail?.guildCreditConversions ?? []) {
        if (conversion?.creditItemHrid) result.add(conversion.creditItemHrid);
      }
    }
    return result;
  }
  function readGuildExchangeContext(modal) {
    const creditHrids = guildCreditHrids();
    const iconHrids = [...modal.querySelectorAll("svg[aria-label]")].map(itemHridFromIcon).filter(Boolean);
    const creditItemHrid = iconHrids.find(
      (itemHrid) => creditHrids.has(itemHrid)
    );
    const selectedItemHrid = iconHrids.find(
      (itemHrid) => itemHrid !== creditItemHrid && runtime.state.initData_itemDetailMap?.[itemHrid]?.guildCreditConversions?.some(
        (conversion) => conversion.creditItemHrid === creditItemHrid
      )
    );
    const batchInput = modal.querySelector('input[type="number"],input');
    return {
      creditItemHrid,
      selectedItemHrid,
      batchCount: Math.max(1, Math.floor(positiveNumber3(batchInput?.value) || 1))
    };
  }
  function collectGuildCreditConversions(creditItemHrid) {
    const result = [];
    for (const [fallbackHrid, detail] of Object.entries(
      runtime.state.initData_itemDetailMap ?? {}
    )) {
      const itemHrid = detail?.hrid ?? detail?.itemHrid ?? fallbackHrid;
      for (const conversion of detail?.guildCreditConversions ?? []) {
        if (conversion?.creditItemHrid !== creditItemHrid) continue;
        const itemCount = positiveNumber3(conversion.itemCount);
        const creditCount = positiveNumber3(conversion.creditCount);
        if (!itemCount || !creditCount) continue;
        result.push({ itemHrid, itemCount, creditCount, detail });
      }
    }
    return result;
  }
  function normalizeOrder(order) {
    if (Array.isArray(order)) {
      return {
        price: positiveNumber3(order[0]),
        quantity: positiveNumber3(order[1])
      };
    }
    const quantity = positiveNumber3(
      order?.quantity ?? order?.count ?? positiveNumber3(order?.orderQuantity) - positiveNumber3(order?.filledQuantity)
    );
    return { price: positiveNumber3(order?.price), quantity };
  }
  function marketLevelBook(itemHrid) {
    return runtime.state.marketOrderBooks?.[itemHrid]?.[0] ?? runtime.state.marketOrderBooks?.[itemHrid]?.["0"] ?? null;
  }
  function quoteOrderBook(itemHrid, quantity, side) {
    const required = positiveNumber3(quantity);
    if (!required) {
      return { available: false, totalValue: 0, estimated: false };
    }
    const levelBook = marketLevelBook(itemHrid);
    const orders = levelBook?.[side];
    if (levelBook) {
      if (!Array.isArray(orders)) {
        return { available: false, totalValue: 0, estimated: false };
      }
      let remaining = required;
      let totalValue = 0;
      const descending = side === "bids";
      const normalized = orders.map(normalizeOrder).filter(({ price, quantity: available }) => price > 0 && available > 0).sort(
        (left, right) => descending ? right.price - left.price : left.price - right.price
      );
      for (const order of normalized) {
        const filled = Math.min(remaining, order.quantity);
        totalValue += filled * order.price;
        remaining -= filled;
        if (remaining <= 0) break;
      }
      return remaining <= 0 ? { available: true, totalValue, estimated: false } : { available: false, totalValue: 0, estimated: false };
    }
    const fallbackPrice = positiveNumber3(
      side === "bids" ? runtime.api.getBidPrice?.(itemHrid, 0) : runtime.api.getAskPrice?.(itemHrid, 0)
    );
    return fallbackPrice ? {
      available: true,
      totalValue: fallbackPrice * required,
      estimated: true
    } : { available: false, totalValue: 0, estimated: true };
  }
  function quoteGuildCreditAsk(itemHrid, requiredItems) {
    const quote = quoteOrderBook(itemHrid, requiredItems, "asks");
    return {
      available: quote.available,
      totalCost: quote.totalValue,
      estimated: quote.estimated
    };
  }
  function quoteGuildCreditBid(itemHrid, quantity) {
    const quote = quoteOrderBook(itemHrid, quantity, "bids");
    return {
      available: quote.available,
      grossValue: quote.totalValue,
      estimated: quote.estimated
    };
  }
  function evaluateGuildCreditConversion(conversion, targetCredits = 1) {
    const batches = Math.max(
      1,
      Math.ceil(positiveNumber3(targetCredits) / conversion.creditCount)
    );
    const requiredItems = batches * conversion.itemCount;
    const producedCredits = batches * conversion.creditCount;
    const quote = quoteGuildCreditAsk(conversion.itemHrid, requiredItems);
    return {
      ...conversion,
      ...quote,
      batches,
      requiredItems,
      producedCredits,
      costPerCredit: quote.available ? quote.totalCost / producedCredits : Number.POSITIVE_INFINITY
    };
  }
  function evaluateGuildCreditOptions(creditItemHrid, targetCredits = 1) {
    return collectGuildCreditConversions(creditItemHrid).map(
      (conversion) => evaluateGuildCreditConversion(conversion, targetCredits)
    ).filter(({ available }) => available).sort(
      (left, right) => left.costPerCredit - right.costPerCredit || left.totalCost - right.totalCost || itemName2(left.itemHrid).localeCompare(itemName2(right.itemHrid))
    );
  }
  function affordableItemsFromAsks(itemHrid, budget) {
    const availableBudget = positiveNumber3(budget);
    if (!availableBudget) return { quantity: 0, estimated: false };
    const levelBook = marketLevelBook(itemHrid);
    if (levelBook) {
      if (!Array.isArray(levelBook.asks)) {
        return { quantity: 0, estimated: false };
      }
      let remainingBudget = availableBudget;
      let quantity = 0;
      const asks = levelBook.asks.map(normalizeOrder).filter(({ price, quantity: available }) => price > 0 && available > 0).sort((left, right) => left.price - right.price);
      for (const ask of asks) {
        const affordable = Math.floor(remainingBudget / ask.price);
        const purchased = Math.min(ask.quantity, affordable);
        if (purchased <= 0) break;
        quantity += purchased;
        remainingBudget -= purchased * ask.price;
      }
      return { quantity, estimated: false };
    }
    const askPrice = positiveNumber3(runtime.api.getAskPrice?.(itemHrid, 0));
    return {
      quantity: askPrice ? Math.floor(availableBudget / askPrice) : 0,
      estimated: Boolean(askPrice)
    };
  }
  function buyGuildCreditsWithinBudget(conversion, budget) {
    const affordable = affordableItemsFromAsks(conversion.itemHrid, budget);
    const batches = Math.floor(affordable.quantity / conversion.itemCount);
    if (batches <= 0) {
      return {
        available: false,
        estimated: affordable.estimated,
        batches: 0,
        requiredItems: 0,
        producedCredits: 0,
        totalCost: 0
      };
    }
    const requiredItems = batches * conversion.itemCount;
    const quote = quoteGuildCreditAsk(conversion.itemHrid, requiredItems);
    return {
      available: quote.available && quote.totalCost <= budget,
      estimated: affordable.estimated || quote.estimated,
      batches,
      requiredItems,
      producedCredits: batches * conversion.creditCount,
      totalCost: quote.totalCost
    };
  }
  function evaluateGuildCreditReplacement(selectedConversion, batchCount, bestConversion) {
    const selectedBatches = Math.max(1, Math.floor(positiveNumber3(batchCount)));
    const directCredits = selectedBatches * selectedConversion.creditCount;
    if (selectedConversion.itemHrid === bestConversion.itemHrid) {
      return { status: "already_optimal", directCredits, difference: 0 };
    }
    const saleQuantity = selectedBatches * selectedConversion.itemCount;
    const sale = quoteGuildCreditBid(selectedConversion.itemHrid, saleQuantity);
    if (!sale.available) {
      return { status: "no_sell_quote", directCredits, saleQuantity, sale };
    }
    const taxRate = Math.max(
      0,
      Math.min(
        1,
        Number(runtime.api.getMarketTaxRate?.(selectedConversion.itemHrid)) || 0
      )
    );
    const netSaleValue = sale.grossValue * (1 - taxRate);
    const replacement = buyGuildCreditsWithinBudget(bestConversion, netSaleValue);
    if (!replacement.available) {
      return {
        status: "unaffordable",
        directCredits,
        saleQuantity,
        taxRate,
        netSaleValue,
        sale,
        replacement
      };
    }
    return {
      status: "ok",
      directCredits,
      saleQuantity,
      taxRate,
      netSaleValue,
      sale,
      replacement,
      difference: replacement.producedCredits - directCredits,
      estimated: sale.estimated || replacement.estimated
    };
  }
  function itemName2(itemHrid) {
    return itemName(itemHrid, { fallback: itemHrid });
  }
  function itemIconMarkup(itemHrid, name) {
    const href = getGameSpriteHref("items", itemHrid);
    if (!href) {
      return `<span class="icon-fallback" aria-label="${escapeHtml3(name)}">?</span>`;
    }
    return `<svg class="item-icon" viewBox="0 0 32 32" role="img" aria-label="${escapeHtml3(name)}"><use href="${escapeHtml3(href)}" xlink:href="${escapeHtml3(href)}"></use></svg>`;
  }
  function creditColor(creditItemHrid) {
    const key = String(creditItemHrid ?? "").split("/").at(-1)?.split("_")[0];
    return CREDIT_COLORS[key] ?? runtime.config.SCRIPT_COLOR_MAIN ?? "#43c4ad";
  }
  function advisorStyles() {
    return `
    :host{position:fixed;z-index:2147483000;display:block;width:min(400px,calc(100vw - 24px));max-height:calc(100dvh - 24px);box-sizing:border-box;color:#f4f5ff;font-family:inherit;font-size:11px;line-height:1.25}
    *{box-sizing:border-box}
    .advisor{display:flex;max-height:inherit;flex-direction:column;overflow:hidden;border:1px solid #414361;border-left:4px solid var(--mwi-credit-accent,#43c4ad);border-radius:8px;background:#171927;box-shadow:0 10px 30px rgba(0,0,0,.48)}
    .head{display:flex;align-items:center;justify-content:space-between;gap:7px;padding:6px 9px;border-bottom:1px solid #414361;background:#24263e}
    .title{display:flex;align-items:center;gap:7px;min-width:0;color:#fff;font-size:13px;font-weight:750;white-space:nowrap}
    .credit{display:flex;align-items:center;gap:4px;min-width:0;overflow:hidden;color:#c7cae4;font-size:9px;font-weight:500;text-overflow:ellipsis;white-space:nowrap}
    .credit::before{width:7px;height:7px;flex:0 0 7px;border-radius:2px;background:var(--mwi-credit-accent,#43c4ad);content:""}
    .basis{color:#aeb1c9;font-size:9px;white-space:nowrap}
    .body{min-height:0;overflow-x:hidden;overflow-y:auto;padding:5px;scrollbar-width:thin}
    .ranking{display:grid;gap:3px}
    .rank-row{display:grid;min-height:34px;grid-template-columns:18px 22px minmax(0,1fr) auto;align-items:center;gap:5px;padding:3px 5px;border:1px solid #3d3f5d;border-radius:5px;background:#202139}
    .rank-row.best{border-color:color-mix(in srgb,var(--mwi-credit-accent,#43c4ad) 72%,#414361);background:color-mix(in srgb,var(--mwi-credit-accent,#43c4ad) 13%,#202139)}
    .rank-row.current-row{margin-top:4px;border-style:dashed;background:#1d1f33}
    .rank{display:grid;width:17px;height:17px;place-items:center;border:1px solid #555976;border-radius:50%;color:#c9cce2;font:700 9px ui-monospace,SFMono-Regular,Menlo,monospace}
    .best .rank{border-color:var(--mwi-credit-accent,#43c4ad);background:var(--mwi-credit-accent,#43c4ad);color:#111827}
    .item-icon,.icon-fallback{display:grid;width:22px;height:22px;place-items:center;overflow:hidden;border-radius:4px;background:#292b45;color:#c7cae4;font-weight:700}
    .copy{display:block;min-width:0}
    .name-line{display:flex;min-width:0;align-items:center;gap:5px}
    .name{min-width:0;overflow:hidden;color:#f5f6ff;font-size:11px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}
    .tag{flex:0 0 auto;padding:1px 4px;border:1px solid color-mix(in srgb,var(--mwi-credit-accent,#43c4ad) 65%,#555976);border-radius:999px;color:var(--mwi-credit-accent,#43c4ad);font-size:8px;font-weight:700}
    .price{display:flex;align-items:baseline;justify-content:flex-end;gap:3px;color:var(--mwi-credit-accent,#43c4ad);font:750 12px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap}
    .price small{color:#aeb1c9;font:500 9px/1.2 inherit}
    .current-heading{margin:4px 2px 3px;color:#aeb1c9;font-size:8px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}
    .summary{margin-top:5px;padding:5px 7px;border-top:1px solid #414361;border-radius:0 0 5px 5px;background:#1d1f31;color:#dfe1f4;font-size:9px;line-height:1.35;text-align:center}
    .summary strong{color:var(--mwi-credit-accent,#43c4ad);font-size:10px}
    .summary.warning strong{color:#ffd17c}
    .estimate{margin-top:3px;color:#9296b0;font-size:8px;text-align:center}
    .empty{padding:14px;color:#bfc2d9;text-align:center}
    @media(max-width:760px){
      :host{width:calc(100vw - 16px);max-height:min(38dvh,300px);font-size:10px}
      .head{padding:5px 7px}
      .title{font-size:12px}
      .basis{font-size:8px}
      .body{padding:4px}
      .rank-row{min-height:32px;grid-template-columns:17px 21px minmax(0,1fr) auto;gap:4px;padding:3px 4px}
      .item-icon,.icon-fallback{width:21px;height:21px}
      .name{font-size:10px}
      .price{font-size:11px}
      .price small{font-size:8px}
      .summary{margin-top:4px;padding:4px 6px}
    }
    @media(max-width:420px){
      :host{width:calc(100vw - 12px);max-height:min(40dvh,280px)}
      .head{gap:5px}
      .title{gap:5px}
      .credit{max-width:92px}
      .rank-row{grid-template-columns:17px 20px minmax(0,1fr) auto}
      .item-icon,.icon-fallback{width:20px;height:20px}
    }
  `;
  }
  function createAdvisorHost() {
    let host = document.getElementById(CARD_ID);
    if (host?.shadowRoot) return host;
    host?.remove();
    host = document.createElement("div");
    host.id = CARD_ID;
    const shadow2 = host.attachShadow({ mode: "open" });
    const style = document.createElement("style");
    style.textContent = advisorStyles();
    const advisor = document.createElement("aside");
    advisor.className = "advisor";
    advisor.setAttribute("role", "complementary");
    advisor.setAttribute("aria-live", "polite");
    shadow2.append(style, advisor);
    return host;
  }
  function rankRowMarkup(option, index, { current = false, separate = false } = {}) {
    const name = itemName2(option.itemHrid);
    const pricePrefix = option.estimated ? "≈" : "";
    return `<div class="rank-row${index === 0 ? " best" : ""}${separate ? " current-row" : ""}">
    <span class="rank">${separate ? "—" : index + 1}</span>
    ${itemIconMarkup(option.itemHrid, name)}
    <span class="copy"><span class="name-line"><span class="name" title="${escapeHtml3(name)}">${escapeHtml3(name)}</span>${current ? `<span class="tag">${escapeHtml3(t6("当前", "Current"))}</span>` : ""}</span></span>
    <span class="price" title="${escapeHtml3(formatExact(option.costPerCredit))}">${pricePrefix}${escapeHtml3(formatNumber2(option.costPerCredit))}<small>${escapeHtml3(t6("每信用点", "per credit"))}</small></span>
  </div>`;
  }
  function replacementSummaryMarkup(result, best) {
    if (!result) {
      return `<div class="summary">${escapeHtml3(t6("选择兑换物品后可比较卖出换购收益。", "Select an exchange item to compare sell-and-rebuy returns."))}</div>`;
    }
    if (result.status === "already_optimal") {
      return `<div class="summary"><strong>${escapeHtml3(t6("当前方案已是单位信用成本最优", "The selected option already has the best unit cost"))}</strong></div>`;
    }
    if (result.status === "no_sell_quote") {
      return `<div class="summary warning"><strong>${escapeHtml3(t6("当前物品没有足够的收购报价", "The selected item has insufficient buy-order depth"))}</strong><br>${escapeHtml3(t6("无法估算卖出换购结果。", "The sell-and-rebuy result cannot be estimated."))}</div>`;
    }
    if (result.status === "unaffordable") {
      return `<div class="summary warning"><strong>${escapeHtml3(t6("税后收入不足以购买一个最优兑换批次", "Net sale proceeds cannot buy one batch of the best option"))}</strong></div>`;
    }
    const name = itemName2(best.itemHrid);
    const difference = Number(result.difference) || 0;
    let conclusion;
    if (difference > 0) {
      conclusion = t6(
        `卖出当前物品并改买${name},可多兑换 ${formatExact(difference)} 点信用。`,
        `Sell the selected items and buy ${name} to gain ${formatExact(difference)} more credits.`
      );
    } else if (difference < 0) {
      conclusion = t6(
        `直接兑换更划算;改买${name}会少 ${formatExact(-difference)} 点信用。`,
        `Direct exchange is better; switching to ${name} yields ${formatExact(-difference)} fewer credits.`
      );
    } else {
      conclusion = t6(
        `直接兑换与改买${name}获得的信用点相同。`,
        `Direct exchange and switching to ${name} yield the same credits.`
      );
    }
    return `<div class="summary"><strong>${escapeHtml3(conclusion)}</strong><br>${escapeHtml3(t6(`税后可用 ${formatNumber2(result.netSaleValue)},可购买 ${formatExact(result.replacement.requiredItems)} 个材料。`, `${formatNumber2(result.netSaleValue)} net proceeds buy ${formatExact(result.replacement.requiredItems)} materials.`))}</div>`;
  }
  function advisorMarkup({
    context,
    ranked,
    selected,
    replacement,
    recommendationCount
  }) {
    const count = Math.min(
      8,
      Math.max(1, Math.floor(Number(recommendationCount)) || 3)
    );
    const top = ranked.slice(0, count);
    const creditName = itemName2(context.creditItemHrid);
    const selectedInTop = top.some(
      ({ itemHrid }) => itemHrid === context.selectedItemHrid
    );
    const ranking = top.map(
      (option, index) => rankRowMarkup(option, index, {
        current: option.itemHrid === context.selectedItemHrid
      })
    ).join("");
    const current = selected && !selectedInTop ? `<div class="current-heading">${escapeHtml3(t6("当前方案", "Selected option"))}</div>${rankRowMarkup(selected, -1, { current: true, separate: true })}` : "";
    const estimated = ranked.some(({ estimated: value }) => value) || replacement?.estimated;
    return `<header class="head"><span class="title">${escapeHtml3(t6("公会信用兑换推荐", "Guild Credit Exchange"))}<span class="credit">${escapeHtml3(creditName)}</span></span><span class="basis">${escapeHtml3(t6("按订单深度", "Order-book depth"))}</span></header>
    <div class="body">${ranking ? `<div class="ranking">${ranking}</div>${current}${replacementSummaryMarkup(replacement, top[0])}${estimated ? `<div class="estimate">${escapeHtml3(t6("带 ≈ 的市场结果使用当前最低报价估算。", "Estimated market results use the current best quote."))}</div>` : ""}` : `<div class="empty">${escapeHtml3(t6("没有具备完整报价的兑换方案。", "No exchange option has a complete market quote."))}</div>`}</div>`;
  }
  function clamp(value, minimum, maximum) {
    return Math.min(Math.max(value, minimum), Math.max(minimum, maximum));
  }
  function findGuildExchangeAnchor(modal) {
    return modal.closest('[class*="Modal_modalContainer"]') ?? modal;
  }
  function viewportSize() {
    return {
      width: Number(globalThis.innerWidth) || document.documentElement.clientWidth,
      height: Number(globalThis.innerHeight) || document.documentElement.clientHeight
    };
  }
  function overlapsRect({ left, top, width, height }, blocker) {
    if (!(blocker?.width > 0) || !(blocker?.height > 0)) return false;
    return !(left + width <= blocker.left || left >= blocker.right || top + height <= blocker.top || top >= blocker.bottom);
  }
  function restoreGuildExchangeAnchor(state = advisorPositionState) {
    if (!state?.anchor || state.originalTranslate === void 0) return;
    if (state.originalTranslate) {
      state.anchor.style.setProperty(
        "translate",
        state.originalTranslate,
        state.originalTranslatePriority
      );
    } else {
      state.anchor.style.removeProperty("translate");
    }
    state.shiftY = 0;
  }
  function shiftGuildExchangeAnchor(state, shiftY) {
    const amount = Math.round(shiftY);
    if (!amount) return;
    state.anchor.style.setProperty("translate", `0 ${amount}px`, "important");
    state.shiftY = amount;
  }
  function keepGuildExchangeAnchorReachable(state, anchorRect, viewport) {
    const shiftY = Math.min(
      0,
      viewport.height - VIEWPORT_MARGIN - anchorRect.bottom
    );
    if (!shiftY) return anchorRect;
    shiftGuildExchangeAnchor(state, shiftY);
    return {
      ...anchorRect,
      top: anchorRect.top + shiftY,
      bottom: anchorRect.bottom + shiftY
    };
  }
  function responsiveAdvisorMaxHeight(viewport) {
    const available = Math.max(72, viewport.height - VIEWPORT_MARGIN * 2);
    if (viewport.width > 760) return available;
    return Math.min(available, 300, Math.max(160, viewport.height * 0.38));
  }
  function positionGuildCreditAdvisor() {
    const state = advisorPositionState;
    if (!state?.host?.isConnected || !state.anchor?.isConnected) return false;
    restoreGuildExchangeAnchor(state);
    let anchorRect = state.anchor.getBoundingClientRect();
    const hostRect = state.host.getBoundingClientRect();
    const viewport = viewportSize();
    anchorRect = keepGuildExchangeAnchorReachable(state, anchorRect, viewport);
    const width = hostRect.width || Math.min(400, viewport.width - 24);
    const responsiveMaxHeight = responsiveAdvisorMaxHeight(viewport);
    const height = Math.min(
      hostRect.height || Math.min(260, viewport.height - 24),
      responsiveMaxHeight
    );
    const selectorRect = findVisibleItemSelector()?.getBoundingClientRect?.();
    const sideTop = clamp(
      anchorRect.top,
      VIEWPORT_MARGIN,
      viewport.height - height - VIEWPORT_MARGIN
    );
    const rightCandidate = {
      left: anchorRect.right + PANEL_GAP,
      top: sideTop,
      width,
      height
    };
    const leftCandidate = {
      left: anchorRect.left - PANEL_GAP - width,
      top: sideTop,
      width,
      height
    };
    const topCandidate = {
      left: clamp(
        anchorRect.left + (anchorRect.width - width) / 2,
        VIEWPORT_MARGIN,
        viewport.width - width - VIEWPORT_MARGIN
      ),
      top: anchorRect.top - PANEL_GAP - height,
      width,
      height
    };
    const fitsRight = rightCandidate.left + width <= viewport.width - VIEWPORT_MARGIN && !overlapsRect(rightCandidate, selectorRect);
    const fitsLeft = leftCandidate.left >= VIEWPORT_MARGIN && !overlapsRect(leftCandidate, selectorRect);
    const fitsTop = topCandidate.top >= VIEWPORT_MARGIN && !overlapsRect(topCandidate, selectorRect);
    const topBoundary = Math.min(
      anchorRect.top,
      selectorRect?.top ?? Number.POSITIVE_INFINITY
    );
    const compressedTopSpace = topBoundary - PANEL_GAP - VIEWPORT_MARGIN;
    const fitsCompressedTop = compressedTopSpace >= 72;
    let placement;
    if (fitsRight) {
      placement = "right";
    } else if (fitsLeft) {
      placement = "left";
    } else if (fitsTop) {
      placement = "top";
    } else if (fitsCompressedTop) {
      placement = "top-compressed";
    } else {
      placement = "overlay";
    }
    let left;
    let top;
    let maxHeight = responsiveMaxHeight;
    if (placement === "right") {
      ({ left, top } = rightCandidate);
    } else if (placement === "left") {
      ({ left, top } = leftCandidate);
    } else if (placement === "top") {
      ({ left, top } = topCandidate);
      maxHeight = Math.min(
        responsiveMaxHeight,
        anchorRect.top - PANEL_GAP - VIEWPORT_MARGIN
      );
    } else if (placement === "top-compressed") {
      left = topCandidate.left;
      maxHeight = Math.min(responsiveMaxHeight, compressedTopSpace);
      top = topBoundary - PANEL_GAP - Math.min(height, maxHeight);
    } else {
      left = topCandidate.left;
      top = VIEWPORT_MARGIN;
    }
    state.host.dataset.placement = placement;
    state.host.style.left = `${Math.round(left)}px`;
    state.host.style.top = `${Math.round(top)}px`;
    state.host.style.maxHeight = `${placement === "top-compressed" ? Math.max(1, Math.round(maxHeight)) : Math.max(72, Math.round(maxHeight))}px`;
    return true;
  }
  function scheduleGuildCreditAdvisorPosition() {
    if (advisorPositionFrame !== null) return;
    const run = () => {
      advisorPositionFrame = null;
      positionGuildCreditAdvisor();
    };
    if (typeof globalThis.requestAnimationFrame === "function") {
      advisorPositionFrame = globalThis.requestAnimationFrame(run);
    } else {
      advisorPositionFrame = setTimeout(run, 0);
    }
  }
  function clearGuildCreditAdvisorPosition() {
    if (advisorPositionFrame !== null) {
      if (typeof globalThis.cancelAnimationFrame === "function") {
        globalThis.cancelAnimationFrame(advisorPositionFrame);
      } else {
        clearTimeout(advisorPositionFrame);
      }
      advisorPositionFrame = null;
    }
    restoreGuildExchangeAnchor();
    advisorPositionState?.resizeObserver?.disconnect();
    advisorPositionState = null;
  }
  function mountGuildCreditAdvisor(host, modal) {
    const anchor = findGuildExchangeAnchor(modal);
    if (host.parentElement !== document.body) document.body.append(host);
    if (advisorPositionState?.host !== host || advisorPositionState?.anchor !== anchor) {
      clearGuildCreditAdvisorPosition();
      const resizeObserver = globalThis.ResizeObserver ? new globalThis.ResizeObserver(scheduleGuildCreditAdvisorPosition) : null;
      resizeObserver?.observe(anchor);
      resizeObserver?.observe(host);
      advisorPositionState = {
        anchor,
        host,
        resizeObserver,
        originalTranslate: anchor.style.getPropertyValue("translate"),
        originalTranslatePriority: anchor.style.getPropertyPriority("translate"),
        shiftY: 0
      };
    }
    positionGuildCreditAdvisor();
  }
  function removeGuildCreditAdvisor() {
    clearGuildCreditAdvisorPosition();
    document.getElementById(CARD_ID)?.remove();
  }
  async function renderGuildCreditAdvisor({ marketReady = false } = {}) {
    const modal = findGuildExchangeModal();
    if (!modal) {
      removeGuildCreditAdvisor();
      return null;
    }
    const context = readGuildExchangeContext(modal);
    if (!context.creditItemHrid) {
      removeGuildCreditAdvisor();
      return null;
    }
    if (!marketReady && !await runtime.api.ensureMarketValueSource?.()) {
      removeGuildCreditAdvisor();
      return null;
    }
    const conversions = collectGuildCreditConversions(context.creditItemHrid);
    const selectedConversion = conversions.find(
      ({ itemHrid }) => itemHrid === context.selectedItemHrid
    );
    const targetCredits = selectedConversion ? selectedConversion.creditCount * context.batchCount : 1;
    const ranked = evaluateGuildCreditOptions(
      context.creditItemHrid,
      targetCredits
    );
    const best = ranked[0];
    const selected = selectedConversion ? evaluateGuildCreditConversion(selectedConversion, targetCredits) : null;
    const replacement = selectedConversion && best ? evaluateGuildCreditReplacement(
      selectedConversion,
      context.batchCount,
      best
    ) : null;
    const host = createAdvisorHost();
    host.style.setProperty(
      "--mwi-credit-accent",
      creditColor(context.creditItemHrid)
    );
    const advisor = host.shadowRoot.querySelector(".advisor");
    advisor.setAttribute(
      "aria-label",
      t6("公会信用兑换推荐", "Guild Credit Exchange")
    );
    advisor.innerHTML = advisorMarkup({
      context,
      ranked,
      selected,
      replacement,
      recommendationCount: runtime.api.getGuildCreditRecommendationCount?.() ?? 3
    });
    mountGuildCreditAdvisor(host, modal);
    return host;
  }
  async function renderGuildCreditRecommendations() {
    if (!findGuildExchangeModal()) {
      cleanup();
      return null;
    }
    if (!await runtime.api.ensureMarketValueSource?.()) {
      cleanup();
      return null;
    }
    const advisor = await renderGuildCreditAdvisor({ marketReady: true });
    return { summaries: [], advisor };
  }
  function cleanup() {
    removeGuildCreditAdvisor();
    document.getElementById(LEGACY_STYLE_ID)?.remove();
    document.querySelectorAll(`.${LEGACY_SUMMARY_CLASS}`).forEach((element) => element.remove());
  }
  runtime.features.register({
    id: "guildCreditConversionsSort",
    setting: "guildCreditConversionsSort",
    scope: "character",
    initialize({ scope }) {
      document.getElementById(LEGACY_STYLE_ID)?.remove();
      document.querySelectorAll(`.${LEGACY_SUMMARY_CLASS}`).forEach((element) => element.remove());
      let frame = null;
      const schedule = () => {
        if (frame !== null) return;
        const run = () => {
          frame = null;
          void renderGuildCreditRecommendations();
        };
        if (typeof globalThis.requestAnimationFrame === "function") {
          frame = globalThis.requestAnimationFrame(run);
        } else {
          frame = setTimeout(run, 0);
        }
      };
      const observer = new MutationObserver((mutations) => {
        const activeModal = findGuildExchangeModal();
        const relevant = mutations.some((mutation) => {
          if (activeModal && (mutation.target === activeModal || activeModal.contains?.(mutation.target) || mutation.target?.contains?.(activeModal))) {
            return true;
          }
          if (mutation.type === "attributes" && (mutation.target?.matches?.(`${MODAL_SELECTOR},${ITEM_SELECTOR}`) || mutation.target?.querySelector?.(
            `${MODAL_SELECTOR},${ITEM_SELECTOR}`
          ))) {
            return true;
          }
          return [...mutation.addedNodes, ...mutation.removedNodes].some(
            (node) => node?.matches?.(MODAL_SELECTOR) || node?.matches?.(ITEM_SELECTOR) || node?.querySelector?.(MODAL_SELECTOR) || node?.querySelector?.(ITEM_SELECTOR)
          );
        });
        if (relevant) schedule();
      });
      scope.observer(observer, document.body, {
        attributes: true,
        attributeFilter: [
          "aria-hidden",
          "aria-label",
          "class",
          "hidden",
          "href",
          "style"
        ],
        childList: true,
        subtree: true
      });
      const scheduleFromModal = (event) => {
        if (event.target?.closest?.(MODAL_SELECTOR)) schedule();
      };
      scope.event(document, "input", scheduleFromModal, true);
      scope.event(document, "change", scheduleFromModal, true);
      scope.event(
        globalThis.window ?? globalThis,
        "resize",
        scheduleGuildCreditAdvisorPosition
      );
      scope.event(
        globalThis.window ?? globalThis,
        "orientationchange",
        scheduleGuildCreditAdvisorPosition
      );
      scope.event(
        globalThis.window ?? globalThis,
        "scroll",
        scheduleGuildCreditAdvisorPosition,
        true
      );
      for (const messageType of [
        "market_item_values_updated",
        "market_item_order_books_updated"
      ]) {
        scope.add(runtime.onMessage(messageType, schedule));
      }
      scope.add(() => {
        if (frame !== null) {
          if (typeof globalThis.cancelAnimationFrame === "function") {
            globalThis.cancelAnimationFrame(frame);
          } else {
            clearTimeout(frame);
          }
        }
        cleanup();
      });
      schedule();
    }
  });
  Object.assign(runtime.api, {
    renderGuildCreditAdvisor,
    renderGuildCreditRecommendations
  });

  // src/features/production-profit-panel.js
  var PANEL_ID3 = "mwitools-production-profit-panel";
  var STYLE_ID7 = "mwitools-production-profit-panel-style";
  var VIEWPORT_MARGIN2 = 12;
  var PANEL_GAP2 = 10;
  var activePanel = null;
  function t7(zh, en) {
    return localize(zh, en);
  }
  function escapeHtml4(value) {
    return String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
  }
  function formatNumber3(value, digits = 1) {
    if (!Number.isFinite(Number(value))) return "—";
    if (Math.abs(Number(value)) >= 1e3) {
      return runtime.api.numberFormatter?.(Number(value), digits) ?? String(value);
    }
    return new Intl.NumberFormat(runtime.config.NUMBER_LOCALE || "en-US", {
      maximumFractionDigits: digits
    }).format(Number(value));
  }
  function formatMoney(value) {
    if (value === null || value === void 0 || value === "") return "—";
    return Number.isFinite(Number(value)) ? formatNumber3(value, 1) : "—";
  }
  function exactNumberTitle(value) {
    if (value === null || value === void 0 || value === "") return "";
    if (!Number.isFinite(Number(value))) return "";
    return escapeHtml4(
      runtime.api.formatExactNumber?.(Number(value)) ?? String(value)
    );
  }
  function numberTitleAttribute(value) {
    const title = exactNumberTitle(value);
    return title ? ` title="${title}"` : "";
  }
  function formatPercent2(value) {
    return `${Number(value || 0) >= 0 ? "+" : ""}${formatNumber3(value, 1)}%`;
  }
  function itemName3(itemHrid) {
    return itemName(itemHrid);
  }
  function actionName2(actionHrid, detail) {
    return actionName(actionHrid, { detail });
  }
  function renderItemIcon(itemHrid, name) {
    const href = getGameSpriteHref("items", itemHrid);
    if (!href) {
      return `<span class="mwi-profit-icon-fallback">${escapeHtml4(
        String(name || "?").trim().charAt(0) || "?"
      )}</span>`;
    }
    return `<svg class="mwi-profit-icon" viewBox="0 0 32 32" aria-label="${escapeHtml4(name)}"><use href="${escapeHtml4(href)}" xlink:href="${escapeHtml4(href)}"></use></svg>`;
  }
  function addStyles5() {
    if (document.getElementById(STYLE_ID7)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID7;
    style.textContent = `
    #${PANEL_ID3} { position:fixed; z-index:2147483000; width:min(760px,calc(100vw - 24px)); max-height:min(78vh,760px); box-sizing:border-box; overflow:auto; pointer-events:none; color:var(--color-text-primary,#f2f2f2); border:1px solid rgba(255,255,255,.16); border-radius:10px; background:linear-gradient(145deg,rgba(35,39,47,.985),rgba(19,22,28,.985)); box-shadow:0 18px 48px rgba(0,0,0,.48),0 2px 8px rgba(0,0,0,.3); font-family:inherit; font-size:calc(12px * var(--mwi-ui-font-scale,1) * var(--mwi-hover-font-scale,1)); line-height:1.35; scrollbar-width:thin; }
    #${PANEL_ID3} * { box-sizing:border-box; }
    .mwi-profit-header { display:flex; align-items:center; gap:10px; padding:12px 14px; border-bottom:1px solid rgba(255,255,255,.1); }
    .mwi-profit-header-icon { display:grid; width:38px; height:38px; flex:0 0 38px; place-items:center; border-radius:8px; background:rgba(255,255,255,.065); }
    .mwi-profit-header-main { min-width:0; }
    .mwi-profit-title { color:#fff; font-size:calc(14px * var(--mwi-hover-font-scale,1)); font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    .mwi-profit-subtitle { margin-top:2px; color:var(--color-text-secondary,#aeb4bf); font-size:calc(11px * var(--mwi-hover-font-scale,1)); }
    .mwi-profit-status { margin-left:auto; padding:3px 8px; border:1px solid currentColor; border-radius:999px; font-size:max(calc(.6875rem * var(--mwi-hover-font-scale,1)),calc(10px * var(--mwi-hover-font-scale,1))); font-weight:650; white-space:nowrap; }
    .mwi-profit-status.complete { color:#7bd69a; background:rgba(66,185,108,.1); }
    .mwi-profit-status.partial { color:#e7bd68; background:rgba(221,164,51,.1); }
    .mwi-profit-status.incomplete { color:#ef8c86; background:rgba(218,73,65,.1); }
    .mwi-profit-status.waiting { color:#9fb8df; background:rgba(74,119,187,.1); }
    .mwi-profit-body { display:grid; grid-template-columns:minmax(0,1fr) 138px minmax(0,1fr); gap:10px; padding:12px; align-items:stretch; }
    .mwi-profit-card { min-width:0; padding:10px; border:1px solid rgba(255,255,255,.095); border-radius:8px; background:rgba(255,255,255,.035); }
    .mwi-profit-card.cost { border-top:2px solid rgba(239,124,111,.72); }
    .mwi-profit-card.income { border-top:2px solid rgba(83,201,132,.72); }
    .mwi-profit-card-title { display:flex; align-items:center; justify-content:space-between; margin-bottom:7px; color:#fff; font-size:calc(11px * var(--mwi-hover-font-scale,1)); font-weight:700; letter-spacing:.04em; }
    .mwi-profit-card-total { color:var(--color-text-secondary,#aeb4bf); font-size:calc(10px * var(--mwi-hover-font-scale,1)); font-weight:600; }
    .mwi-profit-item { display:grid; grid-template-columns:28px minmax(0,1fr) auto; gap:7px; align-items:center; padding:7px 0; border-top:1px solid rgba(255,255,255,.065); }
    .mwi-profit-item:first-of-type { border-top:0; }
    .mwi-profit-item-name { min-width:0; color:#edf0f4; font-size:calc(11px * var(--mwi-hover-font-scale,1)); font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    .mwi-profit-item-meta { margin-top:2px; color:var(--color-text-secondary,#9ba2ad); font-size:calc(9.5px * var(--mwi-hover-font-scale,1)); }
    .mwi-profit-item-value { min-width:64px; text-align:right; }
    .mwi-profit-item-value strong { display:block; color:#fff; font-size:calc(11px * var(--mwi-hover-font-scale,1)); }
    .mwi-profit-item-value span { display:block; margin-top:2px; color:var(--color-text-secondary,#9ba2ad); font-size:calc(9.5px * var(--mwi-hover-font-scale,1)); }
    .mwi-profit-kind { display:inline-block; margin-right:4px; padding:0 4px; border-radius:3px; background:rgba(255,255,255,.075); color:#bdc5d1; font-size:calc(8.5px * var(--mwi-hover-font-scale,1)); }
    .mwi-profit-player { display:flex; min-width:0; flex-direction:column; align-items:center; justify-content:center; gap:7px; padding:9px 7px; border:1px solid rgba(255,255,255,.095); border-radius:8px; background:rgba(255,255,255,.025); text-align:center; }
    .mwi-profit-player-title { color:#fff; font-size:calc(11px * var(--mwi-hover-font-scale,1)); font-weight:700; }
    .mwi-profit-teas { display:flex; min-height:28px; align-items:center; justify-content:center; gap:4px; }
    .mwi-profit-tea { display:grid; width:27px; height:27px; place-items:center; border-radius:6px; background:rgba(255,255,255,.07); }
    .mwi-profit-no-tea { color:var(--color-text-secondary,#9ba2ad); font-size:calc(9.5px * var(--mwi-hover-font-scale,1)); }
    .mwi-profit-effects { display:flex; flex-wrap:wrap; justify-content:center; gap:3px; }
    .mwi-profit-effect { padding:2px 5px; border-radius:999px; color:#d5dbe4; background:rgba(255,255,255,.07); font-size:calc(9px * var(--mwi-hover-font-scale,1)); }
    .mwi-profit-flow { color:var(--color-primary,#70a8ff); font-size:calc(24px * var(--mwi-hover-font-scale,1)); line-height:1; }
    .mwi-profit-stat-list { width:100%; }
    .mwi-profit-stat { display:flex; justify-content:space-between; gap:6px; padding:3px 0; border-top:1px solid rgba(255,255,255,.055); color:var(--color-text-secondary,#a4abb6); font-size:calc(9.5px * var(--mwi-hover-font-scale,1)); }
    .mwi-profit-stat strong { color:#edf0f4; font-weight:650; }
    .mwi-profit-valuations { display:flex; flex-direction:column; gap:4px; padding:0 12px 10px; }
    .mwi-profit-valuation-row { display:grid; grid-template-columns:126px repeat(6,minmax(0,1fr)); min-width:0; overflow:hidden; border:1px solid rgba(255,255,255,.1); border-left:3px solid var(--mwi-valuation-color); border-radius:7px; background:rgba(255,255,255,.03); }
    .mwi-profit-valuation-row[data-mode="fair"] { --mwi-valuation-color:#74a9ef; }
    .mwi-profit-valuation-row[data-mode="conservative"] { --mwi-valuation-color:#e1b65d; }
    .mwi-profit-valuation-row[data-mode="aggressive"] { --mwi-valuation-color:#68c98e; }
    .mwi-profit-valuation-row.mwi-loot-valuation-row { grid-template-columns:126px repeat(3,minmax(0,1fr)); }
    .mwi-profit-valuation-row.mwi-loot-valuation-row.has-entry-key { grid-template-columns:126px repeat(4,minmax(0,1fr)); }
    .mwi-profit-valuation-row.incomplete { opacity:.72; }
    .mwi-profit-valuation-name { display:flex; min-width:0; flex-direction:column; justify-content:center; gap:1px; padding:5px 8px; border-right:1px solid rgba(255,255,255,.08); }
    .mwi-profit-valuation-title { color:#fff; font-size:calc(10.5px * var(--mwi-hover-font-scale,1)); font-weight:750; line-height:1.2; }
    .mwi-profit-valuation-state { color:var(--mwi-valuation-color); font-size:calc(.6875rem * var(--mwi-hover-font-scale,1)); line-height:1.15; }
    .mwi-profit-valuation-metric { min-width:0; padding:5px 4px; border-left:1px solid rgba(255,255,255,.055); text-align:center; }
    .mwi-profit-valuation-name + .mwi-profit-valuation-metric { border-left:0; }
    .mwi-profit-valuation-label { min-height:2.2em; color:var(--color-text-secondary,#9da5b0); font-size:calc(.6875rem * var(--mwi-hover-font-scale,1)); line-height:1.1; }
    .mwi-profit-valuation-value { margin-top:2px; color:#fff; font-size:calc(10.5px * var(--mwi-hover-font-scale,1)); font-weight:700; overflow-wrap:anywhere; }
    .mwi-profit-valuation-metric.profit { background:rgba(55,160,97,.075); }
    .mwi-profit-valuation-metric.profit .mwi-profit-valuation-value { color:#82dfa4; }
    .mwi-profit-warning { margin:0 12px 12px; padding:8px 10px; border:1px solid rgba(224,177,75,.25); border-radius:7px; background:rgba(195,139,30,.09); color:#e3c276; font-size:calc(10px * var(--mwi-hover-font-scale,1)); }
    .mwi-profit-hint { margin:0 12px 12px; color:var(--color-text-secondary,#8b93a0); font-size:calc(9.5px * var(--mwi-hover-font-scale,1)); line-height:1.35; }
    #${PANEL_ID3}.mwi-profit-pinned { pointer-events:auto; }
    .mwi-profit-close { flex:0 0 auto; width:22px; height:22px; margin-left:auto; padding:0; border:1px solid rgba(255,255,255,.16); border-radius:5px; background:rgba(255,255,255,.06); color:#e7e9ef; font-size:calc(14px * var(--mwi-hover-font-scale,1)); line-height:1; cursor:pointer; }
    .mwi-profit-close:hover { background:rgba(255,255,255,.14); }
    .mwi-loot-controls { display:grid; grid-template-columns:minmax(0,1fr); gap:8px; margin:10px 12px 0; padding:8px 10px; border:1px solid rgba(255,255,255,.08); border-radius:7px; background:rgba(0,0,0,.12); }
    .mwi-loot-controls.has-key { grid-template-columns:repeat(3,minmax(0,1fr)); }
    .mwi-loot-control { display:grid; min-width:0; min-height:38px; grid-template-columns:minmax(0,1fr) 36px; grid-template-rows:auto 20px; align-items:center; gap:2px 7px; padding:0 7px; border-left:1px solid rgba(255,255,255,.07); }
    .mwi-loot-control:first-child { padding-left:0; border-left:0; }
    .mwi-loot-control-label { min-width:0; grid-column:1 / 3; color:#edf0f4; font-size:calc(10.5px * var(--mwi-hover-font-scale,1)); font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    .mwi-loot-control-state { min-width:0; color:var(--color-text-secondary,#aeb4bf); font-size:calc(9.5px * var(--mwi-hover-font-scale,1)); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    .mwi-loot-switch { position:relative; width:36px; height:20px; flex:0 0 36px; }
    .mwi-loot-switch input { position:absolute; opacity:0; pointer-events:none; }
    .mwi-loot-switch span { position:absolute; inset:0; border-radius:999px; cursor:pointer; background:#555; transition:.16s; }
    .mwi-loot-switch span::after { content:""; position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:50%; background:#fff; transition:.16s; }
    .mwi-loot-switch input:checked + span { background:var(--color-primary,#70a8ff); }
    .mwi-loot-switch input:checked + span::after { transform:translateX(16px); }
    .mwi-loot-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(112px,1fr)); gap:6px; margin-top:8px; }
    .mwi-loot-cell { position:relative; display:flex; min-width:0; align-items:center; gap:6px; padding:5px 7px; border:1px solid rgba(255,255,255,.08); border-radius:6px; background:rgba(255,255,255,.03); }
    .mwi-loot-cell.unpriced { opacity:.6; }
    .mwi-loot-cell.best-redemption { border-color:rgba(255,193,74,.86); background:linear-gradient(135deg,rgba(255,185,55,.16),rgba(255,255,255,.035)); box-shadow:0 0 0 1px rgba(255,184,55,.12),0 0 14px rgba(255,171,42,.12); }
    .mwi-loot-best-badge { position:absolute; top:-6px; right:-4px; z-index:1; padding:1px 5px; border:1px solid rgba(255,211,112,.72); border-radius:999px; background:#7b5410; color:#fff3cf; font-size:calc(8px * var(--mwi-hover-font-scale,1)); font-weight:750; line-height:1.35; white-space:nowrap; }
    .mwi-loot-cell-icon { position:relative; flex:0 0 26px; width:26px; height:26px; }
    .mwi-loot-cell-icon .mwi-profit-icon,.mwi-loot-cell-icon .mwi-profit-icon-fallback { width:26px; height:26px; }
    .mwi-loot-cell-chance { position:absolute; right:-3px; bottom:-3px; padding:0 3px; border-radius:6px; background:rgba(15,18,28,.92); color:#cbd3f4; font-size:calc(8px * var(--mwi-hover-font-scale,1)); line-height:1.3; box-shadow:0 0 0 1px rgba(255,255,255,.1); }
    .mwi-loot-cell-main { min-width:0; }
    .mwi-loot-cell-name { overflow:hidden; color:#edf0f4; font-size:calc(10.5px * var(--mwi-hover-font-scale,1)); font-weight:600; text-overflow:ellipsis; white-space:nowrap; }
    .mwi-loot-cell-value { margin-top:1px; color:#82dfa4; font-size:calc(10px * var(--mwi-hover-font-scale,1)); font-weight:650; }
    .mwi-loot-cell.unpriced .mwi-loot-cell-value { color:var(--color-text-secondary,#9ba2ad); }
    .mwi-profit-state { margin:12px; padding:18px; border:1px solid rgba(255,255,255,.09); border-radius:8px; background:rgba(255,255,255,.03); color:var(--color-text-secondary,#acb3be); text-align:center; }
    .mwi-profit-icon,.mwi-profit-icon-fallback { width:26px; height:26px; }
    .mwi-profit-icon-fallback { display:grid; place-items:center; border-radius:5px; background:rgba(255,255,255,.09); color:#fff; font-weight:700; }
    .mwi-profit-header-icon .mwi-profit-icon,.mwi-profit-header-icon .mwi-profit-icon-fallback { width:32px; height:32px; }
    .mwi-profit-tea .mwi-profit-icon,.mwi-profit-tea .mwi-profit-icon-fallback { width:23px; height:23px; }
    @media(max-width:760px){#${PANEL_ID3}{max-height:72vh}.mwi-profit-body{grid-template-columns:1fr}.mwi-profit-player{order:-1;flex-direction:row;flex-wrap:wrap}.mwi-profit-flow{transform:rotate(90deg)}.mwi-profit-stat-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 8px}.mwi-profit-valuation-row{grid-template-columns:repeat(2,minmax(0,1fr))}.mwi-profit-valuation-name{grid-column:1 / 3;border-right:0;border-bottom:1px solid rgba(255,255,255,.08)}.mwi-profit-valuation-metric{border-top:1px solid rgba(255,255,255,.055)}.mwi-profit-valuation-name + .mwi-profit-valuation-metric{border-left:0}}
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function renderItemRow(item, type) {
    const name = itemName3(item.itemHrid);
    const isInput = type === "input";
    const baseCount = Number(item.baseCount ?? item.count) || 0;
    const effectiveCount = Number(item.effectiveCount ?? item.count) || 0;
    let quantity = formatNumber3(effectiveCount, 3);
    if (Math.abs(baseCount - effectiveCount) > 1e-9) {
      quantity = `${formatNumber3(baseCount, 3)} → ${quantity}`;
    }
    let kind = "";
    if (item.isUpgradeItem) kind = t7("前置", "Base");
    if (item.kind === "essence") kind = t7("精华", "Essence");
    if (item.kind === "rare") kind = t7("稀有", "Rare");
    const priceLabel = isInput ? t7("市价", "Market value") : item.valueSource === "derived" ? t7("派生期望值", "Derived expected value") : t7("税后市价", "Net market value");
    return `
    <div class="mwi-profit-item" data-item-hrid="${escapeHtml4(item.itemHrid)}">
      <div>${renderItemIcon(item.itemHrid, name)}</div>
      <div>
        <div class="mwi-profit-item-name">${kind ? `<span class="mwi-profit-kind">${escapeHtml4(kind)}</span>` : ""}${escapeHtml4(name)}</div>
        <div class="mwi-profit-item-meta">${escapeHtml4(quantity)} · ${priceLabel} <span${numberTitleAttribute(item.unitPrice)}>${formatMoney(item.unitPrice)}</span></div>
      </div>
      <div class="mwi-profit-item-value">
        <strong${numberTitleAttribute(item.valuePerAction)}>${formatMoney(item.valuePerAction)}</strong>
        <span>${t7("每动作", "per action")}</span>
      </div>
    </div>`;
  }
  function renderValuationMetric(label, value, profit = false) {
    return `<div class="mwi-profit-valuation-metric${profit ? " profit" : ""}"><div class="mwi-profit-valuation-label">${escapeHtml4(label)}</div><div class="mwi-profit-valuation-value"${numberTitleAttribute(value)}>${formatMoney(value)}</div></div>`;
  }
  var VALUATION_ROWS = [
    {
      mode: "fair",
      title: { zh: "市价", en: "Market value" },
      explanation: {
        zh: "市场价值",
        en: "Market value"
      }
    },
    {
      mode: "conservative",
      title: { zh: "效率(高买低卖)", en: "Efficiency (buy high, sell low)" },
      explanation: {
        zh: "卖单买入 · 买单卖出",
        en: "Buy at ask · sell at bid"
      }
    },
    {
      mode: "aggressive",
      title: { zh: "贪心(低买高卖)", en: "Greedy (buy low, sell high)" },
      explanation: {
        zh: "买单买入 · 卖单卖出",
        en: "Buy at bid · sell at ask"
      }
    }
  ];
  function valuationText(value) {
    return value?.[runtime.config.isZH ? "zh" : "en"] ?? "";
  }
  function renderValuationRow(definition, valuation) {
    const complete = Boolean(valuation?.complete);
    const totalCost = complete ? valuation.materialCostPerAction + valuation.teaCostPerAction : null;
    const profitPerDay = complete ? valuation.profitPerHour * 24 : null;
    return `<section class="mwi-profit-valuation-row${complete ? "" : " incomplete"}" data-mode="${definition.mode}">
    <div class="mwi-profit-valuation-name">
      <div class="mwi-profit-valuation-title">${escapeHtml4(valuationText(definition.title))}</div>
      <div class="mwi-profit-valuation-state">${escapeHtml4(valuationText(definition.explanation))}</div>
    </div>
    ${renderValuationMetric(t7("税后收入/动作", "Net revenue/action"), complete ? valuation.revenuePerAction : null)}
    ${renderValuationMetric(t7("材料成本/动作", "Materials/action"), complete ? valuation.materialCostPerAction : null)}
    ${renderValuationMetric(t7("茶饮成本/动作", "Tea cost/action"), complete ? valuation.teaCostPerAction : null)}
    ${renderValuationMetric(t7("总成本/动作", "Total cost/action"), totalCost)}
    ${renderValuationMetric(t7("净利润/动作", "Net profit/action"), complete ? valuation.netProfitPerAction : null, true)}
    ${renderValuationMetric(t7("净利润/天", "Net profit/day"), profitPerDay, true)}
  </section>`;
  }
  function statusInfo(projection) {
    if (projection.status === "waiting") {
      return {
        className: "waiting",
        label: t7("玩家数据未就绪", "Player data pending")
      };
    }
    const valuations = VALUATION_ROWS.map(
      ({ mode }) => projection.valuations?.[mode]
    );
    const completeCount = valuations.filter(
      (valuation) => valuation?.complete
    ).length;
    if (completeCount === 0) {
      return { className: "incomplete", label: t7("无法计算", "Unavailable") };
    }
    if (completeCount < valuations.length) {
      return {
        className: "partial",
        label: t7("部分口径缺价", "Some prices missing")
      };
    }
    if (valuations.some(
      (valuation) => valuation?.fallbackItemHrids?.length > 0 || valuation?.unpricedByproducts?.length > 0 || valuation?.derivedMissingPrices?.length > 0
    )) {
      return { className: "partial", label: t7("部分计价", "Partial pricing") };
    }
    return { className: "complete", label: t7("完整计价", "Fully priced") };
  }
  function renderPanel(panel, itemHrid, projection, options = {}) {
    const productName = itemName3(itemHrid);
    const status = statusInfo(projection);
    const detail = projection.detail;
    const directAction = Boolean(options.directAction);
    const title = directAction ? actionName2(projection.actionHrid, detail) : productName;
    const subtitle = directAction ? `${t7("全部期望产物", "All expected outputs")} · ${t7("当前玩家实时配置", "Current player configuration")}` : `${actionName2(projection.actionHrid, detail)} · ${t7("当前玩家实时配置", "Current player configuration")}`;
    panel.dataset.status = status.className;
    panel.innerHTML = `
    <header class="mwi-profit-header">
      <div class="mwi-profit-header-icon">${renderItemIcon(itemHrid, productName)}</div>
      <div class="mwi-profit-header-main">
        <div class="mwi-profit-title">${escapeHtml4(title)}</div>
        <div class="mwi-profit-subtitle">${escapeHtml4(subtitle)}</div>
      </div>
      <div class="mwi-profit-status ${status.className}">${escapeHtml4(status.label)}</div>
    </header>`;
    if (projection.status === "waiting") {
      panel.insertAdjacentHTML(
        "beforeend",
        `<div class="mwi-profit-state">${t7("正在等待当前角色的装备、技能与茶饮数据,未使用任何默认配置。", "Waiting for this character's equipment, skills, and drink data. No defaults are being used.")}</div>`
      );
      return;
    }
    const inputRows = (projection.inputs ?? []).map((item) => renderItemRow(item, "input")).join("");
    const outputRows = [
      ...projection.outputs ?? [],
      ...projection.byproductOutputs ?? []
    ].map((item) => renderItemRow(item, "output")).join("");
    const teas = projection.teaEffects?.drinks ?? [];
    const teaIcons = teas.length ? teas.map((tea) => {
      const name = itemName3(tea.itemHrid);
      return `<span class="mwi-profit-tea">${renderItemIcon(tea.itemHrid, name)}</span>`;
    }).join("") : `<span class="mwi-profit-no-tea">${t7("未使用茶饮", "No active drinks")}</span>`;
    const effects = [];
    if (projection.teaEffects?.lessResource > 0) {
      effects.push(
        `<span class="mwi-profit-effect">${t7("工匠", "Artisan")} −${formatNumber3(projection.teaEffects.lessResource * 100, 1)}%</span>`
      );
    }
    if (projection.teaEffects?.quantity > 0) {
      effects.push(
        `<span class="mwi-profit-effect">${t7("额外产量", "Extra output")} +${formatNumber3(projection.teaEffects.quantity * 100, 1)}%</span>`
      );
    }
    panel.insertAdjacentHTML(
      "beforeend",
      `<div class="mwi-profit-body">
      <section class="mwi-profit-card cost">
        <div class="mwi-profit-card-title"><span>${t7("投入", "Inputs")}</span><span class="mwi-profit-card-total"${numberTitleAttribute(projection.materialCostPerAction)}>${formatMoney(projection.materialCostPerAction)} / ${t7("动作", "action")}</span></div>
        ${inputRows || `<div class="mwi-profit-no-tea">${t7("无材料投入", "No material inputs")}</div>`}
      </section>
      <section class="mwi-profit-player">
        <div class="mwi-profit-player-title">${t7("当前玩家", "Current player")}</div>
        <div class="mwi-profit-teas">${teaIcons}</div>
        <div class="mwi-profit-effects">${effects.join("")}</div>
        <div class="mwi-profit-flow">→</div>
        <div class="mwi-profit-stat-list">
          <div class="mwi-profit-stat"><span>${t7("饮料浓度", "Drink strength")}</span><strong>×${formatNumber3(projection.teaEffects?.concentrationMultiplier ?? 1, 3)}</strong></div>
          <div class="mwi-profit-stat"><span>${t7("动作速度", "Action speed")}</span><strong>${formatPercent2(projection.speedPercent)}</strong></div>
          <div class="mwi-profit-stat"><span>${t7("综合效率", "Efficiency")}</span><strong>${formatPercent2(projection.efficiencyPercent)}</strong></div>
          <div class="mwi-profit-stat"><span>${t7("动作/小时", "Actions/hour")}</span><strong>${formatNumber3(projection.actionsPerHour, 1)}</strong></div>
          <div class="mwi-profit-stat"><span>${t7("茶费/小时", "Tea cost/hour")}</span><strong${numberTitleAttribute(projection.teaCostPerHour)}>${formatMoney(projection.teaCostPerHour)}</strong></div>
        </div>
      </section>
      <section class="mwi-profit-card income">
        <div class="mwi-profit-card-title"><span>${t7("产出", "Outputs")}</span><span class="mwi-profit-card-total"${numberTitleAttribute(projection.revenuePerAction)}>${formatMoney(projection.revenuePerAction)} / ${t7("动作", "action")}</span></div>
        ${outputRows || `<div class="mwi-profit-no-tea">${t7("无可计价产出", "No priced outputs")}</div>`}
      </section>
    </div>`
    );
    panel.insertAdjacentHTML(
      "beforeend",
      `<div class="mwi-profit-valuations">
      ${VALUATION_ROWS.map((definition) => renderValuationRow(definition, projection.valuations?.[definition.mode])).join("")}
    </div>`
    );
    const missingValuations = VALUATION_ROWS.filter(
      ({ mode }) => !projection.valuations?.[mode]?.complete
    );
    const warningParts = [];
    if (missingValuations.length) {
      const details = missingValuations.map((definition) => {
        const names = (projection.valuations?.[definition.mode]?.missingPrices ?? []).map(itemName3).join(runtime.config.isZH ? "、" : ", ");
        return `${valuationText(definition.title)}:${names || "—"}`;
      }).join(runtime.config.isZH ? ";" : "; ");
      warningParts.push(
        `${t7("以下口径缺少必需市场价格:", "Required prices are missing for: ")}${details}`
      );
    }
    const unpricedByproducts = [
      ...new Set(
        VALUATION_ROWS.flatMap(
          ({ mode }) => projection.valuations?.[mode]?.unpricedByproducts ?? []
        )
      )
    ];
    if (unpricedByproducts.length) {
      warningParts.push(
        `${t7("以下副产物没有市场价,已从利润中排除:", "These byproducts have no market price and were excluded: ")}${unpricedByproducts.map(itemName3).join(runtime.config.isZH ? "、" : ", ")}`
      );
    }
    const derivedMissingPrices = [
      ...new Set(
        VALUATION_ROWS.flatMap(
          ({ mode }) => projection.valuations?.[mode]?.derivedMissingPrices ?? []
        )
      )
    ];
    if (derivedMissingPrices.length) {
      warningParts.push(
        `${t7("派生期望值仍有内部产物缺价,当前利润只计入已知部分:", "Some contents used by derived expected values are unpriced; profit includes only known contents: ")}${derivedMissingPrices.map(itemName3).join(runtime.config.isZH ? "、" : ", ")}`
      );
    }
    const fallbackItemHrids = [
      ...new Set(
        VALUATION_ROWS.flatMap(
          ({ mode }) => projection.valuations?.[mode]?.fallbackItemHrids ?? []
        )
      )
    ];
    if (fallbackItemHrids.length) {
      warningParts.push(
        `${t7("以下物品缺少所选订单簿价格,已使用市场价值兜底:", "Market value was used where the selected order-book price was unavailable: ")}${fallbackItemHrids.map(itemName3).join(runtime.config.isZH ? "、" : ", ")}`
      );
    }
    if (!warningParts.length) return;
    panel.insertAdjacentHTML(
      "beforeend",
      `<div class="mwi-profit-warning">${escapeHtml4(warningParts.join(runtime.config.isZH ? ";" : "; "))}</div>`
    );
  }
  function renderLootChestDropCell(drop) {
    const name = itemName3(drop.itemHrid);
    const chance = drop.dropRate >= 1 ? t7("必得", "100%") : `${formatNumber3(drop.dropRate * 100, drop.dropRate * 100 < 1 ? 2 : 0)}%`;
    const countRange = drop.minCount === drop.maxCount ? formatNumber3(drop.minCount, 0) : `${formatNumber3(drop.minCount, 0)}–${formatNumber3(drop.maxCount, 0)}`;
    const redemptions = drop.redemptions ?? [];
    const redemptionLines = redemptions.map((route) => {
      const tokenName = itemName3(route.tokenItemHrid);
      return `${t7("最佳兑换", "Best exchange")} · ${tokenName}: ${formatNumber3(route.tokenCount, 0)} → ${formatNumber3(route.rewardCount, 0)} ${name} · ${formatMoney(route.valuePerToken)} / ${t7("代币", "token")}`;
    });
    const sourceLabel = drop.valueSource === "redemption" ? t7("最佳兑换折算", "Best redemption") : drop.valueSource === "derived" ? t7("派生期望值", "Derived expected value") : drop.valueSource === "excluded" ? t7("牛铃已忽略", "Cowbells ignored") : drop.valueSource === "zero" ? t7("封印计为 0", "Seal valued at 0") : drop.nested ? t7("开箱期望", "Opening EV") : t7("单价", "Unit");
    const title = [
      `${name}
${t7("概率", "Chance")}: ${chance} · ${t7("数量", "Count")}: ${countRange} · ${t7("期望", "Expected")}: ${formatNumber3(drop.expectedCount, 2)}`,
      `${sourceLabel}: ${drop.priced ? formatMoney(drop.unitValue) : t7("无价", "No price")} · ${t7("期望价值", "Expected value")}: ${drop.priced ? formatMoney(drop.value) : t7("无价", "No price")}`,
      ...redemptionLines
    ].join("\n");
    const valueText = drop.priced ? `${drop.nested ? "≈" : ""}${formatMoney(drop.value)}` : t7("无价", "No price");
    return `
    <div class="mwi-loot-cell${drop.priced ? "" : " unpriced"}${redemptions.length ? " best-redemption" : ""}" data-item-hrid="${escapeHtml4(drop.itemHrid)}" title="${escapeHtml4(title)}">
      ${redemptions.length ? `<span class="mwi-loot-best-badge">${t7("最佳兑换", "Best Exchange")}</span>` : ""}
      <div class="mwi-loot-cell-icon">
        ${renderItemIcon(drop.itemHrid, name)}
        <span class="mwi-loot-cell-chance">${escapeHtml4(chance)}</span>
      </div>
      <div class="mwi-loot-cell-main">
        <div class="mwi-loot-cell-name">${escapeHtml4(name)}</div>
        <div class="mwi-loot-cell-value">${escapeHtml4(valueText)}</div>
      </div>
    </div>`;
  }
  function renderLootSwitch(setting, label, state, checked) {
    return `<div class="mwi-loot-control">
    <span class="mwi-loot-control-label">${escapeHtml4(label)}</span>
    <span class="mwi-loot-control-state">${escapeHtml4(state)}</span>
    <label class="mwi-loot-switch">
      <input type="checkbox" data-mwi-loot-setting="${setting}" aria-label="${escapeHtml4(label)}"${checked ? " checked" : ""}>
      <span aria-hidden="true"></span>
    </label>
  </div>`;
  }
  function renderLootChestControls(config, hasKey) {
    const controls = [
      renderLootSwitch(
        "lootSellAtAsk",
        t7("产物卖出", "Sell drops"),
        config.sellAtAsk ? t7("挂卖单", "List at ask") : t7("立即卖出", "Sell now"),
        config.sellAtAsk
      ),
      renderLootSwitch(
        "lootIgnoreCowbells",
        t7("牛铃价值", "Cowbell value"),
        config.ignoreCowbells ? t7("忽略", "Ignored") : t7("计入", "Included"),
        config.ignoreCowbells
      )
    ];
    if (hasKey) {
      controls.push(
        renderLootSwitch(
          "lootBuyAtAsk",
          t7("钥匙或碎片", "Key or fragments"),
          config.buyAtAsk ? t7("立即买入", "Buy now") : t7("挂买单", "Place bid"),
          config.buyAtAsk
        ),
        renderLootSwitch(
          "lootKeyFromFragments",
          t7("钥匙来源", "Key source"),
          config.fromFragments ? t7("碎片自制", "Craft fragments") : t7("购买成品", "Buy finished"),
          config.fromFragments
        )
      );
    }
    return `<div class="mwi-loot-controls${hasKey ? " has-key" : ""}">${controls.join("")}</div>`;
  }
  function renderLootChestPanel(panel, itemHrid, chest, options = {}) {
    const pinned = Boolean(options.pinned);
    const productName = itemName3(itemHrid);
    const hasKey = Boolean(chest.keyItemHrid);
    const hasEntryKey = Boolean(chest.entryKeyItemHrid);
    const statusClass = chest.complete ? "complete" : "partial";
    const statusLabel3 = chest.complete ? t7("完整计价", "Fully priced") : t7("部分计价", "Partial pricing");
    panel.dataset.status = statusClass;
    panel.classList.toggle("mwi-profit-pinned", pinned);
    panel.innerHTML = `
    <header class="mwi-profit-header">
      <div class="mwi-profit-header-icon">${renderItemIcon(itemHrid, productName)}</div>
      <div class="mwi-profit-header-main">
        <div class="mwi-profit-title">${escapeHtml4(productName)}</div>
        <div class="mwi-profit-subtitle">${escapeHtml4(hasEntryKey ? t7("开箱期望 · 已扣开箱钥匙与门票钥匙成本", "Opening estimate · net of chest and entry key costs") : hasKey ? t7("开箱期望 · 已扣钥匙成本", "Opening estimate · net of key cost") : t7("开箱期望", "Opening estimate"))}</div>
      </div>
      <div class="mwi-profit-status ${statusClass}">${statusLabel3}</div>
      ${pinned ? `<button type="button" class="mwi-profit-close" aria-label="${t7("关闭", "Close")}" data-mwi-loot-close="1">×</button>` : ""}
    </header>`;
    if (pinned) {
      panel.insertAdjacentHTML(
        "beforeend",
        renderLootChestControls(chest.config, hasKey)
      );
    }
    const cells = chest.drops.map(renderLootChestDropCell).join("");
    panel.insertAdjacentHTML(
      "beforeend",
      `<section class="mwi-profit-card income" style="margin:12px;">
      <div class="mwi-profit-card-title"><span>${t7("可能产出", "Possible drops")} (${chest.drops.length})</span><span class="mwi-profit-card-total"${numberTitleAttribute(chest.grossValue)}>${formatMoney(chest.grossValue)}</span></div>
      ${cells ? `<div class="mwi-loot-grid">${cells}</div>` : `<div class="mwi-profit-no-tea">${t7("无可计价产出", "No priced drops")}</div>`}
    </section>`
    );
    const sellLabel = chest.config.sellAtAsk ? t7("挂卖单", "List at ask") : t7("立即卖出", "Sell now");
    const keyLabel = !hasKey ? t7("无需钥匙", "No key") : chest.config.fromFragments ? t7("碎片自制", "Crafted from fragments") : t7("购买成品", "Buy finished");
    panel.insertAdjacentHTML(
      "beforeend",
      `<div class="mwi-profit-valuations">
      <section class="mwi-profit-valuation-row mwi-loot-valuation-row${hasEntryKey ? " has-entry-key" : ""}${chest.complete ? "" : " incomplete"}" data-mode="fair">
        <div class="mwi-profit-valuation-name">
          <div class="mwi-profit-valuation-title">${t7("期望价值", "Expected value")}</div>
          <div class="mwi-profit-valuation-state">${escapeHtml4(`${sellLabel} · ${keyLabel}`)}</div>
        </div>
        ${renderValuationMetric(t7("毛期望价值", "Gross value"), chest.grossValue)}
        ${hasEntryKey ? `${renderValuationMetric(t7("开箱钥匙", "Chest key"), hasKey && !chest.chestKeyComplete ? null : chest.chestKeyCost)}${renderValuationMetric(t7("门票钥匙", "Entry key"), chest.entryKeyComplete ? chest.entryKeyCost : null)}` : renderValuationMetric(t7("钥匙成本", "Key cost"), hasKey && !chest.keyComplete ? null : chest.keyCost)}
        ${renderValuationMetric(t7("净期望价值", "Net value"), chest.netValue, true)}
      </section>
    </div>`
    );
    panel.insertAdjacentHTML(
      "beforeend",
      `<div class="mwi-profit-hint">${t7(
        pinned ? "开关会立即重算并保存;高亮卡片是每枚代币回报最高的兑换物品。" : "双击固定面板后可调整买卖方向和钥匙来源;高亮卡片是每枚代币回报最高的兑换物品。",
        pinned ? "Switches recalculate and save immediately; highlighted cards are the best return per token." : "Double-click to pin and adjust pricing; highlighted cards are the best return per token."
      )}</div>`
    );
    if (chest.missing.length) {
      panel.insertAdjacentHTML(
        "beforeend",
        `<div class="mwi-profit-warning">${escapeHtml4(
          `${t7("以下物品缺少所选口径的价格或配方,未计入期望:", "These items lack prices or recipes for the selected mode and were excluded: ")}${chest.missing.map(itemName3).join(runtime.config.isZH ? "、" : ", ")}`
        )}</div>`
      );
    }
  }
  function clamp2(value, minimum, maximum) {
    return Math.min(Math.max(value, minimum), Math.max(minimum, maximum));
  }
  function positionPanel() {
    const state = activePanel;
    if (!state?.panel?.isConnected) {
      hideProductionProfitPanel();
      return;
    }
    let anchorRect = state.anchorRect;
    if (state.anchor?.isConnected) {
      anchorRect = state.anchor.getBoundingClientRect();
      if (state.sticky) state.anchorRect = anchorRect;
    } else if (!state.sticky && !state.pinned) {
      hideProductionProfitPanel();
      return;
    }
    if (!anchorRect) return;
    const panelRect = state.panel.getBoundingClientRect();
    const viewportWidth = globalThis.innerWidth ?? document.documentElement.clientWidth;
    const viewportHeight = globalThis.innerHeight ?? document.documentElement.clientHeight;
    const roomRight = viewportWidth - anchorRect.right - VIEWPORT_MARGIN2;
    const roomLeft = anchorRect.left - VIEWPORT_MARGIN2;
    let placement = "right";
    let left;
    let top;
    if (roomRight >= panelRect.width + PANEL_GAP2) {
      left = anchorRect.right + PANEL_GAP2;
      top = clamp2(
        anchorRect.top,
        VIEWPORT_MARGIN2,
        viewportHeight - panelRect.height - VIEWPORT_MARGIN2
      );
    } else if (roomLeft >= panelRect.width + PANEL_GAP2) {
      placement = "left";
      left = anchorRect.left - panelRect.width - PANEL_GAP2;
      top = clamp2(
        anchorRect.top,
        VIEWPORT_MARGIN2,
        viewportHeight - panelRect.height - VIEWPORT_MARGIN2
      );
    } else {
      const roomBelow = viewportHeight - anchorRect.bottom - VIEWPORT_MARGIN2;
      placement = roomBelow >= panelRect.height + PANEL_GAP2 ? "bottom" : "top";
      left = clamp2(
        anchorRect.left,
        VIEWPORT_MARGIN2,
        viewportWidth - panelRect.width - VIEWPORT_MARGIN2
      );
      top = placement === "bottom" ? anchorRect.bottom + PANEL_GAP2 : anchorRect.top - panelRect.height - PANEL_GAP2;
      top = clamp2(
        top,
        VIEWPORT_MARGIN2,
        viewportHeight - panelRect.height - VIEWPORT_MARGIN2
      );
    }
    state.panel.dataset.placement = placement;
    state.panel.style.left = `${Math.round(left)}px`;
    state.panel.style.top = `${Math.round(top)}px`;
  }
  function hideProductionProfitPanel(kind = null) {
    const state = activePanel;
    if (!state) {
      document.getElementById(PANEL_ID3)?.remove();
      return;
    }
    if (kind && state.kind !== kind) return;
    state.mutationObserver?.disconnect();
    state.resizeObserver?.disconnect();
    globalThis.removeEventListener?.("resize", state.position);
    globalThis.removeEventListener?.("scroll", state.position, true);
    if (state.outsideHandler) {
      document.removeEventListener("mousedown", state.outsideHandler, true);
      document.removeEventListener("pointerdown", state.outsideHandler, true);
    }
    for (const stop of state.settingStops ?? []) stop?.();
    state.panel?.remove();
    activePanel = null;
  }
  function createPanelElement() {
    const panel = document.createElement("aside");
    panel.id = PANEL_ID3;
    panel.setAttribute("role", "status");
    panel.setAttribute("aria-live", "polite");
    return panel;
  }
  function mountPanel(anchor, panel, extraState = {}) {
    const pinned = Boolean(extraState.pinned);
    const sticky = Boolean(extraState.sticky);
    anchor.insertAdjacentElement("afterend", panel);
    const position = () => globalThis.requestAnimationFrame?.(positionPanel) ?? positionPanel();
    let mutationObserver = null;
    let resizeObserver = null;
    if (!pinned) {
      if (!sticky) {
        mutationObserver = new MutationObserver(() => {
          if (!anchor.isConnected) hideProductionProfitPanel();
        });
        mutationObserver.observe(anchor.parentNode ?? document.body, {
          childList: true
        });
      }
      resizeObserver = globalThis.ResizeObserver ? new globalThis.ResizeObserver(position) : null;
      resizeObserver?.observe(anchor);
      resizeObserver?.observe(panel);
      globalThis.addEventListener?.("resize", position);
      globalThis.addEventListener?.("scroll", position, true);
    }
    activePanel = {
      anchor,
      panel,
      position,
      mutationObserver,
      resizeObserver,
      pinned,
      anchorRect: sticky ? anchor.getBoundingClientRect() : null,
      ...extraState
    };
    if ((pinned || sticky) && document.body && panel.parentElement !== document.body) {
      document.body.appendChild(panel);
    }
    position();
    return panel;
  }
  function attachStickyOutsideHandler(panel, anchor) {
    const outsideHandler = (event) => {
      if (!activePanel?.sticky || activePanel.panel !== panel) return;
      if (panel.contains(event.target)) return;
      runtime.api.clearTooltipProfitHoverContext?.(anchor, null, {
        preserveTouchPress: true
      });
      hideProductionProfitPanel();
    };
    globalThis.setTimeout?.(() => {
      if (activePanel?.panel !== panel) return;
      document.addEventListener("pointerdown", outsideHandler, true);
      activePanel.outsideHandler = outsideHandler;
    }, 0);
  }
  function showProductionProfitPanel(anchor, itemHrid, options = {}) {
    if (activePanel?.pinned) return null;
    if (runtime.api.shouldSuppressMarketFeatures?.()) {
      hideProductionProfitPanel();
      return null;
    }
    const actionHrid = options.actionHrid ?? runtime.api.resolveProductionActionByItemHrid?.(itemHrid);
    if (!anchor?.isConnected || !actionHrid) {
      hideProductionProfitPanel();
      return null;
    }
    hideProductionProfitPanel();
    addStyles5();
    const projection = runtime.api.projectAction(actionHrid, 1);
    const primaryItemHrid = itemHrid ?? runtime.api.getExpectedOutputs?.(projection.detail)?.[0]?.itemHrid;
    if (!primaryItemHrid) return null;
    const panel = createPanelElement();
    const sticky = Boolean(options.sticky);
    panel.classList.toggle("mwi-profit-pinned", sticky);
    renderPanel(panel, primaryItemHrid, projection, {
      directAction: Boolean(options.actionHrid)
    });
    const mounted = mountPanel(anchor, panel, {
      itemHrid: primaryItemHrid,
      actionHrid,
      directAction: Boolean(options.actionHrid),
      sticky,
      kind: "profit"
    });
    if (sticky) attachStickyOutsideHandler(panel, anchor);
    return mounted;
  }
  function rerenderActiveProductionProfitPanel() {
    const state = activePanel;
    if (state?.kind !== "profit" || !state.panel?.isConnected) return;
    const projection = runtime.api.projectAction(state.actionHrid, 1);
    renderPanel(state.panel, state.itemHrid, projection, {
      directAction: state.directAction
    });
    state.position?.();
  }
  function showLootChestPanel(anchor, itemHrid, options = {}) {
    const pinned = Boolean(options.pinned);
    const sticky = Boolean(options.sticky) && !pinned;
    if (!pinned && activePanel?.pinned) return null;
    const chest = runtime.api.projectLootChest?.(itemHrid);
    if (!anchor?.isConnected || !chest) {
      hideProductionProfitPanel();
      return null;
    }
    hideProductionProfitPanel();
    addStyles5();
    const panel = createPanelElement();
    panel.classList.toggle("mwi-profit-pinned", sticky);
    renderLootChestPanel(panel, itemHrid, chest, { pinned });
    mountPanel(anchor, panel, { itemHrid, pinned, sticky, kind: "loot" });
    if (pinned) attachLootChestControls(panel, itemHrid);
    else if (sticky) attachStickyOutsideHandler(panel, anchor);
    return panel;
  }
  function attachLootChestControls(panel, itemHrid) {
    const rerender = () => {
      const chest = runtime.api.projectLootChest?.(itemHrid);
      if (activePanel?.panel !== panel || !chest) return;
      renderLootChestPanel(panel, itemHrid, chest, { pinned: true });
    };
    panel.addEventListener("click", (event) => {
      if (!event.target.closest?.("[data-mwi-loot-close]")) return;
      event.stopPropagation();
      hideProductionProfitPanel();
    });
    panel.addEventListener("change", (event) => {
      const input = event.target.closest?.("input[data-mwi-loot-setting]");
      if (!input) return;
      event.stopPropagation();
      void runtime.settings.set?.(input.dataset.mwiLootSetting, input.checked);
    });
    const settingStops = [
      "lootSellAtAsk",
      "lootBuyAtAsk",
      "lootKeyFromFragments",
      "lootIgnoreCowbells"
    ].map((settingId) => runtime.settings.onChange?.(settingId, rerender));
    settingStops.push(
      runtime.settings.onChange?.("lootChestEstimate", (enabled) => {
        if (!enabled) hideProductionProfitPanel();
      })
    );
    if (activePanel?.panel === panel) activePanel.settingStops = settingStops;
    const outsideHandler = (event) => {
      if (!activePanel?.pinned || activePanel.panel !== panel) return;
      if (panel.contains(event.target)) return;
      hideProductionProfitPanel();
    };
    globalThis.setTimeout?.(() => {
      if (activePanel?.panel !== panel) return;
      document.addEventListener("mousedown", outsideHandler, true);
      activePanel.outsideHandler = outsideHandler;
    }, 0);
  }
  function pinActiveLootChestPanel() {
    const state = activePanel;
    if (!state || state.pinned || state.kind !== "loot" || !state.itemHrid) {
      return false;
    }
    if (!state.anchor?.isConnected) return false;
    return Boolean(
      showLootChestPanel(state.anchor, state.itemHrid, { pinned: true })
    );
  }
  function dismissHoverPanel() {
    if (activePanel?.pinned || activePanel?.sticky) return;
    hideProductionProfitPanel();
  }
  runtime.settings.onChange?.("adaptIronCowMarketFeatures", () => {
    if (runtime.api.shouldSuppressMarketFeatures?.()) {
      hideProductionProfitPanel("profit");
    }
  });
  runtime.settings.onChange?.("itemTooltip_profit", (enabled) => {
    if (!enabled) hideProductionProfitPanel("profit");
  });
  runtime.settings.onChange?.("lootChestEstimate", (enabled) => {
    if (!enabled) hideProductionProfitPanel("loot");
  });
  runtime.onMessage("init_character_data", () => {
    if (runtime.api.shouldSuppressMarketFeatures?.()) {
      hideProductionProfitPanel();
    } else {
      rerenderActiveProductionProfitPanel();
    }
  });
  for (const messageType of [
    "items_updated",
    "skills_updated",
    "house_rooms_updated",
    "achievement_buffs_updated",
    "moo_pass_buffs_updated",
    "community_buffs_updated",
    "consumable_buffs_updated",
    "action_type_consumable_slots_updated",
    "equipment_buffs_updated",
    "personal_buffs_updated",
    "guild_buffs_updated"
  ]) {
    runtime.onMessage(messageType, rerenderActiveProductionProfitPanel);
  }
  Object.assign(runtime.api, {
    hideProductionProfitPanel,
    dismissHoverPanel,
    pinActiveLootChestPanel,
    positionProductionProfitPanel: positionPanel,
    showLootChestPanel,
    showProductionProfitPanel
  });

  // src/features/item-tooltips.js
  var TOUCH_PROFIT_LONG_PRESS_MS = 800;
  var TOUCH_PROFIT_MOVE_TOLERANCE = 12;
  var hoverPanelContext = null;
  var hoverPanelShortcutHeld = false;
  var touchHoverPanelPress = null;
  var touchHoverPanelAuthorizedUntil = 0;
  function isEditableTarget(target) {
    return Boolean(
      target?.closest?.('input,textarea,select,[contenteditable="true"]')
    );
  }
  function isModifierShortcutEvent(event) {
    return ["Control", "Shift", "Alt", "Meta"].includes(event?.key);
  }
  function requiresHoverPanelShortcut() {
    return Boolean(
      runtime.settings.settingsMap.itemTooltip_profitRequireKey?.isTrue
    );
  }
  function canShowHoverPanel(context = hoverPanelContext) {
    if (context?.kind === "enhancement") {
      return Boolean(runtime.settings.settingsMap.enhanceSim?.isTrue);
    }
    if (context?.kind === "loot") {
      return Boolean(runtime.settings.settingsMap.lootChestEstimate?.isTrue);
    }
    return Boolean(
      context?.kind === "profit" && runtime.settings.settingsMap.itemTooltip_profit?.isTrue && !runtime.api.shouldSuppressMarketFeatures?.()
    );
  }
  function hasEnabledHoverPanelFeature() {
    return Boolean(
      runtime.settings.settingsMap.enhanceSim?.isTrue || runtime.settings.settingsMap.lootChestEstimate?.isTrue || runtime.settings.settingsMap.itemTooltip_profit?.isTrue && !runtime.api.shouldSuppressMarketFeatures?.()
    );
  }
  function dismissHoverPanelContext(context = hoverPanelContext) {
    if (context?.kind === "enhancement") {
      runtime.api.hideEnhancementCostPanel?.();
    } else {
      runtime.api.dismissHoverPanel?.();
    }
  }
  function showHoverPanelContext(context = hoverPanelContext, options = {}) {
    if (!context?.anchor?.isConnected || !canShowHoverPanel(context)) {
      dismissHoverPanelContext(context);
      return null;
    }
    if (context.kind === "enhancement") {
      return runtime.api.showEnhancementCostPanel?.(
        context.anchor,
        context.plan ?? null,
        { sticky: Boolean(options.sticky) }
      );
    }
    if (context.kind === "loot") {
      return runtime.api.showLootChestPanel?.(context.anchor, context.itemHrid, {
        sticky: Boolean(options.sticky)
      });
    }
    return runtime.api.showProductionProfitPanel?.(
      context.anchor,
      context.itemHrid ?? null,
      {
        actionHrid: context.actionHrid,
        sticky: Boolean(options.sticky)
      }
    );
  }
  function setHoverPanelContext(context) {
    if (hoverPanelContext && (hoverPanelContext.kind !== context?.kind || hoverPanelContext.anchor !== context?.anchor)) {
      dismissHoverPanelContext(hoverPanelContext);
    }
    hoverPanelContext = context;
    if (!canShowHoverPanel(context)) {
      dismissHoverPanelContext(context);
      return;
    }
    if (!requiresHoverPanelShortcut() || hoverPanelShortcutHeld) {
      showHoverPanelContext(context);
      return;
    }
    if (Date.now() <= touchHoverPanelAuthorizedUntil) {
      touchHoverPanelAuthorizedUntil = 0;
      showHoverPanelContext(context, { sticky: true });
      return;
    }
    if (touchHoverPanelPress?.authorized) {
      touchHoverPanelPress.triggered = true;
      showHoverPanelContext(context, { sticky: true });
      return;
    }
    dismissHoverPanelContext(context);
  }
  function clearHoverPanelContext(anchor = null, kind = null, { preserveTouchPress = false } = {}) {
    if (anchor && hoverPanelContext?.anchor !== anchor) return;
    if (kind && hoverPanelContext?.kind !== kind) return;
    if (!preserveTouchPress) {
      clearTimeout(touchHoverPanelPress?.timer);
      touchHoverPanelPress = null;
      touchHoverPanelAuthorizedUntil = 0;
    }
    const previous = hoverPanelContext;
    hoverPanelContext = null;
    dismissHoverPanelContext(previous);
  }
  function setEnhancementHoverPanelContext(anchor, plan = null) {
    setHoverPanelContext({ kind: "enhancement", anchor, plan });
  }
  function clearEnhancementHoverPanelContext(anchor = null) {
    clearHoverPanelContext(anchor, "enhancement");
  }
  function removeSuppressedTooltipContent() {
    document.querySelectorAll('[data-mwitools-tooltip-market="true"]').forEach((row) => row.remove());
  }
  function productionCostTooltipRows(itemHrid) {
    if (!runtime.settings.settingsMap.itemTooltip_profit?.isTrue) return "";
    const actionHrid = runtime.api.resolveProductionActionByItemHrid?.(itemHrid);
    const label = runtime.config.isZH ? "生产总成本/动作(效率 / 贪心):" : "Production cost/action (Efficiency / Greedy): ";
    let costText = runtime.config.isZH ? "无配方" : "No recipe";
    if (actionHrid) {
      const projection = runtime.api.projectAction?.(actionHrid, 1);
      const costs = ["conservative", "aggressive"].map((mode) => {
        const valuation = projection?.valuations?.[mode];
        return valuation?.costComplete ? (Number(valuation.materialCostPerAction) || 0) + (Number(valuation.teaCostPerAction) || 0) : null;
      });
      costText = costs.map(
        (cost) => cost === null ? runtime.config.isZH ? "缺价" : "Missing price" : numberFormatter2(cost)
      ).join(" / ");
    }
    const shortcut = runtime.api.getTooltipProfitShortcut?.().display ?? "Ctrl";
    const hint = runtime.config.isZH ? `按住 ${shortcut} 显示详情;移动端长按显示详情` : `Hold ${shortcut} for details; long-press on mobile`;
    return `
    <div data-mwitools-tooltip-market="true" style="color: ${runtime.config.SCRIPT_COLOR_TOOLTIP};">${label}${costText}</div>
    <div data-mwitools-tooltip-market="true" style="color: var(--color-text-secondary,#777); font-size: calc(.6875rem * var(--mwi-ui-font-scale,1));">${hint}</div>
  `;
  }
  function consumableEfficiencyTooltipRow(itemHrid, marketValue) {
    if (!runtime.settings.settingsMap.showConsumTips?.isTrue) return "";
    const detail = runtime.state.initData_itemDetailMap?.[itemHrid]?.consumableDetail;
    const restores = [
      {
        amount: Number(detail?.hitpointRestore),
        unit: runtime.config.isZH ? "血" : "hp"
      },
      {
        amount: Number(detail?.manapointRestore),
        unit: runtime.config.isZH ? "蓝" : "mp"
      }
    ].filter(({ amount }) => Number.isFinite(amount) && amount > 0);
    if (!restores.length) return "";
    const value = Number(marketValue);
    const efficiency = Number.isFinite(value) && value > 0 ? restores.map(
      ({ amount, unit }) => runtime.config.isZH ? `${numberFormatter2(value * 100 / amount, 0)}金/100${unit}` : `${numberFormatter2(value * 100 / amount, 0)} coins/100${unit}`
    ).join(runtime.config.isZH ? "," : ", ") : "-";
    const label = runtime.config.isZH ? "消耗品性价比:" : "Consumable efficiency: ";
    return `<div data-mwitools-tooltip-market="true" style="color: ${runtime.config.SCRIPT_COLOR_TOOLTIP};">${label}${efficiency}</div>`;
  }
  function timeReadable(sec) {
    if (!Number.isFinite(sec) || sec < 0) return "—";
    const normalized = Math.round(sec);
    if (normalized >= 86400) {
      const days = Math.floor(normalized / 86400);
      const hours = Math.floor(normalized % 86400 / 3600);
      const minutes = Math.floor(normalized % 3600 / 60);
      const parts = [runtime.config.isZH ? `${days}天` : `${days}d`];
      if (hours > 0) {
        parts.push(runtime.config.isZH ? `${hours}小时` : `${hours}h`);
      }
      if (minutes > 0) {
        parts.push(runtime.config.isZH ? `${minutes}分` : `${minutes}m`);
      }
      return parts.join(runtime.config.isZH ? "" : " ");
    }
    const d = new Date(normalized * 1e3);
    function pad(i) {
      return ("0" + i).slice(-2);
    }
    let str = d.getUTCHours() + "h " + pad(d.getUTCMinutes()) + "m " + pad(d.getUTCSeconds()) + "s";
    return str;
  }
  var tooltipObserver = new MutationObserver(async function(mutations) {
    for (const mutation of mutations) {
      for (const added of mutation.addedNodes) {
        if (added?.nodeType === 1 && added.classList.contains("MuiTooltip-popper")) {
          if (added.querySelector("div.ItemTooltipText_name__2JAHA")) {
            await handleTooltipItem(added);
          } else if (runtime.settings.settingsMap.itemTooltip_profit.isTrue) {
            const actionHrid = resolveGatheringActionFromElement(added);
            if (actionHrid) {
              setHoverPanelContext({
                kind: "profit",
                anchor: added,
                actionHrid
              });
            }
          }
        }
      }
    }
  });
  var GATHERING_ACTION_TYPES = /* @__PURE__ */ new Set([
    "/action_types/foraging",
    "/action_types/milking",
    "/action_types/woodcutting"
  ]);
  var GATHERING_CARD_SELECTOR = [
    '[data-action-hrid^="/actions/"]',
    '[class*="SkillAction_skillAction"]',
    '[class*="GatheringProductionSkillPanel_action"]'
  ].join(",");
  function reactFiberKey(element) {
    return Object.keys(element ?? {}).find(
      (key) => key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")
    );
  }
  function gatheringActionHrid(value) {
    const hrid = String(value ?? "");
    const detail = runtime.state.initData_actionDetailMap?.[hrid];
    return GATHERING_ACTION_TYPES.has(detail?.type) ? hrid : "";
  }
  function actionHridFromReactValue(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
    if (!value || depth > 3 || typeof value !== "object" || seen.has(value)) {
      return "";
    }
    seen.add(value);
    for (const key of ["actionHrid", "hrid"]) {
      const actionHrid = gatheringActionHrid(value[key]);
      if (actionHrid) return actionHrid;
    }
    for (const key of [
      "actionDetail",
      "action",
      "detail",
      "selectedAction",
      "children",
      "props"
    ]) {
      const nested = value[key];
      if (Array.isArray(nested)) {
        for (const entry of nested) {
          const actionHrid2 = actionHridFromReactValue(entry, depth + 1, seen);
          if (actionHrid2) return actionHrid2;
        }
        continue;
      }
      const actionHrid = actionHridFromReactValue(nested, depth + 1, seen);
      if (actionHrid) return actionHrid;
    }
    return "";
  }
  function resolveGatheringActionFromElement(element) {
    for (let current = element; current; current = current.parentElement) {
      const direct = gatheringActionHrid(
        current.dataset?.actionHrid ?? current.getAttribute?.("data-action-hrid")
      );
      if (direct) return direct;
      const key = reactFiberKey(current);
      let fiber = key ? current[key] : null;
      for (let depth = 0; fiber && depth < 8; depth += 1) {
        for (const value of [
          fiber.memoizedProps,
          fiber.pendingProps,
          fiber.memoizedState,
          fiber.stateNode?.props,
          fiber.stateNode?.state
        ]) {
          const fromFiber = actionHridFromReactValue(value);
          if (fromFiber) return fromFiber;
        }
        fiber = fiber.return;
      }
      if (current.classList?.contains("MuiTooltip-popper")) break;
    }
    const texts = [
      runtime.api.getOriTextFromElement?.(element),
      element?.textContent,
      ...[...element?.querySelectorAll?.("div,span") ?? []].filter((node) => String(node.textContent ?? "").trim().length <= 80).map(
        (node) => runtime.api.getOriTextFromElement?.(node) ?? node.textContent
      )
    ].map(
      (text) => String(text ?? "").replaceAll(/\s+/g, " ").trim()
    ).filter(Boolean);
    const matches = Object.values(runtime.state.initData_actionDetailMap ?? {}).filter((detail) => GATHERING_ACTION_TYPES.has(detail?.type)).filter((detail) => {
      const names = [detail.name, getLocalizedEntityName("action", detail.hrid)].map((name) => String(name ?? "").trim()).filter(Boolean);
      return texts.some((text) => names.includes(text));
    }).sort(
      (left, right) => Number(left.sortIndex ?? 0) - Number(right.sortIndex ?? 0) || String(left.hrid).localeCompare(String(right.hrid))
    );
    return matches[0]?.hrid ?? "";
  }
  function gatheringCardFromEventTarget(target) {
    return target?.closest?.(GATHERING_CARD_SELECTOR) ?? null;
  }
  var actionHridToToolsSpeedBuffNamesMap = {
    "/action_types/brewing": "brewingSpeed",
    "/action_types/cheesesmithing": "cheesesmithingSpeed",
    "/action_types/cooking": "cookingSpeed",
    "/action_types/crafting": "craftingSpeed",
    "/action_types/foraging": "foragingSpeed",
    "/action_types/milking": "milkingSpeed",
    "/action_types/tailoring": "tailoringSpeed",
    "/action_types/woodcutting": "woodcuttingSpeed",
    "/action_types/alchemy": "alchemySpeed"
  };
  var actionHridToHouseNamesMap = {
    "/action_types/brewing": "/house_rooms/brewery",
    "/action_types/cheesesmithing": "/house_rooms/forge",
    "/action_types/cooking": "/house_rooms/kitchen",
    "/action_types/crafting": "/house_rooms/workshop",
    "/action_types/foraging": "/house_rooms/garden",
    "/action_types/milking": "/house_rooms/dairy_barn",
    "/action_types/tailoring": "/house_rooms/sewing_parlor",
    "/action_types/woodcutting": "/house_rooms/log_shed",
    "/action_types/alchemy": "/house_rooms/laboratory"
  };
  var itemEnhanceLevelToBuffBonusMap = {
    0: 0,
    1: 2,
    2: 4.2,
    3: 6.6,
    4: 9.2,
    5: 12,
    6: 15,
    7: 18.2,
    8: 21.6,
    9: 25.2,
    10: 29,
    11: 33.4,
    12: 38.4,
    13: 44,
    14: 50.2,
    15: 57,
    16: 64.4,
    17: 72.4,
    18: 81,
    19: 90.2,
    20: 100
  };
  function getToolsSpeedBuffByActionHrid(actionHrid) {
    let totalBuff = 0;
    for (const item of runtime.state.initData_characterItems) {
      if (item.itemLocationHrid.includes("_tool")) {
        const buffName = actionHridToToolsSpeedBuffNamesMap[runtime.state.initData_actionDetailMap[actionHrid].type];
        const enhanceBonus = 1 + itemEnhanceLevelToBuffBonusMap[item.enhancementLevel] / 100;
        const buff = runtime.state.initData_itemDetailMap[item.itemHrid].equipmentDetail.noncombatStats[buffName] || 0;
        totalBuff += buff * enhanceBonus;
      }
    }
    return Number(totalBuff * 100).toFixed(1);
  }
  function getItemEffiBuffByActionHrid(actionHrid) {
    let buff = 0;
    const propertyName = runtime.state.initData_actionDetailMap[actionHrid].type.replace(
      "/action_types/",
      ""
    ) + "Efficiency";
    for (const item of runtime.state.initData_characterItems) {
      if (item.itemLocationHrid === "/item_locations/inventory") {
        continue;
      }
      const itemDetail = runtime.state.initData_itemDetailMap[item.itemHrid];
      const specificStat = itemDetail?.equipmentDetail?.noncombatStats[propertyName];
      if (specificStat && specificStat > 0) {
        let enhanceBonus = 1;
        if (item.itemLocationHrid.includes("earrings") || item.itemLocationHrid.includes("ring") || item.itemLocationHrid.includes("neck")) {
          enhanceBonus = 1 + itemEnhanceLevelToBuffBonusMap[item.enhancementLevel] * 5 / 100;
        } else {
          enhanceBonus = 1 + itemEnhanceLevelToBuffBonusMap[item.enhancementLevel] / 100;
        }
        buff += specificStat * enhanceBonus;
      }
      const skillingStat = itemDetail?.equipmentDetail?.noncombatStats["skillingEfficiency"];
      if (skillingStat && skillingStat > 0) {
        let enhanceBonus = 1;
        if (item.itemLocationHrid.includes("earrings") || item.itemLocationHrid.includes("ring") || item.itemLocationHrid.includes("neck")) {
          enhanceBonus = 1 + itemEnhanceLevelToBuffBonusMap[item.enhancementLevel] * 5 / 100;
        } else {
          enhanceBonus = 1 + itemEnhanceLevelToBuffBonusMap[item.enhancementLevel] / 100;
        }
        buff += skillingStat * enhanceBonus;
      }
    }
    return Number(buff * 100).toFixed(1);
  }
  function getHousesEffBuffByActionHrid(actionHrid) {
    const houseName2 = actionHridToHouseNamesMap[runtime.state.initData_actionDetailMap[actionHrid].type];
    if (!houseName2) {
      return 0;
    }
    const house = runtime.state.initData_characterHouseRoomMap[houseName2];
    if (!house) {
      return 0;
    }
    return house.level * 1.5;
  }
  function getTeaBuffsByActionHrid(actionHrid) {
    const teaBuffs = {
      efficiency: 0,
      // Efficiency tea, specific teas, -Artisan tea.
      quantity: 0,
      // Gathering tea, Gourmet tea.
      lessResource: 0,
      // Artisan tea.
      extraExp: 0,
      // Wisdom tea. Not used.
      upgradedProduct: 0
      // Processing tea. Not used.
    };
    const actionTypeId = runtime.state.initData_actionDetailMap[actionHrid].type;
    const teaList = runtime.state.initData_actionTypeDrinkSlotsMap[actionTypeId];
    for (const tea of teaList) {
      if (!tea || !tea.itemHrid) {
        continue;
      }
      for (const buff of runtime.state.initData_itemDetailMap[tea.itemHrid].consumableDetail.buffs) {
        if (buff.typeHrid === "/buff_types/artisan") {
          teaBuffs.lessResource += buff.flatBoost * 100;
        } else if (buff.typeHrid === "/buff_types/action_level") {
          teaBuffs.efficiency -= buff.flatBoost;
        } else if (buff.typeHrid === "/buff_types/gathering") {
          teaBuffs.quantity += buff.flatBoost * 100;
        } else if (buff.typeHrid === "/buff_types/gourmet") {
          teaBuffs.quantity += buff.flatBoost * 100;
        } else if (buff.typeHrid === "/buff_types/wisdom") {
          teaBuffs.extraExp += buff.flatBoost * 100;
        } else if (buff.typeHrid === "/buff_types/processing") {
          teaBuffs.upgradedProduct += buff.flatBoost * 100;
        } else if (buff.typeHrid === "/buff_types/efficiency") {
          teaBuffs.efficiency += buff.flatBoost * 100;
        } else if (buff.typeHrid === `/buff_types/${actionTypeId.replace("/action_types/", "")}_level`) {
          teaBuffs.efficiency += buff.flatBoost;
        }
      }
    }
    return teaBuffs;
  }
  async function handleTooltipItem(tooltip) {
    const itemNameElems = tooltip.querySelectorAll(
      "div.ItemTooltipText_name__2JAHA span"
    );
    if (itemNameElems.length > 1) {
      clearHoverPanelContext(null, null, { preserveTouchPress: true });
      runtime.api.dismissHoverPanel?.();
      runtime.api.handleItemTooltipWithEnhancementLevel(tooltip);
      return;
    }
    runtime.api.hideEnhancementCostPanel?.();
    const itemNameElem = itemNameElems[0];
    const itemName4 = runtime.api.getOriTextFromElement(itemNameElem);
    const itemHrid = resolveEntityFromElement("item", tooltip) || resolveLocalizedEntity("item", itemName4);
    let amount = 0;
    let insertAfterElem = null;
    const amountSpan = tooltip.querySelectorAll("span")[1];
    if (amountSpan) {
      amount = runtime.api.parseCompactNumber(
        runtime.api.getOriTextFromElement(amountSpan).split(": ")[1]
      );
      insertAfterElem = amountSpan.parentNode.nextSibling;
    } else {
      insertAfterElem = tooltip.querySelectorAll("span")[0].parentNode.nextSibling;
    }
    let appendHTMLStr = "";
    let marketJson = null;
    let ask = null;
    let bid = null;
    let fairValue = null;
    const suppressMarket = Boolean(runtime.api.shouldSuppressMarketFeatures?.());
    if (runtime.settings.settingsMap.itemTooltip_prices.isTrue && !suppressMarket) {
      marketJson = await fetchMarketJSON2();
      if (!marketJson || !marketJson.marketData) {
        console.error(
          runtime.config.isZH ? "[MWITools] 物品悬浮窗无法取得市场数据。" : "[MWITools] Item tooltip market data is unavailable."
        );
      }
      ask = marketJson?.marketData[itemHrid]?.[0]?.a ?? 0;
      bid = marketJson?.marketData[itemHrid]?.[0]?.b ?? 0;
      fairValue = runtime.api.getFairValue(itemHrid, 0);
      appendHTMLStr += `
    <div data-mwitools-tooltip-market="true" style="color: ${runtime.config.SCRIPT_COLOR_TOOLTIP};">${runtime.config.isZH ? "市场价值:" : "Market value: "}${fairValue > 0 ? numberFormatter2(fairValue) : "-"}${fairValue > 0 && amount > 0 ? ` (${numberFormatter2(fairValue * amount)})` : ""}</div>
    <div data-mwitools-tooltip-market="true" style="color: ${runtime.config.SCRIPT_COLOR_TOOLTIP};">${runtime.config.isZH ? "价格: " : "Price: "}${numberFormatter2(ask)} / ${numberFormatter2(bid)} (${ask && ask > 0 ? numberFormatter2(ask * amount) : ""} / ${bid && bid > 0 ? numberFormatter2(bid * amount) : ""})</div>
    `;
      appendHTMLStr += consumableEfficiencyTooltipRow(itemHrid, fairValue);
    }
    if (!suppressMarket) appendHTMLStr += productionCostTooltipRows(itemHrid);
    insertAfterElem.insertAdjacentHTML("afterend", appendHTMLStr);
    const dropMap = runtime.state.initData_openableLootDropMap;
    const isOpenable = Boolean(
      dropMap instanceof Map ? dropMap.get(itemHrid) : dropMap?.[itemHrid]
    );
    if (isOpenable && runtime.settings.settingsMap.lootChestEstimate?.isTrue) {
      setHoverPanelContext({ kind: "loot", anchor: tooltip, itemHrid });
    } else if (!isOpenable && runtime.settings.settingsMap.itemTooltip_profit.isTrue) {
      setHoverPanelContext({ kind: "profit", anchor: tooltip, itemHrid });
    } else {
      clearHoverPanelContext();
      runtime.api.dismissHoverPanel?.();
    }
    const tootip = insertAfterElem.closest(".MuiTooltip-popper");
    const fixOverflow = (tootip2) => {
      if (!tootip2.isConnected) {
        return;
      }
      const bBox = tootip2.getBoundingClientRect();
      if (bBox.top < 0 || bBox.bottom > window.innerHeight) {
        const transformString = tootip2.style.transform.split(/\w+\(|\);?/);
        const transformValues = transformString[1].split(/,\s?/g).map((numStr) => parseInt(numStr));
        tootip2.style.transform = `translate3d(${transformValues[0]}px, 0px, ${transformValues[2]}px)`;
      }
    };
    setTimeout(fixOverflow, 100, tootip);
  }
  async function fetchMarketJSON2(forceFetch = false) {
    return runtime.api.fetchMarketJSON(forceFetch);
  }
  function numberFormatter2(num, digits = 1) {
    return runtime.api.numberFormatter(num, digits);
  }
  function getActionHridFromItemName(name) {
    let newName = name.replace("Milk", "Cow");
    newName = newName.replace("Log", "Tree");
    newName = newName.replace("Cowing", "Milking");
    newName = newName.replace("Rainbow Cow", "Unicow");
    newName = newName.replace("Collector's Boots", "Collectors Boots");
    newName = newName.replace("Knight's Aegis", "Knights Aegis");
    if (!runtime.state.initData_actionDetailMap) {
      console.error(
        runtime.config.isZH ? `[MWITools] 无法按物品名称查找行动:行动数据尚未加载(${name})。` : `[MWITools] Cannot find an action by item name because action data is not loaded (${name}).`
      );
      return null;
    }
    for (const action of Object.values(runtime.state.initData_actionDetailMap)) {
      if (action.name === newName) {
        return action.hrid;
      }
    }
    return null;
  }
  Object.assign(runtime.api, {
    timeReadable,
    getToolsSpeedBuffByActionHrid,
    getItemEffiBuffByActionHrid,
    getHousesEffBuffByActionHrid,
    getTeaBuffsByActionHrid,
    handleTooltipItem,
    getProductionCostTooltipRows: productionCostTooltipRows,
    getActionHridFromItemName,
    clearTooltipProfitHoverContext: clearHoverPanelContext,
    setEnhancementHoverPanelContext,
    clearEnhancementHoverPanelContext
  });
  Object.defineProperties(runtime.state, {
    tooltipObserver: {
      enumerable: true,
      get() {
        return tooltipObserver;
      }
    }
  });
  Object.defineProperties(runtime.data, {
    actionHridToToolsSpeedBuffNamesMap: {
      enumerable: true,
      get() {
        return actionHridToToolsSpeedBuffNamesMap;
      }
    },
    actionHridToHouseNamesMap: {
      enumerable: true,
      get() {
        return actionHridToHouseNamesMap;
      }
    },
    itemEnhanceLevelToBuffBonusMap: {
      enumerable: true,
      get() {
        return itemEnhanceLevelToBuffBonusMap;
      }
    }
  });
  runtime.features.register({
    id: "itemTooltip_prices",
    setting: "itemTooltip_prices",
    initialize({ scope }) {
      const styles = [
        GM_addStyle(`div.Header_actionName__31-L2 {
        overflow: visible !important;
        white-space: normal !important;
        height: auto !important;
      }`),
        GM_addStyle(`span.NavigationBar_label__1uH-y {
        width: 10px !important;
      }`)
      ];
      const attach = () => {
        if (!document.body) return false;
        tooltipObserver.observe(document.body, {
          attributes: false,
          childList: true,
          characterData: false
        });
        return true;
      };
      if (!attach()) {
        scope.event(document, "DOMContentLoaded", attach, { once: true });
      }
      scope.event(
        document,
        "dblclick",
        (event) => {
          if (event.button && event.button !== 0) return;
          if (!runtime.api.pinActiveLootChestPanel?.()) return;
          event.preventDefault();
          event.stopPropagation();
        },
        true
      );
      scope.event(
        window,
        "keydown",
        (event) => {
          if (event.repeat || isEditableTarget(event.target) && !isModifierShortcutEvent(event) || !runtime.api.matchesTooltipProfitShortcut?.(event)) {
            return;
          }
          hoverPanelShortcutHeld = true;
          if (requiresHoverPanelShortcut()) showHoverPanelContext();
        },
        true
      );
      scope.event(
        window,
        "keyup",
        (event) => {
          if (!runtime.api.matchesTooltipProfitShortcut?.(event)) return;
          hoverPanelShortcutHeld = false;
          if (requiresHoverPanelShortcut()) dismissHoverPanelContext();
        },
        true
      );
      scope.event(window, "blur", () => {
        hoverPanelShortcutHeld = false;
        dismissHoverPanelContext();
      });
      scope.event(
        document,
        "pointerdown",
        (event) => {
          if (event.pointerType !== "touch" || !requiresHoverPanelShortcut() || !hasEnabledHoverPanelFeature()) {
            return;
          }
          clearTimeout(touchHoverPanelPress?.timer);
          touchHoverPanelAuthorizedUntil = 0;
          const press = {
            pointerId: event.pointerId,
            x: event.clientX,
            y: event.clientY,
            authorized: false,
            triggered: false,
            timer: null
          };
          press.timer = setTimeout(() => {
            if (touchHoverPanelPress !== press) return;
            press.authorized = true;
            if (hoverPanelContext?.anchor?.isConnected) {
              press.triggered = true;
              showHoverPanelContext(hoverPanelContext, { sticky: true });
            }
          }, TOUCH_PROFIT_LONG_PRESS_MS);
          touchHoverPanelPress = press;
        },
        true
      );
      scope.event(
        document,
        "pointermove",
        (event) => {
          const press = touchHoverPanelPress;
          if (!press || press.pointerId !== event.pointerId) return;
          if (Math.hypot(event.clientX - press.x, event.clientY - press.y) <= TOUCH_PROFIT_MOVE_TOLERANCE) {
            return;
          }
          clearTimeout(press.timer);
          touchHoverPanelPress = null;
          touchHoverPanelAuthorizedUntil = 0;
        },
        true
      );
      const finishTouchPress = (event) => {
        const press = touchHoverPanelPress;
        if (!press || press.pointerId !== event.pointerId) return;
        clearTimeout(press.timer);
        if (press.authorized && !press.triggered) {
          touchHoverPanelAuthorizedUntil = Date.now() + 500;
        }
        touchHoverPanelPress = null;
      };
      scope.event(document, "pointerup", finishTouchPress, true);
      scope.event(
        document,
        "pointercancel",
        (event) => {
          if (touchHoverPanelPress?.pointerId !== event.pointerId) return;
          clearTimeout(touchHoverPanelPress.timer);
          touchHoverPanelPress = null;
          touchHoverPanelAuthorizedUntil = 0;
        },
        true
      );
      const stopRequireKey = runtime.settings.onChange(
        "itemTooltip_profitRequireKey",
        (required) => {
          if (!required) showHoverPanelContext();
          else if (!hoverPanelShortcutHeld) dismissHoverPanelContext();
        }
      );
      const stopIronCow = runtime.settings.onChange(
        "adaptIronCowMarketFeatures",
        () => {
          if (!runtime.api.shouldSuppressMarketFeatures?.()) return;
          clearHoverPanelContext(null, "profit");
          removeSuppressedTooltipContent();
        }
      );
      const stopLootEstimate = runtime.settings.onChange(
        "lootChestEstimate",
        (enabled) => {
          if (!enabled) {
            clearHoverPanelContext(null, "loot");
            runtime.api.hideProductionProfitPanel?.("loot");
          }
        }
      );
      const stopEnhanceSim = runtime.settings.onChange(
        "enhanceSim",
        (enabled) => {
          if (!enabled) {
            clearHoverPanelContext(null, "enhancement");
            runtime.api.hideEnhancementCostPanel?.();
          }
        }
      );
      scope.add(() => {
        stopRequireKey?.();
        stopIronCow?.();
        stopLootEstimate?.();
        stopEnhanceSim?.();
        tooltipObserver.disconnect();
        for (const style of styles) style?.remove?.();
        clearTimeout(touchHoverPanelPress?.timer);
        touchHoverPanelPress = null;
        hoverPanelShortcutHeld = false;
        clearHoverPanelContext();
        runtime.api.hideProductionProfitPanel?.();
        runtime.api.hideEnhancementCostPanel?.();
      });
    }
  });
  runtime.features.register({
    id: "itemTooltip_profit",
    setting: "itemTooltip_profit",
    dependsOn: ["itemTooltip_prices"],
    initialize({ scope }) {
      scope.event(document, "mouseover", (event) => {
        const card = gatheringCardFromEventTarget(event.target);
        if (!card || card.contains(event.relatedTarget)) return;
        const actionHrid = resolveGatheringActionFromElement(card);
        if (!actionHrid) return;
        setHoverPanelContext({ kind: "profit", anchor: card, actionHrid });
      });
      scope.event(document, "mouseout", (event) => {
        const card = gatheringCardFromEventTarget(event.target);
        if (!card || card.contains(event.relatedTarget)) return;
        clearHoverPanelContext(card, "profit");
      });
      scope.add(() => {
        clearHoverPanelContext(null, "profit");
        runtime.api.hideProductionProfitPanel?.("profit");
      });
    }
  });
  runtime.onMessage("init_character_data", () => {
    clearHoverPanelContext();
    runtime.api.hideProductionProfitPanel?.();
    runtime.api.hideEnhancementCostPanel?.();
    if (runtime.api.shouldSuppressMarketFeatures?.()) {
      removeSuppressedTooltipContent();
    }
  });

  // src/features/action-panel.js
  var ACTION_PANEL_STYLE_ID = "mwitools-action-panel-style";
  var EFFICIENCY_BUFF_TYPE = "/buff_types/efficiency";
  var ACTION_LEVEL_BUFF_TYPE = "/buff_types/action_level";
  var ACTION_PANEL_SELECTOR = 'div[class*="SkillActionDetail_regularComponent"],div[class*="SkillActionDetail_skillActionDetail"]';
  var ACTION_PANEL_RETRY_DELAYS = [0, 100, 300, 1e3];
  var actionPanelRetryStates = /* @__PURE__ */ new Map();
  var targetLevelSelections = /* @__PURE__ */ new Map();
  function addActionPanelStyles() {
    if (document.getElementById(ACTION_PANEL_STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = ACTION_PANEL_STYLE_ID;
    style.textContent = `
    .mwi-level-progress { width:100%; max-width:100%; min-width:0; box-sizing:border-box; contain:inline-size; margin-top:6px; padding:6px 8px; border:1px solid rgba(255,255,255,.12); border-radius:5px; background:rgba(255,255,255,.025); color:var(--color-text-primary,#eee); font-size:calc(.6875rem * var(--mwi-ui-font-scale,1)); line-height:1.35; }
    .mwi-level-progress-row { display:flex; align-items:center; gap:6px; min-width:0; }
    .mwi-level-progress-label { flex:0 0 auto; color:var(--color-text-secondary,#aaa); }
    .mwi-target-level-input { width:48px!important; min-width:48px!important; height:23px!important; padding:1px 4px!important; border-radius:3px!important; font:inherit!important; text-align:center; }
    .mwi-level-progress-result { min-width:0; margin-left:auto; text-align:right; font-weight:600; overflow-wrap:anywhere; }
    .mwi-level-meta { margin-top:3px; color:var(--color-text-secondary,#aaa); font-size:calc(.6875rem * var(--mwi-ui-font-scale,1)); }
    .mwi-native-level-stat { font:inherit; }
    @media(max-width:520px){.mwi-level-progress-row{align-items:flex-start;flex-wrap:wrap}.mwi-level-progress-result{width:100%;text-align:left}}
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  async function handleActionPanel(panel) {
    if (!runtime.settings.settingsMap.actionPanel_totalTime.isTrue) return false;
    if (panel.dataset.mwitoolsActionPanel === "true" && panel.querySelector("#mwi-level-progress") && panel.querySelectorAll(".mwi-native-level-stat").length === 4)
      return true;
    const expElement = panel.querySelector(
      'div[class*="SkillActionDetail_expGain"]'
    );
    const inputElem = panel.querySelector(
      'div[class*="SkillActionDetail_maxActionCountInput"] input'
    );
    if (!expElement || !inputElem) return false;
    const activeContext = runtime.api.resolveActiveProductionPanelContext?.();
    if (activeContext?.panel && activeContext.panel !== panel) return false;
    const actionHrid = activeContext?.panel === panel ? activeContext.actionHrid : runtime.api.resolveProductionAction?.(panel);
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    const duration = runtime.api.getProductionPanelDuration?.(panel);
    if (!detail || !Number.isFinite(duration) || duration <= 0) return false;
    const exp = Number(
      String(runtime.api.getOriTextFromElement(expElement) ?? "").replaceAll(runtime.config.THOUSAND_SEPERATOR, "").replaceAll(runtime.config.DECIMAL_SEPERATOR, ".")
    );
    if (!Number.isFinite(exp) || exp <= 0) return false;
    const efficiencyDetails = getActionEfficiencyDetails(actionHrid);
    const effBuff = 1 + efficiencyDetails.total / 100;
    const skillHrid = detail.experienceGain?.skillHrid;
    let currentExp = null;
    let currentLevel = null;
    for (const skill of runtime.state.initData_characterSkills) {
      if (skill.skillHrid === skillHrid) {
        currentExp = skill.experience;
        currentLevel = skill.level;
        break;
      }
    }
    const infoContainer = panel.querySelector(
      'div[class*="SkillActionDetail_info"]'
    );
    const nativeLabel = infoContainer?.querySelector(
      'div[class*="SkillActionDetail_label"]'
    );
    const nativeValue = infoContainer?.querySelector(
      'div[class*="SkillActionDetail_value"]'
    );
    if (currentExp === null || currentLevel === null || !infoContainer || !nativeLabel || !nativeValue) {
      return false;
    }
    panel.querySelector("#mwi-level-progress")?.remove();
    panel.querySelectorAll(".mwi-native-level-stat").forEach((element) => element.remove());
    delete panel.dataset.mwitoolsActionPanel;
    addActionPanelStyles();
    if (currentExp !== null && currentLevel !== null) {
      const calculateNeedToLevel = (currentLevel2, targetLevel, effBuff2, duration2, exp2) => {
        let needTotalTimeSec = 0;
        let needTotalNumOfActions = 0;
        for (let level = currentLevel2; level < targetLevel; level++) {
          let needExpToNextLevel = null;
          if (!Number.isFinite(
            runtime.state.initData_levelExperienceTable?.[level + 1]
          )) {
            return null;
          }
          if (level === currentLevel2) {
            needExpToNextLevel = runtime.state.initData_levelExperienceTable[level + 1] - currentExp;
          } else {
            needExpToNextLevel = runtime.state.initData_levelExperienceTable[level + 1] - runtime.state.initData_levelExperienceTable[level];
          }
          const extraLevelEffBuff = (level - currentLevel2) * (1 + Number(efficiencyDetails.skillLevelRatio || 0)) * 0.01;
          const needNumOfActionsToNextLevel = Math.ceil(needExpToNextLevel / exp2);
          needTotalNumOfActions += needNumOfActionsToNextLevel;
          needTotalTimeSec += needNumOfActionsToNextLevel / (effBuff2 + extraLevelEffBuff) * duration2;
        }
        return { numOfActions: needTotalNumOfActions, timeSec: needTotalTimeSec };
      };
      const maxLevel = Math.min(
        200,
        (runtime.state.initData_levelExperienceTable?.length ?? 201) - 1
      );
      const levelCard = document.createElement("section");
      levelCard.id = "mwi-level-progress";
      levelCard.className = "mwi-level-progress";
      const row = document.createElement("div");
      row.className = "mwi-level-progress-row";
      const label = document.createElement("span");
      label.className = "mwi-level-progress-label";
      label.textContent = runtime.config.isZH ? "目标等级" : "Target level";
      const tillLevelInput = document.createElement("input");
      tillLevelInput.id = "tillLevelInput";
      tillLevelInput.type = "number";
      tillLevelInput.min = String(currentLevel + 1);
      tillLevelInput.max = String(maxLevel);
      const savedTargetLevel = Number(targetLevelSelections.get(actionHrid));
      const initialTargetLevel = Number.isSafeInteger(savedTargetLevel) && savedTargetLevel > currentLevel && savedTargetLevel <= maxLevel ? savedTargetLevel : currentLevel + 1;
      tillLevelInput.value = String(initialTargetLevel);
      tillLevelInput.className = `${inputElem.className} mwi-target-level-input`;
      const tillLevelNumber = document.createElement("span");
      tillLevelNumber.id = "tillLevelNumber";
      tillLevelNumber.className = "mwi-level-progress-result";
      row.append(label, tillLevelInput, tillLevelNumber);
      levelCard.append(row);
      const addNativeStat = (id, labelText, valueText) => {
        const statLabel = document.createElement("div");
        statLabel.className = `${nativeLabel.className} mwi-native-level-stat`;
        statLabel.textContent = labelText;
        const statValue = document.createElement("div");
        statValue.id = id;
        statValue.className = `${nativeValue.className} mwi-native-level-stat`;
        statValue.textContent = valueText;
        infoContainer.append(statLabel, statValue);
      };
      addNativeStat(
        "expPerHour",
        runtime.config.isZH ? "经验/小时" : "XP/hour",
        runtime.api.numberFormatter(
          Math.round(3600 / duration * exp * effBuff)
        )
      );
      addNativeStat(
        "currentEfficiency",
        runtime.config.isZH ? "当前效率" : "Efficiency",
        `+${Number((effBuff - 1) * 100).toFixed(1)}%`
      );
      if (runtime.api.mountProductionModule) {
        runtime.api.mountProductionModule(panel, levelCard, "targetLevel");
      } else {
        const anchor = panel.querySelector(
          'div[class*="SkillActionDetail_actionContainer"]'
        ) ?? inputElem.parentElement;
        anchor.insertAdjacentElement("afterend", levelCard);
      }
      let targetLevelEdited = false;
      const updateTargetLevel = () => {
        const targetLevel = Number(tillLevelInput.value);
        if (targetLevel > currentLevel && targetLevel <= maxLevel) {
          const need = calculateNeedToLevel(
            currentLevel,
            targetLevel,
            effBuff,
            duration,
            exp
          );
          if (need) {
            tillLevelNumber.textContent = runtime.config.isZH ? `还需 ${runtime.api.numberFormatter(need.numOfActions)} 次 · 预计 ${runtime.api.timeReadable(need.timeSec)}` : `${runtime.api.numberFormatter(need.numOfActions)} actions · ${runtime.api.timeReadable(need.timeSec)}`;
            if (targetLevelEdited) {
              reactInputTriggerHack(inputElem, String(need.numOfActions));
            }
          }
        } else {
          tillLevelNumber.textContent = runtime.config.isZH ? `请输入 ${currentLevel + 1}–${maxLevel}` : `Enter ${currentLevel + 1}–${maxLevel}`;
        }
      };
      tillLevelInput.addEventListener("input", () => {
        targetLevelEdited = true;
        const targetLevel = Number(tillLevelInput.value);
        if (Number.isSafeInteger(targetLevel) && targetLevel > currentLevel && targetLevel <= maxLevel) {
          targetLevelSelections.set(actionHrid, targetLevel);
        }
        updateTargetLevel();
      });
      updateTargetLevel();
    }
    panel.dataset.mwitoolsActionPanel = "true";
    if ((panel.querySelector('div[class*="SkillActionDetail_dropTable"]')?.children.length ?? 0) > 1 && runtime.settings.settingsMap.actionPanel_foragingTotal.isTrue && !runtime.api.shouldSuppressMarketFeatures?.()) {
      const projection = runtime.api.projectAction?.(actionHrid, 1);
      const profitPerHour = projection?.valuations?.conservative?.profitPerHour;
      if (!Number.isFinite(profitPerHour)) return true;
      const profitPerDay = 24 * profitPerHour;
      const htmlStr = `<div id="totalProfit" class="mwi-level-meta">${runtime.config.isZH ? "综合利润: " : "Overall profit: "}<span class="mwi-number" title="${runtime.api.formatExactNumber(profitPerHour)}">${runtime.api.numberFormatter(profitPerHour)}</span>${runtime.config.isZH ? "/小时" : "/hour"}, <span class="mwi-number" title="${runtime.api.formatExactNumber(profitPerDay)}">${runtime.api.numberFormatter(profitPerDay)}</span>${runtime.config.isZH ? "/天" : "/day"}</div>`;
      panel.querySelector("#mwi-level-progress")?.insertAdjacentHTML("beforeend", htmlStr);
    }
    return true;
  }
  function scheduleActionPanel(panel) {
    if (!panel?.isConnected || actionPanelRetryStates.has(panel)) return;
    const state = { attempt: 0, timer: null };
    actionPanelRetryStates.set(panel, state);
    const run = async () => {
      state.timer = null;
      if (!panel.isConnected) {
        actionPanelRetryStates.delete(panel);
        return;
      }
      let ready2 = false;
      try {
        ready2 = await handleActionPanel(panel);
      } catch (error) {
        console.info("[MWITools] Action panel enhancement unavailable", error);
      }
      if (ready2 || state.attempt >= ACTION_PANEL_RETRY_DELAYS.length - 1) {
        actionPanelRetryStates.delete(panel);
        return;
      }
      state.attempt += 1;
      state.timer = setTimeout(run, ACTION_PANEL_RETRY_DELAYS[state.attempt]);
    };
    state.timer = setTimeout(run, ACTION_PANEL_RETRY_DELAYS[0]);
  }
  function refreshProductionActionPanel(panel) {
    if (!runtime.settings.settingsMap.actionPanel_totalTime.isTrue || !panel?.isConnected) {
      return false;
    }
    if (!panel.querySelector("#mwi-level-progress") || panel.querySelectorAll(".mwi-native-level-stat").length !== 4) {
      delete panel.dataset.mwitoolsActionPanel;
    }
    scheduleActionPanel(panel);
    return true;
  }
  function clearActionPanelRetries() {
    for (const state of actionPanelRetryStates.values()) {
      if (state.timer !== null) clearTimeout(state.timer);
    }
    actionPanelRetryStates.clear();
  }
  function sumBuffValue(buffs, typeHrid) {
    return (buffs ?? []).reduce(
      (total, buff) => {
        if (buff?.typeHrid !== typeHrid) return total;
        total.ratioBoost += Number(buff.ratioBoost) || 0;
        total.flatBoost += Number(buff.flatBoost) || 0;
        return total;
      },
      { ratioBoost: 0, flatBoost: 0 }
    );
  }
  function isTaskAction(actionHrid) {
    return (runtime.state.characterQuests ?? []).some((quest) => {
      if (quest?.actionHrid !== actionHrid) return false;
      const status = String(quest.status ?? "").toLowerCase();
      return !(quest.isClaimed || quest.claimed || status.includes("claimed") || status.includes("completed"));
    });
  }
  function getAuthoritativeActionBuffs(actionHrid) {
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    const sources = runtime.state.actionTypeBuffSources;
    if (!detail || !sources) return null;
    const buffs = [];
    for (const sourceMap of Object.values(sources)) {
      const actionTypeBuffs = sourceMap?.[detail.type];
      if (Array.isArray(actionTypeBuffs)) buffs.push(...actionTypeBuffs);
    }
    if (isTaskAction(actionHrid)) {
      buffs.push(...runtime.state.equipmentTaskActionBuffs ?? []);
    }
    return buffs;
  }
  function supportsLevelEfficiency(detail) {
    const actionFunction = String(detail?.function ?? "").toLowerCase();
    if (actionFunction) {
      return actionFunction.includes("gathering") || actionFunction.includes("production");
    }
    return Boolean(
      detail?.levelRequirement && (detail?.dropTable || detail?.outputItems)
    );
  }
  function getAuthoritativeEfficiency(actionHrid, buffs) {
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    const directEfficiency = sumBuffValue(buffs, EFFICIENCY_BUFF_TYPE).flatBoost * 100;
    let levelEfficiency = 0;
    let boostedSkillLevel = null;
    let requiredLevel = null;
    let skillLevelRatio = 0;
    if (supportsLevelEfficiency(detail) && detail?.levelRequirement) {
      const skillHrid = detail.levelRequirement.skillHrid;
      const skill = (runtime.state.initData_characterSkills ?? []).find(
        (candidate) => candidate.skillHrid === skillHrid
      );
      const baseSkillLevel = Number(skill?.level);
      if (Number.isFinite(baseSkillLevel)) {
        const skillName = String(skillHrid).split("/").pop();
        const levelBuff = sumBuffValue(buffs, `/buff_types/${skillName}_level`);
        skillLevelRatio = levelBuff.ratioBoost;
        boostedSkillLevel = (1 + levelBuff.ratioBoost) * baseSkillLevel + levelBuff.flatBoost;
        requiredLevel = Number(detail.levelRequirement.level || 0) + sumBuffValue(buffs, ACTION_LEVEL_BUFF_TYPE).flatBoost;
        levelEfficiency = Math.max(0, boostedSkillLevel - requiredLevel);
      }
    }
    return {
      source: "game",
      total: directEfficiency + levelEfficiency,
      directEfficiency,
      levelEfficiency,
      boostedSkillLevel,
      requiredLevel,
      skillLevelRatio
    };
  }
  function getLegacyEfficiency(actionHrid) {
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    if (!detail?.levelRequirement) {
      return { source: "legacy", total: 0, levelEfficiency: 0 };
    }
    const requiredLevel = Number(detail.levelRequirement.level) || 0;
    const currentLevel = Number(
      (runtime.state.initData_characterSkills ?? []).find(
        (skill) => skill.skillHrid === detail.levelRequirement.skillHrid
      )?.level ?? requiredLevel
    );
    const levelEfficiency = Math.max(0, currentLevel - requiredLevel);
    const houseEfficiency = Number(runtime.api.getHousesEffBuffByActionHrid?.(actionHrid)) || 0;
    const teaEfficiency = Number(runtime.api.getTeaBuffsByActionHrid?.(actionHrid)?.efficiency) || 0;
    const equipmentEfficiency = Number(runtime.api.getItemEffiBuffByActionHrid?.(actionHrid)) || 0;
    return {
      source: "legacy",
      total: levelEfficiency + houseEfficiency + teaEfficiency + equipmentEfficiency,
      levelEfficiency,
      houseEfficiency,
      teaEfficiency,
      equipmentEfficiency
    };
  }
  function getActionEfficiencyDetails(actionHrid) {
    const buffs = getAuthoritativeActionBuffs(actionHrid);
    return buffs ? getAuthoritativeEfficiency(actionHrid, buffs) : getLegacyEfficiency(actionHrid);
  }
  function getTotalEffiPercentage(actionHrid, debug = false) {
    const details = getActionEfficiencyDetails(actionHrid);
    if (debug) {
      console.log(
        runtime.config.isZH ? "[MWITools] 行动总效率明细" : "[MWITools] Total action efficiency details",
        actionHrid,
        details
      );
    }
    return Number.isFinite(details.total) ? details.total : 0;
  }
  function getTotalTimeStr(input, duration, effBuff) {
    if (input === "∞") {
      return "[∞]";
    } else if (isNaN(input)) {
      return "Error";
    }
    return "[" + runtime.api.timeReadable(Math.round(input / effBuff) * duration) + "]";
  }
  function reactInputTriggerHack(inputElem, value) {
    const lastValue = inputElem.value;
    inputElem.value = value;
    const EventConstructor = inputElem.ownerDocument?.defaultView?.Event ?? Event;
    const event = new EventConstructor("input", { bubbles: true });
    event.simulated = true;
    const tracker = inputElem._valueTracker;
    if (tracker) {
      tracker.setValue(lastValue);
    }
    inputElem.dispatchEvent(event);
  }
  var waitForProgressBar = () => {
    const elements = document.querySelectorAll(
      ".NavigationBar_currentExperience__3GDeX"
    );
    if (elements.length) {
      removeInsertedDivs();
      elements.forEach((element) => {
        let text = element.style.width;
        text = Number(text.replace("%", "")).toFixed(2) + "%";
        const span = document.createElement("span");
        span.textContent = text;
        span.classList.add("insertedSpan");
        span.style.fontSize = "0.875rem";
        span.style.color = runtime.config.SCRIPT_COLOR_MAIN;
        element.parentNode.parentNode.querySelector(
          "span.NavigationBar_level__3C7eR"
        ).style.width = "auto";
        const insertParent = element.parentNode.parentNode.children[0];
        insertParent.insertBefore(span, insertParent.children[1]);
      });
    }
  };
  var removeInsertedDivs = () => document.querySelectorAll("span.insertedSpan").forEach((div) => div.parentNode.removeChild(div));
  Object.assign(runtime.api, {
    handleActionPanel,
    refreshProductionActionPanel,
    getTotalEffiPercentage,
    getActionEfficiencyDetails,
    getTotalTimeStr,
    reactInputTriggerHack,
    waitForProgressBar,
    removeInsertedDivs
  });
  runtime.features.register({
    id: "expPercentage",
    setting: "expPercentage",
    initialize({ scope }) {
      waitForProgressBar();
      const render = () => {
        removeInsertedDivs();
        waitForProgressBar();
      };
      const scheduler = createFrameScheduler(render);
      const observer = new MutationObserver((records) => {
        const relevant = records.some((record) => {
          const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
          if (target?.closest?.(".insertedSpan")) return false;
          return Boolean(
            target?.matches?.(".NavigationBar_currentExperience__3GDeX") || target?.closest?.(".NavigationBar_currentExperience__3GDeX") || [...record.addedNodes, ...record.removedNodes].some(
              (node) => node?.nodeType === 1 && (node.matches?.(".NavigationBar_currentExperience__3GDeX") || node.querySelector?.(
                ".NavigationBar_currentExperience__3GDeX"
              ))
            )
          );
        });
        if (relevant) scheduler.schedule();
      });
      scope.observer(observer, document.body, {
        attributes: true,
        attributeFilter: ["style"],
        childList: true,
        subtree: true
      });
      scope.add(() => scheduler.cancel());
      scope.add(removeInsertedDivs);
    }
  });
  runtime.settings.onChange?.("adaptIronCowMarketFeatures", () => {
    for (const panel of document.querySelectorAll(
      'div[class*="SkillActionDetail_regularComponent"]'
    )) {
      delete panel.dataset.mwitoolsActionPanel;
      void handleActionPanel(panel);
    }
  });
  runtime.features.register({
    id: "actionPanel_totalTime",
    setting: "actionPanel_totalTime",
    scope: "character",
    initialize({ scope }) {
      targetLevelSelections.clear();
      const observer = new MutationObserver((mutations) => {
        const panels = /* @__PURE__ */ new Set();
        for (const mutation of mutations) {
          const mutationTarget = mutation.target?.nodeType === 1 ? mutation.target : mutation.target?.parentElement;
          const containingPanel = mutationTarget?.closest?.(
            ACTION_PANEL_SELECTOR
          );
          if (containingPanel) panels.add(containingPanel);
          for (const added of mutation.addedNodes) {
            if (added?.matches?.(ACTION_PANEL_SELECTOR)) panels.add(added);
            added?.querySelectorAll?.(ACTION_PANEL_SELECTOR).forEach((panel) => panels.add(panel));
          }
        }
        panels.forEach(scheduleActionPanel);
      });
      scope.observer(observer, document.body, {
        childList: true,
        characterData: true,
        subtree: true
      });
      document.querySelectorAll(ACTION_PANEL_SELECTOR).forEach(scheduleActionPanel);
      const refreshPanels = () => {
        document.querySelectorAll(ACTION_PANEL_SELECTOR).forEach((panel) => {
          delete panel.dataset.mwitoolsActionPanel;
          scheduleActionPanel(panel);
        });
      };
      for (const messageType of [
        "items_updated",
        "skills_updated",
        "house_rooms_updated",
        "achievement_buffs_updated",
        "moo_pass_buffs_updated",
        "community_buffs_updated",
        "consumable_buffs_updated",
        "action_type_consumable_slots_updated",
        "equipment_buffs_updated",
        "personal_buffs_updated",
        "guild_buffs_updated",
        "abilities_updated",
        "character_abilities_updated"
      ]) {
        scope.add(
          runtime.onMessage(messageType, () => {
            refreshPanels();
            if (messageType === "abilities_updated" || messageType === "character_abilities_updated") {
              scope.timeout(refreshPanels, 100);
              scope.timeout(refreshPanels, 300);
            }
          })
        );
      }
      scope.add(() => {
        clearActionPanelRetries();
        targetLevelSelections.clear();
        document.querySelectorAll(
          "#showTotalTime,#quickInputHourButtons,#quickInputCountButtons,#mwi-level-progress,#tillLevel,#expPerHour,#currentEfficiency,#totalProfit,.mwi-native-level-stat"
        ).forEach((node) => node.remove());
        document.querySelectorAll('[data-mwitools-action-panel="true"]').forEach((panel) => delete panel.dataset.mwitoolsActionPanel);
      });
    }
  });

  // src/core/time-format.js
  function formatRemainingDuration(seconds, isZH3 = true) {
    if (seconds === Infinity) return "∞";
    if (!Number.isFinite(seconds)) return "—";
    const normalized = Math.max(0, Math.round(seconds));
    const units = [
      [86400, "天", "d"],
      [3600, "小时", "h"],
      [60, "分", "m"],
      [1, "秒", "s"]
    ];
    let remainder = normalized;
    const parts = [];
    for (const [size, zh, en] of units) {
      const value = Math.floor(remainder / size);
      remainder %= size;
      if (value > 0 || size === 1 && parts.length === 0) {
        parts.push(`${value}${isZH3 ? zh : en}`);
      }
    }
    return parts.join(isZH3 ? "" : " ");
  }
  function format24HourClock(timestamp, isZH3 = true) {
    if (!Number.isFinite(timestamp)) return "—";
    return new Intl.DateTimeFormat(isZH3 ? "zh-CN" : "en-US", {
      hour: "2-digit",
      hourCycle: "h23",
      minute: "2-digit",
      second: "2-digit"
    }).format(new Date(timestamp));
  }
  function calendarDayOffset(fromTimestamp, toTimestamp) {
    const from = new Date(fromTimestamp);
    const to = new Date(toTimestamp);
    if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) return 0;
    const fromDay = Date.UTC(from.getFullYear(), from.getMonth(), from.getDate());
    const toDay = Date.UTC(to.getFullYear(), to.getMonth(), to.getDate());
    return Math.round((toDay - fromDay) / 864e5);
  }
  function formatRemainingTiming(totalSeconds, finishAt, { isZH: isZH3 = true, now = Date.now() } = {}) {
    const duration = formatRemainingDuration(totalSeconds, isZH3);
    if (!Number.isFinite(finishAt)) return duration;
    const clock = format24HourClock(finishAt, isZH3);
    const dayOffset = Math.max(0, calendarDayOffset(now, finishAt));
    const finish = isZH3 ? `(${clock})` : ` (${clock})`;
    if (dayOffset === 0) return `${duration}${finish}`;
    const offset = isZH3 ? `(+${dayOffset}天)` : ` (+${dayOffset} ${dayOffset === 1 ? "day" : "days"})`;
    return `${duration}${finish}${offset}`;
  }

  // src/features/action-dashboard.js
  var PRODUCTION_PROFILE_MESSAGES = Object.freeze([
    "init_character_data",
    "items_updated",
    "skills_updated",
    "house_rooms_updated",
    "achievement_buffs_updated",
    "moo_pass_buffs_updated",
    "community_buffs_updated",
    "consumable_buffs_updated",
    "action_type_consumable_slots_updated",
    "equipment_buffs_updated",
    "personal_buffs_updated",
    "guild_buffs_updated",
    "abilities_updated",
    "character_abilities_updated"
  ]);
  var PRODUCTION_PANEL_REBUILD_MESSAGES = /* @__PURE__ */ new Set([
    "abilities_updated",
    "character_abilities_updated"
  ]);
  var STYLE_ID8 = "mwitools-action-dashboard-style";
  var DEFAULT_QUICK_HOURS = [0.5, 1, 2, 3, 4, 5, 6, 10, 12, 24];
  var DEFAULT_QUICK_COUNTS = [10, 100, 300, 500, 1e3, 2e3];
  var ACTION_SURFACE_SELECTOR = 'div[class*="Header_actionName"],div[class*="SkillActionDetail_regularComponent"],div[class*="SkillActionDetail_skillActionDetail"]';
  var OWNED_ACTION_UI_SELECTOR = "#mwi-action-dashboard,#mwi-production-summary,.mwi-production-quick-inputs,.mwi-max-action-button,.mwi-production-duration-inline,.mwi-production-extensions";
  var PRODUCTION_MODULE_ORDER = Object.freeze({
    quickInputs: 10,
    summary: 20,
    shortage: 30,
    targetLevel: 40
  });
  var PRODUCTION_RECOVERY_DELAYS = Object.freeze([0, 80, 220, 450, 900, 1500]);
  var PRODUCTION_RECOVERY_MODULE_SELECTOR = ".mwi-production-extensions,#mwi-production-summary,.mwi-production-quick-inputs,#mwitools-procurement-production,#mwi-level-progress,.mwi-max-action-button,.mwi-production-duration-inline";
  var PRODUCTION_DROPDOWN_SELECTOR = '[role="listbox"],[class*="MuiPopover-root"],[class*="MuiMenu-root"]';
  var productionDataRevision = 0;
  var enhancementTimingCache = { identity: "", count: null };
  var productionRecoveryGeneration = 0;
  var productionRecoveryTimers = /* @__PURE__ */ new Set();
  function t8(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function formatDuration(seconds) {
    if (seconds === Infinity) return "∞";
    if (!Number.isFinite(seconds)) return "—";
    const normalized = Math.max(0, Math.round(seconds));
    if (normalized < 86400) {
      return runtime.api.timeReadable?.(normalized) || `${normalized}s`;
    }
    const days = Math.floor(normalized / 86400);
    const hours = Math.floor(normalized % 86400 / 3600);
    const minutes = Math.floor(normalized % 3600 / 60);
    const parts = [t8(`${days}天`, `${days}d`)];
    if (hours > 0) parts.push(t8(`${hours}小时`, `${hours}h`));
    if (minutes > 0) parts.push(t8(`${minutes}分`, `${minutes}m`));
    return parts.join(runtime.config.isZH ? "" : " ");
  }
  function number2(value) {
    return runtime.api.createFormattedNumber(value);
  }
  function outputItemName(itemHrid) {
    return itemName(itemHrid, { fallback: "?" });
  }
  function nativeProductionItem(panel, itemHrid, name) {
    const bare = String(itemHrid ?? "").split("/").at(-1);
    const candidates = [
      ...panel?.querySelectorAll(
        ':scope div[class*="SkillActionDetail_dropTable"] div[class*="Item_item"]'
      ) ?? []
    ].filter(
      (candidate) => [...candidate.classList].some(
        (className) => className.startsWith("Item_item__")
      )
    );
    const prototype = candidates.find((candidate) => {
      const href2 = candidate.querySelector("use")?.getAttribute("href") ?? candidate.querySelector("use")?.getAttribute("xlink:href") ?? "";
      return bare && href2.endsWith(`#${bare}`);
    }) ?? candidates[0];
    if (!prototype) return null;
    const item = prototype.cloneNode(true);
    item.classList.add("mwi-production-native-item");
    for (const className of [...item.classList]) {
      if (className.includes("Item_clickable")) item.classList.remove(className);
    }
    const href = getGameSpriteHref("items", itemHrid);
    const use = item.querySelector("use");
    if (use && href && bare) {
      use.setAttribute("href", href);
      use.setAttribute("xlink:href", href);
    }
    const svg = item.querySelector("svg");
    svg?.setAttribute("aria-label", name);
    const itemName4 = item.querySelector('[class*="Item_name"]');
    if (itemName4) itemName4.textContent = name;
    return item;
  }
  function fallbackProductionItem(itemHrid, name) {
    const item = document.createElement("span");
    item.className = "mwi-production-native-fallback";
    const bare = String(itemHrid ?? "").split("/").at(-1);
    const href = getGameSpriteHref("items", itemHrid);
    if (bare && href) {
      const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
      svg.classList.add("mwi-production-output-icon");
      svg.setAttribute("viewBox", "0 0 32 32");
      svg.setAttribute("aria-label", name);
      const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
      use.setAttribute("href", href);
      use.setAttribute("xlink:href", href);
      svg.append(use);
      item.append(svg);
    } else {
      const fallback = document.createElement("span");
      fallback.className = "mwi-production-output-fallback";
      fallback.setAttribute("aria-hidden", "true");
      fallback.textContent = "?";
      item.append(fallback);
    }
    const label = document.createElement("span");
    label.className = "mwi-production-output-name";
    label.textContent = name;
    item.append(label);
    return item;
  }
  function createProductionOutput(output, panel) {
    const item = document.createElement("span");
    item.className = "mwi-production-output-item";
    const name = outputItemName(output.itemHrid);
    const normalizedCount = Number.isFinite(Number(output.expectedCount)) ? Math.round(Number(output.expectedCount) * 1e9) / 1e9 : output.expectedCount;
    const exactCount = runtime.api.formatExactNumber?.(normalizedCount) ?? String(normalizedCount);
    item.title = `${name} ×${exactCount}`;
    item.setAttribute("aria-label", item.title);
    item.append(
      nativeProductionItem(panel, output.itemHrid, name) ?? fallbackProductionItem(output.itemHrid, name)
    );
    const count = document.createElement("span");
    count.className = "mwi-production-output-count";
    count.append("×", number2(output.expectedCount));
    item.append(count);
    return item;
  }
  function addStyles6() {
    if (document.getElementById(STYLE_ID8)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID8;
    const productionFont = runtime.config.isZH ? "inherit" : 'ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif';
    style.textContent = `
    .mwi-action-dashboard-host { position:relative!important; }
    .mwi-action-dashboard { position:absolute; top:50%; right:0; z-index:5; box-sizing:border-box; max-width:var(--mwi-action-dashboard-max-width,calc(100% - var(--mwi-action-dashboard-left,0px))); margin:0; padding:2px 6px; transform:translateY(-50%); border:1px solid rgba(255,255,255,.1); border-radius:4px; background:rgba(0,0,0,.18); font:inherit; font-size:inherit; line-height:1.25; white-space:normal; overflow:hidden; pointer-events:none; }
    .mwi-action-line { display:flex; align-items:center; flex-wrap:nowrap; gap:3px 10px; max-width:100%; color:#ffa500; }
    .mwi-action-line > * { min-width:0; white-space:nowrap; }
    .mwi-action-line strong { color:inherit; font-weight:650; }
    .mwi-action-dashboard[data-compact="true"] { right:auto; width:max-content; padding-inline:4px; }
    .mwi-action-dashboard[data-compact="true"] .mwi-action-line { gap:2px 6px; }
    .mwi-action-time { overflow:hidden; text-overflow:ellipsis; font-variant-numeric:tabular-nums; }
    .mwi-production-card { width:100%; max-width:100%; min-width:0; box-sizing:border-box; contain:inline-size; margin-top:6px; padding:6px; border:1px solid rgba(255,255,255,.12); border-radius:5px; background:rgba(255,255,255,.025); color:var(--color-text-primary,#eee); font-family:${productionFont}; font-size:calc(.6875rem * var(--mwi-ui-font-scale,1)); }
    .mwi-production-extensions { display:contents!important; }
    .mwi-production-extensions > * { flex:0 0 auto!important; align-self:stretch; min-height:0!important; height:auto!important; }
    .mwi-production-card-title { display:flex; width:100%; align-items:center; gap:6px; box-sizing:border-box; padding:0 2px 4px; border:0; background:transparent; color:inherit; font:inherit; font-size:calc(.75rem * var(--mwi-ui-font-scale,1)); font-weight:650; text-align:left; cursor:pointer; }
    .mwi-production-card-title::before { content:"▸"; color:var(--color-text-secondary,#aaa); transition:transform .12s ease; }
    .mwi-production-card[data-expanded="true"] .mwi-production-card-title::before { transform:rotate(90deg); }
    .mwi-production-card[data-mode="expanded"] .mwi-production-card-title { cursor:default; }
    .mwi-production-card[data-mode="expanded"] .mwi-production-card-title::before { content:""; }
    .mwi-production-card-body[hidden] { display:none; }
    .mwi-production-metrics { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,110px),1fr)); gap:4px; }
    .mwi-production-metric { min-width:0; overflow:hidden; padding:4px 3px; border-radius:3px; background:rgba(0,0,0,.14); text-align:center; }
    .mwi-production-label { min-height:1.45em; color:var(--color-text-secondary,#aaa); font-size:calc(.6875rem * var(--mwi-ui-font-scale,1)); line-height:1.2; }
    .mwi-production-value { margin-top:1px; font-size:calc(.72rem * var(--mwi-ui-font-scale,1)); line-height:1.25; font-weight:600; overflow-wrap:anywhere; }
    .mwi-production-output-metric { grid-column:1/-1; }
    .mwi-production-output-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,120px),1fr)); gap:4px 8px; width:100%; }
    .mwi-production-output-grid[data-count="1"] .mwi-production-output-item { grid-column:1/-1; }
    .mwi-production-output-item { display:flex; min-width:0; align-items:center; justify-content:center; gap:4px; overflow:hidden; }
    .mwi-production-native-item,.mwi-production-native-fallback { display:inline-flex!important; min-width:0; align-items:center; gap:4px; overflow:hidden; pointer-events:none; }
    .mwi-production-native-fallback { padding:1px 6px; border:1px solid rgb(152,167,233); border-radius:4px; background:rgb(44,46,69); color:rgb(231,231,231); }
    .mwi-production-native-item [class*="Item_iconContainer"] { width:14px!important; height:14px!important; flex:0 0 14px!important; }
    .mwi-production-native-item [class*="Item_name"],.mwi-production-output-name { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
    .mwi-production-output-icon,.mwi-production-output-fallback { display:grid; width:14px; height:14px; flex:0 0 14px; place-items:center; }
    .mwi-production-output-fallback { border-radius:4px; background:rgba(255,255,255,.08); color:var(--color-text-secondary,#aaa); font-size:calc(.72rem * var(--mwi-ui-font-scale,1)); }
    .mwi-production-output-count { flex:0 0 auto; min-width:0; font-size:calc(.72rem * var(--mwi-ui-font-scale,1)); font-weight:700; line-height:1; white-space:nowrap; }
    .mwi-production-warning { margin:4px 2px 0; color:#d7bb67; font-size:calc(.6875rem * var(--mwi-ui-font-scale,1)); line-height:1.25; }
    .mwi-max-action-button { margin-inline-start:4px; }
    .mwi-production-duration-inline { display:inline-flex; align-items:center; margin-inline-start:7px; padding:0; border:0; background:transparent; color:${runtime.config.SCRIPT_COLOR_MAIN}; font-family:${productionFont}; font-size:calc(.6875rem * var(--mwi-ui-font-scale,1)); line-height:1.2; white-space:nowrap; font-variant-numeric:tabular-nums; }
    .mwi-production-quick-inputs { position:relative; display:grid; z-index:0; box-sizing:border-box; gap:3px; width:100%; min-width:0; margin:4px 0 1px; color:var(--color-text-secondary,#aaa); font-size:calc(.6875rem * var(--mwi-ui-font-scale,1)); }
    .mwi-production-quick-row { display:flex; min-width:0; align-items:flex-start; gap:3px; }
    .mwi-production-quick-label { flex:0 0 3.25em; color:${runtime.config.SCRIPT_COLOR_MAIN}; white-space:nowrap; }
    .mwi-production-quick-buttons { display:flex; min-width:0; flex:1; flex-wrap:wrap; gap:2px; }
    .mwi-production-quick-button { min-width:0!important; height:21px!important; padding:1px 5px!important; font-size:calc(.6875rem * var(--mwi-ui-font-scale,1))!important; line-height:1!important; }
    @media(max-width:520px){.mwi-action-dashboard{right:auto;width:max-content;padding-inline:4px}.mwi-action-line{gap:2px 6px}.mwi-production-card{padding:5px}.mwi-production-card-title{padding-bottom:3px}.mwi-production-metrics,.mwi-production-output-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:3px}.mwi-production-metric{padding:3px 2px}.mwi-production-label{min-height:1.3em}.mwi-production-output-grid[data-count="1"] .mwi-production-output-item{grid-column:1/-1}.mwi-production-output-item{gap:3px}}
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function getLiveActionTiming(host) {
    const currentAction = host?.closest?.('[class*="Header_currentAction"]') ?? host?.parentElement;
    const bar = currentAction?.querySelector?.(
      '[class*="ProgressBar_progressBar"]'
    );
    if (!bar) return { durationPerAction: null, currentCycleRemaining: null };
    let durationPerAction = Number(bar.style?.getPropertyValue?.("--duration"));
    if (!Number.isFinite(durationPerAction) || durationPerAction <= 0) {
      const text = runtime.api.getOriTextFromElement?.(
        bar.querySelector('[class*="ProgressBar_text"]')
      );
      const match2 = String(text ?? "").match(/[\d.,\s\u00a0\u202f]+/);
      durationPerAction = match2 ? parseCompactNumber(match2[0]) : null;
    }
    if (!Number.isFinite(durationPerAction) || durationPerAction <= 0) {
      return { durationPerAction: null, currentCycleRemaining: null };
    }
    const active = bar.querySelector('[class*="ProgressBar_active"]');
    const transform = active ? active.ownerDocument?.defaultView?.getComputedStyle(active).transform ?? active.style?.transform : null;
    const match = String(transform ?? "").match(/^matrix(?:3d)?\(\s*(-?[\d.]+)/);
    const progress = match ? Number(match[1]) : null;
    const currentCycleRemaining = Number.isFinite(progress) ? durationPerAction * (1 - Math.min(1, Math.max(0, progress))) : durationPerAction;
    return { durationPerAction, currentCycleRemaining };
  }
  function getNativeEnhancementCount(host, action) {
    if (!String(action?.actionHrid ?? "").includes("/enhancing")) return null;
    const matches = [
      ...nativeActionText(host).matchAll(/[((]\s*([\d\s.,]+)\s*[))]/g)
    ];
    const match = matches.at(-1);
    if (!match) return null;
    const count = parseCompactNumber(match[1]);
    return Number.isSafeInteger(count) && count >= 0 ? count : null;
  }
  function parseProductionDurationSeconds(value) {
    const token = String(value ?? "").match(/[-+]?\d[\d\s\u00a0\u202f.,]*/)?.[0];
    if (!token) return null;
    let normalized = token.replace(/[\s\u00a0\u202f]/g, "");
    const dot = normalized.lastIndexOf(".");
    const comma = normalized.lastIndexOf(",");
    const decimalIndex = Math.max(dot, comma);
    if (decimalIndex >= 0) {
      const whole = normalized.slice(0, decimalIndex).replace(/[.,]/g, "");
      const fraction = normalized.slice(decimalIndex + 1).replace(/[.,]/g, "");
      normalized = fraction ? `${whole}.${fraction}` : whole;
    }
    const number3 = Number(normalized);
    return Number.isFinite(number3) && number3 > 0 ? number3 : null;
  }
  function parseProductionQuickPresets(value, { integer = false, fallback = [] } = {}) {
    const result = [];
    const seen = /* @__PURE__ */ new Set();
    for (const token of String(value ?? "").split(/[,;\s]+/)) {
      if (!token) continue;
      const number3 = Number(token);
      if (!Number.isFinite(number3) || number3 <= 0 || integer && !Number.isSafeInteger(number3)) {
        continue;
      }
      const key = String(number3);
      if (seen.has(key)) continue;
      seen.add(key);
      result.push(number3);
    }
    return result.length ? result : [...fallback];
  }
  function productionQuickPresets() {
    return {
      hours: parseProductionQuickPresets(
        runtime.settings.getPreference("productionQuickHours"),
        { fallback: DEFAULT_QUICK_HOURS }
      ),
      counts: parseProductionQuickPresets(
        runtime.settings.getPreference("productionQuickCounts"),
        { integer: true, fallback: DEFAULT_QUICK_COUNTS }
      )
    };
  }
  function getProductionPanelDuration(panel) {
    for (const value of panel?.querySelectorAll(
      'div[class*="SkillActionDetail_value"]'
    ) ?? []) {
      const text = String(
        runtime.api.getOriTextFromElement?.(value) ?? ""
      ).trim();
      if (!/s\s*$/i.test(text)) continue;
      const duration = parseProductionDurationSeconds(text);
      if (duration) return duration;
    }
    return null;
  }
  function clearActionDashboard() {
    document.querySelector("#mwi-action-dashboard")?.remove();
    document.querySelectorAll(".mwi-action-dashboard-host").forEach(
      (element) => element.classList.remove("mwi-action-dashboard-host")
    );
    enhancementTimingCache = { identity: "", count: null };
  }
  function nativeActionText(host) {
    return [...host?.childNodes ?? []].filter(
      (node) => node.nodeType !== 1 || node.id !== "mwi-action-dashboard" && node.id !== "script_item_warning"
    ).map((node) => node.textContent ?? "").join(" ").replaceAll(/\s+/g, " ").trim();
  }
  function normalizedActionText(value) {
    return String(value ?? "").replaceAll(/\([^)]*\)\s*$/g, "").replaceAll(/\s+/g, " ").trim().toLocaleLowerCase();
  }
  function actionHeaderNames(actionHrid, detail) {
    const names = /* @__PURE__ */ new Set([
      detail?.name,
      getLocalizedEntityName("action", actionHrid)
    ]);
    for (const output of runtime.api.getExpectedOutputs?.(detail) ?? []) {
      names.add(runtime.state.initData_itemDetailMap?.[output.itemHrid]?.name);
      names.add(getLocalizedEntityName("item", output.itemHrid));
    }
    return [...names].map(normalizedActionText).filter(Boolean);
  }
  function actionMatchesHeader(action, host) {
    const actionHrid = action?.actionHrid;
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    if (!actionHrid || !detail || detail.type === "/action_types/combat") {
      return false;
    }
    const header = normalizedActionText(nativeActionText(host));
    if (!header || /^(doing nothing|无事可做|没有行动)$/.test(header)) {
      return false;
    }
    if (resolveLocalizedEntity("action", header) === actionHrid) return true;
    if (String(actionHrid).includes("/enhancing")) {
      return getNativeEnhancementCount(host, action) !== null || /\+\s*\d+/.test(header) || actionHeaderNames(actionHrid, detail).some(
        (name) => header.includes(name)
      );
    }
    return actionHeaderNames(actionHrid, detail).some(
      (name) => header === name || header.includes(name)
    );
  }
  function rectWidth(rect) {
    return Math.max(
      0,
      Number(rect?.width) || Number(rect?.right) - Number(rect?.left)
    );
  }
  function actionDashboardLayout(host, lastNativeChild) {
    const hostRect = host.getBoundingClientRect();
    const currentAction = host.closest?.('[class*="Header_currentAction"]') ?? host.parentElement;
    const currentActionRect = currentAction?.getBoundingClientRect?.();
    const layoutRect = rectWidth(hostRect) ? hostRect : currentActionRect;
    const layoutLeft = Number(layoutRect?.left) || 0;
    let layoutRight = Number(layoutRect?.right) || layoutLeft;
    const childRect = lastNativeChild?.getBoundingClientRect();
    const childRight = Number(childRect?.right) || layoutLeft;
    for (const sibling of currentAction?.parentElement?.children ?? []) {
      if (sibling === currentAction) continue;
      const siblingRect = sibling.getBoundingClientRect?.();
      const siblingLeft = Number(siblingRect?.left);
      if (rectWidth(siblingRect) > 0 && Number.isFinite(siblingLeft) && siblingLeft >= childRight) {
        layoutRight = Math.min(layoutRight, siblingLeft - 6);
      }
    }
    const left = Math.max(0, childRight - layoutLeft + 7);
    return {
      left,
      availableWidth: Math.max(0, layoutRight - layoutLeft - left)
    };
  }
  function renderActionDashboard() {
    addStyles6();
    const host = document.querySelector('div[class*="Header_actionName"]');
    const actions = [...runtime.state.currentActionsHridList ?? []].sort(
      (left2, right) => Number(left2?.ordinal ?? 0) - Number(right?.ordinal ?? 0)
    );
    const current = actions[0];
    if (!host || !current) {
      clearActionDashboard();
      return null;
    }
    const identity = String(current.id ?? current.actionHrid ?? "");
    const isEnhancement = String(current.actionHrid ?? "").includes("/enhancing");
    const existingRoot = host.querySelector("#mwi-action-dashboard");
    if (!actionMatchesHeader(current, host)) {
      if (isEnhancement && existingRoot?.dataset.actionIdentity === identity) {
        return existingRoot;
      }
      clearActionDashboard();
      return null;
    }
    const timing = getLiveActionTiming(host);
    let enhancementCount = getNativeEnhancementCount(host, current);
    if (isEnhancement) {
      if (enhancementTimingCache.identity !== identity) {
        enhancementTimingCache = { identity, count: null };
      }
      if (enhancementCount !== null) {
        enhancementTimingCache.count = enhancementCount;
      } else if (enhancementTimingCache.count !== null) {
        enhancementCount = enhancementTimingCache.count;
      } else if (existingRoot?.dataset.actionIdentity === identity) {
        return existingRoot;
      } else {
        clearActionDashboard();
        return null;
      }
    } else {
      enhancementTimingCache = { identity: "", count: null };
    }
    const projection = runtime.api.projectAction(
      current,
      enhancementCount ?? void 0,
      {
        durationPerAction: timing.durationPerAction,
        currentCycleRemainingSeconds: timing.currentCycleRemaining
      }
    );
    let root = existingRoot;
    if (!root) {
      root = document.createElement("div");
      root.id = "mwi-action-dashboard";
      root.className = "mwi-action-dashboard";
      host.appendChild(root);
    }
    host.classList.add("mwi-action-dashboard-host");
    root.dataset.actionIdentity = identity;
    root.style.position = "absolute";
    const lastNativeChild = [...host.children].filter(
      (element) => element !== root && element.id !== "script_item_warning"
    ).at(-1);
    const { left, availableWidth } = actionDashboardLayout(host, lastNativeChild);
    root.style.left = `${left}px`;
    root.style.setProperty("--mwi-action-dashboard-left", `${left}px`);
    if (availableWidth > 0) {
      root.style.setProperty(
        "--mwi-action-dashboard-max-width",
        `${availableWidth}px`
      );
    } else {
      root.style.removeProperty("--mwi-action-dashboard-max-width");
    }
    const viewportWidth = Number(host.ownerDocument?.defaultView?.innerWidth) || 0;
    root.dataset.compact = String(
      availableWidth > 0 && availableWidth < 420 || availableWidth === 0 && viewportWidth > 0 && viewportWidth <= 520
    );
    root.dataset.tight = String(availableWidth > 0 && availableWidth < 180);
    root.removeAttribute("title");
    let primary = root.querySelector(":scope > .mwi-action-line");
    if (!primary) {
      primary = document.createElement("div");
      primary.className = "mwi-action-line";
      root.append(primary);
    }
    let currentTime = primary.querySelector(":scope > .mwi-action-time");
    if (!currentTime) {
      currentTime = document.createElement("strong");
      currentTime.className = "mwi-action-time";
      primary.append(currentTime);
    }
    currentTime.textContent = formatRemainingTiming(
      projection.totalSeconds,
      projection.finishAt,
      { isZH: runtime.config.isZH }
    );
    currentTime.removeAttribute("title");
    if (projection.materialLimited) {
      currentTime.title = t8(
        "已按当前库存中的可用原料计算",
        "Limited by materials currently in inventory"
      );
    } else if (enhancementCount !== null) {
      currentTime.title = t8(
        "已按强化栏当前可处理数量计算",
        "Based on the amount currently available for enhancement"
      );
    }
    return root;
  }
  function mutationElement(node) {
    return node?.nodeType === 1 ? node : node?.parentElement;
  }
  function isOwnedActionUi(node) {
    const element = mutationElement(node);
    return Boolean(
      element?.matches?.(OWNED_ACTION_UI_SELECTOR) || element?.closest?.(OWNED_ACTION_UI_SELECTOR)
    );
  }
  function shouldScheduleActionUi(records) {
    return records.some((record) => {
      const target = mutationElement(record.target);
      const removedProductionMount = [...record.removedNodes ?? []].some(
        (node) => node?.nodeType === 1 && (node.matches?.(".mwi-production-extensions") || node.querySelector?.(".mwi-production-extensions"))
      );
      if (removedProductionMount && target?.closest?.(ACTION_SURFACE_SELECTOR)) {
        return true;
      }
      const changed = [
        ...record.addedNodes ?? [],
        ...record.removedNodes ?? []
      ].filter((node) => node?.nodeType === 1);
      if (isOwnedActionUi(target) || changed.length && changed.every(isOwnedActionUi)) {
        return false;
      }
      if (target?.closest?.(ACTION_SURFACE_SELECTOR)) return true;
      return changed.some(
        (node) => node.matches?.(ACTION_SURFACE_SELECTOR) || node.querySelector?.(ACTION_SURFACE_SELECTOR)
      );
    });
  }
  function shouldSettleActionUi(records) {
    return records.some(
      (record) => [...record.addedNodes ?? [], ...record.removedNodes ?? []].some(
        (node) => node?.nodeType === 1 && (node.matches?.(
          '.mwi-production-extensions,div[class*="SkillActionDetail_regularComponent"],div[class*="SkillActionDetail_skillActionDetail"]'
        ) || node.querySelector?.(
          '.mwi-production-extensions,div[class*="SkillActionDetail_regularComponent"],div[class*="SkillActionDetail_skillActionDetail"]'
        ))
      )
    );
  }
  function clearProductionRecoveryTimers() {
    for (const timer of productionRecoveryTimers) clearTimeout(timer);
    productionRecoveryTimers.clear();
  }
  function recoverProductionModules() {
    const context = resolveActiveProductionPanelContext();
    if (!context?.panel?.isConnected) return false;
    if (runtime.settings.get("actionPanel_totalTime_quickInputs")) {
      renderProductionQuickInputs();
    }
    if (runtime.settings.get("productionSummary")) renderProductionPanel();
    if (runtime.settings.get("procurementAssistant") && typeof runtime.api.renderProductionProcurement === "function") {
      runtime.api.renderProductionProcurement();
    }
    if (runtime.settings.get("actionPanel_totalTime") && typeof runtime.api.refreshProductionActionPanel === "function") {
      runtime.api.refreshProductionActionPanel(context.panel);
    }
    return true;
  }
  function scheduleProductionUiRecovery() {
    productionRecoveryGeneration += 1;
    const generation = productionRecoveryGeneration;
    clearProductionRecoveryTimers();
    for (const delay of PRODUCTION_RECOVERY_DELAYS) {
      const timer = setTimeout(() => {
        productionRecoveryTimers.delete(timer);
        if (generation !== productionRecoveryGeneration) return;
        recoverProductionModules();
      }, delay);
      productionRecoveryTimers.add(timer);
    }
    return generation;
  }
  function shouldRecoverProductionUi(records) {
    return records.some((record) => {
      const target = mutationElement(record.target);
      if (record.type === "attributes" && record.attributeName === "aria-hidden" && (target?.matches?.(ACTION_SURFACE_SELECTOR) || target?.querySelector?.(ACTION_SURFACE_SELECTOR))) {
        return true;
      }
      const added = [...record.addedNodes ?? []].filter(
        (node) => node?.nodeType === 1
      );
      if (added.some(
        (node) => node.matches?.(PRODUCTION_DROPDOWN_SELECTOR) || node.querySelector?.(PRODUCTION_DROPDOWN_SELECTOR) || node.matches?.(ACTION_SURFACE_SELECTOR) || node.querySelector?.(ACTION_SURFACE_SELECTOR)
      )) {
        return true;
      }
      return [...record.removedNodes ?? []].some(
        (node) => node?.nodeType === 1 && (node.matches?.(PRODUCTION_RECOVERY_MODULE_SELECTOR) || node.querySelector?.(PRODUCTION_RECOVERY_MODULE_SELECTOR) || node.matches?.(ACTION_SURFACE_SELECTOR) || node.querySelector?.(ACTION_SURFACE_SELECTOR))
      );
    });
  }
  function bindActionUiRenderer(scope, render, messages = []) {
    const scheduler = createFrameScheduler(render);
    const schedule = () => scheduler.schedule();
    subscribeMutationChannel(
      {
        name: "action-ui",
        target: document.body,
        options: { childList: true, subtree: true },
        scope
      },
      (records) => {
        if (!shouldScheduleActionUi(records)) return;
        schedule();
        if (shouldSettleActionUi(records)) {
          scope.timeout(schedule, 80);
          scope.timeout(schedule, 220);
          scope.timeout(schedule, 450);
        }
      }
    );
    const scheduleFromInput = (event) => {
      if (event.target?.closest?.(ACTION_SURFACE_SELECTOR)) schedule();
    };
    scope.event(document, "input", scheduleFromInput, true);
    scope.event(document, "change", scheduleFromInput, true);
    for (const message of messages) {
      scope.add(
        runtime.onMessage(message, () => {
          productionDataRevision += 1;
          schedule();
          if (PRODUCTION_PANEL_REBUILD_MESSAGES.has(message)) {
            scope.timeout(schedule, 100);
            scope.timeout(schedule, 300);
          }
        })
      );
    }
    scope.add(() => scheduler.cancel());
    return { schedule };
  }
  function isHiddenActionElement(element) {
    for (let current = element; current; current = current.parentElement) {
      const className = String(current.className ?? "");
      if (current.hidden || current.style?.display === "none" || current.style?.visibility === "hidden" || /MainPanel_/.test(className) && /hidden/i.test(className)) {
        return true;
      }
      const style = current.ownerDocument?.defaultView?.getComputedStyle(current);
      if (style?.display === "none" || style?.visibility === "hidden") {
        return true;
      }
    }
    return false;
  }
  function resolveActiveProductionPanelContext() {
    const panels = [
      ...document.querySelectorAll(
        'div[class*="SkillActionDetail_regularComponent"],div[class*="SkillActionDetail_skillActionDetail"]'
      )
    ].filter((panel) => !isHiddenActionElement(panel)).sort((left, right) => {
      const modalPriority = Number(Boolean(right.closest('[class*="Modal_modalContainer"]'))) - Number(Boolean(left.closest('[class*="Modal_modalContainer"]')));
      if (modalPriority) return modalPriority;
      if (left.contains(right)) return 1;
      if (right.contains(left)) return -1;
      return 0;
    });
    for (const panel of panels) {
      const actionHrid = resolvePanelAction(panel);
      if (!actionHrid || !isProductionAction(actionHrid)) continue;
      const input = getCountInput(panel);
      const parsedCount = input ? runtime.api.parseCompactNumber?.(input.value) : Number.NaN;
      return {
        panel,
        input,
        actionHrid,
        count: Number.isFinite(parsedCount) && parsedCount > 0 ? Math.ceil(parsedCount) : null
      };
    }
    return null;
  }
  function getProductionPanelMount(panel, { create: create2 = true } = {}) {
    if (!panel) return null;
    let mount = panel.querySelector(":scope > .mwi-production-extensions");
    if (mount || !create2) return mount;
    mount = document.createElement("div");
    mount.className = "mwi-production-extensions";
    mount.dataset.mwitoolsProductionExtension = "true";
    const anchor = panel.querySelector('div[class*="SkillActionDetail_actionContainer"]') ?? panel.querySelector('div[class*="SkillActionDetail_name"]');
    if (anchor) anchor.insertAdjacentElement("afterend", mount);
    else panel.append(mount);
    return mount;
  }
  function mountProductionModule(panel, element, slot) {
    const mount = getProductionPanelMount(panel);
    if (!mount || !element) return null;
    element.dataset.mwitoolsProductionSlot = slot;
    const order = PRODUCTION_MODULE_ORDER[slot] ?? Number.MAX_SAFE_INTEGER;
    const next = [...mount.children].find(
      (child) => (PRODUCTION_MODULE_ORDER[child.dataset.mwitoolsProductionSlot] ?? Number.MAX_SAFE_INTEGER) > order
    );
    mount.insertBefore(element, next ?? null);
    return element;
  }
  function findActionPanel() {
    return resolveActiveProductionPanelContext()?.panel ?? null;
  }
  function getCountInput(panel) {
    return panel?.querySelector(
      'div[class*="SkillActionDetail_maxActionCountInput"] input'
    );
  }
  function findInfinityButton(panel, input) {
    const container = input?.closest('div[class*="SkillActionDetail_actionContainer"]') ?? panel;
    return [...container?.querySelectorAll("button") ?? []].find((button) => {
      if (button.classList.contains("mwi-max-action-button")) return false;
      const text = String(
        runtime.api.getOriTextFromElement?.(button) ?? button.textContent ?? ""
      ).trim();
      return text === "∞" || /infinite|unlimited/i.test(button.title ?? "");
    });
  }
  function setReactInputValue(input, value) {
    if (typeof runtime.api.reactInputTriggerHack === "function") {
      runtime.api.reactInputTriggerHack(input, value);
      return;
    }
    const view = input.ownerDocument?.defaultView ?? window;
    const previous = input.value;
    const setter = Object.getOwnPropertyDescriptor(
      view.HTMLInputElement.prototype,
      "value"
    )?.set;
    if (setter) setter.call(input, value);
    else input.value = value;
    input._valueTracker?.setValue(previous);
    input.dispatchEvent(new view.Event("input", { bubbles: true }));
  }
  function removeProductionQuickInputs() {
    document.querySelectorAll(".mwi-production-quick-inputs").forEach((element) => element.remove());
  }
  function quickButtonPrototype(panel, input) {
    const container = input?.closest('div[class*="SkillActionDetail_maxActionCountInput"]') ?? panel;
    return [...container?.querySelectorAll("button") ?? []].find(
      (button) => !button.classList.contains("mwi-production-quick-button")
    );
  }
  function applyProductionQuickCount(input, count) {
    if (!input?.isConnected || !Number.isSafeInteger(count) || count <= 0) return;
    setReactInputValue(input, String(count));
    input.dispatchEvent(
      new (input.ownerDocument?.defaultView?.Event ?? Event)("change", {
        bubbles: true
      })
    );
    renderProductionPanel();
  }
  function getMinimumCountForDuration(actionHrid, targetSeconds, cycleSeconds) {
    if (!Number.isFinite(targetSeconds) || targetSeconds <= 0 || !Number.isFinite(cycleSeconds) || cycleSeconds <= 0) {
      return 0;
    }
    const efficiencyPercent = Number(
      runtime.api.getTotalEffiPercentage?.(actionHrid)
    );
    const rawMultiplier = 1 + efficiencyPercent / 100;
    const efficiencyMultiplier = Number.isFinite(rawMultiplier) && rawMultiplier > 0 ? rawMultiplier : 1;
    const targetCycles = Math.max(1, Math.ceil(targetSeconds / cycleSeconds));
    let count = Math.max(
      1,
      Math.ceil((targetCycles - 0.5) * efficiencyMultiplier)
    );
    while (Math.round(count / efficiencyMultiplier) < targetCycles) count += 1;
    while (count > 1 && Math.round((count - 1) / efficiencyMultiplier) >= targetCycles) {
      count -= 1;
    }
    return count;
  }
  function createProductionQuickRow({
    panel,
    input,
    id,
    label,
    values,
    resolveCount
  }) {
    const row = document.createElement("div");
    row.id = id;
    row.className = "mwi-production-quick-row";
    const caption = document.createElement("span");
    caption.className = "mwi-production-quick-label";
    caption.textContent = label;
    const buttons = document.createElement("div");
    buttons.className = "mwi-production-quick-buttons";
    const prototype = quickButtonPrototype(panel, input);
    for (const value of values) {
      const button = prototype?.cloneNode(false) ?? document.createElement("button");
      button.type = "button";
      button.classList.add("mwi-production-quick-button");
      button.textContent = runtime.api.numberFormatter(value);
      button.dataset.quickValue = String(value);
      button.addEventListener("click", () => {
        const count = resolveCount(value);
        applyProductionQuickCount(input, count);
      });
      buttons.append(button);
    }
    row.append(caption, buttons);
    return row;
  }
  function renderProductionQuickInputs() {
    addStyles6();
    const context = resolveActiveProductionPanelContext();
    const panel = context?.panel ?? null;
    const input = context?.input ?? null;
    const actionHrid = context?.actionHrid ?? null;
    document.querySelectorAll(".mwi-production-quick-inputs").forEach((host2) => {
      if (!panel?.contains(host2)) host2.remove();
    });
    if (!panel || !input || !actionHrid || !isProductionAction(actionHrid)) {
      panel?.querySelector(".mwi-production-quick-inputs")?.remove();
      return null;
    }
    const countGroup = input.closest(
      'div[class*="SkillActionDetail_maxActionCountInput"]'
    );
    if (!countGroup) return null;
    let host = panel.querySelector(".mwi-production-quick-inputs");
    const duration = getProductionPanelDuration(panel);
    const presets = productionQuickPresets();
    const presetSignature = `${presets.hours.join(",")}|${presets.counts.join(",")}`;
    if (host && host.dataset.presetSignature !== presetSignature) {
      host.remove();
      host = null;
    }
    if (!host) {
      host = document.createElement("div");
      host.className = "mwi-production-quick-inputs";
      host.dataset.presetSignature = presetSignature;
      const hours = createProductionQuickRow({
        panel,
        input,
        id: "quickInputHourButtons",
        label: t8("时长", "Hours"),
        values: presets.hours,
        resolveCount: (hoursValue) => {
          const liveDuration = getProductionPanelDuration(panel);
          return getMinimumCountForDuration(
            actionHrid,
            hoursValue * 3600,
            liveDuration
          );
        }
      });
      const counts = createProductionQuickRow({
        panel,
        input,
        id: "quickInputCountButtons",
        label: t8("次数", "Count"),
        values: presets.counts,
        resolveCount: (count) => count
      });
      host.append(hours, counts);
      mountProductionModule(panel, host, "quickInputs");
    }
    const efficiencyPercent = Number(
      runtime.api.getTotalEffiPercentage?.(actionHrid)
    );
    const normalizedEfficiency = Number.isFinite(efficiencyPercent) && efficiencyPercent > -100 ? efficiencyPercent : 0;
    host.querySelectorAll("#quickInputHourButtons button").forEach((button) => {
      button.disabled = !Number.isFinite(duration) || duration <= 0;
      button.title = button.disabled ? t8("无法读取当前单次耗时", "Current action duration unavailable") : t8(
        `按当前 ${duration}s/次与 ${normalizedEfficiency.toFixed(1)}% 综合效率换算,实际时长不少于所选值;增益变化后请重新选择`,
        `Uses the current ${duration}s cycle and ${normalizedEfficiency.toFixed(1)}% efficiency, rounding up to at least the selected duration; select again after buffs change`
      );
    });
    return host;
  }
  function syncMaxButton(panel, input, maxCraftable) {
    let button = panel?.querySelector(".mwi-max-action-button");
    const infinityButton = findInfinityButton(panel, input);
    if (!input || !infinityButton) {
      button?.remove();
      return;
    }
    if (!button) {
      button = infinityButton.cloneNode(false);
      button.type = "button";
      button.classList.add("mwi-max-action-button");
      button.textContent = t8("最大", "Max");
      button.addEventListener("click", () => {
        const count = Number(button.dataset.maxCraftable);
        if (!Number.isSafeInteger(count) || count <= 0) return;
        const livePanel = button.closest(
          'div[class*="SkillActionDetail_regularComponent"],div[class*="SkillActionDetail_skillActionDetail"]'
        );
        const liveInput = getCountInput(livePanel) ?? input;
        setReactInputValue(liveInput, String(count));
        liveInput.dispatchEvent(
          new (liveInput.ownerDocument?.defaultView?.Event ?? Event)("change", {
            bubbles: true
          })
        );
        renderProductionPanel();
      });
      infinityButton.insertAdjacentElement("afterend", button);
    }
    const enabled = Number.isSafeInteger(maxCraftable) && maxCraftable > 0;
    button.disabled = !enabled;
    button.dataset.maxCraftable = enabled ? String(maxCraftable) : "";
    button.title = enabled ? t8(
      `填入库存最多可做 ${maxCraftable} 次`,
      `Use inventory maximum: ${maxCraftable}`
    ) : t8("当前没有有限的可生产次数", "No finite production maximum");
  }
  function syncProductionDuration(panel, input, totalSeconds) {
    document.querySelectorAll(".mwi-production-duration-inline").forEach((element) => {
      if (!panel?.contains(element)) element.remove();
    });
    const target = input?.closest(
      'div[class*="SkillActionDetail_maxActionCountInput"],div[class*="SkillActionDetail_actionContainer"]'
    ) ?? panel?.querySelector('div[class*="SkillActionDetail_actionContainer"]');
    if (!target) return null;
    let duration = panel.querySelector(".mwi-production-duration-inline");
    if (!duration) {
      duration = document.createElement("span");
      duration.className = "mwi-production-duration-inline";
    }
    duration.textContent = `${t8("耗时", "Duration")} ${formatDuration(totalSeconds)}`;
    target.append(duration);
    return duration;
  }
  function removeProductionDuration() {
    document.querySelectorAll(".mwi-production-duration-inline").forEach((element) => element.remove());
  }
  function resolvePanelAction(panel) {
    const nameElement = panel?.querySelector(
      'div[class*="SkillActionDetail_name"]'
    );
    if (!nameElement) return null;
    const name = runtime.api.getOriTextFromElement?.(nameElement)?.trim();
    if (!name) return null;
    const localizedAction = resolveLocalizedEntity("action", name);
    if (localizedAction) return localizedAction;
    const actionMap = runtime.state.initData_actionDetailMap;
    if (!actionMap) return null;
    const candidateNames = /* @__PURE__ */ new Set([name]);
    for (const [actionHrid, detail] of Object.entries(actionMap)) {
      if (candidateNames.has(detail?.name)) return actionHrid;
    }
    const itemHrid = resolveLocalizedEntity("item", name);
    if (itemHrid) {
      const outputAction = Object.entries(actionMap).find(
        ([, detail]) => runtime.api.getExpectedOutputs?.(detail).some((output) => output.itemHrid === itemHrid)
      );
      if (outputAction) return outputAction[0];
    }
    return runtime.api.getActionHridFromItemName?.(name) ?? null;
  }
  function isProductionAction(actionHrid) {
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    if (!detail) return false;
    const actionType = String(detail.type ?? "");
    if (actionType.includes("combat")) return false;
    return Boolean(runtime.api.getExpectedOutputs?.(detail)?.length);
  }
  function metric(label, value) {
    const box = document.createElement("div");
    box.className = "mwi-production-metric";
    const caption = document.createElement("div");
    caption.className = "mwi-production-label";
    caption.textContent = label;
    const content = document.createElement("div");
    content.className = "mwi-production-value";
    if (value?.nodeType) content.append(value);
    else content.textContent = value;
    box.append(caption, content);
    return box;
  }
  function renderProductionPanel() {
    addStyles6();
    const summaryMode = runtime.settings.getPreference?.("productionSummaryMode") ?? (runtime.settings.get("productionSummary") ? "collapsed" : "off");
    if (!runtime.settings.get("productionSummary") || summaryMode === "off") {
      document.querySelectorAll("#mwi-production-summary").forEach((card2) => card2.remove());
      document.querySelectorAll(".mwi-max-action-button").forEach((button) => button.remove());
      removeProductionDuration();
      return;
    }
    const context = resolveActiveProductionPanelContext();
    const panel = context?.panel ?? null;
    const input = context?.input ?? null;
    const existingCards = [
      ...document.querySelectorAll("#mwi-production-summary")
    ];
    if (!panel) {
      existingCards.forEach((card2) => card2.remove());
      document.querySelectorAll(".mwi-max-action-button").forEach((button) => button.remove());
      removeProductionDuration();
      return;
    }
    existingCards.filter((card2) => !panel.contains(card2)).forEach((card2) => card2.remove());
    document.querySelectorAll(".mwi-max-action-button").forEach((button) => {
      if (!panel.contains(button)) button.remove();
    });
    const existingCard = panel.querySelector("#mwi-production-summary");
    const actionHrid = context?.actionHrid ?? null;
    if (!actionHrid || !isProductionAction(actionHrid)) {
      existingCard?.remove();
      panel.querySelector(".mwi-max-action-button")?.remove();
      panel.querySelector(".mwi-production-duration-inline")?.remove();
      return;
    }
    const count = context?.count ?? Number.POSITIVE_INFINITY;
    const durationPerAction = getProductionPanelDuration(panel);
    const showProfit = runtime.settings.get("productionProfit") && !runtime.api.shouldSuppressMarketFeatures?.();
    const actionType = runtime.state.initData_actionDetailMap?.[actionHrid]?.type ?? null;
    const selectedDrinkHrids = Array.isArray(
      runtime.state.initData_actionTypeDrinkSlotsMap?.[actionType]
    ) ? runtime.state.initData_actionTypeDrinkSlotsMap[actionType].map(
      (drink) => drink?.itemHrid ?? null
    ) : [];
    const signature = JSON.stringify([
      actionHrid,
      Number.isFinite(count) ? count : "infinite",
      durationPerAction,
      showProfit,
      summaryMode,
      runtime.config.isZH,
      productionDataRevision,
      selectedDrinkHrids,
      (runtime.state.initData_characterItems ?? []).map((item) => [
        item.itemHrid,
        item.itemLocationHrid,
        item.enhancementLevel ?? 0,
        item.count ?? 0
      ])
    ]);
    const hasDuration = Boolean(
      panel.querySelector(".mwi-production-duration-inline")
    );
    const needsMaxButton = Boolean(
      input && findInfinityButton(panel, input) && !panel.querySelector(".mwi-max-action-button")
    );
    if (existingCard?.dataset.renderSignature === signature && hasDuration && !needsMaxButton) {
      return existingCard;
    }
    const projection = runtime.api.projectAction(actionHrid, count, {
      durationPerAction,
      respectInventoryLimit: !Number.isFinite(count)
    });
    syncMaxButton(panel, input, projection.maxCraftable);
    syncProductionDuration(panel, input, projection.totalSeconds);
    if (existingCard?.dataset.renderSignature === signature) return existingCard;
    let card = panel.querySelector("#mwi-production-summary");
    if (!card) {
      card = document.createElement("section");
      card.id = "mwi-production-summary";
      card.className = "mwi-production-card";
      mountProductionModule(panel, card, "summary");
    }
    const sameAction = card.dataset.actionHrid === actionHrid;
    const wasExpanded = sameAction && card.dataset.expanded === "true";
    card.dataset.renderSignature = signature;
    card.dataset.actionHrid = actionHrid;
    card.dataset.mode = summaryMode;
    const expanded = summaryMode === "expanded" || wasExpanded;
    card.dataset.expanded = String(expanded);
    const extensions = [
      ...card.querySelectorAll('[data-mwitools-production-extension="true"]')
    ];
    for (const extension of extensions) {
      card.insertAdjacentElement("afterend", extension);
    }
    card.replaceChildren();
    const title = document.createElement("button");
    title.type = "button";
    title.className = "mwi-production-card-title";
    title.textContent = t8("本次生产摘要", "Production summary");
    title.setAttribute("aria-expanded", String(expanded));
    const body = document.createElement("div");
    body.className = "mwi-production-card-body";
    body.hidden = !expanded;
    if (summaryMode === "collapsed") {
      title.addEventListener("click", () => {
        const next = card.dataset.expanded !== "true";
        card.dataset.expanded = String(next);
        title.setAttribute("aria-expanded", String(next));
        body.hidden = !next;
      });
    }
    const grid = document.createElement("div");
    grid.className = "mwi-production-metrics";
    const outputs = document.createElement("div");
    outputs.className = "mwi-production-output-grid";
    outputs.dataset.count = String(projection.outputs?.length ?? 0);
    projection.outputs?.forEach(
      (output) => outputs.append(createProductionOutput(output, panel))
    );
    const outputMetric = metric(
      projection.effectivelyInfinite ? t8("预期单次产出", "Output per action") : t8("预期总产出", "Total output"),
      outputs
    );
    outputMetric.classList.add("mwi-production-output-metric");
    grid.append(
      outputMetric,
      metric(
        t8("当前拥有", "Owned"),
        projection.outputs?.length ? projection.outputs.map((output) => runtime.api.numberFormatter(output.owned)).join(" · ") : "—"
      ),
      metric(
        t8("库存最多可做", "Max craftable"),
        projection.maxCraftable === Infinity ? "∞" : number2(projection.maxCraftable)
      )
    );
    if (showProfit) {
      grid.append(
        metric(
          t8("每次净利润", "Per action"),
          number2(projection.netProfitPerAction)
        ),
        metric(t8("每小时净利润", "Per hour"), number2(projection.profitPerHour)),
        metric(
          t8("每天净利润", "Per day"),
          number2(
            projection.profitPerHour === null ? null : projection.profitPerHour * 24
          )
        ),
        metric(
          t8("本次总净利润", "Total profit"),
          projection.netProfitPerAction === null ? number2(null) : projection.effectivelyInfinite ? "∞" : number2(projection.totalProfit)
        )
      );
    }
    body.append(grid);
    if (showProfit && projection.status === "incomplete") {
      const warning = document.createElement("div");
      warning.className = "mwi-production-warning";
      warning.textContent = t8(
        "部分市场价格缺失,利润暂不显示为 0。",
        "Some market prices are missing; profit is not treated as zero."
      );
      body.append(warning);
    }
    card.append(title, body);
  }
  function removeActionUi() {
    document.querySelector("#mwi-action-dashboard")?.remove();
    document.querySelectorAll(".mwi-action-dashboard-host").forEach(
      (element) => element.classList.remove("mwi-action-dashboard-host")
    );
    document.querySelector("#mwi-production-summary")?.remove();
    document.querySelector(".mwi-max-action-button")?.remove();
    removeProductionDuration();
    removeProductionQuickInputs();
    document.querySelectorAll(".mwi-production-extensions:empty").forEach((mount) => mount.remove());
  }
  runtime.features.register({
    id: "productionUiRecovery",
    scope: "global",
    initialize({ scope }) {
      const attach = () => {
        if (!document.body) return false;
        const MutationObserverRef = globalThis.MutationObserver ?? document.defaultView?.MutationObserver;
        if (!MutationObserverRef) return false;
        const observer = new MutationObserverRef((records) => {
          if (shouldRecoverProductionUi(records)) scheduleProductionUiRecovery();
        });
        scope.observer(observer, document.body, {
          attributes: true,
          attributeFilter: ["aria-hidden"],
          childList: true,
          subtree: true
        });
        return true;
      };
      if (!attach()) {
        scope.event(document, "DOMContentLoaded", attach, { once: true });
      }
      scope.add(() => {
        productionRecoveryGeneration += 1;
        clearProductionRecoveryTimers();
      });
    }
  });
  runtime.features.register({
    id: "totalActionTime",
    setting: "totalActionTime",
    scope: "character",
    initialize({ scope }) {
      addStyles6();
      let refreshTimer2 = null;
      const render = () => {
        const mounted = renderActionDashboard();
        if (refreshTimer2 !== null) clearTimeout(refreshTimer2);
        refreshTimer2 = mounted ? setTimeout(() => renderer.schedule(), 1e3) : null;
      };
      const renderer = bindActionUiRenderer(scope, render, [
        "actions_updated",
        "action_completed",
        ...PRODUCTION_PROFILE_MESSAGES
      ]);
      render();
      scope.add(() => {
        if (refreshTimer2 !== null) clearTimeout(refreshTimer2);
        clearActionDashboard();
      });
    }
  });
  runtime.features.register({
    id: "actionBarProfit",
    setting: "actionBarProfit",
    scope: "character",
    dependsOn: ["totalActionTime"],
    initialize() {
      renderActionDashboard();
      return renderActionDashboard;
    }
  });
  runtime.features.register({
    id: "actionPanel_totalTime_quickInputs",
    setting: "actionPanel_totalTime_quickInputs",
    scope: "character",
    dependsOn: ["actionPanel_totalTime"],
    initialize({ scope }) {
      renderProductionQuickInputs();
      bindActionUiRenderer(scope, renderProductionQuickInputs, [
        "actions_updated",
        ...PRODUCTION_PROFILE_MESSAGES
      ]);
      scope.add(
        runtime.settings.onPreferenceChange?.(
          "productionQuickHours",
          () => renderProductionQuickInputs()
        )
      );
      scope.add(
        runtime.settings.onPreferenceChange?.(
          "productionQuickCounts",
          () => renderProductionQuickInputs()
        )
      );
      scope.add(removeProductionQuickInputs);
    }
  });
  runtime.features.register({
    id: "productionSummary",
    setting: "productionSummary",
    scope: "character",
    initialize({ scope }) {
      renderProductionPanel();
      bindActionUiRenderer(scope, renderProductionPanel, [
        "actions_updated",
        "action_completed",
        "market_item_values_updated",
        "market_item_order_books_updated",
        ...PRODUCTION_PROFILE_MESSAGES
      ]);
      scope.add(
        runtime.settings.onChange?.("productionProfit", () => {
          productionDataRevision += 1;
          renderProductionPanel();
        })
      );
      scope.add(
        runtime.settings.onChange?.("adaptIronCowMarketFeatures", () => {
          productionDataRevision += 1;
          renderProductionPanel();
        })
      );
      scope.add(
        runtime.settings.onPreferenceChange?.("productionSummaryMode", () => {
          productionDataRevision += 1;
          renderProductionPanel();
        })
      );
      scope.add(
        () => document.querySelector("#mwi-production-summary")?.remove()
      );
      scope.add(
        () => document.querySelectorAll(".mwi-production-extensions:empty").forEach((mount) => mount.remove())
      );
    }
  });
  runtime.features.register({
    id: "productionProfit",
    setting: "productionProfit",
    scope: "character",
    dependsOn: ["productionSummary"],
    initialize() {
      renderProductionPanel();
      return renderProductionPanel;
    }
  });
  Object.assign(runtime.api, {
    renderActionDashboard,
    renderProductionPanel,
    getProductionPanelDuration,
    parseProductionDurationSeconds,
    parseProductionQuickPresets,
    getLiveActionTiming,
    resolveProductionAction: resolvePanelAction,
    resolveActiveProductionPanelContext,
    getProductionPanelMount,
    mountProductionModule,
    scheduleProductionUiRecovery,
    renderProductionQuickInputs,
    removeProductionQuickInputs,
    removeActionUi
  });

  // src/features/procurement.js
  var STYLE_ID9 = "mwitools-procurement-style";
  var HOST_ID = "mwitools-procurement-host";
  var MARKET_NAV_ID = "mwitools-procurement-market-nav";
  var PRODUCTION_ID = "mwitools-procurement-production";
  var PRODUCTION_REFRESH_ID = "mwitools-procurement-inventory-refresh";
  var PROCUREMENT_SURFACE_SELECTOR = 'div[class*="SkillActionDetail"],div[class*="MarketplacePanel"],div[class*="HousePanel"],div[class*="HouseRoom"]';
  var OWNED_PROCUREMENT_SELECTOR = `#${HOST_ID},#${MARKET_NAV_ID},#${PRODUCTION_ID},#${PRODUCTION_REFRESH_ID},.mwi-procurement-badge,.mwi-procurement-market-target,.mwi-procurement-refresh-host`;
  var procurement3 = runtime.api.procurement;
  var shell = null;
  var shadow = null;
  var drawerOpen = false;
  var activeTab = "cart";
  var currentMarketTarget = "";
  var armedNextItem = "";
  var marketSessionDone = /* @__PURE__ */ new Map();
  var marketSessionActive = false;
  var marketSessionRequiresModal = false;
  var marketSessionStartedAt = 0;
  var marketSessionModalSeen = false;
  var marketSessionHost = null;
  var marketSessionRestoreNavTarget = "";
  var currentMarketLevel = 0;
  var marketNavigationRevision = 0;
  var lastProductionSignature = "";
  var activeHoldRepeatStop = null;
  var activeCartDrag = null;
  var cartScrollStates = /* @__PURE__ */ new Map();
  var MARKET_SESSION_OPEN_GRACE_MS = 2500;
  var CART_ICON = `<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="21" r="1.6"/><circle cx="19" cy="21" r="1.6"/><path d="M2 3h3l2.6 12.5a2 2 0 0 0 2 1.5h8.7a2 2 0 0 0 2-1.6L22 7H6"/></svg>`;
  var STAR_ICON = `<svg class="icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.5l2.9 6 6.6.9-4.8 4.6 1.2 6.5L12 17.4 6.1 20.5l1.2-6.5L2.5 9.4l6.6-.9z"/></svg>`;
  function t9(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function marketFeaturesSuppressed() {
    return runtime.api.shouldSuppressMarketFeatures?.() ?? false;
  }
  function materialNoun(count) {
    if (runtime.config.isZH) return "种材料";
    return Number(count) === 1 ? "material" : "materials";
  }
  function formatNumber4(value) {
    return runtime.api.numberFormatter?.(value) ?? String(value ?? "—");
  }
  function exactNumber(value) {
    return runtime.api.formatExactNumber?.(value) ?? String(value ?? "—");
  }
  function escapeHtml5(value) {
    return String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
  }
  function addStyles7() {
    if (document.getElementById(STYLE_ID9)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID9;
    style.textContent = `
    .mwi-procurement-badge{position:static!important;display:inline-flex;max-width:78px;min-height:16px;align-items:center;margin-left:4px;padding:0 4px;border:1px solid rgba(255,255,255,.16);border-radius:3px;background:rgba(15,18,28,.72);font:600 calc(.6875rem * var(--mwi-ui-font-scale,1))/1.35 Roboto,Arial,sans-serif;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:auto}
    .mwi-procurement-panel{min-width:330px!important;max-width:min(420px,calc(100vw - 24px))!important}
    .mwi-procurement-requirement-grid{width:100%!important;max-width:100%!important;grid-template-columns:max-content max-content minmax(0,1fr) max-content!important;align-items:center!important;white-space:nowrap!important}
    .mwi-procurement-requirement-cell{grid-row:var(--mwi-procurement-row)!important;min-width:0!important}
    .mwi-procurement-requirement-cell[data-mwitools-procurement-column="owned"]{grid-column:1!important}
    .mwi-procurement-requirement-cell[data-mwitools-procurement-column="required"]{grid-column:2!important}
    .mwi-procurement-requirement-cell[data-mwitools-procurement-column="item"]{grid-column:3!important}
    .mwi-procurement-requirement-cell[data-mwitools-procurement-column="badge"]{grid-column:4!important}
    .mwi-procurement-badge[data-state="missing"]{color:#ffad62;border-color:rgba(255,153,51,.45)}
    .mwi-procurement-badge[data-state="ready"]{color:#43d17f;border-color:#43c979;background:rgba(48,176,105,.12)}
    .mwi-procurement-badge[data-state="locked"]{color:#d9bd72;border-color:rgba(210,180,90,.4)}
    .mwi-procurement-refresh-position-anchor{position:relative!important}
    #${PRODUCTION_REFRESH_ID}{position:absolute;top:6px;right:48px;z-index:4;min-height:24px;padding:2px 7px;border:1px solid rgba(255,255,255,.18);border-radius:4px;background:rgba(37,43,65,.94);color:var(--color-neutral-100,#eee);font:600 calc(.6875rem * var(--mwi-ui-font-scale,1))/1.25 Roboto,Arial,sans-serif;white-space:nowrap;cursor:pointer;box-shadow:0 2px 7px rgba(0,0,0,.24)}
    #${PRODUCTION_REFRESH_ID}:hover{background:var(--color-space-700,#46547e)}
    #${PRODUCTION_REFRESH_ID}:disabled{opacity:.55;cursor:wait}
    #${PRODUCTION_ID}{min-width:0;max-width:100%;box-sizing:border-box;margin-top:5px;padding-top:5px;border-top:1px solid rgba(255,255,255,.08);font:inherit;font-size:calc(.6875rem * var(--mwi-ui-font-scale,1))}
    #${PRODUCTION_ID}[hidden]{display:none}
    .mwi-procurement-summary-line{display:flex;min-width:0;align-items:center;gap:5px;flex-wrap:wrap}
    .mwi-procurement-summary-state{min-width:0;flex:1;color:var(--color-text-secondary,#aaa);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    .mwi-procurement-summary-state strong{color:#ffad62}
    .mwi-procurement-chain-mode{display:inline-flex;align-items:center;gap:4px;color:var(--color-text-secondary,#aaa);font-size:inherit;white-space:nowrap;cursor:pointer}
    .mwi-procurement-chain-mode input{width:14px;height:14px;margin:0;accent-color:#8293d6;cursor:pointer}
    .mwi-procurement-inline-button{min-height:24px;padding:2px 8px;border:1px solid rgba(255,255,255,.16);border-radius:4px;background:var(--color-midnight-500,#343a54);color:var(--color-neutral-100,#eee);font:inherit;cursor:pointer}
    .mwi-procurement-inline-button:hover{background:var(--color-space-700,#46547e)}
    .mwi-procurement-chain{margin-top:4px;border-radius:4px;background:rgba(0,0,0,.12)}
    .mwi-procurement-chain>summary{padding:4px 6px;cursor:pointer;color:var(--color-text-secondary,#aaa)}
    .mwi-procurement-chain-presets{display:flex;gap:4px;padding:0 6px 5px}
    .mwi-procurement-chain-preset{min-height:22px;padding:2px 7px;border:1px solid rgba(255,255,255,.14);border-radius:4px;background:rgba(255,255,255,.04);color:var(--color-text-secondary,#aaa);font:inherit;font-size:calc(.6875rem * var(--mwi-ui-font-scale,1));cursor:pointer}
    .mwi-procurement-chain-preset[aria-pressed="true"]{border-color:#8293d6;background:rgba(82,100,154,.34);color:#fff}
    .mwi-procurement-chain-list{display:grid;gap:3px;padding:0 6px 6px}
    .mwi-procurement-chain-stage{display:flex;align-items:center;gap:6px;min-width:0}
    .mwi-procurement-chain-stage span:first-of-type{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    .mwi-procurement-chain-stage span:last-child{margin-left:auto;color:#d7bb67;white-space:nowrap}
    .mwi-procurement-market-target{outline:2px solid rgba(245,158,11,.72)!important;outline-offset:1px;border-radius:4px;box-shadow:0 0 0 3px rgba(245,158,11,.12)}
    #${MARKET_NAV_ID}{position:fixed;z-index:2147482001;display:flex;box-sizing:border-box;align-items:center;gap:7px;min-height:40px;padding:5px 8px;border:1px solid var(--color-midnight-400,#505776);border-radius:0 0 5px 5px;background:var(--color-midnight-900,#151927);color:var(--color-neutral-100,#eee);box-shadow:0 7px 18px rgba(0,0,0,.38);font:inherit;font-size:calc(.6875rem * var(--mwi-ui-font-scale,1))}
    #${MARKET_NAV_ID}[data-inside="true"]{border-radius:5px 5px 0 0;box-shadow:0 -5px 16px rgba(0,0,0,.35)}
    .mwi-procurement-nav-progress{flex:0 0 auto;color:var(--color-space-300,#9da9d0);white-space:nowrap}
    .mwi-procurement-nav-items{display:flex;min-width:0;flex:1;gap:4px;overflow-x:auto;padding:1px}
    .mwi-procurement-nav-chip{position:relative;display:flex;flex:0 0 34px;width:34px;height:32px;align-items:center;justify-content:center;padding:2px;border:1px solid var(--color-midnight-400,#505776);border-radius:4px;background:var(--color-midnight-700,#272d43);color:inherit;cursor:pointer}
    .mwi-procurement-nav-chip[data-current="true"]{border-color:#8293d6;background:#394568}
    .mwi-procurement-nav-chip[data-done="true"]{opacity:.68;border-color:#4d9d68;cursor:default}
    .mwi-procurement-nav-icon{display:flex;width:27px;height:27px;align-items:center;justify-content:center;overflow:hidden}.mwi-procurement-nav-icon svg{display:block;width:27px;height:27px}.mwi-procurement-nav-icon .item-icon-fallback{font-size:11px;font-weight:700}
    .mwi-procurement-nav-chip b{position:absolute;right:-2px;bottom:-2px;min-width:13px;padding:0 2px;border-radius:5px;background:var(--color-midnight-900,#151927);color:var(--color-neutral-100,#eee);font-size:.55rem;line-height:12px;text-align:center;box-shadow:0 0 0 1px var(--color-midnight-400,#505776)}.mwi-procurement-nav-chip[data-done="true"] b{color:#62d88e}
    .mwi-procurement-nav-next,.mwi-procurement-nav-remove{flex:0 0 auto;min-height:28px;padding:3px 10px;border:0;border-radius:4px;background:var(--color-space-600,#52649a);color:#fff;cursor:pointer;white-space:nowrap}
    .mwi-procurement-nav-remove{background:rgba(184,72,84,.78)}.mwi-procurement-nav-remove:hover{background:rgba(210,82,95,.92)}
    .mwi-procurement-toast{position:fixed;right:14px;top:14px;z-index:2147483000;max-width:min(360px,calc(100vw - 28px));padding:8px 11px;border:1px solid rgba(245,158,11,.55);border-radius:5px;background:rgba(15,18,28,.96);color:#eee;font-size:.75rem;box-shadow:0 8px 22px rgba(0,0,0,.4)}
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function shellStyles() {
    const fontFamily = runtime.config.isZH ? '"PingFang SC","Microsoft YaHei",Roboto,system-ui,sans-serif' : 'ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif';
    return `
    :host{all:initial;color-scheme:dark;--panel:#171b2a;--card:#23283b;--text:#e7e9ef;--muted:#9299aa;--line:#505773;--accent:#5669ab;--gold:#e8c87f;font-family:${fontFamily}}
    :host([data-market-session="true"]){position:relative;z-index:2147482000;pointer-events:none}
    *{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
    button,input,select{border:0;background:none;color:inherit;font:inherit}
    button{cursor:pointer}.icon{display:block}
    ::-webkit-scrollbar{width:7px}::-webkit-scrollbar-thumb{border-radius:4px;background:color-mix(in srgb,var(--muted) 28%,transparent)}
    .handle{position:fixed;right:0;z-index:1002;display:flex;width:32px;height:62px;align-items:center;justify-content:center;border-radius:9px 0 0 9px;background:var(--panel);color:var(--text);box-shadow:-2px 2px 10px rgba(0,0,0,.3);cursor:pointer;opacity:.86;pointer-events:auto;touch-action:none;user-select:none;transition:opacity .15s}
    .handle:hover{opacity:1}.handle .icon{width:16px;height:16px}.handle[data-has-items="true"]{box-shadow:-2px 2px 10px rgba(0,0,0,.3),inset 2px 0 0 var(--gold)}
    .handle-badge{position:absolute;top:9px;right:7px;width:7px;height:7px;border-radius:50%;background:var(--gold);box-shadow:0 0 0 2px var(--panel)}
    .handle-badge::after{content:"";position:absolute;inset:-3px;border:1.5px solid var(--gold);border-radius:50%;opacity:.6;animation:badge-pulse 1.6s ease-out infinite}@keyframes badge-pulse{0%{transform:scale(.7);opacity:.7}100%{transform:scale(1.9);opacity:0}}
    .drawer{position:fixed;top:56px;right:10px;z-index:1001;display:flex;width:var(--drawer-width,360px);max-width:calc(100vw - 26px);min-height:320px;max-height:calc(100vh - 96px);flex-direction:column;border-radius:10px;background:var(--panel);color:var(--text);box-shadow:0 10px 32px rgba(0,0,0,.45),0 0 0 1px color-mix(in srgb,var(--line) 70%,transparent);pointer-events:auto;transform:translateX(calc(100% + 18px));transition:transform .2s ease}
    .drawer[data-open="true"]{transform:translateX(0)}.resize{position:absolute;left:-3px;top:0;bottom:0;width:7px;border-radius:10px 0 0 10px;cursor:ew-resize;touch-action:none}.resize:hover{background:color-mix(in srgb,var(--accent) 25%,transparent)}
    .header{display:flex;flex:0 0 auto;align-items:center;gap:8px;padding:11px 14px 9px;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent)}
    .title{font-size:14px;font-weight:700;letter-spacing:.2px}.head-count{padding:2px 7px;border-radius:5px;background:color-mix(in srgb,var(--gold) 12%,transparent);color:color-mix(in srgb,var(--gold) 85%,white);font-size:10.5px}.head-count:empty{display:none}
    .close{margin-left:auto;width:27px;height:27px;border-radius:5px;color:var(--muted);font-size:15px}.close:hover{background:color-mix(in srgb,var(--text) 9%,transparent);color:var(--text)}
    .tabs{display:flex;flex:0 0 auto;gap:2px;margin:8px 12px 0;padding:2px;border-radius:7px;background:color-mix(in srgb,var(--text) 5%,transparent)}
    .tab{flex:1;min-width:0;padding:7px 0;border-radius:5px;color:var(--muted);font-size:12px;font-weight:600;white-space:nowrap}.tab:hover{color:var(--text)}.tab[data-active="true"]{background:var(--accent);color:#fff}
    .body{min-height:100px;flex:1 1 auto;overflow-y:auto;padding:4px 12px 6px}.empty{margin:12px 2px;padding:26px 12px;border-radius:8px;background:color-mix(in srgb,var(--text) 4%,transparent);color:var(--muted);font-size:12.5px;line-height:1.7;text-align:center}
    .cart-row{display:grid;min-height:56px;grid-template-columns:20px 26px 44px minmax(0,1fr) auto auto;grid-template-rows:auto auto;align-items:center;column-gap:7px;row-gap:2px;padding:7px 2px;border-bottom:1px solid color-mix(in srgb,var(--line) 40%,transparent)}.cart-row:hover,.plan-row:hover{background:color-mix(in srgb,var(--text) 4%,transparent)}.cart-row[data-dragging="true"]{z-index:1;border-radius:7px;background:color-mix(in srgb,var(--accent) 24%,var(--panel));box-shadow:0 5px 16px rgba(0,0,0,.28)}
    .cart-drag{grid-column:1;grid-row:1/span 2;width:20px;height:38px;border-radius:5px;color:color-mix(in srgb,var(--muted) 70%,transparent);font-size:17px;line-height:1;cursor:grab;touch-action:none;user-select:none}.cart-drag:hover{background:color-mix(in srgb,var(--text) 8%,transparent);color:var(--text)}.cart-drag:active{cursor:grabbing}
    .star{grid-column:2;grid-row:1/span 2;width:26px;height:32px;border-radius:5px;color:color-mix(in srgb,var(--muted) 32%,transparent)}.star:hover{color:color-mix(in srgb,var(--gold) 70%,transparent)}.star[data-active="true"]{color:var(--gold)}.star .icon{width:14px;height:14px;margin:auto}
    .item-icon{grid-column:3;grid-row:1/span 2;display:flex;width:42px;height:42px;align-items:center;justify-content:center;border-radius:8px;background:var(--card);cursor:pointer}.item-icon:hover{background:color-mix(in srgb,var(--card) 88%,white)}.item-icon svg{width:32px;height:32px}.item-icon-fallback{color:var(--muted);font-size:13px;font-weight:700}
    .item-name{grid-column:4;grid-row:1;min-width:0;overflow:hidden;color:var(--text);font-size:13.5px;font-weight:600;text-align:left;text-overflow:ellipsis;white-space:nowrap}.item-name:hover{color:var(--gold)}
    .row-controls{grid-column:5;grid-row:1;display:flex;align-items:stretch;overflow:hidden;border-radius:6px;background:color-mix(in srgb,var(--text) 6%,transparent)}.step{width:27px;color:var(--muted);font-size:15px}.step:hover{background:color-mix(in srgb,var(--text) 9%,transparent);color:var(--text)}.qty{width:56px;height:30px;outline:0;background:transparent;color:var(--gold);font-size:13.5px;font-weight:700;text-align:center;font-variant-numeric:tabular-nums}.qty:focus{background:color-mix(in srgb,var(--accent) 16%,transparent)}
    .delete{grid-column:6;grid-row:1;width:26px;height:32px;border-radius:5px;color:color-mix(in srgb,var(--muted) 55%,transparent);font-size:15px}.delete:hover{background:color-mix(in srgb,#e05a64 14%,transparent);color:#ff8d96}
    .row-bottom{grid-column:4/7;grid-row:2;display:flex;min-width:0;align-items:center;gap:6px;color:var(--muted);font-size:11px;white-space:nowrap}.owned,.price{color:var(--muted)}.price{color:color-mix(in srgb,var(--gold) 78%,var(--muted))}.threshold-wrap{display:flex;min-width:0;align-items:center;gap:3px}.threshold{width:52px;padding:2px 4px;border-radius:4px;outline:0;background:color-mix(in srgb,var(--text) 7%,transparent);color:var(--text);font-size:10px;text-align:center}
    .panel-footer{display:flex;flex:0 0 auto;align-items:center;gap:8px;min-height:56px;padding:10px 14px;border-top:1px solid color-mix(in srgb,var(--line) 55%,transparent);color:var(--muted);font-size:11px}.panel-footer:empty{display:none}.footer-total{font-size:10px;line-height:1.35}.footer-total strong{display:block;color:var(--gold);font-size:15px;font-weight:700;font-variant-numeric:tabular-nums}.footer-total small{display:block;color:var(--muted);font-size:9px}.clear{margin-left:auto;padding:9px 18px;border-radius:6px;background:color-mix(in srgb,var(--text) 8%,transparent);color:var(--text);font-size:12.5px;font-weight:700}.clear:hover{background:color-mix(in srgb,#e05a64 14%,transparent);color:#ff8d96}
    .plan-row{display:flex;min-height:58px;flex-direction:column;gap:6px;padding:8px 4px;border-bottom:1px solid color-mix(in srgb,var(--line) 40%,transparent)}.row-top{display:flex;align-items:center;gap:8px;min-width:0}.plan-title{min-width:0;flex:1;overflow:hidden;color:var(--text);font-size:13px;font-weight:600;text-overflow:ellipsis;white-space:nowrap}.plan-status{color:var(--gold);font-size:10.5px}.progress{height:4px;overflow:hidden;border-radius:2px;background:color-mix(in srgb,var(--text) 7%,transparent)}.progress>span{display:block;height:100%;background:var(--accent)}.plan-meta{display:flex;justify-content:space-between;color:var(--muted);font-size:10.5px}.plan-actions{display:flex;gap:5px}.plan-actions button{padding:6px 9px;border-radius:6px;background:color-mix(in srgb,var(--text) 7%,transparent);color:var(--muted);font-size:11px;font-weight:600}.plan-actions button:hover{background:color-mix(in srgb,var(--text) 11%,transparent);color:var(--text)}
    .setting-section{margin-top:5px}.setting-section-title{padding:8px 4px 4px;color:var(--muted);font-size:10.5px;font-weight:700;letter-spacing:.4px}.setting-row{display:flex;min-height:48px;align-items:center;gap:10px;padding:5px 4px;border-bottom:1px solid color-mix(in srgb,var(--line) 32%,transparent)}.setting-label{min-width:0;flex:1;color:var(--text);font-size:13px;font-weight:600}.setting-label small{display:block;margin-top:2px;color:var(--muted);font-size:10.5px;font-weight:400}.switch-state{min-width:18px;color:var(--muted);font-size:10.5px;text-align:right}.switch-state[data-on="true"]{color:#3edd8b;font-weight:700}.switch{position:relative;width:42px;height:23px;flex:0 0 auto;border-radius:99px;background:color-mix(in srgb,var(--text) 10%,transparent);transition:background-color .15s}.switch::after{content:"";position:absolute;top:3px;left:3px;width:17px;height:17px;border-radius:50%;background:#fff;opacity:.5;transition:transform .15s,opacity .15s}.switch[data-on="true"]{background:#29c274}.switch[data-on="true"]::after{transform:translateX(19px);opacity:1}
    .setting-row input[type="number"],.setting-row select,.setting-button{flex:0 0 auto;min-height:28px;padding:5px 8px;border-radius:6px;background:color-mix(in srgb,var(--text) 7%,transparent);color:var(--text);font-size:11.5px}.setting-row input[type="number"]{width:76px;border:1px solid color-mix(in srgb,var(--text) 14%,transparent);outline:0;text-align:center}.setting-row select{max-width:116px}.setting-button{font-weight:600}.setting-button:hover{background:color-mix(in srgb,var(--text) 11%,transparent)}.shortcut{max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    @media(max-width:760px){
      .drawer{left:0;right:0;top:auto;bottom:0;width:100%!important;max-width:none;height:52%;min-height:0;max-height:90%;border-radius:14px 14px 0 0;box-shadow:0 -10px 32px rgba(0,0,0,.5);transform:translateY(105%)}
      .drawer[data-open="true"]{transform:translateY(0)}
      .resize{display:none}
      .header::before{content:"";position:absolute;top:7px;left:50%;width:44px;height:4px;border-radius:2px;background:color-mix(in srgb,var(--muted) 45%,transparent);transform:translateX(-50%)}.header{position:relative;padding-top:18px}
    }
    @media(prefers-reduced-motion:reduce){.drawer{transition:none}.handle-badge::after{animation:none}}
  `;
  }
  function showToast(message) {
    document.querySelectorAll(".mwi-procurement-toast").forEach((node) => node.remove());
    const toast = document.createElement("div");
    toast.className = "mwi-procurement-toast";
    toast.textContent = message;
    document.body.appendChild(toast);
    setTimeout(() => toast.remove(), 2800);
  }
  function pendingItems() {
    return procurement3.getCartItems().filter((item) => item.quantity > 0).sort((a, b) => {
      if (a.starred !== b.starred) return a.starred ? -1 : 1;
      return a.name.localeCompare(b.name, runtime.config.isZH ? "zh" : "en");
    });
  }
  function renderItemIcon2(item) {
    const itemHrid = procurement3.normalizeItemHrid(item.itemHrid);
    const href = getGameSpriteHref("items", itemHrid);
    if (!href) {
      return `<span class="item-icon-fallback">${escapeHtml5((item.name || "?").trim().charAt(0) || "?")}</span>`;
    }
    return `<svg viewBox="0 0 32 32" aria-label="${escapeHtml5(item.name)}"><use href="${escapeHtml5(href)}" xlink:href="${escapeHtml5(href)}"></use></svg>`;
  }
  function captureCartScroll(body) {
    if (!body) return null;
    const rows2 = [...body.querySelectorAll(".cart-row")];
    if (!rows2.length)
      return { scrollTop: body.scrollTop || 0, keys: [], offset: 0 };
    const bodyTop = body.getBoundingClientRect?.().top ?? 0;
    let anchorIndex = rows2.findIndex((row) => {
      const rect = row.getBoundingClientRect?.();
      return rect && rect.bottom > bodyTop;
    });
    if (anchorIndex < 0) anchorIndex = rows2.length - 1;
    const anchor = rows2[anchorIndex];
    const anchorTop = anchor.getBoundingClientRect?.().top ?? bodyTop;
    return {
      scrollTop: body.scrollTop || 0,
      keys: [
        ...rows2.slice(anchorIndex).map((row) => row.dataset.cartKey),
        ...rows2.slice(0, anchorIndex).reverse().map((row) => row.dataset.cartKey)
      ],
      offset: anchorTop - bodyTop
    };
  }
  function restoreCartScroll(body, state) {
    if (!body || !state) return;
    const anchor = state.keys?.map(
      (key) => [...body.querySelectorAll(".cart-row")].find(
        (row) => row.dataset.cartKey === key
      )
    ).find(Boolean);
    if (!anchor) {
      body.scrollTop = state.scrollTop || 0;
      return;
    }
    const bodyTop = body.getBoundingClientRect?.().top ?? 0;
    const anchorTop = anchor.getBoundingClientRect?.().top ?? bodyTop;
    const delta = anchorTop - bodyTop - (state.offset || 0);
    body.scrollTop = Math.max(0, (body.scrollTop || 0) + delta);
  }
  function rememberBodyScroll(body, tab = body?.dataset.tab) {
    if (!body || !tab) return;
    const state = tab === "cart" ? captureCartScroll(body) : { scrollTop: body.scrollTop || 0 };
    cartScrollStates.set(tab, state);
  }
  function renderShell() {
    if (!shadow) return;
    const settings2 = procurement3.getSettings();
    const items = procurement3.getCartItems();
    const activeCount = items.filter((item) => item.quantity > 0).length;
    const handle = shadow.querySelector(".handle");
    const drawer = shadow.querySelector(".drawer");
    if (!handle || !drawer) return;
    shell.dataset.marketSession = String(marketSessionActive);
    handle.style.top = `${clampHandleY(settings2.handleY)}px`;
    drawer.style.setProperty("--drawer-width", `${settings2.drawerWidth}px`);
    drawer.dataset.open = String(drawerOpen);
    handle.dataset.hasItems = String(activeCount > 0);
    handle.setAttribute("aria-expanded", String(drawerOpen));
    handle.querySelector(".handle-badge").hidden = activeCount === 0;
    shadow.querySelector(".head-count").textContent = activeCount ? t9(`缺 ${activeCount} 项`, `${activeCount} missing`) : t9("无缺料", "All set");
    for (const button of shadow.querySelectorAll(".tab")) {
      button.dataset.active = String(button.dataset.tab === activeTab);
    }
    const body = shadow.querySelector(".body");
    let scrollState = null;
    if (body.dataset.tab !== activeTab) {
      rememberBodyScroll(body);
      abandonCartDrag();
      body.replaceChildren();
      body.dataset.tab = activeTab;
      body.scrollTop = cartScrollStates.get(activeTab)?.scrollTop ?? 0;
      scrollState = cartScrollStates.get(activeTab) ?? null;
    } else if (activeTab === "cart") {
      scrollState = captureCartScroll(body);
    }
    if (activeTab === "plans") renderPlans(body);
    else if (activeTab === "settings") renderProcurementSettings(body);
    else renderCart(body, scrollState);
    if (activeTab === "cart") {
      cartScrollStates.set("cart", captureCartScroll(body));
    }
  }
  function prepareFooter(mode) {
    const footer = shadow.querySelector(".panel-footer");
    if (footer.dataset.mode !== mode) {
      footer.replaceChildren();
      footer.dataset.mode = mode;
    }
    return footer;
  }
  function latestCartItem(row) {
    const parsed = procurement3.parseItemKey(row.dataset.cartKey);
    return procurement3.getCartItem(parsed.itemHrid, parsed.enhancementLevel);
  }
  function abandonCartDrag() {
    const drag = activeCartDrag;
    if (!drag) return;
    activeCartDrag = null;
    drag.row.dataset.dragging = "false";
    window.removeEventListener("pointermove", moveCartDrag, true);
    window.removeEventListener("pointerup", stopCartDrag, true);
    window.removeEventListener("pointercancel", cancelCartDrag, true);
  }
  function finishCartDrag(cancelled = false) {
    const drag = activeCartDrag;
    if (!drag) return;
    abandonCartDrag();
    if (cancelled) {
      renderCart(shadow.querySelector(".body"));
      return;
    }
    procurement3.setCartOrder(
      [...shadow.querySelectorAll(".cart-row")].map((row) => row.dataset.cartKey)
    );
  }
  function moveCartDrag(event) {
    if (!activeCartDrag || event.pointerId !== activeCartDrag.pointerId) return;
    event.preventDefault();
    const { row, body } = activeCartDrag;
    const others = [...body.querySelectorAll(".cart-row")].filter(
      (candidate) => candidate !== row
    );
    const before = others.find(
      (candidate) => event.clientY < candidate.getBoundingClientRect().top + candidate.getBoundingClientRect().height / 2
    );
    if (before) body.insertBefore(row, before);
    else body.append(row);
  }
  function stopCartDrag(event) {
    if (!activeCartDrag || event.pointerId !== activeCartDrag.pointerId) return;
    finishCartDrag(false);
  }
  function cancelCartDrag(event) {
    if (!activeCartDrag || event.pointerId !== activeCartDrag.pointerId) return;
    finishCartDrag(true);
  }
  function createCartRow(key, body) {
    const row = document.createElement("article");
    row.className = "cart-row";
    row.dataset.cartKey = key;
    row.innerHTML = `
    <button class="cart-drag" type="button" title="${t9("拖动排序", "Drag to reorder")}" aria-label="${t9("拖动排序", "Drag to reorder")}">⋮⋮</button>
    <button class="star">${STAR_ICON}</button>
    <button class="item-icon"></button>
    <button class="item-name"></button>
    <div class="row-controls"><button class="step" data-step="-1">−</button><input class="qty" inputmode="numeric" aria-label="${t9("待购数量", "Quantity")}"><button class="step" data-step="1">+</button></div>
    <button class="delete" title="${t9("删除", "Remove")}">×</button>
    <div class="row-bottom"><span class="owned"></span><span class="price"></span><label class="threshold-wrap">${t9("常备", "Min")}<input class="threshold" inputmode="numeric" placeholder="0"></label></div>`;
    const setQuantity = (quantity) => {
      const item = latestCartItem(row);
      if (item)
        procurement3.setCartItemQuantity(
          item.itemHrid,
          quantity,
          item.enhancementLevel
        );
    };
    row.querySelector(".star").addEventListener("click", () => {
      const item = latestCartItem(row);
      if (item)
        procurement3.updateCartItem(item.itemHrid, item.enhancementLevel, {
          starred: !item.starred
        });
    });
    for (const target of row.querySelectorAll(".item-name,.item-icon")) {
      target.addEventListener("click", () => {
        const item = latestCartItem(row);
        if (item && !marketFeaturesSuppressed())
          openMarketplace(item.itemHrid, item.enhancementLevel);
      });
    }
    row.querySelector(".delete").addEventListener("click", () => {
      stopActiveHoldRepeat();
      const item = latestCartItem(row);
      if (item) removeCartItemAndAdvance(item);
    });
    const quantityInput = row.querySelector(".qty");
    quantityInput.addEventListener("change", () => {
      setQuantity(runtime.api.parseCompactNumber?.(quantityInput.value));
    });
    for (const step of row.querySelectorAll(".step")) {
      const update = () => {
        const item = latestCartItem(row);
        if (!item) return stopActiveHoldRepeat();
        setQuantity(item.quantity + Number(step.dataset.step));
      };
      step.addEventListener("click", update);
      installHoldRepeat(step, update);
    }
    row.querySelector(".threshold").addEventListener("change", (event) => {
      const item = latestCartItem(row);
      if (item)
        procurement3.updateCartItem(item.itemHrid, item.enhancementLevel, {
          threshold: event.target.value ? runtime.api.parseCompactNumber?.(event.target.value) : null
        });
    });
    const dragHandle = row.querySelector(".cart-drag");
    dragHandle.addEventListener("pointerdown", (event) => {
      if (activeCartDrag) finishCartDrag(true);
      event.preventDefault();
      activeCartDrag = { pointerId: event.pointerId, row, body };
      row.dataset.dragging = "true";
      dragHandle.setPointerCapture?.(event.pointerId);
      window.addEventListener("pointermove", moveCartDrag, true);
      window.addEventListener("pointerup", stopCartDrag, true);
      window.addEventListener("pointercancel", cancelCartDrag, true);
    });
    return row;
  }
  function updateCartRow(row, item, { marketEnabled, pricesEnabled, price }) {
    row.dataset.cartKey = procurement3.itemKey(
      item.itemHrid,
      item.enhancementLevel
    );
    const star = row.querySelector(".star");
    star.dataset.active = String(Boolean(item.starred));
    star.title = t9(
      "收藏:买齐后保留并监控常备数量",
      "Favorite: keep and restock"
    );
    const icon = row.querySelector(".item-icon");
    icon.disabled = !marketEnabled;
    icon.title = marketEnabled ? t9("在市场中打开", "Open in marketplace") : "";
    const iconMarkup2 = renderItemIcon2(item);
    if (icon.mwitoolsIconMarkup !== iconMarkup2) {
      icon.innerHTML = iconMarkup2;
      icon.mwitoolsIconMarkup = iconMarkup2;
    }
    const name = row.querySelector(".item-name");
    name.disabled = !marketEnabled;
    name.title = item.name;
    name.textContent = `${item.name}${item.enhancementLevel ? ` +${item.enhancementLevel}` : ""}`;
    const quantity = row.querySelector(".qty");
    const activeElement = row.getRootNode()?.activeElement;
    if (activeElement !== quantity) quantity.value = item.quantity;
    const owned = procurement3.getInventoryCount(
      item.itemHrid,
      item.enhancementLevel
    );
    const ownedNode = row.querySelector(".owned");
    ownedNode.title = exactNumber(owned);
    ownedNode.textContent = `${t9("库存", "Stock")} ${formatNumber4(owned)}`;
    const priceNode = row.querySelector(".price");
    priceNode.hidden = !pricesEnabled;
    priceNode.title = price > 0 ? exactNumber(price * item.quantity) : "—";
    priceNode.textContent = price > 0 ? `${formatNumber4(price)} · ${t9("计", "total")} ${formatNumber4(price * item.quantity)}` : "—";
    const thresholdWrap = row.querySelector(".threshold-wrap");
    thresholdWrap.hidden = !item.starred;
    const threshold = row.querySelector(".threshold");
    if (activeElement !== threshold) threshold.value = item.threshold ?? "";
  }
  function renderCartFooter({ marketEnabled, settings: settings2, total, unpriced }) {
    const footer = prepareFooter("cart");
    let totalNode = footer.querySelector(".footer-total");
    let clear = footer.querySelector(".clear");
    if (!totalNode) {
      totalNode = document.createElement("span");
      totalNode.className = "footer-total";
      totalNode.innerHTML = `<span></span><strong></strong><small></small>`;
      footer.append(totalNode);
    }
    if (!clear) {
      clear = document.createElement("button");
      clear.className = "clear";
      clear.addEventListener("click", () => {
        stopActiveHoldRepeat();
        procurement3.clearCart();
      });
      footer.append(clear);
    }
    totalNode.hidden = !marketEnabled;
    totalNode.querySelector("span").textContent = t9("补齐合计", "Total");
    const strong = totalNode.querySelector("strong");
    strong.title = unpriced ? t9("部分物品缺少价格", "Some items are unpriced") : exactNumber(total);
    strong.textContent = settings2.cartTotalEnabled && !unpriced ? formatNumber4(total) : "—";
    const small = totalNode.querySelector("small");
    small.textContent = unpriced ? `${unpriced} ${t9("项未估价", "unpriced")}` : "";
    clear.textContent = t9("清空未收藏", "Clear");
  }
  function renderCart(body, scrollState = captureCartScroll(body)) {
    const items = procurement3.getCartItems();
    if (!items.length) {
      if (activeCartDrag) finishCartDrag(true);
      body.replaceChildren();
      const empty = document.createElement("div");
      empty.className = "empty";
      empty.textContent = t9(
        "购物清单还是空的。打开生产或房屋界面,把缺少的材料加入这里。",
        "Your shopping list is empty. Add missing materials from a production or housing panel."
      );
      body.append(empty);
      prepareFooter("empty");
      return;
    }
    body.querySelector(".empty")?.remove();
    const settings2 = procurement3.getSettings();
    const marketEnabled = !marketFeaturesSuppressed();
    const pricesEnabled = marketEnabled && settings2.pricesEnabled;
    const currentRows = new Map(
      [...body.querySelectorAll(".cart-row")].map((row) => [
        row.dataset.cartKey,
        row
      ])
    );
    const wantedKeys = /* @__PURE__ */ new Set();
    const desiredRows = [];
    let total = 0;
    let unpriced = 0;
    for (const item of items) {
      const key = procurement3.itemKey(item.itemHrid, item.enhancementLevel);
      wantedKeys.add(key);
      const row = currentRows.get(key) ?? createCartRow(key, body);
      const price = pricesEnabled ? runtime.api.getAskPrice?.(item.itemHrid, item.enhancementLevel) || runtime.api.getFairValue?.(item.itemHrid, item.enhancementLevel) || 0 : 0;
      if (pricesEnabled && item.quantity > 0) {
        if (price > 0) total += price * item.quantity;
        else unpriced += 1;
      }
      updateCartRow(row, item, { marketEnabled, pricesEnabled, price });
      desiredRows.push(row);
    }
    for (const [key, row] of currentRows) {
      if (wantedKeys.has(key)) continue;
      if (activeCartDrag?.row === row) abandonCartDrag();
      row.remove();
    }
    if (!activeCartDrag) {
      desiredRows.forEach((row, index) => {
        const current = body.children[index] ?? null;
        if (current !== row) body.insertBefore(row, current);
      });
    } else {
      desiredRows.filter((row) => !row.isConnected).forEach((row) => body.append(row));
    }
    renderCartFooter({ marketEnabled, settings: settings2, total, unpriced });
    restoreCartScroll(body, scrollState);
  }
  function stopActiveHoldRepeat() {
    activeHoldRepeatStop?.();
  }
  function installHoldRepeat(button, callback) {
    let delayTimer = null;
    let repeatTimer = null;
    let pointerId = null;
    const stop = () => {
      clearTimeout(delayTimer);
      clearInterval(repeatTimer);
      delayTimer = null;
      repeatTimer = null;
      pointerId = null;
      window.removeEventListener("pointerup", stopForPointer, true);
      window.removeEventListener("pointercancel", stopForPointer, true);
      window.removeEventListener("blur", stop, true);
      if (activeHoldRepeatStop === stop) activeHoldRepeatStop = null;
    };
    const stopForPointer = (event) => {
      if (pointerId === null || event.pointerId === pointerId) stop();
    };
    button.addEventListener("pointerdown", (event) => {
      stopActiveHoldRepeat();
      pointerId = event.pointerId;
      activeHoldRepeatStop = stop;
      window.addEventListener("pointerup", stopForPointer, true);
      window.addEventListener("pointercancel", stopForPointer, true);
      window.addEventListener("blur", stop, true);
      delayTimer = setTimeout(() => {
        repeatTimer = setInterval(callback, 90);
      }, 420);
    });
    button.addEventListener("pointerup", stopForPointer);
    button.addEventListener("pointercancel", stopForPointer);
  }
  function renderPlans(body) {
    const plans2 = procurement3.getPlans();
    body.replaceChildren();
    const footer = prepareFooter("plans");
    if (!plans2.length) {
      const empty = document.createElement("div");
      empty.className = "empty";
      empty.textContent = t9(
        "还没有生产项目。把生产缺料加入购物车时可以同时创建。",
        "No production projects yet. Create one when adding production materials."
      );
      body.append(empty);
      return;
    }
    for (const plan of plans2) {
      const row = document.createElement("article");
      row.className = "plan-row";
      const percent = plan.targetCount ? Math.min(100, plan.progress / plan.targetCount * 100) : 0;
      row.innerHTML = `
      <div class="row-top"><div class="plan-title">${escapeHtml5(plan.name)}</div><span class="plan-status">${plan.status === "completed" ? t9("已完成", "Completed") : t9("进行中", "Active")}</span></div>
      <div class="progress"><span style="width:${percent}%"></span></div>
      <div class="plan-meta"><span>${formatNumber4(plan.progress)} / ${formatNumber4(plan.targetCount)}</span><span>${Object.keys(plan.materials ?? {}).length} ${materialNoun(Object.keys(plan.materials ?? {}).length)}</span></div>
      <div class="plan-actions"><button data-action="count">${t9("修改次数", "Edit count")}</button><button data-action="toggle">${plan.status === "completed" ? t9("重新打开", "Reopen") : t9("完成", "Complete")}</button><button data-action="remove">${t9("删除", "Delete")}</button></div>`;
      row.querySelector('[data-action="count"]').addEventListener("click", () => {
        const value = globalThis.prompt?.(
          t9("输入新的目标次数", "Enter a new target count"),
          String(plan.targetCount)
        );
        if (value != null)
          procurement3.updatePlan(plan.id, { targetCount: value });
      });
      row.querySelector('[data-action="toggle"]').addEventListener("click", () => {
        procurement3.updatePlan(plan.id, {
          status: plan.status === "completed" ? "active" : "completed"
        });
      });
      row.querySelector('[data-action="remove"]').addEventListener("click", () => {
        procurement3.removePlan(plan.id);
      });
      body.append(row);
    }
    footer.innerHTML = `<span>${t9("生产项目", "Projects")} ${plans2.length}</span><button class="clear">${t9("清空项目", "Clear projects")}</button>`;
    footer.querySelector(".clear").addEventListener("click", () => {
      for (const plan of procurement3.getPlans()) procurement3.removePlan(plan.id);
    });
  }
  var SETTING_SECTIONS = [
    {
      title: ["生产材料", "Production materials"],
      rows: [
        ["badgesEnabled", "材料缺口徽标", "Material shortage badges", "bool"],
        ["upgradeChainEnabled", "升级链材料", "Upgrade-chain materials", "bool"],
        [
          "createPlansByDefault",
          "默认建立生产项目",
          "Create projects by default",
          "bool"
        ],
        ["inventorySyncEnabled", "精确库存同步", "Exact inventory sync", "bool"],
        [
          "autoRestockEnabled",
          "收藏物资自动补货",
          "Favorite item restocking",
          "bool"
        ]
      ]
    },
    {
      title: ["市场购物", "Marketplace"],
      rows: [
        ["locateEnabled", "市场材料定位", "Locate market materials", "bool"],
        [
          "autoPrefillEnabled",
          "自动填写购买数量",
          "Prefill purchase quantity",
          "bool"
        ],
        ["purchaseNavEnabled", "购物导航条", "Shopping navigation", "bool"],
        ["pricesEnabled", "显示市场价格", "Show market prices", "bool"],
        ["cartTotalEnabled", "显示购物总价", "Show cart total", "bool"],
        [
          "autoCollapseEnabled",
          "全部补齐后自动收起",
          "Collapse when fulfilled",
          "bool"
        ]
      ]
    },
    {
      title: ["安全材料", "Material safety"],
      rows: [
        ["safetyLevel", "材料安全余量", "Material safety margin", "safety"],
        [
          "safetyThreshold",
          "启用余量的最少次数",
          "Minimum count for margin",
          "number"
        ],
        [
          "guzzlingPouchLevel",
          "狂饮袋等级(-1 自动)",
          "Guzzling pouch level (-1 auto)",
          "pouch"
        ]
      ]
    },
    {
      title: ["界面与快捷键", "Interface & shortcut"],
      rows: [
        [
          "autoExpandOnAddEnabled",
          "加购后自动展开",
          "Expand after adding",
          "bool"
        ],
        ["nextItemShortcut", "下一项快捷键", "Next item shortcut", "shortcut"],
        ["resetHandle", "重置把手位置", "Reset handle position", "button"],
        ["resetDrawer", "重置抽屉宽度", "Reset drawer width", "button"]
      ]
    }
  ];
  var SETTING_DESCRIPTIONS = {
    badgesEnabled: [
      "直接在材料旁显示缺少或余量",
      "Show shortages beside materials"
    ],
    upgradeChainEnabled: [
      "展开查看升级链各阶段材料",
      "Show materials through upgrade chains"
    ],
    createPlansByDefault: [
      "加购缺料时同时锁定制作材料",
      "Create and lock a plan when adding materials"
    ],
    inventorySyncEnabled: [
      "按服务器库存变化自动扣减清单",
      "Update the list from live inventory changes"
    ],
    autoRestockEnabled: [
      "收藏物品低于常备数量时自动补单",
      "Restock favorites below their minimum"
    ],
    locateEnabled: [
      "从清单打开市场并定位对应物品",
      "Open and locate items in the marketplace"
    ],
    autoPrefillEnabled: [
      "只填写待买数量,不会自动下单",
      "Fill quantity without placing an order"
    ],
    purchaseNavEnabled: [
      "买完后快速切换到下一项",
      "Move through remaining shopping items"
    ],
    pricesEnabled: [
      "显示当前单价和物品小计",
      "Show current prices and subtotals"
    ],
    cartTotalEnabled: [
      "在底部显示全部补齐所需金额",
      "Show the total cost to fill the cart"
    ],
    autoCollapseEnabled: [
      "所有项目补齐后收起购物车",
      "Collapse after every item is fulfilled"
    ],
    autoExpandOnAddEnabled: [
      "任意入口成功加购后展开并显示清单",
      "Open the cart list after any successful add"
    ],
    safetyLevel: [
      "为工匠茶的随机省料准备余量",
      "Allow for Artisan's Tea material variance"
    ],
    safetyThreshold: [
      "次数较少时不额外准备材料",
      "Skip the margin for smaller batches"
    ],
    guzzlingPouchLevel: [
      "填写 -1 时自动读取当前等级",
      "Use -1 to detect the current level"
    ],
    nextItemShortcut: [
      "右键可清除已经录制的快捷键",
      "Right-click to clear the shortcut"
    ],
    resetHandle: ["恢复购物车图标的默认高度", "Restore the cart handle position"],
    resetDrawer: ["恢复悬浮购物车的默认宽度", "Restore the floating cart width"]
  };
  function renderProcurementSettings(body) {
    body.replaceChildren();
    prepareFooter("settings");
    const settings2 = procurement3.getSettings();
    for (const sectionDefinition of SETTING_SECTIONS) {
      const section = document.createElement("section");
      section.className = "setting-section";
      const heading = document.createElement("div");
      heading.className = "setting-section-title";
      heading.textContent = t9(...sectionDefinition.title);
      section.append(heading);
      for (const [id, zh, en, type] of sectionDefinition.rows) {
        const row = document.createElement("div");
        row.className = "setting-row";
        const label = document.createElement("span");
        label.className = "setting-label";
        const description = SETTING_DESCRIPTIONS[id];
        label.innerHTML = `${escapeHtml5(t9(zh, en))}${description ? `<small>${escapeHtml5(t9(...description))}</small>` : ""}`;
        row.append(label);
        let control;
        if (type === "bool") {
          const state = document.createElement("span");
          state.className = "switch-state";
          state.dataset.on = String(Boolean(settings2[id]));
          state.textContent = settings2[id] ? t9("开", "On") : t9("关", "Off");
          row.append(state);
          control = document.createElement("button");
          control.type = "button";
          control.className = "switch";
          control.dataset.on = String(Boolean(settings2[id]));
          control.setAttribute("role", "switch");
          control.setAttribute("aria-checked", String(Boolean(settings2[id])));
          control.setAttribute("aria-label", t9(zh, en));
          control.addEventListener(
            "click",
            () => procurement3.setSetting(id, !settings2[id])
          );
        } else if (type === "safety") {
          control = document.createElement("select");
          for (const [value, text] of [
            ["off", t9("关闭", "Off")],
            ["95", t9("标准 95%", "Standard 95%")],
            ["99", t9("充足 99%", "Ample 99%")],
            ["99.9", t9("极高 99.9%", "Full 99.9%")]
          ]) {
            const option = document.createElement("option");
            option.value = value;
            option.textContent = text;
            option.selected = String(settings2[id]) === value;
            control.append(option);
          }
          control.addEventListener(
            "change",
            () => procurement3.setSetting(id, control.value)
          );
        } else if (type === "shortcut") {
          control = document.createElement("button");
          control.type = "button";
          control.className = "setting-button shortcut";
          control.textContent = formatShortcut(settings2.nextItemShortcut) || t9("录制", "Record");
          control.addEventListener("click", () => captureShortcut(control));
          control.addEventListener("contextmenu", (event) => {
            event.preventDefault();
            procurement3.setSetting("nextItemShortcut", null);
          });
        } else if (type === "button") {
          control = document.createElement("button");
          control.type = "button";
          control.className = "setting-button";
          control.textContent = t9("重置", "Reset");
          control.addEventListener("click", () => {
            procurement3.setSetting(
              id === "resetHandle" ? "handleY" : "drawerWidth",
              id === "resetHandle" ? 180 : 360
            );
          });
        } else {
          control = document.createElement("input");
          control.type = "number";
          control.min = id === "guzzlingPouchLevel" ? "-1" : "0";
          if (type === "pouch") control.max = "20";
          control.value = String(settings2[id]);
          control.addEventListener(
            "change",
            () => procurement3.setSetting(id, Number(control.value))
          );
        }
        row.append(control);
        section.append(row);
      }
      body.append(section);
    }
  }
  function formatShortcut(shortcut) {
    if (!shortcut?.code) return "";
    return [
      shortcut.ctrl && "Ctrl",
      shortcut.shift && "Shift",
      shortcut.alt && "Alt",
      shortcut.meta && "Meta",
      shortcut.display || shortcut.code
    ].filter(Boolean).join("+");
  }
  function captureShortcut(button) {
    button.textContent = t9("请按快捷键…", "Press shortcut…");
    const handler = (event) => {
      if (event.key === "Escape") {
        window.removeEventListener("keydown", handler, true);
        renderShell();
        return;
      }
      if (/^(Control|Shift|Alt|Meta|OS)/.test(event.code)) return;
      event.preventDefault();
      event.stopPropagation();
      const display = event.code === "Space" ? "Space" : event.code.startsWith("Arrow") ? event.code.slice(5) : event.key?.length === 1 ? event.key.toUpperCase() : event.key || event.code;
      procurement3.setSetting("nextItemShortcut", {
        code: event.code,
        display,
        ctrl: event.ctrlKey,
        shift: event.shiftKey,
        alt: event.altKey,
        meta: event.metaKey
      });
      window.removeEventListener("keydown", handler, true);
    };
    window.addEventListener("keydown", handler, true);
  }
  function clampHandleY(value) {
    return Math.min(
      Math.max(8, Number(value) || 180),
      Math.max(8, window.innerHeight - 84)
    );
  }
  function createShell(scope) {
    shell = document.createElement("div");
    shell.id = HOST_ID;
    shadow = shell.attachShadow({ mode: "open" });
    const style = document.createElement("style");
    style.textContent = shellStyles();
    shadow.innerHTML = `
    <button class="handle" aria-label="${t9("购物车(可拖动)", "Shopping cart (drag to move)")}" aria-expanded="false">${CART_ICON}<span class="handle-badge"></span></button>
    <aside class="drawer" data-open="false" aria-label="${t9("购物车", "Shopping cart")}">
      <div class="resize"></div>
      <header class="header"><div class="title">${t9("购物车", "Shopping Cart")}</div><span class="head-count"></span><button class="close" aria-label="${t9("收起", "Collapse")}">»</button></header>
      <nav class="tabs"><button class="tab" data-tab="cart">${t9("清单", "Cart")}</button><button class="tab" data-tab="plans">${t9("项目", "Projects")}</button><button class="tab" data-tab="settings">${t9("设置", "Settings")}</button></nav>
      <main class="body"></main>
      <footer class="panel-footer"></footer>
    </aside>`;
    shadow.prepend(style);
    document.body.appendChild(shell);
    const handle = shadow.querySelector(".handle");
    const drawer = shadow.querySelector(".drawer");
    let dragStart = null;
    handle.addEventListener("pointerdown", (event) => {
      dragStart = { y: event.clientY, top: handle.getBoundingClientRect().top };
      handle.setPointerCapture?.(event.pointerId);
    });
    handle.addEventListener("pointermove", (event) => {
      if (!dragStart) return;
      const next = clampHandleY(dragStart.top + event.clientY - dragStart.y);
      handle.style.top = `${next}px`;
    });
    handle.addEventListener("pointerup", (event) => {
      if (!dragStart) return;
      const moved = Math.abs(event.clientY - dragStart.y) > 5;
      dragStart = null;
      procurement3.setSetting("handleY", parseFloat(handle.style.top));
      if (!moved) {
        drawerOpen = !drawerOpen;
        if (!drawerOpen) stopActiveHoldRepeat();
        renderShell();
      }
    });
    shadow.querySelector(".close").addEventListener("click", () => {
      stopActiveHoldRepeat();
      drawerOpen = false;
      renderShell();
    });
    for (const tab of shadow.querySelectorAll(".tab")) {
      tab.addEventListener("click", () => {
        stopActiveHoldRepeat();
        activeTab = tab.dataset.tab;
        renderShell();
      });
    }
    const resize = shadow.querySelector(".resize");
    let resizing = false;
    resize.addEventListener("pointerdown", (event) => {
      resizing = true;
      resize.setPointerCapture?.(event.pointerId);
    });
    resize.addEventListener("pointermove", (event) => {
      if (!resizing) return;
      drawer.style.setProperty(
        "--drawer-width",
        `${Math.min(560, Math.max(300, window.innerWidth - event.clientX))}px`
      );
    });
    resize.addEventListener("pointerup", () => {
      if (!resizing) return;
      resizing = false;
      procurement3.setSetting(
        "drawerWidth",
        parseFloat(drawer.style.getPropertyValue("--drawer-width"))
      );
    });
    scope.event(window, "resize", renderShell);
    scope.event(document, "keydown", (event) => {
      if (event.key === "Escape" && drawerOpen) {
        stopActiveHoldRepeat();
        drawerOpen = false;
        renderShell();
      }
    });
    scope.event(document, "pointerdown", (event) => {
      if (!drawerOpen || event.composedPath().includes(shell)) return;
      stopActiveHoldRepeat();
      drawerOpen = false;
      renderShell();
    });
    scope.add(() => {
      abandonCartDrag();
      cartScrollStates.clear();
      shell?.remove();
      shell = null;
      shadow = null;
    });
    renderShell();
  }
  function resolveActionPanel() {
    const shared = runtime.api.resolveActiveProductionPanelContext?.();
    if (shared?.panel && shared?.input && shared?.actionHrid && !isCombatActionPanel(shared.panel)) {
      return {
        panel: shared.panel,
        input: shared.input,
        actionHrid: shared.actionHrid,
        actionFunction: resolveActionFunction(shared.panel, shared.actionHrid),
        count: shared.count
      };
    }
    const inputs = [
      ...document.querySelectorAll(
        'div[class*="SkillActionDetail_maxActionCountInput"] input'
      )
    ].filter((input) => !isHiddenActionElement2(input)).sort(
      (left, right) => Number(Boolean(right.closest('[class*="Modal_modalContainer"]'))) - Number(Boolean(left.closest('[class*="Modal_modalContainer"]')))
    );
    for (const input of inputs) {
      const panel = input.closest('div[class*="SkillActionDetail_skillActionDetail"]') ?? input.closest('div[class*="SkillActionDetail_regularComponent"]') ?? input.parentElement;
      if (!panel || isHiddenActionElement2(panel) || isCombatActionPanel(panel)) {
        continue;
      }
      const fiberContext = resolveActionFiberContext(panel);
      const actionHrid = fiberContext?.actionHrid ?? runtime.api.resolveProductionAction?.(panel) ?? (panel.closest?.(
        '[class*="EnhancingPanel"], [class*="EnhancementPanel"], [class*="EnhancePanel"]'
      ) ? "/actions/enhancing/enhance" : null);
      const parsedCount = runtime.api.parseCompactNumber?.(input.value);
      if (!actionHrid) continue;
      const actionFunction = resolveActionFunction(
        panel,
        actionHrid,
        fiberContext?.actionFunction
      );
      if (!isProcurementProductionAction(actionHrid, actionFunction)) continue;
      return {
        panel,
        input,
        actionHrid,
        actionFunction,
        count: Number.isFinite(parsedCount) && parsedCount > 0 ? Math.ceil(parsedCount) : null
      };
    }
    return null;
  }
  function isCombatActionPanel(panel) {
    return Boolean(
      panel?.querySelector?.('[class*="SkillActionDetail_combatMonsters"]')
    );
  }
  function isProcurementProductionAction(actionHrid, actionFunction) {
    const normalizedFunction = String(actionFunction ?? "");
    if (normalizedFunction.includes("enhancing")) return true;
    if (normalizedFunction.includes("combat")) return false;
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    if (!detail || String(detail.type ?? "").includes("combat")) return false;
    return Boolean(runtime.api.getExpectedOutputs?.(detail)?.length);
  }
  function isHiddenActionElement2(element) {
    for (let node = element; node?.nodeType === 1; node = node.parentElement) {
      const className = String(node.className ?? "");
      if (node.hidden || node.getAttribute?.("aria-hidden") === "true" || node.style?.display === "none" || node.style?.visibility === "hidden" || /MainPanel_/.test(className) && /hidden/i.test(className)) {
        return true;
      }
    }
    return false;
  }
  function resolveActionFiberContext(panel) {
    let fiber = findReactFiber(panel);
    for (let depth = 0; fiber && depth < 10; depth += 1) {
      const actionDetail = fiber.memoizedProps?.actionDetail;
      if (actionDetail?.hrid) {
        return {
          actionHrid: actionDetail.hrid,
          actionFunction: actionDetail.function ?? ""
        };
      }
      fiber = fiber.return;
    }
    return null;
  }
  function resolveActionFunction(panel, actionHrid, fiberFunction = "") {
    if (fiberFunction) return fiberFunction;
    if (panel.closest?.(
      '[class*="EnhancingPanel"], [class*="EnhancementPanel"], [class*="EnhancePanel"]'
    ) || String(actionHrid).includes("/enhancing/")) {
      return "/action_functions/enhancing";
    }
    if (String(actionHrid).includes("/alchemy/")) {
      return "/action_functions/alchemy";
    }
    return "/action_functions/production";
  }
  function parseRequirementNumber(text) {
    const tokens = String(text ?? "").match(
      /(?:\d(?:[\d.,\s\u00a0\u202f]*\d)?|\.\d+)\s*[kmbt]?/gi
    );
    if (!tokens?.length) return null;
    for (let index = tokens.length - 1; index >= 0; index -= 1) {
      const value = parseCompactNumber(tokens[index]);
      if (Number.isFinite(value) && value >= 0) return value;
    }
    return null;
  }
  function itemHridFromRequirement(element) {
    const use = element?.querySelector("svg use");
    const href = use?.getAttribute("href") ?? use?.getAttribute("xlink:href") ?? "";
    const fragment = href.includes("#") ? href.split("#").at(-1) : href;
    return procurement3.normalizeItemHrid(fragment);
  }
  function calculateEnhancingRequirements(context) {
    const requirementsRoot = context.panel.querySelector(
      '[class*="SkillActionDetail_itemRequirements"]'
    );
    if (!requirementsRoot) return null;
    const itemElements = [
      ...requirementsRoot.querySelectorAll(
        ':scope > [class*="Item_itemContainer"]'
      )
    ];
    const inputElements = [
      ...requirementsRoot.querySelectorAll(
        ':scope > [class*="SkillActionDetail_inputCount"]'
      )
    ];
    if (!itemElements.length || inputElements.length !== itemElements.length) {
      return null;
    }
    const materials = itemElements.map((element, index) => {
      const itemHrid = itemHridFromRequirement(element);
      const perAction = parseRequirementNumber(inputElements[index].textContent);
      if (!itemHrid || perAction === null || perAction <= 0) return null;
      const enhancementLevel = 0;
      const suggested = Math.ceil(perAction * context.count - 1e-9);
      const owned = procurement3.getInventoryCount(itemHrid, enhancementLevel);
      const lockedDetails = procurement3.getLockedDetails(
        itemHrid,
        enhancementLevel
      );
      const effectiveOwned = Math.max(0, owned - lockedDetails.total);
      const cartQuantity = procurement3.getCartItem(itemHrid, enhancementLevel)?.quantity ?? 0;
      return {
        itemHrid,
        enhancementLevel,
        name: procurement3.resolveItemName(itemHrid),
        perAction,
        suggested,
        owned,
        locked: lockedDetails.total,
        lockedByPlans: lockedDetails.byPlan,
        effectiveOwned,
        cartQuantity,
        shortage: Math.max(0, suggested - effectiveOwned),
        addableShortage: Math.max(0, suggested - effectiveOwned - cartQuantity),
        purchasable: itemHrid !== "/items/coin"
      };
    });
    if (materials.some((material) => !material)) return null;
    return {
      status: "complete",
      actionHrid: context.actionHrid,
      count: context.count,
      materials
    };
  }
  function appendSunnyEnhancingCompatibility(root) {
    root.classList.add("mwi-mm-summary-panel");
    const input = document.createElement("input");
    input.className = "mwi-mm-manual-input";
    input.type = "number";
    input.inputMode = "numeric";
    input.hidden = true;
    input.setAttribute("aria-hidden", "true");
    const add = document.createElement("button");
    add.type = "button";
    add.dataset.act = "add";
    add.hidden = true;
    add.setAttribute("aria-hidden", "true");
    add.addEventListener("click", () => {
      if (!root.isConnected || document.getElementById(PRODUCTION_ID) !== root) {
        return;
      }
      const count = parseCompactNumber(input.value);
      const context = resolveActionPanel();
      if (!Number.isFinite(count) || count <= 0 || context?.actionFunction !== "/action_functions/enhancing") {
        return;
      }
      const requirements = calculateEnhancingRequirements({
        ...context,
        count: Math.ceil(count)
      });
      const result = procurement3.addRequirementsToCart(
        requirements?.materials ?? [],
        "enhancing"
      );
      showToast(
        result.added ? t9(
          `已加入 ${result.added} 种材料`,
          `Added ${result.added} ${materialNoun(result.added)}`
        ) : t9("没有新的缺料", "No new shortages")
      );
    });
    root.append(input, add);
  }
  function findMaterialHost(panel, itemHrid) {
    const bare = procurement3.normalizeItemHrid(itemHrid).split("/").at(-1);
    for (const node of panel.querySelectorAll('[class*="Item_itemContainer"]')) {
      const href = node.querySelector("svg use")?.getAttribute("href") ?? node.querySelector("svg use")?.getAttribute("xlink:href") ?? "";
      if (href.includes(bare)) return node;
    }
    return null;
  }
  function markRequirementCell(element, row, column) {
    if (!element) return;
    element.classList.add("mwi-procurement-requirement-cell");
    element.style.setProperty("--mwi-procurement-row", String(row));
    element.dataset.mwitoolsProcurementColumn = column;
  }
  function layoutMaterialBadge(panel, host, badge) {
    const root = host.closest('[class*="SkillActionDetail_itemRequirements"]') ?? host.parentElement;
    if (!root) return;
    const items = [
      ...root.querySelectorAll(':scope > [class*="Item_itemContainer"]')
    ];
    const rowIndex = items.indexOf(host);
    if (rowIndex < 0) return;
    const row = rowIndex + 1;
    const owned = [
      ...root.querySelectorAll(
        ':scope > [class*="SkillActionDetail_inventoryCount"]'
      )
    ];
    const required = [
      ...root.querySelectorAll(
        ':scope > [class*="SkillActionDetail_inputCount"]'
      )
    ];
    root.classList.add("mwi-procurement-requirement-grid");
    markRequirementCell(owned[rowIndex], row, "owned");
    markRequirementCell(required[rowIndex], row, "required");
    markRequirementCell(host, row, "item");
    markRequirementCell(badge, row, "badge");
    panel.classList.add("mwi-procurement-panel");
  }
  function removeInventoryRefreshButtons(keepHost = null) {
    for (const button of document.querySelectorAll(`#${PRODUCTION_REFRESH_ID}`)) {
      if (keepHost && button.parentElement === keepHost) continue;
      const host = button.parentElement;
      button.remove();
      host?.classList.remove("mwi-procurement-refresh-host");
      host?.classList.remove("mwi-procurement-refresh-position-anchor");
    }
    for (const host of document.querySelectorAll(
      ".mwi-procurement-refresh-host"
    )) {
      if (host !== keepHost && !host.querySelector(`#${PRODUCTION_REFRESH_ID}`)) {
        host.classList.remove("mwi-procurement-refresh-host");
        host.classList.remove("mwi-procurement-refresh-position-anchor");
      }
    }
  }
  function resolveInventoryRefreshHost(panel) {
    if (!panel) return null;
    const modal = panel.closest?.(
      '[class*="Modal_modal__"]:not([class*="Modal_modalContainer"])'
    );
    if (modal) return { host: modal, sourcePanel: panel };
    const existingHost = document.getElementById(
      PRODUCTION_REFRESH_ID
    )?.parentElement;
    if (existingHost?.isConnected && !isHiddenActionElement2(existingHost) && existingHost.matches?.(
      '[class*="Modal_modal__"]:not([class*="Modal_modalContainer"])'
    )) {
      const sourcePanel = existingHost.querySelector(
        'div[class*="SkillActionDetail_regularComponent"],div[class*="SkillActionDetail_skillActionDetail"]'
      );
      return { host: existingHost, sourcePanel: sourcePanel ?? panel };
    }
    const modalContainer = panel.closest?.('[class*="Modal_modalContainer"]');
    const nestedModal = modalContainer?.querySelector?.(
      ':scope > [class*="Modal_modal__"]:not([class*="Modal_modalContainer"])'
    );
    if (nestedModal) return { host: nestedModal, sourcePanel: panel };
    const modalPanel = [
      ...document.querySelectorAll(
        '[class*="Modal_modalContainer"] div[class*="SkillActionDetail_regularComponent"],[class*="Modal_modalContainer"] div[class*="SkillActionDetail_skillActionDetail"]'
      )
    ].find((candidate) => !isHiddenActionElement2(candidate));
    const activeModal = modalPanel?.closest?.(
      '[class*="Modal_modal__"]:not([class*="Modal_modalContainer"])'
    );
    return activeModal ? { host: activeModal, sourcePanel: modalPanel } : null;
  }
  function ensureInventoryRefreshButton(panel) {
    const resolved = resolveInventoryRefreshHost(panel);
    if (!resolved) {
      removeInventoryRefreshButtons();
      return null;
    }
    const { host, sourcePanel } = resolved;
    removeInventoryRefreshButtons(host);
    const computedPosition = document.defaultView?.getComputedStyle?.(host)?.position ?? "";
    const needsPositionAnchor = host.classList.contains("mwi-procurement-refresh-position-anchor") || !computedPosition || computedPosition === "static";
    host.classList.add("mwi-procurement-refresh-host");
    host.classList.toggle(
      "mwi-procurement-refresh-position-anchor",
      needsPositionAnchor
    );
    let button = host.querySelector(`#${PRODUCTION_REFRESH_ID}`);
    if (button) return button;
    button = document.createElement("button");
    button.id = PRODUCTION_REFRESH_ID;
    button.type = "button";
    button.textContent = t9("↻ 仓库", "↻ Stock");
    button.title = t9(
      "从游戏当前状态更新仓库,并重新计算购物车、项目占用和生产余缺",
      "Update inventory from the current game state and recalculate the shopping cart, project reservations, and production shortages"
    );
    button.setAttribute("aria-label", t9("更新仓库", "Refresh inventory"));
    for (const eventName of ["pointerdown", "mousedown"]) {
      button.addEventListener(eventName, (event) => event.stopPropagation());
    }
    button.addEventListener("click", (event) => {
      event.preventDefault();
      event.stopPropagation();
      if (button.disabled) return;
      button.disabled = true;
      const liveItems = resolveLiveCharacterItems(sourcePanel);
      if (liveItems === null) {
        button.disabled = false;
        showToast(
          t9(
            "暂时无法读取游戏当前仓库,请稍后再试",
            "Could not read the current game inventory; try again shortly"
          )
        );
        return;
      }
      runtime.state.initData_characterItems = liveItems.map((item) => ({
        ...item
      }));
      const result = procurement3.replaceInventorySnapshot(liveItems);
      lastProductionSignature = "";
      renderShell();
      runtime.api.renderProductionPanel?.();
      renderProductionProcurement();
      showToast(
        runtime.config.isZH ? `仓库已更新:${result.changedItemCount} 种库存变化;购物车 ${result.cartItemCount} 项、项目 ${result.projectCount} 个已按最新库存重算` : `Inventory updated: ${result.changedItemCount} item changes; ${result.cartItemCount} cart items and ${result.projectCount} projects recalculated`
      );
      if (button.isConnected) button.disabled = false;
    });
    host.append(button);
    return button;
  }
  function clearProductionUi() {
    document.getElementById(PRODUCTION_ID)?.remove();
    document.querySelectorAll(".mwi-procurement-badge").forEach((node) => node.remove());
    document.querySelectorAll(".mwi-procurement-requirement-grid").forEach(
      (node) => node.classList.remove("mwi-procurement-requirement-grid")
    );
    document.querySelectorAll(".mwi-procurement-requirement-cell").forEach((node) => {
      node.classList.remove("mwi-procurement-requirement-cell");
      node.style.removeProperty("--mwi-procurement-row");
      delete node.dataset.mwitoolsProcurementColumn;
    });
    document.querySelectorAll(".mwi-procurement-panel").forEach((node) => node.classList.remove("mwi-procurement-panel"));
    lastProductionSignature = "";
  }
  function renderProductionProcurement() {
    const context = resolveActionPanel();
    if (!context) {
      removeInventoryRefreshButtons();
      const houseModal = findActiveHouseModal();
      if (!houseModal) {
        clearProductionUi();
        return;
      }
      renderHouseProcurement(houseModal);
      return;
    }
    if (context.count === null) {
      clearProductionUi();
      const root2 = document.createElement("section");
      root2.id = PRODUCTION_ID;
      root2.dataset.mwitoolsProductionExtension = "true";
      root2.dataset.state = "waiting";
      const summary2 = document.createElement("div");
      summary2.className = "mwi-procurement-summary-line";
      const state = document.createElement("span");
      state.className = "mwi-procurement-summary-state";
      state.textContent = t9("请选择生产数量", "Enter a production quantity");
      summary2.append(state);
      root2.append(summary2);
      if (runtime.api.mountProductionModule) {
        runtime.api.mountProductionModule(context.panel, root2, "shortage");
      } else {
        const anchor = context.panel.querySelector(
          'div[class*="SkillActionDetail_actionContainer"]'
        ) ?? context.input.parentElement;
        anchor.insertAdjacentElement("afterend", root2);
      }
      ensureInventoryRefreshButton(context.panel);
      return;
    }
    const settings2 = procurement3.getSettings();
    const isEnhancing = context.actionFunction === "/action_functions/enhancing";
    const direct = isEnhancing ? calculateEnhancingRequirements(context) : procurement3.calculateRequirements(context.actionHrid, context.count);
    if (!direct?.materials?.length) {
      clearProductionUi();
      ensureInventoryRefreshButton(context.panel);
      return;
    }
    const chain = !isEnhancing && settings2.upgradeChainEnabled && direct.detail?.upgradeItemHrid ? procurement3.calculateUpgradeChain(context.actionHrid, context.count) : null;
    const materials = chain?.leaves?.length ? chain.leaves : direct.materials;
    const signature = JSON.stringify([
      isEnhancing ? "enhancing" : "production",
      runtime.config.isZH,
      context.actionHrid,
      context.count,
      settings2.badgesEnabled,
      settings2.upgradeChainEnabled,
      materials.map((material) => [
        material.itemHrid,
        material.suggested,
        material.owned,
        material.locked,
        material.cartQuantity
      ])
    ]);
    if (signature === lastProductionSignature && document.getElementById(PRODUCTION_ID)) {
      ensureInventoryRefreshButton(context.panel);
      return;
    }
    clearProductionUi();
    lastProductionSignature = signature;
    if (settings2.badgesEnabled) {
      for (const material of direct.materials) {
        const host = findMaterialHost(context.panel, material.itemHrid);
        if (!host) continue;
        const badge = document.createElement("span");
        badge.className = "mwi-procurement-badge";
        badge.dataset.state = material.shortage ? "missing" : "ready";
        badge.textContent = material.shortage ? `${t9("缺", "Need")} ${formatNumber4(material.shortage)}` : `${t9("余", "Spare")} ${formatNumber4(material.effectiveOwned - material.suggested)}`;
        const locks = material.lockedByPlans.map((entry) => `${entry.name}: ${exactNumber(entry.quantity)}`).join("\n");
        badge.title = `${t9("建议准备", "Suggested")}: ${exactNumber(material.suggested)}
${t9("当前拥有", "Owned")}: ${exactNumber(material.owned)}${material.locked ? `
${t9("计划锁定", "Locked")}: ${exactNumber(material.locked)}
${locks}` : ""}`;
        host.insertAdjacentElement("afterend", badge);
        layoutMaterialBadge(context.panel, host, badge);
      }
    }
    const root = document.createElement("section");
    root.id = PRODUCTION_ID;
    root.dataset.mwitoolsProductionExtension = "true";
    const hasSelectableChain = (chain?.stages?.length ?? 0) > 1;
    const previousStepMaterials = hasSelectableChain ? procurement3.selectUpgradeChainMaterials(chain, [
      chain.stages[0].actionHrid
    ]) : materials;
    let stageInputs = [];
    const summary = document.createElement("div");
    summary.className = "mwi-procurement-summary-line";
    const summaryState = document.createElement("span");
    summaryState.className = "mwi-procurement-summary-state";
    let chainModeInput = null;
    if (hasSelectableChain) {
      const chainMode = document.createElement("label");
      chainMode.className = "mwi-procurement-chain-mode";
      chainModeInput = document.createElement("input");
      chainModeInput.type = "checkbox";
      chainModeInput.setAttribute("role", "switch");
      chainModeInput.setAttribute("aria-label", t9("所选链条", "Selected chain"));
      const chainModeLabel = document.createElement("span");
      chainModeLabel.textContent = t9("所选链条", "Selected chain");
      chainMode.append(chainModeInput, chainModeLabel);
      summary.append(summaryState, chainMode);
    } else {
      summary.append(summaryState);
    }
    const add = document.createElement("button");
    add.className = "mwi-procurement-inline-button";
    add.type = "button";
    const selectedMaterials = () => {
      if (!hasSelectableChain || !chainModeInput?.checked) {
        return previousStepMaterials;
      }
      return procurement3.selectUpgradeChainMaterials(
        chain,
        stageInputs.filter((input) => input.checked).map((input) => input.dataset.action)
      );
    };
    const updateSummary = () => {
      const scopedMaterials = selectedMaterials();
      const missing = scopedMaterials.filter(
        (material) => material.purchasable && material.shortage > 0
      );
      const addable = scopedMaterials.filter(
        (material) => material.purchasable && material.addableShortage > 0
      );
      summaryState.innerHTML = missing.length ? `${t9("缺少", "Missing")} <strong>${missing.length}</strong> ${materialNoun(missing.length)} · ${t9("建议准备已包含安全余量", "Suggested amounts include a safety margin")}` : t9("材料充足", "Materials ready");
      root.hidden = Boolean(
        !missing.length && runtime.settings.get("hideReadyProductionShortage")
      );
      add.disabled = addable.length === 0;
      add.textContent = addable.length ? t9("加入购物清单", "Add to shopping list") : t9("已在清单中", "Already listed");
    };
    chainModeInput?.addEventListener("change", updateSummary);
    add.addEventListener("click", () => {
      const scopedMaterials = selectedMaterials();
      const project = !isEnhancing && settings2.createPlansByDefault ? procurement3.createPlan(
        context.actionHrid,
        context.count,
        scopedMaterials
      ) : null;
      const result = project ? procurement3.addProjectRequirementsToCart(project.id) : procurement3.addRequirementsToCart(
        scopedMaterials,
        isEnhancing ? "enhancing" : "production"
      );
      showToast(
        result.added ? t9(
          `已加入 ${result.added} 种材料`,
          `Added ${result.added} ${materialNoun(result.added)}`
        ) : t9("没有新的缺料", "No new shortages")
      );
    });
    summary.append(add);
    root.append(summary);
    if (isEnhancing) appendSunnyEnhancingCompatibility(root);
    if (hasSelectableChain) {
      const details = document.createElement("details");
      details.className = "mwi-procurement-chain";
      const heading = document.createElement("summary");
      heading.textContent = `${t9("升级链", "Upgrade chain")} · ${chain.stages.length} ${t9("阶段", "stages")}${chain.cycle ? ` · ${t9("检测到循环", "cycle detected")}` : ""}${chain.truncated ? ` · ${t9("已达到 25 层", "25-level limit")}` : ""}`;
      const list = document.createElement("div");
      list.className = "mwi-procurement-chain-list";
      for (const stage of chain.stages) {
        const row = document.createElement("label");
        row.className = "mwi-procurement-chain-stage";
        row.innerHTML = `<input type="checkbox" checked data-action="${escapeHtml5(stage.actionHrid)}"><span>${escapeHtml5(stage.name)}</span><span>×${formatNumber4(stage.count)}</span>`;
        list.append(row);
      }
      const presets = document.createElement("div");
      presets.className = "mwi-procurement-chain-presets";
      const allButton = document.createElement("button");
      allButton.type = "button";
      allButton.className = "mwi-procurement-chain-preset";
      allButton.textContent = t9("全链条", "Full chain");
      stageInputs = [...list.querySelectorAll("input[type=checkbox]")];
      const updatePresetState = () => {
        const checked = stageInputs.map((input) => input.checked);
        allButton.setAttribute("aria-pressed", String(checked.every(Boolean)));
        updateSummary();
      };
      allButton.addEventListener("click", () => {
        stageInputs.forEach((input) => {
          input.checked = true;
        });
        updatePresetState();
      });
      stageInputs.forEach(
        (input) => input.addEventListener("change", updatePresetState)
      );
      presets.append(allButton);
      updatePresetState();
      details.append(heading, presets, list);
      root.append(details);
    }
    updateSummary();
    const enhancingInfo = isEnhancing ? context.panel.querySelector('[class*="SkillActionDetail_info"]') : null;
    if (enhancingInfo) enhancingInfo.append(root);
    else {
      if (runtime.api.mountProductionModule) {
        runtime.api.mountProductionModule(context.panel, root, "shortage");
      } else {
        const anchor = context.panel.querySelector(
          '[class*="SkillActionDetail_actionContainer"]'
        ) ?? context.input.parentElement;
        anchor.insertAdjacentElement("afterend", root);
      }
    }
    ensureInventoryRefreshButton(context.panel);
  }
  function findReactFiber(element) {
    if (!element) return null;
    const key = Object.getOwnPropertyNames(element).find(
      (candidate) => candidate.startsWith("__reactFiber") || candidate.startsWith("__reactInternalInstance")
    );
    return key ? element[key] : null;
  }
  function liveItemsFromStateHost(host) {
    const state = host?.state;
    if (!state || state.characterItemMap == null) return null;
    const characterId = state.character?.id ?? state.character?.characterID ?? state.currentCharacter?.id ?? state.characterId ?? state.characterID ?? null;
    if (characterId != null && procurement3.activeCharacterId != null && String(characterId) !== String(procurement3.activeCharacterId)) {
      return null;
    }
    const source = state.characterItemMap;
    let values;
    if (Array.isArray(source)) values = source;
    else if (typeof source?.values === "function") values = [...source.values()];
    else values = Object.values(source ?? {});
    return values.filter((item) => item?.itemHrid && Number(item.count) > 0).map((item) => ({ ...item }));
  }
  function resolveLiveCharacterItems(panel) {
    let fiber = findReactFiber(panel);
    let depth = 0;
    while (fiber && depth < 250) {
      const items = liveItemsFromStateHost(fiber.stateNode);
      if (items !== null) return items;
      fiber = fiber.return;
      depth += 1;
    }
    const root = document.getElementById("root");
    const fibers = [];
    const pushFiber = (value) => {
      const candidate = value?.current ?? value;
      if (candidate && typeof candidate === "object") fibers.push(candidate);
    };
    pushFiber(root?._reactRootContainer?.current);
    pushFiber(root?._reactRootContainer?._internalRoot?.current);
    for (const element of [root, document.body]) {
      for (const key of Object.getOwnPropertyNames(element ?? {})) {
        if (key.startsWith("__reactContainer") || key.startsWith("__reactFiber") || key.startsWith("__reactInternalInstance")) {
          pushFiber(element[key]);
        }
      }
    }
    const seen = /* @__PURE__ */ new Set();
    while (fibers.length && seen.size < 5e4) {
      const candidate = fibers.pop();
      if (!candidate || seen.has(candidate)) continue;
      seen.add(candidate);
      const items = liveItemsFromStateHost(candidate.stateNode);
      if (items !== null) return items;
      if (candidate.child) fibers.push(candidate.child);
      if (candidate.sibling) fibers.push(candidate.sibling);
    }
    return null;
  }
  function findObjectWithItemRequirements(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
    if (!value || typeof value !== "object" || depth > 5 || seen.has(value)) {
      return null;
    }
    seen.add(value);
    for (const candidate of Object.values(value)) {
      if (Array.isArray(candidate) && candidate.some((entry) => entry?.itemHrid && Number(entry?.count) > 0)) {
        return candidate;
      }
    }
    for (const candidate of Object.values(value)) {
      const found = findObjectWithItemRequirements(candidate, depth + 1, seen);
      if (found) return found;
    }
    return null;
  }
  function findActiveHouseModal() {
    return [
      ...document.querySelectorAll('[class*="HousePanel_modalContent"]')
    ].find(
      (candidate) => candidate.getClientRects().length && candidate.querySelector('[class*="HousePanel_itemRequirements"]')
    );
  }
  function parseHouseCount(value) {
    const normalized = String(value ?? "").trim();
    const compact = parseRequirementNumber(normalized);
    if (Number.isFinite(compact) && compact >= 0) return compact;
    const match = normalized.match(/-?\d+(?:\.\d+)?/);
    const number3 = Number(match?.[0]);
    return Number.isFinite(number3) && number3 >= 0 ? number3 : null;
  }
  function houseItemHrid(element) {
    const use = element?.querySelector("svg use");
    const href = use?.getAttribute("href") ?? use?.getAttribute("xlink:href") ?? "";
    const fragment = href.includes("#") ? href.split("#").at(-1) : href;
    return procurement3.normalizeItemHrid(fragment);
  }
  function extractHouseRequirementsFromDom(modal) {
    const requirementsRoot = modal.querySelector(
      '[class*="HousePanel_itemRequirements"]'
    );
    if (!requirementsRoot) return null;
    let itemElements = [
      ...requirementsRoot.querySelectorAll(
        ':scope > [class*="Item_itemContainer"]'
      )
    ];
    if (!itemElements.length) {
      itemElements = [
        ...requirementsRoot.querySelectorAll(
          '[class*="HousePanel_itemRequirementCell"] [class*="Item_itemContainer"]'
        )
      ];
    }
    const inputElements = [
      ...requirementsRoot.querySelectorAll(
        ':scope > [class*="HousePanel_inputCount"]'
      )
    ];
    if (!itemElements.length || inputElements.length < itemElements.length) {
      return null;
    }
    const requirements = itemElements.map((element, index) => ({
      itemHrid: houseItemHrid(element),
      enhancementLevel: 0,
      count: parseHouseCount(inputElements[index]?.textContent)
    }));
    return requirements.every(
      (requirement) => requirement.itemHrid && requirement.count > 0
    ) ? requirements : null;
  }
  function extractHouseRequirementsFromFiber(modal) {
    let fiber = findReactFiber(modal);
    let requirements = null;
    for (let depth = 0; fiber && depth < 12 && !requirements; depth += 1) {
      requirements = findObjectWithItemRequirements({
        props: fiber.memoizedProps,
        state: fiber.memoizedState
      });
      fiber = fiber.return;
    }
    return requirements?.length && requirements.every(
      (requirement) => procurement3.normalizeItemHrid(requirement?.itemHrid) && Number(requirement?.count) > 0
    ) ? requirements : null;
  }
  function renderHouseProcurement(modal = findActiveHouseModal()) {
    if (!modal) return;
    const requirements = extractHouseRequirementsFromDom(modal) ?? extractHouseRequirementsFromFiber(modal);
    if (!requirements?.length) {
      modal.querySelector(`#${PRODUCTION_ID}`)?.remove();
      lastProductionSignature = "";
      return;
    }
    const materials = requirements.map((input) => {
      const owned = procurement3.getEffectiveInventory(
        input.itemHrid,
        input.enhancementLevel
      );
      const suggested = Math.ceil(Number(input.count) || 0);
      const cartQuantity = procurement3.getCartItem(input.itemHrid, input.enhancementLevel)?.quantity ?? 0;
      return {
        itemHrid: procurement3.normalizeItemHrid(input.itemHrid),
        enhancementLevel: Number(input.enhancementLevel) || 0,
        name: procurement3.resolveItemName(input.itemHrid),
        suggested,
        owned,
        shortage: Math.max(0, suggested - owned),
        addableShortage: Math.max(0, suggested - owned - cartQuantity),
        purchasable: procurement3.normalizeItemHrid(input.itemHrid) !== "/items/coin"
      };
    });
    const signature = JSON.stringify([
      "housing",
      runtime.config.isZH,
      materials.map((material) => [
        material.itemHrid,
        material.suggested,
        material.owned,
        material.shortage,
        material.addableShortage
      ])
    ]);
    if (signature === lastProductionSignature && modal.querySelector(`#${PRODUCTION_ID}`)) {
      return;
    }
    document.getElementById(PRODUCTION_ID)?.remove();
    lastProductionSignature = signature;
    const root = document.createElement("section");
    root.id = PRODUCTION_ID;
    root.className = "mwi-procurement-summary-line";
    const missing = materials.filter(
      (material) => material.purchasable && material.shortage > 0
    );
    root.innerHTML = `<span class="mwi-procurement-summary-state">${missing.length ? runtime.config.isZH ? `房屋升级缺少 <strong>${missing.length}</strong> 种材料` : `Missing <strong>${missing.length}</strong> ${materialNoun(missing.length)} for the house upgrade` : t9("房屋升级材料充足", "House materials ready")}</span>`;
    const add = document.createElement("button");
    add.className = "mwi-procurement-inline-button";
    add.type = "button";
    const addable = materials.filter(
      (material) => material.purchasable && material.addableShortage > 0
    );
    add.textContent = addable.length ? t9("加入购物清单", "Add to shopping list") : t9("已在清单中", "Already listed");
    add.disabled = addable.length === 0;
    add.addEventListener("click", () => {
      const result = procurement3.addRequirementsToCart(materials, "housing");
      showToast(
        result.added ? t9(
          `已加入 ${result.added} 种材料`,
          `Added ${result.added} ${materialNoun(result.added)}`
        ) : t9("没有新的缺料", "No new shortages")
      );
      lastProductionSignature = "";
      globalThis.queueMicrotask(() => renderHouseProcurement(modal));
    });
    root.append(add);
    const anchor = modal.querySelector('[class*="HousePanel_upgradeButton"]') ?? modal.lastElementChild;
    anchor?.insertAdjacentElement("beforebegin", root);
  }
  function resolveMarketplaceHandler() {
    const root = document.getElementById("root");
    const fibers = [];
    const pushFiber = (value) => {
      const fiber = value?.current ?? value;
      if (fiber && typeof fiber === "object" && !fibers.includes(fiber)) {
        fibers.push(fiber);
      }
    };
    pushFiber(root?._reactRootContainer?.current);
    pushFiber(root?._reactRootContainer?._internalRoot?.current);
    for (const element of [root, document.body]) {
      for (const key of Object.getOwnPropertyNames(element ?? {})) {
        if (key.startsWith("__reactContainer") || key.startsWith("__reactFiber") || key.startsWith("__reactInternalInstance")) {
          pushFiber(element[key]);
        }
      }
    }
    const seen = /* @__PURE__ */ new Set();
    let fallback = null;
    while (fibers.length && seen.size < 5e4) {
      const fiber = fibers.pop();
      if (!fiber || seen.has(fiber)) continue;
      seen.add(fiber);
      const host = fiber.stateNode;
      if (typeof host?.handleGoToMarketplace === "function" && typeof host?.handleCloseMarketplaceModal === "function" && typeof host?.setState === "function") {
        return { host, fn: host.handleGoToMarketplace, floating: true };
      }
      const fn = host?.handleGoToMarketplace ?? host?.goToMarketplace ?? host?.openMarketplace;
      if (!fallback && typeof fn === "function") fallback = { host, fn };
      if (fiber.child) fibers.push(fiber.child);
      if (fiber.sibling) fibers.push(fiber.sibling);
    }
    return fallback;
  }
  function beginMarketSession(resolved, { requiresModal, restoreNavTarget = "" }) {
    const continuing = marketSessionActive && marketSessionRequiresModal === requiresModal && (!requiresModal || marketSessionHost === resolved.host);
    if (!continuing) {
      marketSessionDone = /* @__PURE__ */ new Map();
      marketSessionRestoreNavTarget = restoreNavTarget;
    } else if (!marketSessionRestoreNavTarget && restoreNavTarget) {
      marketSessionRestoreNavTarget = restoreNavTarget;
    }
    marketSessionActive = true;
    marketSessionRequiresModal = requiresModal;
    marketSessionStartedAt = Date.now();
    marketSessionModalSeen = false;
    marketSessionHost = requiresModal ? resolved.host : null;
    if (shell) shell.dataset.marketSession = "true";
    return continuing;
  }
  function scheduleMarketUiUpdates(revision) {
    for (const delay of [80, 240, 600, 1200]) {
      setTimeout(() => {
        if (revision === marketNavigationRevision && marketSessionActive) {
          updateMarketUi(true);
        }
      }, delay);
    }
  }
  function openMarketplace(itemHrid, enhancementLevel = 0) {
    if (marketFeaturesSuppressed()) return false;
    const resolved = resolveMarketplaceHandler();
    if (!resolved) {
      showToast(
        t9(
          "暂时无法打开市场,请先手动打开市场",
          "Could not open the market; open it manually first"
        )
      );
      return false;
    }
    const target = procurement3.normalizeItemHrid(itemHrid);
    const bareItemId = target.replace(/^\/items\//, "");
    const level = Number(enhancementLevel) || 0;
    const argumentSets = [
      [target, level],
      [target],
      [bareItemId, level],
      [bareItemId]
    ];
    const revision = ++marketNavigationRevision;
    let lastError = null;
    if (resolved.floating) {
      try {
        const newSession = !marketSessionActive || !marketSessionRequiresModal || marketSessionHost !== resolved.host;
        const restoreNavTarget = newSession && resolved.host.state?.navTarget === "marketplace" ? "marketplace" : "";
        const continuingSession = beginMarketSession(resolved, {
          requiresModal: true,
          restoreNavTarget
        });
        currentMarketTarget = target;
        currentMarketLevel = level;
        const showFloatingModal = () => {
          if (revision !== marketNavigationRevision || !marketSessionActive) {
            return;
          }
          resolved.host.setState({
            showMarketplaceModal: true,
            marketViewOverrideData: {
              itemHrid: target,
              enhancementLevel: level
            }
          });
        };
        if (restoreNavTarget) {
          resolved.host.setState(
            { navTarget: "milking", showMarketplaceModal: false },
            showFloatingModal
          );
          setTimeout(() => {
            if (marketSessionActive && revision === marketNavigationRevision && currentMarketTarget === target && resolved.host.state?.navTarget !== "marketplace") {
              resolved.fn.call(resolved.host, target, level);
            }
          }, 240);
        } else if (resolved.host.state?.showMarketplaceModal || continuingSession) {
          resolved.host.setState(
            { showMarketplaceModal: false },
            showFloatingModal
          );
        } else {
          showFloatingModal();
        }
        if (window.matchMedia?.("(max-width:760px)").matches) {
          drawerOpen = false;
          renderShell();
        }
        scheduleMarketUiUpdates(revision);
        return true;
      } catch (error) {
        lastError = error;
      }
    }
    for (const args of argumentSets) {
      try {
        resolved.fn.call(resolved.host, ...args);
        beginMarketSession(resolved, { requiresModal: false });
        currentMarketTarget = target;
        currentMarketLevel = level;
        if (window.matchMedia?.("(max-width:760px)").matches) {
          drawerOpen = false;
          renderShell();
        }
        scheduleMarketUiUpdates(revision);
        return true;
      } catch (error) {
        lastError = error;
      }
    }
    console.warn(
      runtime.config.isZH ? "[MWITools] 无法在市场中打开购物清单物品。" : "[MWITools] Failed to open the shopping-list item in the marketplace.",
      lastError
    );
    showToast(t9("市场跳转失败", "Marketplace navigation failed"));
    return false;
  }
  runtime.api.openProcurementMarketplace = openMarketplace;
  function findMarketPanel() {
    const visible2 = [
      ...document.querySelectorAll(
        '[class*="MarketplacePanel_marketplacePanel"]'
      )
    ].filter((candidate) => candidate.getClientRects().length);
    return visible2.find(
      (candidate) => candidate.closest('[class*="MainPanel_marketplaceModal__"]')
    ) ?? visible2.at(0);
  }
  function detectMarketItem(panel) {
    const current = panel?.querySelector(
      '[class*="MarketplacePanel_currentItem"] svg use, [class*="MarketplacePanel_itemContainer"] svg use'
    );
    const href = current?.getAttribute("href") ?? current?.getAttribute("xlink:href") ?? "";
    const fragment = href.split("#").at(-1);
    return fragment ? procurement3.normalizeItemHrid(fragment) : "";
  }
  function isCurrentMarketItem(item) {
    return Boolean(
      marketSessionActive && procurement3.normalizeItemHrid(item?.itemHrid) === currentMarketTarget && (Number(item?.enhancementLevel) || 0) === currentMarketLevel
    );
  }
  function removeCartItemAndAdvance(item) {
    if (!item) return false;
    const ordered = pendingItems();
    const key = procurement3.itemKey(item.itemHrid, item.enhancementLevel);
    const currentIndex = ordered.findIndex(
      (candidate) => procurement3.itemKey(candidate.itemHrid, candidate.enhancementLevel) === key
    );
    const shouldAdvance = isCurrentMarketItem(item);
    const removed = procurement3.removeFromCart(
      item.itemHrid,
      item.enhancementLevel
    );
    if (!removed.ok || !shouldAdvance) return removed.ok;
    const remaining = pendingItems();
    if (!remaining.length) {
      clearMarketUi({ closeModal: true });
      return true;
    }
    const next = remaining[Math.max(0, currentIndex) % remaining.length];
    openMarketplace(next.itemHrid, next.enhancementLevel);
    return true;
  }
  function clearMarketUi({ preserveSession = false, closeModal = false } = {}) {
    document.getElementById(MARKET_NAV_ID)?.remove();
    document.querySelectorAll(".mwi-procurement-market-target").forEach((node) => node.classList.remove("mwi-procurement-market-target"));
    if (!preserveSession) {
      const restoreHost = marketSessionHost;
      const restoreNavTarget = marketSessionRestoreNavTarget;
      marketNavigationRevision += 1;
      marketSessionActive = false;
      marketSessionRequiresModal = false;
      marketSessionStartedAt = 0;
      marketSessionModalSeen = false;
      marketSessionHost = null;
      marketSessionRestoreNavTarget = "";
      currentMarketTarget = "";
      currentMarketLevel = 0;
      armedNextItem = "";
      marketSessionDone = /* @__PURE__ */ new Map();
      if (shell) shell.dataset.marketSession = "false";
      if (closeModal && restoreHost) {
        try {
          if (typeof restoreHost.handleCloseMarketplaceModal === "function") {
            restoreHost.handleCloseMarketplaceModal();
          } else if (typeof restoreHost.setState === "function") {
            restoreHost.setState({ showMarketplaceModal: false });
          }
        } catch (error) {
          console.info("[MWITools] Marketplace modal close unavailable", error);
        }
      }
      if (restoreNavTarget && typeof restoreHost?.setState === "function" && restoreHost.state?.navTarget !== restoreNavTarget) {
        restoreHost.setState({ navTarget: restoreNavTarget });
      }
    }
  }
  function highlightMarketItems(panel, scroll = false) {
    document.querySelectorAll(".mwi-procurement-market-target").forEach((node) => node.classList.remove("mwi-procurement-market-target"));
    if (marketFeaturesSuppressed() || !procurement3.getSettings().locateEnabled) {
      return;
    }
    const pending = new Set(
      pendingItems().map((item) => item.itemHrid.split("/").at(-1))
    );
    let scrollTarget = null;
    for (const use of panel.querySelectorAll("svg use")) {
      const href = use.getAttribute("href") ?? use.getAttribute("xlink:href") ?? "";
      const matched = [...pending].find((bare) => href.includes(bare));
      if (!matched) continue;
      const host = use.closest('[class*="Item_itemContainer"]') ?? use.parentElement;
      host.classList.add("mwi-procurement-market-target");
      if (currentMarketTarget && currentMarketTarget.endsWith(matched) && !scrollTarget) {
        scrollTarget = host;
      }
    }
    if (scroll && scrollTarget) {
      scrollTarget.scrollIntoView({ behavior: "smooth", block: "center" });
    }
  }
  function prefillPurchaseModal() {
    if (marketFeaturesSuppressed() || !procurement3.getSettings().autoPrefillEnabled) {
      return;
    }
    for (const modal of document.querySelectorAll(
      '[class*="MarketplacePanel_modalContent"]'
    )) {
      if (modal.dataset.mwitoolsProcurementPrefilled) continue;
      const header = modal.querySelector('[class*="MarketplacePanel_header"]')?.textContent ?? "";
      if (!/立即购买|购买挂牌|购买订单|buy|purchase/i.test(header)) continue;
      const use = modal.querySelector("svg use");
      const href = use?.getAttribute("href") ?? use?.getAttribute("xlink:href") ?? "";
      const itemHrid = procurement3.normalizeItemHrid(href.split("#").at(-1));
      const item = procurement3.getCartItems().find((candidate) => candidate.itemHrid === itemHrid);
      const input = modal.querySelector(
        '[class*="MarketplacePanel_quantityInputs"] input'
      );
      if (!item?.quantity || !input) continue;
      const setter = Object.getOwnPropertyDescriptor(
        window.HTMLInputElement.prototype,
        "value"
      )?.set;
      setter?.call(input, String(item.quantity));
      if (!setter) input.value = String(item.quantity);
      input.dispatchEvent(new Event("input", { bubbles: true }));
      input.dispatchEvent(new Event("change", { bubbles: true }));
      modal.dataset.mwitoolsProcurementPrefilled = "true";
    }
  }
  function renderMarketNav(panel) {
    if (marketFeaturesSuppressed() || !marketSessionActive || !procurement3.getSettings().purchaseNavEnabled) {
      document.getElementById(MARKET_NAV_ID)?.remove();
      return;
    }
    const items = pendingItems();
    if (!items.length) {
      document.getElementById(MARKET_NAV_ID)?.remove();
      return;
    }
    const detectedCurrent = detectMarketItem(panel);
    const current = currentMarketTarget || detectedCurrent;
    const currentKey = procurement3.itemKey(current, currentMarketLevel);
    const rows2 = [
      ...items.map((item) => ({ ...item, done: false })),
      ...[...marketSessionDone.values()].filter(
        (done) => !items.some(
          (item) => procurement3.itemKey(item.itemHrid, item.enhancementLevel) === procurement3.itemKey(done.itemHrid, done.enhancementLevel)
        )
      )
    ];
    let nav = document.getElementById(MARKET_NAV_ID);
    if (!nav) {
      nav = document.createElement("div");
      nav.id = MARKET_NAV_ID;
      document.body.appendChild(nav);
    }
    const progressText = t9(`待购 ${items.length}`, `${items.length} pending`);
    const rowModels = rows2.map((item) => {
      const itemName4 = procurement3.resolveItemName(item.itemHrid) || item.name;
      const quantity = item.done ? t9("已完成", "Completed") : exactNumber(item.quantity);
      return {
        item,
        itemName: itemName4,
        quantity,
        current: !item.done && procurement3.itemKey(item.itemHrid, item.enhancementLevel) === currentKey,
        iconMarkup: renderItemIcon2({ ...item, name: itemName4 }),
        badge: item.done ? "✓" : formatNumber4(item.quantity)
      };
    });
    const next = items.find(
      (item) => procurement3.itemKey(item.itemHrid, item.enhancementLevel) !== currentKey
    ) ?? items.at(0) ?? null;
    const currentItem = items.find(
      (item) => procurement3.itemKey(item.itemHrid, item.enhancementLevel) === currentKey
    );
    const nextText = t9("下一项 ›", "Next ›");
    const removeText = t9("删除当前", "Remove");
    const renderSignature = JSON.stringify({
      progressText,
      nextText,
      removeText,
      currentKey: currentItem ? currentKey : "",
      next: next ? procurement3.itemKey(next.itemHrid, next.enhancementLevel) : "",
      rows: rowModels.map(
        ({
          item,
          itemName: itemName4,
          quantity,
          current: isCurrent,
          iconMarkup: iconMarkup2,
          badge
        }) => ({
          key: procurement3.itemKey(item.itemHrid, item.enhancementLevel),
          done: Boolean(item.done),
          itemName: itemName4,
          quantity,
          current: isCurrent,
          iconMarkup: iconMarkup2,
          badge
        })
      )
    });
    if (nav.mwitoolsRenderSignature !== renderSignature) {
      nav.replaceChildren();
      const progress = document.createElement("span");
      progress.className = "mwi-procurement-nav-progress";
      progress.textContent = progressText;
      const list = document.createElement("div");
      list.className = "mwi-procurement-nav-items";
      for (const model of rowModels) {
        const { item } = model;
        const chip = document.createElement("button");
        chip.className = "mwi-procurement-nav-chip";
        chip.dataset.current = String(model.current);
        chip.dataset.done = String(Boolean(item.done));
        chip.title = `${model.itemName} · ${model.quantity}`;
        chip.setAttribute("aria-label", chip.title);
        chip.innerHTML = `<span class="mwi-procurement-nav-icon">${model.iconMarkup}</span><b>${model.badge}</b>`;
        if (!item.done) {
          chip.addEventListener(
            "click",
            () => openMarketplace(item.itemHrid, item.enhancementLevel)
          );
        }
        list.append(chip);
      }
      const nextButton = document.createElement("button");
      nextButton.className = "mwi-procurement-nav-next";
      nextButton.textContent = nextText;
      nextButton.disabled = !next;
      nextButton.addEventListener("click", () => {
        if (next) openMarketplace(next.itemHrid, next.enhancementLevel);
        armedNextItem = "";
      });
      const removeButton = document.createElement("button");
      removeButton.className = "mwi-procurement-nav-remove";
      removeButton.textContent = removeText;
      removeButton.hidden = !currentItem;
      removeButton.addEventListener("click", () => {
        if (currentItem) removeCartItemAndAdvance(currentItem);
      });
      nav.append(progress, list, removeButton, nextButton);
      nav.mwitoolsRenderSignature = renderSignature;
    }
    const modal = panel.closest('[class*="MainPanel_marketplaceModal__"]') ?? panel.closest('[class*="Modal_modalContainer"]') ?? panel;
    const rect = modal.getBoundingClientRect();
    const height = nav.offsetHeight || 40;
    const below = window.innerHeight - rect.bottom;
    nav.style.left = `${rect.left}px`;
    nav.style.width = `${rect.width}px`;
    nav.style.top = `${below >= height ? rect.bottom : Math.max(0, rect.bottom - height)}px`;
    nav.dataset.inside = String(below < height);
  }
  function updateMarketUi(scroll = false) {
    const panel = findMarketPanel();
    if (!panel) {
      const waitingForModal = marketSessionActive && !marketSessionModalSeen && Date.now() - marketSessionStartedAt < MARKET_SESSION_OPEN_GRACE_MS;
      clearMarketUi({ preserveSession: waitingForModal });
      return;
    }
    if (marketSessionActive && marketSessionRequiresModal && !panel.closest('[class*="MainPanel_marketplaceModal__"]')) {
      const waitingForModal = !marketSessionModalSeen && Date.now() - marketSessionStartedAt < MARKET_SESSION_OPEN_GRACE_MS;
      clearMarketUi({ preserveSession: waitingForModal });
      if (waitingForModal) return;
    }
    if (panel.closest('[class*="MainPanel_marketplaceModal__"]')) {
      marketSessionModalSeen = true;
    }
    highlightMarketItems(panel, scroll);
    prefillPurchaseModal();
    renderMarketNav(panel);
  }
  function shortcutMatches(event, shortcut) {
    return event.code === shortcut?.code && event.ctrlKey === Boolean(shortcut.ctrl) && event.shiftKey === Boolean(shortcut.shift) && event.altKey === Boolean(shortcut.alt) && event.metaKey === Boolean(shortcut.meta);
  }
  function handleShortcut(event) {
    if (marketFeaturesSuppressed()) return;
    const shortcut = procurement3.getSettings().nextItemShortcut;
    if (!armedNextItem || !shortcut || !shortcutMatches(event, shortcut)) return;
    const active = document.activeElement;
    if (active?.matches?.("input,textarea,select") || active?.isContentEditable) {
      return;
    }
    const item = procurement3.getCartItems().find(
      (candidate) => candidate.itemHrid === armedNextItem && candidate.quantity > 0
    );
    const fallback = item ?? pendingItems().at(0);
    armedNextItem = "";
    if (!fallback) return;
    event.preventDefault();
    event.stopPropagation();
    openMarketplace(fallback.itemHrid, fallback.enhancementLevel);
  }
  function subscribeProcurement(scope) {
    const rerender = ({ reason, added } = {}) => {
      if (reason === "add" && Number(added) > 0 && procurement3.getSettings().autoExpandOnAddEnabled) {
        drawerOpen = true;
        activeTab = "cart";
      }
      renderShell();
      lastProductionSignature = "";
      renderProductionProcurement();
      updateMarketUi();
    };
    scope.add(procurement3.on("cart:change", rerender));
    scope.add(procurement3.on("plan:change", rerender));
    scope.add(procurement3.on("inventory:change", rerender));
    scope.add(
      procurement3.on("settings:change", ({ id, value }) => {
        if (id === "badgesEnabled" && !value) clearProductionUi();
        if (id === "locateEnabled" && !value) {
          document.querySelectorAll(".mwi-procurement-market-target").forEach(
            (node) => node.classList.remove("mwi-procurement-market-target")
          );
        }
        if (id === "purchaseNavEnabled" && !value) {
          document.getElementById(MARKET_NAV_ID)?.remove();
        }
        renderShell();
        lastProductionSignature = "";
      })
    );
    scope.add(
      procurement3.on("item:fulfilled", ({ item }) => {
        marketSessionDone.set(
          procurement3.itemKey(item.itemHrid, item.enhancementLevel),
          {
            ...item,
            done: true
          }
        );
        const next = pendingItems().at(0);
        armedNextItem = next?.itemHrid ?? "";
        showToast(
          next ? t9(
            `${procurement3.resolveItemName(item.itemHrid)} 已补齐,下一项:${next.name}`,
            `${procurement3.resolveItemName(item.itemHrid)} fulfilled. Next: ${next.name}`
          ) : t9("购物清单已全部补齐", "Shopping list fulfilled")
        );
        updateMarketUi();
      })
    );
    scope.add(
      procurement3.on("all:fulfilled", () => {
        clearMarketUi();
        if (procurement3.getSettings().autoCollapseEnabled) {
          drawerOpen = false;
          renderShell();
        }
      })
    );
  }
  runtime.features.register({
    id: "procurementAssistant",
    setting: "procurementAssistant",
    scope: "character",
    initialize({ scope, characterId }) {
      runtime.api.openProcurementMarketplace = openMarketplace;
      addStyles7();
      if (procurement3.activeCharacterId !== characterId) {
        procurement3.loadCharacterData(characterId);
      }
      createShell(scope);
      subscribeProcurement(scope);
      scope.add(
        runtime.settings.onChange?.("adaptIronCowMarketFeatures", () => {
          clearMarketUi();
          renderShell();
        })
      );
      scope.add(
        runtime.settings.onChange?.("hideReadyProductionShortage", () => {
          lastProductionSignature = "";
          renderProductionProcurement();
        })
      );
      renderProductionProcurement();
      updateMarketUi();
      const renderScheduler = createFrameScheduler(() => {
        renderShell();
        renderProductionProcurement();
        updateMarketUi();
      });
      const scheduleRender = () => renderScheduler.schedule();
      const MutationObserverRef = globalThis.MutationObserver ?? document.defaultView?.MutationObserver;
      const observer = new MutationObserverRef((records) => {
        const relevant = records.some((record) => {
          const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
          const changed = [...record.addedNodes, ...record.removedNodes].filter(
            (node) => node?.nodeType === 1
          );
          const removedProductionModule = [...record.removedNodes].some(
            (node) => node?.nodeType === 1 && (node.matches?.(
              `#${PRODUCTION_ID},.mwi-production-quick-inputs,#mwi-production-summary,#mwi-level-progress`
            ) || node.querySelector?.(
              `#${PRODUCTION_ID},.mwi-production-quick-inputs,#mwi-production-summary,#mwi-level-progress`
            ))
          );
          if (removedProductionModule) return true;
          const replacedProductionMount = changed.some(
            (node) => node.matches?.(
              '.mwi-production-extensions,div[class*="SkillActionDetail_regularComponent"],div[class*="SkillActionDetail_skillActionDetail"]'
            ) || node.querySelector?.(
              '.mwi-production-extensions,div[class*="SkillActionDetail_regularComponent"],div[class*="SkillActionDetail_skillActionDetail"]'
            )
          );
          if (replacedProductionMount) return true;
          if (target?.closest?.(OWNED_PROCUREMENT_SELECTOR) || changed.length && changed.every(
            (node) => node.matches?.(OWNED_PROCUREMENT_SELECTOR) || node.closest?.(OWNED_PROCUREMENT_SELECTOR)
          )) {
            return false;
          }
          if (target?.closest?.(PROCUREMENT_SURFACE_SELECTOR)) return true;
          return changed.some(
            (node) => node.matches?.(PROCUREMENT_SURFACE_SELECTOR) || node.querySelector?.(PROCUREMENT_SURFACE_SELECTOR)
          );
        });
        if (relevant) {
          runtime.api.scheduleProductionUiRecovery?.();
          scheduleRender();
          scope.timeout(scheduleRender, 80);
          scope.timeout(scheduleRender, 220);
          scope.timeout(scheduleRender, 450);
        }
      });
      scope.observer(observer, document.body, {
        childList: true,
        subtree: true
      });
      const scheduleFromInput = (event) => {
        if (event.target?.closest?.(PROCUREMENT_SURFACE_SELECTOR)) {
          scheduleRender();
        }
      };
      scope.event(document, "input", scheduleFromInput, true);
      scope.event(document, "change", scheduleFromInput, true);
      for (const messageType of [
        "items_updated",
        "skills_updated",
        "house_rooms_updated",
        "achievement_buffs_updated",
        "moo_pass_buffs_updated",
        "community_buffs_updated",
        "consumable_buffs_updated",
        "action_type_consumable_slots_updated",
        "equipment_buffs_updated",
        "personal_buffs_updated",
        "guild_buffs_updated",
        "market_item_values_updated",
        "market_item_order_books_updated",
        "init_character_data"
      ]) {
        scope.add(runtime.onMessage(messageType, scheduleRender));
      }
      scope.event(document, "keydown", handleShortcut, true);
      scope.add(() => {
        renderScheduler.cancel();
        stopActiveHoldRepeat();
        removeInventoryRefreshButtons();
        clearProductionUi();
        clearMarketUi();
        document.getElementById(STYLE_ID9)?.remove();
        runtime.api.openProcurementMarketplace = null;
      });
    }
  });
  Object.assign(runtime.api, {
    renderProcurementShell: renderShell,
    renderProductionProcurement,
    updateProcurementMarketUi: updateMarketUi,
    openProcurementMarketplace: openMarketplace
  });

  // src/features/semi-auto-train.js
  var STYLE_ID10 = "mwitools-semi-auto-train-style";
  var CONTROL_CLASS = "mwi-train-controls";
  var DETAIL_CLASS = "mwi-train-detail-modal";
  var ACTIVE_INDICATOR_ID = "mwi-train-active-indicator";
  var WIDE_WINDOW_CLASS = "mwi-train-window-wide";
  var INPUT_SELECTOR = 'div[class*="SkillActionDetail_maxActionCountInput"] input';
  var PANEL_SELECTOR = 'div[class*="SkillActionDetail_regularComponent"],div[class*="SkillActionDetail_skillActionDetail"]';
  var LOADOUT_SELECTOR = '[class*="SkillActionDetail_loadoutDropdown"]';
  var BUTTONS_SELECTOR = '[class*="SkillActionDetail_buttonsContainer"]';
  var TRAIN_TIMEOUT_MS = 6e4;
  var ACTION_NAVIGATION_HANDLERS = [
    "handleGoToActionTypeDetail",
    "handleClickActionTypeDetail",
    "handleGoToActionType",
    "handleSelectActionType",
    "handleGoToActionDetail",
    "handleSelectAction",
    "handleClickAction",
    "handleGoToAction"
  ];
  var activeTrain = null;
  var scanScheduler = createFrameScheduler(scan);
  var navigationRequestId = 0;
  function shouldScanTrainMutations(records) {
    return records.some((record) => {
      const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
      if (target?.closest?.(`.${CONTROL_CLASS},.mwi-train-toast`)) return false;
      if (target?.closest?.(PANEL_SELECTOR)) return true;
      return [...record.addedNodes ?? [], ...record.removedNodes ?? []].filter((node) => node?.nodeType === 1).some(
        (node) => node.matches?.(PANEL_SELECTOR) || node.querySelector?.(PANEL_SELECTOR)
      );
    });
  }
  function t10(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function visible(element) {
    if (!element?.isConnected || element.hidden) return false;
    const style = globalThis.getComputedStyle?.(element);
    return style?.display !== "none" && style?.visibility !== "hidden";
  }
  function showToast2(message) {
    document.querySelectorAll(".mwi-train-toast").forEach((node) => node.remove());
    const toast = document.createElement("div");
    toast.className = "mwi-train-toast";
    toast.textContent = message;
    document.body.appendChild(toast);
    setTimeout(() => toast.remove(), 2600);
  }
  function addStyles8() {
    if (document.getElementById(STYLE_ID10)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID10;
    style.textContent = `
    .${CONTROL_CLASS}{display:flex;min-width:0;max-width:100%;align-items:center;flex-wrap:wrap;justify-content:flex-end;gap:4px;margin-left:auto}
    .mwi-train-button{height:24px;padding:0 8px;border:1px solid rgba(144,166,235,.55);border-radius:4px;background:#282844;color:#e8e8ef;font:600 11px/1 Roboto,Arial,sans-serif;cursor:pointer;white-space:nowrap}
    .mwi-train-button:hover{filter:brightness(1.16)}
    .mwi-train-button:disabled{cursor:default;filter:none;opacity:.58}
    .mwi-train-button[data-kind="cancel"]{border-color:rgba(235,144,144,.55);background:#5c2a2a}
    .mwi-train-button[data-kind="cart"]{border-color:rgba(245,180,70,.65);background:#43351f}
    .mwi-train-cart-mode{height:20px;padding:0 6px;border:1px solid rgba(144,166,235,.45);border-radius:999px;background:#202033;color:#b8bfd8;font:600 10px/1 Roboto,Arial,sans-serif;cursor:pointer;white-space:nowrap}
    .mwi-train-cart-mode[aria-pressed="true"]{border-color:rgba(245,180,70,.75);background:#49381d;color:#ffe2a0}
    .${WIDE_WINDOW_CLASS}{box-sizing:border-box!important;width:min(480px,calc(100vw - 24px))!important;max-width:calc(100vw - 24px)!important;overflow-x:hidden!important}
    .${WIDE_WINDOW_CLASS}>[class*="Modal_modalContent"]{box-sizing:border-box!important;width:100%!important;max-width:100%!important;min-width:0!important;overflow-x:hidden!important}
    .${WIDE_WINDOW_CLASS} [class*="SkillActionDetail_regularComponent"],.${WIDE_WINDOW_CLASS} [class*="SkillActionDetail_skillActionDetail"]{box-sizing:border-box!important;width:100%!important;max-width:100%!important;min-width:0!important;overflow-x:hidden!important}
    .${WIDE_WINDOW_CLASS} [class*="SkillActionDetail_regularComponent"]>*{box-sizing:border-box!important;max-width:100%!important;min-width:0!important}
    .${WIDE_WINDOW_CLASS} [class*="SkillActionDetail_loadoutDropdown"]{min-width:0!important;flex-wrap:wrap!important}
    .${WIDE_WINDOW_CLASS} [class*="SkillActionDetail_buttonsContainer"]{box-sizing:border-box!important;max-width:100%!important;min-width:0!important;flex-wrap:wrap!important}
    .${WIDE_WINDOW_CLASS} .${CONTROL_CLASS}{flex:1 1 100%;width:100%;margin-left:0}
    .mwi-train-shop-target{position:relative;z-index:1;outline:3px solid #ffd257!important;outline-offset:2px;box-shadow:0 0 0 2px rgba(255,210,87,.28),0 0 18px rgba(255,188,55,.8)!important;animation:mwi-train-shop-pulse 1.1s ease-in-out infinite alternate}
    .${DETAIL_CLASS}{position:fixed;inset:0;z-index:2147483100;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.58)}
    .${DETAIL_CLASS}>section{box-sizing:border-box;width:min(470px,calc(100vw - 24px));max-height:80vh;overflow:auto;padding:16px 20px;border:1px solid #90a6eb;border-radius:8px;background:#1c1c2c;color:#e8e8ef;box-shadow:0 5px 20px rgba(0,0,0,.55);font-size:13px}
    .mwi-train-detail-title{margin-bottom:9px;padding-bottom:6px;border-bottom:1px solid #444;font-size:15px;font-weight:700}
    .mwi-train-detail-row{display:flex;align-items:center;gap:7px;padding:4px 0}.mwi-train-detail-row[data-current="true"]{color:#ffe27a}.mwi-train-detail-row>span:first-child{min-width:0;flex:1}.mwi-train-detail-count{flex:0 0 auto;color:#9fd9ff}.mwi-train-detail-terminal{color:#80df91}
    .mwi-train-detail-close{margin-top:12px}
    .mwi-train-toast{position:fixed;right:14px;top:14px;z-index:2147483200;max-width:min(380px,calc(100vw - 28px));padding:8px 11px;border:1px solid rgba(245,158,11,.55);border-radius:5px;background:rgba(15,18,28,.97);color:#eee;font-size:.75rem;box-shadow:0 8px 22px rgba(0,0,0,.4)}
    #${ACTIVE_INDICATOR_ID}{position:fixed;top:max(8px,env(safe-area-inset-top));left:50%;z-index:2147483000;max-width:calc(100vw - 24px);transform:translateX(-50%);overflow:hidden;padding:7px 13px;border:1px solid rgba(245,180,70,.78);border-radius:999px;background:rgba(42,32,18,.96);color:#ffe8a3;font:700 12px/1.15 Roboto,Arial,sans-serif;text-overflow:ellipsis;white-space:nowrap;box-shadow:0 5px 18px rgba(0,0,0,.48),0 0 8px rgba(245,180,70,.22);cursor:pointer}
    #${ACTIVE_INDICATOR_ID}:hover{filter:brightness(1.14)}
    #${ACTIVE_INDICATOR_ID}:focus-visible{outline:2px solid #ffe27a;outline-offset:2px}
    @keyframes mwi-train-shop-pulse{from{filter:brightness(1)}to{filter:brightness(1.28)}}
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function panelContext() {
    for (const input of document.querySelectorAll(INPUT_SELECTOR)) {
      if (!visible(input)) continue;
      const panel = input.closest(PANEL_SELECTOR) ?? input.parentElement;
      const actionHrid = runtime.api.resolveProductionAction?.(panel);
      if (!panel || !actionHrid) continue;
      return { panel, input, actionHrid };
    }
    return null;
  }
  function outputForAction(actionHrid) {
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    return runtime.api.getExpectedOutputs?.(detail)?.[0]?.itemHrid ?? "";
  }
  function localizedItem(itemHrid) {
    return runtime.api.procurement?.resolveItemName?.(itemHrid) ?? itemHrid;
  }
  function localizeStep(step, index) {
    const output = localizedItem(step.outputHrid);
    if (step.kind === "shop") {
      return `${index + 1}. ${t10("商店购买", "Buy from shop")}「${output}」`;
    }
    if (step.kind === "craft") {
      return `${index + 1}. ${t10("制造", "Craft")}「${output}」`;
    }
    return `${index + 1}. ${t10("升级", "Upgrade")}「${localizedItem(step.inputHrid)}」→「${output}」`;
  }
  function createButton(text, kind, handler) {
    const button = document.createElement("button");
    button.type = "button";
    button.className = "mwi-train-button";
    button.dataset.kind = kind;
    button.textContent = text;
    button.addEventListener("click", (event) => {
      event.preventDefault();
      event.stopPropagation();
      handler(event);
    });
    return button;
  }
  function closeDetail() {
    document.querySelector(`.${DETAIL_CLASS}`)?.remove();
  }
  function trainWindow(panel) {
    const container = panel?.closest?.('[class*="Modal_modalContainer"]');
    return container?.querySelector(
      ':scope > [class*="Modal_modal"]:not([class*="Modal_modalContainer"])'
    ) ?? panel?.closest?.(
      '[class*="Modal_modal"]:not([class*="Modal_modalContainer"])'
    ) ?? null;
  }
  function setTrainWindowWide(panel, wide) {
    trainWindow(panel)?.classList.toggle(WIDE_WINDOW_CLASS, Boolean(wide));
  }
  function renderActiveIndicator() {
    let indicator = document.getElementById(ACTIVE_INDICATOR_ID);
    if (!activeTrain) {
      indicator?.remove();
      return;
    }
    if (!indicator) {
      indicator = document.createElement("button");
      indicator.id = ACTIVE_INDICATOR_ID;
      indicator.type = "button";
      indicator.addEventListener(
        "click",
        () => cancelTrain(t10("用户取消", "Cancelled by user"))
      );
      document.body.appendChild(indicator);
    }
    const copy = runtime.config.isZH ? `🚂 正在进行火车 (${activeTrain.index + 1}/${activeTrain.steps.length}) · 点击退出` : `🚂 Train in progress (${activeTrain.index + 1}/${activeTrain.steps.length}) · Click to cancel`;
    if (indicator.textContent !== copy) indicator.textContent = copy;
    indicator.title = t10("点击退出当前火车", "Click to cancel the current train");
  }
  function showTrainDetail(plan, currentIndex = null) {
    closeDetail();
    const modal = document.createElement("div");
    modal.className = DETAIL_CLASS;
    const box = document.createElement("section");
    box.setAttribute("role", "dialog");
    box.setAttribute("aria-modal", "true");
    const title = document.createElement("div");
    title.className = "mwi-train-detail-title";
    title.textContent = t10("🚂 火车详情", "🚂 Train details");
    box.appendChild(title);
    plan.steps.forEach((step, index) => {
      const row = document.createElement("div");
      row.className = "mwi-train-detail-row";
      row.dataset.current = String(index === currentIndex);
      const label = document.createElement("span");
      label.textContent = `${index === currentIndex ? "▶ " : " "}${localizeStep(step, index)}`;
      const count = document.createElement("span");
      count.className = "mwi-train-detail-count";
      count.textContent = Number.isFinite(step.count) ? `× ${runtime.api.formatExactNumber?.(step.count) ?? step.count}` : t10("保持当前次数", "Keep current count");
      row.append(label, count);
      if (index === plan.steps.length - 1) {
        const terminal = document.createElement("span");
        terminal.className = "mwi-train-detail-terminal";
        terminal.textContent = t10("终点", "Final");
        row.appendChild(terminal);
      }
      box.appendChild(row);
    });
    const close = createButton(t10("关闭", "Close"), "detail", closeDetail);
    close.classList.add("mwi-train-detail-close");
    box.appendChild(close);
    modal.appendChild(box);
    modal.addEventListener("click", (event) => {
      if (event.target === modal) closeDetail();
    });
    document.body.appendChild(modal);
    return modal;
  }
  function fiberKey(element) {
    return Object.getOwnPropertyNames(element ?? {}).find(
      (key) => key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")
    );
  }
  function gameInstances() {
    const pageGlobal5 = globalThis.unsafeWindow ?? globalThis;
    const instances = [];
    if (pageGlobal5.mwi?.game) instances.push(pageGlobal5.mwi.game);
    const fibers = [];
    const rootElement = document.getElementById("root");
    for (const root of [
      rootElement?._reactRootContainer?.current,
      rootElement?._reactRootContainer?._internalRoot?.current
    ]) {
      if (root) fibers.push(root);
    }
    for (const element of [
      rootElement,
      document.querySelector('[class*="GamePage_gamePage"]'),
      document.body
    ]) {
      for (const key of Object.getOwnPropertyNames(element ?? {})) {
        if (key.startsWith("__reactContainer$") || key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")) {
          fibers.push(element[key]?.current ?? element[key]);
        }
      }
    }
    const visited = /* @__PURE__ */ new Set();
    while (fibers.length && visited.size < 5e4) {
      const fiber = fibers.pop();
      if (!fiber || visited.has(fiber)) continue;
      visited.add(fiber);
      const instance = fiber.stateNode;
      if (instance && (typeof instance.setState === "function" || ACTION_NAVIGATION_HANDLERS.some(
        (handler) => typeof instance[handler] === "function"
      )) && !instances.includes(instance)) {
        instances.push(instance);
      }
      if (fiber.return) fibers.push(fiber.return);
      if (fiber.child) fibers.push(fiber.child);
      if (fiber.sibling) fibers.push(fiber.sibling);
    }
    return instances;
  }
  function nativeNavigationLink(fragment, labelPattern) {
    return [
      ...document.querySelectorAll('[class*="NavigationBar_navigationLink"]')
    ].filter(
      (candidate) => [...candidate.classList].some(
        (name) => name.startsWith("NavigationBar_navigationLink__")
      )
    ).find((candidate) => {
      const hrefs = [...candidate.querySelectorAll("use")].map(
        (use) => use.getAttribute("href") ?? use.getAttribute("xlink:href") ?? ""
      ).join(" ").toLowerCase();
      return fragment && hrefs.includes(`#${fragment.toLowerCase()}`) || labelPattern?.test(candidate.textContent ?? "");
    });
  }
  function clickActionCard(actionHrid) {
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    const outputHrid = runtime.api.getExpectedOutputs?.(detail)?.[0]?.itemHrid;
    const bare = String(outputHrid ?? "").split("/").at(-1);
    const names = new Set(
      [detail?.name, getLocalizedEntityName("action", actionHrid)].filter(Boolean).map((name) => String(name).replaceAll(/\s+/g, " ").trim())
    );
    const card = [
      ...document.querySelectorAll('[class*="SkillAction_skillAction"]')
    ].filter(visible).find((candidate) => {
      const hrefs = [...candidate.querySelectorAll("use")].map(
        (use) => use.getAttribute("href") ?? use.getAttribute("xlink:href") ?? ""
      );
      const name = String(
        candidate.querySelector('[class*="SkillAction_name"]')?.textContent ?? ""
      ).replaceAll(/\s+/g, " ").trim();
      return bare && hrefs.some((href) => href.endsWith(`#${bare}`)) || names.has(name);
    });
    card?.click();
    return Boolean(card);
  }
  function navigateToTrainAction(actionHrid) {
    const requestId = ++navigationRequestId;
    const invoke = () => {
      for (const game of gameInstances()) {
        for (const name of ACTION_NAVIGATION_HANDLERS) {
          if (typeof game[name] !== "function") continue;
          try {
            game[name](actionHrid);
            return true;
          } catch {
          }
        }
      }
      return false;
    };
    const skill = String(actionHrid).match(/^\/actions\/([^/]+)\//)?.[1];
    if (clickActionCard(actionHrid)) return true;
    const navigation = [
      ...document.querySelectorAll(
        '[class*="NavigationBar_navigationLink"],button,a,[data-target]'
      )
    ].find((candidate) => {
      const identity = [
        candidate.getAttribute("data-target"),
        candidate.getAttribute("href"),
        candidate.getAttribute("aria-label"),
        candidate.id,
        ...[...candidate.querySelectorAll?.("use") ?? []].map(
          (use) => use.getAttribute("href") ?? use.getAttribute("xlink:href") ?? ""
        )
      ].filter(Boolean).join(" ").toLowerCase();
      return skill && identity.includes(skill);
    });
    if (navigation) {
      navigation.click();
      const startedTrain = activeTrain;
      const deadline = Date.now() + TRAIN_TIMEOUT_MS;
      const open = () => {
        if (requestId !== navigationRequestId) return;
        if (startedTrain && activeTrain !== startedTrain) return;
        if (clickActionCard(actionHrid)) return;
        if (invoke()) return;
        if (Date.now() < deadline) setTimeout(open, 100);
      };
      setTimeout(open, 150);
      return true;
    }
    return invoke();
  }
  function clearTrainShopHighlight() {
    document.querySelectorAll(".mwi-train-shop-target").forEach((node) => node.classList.remove("mwi-train-shop-target"));
  }
  function findTrainShopItem(panel, step) {
    const bare = String(step.outputHrid ?? "").split("/").at(-1);
    const itemName4 = localizedItem(step.outputHrid);
    return [...panel.querySelectorAll('[class*="ShopPanel_shopItem__"]')].filter(
      (candidate) => [...candidate.classList].some(
        (name) => name.startsWith("ShopPanel_shopItem__")
      )
    ).filter(visible).find((candidate) => {
      const hrefs = [...candidate.querySelectorAll("use")].map(
        (use) => use.getAttribute("href") ?? use.getAttribute("xlink:href") ?? ""
      );
      const name = String(
        candidate.querySelector('[class*="ShopPanel_name"]')?.textContent ?? ""
      ).trim();
      return bare && hrefs.some((href) => href.endsWith(`#${bare}`)) || name === itemName4;
    });
  }
  function navigateToTrainShop(step) {
    const requestId = ++navigationRequestId;
    const game = gameInstances().find(
      (instance) => typeof instance.setState === "function" && instance.state?.navTarget !== void 0
    );
    if (!step?.shopHrid) return false;
    let navigationAccepted = false;
    if (game) {
      try {
        if (game.state?.navTarget !== "shop")
          game.setState({ navTarget: "shop" });
        navigationAccepted = true;
      } catch {
      }
    }
    if (!navigationAccepted) {
      const navigation = nativeNavigationLink("shop", /^(商店|shop)$/i);
      if (!navigation) return false;
      navigation.click();
      navigationAccepted = true;
    }
    const deadline = Date.now() + TRAIN_TIMEOUT_MS;
    const open = () => {
      if (requestId !== navigationRequestId) return;
      if (!activeTrain || Date.now() >= deadline) return;
      const panel = [
        ...document.querySelectorAll('[class*="ShopPanel_shopPanel"]')
      ].find(visible);
      if (!panel) {
        setTimeout(open, 100);
        return;
      }
      const key = fiberKey(panel);
      let fiber = key ? panel[key] : null;
      let instance = null;
      while (fiber) {
        if (fiber.stateNode?.state?.shopItemHrid !== void 0) {
          instance = fiber.stateNode;
          break;
        }
        fiber = fiber.return;
      }
      const quantity = Math.max(1, Math.ceil(Number(step.count) || 1));
      const shopItem = findTrainShopItem(panel, step);
      if (shopItem) {
        clearTrainShopHighlight();
        shopItem.classList.add("mwi-train-shop-target");
      }
      if (instance) {
        try {
          instance.setState({
            shopItemHrid: step.shopHrid,
            shopItemQuantity: quantity,
            shopItemQuantityError: null
          });
          return;
        } catch {
        }
      }
      if (!shopItem) {
        setTimeout(open, 100);
        return;
      }
      shopItem.click();
      const fill = () => {
        if (requestId !== navigationRequestId) return;
        if (!activeTrain || Date.now() >= deadline) return;
        const input = [
          ...document.querySelectorAll(
            '[class*="ShopPanel"] input[type="number"],[class*="Modal_modal"] input[type="number"]'
          )
        ].find(visible);
        if (input) {
          setInput(input, quantity);
          return;
        }
        setTimeout(fill, 100);
      };
      fill();
    };
    open();
    return navigationAccepted;
  }
  function setInput(input, value) {
    if (!input || !Number.isFinite(value) || value <= 0) return false;
    if (runtime.api.reactInputTriggerHack) {
      runtime.api.reactInputTriggerHack(input, String(value));
      return true;
    }
    const previous = input.value;
    input.value = String(value);
    input._valueTracker?.setValue(previous);
    input.dispatchEvent(new Event("input", { bubbles: true }));
    return true;
  }
  function clearTrainListeners() {
    if (!activeTrain) return;
    clearTrainShopHighlight();
    if (activeTrain.queueButton && activeTrain.queueListener) {
      activeTrain.queueButton.removeEventListener(
        "click",
        activeTrain.queueListener,
        true
      );
    }
    activeTrain.queueButton = null;
    activeTrain.queueListener = null;
    activeTrain.inventoryUnsubscribe?.();
    activeTrain.inventoryUnsubscribe = null;
  }
  function resetTrainTimeout() {
    if (!activeTrain) return;
    clearTimeout(activeTrain.timeout);
    if (activeTrain.index >= activeTrain.steps.length - 1) {
      activeTrain.timeout = null;
      return;
    }
    activeTrain.timeout = setTimeout(
      () => cancelTrain(t10("等待下一站超时", "Timed out waiting for the next stop")),
      TRAIN_TIMEOUT_MS
    );
  }
  function queueSubmissionHost(panel) {
    const buttonsContainer = panel.querySelector(BUTTONS_SELECTOR);
    if (buttonsContainer) return buttonsContainer;
    return [...panel.querySelectorAll("button")].find(
      (button) => matchesGameTranslations(
        "skillActionDetail.buttons.addToQueue",
        button.textContent,
        { fallbackPatterns: [/添加到队列|add to queue/i] }
      )
    );
  }
  function isNativeQueueSubmission(event, host) {
    const button = event.target?.closest?.("button");
    return Boolean(
      button && host.contains(button) && !button.closest(`.${CONTROL_CLASS}`) && !button.disabled && button.getAttribute("aria-disabled") !== "true"
    );
  }
  function activeStepCount(context) {
    const step = activeTrain?.steps?.[activeTrain.index];
    if (!step) return null;
    const entered = runtime.api.trainPlanning.parseTrainCount(
      context?.input?.value
    );
    if (entered) step.count = entered;
    return Number.isFinite(step.count) && step.count > 0 ? step.count : null;
  }
  function plannedStepCount() {
    const step = activeTrain?.steps?.[activeTrain.index];
    return Number.isFinite(step?.count) && step.count > 0 ? step.count : null;
  }
  function hasPlannedProducer(itemHrid) {
    return activeTrain?.allOutputHrids?.has(itemHrid) ?? false;
  }
  function addCurrentTrainStepToCart(context = panelContext(), { usePlannedCount = false } = {}) {
    if (!activeTrain) return { ok: false, added: 0, skipped: 0 };
    const currentIndex = activeTrain.index;
    const current = activeTrain.steps[currentIndex];
    if (current.kind === "shop") {
      showToast2(
        t10("商店站点无需加入市场购物车", "Shop stops do not use the market cart")
      );
      return { ok: false, added: 0, skipped: 0 };
    }
    const count = usePlannedCount ? plannedStepCount() ?? activeStepCount(context) : activeStepCount(context);
    if (!count) {
      showToast2(t10("请先填写本步次数", "Enter the action count first"));
      return { ok: false, added: 0, skipped: 0 };
    }
    current.count = count;
    activeTrain.cartStepIndexes.add(currentIndex);
    const groups = [...activeTrain.cartStepIndexes].map((index) => {
      const step = activeTrain.steps[index];
      const requirements = runtime.api.procurement.calculateRequirements(
        step.actionHrid,
        step.count,
        { excludeActionHrids: /* @__PURE__ */ new Set() }
      );
      return requirements.materials.filter(
        (material) => !(material.itemHrid === step.inputHrid && hasPlannedProducer(step.inputHrid))
      );
    });
    const materials = runtime.api.procurement.aggregateRequirements(groups);
    const result = runtime.api.procurement.addRequirementsToCart(
      materials,
      "train"
    );
    showToast2(
      result.added ? t10(
        `本步缺料已加入购物车(${result.added} 种)`,
        `Added this stop's shortages (${result.added})`
      ) : t10("本步没有新的缺料", "No new shortages for this stop")
    );
    scheduleScan();
    return result;
  }
  function maybeAutoAddCurrentStep(context = panelContext()) {
    if (!activeTrain?.autoCartEnabled) return false;
    const currentIndex = activeTrain.index;
    const step = activeTrain.steps[currentIndex];
    if (step?.kind === "shop" || activeTrain.cartStepIndexes.has(currentIndex) || activeTrain.autoCartPendingIndex === currentIndex || !context || context.actionHrid !== step?.actionHrid) {
      return false;
    }
    activeTrain.autoCartPendingIndex = currentIndex;
    setTimeout(() => {
      if (!activeTrain?.autoCartEnabled || activeTrain.index !== currentIndex || activeTrain.cartStepIndexes.has(currentIndex)) {
        return;
      }
      const latest = panelContext();
      if (latest?.actionHrid === step.actionHrid) {
        addCurrentTrainStepToCart(latest, { usePlannedCount: true });
      }
      if (activeTrain?.index === currentIndex) {
        activeTrain.autoCartPendingIndex = null;
      }
    }, 150);
    return true;
  }
  function setTrainAutoCart(enabled, context = panelContext()) {
    if (!activeTrain) return false;
    activeTrain.autoCartEnabled = Boolean(enabled);
    activeTrain.autoCartPendingIndex = null;
    if (activeTrain.autoCartEnabled) maybeAutoAddCurrentStep(context);
    scheduleScan();
    return true;
  }
  function advanceTrain() {
    if (!activeTrain) return;
    clearTrainListeners();
    if (activeTrain.index >= activeTrain.steps.length - 1) {
      finishTrain();
      return;
    }
    activeTrain.index += 1;
    activeTrain.readyActionHrid = "";
    activeTrain.autoCartPendingIndex = null;
    renderActiveIndicator();
    goToCurrentStep();
  }
  function notifyCurrentTrainStepQueued(context = panelContext()) {
    if (!activeTrain) return false;
    activeStepCount(context);
    setTimeout(advanceTrain, 0);
    return true;
  }
  function wirePanel(context) {
    if (!activeTrain) return;
    const step = activeTrain.steps[activeTrain.index];
    if (step.kind === "shop" || context.actionHrid !== step.actionHrid) return;
    if (activeTrain.readyActionHrid !== step.actionHrid) {
      activeTrain.readyActionHrid = step.actionHrid;
      if (Number.isFinite(step.count) && step.count > 0) {
        setTimeout(() => {
          if (activeTrain?.steps?.[activeTrain.index] !== step) return;
          const latest = panelContext();
          if (latest?.actionHrid === step.actionHrid)
            setInput(latest.input, step.count);
        }, 100);
      }
      if (activeTrain.index === activeTrain.steps.length - 1 && !activeTrain.arrivalShown) {
        activeTrain.arrivalShown = true;
        showToast2(
          t10(
            "火车已到终点,请手动加入队列",
            "Final stop reached; add it to the queue manually"
          )
        );
      }
    }
    const submissionHost = queueSubmissionHost(context.panel);
    if (submissionHost && submissionHost !== activeTrain.queueButton) {
      clearTrainListeners();
      activeTrain.queueButton = submissionHost;
      activeTrain.queueListener = (event) => {
        if (isNativeQueueSubmission(event, submissionHost)) {
          notifyCurrentTrainStepQueued(context);
        }
      };
      submissionHost.addEventListener("click", activeTrain.queueListener, true);
    }
    maybeAutoAddCurrentStep(context);
    resetTrainTimeout();
  }
  function watchShopStep(step) {
    if (!activeTrain || activeTrain.inventoryUnsubscribe) return;
    const owned = runtime.api.procurement.getInventoryCount(step.outputHrid, 0);
    const target = owned + Math.max(0, Number(step.count) || 0);
    if (target <= owned) {
      advanceTrain();
      return;
    }
    activeTrain.inventoryUnsubscribe = runtime.api.procurement.on(
      "inventory:change",
      () => {
        if (!activeTrain) return;
        if (runtime.api.procurement.getInventoryCount(step.outputHrid, 0) < target) {
          return;
        }
        advanceTrain();
      }
    );
    showToast2(
      t10(
        `购买 ${step.count} 个「${localizedItem(step.outputHrid)}」后自动续站`,
        `Buy ${step.count} ${localizedItem(step.outputHrid)} to continue automatically`
      )
    );
    resetTrainTimeout();
  }
  function goToCurrentStep() {
    if (!activeTrain) return;
    const step = activeTrain.steps[activeTrain.index];
    const navigated = step.kind === "shop" ? (activeTrain.navigateShop ?? navigateToTrainShop)(step) : (activeTrain.navigateAction ?? navigateToTrainAction)(step.actionHrid);
    if (!navigated) {
      cancelTrain(t10("无法打开下一站", "Could not open the next stop"));
      return;
    }
    if (step.kind === "shop") watchShopStep(step);
    resetTrainTimeout();
    scheduleScan();
  }
  function startTrain(plan, options = {}) {
    cancelTrain("");
    if (plan?.unavailableOutputs?.length) {
      showToast2(
        t10(
          "当前茶饮使火车所需产物无法产出,请调整茶饮后重新规划",
          "Current drinks prevent a required train output; change drinks and replan"
        )
      );
      return false;
    }
    if (plan?.cycle || plan?.truncated) {
      showToast2(
        plan.cycle ? t10(
          "升级链存在循环,无法启动火车",
          "The upgrade chain contains a cycle"
        ) : t10(
          "升级链过长,无法安全启动火车",
          "The upgrade chain is too long to start safely"
        )
      );
      return false;
    }
    const allSteps = (plan?.steps ?? []).map((step) => ({ ...step }));
    const finalCount = Number(allSteps.at(-1)?.count);
    if (!Number.isFinite(finalCount) || finalCount <= 0) {
      showToast2(
        t10(
          "请先填写生产数量,再开始火车",
          "Enter a production count before starting the train"
        )
      );
      return false;
    }
    const hasPlannedCounts = allSteps.some((step) => Number.isFinite(step.count));
    const firstNeeded = allSteps.findIndex(
      (step) => !Number.isFinite(step.count) || step.count > 0
    );
    const steps = hasPlannedCounts ? allSteps.slice(firstNeeded < 0 ? allSteps.length : firstNeeded) : allSteps;
    if (!steps.length) {
      showToast2(
        t10("库存已经足够,无需开火车", "Inventory already covers this train")
      );
      return false;
    }
    activeTrain = {
      ...plan,
      steps,
      index: 0,
      cartStepIndexes: /* @__PURE__ */ new Set(),
      allOutputHrids: new Set(allSteps.map((step) => step.outputHrid)),
      queueButton: null,
      queueListener: null,
      inventoryUnsubscribe: null,
      timeout: null,
      readyActionHrid: "",
      arrivalShown: false,
      autoCartEnabled: false,
      autoCartPendingIndex: null,
      navigateAction: options.navigateAction,
      navigateShop: options.navigateShop
    };
    renderActiveIndicator();
    goToCurrentStep();
    return true;
  }
  function cancelTrain(reason = "") {
    if (!activeTrain) return false;
    navigationRequestId += 1;
    clearTrainListeners();
    clearTimeout(activeTrain.timeout);
    activeTrain = null;
    closeDetail();
    renderActiveIndicator();
    scheduleScan();
    if (reason) showToast2(`${t10("火车已停止:", "Train stopped: ")}${reason}`);
    return true;
  }
  function finishTrain() {
    if (!activeTrain) return false;
    navigationRequestId += 1;
    clearTrainListeners();
    clearTimeout(activeTrain.timeout);
    activeTrain = null;
    closeDetail();
    renderActiveIndicator();
    scheduleScan();
    showToast2(t10("火车已完成", "Train completed"));
    return true;
  }
  function getTrainState() {
    if (!activeTrain) return null;
    return {
      index: activeTrain.index,
      steps: activeTrain.steps.map((step) => ({ ...step })),
      cartStepIndexes: [...activeTrain.cartStepIndexes],
      autoCartEnabled: activeTrain.autoCartEnabled
    };
  }
  function idlePlan(context) {
    const outputHrid = outputForAction(context.actionHrid);
    if (!outputHrid) return null;
    const count = runtime.api.trainPlanning.parseTrainCount(context.input.value);
    if (count) {
      return {
        ...runtime.api.trainPlanning.createTrainPlan(outputHrid, {
          [outputHrid]: count
        }),
        hasExplicitCount: true
      };
    }
    const chain = runtime.api.trainPlanning.buildTrainChain(outputHrid);
    return {
      ...chain,
      hasExplicitCount: false,
      steps: chain.steps.map((step) => ({ ...step, count: null }))
    };
  }
  function controlsHost(context) {
    return context.panel.querySelector(LOADOUT_SELECTOR) ?? context.panel.querySelector(BUTTONS_SELECTOR) ?? context.input.parentElement;
  }
  function renderControls(context) {
    const host = controlsHost(context);
    if (!host) return;
    const runningStep = activeTrain?.steps?.[activeTrain.index];
    const relevantRunningPanel = activeTrain && (runningStep?.kind === "shop" || context.actionHrid === runningStep?.actionHrid);
    const plan = activeTrain ?? idlePlan(context);
    const shouldShow = relevantRunningPanel || !activeTrain && plan?.steps?.length >= 2;
    let controls = host.querySelector(`:scope > .${CONTROL_CLASS}`);
    setTrainWindowWide(context.panel, shouldShow);
    if (!shouldShow) {
      controls?.remove();
      return;
    }
    const signature = JSON.stringify([
      Boolean(activeTrain),
      activeTrain?.index ?? -1,
      plan.steps.length,
      runningStep?.kind ?? "",
      activeTrain?.autoCartEnabled ?? false,
      plan?.hasExplicitCount !== false,
      context.actionHrid,
      runtime.config.isZH
    ]);
    if (controls?.dataset.signature === signature) return;
    controls?.remove();
    controls = document.createElement("span");
    controls.className = CONTROL_CLASS;
    controls.dataset.signature = signature;
    controls.appendChild(
      createButton(
        t10("📋 详情", "📋 Details"),
        "detail",
        () => showTrainDetail(plan, activeTrain?.index ?? null)
      )
    );
    if (activeTrain) {
      const mode = document.createElement("button");
      mode.type = "button";
      mode.className = "mwi-train-cart-mode";
      mode.setAttribute("aria-pressed", String(activeTrain.autoCartEnabled));
      mode.textContent = activeTrain.autoCartEnabled ? t10("自动加购", "Auto cart") : t10("手动加购", "Manual cart");
      mode.title = t10(
        "切换本步材料的自动或手动加购",
        "Toggle automatic or manual step shopping"
      );
      mode.addEventListener("click", (event) => {
        event.preventDefault();
        event.stopPropagation();
        setTrainAutoCart(!activeTrain?.autoCartEnabled, context);
      });
      controls.appendChild(mode);
      const cart2 = createButton(
        runningStep.kind === "shop" ? t10("本步无需加购", "No cart items") : t10("🛒 本步加购", "🛒 Add step shortages"),
        "cart",
        () => addCurrentTrainStepToCart(context)
      );
      cart2.disabled = runningStep.kind === "shop";
      controls.appendChild(cart2);
      controls.appendChild(
        createButton(
          `🛑 ${t10("取消火车", "Cancel train")} (${activeTrain.index + 1}/${activeTrain.steps.length})`,
          "cancel",
          () => cancelTrain(t10("用户取消", "Cancelled by user"))
        )
      );
    } else {
      const start2 = createButton(
        `🚂 ${t10("开始火车", "Start train")} (${plan.steps.length}${t10("步", " stops")})`,
        "start",
        () => startTrain(idlePlan(context))
      );
      start2.disabled = plan.hasExplicitCount === false;
      if (start2.disabled) {
        start2.title = t10("请先填写生产数量", "Enter a production count first");
      }
      controls.appendChild(start2);
    }
    host.appendChild(controls);
  }
  function scan() {
    renderActiveIndicator();
    const context = panelContext();
    document.querySelectorAll(`.${WIDE_WINDOW_CLASS}`).forEach((window2) => {
      if (!context?.panel || !window2.contains(context.panel)) {
        window2.classList.remove(WIDE_WINDOW_CLASS);
      }
    });
    document.querySelectorAll(`.${CONTROL_CLASS}`).forEach((controls) => {
      if (!context?.panel.contains(controls)) controls.remove();
    });
    if (!context) return;
    if (activeTrain) wirePanel(context);
    renderControls(context);
  }
  function scheduleScan() {
    scanScheduler?.schedule();
  }
  function cleanup2() {
    cancelTrain("");
    document.querySelectorAll(`.${CONTROL_CLASS}`).forEach((node) => node.remove());
    document.querySelectorAll(".mwi-train-toast").forEach((node) => node.remove());
    document.getElementById(ACTIVE_INDICATOR_ID)?.remove();
    document.querySelectorAll(`.${WIDE_WINDOW_CLASS}`).forEach((node) => node.classList.remove(WIDE_WINDOW_CLASS));
    clearTrainShopHighlight();
    document.getElementById(STYLE_ID10)?.remove();
    closeDetail();
  }
  runtime.features.register({
    id: "semiAutoTrain",
    setting: "semiAutoTrain",
    scope: "character",
    dependsOn: ["procurementAssistant"],
    initialize({ scope }) {
      addStyles8();
      scanScheduler?.cancel();
      const scheduler = createFrameScheduler(scan);
      scanScheduler = scheduler;
      scan();
      let observerTimer = null;
      const observer = new MutationObserver((records) => {
        if (!shouldScanTrainMutations(records) || observerTimer !== null) return;
        observerTimer = setTimeout(() => {
          observerTimer = null;
          scheduler.schedule();
        }, 200);
      });
      scope.observer(observer, document.body, { childList: true, subtree: true });
      const scheduleForProductionInput = (event) => {
        if (event.target?.matches?.(INPUT_SELECTOR)) scheduleScan();
      };
      scope.event(document, "input", scheduleForProductionInput, true);
      scope.event(document, "change", scheduleForProductionInput, true);
      scope.add(() => {
        clearTimeout(observerTimer);
        observerTimer = null;
        if (scanScheduler === scheduler) scanScheduler = null;
        scheduler.cancel();
        cleanup2();
      });
    }
  });
  Object.assign(runtime.api, {
    semiAutoTrain: {
      start: startTrain,
      cancel: cancelTrain,
      finish: finishTrain,
      getState: getTrainState,
      addCurrentStepToCart: addCurrentTrainStepToCart,
      setAutoCart: setTrainAutoCart,
      notifyQueued: notifyCurrentTrainStepQueued,
      navigateToAction: navigateToTrainAction,
      navigateToShop: navigateToTrainShop
    }
  });

  // src/core/task-card-resolution.js
  var NAME_SELECTOR = '[class*="RandomTask_name"]';
  var cachedActionMap2 = null;
  var cachedLocale = "";
  var cachedActionLabels = /* @__PURE__ */ new Map();
  function normalize(value) {
    return String(value ?? "").replaceAll(/\s+/g, " ").trim().toLocaleLowerCase();
  }
  function taskCardTaskId(task) {
    const value = task?.id ?? task?.characterQuestID ?? task?.characterQuestId ?? task?.questID ?? task?.questId ?? task?.characterTaskID ?? task?.characterTaskId;
    return value === null || value === void 0 ? "" : String(value);
  }
  function fiberQuest(card) {
    const key = Object.getOwnPropertyNames(card ?? {}).find(
      (name) => name.startsWith("__reactFiber$") || name.startsWith("__reactInternalInstance$")
    );
    let fiber = key ? card[key] : null;
    for (let depth = 0; fiber && depth < 24; depth += 1) {
      for (const props of [
        fiber.memoizedProps,
        fiber.pendingProps,
        fiber.stateNode?.props
      ]) {
        const quest = props?.characterQuest ?? props?.quest ?? props?.task;
        if (quest && typeof quest === "object") return quest;
      }
      fiber = fiber.return;
    }
    return null;
  }
  function cardActionLabel(card) {
    const name = card?.querySelector(NAME_SELECTOR);
    const nativeText = [...name?.childNodes ?? []].filter((node) => node.nodeType === 3).map((node) => node.textContent ?? "").join(" ").trim();
    const title = String(nativeText || name?.textContent || "");
    return normalize(title.split(/\s[-–]\s/).at(-1));
  }
  function cardRemaining(card) {
    const text = String(card?.textContent ?? "");
    const match = text.match(
      /(?:进度|progress)\s*:?\s*([\d,.\s\u00a0\u202f]+)\s*\/\s*([\d,.\s\u00a0\u202f]+)/i
    );
    if (!match) return null;
    const current = parseCompactNumber(match[1]);
    const target = parseCompactNumber(match[2]);
    return Number.isFinite(current) && Number.isFinite(target) ? Math.max(0, target - current) : null;
  }
  function candidateLabels(task, actionHrid) {
    const actionMap = runtime.state.initData_actionDetailMap;
    const locale = getGameLocale();
    if (actionMap !== cachedActionMap2 || locale !== cachedLocale) {
      cachedActionMap2 = actionMap;
      cachedLocale = locale;
      cachedActionLabels = /* @__PURE__ */ new Map();
    }
    const monsterHrid = String(task?.monsterHrid ?? "");
    const cacheKey = `${actionHrid ?? ""}${monsterHrid}`;
    if (cachedActionLabels.has(cacheKey)) {
      return cachedActionLabels.get(cacheKey);
    }
    const detail = actionMap?.[actionHrid];
    const labels = new Set(
      [
        detail?.name,
        getLocalizedEntityName("action", actionHrid, { locale }),
        monsterHrid && getLocalizedEntityName("monster", monsterHrid, { locale }),
        String(actionHrid ?? "").split("/").at(-1)?.replaceAll("_", " "),
        monsterHrid.split("/").at(-1)?.replaceAll("_", " ")
      ].map(normalize).filter(Boolean)
    );
    cachedActionLabels.set(cacheKey, labels);
    return labels;
  }
  function candidateMatches(candidate, label, remaining) {
    if (!label || !candidate.labels.has(label)) return false;
    return remaining === null || Number(candidate.remaining) === Number(remaining);
  }
  function resolveTaskCards(cards, quests, { taskActionHrid: taskActionHrid3, taskRemaining: taskRemaining3, allowReusedPositional = false }) {
    const questList = Array.isArray(quests) ? quests : [];
    const questMetadata = questList.map((task, taskIndex) => {
      const actionHrid = taskActionHrid3(task);
      return {
        task,
        taskIndex,
        taskId: taskCardTaskId(task),
        actionHrid,
        labels: candidateLabels(task, actionHrid),
        remaining: taskRemaining3(task)
      };
    });
    const byId = new Map(
      questMetadata.map((candidate) => [candidate.taskId, candidate]).filter(([id]) => id)
    );
    const semanticIndex = /* @__PURE__ */ new Map();
    for (const candidate of questMetadata) {
      for (const label of candidate.labels) {
        let entry = semanticIndex.get(label);
        if (!entry) {
          entry = { all: [], byRemaining: /* @__PURE__ */ new Map() };
          semanticIndex.set(label, entry);
        }
        entry.all.push(candidate);
        const remainingKey = String(Number(candidate.remaining));
        if (!entry.byRemaining.has(remainingKey)) {
          entry.byRemaining.set(remainingKey, []);
        }
        entry.byRemaining.get(remainingKey).push(candidate);
      }
    }
    const used = /* @__PURE__ */ new Set();
    const rows2 = [...cards].map((card, originalIndex) => {
      let resolved = null;
      let matchSource = null;
      const label = cardActionLabel(card);
      const remaining = cardRemaining(card);
      const fiberTask = fiberQuest(card);
      const fiberId = taskCardTaskId(fiberTask);
      let fiberCandidate = fiberId ? byId.get(fiberId) : null;
      if (!fiberCandidate && fiberTask) {
        const taskIndex = questList.indexOf(fiberTask);
        if (taskIndex >= 0) fiberCandidate = questMetadata[taskIndex];
      }
      if (fiberCandidate && !used.has(fiberCandidate.taskIndex) && candidateMatches(fiberCandidate, label, remaining)) {
        resolved = fiberCandidate;
        matchSource = "fiber";
      }
      const priorId = String(card.dataset.mwitoolsTaskId ?? "");
      const prior = priorId ? byId.get(priorId) : null;
      if (!resolved && prior && !used.has(prior.taskIndex) && candidateMatches(prior, label, remaining)) {
        resolved = prior;
        matchSource = "prior";
      }
      if (Number.isInteger(resolved?.taskIndex)) used.add(resolved.taskIndex);
      return {
        card,
        originalIndex,
        resolved,
        matchSource,
        allowPositional: !fiberTask && !priorId,
        label,
        remaining
      };
    });
    const candidateCursors = /* @__PURE__ */ new Map();
    const firstUnused = (candidates) => {
      let index = candidateCursors.get(candidates) ?? 0;
      while (index < candidates.length && used.has(candidates[index].taskIndex)) {
        index += 1;
      }
      candidateCursors.set(candidates, index + 1);
      return candidates[index] ?? null;
    };
    for (const row of rows2) {
      if (row.resolved) continue;
      const entry = semanticIndex.get(row.label);
      if (!entry) continue;
      const progressCandidates = row.remaining === null ? null : entry.byRemaining.get(String(Number(row.remaining)));
      row.resolved = firstUnused(
        progressCandidates?.length ? progressCandidates : entry.all
      );
      if (!row.resolved) continue;
      row.matchSource = "semantic";
      used.add(row.resolved.taskIndex);
    }
    for (const row of rows2) {
      if (row.resolved || !row.allowPositional && !allowReusedPositional) {
        continue;
      }
      const positional = questList.length === rows2.length && !used.has(row.originalIndex) ? questMetadata[row.originalIndex] : null;
      const onlyUnused = questList.length - used.size === 1 ? questMetadata.find(({ taskIndex }) => !used.has(taskIndex)) : null;
      row.resolved = positional ?? onlyUnused;
      if (!row.resolved) continue;
      row.matchSource = "positional";
      used.add(row.resolved.taskIndex);
    }
    return rows2.map(({ card, originalIndex, resolved, matchSource }) => {
      const task = resolved?.task ?? {};
      const taskIndex = Number.isInteger(resolved?.taskIndex) ? resolved.taskIndex : -1;
      return {
        card,
        resolved: Boolean(resolved),
        matchSource,
        task,
        taskId: taskCardTaskId(task),
        taskIndex,
        originalIndex,
        actionHrid: resolved?.actionHrid ?? taskActionHrid3(task)
      };
    });
  }

  // src/features/tasks.js
  var STYLE_ID11 = "mwitools-task-style";
  var TASK_SELECTOR = 'div[class*="RandomTask_randomTask"]:not([data-mwitools-task-mirror="true"])';
  var REROLL_OPTIONS_SELECTOR = '[class*="RandomTask_rerollOptionsContainer"]';
  var RANGED_REROLL_BUTTON_SELECTOR = ".RangedWayIdleTaskButton[data-more-expensive]";
  var TASK_FILTER_LOCK_STORAGE_PREFIX = "MWITools_task_filter_locks_v1";
  var TASK_FILTER_LOCK_HOLD_MS = 1e3;
  var TASK_FILTER_LOCK_FEEDBACK_DELAY_MS = 500;
  var TASK_FILTER_LOCK_MOVE_TOLERANCE = 10;
  var TASK_MUTATION_OBSERVER_OPTIONS = Object.freeze({
    childList: true,
    characterData: true,
    subtree: true
  });
  var OWNED_TASK_SELECTOR = '.mwi-task-insight,.mwi-task-toolbar,.mwi-task-profession-group,.mwi-task-combat-location,.mwi-task-combat-mode,.mwi-task-bg,.mwi-task-merged-note,.mwi-task-merge-toast,.mwi-task-train-planner,.mwi-task-new-badge,.mwi-task-reroll-lock,[data-mwitools-task-mirror="true"]';
  var MERGE_HANDLER = /* @__PURE__ */ Symbol("mwitoolsTaskMergeHandler");
  var REROLL_LOCK_HANDLER = /* @__PURE__ */ Symbol("mwitoolsTaskRerollLockHandler");
  var REROLL_CHOICE_HANDLER = /* @__PURE__ */ Symbol("mwitoolsTaskRerollChoiceHandler");
  var originalCards = [];
  var taskListParent = null;
  var pageClassifications = /* @__PURE__ */ new Map();
  var pageTaskIds = /* @__PURE__ */ new Map();
  var pageNewTaskIds = /* @__PURE__ */ new Set();
  var pendingResetSlots = /* @__PURE__ */ new Set();
  var nativeResetChoiceUntil = 0;
  var temporaryTaskReturn = null;
  var lastRenderedCards = [];
  var lastTaskRenderSignature = "";
  var lastActionDetails = null;
  var lastActionCategories = null;
  var cachedTaskActionIndex = null;
  var cachedTaskActionIndexMap = null;
  var cachedTaskActionIndexLocale = "";
  var warnedMissingDungeonData = false;
  var taskActionCache = /* @__PURE__ */ new WeakMap();
  var taskRemainingCache = /* @__PURE__ */ new WeakMap();
  var pageOrderBySlot = /* @__PURE__ */ new Map();
  var activeProfessionFilters = /* @__PURE__ */ new Set();
  var combatFilterEnabled = false;
  var activeDungeonFilters = /* @__PURE__ */ new Set();
  var lockedTaskFilters = /* @__PURE__ */ new Set();
  var taskFilterLockStorageKey = "";
  var stickyVisibleSlots = /* @__PURE__ */ new Set();
  var pendingStickyResetSlots = /* @__PURE__ */ new Map();
  var activeRerollContext = null;
  var warnedUnexpectedRerollButtons = false;
  var rerollButtonSnapshots = /* @__PURE__ */ new WeakMap();
  var PROFESSIONS = [
    ["milking", "挤奶", "Milking"],
    ["foraging", "采摘", "Foraging"],
    ["woodcutting", "伐木", "Woodcutting"],
    ["cheesesmithing", "奶酪锻造", "Cheesesmithing"],
    ["crafting", "制作", "Crafting"],
    ["tailoring", "缝纫", "Tailoring"],
    ["cooking", "烹饪", "Cooking"],
    ["brewing", "冲泡", "Brewing"],
    ["combat", "战斗", "Combat"]
  ].map(([key, zh, en], order) => ({ key, zh, en, order }));
  var LIFE_PROFESSIONS = PROFESSIONS.filter(({ key }) => key !== "combat");
  function dungeonFilters() {
    return Object.values(runtime.state.initData_actionDetailMap ?? {}).filter((detail) => detail?.combatZoneInfo?.isDungeon).sort(
      (left, right) => Number(left.sortIndex ?? 0) - Number(right.sortIndex ?? 0)
    ).map((detail) => ({
      actionHrid: detail.hrid,
      label: getLocalizedEntityName("action", detail.hrid, {
        fallback: detail.name
      })
    }));
  }
  function t11(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function taskId(task) {
    return taskCardTaskId(task);
  }
  function taskFilterLocksStorageKey(characterId, server = globalThis.location?.hostname ?? "unknown") {
    return `${TASK_FILTER_LOCK_STORAGE_PREFIX}:${server}:${String(characterId ?? "")}`;
  }
  function normalizedTaskFilterLock(value) {
    const entry = String(value ?? "");
    const separator = entry.indexOf(":");
    if (separator <= 0) return "";
    const kind = entry.slice(0, separator);
    const filterValue = entry.slice(separator + 1);
    if (kind === "profession" && LIFE_PROFESSIONS.some(({ key }) => key === filterValue)) {
      return entry;
    }
    if (kind === "combat" && filterValue === "combat") return entry;
    if (kind === "dungeon" && filterValue.startsWith("/actions/combat/")) {
      return entry;
    }
    return "";
  }
  function readTaskFilterLocks(storageKey) {
    try {
      const value = JSON.parse(localStorage.getItem(storageKey) || "null");
      return new Set(
        (Array.isArray(value?.locked) ? value.locked : []).map(normalizedTaskFilterLock).filter(Boolean)
      );
    } catch {
      return /* @__PURE__ */ new Set();
    }
  }
  function writeTaskFilterLocks(storageKey, locks) {
    if (!storageKey) return;
    try {
      localStorage.setItem(
        storageKey,
        JSON.stringify({ locked: [...locks].sort() })
      );
    } catch {
    }
  }
  function ensureTaskFilterLockState(characterId = runtime.state.currentCharacterId) {
    const storageKey = taskFilterLocksStorageKey(characterId);
    if (taskFilterLockStorageKey === storageKey) return;
    taskFilterLockStorageKey = storageKey;
    lockedTaskFilters = readTaskFilterLocks(storageKey);
  }
  function taskFilterLockKey(kind, value) {
    return normalizedTaskFilterLock(`${kind}:${value}`);
  }
  function isTaskFilterLocked(kind, value) {
    const key = taskFilterLockKey(kind, value);
    return Boolean(key && lockedTaskFilters.has(key));
  }
  function clearTaskFilterLocks({ persist = true } = {}) {
    if (!lockedTaskFilters.size) return false;
    lockedTaskFilters.clear();
    if (persist)
      writeTaskFilterLocks(taskFilterLockStorageKey, lockedTaskFilters);
    lastTaskRenderSignature = "";
    return true;
  }
  function toggleTaskFilterLock(kind, value) {
    ensureTaskFilterLockState();
    const key = taskFilterLockKey(kind, value);
    if (!key) return false;
    if (lockedTaskFilters.has(key)) lockedTaskFilters.delete(key);
    else lockedTaskFilters.add(key);
    const locked = lockedTaskFilters.has(key);
    writeTaskFilterLocks(taskFilterLockStorageKey, lockedTaskFilters);
    lastTaskRenderSignature = "";
    for (const button of document.querySelectorAll(".mwi-task-filter")) {
      if (button.dataset.filterKind === kind && button.dataset.filterValue === value) {
        updateTaskFilterLockIndicator(button, locked);
      }
    }
    renderTasks();
    return locked;
  }
  function armTemporaryTaskReturn(expiresAt) {
    const deadline = Number(expiresAt);
    temporaryTaskReturn = Number.isFinite(deadline) ? { expiresAt: deadline, returning: false } : null;
  }
  function resumeTemporaryTaskReturn(now = Date.now()) {
    if (!temporaryTaskReturn || temporaryTaskReturn.expiresAt <= now) {
      temporaryTaskReturn = null;
      return false;
    }
    temporaryTaskReturn.returning = true;
    return true;
  }
  function cancelTemporaryTaskReturn() {
    temporaryTaskReturn = null;
  }
  function hasTemporaryTaskReturn(now = Date.now()) {
    if (!temporaryTaskReturn || temporaryTaskReturn.expiresAt <= now) {
      temporaryTaskReturn = null;
      return false;
    }
    return true;
  }
  function consumeTemporaryTaskReturn(now = Date.now()) {
    if (!hasTemporaryTaskReturn(now) || !temporaryTaskReturn.returning) {
      return false;
    }
    temporaryTaskReturn = null;
    return true;
  }
  function resetTaskFilters() {
    activeProfessionFilters.clear();
    combatFilterEnabled = false;
    activeDungeonFilters.clear();
  }
  function hasActiveTaskFilters() {
    return activeProfessionFilters.size > 0 || combatFilterEnabled || activeDungeonFilters.size > 0;
  }
  function addStyles9() {
    if (document.getElementById(STYLE_ID11)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID11;
    style.textContent = `
    @property --mwi-task-lock-angle { syntax:"<angle>"; inherits:false; initial-value:0deg; }
    [class*="TasksPanel_taskList"] { grid-template-columns:repeat(auto-fill,minmax(min(100%,270px),1fr)) !important; gap:8px !important; }
    [class*="TasksPanel_taskList"] > * { min-width:0 !important; max-width:100% !important; box-sizing:border-box !important; }
    [class*="RandomTask_randomTask"] { min-width:0 !important; }
    [class*="RandomTask_randomTask"] > [class*="RandomTask_content"] { gap:2px !important; padding:8px !important; font-size:.8125rem; }
    [class*="RandomTask_randomTask"] [class*="RandomTask_taskInfo"] { gap:2px !important; }
    [class*="RandomTask_randomTask"] [class*="RandomTask_buttonsContainer"] { margin-top:2px !important; }
    .mwi-task-toolbar { display:flex; flex-direction:column; align-items:stretch; gap:4px; margin:4px 0 8px; padding:5px; border:1px solid rgba(255,255,255,.12); border-radius:7px; background:rgba(0,0,0,.18); }
    .mwi-task-toolbar-controls { display:flex; width:100%; align-items:center; gap:4px; }
    .mwi-task-filter-groups { display:flex; width:100%; min-width:0; align-items:center; flex-wrap:nowrap; gap:4px; }
    .mwi-task-filter-group { display:flex; align-items:center; flex-wrap:wrap; gap:3px; }
    .mwi-task-filter-group--life,.mwi-task-filter-group--combat { flex-wrap:nowrap; }
    .mwi-task-filter-group--combat { flex:0 0 auto; }
    .mwi-task-dungeon-filters { display:inline-flex; align-items:center; gap:3px; padding-left:4px; border-left:1px solid rgba(255,255,255,.12); }
    .mwi-task-filter,.mwi-task-sort-button { position:relative; display:inline-flex; min-height:28px; align-items:center; justify-content:center; gap:4px; box-sizing:border-box; padding:3px 7px; border:1px solid rgba(255,255,255,.14); border-radius:5px; background:rgba(255,255,255,.08); color:var(--color-text-primary,#eee); font:inherit; font-size:.7rem; cursor:pointer; }
    .mwi-task-filter { touch-action:manipulation; user-select:none; -webkit-user-select:none; }
    .mwi-task-filter:hover,.mwi-task-sort-button:hover { background:rgba(255,255,255,.14); }
    .mwi-task-filter:disabled { opacity:.38; cursor:default; filter:saturate(.35); }
    .mwi-task-filter:focus-visible,.mwi-task-sort-button:focus-visible { outline:2px solid ${runtime.config.SCRIPT_COLOR_MAIN}; outline-offset:1px; }
    .mwi-task-filter[aria-pressed="true"] { border-color:rgba(226,181,79,.62); background:rgba(226,181,79,.18); color:#f3d58b; }
    .mwi-task-filter[aria-pressed="false"] { opacity:.38; filter:saturate(.35); }
    .mwi-task-filter-lock { position:absolute; z-index:3; top:-5px; right:-5px; display:none; width:13px; height:13px; align-items:center; justify-content:center; border:1px solid rgba(151,211,255,.85); border-radius:50%; background:#15304a; color:#dff3ff; font:700 8px/1 system-ui,sans-serif; box-shadow:0 1px 3px rgba(0,0,0,.55); pointer-events:none; }
    .mwi-task-filter[data-mwitools-task-locked="true"] > .mwi-task-filter-lock { display:inline-flex; }
    .mwi-task-filter::after { content:""; position:absolute; z-index:4; inset:-4px; border-radius:9px; padding:2px; opacity:0; background:conic-gradient(from -90deg,${runtime.config.SCRIPT_COLOR_MAIN} var(--mwi-task-lock-angle),transparent var(--mwi-task-lock-angle)); -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0); -webkit-mask-composite:xor; mask-composite:exclude; pointer-events:none; }
    .mwi-task-filter[data-mwitools-lock-pressing="true"]::after { opacity:1; animation:mwi-task-lock-progress var(--mwi-task-lock-progress-duration,${TASK_FILTER_LOCK_HOLD_MS - TASK_FILTER_LOCK_FEEDBACK_DELAY_MS}ms) linear forwards; }
    .mwi-task-filter-icon { display:inline-flex; width:18px; height:18px; flex:0 0 18px; align-items:center; justify-content:center; font-size:13px; line-height:1; }
    .mwi-task-filter-icon svg { width:100%; height:100%; }
    .mwi-task-filter-label { white-space:nowrap; }
    .mwi-task-filter-count { min-width:1.1em; color:inherit; font-weight:750; font-variant-numeric:tabular-nums; text-align:center; }
    .mwi-task-sort-button { margin-left:auto; border-color:rgba(120,174,255,.45); color:#b8d5ff; }
    ${REROLL_OPTIONS_SELECTOR} button[data-mwitools-task-lock-disabled="true"] { position:relative!important; opacity:.38!important; filter:grayscale(.72) saturate(.25)!important; cursor:not-allowed!important; }
    .mwi-task-reroll-lock { position:absolute; z-index:4; top:3px; right:3px; display:inline-flex; width:16px; height:16px; align-items:center; justify-content:center; border-radius:50%; background:rgba(20,34,48,.94); color:#dff3ff; font:700 10px/1 system-ui,sans-serif; box-shadow:0 1px 4px rgba(0,0,0,.55); pointer-events:none; }
    ${TASK_SELECTOR}[data-mwitools-filtered="true"] { display:none !important; }
    .mwi-task-bg { position:absolute; z-index:0; top:6%; right:8%; left:0; display:flex; height:88%; flex-direction:row-reverse; align-items:center; justify-content:flex-start; opacity:.3; pointer-events:none; }
    .mwi-task-bg svg { width:24%; height:100%; flex:0 0 24%; }
    ${TASK_SELECTOR} > :not(.mwi-task-bg) { position:relative; z-index:1; }
    .mwi-task-merge-toast { position:fixed; top:56px; right:14px; z-index:2147483200; max-width:min(360px,calc(100vw - 28px)); box-sizing:border-box; padding:8px 11px; border:1px solid rgba(102,205,135,.5); border-radius:6px; background:rgba(15,24,20,.97); box-shadow:0 8px 22px rgba(0,0,0,.4); color:#a8e5b7; font-size:.75rem; line-height:1.35; animation:mwi-task-toast-in .16s ease-out; }
    @keyframes mwi-task-lock-progress { from { --mwi-task-lock-angle:0deg; } to { --mwi-task-lock-angle:360deg; } }
    @keyframes mwi-task-toast-in { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:translateY(0); } }
    @media (max-width:640px) {
      .mwi-task-toolbar { gap:3px; padding:4px; }
      .mwi-task-filter-groups { flex-wrap:wrap; }
      .mwi-task-filter-group--life { min-width:0; flex:1 1 auto; flex-wrap:wrap; }
      .mwi-task-filter,.mwi-task-sort-button { min-width:28px; min-height:28px; gap:2px; padding:3px 5px; }
    }
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function nestedValue(value, keys) {
    const pending = [value];
    const visited = /* @__PURE__ */ new Set();
    while (pending.length) {
      const current = pending.shift();
      if (!current || typeof current !== "object" || visited.has(current))
        continue;
      visited.add(current);
      for (const key of keys) {
        if (current[key] !== void 0 && current[key] !== null)
          return current[key];
      }
      pending.push(
        ...Object.values(current).filter(
          (child) => child && typeof child === "object"
        )
      );
    }
    return null;
  }
  function normalizeTaskLookupName(value) {
    return String(value ?? "").trim().toLocaleLowerCase();
  }
  function getTaskActionIndex() {
    const actionMap = runtime.state.initData_actionDetailMap ?? {};
    const locale = getGameLocale();
    if (cachedTaskActionIndex && cachedTaskActionIndexMap === actionMap && cachedTaskActionIndexLocale === locale) {
      return cachedTaskActionIndex;
    }
    const combatByName = /* @__PURE__ */ new Map();
    const combatByMonster = /* @__PURE__ */ new Map();
    const dungeonsByMonster = /* @__PURE__ */ new Map();
    const zoneActionByCategory = /* @__PURE__ */ new Map();
    for (const detail of Object.values(actionMap)) {
      if (!String(detail?.hrid).startsWith("/actions/combat/")) continue;
      for (const name of [
        detail.name,
        getLocalizedEntityName("action", detail.hrid, { locale })
      ]) {
        const normalized = normalizeTaskLookupName(name);
        if (normalized && !combatByName.has(normalized)) {
          combatByName.set(normalized, detail);
        }
      }
      if (detail?.category && detail?.combatZoneInfo?.fightInfo?.battlesPerBoss === 10 && !zoneActionByCategory.has(detail.category)) {
        zoneActionByCategory.set(detail.category, detail);
      }
      const dungeonInfo = detail?.combatZoneInfo?.dungeonInfo;
      let monsters;
      if (detail?.combatZoneInfo?.isDungeon) {
        const hasDungeonSpawns = Boolean(
          dungeonInfo?.randomSpawnInfoMap || dungeonInfo?.fixedSpawnsMap
        );
        if (!hasDungeonSpawns && !warnedMissingDungeonData) {
          warnedMissingDungeonData = true;
          console.warn(
            "[MWITools] Official dungeon spawn data is unavailable; dungeon task inference is disabled."
          );
        }
        monsters = hasDungeonSpawns ? fightMonsterHrids([
          dungeonInfo.randomSpawnInfoMap,
          dungeonInfo.fixedSpawnsMap
        ]) : /* @__PURE__ */ new Set();
      } else {
        monsters = fightMonsterHrids(detail?.combatZoneInfo?.fightInfo);
      }
      for (const monsterHrid of monsters) {
        if (!combatByMonster.has(monsterHrid)) {
          combatByMonster.set(monsterHrid, detail);
        }
        if (!detail?.combatZoneInfo?.isDungeon) continue;
        if (!dungeonsByMonster.has(monsterHrid)) {
          dungeonsByMonster.set(monsterHrid, []);
        }
        dungeonsByMonster.get(monsterHrid).push(detail);
      }
    }
    cachedTaskActionIndexMap = actionMap;
    cachedTaskActionIndexLocale = locale;
    cachedTaskActionIndex = {
      combatByName,
      combatByMonster,
      dungeonsByMonster,
      zoneActionByCategory
    };
    return cachedTaskActionIndex;
  }
  function taskActionHrid(task) {
    if (!task || typeof task !== "object") return null;
    const direct = task.actionHrid ?? task.taskActionHrid ?? task.skillActionHrid ?? nestedValue(task, ["actionHrid", "taskActionHrid", "skillActionHrid"]);
    if (direct) return direct;
    const actionMap = runtime.state.initData_actionDetailMap ?? {};
    const monsterHrid = task.monsterHrid ?? nestedValue(task, ["monsterHrid"]);
    const cached = taskActionCache.get(task);
    if (cached?.actionMap === actionMap && cached.monsterHrid === monsterHrid) {
      return cached.value;
    }
    const value = getTaskActionIndex().combatByMonster.get(monsterHrid)?.hrid ?? null;
    taskActionCache.set(task, { actionMap, monsterHrid, value });
    return value;
  }
  function taskRemaining(task) {
    if (!task || typeof task !== "object") return 0;
    const targetSource = task.targetCount ?? task.requiredCount ?? task.goalCount ?? task.count ?? nestedValue(task, ["targetCount", "requiredCount", "goalCount", "count"]);
    const currentSource = task.currentCount ?? task.completedCount ?? task.progressCount ?? nestedValue(task, ["currentCount", "completedCount", "progressCount"]);
    const cached = taskRemainingCache.get(task);
    if (cached?.targetSource === targetSource && cached.currentSource === currentSource) {
      return cached.value;
    }
    const target = Number(targetSource);
    const current = Number(currentSource);
    const value = Number.isFinite(target) ? Math.max(0, target - (Number.isFinite(current) ? current : 0)) : 0;
    taskRemainingCache.set(task, { currentSource, targetSource, value });
    return value;
  }
  function taskRequiredActionCount(task) {
    const remaining = taskRemaining(task);
    const monsterHrid = normalizeMonsterHrid(
      nestedValue(task, [
        "monsterHrid",
        "targetMonsterHrid",
        "combatMonsterHrid"
      ])
    );
    if (!monsterHrid) return remaining;
    for (const detail of Object.values(
      runtime.state.initData_actionDetailMap ?? {}
    )) {
      if (detail?.combatZoneInfo?.isDungeon) continue;
      const fightInfo = detail?.combatZoneInfo?.fightInfo;
      const battlesPerBoss = Number(fightInfo?.battlesPerBoss);
      if (!(Number.isFinite(battlesPerBoss) && battlesPerBoss > 0)) continue;
      if (!fightMonsterHrids(fightInfo?.bossSpawns).has(monsterHrid)) continue;
      return remaining * battlesPerBoss;
    }
    return remaining;
  }
  function rewardValue(task) {
    let rewards = nestedValue(task, ["rewardItems", "rewards", "items"]);
    if (!Array.isArray(rewards) && task?.itemRewardsJSON) {
      try {
        rewards = JSON.parse(task.itemRewardsJSON);
      } catch {
        rewards = [];
      }
    }
    if (!Array.isArray(rewards)) return 0;
    return rewards.reduce((sum, reward) => {
      const itemHrid = reward.itemHrid ?? reward.hrid;
      const price = runtime.api.getNetSellPrice?.(
        itemHrid,
        reward.enhancementLevel ?? 0
      );
      return sum + (Number(price) || 0) * (Number(reward.count) || 0);
    }, 0);
  }
  function taskProjection(task) {
    const actionHrid = taskActionHrid(task);
    if (!actionHrid) return null;
    const remaining = taskRemaining(task);
    const projection = runtime.api.projectAction(actionHrid, remaining);
    const reward = rewardValue(task);
    return {
      ...projection,
      rewardValue: reward,
      taskProfit: projection.totalProfit === null ? null : projection.totalProfit + reward,
      taskProfitPerHour: projection.totalProfit === null || !Number.isFinite(projection.totalSeconds) || projection.totalSeconds <= 0 ? null : (projection.totalProfit + reward) / projection.totalSeconds * 3600
    };
  }
  function targetNameFromTitle(title) {
    return String(title ?? "").split(/\s[-–]\s/).slice(1).join(" - ").trim();
  }
  function itemHridFromDisplayName(name) {
    if (!name) return "";
    const normalized = name.replace(/\s+\+\d+\s*$/, "").trim();
    return resolveLocalizedEntity("item", normalized);
  }
  function namedMonsterHridForCard(card, title = visibleTaskTitle(card)) {
    const monsterName2 = targetNameFromTitle(title).replace(/\s+(?:图|Z)\s*\d+\s*$/i, "").trim();
    const translated = resolveLocalizedEntity("monster", monsterName2);
    if (String(translated).startsWith("/monsters/")) return translated;
    if (String(translated).startsWith("/actions/combat/")) {
      return String(translated).replace("/actions/combat/", "/monsters/");
    }
    const matchingAction = getTaskActionIndex().combatByName.get(
      normalizeTaskLookupName(monsterName2)
    );
    if (matchingAction?.combatZoneInfo?.isDungeon) return null;
    return matchingAction?.hrid?.replace("/actions/combat/", "/monsters/");
  }
  function normalizeMonsterHrid(value) {
    if (typeof value !== "string") return "";
    if (value.startsWith("/monsters/")) return value;
    if (!value.startsWith("/actions/combat/")) return "";
    const candidate = value.replace("/actions/combat/", "/monsters/");
    return runtime.state.initData_monsterDetailMap?.[candidate] ? candidate : "";
  }
  function fightMonsterHrids(value, result = /* @__PURE__ */ new Set(), visited = /* @__PURE__ */ new Set()) {
    if (!value || visited.has(value)) return result;
    if (typeof value === "string") {
      const normalized = normalizeMonsterHrid(value);
      if (normalized) result.add(normalized);
      return result;
    }
    if (typeof value !== "object") return result;
    visited.add(value);
    for (const child of Object.values(value)) {
      fightMonsterHrids(child, result, visited);
    }
    return result;
  }
  function monsterHridForCard(card, task, title = visibleTaskTitle(card)) {
    const named = namedMonsterHridForCard(card, title);
    if (named) return named;
    const direct = normalizeMonsterHrid(
      nestedValue(task, [
        "monsterHrid",
        "targetMonsterHrid",
        "combatMonsterHrid"
      ])
    );
    if (direct) return direct;
    const actionHrid = String(taskActionHrid(task) ?? "");
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    const fightCandidates = [
      ...fightMonsterHrids(detail?.combatZoneInfo?.fightInfo)
    ];
    if (fightCandidates.length === 1) return fightCandidates[0];
    return normalizeMonsterHrid(actionHrid) || null;
  }
  function taskMonsterHrid(task) {
    const direct = normalizeMonsterHrid(
      nestedValue(task, [
        "monsterHrid",
        "targetMonsterHrid",
        "combatMonsterHrid"
      ])
    );
    if (direct) return direct;
    const detail = runtime.state.initData_actionDetailMap?.[String(taskActionHrid(task) ?? "")];
    const monsters = [...fightMonsterHrids(detail?.combatZoneInfo?.fightInfo)];
    return monsters.length === 1 ? monsters[0] : "";
  }
  function actionContainsMonster(actionHrid, monsterHrid) {
    if (!monsterHrid) return false;
    if (normalizeMonsterHrid(actionHrid) === monsterHrid) return true;
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    return fightMonsterHrids(detail?.combatZoneInfo?.fightInfo).has(monsterHrid);
  }
  function taskArtworkForCard(card, task, context = {}) {
    const title = context.title ?? visibleTaskTitle(card);
    const profession = context.profession ?? professionForCard(card, task, title);
    if (profession.key === "combat") {
      const monsterHrid = context.monsterHrid ?? monsterHridForCard(card, task, title);
      if (monsterHrid) return { kind: "combat_monsters", hrid: monsterHrid };
    }
    const namedItemHrid = itemHridFromDisplayName(targetNameFromTitle(title));
    if (namedItemHrid) return { kind: "items", hrid: namedItemHrid };
    const actionHrid = taskActionHrid(task);
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    const outputs = [
      ...detail?.outputItems ?? [],
      ...detail?.dropTable ?? []
    ];
    const outputItemHrid = detail?.upgradeItemHrid ?? (outputs.length === 1 ? outputs[0]?.itemHrid ?? outputs[0]?.hrid : outputs.find((output) => Number(output?.dropRate ?? 1) >= 1)?.itemHrid);
    if (outputItemHrid) return { kind: "items", hrid: outputItemHrid };
    return actionHrid ? { kind: "actions", hrid: actionHrid } : null;
  }
  function taskArtworksForCard(card, task, context = {}) {
    const primary = taskArtworkForCard(card, task, context);
    if (!primary) return [];
    if (primary.kind !== "combat_monsters") return [primary];
    const dungeonLocations = context.dungeonLocations ?? dungeonLocationsForCard(card, task, context);
    const seen = /* @__PURE__ */ new Set();
    const dungeons = dungeonLocations.filter(({ isDungeon, actionHrid }) => isDungeon && actionHrid).filter(({ actionHrid }) => {
      if (seen.has(actionHrid)) return false;
      seen.add(actionHrid);
      return true;
    }).map(({ actionHrid }) => ({ kind: "actions", hrid: actionHrid }));
    return [primary, ...dungeons];
  }
  function artworkHrefs(artworks) {
    return artworks.map(({ kind, hrid }) => getGameSpriteHref(kind, hrid)).filter(Boolean);
  }
  function createArtworkSvg(href) {
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute("width", "100%");
    svg.setAttribute("height", "100%");
    svg.setAttribute("aria-hidden", "true");
    const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
    use.setAttribute("href", href);
    svg.appendChild(use);
    return svg;
  }
  function syncArtworkBackground(existing, hrefs) {
    const background = existing ?? document.createElement("div");
    if (!existing) background.className = "mwi-task-bg";
    hrefs.forEach((href, index) => {
      let svg = background.children[index];
      if (svg?.namespaceURI !== "http://www.w3.org/2000/svg") {
        const replacement = createArtworkSvg(href);
        if (svg) svg.replaceWith(replacement);
        else background.appendChild(replacement);
        svg = replacement;
      }
      let use = svg.querySelector(":scope > use");
      if (!use) {
        use = document.createElementNS("http://www.w3.org/2000/svg", "use");
        svg.appendChild(use);
      }
      if (use.getAttribute("href") !== href) use.setAttribute("href", href);
    });
    while (background.children.length > hrefs.length) {
      background.lastElementChild?.remove();
    }
    background.dataset.spriteHref = hrefs.join("\n");
    return background;
  }
  function decorateCard(card, task, artworks = null) {
    card.querySelector(".mwi-task-insight")?.remove();
    if (!runtime.settings.get("taskIcons")) {
      card.querySelector(":scope > .mwi-task-bg")?.remove();
      delete card.dataset.mwitoolsTaskIconSignature;
      return;
    }
    const hrefs = artworkHrefs(artworks ?? taskArtworksForCard(card, task));
    const signature = hrefs.join("\n");
    const existing = card.querySelector(":scope > .mwi-task-bg");
    if (!hrefs.length) {
      existing?.remove();
      card.dataset.mwitoolsTaskIconSignature = "";
      return;
    }
    if (card.dataset.mwitoolsTaskIconSignature !== signature) {
      card.dataset.mwitoolsTaskIconSignature = signature;
    }
    if (existing?.dataset.spriteHref === signature) return;
    const background = syncArtworkBackground(existing, hrefs);
    card.style.position = "relative";
    if (!existing) card.appendChild(background);
  }
  function taskIconMatches(card) {
    const existing = card.querySelector(":scope > .mwi-task-bg");
    if (!runtime.settings.get("taskIcons")) return !existing;
    if (!("mwitoolsTaskIconSignature" in card.dataset)) return false;
    const signature = card.dataset.mwitoolsTaskIconSignature;
    return signature ? existing?.dataset.spriteHref === signature : existing === null;
  }
  function visibleTaskTitle(card) {
    const name = card.querySelector('div[class*="RandomTask_name"]');
    const text = String(
      runtime.api.getOriTextFromElement?.(name ?? card) ?? name?.textContent ?? card.textContent ?? ""
    );
    return text.trim().split("\n")[0].trim();
  }
  function isQuestTaskCard(card) {
    if (!card.querySelector('div[class*="RandomTask_name"]')) return false;
    if (/(?:进度|progress)\s*:?[\s\S]*?\d[\d,.\s\u00a0\u202f]*\s*\/\s*\d/i.test(
      String(card.textContent ?? "")
    )) {
      return true;
    }
    return [...card.querySelectorAll("button")].some(
      (button) => matchesGameTranslations(
        ["randomTask.reroll", "randomTask.go", "questModal.go"],
        button.textContent,
        { fallbackPatterns: [/^(?:reset|重置|go|前往)$/i] }
      )
    );
  }
  function professionForCard(card, task, title = visibleTaskTitle(card)) {
    for (const profession of PROFESSIONS) {
      const labels = [profession.zh, profession.en];
      if (labels.some(
        (label) => title === label || title.startsWith(`${label} -`) || title.startsWith(`${label} –`)
      )) {
        return profession;
      }
    }
    if (resolveLocalizedEntity("monster", targetNameFromTitle(title))) {
      return PROFESSIONS.find(({ key: key2 }) => key2 === "combat");
    }
    const actionHrid = taskActionHrid(task);
    const actionType = runtime.state.initData_actionDetailMap?.[actionHrid]?.type;
    const key = String(actionType ?? "").split("/").pop();
    const known = PROFESSIONS.find((profession) => profession.key === key);
    if (known) return known;
    const prefix = title.split(/\s[-–]\s/)[0]?.trim() || t11("任务", "Tasks");
    return {
      key: `custom-${prefix.toLowerCase().replaceAll(/[^\p{L}\p{N}]+/gu, "-")}`,
      zh: prefix,
      en: prefix,
      order: PROFESSIONS.length - 1.5
    };
  }
  function isCompletedCard(card, task, cardText = null) {
    const target = Number(
      nestedValue(task, ["targetCount", "requiredCount", "goalCount", "count"])
    );
    if (target > 0 && taskRemaining(task) === 0) return true;
    if ([...card.querySelectorAll("button")].some(
      (button) => matchesGameTranslations(
        ["randomTask.claimReward", "questModal.claimReward"],
        button.textContent,
        { fallbackPatterns: [/claim|领取/i] }
      )
    )) {
      return true;
    }
    const text = String(
      cardText ?? runtime.api.getOriTextFromElement?.(card) ?? card.textContent ?? ""
    );
    const progress = text.match(
      /(?:进度|progress)\s*[::]\s*([\d,.\s\u00a0\u202f]+)\s*\/\s*([\d,.\s\u00a0\u202f]+)/i
    );
    if (progress) {
      const current = parseCompactNumber(progress[1]);
      const target2 = parseCompactNumber(progress[2]);
      if (Number.isFinite(current) && Number.isFinite(target2) && target2 > 0 && current >= target2) {
        return true;
      }
    }
    return false;
  }
  function combatDetailForCard(card, task, { title = visibleTaskTitle(card), monsterHrid = "" } = {}) {
    const taskDetail = runtime.state.initData_actionDetailMap?.[taskActionHrid(task)];
    const monsterName2 = targetNameFromTitle(title).replace(/\s+(?:图|Z)\s*\d+\s*$/i, "").trim();
    const translatedHrid = resolveLocalizedEntity("monster", monsterName2) || resolveLocalizedEntity("action", monsterName2);
    const resolvedMonsterHrid = monsterHrid || String(translatedHrid ?? "").replace("/actions/combat/", "/monsters/");
    const index = getTaskActionIndex();
    const translatedActionHrid = String(translatedHrid ?? "").replace(
      "/monsters/",
      "/actions/combat/"
    );
    const translatedDetail = runtime.state.initData_actionDetailMap?.[translatedActionHrid];
    const indexedDetail = index.combatByName.get(normalizeTaskLookupName(monsterName2)) ?? index.combatByMonster.get(resolvedMonsterHrid);
    if (translatedDetail ?? indexedDetail)
      return translatedDetail ?? indexedDetail;
    return String(taskDetail?.hrid ?? taskActionHrid(task)).startsWith(
      "/actions/combat/"
    ) ? taskDetail : null;
  }
  function combatLocationForCard(card, task, {
    detail = combatDetailForCard(card, task),
    title = visibleTaskTitle(card)
  } = {}) {
    const categories = runtime.state.initData_actionCategoryDetailMap ?? {};
    if (detail?.combatZoneInfo?.isDungeon) {
      const name = getLocalizedEntityName("action", detail.hrid, {
        fallback: detail.name
      });
      return {
        key: `dungeon-${detail.hrid}`,
        label: name ? `${t11("地牢", "Dungeon")} · ${name}` : t11("地牢", "Dungeon"),
        order: 1e4 + Number(detail.sortIndex ?? 0)
      };
    }
    if (detail?.category) {
      const category = categories[detail.category];
      const zoneAction = getTaskActionIndex().zoneActionByCategory.get(
        detail.category
      );
      const name = getLocalizedEntityName("action", zoneAction?.hrid, {
        fallback: zoneAction?.name ?? category?.name
      });
      const sortIndex = Number(category?.sortIndex ?? 9999);
      return {
        key: `zone-${detail.category}`,
        label: `${t11("地图", "Zone")} ${sortIndex}${name ? ` · ${name}` : ""}`,
        order: sortIndex
      };
    }
    const mapIndex = title.match(/(?:图|Z)\s*(\d+)\s*$/i)?.[1];
    if (mapIndex) {
      return {
        key: `zone-index-${mapIndex}`,
        label: `${t11("地图", "Zone")} ${mapIndex}`,
        order: Number(mapIndex)
      };
    }
    return {
      key: "non-dungeon-monsters",
      label: t11("非地牢怪物", "Non-dungeon monsters"),
      order: 99999
    };
  }
  function dungeonLocation(detail) {
    const name = getLocalizedEntityName("action", detail?.hrid, {
      fallback: detail?.name
    });
    return {
      key: `dungeon-${detail?.hrid}`,
      actionHrid: detail?.hrid ?? "",
      isDungeon: true,
      label: name || t11("地牢", "Dungeon"),
      order: Number(detail?.sortIndex ?? 9999)
    };
  }
  function nonDungeonLocation() {
    return {
      key: "non-dungeon-monsters",
      actionHrid: "",
      isDungeon: false,
      label: t11("非地牢怪物", "Non-dungeon monsters"),
      order: 99999
    };
  }
  function dungeonLocationsForCard(card, task, context = {}) {
    const actionHrid = taskActionHrid(task);
    const taskDetail = runtime.state.initData_actionDetailMap?.[actionHrid];
    const monsterHrid = context.monsterHrid ?? monsterHridForCard(card, task, context.title ?? visibleTaskTitle(card));
    if (!monsterHrid) {
      return taskDetail?.combatZoneInfo?.isDungeon ? [dungeonLocation(taskDetail)] : [nonDungeonLocation()];
    }
    const actionDetails = runtime.state.initData_actionDetailMap ?? {};
    const matchingDungeonHrids = new Set(
      (getTaskActionIndex().dungeonsByMonster.get(monsterHrid) ?? []).map(
        (detail) => detail.hrid
      )
    );
    const matches = [...matchingDungeonHrids].map((dungeonActionHrid) => actionDetails[dungeonActionHrid]).filter(Boolean).map(dungeonLocation).sort(
      (left, right) => left.order - right.order || left.label.localeCompare(right.label)
    );
    return matches.length ? matches : [nonDungeonLocation()];
  }
  function actionSortInfo(task, originalIndex) {
    const actionHrid = taskActionHrid(task);
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    if (!detail) return { originalIndex, unknown: true };
    const category = runtime.state.initData_actionCategoryDetailMap?.[detail.category];
    return {
      originalIndex,
      unknown: false,
      category: Number(category?.sortIndex ?? 9999),
      level: Number(detail.levelRequirement?.level ?? 0),
      action: Number(detail.sortIndex ?? detail.actionSortIndex ?? 0),
      name: String(detail.name ?? actionHrid),
      actionHrid
    };
  }
  function productionChains(tasks) {
    if (!runtime.settings.get("taskTrainPlanner")) return null;
    const planning2 = runtime.api.trainPlanning;
    if (!planning2) return null;
    const rows2 = tasks.map((task, index) => {
      const actionHrid = taskActionHrid(task);
      const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
      const outputHrid = runtime.api.getExpectedOutputs?.(detail)?.[0]?.itemHrid;
      const root = outputHrid ? planning2.trainChainRoot(outputHrid) : "";
      const depth = outputHrid ? planning2.trainChainDepth(outputHrid) : -1;
      return {
        actionHrid,
        root: root && depth >= 0 ? root : `task:${index}`,
        depth: Math.max(0, depth),
        index
      };
    });
    const groupOrder = /* @__PURE__ */ new Map();
    for (const row of rows2) {
      if (!groupOrder.has(row.root)) groupOrder.set(row.root, row.index);
    }
    return new Map(
      rows2.map((row) => [
        row.actionHrid,
        { depth: row.depth, group: groupOrder.get(row.root) ?? row.index }
      ])
    );
  }
  function assignStablePageSlots(cards, tasks) {
    const activeTaskIds = new Set(tasks.map(taskId).filter(Boolean));
    const knownSlotByTaskId = new Map(
      [...pageTaskIds].map(([slot, id]) => [id, slot])
    );
    const replacementSlots = [...pendingResetSlots].filter((slot) => {
      const previousId = pageTaskIds.get(slot);
      return !previousId || !activeTaskIds.has(previousId);
    });
    const usedSlots = /* @__PURE__ */ new Set();
    cards.forEach((card, index) => {
      const id = taskId(tasks[index]);
      const existingSlot = Number(card.dataset.mwitoolsOriginalIndex);
      let slot = Number.isInteger(existingSlot) ? existingSlot : void 0;
      if (slot === void 0 && id) slot = knownSlotByTaskId.get(id);
      if (slot === void 0 && replacementSlots.length) {
        slot = replacementSlots.shift();
      }
      if (slot === void 0 || usedSlots.has(slot)) {
        slot = 0;
        while (usedSlots.has(slot)) slot += 1;
      }
      usedSlots.add(slot);
      const slotValue = String(slot);
      if (card.dataset.mwitoolsOriginalIndex !== slotValue) {
        card.dataset.mwitoolsOriginalIndex = slotValue;
      }
    });
  }
  function syncPageNewTasks(cards, tasks, enteredNewTaskPage) {
    if (!runtime.settings.settingsMap.taskNewBadge.isTrue) {
      pageNewTaskIds.clear();
      cards.forEach((card, index) => {
        const id = taskId(tasks[index]);
        if (!id) return;
        const slot = Number(card.dataset.mwitoolsOriginalIndex ?? index);
        const previousId = pageTaskIds.get(slot);
        if (previousId && previousId !== id) pendingResetSlots.delete(slot);
        pageTaskIds.set(slot, id);
        if (card.dataset.mwitoolsTaskId !== id) {
          card.dataset.mwitoolsTaskId = id;
        }
      });
      runtime.state.mwitoolsPageNewTaskIds = /* @__PURE__ */ new Set();
      return false;
    }
    const previousNewTaskIds = new Set(pageNewTaskIds);
    const freshIds = new Set(runtime.api.getNewTaskIds?.() ?? []);
    const activeIds = /* @__PURE__ */ new Set();
    cards.forEach((card, index) => {
      const id = taskId(tasks[index]);
      if (!id) return;
      const slot = Number(card.dataset.mwitoolsOriginalIndex ?? index);
      activeIds.add(id);
      const previousId = pageTaskIds.get(slot);
      const changed = previousId && previousId !== id;
      if (enteredNewTaskPage || !previousId) {
        if (freshIds.has(id)) pageNewTaskIds.add(id);
      } else if (changed) {
        if (freshIds.has(id)) pageNewTaskIds.add(id);
        pendingResetSlots.delete(slot);
      } else if (freshIds.has(id)) {
        pageNewTaskIds.add(id);
      }
      pageTaskIds.set(slot, id);
      if (card.dataset.mwitoolsTaskId !== id) {
        card.dataset.mwitoolsTaskId = id;
      }
    });
    for (const id of [...pageNewTaskIds]) {
      if (!activeIds.has(id)) pageNewTaskIds.delete(id);
    }
    runtime.state.mwitoolsPageNewTaskIds = new Set(pageNewTaskIds);
    const activeFresh = [...freshIds].filter((id) => activeIds.has(id));
    if (activeFresh.length) runtime.api.acknowledgeNewTaskIds?.(activeFresh);
    return previousNewTaskIds.size !== pageNewTaskIds.size || [...pageNewTaskIds].some((id) => !previousNewTaskIds.has(id));
  }
  function cleanupListDecorations({ restoreOrder = true } = {}) {
    if (!taskListParent?.isConnected) return;
    taskListParent.querySelectorAll(
      ":scope > .mwi-task-profession-group,:scope > .mwi-task-combat-location,:scope > .mwi-task-toolbar"
    ).forEach((group) => group.remove());
    taskListParent.parentElement?.querySelectorAll(":scope > .mwi-task-combat-mode").forEach((node) => node.remove());
    taskListParent.querySelectorAll(':scope > [data-mwitools-task-mirror="true"]').forEach((node) => node.remove());
    if (!restoreOrder) return;
    taskListParent.parentElement?.querySelector(":scope > .mwi-task-toolbar")?.remove();
    taskListParent.querySelectorAll(`:scope > ${TASK_SELECTOR}`).forEach((card) => {
      card.style.order = card.dataset.mwitoolsOriginalOrder ?? "";
      delete card.dataset.mwitoolsOriginalOrder;
      delete card.dataset.mwitoolsOriginalIndex;
      delete card.dataset.mwitoolsTaskIndex;
      delete card.dataset.mwitoolsTaskId;
      delete card.dataset.mwitoolsCollapsed;
      delete card.dataset.mwitoolsProfession;
      delete card.dataset.mwitoolsTaskState;
      delete card.dataset.mwitoolsDungeonHrids;
      delete card.dataset.mwitoolsFiltered;
      delete card.dataset.mwitoolsLocation;
      delete card.dataset.mwitoolsDungeonSource;
      delete card.dataset.mwitoolsMapIndex;
      delete card.dataset.mwitoolsTaskIconSignature;
    });
  }
  function taskCardSnapshot(card, task) {
    const title = visibleTaskTitle(card);
    const cardText = String(
      runtime.api.getOriTextFromElement?.(card) ?? card.textContent ?? ""
    );
    return {
      actionHrid: taskActionHrid(task) ?? "",
      card,
      completed: isCompletedCard(card, task, cardText),
      progress: cardText.match(
        /(?:进度|progress)\s*[::]\s*[\d,.\s\u00a0\u202f]+\s*\/\s*[\d,.\s\u00a0\u202f]+/i
      )?.[0] ?? "",
      task,
      taskId: taskId(task),
      title
    };
  }
  function orderedRows(cards, tasks, snapshots = null) {
    const chains = productionChains(tasks);
    const rows2 = cards.map((card, index) => {
      const task = tasks[index];
      const snapshot = snapshots?.[index] ?? taskCardSnapshot(card, task);
      const slot = Number(card.dataset.mwitoolsOriginalIndex ?? index);
      const completed = snapshot.completed;
      const isNew = pageNewTaskIds.has(taskId(task));
      const state = isNew ? "new" : completed ? "completed" : "normal";
      const profession = professionForCard(card, task, snapshot.title);
      const monsterHrid = profession.key === "combat" ? monsterHridForCard(card, task, snapshot.title) : "";
      const combatDetail = profession.key === "combat" ? combatDetailForCard(card, task, {
        title: snapshot.title,
        monsterHrid
      }) : null;
      const location2 = profession.key === "combat" ? combatLocationForCard(card, task, {
        detail: combatDetail,
        title: snapshot.title
      }) : null;
      const mapIndex = profession.key === "combat" ? Number(
        runtime.state.initData_actionCategoryDetailMap?.[combatDetail?.category]?.sortIndex
      ) || 0 : 0;
      const dungeonLocations = profession.key === "combat" ? dungeonLocationsForCard(card, task, {
        title: snapshot.title,
        monsterHrid
      }) : [];
      const monsterGroupKey = profession.key === "combat" ? monsterHrid || snapshot.actionHrid || `combat-slot-${slot}` : "";
      const artworks = runtime.settings.get("taskIcons") ? taskArtworksForCard(card, task, {
        title: snapshot.title,
        profession,
        monsterHrid,
        dungeonLocations
      }) : [];
      const artwork = artworks[0] ?? null;
      pageClassifications.set(slot, {
        completed,
        state,
        profession,
        location: location2,
        dungeonLocations,
        monsterGroupKey,
        artwork,
        artworks,
        mapIndex
      });
      const taskState = state;
      if (card.dataset.mwitoolsTaskState !== taskState) {
        card.dataset.mwitoolsTaskState = taskState;
      }
      if (card.dataset.mwitoolsProfession !== profession.key) {
        card.dataset.mwitoolsProfession = profession.key;
      }
      const dungeonHrids = dungeonLocations.filter(({ isDungeon, actionHrid }) => isDungeon && actionHrid).map(({ actionHrid }) => actionHrid).join(",");
      if (card.dataset.mwitoolsDungeonHrids !== dungeonHrids) {
        card.dataset.mwitoolsDungeonHrids = dungeonHrids;
      }
      const mapIndexValue = mapIndex > 0 ? String(mapIndex) : "";
      if (card.dataset.mwitoolsMapIndex !== mapIndexValue) {
        card.dataset.mwitoolsMapIndex = mapIndexValue;
      }
      return {
        card,
        task,
        slot,
        state,
        profession,
        location: location2,
        dungeonLocations,
        monsterGroupKey,
        artwork,
        artworks,
        mapIndex,
        info: actionSortInfo(task, slot),
        depth: chains?.get(taskActionHrid(task))?.depth ?? 0,
        chain: chains?.get(taskActionHrid(task))?.group ?? index
      };
    });
    const stateOrder = { new: 0, completed: 1, normal: 2 };
    const firstSlotByMonster = /* @__PURE__ */ new Map();
    for (const row of rows2) {
      if (row.profession.key !== "combat") continue;
      const key = row.monsterGroupKey || `combat-slot-${row.info.originalIndex}`;
      const current = firstSlotByMonster.get(key);
      if (current === void 0 || row.info.originalIndex < current) {
        firstSlotByMonster.set(key, row.info.originalIndex);
      }
    }
    rows2.sort((left, right) => {
      const taskStateOrder = stateOrder[left.state] - stateOrder[right.state];
      if (taskStateOrder) return taskStateOrder;
      const professionOrder = left.profession.order - right.profession.order;
      if (professionOrder) return professionOrder;
      if (left.profession.key === "combat") {
        const locationOrder = Number(left.location?.order ?? 99999) - Number(right.location?.order ?? 99999);
        if (locationOrder) return locationOrder;
        const leftKey = left.monsterGroupKey || `combat-slot-${left.info.originalIndex}`;
        const rightKey = right.monsterGroupKey || `combat-slot-${right.info.originalIndex}`;
        return firstSlotByMonster.get(leftKey) - firstSlotByMonster.get(rightKey) || left.info.originalIndex - right.info.originalIndex;
      }
      if (!chains) {
        return left.info.originalIndex - right.info.originalIndex;
      }
      if (!runtime.settings.get("taskAutoSort")) {
        return left.chain - right.chain || left.depth - right.depth || left.info.originalIndex - right.info.originalIndex;
      }
      if (left.info.unknown && right.info.unknown)
        return left.info.originalIndex - right.info.originalIndex;
      if (left.info.unknown) return 1;
      if (right.info.unknown) return -1;
      return left.info.category - right.info.category || left.chain - right.chain || left.depth - right.depth || left.info.level - right.info.level || left.info.action - right.info.action || left.info.name.localeCompare(right.info.name) || left.info.originalIndex - right.info.originalIndex;
    });
    return rows2;
  }
  function updatePressedState(button, pressed) {
    const value = String(Boolean(pressed));
    if (button.getAttribute("aria-pressed") !== value) {
      button.setAttribute("aria-pressed", value);
    }
  }
  function syncTaskFilterPressedIndicators(root = document) {
    for (const button of root.querySelectorAll?.(".mwi-task-filter") ?? []) {
      const { filterKind: kind, filterValue: value } = button.dataset;
      if (kind === "profession") {
        updatePressedState(button, activeProfessionFilters.has(value));
      } else if (kind === "combat") {
        updatePressedState(button, combatFilterEnabled);
      } else if (kind === "dungeon") {
        updatePressedState(button, activeDungeonFilters.has(value));
      }
    }
  }
  function wireTaskFilterLongPress(button, onLongPress, {
    holdMs = TASK_FILTER_LOCK_HOLD_MS,
    feedbackDelayMs = TASK_FILTER_LOCK_FEEDBACK_DELAY_MS,
    moveTolerance = TASK_FILTER_LOCK_MOVE_TOLERANCE
  } = {}) {
    let press = null;
    let suppressClickUntil = 0;
    const progressDelay = Math.min(
      Math.max(0, Number(feedbackDelayMs) || 0),
      Math.max(0, Number(holdMs) || 0)
    );
    const progressDuration = Math.max(0, holdMs - progressDelay);
    const cancelPress = () => {
      if (!press) return;
      clearTimeout(press.timer);
      clearTimeout(press.feedbackTimer);
      press = null;
      delete button.dataset.mwitoolsLockPressing;
      button.style.removeProperty("--mwi-task-lock-progress-duration");
    };
    const finishPress = (event) => {
      if (!press || press.pointerId !== event.pointerId) return;
      cancelPress();
    };
    button.addEventListener("pointerdown", (event) => {
      if (button.disabled || event.button !== void 0 && event.button !== 0) {
        return;
      }
      cancelPress();
      suppressClickUntil = 0;
      const current = {
        pointerId: event.pointerId,
        x: Number(event.clientX) || 0,
        y: Number(event.clientY) || 0,
        timer: null,
        feedbackTimer: null
      };
      current.timer = setTimeout(() => {
        if (press !== current || !button.isConnected || button.disabled) {
          cancelPress();
          return;
        }
        suppressClickUntil = Date.now() + 700;
        onLongPress();
      }, holdMs);
      press = current;
      const showProgress = () => {
        if (press !== current || !button.isConnected || button.disabled) return;
        button.style.setProperty(
          "--mwi-task-lock-progress-duration",
          `${progressDuration}ms`
        );
        button.dataset.mwitoolsLockPressing = "true";
      };
      if (progressDelay > 0) {
        current.feedbackTimer = setTimeout(showProgress, progressDelay);
      } else {
        showProgress();
      }
      try {
        if (event.pointerId !== void 0) {
          button.setPointerCapture?.(event.pointerId);
        }
      } catch {
      }
    });
    button.addEventListener("pointermove", (event) => {
      if (!press || press.pointerId !== event.pointerId) return;
      if (Math.hypot(
        (Number(event.clientX) || 0) - press.x,
        (Number(event.clientY) || 0) - press.y
      ) > moveTolerance) {
        cancelPress();
      }
    });
    button.addEventListener("pointerup", finishPress);
    button.addEventListener("pointercancel", finishPress);
    button.addEventListener("lostpointercapture", cancelPress);
    button.addEventListener("contextmenu", (event) => {
      if (!press && Date.now() > suppressClickUntil) return;
      event.preventDefault();
    });
    button.addEventListener(
      "click",
      (event) => {
        if (Date.now() > suppressClickUntil) return;
        suppressClickUntil = 0;
        event.preventDefault();
        event.stopImmediatePropagation();
      },
      true
    );
    return cancelPress;
  }
  function createTaskFilterButton({
    kind,
    value,
    label,
    iconKind = "",
    iconHrid = "",
    fallback = "•",
    showLabel = false,
    showCount = true,
    onClick,
    onLongPress = null
  }) {
    const button = document.createElement("button");
    button.type = "button";
    button.className = "mwi-task-filter";
    button.dataset.filterKind = kind;
    button.dataset.filterValue = value;
    button.dataset.iconKind = iconKind;
    button.dataset.iconHrid = iconHrid;
    button.dataset.iconFallback = fallback;
    button.title = label;
    button.setAttribute("aria-label", label);
    const icon = document.createElement("span");
    icon.className = "mwi-task-filter-icon";
    const count = document.createElement("span");
    count.className = "mwi-task-filter-count";
    button.append(icon);
    if (showLabel) {
      const text = document.createElement("span");
      text.className = "mwi-task-filter-label";
      text.textContent = label;
      button.append(text);
    }
    if (showCount) button.append(count);
    if (onLongPress) {
      wireTaskFilterLongPress(button, onLongPress);
    }
    button.addEventListener("click", onClick);
    return button;
  }
  function updateTaskFilterIcon(button) {
    const icon = button.querySelector(".mwi-task-filter-icon");
    if (!icon) return;
    const href = button.dataset.iconKind ? getGameSpriteHref(button.dataset.iconKind, button.dataset.iconHrid) : "";
    const signature = href || `fallback:${button.dataset.iconFallback}`;
    if (icon.dataset.signature === signature) return;
    icon.dataset.signature = signature;
    icon.replaceChildren();
    if (!href) {
      icon.textContent = button.dataset.iconFallback || "•";
      return;
    }
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute("aria-hidden", "true");
    const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
    use.setAttribute("href", href);
    svg.append(use);
    icon.append(svg);
  }
  function updateTaskFilterLockIndicator(button, locked) {
    let lock = button.querySelector(":scope > .mwi-task-filter-lock");
    if (locked) {
      button.dataset.mwitoolsTaskLocked = "true";
      if (!lock) {
        lock = document.createElement("span");
        lock.className = "mwi-task-filter-lock";
        lock.textContent = "🔒";
        lock.setAttribute("aria-hidden", "true");
        button.append(lock);
      }
    } else {
      delete button.dataset.mwitoolsTaskLocked;
      lock?.remove();
    }
    const title = button.title;
    const accessibleLabel = locked ? `${title} · ${t11("已锁定", "Locked")}` : title;
    if (button.getAttribute("aria-label") !== accessibleLabel) {
      button.setAttribute("aria-label", accessibleLabel);
    }
  }
  function updateTaskFilterButton(button, { label, count, pressed, locked }) {
    updatePressedState(button, pressed);
    const countText = String(count);
    const countNode = button.querySelector(".mwi-task-filter-count");
    if (countNode?.textContent !== countText) countNode.textContent = countText;
    const title = `${label} (${countText})`;
    if (button.title !== title) button.title = title;
    updateTaskFilterLockIndicator(button, locked);
    updateTaskFilterIcon(button);
  }
  function taskMatchesLockedFilters(classification) {
    if (!classification || !runtime.settings.get("taskStatistics")) return false;
    if (classification.profession?.key !== "combat" && isTaskFilterLocked("profession", classification.profession?.key)) {
      return true;
    }
    if (classification.profession?.key === "combat" && isTaskFilterLocked("combat", "combat")) {
      return true;
    }
    return classification.dungeonLocations?.some(
      ({ isDungeon, actionHrid }) => isDungeon && isTaskFilterLocked("dungeon", actionHrid)
    );
  }
  function applyTaskFilters(rows2) {
    const statisticsEnabled = runtime.settings.get("taskStatistics");
    const hasActiveFilters = hasActiveTaskFilters();
    for (const row of rows2) {
      let visible2 = true;
      if (statisticsEnabled && hasActiveFilters) {
        const professionMatches = activeProfessionFilters.has(row.profession.key);
        const combatMatches = combatFilterEnabled && row.profession.key === "combat";
        const dungeonMatches = row.profession.key === "combat" && row.dungeonLocations.some(
          ({ isDungeon, actionHrid }) => isDungeon && activeDungeonFilters.has(actionHrid)
        );
        visible2 = stickyVisibleSlots.has(row.slot) || professionMatches || combatMatches || dungeonMatches;
      }
      const filtered = String(!visible2);
      if (row.card.dataset.mwitoolsFiltered !== filtered) {
        row.card.dataset.mwitoolsFiltered = filtered;
      }
    }
  }
  function ensureTaskToolbar(rows2) {
    if (!taskListParent?.isConnected) return;
    const statisticsEnabled = runtime.settings.get("taskStatistics");
    const signature = [runtime.config.isZH, statisticsEnabled].join(":");
    let toolbar = taskListParent.parentElement?.querySelector(
      ":scope > .mwi-task-toolbar"
    );
    if (toolbar?.dataset.signature !== signature) {
      toolbar?.remove();
      toolbar = document.createElement("div");
      toolbar.className = "mwi-task-toolbar";
      toolbar.dataset.signature = signature;
      toolbar.setAttribute("role", "toolbar");
      toolbar.setAttribute(
        "aria-label",
        t11("任务排序与筛选", "Task sorting and filters")
      );
      if (statisticsEnabled) {
        const controls2 = document.createElement("div");
        controls2.className = "mwi-task-toolbar-controls";
        controls2.append(
          createTaskFilterButton({
            kind: "reset",
            value: "reset",
            label: t11("重置筛选", "Reset filters"),
            fallback: "↺",
            showLabel: true,
            showCount: false,
            onClick: () => {
              resetTaskFilters();
              syncTaskFilterPressedIndicators();
              lastTaskRenderSignature = "";
              renderTasks();
            }
          })
        );
        toolbar.append(controls2);
        const filterGroups = document.createElement("div");
        filterGroups.className = "mwi-task-filter-groups";
        const lifeFilters = document.createElement("div");
        lifeFilters.className = "mwi-task-filter-group mwi-task-filter-group--life";
        for (const profession of LIFE_PROFESSIONS) {
          lifeFilters.append(
            createTaskFilterButton({
              kind: "profession",
              value: profession.key,
              label: runtime.config.isZH ? profession.zh : profession.en,
              iconKind: "skills",
              iconHrid: profession.key,
              fallback: (runtime.config.isZH ? profession.zh : profession.en)[0],
              onLongPress: () => toggleTaskFilterLock("profession", profession.key),
              onClick: () => {
                if (activeProfessionFilters.has(profession.key)) {
                  activeProfessionFilters.delete(profession.key);
                } else {
                  activeProfessionFilters.add(profession.key);
                }
                syncTaskFilterPressedIndicators();
                lastTaskRenderSignature = "";
                renderTasks();
              }
            })
          );
        }
        filterGroups.append(lifeFilters);
        const combatFilters = document.createElement("div");
        combatFilters.className = "mwi-task-filter-group mwi-task-filter-group--combat";
        combatFilters.append(
          createTaskFilterButton({
            kind: "combat",
            value: "combat",
            label: t11("战斗", "Combat"),
            iconKind: "misc",
            iconHrid: "combat",
            fallback: "⚔",
            onLongPress: () => toggleTaskFilterLock("combat", "combat"),
            onClick: () => {
              combatFilterEnabled = !combatFilterEnabled;
              syncTaskFilterPressedIndicators();
              lastTaskRenderSignature = "";
              renderTasks();
            }
          })
        );
        const dungeons = document.createElement("div");
        dungeons.className = "mwi-task-dungeon-filters";
        for (const dungeon of dungeonFilters()) {
          dungeons.append(
            createTaskFilterButton({
              kind: "dungeon",
              value: dungeon.actionHrid,
              label: dungeon.label,
              iconKind: "actions",
              iconHrid: dungeon.actionHrid,
              fallback: "◆",
              onLongPress: () => toggleTaskFilterLock("dungeon", dungeon.actionHrid),
              onClick: () => {
                if (activeDungeonFilters.has(dungeon.actionHrid)) {
                  activeDungeonFilters.delete(dungeon.actionHrid);
                } else {
                  activeDungeonFilters.add(dungeon.actionHrid);
                }
                syncTaskFilterPressedIndicators();
                lastTaskRenderSignature = "";
                renderTasks();
              }
            })
          );
        }
        combatFilters.append(dungeons);
        filterGroups.append(combatFilters);
        toolbar.append(filterGroups);
      }
      const sortButton = document.createElement("button");
      sortButton.type = "button";
      sortButton.className = "mwi-task-sort-button";
      sortButton.title = t11("重新排序任务", "Sort tasks again");
      sortButton.setAttribute("aria-label", sortButton.title);
      const sortIcon = document.createElement("span");
      sortIcon.className = "mwi-task-filter-icon";
      sortIcon.textContent = "↕";
      const sortLabel = document.createElement("span");
      sortLabel.className = "mwi-task-filter-label";
      sortLabel.textContent = t11("任务排序", "Sort tasks");
      sortButton.append(sortIcon, sortLabel);
      sortButton.addEventListener("click", () => sortTasks());
      const controls = toolbar.querySelector(":scope > .mwi-task-toolbar-controls") ?? toolbar;
      controls.append(sortButton);
      taskListParent.insertAdjacentElement("beforebegin", toolbar);
    }
    if (!statisticsEnabled) return;
    const professionCounts = new Map(LIFE_PROFESSIONS.map(({ key }) => [key, 0]));
    const currentDungeonFilters = dungeonFilters();
    const dungeonCounts = new Map(
      currentDungeonFilters.map(({ actionHrid }) => [actionHrid, 0])
    );
    let combatCount = 0;
    for (const row of rows2) {
      if (professionCounts.has(row.profession.key)) {
        professionCounts.set(
          row.profession.key,
          professionCounts.get(row.profession.key) + 1
        );
      }
      if (row.profession.key !== "combat") continue;
      combatCount += 1;
      for (const { isDungeon, actionHrid } of row.dungeonLocations) {
        if (isDungeon && dungeonCounts.has(actionHrid)) {
          dungeonCounts.set(actionHrid, dungeonCounts.get(actionHrid) + 1);
        }
      }
    }
    const resetButton = toolbar.querySelector('[data-filter-kind="reset"]');
    resetButton.disabled = !hasActiveTaskFilters();
    resetButton.title = t11("清除全部任务筛选", "Clear all task filters");
    resetButton.setAttribute("aria-label", resetButton.title);
    for (const profession of LIFE_PROFESSIONS) {
      const button = toolbar.querySelector(
        `[data-filter-kind="profession"][data-filter-value="${profession.key}"]`
      );
      updateTaskFilterButton(button, {
        label: runtime.config.isZH ? profession.zh : profession.en,
        count: professionCounts.get(profession.key),
        pressed: activeProfessionFilters.has(profession.key),
        locked: isTaskFilterLocked("profession", profession.key)
      });
    }
    updateTaskFilterButton(toolbar.querySelector('[data-filter-kind="combat"]'), {
      label: t11("战斗", "Combat"),
      count: combatCount,
      pressed: combatFilterEnabled,
      locked: isTaskFilterLocked("combat", "combat")
    });
    for (const dungeon of currentDungeonFilters) {
      const button = toolbar.querySelector(
        `[data-filter-kind="dungeon"][data-filter-value="${dungeon.actionHrid}"]`
      );
      updateTaskFilterButton(button, {
        label: dungeon.label,
        count: dungeonCounts.get(dungeon.actionHrid),
        pressed: activeDungeonFilters.has(dungeon.actionHrid),
        locked: isTaskFilterLocked("dungeon", dungeon.actionHrid)
      });
    }
  }
  function applyExplicitSort(rows2) {
    rows2.forEach((row, index) => {
      const order = index + 1;
      const value = String(order);
      if (row.card.style.order !== value) row.card.style.order = value;
      pageOrderBySlot.set(row.slot, order);
    });
  }
  function restoreStableOrders(rows2) {
    let nextOrder = Math.max(0, ...pageOrderBySlot.values()) + 1;
    for (const row of rows2) {
      let order = pageOrderBySlot.get(row.slot);
      if (!Number.isFinite(order)) {
        const current = Number(row.card.style.order);
        order = row.card.style.order && Number.isFinite(current) ? current : nextOrder++;
        pageOrderBySlot.set(row.slot, order);
      }
      const value = String(order);
      if (row.card.style.order !== value) row.card.style.order = value;
    }
  }
  function renderFlatTaskList(rows2, { sort = false } = {}) {
    if (!taskListParent) return;
    cleanupListDecorations({ restoreOrder: false });
    if (sort) applyExplicitSort(rows2);
    else restoreStableOrders(rows2);
    ensureTaskToolbar(rows2);
    applyTaskFilters(rows2);
    return rows2;
  }
  function isMergeNavigationButton(candidate) {
    return matchesGameTranslations(
      ["randomTask.go", "questModal.go"],
      candidate?.textContent,
      { fallbackPatterns: [/^(?:go|前往|开始)$/i] }
    );
  }
  function liveTaskForCard(card, tasks) {
    const parent = card?.parentElement;
    if (!parent) return null;
    const cards = [...document.querySelectorAll(TASK_SELECTOR)].filter(
      (candidate) => candidate.parentElement === parent
    );
    return resolveTaskCards(cards, tasks, {
      taskActionHrid,
      taskRemaining
    }).find((entry) => entry.card === card)?.task ?? null;
  }
  function wireMergeButtons(cards) {
    cards.forEach((card) => {
      if (card[MERGE_HANDLER]) return;
      if (![...card.querySelectorAll("button")].some(isMergeNavigationButton)) {
        return;
      }
      const handler = (event) => {
        const button = event.target?.closest?.("button");
        if (!button || !card.contains(button) || !isMergeNavigationButton(button) || !runtime.settings.get("taskMergeActions")) {
          return;
        }
        const tasks = runtime.state.characterQuests ?? [];
        const currentTask = liveTaskForCard(card, tasks);
        const actionHrid = taskActionHrid(currentTask);
        if (!actionHrid) return;
        const monsterHrid = monsterHridForCard(card, currentTask);
        const matching = tasks.filter(
          (task) => monsterHrid ? taskMonsterHrid(task) === monsterHrid : taskActionHrid(task) === actionHrid
        );
        if (!matching.length) return;
        const pendingMergedTask = {
          actionHrid,
          count: matching.reduce(
            (sum, task) => sum + taskRequiredActionCount(task),
            0
          ),
          taskCount: matching.length
        };
        if (monsterHrid) {
          Object.defineProperty(pendingMergedTask, "monsterHrid", {
            configurable: true,
            value: monsterHrid
          });
        }
        runtime.state.pendingMergedTask = pendingMergedTask;
      };
      card[MERGE_HANDLER] = handler;
      card.dataset.mwitoolsMergeWired = "true";
      card.addEventListener("click", handler, true);
    });
  }
  function stickyResetSignature(card, task) {
    return [taskId(task), taskActionHrid(task), visibleTaskTitle(card)].join(
      ""
    );
  }
  function clearPendingStickyReset(slot) {
    const pending = pendingStickyResetSlots.get(slot);
    if (!pending) return;
    clearTimeout(pending.timeout);
    pendingStickyResetSlots.delete(slot);
  }
  function clearAllPendingStickyResets() {
    for (const slot of [...pendingStickyResetSlots.keys()]) {
      clearPendingStickyReset(slot);
    }
  }
  function finalizeStickyResetSlots(cards, tasks) {
    cards.forEach((card, index) => {
      const slot = Number(card.dataset.mwitoolsOriginalIndex ?? index);
      const pending = pendingStickyResetSlots.get(slot);
      if (!pending) return;
      const task = tasks[index];
      if (task === pending.task && stickyResetSignature(card, task) === pending.signature) {
        return;
      }
      stickyVisibleSlots.add(slot);
      clearPendingStickyReset(slot);
    });
  }
  function wireResetButtons(cards) {
    cards.forEach((card, index) => {
      if (card.dataset.mwitoolsResetWired) return;
      const isResetButton = (candidate) => matchesGameTranslations("randomTask.reroll", candidate.textContent, {
        fallbackPatterns: [/^(?:reset|重置)$/i]
      });
      const hasResetButton = [...card.querySelectorAll("button")].some(
        isResetButton
      );
      if (!hasResetButton) return;
      card.dataset.mwitoolsResetWired = "true";
      card.addEventListener(
        "click",
        (event) => {
          const button = event.target?.closest?.("button");
          if (!button || !card.contains(button) || !isResetButton(button)) {
            return;
          }
          nativeResetChoiceUntil = Date.now() + 1e4;
          const slot = Number(card.dataset.mwitoolsOriginalIndex ?? index);
          pendingResetSlots.add(slot);
          activeRerollContext = {
            slot,
            optionsSeen: false,
            confirmed: false
          };
          clearPendingStickyReset(slot);
          if (runtime.settings.get("taskStatistics") && hasActiveTaskFilters() && card.dataset.mwitoolsFiltered !== "true") {
            const task = liveTaskForCard(
              card,
              runtime.state.characterQuests ?? []
            );
            const pending = {
              task,
              signature: stickyResetSignature(card, task),
              timeout: null
            };
            pending.timeout = setTimeout(() => {
              if (pendingStickyResetSlots.get(slot) === pending) {
                pendingStickyResetSlots.delete(slot);
              }
            }, 3e4);
            pending.timeout?.unref?.();
            pendingStickyResetSlots.set(slot, pending);
          }
          const timeout = setTimeout(
            () => pendingResetSlots.delete(slot),
            3e4
          );
          timeout?.unref?.();
        },
        true
      );
    });
  }
  function applyPendingMerge() {
    const pending = runtime.state.pendingMergedTask;
    if (!pending) return;
    const input = [
      ...document.querySelectorAll(
        'div[class*="SkillActionDetail_maxActionCountInput"] input'
      )
    ].find((candidate) => {
      const panel = candidate.closest('div[class*="SkillActionDetail_regularComponent"]') ?? candidate.closest('div[class*="Modal_modalContainer"]')?.querySelector('div[class*="SkillActionDetail_regularComponent"]') ?? candidate.parentElement;
      const name = runtime.api.getOriTextFromElement?.(
        panel?.querySelector('div[class*="SkillActionDetail_name"]')
      );
      const actionHrid = resolveLocalizedEntity("action", name) || runtime.api.getActionHridFromItemName?.(name);
      return actionHrid === pending.actionHrid || actionContainsMonster(actionHrid, pending.monsterHrid);
    });
    if (!input) return;
    if (runtime.api.reactInputTriggerHack) {
      runtime.api.reactInputTriggerHack(input, pending.count);
    } else {
      input.value = String(pending.count);
      input.dispatchEvent(
        new (input.ownerDocument?.defaultView?.Event ?? Event)("input", {
          bubbles: true
        })
      );
    }
    document.querySelectorAll(".mwi-task-merged-note,.mwi-task-merge-toast").forEach((node) => node.remove());
    const toast = document.createElement("div");
    toast.className = "mwi-task-merge-toast";
    toast.setAttribute("role", "status");
    toast.textContent = t11(
      `已合并 ${pending.taskCount} 个同动作任务,共 ${runtime.api.formatExactNumber(pending.count)} 次。`,
      `Merged ${pending.taskCount} matching tasks for ${runtime.api.formatExactNumber(pending.count)} actions.`
    );
    document.body.append(toast);
    const timeout = setTimeout(() => toast.remove(), 3200);
    timeout?.unref?.();
    runtime.state.pendingMergedTask = null;
  }
  function shouldRenderTaskMutations(records, now = Date.now()) {
    const rerollOptionsChanged = records.some((record) => {
      const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
      const changedNodes = [
        ...record.addedNodes ?? [],
        ...record.removedNodes ?? []
      ].filter((node) => node?.nodeType === 1);
      return target?.closest?.(REROLL_OPTIONS_SELECTOR) || changedNodes.some(
        (node) => node.matches?.(REROLL_OPTIONS_SELECTOR) || node.querySelector?.(REROLL_OPTIONS_SELECTOR)
      );
    });
    if (rerollOptionsChanged) return true;
    if (now < nativeResetChoiceUntil) return false;
    const removedBackground = records.some((record) => {
      const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
      return target?.isConnected && target?.closest?.(TASK_SELECTOR) && [...record.removedNodes ?? []].some(
        (node) => node?.nodeType === 1 && node.matches?.(".mwi-task-bg")
      );
    });
    const addedBackground = records.some(
      (record) => [...record.addedNodes ?? []].some(
        (node) => node?.nodeType === 1 && node.matches?.(".mwi-task-bg")
      )
    );
    if (removedBackground && !addedBackground) return true;
    return records.some((record) => {
      const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
      if (target?.closest?.(OWNED_TASK_SELECTOR)) return false;
      const changedNodes = [
        ...record.addedNodes ?? [],
        ...record.removedNodes ?? []
      ].filter((node) => node?.nodeType === 1);
      if (changedNodes.length && changedNodes.every(
        (node) => node.matches?.(OWNED_TASK_SELECTOR) || node.closest?.(OWNED_TASK_SELECTOR)
      )) {
        return false;
      }
      if (target?.closest?.('[class*="TasksPanel_taskList"]')) return true;
      return changedNodes.some(
        (node) => node.matches?.('[class*="TasksPanel_taskList"]') || node.querySelector?.('[class*="TasksPanel_taskList"]')
      );
    });
  }
  function reactCooldownState(button) {
    const key = Object.getOwnPropertyNames(button ?? {}).find(
      (name) => name.startsWith("__reactFiber$") || name.startsWith("__reactInternalInstance$")
    );
    let fiber = key ? button[key] : null;
    for (let depth = 0; fiber && depth < 8; depth += 1) {
      const state = fiber.stateNode?.state;
      if (state && "isOnCooldown" in state) return state;
      fiber = fiber.return;
    }
    return null;
  }
  function hasNativeDisabledClass(button) {
    return [...button?.classList ?? []].some(
      (name) => name.startsWith("Button_disabled__")
    );
  }
  function rerollChoiceButtons(container) {
    const ranged = [...container.querySelectorAll(RANGED_REROLL_BUTTON_SELECTOR)];
    if (ranged.length) return ranged.length === 2 ? ranged : [];
    const native = [...container.querySelectorAll("button")];
    return native.length === 2 ? native : [];
  }
  function wireRerollChoiceButton(button) {
    if (button[REROLL_CHOICE_HANDLER]) return;
    const handler = () => {
      if (button.dataset.mwitoolsTaskLockDisabled === "true") return;
      if (activeRerollContext) activeRerollContext.confirmed = true;
    };
    button[REROLL_CHOICE_HANDLER] = handler;
    button.addEventListener("click", handler, true);
  }
  function lockRerollButton(button) {
    wireRerollChoiceButton(button);
    if (!rerollButtonSnapshots.has(button)) {
      rerollButtonSnapshots.set(button, {
        disabled: button.disabled,
        ariaDisabled: button.getAttribute("aria-disabled"),
        ariaLabel: button.getAttribute("aria-label")
      });
    }
    if (!button[REROLL_LOCK_HANDLER]) {
      const handler = (event) => {
        if (button.dataset.mwitoolsTaskLockDisabled !== "true") return;
        event.preventDefault();
        event.stopImmediatePropagation();
      };
      button[REROLL_LOCK_HANDLER] = handler;
      button.addEventListener("click", handler, true);
    }
    button.dataset.mwitoolsTaskLockDisabled = "true";
    button.disabled = true;
    button.setAttribute("aria-disabled", "true");
    const snapshot = rerollButtonSnapshots.get(button);
    const baseLabel = snapshot.ariaLabel || String(button.textContent ?? "").trim();
    button.setAttribute(
      "aria-label",
      `${baseLabel}${baseLabel ? " · " : ""}${t11("已锁定", "Locked")}`
    );
    let icon = button.querySelector(":scope > .mwi-task-reroll-lock");
    if (!icon) {
      icon = document.createElement("span");
      icon.className = "mwi-task-reroll-lock";
      icon.textContent = "🔒";
      icon.setAttribute("aria-hidden", "true");
      button.append(icon);
    }
  }
  function restoreRerollButton(button) {
    if (button.dataset.mwitoolsTaskLockDisabled !== "true") return;
    const snapshot = rerollButtonSnapshots.get(button);
    if (snapshot) {
      button.disabled = snapshot.disabled;
      if (snapshot.ariaDisabled === null) button.removeAttribute("aria-disabled");
      else button.setAttribute("aria-disabled", snapshot.ariaDisabled);
      if (snapshot.ariaLabel === null) button.removeAttribute("aria-label");
      else button.setAttribute("aria-label", snapshot.ariaLabel);
    }
    delete button.dataset.mwitoolsTaskLockDisabled;
    button.querySelector(":scope > .mwi-task-reroll-lock")?.remove();
    const handler = button[REROLL_LOCK_HANDLER];
    if (handler) button.removeEventListener("click", handler, true);
    delete button[REROLL_LOCK_HANDLER];
    rerollButtonSnapshots.delete(button);
  }
  function syncTaskRerollLocks(root = document) {
    ensureTaskFilterLockState();
    const containers = [
      ...root.querySelectorAll?.(REROLL_OPTIONS_SELECTOR) ?? []
    ];
    if (!containers.length) {
      for (const button of root.querySelectorAll?.(
        '[data-mwitools-task-lock-disabled="true"]'
      ) ?? []) {
        restoreRerollButton(button);
      }
      if (activeRerollContext?.optionsSeen) {
        if (!activeRerollContext.confirmed) {
          clearPendingStickyReset(activeRerollContext.slot);
          pendingResetSlots.delete(activeRerollContext.slot);
        } else {
          nativeResetChoiceUntil = 0;
        }
        activeRerollContext = null;
      }
      return 0;
    }
    if (activeRerollContext) activeRerollContext.optionsSeen = true;
    const classification = activeRerollContext ? pageClassifications.get(activeRerollContext.slot) : null;
    const locked = taskMatchesLockedFilters(classification);
    let changed = 0;
    for (const container of containers) {
      const buttons = rerollChoiceButtons(container);
      if (buttons.length !== 2) {
        if (activeRerollContext && container.querySelector("button") && !warnedUnexpectedRerollButtons) {
          warnedUnexpectedRerollButtons = true;
          console.warn(
            "[MWITools] Task reroll choices were not recognized; filter locks were left unchanged."
          );
        }
        continue;
      }
      for (const button of buttons) {
        wireRerollChoiceButton(button);
        if (locked) {
          lockRerollButton(button);
          changed += 1;
        } else {
          restoreRerollButton(button);
        }
      }
    }
    return changed;
  }
  function repairRangedWayIdleRerollButtons(root = document) {
    let repaired = 0;
    for (const container of root.querySelectorAll?.(REROLL_OPTIONS_SELECTOR) ?? []) {
      const buttons = [
        ...container.querySelectorAll(RANGED_REROLL_BUTTON_SELECTOR)
      ];
      if (buttons.length !== 2) continue;
      if (buttons.some(
        (button2) => button2.dataset.mwitoolsTaskLockDisabled === "true"
      )) {
        continue;
      }
      const expensive = buttons.filter(
        (button2) => button2.dataset.moreExpensive === "true"
      );
      const preferred = buttons.filter(
        (button2) => button2.dataset.moreExpensive === "false"
      );
      if (expensive.length !== 1 || preferred.length !== 1) continue;
      if (!expensive[0].disabled && !hasNativeDisabledClass(expensive[0])) {
        continue;
      }
      const button = preferred[0];
      const cooldownState = reactCooldownState(button);
      const disabledClass = hasNativeDisabledClass(button);
      if (!button.disabled && !disabledClass && cooldownState?.isOnCooldown !== true) {
        continue;
      }
      button.disabled = false;
      for (const name of [...button.classList]) {
        if (name.startsWith("Button_disabled__")) button.classList.remove(name);
      }
      if (cooldownState?.isOnCooldown === true) {
        cooldownState.isOnCooldown = false;
      }
      repaired += 1;
    }
    return repaired;
  }
  function taskRenderSignature(snapshots) {
    const settings2 = [
      runtime.config.isZH,
      runtime.settings.get("taskAutoSort"),
      runtime.settings.get("taskIcons"),
      runtime.settings.get("taskStatistics"),
      [...pageNewTaskIds].sort().join(","),
      [...activeProfessionFilters].sort().join(","),
      combatFilterEnabled,
      [...activeDungeonFilters].sort().join(","),
      [...lockedTaskFilters].sort().join(","),
      [...stickyVisibleSlots].sort((left, right) => left - right).join(",")
    ];
    const rows2 = snapshots.map((snapshot) => {
      return [
        snapshot.actionHrid,
        snapshot.title,
        snapshot.progress,
        snapshot.completed ? "1" : "0",
        snapshot.taskId
      ].join("");
    });
    return [...settings2, ...rows2].join("");
  }
  function renderTasks({ forceSort = false, allowReusedPositional = true } = {}) {
    ensureTaskFilterLockState();
    if (!runtime.settings.get("taskStatistics")) clearTaskFilterLocks();
    syncTaskRerollLocks();
    repairRangedWayIdleRerollButtons();
    if (document.querySelector(REROLL_OPTIONS_SELECTOR) && !activeRerollContext?.confirmed) {
      return true;
    }
    let cards = [...document.querySelectorAll(TASK_SELECTOR)];
    if (!cards.length) {
      applyPendingMerge();
      document.querySelectorAll(".mwi-task-toolbar").forEach((node) => node.remove());
      if (taskListParent && !taskListParent.isConnected) {
        originalCards = [];
        taskListParent = null;
        lastRenderedCards = [];
        lastTaskRenderSignature = "";
        lastActionDetails = null;
        lastActionCategories = null;
        if (!hasTemporaryTaskReturn() && !pendingResetSlots.size) {
          pageClassifications = /* @__PURE__ */ new Map();
          pageTaskIds = /* @__PURE__ */ new Map();
          pageNewTaskIds = /* @__PURE__ */ new Set();
          pendingResetSlots = /* @__PURE__ */ new Set();
          stickyVisibleSlots = /* @__PURE__ */ new Set();
          clearAllPendingStickyResets();
          pageOrderBySlot = /* @__PURE__ */ new Map();
          runtime.state.mwitoolsPageNewTaskIds = /* @__PURE__ */ new Set();
        }
      }
      return true;
    }
    const observedParent = cards[0]?.parentElement ?? null;
    const enteredNewTaskPage = !taskListParent?.isConnected || observedParent && observedParent !== taskListParent;
    const resumedTaskPage = enteredNewTaskPage && consumeTemporaryTaskReturn();
    const resumedResetPage = enteredNewTaskPage && pendingResetSlots.size > 0;
    const sortOnEntry = enteredNewTaskPage && !resumedResetPage;
    if (enteredNewTaskPage) {
      cleanupListDecorations({ restoreOrder: false });
      document.querySelectorAll(".mwi-task-toolbar").forEach((node) => node.remove());
      originalCards = [];
      if (!resumedTaskPage && !resumedResetPage) {
        pageClassifications = /* @__PURE__ */ new Map();
        pageTaskIds = /* @__PURE__ */ new Map();
        pageNewTaskIds = /* @__PURE__ */ new Set();
        pendingResetSlots = /* @__PURE__ */ new Set();
      }
      if (!resumedResetPage) {
        stickyVisibleSlots = /* @__PURE__ */ new Set();
        clearAllPendingStickyResets();
        activeRerollContext = null;
        pageOrderBySlot = /* @__PURE__ */ new Map();
        resetTaskFilters();
      }
      taskListParent = observedParent;
    }
    cards = cards.filter(
      (card) => card.parentElement === taskListParent && isQuestTaskCard(card)
    );
    if (!cards.length) {
      document.querySelectorAll(".mwi-task-toolbar").forEach((node) => node.remove());
      lastRenderedCards = [];
      lastTaskRenderSignature = "";
      applyPendingMerge();
      return true;
    }
    const tasks = runtime.state.characterQuests ?? [];
    const cardEntries = resolveTaskCards(cards, tasks, {
      taskActionHrid,
      taskRemaining,
      allowReusedPositional
    });
    if (cardEntries.some((entry) => !entry.resolved)) return false;
    const cardTasks = cardEntries.map(({ task }) => task);
    assignStablePageSlots(cards, cardTasks);
    finalizeStickyResetSlots(cards, cardTasks);
    const newTaskSetChanged = syncPageNewTasks(
      cards,
      cardTasks,
      enteredNewTaskPage && !resumedTaskPage && !resumedResetPage
    );
    const snapshots = cards.map(
      (card, index) => taskCardSnapshot(card, cardTasks[index])
    );
    const signature = taskRenderSignature(snapshots);
    const sameCards = cards.length === lastRenderedCards.length && cards.every((card, index) => card === lastRenderedCards[index]);
    const actionDetails = runtime.state.initData_actionDetailMap;
    const actionCategories = runtime.state.initData_actionCategoryDetailMap;
    if (!enteredNewTaskPage && !forceSort && sameCards && actionDetails === lastActionDetails && actionCategories === lastActionCategories && signature === lastTaskRenderSignature && cardEntries.every(({ card }) => taskIconMatches(card))) {
      applyPendingMerge();
      return true;
    }
    originalCards = [...cards];
    originalCards.forEach((card, index) => {
      if (!("mwitoolsOriginalOrder" in card.dataset))
        card.dataset.mwitoolsOriginalOrder = card.style.order;
      const taskIndex = String(cardEntries[index]?.taskIndex ?? -1);
      if (card.dataset.mwitoolsTaskIndex !== taskIndex) {
        card.dataset.mwitoolsTaskIndex = taskIndex;
      }
      if ("mwitoolsLocation" in card.dataset) {
        delete card.dataset.mwitoolsLocation;
      }
    });
    const rows2 = orderedRows(cards, cardTasks, snapshots);
    rows2.forEach((row) => decorateCard(row.card, row.task, row.artworks));
    wireMergeButtons(cards);
    wireResetButtons(cards);
    renderFlatTaskList(rows2, {
      sort: forceSort || sortOnEntry || newTaskSetChanged
    });
    applyPendingMerge();
    lastRenderedCards = [...cards];
    lastActionDetails = actionDetails;
    lastActionCategories = actionCategories;
    lastTaskRenderSignature = signature;
    return true;
  }
  function sortTasks() {
    lastTaskRenderSignature = "";
    renderTasks({ forceSort: true });
  }
  function cleanupTasks() {
    for (const button of document.querySelectorAll(
      '[data-mwitools-task-lock-disabled="true"]'
    )) {
      restoreRerollButton(button);
    }
    cleanupListDecorations();
    document.querySelectorAll(
      ".mwi-task-insight,.mwi-task-toolbar,.mwi-task-profession-group,.mwi-task-bg,.mwi-task-merged-note,.mwi-task-merge-toast"
    ).forEach((node) => node.remove());
    document.querySelectorAll("[data-mwitools-merge-wired]").forEach((node) => {
      const handler = node[MERGE_HANDLER];
      if (handler) node.removeEventListener("click", handler, true);
      delete node[MERGE_HANDLER];
      delete node.dataset.mwitoolsMergeWired;
    });
    document.querySelectorAll("[data-mwitools-reset-wired]").forEach((node) => {
      delete node.dataset.mwitoolsResetWired;
    });
    document.getElementById(STYLE_ID11)?.remove();
    originalCards = [];
    taskListParent = null;
    pageClassifications = /* @__PURE__ */ new Map();
    pageTaskIds = /* @__PURE__ */ new Map();
    pageNewTaskIds = /* @__PURE__ */ new Set();
    pendingResetSlots = /* @__PURE__ */ new Set();
    stickyVisibleSlots = /* @__PURE__ */ new Set();
    clearAllPendingStickyResets();
    activeRerollContext = null;
    nativeResetChoiceUntil = 0;
    temporaryTaskReturn = null;
    runtime.state.mwitoolsPageNewTaskIds = /* @__PURE__ */ new Set();
    lastRenderedCards = [];
    lastTaskRenderSignature = "";
    lastActionDetails = null;
    lastActionCategories = null;
    pageOrderBySlot = /* @__PURE__ */ new Map();
    lockedTaskFilters = /* @__PURE__ */ new Set();
    taskFilterLockStorageKey = "";
    warnedUnexpectedRerollButtons = false;
    resetTaskFilters();
  }
  runtime.features.register({
    id: "taskInsights",
    setting: "taskInsights",
    scope: "character",
    initialize({ scope, characterId }) {
      ensureTaskFilterLockState(characterId);
      addStyles9();
      let settleRetries = 0;
      let renderScheduler = null;
      const render = () => {
        const settled = renderTasks({
          allowReusedPositional: false
        });
        if (!settled && settleRetries < 3) {
          settleRetries += 1;
          renderScheduler.schedule();
        } else {
          settleRetries = 0;
        }
      };
      renderScheduler = createFrameScheduler(render);
      const scheduleRender = () => renderScheduler.schedule();
      const spriteManifest = loadGameSpriteManifest();
      render();
      void spriteManifest.then(() => {
        lastTaskRenderSignature = "";
        scheduleRender();
      });
      subscribeMutationChannel(
        {
          name: "task-surface",
          target: document.body,
          options: TASK_MUTATION_OBSERVER_OPTIONS,
          scope
        },
        (records) => {
          syncTaskRerollLocks();
          repairRangedWayIdleRerollButtons();
          if (shouldRenderTaskMutations(records)) scheduleRender();
        }
      );
      scope.add(
        runtime.onMessage("quests_updated", () => {
          nativeResetChoiceUntil = 0;
          scheduleRender();
        })
      );
      scope.add(() => {
        renderScheduler.cancel();
        cleanupTasks();
      });
    }
  });
  for (const id of [
    "taskMaterials",
    "taskQueueProgress",
    "taskAutoSort",
    "taskIcons",
    "taskStatistics",
    "taskClaimCollector",
    "taskMergeActions"
  ]) {
    runtime.features.register({
      id,
      setting: id,
      scope: "character",
      dependsOn: ["taskInsights"],
      initialize() {
        renderTasks();
        return renderTasks;
      }
    });
  }
  Object.assign(runtime.api, {
    addTaskStyles: addStyles9,
    armTemporaryTaskReturn,
    cancelTemporaryTaskReturn,
    resumeTemporaryTaskReturn,
    taskActionHrid,
    taskRemaining,
    taskProjection,
    renderTasks,
    sortTasks,
    restoreTaskOrder: sortTasks
  });

  // src/features/task-train-planner.js
  var STYLE_ID12 = "mwitools-task-train-planner-style";
  var CONTROL_CLASS2 = "mwi-task-train-planner";
  var TASK_SELECTOR2 = 'div[class*="RandomTask_randomTask"]:not([data-mwitools-task-mirror="true"])';
  var OWNED_TASK_SELECTOR2 = '.mwi-task-train-planner,.mwi-task-insight,.mwi-task-toolbar,.mwi-task-profession-group,.mwi-task-combat-location,.mwi-task-combat-mode,.mwi-task-bg,.mwi-task-merged-note,.mwi-task-merge-toast,.mwi-task-new-badge,[data-mwitools-task-mirror="true"]';
  function t12(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function addStyles10() {
    if (document.getElementById(STYLE_ID12)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID12;
    style.textContent = `
    .${CONTROL_CLASS2}{flex:0 1 auto;min-width:0;max-width:100%;box-sizing:border-box;margin-right:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    button.${CONTROL_CLASS2}{height:28px;padding:0 8px;border:1px solid rgba(144,166,235,.55);border-radius:4px;background:#282844;color:#e8e8ef;font:600 11px/1 Roboto,Arial,sans-serif;cursor:pointer}
    button.${CONTROL_CLASS2}:hover{filter:brightness(1.16)}
    span.${CONTROL_CLASS2}{padding:0 8px;color:#8f96ad;font:italic 11px/28px Roboto,Arial,sans-serif;user-select:none}
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function primaryOutput2(actionHrid) {
    const detail = runtime.state.initData_actionDetailMap?.[actionHrid];
    return runtime.api.getExpectedOutputs?.(detail)?.[0]?.itemHrid ?? "";
  }
  function taskActionHrid2(task) {
    return runtime.api.taskActionHrid?.(task) ?? null;
  }
  function taskRemaining2(task) {
    return runtime.api.taskRemaining?.(task) ?? 0;
  }
  function collectTaskTrainGroups(quests = []) {
    const groups = /* @__PURE__ */ new Map();
    const entries = quests.map((task, index) => {
      const actionHrid = taskActionHrid2(task);
      const remaining = taskRemaining2(task);
      const outputHrid = actionHrid ? primaryOutput2(actionHrid) : "";
      const depth = outputHrid ? runtime.api.trainPlanning.trainChainDepth(outputHrid) : -1;
      const root = depth >= 0 ? runtime.api.trainPlanning.trainChainRoot(outputHrid) : "";
      const entry = {
        index,
        task,
        actionHrid,
        outputHrid,
        remaining,
        depth,
        root,
        state: remaining <= 0 ? "done" : depth < 0 ? "isolated" : "planned"
      };
      if (entry.state === "planned") {
        if (!groups.has(root)) groups.set(root, []);
        groups.get(root).push(entry);
      }
      return entry;
    });
    for (const group of groups.values()) {
      group.sort(
        (left, right) => right.depth - left.depth || left.index - right.index
      );
      const chain = runtime.api.trainPlanning.buildTrainChain(
        group[0].outputHrid
      );
      if (!chain.steps.length || chain.cycle || chain.truncated) {
        for (const entry of group) entry.state = "isolated";
        groups.delete(group[0].root);
      } else {
        group[0].state = "top";
      }
    }
    return { entries, groups };
  }
  function createTaskTrainPlan(root, quests = runtime.state.characterQuests ?? []) {
    const { groups } = collectTaskTrainGroups(quests);
    const group = groups.get(root);
    if (!group?.length) return null;
    const taskCounts = {};
    for (const entry of group) {
      taskCounts[entry.outputHrid] = (taskCounts[entry.outputHrid] ?? 0) + entry.remaining;
    }
    return runtime.api.trainPlanning.createTrainPlan(
      group[0].outputHrid,
      taskCounts
    );
  }
  function findTaskNavigation(card) {
    return [...card.querySelectorAll("button")].find(
      (button) => matchesGameTranslations(
        ["randomTask.go", "questModal.go"],
        button.textContent,
        { fallbackPatterns: [/^(?:前往|go)$/i] }
      )
    );
  }
  function insertBeforeTaskNavigation(card, control) {
    const navigation = findTaskNavigation(card);
    if (navigation?.parentElement) {
      navigation.parentElement.insertBefore(control, navigation);
      return true;
    }
    return false;
  }
  function plannerButton(entry, signature) {
    const button = document.createElement("button");
    button.type = "button";
    button.className = CONTROL_CLASS2;
    button.dataset.signature = signature;
    button.textContent = t12("🚂 规划火车", "🚂 Plan train");
    button.title = t12(
      "合并同一升级链全部未完成任务并按最新库存重新规划",
      "Combine all unfinished tasks in this upgrade chain and recalculate from current inventory"
    );
    button.addEventListener("click", (event) => {
      event.preventDefault();
      event.stopPropagation();
      const plan = createTaskTrainPlan(entry.root);
      if (!plan?.steps?.length) return;
      runtime.api.semiAutoTrain?.start(plan);
    });
    return button;
  }
  function plannerLabel(text, title, signature) {
    const label = document.createElement("span");
    label.className = CONTROL_CLASS2;
    label.dataset.signature = signature;
    label.textContent = text;
    label.title = title;
    return label;
  }
  function renderTaskTrainPlanner(cards = [...document.querySelectorAll(TASK_SELECTOR2)], quests = runtime.state.characterQuests ?? []) {
    if (!cards.length) return true;
    const { entries } = collectTaskTrainGroups(quests);
    const resolvedCards = resolveTaskCards(cards, quests, {
      taskActionHrid: taskActionHrid2,
      taskRemaining: taskRemaining2
    });
    let settled = true;
    for (const { card, resolved, taskIndex } of resolvedCards) {
      if (!resolved) {
        settled = false;
        continue;
      }
      const entry = entries[taskIndex];
      const signature = entry ? [entry.state, entry.root, entry.remaining, runtime.config.isZH].join(
        ":"
      ) : "none";
      const existingControls = [...card.querySelectorAll(`.${CONTROL_CLASS2}`)];
      if (!entry || entry.state === "done" || entry.state === "isolated") {
        existingControls.forEach((node) => node.remove());
        continue;
      }
      const navigation = findTaskNavigation(card);
      if (!navigation?.parentElement) {
        settled = false;
        continue;
      }
      if (existingControls.length === 1 && existingControls[0].dataset.signature === signature && existingControls[0].parentElement === navigation.parentElement && existingControls[0].nextElementSibling === navigation) {
        continue;
      }
      existingControls.forEach((node) => node.remove());
      if (entry.state === "top") {
        insertBeforeTaskNavigation(card, plannerButton(entry, signature));
      } else if (entry.state === "planned") {
        insertBeforeTaskNavigation(
          card,
          plannerLabel(
            t12("已被规划", "Included in plan"),
            t12(
              "已由同一升级链的最高级任务统一规划",
              "Included by the highest-level task in this upgrade chain"
            ),
            signature
          )
        );
      }
    }
    return settled;
  }
  function cleanup3() {
    document.querySelectorAll(`.${CONTROL_CLASS2}`).forEach((node) => node.remove());
    document.getElementById(STYLE_ID12)?.remove();
  }
  function shouldRenderTaskTrainMutations(records) {
    const removedControl = records.some((record) => {
      const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
      return target?.isConnected && target?.closest?.(TASK_SELECTOR2) && [...record.removedNodes ?? []].some(
        (node) => node?.nodeType === 1 && node.matches?.(`.${CONTROL_CLASS2}`)
      );
    });
    const addedControl = records.some(
      (record) => [...record.addedNodes ?? []].some(
        (node) => node?.nodeType === 1 && node.matches?.(`.${CONTROL_CLASS2}`)
      )
    );
    if (removedControl && !addedControl) return true;
    return records.some((record) => {
      const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
      if (target?.closest?.(OWNED_TASK_SELECTOR2)) return false;
      const changedNodes = [
        ...record.addedNodes ?? [],
        ...record.removedNodes ?? []
      ].filter((node) => node?.nodeType === 1);
      if (changedNodes.length && changedNodes.every(
        (node) => node.matches?.(OWNED_TASK_SELECTOR2) || node.closest?.(OWNED_TASK_SELECTOR2)
      )) {
        return false;
      }
      if (target?.closest?.(TASK_SELECTOR2)) return true;
      return changedNodes.some(
        (node) => node.matches?.(TASK_SELECTOR2) || node.querySelector?.(TASK_SELECTOR2)
      );
    });
  }
  runtime.features.register({
    id: "taskTrainPlanner",
    setting: "taskTrainPlanner",
    scope: "character",
    dependsOn: ["semiAutoTrain"],
    initialize({ scope }) {
      addStyles10();
      let settleRetries = 0;
      let renderScheduler = null;
      const render = () => {
        const cards = [...document.querySelectorAll(TASK_SELECTOR2)];
        const quests = runtime.state.characterQuests ?? [];
        const settled = renderTaskTrainPlanner(cards, quests);
        if (!settled && settleRetries < 3) {
          settleRetries += 1;
          renderScheduler.schedule();
        } else {
          settleRetries = 0;
        }
      };
      renderScheduler = createFrameScheduler(render);
      const schedule = () => renderScheduler.schedule();
      render();
      subscribeMutationChannel(
        {
          name: "task-surface",
          target: document.body,
          options: { childList: true, subtree: true },
          scope
        },
        (records) => {
          if (shouldRenderTaskTrainMutations(records)) schedule();
        }
      );
      scope.add(runtime.onMessage("quests_updated", schedule));
      scope.add(() => {
        renderScheduler.cancel();
        cleanup3();
      });
    }
  });
  Object.assign(runtime.api, {
    collectTaskTrainGroups,
    createTaskTrainPlan
  });

  // src/features/task-new-badge.js
  var STYLE_ID13 = "mwitools-task-new-style";
  var TASK_SELECTOR3 = 'div[class*="RandomTask_randomTask"]:not([data-mwitools-task-mirror="true"])';
  var OWNED_TASK_SELECTOR3 = '.mwi-task-new-badge,.mwi-task-insight,.mwi-task-toolbar,.mwi-task-profession-group,.mwi-task-combat-location,.mwi-task-combat-mode,.mwi-task-bg,.mwi-task-merged-note,.mwi-task-merge-toast,.mwi-task-train-planner,[data-mwitools-task-mirror="true"]';
  var liveTaskNewStates = /* @__PURE__ */ new Map();
  function questId(quest) {
    return taskCardTaskId(quest);
  }
  function isRemoved(quest) {
    return Boolean(
      quest?.isClaimed || quest?.claimed || quest?.isDeleted || quest?.deleted || String(quest?.status ?? "").toLowerCase().includes("claimed")
    );
  }
  function isCompleted(quest) {
    if (isRemoved(quest)) return true;
    const target = Number(
      quest?.targetCount ?? quest?.requiredCount ?? quest?.goalCount
    );
    const current = Number(
      quest?.currentCount ?? quest?.completedCount ?? quest?.progressCount
    );
    return Number.isFinite(target) && target > 0 && Number.isFinite(current) ? current >= target : Boolean(quest?.isCompleted || quest?.completed);
  }
  function taskNewStorageKey(characterId, server = globalThis.location?.hostname ?? "unknown") {
    return `MWITools_task_new_v1:${server}:${String(characterId ?? "")}`;
  }
  function readTaskNewState(storageKey) {
    try {
      const value = JSON.parse(localStorage.getItem(storageKey) || "null");
      return {
        known: new Set(Array.isArray(value?.known) ? value.known : []),
        fresh: new Set(Array.isArray(value?.fresh) ? value.fresh : []),
        initialized: value?.initialized === true || Array.isArray(value?.known) && value.known.length > 0
      };
    } catch {
      return { known: /* @__PURE__ */ new Set(), fresh: /* @__PURE__ */ new Set(), initialized: false };
    }
  }
  function writeTaskNewState(storageKey, state) {
    localStorage.setItem(
      storageKey,
      JSON.stringify({
        initialized: state.initialized === true,
        known: [...state.known],
        fresh: [...state.fresh]
      })
    );
  }
  function initializeQuestState(state, quests) {
    const firstBaseline = state.initialized !== true;
    const currentIds = new Set((quests ?? []).map(questId).filter(Boolean));
    for (const id of currentIds) {
      if (!firstBaseline && !state.known.has(id)) state.fresh.add(id);
      state.known.add(id);
    }
    for (const id of [...state.fresh]) {
      if (!currentIds.has(id)) state.fresh.delete(id);
    }
    state.initialized = true;
    return state;
  }
  function applyQuestUpdates(state, updates) {
    for (const update of updates ?? []) {
      const id = questId(update);
      if (!id) continue;
      if (isRemoved(update) || isCompleted(update)) {
        state.fresh.delete(id);
        state.known.delete(id);
        continue;
      }
      if (!state.known.has(id)) state.fresh.add(id);
      state.known.add(id);
    }
    return state;
  }
  function syncQuestSnapshot(state, previousIds, quests) {
    const before = previousIds instanceof Set ? previousIds : new Set(previousIds);
    const current = new Set((quests ?? []).map(questId).filter(Boolean));
    for (const id of current) {
      if (!before.has(id)) state.fresh.add(id);
      state.known.add(id);
    }
    for (const id of [...state.fresh]) {
      if (!current.has(id)) state.fresh.delete(id);
    }
    state.initialized = true;
    return current;
  }
  function addStyles11() {
    if (document.getElementById(STYLE_ID13)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID13;
    style.textContent = `
    ${TASK_SELECTOR3}.mwi-task-is-new{position:relative}
    .mwi-task-new-badge{position:absolute;z-index:5;right:5px;top:5px;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:auto!important;min-width:18px!important;max-width:max-content!important;height:18px!important;min-height:18px!important;margin:0!important;padding:0 4px!important;flex:0 0 auto!important;border:1px solid rgba(255,220,128,.72);border-radius:4px;background:#f0aa2e;color:#221704;font-size:9px;font-weight:800;line-height:16px;letter-spacing:0;white-space:nowrap;box-shadow:0 2px 6px rgba(0,0,0,.32);pointer-events:none}
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function cleanupDom() {
    document.querySelectorAll(".mwi-task-new-badge").forEach((node) => node.remove());
    document.querySelectorAll(".mwi-task-is-new").forEach((node) => {
      node.classList.remove("mwi-task-is-new");
      delete node.dataset.mwitoolsTaskNewWired;
    });
    document.getElementById(STYLE_ID13)?.remove();
  }
  function shouldRenderTaskNewMutations(records) {
    return records.some((record) => {
      const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
      if (target?.closest?.(OWNED_TASK_SELECTOR3)) return false;
      const changedNodes = [
        ...record.addedNodes ?? [],
        ...record.removedNodes ?? []
      ].filter((node) => node?.nodeType === 1);
      if (changedNodes.length && changedNodes.every(
        (node) => node.matches?.(OWNED_TASK_SELECTOR3) || node.closest?.(OWNED_TASK_SELECTOR3)
      )) {
        return false;
      }
      if (target?.closest?.(TASK_SELECTOR3)) return true;
      return changedNodes.some(
        (node) => node.matches?.(TASK_SELECTOR3) || node.querySelector?.(TASK_SELECTOR3)
      );
    });
  }
  runtime.features.register({
    id: "taskNewBadge",
    setting: "taskNewBadge",
    scope: "character",
    dependsOn: ["taskInsights"],
    initialize({ scope, characterId }) {
      addStyles11();
      const storageKey = taskNewStorageKey(characterId);
      const state = readTaskNewState(storageKey);
      liveTaskNewStates.set(storageKey, state);
      const initial = runtime.state.characterQuests ?? [];
      initializeQuestState(state, initial);
      writeTaskNewState(storageKey, state);
      let authoritativeIds = new Set(initial.map(questId).filter(Boolean));
      const render = () => {
        const quests = runtime.state.characterQuests ?? [];
        const activeIds = new Set(quests.map(questId).filter(Boolean));
        let changed = false;
        for (const id of [...state.fresh]) {
          if (!activeIds.has(id)) {
            state.fresh.delete(id);
            state.known.delete(id);
            changed = true;
          }
        }
        if (changed) writeTaskNewState(storageKey, state);
        const cards = [...document.querySelectorAll(TASK_SELECTOR3)];
        const resolvedCards = resolveTaskCards(cards, quests, {
          taskActionHrid: (task) => runtime.api.taskActionHrid?.(task),
          taskRemaining: (task) => runtime.api.taskRemaining?.(task) ?? 0
        });
        resolvedCards.forEach(({ card, resolved, task }) => {
          if (!resolved) return;
          const id = questId(task);
          const fresh = Boolean(
            id && runtime.state.mwitoolsPageNewTaskIds?.has?.(id)
          );
          card.classList.toggle("mwi-task-is-new", Boolean(fresh));
          let badge = card.querySelector(":scope > .mwi-task-new-badge");
          if (fresh && !badge) {
            badge = document.createElement("span");
            badge.className = "mwi-task-new-badge";
            badge.textContent = runtime.config.isZH ? "新" : "NEW";
            card.appendChild(badge);
          } else if (!fresh) {
            badge?.remove();
          }
        });
      };
      const renderScheduler = createFrameScheduler(render);
      const schedule = () => renderScheduler.schedule();
      scope.add(
        runtime.onMessage("quests_updated", () => {
          authoritativeIds = syncQuestSnapshot(
            state,
            authoritativeIds,
            runtime.state.characterQuests ?? []
          );
          writeTaskNewState(storageKey, state);
          schedule();
        })
      );
      subscribeMutationChannel(
        {
          name: "task-surface",
          target: document.body,
          options: { childList: true, subtree: true },
          scope
        },
        (records) => {
          if (shouldRenderTaskNewMutations(records)) schedule();
        }
      );
      render();
      scope.add(() => {
        renderScheduler.cancel();
        if (liveTaskNewStates.get(storageKey) === state) {
          liveTaskNewStates.delete(storageKey);
        }
        cleanupDom();
      });
    }
  });
  Object.assign(runtime.api, {
    getNewTaskIds() {
      const key = taskNewStorageKey(runtime.state.currentCharacterId);
      const state = liveTaskNewStates.get(key) ?? readTaskNewState(key);
      initializeQuestState(state, runtime.state.characterQuests ?? []);
      writeTaskNewState(key, state);
      return [...state.fresh];
    },
    acknowledgeNewTaskIds(ids) {
      const key = taskNewStorageKey(runtime.state.currentCharacterId);
      const state = liveTaskNewStates.get(key) ?? readTaskNewState(key);
      for (const id of ids ?? []) state.fresh.delete(String(id));
      writeTaskNewState(key, state);
    }
  });

  // src/features/task-auto-return.js
  var TASK_SELECTOR4 = '[class*="RandomTask_randomTask"]:not([data-mwitools-task-mirror="true"])';
  var TASK_LIST_SELECTOR = '[class*="TasksPanel_taskList"]';
  var ACTION_DETAIL_SELECTOR = '[class*="SkillActionDetail_regularComponent"],[class*="SkillActionDetail_skillActionDetail"],[class*="ActionDetail_actionDetail"],[class*="SkillActionDetail_modalContent"],[class*="ActionDetail_modalContent"]';
  var RETURN_TTL_MS = 3e4;
  function taskIdentity(task) {
    return taskCardTaskId(task);
  }
  function scrollContainerFor(element, { fallbackToDocument = true } = {}) {
    for (let current = element; current; current = current.parentElement) {
      const style = globalThis.getComputedStyle?.(current);
      if (current.scrollHeight > current.clientHeight && /auto|scroll/.test(`${style?.overflowY ?? ""} ${style?.overflow ?? ""}`)) {
        return current;
      }
    }
    return fallbackToDocument ? document.scrollingElement ?? document.documentElement : null;
  }
  function clampScrollTop(scroller, value) {
    const maximum = Math.max(
      0,
      Number(scroller?.scrollHeight || 0) - Number(scroller?.clientHeight || 0)
    );
    return Math.min(maximum, Math.max(0, Number(value) || 0));
  }
  function centerCardInScroller(card, scroller) {
    const cardRect = card.getBoundingClientRect?.();
    const scrollerRect = scroller.getBoundingClientRect?.();
    if (!cardRect || !scrollerRect) return;
    const target = Number(scroller.scrollTop || 0) + cardRect.top - scrollerRect.top - (Number(scroller.clientHeight || 0) - cardRect.height) / 2;
    scroller.scrollTop = clampScrollTop(scroller, target);
  }
  function taskLayoutSignature(card, list, scroller) {
    const cardRect = card?.getBoundingClientRect?.();
    return [
      card?.dataset.mwitoolsTaskId ?? "",
      list?.children.length ?? 0,
      Math.round(cardRect?.top ?? 0),
      Math.round(cardRect?.height ?? 0),
      Math.round(scroller?.scrollTop ?? 0),
      Math.round(scroller?.scrollHeight ?? 0)
    ].join(":");
  }
  function captureTaskReturnContext(card, quests, now = Date.now()) {
    if (!card) return null;
    const cards = [...document.querySelectorAll(TASK_SELECTOR4)];
    const resolved = resolveTaskCards(cards, quests, {
      taskActionHrid: (task2) => runtime.api.taskActionHrid?.(task2),
      taskRemaining: (task2) => runtime.api.taskRemaining?.(task2) ?? 0
    }).find((entry) => entry.card === card);
    const originalIndex = Number(card.dataset.mwitoolsOriginalIndex);
    const taskIndex = Number.isInteger(resolved?.taskIndex) && resolved.taskIndex >= 0 ? resolved.taskIndex : Number.isInteger(originalIndex) ? originalIndex : cards.indexOf(card);
    const originalSlot = Number.isInteger(originalIndex) ? originalIndex : cards.indexOf(card);
    const task = resolved?.task ?? quests?.[taskIndex];
    const scroller = scrollContainerFor(card);
    return {
      taskId: taskIdentity(task),
      originalIndex: originalSlot,
      profession: card.dataset.mwitoolsProfession ?? card.closest("[data-profession]")?.dataset.profession ?? "",
      scrollTop: Number(scroller?.scrollTop) || 0,
      expiresAt: now + RETURN_TTL_MS,
      sawAction: false
    };
  }
  function buttonText(button) {
    return String(
      runtime.api.getOriTextFromElement?.(button) ?? button?.textContent ?? ""
    ).replaceAll(/\s+/g, " ").trim();
  }
  function isGoButton(button) {
    return matchesGameTranslations(
      ["randomTask.go", "questModal.go"],
      buttonText(button),
      { fallbackPatterns: [/^(?:前往|go)$/i] }
    );
  }
  function isCommitButton(button) {
    return matchesGameTranslations(
      [
        "skillActionDetail.buttons.start",
        "skillActionDetail.buttons.startNow",
        "skillActionDetail.buttons.addToQueue"
      ],
      buttonText(button),
      {
        fallbackPatterns: [
          /^(?:添加任务|添加到队列|加入队列|立即开始|开始任务|开始动作|添加|开始|队列|add task|add to (?:action )?queue|start task|start action|start now|start immediately|add|start|queue)(?:\s*#\d+)?$/i
        ]
      }
    );
  }
  function findGameHost() {
    const roots = [];
    const push = (value) => {
      const root = value?.current ?? value;
      if (root && typeof root === "object" && !roots.includes(root))
        roots.push(root);
    };
    const rootElement = document.getElementById("root");
    push(rootElement?._reactRootContainer?.current);
    push(rootElement?._reactRootContainer?._internalRoot?.current);
    for (const element of [rootElement, document.body]) {
      for (const key of Object.getOwnPropertyNames(element ?? {})) {
        if (key.startsWith("__reactContainer") || key.startsWith("__reactFiber") || key.startsWith("__reactInternalInstance")) {
          push(element[key]);
        }
      }
    }
    const seen = /* @__PURE__ */ new Set();
    while (roots.length && seen.size < 5e4) {
      const fiber = roots.pop();
      if (!fiber || seen.has(fiber)) continue;
      seen.add(fiber);
      const host = fiber.stateNode;
      if (typeof host?.handleChangeNavTarget === "function" && typeof host?.setState === "function") {
        return host;
      }
      if (fiber.child) roots.push(fiber.child);
      if (fiber.sibling) roots.push(fiber.sibling);
    }
    return null;
  }
  function openTasksPage() {
    const host = findGameHost();
    if (host) {
      host.handleChangeNavTarget("tasks");
      return true;
    }
    const buttons = document.querySelectorAll(
      'nav button,[class*="Nav"] button,[class*="nav"] button'
    );
    const taskButton = [...buttons].find(
      (button) => matchesGameTranslations("navigationBar.tasks", buttonText(button), {
        fallbackPatterns: [/^(?:任务|tasks)$/i]
      })
    );
    taskButton?.click();
    return Boolean(taskButton);
  }
  function findTaskCard(context) {
    const cards = [...document.querySelectorAll(TASK_SELECTOR4)];
    if (context.taskId) {
      const matched = cards.find((card) => card.dataset.mwitoolsTaskId === context.taskId) ?? resolveTaskCards(cards, runtime.state.characterQuests ?? [], {
        taskActionHrid: (task) => runtime.api.taskActionHrid?.(task),
        taskRemaining: (task) => runtime.api.taskRemaining?.(task) ?? 0
      }).find(({ task }) => taskIdentity(task) === context.taskId)?.card;
      if (matched) return matched;
      return null;
    }
    return cards[context.originalIndex] ?? null;
  }
  function restoreTaskPosition(context) {
    const card = findTaskCard(context);
    if (card) {
      const list2 = card.closest(TASK_LIST_SELECTOR);
      const scroller2 = scrollContainerFor(card, { fallbackToDocument: false });
      if (scroller2) centerCardInScroller(card, scroller2);
      return {
        restored: true,
        signature: taskLayoutSignature(card, list2, scroller2)
      };
    }
    const list = document.querySelector(TASK_LIST_SELECTOR);
    const scroller = scrollContainerFor(list, { fallbackToDocument: false });
    if (list && scroller) {
      scroller.scrollTop = clampScrollTop(scroller, context.scrollTop);
      return {
        restored: true,
        signature: taskLayoutSignature(null, list, scroller)
      };
    }
    return { restored: false, signature: "" };
  }
  runtime.features.register({
    id: "taskAutoReturn",
    setting: "taskAutoReturn",
    scope: "character",
    initialize({ scope }) {
      let pending = null;
      let expiryTimer = null;
      let returnTimer = null;
      let restoreTimer = null;
      const clearTimers = () => {
        if (expiryTimer !== null) clearTimeout(expiryTimer);
        if (returnTimer !== null) clearTimeout(returnTimer);
        if (restoreTimer !== null) clearTimeout(restoreTimer);
        expiryTimer = returnTimer = restoreTimer = null;
      };
      const clearPending = ({ cancelTaskReturn = true } = {}) => {
        clearTimers();
        pending = null;
        if (cancelTaskReturn) runtime.api.cancelTemporaryTaskReturn?.();
      };
      const armExpiry = () => {
        if (expiryTimer !== null) clearTimeout(expiryTimer);
        expiryTimer = setTimeout(clearPending, RETURN_TTL_MS);
      };
      const returnToOrigin = () => {
        if (!pending || pending.expiresAt <= Date.now()) {
          clearPending();
          return;
        }
        const context = pending;
        clearPending({ cancelTaskReturn: false });
        runtime.api.resumeTemporaryTaskReturn?.();
        if (!openTasksPage()) {
          runtime.api.cancelTemporaryTaskReturn?.();
          return;
        }
        let attempts = 0;
        let previousSignature = "";
        let stableSamples = 0;
        const restore = () => {
          restoreTimer = null;
          const result = restoreTaskPosition(context);
          if (result.restored) {
            stableSamples = result.signature === previousSignature ? stableSamples + 1 : 1;
            previousSignature = result.signature;
            if (stableSamples >= 2) return;
          } else {
            stableSamples = 0;
            previousSignature = "";
          }
          if (attempts >= 40) return;
          attempts += 1;
          restoreTimer = setTimeout(restore, 50);
        };
        restoreTimer = setTimeout(restore, 0);
      };
      const scheduleReturn = (delay = 0) => {
        if (!pending) return;
        if (returnTimer !== null) clearTimeout(returnTimer);
        returnTimer = setTimeout(() => {
          returnTimer = null;
          returnToOrigin();
        }, delay);
      };
      const observeAction = () => {
        if (!pending) return;
        const action = document.querySelector(ACTION_DETAIL_SELECTOR);
        if (action) pending.sawAction = true;
        else if (pending.sawAction) scheduleReturn();
      };
      scope.event(
        document,
        "click",
        (event) => {
          const button = event.target?.closest?.("button");
          if (!button) return;
          const card = button.closest(TASK_SELECTOR4);
          if (card && isGoButton(button)) {
            pending = captureTaskReturnContext(
              card,
              runtime.state.characterQuests ?? []
            );
            if (pending) {
              runtime.api.armTemporaryTaskReturn?.(pending.expiresAt);
              armExpiry();
            }
            return;
          }
          if (pending && button.closest(ACTION_DETAIL_SELECTOR) && isCommitButton(button)) {
            scheduleReturn(500);
          }
        },
        true
      );
      subscribeMutationChannel(
        {
          name: "task-surface",
          target: document.body,
          options: { childList: true, subtree: true },
          scope
        },
        observeAction
      );
      scope.add(clearPending);
    }
  });

  // src/features/ability-book-calculator.js
  var STYLE_ID14 = "mwitools-ability-book-calculator-style";
  var PANEL_CLASS = "mwi-ability-book-calculator";
  var DICTIONARY_SELECTOR = '[class*="ItemDictionary_modalContent"]';
  function t13(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function finite(value) {
    if (value === null || value === void 0 || value === "") return null;
    const number3 = Number(value);
    return Number.isFinite(number3) ? number3 : null;
  }
  function maxAbilityLevel(levelExperienceTable) {
    if (!Array.isArray(levelExperienceTable)) return 0;
    for (let index = levelExperienceTable.length - 1; index >= 1; index -= 1) {
      if (finite(levelExperienceTable[index]) !== null) return index;
    }
    return 0;
  }
  function calculateAbilityBookRequirement({
    isLearned,
    currentLevel,
    currentExperience,
    targetLevel,
    experienceGain,
    levelExperienceTable
  }) {
    const maximumLevel = maxAbilityLevel(levelExperienceTable);
    const target = Number(targetLevel);
    const level = finite(currentLevel);
    const experience = finite(currentExperience);
    const perBook = finite(experienceGain);
    if (!Number.isInteger(target) || target < 1 || target > maximumLevel || level === null || experience === null || perBook === null || perBook <= 0) {
      return { status: "invalid", maximumLevel };
    }
    const targetExperience = finite(levelExperienceTable[target]);
    if (targetExperience === null) return { status: "invalid", maximumLevel };
    if (isLearned && (level >= target || experience >= targetExperience)) {
      return {
        status: "reached",
        maximumLevel,
        targetExperience,
        unlockBooks: 0,
        levelingBooks: 0,
        totalBooks: 0
      };
    }
    const unlockBooks = isLearned ? 0 : 1;
    const levelingBooks = Math.ceil(
      Math.max(0, targetExperience - experience) / perBook
    );
    return {
      status: "ready",
      maximumLevel,
      targetExperience,
      unlockBooks,
      levelingBooks,
      totalBooks: unlockBooks + levelingBooks
    };
  }
  function normalizeItemHrid3(value) {
    const raw = String(value ?? "").trim();
    if (!raw) return "";
    return raw.startsWith("/items/") ? raw : `/items/${raw.split("/").at(-1)}`;
  }
  function itemHridFromIcon2(root) {
    const preferred = root?.querySelectorAll?.(
      '[class*="MarketplacePanel_currentItem"] svg use,[class*="MarketplacePanel_itemContainer"] svg use,[class*="ItemDictionary_item"] svg use'
    );
    const icons = preferred?.length ? preferred : root?.querySelectorAll?.("svg use") ?? [];
    for (const use of icons) {
      const href = use.getAttribute("href") ?? use.getAttribute("xlink:href") ?? "";
      const fragment = href.split("#").at(-1);
      const itemHrid = normalizeItemHrid3(fragment);
      if (runtime.state.initData_itemDetailMap?.[itemHrid]) return itemHrid;
    }
    return "";
  }
  function itemHridFromTitle(root) {
    const title = root?.querySelector?.('[class*="ItemDictionary_title"],h1,h2');
    const name = String(
      runtime.api.getOriTextFromElement?.(title) ?? title?.textContent ?? ""
    ).trim();
    if (!name) return "";
    const itemHrid = resolveLocalizedEntity("item", name);
    if (itemHrid) return itemHrid;
    const abilityHrid = resolveLocalizedEntity("ability", name);
    return abilityHrid ? abilityHrid.replace("/abilities/", "/items/") : normalizeItemHrid3(name);
  }
  function resolveAbilityBookItem(root) {
    const itemHrid = resolveEntityFromElement("item", root) || itemHridFromIcon2(root) || itemHridFromTitle(root);
    return runtime.state.initData_itemDetailMap?.[itemHrid]?.abilityBookDetail ? itemHrid : "";
  }
  function abilityRecord(abilityHrid) {
    const source = runtime.state.initData_characterAbilities;
    if (source === null || source === void 0) return { ready: false };
    const records = Array.isArray(source) ? source : Object.values(source);
    const record = records.find(
      (candidate) => (candidate?.abilityHrid ?? candidate?.hrid) === abilityHrid
    );
    if (!record) {
      return { ready: true, isLearned: false, level: 0, experience: 0 };
    }
    const experience = finite(
      record.experience ?? record.totalExperience ?? record.experiencePoints
    );
    if (experience === null) return { ready: false };
    let level = finite(record.level);
    if (level === null) {
      level = 0;
      const table = runtime.state.initData_levelExperienceTable ?? [];
      for (let index = 1; index < table.length; index += 1) {
        if (finite(table[index]) !== null && experience >= Number(table[index])) {
          level = index;
        }
      }
    }
    return { ready: true, isLearned: true, level, experience };
  }
  function calculatorData(itemHrid) {
    const item = runtime.state.initData_itemDetailMap?.[itemHrid];
    const detail = item?.abilityBookDetail;
    const table = runtime.state.initData_levelExperienceTable;
    if (!detail || !Array.isArray(table)) return { ready: false };
    const abilityHrid = detail.abilityHrid ?? itemHrid.replace("/items/", "/abilities/");
    const characterAbility = abilityRecord(abilityHrid);
    if (!characterAbility.ready) return { ready: false };
    return {
      ready: true,
      itemHrid,
      experienceGain: finite(detail.experienceGain),
      maximumLevel: maxAbilityLevel(table),
      levelExperienceTable: table,
      ...characterAbility
    };
  }
  function addStyles12() {
    if (document.getElementById(STYLE_ID14)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID14;
    style.textContent = `
    .${PANEL_CLASS}{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px 10px;margin:8px 0;padding:8px 10px;border:1px solid rgba(255,255,255,.13);border-radius:6px;background:rgba(0,0,0,.16);color:var(--color-text-primary,#eee);font-size:.72rem;line-height:1.35}
    .${PANEL_CLASS} .mwi-book-title{grid-column:1/-1;font-size:.78rem;font-weight:700;color:var(--color-primary,#e0bc42)}
    .${PANEL_CLASS} .mwi-book-target{display:flex;align-items:center;gap:6px}
    .${PANEL_CLASS} input{width:68px;box-sizing:border-box;border:1px solid rgba(255,255,255,.2);border-radius:4px;background:rgba(0,0,0,.25);color:inherit;padding:3px 5px}
    .${PANEL_CLASS} .mwi-book-result,.${PANEL_CLASS} .mwi-book-cost,.${PANEL_CLASS} .mwi-book-state{grid-column:1/-1}
    .${PANEL_CLASS} .mwi-book-result{font-weight:650;color:#9fd7ab}
    .${PANEL_CLASS}[data-status="invalid"] .mwi-book-result{color:#ff9c8f}
    .${PANEL_CLASS} .mwi-book-muted{color:var(--color-text-secondary,#aaa)}
    .${PANEL_CLASS} .mwi-book-cart{grid-column:1/-1;justify-self:start;min-height:25px;padding:3px 9px;border:1px solid rgba(255,255,255,.16);border-radius:4px;background:var(--color-midnight-500,#343a54);color:var(--color-neutral-100,#eee);font:inherit;font-weight:650;cursor:pointer}
    .${PANEL_CLASS} .mwi-book-cart:hover:not(:disabled){background:var(--color-space-700,#46547e)}
    .${PANEL_CLASS} .mwi-book-cart:disabled{opacity:.55;cursor:default}
    @media(max-width:700px){.${PANEL_CLASS}{grid-template-columns:1fr;gap:4px}.${PANEL_CLASS}>*{grid-column:1!important}}
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function createPanel(onTargetChange, onAddToCart) {
    const panel = document.createElement("section");
    panel.className = PANEL_CLASS;
    panel.dataset.surface = "dictionary";
    const title = document.createElement("div");
    title.className = "mwi-book-title";
    const state = document.createElement("div");
    state.className = "mwi-book-state";
    const perBook = document.createElement("div");
    perBook.className = "mwi-book-muted mwi-book-per-book";
    const target = document.createElement("label");
    target.className = "mwi-book-target";
    const targetText = document.createElement("span");
    const input = document.createElement("input");
    input.type = "number";
    input.step = "1";
    input.addEventListener("input", () => onTargetChange(panel, input.value));
    target.append(targetText, input);
    const result = document.createElement("div");
    result.className = "mwi-book-result";
    result.setAttribute("aria-live", "polite");
    const cost = document.createElement("div");
    cost.className = "mwi-book-cost mwi-book-muted";
    const cart2 = document.createElement("button");
    cart2.type = "button";
    cart2.className = "mwi-book-cart";
    cart2.addEventListener("click", () => onAddToCart(panel));
    panel.append(title, state, perBook, target, result, cost, cart2);
    return panel;
  }
  function setPanelText(panel, selector, value) {
    const element = panel.querySelector(selector);
    if (element && element.textContent !== value) element.textContent = value;
  }
  function updateCartButton(panel, itemHrid, requirement) {
    const button = panel.querySelector(".mwi-book-cart");
    const procurement4 = runtime.api.procurement;
    const cartEnabled = Boolean(
      procurement4 && runtime.settings.settingsMap.procurementAssistant?.isTrue === true
    );
    button.hidden = !cartEnabled;
    button.disabled = true;
    button.dataset.quantity = "0";
    if (!cartEnabled) return;
    if (!requirement || requirement.status === "invalid") {
      button.textContent = t13("加入购物车", "Add to cart");
      return;
    }
    const totalBooks = Math.max(0, Math.ceil(requirement.totalBooks || 0));
    const owned = Math.max(
      0,
      Number(procurement4.getInventoryCount?.(itemHrid, 0)) || 0
    );
    const listed = Math.max(
      0,
      Number(procurement4.getCartItem?.(itemHrid, 0)?.quantity) || 0
    );
    const shortage = Math.max(0, Math.ceil(totalBooks - owned - listed));
    if (shortage <= 0) {
      button.textContent = t13("已备齐", "Covered");
      return;
    }
    button.dataset.quantity = String(shortage);
    button.disabled = false;
    button.textContent = t13(
      `加入购物车(${shortage})`,
      `Add to cart (${shortage})`
    );
  }
  function updatePanel(panel, itemHrid, targetValues) {
    panel.dataset.itemHrid = itemHrid;
    setPanelText(
      panel,
      ".mwi-book-title",
      t13("技能书计算器", "Ability book calculator")
    );
    const data = calculatorData(itemHrid);
    const input = panel.querySelector("input");
    const targetLabel = panel.querySelector(".mwi-book-target span");
    targetLabel.textContent = t13("目标等级", "Target level");
    input.setAttribute("aria-label", targetLabel.textContent);
    if (!data.ready || !data.experienceGain || !data.maximumLevel) {
      panel.dataset.status = "waiting";
      input.disabled = true;
      setPanelText(
        panel,
        ".mwi-book-state",
        t13("等待角色与技能书数据", "Waiting for character and ability-book data")
      );
      setPanelText(panel, ".mwi-book-per-book", "");
      setPanelText(panel, ".mwi-book-result", "—");
      setPanelText(panel, ".mwi-book-cost", "");
      updateCartButton(panel, itemHrid, null);
      return;
    }
    input.disabled = false;
    input.min = "1";
    input.max = String(data.maximumLevel);
    const defaultTarget = Math.min(
      data.maximumLevel,
      Math.max(1, Number(data.level) + 1)
    );
    let target = Number(targetValues.get(itemHrid) ?? defaultTarget);
    if (!Number.isInteger(target)) target = defaultTarget;
    if (String(input.value) !== String(target)) input.value = String(target);
    targetValues.set(itemHrid, target);
    const exact = runtime.api.formatExactNumber ?? ((value) => String(value));
    setPanelText(
      panel,
      ".mwi-book-state",
      data.isLearned ? t13(
        `当前 Lv.${data.level} · 总经验 ${exact(data.experience, 0)}`,
        `Current Lv.${data.level} · total XP ${exact(data.experience, 0)}`
      ) : t13("当前:未学习", "Current: not learned")
    );
    setPanelText(
      panel,
      ".mwi-book-per-book",
      t13(
        `每本增加 ${exact(data.experienceGain, 0)} 经验`,
        `${exact(data.experienceGain, 0)} XP per book`
      )
    );
    const requirement = calculateAbilityBookRequirement({
      isLearned: data.isLearned,
      currentLevel: data.level,
      currentExperience: data.experience,
      targetLevel: target,
      experienceGain: data.experienceGain,
      levelExperienceTable: data.levelExperienceTable
    });
    panel.dataset.status = requirement.status;
    let resultText;
    if (requirement.status === "invalid") {
      resultText = t13(
        `目标等级必须为 1–${data.maximumLevel} 的整数`,
        `Target level must be an integer from 1 to ${data.maximumLevel}`
      );
    } else if (requirement.status === "reached") {
      resultText = t13("已达到目标 · 还需 0 本", "Target reached · 0 books needed");
    } else if (requirement.unlockBooks) {
      resultText = t13(
        `解锁 1 + 升级 ${requirement.levelingBooks} = 合计 ${requirement.totalBooks} 本`,
        `Unlock 1 + level ${requirement.levelingBooks} = ${requirement.totalBooks} books total`
      );
    } else {
      resultText = t13(
        `升级还需 ${requirement.totalBooks} 本`,
        `${requirement.totalBooks} books needed to level`
      );
    }
    setPanelText(panel, ".mwi-book-result", resultText);
    const books = requirement.totalBooks;
    const ask = runtime.api.getAskPrice?.(itemHrid, 0) ?? 0;
    let costText = "";
    if (requirement.status !== "invalid") {
      costText = books === 0 ? t13("参考购买成本:0", "Reference purchase cost: 0") : ask > 0 ? t13(
        `参考购买成本:${runtime.api.numberFormatter(books * ask)}(最低出售价 ${runtime.api.numberFormatter(ask)}/本)`,
        `Reference purchase cost: ${runtime.api.numberFormatter(books * ask)} (best ask ${runtime.api.numberFormatter(ask)}/book)`
      ) : t13(
        "参考购买成本:暂无出售价",
        "Reference purchase cost: no ask price"
      );
    }
    setPanelText(panel, ".mwi-book-cost", costText);
    updateCartButton(panel, itemHrid, requirement);
  }
  function visiblePanels(selector) {
    const panels = [...document.querySelectorAll(selector)];
    const visible2 = panels.filter((panel) => panel.getClientRects().length);
    return visible2.length ? visible2 : panels;
  }
  runtime.features.register({
    id: "skillbook",
    setting: "skillbook",
    scope: "character",
    initialize({ scope }) {
      addStyles12();
      const targetValues = /* @__PURE__ */ new Map();
      let refreshTimer2 = null;
      const updateSurface = (container, itemHrid) => {
        let panel = container.querySelector(
          `.${PANEL_CLASS}[data-surface="dictionary"]`
        );
        if (!panel) {
          panel = createPanel(
            (changedPanel, value) => {
              targetValues.set(changedPanel.dataset.itemHrid, Number(value));
              updatePanel(
                changedPanel,
                changedPanel.dataset.itemHrid,
                targetValues
              );
            },
            (changedPanel) => {
              const itemHrid2 = changedPanel.dataset.itemHrid;
              const quantity = Number(
                changedPanel.querySelector(".mwi-book-cart")?.dataset.quantity
              );
              if (!itemHrid2 || !Number.isSafeInteger(quantity) || quantity <= 0)
                return;
              runtime.api.procurement?.addToCart?.({
                itemHrid: itemHrid2,
                enhancementLevel: 0,
                name: runtime.state.initData_itemDetailMap?.[itemHrid2]?.name,
                quantity,
                source: "ability-book"
              });
              updatePanel(changedPanel, itemHrid2, targetValues);
            }
          );
          container.appendChild(panel);
        }
        updatePanel(panel, itemHrid, targetValues);
      };
      const refresh = () => {
        for (const panel of visiblePanels(DICTIONARY_SELECTOR)) {
          const itemHrid = resolveAbilityBookItem(panel);
          const existing = panel.querySelector(
            `.${PANEL_CLASS}[data-surface="dictionary"]`
          );
          if (itemHrid) updateSurface(panel, itemHrid);
          else existing?.remove();
        }
      };
      const schedule = () => {
        if (refreshTimer2 !== null) return;
        refreshTimer2 = setTimeout(() => {
          refreshTimer2 = null;
          refresh();
        }, 50);
      };
      const observer = new MutationObserver(schedule);
      scope.observer(observer, document.body, { childList: true, subtree: true });
      for (const type of [
        "init_client_data",
        "init_character_data",
        "abilities_updated",
        "character_abilities_updated",
        "action_completed",
        "market_item_values_updated",
        "market_item_order_books_updated"
      ]) {
        scope.add(runtime.onMessage(type, schedule));
      }
      for (const type of ["cart:change", "inventory:change"]) {
        const unsubscribe = runtime.api.procurement?.on?.(type, schedule);
        if (typeof unsubscribe === "function") scope.add(unsubscribe);
      }
      scope.add(() => {
        if (refreshTimer2 !== null) clearTimeout(refreshTimer2);
        document.querySelectorAll(`.${PANEL_CLASS}`).forEach((panel) => panel.remove());
        document.getElementById(STYLE_ID14)?.remove();
      });
      refresh();
    }
  });

  // src/features/inventory-market-double-click.js
  var INVENTORY_SELECTOR = 'div[class*="Inventory_items"]';
  var ITEM_SELECTOR2 = 'div[class*="Item_itemContainer"]';
  function inventoryItemTarget(target) {
    const item = target?.closest?.(ITEM_SELECTOR2);
    if (!item?.closest(INVENTORY_SELECTOR)) return null;
    const category = item.closest(
      'div:has(> button[class*="Inventory_categoryButton"],> div[class*="Inventory_label"])'
    );
    const categoryButton = category?.querySelector(
      'button[class*="Inventory_categoryButton"]'
    );
    const categoryName = String(
      runtime.api.getOriTextFromElement?.(categoryButton) ?? categoryButton?.textContent ?? ""
    ).trim();
    const itemHrid = resolveEntityFromElement("item", item);
    const itemDetail = runtime.state.initData_itemDetailMap?.[itemHrid];
    if (!itemHrid || itemDetail?.isTradable !== true) return null;
    const levelText = item.querySelector('[class*="Item_enhancementLevel"]')?.textContent ?? "";
    const enhancementLevel = Number.parseInt(levelText.replace(/\D/g, ""), 10) || 0;
    return { itemHrid, enhancementLevel, categoryName };
  }
  runtime.features.register({
    id: "inventoryMarketDoubleClick",
    setting: "inventoryMarketDoubleClick",
    scope: "character",
    initialize({ scope }) {
      scope.event(
        document,
        "dblclick",
        (event) => {
          if (event.button && event.button !== 0) return;
          const target = inventoryItemTarget(event.target);
          if (!target) return;
          const open = runtime.api.openProcurementMarketplace;
          if (typeof open !== "function") return;
          event.preventDefault();
          event.stopPropagation();
          open(target.itemHrid, target.enhancementLevel);
        },
        true
      );
    }
  });

  // src/features/opinion-center/announcements.js
  var STORAGE_KEY = "MWITools_opinion_center_seen_announcements_v1";
  var ANNOUNCEMENTS = Object.freeze([
    Object.freeze({
      id: "26.4.14",
      version: "26.4.14",
      publishedAt: "2026-08-16",
      title: Object.freeze({
        zh: "26.4.14 更新公告",
        en: "Version 26.4.14 update"
      }),
      body: Object.freeze({
        zh: Object.freeze([
          "战斗与生活着装评分现在会分别计入当前公会全部战斗神龛和生活神龛的累计升级价值;查看他人资料时会使用对方公开的神龛等级,缺少公会数据时显示横线且不影响其余评分。",
          "地牢宝箱与精炼宝箱的库存估值和开箱期望现在都会同时扣除宝箱开启钥匙与对应地牢门票钥匙;开箱面板会分别展示两项成本,普通无门票宝箱不受影响。",
          "修复强化披风与其他背部装备时未比较贤者之镜方案的问题;包括精炼 +14 在内的强化成本现在会保留保护之镜作为普通保护材料,同时完整比较贤者之镜合成方案并选择总成本更低的路线。",
          "完整队列的耗时与完成时间不再依赖市场悬浮价格;队列重建、无限动作和缺少投影时会自动重试或显示明确原因。普通战斗任务会按目标怪物合并,任务“新”标记只跟随真正新增的任务。",
          "盈亏摘要新增流动资产与非流动资产盈亏,库存总资产会在有昨日记录时显示今日盈亏;公会贡献表会把统计列统一排在试炼层数等原生列之后,横向滚动和闲置人数判断也已修复。",
          "制造面板在打开配装下拉、切换配装或延迟重建后会恢复制造链、计划、缺料、生产摘要和快捷输入等模块;即使游戏只移除其中一张卡片也会自动补回。设置中新增自定义快捷小时与次数,并修复逗号小数及不同千位分隔格式造成的耗时误读。",
          "购物清单较长时,修改数量、删除或打开商品会保持当前滚动位置;商品弹窗打开后可直接切换其他购物项,也可从购物清单或商品导航删除当前项并自动前往下一项。",
          "中国服同时兼容有无 www 的访问地址,市场接口统一使用无 www 端点;关闭 MWITools 任务功能时不再提示 TaskManager 冲突,也可按脚本永久静默并在设置中恢复提醒。",
          "左侧利润网入口现在会注明包含强化模拟,并移除重复的插件设置入口;任务图片会在多次刷新时保持稳定,也修复了与 Ranged Way Idle 同时使用时便宜刷新选项可能错误卡死的问题。",
          "任务筛选按钮现在可用鼠标或触屏长按 1 秒分别锁定;命中锁定类型的任务会让两种刷新选项变灰并显示锁图标。筛选中刷新出的卡片即使改变类型也会保留到重新进入任务页;确认刷新后即使支付选项仍停留在卡片上,任务图片也会立即跟随新任务更新,不再需要点击返回,游戏只更新标题文字节点的情况也能正确同步。关闭任务统计筛选栏会清除已有锁定。锁定只显示小锁,不会改变筛选高亮,再次长按解锁后小锁会立即消失。长按进度会在按住 0.5 秒后才开始显示,满 1 秒才切换锁定;普通短按会立即切换并高亮筛选,刷新确认层打开时锁图标和筛选高亮也会即时同步。",
          "26.4.14 现已标记为重要更新;旧版本玩家会收到顶部更新提示,以获得任务筛选长按锁定、刷新卡片保留和任务图片即时同步等改进。"
        ]),
        en: Object.freeze([
          "Combat and Skilling Gear Scores now include the cumulative upgrade value of every matching guild shrine. Shared profiles use that player's public shrine levels, while missing guild data shows a dash without affecting the remaining score.",
          "Inventory valuations and opening estimates for dungeon and refinement chests now deduct both the chest key and the matching dungeon entry key. The opening panel shows the two costs separately, while ordinary chests without entry keys are unchanged.",
          "Fixed Philosopher's Mirror plans being skipped for enhanced capes and other back equipment. Enhancement costs, including refined +14 items, now keep Mirrors of Protection for regular protection while comparing the full Philosopher's Mirror synthesis route and selecting the lower total cost.",
          "Full-queue duration and completion estimates no longer depend on hover prices. Queue rebuilds now retry, while infinite actions and missing projections show an explicit reason. Regular combat tasks merge by target monster, and New badges follow only genuinely added task IDs.",
          "The P/L summary now includes liquid and non-current asset profit, and Inventory shows today's P/L beside total assets when a prior-day record exists. Guild contribution statistics now stay after native columns such as Trial Level, while horizontal scrolling and idle-member detection have also been fixed.",
          "Manufacturing chains, plans, shortages, production summaries, targets, and quick inputs now recover after opening the loadout picker, switching loadouts, or delayed panel rebuilds, even when the game removes only one extension card. Settings add custom quick-hour and quick-count presets, and duration parsing now supports comma decimals and mixed thousands separators.",
          "Long shopping lists now keep their scroll position when quantities change, items are removed, or products are opened. While a product modal is open, another shopping item can be opened directly, and the current item can be removed from either the cart or product navigation before advancing automatically.",
          "China servers now support both www and bare hostnames while using the bare-host market endpoint. TaskManager warnings are silent when MWITools task features are off, and individual conflicts can be muted permanently and restored from Settings.",
          "The sidebar profit-site shortcut now notes that it includes an enhancement simulator, and the redundant script-settings shortcut has been removed. Task artwork stays stable through repeated rerolls, and cheaper reroll options no longer become incorrectly stuck when Ranged Way Idle is also enabled.",
          "Task filter buttons can now be locked individually with a one-second mouse or touch hold. Tasks matching a locked type gray out both reroll choices and show lock icons. Cards rerolled while filtering remain visible even if their type changes until the task page is re-entered. Once a reroll is confirmed, its artwork follows the new task immediately even while the payment choices remain on the card, without requiring Back navigation; title-text-only game updates are synchronized as well. Disabling task statistics filters clears existing locks. Locking only shows the small padlock without changing filter highlighting, and another one-second hold unlocks it and removes the padlock immediately. Hold progress now appears only after a 0.5-second grace period and toggles the lock at one second; ordinary taps immediately toggle and highlight filters, while lock icons and filter highlighting stay synchronized even when the reroll confirmation is open.",
          "Version 26.4.14 is now marked as an important update. Players on older releases will see the top update prompt for long-press task-filter locks, rerolled-card retention, immediate task-artwork synchronization, and the other improvements in this release."
        ])
      })
    }),
    Object.freeze({
      id: "26.4.13",
      version: "26.4.13",
      publishedAt: "2026-08-15",
      title: Object.freeze({
        zh: "26.4.13 更新公告",
        en: "Version 26.4.13 update"
      }),
      body: Object.freeze({
        zh: Object.freeze([
          "修复制作界面查看升级耗时时,目标等级在自动填写生产次数并触发界面刷新后回到当前等级 +1 的问题;例如从 130 级查看升到 135 级时,刷新后会继续保留 135 级及对应估算。"
        ]),
        en: Object.freeze([
          "Fixed the target level in production upgrade-time estimates resetting to current level +1 after autofilling the action count refreshed the panel. For example, estimating level 130 to 135 now keeps level 135 and its estimate after the refresh."
        ])
      })
    }),
    Object.freeze({
      id: "26.4.12",
      version: "26.4.12",
      publishedAt: "2026-08-15",
      title: Object.freeze({
        zh: "26.4.12 更新公告",
        en: "Version 26.4.12 update"
      }),
      body: Object.freeze({
        zh: Object.freeze([
          "修复金币未计入库存价值的问题;金币现在固定按 1:1 计入库存、货币分类和总资产,不再受市场行情快照是否包含金币影响。以月神之蝶等星球 BOSS 怪物为目标的任务从任务页前往时,也会按官方 BOSS 刷新数据自动换算战斗次数,例如剩余 4 只且每 10 场出现一只时会填写 40 次;普通怪物与地牢任务保持原数量。",
          "修复眼球怪、灵魂猎手等同时出现在多个地牢的战斗任务只显示首个地牢的问题;怪物任务现在会完整显示官方刷怪数据中的全部匹配地牢,明确以地牢为目标的任务仍只显示自身地牢。",
          "战斗人物卡现在会预留一行固定 Buff 区域,Buff 增减不再因换行让卡片跳动;每张卡可独立展开为两行,更多 Buff 会在框内滚动,展开状态会跨战斗与刷新保留。",
          "右上角快捷设置现在会记住上次浏览到的滚动位置,关闭后重新打开或刷新页面都可从原处继续查看。",
          "修复购物车有商品时从“设置”切回“清单”会让清单跑到设置内容下方的问题;页签切换现在会正确移除上一页内容,同时保留清单内部更新时的稳定显示。",
          "修复与 Ranged Way Idle 等持续观察市场界面的脚本同时使用时,打开市场后购物车和采购导航图标反复闪烁、无法点击的问题;未变化的物品图标与导航按钮现在会保持原节点,不再被外部界面刷新反复替换。",
          "生产详情右上角新增手动“更新仓库”按钮,会直接读取游戏当前仓库,并按最新库存重算生产余缺与购物车中的项目采购缺口;手工购物数量保持不变,常备阈值、项目占用和已计算规划也会收到库存更新,避免材料已消耗后仍显示旧余量。按钮固定在生产弹窗边框内,不会下推页面或改变弹窗原有的悬浮定位;游戏弹窗没有自带定位基准时也会正确显示,持续重绘时会保留该定位基准,不再与页面右上角来回切换闪烁。按钮仅在生产与强化详情中显示,战斗怪物面板不会再误显示。",
          "修复自托管更新文件已经上传并成功请求 CDN 缓存刷新后,发布任务仍因无权查询刷新进度而误报失败的问题;发布流程会继续校验产物并请求缓存刷新,但不再依赖额外的刷新状态读取权限。"
        ]),
        en: Object.freeze([
          "Fixed coins being omitted from inventory value. Coins now always count 1:1 toward inventory, the currency category, and total assets regardless of whether the market snapshot contains a coin record. Going to a task targeting a planet boss monster such as Luna Butterfly now also converts the remaining bosses through the official boss spawn data—for example, four remaining bosses that spawn every ten battles fill in 40 actions—while regular monsters and dungeon task counts stay unchanged.",
          "Fixed combat tasks for Eye, Soul Hunter, and other monsters found in multiple dungeons showing only the first dungeon. Monster tasks now show every matching dungeon in the official spawn data, while tasks explicitly targeting a dungeon still show only that dungeon.",
          "Combat unit cards now reserve one fixed row for buffs, so adding or removing buffs no longer makes cards jump when icons wrap. Each card can expand independently to two rows, additional buffs scroll inside the box, and expansion choices persist across battles and reloads.",
          "The top-right quick settings now remember the last scroll position, so reopening the panel or refreshing the page resumes where you left off.",
          "Fixed shopping-list rows appearing below the Settings content when returning to the Cart tab with existing items. Switching tabs now removes the previous view correctly while keeping in-tab cart updates stable.",
          "Fixed shopping-cart and procurement navigation icons flickering and becoming unclickable after opening the marketplace alongside scripts such as Ranged Way Idle. Unchanged item icons and navigation buttons now stay mounted instead of being repeatedly replaced by external UI refreshes.",
          "Added a manual Refresh inventory button in the top-right of production details. It reads the game's current inventory and recalculates production shortages plus project-sourced cart quantities; manual cart quantities stay intact, while restock thresholds, project reservations, and calculated Planning are notified of the new inventory so consumed materials no longer leave stale spare counts. The button stays inside the production dialog border without pushing the page down or changing the dialog's floating position. When the dialog has no positioning context, that context now remains stable across continuous redraws so the button no longer jumps back and forth to the page's top-right corner. It appears only in production and enhancing details, never in combat monster panels.",
          "Fixed self-hosted publishing being reported as failed after update files were uploaded and CDN invalidation was successfully requested, solely because the workflow could not query invalidation progress. Distribution artifacts remain validated and cache invalidation is still requested without requiring the additional status-read permission."
        ])
      })
    }),
    Object.freeze({
      id: "26.4.11",
      version: "26.4.11",
      publishedAt: "2026-08-14",
      title: Object.freeze({
        zh: "26.4.11 重要更新公告",
        en: "Important version 26.4.11 update"
      }),
      body: Object.freeze({
        zh: Object.freeze([
          "优化库存首次打开和切换性能:强化装备会复用相同的概率方案,制作、精炼与商店来源改为按目标物品查找,并减少汇总和排序控件的首屏样式计算;总资产、分类价值和排序仍会同步完整显示。",
          "修复手机端开启“规划”后角色页明显卡顿、规划界面可能迟迟无法加载的问题;规划编辑器现在只在首次点开时构建,响应式布局切换不会反复创建隐藏界面,生产目标也改为按产物索引查找并可在角色数据加载期间正常识别。",
          "新增游戏原生小紫牛风格的性能初始化引导,新装与升级后可先选择生活、战斗或平衡用途,再使用流畅优先、标准、完整功能或分组自定义档位;手机与触屏设备默认推荐流畅优先,桌面默认推荐标准。引导会一次性设置 DPS、Buff 倒计时、任务与资产、公会增强、装饰动画、DPS 趋势图和 1/2 秒刷新节奏,利润等复杂悬浮计算仍统一使用按键或长按触发;左上角总进度会在自定义流程中显示当前步数,应用配置后自动刷新页面。可在通用设置顶部查看当前配置并随时重新开始,取消不会覆盖已有选择。首次启动会先保留消息与数据核心,确认配置后才启动可选功能,避免升级时先全量初始化造成卡顿。",
          "检测到铁牛或旧铁牛角色时,现在会自动开启铁牛模式适配,隐藏该模式不可用的市场价格、交易利润与市场采购操作;切回普通角色时这些功能仍会正常显示。",
          "库存排序旁恢复“刷新价值”按钮,可在需要时主动生成新资产快照,同时保留当前排序与摘要展开状态;购物清单新增鼠标和触屏拖动排序并按角色、服务器保存,清单行与“清空未收藏”按钮也会在库存和价格更新时保持稳定,不再反复重建闪烁。",
          "强化行动的剩余时间改为原位更新,并会在原生强化数量文字短暂缺失时沿用同一行动最后一次有效数量,不再闪成无限或消失。本次生产总耗时已移到数量、无限与最大控制右侧,以无边框的“耗时”文字显示;购物车与生产摘要的英文界面也改用更清晰的系统字体。",
          "行动队列更新现在按行动 ID 合并、去重并按真实序号排序,上下重排后会在已打开的队列内立即刷新耗时。遇到无限行动时保留此前可达的有限总时长并显示“有限时长 + ∞”,无限后的行动不再计算或残留旧时间;队列关闭后会立即停止相关观察与延迟校验。",
          "重复插件提醒新增 MWI TaskManager 识别,仅在任务排序标记与其专用任务、行动、战斗或副本标记组合出现时提示,避免单个通用页面标记造成误报。",
          "通用设置新增“悬浮窗口字号”,可在标准、较大和最大三档之间即时切换生产利润、宝箱估值与强化成本窗口的文字大小;只更新悬浮层样式,不影响游戏原生提示和页面布局。",
          "修复资产中心在强化等高频资产更新期间反复重建当前页面,导致按钮无法点击、图表悬浮提示消失的问题;打开期间现在保持按钮和画布节点稳定,只原位更新顶部资产数字。",
          "优化手机端资产中心的图表生命周期:隐藏的盈亏页不再持续重建图表,打开资产中心时会先释放底层画布,离开资产页后也会立即停止图表工作,避免游戏持续卡顿。",
          "修复 DPS 职业可能长期沿用旧自动缓存的问题;本场明确的武器与近战、魔法战斗属性现在会纠正旧结果,手动指定仍保持最高优先级,只有无法仅凭攻速可靠区分的弓弩继续保留已有精确装备识别。",
          "DPS 面板新增使用“稳定射击”图标的实时命中率排行,可查看每位玩家的可靠直伤命中数与出手数;悬停玩家行可查看对各怪物的命中率。统计会保存到新战斗历史,并排除辅助、持续伤害、反伤与无法归属的同帧结算,避免显示误导数字。",
          "修复 DPS 标题栏在游戏图集尚未加载时永久缺失大部分图标的问题;伤害、恢复、承伤、命中率、历史与设置的官方图标会在资源就绪后自动补回,并兼容游戏调整后的图集路径。",
          "修复角色初始化或重新连接时生产缺料提示可能先读取旧库存的问题;现在直接使用本次角色消息中的完整库存建立快照,避免材料充足却被误报缺少。",
          "恢复发布脚本原有的可读构建,并改为压缩内置备用行情数据,使脚本保持在 Greasy Fork 大小上限以内;备用行情仅在网络行情与缓存均不可用时解压一次,不增加外部 CDN 依赖。",
          "游戏物品、行动、怪物、技能、副本与 Buff 现在直接使用当前游戏版本的官方客户端数据和当前语言资源,覆盖全部九种游戏语言;已移除内置旧中文实体表、固定副本名单、漂移的技能时长和带构建哈希的图标地址。数据在启动时从游戏本地缓存读取一次并按版本保存语言资源,不轮询服务器、不预载其他语言,也不会新增游戏数据网络请求。",
          "修复部分浏览器在资产快照刷新或切换角色页面时抛出 contains 权限错误、导致资产图表刷新失败的问题;图表现在只会在画布仍连接页面时绘制,并会安全处理游戏界面重建。",
          "修复打开角色页“盈亏”后隐藏状态监听与图表重建相互触发、导致单核 CPU 持续占满的问题;盈亏页现在会在界面稳定后停止工作,行动、公会、任务、角色页与顶部入口也会共享重复的页面观察,降低默认运行开销。",
          "恢复任务页地牢筛选按钮的官方图标;即使当前页面尚未加载行动图集,也会从游戏资源清单补全图集地址并自动替换菱形占位符。属于地牢的怪物任务卡现在也会在怪物图旁显示所有匹配地牢的同尺寸图标。",
          "恢复食物与饮品的回复性价比悬浮提示,可按市场价值查看回复 100 血或蓝所需金币;设置中的“消耗品性价比”默认开启,不再显示旧版的每分钟回复和理论每日用量。",
          "优化任务页打开速度:官方名称回退和图集地址现在按当前游戏数据复用,旧地图序号会直接沿用任务分类结果,避免每张任务卡重复扫描整套实体数据和全页图标。",
          "DPS 命中率悬浮明细现在会在怪物名称前显示对应的官方怪物图标,多个目标更容易快速区分。",
          "新增自托管更新源发布流程;发布前会校验脚本、元数据、版本和更新地址,并在 CDN 缓存刷新完成后再确认成功,避免损坏的更新文件或旧脚本被发布。"
        ]),
        en: Object.freeze([
          "Improved first-open and switching performance for Inventory: enhanced equipment now reuses matching probability plans, production, refining, and shop sources are looked up by target item, and the summary and sorting controls do less first-frame style work. Total assets, category values, and sorting still appear synchronously and in full.",
          "Fixed severe character-page lag and Planning sometimes failing to load on mobile while the feature was enabled. The Planning editor is now built only when first opened, responsive layout switches no longer recreate a hidden editor, and production targets use an output index so they remain discoverable while character data is loading.",
          "Added a native Purple Cow-style performance setup for fresh installs and upgrades. Choose Skilling, Combat, or Balanced, then Smooth, Standard, Full features, or grouped Custom settings; phones and touch devices recommend Smooth while desktop recommends Standard. The guide atomically configures DPS, buff countdowns, tasks and assets, guild enhancements, decorative motion, DPS graphs, and one- or two-second refresh cadence, while detailed profit calculations continue to require a held key or long press. An overall progress bar in the upper-left tracks custom steps, and applying the profile now refreshes the page automatically. General settings show the active profile and can restart the guide without changing anything when cancelled. Startup keeps message and data services available but waits to initialize optional features until the profile is confirmed, avoiding an all-features-first upgrade spike.",
          "Detecting an Iron Cow or Legacy Iron Cow character now automatically enables Iron Cow adaptation, hiding marketplace prices, trading profit, and marketplace procurement actions that are unavailable in those modes; these features remain visible after switching back to a standard character.",
          "A Refresh values button has returned beside Inventory sorting, letting players create a fresh asset snapshot on demand while preserving the selected sort and expanded summary. Shopping-list rows can now be reordered with mouse or touch and persist per character and server; keyed rows and the Clear unfavorited button also stay mounted during inventory and price updates instead of being rebuilt and flickering.",
          "Enhancement remaining time now updates in place and keeps the last valid native quantity for the same action when that text briefly disappears, preventing the estimate from flashing to infinity or vanishing. Production duration now appears as unboxed Duration text to the right of the quantity, infinity, and Max controls, and English Shopping Cart and Production Summary surfaces use a clearer system UI font stack.",
          "Action updates now merge and deduplicate by action ID and sort by the authoritative ordinal, so moving actions up or down refreshes an open queue immediately. The queue keeps every reachable finite duration before the first infinite action and displays “finite duration + ∞”; actions after infinity are not calculated and stale timing is removed. Queue observers and transition checks stop as soon as the menu or feature closes.",
          "Duplicate-script warnings now recognize MWI TaskManager only when its task-sort marker appears together with its task, action, combat, or dungeon markers, avoiding false positives from a single generic page ID.",
          "General settings now include Tooltip panel font size, with Standard, Large, and Largest options that update production profit, loot valuation, and enhancement cost text immediately. Only the floating panel styles change, leaving native game tooltips and page layout untouched.",
          "Fixed Asset Center repeatedly rebuilding the active page during high-frequency asset updates such as Enhancement, which made buttons unclickable and chart hover tooltips disappear. Open pages now keep their controls and canvas mounted while only the top asset figures update in place.",
          "Optimized Asset Center chart lifecycles on mobile: hidden P/L pages no longer rebuild charts, opening Asset Center releases the underlying canvas first, and leaving the asset page stops chart work immediately to prevent ongoing game lag.",
          "Fixed DPS roles remaining stuck on stale automatic classifications. Explicit current-battle weapon, melee, and magic evidence now corrects old results, manual choices remain highest priority, and existing exact equipment detection is preserved only when attack speed alone cannot reliably distinguish bows from crossbows.",
          "Added a live accuracy ranking to the DPS panel with the Steady Shot icon, showing each player's reliably resolved direct hits and attempts; hovering a player shows accuracy against each monster. New combat history retains these statistics, while support actions, damage-over-time ticks, reflected damage, and ambiguous simultaneous resolutions are excluded to avoid misleading results.",
          "Fixed most DPS title-bar icons remaining permanently missing when the game sprites had not loaded yet. The official damage, healing, damage-taken, accuracy, history, and settings icons now appear automatically once resources are ready and remain compatible with changed sprite paths.",
          "Fixed production shortage hints occasionally reading stale inventory during character initialization or reconnection. They now build their snapshot directly from the complete inventory in the current character message, preventing materials already owned from being reported as missing.",
          "Restored the original readable userscript build and compressed its embedded backup market data to stay within Greasy Fork's size limit. The backup is decompressed once only when both live and cached prices are unavailable, with no external CDN dependency added.",
          "Game items, actions, monsters, abilities, dungeons, and buffs now use official client data and the active locale resources for the current game version across all nine game languages. The bundled legacy Chinese entity table, fixed dungeon rosters, drifting ability durations, and build-hashed sprite URLs have been removed. Data is read once from the game's local cache at startup and locale resources are cached per version, without server polling, preloading other languages, or adding game-data network requests.",
          "Fixed some browsers throwing a contains permission error during asset snapshot refreshes or Character page switches, which could stop asset charts from refreshing. Charts now render only while their canvas remains connected and safely handle game UI rebuilds.",
          "Fixed the Character-page P/L view saturating one CPU core when hidden-state observation repeatedly triggered chart rebuilds. P/L now becomes idle once the UI settles, while action, guild, task, Character-page, and header features share duplicate page observers to reduce default runtime overhead.",
          "Restored the official icons on Task-page dungeon filters. When the current page has not loaded the action sprite yet, MWITools now completes its sprite registry from the game asset manifest and automatically replaces the diamond placeholders. Monster task cards now also show same-size icons for every matching dungeon beside the monster artwork.",
          "Restored recovery-efficiency details in food and drink tooltips, showing the market-value cost to restore 100 HP or MP. The Consumable efficiency setting is enabled by default without bringing back the old recovery-per-minute or theoretical daily-use figures.",
          "Improved Task-page opening speed by reusing official-name fallbacks and sprite locations for the current game data. Legacy zone labels now consume the existing task classification instead of rescanning every card, the full entity catalog, and page-wide icons.",
          "DPS accuracy details now show each monster's official icon beside its name, making multiple targets easier to distinguish at a glance.",
          "Added the self-hosted update-channel publishing flow. It validates the script, metadata, version, and update URLs before publishing, then waits for CDN cache invalidation before confirming success so broken update files or stale scripts are not released."
        ])
      })
    }),
    Object.freeze({
      id: "26.4.9",
      version: "26.4.9",
      publishedAt: "2026-08-13",
      title: Object.freeze({
        zh: "26.4.9 更新公告",
        en: "Version 26.4.9 update"
      }),
      body: Object.freeze({
        zh: Object.freeze([
          "修复角色页“规划”标签被误认成游戏原生“配装”标签,导致点击后立即重建并关闭;“盈亏”和“规划”现在会稳定共存,点击后保持打开。",
          "“规划”已从采购抽屉移到角色页“盈亏”旁,并改用带游戏原生物品与房屋技能图标的稳定搜索选择器,数据刷新时不再反复重建输入区或让已打开的选择菜单消失;项目对应的采购项全部买完或清除后,项目也会自动删除。",
          "新增独立“规划”计算器,可按物品最终持有量与房屋目标等级递归汇总制作步骤和基础材料;采购抽屉原“计划”更名为“项目”,购物车会分别记录手工、项目与规划来源,多项目统一分配库存,项目完成或规划需求降低时释放数量会转为手工来源而不会静默删除。",
          "规划改为“选择目标 → 选择制作方式 → 基础材料清单”三步流程:第一次计算只展开可按房屋来源分别调整的全链条制作、单步制作或直接购买策略,第二次计算才生成清单;共享物品的混合策略不再污染展开项,房屋名称支持中文,目标行和英文策略开关也更加紧凑稳定。无变化的游戏数据不再清空第 2 步,返回目标页会保留已生成的制作方式;添加器开关在常用宽度保持同排并完整显示,英文购买策略精简为 Buy,最终清单数量改用向上取整的千分位整数。角色页观察与隐藏面板更新也已收窄,减少滚动和切页卡顿。",
          "排行榜徽章新增总等级、迷宫深度、智力、耐力和任务积分,并使用游戏原生图标;徽章名次不再显示 # 前缀,个人主页会在姓名下方完整展示全部徽章,其他位置只保留名次最靠前的三个,好友列表则保持在姓名右侧;前五名彩色徽章默认启用一秒横扫白光、一秒右上角呼吸闪光和三秒停顿循环,也可在设置中关闭。",
          "修复切换到技能页再返回库存后,战斗与生活着装评分、总资产可能被残留的隐藏状态遮住;摘要和排序栏现在始终跟随游戏原生库存面板显隐,即使复用旧节点或晚到回调再次写入隐藏状态也会保持可见。",
          "库存中的战斗着装评分、生活着装评分和总资产现在会在本次页面会话首次计算后保持不变;技能、装备、资产或市场数据变化只会恢复原有显示,游戏在切换技能后单独移除摘要时也会自动补回,刷新网页后才会重新计算。",
          "修复生产面板重建、存在嵌套容器或更换战斗技能后,目标等级和生产次数快捷输入不显示;插件现在会识别实际弹窗表单,并在技能数据与面板先后更新时稳定恢复整组生产扩展。",
          "生活装备提醒现在使用当前游戏的红色厨师帽标识;红色厨师帽、掌上监工、收藏家靴和附魔手套穿戴后都会计入生活着装评分。",
          "任务筛选现在默认全部未选并显示所有任务,选择多个专业、战斗或副本时按并集显示,副本也可独立筛选;新增一键重置筛选,桌面端全部筛选按钮保持同一行,移动端按空间换行。",
          "总等级下方的意见中心左侧新增快捷设置齿轮,可在随时打开的浮窗中搜索并调整完整 MWITools 设置;设置页与快捷浮窗共享相同状态并立即生效。",
          "顶部当前动作条和行动队列中的每项动作现在统一显示纯剩余时长与括号内的 24 小时制结束时间,不再显示剩余次数或多余文字标签;结束时间跨日时会用第二组括号标记 +1 天、+2 天等实际相隔天数,队列总时间也会显示最终结束时刻。",
          "修复游戏改用独立饮品栏消息后,本次生产摘要没有及时读取加工茶的问题;现在切换加工茶后会立即按实际加工率拆分原料与加工品产出,并同步更新产出数量和利润,其他生产规划也会使用最新饮品栏。",
          "26.4.9 已标记为重要更新;仍在使用旧版本的玩家会收到顶部更新提示,以便及时获得本次规划、任务筛选、快捷设置、时间显示及生产修复。",
          "修复前五名排行榜徽章的闪光设置在徽章已经显示后可能不立即刷新,并隔离相关回归验证,避免较慢环境把正常的延迟渲染误判为失败。",
          "修复手机端切换到响应式角色面板后“规划”标签仍留在隐藏桌面面板的问题;规划现在会精确跟随可见的手机页签栏,并继续显示在“盈亏”旁。设置中也新增默认开启的独立“规划计算器”开关,不再与购物车和采购功能共用开关。第 2、3 步计算现在只滚动规划面板内部,不再把游戏页面推高并露出底部空白。第 2 步还会锁定点击计算时的库存与项目占用快照,后续游戏数据更新不会再让结果闪现后消失,第 3 步也不会改用更新后的库存;只有重新计算第 2 步才会换用新库存。“所需”数量现在显示扣除快照库存后的实际制作缺口,不再误显示最终持有目标总量;基础材料的“已覆盖/还需”也只按实际库存判断,不再把购物车数量当成已经持有。物品耗尽时,即使游戏的零数量更新省略了原堆叠 ID,采购库存缓存也会正确移除旧条目,不再把已经用完的碎片或其他物品算作仍然持有。"
        ]),
        en: Object.freeze([
          "Fixed the character-page Planning tab being mistaken for the native Loadout tab, which rebuilt and closed it immediately after a click. P/L and Planning now coexist reliably and Planning stays open after selection.",
          "Planning has moved from the procurement drawer to a character tab beside P/L, with stable item and house pickers that use native game item and skill icons and no longer rebuild the input area or close an open menu during data refreshes. Projects are also removed automatically once all of their shopping rows are purchased or cleared.",
          "MWITools now includes an independent Planning calculator that recursively summarizes production steps and base materials from final item holdings and house-level targets. Procurement “Plans” are now “Projects”; manual, project, and planning cart sources are tracked separately, multiple projects share inventory allocation correctly, and quantities released by completed projects or reduced planning demand become manual instead of being silently removed.",
          "Planning now follows three explicit steps: choose targets, choose production methods, then calculate the base-material list. The first calculation only opens per-target Full chain, One step, or Buy decisions, while the second creates the list. Mixed shared items no longer contaminate expanded source policies, house names are localized in Chinese, and compact target rows and English policy controls fit more reliably. Unchanged game data no longer clears Step 2 when returning from the List; picker policy controls stay inline and fully visible at common widths, the English purchase option is shortened to Buy, and final-list quantities use comma-grouped integers rounded up to cover demand. Narrower character-page observation plus deferred hidden-panel updates also reduce scrolling and tab-switching stutter.",
          "Leaderboard badges now include Total Level, Labyrinth Depth, Intelligence, Stamina, and Task Points with native game icons. Badge ranks no longer show a # prefix, profiles show every badge on a second row below the name, other locations keep only the three best ranks, and friend-list badges stay beside the name. Top-five rainbow badges now enable a one-second white sweep, a one-second upper-right breathing glint, and a three-second pause by default, with an option to turn the effect off.",
          "Fixed combat and skilling gear scores and total assets being obscured by a stale hidden state after switching to Abilities and returning to Inventory. The summary and sorting bar now always follow the native Inventory panel, remaining visible even when a reused node or delayed callback writes another hidden state.",
          "Combat gear score, skilling gear score, and total assets in Inventory now stay fixed after their first calculation in the current page session. Ability, equipment, asset, and market updates only restore the existing display, including when the game removes the summary separately after an ability change; reloading the page recalculates it.",
          "Fixed target-level controls and production count shortcuts not appearing after production-panel rebuilds, nested containers, or combat ability changes. MWITools now identifies the actual modal form and reliably restores the full extension group when ability data and the panel update at different times.",
          "Skilling equipment reminders now use the current Red Culinary Hat identifier. Red Culinary Hat, Eye Watch, Collector's Boots, and Enchanted Gloves all contribute to skilling gear score while equipped.",
          "Task filters now start unselected while showing every task, combine selected professions, combat, and dungeons as a union, and let dungeon filters work independently. A one-click reset was added; desktop keeps every filter on one row while mobile wraps as needed.",
          "A quick-settings gear now sits to the left of the Feedback Center below Total Level, opening the complete searchable MWITools settings in an always-available popover. The settings page and quick panel share state and apply changes immediately.",
          "The top current-action bar and every action in the queue now share the same plain time-remaining and parenthesized 24-hour finish-time format, without remaining counts or extra text labels. A second parenthetical marker shows +1 day, +2 days, and so on after midnight, and the queue total now includes its final finish time.",
          "Fixed Production Summary not picking up Processing Tea after the game moved drink-slot changes to a dedicated message. Switching Processing Tea now immediately splits raw and processed output at the effective rate, updates quantities and profit, and keeps other production planning on the latest drink loadout.",
          "Version 26.4.9 is now marked as an important update. Players still on an older release will see the top update prompt so they can receive the new planner, task filters, quick settings, timing display, and production fixes.",
          "Fixed the top-five leaderboard badge glint setting not always refreshing badges that were already visible, and isolated its regression coverage so slower environments no longer mistake normal deferred rendering for a failure.",
          "Fixed Planning remaining attached to a hidden desktop character panel after mobile switched to its responsive panel. Planning now follows the visible mobile tab bar precisely and stays beside P/L. Settings also include a separate Planning calculator switch, enabled by default and independent from shopping cart and procurement features. Step 2 and Step 3 calculations now scroll only inside Planning, preventing the game page from jumping upward and exposing a blank strip. Step 2 also locks the inventory and project-reservation snapshot from the moment Calculate is clicked: later game updates no longer make the result flash and disappear, Step 3 does not switch to newer inventory, and only recalculating Step 2 captures a new snapshot. Required quantities now show the actual production shortage after snapshot inventory instead of the final holding target total; base-material Covered/Need status also reflects actual inventory without treating cart quantities as already owned. When a stack is depleted, the procurement inventory cache now removes its old entry even if the game's zero-count update omits the original stack ID, so consumed fragments and other items are no longer counted as still owned."
        ])
      })
    }),
    Object.freeze({
      id: "26.4.8",
      version: "26.4.8",
      publishedAt: "2026-08-13",
      title: Object.freeze({
        zh: "26.4.8 更新公告",
        en: "Version 26.4.8 update"
      }),
      body: Object.freeze({
        zh: Object.freeze([
          "生产面板改用稳定挂载区:切换配装或游戏重建面板后,次数快捷输入、本次生产摘要、缺料提示和目标等级会自动恢复且不再重复;未填写数量与材料充足时也保持稳定占位,且不会再让弹窗持续变高,可在设置中隐藏仅材料充足的提示。",
          "本次生产摘要默认折叠,并可改为始终展开或关闭;新增三档 MWITools 字号,仅调整插件界面,改善生产、采购、库存价值、设置与利润详情中的小字阅读。",
          "总资产默认计入公会代币与奇幻、阴森、秘法、海盗地下城代币,并统一归入不可交易代币;设置中可关闭,当前资产、库存分类和后续历史快照会使用相同口径。",
          "普通物品悬浮窗新增效率与贪心两档生产总成本(含材料与茶饮),订单簿缺价时使用市场价值兜底,并提示自定义按键与移动端长按查看完整详情。",
          "装备分类恢复市场价值、出售价和收购价排序,并按强化等级与整堆数量显示价值角标;效率茶继续读取游戏 Buff 数据,回归验证基础效果为 +10%,数字 5 仅代表持续 5 分钟。",
          "降低手机端空闲、生产、市场和战斗统计的后台轮询与重复计算,修复反复打开行动队列后的内存占用增长,减少长时间挂机时的发热、耗电和卡顿。",
          "任务自动返回现在只恢复任务列表内部的滚动位置,并会等待列表布局稳定;新任务同时进入队列时不再把整个页面滚到空白区域。顶部当前动作时间也改为跟随游戏原生字号,并在可用空间不足时隐藏预计完成时间以保持紧凑。",
          "手机端长按打开生产收益、宝箱估值或强化成本后,松手及原生物品提示消失时详情会继续显示;点击详情内部可滚动或操作,只有点击窗口外才会关闭。",
          "制造和指定次数强化的多材料余缺提示改为固定四列逐行对齐,不再横向撑宽;物品价格会下移避开强化等级,同时保留原有文字样式。",
          "技能书计算器新增“加入购物车”,会自动扣除当前库存和购物车已有数量,只加入达到目标等级所需的净缺口。",
          "公会信用兑换推荐默认显示 3 个方案,并可在设置中通过下拉菜单自由选择显示 1–8 个;修改后已打开的推荐会立即更新。",
          "移动端意见中心压缩了表单和公告的空白,输入框会按内容与屏幕高度自适应;标题与三个页签保持可见,公告和表单改为弹窗正文内独立滚动。",
          "任务筛选移除不会出现的炼金与强化类型;桌面端会尽量将生活技能和战斗筛选排在同一行,空间不足时五个战斗按钮会整组换行,同时通过缓存任务解析与战斗索引降低大量任务时的卡顿。",
          "排行榜徽章改用游戏原生技能与名望图标,不再从 MWITools 排行榜服务器加载图标文件。",
          "公会成员与好友列表的排行榜徽章改为在名字下方紧凑排列,不再挤压或截断角色名;名字颜色设置中的角色预览也会显示对应徽章。",
          "修复重置任务后卡片被原地复用时,“前往”仍按旧任务计算合并数量;现在会根据当前卡片和最新任务数据重新汇总。",
          "战斗模拟器重新从最近一场战斗读取队友实际携带的食物和咖啡;尚未取得对应战斗数据时,组队导入也不再中断。",
          "修复中英文下顶部当前动作时间在窄窗口中与动作名称或排队按钮重叠;可用空间不足时会自动精简显示。",
          "修复刷新任务或页面时任务图标串位、被替换后消失,以及火车提示漂移并撑出横向滚动条;普通任务不再显示“无需火车”。",
          "修复开启任务战斗地图序号后,任务统计、筛选和背景图标整组不显示;任务身份匹配现在会忽略插件自身的“图N”标记,并能按目标怪物识别战斗任务。",
          "生产、全链条与火车现在统一读取当前茶饮、社区等行动增益和暴饮之囊;换茶后会即时重算,链条按实际产量规划,采集利润与队列耗时不再漏算社区速度。"
        ]),
        en: Object.freeze([
          "Production panels now use a stable mount area. Quick counts, production summary, shortage hints, and target level automatically return once a loadout switch or game render replaces the panel, without duplicates. Waiting and ready states keep their space stable without making the dialog grow continuously, with an option to hide only the ready hint.",
          "Production summary is collapsed by default and can be set to always expanded or off. Three MWITools font-size levels improve small text in production, procurement, inventory values, settings, and profit details without changing the game's native UI.",
          "Total assets now include Guild, Chimerical, Sinister, Enchanted, and Pirate Tokens by default under non-tradable tokens. The setting applies the same inclusion rule to current assets, inventory categories, and future history snapshots.",
          "Regular item tooltips now show Efficiency and Greedy total production costs including materials and drinks, fall back to market value when an order-book side is missing, and explain the custom-key or mobile long-press gesture for full details.",
          "Equipment categories once again support market-value, ask, and bid sorting with value badges based on enhancement and full stack count. Efficiency Tea remains driven by game Buff data, with regression coverage confirming +10% base effect while 5 only means five minutes of duration.",
          "Reduced background polling and repeated work across idle, production, market, and combat-stat views on mobile, and fixed memory growth after repeatedly opening the action queue to reduce heat, battery drain, and long-session stutter.",
          "Task auto-return now restores only the task list's internal scroll position and waits for its layout to settle, so a newly queued task no longer scrolls the whole page into a blank area. The top current-action time also follows the game's native font size and hides the finish time when space is tight to stay compact.",
          "On mobile, production profit, loot valuation, and enhancement cost details opened by a long press now remain visible after release or after the native item tooltip disappears. Taps and scrolling inside remain interactive, and only a tap outside closes the detail window.",
          "Multi-material shortage indicators in production and fixed-count enhancing now stay aligned in four explicit columns without widening the panel. Item prices move below enhancement levels while keeping their existing text style.",
          "The ability-book calculator now includes Add to cart and subtracts both current inventory and quantities already in the cart, adding only the net shortage needed for the target level.",
          "Guild Credit Exchange shows three recommendations by default, with a settings dropdown for choosing one through eight. Open recommendations update immediately when the value changes.",
          "The mobile Feedback Center now removes excess form and announcement spacing, and text boxes adapt to their content and screen height. The title and all three tabs stay visible while announcements and forms scroll independently inside the modal body.",
          "Task filters no longer include the unavailable Alchemy and Enhancing types. Desktop layouts keep profession and combat filters on one row when possible, move all five combat buttons together when space is tight, and reduce large-task-list lag through cached task parsing and combat indexes.",
          "Leaderboard badges now use the game's native skill and Fame icons instead of loading icon files from the MWITools leaderboard server.",
          "Leaderboard badges in guild member and friend lists now use a compact row below the name instead of squeezing or truncating it, and character previews in the name-color setting now show their badges too.",
          "Fixed Go still using stale merged counts when a rerolled task reused the same card. Merge totals are now recalculated from the current card and latest task data.",
          "Combat simulators once again read teammates' actual food and coffee from the latest battle. Group imports also no longer stop when matching battle data has not been captured yet.",
          "Fixed the top current-action timing overlapping the action name or queued-actions button in narrow windows in both Chinese and English. The summary now simplifies itself when space is limited.",
          'Fixed task icons moving to the wrong card or disappearing after task and page refreshes, along with train labels drifting and causing horizontal overflow. Ordinary tasks no longer show "No train needed."',
          `Fixed task statistics, filters, and background icons all disappearing when task combat-map numbers were enabled. Task identity matching now ignores MWITools' own "Map N" labels and recognizes combat tasks by their target monsters.`,
          "Production, full-chain planning, and trains now share the current drinks, community and other action buffs, and Guzzling Pouch effects. Drink changes recalculate immediately, chains use effective output, and gathering profit and queue timing no longer miss community speed."
        ])
      })
    }),
    Object.freeze({
      id: "26.4.7",
      version: "26.4.7",
      publishedAt: "2026-08-13",
      title: Object.freeze({
        zh: "26.4.7 更新公告",
        en: "Version 26.4.7 update"
      }),
      body: Object.freeze({
        zh: Object.freeze([
          "修复移动浏览器工具栏变化后页面底部偶尔出现白条并整体上移;Sunny 强化倍数按钮现在可再次把对应期望次数的净缺料加入购物车。",
          "资产中心图例在实时资产刷新后继续保持隐藏或显示状态。",
          "修复九种官方语言下库存评分与总资产、当前行动倒计时、任务合并与自动返回、战斗每小时统计不显示或未生效的问题;装备分类也不再因语言不同参与库存排序。",
          "精炼背部装备加入购物清单时不再包含不可交易的原始背部物品;生产时长快捷按钮现在结合当前综合效率向上换算,避免队列早于所选时长结束。",
          "修复任务页重复出现多个规划火车、资产中心日期选择器被实时刷新关闭,以及强化当前行动条的剩余次数与预计完成时间偶尔不显示。",
          "意见审理台现在显示反馈者使用的 MWITools 版本;重大更新清单支持 GitHub 失败后从反馈服务器读取,并明确显示最新版本且每个版本最多提醒一次。",
          "移动端生产摘要改为紧凑双列,顶部行动条只保留剩余次数和时间并随内容收缩;桌面端继续显示预计完成时间。意见中心入口统一精简为单行 MWITools。",
          "DPS 面板改为每秒刷新一次,伤害、治疗和击杀仍按战斗消息实时累计;长时间离线或挂起后会直接快进时间桶,不再逐个循环补齐。",
          "任务卡的语义匹配与火车升级链改用索引,任务附属控件按需刷新,并取消功能关闭后尚未执行的帧回调,降低乱序任务和战斗期间的后台开销。",
          "物品等级、市场筛选和地图编号改为按交互与数据消息刷新;DPS 启动器及收益、强化浮窗缩小观察范围,并移除高 GPU 占用的背景模糊。",
          "强化成本现在与生产收益和宝箱估算共用同一个自定义快捷键;桌面端按住触发,移动端长按触发。",
          "版本公告恢复按版本独立保存,26.4.6 的历史内容不再混入本版公告。",
          "生产购物清单现在默认补齐上一层成品与当前步骤材料,也可通过“所选链条”开关按勾选阶段补齐;有限次数的总产出、耗时和利润不再被当前库存截断,无限次数且无库存时会明确显示为 0。",
          "任务页在当前页面刷新出新任务时,会立即将所有新任务置顶,再按专业和战斗分类排序;普通进度刷新继续保持卡片位置稳定。",
          "修复不同缩放比例或窄窗口下任务页礼物、未读提示和普通任务卡宽度不一致、列边界错位及横向溢出。",
          "修复聊天框或数量输入框仍有焦点时,按住 Ctrl 等修饰键不显示生产利润、宝箱估算和强化成本面板;自定义字母键仍会在输入时避免误触。"
        ]),
        en: Object.freeze([
          "Fixed an occasional bottom white strip and upward-shifted game layout after mobile browser toolbar changes. Sunny's enhancement multiplier buttons can again add the net shortages for their expected action counts to the shopping cart.",
          "Asset Center legend visibility now persists through live asset refreshes.",
          "Fixed inventory scores and total assets, the current-action countdown, task merging and auto-return, and hourly battle statistics not appearing or activating across all nine official game languages. Equipment also stays excluded from inventory sorting in every language.",
          "Refined back equipment no longer adds its untradeable base item to the shopping list. Production duration shortcuts now round up using current total efficiency so queues do not finish before the selected duration.",
          "Fixed duplicate train-planning controls on tasks, live asset refreshes closing date pickers, and remaining counts and completion estimates intermittently missing from the current-action bar while enhancing.",
          "The feedback review console now shows each reporter's MWITools version. Important-update manifests fall back to the feedback server when GitHub fails, show the latest version explicitly, and appear at most once per version.",
          "Mobile production summaries now use a compact two-column layout, and the mobile current-action bar keeps only the remaining count and time while shrinking to its content. Desktop keeps the finish-time estimate. The Feedback Center launcher is shortened to a single-line MWITools label.",
          "The DPS panel now refreshes once per second while damage, healing, and kills remain event-driven. Long suspended gaps fast-forward graph buckets instead of replaying every missing interval.",
          "Task semantic matching and train upgrade lookups now use indexes, task decorations refresh on demand, and queued frames are cancelled on feature cleanup to reduce shuffled-task and combat overhead.",
          "Item levels, market filters, and map indexes now refresh on interactions and data messages. DPS launcher and estimate-panel observers are narrower, and expensive backdrop blur was removed.",
          "Enhancement costs now share the same custom shortcut as production profit and loot chest estimates: hold the key on desktop or long-press on touch devices.",
          "Release announcements are stored separately by version again, so the 26.4.6 history is no longer mixed into this release.",
          "Production shopping lists now default to the direct predecessor and current-step materials, with a “Selected chain” switch for the checked stages. Finite totals are no longer capped by current inventory, while infinite production with no stock now clearly shows zero.",
          "When new tasks arrive on the current task page, all new tasks now move to the top before profession and combat sorting. Ordinary progress refreshes continue to keep card positions stable.",
          "Fixed mismatched widths, misaligned column edges, and horizontal overflow between gift, unread-notice, and regular task cards at different zoom levels or in narrow windows.",
          "Fixed production profit, loot estimate, and enhancement cost panels not appearing while Ctrl or another modifier key was held with focus still in chat or a quantity input. Custom letter shortcuts remain suppressed while typing."
        ])
      })
    }),
    Object.freeze({
      id: "26.4.6",
      version: "26.4.6",
      publishedAt: "2026-08-12",
      title: Object.freeze({
        zh: "26.4.6 更新公告",
        en: "Version 26.4.6 update"
      }),
      emphasizedBodyIndexes: Object.freeze([6]),
      body: Object.freeze({
        zh: Object.freeze([
          "意见反馈升级为意见中心,新增版本公告,并统一使用红点提醒反馈回复和新公告。",
          "任务页改为平铺布局,支持新任务、已完成任务、生活专业、战斗和四个副本的排序与图标筛选,并提供手动重新排序。",
          "资产中心支持在历史记录缺失日期之间补录七项资产;同一轮资产估值固定使用一份行情快照,避免实时价格变化造成统计口径不一致。",
          "公会经验统计改为近 6 小时、24 小时和成员本周平均速率;七日趋势使用 6 小时滚动平均,并修正升级经验与预计升级时间计算。",
          "新增铁牛模式适配开关,自动识别铁牛和旧铁牛角色;开启后隐藏不可用的市场价格、利润与市场采购操作,同时保留资产和宝箱估值。",
          "修复点金、分解、转化和解精炼的完成时间:现在会结合所选物品批量、催化剂、金币、完成次数和当前周期计算,缺少选择时不再显示无穷大。",
          "生产利润和宝箱估算悬浮默认需要同时按住 Ctrl,可在设置中改成任意单键;移动端均需 800 毫秒长按,并支持滑动取消与点外关闭。",
          "迷宫活动期间暂停所有生活装备提醒,离开迷宫后自动恢复。",
          "购物车升级链新增“从上一步开始”,可直接购买上一层成品与当前步骤材料,不再继续拆解上一层装备。",
          "资产与吃书经验不再显示浮点尾数;宝箱碎片自制钥匙会计入工匠减耗、浓缩倍率和泡饮成本,并可选择忽略所有牛铃价值。",
          "购物车新增默认关闭的“加购后自动展开”,开启后任意入口成功加购都会直接打开购物清单。",
          "兼容游戏全部九种内置语言;库存、悬浮窗、任务、行动、市场和 DPS 等功能现在会直接使用游戏当前语言的官方词表,不再因繁体中文或其他语言名称不同而失效。",
          "移除作用有限的消耗品回复速度、单位回复成本和理论每日用量显示。",
          "修复购物车数量加减按钮长按后可能无法停止,现在松手、清空、删除、收起或切换页签都会立即结束连续加减。",
          "数字解析和显示现在跟随游戏内语言,修复逗号作为小数点、句点或空格作为千分位时,生产材料、房屋数量、任务进度和行动时间计算错误,并稳定公会经验速率条宽度。",
          "修复中文以外的游戏语言下火车点击加入队列后不续站,以及角色管理页不显示盈亏标签的问题。",
          "修复全服技能与公会排行榜可能被经验速率排序或当前角色置顶改变顺序;经验速率继续作为只读信息显示。",
          "资产中心的分项图表改为显示各日期的实际资产持有值,图例可点击隐藏或恢复曲线,悬浮数值使用 K/M/B/T 单位。",
          "修复精炼生活披风等背部装备提示没有新缺料的问题;强化缺料加购移至右侧信息列,单阶段与多阶段升级链均可正确加入购物车。",
          "关闭产出与库存摘要后不再重新出现本次生产摘要;同时减少任务页重复的多语言匹配和插件自身刷新,改善英文界面卡顿。"
        ]),
        en: Object.freeze([
          "Feedback is now the Feedback Center, with release announcements and one red-dot notification for replies and new announcements.",
          "Tasks now use a flat layout with sorting and icon filters for new, completed, profession, combat, and four dungeon categories, plus manual re-sorting.",
          "The Asset Center can insert seven-component records into missing historical dates. One market snapshot is used per valuation session to keep totals consistent while live prices change.",
          "Guild XP now shows 6-hour, 24-hour, and member this-week average rates. The seven-day trend uses a 6-hour rolling average, with corrected level requirements and ETA calculations.",
          "Added an Iron Cow adaptation switch that recognizes both Iron Cow modes. When enabled, unavailable market prices, profits, and marketplace purchasing actions are hidden while asset and loot chest valuations remain available.",
          "Fixed completion times for Coinify, Decompose, Transmute, and Unrefine by accounting for the selected stack, bulk size, catalyst, coins, completed count, and current cycle. Missing selections no longer appear as infinite.",
          "Production profit and loot chest estimate tooltips now require holding Ctrl by default, with any single key configurable in settings. Both use an 800 ms long press on touch devices with movement cancellation and outside-tap dismissal.",
          "All skilling equipment reminders pause during an active Labyrinth run and resume automatically after leaving it.",
          "Upgrade chains now offer “Start from previous” to buy the direct predecessor and current-step materials without breaking the predecessor down further.",
          "Asset and ability-book XP displays no longer show floating-point tails. Fragment-crafted key estimates now include Artisan reduction, concentration, and drink costs, with an option to ignore all Cowbell value.",
          "The cart adds an off-by-default “Expand after adding” option that opens the shopping list after any successful addition.",
          "Added compatibility with all nine built-in game languages. Inventory, tooltips, tasks, actions, marketplace tools, DPS, and related features now use the official dictionary for the active game language instead of failing on Traditional Chinese or other localized names.",
          "Removed the low-value consumable recovery-rate, cost-per-recovery, and theoretical daily-use display.",
          "Fixed shopping-cart quantity buttons sometimes continuing forever after a long press. Releasing, clearing, deleting, collapsing, or changing tabs now stops repeat adjustments immediately.",
          "Number parsing and display now follow the in-game language, fixing production materials, house quantities, task progress, and action timing when commas are decimals and periods or spaces are grouping separators, while stabilizing guild XP rate-bar widths.",
          "Fixed trains not advancing after queue submission and the P/L tab missing from Character Management when the game uses a non-Chinese language.",
          "Fixed standard skill and guild leaderboard order being changed by XP-rate sorting or current-character pinning; XP rates remain available as read-only information.",
          "Asset component charts now show actual holdings for each date, legends can hide and restore lines, and hover values use K/M/B/T units.",
          "Fixed refined skilling capes and other back equipment reporting no new shortages. Enhancement shopping now sits in the right-hand information column, and both single- and multi-stage upgrade chains add materials correctly.",
          "Disabling the output and inventory summary now keeps the production summary hidden. Repeated multilingual task matching and MWITools-owned refreshes were also reduced to improve English task-page performance."
        ])
      })
    })
  ]);
  function defaultGetValue(key, fallback) {
    try {
      return typeof GM_getValue === "function" ? GM_getValue(key, fallback) : fallback;
    } catch {
      return fallback;
    }
  }
  function defaultSetValue(key, value) {
    try {
      if (typeof GM_setValue === "function") GM_setValue(key, value);
    } catch {
    }
  }
  function parseSeenIds(value) {
    const parsed = typeof value === "string" ? JSON.parse(value || "[]") : value;
    return new Set(Array.isArray(parsed) ? parsed.map(String) : []);
  }
  var AnnouncementStore = class {
    constructor({
      announcements = ANNOUNCEMENTS,
      getValue = defaultGetValue,
      setValue = defaultSetValue
    } = {}) {
      this.announcements = [...announcements].sort(
        (left, right) => String(right.publishedAt).localeCompare(String(left.publishedAt)) || String(right.version).localeCompare(String(left.version), void 0, {
          numeric: true
        })
      );
      this.getValue = getValue;
      this.setValue = setValue;
      try {
        this.seenIds = parseSeenIds(this.getValue(STORAGE_KEY, []));
      } catch {
        this.seenIds = /* @__PURE__ */ new Set();
      }
    }
    list() {
      return [...this.announcements];
    }
    unread() {
      return this.announcements.filter((item) => !this.seenIds.has(item.id));
    }
    markAllRead() {
      const unread = this.unread();
      if (!unread.length) return 0;
      for (const item of unread) this.seenIds.add(item.id);
      this.setValue(STORAGE_KEY, [...this.seenIds]);
      return unread.length;
    }
  };
  var announcementStorageKey = STORAGE_KEY;

  // src/features/opinion-center/client.js
  var API_BASE = "https://feedback.43.167.210.211.sslip.io/api/v1";
  var TOKEN_PREFIX = "MWITools_feedback_identity_v1";
  var REQUEST_TIMEOUT = 1e4;
  var MAX_IMAGE_LINKS = 3;
  function t14(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  var SERVER_ERROR_LABELS = {
    "Required text is missing": ["必填内容不能为空", "Required text is missing"],
    "Bearer token required": [
      "缺少反馈身份令牌",
      "Feedback identity token is missing"
    ],
    "Invalid bearer token": [
      "反馈身份令牌无效",
      "Feedback identity token is invalid"
    ],
    "Identity is not registered": [
      "反馈身份尚未注册",
      "Feedback identity is not registered"
    ],
    "Invalid image link list": ["图片链接列表无效", "Invalid image link list"],
    "Image links must be a list": [
      "图片链接必须是列表",
      "Image links must be a list"
    ],
    "At most 3 image links are allowed": [
      "最多只能填写 3 个图片链接",
      "At most 3 image links are allowed"
    ],
    "Image links must use http or https": [
      "图片链接只支持 HTTP 或 HTTPS",
      "Image links must use HTTP or HTTPS"
    ],
    "Feedback not found": ["未找到这条反馈", "Feedback not found"],
    "This private identity belongs to another character": [
      "当前反馈身份属于其他角色",
      "This feedback identity belongs to another character"
    ],
    "Invalid feedback type": ["反馈类型无效", "Invalid feedback type"],
    "Invalid context JSON": ["反馈环境信息无效", "Invalid feedback context"],
    "Context must be an object": [
      "反馈环境信息格式无效",
      "Feedback context must be an object"
    ],
    "Weekly feedback limit reached": [
      "本周反馈额度已用完",
      "Weekly feedback limit reached"
    ],
    "Closed feedback cannot be edited": [
      "已结束的反馈不能修改",
      "Closed feedback cannot be edited"
    ],
    "Closed feedback cannot receive messages": [
      "已结束的反馈不能继续留言",
      "Closed feedback cannot receive messages"
    ]
  };
  function localizeErrorDetail(detail) {
    const value = String(detail ?? "").trim();
    const labels = SERVER_ERROR_LABELS[value];
    if (labels) return t14(...labels);
    const limit = /^Text exceeds (\d+) characters$/.exec(value);
    if (limit) {
      return t14(
        `内容不能超过 ${limit[1]} 个字符`,
        `Text cannot exceed ${limit[1]} characters`
      );
    }
    return value;
  }
  function gameServer() {
    return String(globalThis.location?.hostname ?? "unknown");
  }
  function encodeToken(bytes) {
    let binary = "";
    for (const byte of bytes) binary += String.fromCharCode(byte);
    return globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
  }
  function createToken() {
    const bytes = new Uint8Array(32);
    globalThis.crypto.getRandomValues(bytes);
    return encodeToken(bytes);
  }
  function getPrivateValue(key) {
    try {
      return typeof GM_getValue === "function" ? GM_getValue(key, "") : "";
    } catch {
      return "";
    }
  }
  function setPrivateValue(key, value) {
    if (typeof GM_setValue === "function") GM_setValue(key, value);
  }
  function parseResponse(response) {
    const text = response?.responseText ?? response?.response ?? "";
    if (!text) return null;
    if (typeof text === "object") return text;
    try {
      return JSON.parse(text);
    } catch {
      return { detail: String(text) };
    }
  }
  function request({ token, path, method = "GET", body }) {
    const requestFn = typeof GM !== "undefined" && typeof GM.xmlHttpRequest === "function" ? GM.xmlHttpRequest : typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : null;
    const headers = { Authorization: `Bearer ${token}` };
    if (body && !(body instanceof globalThis.FormData))
      headers["Content-Type"] = "application/json";
    if (!requestFn) {
      const controller = new globalThis.AbortController();
      const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
      return globalThis.fetch(`${API_BASE}${path}`, {
        method,
        headers,
        signal: controller.signal,
        body: body instanceof globalThis.FormData ? body : body ? JSON.stringify(body) : void 0
      }).then(async (response) => {
        if (!response.ok) {
          const payload = await response.json().catch(() => ({}));
          throw new Error(
            localizeErrorDetail(payload.detail) || `HTTP ${response.status}`
          );
        }
        return response.json();
      }).catch((error) => {
        if (error?.name === "AbortError") {
          throw new Error(
            t14("意见反馈服务请求超时", "Feedback service request timed out")
          );
        }
        throw error;
      }).finally(() => clearTimeout(timeout));
    }
    return new Promise((resolve, reject) => {
      let settled = false;
      let watchdog;
      const finish = (response) => {
        if (settled) return;
        settled = true;
        clearTimeout(watchdog);
        const status = Number(response?.status) || 0;
        if (status < 200 || status >= 300) {
          const payload = parseResponse(response);
          const error = new Error(
            localizeErrorDetail(payload?.detail) || t14(
              `反馈服务返回 HTTP ${status}`,
              `Feedback service returned HTTP ${status}`
            )
          );
          error.status = status;
          reject(error);
          return;
        }
        resolve(parseResponse(response));
      };
      const fail = (message) => {
        if (settled) return;
        settled = true;
        clearTimeout(watchdog);
        reject(new Error(message));
      };
      watchdog = setTimeout(
        () => fail(t14("意见反馈服务请求超时", "Feedback service request timed out")),
        REQUEST_TIMEOUT + 1e3
      );
      try {
        const result = requestFn({
          method,
          url: `${API_BASE}${path}`,
          headers,
          data: body instanceof globalThis.FormData ? body : body ? JSON.stringify(body) : void 0,
          responseType: "text",
          timeout: REQUEST_TIMEOUT,
          anonymous: false,
          onload: finish,
          onerror: () => fail(
            t14("无法连接意见反馈服务", "Unable to reach the feedback service")
          ),
          ontimeout: () => fail(t14("意见反馈服务请求超时", "Feedback service request timed out"))
        });
        result?.then?.(finish).catch((error) => fail(error.message));
      } catch (error) {
        fail(error.message);
      }
    });
  }
  function normalizeImageLinks(value) {
    const values = Array.isArray(value) ? value : String(value ?? "").split(/\r?\n/);
    const links = values.map((item) => String(item).trim()).filter(Boolean);
    if (links.length > MAX_IMAGE_LINKS) {
      throw new Error(
        t14("最多只能填写 3 个图片链接", "At most 3 image links are allowed")
      );
    }
    for (const link of links) {
      if (link.length > 2e3)
        throw new Error(t14("图片链接过长", "The image link is too long"));
      let url;
      try {
        url = new URL(link);
      } catch {
        throw new Error(t14("图片链接格式不正确", "Invalid image link format"));
      }
      if (!["http:", "https:"].includes(url.protocol)) {
        throw new Error(
          t14("图片链接只支持 HTTP 或 HTTPS", "Image links must use HTTP or HTTPS")
        );
      }
    }
    return links;
  }
  function appendFeedbackForm(form, value) {
    form.append("kind", value.type);
    form.append("title", value.title);
    form.append("detail", value.detail);
    form.append("reproduction", value.reproduction ?? "");
    form.append("expected", value.expected ?? "");
    if (value.context) form.append("context", JSON.stringify(value.context));
    form.append(
      "imageLinks",
      JSON.stringify(normalizeImageLinks(value.imageLinks))
    );
    return form;
  }
  var FeedbackClient = class {
    constructor({ characterId, characterName }) {
      this.characterId = String(characterId);
      this.characterName = String(characterName ?? "");
      this.server = gameServer();
      this.storageKey = `${TOKEN_PREFIX}:${this.server}:${this.characterId}`;
      this.token = getPrivateValue(this.storageKey) || createToken();
      setPrivateValue(this.storageKey, this.token);
      this.registered = false;
      this.identityPromise = null;
    }
    async ensureIdentity() {
      if (this.registered) return;
      if (!this.identityPromise) {
        this.identityPromise = request({
          token: this.token,
          path: "/identity",
          method: "POST",
          body: {
            gameServer: this.server,
            characterId: this.characterId,
            characterName: this.characterName,
            source: "mwitools-userscript"
          }
        }).then((result) => {
          this.registered = true;
          return result;
        }).finally(() => {
          this.identityPromise = null;
        });
      }
      return this.identityPromise;
    }
    async call(path, options = {}) {
      await this.ensureIdentity();
      return request({ token: this.token, path, ...options });
    }
    quota() {
      return this.call("/quota");
    }
    list() {
      return this.call("/feedback");
    }
    detail(id) {
      return this.call(`/feedback/${encodeURIComponent(id)}`);
    }
    submit(value) {
      return this.call("/feedback", {
        method: "POST",
        body: appendFeedbackForm(new globalThis.FormData(), value)
      });
    }
    edit(id, value) {
      return this.call(`/feedback/${encodeURIComponent(id)}`, {
        method: "PUT",
        body: appendFeedbackForm(new globalThis.FormData(), value)
      });
    }
    reply(id, body) {
      return this.call(`/feedback/${encodeURIComponent(id)}/messages`, {
        method: "POST",
        body: { body }
      });
    }
    markRead(id) {
      return this.call(`/feedback/${encodeURIComponent(id)}/seen`, {
        method: "POST"
      });
    }
  };
  function feedbackContext(client) {
    const info = globalThis.GM_info?.script ?? {};
    return {
      scriptVersion: String(info.version ?? "unknown"),
      gameServer: client.server,
      characterId: client.characterId,
      characterName: client.characterName,
      browser: String(globalThis.navigator?.userAgent ?? "").slice(0, 500)
    };
  }

  // src/features/header-tools.js
  var HEADER_TOOLS_ID = "mwitools-header-tools";
  function findHeaderTotalLevel(documentRef = document) {
    const direct = documentRef.querySelector(
      '[class*="Header_totalLevel"],[class*="totalLevel"]'
    );
    if (direct) return direct;
    return [
      ...documentRef.querySelectorAll(
        'header div,header span,[class*="Header"] div'
      )
    ].find((node) => {
      const text = node.textContent?.trim() ?? "";
      return text.length < 80 && /^(总等级|Total Level)\s*[::]/i.test(text);
    });
  }
  function ensureHeaderToolsHost(documentRef = document) {
    const totalLevel = findHeaderTotalLevel(documentRef);
    if (!totalLevel?.parentElement) return null;
    let host = documentRef.getElementById(HEADER_TOOLS_ID);
    if (!host) {
      host = documentRef.createElement("div");
      host.id = HEADER_TOOLS_ID;
    }
    if (host.parentElement !== totalLevel.parentElement || host.previousElementSibling !== totalLevel) {
      totalLevel.insertAdjacentElement("afterend", host);
    }
    return host;
  }
  function removeHeaderToolsHostIfEmpty(documentRef = document) {
    const host = documentRef.getElementById(HEADER_TOOLS_ID);
    if (host && !host.childElementCount) host.remove();
  }

  // src/features/opinion-center/panel.js
  var ROOT_ID2 = "mwitools-feedback-root";
  var BUTTON_ID = "mwitools-feedback-button";
  var STYLE_ID15 = "mwitools-feedback-style";
  function t15(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function statusLabel(status) {
    const labels = {
      pending: ["待处理", "Pending"],
      processing: ["处理中", "Processing"],
      closed: ["已结束", "Closed"]
    };
    return labels[status] ? t15(...labels[status]) : status;
  }
  function addStyles13() {
    if (document.getElementById(STYLE_ID15)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID15;
    style.textContent = `
    #${HEADER_TOOLS_ID}{display:flex;align-items:center;justify-content:center;gap:4px;width:max-content;max-width:100%;margin:2px auto 0}
    #${BUTTON_ID}{position:relative;display:flex;align-items:center;justify-content:center;gap:4px;width:auto;min-width:0;margin:0;padding:1px 6px;border:1px solid rgba(245,158,11,.55);border-radius:4px;background:rgba(245,158,11,.1);color:#ffc45b;font-size:10px;line-height:1.2;white-space:nowrap;cursor:pointer}.mwi-opinion-label{white-space:nowrap}
    #${BUTTON_ID}:hover{background:rgba(245,158,11,.19);color:#ffd887}#${BUTTON_ID}[data-unread="true"]{border-color:#ff6b6b;box-shadow:0 0 8px rgba(255,74,74,.62);animation:mwi-opinion-alert 1.4s ease-in-out infinite}.mwi-opinion-dot{position:absolute;right:-4px;top:-4px;width:9px;height:9px;border:2px solid #171b2a;border-radius:50%;background:#f04444;box-shadow:0 0 6px rgba(255,54,54,.9)}.mwi-opinion-dot[hidden]{display:none}@keyframes mwi-opinion-alert{0%,100%{filter:brightness(1)}50%{filter:brightness(1.32)}}
    #${ROOT_ID2}{position:fixed;inset:0;z-index:2147482600;display:flex;align-items:center;justify-content:center;overflow:hidden;overscroll-behavior:contain;padding:16px;background:rgba(4,6,12,.72);font-family:inherit;color:#e7e9f0}#${ROOT_ID2}[hidden]{display:none}
    .mwi-feedback-modal{display:flex;min-height:0;flex-direction:column;width:min(760px,100%);max-height:min(820px,calc(100vh - 32px));overflow:hidden;border:1px solid #45516f;border-radius:9px;background:#171b2a;box-shadow:0 20px 60px rgba(0,0,0,.65)}
    .mwi-feedback-head{display:flex;align-items:center;padding:12px 15px;border-bottom:1px solid #343c55;background:#1d2336}.mwi-feedback-head h2{margin:0;font-size:16px}.mwi-feedback-close{margin-left:auto;width:30px;height:30px;border:0;border-radius:5px;background:transparent;color:#aab1c4;font-size:20px;cursor:pointer}.mwi-feedback-close:hover{background:#303950;color:white}
    .mwi-feedback-tabs{display:flex;border-bottom:1px solid #343c55}.mwi-feedback-tab{position:relative;flex:1;padding:10px;border:0;background:#191e2e;color:#aeb6ca;cursor:pointer}.mwi-feedback-tab[data-active="true"]{background:#252d45;color:#ffc65b;font-weight:700}
    .mwi-feedback-body{min-height:0;flex:1 1 auto;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;touch-action:pan-y;padding:16px}.mwi-feedback-view[hidden]{display:none}.mwi-feedback-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.mwi-feedback-field{display:flex;min-width:0;flex-direction:column;gap:5px}.mwi-feedback-field.is-wide{grid-column:1/-1}.mwi-feedback-field span{font-size:12px;color:#c5cada}.mwi-feedback-field input,.mwi-feedback-field select,.mwi-feedback-field textarea,.mwi-feedback-reply textarea{width:100%;box-sizing:border-box;padding:8px;border:1px solid #434e6c;border-radius:5px;background:#101522;color:#eef0f6;font:inherit}.mwi-feedback-field textarea{min-height:105px;max-height:38vh;max-height:38dvh;field-sizing:content;overflow-y:auto;resize:vertical}.mwi-feedback-bug-fields{display:contents}.mwi-feedback-bug-fields[hidden]{display:none}
    .mwi-feedback-label-row{display:flex;align-items:center;gap:6px}.mwi-feedback-image-help{display:inline-flex;align-items:center;justify-content:center;width:17px;height:17px;border:1px solid #6f7c9d;border-radius:50%;color:#a9cfff;text-decoration:none;font:700 11px/1 sans-serif}.mwi-feedback-image-help:hover{border-color:#ffc45b;color:#ffc45b}.mwi-feedback-image-links textarea{min-height:76px}.mwi-feedback-field small{color:#8691aa;font-size:11px}.mwi-feedback-link-list{display:grid;gap:6px}.mwi-feedback-link-list a{overflow:hidden;color:#82b8ff;text-overflow:ellipsis;white-space:nowrap}
    .mwi-feedback-footer{display:flex;align-items:center;gap:10px;margin-top:13px}.mwi-feedback-quota{font-size:12px;color:#aeb5c7}.mwi-feedback-submit{margin-left:auto;padding:8px 17px;border:0;border-radius:5px;background:#d58b27;color:#17130c;font-weight:700;cursor:pointer}.mwi-feedback-submit:disabled{opacity:.48;cursor:not-allowed}.mwi-feedback-error{min-height:18px;margin-top:8px;color:#ff8f8f;font-size:12px}.mwi-feedback-error:empty{display:none}.mwi-feedback-success{color:#7ddc96}
    .mwi-feedback-list{display:grid;gap:8px}.mwi-feedback-card{padding:11px;border:1px solid #353f59;border-radius:6px;background:#131927;cursor:pointer}.mwi-feedback-card:hover{background:#1b2336}.mwi-feedback-card h3{margin:0 0 5px;font-size:13px}.mwi-feedback-card-meta{display:flex;gap:7px;align-items:center;color:#959fb8;font-size:11px}.mwi-feedback-status{padding:2px 6px;border-radius:4px;background:#55401c;color:#ffd06f}.mwi-feedback-status.processing{background:#193f58;color:#7ad9ff}.mwi-feedback-status.closed{background:#24452e;color:#84df9d}.mwi-feedback-empty{padding:35px;text-align:center;color:#8d97b0}.mwi-feedback-detail-back{margin-bottom:10px;border:0;background:transparent;color:#81b7ff;cursor:pointer}.mwi-feedback-detail h3{margin:0 0 5px}.mwi-feedback-copy{white-space:pre-wrap;word-break:break-word;line-height:1.5}.mwi-feedback-section{margin-top:12px;padding:11px;border:1px solid #343e58;border-radius:6px;background:#131825}.mwi-feedback-section h4{margin:0 0 7px;font-size:12px;color:#b8c0d3}.mwi-feedback-messages{display:grid;gap:7px}.mwi-feedback-message{padding:8px 10px;border-radius:5px;background:#20283b;border-left:3px solid #f1ae42}.mwi-feedback-message.admin{border-left-color:#68a8ff}.mwi-feedback-message time{display:block;margin-top:4px;color:#8993aa;font-size:10px}.mwi-feedback-actions{display:flex;gap:8px;margin-top:12px}.mwi-feedback-actions button{padding:7px 11px;border:1px solid #465273;border-radius:5px;background:#26314d;color:#e7ebf5;cursor:pointer}.mwi-feedback-reply{display:flex;gap:8px;margin-top:9px}.mwi-feedback-reply textarea{min-height:64px}.mwi-feedback-reply button{align-self:flex-end}.mwi-feedback-notice{margin-bottom:12px;padding:9px;border-radius:5px;background:rgba(64,127,199,.12);color:#b8d7fb;font-size:12px}.mwi-announcement-list{display:grid;gap:10px}.mwi-announcement-card{padding:14px;border:1px solid #3d4967;border-radius:7px;background:#131927}.mwi-announcement-card h3{margin:0;font-size:15px;color:#ffd071}.mwi-announcement-meta{margin-top:4px;color:#8993aa;font-size:11px}.mwi-announcement-card ul{margin:12px 0 0;padding-left:20px}.mwi-announcement-card li{margin:7px 0;color:#d8ddea;line-height:1.5}
    .mwi-announcement-card li strong{color:#ff5f66}
    @media(prefers-reduced-motion:reduce){#${BUTTON_ID}[data-unread="true"]{animation:none}}
    @media(max-width:620px){#${BUTTON_ID}{font-size:9px}#${ROOT_ID2}{align-items:flex-start;padding:6px}.mwi-feedback-modal{max-height:calc(100vh - 12px);max-height:calc(100dvh - 12px)}.mwi-feedback-head{flex:0 0 auto;padding:8px 10px}.mwi-feedback-head h2{font-size:14px}.mwi-feedback-tabs{flex:0 0 auto}.mwi-feedback-tab{min-width:0;padding:8px 4px;font-size:12px}.mwi-feedback-body{min-height:0;padding:9px}.mwi-feedback-notice{margin-bottom:8px;padding:7px}.mwi-feedback-grid{grid-template-columns:1fr;gap:8px}.mwi-feedback-field{gap:3px}.mwi-feedback-field.is-wide{grid-column:1}.mwi-feedback-field textarea{min-height:64px;max-height:28vh;max-height:28dvh}.mwi-feedback-field textarea[name="detail"]{min-height:82px}.mwi-feedback-image-links textarea{min-height:52px}.mwi-feedback-footer{margin-top:9px}.mwi-feedback-reply{flex-direction:column}.mwi-announcement-card{padding:10px}.mwi-announcement-card ul{margin-top:8px;padding-left:18px}.mwi-announcement-card li{margin:5px 0;line-height:1.4}}
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function formatTime(value) {
    try {
      return new Date(value).toLocaleString();
    } catch {
      return String(value ?? "");
    }
  }
  function makeElement(tag, className, text) {
    const element = document.createElement(tag);
    if (className) element.className = className;
    if (text !== void 0) element.textContent = text;
    return element;
  }
  var OpinionCenterPanel = class {
    constructor({ client, scope, announcements = new AnnouncementStore() }) {
      this.client = client;
      this.scope = scope;
      this.announcements = announcements;
      this.items = [];
      this.feedbackUnread = 0;
      this.announcementUnread = this.announcements.unread().length;
      this.acknowledgedFeedbackIds = /* @__PURE__ */ new Set();
      this.pendingReadIds = /* @__PURE__ */ new Set();
      this.currentDetailId = null;
      this.quota = null;
      this.editing = null;
      this.build();
    }
    build() {
      addStyles13();
      this.root = document.createElement("div");
      this.root.id = ROOT_ID2;
      this.root.hidden = true;
      this.root.innerHTML = `
      <section class="mwi-feedback-modal" role="dialog" aria-modal="true" aria-label="${t15("MWITools 意见中心", "MWITools Feedback Center")}">
        <header class="mwi-feedback-head"><h2>${t15("MWITools 意见中心", "MWITools Feedback Center")}</h2><button type="button" class="mwi-feedback-close" aria-label="${t15("关闭", "Close")}">×</button></header>
        <nav class="mwi-feedback-tabs"><button type="button" class="mwi-feedback-tab" data-tab="announcements" data-active="false">${t15("公告", "Announcements")}</button><button type="button" class="mwi-feedback-tab" data-tab="submit" data-active="true">${t15("提交反馈", "Submit")}</button><button type="button" class="mwi-feedback-tab" data-tab="mine" data-active="false">${t15("我的反馈", "My feedback")}</button></nav>
        <div class="mwi-feedback-body">
          <section class="mwi-feedback-view" data-view="announcements" hidden><div class="mwi-announcement-list"></div></section>
          <section class="mwi-feedback-view" data-view="submit"><div class="mwi-feedback-notice">${t15("每个角色每个 UTC+8 自然周最多提交 2 条;编辑和留言不占额度。不会采集聊天、游戏消息正文或凭证。", "Up to 2 new reports per character each UTC+8 week. Edits and messages do not use quota. Chats, game message bodies, and credentials are never collected.")}</div>
            <form class="mwi-feedback-form"><div class="mwi-feedback-grid">
              <label class="mwi-feedback-field"><span>${t15("类型", "Type")}</span><select name="type"><option value="bug">Bug</option><option value="feature">${t15("功能建议", "Feature request")}</option><option value="other">${t15("其他", "Other")}</option></select></label>
              <label class="mwi-feedback-field"><span>${t15("标题", "Title")}</span><input name="title" maxlength="160" required></label>
              <label class="mwi-feedback-field is-wide"><span>${t15("详细说明", "Details")}</span><textarea name="detail" maxlength="12000" required></textarea></label>
              <div class="mwi-feedback-bug-fields"><label class="mwi-feedback-field is-wide"><span>${t15("复现步骤", "Steps to reproduce")}</span><textarea name="reproduction" maxlength="8000"></textarea></label><label class="mwi-feedback-field is-wide"><span>${t15("预期结果", "Expected result")}</span><textarea name="expected" maxlength="8000"></textarea></label></div>
              <label class="mwi-feedback-field is-wide mwi-feedback-image-links"><span class="mwi-feedback-label-row"><span>${t15("图片链接(每行一个,最多 3 个)", "Image links (one per line, up to 3)")}</span><a class="mwi-feedback-image-help" href="https://tupian.li" target="_blank" rel="noopener noreferrer" title="${t15("不知道图床?打开 tupian.li", "Need image hosting? Open tupian.li")}">?</a></span><textarea name="imageLinks" maxlength="6002" placeholder="https://..."></textarea><small>${t15("服务器不会上传、下载或代理图片,只保存你填写的链接。", "The server only stores your links; it never uploads, downloads, or proxies images.")}</small></label>
            </div><div class="mwi-feedback-footer"><span class="mwi-feedback-quota">${t15("正在查询本周额度…", "Checking weekly quota…")}</span><button type="submit" class="mwi-feedback-submit">${t15("提交", "Submit")}</button></div><div class="mwi-feedback-error"></div></form>
          </section>
          <section class="mwi-feedback-view" data-view="mine" hidden><div class="mwi-feedback-list"></div><div class="mwi-feedback-detail" hidden></div><div class="mwi-feedback-error"></div></section>
        </div>
      </section>`;
      document.body.appendChild(this.root);
      this.form = this.root.querySelector(".mwi-feedback-form");
      this.scope.event(
        this.root.querySelector(".mwi-feedback-close"),
        "click",
        () => this.close()
      );
      this.scope.event(this.root, "click", (event) => {
        if (event.target === this.root) this.close();
      });
      this.root.querySelectorAll("[data-tab]").forEach(
        (button) => this.scope.event(
          button,
          "click",
          () => this.showTab(button.dataset.tab)
        )
      );
      this.scope.event(
        this.form.elements.type,
        "change",
        () => this.toggleBugFields()
      );
      this.scope.event(this.form, "submit", (event) => this.submit(event));
      this.scope.event(document, "keydown", (event) => {
        if (event.key === "Escape" && !this.root.hidden) this.close();
      });
      this.scope.add(() => this.destroy());
      this.toggleBugFields();
      this.renderAnnouncements();
    }
    ensureButton() {
      const host = ensureHeaderToolsHost();
      if (!host) return null;
      let button = document.getElementById(BUTTON_ID);
      if (!button) {
        button = document.createElement("button");
        button.type = "button";
        button.id = BUTTON_ID;
        const icon = makeElement("span", "", "✉");
        const label = makeElement("span", "mwi-opinion-label");
        const dot = makeElement("span", "mwi-opinion-dot");
        dot.hidden = true;
        button.append(icon, label, dot);
        this.scope.event(button, "click", () => this.open());
      }
      button.querySelector(".mwi-opinion-label").textContent = "MWITools";
      if (button.parentElement !== host || button !== host.lastElementChild) {
        host.append(button);
      }
      this.updateUnreadIndicator();
      return button;
    }
    findTotalLevel() {
      return findHeaderTotalLevel();
    }
    setUnread(count) {
      this.feedbackUnread = Math.max(0, Number(count) || 0);
      this.updateUnreadIndicator();
    }
    updateUnreadIndicator() {
      const button = document.getElementById(BUTTON_ID);
      if (!button) return;
      const hasUnread = this.feedbackUnread > 0 || this.announcementUnread > 0;
      button.dataset.unread = String(hasUnread);
      const dot = button.querySelector(".mwi-opinion-dot");
      if (dot) dot.hidden = !hasUnread;
      button.setAttribute(
        "aria-label",
        hasUnread ? t15(
          "MWITools 意见中心,有新内容",
          "MWITools Feedback Center, new activity"
        ) : t15("MWITools 意见中心", "MWITools Feedback Center")
      );
    }
    async open() {
      const hadAnnouncementUnread = this.announcementUnread > 0;
      const hadFeedbackUnread = this.feedbackUnread > 0;
      if (this.root.hidden) {
        this.previousBodyOverflow = document.body.style.overflow;
      }
      document.body.style.overflow = "hidden";
      this.root.hidden = false;
      this.showTab(
        hadAnnouncementUnread ? "announcements" : hadFeedbackUnread ? "mine" : "submit"
      );
      this.announcements.markAllRead();
      this.announcementUnread = 0;
      this.feedbackUnread = 0;
      this.updateUnreadIndicator();
      await this.refresh({ keepIndicatorClear: true });
      const discoveredFeedbackUnread = this.feedbackUnread > 0;
      if (!hadAnnouncementUnread && !hadFeedbackUnread && discoveredFeedbackUnread) {
        this.showTab("mine");
      }
      const unreadItems = this.items.filter(
        (item) => item.unread && !this.acknowledgedFeedbackIds.has(item.id)
      );
      this.acknowledgeFeedback(unreadItems);
      this.feedbackUnread = 0;
      this.updateUnreadIndicator();
    }
    close() {
      this.root.hidden = true;
      document.body.style.overflow = this.previousBodyOverflow ?? "";
      this.previousBodyOverflow = null;
    }
    showTab(name) {
      this.root.querySelectorAll("[data-tab]").forEach((button) => {
        button.dataset.active = String(button.dataset.tab === name);
      });
      this.root.querySelectorAll("[data-view]").forEach((view) => {
        view.hidden = view.dataset.view !== name;
      });
      if (name === "mine") this.renderList();
      if (name === "announcements") this.renderAnnouncements();
    }
    toggleBugFields() {
      this.form.querySelector(".mwi-feedback-bug-fields").hidden = this.form.elements.type.value !== "bug";
    }
    formValue() {
      return {
        type: this.form.elements.type.value,
        title: this.form.elements.title.value.trim(),
        detail: this.form.elements.detail.value.trim(),
        reproduction: this.form.elements.reproduction.value.trim(),
        expected: this.form.elements.expected.value.trim(),
        imageLinks: normalizeImageLinks(this.form.elements.imageLinks.value),
        context: feedbackContext(this.client)
      };
    }
    async submit(event) {
      event.preventDefault();
      const error = this.form.querySelector(".mwi-feedback-error");
      const button = this.form.querySelector(".mwi-feedback-submit");
      button.disabled = true;
      error.classList.remove("mwi-feedback-success");
      error.textContent = t15("正在提交…", "Submitting…");
      try {
        const value = this.formValue();
        if (!value.title || !value.detail) {
          throw new Error(
            t15("请填写标题和详细说明。", "Enter a title and details.")
          );
        }
        const editingId = this.editing?.id ?? null;
        const saved = editingId ? await this.client.edit(editingId, value) : await this.client.submit(value);
        if (!editingId && this.quota) {
          this.quota = {
            ...this.quota,
            remaining: Math.max(0, Number(this.quota.remaining) - 1)
          };
        }
        if (saved?.id) {
          this.items = [
            saved,
            ...this.items.filter((item) => item.id !== saved.id)
          ];
        }
        this.resetForm();
        this.renderQuota();
        error.classList.add("mwi-feedback-success");
        error.textContent = t15("已保存反馈。", "Feedback saved.");
        this.showTab("mine");
        void this.refresh();
      } catch (caught) {
        error.classList.remove("mwi-feedback-success");
        error.textContent = caught.message;
      } finally {
        button.disabled = !this.editing && this.quota?.remaining === 0;
      }
    }
    resetForm() {
      this.form.reset();
      this.editing = null;
      this.form.querySelector(".mwi-feedback-submit").textContent = t15(
        "提交",
        "Submit"
      );
      this.toggleBugFields();
    }
    unreadFeedbackCount(result) {
      const unreadItems = this.items.filter((item) => item.unread);
      const acknowledged = unreadItems.filter(
        (item) => this.acknowledgedFeedbackIds.has(item.id)
      ).length;
      return Math.max(
        0,
        Number(result.unread ?? unreadItems.length) - acknowledged
      );
    }
    acknowledgeFeedback(items) {
      for (const item of items) {
        if (!item?.id || this.pendingReadIds.has(item.id)) continue;
        this.acknowledgedFeedbackIds.add(item.id);
        this.pendingReadIds.add(item.id);
        Promise.resolve().then(() => this.client.markRead(item.id)).then(() => {
          this.pendingReadIds.delete(item.id);
          this.acknowledgedFeedbackIds.delete(item.id);
          item.unread = false;
        }).catch(() => {
        });
      }
    }
    retryPendingReads() {
      for (const id of [...this.pendingReadIds]) {
        Promise.resolve().then(() => this.client.markRead(id)).then(() => {
          this.pendingReadIds.delete(id);
          this.acknowledgedFeedbackIds.delete(id);
          const item = this.items.find((candidate) => candidate.id === id);
          if (item) item.unread = false;
        }).catch(() => {
        });
      }
    }
    async refresh({ keepIndicatorClear = false } = {}) {
      try {
        const result = await this.client.list();
        this.items = result.items ?? [];
        this.quota = result.quota;
        this.feedbackUnread = this.unreadFeedbackCount(result);
        if (!keepIndicatorClear) this.updateUnreadIndicator();
        this.renderQuota();
        this.root.querySelectorAll(".mwi-feedback-error").forEach((node) => {
          if (!node.classList.contains("mwi-feedback-success"))
            node.textContent = "";
        });
        if (this.currentDetailId && !this.root.hidden) {
          await this.openDetail(this.currentDetailId);
        } else {
          this.renderList();
        }
        this.retryPendingReads();
        return true;
      } catch (error) {
        this.renderQuota(error.message);
        this.root.querySelector(
          '[data-view="mine"] .mwi-feedback-error'
        ).textContent = error.message;
        return false;
      }
    }
    renderAnnouncements() {
      const host = this.root.querySelector(".mwi-announcement-list");
      if (!host) return;
      host.replaceChildren();
      const announcements = this.announcements.list();
      if (!announcements.length) {
        host.append(
          makeElement(
            "div",
            "mwi-feedback-empty",
            t15("目前还没有公告。", "There are no announcements yet.")
          )
        );
        return;
      }
      for (const item of announcements) {
        const card = makeElement("article", "mwi-announcement-card");
        const title = makeElement(
          "h3",
          "",
          item.title?.[runtime.config.isZH ? "zh" : "en"] ?? item.version
        );
        const meta = makeElement(
          "div",
          "mwi-announcement-meta",
          `${item.version} · ${item.publishedAt}`
        );
        const list = document.createElement("ul");
        const body = item.body?.[runtime.config.isZH ? "zh" : "en"] ?? [];
        const emphasizedIndexes = new Set(item.emphasizedBodyIndexes ?? []);
        list.append(
          ...body.map((line, index) => {
            const listItem = makeElement("li");
            if (!emphasizedIndexes.has(index)) {
              listItem.textContent = line;
              return listItem;
            }
            const strong = document.createElement("strong");
            const underline = document.createElement("u");
            underline.textContent = line;
            strong.append(underline);
            listItem.append(strong);
            return listItem;
          })
        );
        card.append(title, meta, list);
        host.append(card);
      }
    }
    renderQuota(errorMessage = "") {
      const node = this.form.querySelector(".mwi-feedback-quota");
      node.textContent = errorMessage ? t15(
        `额度查询失败:${errorMessage}`,
        `Quota check failed: ${errorMessage}`
      ) : this.quota ? t15(
        `本周剩余 ${this.quota.remaining}/${this.quota.limit} 条`,
        `${this.quota.remaining}/${this.quota.limit} submissions left this week`
      ) : t15("额度暂时不可用", "Quota unavailable");
      this.form.querySelector(".mwi-feedback-submit").disabled = !this.editing && this.quota?.remaining === 0;
    }
    renderList() {
      this.currentDetailId = null;
      const host = this.root.querySelector(".mwi-feedback-list");
      const detail = this.root.querySelector(".mwi-feedback-detail");
      detail.hidden = true;
      host.hidden = false;
      host.replaceChildren();
      if (!this.items.length) {
        host.append(
          makeElement(
            "div",
            "mwi-feedback-empty",
            t15("还没有提交过反馈。", "No feedback yet.")
          )
        );
        return;
      }
      for (const item of this.items) {
        const card = makeElement("article", "mwi-feedback-card");
        const title = makeElement("h3", "", item.title);
        const meta = makeElement("div", "mwi-feedback-card-meta");
        const status = makeElement(
          "span",
          `mwi-feedback-status ${item.status}`,
          statusLabel(item.status)
        );
        meta.append(status, document.createTextNode(formatTime(item.updatedAt)));
        card.append(title, meta);
        card.addEventListener("click", () => this.openDetail(item.id), {
          once: true
        });
        host.append(card);
      }
    }
    async openDetail(id) {
      const host = this.root.querySelector(".mwi-feedback-list");
      const detail = this.root.querySelector(".mwi-feedback-detail");
      try {
        const item = await this.client.detail(id);
        this.currentDetailId = id;
        host.hidden = true;
        detail.hidden = false;
        detail.replaceChildren();
        const back = makeElement(
          "button",
          "mwi-feedback-detail-back",
          `← ${t15("返回列表", "Back")}`
        );
        back.type = "button";
        back.addEventListener("click", () => this.renderList(), { once: true });
        const title = makeElement("h3", "", item.title);
        const meta = makeElement(
          "div",
          "mwi-feedback-card-meta",
          `${statusLabel(item.status)} · ${formatTime(item.updatedAt)}`
        );
        detail.append(
          back,
          title,
          meta,
          this.textSection(t15("详细说明", "Details"), item.detail)
        );
        if (item.type === "bug") {
          detail.append(
            this.textSection(
              t15("复现步骤", "Steps to reproduce"),
              item.reproduction || "—"
            ),
            this.textSection(
              t15("预期结果", "Expected result"),
              item.expected || "—"
            )
          );
        }
        if (item.imageLinks?.length) {
          const section = makeElement("section", "mwi-feedback-section");
          section.append(makeElement("h4", "", t15("图片链接", "Image links")));
          const links = makeElement("div", "mwi-feedback-link-list");
          for (const [index, url] of item.imageLinks.entries()) {
            const link = makeElement(
              "a",
              "",
              `${t15("图片", "Image")} ${index + 1}:${url}`
            );
            link.href = url;
            link.target = "_blank";
            link.rel = "noopener noreferrer";
            links.append(link);
          }
          section.append(links);
          detail.append(section);
        }
        const messages = makeElement("section", "mwi-feedback-section");
        messages.append(makeElement("h4", "", t15("留言", "Messages")));
        const messageList = makeElement("div", "mwi-feedback-messages");
        for (const message of item.messages ?? []) {
          const box = makeElement("div", `mwi-feedback-message ${message.actor}`);
          box.append(
            makeElement(
              "strong",
              "",
              message.actor === "admin" ? t15("管理员", "Admin") : t15("我", "Me")
            ),
            makeElement("div", "mwi-feedback-copy", message.body),
            makeElement("time", "", formatTime(message.createdAt))
          );
          messageList.append(box);
        }
        if (!messageList.childElementCount) {
          messageList.append(
            makeElement(
              "div",
              "mwi-feedback-card-meta",
              t15("暂无留言", "No messages")
            )
          );
        }
        messages.append(messageList);
        detail.append(messages);
        if (item.status !== "closed") {
          const actions = makeElement("div", "mwi-feedback-actions");
          const edit = makeElement("button", "", t15("修改反馈", "Edit feedback"));
          edit.type = "button";
          edit.addEventListener("click", () => this.startEdit(item), {
            once: true
          });
          actions.append(edit);
          detail.append(actions);
          const reply = makeElement("div", "mwi-feedback-reply");
          const input = document.createElement("textarea");
          input.placeholder = t15("补充留言…", "Add a message…");
          input.maxLength = 8e3;
          const send = makeElement("button", "", t15("发送", "Send"));
          send.type = "button";
          send.addEventListener("click", async () => {
            if (!input.value.trim()) return;
            send.disabled = true;
            try {
              await this.client.reply(item.id, input.value.trim());
              await this.openDetail(item.id);
            } catch (error) {
              send.disabled = false;
              input.setCustomValidity(error.message);
              input.reportValidity();
            }
          });
          reply.append(input, send);
          detail.append(reply);
        } else {
          detail.append(
            makeElement(
              "div",
              "mwi-feedback-notice",
              t15(
                "该反馈已结束,内容和留言已锁定。",
                "This feedback is closed and locked."
              )
            )
          );
        }
        await this.client.markRead(item.id).catch(() => {
        });
        if (item.unread) this.setUnread(this.unread - 1);
      } catch (error) {
        detail.hidden = false;
        detail.replaceChildren(
          makeElement("div", "mwi-feedback-error", error.message)
        );
      }
    }
    textSection(label, value) {
      const section = makeElement("section", "mwi-feedback-section");
      section.append(
        makeElement("h4", "", label),
        makeElement("div", "mwi-feedback-copy", value)
      );
      return section;
    }
    startEdit(item) {
      this.editing = { ...item };
      for (const name of [
        "type",
        "title",
        "detail",
        "reproduction",
        "expected"
      ]) {
        this.form.elements[name].value = item[name] ?? "";
      }
      this.form.elements.imageLinks.value = (item.imageLinks ?? []).join("\n");
      this.form.querySelector(".mwi-feedback-submit").textContent = t15(
        "保存修改",
        "Save changes"
      );
      this.toggleBugFields();
      this.renderQuota();
      this.showTab("submit");
    }
    destroy() {
      if (!this.root?.hidden) this.close();
      document.getElementById(BUTTON_ID)?.remove();
      removeHeaderToolsHostIfEmpty();
      this.root?.remove();
      document.getElementById(STYLE_ID15)?.remove();
    }
  };
  var feedbackUiIds = { ROOT_ID: ROOT_ID2, BUTTON_ID, STYLE_ID: STYLE_ID15 };
  var FeedbackPanel = OpinionCenterPanel;

  // src/features/opinion-center/index.js
  var activeClient = null;
  runtime.features.register({
    id: "feedback",
    setting: "feedback",
    scope: "character",
    initialize({ scope, characterId }) {
      const client = new FeedbackClient({
        characterId,
        characterName: runtime.state.currentCharacterName
      });
      activeClient = client;
      const panel = new OpinionCenterPanel({
        client,
        scope,
        announcements: new AnnouncementStore()
      });
      let disposed = false;
      let failures = 0;
      let timer = null;
      const schedule = (delay) => {
        clearTimeout(timer);
        timer = setTimeout(async () => {
          if (disposed) return;
          const ok = await panel.refresh();
          failures = ok ? 0 : Math.min(failures + 1, 6);
          schedule(ok ? 6e4 : Math.min(15 * 6e4, 6e4 * 2 ** failures));
        }, delay);
      };
      const ensure = () => panel.ensureButton();
      ensure();
      const ensureScheduler = createFrameScheduler(ensure);
      subscribeMutationChannel(
        {
          name: "header-mount",
          target: document.body,
          options: { childList: true, subtree: true },
          scope
        },
        (records) => {
          const relevant = records.some(
            (record) => [...record.addedNodes, ...record.removedNodes].some(
              (node) => node?.nodeType === 1 && !node.matches?.(
                "#mwitools-feedback-root,#mwitools-feedback-button"
              ) && (node.matches?.(
                '[class*="Header_totalLevel"],[class*="totalLevel"]'
              ) || node.querySelector?.(
                '[class*="Header_totalLevel"],[class*="totalLevel"]'
              ))
            )
          );
          if (relevant) ensureScheduler.schedule();
        }
      );
      schedule(1500);
      scope.add(() => {
        ensureScheduler.cancel();
        disposed = true;
        clearTimeout(timer);
        if (activeClient === client) activeClient = null;
      });
    }
  });
  runtime.api.feedback = {
    submit: (...args) => activeClient?.submit(...args),
    list: (...args) => activeClient?.list(...args),
    detail: (...args) => activeClient?.detail(...args),
    edit: (...args) => activeClient?.edit(...args),
    reply: (...args) => activeClient?.reply(...args),
    markRead: (...args) => activeClient?.markRead(...args),
    quota: (...args) => activeClient?.quota(...args)
  };

  // src/features/guild-xp.js
  var STYLE_ID16 = "mwitools-guild-xp-style";
  var rateCache = /* @__PURE__ */ new Map();
  var HOUR_MS2 = 60 * 60 * 1e3;
  var TREND_WINDOW_MS = 7 * 24 * HOUR_MS2;
  var TREND_RATE_WINDOW_MS = 6 * HOUR_MS2;
  var TREND_MINIMUM_COVERAGE_MS = HOUR_MS2;
  var GUILD_SURFACE_SELECTOR = '[class*="Guild"],[class*="Leaderboard"]';
  var OWNED_GUILD_SELECTOR = ".mwi-guild-xp-card,.mwi-guild-rate-cell,.mwi-guild-div-rates,.mwi-guild-div-rate-head,.mwi-guild-idle";
  function observeGuildSurface(scope, render) {
    const scheduler = createFrameScheduler(render);
    subscribeMutationChannel(
      {
        name: "guild-surface",
        target: document.body,
        options: { childList: true, subtree: true },
        scope
      },
      (records) => {
        const relevant = records.some((record) => {
          const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
          const changed = [...record.addedNodes, ...record.removedNodes].filter(
            (node) => node?.nodeType === 1
          );
          if (target?.closest?.(OWNED_GUILD_SELECTOR) || changed.length && changed.every(
            (node) => node.matches?.(OWNED_GUILD_SELECTOR) || node.closest?.(OWNED_GUILD_SELECTOR)
          )) {
            return false;
          }
          if (target?.closest?.(GUILD_SURFACE_SELECTOR)) return true;
          return changed.some(
            (node) => node.matches?.(GUILD_SURFACE_SELECTOR) || node.querySelector?.(GUILD_SURFACE_SELECTOR)
          );
        });
        if (relevant) scheduler.schedule();
      }
    );
    scope.add(() => scheduler.cancel());
  }
  function t16(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function findField(object, keys, maxDepth = 4) {
    const pending = [{ value: object, depth: 0 }];
    const visited = /* @__PURE__ */ new Set();
    while (pending.length) {
      const { value, depth } = pending.shift();
      if (!value || typeof value !== "object" || visited.has(value) || depth > maxDepth)
        continue;
      visited.add(value);
      for (const key of keys) {
        if (value[key] !== void 0 && value[key] !== null) return value[key];
      }
      for (const child of Object.values(value)) {
        if (child && typeof child === "object")
          pending.push({ value: child, depth: depth + 1 });
      }
    }
    return null;
  }
  function findOwnField(object, keys) {
    if (!object || typeof object !== "object") {
      return { found: false, value: void 0 };
    }
    for (const key of keys) {
      if (Object.prototype.hasOwnProperty.call(object, key)) {
        return { found: true, value: object[key] };
      }
    }
    return { found: false, value: void 0 };
  }
  function findOwnFieldDeep(object, keys, maxDepth = 4) {
    const pending = [{ value: object, depth: 0 }];
    const visited = /* @__PURE__ */ new Set();
    while (pending.length) {
      const { value, depth } = pending.shift();
      if (!value || typeof value !== "object" || visited.has(value) || depth > maxDepth) {
        continue;
      }
      visited.add(value);
      const own = findOwnField(value, keys);
      if (own.found) return own;
      for (const child of Object.values(value)) {
        if (child && typeof child === "object") {
          pending.push({ value: child, depth: depth + 1 });
        }
      }
    }
    return { found: false, value: void 0 };
  }
  function isGuildMemberIdle(member) {
    const hidden = findOwnFieldDeep(member, [
      "hideOnlineStatus",
      "isOnlineHidden",
      "onlineStatusHidden"
    ]);
    const online = findOwnFieldDeep(member, ["isOnline", "online"]);
    if (hidden.found && (hidden.value === true || hidden.value === 1 || /^(?:true|hidden)$/i.test(String(hidden.value ?? "").trim()))) {
      return false;
    }
    if (online.found && (online.value === false || online.value === 0 || /^(?:false|offline)$/i.test(String(online.value ?? "").trim()))) {
      return false;
    }
    let action = findOwnFieldDeep(member, [
      "actionType",
      "currentActionType",
      "currentActionHrid",
      "actionHrid"
    ]);
    if (!action.found) {
      const currentAction = findOwnFieldDeep(member, ["currentAction"]);
      if (currentAction.found && (currentAction.value === null || currentAction.value === false)) {
        action = currentAction;
      }
    }
    if (!action.found) return false;
    if (action.value === null || action.value === false) return true;
    if (typeof action.value === "string") {
      const value = action.value.trim();
      return value === "" || /(?:^|\/)idle$/i.test(value);
    }
    return false;
  }
  function entityId(entity) {
    return String(
      findField(entity, [
        "id",
        "characterID",
        "characterId",
        "guildID",
        "guildId",
        "name"
      ]) ?? ""
    );
  }
  function entityName2(entity) {
    return String(
      findField(entity, ["name", "guildName", "characterName"]) ?? "—"
    );
  }
  function entityXp(entity) {
    const value = findField(entity, [
      "guildExperience",
      "totalGuildExperience",
      "cumulativeGuildExperience",
      "experience",
      "totalExperience",
      "xp"
    ]);
    return Number.isFinite(Number(value)) ? Number(value) : null;
  }
  function entityWeeklyXpRate(entity, now = Date.now()) {
    const weeklyValue = findField(entity, ["weeklyGuildExperience"]);
    const weeklyXp = weeklyValue === null ? NaN : Number(weeklyValue);
    const weekStartedAt = Date.parse(
      findField(entity, ["weeklyGuildExperienceWeekStartAt"]) ?? ""
    );
    const elapsed = now - weekStartedAt;
    if (!Number.isFinite(weeklyXp) || weeklyXp < 0 || !Number.isFinite(weekStartedAt) || elapsed <= 0) {
      return null;
    }
    return weeklyXp / elapsed * HOUR_MS2;
  }
  function objectKey(kind, entity, parentId = "") {
    const id = entityId(entity);
    return id ? `${kind}:${parentId ? `${parentId}:` : ""}${id}` : "";
  }
  async function refreshRate(key) {
    if (!key) return null;
    const history = await runtime.api.getXpHistory(key);
    const rates = runtime.api.calculateXpRates(history);
    rateCache.set(key, rates);
    return rates;
  }
  async function sampleEntity(kind, entity, parentId = "", at = Date.now()) {
    const key = objectKey(kind, entity, parentId);
    const xp = entityXp(entity);
    if (!key || xp === null) return null;
    await runtime.api.recordXpSnapshot(key, xp, at);
    return refreshRate(key);
  }
  async function sampleGuildState(includeLeaderboard = false) {
    const now = Date.now();
    const guild2 = runtime.state.guild;
    const guildId = entityId(guild2);
    if (guild2) await sampleEntity("guild", guild2, "", now);
    await Promise.all(
      (runtime.state.guildCharacters ?? []).map(
        (member) => sampleEntity("member", member, guildId, now)
      )
    );
    if (includeLeaderboard) {
      await Promise.all(
        (runtime.state.guildLeaderboard ?? []).map(
          (row) => sampleEntity("leaderboard", row, "", now)
        )
      );
    }
  }
  function addStyles14() {
    if (document.getElementById(STYLE_ID16)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID16;
    style.textContent = `
    .mwi-guild-xp-card { margin:10px 0; padding:11px 12px; border:1px solid rgba(255,255,255,.13); border-radius:8px; background:linear-gradient(135deg,rgba(255,255,255,.05),rgba(0,0,0,.17)); color:var(--color-text-primary,#eee); }
    .mwi-guild-xp-head { display:flex; justify-content:space-between; gap:12px; align-items:baseline; }
    .mwi-guild-xp-title { font-weight:700; font-size:.95rem; }
    .mwi-guild-xp-sampled { color:var(--color-text-secondary,#999); font-size:.66rem; }
    .mwi-guild-xp-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(125px,1fr)); gap:7px; margin-top:8px; }
    .mwi-guild-xp-metric { padding:7px 8px; border-radius:5px; background:rgba(0,0,0,.18); }
    .mwi-guild-xp-metric small { display:block; color:var(--color-text-secondary,#aaa); }
    .mwi-guild-xp-metric strong { display:block; margin-top:2px; color:#ffa500; }
    .mwi-guild-trend-label { margin-top:8px; color:var(--color-text-secondary,#aaa); font-size:.68rem; }
    .mwi-guild-trend { width:100%; height:180px; margin-top:8px; overflow:visible; }
    .mwi-guild-trend-axis { stroke:rgba(255,255,255,.38); stroke-width:1; vector-effect:non-scaling-stroke; }
    .mwi-guild-trend-grid { stroke:rgba(255,255,255,.1); stroke-width:1; vector-effect:non-scaling-stroke; }
    .mwi-guild-trend-tick { fill:var(--color-text-secondary,#aaa); font-size:10px; }
    .mwi-guild-trend-empty { fill:var(--color-text-secondary,#aaa); font-size:13px; text-anchor:middle; }
    .mwi-guild-trend polyline { fill:none; stroke:#ffa500; stroke-width:2; vector-effect:non-scaling-stroke; }
    .mwi-guild-idle { display:flex; flex-wrap:wrap; gap:5px; align-items:center; margin-top:8px; }
    .mwi-guild-idle span { padding:2px 7px; border-radius:999px; background:rgba(255,255,255,.07); font-size:.68rem; }
    .mwi-guild-members-wide { width:100% !important; max-width:none !important; min-width:0 !important; }
    .mwi-guild-member-table-wrap { width:100%; max-width:100%; overflow-x:auto; overscroll-behavior-x:contain; }
    .mwi-guild-members-wide .mwi-guild-member-table { width:max-content !important; min-width:100% !important; table-layout:auto !important; }
    .mwi-guild-member-table > thead > tr > th { white-space:nowrap; word-break:keep-all; }
    .mwi-guild-member-table > tbody > tr > td:not(:first-child) { white-space:nowrap; word-break:keep-all; }
    .mwi-guild-member-table > thead > tr > th:nth-child(2),
    .mwi-guild-member-table > thead > tr > th:nth-child(3),
    .mwi-guild-member-table > thead > tr > th:nth-child(4),
    .mwi-guild-member-table > tbody > tr > td:nth-child(2),
    .mwi-guild-member-table > tbody > tr > td:nth-child(3),
    .mwi-guild-member-table > tbody > tr > td:nth-child(4) { min-width:38px; }
    .mwi-guild-member-table > thead > tr > th:nth-child(5),
    .mwi-guild-member-table > tbody > tr > td:nth-child(5) { min-width:96px; }
    .mwi-guild-rate-cell { color:#ffa500; white-space:nowrap; min-width:105px; }
    .mwi-guild-rate-content { display:flex; align-items:center; gap:5px; }
    .mwi-guild-rate-value { flex:0 0 auto; }
    .mwi-guild-rate-track { display:block; flex:1 1 42px; min-width:24px; max-width:68px; height:5px; overflow:hidden; border-radius:999px; background:rgba(255,255,255,.08); }
    .mwi-guild-rate-fill { display:block; height:100%; min-width:2px; border-radius:inherit; background:rgba(91,134,255,.58); }
    .mwi-guild-rate-sort { margin-left:4px; color:var(--color-text-secondary,#aaa); font-size:.62rem; }
    .mwi-guild-div-rate-head,.mwi-guild-div-rates { display:grid; grid-template-columns:repeat(2,minmax(92px,1fr)); gap:8px; margin-left:auto; text-align:right; }
    .mwi-guild-div-rate-head { padding:5px 8px; color:var(--color-text-secondary,#aaa); font-size:.68rem; }
    .mwi-guild-div-rates { padding-left:10px; color:#ffa500; font-size:.7rem; }
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function rateText(value, waiting = false) {
    if (!Number.isFinite(value))
      return waiting ? t16("待再次采样", "Awaiting another sample") : t16("样本不足", "Not enough data");
    return `${runtime.api.numberFormatter(value)}/h`;
  }
  function metric2(label, value, title = "") {
    const box = document.createElement("div");
    box.className = "mwi-guild-xp-metric";
    const caption = document.createElement("small");
    caption.textContent = label;
    const strong = document.createElement("strong");
    if (value?.nodeType) strong.append(value);
    else strong.textContent = value;
    strong.title = title;
    box.append(caption, strong);
    return box;
  }
  function guildXpRatePoints(points, now = Date.now()) {
    const cutoff = now - TREND_WINDOW_MS;
    const sorted = [...points].map((point) => ({ at: Number(point?.at), xp: Number(point?.xp) })).filter((point) => Number.isFinite(point.at) && Number.isFinite(point.xp)).sort((left, right) => left.at - right.at);
    const rates = [];
    for (let index = 1; index < sorted.length; index += 1) {
      const current = sorted[index];
      if (current.at < cutoff) continue;
      let baselineIndex = index - 1;
      while (baselineIndex > 0 && current.at - sorted[baselineIndex - 1].at <= TREND_RATE_WINDOW_MS) {
        baselineIndex -= 1;
      }
      let baseline = sorted[baselineIndex];
      if (current.at - baseline.at < TREND_MINIMUM_COVERAGE_MS) {
        baseline = [...sorted.slice(0, baselineIndex)].reverse().find((point) => current.at - point.at >= TREND_MINIMUM_COVERAGE_MS);
      }
      if (!baseline) continue;
      const elapsed = current.at - baseline.at;
      const gained = current.xp - baseline.xp;
      if (elapsed <= 0 || gained < 0) continue;
      rates.push({ at: current.at, rate: gained / elapsed * HOUR_MS2 });
    }
    return rates;
  }
  function svgElement(name, className = "") {
    const element = document.createElementNS("http://www.w3.org/2000/svg", name);
    if (className) element.setAttribute("class", className);
    return element;
  }
  function niceRateCeiling(value) {
    if (!(value > 0)) return 1;
    const rawStep = value / 4;
    const magnitude = 10 ** Math.floor(Math.log10(rawStep));
    const normalized = rawStep / magnitude;
    const factor = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
    return factor * magnitude * 4;
  }
  function trendTimeLabel(timestamp, longSpan) {
    return new Intl.DateTimeFormat(runtime.config.isZH ? "zh-CN" : "en-US", {
      ...longSpan ? { month: "numeric", day: "numeric" } : { hour: "2-digit", minute: "2-digit", hour12: false }
    }).format(new Date(timestamp));
  }
  function trendSvg(points) {
    const width = 520;
    const height = 180;
    const plot = { left: 58, right: 12, top: 10, bottom: 30 };
    const plotWidth = width - plot.left - plot.right;
    const plotHeight = height - plot.top - plot.bottom;
    const svg = svgElement("svg");
    svg.classList.add("mwi-guild-trend");
    svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
    svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
    const label = t16(
      "公会经验获取速度(6 小时滚动平均,XP/小时)",
      "Guild XP gain rate (6-hour rolling average, XP/hour)"
    );
    svg.setAttribute("role", "img");
    svg.setAttribute("aria-label", label);
    const title = svgElement("title");
    title.textContent = label;
    svg.append(title);
    const recent = guildXpRatePoints(points);
    if (recent.length < 2) {
      const empty = svgElement("text", "mwi-guild-trend-empty");
      empty.setAttribute("x", String(width / 2));
      empty.setAttribute("y", String(height / 2));
      empty.textContent = t16("样本不足", "Not enough data");
      svg.append(empty);
      return svg;
    }
    const yMax = niceRateCeiling(Math.max(...recent.map((point) => point.rate)));
    const minAt = recent[0].at;
    const maxAt = recent.at(-1).at;
    const yAxis = svgElement("line", "mwi-guild-trend-axis mwi-guild-axis-y");
    yAxis.setAttribute("x1", String(plot.left));
    yAxis.setAttribute("x2", String(plot.left));
    yAxis.setAttribute("y1", String(plot.top));
    yAxis.setAttribute("y2", String(plot.top + plotHeight));
    const xAxis = svgElement("line", "mwi-guild-trend-axis mwi-guild-axis-x");
    xAxis.setAttribute("x1", String(plot.left));
    xAxis.setAttribute("x2", String(plot.left + plotWidth));
    xAxis.setAttribute("y1", String(plot.top + plotHeight));
    xAxis.setAttribute("y2", String(plot.top + plotHeight));
    for (let index = 0; index < 5; index += 1) {
      const ratio = index / 4;
      const y = plot.top + plotHeight - ratio * plotHeight;
      const grid = svgElement("line", "mwi-guild-trend-grid");
      grid.setAttribute("x1", String(plot.left));
      grid.setAttribute("x2", String(plot.left + plotWidth));
      grid.setAttribute("y1", String(y));
      grid.setAttribute("y2", String(y));
      const tick = svgElement("text", "mwi-guild-trend-tick mwi-guild-y-tick");
      tick.setAttribute("x", String(plot.left - 7));
      tick.setAttribute("y", String(y + 3));
      tick.setAttribute("text-anchor", "end");
      tick.textContent = runtime.api.numberFormatter(yMax * ratio);
      svg.append(grid, tick);
    }
    const longSpan = maxAt - minAt > 24 * HOUR_MS2;
    for (let index = 0; index < 4; index += 1) {
      const ratio = index / 3;
      const x = plot.left + ratio * plotWidth;
      const tick = svgElement("text", "mwi-guild-trend-tick mwi-guild-x-tick");
      tick.setAttribute("x", String(x));
      tick.setAttribute("y", String(plot.top + plotHeight + 19));
      tick.setAttribute(
        "text-anchor",
        index === 0 ? "start" : index === 3 ? "end" : "middle"
      );
      tick.textContent = trendTimeLabel(
        minAt + ratio * (maxAt - minAt),
        longSpan
      );
      svg.append(tick);
    }
    svg.append(yAxis, xAxis);
    const polyline = svgElement("polyline");
    polyline.setAttribute(
      "points",
      recent.map((point) => {
        const x = plot.left + (point.at - minAt) / Math.max(1, maxAt - minAt) * plotWidth;
        const y = plot.top + plotHeight - point.rate / yMax * plotHeight;
        return `${x},${y}`;
      }).join(" ")
    );
    svg.append(polyline);
    return svg;
  }
  function removeGuildOverviewCards(keep = null) {
    document.querySelectorAll(".mwi-guild-xp-card").forEach((card) => {
      if (card !== keep) card.remove();
    });
  }
  function findGuildOverviewHost() {
    const guildPanel = document.querySelector(
      'div[class*="GuildPanel_guildPanel"]'
    );
    if (!guildPanel) return null;
    const host = guildPanel.querySelector('[class*="GuildPanel_overviewTab"]');
    const tabPanel = host?.closest('[class*="TabPanel_tabPanel"]');
    const tabPanels = [
      ...guildPanel.querySelectorAll('[class*="TabPanel_tabPanel"]')
    ];
    const tabs = [
      ...guildPanel.querySelectorAll('[role="tablist"] [role="tab"]')
    ];
    const panelIndex = tabPanels.indexOf(tabPanel);
    const overviewTab = panelIndex >= 0 ? tabs[panelIndex] : null;
    const ariaSelected = overviewTab?.getAttribute("aria-selected");
    const overviewSelected = overviewTab ? ariaSelected !== null ? ariaSelected === "true" : overviewTab.classList.contains("Mui-selected") || overviewTab.getAttribute("tabindex") === "0" : true;
    if (!host || !overviewSelected || tabPanel?.hidden || tabPanel?.className.includes("TabPanel_hidden")) {
      return null;
    }
    return host;
  }
  async function renderGuildOverview() {
    let host = findGuildOverviewHost();
    const guild2 = runtime.state.guild;
    if (!host || !guild2) {
      removeGuildOverviewCards();
      return;
    }
    const key = objectKey("guild", guild2);
    const rates = rateCache.get(key) ?? await refreshRate(key);
    const currentHost = findGuildOverviewHost();
    if (!currentHost || currentHost !== host) {
      removeGuildOverviewCards();
      return;
    }
    host = currentHost;
    let card = host.querySelector(":scope > .mwi-guild-xp-card");
    removeGuildOverviewCards(card);
    if (!card) {
      card = document.createElement("section");
      card.className = "mwi-guild-xp-card";
      host.prepend(card);
    }
    card.replaceChildren();
    const head = document.createElement("div");
    head.className = "mwi-guild-xp-head";
    const title = document.createElement("div");
    title.className = "mwi-guild-xp-title";
    title.textContent = t16("公会经验进度", "Guild XP progress");
    const sampled = document.createElement("div");
    sampled.className = "mwi-guild-xp-sampled";
    sampled.textContent = rates?.lastSampleAt ? `${t16("最后采样", "Last sample")} ${new Date(rates.lastSampleAt).toLocaleString()}` : t16("待采样", "Awaiting samples");
    head.append(title, sampled);
    const grid = document.createElement("div");
    grid.className = "mwi-guild-xp-grid";
    const xp = entityXp(guild2);
    const level = Number(findField(guild2, ["level", "guildLevel"]));
    const nextXp = Number.isInteger(level) ? Number(runtime.state.initData_levelExperienceTable?.[level + 1]) : NaN;
    const remaining = Number.isFinite(nextXp) && xp !== null && nextXp > xp ? nextXp - xp : null;
    const dayRate = Number(rates?.day);
    const recentRate = Number(rates?.recent);
    const estimateRate = Number.isFinite(dayRate) && dayRate > 0 ? dayRate : Number.isFinite(recentRate) && recentRate > 0 ? recentRate : null;
    const etaHours = remaining !== null && estimateRate !== null ? remaining / estimateRate : null;
    grid.append(
      metric2(
        t16("预计升级", "Level ETA"),
        Number.isFinite(etaHours) ? runtime.api.timeReadable(etaHours * 3600) : t16("样本不足", "Not enough data")
      ),
      metric2(t16("24 小时平均", "24-hour average"), rateText(rates?.day))
    );
    const trendLabel = document.createElement("div");
    trendLabel.className = "mwi-guild-trend-label";
    trendLabel.textContent = t16(
      "最近 7 天经验获取速度(6 小时滚动平均)",
      "XP gain rate over the last 7 days (6-hour rolling average)"
    );
    card.append(head, grid, trendLabel, trendSvg(rates?.points ?? []));
    if (runtime.settings.get("guildIdleMembers")) {
      const idle = (runtime.state.guildCharacters ?? []).filter(
        isGuildMemberIdle
      );
      const idleRow = document.createElement("div");
      idleRow.className = "mwi-guild-idle";
      const label = document.createElement("b");
      label.textContent = `${t16("当前闲置", "Idle now")} (${idle.length}) · ${t16(
        "状态更新",
        "Updated"
      )} ${new Date(runtime.state.guildStateUpdatedAt).toLocaleTimeString()}`;
      idleRow.append(label);
      for (const member of idle) {
        const tag = document.createElement("span");
        tag.textContent = entityName2(member);
        idleRow.append(tag);
      }
      card.append(idleRow);
    }
  }
  function appendRateColumns(table, rows2, kind, parentId = "") {
    if (!table?.tHead?.rows?.[0] || !rows2.length) return;
    table.classList.add(`mwi-guild-${kind}-table`);
    if (kind === "member") {
      table.closest('[class*="GuildPanel_membersTab__"]')?.classList.add("mwi-guild-members-wide");
      table.parentElement?.classList.add("mwi-guild-member-table-wrap");
    }
    const header = table.tHead.rows[0];
    if (!header.querySelector(".mwi-guild-recent-head")) {
      const sortable = kind === "member";
      const columns = [
        ["mwi-guild-recent-head", t16("近 6 小时 XP/h", "6h XP/h")],
        ["mwi-guild-day-head", t16("24 小时 XP/h", "24h XP/h")],
        ...kind === "member" ? [["mwi-guild-week-head", t16("本周平均 XP/h", "This-week avg XP/h")]] : []
      ];
      for (const [rateIndex, [className, label]] of columns.entries()) {
        const cell = document.createElement("th");
        cell.className = className;
        const labelNode = document.createElement("span");
        labelNode.textContent = label;
        cell.append(labelNode);
        if (!sortable) {
          header.append(cell);
          continue;
        }
        const sortIndicator = document.createElement("span");
        sortIndicator.className = "mwi-guild-rate-sort";
        sortIndicator.textContent = "↕";
        cell.append(sortIndicator);
        cell.tabIndex = 0;
        cell.style.cursor = "pointer";
        cell.title = t16("点击按经验速率排序", "Click to sort by XP rate");
        const sortRows = () => {
          const body = table.tBodies[0];
          if (!body) return;
          const direction = cell.dataset.direction === "desc" ? 1 : -1;
          cell.dataset.direction = direction === -1 ? "desc" : "asc";
          header.querySelectorAll(
            ".mwi-guild-recent-head,.mwi-guild-day-head,.mwi-guild-week-head"
          ).forEach((head) => {
            if (head !== cell) {
              delete head.dataset.direction;
              head.setAttribute("aria-sort", "none");
              const indicator = head.querySelector(".mwi-guild-rate-sort");
              if (indicator) indicator.textContent = "↕";
            }
          });
          cell.setAttribute(
            "aria-sort",
            direction === -1 ? "descending" : "ascending"
          );
          sortIndicator.textContent = direction === -1 ? "▼" : "▲";
          const tableRows = [...body.rows];
          tableRows.sort((left, right) => {
            const leftCell = left.querySelectorAll(".mwi-guild-rate-cell")[rateIndex];
            const rightCell = right.querySelectorAll(".mwi-guild-rate-cell")[rateIndex];
            const leftValue = Number(leftCell?.dataset.sortValue ?? -1);
            const rightValue = Number(rightCell?.dataset.sortValue ?? -1);
            if (leftValue < 0 && rightValue < 0) return 0;
            if (leftValue < 0) return 1;
            if (rightValue < 0) return -1;
            return direction === -1 ? rightValue - leftValue : leftValue - rightValue;
          });
          tableRows.forEach((row) => body.append(row));
        };
        cell.addEventListener("click", sortRows);
        cell.addEventListener("keydown", (event) => {
          if (event.key !== "Enter" && event.key !== " ") return;
          event.preventDefault();
          sortRows();
        });
        header.append(cell);
      }
    }
    for (const selector of [
      ".mwi-guild-recent-head",
      ".mwi-guild-day-head",
      ...kind === "member" ? [".mwi-guild-week-head"] : []
    ]) {
      const rateHeader = header.querySelector(selector);
      if (rateHeader) header.append(rateHeader);
    }
    const sourceByKey = new Map(
      rows2.map((source) => [objectKey(kind, source, parentId), source])
    );
    const rowEntries = [...table.tBodies[0]?.rows ?? []].map((row, index) => {
      let key = row.dataset.mwiGuildEntityKey ?? "";
      let source = sourceByKey.get(key);
      if (!source) {
        source = rows2[index];
        key = objectKey(kind, source, parentId);
        if (key) row.dataset.mwiGuildEntityKey = key;
      }
      const rates = rateCache.get(key);
      return {
        row,
        key,
        rates,
        values: [
          rates?.recent,
          rates?.day,
          ...kind === "member" ? [entityWeeklyXpRate(source)] : []
        ]
      };
    });
    const maxima = Array.from(
      { length: kind === "member" ? 3 : 2 },
      (_, index) => Math.max(
        0,
        ...rowEntries.map(
          ({ values }) => Number.isFinite(values[index]) ? values[index] : 0
        )
      )
    );
    rowEntries.forEach(({ row, rates, values }) => {
      row.querySelectorAll(".mwi-guild-rate-cell").forEach((cell) => cell.remove());
      for (const [rateIndex, value] of values.entries()) {
        const cell = document.createElement("td");
        cell.className = "mwi-guild-rate-cell";
        cell.dataset.sortValue = Number.isFinite(value) ? String(value) : "-1";
        const content = document.createElement("div");
        content.className = "mwi-guild-rate-content";
        const valueNode = document.createElement("span");
        valueNode.className = "mwi-guild-rate-value";
        valueNode.textContent = rateText(value, !rates?.lastSampleAt);
        content.append(valueNode);
        if (Number.isFinite(value) && maxima[rateIndex] > 0) {
          const track = document.createElement("span");
          track.className = "mwi-guild-rate-track";
          track.setAttribute("aria-hidden", "true");
          const fill = document.createElement("span");
          fill.className = "mwi-guild-rate-fill";
          const percentage = Math.max(
            0,
            Math.min(100, value / maxima[rateIndex] * 100)
          );
          fill.style.width = `${Math.round(percentage * 1e3) / 1e3}%`;
          track.append(fill);
          content.append(track);
        }
        cell.append(content);
        row.append(cell);
      }
    });
  }
  function appendLeaderboardDivRates(rows2) {
    const leaderboard = document.querySelector(
      'div[class*="LeaderboardPanel_leaderboardTable"]'
    );
    if (!leaderboard || !rows2.length) return;
    let head = leaderboard.parentElement.querySelector(
      ":scope > .mwi-guild-div-rate-head"
    );
    if (!head) {
      head = document.createElement("div");
      head.className = "mwi-guild-div-rate-head";
      head.append(
        Object.assign(document.createElement("span"), {
          textContent: t16("近 6 小时 XP/h", "6h XP/h")
        }),
        Object.assign(document.createElement("span"), {
          textContent: t16("24 小时 XP/h", "24h XP/h")
        })
      );
      leaderboard.before(head);
    }
    const guildNames = [
      ...leaderboard.querySelectorAll('[class*="LeaderboardPanel_guildName"]')
    ];
    guildNames.forEach((name, index) => {
      let row = name;
      while (row.parentElement && row.parentElement !== leaderboard) {
        row = row.parentElement;
      }
      row.querySelector(":scope > .mwi-guild-div-rates")?.remove();
      const rates = rateCache.get(objectKey("leaderboard", rows2[index]));
      const cells = document.createElement("div");
      cells.className = "mwi-guild-div-rates";
      for (const value of [rates?.recent, rates?.day]) {
        const cell = document.createElement("span");
        cell.dataset.sortValue = Number.isFinite(value) ? String(value) : "-1";
        cell.textContent = rateText(value, !rates?.lastSampleAt);
        cells.append(cell);
      }
      row.append(cells);
    });
  }
  function renderGuildTables() {
    if (runtime.settings.get("guildMemberXp")) {
      const memberTable = document.querySelector(
        'div[class*="GuildPanel_membersTab"] table'
      );
      appendRateColumns(
        memberTable,
        runtime.state.guildCharacters,
        "member",
        entityId(runtime.state.guild)
      );
    }
    if (runtime.settings.get("guildLeaderboardXp")) {
      const leaderboardTable = document.querySelector(
        'div[class*="Leaderboard"] table'
      );
      appendRateColumns(
        leaderboardTable,
        runtime.state.guildLeaderboard,
        "leaderboard"
      );
      if (!leaderboardTable) {
        appendLeaderboardDivRates(runtime.state.guildLeaderboard);
      }
    }
  }
  runtime.features.register({
    id: "guildXpTracking",
    setting: "guildXpTracking",
    scope: "character",
    initialize({ scope }) {
      sampleGuildState(false);
      scope.add(
        runtime.onMessage("guild_updated", () => sampleGuildState(false))
      );
      scope.add(
        runtime.onMessage(
          "guild_characters_updated",
          () => sampleGuildState(false)
        )
      );
      scope.add(
        runtime.onMessage("leaderboard_updated", () => sampleGuildState(true))
      );
    }
  });
  runtime.features.register({
    id: "guildOverview",
    setting: "guildOverview",
    scope: "character",
    dependsOn: ["guildXpTracking"],
    initialize({ scope }) {
      addStyles14();
      renderGuildOverview();
      observeGuildSurface(scope, renderGuildOverview);
      scope.add(
        () => document.querySelectorAll(".mwi-guild-xp-card").forEach((node) => node.remove())
      );
    }
  });
  for (const id of ["guildMemberXp", "guildLeaderboardXp", "guildIdleMembers"]) {
    runtime.features.register({
      id,
      setting: id,
      scope: "character",
      dependsOn: id === "guildIdleMembers" ? ["guildXpTracking", "guildOverview"] : ["guildXpTracking"],
      initialize({ scope }) {
        addStyles14();
        renderGuildTables();
        if (id === "guildIdleMembers") renderGuildOverview();
        if (id !== "guildIdleMembers") {
          observeGuildSurface(scope, renderGuildTables);
        }
        scope.add(() => {
          if (id === "guildIdleMembers") {
            document.querySelectorAll(".mwi-guild-idle").forEach((node) => node.remove());
            return;
          }
          const kind = id === "guildMemberXp" ? "member" : "leaderboard";
          document.querySelectorAll(`table.mwi-guild-${kind}-table`).forEach((table) => {
            if (kind === "member") {
              table.closest(".mwi-guild-members-wide")?.classList.remove("mwi-guild-members-wide");
              table.parentElement?.classList.remove(
                "mwi-guild-member-table-wrap"
              );
            }
            table.querySelectorAll(
              ".mwi-guild-rate-cell,.mwi-guild-recent-head,.mwi-guild-day-head,.mwi-guild-week-head"
            ).forEach((node) => node.remove());
            table.classList.remove(`mwi-guild-${kind}-table`);
          });
          if (kind === "leaderboard") {
            document.querySelectorAll(".mwi-guild-div-rate-head,.mwi-guild-div-rates").forEach((node) => node.remove());
          }
        });
      }
    });
  }
  Object.assign(runtime.api, {
    sampleGuildState,
    renderGuildOverview,
    renderGuildTables,
    getGuildEntityXp: entityXp,
    getGuildWeeklyXpRate: entityWeeklyXpRate,
    getGuildXpRatePoints: guildXpRatePoints,
    isGuildMemberIdle
  });

  // src/features/game-widgets.js
  function t17(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  var lastBattleSummaryMessage = null;
  function escapeRegularExpression2(value) {
    return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }
  function translatedTemplateValue(text, path, fallbacks, valuePattern) {
    const templates = [getGameTranslation(path), ...fallbacks].filter(Boolean);
    for (const template of templates) {
      let captured = false;
      const parts = String(template).split(/({{[^}]+}})/g);
      const source = parts.map((part) => {
        if (/^{{[^}]+}}$/.test(part)) {
          if (captured) return `(?:${valuePattern})`;
          captured = true;
          return `(${valuePattern})`;
        }
        return escapeRegularExpression2(part).replace(/\s+/g, "\\s+");
      }).join("");
      if (!captured) continue;
      const match = String(text ?? "").match(new RegExp(source, "iu"));
      if (match?.[1]) return match[1].trim();
    }
    return "";
  }
  function durationSeconds(value) {
    let total = 0;
    let found = false;
    for (const match of String(value ?? "").matchAll(/(\d+)\s*([dhms])/gi)) {
      found = true;
      const amount = Number(match[1]);
      total += amount * ({ d: 86400, h: 3600, m: 60, s: 1 }[match[2].toLowerCase()] ?? 0);
    }
    return found ? total : null;
  }
  function wholeNumber2(value) {
    const digits = String(value ?? "").replace(/\D/g, "");
    return digits ? Number(digits) : null;
  }
  function parseLocalizedBattleInfo(text) {
    const duration = translatedTemplateValue(
      text,
      "battlePanel.combatDuration",
      [
        "Combat Duration: {{duration}}",
        "战斗时间: {{duration}}",
        "战斗时长: {{duration}}"
      ],
      "(?:\\d+\\s*[dhms]\\s*)+"
    );
    const battles = translatedTemplateValue(
      text,
      "battlePanel.battles",
      ["Battles: {{battleId}}", "交战: {{battleId}}", "战斗: {{battleId}}"],
      "[\\d\\s.,\\u00a0\\u202f]+"
    );
    const deaths = translatedTemplateValue(
      text,
      "battlePanel.deaths",
      [
        "Deaths: {{deathCount}}",
        "战败: {{deathCount}}",
        "死亡次数: {{deathCount}}"
      ],
      "[\\d\\s.,\\u00a0\\u202f]+"
    );
    const battleDurationSec = durationSeconds(duration);
    const battleCount = wholeNumber2(battles);
    const deathCount = wholeNumber2(deaths);
    if (!Number.isFinite(battleDurationSec) || !Number.isFinite(battleCount) || !Number.isFinite(deathCount)) {
      return null;
    }
    return { battleDurationSec, battleCount, deathCount };
  }
  async function handleBattleSummary(message) {
    lastBattleSummaryMessage = message;
    const suppressMarket = runtime.api.shouldSuppressMarketFeatures?.() ?? false;
    const marketJson = suppressMarket ? null : await runtime.api.fetchMarketJSON();
    if (!suppressMarket && !marketJson) {
      console.error(
        runtime.config.isZH ? "[MWITools] 市场数据不可用,战斗总结将不显示市场收益。" : "[MWITools] Market data is unavailable; market revenue is omitted from the battle summary."
      );
    }
    let totalPriceAsk = 0;
    let totalPriceAskBid = 0;
    let totalRawCoins = 0;
    if (marketJson && message.unit.totalLootMap) {
      for (const loot of Object.values(message.unit.totalLootMap)) {
        const itemCount = loot.count;
        if (loot.itemHrid === "/items/coin") {
          totalRawCoins += itemCount;
        }
        if (marketJson.marketData?.[loot.itemHrid]) {
          totalPriceAsk += marketJson.marketData[loot.itemHrid][0].a * itemCount;
          totalPriceAskBid += runtime.api.getNetSellPrice(loot.itemHrid, 0) * itemCount;
        } else {
          console.log(
            runtime.config.isZH ? `[MWITools] 无法读取战利品价格:${loot.itemHrid}` : `[MWITools] Could not read the loot price: ${loot.itemHrid}`
          );
        }
      }
    }
    let totalSkillsExp = 0;
    if (message.unit.totalSkillExperienceMap) {
      for (const exp of Object.values(message.unit.totalSkillExperienceMap)) {
        totalSkillsExp += exp;
      }
    }
    let tryTimes = 0;
    findElem();
    function findElem() {
      tryTimes++;
      let elem = document.querySelector(
        ".BattlePanel_gainedExp__3SaCa"
      )?.parentElement;
      if (elem) {
        elem.querySelectorAll(
          '[data-mwitools-battle-summary="true"],#script_battleNumbers,#script_totalIncome,#script_averageIncome,#script_totalIncomeDay,#script_avgRawCoinHour,#script_totalSkillsExp,#script_averageSkillsExp'
        ).forEach((node) => node.remove());
        const appendSummary = (id, html) => {
          const row = document.createElement("div");
          row.id = id;
          row.dataset.mwitoolsBattleSummary = "true";
          row.style.color = runtime.config.SCRIPT_COLOR_MAIN;
          row.innerHTML = html;
          elem.append(row);
          return row;
        };
        let battleDurationSec = null;
        const combatInfoElement = document.querySelector(
          ".BattlePanel_combatInfo__sHGCe"
        );
        if (combatInfoElement) {
          const battleInfo = parseLocalizedBattleInfo(
            combatInfoElement.textContent
          );
          if (battleInfo) {
            battleDurationSec = battleInfo.battleDurationSec;
            const battles = battleInfo.battleCount - 1;
            const efficiencyPerHour = battleDurationSec ? (battles / battleDurationSec * 3600).toFixed(1) : "—";
            appendSummary(
              "script_battleNumbers",
              `${runtime.config.isZH ? "每小时战斗: " : "Encounters/hour: "}${efficiencyPerHour}${runtime.config.isZH ? " 次" : ""}`
            );
          }
        }
        if (!suppressMarket && marketJson) {
          appendSummary(
            "script_totalIncome",
            `${runtime.config.isZH ? "总收获: " : "Total revenue: "}${runtime.api.numberFormatter(totalPriceAsk)} / ${runtime.api.numberFormatter(totalPriceAskBid)}`
          );
          if (battleDurationSec) {
            const hours = battleDurationSec / 3600;
            appendSummary(
              "script_averageIncome",
              `${runtime.config.isZH ? "每小时收获: " : "Revenue/hour: "}${runtime.api.numberFormatter(totalPriceAsk / hours)} / ${runtime.api.numberFormatter(totalPriceAskBid / hours)}`
            );
            appendSummary(
              "script_totalIncomeDay",
              `${runtime.config.isZH ? "每天收获: " : "Revenue/day: "}${runtime.api.numberFormatter(totalPriceAsk / hours * 24)} / ${runtime.api.numberFormatter(totalPriceAskBid / hours * 24)}`
            );
            appendSummary(
              "script_avgRawCoinHour",
              `${runtime.config.isZH ? "每小时仅金币收获: " : "Raw coins/hour: "}${runtime.api.numberFormatter(totalRawCoins / hours)}`
            );
          }
        }
        appendSummary(
          "script_totalSkillsExp",
          `${runtime.config.isZH ? "总经验: " : "Total exp: "}${runtime.api.numberFormatter(totalSkillsExp)}`
        );
        if (battleDurationSec) {
          const hours = battleDurationSec / 3600;
          appendSummary(
            "script_averageSkillsExp",
            `${runtime.config.isZH ? "每小时总经验: " : "Total exp/hour: "}${runtime.api.numberFormatter(totalSkillsExp / hours)}`
          );
          [
            { skillHrid: "/skills/magic", zhName: "魔法", enName: "Magic" },
            { skillHrid: "/skills/ranged", zhName: "远程", enName: "Ranged" },
            { skillHrid: "/skills/defense", zhName: "防御", enName: "Defense" },
            { skillHrid: "/skills/melee", zhName: "近战", enName: "Melee" },
            { skillHrid: "/skills/attack", zhName: "攻击", enName: "Attack" },
            {
              skillHrid: "/skills/intelligence",
              zhName: "智力",
              enName: "Intelligence"
            },
            { skillHrid: "/skills/stamina", zhName: "耐力", enName: "Stamina" }
          ].forEach((skill) => {
            const expGained = message.unit.totalSkillExperienceMap?.[skill.skillHrid];
            if (expGained) {
              appendSummary(
                `script_${skill.skillHrid.split("/").at(-1)}ExpHour`,
                `${runtime.config.isZH ? "每小时" : ""}${runtime.config.isZH ? skill.zhName : skill.enName}${runtime.config.isZH ? "经验: " : " exp/hour: "}${runtime.api.numberFormatter(expGained / hours)}`
              );
            }
          });
        } else {
          console.error(
            runtime.config.isZH ? "[MWITools] 战斗时长无效,无法显示平均经验。" : "[MWITools] Battle duration is invalid; average XP cannot be displayed."
          );
        }
      } else if (tryTimes <= 10) {
        setTimeout(findElem, 200);
      } else {
        console.error(
          runtime.config.isZH ? "[MWITools] 重试 10 次后仍未找到战斗总结。" : "[MWITools] Battle summary was not found after 10 attempts."
        );
      }
    }
  }
  function addItemLevels() {
    const itemDetailMap = runtime.state.initData_itemDetailMap;
    if (!itemDetailMap) return;
    const iconDivs = document.querySelectorAll(
      "div.Item_itemContainer__x7kH1 div.Item_item__2De2O.Item_clickable__3viV6"
    );
    for (const div of iconDivs) {
      if (div.querySelector("div.Item_name__2C42x")) {
        continue;
      }
      const href = div.querySelector("use")?.getAttribute("href");
      if (!href?.includes("#")) continue;
      const hrefName = href.split("#")[1];
      const itemHrid = "/items/" + hrefName;
      let itemDetail;
      try {
        itemDetail = itemDetailMap[itemHrid];
      } catch {
        return;
      }
      const itemLevel = itemDetail?.itemLevel;
      const itemAbilityLevel = itemDetail?.abilityBookDetail?.levelRequirements?.[0]?.level;
      if (itemDetail?.equipmentDetail && itemLevel && itemLevel > 0) {
        if (!div.querySelector("div.script_itemLevel")) {
          div.style.position = "relative";
          div.insertAdjacentHTML(
            "beforeend",
            `<div class="script_itemLevel" style="z-index: 1; position: absolute; top: 2px; right: 2px; text-align: right; color: ${runtime.config.SCRIPT_COLOR_MAIN};">${itemLevel}</div>`
          );
        }
        if (!itemDetail?.equipmentDetail?.type?.includes("_tool") && div.parentElement.parentElement.parentElement.parentElement.className.includes(
          "MarketplacePanel_marketItems__D4k7e"
        )) {
          handleMarketItemFilter(div, itemDetail);
        }
      } else if (itemAbilityLevel && itemAbilityLevel > 0) {
        if (!div.querySelector("div.script_itemLevel")) {
          div.style.position = "relative";
          div.insertAdjacentHTML(
            "beforeend",
            `<div class="script_itemLevel" style="z-index: 1; position: absolute; top: 2px; right: 2px; text-align: right; color: ${runtime.config.SCRIPT_COLOR_MAIN};">${itemAbilityLevel}</div>`
          );
        }
      } else if (runtime.settings.settingsMap.showsKeyInfoInIcon.isTrue && (itemHrid.includes("_key_fragment") || itemHrid.includes("_key"))) {
        const map = /* @__PURE__ */ new Map();
        map.set("/items/blue_key_fragment", runtime.config.isZH ? "图3" : "Z3");
        map.set("/items/green_key_fragment", runtime.config.isZH ? "图4" : "Z4");
        map.set("/items/purple_key_fragment", runtime.config.isZH ? "图5" : "Z5");
        map.set("/items/white_key_fragment", runtime.config.isZH ? "图6" : "Z6");
        map.set("/items/orange_key_fragment", runtime.config.isZH ? "图7" : "Z7");
        map.set("/items/brown_key_fragment", runtime.config.isZH ? "图8" : "Z8");
        map.set("/items/stone_key_fragment", runtime.config.isZH ? "图9" : "Z9");
        map.set("/items/dark_key_fragment", runtime.config.isZH ? "图10" : "Z10");
        map.set(
          "/items/burning_key_fragment",
          runtime.config.isZH ? "图11" : "Z11"
        );
        map.set(
          "/items/chimerical_entry_key",
          runtime.config.isZH ? "牢1" : "D1"
        );
        map.set("/items/sinister_entry_key", runtime.config.isZH ? "牢2" : "D2");
        map.set("/items/enchanted_entry_key", runtime.config.isZH ? "牢3" : "D3");
        map.set("/items/pirate_entry_key", runtime.config.isZH ? "牢4" : "D4");
        map.set("/items/chimerical_chest_key", "3.4.5.6");
        map.set("/items/sinister_chest_key", "5.7.8.10");
        map.set("/items/enchanted_chest_key", "7.8.9.11");
        map.set("/items/pirate_chest_key", "6.9.10.11");
        if (!div.querySelector("div.script_key")) {
          div.style.position = "relative";
          div.insertAdjacentHTML(
            "beforeend",
            `<div class="script_key" style="z-index: 1; position: absolute; top: 2px; right: 2px; text-align: right; color: ${runtime.config.SCRIPT_COLOR_MAIN};">${map.get(
              itemHrid
            )}</div>`
          );
        }
      }
    }
  }
  var onlyShowItemsAboveLevel = 1;
  var onlyShowItemsBelowLevel = 1e3;
  var onlyShowItemsType = "all";
  var onlyShowItemsSkillReq = "all";
  function addMarketFilterButtons() {
    const oriFilter = document.querySelector(
      ".MarketplacePanel_itemFilterContainer__3F3td"
    );
    let filters = document.querySelector("#script_filters");
    if (oriFilter && !filters) {
      oriFilter.insertAdjacentHTML(
        "afterend",
        `<div id="script_filters" style="float: left; color: ${runtime.config.SCRIPT_COLOR_MAIN};"></div>`
      );
      filters = document.querySelector("#script_filters");
      filters.insertAdjacentHTML(
        "beforeend",
        `<span id="script_filter_level" style="float: left; color: ${runtime.config.SCRIPT_COLOR_MAIN};">${runtime.config.isZH ? "等级: 大于等于 " : "Equipment level: >= "}
            <select name="script_filter_level_select" id="script_filter_level_select">
            <option value="1">${t17("全部", "All")}</option>
            <option value="10">10</option>
            <option value="20">20</option>
            <option value="30">30</option>
            <option value="40">40</option>
            <option value="50">50</option>
            <option value="60">60</option>
            <option value="65">65</option>
            <option value="70">70</option>
            <option value="75">75</option>
            <option value="80">80</option>
            <option value="85">85</option>
            <option value="90">90</option>
            <option value="95">95</option>
            <option value="100">100</option>
        </select>&nbsp;</span>`
      );
      filters.insertAdjacentHTML(
        "beforeend",
        `<span id="script_filter_level_to" style="float: left; color: ${runtime.config.SCRIPT_COLOR_MAIN};">${runtime.config.isZH ? "小于 " : "< "}
            <select name="script_filter_level_select_to" id="script_filter_level_select_to">
            <option value="1000">${t17("全部", "All")}</option>
            <option value="10">10</option>
            <option value="20">20</option>
            <option value="30">30</option>
            <option value="40">40</option>
            <option value="50">50</option>
            <option value="60">60</option>
            <option value="65">65</option>
            <option value="70">70</option>
            <option value="75">75</option>
            <option value="80">80</option>
            <option value="85">85</option>
            <option value="90">90</option>
            <option value="95">95</option>
            <option value="100">100</option>
        </select>&emsp;</span>`
      );
      filters.insertAdjacentHTML(
        "beforeend",
        `<span id="script_filter_skill" style="float: left; color: ${runtime.config.SCRIPT_COLOR_MAIN};">${runtime.config.isZH ? "职业: " : "Class: "}
            <select name="script_filter_skill_select" id="script_filter_skill_select">
                <option value="all">${t17("全部", "All")}</option>
                <option value="attack">${t17("攻击", "Attack")}</option>
                <option value="melee">${t17("近战", "Melee")}</option>
                <option value="defense">${t17("防御", "Defense")}</option>
                <option value="ranged">${t17("远程", "Ranged")}</option>
                <option value="magic">${t17("魔法", "Magic")}</option>
                <option value="others">${t17("其他", "Others")}</option>
            </select>&emsp;</span>`
      );
      filters.insertAdjacentHTML(
        "beforeend",
        `<span id="script_filter_location" style="float: left; color: ${runtime.config.SCRIPT_COLOR_MAIN};">${runtime.config.isZH ? "部位: " : "Slot: "}
            <select name="script_filter_location_select" id="script_filter_location_select">
                <option value="all">${t17("全部", "All")}</option>
                <option value="main_hand">${t17("主手", "Main Hand")}</option>
                <option value="off_hand">${t17("副手", "Off Hand")}</option>
                <option value="two_hand">${t17("双手", "Two Hand")}</option>
                <option value="head">${t17("头部", "Head")}</option>
                <option value="body">${t17("身体", "Body")}</option>
                <option value="hands">${t17("手部", "Hands")}</option>
                <option value="legs">${t17("腿部", "Legs")}</option>
                <option value="feet">${t17("脚部", "Feet")}</option>
                <option value="neck">${t17("项链", "Neck")}</option>
                <option value="earrings">${t17("耳饰", "Earrings")}</option>
                <option value="ring">${t17("戒指", "Ring")}</option>
                <option value="pouch">${t17("袋子", "Pouch")}</option>
                <option value="back">${t17("背部", "Back")}</option>
            </select>&emsp;</span>`
      );
      const levelFilter = document.querySelector("#script_filter_level_select");
      levelFilter.addEventListener("change", function() {
        if (levelFilter.value && !isNaN(levelFilter.value)) {
          onlyShowItemsAboveLevel = Number(levelFilter.value);
        }
      });
      const levelToFilter = document.querySelector(
        "#script_filter_level_select_to"
      );
      levelToFilter.addEventListener("change", function() {
        if (levelToFilter.value && !isNaN(levelToFilter.value)) {
          onlyShowItemsBelowLevel = Number(levelToFilter.value);
        }
      });
      const skillFilter = document.querySelector("#script_filter_skill_select");
      skillFilter.addEventListener("change", function() {
        if (skillFilter.value) {
          onlyShowItemsSkillReq = skillFilter.value;
        }
      });
      const locationFilter = document.querySelector(
        "#script_filter_location_select"
      );
      locationFilter.addEventListener("change", function() {
        if (locationFilter.value) {
          onlyShowItemsType = locationFilter.value;
        }
      });
    }
  }
  function handleMarketItemFilter(div, itemDetal) {
    if (!itemDetal.equipmentDetail) {
      return;
    }
    const itemLevel = itemDetal.itemLevel;
    const type = itemDetal.equipmentDetail.type;
    const levelRequirements = itemDetal.equipmentDetail.levelRequirements;
    let isType = false;
    isType = type && type.includes(onlyShowItemsType);
    if (onlyShowItemsType === "all") {
      isType = true;
    }
    let isRequired = false;
    for (const requirement of levelRequirements) {
      if (requirement.skillHrid.includes(onlyShowItemsSkillReq)) {
        isRequired = true;
      }
    }
    if (onlyShowItemsSkillReq === "others") {
      const combatTypes = ["attack", "melee", "defense", "ranged", "magic"];
      isRequired = !combatTypes.some((type2) => {
        for (const requirement of levelRequirements) {
          if (requirement.skillHrid.includes(type2)) {
            return true;
          }
        }
      });
    }
    if (onlyShowItemsSkillReq === "all") {
      isRequired = true;
    }
    if (itemLevel >= onlyShowItemsAboveLevel && itemLevel < onlyShowItemsBelowLevel && isType && isRequired) {
      div.style.display = "block";
    } else {
      div.style.display = "none";
    }
  }
  function handleTaskCard() {
    const taskNameDivs = document.querySelectorAll(
      "div.RandomTask_randomTask__3B9fA div.RandomTask_name__1hl1b"
    );
    for (const div of taskNameDivs) {
      if (div.querySelector("span.script_taskMapIndex")) {
        continue;
      }
      const card = div.closest('div[class*="RandomTask_randomTask"]');
      if (card && "mwitoolsMapIndex" in card.dataset) {
        const cachedIndex = Number(card.dataset.mwitoolsMapIndex);
        if (cachedIndex > 0) {
          div.insertAdjacentHTML(
            "beforeend",
            `<span class="script_taskMapIndex" style="text-align: right; color: ${runtime.config.SCRIPT_COLOR_MAIN};"> ${runtime.config.isZH ? "图" : "Z"}${cachedIndex}</span>`
          );
        }
        continue;
      }
      const taskStr = runtime.api.getOriTextFromElement(div);
      const monsterName2 = taskStr.split(/\s[-–]\s/).slice(1).join(" - ").trim();
      const actionHrid = (resolveLocalizedEntity("monster", monsterName2) || resolveLocalizedEntity("action", monsterName2)).replaceAll("/monsters/", "/actions/combat/");
      if (!actionHrid) continue;
      let actionObj = null;
      for (const action of Object.values(
        runtime.state.initData_actionDetailMap
      )) {
        if (action.hrid.includes("/combat/")) {
          if (action.hrid === actionHrid || action.name.toLowerCase() === monsterName2.toLowerCase()) {
            actionObj = action;
            break;
          } else if (action.combatZoneInfo.fightInfo.battlesPerBoss === 10) {
            if (actionHrid?.replaceAll("/actions/combat/", "/monsters/") === action.combatZoneInfo.fightInfo.bossSpawns[0].combatMonsterHrid || "/monsters/" + monsterName2.toLowerCase().replaceAll(" ", "_") === action.combatZoneInfo.fightInfo.bossSpawns[0].combatMonsterHrid) {
              actionObj = action;
              break;
            }
          }
        }
      }
      const actionCategoryHrid = actionObj?.category;
      const index = runtime.state.initData_actionCategoryDetailMap?.[actionCategoryHrid]?.sortIndex;
      if (index) {
        div.insertAdjacentHTML(
          "beforeend",
          `<span class="script_taskMapIndex" style="text-align: right; color: ${runtime.config.SCRIPT_COLOR_MAIN};"> ${runtime.config.isZH ? "图" : "Z"}${index}</span>`
        );
      }
    }
  }
  function addIndexToMaps() {
    const buttons = document.querySelectorAll(
      "div.MainPanel_subPanelContainer__1i-H9 div.CombatPanel_tabsComponentContainer__GsQlg div.MuiTabs-root.MuiTabs-vertical.css-6x4ics button.MuiButtonBase-root.MuiTab-root.MuiTab-textColorPrimary.css-1q2h7u5 span.MuiBadge-root.TabsComponent_badge__1Du26.css-1rzb3uu"
    );
    let index = 1;
    for (const button of buttons) {
      if (!button.querySelector("span.script_mapIndex")) {
        button.insertAdjacentHTML(
          "afterbegin",
          `<span class="script_mapIndex" style="color: ${runtime.config.SCRIPT_COLOR_MAIN};">${index++}. </span>`
        );
      }
    }
  }
  Object.assign(runtime.api, {
    handleBattleSummary,
    addItemLevels,
    addMarketFilterButtons,
    handleMarketItemFilter,
    handleTaskCard,
    addIndexToMaps
  });
  var refreshVisibleBattleSummary = () => {
    if (lastBattleSummaryMessage && document.querySelector(".BattlePanel_gainedExp__3SaCa")) {
      void handleBattleSummary(lastBattleSummaryMessage);
    }
  };
  runtime.settings.onChange?.(
    "adaptIronCowMarketFeatures",
    refreshVisibleBattleSummary
  );
  runtime.onMessage("init_character_data", refreshVisibleBattleSummary);
  Object.defineProperties(runtime.state, {
    onlyShowItemsAboveLevel: {
      enumerable: true,
      get() {
        return onlyShowItemsAboveLevel;
      },
      set(value) {
        onlyShowItemsAboveLevel = value;
      }
    },
    onlyShowItemsBelowLevel: {
      enumerable: true,
      get() {
        return onlyShowItemsBelowLevel;
      },
      set(value) {
        onlyShowItemsBelowLevel = value;
      }
    },
    onlyShowItemsType: {
      enumerable: true,
      get() {
        return onlyShowItemsType;
      },
      set(value) {
        onlyShowItemsType = value;
      }
    },
    onlyShowItemsSkillReq: {
      enumerable: true,
      get() {
        return onlyShowItemsSkillReq;
      },
      set(value) {
        onlyShowItemsSkillReq = value;
      }
    }
  });
  function scanOnDemand(scope, scan2, messages = []) {
    let coalescedTimer = null;
    let trailingTimer = null;
    const flush = () => {
      coalescedTimer = null;
      scan2();
    };
    const scheduleSoon = () => {
      if (coalescedTimer !== null) return;
      coalescedTimer = setTimeout(flush, 0);
    };
    const onInteraction = () => {
      scheduleSoon();
      clearTimeout(trailingTimer);
      trailingTimer = setTimeout(scan2, 250);
    };
    scan2();
    scope.event(document, "click", onInteraction, true);
    messages.forEach((message) => {
      scope.add(runtime.onMessage(message, scheduleSoon));
    });
    scope.add(() => {
      clearTimeout(coalescedTimer);
      clearTimeout(trailingTimer);
    });
  }
  runtime.features.register({
    id: "itemIconLevel",
    setting: "itemIconLevel",
    initialize({ scope }) {
      scanOnDemand(scope, addItemLevels, ["items_updated"]);
      scope.add(
        () => document.querySelectorAll(".script_itemLevel,.script_key").forEach((node) => node.remove())
      );
    }
  });
  runtime.features.register({
    id: "showsKeyInfoInIcon",
    setting: "showsKeyInfoInIcon",
    dependsOn: ["itemIconLevel"],
    initialize() {
      addItemLevels();
      return () => document.querySelectorAll(".script_key").forEach((node) => node.remove());
    }
  });
  runtime.features.register({
    id: "marketFilter",
    setting: "marketFilter",
    initialize({ scope }) {
      scanOnDemand(scope, addMarketFilterButtons);
      scope.add(() => document.querySelector("#script_filters")?.remove());
    }
  });
  runtime.features.register({
    id: "taskMapIndex",
    setting: "taskMapIndex",
    scope: "character",
    initialize({ scope }) {
      scanOnDemand(scope, handleTaskCard, ["quests_updated"]);
      scope.add(
        () => document.querySelectorAll(".script_taskMapIndex").forEach((node) => node.remove())
      );
    }
  });
  runtime.features.register({
    id: "mapIndex",
    setting: "mapIndex",
    initialize({ scope }) {
      scanOnDemand(scope, addIndexToMaps);
      scope.add(
        () => document.querySelectorAll(".script_mapIndex").forEach((node) => node.remove())
      );
    }
  });

  // src/features/navigation-action-queue.js
  var THIRD_PARTY_LINKS = [
    {
      zh: "插件合集 Q7",
      en: "Plugin collection Q7",
      url: "https://js.nainai.eu.org/"
    },
    {
      zh: "利润网 Polokikiki(含强化模拟)",
      en: "Profit site Polokikiki (incl. enhancement simulator)",
      url: "https://polokikiki.github.io/Milkonomy/#/dashboard"
    },
    {
      zh: "战斗模拟 shykai",
      en: "Combat sim shykai",
      url: "https://shykai.github.io/MWICombatSimulatorTest/dist/"
    },
    {
      zh: "新战斗模拟 Stella",
      en: "New combat sim Stella",
      url: "https://mwisim.org/combat/setup"
    },
    {
      zh: "战斗榜 socko",
      en: "Combat Tracker socko",
      url: "https://sockosnewcombattracker.pages.dev/"
    },
    {
      zh: "人才市场 Shiin",
      en: "Talent market Shiin",
      url: "https://greasyfork.org/zh-CN/scripts/559347-mwi-talent-market"
    }
  ];
  function createMinorNavigationLink(label, onClick) {
    const div = document.createElement("div");
    div.setAttribute("class", "NavigationBar_minorNavigationLink__31K7Y");
    div.dataset.mwitoolsExternalLink = "true";
    div.style.color = runtime.config.SCRIPT_COLOR_MAIN;
    div.textContent = label;
    div.addEventListener("click", onClick);
    return div;
  }
  function add3rdPartyLinks() {
    if (!runtime.settings.get("ThirdPartyLinks")) return;
    const targetNode = document.querySelector(
      "div.NavigationBar_minorNavigationLinks__dbxh7"
    );
    if (!targetNode || targetNode.querySelector('[data-mwitools-external-link="true"]')) {
      return;
    }
    const links = THIRD_PARTY_LINKS.map(
      (link) => createMinorNavigationLink(
        runtime.config.isZH ? link.zh : link.en,
        () => window.open(link.url, "_blank")
      )
    );
    if (runtime.config.isZH) {
      links.push(
        createMinorNavigationLink("牛牛手册", () => {
          window.open(
            "https://test-ctmd6jnzo6t9.feishu.cn/docx/KG9ddER6Eo2uPoxJFkicsvbEnCe",
            "_blank"
          );
        })
      );
    }
    const fragment = document.createDocumentFragment();
    links.forEach((link) => fragment.append(link));
    targetNode.insertBefore(fragment, targetNode.firstChild);
  }
  var activeActionQueueObserver = null;
  var ACTION_QUEUE_MENU_SELECTOR = "div.QueuedActions_queuedActionsEditMenu__3OoQH";
  function disconnectActionQueueObserver(root = null) {
    if (!activeActionQueueObserver) return false;
    if (root && activeActionQueueObserver.menu !== root && !root.contains?.(activeActionQueueObserver.menu)) {
      return false;
    }
    if (activeActionQueueObserver.frameId !== null) {
      (globalThis.cancelAnimationFrame ?? clearTimeout)(
        activeActionQueueObserver.frameId
      );
    }
    clearTimeout(activeActionQueueObserver.retryId);
    activeActionQueueObserver.observer.disconnect();
    activeActionQueueObserver = null;
    return true;
  }
  function scheduleActionQueueRefresh(added, { retry = true } = {}) {
    const active = activeActionQueueObserver;
    if (!active || active.menu !== added) return;
    if (active.frameId !== null) {
      (globalThis.cancelAnimationFrame ?? clearTimeout)(active.frameId);
    }
    clearTimeout(active.retryId);
    const requestFrame = globalThis.requestAnimationFrame ?? ((callback) => setTimeout(callback, 0));
    active.frameId = requestFrame(() => {
      if (activeActionQueueObserver !== active) return;
      active.frameId = null;
      if (!runtime.settings.get("actionQueue") || !added.isConnected) {
        disconnectActionQueueObserver(added);
        return;
      }
      const settled = handleActionQueueMenueCalculateTime(added);
      if (retry && !settled && active.retryCount < 4) {
        active.retryCount += 1;
        active.retryId = setTimeout(
          () => {
            if (activeActionQueueObserver === active && added.isConnected) {
              scheduleActionQueueRefresh(added, { retry: true });
            }
          },
          [50, 100, 200, 350][active.retryCount - 1]
        );
      } else if (settled) {
        active.retryCount = 0;
      }
    });
  }
  function disconnectActionQueueObservers() {
    disconnectActionQueueObserver();
  }
  function handleActionQueueMenue(added) {
    if (!runtime.settings.get("actionQueue")) return;
    const listDiv = added.querySelector(".QueuedActions_actions__2Lur6");
    if (!listDiv) return;
    if (activeActionQueueObserver?.menu === added) {
      activeActionQueueObserver.retryCount = 0;
      scheduleActionQueueRefresh(added);
      return;
    }
    disconnectActionQueueObserver();
    const observer = new MutationObserver(() => {
      if (!runtime.settings.get("actionQueue") || !added.isConnected) {
        disconnectActionQueueObserver(added);
        return;
      }
      scheduleActionQueueRefresh(added);
    });
    activeActionQueueObserver = {
      menu: added,
      observer,
      frameId: null,
      retryId: null,
      retryCount: 0
    };
    observer.observe(listDiv, {
      characterData: false,
      subtree: false,
      childList: true
    });
    handleActionQueueMenueCalculateTime(added);
  }
  function handleActionQueueMenueCalculateTime(added) {
    const actionDivList = added.querySelectorAll(
      "div.QueuedActions_action__r3HlD"
    );
    const actions = [...runtime.state.currentActionsHridList ?? []].sort(
      (left, right) => Number(left?.ordinal ?? 0) - Number(right?.ordinal ?? 0)
    );
    if (!actionDivList.length && actions.length > 1) return false;
    if (actionDivList.length !== Math.max(0, actions.length - 1)) {
      console.error(
        runtime.config.isZH ? "[MWITools] 行动队列提示中的行动数量不一致。" : "[MWITools] Action count mismatch in the action queue tooltip."
      );
      return false;
    }
    let finitePrefixSeconds = 0;
    let blockedBy = "";
    const now = Date.now();
    for (const [index, actionObj] of actions.entries()) {
      const queuedRow = index > 0 ? actionDivList[index - 1] : null;
      const target = queuedRow?.querySelector("div");
      const current = target?.querySelector("div.script_actionTime");
      if (blockedBy) {
        if (queuedRow) {
          const output = current ?? document.createElement("div");
          output.className = "script_actionTime";
          output.style.color = runtime.config.SCRIPT_COLOR_MAIN;
          output.textContent = blockedBy === "infinite" ? runtime.config.isZH ? "前序动作无限,无法预计" : "After an infinite action" : runtime.config.isZH ? "前序动作无法预计" : "After an unavailable estimate";
          if (!current) target?.append(output);
        }
        continue;
      }
      const actionHrid = String(actionObj.actionHrid ?? "");
      const count = actionObj.maxCount - actionObj.currentCount;
      const isInfinite = count === 0 || actionHrid.includes("/combat/");
      const detail = runtime.state.initData_actionDetailMap[actionHrid];
      const timing = detail ? runtime.api.projectActionTiming?.(
        actionHrid,
        isInfinite ? Infinity : count
      ) : null;
      const totalTimeSec = timing?.totalSeconds;
      const unavailable = !Number.isFinite(totalTimeSec);
      const boundary = isInfinite || unavailable;
      if (boundary) blockedBy = isInfinite ? "infinite" : "unavailable";
      else finitePrefixSeconds += totalTimeSec;
      if (queuedRow) {
        const output = current ?? document.createElement("div");
        output.className = "script_actionTime";
        output.style.color = runtime.config.SCRIPT_COLOR_MAIN;
        output.textContent = isInfinite ? "∞" : unavailable ? runtime.config.isZH ? "无法预计" : "Unavailable" : formatRemainingTiming(
          totalTimeSec,
          now + finitePrefixSeconds * 1e3,
          { isZH: runtime.config.isZH, now }
        );
        if (!current) target?.append(output);
      }
    }
    const currentTotal = added.parentElement?.querySelector(
      ":scope > div#script_queueTotalTime"
    );
    const total = currentTotal ?? document.createElement("div");
    total.id = "script_queueTotalTime";
    total.style.color = runtime.config.SCRIPT_COLOR_MAIN;
    const blockedText = blockedBy === "infinite" ? "∞" : runtime.config.isZH ? "无法预计" : "Unavailable";
    const totalText = blockedBy ? finitePrefixSeconds > 0 ? `${formatRemainingDuration(finitePrefixSeconds, runtime.config.isZH)} + ${blockedText}` : blockedText : formatRemainingTiming(
      finitePrefixSeconds,
      now + finitePrefixSeconds * 1e3,
      { isZH: runtime.config.isZH, now }
    );
    total.textContent = `${runtime.config.isZH ? "总时间:" : "Total time: "}${totalText}`;
    if (!currentTotal) added.insertAdjacentElement("afterend", total);
    return true;
  }
  function actionQueueMenuFromNode(node) {
    if (node?.nodeType !== 1) return null;
    if (node.matches?.(ACTION_QUEUE_MENU_SELECTOR)) return node;
    return node.querySelector?.(ACTION_QUEUE_MENU_SELECTOR) ?? null;
  }
  function observeActionQueueMenus(scope) {
    const attach = () => {
      if (!document.body) return false;
      const existing = document.querySelector(ACTION_QUEUE_MENU_SELECTOR);
      if (existing) handleActionQueueMenue(existing);
      const observer = new MutationObserver((records) => {
        for (const record of records) {
          for (const removed of record.removedNodes ?? []) {
            if (activeActionQueueObserver?.menu && (removed === activeActionQueueObserver.menu || removed.contains?.(activeActionQueueObserver.menu))) {
              disconnectActionQueueObserver();
            }
          }
          for (const added of record.addedNodes ?? []) {
            const menu = actionQueueMenuFromNode(added);
            if (menu) handleActionQueueMenue(menu);
          }
        }
      });
      observer.observe(document.body, { childList: true, subtree: true });
      scope.add(() => observer.disconnect());
      return true;
    };
    if (!attach())
      scope.event(document, "DOMContentLoaded", attach, { once: true });
  }
  runtime.onMessage("actions_updated", () => {
    if (activeActionQueueObserver) {
      scheduleActionQueueRefresh(activeActionQueueObserver.menu);
    }
  });
  function getOriTextFromElement(element) {
    if (!element) {
      console.error(
        runtime.config.isZH ? "[MWITools] 无法读取空元素的文字。" : "[MWITools] Cannot read text from a missing element."
      );
      return "";
    }
    return element.getAttribute("script_translatedfrom") || element.textContent;
  }
  Object.assign(runtime.api, {
    add3rdPartyLinks,
    disconnectActionQueueObserver,
    disconnectActionQueueObservers,
    getActiveActionQueueObserverCount: () => activeActionQueueObserver === null ? 0 : 1,
    getOriTextFromElement,
    handleActionQueueMenue,
    handleActionQueueMenueCalculateTime,
    observeActionQueueMenus
  });

  // src/features/enhancement-planner.js
  var ENHANCEMENT_PROFILE = Object.freeze({
    playerLevel: 140,
    houseLevel: 8,
    tool: { hrid: "/items/celestial_enhancer", enhancementLevel: 14 },
    top: { hrid: "/items/enhancers_top", enhancementLevel: 10 },
    bottoms: { hrid: "/items/enhancers_bottoms", enhancementLevel: 10 },
    gloves: { hrid: "/items/enchanted_gloves", enhancementLevel: 10 },
    cape: { hrid: "/items/chance_cape_refined", enhancementLevel: 5 },
    teas: ["/items/ultra_enhancing_tea", "/items/blessed_tea"],
    ultraTeaLevel: 8,
    ultraTeaSpeed: 0.06,
    blessedChance: 0.01,
    houseSpeedPerLevel: 0.01,
    houseSuccessPerLevel: 5e-4,
    baseActionSeconds: 12,
    teaDurationSeconds: 300
  });
  var DEFAULT_SUCCESS_RATES = [
    0.5,
    0.45,
    0.45,
    0.4,
    0.4,
    0.4,
    0.35,
    0.35,
    0.35,
    0.35,
    0.3,
    0.3,
    0.3,
    0.3,
    0.3,
    0.3,
    0.3,
    0.3,
    0.3,
    0.3
  ];
  var DEFAULT_BONUS_MULTIPLIERS = [
    0,
    1,
    2.1,
    3.3,
    4.6,
    6,
    7.5,
    9.1,
    10.8,
    12.6,
    14.5,
    16.7,
    19.2,
    22,
    25.1,
    28.5,
    32.2,
    36.2,
    40.5,
    45.1,
    50
  ];
  var EPSILON2 = 1e-9;
  var ENHANCEMENT_FLOW_CACHE_LIMIT = 4096;
  var enhancementFlowCache = /* @__PURE__ */ new Map();
  var enhancementFlowCacheWeight = 0;
  function finitePositive(value) {
    const number3 = Number(value);
    return Number.isFinite(number3) && number3 > 0 ? number3 : 0;
  }
  function normalizedTable(source, fallback) {
    if (!source) return fallback;
    const values = Array.isArray(source) ? source : Object.keys(source).sort((left, right) => Number(left) - Number(right)).map((key) => source[key]);
    return values.length ? values.map(Number) : fallback;
  }
  function entriesOfMap2(value) {
    return value instanceof Map ? [...value.entries()] : Object.entries(value ?? {});
  }
  function nonTradableCoinShopPrice(itemHrid, itemDetailMap, shopItemDetailMap) {
    if (itemDetailMap?.[itemHrid]?.isTradable === true) return 0;
    let best = Number.POSITIVE_INFINITY;
    for (const [, detail] of entriesOfMap2(shopItemDetailMap)) {
      if (detail?.itemHrid !== itemHrid) continue;
      const costs = Array.isArray(detail.costs) ? detail.costs : [];
      if (costs.length !== 1 || costs[0]?.itemHrid !== "/items/coin" || !(Number(costs[0]?.count) > 0)) {
        continue;
      }
      best = Math.min(best, Number(costs[0].count));
    }
    return Number.isFinite(best) ? best : 0;
  }
  function charmRecipe(itemHrid, actionDetailMap) {
    return entriesOfMap2(actionDetailMap).map(([fallbackHrid, detail]) => ({
      actionHrid: detail?.hrid ?? fallbackHrid,
      detail
    })).find(
      ({ detail }) => detail?.outputItems?.some((output) => output.itemHrid === itemHrid)
    );
  }
  function projectedCharmRecipe(recipe, itemHrid, projectAction2) {
    if (!recipe || typeof projectAction2 !== "function") return null;
    const projection = projectAction2(recipe.actionHrid, 1, {
      respectInventoryLimit: false
    });
    if (!["complete", "incomplete"].includes(projection?.status) || !Array.isArray(projection.inputs)) {
      return null;
    }
    const outputCount = projection.outputs?.filter((output) => output.itemHrid === itemHrid).reduce(
      (total, output) => total + finitePositive(output.effectiveCount ?? output.count),
      0
    );
    const fallbackOutputCount = recipe.detail.outputItems.filter((output) => output.itemHrid === itemHrid).reduce((total, output) => total + finitePositive(output.count), 0);
    const actionsPerHour = finitePositive(projection.actionsPerHour);
    const teaInputs = [];
    for (const drink of projection.teaEffects?.drinks ?? []) {
      const count = actionsPerHour ? finitePositive(drink.countPerHour) / actionsPerHour : 0;
      if (drink.itemHrid && count > 0) {
        teaInputs.push({ itemHrid: drink.itemHrid, count });
      }
    }
    return {
      outputCount: finitePositive(outputCount) || fallbackOutputCount,
      inputs: projection.inputs.map((input) => ({
        itemHrid: input.itemHrid,
        count: finitePositive(input.effectiveCount)
      })),
      teaInputs
    };
  }
  function charmBaseCost({
    itemHrid,
    actionDetailMap,
    projectAction: projectAction2,
    resolveLeafPrice,
    visited = /* @__PURE__ */ new Set()
  }) {
    if (!itemHrid || visited.has(itemHrid)) return 0;
    const recipe = charmRecipe(itemHrid, actionDetailMap);
    if (!recipe) return resolveLeafPrice(itemHrid);
    const projected = projectedCharmRecipe(recipe, itemHrid, projectAction2);
    if (!projected?.outputCount) return 0;
    const nextVisited = new Set(visited).add(itemHrid);
    let totalCost = 0;
    for (const input of projected.inputs) {
      if (!input.itemHrid || !(input.count > 0)) continue;
      const unitPrice = input.itemHrid.endsWith("_charm") ? charmBaseCost({
        itemHrid: input.itemHrid,
        actionDetailMap,
        projectAction: projectAction2,
        resolveLeafPrice,
        visited: nextVisited
      }) : resolveLeafPrice(input.itemHrid);
      if (!(unitPrice > 0)) return 0;
      totalCost += input.count * unitPrice;
    }
    for (const tea of projected.teaInputs) {
      const unitPrice = resolveLeafPrice(tea.itemHrid);
      if (!(unitPrice > 0)) return 0;
      totalCost += tea.count * unitPrice;
    }
    return totalCost > 0 ? totalCost / projected.outputCount : 0;
  }
  function successRateAt(table, level) {
    const value = Number(table[level] ?? table.at(-1));
    if (!Number.isFinite(value)) return 0;
    return value > 1 ? value / 100 : value;
  }
  function enhancementFlowProfileKey({
    targetLevel,
    successRates,
    successBonus,
    blessedChance
  }) {
    const target = Math.max(0, Math.floor(Number(targetLevel) || 0));
    const bonus = Number(successBonus) || 0;
    const blessed = Number(blessedChance) || 0;
    const effectiveSuccessRates = Array.from(
      { length: target },
      (_, level) => Math.min(1, successRateAt(successRates, level) * (1 + bonus))
    );
    return `${target}|${blessed}|${effectiveSuccessRates.join(",")}`;
  }
  function freezeEnhancementFlow(flow) {
    if (!flow) return null;
    return Object.freeze({
      ...flow,
      actionsByLevel: Object.freeze([...flow.actionsByLevel ?? []])
    });
  }
  function cloneEnhancementFlow(flow) {
    return flow ? { ...flow, actionsByLevel: [...flow.actionsByLevel ?? []] } : null;
  }
  function cachedEnhancementValue(key, weight, calculate2) {
    if (enhancementFlowCache.has(key)) {
      const cached = enhancementFlowCache.get(key);
      enhancementFlowCache.delete(key);
      enhancementFlowCache.set(key, cached);
      return cached.value;
    }
    const normalizedWeight = Math.max(1, Math.floor(Number(weight) || 1));
    const value = calculate2();
    enhancementFlowCache.set(key, { value, weight: normalizedWeight });
    enhancementFlowCacheWeight += normalizedWeight;
    while (enhancementFlowCacheWeight > ENHANCEMENT_FLOW_CACHE_LIMIT) {
      const oldestKey = enhancementFlowCache.keys().next().value;
      const oldest = enhancementFlowCache.get(oldestKey);
      enhancementFlowCache.delete(oldestKey);
      enhancementFlowCacheWeight -= oldest?.weight ?? 0;
    }
    return value;
  }
  function cachedEnhancementFlow(key, calculate2) {
    return cachedEnhancementValue(
      key,
      1,
      () => freezeEnhancementFlow(calculate2())
    );
  }
  function solveLinearSystem(matrix, vector) {
    const size = matrix.length;
    const augmented = matrix.map((row, index) => [
      ...row.map(Number),
      Number(vector[index])
    ]);
    for (let column = 0; column < size; column++) {
      let pivot = column;
      for (let row = column + 1; row < size; row++) {
        if (Math.abs(augmented[row][column]) > Math.abs(augmented[pivot][column])) {
          pivot = row;
        }
      }
      if (Math.abs(augmented[pivot][column]) < 1e-12) return null;
      if (pivot !== column) {
        [augmented[pivot], augmented[column]] = [
          augmented[column],
          augmented[pivot]
        ];
      }
      const divisor = augmented[column][column];
      for (let index = column; index <= size; index++) {
        augmented[column][index] /= divisor;
      }
      for (let row = 0; row < size; row++) {
        if (row === column) continue;
        const factor = augmented[row][column];
        if (Math.abs(factor) < 1e-15) continue;
        for (let index = column; index <= size; index++) {
          augmented[row][index] -= factor * augmented[column][index];
        }
      }
    }
    const result = augmented.map((row) => row[size]);
    return result.every(Number.isFinite) ? result : null;
  }
  function equipmentStat(itemMap, bonusTable, equipment, stat) {
    const detail = itemMap?.[equipment.hrid]?.equipmentDetail;
    const base = Number(detail?.noncombatStats?.[stat]);
    const perMultiplier = Number(
      detail?.noncombatEnhancementBonuses?.[stat] ?? 0
    );
    const multiplier = Number(bonusTable[equipment.enhancementLevel]);
    if (!Number.isFinite(base) || !Number.isFinite(multiplier)) return null;
    return base + perMultiplier * multiplier;
  }
  function getEnhancementProfileStats({
    itemLevel,
    itemDetailMap = runtime.state.initData_itemDetailMap,
    bonusMultiplierTable = runtime.state.initData_enhancementLevelTotalBonusMultiplierTable
  } = {}) {
    const bonusTable = normalizedTable(
      bonusMultiplierTable,
      DEFAULT_BONUS_MULTIPLIERS
    );
    const toolSuccess = equipmentStat(
      itemDetailMap,
      bonusTable,
      ENHANCEMENT_PROFILE.tool,
      "enhancingSuccess"
    );
    const gloveSpeed = equipmentStat(
      itemDetailMap,
      bonusTable,
      ENHANCEMENT_PROFILE.gloves,
      "enhancingSpeed"
    );
    const topSpeed = equipmentStat(
      itemDetailMap,
      bonusTable,
      ENHANCEMENT_PROFILE.top,
      "enhancingSpeed"
    );
    const bottomsSpeed = equipmentStat(
      itemDetailMap,
      bonusTable,
      ENHANCEMENT_PROFILE.bottoms,
      "enhancingSpeed"
    );
    const capeSpeed = equipmentStat(
      itemDetailMap,
      bonusTable,
      ENHANCEMENT_PROFILE.cape,
      "enhancingSpeed"
    );
    const targetItemLevel = Number(itemLevel);
    if (toolSuccess === null || topSpeed === null || bottomsSpeed === null || gloveSpeed === null || capeSpeed === null || !Number.isFinite(targetItemLevel) || targetItemLevel <= 0) {
      return null;
    }
    const effectiveLevel = ENHANCEMENT_PROFILE.playerLevel + ENHANCEMENT_PROFILE.ultraTeaLevel;
    const levelSuccess = effectiveLevel >= targetItemLevel ? (effectiveLevel - targetItemLevel) * 5e-4 : -0.5 * (1 - effectiveLevel / targetItemLevel);
    const successBonus = levelSuccess + toolSuccess + ENHANCEMENT_PROFILE.houseLevel * ENHANCEMENT_PROFILE.houseSuccessPerLevel;
    const speedBonus = gloveSpeed + topSpeed + bottomsSpeed + capeSpeed + ENHANCEMENT_PROFILE.houseLevel * ENHANCEMENT_PROFILE.houseSpeedPerLevel + ENHANCEMENT_PROFILE.ultraTeaSpeed + Math.max(0, effectiveLevel - targetItemLevel) * 0.01;
    return {
      effectiveLevel,
      toolSuccess,
      gloveSpeed,
      topSpeed,
      bottomsSpeed,
      capeSpeed,
      successBonus,
      speedBonus,
      blessedChance: ENHANCEMENT_PROFILE.blessedChance,
      secondsPerAction: ENHANCEMENT_PROFILE.baseActionSeconds / (1 + speedBonus)
    };
  }
  function addTransition(matrix, from, to, rate, targetLevel) {
    if (rate <= 0 || to >= targetLevel) return;
    matrix[to][from] -= rate;
  }
  function calculateNormalEnhancementFlowUncached({
    targetLevel,
    protectLevel,
    successRates = DEFAULT_SUCCESS_RATES,
    successBonus,
    blessedChance
  }) {
    if (targetLevel < 1) return null;
    const matrix = Array.from(
      { length: targetLevel },
      (_, row) => Array.from(
        { length: targetLevel },
        (_2, column) => row === column ? 1 : 0
      )
    );
    const source = Array(targetLevel).fill(0);
    source[0] = 1;
    const failRates = [];
    for (let level = 0; level < targetLevel; level++) {
      const success = Math.min(
        1,
        successRateAt(successRates, level) * (1 + successBonus)
      );
      const fail = Math.max(0, 1 - success);
      failRates[level] = fail;
      addTransition(
        matrix,
        level,
        level + 1,
        success * (1 - blessedChance),
        targetLevel
      );
      addTransition(
        matrix,
        level,
        level + 2,
        success * blessedChance,
        targetLevel
      );
      const failLevel = level >= protectLevel ? Math.max(0, level - 1) : 0;
      addTransition(matrix, level, failLevel, fail, targetLevel);
    }
    const actionsByLevel = solveLinearSystem(matrix, source);
    if (!actionsByLevel || actionsByLevel.some((value) => value < -EPSILON2 || !Number.isFinite(value))) {
      return null;
    }
    const normalizedActions = actionsByLevel.map(
      (value) => Math.abs(value) < EPSILON2 ? 0 : value
    );
    const protectionCount = normalizedActions.reduce(
      (sum, actions, level) => sum + (level >= protectLevel ? actions * failRates[level] : 0),
      0
    );
    return {
      actionsByLevel: normalizedActions,
      totalActions: normalizedActions.reduce((sum, value) => sum + value, 0),
      protectionCount
    };
  }
  function getCachedNormalEnhancementFlow(options, profileKeys = null) {
    const protectLevel = Math.max(
      0,
      Math.floor(Number(options.protectLevel) || 0)
    );
    const targetLevel = Math.max(0, Math.floor(Number(options.targetLevel) || 0));
    let profileKey = profileKeys?.get(targetLevel);
    if (!profileKey) {
      profileKey = enhancementFlowProfileKey(options);
      profileKeys?.set(targetLevel, profileKey);
    }
    return cachedEnhancementFlow(
      `normal|${protectLevel}|${profileKey}`,
      () => calculateNormalEnhancementFlowUncached(options)
    );
  }
  function calculateNormalEnhancementFlow(options) {
    return cloneEnhancementFlow(
      getCachedNormalEnhancementFlow(options, /* @__PURE__ */ new Map())
    );
  }
  function calculateMirrorRequirements(targetLevel, philosopherStartLevel) {
    const requirements = Array(targetLevel + 1).fill(0);
    const actionsByLevel = Array(targetLevel).fill(0);
    requirements[targetLevel] = 1;
    for (let level = targetLevel - 1; level >= philosopherStartLevel; level--) {
      const actions = requirements[level + 1];
      actionsByLevel[level] = actions;
      requirements[level] += actions;
      requirements[level - 1] += actions;
    }
    const aCount = requirements[philosopherStartLevel];
    const bCount = requirements[philosopherStartLevel - 1];
    return {
      actionsByLevel,
      aCount,
      bCount,
      mirrorCount: aCount + bCount - 1
    };
  }
  function calculatePhilosopherEnhancementFlowUncached({
    targetLevel,
    protectLevel,
    philosopherStartLevel,
    successRates = DEFAULT_SUCCESS_RATES,
    successBonus,
    blessedChance
  }, resolveNormalFlow) {
    if (targetLevel <= 1 || philosopherStartLevel < 1 || philosopherStartLevel >= targetLevel) {
      return null;
    }
    const mirror = calculateMirrorRequirements(
      targetLevel,
      philosopherStartLevel
    );
    const aFlow = resolveNormalFlow({
      targetLevel: philosopherStartLevel,
      protectLevel,
      successRates,
      successBonus,
      blessedChance
    });
    const bFlow = philosopherStartLevel > 1 ? resolveNormalFlow({
      targetLevel: philosopherStartLevel - 1,
      protectLevel,
      successRates,
      successBonus,
      blessedChance
    }) : { actionsByLevel: [], totalActions: 0, protectionCount: 0 };
    if (!aFlow || !bFlow) return null;
    const actionsByLevel = [...mirror.actionsByLevel];
    for (let level = 0; level < philosopherStartLevel; level++) {
      actionsByLevel[level] = mirror.aCount * (aFlow.actionsByLevel[level] ?? 0) + mirror.bCount * (bFlow.actionsByLevel[level] ?? 0);
    }
    const normalActions = mirror.aCount * aFlow.totalActions + mirror.bCount * bFlow.totalActions;
    const protectionCount = mirror.aCount * aFlow.protectionCount + mirror.bCount * bFlow.protectionCount;
    return {
      actionsByLevel,
      baseItemCount: mirror.aCount + mirror.bCount,
      mirrorCount: mirror.mirrorCount,
      protectionCount,
      totalActions: normalActions + mirror.mirrorCount,
      aCount: mirror.aCount,
      bCount: mirror.bCount
    };
  }
  function getCachedPhilosopherEnhancementFlow(options, profileKeys = null) {
    const protectLevel = Math.max(
      0,
      Math.floor(Number(options.protectLevel) || 0)
    );
    const philosopherStartLevel = Math.max(
      0,
      Math.floor(Number(options.philosopherStartLevel) || 0)
    );
    const targetLevel = Math.max(0, Math.floor(Number(options.targetLevel) || 0));
    let profileKey = profileKeys?.get(targetLevel);
    if (!profileKey) {
      profileKey = enhancementFlowProfileKey(options);
      profileKeys?.set(targetLevel, profileKey);
    }
    return cachedEnhancementFlow(
      `philosopher|${protectLevel}|${philosopherStartLevel}|${profileKey}`,
      () => calculatePhilosopherEnhancementFlowUncached(
        options,
        (normalOptions) => getCachedNormalEnhancementFlow(normalOptions, profileKeys)
      )
    );
  }
  function calculatePhilosopherEnhancementFlow(options) {
    return cloneEnhancementFlow(
      getCachedPhilosopherEnhancementFlow(options, /* @__PURE__ */ new Map())
    );
  }
  function getCachedEnhancementFlowTable({
    targetLevel,
    successRates,
    successBonus,
    blessedChance
  }) {
    const target = Math.max(0, Math.floor(Number(targetLevel) || 0));
    const profileKey = enhancementFlowProfileKey({
      targetLevel: target,
      successRates,
      successBonus,
      blessedChance
    });
    const flowCount = target + target * (target - 1) / 2;
    return cachedEnhancementValue(`table|${profileKey}`, flowCount, () => {
      const localNormalFlows = Array.from({ length: target + 1 }, () => []);
      const resolveNormalFlow = (options) => {
        const flowTarget = Math.max(
          0,
          Math.floor(Number(options.targetLevel) || 0)
        );
        const protectLevel = Math.max(
          0,
          Math.floor(Number(options.protectLevel) || 0)
        );
        if (localNormalFlows[flowTarget][protectLevel] === void 0) {
          localNormalFlows[flowTarget][protectLevel] = calculateNormalEnhancementFlowUncached(options);
        }
        return localNormalFlows[flowTarget][protectLevel];
      };
      const normal = Array(target + 1).fill(null);
      for (let protectLevel = 1; protectLevel <= target; protectLevel++) {
        normal[protectLevel] = resolveNormalFlow({
          targetLevel: target,
          protectLevel,
          successRates,
          successBonus,
          blessedChance
        });
      }
      const philosopher = Array.from({ length: target }, () => []);
      for (let philosopherStartLevel = 1; philosopherStartLevel < target; philosopherStartLevel++) {
        for (let protectLevel = 1; protectLevel <= philosopherStartLevel; protectLevel++) {
          philosopher[philosopherStartLevel][protectLevel] = calculatePhilosopherEnhancementFlowUncached(
            {
              targetLevel: target,
              protectLevel,
              philosopherStartLevel,
              successRates,
              successBonus,
              blessedChance
            },
            resolveNormalFlow
          );
        }
      }
      return { normal, philosopher };
    });
  }
  function unavailableResult(missingMarketValues = []) {
    return {
      status: "unavailable",
      totalCost: null,
      baseCost: null,
      refinementCost: null,
      totalSeconds: null,
      normalProtectStart: null,
      expectedProtectionCount: null,
      expectedNormalProtectionCount: null,
      expectedPhilosopherMirrorCount: null,
      philosopherStart: null,
      aLevel: null,
      aCount: null,
      bLevel: null,
      bCount: null,
      missingMarketValues: [...new Set(missingMarketValues)]
    };
  }
  function refinementRecipe(itemHrid, baseItemHrid, actionDetailMap) {
    if (itemHrid === baseItemHrid) return { actionHrid: "", inputItems: [] };
    const actions = actionDetailMap instanceof Map ? [...actionDetailMap.entries()] : Object.entries(actionDetailMap ?? {});
    const match = actions.find(
      ([, detail]) => detail?.upgradeItemHrid === baseItemHrid && detail?.outputItems?.some((output) => output.itemHrid === itemHrid)
    );
    if (!match) return null;
    const [fallbackHrid, action] = match;
    return {
      actionHrid: action.hrid ?? fallbackHrid,
      inputItems: action.inputItems ?? []
    };
  }
  function refinementCostComponents(recipe, projectAction2) {
    const rawInputs = recipe.inputItems.map((input) => ({
      itemHrid: input.itemHrid,
      count: Number(input.count) || 0
    }));
    if (!recipe.actionHrid || typeof projectAction2 !== "function") {
      return rawInputs;
    }
    const projection = projectAction2(recipe.actionHrid, 1, {
      respectInventoryLimit: false
    });
    if (!["complete", "incomplete"].includes(projection?.status) || !Array.isArray(projection.inputs)) {
      return rawInputs;
    }
    const components = projection.inputs.filter((input) => !input.isUpgradeItem).map((input) => ({
      itemHrid: input.itemHrid,
      count: Number(input.effectiveCount) || 0
    }));
    const actionsPerHour = finitePositive(projection.actionsPerHour);
    if (actionsPerHour) {
      for (const drink of projection.teaEffects?.drinks ?? []) {
        const count = finitePositive(drink.countPerHour) / actionsPerHour;
        if (drink.itemHrid && count > 0) {
          components.push({ itemHrid: drink.itemHrid, count });
        }
      }
    }
    return components;
  }
  function calculateEnhancementPlan({
    itemHrid,
    targetLevel,
    itemDetailMap = runtime.state.initData_itemDetailMap,
    successRateTable = runtime.state.initData_enhancementLevelSuccessRateTable,
    bonusMultiplierTable = runtime.state.initData_enhancementLevelTotalBonusMultiplierTable,
    actionDetailMap = runtime.state.initData_actionDetailMap,
    shopItemDetailMap = runtime.state.initData_shopItemDetailMap,
    getFairValue: getFairValue2 = runtime.api.getFairValue,
    getMarketValue = getFairValue2,
    projectAction: projectAction2 = runtime.api.projectAction,
    forcedProtectionItemHrid = null,
    allowPhilosopherMirror = true
  } = {}) {
    const target = Math.max(0, Math.floor(Number(targetLevel) || 0));
    const baseItemHrid = itemHrid.endsWith("_refined") ? itemHrid.replace("_refined", "") : itemHrid;
    const item = itemDetailMap?.[baseItemHrid];
    const refiningRecipe = refinementRecipe(
      itemHrid,
      baseItemHrid,
      actionDetailMap
    );
    if (refiningRecipe === null) return unavailableResult();
    if (!item?.enhancementCosts?.length || target < 1) return unavailableResult();
    const stats = getEnhancementProfileStats({
      itemLevel: item.itemLevel,
      itemDetailMap,
      bonusMultiplierTable
    });
    if (!stats) return unavailableResult();
    const missing = /* @__PURE__ */ new Set();
    const resolvePrice = (resolver, hrid, level = 0) => {
      if (hrid === "/items/coin") return 1;
      const value = finitePositive(resolver?.(hrid, level));
      if (!value) missing.add(hrid);
      return value;
    };
    const acquisitionPrice = (hrid, level = 0) => resolvePrice(getFairValue2, hrid, level);
    const marketPrice = (hrid, level = 0) => resolvePrice(getMarketValue, hrid, level);
    const optionalMarketPrice = (hrid, level = 0) => {
      if (hrid === "/items/coin") return 1;
      return finitePositive(getMarketValue?.(hrid, level));
    };
    const resolveCharmLeafPrice = (hrid) => {
      const value = optionalMarketPrice(hrid, 0) || nonTradableCoinShopPrice(hrid, itemDetailMap, shopItemDetailMap);
      if (!value) missing.add(hrid);
      return value;
    };
    const basePrice = baseItemHrid.endsWith("_charm") ? charmBaseCost({
      itemHrid: baseItemHrid,
      actionDetailMap,
      projectAction: projectAction2,
      resolveLeafPrice: resolveCharmLeafPrice
    }) : acquisitionPrice(baseItemHrid, 0);
    if (!basePrice && baseItemHrid.endsWith("_charm")) {
      missing.add(baseItemHrid);
    }
    let materialCostPerAction = 0;
    let hasMissingRequiredPrice = !basePrice;
    for (const cost of item.enhancementCosts) {
      const unitPrice = optionalMarketPrice(cost.itemHrid, 0) || nonTradableCoinShopPrice(cost.itemHrid, itemDetailMap, shopItemDetailMap);
      if (!unitPrice) {
        missing.add(cost.itemHrid);
        hasMissingRequiredPrice = true;
      }
      materialCostPerAction += unitPrice * Number(cost.count || 0);
    }
    let refinementCost = 0;
    for (const cost of refinementCostComponents(refiningRecipe, projectAction2)) {
      const unitPrice = acquisitionPrice(cost.itemHrid, 0);
      if (!unitPrice) hasMissingRequiredPrice = true;
      refinementCost += unitPrice * Number(cost.count || 0);
    }
    const ultraTeaPrice = marketPrice("/items/ultra_enhancing_tea", 0);
    const blessedTeaPrice = marketPrice("/items/blessed_tea", 0);
    if (!ultraTeaPrice || !blessedTeaPrice) hasMissingRequiredPrice = true;
    if (hasMissingRequiredPrice) return unavailableResult([...missing]);
    let protectionChoice = null;
    const considerProtection = (hrid, value) => {
      if (hrid && value > 0 && (!protectionChoice || value < protectionChoice.value)) {
        protectionChoice = { hrid, value };
      }
    };
    if (forcedProtectionItemHrid) {
      considerProtection(
        forcedProtectionItemHrid,
        optionalMarketPrice(forcedProtectionItemHrid, 0)
      );
    } else {
      considerProtection(baseItemHrid, optionalMarketPrice(baseItemHrid, 0));
      for (const candidate of new Set(item.protectionItemHrids ?? [])) {
        considerProtection(candidate, optionalMarketPrice(candidate, 0));
      }
      considerProtection(
        "/items/mirror_of_protection",
        optionalMarketPrice("/items/mirror_of_protection", 0)
      );
    }
    const protectionPrice = protectionChoice?.value ?? 0;
    const philosopherMirrorPrice = marketPrice("/items/philosophers_mirror", 0);
    const successRates = normalizedTable(successRateTable, DEFAULT_SUCCESS_RATES);
    const ultraTeaCostPerAction = stats.secondsPerAction / ENHANCEMENT_PROFILE.teaDurationSeconds * ultraTeaPrice;
    const blessedTeaCostPerNormalAction = stats.secondsPerAction / ENHANCEMENT_PROFILE.teaDurationSeconds * blessedTeaPrice;
    const normalActionCost = materialCostPerAction + ultraTeaCostPerAction + blessedTeaCostPerNormalAction;
    let best = null;
    const flowTable = getCachedEnhancementFlowTable({
      targetLevel: target,
      successRates,
      successBonus: stats.successBonus,
      blessedChance: stats.blessedChance
    });
    for (let protectLevel = 1; protectLevel <= target; protectLevel++) {
      const flow = flowTable.normal[protectLevel];
      if (!flow) continue;
      if (flow.protectionCount > EPSILON2 && !protectionPrice) continue;
      const totalCost = basePrice + flow.totalActions * normalActionCost + flow.protectionCount * protectionPrice;
      if (!best || totalCost < best.totalCost) {
        best = {
          mode: "normal",
          totalCost,
          totalActions: flow.totalActions,
          protectionCount: flow.protectionCount,
          mirrorCount: 0,
          protectLevel,
          philosopherStartLevel: null,
          aCount: 0,
          bCount: 0
        };
      }
    }
    if (allowPhilosopherMirror && philosopherMirrorPrice > 0) {
      for (let philosopherStartLevel = 1; philosopherStartLevel < target; philosopherStartLevel++) {
        for (let protectLevel = 1; protectLevel <= philosopherStartLevel; protectLevel++) {
          const flow = flowTable.philosopher[philosopherStartLevel][protectLevel];
          if (!flow || flow.baseItemCount < -EPSILON2) continue;
          if (flow.protectionCount > EPSILON2 && !protectionPrice) continue;
          const totalCost = flow.baseItemCount * basePrice + flow.totalActions * (materialCostPerAction + ultraTeaCostPerAction) + (flow.totalActions - flow.mirrorCount) * blessedTeaCostPerNormalAction + flow.protectionCount * protectionPrice + flow.mirrorCount * philosopherMirrorPrice;
          if (!best || totalCost < best.totalCost) {
            best = {
              mode: "philosopher",
              totalCost,
              totalActions: flow.totalActions,
              protectionCount: flow.protectionCount,
              mirrorCount: flow.mirrorCount,
              protectLevel,
              philosopherStartLevel,
              aCount: flow.aCount,
              bCount: flow.bCount
            };
          }
        }
      }
    }
    if (!best) return unavailableResult([...missing]);
    return {
      status: "complete",
      totalCost: best.totalCost + refinementCost,
      baseCost: basePrice,
      refinementCost,
      totalSeconds: best.totalActions * stats.secondsPerAction,
      normalProtectStart: best.protectionCount > EPSILON2 ? best.protectLevel : null,
      expectedProtectionCount: best.mode === "philosopher" ? best.mirrorCount : best.protectionCount,
      expectedNormalProtectionCount: best.protectionCount,
      expectedPhilosopherMirrorCount: best.mirrorCount,
      protectionItemHrid: best.protectionCount > EPSILON2 ? protectionChoice?.hrid ?? null : null,
      protectionUnitCost: best.protectionCount > EPSILON2 ? protectionPrice : 0,
      philosopherStart: best.philosopherStartLevel,
      aLevel: best.philosopherStartLevel,
      aCount: best.aCount,
      bLevel: best.philosopherStartLevel === null ? null : best.philosopherStartLevel - 1,
      bCount: best.bCount,
      missingMarketValues: [...missing]
    };
  }
  Object.assign(runtime.api, {
    calculateEnhancementPlan,
    calculateNormalEnhancementFlow,
    calculatePhilosopherEnhancementFlow,
    getEnhancementProfileStats
  });

  // src/features/enhancement-cost-panel.js
  var PANEL_ID4 = "mwitools-enhancement-cost-panel";
  var STYLE_ID17 = "mwitools-enhancement-cost-panel-style";
  var VIEWPORT_MARGIN3 = 12;
  var PANEL_GAP3 = 8;
  var activePanel2 = null;
  function t18(zh, en) {
    return runtime.config.isZH ? zh : en;
  }
  function addStyles15() {
    if (document.getElementById(STYLE_ID17)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID17;
    style.textContent = `
    #${PANEL_ID4} { position:fixed; z-index:2147483000; width:min(252px,calc(100vw - 24px)); box-sizing:border-box; overflow:hidden; pointer-events:none; color:var(--color-text-primary,#eef1f6); border:1px solid rgba(255,255,255,.16); border-radius:8px; background:linear-gradient(145deg,rgba(34,38,47,.985),rgba(18,21,27,.985)); box-shadow:0 12px 34px rgba(0,0,0,.44),0 2px 7px rgba(0,0,0,.28); font-family:inherit; font-size:calc(11px * var(--mwi-hover-font-scale,1)); line-height:1.25; }
    #${PANEL_ID4}.mwi-enhancement-sticky { pointer-events:auto; }
    #${PANEL_ID4} * { box-sizing:border-box; }
    .mwi-enhancement-grid { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; }
    .mwi-enhancement-metric { display:contents; }
    .mwi-enhancement-label,.mwi-enhancement-value { min-height:27px; display:flex; align-items:center; padding:5px 9px; border-bottom:1px solid rgba(255,255,255,.075); }
    .mwi-enhancement-metric:last-child .mwi-enhancement-label,.mwi-enhancement-metric:last-child .mwi-enhancement-value { border-bottom:0; }
    .mwi-enhancement-label { min-width:0; color:var(--color-text-secondary,#aeb5c0); }
    .mwi-enhancement-value { justify-content:flex-end; color:#fff; font-weight:650; font-variant-numeric:tabular-nums; white-space:nowrap; }
    .mwi-enhancement-protection .mwi-enhancement-label { display:none; }
    .mwi-enhancement-protection .mwi-enhancement-value { grid-column:1/-1; justify-content:flex-start; white-space:normal; }
  `;
    document.head.append(style);
  }
  function exactTitle(value) {
    if (!Number.isFinite(Number(value))) return "";
    return runtime.api.formatExactNumber?.(Number(value)) ?? String(value);
  }
  function compactNumber(value, digits = 1) {
    if (!Number.isFinite(Number(value))) return "—";
    if (Math.abs(Number(value)) >= 1e3) {
      return runtime.api.numberFormatter?.(Number(value), digits) ?? String(value);
    }
    return new Intl.NumberFormat(runtime.config.NUMBER_LOCALE || "en-US", {
      minimumFractionDigits: digits,
      maximumFractionDigits: digits
    }).format(Number(value));
  }
  function countWithUnit(value) {
    const number3 = Number(value);
    if (!Number.isFinite(number3)) return "—";
    const rounded = Math.round(number3);
    const digits = Math.abs(number3 - rounded) < 1e-8 ? 0 : 1;
    return `${compactNumber(number3, digits)} ${t18("个", "pcs")}`;
  }
  function metric3(label, value, exactValue = null, titleText = "") {
    const row = document.createElement("div");
    row.className = "mwi-enhancement-metric";
    const caption = document.createElement("div");
    caption.className = "mwi-enhancement-label";
    caption.textContent = label;
    const content = document.createElement("div");
    content.className = "mwi-enhancement-value";
    content.textContent = value;
    const title = titleText || exactTitle(exactValue);
    if (title) content.title = title;
    row.append(caption, content);
    return row;
  }
  function protectionUsage(plan) {
    const normal = Number(plan?.expectedNormalProtectionCount);
    const mirror = Number(plan?.expectedPhilosopherMirrorCount);
    if (!Number.isFinite(normal) || !Number.isFinite(mirror)) {
      return { text: "—", title: "" };
    }
    return {
      text: t18(
        `普通保护 ${compactNumber(normal, 1)} 次,贤者之镜 ${compactNumber(mirror, 1)} 次`,
        `Regular protection: ${compactNumber(normal, 1)} uses; Philosopher's Mirror: ${compactNumber(mirror, 1)} uses`
      ),
      title: t18(
        `普通保护:${exactTitle(normal)} 次;贤者之镜:${exactTitle(mirror)} 次`,
        `Regular protection: ${exactTitle(normal)} uses; Philosopher's Mirror: ${exactTitle(mirror)} uses`
      )
    };
  }
  function renderPanel2(panel, plan) {
    const complete = plan?.status === "complete";
    const protection = complete ? protectionUsage(plan) : { text: "—", title: "" };
    const normalStart = complete ? plan.normalProtectStart === null ? t18("不用", "None") : `+${plan.normalProtectStart}` : "—";
    const philosopherStart = complete ? plan.philosopherStart === null ? t18("不用", "None") : `+${plan.philosopherStart}` : "—";
    const aLabel = complete && plan.aLevel !== null ? t18(`需要 +${plan.aLevel}`, `Need +${plan.aLevel}`) : t18("需要", "Need");
    const bLabel = complete && plan.bLevel !== null ? t18(`需要 +${plan.bLevel}`, `Need +${plan.bLevel}`) : t18("需要", "Need");
    const grid = document.createElement("div");
    grid.className = "mwi-enhancement-grid";
    const protectionMetric = metric3("", protection.text, null, protection.title);
    protectionMetric.classList.add("mwi-enhancement-protection");
    grid.append(
      metric3(
        t18("总成本", "Total cost"),
        complete ? compactNumber(plan.totalCost, 1) : "—",
        plan?.totalCost
      ),
      metric3(
        t18("底子成本", "Base cost"),
        complete ? compactNumber(plan.baseCost, 1) : "—",
        plan?.baseCost
      ),
      ...complete && Number(plan.refinementCost) > 0 ? [
        metric3(
          t18("其中精炼", "Includes refining"),
          compactNumber(plan.refinementCost, 1),
          plan.refinementCost
        )
      ] : [],
      metric3(
        t18("耗时", "Time"),
        complete ? runtime.api.timeReadable(plan.totalSeconds) : "—",
        plan?.totalSeconds
      ),
      metric3(t18("开始保护", "Protect from"), normalStart),
      protectionMetric,
      metric3(t18("开始贤者保护", "Philosopher's Mirror from"), philosopherStart),
      metric3(aLabel, complete ? countWithUnit(plan.aCount) : "—", plan?.aCount),
      metric3(bLabel, complete ? countWithUnit(plan.bCount) : "—", plan?.bCount)
    );
    panel.replaceChildren(grid);
    panel.dataset.status = complete ? "complete" : "unavailable";
  }
  function positionPanel2() {
    const state = activePanel2;
    if (!state?.panel?.isConnected) {
      hideEnhancementCostPanel();
      return;
    }
    let anchorRect = state.anchorRect;
    if (state.anchor?.isConnected) {
      anchorRect = state.anchor.getBoundingClientRect();
      if (state.sticky) state.anchorRect = anchorRect;
    } else if (!state.sticky) {
      hideEnhancementCostPanel();
      return;
    }
    if (!anchorRect) return;
    const panelRect = state.panel.getBoundingClientRect();
    const viewportWidth = Number(globalThis.innerWidth) || document.documentElement.clientWidth;
    const viewportHeight = Number(globalThis.innerHeight) || document.documentElement.clientHeight;
    const roomRight = viewportWidth - anchorRect.right - VIEWPORT_MARGIN3;
    const roomLeft = anchorRect.left - VIEWPORT_MARGIN3;
    const roomBelow = viewportHeight - anchorRect.bottom - VIEWPORT_MARGIN3;
    const roomAbove = anchorRect.top - VIEWPORT_MARGIN3;
    let placement = "right";
    let left = anchorRect.right + PANEL_GAP3;
    let top = anchorRect.top;
    if (roomRight < panelRect.width + PANEL_GAP3 && roomLeft >= panelRect.width + PANEL_GAP3) {
      placement = "left";
      left = anchorRect.left - panelRect.width - PANEL_GAP3;
    } else if (roomRight < panelRect.width + PANEL_GAP3 && roomLeft < panelRect.width + PANEL_GAP3) {
      if (roomBelow >= panelRect.height + PANEL_GAP3 || roomBelow >= roomAbove) {
        placement = "bottom";
        left = anchorRect.left;
        top = anchorRect.bottom + PANEL_GAP3;
      } else {
        placement = "top";
        left = anchorRect.left;
        top = anchorRect.top - panelRect.height - PANEL_GAP3;
      }
    }
    left = Math.min(
      Math.max(VIEWPORT_MARGIN3, left),
      viewportWidth - panelRect.width - VIEWPORT_MARGIN3
    );
    top = Math.min(
      Math.max(VIEWPORT_MARGIN3, top),
      viewportHeight - panelRect.height - VIEWPORT_MARGIN3
    );
    state.panel.dataset.placement = placement;
    state.panel.style.left = `${Math.round(left)}px`;
    state.panel.style.top = `${Math.round(top)}px`;
  }
  function hideEnhancementCostPanel() {
    const state = activePanel2;
    if (!state) {
      document.getElementById(PANEL_ID4)?.remove();
      return;
    }
    state.mutationObserver?.disconnect();
    state.resizeObserver?.disconnect();
    globalThis.removeEventListener?.("resize", state.position);
    globalThis.removeEventListener?.("scroll", state.position, true);
    if (state.outsideHandler) {
      document.removeEventListener("pointerdown", state.outsideHandler, true);
    }
    state.panel?.remove();
    activePanel2 = null;
  }
  function attachStickyOutsideHandler2(panel, anchor) {
    const outsideHandler = (event) => {
      if (!activePanel2?.sticky || activePanel2.panel !== panel) return;
      if (panel.contains(event.target)) return;
      runtime.api.clearTooltipProfitHoverContext?.(anchor, null, {
        preserveTouchPress: true
      });
      hideEnhancementCostPanel();
    };
    globalThis.setTimeout?.(() => {
      if (activePanel2?.panel !== panel) return;
      document.addEventListener("pointerdown", outsideHandler, true);
      activePanel2.outsideHandler = outsideHandler;
    }, 0);
  }
  function showEnhancementCostPanel(anchor, plan = null, options = {}) {
    if (!anchor?.isConnected) {
      hideEnhancementCostPanel();
      return null;
    }
    const sticky = Boolean(
      options.sticky || activePanel2?.anchor === anchor && activePanel2?.sticky
    );
    if (activePanel2?.anchor === anchor && activePanel2.panel?.isConnected && activePanel2.sticky === sticky) {
      renderPanel2(activePanel2.panel, plan);
      activePanel2.position();
      return activePanel2.panel;
    }
    hideEnhancementCostPanel();
    addStyles15();
    const panel = document.createElement("aside");
    panel.id = PANEL_ID4;
    panel.classList.toggle("mwi-enhancement-sticky", sticky);
    panel.setAttribute("role", "status");
    panel.setAttribute("aria-live", "polite");
    renderPanel2(panel, plan);
    anchor.insertAdjacentElement("afterend", panel);
    const position = () => globalThis.requestAnimationFrame?.(positionPanel2) ?? positionPanel2();
    const mutationObserver = sticky ? null : new MutationObserver(() => {
      if (!anchor.isConnected) hideEnhancementCostPanel();
    });
    mutationObserver?.observe(anchor.parentNode ?? document.body, {
      childList: true
    });
    const resizeObserver = globalThis.ResizeObserver ? new globalThis.ResizeObserver(position) : null;
    resizeObserver?.observe(anchor);
    resizeObserver?.observe(panel);
    activePanel2 = {
      anchor,
      mutationObserver,
      panel,
      position,
      resizeObserver,
      sticky,
      anchorRect: sticky ? anchor.getBoundingClientRect() : null
    };
    if (sticky && document.body && panel.parentElement !== document.body) {
      document.body.appendChild(panel);
    }
    globalThis.addEventListener?.("resize", position);
    globalThis.addEventListener?.("scroll", position, true);
    position();
    if (sticky) attachStickyOutsideHandler2(panel, anchor);
    return panel;
  }
  var positionEnhancementCostPanel = positionPanel2;
  runtime.settings.onChange?.("enhanceSim", (enabled) => {
    if (!enabled) hideEnhancementCostPanel();
  });
  Object.assign(runtime.api, {
    hideEnhancementCostPanel,
    positionEnhancementCostPanel,
    showEnhancementCostPanel
  });

  // src/features/enhancement-tooltip.js
  function setEnhancementContext(tooltip, plan) {
    if (runtime.api.setEnhancementHoverPanelContext) {
      runtime.api.setEnhancementHoverPanelContext(tooltip, plan);
    } else {
      showEnhancementCostPanel(tooltip, plan);
    }
  }
  function clearEnhancementContext(tooltip) {
    if (runtime.api.clearEnhancementHoverPanelContext) {
      runtime.api.clearEnhancementHoverPanelContext(tooltip);
    } else {
      hideEnhancementCostPanel();
    }
  }
  function appendMarketRows(tooltipContent, itemHrid, enhancementLevel) {
    tooltipContent.querySelector('[data-mwitools-enhancement-market="true"]')?.remove();
    if (!runtime.settings.settingsMap.itemTooltip_prices.isTrue || runtime.api.shouldSuppressMarketFeatures?.()) {
      return;
    }
    const wrapper = document.createElement("div");
    wrapper.dataset.mwitoolsEnhancementMarket = "true";
    wrapper.style.color = runtime.config.SCRIPT_COLOR_TOOLTIP;
    const fairValue = runtime.api.getFairValue(itemHrid, enhancementLevel);
    const ask = runtime.api.getAskPrice(itemHrid, enhancementLevel);
    const bid = runtime.api.getBidPrice(itemHrid, enhancementLevel);
    const valueRow = document.createElement("div");
    valueRow.textContent = `${runtime.config.isZH ? "市场价值: " : "Market value: "}${fairValue > 0 ? runtime.api.numberFormatter(fairValue) : "-"}`;
    const priceRow = document.createElement("div");
    priceRow.textContent = `${runtime.config.isZH ? "价格: " : "Price: "}${runtime.api.numberFormatter(ask)} / ${runtime.api.numberFormatter(bid)}`;
    wrapper.append(valueRow, priceRow);
    tooltipContent.append(wrapper);
  }
  function getTooltipEnhancementPlanOptions(itemHrid) {
    const forceProtectionMirror = Boolean(
      runtime.api.isBackEquipment?.(itemHrid)
    );
    return {
      forcedProtectionItemHrid: forceProtectionMirror ? "/items/mirror_of_protection" : null,
      allowPhilosopherMirror: true,
      getFairValue: (hrid, level = 0) => runtime.api.getAssetValue?.(hrid, level, {
        forceAcquisitionValue: true
      }) || runtime.api.getFairValue(hrid, level) || 0,
      getMarketValue: (hrid, level = 0) => runtime.api.getFairValue(hrid, level) || 0
    };
  }
  function readEnhancedTooltipItem(tooltip) {
    const itemNameElements = [
      ...tooltip?.querySelectorAll("div.ItemTooltipText_name__2JAHA span") ?? []
    ];
    const enhancementText = itemNameElements.find(
      (element) => /\+\s*\d+/.test(element.textContent ?? "")
    )?.textContent;
    const enhancementLevel = Math.max(
      0,
      Math.floor(Number(enhancementText?.match(/\+\s*(\d+)/)?.[1]) || 0)
    );
    const iconHrid = resolveEntityFromElement("item", tooltip);
    if (iconHrid) return { itemHrid: iconHrid, enhancementLevel };
    const itemName4 = runtime.api.getOriTextFromElement?.(itemNameElements[0]);
    return {
      itemHrid: resolveLocalizedEntity("item", itemName4),
      enhancementLevel
    };
  }
  async function handleEnhancedItemTooltip(tooltip) {
    const tooltipContent = tooltip?.querySelector(
      ".ItemTooltipText_itemTooltipText__zFq3A"
    );
    if (!tooltipContent) {
      clearEnhancementContext(tooltip);
      hideEnhancementCostPanel();
      return;
    }
    const { itemHrid, enhancementLevel } = readEnhancedTooltipItem(tooltip);
    if (!itemHrid || !runtime.state.initData_itemDetailMap?.[itemHrid]) {
      clearEnhancementContext(tooltip);
      hideEnhancementCostPanel();
      return;
    }
    if (runtime.settings.settingsMap.enhanceSim.isTrue) {
      setEnhancementContext(tooltip, null);
    } else {
      clearEnhancementContext(tooltip);
      hideEnhancementCostPanel();
    }
    if (!runtime.api.shouldSuppressMarketFeatures?.()) {
      await runtime.api.fetchMarketJSON();
      if (!tooltip.isConnected) return;
      appendMarketRows(tooltipContent, itemHrid, enhancementLevel);
    } else {
      appendMarketRows(tooltipContent, itemHrid, enhancementLevel);
    }
    if (!runtime.settings.settingsMap.enhanceSim.isTrue) return;
    const plan = calculateEnhancementPlan({
      itemHrid,
      targetLevel: enhancementLevel,
      ...getTooltipEnhancementPlanOptions(itemHrid)
    });
    if (tooltip.isConnected) setEnhancementContext(tooltip, plan);
  }
  runtime.api.handleItemTooltipWithEnhancementLevel = handleEnhancedItemTooltip;
  runtime.onMessage("init_character_data", () => {
    if (!runtime.api.shouldSuppressMarketFeatures?.()) return;
    document.querySelectorAll('[data-mwitools-enhancement-market="true"]').forEach((row) => row.remove());
  });
  runtime.settings.onChange?.("adaptIronCowMarketFeatures", () => {
    if (!runtime.api.shouldSuppressMarketFeatures?.()) return;
    document.querySelectorAll('[data-mwitools-enhancement-market="true"]').forEach((row) => row.remove());
  });

  // src/features/settings-and-notifications.js
  var SETTINGS_V2_KEY = "MWITools_settings_v2";
  var BACK_MIRROR_DEFAULT_CORRECTION_KEY = "MWITools_back_mirror_default_disabled_v2";
  var SETTINGS_STYLE_ID = "mwitools-settings-style";
  var EQUIPMENT_WARNING_STYLE_ID = "mwitools-equipment-warning-style";
  var SETTINGS_TAB_ATTRIBUTE = "data-mwitools-settings-tab";
  var SETTINGS_PANEL_ATTRIBUTE = "data-mwitools-settings-panel";
  var SETTINGS_ROOT_ATTRIBUTE = "data-mwitools-settings-root";
  var SETTINGS_BUTTON_ID = "mwitools-settings-button";
  var SETTINGS_POPOVER_ID = "mwitools-settings-popover";
  var SETTINGS_POPOVER_SCROLL_KEY = "MWITools_settings_popover_scroll_v1";
  var TOOLTIP_PROFIT_SHORTCUT_KEY = "MWITools_tooltip_profit_key_v1";
  var GUILD_CREDIT_RECOMMENDATION_COUNT_KEY = "MWITools_guild_credit_recommendation_count_v1";
  function normalizeGuildCreditRecommendationCount(value) {
    const count = Math.floor(Number(value));
    return Number.isFinite(count) ? Math.min(8, Math.max(1, count)) : 3;
  }
  function loadGuildCreditRecommendationCount() {
    const stored = localStorage.getItem(GUILD_CREDIT_RECOMMENDATION_COUNT_KEY);
    return stored === null ? 3 : normalizeGuildCreditRecommendationCount(stored);
  }
  var guildCreditRecommendationCount = loadGuildCreditRecommendationCount();
  function getGuildCreditRecommendationCount() {
    return guildCreditRecommendationCount;
  }
  function setGuildCreditRecommendationCount(value) {
    guildCreditRecommendationCount = normalizeGuildCreditRecommendationCount(value);
    localStorage.setItem(
      GUILD_CREDIT_RECOMMENDATION_COUNT_KEY,
      String(guildCreditRecommendationCount)
    );
    document.querySelectorAll?.("[data-mwitools-guild-credit-count]").forEach((select) => {
      select.value = String(guildCreditRecommendationCount);
    });
    if (runtime.settings.get("guildCreditConversionsSort")) {
      void runtime.api.renderGuildCreditRecommendations?.();
    }
    return guildCreditRecommendationCount;
  }
  function normalizeTooltipProfitShortcut(value) {
    const code = String(value?.code ?? "").trim();
    if (!code) return { code: "Control", display: "Ctrl" };
    return {
      code,
      display: String(value?.display ?? code).trim() || code
    };
  }
  function loadTooltipProfitShortcut() {
    try {
      return normalizeTooltipProfitShortcut(
        JSON.parse(localStorage.getItem(TOOLTIP_PROFIT_SHORTCUT_KEY) || "null")
      );
    } catch {
      return normalizeTooltipProfitShortcut(null);
    }
  }
  var tooltipProfitShortcut = loadTooltipProfitShortcut();
  function shortcutCodeFromEvent(event) {
    if (["Control", "Shift", "Alt", "Meta"].includes(event?.key)) {
      return event.key;
    }
    return String(event?.code ?? event?.key ?? "");
  }
  function shortcutDisplayFromEvent(event) {
    if (event?.key === "Control") return "Ctrl";
    if (event?.key === "Meta") return "Meta";
    if (["Shift", "Alt"].includes(event?.key)) return event.key;
    if (event?.code === "Space") return "Space";
    if (String(event?.code).startsWith("Arrow")) {
      return String(event.code).slice(5);
    }
    return event?.key?.length === 1 ? event.key.toUpperCase() : event?.key || event?.code;
  }
  function getTooltipProfitShortcut() {
    return { ...tooltipProfitShortcut };
  }
  function setTooltipProfitShortcut(value) {
    tooltipProfitShortcut = normalizeTooltipProfitShortcut(value);
    localStorage.setItem(
      TOOLTIP_PROFIT_SHORTCUT_KEY,
      JSON.stringify(tooltipProfitShortcut)
    );
    return getTooltipProfitShortcut();
  }
  function matchesTooltipProfitShortcut(event) {
    return shortcutCodeFromEvent(event) === tooltipProfitShortcut.code;
  }
  function persistSettings() {
    const values = Object.fromEntries(
      Object.entries(runtime.settings.settingsMap).map(([id, setting]) => [
        id,
        Boolean(setting.isTrue)
      ])
    );
    const preferences = Object.fromEntries(
      Object.keys(runtime.settings.preferenceDefinitions ?? {}).map((id) => [
        id,
        runtime.settings.getPreference(id)
      ])
    );
    localStorage.setItem(
      SETTINGS_V2_KEY,
      JSON.stringify({ version: 2, values, preferences })
    );
    localStorage.setItem(
      "script_settingsMap",
      JSON.stringify(runtime.settings.settingsMap)
    );
  }
  function applyStoredSetting(id, value) {
    const setting = runtime.settings.settingsMap[id];
    if (!setting) return;
    setting.isTrue = Boolean(value);
  }
  function applyVisualSettings() {
    runtime.config.isZH = runtime.settings.settingsMap.forceMWIToolsDisplayZH.isTrue || runtime.config.isZHInGameSetting;
    runtime.config.SCRIPT_COLOR_MAIN = runtime.settings.settingsMap.useOrangeAsMainColor.isTrue ? "orange" : "green";
    runtime.config.SCRIPT_COLOR_TOOLTIP = runtime.settings.settingsMap.useOrangeAsMainColor.isTrue ? "#804600" : "darkgreen";
    const scale = { standard: 1, large: 1.12, largest: 1.25 }[runtime.settings.getPreference("uiFontScale")] ?? 1;
    document.documentElement?.style.setProperty(
      "--mwi-ui-font-scale",
      String(scale)
    );
    const hoverScale = { standard: 1, large: 1.12, largest: 1.25 }[runtime.settings.getPreference("hoverFontScale")] ?? 1;
    document.documentElement?.style.setProperty(
      "--mwi-hover-font-scale",
      String(hoverScale)
    );
  }
  runtime.settings.onPreferenceChange?.("uiFontScale", applyVisualSettings);
  runtime.settings.onPreferenceChange?.("hoverFontScale", applyVisualSettings);
  function readSettings() {
    let loadedV2 = false;
    let storedPreferences = null;
    try {
      const storedV2 = JSON.parse(
        localStorage.getItem(SETTINGS_V2_KEY) || "null"
      );
      if (storedV2?.version === 2 && storedV2.values) {
        for (const [id, value] of Object.entries(storedV2.values)) {
          applyStoredSetting(id, value);
        }
        storedPreferences = storedV2.preferences ?? null;
        loadedV2 = true;
      }
    } catch (error) {
      console.warn(
        runtime.config.isZH ? "[MWITools] 无法读取新版设置。" : "[MWITools] Could not read v2 settings.",
        error
      );
    }
    if (!loadedV2) {
      try {
        const legacy = JSON.parse(
          localStorage.getItem("script_settingsMap") || "null"
        );
        for (const option of Object.values(legacy ?? {})) {
          if (!option?.id) continue;
          applyStoredSetting(option.id, option.isTrue);
        }
      } catch (error) {
        console.warn(
          runtime.config.isZH ? "[MWITools] 无法迁移旧版设置。" : "[MWITools] Could not migrate legacy settings.",
          error
        );
      }
    }
    const legacyProductionSummaryEnabled = Boolean(
      runtime.settings.settingsMap.productionSummary.isTrue
    );
    for (const [id, definition] of Object.entries(
      runtime.settings.preferenceDefinitions ?? {}
    )) {
      const value = storedPreferences?.[id] ?? (id === "productionSummaryMode" && !legacyProductionSummaryEnabled ? "off" : definition.defaultValue);
      void runtime.settings.setPreference(id, value, { persist: false });
    }
    runtime.settings.settingsMap.productionSummary.isTrue = runtime.settings.getPreference("productionSummaryMode") !== "off";
    if (!localStorage.getItem(BACK_MIRROR_DEFAULT_CORRECTION_KEY)) {
      runtime.settings.settingsMap.valueBackEquipmentWithProtectionMirror.isTrue = false;
      localStorage.setItem(BACK_MIRROR_DEFAULT_CORRECTION_KEY, "1");
    }
    applyVisualSettings();
    persistSettings();
  }
  function addSettingsStyles() {
    if (document.getElementById(SETTINGS_STYLE_ID)) return;
    const styleHost = document.head ?? document.documentElement;
    if (!styleHost) return;
    const style = document.createElement("style");
    style.id = SETTINGS_STYLE_ID;
    style.textContent = `
    #${HEADER_TOOLS_ID} { display:flex; align-items:center; justify-content:center; gap:4px; width:max-content; max-width:100%; margin:2px auto 0; }
    #${SETTINGS_BUTTON_ID} { display:inline-flex; width:22px; height:20px; flex:0 0 22px; align-items:center; justify-content:center; box-sizing:border-box; margin:0; padding:0; border:1px solid rgba(160,176,210,.45); border-radius:4px; background:rgba(118,138,180,.12); color:#bdc9e5; font:700 13px/1 sans-serif; cursor:pointer; }
    #${SETTINGS_BUTTON_ID}:hover,#${SETTINGS_BUTTON_ID}[aria-expanded="true"] { border-color:rgba(245,158,11,.65); background:rgba(245,158,11,.16); color:#ffd27a; }
    #${SETTINGS_BUTTON_ID}:focus-visible { outline:2px solid var(--color-primary,${runtime.config.SCRIPT_COLOR_MAIN}); outline-offset:1px; }
    #${SETTINGS_POPOVER_ID} { position:fixed; z-index:2147482500; overflow-x:hidden; overflow-y:auto; overscroll-behavior:contain; box-sizing:border-box; padding:10px; border:1px solid rgba(116,132,170,.72); border-radius:9px; background:var(--color-background-primary,#171b2a); box-shadow:0 18px 54px rgba(0,0,0,.62); color:var(--color-text-primary,#eee); }
    #${SETTINGS_POPOVER_ID}[hidden] { display:none; }
    #script_settings,[${SETTINGS_ROOT_ATTRIBUTE}] { width:100%; color:var(--color-text-primary,#eee); }
    #script_settings { margin-top:14px; }
    [${SETTINGS_PANEL_ATTRIBUTE}] { width:100%; box-sizing:border-box; }
    [${SETTINGS_TAB_ATTRIBUTE}][aria-selected="true"] { color:var(--color-primary,#fff); }
    .mwi-settings-hero { display:flex; justify-content:space-between; gap:14px; align-items:end; margin-bottom:11px; }
    .mwi-settings-hero-actions { display:flex; align-items:center; gap:7px; }
    .mwi-settings-close { display:inline-flex; width:30px; height:30px; flex:0 0 30px; align-items:center; justify-content:center; border:0; border-radius:5px; background:rgba(255,255,255,.06); color:var(--color-text-secondary,#aaa); font-size:19px; cursor:pointer; }
    .mwi-settings-close:hover { background:rgba(255,255,255,.12); color:var(--color-text-primary,#fff); }
    .mwi-settings-title { font-size:1.2rem; font-weight:700; letter-spacing:.01em; }
    .mwi-settings-subtitle { color:var(--color-text-secondary,#aaa); margin-top:3px; font-size:calc(.78rem * var(--mwi-ui-font-scale,1)); line-height:1.35; }
    .mwi-settings-search { width:min(320px,100%); box-sizing:border-box; border:1px solid rgba(255,255,255,.16); border-radius:5px; background:rgba(0,0,0,.2); color:inherit; padding:7px 9px; }
    .mwi-settings-group { margin:0 0 10px; border:1px solid rgba(255,255,255,.12); border-radius:7px; background:rgba(0,0,0,.13); overflow:hidden; }
    .mwi-settings-group-head { padding:10px 13px 8px; border-bottom:1px solid rgba(255,255,255,.08); }
    .mwi-settings-group-title { font-size:1rem; font-weight:700; }
    .mwi-settings-group-summary { color:var(--color-text-secondary,#aaa); font-size:calc(.75rem * var(--mwi-ui-font-scale,1)); margin-top:2px; line-height:1.35; }
    .mwi-settings-grid { display:flex; flex-direction:column; padding:0 10px; }
    .mwi-performance-settings-card { display:flex; min-height:58px; align-items:center; justify-content:space-between; gap:14px; padding:9px 4px; border-bottom:1px solid rgba(255,255,255,.075); }
    .mwi-performance-settings-copy { min-width:0; }
    .mwi-performance-settings-title { display:flex; align-items:center; gap:8px; font-size:calc(.84rem * var(--mwi-ui-font-scale,1)); font-weight:700; }
    .mwi-performance-settings-profile { display:inline-flex; padding:1px 7px; border-radius:999px; background:rgba(238,154,29,.14); color:#ffd084; font-size:calc(.6875rem * var(--mwi-ui-font-scale,1)); white-space:nowrap; }
    .mwi-performance-settings-summary { margin-top:3px; color:var(--color-text-secondary,#aaa); font-size:calc(.71rem * var(--mwi-ui-font-scale,1)); line-height:1.35; }
    .mwi-performance-settings-open { flex:0 0 auto; border:1px solid rgba(238,154,29,.62); border-radius:4px; padding:6px 10px; background:rgba(238,154,29,.12); color:#ffd084; font:inherit; font-size:calc(.72rem * var(--mwi-ui-font-scale,1)); cursor:pointer; }
    .mwi-performance-settings-open:hover { background:rgba(238,154,29,.2); }
    .mwi-setting-card { min-width:0; padding:7px 4px; border-bottom:1px solid rgba(255,255,255,.075); transition:background .15s; }
    .mwi-setting-card:last-child { border-bottom:0; }
    .mwi-setting-card:hover { background:rgba(255,255,255,.025); }
    .mwi-setting-card.mwi-setting-child { margin-top:5px; padding:6px 8px; border:1px solid rgba(255,255,255,.075); border-radius:5px; background:rgba(0,0,0,.12); }
    .mwi-setting-card.mwi-setting-child:has(input:disabled) { opacity:.52; }
    .mwi-setting-row { display:grid; min-height:42px; grid-template-columns:minmax(170px,.72fr) minmax(260px,1.5fr) auto minmax(40px,auto); align-items:center; gap:8px 14px; }
    .mwi-setting-copy { display:contents; }
    .mwi-setting-title-line { display:flex; min-width:0; grid-column:1; grid-row:1; align-items:center; gap:7px; text-align:left; }
    .mwi-setting-title { min-width:0; font-size:calc(.84rem * var(--mwi-ui-font-scale,1)); font-weight:650; line-height:1.25; }
    .mwi-setting-summary { overflow:hidden; grid-column:2; grid-row:1; color:var(--color-text-secondary,#aaa); font-size:calc(.71rem * var(--mwi-ui-font-scale,1)); line-height:1.3; text-align:left; text-overflow:ellipsis; white-space:nowrap; }
    .mwi-setting-status { display:inline-flex; flex:0 0 auto; padding:1px 6px; border-radius:999px; font-size:calc(.6875rem * var(--mwi-ui-font-scale,1)); color:#aaa; background:rgba(255,255,255,.07); }
    .mwi-setting-status[data-status="active"] { color:#87d7a0; background:rgba(70,170,100,.13); }
    .mwi-setting-status[data-status="failed"] { color:#ff9a90; background:rgba(210,70,60,.14); }
    .mwi-setting-status[data-status="waiting"] { color:#e3c56d; background:rgba(210,170,60,.13); }
    .mwi-setting-toggle { position:relative; width:36px; height:20px; grid-column:4; grid-row:1; justify-self:end; }
    .mwi-setting-toggle input { position:absolute; opacity:0; }
    .mwi-setting-toggle span { position:absolute; inset:0; border-radius:999px; cursor:pointer; background:#555; transition:.16s; }
    .mwi-setting-toggle span::after { content:""; position:absolute; width:16px; height:16px; left:2px; top:2px; border-radius:50%; background:#fff; transition:.16s; }
    .mwi-setting-toggle input:checked + span { background:var(--color-primary,${runtime.config.SCRIPT_COLOR_MAIN}); }
    .mwi-setting-toggle input:checked + span::after { transform:translateX(16px); }
    .mwi-setting-more { grid-column:3; grid-row:1; margin:0; font-size:calc(.6875rem * var(--mwi-ui-font-scale,1)); color:var(--color-text-secondary,#aaa); text-align:left; white-space:nowrap; }
    .mwi-setting-more summary { display:inline-block; cursor:pointer; color:var(--color-primary,${runtime.config.SCRIPT_COLOR_MAIN}); list-style-position:inside; }
    .mwi-setting-more[open] { grid-column:1 / 4; grid-row:2; margin:0; padding-top:5px; border-top:1px solid rgba(255,255,255,.06); white-space:normal; }
    .mwi-setting-more p { margin:4px 0 1px; line-height:1.4; }
    .mwi-setting-retry { margin-left:8px; border:0; border-radius:4px; padding:2px 6px; cursor:pointer; color:inherit; background:rgba(255,255,255,.1); }
    .mwi-setting-shortcut-row { display:flex; align-items:center; justify-content:flex-end; gap:8px; margin:5px 44px 1px 0; color:var(--color-text-secondary,#aaa); font-size:calc(.7rem * var(--mwi-ui-font-scale,1)); }
    .mwi-setting-shortcut { min-width:92px; border:1px solid rgba(255,255,255,.16); border-radius:5px; padding:4px 8px; cursor:pointer; color:inherit; background:rgba(255,255,255,.07); }
    .mwi-setting-preset-input { width:min(310px,62vw); box-sizing:border-box; border:1px solid rgba(255,255,255,.16); border-radius:5px; padding:5px 8px; color:inherit; background:rgba(0,0,0,.2); font:inherit; }
    .mwi-setting-preset-input:disabled { cursor:not-allowed; opacity:.5; }
    .mwi-setting-select { min-width:92px; border:1px solid rgba(255,255,255,.16); border-radius:5px; padding:4px 24px 4px 8px; color:inherit; background:var(--color-background-secondary,#292929); font:inherit; }
    .mwi-setting-primary-select { grid-column:4; grid-row:1; justify-self:end; }
    .mwi-setting-select:disabled { cursor:not-allowed; opacity:.5; }
    @media (max-width:700px) { #${SETTINGS_POPOVER_ID} { padding:8px; } .mwi-settings-hero { align-items:stretch; flex-direction:column; } .mwi-settings-hero-actions { width:100%; } .mwi-settings-hero-actions .mwi-settings-search { flex:1; } .mwi-settings-search { width:100%; } .mwi-performance-settings-card { align-items:flex-start; } .mwi-performance-settings-open { max-width:118px; } .mwi-setting-row { grid-template-columns:minmax(0,1fr) auto; gap:3px 10px; padding:3px 0; } .mwi-setting-title-line { grid-column:1;grid-row:1; } .mwi-setting-summary { grid-column:1;grid-row:2;white-space:normal; } .mwi-setting-more { grid-column:1;grid-row:3; } .mwi-setting-more[open] { grid-column:1 / 3;grid-row:3; } .mwi-setting-toggle { grid-column:2;grid-row:1 / 4; } .mwi-setting-primary-select { grid-column:2;grid-row:1 / 3; } }
  `;
    styleHost.appendChild(style);
  }
  function localizedText2(value) {
    return value?.[runtime.config.isZH ? "zh" : "en"] ?? "";
  }
  function featureStatusForSetting(id) {
    const featureStatus = runtime.features.getStatus(id);
    if (featureStatus.status !== "unregistered") return featureStatus;
    return {
      id,
      status: runtime.settings.get(id) ? "active" : "disabled",
      error: null
    };
  }
  function statusLabel2(status) {
    const labels = runtime.config.isZH ? {
      active: "已启用",
      disabled: "已关闭",
      initializing: "正在启动",
      waiting: "等待游戏数据",
      failed: "启动失败"
    } : {
      active: "Enabled",
      disabled: "Disabled",
      initializing: "Starting",
      waiting: "Waiting for game data",
      failed: "Failed to start"
    };
    return labels[status] ?? labels.disabled;
  }
  function getSettingDescendants(id) {
    return Object.values(runtime.settings.catalog).filter((candidate) => {
      let parent = candidate.parent;
      while (parent) {
        if (parent === id) return true;
        parent = runtime.settings.catalog[parent]?.parent;
      }
      return false;
    });
  }
  function areSettingParentsEnabled(definition) {
    let parent = definition.parent;
    while (parent) {
      if (!runtime.settings.get(parent)) return false;
      parent = runtime.settings.catalog[parent]?.parent;
    }
    return true;
  }
  function settingsRoots() {
    return [
      ...document.querySelectorAll(
        `#script_settings,[${SETTINGS_ROOT_ATTRIBUTE}]`
      )
    ];
  }
  function cleanupSettingsRoot(root) {
    for (const card of root?.querySelectorAll(
      ".mwi-setting-card,.mwi-performance-settings-card"
    ) ?? []) {
      card._mwitoolsCleanup?.();
    }
  }
  function renderAllSettings() {
    for (const root of settingsRoots()) renderSettings(root);
    const label = runtime.config.isZH ? "MWITools 快捷设置" : "MWITools quick settings";
    document.getElementById(SETTINGS_BUTTON_ID)?.setAttribute("aria-label", label);
    document.getElementById(SETTINGS_POPOVER_ID)?.setAttribute("aria-label", label);
  }
  function createSelectSettingCard(definition, options = {}) {
    const card = document.createElement("article");
    card.className = "mwi-setting-card";
    if (options.child) card.classList.add("mwi-setting-child");
    card.dataset.search = [
      definition.title?.zh,
      definition.title?.en,
      definition.summary?.zh,
      definition.summary?.en
    ].filter(Boolean).join(" ").toLowerCase();
    const row = document.createElement("div");
    row.className = "mwi-setting-row";
    const copy = document.createElement("div");
    copy.className = "mwi-setting-copy";
    const titleLine = document.createElement("div");
    titleLine.className = "mwi-setting-title-line";
    const title = document.createElement("div");
    title.className = "mwi-setting-title";
    title.textContent = localizedText2(definition.title);
    titleLine.append(title);
    const summary = document.createElement("div");
    summary.className = "mwi-setting-summary";
    summary.textContent = localizedText2(definition.summary);
    copy.append(titleLine, summary);
    if (definition.details) {
      const details = document.createElement("details");
      details.className = "mwi-setting-more";
      const heading = document.createElement("summary");
      heading.textContent = runtime.config.isZH ? "详细说明" : "Details";
      const detailsCopy = document.createElement("p");
      detailsCopy.textContent = localizedText2(definition.details);
      details.append(heading, detailsCopy);
      copy.append(details);
    }
    const select = document.createElement("select");
    select.className = "mwi-setting-select mwi-setting-primary-select";
    select.setAttribute("aria-label", localizedText2(definition.title));
    for (const [value, label] of definition.control.options) {
      const option = document.createElement("option");
      option.value = value;
      option.textContent = localizedText2(label);
      select.append(option);
    }
    const preferenceId = definition.control.preference;
    select.value = runtime.settings.getPreference(preferenceId);
    select.addEventListener("change", async () => {
      const value = select.value;
      await runtime.settings.setPreference(preferenceId, value);
      if (preferenceId === "productionSummaryMode") {
        await runtime.settings.set("productionSummary", value !== "off");
      }
      if (preferenceId === "uiFontScale") applyVisualSettings();
    });
    row.append(copy, select);
    card.append(row);
    const stopPreferenceListener = runtime.settings.onPreferenceChange(
      preferenceId,
      (value) => {
        select.value = value;
        if (preferenceId === "uiFontScale") applyVisualSettings();
      }
    );
    card._mwitoolsCleanup = () => stopPreferenceListener?.();
    return card;
  }
  function createSettingCard(definition, options = {}) {
    if (definition.control?.type === "select") {
      return createSelectSettingCard(definition, options);
    }
    const setting = runtime.settings.settingsMap[definition.id];
    const children = Object.values(runtime.settings.catalog).filter(
      (candidate) => candidate.parent === definition.id
    );
    const descendants = getSettingDescendants(definition.id);
    const card = document.createElement("article");
    let cancelShortcutCapture = null;
    const auxiliaryControls = [];
    const preferenceCleanups = [];
    card.className = "mwi-setting-card";
    if (options.child) card.classList.add("mwi-setting-child");
    card.dataset.search = [
      definition.title?.zh,
      definition.title?.en,
      definition.summary?.zh,
      definition.summary?.en,
      ...definition.id === "guildCreditConversionsSort" ? ["推荐数量", "recommendation count"] : [],
      ...descendants.flatMap((child) => [
        child.title?.zh,
        child.title?.en,
        child.summary?.zh,
        child.summary?.en
      ])
    ].filter(Boolean).join(" ").toLowerCase();
    const row = document.createElement("div");
    row.className = "mwi-setting-row";
    const copy = document.createElement("div");
    copy.className = "mwi-setting-copy";
    const title = document.createElement("div");
    title.className = "mwi-setting-title";
    title.textContent = localizedText2(definition.title);
    const summary = document.createElement("div");
    summary.className = "mwi-setting-summary";
    summary.textContent = localizedText2(definition.summary);
    const status = document.createElement("span");
    status.className = "mwi-setting-status";
    const setStatus = () => {
      const current = featureStatusForSetting(definition.id);
      status.dataset.status = current.status;
      status.textContent = statusLabel2(current.status);
      if (current.error) status.title = current.error;
      if (current.status === "failed") {
        const retry = document.createElement("button");
        retry.className = "mwi-setting-retry";
        retry.type = "button";
        retry.textContent = runtime.config.isZH ? "重试" : "Retry";
        retry.addEventListener(
          "click",
          () => runtime.features.restart(definition.id)
        );
        status.appendChild(retry);
      }
      for (const control of auxiliaryControls) {
        control.disabled = !checkbox.checked;
      }
    };
    setStatus();
    const titleLine = document.createElement("div");
    titleLine.className = "mwi-setting-title-line";
    titleLine.append(title, status);
    copy.append(titleLine, summary);
    const toggle = document.createElement("label");
    toggle.className = "mwi-setting-toggle";
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.dataset.settingId = definition.id;
    checkbox.checked = Boolean(setting.isTrue);
    if (definition.parent) {
      checkbox.disabled = !areSettingParentsEnabled(definition);
    }
    checkbox.setAttribute("aria-label", localizedText2(definition.title));
    const track = document.createElement("span");
    toggle.append(checkbox, track);
    if (definition.details) {
      const details = document.createElement("details");
      details.className = "mwi-setting-more";
      const detailsSummary = document.createElement("summary");
      detailsSummary.textContent = runtime.config.isZH ? "详细说明" : "Details";
      details.append(detailsSummary);
      const detailsCopy = document.createElement("p");
      detailsCopy.textContent = localizedText2(definition.details);
      details.append(detailsCopy);
      copy.append(details);
    }
    row.append(copy, toggle);
    card.append(row);
    if (definition.id === "itemTooltip_profitRequireKey") {
      const shortcutRow = document.createElement("div");
      shortcutRow.className = "mwi-setting-shortcut-row";
      const shortcutLabel = document.createElement("span");
      shortcutLabel.textContent = runtime.config.isZH ? "触发按键" : "Trigger key";
      const shortcutButton = document.createElement("button");
      shortcutButton.type = "button";
      shortcutButton.className = "mwi-setting-shortcut";
      const updateShortcutText = () => {
        shortcutButton.textContent = getTooltipProfitShortcut().display;
      };
      updateShortcutText();
      shortcutButton.addEventListener("click", () => {
        cancelShortcutCapture?.();
        shortcutButton.textContent = runtime.config.isZH ? "请按一个键…" : "Press one key…";
        const capture = (event) => {
          event.preventDefault();
          event.stopPropagation();
          cancelShortcutCapture?.();
          if (event.key === "Escape") {
            updateShortcutText();
            return;
          }
          setTooltipProfitShortcut({
            code: shortcutCodeFromEvent(event),
            display: shortcutDisplayFromEvent(event)
          });
          updateShortcutText();
        };
        cancelShortcutCapture = () => {
          window.removeEventListener("keydown", capture, true);
          cancelShortcutCapture = null;
        };
        window.addEventListener("keydown", capture, true);
      });
      shortcutRow.append(shortcutLabel, shortcutButton);
      card.append(shortcutRow);
    }
    if (definition.id === "guildCreditConversionsSort") {
      const countRow = document.createElement("div");
      countRow.className = "mwi-setting-shortcut-row";
      const countLabel = document.createElement("span");
      countLabel.textContent = runtime.config.isZH ? "推荐数量" : "Recommendations";
      const countSelect = document.createElement("select");
      countSelect.className = "mwi-setting-select";
      countSelect.dataset.mwitoolsGuildCreditCount = "";
      countSelect.setAttribute(
        "aria-label",
        runtime.config.isZH ? "公会信用推荐数量" : "Guild credit recommendations"
      );
      for (let count = 1; count <= 8; count += 1) {
        const option = document.createElement("option");
        option.value = String(count);
        option.textContent = String(count);
        countSelect.appendChild(option);
      }
      countSelect.value = String(getGuildCreditRecommendationCount());
      countSelect.disabled = !checkbox.checked;
      countSelect.addEventListener("change", () => {
        countSelect.value = String(
          setGuildCreditRecommendationCount(countSelect.value)
        );
      });
      countRow.append(countLabel, countSelect);
      card.append(countRow);
      auxiliaryControls.push(countSelect);
    }
    if (definition.id === "actionPanel_totalTime_quickInputs") {
      const presets = [
        [
          "productionQuickHours",
          runtime.config.isZH ? "快捷小时" : "Quick hours",
          runtime.config.isZH ? "例如:0.5, 1, 2, 6" : "Example: 0.5, 1, 2, 6"
        ],
        [
          "productionQuickCounts",
          runtime.config.isZH ? "快捷次数" : "Quick counts",
          runtime.config.isZH ? "例如:10, 100, 500" : "Example: 10, 100, 500"
        ]
      ];
      for (const [preferenceId, labelText, placeholder] of presets) {
        const presetRow = document.createElement("label");
        presetRow.className = "mwi-setting-shortcut-row";
        const presetLabel = document.createElement("span");
        presetLabel.textContent = labelText;
        const input = document.createElement("input");
        input.type = "text";
        input.className = "mwi-setting-preset-input";
        input.placeholder = placeholder;
        input.value = runtime.settings.getPreference(preferenceId);
        input.setAttribute("aria-label", labelText);
        input.addEventListener("change", async () => {
          await runtime.settings.setPreference(preferenceId, input.value);
          input.value = runtime.settings.getPreference(preferenceId);
        });
        preferenceCleanups.push(
          runtime.settings.onPreferenceChange(preferenceId, (value) => {
            if (document.activeElement !== input) input.value = value;
          })
        );
        auxiliaryControls.push(input);
        presetRow.append(presetLabel, input);
        card.append(presetRow);
      }
    }
    setStatus();
    for (const child of children) {
      card.append(createSettingCard(child, { child: true }));
    }
    checkbox.addEventListener("change", async () => {
      await runtime.settings.set(definition.id, checkbox.checked);
      if (definition.id === "forceMWIToolsDisplayZH" || definition.id === "useOrangeAsMainColor" || children.length) {
        applyVisualSettings();
        renderAllSettings();
        return;
      }
      setStatus();
    });
    const stopStatusListener = runtime.features.onStatusChange((id) => {
      if (id === definition.id) setStatus();
    });
    const stopSettingListener = runtime.settings.onChange(
      definition.id,
      (value) => {
        checkbox.checked = Boolean(value);
        setStatus();
      }
    );
    card._mwitoolsCleanup = () => {
      cancelShortcutCapture?.();
      preferenceCleanups.forEach((cleanup4) => cleanup4?.());
      stopStatusListener?.();
      stopSettingListener?.();
    };
    return card;
  }
  function createDuplicateWarningSettingsCard() {
    const card = document.createElement("article");
    card.className = "mwi-performance-settings-card";
    card.dataset.search = "冲突 提醒 不再提示 conflict warning restore reset";
    const copy = document.createElement("div");
    copy.className = "mwi-performance-settings-copy";
    const title = document.createElement("div");
    title.className = "mwi-performance-settings-title";
    title.textContent = runtime.config.isZH ? "冲突脚本提醒" : "Conflicting-script warnings";
    const summary = document.createElement("div");
    summary.className = "mwi-performance-settings-summary";
    const button = document.createElement("button");
    button.type = "button";
    button.className = "mwi-performance-settings-open";
    const update = () => {
      const count = runtime.api.getMutedDuplicateScriptIds?.().length ?? 0;
      summary.textContent = count ? runtime.config.isZH ? `已永久静默 ${count} 个脚本。` : `${count} script warning(s) muted.` : runtime.config.isZH ? "当前没有永久静默的冲突提醒。" : "No conflict warnings are currently muted.";
      button.disabled = count === 0;
      button.textContent = runtime.config.isZH ? "恢复提醒" : "Restore warnings";
    };
    button.addEventListener("click", () => {
      runtime.api.clearMutedDuplicateScriptIds?.();
      update();
    });
    update();
    copy.append(title, summary);
    card.append(copy, button);
    return card;
  }
  function performanceProfileLabel(state = runtime.api.performanceProfiles?.getState?.()) {
    const usages = runtime.config.isZH ? { life: "生活", combat: "战斗", balanced: "平衡" } : { life: "Skilling", combat: "Combat", balanced: "Balanced" };
    const tiers = runtime.config.isZH ? {
      smooth: "流畅优先",
      standard: "标准",
      full: "完整功能",
      custom: "自定义"
    } : {
      smooth: "Smooth",
      standard: "Standard",
      full: "Full features",
      custom: "Custom"
    };
    return `${usages[state?.usage] ?? usages.balanced} · ${tiers[state?.tier] ?? tiers.custom}`;
  }
  function createPerformanceSettingsCard() {
    const card = document.createElement("article");
    card.className = "mwi-performance-settings-card";
    card.dataset.search = "性能 引导 档位 生活 战斗 平衡 performance guide tier";
    const copy = document.createElement("div");
    copy.className = "mwi-performance-settings-copy";
    const title = document.createElement("div");
    title.className = "mwi-performance-settings-title";
    const titleText = document.createElement("span");
    titleText.textContent = runtime.config.isZH ? "性能与初始化引导" : "Performance setup guide";
    const profile = document.createElement("span");
    profile.className = "mwi-performance-settings-profile";
    const update = () => {
      profile.textContent = performanceProfileLabel();
    };
    update();
    title.append(titleText, profile);
    const summary = document.createElement("div");
    summary.className = "mwi-performance-settings-summary";
    summary.textContent = runtime.config.isZH ? "重新选择玩法、设备档位,或按功能组自定义。取消不会修改当前设置。" : "Choose your play style and device tier again, or customize each group. Cancelling keeps current settings.";
    copy.append(title, summary);
    const button = document.createElement("button");
    button.type = "button";
    button.className = "mwi-performance-settings-open";
    button.textContent = runtime.config.isZH ? "重新开始引导" : "Restart guide";
    button.addEventListener("click", () => {
      void runtime.api.openPerformanceOnboarding?.({ firstRun: false });
    });
    const onProfileChange = () => update();
    document.addEventListener(
      "mwitools:performance-profile-change",
      onProfileChange
    );
    card._mwitoolsCleanup = () => document.removeEventListener(
      "mwitools:performance-profile-change",
      onProfileChange
    );
    card.append(copy, button);
    return card;
  }
  function renderSettings(root) {
    if (!root) return;
    cleanupSettingsRoot(root);
    root.replaceChildren();
    const hero = document.createElement("div");
    hero.className = "mwi-settings-hero";
    const heroCopy = document.createElement("div");
    const heading = document.createElement("div");
    heading.className = "mwi-settings-title";
    heading.textContent = "MWITools";
    const subtitle = document.createElement("div");
    subtitle.className = "mwi-settings-subtitle";
    subtitle.textContent = runtime.config.isZH ? "所有设置会立即生效。功能数据与公会经验只保存在当前设备。" : "Settings apply immediately. Feature data and guild XP stay on this device.";
    heroCopy.append(heading, subtitle);
    const search = document.createElement("input");
    search.className = "mwi-settings-search";
    search.type = "search";
    search.placeholder = runtime.config.isZH ? "搜索功能或说明" : "Search settings";
    const heroActions = document.createElement("div");
    heroActions.className = "mwi-settings-hero-actions";
    heroActions.append(search);
    if (root.hasAttribute(SETTINGS_ROOT_ATTRIBUTE)) {
      const close = document.createElement("button");
      close.type = "button";
      close.className = "mwi-settings-close";
      close.dataset.mwitoolsSettingsClose = "";
      close.setAttribute("aria-label", runtime.config.isZH ? "关闭" : "Close");
      close.textContent = "×";
      heroActions.append(close);
    }
    hero.append(heroCopy, heroActions);
    root.append(hero);
    for (const [groupId, group] of Object.entries(runtime.settings.groups)) {
      const definitions = Object.values(runtime.settings.catalog).filter(
        (definition) => definition.group === groupId && !definition.parent && !definition.hidden && (runtime.settings.settingsMap[definition.id] || definition.control)
      );
      if (!definitions.length) continue;
      const section = document.createElement("section");
      section.className = "mwi-settings-group";
      const head = document.createElement("header");
      head.className = "mwi-settings-group-head";
      const groupTitle = document.createElement("div");
      groupTitle.className = "mwi-settings-group-title";
      groupTitle.textContent = localizedText2(group.title);
      const groupSummary = document.createElement("div");
      groupSummary.className = "mwi-settings-group-summary";
      groupSummary.textContent = localizedText2(group.summary);
      head.append(groupTitle, groupSummary);
      const grid = document.createElement("div");
      grid.className = "mwi-settings-grid";
      if (groupId === "general") {
        grid.append(
          createPerformanceSettingsCard(),
          createDuplicateWarningSettingsCard()
        );
      }
      for (const definition of definitions) {
        grid.appendChild(createSettingCard(definition));
      }
      section.append(head, grid);
      root.append(section);
    }
    search.addEventListener("input", () => {
      const query = search.value.trim().toLowerCase();
      for (const card of root.querySelectorAll(
        ".mwi-setting-card,.mwi-performance-settings-card"
      )) {
        card.hidden = Boolean(query) && !card.dataset.search.includes(query);
      }
      for (const group of root.querySelectorAll(".mwi-settings-group")) {
        group.hidden = ![
          ...group.querySelectorAll(
            ".mwi-setting-card,.mwi-performance-settings-card"
          )
        ].some((card) => !card.hidden);
      }
    });
  }
  function positionSettingsPopover() {
    const button = document.getElementById(SETTINGS_BUTTON_ID);
    const popover = document.getElementById(SETTINGS_POPOVER_ID);
    if (!button || !popover || popover.hidden) return;
    const rect = button.getBoundingClientRect();
    const viewportWidth = Math.max(
      320,
      globalThis.innerWidth || document.documentElement?.clientWidth || 0
    );
    const viewportHeight = Math.max(
      320,
      globalThis.innerHeight || document.documentElement?.clientHeight || 0
    );
    const margin = 8;
    const width = Math.min(620, viewportWidth - margin * 2);
    let top = rect.bottom + 6;
    let maxHeight = viewportHeight - top - margin;
    if (maxHeight < 240) {
      top = margin;
      maxHeight = viewportHeight - margin * 2;
    }
    const left = Math.min(
      viewportWidth - width - margin,
      Math.max(margin, rect.right - width)
    );
    Object.assign(popover.style, {
      width: `${width}px`,
      maxHeight: `${Math.max(180, maxHeight)}px`,
      left: `${left}px`,
      top: `${top}px`
    });
  }
  function closeSettingsPopover({ restoreFocus = false } = {}) {
    const button = document.getElementById(SETTINGS_BUTTON_ID);
    const popover = document.getElementById(SETTINGS_POPOVER_ID);
    if (!popover || popover.hidden) return false;
    persistSettingsPopoverScrollTop(popover.scrollTop);
    popover.hidden = true;
    button?.setAttribute("aria-expanded", "false");
    if (restoreFocus) button?.focus();
    return true;
  }
  function readSettingsPopoverScrollTop() {
    try {
      const stored = Number(localStorage.getItem(SETTINGS_POPOVER_SCROLL_KEY));
      return Number.isFinite(stored) && stored > 0 ? stored : 0;
    } catch {
      return 0;
    }
  }
  function persistSettingsPopoverScrollTop(value) {
    const scrollTop = Math.max(0, Number(value) || 0);
    try {
      localStorage.setItem(SETTINGS_POPOVER_SCROLL_KEY, String(scrollTop));
    } catch {
    }
  }
  function ensureSettingsPopover() {
    let popover = document.getElementById(SETTINGS_POPOVER_ID);
    if (popover) {
      popover.setAttribute(
        "aria-label",
        runtime.config.isZH ? "MWITools 快捷设置" : "MWITools quick settings"
      );
      return popover;
    }
    popover = document.createElement("section");
    popover.id = SETTINGS_POPOVER_ID;
    popover.hidden = true;
    popover.setAttribute("role", "dialog");
    popover.setAttribute(
      "aria-label",
      runtime.config.isZH ? "MWITools 快捷设置" : "MWITools quick settings"
    );
    const root = document.createElement("div");
    root.setAttribute(SETTINGS_ROOT_ATTRIBUTE, "");
    popover.append(root);
    popover.addEventListener("click", (event) => {
      if (event.target.closest?.("[data-mwitools-settings-close]")) {
        closeSettingsPopover({ restoreFocus: true });
      }
    });
    popover.addEventListener(
      "scroll",
      () => persistSettingsPopoverScrollTop(popover.scrollTop),
      { passive: true }
    );
    document.body.append(popover);
    renderSettings(root);
    return popover;
  }
  function toggleSettingsPopover() {
    const button = document.getElementById(SETTINGS_BUTTON_ID);
    const popover = ensureSettingsPopover();
    if (!button) return false;
    if (!popover.hidden) return closeSettingsPopover();
    renderSettings(popover.querySelector(`[${SETTINGS_ROOT_ATTRIBUTE}]`));
    popover.hidden = false;
    button.setAttribute("aria-expanded", "true");
    positionSettingsPopover();
    popover.querySelector(".mwi-settings-search")?.focus({ preventScroll: true });
    popover.scrollTop = readSettingsPopoverScrollTop();
    return true;
  }
  function ensureSettingsLauncher() {
    const host = ensureHeaderToolsHost();
    if (!host) return null;
    let button = document.getElementById(SETTINGS_BUTTON_ID);
    if (!button) {
      button = document.createElement("button");
      button.type = "button";
      button.id = SETTINGS_BUTTON_ID;
      button.textContent = "⚙";
      button.setAttribute("aria-expanded", "false");
      button.setAttribute("aria-controls", SETTINGS_POPOVER_ID);
      button.addEventListener("click", () => toggleSettingsPopover());
    }
    button.setAttribute(
      "aria-label",
      runtime.config.isZH ? "MWITools 快捷设置" : "MWITools quick settings"
    );
    if (button.parentElement !== host || button !== host.firstElementChild) {
      host.prepend(button);
    }
    return button;
  }
  function restoreNativeSettingsPanels(tabList, panelsContainer, selectedTab) {
    const customTab = tabList?.querySelector(`[${SETTINGS_TAB_ATTRIBUTE}]`);
    const customPanel = panelsContainer?.querySelector(
      `[${SETTINGS_PANEL_ATTRIBUTE}]`
    );
    if (!customTab || !customPanel) return;
    customTab.setAttribute("aria-selected", "false");
    customTab.tabIndex = -1;
    customTab.classList.remove("Mui-selected");
    customPanel.hidden = true;
    const nativeTabs = [
      ...tabList.querySelectorAll(
        'button[role="tab"]:not([data-mwitools-settings-tab])'
      )
    ];
    if (selectedTab) {
      for (const tab of nativeTabs) {
        const selected = tab === selectedTab;
        tab.setAttribute("aria-selected", String(selected));
        tab.tabIndex = selected ? 0 : -1;
        tab.classList.toggle("Mui-selected", selected);
      }
    }
    for (const panel of panelsContainer.children) {
      if (panel !== customPanel) panel.hidden = false;
    }
    const indicator = tabList.querySelector('[class*="MuiTabs-indicator"]');
    if (indicator?.dataset.mwitoolsOriginalStyle != null) {
      const originalStyle = indicator.dataset.mwitoolsOriginalStyle;
      if (originalStyle) indicator.setAttribute("style", originalStyle);
      else indicator.removeAttribute("style");
      delete indicator.dataset.mwitoolsOriginalStyle;
    }
  }
  function ensureSettingsPanel() {
    const settingsPanel = document.querySelector(
      '[class*="SettingsPanel_settingsPanel"]'
    );
    const tabList = settingsPanel?.querySelector('[role="tablist"]');
    const nativePanels = [
      ...settingsPanel?.querySelectorAll(
        '[class*="TabPanel_tabPanel"]:not([data-mwitools-settings-panel])'
      ) ?? []
    ];
    const panelsContainer = settingsPanel?.querySelector(
      '[class*="TabsComponent_tabPanelsContainer"]'
    ) ?? nativePanels[0]?.parentElement;
    const nativeTabs = [
      ...tabList?.querySelectorAll(
        'button[role="tab"]:not([data-mwitools-settings-tab])'
      ) ?? []
    ];
    if (!tabList || !panelsContainer || !nativeTabs.length || !nativePanels.length)
      return;
    let customTab = tabList.querySelector(`[${SETTINGS_TAB_ATTRIBUTE}]`);
    if (!customTab) {
      customTab = nativeTabs.at(-1).cloneNode(true);
      customTab.removeAttribute("id");
      customTab.removeAttribute("aria-controls");
      customTab.setAttribute(SETTINGS_TAB_ATTRIBUTE, "");
      customTab.setAttribute("aria-selected", "false");
      customTab.tabIndex = -1;
      customTab.classList.remove("Mui-selected");
      customTab.textContent = "MWITools";
      tabList.append(customTab);
    }
    let customPanel = panelsContainer.querySelector(
      `[${SETTINGS_PANEL_ATTRIBUTE}]`
    );
    if (!customPanel) {
      customPanel = document.createElement("div");
      customPanel.className = nativePanels[0].className;
      customPanel.setAttribute(SETTINGS_PANEL_ATTRIBUTE, "");
      customPanel.setAttribute("role", "tabpanel");
      customPanel.hidden = true;
      panelsContainer.append(customPanel);
    }
    let root = document.querySelector("#script_settings");
    if (!root) {
      root = document.createElement("div");
      root.id = "script_settings";
    }
    if (root.parentElement !== customPanel) customPanel.append(root);
    if (root.dataset.mwitoolsVersion !== "3") {
      root.dataset.mwitoolsVersion = "3";
      renderSettings(root);
    }
    if (!customTab._mwitoolsActivateSettings) {
      const activate = () => {
        for (const tab of nativeTabs) {
          tab.setAttribute("aria-selected", "false");
          tab.tabIndex = -1;
          tab.classList.remove("Mui-selected");
        }
        for (const panel of nativePanels) panel.hidden = true;
        customTab.setAttribute("aria-selected", "true");
        customTab.tabIndex = 0;
        customTab.classList.add("Mui-selected");
        customPanel.hidden = false;
        const indicator = tabList.querySelector('[class*="MuiTabs-indicator"]');
        if (indicator) {
          if (indicator.dataset.mwitoolsOriginalStyle == null) {
            indicator.dataset.mwitoolsOriginalStyle = indicator.getAttribute("style") ?? "";
          }
          indicator.style.left = `${customTab.offsetLeft}px`;
          indicator.style.width = `${customTab.offsetWidth}px`;
        }
      };
      customTab.addEventListener("click", activate);
      customTab._mwitoolsActivateSettings = activate;
    }
    if (!tabList._mwitoolsRestoreSettings) {
      const restore = (event) => {
        if (event.target.closest?.(`[${SETTINGS_TAB_ATTRIBUTE}]`)) return;
        const selectedTab = event.target.closest?.('button[role="tab"]');
        if (!selectedTab) return;
        setTimeout(
          () => restoreNativeSettingsPanels(tabList, panelsContainer, selectedTab)
        );
      };
      tabList.addEventListener("click", restore);
      tabList._mwitoolsRestoreSettings = restore;
    }
  }
  function getEquipmentWarning() {
    if (runtime.state.labyrinthActive) return null;
    const currentActionHrid = runtime.state.currentActionsHridList?.[0]?.actionHrid;
    if (!currentActionHrid) return null;
    const hasHat = runtime.state.currentEquipmentMap["/item_locations/head"]?.itemHrid === "/items/red_culinary_hat" ? true : false;
    const hasOffHand = runtime.state.currentEquipmentMap["/item_locations/off_hand"]?.itemHrid === "/items/eye_watch" ? true : false;
    const hasBoot = runtime.state.currentEquipmentMap["/item_locations/feet"]?.itemHrid === "/items/collectors_boots" ? true : false;
    const hasGlove = runtime.state.currentEquipmentMap["/item_locations/hands"]?.itemHrid === "/items/enchanted_gloves" ? true : false;
    if (currentActionHrid.includes("/actions/combat/")) {
      if (hasHat || hasOffHand || hasBoot || hasGlove) {
        return {
          code: "skilling-gear-in-combat",
          text: runtime.config.isZH ? "正在穿着生活装备" : "Skilling gear equipped in combat"
        };
      }
    } else if (currentActionHrid.includes("/actions/cooking/") || currentActionHrid.includes("/actions/brewing/")) {
      if (!hasHat && hasItemHridInInv("/items/red_culinary_hat")) {
        return {
          code: "missing-production-hat",
          itemHrid: "/items/red_culinary_hat",
          text: runtime.config.isZH ? "未装备生活帽" : "Skilling hat not equipped"
        };
      }
    } else if (currentActionHrid.includes("/actions/cheesesmithing/") || currentActionHrid.includes("/actions/crafting/") || currentActionHrid.includes("/actions/tailoring/")) {
      if (!hasOffHand && hasItemHridInInv("/items/eye_watch")) {
        return {
          code: "missing-production-off-hand",
          itemHrid: "/items/eye_watch",
          text: runtime.config.isZH ? "未装备生活副手" : "Skilling off-hand not equipped"
        };
      }
    } else if (currentActionHrid.includes("/actions/milking/") || currentActionHrid.includes("/actions/foraging/") || currentActionHrid.includes("/actions/woodcutting/")) {
      if (!hasBoot && hasItemHridInInv("/items/collectors_boots")) {
        return {
          code: "missing-production-boots",
          itemHrid: "/items/collectors_boots",
          text: runtime.config.isZH ? "未装备生活鞋" : "Skilling boots not equipped"
        };
      }
    } else if (currentActionHrid.includes("/actions/enhancing")) {
      if (!hasGlove && hasItemHridInInv("/items/enchanted_gloves")) {
        return {
          code: "missing-enhancing-gloves",
          itemHrid: "/items/enchanted_gloves",
          text: runtime.config.isZH ? "未装备强化手套" : "Enhancing gloves not equipped"
        };
      }
    }
    return null;
  }
  function addEquipmentWarningStyles() {
    if (document.getElementById(EQUIPMENT_WARNING_STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = EQUIPMENT_WARNING_STYLE_ID;
    style.textContent = `
    .mwi-equipment-warning-host { position:relative!important; }
    @keyframes mwi-equipment-warning-pulse { 0%,100% { box-shadow:0 0 0 2px rgba(255,75,75,.38),0 2px 10px rgba(0,0,0,.42); } 50% { box-shadow:0 0 0 4px rgba(255,75,75,.16),0 2px 12px rgba(0,0,0,.5); } }
    #script_item_warning { position:absolute; z-index:7; display:flex; box-sizing:border-box; min-width:28px; max-width:var(--mwi-equipment-warning-space,216px); height:22px; align-items:center; gap:5px; padding:1px 7px; border:2px solid #ff5b5b; outline:1px solid rgba(255,194,194,.72); outline-offset:2px; border-radius:999px; background:rgba(91,14,22,.96); color:#fff4f4; box-shadow:0 0 0 2px rgba(255,75,75,.38),0 2px 10px rgba(0,0,0,.42); text-shadow:0 1px 1px rgba(0,0,0,.9); font:inherit; font-size:calc(.6875rem * var(--mwi-ui-font-scale,1)); font-weight:750; line-height:1; white-space:nowrap; overflow:hidden; pointer-events:none; animation:mwi-equipment-warning-pulse 1.8s ease-in-out infinite; }
    .mwi-equipment-warning-icon { flex:0 0 auto; color:#ffb7b7; font-size:.78rem; }
    .mwi-equipment-warning-text { min-width:0; overflow:hidden; text-overflow:ellipsis; }
    @media(prefers-reduced-motion:reduce) { #script_item_warning { animation:none; } }
    @media(max-width:680px) { #script_item_warning { width:28px; max-width:28px; justify-content:center; padding:2px; } .mwi-equipment-warning-text { display:none; } }
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function removeEquipmentWarning() {
    document.querySelector("#script_item_warning")?.remove();
    document.querySelectorAll(".mwi-equipment-warning-host").forEach((host) => host.classList.remove("mwi-equipment-warning-host"));
  }
  function positionEquipmentWarning(warning, host, communityBuffs) {
    const hostRect = host.getBoundingClientRect();
    const anchorRect = communityBuffs.getBoundingClientRect();
    const left = Math.max(0, anchorRect.left - hostRect.left);
    const top = Math.max(0, anchorRect.bottom - hostRect.top + 4);
    const viewportWidth = host.ownerDocument?.defaultView?.innerWidth ?? 0;
    const availableInViewport = viewportWidth ? Math.max(26, viewportWidth - hostRect.left - left - 12) : anchorRect.width;
    warning.style.left = `${left}px`;
    warning.style.top = `${top}px`;
    warning.style.setProperty(
      "--mwi-equipment-warning-space",
      `${Math.min(216, anchorRect.width || 216, availableInViewport)}px`
    );
  }
  function checkEquipment() {
    const warningState = getEquipmentWarning();
    const host = document.querySelector('div[class*="Header_actionInfo"]');
    const communityBuffs = host?.querySelector(
      'div[class*="Header_communityBuffs"]'
    );
    if (!warningState || !host || !communityBuffs) {
      removeEquipmentWarning();
      return warningState;
    }
    addEquipmentWarningStyles();
    document.querySelectorAll(".mwi-equipment-warning-host").forEach((element) => {
      if (element !== host)
        element.classList.remove("mwi-equipment-warning-host");
    });
    host.classList.add("mwi-equipment-warning-host");
    let warning = document.querySelector("#script_item_warning");
    if (!warning) {
      warning = document.createElement("div");
      warning.id = "script_item_warning";
      warning.setAttribute("role", "status");
      const icon = document.createElement("span");
      icon.className = "mwi-equipment-warning-icon";
      icon.textContent = "⚠";
      const text = document.createElement("span");
      text.className = "mwi-equipment-warning-text";
      warning.append(icon, text);
    }
    if (warning.parentElement !== host) host.appendChild(warning);
    warning.dataset.code = warningState.code;
    warning.querySelector(".mwi-equipment-warning-text").textContent = warningState.text;
    warning.title = warningState.text;
    positionEquipmentWarning(warning, host, communityBuffs);
    return warningState;
  }
  function hasItemHridInInv(hrid) {
    let result = null;
    for (const item of runtime.state.initData_characterItems) {
      if (item.itemHrid === hrid && item.itemLocationHrid === "/item_locations/inventory") {
        result = item;
      }
    }
    return result ? true : false;
  }
  function notificate() {
    if (typeof GM_notification === "undefined" || !GM_notification) {
      console.error(
        runtime.config.isZH ? "[MWITools] 当前环境不支持系统通知。" : "[MWITools] System notifications are unavailable."
      );
      return;
    }
    if (runtime.state.currentActionsHridList.length > 0) {
      return;
    }
    console.log(
      runtime.config.isZH ? "[MWITools] 行动队列为空,发送系统通知。" : "[MWITools] Action queue is empty; sending a notification."
    );
    GM_notification({
      text: runtime.config.isZH ? "动作队列为空" : "Action queue is empty.",
      title: "MWITools"
    });
  }
  function handleMarketNewOrder(node) {
    const title = runtime.api.getOriTextFromElement(
      node.querySelector(".MarketplacePanel_header__yahJo")
    );
    const normalizedTitle = String(title ?? "").toLocaleLowerCase();
    const immediateTitles = [
      getGameTranslation("marketplacePanel.buyNow"),
      getGameTranslation("marketplacePanel.sellNow"),
      "Buy Now",
      "Sell Now",
      "立即购买",
      "立即出售"
    ].filter(Boolean).map((value) => value.toLocaleLowerCase());
    if (!title || immediateTitles.some((value) => normalizedTitle.includes(value))) {
      return;
    }
    const label = node.querySelector("span.MarketplacePanel_bestPrice__3bgKp");
    const inputDiv = node.querySelector(
      ".MarketplacePanel_inputContainer__3xmB2 .MarketplacePanel_priceInputs__3iWxy"
    );
    if (!label || !inputDiv) {
      console.error(
        runtime.config.isZH ? "[MWITools] 市场订单窗口缺少价格输入控件。" : "[MWITools] The market order dialog is missing price controls."
      );
      return;
    }
    label.click();
    const clickAdjustmentButton = (direction) => {
      const buttons = [...inputDiv.querySelectorAll("button")];
      const target = buttons.find((button) => {
        const label2 = `${button.textContent} ${button.getAttribute("aria-label") ?? ""} ${button.title ?? ""}`.trim().toLowerCase();
        if (direction === "increase") {
          return label2 === "+" || label2.includes("increase");
        }
        return label2 === "-" || label2 === "−" || label2.includes("decrease");
      });
      target?.click();
      return Boolean(target);
    };
    const priceLabel = String(
      runtime.api.getOriTextFromElement(label.parentElement) ?? label.parentElement.textContent ?? ""
    ).toLocaleLowerCase();
    const buyLabel = getGameTranslation(
      "marketplacePanel.buy"
    ).toLocaleLowerCase();
    const sellLabel = getGameTranslation(
      "marketplacePanel.sell"
    ).toLocaleLowerCase();
    if (matchesGameTranslation("marketplacePanel.priceBestBuyOffer", priceLabel) || priceLabel.includes("best buy") || priceLabel.includes("购买") || buyLabel && priceLabel.includes(buyLabel)) {
      if (!clickAdjustmentButton("increase")) {
        console.error(
          runtime.config.isZH ? "[MWITools] 未找到提高收购价按钮。" : "[MWITools] The increase-bid-price button was not found."
        );
      }
    } else if (matchesGameTranslation("marketplacePanel.priceBestSellOffer", priceLabel) || priceLabel.includes("best sell") || priceLabel.includes("出售") || sellLabel && priceLabel.includes(sellLabel)) {
      if (!clickAdjustmentButton("decrease")) {
        console.error(
          runtime.config.isZH ? "[MWITools] 未找到降低出售价按钮。" : "[MWITools] The decrease-ask-price button was not found."
        );
      }
    }
  }
  Object.assign(runtime.api, {
    persistSettings,
    readSettings,
    getTooltipProfitShortcut,
    setTooltipProfitShortcut,
    matchesTooltipProfitShortcut,
    getGuildCreditRecommendationCount,
    setGuildCreditRecommendationCount,
    getEquipmentWarning,
    checkEquipment,
    hasItemHridInInv,
    notificate,
    handleMarketNewOrder
  });
  runtime.features.register({
    id: "settingsUi",
    scope: "global",
    initialize({ scope }) {
      addSettingsStyles();
      ensureSettingsPanel();
      ensureSettingsLauncher();
      const render = () => {
        addSettingsStyles();
        ensureSettingsPanel();
        ensureSettingsLauncher();
      };
      const scheduler = createFrameScheduler(render);
      subscribeMutationChannel(
        {
          name: "header-mount",
          target: document.body,
          options: { childList: true, subtree: true },
          scope
        },
        (records) => {
          const relevant = records.some((record) => {
            const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
            if (target?.closest?.(
              `#script_settings,[${SETTINGS_ROOT_ATTRIBUTE}],[${SETTINGS_TAB_ATTRIBUTE}],[${SETTINGS_PANEL_ATTRIBUTE}],#${SETTINGS_BUTTON_ID},#${SETTINGS_POPOVER_ID},#${HEADER_TOOLS_ID}`
            )) {
              return false;
            }
            if (target?.closest?.('[class*="SettingsPanel_settingsPanel"]')) {
              return true;
            }
            return [...record.addedNodes, ...record.removedNodes].some(
              (node) => node?.nodeType === 1 && (node.matches?.(
                '[class*="SettingsPanel_settingsPanel"],[class*="Header_totalLevel"],[class*="totalLevel"]'
              ) || node.querySelector?.(
                '[class*="SettingsPanel_settingsPanel"],[class*="Header_totalLevel"],[class*="totalLevel"]'
              ))
            );
          });
          if (relevant) scheduler.schedule();
        }
      );
      scope.event(document, "click", (event) => {
        const popover = document.getElementById(SETTINGS_POPOVER_ID);
        if (!popover || popover.hidden || popover.contains(event.target) || event.target.closest?.(`#${SETTINGS_BUTTON_ID}`)) {
          return;
        }
        closeSettingsPopover();
      });
      scope.event(document, "keydown", (event) => {
        if (event.key === "Escape") {
          closeSettingsPopover({ restoreFocus: true });
        }
      });
      scope.event(globalThis, "resize", positionSettingsPopover);
      scope.event(globalThis, "scroll", positionSettingsPopover, true);
      scope.add(() => {
        scheduler.cancel();
        for (const root of settingsRoots()) cleanupSettingsRoot(root);
        const customTab = document.querySelector(`[${SETTINGS_TAB_ATTRIBUTE}]`);
        const tabList = customTab?.parentElement;
        if (tabList?._mwitoolsRestoreSettings) {
          tabList.removeEventListener("click", tabList._mwitoolsRestoreSettings);
          delete tabList._mwitoolsRestoreSettings;
        }
        customTab?.remove();
        document.querySelector(`[${SETTINGS_PANEL_ATTRIBUTE}]`)?.remove();
        document.querySelector("#script_settings")?.remove();
        document.getElementById(SETTINGS_POPOVER_ID)?.remove();
        document.getElementById(SETTINGS_BUTTON_ID)?.remove();
        removeHeaderToolsHostIfEmpty();
        document.getElementById(SETTINGS_STYLE_ID)?.remove();
      });
    }
  });

  // src/core/performance-profile.js
  var PERFORMANCE_PROFILE_STORAGE_KEY = "MWITools_performance_profile_v1";
  var PERFORMANCE_PROFILE_VERSION = 1;
  var USAGES = Object.freeze(["life", "combat", "balanced"]);
  var TIERS = Object.freeze(["smooth", "standard", "full", "custom"]);
  var REFRESH_INTERVALS = Object.freeze([1e3, 2e3]);
  var EXISTING_SETTINGS_AT_LOAD = Boolean(
    globalThis.localStorage?.getItem("MWITools_settings_v2") || globalThis.localStorage?.getItem("script_settingsMap")
  );
  var GUILD_SETTINGS = Object.freeze([
    "guildXpTracking",
    "guildOverview",
    "guildMemberXp",
    "guildLeaderboardXp",
    "guildIdleMembers",
    "leaderboardOverlay",
    "leaderboardXpRate"
  ]);
  var COMPLEX_CALCULATION_SETTINGS = Object.freeze([
    "actionBarProfit",
    "productionProfit",
    "itemTooltip_profit",
    "lootChestEstimate",
    "enhanceSim"
  ]);
  var DEFAULT_CHOICES = Object.freeze({
    dps: true,
    battleBuffs: true,
    taskEnhancements: true,
    taskArt: true,
    assetHistory: true,
    totalAssetsAndSort: true,
    productionSummary: true,
    complexCalculations: true,
    guildEnhancements: true,
    decorativeAnimations: true,
    dpsGraph: false,
    refreshIntervalMs: 1e3
  });
  function normalizeUsage(value) {
    return USAGES.includes(value) ? value : "balanced";
  }
  function normalizeTier(value) {
    return TIERS.includes(value) ? value : "standard";
  }
  function normalizeRefreshInterval(value) {
    const interval = Number(value);
    return REFRESH_INTERVALS.includes(interval) ? interval : 1e3;
  }
  function normalizeChoices(value = {}, fallback = DEFAULT_CHOICES) {
    return {
      dps: Boolean(value.dps ?? fallback.dps),
      battleBuffs: Boolean(value.battleBuffs ?? fallback.battleBuffs),
      taskEnhancements: Boolean(
        value.taskEnhancements ?? fallback.taskEnhancements
      ),
      taskArt: Boolean(value.taskArt ?? fallback.taskArt),
      assetHistory: Boolean(value.assetHistory ?? fallback.assetHistory),
      totalAssetsAndSort: Boolean(
        value.totalAssetsAndSort ?? fallback.totalAssetsAndSort
      ),
      productionSummary: Boolean(
        value.productionSummary ?? fallback.productionSummary
      ),
      complexCalculations: Boolean(
        value.complexCalculations ?? fallback.complexCalculations
      ),
      guildEnhancements: Boolean(
        value.guildEnhancements ?? fallback.guildEnhancements
      ),
      decorativeAnimations: Boolean(
        value.decorativeAnimations ?? fallback.decorativeAnimations
      ),
      dpsGraph: Boolean(value.dpsGraph ?? fallback.dpsGraph),
      refreshIntervalMs: normalizeRefreshInterval(
        value.refreshIntervalMs ?? fallback.refreshIntervalMs
      )
    };
  }
  function normalizeStoredProfile(value) {
    const profile = value && typeof value === "object" ? value : {};
    return {
      version: PERFORMANCE_PROFILE_VERSION,
      completed: profile.version === PERFORMANCE_PROFILE_VERSION && profile.completed === true,
      usage: normalizeUsage(profile.usage),
      tier: normalizeTier(profile.tier),
      choices: normalizeChoices(profile.choices)
    };
  }
  function loadStoredProfile() {
    try {
      return normalizeStoredProfile(
        JSON.parse(
          globalThis.localStorage?.getItem(PERFORMANCE_PROFILE_STORAGE_KEY) || "null"
        )
      );
    } catch {
      return normalizeStoredProfile(null);
    }
  }
  var storedProfile = loadStoredProfile();
  function saveStoredProfile(profile) {
    storedProfile = normalizeStoredProfile({
      ...profile,
      version: PERFORMANCE_PROFILE_VERSION,
      completed: true
    });
    globalThis.localStorage?.setItem(
      PERFORMANCE_PROFILE_STORAGE_KEY,
      JSON.stringify(storedProfile)
    );
    return storedProfile;
  }
  function resolveConditionalRule(rule, usage) {
    const normalizedUsage = normalizeUsage(usage);
    if (rule === "all-on") return true;
    if (rule === "all-off") return false;
    if (rule === "combat-on") return normalizedUsage !== "life";
    if (rule === "life-on") return normalizedUsage !== "combat";
    return Boolean(rule);
  }
  function resolvePresetChoices(usage, tier) {
    const normalizedUsage = normalizeUsage(usage);
    const normalizedTier = normalizeTier(tier);
    if (normalizedTier === "custom") return normalizeChoices();
    return normalizeChoices({
      dps: resolveConditionalRule(
        normalizedTier === "smooth" ? "combat-on" : "all-on",
        normalizedUsage
      ),
      battleBuffs: resolveConditionalRule(
        normalizedTier === "smooth" ? "all-off" : normalizedTier === "standard" ? "combat-on" : "all-on",
        normalizedUsage
      ),
      taskEnhancements: true,
      taskArt: true,
      assetHistory: normalizedTier !== "smooth",
      totalAssetsAndSort: true,
      productionSummary: true,
      complexCalculations: true,
      guildEnhancements: true,
      decorativeAnimations: normalizedTier !== "smooth",
      dpsGraph: normalizedTier === "full",
      refreshIntervalMs: normalizedTier === "smooth" ? 2e3 : 1e3
    });
  }
  function configurationFromChoices(choices) {
    const normalized = normalizeChoices(choices);
    const values = {
      showDamage: normalized.dps,
      battleBuffs: normalized.battleBuffs,
      taskInsights: normalized.taskEnhancements,
      taskIcons: normalized.taskEnhancements && normalized.taskArt,
      assetHistory: normalized.assetHistory,
      invWorth: normalized.totalAssetsAndSort,
      invSort: normalized.totalAssetsAndSort,
      actionPanel_totalTime: true,
      productionSummary: normalized.productionSummary,
      itemTooltip_prices: true,
      itemTooltip_profitRequireKey: true,
      leaderboardBadgeGlint: normalized.guildEnhancements && normalized.decorativeAnimations
    };
    for (const id of COMPLEX_CALCULATION_SETTINGS) {
      values[id] = normalized.complexCalculations;
    }
    for (const id of GUILD_SETTINGS) {
      values[id] = normalized.guildEnhancements;
    }
    return {
      values,
      preferences: {
        productionSummaryMode: normalized.productionSummary ? "collapsed" : "off"
      },
      dps: {
        showGraph: normalized.dpsGraph,
        recountShowGraph: normalized.dpsGraph,
        refreshIntervalMs: normalized.refreshIntervalMs
      },
      decorativeAnimations: normalized.decorativeAnimations
    };
  }
  function currentChoices() {
    const get = (id) => Boolean(runtime.settings.get?.(id));
    const dps = runtime.api.dpsPerformance?.get?.() ?? {};
    const decorativeMotion = globalThis.document?.documentElement?.dataset.mwitoolsDecorativeMotion;
    const summaryMode = runtime.settings.getPreference?.("productionSummaryMode") ?? "collapsed";
    return normalizeChoices({
      dps: get("showDamage"),
      battleBuffs: get("battleBuffs"),
      taskEnhancements: get("taskInsights"),
      taskArt: get("taskIcons"),
      assetHistory: get("assetHistory"),
      totalAssetsAndSort: get("invWorth") && get("invSort"),
      productionSummary: get("productionSummary") && summaryMode !== "off",
      complexCalculations: COMPLEX_CALCULATION_SETTINGS.every(get),
      guildEnhancements: GUILD_SETTINGS.every(get),
      decorativeAnimations: decorativeMotion === "on" || decorativeMotion !== "off" && get("leaderboardBadgeGlint"),
      dpsGraph: Boolean(dps.showGraph && dps.recountShowGraph),
      refreshIntervalMs: dps.refreshIntervalMs
    });
  }
  function choicesMatch(left, right) {
    return Object.keys(DEFAULT_CHOICES).every((key) => left[key] === right[key]);
  }
  function applyDecorativeMotion(enabled) {
    const root = globalThis.document?.documentElement;
    if (root) root.dataset.mwitoolsDecorativeMotion = enabled ? "on" : "off";
  }
  function emitProfileChange() {
    const EventRef = globalThis.CustomEvent ?? globalThis.document?.defaultView?.CustomEvent;
    if (!EventRef || !globalThis.document?.dispatchEvent) return;
    globalThis.document.dispatchEvent(
      new EventRef("mwitools:performance-profile-change", {
        detail: getProfileState()
      })
    );
  }
  function getProfileState() {
    const current = currentChoices();
    const tier = normalizeTier(storedProfile.tier);
    const matches = tier !== "custom" && choicesMatch(current, resolvePresetChoices(storedProfile.usage, tier));
    return {
      ...storedProfile,
      tier: matches ? tier : "custom",
      choices: current
    };
  }
  function shouldRunPerformanceOnboarding() {
    return !storedProfile.completed;
  }
  function recommendPerformanceTier(windowRef = globalThis.window ?? globalThis) {
    const coarse = Boolean(
      windowRef.matchMedia?.("(any-pointer: coarse)")?.matches
    );
    const narrow = Number(windowRef.innerWidth) > 0 && windowRef.innerWidth <= 760;
    return coarse || narrow ? "smooth" : "standard";
  }
  function hasExistingSettingsAtLoad() {
    return EXISTING_SETTINGS_AT_LOAD;
  }
  async function applyPerformanceProfile({
    usage = "balanced",
    tier = "standard",
    choices = null
  } = {}) {
    const normalizedUsage = normalizeUsage(usage);
    const normalizedTier = normalizeTier(tier);
    const resolvedChoices = normalizedTier === "custom" ? normalizeChoices(choices, currentChoices()) : resolvePresetChoices(normalizedUsage, normalizedTier);
    const configuration = configurationFromChoices(resolvedChoices);
    if (typeof runtime.settings.applyBatch !== "function") {
      throw new Error(
        runtime.config.isZH ? "MWITools 批量设置接口不可用" : "MWITools batch settings API is unavailable"
      );
    }
    await runtime.settings.applyBatch({
      values: configuration.values,
      preferences: configuration.preferences
    });
    runtime.api.dpsPerformance?.set?.(configuration.dps);
    applyDecorativeMotion(configuration.decorativeAnimations);
    saveStoredProfile({
      usage: normalizedUsage,
      tier: normalizedTier,
      choices: resolvedChoices
    });
    emitProfileChange();
    return getProfileState();
  }
  async function completePerformanceOnboardingWithoutChanges() {
    if (!EXISTING_SETTINGS_AT_LOAD) {
      return applyPerformanceProfile({ usage: "balanced", tier: "standard" });
    }
    const choices = currentChoices();
    applyDecorativeMotion(choices.decorativeAnimations);
    saveStoredProfile({
      usage: storedProfile.usage,
      tier: "custom",
      choices
    });
    emitProfileChange();
    return getProfileState();
  }
  function initializePerformancePolicy() {
    applyDecorativeMotion(storedProfile.choices.decorativeAnimations);
    return getProfileState();
  }
  function resetPerformanceProfileForTests() {
    storedProfile = loadStoredProfile();
    initializePerformancePolicy();
  }
  Object.assign(runtime.api, {
    performanceProfiles: {
      apply: applyPerformanceProfile,
      completeWithoutChanges: completePerformanceOnboardingWithoutChanges,
      currentChoices,
      getState: getProfileState,
      hasExistingSettingsAtLoad,
      initializePolicy: initializePerformancePolicy,
      recommendTier: recommendPerformanceTier,
      resolveConditionalRule,
      resolvePresetChoices,
      shouldRun: shouldRunPerformanceOnboarding
    }
  });

  // src/features/performance-onboarding.js
  var PERFORMANCE_ONBOARDING_ID = "mwitools-performance-onboarding";
  var PERFORMANCE_STYLE_ID = "mwitools-performance-onboarding-style";
  var TEXT = Object.freeze({
    usage: {
      life: { zh: "生活", en: "Skilling" },
      combat: { zh: "战斗", en: "Combat" },
      balanced: { zh: "平衡", en: "Balanced" }
    },
    tier: {
      smooth: { zh: "流畅优先", en: "Smooth" },
      standard: { zh: "标准", en: "Standard" },
      full: { zh: "完整功能", en: "Full features" },
      custom: { zh: "自定义", en: "Custom" }
    }
  });
  var CUSTOM_GROUPS = Object.freeze([
    {
      id: "combat",
      title: { zh: "战斗与刷新", en: "Combat & refresh" },
      fields: ["dps", "battleBuffs", "dpsGraph", "refreshIntervalMs"]
    },
    {
      id: "tasks-assets",
      title: { zh: "任务与资产", en: "Tasks & assets" },
      fields: [
        "taskEnhancements",
        "taskArt",
        "assetHistory",
        "totalAssetsAndSort"
      ]
    },
    {
      id: "production",
      title: { zh: "生产与悬浮计算", en: "Production & tooltip calculations" },
      fields: ["productionSummary", "complexCalculations"]
    },
    {
      id: "guild-visual",
      title: { zh: "公会与视觉", en: "Guild & visuals" },
      fields: ["guildEnhancements", "decorativeAnimations"]
    }
  ]);
  var FIELD_TEXT = Object.freeze({
    dps: {
      title: { zh: "DPS / HPS / 承伤", en: "DPS / HPS / damage taken" },
      summary: {
        zh: "记录实时战斗、片段和历史统计。",
        en: "Track live combat, segments, and history."
      }
    },
    battleBuffs: {
      title: { zh: "战斗 Buff 倒计时", en: "Combat buff countdowns" },
      summary: {
        zh: "在战斗单位下显示增益、减益与剩余时间。",
        en: "Show buffs, debuffs, and remaining time below combat units."
      }
    },
    taskEnhancements: {
      title: { zh: "任务增强布局", en: "Enhanced task layout" },
      summary: {
        zh: "启用任务平铺、整理、筛选与相关增强。",
        en: "Enable flat tasks, organization, filters, and related enhancements."
      }
    },
    taskArt: {
      title: { zh: "任务背景图", en: "Task artwork" },
      summary: {
        zh: "显示任务物品、怪物与副本的原生图标。",
        en: "Show native item, monster, and dungeon artwork."
      }
    },
    assetHistory: {
      title: { zh: "资产历史图表", en: "Asset history charts" },
      summary: {
        zh: "保存每日资产快照并显示盈亏趋势。",
        en: "Store daily asset snapshots and show P/L trends."
      }
    },
    totalAssetsAndSort: {
      title: { zh: "总资产与库存排序", en: "Total assets & inventory sorting" },
      summary: {
        zh: "计算着装评分、总资产并按价值整理库存。",
        en: "Calculate gear scores and assets and sort inventory by value."
      }
    },
    productionSummary: {
      title: { zh: "生产摘要", en: "Production summary" },
      summary: {
        zh: "开启时默认折叠显示产出、库存与最大次数。",
        en: "Show output, inventory, and maximum count collapsed by default."
      }
    },
    complexCalculations: {
      title: {
        zh: "利润与复杂悬浮计算",
        en: "Profit & detailed tooltip calculations"
      },
      summary: {
        zh: "开启后仍需按键或移动端长按才显示复杂详情。",
        en: "Detailed calculations still require a held key or mobile long press."
      }
    },
    guildEnhancements: {
      title: {
        zh: "公会趋势与排行榜增强",
        en: "Guild trends & leaderboard enhancements"
      },
      summary: {
        zh: "显示公会经验趋势、成员速率、名次徽章与排行榜速率。",
        en: "Show guild trends, member rates, rank badges, and leaderboard rates."
      }
    },
    decorativeAnimations: {
      title: { zh: "持续装饰动画", en: "Continuous decorative motion" },
      summary: {
        zh: "控制徽章扫光、提示脉冲等持续动画。",
        en: "Control badge glints, notification pulses, and similar motion."
      }
    },
    dpsGraph: {
      title: { zh: "DPS 趋势图默认显示", en: "Show DPS trend by default" },
      summary: {
        zh: "同时控制新版和兼容面板的趋势图初始状态。",
        en: "Set the initial graph state in both modern and compatible panels."
      }
    },
    refreshIntervalMs: {
      title: { zh: "可见面板刷新间隔", en: "Visible panel refresh interval" },
      summary: {
        zh: "只调整定时刷新的 MWITools 面板;事件驱动功能保持即时。",
        en: "Adjust timed MWITools panels; event-driven features stay immediate."
      }
    }
  });
  function isZH2() {
    return Boolean(runtime.config.isZH);
  }
  function t19(value) {
    if (typeof value === "string") return value;
    return value?.[isZH2() ? "zh" : "en"] ?? "";
  }
  function addStyles16() {
    if (document.getElementById(PERFORMANCE_STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = PERFORMANCE_STYLE_ID;
    style.textContent = `
    #${PERFORMANCE_ONBOARDING_ID}{position:fixed;inset:0;z-index:2147483400;display:grid;grid-template:1fr/1fr;opacity:0;transition:opacity .3s linear;color:var(--color-neutral-100,#e7e7e7);font:14px/1.5 Roboto,Helvetica,Arial,sans-serif}
    #${PERFORMANCE_ONBOARDING_ID}.mwi-performance-open{opacity:1}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-backdrop{grid-area:1/1;background:var(--color-midnight-800-opacity-80,rgba(25,26,36,.82))}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-dialog{grid-area:1/1;place-self:center;display:flex;align-items:flex-end;max-width:calc(100vw - 24px);transform:translate(50vw,-50vh) scale(0);transition:transform .3s linear}
    #${PERFORMANCE_ONBOARDING_ID}.mwi-performance-open .mwi-performance-dialog{transform:none}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-mascot{position:relative;z-index:1;width:130px;flex:0 0 130px;margin:0 -20px 0 -40px;text-align:center}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-mascot svg{display:block;width:130px;height:100px}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-mascot-name{margin:0 10px;padding:1px 7px;border-radius:4px;background:var(--color-space-600,#394064);font-size:14px;font-weight:600;white-space:nowrap}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-card{position:relative;display:grid;width:min(560px,calc(100vw - 24px));max-height:min(760px,calc(100vh - 32px));overflow:hidden;border:1px solid var(--color-neutral-200,#d0d0d0);border-radius:4px;background:var(--color-midnight-900,#131419);box-shadow:rgba(208,208,208,.28) 0 0 4px 4px;grid-template-rows:auto minmax(0,1fr) auto}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-head{position:relative;min-height:38px;padding:14px 150px 8px;text-align:center}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-title{font-size:17px;font-weight:700}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-progress{position:absolute;left:14px;top:10px;width:120px;color:var(--color-neutral-400,#999);font-size:10px;text-align:left}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-progress-label{display:flex;justify-content:space-between;gap:6px;margin-bottom:3px}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-progress-track{height:5px;overflow:hidden;border-radius:999px;background:rgba(255,255,255,.13)}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-progress-fill{display:block;height:100%;border-radius:inherit;background:var(--color-primary,#ee9a1d);transition:width .2s ease}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-close{position:absolute;z-index:2;right:8px;top:7px;display:flex;width:30px;height:30px;align-items:center;justify-content:center;border:0;background:transparent;color:#fff;font-size:25px;line-height:1;cursor:pointer}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-body{min-height:180px;overflow:auto;overscroll-behavior:contain;padding:10px 18px 16px}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-copy{margin:0 auto 14px;max-width:470px;color:var(--color-neutral-200,#d0d0d0);text-align:center}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-note{margin:10px 0 0;padding:8px 10px;border-radius:4px;background:var(--color-midnight-700,#292d3e);color:var(--color-neutral-300,#b7b7b7);font-size:12px}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-options{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-option{position:relative;min-height:82px;padding:11px;border:1px solid rgba(208,208,208,.24);border-radius:4px;background:var(--color-midnight-700,#292d3e);color:inherit;text-align:left;cursor:pointer}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-option:hover{border-color:rgba(238,154,29,.7)}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-option[aria-checked="true"]{border-color:var(--color-primary,#ee9a1d);box-shadow:inset 0 0 0 1px var(--color-primary,#ee9a1d);background:rgba(238,154,29,.12)}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-option-title{font-size:14px;font-weight:700}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-option-copy{margin-top:4px;color:var(--color-neutral-300,#b7b7b7);font-size:11px;line-height:1.4}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-recommended{position:absolute;right:7px;top:7px;padding:1px 5px;border-radius:999px;background:var(--color-primary,#ee9a1d);color:#111;font-size:9px;font-weight:800}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-fields{display:flex;flex-direction:column;gap:7px}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-field{display:grid;min-height:56px;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:10px;padding:8px 10px;border-radius:4px;background:var(--color-midnight-700,#292d3e)}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-field-title{font-weight:650}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-field-copy{margin-top:2px;color:var(--color-neutral-400,#999);font-size:10.5px;line-height:1.35}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-switch{position:relative;width:42px;height:23px;cursor:pointer}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-switch input{position:absolute;opacity:0}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-switch span{position:absolute;inset:0;border-radius:99px;background:#555}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-switch span::after{content:"";position:absolute;left:3px;top:3px;width:17px;height:17px;border-radius:50%;background:#fff;opacity:.65;transition:transform .15s}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-switch input:checked+span{background:#29c274}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-switch input:checked+span::after{transform:translateX(19px);opacity:1}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-select{min-width:100px;border:1px solid rgba(255,255,255,.2);border-radius:4px;padding:6px 24px 6px 8px;background:var(--color-midnight-900,#131419);color:inherit}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-review{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-review-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 9px;border-radius:4px;background:var(--color-midnight-700,#292d3e);font-size:11px}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-review-row b{color:#8ed9a6;font-weight:700;text-align:right}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-footer{display:flex;justify-content:flex-end;gap:8px;padding:10px 16px 14px;border-top:1px solid rgba(255,255,255,.09)}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-button{display:flex;min-height:36px;align-items:center;justify-content:center;border:0;border-radius:4px;padding:0 14px;font:600 14px/1 Roboto,Arial,sans-serif;cursor:pointer}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-button:disabled{cursor:wait;opacity:.55}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-button-secondary{background:var(--color-midnight-600,#39405a);color:#eee}
    #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-button-primary{background:var(--color-primary,#ee9a1d);color:#111}
    html[data-mwitools-decorative-motion="off"] .mwi-lb-badge--top-five::before,
    html[data-mwitools-decorative-motion="off"] .mwi-lb-badge--top-five::after,
    html[data-mwitools-decorative-motion="off"] #script_item_warning,
    html[data-mwitools-decorative-motion="off"] .mwi-train-shop-target,
    html[data-mwitools-decorative-motion="off"] .handle-badge::after,
    html[data-mwitools-decorative-motion="off"] #mwitools-feedback-button[data-unread="true"]{animation:none!important}
    @media(max-width:700px){
      #${PERFORMANCE_ONBOARDING_ID}{align-items:end}
      #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-dialog{width:calc(100vw - 16px);max-width:none;max-height:calc(100vh - 12px);flex-direction:column;align-items:flex-start;place-self:end center}
      #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-mascot{display:flex;width:auto;height:58px;flex:0 0 58px;align-items:flex-end;margin:0 0 -12px 9px}
      #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-mascot svg{width:76px;height:58px}
      #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-mascot-name{margin:0 0 12px -8px;font-size:11px}
      #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-card{width:100%;max-height:calc(100vh - 70px);max-height:calc(100dvh - 70px)}
      #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-head{min-height:32px;padding:42px 40px 8px 12px}
      #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-progress{left:12px;right:44px;top:9px;width:auto}
      #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-body{padding:8px 12px 12px}
      #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-options,#${PERFORMANCE_ONBOARDING_ID} .mwi-performance-review{grid-template-columns:1fr}
      #${PERFORMANCE_ONBOARDING_ID} .mwi-performance-option{min-height:68px}
    }
    @media(prefers-reduced-motion:reduce){#${PERFORMANCE_ONBOARDING_ID},#${PERFORMANCE_ONBOARDING_ID} .mwi-performance-dialog{transition:none}}
  `;
    (document.head ?? document.documentElement).append(style);
  }
  function findPurpleCowSprite() {
    const direct = document.querySelector('svg[aria-label="Purple Cow"] use')?.getAttribute("href");
    if (direct) return direct.replace(/#.*$/, "#purple_cow_hello");
    const existing = document.querySelector(
      'use[href*="misc_sprite"],use[xlink\\:href*="misc_sprite"]'
    );
    const existingHref = existing?.getAttribute("href") ?? existing?.getAttribute("xlink:href");
    if (existingHref) {
      return existingHref.replace(/#.*$/, "#purple_cow_hello");
    }
    for (const entry of globalThis.performance?.getEntriesByType?.("resource") ?? []) {
      if (/\/misc_sprite\.[^/]+\.svg(?:$|\?)/.test(entry.name)) {
        return `${entry.name.split("?")[0]}#purple_cow_hello`;
      }
    }
    return "";
  }
  function createMascot() {
    const host = document.createElement("div");
    host.className = "mwi-performance-mascot";
    const href = findPurpleCowSprite();
    if (href) {
      const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
      svg.setAttribute("role", "img");
      svg.setAttribute("aria-label", isZH2() ? "小紫牛" : "Purple Cow");
      const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
      use.setAttribute("href", href);
      svg.append(use);
      host.append(svg);
    } else {
      host.dataset.mascotMissing = "true";
    }
    const name = document.createElement("div");
    name.className = "mwi-performance-mascot-name";
    name.textContent = isZH2() ? "MWITools 小紫牛" : "MWITools Purple";
    host.append(name);
    return host;
  }
  function optionCopy(kind, id) {
    const copies = {
      usage: {
        life: {
          zh: "优先生活技能与日常生产;流畅档会关闭实时战斗统计。",
          en: "Prioritize skilling and production; Smooth disables live combat stats."
        },
        combat: {
          zh: "保留实时战斗统计;按设备性能调整战斗显示。",
          en: "Keep live combat stats and scale combat visuals to the device."
        },
        balanced: {
          zh: "兼顾生活与战斗;当前战斗条件与战斗模式相同。",
          en: "Cover both skilling and combat; combat conditions currently match Combat."
        }
      },
      tier: {
        smooth: {
          zh: "低性能或旧款手机推荐:2 秒刷新,关闭资产历史与持续动画。",
          en: "For slower phones: 2-second refresh, no asset history or continuous motion."
        },
        standard: {
          zh: "大多数设备推荐:1 秒刷新,开启常用功能。",
          en: "Recommended for most devices: 1-second refresh and common features."
        },
        full: {
          zh: "高性能设备:开启全部显示,并默认展示 DPS 趋势图。",
          en: "For fast devices: all visuals and DPS trends enabled by default."
        },
        custom: {
          zh: "按功能组逐步选择,每项都由你决定。",
          en: "Choose each feature group step by step."
        }
      }
    };
    return t19(copies[kind][id]);
  }
  function choiceStatus(field, value) {
    if (field === "refreshIntervalMs") {
      return `${Number(value) / 1e3} ${isZH2() ? "秒" : "sec"}`;
    }
    if (field === "productionSummary" && value) {
      return isZH2() ? "折叠显示" : "Collapsed";
    }
    if (field === "complexCalculations" && value) {
      return isZH2() ? "按键 / 长按" : "Key / long press";
    }
    return value ? isZH2() ? "开启" : "On" : isZH2() ? "关闭" : "Off";
  }
  var PerformanceOnboarding = class {
    constructor({ firstRun = false } = {}) {
      this.firstRun = firstRun;
      const current = getProfileState();
      this.usage = current.completed ? current.usage : "balanced";
      this.tier = current.completed ? current.tier : recommendPerformanceTier();
      this.choices = current.completed ? { ...current.choices } : resolvePresetChoices(this.usage, this.tier);
      this.stage = "welcome";
      this.history = [];
      this.previousBodyOverflow = "";
      this.trigger = document.activeElement;
      this.resolve = null;
      this.closing = false;
      this.promise = new Promise((resolve) => {
        this.resolve = resolve;
      });
      this.handleKeydown = this.handleKeydown.bind(this);
    }
    open() {
      addStyles16();
      document.getElementById(PERFORMANCE_ONBOARDING_ID)?.remove();
      this.root = document.createElement("div");
      this.root.id = PERFORMANCE_ONBOARDING_ID;
      this.root.setAttribute("role", "dialog");
      this.root.setAttribute("aria-modal", "true");
      this.root.setAttribute(
        "aria-label",
        isZH2() ? "MWITools 性能引导" : "MWITools performance guide"
      );
      const backdrop = document.createElement("div");
      backdrop.className = "mwi-performance-backdrop";
      backdrop.addEventListener("click", () => void this.cancel());
      this.dialog = document.createElement("div");
      this.dialog.className = "mwi-performance-dialog";
      const card = document.createElement("section");
      card.className = "mwi-performance-card";
      const close = document.createElement("button");
      close.type = "button";
      close.className = "mwi-performance-close";
      close.setAttribute("aria-label", isZH2() ? "关闭引导" : "Close guide");
      close.textContent = "×";
      close.addEventListener("click", () => void this.cancel());
      this.head = document.createElement("header");
      this.head.className = "mwi-performance-head";
      this.body = document.createElement("div");
      this.body.className = "mwi-performance-body";
      this.footer = document.createElement("footer");
      this.footer.className = "mwi-performance-footer";
      card.append(close, this.head, this.body, this.footer);
      this.dialog.append(createMascot(), card);
      this.root.append(backdrop, this.dialog);
      this.previousBodyOverflow = document.body.style.overflow;
      document.body.style.overflow = "hidden";
      document.body.append(this.root);
      document.addEventListener("keydown", this.handleKeydown, true);
      this.render();
      setTimeout(() => {
        this.root?.classList.add("mwi-performance-open");
        this.root?.querySelector(".mwi-performance-button-primary")?.focus();
      }, 0);
      return this.promise;
    }
    handleKeydown(event) {
      if (!this.root) return;
      if (event.key === "Escape") {
        event.preventDefault();
        void this.cancel();
        return;
      }
      if (event.key !== "Tab") return;
      const focusable = [
        ...this.root.querySelectorAll(
          'button:not(:disabled),input:not(:disabled),select:not(:disabled),[tabindex="0"]'
        )
      ];
      if (!focusable.length) return;
      const first = focusable[0];
      const last = focusable.at(-1);
      if (event.shiftKey && document.activeElement === first) {
        event.preventDefault();
        last.focus();
      } else if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first.focus();
      }
    }
    stageInfo() {
      if (this.stage === "welcome") {
        return {
          title: isZH2() ? "欢迎使用 MWITools" : "Welcome to MWITools"
        };
      }
      if (this.stage === "usage") {
        return {
          title: isZH2() ? "你主要怎么玩?" : "How do you usually play?"
        };
      }
      if (this.stage === "tier") {
        return {
          title: isZH2() ? "选择设备性能档位" : "Choose a performance tier"
        };
      }
      if (this.stage.startsWith("custom:")) {
        const index = Number(this.stage.split(":")[1]);
        return {
          title: t19(CUSTOM_GROUPS[index].title)
        };
      }
      return {
        title: isZH2() ? "确认性能设置" : "Confirm performance settings"
      };
    }
    overallProgress() {
      const custom = this.tier === "custom";
      const total = custom ? CUSTOM_GROUPS.length + 3 : 3;
      if (this.stage === "welcome") return { current: 0, total };
      if (this.stage === "usage") return { current: 1, total };
      if (this.stage === "tier") return { current: 2, total };
      if (this.stage.startsWith("custom:")) {
        return {
          current: Number(this.stage.split(":")[1]) + 3,
          total
        };
      }
      return { current: total, total };
    }
    render() {
      const info = this.stageInfo();
      const overall = this.overallProgress();
      this.head.replaceChildren();
      const title = document.createElement("div");
      title.className = "mwi-performance-title";
      title.textContent = info.title;
      const progress = document.createElement("div");
      progress.className = "mwi-performance-progress";
      progress.setAttribute("role", "progressbar");
      progress.setAttribute("aria-valuemin", "0");
      progress.setAttribute("aria-valuemax", String(overall.total));
      progress.setAttribute("aria-valuenow", String(overall.current));
      const progressLabel = document.createElement("div");
      progressLabel.className = "mwi-performance-progress-label";
      const progressTitle = document.createElement("span");
      progressTitle.textContent = isZH2() ? "总进度" : "Overall";
      const progressValue = document.createElement("span");
      progressValue.textContent = `${overall.current} / ${overall.total}`;
      progressLabel.append(progressTitle, progressValue);
      const progressTrack = document.createElement("div");
      progressTrack.className = "mwi-performance-progress-track";
      const progressFill = document.createElement("span");
      progressFill.className = "mwi-performance-progress-fill";
      progressFill.style.width = `${overall.current / overall.total * 100}%`;
      progressTrack.append(progressFill);
      progress.append(progressLabel, progressTrack);
      this.head.append(title, progress);
      this.body.replaceChildren();
      this.footer.replaceChildren();
      if (this.stage === "welcome") this.renderWelcome();
      else if (this.stage === "usage") this.renderOptions("usage");
      else if (this.stage === "tier") this.renderOptions("tier");
      else if (this.stage.startsWith("custom:")) this.renderCustom();
      else this.renderReview();
      this.renderFooter();
    }
    renderWelcome() {
      const copy = document.createElement("p");
      copy.className = "mwi-performance-copy";
      copy.textContent = isZH2() ? "根据你的玩法和设备选择合适配置,可以减少设备长时间挂机时的发热、耗电和卡顿。所有选项之后都能在设置中重新调整。" : "Choose a setup for your play style and device to reduce heat, power use, and stutter during long unattended sessions. You can restart this guide from Settings at any time.";
      const note = document.createElement("div");
      note.className = "mwi-performance-note";
      note.textContent = isZH2() ? "只会调整性能相关功能,不会修改语言、估值口径、通知或已保存的数据。" : "Only performance-related features change. Language, valuation rules, notifications, and saved data stay untouched.";
      this.body.append(copy, note);
    }
    renderOptions(kind) {
      const selected = kind === "usage" ? this.usage : this.tier;
      const ids = kind === "usage" ? ["life", "combat", "balanced"] : ["smooth", "standard", "full", "custom"];
      const recommended = kind === "tier" ? recommendPerformanceTier() : null;
      const options = document.createElement("div");
      options.className = "mwi-performance-options";
      options.setAttribute("role", "radiogroup");
      for (const id of ids) {
        const button = document.createElement("button");
        button.type = "button";
        button.className = "mwi-performance-option";
        button.setAttribute("role", "radio");
        button.setAttribute("aria-checked", String(selected === id));
        button.dataset.value = id;
        const heading = document.createElement("div");
        heading.className = "mwi-performance-option-title";
        heading.textContent = t19(TEXT[kind][id]);
        const copy = document.createElement("div");
        copy.className = "mwi-performance-option-copy";
        copy.textContent = optionCopy(kind, id);
        button.append(heading, copy);
        if (recommended === id) {
          const badge = document.createElement("span");
          badge.className = "mwi-performance-recommended";
          badge.textContent = isZH2() ? "推荐" : "Recommended";
          button.append(badge);
        }
        button.addEventListener("click", () => {
          if (kind === "usage") this.usage = id;
          else {
            this.tier = id;
            this.choices = id === "custom" ? { ...getProfileState().choices } : resolvePresetChoices(this.usage, id);
          }
          this.render();
        });
        options.append(button);
      }
      this.body.append(options);
    }
    renderCustom() {
      const index = Number(this.stage.split(":")[1]);
      const group = CUSTOM_GROUPS[index];
      const fields = document.createElement("div");
      fields.className = "mwi-performance-fields";
      for (const field of group.fields) {
        const row = document.createElement("div");
        row.className = "mwi-performance-field";
        const copy = document.createElement("div");
        const heading = document.createElement("div");
        heading.className = "mwi-performance-field-title";
        heading.textContent = t19(FIELD_TEXT[field].title);
        const summary = document.createElement("div");
        summary.className = "mwi-performance-field-copy";
        summary.textContent = t19(FIELD_TEXT[field].summary);
        copy.append(heading, summary);
        if (field === "refreshIntervalMs") {
          const select = document.createElement("select");
          select.className = "mwi-performance-select";
          select.setAttribute("aria-label", heading.textContent);
          for (const value of [1e3, 2e3]) {
            const option = document.createElement("option");
            option.value = String(value);
            option.textContent = `${value / 1e3} ${isZH2() ? "秒" : "sec"}`;
            select.append(option);
          }
          select.value = String(this.choices[field]);
          select.addEventListener("change", () => {
            this.choices[field] = Number(select.value);
          });
          row.append(copy, select);
        } else {
          const toggle = document.createElement("label");
          toggle.className = "mwi-performance-switch";
          const input = document.createElement("input");
          input.type = "checkbox";
          input.checked = Boolean(this.choices[field]);
          input.setAttribute("aria-label", heading.textContent);
          const track = document.createElement("span");
          input.addEventListener("change", () => {
            this.choices[field] = input.checked;
          });
          toggle.append(input, track);
          row.append(copy, toggle);
        }
        fields.append(row);
      }
      this.body.append(fields);
    }
    renderReview() {
      const copy = document.createElement("p");
      copy.className = "mwi-performance-copy";
      copy.textContent = `${t19(TEXT.usage[this.usage])} · ${t19(TEXT.tier[this.tier])}`;
      const review = document.createElement("div");
      review.className = "mwi-performance-review";
      for (const field of Object.keys(FIELD_TEXT)) {
        const row = document.createElement("div");
        row.className = "mwi-performance-review-row";
        const label = document.createElement("span");
        label.textContent = t19(FIELD_TEXT[field].title);
        const status = document.createElement("b");
        status.textContent = choiceStatus(field, this.choices[field]);
        row.append(label, status);
        review.append(row);
      }
      this.body.append(copy, review);
    }
    renderFooter() {
      if (this.stage !== "welcome") {
        const back = document.createElement("button");
        back.type = "button";
        back.className = "mwi-performance-button mwi-performance-button-secondary";
        back.textContent = isZH2() ? "返回" : "Back";
        back.addEventListener("click", () => this.goBack());
        this.footer.append(back);
      }
      const next = document.createElement("button");
      next.type = "button";
      next.className = "mwi-performance-button mwi-performance-button-primary";
      next.textContent = this.stage === "review" ? isZH2() ? "应用并进入游戏" : "Apply and enter game" : isZH2() ? "下一步" : "Next";
      next.addEventListener("click", async () => {
        if (this.stage === "review") {
          if (this.closing) return;
          this.closing = true;
          next.disabled = true;
          try {
            await applyPerformanceProfile({
              usage: this.usage,
              tier: this.tier,
              choices: this.tier === "custom" ? this.choices : null
            });
            this.finish("applied");
            this.reloadPage();
          } catch (error) {
            this.closing = false;
            next.disabled = false;
            const note = document.createElement("div");
            note.className = "mwi-performance-note";
            note.setAttribute("role", "alert");
            note.textContent = isZH2() ? "设置保存失败,请重试。" : "Could not save these settings. Please try again.";
            this.body.prepend(note);
            console.error(
              "[MWITools] Failed to apply performance profile",
              error
            );
          }
          return;
        }
        this.goNext();
      });
      this.footer.append(next);
    }
    reloadPage() {
      try {
        if (typeof runtime.api.reloadPage === "function") {
          runtime.api.reloadPage();
          return;
        }
        globalThis.location?.reload?.();
      } catch (error) {
        console.error(
          isZH2() ? "[MWITools] 应用性能设置后刷新页面失败" : "[MWITools] Failed to reload after applying performance settings",
          error
        );
      }
    }
    goNext() {
      this.history.push(this.stage);
      if (this.stage === "welcome") this.stage = "usage";
      else if (this.stage === "usage") this.stage = "tier";
      else if (this.stage === "tier") {
        this.choices = this.tier === "custom" ? { ...this.choices } : resolvePresetChoices(this.usage, this.tier);
        this.stage = this.tier === "custom" ? "custom:0" : "review";
      } else if (this.stage.startsWith("custom:")) {
        const index = Number(this.stage.split(":")[1]);
        this.stage = index + 1 < CUSTOM_GROUPS.length ? `custom:${index + 1}` : "review";
      }
      this.render();
      this.body.scrollTop = 0;
    }
    goBack() {
      this.stage = this.history.pop() ?? "welcome";
      this.render();
      this.body.scrollTop = 0;
    }
    async cancel() {
      if (this.closing) return;
      this.closing = true;
      try {
        if (this.firstRun) await completePerformanceOnboardingWithoutChanges();
      } finally {
        this.finish("cancelled");
      }
    }
    finish(result) {
      if (!this.root) return;
      const root = this.root;
      this.root = null;
      document.removeEventListener("keydown", this.handleKeydown, true);
      document.body.style.overflow = this.previousBodyOverflow;
      root.classList.remove("mwi-performance-open");
      setTimeout(() => root.remove(), 300);
      this.trigger?.focus?.();
      this.resolve?.({ result, profile: getProfileState() });
      this.resolve = null;
      activeOnboarding = null;
    }
  };
  var activeOnboarding = null;
  async function openPerformanceOnboarding(options = {}) {
    if (activeOnboarding) return activeOnboarding.promise;
    if (!document.body) {
      await new Promise(
        (resolve) => document.addEventListener("DOMContentLoaded", resolve, { once: true })
      );
    }
    activeOnboarding = new PerformanceOnboarding(options);
    return activeOnboarding.open();
  }
  async function runPerformanceOnboardingIfNeeded() {
    initializePerformancePolicy();
    if (!shouldRunPerformanceOnboarding()) return getProfileState();
    const result = await openPerformanceOnboarding({ firstRun: true });
    return result.profile;
  }
  Object.assign(runtime.api, {
    openPerformanceOnboarding,
    runPerformanceOnboardingIfNeeded
  });

  // src/features/update-banner.js
  var MANIFEST_URL = "https://raw.githubusercontent.com/YangLeda/Userscripts-For-MilkyWayIdle/main/release-manifest.json";
  var FALLBACK_MANIFEST_URL = "https://feedback.43.167.210.211.sslip.io/api/v1/release-manifest";
  var GREASY_FORK_DOWNLOAD_URL = "https://update.greasyfork.org/scripts/494467/MWITools.user.js";
  var STYLE_ID18 = "mwitools-important-update-style";
  var BANNER_ID = "mwitools-important-update-banner";
  function t20(value) {
    if (typeof value === "string") return value;
    return value?.[runtime.config.isZH ? "zh" : "en"] ?? value?.en ?? "";
  }
  function currentVersion() {
    return String(globalThis.GM_info?.script?.version ?? "26.4.14");
  }
  function isTestBuild() {
    const info = globalThis.GM_info?.script;
    return /测试|test/i.test(String(info?.name ?? "")) || /mwitools-test/i.test(String(info?.updateURL ?? info?.downloadURL ?? ""));
  }
  function versionParts(value) {
    return String(value ?? "").split(/[.-]/).map((part) => Number.parseInt(part, 10)).map((part) => Number.isFinite(part) ? part : 0);
  }
  function compareVersions(left, right) {
    const a = versionParts(left);
    const b = versionParts(right);
    for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
      const difference = (a[index] ?? 0) - (b[index] ?? 0);
      if (difference) return Math.sign(difference);
    }
    return 0;
  }
  function shouldShowImportantUpdate(manifest, installedVersion = currentVersion()) {
    return Boolean(
      manifest?.latestVersion && manifest?.importantVersion && compareVersions(installedVersion, manifest.importantVersion) < 0 && localStorage.getItem(
        `MWITools_update_banner_seen_${manifest.latestVersion}`
      ) !== "true"
    );
  }
  function requestManifest(url) {
    const request2 = typeof GM !== "undefined" && typeof GM.xmlHttpRequest === "function" ? GM.xmlHttpRequest : typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : null;
    if (!request2) {
      return globalThis.fetch(url, { cache: "no-store" }).then((response) => {
        if (!response.ok) {
          throw new Error(
            t20({
              zh: `更新清单请求失败(HTTP ${response.status})`,
              en: `Update manifest request failed (HTTP ${response.status})`
            })
          );
        }
        return response.json();
      });
    }
    return new Promise((resolve, reject) => {
      const finish = (response) => {
        try {
          if (Number(response?.status) < 200 || Number(response?.status) >= 300) {
            reject(
              new Error(
                t20({
                  zh: `更新清单请求失败(HTTP ${response?.status})`,
                  en: `Update manifest request failed (HTTP ${response?.status})`
                })
              )
            );
            return;
          }
          resolve(JSON.parse(response.responseText));
        } catch (error) {
          reject(error);
        }
      };
      try {
        const result = request2({
          method: "GET",
          url,
          timeout: 5e3,
          onload: finish,
          onerror: () => reject(
            new Error(
              t20({
                zh: "更新清单请求失败。",
                en: "Update manifest request failed."
              })
            )
          ),
          ontimeout: () => reject(
            new Error(
              t20({
                zh: "更新清单请求超时。",
                en: "Update manifest request timed out."
              })
            )
          )
        });
        result?.then?.(finish).catch(reject);
      } catch (error) {
        reject(error);
      }
    });
  }
  function validateManifest(manifest) {
    if (!manifest || typeof manifest !== "object" || manifest.version !== 1 || typeof manifest.latestVersion !== "string" || !manifest.latestVersion.trim() || typeof manifest.importantVersion !== "string" || !manifest.importantVersion.trim() || !manifest.title || typeof manifest.title !== "object" || !manifest.message || typeof manifest.message !== "object") {
      throw new Error(
        t20({ zh: "更新清单格式无效。", en: "Invalid update manifest." })
      );
    }
    return manifest;
  }
  async function fetchImportantUpdateManifest({
    urls = [MANIFEST_URL, FALLBACK_MANIFEST_URL],
    request: request2 = requestManifest
  } = {}) {
    let lastError = null;
    for (const url of urls) {
      try {
        return validateManifest(await request2(url));
      } catch (error) {
        lastError = error;
      }
    }
    throw lastError ?? new Error(t20({ zh: "更新清单不可用。", en: "Update manifest unavailable." }));
  }
  var manifestCheck = null;
  function getImportantUpdateManifest() {
    manifestCheck ??= fetchImportantUpdateManifest();
    return manifestCheck;
  }
  function updateDownloadUrl() {
    return String(globalThis.GM_info?.script?.downloadURL ?? "").trim() || GREASY_FORK_DOWNLOAD_URL;
  }
  function addStyles17() {
    if (document.getElementById(STYLE_ID18)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID18;
    style.textContent = `
    #${BANNER_ID}{position:fixed;left:50%;top:8px;z-index:2147482500;display:flex;box-sizing:border-box;width:min(720px,calc(100vw - 24px));align-items:center;gap:10px;padding:8px 10px;border:1px solid rgba(245,158,11,.62);border-radius:6px;background:rgba(25,28,42,.97);color:var(--color-neutral-100,#eee);box-shadow:0 9px 24px rgba(0,0,0,.42);font:inherit;transform:translateX(-50%)}
    .mwi-update-banner-icon{display:flex;width:28px;height:28px;flex:0 0 auto;align-items:center;justify-content:center;border-radius:5px;background:rgba(245,158,11,.14);color:#f5a623;font-weight:800}
    .mwi-update-banner-copy{min-width:0;flex:1}
    .mwi-update-banner-title{font-size:.78rem;font-weight:700;line-height:1.25}
    .mwi-update-banner-message{margin-top:2px;color:var(--color-text-secondary,#aaa);font-size:.68rem;line-height:1.3}
    .mwi-update-banner-action{flex:0 0 auto;min-height:29px;padding:3px 11px;border:0;border-radius:4px;background:#d98b2b;color:#171717;font-size:.7rem;font-weight:700;cursor:pointer;text-decoration:none}
    .mwi-update-banner-action:hover{background:#f0a13e}
    .mwi-update-banner-close{flex:0 0 auto;width:27px;height:27px;padding:0;border:0;border-radius:4px;background:transparent;color:#9299aa;cursor:pointer}
    .mwi-update-banner-close:hover{background:rgba(255,255,255,.08);color:#fff}
    @media(max-width:600px){#${BANNER_ID}{align-items:flex-start;gap:7px;padding:7px}.mwi-update-banner-icon{display:none}.mwi-update-banner-action{align-self:center;padding:3px 8px}.mwi-update-banner-message{display:none}}
  `;
    (document.head ?? document.documentElement).appendChild(style);
  }
  function renderImportantUpdateBanner(manifest) {
    document.getElementById(BANNER_ID)?.remove();
    if (!shouldShowImportantUpdate(manifest)) return false;
    addStyles17();
    const banner = document.createElement("aside");
    banner.id = BANNER_ID;
    banner.setAttribute("role", "status");
    banner.innerHTML = `
    <span class="mwi-update-banner-icon">↑</span>
    <div class="mwi-update-banner-copy">
      <div class="mwi-update-banner-title"></div>
      <div class="mwi-update-banner-message"></div>
    </div>
    <a class="mwi-update-banner-action" target="_blank" rel="noopener noreferrer"></a>
    <button class="mwi-update-banner-close" aria-label="${runtime.config.isZH ? "关闭" : "Dismiss"}">×</button>`;
    banner.querySelector(".mwi-update-banner-title").textContent = t20(manifest.title) || (runtime.config.isZH ? "MWITools 有重要更新" : "Important MWITools update");
    const message = t20(manifest.message) || (runtime.config.isZH ? `建议更新到 ${manifest.latestVersion}` : `Update to ${manifest.latestVersion} is recommended.`);
    banner.querySelector(".mwi-update-banner-message").textContent = runtime.config.isZH ? `最新版本 ${manifest.latestVersion} · ${message}` : `Latest version ${manifest.latestVersion} · ${message}`;
    const action = banner.querySelector(".mwi-update-banner-action");
    action.textContent = runtime.config.isZH ? "前往更新" : "Update";
    action.href = updateDownloadUrl();
    localStorage.setItem(
      `MWITools_update_banner_seen_${manifest.latestVersion}`,
      "true"
    );
    banner.querySelector(".mwi-update-banner-close").addEventListener("click", () => banner.remove());
    document.body.appendChild(banner);
    return true;
  }
  runtime.features.register({
    id: "importantUpdateBanner",
    initialize({ scope }) {
      if (isTestBuild()) return;
      let disposed = false;
      getImportantUpdateManifest().then((manifest) => {
        if (!disposed) renderImportantUpdateBanner(manifest);
      }).catch((error) => {
        console.info(
          runtime.config.isZH ? "[MWITools] 暂时无法检查重要更新" : "[MWITools] Important update check unavailable",
          error.message
        );
      });
      scope.add(() => {
        disposed = true;
        document.getElementById(BANNER_ID)?.remove();
        document.getElementById(STYLE_ID18)?.remove();
      });
    }
  });
  Object.assign(runtime.api, {
    compareVersions,
    shouldShowImportantUpdate,
    renderImportantUpdateBanner,
    fetchImportantUpdateManifest,
    getImportantUpdateManifest,
    updateDownloadUrl
  });

  // src/features/dps/assets/close.png
  var close_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAIa0lEQVR42u1YXWhlVxVea+29zzn3L7k3yfyUvtTCtE2nBQthEKZgx74IImptKYhSqlAtIqJQ+1DHgToVOvogfXFQaMGX0rSiUlo7RYgFtX24gmjG/FSdKZmZTCa5uZnc83/OXsuHe25+bybJWH3qhnDhZt+1vv2ttb699gL4aP2PlgCgiCgR0d2/cfWh2RahNbvjN2FXRHAnwx8GuO3fndrRru4HDhEFACALLnyBBI+zcJpl8TlEfKdwIL09+wI3Pq4Q0a4u/u2usld5kBFLYO2fEI+8u2fmRIRkYbKatN9/U3hBJJ8XsfMi2RWJlmd/UuxTO7G8s+0JDQAQLk4/nQcfJGKvitirIsklCZemfz4+Pq6Kw+ONACoAgHBp+qxIW+KVuSTxl7N4ZS6LFqdzkSWJ2jM/BQCQiQm9V5Ai0gW3NPWM8ILErVkOF6ezcHE6ixZnMpEVCVtTT23E0Fu4NbSrV2YPGMde0KZUEmcEEQkBEThui2SruTs8YuJ2+8eloSPf67LygL1RuEVEI2Iet2aecuuDZ5KV1ZyZFREiCACLcKnkSRInl9xhvBPgjhRAoGdzLTlfffVVAgBQhm8z2lTyPEMEQRAGYAvkNRCdQRO3lnKv0XgqWp55HvFEDvAHJdI/LNJsGkTMw2vT33Xrg2eS651chBVhFxwAACJimqZKAG6J2tnh4j/Yj0FCRI5WZm5Hi7MgTKBLoLwRLDYAkAJOVoDT1dwbHtbpSvuHbuPID7ohRLvuFqDZbJqxsbEsWpr5ljc48ELa8XO2ViEibpYzEaUUWLadVPPtg4N3tzYW6hqDiMgiQt7gHResyHtuo46cBpaT5d6GLpNuHcgZ0HGrlTv1+sl4efYkIuYA67mzBm5x5uveQO2FpONbtlYhEW7iGgGExZqBGqLgnwtwtDFldL/QMPGTWRS945YrjSRcZQQgdIc2gQQRlSwv526j8Wy8/D4j4nNdJv+CiGNZ1Jp5wqvVziZ+YNlaIkQEEFjjGAHYMhvP1XkY+VkuT/crOuwnpIjIwfzkMVOrniOBehqFrNwakTvcvWOKcNtoScCG1m00dLJy/Rlv6MiPAACipanHvWrtxTSMrO2Bw/UrCgCAmVm7DpFSUdTpfKZ2y70TPd83BNjTLMQTeXD1H59wquW3UaCWRiFrt0bYA4kEknbAxkuCyli3UddJa/nbjHCtVK2+nAQhs7VIvZzbAJCFWRuDpE0aX1/9XPXWe8/1fO7K4FZ5WL08eX9psPomitTSKGTl1IicOgAI2LgFIBmIgCAiGKNRRMBaBrZWcCO4ojaZmZXWSMbYJAw/Xz149xu9nO2HA3cT2E0gWWppHDEpTWuhBuzmpgiIFIIjsLVYezknyhghoyH1g4cqh4/+9kbgdgW4DeRA5S0QqWRxwqSINubUhi6or1EWFqWNKEdD4vuPVg7d89pu4DbJzI4nQMxFRA/ces8fY9//LBCFxnVJWLjfEXcEp7Rox1AahF/eK7g9MbiVyc7c5Ke8RvUNEvGSKBYi2i1NhLQCp1LGoL38WPXQvb+UZtPgHsDtC+BGkOHC34+bcuV1yXnQ5jluT7i1WwIQkbXrZWnoP1Y+ePSV/YDbU4j7OraQAqAI7NrNCACCiAChSm7GF+2XvdXLk/c7A7UJQhyyWQ47sdcNDyIzI+eZ49Yqvw4Xp76EY2OZNJtmr35xf6GdPm4q3u9QpJbGMRMWlYzbq3lTkTCL0lq061AS+I+WDx4d32uoaa/ggvnJY6biFYIdMxHR1nPuhJGI0OY55mnKTqX6crAw+TCOjWXNPTCJu7XpiCfyYH7ymFurvQXCjTSKt2tgtyuBQqKLJwvC1j1sWbQxQo6GxI8erRwafa1HwL4Z7IFbmZ885g7U3sKN4LaKM4sQETuOQa0UEhEIi2wtF1KENs+B0xzcavmVYGHyYUTMb5STOz0vFSJa/9Jf73Pr9d8TwFASRZaI1EZGeuCQyLpDdZ20lr/DCFdK1eor683CFp3sss2kCMlxJPWDL1YOH/3NnpuFXsvjXzp/n1svv02AI2kUWyRU2681EUSybn1AR+32yfLI6GkAgGhh6nFvoPpiGsfWZjmRItx6Dwozk9JIjrE2ih7yDoy+3q/doj6PaonaU7eZWumcQhpJomgzOCxYEAFAtG6jrqP2yqnyyOjp7rSgaUqHRl8KVv2vOp6nlNHMzLL2eynUEYmszYWzTKly6Vdpa/p4t6vfPGmg7VcvCud4yhmoHYjDMNsU1sKBsAAi5l6joePW8unyyF3PikxoQLSIXZ2rHhp9Kej4X3M8TymlmIsq2uQciWyeW22MsSLPd799RPoCLB4qVkQMonySfV8QSa29RzZcX4CYu0N1HbdWTpcO3HWy+/w8YbGo2Z4YVw+Ovhh0Ok84lco6yC1PJkRUmR8IAH5cOv86hIh86tT6KKRPFV9UKOgU/d0mrejGCXNvuKHT9vXnSgfuOLn+Nt6coesg7/5F2F550qmUuyC5x+SGqHf9qDCxZsfZDCJKd3bysThcnJ6kkncrJGkGAgYQgLsZnLvDDZO222fc4Tu/v9vDHcfGskLnzgZXz0O5MfizJAhtUd2EAMAi1imXKAqCf5dHjswXkeT+DD5SfCp8jrMcvJJnWMSKiNVaoTs8ZNL29TPu0J1Py8TuU4VeP9lsNk3l8NGz8WrnG261olzXJSnsGqM1eC6B8LOIaHe93XrjsfDa+Ucy/+KcRHMi0ZzkwcV2uDT1zM0Pj4r5zOL5r+T+hTkJLoqEH0jmX1jszE9+c1/jvd7Gpdn3BrLOxQcz/8Kn/WvNW/7bGWFPQpZm3xvI2u8/kHX++eDq5ebITdntN/n8MKas/WzcyC7uYdLaOxnfzNDy/2n3o9Vv/QcBmorNJ3wSfAAAAABJRU5ErkJggg==";

  // src/features/dps/assets/copy.png
  var copy_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAFRklEQVR42u1Yz28bRRT+3uysncRO69AmIFFQKooETcuh6QEq1DZISJWKKi6JhLjQA/QMEmfHB/4FJESrSkXikBwQpeJAD0lB9AJFVFA4tEK0UJUQqSWJ7dg7O/Nx2LW9dhzHjovEoSON5dXuvPner++9GeDR6G/ItlaRMj0P9feNbtcvAjiOsQlwfhoOIvwPdaL0L6J7GbpnwSKcvHh3KFva+Yp1atDabvxEgiIqpSpLS3e+uiVSRZ4KBXEPz8Wx1i+eXRnR/tDldMY/5MJIAhFNSQhkyyYkoDQQVsKvw+F/Xrv6+uhaPk9V2AKk6hbfsVl4EKHo1MlU1j9UXTGBKRsbFI01JWPDorGmaGxQip8T05SMDcvGmlVjdEof1cXcl0fO/jpcKIjL56n6AkhS8qQam4DKk0o8ydLCiUCJiKeUeEo2TmmZEPEo4gdrJtS+flmn91164cK9TKEgDh1A6i3AKRFxsccCADjw8Xolp6GMhZM2QSISuVMrQAngmPQzACW6umbC1LB/NGd2XZq8ePfktVNSrsV39zEYL5j7mdnxPRi792dF5bKw567zjSIHPzAlYyHiJWNOpBGHKxVg3QBpH7BsxGFNAdKFqeGUNlWz4A34p67cQBmzYCvIthacm6M3I2IXb5tXB9L2XGWFj+8Y8sQ54PRBKMKCUBG42q5glBgElAJWK8CF68DtFSCbikAmzSKitFkzJpXzp6ql4DwK6ek8qArN+dXegiRlFpBjvwU/5R5L7V+5b6i82FgkSDbnq0htIQDAAUh7QDEQfPQ9cWcVyKQA6xKfRr9OeT6cC5buZ26O/zJzIIh4tmFF1Q6ciHAC0BDJrhfhREDnLOkcSca0Em8BAeugG3RTCYEhnzhzWPD0TqAcAF7dvdGHsZ5KiVS2lcUknCgoQCCR7hK5p2EJaeG/2n9PgMACGZ84Mwk8tQMomyhxWjOB2DyL1RYsLpHGTEpL7ND0EP1LuFAJULVAxgfeOSzYsyNKHC+hXN2S/RB1zVoOEW04RvFk2Txr75o2iEFmfeLtSWAsCxibsHjCG33VYhFgyItpYpMME0RuNW4jyEoI7B4EntsFLKwBKa+R2Z0s2DVAY4Ef/gLKRupcBmm4CTEp7xshnhyOQEqLgpbNIdB4J9sHaBlRxo9LwIffAYOacExYMQZby9z9o8D7R3ptNLl9gEoia+zNASeeieIpCSoJNLQRwK1iuZemSnfTj1kCIwPAmwejRGm8kzofJtuqqu0sr1YSH2rDGhIwplUwNyRMjV56OnCQfQJkvGkbkpVNnmWTyGJzIeiYwT01rJ1DefNv2yrAVoGyPYAusb6fY5i0CYNaAirVmai7a7f7QMcWg3Fjve+PZtrRA7cAJD2GATuI1L1aQ/oFRzowLtmEA6BB6NHRZbftJGEH6yUbG+lCkDfgKz/raz/jaz/rp3QGinCfXJmaCqfn6CWb1d4P7p2orKW53vgdnZf21BPp4LNq2X47YJWEWpFVd+vq6cHPQcq8iN2miwX9XqYogVMppabG3RfvvTR0vk2gs/djZ51HN48uJsoXt6AsOvDBugwfW6A+Pg69+DvCsWVwfkZsz0ny4No17h456AsQRn1qoi1vwcskZUibe48IYagUtCjyypSEsyQKeyXsuRaLCOfm6M0cFnP5ZuVybjfeKq76UKp9lWhn2w33NAQ8DV0qWhdQfQMAx5v7jt4ujxhfFM3/gYExmndF5PkgYL0/dd2mv4qKkIjQH1DVUjH49MSzgwu1k+P/8saU7O1+cau7GVlchPewwC0vgyJi8Wg8Go3xL8iPky24WzbFAAAAAElFTkSuQmCC";

  // src/features/dps/assets/debug.png
  var debug_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAH0ElEQVR42u1YbYyUVxV+zr3vO9+zszO7C0vFiAhFsOoPTE1J6+7GKNpAgZaZ8tWSYFps/GO1/ig/Om6aGGNU/GVSaJMqLLvMiCU0MdjELktaTUwhUJUCJhiQj7K7M7O7s/P5vvcef7zz7s6O+8GupcSkN5mPzLz3nOc+5zznnnuBT8Y9GgwmTrJIxVnO9EwqzpKTLBhM9xRsEiymW8C9AZN0wByKD685Ga+ePLI5t8dlC2ACmFxWD23O7XkrUf1jKj68pn7ufIYx3wlfuAACAGnLdo8w1y+RzeuPbhmNJNK0n8H0Y4ASaVJHnsg9v0Q0/xIMANwO4II7dz6DFspidzfp32y6vb6No28waU82lP3SicPvXEIaWP+7R1a3V6PnDBKVnBjbsv1Y61vunI8FIAC8spbNvWfI6tmQ+dEST+xnI6pwRQutAQIpMmKe4LJbOvvCjt+3/MJ9diF+xMJEkRTPnoF9cNuHiwOG/5tF29YmvMuDHF4R5NAKD7zLCpatwwh+O7X7dvveM7CTSC7IFy1Utfc9e0bGbq96u1WGHs5VC5qICCAmqpUgzRzzB0XWLvx5qC3YefMAVDeIAScr7xqD/R1sdIN0ZGjlrhYZejhXLlpEQoCIAAgwBIFIkBDZUsFqlsF1LcP5p7tBur9j5pr5kQHs7IRmMEkld9kKmkTNBmPig9k1TsJW0EJjB4OpsxP67udgN5hAzEyLbECAQCRqlohAtaRxKiLIZghALCIQo3t+4Z0TYH8HG/0dbLi7QhJJQSD+9Y6rUZNEs4bNymayy4AuA6x5EoHzhRTbLCFjPdtvtBKI3V2HweTan00LdOdl5T0zunytvuo971s+vuJPAQp8tVAoan+bENHVgFKM4bMAFIFqy3ZCrbVPBkSFy2fzoetd3urKQu4KxJ2WnWkAMjGAAxtu+iNmYJ+HzNKQ9/ore3s/PwwAfZtGXlskI3uyhYLd+kVhtK4FhAHIAOPqSWDsEsHwOQxyzYFmbTd7gkZW53u2Hm/aBQAHv3MtFh2N7YVWoWKg+JOnD7cXa5B4VoCpOMtEmlTvE5l1nzFi75YsoKDLN8qivL9qWpci5eibVtFS0S9Dtq8DVAkgEyhlGDf7CfY4IGSdZQYYDGJSpuGVeU92CyzPsgC8PwgK76fDXuCanevYmoqddn3PFWJiMA5suOmPkv9lH/zPNHl8YQvAuF0CVwFPM2PZRmcqCWDwHJB93zFG0gFF4r/aG+ddEgWEDx4BjNmVQhXV14qhwr6nDrcX6U4YbBy9m6/f7+Pm5yWbTzLriFVRtPghQbE1ABEje5FwawAw/DVrPNUq0WS4GcwSgkkgr6ROlc3S/u1HWz9YUDeTirMM5f9pPHp86WUAz/U+njkV4VivVS0wiMnwA9VxIPt3QHqnB9dInmBoj8cn8kbue4lUrAcA/vCty97x8Eq7MbRzAqxNmJjELKoGQGSQypyHUFWgMkSw8wAZDWhoEi/zZLCYmA0BYsETCn705P2VeTLIBBC/Hr/1YJPZtKRqV8liW6PC37BJQQiQtgjD7zl5JsypxPGMecMggJRSEGR8/ci2oTJrCL8R4JJVurUz3fpX1/eMOeiqqO+x7NdazOhAvUNLASWrACIxpc41gmGu5d0EhY6KnXwkMDN8ZgCmnLQtBZBRI13xY9FTjUqewuA/1jiWLKjbhWr5bxocVcomEgQwQhIywmAnbOwqYIbCWte3UO1XZoYQhLJdGC1aPA4AkiQLSSMk7A/rMcyt4iREauh2IDc0LmW4SfsylGihlldLdkEBJMlNtAb2nLVMvyUwsx3yBIwxkd07Eta9Kj8mom0hlWhbXET39I0EzXSkpFouuJT/duO1FREsvqzYrkWLGpKOZ1kvuwyyx/DSqH9w9c7eT12sD2e9zzmbBWdTdzrgRJrUoZ3Da8Jm6z4CW43dwFSKpvmHp/7IrKyQHXuxb1fmARdcMsliOnBzMvj6k1c/F7baXpAKT0UMf7CigIJdYLepcoowTZyAySWRGJPHYp4sM6w5aAbJYwBjVrkIg3tGZPbnu48uvXzHDLqrORQfWtVUbDsfhf+7YekPZqv5q3k9dsSAOemeqXaqbKBtypndaQy11mwID+V5rC9byl8JCV8gAv8zzZXWc32JzAMEmvbcPGM/KBQIWvOoVboxRoWXxpt5bfxEZGdFV973Cz9pZjVVteyUE67vrh3FM2vlN/ywqXph6/HI9nJT7iujurBvpFr8NzPrGXJmVhUTAH71sQ/uMyPhyu5DSzMAwEkWPWcHH4og+o6CshXbkiCn6LZe3Q5AZklCeQ2vMSKyXRePtZ7uhnM+Phi/FrNR8j+XXnVj3g1rfU70d/QbnQOdKh2HSKRJ9W3Mfb/NaN4/Vi1pzYpJCDnZHNDkClkrgkDY45cZO/di4kTsp6k4y3ga+lQHZNcA2bMpeNYQu0pmMHUNdNkE4kSaVLKj39j2ZvRXOWvkh6Y0KGiEJDMziG33xdC2ZsV+GZRewyNzenSfCy6RJkUg7hogm8GUTCZnVPCCz8UTTe2mwUcCHH5ZQHb4DLNu5wDKygYL9W6J8i8l3mh7e7pm9K5efdQ7TG0eeVAI6iAlP6uZhZD8LzL49OPpyF8an/14LzHv4HIyFU/J/8XHR3LJmIqnZNtgfIqtoUXge8LaJ+P/bfwHrZD7qmvnWGYAAAAASUVORK5CYII=";

  // src/features/dps/assets/reset.png
  var reset_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAJZUlEQVR42uVYa4xV1RX+1tr73McM84LOiKJNpWAUbGKCAQH13okoitXUmDNNRW2NdTC0vqrG+gMPx6Qqtr6w2g6mIUZtm7mxTasRBMrcK9OgFWprZIqPplErgYHOOI87c+85e+/VH3fQGRhg0NH+6P5zf5zH/c56fOtbH/B/dQQEAU3mKz/Py8hv97m7cQ4V9ncJWnJ2LNiAM3lwU7ZLcsg5EORLi1KmI6PHu9S6o81r3dHmjXct05HREBkbEBHyxVcQXyEI+HNH0G/3VW4kUh2ZDv3Amj8sjLTJGsRnW5GvibI1EBAJFxn0EZN+M+F0x2n/anjlyZZwEAB88VWODon2JKSY/Hafcy05u/yZoHbPGQdaYy/+ntOYKymGWAeJLQ4mkZgATWClILEDl/G+suqZKXuqn3jpmz/b64uvcqvniL8gmra/sXeN87iBinx3YfHPdyMIGGHoJg5QhAACCLJk28qWUp25303hmXY4hi0ZC1DMBAWBBwWACHAARGIRsiCn2NOerk5ABtw+LmFV54K2pwDg/FdveNadklguVmC7y68vfeHkc0IAEwcoQiCSdr9dPXHH1rVxvVtpjIGUbAwAlNSeSmlI2UGGTURCAyCII1RzktOUUnCRhS0ZQw6Ok5xQ6QRkwP36oneXXLfllA35uF7Nl5IBG+6Z3XPGqc8uvbMIAR1sKn1UcKuJgvVB8vFvbHneTKNlUU/ZsoijpPZUQsMN2t087H6nDOfTqH03tbf+48SUkgzUSM1QPHSqHY4youjKRFXiLGsqQG00BG9q+qrNp2w6AQ7VsKKICRBnxoOhj1hzaOH2UNz5r7W222lqmekZjhkgXZvypOg+0EN8z/T3T/xNriWMxnm+D8C/AWyTQO7LXrbyStHuftWQ+nrcW5L4PyWravQFEgMybISSikAgYO/EAPric45yNvPnFfe5E/Rlcc9QDBCp2qSmPrxQ152+fuOyx/cfpI+m/U2S2zVHsDqstMnqgPy5XdTd2E2UJQtCDkBu4Rvff5JT6gZXtuxKxgFEIDq+Lj5IJUu23Xzu0NTStiiODKxA1yW19Mhzr81/6uoKsEAXmkMLHIWAR2rppsceS759znvNiL26YT1wb1RlT5PYOQAMEUFCEZdl76yeqbOeXfrQ0Wswt2uOBIHwZu/6R40iYFicmuIlqEfy2+evu4bkJA4AhBSaY7NTQEEAyi/Y9Ss33VtuS0WYwRiIBQLwp1ESiECA6UdPcaYjowvNodnW0f0tqdfz3GDZcII1ldBX01NzLYHEz/kctkyAaCsM4Lraf9IYKbPc9kaRGDghp1EpOEiFlQSVRHuNfewOfc2Y8VLIZh0AlNLxSkdOCLBclWA1pNZsuvjhDzMdgc61THAKEAkk4O7GqBfCm1KNUxJebSLl1aW0V5vSui6lvbqU1jVJLzmtWjO8TY+03D4cSMCj5/anNTjC4P6W22Z8UP/xe05JkpiIYu6tO1A7a9PSR3oBqfzx8agbgizZfFedaej7rnPc4IyBjMxkEhLyFLGifTV7Gp9+8fJwaHT9jUlxJpvnQgjXnSwu4ik6ZYtRWU9JJKlHNm66+JGeygwle5xaSQBgy4Vr+gCsPZ5nxqnBLIACHMtZUFT5PhBUpP4EAXXn59DnUkD5QB3tlkI2tONJslEA85Ufha8KAIJTUrJIkO4CQZrauz67niNIAUfp+iBgZGXcyftJkzRlmwQAjLIJiECI2JWd02Xdc5B+vhh9OaJeiGQ8TXgYDzo4MDRIQBCBVQn3BcpfAoXu0m0/njnY29NfuDw8MBJGOSyC3fluAgBtdQQigGApyWy4OBUA/Lldk7prBBIwAGnefuOqvmm9u+Ov4q3zO3+wcNS1Q3kwW/kkyx9WxiNZSmtEKZkNgLob50weQBEKKXTf+eOPvlJS5VVRFHlcr0+wXnwzAMnn8+MBzI+oYfwdVgARIgIgURaANGW7Jq0G/VwLA6B9tUNnIKW0GIlcbB05GjpikxTylSmSNKntdiCOQJQwQxGskksvfPn26hxybrJWypFsiPHMJVSliIgcETE7fnN0NsdGMAwdJOAt5639gIW2qyoNiVzE9d70gZr+q0GQY3HZRDmxkIW7rf2htPHcNXY4Bkg8N2CkOk5tBYBsFm7cWZwZyb2C18aKCQRyw7FISlYt2XxXXSELd6T1cKJn3s5WDQrdG9N334QG72RXdhFXaUaE11/KPvwWgoBDCscHWGguWAjoxPT05+3H8duc1tqVjeFaNaNY39MGCl0mm+fD9tsJnkxHoHeevS6+bMOtc6NaF8TFsgMJsdKUkMSjBJJMdiymQ6Mhfs7n3Jlh5LnkHUorEiYy/WWDBvXtxa+veLDQXDAAwRf/eNJN83a0eoXm0Fzx/K0n9jYN/t54qCIjVlV7nvSYv2Y3NOYgAReax06cw9KVa8lZv91XnQt++aLrdeu9hpSGiJj+knUNdOeinSvaLl77WCJHOYsAnOnI6EACPqyBBBRIwJmOQAOQg5E7MHNoq6nGbDdsjGhiRHBVw1U3hmFo/NzhXEtHKmQ/53NV4xzvndo9Barn+aa3FDsi9uqSCoPyt0RZ311Y8OTGI0msMaS8fn1q29wdK8qJ6F6TcLVuyBhhIFGX1rzX3dK5eN3aIzkOdNQBHobu3JduajQnlTZQDc+Le0sGAFS1p1kYVHKdyunfJgfTr8zsP/2f6y5f8QmP3f7yT6v/Ub1v9nCieEmk42ulhk83QzEkcgYKnKivYtrrHuhc1Hb3yH5jjt/6kIBBoVvS3lpXnCXrqU5dYQbLkNhFQqRUtVasFVx/DHb0ETveD3LDEKQdo1GUzKAaDVseWfZFwGnPY2Gofr6rc1Hbg8fyao7djaO8ksU7b/ih9eQerlKNphhDjI0hcCB45DGTViACRBzECMSIhcAAUJxUWqU0pCi7VL+6tfO8X2xBu69wjBXi2J1YKEilAQL+8KqHXzv9ioXPxdpGApnJVbpepbUGEcEKJHKQ2DoxAmIildSsqj3NiplivKuK+r4Z71W1blz2+Du++KrrzGPvN8dnv41KR6bjlnpXay4QMhc5sWdbkZMhUkdCGhBLjF4Qv6+I/8JWbzj1rcatT18Xlg618SbfYRUhHy18aN0sfzWo7YtL9VSkhOepOF3Wvc8tC/sP+8AvzW0VkC++OpLbOtqNrbion01o0KQa6KsDCgCEALA6lC/Pl/4fnv8C4gvFAbl4eBAAAAAASUVORK5CYII=";

  // src/features/dps/assets/trend.png
  var trend_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAFXklEQVR42u1YTWxUVRT+zr33zZuhM/3DYistRVoCihhDMCGEiEVATQiRSKs7JVE3JprgwoXRabeulOjGEBPUmNhuwCAhYJnya9SIxggBKn+1VFpKpy2dedN5997jojPNtDOVUEph0W83d87M/e653/nOeQ+Ywxzm8GCCGYJjULEYFDPogSOXtxbNXwMAdR/IERHswLFHnwwpWsuMobPX4geoKT6U+Y5z42m2M0cE23eoOlocslFXaYIQ8PzApeGU3Fa58fKf2Zjsb8SskWuFJIK90b5oQ0WJbU7zQxgt2aGTzuZ0yMWSAPQejq1XaL5fV1wBikYhiPAKCNatfNUEilc6zBYjXTeswm9PXbd9y6pacCY3i2IWCYqWFlg26AcgYJMACMSGidNsQSwt+/elSDgGRU8gfe37qkXBgN2cSpFOJ1qlSV03JtVrw855ZwjqaMXGc50cnWUNcgyKGqBvHK58urRMnXCVWE2QCiYhnOG9MqR/cbwR+pV9/XqhklUzbSEAQARmgBCDpAbo/sOVLxeFnD1Wo8hxgEHP7GcEPvBMcIkjMPTaZxePtbXBZC3onnhbrvlyK2T2c/xIzc7UiRq+1VFj7E+LeKh94eeFOsdU3YRmyHgZAAaPryyDNFS69uwAAAwfrf40HJTvJJLWug5EIs3vl23o/pijEFiRs3cTLGGiQc8IwSy5WycfXkDG3QU9+hwRCd+4P1jSkbKIfCmZZEgJL+WbHaUber7jGBSehZncMaYC3ZXeOiCBWhU3JlZWzmuSqUWQguDyVfhGAbAwlnuSSds4/4WeU9mCuZN9/reKOQrBDJGrj4zeJDrGCqAPcnVZWK9J+KvSwdr32KnZyUm5ytc6pbUVpwc9f910yU1JkBnEUQhqgSWCJQJzK2QsBkUEJoKhBujhWNW6sOQPR0e1VpHlUsgQCRkkMW8ZhYIsE776pOr53ssXDsCdDrkpbSajDx48VVcfCrnhXm/4PK3t9gDgr9aK8OLKwDZY+QaAZxzB0H4Auv+IlW61ZmbowRh8VuSQfzYahVj6IvzpSokKlfpgR22Jktgt7OhWZuOQCl0a9fGRsH6148q3AlIuEQCEAJKehbZ0ZZ6rFyvHARsGuRaDcdpVtvGfdydPJ3dLUBDBDrTXtJXN5+0Jr96Ss4DU6GmSlIIQMuPAQCLFnhTYn07rL4aLxMmwJ7YHFW+1zDAs9pU0dH3DDAIBU1nIHRHMau7q4aVLijHcGYgs52D124JUmNI3263X/bV13JBi2GtC4Ftj9JfFDf+em7LAALobYvkabAbQAgiBiIQVLMtZqCICG1CgkiMRRV6a9w2M0I7aLV3xbMcAADTCAhBoy/xXI0AEMxNdSuXkkplBnZ18rqg7cLFE/1Hn9R1Ky9BCMdq7lyNBcrwEdtdu6YrzAbj4GT41TSBhcA9AeVNvE8z1H2s2lbi8Nxgy82AAKIHBuPyqdKDrTTwOYAXMeLZmAo0AmsHUkl9MVMicqQX2dGt9xWNV3MRsSzXz8eL1V47d89GsgG6pUG+9drBq+SPzRWX/iBkBgQSEAtsSDUAxTRS+LHC5EpAmsyxzlg1gJsWzBJcUSeqN28vVm3ouTH6yUxMyR7BdB6vriwL0O1sZLA8p0NjJYMzYcZgzZ809m5qUBwBQmWDKyUM2TvF4mAUAH4hISvXG6lYSXfw7e4sFO4nrEIPJas0wGqBJCeOxToM8B2GACaDxvTnnijj/3pjHJ1s71r4MvDTftpMQgXsP1NSVl1PtyIjJWO00oAscv9Aag8Nh0M1b/pXKTX2XCj285wn1vr0SKbA3TTVmTZh4ZwNnCtvMHOYwh9vgP/Xfodt7xGPxAAAAAElFTkSuQmCC";

  // src/features/dps/00-bootstrap.js
  var pageWindow2 = globalThis.unsafeWindow ?? globalThis;
  var MWI = pageWindow2.__MWI_DPS = pageWindow2.__MWI_DPS || {};
  MWI.__mwitoolsIntegrated = true;
  var VERSION = "1.0.51";
  var TAB_CONTAINER_CLASS = "TabsComponent_tabsContainer__3BDUp";
  var ACCENT = "#d4af37";
  var GameAssets = Object.freeze({
    ability: (id) => getGameSpriteHref("abilities", id),
    skill: (id) => getGameSpriteHref("skills", id),
    item: (id) => getGameSpriteHref("items", id),
    monster: (id) => getGameSpriteHref("combat_monsters", id),
    misc: (id) => getGameSpriteHref("misc", id),
    avatar: (id) => getGameSpriteHref("avatars", id),
    ready: () => loadGameSpriteManifest(),
    scan: () => scanGameSpriteSources({ force: true })
  });
  var SKILL_MODE_ICONS = {
    get attack() {
      return GameAssets.skill("attack");
    },
    get defense() {
      return GameAssets.skill("defense");
    },
    get stamina() {
      return GameAssets.skill("stamina");
    },
    get steadyShot() {
      return GameAssets.ability("/abilities/steady_shot");
    }
  };
  var TOOLBAR_ICONS = {
    get history() {
      return GameAssets.misc("loot_tracker");
    },
    get settings() {
      return GameAssets.misc("settings");
    },
    trend: trend_default,
    debug: debug_default,
    reset: reset_default,
    copy: copy_default,
    close: close_default
  };
  var PALETTE = [
    "#C41E3A",
    "#00FF98",
    "#3FC7EB",
    "#C69B6D",
    "#A330C9",
    "#FFF468",
    "#AAD372",
    "#0070DD",
    "#F48CBA"
  ];
  var Settings = (() => {
    const KEY = "kikimeter:settings:v4";
    const LEGACY_KEY = "kikimeter:settings:v3";
    const defaultLanguage = typeof runtime.config.isZH === "boolean" ? runtime.config.isZH ? "zh" : "en" : /^zh\b/i.test(globalThis.navigator?.language ?? "") ? "zh" : "en";
    const defaults = {
      colors: {},
      classOverrides: {},
      classCache: {},
      weaponCache: {},
      mainMode: "dps",
      showHealing: true,
      showGraph: false,
      autoReset: true,
      language: defaultLanguage,
      panelOpacity: 100,
      refreshIntervalMs: 1e3
    };
    let state = { ...defaults };
    try {
      const s = JSON.parse(
        localStorage.getItem(KEY) || localStorage.getItem(LEGACY_KEY) || "{}"
      );
      if (s && typeof s === "object") state = { ...defaults, ...s };
    } catch (e) {
    }
    function save() {
      try {
        localStorage.setItem(KEY, JSON.stringify(state));
      } catch (e) {
      }
    }
    return {
      getColor: (n) => state.colors[n],
      setColor: (n, c) => {
        state.colors[n] = c;
        save();
      },
      getClassOverride: (n) => state.classOverrides[n],
      setClassOverride: (n, c) => {
        if (c) state.classOverrides[n] = c;
        else delete state.classOverrides[n];
        save();
      },
      getCachedClass: (n) => state.classCache[n],
      setCachedClass: (n, c) => {
        if (c) state.classCache[n] = c;
        else delete state.classCache[n];
        save();
      },
      getCachedWeapon: (n) => state.weaponCache[n] || "",
      setCachedWeapon: (n, w) => {
        if (w) state.weaponCache[n] = w;
        else delete state.weaponCache[n];
        save();
      },
      getShowHealing: () => state.showHealing,
      setShowHealing: (v) => {
        state.showHealing = v;
        save();
      },
      getShowGraph: () => state.showGraph,
      setShowGraph: (v) => {
        state.showGraph = v;
        save();
      },
      getMainMode: () => ["dps", "hps", "taken", "accuracy", "debug"].includes(state.mainMode) ? state.mainMode : "dps",
      setMainMode: (v) => {
        state.mainMode = ["dps", "hps", "taken", "accuracy", "debug"].includes(v) ? v : "dps";
        save();
      },
      getAutoReset: () => state.autoReset,
      setAutoReset: (v) => {
        state.autoReset = v;
        save();
      },
      getRecountMode: () => state.recountMode || "dmg",
      setRecountMode: (v) => {
        state.recountMode = v;
        save();
      },
      getRecountPos: () => state.recountPos,
      setRecountPos: (p) => {
        state.recountPos = p;
        save();
      },
      getRecountSize: () => state.recountSize,
      setRecountSize: (s) => {
        state.recountSize = s;
        save();
      },
      getPanelLayoutVersion: () => Number(state.panelLayoutVersion) || 0,
      setPanelLayoutVersion: (v) => {
        state.panelLayoutVersion = Number(v) || 0;
        save();
      },
      getRecountShowGraph: () => state.recountShowGraph !== false,
      setRecountShowGraph: (v) => {
        state.recountShowGraph = v;
        save();
      },
      getRefreshInterval: () => Number(state.refreshIntervalMs) === 2e3 ? 2e3 : 1e3,
      getPerformance: () => ({
        showGraph: Boolean(state.showGraph),
        recountShowGraph: state.recountShowGraph !== false,
        refreshIntervalMs: Number(state.refreshIntervalMs) === 2e3 ? 2e3 : 1e3
      }),
      setPerformance: (patch = {}) => {
        if (patch.showGraph !== void 0) {
          state.showGraph = Boolean(patch.showGraph);
        }
        if (patch.recountShowGraph !== void 0) {
          state.recountShowGraph = Boolean(patch.recountShowGraph);
        }
        if (patch.refreshIntervalMs !== void 0) {
          state.refreshIntervalMs = Number(patch.refreshIntervalMs) === 2e3 ? 2e3 : 1e3;
        }
        save();
        return {
          showGraph: Boolean(state.showGraph),
          recountShowGraph: state.recountShowGraph !== false,
          refreshIntervalMs: Number(state.refreshIntervalMs) === 2e3 ? 2e3 : 1e3
        };
      },
      getDebugMode: () => state.debugMode || false,
      setDebugMode: (v) => {
        state.debugMode = v;
        save();
      },
      getLanguage: () => state.language === "en" ? "en" : "zh",
      setLanguage: (v) => {
        state.language = v === "en" ? "en" : "zh";
        save();
      },
      getPanelOpacity: () => Math.max(10, Math.min(100, Number(state.panelOpacity) || 100)),
      setPanelOpacity: (v) => {
        state.panelOpacity = Math.max(10, Math.min(100, Number(v) || 100));
        save();
      },
      getLauncherPos: () => state.launcherPos,
      setLauncherPos: (p) => {
        state.launcherPos = p && typeof p === "object" ? p : null;
        save();
      }
    };
  })();
  runtime.api.dpsPerformance = {
    get: Settings.getPerformance,
    set: Settings.setPerformance
  };
  function formatDamage(n) {
    const value = Number(n) || 0, absolute = Math.abs(value);
    if (absolute >= 1e6) return (value / 1e6).toFixed(1) + "M";
    if (absolute >= 1e3) return (value / 1e3).toFixed(1) + "K";
    return Math.round(value).toString();
  }
  function formatRate(n) {
    const value = Number(n) || 0;
    return Math.abs(value) >= 1e3 ? formatDamage(value) : value.toFixed(1);
  }
  function formatDuration2(s) {
    const sec = Math.floor(s % 60), min = Math.floor(s / 60) % 60, hr = Math.floor(s / 3600);
    const p = (n) => String(n).padStart(2, "0");
    return hr > 0 ? hr + ":" + p(min) + ":" + p(sec) : min + ":" + p(sec);
  }
  function el(tag, styles) {
    const e = document.createElement(tag);
    if (styles) Object.assign(e.style, styles);
    return e;
  }
  function iconElement(source, label = "") {
    const value = String(source || "");
    if (/\.svg(?:\?[^#]*)?#[^#]+$/i.test(value)) {
      const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
      svg.setAttribute("role", "img");
      svg.setAttribute("aria-label", label);
      svg.setAttribute("width", "100%");
      svg.setAttribute("height", "100%");
      const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
      use.setAttribute("href", value);
      svg.appendChild(use);
      return svg;
    }
    const image = document.createElement("img");
    image.src = value;
    image.alt = label;
    return image;
  }
  var CombatIdentity = /* @__PURE__ */ (() => {
    function dayStamp(value = /* @__PURE__ */ new Date()) {
      const date = value instanceof Date ? value : new Date(value);
      if (Number.isNaN(date.getTime())) return "";
      const pad = (number3) => String(number3).padStart(2, "0");
      return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate());
    }
    function resolve(detail, type, characterId, now = /* @__PURE__ */ new Date()) {
      const rawKey = String(detail && detail.combatKey || "");
      const day = dayStamp(now);
      return type === "trial" ? { key: "guild-trial-day-" + characterId + "-" + day, rawKey, day } : { key: rawKey, rawKey, day };
    }
    function matches(oldMeta, resolved, type, characterId) {
      if (!oldMeta || String(oldMeta.characterId || "unknown") !== String(characterId))
        return false;
      if (oldMeta.combatKey === resolved.key) return true;
      return type === "trial" && oldMeta.type === "trial" && !oldMeta.manualReset && !String(oldMeta.combatKey || "").includes("-manual-") && dayStamp(oldMeta.startedAt) === resolved.day;
    }
    return { dayStamp, resolve, matches };
  })();
  function combatEventMatchesSession(detail = {}, meta = {}) {
    const eventType = detail.battleType, sessionType = meta.type;
    if (!eventType || !sessionType) return true;
    return eventType === sessionType || eventType === "combat" && sessionType === "labyrinth";
  }
  function isSelectedTrialTabBar(container) {
    if (!container || typeof container.querySelector !== "function") return false;
    const selected = container.querySelector(
      '[role="tab"][aria-selected="true"]'
    );
    const label = String(selected && selected.textContent || "").replace(/\s+/g, "").toLowerCase();
    const trialLabels = [
      getGameTranslation("guildPanel.trials"),
      getGameTranslation("guildPanel.combatTrial"),
      getGameTranslation("guildPanel.skillingTrial"),
      "试炼",
      "試煉",
      "trials",
      "trial"
    ].map((value) => String(value).replace(/\s+/g, "").toLowerCase()).filter(Boolean);
    return trialLabels.some((value) => label.startsWith(value));
  }
  function isSelectedGuildProgressTabBar(container) {
    if (!container || typeof container.querySelector !== "function") return false;
    const selected = container.querySelector(
      '[role="tab"][aria-selected="true"]'
    );
    const label = String(selected && selected.textContent || "").replace(/\s+/g, "").toLowerCase();
    const progressLabels = [
      getGameTranslation("guildPanel.trialInProgress"),
      "进行中",
      "進行中",
      "inprogress"
    ].map((value) => String(value).replace(/\s+/g, "").toLowerCase()).filter(Boolean);
    if (!progressLabels.some((value) => label.startsWith(value))) return false;
    const contextLabels = [
      getGameTranslation("guildPanel.trials"),
      getGameTranslation("guildPanel.combatTrial"),
      getGameTranslation("guildPanel.skillingTrial"),
      getGameTranslation("navigationBar.guild"),
      "试炼",
      "試煉",
      "trial",
      "公会",
      "公會",
      "guild"
    ].map((value) => String(value).replace(/\s+/g, "").toLowerCase()).filter(Boolean);
    let node = container;
    for (let depth = 0; node && depth < 4; depth++, node = node.parentElement) {
      const context = String(node.textContent || "").replace(/\s+/g, "").toLowerCase();
      if (contextLabels.some((value) => context.includes(value))) return true;
      if (typeof document !== "undefined" && (node === document.body || node === document.documentElement))
        break;
    }
    return false;
  }

  // src/features/dps/10-combat-sources.js
  var ClassSystem = (() => {
    const UNKNOWN = "unknown";
    const bus = new EventTarget();
    const definitions = {
      fire: {
        get label() {
          return Settings.getLanguage() === "en" ? "Fire Mage" : "火法";
        },
        color: "#C41E3A",
        get icon() {
          return GameAssets.item("blazing_trident");
        }
      },
      nature: {
        get label() {
          return Settings.getLanguage() === "en" ? "Nature Mage" : "自然法";
        },
        color: "#00FF98",
        get icon() {
          return GameAssets.item("blooming_trident");
        }
      },
      water: {
        get label() {
          return Settings.getLanguage() === "en" ? "Water Mage" : "水法";
        },
        color: "#3FC7EB",
        get icon() {
          return GameAssets.item("rippling_trident");
        }
      },
      sword: {
        get label() {
          return Settings.getLanguage() === "en" ? "Sword" : "剑";
        },
        color: "#C69B6D",
        get icon() {
          return GameAssets.item("regal_sword");
        }
      },
      mace: {
        get label() {
          return Settings.getLanguage() === "en" ? "Mace" : "锤";
        },
        color: "#A330C9",
        get icon() {
          return GameAssets.item("chaotic_flail");
        }
      },
      spear: {
        get label() {
          return Settings.getLanguage() === "en" ? "Spear" : "枪";
        },
        color: "#FFF468",
        get icon() {
          return GameAssets.item("furious_spear");
        }
      },
      bow: {
        get label() {
          return Settings.getLanguage() === "en" ? "Bow" : "弓";
        },
        color: "#AAD372",
        get icon() {
          return GameAssets.item("cursed_bow");
        }
      },
      crossbow: {
        get label() {
          return Settings.getLanguage() === "en" ? "Crossbow" : "弩";
        },
        color: "#0070DD",
        get icon() {
          return GameAssets.item("sundering_crossbow");
        }
      },
      shield: {
        get label() {
          return Settings.getLanguage() === "en" ? "Shield" : "盾";
        },
        color: "#F48CBA",
        get icon() {
          return GameAssets.item("griffin_bulwark");
        }
      },
      unknown: {
        get label() {
          return Settings.getLanguage() === "en" ? "Unknown" : "未知";
        },
        color: "#7f8c8d",
        get icon() {
          return GameAssets.skill("attack");
        }
      }
    };
    const detected = /* @__PURE__ */ new Map();
    let itemDetailMap = {};
    const representativeWeapons = {
      blazing_trident: "fire",
      blooming_trident: "nature",
      rippling_trident: "water",
      regal_sword: "sword",
      chaotic_flail: "mace",
      furious_spear: "spear",
      cursed_bow: "bow",
      sundering_crossbow: "crossbow",
      griffin_bulwark: "shield"
    };
    function tail2(v) {
      return String(v || "").split("/").pop().toLowerCase();
    }
    function normalizeWeapon(v) {
      return tail2(v).replace(/_refined$/, "");
    }
    function identifyStats(stats, fallbackInterval = 0) {
      stats = stats && typeof stats === "object" ? stats : {};
      const styles = (Array.isArray(stats.combatStyleHrids) ? stats.combatStyleHrids : stats.combatStyleHrid ? [stats.combatStyleHrid] : []).map(tail2);
      const style = styles[0] || "";
      const damageType = tail2(stats.damageType);
      const primary = tail2(stats.primaryTraining);
      const interval = Number(stats.attackInterval || fallbackInterval || 0);
      if (style === "magic") {
        if (damageType === "fire") return "fire";
        if (damageType === "nature") return "nature";
        if (damageType === "water") return "water";
      }
      if (style === "slash") return "sword";
      if (style === "stab") return "spear";
      if (style === "smash") return primary === "defense" ? "shield" : "mace";
      if (style === "ranged") {
        if (interval > 0)
          return Math.abs(interval - 32e8) <= Math.abs(interval - 36e8) ? "bow" : "crossbow";
        return UNKNOWN;
      }
      return UNKNOWN;
    }
    function itemDetailFor(weaponHrid) {
      const exact = String(weaponHrid || ""), short = tail2(exact), base = normalizeWeapon(exact);
      const keys = [exact, short, "/items/" + short, base, "/items/" + base];
      for (const key of keys) {
        const value = itemDetailMap instanceof Map ? itemDetailMap.get(key) : itemDetailMap && itemDetailMap[key];
        if (value && typeof value === "object") return value;
      }
      const values = itemDetailMap instanceof Map ? [...itemDetailMap.values()] : Object.values(itemDetailMap || {});
      return values.find((value) => {
        const hrid = value && (value.hrid || value.itemHrid || value.item && value.item.hrid || value.itemDetail && value.itemDetail.hrid);
        return hrid && normalizeWeapon(hrid) === base;
      }) || {};
    }
    function statsFromItemDetail(detail) {
      return detail && (detail.equipmentDetail && detail.equipmentDetail.combatStats || detail.item && detail.item.equipmentDetail && detail.item.equipmentDetail.combatStats || detail.itemDetail && detail.itemDetail.equipmentDetail && detail.itemDetail.equipmentDetail.combatStats || detail.combatStats) || null;
    }
    function namedWeaponClass(weaponHrid) {
      const weapon = normalizeWeapon(weaponHrid);
      for (const [needle, classId] of Object.entries(representativeWeapons))
        if (weapon.includes(needle)) return classId;
      if (/(^|_)(crossbow|xbow)(_|$)/.test(weapon) || weapon.includes("crossbow"))
        return "crossbow";
      if (/(^|_)bow(_|$)/.test(weapon) || weapon === "gobo_shooter") return "bow";
      if (weapon.includes("_fire_staff") || weapon === "infernal_battlestaff" || weapon === "gobo_boomstick")
        return "fire";
      if (weapon.includes("_nature_staff") || weapon === "jackalope_staff")
        return "nature";
      if (weapon.includes("_water_staff") || weapon === "frost_staff")
        return "water";
      if (/(^|_)(sword|slasher|dirk)(_|$)/.test(weapon)) return "sword";
      if (/(^|_)(mace|flail|bludgeon|smasher)(_|$)/.test(weapon)) return "mace";
      if (/(^|_)(spear|stabber)(_|$)/.test(weapon)) return "spear";
      if (/(^|_)(bulwark|shield|aegis)(_|$)/.test(weapon)) return "shield";
      return UNKNOWN;
    }
    function weaponHridFromWearable(wearable) {
      if (!wearable || typeof wearable !== "object") return "";
      const entries = wearable instanceof Map ? [...wearable.entries()] : Array.isArray(wearable) ? wearable.map((value, index) => [String(index), value]) : Object.entries(wearable);
      const atSlot = (...slots) => {
        for (const slot of slots) {
          const direct = wearable instanceof Map ? wearable.get(slot) : wearable[slot];
          if (direct) return direct;
          const found = entries.find(
            ([key, value]) => key === slot || value && (value.itemLocationHrid === slot || value.equipmentTypeHrid === slot || value.locationHrid === slot)
          );
          if (found) return found[1];
        }
        return null;
      };
      const weapon = atSlot(
        "/item_locations/main_hand",
        "/equipment_types/main_hand",
        "main_hand",
        "mainHand"
      ) || atSlot(
        "/item_locations/two_hand",
        "/equipment_types/two_hand",
        "two_hand",
        "twoHand"
      );
      return typeof weapon === "string" ? weapon : weapon && (weapon.itemHrid || weapon.hrid || weapon.item && weapon.item.hrid) || "";
    }
    function weaponHridFromPlayer(player) {
      if (!player || typeof player !== "object") return "";
      const direct = player.weaponHrid || player.mainHandItemHrid || player.twoHandItemHrid || player.combatDetails && player.combatDetails.weaponHrid;
      if (direct) return direct;
      const wearable = player.wearableItemMap || player.equipmentMap || player.equippedItems || player.equipment || {};
      const equipped = weaponHridFromWearable(wearable);
      if (equipped) return equipped;
      const stats = player.combatDetails && player.combatDetails.combatStats || player.combatStats || {};
      if (Number(stats.blaze) > 0) return "/items/blazing_trident";
      if (Number(stats.mayhem) > 0) return "/items/chaotic_flail";
      if (Number(stats.pierce) > 0) return "/items/sundering_crossbow";
      return "";
    }
    function identify(player) {
      const weaponHrid = weaponHridFromPlayer(player);
      if (weaponHrid) {
        const fromWeapon = classFromWeapon(weaponHrid);
        if (fromWeapon !== UNKNOWN) return fromWeapon;
      }
      const stats = player && player.combatDetails && player.combatDetails.combatStats || player && player.combatStats || {};
      return identifyStats(stats, player && player.attackInterval);
    }
    function syncWeaponCache(name, player) {
      if (!name) return "";
      const weaponHrid = weaponHridFromPlayer(player);
      if (weaponHrid) {
        Settings.setCachedWeapon(name, weaponHrid);
        return weaponHrid;
      }
      const stats = player && player.combatDetails && player.combatDetails.combatStats || player && player.combatStats;
      if (stats && ["blaze", "mayhem", "pierce"].some(
        (key) => Object.prototype.hasOwnProperty.call(stats, key)
      ))
        Settings.setCachedWeapon(name, "");
      return "";
    }
    function setDetected(name, classId, source = "被动战斗数据") {
      if (!name || !definitions[classId] || classId === UNKNOWN) return false;
      const changed = (detected.get(name) || Settings.getCachedClass(name)) !== classId;
      detected.set(name, classId);
      if (Settings.getCachedClass(name) !== classId)
        Settings.setCachedClass(name, classId);
      if (changed)
        bus.dispatchEvent(
          new CustomEvent("change", { detail: { name, classId, source } })
        );
      return true;
    }
    function classFor(name) {
      return Settings.getClassOverride(name) || detected.get(name) || Settings.getCachedClass(name) || UNKNOWN;
    }
    function get(name) {
      return definitions[classFor(name)] || definitions.unknown;
    }
    function setOverride(name, classId) {
      Settings.setClassOverride(name, classId === "auto" ? null : classId);
    }
    function resolveBattleClass(name, player, source) {
      const override = name ? Settings.getClassOverride(name) : null;
      if (override) return override;
      const known = name ? detected.get(name) || Settings.getCachedClass(name) || UNKNOWN : UNKNOWN;
      const live = identify(player);
      if (live === UNKNOWN) return known;
      const ranged = /* @__PURE__ */ new Set(["bow", "crossbow"]);
      const onlyRangedIntervalChanged = !weaponHridFromPlayer(player) && ranged.has(known) && ranged.has(live);
      const classId = onlyRangedIntervalChanged ? known : live;
      if (name && classId !== known) setDetected(name, classId, source);
      return classId;
    }
    function registerPlayers(players) {
      const out = {};
      (players || []).forEach((p) => {
        const name = p.character && p.character.name || p.name;
        syncWeaponCache(name, p);
        const classId = resolveBattleClass(name, p, "本场战斗人物属性");
        if (name) out[name] = classId;
      });
      return out;
    }
    function learnBattleUnit(payload) {
      const unit = payload && (payload.unit || payload.battleUnit || payload.combatUnit) || payload;
      if (!unit || typeof unit !== "object") return null;
      const name = unit.character && unit.character.name || unit.characterName || unit.name || payload && payload.characterName || "";
      syncWeaponCache(name, unit);
      const known = name ? classFor(name) : UNKNOWN;
      const classId = resolveBattleClass(name, unit, "战斗人物属性");
      if (!name || classId === UNKNOWN) return { name, classId, updated: false };
      return {
        name,
        classId,
        updated: known !== classId,
        source: "combatDetails.combatStats"
      };
    }
    function cacheItemDetails(map) {
      if (map && typeof map === "object") itemDetailMap = map;
    }
    function classFromWeapon(weaponHrid) {
      if (!weaponHrid) return UNKNOWN;
      const named = namedWeaponClass(weaponHrid);
      if (named !== UNKNOWN) return named;
      const detail = itemDetailFor(weaponHrid);
      const stats = statsFromItemDetail(detail);
      if (stats) {
        const classId = identifyStats(stats);
        if (classId !== UNKNOWN) return classId;
      }
      return UNKNOWN;
    }
    function learnProfile(payload) {
      const profile = payload && (payload.profile || payload.profileSharedData || payload.profileData) || payload;
      if (!profile || typeof profile !== "object") return null;
      const sharable = profile.sharableCharacter || {};
      const name = sharable.name || profile.characterName || profile.name || "";
      const wearable = profile.wearableItemMap || profile.equipmentMap || profile.equippedItems || {};
      const weaponHrid = weaponHridFromWearable(wearable);
      if (name && weaponHrid) Settings.setCachedWeapon(name, weaponHrid);
      const classId = classFromWeapon(weaponHrid);
      if (!name || classId === UNKNOWN)
        return {
          name,
          classId,
          weaponHrid,
          updated: false,
          source: "profile_shared.wearableItemMap"
        };
      return {
        name,
        classId,
        weaponHrid,
        updated: setDetected(name, classId, "手动点击人物装备"),
        source: "profile_shared.wearableItemMap"
      };
    }
    function classFromAbility(abilityHrid) {
      const ability = String(abilityHrid || "").toLowerCase();
      if (ability === "/abilities/maim" || ability === "/abilities/crippling_slash")
        return "sword";
      return UNKNOWN;
    }
    function learnAbility(name, abilityHrid) {
      const classId = classFromAbility(abilityHrid);
      if (!name || classId === UNKNOWN)
        return { name, classId, abilityHrid, updated: false };
      if (Settings.getClassOverride(name))
        return {
          name,
          classId: classFor(name),
          abilityHrid,
          updated: false,
          source: "手动职业覆盖"
        };
      return {
        name,
        classId,
        abilityHrid,
        updated: setDetected(name, classId, "武器专属技能"),
        source: "abilityHrid"
      };
    }
    function applyClasses(map) {
      Object.entries(map || {}).forEach(([name, c]) => setDetected(name, c));
    }
    function getWeapon(name) {
      return Settings.getCachedWeapon(name);
    }
    function diagnostics() {
      return Object.fromEntries(
        Array.from(detected, ([name, c]) => [
          name,
          { classId: classFor(name), detected: c, ...get(name) }
        ])
      );
    }
    return {
      bus,
      definitions,
      identify,
      registerPlayers,
      learnBattleUnit,
      learnProfile,
      learnAbility,
      classFromAbility,
      classFromWeapon,
      cacheItemDetails,
      applyClasses,
      setDetected,
      setOverride,
      classFor,
      get,
      getWeapon,
      diagnostics
    };
  })();
  var ClassDebug = (() => {
    const KEY = "kikimeter:class-debug:v1", MAX_EVENTS = 12;
    const bus = new EventTarget();
    let events = [];
    try {
      const saved = JSON.parse(localStorage.getItem(KEY) || "[]");
      if (Array.isArray(saved)) events = saved.slice(0, MAX_EVENTS);
    } catch (e) {
    }
    function tail2(v) {
      return String(v || "").split("/").pop().toLowerCase();
    }
    function candidateStats(player) {
      return [
        [
          "combatDetails.combatStats",
          player && player.combatDetails && player.combatDetails.combatStats
        ],
        [
          "combatUnit.combatDetails.combatStats",
          player && player.combatUnit && player.combatUnit.combatDetails && player.combatUnit.combatDetails.combatStats
        ],
        [
          "character.combatDetails.combatStats",
          player && player.character && player.character.combatDetails && player.character.combatDetails.combatStats
        ],
        ["combatStats", player && player.combatStats]
      ].filter(([, value]) => value && typeof value === "object").map(([path, stats]) => ({
        path,
        combatStyleHrids: Array.isArray(stats.combatStyleHrids) ? stats.combatStyleHrids.slice(0, 8) : stats.combatStyleHrids,
        combatStyleHrid: stats.combatStyleHrid,
        damageType: stats.damageType,
        primaryTraining: stats.primaryTraining,
        attackInterval: stats.attackInterval
      }));
    }
    function summarizePlayer(player, index) {
      const name = player && player.character && player.character.name || player && player.name || "玩家槽位" + index;
      const candidates = candidateStats(player);
      const primary = candidates[0] || {};
      const detectedClass = ClassSystem.identify(player);
      return {
        index,
        name,
        detectedClass,
        detectedLabel: (ClassSystem.definitions[detectedClass] || ClassSystem.definitions.unknown).label,
        selectedPath: primary.path || "未找到",
        topLevelKeys: player && typeof player === "object" ? Object.keys(player).sort() : [],
        candidates,
        normalized: {
          style: tail2(
            Array.isArray(primary.combatStyleHrids) ? primary.combatStyleHrids[0] : primary.combatStyleHrid
          ),
          damageType: tail2(primary.damageType),
          primaryTraining: tail2(primary.primaryTraining),
          attackInterval: Number(
            primary.attackInterval || player && player.attackInterval || 0
          )
        }
      };
    }
    function save() {
      try {
        localStorage.setItem(KEY, JSON.stringify(events));
      } catch (e) {
      }
    }
    function record(type, payload) {
      if (type !== "new_battle" && type !== "new_guild_battle") return;
      const players = Array.isArray(payload && payload.players) ? payload.players : [];
      events.unshift({
        time: (/* @__PURE__ */ new Date()).toISOString(),
        type,
        battleKey: String(
          payload && (payload.battleId || payload.guildBattleId || payload.combatStartTime || payload.combatId) || ""
        ),
        playerCount: players.length,
        players: players.map(summarizePlayer)
      });
      events = events.slice(0, MAX_EVENTS);
      save();
      bus.dispatchEvent(new Event("change"));
    }
    function clear() {
      events = [];
      save();
      bus.dispatchEvent(new Event("change"));
    }
    function get() {
      return JSON.parse(JSON.stringify(events));
    }
    function report() {
      if (Settings.getLanguage() === "en") {
        const output = events.map((event) => ({
          ...event,
          players: event.players.map((player) => ({
            ...player,
            detectedLabel: (ClassSystem.definitions[player.detectedClass] || ClassSystem.definitions.unknown).label
          }))
        }));
        return [
          `=== MWI DPS Meter | Class Diagnostics | ${VERSION} ===`,
          `Generated at: ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
          "Note: icons represent each class's signature weapon. The data below contains only class-identification fields.",
          `Recorded events: ${events.length}`,
          "",
          JSON.stringify(output, null, 2)
        ].join("\n");
      }
      const lines = [
        `=== 银河奶牛DPS统计|职业调试报告|${VERSION} ===`,
        "生成时间:" + (/* @__PURE__ */ new Date()).toLocaleString(),
        "说明:图标为职业代表武器;以下内容仅包含职业识别字段。",
        "记录事件数:" + events.length,
        ""
      ];
      events.forEach((event, eventIndex) => {
        lines.push(
          "#" + (eventIndex + 1) + " " + event.type + "|" + event.time + "|战斗标识 " + (event.battleKey || "无") + "|玩家 " + event.playerCount
        );
        event.players.forEach((p) => {
          lines.push(
            "  [" + p.index + "] " + p.name + " → " + p.detectedClass + "(" + p.detectedLabel + ")|读取路径 " + p.selectedPath
          );
          lines.push(
            "      规范化:style=" + (p.normalized.style || "空") + ",damageType=" + (p.normalized.damageType || "空") + ",primaryTraining=" + (p.normalized.primaryTraining || "空") + ",attackInterval=" + (p.normalized.attackInterval || 0)
          );
          if (p.candidates.length) {
            p.candidates.forEach(
              (c) => lines.push("      候选 " + c.path + ":" + JSON.stringify(c))
            );
          } else
            lines.push(
              "      未找到 combatStats;顶层字段:" + p.topLevelKeys.join(", ")
            );
        });
        lines.push("");
      });
      return lines.join("\n");
    }
    return { bus, record, clear, get, report, size: () => events.length };
  })();
  var ClassProbe = (() => {
    const KEY = "kikimeter:class-probe:v4", MAX_VALUES = 16, MAX_SAMPLES = 5;
    const MAX_DOM_SNAPSHOTS = 36, MAX_DOM_HTML = 12e4;
    const bus = new EventTarget();
    let abilityCatalog = {}, ticker = null, active = false, captureChars = 0;
    let clickHandler = null, domObserver = null, domTimer = null, state = load() || emptyState();
    function emptyState() {
      return {
        schemaVersion: 4,
        startedAt: null,
        endedAt: null,
        durationMs: 0,
        stopReason: "",
        messageCounts: {},
        roster: {},
        currentRoster: { trial: {}, combat: {} },
        rosterGeneration: { trial: -1, combat: -1 },
        rosterSignature: { trial: "", combat: "" },
        players: {},
        messageShapes: {},
        relatedMessages: {},
        fullMessages: [],
        domSnapshots: [],
        clicks: [],
        learnedClasses: []
      };
    }
    function load() {
      try {
        const value = JSON.parse(localStorage.getItem(KEY) || "null");
        return value && value.schemaVersion === 4 ? value : null;
      } catch (e) {
        return null;
      }
    }
    function save() {
      try {
        state.storageNotice = "全量消息正文仅保留在当前页面内存中,请在刷新或关闭页面前下载";
        const summary = { ...state, fullMessages: [] };
        localStorage.setItem(KEY, JSON.stringify(summary));
      } catch (ignore) {
      }
    }
    function addUnique(array, value, limit = MAX_VALUES) {
      if (value === void 0 || value === null) return;
      const normalized = typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value);
      if (!array.some((item) => item === normalized) && array.length < limit)
        array.push(normalized);
    }
    function safeValue(value) {
      if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean")
        return value;
      if (Array.isArray(value)) return value.slice(0, 8).map(safeValue);
      return "[对象]";
    }
    function sanitize(value, type = "", key = "", depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
      if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean")
        return value;
      if (typeof value === "string") {
        if (/password|token|authorization|cookie|secret|credential|email/i.test(key))
          return "[已脱敏]";
        if (/chat|whisper|mail/i.test(type) && /message|text|content/i.test(key))
          return "[聊天正文已脱敏]";
        return value;
      }
      if (typeof value !== "object") return String(value);
      if (seen.has(value)) return "[循环引用]";
      seen.add(value);
      if (Array.isArray(value))
        return value.map((item) => sanitize(item, type, key, depth + 1, seen));
      const out = {};
      Object.entries(value).forEach(([childKey, child]) => {
        out[childKey] = sanitize(child, type, childKey, depth + 1, seen);
      });
      return out;
    }
    function recordFullMessage(type, payload) {
      let text = "";
      try {
        text = JSON.stringify(sanitize(payload, type));
      } catch (e) {
        text = JSON.stringify({ type, error: "消息序列化失败:" + String(e) });
      }
      const entryChars = text.length + type.length + 80;
      captureChars += entryChars;
      state.fullMessages.push({
        offsetMs: state.startedAt ? Math.max(0, Date.now() - new Date(state.startedAt).getTime()) : 0,
        type,
        payloadText: text
      });
    }
    function recordUnparsed(payload) {
      if (!active) return;
      const value = typeof payload === "string" ? payload : {
        dataType: Object.prototype.toString.call(payload),
        size: Number(
          payload && payload.size || payload && payload.byteLength
        ) || 0
      };
      const type = "__non_json_message__";
      state.messageCounts[type] = (state.messageCounts[type] || 0) + 1;
      recordFullMessage(type, { raw: value });
      bus.dispatchEvent(new Event("change"));
    }
    function elementSnapshot(element) {
      if (!element) return null;
      let html = "";
      try {
        html = String(element.outerHTML || "").slice(0, MAX_DOM_HTML);
      } catch (e) {
      }
      const attrs = {};
      try {
        Array.from(element.attributes || []).forEach((attr) => {
          if (!/style/i.test(attr.name)) attrs[attr.name] = attr.value;
        });
      } catch (e) {
      }
      return {
        tag: String(element.tagName || ""),
        className: String(element.className || ""),
        attributes: attrs,
        text: String(element.textContent || "").trim().slice(0, 3e4),
        html
      };
    }
    function isVisible2(element) {
      if (!element) return false;
      try {
        const style = getComputedStyle(element);
        return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
      } catch (e) {
        return true;
      }
    }
    function captureDom(reason) {
      if (!active || state.domSnapshots.length >= MAX_DOM_SNAPSHOTS || !document.querySelectorAll)
        return;
      const selectors = [
        '[role="dialog"]',
        '[class*="BattleUnit"]',
        '[class*="CombatUnitStatsText"]',
        '[class*="Modal_modal"]',
        '[class*="modalContent"]'
      ];
      const elements = [];
      selectors.forEach((selector) => {
        try {
          document.querySelectorAll(selector).forEach((element) => {
            if (element.closest && element.closest('[data-kikimeter="true"]'))
              return;
            if (isVisible2(element) && !elements.includes(element))
              elements.push(element);
          });
        } catch (e) {
        }
      });
      if (!elements.length) return;
      const views = elements.slice(0, 20).map(elementSnapshot).filter(Boolean);
      const signature = views.map((view) => view.className + "|" + view.text).join("\n").slice(0, 2e5);
      const previous = state.domSnapshots[state.domSnapshots.length - 1];
      if (previous && previous.signature === signature) return;
      state.domSnapshots.push({
        offsetMs: Math.max(0, Date.now() - new Date(state.startedAt).getTime()),
        reason,
        signature,
        views
      });
    }
    function scheduleDomCapture(reason) {
      if (!active) return;
      if (domTimer !== null) clearTimeout(domTimer);
      domTimer = setTimeout(() => {
        domTimer = null;
        captureDom(reason);
      }, 120);
    }
    function attachInteractionObservers() {
      if (!document.addEventListener) return;
      clickHandler = (event) => {
        if (!active) return;
        const target = event.target;
        const unit = target && target.closest ? target.closest('[class*="CombatUnit"],[class*="MiniUnit"]') : null;
        state.clicks.push({
          offsetMs: Math.max(0, Date.now() - new Date(state.startedAt).getTime()),
          target: elementSnapshot(target),
          unit: elementSnapshot(unit)
        });
        scheduleDomCapture("点击后页面变化");
        setTimeout(() => captureDom("点击后500毫秒"), 500);
      };
      document.addEventListener("click", clickHandler, true);
      if (typeof MutationObserver !== "undefined" && document.documentElement) {
        domObserver = new MutationObserver(
          () => scheduleDomCapture("人物面板DOM变化")
        );
        domObserver.observe(document.documentElement, {
          childList: true,
          subtree: true,
          attributes: true,
          attributeFilter: ["class", "role", "aria-hidden"]
        });
      }
    }
    function detachInteractionObservers() {
      if (clickHandler && document.removeEventListener)
        document.removeEventListener("click", clickHandler, true);
      clickHandler = null;
      if (domObserver) domObserver.disconnect();
      domObserver = null;
      if (domTimer !== null) clearTimeout(domTimer);
      domTimer = null;
    }
    function scanRelevant(value, path = "", depth = 0, out = { paths: [], values: {}, hrids: [] }) {
      if (depth > 5 || value === null || value === void 0) return out;
      if (typeof value === "string") {
        if (value.startsWith("/")) addUnique(out.hrids, value, 40);
        return out;
      }
      if (typeof value !== "object") return out;
      const entries = Array.isArray(value) ? value.slice(0, 12).map((item, index) => [String(index), item]) : Object.entries(value).slice(0, 80);
      entries.forEach(([key, child]) => {
        const childPath = path ? path + "." + key : key;
        addUnique(out.paths, childPath, 120);
        if (/ability|combat|style|damage|type|training|attack|interval|weapon|equipment|item|hrid|auto/i.test(
          key
        )) {
          if (child === null || typeof child !== "object") {
            if (!out.values[childPath]) out.values[childPath] = [];
            addUnique(out.values[childPath], safeValue(child));
          } else if (Array.isArray(child) && child.every((item) => item === null || typeof item !== "object")) {
            if (!out.values[childPath]) out.values[childPath] = [];
            child.slice(0, 12).forEach(
              (item) => addUnique(out.values[childPath], safeValue(item))
            );
          }
        }
        if (typeof child === "string" && child.startsWith("/"))
          addUnique(out.hrids, child, 40);
        if (child && typeof child === "object")
          scanRelevant(child, childPath, depth + 1, out);
      });
      return out;
    }
    function mergeScan(target, scan2) {
      scan2.paths.forEach((path) => addUnique(target.paths, path, 120));
      scan2.hrids.forEach((hrid) => addUnique(target.hrids, hrid, 40));
      Object.entries(scan2.values).forEach(([path, values]) => {
        if (!target.values[path]) target.values[path] = [];
        values.forEach((value) => addUnique(target.values[path], value));
      });
    }
    function cacheClientData(payload) {
      if (payload && payload.abilityDetailMap && typeof payload.abilityDetailMap === "object")
        abilityCatalog = payload.abilityDetailMap;
    }
    function channelLabel(channel) {
      return channel === "trial" ? "试炼" : "普通";
    }
    function registerRoster(players, channel) {
      if (!Array.isArray(players) || !players.length) return;
      const entries = players.map((player, index) => ({
        slot: String(index),
        name: player.character && player.character.name || player.name || "玩家槽位" + index,
        player
      }));
      const signature = entries.map((entry) => entry.slot + "=" + entry.name).join("|");
      if (signature !== state.rosterSignature[channel]) {
        const previous = Number(state.rosterGeneration[channel]);
        state.rosterGeneration[channel] = (Number.isFinite(previous) ? previous : -1) + 1;
        state.rosterSignature[channel] = signature;
        state.currentRoster[channel] = {};
      }
      const generation = Math.max(
        0,
        Number(state.rosterGeneration[channel]) || 0
      );
      entries.forEach(({ slot, name, player }) => {
        const key = channel + ":" + generation + ":" + slot;
        state.currentRoster[channel][slot] = key;
        state.roster[key] = {
          channel,
          generation,
          slot,
          name,
          initialClass: ClassSystem.identify(player),
          initialPath: player.combatDetails && player.combatDetails.combatStats ? "combatDetails.combatStats" : "未找到"
        };
        if (state.players[key]) state.players[key].name = name;
      });
    }
    function seedRoster() {
      const latest = ClassDebug.get()[0];
      if (!latest || !Array.isArray(latest.players)) return;
      const channel = latest.type === "new_guild_battle" ? "trial" : "combat";
      registerRoster(
        latest.players.map((player) => ({
          name: player.name,
          combatDetails: player.selectedStats ? { combatStats: player.selectedStats } : void 0
        })),
        channel
      );
    }
    function ensurePlayer(slot, channel) {
      const key = state.currentRoster[channel] && state.currentRoster[channel][slot] || channel + ":unknown:" + slot;
      const roster = state.roster[key];
      if (!state.players[key])
        state.players[key] = {
          key,
          channel,
          generation: roster ? roster.generation : -1,
          slot,
          name: roster && roster.name || "玩家槽位" + slot,
          updateCount: 0,
          fieldKeys: [],
          paths: [],
          values: {},
          hrids: [],
          abilities: [],
          autoAttackCount: 0,
          mpDropCount: 0,
          mpDropValues: [],
          lastMP: null,
          samples: []
        };
      return state.players[key];
    }
    function recordRoster(payload, channel) {
      registerRoster(payload.players || [], channel);
    }
    function recordPlayerMap(payload, channel) {
      const pMap = payload && payload.pMap && typeof payload.pMap === "object" ? payload.pMap : {};
      Object.entries(pMap).forEach(([slot, update]) => {
        if (!update || typeof update !== "object") return;
        const player = ensurePlayer(slot, channel);
        player.updateCount++;
        Object.keys(update).forEach(
          (key) => addUnique(player.fieldKeys, key, 80)
        );
        const scan2 = scanRelevant(update);
        mergeScan(player, scan2);
        scan2.hrids.filter((value) => String(value).startsWith("/abilities/")).forEach((value) => addUnique(player.abilities, value, 30));
        Object.entries(scan2.values).forEach(([path, values]) => {
          if (/ability.*hrid/i.test(path))
            values.forEach(
              (value) => typeof value === "string" && addUnique(player.abilities, value, 30)
            );
        });
        if (update.isAutoAtk === true || update.isAutoAttack === true)
          player.autoAttackCount++;
        const mp = Number(
          update.cMP !== void 0 ? update.cMP : update.currentManapoints
        );
        if (Number.isFinite(mp)) {
          if (player.lastMP !== null && mp < player.lastMP) {
            player.mpDropCount++;
            addUnique(player.mpDropValues, player.lastMP - mp, 20);
          }
          player.lastMP = mp;
        }
        const sample = JSON.stringify(update);
        addUnique(player.samples, sample, MAX_SAMPLES);
      });
    }
    function recordRelated(type, payload) {
      if (!/(battle|combat|ability|equipment|character_stats)/i.test(type))
        return;
      const scan2 = scanRelevant(payload);
      if (!state.relatedMessages[type])
        state.relatedMessages[type] = {
          count: 0,
          paths: [],
          values: {},
          hrids: []
        };
      const target = state.relatedMessages[type];
      target.count++;
      mergeScan(target, scan2);
    }
    function record(type, payload) {
      if (type === "init_client_data") cacheClientData(payload);
      if (!active) return;
      state.messageCounts[type] = (state.messageCounts[type] || 0) + 1;
      if (!state.messageShapes[type]) state.messageShapes[type] = [];
      Object.keys(payload || {}).forEach(
        (key) => addUnique(state.messageShapes[type], key, 80)
      );
      recordFullMessage(type, payload);
      if (type === "new_guild_battle") recordRoster(payload, "trial");
      if (type === "new_battle") recordRoster(payload, "combat");
      if (type === "guild_battle_updated") recordPlayerMap(payload, "trial");
      if (type === "battle_updated") recordPlayerMap(payload, "combat");
      if (type === "battle_unit_fetched" || type === "profile_shared") {
        const learned = type === "profile_shared" ? ClassSystem.learnProfile(payload) : ClassSystem.learnBattleUnit(payload);
        if (learned) addUnique(state.learnedClasses, JSON.stringify(learned), 80);
        scheduleDomCapture("收到 " + type);
      }
      recordRelated(type, payload);
    }
    function start2() {
      if (active) return status();
      state = emptyState();
      state.startedAt = (/* @__PURE__ */ new Date()).toISOString();
      active = true;
      captureChars = 0;
      seedRoster();
      attachInteractionObservers();
      ticker = setInterval(() => bus.dispatchEvent(new Event("change")), 1e3);
      save();
      bus.dispatchEvent(new Event("change"));
      console.info(
        "[KikiMeter] 全量入站消息采集已开始;点击“结束采集”才会停止。"
      );
      return status();
    }
    function stop(reason = "手动停止") {
      if (!active) return status();
      active = false;
      if (ticker !== null) clearInterval(ticker);
      ticker = null;
      detachInteractionObservers();
      state.endedAt = (/* @__PURE__ */ new Date()).toISOString();
      state.durationMs = Math.max(
        0,
        new Date(state.endedAt) - new Date(state.startedAt)
      );
      state.stopReason = reason;
      save();
      bus.dispatchEvent(new Event("change"));
      console.info(
        "[KikiMeter] 全量入站消息采集已结束,共 " + state.fullMessages.length + " 条消息。"
      );
      return status();
    }
    function clear() {
      if (active) stop("清空前停止");
      state = emptyState();
      captureChars = 0;
      save();
      bus.dispatchEvent(new Event("change"));
    }
    function status() {
      const elapsed = active && state.startedAt ? Date.now() - new Date(state.startedAt).getTime() : state.durationMs || 0;
      return {
        active,
        elapsedMs: elapsed,
        startedAt: state.startedAt,
        endedAt: state.endedAt,
        captureChars,
        playerCount: Object.keys(state.players).length,
        messageCount: Object.values(state.messageCounts).reduce(
          (sum, count) => sum + count,
          0
        ),
        fullMessageCount: state.fullMessages.length,
        domSnapshotCount: state.domSnapshots.length
      };
    }
    function abilityEvidence(abilityHrid) {
      const detail = abilityCatalog && abilityCatalog[abilityHrid];
      return detail ? scanRelevant(detail) : null;
    }
    function report() {
      if (Settings.getLanguage() === "en") {
        const exportedState = JSON.parse(JSON.stringify(state));
        if (exportedState.storageNotice) {
          exportedState.storageNotice = "Full message bodies are kept only in this page's memory. Download them before refreshing or closing the page.";
        }
        return [
          `=== MWI DPS Meter | Manual Full Incoming-Message Probe | ${VERSION} ===`,
          `Generated at: ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
          `Capture started: ${state.startedAt || "Not started"}`,
          `Capture ended: ${state.endedAt || (active ? "In progress" : "None")}`,
          `Effective capture: ${(status().elapsedMs / 1e3).toFixed(1)} seconds`,
          "Network policy: this probe does not call fetch, XHR, or WebSocket.send. It only reads data the game already received.",
          "Scope: all incoming game WebSocket messages, user-selected targets, and character-panel DOM between start and stop. Chat bodies and credential fields are redacted.",
          `Messages: ${state.fullMessages.length} | Message bodies: ${(captureChars / 1024 / 1024).toFixed(2)} MB | DOM snapshots: ${state.domSnapshots.length}`,
          "",
          JSON.stringify(exportedState, null, 2),
          "",
          "--- Initial-message class report ---",
          ClassDebug.report()
        ].join("\n");
      }
      const lines = [
        `=== 银河奶牛DPS统计|手动全量入站消息探针|${VERSION} ===`,
        "生成时间:" + (/* @__PURE__ */ new Date()).toLocaleString(),
        "采样开始:" + (state.startedAt || "未开始"),
        "采样结束:" + (state.endedAt || (active ? "进行中" : "无")),
        "有效采样:" + (status().elapsedMs / 1e3).toFixed(1) + " 秒|" + (state.stopReason || "尚未完成"),
        "网络原则:本探针没有调用 fetch、XHR 或 WebSocket.send,只被动读取游戏原本收到的数据。",
        "采集范围:从点击开始到点击结束期间的全部游戏 WebSocket 入站消息、用户点击目标、人物面板 DOM;聊天正文及凭证类字段已脱敏。",
        "完整消息:" + state.fullMessages.length + " 条|消息正文:" + (captureChars / 1024 / 1024).toFixed(2) + " MB|DOM快照:" + state.domSnapshots.length + " 份",
        "",
        "消息计数:" + JSON.stringify(state.messageCounts),
        "消息顶层字段:" + JSON.stringify(state.messageShapes),
        ""
      ];
      if (state.storageNotice) lines.push("存储提示:" + state.storageNotice, "");
      if (state.learnedClasses.length)
        lines.push(
          "人物点击即时职业识别:" + state.learnedClasses.join(" | "),
          ""
        );
      const rosterValues = Object.values(state.roster), uniqueRoster = /* @__PURE__ */ new Map();
      rosterValues.forEach(
        (player) => uniqueRoster.set(player.channel + "|" + player.name, player)
      );
      const uniqueValues = [...uniqueRoster.values()], withStats = uniqueValues.filter(
        (player) => player.initialClass && player.initialClass !== "unknown"
      ).length;
      lines.push(
        "名册人数:" + uniqueValues.length + "|初始化可识别:" + withStats + "|初始化无职业字段:" + Math.max(0, uniqueValues.length - withStats),
        ""
      );
      Object.values(state.players).sort(
        (a, b) => a.channel.localeCompare(b.channel) || a.generation - b.generation || Number(a.slot) - Number(b.slot)
      ).forEach((player) => {
        const formation = player.generation >= 0 ? "第" + (player.generation + 1) + "阵容" : "未知阵容";
        lines.push(
          "[" + channelLabel(player.channel) + " " + formation + " slot " + player.slot + "] " + player.name + "|更新 " + player.updateCount + "|MP下降 " + player.mpDropCount + "|自动攻击 " + player.autoAttackCount
        );
        lines.push("  pMap字段:" + (player.fieldKeys.join(", ") || "无"));
        lines.push("  技能:" + (player.abilities.join(", ") || "未出现"));
        lines.push("  MP消耗:" + (player.mpDropValues.join(", ") || "未出现"));
        lines.push("  Hrid:" + (player.hrids.join(", ") || "无"));
        lines.push("  相关值:" + JSON.stringify(player.values));
        lines.push("  原始样本:" + (player.samples.join(" || ") || "无"));
        player.abilities.forEach((ability) => {
          const evidence = abilityEvidence(ability);
          lines.push(
            "  技能定义 " + ability + ":" + (evidence ? JSON.stringify({
              values: evidence.values,
              hrids: evidence.hrids
            }) : "客户端能力表中未找到")
          );
        });
        lines.push("");
      });
      lines.push("其他相关消息摘要:" + JSON.stringify(state.relatedMessages));
      lines.push("", "--- 用户点击记录 ---");
      if (!state.clicks.length) lines.push("采样期间没有捕获到页面点击。");
      state.clicks.forEach(
        (click, index) => lines.push(
          "#" + (index + 1) + " +" + click.offsetMs + "ms\n" + JSON.stringify(click)
        )
      );
      lines.push("", "--- 人物面板 DOM 快照 ---");
      if (!state.domSnapshots.length)
        lines.push(
          "没有捕获到可见的人物面板;如需分析人物详情,请在采集期间点开战斗中的人物。"
        );
      state.domSnapshots.forEach((snapshot, index) => {
        const output = { ...snapshot };
        delete output.signature;
        lines.push(
          "#" + (index + 1) + " +" + snapshot.offsetMs + "ms|" + snapshot.reason + "\n" + JSON.stringify(output)
        );
      });
      lines.push("", "--- 全部入站消息(按接收顺序) ---");
      if (!state.fullMessages.length) lines.push("没有记录到入站消息。");
      state.fullMessages.forEach((message, index) => {
        lines.push(
          "#" + (index + 1) + " +" + message.offsetMs + "ms|" + message.type + "\n" + message.payloadText
        );
      });
      lines.push("", "--- 初始化消息职业报告 ---", ClassDebug.report());
      return lines.join("\n");
    }
    function download() {
      if (!state.startedAt) {
        console.warn("[KikiMeter] 尚未开始全量消息采集,已取消空报告下载。");
        return null;
      }
      if (active) {
        console.warn("[KikiMeter] 请先点击“结束采集”,再下载全量 MSG。");
        return null;
      }
      const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
      const blob = new Blob(["\uFEFF" + report()], {
        type: "text/plain;charset=utf-8"
      });
      const url = URL.createObjectURL(blob);
      const link = Object.assign(document.createElement("a"), {
        href: url,
        download: "mwi-full-msg-capture-" + stamp + ".txt"
      });
      document.body.appendChild(link);
      link.click();
      link.remove();
      URL.revokeObjectURL(url);
      return link.download;
    }
    return {
      bus,
      record,
      recordUnparsed,
      start: start2,
      stop,
      clear,
      status,
      report,
      download,
      get: () => JSON.parse(JSON.stringify(state))
    };
  })();
  function installThemeFont() {
    if (document.getElementById("kikimeter-zh-theme")) return;
    const style = document.createElement("style");
    style.id = "kikimeter-zh-theme";
    style.textContent = `[data-kikimeter="true"]{font-family:'Microsoft YaHei','微软雅黑','Noto Sans SC',sans-serif!important}`;
    (document.head || document.documentElement).appendChild(style);
  }
  var DamageSources = /* @__PURE__ */ (() => {
    const combatIcon = () => GameAssets.skill("attack");
    const labels = {
      auto: ["普通攻击", "Auto Attack"],
      reflect: ["反伤", "Reflection"],
      dot: ["持续伤害", "Damage Over Time"],
      unknown: ["未识别来源", "Unknown Source"],
      legacy: ["旧版本未记录来源", "Legacy Untracked"]
    };
    const itemLabels = {
      "/items/blazing_trident": ["炽焰三叉戟特效", "Blazing Trident Effect"],
      "/items/blazing_trident_refined": [
        "炽焰三叉戟★特效",
        "Blazing Trident ★ Effect"
      ],
      "/items/chaotic_flail": ["混沌连枷特效", "Chaotic Flail Effect"],
      "/items/chaotic_flail_refined": ["混沌连枷★特效", "Chaotic Flail ★ Effect"],
      "/items/sundering_crossbow": ["裂空弩特效", "Sundering Crossbow Effect"],
      "/items/sundering_crossbow_refined": [
        "裂空弩★特效",
        "Sundering Crossbow ★ Effect"
      ]
    };
    const supportAbilities = /* @__PURE__ */ new Set([
      "/abilities/aqua_aura",
      "/abilities/berserk",
      "/abilities/critical_aura",
      "/abilities/elemental_affinity",
      "/abilities/elusiveness",
      "/abilities/fierce_aura",
      "/abilities/flame_aura",
      "/abilities/frenzy",
      "/abilities/guardian_aura",
      "/abilities/heal",
      "/abilities/insanity",
      "/abilities/invincible",
      "/abilities/minor_heal",
      "/abilities/mystic_aura",
      "/abilities/precision",
      "/abilities/provoke",
      "/abilities/quick_aid",
      "/abilities/rejuvenate",
      "/abilities/revive",
      "/abilities/speed_aura",
      "/abilities/sylvan_aura",
      "/abilities/taunt",
      "/abilities/toughness",
      "/abilities/vampirism"
    ]);
    function normalize2(source) {
      const value = String(source || "").trim();
      if (!value || value === "idle") return "unknown";
      return value;
    }
    function decodeCombined(value) {
      const parts = String(value || "").slice(9).split("|");
      const decode = (part) => {
        try {
          return decodeURIComponent(part || "");
        } catch (ignore) {
          return part || "";
        }
      };
      return { weapon: decode(parts[0]), action: decode(parts[1]) };
    }
    function isCombined(source) {
      return normalize2(source).startsWith("combined:");
    }
    function isSupport(source) {
      return supportAbilities.has(normalize2(source));
    }
    function clientAbilityName(value) {
      const map = runtime.state.initData_abilityDetailMap;
      const detail = map instanceof Map ? map.get(value) : map?.[value];
      return String(detail?.name || "").trim();
    }
    function abilityLabel(value, english) {
      if (english)
        return clientAbilityName(value) || (value === "/abilities/natures_veil" ? "Nature's Veil" : "") || value.split("/").pop().replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
      return getLocalizedEntityName("ability", value) || clientAbilityName(value) || value.split("/").pop().replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
    }
    function canonical(source, playerName = "") {
      const value = normalize2(source);
      if (value.startsWith("combined:")) {
        const { weapon, action } = decodeCombined(value);
        return isSupport(action) ? normalize2(weapon) : normalize2(action);
      }
      if (isSupport(value)) {
        const weapon = String(ClassSystem.getWeapon(playerName) || "");
        if (weapon.includes("blazing_trident")) return weapon;
      }
      return value;
    }
    function label(source) {
      const value = normalize2(source), english = Settings.getLanguage() === "en";
      if (value.startsWith("/abilities/")) return abilityLabel(value, english);
      if (labels[value]) return labels[value][english ? 1 : 0];
      if (value.startsWith("dot:")) {
        const ability = value.slice(4), abilityName2 = ability.startsWith("/abilities/") ? abilityLabel(ability, english) : labels[ability]?.[english ? 1 : 0] || ability.split("/").pop().replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
        return english ? "Damage Over Time (" + abilityName2 + ")" : "持续伤害(" + abilityName2 + ")";
      }
      if (value.startsWith("combined:")) {
        const { weapon, action } = decodeCombined(value);
        const weaponName = label(weapon);
        return label(action) + "(" + (english ? "includes " : "含") + (english ? weaponName : weaponName.startsWith("武器特效:") ? weaponName.slice(5) + "特效" : weaponName) + ")";
      }
      if (value.startsWith("/items/")) {
        const itemDetailMap = runtime.state.initData_itemDetailMap;
        const itemDetail = itemDetailMap instanceof Map ? itemDetailMap.get(value) : itemDetailMap?.[value];
        const englishName = String(itemDetail?.name || "").trim() || itemLabels[value]?.[1] || value.split("/").pop().replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
        const localizedName = getLocalizedEntityName("item", value) || itemLabels[value]?.[0] || englishName;
        return english ? englishName + " Effect" : "武器特效:" + localizedName;
      }
      const tail2 = value.split("/").pop().replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
      return tail2 || labels.unknown[english ? 1 : 0];
    }
    function icon(source, playerName = "") {
      const value = normalize2(source);
      if (value.startsWith("dot:")) {
        const ability = value.slice(4);
        return ability.startsWith("/abilities/") ? GameAssets.ability(ability) : combatIcon();
      }
      if (value.startsWith("combined:")) {
        const { action } = decodeCombined(value);
        if (action.startsWith("/abilities/")) return GameAssets.ability(action);
        return combatIcon();
      }
      if (value.startsWith("/abilities/")) return GameAssets.ability(value);
      if (value.startsWith("/items/")) {
        const classId = ClassSystem.classFromWeapon(value), definition = ClassSystem.definitions[classId];
        if (definition && definition.icon) return definition.icon;
      }
      if (value.startsWith("weapon:"))
        return ClassSystem.get(playerName).icon || combatIcon();
      return combatIcon();
    }
    return { normalize: normalize2, canonical, isCombined, isSupport, label, icon };
  })();
  var TakenSources = /* @__PURE__ */ (() => {
    function encode(monsterName2, monsterHrid, ability) {
      return "taken:" + [monsterName2 || "", monsterHrid || "", DamageSources.normalize(ability)].map(encodeURIComponent).join("|");
    }
    function decode(source) {
      const raw = String(source || "");
      if (!raw.startsWith("taken:"))
        return { monsterName: "", monsterHrid: "", ability: "unknown" };
      const parts = raw.slice(6).split("|").map((part) => {
        try {
          return decodeURIComponent(part);
        } catch (ignore) {
          return part;
        }
      });
      return {
        monsterName: parts[0] || "",
        monsterHrid: parts[1] || "",
        ability: parts[2] || "unknown"
      };
    }
    function monsterLabel(detail) {
      if (Settings.getLanguage() !== "en") {
        const officialName = getLocalizedEntityName(
          "monster",
          detail.monsterHrid
        );
        if (officialName) return officialName;
      }
      if (detail.monsterName) return detail.monsterName;
      const tail2 = String(detail.monsterHrid || "").split("/").pop().replace(/_/g, " ");
      return tail2 ? tail2.replace(/\b\w/g, (char) => char.toUpperCase()) : Settings.getLanguage() === "en" ? "Unknown Monster" : "未知怪物";
    }
    function label(source) {
      const detail = decode(source);
      return monsterLabel(detail) + " · " + DamageSources.label(detail.ability);
    }
    function icon(source) {
      return DamageSources.icon(decode(source).ability);
    }
    return { encode, decode, label, icon };
  })();

  // src/features/dps/20-session.js
  var Session = (() => {
    let startTs = performance.now();
    let elapsedOffset = 0;
    let teamDamage = 0;
    let frozenAt = null;
    let meta = {};
    let fragments = [];
    let fragmentStart = Date.now();
    let fragmentReason = "开始战斗";
    let fragmentBase = {
      teamDamage: 0,
      damage: {},
      sources: {},
      healing: {},
      taken: {},
      takenSources: {},
      kills: {},
      accuracy: {}
    };
    let fragmentPrefix = null;
    const playerDamage = /* @__PURE__ */ new Map();
    const playerDamageSources = /* @__PURE__ */ new Map();
    const playerHealing = /* @__PURE__ */ new Map();
    const playerKills = /* @__PURE__ */ new Map();
    const playerTaken = /* @__PURE__ */ new Map();
    const playerTakenSources = /* @__PURE__ */ new Map();
    const playerAccuracy = /* @__PURE__ */ new Map();
    const BUCKETS = 150, BUCKET_MS = 2e3;
    const dmgBuckets = new Array(BUCKETS).fill(0);
    const bossBuckets = new Array(BUCKETS).fill(false);
    let curBucket = 0, bucketTs = performance.now();
    let _isBoss = false;
    const FULL_MAX_BUCKETS = 1900;
    const fullDmgBuckets = [0];
    const fullBossBuckets = [false];
    let fullBucketTs = performance.now();
    const mapObject = (m) => Object.fromEntries(m);
    const restoreMap = (target, obj) => {
      target.clear();
      Object.entries(obj || {}).forEach(
        ([k, v]) => target.set(k, Number(v) || 0)
      );
    };
    const nestedMapObject = (map) => Object.fromEntries(
      Array.from(map, ([name, sources]) => [name, Object.fromEntries(sources)])
    );
    const restoreNestedMap = (target, obj) => {
      target.clear();
      Object.entries(obj || {}).forEach(([name, sources]) => {
        const map = /* @__PURE__ */ new Map();
        Object.entries(sources || {}).forEach(
          ([source, value]) => map.set(source, Number(value) || 0)
        );
        target.set(name, map);
      });
    };
    const accuracyObject = (map) => Object.fromEntries(
      Array.from(map, ([name, value]) => [
        name,
        {
          attempts: Number(value.attempts) || 0,
          hits: Number(value.hits) || 0,
          monsters: Object.fromEntries(value.monsters || [])
        }
      ])
    );
    const restoreAccuracy = (target, raw) => {
      target.clear();
      Object.entries(raw || {}).forEach(([name, value]) => {
        const monsters = /* @__PURE__ */ new Map();
        Object.entries(value && value.monsters || {}).forEach(
          ([key, monster]) => {
            const attempts2 = Number(monster && monster.attempts) || 0;
            if (!(attempts2 > 0)) return;
            monsters.set(key, {
              monsterName: String(monster && monster.monsterName || ""),
              monsterHrid: String(monster && monster.monsterHrid || ""),
              attempts: attempts2,
              hits: Math.max(
                0,
                Math.min(attempts2, Number(monster && monster.hits) || 0)
              )
            });
          }
        );
        const attempts = Number(value && value.attempts) || 0;
        if (!(attempts > 0) && !monsters.size) return;
        target.set(name, {
          attempts,
          hits: Math.max(0, Math.min(attempts, Number(value && value.hits) || 0)),
          monsters
        });
      });
    };
    const accuracyDelta = (map, base = {}) => Object.fromEntries(
      Array.from(map, ([name, value]) => {
        const previous = base[name] || {}, previousMonsters = previous.monsters || {};
        const monsters = Object.fromEntries(
          Array.from(value.monsters || [], ([key, monster]) => {
            const old = previousMonsters[key] || {}, attempts2 = (Number(monster.attempts) || 0) - (Number(old.attempts) || 0), hits2 = (Number(monster.hits) || 0) - (Number(old.hits) || 0);
            return [
              key,
              {
                monsterName: monster.monsterName || old.monsterName || "",
                monsterHrid: monster.monsterHrid || old.monsterHrid || "",
                attempts: attempts2,
                hits: hits2
              }
            ];
          }).filter(([, monster]) => monster.attempts > 0)
        );
        const attempts = (Number(value.attempts) || 0) - (Number(previous.attempts) || 0), hits = (Number(value.hits) || 0) - (Number(previous.hits) || 0);
        return [name, { attempts, hits, monsters }];
      }).filter(
        ([, value]) => value.attempts > 0 || Object.keys(value.monsters).length
      )
    );
    const accuracyBaseFromDelta = (map, delta = {}) => {
      const base = accuracyObject(map);
      Object.entries(delta || {}).forEach(([name, value]) => {
        if (!base[name]) return;
        base[name].attempts = Math.max(
          0,
          (Number(base[name].attempts) || 0) - (Number(value && value.attempts) || 0)
        );
        base[name].hits = Math.max(
          0,
          (Number(base[name].hits) || 0) - (Number(value && value.hits) || 0)
        );
        Object.entries(value && value.monsters || {}).forEach(
          ([key, monster]) => {
            if (!base[name].monsters[key]) return;
            base[name].monsters[key].attempts = Math.max(
              0,
              (Number(base[name].monsters[key].attempts) || 0) - (Number(monster && monster.attempts) || 0)
            );
            base[name].monsters[key].hits = Math.max(
              0,
              (Number(base[name].monsters[key].hits) || 0) - (Number(monster && monster.hits) || 0)
            );
            if (!(base[name].monsters[key].attempts > 0))
              delete base[name].monsters[key];
          }
        );
        if (!(base[name].attempts > 0) && !Object.keys(base[name].monsters).length)
          delete base[name];
      });
      return base;
    };
    const nestedDelta = (map, base = {}) => Object.fromEntries(
      Array.from(map, ([name, sources]) => {
        const previous = base[name] || {};
        const values = Object.fromEntries(
          Array.from(sources, ([source, value]) => [
            source,
            value - (Number(previous[source]) || 0)
          ]).filter(([, value]) => value !== 0)
        );
        return [name, values];
      }).filter(([, values]) => Object.keys(values).length)
    );
    const mapDelta = (map, base) => Object.fromEntries(
      Array.from(map, ([k, v]) => [k, v - (base[k] || 0)]).filter(
        ([, v]) => v !== 0
      )
    );
    function elapsed() {
      return frozenAt !== null ? frozenAt : elapsedOffset + (performance.now() - startTs) / 1e3;
    }
    function currentFragment(endAt = Date.now()) {
      if (fragmentStart === null) return null;
      const activeDuration = Math.max(0, endAt - fragmentStart);
      return {
        startedAt: fragmentPrefix && fragmentPrefix.startedAt || new Date(fragmentStart).toISOString(),
        endedAt: new Date(endAt).toISOString(),
        reason: fragmentReason,
        durationMs: (fragmentPrefix && fragmentPrefix.durationMs || 0) + activeDuration,
        teamDamage: teamDamage - fragmentBase.teamDamage,
        players: {
          damage: mapDelta(playerDamage, fragmentBase.damage),
          sources: nestedDelta(playerDamageSources, fragmentBase.sources),
          healing: mapDelta(playerHealing, fragmentBase.healing),
          taken: mapDelta(playerTaken, fragmentBase.taken),
          takenSources: nestedDelta(
            playerTakenSources,
            fragmentBase.takenSources
          ),
          kills: mapDelta(playerKills, fragmentBase.kills),
          accuracy: accuracyDelta(playerAccuracy, fragmentBase.accuracy)
        }
      };
    }
    function beginFragment(reason) {
      fragmentStart = Date.now();
      fragmentReason = reason || "断线续传";
      fragmentBase = {
        teamDamage,
        damage: mapObject(playerDamage),
        sources: nestedMapObject(playerDamageSources),
        healing: mapObject(playerHealing),
        taken: mapObject(playerTaken),
        takenSources: nestedMapObject(playerTakenSources),
        kills: mapObject(playerKills),
        accuracy: accuracyObject(playerAccuracy)
      };
      fragmentPrefix = null;
    }
    function closeFragment(reason) {
      const part = currentFragment();
      if (part) {
        if (reason) part.endReason = reason;
        fragments.push(part);
      }
      fragmentStart = null;
      fragmentBase = null;
      fragmentPrefix = null;
    }
    function tickBuckets(ts) {
      const steps = Math.floor((ts - bucketTs) / BUCKET_MS);
      if (steps <= 0) return;
      if (steps >= BUCKETS) {
        dmgBuckets.fill(0);
        bossBuckets.fill(_isBoss);
        curBucket = (curBucket + steps) % BUCKETS;
        bucketTs += steps * BUCKET_MS;
        return;
      }
      for (let step = 0; step < steps; step += 1) {
        curBucket = (curBucket + 1) % BUCKETS;
        dmgBuckets[curBucket] = 0;
        bossBuckets[curBucket] = _isBoss;
      }
      bucketTs += steps * BUCKET_MS;
    }
    function tickFullBuckets(ts) {
      const steps = Math.floor((ts - fullBucketTs) / BUCKET_MS);
      if (steps <= 0) return;
      const appendCount = Math.min(
        steps,
        FULL_MAX_BUCKETS - fullDmgBuckets.length
      );
      if (appendCount > 0) {
        fullDmgBuckets.push(...new Array(appendCount).fill(0));
        fullBossBuckets.push(...new Array(appendCount).fill(_isBoss));
      }
      fullBucketTs += steps * BUCKET_MS;
    }
    return {
      reset(nextMeta = {}) {
        startTs = performance.now();
        elapsedOffset = 0;
        teamDamage = 0;
        frozenAt = null;
        meta = { ...nextMeta };
        playerDamage.clear();
        playerDamageSources.clear();
        playerHealing.clear();
        playerKills.clear();
        playerTaken.clear();
        playerTakenSources.clear();
        playerAccuracy.clear();
        dmgBuckets.fill(0);
        bossBuckets.fill(false);
        curBucket = 0;
        bucketTs = performance.now();
        _isBoss = false;
        fullDmgBuckets.length = 0;
        fullBossBuckets.length = 0;
        fullDmgBuckets.push(0);
        fullBossBuckets.push(false);
        fullBucketTs = performance.now();
        fragments = [];
        fragmentStart = Date.now();
        fragmentReason = "开始战斗";
        fragmentBase = {
          teamDamage: 0,
          damage: {},
          sources: {},
          healing: {},
          taken: {},
          takenSources: {},
          kills: {},
          accuracy: {}
        };
        fragmentPrefix = null;
      },
      // Fige durée ET donc DPS au moment de l'appel — les dégâts/kills restent
      // affichés mais n'évoluent plus, le temps écoulé ne progresse plus non
      // plus. Idempotent (rappeler ne change rien si déjà figé).
      freeze(reason = "战斗结束") {
        if (frozenAt === null) {
          frozenAt = elapsed();
          closeFragment(reason);
        }
      },
      unfreeze(reason = "断线续传") {
        if (frozenAt !== null) {
          elapsedOffset = frozenAt;
          startTs = performance.now();
          frozenAt = null;
          beginFragment(reason);
        }
      },
      pause(reason = "连接中断") {
        this.freeze(reason);
      },
      resume(reason = "断线续传") {
        this.unfreeze(reason);
      },
      // 同日试炼升 tier 不是断线,不新增片段。若层间结束信号曾短暂冻结,
      // 将刚关闭的母片段重新接回,并排除等待下一层的空档时间。
      resumeTrialTier(reason = "进入下一层") {
        if (frozenAt === null) return;
        elapsedOffset = frozenAt;
        startTs = performance.now();
        frozenAt = null;
        const previous = fragments.length ? fragments.pop() : null;
        if (!previous) {
          beginFragment(reason);
          return;
        }
        const deltas = previous.players || {};
        const baseFrom = (map, delta) => Object.fromEntries(
          Array.from(map, ([name, value]) => [
            name,
            value - (Number(delta && delta[name]) || 0)
          ])
        );
        const sourceBase = nestedMapObject(playerDamageSources);
        Object.entries(deltas.sources || {}).forEach(
          ([name, sources]) => Object.entries(sources || {}).forEach(([source, value]) => {
            if (sourceBase[name])
              sourceBase[name][source] = (Number(sourceBase[name][source]) || 0) - (Number(value) || 0);
          })
        );
        const takenSourceBase = nestedMapObject(playerTakenSources);
        Object.entries(deltas.takenSources || {}).forEach(
          ([name, sources]) => Object.entries(sources || {}).forEach(([source, value]) => {
            if (takenSourceBase[name])
              takenSourceBase[name][source] = (Number(takenSourceBase[name][source]) || 0) - (Number(value) || 0);
          })
        );
        fragmentStart = Date.now();
        fragmentReason = previous.reason || "开始战斗";
        fragmentBase = {
          teamDamage: teamDamage - (Number(previous.teamDamage) || 0),
          damage: baseFrom(playerDamage, deltas.damage),
          sources: sourceBase,
          healing: baseFrom(playerHealing, deltas.healing),
          taken: baseFrom(playerTaken, deltas.taken),
          takenSources: takenSourceBase,
          kills: baseFrom(playerKills, deltas.kills),
          accuracy: accuracyBaseFromDelta(playerAccuracy, deltas.accuracy)
        };
        fragmentPrefix = {
          startedAt: previous.startedAt,
          durationMs: Number(previous.durationMs) || 0
        };
      },
      splitFragment(reason = "进入下一关") {
        if (fragmentStart !== null) {
          closeFragment(reason);
          beginFragment(reason);
        }
      },
      isFrozen() {
        return frozenAt !== null;
      },
      setMeta(v) {
        meta = { ...meta, ...v };
      },
      getMeta() {
        return { ...meta };
      },
      serialize() {
        const open = currentFragment();
        return {
          schemaVersion: 2,
          meta: { ...meta },
          savedAt: (/* @__PURE__ */ new Date()).toISOString(),
          durationMs: Math.round(elapsed() * 1e3),
          teamDamage,
          players: {
            damage: mapObject(playerDamage),
            sources: nestedMapObject(playerDamageSources),
            healing: mapObject(playerHealing),
            taken: mapObject(playerTaken),
            takenSources: nestedMapObject(playerTakenSources),
            kills: mapObject(playerKills),
            accuracy: accuracyObject(playerAccuracy)
          },
          classes: Object.fromEntries(
            this.getAllPlayerNames().map((n) => [n, ClassSystem.classFor(n)])
          ),
          fragments: open ? [...fragments, open] : [...fragments],
          isBoss: _isBoss,
          frozen: frozenAt !== null,
          graph: { damage: [...fullDmgBuckets], boss: [...fullBossBuckets] }
        };
      },
      restore(s) {
        if (!s || s.schemaVersion !== 2) {
          throw new Error(
            Settings.getLanguage() === "en" ? "Unsupported combat cache format" : "不支持的战斗缓存格式"
          );
        }
        meta = { ...s.meta || {} };
        teamDamage = Number(s.teamDamage) || 0;
        elapsedOffset = (Number(s.durationMs) || 0) / 1e3;
        startTs = performance.now();
        frozenAt = elapsedOffset;
        restoreMap(playerDamage, s.players && s.players.damage);
        restoreMap(playerHealing, s.players && s.players.healing);
        restoreNestedMap(playerDamageSources, s.players && s.players.sources);
        restoreMap(playerTaken, s.players && s.players.taken);
        restoreNestedMap(playerTakenSources, s.players && s.players.takenSources);
        restoreMap(playerKills, s.players && s.players.kills);
        restoreAccuracy(playerAccuracy, s.players && s.players.accuracy);
        ClassSystem.applyClasses(s.classes);
        fragments = Array.isArray(s.fragments) ? s.fragments : [];
        fragmentStart = null;
        fragmentBase = null;
        fragmentPrefix = null;
        _isBoss = !!s.isBoss;
        fullDmgBuckets.length = 0;
        fullBossBuckets.length = 0;
        (s.graph && s.graph.damage || [0]).slice(-FULL_MAX_BUCKETS).forEach((v) => fullDmgBuckets.push(Number(v) || 0));
        (s.graph && s.graph.boss || [false]).slice(-FULL_MAX_BUCKETS).forEach((v) => fullBossBuckets.push(!!v));
        if (!fullDmgBuckets.length) fullDmgBuckets.push(0);
        while (fullBossBuckets.length < fullDmgBuckets.length)
          fullBossBuckets.push(false);
        dmgBuckets.fill(0);
        bossBuckets.fill(false);
        const td = fullDmgBuckets.slice(-BUCKETS), tb = fullBossBuckets.slice(-BUCKETS);
        td.forEach((v, i) => {
          dmgBuckets[i] = v;
          bossBuckets[i] = !!tb[i];
        });
        curBucket = BUCKETS - 1;
        bucketTs = fullBucketTs = performance.now();
      },
      getFragments() {
        const open = currentFragment();
        return open ? [...fragments, open] : [...fragments];
      },
      setBoss(v) {
        _isBoss = v;
        bossBuckets[curBucket] = v;
        if (fullBossBuckets.length)
          fullBossBuckets[fullBossBuckets.length - 1] = v;
      },
      addTeamDamage(a, ts) {
        teamDamage += a;
        tickBuckets(ts);
        dmgBuckets[curBucket] += a;
        bossBuckets[curBucket] = _isBoss;
        tickFullBuckets(ts);
        if (fullDmgBuckets.length) fullDmgBuckets[fullDmgBuckets.length - 1] += a;
      },
      addPlayerDamage(n, a, source = "unknown") {
        playerDamage.set(n, (playerDamage.get(n) || 0) + a);
        const key = DamageSources.normalize(source), sources = playerDamageSources.get(n) || /* @__PURE__ */ new Map();
        sources.set(key, (sources.get(key) || 0) + a);
        playerDamageSources.set(n, sources);
      },
      addPlayerHealing(n, a) {
        playerHealing.set(n, (playerHealing.get(n) || 0) + a);
      },
      addPlayerTaken(n, a, source = "unknown") {
        playerTaken.set(n, (playerTaken.get(n) || 0) + a);
        const key = String(source || "unknown"), sources = playerTakenSources.get(n) || /* @__PURE__ */ new Map();
        sources.set(key, (sources.get(key) || 0) + a);
        playerTakenSources.set(n, sources);
      },
      addPlayerKill(n) {
        playerKills.set(n, (playerKills.get(n) || 0) + 1);
      },
      addPlayerAccuracy(n, hit, targets = []) {
        if (!n) return;
        const value = playerAccuracy.get(n) || {
          attempts: 0,
          hits: 0,
          monsters: /* @__PURE__ */ new Map()
        };
        value.attempts++;
        if (hit) value.hits++;
        const seen = /* @__PURE__ */ new Set();
        (Array.isArray(targets) ? targets : []).forEach((target) => {
          const monsterName2 = String(target && target.monsterName || ""), monsterHrid = String(target && target.monsterHrid || ""), key = monsterHrid || "name:" + monsterName2;
          if (!key || key === "name:" || seen.has(key)) return;
          seen.add(key);
          const monster = value.monsters.get(key) || {
            monsterName: monsterName2,
            monsterHrid,
            attempts: 0,
            hits: 0
          };
          monster.monsterName = monster.monsterName || monsterName2;
          monster.monsterHrid = monster.monsterHrid || monsterHrid;
          monster.attempts++;
          if (target.hit) monster.hits++;
          value.monsters.set(key, monster);
        });
        playerAccuracy.set(n, value);
      },
      // Fusionne les stats d'un ancien label (ex: fallback "Joueur6") vers le
      // nom réel confirmé, au lieu de laisser deux lignes distinctes pour la
      // même personne. Utilisé quand la résolution de noms du Trial de guilde
      // identifie enfin le vrai pseudo d'un slot déjà en cours de tracking.
      renamePlayer(oldName, newName) {
        if (!oldName || !newName || oldName === newName) return;
        const merge = (map) => {
          if (!map.has(oldName)) return;
          map.set(newName, (map.get(newName) || 0) + map.get(oldName));
          map.delete(oldName);
        };
        merge(playerDamage);
        merge(playerHealing);
        merge(playerKills);
        merge(playerTaken);
        if (playerDamageSources.has(oldName)) {
          const target = playerDamageSources.get(newName) || /* @__PURE__ */ new Map();
          playerDamageSources.get(oldName).forEach(
            (value, source) => target.set(source, (target.get(source) || 0) + value)
          );
          playerDamageSources.set(newName, target);
          playerDamageSources.delete(oldName);
        }
        if (playerTakenSources.has(oldName)) {
          const target = playerTakenSources.get(newName) || /* @__PURE__ */ new Map();
          playerTakenSources.get(oldName).forEach(
            (value, source) => target.set(source, (target.get(source) || 0) + value)
          );
          playerTakenSources.set(newName, target);
          playerTakenSources.delete(oldName);
        }
        if (playerAccuracy.has(oldName)) {
          const source = playerAccuracy.get(oldName), target = playerAccuracy.get(newName) || {
            attempts: 0,
            hits: 0,
            monsters: /* @__PURE__ */ new Map()
          };
          target.attempts += Number(source.attempts) || 0;
          target.hits += Number(source.hits) || 0;
          source.monsters.forEach((monster, key) => {
            const current = target.monsters.get(key) || {
              monsterName: monster.monsterName || "",
              monsterHrid: monster.monsterHrid || "",
              attempts: 0,
              hits: 0
            };
            current.attempts += Number(monster.attempts) || 0;
            current.hits += Number(monster.hits) || 0;
            target.monsters.set(key, current);
          });
          playerAccuracy.set(newName, target);
          playerAccuracy.delete(oldName);
        }
      },
      getTeamDps() {
        const e = elapsed();
        return e < 1 ? 0 : teamDamage / e;
      },
      getTeamDamage() {
        return teamDamage;
      },
      getTeamKills() {
        let t21 = 0;
        playerKills.forEach((v) => t21 += v);
        return t21;
      },
      getPlayerDps(n) {
        const e = elapsed();
        return e < 1 ? 0 : (playerDamage.get(n) || 0) / e;
      },
      getPlayerDamage(n) {
        return playerDamage.get(n) || 0;
      },
      getPlayerDamageSources(n) {
        return Object.fromEntries(playerDamageSources.get(n) || []);
      },
      getPlayerHps(n) {
        const e = elapsed();
        return e < 1 ? 0 : (playerHealing.get(n) || 0) / e;
      },
      getPlayerHealing(n) {
        return playerHealing.get(n) || 0;
      },
      getPlayerTaken(n) {
        return playerTaken.get(n) || 0;
      },
      getPlayerTakenSources(n) {
        return Object.fromEntries(playerTakenSources.get(n) || []);
      },
      getPlayerTakenPs(n) {
        const e = elapsed();
        return e < 1 ? 0 : (playerTaken.get(n) || 0) / e;
      },
      getPlayerKills(n) {
        return playerKills.get(n) || 0;
      },
      getPlayerAccuracy(n) {
        const value = playerAccuracy.get(n);
        if (!value) return null;
        return {
          attempts: Number(value.attempts) || 0,
          hits: Number(value.hits) || 0,
          monsters: Object.fromEntries(value.monsters || [])
        };
      },
      getElapsedSeconds() {
        return elapsed();
      },
      getAllPlayerNames() {
        return Array.from(
          /* @__PURE__ */ new Set([
            ...playerDamage.keys(),
            ...playerKills.keys(),
            ...playerHealing.keys(),
            ...playerTaken.keys(),
            ...playerAccuracy.keys()
          ])
        );
      },
      // Avance les buckets au temps actuel — appelé à chaque render tick (1s).
      // Sans ça, curBucket ne s'avance qu'à la réception de dégâts. En idle (entre
      // deux vagues), le bucket courant garde sa valeur accumulée et s'affiche comme
      // le point "now" pendant toute la pause → pic artificiel dans le graphe.
      advanceBuckets() {
        tickBuckets(performance.now());
        tickFullBuckets(performance.now());
      },
      getGraphPoints() {
        const pts = [];
        for (let i = 1; i <= BUCKETS; i++) {
          const idx = (curBucket + i) % BUCKETS;
          pts.push({
            dps: dmgBuckets[idx] / (BUCKET_MS / 1e3),
            isBoss: bossBuckets[idx]
          });
        }
        return pts;
      },
      // Historique COMPLET (non plafonné à 5 min) pour le graphe du Recount —
      // grandit sur toute la durée du Trial au lieu de perdre les points les
      // plus anciens comme le fait le buffer circulaire du panneau principal.
      getFullGraphPoints() {
        const pts = [];
        for (let i = 0; i < fullDmgBuckets.length; i++) {
          pts.push({
            dps: fullDmgBuckets[i] / (BUCKET_MS / 1e3),
            isBoss: fullBossBuckets[i]
          });
        }
        return pts;
      }
    };
  })();
  var Diagnostics = /* @__PURE__ */ (() => {
    let nominal = 0, coldPlayer = 0, coldMonsterAmbig = 0, coldMonsterAmbigDmg = 0;
    let collision = 0, collisionSingleHit = 0;
    let orphan = 0, orphanDmg = 0;
    const MAX = 5;
    let orphLog = 0;
    const w = (cnt, msg) => {
      if (cnt < MAX) {
        console.warn("[KikiMeter] " + msg);
        return cnt + 1;
      }
      return cnt;
    };
    return {
      recordNominal() {
        nominal++;
      },
      recordColdPlayer() {
        coldPlayer++;
      },
      recordColdMonsterAmbig(a) {
        coldMonsterAmbig++;
        coldMonsterAmbigDmg += a;
      },
      recordCollision(n) {
        collision++;
      },
      recordCollisionSingleHit(n, a) {
        collisionSingleHit++;
      },
      recordOrphan(a) {
        orphan++;
        orphanDmg += a;
        orphLog = w(orphLog, "未归属伤害 " + a + ",序号 " + orphan);
      },
      summary() {
        return {
          nominal,
          coldPlayer,
          coldMonsterAmbig,
          coldMonsterAmbigDmg,
          collision,
          collisionSingleHit,
          orphan,
          orphanDmg
        };
      }
    };
  })();
  var Capture = /* @__PURE__ */ (() => {
    let active = false, log = [], counts = {};
    return {
      start() {
        active = true;
        log = [];
        counts = {};
        console.info("[KikiMeter] 已开始抓取战斗消息。");
      },
      stop() {
        active = false;
        console.info("[KikiMeter] 已停止抓取,共 " + log.length + " 条消息。");
      },
      record(type, payload) {
        if (!active) return;
        counts[type] = (counts[type] || 0) + 1;
        log.push({ t: Date.now(), payload });
      },
      download(fn) {
        const blob = new Blob(
          [JSON.stringify({ typeCounts: counts, combatLog: log })],
          { type: "application/json" }
        );
        const url = URL.createObjectURL(blob);
        const a = Object.assign(document.createElement("a"), {
          href: url,
          download: fn || "mwi-capture-" + Date.now() + ".json"
        });
        document.body.appendChild(a);
        a.click();
        a.remove();
        URL.revokeObjectURL(url);
      },
      size() {
        return log.length;
      }
    };
  })();

  // src/features/dps/40-socket-parser.js
  var SocketHook = (() => {
    const bus = new EventTarget();
    let monstersHP = [];
    let monstersAlive = [];
    let monsterNames = [], monsterHrids = [];
    let monstersAtkCounter = [], monsterCurrentAction = [], monsterKnownDotAbilities = {}, monsterDotUntil = {};
    let playersMP = [];
    let playersAtkCounter = [];
    let playersDmgCounter = [];
    let playersHP = [];
    let playerKnownDotAbilities = {};
    let playerDotUntil = {};
    let playerDotCasts = [];
    let playerKnownReflectionAbilities = {};
    let playerReflectUntil = {};
    let playerReflectSource = {};
    let currentAction = [];
    const keyToName = /* @__PURE__ */ new Map();
    let haveBattle = false;
    let lastCombatStartTime = null;
    let currentCharacterId2 = null;
    let currentCombatKey = null;
    let testNow = null;
    function clockNow() {
      return testNow === null ? performance.now() : testNow;
    }
    let guildSlotNames = /* @__PURE__ */ new Map();
    const guildSlotLocked = /* @__PURE__ */ new Set();
    let guildMonstersHP = {};
    let guildMonstersMHP = {};
    let guildMonstersDmgCounter = {};
    let guildPlayersHP = {};
    let guildPlayersMP = {};
    let guildPlayersAtkCounter = {};
    let guildPlayersDmgCounter = {};
    let guildCurrentAction = {};
    let guildReflectUntil = {};
    let guildReflectSource = {};
    let guildKnownDotAbilities = {};
    let guildDotUntil = {};
    let guildKnownReflectionAbilities = {};
    let guildMonstersAtkCounter = {};
    let guildMonsterCurrentAction = {};
    let guildMonsterNames = {}, guildMonsterHrids = {}, guildMonsterKnownDotAbilities = {}, guildMonsterDotUntil = {};
    let currentGuildTier = null;
    let currentGuildStageSignature = "";
    let isGuildBattle = false;
    let isInLabyrinth = false;
    let guildMaxSlot = 0;
    let guildCombatWasActive = false;
    let guildTrialEnded = false;
    let lastGuildSnapshotSignature = "";
    const GUILD_NAME_NOISE = /* @__PURE__ */ new Set([
      "milking",
      "foraging",
      "woodcutting",
      "cheesesmithing",
      "crafting",
      "tailoring",
      "cooking",
      "brewing",
      "alchemy",
      "enhancing",
      "combat",
      "stamina",
      "intelligence",
      "attack",
      "defense",
      "melee",
      "ranged",
      "magic",
      "stealth",
      "power",
      "marketplace",
      "tasks",
      "task",
      "labyrinth",
      "shop",
      "loot",
      "abilities",
      "equipment",
      "inventory",
      "house",
      "loadouts",
      "statistics",
      "buildings",
      "overview",
      "members",
      "trials",
      "application",
      "guild",
      "overview",
      "dispatch",
      "findparty",
      "myparty",
      "combatzones"
    ]);
    function looksLikeNoise(t21) {
      const low = t21.toLowerCase();
      if (GUILD_NAME_NOISE.has(low)) return true;
      if (/^lv\.?\d+$/i.test(t21)) return true;
      if (/^\d+%?$/.test(t21)) return true;
      if (/^[\d.,]+[km]?$/i.test(t21)) return true;
      return false;
    }
    function resolveGuildNames(expectedSlots) {
      const SCOPED_SELECTORS = [
        '[class*="MiniUnit_name"]',
        '[class*="CombatUnit_name"]',
        ".GuildBattle__members .Character__name",
        '[class*="GuildBattle"] [class*="name"]',
        '[class*="guildBattle"] [class*="name"]',
        '[class*="GuildMember"] [class*="name"]',
        '[class*="TrialBattle"] [class*="name"]'
      ];
      let candidates = [];
      for (const sel of SCOPED_SELECTORS) {
        try {
          candidates = [...document.querySelectorAll(sel)];
        } catch (e) {
          candidates = [];
        }
        if (candidates.length > 0) break;
      }
      const names = candidates.map((el2) => el2.textContent.trim()).filter((t21) => t21 && !looksLikeNoise(t21) && !/^trial\s/i.test(t21));
      const localName = [...keyToName.values()][0];
      const localInList = localName && names.includes(localName);
      const offset = !localName || !localInList ? 1 : 0;
      const resolved = /* @__PURE__ */ new Map();
      if (offset === 1 && localName) resolved.set("0", localName);
      names.slice(0, expectedSlots ? expectedSlots - offset : names.length).forEach((t21, i) => {
        resolved.set(String(i + offset), t21);
      });
      for (const [slot, name] of resolved) {
        if (guildSlotLocked.has(slot)) continue;
        const oldLabel = guildSlotLabel(slot);
        guildSlotNames.set(slot, name);
        guildSlotLocked.add(slot);
        if (oldLabel !== name) {
          bus.dispatchEvent(
            new CustomEvent("guildSlotRenamed", {
              detail: { oldName: oldLabel, newName: name }
            })
          );
        }
      }
      if (guildSlotNames.size === 0 && Settings.getDebugMode()) {
        console.warn(
          "[KikiMeter][Guild] 玩家姓名解析失败,页面选择器可能已经变化。请在试炼中运行 window.__MWI_DPS.scanGuildNames() 查看诊断。"
        );
      }
    }
    function isOwnUI(el2) {
      return !!(el2.closest("#kikimeter-panel") || el2.closest('[data-kikimeter="true"]'));
    }
    function scanGuildNamesByLocalName() {
      const localName = [...keyToName.values()][0];
      if (!localName) {
        console.warn(
          "[KikiMeter] 尚不知道本地玩家姓名。请在公会试炼进行中再次运行此命令。"
        );
        return [];
      }
      console.log(`[KikiMeter] 正在精确搜索文本 "${localName}" (页面 DOM)…`);
      const matches = [];
      document.querySelectorAll("*").forEach((el2) => {
        if (isOwnUI(el2)) return;
        if (el2.children.length <= 1 && el2.textContent.trim() === localName) {
          matches.push(el2);
        }
      });
      console.log(`[KikiMeter] ${matches.length} 个完全匹配项。`);
      console.log("=====================================================");
      matches.forEach((el2, i) => {
        console.log(`--- 匹配项 #${i} ---`);
        let cur = el2, depth = 0;
        while (cur && depth < 6) {
          const cls = (cur.className || "") + "" || "(无类名)";
          const siblingsTexts = cur.parentElement ? [...cur.parentElement.children].slice(0, 8).map((s) => s.textContent.trim().slice(0, 16)) : [];
          console.log(
            `  层级 ${depth} | <${cur.tagName}> 类名="${cls}" | 子元素=${cur.children.length} | 同级元素=${cur.parentElement ? cur.parentElement.children.length : 0} | 同级示例: [${siblingsTexts.join(" ¦ ")}]`
          );
          cur = cur.parentElement;
          depth++;
        }
        console.log("  ---");
      });
      console.log("=====================================================");
      return matches;
    }
    function scanGuildNames() {
      const target = guildMaxSlot || 35;
      const lo = Math.max(2, target - 5), hi = target + 10;
      const out = [];
      document.querySelectorAll("div, ul, section").forEach((el2) => {
        if (isOwnUI(el2)) return;
        const n = el2.children.length;
        if (n >= lo && n <= hi) {
          const texts = [...el2.children].slice(0, 6).map((c) => c.textContent.trim().slice(0, 20));
          if (texts.some((t21) => t21.length > 0)) {
            out.push({
              selector: (el2.className || el2.tagName) + "",
              tag: el2.tagName,
              childCount: n,
              sample: texts
            });
          }
        }
      });
      console.log(
        `[KikiMeter] 预计玩家数:约 ${target} 名;${out.length} 个候选容器:`
      );
      console.log("=====================================================");
      out.forEach((c, i) => {
        console.log(
          `#${i} | 类名="${c.selector}" | 子元素=${c.childCount} | 示例: [${c.sample.join(" ¦ ")}]`
        );
      });
      console.log("=====================================================");
      if (out.length === 0) {
        console.warn(
          "[KikiMeter] 没有找到子元素数量完全匹配的容器。请运行 window.__MWI_DPS.scanGuildNamesLoose() 执行宽松搜索。"
        );
      }
      return out;
    }
    function scanGuildNamesLoose() {
      const out = [];
      document.querySelectorAll("div, ul, section").forEach((el2) => {
        if (isOwnUI(el2)) return;
        const n = el2.children.length;
        if (n >= 10 && n <= 200) {
          let nameLikeCount = 0;
          const texts = [];
          el2.querySelectorAll(":scope > * ").forEach((c) => {
            const t21 = c.textContent.trim();
            if (t21.length >= 2 && t21.length <= 20 && !looksLikeNoise(t21)) {
              nameLikeCount++;
              texts.push(t21.slice(0, 20));
            }
          });
          if (nameLikeCount >= 10) {
            out.push({
              selector: (el2.className || el2.tagName) + "",
              tag: el2.tagName,
              childCount: n,
              nameLikeCount,
              sample: texts.slice(0, 8)
            });
          }
        }
      });
      out.sort((a, b) => b.nameLikeCount - a.nameLikeCount);
      const top = out.slice(0, 15);
      console.log(
        `[KikiMeter] 宽松搜索:${out.length} 个候选容器,显示前 15 个:`
      );
      console.log("=====================================================");
      top.forEach((c, i) => {
        console.log(
          `#${i} | 类名="${c.selector}" | 子元素=${c.childCount} | 疑似姓名=${c.nameLikeCount} | 示例: [${c.sample.join(" ¦ ")}]`
        );
      });
      console.log("=====================================================");
      return out;
    }
    function scanGuildNamesAttrs() {
      const out = [];
      document.querySelectorAll("[title], [aria-label], img[alt]").forEach((el2) => {
        const v = el2.getAttribute("title") || el2.getAttribute("aria-label") || el2.getAttribute("alt");
        if (v && v.length >= 2 && v.length <= 20 && !looksLikeNoise(v)) {
          out.push({
            tag: el2.tagName,
            attr: el2.getAttribute("title") ? "title" : el2.getAttribute("aria-label") ? "aria-label" : "alt",
            value: v,
            class: (el2.className || "") + ""
          });
        }
      });
      console.log(`[KikiMeter] ${out.length} 个候选属性:`);
      console.table(out.slice(0, 60));
      return out;
    }
    function guildSlotLabel(k) {
      return guildSlotNames.get(k) || (k === "0" ? [...keyToName.values()][0] || "Slot0" : "Joueur" + (+k + 1));
    }
    function fallbackGuildDetail(payload = {}, source = "guild_fallback") {
      const fallbackDay = CombatIdentity.dayStamp(/* @__PURE__ */ new Date());
      const rawKey = payload.combatStartTime || payload.guildBattleId || payload.battleId || payload.combatId || "guild-fallback-" + fallbackDay;
      return {
        combatKey: String(rawKey),
        stageId: String(rawKey),
        characterId: currentCharacterId2 || "unknown",
        classes: {},
        fallback: true,
        source
      };
    }
    function finiteCounter(value) {
      const number3 = Number(value);
      return value !== void 0 && value !== null && Number.isFinite(number3) ? number3 : void 0;
    }
    function playerAttackCounter(unit = {}) {
      return finiteCounter(
        unit.attackAttemptCounter !== void 0 ? unit.attackAttemptCounter : unit.atkCounter
      );
    }
    function monsterDamageCounter(unit = {}) {
      return finiteCounter(
        unit.damageSplatCounter !== void 0 ? unit.damageSplatCounter : unit.dmgCounter
      );
    }
    const REFLECTION_TYPES = /* @__PURE__ */ new Set([
      "/buff_types/physical_thorns",
      "/buff_types/elemental_thorns",
      "/buff_types/retaliation"
    ]);
    const reflectionAbilityRules = /* @__PURE__ */ new Map([
      ["/abilities/spike_shell", 3e4],
      ["/abilities/retribution", 3e4]
    ]);
    const abilityDamageRules = /* @__PURE__ */ new Map([
      [
        "/abilities/firestorm",
        { direct: true, dotDuration: 6e3, dotInterval: 3e3 }
      ],
      ["/abilities/maim", { direct: true, dotDuration: 9e3, dotInterval: 3e3 }]
    ]);
    const DOT_DEFAULT_INTERVAL_MS = 3e3;
    const DOT_MATCH_TOLERANCE_MS = 500;
    const DOT_TARGET_BIND_WINDOW_MS = 2e3;
    const DOT_CAST_RETENTION_MS = 3e4;
    const reflectionBuffSources = /* @__PURE__ */ new Map();
    const reflectionTypeSources = /* @__PURE__ */ new Map();
    const reflectionItemHrids = /* @__PURE__ */ new Set();
    function normalizedReflectType(value) {
      const text = String(value || "").toLowerCase();
      if (text.includes("physical_thorns")) return "/buff_types/physical_thorns";
      if (text.includes("elemental_thorns"))
        return "/buff_types/elemental_thorns";
      if (text.includes("retaliation")) return "/buff_types/retaliation";
      return "";
    }
    function containsReflection(value, depth = 0) {
      if (depth > 8 || value === null || value === void 0) return false;
      if (typeof value === "string") return !!normalizedReflectType(value);
      if (typeof value !== "object") return false;
      return Object.entries(value).some(
        ([key, child]) => !!normalizedReflectType(key) || containsReflection(child, depth + 1)
      );
    }
    function durationToMs(value) {
      const number3 = Number(value);
      if (!Number.isFinite(number3) || number3 <= 0) return 0;
      if (number3 > 864e5) return number3 / 1e6;
      if (number3 < 1e3) return number3 * 1e3;
      return number3;
    }
    function reflectionDuration(detail) {
      const candidates = [];
      const walk = (value, depth = 0) => {
        if (depth > 8 || !value || typeof value !== "object") return;
        Object.entries(value).forEach(([key, child]) => {
          if (/duration/i.test(key) && typeof child === "number") {
            const ms = durationToMs(child);
            if (ms >= 100 && ms <= 36e5) candidates.push(ms);
          }
          if (child && typeof child === "object") walk(child, depth + 1);
        });
      };
      walk(detail);
      return candidates.length ? Math.max(...candidates) : 3e4;
    }
    function abilityEffects(detail = {}) {
      const list = detail.abilityEffects || detail.effects || detail.abilityDetail && detail.abilityDetail.abilityEffects || [];
      return Array.isArray(list) ? list : [];
    }
    function reflectionBuffs(value, depth = 0, out = []) {
      if (depth > 8 || !value || typeof value !== "object") return out;
      if (normalizedReflectType(value.typeHrid || value.uniqueHrid))
        out.push(value);
      Object.values(value).forEach((child) => {
        if (child && typeof child === "object")
          reflectionBuffs(child, depth + 1, out);
      });
      return out;
    }
    function cacheAbilityRule(hrid, detail = {}) {
      const effects = abilityEffects(detail);
      const damageEffects = effects.filter(
        (effect) => String(effect && effect.effectType || "").includes(
          "/ability_effect_types/damage"
        )
      );
      const dotDuration = damageEffects.reduce((max, effect) => {
        if (!(Number(effect.damageOverTimeRatio) > 0) || !(Number(effect.damageOverTimeDuration) > 0))
          return max;
        return Math.max(max, durationToMs(effect.damageOverTimeDuration));
      }, 0);
      const dotInterval = damageEffects.reduce((minimum, effect) => {
        if (!(Number(effect.damageOverTimeRatio) > 0)) return minimum;
        const raw = effect.damageOverTimeInterval ?? effect.damageOverTimeTickInterval ?? effect.tickInterval;
        const interval = durationToMs(raw);
        if (!(interval > 0)) return minimum;
        return minimum > 0 ? Math.min(minimum, interval) : interval;
      }, 0);
      const targetTypes = [
        ...new Set(
          damageEffects.map((effect) => String(effect.targetType || "")).filter(Boolean)
        )
      ];
      if (effects.length || damageEffects.length || dotDuration) {
        abilityDamageRules.set(hrid, {
          direct: damageEffects.length > 0,
          dotDuration,
          dotInterval: dotDuration ? dotInterval || DOT_DEFAULT_INTERVAL_MS : 0,
          targetTypes
        });
      }
      const reflectBuffs = reflectionBuffs(detail);
      if (reflectBuffs.length) {
        reflectionAbilityRules.set(hrid, reflectionDuration(detail));
        reflectBuffs.forEach((buff) => {
          const unique = String(buff.uniqueHrid || "");
          if (unique) reflectionBuffSources.set(unique, hrid);
          const type = normalizedReflectType(buff.typeHrid || buff.uniqueHrid);
          if (type) {
            if (!reflectionTypeSources.has(type))
              reflectionTypeSources.set(type, /* @__PURE__ */ new Set());
            reflectionTypeSources.get(type).add(hrid);
          }
        });
      }
    }
    function cacheReflectionDefinitions(payload = {}) {
      const sources = [
        payload,
        payload.data,
        payload.initGameData,
        payload.init_game_data,
        payload.initClientData,
        payload.init_client_data,
        payload.clientData,
        payload.client_data
      ].filter((value) => value && typeof value === "object");
      for (const source of sources) {
        const map = source.abilityDetailMap || source.gameAbilityDetailMap;
        if (map && typeof map === "object") {
          const entries = map instanceof Map ? [...map.entries()] : Object.entries(map);
          for (const [key, detail] of entries) {
            const hrid = String(
              detail && detail.hrid || detail && detail.abilityHrid || key
            );
            if (hrid) cacheAbilityRule(hrid, detail || {});
          }
        }
        const itemMap = source.itemDetailMap || source.gameItemDetailMap;
        if (itemMap && typeof itemMap === "object") {
          const entries = itemMap instanceof Map ? [...itemMap.entries()] : Object.entries(itemMap);
          for (const [key, detail] of entries) {
            if (!containsReflection(detail)) continue;
            const hrid = String(
              detail && detail.hrid || detail && detail.itemHrid || key
            );
            if (hrid) reflectionItemHrids.add(hrid);
          }
        }
      }
    }
    function hasReflectionDefinitions(payload = {}) {
      const sources = [
        payload,
        payload.data,
        payload.initGameData,
        payload.init_game_data,
        payload.initClientData,
        payload.init_client_data,
        payload.clientData,
        payload.client_data
      ].filter((value) => value && typeof value === "object");
      return sources.some(
        (source) => source.abilityDetailMap || source.gameAbilityDetailMap || source.itemDetailMap || source.gameItemDetailMap
      );
    }
    function abilityHridsFrom(value, depth = 0, out = /* @__PURE__ */ new Set()) {
      if (depth > 7 || value === null || value === void 0) return out;
      if (typeof value === "string") {
        if (value.startsWith("/abilities/")) out.add(value);
        return out;
      }
      if (typeof value !== "object") return out;
      Object.entries(value).forEach(([key, child]) => {
        if (typeof child === "string" && child.startsWith("/abilities/"))
          out.add(child);
        else if (child && typeof child === "object")
          abilityHridsFrom(child, depth + 1, out);
      });
      return out;
    }
    function ensureSet(container, key) {
      return container[key] || (container[key] = /* @__PURE__ */ new Set());
    }
    function rememberAbilityKnowledge(dotContainer, reflectContainer, key, value) {
      abilityHridsFrom(value).forEach((hrid) => {
        const rule = abilityDamageRules.get(hrid);
        if (rule && rule.dotDuration > 0) ensureSet(dotContainer, key).add(hrid);
        if (reflectContainer && reflectionAbilityRules.has(hrid))
          ensureSet(reflectContainer, key).add(hrid);
      });
    }
    function actionDamageKind(action) {
      const value = String(action || "");
      if (value === "auto") return "direct";
      if (DamageSources.isSupport(value)) return "support";
      const rule = abilityDamageRules.get(value);
      if (!rule) return "unknown";
      return rule.direct ? "direct" : "support";
    }
    function targetsAllEnemies(action) {
      const rule = abilityDamageRules.get(String(action || ""));
      return !!(rule && Array.isArray(rule.targetTypes) && rule.targetTypes.some((type) => /allEnemies|all_enemies/i.test(type)));
    }
    function accuracyTargets(action, hits, aliveKeys, names, hrids) {
      const hitKeys = new Set((hits || []).map((hit) => String(hit.key))), living = [...new Set((aliveKeys || []).map(String))];
      let keys;
      if (targetsAllEnemies(action)) keys = living;
      else if (hitKeys.size) keys = [...hitKeys];
      else keys = living.length === 1 ? living : [];
      return keys.map((key) => {
        const monsterHrid = String(hrids && hrids[key] || ""), fallback = monsterHrid.split("/").pop().replace(/_/g, " "), monsterName2 = String(
          names && names[key] || fallback || "怪物" + (+key + 1)
        );
        return {
          monsterName: monsterName2,
          monsterHrid,
          hit: hitKeys.has(key)
        };
      });
    }
    function activateDot(activeContainer, dotContainer, key, abilityHrid, ts, timeline = null) {
      const hrid = String(abilityHrid || ""), rule = abilityDamageRules.get(hrid);
      if (!rule || !(rule.dotDuration > 0)) return;
      ensureSet(dotContainer, key).add(hrid);
      const active = activeContainer[key] || (activeContainer[key] = /* @__PURE__ */ new Map());
      active.set(hrid, Math.max(active.get(hrid) || 0, ts + rule.dotDuration));
      if (timeline) scheduleDotCast(timeline, key, hrid, ts);
    }
    function pruneDotCasts(timeline, ts) {
      for (let index = timeline.length - 1; index >= 0; index -= 1) {
        if (timeline[index].expiresAt + DOT_CAST_RETENTION_MS < ts) {
          timeline.splice(index, 1);
        }
      }
    }
    function dotTickSchedule(startedAt, rule) {
      const interval = rule.dotInterval || DOT_DEFAULT_INTERVAL_MS;
      const schedule = [];
      for (let elapsed = interval; elapsed <= rule.dotDuration; elapsed += interval) {
        schedule.push({ dueAt: startedAt + elapsed, consumed: false });
      }
      return schedule;
    }
    function scheduleDotCast(timeline, key, abilityHrid, ts) {
      const rule = abilityDamageRules.get(abilityHrid);
      if (!rule || !(rule.dotDuration > 0)) return;
      pruneDotCasts(timeline, ts);
      timeline.push({
        key: String(key),
        abilityHrid,
        startedAt: ts,
        landedAt: null,
        expiresAt: ts + rule.dotDuration,
        targets: /* @__PURE__ */ new Map()
      });
    }
    function bindDotTargets(timeline, key, abilityHrid, hits, ts) {
      if (!Array.isArray(hits) || !hits.length) return;
      pruneDotCasts(timeline, ts);
      const cast = timeline.slice().reverse().find(
        (candidate) => candidate.key === String(key) && candidate.abilityHrid === abilityHrid && ts - candidate.startedAt >= 0 && ts - candidate.startedAt <= DOT_TARGET_BIND_WINDOW_MS
      );
      if (!cast) return;
      if (cast.landedAt === null) {
        cast.landedAt = ts;
        const rule2 = abilityDamageRules.get(abilityHrid);
        cast.expiresAt = ts + (rule2?.dotDuration || 0);
      }
      const rule = abilityDamageRules.get(abilityHrid);
      if (!rule) return;
      for (const hit of hits) {
        const targetKey = String(hit.key);
        if (!cast.targets.has(targetKey)) {
          cast.targets.set(targetKey, dotTickSchedule(cast.landedAt, rule));
        }
      }
    }
    function scheduledDotCandidate(timeline, targetKey, ts, { key = null, abilityHrid = "" } = {}) {
      pruneDotCasts(timeline, ts);
      const candidates = [];
      for (const cast of timeline) {
        if (key !== null && cast.key !== String(key)) continue;
        if (abilityHrid && cast.abilityHrid !== abilityHrid) continue;
        for (const tick of cast.targets.get(String(targetKey)) || []) {
          if (tick.consumed) continue;
          const lateness = ts - tick.dueAt;
          if (lateness >= 0 && lateness <= DOT_MATCH_TOLERANCE_MS) {
            candidates.push({ cast, tick, distance: lateness });
          }
        }
      }
      candidates.sort(
        (left, right) => left.distance - right.distance || left.tick.dueAt - right.tick.dueAt || left.cast.startedAt - right.cast.startedAt
      );
      if (!candidates.length) return null;
      if (candidates.length > 1 && Math.abs(candidates[0].distance - candidates[1].distance) < 1) {
        return { ambiguous: true };
      }
      return candidates[0];
    }
    function consumeScheduledDotHits(timeline, hits, ts, { key = null, abilityHrid = "" } = {}) {
      const matched = [];
      const unmatched = [];
      let ambiguous = false;
      for (const hit of hits || []) {
        const candidate = scheduledDotCandidate(timeline, hit.key, ts, {
          key,
          abilityHrid
        });
        if (!candidate || candidate.ambiguous) {
          ambiguous ||= Boolean(candidate?.ambiguous);
          unmatched.push(hit);
          continue;
        }
        candidate.tick.consumed = true;
        matched.push({ hit, cast: candidate.cast });
      }
      return { matched, unmatched, ambiguous };
    }
    function scheduledDotDamage(timeline, hits, ts) {
      pruneDotCasts(timeline, ts);
      const targetKeys = new Set((hits || []).map((hit) => String(hit.key)));
      const hasPendingTargetSchedule = timeline.some(
        (cast) => [...targetKeys].some(
          (targetKey) => (cast.targets.get(targetKey) || []).some((tick) => !tick.consumed)
        )
      );
      const { matched, unmatched, ambiguous } = consumeScheduledDotHits(
        timeline,
        hits,
        ts
      );
      const groups = /* @__PURE__ */ new Map();
      for (const { hit, cast } of matched) {
        const groupKey = `${cast.key}\0${cast.abilityHrid}`;
        const group = groups.get(groupKey) || {
          key: cast.key,
          abilityHrid: cast.abilityHrid,
          amount: 0
        };
        group.amount += Number(hit.amount) || 0;
        groups.set(groupKey, group);
      }
      return {
        groups: [...groups.values()],
        unmatchedAmount: unmatched.reduce(
          (sum, hit) => sum + (Number(hit.amount) || 0),
          0
        ),
        ambiguous,
        hasPendingTargetSchedule
      };
    }
    function dotAbilityFromSource(source) {
      const value = String(source || "");
      if (value.startsWith("dot:/abilities/")) return value.slice(4);
      return abilityDamageRules.get(value)?.dotDuration > 0 ? value : "";
    }
    function recordDotAttribution(timeline, key, source, hits, ts) {
      const value = String(source || ""), abilityHrid = dotAbilityFromSource(value);
      if (abilityHrid && !value.startsWith("dot:")) {
        bindDotTargets(timeline, key, abilityHrid, hits, ts);
        return;
      }
      if (value === "dot" || value.startsWith("dot:")) {
        consumeScheduledDotHits(timeline, hits, ts, { key, abilityHrid });
      }
    }
    function activeDotCandidates(keys, activeContainer, ts) {
      return keys.filter(
        (key) => [...(activeContainer[key] || /* @__PURE__ */ new Map()).values()].some(
          (until) => until > ts
        )
      );
    }
    function uniqueActiveDotCandidate(keys, activeContainer, ts) {
      const candidates = activeDotCandidates(keys, activeContainer, ts);
      return candidates.length === 1 ? candidates[0] : null;
    }
    function activeDotSourceFor(activeContainer, key, ts) {
      const abilities = [...(activeContainer[key] || /* @__PURE__ */ new Map()).entries()].filter(([, until]) => until > ts).map(([hrid]) => hrid);
      return abilities.length === 1 ? `dot:${abilities[0]}` : "dot";
    }
    function dotAbilitiesFor(dotContainer, activeContainer, key, ts) {
      const known = [...dotContainer[key] || []];
      const active = [...(activeContainer[key] || /* @__PURE__ */ new Map()).entries()].filter(([, until]) => until > ts).map(([hrid]) => hrid);
      if (known.length === 1) return known;
      if (active.length === 1) return active;
      return active.length ? active : known;
    }
    function dotSourceFor(dotContainer, activeContainer, key, ts) {
      const abilities = dotAbilitiesFor(dotContainer, activeContainer, key, ts);
      return abilities.length === 1 ? "dot:" + abilities[0] : "dot";
    }
    function uniqueDotCandidate(keys, dotContainer, activeContainer, ts) {
      const candidates = keys.filter(
        (key) => dotAbilitiesFor(dotContainer, activeContainer, key, ts).length > 0
      );
      return candidates.length === 1 ? candidates[0] : null;
    }
    const DIRECT_WEAPON_EFFECTS = [
      "blazing_trident",
      "chaotic_flail",
      "sundering_crossbow"
    ];
    function directWeaponEffectFor(playerName) {
      const weapon = String(ClassSystem.getWeapon(playerName) || "");
      return DIRECT_WEAPON_EFFECTS.some((name) => weapon.includes(name)) ? weapon : "";
    }
    function combinedWeaponSource(weapon, action) {
      return "combined:" + encodeURIComponent(weapon) + "|" + encodeURIComponent(DamageSources.normalize(action));
    }
    function sourceWithWeaponEffect(playerName, action, damagedTargetCount) {
      const weapon = directWeaponEffectFor(playerName);
      if (!weapon) return action;
      if (DamageSources.normalize(action) === DamageSources.normalize(weapon))
        return weapon;
      const kind = actionDamageKind(action), rule = abilityDamageRules.get(String(action || ""));
      if (kind === "support")
        return weapon.includes("blazing_trident") ? weapon : action;
      if (weapon.includes("chaotic_flail") && action === "auto") return action;
      if (weapon.includes("sundering_crossbow") && action === "auto")
        return action;
      const singleTarget = action === "auto" || !rule || !(Array.isArray(rule.targetTypes) && rule.targetTypes.some((type) => /allEnemies|all_enemies/i.test(type)));
      if (weapon.includes("blazing_trident") && singleTarget && damagedTargetCount > 1)
        return combinedWeaponSource(weapon, action);
      return action;
    }
    function splitCrossbowPierce(playerName, action, hits, primaryKey) {
      const weapon = directWeaponEffectFor(playerName);
      if (action !== "auto" || !weapon.includes("sundering_crossbow") || !Array.isArray(hits) || hits.length < 2)
        return null;
      const primary = hits.find((hit) => String(hit.key) === String(primaryKey));
      if (!primary) return null;
      const effect = hits.filter((hit) => String(hit.key) !== String(primaryKey)).reduce((sum, hit) => sum + (Number(hit.amount) || 0), 0);
      return effect > 0 ? { base: Number(primary.amount) || 0, effect, weapon } : null;
    }
    function monsterIdentity(names, hrids, key) {
      const baseName = names[key] || "怪物" + (+key + 1), hrid = hrids[key] || "";
      const duplicateCount = Object.keys(names || {}).filter(
        (candidate) => names[candidate] === baseName && (hrids[candidate] || "") === hrid
      ).length;
      return {
        name: duplicateCount > 1 ? baseName + " #" + (+key + 1) : baseName,
        hrid
      };
    }
    function takenSource(names, hrids, key, action) {
      const monster = monsterIdentity(names, hrids, key);
      return TakenSources.encode(monster.name, monster.hrid, action);
    }
    function actionFromUpdate(unit = {}, fallback = "") {
      return unit.abilityHrid || (unit.isAutoAtk || unit.isAutoAttack ? "auto" : fallback);
    }
    function activateReflection(activeContainer, sourceContainer, slot, abilityHrid, ts) {
      const duration = reflectionAbilityRules.get(String(abilityHrid || ""));
      if (duration) {
        activeContainer[slot] = Math.max(
          activeContainer[slot] || 0,
          ts + duration
        );
        sourceContainer[slot] = String(abilityHrid);
      }
    }
    function activateGuildReflection(slot, abilityHrid, ts) {
      activateReflection(
        guildReflectUntil,
        guildReflectSource,
        slot,
        abilityHrid,
        ts
      );
    }
    function isGuildReflecting(slot, ts) {
      return Number(guildReflectUntil[slot]) > ts;
    }
    function activatePlayerReflection(slot, abilityHrid, ts) {
      activateReflection(
        playerReflectUntil,
        playerReflectSource,
        slot,
        abilityHrid,
        ts
      );
    }
    function isPlayerReflecting(slot, ts) {
      return Number(playerReflectUntil[slot]) > ts;
    }
    function learnPlayerReflectionState(slot, unit = {}) {
      rememberAbilityKnowledge(
        playerKnownDotAbilities,
        playerKnownReflectionAbilities,
        slot,
        unit
      );
      const nowWall = Date.now(), nowPerf = clockNow();
      let remaining = 0, foundBuff = false;
      Object.values(unit.combatBuffMap || {}).forEach((buff) => {
        const type = normalizedReflectType(
          buff && buff.typeHrid || buff && buff.uniqueHrid
        );
        if (!REFLECTION_TYPES.has(type)) return;
        foundBuff = true;
        const duration = durationToMs(buff.duration);
        const started = Date.parse(buff.startTime || "");
        const expires = Number.isFinite(started) ? started + duration : nowWall + duration;
        remaining = Math.max(
          remaining,
          duration === 0 ? Infinity : expires - nowWall
        );
        const unique = String(buff && buff.uniqueHrid || ""), byUnique = reflectionBuffSources.get(unique), byType = [...reflectionTypeSources.get(type) || []], known = [...playerKnownReflectionAbilities[slot] || []], evidence = unique.toLowerCase();
        const exact = byUnique || (byType.length === 1 ? byType[0] : "") || (known.length === 1 ? known[0] : "") || (evidence.includes("spike_shell") ? "/abilities/spike_shell" : evidence.includes("retribution") ? "/abilities/retribution" : "");
        if (exact) playerReflectSource[slot] = exact;
      });
      const stats = unit.combatDetails && unit.combatDetails.combatStats || {};
      const permanent = !foundBuff && (Number(stats.physicalThorns) || Number(stats.elementalThorns) || Number(stats.retaliation));
      if (permanent) playerReflectUntil[slot] = Infinity;
      else if (remaining > 0)
        playerReflectUntil[slot] = Math.max(
          playerReflectUntil[slot] || 0,
          nowPerf + remaining
        );
      if (!playerReflectSource[slot]) {
        const known = [...playerKnownReflectionAbilities[slot] || []];
        if (known.length === 1) playerReflectSource[slot] = known[0];
      }
    }
    function learnPlayerReflectionUnit(unit = {}) {
      const name = unit.name || unit.character && unit.character.name;
      if (!name) return;
      for (const [slot, slotName] of keyToName) {
        if (slotName === name) learnPlayerReflectionState(slot, unit);
      }
    }
    function learnGuildReflectionState(unit = {}) {
      const name = unit.name || unit.character && unit.character.name;
      if (!name) return;
      const slots = [...guildSlotNames.entries()].filter(([, slotName]) => slotName === name).map(([slot]) => slot);
      if (!slots.length) return;
      slots.forEach(
        (slot) => rememberAbilityKnowledge(
          guildKnownDotAbilities,
          guildKnownReflectionAbilities,
          slot,
          unit
        )
      );
      const nowWall = Date.now(), nowPerf = clockNow();
      let remaining = 0, foundBuff = false;
      Object.values(unit.combatBuffMap || {}).forEach((buff) => {
        const type = normalizedReflectType(
          buff && buff.typeHrid || buff && buff.uniqueHrid
        );
        if (!REFLECTION_TYPES.has(type)) return;
        foundBuff = true;
        const duration = durationToMs(buff.duration);
        const started = Date.parse(buff.startTime || "");
        const expires = Number.isFinite(started) ? started + duration : nowWall + duration;
        remaining = Math.max(
          remaining,
          duration === 0 ? Infinity : expires - nowWall
        );
        const unique = String(buff && buff.uniqueHrid || "");
        const byUnique = reflectionBuffSources.get(unique);
        const byType = [...reflectionTypeSources.get(type) || []];
        const evidence = unique.toLowerCase();
        slots.forEach((slot) => {
          const known = [...guildKnownReflectionAbilities[slot] || []];
          const exact = byUnique || (byType.length === 1 ? byType[0] : "") || (known.length === 1 ? known[0] : "") || (evidence.includes("spike_shell") ? "/abilities/spike_shell" : evidence.includes("retribution") ? "/abilities/retribution" : "");
          if (exact) guildReflectSource[slot] = exact;
        });
      });
      const stats = unit.combatDetails && unit.combatDetails.combatStats || {};
      const permanent = !foundBuff && (Number(stats.physicalThorns) || Number(stats.elementalThorns) || Number(stats.retaliation));
      slots.forEach((slot) => {
        if (permanent) guildReflectUntil[slot] = Infinity;
        else if (remaining > 0)
          guildReflectUntil[slot] = Math.max(
            guildReflectUntil[slot] || 0,
            nowPerf + remaining
          );
        if (!guildReflectSource[slot]) {
          const known = [...guildKnownReflectionAbilities[slot] || []];
          if (known.length === 1) guildReflectSource[slot] = known[0];
        }
      });
    }
    function learnProfileReflectionEquipment(payload = {}) {
      const profile = payload.profile || payload.data && payload.data.profile || payload;
      const shared = profile.sharableCharacter || profile.character || profile;
      const name = shared.name || profile.name;
      if (!name) return;
      const wearable = profile.wearableItemMap || shared.wearableItemMap || {};
      const equipped = /* @__PURE__ */ new Set();
      const walk = (value, depth = 0) => {
        if (depth > 5 || value === null || value === void 0) return;
        if (typeof value === "string") {
          if (value.startsWith("/items/")) equipped.add(value);
          return;
        }
        if (typeof value === "object")
          Object.values(value).forEach((child) => walk(child, depth + 1));
      };
      walk(wearable);
      const reflectionItem = [...equipped].find(
        (hrid) => reflectionItemHrids.has(hrid)
      );
      if (!reflectionItem) return;
      [...guildSlotNames.entries()].filter(([, slotName]) => slotName === name).forEach(([slot]) => {
        guildReflectUntil[slot] = Infinity;
        guildReflectSource[slot] = reflectionItem;
      });
    }
    function guildStageSignature(payload = {}) {
      const base = String(
        payload.combatStartTime || payload.guildBattleId || payload.battleId || payload.combatId || "guild-" + CombatIdentity.dayStamp(/* @__PURE__ */ new Date())
      );
      const tier = payload.tier !== void 0 ? String(payload.tier) : "unknown";
      return { base, signature: base + "|tier:" + tier, tier };
    }
    function processNewGuildBattle(p) {
      const players = p.players || [];
      if (players.length === 0) return;
      const stage = guildStageSignature(p);
      const wasGuildBattle = isGuildBattle;
      isGuildBattle = true;
      const stageChanged = wasGuildBattle && currentGuildStageSignature && stage.signature !== currentGuildStageSignature;
      if (stageChanged) {
        guildTrialEnded = false;
        guildCombatWasActive = false;
      }
      currentGuildStageSignature = stage.signature;
      currentGuildTier = stage.tier;
      currentCombatKey = stage.base;
      const classes = ClassSystem.registerPlayers(players);
      guildMaxSlot = Math.max(guildMaxSlot, players.length);
      guildPlayersHP = {};
      guildPlayersMP = {};
      guildPlayersAtkCounter = {};
      guildPlayersDmgCounter = {};
      guildCurrentAction = {};
      guildReflectUntil = {};
      guildReflectSource = {};
      guildKnownDotAbilities = {};
      guildDotUntil = {};
      guildKnownReflectionAbilities = {};
      guildMonstersHP = {};
      guildMonstersMHP = {};
      guildMonstersDmgCounter = {};
      guildMonstersAtkCounter = {};
      guildMonsterCurrentAction = {};
      guildMonsterNames = {};
      guildMonsterHrids = {};
      guildMonsterKnownDotAbilities = {};
      guildMonsterDotUntil = {};
      players.forEach((pl, i) => {
        const slot = String(i);
        const name = pl.name || pl.character && pl.character.name;
        if (pl.currentHitpoints !== void 0)
          guildPlayersHP[slot] = Number(pl.currentHitpoints) || 0;
        if (pl.currentManapoints !== void 0)
          guildPlayersMP[slot] = Number(pl.currentManapoints) || 0;
        guildPlayersAtkCounter[slot] = playerAttackCounter(pl) ?? 0;
        guildPlayersDmgCounter[slot] = monsterDamageCounter(pl) ?? 0;
        guildCurrentAction[slot] = actionFromUpdate(
          {
            abilityHrid: pl.preparingAbilityHrid,
            isAutoAtk: pl.isPreparingAutoAttack
          },
          ""
        );
        rememberAbilityKnowledge(
          guildKnownDotAbilities,
          guildKnownReflectionAbilities,
          slot,
          pl
        );
        if (!name) return;
        const wasNeverResolved = !guildSlotNames.has(slot);
        const oldLabel = guildSlotLabel(slot);
        guildSlotNames.set(slot, name);
        guildSlotLocked.add(slot);
        if (wasNeverResolved && oldLabel !== name) {
          bus.dispatchEvent(
            new CustomEvent("guildSlotRenamed", {
              detail: { oldName: oldLabel, newName: name }
            })
          );
        }
      });
      (p.monsters || []).forEach((monster, index) => {
        const key = String(index);
        guildMonstersHP[key] = Number(
          monster.currentHitpoints !== void 0 ? monster.currentHitpoints : monster.cHP
        ) || 0;
        guildMonstersMHP[key] = Number(
          monster.maxHitpoints !== void 0 ? monster.maxHitpoints : monster.mHP
        ) || 0;
        const counter = monsterDamageCounter(monster);
        if (counter !== void 0) guildMonstersDmgCounter[key] = counter;
        guildMonstersAtkCounter[key] = playerAttackCounter(monster) ?? 0;
        guildMonsterCurrentAction[key] = actionFromUpdate(
          {
            abilityHrid: monster.preparingAbilityHrid,
            isAutoAtk: monster.isPreparingAutoAttack
          },
          ""
        );
        guildMonsterNames[key] = monster.name || String(monster.hrid || "").split("/").pop().replace(/_/g, " ") || "怪物" + (index + 1);
        guildMonsterHrids[key] = monster.hrid || "";
        rememberAbilityKnowledge(
          guildMonsterKnownDotAbilities,
          null,
          key,
          monster
        );
      });
      players.forEach(learnGuildReflectionState);
      bus.dispatchEvent(
        new CustomEvent("guildBattleDetected", {
          detail: {
            combatKey: currentCombatKey,
            stageId: stage.signature,
            tier: stage.tier,
            stageChanged,
            characterId: currentCharacterId2,
            classes
          }
        })
      );
    }
    function processGuildCombatSnapshot(battle) {
      if (!battle || !Array.isArray(battle.players) || battle.players.length === 0)
        return false;
      const players = battle.players, monsters = Array.isArray(battle.monsters) ? battle.monsters : [];
      const signature = [
        battle.combatStartTime || "",
        battle.battleId ?? "",
        battle.battleWave ?? "",
        battle.tier ?? "",
        players.length,
        monsters.length
      ].join("|");
      if (signature === lastGuildSnapshotSignature && isGuildBattle) return false;
      lastGuildSnapshotSignature = signature;
      if ((!currentCharacterId2 || currentCharacterId2 === "unknown") && players[0] && players[0].character && players[0].character.id) {
        currentCharacterId2 = String(players[0].character.id);
      }
      processNewGuildBattle(battle);
      guildCombatWasActive = true;
      guildTrialEnded = false;
      return true;
    }
    function guildCombatSnapshotFromMessage(obj) {
      const sources = [
        obj,
        obj && obj.data,
        obj && obj.clientData,
        obj && obj.client_data,
        obj && obj.initCharacterData
      ].filter((source) => source && typeof source === "object");
      for (const source of sources) {
        if (source.guildCombatBattle && typeof source.guildCombatBattle === "object")
          return source.guildCombatBattle;
      }
      return null;
    }
    function processGuildUpdated(p) {
      if (!isGuildBattle || guildTrialEnded) return;
      const raw = p.guild && p.guild.currentTrialsData;
      if (!raw) return;
      let trials;
      try {
        trials = JSON.parse(raw);
      } catch (e) {
        return;
      }
      const combat = trials && trials.combat;
      if (!combat) return;
      if (combat.status === "in_progress") guildCombatWasActive = true;
      const parties = combat.parties;
      const allDone = parties && Object.keys(parties).length > 0 && Object.values(parties).every((party) => party && party.done === true);
      const statusLeftProgress = guildCombatWasActive && combat.status !== "in_progress";
      if (guildCombatWasActive && (allDone || statusLeftProgress)) {
        guildTrialEnded = true;
        bus.dispatchEvent(new CustomEvent("guildTrialEnded"));
      }
    }
    function processGuildBattleUpdated(p) {
      if (!isGuildBattle) return;
      const mMap = p.mMap || {}, pMap = p.pMap || {};
      const idx = Object.keys(pMap);
      const ts = clockNow();
      for (const k of idx) guildMaxSlot = Math.max(guildMaxSlot, +k + 1);
      const incomingTier = p.tier !== void 0 ? String(p.tier) : currentGuildTier;
      if (currentGuildTier === null || incomingTier !== null && String(currentGuildTier) !== String(incomingTier)) {
        currentGuildTier = incomingTier;
        currentGuildStageSignature = String(p.battleId || currentCombatKey || "guild") + "|tier:" + String(incomingTier || "unknown");
        guildPlayersHP = {};
        guildPlayersMP = {};
        guildPlayersAtkCounter = {};
        guildPlayersDmgCounter = {};
        guildCurrentAction = {};
        guildReflectUntil = {};
        guildReflectSource = {};
        guildKnownDotAbilities = {};
        guildDotUntil = {};
        guildKnownReflectionAbilities = {};
        guildMonstersHP = {};
        guildMonstersMHP = {};
        guildMonstersDmgCounter = {};
        guildMonstersAtkCounter = {};
        guildMonsterCurrentAction = {};
        guildMonsterNames = {};
        guildMonsterHrids = {};
        guildMonsterKnownDotAbilities = {};
        guildMonsterDotUntil = {};
        for (const k of idx) {
          const unit = pMap[k], counter = playerAttackCounter(unit);
          guildPlayersHP[k] = Number(unit.cHP) || 0;
          guildPlayersMP[k] = Number(unit.cMP) || 0;
          if (counter !== void 0) guildPlayersAtkCounter[k] = counter;
          const damageCounter = monsterDamageCounter(unit);
          if (damageCounter !== void 0)
            guildPlayersDmgCounter[k] = damageCounter;
          guildCurrentAction[k] = actionFromUpdate(unit, "");
          rememberAbilityKnowledge(
            guildKnownDotAbilities,
            guildKnownReflectionAbilities,
            k,
            unit
          );
        }
        for (const mk in mMap) {
          const unit = mMap[mk], counter = monsterDamageCounter(unit);
          guildMonstersHP[mk] = Number(unit.cHP) || 0;
          guildMonstersMHP[mk] = Number(unit.mHP) || 0;
          if (counter !== void 0) guildMonstersDmgCounter[mk] = counter;
          guildMonstersAtkCounter[mk] = playerAttackCounter(unit) ?? 0;
          guildMonsterCurrentAction[mk] = actionFromUpdate(unit, "");
          guildMonsterNames[mk] = unit.name || String(unit.hrid || "").split("/").pop().replace(/_/g, " ") || "怪物" + (+mk + 1);
          guildMonsterHrids[mk] = unit.hrid || "";
          rememberAbilityKnowledge(guildMonsterKnownDotAbilities, null, mk, unit);
        }
        guildTrialEnded = false;
        guildCombatWasActive = true;
        return;
      }
      const counterActors = [], counterDeltas = {}, completedActions = {}, hitPlayers = /* @__PURE__ */ new Set();
      for (const k of idx) {
        const unit = pMap[k], counter = playerAttackCounter(unit), previous = guildPlayersAtkCounter[k];
        rememberAbilityKnowledge(
          guildKnownDotAbilities,
          guildKnownReflectionAbilities,
          k,
          unit
        );
        if (unit.abilityHrid)
          ClassSystem.learnAbility(guildSlotLabel(k), unit.abilityHrid);
        guildPlayersMP[k] = Number(unit.cMP) || 0;
        if (counter !== void 0) {
          if (previous !== void 0 && counter > previous) {
            const completed = guildCurrentAction[k] || (unit.isAutoAtk || unit.isAutoAttack ? "auto" : "unknown");
            counterActors.push(k);
            counterDeltas[k] = counter - previous;
            completedActions[k] = completed;
            activateGuildReflection(k, completed, ts);
            activateDot(guildDotUntil, guildKnownDotAbilities, k, completed, ts);
          }
          guildPlayersAtkCounter[k] = counter;
        }
        const damageCounter = monsterDamageCounter(unit), previousDamage = guildPlayersDmgCounter[k];
        if (damageCounter !== void 0) {
          if (previousDamage !== void 0 && damageCounter > previousDamage)
            hitPlayers.add(k);
          guildPlayersDmgCounter[k] = damageCounter;
        }
        guildCurrentAction[k] = actionFromUpdate(unit, guildCurrentAction[k]);
      }
      const monsterActors = [], monsterCompletedActions = {};
      for (const mk in mMap) {
        const unit = mMap[mk], counter = playerAttackCounter(unit), previous = guildMonstersAtkCounter[mk];
        rememberAbilityKnowledge(guildMonsterKnownDotAbilities, null, mk, unit);
        if (counter !== void 0) {
          if (previous !== void 0 && counter > previous) {
            const completed = guildMonsterCurrentAction[mk] || actionFromUpdate(unit, "unknown");
            monsterActors.push(mk);
            monsterCompletedActions[mk] = completed;
            activateDot(
              guildMonsterDotUntil,
              guildMonsterKnownDotAbilities,
              mk,
              completed,
              ts
            );
          }
          guildMonstersAtkCounter[mk] = counter;
        }
        guildMonsterCurrentAction[mk] = actionFromUpdate(
          unit,
          guildMonsterCurrentAction[mk]
        );
      }
      const soleAccuracyActor = counterActors.length === 1 && counterDeltas[counterActors[0]] === 1 && monsterActors.length === 0 && Object.keys(mMap).length > 0 && actionDamageKind(completedActions[counterActors[0]]) === "direct" ? counterActors[0] : null, accuracyAction = soleAccuracyActor === null ? "" : completedActions[soleAccuracyActor] || "", accuracyAliveMonsterKeys = Object.keys(guildMonstersHP).filter(
        (key) => mMap[key] && Number(guildMonstersHP[key]) > 0
      );
      let incomingTakenSource = TakenSources.encode("未知怪物", "", "unknown");
      if (monsterActors.length === 1) {
        const mk = monsterActors[0];
        incomingTakenSource = takenSource(
          guildMonsterNames,
          guildMonsterHrids,
          mk,
          monsterCompletedActions[mk] || "unknown"
        );
      } else if (monsterActors.length === 0) {
        const monsterKeys = Object.keys(mMap), dotMonster = uniqueDotCandidate(
          monsterKeys,
          guildMonsterKnownDotAbilities,
          guildMonsterDotUntil,
          ts
        );
        if (dotMonster !== null)
          incomingTakenSource = takenSource(
            guildMonsterNames,
            guildMonsterHrids,
            dotMonster,
            dotSourceFor(
              guildMonsterKnownDotAbilities,
              guildMonsterDotUntil,
              dotMonster,
              ts
            )
          );
        else if (monsterKeys.length === 1)
          incomingTakenSource = takenSource(
            guildMonsterNames,
            guildMonsterHrids,
            monsterKeys[0],
            "unknown"
          );
      }
      for (const k of idx) {
        const hp = pMap[k].cHP || 0;
        if (guildPlayersHP[k] !== void 0) {
          const name = guildSlotLabel(k);
          const diff = guildPlayersHP[k] - hp;
          if (diff > 0)
            bus.dispatchEvent(
              new CustomEvent("playerDamageTaken", {
                detail: {
                  name,
                  amount: diff,
                  source: incomingTakenSource,
                  battleType: "trial"
                }
              })
            );
          else if (hp > guildPlayersHP[k])
            bus.dispatchEvent(
              new CustomEvent("healing", {
                detail: {
                  name,
                  amount: hp - guildPlayersHP[k],
                  battleType: "trial"
                }
              })
            );
        }
        guildPlayersHP[k] = pMap[k].cHP || 0;
      }
      const primaryGuildMonsterKey = Object.keys(guildMonstersHP).sort((a, b) => +a - +b).find((key) => Number(guildMonstersHP[key]) > 0);
      const guildMonsterHits = [];
      let tickDmg = 0, damagedTargetCount = 0;
      for (const mk in mMap) {
        const mv = mMap[mk];
        if (guildMonstersHP[mk] === void 0 || guildMonstersMHP[mk] !== mv.mHP) {
          guildMonstersHP[mk] = mv.cHP || 0;
          guildMonstersMHP[mk] = mv.mHP;
          const initialCounter = monsterDamageCounter(mv);
          if (initialCounter !== void 0)
            guildMonstersDmgCounter[mk] = initialCounter;
          continue;
        }
        const previousHP = guildMonstersHP[mk], previousCounter = guildMonstersDmgCounter[mk];
        const nextCounter = monsterDamageCounter(mv), d = previousHP - (mv.cHP || 0);
        if (d > 0 && nextCounter !== void 0 && previousCounter !== void 0 && nextCounter > previousCounter) {
          tickDmg += d;
          damagedTargetCount++;
          guildMonsterHits.push({ key: mk, amount: d });
        }
        guildMonstersHP[mk] = mv.cHP || 0;
        if (nextCounter !== void 0) guildMonstersDmgCounter[mk] = nextCounter;
      }
      if (tickDmg > 0) {
        bus.dispatchEvent(
          new CustomEvent("damage", {
            detail: { amount: tickDmg, ts, battleType: "trial" }
          })
        );
        let attributedKey = null, source = "unknown";
        const soleCounter = counterActors.length === 1 ? counterActors[0] : null;
        if (monsterActors.length === 1) {
          const hitReflectors = idx.filter(
            (k) => hitPlayers.has(k) && isGuildReflecting(k, ts)
          );
          const reflectors = hitReflectors.length ? hitReflectors : idx.filter((k) => isGuildReflecting(k, ts));
          if (reflectors.length === 1) {
            attributedKey = reflectors[0];
            source = guildReflectSource[attributedKey] || "reflect";
          }
        }
        if (attributedKey === null && soleCounter !== null) {
          const completed = completedActions[soleCounter] || "unknown", kind = actionDamageKind(completed);
          const actorName = guildSlotLabel(soleCounter), weapon = directWeaponEffectFor(actorName);
          if (kind !== "support") {
            attributedKey = soleCounter;
            source = completed;
          } else if (weapon.includes("blazing_trident")) {
            attributedKey = soleCounter;
            source = weapon;
          } else {
            const dotKey = uniqueDotCandidate(
              idx,
              guildKnownDotAbilities,
              guildDotUntil,
              ts
            );
            if (dotKey !== null) {
              attributedKey = dotKey;
              source = dotSourceFor(
                guildKnownDotAbilities,
                guildDotUntil,
                dotKey,
                ts
              );
            }
          }
        }
        if (attributedKey === null && monsterActors.length === 0) {
          const dotKey = uniqueDotCandidate(
            idx,
            guildKnownDotAbilities,
            guildDotUntil,
            ts
          );
          if (dotKey !== null) {
            attributedKey = dotKey;
            source = dotSourceFor(
              guildKnownDotAbilities,
              guildDotUntil,
              dotKey,
              ts
            );
          } else if (idx.length === 1) {
            attributedKey = idx[0];
            source = dotSourceFor(
              guildKnownDotAbilities,
              guildDotUntil,
              idx[0],
              ts
            );
          }
        }
        if (attributedKey !== null) {
          const name = guildSlotLabel(attributedKey);
          const pierce = splitCrossbowPierce(
            name,
            source,
            guildMonsterHits,
            primaryGuildMonsterKey
          );
          if (pierce) {
            if (pierce.base > 0)
              bus.dispatchEvent(
                new CustomEvent("playerDamage", {
                  detail: {
                    name,
                    amount: pierce.base,
                    source: "auto",
                    ts,
                    battleType: "trial"
                  }
                })
              );
            bus.dispatchEvent(
              new CustomEvent("playerDamage", {
                detail: {
                  name,
                  amount: pierce.effect,
                  source: pierce.weapon,
                  ts,
                  battleType: "trial"
                }
              })
            );
          } else {
            source = sourceWithWeaponEffect(name, source, damagedTargetCount);
            bus.dispatchEvent(
              new CustomEvent("playerDamage", {
                detail: {
                  name,
                  amount: tickDmg,
                  source,
                  ts,
                  battleType: "trial"
                }
              })
            );
          }
          Diagnostics.recordNominal();
        } else if (monsterActors.length === 1) {
          const hitReflectors = idx.filter(
            (k) => hitPlayers.has(k) && isGuildReflecting(k, ts)
          );
          const reflectors = hitReflectors.length ? hitReflectors : idx.filter((k) => isGuildReflecting(k, ts));
          if (reflectors.length) {
            const share = tickDmg / reflectors.length;
            reflectors.forEach(
              (k) => bus.dispatchEvent(
                new CustomEvent("playerDamage", {
                  detail: {
                    name: guildSlotLabel(k),
                    amount: share,
                    source: guildReflectSource[k] || "reflect",
                    ts,
                    battleType: "trial"
                  }
                })
              )
            );
            Diagnostics.recordNominal();
          } else Diagnostics.recordOrphan(tickDmg);
        } else {
          Diagnostics.recordOrphan(tickDmg);
        }
      }
      if (soleAccuracyActor !== null) {
        bus.dispatchEvent(
          new CustomEvent("attackResolved", {
            detail: {
              name: guildSlotLabel(soleAccuracyActor),
              hit: guildMonsterHits.length > 0,
              targets: accuracyTargets(
                accuracyAction,
                guildMonsterHits,
                accuracyAliveMonsterKeys,
                guildMonsterNames,
                guildMonsterHrids
              ),
              battleType: "trial"
            }
          })
        );
      }
    }
    function resetBattleState() {
      monstersHP = [];
      monstersAlive = [];
      monsterNames = [];
      monsterHrids = [];
      monstersAtkCounter = [];
      monsterCurrentAction = [];
      monsterKnownDotAbilities = {};
      monsterDotUntil = {};
      playersMP = [];
      playersAtkCounter = [];
      playersDmgCounter = [];
      playersHP = [];
      playerKnownDotAbilities = {};
      playerDotUntil = {};
      playerDotCasts = [];
      playerKnownReflectionAbilities = {};
      playerReflectUntil = {};
      playerReflectSource = {};
      currentAction = [];
      keyToName.clear();
      haveBattle = false;
      lastCombatStartTime = null;
      isGuildBattle = false;
      guildMonstersHP = {};
      guildMonstersMHP = {};
      guildMonstersDmgCounter = {};
      guildMonstersAtkCounter = {};
      guildMonsterCurrentAction = {};
      guildMonsterNames = {};
      guildMonsterHrids = {};
      guildMonsterKnownDotAbilities = {};
      guildMonsterDotUntil = {};
      guildPlayersHP = {};
      guildPlayersMP = {};
      guildPlayersAtkCounter = {};
      guildPlayersDmgCounter = {};
      guildCurrentAction = {};
      guildReflectUntil = {};
      guildReflectSource = {};
      guildMaxSlot = 0;
      guildKnownDotAbilities = {};
      guildDotUntil = {};
      guildKnownReflectionAbilities = {};
      currentGuildTier = null;
      currentGuildStageSignature = "";
      guildSlotNames.clear();
      guildSlotLocked.clear();
      guildCombatWasActive = false;
      guildTrialEnded = false;
      lastGuildSnapshotSignature = "";
    }
    function processNewBattle(p) {
      const parallelGuildBattle = isGuildBattle;
      const incomingCombatKey = p.combatStartTime || p.battleId || p.combatId;
      if (incomingCombatKey && !parallelGuildBattle) {
        if (lastCombatStartTime !== null && incomingCombatKey !== lastCombatStartTime) {
          bus.dispatchEvent(new CustomEvent("newCombatInstance"));
        }
        lastCombatStartTime = incomingCombatKey;
        currentCombatKey = String(incomingCombatKey);
      }
      if (!parallelGuildBattle) {
        guildMonstersHP = {};
        guildMonstersMHP = {};
        guildMonstersDmgCounter = {};
        guildMonstersAtkCounter = {};
        guildMonsterCurrentAction = {};
        guildMonsterNames = {};
        guildMonsterHrids = {};
        guildMonsterKnownDotAbilities = {};
        guildMonsterDotUntil = {};
        guildPlayersHP = {};
        guildPlayersMP = {};
        guildPlayersAtkCounter = {};
        guildPlayersDmgCounter = {};
        guildCurrentAction = {};
        guildReflectUntil = {};
        guildReflectSource = {};
        guildKnownDotAbilities = {};
        guildDotUntil = {};
        guildKnownReflectionAbilities = {};
      }
      monstersHP = (p.monsters || []).map(
        (m) => m.currentHitpoints !== void 0 ? m.currentHitpoints : m.cHP || 0
      );
      monstersAlive = monstersHP.map((hp) => hp > 0);
      monsterNames = (p.monsters || []).map(
        (monster, index) => monster.name || String(monster.hrid || "").split("/").pop().replace(/_/g, " ") || "怪物" + (index + 1)
      );
      monsterHrids = (p.monsters || []).map((monster) => monster.hrid || "");
      monstersAtkCounter = (p.monsters || []).map(
        (monster) => playerAttackCounter(monster) ?? 0
      );
      monsterCurrentAction = (p.monsters || []).map(
        (monster) => monster.preparingAbilityHrid ? monster.preparingAbilityHrid : monster.isPreparingAutoAttack ? "auto" : ""
      );
      monsterKnownDotAbilities = {};
      monsterDotUntil = {};
      (p.monsters || []).forEach(
        (monster, index) => rememberAbilityKnowledge(
          monsterKnownDotAbilities,
          null,
          String(index),
          monster
        )
      );
      playersMP = (p.players || []).map(
        (pl) => pl.currentManapoints !== void 0 ? pl.currentManapoints : pl.cMP || 0
      );
      playersAtkCounter = (p.players || []).map((pl) => {
        const value = pl.attackAttemptCounter !== void 0 ? pl.attackAttemptCounter : pl.atkCounter;
        const number3 = Number(value);
        return value !== void 0 && Number.isFinite(number3) ? number3 : void 0;
      });
      playersDmgCounter = (p.players || []).map((pl) => monsterDamageCounter(pl));
      playersHP = (p.players || []).map(
        (pl) => pl.currentHitpoints !== void 0 ? pl.currentHitpoints : pl.cHP || 0
      );
      playerKnownDotAbilities = {};
      playerDotUntil = {};
      playerDotCasts = [];
      playerKnownReflectionAbilities = {};
      playerReflectUntil = {};
      playerReflectSource = {};
      keyToName.clear();
      const names = [];
      const classes = ClassSystem.registerPlayers(p.players || []);
      (p.players || []).forEach((pl, i) => {
        const n = pl.character && pl.character.name || pl.name;
        if (n) {
          names.push(n);
          keyToName.set(String(i), n);
        }
        learnPlayerReflectionState(String(i), pl);
      });
      currentAction = (p.players || []).map(
        (pl) => pl.preparingAbilityHrid ? pl.preparingAbilityHrid : pl.isPreparingAutoAttack ? "auto" : "idle"
      );
      haveBattle = true;
      const NORMAL_ENRAGE_NS = 18e10;
      const isBoss = (p.monsters || []).some(
        (m) => (m.enrageTimerDuration || 0) > NORMAL_ENRAGE_NS
      );
      bus.dispatchEvent(
        new CustomEvent("newBattle", {
          detail: {
            names,
            isBoss,
            classes,
            combatKey: parallelGuildBattle ? String(incomingCombatKey || "") : currentCombatKey || String(p.battleId || ""),
            characterId: currentCharacterId2,
            parallelGuildBattle
          }
        })
      );
    }
    function processBattleUpdated(p) {
      if (!haveBattle) return;
      if (isGuildBattle) return;
      const mMap = p.mMap || {}, pMap = p.pMap || {};
      const idx = Object.keys(pMap);
      const ts = clockNow();
      const battleType = isInLabyrinth ? "labyrinth" : "combat";
      const counterActors = [], counterDeltas = {}, mpDroppers = [], completedActions = {}, hitPlayers = /* @__PURE__ */ new Set();
      for (const k of idx) {
        const i = +k, mp = pMap[k].cMP || 0;
        rememberAbilityKnowledge(
          playerKnownDotAbilities,
          playerKnownReflectionAbilities,
          k,
          pMap[k]
        );
        const playerName = keyToName.get(k);
        if (playerName && pMap[k].abilityHrid)
          ClassSystem.learnAbility(playerName, pMap[k].abilityHrid);
        if (playersMP[i] !== void 0 && mp < playersMP[i]) mpDroppers.push(k);
        playersMP[i] = mp;
        const rawCounter = pMap[k].atkCounter !== void 0 ? pMap[k].atkCounter : pMap[k].attackAttemptCounter;
        const nextCounter = Number(rawCounter), previousCounter = playersAtkCounter[i];
        if (rawCounter !== void 0 && Number.isFinite(nextCounter)) {
          if (previousCounter !== void 0 && nextCounter > previousCounter) {
            const completed = currentAction[i] || (pMap[k].isAutoAtk || pMap[k].isAutoAttack ? "auto" : "unknown");
            counterActors.push(k);
            counterDeltas[k] = nextCounter - previousCounter;
            completedActions[k] = completed;
            activatePlayerReflection(k, completed, ts);
            activateDot(
              playerDotUntil,
              playerKnownDotAbilities,
              k,
              completed,
              ts,
              playerDotCasts
            );
          }
          playersAtkCounter[i] = nextCounter;
        }
        const damageCounter = monsterDamageCounter(pMap[k]), previousDamage = playersDmgCounter[i];
        if (damageCounter !== void 0) {
          if (previousDamage !== void 0 && damageCounter > previousDamage)
            hitPlayers.add(k);
          playersDmgCounter[i] = damageCounter;
        }
        const nextHP = Number(pMap[k].cHP);
        if (Number.isFinite(nextHP) && playersHP[i] !== void 0 && nextHP < playersHP[i])
          hitPlayers.add(k);
      }
      const counterActor = counterActors.length === 1 ? counterActors[0] : null;
      const castPlayer = mpDroppers.length === 1 ? mpDroppers[0] : null;
      const monsterActors = [], monsterCompletedActions = {};
      for (const mk of Object.keys(mMap)) {
        const unit = mMap[mk], i = +mk, counter = playerAttackCounter(unit), previous = monstersAtkCounter[i];
        rememberAbilityKnowledge(monsterKnownDotAbilities, null, mk, unit);
        if (counter !== void 0) {
          if (previous !== void 0 && counter > previous) {
            const completed = monsterCurrentAction[i] || actionFromUpdate(unit, "unknown");
            monsterActors.push(mk);
            monsterCompletedActions[mk] = completed;
            activateDot(
              monsterDotUntil,
              monsterKnownDotAbilities,
              mk,
              completed,
              ts
            );
          }
          monstersAtkCounter[i] = counter;
        }
      }
      const soleAccuracyActor = counterActors.length === 1 && counterDeltas[counterActors[0]] === 1 && monsterActors.length === 0 && Object.keys(mMap).length > 0 && actionDamageKind(completedActions[counterActors[0]]) === "direct" ? counterActors[0] : null, accuracyAction = soleAccuracyActor === null ? "" : completedActions[soleAccuracyActor] || "", accuracyAliveMonsterKeys = monstersAlive.map((alive, index) => alive ? String(index) : null).filter((key) => key !== null && mMap[key]);
      let incomingTakenSource = TakenSources.encode("未知怪物", "", "unknown");
      if (monsterActors.length === 1) {
        const mk = monsterActors[0];
        incomingTakenSource = takenSource(
          monsterNames,
          monsterHrids,
          mk,
          monsterCompletedActions[mk] || "unknown"
        );
      } else if (monsterActors.length === 0) {
        const monsterKeys = Object.keys(mMap), dotMonster = uniqueDotCandidate(
          monsterKeys,
          monsterKnownDotAbilities,
          monsterDotUntil,
          ts
        );
        if (dotMonster !== null)
          incomingTakenSource = takenSource(
            monsterNames,
            monsterHrids,
            dotMonster,
            dotSourceFor(
              monsterKnownDotAbilities,
              monsterDotUntil,
              dotMonster,
              ts
            )
          );
        else if (monsterKeys.length === 1)
          incomingTakenSource = takenSource(
            monsterNames,
            monsterHrids,
            monsterKeys[0],
            "unknown"
          );
      }
      for (const k of idx) {
        const i = +k, hp = pMap[k].cHP || 0;
        if (playersHP[i] !== void 0) {
          const name = keyToName.get(k);
          if (name && hp > playersHP[i]) {
            bus.dispatchEvent(
              new CustomEvent("healing", {
                detail: { name, amount: hp - playersHP[i], battleType }
              })
            );
          } else if (name && hp < playersHP[i]) {
            bus.dispatchEvent(
              new CustomEvent("playerDamageTaken", {
                detail: {
                  name,
                  amount: playersHP[i] - hp,
                  source: incomingTakenSource,
                  battleType
                }
              })
            );
          }
        }
        playersHP[i] = hp;
      }
      let attributedKey = null, attributedSource = "unknown";
      const hitReflectors = idx.filter(
        (k) => hitPlayers.has(k) && isPlayerReflecting(k, ts)
      ), reflectors = monsterActors.length === 1 ? hitReflectors.length ? hitReflectors : idx.filter((k) => isPlayerReflecting(k, ts)) : [];
      const actionActor = counterActor !== null ? counterActor : castPlayer;
      if (reflectors.length === 1) {
        attributedKey = reflectors[0];
        attributedSource = playerReflectSource[attributedKey] || "reflect";
      } else if (actionActor !== null) {
        const action = completedActions[actionActor] || currentAction[+actionActor] || "unknown";
        const actorName = keyToName.get(actionActor), weapon = directWeaponEffectFor(actorName);
        if (actionDamageKind(action) !== "support") {
          attributedKey = actionActor;
          attributedSource = action && action !== "idle" ? action : pMap[actionActor].isAutoAtk ? "auto" : "unknown";
        } else if (weapon.includes("blazing_trident")) {
          attributedKey = actionActor;
          attributedSource = weapon;
        }
      }
      const primaryMonsterIndex = monstersAlive.findIndex(Boolean), monsterHits = [];
      let tickDmg = 0, killed = 0, damagedTargetCount = 0;
      for (const mk in mMap) {
        const i = +mk, mv = mMap[mk];
        if (monstersHP[i] === void 0) continue;
        const hpDiff = monstersHP[i] - (mv.cHP || 0);
        monstersHP[i] = mv.cHP || 0;
        if (hpDiff > 0) {
          tickDmg += hpDiff;
          damagedTargetCount++;
          monsterHits.push({ key: mk, amount: hpDiff });
        }
        if (monstersAlive[i] && (mv.cHP || 0) <= 0) {
          monstersAlive[i] = false;
          killed++;
        }
      }
      let killAttributionKey = attributedKey;
      if (tickDmg > 0) {
        bus.dispatchEvent(
          new CustomEvent("damage", {
            detail: { amount: tickDmg, ts, battleType }
          })
        );
        let scheduledDot = null;
        if (attributedKey === null && reflectors.length === 0 && monsterActors.length === 0) {
          const playerKeys = [...keyToName.keys()], activeKeys = activeDotCandidates(playerKeys, playerDotUntil, ts);
          scheduledDot = scheduledDotDamage(playerDotCasts, monsterHits, ts);
          if (scheduledDot.groups.length) {
            if (scheduledDot.groups.length === 1 && scheduledDot.unmatchedAmount === 0) {
              killAttributionKey = scheduledDot.groups[0].key;
            }
          } else if (!scheduledDot.ambiguous && !scheduledDot.hasPendingTargetSchedule && activeKeys.length === 1) {
            const activeKey = uniqueActiveDotCandidate(
              playerKeys,
              playerDotUntil,
              ts
            );
            attributedKey = activeKey;
            attributedSource = activeDotSourceFor(playerDotUntil, activeKey, ts);
            killAttributionKey = activeKey;
          } else if (!scheduledDot.ambiguous && !scheduledDot.hasPendingTargetSchedule && activeKeys.length === 0 && idx.length === 1) {
            attributedKey = idx[0];
            killAttributionKey = attributedKey;
            const action = currentAction[+attributedKey];
            attributedSource = action === "auto" || pMap[attributedKey].isAutoAtk ? "auto" : "unknown";
          }
        }
        if (attributedKey !== null) {
          const name = keyToName.get(attributedKey);
          if (name) {
            recordDotAttribution(
              playerDotCasts,
              attributedKey,
              attributedSource,
              monsterHits,
              ts
            );
            const pierce = splitCrossbowPierce(
              name,
              attributedSource,
              monsterHits,
              primaryMonsterIndex
            );
            if (pierce) {
              if (pierce.base > 0)
                bus.dispatchEvent(
                  new CustomEvent("playerDamage", {
                    detail: {
                      name,
                      amount: pierce.base,
                      source: "auto",
                      ts,
                      battleType
                    }
                  })
                );
              bus.dispatchEvent(
                new CustomEvent("playerDamage", {
                  detail: {
                    name,
                    amount: pierce.effect,
                    source: pierce.weapon,
                    ts,
                    battleType
                  }
                })
              );
            } else {
              attributedSource = sourceWithWeaponEffect(
                name,
                attributedSource,
                damagedTargetCount
              );
              bus.dispatchEvent(
                new CustomEvent("playerDamage", {
                  detail: {
                    name,
                    amount: tickDmg,
                    source: attributedSource,
                    ts,
                    battleType
                  }
                })
              );
            }
            Diagnostics.recordNominal();
          }
        } else if (scheduledDot?.groups.length) {
          for (const group of scheduledDot.groups) {
            const name = keyToName.get(group.key);
            if (!name || !(group.amount > 0)) continue;
            bus.dispatchEvent(
              new CustomEvent("playerDamage", {
                detail: {
                  name,
                  amount: group.amount,
                  source: `dot:${group.abilityHrid}`,
                  ts,
                  battleType
                }
              })
            );
          }
          if (scheduledDot.unmatchedAmount > 0) {
            Diagnostics.recordOrphan(scheduledDot.unmatchedAmount);
          } else {
            Diagnostics.recordNominal();
          }
        } else if (reflectors.length > 1) {
          const share = tickDmg / reflectors.length;
          reflectors.forEach((key) => {
            const name = keyToName.get(key);
            if (!name) return;
            bus.dispatchEvent(
              new CustomEvent("playerDamage", {
                detail: {
                  name,
                  amount: share,
                  source: playerReflectSource[key] || "reflect",
                  ts,
                  battleType
                }
              })
            );
          });
          Diagnostics.recordNominal();
        } else {
          Diagnostics.recordOrphan(tickDmg);
        }
      }
      if (killed > 0 && killAttributionKey !== null) {
        const name = keyToName.get(killAttributionKey);
        if (name)
          for (let n = 0; n < killed; n++)
            bus.dispatchEvent(
              new CustomEvent("kill", { detail: { name, battleType } })
            );
      }
      if (soleAccuracyActor !== null) {
        const name = keyToName.get(soleAccuracyActor);
        if (name)
          bus.dispatchEvent(
            new CustomEvent("attackResolved", {
              detail: {
                name,
                hit: monsterHits.length > 0,
                targets: accuracyTargets(
                  accuracyAction,
                  monsterHits,
                  accuracyAliveMonsterKeys,
                  monsterNames,
                  monsterHrids
                ),
                battleType
              }
            })
          );
      }
      for (const k of idx) {
        const i = +k, pv = pMap[k];
        currentAction[i] = pv.abilityHrid ? pv.abilityHrid : pv.isAutoAtk ? "auto" : "idle";
      }
      for (const mk of Object.keys(mMap)) {
        const i = +mk;
        monsterCurrentAction[i] = actionFromUpdate(
          mMap[mk],
          monsterCurrentAction[i]
        );
      }
    }
    function handleMessage2(message) {
      let obj = message;
      if (typeof message === "string") {
        try {
          obj = JSON.parse(message);
        } catch (e) {
          ClassProbe.recordUnparsed(message);
          return;
        }
      }
      if (!obj || typeof obj !== "object" || !obj.type) return;
      const messageData = obj.data && typeof obj.data === "object" ? obj.data : obj;
      const receivedItemDetails = messageData.itemDetailMap || messageData.gameItemDetailMap || obj.itemDetailMap || obj.gameItemDetailMap;
      if (receivedItemDetails) ClassSystem.cacheItemDetails(receivedItemDetails);
      if (receivedItemDetails || hasReflectionDefinitions(obj)) {
        cacheReflectionDefinitions(obj);
      }
      Capture.record(obj.type, obj);
      ClassDebug.record(obj.type, obj);
      ClassProbe.record(obj.type, obj);
      if (obj.type === "init_character_data") {
        const ch = messageData.character || obj.initCharacterData && obj.initCharacterData.character || {};
        const nextCharacterId = String(
          ch.id || ch.characterID || messageData.characterId || obj.characterId || "unknown"
        );
        const previousCharacterId = currentCharacterId2;
        currentCharacterId2 = nextCharacterId;
        resetBattleState();
        currentCharacterId2 = nextCharacterId;
        bus.dispatchEvent(
          new CustomEvent("socketReconnected", {
            detail: { characterId: nextCharacterId, previousCharacterId }
          })
        );
      }
      const guildSnapshot = obj.type === "init_character_data" || obj.type === "new_guild_battle" || obj.type === "guild_battle_updated" ? guildCombatSnapshotFromMessage(obj) : null;
      if (guildSnapshot) processGuildCombatSnapshot(guildSnapshot);
      if (obj.type === "battle_unit_fetched" || obj.type === "profile_shared") {
        const learned = obj.type === "profile_shared" ? ClassSystem.learnProfile(obj) : ClassSystem.learnBattleUnit(obj);
        if (obj.type === "battle_unit_fetched") {
          learnPlayerReflectionUnit(obj.unit || messageData.unit || {});
          learnGuildReflectionState(obj.unit || messageData.unit || {});
        } else learnProfileReflectionEquipment(obj);
        if (learned && learned.name && learned.classId !== "unknown") {
          bus.dispatchEvent(new CustomEvent("classLearned", { detail: learned }));
        }
      }
      if (obj.type === "new_battle") processNewBattle(obj);
      else if (obj.type === "battle_updated") processBattleUpdated(obj);
      else if (obj.type === "new_guild_battle") processNewGuildBattle(obj);
      else if (obj.type === "guild_battle_updated") {
        if (!isGuildBattle) {
          isGuildBattle = true;
          setTimeout(() => resolveGuildNames(guildMaxSlot), 300);
          setTimeout(() => resolveGuildNames(guildMaxSlot), 2e3);
          setTimeout(() => resolveGuildNames(guildMaxSlot), 5e3);
          bus.dispatchEvent(
            new CustomEvent("guildBattleDetected", {
              detail: fallbackGuildDetail(obj, "guild_battle_updated")
            })
          );
        }
        processGuildBattleUpdated(obj);
      } else if (obj.type === "battle_consumable_ability_updated" && obj.isGuildBattle) {
        if (!isGuildBattle) {
          isGuildBattle = true;
          bus.dispatchEvent(
            new CustomEvent("guildBattleDetected", {
              detail: fallbackGuildDetail(
                obj,
                "battle_consumable_ability_updated"
              )
            })
          );
        }
      } else if (obj.type === "guild_updated") processGuildUpdated(obj);
      else if (obj.type === "labyrinth_updated") {
        const lab = obj.labyrinth;
        if (lab) isInLabyrinth = !!lab.isActive;
      }
    }
    function previewGuildNames() {
      resolveGuildNames(guildMaxSlot);
      console.log(
        `[KikiMeter] ${guildSlotNames.size} 个姓名已解析(预计人数约 ${guildMaxSlot}) :`
      );
      console.log("=====================================================");
      [...guildSlotNames.entries()].forEach(([slot, name]) => {
        console.log(`  slot ${slot} → "${name}"`);
      });
      console.log("=====================================================");
      if (guildSlotNames.size !== guildMaxSlot) {
        console.warn(
          `[KikiMeter] 注意:${guildSlotNames.size} 个姓名已解析,但预计有 ${guildMaxSlot}  个位置,映射可能发生偏移。请确认位置 0 对应本地角色且顺序正确。`
        );
      }
      return Object.fromEntries(guildSlotNames);
    }
    function debugCombatUnitNames() {
      const els = [...document.querySelectorAll('[class*="CombatUnit_name"]')];
      console.log(
        `[KikiMeter] ${els.length} 个元素 [class*="CombatUnit_name"](原始、未过滤):`
      );
      console.log("=====================================================");
      els.forEach((el2, i) => {
        console.log(
          `  #${i} | 类名="${el2.className}" | 文本="${el2.textContent.trim()}" | 可见=${el2.offsetParent !== null}`
        );
      });
      console.log("=====================================================");
      if (els.length === 0) {
        console.warn(
          "[KikiMeter] 没有找到元素:可能已离开试炼页面,或者游戏 CSS 类名已变化。请停留在试炼进行中页面并再次运行命令。"
        );
      }
      return els;
    }
    function scanGuildNamesByEllipsis() {
      const out = [];
      document.querySelectorAll("*").forEach((el2) => {
        if (isOwnUI(el2)) return;
        if (el2.children.length > 1) return;
        const t21 = el2.textContent.trim();
        if (!t21 || t21.length < 2 || t21.length > 40) return;
        const literalEllipsis = /(\.\.\.|…)$/.test(t21);
        let cssEllipsis = false;
        if (!literalEllipsis && t21.length <= 20 && !t21.includes(" ") && !looksLikeNoise(t21)) {
          try {
            const cs = getComputedStyle(el2);
            cssEllipsis = cs.textOverflow === "ellipsis" && cs.overflow !== "visible";
          } catch (e) {
          }
        }
        if (literalEllipsis || cssEllipsis) {
          out.push({ el: el2, mode: literalEllipsis ? "文本" : "CSS" });
        }
      });
      console.log(
        `[KikiMeter] ${out.length} 个被省略显示的元素(文本省略号或 CSS ellipsis):`
      );
      console.log("=====================================================");
      out.forEach(({ el: el2, mode }, i) => {
        const parent = el2.parentElement;
        console.log(
          `  #${i} [${mode}] | 文本="${el2.textContent.trim()}" | 类名="${el2.className}" | 父级类名="${parent ? parent.className : "?"}" | 父级子元素=${parent ? parent.children.length : 0} | 父级同级元素=${parent && parent.parentElement ? parent.parentElement.children.length : 0}`
        );
      });
      console.log("=====================================================");
      return out;
    }
    function countMiniUnitNames() {
      const els = [...document.querySelectorAll('[class*="MiniUnit_name"]')];
      console.log(
        `[KikiMeter] ${els.length} 个元素 [class*="MiniUnit_name"](预计人数约 ${guildMaxSlot || "?"}) :`
      );
      console.log("=====================================================");
      els.forEach((el2, i) => {
        console.log(`  #${i} | 文本="${el2.textContent.trim()}"`);
      });
      console.log("=====================================================");
      return els.map((el2) => el2.textContent.trim());
    }
    function scanForOutlierMiniUnit() {
      const nameEls = [...document.querySelectorAll('[class*="MiniUnit_name"]')];
      if (nameEls.length === 0) {
        console.warn(
          "[KikiMeter] 没有找到 MiniUnit 单元,请确认公会试炼正在进行。"
        );
        return [];
      }
      const cells = nameEls.map(
        (el2) => el2.closest('[class*="MiniUnit_miniUnit"]') || el2.parentElement
      );
      const classSets = cells.map((c) => (c.className || "") + "");
      const counts = {};
      classSets.forEach((c) => {
        counts[c] = (counts[c] || 0) + 1;
      });
      const majority = Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];
      const outliers = [];
      cells.forEach((c, i) => {
        if (classSets[i] !== majority) {
          outliers.push({
            index: i,
            name: nameEls[i].textContent.trim(),
            classes: classSets[i]
          });
        }
      });
      console.log(
        `[KikiMeter] 主流类名(${counts[majority]}/${cells.length} 个单元) : "${majority}"`
      );
      console.log(
        `[KikiMeter] ${outliers.length} 个异常单元(可能是本地玩家):`
      );
      console.log("=====================================================");
      outliers.forEach((o) => {
        console.log(`  #${o.index} | 姓名="${o.name}" | 类名="${o.classes}"`);
      });
      console.log("=====================================================");
      if (outliers.length === 0) {
        console.warn(
          "[KikiMeter] 未发现异常单元;当前服务器可能没有本地玩家高亮,或者所有单元的样式完全相同。请观察角色单元是否存在边框或高亮。"
        );
      } else if (outliers.length > 1) {
        console.warn(
          "[KikiMeter] 发现多个异常单元,仅凭类名无法确定本地玩家,请与页面显示进行比对。"
        );
      }
      return outliers;
    }
    return {
      handleMessage: handleMessage2,
      bus,
      isGuildBattle: () => isGuildBattle,
      isGuildBattleActive: () => isGuildBattle && !guildTrialEnded,
      isInLabyrinth: () => isInLabyrinth,
      getCharacterId: () => currentCharacterId2,
      getCombatKey: () => currentCombatKey,
      testHandleMessage: handleMessage2,
      testSetNow(value) {
        testNow = Number.isFinite(value) ? Number(value) : null;
      },
      scanGuildNames,
      scanGuildNamesAttrs,
      scanGuildNamesLoose,
      scanGuildNamesByLocalName,
      previewGuildNames,
      debugCombatUnitNames,
      scanGuildNamesByEllipsis,
      countMiniUnitNames,
      scanForOutlierMiniUnit
    };
  })();

  // src/features/dps/30-history.js
  var langText = (zh, en) => Settings.getLanguage() === "en" ? en : zh;
  var UNATTRIBUTED_EPSILON = 1e-4;
  function withUnattributedDamage(players, teamDamage, elapsed) {
    const attributed = players.reduce(
      (sum, player) => sum + (Number(player.damage) || 0),
      0
    ), damage = Math.max(0, (Number(teamDamage) || 0) - attributed);
    if (!(damage > UNATTRIBUTED_EPSILON)) return players;
    return [
      ...players,
      {
        name: langText("无法归属伤害", "Unattributed Damage"),
        synthetic: "unattributed-damage",
        classId: "unknown",
        damage,
        dps: elapsed > 0 ? damage / elapsed : 0,
        healing: 0,
        hps: 0,
        taken: 0,
        takenPs: 0,
        kills: 0,
        accuracy: null,
        breakdown: null,
        takenBreakdown: null
      }
    ];
  }
  var HistoryStore = /* @__PURE__ */ (() => {
    const KEY = "kikimeter:history:v2", LEGACY_KEY = "kikimeter:history:v1", ACTIVE_KEY = "kikimeter:active:v2";
    const MAX_PER_TYPE = 10;
    let revision = 0;
    function validArray(raw) {
      try {
        const v = JSON.parse(raw || "[]");
        return Array.isArray(v) ? v : [];
      } catch (e) {
        return [];
      }
    }
    function migrateLegacy() {
      if (localStorage.getItem(KEY) !== null) return;
      const migrated = validArray(localStorage.getItem(LEGACY_KEY)).map(
        (e, i) => ({
          ...e,
          schemaVersion: 2,
          id: "legacy-" + (e.date || i),
          type: e.type || "combat",
          characterId: "legacy",
          combatKey: "legacy-" + i,
          startedAt: e.date,
          endedAt: e.date,
          durationMs: (e.durationSeconds || 0) * 1e3,
          fragments: [
            {
              reason: "旧版记录",
              startedAt: e.date,
              endedAt: e.date,
              durationMs: (e.durationSeconds || 0) * 1e3,
              teamDamage: e.teamDamage || 0
            }
          ]
        })
      );
      try {
        localStorage.setItem(KEY, JSON.stringify(migrated));
      } catch (e) {
      }
    }
    function entryKey(entry, index = 0) {
      return String(
        entry && (entry.id || entry.combatKey || entry.date || entry.startedAt) || "history-" + index
      );
    }
    function trim(entries) {
      const counts = { combat: 0, labyrinth: 0, trial: 0 };
      return entries.filter((entry) => {
        if (entry && entry.favorite === true) return true;
        const type = entry && entry.type || "combat";
        counts[type] = (counts[type] || 0) + 1;
        return counts[type] <= MAX_PER_TYPE;
      });
    }
    function load() {
      migrateLegacy();
      const raw = validArray(localStorage.getItem(KEY)), data = trim(raw);
      if (data.length !== raw.length) {
        try {
          localStorage.setItem(KEY, JSON.stringify(data));
        } catch (e) {
        }
      }
      return data;
    }
    function writeWithQuotaRetry(entries) {
      let data = trim(entries);
      while (data.length >= 0) {
        try {
          localStorage.setItem(KEY, JSON.stringify(data));
          return true;
        } catch (e) {
          const idx = [...data].reverse().findIndex((x) => x && x.favorite !== true);
          if (idx < 0) return false;
          data.splice(data.length - 1 - idx, 1);
        }
      }
      return false;
    }
    function save(entry) {
      const type = entry.type || "combat";
      entry.type = type;
      const all = load(), key = entryKey(entry), previous = all.find((item, index) => entryKey(item, index) === key);
      if (previous && previous.favorite === true && entry.favorite !== false)
        entry.favorite = true;
      const h = all.filter((item, index) => entryKey(item, index) !== key);
      h.unshift(entry);
      writeWithQuotaRetry(h);
      revision++;
    }
    function updateEntry(id, updater) {
      const all = load(), index = all.findIndex((entry, i) => entryKey(entry, i) === String(id));
      if (index < 0) return false;
      const next = updater({ ...all[index] });
      if (next === null) all.splice(index, 1);
      else all[index] = next;
      const ok = writeWithQuotaRetry(all);
      if (ok) revision++;
      return ok;
    }
    return {
      push(entry) {
        save(entry);
      },
      getAll(type) {
        const all = load();
        if (!type) return all;
        return all.filter((e) => (e.type || "combat") === type);
      },
      clear(type) {
        if (!type) {
          try {
            localStorage.removeItem(KEY);
          } catch (e) {
          }
          revision++;
          return;
        }
        const remaining = load().filter((e) => (e.type || "combat") !== type);
        try {
          localStorage.setItem(KEY, JSON.stringify(remaining));
        } catch (e) {
        }
        revision++;
      },
      setFavorite(id, value) {
        return updateEntry(id, (entry) => ({
          ...entry,
          favorite: !!value,
          favoritedAt: value ? (/* @__PURE__ */ new Date()).toISOString() : void 0
        }));
      },
      rename(id, name) {
        return updateEntry(id, (entry) => {
          const customName = String(name || "").trim().slice(0, 40);
          return { ...entry, customName: customName || void 0 };
        });
      },
      remove(id) {
        return updateEntry(id, () => null);
      },
      entryKey,
      saveActive(snapshot) {
        try {
          localStorage.setItem(ACTIVE_KEY, JSON.stringify(snapshot));
          return true;
        } catch (e) {
          return false;
        }
      },
      loadActive() {
        try {
          const v = JSON.parse(localStorage.getItem(ACTIVE_KEY) || "null");
          return v && v.schemaVersion === 2 ? v : null;
        } catch (e) {
          return null;
        }
      },
      clearActive() {
        try {
          localStorage.removeItem(ACTIVE_KEY);
        } catch (e) {
        }
      },
      getRevision: () => revision,
      keys: { history: KEY, active: ACTIVE_KEY }
    };
  })();
  var SegmentSelection = (() => {
    let selectedKey = "current";
    let cachedRevision2 = -1, cachedLanguage = "", cachedOptions = [];
    const bus = new EventTarget();
    const fightKey = (id) => "fight:" + encodeURIComponent(String(id));
    const fragmentKey = (id, index) => "fragment:" + encodeURIComponent(String(id)) + ":" + index;
    function entryId(entry, index) {
      return entry.id || entry.combatKey || entry.date || "history-" + index;
    }
    function dateLabel(entry) {
      const d = new Date(entry.date || entry.startedAt || Date.now());
      const pad = (value) => String(value).padStart(2, "0");
      const type = entry.type || "combat";
      const typeLabel = Settings.getLanguage() === "en" ? { combat: "Combat", labyrinth: "Labyrinth", trial: "Trial" }[type] || "Combat" : { combat: "普通", labyrinth: "迷宫", trial: "试炼" }[type] || "普通";
      const count = (entry.players || []).length;
      return Settings.getLanguage() === "en" ? `${typeLabel} ${count} players ${d.getMonth() + 1}/${d.getDate()} ${pad(d.getHours())}:${pad(d.getMinutes())}` : `${typeLabel} ${count}人 ${d.getMonth() + 1}月${d.getDate()}日${pad(d.getHours())}:${pad(d.getMinutes())}`;
    }
    function options() {
      const revision = HistoryStore.getRevision(), language = Settings.getLanguage();
      if (revision === cachedRevision2 && language === cachedLanguage)
        return cachedOptions;
      const out = [
        {
          key: "current",
          label: langText("当前战斗", "Current combat"),
          current: true
        }
      ];
      const ordered = HistoryStore.getAll().map((entry, index) => ({ entry, index })).sort((left, right) => {
        const favoriteDiff = Number(right.entry.favorite === true) - Number(left.entry.favorite === true);
        if (favoriteDiff) return favoriteDiff;
        if (left.entry.favorite === true && right.entry.favorite === true) {
          const timeDiff = new Date(right.entry.favoritedAt || right.entry.date || 0) - new Date(left.entry.favoritedAt || left.entry.date || 0);
          if (timeDiff) return timeDiff;
        }
        const typeOrder = { combat: 0, labyrinth: 1, trial: 2 };
        const typeDiff = (typeOrder[left.entry.type || "combat"] ?? 9) - (typeOrder[right.entry.type || "combat"] ?? 9);
        if (typeDiff) return typeDiff;
        return left.index - right.index;
      });
      ordered.forEach(({ entry, index: entryIndex }) => {
        const id = entryId(entry, entryIndex);
        const favorite = entry.favorite === true, group = favorite ? "favorite" : entry.type || "combat";
        const displayName = favorite && entry.customName ? entry.customName : dateLabel(entry);
        out.push({
          key: fightKey(id),
          label: (favorite ? "★ " : "") + displayName,
          entry,
          group,
          favorite
        });
        const parts = Array.isArray(entry.fragments) ? entry.fragments : [];
        if (parts.length > 1)
          parts.forEach(
            (fragment, index) => out.push({
              key: fragmentKey(id, index),
              label: langText("↳ 重连片段 ", "↳ Reconnect fragment ") + (index + 1) + " · " + formatDuration2((Number(fragment.durationMs) || 0) / 1e3),
              entry,
              fragment,
              fragmentIndex: index,
              group,
              favorite
            })
          );
      });
      cachedRevision2 = revision;
      cachedLanguage = language;
      cachedOptions = out;
      return cachedOptions;
    }
    function resolve() {
      const all = options();
      const found = all.find((x) => x.key === selectedKey);
      if (found) return found;
      selectedKey = "current";
      return all[0];
    }
    function select(key) {
      const next = options().some((x) => x.key === key) ? key : "current";
      if (next === selectedKey) return resolve();
      selectedKey = next;
      const selected = resolve();
      bus.dispatchEvent(new CustomEvent("change", { detail: selected }));
      return selected;
    }
    return {
      bus,
      options,
      resolve,
      select,
      getKey: () => selectedKey,
      isCurrent: () => selectedKey === "current"
    };
  })();
  function closeSegmentPicker(picker) {
    if (picker && picker._menu) {
      picker._menu.remove();
      picker._menu = null;
    }
    if (picker && Array.isArray(picker._sideMenus)) {
      picker._sideMenus.forEach((side) => side.remove());
      picker._sideMenus = [];
    }
    if (picker && picker._button)
      picker._button.style.borderColor = "rgba(212,175,55,.45)";
  }
  function buildSegmentMenu(picker) {
    closeSegmentPicker(picker);
    if (!(picker._collapsedGroups instanceof Set))
      picker._collapsedGroups = /* @__PURE__ */ new Set();
    if (!(picker._expandedEntries instanceof Set))
      picker._expandedEntries = /* @__PURE__ */ new Set();
    const menu = el("div", {
      position: "fixed",
      zIndex: "10003",
      width: "300px",
      maxHeight: "min(390px,72vh)",
      overflowY: "auto",
      padding: "4px",
      boxSizing: "border-box",
      background: "rgba(14,14,14,.995)",
      border: "1px solid rgba(212,175,55,.55)",
      borderRadius: "4px",
      boxShadow: "0 7px 24px rgba(0,0,0,.85)",
      color: "#f2f2f2",
      fontSize: "11px"
    });
    menu.id = "kikimeter-segment-menu";
    menu.dataset.kikimeter = "true";
    picker._menu = menu;
    const notify = () => {
      refreshSegmentSelect(picker);
      if (picker._onChanged) picker._onChanged();
    };
    const options = SegmentSelection.options();
    const addRecord = (item, fragment = false) => {
      const row = el("div", {
        display: "flex",
        alignItems: "center",
        minHeight: "28px",
        padding: fragment ? "4px 6px 4px 18px" : "4px 6px",
        boxSizing: "border-box",
        borderRadius: "3px",
        cursor: "pointer",
        whiteSpace: "nowrap"
      });
      row.dataset.segmentKey = item.key;
      const id = item.entry ? HistoryStore.entryKey(item.entry) : "";
      const fragments = !fragment && item.entry ? options.filter(
        (candidate) => candidate.fragment && candidate.entry === item.entry
      ) : [];
      if (fragments.length) {
        const expanded = picker._expandedEntries.has(id), disclosure = el("button", {
          width: "18px",
          height: "20px",
          padding: "0",
          marginRight: "2px",
          flexShrink: "0",
          cursor: "pointer",
          background: "transparent",
          border: "none",
          color: "rgba(255,255,255,.62)",
          fontSize: "11px"
        });
        disclosure.type = "button";
        disclosure.textContent = expanded ? "▾" : "▸";
        disclosure.title = expanded ? langText("收起重连片段", "Collapse reconnect fragments") : langText("展开重连片段", "Expand reconnect fragments");
        disclosure.addEventListener("click", (event) => {
          event.stopPropagation();
          expanded ? picker._expandedEntries.delete(id) : picker._expandedEntries.add(id);
          buildSegmentMenu(picker);
        });
        row.appendChild(disclosure);
      }
      const label = el("span", {
        overflow: "hidden",
        textOverflow: "ellipsis",
        flex: "1"
      });
      label.textContent = item.label;
      row.appendChild(label);
      row.addEventListener("mouseenter", () => {
        row.style.background = "rgba(212,175,55,.13)";
      });
      row.addEventListener("mouseleave", () => {
        row.style.background = "transparent";
      });
      row.addEventListener("click", () => {
        SegmentSelection.select(item.key);
        closeSegmentPicker(picker);
        notify();
      });
      if (item.entry && !fragment) {
        const miniButton = (text, title, color, handler) => {
          const button = document.createElement("button");
          button.type = "button";
          button.textContent = text;
          button.title = title;
          Object.assign(button.style, {
            width: "23px",
            height: "22px",
            padding: "0",
            marginLeft: "2px",
            cursor: "pointer",
            background: "transparent",
            border: "none",
            borderRadius: "3px",
            color,
            fontSize: "14px",
            lineHeight: "20px",
            flexShrink: "0"
          });
          button.addEventListener(
            "mouseenter",
            () => button.style.background = "rgba(255,255,255,.12)"
          );
          button.addEventListener(
            "mouseleave",
            () => button.style.background = "transparent"
          );
          button.addEventListener("click", (event) => {
            event.stopPropagation();
            if (handler(event) !== false) {
              notify();
              buildSegmentMenu(picker);
            }
          });
          return button;
        };
        const star = miniButton(
          item.entry.favorite === true ? "★" : "☆",
          item.entry.favorite === true ? langText("取消收藏", "Remove favorite") : langText("收藏", "Favorite"),
          "#facc15",
          () => HistoryStore.setFavorite(id, item.entry.favorite !== true)
        );
        const rename = item.entry.favorite === true ? miniButton(
          "✎",
          langText("修改收藏名称", "Rename favorite"),
          "#93c5fd",
          () => {
            const defaultName = item.entry.customName || String(item.label || "").replace(/^★\s*/, "");
            const input = document.createElement("input");
            input.type = "text";
            input.value = defaultName;
            input.maxLength = 40;
            Object.assign(input.style, {
              minWidth: "0",
              height: "22px",
              flex: "1",
              boxSizing: "border-box",
              padding: "2px 5px",
              background: "#090909",
              border: "1px solid #93c5fd",
              borderRadius: "3px",
              outline: "none",
              color: "#fff",
              font: "inherit"
            });
            let finished = false;
            const finish = (save) => {
              if (finished) return;
              finished = true;
              if (save) HistoryStore.rename(id, input.value);
              notify();
              buildSegmentMenu(picker);
            };
            input.addEventListener(
              "mousedown",
              (event) => event.stopPropagation()
            );
            input.addEventListener(
              "click",
              (event) => event.stopPropagation()
            );
            input.addEventListener("keydown", (event) => {
              event.stopPropagation();
              if (event.key === "Enter") {
                event.preventDefault();
                finish(true);
              } else if (event.key === "Escape") {
                event.preventDefault();
                finish(false);
              }
            });
            input.addEventListener("blur", () => finish(true), {
              once: true
            });
            label.replaceWith(input);
            input.focus();
            input.select();
            return false;
          }
        ) : null;
        const remove = miniButton(
          "✕",
          langText("删除记录", "Delete record"),
          "#f87171",
          () => {
            const selected = SegmentSelection.resolve();
            if (selected.entry && HistoryStore.entryKey(selected.entry) === id)
              SegmentSelection.select("current");
            HistoryStore.remove(id);
          }
        );
        if (rename) row.append(rename);
        row.append(star, remove);
      }
      menu.appendChild(row);
    };
    const current = options.find((item) => item.current);
    if (current) addRecord(current);
    [
      ["favorite", langText("收藏", "Favorites")],
      ["combat", langText("普通", "Combat")],
      ["labyrinth", langText("迷宫", "Labyrinth")],
      ["trial", langText("试炼", "Trial")]
    ].forEach(([group, title]) => {
      const records = options.filter(
        (item) => !item.current && item.group === group
      );
      if (!records.length) return;
      const count = records.filter((item) => !item.fragment).length, collapsed = picker._collapsedGroups.has(group);
      const heading = el("div", {
        display: "flex",
        alignItems: "center",
        gap: "5px",
        padding: "7px 7px 4px",
        color: ACCENT,
        fontSize: "10px",
        fontWeight: "700",
        borderTop: "1px solid rgba(255,255,255,.08)",
        cursor: "pointer",
        userSelect: "none"
      });
      heading.textContent = (collapsed ? "▸ " : "▾ ") + title + "(" + count + ")";
      heading.title = collapsed ? langText(`展开${title}`, `Expand ${title}`) : langText(`折叠${title}`, `Collapse ${title}`);
      heading.addEventListener("click", (event) => {
        event.stopPropagation();
        collapsed ? picker._collapsedGroups.delete(group) : picker._collapsedGroups.add(group);
        buildSegmentMenu(picker);
      });
      menu.appendChild(heading);
      if (!collapsed)
        records.filter((item) => !item.fragment).forEach((item) => {
          addRecord(item);
          if (picker._expandedEntries.has(HistoryStore.entryKey(item.entry)))
            records.filter(
              (fragment) => fragment.fragment && fragment.entry === item.entry
            ).forEach((fragment) => addRecord(fragment, true));
        });
    });
    document.body.appendChild(menu);
    const rect = picker.getBoundingClientRect(), menuHeight = menu.offsetHeight || 300;
    const top = rect.bottom + 3 + menuHeight <= window.innerHeight ? rect.bottom + 3 : Math.max(4, rect.top - menuHeight - 3);
    const left = Math.max(4, Math.min(rect.right - 300, window.innerWidth - 304));
    Object.assign(menu.style, { left: left + "px", top: top + "px" });
    picker._button.style.borderColor = ACCENT;
  }
  function buildSegmentPicker(onChanged, compact = false) {
    const picker = el("div", {
      position: "relative",
      minWidth: "0",
      flex: compact ? "none" : "1"
    });
    picker.dataset.kikimeter = "true";
    picker.dataset.segmentPicker = "true";
    picker._compact = compact;
    const button = document.createElement("button");
    button.type = "button";
    Object.assign(button.style, {
      display: "flex",
      alignItems: "center",
      justifyContent: "space-between",
      gap: "6px",
      width: "100%",
      minWidth: "0",
      background: "#121212",
      color: "#f2f2f2",
      border: "1px solid rgba(212,175,55,.45)",
      borderRadius: "3px",
      padding: "4px 6px",
      fontSize: "11px",
      cursor: "pointer"
    });
    const textEl = el("span", {
      overflow: "hidden",
      textOverflow: "ellipsis",
      whiteSpace: "nowrap"
    }), arrow = el("span", { opacity: ".65" });
    arrow.textContent = "▾";
    if (compact) {
      Object.assign(button.style, {
        width: "25px",
        height: "23px",
        padding: "0",
        justifyContent: "center",
        background: "transparent",
        borderColor: "transparent"
      });
      picker._refreshIcon = () => {
        const source = TOOLBAR_ICONS.history;
        if (!source) return false;
        const historyIcon = iconElement(source, "");
        Object.assign(historyIcon.style, {
          width: "17px",
          height: "17px",
          objectFit: "contain",
          pointerEvents: "none"
        });
        textEl.replaceChildren(historyIcon);
        return true;
      };
      picker._refreshIcon();
      button.appendChild(textEl);
    } else button.append(textEl, arrow);
    picker.appendChild(button);
    picker._button = button;
    picker._text = textEl;
    picker._onChanged = onChanged;
    button.addEventListener("mousedown", (event) => event.stopPropagation());
    button.addEventListener("click", (event) => {
      event.stopPropagation();
      picker._menu ? closeSegmentPicker(picker) : buildSegmentMenu(picker);
    });
    document.addEventListener("pointerdown", (event) => {
      if (picker._menu && !picker.contains(event.target) && !picker._menu.contains(event.target))
        closeSegmentPicker(picker);
    });
    refreshSegmentSelect(picker);
    return picker;
  }
  function refreshSegmentSelect(picker) {
    if (!picker || !picker._text) return;
    const selected = SegmentSelection.resolve();
    picker.value = selected.key;
    if (!picker._compact) picker._text.textContent = selected.label;
    const english = Settings.getLanguage() === "en";
    picker.title = (selected.current ? english ? "Select combat record" : "选择战斗记录" : (english ? "Viewing: " : "正在查看:") + selected.label) + (picker._compact ? english ? " | Open combat history" : "|点击展开历史" : "");
  }
  var ViewData = /* @__PURE__ */ (() => {
    function graphPoints(graph) {
      const damage = graph && Array.isArray(graph.damage) ? graph.damage : [];
      const boss = graph && Array.isArray(graph.boss) ? graph.boss : [];
      return damage.map((value, index) => ({
        dps: (Number(value) || 0) / 2,
        isBoss: !!boss[index]
      }));
    }
    function damageBreakdown(raw, total, elapsed, playerName = "") {
      const merged = /* @__PURE__ */ new Map();
      Object.entries(raw || {}).forEach(([source, value]) => {
        const canonical = DamageSources.canonical(source, playerName), amount = Number(value) || 0;
        if (amount <= 0) return;
        const item = merged.get(canonical) || {
          value: 0,
          displaySource: canonical
        };
        item.value += amount;
        if (DamageSources.isCombined(source)) item.displaySource = source;
        merged.set(canonical, item);
      });
      const sources = [...merged].map(([source, item]) => ({
        source: item.displaySource,
        label: DamageSources.label(item.displaySource),
        value: item.value
      }));
      const recorded = sources.reduce((sum, item) => sum + item.value, 0), missing = Math.max(0, (Number(total) || 0) - recorded);
      if (missing > 1e-4)
        sources.push({
          source: "legacy",
          label: DamageSources.label("legacy"),
          value: missing
        });
      return sources.sort((a, b) => b.value - a.value).map((item) => ({
        ...item,
        ps: elapsed > 0 ? item.value / elapsed : 0,
        pct: total > 0 ? item.value * 100 / total : 0
      }));
    }
    function takenBreakdown(raw, total, elapsed) {
      const sources = Object.entries(raw || {}).map(([source, value]) => ({
        source,
        label: TakenSources.label(source),
        icon: TakenSources.icon(source),
        value: Number(value) || 0
      })).filter((item) => item.value > 0);
      const recorded = sources.reduce((sum, item) => sum + item.value, 0), missing = Math.max(0, (Number(total) || 0) - recorded);
      if (missing > 1e-4)
        sources.push({
          source: "",
          label: Settings.getLanguage() === "en" ? "Legacy / Unknown Source" : "旧记录/未知来源",
          icon: DamageSources.icon("unknown"),
          value: missing
        });
      return sources.sort((a, b) => b.value - a.value).map((item) => ({
        ...item,
        ps: elapsed > 0 ? item.value / elapsed : 0,
        pct: total > 0 ? item.value * 100 / total : 0
      }));
    }
    function accuracyBreakdown(raw) {
      const attempts = Number(raw && raw.attempts) || 0;
      if (!(attempts > 0)) return null;
      const hits = Math.max(0, Math.min(attempts, Number(raw && raw.hits) || 0));
      const monsters = Object.values(raw && raw.monsters || {}).map((monster) => {
        const monsterAttempts = Number(monster && monster.attempts) || 0, monsterHits = Math.max(
          0,
          Math.min(monsterAttempts, Number(monster && monster.hits) || 0)
        ), monsterHrid = String(monster && monster.monsterHrid || ""), monsterName2 = String(monster && monster.monsterName || ""), localized = monsterHrid ? getLocalizedEntityName("monster", monsterHrid) : "";
        return {
          monsterName: localized || monsterName2 || (Settings.getLanguage() === "en" ? "Unknown Monster" : "未知怪物"),
          monsterHrid,
          attempts: monsterAttempts,
          hits: monsterHits,
          pct: monsterAttempts > 0 ? monsterHits * 100 / monsterAttempts : 0
        };
      }).filter((monster) => monster.attempts > 0).sort(
        (a, b) => b.attempts - a.attempts || b.pct - a.pct || a.monsterName.localeCompare(b.monsterName)
      );
      return { attempts, hits, pct: hits * 100 / attempts, monsters };
    }
    function current() {
      const elapsed = Session.getElapsedSeconds(), names = Session.getAllPlayerNames(), teamDamage = Session.getTeamDamage(), players = names.map((name) => ({
        name,
        classId: ClassSystem.classFor(name),
        damage: Session.getPlayerDamage(name),
        dps: Session.getPlayerDps(name),
        healing: Session.getPlayerHealing(name),
        hps: Session.getPlayerHps(name),
        taken: Session.getPlayerTaken(name),
        takenPs: Session.getPlayerTakenPs(name),
        kills: Session.getPlayerKills(name),
        accuracy: accuracyBreakdown(Session.getPlayerAccuracy(name)),
        breakdown: damageBreakdown(
          Session.getPlayerDamageSources(name),
          Session.getPlayerDamage(name),
          elapsed,
          name
        ),
        takenBreakdown: takenBreakdown(
          Session.getPlayerTakenSources(name),
          Session.getPlayerTaken(name),
          elapsed
        )
      }));
      return {
        key: "current",
        label: langText("当前战斗", "Current combat"),
        current: true,
        type: Session.getMeta().type || "combat",
        elapsed,
        teamDamage,
        teamDps: Session.getTeamDps(),
        teamKills: Session.getTeamKills(),
        fragmentCount: Session.getFragments().length,
        graphPoints: Session.getFullGraphPoints(),
        players: withUnattributedDamage(players, teamDamage, elapsed)
      };
    }
    function historical(selected) {
      const entry = selected.entry || {}, fragment = selected.fragment;
      const elapsed = fragment ? (Number(fragment.durationMs) || 0) / 1e3 : Number(entry.durationSeconds) || (Number(entry.durationMs) || 0) / 1e3;
      let players;
      if (fragment) {
        const maps = fragment.players || {}, damage = maps.damage || {}, healing = maps.healing || {}, taken = maps.taken || {}, kills = maps.kills || {}, accuracy = maps.accuracy || {};
        const names = [
          .../* @__PURE__ */ new Set([
            ...Object.keys(damage),
            ...Object.keys(healing),
            ...Object.keys(taken),
            ...Object.keys(kills),
            ...Object.keys(accuracy)
          ])
        ];
        const sources = maps.sources || {}, takenSources = maps.takenSources || {};
        players = names.map((name) => ({
          name,
          classId: (entry.classes || {})[name],
          damage: Number(damage[name]) || 0,
          healing: Number(healing[name]) || 0,
          taken: Number(taken[name]) || 0,
          kills: Number(kills[name]) || 0,
          accuracy: accuracyBreakdown(accuracy[name]),
          dps: elapsed > 0 ? (Number(damage[name]) || 0) / elapsed : 0,
          hps: elapsed > 0 ? (Number(healing[name]) || 0) / elapsed : 0,
          takenPs: elapsed > 0 ? (Number(taken[name]) || 0) / elapsed : 0,
          breakdown: damageBreakdown(
            sources[name],
            Number(damage[name]) || 0,
            elapsed,
            name
          ),
          takenBreakdown: takenBreakdown(
            takenSources[name],
            Number(taken[name]) || 0,
            elapsed
          )
        }));
      } else {
        players = (entry.players || []).map((p) => ({
          ...p,
          accuracy: accuracyBreakdown(p.accuracy),
          takenPs: elapsed > 0 ? (Number(p.taken) || 0) / elapsed : 0,
          breakdown: damageBreakdown(
            p.sources,
            Number(p.damage) || 0,
            elapsed,
            p.name
          ),
          takenBreakdown: takenBreakdown(
            p.takenSources,
            Number(p.taken) || 0,
            elapsed
          )
        }));
      }
      players.forEach((p) => {
        if (p.classId) ClassSystem.setDetected(p.name, p.classId);
      });
      const teamDamage = fragment ? Number(fragment.teamDamage) || players.reduce((s, p) => s + (Number(p.damage) || 0), 0) : Number(entry.teamDamage) || 0;
      const teamKills = players.reduce((s, p) => s + (Number(p.kills) || 0), 0);
      return {
        key: selected.key,
        label: selected.label,
        current: false,
        type: entry.type || "combat",
        elapsed,
        teamDamage,
        teamDps: elapsed > 0 ? teamDamage / elapsed : 0,
        teamKills,
        fragmentCount: fragment ? 1 : (entry.fragments || []).length || 1,
        graphPoints: fragment ? [] : graphPoints(entry.graph),
        players: withUnattributedDamage(players, teamDamage, elapsed)
      };
    }
    function get() {
      const selected = SegmentSelection.resolve();
      return selected.current ? current() : historical(selected);
    }
    return { get };
  })();

  // src/features/dps/50-graph-components.js
  var BOSS_COLOR = "#FF3F34";
  var langText2 = (zh, en) => Settings.getLanguage() === "en" ? en : zh;
  function buildGraph() {
    const GRAPH_BUCKET_MS = 2e3;
    const canvas = document.createElement("canvas");
    const CW = 440, CH = 160;
    canvas.width = CW;
    canvas.height = CH;
    Object.assign(canvas.style, {
      width: "100%",
      height: "80px",
      display: "block",
      borderRadius: "4px",
      background: "rgba(0,0,0,0.3)"
    });
    const PAD = { top: 14, right: 8, bottom: 22, left: 50 };
    const DW = CW - PAD.left - PAD.right;
    const DH = CH - PAD.top - PAD.bottom;
    function niceInterval(maxVal, targetTicks) {
      if (maxVal <= 0) return 100;
      const raw = maxVal / targetTicks;
      const mag = Math.pow(10, Math.floor(Math.log10(raw)));
      const frac = raw / mag;
      let nice;
      if (frac < 1.5) nice = 1;
      else if (frac < 3.5) nice = 2;
      else if (frac < 7.5) nice = 5;
      else nice = 10;
      return nice * mag;
    }
    function render(points) {
      canvas.style.display = "block";
      const ctx = canvas.getContext("2d");
      ctx.clearRect(0, 0, CW, CH);
      const SMOOTH = 8;
      const smoothed = points.map((p, i) => {
        let sum = 0, count = 0;
        for (let k = Math.max(0, i - SMOOTH + 1); k <= i; k++) {
          sum += points[k].dps;
          count++;
        }
        return { dps: sum / count, isBoss: p.isBoss };
      });
      const N = smoothed.length;
      const max = Math.max(...smoothed.map((p) => p.dps), 1);
      const yInterval = niceInterval(max, 4);
      const yMax = Math.ceil(max / yInterval) * yInterval;
      function xOf(i) {
        return PAD.left + i / (N - 1) * DW;
      }
      function yOf(v) {
        return PAD.top + DH * (1 - v / yMax);
      }
      ctx.font = "18px monospace";
      ctx.textAlign = "right";
      for (let v = 0; v <= yMax; v += yInterval) {
        const y = yOf(v);
        ctx.beginPath();
        ctx.strokeStyle = "rgba(255,255,255,0.08)";
        ctx.lineWidth = 1;
        ctx.setLineDash([4, 4]);
        ctx.moveTo(PAD.left, y);
        ctx.lineTo(PAD.left + DW, y);
        ctx.stroke();
        ctx.setLineDash([]);
        ctx.fillStyle = "rgba(255,255,255,0.45)";
        const label = v >= 1e3 ? (v / 1e3).toFixed(v % 1e3 === 0 ? 0 : 1) + "k" : String(Math.round(v));
        ctx.fillText(label, PAD.left - 4, y + 6);
      }
      const totalMinutes = N * GRAPH_BUCKET_MS / 6e4;
      let stepMin;
      if (totalMinutes <= 6) stepMin = 1;
      else if (totalMinutes <= 15) stepMin = 2;
      else if (totalMinutes <= 30) stepMin = 5;
      else stepMin = 10;
      const bucketsPerStep = Math.round(stepMin * 6e4 / GRAPH_BUCKET_MS);
      ctx.font = "17px monospace";
      ctx.textAlign = "center";
      for (let step = 1; ; step++) {
        const bucketIdx = N - 1 - step * bucketsPerStep;
        if (bucketIdx < 0) break;
        const x = xOf(bucketIdx);
        ctx.beginPath();
        ctx.strokeStyle = "rgba(255,255,255,0.12)";
        ctx.lineWidth = 1;
        ctx.setLineDash([2, 4]);
        ctx.moveTo(x, PAD.top);
        ctx.lineTo(x, PAD.top + DH);
        ctx.stroke();
        ctx.setLineDash([]);
        ctx.fillStyle = "rgba(255,255,255,0.35)";
        ctx.fillText("-" + step * stepMin + "m", x, CH - 4);
      }
      ctx.textAlign = "right";
      ctx.fillStyle = "rgba(255,255,255,0.3)";
      ctx.fillText("now", CW - 2, CH - 4);
      let segColor = null;
      const coords = smoothed.map((p, i) => ({
        x: xOf(i),
        y: yOf(p.dps),
        isBoss: p.isBoss
      }));
      coords.forEach((pt, i) => {
        const color = pt.isBoss ? BOSS_COLOR : ACCENT;
        if (color !== segColor) {
          if (segColor !== null) ctx.stroke();
          ctx.beginPath();
          ctx.strokeStyle = color;
          ctx.lineWidth = 2.5;
          if (i > 0) ctx.moveTo(coords[i - 1].x, coords[i - 1].y);
          else ctx.moveTo(pt.x, pt.y);
          segColor = color;
        }
        ctx.lineTo(pt.x, pt.y);
      });
      if (segColor !== null) ctx.stroke();
      [false, true].forEach((bossFlag) => {
        ctx.fillStyle = bossFlag ? "rgba(255,63,52,0.12)" : "rgba(0,116,116,0.12)";
        ctx.beginPath();
        let started = false;
        coords.forEach((pt, i) => {
          if (pt.isBoss !== bossFlag) {
            if (started) {
              ctx.lineTo(pt.x, PAD.top + DH);
              ctx.closePath();
              ctx.fill();
              started = false;
            }
            return;
          }
          if (!started) {
            ctx.beginPath();
            ctx.moveTo(pt.x, PAD.top + DH);
            started = true;
          }
          ctx.lineTo(pt.x, pt.y);
        });
        if (started) {
          ctx.lineTo(coords[N - 1].x, PAD.top + DH);
          ctx.closePath();
          ctx.fill();
        }
      });
      ctx.beginPath();
      ctx.strokeStyle = "rgba(255,255,255,0.15)";
      ctx.lineWidth = 1;
      ctx.moveTo(PAD.left, PAD.top);
      ctx.lineTo(PAD.left, PAD.top + DH);
      ctx.lineTo(PAD.left + DW, PAD.top + DH);
      ctx.stroke();
    }
    return { canvas, render };
  }
  function buildDetailsGraph() {
    const GRAPH_BUCKET_MS = 2e3, CSS_HEIGHT = 112, canvas = document.createElement("canvas");
    Object.assign(canvas.style, {
      width: "100%",
      height: CSS_HEIGHT + "px",
      display: "block",
      borderRadius: "5px",
      boxSizing: "border-box",
      background: "linear-gradient(180deg,rgba(18,22,28,.94),rgba(6,8,12,.94))",
      border: "1px solid rgba(212,175,55,.24)"
    });
    function niceInterval(maxValue, targetTicks = 3) {
      if (maxValue <= 0) return 1;
      const raw = maxValue / targetTicks, mag = 10 ** Math.floor(Math.log10(raw)), fraction = raw / mag;
      return (fraction < 1.5 ? 1 : fraction < 3.5 ? 2 : fraction < 7.5 ? 5 : 10) * mag;
    }
    function traceSmooth(ctx, coords) {
      if (!coords.length) return;
      ctx.moveTo(coords[0].x, coords[0].y);
      for (let index = 1; index < coords.length; index++) {
        const previous = coords[index - 1], point = coords[index], middle = (previous.x + point.x) / 2;
        ctx.bezierCurveTo(middle, previous.y, middle, point.y, point.x, point.y);
      }
    }
    function durationLabel(seconds, english) {
      if (seconds >= 60)
        return "-" + Math.round(seconds / 60) + (english ? "m" : "分");
      return "-" + Math.round(seconds) + (english ? "s" : "秒");
    }
    function render(rawPoints) {
      const points = Array.isArray(rawPoints) ? rawPoints : [], english = Settings.getLanguage() === "en";
      const width = Math.max(
        240,
        Math.round(canvas.getBoundingClientRect().width || 320)
      ), height = CSS_HEIGHT;
      const ratio = Math.max(
        1,
        Math.min(Number(window.devicePixelRatio) || 1, 2)
      );
      if (canvas.width !== Math.round(width * ratio) || canvas.height !== Math.round(height * ratio)) {
        canvas.width = Math.round(width * ratio);
        canvas.height = Math.round(height * ratio);
      }
      const ctx = canvas.getContext("2d");
      ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
      ctx.clearRect(0, 0, width, height);
      canvas.title = english ? "Smoothed total team DPS trend; red areas indicate boss combat." : "平滑后的团队总 DPS 趋势;红色区段表示 Boss 战斗。";
      const padding = { top: 27, right: 10, bottom: 19, left: 39 }, drawWidth = width - padding.left - padding.right, drawHeight = height - padding.top - padding.bottom;
      ctx.font = "600 9px 'Microsoft YaHei','Noto Sans SC',sans-serif";
      ctx.textBaseline = "middle";
      ctx.fillStyle = "rgba(255,255,255,.72)";
      ctx.textAlign = "left";
      ctx.fillText(english ? "TEAM DPS TREND" : "团队 DPS 趋势", 10, 13);
      if (!points.length) {
        ctx.fillStyle = "rgba(255,255,255,.34)";
        ctx.textAlign = "center";
        ctx.font = "10px 'Microsoft YaHei','Noto Sans SC',sans-serif";
        ctx.fillText(
          english ? "Waiting for combat data" : "等待战斗数据",
          width / 2,
          62
        );
        return;
      }
      const smoothWindow = Math.max(
        2,
        Math.min(6, Math.round(points.length / 20) || 2)
      );
      const smoothed = points.map((point, index) => {
        let weighted = 0, weightTotal = 0;
        for (let i = Math.max(0, index - smoothWindow + 1); i <= index; i++) {
          const weight = i - (index - smoothWindow);
          weighted += (Number(points[i].dps) || 0) * weight;
          weightTotal += weight;
        }
        return {
          dps: weightTotal ? weighted / weightTotal : 0,
          isBoss: !!point.isBoss
        };
      });
      const count = smoothed.length, peakValue = Math.max(...smoothed.map((point) => point.dps), 0), scalePeak = Math.max(peakValue, 1);
      const interval = niceInterval(scalePeak), yMax = Math.max(interval, Math.ceil(scalePeak / interval) * interval);
      const xOf = (index) => count <= 1 ? padding.left + drawWidth : padding.left + index / (count - 1) * drawWidth;
      const yOf = (value) => padding.top + drawHeight * (1 - Math.max(0, value) / yMax);
      ctx.font = "8px 'Microsoft YaHei','Noto Sans SC',sans-serif";
      ctx.textAlign = "right";
      ctx.textBaseline = "middle";
      for (let value = 0; value <= yMax + 1e-4; value += interval) {
        const y = yOf(value);
        ctx.beginPath();
        ctx.strokeStyle = "rgba(255,255,255,.075)";
        ctx.lineWidth = 1;
        ctx.moveTo(padding.left, y + 0.5);
        ctx.lineTo(width - padding.right, y + 0.5);
        ctx.stroke();
        ctx.fillStyle = "rgba(255,255,255,.38)";
        ctx.fillText(formatDamage(value), padding.left - 5, y);
      }
      const totalSeconds = Math.max(0, (count - 1) * GRAPH_BUCKET_MS / 1e3);
      ctx.textAlign = "center";
      ctx.textBaseline = "bottom";
      ctx.fillStyle = "rgba(255,255,255,.32)";
      [0, 0.5, 1].forEach((fraction) => {
        const x = padding.left + drawWidth * fraction;
        if (fraction > 0 && fraction < 1) {
          ctx.beginPath();
          ctx.strokeStyle = "rgba(255,255,255,.045)";
          ctx.moveTo(x, padding.top);
          ctx.lineTo(x, padding.top + drawHeight);
          ctx.stroke();
        }
        const ago = totalSeconds * (1 - fraction), label = fraction === 1 ? english ? "NOW" : "现在" : durationLabel(ago, english);
        ctx.fillText(label, x, height - 3);
      });
      let bossStart = -1;
      for (let index = 0; index <= count; index++) {
        const boss = index < count && smoothed[index].isBoss;
        if (boss && bossStart < 0) bossStart = index;
        if (!boss && bossStart >= 0) {
          const half = count > 1 ? drawWidth / (count - 1) / 2 : 0, from = Math.max(padding.left, xOf(bossStart) - half), to = Math.min(width - padding.right, xOf(index - 1) + half);
          ctx.fillStyle = "rgba(255,63,52,.07)";
          ctx.fillRect(from, padding.top, to - from, drawHeight);
          bossStart = -1;
        }
      }
      const coords = smoothed.map((point, index) => ({
        x: xOf(index),
        y: yOf(point.dps),
        isBoss: point.isBoss,
        value: point.dps
      }));
      const area = ctx.createLinearGradient(
        0,
        padding.top,
        0,
        padding.top + drawHeight
      );
      area.addColorStop(0, "rgba(212,175,55,.30)");
      area.addColorStop(1, "rgba(212,175,55,.015)");
      ctx.beginPath();
      traceSmooth(ctx, coords);
      ctx.lineTo(coords[count - 1].x, padding.top + drawHeight);
      ctx.lineTo(coords[0].x, padding.top + drawHeight);
      ctx.closePath();
      ctx.fillStyle = area;
      ctx.fill();
      ctx.save();
      ctx.shadowColor = "rgba(212,175,55,.45)";
      ctx.shadowBlur = 5;
      ctx.beginPath();
      traceSmooth(ctx, coords);
      ctx.strokeStyle = ACCENT;
      ctx.lineWidth = 2;
      ctx.lineCap = "round";
      ctx.lineJoin = "round";
      ctx.stroke();
      ctx.restore();
      for (let index = 1; index < count; index++)
        if (coords[index].isBoss) {
          ctx.beginPath();
          ctx.moveTo(coords[index - 1].x, coords[index - 1].y);
          ctx.lineTo(coords[index].x, coords[index].y);
          ctx.strokeStyle = BOSS_COLOR;
          ctx.lineWidth = 2.2;
          ctx.lineCap = "round";
          ctx.stroke();
        }
      const peakIndex = smoothed.findIndex((point) => point.dps === peakValue), peakPoint = coords[Math.max(0, peakIndex)], latest = coords[count - 1];
      ctx.beginPath();
      ctx.arc(peakPoint.x, peakPoint.y, 2.3, 0, Math.PI * 2);
      ctx.fillStyle = "#fff0a8";
      ctx.fill();
      ctx.beginPath();
      ctx.arc(latest.x, latest.y, 3.2, 0, Math.PI * 2);
      ctx.fillStyle = latest.isBoss ? BOSS_COLOR : ACCENT;
      ctx.fill();
      ctx.strokeStyle = "#17130a";
      ctx.lineWidth = 1;
      ctx.stroke();
      ctx.textAlign = "right";
      ctx.textBaseline = "middle";
      ctx.font = "700 10px 'Microsoft YaHei','Noto Sans SC',sans-serif";
      ctx.fillStyle = latest.isBoss ? "#ff817a" : "#f5d568";
      ctx.fillText(formatRate(smoothed[count - 1].dps) + " DPS", width - 9, 13);
    }
    return { canvas, render };
  }
  function openClassPicker(name, anchor, rerender) {
    const old = document.getElementById("kikimeter-class-picker");
    if (old) old.remove();
    const select = document.createElement("select");
    select.id = "kikimeter-class-picker";
    select.dataset.kikimeter = "true";
    select.append(new Option(langText2("自动识别", "Auto-detect"), "auto"));
    Object.entries(ClassSystem.definitions).filter(([id]) => id !== "unknown").forEach(([id, d]) => select.append(new Option(d.label, id)));
    select.value = Settings.getClassOverride(name) || "auto";
    const r = anchor.getBoundingClientRect();
    Object.assign(select.style, {
      position: "fixed",
      zIndex: "10002",
      left: r.left + "px",
      top: r.bottom + "px",
      background: "#171717",
      color: "#fff",
      border: "1px solid #d4af37",
      borderRadius: "3px",
      padding: "3px"
    });
    const finish = () => setTimeout(() => select.remove(), 0);
    select.addEventListener("change", () => {
      ClassSystem.setOverride(name, select.value);
      finish();
      rerender();
    });
    select.addEventListener("blur", finish);
    document.body.appendChild(select);
    select.focus();
  }
  var DamageBreakdownTooltip = /* @__PURE__ */ (() => {
    let popup = null, container = null, playerName = "", closeTimer = null, lastAnchor = null;
    const langText4 = (zh, en) => Settings.getLanguage() === "en" ? en : zh;
    function cancelClose() {
      if (closeTimer !== null) clearTimeout(closeTimer);
      closeTimer = null;
    }
    function close() {
      cancelClose();
      if (popup) popup.remove();
      popup = null;
      container = null;
      playerName = "";
      lastAnchor = null;
    }
    function scheduleClose() {
      cancelClose();
      closeTimer = setTimeout(close, 140);
    }
    function position(anchor) {
      if (!popup || !anchor) return;
      const rect = anchor.getBoundingClientRect(), width = Math.min(340, window.innerWidth - 12);
      const left = rect.right + 6 + width <= window.innerWidth ? rect.right + 6 : Math.max(6, rect.left - width - 6);
      const height = popup.offsetHeight || 180, top = Math.max(6, Math.min(rect.top, window.innerHeight - height - 6));
      Object.assign(popup.style, {
        width: width + "px",
        left: left + "px",
        top: top + "px"
      });
    }
    function render(row) {
      if (!popup || !row) return;
      const scrollTop = popup.scrollTop;
      popup.innerHTML = "";
      const cls = ClassSystem.get(row.name), header = el("div", {
        display: "flex",
        alignItems: "center",
        gap: "6px",
        padding: "6px 8px",
        fontWeight: "700",
        borderBottom: "1px solid rgba(255,255,255,.13)",
        color: "#fff"
      });
      const classIcon = iconElement(cls.icon, cls.label);
      Object.assign(classIcon.style, {
        width: "20px",
        height: "20px",
        objectFit: "contain"
      });
      const headerText = el("span", {
        overflow: "hidden",
        textOverflow: "ellipsis",
        whiteSpace: "nowrap"
      });
      headerText.textContent = row.name + " · " + (row.breakdownTitle || langText4("伤害构成", "Damage Breakdown"));
      header.append(classIcon, headerText);
      popup.appendChild(header);
      const items = Array.isArray(row.breakdown) ? row.breakdown : [];
      const max = items.length ? Math.max(...items.map((item) => Number(item.value) || 0), 1) : 1;
      items.forEach((item, index) => {
        const line = el("div", {
          position: "relative",
          height: "27px",
          margin: "3px 5px",
          overflow: "hidden",
          borderRadius: "2px",
          background: "rgba(0,0,0,.46)",
          border: "1px solid rgba(255,255,255,.06)"
        });
        const bar = el("div", {
          position: "absolute",
          inset: "0 auto 0 0",
          width: (100 * (Number(item.value) || 0) / max).toFixed(2) + "%",
          background: `linear-gradient(90deg,${cls.color}c9,${cls.color}55)`
        });
        const content = el("div", {
          position: "absolute",
          inset: "0",
          display: "flex",
          alignItems: "center",
          gap: "5px",
          padding: "1px 5px",
          textShadow: "0 1px 2px #000"
        });
        const rank = el("span", {
          width: "15px",
          textAlign: "right",
          fontSize: "10px",
          opacity: ".8"
        });
        rank.textContent = String(index + 1) + ".";
        const itemLabel = item.label || DamageSources.label(item.source);
        const icon = iconElement(
          item.icon || DamageSources.icon(item.source, row.name),
          itemLabel
        );
        Object.assign(icon.style, {
          width: "20px",
          height: "20px",
          objectFit: "contain",
          flexShrink: "0",
          filter: "drop-shadow(0 1px 1px #000)"
        });
        const label = el("span", {
          flex: "1",
          minWidth: "40px",
          overflow: "hidden",
          textOverflow: "ellipsis",
          whiteSpace: "nowrap",
          fontWeight: "600"
        });
        label.textContent = itemLabel;
        label.title = label.textContent;
        const stats = el("span", {
          fontSize: "10px",
          fontVariantNumeric: "tabular-nums",
          whiteSpace: "nowrap"
        });
        stats.textContent = formatDamage(item.value) + "(" + formatRate(item.ps) + " " + (row.breakdownRateLabel || "DPS") + "," + (Number(item.pct) || 0).toFixed(1) + "%)";
        content.append(rank, icon, label, stats);
        line.append(bar, content);
        popup.appendChild(line);
      });
      if (!items.length) {
        const empty = el("div", {
          padding: "14px",
          textAlign: "center",
          opacity: ".55"
        });
        empty.textContent = row.breakdownEmpty || langText4("暂无伤害来源", "No damage sources");
        popup.appendChild(empty);
      }
      position(lastAnchor);
      popup.scrollTop = scrollTop;
    }
    function show(anchor, row, owner) {
      cancelClose();
      lastAnchor = anchor;
      container = owner;
      playerName = row.name;
      if (!popup) {
        popup = el("div", {
          position: "fixed",
          zIndex: "10004",
          maxHeight: "min(420px,calc(100vh - 12px))",
          overflowY: "auto",
          boxSizing: "border-box",
          background: "linear-gradient(180deg,rgba(25,25,25,.99),rgba(7,7,7,.99))",
          border: "1px solid rgba(212,175,55,.72)",
          borderRadius: "5px",
          boxShadow: "0 8px 30px rgba(0,0,0,.9)",
          color: "#f2f2f2",
          fontSize: "11px"
        });
        popup.dataset.kikimeter = "true";
        popup.addEventListener("mouseenter", cancelClose);
        popup.addEventListener("mouseleave", scheduleClose);
        document.body.appendChild(popup);
      }
      render(row);
    }
    function update(rows2) {
      if (!popup) return false;
      const row = (rows2 || []).find((item) => item.name === playerName);
      if (row && Array.isArray(row.breakdown)) render(row);
      else close();
      return !!popup;
    }
    function isOpenFor(owner) {
      return !!popup && container === owner;
    }
    return { show, update, isOpenFor, scheduleClose, cancelClose, close };
  })();
  var AccuracyBreakdownTooltip = /* @__PURE__ */ (() => {
    let popup = null, closeTimer = null, container = null, playerName = "", lastAnchor = null;
    function cancelClose() {
      if (closeTimer !== null) clearTimeout(closeTimer);
      closeTimer = null;
    }
    function close() {
      cancelClose();
      popup?.remove();
      popup = null;
      container = null;
      playerName = "";
      lastAnchor = null;
    }
    function scheduleClose() {
      cancelClose();
      closeTimer = setTimeout(close, 140);
    }
    function position(anchor) {
      if (!popup || !anchor) return;
      const rect = anchor.getBoundingClientRect(), width = Math.min(340, window.innerWidth - 12), left = rect.right + 6 + width <= window.innerWidth ? rect.right + 6 : Math.max(6, rect.left - width - 6), height = popup.offsetHeight || 180, top = Math.max(6, Math.min(rect.top, window.innerHeight - height - 6));
      Object.assign(popup.style, {
        width: width + "px",
        left: left + "px",
        top: top + "px"
      });
    }
    function render(row) {
      if (!popup || !row) return;
      const scrollTop = popup.scrollTop, cls = ClassSystem.get(row.name), header = el("div", {
        display: "flex",
        alignItems: "center",
        gap: "6px",
        padding: "6px 8px",
        fontWeight: "700",
        borderBottom: "1px solid rgba(255,255,255,.13)"
      });
      popup.replaceChildren();
      const classIcon = iconElement(cls.icon, cls.label);
      Object.assign(classIcon.style, {
        width: "20px",
        height: "20px",
        objectFit: "contain"
      });
      const title = document.createElement("span");
      title.textContent = `${row.name} · ${langText2("对怪物命中率", "Accuracy by monster")}`;
      header.append(classIcon, title);
      popup.appendChild(header);
      const monsters = Array.isArray(row.monsters) ? row.monsters : [];
      monsters.forEach((monster) => {
        const line = el("div", {
          position: "relative",
          height: "27px",
          margin: "3px 5px",
          overflow: "hidden",
          borderRadius: "2px",
          background: "rgba(0,0,0,.46)",
          border: "1px solid rgba(255,255,255,.06)"
        }), bar = el("div", {
          position: "absolute",
          inset: "0 auto 0 0",
          width: Math.max(0, Math.min(100, Number(monster.pct) || 0)) + "%",
          background: `linear-gradient(90deg,${cls.color}c9,${cls.color}55)`
        }), content = el("div", {
          position: "absolute",
          inset: "0",
          display: "flex",
          alignItems: "center",
          gap: "6px",
          padding: "1px 6px",
          textShadow: "0 1px 2px #000"
        }), label = el("span", {
          flex: "1",
          minWidth: "40px",
          overflow: "hidden",
          textOverflow: "ellipsis",
          whiteSpace: "nowrap",
          fontWeight: "600"
        }), stats = el("span", {
          fontSize: "10px",
          fontVariantNumeric: "tabular-nums",
          whiteSpace: "nowrap"
        });
        line.dataset.monsterHrid = monster.monsterHrid || "";
        const monsterIconSource = monster.monsterHrid ? GameAssets.monster(monster.monsterHrid) : "";
        const monsterIcon = monsterIconSource ? iconElement(monsterIconSource, monster.monsterName) : null;
        if (monsterIcon) {
          Object.assign(monsterIcon.style, {
            width: "20px",
            height: "20px",
            objectFit: "contain",
            flexShrink: "0",
            filter: "drop-shadow(0 1px 1px #000)"
          });
        }
        label.textContent = monster.monsterName;
        label.title = monster.monsterName;
        stats.textContent = `${(Number(monster.pct) || 0).toFixed(1)}% (${Number(monster.hits) || 0}/${Number(monster.attempts) || 0})`;
        if (monsterIcon) content.appendChild(monsterIcon);
        content.append(label, stats);
        line.append(bar, content);
        popup.appendChild(line);
      });
      if (!monsters.length) {
        const empty = el("div", {
          padding: "12px 10px 7px",
          textAlign: "center",
          opacity: ".58"
        });
        empty.textContent = langText2(
          "暂无可判定的怪物目标",
          "No resolved monster targets"
        );
        popup.appendChild(empty);
      }
      const note = el("div", {
        padding: "5px 8px 7px",
        borderTop: "1px solid rgba(255,255,255,.08)",
        color: "rgba(255,255,255,.58)",
        fontSize: "9px",
        lineHeight: "1.4"
      });
      note.textContent = langText2(
        "仅包含可判定目标;多怪物时无法确定目标的未命中不会硬性分配。",
        "Resolved targets only; ambiguous misses with multiple monsters are not assigned."
      );
      popup.appendChild(note);
      position(lastAnchor);
      popup.scrollTop = scrollTop;
    }
    function show(anchor, row, owner) {
      cancelClose();
      lastAnchor = anchor;
      container = owner;
      playerName = row.name;
      if (!popup) {
        popup = el("div", {
          position: "fixed",
          zIndex: "10004",
          maxHeight: "min(420px,calc(100vh - 12px))",
          overflowY: "auto",
          boxSizing: "border-box",
          background: "linear-gradient(180deg,rgba(25,25,25,.99),rgba(7,7,7,.99))",
          border: "1px solid rgba(212,175,55,.72)",
          borderRadius: "5px",
          boxShadow: "0 8px 30px rgba(0,0,0,.9)",
          color: "#f2f2f2",
          fontSize: "11px"
        });
        popup.dataset.kikimeter = "true";
        popup.dataset.kikimeterAccuracyTooltip = "true";
        popup.addEventListener("mouseenter", cancelClose);
        popup.addEventListener("mouseleave", scheduleClose);
        document.body.appendChild(popup);
      }
      render(row);
    }
    function update(rows2) {
      if (!popup) return false;
      const row = (rows2 || []).find((item) => item.name === playerName);
      if (row) render(row);
      else close();
      return !!popup;
    }
    function isOpenFor(owner) {
      return !!popup && container === owner;
    }
    return { show, update, isOpenFor, scheduleClose, cancelClose, close };
  })();
  function renderAccuracyRows(container, rows2, rerender, emptyText) {
    if (AccuracyBreakdownTooltip.isOpenFor(container)) {
      AccuracyBreakdownTooltip.update(rows2);
      const existing = new Map(
        [...container.querySelectorAll("[data-kikimeter-accuracy-row]")].map(
          (line) => [line.dataset.player, line]
        )
      );
      if (existing.size === rows2.length && rows2.every((row) => existing.has(row.name))) {
        rows2.forEach((row, index) => {
          const line = existing.get(row.name);
          line._accuracyRow = row;
          line._accuracyRank.textContent = String(index + 1) + ".";
          line._accuracyBar.style.width = Math.max(0, Math.min(100, Number(row.pct) || 0)) + "%";
          line._accuracyStats.textContent = `${(Number(row.pct) || 0).toFixed(1)}% (${Number(row.hits) || 0}/${Number(row.attempts) || 0})`;
        });
        rows2.forEach((row) => container.appendChild(existing.get(row.name)));
        return;
      }
      AccuracyBreakdownTooltip.close();
    }
    container.replaceChildren();
    rows2.forEach((row, index) => {
      const cls = ClassSystem.get(row.name), line = el("div", {
        position: "relative",
        height: "24px",
        margin: "2px 0",
        overflow: "hidden",
        minHeight: "24px",
        flexShrink: "0",
        boxSizing: "border-box",
        borderRadius: "2px",
        background: "rgba(0,0,0,.42)",
        border: "1px solid rgba(255,255,255,.06)",
        color: "#fff"
      }), bar = el("div", {
        position: "absolute",
        inset: "0 auto 0 0",
        width: Math.max(0, Math.min(100, Number(row.pct) || 0)) + "%",
        background: `linear-gradient(90deg,${cls.color}d9,${cls.color}70)`,
        boxShadow: `inset 0 0 5px ${cls.color}`
      }), content = el("div", {
        position: "absolute",
        inset: "0",
        display: "flex",
        alignItems: "center",
        gap: "4px",
        padding: "1px 5px",
        whiteSpace: "nowrap",
        textShadow: "0 1px 2px #000"
      }), rank = el("span", {
        width: "18px",
        textAlign: "right",
        opacity: ".85",
        fontSize: "11px"
      }), icon = iconElement(cls.icon, cls.label), name = el("span", {
        fontWeight: "600",
        overflow: "hidden",
        textOverflow: "ellipsis",
        flex: "1",
        minWidth: "30px"
      }), stats = el("span", {
        fontSize: "11px",
        fontVariantNumeric: "tabular-nums",
        textAlign: "right"
      });
      line.dataset.kikimeterAccuracyRow = "true";
      line.dataset.player = row.name;
      line._accuracyRow = row;
      line._accuracyRank = rank;
      line._accuracyBar = bar;
      line._accuracyStats = stats;
      rank.textContent = String(index + 1) + ".";
      Object.assign(icon.style, {
        width: "19px",
        height: "19px",
        objectFit: "contain",
        flexShrink: "0",
        cursor: "pointer",
        filter: "drop-shadow(0 1px 1px #000)"
      });
      icon.title = `${cls.label}${langText2("|点击选择职业", " | Click to choose class")}`;
      icon.addEventListener("click", (event) => {
        event.stopPropagation();
        openClassPicker(row.name, icon, rerender);
      });
      name.textContent = row.name;
      stats.textContent = `${(Number(row.pct) || 0).toFixed(1)}% (${Number(row.hits) || 0}/${Number(row.attempts) || 0})`;
      line.title = langText2(
        "悬停查看对各怪物的命中率",
        "Hover to view accuracy by monster"
      );
      line.addEventListener(
        "mouseenter",
        () => AccuracyBreakdownTooltip.show(line, line._accuracyRow, container)
      );
      line.addEventListener("mouseleave", AccuracyBreakdownTooltip.scheduleClose);
      content.append(rank, icon, name, stats);
      line.append(bar, content);
      container.appendChild(line);
    });
    if (!rows2.length) {
      const empty = el("div", {
        padding: "14px",
        textAlign: "center",
        opacity: ".5"
      });
      empty.textContent = emptyText || langText2("暂无命中率数据", "No accuracy data");
      container.appendChild(empty);
    }
  }
  function renderDetailsRows(container, rows2, rerender) {
    if (DamageBreakdownTooltip.isOpenFor(container) && DamageBreakdownTooltip.update(rows2))
      return;
    container.innerHTML = "";
    const max = rows2.length ? Math.max(...rows2.map((r) => r.value), 1) : 1;
    rows2.forEach((r, i) => {
      const synthetic = r.synthetic === "unattributed-damage", cls = synthetic ? ClassSystem.definitions.unknown : ClassSystem.get(r.name), line = el("div", {
        position: "relative",
        height: "24px",
        margin: "2px 0",
        overflow: "hidden",
        minHeight: "24px",
        flexShrink: "0",
        boxSizing: "border-box",
        borderRadius: "2px",
        background: "rgba(0,0,0,.42)",
        border: "1px solid rgba(255,255,255,.06)",
        color: "#fff"
      });
      line.dataset.player = r.name;
      const bar = el("div", {
        position: "absolute",
        left: "0",
        top: "0",
        bottom: "0",
        width: (100 * r.value / max).toFixed(2) + "%",
        background: `linear-gradient(90deg,${cls.color}d9,${cls.color}70)`,
        boxShadow: `inset 0 0 5px ${cls.color}`
      });
      const content = el("div", {
        position: "absolute",
        inset: "0",
        display: "flex",
        alignItems: "center",
        gap: "4px",
        padding: "1px 5px",
        whiteSpace: "nowrap",
        textShadow: "0 1px 2px #000"
      });
      const rank = el("span", {
        width: "18px",
        textAlign: "right",
        opacity: ".85",
        fontSize: "11px"
      });
      rank.textContent = String(i + 1) + ".";
      const icon = iconElement(cls.icon, cls.label);
      icon.title = synthetic ? r.name : `${cls.label}${langText2("|点击选择职业", " | Click to choose class")}`;
      Object.assign(icon.style, {
        width: "19px",
        height: "19px",
        objectFit: "contain",
        flexShrink: "0",
        cursor: synthetic ? "default" : "pointer",
        filter: "drop-shadow(0 1px 1px #000)"
      });
      if (!synthetic)
        icon.addEventListener("click", (e) => {
          e.stopPropagation();
          openClassPicker(r.name, icon, rerender);
        });
      const name = el("span", {
        fontWeight: "600",
        overflow: "hidden",
        textOverflow: "ellipsis",
        flex: "1",
        minWidth: "30px"
      });
      name.textContent = r.name;
      const stats = el("span", {
        fontSize: "11px",
        fontVariantNumeric: "tabular-nums",
        textAlign: "right"
      });
      stats.textContent = langText2(
        `${formatDamage(r.value)}(${formatRate(r.ps)} ${r.rateLabel || "DPS"},${r.pct.toFixed(1)}%)`,
        `${formatDamage(r.value)} (${formatRate(r.ps)} ${r.rateLabel || "DPS"}, ${r.pct.toFixed(1)}%)`
      );
      if (Array.isArray(r.breakdown)) {
        line.title = r.breakdownHover || (Settings.getLanguage() === "en" ? "Hover to view damage breakdown" : "悬停查看伤害构成");
        line.addEventListener(
          "mouseenter",
          () => DamageBreakdownTooltip.show(line, r, container)
        );
        line.addEventListener("mouseleave", DamageBreakdownTooltip.scheduleClose);
      }
      content.append(rank, icon, name, stats);
      line.append(bar, content);
      container.appendChild(line);
    });
    if (!rows2.length) {
      const empty = el("div", {
        padding: "14px",
        textAlign: "center",
        opacity: ".5"
      });
      empty.textContent = langText2("暂无战斗数据", "No combat data");
      container.appendChild(empty);
    }
  }

  // src/features/dps/60-main-panel.js
  var KikiMeter = (() => {
    const langText4 = (zh, en) => Settings.getLanguage() === "en" ? en : zh;
    const localizeReason = (reason) => {
      const labels = {
        legacy: ["旧版记录", "Legacy record"],
        archived: ["归档", "Archived"],
        reconnect: ["断线续传", "Reconnect continuation"],
        nextTier: ["进入下一层", "Entered next tier"],
        continued: ["继续战斗", "Combat continued"],
        anotherCombat: ["开始另一场战斗", "Another combat started"],
        manual: ["手动结束", "Ended manually"],
        trialEnded: ["公会试炼阶段结束", "Guild Trial stage ended"],
        characterSwitched: ["切换角色", "Character switched"],
        disconnected: ["连接中断", "Connection interrupted"],
        pageClosed: ["页面关闭", "Page closed"],
        pageResumed: ["页面恢复", "Page resumed"],
        combat: ["战斗", "Combat"]
      };
      const aliases = {
        旧版记录: "legacy",
        归档: "archived",
        断线续传: "reconnect",
        进入下一层: "nextTier",
        继续战斗: "continued",
        开始另一场战斗: "anotherCombat",
        手动结束: "manual",
        公会试炼阶段结束: "trialEnded",
        切换角色: "characterSwitched",
        连接中断: "disconnected",
        页面关闭: "pageClosed",
        页面恢复: "pageResumed",
        开始战斗: "combat",
        战斗结束: "combat",
        战斗: "combat"
      };
      const pair = labels[aliases[reason] || reason];
      return pair?.[Settings.getLanguage() === "en" ? 1 : 0] || reason || langText4("战斗", "Combat");
    };
    const PANEL_LAYOUT_VERSION = 2, DEFAULT_PANEL_HEIGHT = 212, MIN_PANEL_HEIGHT = 180;
    let panelOpen = false, tabBtn = null, panel = null;
    let reinjector = null, throttleTimer = null, viewportHandler = null;
    let historyFilter = "combat";
    let titleEl, playersListEl, segmentSelect, dpsTab, hpsTab, takenTab, accuracyTab, debugTab, graphTab, settingsTab, settingsMenu, langTab, resetTab, copyTab, closeTab, trialClassNotice;
    let mainGraphObj = null, mainGraphWrap = null;
    let mainMode = Settings.getMainMode();
    if (mainMode === "debug" && !Settings.getDebugMode()) mainMode = "dps";
    let callbacks = {};
    const isMobileViewport = () => Boolean(
      window.matchMedia && window.matchMedia("(max-width:700px), (pointer:coarse)").matches
    );
    const viewportBounds = () => {
      const visualViewport = window.visualViewport;
      const left = Number(visualViewport?.offsetLeft) || 0;
      const top = Number(visualViewport?.offsetTop) || 0;
      const width = Number(visualViewport?.width) || Number(window.innerWidth) || 0;
      const height = Number(visualViewport?.height) || Number(window.innerHeight) || 0;
      return {
        left,
        top,
        width,
        height,
        right: left + width,
        bottom: top + height
      };
    };
    const communityBuffRect = () => document.querySelector('div[class*="Header_communityBuffs"]')?.getBoundingClientRect?.() ?? null;
    const isStoredPosition = (value) => value && Number.isFinite(Number(value.left)) && Number.isFinite(Number(value.top));
    function readResponsivePosition(value, mode) {
      if (!value) return null;
      if (value.mobile || value.desktop) return value[mode] ?? null;
      return mode === "desktop" && isStoredPosition(value) ? value : null;
    }
    function writeResponsivePosition(value, mode, position) {
      const responsive = value?.mobile || value?.desktop ? { ...value } : {};
      if (isStoredPosition(value) && !responsive.desktop)
        responsive.desktop = value;
      responsive[mode] = position;
      return responsive;
    }
    const clamp3 = (value, min, max) => Math.max(min, Math.min(value, max));
    function refreshModeTabs() {
      [
        [dpsTab, "dps"],
        [hpsTab, "hps"],
        [takenTab, "taken"],
        [accuracyTab, "accuracy"],
        [debugTab, "debug"]
      ].forEach(([button, mode]) => {
        if (!button) return;
        const active = mainMode === mode;
        Object.assign(button.style, {
          color: active ? ACCENT : "rgba(255,255,255,.52)",
          background: active ? "rgba(212,175,55,.12)" : "transparent",
          borderColor: active ? "rgba(212,175,55,.38)" : "transparent"
        });
      });
      if (graphTab) {
        const active = Settings.getShowGraph();
        Object.assign(graphTab.style, {
          color: active ? ACCENT : "rgba(255,255,255,.52)",
          background: active ? "rgba(212,175,55,.12)" : "transparent",
          borderColor: active ? "rgba(212,175,55,.38)" : "transparent"
        });
        graphTab.setAttribute("aria-pressed", active ? "true" : "false");
      }
      if (debugTab)
        debugTab.style.display = Settings.getDebugMode() ? "" : "none";
    }
    function setMainMode(mode) {
      DamageBreakdownTooltip.close();
      AccuracyBreakdownTooltip.close();
      mainMode = ["dps", "hps", "taken", "accuracy", "debug"].includes(mode) ? mode : "dps";
      if (mainMode === "debug" && !Settings.getDebugMode()) mainMode = "dps";
      Settings.setMainMode(mainMode);
      refreshModeTabs();
      renderView(ViewData.get());
    }
    function toggleMainGraph() {
      const shouldShowGraph = mainGraphWrap ? mainGraphWrap.hidden : !Settings.getShowGraph();
      Settings.setShowGraph(shouldShowGraph);
      if (mainGraphWrap) {
        mainGraphWrap.hidden = !shouldShowGraph;
        if (shouldShowGraph && mainGraphObj)
          mainGraphObj.render(ViewData.get().graphPoints || []);
      }
      if (graphTab)
        graphTab.setAttribute("aria-pressed", shouldShowGraph ? "true" : "false");
      refreshModeTabs();
    }
    function refreshLanguageSwitch() {
      if (!langTab) return;
      const english = Settings.getLanguage() === "en";
      langTab.setAttribute("aria-checked", english ? "true" : "false");
      langTab.title = english ? "Switch to Chinese" : "切换为英文";
      if (langTab._knob)
        langTab._knob.style.transform = english ? "translateX(19px)" : "translateX(0)";
      if (langTab._label) langTab._label.textContent = english ? "EN" : "中";
    }
    function refreshToolbarLanguage() {
      const english = Settings.getLanguage() === "en";
      [
        [dpsTab, "伤害输出(DPS)", "Damage Done (DPS)"],
        [hpsTab, "恢复量(HPS)", "Healing (HPS)"],
        [takenTab, "承受伤害(DTPS)", "Damage Taken (DTPS)"],
        [accuracyTab, "实时命中率", "Live Accuracy"],
        [graphTab, "显示或隐藏 DPS 趋势", "Show or hide DPS trend"],
        [debugTab, "职业调试", "Class Debug"],
        [settingsTab, "设置", "Settings"],
        [resetTab, "结束并新建记录", "End fight and start a new record"],
        [copyTab, "复制统计", "Copy statistics"],
        [closeTab, "隐藏面板", "Hide panel"]
      ].forEach(([button, zh, en]) => {
        if (button) button.title = english ? en : zh;
      });
      refreshSegmentSelect(segmentSelect);
      if (trialClassNotice)
        trialClassNotice.textContent = english ? "Click each combat unit to identify its class accurately and cache it permanently." : "点击战斗界面中的人物,可准确识别职业并永久缓存。";
    }
    function toggleLanguage() {
      DamageBreakdownTooltip.close();
      AccuracyBreakdownTooltip.close();
      closeSettingsMenu();
      Settings.setLanguage(Settings.getLanguage() === "zh" ? "en" : "zh");
      refreshLanguageSwitch();
      refreshToolbarLanguage();
      renderView(ViewData.get());
    }
    function applyPanelOpacity() {
      if (!panel) return;
      const alpha = Settings.getPanelOpacity() / 100;
      panel.style.background = `linear-gradient(180deg,rgba(24,24,24,${alpha}),rgba(8,8,8,${alpha}))`;
    }
    function buildLanguageSwitch() {
      const button = document.createElement("button");
      button.type = "button";
      button.dataset.compactAction = "true";
      button.setAttribute("role", "switch");
      Object.assign(button.style, {
        position: "relative",
        width: "43px",
        height: "23px",
        padding: "0 4px",
        cursor: "pointer",
        background: "rgba(255,255,255,.12)",
        border: "1px solid rgba(255,255,255,.22)",
        borderRadius: "12px",
        color: "#fff",
        fontSize: "9px",
        overflow: "hidden"
      });
      const knob = el("span", {
        position: "absolute",
        left: "2px",
        top: "2px",
        width: "17px",
        height: "17px",
        borderRadius: "50%",
        background: ACCENT,
        boxShadow: "0 1px 4px rgba(0,0,0,.8)",
        transition: "transform .16s"
      });
      const label = el("span", {
        position: "relative",
        zIndex: "1",
        fontWeight: "700",
        textShadow: "0 1px 2px #000"
      });
      button._knob = knob;
      button._label = label;
      button.append(knob, label);
      button.addEventListener("mousedown", (event) => event.stopPropagation());
      button.addEventListener("click", (event) => {
        event.stopPropagation();
        toggleLanguage();
      });
      langTab = button;
      refreshLanguageSwitch();
      return button;
    }
    function closeSettingsMenu() {
      if (settingsMenu) {
        settingsMenu.remove();
        settingsMenu = null;
      }
    }
    function toggleSettingsMenu(anchor) {
      if (settingsMenu) {
        closeSettingsMenu();
        return;
      }
      const english = Settings.getLanguage() === "en";
      const menu = el("div", {
        position: "fixed",
        zIndex: "10005",
        width: "230px",
        padding: "10px",
        boxSizing: "border-box",
        background: "rgba(18,18,18,.98)",
        border: "1px solid rgba(212,175,55,.65)",
        borderRadius: "5px",
        boxShadow: "0 8px 26px rgba(0,0,0,.85)",
        color: "#f3f3f3",
        fontSize: "11px"
      });
      menu.dataset.kikimeter = "true";
      const heading = el("div", {
        fontWeight: "700",
        fontSize: "12px",
        marginBottom: "9px",
        paddingBottom: "6px",
        borderBottom: "1px solid rgba(255,255,255,.12)"
      });
      heading.textContent = english ? "Settings" : "设置";
      const opacityRow = el("div", {
        display: "flex",
        alignItems: "center",
        gap: "7px",
        marginBottom: "10px"
      });
      const opacityLabel = el("span", { minWidth: "62px" });
      opacityLabel.textContent = english ? "Opacity" : "不透明度";
      const range = document.createElement("input");
      range.type = "range";
      range.min = "10";
      range.max = "100";
      range.step = "5";
      range.value = String(Settings.getPanelOpacity());
      Object.assign(range.style, {
        flex: "1",
        accentColor: ACCENT,
        minWidth: "80px"
      });
      const value = el("span", {
        width: "34px",
        textAlign: "right",
        fontVariantNumeric: "tabular-nums"
      });
      value.textContent = range.value + "%";
      range.addEventListener("input", () => {
        Settings.setPanelOpacity(range.value);
        value.textContent = Settings.getPanelOpacity() + "%";
        applyPanelOpacity();
      });
      opacityRow.append(opacityLabel, range, value);
      const debugRow = el("label", {
        display: "flex",
        alignItems: "center",
        gap: "7px",
        cursor: "pointer"
      });
      const checkbox = document.createElement("input");
      checkbox.type = "checkbox";
      checkbox.checked = Settings.getDebugMode();
      checkbox.style.accentColor = ACCENT;
      const debugLabel = el("span");
      debugLabel.textContent = english ? "Enable Debug mode" : "启用 Debug 模式";
      checkbox.addEventListener("change", () => {
        Settings.setDebugMode(checkbox.checked);
        refreshModeTabs();
        if (!checkbox.checked && mainMode === "debug") setMainMode("dps");
      });
      debugRow.append(checkbox, debugLabel);
      menu.append(heading, opacityRow, debugRow);
      document.body.appendChild(menu);
      settingsMenu = menu;
      const rect = anchor.getBoundingClientRect(), width = menu.offsetWidth || 230, height = menu.offsetHeight || 120;
      const left = Math.max(
        5,
        Math.min(rect.right - width, window.innerWidth - width - 5)
      );
      const top = rect.bottom + 4 + height <= window.innerHeight ? rect.bottom + 4 : Math.max(5, rect.top - height - 4);
      Object.assign(menu.style, { left: left + "px", top: top + "px" });
      setTimeout(
        () => document.addEventListener("pointerdown", function outside(event) {
          if (!settingsMenu) {
            document.removeEventListener("pointerdown", outside);
            return;
          }
          if (!settingsMenu.contains(event.target) && event.target !== settingsTab) {
            closeSettingsMenu();
            document.removeEventListener("pointerdown", outside);
          }
        }),
        0
      );
    }
    function buildPanel() {
      const p = document.createElement("div");
      p.id = "kikimeter-panel";
      p.dataset.kikimeter = "true";
      let savedSize = Settings.getRecountSize() || {};
      if (Settings.getPanelLayoutVersion() < PANEL_LAYOUT_VERSION) {
        savedSize = { ...savedSize, height: DEFAULT_PANEL_HEIGHT };
        Settings.setRecountSize(savedSize);
        Settings.setShowGraph(false);
        Settings.setPanelLayoutVersion(PANEL_LAYOUT_VERSION);
      }
      const initialWidth = Math.max(
        0,
        Math.min(
          Math.max(280, Number(savedSize.width) || 330),
          viewportBounds().width - 8
        )
      );
      const initialHeight = Math.max(
        0,
        Math.min(
          Math.max(
            MIN_PANEL_HEIGHT,
            Number(savedSize.height) || DEFAULT_PANEL_HEIGHT
          ),
          viewportBounds().height - 8
        )
      );
      const panelAlpha = Settings.getPanelOpacity() / 100;
      Object.assign(p.style, {
        display: "none",
        position: "fixed",
        zIndex: "9999",
        width: initialWidth + "px",
        height: initialHeight + "px",
        minWidth: "min(280px, calc(100vw - 8px))",
        minHeight: `min(${MIN_PANEL_HEIGHT}px, calc(100vh - 8px))`,
        maxWidth: "calc(100vw - 8px)",
        maxHeight: "calc(100vh - 8px)",
        boxSizing: "border-box",
        flexDirection: "column",
        overflow: "hidden",
        background: `linear-gradient(180deg,rgba(24,24,24,${panelAlpha}),rgba(8,8,8,${panelAlpha}))`,
        color: "#f2f2f2",
        borderRadius: "5px",
        padding: "8px",
        boxShadow: "0 6px 24px rgba(0,0,0,.85)",
        fontSize: "12px",
        lineHeight: "1.35",
        border: "1px solid rgba(212,175,55,.58)"
      });
      const titleRow = el("div", {
        display: "flex",
        alignItems: "center",
        justifyContent: "space-between",
        marginBottom: "7px",
        cursor: "move",
        touchAction: "none",
        userSelect: "none",
        flexShrink: "0",
        paddingBottom: "5px",
        borderBottom: "1px solid rgba(255,255,255,.1)"
      });
      titleEl = el("div", {
        fontWeight: "bold",
        fontSize: "13px",
        color: "#e8e8e8",
        letterSpacing: ".2px",
        flex: "1",
        minWidth: "0",
        overflow: "hidden",
        textOverflow: "ellipsis",
        whiteSpace: "nowrap"
      });
      titleEl.textContent = Settings.getLanguage() === "en" ? mainMode === "hps" ? "Healing" : mainMode === "taken" ? "Damage Taken" : mainMode === "accuracy" ? "Accuracy" : mainMode === "debug" ? "Class Debug" : "Damage Done" : mainMode === "hps" ? "恢复量" : mainMode === "taken" ? "承受伤害" : mainMode === "accuracy" ? "命中率" : mainMode === "debug" ? "职业调试" : "伤害输出";
      const titleTools = el("div", {
        display: "flex",
        alignItems: "center",
        gap: "1px",
        flexShrink: "0"
      });
      const iconButton = (content, title, handler) => {
        const button = document.createElement("button");
        button.type = "button";
        button.title = title;
        button.dataset.compactAction = "true";
        Object.assign(button.style, {
          width: "23px",
          height: "23px",
          padding: "0",
          cursor: "pointer",
          background: "transparent",
          color: "rgba(255,255,255,.52)",
          border: "1px solid transparent",
          borderRadius: "3px",
          fontSize: "12px",
          lineHeight: "20px",
          display: "inline-flex",
          alignItems: "center",
          justifyContent: "center"
        });
        button._setContent = (nextContent) => {
          const source = String(nextContent || "");
          if (!source) return false;
          const isIcon = source.startsWith("data:image/") || /\.svg(?:\?[^#]*)?#[^#]+$/i.test(source);
          if (!isIcon) {
            button.textContent = source;
            button._icon = null;
            return true;
          }
          const icon = iconElement(source, "");
          Object.assign(icon.style, {
            width: "17px",
            height: "17px",
            objectFit: "contain",
            pointerEvents: "none"
          });
          button._icon = icon;
          button.replaceChildren(icon);
          return true;
        };
        button._setContent(content);
        button.addEventListener("mousedown", (event) => event.stopPropagation());
        button.addEventListener("click", (event) => {
          event.stopPropagation();
          handler();
        });
        return button;
      };
      segmentSelect = buildSegmentPicker(() => {
        if (callbacks.onSegmentChange) callbacks.onSegmentChange();
      }, true);
      dpsTab = iconButton(
        SKILL_MODE_ICONS.attack,
        "伤害输出(DPS)",
        () => setMainMode("dps")
      );
      hpsTab = iconButton(
        SKILL_MODE_ICONS.stamina,
        "恢复量(HPS)",
        () => setMainMode("hps")
      );
      takenTab = iconButton(
        SKILL_MODE_ICONS.defense,
        "承受伤害(DTPS)",
        () => setMainMode("taken")
      );
      accuracyTab = iconButton(
        SKILL_MODE_ICONS.steadyShot,
        "实时命中率",
        () => setMainMode("accuracy")
      );
      graphTab = iconButton(
        TOOLBAR_ICONS.trend,
        "显示或隐藏 DPS 趋势",
        toggleMainGraph
      );
      debugTab = iconButton(
        TOOLBAR_ICONS.debug,
        "Debug",
        () => setMainMode("debug")
      );
      settingsTab = iconButton(
        TOOLBAR_ICONS.settings,
        "设置",
        () => toggleSettingsMenu(settingsTab)
      );
      buildLanguageSwitch();
      resetTab = iconButton(TOOLBAR_ICONS.reset, "结束并新建记录", () => {
        if (callbacks.onReset) callbacks.onReset();
      });
      copyTab = iconButton(TOOLBAR_ICONS.copy, "复制统计", () => {
        if (callbacks.onCopy) callbacks.onCopy(copyTab);
      });
      closeTab = iconButton(TOOLBAR_ICONS.close, "隐藏面板", close);
      const refreshGameAssetIcons = () => {
        dpsTab?._setContent(SKILL_MODE_ICONS.attack);
        hpsTab?._setContent(SKILL_MODE_ICONS.stamina);
        takenTab?._setContent(SKILL_MODE_ICONS.defense);
        accuracyTab?._setContent(SKILL_MODE_ICONS.steadyShot);
        settingsTab?._setContent(TOOLBAR_ICONS.settings);
        segmentSelect?._refreshIcon?.();
      };
      void GameAssets.ready().then(() => {
        if (p.isConnected) refreshGameAssetIcons();
      });
      titleTools.append(
        segmentSelect,
        dpsTab,
        hpsTab,
        takenTab,
        accuracyTab,
        graphTab,
        debugTab,
        settingsTab,
        resetTab,
        copyTab,
        langTab,
        closeTab
      );
      titleRow.append(titleEl, titleTools);
      p.appendChild(titleRow);
      refreshModeTabs();
      refreshToolbarLanguage();
      trialClassNotice = el("div", {
        display: "none",
        margin: "0 0 7px",
        padding: "5px 7px",
        border: "1px solid rgba(63,199,235,.35)",
        borderRadius: "3px",
        background: "rgba(63,199,235,.08)",
        color: "#9bddf3",
        fontSize: "10px",
        lineHeight: "1.45"
      });
      trialClassNotice.textContent = langText4(
        "点击战斗界面中的人物,可准确识别职业并永久缓存。",
        "Click a character in the battle view to identify and cache their class."
      );
      p.appendChild(trialClassNotice);
      playersListEl = el("div", {
        marginBottom: "8px",
        paddingBottom: "8px",
        borderBottom: "1px solid rgba(255,255,255,.1)",
        flex: "1 1 auto",
        minHeight: "50px",
        overflowY: "auto"
      });
      p.appendChild(playersListEl);
      mainGraphObj = buildDetailsGraph();
      mainGraphWrap = el("div", {
        flexShrink: "0",
        paddingTop: "6px",
        borderTop: "1px solid rgba(255,255,255,.1)"
      });
      mainGraphWrap.hidden = !Settings.getShowGraph();
      mainGraphWrap.append(mainGraphObj.canvas);
      p.appendChild(mainGraphWrap);
      installWindowControls(p, titleRow);
      document.body.appendChild(p);
      return p;
    }
    function installWindowControls(root, titleRow) {
      let drag = null, resize = null;
      const mode = () => isMobileViewport() ? "mobile" : "desktop";
      const storeWindowState = () => {
        const currentPosition = Settings.getRecountPos();
        Settings.setRecountPos(
          writeResponsivePosition(currentPosition, mode(), {
            left: Number.parseFloat(root.style.left) || 0,
            top: Number.parseFloat(root.style.top) || 0
          })
        );
        Settings.setRecountSize({
          width: root.offsetWidth,
          height: root.offsetHeight
        });
      };
      titleRow.addEventListener("pointerdown", (event) => {
        if (event.button !== 0 || event.target.closest("button,input,select"))
          return;
        const rect = root.getBoundingClientRect();
        Object.assign(root.style, {
          left: rect.left + "px",
          top: rect.top + "px",
          right: "auto"
        });
        drag = {
          id: event.pointerId,
          dx: event.clientX - rect.left,
          dy: event.clientY - rect.top
        };
        if (titleRow.setPointerCapture)
          try {
            titleRow.setPointerCapture(event.pointerId);
          } catch (ignore) {
          }
        event.preventDefault();
      });
      const handles = {
        nw: ["0", "auto", "auto", "0", "nwse-resize"],
        ne: ["0", "0", "auto", "auto", "nesw-resize"],
        sw: ["auto", "auto", "0", "0", "nesw-resize"],
        se: ["auto", "0", "0", "auto", "nwse-resize"]
      };
      Object.entries(handles).forEach(
        ([direction, [top, right, bottom, left, cursor]]) => {
          const handle = el("div", {
            position: "absolute",
            top,
            right,
            bottom,
            left,
            width: "16px",
            height: "16px",
            zIndex: "4",
            cursor,
            touchAction: "none"
          });
          handle.dataset.resizeCorner = direction;
          handle.addEventListener("pointerdown", (event) => {
            if (event.button !== 0) return;
            const rect = root.getBoundingClientRect();
            resize = {
              id: event.pointerId,
              direction,
              sx: event.clientX,
              sy: event.clientY,
              rect
            };
            if (handle.setPointerCapture)
              try {
                handle.setPointerCapture(event.pointerId);
              } catch (ignore) {
              }
            event.preventDefault();
            event.stopPropagation();
          });
          root.appendChild(handle);
        }
      );
      window.addEventListener("pointermove", (event) => {
        const bounds = viewportBounds();
        if (drag && drag.id === event.pointerId) {
          const left2 = clamp3(
            event.clientX - drag.dx,
            bounds.left,
            Math.max(bounds.left, bounds.right - root.offsetWidth)
          );
          const top2 = clamp3(
            event.clientY - drag.dy,
            bounds.top,
            Math.max(bounds.top, bounds.bottom - root.offsetHeight)
          );
          root.style.left = left2 + "px";
          root.style.top = top2 + "px";
          event.preventDefault();
          return;
        }
        if (!resize || resize.id !== event.pointerId) return;
        const dx = event.clientX - resize.sx, dy = event.clientY - resize.sy, r = resize.rect, d = resize.direction;
        const maxWidth = Math.max(0, bounds.width - 8), maxHeight = Math.max(0, bounds.height - 8), minWidth = Math.min(280, maxWidth), minHeight = Math.min(MIN_PANEL_HEIGHT, maxHeight);
        let width = d.includes("w") ? r.width - dx : r.width + dx;
        let height = d.includes("n") ? r.height - dy : r.height + dy;
        width = clamp3(width, minWidth, maxWidth);
        height = clamp3(height, minHeight, maxHeight);
        let left = d.includes("w") ? r.right - width : r.left, top = d.includes("n") ? r.bottom - height : r.top;
        left = clamp3(
          left,
          bounds.left,
          Math.max(bounds.left, bounds.right - width)
        );
        top = clamp3(
          top,
          bounds.top,
          Math.max(bounds.top, bounds.bottom - height)
        );
        Object.assign(root.style, {
          left: left + "px",
          top: top + "px",
          right: "auto",
          width: width + "px",
          height: height + "px"
        });
        event.preventDefault();
      });
      const finishPointerAction = (event) => {
        const matchesDrag = drag && drag.id === event.pointerId;
        const matchesResize = resize && resize.id === event.pointerId;
        if (!matchesDrag && !matchesResize) return;
        storeWindowState();
        drag = null;
        resize = null;
      };
      window.addEventListener("pointerup", finishPointerAction);
      window.addEventListener("pointercancel", finishPointerAction);
    }
    function close() {
      panelOpen = false;
      closeSettingsMenu();
      DamageBreakdownTooltip.close();
      AccuracyBreakdownTooltip.close();
      if (panel) panel.style.display = "none";
      if (tabBtn) tabBtn.style.filter = "none";
    }
    function placePanel() {
      if (!panel || !panelOpen) return;
      const bounds = viewportBounds();
      const rect = panel.getBoundingClientRect();
      const width = rect.width || panel.offsetWidth || Number.parseFloat(panel.style.width) || 280;
      const height = rect.height || panel.offsetHeight || Number.parseFloat(panel.style.height) || MIN_PANEL_HEIGHT;
      const mode = isMobileViewport() ? "mobile" : "desktop";
      const saved = readResponsivePosition(Settings.getRecountPos(), mode);
      let left;
      let top;
      if (isStoredPosition(saved)) {
        left = Number(saved.left);
        top = Number(saved.top);
      } else if (mode === "mobile" && communityBuffRect()) {
        const anchorRect = communityBuffRect();
        left = anchorRect.right + 6;
        top = anchorRect.top;
      } else {
        left = bounds.right - width - 12;
        top = bounds.bottom - height - 12;
      }
      left = clamp3(
        left,
        bounds.left + 4,
        Math.max(bounds.left + 4, bounds.right - width - 4)
      );
      top = clamp3(
        top,
        bounds.top + 4,
        Math.max(bounds.top + 4, bounds.bottom - height - 4)
      );
      Object.assign(panel.style, {
        display: "flex",
        left: left + "px",
        top: top + "px",
        right: "auto"
      });
    }
    function toggle(v, anchor) {
      const currentlyOpen = !!panel && panel.style.display !== "none";
      const next = v === void 0 ? !currentlyOpen : !!v;
      if (!next) {
        close();
        return false;
      }
      panelOpen = true;
      panel.style.display = "flex";
      placePanel();
      if (tabBtn) tabBtn.style.filter = "brightness(1.15)";
      return true;
    }
    function buildToggle(label, initial, onChange) {
      const row = el("div", {
        display: "flex",
        alignItems: "center",
        gap: "6px",
        marginBottom: "3px"
      });
      const cb = document.createElement("input");
      cb.type = "checkbox";
      cb.checked = initial;
      cb.style.accentColor = ACCENT;
      cb.addEventListener("change", () => onChange(cb.checked));
      const lbl = el("span", { fontSize: "12px" });
      lbl.textContent = label;
      row.append(cb, lbl);
      return row;
    }
    function buildTabBtn() {
      const btn = document.createElement("button");
      btn.type = "button";
      btn.id = "kikimeter-tab-btn";
      btn.dataset.kikimeter = "true";
      btn.dataset.kikimeterBound = VERSION;
      btn.textContent = "DPS";
      Object.assign(btn.style, {
        display: "inline-flex",
        alignItems: "center",
        justifyContent: "center",
        position: "fixed",
        zIndex: "9998",
        cursor: "pointer",
        background: ACCENT,
        color: "#fff",
        border: "none",
        borderRadius: "4px",
        width: "54px",
        height: "28px",
        padding: "0",
        margin: "0",
        fontWeight: "600",
        fontSize: "13px",
        fontFamily: "inherit",
        whiteSpace: "nowrap",
        transition: "filter .12s,left .16s ease",
        touchAction: "none",
        boxShadow: "0 2px 9px rgba(0,0,0,.58)",
        outline: "none",
        flexShrink: "0",
        letterSpacing: ".2px"
      });
      let drag = null, moved = false, edgeHideTimer = null;
      const mode = () => isMobileViewport() ? "mobile" : "desktop";
      const defaultPos = (width = 54, height = 28) => {
        const bounds = viewportBounds();
        if (mode() === "mobile") {
          const anchorRect = communityBuffRect();
          if (anchorRect)
            return {
              left: anchorRect.right + 6,
              top: anchorRect.top + Math.max(0, (anchorRect.height - height) / 2),
              edge: ""
            };
          return { left: bounds.left + 8, top: bounds.top + 8, edge: "" };
        }
        return { left: bounds.left + 130, top: bounds.top + 80, edge: "" };
      };
      const savedPos = (width, height) => readResponsivePosition(Settings.getLauncherPos(), mode()) || defaultPos(width, height);
      const place = (reveal = false) => {
        const width = btn.offsetWidth || 54, height = btn.offsetHeight || 28, saved = savedPos(width, height), bounds = viewportBounds();
        let left = Number(saved.left), top = Number(saved.top);
        if (!Number.isFinite(left)) left = defaultPos(width, height).left;
        if (!Number.isFinite(top)) top = defaultPos(width, height).top;
        top = clamp3(
          top,
          bounds.top,
          Math.max(bounds.top, bounds.bottom - height)
        );
        if (mode() === "mobile")
          left = clamp3(
            left,
            bounds.left,
            Math.max(bounds.left, bounds.right - width)
          );
        else if (saved.edge === "left")
          left = reveal ? bounds.left : bounds.left - (width - 14);
        else if (saved.edge === "right")
          left = reveal ? Math.max(bounds.left, bounds.right - width) : Math.max(bounds.left, bounds.right - 14);
        else
          left = clamp3(
            left,
            bounds.left,
            Math.max(bounds.left, bounds.right - width)
          );
        Object.assign(btn.style, { left: left + "px", top: top + "px" });
      };
      const hideAtEdge = () => {
        if (mode() === "mobile") return;
        if (edgeHideTimer !== null) clearTimeout(edgeHideTimer);
        edgeHideTimer = setTimeout(() => place(false), 350);
      };
      btn._placeLauncher = place;
      btn.addEventListener("mouseenter", () => {
        if (edgeHideTimer !== null) clearTimeout(edgeHideTimer);
        btn.style.filter = "brightness(1.15)";
        place(true);
      });
      btn.addEventListener("mouseleave", () => {
        btn.style.filter = panelOpen ? "brightness(1.15)" : "none";
        if (savedPos().edge) hideAtEdge();
      });
      btn.addEventListener("pointerdown", (event) => {
        if (event.button !== void 0 && event.button !== 0) return;
        event.stopPropagation();
        if (edgeHideTimer !== null) clearTimeout(edgeHideTimer);
        place(true);
        const rect = btn.getBoundingClientRect();
        drag = {
          id: event.pointerId,
          dx: event.clientX - rect.left,
          dy: event.clientY - rect.top,
          startX: event.clientX,
          startY: event.clientY
        };
        moved = false;
        if (btn.setPointerCapture)
          try {
            btn.setPointerCapture(event.pointerId);
          } catch (ignore) {
          }
      });
      btn.addEventListener("pointermove", (event) => {
        if (!drag || drag.id !== event.pointerId) return;
        if (Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) > 4)
          moved = true;
        if (!moved) return;
        const width = btn.offsetWidth || 54, height = btn.offsetHeight || 28, bounds = viewportBounds();
        const left = clamp3(
          event.clientX - drag.dx,
          bounds.left,
          Math.max(bounds.left, bounds.right - width)
        );
        const top = clamp3(
          event.clientY - drag.dy,
          bounds.top,
          Math.max(bounds.top, bounds.bottom - height)
        );
        btn.style.transition = "none";
        Object.assign(btn.style, { left: left + "px", top: top + "px" });
        event.preventDefault();
      });
      const finishDrag = (event) => {
        if (!drag || drag.id !== event.pointerId) return;
        const width = btn.offsetWidth || 54, rect = btn.getBoundingClientRect(), bounds = viewportBounds();
        let edge = "";
        if (mode() !== "mobile") {
          if (rect.left <= bounds.left + 24) edge = "left";
          else if (rect.right >= bounds.right - 24) edge = "right";
        }
        Settings.setLauncherPos(
          writeResponsivePosition(Settings.getLauncherPos(), mode(), {
            left: clamp3(
              rect.left,
              bounds.left,
              Math.max(bounds.left, bounds.right - width)
            ),
            top: clamp3(
              rect.top,
              bounds.top,
              Math.max(bounds.top, bounds.bottom - (btn.offsetHeight || 28))
            ),
            edge
          })
        );
        drag = null;
        btn.style.transition = "filter .12s,left .16s ease";
        if (edge) hideAtEdge();
      };
      btn.addEventListener("pointerup", finishDrag);
      btn.addEventListener("pointercancel", finishDrag);
      btn.addEventListener("click", (e) => {
        e.stopPropagation();
        if (moved) {
          moved = false;
          return;
        }
        toggle(void 0, btn);
      });
      setTimeout(() => place(false), 0);
      return btn;
    }
    function dedupeAndGetButton() {
      const all = document.querySelectorAll("#kikimeter-tab-btn");
      all.forEach((b, i) => {
        if (i > 0) b.remove();
      });
      return all[0] || null;
    }
    function findCombatTabBar() {
      const containers = [
        .../* @__PURE__ */ new Set([
          ...document.querySelectorAll("div." + TAB_CONTAINER_CLASS),
          ...document.querySelectorAll(
            'div[class*="TabsComponent_tabsContainer"]'
          )
        ])
      ];
      const combatZones = getGameTranslation("combatPanel.combatZones");
      const labyrinthLabels = [
        getGameTranslation("labyrinthPanel.labyrinth"),
        getGameTranslation("labyrinthPanel.room"),
        getGameTranslation("labyrinthPanel.automation")
      ];
      for (const c of containers) {
        const t21 = c.textContent;
        if (combatZones && t21.includes(combatZones) || t21.includes("Combat Zones") || t21.includes("战斗区域") || t21.includes("戰鬥區域"))
          return c;
        if (labyrinthLabels.every(Boolean) && labyrinthLabels.every((label) => t21.includes(label)) || t21.includes("Labyrinth") && t21.includes("Room") && t21.includes("Automation") || t21.includes("迷宫") && (t21.includes("房间") || t21.includes("自动化")) || t21.includes("迷宮") && (t21.includes("房間") || t21.includes("自動化")))
          return c;
        if (isSelectedTrialTabBar(c)) return c;
        if (isSelectedGuildProgressTabBar(c)) return c;
      }
      return null;
    }
    function setTabButtonStyle(btn) {
      btn.title = Settings.getLanguage() === "en" ? "Open DPS meter" : "打开 DPS 统计";
      if (btn._placeLauncher) btn._placeLauncher(false);
    }
    function inject() {
      let existing = dedupeAndGetButton();
      if (existing && existing.dataset.kikimeterBound !== VERSION) {
        const replacement = buildTabBtn();
        existing.replaceWith(replacement);
        existing = replacement;
      }
      if (existing) {
        existing.style.display = "inline-flex";
        setTabButtonStyle(existing);
        if (existing.parentElement !== document.body)
          document.body.appendChild(existing);
        tabBtn = existing;
        return true;
      }
      tabBtn = buildTabBtn();
      setTabButtonStyle(tabBtn);
      document.body.appendChild(tabBtn);
      return true;
    }
    function init(cb) {
      callbacks = cb;
      if (!panel) panel = buildPanel();
      else if (!panel.isConnected) document.body.appendChild(panel);
      tabBtn = null;
      inject();
      if (reinjector) reinjector.disconnect();
      reinjector = new MutationObserver(() => {
        if (throttleTimer) return;
        throttleTimer = setTimeout(() => {
          throttleTimer = null;
          inject();
        }, 200);
      });
      reinjector.observe(document.body, { childList: true });
      if (viewportHandler) {
        window.removeEventListener("resize", viewportHandler);
        window.visualViewport?.removeEventListener("resize", viewportHandler);
      }
      viewportHandler = () => {
        tabBtn?._placeLauncher?.(false);
        placePanel();
      };
      window.addEventListener("resize", viewportHandler);
      window.visualViewport?.addEventListener("resize", viewportHandler);
    }
    function destroy() {
      close();
      if (throttleTimer) {
        clearTimeout(throttleTimer);
        throttleTimer = null;
      }
      if (reinjector) {
        reinjector.disconnect();
        reinjector = null;
      }
      if (viewportHandler) {
        window.removeEventListener("resize", viewportHandler);
        window.visualViewport?.removeEventListener("resize", viewportHandler);
        viewportHandler = null;
      }
      document.querySelectorAll("#kikimeter-tab-btn").forEach((button) => button.remove());
      document.querySelector("#kikimeter-segment-menu")?.remove();
      document.querySelector("#kikimeter-class-picker")?.remove();
      if (panel) panel.remove();
      tabBtn = null;
    }
    function renderPlayers(names, getDps, getDmg, getHps, getKills, getSharePct, getColor) {
      if (!playersListEl) return;
      const rows2 = names.map((name) => ({
        name,
        value: getDmg(name),
        ps: getDps(name),
        pct: getSharePct(name),
        rateLabel: "DPS"
      })).sort((a, b) => b.value - a.value);
      renderDetailsRows(
        playersListEl,
        rows2,
        () => renderPlayers(
          names,
          getDps,
          getDmg,
          getHps,
          getKills,
          getSharePct,
          getColor
        )
      );
    }
    function renderView(view) {
      refreshSegmentSelect(segmentSelect);
      refreshModeTabs();
      refreshToolbarLanguage();
      if (titleEl)
        titleEl.textContent = Settings.getLanguage() === "en" ? mainMode === "hps" ? "Healing" : mainMode === "taken" ? "Damage Taken" : mainMode === "accuracy" ? "Accuracy" : mainMode === "debug" ? "Class Debug" : "Damage Done" : mainMode === "hps" ? "恢复量" : mainMode === "taken" ? "承受伤害" : mainMode === "accuracy" ? "命中率" : mainMode === "debug" ? "职业调试" : "伤害输出";
      if (mainGraphObj) mainGraphObj.render(view.graphPoints || []);
      if (trialClassNotice)
        trialClassNotice.style.display = mainMode !== "debug" && view.type === "trial" ? "block" : "none";
      if (mainMode === "debug") {
        playersListEl.innerHTML = "";
        const hint = el("div", {
          fontSize: "11px",
          lineHeight: "1.55",
          color: "rgba(255,255,255,.76)",
          marginBottom: "7px"
        });
        hint.textContent = langText4(
          "全量探针会从点击开始持续被动记录全部游戏入站消息,直到你手动结束;不会主动发送任何请求。聊天正文和凭证字段会脱敏,结束后请在刷新页面前下载。",
          "The full probe passively records all incoming game messages after you start it until you stop it manually. It sends no requests. Chat content and credential fields are redacted; download the capture before refreshing the page."
        );
        const probeStatus = ClassProbe.status();
        const probeButtons = el("div", {
          display: "flex",
          gap: "5px",
          marginBottom: "7px"
        });
        const startProbe = document.createElement("button");
        startProbe.textContent = probeStatus.active ? langText4("全量采集中…", "Capturing…") : langText4("开始全量采集", "Start full capture");
        startProbe.disabled = probeStatus.active;
        const stopProbe = document.createElement("button");
        stopProbe.textContent = langText4("结束采集", "Stop capture");
        stopProbe.disabled = !probeStatus.active;
        const downloadProbe = document.createElement("button");
        downloadProbe.textContent = langText4(
          "⬇ 下载全量 MSG",
          "⬇ Download full messages"
        );
        downloadProbe.disabled = probeStatus.active || !probeStatus.startedAt;
        [startProbe, stopProbe, downloadProbe].forEach(
          (button) => Object.assign(button.style, {
            flex: "1",
            cursor: button.disabled ? "default" : "pointer",
            padding: "5px 2px",
            fontSize: "9px",
            borderRadius: "3px",
            border: "1px solid rgba(255,255,255,.18)",
            background: "rgba(255,255,255,.07)",
            color: "#e8eaf6",
            opacity: button.disabled ? ".45" : "1"
          })
        );
        startProbe.addEventListener("mousedown", (event) => {
          event.preventDefault();
          if (!startProbe.disabled) ClassProbe.start();
        });
        stopProbe.addEventListener("mousedown", (event) => {
          event.preventDefault();
          if (!stopProbe.disabled) ClassProbe.stop();
        });
        downloadProbe.addEventListener("mousedown", (event) => {
          event.preventDefault();
          if (downloadProbe.disabled) return;
          if (ClassProbe.download()) {
            downloadProbe.textContent = langText4("✓ 已下载", "✓ Downloaded");
            setTimeout(
              () => downloadProbe.textContent = langText4(
                "⬇ 下载全量 MSG",
                "⬇ Download full messages"
              ),
              1500
            );
          }
        });
        probeButtons.append(startProbe, stopProbe, downloadProbe);
        const report = el("pre", {
          margin: "0 0 7px",
          padding: "7px",
          maxHeight: "210px",
          overflow: "auto",
          whiteSpace: "pre-wrap",
          wordBreak: "break-all",
          background: "rgba(0,0,0,.35)",
          border: "1px solid rgba(255,255,255,.12)",
          borderRadius: "3px",
          fontSize: "9px",
          lineHeight: "1.45"
        });
        report.textContent = probeStatus.active ? langText4(
          `全量探针正在采集,已记录 ${probeStatus.messageCount} 条消息,正文约 ${(probeStatus.captureChars / 1024 / 1024).toFixed(2)} MB;点击“结束采集”才会停止。`,
          `The full probe is capturing. ${probeStatus.messageCount} messages recorded (${(probeStatus.captureChars / 1024 / 1024).toFixed(2)} MB). Click “Stop capture” to stop it.`
        ) : probeStatus.startedAt ? ClassProbe.report().slice(0, 6e3) : ClassDebug.report();
        const buttons = el("div", { display: "flex", gap: "6px" });
        const copy = document.createElement("button");
        copy.textContent = langText4(
          "📋 复制完整探针报告",
          "📋 Copy full probe report"
        );
        const clear = document.createElement("button");
        clear.textContent = langText4("清空报告", "Clear report");
        [copy, clear].forEach(
          (button) => Object.assign(button.style, {
            flex: "1",
            cursor: "pointer",
            padding: "5px",
            fontSize: "10px",
            borderRadius: "3px",
            border: "1px solid rgba(255,255,255,.18)",
            background: "rgba(255,255,255,.07)",
            color: "#e8eaf6"
          })
        );
        copy.addEventListener(
          "click",
          () => navigator.clipboard.writeText(ClassProbe.report()).then(() => {
            copy.textContent = langText4("✓ 已复制", "✓ Copied");
            setTimeout(
              () => copy.textContent = langText4(
                "📋 复制完整探针报告",
                "📋 Copy full probe report"
              ),
              1500
            );
          }).catch(() => {
          })
        );
        clear.addEventListener("click", () => {
          ClassDebug.clear();
          ClassProbe.clear();
          renderView(ViewData.get());
        });
        buttons.append(copy, clear);
        playersListEl.append(hint, probeButtons, report, buttons);
        return;
      }
      if (mainMode === "accuracy") {
        const rows3 = (view.players || []).filter((player) => player.accuracy && player.accuracy.attempts > 0).map((player) => ({
          name: player.name,
          attempts: player.accuracy.attempts,
          hits: player.accuracy.hits,
          pct: player.accuracy.pct,
          monsters: player.accuracy.monsters
        })).sort(
          (a, b) => b.pct - a.pct || b.attempts - a.attempts || a.name.localeCompare(b.name)
        );
        renderAccuracyRows(
          playersListEl,
          rows3,
          () => renderView(ViewData.get()),
          view.current ? langText4(
            "暂无可靠判定的直接攻击",
            "No reliably resolved direct attacks yet"
          ) : langText4(
            "该历史记录暂无命中率数据",
            "No accuracy data for this combat record"
          )
        );
        return;
      }
      const total = mainMode === "hps" ? (view.players || []).reduce(
        (sum, p) => sum + (Number(p.healing) || 0),
        0
      ) : mainMode === "taken" ? (view.players || []).reduce(
        (sum, p) => sum + (Number(p.taken) || 0),
        0
      ) : view.teamDamage;
      const rows2 = (view.players || []).map((p) => ({
        name: p.name,
        synthetic: p.synthetic,
        value: mainMode === "hps" ? Number(p.healing) || 0 : mainMode === "taken" ? Number(p.taken) || 0 : Number(p.damage) || 0,
        ps: mainMode === "hps" ? Number(p.hps) || 0 : mainMode === "taken" ? Number(p.takenPs) || 0 : Number(p.dps) || 0,
        pct: total > 0 ? (mainMode === "hps" ? Number(p.healing) || 0 : mainMode === "taken" ? Number(p.taken) || 0 : Number(p.damage) || 0) * 100 / total : 0,
        rateLabel: mainMode === "hps" ? "HPS" : mainMode === "taken" ? "DTPS" : "DPS",
        breakdown: mainMode === "dps" ? p.breakdown : mainMode === "taken" ? p.takenBreakdown : null,
        breakdownTitle: mainMode === "taken" ? Settings.getLanguage() === "en" ? "Damage Taken Sources" : "承伤来源" : void 0,
        breakdownRateLabel: mainMode === "taken" ? "DTPS" : "DPS",
        breakdownHover: mainMode === "taken" ? Settings.getLanguage() === "en" ? "Hover to view monster and skill sources" : "悬停查看怪物与技能来源" : void 0,
        breakdownEmpty: mainMode === "taken" ? Settings.getLanguage() === "en" ? "No damage-taken sources" : "暂无承伤来源" : void 0
      })).filter((r) => r.value > 0).sort((a, b) => b.value - a.value);
      renderDetailsRows(playersListEl, rows2, () => renderView(ViewData.get()));
    }
    function entryToText(entry) {
      const d = new Date(entry.date);
      const dateStr = d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
      const dur = formatDuration2(entry.durationSeconds);
      const total = entry.teamDamage;
      let out = langText4(
        `=== KikiMeter 战斗记录|${dateStr}|${dur} ===
`,
        `=== KikiMeter Combat Record | ${dateStr} | ${dur} ===
`
      );
      out += langText4(
        `团队:${formatRate(entry.teamDps || 0)} DPS|总伤害 ${formatDamage(total || 0)}`,
        `Team: ${formatRate(entry.teamDps || 0)} DPS | Total damage ${formatDamage(total || 0)}`
      );
      if (entry.teamKills > 0)
        out += langText4(
          `|击杀 ${entry.teamKills}`,
          ` | Kills ${entry.teamKills}`
        );
      out += "\n";
      (entry.players || []).forEach((p) => {
        const pct = total > 0 ? (p.damage / total * 100).toFixed(0) : "0";
        const name = p.name.padEnd(12).slice(0, 12);
        out += name + langText4(":", ": ") + formatRate(p.dps || 0).padStart(6) + langText4(" DPS|", " DPS | ");
        out += formatDamage(p.damage).padStart(7) + " (" + pct + "%)";
        if (p.kills > 0)
          out += langText4(`|击杀 ${p.kills}`, ` | Kills ${p.kills}`);
        if (p.hps > 0.1)
          out += langText4(
            `|HPS ${formatRate(p.hps)}`,
            ` | HPS ${formatRate(p.hps)}`
          );
        out += "\n";
      });
      return out;
    }
    function renderHistory(container) {
      container.innerHTML = "";
      const TYPES = [
        { id: "combat", label: langText4("普通战斗", "Combat") },
        { id: "trial", label: langText4("公会试炼", "Guild Trial") },
        { id: "labyrinth", label: langText4("迷宫", "Labyrinth") }
      ];
      const filterRow = el("div", {
        display: "flex",
        gap: "4px",
        marginBottom: "8px"
      });
      TYPES.forEach((t21) => {
        const btn = document.createElement("button");
        btn.textContent = t21.label;
        const active = historyFilter === t21.id;
        Object.assign(btn.style, {
          flex: "1",
          cursor: "pointer",
          fontSize: "11px",
          padding: "4px 0",
          borderRadius: "4px",
          border: "none",
          fontWeight: active ? "700" : "400",
          background: active ? ACCENT : "rgba(255,255,255,.08)",
          color: active ? "#fff" : "rgba(255,255,255,.65)",
          transition: "background .12s"
        });
        btn.addEventListener("click", () => {
          historyFilter = t21.id;
          renderHistory(container);
        });
        filterRow.appendChild(btn);
      });
      container.appendChild(filterRow);
      const entries = HistoryStore.getAll(historyFilter);
      if (!entries.length) {
        const empty = el("div", {
          opacity: ".5",
          fontSize: "11px",
          padding: "6px 0"
        });
        empty.textContent = langText4(
          "还没有保存的战斗记录。",
          "No saved combat records yet."
        );
        container.appendChild(empty);
        return;
      }
      entries.forEach((entry, idx) => {
        const d = new Date(entry.date || entry.startedAt);
        const dateStr = d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
        const block = el("div", {
          marginBottom: "8px",
          paddingBottom: "8px",
          borderBottom: idx < entries.length - 1 ? "1px solid rgba(255,255,255,.07)" : "none"
        });
        const header = el("div", {
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
          marginBottom: "2px"
        });
        const dateEl = el("span", {
          fontSize: "11px",
          color: "#a5b4fc",
          fontWeight: "bold"
        });
        dateEl.textContent = dateStr + " · " + formatDuration2(entry.durationSeconds || entry.durationMs / 1e3 || 0);
        const copyEntryBtn = el("button", {
          fontSize: "10px",
          cursor: "pointer",
          background: "transparent",
          color: "#93c5fd",
          border: "none",
          padding: "0 2px"
        });
        copyEntryBtn.textContent = "📋";
        copyEntryBtn.title = langText4("复制这场战斗", "Copy this combat record");
        copyEntryBtn.addEventListener("click", (e) => {
          e.stopPropagation();
          navigator.clipboard.writeText(entryToText(entry)).catch(() => {
          });
          copyEntryBtn.textContent = "✓";
          setTimeout(() => {
            copyEntryBtn.textContent = "📋";
          }, 1500);
        });
        header.append(dateEl, copyEntryBtn);
        block.appendChild(header);
        const teamLine = el("div", {
          fontSize: "11px",
          opacity: ".75",
          marginBottom: "3px"
        });
        teamLine.textContent = langText4(
          `团队:${formatRate(entry.teamDps || 0)} DPS · ${formatDamage(entry.teamDamage || 0)}${entry.teamKills > 0 ? ` · ${entry.teamKills} 次击杀` : ""} · ${(entry.fragments || []).length || 1} 个片段`,
          `Team: ${formatRate(entry.teamDps || 0)} DPS · ${formatDamage(entry.teamDamage || 0)}${entry.teamKills > 0 ? ` · ${entry.teamKills} kills` : ""} · ${(entry.fragments || []).length || 1} fragments`
        );
        block.appendChild(teamLine);
        if ((entry.fragments || []).length > 1) {
          const details = document.createElement("details");
          details.dataset.kikimeter = "true";
          const summary = document.createElement("summary");
          summary.textContent = langText4(
            "查看断线续传片段",
            "View reconnect fragments"
          );
          summary.style.cursor = "pointer";
          details.appendChild(summary);
          entry.fragments.forEach((f, i) => {
            const line = el("div", {
              fontSize: "10px",
              opacity: ".65",
              paddingLeft: "9px"
            });
            line.textContent = langText4(
              `片段 ${i + 1}|${localizeReason(f.reason)}|${formatDuration2((f.durationMs || 0) / 1e3)}|伤害 ${formatDamage(f.teamDamage || 0)}`,
              `Fragment ${i + 1} | ${localizeReason(f.reason)} | ${formatDuration2((f.durationMs || 0) / 1e3)} | Damage ${formatDamage(f.teamDamage || 0)}`
            );
            details.appendChild(line);
          });
          block.appendChild(details);
        }
        (entry.players || []).forEach((p) => {
          if (p.classId) ClassSystem.setDetected(p.name, p.classId);
          const pct = entry.teamDamage > 0 ? (p.damage / entry.teamDamage * 100).toFixed(0) : "0";
          const pLine = el("div", {
            fontSize: "11px",
            display: "flex",
            alignItems: "center",
            gap: "4px"
          });
          const icon = iconElement(
            ClassSystem.get(p.name).icon,
            ClassSystem.get(p.name).label
          );
          Object.assign(icon.style, {
            width: "15px",
            height: "15px",
            objectFit: "contain"
          });
          const nameEl = el("span", { color: "#e8eaf6" });
          nameEl.textContent = p.name;
          const statsEl = el("span", { opacity: ".7", marginLeft: "auto" });
          statsEl.textContent = formatRate(p.dps || 0) + langText4("/秒 · ", "/s · ") + formatDamage(p.damage || 0) + " (" + pct + "%)";
          pLine.append(icon, nameEl, statsEl);
          block.appendChild(pLine);
        });
        container.appendChild(block);
      });
      const clearBtn = document.createElement("button");
      clearBtn.textContent = langText4(
        `清空${(TYPES.find((t21) => t21.id === historyFilter) || {}).label}记录`,
        `Clear ${(TYPES.find((t21) => t21.id === historyFilter) || {}).label} records`
      );
      Object.assign(clearBtn.style, {
        width: "100%",
        cursor: "pointer",
        background: "transparent",
        color: "rgba(255,255,255,.3)",
        border: "1px solid rgba(255,255,255,.15)",
        borderRadius: "4px",
        padding: "4px",
        fontSize: "11px",
        marginTop: "4px"
      });
      clearBtn.addEventListener("click", () => {
        HistoryStore.clear(historyFilter);
        renderHistory(container);
      });
      container.appendChild(clearBtn);
    }
    return {
      init,
      destroy,
      toggle,
      close,
      isOpen: () => panelOpen,
      renderPlayers,
      renderView,
      refreshSegments: () => refreshSegmentSelect(segmentSelect)
    };
  })();

  // src/features/dps/70-recount-compat.js
  var RecountPanel = /* @__PURE__ */ (() => {
    const langText4 = (zh, en) => Settings.getLanguage() === "en" ? en : zh;
    const MODES = [
      {
        id: "dmg",
        zh: "造成伤害",
        en: "Damage Done",
        value: "damage",
        perSecond: "dps"
      },
      {
        id: "heal",
        zh: "恢复量",
        en: "Healing",
        value: "healing",
        perSecond: "hps"
      },
      {
        id: "taken",
        zh: "承受伤害",
        en: "Damage Taken",
        value: "taken",
        perSecond: "takenPs"
      }
    ];
    let root = null, listEl = null, titleEl = null, segmentSelect = null, modeIdx = 0, open = false, graphObj = null, graphWrap = null;
    const highlighted = /* @__PURE__ */ new Set();
    function modeById(id) {
      const i = MODES.findIndex((m) => m.id === id);
      return i >= 0 ? i : 0;
    }
    function toggleGraph() {
      const show = !Settings.getRecountShowGraph();
      Settings.setRecountShowGraph(show);
      if (graphWrap) graphWrap.style.display = show ? "block" : "none";
    }
    function build() {
      if (root) return;
      modeIdx = modeById(Settings.getRecountMode());
      root = document.createElement("div");
      root.dataset.kikimeter = "true";
      Object.assign(root.style, {
        position: "fixed",
        zIndex: "9999",
        background: "linear-gradient(180deg,rgba(24,24,24,.98),rgba(8,8,8,.98))",
        border: "1px solid rgba(212,175,55,.58)",
        borderRadius: "5px",
        fontSize: "11px",
        color: "#f2f2f2",
        boxShadow: "0 4px 16px rgba(0,0,0,.6)",
        userSelect: "none"
      });
      const pos = Settings.getRecountPos();
      const size = Settings.getRecountSize() || { width: 270, height: 430 };
      const initialWidth = Math.max(
        120,
        Math.min(size.width, window.innerWidth - 16)
      );
      const initialLeft = pos && pos.left != null ? pos.left : 12;
      root.style.left = Math.max(0, Math.min(initialLeft, window.innerWidth - initialWidth)) + "px";
      root.style.top = pos && pos.top != null ? pos.top + "px" : "90px";
      root.style.width = initialWidth + "px";
      root.style.maxWidth = "calc(100vw - 8px)";
      root.style.boxSizing = "border-box";
      const header = document.createElement("div");
      Object.assign(header.style, {
        display: "flex",
        alignItems: "center",
        justifyContent: "space-between",
        padding: "4px 6px",
        cursor: "move",
        background: "rgba(255,255,255,.07)",
        borderRadius: "6px 6px 0 0",
        fontWeight: "bold"
      });
      titleEl = document.createElement("span");
      const btns = document.createElement("span");
      const mkBtn = (txt, tip, fn) => {
        const b = document.createElement("button");
        b.textContent = txt;
        b.title = tip;
        Object.assign(b.style, {
          cursor: "pointer",
          background: "transparent",
          color: "rgba(255,255,255,.6)",
          border: "none",
          font: "12px monospace",
          padding: "0 3px"
        });
        b.addEventListener("mouseenter", () => b.style.color = "#fff");
        b.addEventListener(
          "mouseleave",
          () => b.style.color = "rgba(255,255,255,.6)"
        );
        b.addEventListener("mousedown", (e) => e.stopPropagation());
        b.addEventListener("click", (e) => {
          e.stopPropagation();
          fn();
        });
        return b;
      };
      btns.appendChild(
        mkBtn("⚔", langText4("造成伤害", "Damage Done"), () => setMode(0))
      );
      btns.appendChild(
        mkBtn("✚", langText4("恢复量", "Healing"), () => setMode(1))
      );
      btns.appendChild(
        mkBtn("🛡", langText4("承受伤害", "Damage Taken"), () => setMode(2))
      );
      const graphBtn = mkBtn(
        "📈",
        langText4("显示或隐藏趋势图", "Show or hide trend graph"),
        () => toggleGraph()
      );
      btns.appendChild(graphBtn);
      btns.appendChild(
        mkBtn("✕", langText4("关闭", "Close"), () => toggle(false))
      );
      header.append(titleEl, btns);
      root.appendChild(header);
      segmentSelect = buildSegmentPicker(() => {
        KikiMeter.refreshSegments();
        render();
      });
      Object.assign(segmentSelect.style, {
        display: "block",
        width: "calc(100% - 8px)",
        margin: "4px",
        boxSizing: "border-box",
        flex: "none"
      });
      root.appendChild(segmentSelect);
      listEl = document.createElement("div");
      Object.assign(listEl.style, {
        maxHeight: size.height + "px",
        overflowY: "auto",
        padding: "3px"
      });
      root.appendChild(listEl);
      graphObj = buildDetailsGraph();
      graphWrap = document.createElement("div");
      Object.assign(graphWrap.style, { padding: "4px 6px 6px 6px" });
      graphWrap.appendChild(graphObj.canvas);
      graphWrap.style.display = Settings.getRecountShowGraph() ? "block" : "none";
      root.appendChild(graphWrap);
      const resizer = document.createElement("div");
      Object.assign(resizer.style, {
        position: "absolute",
        right: "0",
        bottom: "0",
        width: "14px",
        height: "14px",
        cursor: "nwse-resize",
        background: "transparent"
      });
      resizer.innerHTML = '<svg width="10" height="10" style="position:absolute;right:2px;bottom:2px;opacity:.4"><path d="M10 0 L10 10 L0 10 Z" fill="currentColor"/></svg>';
      root.appendChild(resizer);
      let resize = null;
      const MIN_W = 180, MAX_W = 520, MIN_H = 100, MAX_H = 800;
      resizer.addEventListener("mousedown", (e) => {
        resize = {
          sx: e.clientX,
          sy: e.clientY,
          sw: root.offsetWidth,
          sh: listEl.offsetHeight
        };
        e.preventDefault();
        e.stopPropagation();
      });
      window.addEventListener("mousemove", (e) => {
        if (!resize) return;
        const maxWidth = Math.max(120, Math.min(MAX_W, window.innerWidth - 8));
        const w = Math.min(
          maxWidth,
          Math.max(
            Math.min(MIN_W, maxWidth),
            resize.sw + (e.clientX - resize.sx)
          )
        );
        const h = Math.min(
          MAX_H,
          Math.max(MIN_H, resize.sh + (e.clientY - resize.sy))
        );
        root.style.width = w + "px";
        listEl.style.maxHeight = h + "px";
      });
      window.addEventListener("mouseup", () => {
        if (!resize) return;
        resize = null;
        Settings.setRecountSize({
          width: root.offsetWidth,
          height: listEl.offsetHeight
        });
      });
      let drag = null;
      header.addEventListener("mousedown", (e) => {
        drag = {
          dx: e.clientX - root.offsetLeft,
          dy: e.clientY - root.offsetTop
        };
        e.preventDefault();
      });
      window.addEventListener("mousemove", (e) => {
        if (!drag) return;
        root.style.left = Math.max(
          0,
          Math.min(e.clientX - drag.dx, window.innerWidth - root.offsetWidth)
        ) + "px";
        root.style.top = Math.max(0, e.clientY - drag.dy) + "px";
      });
      window.addEventListener("mouseup", () => {
        if (!drag) return;
        drag = null;
        Settings.setRecountPos({ left: root.offsetLeft, top: root.offsetTop });
      });
      document.body.appendChild(root);
      root.style.display = "none";
    }
    function setMode(i) {
      modeIdx = i;
      Settings.setRecountMode(MODES[i].id);
      render();
    }
    function toggle(v) {
      build();
      open = v === void 0 ? !open : v;
      if (open) KikiMeter.close();
      root.style.display = open ? "block" : "none";
      if (open) render();
      return open;
    }
    function render(view = ViewData.get()) {
      if (!open || !root) return;
      refreshSegmentSelect(segmentSelect);
      if (graphObj && Settings.getRecountShowGraph())
        graphObj.render(view.graphPoints || []);
      const mode = MODES[modeIdx];
      titleEl.textContent = langText4(mode.zh, mode.en);
      const rows2 = (view.players || []).map((p) => ({
        n: p.name,
        synthetic: p.synthetic,
        v: Number(p[mode.value]) || 0,
        ps: Number(p[mode.perSecond]) || 0,
        breakdown: mode.id === "dmg" ? p.breakdown : mode.id === "taken" ? p.takenBreakdown : null
      })).filter((r) => r.v > 0).sort((a, b) => b.v - a.v);
      const total = rows2.reduce((s, r) => s + r.v, 0) || 1;
      const max = rows2.length ? rows2[0].v : 1;
      renderDetailsRows(
        listEl,
        rows2.map((r) => ({
          name: r.n,
          synthetic: r.synthetic,
          value: r.v,
          ps: r.ps,
          pct: 100 * r.v / total,
          breakdown: r.breakdown,
          rateLabel: mode.id === "heal" ? "HPS" : mode.id === "taken" ? "DTPS" : "DPS",
          breakdownTitle: mode.id === "taken" ? Settings.getLanguage() === "en" ? "Damage Taken Sources" : "承伤来源" : void 0,
          breakdownRateLabel: mode.id === "taken" ? "DTPS" : "DPS"
        })),
        render
      );
    }
    return { toggle, render, isOpen: () => open };
  })();

  // src/features/dps/90-application.js
  var langText3 = (zh, en) => Settings.getLanguage() === "en" ? en : zh;
  function start(scope) {
    installThemeFont();
    let currentPlayerNames = [];
    let currentIsBoss = false;
    let hasConfirmedCombat = false;
    let pendingReconnect = false;
    let lastActiveSave = 0;
    const cached = HistoryStore.loadActive();
    if (cached) {
      try {
        Session.restore(cached);
        currentPlayerNames = Session.getAllPlayerNames();
        pendingReconnect = true;
        hasConfirmedCombat = true;
      } catch (e) {
        HistoryStore.clearActive();
        console.warn("[KikiMeter] 已忽略损坏的活动战斗缓存。");
      }
    }
    function buildHistoryEntry() {
      const snap = Session.serialize(), m = snap.meta || {};
      if (!hasConfirmedCombat && !m.combatKey) return null;
      const names = currentPlayerNames.length ? currentPlayerNames : Session.getAllPlayerNames();
      const type = SocketHook.isGuildBattle() ? "trial" : SocketHook.isInLabyrinth() ? "labyrinth" : m.type || "combat";
      return {
        schemaVersion: 2,
        id: m.id || "fight-" + (m.characterId || "unknown") + "-" + (m.combatKey || Date.now()),
        type,
        characterId: m.characterId || "unknown",
        combatKey: m.combatKey || "",
        startedAt: m.startedAt || snap.savedAt,
        endedAt: snap.savedAt,
        date: m.startedAt || snap.savedAt,
        durationMs: snap.durationMs,
        durationSeconds: Math.floor(snap.durationMs / 1e3),
        teamDps: snap.durationMs > 0 ? snap.teamDamage / (snap.durationMs / 1e3) : 0,
        teamDamage: snap.teamDamage,
        teamKills: Session.getTeamKills(),
        classes: snap.classes,
        fragments: snap.fragments,
        graph: snap.graph,
        players: names.map((n) => ({
          name: n,
          classId: ClassSystem.classFor(n),
          damage: Session.getPlayerDamage(n),
          dps: Session.getPlayerDps(n),
          kills: Session.getPlayerKills(n),
          hps: Session.getPlayerHps(n),
          healing: Session.getPlayerHealing(n),
          taken: Session.getPlayerTaken(n),
          sources: Session.getPlayerDamageSources(n),
          takenSources: Session.getPlayerTakenSources(n),
          accuracy: Session.getPlayerAccuracy(n)
        }))
      };
    }
    function saveCurrentSession(reason = "归档") {
      const entry = buildHistoryEntry();
      if (!entry) return null;
      entry.endReason = reason;
      HistoryStore.push(entry);
      return entry;
    }
    function persistActive(force = false) {
      if (!hasConfirmedCombat) return false;
      const now = Date.now();
      if (!force && now - lastActiveSave < 2e3) return false;
      lastActiveSave = now;
      return HistoryStore.saveActive(Session.serialize());
    }
    function beginEncounter(detail, typeHint) {
      const characterId = String(
        detail.characterId || SocketHook.getCharacterId() || "unknown"
      );
      const identity = CombatIdentity.resolve(detail, typeHint, characterId), key = identity.key;
      const old = Session.getMeta();
      const sameEncounter = old.combatKey && CombatIdentity.matches(old, identity, typeHint, characterId);
      const oldStages = Array.isArray(old.trialStageIds) ? old.trialStageIds : [];
      const stageId = typeHint === "trial" ? String(detail.stageId || identity.rawKey || "") : "";
      const isNewStage = stageId && !oldStages.includes(stageId);
      if (sameEncounter) {
        if (Session.isFrozen()) {
          if (pendingReconnect) Session.resume("断线续传");
          else if (typeHint === "trial" && isNewStage)
            Session.resumeTrialTier("进入下一层");
          else Session.resume("继续战斗");
        }
        if (typeHint === "trial")
          Session.setMeta({
            id: "fight-" + characterId + "-" + key,
            combatKey: key,
            type: "trial",
            trialDay: identity.day,
            trialStageIds: isNewStage ? [...oldStages, stageId] : oldStages,
            manualReset: false
          });
      } else {
        if (old.combatKey) {
          Session.freeze("开始另一场战斗");
          saveCurrentSession("开始另一场战斗");
        }
        const startedAt = (/* @__PURE__ */ new Date()).toISOString();
        Session.reset({
          id: "fight-" + characterId + "-" + (key || Date.now()),
          combatKey: key,
          characterId,
          startedAt,
          type: typeHint || "combat"
        });
        if (typeHint === "trial")
          Session.setMeta({
            trialDay: identity.day,
            trialStageIds: stageId ? [stageId] : [],
            manualReset: false
          });
      }
      ClassSystem.applyClasses(detail.classes);
      hasConfirmedCombat = true;
      pendingReconnect = false;
      persistActive(true);
    }
    function resetSession(reason) {
      const old = Session.getMeta();
      Session.freeze(reason);
      saveCurrentSession(reason);
      HistoryStore.clearActive();
      const now = Date.now();
      Session.reset({
        id: "fight-" + (old.characterId || "unknown") + "-manual-" + now,
        combatKey: (old.combatKey || "manual") + "-manual-" + now,
        characterId: old.characterId || SocketHook.getCharacterId() || "unknown",
        startedAt: new Date(now).toISOString(),
        type: old.type || "combat",
        manualReset: true
      });
      hasConfirmedCombat = true;
      currentPlayerNames = Session.getAllPlayerNames();
      persistActive(true);
      console.info("[KikiMeter] 已结束当前记录并新建记录:" + reason);
    }
    function buildClipboardText() {
      const view = ViewData.get(), total = view.teamDamage;
      const d = /* @__PURE__ */ new Date();
      const dateStr = d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
      let out = langText3(
        `=== 银河奶牛 DPS 统计|${view.label}|${dateStr}|${formatDuration2(view.elapsed)} ===
`,
        `=== MWI DPS Meter | ${view.label} | ${dateStr} | ${formatDuration2(view.elapsed)} ===
`
      );
      out += langText3(
        `团队:${formatRate(view.teamDps)} DPS|总伤害 ${formatDamage(total)}`,
        `Team: ${formatRate(view.teamDps)} DPS | Total damage ${formatDamage(total)}`
      );
      if (view.teamKills > 0)
        out += langText3(`|击杀 ${view.teamKills}`, ` | Kills ${view.teamKills}`);
      out += "\n";
      view.players.forEach((p) => {
        const pct = total > 0 ? ((Number(p.damage) || 0) / total * 100).toFixed(0) : "0";
        const name = p.name.padEnd(12).slice(0, 12);
        out += name + langText3(":", ": ") + formatRate(p.dps || 0).padStart(6) + langText3(" DPS|", " DPS | ");
        out += formatDamage(Number(p.damage) || 0).padStart(7) + " (" + pct + "%)";
        if (Number(p.kills) > 0)
          out += langText3(`|击杀 ${p.kills}`, ` | Kills ${p.kills}`);
        if (Settings.getShowHealing() && Number(p.hps) > 0.1)
          out += langText3(
            `|HPS ${formatRate(p.hps)}`,
            ` | HPS ${formatRate(p.hps)}`
          );
        out += "\n";
      });
      return out;
    }
    function renderSelectedPanels() {
      const view = ViewData.get();
      KikiMeter.renderView(view);
    }
    KikiMeter.init({
      onReset: () => resetSession("手动结束"),
      onSegmentChange: renderSelectedPanels,
      onCopy: (btn) => {
        const compact = btn && btn.dataset.compactAction === "true", original = btn && btn.textContent;
        navigator.clipboard.writeText(buildClipboardText()).then(() => {
          btn.textContent = compact ? "✓" : langText3("✓ 已复制", "✓ Copied");
          setTimeout(() => {
            btn.textContent = original || langText3("复制统计", "Copy statistics");
          }, 2e3);
        }).catch(() => {
          btn.textContent = compact ? "!" : langText3("✗ 复制失败", "✗ Copy failed");
          setTimeout(() => {
            btn.textContent = original || langText3("复制统计", "Copy statistics");
          }, 2e3);
        });
      }
    });
    scope.event(SocketHook.bus, "guildBattleDetected", (ev) => {
      const detail = { ...ev.detail || {} };
      if (!detail.combatKey)
        detail.combatKey = "guild-fallback-" + CombatIdentity.dayStamp(/* @__PURE__ */ new Date());
      if (!detail.stageId) detail.stageId = String(detail.combatKey);
      beginEncounter(detail, "trial");
    });
    scope.event(SocketHook.bus, "guildTrialEnded", () => {
      Session.freeze("公会试炼阶段结束");
      saveCurrentSession("公会试炼阶段结束");
      persistActive(true);
      hasConfirmedCombat = true;
      console.info(
        "[KikiMeter] 公会试炼阶段已结束;当天进入下一关时将继续累计。"
      );
    });
    scope.event(SocketHook.bus, "guildSlotRenamed", (ev) => {
      Session.renamePlayer(ev.detail.oldName, ev.detail.newName);
    });
    scope.event(SocketHook.bus, "newBattle", (ev) => {
      if (ev.detail && ev.detail.parallelGuildBattle) return;
      beginEncounter(
        ev.detail,
        SocketHook.isInLabyrinth() ? "labyrinth" : "combat"
      );
      currentPlayerNames = ev.detail.names;
      currentIsBoss = ev.detail.isBoss;
      Session.setBoss(ev.detail.isBoss);
    });
    function acceptsCombatEvent(detail = {}) {
      return combatEventMatchesSession(detail, Session.getMeta());
    }
    scope.event(SocketHook.bus, "damage", (ev) => {
      if (!acceptsCombatEvent(ev.detail)) return;
      Session.addTeamDamage(ev.detail.amount, ev.detail.ts);
      persistActive();
    });
    scope.event(SocketHook.bus, "playerDamage", (ev) => {
      if (acceptsCombatEvent(ev.detail))
        Session.addPlayerDamage(
          ev.detail.name,
          ev.detail.amount,
          ev.detail.source
        );
    });
    scope.event(SocketHook.bus, "attackResolved", (ev) => {
      if (acceptsCombatEvent(ev.detail))
        Session.addPlayerAccuracy(
          ev.detail.name,
          ev.detail.hit,
          ev.detail.targets
        );
    });
    scope.event(SocketHook.bus, "healing", (ev) => {
      if (acceptsCombatEvent(ev.detail))
        Session.addPlayerHealing(ev.detail.name, ev.detail.amount);
    });
    scope.event(SocketHook.bus, "playerDamageTaken", (ev) => {
      if (acceptsCombatEvent(ev.detail))
        Session.addPlayerTaken(
          ev.detail.name,
          ev.detail.amount,
          ev.detail.source
        );
    });
    scope.event(SocketHook.bus, "kill", (ev) => {
      if (acceptsCombatEvent(ev.detail) && ev.detail.name)
        Session.addPlayerKill(ev.detail.name);
    });
    scope.event(SocketHook.bus, "socketReconnected", (ev) => {
      const next = String(ev.detail && ev.detail.characterId || "unknown"), old = Session.getMeta();
      if (old.characterId && old.characterId !== "unknown" && next !== "unknown" && String(old.characterId) !== next) {
        Session.freeze("切换角色");
        saveCurrentSession("切换角色");
        HistoryStore.clearActive();
        Session.reset();
        hasConfirmedCombat = false;
        currentPlayerNames = [];
      } else if (hasConfirmedCombat) {
        Session.pause("连接中断");
        persistActive(true);
        pendingReconnect = true;
      }
    });
    scope.event(ClassDebug.bus, "change", () => {
      if (Settings.getMainMode() === "debug") renderSelectedPanels();
    });
    scope.event(ClassProbe.bus, "change", () => {
      if (Settings.getMainMode() === "debug") renderSelectedPanels();
    });
    scope.event(ClassSystem.bus, "change", () => {
      renderSelectedPanels();
      persistActive();
    });
    scope.event(SocketHook.bus, "newCombatInstance", () => {
    });
    scope.event(document, "visibilitychange", () => {
      if (document.hidden) persistActive(true);
    });
    scope.event(window, "pagehide", () => {
      if (hasConfirmedCombat) {
        Session.pause("页面关闭");
        persistActive(true);
      }
    });
    scope.event(window, "pageshow", (ev) => {
      if (ev.persisted && hasConfirmedCombat && Session.isFrozen()) {
        Session.resume("页面恢复");
        persistActive(true);
      }
    });
    let refreshTimer2 = null;
    const refresh = () => {
      refreshTimer2 = null;
      Session.advanceBuckets();
      persistActive();
      if (KikiMeter.isOpen()) renderSelectedPanels();
      refreshTimer2 = setTimeout(refresh, Settings.getRefreshInterval());
    };
    refreshTimer2 = setTimeout(refresh, Settings.getRefreshInterval());
    scope.add(() => {
      if (refreshTimer2 !== null) clearTimeout(refreshTimer2);
      refreshTimer2 = null;
    });
    Object.assign(MWI, {
      enabled: true,
      bus: SocketHook.bus,
      getSessionDps: Session.getTeamDps,
      getSessionDamage: Session.getTeamDamage,
      getTeamKills: Session.getTeamKills,
      getPlayerDps: Session.getPlayerDps,
      getPlayerDamage: Session.getPlayerDamage,
      getPlayerDamageSources: Session.getPlayerDamageSources,
      getPlayerTakenSources: Session.getPlayerTakenSources,
      getPlayerTaken: Session.getPlayerTaken,
      getPlayerHps: Session.getPlayerHps,
      getPlayerKills: Session.getPlayerKills,
      getCurrentBattle: () => Session.serialize(),
      getBattleHistory: (type) => HistoryStore.getAll(type),
      getDisplayedSegment: () => ViewData.get(),
      listSegments: () => SegmentSelection.options().map((x) => ({
        key: x.key,
        label: x.label,
        current: !!x.current
      })),
      selectSegment: (key) => {
        SegmentSelection.select(key);
        renderSelectedPanels();
        return ViewData.get();
      },
      getClassDiagnostics: ClassSystem.diagnostics,
      getClassDebugEvents: ClassDebug.get,
      getClassDebugReport: ClassDebug.report,
      clearClassDebugReport: ClassDebug.clear,
      startClassProbe: ClassProbe.start,
      stopClassProbe: ClassProbe.stop,
      getClassProbeStatus: ClassProbe.status,
      getClassProbeReport: ClassProbe.report,
      downloadClassProbeReport: ClassProbe.download,
      clearClassProbe: ClassProbe.clear,
      setPlayerClass: (name, classId) => {
        ClassSystem.setOverride(name, classId);
        renderSelectedPanels();
      },
      getLanguage: Settings.getLanguage,
      setLanguage: (language) => {
        Settings.setLanguage(language);
        DamageBreakdownTooltip.close();
        renderSelectedPanels();
        return Settings.getLanguage();
      },
      forceSave: () => persistActive(true),
      resetSession: () => resetSession("控制台调用"),
      diagnostics: Diagnostics.summary,
      captureStart: Capture.start,
      captureStop: Capture.stop,
      captureDownload: Capture.download,
      captureSize: Capture.size,
      scanGuildNames: SocketHook.scanGuildNames,
      scanGuildNamesAttrs: SocketHook.scanGuildNamesAttrs,
      scanGuildNamesLoose: SocketHook.scanGuildNamesLoose,
      scanGuildNamesByLocalName: SocketHook.scanGuildNamesByLocalName,
      previewGuildNames: SocketHook.previewGuildNames,
      debugCombatUnitNames: SocketHook.debugCombatUnitNames,
      scanGuildNamesByEllipsis: SocketHook.scanGuildNamesByEllipsis,
      countMiniUnitNames: SocketHook.countMiniUnitNames,
      scanForOutlierMiniUnit: SocketHook.scanForOutlierMiniUnit
    });
    return () => {
      persistActive(true);
      ClassProbe.stop();
      DamageBreakdownTooltip.close();
      KikiMeter.destroy();
      document.getElementById("kikimeter-zh-theme")?.remove();
      MWI.enabled = false;
    };
  }

  // src/features/dps/index.js
  MWI.enabled = false;
  runtime.features.register({
    id: "dps",
    setting: "showDamage",
    initialize({ scope }) {
      let cleanupApplication = null;
      const activate = () => {
        if (cleanupApplication) return;
        cleanupApplication = start(scope);
        scope.add(
          runtime.onMessage("*", (payload) => {
            SocketHook.handleMessage(payload);
          })
        );
      };
      if (document.readyState === "loading") {
        scope.event(document, "DOMContentLoaded", activate, { once: true });
      } else {
        activate();
      }
      return () => cleanupApplication?.();
    }
  });
  Object.assign(runtime.api, {
    dps: MWI
  });

  // src/features/external-tools.js
  function addImportButtonForAmvoidguy() {
    const checkElem = () => {
      const selectedElement = document.querySelector(`button#buttonImportExport`);
      if (selectedElement) {
        clearInterval(timer);
        let button = document.createElement("button");
        selectedElement.parentNode.parentElement.parentElement.insertBefore(
          button,
          selectedElement.parentElement.parentElement.nextSibling
        );
        button.textContent = runtime.config.isZH ? "单人/组队导入(刷新游戏网页更新人物数据)" : "Import solo/group (Refresh game page to update character set)";
        button.style.backgroundColor = runtime.config.SCRIPT_COLOR_MAIN;
        button.style.padding = "5px";
        button.onclick = function() {
          console.log(
            runtime.config.isZH ? "[MWITools] 已点击战斗模拟器导入按钮。" : "[MWITools] Combat simulator import button clicked."
          );
          const getPriceButton = document.querySelector(`button#buttonGetPrices`);
          if (getPriceButton) {
            console.log(
              runtime.config.isZH ? "[MWITools] 正在刷新战斗模拟器价格。" : "[MWITools] Refreshing combat simulator prices."
            );
            getPriceButton.click();
          }
          importDataForAmvoidguy(button);
          return false;
        };
      }
    };
    let timer = setInterval(checkElem, 200);
  }
  async function importDataForAmvoidguy(button) {
    const [
      exportObj,
      playerIDs,
      importedPlayerPositions,
      zone,
      difficultyTier,
      isZoneDungeon,
      isParty
    ] = constructGroupExportObj();
    console.log(exportObj);
    console.log(playerIDs);
    document.querySelector(`a#group-combat-tab`).click();
    const importInputElem = document.querySelector(
      `input#inputSetGroupCombatAll`
    );
    importInputElem.value = JSON.stringify(exportObj);
    document.querySelector(`button#buttonImportSet`).click();
    document.querySelector(`a#player1-tab`).textContent = playerIDs[0];
    document.querySelector(`a#player2-tab`).textContent = playerIDs[1];
    document.querySelector(`a#player3-tab`).textContent = playerIDs[2];
    document.querySelector(`a#player4-tab`).textContent = playerIDs[3];
    document.querySelector(`a#player5-tab`).textContent = playerIDs[4];
    if (zone) {
      if (isZoneDungeon) {
        document.querySelector(`input#simDungeonToggle`).checked = true;
        document.querySelector(`input#simDungeonToggle`).dispatchEvent(new Event("change"));
        const selectDungeon = document.querySelector(`select#selectDungeon`);
        for (let i = 0; i < selectDungeon.options.length; i++) {
          if (selectDungeon.options[i].value === zone) {
            selectDungeon.options[i].selected = true;
            break;
          }
        }
      } else {
        document.querySelector(`input#simDungeonToggle`).checked = false;
        document.querySelector(`input#simDungeonToggle`).dispatchEvent(new Event("change"));
        const selectZone = document.querySelector(`select#selectZone`);
        for (let i = 0; i < selectZone.options.length; i++) {
          if (selectZone.options[i].value === zone) {
            selectZone.options[i].selected = true;
            break;
          }
        }
      }
      if (difficultyTier) {
        const selectDifficulty = document.querySelector(
          `select#selectDifficulty`
        );
        for (let i = 0; i < selectDifficulty.options.length; i++) {
          if (Number(selectDifficulty.options[i].value) === difficultyTier) {
            selectDifficulty.options[i].selected = true;
            break;
          }
        }
      }
    }
    for (let i = 0; i < 5; i++) {
      if (importedPlayerPositions[i]) {
        if (document.querySelector(
          `input#player${i + 1}.form-check-input.player-checkbox`
        )) {
          document.querySelector(
            `input#player${i + 1}.form-check-input.player-checkbox`
          ).checked = true;
          document.querySelector(
            `input#player${i + 1}.form-check-input.player-checkbox`
          ).dispatchEvent(new Event("change"));
        }
      } else {
        if (document.querySelector(
          `input#player${i + 1}.form-check-input.player-checkbox`
        )) {
          document.querySelector(
            `input#player${i + 1}.form-check-input.player-checkbox`
          ).checked = false;
          document.querySelector(
            `input#player${i + 1}.form-check-input.player-checkbox`
          ).dispatchEvent(new Event("change"));
        }
      }
    }
    document.querySelector(`input#inputSimulationTime`).value = 24;
    button.textContent = runtime.config.isZH ? "已导入" : "Imported";
    if (!isParty) {
      setTimeout(() => {
        document.querySelector(`button#buttonStartSimulation`).click();
      }, 500);
    }
  }
  function constructGroupExportObj() {
    const characterObj = JSON.parse(GM_getValue("init_character_data", ""));
    const clientObj = JSON.parse(GM_getValue("init_client_data", ""));
    let battleObj = null;
    if (GM_getValue("new_battle", "")) {
      battleObj = JSON.parse(GM_getValue("new_battle", ""));
    }
    const storedProfileList = JSON.parse(
      GM_getValue("profile_export_list", "[]")
    );
    const BLANK_PLAYER_JSON = `{"player":{"attackLevel":1,"magicLevel":1,"meleeLevel":1,"rangedLevel":1,"defenseLevel":1,"staminaLevel":1,"intelligenceLevel":1,"equipment":[]},"food":{"/action_types/combat":[{"itemHrid":""},{"itemHrid":""},{"itemHrid":""}]},"drinks":{"/action_types/combat":[{"itemHrid":""},{"itemHrid":""},{"itemHrid":""}]},"abilities":[{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"}],"triggerMap":{},"zone":"/actions/combat/fly","simulationTime":"100","houseRooms":{"/house_rooms/dairy_barn":0,"/house_rooms/garden":0,"/house_rooms/log_shed":0,"/house_rooms/forge":0,"/house_rooms/workshop":0,"/house_rooms/sewing_parlor":0,"/house_rooms/kitchen":0,"/house_rooms/brewery":0,"/house_rooms/laboratory":0,"/house_rooms/observatory":0,"/house_rooms/dining_room":0,"/house_rooms/library":0,"/house_rooms/dojo":0,"/house_rooms/gym":0,"/house_rooms/armory":0,"/house_rooms/archery_range":0,"/house_rooms/mystical_study":0}}`;
    const exportObj = {};
    exportObj[1] = BLANK_PLAYER_JSON;
    exportObj[2] = BLANK_PLAYER_JSON;
    exportObj[3] = BLANK_PLAYER_JSON;
    exportObj[4] = BLANK_PLAYER_JSON;
    exportObj[5] = BLANK_PLAYER_JSON;
    let isParty = false;
    const playerIDs = [
      "Player 1",
      "Player 2",
      "Player 3",
      "Player 4",
      "Player 5"
    ];
    const importedPlayerPositions = [false, false, false, false, false];
    let zone = "/actions/combat/fly";
    let isZoneDungeon = false;
    let difficultyTier = 0;
    if (!characterObj?.partyInfo?.partySlotMap) {
      exportObj[1] = JSON.stringify(
        constructSelfPlayerExportObjFromInitCharacterData(
          characterObj,
          clientObj
        )
      );
      playerIDs[0] = characterObj.character.name;
      importedPlayerPositions[0] = true;
      for (const action of characterObj.characterActions) {
        if (action && action.actionHrid.includes("/actions/combat/")) {
          zone = action.actionHrid;
          difficultyTier = action.difficultyTier;
          isZoneDungeon = clientObj.actionDetailMap[action.actionHrid]?.combatZoneInfo?.isDungeon;
          break;
        }
      }
    } else {
      isParty = true;
      let i = 1;
      for (const member of Object.values(characterObj.partyInfo.partySlotMap)) {
        if (member.characterID) {
          if (member.characterID === characterObj.character.id) {
            exportObj[i] = JSON.stringify(
              constructSelfPlayerExportObjFromInitCharacterData(
                characterObj,
                clientObj
              )
            );
            playerIDs[i - 1] = characterObj.character.name;
            importedPlayerPositions[i - 1] = true;
          } else {
            const profileList = storedProfileList.filter(
              (item) => item.characterID === member.characterID
            );
            if (profileList.length !== 1) {
              console.log(
                runtime.config.isZH ? `[MWITools] 找不到角色 ${member.characterID} 的已保存资料。` : `[MWITools] Cannot find a saved profile for character ${member.characterID}.`
              );
              playerIDs[i - 1] = runtime.config.isZH ? "需要点开资料" : "Open profile in game";
              i++;
              continue;
            }
            const profile = profileList[0];
            const battlePlayers = Array.isArray(battleObj?.players) ? battleObj.players : [];
            const battlePlayer = battlePlayers.find(
              (item) => item.character?.id === member.characterID
            ) ?? null;
            exportObj[i] = JSON.stringify(
              constructPlayerExportObjFromStoredProfile(
                profile,
                clientObj,
                battlePlayer
              )
            );
            playerIDs[i - 1] = profile.characterName;
            importedPlayerPositions[i - 1] = true;
          }
        }
        i++;
      }
      zone = characterObj.partyInfo?.party?.actionHrid;
      difficultyTier = characterObj.partyInfo?.party?.difficultyTier;
      isZoneDungeon = clientObj.actionDetailMap[zone]?.combatZoneInfo?.isDungeon;
    }
    return [
      exportObj,
      playerIDs,
      importedPlayerPositions,
      zone,
      difficultyTier,
      isZoneDungeon,
      isParty
    ];
  }
  function constructSelfPlayerExportObjFromInitCharacterData(characterObj, clientObj) {
    const playerObj = {};
    playerObj.player = {};
    for (const skill of characterObj.characterSkills) {
      if (skill.skillHrid.includes("stamina")) {
        playerObj.player.staminaLevel = skill.level;
      } else if (skill.skillHrid.includes("intelligence")) {
        playerObj.player.intelligenceLevel = skill.level;
      } else if (skill.skillHrid.includes("attack")) {
        playerObj.player.attackLevel = skill.level;
      } else if (skill.skillHrid.includes("melee")) {
        playerObj.player.meleeLevel = skill.level;
      } else if (skill.skillHrid.includes("defense")) {
        playerObj.player.defenseLevel = skill.level;
      } else if (skill.skillHrid.includes("ranged")) {
        playerObj.player.rangedLevel = skill.level;
      } else if (skill.skillHrid.includes("magic")) {
        playerObj.player.magicLevel = skill.level;
      }
    }
    playerObj.player.equipment = [];
    for (const item of characterObj.characterItems) {
      if (!item.itemLocationHrid.includes("/item_locations/inventory")) {
        playerObj.player.equipment.push({
          itemLocationHrid: item.itemLocationHrid,
          itemHrid: item.itemHrid,
          enhancementLevel: item.enhancementLevel
        });
      }
    }
    playerObj.food = {};
    playerObj.food["/action_types/combat"] = [];
    for (const food of characterObj.actionTypeFoodSlotsMap["/action_types/combat"]) {
      if (food) {
        playerObj.food["/action_types/combat"].push({
          itemHrid: food.itemHrid
        });
      } else {
        playerObj.food["/action_types/combat"].push({
          itemHrid: ""
        });
      }
    }
    playerObj.drinks = {};
    playerObj.drinks["/action_types/combat"] = [];
    for (const drink of characterObj.actionTypeDrinkSlotsMap["/action_types/combat"]) {
      if (drink) {
        playerObj.drinks["/action_types/combat"].push({
          itemHrid: drink.itemHrid
        });
      } else {
        playerObj.drinks["/action_types/combat"].push({
          itemHrid: ""
        });
      }
    }
    playerObj.abilities = [
      {
        abilityHrid: "",
        level: "1"
      },
      {
        abilityHrid: "",
        level: "1"
      },
      {
        abilityHrid: "",
        level: "1"
      },
      {
        abilityHrid: "",
        level: "1"
      },
      {
        abilityHrid: "",
        level: "1"
      }
    ];
    let normalAbillityIndex = 1;
    for (const ability of characterObj.combatUnit.combatAbilities) {
      if (ability && clientObj.abilityDetailMap[ability.abilityHrid].isSpecialAbility) {
        playerObj.abilities[0] = {
          abilityHrid: ability.abilityHrid,
          level: ability.level
        };
      } else if (ability) {
        playerObj.abilities[normalAbillityIndex++] = {
          abilityHrid: ability.abilityHrid,
          level: ability.level
        };
      }
    }
    playerObj.triggerMap = {
      ...characterObj.abilityCombatTriggersMap,
      ...characterObj.consumableCombatTriggersMap
    };
    playerObj.houseRooms = {};
    for (const house of Object.values(characterObj.characterHouseRoomMap)) {
      playerObj.houseRooms[house.houseRoomHrid] = house.level;
    }
    playerObj.achievements = {};
    for (const achievement of Object.values(characterObj.characterAchievements)) {
      playerObj.achievements[achievement.achievementHrid] = achievement.isCompleted;
    }
    return playerObj;
  }
  function constructPlayerExportObjFromStoredProfile(profile, clientObj, battlePlayer) {
    const playerObj = {};
    playerObj.player = {};
    for (const skill of profile.profile.characterSkills) {
      if (skill.skillHrid.includes("stamina")) {
        playerObj.player.staminaLevel = skill.level;
      } else if (skill.skillHrid.includes("intelligence")) {
        playerObj.player.intelligenceLevel = skill.level;
      } else if (skill.skillHrid.includes("attack")) {
        playerObj.player.attackLevel = skill.level;
      } else if (skill.skillHrid.includes("melee")) {
        playerObj.player.meleeLevel = skill.level;
      } else if (skill.skillHrid.includes("defense")) {
        playerObj.player.defenseLevel = skill.level;
      } else if (skill.skillHrid.includes("ranged")) {
        playerObj.player.rangedLevel = skill.level;
      } else if (skill.skillHrid.includes("magic")) {
        playerObj.player.magicLevel = skill.level;
      }
    }
    playerObj.player.equipment = [];
    if (profile.profile.wearableItemMap) {
      for (const key in profile.profile.wearableItemMap) {
        const item = profile.profile.wearableItemMap[key];
        playerObj.player.equipment.push({
          itemLocationHrid: item.itemLocationHrid,
          itemHrid: item.itemHrid,
          enhancementLevel: item.enhancementLevel
        });
      }
    }
    playerObj.food = {};
    playerObj.food["/action_types/combat"] = [];
    playerObj.drinks = {};
    playerObj.drinks["/action_types/combat"] = [];
    if (battlePlayer?.combatConsumables) {
      for (const foodOrDrink of battlePlayer.combatConsumables) {
        if (foodOrDrink.itemHrid.includes("coffee")) {
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: foodOrDrink.itemHrid
          });
        } else {
          playerObj.food["/action_types/combat"].push({
            itemHrid: foodOrDrink.itemHrid
          });
        }
      }
    } else {
      const weapon = profile.profile.wearableItemMap && (profile.profile.wearableItemMap["/item_locations/main_hand"]?.itemHrid || profile.profile.wearableItemMap["/item_locations/two_hand"]?.itemHrid);
      if (weapon) {
        if (weapon.includes("shooter") || weapon.includes("bow")) {
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/wisdom_coffee"
          });
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/super_ranged_coffee"
          });
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/critical_coffee"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/spaceberry_donut"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/spaceberry_cake"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/star_fruit_yogurt"
          });
        } else if (weapon.includes("boomstick") || weapon.includes("staff") || weapon.includes("trident")) {
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/wisdom_coffee"
          });
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/super_magic_coffee"
          });
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/channeling_coffee"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/spaceberry_cake"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/star_fruit_gummy"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/star_fruit_yogurt"
          });
        } else if (weapon.includes("bulwark")) {
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/wisdom_coffee"
          });
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/super_defense_coffee"
          });
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/super_stamina_coffee"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/spaceberry_donut"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/spaceberry_cake"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/star_fruit_yogurt"
          });
        } else {
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/wisdom_coffee"
          });
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/super_melee_coffee"
          });
          playerObj.drinks["/action_types/combat"].push({
            itemHrid: "/items/swiftness_coffee"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/spaceberry_donut"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/spaceberry_cake"
          });
          playerObj.food["/action_types/combat"].push({
            itemHrid: "/items/star_fruit_yogurt"
          });
        }
      }
    }
    playerObj.abilities = [
      {
        abilityHrid: "",
        level: "1"
      },
      {
        abilityHrid: "",
        level: "1"
      },
      {
        abilityHrid: "",
        level: "1"
      },
      {
        abilityHrid: "",
        level: "1"
      },
      {
        abilityHrid: "",
        level: "1"
      }
    ];
    if (profile.profile.equippedAbilities) {
      let normalAbillityIndex = 1;
      for (const ability of profile.profile.equippedAbilities) {
        if (ability && clientObj.abilityDetailMap[ability.abilityHrid].isSpecialAbility) {
          playerObj.abilities[0] = {
            abilityHrid: ability.abilityHrid,
            level: ability.level
          };
        } else if (ability) {
          playerObj.abilities[normalAbillityIndex++] = {
            abilityHrid: ability.abilityHrid,
            level: ability.level
          };
        }
      }
    }
    if (profile.profile.abilityCombatTriggersMap && profile.profile.consumableCombatTriggersMap) {
      playerObj.triggerMap = {
        ...profile.profile.abilityCombatTriggersMap,
        ...profile.profile.consumableCombatTriggersMap
      };
    }
    playerObj.houseRooms = {};
    for (const house of Object.values(profile.profile.characterHouseRoomMap)) {
      playerObj.houseRooms[house.houseRoomHrid] = house.level;
    }
    playerObj.achievements = {};
    for (const achievement of Object.values(
      profile.profile.characterAchievements
    )) {
      playerObj.achievements[achievement.achievementHrid] = achievement.isCompleted;
    }
    return playerObj;
  }
  async function observeResultsForAmvoidguy() {
    let resultDiv = document.querySelector(`div.row`)?.querySelectorAll(`div.col-md-5`)?.[2]?.querySelector(`div.row > div.col-md-5`);
    while (!resultDiv) {
      await new Promise((resolve) => setTimeout(resolve, 100));
      resultDiv = document.querySelector(`div.row`)?.querySelectorAll(`div.col-md-5`)?.[2]?.querySelector(`div.row > div.col-md-5`);
    }
    const deathDiv = document.querySelector(`div#simulationResultPlayerDeaths`);
    const expDiv = document.querySelector(`div#simulationResultExperienceGain`);
    const consumeDiv = document.querySelector(
      `div#simulationResultConsumablesUsed`
    );
    deathDiv.style.backgroundColor = "#FFEAE9";
    deathDiv.style.color = "black";
    expDiv.style.backgroundColor = "#CDFFDD";
    expDiv.style.color = "black";
    consumeDiv.style.backgroundColor = "#F0F8FF";
    consumeDiv.style.color = "black";
    let div = document.createElement("div");
    div.id = "tillLevel";
    div.style.backgroundColor = "#FFFFE0";
    div.style.color = "black";
    div.textContent = "";
    resultDiv.append(div);
    new MutationObserver((mutationsList) => {
      mutationsList.forEach((mutation) => {
        if (mutation.addedNodes.length >= 3) {
          handleResultForAmvoidguy(mutation.addedNodes, div);
        }
      });
    }).observe(expDiv, { childList: true, subtree: true });
  }
  function handleResultForAmvoidguy(expNodes, parentDiv) {
    const isZHIn3rdPartyWebsites = localStorage.getItem("i18nextLng")?.toLowerCase()?.startsWith("zh");
    let perHourGainExp = {
      stamina: 0,
      intelligence: 0,
      attack: 0,
      melee: 0,
      defense: 0,
      ranged: 0,
      magic: 0
    };
    expNodes.forEach((expNode) => {
      if (runtime.api.getOriTextFromElement(expNode.children[0]).includes("Stamina") || runtime.api.getOriTextFromElement(expNode.children[0]).includes("耐力")) {
        perHourGainExp.stamina = Number(expNode.children[1].textContent);
      } else if (runtime.api.getOriTextFromElement(expNode.children[0]).includes("Intelligence") || runtime.api.getOriTextFromElement(expNode.children[0]).includes("智力")) {
        perHourGainExp.intelligence = Number(expNode.children[1].textContent);
      } else if (runtime.api.getOriTextFromElement(expNode.children[0]).includes("Attack") || runtime.api.getOriTextFromElement(expNode.children[0]).includes("攻击")) {
        perHourGainExp.attack = Number(expNode.children[1].textContent);
      } else if (runtime.api.getOriTextFromElement(expNode.children[0]).includes("Melee") || runtime.api.getOriTextFromElement(expNode.children[0]).includes("近战")) {
        perHourGainExp.melee = Number(expNode.children[1].textContent);
      } else if (runtime.api.getOriTextFromElement(expNode.children[0]).includes("Defense") || runtime.api.getOriTextFromElement(expNode.children[0]).includes("防御")) {
        perHourGainExp.defense = Number(expNode.children[1].textContent);
      } else if (runtime.api.getOriTextFromElement(expNode.children[0]).includes("Ranged") || runtime.api.getOriTextFromElement(expNode.children[0]).includes("远程")) {
        perHourGainExp.ranged = Number(expNode.children[1].textContent);
      } else if (runtime.api.getOriTextFromElement(expNode.children[0]).includes("Magic") || runtime.api.getOriTextFromElement(expNode.children[0]).includes("魔法")) {
        perHourGainExp.magic = Number(expNode.children[1].textContent);
      }
    });
    let data = GM_getValue("init_character_data", null);
    let obj = JSON.parse(data);
    if (!obj || !obj.characterSkills || !obj.currentTimestamp) {
      console.error(
        runtime.config.isZH ? "[MWITools] 无法导出:本地没有角色数据。" : "[MWITools] Export failed because no character data is stored locally."
      );
      return;
    }
    let skillLevels = {};
    for (const skill of obj.characterSkills) {
      if (skill.skillHrid.includes("stamina")) {
        skillLevels.stamina = {};
        skillLevels.stamina.skillName = "Stamina";
        skillLevels.stamina.skillZhName = "耐力";
        skillLevels.stamina.currentLevel = skill.level;
        skillLevels.stamina.currentExp = skill.experience;
      } else if (skill.skillHrid.includes("intelligence")) {
        skillLevels.intelligence = {};
        skillLevels.intelligence.skillName = "Intelligence";
        skillLevels.intelligence.skillZhName = "智力";
        skillLevels.intelligence.currentLevel = skill.level;
        skillLevels.intelligence.currentExp = skill.experience;
      } else if (skill.skillHrid.includes("attack")) {
        skillLevels.attack = {};
        skillLevels.attack.skillName = "Attack";
        skillLevels.attack.skillZhName = "攻击";
        skillLevels.attack.currentLevel = skill.level;
        skillLevels.attack.currentExp = skill.experience;
      } else if (skill.skillHrid.includes("melee")) {
        skillLevels.melee = {};
        skillLevels.melee.skillName = "Melee";
        skillLevels.melee.skillZhName = "近战";
        skillLevels.melee.currentLevel = skill.level;
        skillLevels.melee.currentExp = skill.experience;
      } else if (skill.skillHrid.includes("defense")) {
        skillLevels.defense = {};
        skillLevels.defense.skillName = "Defense";
        skillLevels.defense.skillZhName = "防御";
        skillLevels.defense.currentLevel = skill.level;
        skillLevels.defense.currentExp = skill.experience;
      } else if (skill.skillHrid.includes("ranged")) {
        skillLevels.ranged = {};
        skillLevels.ranged.skillName = "Ranged";
        skillLevels.ranged.skillZhName = "远程";
        skillLevels.ranged.currentLevel = skill.level;
        skillLevels.ranged.currentExp = skill.experience;
      } else if (skill.skillHrid.includes("magic")) {
        skillLevels.magic = {};
        skillLevels.magic.skillName = "Magic";
        skillLevels.magic.skillZhName = "魔法";
        skillLevels.magic.currentLevel = skill.level;
        skillLevels.magic.currentExp = skill.experience;
      }
    }
    const skillNamesInOrder = [
      "stamina",
      "intelligence",
      "attack",
      "melee",
      "defense",
      "ranged",
      "magic"
    ];
    let hTMLStr = "";
    for (const skill of skillNamesInOrder) {
      hTMLStr += `<div id="${"inputDiv_" + skill}" style="display: flex; justify-content: flex-end">${isZHIn3rdPartyWebsites ? skillLevels[skill].skillZhName : skillLevels[skill].skillName}${isZHIn3rdPartyWebsites ? "到" : " to level "}<input id="${"input_" + skill}" type="number" value="${skillLevels[skill].currentLevel + 1}" min="${skillLevels[skill].currentLevel + 1}" max="200">${isZHIn3rdPartyWebsites ? "级" : ""}</div>`;
    }
    hTMLStr += `<div id="script_afterDays" style="display: flex; justify-content: flex-end"><input id="script_afterDays_input" type="number" value="1" min="0" max="200">${isZHIn3rdPartyWebsites ? "天后" : "days after"}</div>`;
    hTMLStr += `<div id="needDiv"></div>`;
    hTMLStr += `<div id="needListDiv"></div>`;
    parentDiv.innerHTML = hTMLStr;
    for (const skill of skillNamesInOrder) {
      const skillDiv = parentDiv.querySelector(`div#${"inputDiv_" + skill}`);
      const skillInput = parentDiv.querySelector(`input#${"input_" + skill}`);
      skillInput.onchange = () => {
        calculateTill(
          skill,
          skillInput,
          skillLevels,
          parentDiv,
          perHourGainExp,
          isZHIn3rdPartyWebsites
        );
      };
      skillInput.addEventListener("keyup", function(evt) {
        calculateTill(
          skill,
          skillInput,
          skillLevels,
          parentDiv,
          perHourGainExp,
          isZHIn3rdPartyWebsites
        );
      });
      skillDiv.onclick = () => {
        calculateTill(
          skill,
          skillInput,
          skillLevels,
          parentDiv,
          perHourGainExp,
          isZHIn3rdPartyWebsites
        );
      };
    }
    const daysAfterDiv = parentDiv.querySelector(`div#script_afterDays`);
    const daysAfterInput = parentDiv.querySelector(
      `input#script_afterDays_input`
    );
    daysAfterInput.onchange = () => {
      calculateAfterDays(
        daysAfterInput,
        skillLevels,
        parentDiv,
        perHourGainExp,
        skillNamesInOrder,
        isZHIn3rdPartyWebsites
      );
    };
    daysAfterInput.addEventListener("keyup", function(evt) {
      calculateAfterDays(
        daysAfterInput,
        skillLevels,
        parentDiv,
        perHourGainExp,
        skillNamesInOrder,
        isZHIn3rdPartyWebsites
      );
    });
    daysAfterDiv.onclick = () => {
      calculateAfterDays(
        daysAfterInput,
        skillLevels,
        parentDiv,
        perHourGainExp,
        skillNamesInOrder,
        isZHIn3rdPartyWebsites
      );
    };
    const expensesSpan = document.querySelector(`span#expensesSpan`);
    const revenueSpan = document.querySelector(`span#revenueSpan`);
    const profitSpan = document.querySelector(`span#profitPreview`);
    const expenseDiv = document.querySelector(`div#script_expense`);
    const revenueDiv = document.querySelector(`div#script_revenue`);
    if (expenseDiv && expenseDiv) {
      expenseDiv.textContent = expensesSpan.parentNode.textContent;
      revenueDiv.textContent = revenueSpan.parentNode.textContent;
    } else {
      profitSpan.parentNode.insertAdjacentHTML(
        "beforeend",
        `<div id="script_expense" style="background-color: #DCDCDC; color: black;">${expensesSpan.parentNode.textContent}</div><div id="script_revenue" style="background-color: #DCDCDC; color: black;">${revenueSpan.parentNode.textContent}</div>`
      );
    }
  }
  function calculateAfterDays(daysAfterInput, skillLevels, parentDiv, perHourGainExp, skillNamesInOrder, isZHIn3rdPartyWebsites) {
    const initData_levelExperienceTable2 = JSON.parse(
      GM_getValue("init_client_data", null)
    ).levelExperienceTable;
    const days = Number(daysAfterInput.value);
    parentDiv.querySelector(`div#needDiv`).textContent = `${isZHIn3rdPartyWebsites ? "" : "After"} ${days} ${isZHIn3rdPartyWebsites ? "天后:" : "days: "}`;
    const listDiv = parentDiv.querySelector(`div#needListDiv`);
    let html = "";
    let resultLevels = {};
    for (const skillName of skillNamesInOrder) {
      for (const skill of Object.values(skillLevels)) {
        if (skill.skillName.toLowerCase() === skillName.toLowerCase()) {
          const exp = skill.currentExp + perHourGainExp[skill.skillName.toLowerCase()] * days * 24;
          let level = 1;
          while (initData_levelExperienceTable2[level] < exp) {
            level++;
          }
          level--;
          const minExpAtLevel = initData_levelExperienceTable2[level];
          const maxExpAtLevel = initData_levelExperienceTable2[level + 1] - 1;
          const expSpanInLevel = maxExpAtLevel - minExpAtLevel;
          const levelPercentage = Number(
            (exp - minExpAtLevel) / expSpanInLevel * 100
          ).toFixed(1);
          resultLevels[skillName.toLowerCase()] = level;
          html += `<div>${isZHIn3rdPartyWebsites ? skill.skillZhName : skill.skillName} ${isZHIn3rdPartyWebsites ? "" : "level"} ${level} ${isZHIn3rdPartyWebsites ? "级" : ""} ${levelPercentage}%</div>`;
          break;
        }
      }
    }
    const combatLevel = 0.1 * (resultLevels.stamina + resultLevels.intelligence + resultLevels.defense + resultLevels.attack + Math.max(resultLevels.melee, resultLevels.ranged, resultLevels.magic)) + 0.5 * Math.max(
      resultLevels.attack,
      resultLevels.defense,
      resultLevels.melee,
      resultLevels.ranged,
      resultLevels.magic
    );
    html += `<div>${isZHIn3rdPartyWebsites ? "战斗等级:" : "Combat level: "} ${combatLevel.toFixed(1)}</div>`;
    listDiv.innerHTML = html;
  }
  function calculateTill(skillName, skillInputElem, skillLevels, parentDiv, perHourGainExp, isZHIn3rdPartyWebsites) {
    const initData_levelExperienceTable2 = JSON.parse(
      GM_getValue("init_client_data", null)
    ).levelExperienceTable;
    const targetLevel = Number(skillInputElem.value);
    parentDiv.querySelector(`div#needDiv`).textContent = `${isZHIn3rdPartyWebsites ? skillLevels[skillName].skillZhName : skillLevels[skillName].skillName} ${isZHIn3rdPartyWebsites ? "到" : "to level"} ${targetLevel} ${isZHIn3rdPartyWebsites ? "级 还需:" : " takes: "}`;
    const listDiv = parentDiv.querySelector(`div#needListDiv`);
    const currentLevel = Number(skillLevels[skillName].currentLevel);
    const currentExp = Number(skillLevels[skillName].currentExp);
    if (targetLevel > currentLevel && targetLevel <= 200) {
      if (perHourGainExp[skillName] === 0) {
        listDiv.innerHTML = isZHIn3rdPartyWebsites ? "永远" : "Forever";
      } else {
        let needExp = initData_levelExperienceTable2[targetLevel] - currentExp;
        let needHours = needExp / perHourGainExp[skillName];
        let html = "";
        html += `<div>[${hoursToReadableString(needHours)}]</div>`;
        const consumeDivs = document.querySelectorAll(
          `div#simulationResultConsumablesUsed div.row`
        );
        for (const elem of consumeDivs) {
          const conName = elem.children[0].textContent;
          const conPerHour = Number(elem.children[1].textContent);
          html += `<div>${conName} ${Number(conPerHour * needHours).toFixed(0)}</div>`;
        }
        listDiv.innerHTML = html;
      }
    } else {
      listDiv.innerHTML = isZHIn3rdPartyWebsites ? "输入错误" : "Input error";
    }
  }
  function addImportButtonForMooneycalc() {
    const checkElem = () => {
      const selectedElement = document.querySelector(`div[role="tablist"]`);
      if (selectedElement) {
        clearInterval(timer);
        const button = document.createElement("button");
        selectedElement.parentNode.insertBefore(
          button,
          selectedElement.nextSibling
        );
        button.textContent = runtime.config.isZH ? "导入人物数据 (刷新游戏网页更新人物数据)" : "Import character settings (Refresh game page to update character settings)";
        button.style.backgroundColor = runtime.config.SCRIPT_COLOR_MAIN;
        button.style.color = "black";
        button.style.padding = "5px";
        button.onclick = function() {
          console.log(
            runtime.config.isZH ? "[MWITools] 已点击 Mooneycalc 导入按钮。" : "[MWITools] Mooneycalc import button clicked."
          );
          importDataForMooneycalc(button);
          return false;
        };
      }
    };
    let timer = setInterval(checkElem, 200);
  }
  async function importDataForMooneycalc(button) {
    const characterData = JSON.parse(GM_getValue("init_character_data", ""));
    console.log(characterData);
    if (!characterData || !characterData.characterSkills || !characterData.currentTimestamp) {
      button.textContent = runtime.config.isZH ? "错误:没有人物数据" : "Error: no character settings found";
      return;
    }
    const ls = constructMooneycalcLocalStorage(characterData);
    localStorage.setItem("settings", ls);
    button.textContent = runtime.config.isZH ? "已导入" : "Imported";
    await new Promise((r) => setTimeout(r, 500));
    location.reload();
  }
  function constructMooneycalcLocalStorage(characterData) {
    const ls = localStorage.getItem("settings");
    let lsObj = JSON.parse(ls);
    lsObj.state.settings.levels = {};
    for (const skill of characterData.characterSkills) {
      lsObj.state.settings.levels[skill.skillHrid] = skill.level;
    }
    lsObj.state.settings.communityBuffs = {};
    for (const buff of characterData.communityBuffs) {
      lsObj.state.settings.communityBuffs[buff.hrid] = buff.level;
    }
    lsObj.state.settings.equipment = {};
    lsObj.state.settings.equipmentLevels = {};
    for (const item of characterData.characterItems) {
      if (item.itemLocationHrid !== "/item_locations/inventory") {
        lsObj.state.settings.equipment[item.itemLocationHrid.replace("item_locations", "equipment_types")] = item.itemHrid;
        lsObj.state.settings.equipmentLevels[item.itemLocationHrid.replace("item_locations", "equipment_types")] = item.enhancementLevel;
      }
    }
    lsObj.state.settings.houseRooms = {};
    for (const house of Object.values(characterData.characterHouseRoomMap)) {
      lsObj.state.settings.houseRooms[house.houseRoomHrid] = house.level;
    }
    return JSON.stringify(lsObj);
  }
  function hoursToReadableString(hours) {
    const sec = hours * 60 * 60;
    return runtime.api.timeReadable?.(sec) ?? `${Math.round(hours)}h`;
  }
  function addExportButton(obj) {
    const checkElem = () => {
      const selectedElement = document.querySelector(
        `div.SharableProfile_overviewTab__W4dCV`
      );
      if (selectedElement) {
        clearInterval(timer);
        const button = document.createElement("button");
        selectedElement.appendChild(button);
        button.textContent = runtime.config.isZH ? "导出人物到剪贴板" : "Export to clipboard";
        button.style.borderRadius = "5px";
        button.style.height = "30px";
        button.style.backgroundColor = runtime.config.SCRIPT_COLOR_MAIN;
        button.style.color = "black";
        button.style.boxShadow = "none";
        button.style.border = "0px";
        button.onclick = function() {
          let exportString = "";
          const playerID = obj.profile.characterSkills[0].characterID;
          const clientObj = JSON.parse(GM_getValue("init_client_data", ""));
          const characterObj = JSON.parse(GM_getValue("init_character_data", ""));
          if (playerID === characterObj.character.id) {
            exportString = JSON.stringify(
              constructSelfPlayerExportObjFromInitCharacterData(
                characterObj,
                clientObj
              )
            );
          } else {
            const storedProfileList = JSON.parse(
              GM_getValue("profile_export_list", "[]")
            );
            const profileList = storedProfileList.filter(
              (item) => item.characterID === playerID
            );
            let profile = null;
            if (profileList.length !== 1) {
              console.log(
                runtime.config.isZH ? `[MWITools] 找不到角色 ${playerID} 的已保存资料。` : `[MWITools] Cannot find a saved profile for character ${playerID}.`
              );
              return;
            }
            profile = profileList[0];
            let battlePlayer = null;
            if (GM_getValue("new_battle", "")) {
              const battleObj = JSON.parse(GM_getValue("new_battle", ""));
              const battlePlayerList = battleObj.players.filter(
                (item) => item.character.id === playerID
              );
              if (battlePlayerList.length === 1) {
                battlePlayer = battlePlayerList[0];
              }
            }
            exportString = JSON.stringify(
              constructPlayerExportObjFromStoredProfile(
                profile,
                clientObj,
                battlePlayer
              )
            );
          }
          console.log(exportString);
          navigator.clipboard.writeText(exportString);
          button.textContent = runtime.config.isZH ? "已复制" : "Copied";
          return false;
        };
        return false;
      }
    };
    let timer = setInterval(checkElem, 200);
  }
  var addImportButtonFor9Battles = addImportButtonForAmvoidguy;
  Object.assign(runtime.api, {
    addImportButtonFor9Battles,
    addImportButtonForAmvoidguy,
    importDataForAmvoidguy,
    constructGroupExportObj,
    constructSelfPlayerExportObjFromInitCharacterData,
    constructPlayerExportObjFromStoredProfile,
    observeResultsForAmvoidguy,
    handleResultForAmvoidguy,
    calculateAfterDays,
    calculateTill,
    addImportButtonForMooneycalc,
    importDataForMooneycalc,
    constructMooneycalcLocalStorage,
    hoursToReadableString,
    addExportButton
  });

  // src/features/legacy-lifecycle.js
  function removeAll(selector) {
    document.querySelectorAll(selector).forEach((node) => node.remove());
  }
  function observeRelevantDom(scope, selector, callback) {
    const scheduler = createFrameScheduler(callback);
    subscribeMutationChannel(
      {
        name: "legacy-dom",
        target: document.body,
        options: { childList: true, subtree: true },
        scope
      },
      (records) => {
        const relevant = records.some((record) => {
          const target = record.target?.nodeType === 1 ? record.target : record.target?.parentElement;
          if (target?.closest?.(selector)) return true;
          return [...record.addedNodes, ...record.removedNodes].some(
            (node) => node?.nodeType === 1 && (node.matches?.(selector) || node.querySelector?.(selector))
          );
        });
        if (relevant) scheduler.schedule();
      }
    );
    scope.add(() => scheduler.cancel());
    return scheduler;
  }
  function refreshInventoryIfNeeded(className, outputSelector) {
    const needsRender = [
      ...document.querySelectorAll('div[class*="Inventory_items"]')
    ].some(
      (node) => !node.classList.contains(className) || outputSelector && !node.parentElement?.querySelector(outputSelector)
    );
    if (needsRender) runtime.api.scheduleNetworthRefresh?.();
  }
  var adapters = {
    invWorth: {
      scope: "character",
      initialize({ scope }) {
        runtime.api.scheduleNetworthRefresh?.();
        observeRelevantDom(
          scope,
          'div[class*="Inventory_items"],#script_inventory_summary',
          () => refreshInventoryIfNeeded(
            "script_buildScore_added",
            "#script_inventory_summary"
          )
        );
      },
      cleanup() {
        removeAll("#script_inventory_summary");
        document.querySelectorAll(".script_buildScore_added").forEach((node) => {
          node.classList.remove("script_buildScore_added");
          delete node.dataset.mwitoolsInventoryDisplayVersion;
        });
        removeAll(".mwi-inventory-category-value");
        removeAll("#script_inv_sort_controls");
        document.querySelectorAll(".script_invSort_added").forEach((node) => node.classList.remove("script_invSort_added"));
        runtime.api.scheduleNetworthRefresh?.();
      }
    },
    invSort: {
      scope: "character",
      initialize({ scope }) {
        runtime.api.scheduleNetworthRefresh?.();
        observeRelevantDom(
          scope,
          'div[class*="Inventory_items"]',
          () => refreshInventoryIfNeeded(
            "script_invSort_added",
            "#script_inv_sort_controls"
          )
        );
      },
      cleanup() {
        removeAll("#script_inv_sort_controls,#script_stack_price");
        document.querySelectorAll(".script_invSort_added").forEach((node) => node.classList.remove("script_invSort_added"));
        runtime.api.scheduleNetworthRefresh?.();
      }
    },
    actionQueue: {
      scope: "character",
      initialize({ scope }) {
        runtime.api.observeActionQueueMenus?.(scope);
      },
      cleanup() {
        runtime.api.disconnectActionQueueObservers?.();
        removeAll(".script_actionTime,#script_queueTotalTime");
      }
    },
    checkEquipment: {
      scope: "character",
      initialize({ scope }) {
        runtime.api.checkEquipment?.();
        observeRelevantDom(
          scope,
          'div[class*="Header_actionInfo"]',
          () => runtime.api.checkEquipment?.()
        );
      },
      cleanup() {
        removeAll("#script_item_warning");
        removeAll("#mwitools-equipment-warning-style");
        document.querySelectorAll(".mwi-equipment-warning-host").forEach((host) => host.classList.remove("mwi-equipment-warning-host"));
      }
    },
    actionPanel_foragingTotal: {
      scope: "character",
      dependsOn: ["actionPanel_totalTime"],
      cleanup() {
        removeAll("#totalProfit");
      }
    }
  };
  for (const id of [
    "useOrangeAsMainColor",
    "profileBuildScore",
    "battlePanel",
    "enhanceSim",
    "forceMWIToolsDisplayZH"
  ]) {
    adapters[id] = {};
  }
  adapters.ThirdPartyLinks = {
    initialize({ scope }) {
      runtime.api.add3rdPartyLinks?.();
      observeRelevantDom(
        scope,
        'div[class*="NavigationBar_minorNavigationLinks"]',
        () => runtime.api.add3rdPartyLinks?.()
      );
    },
    cleanup() {
      removeAll('[data-mwitools-external-link="true"]');
    }
  };
  adapters.notifiEmptyAction = {
    scope: "character",
    initialize() {
      runtime.api.notificate?.();
    }
  };
  adapters.fillMarketOrderPrice = {
    scope: "character",
    initialize({ scope }) {
      let observed = null;
      let listingObserver = null;
      const attach = () => {
        const target = document.querySelector(
          ".MarketplacePanel_marketListings__1GCyQ"
        );
        if (!target || target === observed) return;
        listingObserver?.disconnect();
        observed = target;
        const observer = new MutationObserver((mutations) => {
          for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
              if (node?.classList?.contains("Modal_modalContainer__3B80m")) {
                runtime.api.handleMarketNewOrder?.(node);
              }
            }
          }
        });
        observer.observe(target, { childList: true });
        listingObserver = observer;
      };
      attach();
      observeRelevantDom(
        scope,
        ".MarketplacePanel_marketListings__1GCyQ",
        attach
      );
      scope.add(() => {
        listingObserver?.disconnect();
        listingObserver = null;
        observed = null;
      });
    }
  };
  for (const [id, adapter] of Object.entries(adapters)) {
    if (runtime.features.getStatus(id).status !== "unregistered") continue;
    runtime.features.register({
      id,
      setting: id,
      scope: adapter.scope ?? "global",
      dependsOn: adapter.dependsOn,
      initialize(context) {
        adapter.initialize?.(context);
      },
      cleanup() {
        adapter.cleanup?.();
      }
    });
  }

  // src/features/message-effects.js
  function refreshAssets() {
    const settings2 = runtime.settings.settingsMap;
    if (settings2.invWorth.isTrue || settings2.invSort.isTrue) {
      runtime.api.scheduleNetworthRefresh();
    }
    if (settings2.assetHistory.isTrue) {
      runtime.api.assetHistory.scheduleRefresh();
    }
  }
  function enableIronCowAdaptation() {
    if (!runtime.api.isIronCowCharacter?.() || runtime.settings.settingsMap.adaptIronCowMarketFeatures?.isTrue) {
      return;
    }
    void runtime.settings.set("adaptIronCowMarketFeatures", true).catch((error) => {
      console.error(
        runtime.config.isZH ? "[MWITools] 自动开启铁牛模式适配失败。" : "[MWITools] Failed to enable Iron Cow adaptation automatically.",
        error
      );
    });
  }
  runtime.onMessage("init_client_data", (payload, message) => {
    console.log(payload);
    GM_setValue("init_client_data", message);
  });
  runtime.onMessage("init_character_data", (payload, message) => {
    console.log(payload);
    GM_setValue("init_character_data", message);
    enableIronCowAdaptation();
    const settings2 = runtime.settings.settingsMap;
    refreshAssets();
    if (settings2.checkEquipment.isTrue) runtime.api.checkEquipment();
  });
  runtime.onMessage("actions_updated", () => {
    const settings2 = runtime.settings.settingsMap;
    if (settings2.checkEquipment.isTrue) runtime.api.checkEquipment();
    if (settings2.notifiEmptyAction.isTrue)
      setTimeout(runtime.api.notificate, 1e3);
  });
  runtime.onMessage("battle_unit_fetched", (payload) => {
    if (runtime.settings.settingsMap.battlePanel.isTrue)
      runtime.api.handleBattleSummary(payload);
  });
  runtime.onMessage("new_battle", (_payload, message) => {
    if (typeof message === "string") GM_setValue("new_battle", message);
  });
  runtime.onMessage("items_updated", () => {
    if (runtime.settings.settingsMap.checkEquipment.isTrue)
      runtime.api.checkEquipment();
    refreshAssets();
  });
  for (const messageType of [
    "market_item_values_updated",
    "market_item_order_books_updated",
    "market_listings_updated",
    "guild_updated",
    "house_rooms_updated",
    "abilities_updated",
    "character_abilities_updated"
  ]) {
    runtime.onMessage(messageType, () => {
      refreshAssets();
    });
  }
  runtime.onMessage("profile_shared", (payload) => {
    let stored = GM_getValue("profile_export_list", null);
    if (stored) {
      const parsed = JSON.parse(stored);
      if (!parsed?.filter) {
        console.error(
          runtime.config.isZH ? "[MWITools] 已保存的资料导出列表无效,现已清空。" : "[MWITools] The saved profile export list was invalid and has been cleared."
        );
        GM_setValue("profile_export_list", JSON.stringify([]));
      }
    } else {
      GM_setValue("profile_export_list", JSON.stringify([]));
    }
    payload.characterID = payload.profile.characterSkills[0].characterID;
    payload.characterName = payload.profile.sharableCharacter.name;
    payload.timestamp = Date.now();
    stored = GM_getValue("profile_export_list", null) || JSON.stringify([]);
    const profiles = JSON.parse(stored).filter(
      (item) => item.characterID !== payload.characterID
    );
    profiles.unshift(payload);
    if (profiles.length > 20) profiles.pop();
    GM_setValue("profile_export_list", JSON.stringify(profiles));
    runtime.api.addExportButton(payload);
    if (runtime.settings.settingsMap.profileBuildScore.isTrue)
      runtime.api.showBuildScoreOnProfile(payload);
  });

  // src/main.js
  async function startGame() {
    const clientDataLoaded = runtime.api.refreshGameClientData();
    if (!clientDataLoaded) {
      runtime.features.register({
        id: "clientDataCache",
        initialize({ scope }) {
          const interval = scope.interval(() => {
            if (runtime.api.refreshGameClientData()) clearInterval(interval);
          }, 250);
        }
      });
    }
    runtime.api.loadMarketItemValuesFromStorage();
    runtime.api.hookWS();
    const currentApiVersion = 3;
    const storedApiVersion = localStorage.getItem(
      "MWITools_marketAPI_ApiVersion"
    );
    if (!storedApiVersion || parseInt(storedApiVersion) < currentApiVersion) {
      console.log(
        runtime.config.isZH ? "[MWITools] 市场 API 版本已更新,正在清理旧缓存。" : "[MWITools] Market API version changed; clearing the old cache."
      );
      localStorage.setItem("MWITools_marketAPI_timestamp", JSON.stringify(0));
      localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(null));
      localStorage.setItem(
        "MWITools_marketAPI_ApiVersion",
        JSON.stringify(currentApiVersion)
      );
    }
    runtime.api.fetchMarketJSON(true);
    try {
      await runtime.api.runPerformanceOnboardingIfNeeded();
    } catch (error) {
      console.error(
        runtime.config.isZH ? "[MWITools] 性能引导启动失败,将继续使用当前设置。" : "[MWITools] Performance setup failed; continuing with current settings.",
        error
      );
    }
    await runtime.start();
  }
  async function main() {
    runtime.features.pauseInitialization();
    runtime.api.readSettings();
    if (document.URL.includes("amvoidguy.github.io") || document.URL.includes("shykai.github.io/MWICombatSimulatorTest/")) {
      runtime.api.addImportButtonForAmvoidguy();
      runtime.api.observeResultsForAmvoidguy();
      return;
    }
    if (document.URL.includes("mooneycalc.netlify.app") || document.URL.includes("mooneycalc.vercel.app")) {
      runtime.api.addImportButtonForMooneycalc();
      return;
    }
    await startGame();
  }
  void main();
})();