/** * Dex * Pokemon Showdown - http://pokemonshowdown.com/ * * Handles getting data about pokemon, items, etc. Also contains some useful * helper functions for using dex data. * * By default, nothing is loaded until you call Dex.mod(mod) or * Dex.forFormat(format). * * You may choose to preload some things: * - Dex.includeMods() ~10ms * This will preload `Dex.dexes`, giving you a list of possible mods. * - Dex.includeFormats() ~30ms * As above, but will also preload `Dex.formats.all()`. * - Dex.includeData() ~500ms * As above, but will also preload all of Dex.data for Gen 8, so * functions like `Dex.species.get`, etc will be instantly usable. * - Dex.includeModData() ~1500ms * As above, but will also preload `Dex.dexes[...].data` for all mods. * * Note that preloading is never necessary. All the data will be * automatically preloaded when needed, preloading will just spend time * now so you don't need to spend time later. * * @license MIT */ import * as fs from 'fs'; import * as path from 'path'; import * as Data from './dex-data'; import { Condition, DexConditions } from './dex-conditions'; import { DataMove, DexMoves } from './dex-moves'; import { Item, DexItems } from './dex-items'; import { Ability, DexAbilities } from './dex-abilities'; import { Species, DexSpecies } from './dex-species'; import { Format, DexFormats } from './dex-formats'; import { Utils } from '../lib/utils'; const BASE_MOD = 'gen9' as ID; const DATA_DIR = path.resolve(__dirname, '../data'); const MODS_DIR = path.resolve(DATA_DIR, './mods'); const dexes: { [mod: string]: ModdedDex } = Object.create(null); type DataType = 'Abilities' | 'Rulesets' | 'FormatsData' | 'Items' | 'Learnsets' | 'Moves' | 'Natures' | 'Pokedex' | 'Scripts' | 'Conditions' | 'TypeChart' | 'PokemonGoData'; const DATA_TYPES: (DataType | 'Aliases')[] = [ 'Abilities', 'Rulesets', 'FormatsData', 'Items', 'Learnsets', 'Moves', 'Natures', 'Pokedex', 'Scripts', 'Conditions', 'TypeChart', 'PokemonGoData', ]; const DATA_FILES = { Abilities: 'abilities', Aliases: 'aliases', Rulesets: 'rulesets', FormatsData: 'formats-data', Items: 'items', Learnsets: 'learnsets', Moves: 'moves', Natures: 'natures', Pokedex: 'pokedex', PokemonGoData: 'pokemongo', Scripts: 'scripts', Conditions: 'conditions', TypeChart: 'typechart', }; /** Unfortunately we do for..in too much to want to deal with the casts */ export interface DexTable { [id: string]: T } export interface AliasesTable { [id: IDEntry]: string } interface DexTableData { Abilities: DexTable; Aliases: DexTable; Rulesets: DexTable; Items: DexTable; Learnsets: DexTable; Moves: DexTable; Natures: DexTable; Pokedex: DexTable; FormatsData: DexTable; PokemonGoData: DexTable; Scripts: DexTable; Conditions: DexTable; TypeChart: DexTable; } interface TextTableData { Abilities: DexTable; Items: DexTable; Moves: DexTable; Pokedex: DexTable; Default: DexTable; } export const toID = Data.toID; export class ModdedDex { readonly Data = Data; readonly Condition = Condition; readonly Ability = Ability; readonly Item = Item; readonly Move = DataMove; readonly Species = Species; readonly Format = Format; readonly ModdedDex = ModdedDex; readonly name = "[ModdedDex]"; readonly isBase: boolean; readonly currentMod: string; readonly dataDir: string; readonly toID = Data.toID; gen = 0; parentMod = ''; modsLoaded = false; dataCache: DexTableData | null; textCache: TextTableData | null; deepClone = Utils.deepClone; deepFreeze = Utils.deepFreeze; Multiset = Utils.Multiset; readonly formats: DexFormats; readonly abilities: DexAbilities; readonly items: DexItems; readonly moves: DexMoves; readonly species: DexSpecies; readonly conditions: DexConditions; readonly natures: Data.DexNatures; readonly types: Data.DexTypes; readonly stats: Data.DexStats; constructor(mod = 'base') { this.isBase = (mod === 'base'); this.currentMod = mod; this.dataDir = (this.isBase ? DATA_DIR : MODS_DIR + '/' + this.currentMod); this.dataCache = null; this.textCache = null; this.formats = new DexFormats(this); this.abilities = new DexAbilities(this); this.items = new DexItems(this); this.moves = new DexMoves(this); this.species = new DexSpecies(this); this.conditions = new DexConditions(this); this.natures = new Data.DexNatures(this); this.types = new Data.DexTypes(this); this.stats = new Data.DexStats(this); } get data(): DexTableData { return this.loadData(); } get dexes(): { [mod: string]: ModdedDex } { this.includeMods(); return dexes; } mod(mod: string | undefined): ModdedDex { if (!dexes['base'].modsLoaded) dexes['base'].includeMods(); return dexes[mod || 'base'].includeData(); } forGen(gen: number) { if (!gen) return this; return this.mod(`gen${gen}`); } forFormat(format: Format | string): ModdedDex { if (!this.modsLoaded) this.includeMods(); const mod = this.formats.get(format).mod; return dexes[mod || BASE_MOD].includeData(); } modData(dataType: DataType, id: string) { 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] = Utils.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. */ getName(name: any): string { 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. */ getImmunity( source: { type: string } | string, target: { getTypes: () => string[] } | { types: string[] } | string[] | string ): boolean { const sourceType: string = typeof source !== 'string' ? source.type : source; // @ts-expect-error really wish TS would support this const targetTyping: string[] | string = target.getTypes?.() || target.types || target; if (Array.isArray(targetTyping)) { for (const type of targetTyping) { if (!this.getImmunity(sourceType, type)) return false; } return true; } const typeData = this.types.get(targetTyping); if (typeData && typeData.damageTaken[sourceType] === 3) return false; return true; } getEffectiveness( source: { type: string } | string, target: { getTypes: () => string[] } | { types: string[] } | string[] | string ): number { const sourceType: string = typeof source !== 'string' ? source.type : source; // @ts-expect-error really wish TS would support this const targetTyping: string[] | string = target.getTypes?.() || target.types || target; let totalTypeMod = 0; if (Array.isArray(targetTyping)) { for (const type of targetTyping) { totalTypeMod += this.getEffectiveness(sourceType, type); } return totalTypeMod; } const typeData = this.types.get(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; } } getDescs(table: keyof TextTableData, id: ID, dataEntry: AnyObject) { if (dataEntry.shortDesc) { return { desc: dataEntry.desc, shortDesc: dataEntry.shortDesc, }; } const entry = this.loadTextData()[table][id]; if (!entry) return null; const descs = { desc: '', shortDesc: '', }; for (let i = this.gen; i < dexes['base'].gen; i++) { const curDesc = entry[`gen${i}` as keyof typeof entry]?.desc; const curShortDesc = entry[`gen${i}` as keyof typeof entry]?.shortDesc; if (!descs.desc && curDesc) { descs.desc = curDesc; } if (!descs.shortDesc && curShortDesc) { descs.shortDesc = curShortDesc; } if (descs.desc && descs.shortDesc) break; } if (!descs.shortDesc) descs.shortDesc = entry.shortDesc || ''; if (!descs.desc) descs.desc = entry.desc || descs.shortDesc; return descs; } /** * 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.getActiveMove(moveCopy) * * If you really want to, use: * moveCopyCopy = Dex.getActiveMove(moveCopy.id) */ getActiveMove(move: Move | string): ActiveMove { if (move && typeof (move as ActiveMove).hit === 'number') return move as ActiveMove; move = this.moves.get(move); const moveCopy: ActiveMove = this.deepClone(move); moveCopy.hit = 0; return moveCopy; } getHiddenPower(ivs: StatsTable) { const hpTypes = [ 'Fighting', 'Flying', 'Poison', 'Ground', 'Rock', 'Bug', 'Ghost', 'Steel', 'Fire', 'Water', 'Grass', 'Electric', 'Psychic', 'Ice', 'Dragon', 'Dark', ]; const tr = this.trunc; 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 = tr(ivs.atk / 2); const defDV = tr(ivs.def / 2); const speDV = tr(ivs.spe / 2); const spcDV = tr(ivs.spa / 2); return { type: hpTypes[4 * (atkDV % 4) + (defDV % 4)], power: tr( (5 * ((spcDV >> 3) + (2 * (speDV >> 3)) + (4 * (defDV >> 3)) + (8 * (atkDV >> 3))) + (spcDV % 4)) / 2 + 31 ), }; } else { // Hidden Power check for Gen 3 onwards let hpTypeX = 0; let hpPowerX = 0; let i = 1; for (const s in stats) { hpTypeX += i * (ivs[s as StatID] % 2); hpPowerX += i * (tr(ivs[s as StatID] / 2) % 2); i *= 2; } return { type: hpTypes[tr(hpTypeX * 15 / 63)], // After Gen 6, Hidden Power is always 60 base power power: (this.gen && this.gen < 6) ? tr(hpPowerX * 40 / 63) + 30 : 60, }; } } /** * Truncate a number into an unsigned 32-bit integer, for * compatibility with the cartridge games' math systems. */ trunc(this: void, num: number, bits = 0) { if (bits) return (num >>> 0) % (2 ** bits); return num >>> 0; } dataSearch( target: string, searchIn?: ('Pokedex' | 'Moves' | 'Abilities' | 'Items' | 'Natures' | 'TypeChart')[] | null, isInexact?: boolean ): AnyObject[] | null { if (!target) return null; searchIn = searchIn || ['Pokedex', 'Moves', 'Abilities', 'Items', 'Natures']; const searchObjects = { Pokedex: 'species', Moves: 'moves', Abilities: 'abilities', Items: 'items', Natures: 'natures', TypeChart: 'types', } as const; const searchTypes = { Pokedex: 'pokemon', Moves: 'move', Abilities: 'ability', Items: 'item', Natures: 'nature', TypeChart: 'type', } as const; let searchResults: AnyObject[] | null = []; for (const table of searchIn) { const res = this[searchObjects[table]].get(target); if (res.exists && res.gen <= this.gen) { searchResults.push({ isInexact, searchType: searchTypes[table], name: res.name, }); } } if (searchResults.length) return searchResults; if (isInexact) return null; // prevent infinite loop const cmpTarget = toID(target); let maxLd = 3; if (cmpTarget.length <= 1) { return null; } else if (cmpTarget.length <= 4) { maxLd = 1; } else if (cmpTarget.length <= 6) { maxLd = 2; } searchResults = null; for (const table of [...searchIn, 'Aliases'] as const) { const searchObj = this.data[table] as DexTable; if (!searchObj) continue; for (const j in searchObj) { const ld = Utils.levenshtein(cmpTarget, j, maxLd); if (ld <= maxLd) { const word = searchObj[j].name || j; const results = this.dataSearch(word, searchIn, word); if (results) { searchResults = results; maxLd = ld; } } } } return searchResults; } loadDataFile(basePath: string, dataType: DataType | 'Aliases'): AnyObject | void { try { const filePath = basePath + DATA_FILES[dataType]; const dataObject = require(filePath); if (!dataObject || typeof dataObject !== 'object') { throw new TypeError(`${filePath}, if it exists, must export a non-null object`); } if (dataObject[dataType]?.constructor?.name !== 'Object') { throw new TypeError(`${filePath}, if it exists, must export an object whose '${dataType}' property is an Object`); } return dataObject[dataType]; } catch (e: any) { if (e.code !== 'MODULE_NOT_FOUND' && e.code !== 'ENOENT') { throw e; } } } loadTextFile( name: string, exportName: string ): DexTable { return require(`${DATA_DIR}/text/${name}`)[exportName]; } includeMods(): this { if (!this.isBase) throw new Error(`This must be called on the base Dex`); if (this.modsLoaded) return this; for (const mod of fs.readdirSync(MODS_DIR)) { dexes[mod] = new ModdedDex(mod); } this.modsLoaded = true; return this; } includeModData(): this { for (const mod in this.dexes) { dexes[mod].includeData(); } return this; } includeData(): this { this.loadData(); return this; } loadTextData() { if (dexes['base'].textCache) return dexes['base'].textCache; dexes['base'].textCache = { Pokedex: this.loadTextFile('pokedex', 'PokedexText') as DexTable, Moves: this.loadTextFile('moves', 'MovesText') as DexTable, Abilities: this.loadTextFile('abilities', 'AbilitiesText') as DexTable, Items: this.loadTextFile('items', 'ItemsText') as DexTable, Default: this.loadTextFile('default', 'DefaultText') as DexTable, }; return dexes['base'].textCache; } loadData(): DexTableData { if (this.dataCache) return this.dataCache; dexes['base'].includeMods(); const dataCache: { [k in keyof DexTableData]?: any } = {}; const basePath = this.dataDir + '/'; const Scripts = this.loadDataFile(basePath, 'Scripts') || {}; // We want to inherit most of Scripts but not this. const init = Scripts.init; this.parentMod = this.isBase ? '' : (Scripts.inherit || 'base'); let parentDex; if (this.parentMod) { parentDex = dexes[this.parentMod]; if (!parentDex || parentDex === this) { throw new Error( `Unable to load ${this.currentMod}. 'inherit' in scripts.ts should specify a parent mod from which to inherit data, or must be not specified.` ); } } if (!parentDex) { // Formats are inherited by mods and used by Rulesets this.includeFormats(); } for (const dataType of DATA_TYPES.concat('Aliases')) { dataCache[dataType] = this.loadDataFile(basePath, dataType); if (dataType === 'Rulesets' && !parentDex) { for (const format of this.formats.all()) { dataCache.Rulesets[format.id] = { ...format, ruleTable: null }; } } } if (parentDex) { for (const dataType of DATA_TYPES) { const parentTypedData: DexTable = parentDex.data[dataType]; if (!dataCache[dataType] && !init) { dataCache[dataType] = parentTypedData; continue; } const childTypedData: DexTable = dataCache[dataType] || (dataCache[dataType] = {}); for (const 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 childTypedData[entryId] = parentTypedData[entryId]; } else if (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 and child's entry, with child overwriting parent. childTypedData[entryId] = { ...parentTypedData[entryId], ...childTypedData[entryId] }; } } } dataCache['Aliases'] = parentDex.data['Aliases']; } // Flag the generation. Required for team validator. this.gen = dataCache.Scripts.gen; if (!this.gen) throw new Error(`Mod ${this.currentMod} needs a generation number in scripts.js`); this.dataCache = dataCache as DexTableData; // Execute initialization script. if (init) init.call(this); return this.dataCache; } includeFormats(): this { this.formats.load(); return this; } } dexes['base'] = new ModdedDex(); // "gen9" is an alias for the current base data dexes[BASE_MOD] = dexes['base']; export const Dex = dexes['base']; export declare namespace Dex { export type Species = import('./dex-species').Species; export type Item = import('./dex-items').Item; export type Move = import('./dex-moves').Move; export type Ability = import('./dex-abilities').Ability; export type HitEffect = import('./dex-moves').HitEffect; export type SecondaryEffect = import('./dex-moves').SecondaryEffect; export type RuleTable = import('./dex-formats').RuleTable; } export default Dex;