Pokemon_server / sim /dex-data.ts
Jofthomas's picture
Jofthomas HF staff
Upload 4781 files
5c2ed06 verified
/**
* 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;
}
}