Showdown Randbats

Use the Showdown Randbats tooltip on your iPhone via an app like Stay. Or anywhere you use userscripts. Userscript version of https://github.com/pkmn/randbats

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name     Showdown Randbats
// @description Use the Showdown Randbats tooltip on your iPhone via an app like Stay. Or anywhere you use userscripts. Userscript version of https://github.com/pkmn/randbats
// @version  1.5.5
// @include *pokemonshowdown*
// @grant none
// @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
// @icon https://github.com/pkmn/randbats/blob/main/extension/32x32.png?raw=true
// @namespace https://greasyfork.org/users/1416567
// ==/UserScript==

// @updateURL todo
// @downloadURL todo

var DATA = {};

var SUPPORTED = [
  'gen9randombattle', 'gen9randomdoublesbattle', 'gen9babyrandombattle',
  'gen8randombattle', 'gen8randomdoublesbattle', 'gen8bdsprandombattle',
  'gen7randombattle', 'gen7letsgorandombattle', 'gen7randomdoublesbattle',
  'gen6randombattle', 'gen5randombattle', 'gen4randombattle', 'gen3randombattle',
  'gen2randombattle', 'gen1randombattle',
];

// Random Battle sets are generated based on battle-only forms which makes disambiguating sets
// difficult sometimes. We first try searching by level as sometimes this is sufficient to
// differentiate and then by base species - if there is only one set then we can return it.
// Otherwise, if the Pokémon is not the base forme and there is only one set for that forme we can
// return that. However, if the Pokémon is still in its base forme we return multiple (labelled)
// sets.

var TOOLTIP = undefined;
try { TOOLTIP = BattleTooltips.prototype.showPokemonTooltip; } catch {}
if (TOOLTIP) {
  for (var format of SUPPORTED) {
    (function (f) {
      var request = new XMLHttpRequest();
      request.addEventListener('load', function() {
        try {
          var data = {};
          var json = JSON.parse(request.responseText);
          for (var name in json) {
            var pokemon = json[name];
            // Zoroark has an actual level but the "Illusion Level Mod" means the server will lie
            // about its level making it difficult to find. Instead we special case things here and
            // below to always just set Zoroark's level to 0 for searching (the actual clientPokemon
            // level gets used for computing stats)
            if (name.startsWith('Zoroark')) pokemon.level = 0;
            data[pokemon.level] = data[pokemon.level] || {};
            // Dex.forGen not important here because we're not looking at stats
            var species = Dex.species.get(name);
            var id = toID(species.forme === 'Gmax'
              ? species.baseSpecies
              : species.battleOnly || species.name);
            data[pokemon.level][id] = data[pokemon.level][id] || [];
            data[pokemon.level][id].push(Object.assign({name: name}, pokemon));
          }
          DATA[f] = data;
        } catch (err) {
          console.error('Unable to load data for ' + f +
            ' - please check to see if your Pokémon Showdown Randbats Tooltip is up to date.');
        }
      });
      request.open('GET', 'https://pkmn.github.io/randbats/data/stats/' + f + '.json');
      request.send(null);
    })(format);
  }

  BattleTooltips.prototype.showPokemonTooltip = function (clientPokemon, serverPokemon) {
    var original = TOOLTIP.apply(this, arguments);
    if (!clientPokemon || serverPokemon) return original;

    var format = toID(this.battle.tier);
    if (!format || !format.includes('random')) return original;

    var gen = Number(format.charAt(3));
    var letsgo = format.includes('letsgo');
    var gameType = this.battle.gameType;

    var species = Dex.forGen(gen).species.get(
      clientPokemon.volatiles.formechange
      ? clientPokemon.volatiles.formechange[1]
      : clientPokemon.speciesForme);
    if (!species) return original;

    if (!['singles', 'doubles'].includes(gameType)) {
      format = 'gen' + gen + 'randomdoublesbattle';
    } else if (format.includes('monotype') || format.includes('unrated')) {
      format = 'gen' + gen + 'randombattle';
    } else if (format.endsWith('blitz')) {
      format = format.slice(0, -5);
    }
    if (!DATA[format]) return original;

    var data = DATA[format][species.baseSpecies === 'Zoroark' ? 0 : clientPokemon.level];
    if (!data) return original;

    var cosmetic = species.cosmeticFormes && species.cosmeticFormes.includes(species.name);
    var id = toID((species.forme === 'Gmax' || cosmetic)
      ? species.baseSpecies : species.battleOnly || species.name);
    if (id.startsWith('pikachu')) id = id.endsWith('gmax') ? 'pikachugmax' : 'pikachu';
    var forme = cosmetic ? species.baseSpecies : clientPokemon.speciesForme;
    if (forme.startsWith('Pikachu')) forme = forme.endsWith('Gmax') ? 'Pikachu-Gmax' : 'Pikachu';

    var d = data;
    data = data[id];
    if (!data) return original;

    if (id === 'greninja' && 'greninjabond' in d) {
      data = data.concat(d['greninjabond']);
    }

    if (data.length === 1) {
      data[0].level = clientPokemon.level;
      return original + displaySet(gen, gameType, letsgo, species, data[0], undefined, clientPokemon);
    }
    if (toID(forme) !== id) {
      var match = [];
      for (var set of data) {
        set.level = clientPokemon.level;
        if (set.name === forme) {
          match.push(displaySet(gen, gameType, letsgo, species, set, undefined, clientPokemon));
        }
      }
      if (match.length === 1) return original + match[0];
    }
    var buf = original;
    for (var set of data) {
      set.level = clientPokemon.level;
      // Technically different formes will have different base stats, but given at this stage
      // we're still in the base forme we simply use the base forme base stats for everything.
      buf += displaySet(gen, gameType, letsgo, species, set, set.name, clientPokemon);
    }
    return buf;
  }

  function displaySet(gen, gameType, letsgo, species, data, name, clientPokemon) {
    var noHP = true;
    if (data.moves) {
      for (var move in data.moves) {
        if (move.startsWith('Hidden Power')) {
          noHP = false;
          break;
        }
      }
    }

    var buf = '<div style="border-top: 1px solid #888; background: #dedede">';
    if (name) buf += '<p><b>' + name + '</b></p>';

    var multi = !['singles', 'doubles'].includes(gameType);
    if (data.roles) {
      var roles = filter(data.roles, clientPokemon);
      if (!roles.length) return '';
      var i = 0;
      for (var role of roles) {
        buf += (i == 0 ? '<div>' : '<div style="border-top: 1px solid #888;">');
        buf += '<p><span style="text-decoration: underline;">' + role[0] + '</span> ' +
          '<small>(' + Math.round(role[1].weight * 100) + '%)</small>';
          if (gen >= 3 && !letsgo) {
            buf += '<p><small>Abilities:</small> ' + display(role[1].abilities) + '</p>';
          }
          if (gen >= 2 && !(letsgo && !role[1].items)) {
            buf += '<p><small>Items:</small> ' +
              (role[1].items ? display(role[1].items) : '(No Item)') + '</p>';
          }
        if (gen === 9) {
          buf += '<p><small>Tera Types:</small> ' + display(role[1].teraTypes) + '</p>';
        }
        buf += '<p><small>Moves:</small> ' + display(role[1].moves, multi) + '</p>';
        buf += displayStats(gen, letsgo, species, role[1], data.level, noHP) + '</div>';
        i++;
      }
    } else {
      if (gen >= 3 && !letsgo) {
        buf += '<p><small>Abilities:</small> ' + display(data.abilities) + '</p>';
      }
      if (gen >= 2 && !(letsgo && !data.items)) {
        buf += '<p><small>Items:</small> ' +
          (data.items ? display(data.items) : '(No Item)') + '</p>';
      }
      buf += '<p><small>Moves:</small> ' + display(data.moves, multi) + '</p>';
      buf += displayStats(gen, letsgo, species, data, data.level, noHP);
    }

    buf += '</div>';
    return buf;
  }

  function displayStats(gen, letsgo, species, data, level, noHP) {
    var stats = {};
    for (var stat in species.baseStats) {
      stats[stat] = calc(
        gen,
        stat,
        species.baseStats[stat],
        'ivs' in data && stat in data.ivs ? data.ivs[stat] : (gen < 3 ? 30 : 31),
        'evs' in data && stat in data.evs ? data.evs[stat] : (gen < 3 ? 255 : letsgo ? 0 : 85),
        level,
        letsgo);
    }

    buf ='<p>';
    for (var statName of Dex.statNamesExceptHP) {
      if (gen === 1 && statName === 'spd') continue;
      var known = gen === 1 || (gen === 2 && noHP) ||
        ('ivs' in data && statName in data.ivs) || ('evs' in data && statName in data.evs);
      var statLabel = gen === 1 && statName === 'spa' ? 'spc' : statName;
      buf += statName === 'atk' ? '<small>' : '<small> / ';
      buf += '' + BattleText[statLabel].statShortName + '&nbsp;</small>';
      var italic = !known && (statName === 'atk' || statName === 'spe');
      buf += (italic ? '<i>' : '') + stats[statName] + (italic ? '</i>' : '');
    }
    buf += '</p>';
    return buf;
  }

  function compare(a, b) {
    return b[1] - a[1] || a[0].localeCompare(b[0]);
  }

  function filter(roles, clientPokemon) {
    var all = Object.entries(roles);
    if (!clientPokemon) return all;

    var possible = [];
    outer: for (var role of all) {
      if (clientPokemon.terastallized && !role[1].teraTypes[clientPokemon.terastallized]) continue;
      for (var moveslot of clientPokemon.moveTrack) {
        if (!role[1].moves[moveslot[0]] &&
            (moveslot[0] !== 'Hidden Power' || !hasHiddenPower(role[1].moves))) {
          continue outer;
        }
      }
      possible.push(role);
    }
    return possible;
  }

  function hasHiddenPower(moves) {
    for (var move in moves) {
      if (move.startsWith('Hidden Power')) return true;
    }
    return false;
  }

  function display(stats, multi) {
    var buf = [];
    for (var key in stats) {
      if (stats[key] === 0 || (multi && key === 'Ally Switch')) continue;
      buf.push(key + (stats[key] >= 1
        ? '' : ' <small>(' + Math.round(stats[key] * 100) + '%)</small>'));
    }
    return buf.join(', ');
  }

  function tr(num) {
    return num >>> 0
  }

  function calc(gen, stat, base, iv, ev, level, letsgo) {
    if (gen < 3) iv = Math.floor(iv / 2) * 2;
    if (stat === 'hp') {
      var val = base === 1 ? base : tr(tr(2 * base + iv + tr(ev / 4) + 100) * level / 100 + 10);
      return letsgo ? val + 20 : val;
    } else {
      var val = tr(tr(2 * base + iv + tr(ev / 4)) * level / 100 + 5);
      return letsgo ? tr(val * 102 / 100) + 20 : val;
    }
  }
}

  // {{{ changelog :

  // [2025-01-13 Thu] Hello

  // }}}

  // {{{ contact :

  // }}}