Wod增强

提供如下功能:1.自动荣誉投票 2.自动减少地城探索时间 3.链接替换为新窗口打开 4.登录失效自动返回登录页面 5.显示技能类别提示

// ==UserScript==
// @name         Wod增强
// @namespace    https://github.com/knight000/Wod_Script
// @description  提供如下功能:1.自动荣誉投票 2.自动减少地城探索时间 3.链接替换为新窗口打开 4.登录失效自动返回登录页面 5.显示技能类别提示
// @author       knight000
// @match        http*://*.world-of-dungeons.org/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/plugin/customParseFormat.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// @icon         http://info.world-of-dungeons.org/wod/css/WOD.gif
// @require      https://code.jquery.com/jquery-3.3.1.min.js
// @grant        GM_addStyle
// @modifier     Christophero
// @version      2023.12.21.1
// ==/UserScript==

const WOD_ENHANCE_CONFIG = "WOD_ENHANCE_CONFIG";
let funArr = [];
let $panelBody = null;
const baseParams = [
  "session_hero_id",
  "wod_post_id",
  "wod_post_world",
  "klasse_id",
  "klasse_name",
  "rasse_id",
  "rasse_name",
  "gruppe_id",
  "gruppe_name",
  "clan_id",
  "clan_name",
  "stufe",
  "heldenname",
  "spielername",
];
const attrEnMap = {
  力量: "st",
  体质: "ko",
  智力: "in",
  灵巧: "ge",
  魅力: "ch",
  敏捷: "sn",
  感知: "wa",
  意志: "wi",
};
let globalSteps = 0;
let globalCurSteps = 0;

(function () {
  "use strict";
  // 相关模块,注释掉来取消使用
  addControlPanel();
  funControl("autoVote", "自动投票", autoVote, false);
  funControl("autoReduce", "加速探索", autoReduce, true);
  funControl("replaceLink", "链接新窗口", replaceLink, true);
  funControl("autoGotoLogin", "自动跳转登录", autoGotoLogin, true);
  funControl("showSkillTypeTips", "技能类别提示", showSkillTypeTips, true);
  funControl("forceDisplayTip", "手机强制提示", forceDisplayTip, true);
  funControl("chaseWarning", "反抗军追击警告", chaseWarning, true);
  funControl("easySelling", "出售增强", easySelling, true);
  funControl("quickViewDrop", "地城掉落", quickViewDrop, true);
  funControl("styleEnhance", "样式优化", styleEnhance, true);
  funControl("calcNpcPrice", "交易物品总N价", calcNpcPrice, true);
  funControl("resetSkillPoint", "技能重置", resetSkillPoint, false);
  funControl("refreshSkill", "刷新技能数值", refreshSkill, false);
  funControl("smartSelect", "批量框选", smartSelect, false);
  funControl("cancelDungeon", "取消探险", cancelDungeon, true);
  funControl("battlePreCheck", "耗材检查", battlePreCheck, true);
  funControl("multiTombola", "彩票十连", multiTombola, true);
  funControl("sibebarDungeon", "侧边选地城", sibebarDungeon, true);
  funControl("easyAttrVal", "简易属性提升", easyAttrVal, false);
  funControl("easyChangeSkill", "简易技能等级", easyChangeSkill, false);
  funControl("importHeroTemplate", "导入英雄模版", importHeroTemplate, false);
  funControl("batchDelProfile", "批量删除设置", batchDelProfile, true);
  funControl("batchChangeOwner", "物品归属变更", batchChangeOwner, true);
  funControl("autoCleanWarehouse", "自动清仓", autoCleanWarehouse, false);
  funControl("viewFullScreen", "主体切换全屏", viewFullScreen, false);
})();

function addControlPanel() {
  const $main = $(
    `
    <br>
    <div class="spoilerbox">
      <input type="button" class="spoilerbutton" value="+" onclick="this.value=this.value=='+'?'-':'+';">
      <span class="spoilerheader"><b>Wod增强控制面板</b></span>
      <div class="spoiler">
        <div>
          <button id="saveEnhanceConfig" class="button clickable" title="仅启用勾选功能">仅启用勾选功能</button><br>
        </div>
      </div>
    </div>`
  );
  $("#gadgettable-left").append($main);
  try {
    $panelBody = $main.find(".spoiler>div");
  } catch (error) {
    console.log(error);
  }

  $("#saveEnhanceConfig").click(function () {
    if (confirm("是否仅启用当前勾选功能?")) {
      const config = loadConfig();
      $panelBody.find("input:checkbox").each((i, e) => {
        config[$(e).attr("name")] = $(e).is(":checked");
      });
      saveConfig(config);
      alert("保存完毕");
    }
  });
}

function funControl(name, label, fun, defStatus) {
  let config = loadConfig();
  if (!config.hasOwnProperty(name)) {
    config[name] = defStatus;
    saveConfig(config);
  }
  const $chk = $(`<input type="checkbox" name="${name}">`);
  $chk.prop("checked", config[name]);
  $panelBody.append($chk).append(label).append("<br>");
  if (config[name]) {
    fun();
  } else {
    console.log(label + " 功能未启用");
  }
}

function loadConfig() {
  let configJson = localStorage.getItem(WOD_ENHANCE_CONFIG);
  try {
    return JSON.parse(configJson) || {};
  } catch (ex) {
    return {};
  }
}

function saveConfig(config) {
  localStorage.setItem(WOD_ENHANCE_CONFIG, JSON.stringify(config));
}

/**
 * @function 自动投票模块
 */
function autoVote() {
  const $voteBanner = $(".vote.banner > a");
  const $voteReward = $(".vote.reward");
  // 如果有$(".vote.banner")就表明是投票页面进行操作
  if ($voteBanner.length != 0) {
    // 移除链接,这样就不会弹出窗口了
    $voteBanner.removeAttr("href");
    for (let i = 0; i < $voteBanner.length; i++) {
      console.log("[Wod增强]正在检测投票链接" + i);
      let $voteSpan;
      try {
        // 把投票链接后的说明提取出来,以此判定是否已投票,正则把多余的"去除
        $voteSpan = $voteReward[i]
          .getElementsByTagName("span")[0]
          .innerHTML.replace(/"/g, "");
      } catch (ex) {
        continue;
      }
      // 如果是5或者3荣誉就进行点击
      if (($voteSpan && $voteSpan[0] == "5") || $voteSpan[0] == "3") {
        $voteBanner[i].click();
        break;
      }
    }
    console.log("[Wod增强]全部投票完毕");
  } else {
    // 非投票页面检测未投票就打开投票窗口,如果默认设置拦截弹出窗口的话就打不开了
    if ($("center:contains('点链接获5')").length != 0) {
      window.open(
        $("a[href^='/wod/spiel/rewards/vote.php?']:last").attr("href")
      );
      console.log("[Wod增强]非投票页面,未投票,已跳转");
    } else {
      console.log("[Wod增强]非投票页面,已投票");
    }
  }
}

/**
 * @function 自动减少地域探索时间模块
 */
function autoReduce() {
  let $reduce = $('[name="reduce_dungeon_time"]');
  // 有这个按钮就按一下
  if ($reduce.length) {
    $reduce[0].click();
    console.log("[Wod增强]已自动减少地域时间");
  } else {
    console.log("[Wod增强]无需自动减少地域时间");
  }
}

/**
 * @function 链接替换为新窗口打开模块
 */
function replaceLink() {
  // 团队说明的快速链接
  $("div.gadget.groupmsg.lang-cn a[target!='_blank']").attr("target", "_blank");
  // 物品链接
  $("a.item_unique[target!='_blank']").attr("target", "_blank");
  // 拍卖详情
  $("a:contains('详情')[target!='_blank']").attr("target", "_blank");
  // 英雄详情
  $("a.hero_active,a.hero_inactive").attr("target", "_blank");
  console.log("[Wod增强]链接替换为新窗口打开");
}

/**
 * @function 自动跳转登录页面
 */
function autoGotoLogin() {
  if (document.title && document.title.endsWith(" OR cookies disabled.")) {
    window.location = location.origin;
  }
}

/**
 * @function 显示技能类别提示
 */
function showSkillTypeTips() {
  const genAuctionAnchor = (skillName) => {
    let skillDisplayUrl = `/wod/spiel/hero/skill.php?name=${skillName}&IS_POPUP=1`;
    return `<a href=\\"${skillDisplayUrl}\\" target=\\"_blank\\" onclick=\\"return wo(\\'${skillDisplayUrl}\\');\\">${skillName}</a>`;
  };

  // 添加全局技能类别查看
  function renderSkillTips(skillMap) {
    let $tds = $('td.content_table:contains("类别的所有技能")');
    $tds.each(function (i) {
      const type = $(this).find("i").text();
      const skillList = skillMap[type];
      if (!skillList) {
        $(this).attr(
          "onmouseover",
          `wodToolTip(this, "<b>目前尚未收录 ${type} 类别的技能</b>")`
        );
        return;
      }
      let str = "<b>" + type + " 类别包含以下技能</b><br>";
      str += skillList
        .map((skillName) => genAuctionAnchor(skillName))
        .join("<br>");
      $(this).attr("onmouseover", `wodToolTip(this, "${str}")`);
      $(this).click(function () {
        window.open(
          `https://www.christophero.xyz/skill?type=${encodeURIComponent(type)}`,
          "_blank"
        );
      });
    });
  }
  const SKILL_MAP_KEY = "skillMap";
  const SKILL_MAP_VERSION = "skillMapVersion";
  let skillMapStr = localStorage.getItem(SKILL_MAP_KEY);
  let skillMapVer = localStorage.getItem(SKILL_MAP_VERSION);
  let skillMap = {};
  let nowTime = new Date().getTime();
  if (
    skillMapStr &&
    skillMapVer &&
    nowTime - skillMapVer < 7 * 24 * 60 * 60 * 1000
  ) {
    skillMap = JSON.parse(skillMapStr);
    renderSkillTips(skillMap);
  } else {
    // 获取技能信息
    fetch("https://www.christophero.xyz/wod/skill/all", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
    })
      .then((response) => {
        return response.json();
      })
      .then((res) => {
        if (!(res && res.code === 200)) {
          return;
        }
        const data = res.data;
        for (let skill of data) {
          if (!skillMap.hasOwnProperty(skill.type)) {
            skillMap[skill.type] = [skill.name];
          } else {
            skillMap[skill.type].push(skill.name);
          }
        }
        localStorage.setItem(SKILL_MAP_KEY, JSON.stringify(skillMap));
        localStorage.setItem(SKILL_MAP_VERSION, nowTime);
        renderSkillTips(skillMap);
      });
  }
}

/**
 *
 * @returns 返回是否是移动端
 */
function isMobile() {
  const userAgentInfo = navigator.userAgent;

  const mobileAgents = [
    "Android",
    "iPhone",
    "SymbianOS",
    "Windows Phone",
    "iPad",
    "iPod",
  ];

  let mobileFlag = false;

  //根据userAgent判断是否是手机
  for (let v = 0; v < mobileAgents.length; v++) {
    if (userAgentInfo.indexOf(mobileAgents[v]) > 0) {
      mobileFlag = true;
      break;
    }
  }

  const screen_width = window.screen.width;
  const screen_height = window.screen.height;

  //根据屏幕分辨率判断是否是手机
  if (screen_width < 500 && screen_height < 800) {
    mobileFlag = true;
  }

  return mobileFlag;
}

/**
 * 强制显示提示,重写了部分显示代码
 */
function forceDisplayTip() {
  if (isMobile()) {
    if (typeof GM_addStyle == "undefined") {
      function GM_addStyle(styles) {
        var S = document.createElement("style");
        S.type = "text/css";
        var T = "" + styles + "";
        T = document.createTextNode(T);
        S.appendChild(T);
        document.body.appendChild(S);
        return;
      }
    }

    GM_addStyle(`
        .tooltip {
          min-width: 150px;
          min-height: 90px;
        }
      `);

    $('[src$="images/icons/inf.gif"]').click(function () {
      return false;
    });

    window._wodTooltipSetSize = function (content) {
      var offsetX, offsetY, screenWidth, screenHeight;

      if (self.pageYOffset) {
        offsetX = self.pageXOffset;
        offsetY = self.pageYOffset;
      } else if (
        document.documentElement &&
        document.documentElement.scrollTop
      ) {
        offsetX = document.documentElement.scrollLeft;
        offsetY = document.documentElement.scrollTop;
      } else if (document.body) {
        offsetX = document.body.scrollLeft;
        offsetY = document.body.scrollTop;
      }

      if (self.innerHeight) {
        screenWidth = self.innerWidth;
        screenHeight = self.innerHeight;
      } else if (
        document.documentElement &&
        document.documentElement.clientHeight
      ) {
        screenWidth = document.documentElement.clientWidth;
        screenHeight = document.documentElement.clientHeight;
      } else if (document.body) {
        screenWidth = document.body.clientWidth;
        screenHeight = document.body.clientHeight;
      }

      if (typeof screenWidth == "undefined" || screenWidth <= 10)
        screenWidth = 800;

      if (typeof screenHeight == "undefined" || screenHeight <= 10)
        screenHeight = 600;

      var top;
      var bottom;
      var left;
      var right;

      if (
        wodToolTipCurrentMouseY + wodToolTip_OffY + content.offsetHeight >
        screenHeight - wodToolTip_ScrollBarWidth
      ) {
        bottom = wodToolTip_OffY;

        if (
          content.offsetHeight <
          screenHeight - 2 * wodToolTip_ScrollBarWidth
        ) {
          top =
            screenHeight -
            content.offsetHeight -
            wodToolTip_ScrollBarWidth -
            wodToolTip_OffY;
        } else top = wodToolTip_OffY;
      } else {
        top = wodToolTipCurrentMouseY + wodToolTip_OffY;
        bottom =
          screenHeight -
          (wodToolTipCurrentMouseY +
            wodToolTip_OffY +
            content.offsetHeight +
            wodToolTip_ScrollBarWidth);
      }

      if (
        wodToolTipCurrentMouseX + wodToolTip_OffX + content.offsetWidth >
        screenWidth - wodToolTip_ScrollBarWidth
      ) {
        right = wodToolTip_OffX;

        if (content.offsetWidth < screenWidth - 2 * wodToolTip_ScrollBarWidth) {
          left =
            screenWidth -
            content.offsetWidth -
            wodToolTip_ScrollBarWidth -
            wodToolTip_OffX;
        } else left = wodToolTip_OffX;
      } else {
        left = wodToolTipCurrentMouseX + wodToolTip_OffX;

        right =
          screenWidth -
          (wodToolTipCurrentMouseX +
            wodToolTip_OffX +
            content.offsetWidth +
            wodToolTip_ScrollBarWidth);
      }

      var is_absolut_position = isSafari2() || isIE6() || isChrome();
      if (is_absolut_position) {
        top += offsetY;
        bottom -= offsetY;
        left += offsetX;
        right -= offsetX;
      }

      content.style.top = top + "px";
      content.style.left = left + "px";
    };
  }
}

/**
 * 反抗军追击警告
 */
function chaseWarning() {
  const $dungeons = $(
    '.gadget.nextdungeon a.menu, form[action^="/wod/spiel/dungeon/dungeon.php"] table.top .content_table>tbody>tr:visible>td:first-child'
  );
  if (!$dungeons.length) return;
  const day = new Date().getDay();
  $dungeons.each(function (i, e) {
    const $dungeon = $(e);
    const dungeonName = $dungeon.text().trim();
    const chuyunfeiHauntDungeons = [
      "发狂的炼金术士",
      "格兰斯凯巴的生命之水",
      "银刺",
      "竞技比赛的前夜",
    ];
    const guangtoHauntDungeons = ["尤里佛的一天", "冰冷大地"];
    const liyunlongHauntDungeons = [
      "阴影之心",
      "图书馆的一夜",
      "冰冷大地",
      "血腥之手",
      "尤里佛的一天",
      "一场公平的决斗",
      "笼罩缇琳的阴云",
      "父债子偿",
    ];

    if (
      chuyunfeiHauntDungeons.includes(dungeonName) ||
      guangtoHauntDungeons.includes(dungeonName) ||
      liyunlongHauntDungeons.includes(dungeonName)
    ) {
      $dungeon.css({ color: "#FF8C27" });
      let tip = "";
      if ("冰冷大地" == dungeonName) {
        if ([0, 1, 3, 5].includes(day)) {
          tip += "星期一三五七该地城3层房间2将与<b>强大的阿瑞露</b>为敌!<br>";
        } else {
          tip +=
            "星期二四六该地城3层房间2将与阿瑞露一起对抗<b>顺劈骑士团</b>!<br>";
        }
      }
      tip += "携带反抗军装备时,<br>";
      if (chuyunfeiHauntDungeons.includes(dungeonName)) {
        tip += "该地城有<b>林云</b>出没<br>";
      }
      if (guangtoHauntDungeons.includes(dungeonName)) {
        tip += "该地城有<b>光头</b>出没<br>";
      }
      // 周日圣女放假
      if (liyunlongHauntDungeons.includes(dungeonName)) {
        tip += "除周日外该地城有<b>圣女</b>出没<br>";
      }

      tip +=
        '查看<a href="https://www.christophero.xyz/itemList?categoryName=%E5%8D%A1%E6%B4%9B%E6%96%AF%E5%8F%8D%E6%8A%97%E5%86%9B%E6%94%B9%E8%89%AF%E5%85%B5%E5%99%A8" target="_blank" >反抗军武器</a><br>';
      tip +=
        '重点关注: 【<a href="/wod/spiel/hero/item.php?name=%E7%81%B0%E6%9A%AE%E4%B9%8B%E5%BD%A2&is_popup=1" target="_blank" >灰暮之形</a>】【<a href="/wod/spiel/hero/item.php?name=%E6%96%A9%E6%96%AD%E5%9B%A0%E6%9E%9C%E7%9A%84%E5%87%8C%E5%86%BD%E6%B3%95%E5%88%83&is_popup=1" target="_blank" >斩断因果的凌冽法刃</a>】';
      $dungeon.mouseenter(function () {
        wodToolTip(e, tip);
      });
      // $dungeon.parent().attr("onmouseover", `wodToolTip(this, "${tip}")`);
    }
  });
}

/**
 * 增强出售操作,更方便移除物品
 * @returns
 */
function easySelling() {
  let pathName = location.pathname;
  if (pathName !== "/wod/spiel/hero/items.php") {
    return;
  }
  if (
    !(
      $('h1:contains("请确认")').length ||
      $('p:contains("物品现在将以NPC价出售")').length
    )
  ) {
    return;
  }
  const $tip = $('p:contains("物品现在将以NPC价出售")');
  $('p:contains("物品现在将以NPC价出售")~table').on(
    "click",
    "td>img:first-child",
    function () {
      const $a = $(this).nextAll("a:first");
      const itemName = $a.text();
      const params = $a.attr("href").split("?")[1];
      const instanceId = new URLSearchParams(params).get("item_instance_id");
      const $sellids = $('input[name="sellids"]');
      if ($a.css("text-decoration-line") != "line-through") {
        if (confirm(`是否将物品[${itemName}]从出售列表移除`)) {
          let idArr = $sellids
            .val()
            .split(",")
            .filter((id) => id);
          idArr = idArr.filter((id) => id != instanceId);
          $sellids.val(idArr.join(","));
          $a.css({ "text-decoration-line": "line-through" });
          $tip.text(`以下${idArr.length}件物品现在将以NPC价出售:`);
        }
      } else if ($a.css("text-decoration-line") != "none") {
        if (confirm(`是否将物品[${itemName}]加入出售列表`)) {
          let idArr = $sellids
            .val()
            .split(",")
            .filter((id) => id);
          idArr.push(instanceId);
          $sellids.val(idArr.join(","));
          $a.css({ "text-decoration-line": "none" });
          $tip.text(`以下${idArr.length}件物品现在将以NPC价出售:`);
        }
      }
    }
  );
}

/**
 * 查看地城掉落
 * @returns
 */
function quickViewDrop() {
  // 右下角地城添加快速链接
  const $quickViewDropBtn = $(
    '<button type="button" class="button clickable" title="查看地城掉落">查看地城掉落</button>'
  );
  const $curConfigAnchor = $(
    "div.nextdungeon div[id^='CombatDungeonConfigSelector'], form[action^='/wod/spiel/quests/quests.php'] div[id^='CombatDungeonConfigSelector'], form[action^='/wod/spiel/dungeon/dungeon.php'] div[id^='CombatDungeonConfigSelector']"
  );

  $quickViewDropBtn.click(function () {
    let url = "https://www.christophero.xyz/groupDungeon/";
    let curDungeonName = "";
    const $that = $(this);
    if ($that.parents("div.nextdungeon").length) {
      // 侧边
      curDungeonName = $that
        .parent()
        .find(
          'a[href^="/wod/spiel/dungeon/dungeon.php"],a[href^="/wod/spiel/quests/quests.php"]'
        )
        .text()
        .trim();
    } else if ($that.parents("tr[class^='row']").length) {
      // 地城列表
      curDungeonName = $that
        .parents("tr[class^='row']:first")
        .find("td:first")
        .text()
        .trim();
    } else if (
      $('h1:contains("任务: ")').length &&
      $that.parents('form[action^="/wod/spiel/quests/quests.php"]')
    ) {
      let res = null;
      // 任务已经在执行了
      if ($("#progressBar1_txt1").length) {
        const reg = /^地城:\s*(.+)$/g;
        curDungeonName = $that
          .parents("form:first")
          .find("h2:first")
          .text()
          .trim();
        res = reg.exec(curDungeonName);
      } else {
        // 任务
        const reg = /^\d+\.\s*(.+)$/g;
        curDungeonName = $that
          .parents("tr[class^='row']:first")
          .find("h3:first")
          .text()
          .trim();
        res = reg.exec(curDungeonName);
      }
      if (res) {
        curDungeonName = res[1];
      }
    } else if (
      $that.parents("table:first").prev(".progressBar_container").length
    ) {
      curDungeonName = $that
        .parents("table:first")
        .prevAll("p:first")
        .find("b:nth-child(2)")
        .text()
        .trim();
    }
    url += encodeURIComponent(curDungeonName);
    url += `?groupId=${$('input[name="gruppe_id"]').val()}`;
    window.open(url, "_blank");
  });
  $curConfigAnchor.after($quickViewDropBtn);
}

function calcNpcPrice() {
  if (
    ![
      "/wod/spiel/trade/exchange_details.php",
      "/wod/module/search/select_item.php",
    ].includes(location.pathname)
  )
    return;
  const $tradeTds = $('td[valign="top"][align="center"]:not([colspan])');
  if (!$tradeTds.length) return;
  $tradeTds.each((i, e) => {
    let gold = parseInt($(e).find('input[name="gold"]').val());
    if (isNaN(gold)) {
      gold = parseInt(e.childNodes[0].textContent.trim());
    }
    const $tbody = $(e).find("tbody");
    const $trs = $tbody.find("tr");
    let ntotal = 0;

    let promiseArr = [];
    for (const tr of Array.from($trs)) {
      let price = 0;
      if (i == 1) {
        promiseArr.push(
          fetchNpcPrice(location.origin + $(tr).find("a").attr("href"), tr)
        );
      } else {
        price = parseInt($(tr).find("td:eq(3)").text().trim());
      }
      if (price) ntotal += price;
    }
    Promise.all(promiseArr).then((priceArr) => {
      ntotal += priceArr.reduce((t, c) => t + c, 0);
      const total = ntotal + gold;
      $tbody.append(
        `<tr><td align="right"></td><td valign="top" nowrap>小计</td><td valign="top" nowrap></td><td nowrap>${ntotal} <img alt="" border="0" src="/wod/css//skins/skin-4/images/icons/lang/cn/gold.gif" title="金币"></td></tr>
      <tr><td align="right"></td><td valign="top" nowrap>合计</td><td valign="top" nowrap></td><td nowrap>${total} <img alt="" border="0" src="/wod/css//skins/skin-4/images/icons/lang/cn/gold.gif" title="金币"></td></tr>`
      );
    });
  });
}

function fetchNpcPrice(url, tr) {
  return fetch(url, {
    headers: {
      accept:
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
      "content-type": "application/x-www-form-urlencoded",
    },
    method: "GET",
  }).then(async (response) => {
    let price = 0;
    if (response.status === 200) {
      const text = await response.text();
      const $priceTd = $(text).find(
        '#details td:contains("NPC价/全新时NPC价") +td'
      );
      price = parseInt($priceTd[0].childNodes[0].textContent.trim());
      if (isNaN(price)) price = 0;
    }
    $(tr).append(
      `<td></td><td>${price} <img alt="" border="0" src="/wod/css//skins/skin-4/images/icons/lang/cn/gold.gif" title="金币"></td>`
    );
    return new Promise((resolve, reject) => {
      resolve(price);
    });
  });
}

function resetSkillPoint() {
  if (location.pathname !== "/wod/spiel/hero/skills.php") return;
  $('<button class="button clickable" type="button">技能重置</button>')
    .insertBefore('input[name="hide_all"],input[name="show_all"]')
    .click(function () {
      $('input[name^="undo["][src$="undo_steigern_enabled.gif"]').each(
        (i, e) => {
          let currentCnt = parseInt(
            $(e)
              .parents("tr:first")
              .find('div[id^="skill_rang_"]')
              .text()
              .trim()
          );
          let skillId = $(e)
            .prop("name")
            .match(/undo\[(\d+)\]/)[1];
          const deltaRang = -1 * currentCnt;
          if (isAbleToChange(skillId, deltaRang)) {
            advanceSkill(skillId, deltaRang);
            hero["stufe"] = engineLevelGainedForEps(
              hero["fc_ep"],
              hero["fc_ep"] - hero["ep"]
            );
            if (hero["stufe"] < hero["stufe_orig"])
              hero["stufe"] = hero["stufe_orig"];
            updateSkills(skillId);
          }
        }
      );
      alert("技能已重置");
    });
}

function insertCss(select, styles) {
  if (document.styleSheets.length === 0) {
    //如果没有style标签,则创建一个style标签
    var style = document.createElement("style");
    document.head.appendChild(style);
  }
  var styleSheet = document.styleSheets[document.styleSheets.length - 1]; //如果有style 标签.则插入到最后一个style标签中
  var str = select + " {"; //插入的内容必须是字符串,所以得把obj转化为字符串
  for (var prop in styles) {
    str +=
      prop.replace(/([A-Z])/g, function (item) {
        //使用正则把大写字母替换成 '-小写字母'
        return "-" + item.toLowerCase();
      }) +
      ":" +
      styles[prop] +
      ";";
  }
  str += "}";
  styleSheet.insertRule(str, styleSheet.cssRules.length); //插入样式到最后一个style标签中的最后面
}

function styleEnhance() {
  insertCss(".orders_top_row, .orders_top_row > *", {
    "vertical-align": "unset",
  });
}

function refreshSkill() {
  if (
    location.pathname !== "/wod/spiel/hero/skills.php" &&
    !$('h1:contains("装备着的物品")').length
  )
    return;

  const $btn = $(
    '<button class="button clickable" type="button">刷新套装加成</button>'
  ).click(function () {
    let $btn = $(this);
    let baseUrl = location.origin + "/wod/spiel/hero/attributes.php?is_popup=1";
    const searchParams = getBaseSearchParams();
    searchParams.set("do_reset", "");
    searchParams.set("levelup_warned", "0");
    searchParams.set("geschlecht", "f");
    searchParams.set("change_gender", "更改");
    searchParams.set("amor_details", "关闭");
    $btn.text("处理中请稍候...");
    const heroId = searchParams.get("session_hero_id");
    baseUrl += "&session_hero_id=" + heroId;
    fetch(baseUrl, {
      headers: {
        accept:
          "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
        "content-type": "application/x-www-form-urlencoded",
      },
      method: "POST",
      body: searchParams.toString(),
    }).then((resp) => {
      $btn.text("处理完成刷新页面");
      if (location.pathname == "/wod/spiel/hero/items.php") {
        location.replace(
          location.pathname +
            "?menukey=hero_gear&view=gear&session_hero_id=" +
            heroId
        );
      } else {
        location.replace(location.href);
      }
    });
  });
  if (location.pathname === "/wod/spiel/hero/skills.php") {
    $btn.insertBefore('input[name="hide_all"],input[name="show_all"]');
  } else {
    $btn.insertAfter('input[name="ok"][value="应用改动"]');
  }
}

function smartSelect() {
  // is dragging active
  let isDragging = false;

  // the rectangle to show while dragging
  var rectangle = undefined;

  // rectangle object specify the selected range
  let rec = {
    startX: 0,
    startY: 0,
    endX: 0,
    endY: 0,
  };

  // rectangle object to draw the box upon dragging
  let recDraw = {
    startX: 0,
    startY: 0,
    endX: 0,
    endY: 0,
    style:
      "position:absolute;border: 2px dotted black;background: white; opacity:0.5;z-index:200000000;font-size: 0px",
  };

  document.addEventListener("mousedown", (e) => {
    // start dragging-mode if shift key is pressed
    if (e.shiftKey || e.altKey) {
      isDragging = true;
      rec.startX = rec.endX = recDraw.startX = recDraw.endX = e.pageX;
      rec.startY = rec.endY = recDraw.startY = recDraw.endY = e.pageY;
      e.preventDefault();
    }
  });

  document.addEventListener("mousemove", (e) => {
    // show dragging rectangle is drag mode is activated
    if ((e.shiftKey || e.altKey) && isDragging) {
      rectangle.style.visibility = "visible";
      e.preventDefault();

      // Take care that the rectangle is displayed also with negative x or y values
      if (e.pageX >= rec.startX && e.pageY >= rec.startY) {
        //console.log("right bottom")
        recDraw.endX = e.pageX;
        recDraw.endY = e.pageY;
      } else if (e.pageX < rec.startX && e.pageY >= rec.startY) {
        //console.log("left bottom - negative X")
        recDraw.startX = e.pageX;
        recDraw.endX = rec.startX;
        recDraw.endY = e.pageY;
      } else if (e.pageX >= rec.startX && e.pageY < rec.startY) {
        //console.log("right top - negative Y")
        recDraw.endY = rec.startY;
        recDraw.startY = e.pageY;
        recDraw.endX = e.pageX;
      } else if (e.pageX < rec.startX && e.pageY < rec.startY) {
        //console.log("left top - negative X + negative Y")
        recDraw.startY = e.pageY;
        recDraw.startX = e.pageX;
        recDraw.endX = rec.startX;
        recDraw.endY = rec.startY;
      }

      drawRectangle();
    }
  });

  document.addEventListener("mouseup", (e) => {
    // stop dragging mode if mouse button is released
    if (isDragging) {
      rec.endX = e.pageX;
      rec.endY = e.pageY;
      isDragging = false;
      rectangle.style.visibility = "hidden";
      if (e.shiftKey) {
        checkCheckboxes();
      } else if (e.altKey) {
        batchSelectOrEdit();
      }
    }
  });

  //////// Functions ////////

  function drawRectangle() {
    rectangle.style.left = `${recDraw.startX}px`;
    rectangle.style.top = `${recDraw.startY}px`;
    rectangle.style.width = `${recDraw.endX - recDraw.startX}px`;
    rectangle.style.height = `${recDraw.endY - recDraw.startY}px`;
  }

  function checkCheckboxes() {
    let left = rec.startX;
    let right = rec.endX;
    let top = rec.startY;
    let bottom = rec.endY;
    if (left > right) {
      [left, right] = [right, left];
    }
    if (top > bottom) {
      [top, bottom] = [bottom, top];
    }
    const checkboxes = document.querySelectorAll('input[type="checkbox"]');
    checkboxes.forEach((ele) => {
      const $ele = $(ele);
      const eleOffset = $(ele).offset();
      const eleTop = eleOffset.top;
      const eleLeft = eleOffset.left;
      const eleBottom = eleTop + $ele.height();
      const eleRight = eleLeft + $ele.width();
      const verticalCenter = (eleTop + eleBottom) / 2;
      const horizonCenter = (eleLeft + eleRight) / 2;

      // 中心点在矩形范围内就勾选
      if (
        left <= horizonCenter &&
        horizonCenter <= right &&
        top <= verticalCenter &&
        verticalCenter <= bottom
      ) {
        // toggle status of the checkbox in range
        $(ele).click();
        // ckbox.checked ? (ckbox.checked = false) : (ckbox.checked = true);
      }
    });
  }

  // 批量选择下拉框
  function batchSelectOrEdit() {
    let left = rec.startX;
    let right = rec.endX;
    let top = rec.startY;
    let bottom = rec.endY;
    if (left > right) {
      [left, right] = [right, left];
    }
    if (top > bottom) {
      [top, bottom] = [bottom, top];
    }
    const $eles = $("select:visible,input:visible");
    const opts = [];
    const boxedSels = [];
    const boxedInputs = [];
    $eles.each((i, ele) => {
      const $ele = $(ele);
      const eleOffset = $(ele).offset();
      const eleTop = eleOffset.top;
      const eleLeft = eleOffset.left;
      const eleBottom = eleTop + $ele.height();
      const eleRight = eleLeft + $ele.width();
      const verticalCenter = (eleTop + eleBottom) / 2;
      const horizonCenter = (eleLeft + eleRight) / 2;

      // 中心点在矩形范围内就选择
      if (
        left <= horizonCenter &&
        horizonCenter <= right &&
        top <= verticalCenter &&
        verticalCenter <= bottom
      ) {
        if (ele.tagName == "SELECT") {
          boxedSels.push(ele);
          // 设置选项
          $(ele)
            .find("option")
            .each((i, opt) => {
              const text = opt.textContent;
              const value = opt.value;
              if (
                opts.some((opt) => opt.text === text && opt.value === value)
              ) {
                return;
              }
              opts.push({ text, value });
            });
        } else if (ele.tagName == "INPUT") {
          boxedInputs.push(ele);
        }
      }
    });

    // 没有框选到任何下拉框则不弹出选项
    if (!boxedSels.length && !boxedInputs.length) return;

    // 展示批量选择框
    const targetId = "ajax_dialog";
    const innerHtml =
      '<select id="batchSelect" style="visibility:hidden;"></select><input type="text" id="batchInput" style="visibility:hidden;">';
    _ajaxCreateDialogDirect(
      targetId,
      innerHtml,
      boxedSels.length > boxedInputs.length
        ? "批量选择下拉框选值"
        : "批量修改输入框数值",
      AJAX_YES | AJAX_CLOSE,
      async function (event) {
        if (event == "yes") {
          if (boxedSels.length > boxedInputs.length) {
            const selectedVal = $("#batchSelect").val();
            boxedSels.forEach((select) => {
              $(select).val(selectedVal).trigger("change");
            });
          } else {
            const inputVal = $("#batchInput").val();
            boxedInputs.forEach((input) => {
              input.value = inputVal;
            });
          }
        }
        _ajaxStopWaiting(targetId);
        _ajaxCloseModalDialog(targetId);
      },
      function () {
        altDown = false;
        document.querySelector(`#${targetId}_buttons button`).innerText =
          "确定";
        if (boxedSels.length > boxedInputs.length) {
          const $batchSelect = $("#batchSelect");
          for (let opt of opts) {
            const $opt = $("<option></option>");
            $opt.attr("value", opt.value).text(opt.text);
            $batchSelect.append($opt);
          }
          $("#batchSelect").css("visibility", "visible");
        } else {
          $("#batchInput").css("visibility", "visible");
        }
      }
    );
  }

  function initCheckboxSelector() {
    // create div which is used for showing the rectangle and initialize it
    rectangle = document.createElement("div");
    rectangle.style.cssText = recDraw.style;
    document.body.appendChild(rectangle);
  }

  initCheckboxSelector();
}

function cancelDungeon() {
  const $nextDungeonDiv = $('.block_inner:contains("下一个地城:")');
  if (!$nextDungeonDiv.length) return;
  const $cancelDungeonBtn = $(
    '<button id="cancelDungeon" class="button clickable" title="取消探险">取消探险</button>'
  );
  $nextDungeonDiv.append($cancelDungeonBtn);
  $cancelDungeonBtn.click(function () {
    if (!confirm("确定要取消当前探险吗?")) return;
    let $btn = $(this);
    let baseUrl = location.origin + "/wod/spiel/dungeon/dungeon.php?is_popup=1";
    const searchParams = getBaseSearchParams();
    const groupLv = searchParams.get("stufe");
    searchParams.set("TABLE_DEFAULT_SORT_DIR", "DESC");
    searchParams.set("TABLE_DEFAULT_SORT_COL", "7");
    searchParams.set("TABLE_DEFAULT_PAGE", "1");
    searchParams.set("TABLE_DEFAULT_PSNR[1]", "20");
    searchParams.set("TABLE_DEFAULT_PSNR[2]", "20");
    searchParams.set("TABLE_DATED_SORT_DIR", "ASC");
    searchParams.set("TABLE_DATED_SORT_COL", "14");
    searchParams.set("TABLE_DATED_PAGE", "1");

    searchParams.set("dungeon_1name", "不可能存在的地城");
    searchParams.set(
      "profile_data_dungeon_1_profile_data",
      "HogNB0ny8I/FjaOg6FXrzZvwI0A1hZgI77jjRYoCRE27wTAWqILHdEzHicsmJZl6X7ZRBEIW822E5+rsUoHjcPHJM1dTqbJE0c1Ad4c6gLXcUjcGy5WB8H0lm1qobBxf"
    );
    searchParams.set(
      "callback_js_code_dungeon_1_callback_js_code",
      "9hAUpnwetF8TrxnkIxgBdD27k4isavkaEEd/PwhJTLL8yf4MvBsSYedcxPRse51t2x6Aw2bcXKHHlyCszAStN2nnub0CncNJMDrZQePru5mpXW3It99S/D+JypTOewMQ/T9+eXLlhJZ7vK+j9IAKgKVS+EFJPGzy61GBgu60fBk="
    );
    searchParams.set("dungeon_1level", "99");
    searchParams.set("dungeon_1level_to", "99");
    searchParams.set("dungeon_1level_allowed", "99");
    searchParams.set("dungeon_1level_allowed_to", "99");
    searchParams.set("dungeon_1groupLevel", groupLv);
    searchParams.set("dungeon_1profile_id", "0");
    searchParams.set("dungeon_1is_open", "1");
    searchParams.set("dungeon_2name", "不可能存在的地城");
    searchParams.set(
      "profile_data_dungeon_1_profile_data",
      "HogNB0ny8I/FjaOg6FXrzZvwI0A1hZgI77jjRYoCRE26Yi3zTNw4kxlf3EBWtEk1b6aVuW+FUuN8kSMRggg8h3JkxoL2NsUxavZXWRdyOxUUEMX5AKRE3eAUHOs1WJk3"
    );
    searchParams.set(
      "callback_js_code_dungeon_1_callback_js_code",
      "hjeqjpM+qZ3O91mfrGQpvhGIqpJEtjGwgXlmEmXfIQzBrxMpR69Guzy7+7UjXgADJMzqeXDJhCUPVMr4x2KfpOKujjttFf22twLrAnOMemRyC0FkOn1zx6YBUufUkR7Ckg4pf2y/2z74zxDx8svUS6Kwihgpc54Xeb1xFKyGmQQ="
    );
    searchParams.set("dungeon_2level", "99");
    searchParams.set("dungeon_2level_to", "99");
    searchParams.set("dungeon_2level_allowed", "99");
    searchParams.set("dungeon_2level_allowed_to", "99");
    searchParams.set("dungeon_2groupLevel", groupLv);
    searchParams.set("dungeon_2profile_id", "0");
    searchParams.set("dungeon_2is_open", "1");

    searchParams.set("unvisit", "取消探险");
    $btn.text("取消中请稍候...");
    fetch(baseUrl, {
      headers: {
        accept:
          "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
        "content-type": "application/x-www-form-urlencoded",
      },
      method: "POST",
      body: searchParams.toString(),
    }).then((resp) => {
      $btn.text("处理完成刷新页面");
      location.reload();
    });
  });
}

/**
 * 战斗耗材预检查
 * @returns
 */
function battlePreCheck() {
  const $nextDungeonDiv = $('.block_inner:contains("下一个地城:")');
  if (!$nextDungeonDiv.length) return;
  const $preCheckBtn = $(
    '<button id="battlePreCheck" class="button clickable" title="耗材检查">耗材检查</button>'
  );
  $nextDungeonDiv.append($preCheckBtn);
  $preCheckBtn.click(function () {
    let $btn = $(this);
    // 1. 获得团员列表
    let baseUrl = location.origin + "/wod/spiel/dungeon/group.php?is_popup=1";
    const searchParams = getBaseSearchParams();
    $btn.text("获取团员列表中...");
    fetch(baseUrl, {
      headers: {
        accept:
          "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
        "content-type": "application/x-www-form-urlencoded",
      },
      method: "POST",
      body: searchParams.toString(),
    })
      .then((resp) => {
        return resp.text();
      })
      .then((text) => {
        // 2. 获取所有人装备与耗材检查页面
        $btn.text("分析成员设置中...");
        $("#ajax_editor_container").show();
        $("#ajax_editor_title").append("<span>耗材检查</span>");
        $('<button class="button clickable" title="关闭">关闭</button>')
          .appendTo($("#ajax_editor_buttons"))
          .click(function () {
            $("#ajax_editor_container").hide();
            $(
              "#ajax_editor_title, #ajax_editor_content, #ajax_editor_buttons"
            ).empty();
          });
        $btn.text("耗材检查");
        const $doc = $(text);
        const $heroRows = $doc.find(
          "#smarttabs__members_inner .content_table [class^=row]"
        );
        const searchParams = new URLSearchParams();
        const wodPostId = $doc.find('input[name="wod_post_id"]').val();
        const heroId = $doc.find('input[name="sessionHeroId"]').val();
        const playerId = $doc.find('input[name="session_player_id"]').val();
        searchParams.set("wod_post_id", wodPostId);
        searchParams.set("session_hero_id", heroId);
        searchParams.set("session_player_id", playerId);
        searchParams.set("ajax_class_name", "RenderHeroItemViewer");
        searchParams.set("RenderHeroItemViewer_dialogStatus", "open");
        searchParams.set("ajax", 1);
        $heroRows.each(function () {
          const $row = $(this);
          const itemViewParams = $row
            .find("[id^=HeroItemViewer]")
            .attr("id")
            .replace("HeroItemViewer", "");
          searchParams.set("ajax_object_id", itemViewParams);
          let renderUrl = location.origin + "/wod/ajax/render.php";
          fetch(renderUrl, {
            headers: {
              accept: "*/*",
              "content-type": "application/x-www-form-urlencoded",
              "x-requested-with": "XMLHttpRequest",
            },
            method: "POST",
            body: searchParams.toString(),
          })
            .then((resp) => {
              return resp.text();
            })
            .then((text) => {
              // 3. 将所有检查结果显示在页面上
              const $render = $("<div></div>").append($(`<div>${text}</div>`));
              const $errDiv = $render.find(
                'div[id^="CombatConfigErrorViewer"]'
              );
              const $heroName = $render.find(
                'a[href^="/wod/spiel/hero/profile.php?"]'
              );
              $("#ajax_editor_content")
                .append($heroName)
                .append($errDiv)
                .append("<br>");
            });
        });
      });
  });
}

async function tombolaOnce(params, times) {
  let response = await fetch(
    `${location.origin}/wod/spiel/rewards/tombola.php?is_popup=1`,
    // `${location.origin}?is_popup=1`,
    {
      headers: {
        accept:
          "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
        "content-type": "application/x-www-form-urlencoded",
      },
      body: params.toString(),
      method: "POST",
      mode: "cors",
      credentials: "include",
    }
  );
  let text = await response.text();
  const $tombolaProgress = $("#tombolaProgress");
  $tombolaProgress.val(parseInt($tombolaProgress.val()) + 1);
  ajaxAlert(`目前进度:${$tombolaProgress.val()}/${times}`);

  const $jq = $(text);
  const $p = $jq.find(".content_block p:last");
  if ($p.length) {
    $("#tombolaDetailContainer").append($p);
  } else {
    $("#tombolaDetailContainer").append(
      "<p>没有找到获奖信息,请检查彩票是否耗尽或者当日次数已达限制</p>"
    );
  }
  return new Promise((resolve, reject) => {
    resolve(text);
  });
}

async function autoTombola(times) {
  ajaxAlert(`即将开始兑换奖券...`);
  $("#tombolaProgress").val(0);
  $("#tombolaDetailContainer").empty();
  const params = getAllHiddenParams();
  params.set("exec", "和克劳斯.钱袋兑换一张奖券");
  const promiseArr = [];
  for (let i = 0; i < times; i++) {
    promiseArr.push(tombolaOnce(params, times));
  }
  const respArr = await Promise.all(promiseArr);
  _ajaxStopWaiting("ajax_editor");
  ajaxAlert("兑换奖券完毕!");
  setTimeout(function () {
    _ajaxCloseModalDialog("ajax_dialog");
  }, 800);
}

/**
 * 彩票一键N连
 * @returns
 */
function multiTombola() {
  const $drawBtn = $('input[name="exec"][value="和克劳斯.钱袋兑换一张奖券"]');
  if (!$drawBtn.length) return;
  $(
    '<input type="button" name="multiTombola" value="一键50连" class="button clickable">'
  )
    .click(function () {
      if (confirm("是否进行一键50连?")) {
        createTombolaDialog(50);
      }
    })
    .insertAfter($drawBtn);
  $(
    '<input type="button" name="multiTombola" value="一键十连" class="button clickable">'
  )
    .click(function () {
      if (confirm("是否进行一键十连?")) {
        createTombolaDialog(10);
      }
    })
    .insertAfter($drawBtn);
}

function createTombolaDialog(times) {
  const targetId = "ajax_editor";
  _ajaxCreateDialogDirect(
    targetId,
    null,
    "抽奖记录",
    AJAX_YES | AJAX_CLOSE,
    async function (event) {
      if (event == "yes") {
        autoTombola(times);
      } else {
        _ajaxStopWaiting(targetId);
        _ajaxCloseModalDialog("ajax_editor");
      }
    },
    function () {
      document.querySelector("#ajax_editor_buttons button").innerText =
        "再来一次";
      console.log("init");
    }
  );

  _ajaxStartWaiting(targetId);

  $("#ajax_editor_content")
    .append('<input type="hidden" id="tombolaProgress">')
    .append('<div id="tombolaDetailContainer"></div>');
  autoTombola(times);
}

function getAllHiddenParams() {
  const searchParams = new URLSearchParams();
  $("input:hidden").each(function () {
    const $this = $(this);
    searchParams.set($this.attr("name"), $this.val());
  });
  return searchParams;
}

function getBaseSearchParams() {
  const searchParams = new URLSearchParams();
  $("input:hidden").each(function () {
    const $this = $(this);
    const key = $this.attr("name");
    if (baseParams.includes(key)) {
      searchParams.set($this.attr("name"), $this.val());
    }
  });
  return searchParams;
}

function sibebarDungeon() {
  const DUNGEON_LIST_KEY = "dungeonList";
  const DUNGEON_LIST_VERSION = "dungeonListVersion";
  // 每天更新一次地城信息
  let dungeonListStr = localStorage.getItem(DUNGEON_LIST_KEY);
  let dungeonListVer = localStorage.getItem(DUNGEON_LIST_VERSION);
  let dungeonList = {};
  const today = dayjs();
  const todayStr = today.format("YYYYMMDD");
  if (dungeonListStr && dungeonListVer && todayStr == dungeonListVer) {
    dungeonList = JSON.parse(dungeonListStr);
    renderDungeonSelector(dungeonList);
  } else {
    // 获取技能信息
    fetch("https://www.christophero.xyz/wod/dungeon/listCommon", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
    })
      .then((response) => {
        return response.json();
      })
      .then((res) => {
        if (!(res && res.code === 200)) {
          return;
        }
        const dungeonList = res.data;
        localStorage.setItem(DUNGEON_LIST_KEY, JSON.stringify(dungeonList));
        localStorage.setItem(DUNGEON_LIST_VERSION, todayStr);
        renderDungeonSelector(dungeonList);
      });
  }
}

function renderDungeonSelector(dungeonList) {
  // 将地城划分为常驻和限时地城
  let normalList = [];
  let limitList = [];
  const now = dayjs();
  const heroLv = parseInt($('input[name="stufe"]:hidden').val());
  for (let d of dungeonList) {
    if (d.type == "P" && d.minLevel <= heroLv && d.maxLevel >= heroLv) {
      d.desc = "常";
      normalList.push(d);
    } else if (d.type == "L" && d.minLevel <= heroLv && d.maxLevel >= heroLv) {
      const startTime = dayjs(d.startTime);
      const endTime = dayjs(d.endTime);
      if (startTime.isBefore(now) && endTime.isAfter(now)) {
        d.desc = "今";
        limitList.push(d);
      } else if (
        now.isBefore(startTime) &&
        now.add(7, "hour").isAfter(startTime)
      ) {
        d.desc = "明";
        limitList.push(d);
      }
    }
  }
  // 将地城插入侧边栏
  const $groupCrashBox = $(".gadget.group_cash_box");
  const $fastDungeonContaner = $(`
  <div class="gadget fast_dungeon lang-cn">
    <div class="gadget_inner">
      <div class="gadget_body" style="white-space: normal">
        <div class="block">
          <div class="block_body">
            <div class="background"></div>
            <div class="border-top"></div>
            <div class="border-bottom"></div>
            <div class="block_inner">
              <div class="blockHeadline">
                <a href="/wod/spiel/dungeon/dungeon.php?session_hero_id=115817">
                  <span class="font_Block_Headline">快捷地城</span>
                </a>
              </div>
              <div class="blockParagraph fast_dungeon">
                <div>
                  <label><input type="radio" name="fast_dungeon_type" checked value="normal" />常规</label>
                  <label><input type="radio" name="fast_dungeon_type" value="limit" />限时</label>
                  <button id="gotoTravel" class="button clickable" title="出发">出发</button>
                </div>
                <div>
                  <select name="fast_dungeon_select">
                    <option value="" selected="selected">&nbsp;</option>
                  </select>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  `);
  $groupCrashBox.before($fastDungeonContaner);
  rebuildDungeonSelect("normal", normalList, limitList);
  $fastDungeonContaner
    .find('input[name="fast_dungeon_type"]')
    .change(function () {
      console.log($(this).val());
      rebuildDungeonSelect($(this).val(), normalList, limitList);
    });
  $fastDungeonContaner.find("#gotoTravel").click(function () {
    let dungeonId = $('select[name="fast_dungeon_select"]').val();
    console.log(dungeonId);
    if (!dungeonId) return;
    gotoTravel(dungeonId);
  });
}

function rebuildDungeonSelect(dungeonType, normalList, limitList) {
  let list = dungeonType == "normal" ? normalList : limitList;
  list = list.sort((d1, d2) => {
    let timeDiff = dayjs(d1.startTime).diff(dayjs(d2.startTime));
    let lvDiff = d2.minLevel - d1.minLevel;
    return timeDiff || lvDiff;
  });
  const $select = $('select[name="fast_dungeon_select"]');
  $select
    .empty()
    .append('<option value="" selected="selected">&nbsp;</option>');
  for (let dungeon of list) {
    let name = "";
    if (dungeon.name.startsWith("巨兽讨伐战-")) {
      name = dungeon.name.replace("巨兽讨伐战-", "");
    } else {
      name = dungeon.name.length > 8 ? dungeon.name.substring(0, 4) + "..." +
            dungeon.name.substring(dungeon.name.length - 4)
          : dungeon.name;
    }
    $select.append(
      $("<option>&nbsp;</option>")
        .attr("value", dungeon.sysId)
        .text(`${dungeon.desc}|${name}`)
    );
  }
}

function gotoTravel(sysId) {
  let baseUrl = location.origin + "/wod/spiel/dungeon/dungeon.php?";
  const searchParams = getBaseSearchParams();
  const groupLv = searchParams.get("stufe");
  searchParams.set("TABLE_DEFAULT_SORT_DIR", "DESC");
  searchParams.set("TABLE_DEFAULT_SORT_COL", "7");
  searchParams.set("TABLE_DEFAULT_PAGE", "1");
  searchParams.set("TABLE_DEFAULT_PSNR[1]", "20");
  searchParams.set("TABLE_DEFAULT_PSNR[2]", "20");
  searchParams.set("TABLE_DATED_SORT_DIR", "ASC");
  searchParams.set("TABLE_DATED_SORT_COL", "14");
  searchParams.set("TABLE_DATED_PAGE", "1");

  searchParams.set("dungeon_1name", "不可能存在的地城");
  searchParams.set(
    "profile_data_dungeon_1_profile_data",
    "HogNB0ny8I/FjaOg6FXrzZvwI0A1hZgI77jjRYoCRE27wTAWqILHdEzHicsmJZl6X7ZRBEIW822E5+rsUoHjcPHJM1dTqbJE0c1Ad4c6gLXcUjcGy5WB8H0lm1qobBxf"
  );
  searchParams.set(
    "callback_js_code_dungeon_1_callback_js_code",
    "9hAUpnwetF8TrxnkIxgBdD27k4isavkaEEd/PwhJTLL8yf4MvBsSYedcxPRse51t2x6Aw2bcXKHHlyCszAStN2nnub0CncNJMDrZQePru5mpXW3It99S/D+JypTOewMQ/T9+eXLlhJZ7vK+j9IAKgKVS+EFJPGzy61GBgu60fBk="
  );
  searchParams.set("dungeon_1level", "99");
  searchParams.set("dungeon_1level_to", "99");
  searchParams.set("dungeon_1level_allowed", "99");
  searchParams.set("dungeon_1level_allowed_to", "99");
  searchParams.set("dungeon_1groupLevel", groupLv);
  searchParams.set("dungeon_1profile_id", "0");
  searchParams.set("dungeon_1is_open", "1");
  searchParams.set(`visit[${sysId}]`, "探索");
  searchParams.set("dungeon_2name", "不可能存在的地城");
  searchParams.set(
    "profile_data_dungeon_1_profile_data",
    "HogNB0ny8I/FjaOg6FXrzZvwI0A1hZgI77jjRYoCRE26Yi3zTNw4kxlf3EBWtEk1b6aVuW+FUuN8kSMRggg8h3JkxoL2NsUxavZXWRdyOxUUEMX5AKRE3eAUHOs1WJk3"
  );
  searchParams.set(
    "callback_js_code_dungeon_1_callback_js_code",
    "hjeqjpM+qZ3O91mfrGQpvhGIqpJEtjGwgXlmEmXfIQzBrxMpR69Guzy7+7UjXgADJMzqeXDJhCUPVMr4x2KfpOKujjttFf22twLrAnOMemRyC0FkOn1zx6YBUufUkR7Ckg4pf2y/2z74zxDx8svUS6Kwihgpc54Xeb1xFKyGmQQ="
  );
  searchParams.set("dungeon_2level", "99");
  searchParams.set("dungeon_2level_to", "99");
  searchParams.set("dungeon_2level_allowed", "99");
  searchParams.set("dungeon_2level_allowed_to", "99");
  searchParams.set("dungeon_2groupLevel", groupLv);
  searchParams.set("dungeon_2profile_id", "0");
  searchParams.set("dungeon_2is_open", "1");
  baseUrl += "session_hero_id=" + searchParams.get("session_hero_id");
  ajaxAlert("切换中,请稍候...");
  fetch(baseUrl, {
    headers: {
      accept:
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
      "content-type": "application/x-www-form-urlencoded",
    },
    method: "POST",
    body: searchParams.toString(),
  })
    .then((resp) => {
      // location.replace(location.href);
      return resp.text();
    })
    .then((html) => {
      console.log(html);
      let $curT = $("#gadgetNextdungeonTime");
      if (!$curT.length) {
        // 处理偶尔出现的刷新后英雄变化的问题
        let [baseUrl, params] = location.href.split("?");
        const urlParams = new URLSearchParams(params);
        if (!urlParams.has("session_hero_id")) {
          urlParams.set("session_hero_id", searchParams.get("session_hero_id"));
          params = urlParams.toString();
        }
        location.replace(baseUrl + "?" + params);
        return;
      }
      let $curB = $curT.prevAll("b:first");
      let $curC = $curT.nextAll("div[id^=CombatDungeonConfigSelector]:first");
      let $newT = $(html).find("#gadgetNextdungeonTime");
      let $newB = $newT.prevAll("b:first");
      let $newC = $newT.nextAll("div[id^=CombatDungeonConfigSelector]:first");
      $curT.replaceWith($newT);
      $curB.replaceWith($newB);
      $curC.replaceWith($newC);
      ajaxAlert("已切换!");
      setTimeout(function () {
        _ajaxCloseModalDialog("ajax_dialog");
      }, 800);
    });
}

/**
 * 简易属性控制
 */
function easyAttrVal() {
  const urls = ["/wod/spiel/hero/attributes.php"];
  if (!urls.includes(location.pathname) || location.host.startsWith("zhao")) {
    return;
  }
  // 添加便捷属性
  $(
    '<img src="/wod/css/skins/skin-7/images/icons/reset.gif" style="cursor:pointer;width:20px;height:20px;"/>'
  )
    .insertAfter(
      $('input[name^="improve[at_"], img[src$="icons/steigern_disabled.gif"]')
    )
    .click(async function () {
      const urlParams = getBaseSearchParams();
      let targetVal = prompt("请输入目标数值", 10);
      if (targetVal == null) return;
      targetVal = parseNum(targetVal);
      if (targetVal > 30) {
        alert("目标数值不能大于30!");
        return;
      }
      const attrName = $(this)
        .parents("tr:eq(1)")
        .find("td:first")
        .text()
        .trim();
      ajaxAlert("正在处理,请稍候...");
      await attrChangeBatch(urlParams, attrName, targetVal);
      ajaxAlert("处理完成,即将刷新页面");
      setTimeout(function () {
        _ajaxCloseModalDialog("ajax_dialog");
        location.replace(location.origin + "/wod/spiel/hero/attributes.php");
      }, 800);
    });

  // 添加高手低手全13配置
  const $attrTableTd = $(
    '#main_content h1:contains("属性与特性") +table:first td:first'
  );
  $('<input type="button" value="成为高手" class="button clickable">')
    .appendTo($attrTableTd)
    .click(async function () {
      ajaxAlert("正在处理,请稍候...");
      await allAttrTo(urlParams, 2);
      ajaxAlert("处理完成,即将刷新页面");
      setTimeout(function () {
        _ajaxCloseModalDialog("ajax_dialog");
        location.replace(location.origin + "/wod/spiel/hero/attributes.php");
      }, 800);
    });

  $('<input type="button" value="变成低手" class="button clickable">')
    .appendTo($attrTableTd)
    .click(async function () {
      ajaxAlert("正在处理,请稍候...");
      await allAttrTo(urlParams, 10);
      ajaxAlert("处理完成,即将刷新页面");
      setTimeout(function () {
        _ajaxCloseModalDialog("ajax_dialog");
        location.replace(location.origin + "/wod/spiel/hero/attributes.php");
      }, 800);
    });

  $('<input type="button" value="八徽章" class="button clickable">')
    .appendTo($attrTableTd)
    .click(async function () {
      ajaxAlert("正在处理,请稍候...");
      await allAttrTo(urlParams, 13);
      ajaxAlert("处理完成,即将刷新页面");
      setTimeout(function () {
        _ajaxCloseModalDialog("ajax_dialog");
        location.replace(location.origin + "/wod/spiel/hero/attributes.php");
      }, 800);
    });
}

function easyChangeSkill() {
  const urls = ["/wod/spiel/hero/skills.php"];
  if (!urls.includes(location.pathname) || location.host.startsWith("zhao")) {
    return;
  }
  const urlParams = getBaseSearchParams();
  $(
    '<img src="/wod/css/skins/skin-7/images/icons/reset.gif" style="cursor:pointer;width:20px;height:20px;"/>'
  )
    .insertAfter($('input[id^="button_steigern_"]'))
    .click(async function () {
      const urlParams = getBaseSearchParams();
      let btnId = $(this).prev('input[id^="button_steigern_"]').attr("id");
      const skillId = /button_steigern_(\d+)/.exec(btnId)[1];
      const skillLv =
        parseInt(
          $(this).parents("tr:first").find("div[id^=skill_rang]").text().trim()
        ) || 0;
      let targetVal = prompt("请输入目标技能等级", skillLv);
      if (targetVal == null) return;
      targetVal = parseNum(targetVal);
      changeSkillBatch(skillId, skillLv, targetVal, true);
    });
}

/**
 * 快速变更技能等级
 * @param {*} skillId
 * @param {*} curLv
 * @param {*} targetLv
 * @param {*} force
 * @returns
 */
function changeSkillBatch(skillId, curLv, targetLv, force) {
  if (targetLv > 50 || targetLv < 0) {
    alert("目标技能等级只能在0到50之间!");
    return;
  }
  const diff = Math.abs(targetLv - curLv);
  for (let i = 0; i < diff; i++) {
    change_skill(skillId, curLv > targetLv ? "-" : "+", force);
  }
}

/**
 * 属性变更
 * @param {*} urlParams
 * @param {*} attrName
 * @param {*} improve
 * @returns
 */
async function attrChange(urlParams, attrName, improve) {
  if (!improve) return;
  const pname = `${improve ? "improve" : "undo"}[at_${attrEnMap[attrName]}]`;
  const params = new URLSearchParams(urlParams.toString());
  params.set(pname + ".x", 10);
  params.set(pname + ".y", 10);
  params.set("do_reset", "");
  params.set("levelup_warned", "0");
  params.set("geschlecht", "f");
  params.set("amor_details", "关闭");
  const url = `${
    location.origin
  }/wod/spiel/hero/attributes.php?is_popup=1&session_hero_id=${params.get(
    "session_hero_id"
  )}`;
  const response = await fetch(url, {
    headers: {
      accept:
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
      "content-type": "application/x-www-form-urlencoded",
    },
    body: params.toString(),
    method: "POST",
  });
  globalCurSteps++;
  ajaxAlert(`${attrName}变更进度: ${globalCurSteps}/${globalSteps}`);
  return await response.text();
}

/**
 * 属性批量变更
 * @param {*} urlParams
 * @param {*} attrName
 * @param {*} targetVal
 */
async function attrChangeBatch(urlParams, attrName, targetVal) {
  const curVal = getAttrVal(attrName);
  await attrChangeBatchWithInitial(urlParams, attrName, curVal, targetVal);
}

/**
 * 属性批量变更(手动指定初始属性)
 * @param {*} urlParams
 * @param {*} attrName
 * @param {*} curVal
 * @param {*} targetVal
 */
async function attrChangeBatchWithInitial(
  urlParams,
  attrName,
  curVal,
  targetVal
) {
  globalCurSteps = 0;
  globalSteps = Math.abs(targetVal - curVal);
  let improve = true;
  if (curVal > targetVal) {
    improve = false;
  }
  for (let i = 0; i < globalSteps; i++) {
    await attrChange(urlParams, attrName, improve);
  }
}

/**
 * 获得角色属性数值
 * @param {*} attr
 * @returns
 */
function getAttrVal(attr, $context) {
  $context = $context || $(document);
  let val = $context
    .find(`td:contains("${attr}")`)
    .next()
    .find("td:eq(1)")[0]
    .childNodes[0].textContent.trim();
  val = parseInt(val);
  return val;
}

function parseNum(numStr) {
  let num = 1;
  try {
    num = parseInt(numStr);
  } catch (ex) {
    num = 1;
  }
  if (isNaN(num) || num < 0) {
    num = 1;
  }
  return num;
}

// 导入英雄模版
function importHeroTemplate() {
  // 只在技能页面生效
  const urls = ["/wod/spiel/hero/skills.php"];
  if (!urls.includes(location.pathname) || location.host.startsWith("zhao")) {
    return;
  }
  // 技能列表页面添加导入英雄模版按钮
  const $btn = $(
    '<button class="button clickable" type="button">导入英雄模版</button>'
  )
    .insertBefore('input[name="hide_all"]')
    .click(async function () {
      const urlParams = getBaseSearchParams();
      // 1. 获取人物卡
      // 2. 分析人物卡
      // 3. 获取8属性数值
      // 4. 比较属性差异,升级或者降级属性
      // 5. 比较技能差异,升级或者降级技能

      createInputHeroCardDialog(urlParams, analyseHeroCard);
    });
}

function createInputHeroCardDialog(urlParams, callback) {
  const targetId = "ajax_editor";
  const innerHtml =
    '<div id="heroCardContainer" contentEditable="true" style="height: 100%;"></div>';
  _ajaxCreateDialogDirect(
    targetId,
    innerHtml,
    "请在下方输入人物卡,不要有多余文本",
    AJAX_YES | AJAX_CLOSE,
    async function (event) {
      if (event == "yes") {
        let heroCard = document.getElementById("heroCardContainer").textContent;
        callback(urlParams, heroCard);
      } else {
        _ajaxStopWaiting(targetId);
        _ajaxCloseModalDialog("ajax_editor");
      }
    },
    function () {
      console.log("init");
      document.querySelector("#ajax_editor_buttons button").innerText = "导入";
    }
  );
}

async function analyseHeroCard(urlParams, heroCard) {
  try {
    heroCard = heroCard
      .replace(/\[\d+\]/g, "")
      .replace(/:g\w:/g, "")
      .replace(/\[test\]/g, "")
      .replace(/\[clone\]/g, "")
      .replace(/\[/g, "<")
      .replace(/\]/g, ">")
      .replace(/[\r\n]/g, "")
      .replace(
        /<skill:\s*(.+?)\s*>/g,
        (fullHtml, skillName) => `<skill>${skillName}</skill>`
      )
      .replace(
        /<item:\s*(.+?)\s*>/g,
        (fullHtml, itemName) => `<item>${itemName}</item>`
      )
      .replace(/color=orange/g, "orange");
  } catch (error) {
    ajaxAlert("浏览器版本过低,不能使用replaceAll函数");
    return;
  }
  let $heroCard;
  try {
    $heroCard = $(heroCard);
  } catch (error) {
    ajaxAlert("人物卡格式可能存在问题,不能正确分析");
    return;
  }
  const attrNameList = Object.keys(attrEnMap);
  ajaxAlert("1. 分析人物卡属性清单");
  // 获取属性清单
  const targetAttrMap = {};
  $heroCard
    .find(attrNameList.map((attr) => `orange:contains("${attr}")`).join(","))
    .each(function (i, e) {
      const attrVal = $(e).parent().next().text();
      const attrName = e.textContent;
      targetAttrMap[attrName] = parseInt(attrVal);
    });
  ajaxAlert("2. 分析人物卡技能清单");
  // 获取技能等级一览
  const skillLvMap = {};
  $heroCard.find("skill").each(function (i, e) {
    const skillLv = $(e).parent().next().text();
    const skillName = e.textContent;
    skillLvMap[skillName] = parseInt(skillLv);
  });
  ajaxAlert("3. 分析人物卡装备清单");
  // 获取装备物品一览
  const equipItems = [];
  $heroCard.find("item").each((i, e) => equipItems.push(e.textContent));
  console.log(targetAttrMap);
  console.log(skillLvMap);
  console.log(equipItems);

  if (confirm("是否使属性保持一致(注意不会使用重置点进行属性回退)?")) {
    const currentAttrMap = await getHeroAttrs(urlParams);
    ajaxAlert("4. 开始进行属性变更");
    // 变更属性
    for (let attrName of attrNameList) {
      await attrChangeBatchWithInitial(
        urlParams,
        attrName,
        currentAttrMap[attrName],
        targetAttrMap[attrName]
      );
    }
  }

  ajaxAlert("5. 开始变更当前技能等级");
  // 变更页面上的技能等级
  for (let skillName of Object.keys(skillLvMap)) {
    const $skillAnchor = $(
      `a[href^="/wod/spiel/hero/skill.php"]:contains("${skillName}")`
    );
    if (!$skillAnchor.length) continue;
    const $skillRang = $skillAnchor
      .parent("td:first")
      .next()
      .find('[id^="skill_rang_"]');
    if (!$skillRang.length) continue;
    const skillId = $skillRang.attr("id").replace("skill_rang_", "");
    const skillLv = parseInt($skillRang.text().trim()) || 0;
    changeSkillBatch(skillId, skillLv, skillLvMap[skillName], true);
  }
  ajaxAlert("处理完毕!");
  setTimeout(function () {
    _ajaxStopWaiting("ajax_editor");
  }, 800);
}

async function getHeroAttrs(urlParams) {
  ajaxAlert("正在获取英雄当前属性");
  const shId = urlParams.get("session_hero_id");
  const url =
    location.origin +
    "/wod/spiel/hero/attributes.php?is_popup=1&session_hero_id=" +
    shId;
  const response = await fetch(url, {
    headers: {
      accept:
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
      "content-type": "application/x-www-form-urlencoded",
    },
    body: urlParams.toString(),
    method: "POST",
  });
  const text = await response.text();
  const $context = $(text);
  const attrMap = {
    力量: getAttrVal("力量", $context),
    体质: getAttrVal("体质", $context),
    智力: getAttrVal("智力", $context),
    灵巧: getAttrVal("灵巧", $context),
    魅力: getAttrVal("魅力", $context),
    敏捷: getAttrVal("敏捷", $context),
    感知: getAttrVal("感知", $context),
    意志: getAttrVal("意志", $context),
  };
  console.log(attrMap);
  ajaxAlert(
    `英雄当前属性已获取:<br>
    力量:${attrMap["力量"]}<br>
    体质:${attrMap["体质"]}<br>
    智力:${attrMap["智力"]}<br>
    灵巧:${attrMap["灵巧"]}<br>
    魅力:${attrMap["魅力"]}<br>
    敏捷:${attrMap["敏捷"]}<br>
    感知:${attrMap["感知"]}<br>
    意志:${attrMap["意志"]}`
  );
  return attrMap;
}

/**
 * 批量删除设置
 */
function batchDelProfile() {
  // 只在技能页面生效
  const urls = ["/wod/spiel/hero/skillconfig.php"];
  if (!urls.includes(location.pathname)) {
    return;
  }
  $('<input type="button" value="批量删除">')
    .insertAfter('input[value="删除"]')
    .click(function () {
      const urlParams = getBaseSearchParams();
      urlParams.set("SELECTED_TAB", $('input[name="SELECTED_TAB"]').val());
      urlParams.set("SELECTED_LVL", $('input[name="SELECTED_LVL"]').val());
      urlParams.set("SELECTED_DUEL", $('input[name="SELECTED_DUEL"]').val());
      urlParams.set("fig_type", $('input[name="fig_type"]').val());
      urlParams.set("fig_id", $('input[name="fig_id"]').val());
      urlParams.set("world", $('input[name="world"]').val());
      urlParams.set("orig_profile", $('input[name="orig_profile"]').val());
      urlParams.set("is_popup", "1");
      urlParams.set("action", "delete");
      createDelProfileDialog(urlParams);
    });
}

function createDelProfileDialog(urlParams) {
  const targetId = "ajax_editor";
  const innerHtml =
    '<div id="delProfileContainer" style="height: 100%;"></div>';
  _ajaxCreateDialogDirect(
    targetId,
    innerHtml,
    "请勾选需要删除的设置(默认和决斗不在此列)",
    AJAX_YES | AJAX_CLOSE,
    async function (event) {
      if (event == "yes") {
        console.log($('input:checked[id^="profile_"]'));
        const profileList = $('input:checked[id^="profile_"]')
          .map((i, e) => e.value)
          .get();
        if (!profileList.length) {
          ajaxAlert(`没有勾选任何设置!`);
          setTimeout(function () {
            _ajaxStopWaiting(targetId);
          });
          return;
        }
        if (confirm(`是否删除这${profileList.length}条设置?`)) {
          globalCurSteps = 0;
          globalSteps = profileList.length;
          let promiseArr = [];
          for (const profileId of profileList) {
            promiseArr.push(delProfile(urlParams, profileId));
          }
          ajaxAlert(`删除进度:${globalCurSteps}/${globalSteps}`);
          Promise.all(promiseArr).then((responseArr) => {
            _ajaxStopWaiting(targetId);
            ajaxAlert(`删除完成,即将刷新页面`);
            setTimeout(function () {
              location.replace(
                location.origin + "/wod/spiel/hero/skillconfig.php"
              );
            }, 800);
          });
        } else {
          setTimeout(function () {
            _ajaxStopWaiting(targetId);
          });
        }
      } else {
        _ajaxStopWaiting(targetId);
        _ajaxCloseModalDialog("ajax_editor");
      }
    },
    function () {
      console.log("init");
      document.querySelector("#ajax_editor_buttons button").innerText =
        "开始删除";
      const $container = $("#delProfileContainer");
      for (let option of THE_ORDERS.profileDropdown.options) {
        const userObj = option.userObject;
        if (userObj.active || userObj.id < 1) continue;
        $container.append(
          `<input type="checkbox" value="${userObj.id}" id="profile_${userObj.id}"/><label for="profile_${userObj.id}">${userObj.name}</label><br>`
        );
      }
    }
  );
}

async function delProfile(urlParams, profileId) {
  const shId = urlParams.get("session_hero_id");
  const url =
    location.origin +
    "/wod/spiel/hero/skillconfig.php?is_popup=1&session_hero_id=" +
    shId;
  urlParams.set("profile", profileId);
  const response = await fetch(url, {
    headers: {
      accept:
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
      "content-type": "application/x-www-form-urlencoded",
    },
    body: urlParams.toString(),
    method: "POST",
  });
  globalCurSteps++;
  ajaxAlert(`删除进度:${globalCurSteps}/${globalSteps}`);
  return response.text();
}

function batchChangeOwner() {
  // 只在物品列表页面生效
  const urls = ["/wod/spiel/hero/items.php"];
  if (!urls.includes(location.pathname)) {
    return;
  }
  // 没找到拥有者则退出
  let $itemOwnerSelect = $(
    'select[name="item_4owner"],select[name="item_5owner"],select[name="item_6owner"]'
  );
  if (!$itemOwnerSelect.length) {
    return;
  }
  const $changeOwnerBtn = $(
    '<button type="button" class="button clickable" title="变更物品归属">变更物品归属</button>'
  )
    .insertAfter($('input[value="应用改动"]'))
    .click(function () {
      createChangeOwnerDialog($itemOwnerSelect);
    });
}

function createChangeOwnerDialog($itemOwnerSelect) {
  const pageSize = 20;
  let idx = $itemOwnerSelect
    .prop("name")
    .replace("item_", "")
    .replace("owner", "");
  const urlParams = getBaseSearchParams();
  urlParams.set("ok", "应用改动");
  urlParams.set("IS_POPUP", 1);
  urlParams.set(
    "pay_from_group_cash_box",
    $('input[name="pay_from_group_cash_box"]').val()
  );
  urlParams.set("put_purchases_to", $('input[name="put_purchases_to"]').val());
  urlParams.set("view", $('input[name="view"]').val());
  urlParams.set("common_cellar", $('input[name="common_cellar"]').val());
  urlParams.set("marketview", $('input[name="marketview"]').val());
  urlParams.set("ITEMS_KELLER_SORT_DIR", "ASC");
  urlParams.set("ITEMS_KELLER_SORT_COL", 2);
  urlParams.set("ITEMS_KELLER_PAGE", 1);
  urlParams.set(`item_${idx}name`, $(`input[name="item_${idx}name"]`).val());
  urlParams.set(
    `profile_data_item_${idx}_profile_data`,
    $(`input[name="profile_data_item_${idx}_profile_data"]`).val()
  );
  urlParams.set(
    `callback_js_code_item_${idx}_callback_js_code`,
    $(`input[name="callback_js_code_item_${idx}_callback_js_code"]`).val()
  );
  urlParams.set(`item_${idx}hero_class`, 0);
  urlParams.set(`item_${idx}hero_race`, 0);
  urlParams.set(`item_${idx}location`, "");
  urlParams.set(`item_${idx}unique`, "");
  urlParams.set(`item_${idx}bonus_attr`, "NULL");
  urlParams.set(`item_${idx}item_class`, 0);
  urlParams.set(`item_${idx}any_skill`, 0);
  urlParams.set(`item_${idx}skill`, "");
  urlParams.set(`item_${idx}any_skillclass`, 0);
  urlParams.set(`item_${idx}set`, 0);
  urlParams.set(`item_${idx}item_condition`, 0);
  urlParams.set(`item_${idx}sockets`, "NULL");
  urlParams.set(`item_${idx}item_conditionMax`, 6);
  urlParams.set(`item_${idx}usage_item`, "");
  urlParams.set(`item_${idx}hero_level_enabled_posted`, 1);
  urlParams.set(`item_${idx}hero_level`, 40);
  urlParams.set(`item_${idx}hero_level_stored`, 40);
  urlParams.set(`item_${idx}group_item`, "");
  urlParams.set(`item_${idx}attribute_name`, "eff_at_st");
  urlParams.set(`item_${idx}attribute_value`, "");
  urlParams.set(`item_${idx}profile_id`, 0);
  urlParams.set(`item_${idx}is_open`, 1);
  urlParams.set(`ITEMS_KELLER_JPNR[1]:`, 1);
  urlParams.set(`ITEMS_KELLER_PSNR[1]`, pageSize);
  urlParams.set(`dummy`, "");
  urlParams.set(`ITEMS_KELLER_JPNR[2]:`, 1);
  urlParams.set(`ITEMS_KELLER_PSNR[2]`, pageSize);
  $itemOwnerSelect = $itemOwnerSelect.clone().attr("id", "owner-select");
  $itemOwnerSelect[0].removeChild($itemOwnerSelect.find(".option_")[0]);
  $itemOwnerSelect[0].removeChild(
    $itemOwnerSelect.find(
      `option[value="${urlParams.get("session_hero_id")}"]`
    )[0]
  );
  const targetId = "ajax_dialog";
  const innerHtml = $itemOwnerSelect.prop("outerHTML");
  _ajaxCreateDialogDirect(
    targetId,
    innerHtml,
    "请选择需要转移物品归属的拥有者",
    AJAX_YES | AJAX_CLOSE,
    async function (event) {
      if (event == "yes") {
        let targetVal = prompt("请输入转移物品数量", 1000);
        if (targetVal == null) return;
        targetVal = parseNum(targetVal);
        if (targetVal < 0) {
          alert("目标数值不能小于0");
          return;
        }
        urlParams.set(`item_${idx}owner`, $("#owner-select").val());
        let goLagerArr = [];
        let goBackArr = [];
        let remainCnt = targetVal;
        while (remainCnt) {
          const lastGoLagerArr = [...goLagerArr];
          goLagerArr = await changItemsOwner(urlParams, goLagerArr, goBackArr);
          goBackArr = lastGoLagerArr;
          if (remainCnt <= 0 || goLagerArr.length == 0) break;
          remainCnt -= goLagerArr.length;
          ajaxAlert(`处理进度:${targetVal - remainCnt}/${targetVal}`);
        }
        await changItemsOwner(urlParams, [], goBackArr);
        ajaxAlert("处理完成!");
      } else {
        _ajaxStopWaiting(targetId);
        _ajaxCloseModalDialog(targetId);
      }
    },
    function () {
      document.querySelector(`#${targetId}_buttons button`).innerText = "确定";
    }
  );
}

async function changItemsOwner(urlParams, goLagerArr, goBackArr) {
  const params = new URLSearchParams(urlParams.toString());
  const shId = urlParams.get("session_hero_id");
  const goWhere = urlParams.get("backWhere");
  for (let id of goLagerArr) {
    params.set(id, "go_lager");
  }
  for (let id of goBackArr) {
    params.set(id, goWhere);
  }
  const url =
    location.origin +
    "/wod/spiel/hero/items.php?is_popup=1&session_hero_id=" +
    shId;
  const response = await fetch(url, {
    headers: {
      accept:
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
      "content-type": "application/x-www-form-urlencoded",
    },
    body: params.toString(),
    method: "POST",
  });
  const text = await response.text();
  const $context = $(text);
  const $firstOption = $context.find(
    'select[name^=EquipItem] option[value^="-go"]:first'
  );
  if ($firstOption.length) {
    const backWhere = $firstOption.val().replace("-", "");
    urlParams.set("backWhere", backWhere);
  }
  return $context
    .find("select[name^=EquipItem]")
    .filter((i, e) => $(e).find("option[value*=go_group]").length)
    .map((i, e) => e.name)
    .get();
}

function viewFullScreen() {
  const $fcBtn = $(
    '<button type="button" class="button" style="position: absolute;right: 0;top: 0;">切换全屏</button>'
  );
  const $main = $("#main_content");
  GM_addStyle(`div#main_content:fullscreen {overflow: scroll;}`);
  $fcBtn.prependTo($main).click(function () {
    const isFullscreen =
      document.fullScreen ||
      document.mozFullScreen ||
      document.webkitIsFullScreen;
    if (isFullscreen) {
      exitFullScreen($main[0]);
    } else {
      fullScreen($main[0]);
    }
  });
}

function fullScreen(el) {
  let rfs =
    el.requestFullScreen ||
    el.webkitRequestFullScreen ||
    el.mozRequestFullScreen ||
    el.msRequestFullScreen;
  $(el).css("overflow", "scroll");
  if (typeof rfs != "undefined" && rfs) {
    rfs.call(el);
    return;
  }
  if (typeof window.ActiveXObject != "undefined") {
    const wscript = new ActiveXObject("WScript.Shell");
    if (wscript) {
      wscript.SendKeys("{F11}");
    }
  }
}
// 定义退出全屏
function exitFullScreen() {
  const el = document;
  const cfs =
    el.cancelFullScreen ||
    el.webkitCancelFullScreen ||
    el.mozCancelFullScreen ||
    el.exitFullScreen;
  $(el).css("overflow", "unset");
  if (typeof cfs != "undefined" && cfs) {
    cfs.call(el);
    return;
  }
  if (typeof window.ActiveXObject != "undefined") {
    const wscript = new ActiveXObject("WScript.Shell");
    if (wscript != null) {
      wscript.SendKeys("{F11}");
    }
  }
}

let totalPage = 0;
let currentPage = 0;

function autoCleanWarehouse() {
  // 只在物品列表页面生效
  const urls = ["/wod/spiel/hero/items.php"];
  if (!urls.includes(location.pathname)) {
    return;
  }
  const view = $('input[name="view"]').val();
  if (!["cellar", "groupcellar_2"].includes(view)) return;
  let pagePrefix = "GROUPCELLAR";
  if (view === "groupcellar_2") {
    pagePrefix = "GROUPCELLAR";
  } else if (view === "cellar") {
    pagePrefix = "KELLER";
  }
  const $cleanWarehouseBtn = $(
    '<button type="button" class="button clickable" title="按指定规则自动清仓">自动清仓</button>'
  )
    .insertAfter($('input[value="应用改动"]'))
    .click(async function () {
      // 1. 获取第一页内容,并且获得总页数
      // 2. 遍历所有页,获得物品列表,目前只清理耗材,生成类似下面的数据结构
      // {冰晶:[{cnt: 30, id: 10}, {cnt: 45, id: 12}], 硫磺: [{cnt: 30, id: 21}, {cnt: 45, id: 22}]}
      // 3. 将物品按剩余次数从小向下排序,增加合计,然后按照预设的清仓数量从小到大进行标记
      // 4. 将所有标记的数据提取到列表中,然后200个一组依次出售
      ajaxAlert(`处理开始,请稍候...`);
      totalPage = 1;
      currentPage = 0;
      let firstPage = await fetchConsumablesPage(1, pagePrefix);
      let coll = {};
      let $doc = $(firstPage);
      totalPage = $doc
        .find(`input[name^="ITEMS_${pagePrefix}_PAGE"]:last`)
        .val()
        .trim();
      buildColl(coll, firstPage);
      const promiseArr = [];
      for (let pageIndex = 2; pageIndex <= totalPage; pageIndex++) {
        promiseArr.push(fetchConsumablesPage(pageIndex, pagePrefix));
      }
      Promise.all(promiseArr).then(async (textArr) => {
        for (let text of textArr) {
          buildColl(coll, text);
        }
        console.log(coll);
        ajaxAlert(`已分析全部数据,开始出售物品!`);

        const settings = loadCleanWarehouseSettings();
        if (!settings) {
          ajaxAlert(`没有预设清仓列表,请设置后重试`);
          return;
        }
        let keepMap = settings["normal"];
        let sellList = [];
        for (let name of Object.keys(keepMap)) {
          if (!coll[name]) continue;
          const curTotal = coll[name].total || 0;
          const max = keepMap[name];
          if (curTotal <= max) continue;
          const sellCnt = curTotal - max;
          let markedCnt = 0;
          for (let item of coll[name].list) {
            markedCnt += item.cnt;
            if (markedCnt <= sellCnt) {
              sellList.push(item);
            } else {
              break;
            }
          }
        }
        console.log(sellList);

        const chunkSellList = _.chunk(sellList, 200);

        for (let i = 0; i < chunkSellList.length; i++) {
          let list = chunkSellList[i];
          ajaxAlert(`出售进度:${i}/${chunkSellList.length}`);
          await sellItems(
            list.map((i) => i.id),
            pagePrefix
          );
        }
        ajaxAlert(`处理完成!`);
      });
    });
}

/**
 * 根据类型和页数获取消耗品列表
 * @param {*} pageIndex
 * @param {*} type
 */
async function fetchConsumablesPage(pageIndex, pagePrefix) {
  const pageSize = 200;
  let idx = 5;
  if (pagePrefix === "GROUPCELLAR") {
    idx = 5;
  } else if (pagePrefix === "KELLER") {
    idx = 4;
  }
  const urlParams = getBaseSearchParams();
  const shId = urlParams.get("session_hero_id");
  urlParams.set("IS_POPUP", 1);
  urlParams.set(
    "pay_from_group_cash_box",
    $('input[name="pay_from_group_cash_box"]').val()
  );
  urlParams.set("put_purchases_to", $('input[name="put_purchases_to"]').val());
  urlParams.set("view", $('input[name="view"]').val());
  urlParams.set("common_cellar", $('input[name="common_cellar"]').val());
  urlParams.set("marketview", $('input[name="marketview"]').val());
  urlParams.set(`ITEMS_${pagePrefix}_SORT_DIR`, "ASC");
  urlParams.set(`ITEMS_${pagePrefix}_SORT_COL`, 2);
  urlParams.set(`ITEMS_${pagePrefix}_PAGE`, 1);
  urlParams.set(`item_${idx}name`, $(`input[name="item_${idx}name"]`).val());
  urlParams.set(
    `profile_data_item_${idx}_profile_data`,
    $(`input[name="profile_data_item_${idx}_profile_data"]`).val()
  );
  urlParams.set(
    `callback_js_code_item_${idx}_callback_js_code`,
    $(`input[name="callback_js_code_item_${idx}_callback_js_code"]`).val()
  );
  urlParams.set(`item_${idx}hero_class`, 0);
  urlParams.set(`item_${idx}hero_race`, 0);
  urlParams.set(`item_${idx}location`, "");
  urlParams.set(`item_${idx}unique`, "");
  urlParams.set(`item_${idx}bonus_attr`, "NULL");
  urlParams.set(`item_${idx}item_class`, 0);
  urlParams.set(`item_${idx}any_skill`, 0);
  urlParams.set(`item_${idx}skill`, "");
  urlParams.set(`item_${idx}any_skillclass`, 0);
  urlParams.set(`item_${idx}set`, 0);
  urlParams.set(`item_${idx}item_condition`, 0);
  urlParams.set(`item_${idx}sockets`, "NULL");
  urlParams.set(`item_${idx}item_conditionMax`, 6);
  urlParams.set(`item_${idx}usage_item`, "yes");
  urlParams.set(`item_${idx}hero_level_enabled_posted`, 1);
  urlParams.set(`item_${idx}hero_level`, 40);
  urlParams.set(`item_${idx}hero_level_stored`, 40);
  urlParams.set(`item_${idx}group_item`, "no");
  urlParams.set(`item_${idx}attribute_name`, "eff_at_st");
  urlParams.set(`item_${idx}attribute_value`, "");
  urlParams.set(`item_${idx}profile_id`, 0);
  urlParams.set(`item_${idx}is_open`, 1);
  urlParams.set(`dummy`, "");
  urlParams.set(`ITEMS_${pagePrefix}_PAGE[${pageIndex}]:`, pageIndex);
  urlParams.set(`ITEMS_${pagePrefix}_JPNR[1]:`, 1);
  urlParams.set(`ITEMS_${pagePrefix}_PSNR[1]`, pageSize);
  urlParams.set(`ITEMS_${pagePrefix}_JPNR[2]:`, 1);
  urlParams.set(`ITEMS_${pagePrefix}_PSNR[2]`, pageSize);
  const url =
    location.origin +
    "/wod/spiel/hero/items.php?is_popup=1&session_hero_id=" +
    shId;
  const response = await fetch(url, {
    headers: {
      accept:
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
      "content-type": "application/x-www-form-urlencoded",
    },
    body: urlParams.toString(),
    method: "POST",
  });
  const text = await response.text();
  currentPage++;
  ajaxAlert(`目前进度:${currentPage}/${totalPage}`);
  return text;
}

/**
 * 构建物品映射
 * @param {*} coll
 * @param {*} text
 */
function buildColl(coll, text) {
  let $doc = $(text);
  $doc.find('a[href^="/wod/spiel/hero/item.php?"]').each(function (i, e) {
    const $e = $(e);
    const name = $e.text();
    const text = $e.parent().text();
    const reResult = text.match(/(.+)\((\d+)\/(\d+)\)/);
    if (!reResult) {
      console.error(text);
      return;
    }
    const url = $e.attr("href");
    const searchParams = new URLSearchParams(url.split("?")[1]);
    const itemId = searchParams.get("item_instance_id");
    const count = parseInt(reResult[2]);
    const max = parseInt(reResult[3]);
    console.log(
      `【${name}】: ${reResult[2]}/${reResult[3]}  with ID: ${itemId}`
    );
    if (!coll[name]) {
      coll[name] = {
        total: count,
        list: [{ cnt: count, id: itemId, name, max }],
      };
    } else {
      coll[name].total += count;
      coll[name].list.push({ cnt: count, id: itemId, name, max });
    }
  });
}

/**
 * 出售物品
 * @param {*} itemIds
 * @param {*} pagePrefix
 * @returns
 */
async function sellItems(itemIds, pagePrefix) {
  const pageSize = 200;
  let idx = 5;
  if (pagePrefix === "GROUPCELLAR") {
    idx = 5;
  } else if (pagePrefix === "KELLER") {
    idx = 4;
  }
  const urlParams = getBaseSearchParams();
  const shId = urlParams.get("session_hero_id");
  urlParams.set("IS_POPUP", 1);
  urlParams.set(
    "pay_from_group_cash_box",
    $('input[name="pay_from_group_cash_box"]').val()
  );
  urlParams.set("put_purchases_to", $('input[name="put_purchases_to"]').val());
  urlParams.set("view", $('input[name="view"]').val());
  urlParams.set("common_cellar", $('input[name="common_cellar"]').val());
  urlParams.set("marketview", $('input[name="marketview"]').val());
  urlParams.set(`ITEMS_${pagePrefix}_SORT_DIR`, "ASC");
  urlParams.set(`ITEMS_${pagePrefix}_SORT_COL`, 2);
  urlParams.set(`ITEMS_${pagePrefix}_PAGE`, 1);
  urlParams.set(`item_${idx}name`, "不可能存在的物品");
  urlParams.set(
    `profile_data_item_${idx}_profile_data`,
    $(`input[name="profile_data_item_${idx}_profile_data"]`).val()
  );
  urlParams.set(
    `callback_js_code_item_${idx}_callback_js_code`,
    $(`input[name="callback_js_code_item_${idx}_callback_js_code"]`).val()
  );
  urlParams.set(`item_${idx}hero_class`, 0);
  urlParams.set(`item_${idx}hero_race`, 0);
  urlParams.set(`item_${idx}location`, "");
  urlParams.set(`item_${idx}unique`, "");
  urlParams.set(`item_${idx}bonus_attr`, "NULL");
  urlParams.set(`item_${idx}item_class`, 0);
  urlParams.set(`item_${idx}any_skill`, 0);
  urlParams.set(`item_${idx}skill`, "");
  urlParams.set(`item_${idx}any_skillclass`, 0);
  urlParams.set(`item_${idx}set`, 0);
  urlParams.set(`item_${idx}item_condition`, 0);
  urlParams.set(`item_${idx}sockets`, "NULL");
  urlParams.set(`item_${idx}item_conditionMax`, 6);
  urlParams.set(`item_${idx}usage_item`, "yes");
  urlParams.set(`item_${idx}hero_level_enabled_posted`, 1);
  urlParams.set(`item_${idx}hero_level`, 40);
  urlParams.set(`item_${idx}hero_level_stored`, 40);
  urlParams.set(`item_${idx}group_item`, "no");
  urlParams.set(`item_${idx}attribute_name`, "eff_at_st");
  urlParams.set(`item_${idx}attribute_value`, "");
  urlParams.set(`item_${idx}profile_id`, 0);
  urlParams.set(`item_${idx}is_open`, 1);
  urlParams.set(`dummy`, "");
  urlParams.set(`ITEMS_${pagePrefix}_PAGE[1]:`, 1);
  urlParams.set(`ITEMS_${pagePrefix}_JPNR[1]:`, 1);
  urlParams.set(`ITEMS_${pagePrefix}_PSNR[1]`, pageSize);
  urlParams.set(`ITEMS_${pagePrefix}_JPNR[2]:`, 1);
  urlParams.set(`ITEMS_${pagePrefix}_PSNR[2]`, pageSize);
  urlParams.set(`sellids`, itemIds.join(","));
  urlParams.set(`sellconfirm`, "   OK   ");

  const url =
    location.origin +
    "/wod/spiel/hero/items.php?is_popup=1&session_hero_id=" +
    shId;
  const response = await fetch(url, {
    headers: {
      accept:
        "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
      "content-type": "application/x-www-form-urlencoded",
    },
    body: urlParams.toString(),
    method: "POST",
  });
  const text = await response.text();
  return text;
}

const CLEAN_WARE_HOUSE_KEY = "cleanWareHouseSettings";
/**
 * 加载设置
 */
function loadCleanWarehouseSettings() {
  let settings = {};
  const textSettings = localStorage.getItem(CLEAN_WARE_HOUSE_KEY);
  if (!textSettings) {
    settings = JSON.parse(JSON.stringify(defaultMap));
  } else {
    settings = JSON.parse(textSettings);
  }
  return settings;
}