mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
* Remove hardcoding of formes inheriting learnsets * Fix learnset inheritance * Update learnsets.ts
809 lines
30 KiB
TypeScript
809 lines
30 KiB
TypeScript
import { assignMissingFields, BasicEffect, toID } from './dex-data';
|
||
import { Utils } from '../lib/utils';
|
||
import { isDeepStrictEqual } from 'node:util';
|
||
|
||
interface SpeciesAbility {
|
||
0: string;
|
||
1?: string;
|
||
H?: string;
|
||
S?: string;
|
||
}
|
||
|
||
type SpeciesTag = "Mythical" | "Restricted Legendary" | "Sub-Legendary" | "Ultra Beast" | "Paradox";
|
||
|
||
export interface SpeciesData extends Partial<Species> {
|
||
name: string;
|
||
/** National Dex number */
|
||
num: number;
|
||
|
||
types: string[];
|
||
abilities: SpeciesAbility;
|
||
baseStats: StatsTable;
|
||
eggGroups: string[];
|
||
weightkg: number;
|
||
}
|
||
export interface CosmeticFormeData {
|
||
isCosmeticForme: boolean;
|
||
name: string;
|
||
baseSpecies: string;
|
||
forme: string;
|
||
color: string;
|
||
}
|
||
|
||
export type ModdedSpeciesData = SpeciesData | CosmeticFormeData |
|
||
Partial<Omit<SpeciesData, 'name'>> & { inherit: true } |
|
||
Partial<Omit<CosmeticFormeData, 'isCosmeticForme'>> & { inherit: true };
|
||
|
||
export interface SpeciesFormatsData {
|
||
doublesTier?: TierTypes.Doubles | TierTypes.Other;
|
||
gmaxUnreleased?: boolean;
|
||
isNonstandard?: Nonstandard | null;
|
||
natDexTier?: TierTypes.Singles | TierTypes.Other;
|
||
tier?: TierTypes.Singles | TierTypes.Other;
|
||
}
|
||
|
||
export type ModdedSpeciesFormatsData = SpeciesFormatsData & { inherit?: true };
|
||
|
||
export interface LearnsetData {
|
||
learnset?: { [moveid: IDEntry]: MoveSource[] };
|
||
eventData?: EventInfo[];
|
||
eventOnly?: boolean;
|
||
encounters?: EventInfo[];
|
||
exists?: boolean;
|
||
}
|
||
|
||
export type ModdedLearnsetData = LearnsetData & { inherit?: true };
|
||
|
||
export interface PokemonGoData {
|
||
encounters?: string[];
|
||
LGPERestrictiveMoves?: { [moveid: string]: number | null };
|
||
}
|
||
|
||
export interface SpeciesDataTable { [speciesid: IDEntry]: SpeciesData | CosmeticFormeData }
|
||
export interface ModdedSpeciesDataTable { [speciesid: IDEntry]: ModdedSpeciesData }
|
||
export interface SpeciesFormatsDataTable { [speciesid: IDEntry]: SpeciesFormatsData }
|
||
export interface ModdedSpeciesFormatsDataTable { [speciesid: IDEntry]: ModdedSpeciesFormatsData }
|
||
export interface LearnsetDataTable { [speciesid: IDEntry]: LearnsetData }
|
||
export interface ModdedLearnsetDataTable { [speciesid: IDEntry]: ModdedLearnsetData }
|
||
export interface PokemonGoDataTable { [speciesid: IDEntry]: PokemonGoData }
|
||
|
||
/**
|
||
* Describes a possible way to get a move onto a pokemon.
|
||
*
|
||
* First character is a generation number, 1-9.
|
||
* Second character is a source ID, one of:
|
||
*
|
||
* - M = TM/HM
|
||
* - T = tutor
|
||
* - L = start or level-up, 3rd char+ is the level
|
||
* - R = restricted (special moves like Rotom moves)
|
||
* - E = egg
|
||
* - D = Dream World, only 5D is valid
|
||
* - S = event, 3rd char+ is the index in .eventData
|
||
* - V = Virtual Console or Let's Go transfer, only 7V/8V is valid
|
||
* - C = NOT A REAL SOURCE, see note, only 3C/4C is valid
|
||
*
|
||
* C marks certain moves learned by a pokemon's prevo. It's used to
|
||
* work around the chainbreeding checker's shortcuts for performance;
|
||
* it lets the pokemon be a valid father for teaching the move, but
|
||
* is otherwise ignored by the learnset checker (which will actually
|
||
* check prevos for compatibility).
|
||
*/
|
||
export type MoveSource = `${
|
||
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
|
||
}${
|
||
'M' | 'T' | 'L' | 'R' | 'E' | 'D' | 'S' | 'V' | 'C'
|
||
}${string}`;
|
||
|
||
export class Species extends BasicEffect implements Readonly<BasicEffect & SpeciesFormatsData> {
|
||
declare readonly effectType: 'Pokemon';
|
||
/**
|
||
* Species ID. Identical to ID. Note that this is the full ID, e.g.
|
||
* 'basculinbluestriped'. To get the base species ID, you need to
|
||
* manually read toID(species.baseSpecies).
|
||
*/
|
||
declare readonly id: ID;
|
||
/**
|
||
* Name. Note that this is the full name with forme,
|
||
* e.g. 'Basculin-Blue-Striped'. To get the name without forme, see
|
||
* `species.baseSpecies`.
|
||
*/
|
||
declare readonly name: string;
|
||
/**
|
||
* Base species. Species, but without the forme name.
|
||
*
|
||
* DO NOT ASSUME A POKEMON CAN TRANSFORM FROM `baseSpecies` TO
|
||
* `species`. USE `changesFrom` FOR THAT.
|
||
*/
|
||
readonly baseSpecies: string;
|
||
/**
|
||
* Forme name. If the forme exists,
|
||
* `species.name === species.baseSpecies + '-' + species.forme`
|
||
*
|
||
* The games make a distinction between Forme (foorumu) (legendary Pokémon)
|
||
* and Form (sugata) (non-legendary Pokémon). PS does not use the same
|
||
* distinction – they're all "Forme" to PS, reflecting current community
|
||
* use of the term.
|
||
*
|
||
* This property only tracks non-cosmetic formes, and will be `''` for
|
||
* cosmetic formes.
|
||
*/
|
||
readonly forme: string;
|
||
/**
|
||
* Base forme name (e.g. 'Altered' for Giratina).
|
||
*/
|
||
readonly baseForme: string;
|
||
/**
|
||
* Other forms. List of names of cosmetic forms. These should have
|
||
* `aliases.js` aliases to this entry, but not have their own
|
||
* entry in `pokedex.js`.
|
||
*/
|
||
readonly cosmeticFormes?: string[];
|
||
/**
|
||
* Other formes. List of names of formes, appears only on the base
|
||
* forme. Unlike forms, these have their own entry in `pokedex.js`.
|
||
*/
|
||
readonly otherFormes?: string[];
|
||
/**
|
||
* List of forme speciesNames in the order they appear in the game data -
|
||
* the union of baseSpecies, otherFormes and cosmeticFormes. Appears only on
|
||
* the base species forme.
|
||
*
|
||
* A species's alternate formeindex may change from generation to generation -
|
||
* the forme with index N in Gen A is not guaranteed to be the same forme as the
|
||
* forme with index in Gen B.
|
||
*
|
||
* Gigantamaxes are not considered formes by the game (see data/FORMES.md - PS
|
||
* labels them as such for convenience) - Gigantamax "formes" are instead included at
|
||
* the end of the formeOrder list so as not to interfere with the correct index numbers.
|
||
*/
|
||
readonly formeOrder?: string[];
|
||
/**
|
||
* Sprite ID. Basically the same as ID, but with a dash between
|
||
* species and forme.
|
||
*/
|
||
readonly spriteid: string;
|
||
/** Abilities. */
|
||
readonly abilities: SpeciesAbility;
|
||
/** Types. */
|
||
readonly types: string[];
|
||
/** Added type (added by Trick-Or-Treat or Forest's Curse, but only listed in species by OMs). */
|
||
readonly addedType?: string;
|
||
/** Pre-evolution. '' if nothing evolves into this Pokemon. */
|
||
readonly prevo: string;
|
||
/** Evolutions. Array because many Pokemon have multiple evolutions. */
|
||
readonly evos: string[];
|
||
readonly evoType?: 'trade' | 'useItem' | 'levelMove' | 'levelExtra' | 'levelFriendship' | 'levelHold' | 'other';
|
||
/** Evolution condition. falsy if doesn't evolve. */
|
||
declare readonly evoCondition?: string;
|
||
/** Evolution item. falsy if doesn't evolve. */
|
||
declare readonly evoItem?: string;
|
||
/** Evolution move. falsy if doesn't evolve. */
|
||
readonly evoMove?: string;
|
||
/** Region required to be in for evolution. falsy if doesn't evolve. */
|
||
readonly evoRegion?: 'Alola' | 'Galar';
|
||
/** Evolution level. falsy if doesn't evolve. */
|
||
readonly evoLevel?: number;
|
||
/** Is NFE? True if this Pokemon can evolve (Mega evolution doesn't count). */
|
||
readonly nfe: boolean;
|
||
/** Egg groups. */
|
||
readonly eggGroups: string[];
|
||
/** True if this species can hatch from an Egg. */
|
||
readonly canHatch: boolean;
|
||
/** True if this species is a purely cosmetic forme. */
|
||
readonly isCosmeticForme: boolean;
|
||
/**
|
||
* Gender. M = always male, F = always female, N = always
|
||
* genderless, '' = sometimes male sometimes female.
|
||
*/
|
||
readonly gender: GenderName;
|
||
/** Gender ratio. Should add up to 1 unless genderless. */
|
||
readonly genderRatio: { M: number, F: number };
|
||
/** Base stats. */
|
||
readonly baseStats: StatsTable;
|
||
/** Max HP. Overrides usual HP calculations (for Shedinja). */
|
||
readonly maxHP?: number;
|
||
/** A Pokemon's Base Stat Total */
|
||
readonly bst: number;
|
||
/** Weight (in kg). Not valid for OMs; use weighthg / 10 instead. */
|
||
readonly weightkg: number;
|
||
/** Weight (in integer multiples of 0.1kg). */
|
||
readonly weighthg: number;
|
||
/** Height (in m). */
|
||
readonly heightm: number;
|
||
/** Color. */
|
||
readonly color: string;
|
||
/**
|
||
* Tags, boolean data. Currently just legendary/mythical status.
|
||
*/
|
||
readonly tags: SpeciesTag[];
|
||
/** Does this Pokemon have an unreleased hidden ability? */
|
||
readonly unreleasedHidden: boolean | 'Past';
|
||
/**
|
||
* Is it only possible to get the hidden ability on a male pokemon?
|
||
* This is mainly relevant to Gen 5.
|
||
*/
|
||
readonly maleOnlyHidden: boolean;
|
||
/** Possible mother for a male-only Pokemon. */
|
||
readonly mother?: string;
|
||
/** True if a pokemon is mega. */
|
||
readonly isMega?: boolean;
|
||
/** True if a pokemon is primal. */
|
||
declare readonly isPrimal?: boolean;
|
||
/** Name of its Gigantamax move, if a pokemon is capable of gigantamaxing. */
|
||
readonly canGigantamax?: string;
|
||
/** If this Pokemon can gigantamax, is its gigantamax released? */
|
||
readonly gmaxUnreleased?: boolean;
|
||
/** True if a Pokemon species is incapable of dynamaxing */
|
||
readonly cannotDynamax?: boolean;
|
||
/** The Tera Type this Pokemon is forced to use */
|
||
readonly requiredTeraType?: string;
|
||
/** What it transforms from, if a pokemon is a forme that is only accessible in battle. */
|
||
readonly battleOnly?: string | string[];
|
||
/** Required item. Do not use this directly; see requiredItems. */
|
||
readonly requiredItem?: string;
|
||
/** Required move. Move required to use this forme in-battle. */
|
||
declare readonly requiredMove?: string;
|
||
/** Required ability. Ability required to use this forme in-battle. */
|
||
declare readonly requiredAbility?: string;
|
||
/**
|
||
* Required items. Items required to be in this forme, e.g. a mega
|
||
* stone, or Griseous Orb. Array because Arceus formes can hold
|
||
* either a Plate or a Z-Crystal.
|
||
*/
|
||
readonly requiredItems?: string[];
|
||
|
||
/**
|
||
* Formes that can transform into this Pokemon, to inherit learnsets
|
||
* from. (Like `prevo`, but for transformations that aren't
|
||
* technically evolution. Includes in-battle transformations like
|
||
* Zen Mode and out-of-battle transformations like Rotom.)
|
||
*
|
||
* Not filled out for megas/primals - fall back to baseSpecies
|
||
* for in-battle formes.
|
||
*/
|
||
readonly changesFrom?: string;
|
||
|
||
/**
|
||
* List of sources and other availability for a Pokemon transferred from
|
||
* Pokemon GO.
|
||
*/
|
||
readonly pokemonGoData?: string[];
|
||
|
||
/**
|
||
* Singles Tier. The Pokemon's location in the Smogon tier system.
|
||
*/
|
||
readonly tier: TierTypes.Singles | TierTypes.Other;
|
||
/**
|
||
* Doubles Tier. The Pokemon's location in the Smogon doubles tier system.
|
||
*/
|
||
readonly doublesTier: TierTypes.Doubles | TierTypes.Other;
|
||
/**
|
||
* National Dex Tier. The Pokemon's location in the Smogon National Dex tier system.
|
||
*/
|
||
readonly natDexTier: TierTypes.Singles | TierTypes.Other;
|
||
|
||
constructor(data: AnyObject) {
|
||
super(data);
|
||
|
||
this.fullname = `pokemon: ${data.name}`;
|
||
this.effectType = 'Pokemon';
|
||
this.baseSpecies = data.baseSpecies || this.name;
|
||
this.forme = data.forme || '';
|
||
this.baseForme = data.baseForme || '';
|
||
this.cosmeticFormes = data.cosmeticFormes || undefined;
|
||
this.otherFormes = data.otherFormes || undefined;
|
||
this.formeOrder = data.formeOrder || undefined;
|
||
this.spriteid = data.spriteid ||
|
||
(toID(this.baseSpecies) + (this.baseSpecies !== this.name ? `-${toID(this.forme)}` : ''));
|
||
this.abilities = data.abilities || { 0: "" };
|
||
this.types = data.types || ['???'];
|
||
this.addedType = data.addedType || undefined;
|
||
this.prevo = data.prevo || '';
|
||
this.tier = data.tier || '';
|
||
this.doublesTier = data.doublesTier || '';
|
||
this.natDexTier = data.natDexTier || '';
|
||
this.evos = data.evos || [];
|
||
this.evoType = data.evoType || undefined;
|
||
this.evoMove = data.evoMove || undefined;
|
||
this.evoLevel = data.evoLevel || undefined;
|
||
this.nfe = data.nfe || false;
|
||
this.eggGroups = data.eggGroups || [];
|
||
this.canHatch = data.canHatch || false;
|
||
this.gender = data.gender || '';
|
||
this.genderRatio = data.genderRatio || (this.gender === 'M' ? { M: 1, F: 0 } :
|
||
this.gender === 'F' ? { M: 0, F: 1 } :
|
||
this.gender === 'N' ? { M: 0, F: 0 } :
|
||
{ M: 0.5, F: 0.5 });
|
||
this.requiredItem = data.requiredItem || undefined;
|
||
this.requiredItems = data.requiredItems || (this.requiredItem ? [this.requiredItem] : undefined);
|
||
this.baseStats = data.baseStats || { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
|
||
this.bst = this.baseStats.hp + this.baseStats.atk + this.baseStats.def +
|
||
this.baseStats.spa + this.baseStats.spd + this.baseStats.spe;
|
||
this.weightkg = data.weightkg || 0;
|
||
this.weighthg = this.weightkg * 10;
|
||
this.heightm = data.heightm || 0;
|
||
this.color = data.color || '';
|
||
this.isCosmeticForme = data.isCosmeticForme || undefined;
|
||
this.tags = data.tags || [];
|
||
this.unreleasedHidden = data.unreleasedHidden || false;
|
||
this.maleOnlyHidden = !!data.maleOnlyHidden;
|
||
this.maxHP = data.maxHP || undefined;
|
||
this.isMega = this.forme.includes('Mega') || undefined;
|
||
this.isPrimal = this.forme === 'Primal' || undefined;
|
||
this.canGigantamax = data.canGigantamax || undefined;
|
||
this.gmaxUnreleased = !!data.gmaxUnreleased;
|
||
this.cannotDynamax = !!data.cannotDynamax;
|
||
this.battleOnly = data.battleOnly || (this.isMega || this.isPrimal ? this.baseSpecies : undefined);
|
||
this.changesFrom = data.changesFrom ||
|
||
(this.battleOnly !== this.baseSpecies ? this.battleOnly : this.baseSpecies);
|
||
if (Array.isArray(this.changesFrom)) this.changesFrom = this.changesFrom[0];
|
||
this.pokemonGoData = data.pokemonGoData || undefined;
|
||
|
||
if (!this.gen && this.num >= 1) {
|
||
if (this.num >= 906 || this.forme.includes('Paldea')) {
|
||
this.gen = 9;
|
||
} else if (this.num >= 810 || ['Gmax', 'Galar', 'Galar-Zen', 'Hisui'].includes(this.forme)) {
|
||
this.gen = 8;
|
||
} else if (this.num >= 722 || this.forme.startsWith('Alola') || this.forme === 'Starter') {
|
||
this.gen = 7;
|
||
} else if (this.num >= 650 || this.isMega || this.isPrimal) {
|
||
this.gen = 6;
|
||
} else if (this.num >= 494) {
|
||
this.gen = 5;
|
||
} else if (this.num >= 387) {
|
||
this.gen = 4;
|
||
} else if (this.num >= 252) {
|
||
this.gen = 3;
|
||
} else if (this.num >= 152) {
|
||
this.gen = 2;
|
||
} else {
|
||
this.gen = 1;
|
||
}
|
||
}
|
||
assignMissingFields(this, data);
|
||
}
|
||
}
|
||
|
||
const EMPTY_SPECIES = Utils.deepFreeze(new Species({
|
||
id: '', name: '', exists: false,
|
||
tier: 'Illegal', doublesTier: 'Illegal',
|
||
natDexTier: 'Illegal', isNonstandard: 'Custom',
|
||
}));
|
||
|
||
export class Learnset {
|
||
readonly effectType: 'Learnset';
|
||
/**
|
||
* Keeps track of exactly how a pokemon might learn a move, in the
|
||
* form moveid:sources[].
|
||
*/
|
||
readonly learnset?: { [moveid: string]: MoveSource[] };
|
||
/** True if the only way to get this Pokemon is from events. */
|
||
readonly eventOnly: boolean;
|
||
/** List of event data for each event. */
|
||
readonly eventData?: EventInfo[];
|
||
readonly encounters?: EventInfo[];
|
||
readonly exists: boolean;
|
||
readonly species: Species;
|
||
|
||
constructor(data: AnyObject, species: Species) {
|
||
this.exists = true;
|
||
this.effectType = 'Learnset';
|
||
this.learnset = data.learnset || undefined;
|
||
this.eventOnly = !!data.eventOnly;
|
||
this.eventData = data.eventData || undefined;
|
||
this.encounters = data.encounters || undefined;
|
||
this.species = species;
|
||
|
||
const eventData = Utils.deepClone(this.eventData);
|
||
let update = false;
|
||
if (eventData) {
|
||
for (const eventInfo of eventData) {
|
||
if (eventInfo.source === 'gen8legends') {
|
||
eventInfo.pokeball = 'strangeball';
|
||
update = true;
|
||
}
|
||
}
|
||
}
|
||
if (update) this.eventData = Utils.deepFreeze(eventData);
|
||
}
|
||
}
|
||
|
||
export class DexSpecies {
|
||
readonly dex: ModdedDex;
|
||
readonly speciesCache = new Map<ID, Species>();
|
||
readonly learnsetCache = new Map<ID, Learnset>();
|
||
allCache: readonly Species[] | null = null;
|
||
|
||
constructor(dex: ModdedDex) {
|
||
this.dex = dex;
|
||
}
|
||
|
||
get(name?: string | Species): Species {
|
||
if (name && typeof name !== 'string') return name;
|
||
|
||
let id = '' as ID;
|
||
if (name) {
|
||
name = name.trim();
|
||
id = toID(name);
|
||
if (id === 'nidoran' && name.endsWith('♀')) {
|
||
id = 'nidoranf' as ID;
|
||
} else if (id === 'nidoran' && name.endsWith('♂')) {
|
||
id = 'nidoranm' as ID;
|
||
}
|
||
}
|
||
return this.getByID(id);
|
||
}
|
||
|
||
getByID(id: ID): Species {
|
||
if (id === '' || id === 'constructor') return EMPTY_SPECIES;
|
||
let species: Mutable<Species> | undefined = this.speciesCache.get(id);
|
||
if (species) return species;
|
||
|
||
const alias = this.dex.getAlias(id);
|
||
if (alias) {
|
||
if (this.dex.data.FormatsData.hasOwnProperty(id)) {
|
||
// special event ID
|
||
species = new Species({
|
||
...this.dex.data.Pokedex[alias],
|
||
...this.dex.data.FormatsData[id],
|
||
name: id,
|
||
});
|
||
species.abilities = { 0: species.abilities['S']! };
|
||
} else {
|
||
species = this.get(alias);
|
||
if (this.dex.data.Pokedex?.[id]?.isCosmeticForme) {
|
||
const cosmeticForme = this.dex.data.Pokedex[id];
|
||
species = new Species({
|
||
...species,
|
||
...cosmeticForme,
|
||
name: species.baseSpecies + '-' + cosmeticForme.forme!, // Forme always exists on cosmetic forme entries
|
||
baseForme: "",
|
||
otherFormes: null,
|
||
cosmeticFormes: null,
|
||
});
|
||
}
|
||
if (species.cosmeticFormes) {
|
||
for (const forme of species.cosmeticFormes) {
|
||
if (this.dex.data.Pokedex.hasOwnProperty(toID(forme))) continue;
|
||
if (toID(forme) === id) {
|
||
species = new Species({
|
||
...species,
|
||
name: forme,
|
||
forme: forme.slice(species.name.length + 1),
|
||
baseSpecies: species.name,
|
||
baseForme: "",
|
||
isCosmeticForme: true,
|
||
otherFormes: null,
|
||
cosmeticFormes: null,
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
this.speciesCache.set(id, this.dex.deepFreeze(species));
|
||
return species;
|
||
}
|
||
|
||
if (!this.dex.data.Pokedex.hasOwnProperty(id)) {
|
||
let aliasTo = '';
|
||
const formeNames: { [k: IDEntry]: IDEntry[] } = {
|
||
alola: ['a', 'alola', 'alolan'],
|
||
galar: ['g', 'galar', 'galarian'],
|
||
hisui: ['h', 'hisui', 'hisuian'],
|
||
paldea: ['p', 'paldea', 'paldean'],
|
||
mega: ['m', 'mega'],
|
||
primal: ['p', 'primal'],
|
||
};
|
||
for (const forme in formeNames) {
|
||
let pokeName = '';
|
||
for (const i of formeNames[forme as ID]) {
|
||
if (id.startsWith(i)) {
|
||
pokeName = id.slice(i.length);
|
||
} else if (id.endsWith(i)) {
|
||
pokeName = id.slice(0, -i.length);
|
||
}
|
||
}
|
||
pokeName = this.dex.getAlias(pokeName as ID) || pokeName;
|
||
if (this.dex.data.Pokedex[pokeName + forme]) {
|
||
aliasTo = pokeName + forme;
|
||
break;
|
||
}
|
||
}
|
||
if (aliasTo) {
|
||
species = this.get(aliasTo);
|
||
if (species.exists) {
|
||
this.speciesCache.set(id, species);
|
||
return species;
|
||
}
|
||
}
|
||
}
|
||
if (id && this.dex.data.Pokedex.hasOwnProperty(id)) {
|
||
const pokedexData = this.dex.data.Pokedex[id];
|
||
const baseSpeciesTags = pokedexData.baseSpecies && this.dex.data.Pokedex[toID(pokedexData.baseSpecies)].tags;
|
||
species = new Species({
|
||
tags: baseSpeciesTags,
|
||
...pokedexData,
|
||
...this.dex.data.FormatsData[id],
|
||
});
|
||
// Inherit any statuses from the base species (Arceus, Silvally).
|
||
const baseSpeciesStatuses = this.dex.data.Conditions[toID(species.baseSpecies)];
|
||
if (baseSpeciesStatuses !== undefined) {
|
||
for (const key in baseSpeciesStatuses) {
|
||
if (!(key in species)) {
|
||
(species as any)[key] = (baseSpeciesStatuses as any)[key];
|
||
}
|
||
}
|
||
}
|
||
if (!species.tier && !species.doublesTier && !species.natDexTier && species.baseSpecies !== species.name) {
|
||
if (species.baseSpecies === 'Mimikyu') {
|
||
species.tier = this.dex.data.FormatsData[toID(species.baseSpecies)].tier || 'Illegal';
|
||
species.doublesTier = this.dex.data.FormatsData[toID(species.baseSpecies)].doublesTier || species.tier as any;
|
||
species.natDexTier = this.dex.data.FormatsData[toID(species.baseSpecies)].natDexTier || species.tier;
|
||
} else if (species.id.endsWith('totem')) {
|
||
species.tier = this.dex.data.FormatsData[species.id.slice(0, -5)].tier || 'Illegal';
|
||
species.doublesTier = this.dex.data.FormatsData[species.id.slice(0, -5)].doublesTier || species.tier as any;
|
||
species.natDexTier = this.dex.data.FormatsData[species.id.slice(0, -5)].natDexTier || species.tier;
|
||
} else if (species.battleOnly) {
|
||
species.tier = this.dex.data.FormatsData[toID(species.battleOnly)]?.tier || 'Illegal';
|
||
species.doublesTier = this.dex.data.FormatsData[toID(species.battleOnly)]?.doublesTier || species.tier as any;
|
||
species.natDexTier = this.dex.data.FormatsData[toID(species.battleOnly)]?.natDexTier || species.tier;
|
||
} else {
|
||
const baseFormatsData = this.dex.data.FormatsData[toID(species.baseSpecies)];
|
||
if (!baseFormatsData) {
|
||
throw new Error(`${species.baseSpecies} has no formats-data entry`);
|
||
}
|
||
species.tier = baseFormatsData.tier || 'Illegal';
|
||
species.doublesTier = baseFormatsData.doublesTier || species.tier as any;
|
||
species.natDexTier = baseFormatsData.natDexTier || species.tier;
|
||
}
|
||
}
|
||
if (!species.tier) species.tier = 'Illegal';
|
||
if (!species.doublesTier) species.doublesTier = species.tier as any;
|
||
if (!species.natDexTier) species.natDexTier = species.tier;
|
||
if (species.gen > this.dex.gen) {
|
||
species.tier = 'Illegal';
|
||
species.doublesTier = 'Illegal';
|
||
species.natDexTier = 'Illegal';
|
||
species.isNonstandard = 'Future';
|
||
}
|
||
if (this.dex.currentMod === 'gen7letsgo' && !species.isNonstandard) {
|
||
const isLetsGo = (
|
||
species.gen <= 7 && (species.num <= 151 || ['Meltan', 'Melmetal'].includes(species.name)) &&
|
||
(!species.forme || species.isMega || (['Alola', 'Starter'].includes(species.forme) &&
|
||
species.name !== 'Pikachu-Alola'))
|
||
);
|
||
if (!isLetsGo) species.isNonstandard = 'Past';
|
||
}
|
||
if (this.dex.currentMod === 'gen8bdsp' &&
|
||
(!species.isNonstandard || ["Gigantamax", "CAP"].includes(species.isNonstandard))) {
|
||
if (species.gen > 4 || (species.num < 1 && species.isNonstandard !== 'CAP') ||
|
||
species.id === 'pichuspikyeared') {
|
||
species.isNonstandard = 'Future';
|
||
species.tier = species.doublesTier = species.natDexTier = 'Illegal';
|
||
}
|
||
}
|
||
species.nfe = species.evos.some(evo => {
|
||
const evoSpecies = this.get(evo);
|
||
return !evoSpecies.isNonstandard ||
|
||
evoSpecies.isNonstandard === species?.isNonstandard ||
|
||
// Pokemon with Hisui evolutions
|
||
evoSpecies.isNonstandard === "Unobtainable";
|
||
});
|
||
species.canHatch = species.canHatch ||
|
||
(!['Ditto', 'Undiscovered'].includes(species.eggGroups[0]) && !species.prevo && species.name !== 'Manaphy');
|
||
if (this.dex.gen === 1) species.bst -= species.baseStats.spd;
|
||
if (this.dex.gen < 5) {
|
||
species.abilities = this.dex.deepClone(species.abilities);
|
||
delete species.abilities['H'];
|
||
}
|
||
if (this.dex.gen === 3 && this.dex.abilities.get(species.abilities['1']).gen === 4) delete species.abilities['1'];
|
||
|
||
if (this.dex.parentMod) {
|
||
// if this species is exactly identical to parentMod's species, reuse parentMod's copy
|
||
const parentMod = this.dex.mod(this.dex.parentMod);
|
||
if (this.dex.data.Pokedex[id] === parentMod.data.Pokedex[id]) {
|
||
const parentSpecies = parentMod.species.getByID(id);
|
||
// checking tier cheaply filters out some non-matches.
|
||
// The construction logic is very complex so we ultimately need to do a deep equality check
|
||
if (species.tier === parentSpecies.tier && isDeepStrictEqual(species, parentSpecies)) {
|
||
species = parentSpecies;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
species = new Species({
|
||
id, name: id,
|
||
exists: false, tier: 'Illegal', doublesTier: 'Illegal', natDexTier: 'Illegal', isNonstandard: 'Custom',
|
||
});
|
||
}
|
||
if (species.exists) this.speciesCache.set(id, this.dex.deepFreeze(species));
|
||
return species;
|
||
}
|
||
|
||
/**
|
||
* @param id the ID of the species the move pool belongs to
|
||
* @param isNatDex
|
||
* @returns a Set of IDs of the full valid movepool of the given species for the current generation/mod.
|
||
* Note that inter-move incompatibilities, such as those from exclusive events, are not considered and all moves are
|
||
* lumped together. However, Necturna and Necturine's Sketchable moves are omitted from this pool, as their fundamental
|
||
* incompatibility with each other is essential to the nature of those species.
|
||
*/
|
||
getMovePool(id: ID, isNatDex = false): Set<ID> {
|
||
let eggMovesOnly = false;
|
||
let maxGen = this.dex.gen;
|
||
const gen3HMMoves = ['cut', 'fly', 'surf', 'strength', 'flash', 'rocksmash', 'waterfall', 'dive'];
|
||
const gen4HMMoves = ['cut', 'fly', 'surf', 'strength', 'rocksmash', 'waterfall', 'rockclimb'];
|
||
const movePool = new Set<ID>();
|
||
for (const { species, learnset } of this.getFullLearnset(id)) {
|
||
if (!eggMovesOnly) eggMovesOnly = this.eggMovesOnly(species, this.get(id));
|
||
for (const moveid in learnset) {
|
||
if (species.isNonstandard !== 'CAP') {
|
||
if (gen4HMMoves.includes(moveid) && this.dex.gen >= 5) {
|
||
if (!learnset[moveid].some(source => parseInt(source.charAt(0)) >= 5 &&
|
||
parseInt(source.charAt(0)) <= this.dex.gen)) continue;
|
||
} else if (
|
||
gen3HMMoves.includes(moveid) && this.dex.gen >= 4 &&
|
||
!learnset[moveid].some(
|
||
source => parseInt(source.charAt(0)) >= 4 && parseInt(source.charAt(0)) <= this.dex.gen
|
||
)
|
||
) {
|
||
continue;
|
||
}
|
||
}
|
||
if (eggMovesOnly) {
|
||
if (learnset[moveid].some(source => source.startsWith('9E'))) {
|
||
movePool.add(moveid as ID);
|
||
}
|
||
} else if (maxGen >= 9) {
|
||
// Pokemon Home now strips learnsets on withdrawal
|
||
if (isNatDex || learnset[moveid].some(source => source.startsWith('9'))) {
|
||
movePool.add(moveid as ID);
|
||
}
|
||
} else {
|
||
if (learnset[moveid].some(source => parseInt(source.charAt(0)) <= maxGen)) {
|
||
movePool.add(moveid as ID);
|
||
}
|
||
}
|
||
if (moveid === 'sketch' && movePool.has('sketch' as ID)) {
|
||
if (species.isNonstandard === 'CAP') {
|
||
// Given what this function is generally used for, adding all sketchable moves to Necturna and Necturine's
|
||
// movepools would be undesirable as it would be impossible to tell sketched moves apart from normal ones
|
||
// so any code calling this one will need to get and handle those moves separately themselves
|
||
continue;
|
||
}
|
||
// Smeargle time
|
||
// A few moves like Dark Void were made unSketchable in a generation later than when they were introduced
|
||
// However, this has only happened in a gen where transfer moves are unavailable
|
||
const sketchables = this.dex.moves.all().filter(m => !m.flags['nosketch'] && !m.isNonstandard);
|
||
for (const move of sketchables) {
|
||
movePool.add(move.id);
|
||
}
|
||
// Smeargle has some event moves; they're all sketchable, so let's just skip them
|
||
break;
|
||
}
|
||
}
|
||
if (species.evoRegion) {
|
||
// species can only evolve in this gen, so prevo can't have any moves
|
||
// from after that gen
|
||
if (this.dex.gen >= 9) eggMovesOnly = true;
|
||
if (this.dex.gen === 8 && species.evoRegion === 'Alola') maxGen = 7;
|
||
}
|
||
}
|
||
return movePool;
|
||
}
|
||
|
||
getFullLearnset(id: ID): (Learnset & { learnset: NonNullable<Learnset['learnset']> })[] {
|
||
const originalSpecies = this.get(id);
|
||
let species: Species | null = originalSpecies;
|
||
const out: (Learnset & { learnset: NonNullable<Learnset['learnset']> })[] = [];
|
||
const alreadyChecked: { [k: string]: boolean } = {};
|
||
|
||
while (species?.name && !alreadyChecked[species.id]) {
|
||
alreadyChecked[species.id] = true;
|
||
const learnset = this.getLearnsetData(species.id);
|
||
if (learnset.learnset) {
|
||
out.push(learnset as any);
|
||
species = this.learnsetParent(species, true);
|
||
continue;
|
||
}
|
||
|
||
// no learnset
|
||
if ((species.changesFrom || species.baseSpecies) !== species.name) {
|
||
// forme without its own learnset
|
||
species = this.get(species.changesFrom || species.baseSpecies);
|
||
// warning: formes with their own learnset, like Wormadam, should NOT
|
||
// inherit from their base forme unless they're freely switchable
|
||
continue;
|
||
}
|
||
if (species.isNonstandard) {
|
||
// It's normal for a nonstandard species not to have learnset data
|
||
|
||
// Formats should replace the `Obtainable Moves` rule if they want to
|
||
// allow pokemon without learnsets.
|
||
return out;
|
||
}
|
||
if (species.prevo && this.getLearnsetData(toID(species.prevo)).learnset) {
|
||
species = this.get(toID(species.prevo));
|
||
continue;
|
||
}
|
||
|
||
// should never happen
|
||
throw new Error(`Species with no learnset data: ${species.id}`);
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
learnsetParent(species: Species, checkingMoves = false) {
|
||
// Own Tempo Rockruff and Battle Bond Greninja are special event formes
|
||
// that are visually indistinguishable from their base forme but have
|
||
// different learnsets. To prevent a leak, we make them show up as their
|
||
// base forme, but hardcode their learnsets into Rockruff-Dusk and
|
||
// Greninja-Ash
|
||
if (!this.getLearnsetData(species.id).learnset && species.forme) {
|
||
return this.get(species.changesFrom || species.baseSpecies);
|
||
} else if (species.prevo) {
|
||
// there used to be a check for Hidden Ability here, but apparently it's unnecessary
|
||
// Shed Skin Pupitar can definitely evolve into Unnerve Tyranitar
|
||
species = this.get(species.prevo);
|
||
if (species.gen > Math.max(2, this.dex.gen)) return null;
|
||
return species;
|
||
} else if (species.changesFrom && species.baseSpecies !== 'Kyurem') {
|
||
// For Pokemon like Rotom and Necrozma whose movesets are extensions are their base formes
|
||
return this.get(species.changesFrom);
|
||
} else if (
|
||
checkingMoves && !species.prevo && species.baseSpecies && this.get(species.baseSpecies).prevo
|
||
) {
|
||
// For Pokemon like Cap Pikachu, who should be able to have egg moves in Gen 9
|
||
let baseEvo = this.get(species.baseSpecies);
|
||
while (baseEvo.prevo) {
|
||
baseEvo = this.get(baseEvo.prevo);
|
||
}
|
||
return baseEvo;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Gets the raw learnset data for the species.
|
||
*
|
||
* In practice, if you're trying to figure out what moves a pokemon learns,
|
||
* you probably want to `getFullLearnset` or `getMovePool` instead.
|
||
*/
|
||
getLearnsetData(id: ID): Learnset {
|
||
let learnsetData = this.learnsetCache.get(id);
|
||
if (learnsetData) return learnsetData;
|
||
if (!this.dex.data.Learnsets.hasOwnProperty(id)) {
|
||
return new Learnset({ exists: false }, this.get(id));
|
||
}
|
||
learnsetData = new Learnset(this.dex.data.Learnsets[id], this.get(id));
|
||
this.learnsetCache.set(id, this.dex.deepFreeze(learnsetData));
|
||
return learnsetData;
|
||
}
|
||
|
||
getPokemonGoData(id: ID): PokemonGoData {
|
||
return this.dex.data.PokemonGoData[id];
|
||
}
|
||
|
||
all(): readonly Species[] {
|
||
if (this.allCache) return this.allCache;
|
||
const species = [];
|
||
for (const id in this.dex.data.Pokedex) {
|
||
species.push(this.getByID(id as ID));
|
||
}
|
||
this.allCache = Object.freeze(species);
|
||
return this.allCache;
|
||
}
|
||
|
||
eggMovesOnly(child: Species, father: Species | null) {
|
||
if (child.baseSpecies === father?.baseSpecies) return false;
|
||
while (father) {
|
||
if (father.name === child.name) return false;
|
||
father = this.learnsetParent(father);
|
||
}
|
||
return true;
|
||
}
|
||
}
|