mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-05-21 06:37:09 -05:00
The padStart shim is currently unused and commented out. If we ever do need it, we should increase the minimum Node version to 8.0.
1360 lines
39 KiB
JavaScript
1360 lines
39 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');
|
|
|
|
const Data = require('./dex-data');
|
|
const {Effect, PureEffect, RuleTable, Format, Item, Template, Move, Ability} = Data; // eslint-disable-line no-unused-vars
|
|
|
|
const DATA_DIR = path.resolve(__dirname, '../data');
|
|
const MODS_DIR = path.resolve(__dirname, '../mods');
|
|
const FORMATS = path.resolve(__dirname, '../config/formats');
|
|
|
|
/** @type {{[mod: string]: ModdedDex}} */
|
|
let dexes = {};
|
|
|
|
/** @typedef {'Pokedex' | 'FormatsData' | 'Learnsets' | 'Movedex' | 'Statuses' | 'TypeChart' | 'Scripts' | 'Items' | 'Abilities' | 'Natures' | 'Formats'} DataType */
|
|
/** @type {DataType[]} */
|
|
const DATA_TYPES = ['Pokedex', 'FormatsData', 'Learnsets', 'Movedex', 'Statuses', 'TypeChart', 'Scripts', 'Items', 'Abilities', 'Natures', 'Formats'];
|
|
|
|
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, [k: string]: any}} DexTemplate */
|
|
/** @typedef {{[id: string]: AnyObject}} DexTable */
|
|
|
|
/** @typedef {{Pokedex: DexTable, Movedex: DexTable, Statuses: DexTable, TypeChart: DexTable, Scripts: DexTable, Items: DexTable, Abilities: DexTable, FormatsData: DexTable, Learnsets: DexTable, Aliases: {[id: string]: string}, Natures: DexTable, Formats: DexTable}} DexTableData */
|
|
|
|
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'},
|
|
};
|
|
|
|
const toId = Data.Tools.getId;
|
|
|
|
class ModdedDex {
|
|
/**
|
|
* @param {string} [mod = 'base']
|
|
*/
|
|
constructor(mod = 'base') {
|
|
this.gen = 0;
|
|
|
|
this.name = "[ModdedDex]";
|
|
|
|
this.isBase = (mod === 'base');
|
|
this.currentMod = mod;
|
|
this.parentMod = '';
|
|
|
|
/** @type {?DexTableData} */
|
|
this.dataCache = null;
|
|
/** @type {?DexTable} */
|
|
this.formatsCache = null;
|
|
|
|
/** @type {Map<string, Template>} */
|
|
this.templateCache = new Map();
|
|
/** @type {Map<string, Move>} */
|
|
this.moveCache = new Map();
|
|
/** @type {Map<string, Item>} */
|
|
this.itemCache = new Map();
|
|
/** @type {Map<string, Ability>} */
|
|
this.abilityCache = new Map();
|
|
|
|
this.modsLoaded = false;
|
|
|
|
this.getString = Data.Tools.getString;
|
|
this.getId = Data.Tools.getId;
|
|
this.ModdedDex = ModdedDex;
|
|
this.Data = Data;
|
|
}
|
|
|
|
/**
|
|
* @return {DexTableData}
|
|
*/
|
|
get data() {
|
|
return this.loadData();
|
|
}
|
|
/**
|
|
* @return {DexTable}
|
|
*/
|
|
get formats() {
|
|
this.includeFormats();
|
|
// @ts-ignore
|
|
return this.formatsCache;
|
|
}
|
|
/**
|
|
* @return {{[mod: string]: ModdedDex}}
|
|
*/
|
|
get dexes() {
|
|
this.includeMods();
|
|
return dexes;
|
|
}
|
|
|
|
/**
|
|
* @param {string} mod
|
|
* @return {ModdedDex}
|
|
*/
|
|
mod(mod) {
|
|
if (!dexes['base'].modsLoaded) dexes['base'].includeMods();
|
|
if (!mod) mod = 'base';
|
|
return dexes[mod];
|
|
}
|
|
/**
|
|
* @param {Format | string} format
|
|
* @return {ModdedDex}
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* returns false if the target is immune; true otherwise
|
|
*
|
|
* also checks immunity to some statuses
|
|
* @param {{type: string} | string} source
|
|
* @param {{types: string[]} | string[] | string} target
|
|
* @return {boolean}
|
|
*/
|
|
getImmunity(source, target) {
|
|
/** @type {string} */
|
|
// @ts-ignore
|
|
let sourceType = source.type || source;
|
|
/** @type {string[] | string} */
|
|
// @ts-ignore
|
|
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;
|
|
}
|
|
/**
|
|
* @param {{type: string} | string} source
|
|
* @param {{types: string[]} | string[] | string} target
|
|
* @return {number}
|
|
*/
|
|
getEffectiveness(source, target) {
|
|
/** @type {string} */
|
|
// @ts-ignore
|
|
let sourceType = source.type || source;
|
|
let totalTypeMod = 0;
|
|
/** @type {string[] | string} */
|
|
// @ts-ignore
|
|
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 Dex.getSpecies(id) differs from
|
|
* Dex.getTemplate(id).species).
|
|
*
|
|
* @param {string | Template} 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);
|
|
return template.species + '-' + form[0].toUpperCase() + form.slice(1);
|
|
} else {
|
|
return template.species;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string | Template} name
|
|
* @return {Template}
|
|
*/
|
|
getTemplate(name) {
|
|
if (name && typeof name !== 'string') {
|
|
return name;
|
|
}
|
|
name = (name || '').trim();
|
|
let id = toId(name);
|
|
if (id === 'nidoran' && name.slice(-1) === '♀') {
|
|
id = 'nidoranf';
|
|
} else if (id === 'nidoran' && name.slice(-1) === '♂') {
|
|
id = 'nidoranm';
|
|
}
|
|
let template = this.templateCache.get(id);
|
|
if (template) return template;
|
|
if (this.data.Aliases.hasOwnProperty(id)) {
|
|
template = this.getTemplate(this.data.Aliases[id]);
|
|
if (template) {
|
|
this.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.templateCache.set(id, template);
|
|
return template;
|
|
}
|
|
}
|
|
}
|
|
if (id && this.data.Pokedex.hasOwnProperty(id)) {
|
|
template = new Data.Template({name}, this.data.Pokedex[id], this.data.FormatsData[id], this.data.Learnsets[id]);
|
|
if (!template.tier && template.baseSpecies !== template.species) template.tier = this.data.FormatsData[toId(template.baseSpecies)].tier;
|
|
if (!template.tier) template.tier = 'Illegal';
|
|
} else {
|
|
template = new Data.Template({name, exists: false});
|
|
}
|
|
if (template.exists) this.templateCache.set(id, template);
|
|
return template;
|
|
}
|
|
/**
|
|
* @param {string | AnyObject} template
|
|
* @return {?AnyObject}
|
|
*/
|
|
getLearnset(template) {
|
|
const id = toId(template);
|
|
if (!this.data.Learnsets[id]) return null;
|
|
return this.data.Learnsets[id].learnset;
|
|
}
|
|
/**
|
|
* @param {string | Move} name
|
|
* @return {Move}
|
|
*/
|
|
getMove(name) {
|
|
if (name && typeof name !== 'string') {
|
|
return name;
|
|
}
|
|
name = (name || '').trim();
|
|
let id = toId(name);
|
|
let move = this.moveCache.get(id);
|
|
if (move) return move;
|
|
if (this.data.Aliases.hasOwnProperty(id)) {
|
|
move = this.getMove(this.data.Aliases[id]);
|
|
if (move.exists) {
|
|
this.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 = new Data.Move({name}, this.data.Movedex[id]);
|
|
} else {
|
|
move = new Data.Move({name, exists: false});
|
|
}
|
|
if (move.exists) this.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 === Dex.getMoveCopy(moveCopy)
|
|
*
|
|
* If you really want to, use:
|
|
* moveCopyCopy = Dex.getMoveCopy(moveCopy.id)
|
|
*
|
|
* @param {Move | string} move - Move ID, move object, or movecopy object describing move to copy
|
|
* @return {Move} movecopy object
|
|
*/
|
|
getMoveCopy(move) {
|
|
// @ts-ignore
|
|
if (move && move.isCopy) return move;
|
|
move = this.getMove(move);
|
|
let moveCopy = this.deepClone(move);
|
|
moveCopy.isCopy = true;
|
|
return moveCopy;
|
|
}
|
|
/**
|
|
* @param {string | Effect} name
|
|
* @return {Effect}
|
|
*/
|
|
getEffect(name) {
|
|
if (name && typeof name !== 'string') {
|
|
return name;
|
|
}
|
|
let id = toId(name);
|
|
let effect;
|
|
if (id && this.data.Statuses.hasOwnProperty(id)) {
|
|
effect = new Data.PureEffect({name}, this.data.Statuses[id]);
|
|
} else if (id && this.data.Movedex.hasOwnProperty(id) && this.data.Movedex[id].effect) {
|
|
name = this.data.Movedex[id].name || name;
|
|
effect = new Data.PureEffect({name}, this.data.Movedex[id].effect);
|
|
} else if (id && this.data.Abilities.hasOwnProperty(id) && this.data.Abilities[id].effect) {
|
|
name = this.data.Abilities[id].name || name;
|
|
effect = new Data.PureEffect({name}, this.data.Abilities[id].effect);
|
|
} else if (id && this.data.Items.hasOwnProperty(id) && this.data.Items[id].effect) {
|
|
name = this.data.Items[id].name || name;
|
|
effect = new Data.PureEffect({name}, this.data.Items[id].effect);
|
|
} else if (id && this.data.Formats.hasOwnProperty(id)) {
|
|
effect = new Data.Format({name}, this.data.Formats[id]);
|
|
} else if (id === 'recoil') {
|
|
effect = new Data.PureEffect({name: 'Recoil', effectType: 'Recoil'});
|
|
} else if (id === 'drain') {
|
|
effect = new Data.PureEffect({name: 'Drain', effectType: 'Drain'});
|
|
} else {
|
|
effect = new Data.PureEffect({name, exists: false});
|
|
}
|
|
return effect;
|
|
}
|
|
/**
|
|
* @param {string | Format} name
|
|
* @param {string | string[]} [customBanlist]
|
|
* @param {string} [customId]
|
|
* @return {Format}
|
|
*/
|
|
getFormat(name, customBanlist, customId) {
|
|
if (name && typeof name !== 'string') {
|
|
return name;
|
|
}
|
|
name = (name || '').trim();
|
|
let id = toId(name);
|
|
if (this.data.Aliases[id]) {
|
|
name = this.data.Aliases[id];
|
|
id = toId(name);
|
|
}
|
|
let effect;
|
|
if (this.data.Formats.hasOwnProperty(id)) {
|
|
let format = this.data.Formats[id];
|
|
if (customBanlist) {
|
|
if (typeof customBanlist === 'string') customBanlist = customBanlist.split(',');
|
|
let customRules = [];
|
|
for (let ban of customBanlist) {
|
|
let unban = false;
|
|
if (ban.charAt(0) === '!') {
|
|
unban = true;
|
|
ban = ban.substr(1);
|
|
}
|
|
if (ban.startsWith('Rule:')) {
|
|
ban = ban.substr(5).trim();
|
|
if (unban) {
|
|
customRules.unshift('!' + ban);
|
|
} else {
|
|
customRules.push(ban);
|
|
}
|
|
} else {
|
|
if (unban) {
|
|
customRules.push('+' + ban);
|
|
} else {
|
|
customRules.push('-' + ban);
|
|
}
|
|
}
|
|
}
|
|
effect = new Data.Format({name}, format, {customRules});
|
|
if (customId === 'pokemon') throw new Error('wtf');
|
|
if (customId) this.data.Formats[customId] = format;
|
|
} else {
|
|
effect = new Data.Format({name}, format);
|
|
}
|
|
} else if (this.data.Formats.hasOwnProperty(name)) {
|
|
effect = new Data.Format({name}, this.data.Formats[name]);
|
|
} else {
|
|
effect = new Data.Format({name, exists: false});
|
|
}
|
|
return effect;
|
|
}
|
|
/**
|
|
* @param {string | Item} name
|
|
* @return {Item}
|
|
*/
|
|
getItem(name) {
|
|
if (name && typeof name !== 'string') {
|
|
return name;
|
|
}
|
|
name = (name || '').trim();
|
|
let id = toId(name);
|
|
let item = this.itemCache.get(id);
|
|
if (item) return item;
|
|
if (this.data.Aliases.hasOwnProperty(id)) {
|
|
item = this.getItem(this.data.Aliases[id]);
|
|
if (item.exists) {
|
|
this.itemCache.set(id, item);
|
|
}
|
|
return item;
|
|
}
|
|
if (id && !this.data.Items[id] && this.data.Items[id + 'berry']) {
|
|
item = this.getItem(id + 'berry');
|
|
this.itemCache.set(id, item);
|
|
return item;
|
|
}
|
|
if (id && this.data.Items.hasOwnProperty(id)) {
|
|
item = new Data.Item({name}, this.data.Items[id]);
|
|
} else {
|
|
item = new Data.Item({name, exists: false});
|
|
}
|
|
|
|
if (item.exists) this.itemCache.set(id, item);
|
|
return item;
|
|
}
|
|
/**
|
|
* @param {string | Ability} name
|
|
* @return {Ability}
|
|
*/
|
|
getAbility(name) {
|
|
if (name && typeof name !== 'string') {
|
|
return name;
|
|
}
|
|
let id = toId(name);
|
|
let ability = this.abilityCache.get(id);
|
|
if (ability) return ability;
|
|
if (id && this.data.Abilities.hasOwnProperty(id)) {
|
|
ability = new Data.Ability({name}, this.data.Abilities[id]);
|
|
} else {
|
|
ability = new Data.Ability({name, exists: false});
|
|
}
|
|
|
|
if (ability.exists) this.abilityCache.set(id, ability);
|
|
return ability;
|
|
}
|
|
/**
|
|
* @param {string | AnyObject} type
|
|
* @return {AnyObject}
|
|
*/
|
|
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;
|
|
}
|
|
/**
|
|
* @param {string | AnyObject} nature
|
|
* @return {AnyObject}
|
|
*/
|
|
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';
|
|
if (!nature.gen) nature.gen = 3;
|
|
}
|
|
return nature;
|
|
}
|
|
/**
|
|
* @param {AnyObject} stats
|
|
* @param {AnyObject} set
|
|
* @return {AnyObject}
|
|
*/
|
|
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);
|
|
}
|
|
/**
|
|
* @param {AnyObject} stats
|
|
* @param {string | AnyObject} nature
|
|
* @return {AnyObject}
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param {AnyObject} ivs
|
|
*/
|
|
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,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Format} format
|
|
* @param {number} [depth = 0]
|
|
* @return {RuleTable}
|
|
*/
|
|
getRuleTable(format, depth = 0) {
|
|
/** @type {RuleTable} */
|
|
let ruleTable = new RuleTable();
|
|
if (format.ruleTable) return format.ruleTable;
|
|
|
|
const ruleset = format.ruleset.slice();
|
|
for (const ban of format.banlist) {
|
|
ruleset.push('-' + ban);
|
|
}
|
|
for (const ban of format.unbanlist) {
|
|
ruleset.push('+' + ban);
|
|
}
|
|
if (format.customRules) {
|
|
for (const rule of format.customRules) {
|
|
if (rule.startsWith('!')) {
|
|
ruleset.unshift(rule);
|
|
} else {
|
|
ruleset.push(rule);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const rule of ruleset) {
|
|
if (rule.charAt(0) === '-' || rule.charAt(0) === '+') { // ban or unban
|
|
const type = rule.charAt(0);
|
|
let buf = rule.slice(1);
|
|
const gtIndex = buf.lastIndexOf('>');
|
|
let limit = 0;
|
|
if (gtIndex >= 0 && /^[0-9]+$/.test(buf.slice(gtIndex + 1).trim())) {
|
|
limit = parseInt(buf.slice(gtIndex + 1));
|
|
buf = buf.slice(0, gtIndex);
|
|
}
|
|
let checkTeam = buf.includes('++');
|
|
const banNames = buf.split(checkTeam ? '++' : '+').map(v => v.trim());
|
|
if (banNames.length === 1 && limit > 0) checkTeam = true;
|
|
const innerRule = banNames.join(checkTeam ? ' ++ ' : ' + ');
|
|
const bans = banNames.map(v => toId(v));
|
|
|
|
if (checkTeam) {
|
|
ruleTable.complexTeamBans.push([innerRule, '', limit, bans]);
|
|
continue;
|
|
}
|
|
if (bans.length > 1 || limit > 0) {
|
|
ruleTable.complexBans.push([innerRule, '', limit, bans]);
|
|
}
|
|
const ban = toId(buf);
|
|
ruleTable.delete('+' + ban);
|
|
ruleTable.delete('-' + ban);
|
|
ruleTable.set(type + ban, '');
|
|
continue;
|
|
}
|
|
if (rule.startsWith('!')) {
|
|
ruleTable.set('!' + toId(rule), '');
|
|
continue;
|
|
}
|
|
const subformat = this.getFormat(rule);
|
|
if (ruleTable.has('!' + subformat.id)) continue;
|
|
ruleTable.set(subformat.id, '');
|
|
if (!subformat.exists) continue;
|
|
if (depth > 16) {
|
|
throw new Error(`Excessive ruleTable recursion in ${format.name}: ${rule} of ${format.ruleset}`);
|
|
}
|
|
const subRuleTable = this.getRuleTable(subformat, depth + 1);
|
|
subRuleTable.forEach((v, k) => {
|
|
ruleTable.set(k, v || subformat.name);
|
|
});
|
|
for (const [rule, source, limit, bans] of subRuleTable.complexBans) {
|
|
ruleTable.complexBans.push([rule, source || subformat.name, limit, bans]);
|
|
}
|
|
for (const [rule, source, limit, bans] of subRuleTable.complexTeamBans) {
|
|
ruleTable.complexTeamBans.push([rule, source || subformat.name, limit, bans]);
|
|
}
|
|
}
|
|
|
|
format.ruleTable = ruleTable;
|
|
return ruleTable;
|
|
}
|
|
|
|
/**
|
|
* TODO: TypeScript generics
|
|
* @param {Array} arr
|
|
* @return {Array}
|
|
*/
|
|
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
|
|
* @return {number} - distance
|
|
*/
|
|
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];
|
|
}
|
|
|
|
/**
|
|
* Forces num to be an integer (between min and max).
|
|
* @param {any} num
|
|
* @param {number=} min
|
|
* @param {number=} max
|
|
* @return {number}
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param {string} target
|
|
* @param {DataType[] | null=} searchIn
|
|
* @param {boolean=} isInexact
|
|
* @return {AnyObject[] | false}
|
|
*/
|
|
dataSearch(target, searchIn, isInexact) {
|
|
if (!target) {
|
|
return false;
|
|
}
|
|
|
|
/** @type {DataType[]} */
|
|
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'};
|
|
/** @type {AnyObject[] | false} */
|
|
let searchResults = [];
|
|
for (let i = 0; i < searchIn.length; i++) {
|
|
/** @type {AnyObject} */
|
|
// @ts-ignore
|
|
let res = this[searchFunctions[searchIn[i]]](target);
|
|
if (res.exists && res.gen <= this.gen) {
|
|
searchResults.push({
|
|
isInexact: isInexact,
|
|
searchType: searchTypes[searchIn[i]],
|
|
name: res.name,
|
|
});
|
|
}
|
|
}
|
|
if (searchResults.length) {
|
|
return searchResults;
|
|
}
|
|
if (isInexact) {
|
|
return false; // prevent infinite loop
|
|
}
|
|
|
|
let cmpTarget = toId(target);
|
|
let maxLd = 3;
|
|
if (cmpTarget.length <= 1) {
|
|
return false;
|
|
} else if (cmpTarget.length <= 4) {
|
|
maxLd = 1;
|
|
} else if (cmpTarget.length <= 6) {
|
|
maxLd = 2;
|
|
}
|
|
searchResults = false;
|
|
for (let i = 0; i <= searchIn.length; i++) {
|
|
let searchObj = this.data[searchIn[i] || 'Aliases'];
|
|
if (!searchObj) {
|
|
continue;
|
|
}
|
|
|
|
for (let j in searchObj) {
|
|
let ld = this.levenshtein(cmpTarget, j, maxLd);
|
|
if (ld <= maxLd) {
|
|
// @ts-ignore
|
|
let word = searchObj[j].name || searchObj[j].species || j;
|
|
let results = this.dataSearch(word, searchIn, word);
|
|
if (results) {
|
|
searchResults = results;
|
|
maxLd = ld;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return searchResults;
|
|
}
|
|
|
|
/**
|
|
* @param {AnyObject[]} team
|
|
* @return {string}
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param {string} buf
|
|
* @return {?AnyObject[]}
|
|
*/
|
|
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 null;
|
|
set.name = buf.substring(i, j);
|
|
i = j + 1;
|
|
|
|
// species
|
|
j = buf.indexOf('|', i);
|
|
if (j < 0) return null;
|
|
set.species = buf.substring(i, j) || set.name;
|
|
i = j + 1;
|
|
|
|
// item
|
|
j = buf.indexOf('|', i);
|
|
if (j < 0) return null;
|
|
set.item = buf.substring(i, j);
|
|
i = j + 1;
|
|
|
|
// ability
|
|
j = buf.indexOf('|', i);
|
|
if (j < 0) return null;
|
|
let ability = buf.substring(i, j);
|
|
let template = dexes['base'].getTemplate(set.species);
|
|
// @ts-ignore
|
|
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 null;
|
|
set.moves = buf.substring(i, j).split(',', 24);
|
|
i = j + 1;
|
|
|
|
// nature
|
|
j = buf.indexOf('|', i);
|
|
if (j < 0) return null;
|
|
set.nature = buf.substring(i, j);
|
|
i = j + 1;
|
|
|
|
// evs
|
|
j = buf.indexOf('|', i);
|
|
if (j < 0) return null;
|
|
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 null;
|
|
if (i !== j) set.gender = buf.substring(i, j);
|
|
i = j + 1;
|
|
|
|
// ivs
|
|
j = buf.indexOf('|', i);
|
|
if (j < 0) return null;
|
|
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 null;
|
|
if (i !== j) set.shiny = true;
|
|
i = j + 1;
|
|
|
|
// level
|
|
j = buf.indexOf('|', i);
|
|
if (j < 0) return null;
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param {any} obj
|
|
* @return {any}
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param {string} basePath
|
|
* @param {DataType} dataType
|
|
* @return {AnyObject}
|
|
*/
|
|
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 {};
|
|
}
|
|
|
|
/**
|
|
* @return {ModdedDex}
|
|
*/
|
|
includeMods() {
|
|
if (!this.isBase) throw new Error(`This must be called on the base Dex`);
|
|
if (this.modsLoaded) return this;
|
|
|
|
let modList = fs.readdirSync(MODS_DIR);
|
|
for (let i = 0; i < modList.length; i++) {
|
|
dexes[modList[i]] = new ModdedDex(modList[i]);
|
|
}
|
|
this.modsLoaded = true;
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* @return {ModdedDex}
|
|
*/
|
|
includeModData() {
|
|
for (const mod in this.dexes) {
|
|
dexes[mod].includeData();
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* @return {ModdedDex}
|
|
*/
|
|
includeData() {
|
|
this.loadData();
|
|
return this;
|
|
}
|
|
/**
|
|
* @return {DexTableData}
|
|
*/
|
|
loadData() {
|
|
if (this.dataCache) return this.dataCache;
|
|
dexes['base'].includeMods();
|
|
let dataCache = {};
|
|
|
|
let basePath = (this.isBase ? DATA_DIR : MODS_DIR + '/' + 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.");
|
|
}
|
|
|
|
// @ts-ignore
|
|
for (let dataType of DATA_TYPES.concat('Aliases')) {
|
|
if (dataType === 'Natures' && this.isBase) {
|
|
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 !== dataCache[dataType]) dataCache[dataType] = Object.assign(BattleData, dataCache[dataType]);
|
|
if (dataType === 'Formats' && !parentDex) Object.assign(BattleData, this.formats);
|
|
}
|
|
if (!parentDex) {
|
|
// Formats are inherited by mods
|
|
this.includeFormats();
|
|
} else {
|
|
for (let dataType of DATA_TYPES) {
|
|
const parentTypedData = parentDex.data[dataType];
|
|
const childTypedData = dataCache[dataType] || (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];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
dataCache['Aliases'] = parentDex.data['Aliases'];
|
|
}
|
|
|
|
// Flag the generation. Required for team validator.
|
|
this.gen = dataCache.Scripts.gen || 7;
|
|
// @ts-ignore
|
|
this.dataCache = dataCache;
|
|
|
|
// Execute initialization script.
|
|
if (BattleScripts.init) BattleScripts.init.call(this);
|
|
|
|
// @ts-ignore TypeScript bug
|
|
return this.dataCache;
|
|
}
|
|
|
|
/**
|
|
* @return {ModdedDex}
|
|
*/
|
|
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(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';
|
|
format.baseRuleset = format.ruleset ? format.ruleset.slice() : [];
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param {string} id - Format ID
|
|
* @param {Format} format - Format
|
|
*/
|
|
installFormat(id, format) {
|
|
dexes['base'].includeFormats();
|
|
// @ts-ignore
|
|
dexes['base'].formatsCache[id] = format;
|
|
if (this.dataCache) this.dataCache.Formats[id] = format;
|
|
if (!this.isBase) {
|
|
// @ts-ignore
|
|
if (dexes['base'].dataCache) dexes['base'].dataCache.Formats[id] = format;
|
|
}
|
|
}
|
|
}
|
|
|
|
dexes['base'] = new ModdedDex();
|
|
|
|
// "gen7" is an alias for the current base data
|
|
dexes['gen7'] = dexes['base'];
|
|
|
|
module.exports = dexes['gen7'];
|