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 = new Map(); 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)); } } }