Spaces:
Running
Running
/** | |
* 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<T> { [id: string]: T } | |
export interface AliasesTable { [id: IDEntry]: string } | |
interface DexTableData { | |
Abilities: DexTable<import('./dex-abilities').AbilityData>; | |
Aliases: DexTable<string>; | |
Rulesets: DexTable<import('./dex-formats').FormatData>; | |
Items: DexTable<import('./dex-items').ItemData>; | |
Learnsets: DexTable<import('./dex-species').LearnsetData>; | |
Moves: DexTable<import('./dex-moves').MoveData>; | |
Natures: DexTable<import('./dex-data').NatureData>; | |
Pokedex: DexTable<import('./dex-species').SpeciesData>; | |
FormatsData: DexTable<import('./dex-species').SpeciesFormatsData>; | |
PokemonGoData: DexTable<import('./dex-species').PokemonGoData>; | |
Scripts: DexTable<AnyObject>; | |
Conditions: DexTable<import('./dex-conditions').ConditionData>; | |
TypeChart: DexTable<import('./dex-data').TypeData>; | |
} | |
interface TextTableData { | |
Abilities: DexTable<AbilityText>; | |
Items: DexTable<ItemText>; | |
Moves: DexTable<MoveText>; | |
Pokedex: DexTable<PokedexText>; | |
Default: DexTable<DefaultText>; | |
} | |
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<any>; | |
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<MoveText | ItemText | AbilityText | PokedexText | DefaultText> { | |
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<PokedexText>, | |
Moves: this.loadTextFile('moves', 'MovesText') as DexTable<MoveText>, | |
Abilities: this.loadTextFile('abilities', 'AbilitiesText') as DexTable<AbilityText>, | |
Items: this.loadTextFile('items', 'ItemsText') as DexTable<ItemText>, | |
Default: this.loadTextFile('default', 'DefaultText') as DexTable<DefaultText>, | |
}; | |
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<any> = parentDex.data[dataType]; | |
if (!dataCache[dataType] && !init) { | |
dataCache[dataType] = parentTypedData; | |
continue; | |
} | |
const childTypedData: DexTable<any> = 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; | |