/** * Dex Data * Pokemon Showdown - http://pokemonshowdown.com/ * * @license MIT */ import { Utils } from '../lib/utils'; /** * 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. * * Generally assigned to the global toID, because of how * commonly it's used. */ export function toID(text: any): ID { if (typeof text !== 'string') { if (text) text = text.id || text.userid || text.roomid || text; if (typeof text === 'number') text = `${text}`; else if (typeof text !== 'string') return ''; } return text.toLowerCase().replace(/[^a-z0-9]+/g, '') as ID; } /** * Like Object.assign but only assigns fields missing from self. * Facilitates consistent field ordering in constructors. * Modifies self in-place. */ export function assignMissingFields(self: AnyObject, data: AnyObject) { for (const k in data) { if (k in self) continue; self[k] = data[k]; } } export abstract class BasicEffect implements EffectData { /** * ID. This will be a lowercase version of the name with all the * non-alphanumeric characters removed. So, for instance, "Mr. Mime" * becomes "mrmime", and "Basculin-Blue-Striped" becomes * "basculinbluestriped". */ id: ID; /** * Name. Currently does not support Unicode letters, so "Flabébé" * is "Flabebe" and "Nidoran♀" is "Nidoran-F". */ name: string; /** * Full name. Prefixes the name with the effect type. For instance, * Leftovers would be "item: Leftovers", confusion the status * condition would be "confusion", etc. */ fullname: string; /** Effect type. */ effectType: EffectType; /** * Does it exist? For historical reasons, when you use an accessor * for an effect that doesn't exist, you get a dummy effect that * doesn't do anything, and this field set to false. */ exists: boolean; /** * Dex number? For a Pokemon, this is the National Dex number. For * other effects, this is often an internal ID (e.g. a move * number). Not all effects have numbers, this will be 0 if it * doesn't. Nonstandard effects (e.g. CAP effects) will have * negative numbers. */ num: number; /** * The generation of Pokemon game this was INTRODUCED (NOT * necessarily the current gen being simulated.) Not all effects * track generation; this will be 0 if not known. */ gen: number; /** * A shortened form of the description of this effect. * Not all effects have this. */ shortDesc: string; /** The full description for this effect. */ desc: string; /** * Is this item/move/ability/pokemon nonstandard? Specified for effects * that have no use in standard formats: made-up pokemon (CAP), * glitches (MissingNo etc), Pokestar pokemon, etc. */ isNonstandard: Nonstandard | null; /** The duration of the condition - only for pure conditions. */ duration?: number; /** Whether or not the condition is ignored by Baton Pass - only for pure conditions. */ noCopy: boolean; /** Whether or not the condition affects fainted Pokemon. */ affectsFainted: boolean; /** Moves only: what status does it set? */ status?: ID; /** Moves only: what weather does it set? */ weather?: ID; /** ??? */ sourceEffect: string; constructor(data: AnyObject) { this.name = Utils.getString(data.name).trim(); this.id = data.realMove ? toID(data.realMove) : toID(this.name); // Hidden Power hack this.fullname = Utils.getString(data.fullname) || this.name; this.effectType = Utils.getString(data.effectType) as EffectType || 'Condition'; this.exists = data.exists ?? !!this.id; this.num = data.num || 0; this.gen = data.gen || 0; this.shortDesc = data.shortDesc || ''; this.desc = data.desc || ''; this.isNonstandard = data.isNonstandard || null; this.duration = data.duration; this.noCopy = !!data.noCopy; this.affectsFainted = !!data.affectsFainted; this.status = data.status as ID || undefined; this.weather = data.weather as ID || undefined; this.sourceEffect = data.sourceEffect || ''; } toString() { return this.name; } } export class Nature extends BasicEffect implements Readonly<BasicEffect & NatureData> { readonly effectType: 'Nature'; readonly plus?: StatIDExceptHP; readonly minus?: StatIDExceptHP; constructor(data: AnyObject) { super(data); this.fullname = `nature: ${this.name}`; this.effectType = 'Nature'; this.gen = 3; this.plus = data.plus || undefined; this.minus = data.minus || undefined; assignMissingFields(this, data); } } const EMPTY_NATURE = Utils.deepFreeze(new Nature({ name: '', exists: false })); export interface NatureData { name: string; plus?: StatIDExceptHP; minus?: StatIDExceptHP; } export type ModdedNatureData = NatureData | Partial<Omit<NatureData, 'name'>> & { inherit: true }; export interface NatureDataTable { [natureid: IDEntry]: NatureData } export class DexNatures { readonly dex: ModdedDex; readonly natureCache = new Map<ID, Nature>(); allCache: readonly Nature[] | null = null; constructor(dex: ModdedDex) { this.dex = dex; } get(name: string | Nature): Nature { if (name && typeof name !== 'string') return name; return this.getByID(toID(name)); } getByID(id: ID): Nature { if (id === '') return EMPTY_NATURE; let nature = this.natureCache.get(id); if (nature) return nature; if (this.dex.data.Aliases.hasOwnProperty(id)) { nature = this.get(this.dex.data.Aliases[id]); if (nature.exists) { this.natureCache.set(id, nature); } return nature; } if (id && this.dex.data.Natures.hasOwnProperty(id)) { const natureData = this.dex.data.Natures[id]; nature = new Nature(natureData); if (nature.gen > this.dex.gen) nature.isNonstandard = 'Future'; } else { nature = new Nature({ name: id, exists: false }); } if (nature.exists) this.natureCache.set(id, this.dex.deepFreeze(nature)); return nature; } all(): readonly Nature[] { if (this.allCache) return this.allCache; const natures = []; for (const id in this.dex.data.Natures) { natures.push(this.getByID(id as ID)); } this.allCache = Object.freeze(natures); return this.allCache; } } export interface TypeData { damageTaken: { [attackingTypeNameOrEffectid: string]: number }; HPdvs?: SparseStatsTable; HPivs?: SparseStatsTable; isNonstandard?: Nonstandard | null; } export type ModdedTypeData = TypeData | Partial<Omit<TypeData, 'name'>> & { inherit: true }; export interface TypeDataTable { [typeid: IDEntry]: TypeData } export interface ModdedTypeDataTable { [typeid: IDEntry]: ModdedTypeData } type TypeInfoEffectType = 'Type' | 'EffectType'; export class TypeInfo implements Readonly<TypeData> { /** * ID. This will be a lowercase version of the name with all the * non-alphanumeric characters removed. e.g. 'flying' */ readonly id: ID; /** Name. e.g. 'Flying' */ readonly name: string; /** Effect type. */ readonly effectType: TypeInfoEffectType; /** * Does it exist? For historical reasons, when you use an accessor * for an effect that doesn't exist, you get a dummy effect that * doesn't do anything, and this field set to false. */ readonly exists: boolean; /** * The generation of Pokemon game this was INTRODUCED (NOT * necessarily the current gen being simulated.) Not all effects * track generation; this will be 0 if not known. */ readonly gen: number; /** * Set to 'Future' for types before they're released (like Fairy * in Gen 5 or Dark in Gen 1). */ readonly isNonstandard: Nonstandard | null; /** * Type chart, attackingTypeName:result, effectid:result * result is: 0 = normal, 1 = weakness, 2 = resistance, 3 = immunity */ readonly damageTaken: { [attackingTypeNameOrEffectid: string]: number }; /** The IVs to get this Type Hidden Power (in gen 3 and later) */ readonly HPivs: SparseStatsTable; /** The DVs to get this Type Hidden Power (in gen 2). */ readonly HPdvs: SparseStatsTable; constructor(data: AnyObject) { this.name = data.name; this.id = data.id; this.effectType = Utils.getString(data.effectType) as TypeInfoEffectType || 'Type'; this.exists = data.exists ?? !!this.id; this.gen = data.gen || 0; this.isNonstandard = data.isNonstandard || null; this.damageTaken = data.damageTaken || {}; this.HPivs = data.HPivs || {}; this.HPdvs = data.HPdvs || {}; assignMissingFields(this, data); } toString() { return this.name; } } const EMPTY_TYPE_INFO = Utils.deepFreeze(new TypeInfo({ name: '', id: '', exists: false, effectType: 'EffectType' })); export class DexTypes { readonly dex: ModdedDex; readonly typeCache = new Map<ID, TypeInfo>(); allCache: readonly TypeInfo[] | null = null; namesCache: readonly string[] | null = null; constructor(dex: ModdedDex) { this.dex = dex; } get(name: string | TypeInfo): TypeInfo { if (name && typeof name !== 'string') return name; return this.getByID(toID(name)); } getByID(id: ID): TypeInfo { if (id === '') return EMPTY_TYPE_INFO; let type = this.typeCache.get(id); if (type) return type; const typeName = id.charAt(0).toUpperCase() + id.substr(1); if (typeName && this.dex.data.TypeChart.hasOwnProperty(id)) { type = new TypeInfo({ name: typeName, id, ...this.dex.data.TypeChart[id] }); } else { type = new TypeInfo({ name: typeName, id, exists: false, effectType: 'EffectType' }); } if (type.exists) this.typeCache.set(id, this.dex.deepFreeze(type)); return type; } names(): readonly string[] { if (this.namesCache) return this.namesCache; this.namesCache = this.all().filter(type => !type.isNonstandard).map(type => type.name); return this.namesCache; } isName(name: string): boolean { const id = name.toLowerCase(); const typeName = id.charAt(0).toUpperCase() + id.substr(1); return name === typeName && this.dex.data.TypeChart.hasOwnProperty(id); } all(): readonly TypeInfo[] { if (this.allCache) return this.allCache; const types = []; for (const id in this.dex.data.TypeChart) { types.push(this.getByID(id as ID)); } this.allCache = Object.freeze(types); return this.allCache; } } const idsCache: readonly StatID[] = ['hp', 'atk', 'def', 'spa', 'spd', 'spe']; const reverseCache: { readonly [k: IDEntry]: StatID } = { __proto: null as any, "hitpoints": 'hp', "attack": 'atk', "defense": 'def', "specialattack": 'spa', "spatk": 'spa', "spattack": 'spa', "specialatk": 'spa', "special": 'spa', "spc": 'spa', "specialdefense": 'spd', "spdef": 'spd', "spdefense": 'spd', "specialdef": 'spd', "speed": 'spe', }; export class DexStats { readonly shortNames: { readonly [k in StatID]: string }; readonly mediumNames: { readonly [k in StatID]: string }; readonly names: { readonly [k in StatID]: string }; constructor(dex: ModdedDex) { if (dex.gen !== 1) { this.shortNames = { __proto__: null, hp: "HP", atk: "Atk", def: "Def", spa: "SpA", spd: "SpD", spe: "Spe", } as any; this.mediumNames = { __proto__: null, hp: "HP", atk: "Attack", def: "Defense", spa: "Sp. Atk", spd: "Sp. Def", spe: "Speed", } as any; this.names = { __proto__: null, hp: "HP", atk: "Attack", def: "Defense", spa: "Special Attack", spd: "Special Defense", spe: "Speed", } as any; } else { this.shortNames = { __proto__: null, hp: "HP", atk: "Atk", def: "Def", spa: "Spc", spd: "[SpD]", spe: "Spe", } as any; this.mediumNames = { __proto__: null, hp: "HP", atk: "Attack", def: "Defense", spa: "Special", spd: "[Sp. Def]", spe: "Speed", } as any; this.names = { __proto__: null, hp: "HP", atk: "Attack", def: "Defense", spa: "Special", spd: "[Special Defense]", spe: "Speed", } as any; } } getID(name: string) { if (name === 'Spd') return 'spe' as StatID; const id = toID(name); if (reverseCache[id]) return reverseCache[id]; if (idsCache.includes(id as StatID)) return id as StatID; return null; } ids(): typeof idsCache { return idsCache; } }