Spaces:
Paused
Paused
| /** | |
| * 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; | |
| } | |
| } | |