Greasy Fork 支持简体中文。

WoD 角色选择下拉框

Adds a hero selection dropdown at the top of all World of Dungeons pages. Not all skins are supported.

// ==UserScript==
// @name         WoD 角色选择下拉框
// @namespace    com.dobydigital.userscripts.wod
// @description  Adds a hero selection dropdown at the top of all World of Dungeons pages. Not all skins are supported.
// @author       XaeroDegreaz
// @home         https://github.com/XaeroDegreaz/world-of-dungeons-userscripts
// @supportUrl   https://github.com/XaeroDegreaz/world-of-dungeons-userscripts/issues
// @source       https://raw.githubusercontent.com/XaeroDegreaz/world-of-dungeons-userscripts/main/src/hero-switcher-dropdown.user.js
// @include      http*://*.world-of-dungeons.*/wod/spiel/*
// @include      http*://*.world-of-dungeons.*
// @icon         http://info.world-of-dungeons.net/wod/css/WOD.gif
// @require      https://gcore.jsdelivr.net/jquery/1.10.2/jquery.min.js
// @modifier     Christophero
// @version      2023.06.25.1
// ==/UserScript==

(function () {
  ("use strict");

  const targetElement = $("td.gadget_table_cell.merged");
  if (!targetElement.length) {
    console.error(
      `Hero Selector Dropdown Userscript: Unable to find target element for dropdown.`,
      targetElement
    );
    return;
  }
  const SESSION_HERO_ID_KEY = "session_hero_id";
  const GOTO_KEY = "goto";
  const HERO_LIST_STORAGE_KEY =
    "com.dobydigital.userscripts.wod.heroswitcherdropdown.herolist";
  const USERSCRIPT_CONTAINER_ID =
    "xaerodegreaz_userscript_hero_select_container";
  const DROPDOWN_ID = "xaerodegreaz_userscript_hero_select";

  const storage = window.localStorage;
  const heroList = load(HERO_LIST_STORAGE_KEY);

  (function Main() {
    try {
      if (!heroList) {
        refreshHeroList();
      } else {
        displayHeroSelector(heroList);
      }
    } catch (e) {
      console.error("Hero Selector Dropdown Userscript: Uncaught exception", e);
    }
  })();

  function load(key) {
    try {
      const raw = storage.getItem(HERO_LIST_STORAGE_KEY);
      return raw ? JSON.parse(raw) : undefined;
    } catch (e) {
      console.error(
        `Hero Selector Dropdown Userscript: Unable to load key:${key}`,
        e
      );
      return undefined;
    }
  }

  function save(key, value) {
    try {
      storage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error(
        `Hero Selector Dropdown Userscript: Unable to save key:${key}`,
        e
      );
    }
  }

  function refreshHeroList() {
    $(`div[id=${USERSCRIPT_CONTAINER_ID}]`).remove();
    fetch("/wod/spiel/settings/heroes.php")
      .then(function (res) {
        return res.text();
      })
      .then(function (text) {
        const jq = $(text);
        const inputs = jq.find("input[name=FIGUR]");
        if (!inputs.length) {
          console.error(
            'Hero Selector Dropdown Userscript: Unable to find hero list inputs on "heroes" page.',
            inputs
          );
          return;
        }
        const heroes = inputs
          .map(function () {
            const characterId = $(this).val();
            const characterName = $(this).parent().find("a").text();
            return { characterId, characterName };
          })
          .toArray();
        save(HERO_LIST_STORAGE_KEY, heroes);
        displayHeroSelector(heroes);
      });
  }

  function displayHeroSelector(heroes) {
    //# We could process the query parameters ahead of time, but, for a small amount of efficiency, I think it's best to defer so it can be done asynchronously.
    const rawVars = getUrlVars();
    //# We want to capture all query parameters so they can be passed when changing users. We don't want the session_hero_id, because we will be replacing that.
    const gotoFlag = rawVars[GOTO_KEY];
    const urlVars = rawVars
      .filter((key) => key !== SESSION_HERO_ID_KEY)
      .filter((key) => key != GOTO_KEY);
    let currentSessionHeroId = rawVars[SESSION_HERO_ID_KEY];
    //# Location without any query parameters
    let basePath = window.location.href.split("?")[0];
    if (window.location.pathname == "/") {
      basePath += "wod/spiel/news/news.php";
    }
    const remainingQueryParameters = urlVars
      .map((x) => `${x}=${rawVars[x]}`)
      .join("&");
    const querystring =
      urlVars.length > 0 ? `&${remainingQueryParameters}` : "";
    //# Begin generating new DOM elements.
    const newDiv = $(
      `<div id="${USERSCRIPT_CONTAINER_ID}" class="gadget"><label for="${DROPDOWN_ID}">切换英雄: </label></div>`
    );
    if (!currentSessionHeroId) {
      currentSessionHeroId = $('input[name="session_hero_id"]').val();
    }
    const index = heroes.findIndex(
      (hero) => hero.characterId == currentSessionHeroId
    );
    const addVal =
      gotoFlag == "prevactivehero" ? -1 : gotoFlag == "nextactivehero" ? 1 : 0;
    const realHeroId =
      heroes[(index + heroes.length + addVal) % heroes.length].characterId;
    const options = heroes
      .map((hero) => {
        const selected = hero.characterId === realHeroId ? "selected" : "";
        return `<option ${selected} value="${basePath}?${SESSION_HERO_ID_KEY}=${hero.characterId}${querystring}">${hero.characterName}</option>`;
      })
      .join("");
    const select = $(`<select id="${DROPDOWN_ID}">${options}</select>`).change(
      function () {
        window.location.href = $(this).val();
      }
    );
    const refreshButton = $(
      '<button class="button" title="Refresh Hero List">🗘</button>'
    ).click(function () {
      refreshHeroList();
    });
    newDiv.append(select, refreshButton);
    targetElement.prepend(newDiv);
  }

  function getHiddenInputValuesForSubPages() {
    return {
      //# items.php - cellar, treasure vault, etc
      view: $("input[type=hidden][name=view]").val(),
      //# trade.php - market, auctions, etc
      show: $("input[type=hidden][name=show]").val(),
    };
  }

  function getUrlVars() {
    const vars = [];
    const href = window.location.href;
    if (href.includes("?") && href.includes("&")) {
      const hashes = href
        .slice(window.location.href.indexOf("?") + 1)
        .split("&");
      for (let i = 0; i < hashes.length; i++) {
        const hash = hashes[i].split("=");
        vars.push(hash[0]);
        vars[hash[0]] = hash[1];
      }
    }

    //# After performing a search on any of the items pages (cellar, treasure vault), the 'view' query parameter goes missing, so switching to another character
    //# will redirect to the general storage. We want to be able to capture that view value from the form inputs, and set the 'view' query param
    //# if it isn't already present.
    //# We see the same behaviour on the market / auction pages, only the query param is 'show' instead of view.
    const subPages = getHiddenInputValuesForSubPages();
    Object.keys(subPages).map((key) => {
      if (subPages[key] && !vars[key]) {
        vars.push(key);
        vars[key] = subPages[key];
      }
    });
    return vars;
  }
})();