pokerogue/src/ui/pokedex-page-ui-handler.ts
Sirz Benjie ff44cbfa97
[Refactor] Refactor ability file part 1 (#5589)
* Move ability.ts to subfolder

* Extract types out of ability.ts

* Update imports in ability.ts and friends

* Cleanup imports in ability.ts

* Re-add imports lost during sort

* Update imports forgotten during rebase

* Re-import proper type from enums

* Update biome.jsonc

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Add commit to force tests to rerun

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-04-15 14:08:35 +00:00

2832 lines
108 KiB
TypeScript

import type { SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions";
import { pokemonEvolutions, pokemonPrevolutions, pokemonStarters } from "#app/data/balance/pokemon-evolutions";
import type { Variant } from "#app/sprites/variant";
import { getVariantTint, getVariantIcon } from "#app/sprites/variant";
import { argbFromRgba } from "@material/material-color-utilities";
import i18next from "i18next";
import { starterColors } from "#app/battle-scene";
import { allAbilities } from "#app/data/data-lists";
import { speciesEggMoves } from "#app/data/balance/egg-moves";
import { GrowthRate, getGrowthRateColor } from "#app/data/exp";
import { Gender, getGenderColor, getGenderSymbol } from "#app/data/gender";
import { allMoves } from "#app/data/moves/move";
import { getNatureName } from "#app/data/nature";
import type { SpeciesFormChange } from "#app/data/pokemon-forms";
import { pokemonFormChanges } from "#app/data/pokemon-forms";
import type { LevelMoves } from "#app/data/balance/pokemon-level-moves";
import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves";
import type PokemonSpecies from "#app/data/pokemon-species";
import { allSpecies, getPokemonSpecies, getPokemonSpeciesForm, normalForm } from "#app/data/pokemon-species";
import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters";
import { starterPassiveAbilities } from "#app/data/balance/passives";
import { PokemonType } from "#enums/pokemon-type";
import type { DexEntry, StarterAttributes } from "#app/system/game-data";
import { AbilityAttr, DexAttr } from "#app/system/game-data";
import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import MessageUiHandler from "#app/ui/message-ui-handler";
import { StatsContainer } from "#app/ui/stats-container";
import { TextStyle, addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions } from "#app/ui/text";
import { Mode } from "#app/ui/ui";
import { addWindow } from "#app/ui/ui-theme";
import { Egg } from "#app/data/egg";
import Overrides from "#app/overrides";
import { SettingKeyboard } from "#app/system/settings/settings-keyboard";
import { Passive as PassiveAttr } from "#enums/passive";
import MoveInfoOverlay from "#app/ui/move-info-overlay";
import PokedexInfoOverlay from "#app/ui/pokedex-info-overlay";
import { getEggTierForSpecies } from "#app/data/egg";
import { Device } from "#enums/devices";
import type { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Button } from "#enums/buttons";
import { EggSourceType } from "#enums/egg-source-types";
import {
getPassiveCandyCount,
getValueReductionCandyCounts,
getSameSpeciesEggCandyCounts,
} from "#app/data/balance/starters";
import {
BooleanHolder,
getLocalizedSpriteKey,
isNullOrUndefined,
padInt,
rgbHexToRgba,
toReadableString,
} from "#app/utils";
import type { Nature } from "#enums/nature";
import { getEnumKeys } from "#app/utils";
import { speciesTmMoves } from "#app/data/balance/tms";
import type { BiomeTierTod } from "#app/data/balance/biomes";
import { BiomePoolTier, catchableSpecies } from "#app/data/balance/biomes";
import { Biome } from "#app/enums/biome";
import { TimeOfDay } from "#app/enums/time-of-day";
import type { Abilities } from "#app/enums/abilities";
import { BaseStatsOverlay } from "#app/ui/base-stats-overlay";
import { globalScene } from "#app/global-scene";
import type BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText";
interface LanguageSetting {
starterInfoTextSize: string;
instructionTextSize: string;
starterInfoXPos?: number;
starterInfoYOffset?: number;
}
const languageSettings: { [key: string]: LanguageSetting } = {
en: {
starterInfoTextSize: "56px",
instructionTextSize: "38px",
},
de: {
starterInfoTextSize: "48px",
instructionTextSize: "35px",
starterInfoXPos: 33,
},
"es-ES": {
starterInfoTextSize: "56px",
instructionTextSize: "35px",
},
fr: {
starterInfoTextSize: "54px",
instructionTextSize: "38px",
},
it: {
starterInfoTextSize: "56px",
instructionTextSize: "38px",
},
pt_BR: {
starterInfoTextSize: "47px",
instructionTextSize: "38px",
starterInfoXPos: 33,
},
zh: {
starterInfoTextSize: "47px",
instructionTextSize: "38px",
starterInfoYOffset: 1,
starterInfoXPos: 24,
},
pt: {
starterInfoTextSize: "48px",
instructionTextSize: "42px",
starterInfoXPos: 33,
},
ko: {
starterInfoTextSize: "52px",
instructionTextSize: "38px",
},
ja: {
starterInfoTextSize: "51px",
instructionTextSize: "38px",
},
"ca-ES": {
starterInfoTextSize: "56px",
instructionTextSize: "38px",
},
};
const valueReductionMax = 2;
// Position of UI elements
const speciesContainerX = 109;
interface SpeciesDetails {
shiny?: boolean;
formIndex?: number;
female?: boolean;
variant?: number;
}
enum MenuOptions {
BASE_STATS,
ABILITIES,
LEVEL_MOVES,
EGG_MOVES,
TM_MOVES,
BIOMES,
NATURES,
TOGGLE_IVS,
EVOLUTIONS,
}
export default class PokedexPageUiHandler extends MessageUiHandler {
private starterSelectContainer: Phaser.GameObjects.Container;
private shinyOverlay: Phaser.GameObjects.Image;
private pokemonNumberText: Phaser.GameObjects.Text;
private pokemonSprite: Phaser.GameObjects.Sprite;
private pokemonNameText: Phaser.GameObjects.Text;
private pokemonGrowthRateLabelText: Phaser.GameObjects.Text;
private pokemonGrowthRateText: Phaser.GameObjects.Text;
private type1Icon: Phaser.GameObjects.Sprite;
private type2Icon: Phaser.GameObjects.Sprite;
private pokemonLuckLabelText: Phaser.GameObjects.Text;
private pokemonLuckText: Phaser.GameObjects.Text;
private pokemonGenderText: Phaser.GameObjects.Text;
private pokemonUncaughtText: Phaser.GameObjects.Text;
private pokemonCandyContainer: Phaser.GameObjects.Container;
private pokemonCandyIcon: Phaser.GameObjects.Sprite;
private pokemonCandyDarknessOverlay: Phaser.GameObjects.Sprite;
private pokemonCandyOverlayIcon: Phaser.GameObjects.Sprite;
private pokemonCandyCountText: Phaser.GameObjects.Text;
private pokemonCaughtHatchedContainer: Phaser.GameObjects.Container;
private pokemonCaughtCountText: Phaser.GameObjects.Text;
private pokemonFormText: Phaser.GameObjects.Text;
private pokemonHatchedIcon: Phaser.GameObjects.Sprite;
private pokemonHatchedCountText: Phaser.GameObjects.Text;
private pokemonShinyIcons: Phaser.GameObjects.Sprite[];
private activeTooltip: "ABILITY" | "PASSIVE" | "CANDY" | undefined;
private instructionsContainer: Phaser.GameObjects.Container;
private filterInstructionsContainer: Phaser.GameObjects.Container;
private shinyIconElement: Phaser.GameObjects.Sprite;
private formIconElement: Phaser.GameObjects.Sprite;
private genderIconElement: Phaser.GameObjects.Sprite;
private variantIconElement: Phaser.GameObjects.Sprite;
private shinyLabel: Phaser.GameObjects.Text;
private formLabel: Phaser.GameObjects.Text;
private genderLabel: Phaser.GameObjects.Text;
private variantLabel: Phaser.GameObjects.Text;
private candyUpgradeIconElement: Phaser.GameObjects.Sprite;
private candyUpgradeLabel: Phaser.GameObjects.Text;
private showBackSpriteIconElement: Phaser.GameObjects.Sprite;
private showBackSpriteLabel: Phaser.GameObjects.Text;
private starterSelectMessageBox: Phaser.GameObjects.NineSlice;
private starterSelectMessageBoxContainer: Phaser.GameObjects.Container;
private statsContainer: StatsContainer;
private moveInfoOverlay: MoveInfoOverlay;
private infoOverlay: PokedexInfoOverlay;
private baseStatsOverlay: BaseStatsOverlay;
private statsMode: boolean;
private allSpecies: PokemonSpecies[] = [];
private species: PokemonSpecies;
private starterId: number;
private formIndex: number;
private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>();
private levelMoves: LevelMoves;
private eggMoves: Moves[] = [];
private hasEggMoves: boolean[] = [];
private tmMoves: Moves[] = [];
private ability1: Abilities;
private ability2: Abilities | undefined;
private abilityHidden: Abilities | undefined;
private passive: Abilities;
private hasPassive: boolean;
private hasAbilities: number[];
private biomes: BiomeTierTod[];
private preBiomes: BiomeTierTod[];
private baseStats: number[];
private baseTotal: number;
private evolutions: SpeciesFormEvolution[];
private battleForms: SpeciesFormChange[];
private prevolutions: SpeciesFormEvolution[];
private speciesStarterDexEntry: DexEntry | null;
private canCycleShiny: boolean;
private canCycleForm: boolean;
private canCycleGender: boolean;
private assetLoadCancelled: BooleanHolder | null;
public cursorObj: Phaser.GameObjects.Image;
// variables to keep track of the dynamically rendered list of instruction prompts for starter select
private instructionRowX = 0;
private instructionRowY = 0;
private instructionRowTextOffset = 9;
private filterInstructionRowX = 0;
private filterInstructionRowY = 0;
private starterAttributes: StarterAttributes;
private savedStarterAttributes: StarterAttributes;
private previousSpecies: PokemonSpecies[];
private previousStarterAttributes: StarterAttributes[];
protected blockInput = false;
protected blockInputOverlay = false;
private showBackSprite = false;
// Menu
private menuContainer: Phaser.GameObjects.Container;
private menuBg: Phaser.GameObjects.NineSlice;
protected optionSelectText: BBCodeText;
private menuOptions: MenuOptions[];
protected scale = 0.1666666667;
private menuDescriptions: string[];
private isFormGender: boolean;
private filteredIndices: Species[] | null = null;
private availableVariants: number;
private unlockedVariants: boolean[];
private canUseCandies: boolean;
private exitCallback;
constructor() {
super(Mode.POKEDEX_PAGE);
}
setup() {
const ui = this.getUi();
const currentLanguage = i18next.resolvedLanguage ?? "en";
const langSettingKey = Object.keys(languageSettings).find(lang => currentLanguage.includes(lang)) ?? "en";
const textSettings = languageSettings[langSettingKey];
this.starterSelectContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6);
this.starterSelectContainer.setVisible(false);
ui.add(this.starterSelectContainer);
const bgColor = globalScene.add.rectangle(
0,
0,
globalScene.game.canvas.width / 6,
globalScene.game.canvas.height / 6,
0x006860,
);
bgColor.setOrigin(0, 0);
this.starterSelectContainer.add(bgColor);
const starterSelectBg = globalScene.add.image(0, 0, "pokedex_summary_bg");
starterSelectBg.setOrigin(0, 0);
this.starterSelectContainer.add(starterSelectBg);
this.shinyOverlay = globalScene.add.image(6, 6, "summary_overlay_shiny");
this.shinyOverlay.setOrigin(0, 0);
this.shinyOverlay.setVisible(false);
this.starterSelectContainer.add(this.shinyOverlay);
this.pokemonNumberText = addTextObject(17, 1, "0000", TextStyle.SUMMARY);
this.pokemonNumberText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonNumberText);
this.pokemonNameText = addTextObject(6, 112, "", TextStyle.SUMMARY);
this.pokemonNameText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonNameText);
this.pokemonGrowthRateLabelText = addTextObject(
8,
106,
i18next.t("pokedexUiHandler:growthRate"),
TextStyle.SUMMARY_ALT,
{ fontSize: "36px" },
);
this.pokemonGrowthRateLabelText.setOrigin(0, 0);
this.pokemonGrowthRateLabelText.setVisible(false);
this.starterSelectContainer.add(this.pokemonGrowthRateLabelText);
this.pokemonGrowthRateText = addTextObject(34, 106, "", TextStyle.SUMMARY_PINK, { fontSize: "36px" });
this.pokemonGrowthRateText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonGrowthRateText);
this.pokemonGenderText = addTextObject(96, 112, "", TextStyle.SUMMARY_ALT);
this.pokemonGenderText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonGenderText);
this.pokemonUncaughtText = addTextObject(6, 127, i18next.t("pokedexUiHandler:uncaught"), TextStyle.WINDOW, {
fontSize: "56px",
});
this.pokemonUncaughtText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonUncaughtText);
const starterBoxContainer = globalScene.add.container(speciesContainerX + 6, 9); //115
for (const species of allSpecies) {
if (!speciesStarterCosts.hasOwnProperty(species.speciesId) || !species.isObtainable()) {
continue;
}
this.speciesLoaded.set(species.speciesId, false);
this.allSpecies.push(species);
}
this.starterSelectContainer.add(starterBoxContainer);
this.pokemonSprite = globalScene.add.sprite(53, 63, "pkmn__sub");
this.pokemonSprite.setPipeline(globalScene.spritePipeline, {
tone: [0.0, 0.0, 0.0, 0.0],
ignoreTimeTint: true,
});
this.starterSelectContainer.add(this.pokemonSprite);
this.type1Icon = globalScene.add.sprite(8, 98, getLocalizedSpriteKey("types"));
this.type1Icon.setScale(0.5);
this.type1Icon.setOrigin(0, 0);
this.starterSelectContainer.add(this.type1Icon);
this.type2Icon = globalScene.add.sprite(26, 98, getLocalizedSpriteKey("types"));
this.type2Icon.setScale(0.5);
this.type2Icon.setOrigin(0, 0);
this.starterSelectContainer.add(this.type2Icon);
this.pokemonLuckLabelText = addTextObject(8, 89, i18next.t("common:luckIndicator"), TextStyle.WINDOW_ALT, {
fontSize: "56px",
});
this.pokemonLuckLabelText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonLuckLabelText);
this.pokemonLuckText = addTextObject(8 + this.pokemonLuckLabelText.displayWidth + 2, 89, "0", TextStyle.WINDOW, {
fontSize: "56px",
});
this.pokemonLuckText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonLuckText);
// Candy icon and count
this.pokemonCandyContainer = globalScene.add.container(4.5, 18);
this.pokemonCandyIcon = globalScene.add.sprite(0, 0, "candy");
this.pokemonCandyIcon.setScale(0.5);
this.pokemonCandyIcon.setOrigin(0, 0);
this.pokemonCandyContainer.add(this.pokemonCandyIcon);
this.pokemonCandyOverlayIcon = globalScene.add.sprite(0, 0, "candy_overlay");
this.pokemonCandyOverlayIcon.setScale(0.5);
this.pokemonCandyOverlayIcon.setOrigin(0, 0);
this.pokemonCandyContainer.add(this.pokemonCandyOverlayIcon);
this.pokemonCandyDarknessOverlay = globalScene.add.sprite(0, 0, "candy");
this.pokemonCandyDarknessOverlay.setScale(0.5);
this.pokemonCandyDarknessOverlay.setOrigin(0, 0);
this.pokemonCandyDarknessOverlay.setTint(0x000000);
this.pokemonCandyDarknessOverlay.setAlpha(0.5);
this.pokemonCandyContainer.add(this.pokemonCandyDarknessOverlay);
this.pokemonCandyCountText = addTextObject(9.5, 0, "x0", TextStyle.WINDOW_ALT, { fontSize: "56px" });
this.pokemonCandyCountText.setOrigin(0, 0);
this.pokemonCandyContainer.add(this.pokemonCandyCountText);
this.pokemonCandyContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 30, 20), Phaser.Geom.Rectangle.Contains);
this.starterSelectContainer.add(this.pokemonCandyContainer);
this.pokemonFormText = addTextObject(6, 42, "Form", TextStyle.WINDOW_ALT, {
fontSize: "42px",
});
this.pokemonFormText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonFormText);
this.pokemonCaughtHatchedContainer = globalScene.add.container(2, 25);
this.pokemonCaughtHatchedContainer.setScale(0.5);
this.starterSelectContainer.add(this.pokemonCaughtHatchedContainer);
const pokemonCaughtIcon = globalScene.add.sprite(1, 0, "items", "pb");
pokemonCaughtIcon.setOrigin(0, 0);
pokemonCaughtIcon.setScale(0.75);
this.pokemonCaughtHatchedContainer.add(pokemonCaughtIcon);
this.pokemonCaughtCountText = addTextObject(24, 4, "0", TextStyle.SUMMARY_ALT);
this.pokemonCaughtCountText.setOrigin(0, 0);
this.pokemonCaughtHatchedContainer.add(this.pokemonCaughtCountText);
this.pokemonHatchedIcon = globalScene.add.sprite(1, 14, "egg_icons");
this.pokemonHatchedIcon.setOrigin(0.15, 0.2);
this.pokemonHatchedIcon.setScale(0.8);
this.pokemonCaughtHatchedContainer.add(this.pokemonHatchedIcon);
this.pokemonShinyIcons = [];
for (let i = 0; i < 3; i++) {
const pokemonShinyIcon = globalScene.add.sprite(153 + i * 13, 160, "shiny_icons");
pokemonShinyIcon.setOrigin(0.15, 0.2);
pokemonShinyIcon.setScale(1);
pokemonShinyIcon.setFrame(getVariantIcon(i as Variant));
pokemonShinyIcon.setVisible(false);
this.pokemonCaughtHatchedContainer.add(pokemonShinyIcon);
this.pokemonShinyIcons.push(pokemonShinyIcon);
}
this.pokemonHatchedCountText = addTextObject(24, 19, "0", TextStyle.SUMMARY_ALT);
this.pokemonHatchedCountText.setOrigin(0, 0);
this.pokemonCaughtHatchedContainer.add(this.pokemonHatchedCountText);
// The font size should be set per language
const instructionTextSize = textSettings.instructionTextSize;
this.instructionsContainer = globalScene.add.container(4, 128);
this.instructionsContainer.setVisible(true);
this.starterSelectContainer.add(this.instructionsContainer);
this.candyUpgradeIconElement = new Phaser.GameObjects.Sprite(
globalScene,
this.instructionRowX,
this.instructionRowY,
"keyboard",
"C.png",
);
this.candyUpgradeIconElement.setName("sprite-candyUpgrade-icon-element");
this.candyUpgradeIconElement.setScale(0.675);
this.candyUpgradeIconElement.setOrigin(0.0, 0.0);
this.candyUpgradeLabel = addTextObject(
this.instructionRowX + this.instructionRowTextOffset,
this.instructionRowY,
i18next.t("pokedexUiHandler:candyUpgrade"),
TextStyle.PARTY,
{ fontSize: instructionTextSize },
);
this.candyUpgradeLabel.setName("text-candyUpgrade-label");
// instruction rows that will be pushed into the container dynamically based on need
// creating new sprites since they will be added to the scene later
this.shinyIconElement = new Phaser.GameObjects.Sprite(
globalScene,
this.instructionRowX,
this.instructionRowY,
"keyboard",
"R.png",
);
this.shinyIconElement.setName("sprite-shiny-icon-element");
this.shinyIconElement.setScale(0.675);
this.shinyIconElement.setOrigin(0.0, 0.0);
this.shinyLabel = addTextObject(
this.instructionRowX + this.instructionRowTextOffset,
this.instructionRowY,
i18next.t("pokedexUiHandler:cycleShiny"),
TextStyle.PARTY,
{ fontSize: instructionTextSize },
);
this.shinyLabel.setName("text-shiny-label");
this.formIconElement = new Phaser.GameObjects.Sprite(
globalScene,
this.instructionRowX,
this.instructionRowY,
"keyboard",
"F.png",
);
this.formIconElement.setName("sprite-form-icon-element");
this.formIconElement.setScale(0.675);
this.formIconElement.setOrigin(0.0, 0.0);
this.formLabel = addTextObject(
this.instructionRowX + this.instructionRowTextOffset,
this.instructionRowY,
i18next.t("pokedexUiHandler:cycleForm"),
TextStyle.PARTY,
{ fontSize: instructionTextSize },
);
this.formLabel.setName("text-form-label");
this.genderIconElement = new Phaser.GameObjects.Sprite(
globalScene,
this.instructionRowX,
this.instructionRowY,
"keyboard",
"G.png",
);
this.genderIconElement.setName("sprite-gender-icon-element");
this.genderIconElement.setScale(0.675);
this.genderIconElement.setOrigin(0.0, 0.0);
this.genderLabel = addTextObject(
this.instructionRowX + this.instructionRowTextOffset,
this.instructionRowY,
i18next.t("pokedexUiHandler:cycleGender"),
TextStyle.PARTY,
{ fontSize: instructionTextSize },
);
this.genderLabel.setName("text-gender-label");
this.variantIconElement = new Phaser.GameObjects.Sprite(
globalScene,
this.instructionRowX,
this.instructionRowY,
"keyboard",
"V.png",
);
this.variantIconElement.setName("sprite-variant-icon-element");
this.variantIconElement.setScale(0.675);
this.variantIconElement.setOrigin(0.0, 0.0);
this.variantLabel = addTextObject(
this.instructionRowX + this.instructionRowTextOffset,
this.instructionRowY,
i18next.t("pokedexUiHandler:cycleVariant"),
TextStyle.PARTY,
{ fontSize: instructionTextSize },
);
this.variantLabel.setName("text-variant-label");
this.showBackSpriteIconElement = new Phaser.GameObjects.Sprite(globalScene, 50, 7, "keyboard", "E.png");
this.showBackSpriteIconElement.setName("show-backSprite-icon-element");
this.showBackSpriteIconElement.setScale(0.675);
this.showBackSpriteIconElement.setOrigin(0.0, 0.0);
this.showBackSpriteLabel = addTextObject(60, 7, i18next.t("pokedexUiHandler:showBackSprite"), TextStyle.PARTY, {
fontSize: instructionTextSize,
});
this.showBackSpriteLabel.setName("show-backSprite-label");
this.starterSelectContainer.add(this.showBackSpriteIconElement);
this.starterSelectContainer.add(this.showBackSpriteLabel);
this.hideInstructions();
this.filterInstructionsContainer = globalScene.add.container(50, 5);
this.filterInstructionsContainer.setVisible(true);
this.starterSelectContainer.add(this.filterInstructionsContainer);
this.starterSelectMessageBoxContainer = globalScene.add.container(0, globalScene.game.canvas.height / 6);
this.starterSelectMessageBoxContainer.setVisible(false);
this.starterSelectContainer.add(this.starterSelectMessageBoxContainer);
this.starterSelectMessageBox = addWindow(1, -1, 318, 28);
this.starterSelectMessageBox.setOrigin(0, 1);
this.starterSelectMessageBoxContainer.add(this.starterSelectMessageBox);
this.message = addTextObject(8, 8, "", TextStyle.WINDOW, { maxLines: 2 });
this.message.setOrigin(0, 0);
this.starterSelectMessageBoxContainer.add(this.message);
// arrow icon for the message box
this.initPromptSprite(this.starterSelectMessageBoxContainer);
this.statsContainer = new StatsContainer(6, 16);
globalScene.add.existing(this.statsContainer);
this.statsContainer.setVisible(false);
this.starterSelectContainer.add(this.statsContainer);
// Adding menu container
this.menuContainer = globalScene.add.container(-130, 0);
this.menuContainer.setName("menu");
this.menuContainer.setInteractive(
new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6),
Phaser.Geom.Rectangle.Contains,
);
this.menuContainer.setVisible(false);
this.menuOptions = getEnumKeys(MenuOptions).map(m => Number.parseInt(MenuOptions[m]) as MenuOptions);
this.optionSelectText = addBBCodeTextObject(
0,
0,
this.menuOptions.map(o => `${i18next.t(`pokedexUiHandler:${MenuOptions[o]}`)}`).join("\n"),
TextStyle.WINDOW,
{ maxLines: this.menuOptions.length, lineSpacing: 12 },
);
this.menuDescriptions = [
i18next.t("pokedexUiHandler:showBaseStats"),
i18next.t("pokedexUiHandler:showAbilities"),
i18next.t("pokedexUiHandler:showLevelMoves"),
i18next.t("pokedexUiHandler:showEggMoves"),
i18next.t("pokedexUiHandler:showTmMoves"),
i18next.t("pokedexUiHandler:showBiomes"),
i18next.t("pokedexUiHandler:showNatures"),
i18next.t("pokedexUiHandler:toggleIVs"),
i18next.t("pokedexUiHandler:showEvolutions"),
];
this.scale = getTextStyleOptions(TextStyle.WINDOW, globalScene.uiTheme).scale;
this.menuBg = addWindow(
globalScene.game.canvas.width / 6 - 83,
0,
this.optionSelectText.displayWidth + 19 + 24 * this.scale,
globalScene.game.canvas.height / 6 - 2,
);
this.menuBg.setOrigin(0, 0);
this.optionSelectText.setPosition(this.menuBg.x + 10 + 24 * this.scale, this.menuBg.y + 6);
this.menuContainer.add(this.menuBg);
this.menuContainer.add(this.optionSelectText);
ui.add(this.menuContainer);
this.starterSelectContainer.add(this.menuContainer);
// adding base stats
this.baseStatsOverlay = new BaseStatsOverlay({ x: 317, y: 0, width: 133 });
this.menuContainer.add(this.baseStatsOverlay);
this.menuContainer.bringToTop(this.baseStatsOverlay);
// add the info overlay last to be the top most ui element and prevent the IVs from overlaying this
const overlayScale = 1;
this.moveInfoOverlay = new MoveInfoOverlay({
scale: overlayScale,
top: true,
x: 1,
y: globalScene.game.canvas.height / 6 - MoveInfoOverlay.getHeight(overlayScale) - 29,
});
this.starterSelectContainer.add(this.moveInfoOverlay);
this.infoOverlay = new PokedexInfoOverlay({
scale: overlayScale,
x: 1,
y: globalScene.game.canvas.height / 6 - PokedexInfoOverlay.getHeight(overlayScale) - 29,
});
this.starterSelectContainer.add(this.infoOverlay);
// Filter bar sits above everything, except the message box
this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer);
this.previousSpecies = [];
this.previousStarterAttributes = [];
}
show(args: any[]): boolean {
// Allow the use of candies if we are in one of the whitelisted phases
this.canUseCandies = ["TitlePhase", "SelectStarterPhase", "CommandPhase"].includes(
globalScene.getCurrentPhase()?.constructor.name ?? "",
);
if (args.length >= 1 && args[0] === "refresh") {
return false;
}
this.species = args[0];
this.savedStarterAttributes = args[1] ?? {
shiny: false,
female: true,
variant: 0,
form: 0,
};
this.formIndex = this.savedStarterAttributes.form ?? 0;
this.filteredIndices = args[2] ?? null;
this.starterSetup();
if (args[4] instanceof Function) {
this.exitCallback = args[4];
}
this.moveInfoOverlay.clear(); // clear this when removing a menu; the cancel button doesn't seem to trigger this automatically on controllers
this.infoOverlay.clear();
super.show(args);
this.starterSelectContainer.setVisible(true);
this.getUi().bringToTop(this.starterSelectContainer);
this.starterAttributes = this.initStarterPrefs();
this.menuOptions = getEnumKeys(MenuOptions).map(m => Number.parseInt(MenuOptions[m]) as MenuOptions);
this.menuContainer.setVisible(true);
this.speciesStarterDexEntry = this.species ? globalScene.gameData.dexData[this.species.speciesId] : null;
this.setSpecies();
this.updateInstructions();
this.optionSelectText.setText(this.getMenuText());
this.setCursor(0);
return true;
}
getMenuText(): string {
const isSeen = this.isSeen();
const isStarterCaught = !!this.isCaught(this.getStarterSpecies(this.species));
return this.menuOptions
.map(o => {
const label = `${i18next.t(`pokedexUiHandler:${MenuOptions[o]}`)}`;
const isDark =
!isSeen ||
(!isStarterCaught && (o === MenuOptions.TOGGLE_IVS || o === MenuOptions.NATURES)) ||
(this.tmMoves.length < 1 && o === MenuOptions.TM_MOVES);
const color = getTextColor(
isDark ? TextStyle.SHADOW_TEXT : TextStyle.SETTINGS_VALUE,
false,
globalScene.uiTheme,
);
const shadow = getTextColor(
isDark ? TextStyle.SHADOW_TEXT : TextStyle.SETTINGS_VALUE,
true,
globalScene.uiTheme,
);
return `[shadow=${shadow}][color=${color}]${label}[/color][/shadow]`;
})
.join("\n");
}
starterSetup(): void {
this.evolutions = [];
this.prevolutions = [];
this.battleForms = [];
const species = this.species;
let formKey = this.species?.forms.length > 0 ? this.species.forms[this.formIndex].formKey : "";
this.isFormGender = formKey === "male" || formKey === "female";
if (
this.isFormGender &&
((this.savedStarterAttributes.female === true && formKey === "male") ||
(this.savedStarterAttributes.female === false && formKey === "female"))
) {
this.formIndex = (this.formIndex + 1) % 2;
formKey = this.species.forms[this.formIndex].formKey;
}
const formIndex = this.formIndex ?? 0;
this.starterId = this.getStarterSpeciesId(this.species.speciesId);
const allEvolutions = pokemonEvolutions.hasOwnProperty(species.speciesId)
? pokemonEvolutions[species.speciesId]
: [];
if (species.forms.length > 0) {
const form = species.forms[formIndex];
// If this form has a specific set of moves, we get them.
this.levelMoves =
formIndex > 0 &&
pokemonFormLevelMoves.hasOwnProperty(species.speciesId) &&
pokemonFormLevelMoves[species.speciesId].hasOwnProperty(formIndex)
? pokemonFormLevelMoves[species.speciesId][formIndex]
: pokemonSpeciesLevelMoves[species.speciesId];
this.ability1 = form.ability1;
this.ability2 = form.ability2 === form.ability1 ? undefined : form.ability2;
this.abilityHidden = form.abilityHidden === form.ability1 ? undefined : form.abilityHidden;
this.evolutions = allEvolutions.filter(e => e.preFormKey === form.formKey || e.preFormKey === null);
this.baseStats = form.baseStats;
this.baseTotal = form.baseTotal;
} else {
this.levelMoves = pokemonSpeciesLevelMoves[species.speciesId];
this.ability1 = species.ability1;
this.ability2 = species.ability2 === species.ability1 ? undefined : species.ability2;
this.abilityHidden = species.abilityHidden === species.ability1 ? undefined : species.abilityHidden;
this.evolutions = allEvolutions;
this.baseStats = species.baseStats;
this.baseTotal = species.baseTotal;
}
this.eggMoves = speciesEggMoves[this.starterId] ?? [];
this.hasEggMoves = Array.from(
{ length: 4 },
(_, em) => (globalScene.gameData.starterData[this.starterId].eggMoves & (1 << em)) !== 0,
);
this.tmMoves =
speciesTmMoves[species.speciesId]
?.filter(m => (Array.isArray(m) ? m[0] === formKey : true))
.map(m => (Array.isArray(m) ? m[1] : m))
.sort((a, b) => (allMoves[a].name > allMoves[b].name ? 1 : -1)) ?? [];
const passiveId = starterPassiveAbilities.hasOwnProperty(species.speciesId)
? species.speciesId
: starterPassiveAbilities.hasOwnProperty(this.starterId)
? this.starterId
: pokemonPrevolutions[this.starterId];
const passives = starterPassiveAbilities[passiveId];
this.passive = this.formIndex in passives ? passives[formIndex] : passives[0];
const starterData = globalScene.gameData.starterData[this.starterId];
const abilityAttr = starterData.abilityAttr;
this.hasPassive = starterData.passiveAttr > 0;
const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1;
const hasAbility2 = abilityAttr & AbilityAttr.ABILITY_2;
const hasHiddenAbility = abilityAttr & AbilityAttr.ABILITY_HIDDEN;
this.hasAbilities = [hasAbility1, hasAbility2, hasHiddenAbility];
const allBiomes = catchableSpecies[species.speciesId] ?? [];
this.preBiomes = this.sanitizeBiomes(
(catchableSpecies[this.starterId] ?? []).filter(
b => !allBiomes.some(bm => b.biome === bm.biome && b.tier === bm.tier) && !(b.biome === Biome.TOWN),
),
this.starterId,
);
this.biomes = this.sanitizeBiomes(allBiomes, species.speciesId);
const allFormChanges = pokemonFormChanges.hasOwnProperty(species.speciesId)
? pokemonFormChanges[species.speciesId]
: [];
this.battleForms = allFormChanges.filter(f => f.preFormKey === this.species.forms[this.formIndex].formKey);
const preSpecies = pokemonPrevolutions.hasOwnProperty(this.species.speciesId)
? allSpecies.find(sp => sp.speciesId === pokemonPrevolutions[this.species.speciesId])
: null;
if (preSpecies) {
const preEvolutions = pokemonEvolutions.hasOwnProperty(preSpecies.speciesId)
? pokemonEvolutions[preSpecies.speciesId]
: [];
this.prevolutions = preEvolutions.filter(
e =>
e.speciesId === species.speciesId &&
(((e.evoFormKey === "" || e.evoFormKey === null) &&
// This takes care of Cosplay Pikachu (Pichu is not shown)
(preSpecies.forms.some(form => form.formKey === species.forms[formIndex]?.formKey) ||
// This takes care of Gholdengo
(preSpecies.forms.length > 0 && species.forms.length === 0) ||
// This takes care of everything else
(preSpecies.forms.length === 0 &&
(species.forms.length === 0 || species.forms[formIndex]?.formKey === "")))) ||
// This takes care of Burmy, Shellos etc
e.evoFormKey === species.forms[formIndex]?.formKey),
);
}
this.availableVariants = species.getFullUnlocksData() & DexAttr.VARIANT_3 ? 3 : 1;
}
// Function to ensure that forms appear in the appropriate biome and tod
sanitizeBiomes(biomes: BiomeTierTod[], speciesId: number): BiomeTierTod[] {
if (speciesId === Species.BURMY || speciesId === Species.WORMADAM) {
return biomes.filter(b => {
const formIndex = (() => {
switch (b.biome) {
case Biome.BEACH:
return 1;
case Biome.SLUM:
return 2;
default:
return 0;
}
})();
return this.formIndex === formIndex;
});
}
if (speciesId === Species.ROTOM) {
return biomes.filter(b => {
const formIndex = (() => {
switch (b.biome) {
case Biome.VOLCANO:
return 1;
case Biome.SEA:
return 2;
case Biome.ICE_CAVE:
return 3;
case Biome.MOUNTAIN:
return 4;
case Biome.TALL_GRASS:
return 5;
default:
return 0;
}
})();
return this.formIndex === formIndex;
});
}
if (speciesId === Species.LYCANROC) {
return biomes.filter(b => {
const formIndex = (() => {
switch (b.tod[0]) {
case TimeOfDay.DAY:
case TimeOfDay.DAWN:
return 0;
case TimeOfDay.DUSK:
return 2;
case TimeOfDay.NIGHT:
return 1;
default:
return 0;
}
})();
return this.formIndex === formIndex;
});
}
return biomes;
}
isCaught(otherSpecies?: PokemonSpecies): bigint {
if (globalScene.dexForDevs) {
return 255n;
}
const species = otherSpecies ? otherSpecies : this.species;
const dexEntry = globalScene.gameData.dexData[species.speciesId];
const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)];
return (dexEntry?.caughtAttr ?? 0n) & (starterDexEntry?.caughtAttr ?? 0n) & species.getFullUnlocksData();
}
/**
* Check whether a given form is caught for a given species.
* All forms that can be reached through a form change during battle are considered caught and show up in the dex as such.
*
* @param otherSpecies The species to check; defaults to current species
* @param otherFormIndex The form index of the form to check; defaults to current form
* @returns StarterAttributes for the species
*/
isFormCaught(otherSpecies?: PokemonSpecies, otherFormIndex?: number | undefined): boolean {
if (globalScene.dexForDevs) {
return true;
}
const species = otherSpecies ? otherSpecies : this.species;
const formIndex = otherFormIndex !== undefined ? otherFormIndex : this.formIndex;
const caughtAttr = this.isCaught(species);
if (caughtAttr && (!species.forms.length || species.forms.length === 1)) {
return true;
}
const isFormCaught = (caughtAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n;
return isFormCaught;
}
isSeen(): boolean {
if (this.speciesStarterDexEntry?.seenAttr) {
return true;
}
const starterCaughtAttr = this.isCaught(this.getStarterSpecies(this.species));
return !!starterCaughtAttr;
}
/**
* Get the starter attributes for the given PokemonSpecies, after sanitizing them.
* If somehow a preference is set for a form, variant, gender, ability or nature
* that wasn't actually unlocked or is invalid it will be cleared here
*
* @param species The species to get Starter Preferences for
* @returns StarterAttributes for the species
*/
initStarterPrefs(): StarterAttributes {
const starterAttributes: StarterAttributes | null = this.species ? { ...this.savedStarterAttributes } : null;
const caughtAttr = this.isCaught();
// no preferences or Pokemon wasn't caught, return empty attribute
if (!starterAttributes || !this.isSeen()) {
return {};
}
const hasShiny = caughtAttr & DexAttr.SHINY;
const hasNonShiny = caughtAttr & DexAttr.NON_SHINY;
if (!hasShiny || (starterAttributes.shiny === undefined && hasNonShiny)) {
// shiny form wasn't unlocked, purging shiny and variant setting
starterAttributes.shiny = false;
starterAttributes.variant = 0;
} else if (!hasNonShiny || (starterAttributes.shiny === undefined && hasShiny)) {
starterAttributes.shiny = true;
starterAttributes.variant = 0;
}
this.unlockedVariants = [
!!(hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT),
!!(hasShiny && caughtAttr & DexAttr.VARIANT_2),
!!(hasShiny && caughtAttr & DexAttr.VARIANT_3),
];
if (
starterAttributes.variant === undefined ||
Number.isNaN(starterAttributes.variant) ||
starterAttributes.variant < 0
) {
starterAttributes.variant = 0;
} else if (!this.unlockedVariants[starterAttributes.variant]) {
let highestValidIndex = -1;
for (let i = 0; i <= starterAttributes.variant && i < this.unlockedVariants.length; i++) {
if (this.unlockedVariants[i]) {
highestValidIndex = i;
}
}
// Set to the highest valid index found or default to 0
starterAttributes.variant = highestValidIndex !== -1 ? highestValidIndex : 0;
}
if (starterAttributes.female !== undefined) {
if (
(starterAttributes.female && !(caughtAttr & DexAttr.FEMALE)) ||
(!starterAttributes.female && !(caughtAttr & DexAttr.MALE))
) {
starterAttributes.female = !starterAttributes.female;
}
} else {
if (caughtAttr & DexAttr.FEMALE) {
starterAttributes.female = true;
} else if (caughtAttr & DexAttr.MALE) {
starterAttributes.female = false;
}
}
return starterAttributes;
}
showText(
text: string,
delay?: number,
callback?: Function,
callbackDelay?: number,
prompt?: boolean,
promptDelay?: number,
moveToTop?: boolean,
) {
super.showText(text, delay, callback, callbackDelay, prompt, promptDelay);
const singleLine = text?.indexOf("\n") === -1;
this.starterSelectMessageBox.setSize(318, singleLine ? 28 : 42);
if (moveToTop) {
this.starterSelectMessageBox.setOrigin(0, 0);
this.starterSelectMessageBoxContainer.setY(0);
this.message.setY(4);
} else {
this.starterSelectMessageBoxContainer.setY(globalScene.game.canvas.height / 6);
this.starterSelectMessageBox.setOrigin(0, 1);
this.message.setY(singleLine ? -22 : -37);
}
this.starterSelectMessageBoxContainer.setVisible(!!text?.length);
}
/**
* Determines if 'Icon' based upgrade notifications should be shown
* @returns true if upgrade notifications are enabled and set to display an 'Icon'
*/
isUpgradeIconEnabled(): boolean {
return globalScene.candyUpgradeNotification !== 0 && globalScene.candyUpgradeDisplay === 0;
}
/**
* Determines if 'Animation' based upgrade notifications should be shown
* @returns true if upgrade notifications are enabled and set to display an 'Animation'
*/
isUpgradeAnimationEnabled(): boolean {
return globalScene.candyUpgradeNotification !== 0 && globalScene.candyUpgradeDisplay === 1;
}
/**
* If the pokemon is an evolution, find speciesId of its starter.
* @param speciesId the id of the species to check
* @returns the id of the corresponding starter
*/
getStarterSpeciesId(speciesId): number {
if (speciesId === Species.PIKACHU) {
if ([0, 1, 8].includes(this.formIndex)) {
return Species.PICHU;
}
return Species.PIKACHU;
}
if (speciesStarterCosts.hasOwnProperty(speciesId)) {
return speciesId;
}
return pokemonStarters[speciesId];
}
getStarterSpecies(species): PokemonSpecies {
if (speciesStarterCosts.hasOwnProperty(species.speciesId)) {
return species;
}
return allSpecies.find(sp => sp.speciesId === pokemonStarters[species.speciesId]) ?? species;
}
processInput(button: Button): boolean {
if (this.blockInput) {
return false;
}
const ui = this.getUi();
let success = false;
let error = false;
const isCaught = this.isCaught();
const isFormCaught = this.isFormCaught();
const isSeen = this.isSeen();
const isStarterCaught = !!this.isCaught(this.getStarterSpecies(this.species));
if (this.blockInputOverlay) {
if (button === Button.CANCEL || button === Button.ACTION) {
this.blockInputOverlay = false;
this.baseStatsOverlay.clear();
ui.showText("");
return true;
}
if (button === Button.UP || button === Button.DOWN) {
this.blockInputOverlay = false;
this.baseStatsOverlay.clear();
ui.showText("");
} else {
return false;
}
}
if (button === Button.SUBMIT) {
success = true;
} else if (button === Button.CANCEL) {
if (this.statsMode) {
this.toggleStatsMode(false);
success = true;
} else if (this.previousSpecies.length > 0) {
this.blockInput = true;
ui.setModeWithoutClear(Mode.OPTION_SELECT).then(() => {
const species = this.previousSpecies.pop();
const starterAttributes = this.previousStarterAttributes.pop();
this.moveInfoOverlay.clear();
this.clearText();
ui.setModeForceTransition(Mode.POKEDEX_PAGE, species, starterAttributes);
success = true;
});
this.blockInput = false;
} else {
ui.revertMode().then(() => {
console.log("exitCallback", this.exitCallback);
if (this.exitCallback instanceof Function) {
const exitCallback = this.exitCallback;
this.exitCallback = null;
exitCallback();
}
});
success = true;
}
} else {
const starterData = globalScene.gameData.starterData[this.starterId];
// prepare persistent starter data to store changes
const starterAttributes = this.starterAttributes;
if (button === Button.ACTION) {
switch (this.cursor) {
case MenuOptions.BASE_STATS:
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => {
ui.showText(i18next.t("pokedexUiHandler:showBaseStats"), null, () => {
this.baseStatsOverlay.show(this.baseStats, this.baseTotal);
this.blockInput = false;
this.blockInputOverlay = true;
return true;
});
success = true;
});
}
break;
case MenuOptions.LEVEL_MOVES:
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => {
ui.showText(i18next.t("pokedexUiHandler:showLevelMoves"), null, () => {
this.moveInfoOverlay.show(allMoves[this.levelMoves[0][1]]);
ui.setModeWithoutClear(Mode.OPTION_SELECT, {
options: this.levelMoves
.map(m => {
const levelNumber = m[0] > 0 ? String(m[0]) : "";
const option: OptionSelectItem = {
label: levelNumber.padEnd(4, " ") + allMoves[m[1]].name,
handler: () => {
return false;
},
onHover: () => {
this.moveInfoOverlay.show(allMoves[m[1]]);
if (m[0] === 0) {
this.showText(i18next.t("pokedexUiHandler:onlyEvolutionMove"));
} else if (m[0] === -1) {
this.showText(i18next.t("pokedexUiHandler:onlyRecallMove"));
} else if (m[0] <= 5) {
this.showText(i18next.t("pokedexUiHandler:onStarterSelectMove"));
} else {
this.showText(i18next.t("pokedexUiHandler:byLevelUpMove"));
}
},
};
return option;
})
.concat({
label: i18next.t("menu:cancel"),
handler: () => {
this.moveInfoOverlay.clear();
this.clearText();
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
return true;
},
onHover: () => {
this.moveInfoOverlay.clear();
},
}),
supportHover: true,
maxOptions: 8,
yOffset: 19,
});
this.blockInput = false;
});
});
success = true;
}
break;
case MenuOptions.EGG_MOVES:
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => {
if (this.eggMoves.length === 0) {
ui.showText(i18next.t("pokedexUiHandler:noEggMoves"));
this.blockInput = false;
return true;
}
ui.showText(i18next.t("pokedexUiHandler:showEggMoves"), null, () => {
this.moveInfoOverlay.show(allMoves[this.eggMoves[0]]);
ui.setModeWithoutClear(Mode.OPTION_SELECT, {
options: [
{
label: i18next.t("pokedexUiHandler:common"),
skip: true,
style: TextStyle.MONEY_WINDOW,
handler: () => false, // Non-selectable, but handler is required
onHover: () => this.moveInfoOverlay.clear(), // No hover behavior for titles
},
...this.eggMoves.slice(0, 3).map((m, i) => ({
label: allMoves[m].name,
style: this.hasEggMoves[i] ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT,
handler: () => false,
onHover: () => this.moveInfoOverlay.show(allMoves[m]),
})),
{
label: i18next.t("pokedexUiHandler:rare"),
skip: true,
style: TextStyle.MONEY_WINDOW,
handler: () => false,
onHover: () => this.moveInfoOverlay.clear(),
},
{
label: allMoves[this.eggMoves[3]].name,
style: this.hasEggMoves[3] ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT,
handler: () => false,
onHover: () => this.moveInfoOverlay.show(allMoves[this.eggMoves[3]]),
},
{
label: i18next.t("menu:cancel"),
handler: () => {
this.moveInfoOverlay.clear();
this.clearText();
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
return true;
},
onHover: () => this.moveInfoOverlay.clear(),
},
],
supportHover: true,
maxOptions: 8,
yOffset: 19,
});
this.blockInput = false;
});
});
success = true;
}
break;
case MenuOptions.TM_MOVES:
if (!isSeen) {
error = true;
} else if (this.tmMoves.length < 1) {
ui.showText(i18next.t("pokedexUiHandler:noTmMoves"));
error = true;
} else {
this.blockInput = true;
ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => {
ui.showText(i18next.t("pokedexUiHandler:showTmMoves"), null, () => {
this.moveInfoOverlay.show(allMoves[this.tmMoves[0]]);
ui.setModeWithoutClear(Mode.OPTION_SELECT, {
options: this.tmMoves
.map(m => {
const option: OptionSelectItem = {
label: allMoves[m].name,
handler: () => {
return false;
},
onHover: () => {
this.moveInfoOverlay.show(allMoves[m]);
},
};
return option;
})
.concat({
label: i18next.t("menu:cancel"),
handler: () => {
this.moveInfoOverlay.clear();
this.clearText();
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
return true;
},
onHover: () => {
this.moveInfoOverlay.clear();
},
}),
supportHover: true,
maxOptions: 8,
yOffset: 19,
});
this.blockInput = false;
});
});
success = true;
}
break;
case MenuOptions.ABILITIES:
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => {
ui.showText(i18next.t("pokedexUiHandler:showAbilities"), null, () => {
this.infoOverlay.show(allAbilities[this.ability1].description);
const options: any[] = [];
if (this.ability1) {
options.push({
label: allAbilities[this.ability1].name,
style: this.hasAbilities[0] > 0 ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT,
handler: () => false,
onHover: () => this.infoOverlay.show(allAbilities[this.ability1].description),
});
}
if (this.ability2) {
const ability = allAbilities[this.ability2];
options.push({
label: ability?.name,
style: this.hasAbilities[1] > 0 ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT,
handler: () => false,
onHover: () => this.infoOverlay.show(ability?.description),
});
}
if (this.abilityHidden) {
options.push({
label: i18next.t("pokedexUiHandler:hidden"),
skip: true,
style: TextStyle.MONEY_WINDOW,
handler: () => false,
onHover: () => this.infoOverlay.clear(),
});
const ability = allAbilities[this.abilityHidden];
options.push({
label: allAbilities[this.abilityHidden].name,
style: this.hasAbilities[2] > 0 ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT,
handler: () => false,
onHover: () => this.infoOverlay.show(ability?.description),
});
}
if (this.passive) {
options.push({
label: i18next.t("pokedexUiHandler:passive"),
skip: true,
style: TextStyle.MONEY_WINDOW,
handler: () => false,
onHover: () => this.infoOverlay.clear(),
});
options.push({
label: allAbilities[this.passive].name,
style: this.hasPassive ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT,
handler: () => false,
onHover: () => this.infoOverlay.show(allAbilities[this.passive].description),
});
}
options.push({
label: i18next.t("menu:cancel"),
handler: () => {
this.infoOverlay.clear();
this.clearText();
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
return true;
},
onHover: () => this.infoOverlay.clear(),
});
ui.setModeWithoutClear(Mode.OPTION_SELECT, {
options: options,
supportHover: true,
maxOptions: 8,
yOffset: 19,
});
this.blockInput = false;
});
});
success = true;
}
break;
case MenuOptions.BIOMES:
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => {
if ((!this.biomes || this.biomes?.length === 0) && (!this.preBiomes || this.preBiomes?.length === 0)) {
ui.showText(i18next.t("pokedexUiHandler:noBiomes"));
ui.playError();
this.blockInput = false;
return true;
}
const options: any[] = [];
ui.showText(i18next.t("pokedexUiHandler:showBiomes"), null, () => {
this.biomes.map(b => {
options.push({
label:
i18next.t(`biome:${Biome[b.biome].toUpperCase()}`) +
" - " +
i18next.t(`biome:${BiomePoolTier[b.tier].toUpperCase()}`) +
(b.tod.length === 1 && b.tod[0] === -1
? ""
: " (" +
b.tod.map(tod => i18next.t(`biome:${TimeOfDay[tod].toUpperCase()}`)).join(", ") +
")"),
handler: () => false,
});
});
if (this.preBiomes.length > 0) {
options.push({
label: i18next.t("pokedexUiHandler:preBiomes"),
skip: true,
handler: () => false,
});
this.preBiomes.map(b => {
options.push({
label:
i18next.t(`biome:${Biome[b.biome].toUpperCase()}`) +
" - " +
i18next.t(`biome:${BiomePoolTier[b.tier].toUpperCase()}`) +
(b.tod.length === 1 && b.tod[0] === -1
? ""
: " (" +
b.tod.map(tod => i18next.t(`biome:${TimeOfDay[tod].toUpperCase()}`)).join(", ") +
")"),
handler: () => false,
});
});
}
options.push({
label: i18next.t("menu:cancel"),
handler: () => {
this.moveInfoOverlay.clear();
this.clearText();
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
return true;
},
onHover: () => this.moveInfoOverlay.clear(),
});
ui.setModeWithoutClear(Mode.OPTION_SELECT, {
options: options,
supportHover: true,
maxOptions: 8,
yOffset: 19,
});
this.blockInput = false;
});
});
success = true;
}
break;
case MenuOptions.EVOLUTIONS:
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => {
const options: any[] = [];
if (
(!this.prevolutions || this.prevolutions?.length === 0) &&
(!this.evolutions || this.evolutions?.length === 0) &&
(!this.battleForms || this.battleForms?.length === 0)
) {
ui.showText(i18next.t("pokedexUiHandler:noEvolutions"));
ui.playError();
this.blockInput = false;
return true;
}
ui.showText(i18next.t("pokedexUiHandler:showEvolutions"), null, () => {
if (this.prevolutions?.length > 0) {
options.push({
label: i18next.t("pokedexUiHandler:prevolutions"),
style: TextStyle.MONEY_WINDOW,
skip: true,
handler: () => false,
});
this.prevolutions.map(pre => {
const preSpecies = allSpecies.find(
species => species.speciesId === pokemonPrevolutions[this.species.speciesId],
);
const preFormIndex: number =
preSpecies?.forms.find(f => f.formKey === pre.preFormKey)?.formIndex ?? 0;
const conditionText: string = pre.description;
options.push({
label: pre.preFormKey
? (preSpecies ?? this.species).getFormNameToDisplay(preFormIndex, true)
: (preSpecies ?? this.species).getExpandedSpeciesName(),
handler: () => {
this.previousSpecies.push(this.species);
this.previousStarterAttributes.push({ ...this.savedStarterAttributes });
const newSpecies = allSpecies.find(
species => species.speciesId === pokemonPrevolutions[pre.speciesId],
);
// Attempts to find the formIndex of the prevolved species
const newFormKey = pre.preFormKey
? pre.preFormKey
: this.species.forms.length > 0
? this.species.forms[this.formIndex].formKey
: "";
const matchingForm = newSpecies?.forms.find(form => form.formKey === newFormKey);
const newFormIndex = matchingForm ? matchingForm.formIndex : 0;
this.starterAttributes.form = newFormIndex;
this.savedStarterAttributes.form = newFormIndex;
this.moveInfoOverlay.clear();
this.clearText();
ui.setMode(Mode.POKEDEX_PAGE, newSpecies, this.savedStarterAttributes);
return true;
},
onHover: () => this.showText(conditionText),
});
});
}
if (this.evolutions.length > 0) {
options.push({
label: i18next.t("pokedexUiHandler:evolutions"),
style: TextStyle.MONEY_WINDOW,
skip: true,
handler: () => false,
});
this.evolutions.map(evo => {
const evoSpecies = allSpecies.find(species => species.speciesId === evo.speciesId);
const isCaughtEvo = !!this.isCaught(evoSpecies);
// Attempts to find the formIndex of the evolved species
const newFormKey = evo.evoFormKey
? evo.evoFormKey
: this.species.forms.length > 0
? this.species.forms[this.formIndex].formKey
: "";
const matchingForm = evoSpecies?.forms.find(form => form.formKey === newFormKey);
const newFormIndex = matchingForm ? matchingForm.formIndex : 0;
const isFormCaughtEvo = this.isFormCaught(evoSpecies, newFormIndex);
const conditionText: string = evo.description;
options.push({
label: evo.evoFormKey
? (evoSpecies ?? this.species).getFormNameToDisplay(newFormIndex, true)
: (evoSpecies ?? this.species).getExpandedSpeciesName(),
style: isCaughtEvo && isFormCaughtEvo ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT,
handler: () => {
this.previousSpecies.push(this.species);
this.previousStarterAttributes.push({ ...this.savedStarterAttributes });
this.starterAttributes.form = newFormIndex;
this.savedStarterAttributes.form = newFormIndex;
this.moveInfoOverlay.clear();
this.clearText();
ui.setMode(Mode.POKEDEX_PAGE, evoSpecies, this.savedStarterAttributes);
return true;
},
onHover: () => this.showText(conditionText),
});
});
}
if (this.battleForms.length > 0) {
options.push({
label: i18next.t("pokedexUiHandler:forms"),
style: TextStyle.MONEY_WINDOW,
skip: true,
handler: () => false,
});
this.battleForms.map(bf => {
const matchingForm = this.species?.forms.find(form => form.formKey === bf.formKey);
const newFormIndex = matchingForm ? matchingForm.formIndex : 0;
let conditionText = "";
if (bf.trigger) {
conditionText = bf.trigger.description;
} else {
conditionText = "";
}
let label: string = this.species.getFormNameToDisplay(newFormIndex);
if (label === "") {
label = this.species.name;
}
const isFormCaught = this.isFormCaught(this.species, newFormIndex);
if (conditionText) {
options.push({
label: label,
style: isFormCaught ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT,
handler: () => {
this.previousSpecies.push(this.species);
this.previousStarterAttributes.push({ ...this.savedStarterAttributes });
const newSpecies = this.species;
const newFormIndex = this.species.forms.find(f => f.formKey === bf.formKey)?.formIndex;
this.starterAttributes.form = newFormIndex;
this.savedStarterAttributes.form = newFormIndex;
this.moveInfoOverlay.clear();
this.clearText();
ui.setMode(
Mode.POKEDEX_PAGE,
newSpecies,
this.savedStarterAttributes,
this.filteredIndices,
);
return true;
},
onHover: () => this.showText(conditionText),
});
}
});
}
options.push({
label: i18next.t("menu:cancel"),
handler: () => {
this.moveInfoOverlay.clear();
this.clearText();
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
return true;
},
onHover: () => this.moveInfoOverlay.clear(),
});
ui.setModeWithoutClear(Mode.OPTION_SELECT, {
options: options,
supportHover: true,
maxOptions: 8,
yOffset: 19,
});
this.blockInput = false;
});
});
success = true;
}
break;
case MenuOptions.TOGGLE_IVS:
if (!isStarterCaught) {
error = true;
} else {
this.toggleStatsMode();
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
success = true;
}
break;
case MenuOptions.NATURES:
if (!isStarterCaught) {
error = true;
} else {
this.blockInput = true;
ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => {
ui.showText(i18next.t("pokedexUiHandler:showNature"), null, () => {
const natures = globalScene.gameData.getNaturesForAttr(this.speciesStarterDexEntry?.natureAttr);
ui.setModeWithoutClear(Mode.OPTION_SELECT, {
options: natures
.map((n: Nature, _i: number) => {
const option: OptionSelectItem = {
label: getNatureName(n, true, true, true, globalScene.uiTheme),
handler: () => {
return false;
},
};
return option;
})
.concat({
label: i18next.t("menu:cancel"),
handler: () => {
this.clearText();
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
this.blockInput = false;
return true;
},
}),
maxOptions: 8,
yOffset: 19,
});
});
});
success = true;
}
break;
}
} else {
const props = globalScene.gameData.getSpeciesDexAttrProps(
this.species,
this.getCurrentDexProps(this.species.speciesId),
);
switch (button) {
case Button.CYCLE_SHINY:
if (this.canCycleShiny) {
if (!starterAttributes.shiny) {
// Change to shiny, we need to get the proper default variant
const newVariant = starterAttributes.variant ? (starterAttributes.variant as Variant) : 0;
this.setSpeciesDetails(this.species, {
shiny: true,
variant: newVariant,
});
globalScene.playSound("se/sparkle");
starterAttributes.shiny = true;
this.savedStarterAttributes.shiny = starterAttributes.shiny;
} else {
let newVariant = props.variant;
do {
newVariant = (newVariant + 1) % 3;
if (newVariant === 0) {
if (this.isCaught() & DexAttr.DEFAULT_VARIANT) {
break;
}
} else if (newVariant === 1) {
if (this.isCaught() & DexAttr.VARIANT_2) {
break;
}
} else {
if (this.isCaught() & DexAttr.VARIANT_3) {
break;
}
}
} while (newVariant !== props.variant);
starterAttributes.variant = newVariant; // store the selected variant
this.savedStarterAttributes.variant = starterAttributes.variant;
if (this.isCaught() & DexAttr.NON_SHINY && newVariant <= props.variant) {
this.setSpeciesDetails(this.species, {
shiny: false,
variant: 0,
});
success = true;
starterAttributes.shiny = false;
this.savedStarterAttributes.shiny = starterAttributes.shiny;
} else {
this.setSpeciesDetails(this.species, {
variant: newVariant as Variant,
});
success = true;
}
}
}
break;
case Button.CYCLE_FORM:
if (this.canCycleForm) {
const formCount = this.species.forms.length;
let newFormIndex = this.formIndex;
do {
newFormIndex = (newFormIndex + 1) % formCount;
if (this.species.forms[newFormIndex].isStarterSelectable || globalScene.dexForDevs) {
// TODO: are those bangs correct?
break;
}
} while (newFormIndex !== props.formIndex || this.species.forms[newFormIndex].isUnobtainable);
starterAttributes.form = newFormIndex; // store the selected form
this.savedStarterAttributes.form = starterAttributes.form;
this.formIndex = newFormIndex;
// Some forms are tied to the gender and should change accordingly
let newFemale = props.female;
if (this.isFormGender) {
newFemale = !props.female;
}
starterAttributes.female = newFemale;
this.savedStarterAttributes.female = starterAttributes.female;
this.starterSetup();
this.setSpeciesDetails(this.species, {
formIndex: newFormIndex,
female: newFemale,
});
success = this.setCursor(this.cursor);
}
break;
case Button.CYCLE_GENDER:
if (this.canCycleGender) {
starterAttributes.female = !props.female;
this.savedStarterAttributes.female = starterAttributes.female;
let newFormIndex = this.formIndex;
// Some forms are tied to the gender and should change accordingly
if (this.isFormGender) {
newFormIndex = this.formIndex === 0 ? 1 : 0;
}
starterAttributes.form = newFormIndex; // store the selected form
this.savedStarterAttributes.form = starterAttributes.form;
this.formIndex = newFormIndex;
this.starterSetup();
this.setSpeciesDetails(this.species, {
female: !props.female,
formIndex: newFormIndex,
});
success = true;
}
break;
case Button.STATS:
if (!isCaught || !isFormCaught || !this.canUseCandies) {
error = true;
} else {
const ui = this.getUi();
ui.showText("");
const options: any[] = []; // TODO: add proper type
const passiveAttr = starterData.passiveAttr;
const candyCount = starterData.candyCount;
if (!(passiveAttr & PassiveAttr.UNLOCKED)) {
const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.starterId]);
options.push({
label: `x${passiveCost} ${i18next.t("pokedexUiHandler:unlockPassive")} (${allAbilities[this.passive].name})`,
handler: () => {
if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= passiveCost) {
starterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED;
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= passiveCost;
}
this.pokemonCandyCountText.setText(`x${starterData.candyCount}`);
globalScene.gameData.saveSystem().then(success => {
if (!success) {
return globalScene.reset(true);
}
});
this.setSpeciesDetails(this.species);
globalScene.playSound("se/buy");
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
return true;
}
return false;
},
style: this.isPassiveAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT,
item: "candy",
itemArgs: this.isPassiveAvailable() ? starterColors[this.starterId] : ["808080", "808080"],
});
}
// Reduce cost option
const valueReduction = starterData.valueReduction;
if (valueReduction < valueReductionMax) {
const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.starterId])[valueReduction];
options.push({
label: `x${reductionCost} ${i18next.t("pokedexUiHandler:reduceCost")}`,
handler: () => {
if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= reductionCost) {
starterData.valueReduction++;
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= reductionCost;
}
this.pokemonCandyCountText.setText(`x${starterData.candyCount}`);
globalScene.gameData.saveSystem().then(success => {
if (!success) {
return globalScene.reset(true);
}
});
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
globalScene.playSound("se/buy");
return true;
}
return false;
},
style: this.isValueReductionAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT,
item: "candy",
itemArgs: this.isValueReductionAvailable() ? starterColors[this.starterId] : ["808080", "808080"],
});
}
// Same species egg menu option.
const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId]);
options.push({
label: `x${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`,
handler: () => {
if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost) {
if (globalScene.gameData.eggs.length >= 99 && !Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) {
// Egg list full, show error message at the top of the screen and abort
this.showText(
i18next.t("egg:tooManyEggs"),
undefined,
() => this.showText("", 0, () => (this.tutorialActive = false)),
2000,
false,
undefined,
true,
);
return false;
}
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= sameSpeciesEggCost;
}
this.pokemonCandyCountText.setText(`x${starterData.candyCount}`);
const egg = new Egg({
scene: globalScene,
species: this.starterId,
sourceType: EggSourceType.SAME_SPECIES_EGG,
});
egg.addEggToGameData();
globalScene.gameData.saveSystem().then(success => {
if (!success) {
return globalScene.reset(true);
}
});
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
globalScene.playSound("se/buy");
return true;
}
return false;
},
style: this.isSameSpeciesEggAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT,
item: "candy",
itemArgs: this.isSameSpeciesEggAvailable() ? starterColors[this.starterId] : ["808080", "808080"],
});
options.push({
label: i18next.t("menu:cancel"),
handler: () => {
ui.setMode(Mode.POKEDEX_PAGE, "refresh");
return true;
},
});
ui.setModeWithoutClear(Mode.OPTION_SELECT, {
options: options,
yOffset: 47,
});
success = true;
}
break;
case Button.CYCLE_ABILITY:
this.showBackSprite = !this.showBackSprite;
if (this.showBackSprite) {
this.showBackSpriteLabel.setText(i18next.t("pokedexUiHandler:showFrontSprite"));
} else {
this.showBackSpriteLabel.setText(i18next.t("pokedexUiHandler:showBackSprite"));
}
this.setSpeciesDetails(this.species, {}, true);
success = true;
break;
case Button.UP:
if (this.cursor) {
success = this.setCursor(this.cursor - 1);
} else {
success = this.setCursor(this.menuOptions.length - 1);
}
break;
case Button.DOWN:
if (this.cursor + 1 < this.menuOptions.length) {
success = this.setCursor(this.cursor + 1);
} else {
success = this.setCursor(0);
}
break;
case Button.LEFT:
if (this.filteredIndices && this.filteredIndices.length <= 1) {
ui.playError();
this.blockInput = false;
return true;
}
this.blockInput = true;
ui.setModeWithoutClear(Mode.OPTION_SELECT).then(() => {
// Always go back to first selection after scrolling around
if (this.previousSpecies.length === 0) {
this.previousSpecies.push(this.species);
this.previousStarterAttributes.push({ ...this.savedStarterAttributes });
}
let newSpecies: PokemonSpecies;
if (this.filteredIndices) {
const index = this.filteredIndices.findIndex(id => id === this.species.speciesId);
const newIndex = index <= 0 ? this.filteredIndices.length - 1 : index - 1;
newSpecies = getPokemonSpecies(this.filteredIndices[newIndex]);
} else {
const index = allSpecies.findIndex(species => species.speciesId === this.species.speciesId);
const newIndex = index <= 0 ? allSpecies.length - 1 : index - 1;
newSpecies = allSpecies[newIndex];
}
const matchingForm = newSpecies?.forms.find(
form => form.formKey === this.species?.forms[this.formIndex]?.formKey,
);
const newFormIndex = matchingForm ? matchingForm.formIndex : 0;
this.starterAttributes.form = newFormIndex;
this.savedStarterAttributes.form = newFormIndex;
this.moveInfoOverlay.clear();
this.clearText();
ui.setModeForceTransition(
Mode.POKEDEX_PAGE,
newSpecies,
this.savedStarterAttributes,
this.filteredIndices,
);
});
this.blockInput = false;
break;
case Button.RIGHT:
if (this.filteredIndices && this.filteredIndices.length <= 1) {
ui.playError();
this.blockInput = false;
return true;
}
ui.setModeWithoutClear(Mode.OPTION_SELECT).then(() => {
// Always go back to first selection after scrolling around
if (this.previousSpecies.length === 0) {
this.previousSpecies.push(this.species);
this.previousStarterAttributes.push({ ...this.savedStarterAttributes });
}
let newSpecies: PokemonSpecies;
if (this.filteredIndices) {
const index = this.filteredIndices.findIndex(id => id === this.species.speciesId);
const newIndex = index >= this.filteredIndices.length - 1 ? 0 : index + 1;
newSpecies = getPokemonSpecies(this.filteredIndices[newIndex]);
} else {
const index = allSpecies.findIndex(species => species.speciesId === this.species.speciesId);
const newIndex = index >= allSpecies.length - 1 ? 0 : index + 1;
newSpecies = allSpecies[newIndex];
}
const matchingForm = newSpecies?.forms.find(
form => form.formKey === this.species?.forms[this.formIndex]?.formKey,
);
const newFormIndex = matchingForm ? matchingForm.formIndex : 0;
this.starterAttributes.form = newFormIndex;
this.savedStarterAttributes.form = newFormIndex;
this.moveInfoOverlay.clear();
this.clearText();
ui.setModeForceTransition(
Mode.POKEDEX_PAGE,
newSpecies,
this.savedStarterAttributes,
this.filteredIndices,
);
});
break;
}
}
}
if (success) {
ui.playSelect();
} else if (error) {
ui.playError();
}
return success || error;
}
updateButtonIcon(iconSetting, gamepadType, iconElement, controlLabel): void {
// biome-ignore lint/suspicious/noImplicitAnyLet: TODO
let iconPath;
// touch controls cannot be rebound as is, and are just emulating a keyboard event.
// Additionally, since keyboard controls can be rebound (and will be displayed when they are), we need to have special handling for the touch controls
if (gamepadType === "touch") {
gamepadType = "keyboard";
switch (iconSetting) {
case SettingKeyboard.Button_Cycle_Shiny:
iconPath = "R.png";
break;
case SettingKeyboard.Button_Cycle_Form:
iconPath = "F.png";
break;
case SettingKeyboard.Button_Cycle_Gender:
iconPath = "G.png";
break;
case SettingKeyboard.Button_Cycle_Ability:
iconPath = "E.png";
break;
default:
break;
}
} else {
iconPath = globalScene.inputController?.getIconForLatestInputRecorded(iconSetting);
}
iconElement.setTexture(gamepadType, iconPath);
iconElement.setPosition(this.instructionRowX, this.instructionRowY);
controlLabel.setPosition(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY);
iconElement.setVisible(true);
controlLabel.setVisible(true);
this.instructionsContainer.add([iconElement, controlLabel]);
this.instructionRowY += 8;
if (this.instructionRowY >= 24) {
this.instructionRowY = 8;
this.instructionRowX += 50;
}
}
updateInstructions(): void {
this.instructionRowX = 0;
this.instructionRowY = 0;
this.filterInstructionRowX = 0;
this.filterInstructionRowY = 0;
this.hideInstructions();
this.instructionsContainer.removeAll();
this.filterInstructionsContainer.removeAll();
// biome-ignore lint/suspicious/noImplicitAnyLet: TODO
let gamepadType;
if (globalScene.inputMethod === "gamepad") {
gamepadType = globalScene.inputController.getConfig(
globalScene.inputController.selectedDevice[Device.GAMEPAD],
).padType;
} else {
gamepadType = globalScene.inputMethod;
}
if (!gamepadType) {
return;
}
const isFormCaught = this.isFormCaught();
if (this.isCaught()) {
if (isFormCaught) {
if (this.canUseCandies) {
this.updateButtonIcon(
SettingKeyboard.Button_Stats,
gamepadType,
this.candyUpgradeIconElement,
this.candyUpgradeLabel,
);
}
if (this.canCycleShiny) {
this.updateButtonIcon(
SettingKeyboard.Button_Cycle_Shiny,
gamepadType,
this.shinyIconElement,
this.shinyLabel,
);
}
if (this.canCycleGender) {
this.updateButtonIcon(
SettingKeyboard.Button_Cycle_Gender,
gamepadType,
this.genderIconElement,
this.genderLabel,
);
}
} else {
// Making space for "Uncaught" text
this.instructionRowY += 8;
}
if (this.canCycleForm) {
this.updateButtonIcon(SettingKeyboard.Button_Cycle_Form, gamepadType, this.formIconElement, this.formLabel);
}
}
}
setCursor(cursor: number): boolean {
const ret = super.setCursor(cursor);
if (!this.cursorObj) {
this.cursorObj = globalScene.add.image(0, 0, "cursor");
this.cursorObj.setOrigin(0, 0);
this.menuContainer.add(this.cursorObj);
}
this.cursorObj.setScale(this.scale * 6);
this.cursorObj.setPositionRelative(this.menuBg, 7, 6 + (18 + this.cursor * 96) * this.scale);
const ui = this.getUi();
if ((this.isCaught() && this.isFormCaught()) || this.isSeen()) {
ui.showText(this.menuDescriptions[cursor]);
} else {
ui.showText("");
}
return ret;
}
getFriendship(_speciesId: number) {
let currentFriendship = globalScene.gameData.starterData[this.starterId].friendship;
if (!currentFriendship || currentFriendship === undefined) {
currentFriendship = 0;
}
const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[this.starterId]);
return { currentFriendship, friendshipCap };
}
/**
* Determines if a passive upgrade is available for the current species
* @returns true if the user has enough candies and a passive has not been unlocked already
*/
isPassiveAvailable(): boolean {
// Get this species ID's starter data
const starterData = globalScene.gameData.starterData[this.starterId];
return (
starterData.candyCount >= getPassiveCandyCount(speciesStarterCosts[this.starterId]) &&
!(starterData.passiveAttr & PassiveAttr.UNLOCKED)
);
}
/**
* Determines if a value reduction upgrade is available for the current species
* @returns true if the user has enough candies and all value reductions have not been unlocked already
*/
isValueReductionAvailable(): boolean {
// Get this species ID's starter data
const starterData = globalScene.gameData.starterData[this.starterId];
return (
starterData.candyCount >=
getValueReductionCandyCounts(speciesStarterCosts[this.starterId])[starterData.valueReduction] &&
starterData.valueReduction < valueReductionMax
);
}
/**
* Determines if an same species egg can be bought for the current species
* @returns true if the user has enough candies
*/
isSameSpeciesEggAvailable(): boolean {
// Get this species ID's starter data
const starterData = globalScene.gameData.starterData[this.starterId];
return starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId]);
}
setSpecies() {
const species = this.species;
const starterAttributes: StarterAttributes | null = species ? { ...this.starterAttributes } : null;
if (!species && globalScene.ui.getTooltip().visible) {
globalScene.ui.hideTooltip();
}
if (this.statsMode) {
if (this.isCaught()) {
this.statsContainer.setVisible(true);
this.showStats();
} else {
this.statsContainer.setVisible(false);
//@ts-ignore
this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. what. how? huh?
}
}
if (species && (this.isSeen() || this.isCaught())) {
this.pokemonNumberText.setText(padInt(species.speciesId, 4));
if (this.isCaught()) {
const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
// Set default attributes if for some reason starterAttributes does not exist or attributes missing
const props: StarterAttributes = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
if (starterAttributes?.variant && !Number.isNaN(starterAttributes.variant)) {
if (props.shiny) {
props.variant = starterAttributes.variant as Variant;
}
}
props.form = starterAttributes?.form ?? props.form;
props.female = starterAttributes?.female ?? props.female;
this.setSpeciesDetails(species, {
shiny: props.shiny,
formIndex: props.form,
female: props.female,
variant: props.variant ?? 0,
});
} else {
this.pokemonGrowthRateText.setText("");
this.pokemonGrowthRateLabelText.setVisible(false);
this.type1Icon.setVisible(true);
this.type2Icon.setVisible(true);
this.pokemonLuckLabelText.setVisible(false);
this.pokemonLuckText.setVisible(false);
for (const icon of this.pokemonShinyIcons) {
icon.setVisible(false);
}
this.pokemonUncaughtText.setVisible(true);
this.pokemonCaughtHatchedContainer.setVisible(true);
this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false);
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true);
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
this.setSpeciesDetails(species, {
shiny: props.shiny,
formIndex: props.formIndex,
female: props.female,
variant: props.variant,
});
this.pokemonSprite.setTint(0x808080);
}
} else {
this.pokemonNumberText.setText(species ? padInt(species.speciesId, 4) : "");
this.pokemonNameText.setText(species ? "???" : "");
this.pokemonGrowthRateText.setText("");
this.pokemonGrowthRateLabelText.setVisible(false);
this.type1Icon.setVisible(false);
this.type2Icon.setVisible(false);
this.pokemonLuckLabelText.setVisible(false);
this.pokemonLuckText.setVisible(false);
for (const icon of this.pokemonShinyIcons) {
icon.setVisible(false);
}
this.pokemonUncaughtText.setVisible(!!species);
this.pokemonCaughtHatchedContainer.setVisible(false);
this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false);
this.setSpeciesDetails(species!, {
// TODO: is this bang correct?
shiny: false,
formIndex: 0,
female: false,
variant: 0,
});
this.pokemonSprite.setTint(0x000000);
}
}
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, forceUpdate?: boolean): void {
let { shiny, formIndex, female, variant } = options;
const oldProps = species ? this.starterAttributes : null;
// We will only update the sprite if there is a change to form, shiny/variant
// or gender for species with gender sprite differences
const shouldUpdateSprite =
(species?.genderDiffs && !isNullOrUndefined(female)) ||
!isNullOrUndefined(formIndex) ||
!isNullOrUndefined(shiny) ||
!isNullOrUndefined(variant) ||
forceUpdate;
if (this.activeTooltip === "CANDY") {
if (this.species && this.pokemonCandyContainer.visible) {
const { currentFriendship, friendshipCap } = this.getFriendship(this.species.speciesId);
globalScene.ui.editTooltip("", `${currentFriendship}/${friendshipCap}`);
} else {
globalScene.ui.hideTooltip();
}
}
if (species?.forms?.find(f => f.formKey === "female")) {
if (female !== undefined) {
formIndex = female ? 1 : 0;
} else if (formIndex !== undefined) {
female = formIndex === 1;
}
}
if (species) {
// Only assign shiny, female, and variant if they are undefined
if (shiny === undefined) {
shiny = oldProps?.shiny ?? false;
}
if (female === undefined) {
female = oldProps?.female ?? false;
}
if (variant === undefined) {
variant = oldProps?.variant ?? 0;
}
if (formIndex === undefined) {
formIndex = oldProps?.form ?? 0;
}
}
this.pokemonSprite.setVisible(false);
if (this.assetLoadCancelled) {
this.assetLoadCancelled.value = true;
this.assetLoadCancelled = null;
}
if (species) {
const caughtAttr = this.isCaught(species);
if (!caughtAttr) {
const props = this.starterAttributes;
if (shiny === undefined || shiny !== props.shiny) {
shiny = props.shiny;
}
if (formIndex === undefined || formIndex !== props.form) {
formIndex = props.form;
}
if (female === undefined || female !== props.female) {
female = props.female;
}
if (variant === undefined || variant !== props.variant) {
variant = props.variant;
}
}
const isFormCaught = this.isFormCaught();
const isFormSeen = this.isSeen();
this.shinyOverlay.setVisible(shiny ?? false); // TODO: is false the correct default?
this.pokemonNumberText.setColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, false));
this.pokemonNumberText.setShadowColor(
this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, true),
);
const assetLoadCancelled = new BooleanHolder(false);
this.assetLoadCancelled = assetLoadCancelled;
if (shouldUpdateSprite) {
const back = !!this.showBackSprite;
species.loadAssets(female!, formIndex, shiny, variant as Variant, true, back).then(() => {
// TODO: is this bang correct?
if (assetLoadCancelled.value) {
return;
}
this.assetLoadCancelled = null;
this.speciesLoaded.set(species.speciesId, true);
this.pokemonSprite.play(species.getSpriteKey(female!, formIndex, shiny, variant, back)); // TODO: is this bang correct?
this.pokemonSprite.setPipelineData("shiny", shiny);
this.pokemonSprite.setPipelineData("variant", variant);
this.pokemonSprite.setPipelineData(
"spriteKey",
species.getSpriteKey(female!, formIndex, shiny, variant, back),
); // TODO: is this bang correct?
this.pokemonSprite.setVisible(!this.statsMode);
});
} else {
this.pokemonSprite.setVisible(!this.statsMode);
}
const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY);
const isShinyCaught = !!(caughtAttr & DexAttr.SHINY);
const caughtVariants = [DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3].filter(
v => caughtAttr & v,
);
this.canCycleShiny = (isNonShinyCaught && isShinyCaught) || (isShinyCaught && caughtVariants.length > 1);
const isMaleCaught = !!(caughtAttr & DexAttr.MALE);
const isFemaleCaught = !!(caughtAttr & DexAttr.FEMALE);
this.canCycleGender = isMaleCaught && isFemaleCaught;
// If the dev option for the dex is selected, all forms can be cycled through
this.canCycleForm = globalScene.dexForDevs
? species.forms.length > 1
: species.forms.filter(f => f.isStarterSelectable).filter(f => f).length > 1;
if (caughtAttr && species.malePercent !== null) {
const gender = !female ? Gender.MALE : Gender.FEMALE;
this.pokemonGenderText.setText(getGenderSymbol(gender));
this.pokemonGenderText.setColor(getGenderColor(gender));
this.pokemonGenderText.setShadowColor(getGenderColor(gender, true));
} else {
this.pokemonGenderText.setText("");
}
// Setting the name
if (isFormCaught || isFormSeen) {
this.pokemonNameText.setText(species.name);
} else {
this.pokemonNameText.setText(species ? "???" : "");
}
// Setting tint of the sprite
if (isFormCaught) {
this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => {
const crier =
this.species.forms && this.species.forms.length > 0
? this.species.forms[formIndex ?? this.formIndex]
: this.species;
crier.cry();
});
this.pokemonSprite.clearTint();
} else if (isFormSeen) {
this.pokemonSprite.setTint(0x808080);
} else {
this.pokemonSprite.setTint(0);
}
// Setting luck text and sparks
if (isFormCaught) {
const luck = globalScene.gameData.getDexAttrLuck(this.isCaught());
this.pokemonLuckText.setVisible(!!luck);
this.pokemonLuckText.setText(luck.toString());
this.pokemonLuckText.setTint(getVariantTint(Math.min(luck - 1, 2) as Variant));
this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible);
} else {
this.pokemonLuckText.setVisible(false);
this.pokemonLuckLabelText.setVisible(false);
}
// Setting growth rate text
if (isFormCaught) {
let growthReadable = toReadableString(GrowthRate[species.growthRate]);
const growthAux = growthReadable.replace(" ", "_");
if (i18next.exists("growth:" + growthAux)) {
growthReadable = i18next.t(("growth:" + growthAux) as any);
}
this.pokemonGrowthRateText.setText(growthReadable);
this.pokemonGrowthRateText.setColor(getGrowthRateColor(species.growthRate));
this.pokemonGrowthRateText.setShadowColor(getGrowthRateColor(species.growthRate, true));
this.pokemonGrowthRateLabelText.setVisible(true);
} else {
this.pokemonGrowthRateText.setText("");
this.pokemonGrowthRateLabelText.setVisible(false);
}
// Caught and hatched
if (isFormCaught) {
const colorScheme = starterColors[this.starterId];
this.pokemonUncaughtText.setVisible(false);
this.pokemonCaughtCountText.setText(`${this.speciesStarterDexEntry?.caughtCount}`);
if (species.speciesId === Species.MANAPHY || species.speciesId === Species.PHIONE) {
this.pokemonHatchedIcon.setFrame("manaphy");
} else {
this.pokemonHatchedIcon.setFrame(getEggTierForSpecies(species));
}
this.pokemonHatchedCountText.setText(`${this.speciesStarterDexEntry?.hatchedCount}`);
const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
const variant = defaultProps.variant;
for (let v = 0; v < 3; v++) {
const icon = this.pokemonShinyIcons[v];
if (v < this.availableVariants) {
if (!this.unlockedVariants[v]) {
icon.setTint(0x000000);
} else if (shiny && v === variant) {
const tint = getVariantTint(v as Variant);
icon.setTint(tint);
} else {
icon.setTint(0x808080);
}
icon.setVisible(true);
} else {
icon.setVisible(false);
}
}
this.pokemonCaughtHatchedContainer.setVisible(true);
this.pokemonCaughtHatchedContainer.setY(25);
this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0])));
this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1])));
this.pokemonCandyCountText.setText(
`x${species.speciesId === Species.PIKACHU ? 0 : globalScene.gameData.starterData[this.starterId].candyCount}`,
);
this.pokemonCandyContainer.setVisible(true);
if (pokemonPrevolutions.hasOwnProperty(species.speciesId)) {
this.pokemonHatchedIcon.setVisible(false);
this.pokemonHatchedCountText.setVisible(false);
this.pokemonFormText.setY(36);
} else {
this.pokemonHatchedIcon.setVisible(true);
this.pokemonHatchedCountText.setVisible(true);
this.pokemonFormText.setY(42);
const { currentFriendship, friendshipCap } = this.getFriendship(this.species.speciesId);
const candyCropY = 16 - 16 * (currentFriendship / friendshipCap);
this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY);
this.pokemonCandyContainer.on("pointerover", () => {
globalScene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true);
this.activeTooltip = "CANDY";
});
this.pokemonCandyContainer.on("pointerout", () => {
globalScene.ui.hideTooltip();
this.activeTooltip = undefined;
});
}
} else {
this.pokemonUncaughtText.setVisible(true);
this.pokemonCaughtHatchedContainer.setVisible(false);
this.pokemonCandyContainer.setVisible(false);
for (const icon of this.pokemonShinyIcons) {
icon.setVisible(false);
}
}
// Setting type icons and form text
if (isFormCaught || isFormSeen) {
const speciesForm = getPokemonSpeciesForm(species.speciesId, formIndex!); // TODO: is the bang correct?
this.setTypeIcons(speciesForm.type1, speciesForm.type2);
// TODO: change this once forms are refactored
if (normalForm.includes(species.speciesId) && !formIndex) {
this.pokemonFormText.setText("");
} else {
this.pokemonFormText.setText(species.getFormNameToDisplay(formIndex));
}
this.pokemonFormText.setVisible(true);
if (!isFormCaught) {
this.pokemonFormText.setY(18);
}
} else {
this.setTypeIcons(null, null);
this.pokemonFormText.setText("");
this.pokemonFormText.setVisible(false);
}
} else {
this.shinyOverlay.setVisible(false);
this.pokemonNumberText.setColor(this.getTextColor(TextStyle.SUMMARY));
this.pokemonNumberText.setShadowColor(this.getTextColor(TextStyle.SUMMARY, true));
this.pokemonGenderText.setText("");
this.setTypeIcons(null, null);
}
this.updateInstructions();
}
setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void {
if (type1 !== null) {
this.type1Icon.setVisible(true);
this.type1Icon.setFrame(PokemonType[type1].toLowerCase());
} else {
this.type1Icon.setVisible(false);
}
if (type2 !== null) {
this.type2Icon.setVisible(true);
this.type2Icon.setFrame(PokemonType[type2].toLowerCase());
} else {
this.type2Icon.setVisible(false);
}
}
/**
* Creates a temporary dex attr props that will be used to display the correct shiny, variant, and form based on this.starterAttributes
*
* @param speciesId the id of the species to get props for
* @returns the dex props
*/
getCurrentDexProps(speciesId: number): bigint {
let props = 0n;
const species = allSpecies.find(sp => sp.speciesId === speciesId);
const caughtAttr =
globalScene.gameData.dexData[speciesId].caughtAttr &
globalScene.gameData.dexData[this.getStarterSpeciesId(speciesId)].caughtAttr &
(species?.getFullUnlocksData() ?? 0n);
/* this checks the gender of the pokemon; this works by checking a) that the starter preferences for the species exist, and if so, is it female. If so, it'll add DexAttr.FEMALE to our temp props
* It then checks b) if the caughtAttr for the pokemon is female and NOT male - this means that the ONLY gender we've gotten is female, and we need to add DexAttr.FEMALE to our temp props
* If neither of these pass, we add DexAttr.MALE to our temp props
*/
if (this.starterAttributes?.female || ((caughtAttr & DexAttr.FEMALE) > 0n && (caughtAttr & DexAttr.MALE) === 0n)) {
props += DexAttr.FEMALE;
} else {
props += DexAttr.MALE;
}
/* This part is very similar to above, but instead of for gender, it checks for shiny within starter preferences.
* If they're not there, it enables shiny state by default if any shiny was caught
*/
if (
this.starterAttributes?.shiny ||
((caughtAttr & DexAttr.SHINY) > 0n && this.starterAttributes?.shiny !== false)
) {
props += DexAttr.SHINY;
if (this.starterAttributes?.variant !== undefined) {
props += BigInt(Math.pow(2, this.starterAttributes?.variant)) * DexAttr.DEFAULT_VARIANT;
} else {
/* This calculates the correct variant if there's no starter preferences for it.
* This gets the highest tier variant that you've caught and adds it to the temp props
*/
if ((caughtAttr & DexAttr.VARIANT_3) > 0) {
props += DexAttr.VARIANT_3;
} else if ((caughtAttr & DexAttr.VARIANT_2) > 0) {
props += DexAttr.VARIANT_2;
} else {
props += DexAttr.DEFAULT_VARIANT;
}
}
} else {
props += DexAttr.NON_SHINY;
props += DexAttr.DEFAULT_VARIANT; // we add the default variant here because non shiny versions are listed as default variant
}
if (this.starterAttributes?.form) {
// this checks for the form of the pokemon
props += BigInt(Math.pow(2, this.starterAttributes?.form)) * DexAttr.DEFAULT_FORM;
} else {
// Get the first unlocked form
props += globalScene.gameData.getFormAttr(globalScene.gameData.getFormIndex(caughtAttr));
}
return props;
}
toggleStatsMode(on?: boolean): void {
if (on === undefined) {
on = !this.statsMode;
}
if (on) {
this.showStats();
this.statsMode = true;
this.pokemonSprite.setVisible(false);
} else {
this.statsMode = false;
this.statsContainer.setVisible(false);
this.pokemonSprite.setVisible(true);
//@ts-ignore
this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. !?!?
}
}
showStats(): void {
if (!this.speciesStarterDexEntry) {
return;
}
this.statsContainer.setVisible(true);
this.statsContainer.updateIvs(this.speciesStarterDexEntry.ivs);
}
clearText() {
this.starterSelectMessageBoxContainer.setVisible(false);
super.clearText();
}
hideInstructions(): void {
this.candyUpgradeIconElement.setVisible(false);
this.candyUpgradeLabel.setVisible(false);
this.shinyIconElement.setVisible(false);
this.shinyLabel.setVisible(false);
this.formIconElement.setVisible(false);
this.formLabel.setVisible(false);
this.genderIconElement.setVisible(false);
this.genderLabel.setVisible(false);
this.variantIconElement.setVisible(false);
this.variantLabel.setVisible(false);
}
clear(): void {
super.clear();
this.cursor = -1;
this.hideInstructions();
this.activeTooltip = undefined;
globalScene.ui.hideTooltip();
this.starterSelectContainer.setVisible(false);
this.blockInput = false;
this.showBackSprite = false;
this.showBackSpriteLabel.setText(i18next.t("pokedexUiHandler:showBackSprite"));
if (this.statsMode) {
this.toggleStatsMode(false);
}
}
checkIconId(
icon: Phaser.GameObjects.Sprite,
species: PokemonSpecies,
female: boolean,
formIndex: number,
shiny: boolean,
variant: number,
) {
if (icon.frame.name !== species.getIconId(female, formIndex, shiny, variant)) {
console.log(
`${species.name}'s icon ${icon.frame.name} does not match getIconId with female: ${female}, formIndex: ${formIndex}, shiny: ${shiny}, variant: ${variant}`,
);
icon.setTexture(species.getIconAtlasKey(formIndex, false, variant));
icon.setFrame(species.getIconId(female, formIndex, false, variant));
}
}
}