All pastes #5kPLpH6qr8 Raw Edit

HV Monster PL Planner

public javascript v1 · immutable
#5kPLpH6qr8 ·published 2026-07-23 06:09 UTC
rendered paste body
// ==UserScript==// @name         HV Monster PL Planner// @namespace    https://hentaiverse.org/// @version      0.2.5// @description  Standalone Monster Lab batch PL planner, upgrader and crystal buyer with HV Utils compatibility.// @author       KirisameReiko// @include      /^https:\/\/hentaiverse\.org\/\?s=Bazaar&ss=ml(?:&.*)?$/// @include      /^https:\/\/alt\.hentaiverse\.org\/\?s=Bazaar&ss=ml(?:&.*)?$/// @grant        GM_addStyle// @grant        GM_getValue// @grant        GM_setValue// @run-at       document-end// ==/UserScript==(function () {  'use strict';  const query = Object.fromEntries(new URLSearchParams(location.search));  if (    location.protocol !== 'https:' ||    !['hentaiverse.org', 'alt.hentaiverse.org'].includes(location.hostname) ||    location.pathname !== '/' ||    query.s !== 'Bazaar' ||    query.ss !== 'ml'  ) return;  const STORE_KEY = 'hv_exact_pl_planner_v1';  const EPS = 1e-9;  const PL_SCALE = 2;  const PRIMARY_COST_BASE = 50;  const PRIMARY_COST_RATE = 1.555079154;  const ELEMENTAL_COST_BASE = 10;  const ELEMENTAL_COST_RATE = 1.26485522;  const REQUEST_DELAY_MS = 300;  const DIRECT_BUY_RETRY_DELAY_MS = 5000;  const DIRECT_BUY_MAX_RETRIES = 3;  const MARKET_DETAIL_CONCURRENCY = 6;  const primary = ['STR', 'DEX', 'AGI', 'END', 'INT', 'WIS'];  const elemental = ['FIRE', 'COLD', 'ELEC', 'WIND', 'HOLY', 'DARK'];  const all = [...primary, ...elemental];  const displayElemental = ['WIND', 'ELEC', 'FIRE', 'COLD', 'DARK', 'HOLY'];  const displayAll = [...primary, ...displayElemental];  const primarySet = new Set(primary);  // Add new Easter eggs here, one string per line.  const easterEggMessages = [    'Isekaijoucho',    '🍜',    '',    'Dokidoki Wakuwaku',    'wawawaa',    '( ・_ゝ・)',    'Rina Hidaka',    'How high can borrowed wings really fly?',    'Aizawa Nachu',    'Soyokaze Ame',    'Otherside Picnic',  ];  const languageOptions = [    ['en', 'English'],    ['zh-CN', '简体中文'],  ];  const languageValues = languageOptions.map(([value]) => value);  const crystalByAttr = {    STR: 'Crystal of Vigor',    DEX: 'Crystal of Finesse',    AGI: 'Crystal of Swiftness',    END: 'Crystal of Fortitude',    INT: 'Crystal of Cunning',    WIS: 'Crystal of Knowledge',    FIRE: 'Crystal of Flames',    COLD: 'Crystal of Frost',    ELEC: 'Crystal of Lightning',    WIND: 'Crystal of Tempest',    HOLY: 'Crystal of Devotion',    DARK: 'Crystal of Corruption',  };  const upgradeQueryByAttr = {    STR: 'pa_str',    DEX: 'pa_dex',    AGI: 'pa_agi',    END: 'pa_end',    INT: 'pa_int',    WIS: 'pa_wis',    FIRE: 'er_fire',    COLD: 'er_cold',    ELEC: 'er_elec',    WIND: 'er_wind',    HOLY: 'er_holy',    DARK: 'er_dark',  };  const crystalNames = all.map((a) => crystalByAttr[a]);  const attrByCrystal = Object.fromEntries(all.map((attr) => [crystalByAttr[attr], attr]));  const priceSourceValues = ['ask', 'bid', 'day', 'week', 'month', 'year', 'hvut'];  const marketPriceSources = ['ask', 'bid', 'day', 'week', 'month', 'year'];  const translations = {    en: {      plannerTitle: 'PL Planner',      attr: {        STR: 'STR',        DEX: 'DEX',        AGI: 'AGI',        END: 'END',        INT: 'INT',        WIS: 'WIS',        FIRE: 'FIRE',        COLD: 'COLD',        ELEC: 'ELEC',        WIND: 'WIND',        HOLY: 'HOLY',        DARK: 'DARK',      },      crystal: {        STR: 'Crystal of Vigor',        DEX: 'Crystal of Finesse',        AGI: 'Crystal of Swiftness',        END: 'Crystal of Fortitude',        INT: 'Crystal of Cunning',        WIS: 'Crystal of Knowledge',        FIRE: 'Crystal of Flames',        COLD: 'Crystal of Frost',        ELEC: 'Crystal of Lightning',        WIND: 'Crystal of Tempest',        HOLY: 'Crystal of Devotion',        DARK: 'Crystal of Corruption',      },      labelCurrentPL: 'Current PL',      labelTargetPL: 'Target PL',      labelNeedPL: 'Needed',      labelMaxPL: 'Max',      labelTargetInput: 'Target PL',      labelPriceSource: 'Market Price Source',      labelOrderPriceSource: 'Buy Order Price Source',      labelSelectedMonsters: 'Selected Monsters',      labelLoadedCount: 'Cached',      headingMonsterSelection: 'Monster Selection',      headingCrystalNeeds: 'Crystal Requirements and Inventory',      crystalPlanUnavailable: 'Crystal actions are unavailable until every selected monster has a valid plan. Review the monster errors below.',      buttonFetchPrice: 'Refresh Prices',      buttonLoadMonster: 'Refresh Monster Levels',      buttonSelectAll: 'Select All',      buttonClearSelection: 'Clear',      buttonLoadStock: 'Read Crystal Inventory and Orders',      buttonDirectBuy: 'Direct Buy Missing Crystals',      buttonPlaceBuyOrders: 'Place Crystal Buy Orders',      buttonRunUpgrade: 'Upgrade Selected Monsters by Plan',      buttonRunningUpgrade: ({ current, total }) => `Upgrading ${current}/${total}`,      buttonBuying: ({ current, total }) => `Buying ${current}/${total}`,      totalCost: 'Total Cost:',      totalMonsters: 'Monsters:',      noUpgradeNeeded: 'No upgrade needed',      tableAttr: 'Attribute',      tableCurrentLevel: 'Current Level',      tableTargetLevel: 'Upgrade To',      tableIncrease: 'Increase',      tableCost: 'Cost',      tableCrystal: 'Crystal',      tableRequired: 'Required',      tableStock: 'Stock',      tableShortage: 'Shortage',      tableEstimatedCost: 'Estimated Spend',      tableOrderPrice: 'Buy Price / Batch',      stockUnknown: 'Not loaded',      estimateUnavailable: 'Read order books first',      estimateUsesSupply: ({ batches }) => `Includes ${batches} guaranteed-supply batch(es)`,      estimateSummary: ({ cost, balance }) => `Order-book estimated spend: ${cost}. Estimated starting Market Balance required by whole-order maximum bids: ${balance}. Estimates use the current snapshot; final matches use the market response.`,      listSeparator: ', ',      priceSource: {        ask: 'Ask',        bid: 'Bid',        day: 'Daily Avg',        week: 'Weekly Avg',        month: 'Monthly Avg',        year: 'Yearly Avg',        hvut: 'HV Utils Saved',      },      errorTargetOverMax: ({ max }) => `Target exceeds max reachable PL: ${max}.`,      errorTargetUnit: 'Cannot upgrade exactly to the target PL: the minimum PL unit is 0.5. Set the target PL to a multiple of 0.5.',      errorTargetUnreachable: 'Cannot upgrade exactly to the target PL: this target is unreachable with the current levels and level caps.',      errorCalculationPath: 'Calculation path error.',      errorParseMonster: 'Could not parse attribute levels from the monster details page. Please confirm the page structure has not changed.',      errorNoMonsterSelection: 'Select at least one monster first.',      errorMonstersNotLoaded: 'Some selected monsters have no saved levels. Refresh their levels first.',      errorNoValidPlan: 'Calculate a valid multi-monster plan first.',      errorMarketItemForm: ({ crystal }) => `Could not parse the buy order form for ${crystal}. The market page structure may have changed. No order was submitted.`,      errorCrystalBatch: ({ crystal }) => `Could not determine the live market batch size for ${crystal}. No order was submitted.`,      errorCrystalBatchChanged: ({ crystal, expected, actual }) => `${crystal}'s market batch size changed from ${expected} to ${actual}. Recalculate before ordering.`,      errorExistingBuyOrder: ({ crystals }) => `Direct buy stopped before submitting: an existing buy order was found for ${crystals}. Delete it manually or use persistent buy-order mode.`,      errorDirectRemainder: ({ crystal, batches }) => `Direct buy stopped: ${crystal} left ${batches} high-price batch(es) unmatched and no usable delete control was returned. Check the market immediately.`,      errorDirectRemainderCancelled: ({ crystal, batches }) => `Direct buy stopped after ${crystal}: ${batches} high-price unmatched batch(es) were automatically cancelled. No later crystal orders were submitted.`,      errorOrderNotApplied: ({ crystal }) => `${crystal}'s response showed neither an inventory increase nor a remaining buy order. The market ignored the request; processing stopped.`,      errorOrderResultMismatch: ({ crystal, submitted, matched, remaining }) => `${crystal}'s market response is inconsistent: submitted ${submitted}, matched ${matched}, remaining ${remaining}. Processing stopped.`,      errorDirectPrice: ({ crystal }) => `Could not read the market's guaranteed direct-buy price for ${crystal}. No order was submitted.`,      errorDirectBalance: ({ crystal, required, balance }) => `${crystal}'s single direct-buy order needs at least ${required} C of available Market Balance because it is submitted at the market's guaranteed ceiling price. This is a maximum-bid balance requirement, not the estimated or final cost; completed matches are charged at their actual sell prices. Current balance: ${balance} C. No order was submitted.`,      statusLoadingMonsters: ({ done, total }) => `Refreshing and saving monster levels... ${done}/${total}`,      statusLoadedMonsters: ({ total }) => `Refreshed and saved ${total} selected monster(s).`,      statusLoadedCurrentMonster: 'Refreshed and saved levels from the current monster details page.',      statusReadingMarket: ({ done = 0, total = 12 }) => done        ? `Reading prices, inventory, and order books... ${done}/${total}`        : 'Reading prices, inventory, and order books...',      statusInventoryPartial: ({ failed }) => `Market snapshot loaded partially. Missing: ${failed}.`,      statusMarketPartial: ({ updated, failed }) => `Saved price cache refreshed: ${updated}/12 items. Missing: ${failed}. Existing saved values were kept.`,      statusMarketComplete: ({ source }) => `All price sources were refreshed and saved. Calculation source: ${source}.`,      statusNoUpgrade: 'No upgrade is needed.',      statusNoShortage: 'The loaded inventory covers this plan; no crystals need to be bought.',      statusCrystalShortage: ({ failed }) => `Upgrade stopped because crystal inventory is short: ${failed}.`,      statusOrderPricesComplete: ({ source }) => `Buy order source changed to ${source} and saved prices were applied; every row remains manually editable.`,      statusOrderPricesPartial: ({ source, updated, failed }) => `Buy order source changed to ${source}; applied ${updated}/12 saved prices. Missing: ${failed}. Existing values were kept.`,      statusCheckingMonsters: ({ done, total }) => `Checking selected monsters... ${done}/${total}`,      statusBatchUpgradeCancelled: ({ slots }) => `Batch upgrade cancelled before making changes: current levels changed for monster(s) ${slots}. Please calculate again.`,      statusExecutingUpgrade: ({ current, total, slot, attr, count }) => `Executing upgrade ${current}/${total}: monster #${slot}, ${attr} +${count}`,      statusUpgradeComplete: ({ total }) => `Batch upgrade complete: ${total} monster(s) reached their plans.`,      statusUpgradeMismatch: 'Upgrade requests completed, but the final levels do not exactly match the plan. Please check crystal stock or page messages.',      statusCheckingPurchase: 'Refreshing inventory and validating crystal orders...',      statusExecutingPurchase: ({ current, total, crystal, crystals, price }) => `Submitting crystal order ${current}/${total}: ${crystal}, ${crystals} crystals in one order @ ${price} C per market batch`,      statusRetryingPurchase: ({ crystal, attempt }) => `${crystal}'s order was temporarily ignored; retrying after a cooldown (${attempt}/${DIRECT_BUY_MAX_RETRIES})...`,      statusDirectBuyComplete: ({ total }) => `Direct-buy requests completed for ${total} crystal type(s); inventory has been refreshed.`,      statusBuyOrdersComplete: ({ total }) => `Buy orders placed for ${total} crystal type(s).`,      statusSavedPriceSource: ({ source }) => `Using saved ${source} prices for calculation.`,      statusSavedPriceSourcePartial: ({ source, updated, failed }) => `Using saved ${source} prices for calculation: ${updated}/12 applied. Missing: ${failed}. Existing calculation prices were kept for those items.`,      statusFetchPriceFailed: ({ message }) => `Price loading failed: ${message}`,      statusLoadMonsterFailed: ({ message }) => `Monster level loading failed: ${message}`,      statusUpgradeFailed: ({ message }) => `Upgrade failed: ${message}`,      statusPurchaseFailed: ({ message }) => `Crystal purchase failed: ${message}`,      statusLanguageSaved: 'Language saved.',      confirmUpgrade: ({ monsters, requests, levels }) => `This will execute ${requests} upgrade requests for ${monsters} monster(s), increasing ${levels} total levels. Every monster and the crystal inventory will be checked again first. Continue?`,      monsterFallback: ({ slot }) => `Monster ${slot}`,    },    'zh-CN': {      plannerTitle: 'PL计划器',      attr: {        STR: '力量',        DEX: '灵巧',        AGI: '敏捷',        END: '体质',        INT: '智力',        WIS: '智慧',        FIRE: '',        COLD: '',        ELEC: '',        WIND: '',        HOLY: '',        DARK: '',      },      crystal: {        STR: '力量水晶',        DEX: '灵巧水晶',        AGI: '敏捷水晶',        END: '体质水晶',        INT: '智力水晶',        WIS: '智慧水晶',        FIRE: '火焰水晶',        COLD: '冰冻水晶',        ELEC: '闪电水晶',        WIND: '疾风水晶',        HOLY: '神圣水晶',        DARK: '暗黑水晶',      },      labelCurrentPL: '当前 PL',      labelTargetPL: '目标 PL',      labelNeedPL: '还需',      labelMaxPL: '最大',      labelTargetInput: '目标 PL',      labelPriceSource: '市场价来源',      labelOrderPriceSource: '收购价来源',      labelSelectedMonsters: '已选怪物',      labelLoadedCount: '已缓存',      headingMonsterSelection: '批量选择怪物',      headingCrystalNeeds: '水晶需求、库存与缺口',      crystalPlanUnavailable: '全部选中怪物都得到有效方案后才能使用水晶功能;请检查下方的怪物错误。',      buttonFetchPrice: '刷新价格',      buttonLoadMonster: '刷新怪物等级',      buttonSelectAll: '全选',      buttonClearSelection: '清空选择',      buttonLoadStock: '读取水晶库存与订单',      buttonDirectBuy: '直接买入缺少水晶',      buttonPlaceBuyOrders: '挂水晶买单',      buttonRunUpgrade: '按方案批量升级选中怪物',      buttonRunningUpgrade: ({ current, total }) => `升级中 ${current}/${total}`,      buttonBuying: ({ current, total }) => `买入中 ${current}/${total}`,      totalCost: '总成本:',      totalMonsters: '怪物数:',      noUpgradeNeeded: '无需升级',      tableAttr: '属性',      tableCurrentLevel: '当前等级',      tableTargetLevel: '需要提升到',      tableIncrease: '提升',      tableCost: '成本',      tableCrystal: '水晶',      tableRequired: '需要',      tableStock: '库存',      tableShortage: '缺口',      tableEstimatedCost: '预估消耗',      tableOrderPrice: '收购价 / 每批',      stockUnknown: '未读取',      estimateUnavailable: '请先读取订单簿',      estimateUsesSupply: ({ batches }) => `${batches} 批商店保证供货`,      estimateSummary: ({ cost, balance }) => `订单簿预计支出:${cost};按整笔最高报价下单,预计所需初始市场余额:${balance}。预估使用当前快照,最终以市场响应为准。`,      listSeparator: '',      priceSource: {        ask: '卖价',        bid: '买价',        day: '日均价',        week: '周均价',        month: '月均价',        year: '年均价',        hvut: 'HV Utils 保存价',      },      errorTargetOverMax: ({ max }) => `目标超过最大可达 PL:${max}`,      errorTargetUnit: '无法精确升级到指定 PL:PL 最小单位是 0.5,请把目标 PL 设为 0.5 的倍数。',      errorTargetUnreachable: '无法精确升级到指定 PL:该目标在当前等级与等级上限下不可达。',      errorCalculationPath: '计算路径异常。',      errorParseMonster: '未能从怪物详情页解析属性等级。请确认怪物详情页结构未变化。',      errorNoMonsterSelection: '请先至少选择一个怪物。',      errorMonstersNotLoaded: '部分选中怪物没有已保存的等级,请先刷新怪物等级。',      errorNoValidPlan: '请先计算出有效的多怪物升级方案。',      errorMarketItemForm: ({ crystal }) => `无法解析 ${crystal} 的买单表单,市场页面结构可能已变化;本次没有提交该订单。`,      errorCrystalBatch: ({ crystal }) => `无法从市场详情页确认 ${crystal} 的实际每批数量;本次没有提交该订单。`,      errorCrystalBatchChanged: ({ crystal, expected, actual }) => `${crystal} 的市场批量已从 ${expected} 变为 ${actual},请重新计算后再下单。`,      errorExistingBuyOrder: ({ crystals }) => `直接买入在提交前停止:${crystals} 已有买单。请手动删除,或改用“挂水晶买单”模式。`,      errorDirectRemainder: ({ crystal, batches }) => `直接买入已停止:${crystal} 留下 ${batches} 批高价余单,且响应页没有可用的删除控件。请立即检查市场。`,      errorDirectRemainderCancelled: ({ crystal, batches }) => `直接买入已在 ${crystal} 后停止:${batches} 批高价余单已自动撤销,后续水晶没有继续下单。`,      errorOrderNotApplied: ({ crystal }) => `${crystal} 的响应既没有增加库存,也没有生成剩余买单;市场忽略了该请求,已停止后续处理。`,      errorOrderResultMismatch: ({ crystal, submitted, matched, remaining }) => `${crystal} 的市场响应不一致:提交 ${submitted} 批、成交 ${matched} 批、剩余 ${remaining} 批,已停止后续处理。`,      errorDirectPrice: ({ crystal }) => `无法读取 ${crystal} 的市场保证直接供货价,本次没有提交订单。`,      errorDirectBalance: ({ crystal, required, balance }) => `${crystal} 的本次整笔直接买入按市场保证成交的最高报价提交,因此市场余额至少需要 ${required} C 才能下单。这是最高报价对应的可用余额要求,不是预计或最终花费;成交后按实际卖价扣款。当前余额为 ${balance} C,本次没有提交订单。`,      statusLoadingMonsters: ({ done, total }) => `正在刷新并保存怪物等级... ${done}/${total}`,      statusLoadedMonsters: ({ total }) => `已刷新并保存 ${total} 个选中怪物的等级。`,      statusLoadedCurrentMonster: '已从当前怪物详情页刷新并保存等级。',      statusReadingMarket: ({ done = 0, total = 12 }) => done        ? `读取价格、库存和订单簿... ${done}/${total}`        : '读取价格、库存和订单簿...',      statusInventoryPartial: ({ failed }) => `市场快照仅部分读取成功,缺少:${failed}`,      statusMarketPartial: ({ updated, failed }) => `价格缓存已刷新 ${updated}/12 项,缺少:${failed};保留原有保存值。`,      statusMarketComplete: ({ source }) => `所有价格来源均已刷新并保存;当前计算来源:${source}`,      statusNoUpgrade: '无需执行升级。',      statusNoShortage: '当前库存足够执行方案,无需买入水晶。',      statusCrystalShortage: ({ failed }) => `已停止升级,水晶库存仍有缺口:${failed}`,      statusOrderPricesComplete: ({ source }) => `收购价来源已改为 ${source},并自动应用保存价格;每种水晶仍可手动调整。`,      statusOrderPricesPartial: ({ source, updated, failed }) => `收购价来源已改为 ${source},已应用 ${updated}/12 项保存价格;缺少:${failed},这些项目保留原值。`,      statusCheckingMonsters: ({ done, total }) => `校验选中怪物... ${done}/${total}`,      statusBatchUpgradeCancelled: ({ slots }) => `批量升级在产生变更前取消:怪物 ${slots} 的当前等级已变化,请重新读取并计算。`,      statusExecutingUpgrade: ({ current, total, slot, attr, count }) => `执行升级 ${current}/${total}:怪物 #${slot}${attr} +${count}`,      statusUpgradeComplete: ({ total }) => `批量升级完成:${total} 个怪物已达到各自方案。`,      statusUpgradeMismatch: '升级请求已完成,但最终等级与方案不完全一致,请检查水晶库存或页面提示。',      statusCheckingPurchase: '正在刷新库存并校验水晶订单...',      statusExecutingPurchase: ({ current, total, crystal, crystals, price }) => `提交水晶订单 ${current}/${total}${crystal},一次买入 ${crystals} 个,市场每组价格 ${price} C`,      statusRetryingPurchase: ({ crystal, attempt }) => `${crystal} 的订单被市场临时忽略,冷却后重试(${attempt}/${DIRECT_BUY_MAX_RETRIES})...`,      statusDirectBuyComplete: ({ total }) => `${total} 种水晶的直接买入请求已完成,库存已重新读取。`,      statusBuyOrdersComplete: ({ total }) => `已为 ${total} 种水晶挂出买单。`,      statusSavedPriceSource: ({ source }) => `计算已改用保存的 ${source} 价格。`,      statusSavedPriceSourcePartial: ({ source, updated, failed }) => `计算已改用保存的 ${source} 价格:已应用 ${updated}/12 项;缺少 ${failed},这些项目保留原计算价格。`,      statusFetchPriceFailed: ({ message }) => `读取价格失败:${message}`,      statusLoadMonsterFailed: ({ message }) => `读取怪物等级失败:${message}`,      statusUpgradeFailed: ({ message }) => `执行升级失败:${message}`,      statusPurchaseFailed: ({ message }) => `买入水晶失败:${message}`,      statusLanguageSaved: '语言已保存。',      confirmUpgrade: ({ monsters, requests, levels }) => `将对 ${monsters} 个怪物执行 ${requests} 个升级请求,共提升 ${levels} 级。脚本会先重新校验全部怪物与水晶库存。是否继续?`,      monsterFallback: ({ slot }) => `怪物 ${slot}`,    },  };  const crystalNameAliases = new Map();  all.forEach((attr) => {    const canonical = crystalByAttr[attr];    [canonical, translations.en.crystal[attr], translations['zh-CN'].crystal[attr]]      .forEach((name) => crystalNameAliases.set(name, canonical));  });  const defaultPrice = {    STR: 2.18,    DEX: 1.52,    AGI: 1.23,    END: 1.95,    INT: 1.25,    WIS: 1.23,    FIRE: 1.11,    COLD: 1.09,    ELEC: 1.11,    WIND: 1.11,    HOLY: 1.09,    DARK: 1.10,  };  function $(q, d = document) {    return d.querySelector(q);  }  function $all(q, d = document) {    return Array.from(d.querySelectorAll(q));  }  function randomEasterEgg(current = '') {    const candidates = easterEggMessages.filter((message) => message !== current);    const pool = candidates.length ? candidates : easterEggMessages;    return pool[Math.floor(Math.random() * pool.length)] || '';  }  function htmlToDoc(html) {    const doc = document.implementation.createHTMLDocument('');    doc.documentElement.innerHTML = html;    return doc;  }  function elt(tag, attrs = {}, children = []) {    const e = document.createElement(tag);    Object.entries(attrs || {}).forEach(([k, v]) => {      if (v === undefined || v === null) return;      if (k === 'class') {        e.className = v;      } else if (k === 'style') {        e.style.cssText = v;      } else if (k === 'dataset') {        Object.assign(e.dataset, v);      } else if (k === 'text') {        e.textContent = v;      } else if (k in e) {        e[k] = v;      } else {        e.setAttribute(k, v);      }    });    if (!Array.isArray(children)) children = [children];    children.forEach((c) => {      if (c === null || c === undefined) return;      e.appendChild(        typeof c === 'string' || typeof c === 'number'          ? document.createTextNode(String(c))          : c      );    });    return e;  }  function parseNum(text) {    const m = String(text || '').replace(/,/g, '').match(/-?\d+(?:\.\d+)?/);    return m ? Number(m[0]) : NaN;  }  function positiveNumber(value) {    const n = Number(value);    return Number.isFinite(n) && n > 0 ? n : 0;  }  function clampInt(value, min, max) {    const n = parseInt(value, 10);    return Number.isFinite(n) ? Math.max(min, Math.min(max, n)) : min;  }  function gmGet(key, fallback) {    try {      const v = typeof GM_getValue === 'function'        ? GM_getValue(key, null)        : localStorage.getItem(key);      return parseStoredJson(v, fallback);    } catch (e) {      return fallback;    }  }  function gmSet(key, value) {    try {      if (typeof GM_setValue === 'function') {        GM_setValue(key, JSON.stringify(value));      } else {        localStorage.setItem(key, JSON.stringify(value));      }    } catch (e) {      console.warn('[PL Planner] Save failed:', e);    }  }  function parseStoredJson(value, fallback = null) {    if (value === null || value === undefined) return fallback;    if (typeof value === 'string') {      try {        return JSON.parse(value);      } catch (e) {        return fallback;      }    }    return value;  }  function isPlainObject(value) {    return value && typeof value === 'object' && !Array.isArray(value);  }  function normalizeLanguage(language) {    return languageValues.includes(language) ? language : 'en';  }  function readTranslation(path, language = state.language) {    const parts = path.split('.');    let value = translations[normalizeLanguage(language)];    for (const part of parts) {      value = value?.[part];    }    if (value !== undefined) return value;    value = translations.en;    for (const part of parts) {      value = value?.[part];    }    return value ?? path;  }  function t(path, params = {}) {    const value = readTranslation(path);    return typeof value === 'function' ? value(params) : value;  }  function attrLabel(attr) {    return t(`attr.${attr}`);  }  function crystalLabel(crystalOrAttr) {    const attr = crystalByAttr[crystalOrAttr] ? crystalOrAttr : attrByCrystal[crystalOrAttr];    return attr ? t(`crystal.${attr}`) : String(crystalOrAttr || '');  }  function canonicalCrystalName(name) {    return crystalNameAliases.get(String(name || '').trim()) || '';  }  function priceSourceLabel(value) {    return t(`priceSource.${value}`);  }  function joinList(values) {    return values.join(t('listSeparator'));  }  function readHvutPriceStore() {    try {      const prices = parseStoredJson(localStorage.getItem('hvut_prices'));      if (isPlainObject(prices)) return prices;    } catch (e) {      // fall through to this script's GM storage    }    try {      if (typeof GM_getValue === 'function') {        const prices = parseStoredJson(GM_getValue('hvut_prices', null));        if (isPlainObject(prices)) return prices;      }    } catch (e) {      // ignore invalid external storage    }    return {};  }  function writeHvutPrices(pricePatch) {    const prices = Object.assign({}, readHvutPriceStore(), pricePatch);    try {      if (typeof GM_setValue === 'function') {        GM_setValue('hvut_prices', prices);      }    } catch (e) {      console.warn('[PL Planner] HV Utils GM price save failed:', e);    }    try {      localStorage.setItem('hvut_prices', JSON.stringify(prices));    } catch (e) {      console.warn('[PL Planner] HV Utils localStorage price save failed:', e);    }  }  async function fetchText(url, data = null) {    const full = new URL(url, location.origin).href;    const response = await fetch(full, {      method: data ? 'POST' : 'GET',      body: data || undefined,      credentials: 'same-origin',      headers: data ? { 'Content-Type': 'application/x-www-form-urlencoded' } : undefined,      redirect: 'follow',      cache: 'no-store',    });    const text = await response.text();    if (!response.ok) throw new Error(`${response.status} ${response.statusText}: ${full}`);    if (text === 'state lock limiter in effect') throw new Error(text);    return text;  }  function defaultLevels() {    return Object.fromEntries(all.map((a) => [a, 0]));  }  function normalizeMonsterLevels(levels) {    if (!isPlainObject(levels) || all.some((a) => !Number.isFinite(Number(levels[a])))) return null;    return Object.fromEntries(all.map((a) => [a, clampInt(levels[a], 0, maxLevel(a))]));  }  function normalizeAttrPrices(value) {    const source = isPlainObject(value) ? value : {};    return Object.fromEntries(all.map((attr) => [      attr,      positiveNumber(source[attr]) || defaultPrice[attr],    ]));  }  const savedState = gmGet(STORE_KEY, {});  const storedState = isPlainObject(savedState) ? savedState : {};  const state = {    language: storedState.language ?? 'en',    targetPL: storedState.targetPL ?? 750,    priceSource: storedState.priceSource ?? 'ask',    orderPriceSource: storedState.orderPriceSource ?? 'bid',    prices: storedState.prices,    priceCache: storedState.priceCache,    orderUnitPrices: storedState.orderUnitPrices,    levels: storedState.levels,    monsterCache: storedState.monsterCache,    selectedMonsterSlots: storedState.selectedMonsterSlots,    lastMonsterSlot: storedState.lastMonsterSlot ?? '',  };  state.prices = normalizeAttrPrices(state.prices);  const storedPriceCache = isPlainObject(state.priceCache) ? state.priceCache : {};  state.priceCache = {};  state.levels = normalizeMonsterLevels(state.levels) || defaultLevels();  const normalizedMonsterCache = {};  if (isPlainObject(state.monsterCache)) {    Object.entries(state.monsterCache).forEach(([slot, record]) => {      const levels = normalizeMonsterLevels(record?.levels);      if (!/^\d+$/.test(slot) || !levels) return;      normalizedMonsterCache[slot] = {        slot,        name: String(record.name || `Monster ${slot}`),        levels,      };    });  }  state.monsterCache = normalizedMonsterCache;  const legacyMonsterLevels = normalizeMonsterLevels(storedState.levels);  if (/^\d+$/.test(String(state.lastMonsterSlot)) && legacyMonsterLevels && !state.monsterCache[state.lastMonsterSlot]) {    const slot = String(state.lastMonsterSlot);    state.monsterCache[slot] = {      slot,      name: `Monster ${slot}`,      levels: legacyMonsterLevels,    };  }  state.orderUnitPrices = normalizeAttrPrices(state.orderUnitPrices);  if (!priceSourceValues.includes(state.priceSource)) {    state.priceSource = 'ask';  }  if (!priceSourceValues.includes(state.orderPriceSource)) {    state.orderPriceSource = 'bid';  }  if (!Array.isArray(state.selectedMonsterSlots)) {    state.selectedMonsterSlots = [];  }  state.selectedMonsterSlots = Array.from(new Set(    state.selectedMonsterSlots.map(String).filter((slot) => /^\d+$/.test(slot))  ));  state.language = normalizeLanguage(state.language);  const hvutSavedPrices = readHvutPriceStore();  for (const a of all) {    const crystal = crystalByAttr[a];    const storedCache = isPlainObject(storedPriceCache[crystal]) ? storedPriceCache[crystal] : {};    const cached = {};    state.priceCache[crystal] = cached;    marketPriceSources.forEach((source) => {      const value = positiveNumber(storedCache[source]);      if (value) cached[source] = value;    });    cached.hvut = positiveNumber(storedCache.hvut) || positiveNumber(hvutSavedPrices[crystal]) || undefined;    cached[state.priceSource] = positiveNumber(cached[state.priceSource]) || state.prices[a];    cached.batchSize = Math.floor(positiveNumber(storedCache.batchSize)) || undefined;    state.levels[a] = clampInt(state.levels[a], 0, maxLevel(a));  }  const runtime = {    monsterList: [],    monsters: new Map(Object.entries(state.monsterCache)),    marketData: Object.fromEntries(crystalNames.map((name) => [name, Object.assign({}, state.priceCache[name])])),    inventoryLoaded: false,    orderBooksLoaded: false,    lastPlan: null,    busy: false,    calculationTimer: null,    calculationPending: false,  };  const initialCachedMonster = runtime.monsters.get(String(state.lastMonsterSlot || state.selectedMonsterSlots[0] || ''));  if (initialCachedMonster) state.levels = Object.assign({}, initialCachedMonster.levels);  function saveState() {    gmSet(STORE_KEY, state);  }  function isPrimary(a) {    return primarySet.has(a);  }  function maxLevel(a) {    return isPrimary(a) ? 25 : 50;  }  function primaryTotalPL(n) {    return n * (6 + (n - 1) * 0.5) / 2;  }  function elementalTotalPL(n) {    const t = Math.floor(n / 10);    const r = n % 10;    return (t + 1) * (t * 5 + r);  }  function deltaPL(a, n) {    return isPrimary(a) ? 3 + n * 0.5 : Math.floor(1 + n * 0.1);  }  function crystalCost(a, n) {    const primaryAttr = isPrimary(a);    const base = primaryAttr ? PRIMARY_COST_BASE : ELEMENTAL_COST_BASE;    const rate = primaryAttr ? PRIMARY_COST_RATE : ELEMENTAL_COST_RATE;    return Math.round(base * Math.pow(rate, n));  }  function totalPL(levels) {    let pl = 0;    primary.forEach((a) => {      pl += primaryTotalPL(levels[a] || 0);    });    elemental.forEach((a) => {      pl += elementalTotalPL(levels[a] || 0);    });    return pl;  }  const maxPL = primary.length * primaryTotalPL(25) + elemental.length * elementalTotalPL(50);  function formatPL(v) {    return Math.abs(v - Math.round(v)) < EPS      ? String(Math.round(v))      : v.toFixed(2).replace(/\.?0+$/, '');  }  function formatMoney(v) {    if (!Number.isFinite(v)) return '-';    if (v >= 1000000) return (v / 1000000).toFixed(3) + ' MC';    if (v >= 1000) return (v / 1000).toFixed(3) + ' KC';    return v.toFixed(2) + ' C';  }  function buildOptions(attr, startLevel, targetW) {    const opts = [{      k: 0,      w: 0,      cost: 0,    }];    let accW = 0;    let accCrystals = 0;    let accCost = 0;    for (let n = startLevel; n < maxLevel(attr); n++) {      const dpl = deltaPL(attr, n);      const w = Math.round(dpl * PL_SCALE);      const crystals = crystalCost(attr, n);      const c = crystals * Number(state.prices[attr] || defaultPrice[attr]);      accW += w;      accCrystals += crystals;      accCost += c;      if (accW > targetW) break;      opts.push({        attr,        k: n - startLevel + 1,        w: accW,        crystals: accCrystals,        cost: accCost,      });    }    return opts;  }  function solveExact(startLevels, target) {    const currentPL = totalPL(startLevels);    const effectiveTarget = Math.max(target, currentPL);    if (effectiveTarget > maxPL + EPS) {      return {        ok: false,        message: t('errorTargetOverMax', { max: formatPL(maxPL) }),      };    }    const need = effectiveTarget - currentPL;    const needWFloat = need * PL_SCALE;    const needW = Math.round(needWFloat);    if (Math.abs(needWFloat - needW) > 1e-7) {      return {        ok: false,        message: t('errorTargetUnit'),      };    }    let dp = new Array(needW + 1).fill(Infinity);    dp[0] = 0;    const parents = [];    for (let gi = 0; gi < all.length; gi++) {      const attr = all[gi];      const opts = buildOptions(attr, startLevels[attr], needW);      const ndp = new Array(needW + 1).fill(Infinity);      const parent = new Array(needW + 1).fill(null);      for (let j = 0; j <= needW; j++) {        if (!Number.isFinite(dp[j])) continue;        for (const opt of opts) {          const nj = j + opt.w;          if (nj > needW) continue;          const candidate = dp[j] + opt.cost;          if (candidate + EPS < ndp[nj]) {            ndp[nj] = candidate;            parent[nj] = { prev: j, opt };          }        }      }      dp = ndp;      parents.push(parent);    }    if (!Number.isFinite(dp[needW])) {      return {        ok: false,        message: t('errorTargetUnreachable'),      };    }    const chosen = [];    let cur = needW;    for (let gi = all.length - 1; gi >= 0; gi--) {      const p = parents[gi][cur];      if (!p) {        return {          ok: false,          message: t('errorCalculationPath'),        };      }      if (p.opt.k > 0) chosen.push(p.opt);      cur = p.prev;    }    const finalLevels = Object.assign({}, startLevels);    const agg = Object.fromEntries(all.map((attr) => [attr, {      from: startLevels[attr],      to: startLevels[attr],      k: 0,      crystals: 0,      cost: 0,    }]));    let totalCost = 0;    chosen.forEach((opt) => {      finalLevels[opt.attr] += opt.k;      const item = agg[opt.attr];      item.to += opt.k;      item.k += opt.k;      item.crystals += opt.crystals;      item.cost += opt.cost;      totalCost += opt.cost;    });    return {      ok: true,      currentPL,      targetPL: effectiveTarget,      finalLevels,      agg,      totalCost,    };  }  function readInputs({ save = true } = {}) {    all.forEach((a) => {      const orderPrice = $(`#hvmepp-order-price-${a}`);      if (orderPrice) {        const crystal = crystalByAttr[a];        const batchSize = Math.floor(positiveNumber(runtime.marketData[crystal]?.batchSize));        const batchPrice = Math.max(1, Math.round(positiveNumber(orderPrice.value)));        if (batchSize && batchPrice) {          state.orderUnitPrices[a] = batchPrice / batchSize;          orderPrice.value = batchPrice;        }      }    });    const target = $('#hvmepp-target');    if (target) state.targetPL = parseFloat(target.value) || 0;    const source = $('#hvmepp-source');    if (source) state.priceSource = source.value;    const orderSource = $('#hvmepp-order-source');    if (orderSource) state.orderPriceSource = orderSource.value;    if (save) saveState();  }  function setLevels(levels, { save = true } = {}) {    all.forEach((a) => {      state.levels[a] = clampInt(levels[a], 0, maxLevel(a));    });    updateCurrentPL({ save: false });    if (save) saveState();  }  function updateCurrentPL({ save = true } = {}) {    readInputs({ save });    const current = totalPL(state.levels);    const need = (Number(state.targetPL) || 0) - current;    const node = $('#hvmepp-live');    if (node) {      node.replaceChildren(        `${t('labelCurrentPL')}: `, elt('b', { text: formatPL(current) }),        `  ${t('labelTargetPL')}: `, elt('b', { text: formatPL(state.targetPL || 0) }),        `  ${t('labelNeedPL')}: `, elt('b', { text: formatPL(need) }),        `  ${t('labelMaxPL')}: `, elt('b', { text: formatPL(maxPL) })      );    }  }  function parseMonsterLevelsFromDoc(doc) {    const stats = $all('#monsterstats_top td:nth-child(2)', doc)      .map((td) => parseInt(td.textContent, 10))      .filter((n) => Number.isFinite(n));    if (stats.length < 12) return null;    return Object.fromEntries(all.map((a, i) => [a, stats[i]]));  }  function extractSlotFromMonsterRow(row) {    const candidates = [      row.getAttribute('onclick') || '',      ...$all('a[href]', row).map((a) => a.getAttribute('href') || ''),    ];    for (const text of candidates) {      const slot = text.match(/[?&]slot=(\d+)/)?.[1];      if (slot) return slot;    }    return '';  }  function parseMonsterName(row, slot) {    const explicit = row.querySelector('[data-name], .hvmepp-monster-name')?.textContent?.trim();    if (explicit) return explicit;    const cells = Array.from(row.children);    const originalName = cells[1]?.textContent?.trim();    if (originalName) return originalName;    const text = row.textContent.replace(/\s+/g, ' ').trim();    const withoutPL = text.replace(/\bPL\s*\d+\b/i, '').trim();    return withoutPL || t('monsterFallback', { slot });  }  function parseMonsterPL(row) {    const text = row.textContent.replace(/\s+/g, ' ');    const fromText = text.match(/\bPL\s*(\d+)/i)?.[1];    if (fromText) return parseInt(fromText, 10);    const structuredPL = parseNum(row.children[2]?.textContent);    if (Number.isFinite(structuredPL)) return Math.floor(structuredPL);    const originalPL = Array.from(row.children).slice(2)      .map((child) => child.textContent || '')      .map((text) => text.match(/^\s*(?:PL\s*)?(\d{1,4})\s*$/i)?.[1])      .find(Boolean);    return originalPL ? parseInt(originalPL, 10) : 0;  }  function parseMonsterList() {    return $all('#slot_pane > div').map((div) => {      const onclick = div.getAttribute('onclick') || '';      if (onclick.includes('&create=new')) return null;      const index = extractSlotFromMonsterRow(div);      if (!index) return null;      return {        index,        name: parseMonsterName(div, index),        pl: parseMonsterPL(div),      };    }).filter(Boolean);  }  async function fetchMonsterLevels(slot) {    const html = await fetchText(`?s=Bazaar&ss=ml&slot=${encodeURIComponent(slot)}`);    const doc = htmlToDoc(html);    const levels = parseMonsterLevelsFromDoc(doc);    if (!levels) {      throw new Error(t('errorParseMonster'));    }    return levels;  }  function getMonsterMeta(slot) {    const key = String(slot);    return runtime.monsterList.find((monster) => String(monster.index) === key) || {      name: t('monsterFallback', { slot: key }),    };  }  function cacheMonsterRecord(record, { save = true } = {}) {    const slot = String(record?.slot || '');    const levels = normalizeMonsterLevels(record?.levels);    if (!/^\d+$/.test(slot) || !levels) return null;    const meta = getMonsterMeta(slot);    const cached = {      slot,      name: String(record.name || meta.name || t('monsterFallback', { slot })),      levels,    };    state.monsterCache[slot] = cached;    runtime.monsters.set(slot, cached);    if (save) saveState();    return cached;  }  function getSelectedMonsterSlots() {    const checkboxes = $all('.hvmepp-monster-check');    if (checkboxes.length) {      return checkboxes.filter((checkbox) => checkbox.checked).map((checkbox) => String(checkbox.value));    }    if (query.slot) return [String(query.slot)];    return state.selectedMonsterSlots.slice();  }  function clearCurrentPlan() {    runtime.lastPlan = null;    const box = $('#hvmepp-result');    if (box) replacePlanContent(box);  }  function invalidateCurrentPlan() {    runtime.lastPlan = null;    const box = $('#hvmepp-result');    if (!box) return;    box.setAttribute('aria-busy', 'true');    $all('button, input, select', box).forEach((control) => {      control.disabled = true;    });  }  function clearScheduledCalculation() {    if (runtime.calculationTimer !== null) clearTimeout(runtime.calculationTimer);    runtime.calculationTimer = null;    runtime.calculationPending = false;  }  function scheduleCalculation(delay = 0) {    if (runtime.calculationTimer !== null) clearTimeout(runtime.calculationTimer);    runtime.calculationPending = false;    invalidateCurrentPlan();    runtime.calculationTimer = setTimeout(() => {      runtime.calculationTimer = null;      if (runtime.busy) {        runtime.calculationPending = true;        return;      }      calculate();    }, Math.max(0, Number(delay) || 0));  }  function syncMonsterSelection() {    if (runtime.busy) {      const selected = new Set(state.selectedMonsterSlots.map(String));      $all('.hvmepp-monster-check').forEach((checkbox) => {        checkbox.checked = selected.has(String(checkbox.value));      });      runtime.calculationPending = true;      return;    }    state.selectedMonsterSlots = getSelectedMonsterSlots();    if (state.selectedMonsterSlots.length) {      state.lastMonsterSlot = state.selectedMonsterSlots[0];      const loaded = runtime.monsters.get(state.lastMonsterSlot);      setLevels(loaded?.levels || defaultLevels(), { save: false });    } else {      state.lastMonsterSlot = '';      setLevels(defaultLevels(), { save: false });    }    refreshMonsterSelectionSummary();    if (state.selectedMonsterSlots.length) scheduleCalculation();    else calculate();  }  function refreshMonsterSelectionSummary() {    const node = $('#hvmepp-selection-summary');    if (!node) return;    const slots = getSelectedMonsterSlots();    const loaded = slots.filter((slot) => runtime.monsters.has(String(slot))).length;    node.textContent = `${t('labelSelectedMonsters')}: ${slots.length} (${t('labelLoadedCount')}: ${loaded}/${slots.length})`;  }  async function mapLimit(values, limit, worker) {    const items = Array.from(values);    const results = new Array(items.length);    let nextIndex = 0;    async function run() {      while (nextIndex < items.length) {        const index = nextIndex++;        results[index] = await worker(items[index], index);      }    }    await Promise.all(Array.from({ length: Math.min(limit, items.length) }, run));    return results;  }  async function loadSelectedMonsters() {    const slots = getSelectedMonsterSlots();    if (!slots.length) throw new Error(t('errorNoMonsterSelection'));    state.selectedMonsterSlots = slots;    state.lastMonsterSlot = slots[0];    saveState();    let done = 0;    const records = await mapLimit(slots, 4, async (slot) => {      const levels = query.slot === slot        ? parseMonsterLevelsFromDoc(document) || await fetchMonsterLevels(slot)        : await fetchMonsterLevels(slot);      const meta = getMonsterMeta(slot);      const record = cacheMonsterRecord({        slot: String(slot),        name: meta.name,        levels,      }, { save: false });      done++;      setStatus(t('statusLoadingMonsters', { done, total: slots.length }));      refreshMonsterSelectionSummary();      return record;    });    if (records[0]?.levels) setLevels(records[0].levels, { save: false });    calculate();    setStatus(t('statusLoadedMonsters', { total: records.length }));    return records;  }  function readCurrentMonsterIfAny({ recalculate = true } = {}) {    const levels = parseMonsterLevelsFromDoc(document);    if (!levels) return false;    setLevels(levels, { save: false });    const slot = query.slot || state.lastMonsterSlot || '';    if (slot) {      const meta = getMonsterMeta(slot);      state.lastMonsterSlot = String(slot);      state.selectedMonsterSlots = [String(slot)];      cacheMonsterRecord({        slot: String(slot),        name: meta.name,        levels,      }, { save: false });    }    saveState();    refreshMonsterSelectionSummary();    if (recalculate) calculate();    setStatus(t('statusLoadedCurrentMonster'));    return true;  }  function parseMarketRows(doc, result) {    const table = $('#market_itemlist table', doc);    if (!table) return;    const headers = Array.from(table.rows[0]?.cells || [])      .map((cell) => cell.textContent.trim().replace(/\s+/g, ' '));    const findCol = (...names) => headers.findIndex((header) => names.includes(header));    const nameCol = Math.max(0, findCol('Item', '物品'));    const stockCol = findCol('Your Stock', '你的库存');    const bidCol = findCol('Market Bid', '市场买价');    const askCol = findCol('Market Ask', '市场卖价');    Array.from(table.rows).forEach((tr, i) => {      if (i === 0 || !tr.cells || tr.cells.length < 2) return;      const name = canonicalCrystalName(tr.cells[nameCol]?.textContent);      if (!name) return;      const onclick = tr.getAttribute('onclick') || '';      const href = tr.querySelector('a[href*="itemid="]')?.getAttribute('href') || '';      const itemid = (onclick + ' ' + href).match(/itemid=(\d+)/)?.[1];      const stock = stockCol < 0 ? NaN : parseNum(tr.cells[stockCol]?.textContent);      const bid = bidCol < 0 ? NaN : parseNum(tr.cells[bidCol]?.textContent);      const ask = askCol < 0 ? NaN : parseNum(tr.cells[askCol]?.textContent);      result[name] = Object.assign(result[name] || {}, {        itemid,        stock,        bid,        ask,        unitBid: bid,        unitAsk: ask,      });    });  }  async function fetchMarketCrystalIndex() {    const result = {};    const urls = [      '?s=Bazaar&ss=mk&filter=mo&screen=browseitems',      '?s=Bazaar&ss=mk&filter=ma&screen=browseitems',      '?s=Bazaar&ss=mk&screen=browseitems',    ];    for (const url of urls) {      try {        const html = await fetchText(url);        parseMarketRows(htmlToDoc(html), result);        if (          crystalNames.every((n) =>            result[n]?.itemid ||            Number.isFinite(result[n]?.ask) ||            Number.isFinite(result[n]?.bid)          )        ) {          break;        }      } catch (e) {        console.warn('[PL Planner] Market browse failed:', url, e);      }    }    return result;  }  function parseMarketBatchSize(doc) {    const infoText = `${$('#market_iteminfo', doc)?.textContent || ''} ${$('.market_placeorder', doc)?.textContent || ''}`;    const explicitBatch = infoText.match(/batches?\s+of\s+([\d,]+)/i)?.[1];    const localizedBatch = infoText.match(/(?:|)\s*([\d,]+)\s*(?:||)/i)?.[1];    const xBatch = infoText.match(/[]\s*([\d,]+)/i)?.[1];    const batchSize = Math.floor(parseNum(explicitBatch || localizedBatch || xBatch));    return Number.isFinite(batchSize) && batchSize > 0 ? batchSize : 0;  }  function parseMarketDirectBuyPrice(doc) {    const infoText = $('#market_iteminfo', doc)?.textContent || '';    const raw = infoText.match(/Can always be bought for\s+([\d,]+)\s*C/i)?.[1]      || infoText.match(/(?:direct supply price|直接供貨價|直接供货价)[^\d]*([\d,]+)\s*C/i)?.[1];    const price = Math.floor(parseNum(raw));    return Number.isFinite(price) && price > 0 ? price : 0;  }  function parseMarketBalance(doc) {    const candidates = [      ...$all('.credit_balance', doc),      $('#market_xfer', doc),      doc.body,    ].filter(Boolean);    for (const node of candidates) {      const match = (node.textContent || '').match(/Market Balance[\s\S]*?([\d,]+)\s*C/i);      const balance = parseNum(match?.[1]);      if (Number.isFinite(balance)) return Math.max(0, Math.floor(balance));    }    return null;  }  function parseOrderBook(doc, selector) {    return $all(`${selector} .market_itemorders table tr`, doc).slice(1).map((row) => ({      crystals: Math.max(0, Math.floor(parseNum(row.cells?.[0]?.textContent) || 0)),      batchPrice: Math.max(0, Math.round(parseNum(row.cells?.[1]?.textContent) || 0)),    })).filter((order) => order.crystals > 0 && order.batchPrice > 0);  }  async function fetchMarketDetailData(itemid) {    const html = await fetchText(`?s=Bazaar&ss=mk&screen=browseitems&filter=mo&itemid=${encodeURIComponent(itemid)}`);    const doc = htmlToDoc(html);    const name = canonicalCrystalName($('#market_itemheader', doc)?.children?.[1]?.textContent);    const batchSize = parseMarketBatchSize(doc);    const count = $('#buyorder_batchcount, input[name="buyorder_batchcount"]', doc);    const rows = $('#market_price', doc)?.rows;    if (!name) return null;    const result = {      batchSize,      directBuyPrice: parseMarketDirectBuyPrice(doc),      askOrders: parseOrderBook(doc, '#market_itemsell'),      bidOrders: parseOrderBook(doc, '#market_itembuy'),      currentOrder: {        count: Math.max(0, Math.floor(parseNum(count?.value) || 0)),      },    };    if (!batchSize || !rows || rows.length < 5) return result;    function avg(rowIndex) {      const text = rows[rowIndex]?.cells?.[3]?.textContent || '';      const v = parseNum(text);      return v ? v / batchSize : 0;    }    return Object.assign(result, {      day: avg(1),      week: avg(2),      month: avg(3),      year: avg(4),    });  }  async function enrichCrystalMarketData(data) {    const targets = crystalNames.filter((name) => data[name]?.itemid);    let done = 0;    await mapLimit(targets, MARKET_DETAIL_CONCURRENCY, async (name) => {      let detail = null;      try {        detail = await fetchMarketDetailData(data[name].itemid);      } catch (e) {        console.warn('[PL Planner] Market crystal detail failed:', name, e);      }      done++;      setStatus(t('statusReadingMarket', { done, total: targets.length }));      if (detail) {        const row = Object.assign(data[name], detail);        const batchSize = positiveNumber(row.batchSize);        row.batchBid = batchSize && positiveNumber(row.unitBid) ? Math.max(1, Math.round(row.unitBid * batchSize)) : 0;        row.batchAsk = batchSize && positiveNumber(row.unitAsk) ? Math.max(1, Math.round(row.unitAsk * batchSize)) : 0;      }    });  }  async function fetchCrystalMarketData() {    const data = await fetchMarketCrystalIndex();    await enrichCrystalMarketData(data);    runtime.marketData = Object.assign({}, runtime.marketData, data);    return data;  }  function getCrystalPricesForSource(source, data = runtime.marketData) {    const prices = {};    all.forEach((attr) => {      const crystal = crystalByAttr[attr];      const value = positiveNumber(data[crystal]?.[source]);      if (value) prices[crystal] = value;    });    return prices;  }  function applyPricesByCrystal(priceByCrystal) {    let updated = 0;    const failed = [];    displayAll.forEach((a) => {      const crystal = crystalByAttr[a];      const v = positiveNumber(priceByCrystal[crystal]);      if (v) {        state.prices[a] = v;        updated++;      } else {        failed.push(crystalLabel(crystal));      }    });    return { updated, failed };  }  function rebuildLastPlan() {    if (!runtime.lastPlan?.results?.length) return;    const monsters = runtime.lastPlan.results.map((result) => ({      slot: result.monsterSlot,      name: result.monsterName,      levels: result.startLevels,    }));    runtime.lastPlan = buildBatchPlan(monsters, Number(state.targetPL));  }  function persistMarketPriceCache(data) {    const hvutPrices = readHvutPriceStore();    let updated = 0;    const failed = [];    all.forEach((attr) => {      const crystal = crystalByAttr[attr];      const market = data[crystal] || {};      const cached = state.priceCache[crystal] || (state.priceCache[crystal] = {});      marketPriceSources.forEach((source) => {        const value = positiveNumber(market[source]);        if (value) cached[source] = value;      });      const hvut = positiveNumber(hvutPrices[crystal]);      if (hvut) cached.hvut = hvut;      if (positiveNumber(market.batchSize)) cached.batchSize = Math.floor(market.batchSize);      if (marketPriceSources.every((source) => positiveNumber(cached[source]))) updated++;      else failed.push(crystalLabel(crystal));    });    return { updated, failed };  }  function applyCalculationPriceSource(source = state.priceSource) {    return applyPricesByCrystal(getCrystalPricesForSource(source, state.priceCache));  }  function applyOrderPricesFromCache({ save = true, notify = true } = {}) {    const source = state.orderPriceSource || 'bid';    const prices = getCrystalPricesForSource(source, state.priceCache);    let updated = 0;    const failed = [];    all.forEach((attr) => {      const unitPrice = positiveNumber(prices[crystalByAttr[attr]]);      if (!unitPrice) {        failed.push(crystalLabel(attr));        return;      }      state.orderUnitPrices[attr] = unitPrice;      const batchSize = Math.floor(positiveNumber(        runtime.marketData[crystalByAttr[attr]]?.batchSize        || state.priceCache[crystalByAttr[attr]]?.batchSize      ));      const input = $(`#hvmepp-order-price-${attr}`);      if (input && batchSize) {        input.value = Math.max(1, Math.round(unitPrice * batchSize));        input.disabled = false;      }      updated++;    });    if (save) saveState();    if (notify) {      setStatus(failed.length        ? t('statusOrderPricesPartial', {          source: priceSourceLabel(source),          updated,          failed: joinList(failed),        })        : t('statusOrderPricesComplete', { source: priceSourceLabel(source) }));    }    return { updated, failed };  }  async function refreshMarketSnapshot({ silent = false, rerender = true, applySavedSources = true } = {}) {    readInputs({ save: false });    if (!silent) setStatus(t('statusReadingMarket'));    const data = await fetchCrystalMarketData();    runtime.inventoryLoaded = true;    runtime.orderBooksLoaded = true;    const cacheResult = persistMarketPriceCache(data);    if (applySavedSources) {      applyCalculationPriceSource();      applyOrderPricesFromCache({ save: false, notify: false });      const selectedPrices = getCrystalPricesForSource(state.priceSource, state.priceCache);      if (Object.keys(selectedPrices).length) {        writeHvutPrices(selectedPrices);        Object.entries(selectedPrices).forEach(([crystal, value]) => {          state.priceCache[crystal].hvut = value;        });      }      rebuildLastPlan();    }    saveState();    if (rerender && runtime.lastPlan) renderPlan(runtime.lastPlan);    const snapshotMissing = crystalNames.filter((name) =>      !Number.isFinite(runtime.marketData[name]?.stock)      || !positiveNumber(runtime.marketData[name]?.batchSize)      || !Array.isArray(runtime.marketData[name]?.askOrders)      || !Array.isArray(runtime.marketData[name]?.bidOrders)    );    const failed = Array.from(new Set([...cacheResult.failed, ...snapshotMissing.map(crystalLabel)]));    if (!silent) {      setStatus(failed.length        ? t('statusMarketPartial', { updated: cacheResult.updated, failed: joinList(failed) })        : t('statusMarketComplete', { source: priceSourceLabel(state.priceSource) }));    }    return runtime.marketData;  }  function levelsEqual(a, b) {    return all.every((attr) => Number(a?.[attr]) === Number(b?.[attr]));  }  function getSelectedLoadedMonsters() {    const slots = getSelectedMonsterSlots();    return slots.map((slot) => runtime.monsters.get(String(slot))).filter(Boolean);  }  function buildBatchPlan(monsters, target) {    const results = monsters.map((monster) => {      const levels = Object.assign({}, monster.levels);      const result = solveExact(levels, target);      return Object.assign(result, {        startLevels: levels,        monsterSlot: String(monster.slot),        monsterName: monster.name || t('monsterFallback', { slot: monster.slot }),      });    });    const requirements = Object.fromEntries(all.map((attr) => [attr, 0]));    let totalCost = 0;    results.forEach((result) => {      if (!result.ok) return;      totalCost += result.totalCost;      all.forEach((attr) => {        requirements[attr] += Number(result.agg[attr].crystals) || 0;      });    });    return {      ok: results.length > 0 && results.every((result) => result.ok),      targetPL: target,      results,      requirements,      totalCost,      monsterCount: results.length,    };  }  function buildUpgradeRequests(result, slot) {    const requests = [];    all.forEach((attr) => {      let count = result.agg[attr].k;      const queryName = upgradeQueryByAttr[attr];      while (count > 0) {        const step = Math.min(10, count);        requests.push({          attr,          count: step,          slot: String(slot),          url: `?s=Bazaar&ss=ml&slot=${encodeURIComponent(slot)}`,          data: `crystal_upgrade=${encodeURIComponent(queryName)}&crystal_count=${step}`,        });        count -= step;      }    });    return requests;  }  function requiredTradeBatches(shortage, batchSize) {    const missing = Math.max(0, Math.ceil(Number(shortage) || 0));    if (!missing) return 0;    const size = Math.floor(positiveNumber(batchSize));    return size ? Math.floor(missing / size) + 1 : null;  }  function estimateOrderBookPurchase(crystalsToBuy, batchSize, askOrders, directBuyPrice) {    const size = Math.floor(positiveNumber(batchSize));    const totalBatches = size ? Math.floor(positiveNumber(crystalsToBuy) / size) : 0;    if (!size || !totalBatches || !Array.isArray(askOrders)) return null;    let remainingBatches = totalBatches;    let estimatedCost = 0;    let lowestBatchPrice = 0;    let highestBatchPrice = 0;    const sortedAsks = askOrders.slice().sort((a, b) => a.batchPrice - b.batchPrice);    sortedAsks.forEach((order) => {      if (!remainingBatches) return;      const availableBatches = Math.floor(positiveNumber(order.crystals) / size);      const batchPrice = Math.round(positiveNumber(order.batchPrice));      if (!availableBatches || !batchPrice) return;      const matched = Math.min(remainingBatches, availableBatches);      estimatedCost += matched * batchPrice;      remainingBatches -= matched;      if (!lowestBatchPrice) lowestBatchPrice = batchPrice;      highestBatchPrice = batchPrice;    });    const guaranteedPrice = Math.round(positiveNumber(directBuyPrice));    const guaranteedBatches = remainingBatches;    if (remainingBatches && guaranteedPrice) {      estimatedCost += remainingBatches * guaranteedPrice;      if (!lowestBatchPrice) lowestBatchPrice = guaranteedPrice;      highestBatchPrice = Math.max(highestBatchPrice, guaranteedPrice);      remainingBatches = 0;    }    return {      estimatedCost: remainingBatches ? null : estimatedCost,      lowestBatchPrice,      highestBatchPrice,      guaranteedBatches,    };  }  function getPurchaseEstimateSummary(rows) {    let estimatedCost = 0;    let requiredStartingBalance = 0;    const shortages = rows.filter((row) => row.shortage > 0 && row.batches > 0);    if (!shortages.length || shortages.some((row) => !Number.isFinite(row.estimatedDirectCost))) return null;    shortages.forEach((row) => {      requiredStartingBalance = Math.max(        requiredStartingBalance,        estimatedCost + row.directBalanceRequired      );      estimatedCost += row.estimatedDirectCost;    });    return { estimatedCost, requiredStartingBalance };  }  function getCrystalPlanRows(plan) {    return displayAll.map((attr) => {      const crystal = crystalByAttr[attr];      const market = runtime.marketData[crystal] || {};      const required = Math.max(0, Math.ceil(Number(plan?.requirements?.[attr]) || 0));      const rawStock = market.stock;      const stock = runtime.inventoryLoaded && Number.isFinite(rawStock) ? Math.max(0, rawStock) : null;      const shortage = stock === null ? null : Math.max(0, required - stock);      const batchSize = Math.floor(positiveNumber(market.batchSize)) || null;      const batches = shortage === null ? null : requiredTradeBatches(shortage, batchSize);      const crystalsToBuy = batches === null || !batchSize ? null : batches * batchSize;      const orderUnitPrice = positiveNumber(state.orderUnitPrices[attr]) || defaultPrice[attr];      const orderBatchPrice = batchSize ? Math.max(1, Math.round(orderUnitPrice * batchSize)) : null;      const directBuyPrice = Math.floor(positiveNumber(market.directBuyPrice)) || 0;      const estimate = runtime.orderBooksLoaded && crystalsToBuy        ? estimateOrderBookPurchase(crystalsToBuy, batchSize, market.askOrders, directBuyPrice)        : null;      return {        attr,        crystal,        label: crystalLabel(attr),        itemid: market.itemid || '',        required,        stock,        shortage,        batchSize,        batches,        crystalsToBuy,        orderBatchPrice,        directBuyPrice,        estimatedDirectCost: estimate?.estimatedCost ?? null,        estimatedLowestBatchPrice: estimate?.lowestBatchPrice || 0,        estimatedHighestBatchPrice: estimate?.highestBatchPrice || 0,        guaranteedBatches: estimate?.guaranteedBatches || 0,        directBalanceRequired: batches && directBuyPrice ? batches * directBuyPrice : 0,        currentOrder: {          count: Math.max(0, Math.floor(Number(market.currentOrder?.count) || 0)),        },      };    });  }  function getCrystalShortages(rows) {    return rows.filter((row) => row.shortage > 0 && row.batches > 0);  }  function getUnknownCrystalRows(rows) {    return rows.filter((row) =>      row.required > 0      && (row.stock === null || (row.shortage > 0 && (!row.itemid || !row.batchSize)))    );  }  function sleep(ms) {    return new Promise((resolve) => setTimeout(resolve, ms));  }  function marketResponseError(doc) {    const messages = $all('#messagebox_inner p', doc).map((node) => node.textContent.trim()).filter(Boolean);    return messages.find((message) => /(?:error|invalid|cannot|not enough|insufficient|exceed|failed)/i.test(message)) || '';  }  function orderNotAppliedError(crystal) {    const error = new Error(t('errorOrderNotApplied', { crystal: crystalLabel(crystal) }));    error.code = 'ORDER_NOT_APPLIED';    return error;  }  function parseCrystalBuyForm(doc, crystal) {    const section = $('#market_itembuy', doc);    if (!section) throw new Error(t('errorMarketItemForm', { crystal: crystalLabel(crystal) }));    const controls = $all('input, button', section);    const token = section.querySelector('input[name="marketoken"]');    const count = section.querySelector('#buyorder_batchcount, input[name="buyorder_batchcount"]')      || controls.find((node) => /buyorder.*count/i.test(`${node.name || ''} ${node.id || ''}`));    const price = section.querySelector('#buyorder_batchprice, input[name="buyorder_batchprice"]')      || controls.find((node) => /buyorder.*price/i.test(`${node.name || ''} ${node.id || ''}`));    const submit = section.querySelector('input[name="buyorder_update"], button[name="buyorder_update"]')      || controls.find((node) => /buyorder.*(?:update|submit|place)/i.test(`${node.name || ''} ${node.id || ''}`));    const deleteControl = controls.find((node) => {      const signature = `${node.name || ''} ${node.id || ''} ${node.value || ''} ${node.textContent || ''}`;      return node !== submit && /buyorder.*(?:delete|cancel|remove)|(?:delete|cancel|remove).*buyorder/i.test(signature);    });    const batchSize = parseMarketBatchSize(doc);    const itemInfoText = $('#market_iteminfo', doc)?.textContent || '';    const inventoryStock = parseNum(      itemInfoText.match(/You have\s+([\d,]+)\s+available/i)?.[1]      || itemInfoText.match(/你有\s*([\d,]+)\s*(?:||)/i)?.[1]    );    if (!token || !count || !price || !submit) {      throw new Error(t('errorMarketItemForm', { crystal: crystalLabel(crystal) }));    }    if (!batchSize) {      throw new Error(t('errorCrystalBatch', { crystal: crystalLabel(crystal) }));    }    function controlName(node) {      return node.getAttribute('name') || node.id || '';    }    const names = {      token: controlName(token),      count: controlName(count),      price: controlName(price),      submit: controlName(submit),    };    if (Object.values(names).some((name) => !name)) {      throw new Error(t('errorMarketItemForm', { crystal: crystalLabel(crystal) }));    }    return {      names,      token: token.value,      submitValue: 'Update',      batchSize,      directBuyPrice: parseMarketDirectBuyPrice(doc),      inventoryStock: Number.isFinite(inventoryStock) ? Math.max(0, Math.floor(inventoryStock)) : null,      currentOrder: {        count: Math.max(0, Math.floor(parseNum(count.value) || 0)),        price: Math.max(0, Math.round(parseNum(price.value) || 0)),      },      deleteControl: deleteControl ? {        name: controlName(deleteControl),        value: deleteControl.value || deleteControl.textContent?.trim() || 'Delete',      } : null,    };  }  async function cancelCrystalBuyOrder(url, responseDoc, form, crystal) {    if (!form.deleteControl?.name) return false;    const params = new URLSearchParams();    params.set(form.names.token, form.token);    params.set(form.deleteControl.name, form.deleteControl.value);    const cancelDoc = htmlToDoc(await fetchText(url, params.toString()));    const error = marketResponseError(cancelDoc);    if (error) throw new Error(error);    try {      return parseCrystalBuyForm(cancelDoc, crystal).currentOrder.count === 0;    } catch (e) {      console.warn('[PL Planner] Could not verify automatic buy-order cancellation:', e);      return false;    }  }  async function placeCrystalBuyOrder(row, mode) {    const label = crystalLabel(row.crystal);    if (!row.itemid) throw new Error(t('errorMarketItemForm', { crystal: label }));    const url = `?s=Bazaar&ss=mk&screen=browseitems&filter=mo&itemid=${encodeURIComponent(row.itemid)}`;    const detailDoc = htmlToDoc(await fetchText(url));    const form = parseCrystalBuyForm(detailDoc, row.crystal);    if (form.batchSize !== row.batchSize) {      throw new Error(t('errorCrystalBatchChanged', {        crystal: label,        expected: row.batchSize,        actual: form.batchSize,      }));    }    if (mode === 'direct' && form.currentOrder.count > 0) {      throw new Error(t('errorExistingBuyOrder', { crystals: label }));    }    const submittedPrice = mode === 'direct' ? form.directBuyPrice : row.orderBatchPrice;    const marketBalance = mode === 'direct' ? parseMarketBalance(detailDoc) : null;    const submittedBatches = row.batches;    if (mode === 'direct' && !positiveNumber(submittedPrice)) {      throw new Error(t('errorDirectPrice', { crystal: label }));    }    const reservedTotal = submittedBatches * submittedPrice;    if (mode === 'direct' && (!Number.isFinite(marketBalance) || marketBalance < reservedTotal)) {      throw new Error(t('errorDirectBalance', {        crystal: label,        required: reservedTotal.toLocaleString(),        balance: Number.isFinite(marketBalance) ? marketBalance.toLocaleString() : '-',      }));    }    const params = new URLSearchParams();    params.set(form.names.token, form.token);    params.set(form.names.count, String(submittedBatches));    params.set(form.names.price, String(submittedPrice));    params.set(form.names.submit, form.submitValue);    const responseDoc = htmlToDoc(await fetchText(url, params.toString()));    const error = marketResponseError(responseDoc);    if (error) throw new Error(error);    const responseForm = parseCrystalBuyForm(responseDoc, row.crystal);    if (responseForm.batchSize !== row.batchSize) {      throw new Error(t('errorCrystalBatchChanged', {        crystal: label,        expected: row.batchSize,        actual: responseForm.batchSize,      }));    }    const remainingBatches = responseForm.currentOrder.count;    const remainingPrice = responseForm.currentOrder.price;    if (!Number.isFinite(form.inventoryStock) || !Number.isFinite(responseForm.inventoryStock)) {      throw new Error(t('errorMarketItemForm', { crystal: label }));    }    const inventoryDelta = responseForm.inventoryStock - form.inventoryStock;    const matchedBatches = inventoryDelta >= 0 && inventoryDelta % row.batchSize === 0      ? inventoryDelta / row.batchSize      : -1;    if (matchedBatches === 0 && remainingBatches === 0 && submittedBatches > 0) {      throw orderNotAppliedError(row.crystal);    }    if (matchedBatches < 0 || matchedBatches + remainingBatches !== submittedBatches) {      throw new Error(t('errorOrderResultMismatch', {        crystal: label,        submitted: submittedBatches,        matched: Math.max(0, matchedBatches),        remaining: remainingBatches,      }));    }    const result = {      matchedBatches,      remainingBatches,      responseStatus: remainingBatches > 0 ? 'pending' : (matchedBatches > 0 ? 'filled' : 'accepted'),      cancelledRemainder: false,    };    if (mode === 'order' && (      remainingBatches > submittedBatches      || (remainingBatches > 0 && remainingPrice !== submittedPrice)    )) {      throw new Error(t('errorMarketItemForm', { crystal: label }));    }    if (mode === 'direct' && remainingBatches > 0) {      const cancelled = await cancelCrystalBuyOrder(url, responseDoc, responseForm, row.crystal);      if (!cancelled) {        throw new Error(t('errorDirectRemainder', {          crystal: label,          batches: remainingBatches,        }));      }      result.cancelledRemainder = true;      result.responseStatus = 'remainder-cancelled';    }    return result;  }  async function executeCrystalPurchase(mode, button) {    const plan = runtime.lastPlan;    if (!plan?.ok) {      setStatus(t('errorNoValidPlan'));      return;    }    setStatus(t('statusCheckingPurchase'));    await refreshMarketSnapshot({ silent: true, rerender: false, applySavedSources: false });    const crystalRows = getCrystalPlanRows(plan);    renderPlan(plan, crystalRows);    const unknown = getUnknownCrystalRows(crystalRows);    if (unknown.length) {      throw new Error(t('statusInventoryPartial', { failed: joinList(unknown.map((row) => row.label)) }));    }    const rows = getCrystalShortages(crystalRows);    if (!rows.length) {      setStatus(t('statusNoShortage'));      return;    }    if (mode === 'order' && rows.some((row) => !positiveNumber(row.orderBatchPrice))) {      throw new Error(t('errorNoValidPlan'));    }    if (mode === 'direct') {      const missingDirectPrice = rows.find((row) => !positiveNumber(row.directBuyPrice));      if (missingDirectPrice) {        throw new Error(t('errorDirectPrice', { crystal: missingDirectPrice.label }));      }      const existing = rows.filter((row) => row.currentOrder.count > 0);      if (existing.length) {        throw new Error(t('errorExistingBuyOrder', {          crystals: joinList(existing.map((row) => row.label)),        }));      }    }    for (let i = 0; i < rows.length; i++) {      const row = rows[i];      const price = mode === 'direct' ? row.directBuyPrice : row.orderBatchPrice;      button.textContent = t('buttonBuying', { current: i + 1, total: rows.length });      setStatus(t('statusExecutingPurchase', {        current: i + 1,        total: rows.length,        crystal: row.label,        crystals: row.crystalsToBuy.toLocaleString(),        price: price.toLocaleString(),      }));      let ignoredRetries = 0;      let result;      while (!result) {        try {          result = await placeCrystalBuyOrder(row, mode);        } catch (error) {          if (mode !== 'direct' || error?.code !== 'ORDER_NOT_APPLIED' || ignoredRetries >= DIRECT_BUY_MAX_RETRIES) {            throw error;          }          ignoredRetries++;          setStatus(t('statusRetryingPurchase', {            crystal: row.label,            attempt: ignoredRetries,          }));          await sleep(DIRECT_BUY_RETRY_DELAY_MS * ignoredRetries);        }      }      if (result.cancelledRemainder) {        throw new Error(t('errorDirectRemainderCancelled', {          crystal: row.label,          batches: result.remainingBatches,        }));      }      if (i + 1 < rows.length) await sleep(REQUEST_DELAY_MS);    }    if (mode === 'direct') {      await refreshMarketSnapshot({ silent: true, rerender: false, applySavedSources: false });      renderPlan(plan);      setStatus(t('statusDirectBuyComplete', { total: rows.length }));    } else {      setStatus(t('statusBuyOrdersComplete', { total: rows.length }));    }  }  async function executeBatchUpgradePlan(plan, button) {    if (!plan?.ok) {      setStatus(t('errorNoValidPlan'));      return;    }    const requests = plan.results.flatMap((result) => buildUpgradeRequests(result, result.monsterSlot));    if (!requests.length) {      setStatus(t('statusNoUpgrade'));      return;    }    let checked = 0;    const currentRecords = await mapLimit(plan.results, 4, async (result) => {      const levels = await fetchMonsterLevels(result.monsterSlot);      checked++;      setStatus(t('statusCheckingMonsters', { done: checked, total: plan.results.length }));      const record = {        slot: result.monsterSlot,        name: result.monsterName,        levels,      };      return cacheMonsterRecord(record, { save: false });    });    saveState();    const changed = plan.results.filter((result, index) => !levelsEqual(currentRecords[index].levels, result.startLevels));    if (changed.length) {      setLevels(currentRecords[0].levels);      setStatus(t('statusBatchUpgradeCancelled', { slots: joinList(changed.map((result) => `#${result.monsterSlot}`)) }));      return;    }    await refreshMarketSnapshot({ silent: true, rerender: false, applySavedSources: false });    const crystalRows = getCrystalPlanRows(plan);    renderPlan(plan, crystalRows);    const unknown = getUnknownCrystalRows(crystalRows);    if (unknown.length) {      setStatus(t('statusInventoryPartial', { failed: joinList(unknown.map((row) => row.label)) }));      return;    }    const shortages = getCrystalShortages(crystalRows);    if (shortages.length) {      setStatus(t('statusCrystalShortage', {        failed: joinList(shortages.map((row) => `${row.label} ${row.shortage.toLocaleString()}`)),      }));      return;    }    const totalLevels = requests.reduce((sum, request) => sum + request.count, 0);    if (!confirm(t('confirmUpgrade', {      monsters: plan.monsterCount,      requests: requests.length,      levels: totalLevels,    }))) return;    for (let i = 0; i < requests.length; i++) {      const request = requests[i];      button.textContent = t('buttonRunningUpgrade', { current: i + 1, total: requests.length });      setStatus(t('statusExecutingUpgrade', {        current: i + 1,        total: requests.length,        slot: request.slot,        attr: attrLabel(request.attr),        count: request.count,      }));      await fetchText(request.url, request.data);      if (i + 1 < requests.length) await sleep(REQUEST_DELAY_MS);    }    const finalRecords = await mapLimit(plan.results, 4, async (result) => {      const levels = await fetchMonsterLevels(result.monsterSlot);      const record = { slot: result.monsterSlot, name: result.monsterName, levels };      return cacheMonsterRecord(record, { save: false });    });    saveState();    const complete = plan.results.every((result, index) => levelsEqual(finalRecords[index].levels, result.finalLevels));    if (finalRecords[0]?.levels) setLevels(finalRecords[0].levels);    await refreshMarketSnapshot({ silent: true, rerender: false, applySavedSources: false });    runtime.lastPlan = buildBatchPlan(finalRecords, plan.targetPL);    renderPlan(runtime.lastPlan);    setStatus(complete      ? t('statusUpgradeComplete', { total: finalRecords.length })      : t('statusUpgradeMismatch'));  }  function renderPriceSourceSelect(id, selectedValue) {    const select = elt('select', { id });    priceSourceValues.forEach((value) => {      select.appendChild(elt('option', {        value,        text: priceSourceLabel(value),        dataset: { source: value },        selected: selectedValue === value,      }));    });    return select;  }  function renderButton(id, textKey) {    return elt('button', {      id,      type: 'button',      text: t(textKey),      dataset: { i18n: textKey },    });  }  function setRuntimeBusy(busy) {    const nextBusy = Boolean(busy);    const resumeCalculation = !nextBusy && runtime.calculationPending;    runtime.busy = nextBusy;    $all('.hvmepp-monster-check, #hvmepp-select-all-monsters, #hvmepp-clear-monsters')      .forEach((control) => { control.disabled = nextBusy; });    if (resumeCalculation) scheduleCalculation();  }  function bindManagedAction(button, textKey, errorKey, action) {    button.addEventListener('click', async () => {      if (runtime.busy) return;      setRuntimeBusy(true);      button.disabled = true;      try {        await action(button);      } catch (error) {        setStatus(t(errorKey, { message: error.message }));      } finally {        button.disabled = false;        button.textContent = t(textKey);        setRuntimeBusy(false);      }    });  }  function setStatus(text) {    const n = $('#hvmepp-status');    if (n) n.textContent = text || '';  }  function refreshLanguageButtons() {    $all('.hvmepp-lang-switch button').forEach((button) => {      const active = button.dataset.lang === state.language;      button.classList.toggle('hvmepp-lang-active', active);      button.setAttribute('aria-pressed', active ? 'true' : 'false');    });  }  function refreshPriceSourceOptions() {    $all('#hvmepp-source option, #hvmepp-order-source option').forEach((option) => {      const source = option.dataset.source || option.value;      option.textContent = priceSourceLabel(source);    });  }  function refreshLocalizedText({ rerenderResult = true } = {}) {    $all('[data-i18n]').forEach((node) => {      node.textContent = t(node.dataset.i18n);    });    $all('[data-i18n-value]').forEach((node) => {      node.value = t(node.dataset.i18nValue);    });    $all('[data-attr]').forEach((node) => {      node.textContent = attrLabel(node.dataset.attr);    });    refreshPriceSourceOptions();    refreshLanguageButtons();    updateCurrentPL({ save: false });    if (rerenderResult && runtime.lastPlan && $('#hvmepp-result')?.hasChildNodes()) {      renderPlan(runtime.lastPlan);    }  }  function setLanguage(language) {    const nextLanguage = normalizeLanguage(language);    readInputs({ save: false });    state.language = nextLanguage;    rebuildLastPlan();    saveState();    refreshLocalizedText();    setStatus(t('statusLanguageSaved'));  }  function renderLanguageSwitcher() {    const bar = elt('div', { class: 'hvmepp-lang-switch' });    languageOptions.forEach(([value, label]) => {      const active = value === state.language;      const button = elt('button', {        type: 'button',        text: label,        class: active ? 'hvmepp-lang-active' : '',        dataset: { lang: value },        ariaPressed: active ? 'true' : 'false',      });      button.addEventListener('click', () => setLanguage(value));      bar.appendChild(button);    });    return bar;  }  function renderSingleMonsterResult(result) {    const details = elt('details', { class: 'hvmepp-monster-result' });    const summaryText = result.ok      ? `#${result.monsterSlot || '-'} ${result.monsterName} / PL ${formatPL(result.currentPL)}${formatPL(result.targetPL)} / ${formatMoney(result.totalCost)}`      : `#${result.monsterSlot || '-'} ${result.monsterName}`;    details.appendChild(elt('summary', { text: summaryText }));    if (!result.ok) {      details.appendChild(elt('div', { class: 'hvmepp-alert', text: result.message }));      return details;    }    const table = elt('table', { class: 'hvmepp-table' });    table.appendChild(elt('tr', {}, [      t('tableAttr'),      t('tableCurrentLevel'),      t('tableTargetLevel'),      t('tableIncrease'),      t('tableCost'),    ].map((heading) => elt('th', { text: heading }))));    const upgraded = displayAll.filter((attr) => result.agg[attr].k > 0);    upgraded.forEach((attr) => {      const item = result.agg[attr];      table.appendChild(elt('tr', { class: 'hvmepp-upgraded' }, [        elt('td', { text: attrLabel(attr) }),        elt('td', { text: item.from }),        elt('td', { text: item.to }),        elt('td', { text: `+${item.k}` }),        elt('td', { text: formatMoney(item.cost) }),      ]));    });    if (!upgraded.length) {      table.appendChild(elt('tr', {}, [        elt('td', { colspan: 5, text: t('noUpgradeNeeded') }),      ]));    }    details.appendChild(table);    return details;  }  function renderCrystalPlan(plan, rows) {    const card = elt('div', { class: 'hvmepp-card hvmepp-crystal-card' });    card.appendChild(elt('h3', {      text: t('headingCrystalNeeds'),      dataset: { i18n: 'headingCrystalNeeds' },    }));    if (!plan.ok) {      card.appendChild(elt('div', { class: 'hvmepp-alert', text: t('crystalPlanUnavailable') }));      return card;    }    const orderSource = renderPriceSourceSelect('hvmepp-order-source', state.orderPriceSource);    orderSource.addEventListener('change', () => {      state.orderPriceSource = orderSource.value;      applyOrderPricesFromCache();    });    card.appendChild(elt('div', { class: 'hvmepp-controls hvmepp-crystal-price-controls' }, [      elt('label', {}, [        elt('span', { text: t('labelOrderPriceSource'), dataset: { i18n: 'labelOrderPriceSource' } }),        ' ',        orderSource,      ]),    ]));    const table = elt('table', { class: 'hvmepp-table hvmepp-crystal-table' });    table.appendChild(elt('tr', {}, [      t('tableCrystal'),      t('tableRequired'),      t('tableStock'),      t('tableShortage'),      t('tableEstimatedCost'),      t('tableOrderPrice'),    ].map((heading) => elt('th', { text: heading }))));    rows.forEach((row) => {      const shortage = row.shortage === null ? '-' : row.shortage.toLocaleString();      let estimatedCost = row.shortage > 0 ? t('estimateUnavailable') : formatMoney(0);      let estimateTitle = '';      if (Number.isFinite(row.estimatedDirectCost)) {        estimatedCost = formatMoney(row.estimatedDirectCost);        const range = row.estimatedLowestBatchPrice === row.estimatedHighestBatchPrice          ? `${row.estimatedLowestBatchPrice} C`          : `${row.estimatedLowestBatchPrice}-${row.estimatedHighestBatchPrice} C`;        estimateTitle = range;        if (row.guaranteedBatches) {          estimateTitle += ` / ${t('estimateUsesSupply', { batches: row.guaranteedBatches.toLocaleString() })}`;        }      }      const tr = elt('tr', { class: row.shortage > 0 ? 'hvmepp-shortage' : '' }, [        elt('td', { text: row.label }),        elt('td', { text: row.required.toLocaleString() }),        elt('td', { text: row.stock === null ? t('stockUnknown') : row.stock.toLocaleString() }),        elt('td', { text: shortage }),        elt('td', { text: estimatedCost, title: estimateTitle }),        elt('td', {}, elt('input', {          id: `hvmepp-order-price-${row.attr}`,          type: 'number',          min: 1,          step: 1,          value: row.orderBatchPrice || '',          disabled: !row.batchSize,          title: row.label,        })),      ]);      table.appendChild(tr);    });    card.appendChild(table);    const estimateSummary = getPurchaseEstimateSummary(rows);    if (estimateSummary) {      card.appendChild(elt('div', {        class: 'hvmepp-estimate-summary',        text: t('estimateSummary', {          cost: formatMoney(estimateSummary.estimatedCost),          balance: formatMoney(estimateSummary.requiredStartingBalance),        }),      }));    }    const actions = elt('div', { class: 'hvmepp-controls hvmepp-result-actions' });    const stockButton = renderButton('hvmepp-load-stock', 'buttonLoadStock');    const directButton = renderButton('hvmepp-direct-buy', 'buttonDirectBuy');    const orderButton = renderButton('hvmepp-place-buy-orders', 'buttonPlaceBuyOrders');    bindManagedAction(stockButton, 'buttonLoadStock', 'statusFetchPriceFailed', refreshMarketSnapshot);    bindManagedAction(directButton, 'buttonDirectBuy', 'statusPurchaseFailed',      (managedButton) => executeCrystalPurchase('direct', managedButton));    bindManagedAction(orderButton, 'buttonPlaceBuyOrders', 'statusPurchaseFailed',      (managedButton) => executeCrystalPurchase('order', managedButton));    actions.append(stockButton, directButton, orderButton);    card.appendChild(actions);    return card;  }  function renderPlan(plan, crystalRows = null) {    const box = $('#hvmepp-result');    if (!box) return;    const fragment = document.createDocumentFragment();    if (!plan || plan.message) {      fragment.appendChild(elt('div', { class: 'hvmepp-alert', text: plan?.message || t('errorNoValidPlan') }));      replacePlanContent(box, fragment);      return;    }    fragment.appendChild(elt('div', { class: 'hvmepp-total' }, [      `${t('totalMonsters')} `,      elt('span', { class: 'hvmepp-good', text: String(plan.monsterCount) }),      `   ${t('totalCost')} `,      elt('span', { class: 'hvmepp-good', text: formatMoney(plan.totalCost) }),    ]));    const rows = crystalRows || getCrystalPlanRows(plan);    fragment.appendChild(renderCrystalPlan(plan, rows));    if (plan.ok) {      const requests = plan.results.flatMap((result) => buildUpgradeRequests(result, result.monsterSlot));      const executable = plan.results.every((result) => /^\d+$/.test(result.monsterSlot));      if (requests.length && executable) {        const runButton = renderButton('hvmepp-run-upgrade', 'buttonRunUpgrade');        bindManagedAction(runButton, 'buttonRunUpgrade', 'statusUpgradeFailed',          (managedButton) => executeBatchUpgradePlan(plan, managedButton));        fragment.appendChild(elt('div', { class: 'hvmepp-controls hvmepp-result-actions' }, [runButton]));      }    }    plan.results.forEach((result) => {      fragment.appendChild(renderSingleMonsterResult(result));    });    replacePlanContent(box, fragment);  }  function replacePlanContent(box, fragment = null) {    const panel = $('#hvmepp-panel');    const scrollTop = panel?.scrollTop || 0;    if (fragment) box.replaceChildren(fragment);    else box.replaceChildren();    box.removeAttribute('aria-busy');    if (panel) panel.scrollTop = scrollTop;  }  function calculate() {    clearScheduledCalculation();    readInputs();    updateCurrentPL({ save: false });    const selected = getSelectedMonsterSlots();    if (!selected.length) {      clearCurrentPlan();      return null;    }    const monsters = getSelectedLoadedMonsters();    if (monsters.length !== selected.length) {      const invalidPlan = { message: t('errorMonstersNotLoaded') };      runtime.lastPlan = null;      renderPlan(invalidPlan);      setStatus(invalidPlan.message);      return invalidPlan;    }    const plan = buildBatchPlan(monsters, Number(state.targetPL));    runtime.lastPlan = plan;    renderPlan(plan);    return plan;  }  function renderMonsterSelection(monsterList) {    const card = elt('div', { class: 'hvmepp-card hvmepp-monster-select-card' });    card.appendChild(elt('h3', {      text: t('headingMonsterSelection'),      dataset: { i18n: 'headingMonsterSelection' },    }));    const toolbar = elt('div', { class: 'hvmepp-controls' });    const selectAllButton = renderButton('hvmepp-select-all-monsters', 'buttonSelectAll');    const clearButton = renderButton('hvmepp-clear-monsters', 'buttonClearSelection');    selectAllButton.disabled = runtime.busy;    clearButton.disabled = runtime.busy;    toolbar.append(selectAllButton, clearButton, elt('span', { id: 'hvmepp-selection-summary' }));    card.appendChild(toolbar);    const selected = new Set(state.selectedMonsterSlots.map(String));    const validSlots = new Set(monsterList.map((monster) => String(monster.index)));    if (query.slot) selected.add(String(query.slot));    if (![...selected].some((slot) => validSlots.has(slot)) && validSlots.has(String(state.lastMonsterSlot))) {      selected.add(String(state.lastMonsterSlot));    }    const list = elt('div', { class: 'hvmepp-monster-checklist' });    monsterList.forEach((monster) => {      const slot = String(monster.index);      const checkbox = elt('input', {        type: 'checkbox',        value: slot,        checked: selected.has(slot),        disabled: runtime.busy,        class: 'hvmepp-monster-check',      });      checkbox.addEventListener('change', syncMonsterSelection);      list.appendChild(elt('label', {}, [        checkbox,        elt('span', { text: `#${slot} ${monster.name} / PL ${monster.pl}` }),      ]));    });    card.appendChild(list);    selectAllButton.addEventListener('click', () => {      if (runtime.busy) return;      $all('.hvmepp-monster-check').forEach((checkbox) => { checkbox.checked = true; });      syncMonsterSelection();    });    clearButton.addEventListener('click', () => {      if (runtime.busy) return;      $all('.hvmepp-monster-check').forEach((checkbox) => { checkbox.checked = false; });      syncMonsterSelection();    });    queueMicrotask(syncMonsterSelection);    return card;  }  function renderPanel() {    let overlay = $('#hvmepp-overlay');    if (overlay) {      overlay.classList.remove('hvmepp-hidden');      const easterEgg = $('#hvmepp-easter-egg');      if (easterEgg) easterEgg.textContent = randomEasterEgg();      refreshLocalizedText();      return;    }    overlay = elt('div', { id: 'hvmepp-overlay' });    const panel = elt('div', { id: 'hvmepp-panel' });    overlay.appendChild(panel);    panel.appendChild(renderLanguageSwitcher());    const easterEgg = elt('button', {      id: 'hvmepp-easter-egg',      class: 'hvmepp-easter-egg',      type: 'button',      text: randomEasterEgg(),      title: 'Click to refresh',    });    easterEgg.addEventListener('click', () => {      easterEgg.textContent = randomEasterEgg(easterEgg.textContent);    });    const closeButton = elt('button', { text: '×', class: 'hvmepp-close' });    const title = elt('div', { class: 'hvmepp-title' }, [      elt('span', { text: t('plannerTitle'), dataset: { i18n: 'plannerTitle' } }),      elt('div', { class: 'hvmepp-title-actions' }, [        easterEgg,        closeButton,      ]),    ]);    closeButton.addEventListener('click', () => {      overlay.classList.add('hvmepp-hidden');    });    panel.appendChild(title);    panel.appendChild(elt('div', {      id: 'hvmepp-live',      class: 'hvmepp-live',    }));    const controls = elt('div', { class: 'hvmepp-controls' });    controls.appendChild(elt('label', {}, [      elt('span', { text: t('labelTargetInput'), dataset: { i18n: 'labelTargetInput' } }),      ' ',      elt('input', {        id: 'hvmepp-target',        type: 'number',        step: 0.5,        value: state.targetPL,      }),    ]));    controls.appendChild(elt('label', {}, [      elt('span', { text: t('labelPriceSource'), dataset: { i18n: 'labelPriceSource' } }),      ' ',      renderPriceSourceSelect('hvmepp-source', state.priceSource),    ]));    controls.appendChild(renderButton('hvmepp-fetch-price', 'buttonFetchPrice'));    controls.appendChild(renderButton('hvmepp-load-monster', 'buttonLoadMonster'));    panel.appendChild(controls);    runtime.monsterList = parseMonsterList();    if (query.slot && !runtime.monsterList.some((monster) => String(monster.index) === String(query.slot))) {      const currentLevels = parseMonsterLevelsFromDoc(document);      runtime.monsterList.push({        index: String(query.slot),        name: $('.msl > div:nth-child(2)')?.textContent?.trim() || t('monsterFallback', { slot: query.slot }),        pl: currentLevels ? totalPL(currentLevels) : 0,      });    }    if (runtime.monsterList.length) {      panel.appendChild(renderMonsterSelection(runtime.monsterList));    }    panel.appendChild(elt('div', {      id: 'hvmepp-status',      class: 'hvmepp-status',    }));    panel.appendChild(elt('div', {      id: 'hvmepp-result',    }));    document.body.appendChild(overlay);    $('#hvmepp-target').addEventListener('input', () => scheduleCalculation(80));    $('#hvmepp-source').addEventListener('change', () => {      readInputs({ save: false });      const result = applyCalculationPriceSource();      scheduleCalculation();      setStatus(result.failed.length        ? t('statusSavedPriceSourcePartial', {          source: priceSourceLabel(state.priceSource),          updated: result.updated,          failed: joinList(result.failed),        })        : t('statusSavedPriceSource', { source: priceSourceLabel(state.priceSource) }));    });    bindManagedAction($('#hvmepp-fetch-price'), 'buttonFetchPrice', 'statusFetchPriceFailed', refreshMarketSnapshot);    bindManagedAction($('#hvmepp-load-monster'), 'buttonLoadMonster', 'statusLoadMonsterFailed', async () => {      if (query.slot && readCurrentMonsterIfAny()) return;      await loadSelectedMonsters();    });    readCurrentMonsterIfAny({ recalculate: false });    updateCurrentPL({ save: false });  }  function findPowerLevelCalculatorButton(side) {    const nodes = $all('input[type="button"], button', side);    return nodes.find((n) => {      const label = (n.value || n.textContent || '').trim();      return label === 'Power Level Calculator';    });  }  function createEntryButton() {    const btn = elt('input', {      id: 'hvmepp-entry',      type: 'button',      value: t('plannerTitle'),      dataset: { i18nValue: 'plannerTitle' },    });    btn.addEventListener('click', renderPanel);    return btn;  }  function getOrCreateEntrySide(createFallback = false) {    const hvutSide = $('.hvut-ml-side');    if (hvutSide) return hvutSide;    if (!createFallback) return null;    const monsterOuter = $('#monster_outer');    const host = monsterOuter || $('#mainpane') || document.body;    const side = elt('div', {      class: monsterOuter        ? 'hvmepp-side-fallback'        : 'hvmepp-side-fallback hvmepp-side-fallback-static',    });    if (monsterOuter) {      monsterOuter.classList.add('hvmepp-ml-host-fallback');    } else if (host && getComputedStyle(host).position === 'static') {      host.style.position = 'relative';    }    host.appendChild(side);    if (monsterOuter) {      requestAnimationFrame(() => clampFallbackSide(side));      window.addEventListener('resize', () => clampFallbackSide(side));    }    return side;  }  function clampFallbackSide(side) {    if (getComputedStyle(side).position !== 'absolute') {      side.style.left = '';      return;    }    side.style.left = '';    const rect = side.getBoundingClientRect();    const min = 8;    const max = window.innerWidth - 8;    let left = parseFloat(getComputedStyle(side).left) || 0;    if (rect.left < min) {      left += min - rect.left;    } else if (rect.right > max) {      left -= rect.right - max;    } else {      return;    }    side.style.left = `${Math.round(left)}px`;  }  function mountEntryButton(createFallback = false) {    if ($('#hvmepp-entry')) return true;    const side = getOrCreateEntrySide(createFallback);    if (!side) return false;    const btn = createEntryButton();    const plcButton = findPowerLevelCalculatorButton(side);    if (plcButton) {      plcButton.insertAdjacentElement('afterend', btn);    } else {      side.appendChild(btn);    }    return true;  }  function scheduleEntryButton() {    let fallbackTimer = null;    let observer = null;    function cleanup() {      if (fallbackTimer) clearTimeout(fallbackTimer);      if (observer) observer.disconnect();    }    if (mountEntryButton()) return;    observer = new MutationObserver(() => {      if (mountEntryButton()) {        cleanup();      }    });    observer.observe(document.body, { childList: true, subtree: true });    fallbackTimer = setTimeout(() => {      mountEntryButton(true);      cleanup();    }, 3000);  }  GM_addStyle(`    #hvmepp-entry {      cursor: pointer;      display: block;      box-sizing: border-box;    }    .hvmepp-ml-host-fallback {      margin-left: 150px;      position: relative;    }    .hvmepp-side-fallback {      position: absolute;      top: 38px;      left: -110px;      width: 100px;      display: flex;      flex-direction: column;      z-index: 10;    }    .hvmepp-side-fallback input {      margin: 3px 0;      padding: 1px;      white-space: normal;    }    .hvmepp-side-fallback-static {      position: static;      width: 100px;      margin: 4px 0;    }    #hvmepp-overlay {      position: fixed;      inset: 0;      z-index: 9999;      background: rgba(0,0,0,.45);      display: flex;      align-items: flex-start;      justify-content: center;      box-sizing: border-box;      padding: 8px;      color: #222;    }    #hvmepp-overlay.hvmepp-hidden {      display: none;    }    #hvmepp-panel {      width: min(1120px, calc(100vw - 16px));      max-height: calc(100vh - 16px);      overflow: auto;      overflow-anchor: none;      background: #EDEBDF;      border: 1px solid #5C0D11;      padding: 8px;      box-shadow: 0 6px 20px rgba(0,0,0,.35);      font: 9pt/1.25 Arial, sans-serif;      text-align: left;      box-sizing: border-box;    }    .hvmepp-lang-switch {      display: flex;      justify-content: center;      align-items: center;      gap: 6px;      margin: 0 32px 5px;    }    .hvmepp-lang-switch button {      min-width: 76px;      padding: 2px 8px;      border: 1px solid #8f806c;      background: #f7f1d9;      cursor: pointer;      font: inherit;    }    .hvmepp-lang-switch button.hvmepp-lang-active {      border-color: #5C0D11;      background: #edb;      font-weight: bold;    }    .hvmepp-title {      display: flex;      justify-content: space-between;      align-items: center;      font-weight: bold;      font-size: 11pt;      margin-bottom: 4px;    }    .hvmepp-title-actions {      display: flex;      align-items: center;      gap: 8px;    }    .hvmepp-easter-egg {      padding: 0;      border: 0;      background: none;      color: #7a5a3a;      cursor: pointer;      font-family: inherit;      font-size: 9pt;      font-weight: normal;      white-space: nowrap;    }    .hvmepp-close {      width: 24px;      height: 22px;      cursor: pointer;    }    .hvmepp-live,    .hvmepp-status {      margin: 4px 0;      padding: 4px 6px;      background: #fff8;      border: 1px solid #b9aa99;    }    .hvmepp-controls {      display: flex;      flex-wrap: wrap;      gap: 4px 8px;      align-items: center;      margin: 5px 0;    }    .hvmepp-controls input,    .hvmepp-controls select {      font-size: 9pt;    }    .hvmepp-controls button {      cursor: pointer;      font: inherit;      padding: 2px 6px;    }    .hvmepp-card {      border: 1px solid #b9aa99;      background: #fff8;      padding: 5px;    }    .hvmepp-card h3 {      margin: 0 0 4px;      font-size: 10pt;    }    .hvmepp-monster-select-card,    .hvmepp-crystal-card {      margin: 6px 0;    }    .hvmepp-monster-checklist {      display: grid;      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));      gap: 2px 8px;      max-height: 170px;      overflow: auto;      padding: 4px;      border: 1px solid #b9aa99;      background: #fff8;    }    .hvmepp-monster-checklist label {      display: flex;      align-items: center;      gap: 4px;      min-width: 0;      white-space: nowrap;    }    .hvmepp-monster-checklist label span {      overflow: hidden;      text-overflow: ellipsis;    }    .hvmepp-table {      width: 100%;      border-collapse: collapse;      margin: 4px 0;      table-layout: fixed;      font-size: 9pt;    }    .hvmepp-table th,    .hvmepp-table td {      border: 1px solid #b9aa99;      padding: 2px 3px;      text-align: center;      word-break: keep-all;    }    .hvmepp-table th {      background: #edb;    }    .hvmepp-table input {      width: 58px;      text-align: right;      box-sizing: border-box;    }    .hvmepp-total {      margin: 5px 0;      padding: 5px 6px;      border: 1px solid #b9aa99;      background: #fff;      line-height: 1.4;      font-weight: bold;    }    .hvmepp-monster-result {      margin: 5px 0;      border: 1px solid #b9aa99;      background: #fff8;    }    .hvmepp-monster-result > summary {      padding: 5px 6px;      cursor: pointer;      font-weight: bold;      background: #edb;    }    .hvmepp-monster-result > .hvmepp-table,    .hvmepp-monster-result > .hvmepp-alert {      width: calc(100% - 8px);      margin: 4px;    }    .hvmepp-crystal-table td:first-child,    .hvmepp-crystal-table th:first-child {      width: 230px;      text-align: left;    }    .hvmepp-crystal-table td:last-child,    .hvmepp-crystal-table th:last-child {      width: 105px;    }    .hvmepp-crystal-table td:last-child input {      width: 96px;    }    .hvmepp-crystal-price-controls {      margin: 2px 0 4px;    }    .hvmepp-estimate-summary {      margin: 4px 0;      padding: 5px 6px;      border: 1px solid #8f806c;      background: #fffbe8;      line-height: 1.4;      font-weight: bold;    }    .hvmepp-shortage {      color: #b00020;      background: #fff2f2;      font-weight: bold;    }    .hvmepp-good {      color: #006400;      font-weight: bold;    }    .hvmepp-alert {      margin: 5px 0;      padding: 5px 6px;      border: 1px solid #b00020;      color: #b00020;      background: #fff2f2;      font-weight: bold;    }    .hvmepp-upgraded {      background: #f4fff4;    }    @media (max-width: 900px) {      #hvmepp-panel {        width: calc(100vw - 10px);        max-height: calc(100vh - 10px);        padding: 6px;      }      .hvmepp-lang-switch {        margin: 0 28px 4px;        gap: 4px;      }      .hvmepp-lang-switch button {        min-width: 68px;        padding: 2px 4px;      }      .hvmepp-table input {        width: 50px;      }      .hvmepp-side-fallback {        position: static;        margin: 4px 0;      }      .hvmepp-ml-host-fallback {        margin-left: 0;      }    }  `);  scheduleEntryButton();})();