pokemon-showdown/tools.js
Guangcong Luo 3716f360ff TypeScript!
Currently TypeScript is validating tools.js and is not particularly
strict about anything and we use 'any' a lot and it's not part of
'npm test' yet, but everything has to start somewhere!

tools.js has also been refactored majorly to use accessors rather
than loader functions. This basically means you don't need to do
Tools.includeData() or anything like that anymore. The new system is
also easier to make TypeScript-compatible.

See #3278
2017-05-03 16:19:49 -07:00

1403 lines
43 KiB
JavaScript

/**
* Dex
* Pokemon Showdown - http://pokemonshowdown.com/
*
* Handles getting data about pokemon, items, etc.
*
* This file is used by basically every PS process. Sim processes use it
* to get game data for simulation, team validators use it to get data
* for validation, dexsearch uses it for dex data, and the main process
* uses it for format listing and miscellaneous dex lookup chat commands.
*
* It currently also contains our shims, since it has no dependencies and
* is included by nearly every process.
*
* By default, nothing is loaded until you call Dex.mod(mod) or
* Dex.format(format).
*
* You may choose to preload some things:
* - Dex.includeMods() ~10ms
* This will populate Dex.dexes, giving you a list of possible mods.
* Note that you don't need this for Dex.mod, Dex.mod will
* automatically populate this.
* - Dex.includeFormats() ~30ms
* As above, but will also populate Dex.formats, giving an object
* containing formats.
* - Dex.includeData() ~500ms
* As above, but will also populate all of Dex.data, giving access to
* the data access functions like Dex.getTemplate, Dex.getMove, etc.
* Note that you don't need this if you access the functions through
* Dex.mod(...).getTemplate, because Dex.mod automatically populates
* data for the relevant mod.
* - Dex.includeModData() ~1500ms
* As above, but will also populate Dex.dexes[...].data for all mods.
* Note that Dex.mod(...) will automatically populate .data, so use
* this only if you need to manually iterate Dex.dexes.
*
* Note that preloading is unnecessary. The getters for Dex.data etc
* will automatically load this data as needed.
*
* @license MIT license
*/
'use strict';
const fs = require('fs');
const path = require('path');
// shim Object.values
if (!Object.values) {
Object.values = function (object) {
let values = [];
for (let k in object) values.push(object[k]);
return values;
};
// @ts-ignore
Object.entries = function (object) {
let entries = [];
for (let k in object) entries.push([k, object[k]]);
return entries;
};
}
// shim padStart
// if (!String.prototype.padStart) {
// String.prototype.padStart = function padStart(maxLength, filler) {
// filler = filler || ' ';
// while (filler.length + this.length < maxLength) {
// filler += filler;
// }
// return filler.slice(0, maxLength - this.length) + this;
// };
// }
/** @type {{[mod: string]: BattleDex}} */
let dexes = {};
/** @typedef {'Pokedex' | 'FormatsData' | 'Learnsets' | 'Movedex' | 'Statuses' | 'TypeChart' | 'Scripts' | 'Items' | 'Abilities' | 'Natures' | 'Formats' | 'Aliases'} DataType */
const DATA_TYPES = ['Pokedex', 'FormatsData', 'Learnsets', 'Movedex', 'Statuses', 'TypeChart', 'Scripts', 'Items', 'Abilities', 'Natures', 'Formats', 'Aliases'];
const DATA_FILES = {
'Pokedex': 'pokedex',
'Movedex': 'moves',
'Statuses': 'statuses',
'TypeChart': 'typechart',
'Scripts': 'scripts',
'Items': 'items',
'Abilities': 'abilities',
'Formats': 'rulesets',
'FormatsData': 'formats-data',
'Learnsets': 'learnsets',
'Aliases': 'aliases',
'Natures': 'natures',
};
/** @typedef {{
id: string,
name: string,
}} DexTemplate */
/** @typedef {{Pokedex: object, Movedex: object, Statuses: object, TypeChart: object, Scripts: object, Items: object, Abilities: object, FormatsData: object, Learnsets: object, Aliases: object, Natures: object}} BattleDexData */
const BattleNatures = {
adamant: {name:"Adamant", plus:'atk', minus:'spa'},
bashful: {name:"Bashful"},
bold: {name:"Bold", plus:'def', minus:'atk'},
brave: {name:"Brave", plus:'atk', minus:'spe'},
calm: {name:"Calm", plus:'spd', minus:'atk'},
careful: {name:"Careful", plus:'spd', minus:'spa'},
docile: {name:"Docile"},
gentle: {name:"Gentle", plus:'spd', minus:'def'},
hardy: {name:"Hardy"},
hasty: {name:"Hasty", plus:'spe', minus:'def'},
impish: {name:"Impish", plus:'def', minus:'spa'},
jolly: {name:"Jolly", plus:'spe', minus:'spa'},
lax: {name:"Lax", plus:'def', minus:'spd'},
lonely: {name:"Lonely", plus:'atk', minus:'def'},
mild: {name:"Mild", plus:'spa', minus:'def'},
modest: {name:"Modest", plus:'spa', minus:'atk'},
naive: {name:"Naive", plus:'spe', minus:'spd'},
naughty: {name:"Naughty", plus:'atk', minus:'spd'},
quiet: {name:"Quiet", plus:'spa', minus:'spe'},
quirky: {name:"Quirky"},
rash: {name:"Rash", plus:'spa', minus:'spd'},
relaxed: {name:"Relaxed", plus:'def', minus:'spe'},
sassy: {name:"Sassy", plus:'spd', minus:'spe'},
serious: {name:"Serious"},
timid: {name:"Timid", plus:'spe', minus:'atk'},
};
function toId(text) {
// this is a duplicate of Dex.getId, for performance reasons
if (text && text.id) {
text = text.id;
} else if (text && text.userid) {
text = text.userid;
}
if (typeof text !== 'string' && typeof text !== 'number') return '';
return ('' + text).toLowerCase().replace(/[^a-z0-9]+/g, '');
}
class BattleDex {
/**
* @param {string=} mod
*/
constructor(mod = 'base') {
this.gen = 0;
this.name = "[BattleDex]";
this.isBase = (mod === 'base');
this.currentMod = mod;
this.parentMod = '';
/** @type {any} */
this.dataCache = null;
/** @type {any} */
this.formatsCache = null;
this.modsLoaded = false;
this.BattleDex = BattleDex;
}
get data() {
this.includeData();
return this.dataCache;
}
get formats() {
this.includeFormats();
return this.formatsCache;
}
get dexes() {
this.includeMods();
return dexes;
}
/**
* @param {string} mod
* @return {BattleDex}
*/
mod(mod) {
if (!dexes['base'].modsLoaded) dexes['base'].includeMods();
if (!mod) mod = 'base';
return dexes[mod];
}
format(format) {
if (!this.modsLoaded) this.includeMods();
const mod = this.getFormat(format).mod;
// TODO: change default format mod as gen7 becomes stable
if (!mod) return dexes['gen6'];
return dexes[mod];
}
/**
* @param {DataType} dataType
* @param {string} id
*/
modData(dataType, id) {
if (this.isBase) return this.data[dataType][id];
if (this.data[dataType][id] !== dexes[this.parentMod].data[dataType][id]) return this.data[dataType][id];
return (this.data[dataType][id] = this.deepClone(this.data[dataType][id]));
}
effectToString() {
return this.name;
}
/**
* Safely converts the passed variable into a string. Unlike '' + str,
* String(str), or str.toString(), Tools.getString is guaranteed not to
* crash.
*
* The other methods of casting to string can crash if str.toString crashes
* or isn't a function. Instead, Tools.getString simply returns '' if the
* passed variable isn't a string or a number.
*
* @param {any} str
* @return {string}
*/
getString(str) {
if (typeof str === 'string' || typeof str === 'number') return '' + str;
return '';
}
/**
* Sanitizes a username or Pokemon nickname
*
* Returns the passed name, sanitized for safe use as a name in the PS
* protocol.
*
* Such a string must uphold these guarantees:
* - must not contain any ASCII whitespace character other than a space
* - must not start or end with a space character
* - must not contain any of: | , [ ]
* - must not be the empty string
* - must not contain Unicode RTL control characters
*
* If no such string can be found, returns the empty string. Calling
* functions are expected to check for that condition and deal with it
* accordingly.
*
* getName also enforces that there are not multiple consecutive space
* characters in the name, although this is not strictly necessary for
* safety.
*
* @param {any} name
* @return {string}
*/
getName(name) {
if (typeof name !== 'string' && typeof name !== 'number') return '';
name = ('' + name).replace(/[\|\s\[\]\,\u202e]+/g, ' ').trim();
if (name.length > 18) name = name.substr(0, 18).trim();
// remove zalgo
name = name.replace(/[\u0300-\u036f\u0483-\u0489\u0610-\u0615\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06ED\u0E31\u0E34-\u0E3A\u0E47-\u0E4E]{3,}/g, '');
name = name.replace(/[\u239b-\u23b9]/g, '');
return name;
}
/**
* Converts anything to an ID. An ID must have only lowercase alphanumeric
* characters.
* If a string is passed, it will be converted to lowercase and
* non-alphanumeric characters will be stripped.
* If an object with an ID is passed, its ID will be returned.
* Otherwise, an empty string will be returned.
*
* Tools.getId is generally assigned to the global toId, because of how
* commonly it's used.
*
* @param {any} text
* @return {string}
*/
getId(text) {
if (text && text.id) {
text = text.id;
} else if (text && text.userid) {
text = text.userid;
}
if (typeof text !== 'string' && typeof text !== 'number') return '';
return ('' + text).toLowerCase().replace(/[^a-z0-9]+/g, '');
}
/**
* returns false if the target is immune; true otherwise
*
* also checks immunity to some statuses
*/
getImmunity(source, target) {
let sourceType = source.type || source;
let targetTyping = target.getTypes && target.getTypes() || target.types || target;
if (Array.isArray(targetTyping)) {
for (let i = 0; i < targetTyping.length; i++) {
if (!this.getImmunity(sourceType, targetTyping[i])) return false;
}
return true;
}
let typeData = this.data.TypeChart[targetTyping];
if (typeData && typeData.damageTaken[sourceType] === 3) return false;
return true;
}
getEffectiveness(source, target) {
let sourceType = source.type || source;
let totalTypeMod = 0;
let targetTyping = target.getTypes && target.getTypes() || target.types || target;
if (Array.isArray(targetTyping)) {
for (let i = 0; i < targetTyping.length; i++) {
totalTypeMod += this.getEffectiveness(sourceType, targetTyping[i]);
}
return totalTypeMod;
}
let typeData = this.data.TypeChart[targetTyping];
if (!typeData) return 0;
switch (typeData.damageTaken[sourceType]) {
case 1: return 1; // super-effective
case 2: return -1; // resist
// in case of weird situations like Gravity, immunity is
// handled elsewhere
default: return 0;
}
}
/**
* Convert a pokemon name, ID, or template into its species name, preserving
* form name (which is the main way Tools.getSpecies(id) differs from
* Tools.getTemplate(id).species).
*
* @param {string|DexTemplate} species
* @return {string}
*/
getSpecies(species) {
let id = toId(species || '');
let template = this.getTemplate(id);
if (template.otherForms && template.otherForms.indexOf(id) >= 0) {
let form = id.slice(template.species.length);
species = template.species + '-' + form[0].toUpperCase() + form.slice(1);
} else {
species = template.species;
}
return species;
}
getTemplate(template) {
if (!template || typeof template === 'string') {
let name = (template || '').trim();
let id = toId(name);
if (id === 'nidoran' && name.slice(-1) === '♀') {
id = 'nidoranf';
} else if (id === 'nidoran' && name.slice(-1) === '♂') {
id = 'nidoranm';
}
template = this.data.TemplateCache.get(id);
if (template) return template;
if (this.data.Aliases.hasOwnProperty(id)) {
template = this.getTemplate(this.data.Aliases[id]);
if (template) {
this.data.TemplateCache.set(id, template);
}
return template;
}
if (!this.data.Pokedex.hasOwnProperty(id)) {
let aliasTo = '';
if (id.startsWith('mega') && this.data.Pokedex[id.slice(4) + 'mega']) {
aliasTo = id.slice(4) + 'mega';
} else if (id.startsWith('m') && this.data.Pokedex[id.slice(1) + 'mega']) {
aliasTo = id.slice(1) + 'mega';
} else if (id.startsWith('primal') && this.data.Pokedex[id.slice(6) + 'primal']) {
aliasTo = id.slice(6) + 'primal';
} else if (id.startsWith('p') && this.data.Pokedex[id.slice(1) + 'primal']) {
aliasTo = id.slice(1) + 'primal';
}
if (aliasTo) {
template = this.getTemplate(aliasTo);
if (template.exists) {
this.data.TemplateCache.set(id, template);
return template;
}
}
}
if (id && this.data.Pokedex.hasOwnProperty(id)) {
template = this.deepClone(this.data.Pokedex[id]);
template.exists = true;
} else {
template = {};
}
name = template.species || template.name || name;
if (this.data.FormatsData[id]) {
Object.assign(template, this.data.FormatsData[id]);
}
if (this.data.Learnsets[id]) {
Object.assign(template, this.data.Learnsets[id]);
}
if (!template.id) template.id = id;
if (!template.name) template.name = name;
if (!template.speciesid) template.speciesid = id;
if (!template.species) template.species = name;
if (!template.baseSpecies) template.baseSpecies = name;
if (!template.forme) template.forme = '';
if (!template.formeLetter) template.formeLetter = '';
if (!template.spriteid) template.spriteid = toId(template.baseSpecies) + (template.baseSpecies !== name ? '-' + toId(template.forme) : '');
if (!template.prevo) template.prevo = '';
if (!template.evos) template.evos = [];
if (!template.nfe) template.nfe = !!template.evos.length;
if (!template.eggGroups) template.eggGroups = [];
if (!template.gender) template.gender = '';
if (!template.genderRatio && template.gender === 'M') template.genderRatio = {M:1, F:0};
if (!template.genderRatio && template.gender === 'F') template.genderRatio = {M:0, F:1};
if (!template.genderRatio && template.gender === 'N') template.genderRatio = {M:0, F:0};
if (!template.genderRatio) template.genderRatio = {M:0.5, F:0.5};
if (!template.tier && template.baseSpecies !== template.species) template.tier = this.data.FormatsData[toId(template.baseSpecies)].tier;
if (!template.requiredItems && template.requiredItem) template.requiredItems = [template.requiredItem];
if (!template.tier) template.tier = 'Illegal';
if (!template.gen) {
if (template.num >= 722 || template.forme === 'Alola') {
template.gen = 7;
} else if (template.forme && template.forme in {'Mega':1, 'Mega-X':1, 'Mega-Y':1}) {
template.gen = 6;
template.isMega = true;
template.battleOnly = true;
} else if (template.forme === 'Primal') {
template.gen = 6;
template.isPrimal = true;
template.battleOnly = true;
} else if (template.num >= 650) {
template.gen = 6;
} else if (template.num >= 494) {
template.gen = 5;
} else if (template.num >= 387) {
template.gen = 4;
} else if (template.num >= 252) {
template.gen = 3;
} else if (template.num >= 152) {
template.gen = 2;
} else if (template.num >= 1) {
template.gen = 1;
} else {
template.gen = 0;
}
}
if (template.exists) this.data.TemplateCache.set(id, template);
}
return template;
}
getLearnset(template) {
const id = toId(template);
if (!this.data.Learnsets[id]) return null;
return this.data.Learnsets[id].learnset;
}
getMove(move) {
if (!move || typeof move === 'string') {
let name = (move || '').trim();
let id = toId(name);
move = this.data.MoveCache.get(id);
if (move) return move;
if (this.data.Aliases.hasOwnProperty(id)) {
move = this.getMove(this.data.Aliases[id]);
if (move.exists) {
this.data.MoveCache.set(id, move);
}
return move;
}
if (id.substr(0, 11) === 'hiddenpower') {
let matches = /([a-z]*)([0-9]*)/.exec(id);
// @ts-ignore
id = matches[1];
}
if (id && this.data.Movedex.hasOwnProperty(id)) {
move = this.deepClone(this.data.Movedex[id]);
move.exists = true;
} else {
move = {};
}
if (!move.id) move.id = id;
if (!move.name) move.name = name;
if (!move.fullname) move.fullname = 'move: ' + move.name;
move.toString = this.effectToString;
if (!move.critRatio) move.critRatio = 1;
if (!move.baseType) move.baseType = move.type;
if (!move.effectType) move.effectType = 'Move';
if (!move.secondaries && move.secondary) move.secondaries = [move.secondary];
if (!move.gen) {
if (move.num >= 622) {
move.gen = 7;
} else if (move.num >= 560) {
move.gen = 6;
} else if (move.num >= 468) {
move.gen = 5;
} else if (move.num >= 355) {
move.gen = 4;
} else if (move.num >= 252) {
move.gen = 3;
} else if (move.num >= 166) {
move.gen = 2;
} else if (move.num >= 1) {
move.gen = 1;
} else {
move.gen = 0;
}
}
if (!move.priority) move.priority = 0;
if (move.ignoreImmunity === undefined) move.ignoreImmunity = (move.category === 'Status');
if (!move.flags) move.flags = {};
if (move.exists) this.data.MoveCache.set(id, move);
}
return move;
}
/**
* Ensure we're working on a copy of a move (and make a copy if we aren't)
*
* Remember: "ensure" - by default, it won't make a copy of a copy:
* moveCopy === Tools.getMoveCopy(moveCopy)
*
* If you really want to, use:
* moveCopyCopy = Tools.getMoveCopy(moveCopy.id)
*
* @param move Move ID, move object, or movecopy object describing move to copy
* @return movecopy object
*/
getMoveCopy(move) {
if (move && move.isCopy) return move;
move = this.getMove(move);
let moveCopy = this.deepClone(move);
moveCopy.isCopy = true;
return moveCopy;
}
getEffect(effect) {
if (!effect || typeof effect === 'string') {
let name = (effect || '').trim();
let id = toId(name);
effect = {};
if (id && this.data.Statuses[id]) {
effect = this.data.Statuses[id];
effect.name = effect.name || this.data.Statuses[id].name;
} else if (id && this.data.Movedex[id] && this.data.Movedex[id].effect) {
effect = this.data.Movedex[id].effect;
effect.name = effect.name || this.data.Movedex[id].name;
} else if (id && this.data.Abilities[id] && this.data.Abilities[id].effect) {
effect = this.data.Abilities[id].effect;
effect.name = effect.name || this.data.Abilities[id].name;
} else if (id && this.data.Items[id] && this.data.Items[id].effect) {
effect = this.data.Items[id].effect;
effect.name = effect.name || this.data.Items[id].name;
} else if (id && this.data.Formats[id]) {
effect = this.data.Formats[id];
effect.name = effect.name || this.data.Formats[id].name;
if (!effect.mod) effect.mod = 'gen6';
if (!effect.effectType) effect.effectType = 'Format';
} else if (id === 'recoil') {
effect = {
effectType: 'Recoil',
};
} else if (id === 'drain') {
effect = {
effectType: 'Drain',
};
}
if (!effect.id) effect.id = id;
if (!effect.name) effect.name = name;
if (!effect.fullname) effect.fullname = effect.name;
effect.toString = this.effectToString;
if (!effect.category) effect.category = 'Effect';
if (!effect.effectType) effect.effectType = 'Effect';
}
return effect;
}
getFormat(effect) {
if (!effect || typeof effect === 'string') {
let name = (effect || '').trim();
let id = toId(name);
if (this.data.Aliases[id]) {
name = this.data.Aliases[id];
id = toId(name);
}
effect = {};
if (this.data.Formats.hasOwnProperty(id)) {
effect = this.data.Formats[id];
if (effect.cached) return effect;
effect.cached = true;
effect.exists = true;
effect.name = effect.name || this.data.Formats[id].name;
if (!effect.mod) effect.mod = 'gen6';
if (!effect.effectType) effect.effectType = 'Format';
}
if (!effect.id) effect.id = id;
if (!effect.name) effect.name = name;
if (!effect.fullname) effect.fullname = effect.name;
effect.toString = this.effectToString;
if (!effect.category) effect.category = 'Effect';
if (!effect.effectType) effect.effectType = 'Effect';
}
return effect;
}
getItem(item) {
if (!item || typeof item === 'string') {
let name = (item || '').trim();
let id = toId(name);
item = this.data.ItemCache.get(id);
if (item) return item;
if (this.data.Aliases.hasOwnProperty(id)) {
item = this.getItem(this.data.Aliases[id]);
if (item.exists) {
this.data.ItemCache.set(id, item);
}
return item;
}
if (id && !this.data.Items[id] && this.data.Items[id + 'berry']) {
item = this.getItem(id + 'berry');
this.data.ItemCache.set(id, item);
return item;
}
if (id && this.data.Items.hasOwnProperty(id)) {
item = this.data.Items[id];
item.exists = true;
} else {
item = {};
}
if (!item.id) item.id = id;
if (!item.name) item.name = name;
if (!item.fullname) item.fullname = 'item: ' + item.name;
item.toString = this.effectToString;
if (!item.category) item.category = 'Effect';
if (!item.effectType) item.effectType = 'Item';
if (item.isBerry) item.fling = {basePower: 10};
if (item.id.endsWith('plate')) item.fling = {basePower: 90};
if (item.onDrive) item.fling = {basePower: 70};
if (item.megaStone) item.fling = {basePower: 80};
if (item.onMemory) item.fling = {basePower: 50};
if (!item.gen) {
if (item.num >= 689) {
item.gen = 7;
} else if (item.num >= 577) {
item.gen = 6;
} else if (item.num >= 537) {
item.gen = 5;
} else if (item.num >= 377) {
item.gen = 4;
} else {
item.gen = 3;
}
// Due to difference in storing items, gen 2 items must be specified manually
}
if (item.exists) this.data.ItemCache.set(id, item);
}
return item;
}
getAbility(ability) {
if (!ability || typeof ability === 'string') {
let name = (ability || '').trim();
let id = toId(name);
ability = this.data.AbilityCache.get(id);
if (ability) return ability;
if (id && this.data.Abilities.hasOwnProperty(id)) {
ability = this.data.Abilities[id];
if (ability.cached) return ability;
ability.cached = true;
ability.exists = true;
} else {
ability = {};
}
if (!ability.id) ability.id = id;
if (!ability.name) ability.name = name;
if (!ability.fullname) ability.fullname = 'ability: ' + ability.name;
ability.toString = this.effectToString;
if (!ability.category) ability.category = 'Effect';
if (!ability.effectType) ability.effectType = 'Ability';
if (!ability.gen) {
if (ability.num >= 192) {
ability.gen = 7;
} else if (ability.num >= 165) {
ability.gen = 6;
} else if (ability.num >= 124) {
ability.gen = 5;
} else if (ability.num >= 77) {
ability.gen = 4;
} else if (ability.num >= 1) {
ability.gen = 3;
} else {
ability.gen = 0;
}
}
if (ability.exists) this.data.AbilityCache.set(id, ability);
}
return ability;
}
getType(type) {
if (!type || typeof type === 'string') {
let id = toId(type);
id = id.charAt(0).toUpperCase() + id.substr(1);
type = {};
if (id && id !== 'constructor' && this.data.TypeChart[id]) {
type = this.data.TypeChart[id];
if (type.cached) return type;
type.cached = true;
type.exists = true;
type.isType = true;
type.effectType = 'Type';
}
if (!type.id) type.id = id;
if (!type.effectType) {
// man, this is really meta
type.effectType = 'EffectType';
}
}
return type;
}
getNature(nature) {
if (!nature || typeof nature === 'string') {
let name = (nature || '').trim();
let id = toId(name);
nature = {};
if (id && id !== 'constructor' && this.data.Natures[id]) {
nature = this.data.Natures[id];
if (nature.cached) return nature;
nature.cached = true;
nature.exists = true;
}
if (!nature.id) nature.id = id;
if (!nature.name) nature.name = name;
nature.toString = this.effectToString;
if (!nature.effectType) nature.effectType = 'Nature';
}
return nature;
}
spreadModify(stats, set) {
const modStats = {atk:10, def:10, spa:10, spd:10, spe:10};
for (let statName in modStats) {
let stat = stats[statName];
modStats[statName] = Math.floor(Math.floor(2 * stat + set.ivs[statName] + Math.floor(set.evs[statName] / 4)) * set.level / 100 + 5);
}
if ('hp' in stats) {
let stat = stats['hp'];
modStats['hp'] = Math.floor(Math.floor(2 * stat + set.ivs['hp'] + Math.floor(set.evs['hp'] / 4) + 100) * set.level / 100 + 10);
}
return this.natureModify(modStats, set.nature);
}
natureModify(stats, nature) {
nature = this.getNature(nature);
if (nature.plus) stats[nature.plus] = Math.floor(stats[nature.plus] * 1.1);
if (nature.minus) stats[nature.minus] = Math.floor(stats[nature.minus] * 0.9);
return stats;
}
getHiddenPower(ivs) {
const hpTypes = ['Fighting', 'Flying', 'Poison', 'Ground', 'Rock', 'Bug', 'Ghost', 'Steel', 'Fire', 'Water', 'Grass', 'Electric', 'Psychic', 'Ice', 'Dragon', 'Dark'];
const stats = {hp: 31, atk: 31, def: 31, spe: 31, spa: 31, spd: 31};
if (this.gen <= 2) {
// Gen 2 specific Hidden Power check. IVs are still treated 0-31 so we get them 0-15
const atkDV = Math.floor(ivs.atk / 2);
const defDV = Math.floor(ivs.def / 2);
const speDV = Math.floor(ivs.spe / 2);
const spcDV = Math.floor(ivs.spa / 2);
return {
type: hpTypes[4 * (atkDV % 4) + (defDV % 4)],
power: Math.floor((5 * ((spcDV >> 3) + (2 * (speDV >> 3)) + (4 * (defDV >> 3)) + (8 * (atkDV >> 3))) + (spcDV > 2 ? 3 : spcDV)) / 2 + 31),
};
} else {
// Hidden Power check for gen 3 onwards
let hpTypeX = 0, hpPowerX = 0;
let i = 1;
for (const s in stats) {
hpTypeX += i * (ivs[s] % 2);
hpPowerX += i * (Math.floor(ivs[s] / 2) % 2);
i *= 2;
}
return {
type: hpTypes[Math.floor(hpTypeX * 15 / 63)],
// In Gen 6, Hidden Power is always 60 base power
power: (this.gen && this.gen < 6) ? Math.floor(hpPowerX * 40 / 63) + 30 : 60,
};
}
}
getBanlistTable(format, subformat, depth) {
let banlistTable;
if (!depth) depth = 0;
if (depth > 8) return; // avoid infinite recursion
if (format.banlistTable && !subformat) {
banlistTable = format.banlistTable;
} else {
if (!format.banlistTable) format.banlistTable = {};
if (!format.setBanTable) format.setBanTable = [];
if (!format.teamBanTable) format.teamBanTable = [];
if (!format.teamLimitTable) format.teamLimitTable = [];
banlistTable = format.banlistTable;
if (!subformat) subformat = format;
if (subformat.unbanlist) {
for (let i = 0; i < subformat.unbanlist.length; i++) {
banlistTable[subformat.unbanlist[i]] = false;
banlistTable[toId(subformat.unbanlist[i])] = false;
}
}
if (subformat.banlist) {
for (let i = 0; i < subformat.banlist.length; i++) {
// don't revalidate what we already validate
if (banlistTable[toId(subformat.banlist[i])] !== undefined) continue;
banlistTable[subformat.banlist[i]] = subformat.name || true;
banlistTable[toId(subformat.banlist[i])] = subformat.name || true;
let complexList;
if (subformat.banlist[i].includes('>')) {
complexList = subformat.banlist[i].split('>');
let limit = parseInt(complexList[1]);
let banlist = complexList[0].trim();
complexList = banlist.split('+').map(toId);
complexList.unshift(banlist, subformat.name, limit);
format.teamLimitTable.push(complexList);
} else if (subformat.banlist[i].includes('+')) {
if (subformat.banlist[i].includes('++')) {
complexList = subformat.banlist[i].split('++');
let banlist = complexList.join('+');
for (let j = 0; j < complexList.length; j++) {
complexList[j] = toId(complexList[j]);
}
complexList.unshift(banlist);
format.teamBanTable.push(complexList);
} else {
complexList = subformat.banlist[i].split('+');
for (let j = 0; j < complexList.length; j++) {
complexList[j] = toId(complexList[j]);
}
complexList.unshift(subformat.banlist[i]);
format.setBanTable.push(complexList);
}
}
}
}
if (subformat.ruleset) {
for (let i = 0; i < subformat.ruleset.length; i++) {
// don't revalidate what we already validate
if (banlistTable['Rule:' + toId(subformat.ruleset[i])] !== undefined) continue;
banlistTable['Rule:' + toId(subformat.ruleset[i])] = subformat.ruleset[i];
if (!format.ruleset.includes(subformat.ruleset[i])) format.ruleset.push(subformat.ruleset[i]);
let subsubformat = this.getFormat(subformat.ruleset[i]);
if (subsubformat.ruleset || subsubformat.banlist) {
this.getBanlistTable(format, subsubformat, depth + 1);
}
}
}
}
return banlistTable;
}
shuffle(arr) {
// In-place shuffle by Fisher-Yates algorithm
for (let i = arr.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
/**
* @param {string} s - string 1
* @param {string} t - string 2
* @param {number} l - limit
*/
levenshtein(s, t, l) {
// Original levenshtein distance function by James Westgate, turned out to be the fastest
/** @type {number[][]} */
let d = [];
// Step 1
let n = s.length;
let m = t.length;
if (n === 0) return m;
if (m === 0) return n;
if (l && Math.abs(m - n) > l) return Math.abs(m - n);
// Create an array of arrays in javascript (a descending loop is quicker)
for (let i = n; i >= 0; i--) d[i] = [];
// Step 2
for (let i = n; i >= 0; i--) d[i][0] = i;
for (let j = m; j >= 0; j--) d[0][j] = j;
// Step 3
for (let i = 1; i <= n; i++) {
let s_i = s.charAt(i - 1);
// Step 4
for (let j = 1; j <= m; j++) {
// Check the jagged ld total so far
if (i === j && d[i][j] > 4) return n;
let t_j = t.charAt(j - 1);
let cost = (s_i === t_j) ? 0 : 1; // Step 5
// Calculate the minimum
let mi = d[i - 1][j] + 1;
let b = d[i][j - 1] + 1;
let c = d[i - 1][j - 1] + cost;
if (b < mi) mi = b;
if (c < mi) mi = c;
d[i][j] = mi; // Step 6
}
}
// Step 7
return d[n][m];
}
clampIntRange(num, min, max) {
if (typeof num !== 'number') num = 0;
num = Math.floor(num);
if (num < min) num = min;
if (max !== undefined && num > max) num = max;
return num;
}
dataSearch(target, searchIn, isInexact) {
if (!target) {
return false;
}
searchIn = searchIn || ['Pokedex', 'Movedex', 'Abilities', 'Items', 'Natures'];
let searchFunctions = {Pokedex: 'getTemplate', Movedex: 'getMove', Abilities: 'getAbility', Items: 'getItem', Natures: 'getNature'};
let searchTypes = {Pokedex: 'pokemon', Movedex: 'move', Abilities: 'ability', Items: 'item', Natures: 'nature'};
let searchResults = [];
for (let i = 0; i < searchIn.length; i++) {
let res = this[searchFunctions[searchIn[i]]](target);
if (res.exists) {
searchResults.push({
exactMatch: !isInexact,
searchType: searchTypes[searchIn[i]],
name: res.name,
});
}
}
if (searchResults.length) {
return searchResults;
}
let cmpTarget = target.toLowerCase();
let maxLd = 3;
let fuzzyResults = [];
if (cmpTarget.length <= 1) {
return false;
} else if (cmpTarget.length <= 4) {
maxLd = 1;
} else if (cmpTarget.length <= 6) {
maxLd = 2;
}
for (let i = 0; i < searchIn.length; i++) {
let searchObj = this.data[searchIn[i]];
if (!searchObj) {
continue;
}
for (let j in searchObj) {
let word = searchObj[j];
if (typeof word === "object") {
word = word.name || word.species;
}
if (!word) {
continue;
}
let ld = this.levenshtein(cmpTarget, word.toLowerCase(), maxLd);
if (ld <= maxLd) {
fuzzyResults.push({word: word, ld: ld});
}
}
}
if (fuzzyResults.length) {
/** @type {any} */
let newTarget = "";
let newLD = 10;
for (let i = 0, l = fuzzyResults.length; i < l; i++) {
if (fuzzyResults[i].ld < newLD) {
newTarget = fuzzyResults[i];
newLD = fuzzyResults[i].ld;
}
}
// To make sure we aren't in an infinite loop...
if (cmpTarget !== newTarget.word) {
return this.dataSearch(newTarget.word, null, true);
}
}
return false;
}
packTeam(team) {
if (!team) return '';
let buf = '';
for (let i = 0; i < team.length; i++) {
let set = team[i];
if (buf) buf += ']';
// name
buf += (set.name || set.species);
// species
let id = toId(set.species || set.name);
buf += '|' + (toId(set.name || set.species) === id ? '' : id);
// item
buf += '|' + toId(set.item);
// ability
let template = dexes['base'].getTemplate(set.species || set.name);
let abilities = template.abilities;
id = toId(set.ability);
if (abilities) {
if (id === toId(abilities['0'])) {
buf += '|';
} else if (id === toId(abilities['1'])) {
buf += '|1';
} else if (id === toId(abilities['H'])) {
buf += '|H';
} else {
buf += '|' + id;
}
} else {
buf += '|' + id;
}
// moves
buf += '|' + set.moves.map(toId).join(',');
// nature
buf += '|' + set.nature;
// evs
let evs = '|';
if (set.evs) {
evs = '|' + (set.evs['hp'] || '') + ',' + (set.evs['atk'] || '') + ',' + (set.evs['def'] || '') + ',' + (set.evs['spa'] || '') + ',' + (set.evs['spd'] || '') + ',' + (set.evs['spe'] || '');
}
if (evs === '|,,,,,') {
buf += '|';
} else {
buf += evs;
}
// gender
if (set.gender && set.gender !== template.gender) {
buf += '|' + set.gender;
} else {
buf += '|';
}
// ivs
let ivs = '|';
if (set.ivs) {
ivs = '|' + (set.ivs['hp'] === 31 || set.ivs['hp'] === undefined ? '' : set.ivs['hp']) + ',' + (set.ivs['atk'] === 31 || set.ivs['atk'] === undefined ? '' : set.ivs['atk']) + ',' + (set.ivs['def'] === 31 || set.ivs['def'] === undefined ? '' : set.ivs['def']) + ',' + (set.ivs['spa'] === 31 || set.ivs['spa'] === undefined ? '' : set.ivs['spa']) + ',' + (set.ivs['spd'] === 31 || set.ivs['spd'] === undefined ? '' : set.ivs['spd']) + ',' + (set.ivs['spe'] === 31 || set.ivs['spe'] === undefined ? '' : set.ivs['spe']);
}
if (ivs === '|,,,,,') {
buf += '|';
} else {
buf += ivs;
}
// shiny
if (set.shiny) {
buf += '|S';
} else {
buf += '|';
}
// level
if (set.level && set.level !== 100) {
buf += '|' + set.level;
} else {
buf += '|';
}
// happiness
if (set.happiness !== undefined && set.happiness !== 255) {
buf += '|' + set.happiness;
} else {
buf += '|';
}
if (set.pokeball || set.hpType) {
buf += ',' + set.hpType;
buf += ',' + toId(set.pokeball);
}
}
return buf;
}
fastUnpackTeam(buf) {
if (!buf) return null;
let team = [];
let i = 0, j = 0;
// limit to 24
for (let count = 0; count < 24; count++) {
let set = {};
team.push(set);
// name
j = buf.indexOf('|', i);
if (j < 0) return;
set.name = buf.substring(i, j);
i = j + 1;
// species
j = buf.indexOf('|', i);
if (j < 0) return;
set.species = buf.substring(i, j) || set.name;
i = j + 1;
// item
j = buf.indexOf('|', i);
if (j < 0) return;
set.item = buf.substring(i, j);
i = j + 1;
// ability
j = buf.indexOf('|', i);
if (j < 0) return;
let ability = buf.substring(i, j);
let template = dexes['base'].getTemplate(set.species);
set.ability = (template.abilities && ability in {'':1, 0:1, 1:1, H:1} ? template.abilities[ability || '0'] : ability);
i = j + 1;
// moves
j = buf.indexOf('|', i);
if (j < 0) return;
set.moves = buf.substring(i, j).split(',', 24);
i = j + 1;
// nature
j = buf.indexOf('|', i);
if (j < 0) return;
set.nature = buf.substring(i, j);
i = j + 1;
// evs
j = buf.indexOf('|', i);
if (j < 0) return;
if (j !== i) {
let evs = buf.substring(i, j).split(',', 6);
set.evs = {
hp: Number(evs[0]) || 0,
atk: Number(evs[1]) || 0,
def: Number(evs[2]) || 0,
spa: Number(evs[3]) || 0,
spd: Number(evs[4]) || 0,
spe: Number(evs[5]) || 0,
};
}
i = j + 1;
// gender
j = buf.indexOf('|', i);
if (j < 0) return;
if (i !== j) set.gender = buf.substring(i, j);
i = j + 1;
// ivs
j = buf.indexOf('|', i);
if (j < 0) return;
if (j !== i) {
let ivs = buf.substring(i, j).split(',', 6);
set.ivs = {
hp: ivs[0] === '' ? 31 : Number(ivs[0]) || 0,
atk: ivs[1] === '' ? 31 : Number(ivs[1]) || 0,
def: ivs[2] === '' ? 31 : Number(ivs[2]) || 0,
spa: ivs[3] === '' ? 31 : Number(ivs[3]) || 0,
spd: ivs[4] === '' ? 31 : Number(ivs[4]) || 0,
spe: ivs[5] === '' ? 31 : Number(ivs[5]) || 0,
};
}
i = j + 1;
// shiny
j = buf.indexOf('|', i);
if (j < 0) return;
if (i !== j) set.shiny = true;
i = j + 1;
// level
j = buf.indexOf('|', i);
if (j < 0) return;
if (i !== j) set.level = parseInt(buf.substring(i, j));
i = j + 1;
// happiness
j = buf.indexOf(']', i);
let misc;
if (j < 0) {
if (i < buf.length) misc = buf.substring(i).split(',', 3);
} else {
if (i !== j) misc = buf.substring(i, j).split(',', 3);
}
if (misc) {
set.happiness = (misc[0] ? Number(misc[0]) : 255);
set.hpType = misc[1];
set.pokeball = misc[2];
}
if (j < 0) break;
i = j + 1;
}
return team;
}
deepClone(obj) {
if (typeof obj === 'function') return obj;
if (obj === null || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) return obj.map(prop => this.deepClone(prop));
const clone = Object.create(Object.getPrototypeOf(obj));
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
clone[keys[i]] = this.deepClone(obj[keys[i]]);
}
return clone;
}
loadDataFile(basePath, dataType) {
try {
const filePath = basePath + DATA_FILES[dataType];
const dataObject = require(filePath);
const key = `Battle${dataType}`;
if (!dataObject || typeof dataObject !== 'object') return new TypeError(`${filePath}, if it exists, must export a non-null object`);
if (!dataObject[key] || typeof dataObject[key] !== 'object') return new TypeError(`${filePath}, if it exists, must export an object whose '${key}' property is a non-null object`);
return dataObject[key];
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
}
return {};
}
includeMods() {
if (!this.isBase) throw new Error(`This must be called on the base Dex`);
if (this.modsLoaded) return this;
let modList = fs.readdirSync(path.resolve(__dirname, 'mods'));
for (let i = 0; i < modList.length; i++) {
dexes[modList[i]] = new BattleDex(modList[i]);
}
this.modsLoaded = true;
return this;
}
includeModData() {
for (const mod in this.dexes) {
dexes[mod].includeData();
}
}
includeData() {
if (this.dataCache) return this;
dexes['base'].includeMods();
this.dataCache = {};
let basePath = this.isBase ? './data/' : './mods/' + this.currentMod + '/';
let BattleScripts = this.loadDataFile(basePath, 'Scripts');
this.parentMod = this.isBase ? '' : (BattleScripts.inherit || 'base');
let parentDex;
if (this.parentMod) {
parentDex = dexes[this.parentMod];
if (!parentDex || parentDex === this) throw new Error("Unable to load " + this.currentMod + ". `inherit` should specify a parent mod from which to inherit data, or must be not specified.");
}
for (let dataType of DATA_TYPES) {
if (dataType === 'Natures' && this.isBase) {
this.dataCache[dataType] = BattleNatures;
continue;
}
let BattleData = this.loadDataFile(basePath, dataType);
if (!BattleData || typeof BattleData !== 'object') throw new TypeError("Exported property `Battle" + dataType + "`from `" + './data/' + DATA_FILES[dataType] + "` must be an object except `null`.");
if (BattleData !== this.dataCache[dataType]) this.dataCache[dataType] = Object.assign(BattleData, this.dataCache[dataType]);
if (dataType === 'Formats' && !parentDex) Object.assign(BattleData, this.formats);
}
this.dataCache['MoveCache'] = new Map();
this.dataCache['ItemCache'] = new Map();
this.dataCache['AbilityCache'] = new Map();
this.dataCache['TemplateCache'] = new Map();
if (!parentDex) {
// Formats are inherited by mods
this.includeFormats();
} else {
for (let dataType of DATA_TYPES) {
const parentTypedData = parentDex.data[dataType];
const childTypedData = this.dataCache[dataType] || (this.dataCache[dataType] = {});
for (let entryId in parentTypedData) {
if (childTypedData[entryId] === null) {
// null means don't inherit
delete childTypedData[entryId];
} else if (!(entryId in childTypedData)) {
// If it doesn't exist it's inherited from the parent data
if (dataType === 'Pokedex') {
// Pokedex entries can be modified too many different ways
// e.g. inheriting different formats-data/learnsets
childTypedData[entryId] = this.deepClone(parentTypedData[entryId]);
} else {
childTypedData[entryId] = parentTypedData[entryId];
}
} else if (childTypedData[entryId] && childTypedData[entryId].inherit) {
// {inherit: true} can be used to modify only parts of the parent data,
// instead of overwriting entirely
delete childTypedData[entryId].inherit;
// Merge parent into children entry, preserving existing childs' properties.
for (let key in parentTypedData[entryId]) {
if (key in childTypedData[entryId]) continue;
childTypedData[entryId][key] = parentTypedData[entryId][key];
}
}
}
}
}
// Flag the generation. Required for team validator.
this.gen = this.dataCache.Scripts.gen || 7;
// Execute initialization script.
if (BattleScripts.init) BattleScripts.init.call(this);
return this;
}
includeFormats() {
if (!this.isBase) throw new Error(`This should only be run on the base mod`);
this.includeMods();
if (this.formatsCache) return this;
if (!this.formatsCache) this.formatsCache = {};
// Load formats
let Formats;
try {
Formats = require('./config/formats').Formats;
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') throw e;
}
if (!Array.isArray(Formats)) throw new TypeError(`Exported property 'Formats' from "./config/formats.js" must be an array`);
let section = '';
let column = 1;
for (let i = 0; i < Formats.length; i++) {
let format = Formats[i];
let id = toId(format.name);
if (format.section) section = format.section;
if (format.column) column = format.column;
if (!format.name && format.section) continue;
if (!id) throw new RangeError(`Format #${i + 1} must have a name with alphanumeric characters, not '${format.name}'`);
if (!format.section) format.section = section;
if (!format.column) format.column = column;
if (this.formatsCache[id]) throw new Error(`Format #${i + 1} has a duplicate ID: '${id}'`);
format.effectType = 'Format';
if (format.challengeShow === undefined) format.challengeShow = true;
if (format.searchShow === undefined) format.searchShow = true;
if (format.tournamentShow === undefined) format.tournamentShow = true;
if (format.mod === undefined) format.mod = 'gen6';
if (!dexes[format.mod]) throw new Error(`Format "${format.name}" requires nonexistent mod: '${format.mod}'`);
this.formatsCache[id] = format;
}
return this;
}
installFormat(id, format) {
dexes['base'].includeFormats();
dexes['base'].formatsCache[id] = format;
if (this.dataCache) this.dataCache.Formats[id] = format;
if (!this.isBase) {
if (dexes['base'].dataCache) dexes['base'].dataCache.Formats[id] = format;
}
}
/**
* Install our Tools functions into the battle object
*/
install(battle) {
for (let i in this.data.Scripts) {
battle[i] = this.data.Scripts[i];
}
}
}
dexes['base'] = new BattleDex();
// "gen7" is an alias for the current base data
dexes['gen7'] = dexes['base'];
module.exports = dexes['gen7'];