mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-12-26 19:49:22 +01:00
* Added `noBannedTypes` as a biome rule * Added `useShorthandAssign` rule * Added `useConsistentArrayType` * Update src/field/pokemon.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/pokeball.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Apply Biome after merge --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
1206 lines
38 KiB
TypeScript
1206 lines
38 KiB
TypeScript
import { determineEnemySpecies } from "#app/ai/ai-species-gen";
|
|
import type { AnySound } from "#app/battle-scene";
|
|
import type { GameMode } from "#app/game-mode";
|
|
import { globalScene } from "#app/global-scene";
|
|
import { speciesEggMoves } from "#balance/egg-moves";
|
|
import { starterPassiveAbilities } from "#balance/passives";
|
|
import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
|
import {
|
|
pokemonFormLevelMoves,
|
|
pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves,
|
|
pokemonSpeciesLevelMoves,
|
|
} from "#balance/pokemon-level-moves";
|
|
import { speciesStarterCosts } from "#balance/starters";
|
|
import { uncatchableSpecies } from "#data/data-lists";
|
|
import type { GrowthRate } from "#data/exp";
|
|
import { Gender } from "#data/gender";
|
|
import { AbilityId } from "#enums/ability-id";
|
|
import { DexAttr } from "#enums/dex-attr";
|
|
import { EvoLevelThresholdKind } from "#enums/evo-level-threshold-kind";
|
|
import { PartyMemberStrength } from "#enums/party-member-strength";
|
|
import type { PokemonType } from "#enums/pokemon-type";
|
|
import { SpeciesFormKey } from "#enums/species-form-key";
|
|
import { SpeciesId } from "#enums/species-id";
|
|
import type { Stat } from "#enums/stat";
|
|
import { loadPokemonVariantAssets } from "#sprites/pokemon-sprite";
|
|
import { hasExpSprite } from "#sprites/sprite-utils";
|
|
import type { Variant, VariantSet } from "#sprites/variant";
|
|
import { populateVariantColorCache, variantColorCache, variantData } from "#sprites/variant";
|
|
import type { Localizable } from "#types/locales";
|
|
import type { LevelMoves } from "#types/pokemon-level-moves";
|
|
import type { StarterMoveset } from "#types/save-data";
|
|
import type { EvolutionLevel, EvolutionLevelWithThreshold } from "#types/species-gen-types";
|
|
import { randSeedFloat, randSeedGauss } from "#utils/common";
|
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
|
import { toCamelCase, toPascalCase } from "#utils/strings";
|
|
import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities";
|
|
import i18next from "i18next";
|
|
|
|
export enum Region {
|
|
NORMAL,
|
|
ALOLA,
|
|
GALAR,
|
|
HISUI,
|
|
PALDEA,
|
|
}
|
|
|
|
// TODO: this is horrible and will need to be removed once a refactor/cleanup of forms is executed.
|
|
export const normalForm: SpeciesId[] = [
|
|
SpeciesId.PIKACHU,
|
|
SpeciesId.RAICHU,
|
|
SpeciesId.EEVEE,
|
|
SpeciesId.JOLTEON,
|
|
SpeciesId.FLAREON,
|
|
SpeciesId.VAPOREON,
|
|
SpeciesId.ESPEON,
|
|
SpeciesId.UMBREON,
|
|
SpeciesId.LEAFEON,
|
|
SpeciesId.GLACEON,
|
|
SpeciesId.SYLVEON,
|
|
SpeciesId.PICHU,
|
|
SpeciesId.ROTOM,
|
|
SpeciesId.DIALGA,
|
|
SpeciesId.PALKIA,
|
|
SpeciesId.KYUREM,
|
|
SpeciesId.GENESECT,
|
|
SpeciesId.FROAKIE,
|
|
SpeciesId.FROGADIER,
|
|
SpeciesId.GRENINJA,
|
|
SpeciesId.ROCKRUFF,
|
|
SpeciesId.NECROZMA,
|
|
SpeciesId.MAGEARNA,
|
|
SpeciesId.MARSHADOW,
|
|
SpeciesId.CRAMORANT,
|
|
SpeciesId.ZARUDE,
|
|
SpeciesId.CALYREX,
|
|
];
|
|
|
|
export type PokemonSpeciesFilter = (species: PokemonSpecies) => boolean;
|
|
|
|
export abstract class PokemonSpeciesForm {
|
|
public speciesId: SpeciesId;
|
|
protected _formIndex: number;
|
|
protected _generation: number;
|
|
readonly type1: PokemonType;
|
|
readonly type2: PokemonType | null;
|
|
readonly height: number;
|
|
readonly weight: number;
|
|
readonly ability1: AbilityId;
|
|
readonly ability2: AbilityId;
|
|
readonly abilityHidden: AbilityId;
|
|
readonly baseTotal: number;
|
|
readonly baseStats: number[];
|
|
readonly catchRate: number;
|
|
readonly baseFriendship: number;
|
|
readonly baseExp: number;
|
|
readonly genderDiffs: boolean;
|
|
readonly isStarterSelectable: boolean;
|
|
|
|
constructor(
|
|
type1: PokemonType,
|
|
type2: PokemonType | null,
|
|
height: number,
|
|
weight: number,
|
|
ability1: AbilityId,
|
|
ability2: AbilityId,
|
|
abilityHidden: AbilityId,
|
|
baseTotal: number,
|
|
baseHp: number,
|
|
baseAtk: number,
|
|
baseDef: number,
|
|
baseSpatk: number,
|
|
baseSpdef: number,
|
|
baseSpd: number,
|
|
catchRate: number,
|
|
baseFriendship: number,
|
|
baseExp: number,
|
|
genderDiffs: boolean,
|
|
isStarterSelectable: boolean,
|
|
) {
|
|
this.type1 = type1;
|
|
this.type2 = type2;
|
|
this.height = height;
|
|
this.weight = weight;
|
|
this.ability1 = ability1;
|
|
this.ability2 = ability2 === AbilityId.NONE ? ability1 : ability2;
|
|
this.abilityHidden = abilityHidden;
|
|
this.baseTotal = baseTotal;
|
|
this.baseStats = [baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd];
|
|
this.catchRate = catchRate;
|
|
this.baseFriendship = baseFriendship;
|
|
this.baseExp = baseExp;
|
|
this.genderDiffs = genderDiffs;
|
|
this.isStarterSelectable = isStarterSelectable;
|
|
}
|
|
|
|
/**
|
|
* Method to get the root species id of a Pokemon.
|
|
* Magmortar.getRootSpeciesId(true) => Magmar
|
|
* Magmortar.getRootSpeciesId(false) => Magby
|
|
* @param forStarter boolean to get the nonbaby form of a starter
|
|
* @returns The species
|
|
*/
|
|
getRootSpeciesId(forStarter = false): SpeciesId {
|
|
let ret = this.speciesId;
|
|
while (pokemonPrevolutions.hasOwnProperty(ret) && (!forStarter || !speciesStarterCosts.hasOwnProperty(ret))) {
|
|
ret = pokemonPrevolutions[ret];
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
get generation(): number {
|
|
return this._generation;
|
|
}
|
|
|
|
set generation(generation: number) {
|
|
this._generation = generation;
|
|
}
|
|
|
|
get formIndex(): number {
|
|
return this._formIndex;
|
|
}
|
|
|
|
set formIndex(formIndex: number) {
|
|
this._formIndex = formIndex;
|
|
}
|
|
|
|
isOfType(type: number): boolean {
|
|
return this.type1 === type || (this.type2 !== null && this.type2 === type);
|
|
}
|
|
|
|
/**
|
|
* Method to get the total number of abilities a Pokemon species has.
|
|
* @returns Number of abilities
|
|
*/
|
|
getAbilityCount(): number {
|
|
return this.abilityHidden !== AbilityId.NONE ? 3 : 2;
|
|
}
|
|
|
|
/**
|
|
* Method to get the ability of a Pokemon species.
|
|
* @param abilityIndex Which ability to get (should only be 0-2)
|
|
* @returns The id of the Ability
|
|
*/
|
|
getAbility(abilityIndex: number): AbilityId {
|
|
let ret: AbilityId;
|
|
if (abilityIndex === 0) {
|
|
ret = this.ability1;
|
|
} else if (abilityIndex === 1) {
|
|
ret = this.ability2;
|
|
} else {
|
|
ret = this.abilityHidden;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Method to get the passive ability of a Pokemon species
|
|
* @param formIndex The form index to use, defaults to form for this species instance
|
|
* @returns The id of the ability
|
|
*/
|
|
getPassiveAbility(formIndex?: number): AbilityId {
|
|
if (formIndex == null) {
|
|
formIndex = this.formIndex;
|
|
}
|
|
let starterSpeciesId = this.speciesId;
|
|
while (
|
|
!(starterSpeciesId in starterPassiveAbilities)
|
|
|| !(formIndex in starterPassiveAbilities[starterSpeciesId])
|
|
) {
|
|
if (pokemonPrevolutions.hasOwnProperty(starterSpeciesId)) {
|
|
starterSpeciesId = pokemonPrevolutions[starterSpeciesId];
|
|
} else {
|
|
// If we've reached the base species and still haven't found a matching ability, use form 0 if possible
|
|
if (0 in starterPassiveAbilities[starterSpeciesId]) {
|
|
return starterPassiveAbilities[starterSpeciesId][0];
|
|
}
|
|
console.log("No passive ability found for %s, using run away", this.speciesId);
|
|
return AbilityId.RUN_AWAY;
|
|
}
|
|
}
|
|
return starterPassiveAbilities[starterSpeciesId][formIndex];
|
|
}
|
|
|
|
getLevelMoves(): LevelMoves {
|
|
if (
|
|
pokemonSpeciesFormLevelMoves.hasOwnProperty(this.speciesId)
|
|
&& pokemonSpeciesFormLevelMoves[this.speciesId].hasOwnProperty(this.formIndex)
|
|
) {
|
|
return pokemonSpeciesFormLevelMoves[this.speciesId][this.formIndex].slice(0);
|
|
}
|
|
return pokemonSpeciesLevelMoves[this.speciesId].slice(0);
|
|
}
|
|
|
|
getRegion(): Region {
|
|
return Math.floor(this.speciesId / 2000) as Region;
|
|
}
|
|
|
|
isObtainable(): boolean {
|
|
return this.generation <= 9 || pokemonPrevolutions.hasOwnProperty(this.speciesId);
|
|
}
|
|
|
|
isCatchable(): boolean {
|
|
return this.isObtainable() && uncatchableSpecies.indexOf(this.speciesId) === -1;
|
|
}
|
|
|
|
isRegional(): boolean {
|
|
return this.getRegion() > Region.NORMAL;
|
|
}
|
|
|
|
isTrainerForbidden(): boolean {
|
|
return [SpeciesId.ETERNAL_FLOETTE, SpeciesId.BLOODMOON_URSALUNA].includes(this.speciesId);
|
|
}
|
|
|
|
isRareRegional(): boolean {
|
|
switch (this.getRegion()) {
|
|
case Region.HISUI:
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Gets the BST for the species
|
|
* @returns The species' BST.
|
|
*/
|
|
getBaseStatTotal(): number {
|
|
return this.baseStats.reduce((i, n) => n + i);
|
|
}
|
|
|
|
/**
|
|
* Gets the species' base stat amount for the given stat.
|
|
* @param stat The desired stat.
|
|
* @returns The species' base stat amount.
|
|
*/
|
|
getBaseStat(stat: Stat): number {
|
|
return this.baseStats[stat];
|
|
}
|
|
|
|
getBaseExp(): number {
|
|
let ret = this.baseExp;
|
|
switch (this.getFormSpriteKey()) {
|
|
case SpeciesFormKey.MEGA:
|
|
case SpeciesFormKey.MEGA_X:
|
|
case SpeciesFormKey.MEGA_Y:
|
|
case SpeciesFormKey.PRIMAL:
|
|
case SpeciesFormKey.GIGANTAMAX:
|
|
case SpeciesFormKey.ETERNAMAX:
|
|
ret *= 1.5;
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number, back?: boolean): string {
|
|
const spriteId = this.getSpriteId(female, formIndex, shiny, variant, back).replace(/_{2}/g, "/");
|
|
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
|
|
}
|
|
|
|
getBaseSpriteKey(female: boolean, formIndex?: number): string {
|
|
if (formIndex === undefined || this instanceof PokemonForm) {
|
|
formIndex = this.formIndex;
|
|
}
|
|
|
|
const formSpriteKey = this.getFormSpriteKey(formIndex);
|
|
const showGenderDiffs =
|
|
this.genderDiffs
|
|
&& female
|
|
&& ![SpeciesFormKey.MEGA, SpeciesFormKey.GIGANTAMAX].includes(formSpriteKey as SpeciesFormKey);
|
|
|
|
return `${showGenderDiffs ? "female__" : ""}${this.speciesId}${formSpriteKey ? `-${formSpriteKey}` : ""}`;
|
|
}
|
|
|
|
/** Compute the sprite ID of the pokemon form. */
|
|
getSpriteId(female: boolean, formIndex?: number, shiny?: boolean, variant = 0, back = false): string {
|
|
const baseSpriteKey = this.getBaseSpriteKey(female, formIndex);
|
|
|
|
let config = variantData;
|
|
`${back ? "back__" : ""}${baseSpriteKey}`.split("__").map(p => (config ? (config = config[p]) : null));
|
|
const variantSet = config as VariantSet;
|
|
|
|
return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`;
|
|
}
|
|
|
|
getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number, back?: boolean): string {
|
|
return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant, back)}`;
|
|
}
|
|
|
|
abstract getFormSpriteKey(formIndex?: number): string;
|
|
|
|
/**
|
|
* Variant Data key/index is either species id or species id followed by -formkey
|
|
* @param formIndex optional form index for pokemon with different forms
|
|
* @returns species id if no additional forms, index with formkey if a pokemon with a form
|
|
*/
|
|
getVariantDataIndex(formIndex?: number) {
|
|
let formkey: string | null = null;
|
|
let variantDataIndex: number | string = this.speciesId;
|
|
const species = getPokemonSpecies(this.speciesId);
|
|
if (species.forms.length > 0 && formIndex !== undefined) {
|
|
formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex);
|
|
if (formkey) {
|
|
variantDataIndex = `${this.speciesId}-${formkey}`;
|
|
}
|
|
}
|
|
return variantDataIndex;
|
|
}
|
|
|
|
getIconAtlasKey(formIndex?: number, shiny?: boolean, variant?: number): string {
|
|
const variantDataIndex = this.getVariantDataIndex(formIndex);
|
|
const isVariant =
|
|
shiny && variantData[variantDataIndex] && variant !== undefined && variantData[variantDataIndex][variant];
|
|
return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`;
|
|
}
|
|
|
|
getIconId(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
|
|
if (formIndex === undefined) {
|
|
formIndex = this.formIndex;
|
|
}
|
|
|
|
const variantDataIndex = this.getVariantDataIndex(formIndex);
|
|
|
|
let ret = this.speciesId.toString();
|
|
|
|
const isVariant =
|
|
shiny && variantData[variantDataIndex] && variant !== undefined && variantData[variantDataIndex][variant];
|
|
|
|
if (shiny && !isVariant) {
|
|
ret += "s";
|
|
}
|
|
|
|
switch (this.speciesId) {
|
|
case SpeciesId.DODUO:
|
|
case SpeciesId.DODRIO:
|
|
case SpeciesId.MEGANIUM:
|
|
case SpeciesId.TORCHIC:
|
|
case SpeciesId.COMBUSKEN:
|
|
case SpeciesId.BLAZIKEN:
|
|
case SpeciesId.HIPPOPOTAS:
|
|
case SpeciesId.HIPPOWDON:
|
|
case SpeciesId.UNFEZANT:
|
|
case SpeciesId.FRILLISH:
|
|
case SpeciesId.JELLICENT:
|
|
case SpeciesId.PYROAR:
|
|
ret += female ? "-f" : "";
|
|
break;
|
|
}
|
|
|
|
let formSpriteKey = this.getFormSpriteKey(formIndex);
|
|
if (formSpriteKey) {
|
|
switch (this.speciesId) {
|
|
case SpeciesId.DUDUNSPARCE:
|
|
break;
|
|
case SpeciesId.ZACIAN:
|
|
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Intentionally falls through
|
|
case SpeciesId.ZAMAZENTA:
|
|
if (formSpriteKey.startsWith("behemoth")) {
|
|
formSpriteKey = "crowned";
|
|
}
|
|
default:
|
|
ret += `-${formSpriteKey}`;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isVariant) {
|
|
ret += `_${variant + 1}`;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
getCryKey(formIndex?: number): string {
|
|
let speciesId = this.speciesId;
|
|
if (this.speciesId > 2000) {
|
|
switch (this.speciesId) {
|
|
case SpeciesId.GALAR_SLOWPOKE:
|
|
break;
|
|
case SpeciesId.ETERNAL_FLOETTE:
|
|
break;
|
|
case SpeciesId.BLOODMOON_URSALUNA:
|
|
break;
|
|
default:
|
|
speciesId %= 2000;
|
|
break;
|
|
}
|
|
}
|
|
let ret = speciesId.toString();
|
|
const forms = getPokemonSpecies(speciesId).forms;
|
|
if (forms.length > 0) {
|
|
if (formIndex !== undefined && formIndex >= forms.length) {
|
|
console.warn(
|
|
`Attempted accessing form with index ${formIndex} of species ${getPokemonSpecies(speciesId).getName()} with only ${forms.length || 0} forms`,
|
|
);
|
|
formIndex = Math.min(formIndex, forms.length - 1);
|
|
}
|
|
const formKey = forms[formIndex || 0].formKey;
|
|
switch (formKey) {
|
|
case SpeciesFormKey.MEGA:
|
|
case SpeciesFormKey.MEGA_X:
|
|
case SpeciesFormKey.MEGA_Y:
|
|
case SpeciesFormKey.GIGANTAMAX:
|
|
case SpeciesFormKey.GIGANTAMAX_SINGLE:
|
|
case SpeciesFormKey.GIGANTAMAX_RAPID:
|
|
case "white":
|
|
case "black":
|
|
case "therian":
|
|
case "sky":
|
|
case "gorging":
|
|
case "gulping":
|
|
case "no-ice":
|
|
case "hangry":
|
|
case "crowned":
|
|
case "eternamax":
|
|
case "four":
|
|
case "droopy":
|
|
case "stretchy":
|
|
case "hero":
|
|
case "roaming":
|
|
case "complete":
|
|
case "10-complete":
|
|
case "10":
|
|
case "10-pc":
|
|
case "super":
|
|
case "unbound":
|
|
case "pau":
|
|
case "pompom":
|
|
case "sensu":
|
|
case "dusk":
|
|
case "midnight":
|
|
case "school":
|
|
case "dawn-wings":
|
|
case "dusk-mane":
|
|
case "ultra":
|
|
ret += `-${formKey}`;
|
|
break;
|
|
}
|
|
}
|
|
return `cry/${ret}`;
|
|
}
|
|
|
|
validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean {
|
|
const rootSpeciesId = this.getRootSpeciesId();
|
|
for (const moveId of moveset) {
|
|
if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) {
|
|
const eggMoveIndex = speciesEggMoves[rootSpeciesId].indexOf(moveId);
|
|
if (eggMoveIndex > -1 && eggMoves & (1 << eggMoveIndex)) {
|
|
continue;
|
|
}
|
|
}
|
|
if (
|
|
pokemonFormLevelMoves.hasOwnProperty(this.speciesId)
|
|
&& pokemonFormLevelMoves[this.speciesId].hasOwnProperty(this.formIndex)
|
|
) {
|
|
if (!pokemonFormLevelMoves[this.speciesId][this.formIndex].find(lm => lm[0] <= 5 && lm[1] === moveId)) {
|
|
return false;
|
|
}
|
|
} else if (!pokemonSpeciesLevelMoves[this.speciesId].find(lm => lm[0] <= 5 && lm[1] === moveId)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Load the variant colors for the species into the variant color cache
|
|
*
|
|
* @param spriteKey - The sprite key to use
|
|
* @param female - Whether to load female instead of male
|
|
* @param back - Whether the back sprite is being loaded
|
|
*
|
|
*/
|
|
async loadVariantColors(
|
|
spriteKey: string,
|
|
female: boolean,
|
|
variant: Variant,
|
|
back = false,
|
|
formIndex?: number,
|
|
): Promise<void> {
|
|
let baseSpriteKey = this.getBaseSpriteKey(female, formIndex);
|
|
if (back) {
|
|
baseSpriteKey = "back__" + baseSpriteKey;
|
|
}
|
|
|
|
if (variantColorCache.hasOwnProperty(baseSpriteKey)) {
|
|
// Variant colors have already been loaded
|
|
return;
|
|
}
|
|
|
|
const variantInfo = variantData[this.getVariantDataIndex(formIndex)];
|
|
// Do nothing if there is no variant information or the variant does not have color replacements
|
|
if (!variantInfo || variantInfo[variant] !== 1) {
|
|
return;
|
|
}
|
|
|
|
await populateVariantColorCache(
|
|
"pkmn__" + baseSpriteKey,
|
|
globalScene.experimentalSprites && hasExpSprite(spriteKey),
|
|
baseSpriteKey.replace("__", "/"),
|
|
);
|
|
}
|
|
|
|
async loadAssets(
|
|
female: boolean,
|
|
formIndex?: number,
|
|
shiny = false,
|
|
variant?: Variant,
|
|
startLoad = false,
|
|
back = false,
|
|
): Promise<void> {
|
|
// We need to populate the color cache for this species' variant
|
|
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant, back);
|
|
globalScene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant, back));
|
|
globalScene.load.audio(this.getCryKey(formIndex), `audio/${this.getCryKey(formIndex)}.m4a`);
|
|
if (variant != null) {
|
|
await this.loadVariantColors(spriteKey, female, variant, back, formIndex);
|
|
}
|
|
return new Promise<void>(resolve => {
|
|
globalScene.load.once(Phaser.Loader.Events.COMPLETE, () => {
|
|
const originalWarn = console.warn;
|
|
// Ignore warnings for missing frames, because there will be a lot
|
|
console.warn = () => {};
|
|
const frameNames = globalScene.anims.generateFrameNames(spriteKey, {
|
|
zeroPad: 4,
|
|
suffix: ".png",
|
|
start: 1,
|
|
end: 400,
|
|
});
|
|
console.warn = originalWarn;
|
|
if (!globalScene.anims.exists(spriteKey)) {
|
|
globalScene.anims.create({
|
|
key: this.getSpriteKey(female, formIndex, shiny, variant, back),
|
|
frames: frameNames,
|
|
frameRate: 10,
|
|
repeat: -1,
|
|
});
|
|
} else {
|
|
globalScene.anims.get(spriteKey).frameRate = 10;
|
|
}
|
|
const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant, back)
|
|
.replace("variant/", "")
|
|
.replace(/_[1-3]$/, "");
|
|
if (variant != null) {
|
|
loadPokemonVariantAssets(spriteKey, spritePath, variant).then(() => resolve());
|
|
}
|
|
});
|
|
if (startLoad) {
|
|
if (!globalScene.load.isLoading()) {
|
|
globalScene.load.start();
|
|
}
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, ignorePlay?: boolean): AnySound | null {
|
|
const cryKey = this.getCryKey(this.formIndex);
|
|
let cry: AnySound | null = globalScene.sound.get(cryKey) as AnySound;
|
|
if (cry?.pendingRemove) {
|
|
cry = null;
|
|
}
|
|
cry = globalScene.playSound(cry ?? cryKey, soundConfig);
|
|
if (cry && ignorePlay) {
|
|
cry.stop();
|
|
}
|
|
return cry;
|
|
}
|
|
|
|
generateCandyColors(): number[][] {
|
|
const sourceTexture = globalScene.textures.get(this.getSpriteKey(false));
|
|
|
|
const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame];
|
|
const sourceImage = sourceTexture.getSourceImage() as HTMLImageElement;
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
const spriteColors: number[][] = [];
|
|
|
|
const context = canvas.getContext("2d");
|
|
const frame = sourceFrame;
|
|
canvas.width = frame.width;
|
|
canvas.height = frame.height;
|
|
context?.drawImage(sourceImage, frame.cutX, frame.cutY, frame.width, frame.height, 0, 0, frame.width, frame.height);
|
|
const imageData = context?.getImageData(frame.cutX, frame.cutY, frame.width, frame.height);
|
|
const pixelData = imageData?.data;
|
|
const pixelColors: number[] = [];
|
|
|
|
if (pixelData?.length !== undefined) {
|
|
for (let i = 0; i < pixelData.length; i += 4) {
|
|
if (pixelData[i + 3]) {
|
|
const pixel = pixelData.slice(i, i + 4);
|
|
const [r, g, b, a] = pixel;
|
|
if (!spriteColors.find(c => c[0] === r && c[1] === g && c[2] === b)) {
|
|
spriteColors.push([r, g, b, a]);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < pixelData.length; i += 4) {
|
|
const total = pixelData.slice(i, i + 3).reduce((total: number, value: number) => total + value, 0);
|
|
if (!total) {
|
|
continue;
|
|
}
|
|
pixelColors.push(
|
|
argbFromRgba({
|
|
r: pixelData[i],
|
|
g: pixelData[i + 1],
|
|
b: pixelData[i + 2],
|
|
a: pixelData[i + 3],
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
let paletteColors: Map<number, number> = new Map();
|
|
|
|
const originalRandom = Math.random;
|
|
Math.random = randSeedFloat;
|
|
|
|
globalScene.executeWithSeedOffset(
|
|
() => {
|
|
paletteColors = QuantizerCelebi.quantize(pixelColors, 2);
|
|
},
|
|
0,
|
|
"This result should not vary",
|
|
);
|
|
|
|
Math.random = originalRandom;
|
|
|
|
return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as number[]);
|
|
}
|
|
}
|
|
|
|
export class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
|
|
public name: string;
|
|
readonly subLegendary: boolean;
|
|
readonly legendary: boolean;
|
|
readonly mythical: boolean;
|
|
public category: string;
|
|
readonly growthRate: GrowthRate;
|
|
/** The chance (as a decimal) for this Species to be male, or `null` for genderless species */
|
|
readonly malePercent: number | null;
|
|
readonly genderDiffs: boolean;
|
|
readonly canChangeForm: boolean;
|
|
readonly forms: PokemonForm[];
|
|
|
|
constructor(
|
|
id: SpeciesId,
|
|
generation: number,
|
|
subLegendary: boolean,
|
|
legendary: boolean,
|
|
mythical: boolean,
|
|
category: string,
|
|
type1: PokemonType,
|
|
type2: PokemonType | null,
|
|
height: number,
|
|
weight: number,
|
|
ability1: AbilityId,
|
|
ability2: AbilityId,
|
|
abilityHidden: AbilityId,
|
|
baseTotal: number,
|
|
baseHp: number,
|
|
baseAtk: number,
|
|
baseDef: number,
|
|
baseSpatk: number,
|
|
baseSpdef: number,
|
|
baseSpd: number,
|
|
catchRate: number,
|
|
baseFriendship: number,
|
|
baseExp: number,
|
|
growthRate: GrowthRate,
|
|
malePercent: number | null,
|
|
genderDiffs: boolean,
|
|
canChangeForm?: boolean,
|
|
...forms: PokemonForm[]
|
|
) {
|
|
super(
|
|
type1,
|
|
type2,
|
|
height,
|
|
weight,
|
|
ability1,
|
|
ability2,
|
|
abilityHidden,
|
|
baseTotal,
|
|
baseHp,
|
|
baseAtk,
|
|
baseDef,
|
|
baseSpatk,
|
|
baseSpdef,
|
|
baseSpd,
|
|
catchRate,
|
|
baseFriendship,
|
|
baseExp,
|
|
genderDiffs,
|
|
false,
|
|
);
|
|
this.speciesId = id;
|
|
this.formIndex = 0;
|
|
this.generation = generation;
|
|
this.subLegendary = subLegendary;
|
|
this.legendary = legendary;
|
|
this.mythical = mythical;
|
|
this.category = category;
|
|
this.growthRate = growthRate;
|
|
this.malePercent = malePercent;
|
|
this.genderDiffs = genderDiffs;
|
|
this.canChangeForm = !!canChangeForm;
|
|
this.forms = forms;
|
|
|
|
this.localize();
|
|
|
|
forms.forEach((form, f) => {
|
|
form.speciesId = id;
|
|
form.formIndex = f;
|
|
form.generation = generation;
|
|
});
|
|
}
|
|
|
|
getName(formIndex?: number): string {
|
|
if (formIndex !== undefined && this.forms.length > 0) {
|
|
const form = this.forms[formIndex];
|
|
let key: string | null;
|
|
switch (form.formKey) {
|
|
case SpeciesFormKey.MEGA:
|
|
case SpeciesFormKey.PRIMAL:
|
|
case SpeciesFormKey.ETERNAMAX:
|
|
case SpeciesFormKey.MEGA_X:
|
|
case SpeciesFormKey.MEGA_Y:
|
|
key = form.formKey;
|
|
break;
|
|
default:
|
|
if (form.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1) {
|
|
key = "gigantamax";
|
|
} else {
|
|
key = null;
|
|
}
|
|
}
|
|
|
|
if (key) {
|
|
return i18next.t(`battlePokemonForm:${toCamelCase(key)}`, {
|
|
pokemonName: this.name,
|
|
});
|
|
}
|
|
}
|
|
return this.name;
|
|
}
|
|
|
|
/**
|
|
* Pick and return a random {@linkcode Gender} for a {@linkcode Pokemon}.
|
|
* @returns A randomly rolled gender based on this Species' {@linkcode malePercent}.
|
|
*/
|
|
generateGender(): Gender {
|
|
if (this.malePercent == null) {
|
|
return Gender.GENDERLESS;
|
|
}
|
|
|
|
if (randSeedFloat() * 100 <= this.malePercent) {
|
|
return Gender.MALE;
|
|
}
|
|
return Gender.FEMALE;
|
|
}
|
|
|
|
/**
|
|
* Find the name of species with proper attachments for regionals and separate starter forms (Floette, Ursaluna)
|
|
* @returns a string with the region name or other form name attached
|
|
*/
|
|
getExpandedSpeciesName(): string {
|
|
if (this.speciesId < 2000) {
|
|
return this.name; // Other special cases could be put here too
|
|
}
|
|
// Everything beyond this point essentially follows the pattern of FORMNAME_SPECIES
|
|
return i18next.t(`pokemonForm:appendForm.${toCamelCase(SpeciesId[this.speciesId].split("_")[0])}`, {
|
|
pokemonName: this.name,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Find the form name for species with just one form (regional variants, Floette, Ursaluna)
|
|
* @param formIndex The form index to check (defaults to 0)
|
|
* @param append Whether to append the species name to the end (defaults to false)
|
|
* @returns the pokemon-form locale key for the single form name ("Alolan Form", "Eternal Flower" etc)
|
|
*/
|
|
getFormNameToDisplay(formIndex = 0, append = false): string {
|
|
const formKey = this.forms[formIndex]?.formKey ?? "";
|
|
const formText = toPascalCase(formKey);
|
|
const speciesName = toCamelCase(SpeciesId[this.speciesId]);
|
|
let ret = "";
|
|
|
|
const region = this.getRegion();
|
|
if (this.speciesId === SpeciesId.ARCEUS) {
|
|
ret = i18next.t(`pokemonInfo:type.${toCamelCase(formText)}`);
|
|
} else if (
|
|
[
|
|
SpeciesFormKey.MEGA,
|
|
SpeciesFormKey.MEGA_X,
|
|
SpeciesFormKey.MEGA_Y,
|
|
SpeciesFormKey.PRIMAL,
|
|
SpeciesFormKey.GIGANTAMAX,
|
|
SpeciesFormKey.GIGANTAMAX_RAPID,
|
|
SpeciesFormKey.GIGANTAMAX_SINGLE,
|
|
SpeciesFormKey.ETERNAMAX,
|
|
].includes(formKey as SpeciesFormKey)
|
|
) {
|
|
return append
|
|
? i18next.t(`battlePokemonForm:${toCamelCase(formKey)}`, { pokemonName: this.name })
|
|
: i18next.t(`pokemonForm:battleForm.${toCamelCase(formKey)}`);
|
|
} else if (
|
|
region === Region.NORMAL
|
|
|| (this.speciesId === SpeciesId.GALAR_DARMANITAN && formIndex > 0)
|
|
|| this.speciesId === SpeciesId.PALDEA_TAUROS
|
|
) {
|
|
// More special cases can be added here
|
|
const i18key = `pokemonForm:${speciesName}${formText}`;
|
|
if (i18next.exists(i18key)) {
|
|
ret = i18next.t(i18key);
|
|
} else {
|
|
const rootSpeciesName = toCamelCase(SpeciesId[this.getRootSpeciesId()]);
|
|
const i18RootKey = `pokemonForm:${rootSpeciesName}${formText}`;
|
|
ret = i18next.exists(i18RootKey) ? i18next.t(i18RootKey) : formText;
|
|
}
|
|
} else if (append) {
|
|
// Everything beyond this has an expanded name
|
|
return this.getExpandedSpeciesName();
|
|
} else if (this.speciesId === SpeciesId.ETERNAL_FLOETTE) {
|
|
// Not a real form, so the key is made up
|
|
return i18next.t("pokemonForm:floetteEternalFlower");
|
|
} else if (this.speciesId === SpeciesId.BLOODMOON_URSALUNA) {
|
|
// Not a real form, so the key is made up
|
|
return i18next.t("pokemonForm:ursalunaBloodmoon");
|
|
} else {
|
|
// Only regional forms should be left at this point
|
|
return i18next.t(`pokemonForm:regionalForm.${toCamelCase(Region[region])}`);
|
|
}
|
|
return append
|
|
? i18next.t("pokemonForm:appendForm.generic", {
|
|
pokemonName: this.name,
|
|
formName: ret,
|
|
})
|
|
: ret;
|
|
}
|
|
|
|
localize(): void {
|
|
this.name = i18next.t(`pokemon:${toCamelCase(SpeciesId[this.speciesId])}`);
|
|
this.category = i18next.t(`pokemonCategory:${toCamelCase(SpeciesId[this.speciesId])}Category`);
|
|
}
|
|
|
|
getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): SpeciesId {
|
|
return this.getSpeciesForLevel(
|
|
level,
|
|
allowEvolving,
|
|
false,
|
|
(isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0),
|
|
isBoss ? EvoLevelThresholdKind.NORMAL : EvoLevelThresholdKind.WILD,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Determine which species of Pokémon to use for a given level in a trainer battle.
|
|
*
|
|
* @see {@linkcode getSpeciesForLevel}
|
|
*/
|
|
getTrainerSpeciesForLevel(
|
|
level: number,
|
|
allowEvolving = false,
|
|
strength: PartyMemberStrength = PartyMemberStrength.WEAKER,
|
|
encounterKind: EvoLevelThresholdKind = EvoLevelThresholdKind.NORMAL,
|
|
): SpeciesId {
|
|
return this.getSpeciesForLevel(level, allowEvolving, true, strength, encounterKind);
|
|
}
|
|
|
|
/**
|
|
* Determine which species of Pokémon to use for a given level
|
|
* @see {@linkcode determineEnemySpecies}
|
|
*/
|
|
getSpeciesForLevel(
|
|
level: number,
|
|
allowEvolving = false,
|
|
forTrainer = false,
|
|
strength: PartyMemberStrength = PartyMemberStrength.WEAKER,
|
|
encounterKind: EvoLevelThresholdKind = EvoLevelThresholdKind.NORMAL,
|
|
): SpeciesId {
|
|
return determineEnemySpecies(this, level, allowEvolving, forTrainer, strength, encounterKind);
|
|
}
|
|
|
|
getEvolutionLevels(): EvolutionLevel[] {
|
|
const evolutionLevels: EvolutionLevel[] = [];
|
|
|
|
//console.log(Species[this.speciesId], pokemonEvolutions[this.speciesId])
|
|
|
|
if (pokemonEvolutions.hasOwnProperty(this.speciesId)) {
|
|
for (const e of pokemonEvolutions[this.speciesId]) {
|
|
const speciesId = e.speciesId;
|
|
const level = e.level;
|
|
evolutionLevels.push([speciesId, level]);
|
|
//console.log(Species[speciesId], getPokemonSpecies(speciesId), getPokemonSpecies(speciesId).getEvolutionLevels());
|
|
const nextEvolutionLevels = getPokemonSpecies(speciesId).getEvolutionLevels();
|
|
for (const npl of nextEvolutionLevels) {
|
|
evolutionLevels.push(npl);
|
|
}
|
|
}
|
|
}
|
|
|
|
return evolutionLevels;
|
|
}
|
|
|
|
/**
|
|
* Get all prevolution levels for this species
|
|
*
|
|
* @remarks
|
|
* `withThresholds` is used to return the evolution level thresholds for the species, to be used
|
|
* when generating
|
|
*
|
|
* @param withThresholds - Whether to include evolution level thresholds in the returned data; default `false`
|
|
*/
|
|
getPrevolutionLevels(withThresholds: true): EvolutionLevelWithThreshold[];
|
|
getPrevolutionLevels(withThresholds: false): EvolutionLevel[];
|
|
getPrevolutionLevels(
|
|
withThresholds?: boolean,
|
|
): typeof withThresholds extends false ? EvolutionLevel[] : EvolutionLevelWithThreshold[];
|
|
getPrevolutionLevels(withThresholds = false): EvolutionLevelWithThreshold[] | EvolutionLevel[] {
|
|
const prevolutionLevels: (EvolutionLevel | EvolutionLevelWithThreshold)[] = [];
|
|
|
|
const allEvolvingPokemon = Object.keys(pokemonEvolutions);
|
|
for (const p of allEvolvingPokemon) {
|
|
const speciesId = Number.parseInt(p) as SpeciesId;
|
|
for (const e of pokemonEvolutions[p]) {
|
|
if (
|
|
e.speciesId === this.speciesId
|
|
&& (this.forms.length === 0 || !e.evoFormKey || e.evoFormKey === this.forms[this.formIndex].formKey)
|
|
&& prevolutionLevels.every(pe => pe[0] !== speciesId)
|
|
) {
|
|
const level = e.level;
|
|
if (withThresholds && e.evoLevelThreshold) {
|
|
prevolutionLevels.push([speciesId, level, e.evoLevelThreshold]);
|
|
} else {
|
|
prevolutionLevels.push([speciesId, level]);
|
|
}
|
|
const subPrevolutionLevels = getPokemonSpecies(speciesId).getPrevolutionLevels(withThresholds);
|
|
for (const spl of subPrevolutionLevels) {
|
|
prevolutionLevels.push(spl);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return prevolutionLevels;
|
|
}
|
|
|
|
// This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon
|
|
getSimulatedEvolutionChain(
|
|
currentLevel: number,
|
|
forTrainer = false,
|
|
isBoss = false,
|
|
player = false,
|
|
): EvolutionLevel[] {
|
|
const ret: EvolutionLevel[] = [];
|
|
if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) {
|
|
const prevolutionLevels = this.getPrevolutionLevels().reverse();
|
|
const levelDiff = player ? 0 : forTrainer || isBoss ? (forTrainer && isBoss ? 2.5 : 5) : 10;
|
|
ret.push([prevolutionLevels[0][0], 1]);
|
|
for (let l = 1; l < prevolutionLevels.length; l++) {
|
|
const evolution = pokemonEvolutions[prevolutionLevels[l - 1][0]].find(
|
|
e => e.speciesId === prevolutionLevels[l][0],
|
|
);
|
|
ret.push([
|
|
prevolutionLevels[l][0],
|
|
Math.min(
|
|
Math.max(
|
|
evolution?.level!
|
|
+ Math.round(
|
|
randSeedGauss(0.5, 1 + levelDiff * 0.2)
|
|
* Math.max(evolution?.evoLevelThreshold?.[EvoLevelThresholdKind.WILD] ?? 0, 0.5)
|
|
* 5,
|
|
)
|
|
- 1,
|
|
2,
|
|
evolution?.level!,
|
|
),
|
|
currentLevel - 1,
|
|
),
|
|
]); // TODO: are those bangs correct?
|
|
}
|
|
const lastPrevolutionLevel = ret[prevolutionLevels.length - 1][1];
|
|
const evolution = pokemonEvolutions[prevolutionLevels.at(-1)![0]].find(e => e.speciesId === this.speciesId);
|
|
ret.push([
|
|
this.speciesId,
|
|
Math.min(
|
|
Math.max(
|
|
lastPrevolutionLevel
|
|
+ Math.round(
|
|
randSeedGauss(0.5, 1 + levelDiff * 0.2)
|
|
* Math.max(evolution?.evoLevelThreshold?.[EvoLevelThresholdKind.WILD] ?? 0, 0.5)
|
|
* 5,
|
|
),
|
|
lastPrevolutionLevel + 1,
|
|
evolution?.level!,
|
|
),
|
|
currentLevel,
|
|
),
|
|
]); // TODO: are those bangs correct?
|
|
} else {
|
|
ret.push([this.speciesId, 1]);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
getCompatibleFusionSpeciesFilter(): PokemonSpeciesFilter {
|
|
const hasEvolution = pokemonEvolutions.hasOwnProperty(this.speciesId);
|
|
const hasPrevolution = pokemonPrevolutions.hasOwnProperty(this.speciesId);
|
|
const subLegendary = this.subLegendary;
|
|
const legendary = this.legendary;
|
|
const mythical = this.mythical;
|
|
return species => {
|
|
return (
|
|
(subLegendary
|
|
|| legendary
|
|
|| mythical
|
|
|| (pokemonEvolutions.hasOwnProperty(species.speciesId) === hasEvolution
|
|
&& pokemonPrevolutions.hasOwnProperty(species.speciesId) === hasPrevolution))
|
|
&& species.subLegendary === subLegendary
|
|
&& species.legendary === legendary
|
|
&& species.mythical === mythical
|
|
&& (this.isTrainerForbidden() || !species.isTrainerForbidden())
|
|
&& species.speciesId !== SpeciesId.DITTO
|
|
);
|
|
};
|
|
}
|
|
|
|
hasVariants() {
|
|
let variantDataIndex: string | number = this.speciesId;
|
|
if (this.forms.length > 0) {
|
|
const formKey = this.forms[this.formIndex]?.formKey;
|
|
if (formKey) {
|
|
variantDataIndex = `${variantDataIndex}-${formKey}`;
|
|
}
|
|
}
|
|
return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId);
|
|
}
|
|
|
|
getFormSpriteKey(formIndex?: number) {
|
|
if (this.forms.length > 0 && formIndex !== undefined && formIndex >= this.forms.length) {
|
|
console.warn(
|
|
`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`,
|
|
);
|
|
formIndex = Math.min(formIndex, this.forms.length - 1);
|
|
}
|
|
return this.forms?.length > 0 ? this.forms[formIndex || 0].getFormSpriteKey() : "";
|
|
}
|
|
|
|
/**
|
|
* Generates a {@linkcode BigInt} corresponding to the maximum unlocks possible for this species,
|
|
* taking into account if the species has a male/female gender, and which variants are implemented.
|
|
* @returns The maximum unlocks for the species as a `BigInt`; can be compared with {@linkcode DexEntry.caughtAttr}.
|
|
*/
|
|
getFullUnlocksData(): bigint {
|
|
let caughtAttr = 0n;
|
|
caughtAttr += DexAttr.NON_SHINY;
|
|
caughtAttr += DexAttr.SHINY;
|
|
if (this.malePercent !== null) {
|
|
if (this.malePercent > 0) {
|
|
caughtAttr += DexAttr.MALE;
|
|
}
|
|
if (this.malePercent < 100) {
|
|
caughtAttr += DexAttr.FEMALE;
|
|
}
|
|
}
|
|
caughtAttr += DexAttr.DEFAULT_VARIANT;
|
|
if (this.hasVariants()) {
|
|
caughtAttr += DexAttr.VARIANT_2;
|
|
caughtAttr += DexAttr.VARIANT_3;
|
|
}
|
|
|
|
// Summing successive bigints for each obtainable form
|
|
caughtAttr +=
|
|
this?.forms?.length > 1
|
|
? this.forms
|
|
.map((f, index) => (f.isUnobtainable ? 0n : 128n * 2n ** BigInt(index)))
|
|
.reduce((acc, val) => acc + val, 0n)
|
|
: DexAttr.DEFAULT_FORM;
|
|
|
|
return caughtAttr;
|
|
}
|
|
}
|
|
|
|
export class PokemonForm extends PokemonSpeciesForm {
|
|
public formName: string;
|
|
public formKey: string;
|
|
public formSpriteKey: string | null;
|
|
public isUnobtainable: boolean;
|
|
|
|
// This is a collection of form keys that have in-run form changes, but should still be separately selectable from the start screen
|
|
private starterSelectableKeys: string[] = [
|
|
"10",
|
|
"50",
|
|
"10-pc",
|
|
"50-pc",
|
|
"red",
|
|
"orange",
|
|
"yellow",
|
|
"green",
|
|
"blue",
|
|
"indigo",
|
|
"violet",
|
|
];
|
|
|
|
constructor(
|
|
formName: string,
|
|
formKey: string,
|
|
type1: PokemonType,
|
|
type2: PokemonType | null,
|
|
height: number,
|
|
weight: number,
|
|
ability1: AbilityId,
|
|
ability2: AbilityId,
|
|
abilityHidden: AbilityId,
|
|
baseTotal: number,
|
|
baseHp: number,
|
|
baseAtk: number,
|
|
baseDef: number,
|
|
baseSpatk: number,
|
|
baseSpdef: number,
|
|
baseSpd: number,
|
|
catchRate: number,
|
|
baseFriendship: number,
|
|
baseExp: number,
|
|
genderDiffs = false,
|
|
formSpriteKey: string | null = null,
|
|
isStarterSelectable = false,
|
|
isUnobtainable = false,
|
|
) {
|
|
super(
|
|
type1,
|
|
type2,
|
|
height,
|
|
weight,
|
|
ability1,
|
|
ability2,
|
|
abilityHidden,
|
|
baseTotal,
|
|
baseHp,
|
|
baseAtk,
|
|
baseDef,
|
|
baseSpatk,
|
|
baseSpdef,
|
|
baseSpd,
|
|
catchRate,
|
|
baseFriendship,
|
|
baseExp,
|
|
genderDiffs,
|
|
isStarterSelectable || !formKey,
|
|
);
|
|
this.formName = formName;
|
|
this.formKey = formKey;
|
|
this.formSpriteKey = formSpriteKey;
|
|
this.isUnobtainable = isUnobtainable;
|
|
}
|
|
|
|
getFormSpriteKey(_formIndex?: number) {
|
|
return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey;
|
|
}
|
|
}
|