diff --git a/src/@types/save-data.ts b/src/@types/save-data.ts index ae359c20949..fe7b9e88121 100644 --- a/src/@types/save-data.ts +++ b/src/@types/save-data.ts @@ -91,11 +91,11 @@ export interface StarterMoveData { [key: number]: StarterMoveset | StarterFormMoveData; } -export interface StarterAttributes { +export interface StarterPreferences { nature?: number; - ability?: number; + abilityIndex?: number; variant?: number; - form?: number; + formIndex?: number; female?: boolean; shiny?: boolean; favorite?: boolean; diff --git a/src/constants.ts b/src/constants.ts index 17cf08aa7e2..977334ed424 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -107,3 +107,8 @@ export const FAKE_TITLE_LOGO_CHANCE = 10000; * Using rare candies will never increase friendship beyond this value. */ export const RARE_CANDY_FRIENDSHIP_CAP = 200; + +/** + * The maximum number of cost reduction upgrades that can be bought with candy. + */ +export const VALUE_REDUCTION_MAX = 2; diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 3ffa7482706..4a521f8e9b9 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1991,6 +1991,29 @@ export class GameData { return ret; } + getSpeciesDefaultDexAttrProps(species: PokemonSpecies): DexAttrProps { + const dexAttr = this.dexData[species.speciesId].caughtAttr; + // Default shiny is true if caught + const shiny = !!(dexAttr & DexAttr.SHINY); + // Default is female only for species where malePercent is not null but 0 + const female = species.malePercent === 0; + // Default is the highest variant + let variant: Variant = 0; + if (dexAttr & DexAttr.VARIANT_3) { + variant = 2; + } else if (dexAttr & DexAttr.VARIANT_2) { + variant = 1; + } + const formIndex = 0; + + return { + shiny, + female, + variant, + formIndex, + }; + } + getSpeciesDexAttrProps(_species: PokemonSpecies, dexAttr: bigint): DexAttrProps { const shiny = !(dexAttr & DexAttr.NON_SHINY); const female = !(dexAttr & DexAttr.MALE); diff --git a/src/ui/containers/starter-container.ts b/src/ui/containers/starter-container.ts index f81ac8e5bfb..c6b265532ed 100644 --- a/src/ui/containers/starter-container.ts +++ b/src/ui/containers/starter-container.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; import type { PokemonSpecies } from "#data/pokemon-species"; import { TextStyle } from "#enums/text-style"; +import type { DexAttrProps } from "#types/save-data"; import { addTextObject } from "#ui/text"; export class StarterContainer extends Phaser.GameObjects.Container { @@ -19,10 +20,9 @@ export class StarterContainer extends Phaser.GameObjects.Container { constructor(species: PokemonSpecies) { super(globalScene, 0, 0); - this.species = species; - const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true); const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); + this.setSpecies(species, defaultProps); // starter passive bg const starterPassiveBg = globalScene.add.image(2, 5, "passive_bg"); @@ -32,21 +32,6 @@ export class StarterContainer extends Phaser.GameObjects.Container { this.add(starterPassiveBg); this.starterPassiveBgs = starterPassiveBg; - // icon - this.icon = globalScene.add.sprite( - -2, - 2, - species.getIconAtlasKey(defaultProps.formIndex, defaultProps.shiny, defaultProps.variant), - ); - this.icon.setScale(0.5); - this.icon.setOrigin(0, 0); - this.icon.setFrame( - species.getIconId(defaultProps.female, defaultProps.formIndex, defaultProps.shiny, defaultProps.variant), - ); - this.checkIconId(defaultProps.female, defaultProps.formIndex, defaultProps.shiny, defaultProps.variant); - this.icon.setTint(0); - this.add(this.icon); - // shiny icons for (let i = 0; i < 3; i++) { const shinyIcon = globalScene.add.image(i * -3 + 12, 2, "shiny_star_small"); @@ -108,6 +93,39 @@ export class StarterContainer extends Phaser.GameObjects.Container { this.candyUpgradeOverlayIcon = candyUpgradeOverlayIcon; } + setSpecies(species: PokemonSpecies, props: DexAttrProps) { + this.species = species; + + const { shiny, formIndex, female, variant } = props; + + if (this.icon) { + this.remove(this.icon); + this.icon.destroy(); // Properly removes the sprite from memory + } + + // icon + this.icon = globalScene.add.sprite(-2, 2, species.getIconAtlasKey(formIndex, shiny, variant)); + this.icon.setScale(0.5); + this.icon.setOrigin(0, 0); + this.icon.setFrame(species.getIconId(female, formIndex, shiny, variant)); + this.checkIconId(female, formIndex, shiny, variant); + this.icon.setTint(0); + this.add(this.icon); + this.icon.setBelow(this.label); + + [ + this.hiddenAbilityIcon, + this.favoriteIcon, + this.classicWinIcon, + this.candyUpgradeIcon, + this.candyUpgradeOverlayIcon, + ].forEach(icon => { + if (icon) { + this.bringToTop(icon); + } + }); + } + checkIconId(female, formIndex, shiny, variant) { if (this.icon.frame.name !== this.species.getIconId(female, formIndex, shiny, variant)) { console.log(`${this.species.name}'s variant icon does not exist. Replacing with default.`); diff --git a/src/ui/containers/starter-summary.ts b/src/ui/containers/starter-summary.ts new file mode 100644 index 00000000000..58ebe0352d6 --- /dev/null +++ b/src/ui/containers/starter-summary.ts @@ -0,0 +1,866 @@ +import { globalScene } from "#app/global-scene"; +import { starterColors } from "#app/global-vars/starter-colors"; +import { speciesEggMoves } from "#balance/egg-moves"; +import { pokemonPrevolutions } from "#balance/pokemon-evolutions"; +import { allAbilities, allMoves } from "#data/data-lists"; +import { getEggTierForSpecies } from "#data/egg"; +import { GrowthRate, getGrowthRateColor } from "#data/exp"; +import { Gender, getGenderColor, getGenderSymbol } from "#data/gender"; +import { getNatureName } from "#data/nature"; +import type { PokemonSpecies } from "#data/pokemon-species"; +import { Challenges } from "#enums/challenges"; +import type { Nature } from "#enums/nature"; +import { Passive } from "#enums/passive"; +import { PokemonType } from "#enums/pokemon-type"; +import { SpeciesId } from "#enums/species-id"; +import { TextStyle } from "#enums/text-style"; +import { getVariantIcon, getVariantTint, type Variant } from "#sprites/variant"; +import { achvs } from "#system/achv"; +import type { Ability } from "#types/ability-types"; +import type { StarterMoveset, StarterPreferences } from "#types/save-data"; +import { BooleanHolder, getLocalizedSpriteKey, padInt, rgbHexToRgba } from "#utils/common"; +import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; +import { toCamelCase, toTitleCase } from "#utils/strings"; +import { argbFromRgba } from "@material/material-color-utilities"; +import i18next from "i18next"; +import type { GameObjects } from "phaser"; +import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; +import { addBBCodeTextObject, addTextObject, getTextColor } from "../text"; +import { + getDexAttrFromPreferences, + getFriendship, + getSpeciesData, + getStarterSelectTextSettings, + type SpeciesDetails, +} from "../utils/starter-select-ui-utils"; +import { StatsContainer } from "./stats-container"; + +export class StarterSummary extends Phaser.GameObjects.Container { + private pokemonSprite: Phaser.GameObjects.Sprite; + private pokemonNumberText: Phaser.GameObjects.Text; + private shinyOverlay: Phaser.GameObjects.Image; + 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 pokemonAbilityLabelText: Phaser.GameObjects.Text; + private pokemonAbilityText: Phaser.GameObjects.Text; + private pokemonPassiveLabelText: Phaser.GameObjects.Text; + private pokemonPassiveText: Phaser.GameObjects.Text; + private pokemonNatureLabelText: Phaser.GameObjects.Text; + private pokemonNatureText: BBCodeText; + private pokemonMovesContainer: Phaser.GameObjects.Container; + private pokemonMoveContainers: Phaser.GameObjects.Container[]; + private pokemonMoveBgs: Phaser.GameObjects.NineSlice[]; + private pokemonMoveLabels: Phaser.GameObjects.Text[]; + private pokemonAdditionalMoveCountLabel: Phaser.GameObjects.Text; + private eggMovesLabel: Phaser.GameObjects.Text; + private pokemonEggMovesContainer: Phaser.GameObjects.Container; + private pokemonEggMoveContainers: Phaser.GameObjects.Container[]; + private pokemonEggMoveBgs: Phaser.GameObjects.NineSlice[]; + private pokemonEggMoveLabels: 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 pokemonShinyIcon: Phaser.GameObjects.Sprite; + private pokemonPassiveDisabledIcon: Phaser.GameObjects.Sprite; + private pokemonPassiveLockedIcon: Phaser.GameObjects.Sprite; + private teraIcon: Phaser.GameObjects.Sprite; + + // Whether the tera type icon should be displayed + private allowTera: boolean; + + // Container for ivs, whether they should be shown + private statsContainer: StatsContainer; + private statsMode = false; + + private assetLoadCancelled: BooleanHolder | null; + + // Which of the tooltips is displayed (on mouse hover) + private activeTooltip: "ABILITY" | "PASSIVE" | "CANDY" | undefined; + + // Container for type, growth rate, luck + private pokemonPermanentInfoContainer: GameObjects.Container; + // Container for numbers of caught pokémon, eggs + private pokemonStatisticsContainer: GameObjects.Container; + // Container for everything that's a preference (abilities, nature, form...) + private pokemonPreferencesContainer: GameObjects.Container; + + private speciesId: SpeciesId; + + constructor(x: number, y: number) { + super(globalScene, x, y); + + 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.shinyOverlay = globalScene.add + .image(6, 111, getLocalizedSpriteKey("summary_dexnb_label_overlay_shiny")) + .setOrigin(0, 1) + .setVisible(false); // Pixel text 'No' shiny + + this.pokemonNumberText = addTextObject(17, 1, "0000", TextStyle.SUMMARY_DEX_NUM).setOrigin(0); + + this.pokemonNameText = addTextObject(6, 112, "", TextStyle.SUMMARY).setOrigin(0); + + this.pokemonUncaughtText = addTextObject( + 6, + 127, + i18next.t("starterSelectUiHandler:uncaught"), + TextStyle.SUMMARY_ALT, + { fontSize: "56px" }, + ).setOrigin(0); + + this.pokemonMoveContainers = []; + this.pokemonMoveBgs = []; + this.pokemonMoveLabels = []; + + this.pokemonEggMoveContainers = []; + this.pokemonEggMoveBgs = []; + this.pokemonEggMoveLabels = []; + + this.pokemonPreferencesContainer = this.setupPokemonPreferencesContainer(); + this.pokemonPermanentInfoContainer = this.setupPokemonPermanentInfoContainer(); + this.pokemonStatisticsContainer = this.setupPokemonStatisticsContainer(); + + for (let m = 0; m < 4; m++) { + const moveContainer = globalScene.add.container(0, 14 * m); + + const moveBg = globalScene.add.nineslice(0, 0, "type_bgs", "unknown", 92, 14, 2, 2, 2, 2); + moveBg.setOrigin(1, 0); + + const moveLabel = addTextObject(-moveBg.width / 2, 0, "-", TextStyle.MOVE_LABEL); + moveLabel.setOrigin(0.5, 0); + + this.pokemonMoveBgs.push(moveBg); + this.pokemonMoveLabels.push(moveLabel); + + moveContainer.add([moveBg, moveLabel]); + + this.pokemonMoveContainers.push(moveContainer); + this.pokemonMovesContainer.add(moveContainer); + } + + this.pokemonAdditionalMoveCountLabel = addTextObject( + -this.pokemonMoveBgs[0].width / 2, + 56, + "(+0)", + TextStyle.MOVE_LABEL, + ).setOrigin(0.5, 0); + + this.pokemonMovesContainer.add(this.pokemonAdditionalMoveCountLabel); + + this.pokemonEggMovesContainer = globalScene.add.container(102, 85).setScale(0.375); + + this.eggMovesLabel = addTextObject( + -46, + 0, + i18next.t("starterSelectUiHandler:eggMoves"), + TextStyle.WINDOW_ALT, + ).setOrigin(0.5, 0); + + this.pokemonEggMovesContainer.add(this.eggMovesLabel); + + for (let m = 0; m < 4; m++) { + const eggMoveContainer = globalScene.add.container(0, 16 + 14 * m); + + const eggMoveBg = globalScene.add.nineslice(0, 0, "type_bgs", "unknown", 92, 14, 2, 2, 2, 2); + eggMoveBg.setOrigin(1, 0); + + const eggMoveLabel = addTextObject(-eggMoveBg.width / 2, 0, "???", TextStyle.MOVE_LABEL); + eggMoveLabel.setOrigin(0.5, 0); + + this.pokemonEggMoveBgs.push(eggMoveBg); + this.pokemonEggMoveLabels.push(eggMoveLabel); + + eggMoveContainer.add([eggMoveBg, eggMoveLabel]); + + this.pokemonEggMoveContainers.push(eggMoveContainer); + + this.pokemonEggMovesContainer.add(eggMoveContainer); + } + + this.statsContainer = new StatsContainer(6, 16).setVisible(false); + + globalScene.add.existing(this.statsContainer); + + this.add([ + this.pokemonSprite, + this.shinyOverlay, + this.pokemonNumberText, + this.pokemonNameText, + this.pokemonUncaughtText, + this.pokemonPreferencesContainer, + this.pokemonPermanentInfoContainer, + this.pokemonStatisticsContainer, + this.pokemonMovesContainer, + this.pokemonEggMovesContainer, + this.statsContainer, + ]); + + this.allowTera = globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id); + } + + setupPokemonPreferencesContainer(): GameObjects.Container { + const pokemonPreferencesContainer = globalScene.add.container(0, 0); + + const textSettings = getStarterSelectTextSettings(); + + // The position should be set per language + const starterInfoXPos = textSettings?.starterInfoXPos || 31; + const starterInfoYOffset = textSettings?.starterInfoYOffset || 0; + + // The font size should be set per language + const starterInfoTextSize = textSettings?.starterInfoTextSize || 56; + + this.pokemonGenderText = addTextObject(96, 112, "", TextStyle.SUMMARY_ALT).setOrigin(0); + + this.pokemonFormText = addTextObject(6, 42, "Form", TextStyle.WINDOW_ALT, { + fontSize: "42px", + }).setOrigin(0); + + this.pokemonAbilityLabelText = addTextObject( + 6, + 127 + starterInfoYOffset, + i18next.t("starterSelectUiHandler:ability"), + TextStyle.SUMMARY_ALT, + { fontSize: starterInfoTextSize }, + ).setOrigin(0); + + this.pokemonAbilityText = addTextObject(starterInfoXPos, 127 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { + fontSize: starterInfoTextSize, + }) + .setOrigin(0) + .setInteractive(new Phaser.Geom.Rectangle(0, 0, 250, 55), Phaser.Geom.Rectangle.Contains); + + this.pokemonPassiveLabelText = addTextObject( + 6, + 136 + starterInfoYOffset, + i18next.t("starterSelectUiHandler:passive"), + TextStyle.SUMMARY_ALT, + { fontSize: starterInfoTextSize }, + ).setOrigin(0); + + this.pokemonPassiveText = addTextObject(starterInfoXPos, 136 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { + fontSize: starterInfoTextSize, + }) + .setOrigin(0) + .setInteractive(new Phaser.Geom.Rectangle(0, 0, 250, 55), Phaser.Geom.Rectangle.Contains); + + this.pokemonPassiveDisabledIcon = globalScene.add + .sprite(starterInfoXPos, 137 + starterInfoYOffset, "icon_stop") + .setOrigin(0, 0.5) + .setScale(0.35) + .setVisible(false); + + this.pokemonPassiveLockedIcon = globalScene.add + .sprite(starterInfoXPos, 137 + starterInfoYOffset, "icon_lock") + .setOrigin(0, 0.5) + .setScale(0.42, 0.38) + .setVisible(false); + + this.pokemonNatureLabelText = addTextObject( + 6, + 145 + starterInfoYOffset, + i18next.t("starterSelectUiHandler:nature"), + TextStyle.SUMMARY_ALT, + { fontSize: starterInfoTextSize }, + ).setOrigin(0); + + this.pokemonNatureText = addBBCodeTextObject(starterInfoXPos, 145 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { + fontSize: starterInfoTextSize, + }).setOrigin(0); + + this.pokemonShinyIcon = globalScene.add.sprite(12, 0, "shiny_icons").setScale(0.5); + + this.teraIcon = globalScene.add.sprite(85, 63, "button_tera").setName("terastallize-icon").setFrame("fire"); + + pokemonPreferencesContainer.add([ + this.pokemonGenderText, + this.pokemonFormText, + this.pokemonAbilityLabelText, + this.pokemonAbilityText, + this.pokemonPassiveLabelText, + this.pokemonPassiveText, + this.pokemonPassiveDisabledIcon, + this.pokemonPassiveLockedIcon, + this.pokemonNatureLabelText, + this.pokemonNatureText, + this.pokemonShinyIcon, + this.teraIcon, + ]); + + return pokemonPreferencesContainer; + } + + setupPokemonPermanentInfoContainer(): GameObjects.Container { + const pokemonPermanentInfoContainer = globalScene.add.container(0, 0); + + this.type1Icon = globalScene.add.sprite(8, 98, getLocalizedSpriteKey("types")).setScale(0.5).setOrigin(0); + this.type2Icon = globalScene.add.sprite(26, 98, getLocalizedSpriteKey("types")).setScale(0.5).setOrigin(0); + + this.pokemonGrowthRateLabelText = addTextObject( + 8, + 106, + i18next.t("starterSelectUiHandler:growthRate"), + TextStyle.SUMMARY_ALT, + { fontSize: "36px" }, + ).setOrigin(0); + + this.pokemonGrowthRateText = addTextObject(34, 106, "", TextStyle.GROWTH_RATE_TYPE, { fontSize: "36px" }).setOrigin( + 0, + ); + + this.pokemonLuckLabelText = addTextObject(8, 89, i18next.t("common:luckIndicator"), TextStyle.WINDOW_ALT, { + fontSize: "56px", + }).setOrigin(0); + + this.pokemonLuckText = addTextObject( + 8 + this.pokemonLuckLabelText.displayWidth + 2, + 89, + "0", + TextStyle.LUCK_VALUE, + { fontSize: "56px" }, + ).setOrigin(0); + + pokemonPermanentInfoContainer.add([ + this.type1Icon, + this.type2Icon, + this.pokemonGrowthRateLabelText, + this.pokemonGrowthRateText, + this.pokemonLuckLabelText, + this.pokemonLuckText, + ]); + + return pokemonPermanentInfoContainer; + } + + setupPokemonStatisticsContainer(): GameObjects.Container { + const pokemonStatisticsContainer = globalScene.add.container(0, 0); + + // Candy icon and count + this.pokemonCandyContainer = globalScene.add + .container(4.5, 18) + .setInteractive(new Phaser.Geom.Rectangle(0, 0, 30, 20), Phaser.Geom.Rectangle.Contains); + this.pokemonCandyIcon = globalScene.add.sprite(0, 0, "candy").setScale(0.5).setOrigin(0); + this.pokemonCandyOverlayIcon = globalScene.add.sprite(0, 0, "candy_overlay").setScale(0.5).setOrigin(0); + this.pokemonCandyDarknessOverlay = globalScene.add + .sprite(0, 0, "candy") + .setScale(0.5) + .setOrigin(0) + .setTint(0x000000) + .setAlpha(0.5); + + this.pokemonCandyCountText = addTextObject(9.5, 0, "x0", TextStyle.WINDOW_ALT, { fontSize: "56px" }).setOrigin(0); + this.pokemonCandyContainer.add([ + this.pokemonCandyIcon, + this.pokemonCandyOverlayIcon, + this.pokemonCandyDarknessOverlay, + this.pokemonCandyCountText, + ]); + + this.pokemonCaughtHatchedContainer = globalScene.add.container(2, 25).setScale(0.5); + + const pokemonCaughtIcon = globalScene.add.sprite(1, 0, "items", "pb").setOrigin(0).setScale(0.75); + + this.pokemonCaughtCountText = addTextObject(24, 4, "0", TextStyle.SUMMARY_ALT).setOrigin(0); + this.pokemonHatchedIcon = globalScene.add.sprite(1, 14, "egg_icons").setOrigin(0.15, 0.2).setScale(0.8); + this.pokemonHatchedCountText = addTextObject(24, 19, "0", TextStyle.SUMMARY_ALT).setOrigin(0); + this.pokemonMovesContainer = globalScene.add.container(102, 16).setScale(0.375); + this.pokemonCaughtHatchedContainer.add([ + pokemonCaughtIcon, + this.pokemonCaughtCountText, + this.pokemonHatchedIcon, + this.pokemonHatchedCountText, + ]); + + pokemonStatisticsContainer.add([this.pokemonCandyContainer, this.pokemonCaughtHatchedContainer]); + + return pokemonStatisticsContainer; + } + + applyChallengeVisibility() { + const notFreshStart = !globalScene.gameMode.hasChallenge(Challenges.FRESH_START); + + for (const container of this.pokemonEggMoveContainers) { + container.setVisible(notFreshStart); + } + this.eggMovesLabel.setVisible(notFreshStart); + // This is not enough, we need individual checks in setStarterSpecies too! :) + this.pokemonPassiveDisabledIcon.setVisible(notFreshStart); + this.pokemonPassiveLabelText.setVisible(notFreshStart); + this.pokemonPassiveLockedIcon.setVisible(notFreshStart); + this.pokemonPassiveText.setVisible(notFreshStart); + } + + updateName(name: string) { + this.pokemonNameText.setText(name); + } + + updateCandyCount(count: number) { + this.pokemonCandyCountText.setText(`×${count}`); + } + + setNameAndNumber(species: PokemonSpecies, starterPreferences: StarterPreferences) { + this.pokemonNumberText.setText(padInt(species.speciesId, 4)); + + if (starterPreferences?.nickname) { + const name = decodeURIComponent(escape(atob(starterPreferences.nickname))); + this.pokemonNameText.setText(name); + } else { + this.pokemonNameText.setText(species.name); + } + } + + setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void { + if (type1 !== null) { + this.type1Icon.setVisible(true).setFrame(PokemonType[type1].toLowerCase()); + } else { + this.type1Icon.setVisible(false); + } + if (type2 !== null) { + this.type2Icon.setVisible(true).setFrame(PokemonType[type2].toLowerCase()); + } else { + this.type2Icon.setVisible(false); + } + } + + setShinyIcon(shiny = true, variant: Variant = 0) { + const tint = getVariantTint(variant); + this.pokemonShinyIcon.setFrame(getVariantIcon(variant)).setTint(tint).setVisible(shiny); + } + + setNoSpecies() { + if (globalScene.ui.getTooltip().visible) { + globalScene.ui.hideTooltip(); + } + + this.pokemonAbilityText.off("pointerover"); + this.pokemonPassiveText.off("pointerover"); + + if (this.statsMode) { + this.statsContainer.setVisible(false); + } + + this.cleanStarterSprite(); + } + + setSpecies(species: PokemonSpecies, starterPreferences: StarterPreferences) { + this.speciesId = species.speciesId; + + // First, we load from the dex entry to get defaults + const { dexEntry } = getSpeciesData(species.speciesId); + + this.pokemonAbilityText.off("pointerover"); + this.pokemonPassiveText.off("pointerover"); + + // Hiding ivs container if the species is not caught + if (this.statsMode) { + if (dexEntry?.caughtAttr) { + this.statsContainer.setVisible(true); + this.showStats(); + } else { + this.statsContainer.setVisible(false); + } + } + + if (dexEntry.caughtAttr) { + this.setNameAndNumber(species, starterPreferences); + + const colorScheme = starterColors[species.speciesId]; + + this.pokemonUncaughtText.setVisible(false); + this.pokemonPermanentInfoContainer.setVisible(true); + this.pokemonStatisticsContainer.setVisible(true); + + const luck = globalScene.gameData.getDexAttrLuck(dexEntry.caughtAttr); + this.pokemonLuckText + .setVisible(!!luck) + .setText(luck.toString()) + .setTint(getVariantTint(Math.min(luck - 1, 2) as Variant)); + this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible); + + //Growth translate + let growthReadable = toTitleCase(GrowthRate[species.growthRate]); + const growthAux = toCamelCase(growthReadable); + if (i18next.exists("growth:" + growthAux)) { + growthReadable = i18next.t(("growth:" + growthAux) as any); + } + this.pokemonGrowthRateText + .setText(growthReadable) + .setColor(getGrowthRateColor(species.growthRate)) + .setShadowColor(getGrowthRateColor(species.growthRate, true)); + this.pokemonCaughtCountText.setText(`${dexEntry.caughtCount}`); + if (species.speciesId === SpeciesId.MANAPHY || species.speciesId === SpeciesId.PHIONE) { + this.pokemonHatchedIcon.setFrame("manaphy"); + } else { + this.pokemonHatchedIcon.setFrame(getEggTierForSpecies(species)); + } + this.pokemonHatchedCountText.setText(`${dexEntry.hatchedCount}`); + + const defaultDexAttr = getDexAttrFromPreferences(species.speciesId, starterPreferences); + + if (pokemonPrevolutions.hasOwnProperty(species.speciesId)) { + this.pokemonCaughtHatchedContainer.setVisible(false); + this.pokemonShinyIcon.setY(104); + this.pokemonFormText.setY(25); + } else { + this.pokemonCaughtHatchedContainer.setVisible(true); + this.pokemonShinyIcon.setY(86); + this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0]))); + this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1]))); + this.pokemonCandyCountText.setText(`×${globalScene.gameData.starterData[species.speciesId].candyCount}`); + this.pokemonFormText.setY(42); + this.pokemonHatchedIcon.setVisible(true); + this.pokemonHatchedCountText.setVisible(true); + + const { currentFriendship, friendshipCap } = getFriendship(species.speciesId); + const candyCropY = 16 - 16 * (currentFriendship / friendshipCap); + this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY); + + this.pokemonCandyContainer + .setVisible(true) + .on("pointerover", () => { + globalScene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true); + this.activeTooltip = "CANDY"; + }) + .on("pointerout", () => { + globalScene.ui.hideTooltip(); + this.activeTooltip = undefined; + }); + } + + const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); + props.formIndex = starterPreferences?.formIndex ?? props.formIndex; + const speciesForm = getPokemonSpeciesForm(species.speciesId, props.formIndex); + this.setTypeIcons(speciesForm.type1, speciesForm.type2); + + this.pokemonSprite.clearTint(); + } else if (dexEntry.seenAttr) { + this.cleanStarterSprite(species, true); + + const props = globalScene.gameData.getSpeciesDefaultDexAttrProps(species); + + const formIndex = props.formIndex; + const female = props.female; + const shiny = props.shiny; + const variant = props.variant; + + this.updateSprite(species, female, formIndex, shiny, variant); + this.pokemonSprite.setVisible(true); + this.pokemonSprite.setTint(0x808080); + } else { + this.cleanStarterSprite(species); + + const props = globalScene.gameData.getSpeciesDefaultDexAttrProps(species); + + const formIndex = props.formIndex; + const female = props.female; + const shiny = props.shiny; + const variant = props.variant; + + this.updateSprite(species, female, formIndex, shiny, variant); + this.pokemonSprite.setVisible(true); + this.pokemonSprite.setTint(0x000000); + } + } + + cleanStarterSprite(species?: PokemonSpecies, isSeen = false) { + if (isSeen && species) { + this.setNameAndNumber(species, {}); + } else { + this.pokemonNumberText.setText(padInt(0, 4)); + this.pokemonNameText.setText(species ? "???" : ""); + } + + this.pokemonSprite.setVisible(!!species); + this.pokemonUncaughtText.setVisible(!!species); + + this.pokemonPermanentInfoContainer.setVisible(false); + this.pokemonStatisticsContainer.setVisible(false); + this.resetSpeciesDetails(); + } + + resetSpeciesDetails() { + globalScene.ui.hideTooltip(); + + this.pokemonPreferencesContainer.setVisible(false); + + if (this.assetLoadCancelled) { + this.assetLoadCancelled.value = true; + this.assetLoadCancelled = null; + } + + this.shinyOverlay.setVisible(false); + this.pokemonNumberText + .setColor(getTextColor(TextStyle.SUMMARY)) + .setShadowColor(getTextColor(TextStyle.SUMMARY, true)); + + for (let m = 0; m < 4; m++) { + this.pokemonMoveContainers[m].setVisible(false); + } + this.pokemonEggMovesContainer.setVisible(false); + this.pokemonAdditionalMoveCountLabel.setVisible(false); + } + + setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void { + // Here we pass some options to override everything else + let { shiny, formIndex, female, variant, abilityIndex, natureIndex, teraType } = options; + console.log("OPTIONS", options); + + // 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 && female != null) || formIndex != null || shiny != null || variant != null; + + this.updateCandyTooltip(); + + this.pokemonSprite.setVisible(false); + this.teraIcon.setVisible(false); + + if (this.assetLoadCancelled) { + this.assetLoadCancelled.value = true; + this.assetLoadCancelled = null; + } + + this.pokemonPreferencesContainer.setVisible(true); + + this.shinyOverlay.setVisible(shiny ?? false); // TODO: is false the correct default? + this.pokemonNumberText.setColor( + getTextColor(shiny ? TextStyle.SUMMARY_DEX_NUM_GOLD : TextStyle.SUMMARY_DEX_NUM, false), + ); + this.pokemonNumberText.setShadowColor( + getTextColor(shiny ? TextStyle.SUMMARY_DEX_NUM_GOLD : TextStyle.SUMMARY_DEX_NUM, true), + ); + + this.setShinyIcon(shiny, variant); + + const assetLoadCancelled = new BooleanHolder(false); + this.assetLoadCancelled = assetLoadCancelled; + + // TODO: should this line be here, or in .updateSprite? + female ??= false; + if (shouldUpdateSprite) { + this.updateSprite(species, female, formIndex, shiny, variant); + } else { + this.pokemonSprite.setVisible(!this.statsMode); + } + + // Set the gender text + if (species.malePercent !== null) { + const gender = !female ? Gender.MALE : Gender.FEMALE; + this.pokemonGenderText + .setText(getGenderSymbol(gender)) + .setColor(getGenderColor(gender)) + .setShadowColor(getGenderColor(gender, true)); + } else { + this.pokemonGenderText.setText(""); + } + + // Update ability text + let ability: Ability; + if (species.forms?.length > 1) { + ability = allAbilities[species.forms[formIndex ?? 0].getAbility(abilityIndex!)]; + } else { + ability = allAbilities[species.getAbility(abilityIndex!)]; // TODO: is this bang correct? + } + + const isHidden = abilityIndex === (species.ability2 ? 2 : 1); + this.pokemonAbilityText + .setText(ability.name) + .setColor(getTextColor(!isHidden ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GOLD)) + .setShadowColor(getTextColor(!isHidden ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GOLD, true)); + + if (this.pokemonAbilityText.visible) { + if (this.activeTooltip === "ABILITY") { + globalScene.ui.editTooltip(`${ability.name}`, `${ability.description}`); + } + + this.pokemonAbilityText.on("pointerover", () => { + globalScene.ui.showTooltip(`${ability.name}`, `${ability.description}`, true); + this.activeTooltip = "ABILITY"; + }); + this.pokemonAbilityText.on("pointerout", () => { + globalScene.ui.hideTooltip(); + this.activeTooltip = undefined; + }); + } + + this.updatePassiveDisplay(species.speciesId, formIndex); + + // Update nature text + this.pokemonNatureText.setText(getNatureName(natureIndex as unknown as Nature, true, true, false)); + + // Update form text + const speciesForm = getPokemonSpeciesForm(species.speciesId, formIndex!); // TODO: is the bang correct? + const formText = species.getFormNameToDisplay(formIndex); + this.pokemonFormText.setText(formText); + + // Update type icons + this.setTypeIcons(speciesForm.type1, speciesForm.type2); + + // Update tera icon + const newTeraType = teraType ?? speciesForm.type1; + this.teraIcon.setFrame(PokemonType[newTeraType].toLowerCase()); + this.teraIcon.setVisible(!this.statsMode && this.allowTera); + } + + showStats(): void { + const { dexEntry } = getSpeciesData(this.speciesId); + this.statsContainer.setVisible(true); + this.statsContainer.updateIvs(dexEntry.ivs); + } + + updatePassiveDisplay(speciesId: SpeciesId, formIndex = 0) { + this.pokemonPassiveLabelText.setVisible(false); + this.pokemonPassiveText.setVisible(false); + this.pokemonPassiveDisabledIcon.setVisible(false); + this.pokemonPassiveLockedIcon.setVisible(false); + + const isFreshStartChallenge = globalScene.gameMode.hasChallenge(Challenges.FRESH_START); + + const { starterDataEntry } = getSpeciesData(speciesId); + + const passiveAttr = starterDataEntry.passiveAttr; + const passiveAbility = allAbilities[getPokemonSpecies(speciesId).getPassiveAbility(formIndex)]; + + if (passiveAbility) { + const isUnlocked = !!(passiveAttr & Passive.UNLOCKED); + const isEnabled = !!(passiveAttr & Passive.ENABLED); + + const textStyle = isUnlocked && isEnabled ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GRAY; + const textAlpha = isUnlocked && isEnabled ? 1 : 0.5; + + this.pokemonPassiveLabelText + .setVisible(!isFreshStartChallenge) + .setColor(getTextColor(TextStyle.SUMMARY_ALT)) + .setShadowColor(getTextColor(TextStyle.SUMMARY_ALT, true)); + this.pokemonPassiveText + .setVisible(!isFreshStartChallenge) + .setText(passiveAbility.name) + .setColor(getTextColor(textStyle)) + .setAlpha(textAlpha) + .setShadowColor(getTextColor(textStyle, true)); + + if (this.activeTooltip === "PASSIVE") { + globalScene.ui.editTooltip(`${passiveAbility.name}`, `${passiveAbility.description}`); + } + + if (this.pokemonPassiveText.visible) { + this.pokemonPassiveText.on("pointerover", () => { + globalScene.ui.showTooltip(`${passiveAbility.name}`, `${passiveAbility.description}`, true); + this.activeTooltip = "PASSIVE"; + }); + this.pokemonPassiveText.on("pointerout", () => { + globalScene.ui.hideTooltip(); + this.activeTooltip = undefined; + }); + } + + const iconPosition = { + x: this.pokemonPassiveText.x + this.pokemonPassiveText.displayWidth + 1, + y: this.pokemonPassiveText.y + this.pokemonPassiveText.displayHeight / 2, + }; + this.pokemonPassiveDisabledIcon + .setVisible(isUnlocked && !isEnabled && !isFreshStartChallenge) + .setPosition(iconPosition.x, iconPosition.y); + this.pokemonPassiveLockedIcon + .setVisible(!isUnlocked && !isFreshStartChallenge) + .setPosition(iconPosition.x, iconPosition.y); + } else if (this.activeTooltip === "PASSIVE") { + // No passive and passive tooltip is active > hide it + globalScene.ui.hideTooltip(); + } + } + + updateSprite( + species: PokemonSpecies, + female: boolean, + formIndex?: number | undefined, + shiny?: boolean, + variant?: Variant | undefined, + ) { + species.loadAssets(female, formIndex, shiny, variant, true).then(() => { + if (this.assetLoadCancelled?.value) { + return; + } + this.assetLoadCancelled = null; + // Note: Bangs are correct due to `female ??= false` above + this.pokemonSprite + .play(species.getSpriteKey(female!, formIndex, shiny, variant)) + .setPipelineData("shiny", shiny) + .setPipelineData("variant", variant) + .setPipelineData("spriteKey", species.getSpriteKey(female!, formIndex, shiny, variant)) + .setVisible(!this.statsMode); + }); + } + + updateCandyTooltip() { + if (this.activeTooltip === "CANDY") { + if (this.speciesId && this.pokemonCandyContainer.visible) { + const { currentFriendship, friendshipCap } = getFriendship(this.speciesId); + globalScene.ui.editTooltip("", `${currentFriendship}/${friendshipCap}`); + } else { + globalScene.ui.hideTooltip(); + } + } + } + + updateMoveset(starterMoveset: StarterMoveset, totalMoves: number) { + for (let m = 0; m < 4; m++) { + const move = m < starterMoveset.length ? allMoves[starterMoveset[m]] : null; + this.pokemonMoveBgs[m].setFrame(PokemonType[move ? move.type : PokemonType.UNKNOWN].toString().toLowerCase()); + this.pokemonMoveLabels[m].setText(move ? move.name : "-"); + this.pokemonMoveContainers[m].setVisible(!!move); + } + + this.pokemonAdditionalMoveCountLabel.setText(`(+${Math.max(totalMoves - 4, 0)})`).setVisible(totalMoves > 4); + } + + updateEggMoves(eggMoves: number) { + for (let em = 0; em < 4; em++) { + const eggMove = allMoves[speciesEggMoves[this.speciesId][em]]; + const eggMoveUnlocked = eggMove && eggMoves & (1 << em); + this.pokemonEggMoveBgs[em].setFrame( + PokemonType[eggMove ? eggMove.type : PokemonType.UNKNOWN].toString().toLowerCase(), + ); + this.pokemonEggMoveLabels[em].setText(eggMove && eggMoveUnlocked ? eggMove.name : "???"); + } + + this.pokemonEggMovesContainer.setVisible(true); + } + + showIvs() { + this.showStats(); + this.statsMode = true; + this.pokemonSprite.setVisible(false); + this.teraIcon.setVisible(false); + } + + hideIvs(caught = true) { + this.statsMode = false; + this.statsContainer.setVisible(false); + this.pokemonSprite.setVisible(caught); + this.teraIcon.setVisible(this.allowTera); + } + + clear() { + globalScene.ui.hideTooltip(); + this.activeTooltip = undefined; + } +} diff --git a/src/ui/handlers/pokedex-page-ui-handler.ts b/src/ui/handlers/pokedex-page-ui-handler.ts index 684ead7d45a..d86f745898c 100644 --- a/src/ui/handlers/pokedex-page-ui-handler.ts +++ b/src/ui/handlers/pokedex-page-ui-handler.ts @@ -1,3 +1,4 @@ +import { VALUE_REDUCTION_MAX } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { starterColors } from "#app/global-vars/starter-colors"; import Overrides from "#app/overrides"; @@ -45,7 +46,7 @@ import type { Variant } from "#sprites/variant"; import { getVariantIcon, getVariantTint } from "#sprites/variant"; import { SettingKeyboard } from "#system/settings-keyboard"; import type { DexEntry } from "#types/dex-data"; -import type { StarterAttributes } from "#types/save-data"; +import type { StarterPreferences } from "#types/save-data"; import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { BaseStatsOverlay } from "#ui/base-stats-overlay"; import { MessageUiHandler } from "#ui/message-ui-handler"; @@ -61,6 +62,11 @@ import { toCamelCase, toTitleCase } from "#utils/strings"; import { argbFromRgba } from "@material/material-color-utilities"; import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText"; +import { + isPassiveAvailable, + isSameSpeciesEggAvailable, + isValueReductionAvailable, +} from "../utils/starter-select-ui-utils"; interface LanguageSetting { starterInfoTextSize: string; @@ -148,8 +154,6 @@ const languageSettings: { [key: string]: LanguageSetting } = { }, }; -const valueReductionMax = 2; - // Position of UI elements const speciesContainerX = 109; @@ -261,11 +265,11 @@ export class PokedexPageUiHandler extends MessageUiHandler { private instructionRowY = 0; private instructionRowTextOffset = 9; - private starterAttributes: StarterAttributes; - private savedStarterAttributes: StarterAttributes; + private starterPreferences: StarterPreferences; + private savedStarterPreferences: StarterPreferences; private previousSpecies: PokemonSpecies[]; - private previousStarterAttributes: StarterAttributes[]; + private previousStarterPreferences: StarterPreferences[]; protected blockInput = false; protected blockInputOverlay = false; @@ -702,7 +706,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer); this.previousSpecies = []; - this.previousStarterAttributes = []; + this.previousStarterPreferences = []; } show(args: any[]): boolean { @@ -715,13 +719,13 @@ export class PokedexPageUiHandler extends MessageUiHandler { return false; } this.species = args[0]; - this.savedStarterAttributes = args[1] ?? { + this.savedStarterPreferences = args[1] ?? { shiny: false, female: true, variant: 0, form: 0, }; - this.formIndex = this.savedStarterAttributes.form ?? 0; + this.formIndex = this.savedStarterPreferences.formIndex ?? 0; this.filteredIndices = args[2] ?? null; this.starterSetup(); @@ -737,7 +741,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.starterSelectContainer.setVisible(true); this.getUi().bringToTop(this.starterSelectContainer); - this.starterAttributes = this.initStarterPrefs(); + this.starterPreferences = this.initStarterPrefs(); this.menuOptions = getEnumValues(MenuOptions); @@ -783,8 +787,8 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.isFormGender = formKey === "male" || formKey === "female"; if ( this.isFormGender - && ((this.savedStarterAttributes.female === true && formKey === "male") - || (this.savedStarterAttributes.female === false && formKey === "female")) + && ((this.savedStarterPreferences.female === true && formKey === "male") + || (this.savedStarterPreferences.female === false && formKey === "female")) ) { this.formIndex = (this.formIndex + 1) % 2; formKey = this.species.forms[this.formIndex].formKey; @@ -1009,26 +1013,26 @@ export class PokedexPageUiHandler extends MessageUiHandler { * 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 + * @returns StarterPreferences for the species */ - initStarterPrefs(): StarterAttributes { - const starterAttributes: StarterAttributes | null = this.species ? { ...this.savedStarterAttributes } : null; + initStarterPrefs(): StarterPreferences { + const starterPreferences: StarterPreferences | null = this.species ? { ...this.savedStarterPreferences } : null; const caughtAttr = this.isCaught(); // no preferences or Pokemon wasn't caught, return empty attribute - if (!starterAttributes || !this.isSeen()) { + if (!starterPreferences || !this.isSeen()) { return {}; } const hasShiny = caughtAttr & DexAttr.SHINY; const hasNonShiny = caughtAttr & DexAttr.NON_SHINY; - if (!hasShiny || (starterAttributes.shiny === undefined && hasNonShiny)) { + if (!hasShiny || (starterPreferences.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; + starterPreferences.shiny = false; + starterPreferences.variant = 0; + } else if (!hasNonShiny || (starterPreferences.shiny === undefined && hasShiny)) { + starterPreferences.shiny = true; + starterPreferences.variant = 0; } this.unlockedVariants = [ @@ -1037,36 +1041,36 @@ export class PokedexPageUiHandler extends MessageUiHandler { !!(hasShiny && caughtAttr & DexAttr.VARIANT_3), ]; if ( - starterAttributes.variant === undefined - || Number.isNaN(starterAttributes.variant) - || starterAttributes.variant < 0 + starterPreferences.variant === undefined + || Number.isNaN(starterPreferences.variant) + || starterPreferences.variant < 0 ) { - starterAttributes.variant = 0; - } else if (!this.unlockedVariants[starterAttributes.variant]) { + starterPreferences.variant = 0; + } else if (!this.unlockedVariants[starterPreferences.variant]) { let highestValidIndex = -1; - for (let i = 0; i <= starterAttributes.variant && i < this.unlockedVariants.length; i++) { + for (let i = 0; i <= starterPreferences.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; + starterPreferences.variant = highestValidIndex !== -1 ? highestValidIndex : 0; } - if (starterAttributes.female !== undefined) { + if (starterPreferences.female !== undefined) { if ( - (starterAttributes.female && !(caughtAttr & DexAttr.FEMALE)) - || (!starterAttributes.female && !(caughtAttr & DexAttr.MALE)) + (starterPreferences.female && !(caughtAttr & DexAttr.FEMALE)) + || (!starterPreferences.female && !(caughtAttr & DexAttr.MALE)) ) { - starterAttributes.female = !starterAttributes.female; + starterPreferences.female = !starterPreferences.female; } } else if (caughtAttr & DexAttr.FEMALE) { - starterAttributes.female = true; + starterPreferences.female = true; } else if (caughtAttr & DexAttr.MALE) { - starterAttributes.female = false; + starterPreferences.female = false; } - return starterAttributes; + return starterPreferences; } showText( @@ -1178,10 +1182,10 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.blockInput = true; ui.setModeWithoutClear(UiMode.OPTION_SELECT).then(() => { const species = this.previousSpecies.pop(); - const starterAttributes = this.previousStarterAttributes.pop(); + const starterPreferences = this.previousStarterPreferences.pop(); this.moveInfoOverlay.clear(); this.clearText(); - ui.setModeForceTransition(UiMode.POKEDEX_PAGE, species, starterAttributes); + ui.setModeForceTransition(UiMode.POKEDEX_PAGE, species, starterPreferences); success = true; }); this.blockInput = false; @@ -1198,7 +1202,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { } else { const starterData = globalScene.gameData.starterData[this.starterId]; // prepare persistent starter data to store changes - const starterAttributes = this.starterAttributes; + const starterPreferences = this.starterPreferences; if (button === Button.ACTION) { switch (this.cursor) { @@ -1608,7 +1612,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { : (preSpecies ?? this.species).getExpandedSpeciesName(), handler: () => { this.previousSpecies.push(this.species); - this.previousStarterAttributes.push({ ...this.savedStarterAttributes }); + this.previousStarterPreferences.push({ ...this.savedStarterPreferences }); const newSpecies = allSpecies.find( species => species.speciesId === pokemonPrevolutions[pre.speciesId], ); @@ -1620,11 +1624,11 @@ export class PokedexPageUiHandler extends MessageUiHandler { : ""; const matchingForm = newSpecies?.forms.find(form => form.formKey === newFormKey); const newFormIndex = matchingForm ? matchingForm.formIndex : 0; - this.starterAttributes.form = newFormIndex; - this.savedStarterAttributes.form = newFormIndex; + this.starterPreferences.formIndex = newFormIndex; + this.savedStarterPreferences.formIndex = newFormIndex; this.moveInfoOverlay.clear(); this.clearText(); - ui.setMode(UiMode.POKEDEX_PAGE, newSpecies, this.savedStarterAttributes); + ui.setMode(UiMode.POKEDEX_PAGE, newSpecies, this.savedStarterPreferences); return true; }, onHover: () => this.showText(conditionText), @@ -1661,12 +1665,12 @@ export class PokedexPageUiHandler extends MessageUiHandler { 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.previousStarterPreferences.push({ ...this.savedStarterPreferences }); + this.starterPreferences.formIndex = newFormIndex; + this.savedStarterPreferences.formIndex = newFormIndex; this.moveInfoOverlay.clear(); this.clearText(); - ui.setMode(UiMode.POKEDEX_PAGE, evoSpecies, this.savedStarterAttributes); + ui.setMode(UiMode.POKEDEX_PAGE, evoSpecies, this.savedStarterPreferences); return true; }, onHover: () => this.showText(conditionText), @@ -1703,17 +1707,17 @@ export class PokedexPageUiHandler extends MessageUiHandler { style: isFormCaught ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, handler: () => { this.previousSpecies.push(this.species); - this.previousStarterAttributes.push({ ...this.savedStarterAttributes }); + this.previousStarterPreferences.push({ ...this.savedStarterPreferences }); 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.starterPreferences.formIndex = newFormIndex; + this.savedStarterPreferences.formIndex = newFormIndex; this.moveInfoOverlay.clear(); this.clearText(); ui.setMode( UiMode.POKEDEX_PAGE, newSpecies, - this.savedStarterAttributes, + this.savedStarterPreferences, this.filteredIndices, ); return true; @@ -1806,9 +1810,9 @@ export class PokedexPageUiHandler extends MessageUiHandler { switch (button) { case Button.CYCLE_SHINY: if (this.canCycleShiny) { - if (!starterAttributes.shiny) { + if (!starterPreferences.shiny) { // Change to shiny, we need to get the proper default variant - const newVariant = starterAttributes.variant ? (starterAttributes.variant as Variant) : 0; + const newVariant = starterPreferences.variant ? (starterPreferences.variant as Variant) : 0; this.setSpeciesDetails(this.species, { shiny: true, variant: newVariant, @@ -1816,8 +1820,8 @@ export class PokedexPageUiHandler extends MessageUiHandler { globalScene.playSound("se/sparkle"); - starterAttributes.shiny = true; - this.savedStarterAttributes.shiny = starterAttributes.shiny; + starterPreferences.shiny = true; + this.savedStarterPreferences.shiny = starterPreferences.shiny; } else { let newVariant = props.variant; do { @@ -1835,16 +1839,16 @@ export class PokedexPageUiHandler extends MessageUiHandler { } } while (newVariant !== props.variant); - starterAttributes.variant = newVariant; // store the selected variant - this.savedStarterAttributes.variant = starterAttributes.variant; + starterPreferences.variant = newVariant; // store the selected variant + this.savedStarterPreferences.variant = starterPreferences.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; + starterPreferences.shiny = false; + this.savedStarterPreferences.shiny = starterPreferences.shiny; } else { this.setSpeciesDetails(this.species, { variant: newVariant as Variant, @@ -1865,16 +1869,16 @@ export class PokedexPageUiHandler extends MessageUiHandler { break; } } while (newFormIndex !== props.formIndex || this.species.forms[newFormIndex].isUnobtainable); - starterAttributes.form = newFormIndex; // store the selected form - this.savedStarterAttributes.form = starterAttributes.form; + starterPreferences.formIndex = newFormIndex; // store the selected form + this.savedStarterPreferences.formIndex = starterPreferences.formIndex; 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; + starterPreferences.female = newFemale; + this.savedStarterPreferences.female = starterPreferences.female; this.starterSetup(); this.setSpeciesDetails(this.species, { formIndex: newFormIndex, @@ -1885,15 +1889,15 @@ export class PokedexPageUiHandler extends MessageUiHandler { break; case Button.CYCLE_GENDER: if (this.canCycleGender) { - starterAttributes.female = !props.female; - this.savedStarterAttributes.female = starterAttributes.female; + starterPreferences.female = !props.female; + this.savedStarterPreferences.female = starterPreferences.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; + starterPreferences.formIndex = newFormIndex; // store the selected form + this.savedStarterPreferences.formIndex = starterPreferences.formIndex; this.formIndex = newFormIndex; this.starterSetup(); this.setSpeciesDetails(this.species, { @@ -1938,15 +1942,17 @@ export class PokedexPageUiHandler extends MessageUiHandler { } return false; }, - style: this.isPassiveAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, + style: isPassiveAvailable(this.species.speciesId) ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, item: "candy", - itemArgs: this.isPassiveAvailable() ? starterColors[this.starterId] : ["808080", "808080"], + itemArgs: isPassiveAvailable(this.species.speciesId) + ? starterColors[this.starterId] + : ["808080", "808080"], }); } // Reduce cost option const valueReduction = starterData.valueReduction; - if (valueReduction < valueReductionMax) { + if (valueReduction < VALUE_REDUCTION_MAX) { const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.starterId])[valueReduction]; options.push({ label: `×${reductionCost} ${i18next.t("pokedexUiHandler:reduceCost")}`, @@ -1969,9 +1975,11 @@ export class PokedexPageUiHandler extends MessageUiHandler { } return false; }, - style: this.isValueReductionAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, + style: isValueReductionAvailable(this.species.speciesId) ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, item: "candy", - itemArgs: this.isValueReductionAvailable() ? starterColors[this.starterId] : ["808080", "808080"], + itemArgs: isValueReductionAvailable(this.species.speciesId) + ? starterColors[this.starterId] + : ["808080", "808080"], }); } @@ -2018,9 +2026,11 @@ export class PokedexPageUiHandler extends MessageUiHandler { } return false; }, - style: this.isSameSpeciesEggAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, + style: isSameSpeciesEggAvailable(this.species.speciesId) ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, item: "candy", - itemArgs: this.isSameSpeciesEggAvailable() ? starterColors[this.starterId] : ["808080", "808080"], + itemArgs: isSameSpeciesEggAvailable(this.species.speciesId) + ? starterColors[this.starterId] + : ["808080", "808080"], }); options.push({ label: i18next.t("menu:cancel"), @@ -2071,7 +2081,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { // Always go back to first selection after scrolling around if (this.previousSpecies.length === 0) { this.previousSpecies.push(this.species); - this.previousStarterAttributes.push({ ...this.savedStarterAttributes }); + this.previousStarterPreferences.push({ ...this.savedStarterPreferences }); } let newSpecies: PokemonSpecies; if (this.filteredIndices) { @@ -2087,14 +2097,14 @@ export class PokedexPageUiHandler extends MessageUiHandler { form => form.formKey === this.species?.forms[this.formIndex]?.formKey, ); const newFormIndex = matchingForm ? matchingForm.formIndex : 0; - this.starterAttributes.form = newFormIndex; - this.savedStarterAttributes.form = newFormIndex; + this.starterPreferences.formIndex = newFormIndex; + this.savedStarterPreferences.formIndex = newFormIndex; this.moveInfoOverlay.clear(); this.clearText(); ui.setModeForceTransition( UiMode.POKEDEX_PAGE, newSpecies, - this.savedStarterAttributes, + this.savedStarterPreferences, this.filteredIndices, ); }); @@ -2110,7 +2120,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { // Always go back to first selection after scrolling around if (this.previousSpecies.length === 0) { this.previousSpecies.push(this.species); - this.previousStarterAttributes.push({ ...this.savedStarterAttributes }); + this.previousStarterPreferences.push({ ...this.savedStarterPreferences }); } let newSpecies: PokemonSpecies; if (this.filteredIndices) { @@ -2126,14 +2136,14 @@ export class PokedexPageUiHandler extends MessageUiHandler { form => form.formKey === this.species?.forms[this.formIndex]?.formKey, ); const newFormIndex = matchingForm ? matchingForm.formIndex : 0; - this.starterAttributes.form = newFormIndex; - this.savedStarterAttributes.form = newFormIndex; + this.starterPreferences.formIndex = newFormIndex; + this.savedStarterPreferences.formIndex = newFormIndex; this.moveInfoOverlay.clear(); this.clearText(); ui.setModeForceTransition( UiMode.POKEDEX_PAGE, newSpecies, - this.savedStarterAttributes, + this.savedStarterPreferences, this.filteredIndices, ); }); @@ -2283,49 +2293,9 @@ export class PokedexPageUiHandler extends MessageUiHandler { 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; + const starterPreferences: StarterPreferences | null = species ? { ...this.starterPreferences } : null; if (!species && globalScene.ui.getTooltip().visible) { globalScene.ui.hideTooltip(); @@ -2347,17 +2317,17 @@ export class PokedexPageUiHandler extends MessageUiHandler { 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) && props.shiny) { - props.variant = starterAttributes.variant as Variant; + // Set default attributes if for some reason starterPreferences does not exist or attributes missing + const props: StarterPreferences = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); + if (starterPreferences?.variant && !Number.isNaN(starterPreferences.variant) && props.shiny) { + props.variant = starterPreferences.variant as Variant; } - props.form = starterAttributes?.form ?? props.form; - props.female = starterAttributes?.female ?? props.female; + props.formIndex = starterPreferences?.formIndex ?? props.formIndex; + props.female = starterPreferences?.female ?? props.female; this.setSpeciesDetails(species, { shiny: props.shiny, - formIndex: props.form, + formIndex: props.formIndex, female: props.female, variant: props.variant ?? 0, }); @@ -2419,7 +2389,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, forceUpdate?: boolean): void { let { shiny, formIndex, female, variant } = options; - const oldProps = species ? this.starterAttributes : null; + const oldProps = species ? this.starterPreferences : null; // We will only update the sprite if there is a change to form, shiny/variant // or gender for species with gender sprite differences @@ -2455,7 +2425,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { variant = oldProps?.variant ?? 0; } if (formIndex === undefined) { - formIndex = oldProps?.form ?? 0; + formIndex = oldProps?.formIndex ?? 0; } } @@ -2470,13 +2440,13 @@ export class PokedexPageUiHandler extends MessageUiHandler { const caughtAttr = this.isCaught(species); if (!caughtAttr) { - const props = this.starterAttributes; + const props = this.starterPreferences; if (shiny === undefined || shiny !== props.shiny) { shiny = props.shiny; } - if (formIndex === undefined || formIndex !== props.form) { - formIndex = props.form; + if (formIndex === undefined || formIndex !== props.formIndex) { + formIndex = props.formIndex; } if (female === undefined || female !== props.female) { female = props.female; @@ -2727,7 +2697,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { } /** - * Creates a temporary dex attr props that will be used to display the correct shiny, variant, and form based on this.starterAttributes + * Creates a temporary dex attr props that will be used to display the correct shiny, variant, and form based on this.starterPreferences * * @param speciesId the id of the species to get props for * @returns the dex props @@ -2744,7 +2714,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { * 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)) { + if (this.starterPreferences?.female || ((caughtAttr & DexAttr.FEMALE) > 0n && (caughtAttr & DexAttr.MALE) === 0n)) { props += DexAttr.FEMALE; } else { props += DexAttr.MALE; @@ -2753,12 +2723,12 @@ export class PokedexPageUiHandler extends MessageUiHandler { * 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) + this.starterPreferences?.shiny + || ((caughtAttr & DexAttr.SHINY) > 0n && this.starterPreferences?.shiny !== false) ) { props += DexAttr.SHINY; - if (this.starterAttributes?.variant !== undefined) { - props += BigInt(Math.pow(2, this.starterAttributes?.variant)) * DexAttr.DEFAULT_VARIANT; + if (this.starterPreferences?.variant !== undefined) { + props += BigInt(Math.pow(2, this.starterPreferences?.variant)) * DexAttr.DEFAULT_VARIANT; /* This chunk 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 */ @@ -2773,9 +2743,9 @@ export class PokedexPageUiHandler extends MessageUiHandler { 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) { + if (this.starterPreferences?.formIndex) { // this checks for the form of the pokemon - props += BigInt(Math.pow(2, this.starterAttributes?.form)) * DexAttr.DEFAULT_FORM; + props += BigInt(Math.pow(2, this.starterPreferences?.formIndex)) * DexAttr.DEFAULT_FORM; } else { // Get the first unlocked form props += globalScene.gameData.getFormAttr(globalScene.gameData.getFormIndex(caughtAttr)); diff --git a/src/ui/handlers/pokedex-ui-handler.ts b/src/ui/handlers/pokedex-ui-handler.ts index c6f9dbee448..127bcd824c1 100644 --- a/src/ui/handlers/pokedex-ui-handler.ts +++ b/src/ui/handlers/pokedex-ui-handler.ts @@ -4,14 +4,7 @@ import { catchableSpecies } from "#balance/biomes"; import { speciesEggMoves } from "#balance/egg-moves"; import { pokemonStarters } from "#balance/pokemon-evolutions"; import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#balance/pokemon-level-moves"; -import { - getPassiveCandyCount, - getSameSpeciesEggCandyCounts, - getStarterValueFriendshipCap, - getValueReductionCandyCounts, - POKERUS_STARTER_COUNT, - speciesStarterCosts, -} from "#balance/starters"; +import { getStarterValueFriendshipCap, POKERUS_STARTER_COUNT, speciesStarterCosts } from "#balance/starters"; import { speciesTmMoves } from "#balance/tms"; import { allAbilities, allMoves, allSpecies } from "#data/data-lists"; import type { PokemonForm, PokemonSpecies } from "#data/pokemon-species"; @@ -23,7 +16,6 @@ import { Button } from "#enums/buttons"; import { DexAttr } from "#enums/dex-attr"; import { DropDownColumn } from "#enums/drop-down-column"; import type { Nature } from "#enums/nature"; -import { Passive as PassiveAttr } from "#enums/passive"; import { PokemonType } from "#enums/pokemon-type"; import type { SpeciesId } from "#enums/species-id"; import { TextStyle } from "#enums/text-style"; @@ -33,7 +25,7 @@ import type { Variant } from "#sprites/variant"; import { getVariantIcon, getVariantTint } from "#sprites/variant"; import { SettingKeyboard } from "#system/settings-keyboard"; import type { DexEntry } from "#types/dex-data"; -import type { DexAttrProps, StarterAttributes } from "#types/save-data"; +import type { DexAttrProps, StarterPreferences } from "#types/save-data"; import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler"; import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#ui/dropdown"; import { FilterBar } from "#ui/filter-bar"; @@ -45,12 +37,17 @@ import { ScrollBar } from "#ui/scroll-bar"; import { addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { BooleanHolder, fixedInt, getLocalizedSpriteKey, padInt, randIntRange, rgbHexToRgba } from "#utils/common"; -import type { StarterPreferences } from "#utils/data"; +import type { AllStarterPreferences } from "#utils/data"; import { loadStarterPreferences } from "#utils/data"; import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils"; import { toCamelCase } from "#utils/strings"; import { argbFromRgba } from "@material/material-color-utilities"; import i18next from "i18next"; +import { + isPassiveAvailable, + isSameSpeciesEggAvailable, + isValueReductionAvailable, +} from "../utils/starter-select-ui-utils"; interface LanguageSetting { starterInfoTextSize: string; @@ -131,8 +128,6 @@ interface ContainerData { passive2?: boolean; } -const valueReductionMax = 2; - // Position of UI elements const filterBarHeight = 17; const speciesContainerX = 143; @@ -193,7 +188,7 @@ export class PokedexUiHandler extends MessageUiHandler { private iconAnimHandler: PokemonIconAnimHelper; - private starterPreferences: StarterPreferences; + private starterPreferences: AllStarterPreferences; protected blockInput = false; @@ -681,15 +676,15 @@ export class PokedexUiHandler extends MessageUiHandler { * 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 + * @returns StarterPreferences for the species */ - initStarterPrefs(species: PokemonSpecies): StarterAttributes { - const starterAttributes = this.starterPreferences[species.speciesId]; + initStarterPrefs(species: PokemonSpecies): StarterPreferences { + const starterPreferences = this.starterPreferences[species.speciesId]; const dexEntry = globalScene.gameData.dexData[species.speciesId]; const starterData = globalScene.gameData.starterData[species.speciesId]; // no preferences or Pokemon wasn't caught, return empty attribute - if (!starterAttributes || !dexEntry.caughtAttr) { + if (!starterPreferences || !dexEntry.caughtAttr) { return {}; } @@ -697,41 +692,41 @@ export class PokedexUiHandler extends MessageUiHandler { const hasShiny = caughtAttr & DexAttr.SHINY; const hasNonShiny = caughtAttr & DexAttr.NON_SHINY; - if (starterAttributes.shiny && !hasShiny) { + if (starterPreferences.shiny && !hasShiny) { // shiny form wasn't unlocked, purging shiny and variant setting - starterAttributes.shiny = undefined; - starterAttributes.variant = undefined; - } else if (starterAttributes.shiny === false && !hasNonShiny) { + starterPreferences.shiny = undefined; + starterPreferences.variant = undefined; + } else if (starterPreferences.shiny === false && !hasNonShiny) { // non shiny form wasn't unlocked, purging shiny setting - starterAttributes.shiny = undefined; + starterPreferences.shiny = undefined; } - if (starterAttributes.variant !== undefined) { + if (starterPreferences.variant !== undefined) { const unlockedVariants = [ hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT, hasShiny && caughtAttr & DexAttr.VARIANT_2, hasShiny && caughtAttr & DexAttr.VARIANT_3, ]; if ( - Number.isNaN(starterAttributes.variant) - || starterAttributes.variant < 0 - || !unlockedVariants[starterAttributes.variant] + Number.isNaN(starterPreferences.variant) + || starterPreferences.variant < 0 + || !unlockedVariants[starterPreferences.variant] ) { // variant value is invalid or requested variant wasn't unlocked, purging setting - starterAttributes.variant = undefined; + starterPreferences.variant = undefined; } } if ( - starterAttributes.female !== undefined - && !(starterAttributes.female ? caughtAttr & DexAttr.FEMALE : caughtAttr & DexAttr.MALE) + starterPreferences.female !== undefined + && !(starterPreferences.female ? caughtAttr & DexAttr.FEMALE : caughtAttr & DexAttr.MALE) ) { // requested gender wasn't unlocked, purging setting - starterAttributes.female = undefined; + starterPreferences.female = undefined; } - if (starterAttributes.ability !== undefined) { + if (starterPreferences.abilityIndex !== undefined) { const speciesHasSingleAbility = species.ability2 === species.ability1; const abilityAttr = starterData.abilityAttr; const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1; @@ -744,31 +739,31 @@ export class PokedexUiHandler extends MessageUiHandler { speciesHasSingleAbility ? hasAbility2 && !hasAbility1 : hasAbility2, hasHiddenAbility, ]; - if (!unlockedAbilities[starterAttributes.ability]) { + if (!unlockedAbilities[starterPreferences.abilityIndex]) { // requested ability wasn't unlocked, purging setting - starterAttributes.ability = undefined; + starterPreferences.abilityIndex = undefined; } } - const selectedForm = starterAttributes.form; + const selectedForm = starterPreferences.formIndex; if ( selectedForm !== undefined && (!species.forms[selectedForm]?.isStarterSelectable || !(caughtAttr & globalScene.gameData.getFormAttr(selectedForm))) ) { // requested form wasn't unlocked/isn't a starter form, purging setting - starterAttributes.form = undefined; + starterPreferences.formIndex = undefined; } - if (starterAttributes.nature !== undefined) { + if (starterPreferences.nature !== undefined) { const unlockedNatures = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr); - if (unlockedNatures.indexOf(starterAttributes.nature as unknown as Nature) < 0) { + if (unlockedNatures.indexOf(starterPreferences.nature as unknown as Nature) < 0) { // requested nature wasn't unlocked, purging setting - starterAttributes.nature = undefined; + starterPreferences.nature = undefined; } } - return starterAttributes; + return starterPreferences; } /** @@ -844,52 +839,6 @@ export class PokedexUiHandler extends MessageUiHandler { return pokemonStarters[speciesId]; } - /** - * Determines if a passive upgrade is available for the given species ID - * @param speciesId The ID of the species to check the passive of - * @returns true if the user has enough candies and a passive has not been unlocked already - */ - isPassiveAvailable(speciesId: number): boolean { - // Get this species ID's starter data - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)]; - - return ( - starterData.candyCount >= getPassiveCandyCount(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]) - && !(starterData.passiveAttr & PassiveAttr.UNLOCKED) - ); - } - - /** - * Determines if a value reduction upgrade is available for the given species ID - * @param speciesId The ID of the species to check the value reduction of - * @returns true if the user has enough candies and all value reductions have not been unlocked already - */ - isValueReductionAvailable(speciesId: number): boolean { - // Get this species ID's starter data - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)]; - - return ( - starterData.candyCount - >= getValueReductionCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(speciesId)])[ - starterData.valueReduction - ] && starterData.valueReduction < valueReductionMax - ); - } - - /** - * Determines if an same species egg can be bought for the given species ID - * @param speciesId The ID of the species to check the value reduction of - * @returns true if the user has enough candies - */ - isSameSpeciesEggAvailable(speciesId: number): boolean { - // Get this species ID's starter data - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)]; - - return ( - starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]) - ); - } - /** * Sets a bounce animation if enabled and the Pokemon has an upgrade * @param icon {@linkcode Phaser.GameObjects.GameObject} to animate @@ -931,9 +880,9 @@ export class PokedexUiHandler extends MessageUiHandler { }; if ( - this.isPassiveAvailable(species.speciesId) + isPassiveAvailable(species.speciesId) || (globalScene.candyUpgradeNotification === 2 - && (this.isValueReductionAvailable(species.speciesId) || this.isSameSpeciesEggAvailable(species.speciesId))) + && (isValueReductionAvailable(species.speciesId) || isSameSpeciesEggAvailable(species.speciesId))) ) { const chain = globalScene.tweens.chain(tweenChain); if (!startPaused) { @@ -959,19 +908,19 @@ export class PokedexUiHandler extends MessageUiHandler { return; } - const isPassiveAvailable = this.isPassiveAvailable(species.speciesId); - const isValueReductionAvailable = this.isValueReductionAvailable(species.speciesId); - const isSameSpeciesEggAvailable = this.isSameSpeciesEggAvailable(species.speciesId); + const passiveAvailable = isPassiveAvailable(species.speciesId); + const valueReductionAvailable = isValueReductionAvailable(species.speciesId); + const sameSpeciesEggAvailable = isSameSpeciesEggAvailable(species.speciesId); // 'Passive Only' mode if (globalScene.candyUpgradeNotification === 1) { - starter.candyUpgradeIcon.setVisible(slotVisible && isPassiveAvailable); + starter.candyUpgradeIcon.setVisible(slotVisible && passiveAvailable); starter.candyUpgradeOverlayIcon.setVisible(slotVisible && starter.candyUpgradeIcon.visible); // 'On' mode } else if (globalScene.candyUpgradeNotification === 2) { starter.candyUpgradeIcon.setVisible( - slotVisible && (isPassiveAvailable || isValueReductionAvailable || isSameSpeciesEggAvailable), + slotVisible && (passiveAvailable || valueReductionAvailable || sameSpeciesEggAvailable), ); starter.candyUpgradeOverlayIcon.setVisible(slotVisible && starter.candyUpgradeIcon.visible); } @@ -1533,7 +1482,7 @@ export class PokedexUiHandler extends MessageUiHandler { // Passive Filter const isPassiveUnlocked = starterData.passiveAttr > 0; - const isPassiveUnlockable = this.isPassiveAvailable(species.speciesId) && !isPassiveUnlocked; + const isPassiveUnlockable = isPassiveAvailable(species.speciesId) && !isPassiveUnlocked; const fitsPassive = this.filterBar.getVals(DropDownColumn.UNLOCKS).some(unlocks => { if (unlocks.val === "PASSIVE" && unlocks.state === DropDownState.ON) { return isPassiveUnlocked; @@ -1552,7 +1501,7 @@ export class PokedexUiHandler extends MessageUiHandler { // Cost Reduction Filter const isCostReducedByOne = starterData.valueReduction === 1; const isCostReducedByTwo = starterData.valueReduction === 2; - const isCostReductionUnlockable = this.isValueReductionAvailable(species.speciesId); + const isCostReductionUnlockable = isValueReductionAvailable(species.speciesId); const fitsCostReduction = this.filterBar.getVals(DropDownColumn.UNLOCKS).some(unlocks => { if (unlocks.val === "COST_REDUCTION" && unlocks.state === DropDownState.ON) { return isCostReducedByOne || isCostReducedByTwo; @@ -1664,7 +1613,7 @@ export class PokedexUiHandler extends MessageUiHandler { }); // Egg Purchasable Filter - const isEggPurchasable = this.isSameSpeciesEggAvailable(species.speciesId); + const isEggPurchasable = isSameSpeciesEggAvailable(species.speciesId); const fitsEgg = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { if (misc.val === "EGG" && misc.state === DropDownState.ON) { return isEggPurchasable; @@ -2347,7 +2296,7 @@ export class PokedexUiHandler extends MessageUiHandler { /** * Creates a temporary dex attr props that will be used to - * display the correct shiny, variant, and form based on the StarterPreferences + * display the correct shiny, variant, and form based on the AllStarterPreferences * * @param speciesId the id of the species to get props for * @returns the dex props @@ -2396,9 +2345,9 @@ export class PokedexUiHandler extends MessageUiHandler { 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.starterPreferences[speciesId]?.form) { + if (this.starterPreferences[speciesId]?.formIndex) { // this checks for the form of the pokemon - props += BigInt(Math.pow(2, this.starterPreferences[speciesId]?.form)) * DexAttr.DEFAULT_FORM; + props += BigInt(Math.pow(2, this.starterPreferences[speciesId]?.formIndex)) * DexAttr.DEFAULT_FORM; } else { // Get the first unlocked form props += globalScene.gameData.getFormAttr(globalScene.gameData.getFormIndex(caughtAttr)); diff --git a/src/ui/handlers/starter-select-ui-handler.ts b/src/ui/handlers/starter-select-ui-handler.ts index d0bef69aa81..23fe723b03a 100644 --- a/src/ui/handlers/starter-select-ui-handler.ts +++ b/src/ui/handlers/starter-select-ui-handler.ts @@ -1,5 +1,4 @@ -import type { Ability } from "#abilities/ability"; -import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; +import { PLAYER_PARTY_MAX_SIZE, VALUE_REDUCTION_MAX } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { starterColors } from "#app/global-vars/starter-colors"; import Overrides from "#app/overrides"; @@ -11,22 +10,18 @@ import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#balance/pokemo import { getPassiveCandyCount, getSameSpeciesEggCandyCounts, - getStarterValueFriendshipCap, getValueReductionCandyCounts, POKERUS_STARTER_COUNT, speciesStarterCosts, } from "#balance/starters"; -import { allAbilities, allMoves, allSpecies } from "#data/data-lists"; -import { Egg, getEggTierForSpecies } from "#data/egg"; -import { GrowthRate, getGrowthRateColor } from "#data/exp"; -import { Gender, getGenderColor, getGenderSymbol } from "#data/gender"; +import { allMoves, allSpecies } from "#data/data-lists"; +import { Egg } from "#data/egg"; import { getNatureName } from "#data/nature"; import { pokemonFormChanges } from "#data/pokemon-forms"; import type { PokemonSpecies } from "#data/pokemon-species"; import { AbilityAttr } from "#enums/ability-attr"; import { AbilityId } from "#enums/ability-id"; import { Button } from "#enums/buttons"; -import { ChallengeType } from "#enums/challenge-type"; import { Challenges } from "#enums/challenges"; import { Device } from "#enums/devices"; import { DexAttr } from "#enums/dex-attr"; @@ -37,7 +32,7 @@ import type { MoveId } from "#enums/move-id"; import type { Nature } from "#enums/nature"; import { Passive as PassiveAttr } from "#enums/passive"; import { PokemonType } from "#enums/pokemon-type"; -import { SpeciesId } from "#enums/species-id"; +import type { SpeciesId } from "#enums/species-id"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { UiTheme } from "#enums/ui-theme"; @@ -49,7 +44,7 @@ import { achvs } from "#system/achv"; import { RibbonData } from "#system/ribbons/ribbon-data"; import { SettingKeyboard } from "#system/settings-keyboard"; import type { DexEntry } from "#types/dex-data"; -import type { Starter, StarterAttributes, StarterDataEntry, StarterMoveset } from "#types/save-data"; +import type { DexAttrProps, Starter, StarterMoveset, StarterPreferences } from "#types/save-data"; import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#ui/dropdown"; import { FilterBar } from "#ui/filter-bar"; @@ -58,122 +53,39 @@ import { MoveInfoOverlay } from "#ui/move-info-overlay"; import { PokemonIconAnimHelper, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-helper"; import { ScrollBar } from "#ui/scroll-bar"; import { StarterContainer } from "#ui/starter-container"; -import { StatsContainer } from "#ui/stats-container"; -import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; +import { StarterSummary } from "#ui/starter-summary"; +import { addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; -import { applyChallenges, checkStarterValidForChallenge } from "#utils/challenge-utils"; import { - BooleanHolder, - fixedInt, - getLocalizedSpriteKey, - NumberHolder, - padInt, - randIntRange, - rgbHexToRgba, -} from "#utils/common"; -import type { StarterPreferences } from "#utils/data"; + getDexAttrFromPreferences, + getRunValueLimit, + getSpeciesData, + getSpeciesDetailsFromPreferences, + getSpeciesPropsFromPreferences, + getStarterSelectTextSettings, + isPassiveAvailable, + isSameSpeciesEggAvailable, + isStarterValidForChallenge, + isUpgradeAnimationEnabled, + isUpgradeIconEnabled, + isValueReductionAvailable, +} from "#ui/utils/starter-select-ui-utils"; +import { checkStarterValidForChallenge } from "#utils/challenge-utils"; +import { fixedInt, getLocalizedSpriteKey, randIntRange, rgbHexToRgba } from "#utils/common"; +import type { AllStarterPreferences } from "#utils/data"; import { deepCopy, loadStarterPreferences, saveStarterPreferences } from "#utils/data"; -import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils"; -import { toCamelCase, toTitleCase } from "#utils/strings"; +import { getPokemonSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils"; import { argbFromRgba } from "@material/material-color-utilities"; import i18next from "i18next"; import type { GameObjects } from "phaser"; -import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; + +const COLUMNS = 9; +const ROWS = 9; +const STARTER_ICONS_CURSOR_X_OFFSET = -3; +const STARTER_ICONS_CURSOR_Y_OFFSET = 1; export type StarterSelectCallback = (starters: Starter[]) => void; -interface LanguageSetting { - starterInfoTextSize: string; - instructionTextSize: string; - starterInfoXPos?: number; - starterInfoYOffset?: number; -} - -const languageSettings: { [key: string]: LanguageSetting } = { - en: { - starterInfoTextSize: "56px", - instructionTextSize: "38px", - }, - de: { - starterInfoTextSize: "54px", - instructionTextSize: "35px", - starterInfoXPos: 35, - }, - "es-ES": { - starterInfoTextSize: "50px", - instructionTextSize: "38px", - starterInfoYOffset: 0.5, - starterInfoXPos: 38, - }, - "es-419": { - starterInfoTextSize: "50px", - instructionTextSize: "38px", - starterInfoYOffset: 0.5, - starterInfoXPos: 38, - }, - fr: { - starterInfoTextSize: "54px", - instructionTextSize: "38px", - }, - it: { - starterInfoTextSize: "56px", - instructionTextSize: "38px", - }, - "pt-BR": { - starterInfoTextSize: "48px", - instructionTextSize: "42px", - starterInfoYOffset: 0.5, - starterInfoXPos: 33, - }, - zh: { - starterInfoTextSize: "56px", - instructionTextSize: "36px", - starterInfoXPos: 26, - }, - ko: { - starterInfoTextSize: "60px", - instructionTextSize: "38px", - starterInfoYOffset: -0.5, - starterInfoXPos: 30, - }, - ja: { - starterInfoTextSize: "48px", - instructionTextSize: "40px", - starterInfoYOffset: 1, - starterInfoXPos: 32, - }, - ca: { - starterInfoTextSize: "48px", - instructionTextSize: "38px", - starterInfoYOffset: 0.5, - starterInfoXPos: 29, - }, - da: { - starterInfoTextSize: "56px", - instructionTextSize: "38px", - }, - tr: { - starterInfoTextSize: "56px", - instructionTextSize: "38px", - }, - ro: { - starterInfoTextSize: "56px", - instructionTextSize: "38px", - }, - ru: { - starterInfoTextSize: "46px", - instructionTextSize: "38px", - starterInfoYOffset: 0.5, - starterInfoXPos: 26, - }, - tl: { - starterInfoTextSize: "56px", - instructionTextSize: "38px", - }, -}; - -const valueReductionMax = 2; - // Position of UI elements const filterBarHeight = 17; const speciesContainerX = 109; // if team on the RIGHT: 109 / if on the LEFT: 143 @@ -188,11 +100,11 @@ const randomSelectionWindowHeight = 20; * @param index UI index to calculate the starter position of * @returns An interface with an x and y property */ -function calcStarterPosition(index: number, scrollCursor = 0): { x: number; y: number } { +function calcStarterContainerPosition(index: number): { x: number; y: number } { const yOffset = 13; const height = 17; const x = (index % 9) * 18; - const y = yOffset + (Math.floor(index / 9) - scrollCursor) * height; + const y = yOffset + Math.floor(index / 9) * height; return { x, y }; } @@ -227,89 +139,15 @@ function findClosestStarterIndex(y: number, teamSize = 6): number { return closestStarterIndex; } -/** - * Finds the row of the filtered Pokemon closest vertically to the given Pokemon in the team - * @param index index of the Pokemon in the team (0-5) - * @param numberOfRows the number of rows to check against - * @returns index of the row closest vertically to the given Pokemon - */ -function findClosestStarterRow(index: number, numberOfRows: number) { - const currentY = calcStarterIconY(index) - 13; - let smallestDistance = teamWindowHeight; - let closestRowIndex = 0; - for (let i = 0; i < numberOfRows; i++) { - const distance = Math.abs(currentY - calcStarterPosition(i * 9).y); - if (distance < smallestDistance) { - closestRowIndex = i; - smallestDistance = distance; - } - } - return closestRowIndex; -} - -interface SpeciesDetails { - shiny?: boolean; - formIndex?: number; - female?: boolean; - variant?: Variant; - abilityIndex?: number; - natureIndex?: number; - forSeen?: boolean; // default = false - teraType?: PokemonType; -} - export class StarterSelectUiHandler extends MessageUiHandler { private starterSelectContainer: Phaser.GameObjects.Container; private starterSelectScrollBar: ScrollBar; - private filterBarContainer: Phaser.GameObjects.Container; private filterBar: FilterBar; - private shinyOverlay: Phaser.GameObjects.Image; private starterContainers: StarterContainer[] = []; - private filteredStarterContainers: StarterContainer[] = []; - private validStarterContainers: StarterContainer[] = []; - 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 pokemonAbilityLabelText: Phaser.GameObjects.Text; - private pokemonAbilityText: Phaser.GameObjects.Text; - private pokemonPassiveLabelText: Phaser.GameObjects.Text; - private pokemonPassiveText: Phaser.GameObjects.Text; - private pokemonNatureLabelText: Phaser.GameObjects.Text; - private pokemonNatureText: BBCodeText; - private pokemonMovesContainer: Phaser.GameObjects.Container; - private pokemonMoveContainers: Phaser.GameObjects.Container[]; - private pokemonMoveBgs: Phaser.GameObjects.NineSlice[]; - private pokemonMoveLabels: Phaser.GameObjects.Text[]; - private pokemonAdditionalMoveCountLabel: Phaser.GameObjects.Text; - private eggMovesLabel: Phaser.GameObjects.Text; - private pokemonEggMovesContainer: Phaser.GameObjects.Container; - private pokemonEggMoveContainers: Phaser.GameObjects.Container[]; - private pokemonEggMoveBgs: Phaser.GameObjects.NineSlice[]; - private pokemonEggMoveLabels: 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 pokemonShinyIcon: Phaser.GameObjects.Sprite; - private pokemonPassiveDisabledIcon: Phaser.GameObjects.Sprite; - private pokemonPassiveLockedIcon: Phaser.GameObjects.Sprite; - private teraIcon: Phaser.GameObjects.Sprite; + private filteredStarterIds: SpeciesId[] = []; + + private starterSummary: StarterSummary; - private activeTooltip: "ABILITY" | "PASSIVE" | "CANDY" | undefined; private instructionsContainer: Phaser.GameObjects.Container; private filterInstructionsContainer: Phaser.GameObjects.Container; private shinyIconElement: Phaser.GameObjects.Sprite; @@ -333,12 +171,9 @@ export class StarterSelectUiHandler extends MessageUiHandler { private starterSelectMessageBox: Phaser.GameObjects.NineSlice; private starterSelectMessageBoxContainer: Phaser.GameObjects.Container; - private statsContainer: StatsContainer; private moveInfoOverlay: MoveInfoOverlay; private statsMode: boolean; - private starterIconsCursorXOffset = -3; - private starterIconsCursorYOffset = 1; private starterIconsCursorIndex: number; private filterMode: boolean; private dexAttrCursor = 0n; @@ -349,16 +184,14 @@ export class StarterSelectUiHandler extends MessageUiHandler { private starterMoveset: StarterMoveset | null; private scrollCursor: number; - private allSpecies: PokemonSpecies[] = []; + private allStarterSpecies: PokemonSpecies[] = []; private lastSpecies: PokemonSpecies; - private speciesLoaded: Map = new Map(); private starters: Starter[] = []; public starterSpecies: PokemonSpecies[] = []; private pokerusSpecies: PokemonSpecies[] = []; private speciesStarterDexEntry: DexEntry | null; private speciesStarterMoves: MoveId[]; - private canCycleShiny: boolean; private canCycleForm: boolean; private canCycleGender: boolean; @@ -366,7 +199,6 @@ export class StarterSelectUiHandler extends MessageUiHandler { private canCycleNature: boolean; private canCycleTera: boolean; - private assetLoadCancelled: BooleanHolder | null; public cursorObj: Phaser.GameObjects.Image; private starterCursorObjs: Phaser.GameObjects.Image[]; private pokerusCursorObjs: Phaser.GameObjects.Image[]; @@ -387,8 +219,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { private starterSelectCallback: StarterSelectCallback | null; - private starterPreferences: StarterPreferences; - private originalStarterPreferences: StarterPreferences; + private starterPreferences: AllStarterPreferences; + private originalStarterPreferences: AllStarterPreferences; /** * Used to check whether any moves were swapped using the reorder menu, to decide @@ -398,6 +230,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { protected blockInput = false; private allowTera: boolean; + private partyColumn: GameObjects.Container; + private oldCursor = -1; constructor() { super(UiMode.STARTER_SELECT); @@ -405,9 +239,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { 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]; + /** Scaled canvas height */ const sHeight = globalScene.scaledCanvas.height; /** Scaled canvas width */ @@ -423,19 +255,140 @@ export class StarterSelectUiHandler extends MessageUiHandler { .setOrigin(0, 1); // Pixel text 'No' const starterSelectBg = globalScene.add.image(0, 0, "starter_select_bg").setOrigin(0); - this.shinyOverlay = globalScene.add - .image(6, 111, getLocalizedSpriteKey("summary_dexnb_label_overlay_shiny")) - .setOrigin(0, 1) - .setVisible(false); // Pixel text 'No' shiny - const starterContainerWindow = addWindow(speciesContainerX, filterBarHeight + 1, 175, 161); const starterContainerBg = globalScene.add .image(speciesContainerX + 1, filterBarHeight + 2, "starter_container_bg") .setOrigin(0); // Create and initialise filter bar - this.filterBarContainer = globalScene.add.container(0, 0); - this.filterBar = new FilterBar(Math.min(speciesContainerX, teamWindowX), 1, 210, filterBarHeight); + this.filterBar = this.setupFilterBar(); + + this.iconAnimHandler = new PokemonIconAnimHelper(); + this.iconAnimHandler.setup(); + + this.partyColumn = this.setupPartyColumn(); + + const starterBoxContainer = globalScene.add.container(speciesContainerX + 6, 9); //115 + + this.starterSelectScrollBar = new ScrollBar(161, 12, 5, 155, 9); + + starterBoxContainer.add(this.starterSelectScrollBar); + + this.pokerusCursorObjs = []; + for (let i = 0; i < POKERUS_STARTER_COUNT; i++) { + const cursorObj = globalScene.add.image(0, 0, "select_cursor_pokerus"); + cursorObj.setVisible(false); + cursorObj.setOrigin(0); + starterBoxContainer.add(cursorObj); + this.pokerusCursorObjs.push(cursorObj); + } + + this.starterCursorObjs = []; + for (let i = 0; i < 6; i++) { + const cursorObj = globalScene.add.image(0, 0, "select_cursor_highlight"); + cursorObj.setVisible(false); + cursorObj.setOrigin(0); + starterBoxContainer.add(cursorObj); + this.starterCursorObjs.push(cursorObj); + } + + this.cursorObj = globalScene.add.image(0, 0, "select_cursor").setOrigin(0); + + starterBoxContainer.add(this.cursorObj); + + // TODO: Apply the same logic done in the pokedex to only have 81 containers whose sprites are cycled + for (const species of allSpecies) { + if (!speciesStarterCosts.hasOwnProperty(species.speciesId) || !species.isObtainable()) { + continue; + } + this.allStarterSpecies.push(species); + } + + for (let i = 0; i < 81; i++) { + const starterContainer = new StarterContainer(this.allStarterSpecies[i]).setVisible(false); + const pos = calcStarterContainerPosition(i); + starterContainer.setPosition(pos.x, pos.y); + this.iconAnimHandler.addOrUpdate(starterContainer.icon, PokemonIconAnimMode.NONE); + this.starterContainers.push(starterContainer); + starterBoxContainer.add(starterContainer); + } + + this.starterSummary = new StarterSummary(0, 0); + + this.setupInstructionButtons(); + this.instructionsContainer = globalScene.add.container(4, 156).setVisible(true); + + /** TODO: Uncomment this and update `this.hideInstructions` once our testing infra supports mocks of `Phaser.GameObject.Group` */ + /* + this.instructionElemGroup = globalScene.add.group([ + this.shinyIconElement, + this.shinyLabel, + this.formIconElement, + this.formLabel, + this.genderIconElement, + this.genderLabel, + this.abilityIconElement, + this.abilityLabel, + this.natureIconElement, + this.natureLabel, + this.teraIconElement, + this.teraLabel, + this.goFilterIconElement, + this.goFilterLabel, + ]); + */ + + this.hideInstructions(); + + this.filterInstructionsContainer = globalScene.add.container(50, 5).setVisible(true); + + this.starterSelectMessageBoxContainer = globalScene.add.container(0, sHeight).setVisible(false); + + this.starterSelectMessageBox = addWindow(1, -1, 318, 28).setOrigin(0, 1); + this.starterSelectMessageBoxContainer.add(this.starterSelectMessageBox); + + this.message = addTextObject(8, 8, "", TextStyle.WINDOW, { maxLines: 2 }).setOrigin(0); + this.starterSelectMessageBoxContainer.add(this.message); + + // arrow icon for the message box + this.initPromptSprite(this.starterSelectMessageBoxContainer); + + // add the info overlay last to be the top most ui element and prevent the IVs from overlaying this + this.moveInfoOverlay = new MoveInfoOverlay({ + top: true, + x: 1, + y: globalScene.scaledCanvas.height - MoveInfoOverlay.getHeight() - 29, + }); + + this.starterSelectContainer.add([ + bgColor, + starterSelectBg, + starterDexNoLabel, + starterContainerBg, + this.partyColumn, + starterBoxContainer, + this.starterSummary, + this.instructionsContainer, + this.filterInstructionsContainer, + this.starterSelectMessageBoxContainer, + this.moveInfoOverlay, + // Filter bar sits above everything, except the tutorial overlay and message box. + // Do not put anything below this unless it must appear below the filter bar. + this.filterBar, + ]); + + this.initTutorialOverlay(this.starterSelectContainer); + this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer); + + globalScene.eventTarget.addEventListener(BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED, e => + this.onCandyUpgradeDisplayChanged(e), + ); + + this.updateInstructions(); + } + + setupFilterBar(): FilterBar { + const filterBar = new FilterBar(Math.min(speciesContainerX, teamWindowX), 1, 210, filterBarHeight); // gen filter const genOptions: DropDownOption[] = Array.from( @@ -443,7 +396,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { (_, i) => new DropDownOption(i + 1, new DropDownLabel(i18next.t(`starterSelectUiHandler:gen${i + 1}`))), ); const genDropDown: DropDown = new DropDown(0, 0, genOptions, this.updateStarters, DropDownType.HYBRID); - this.filterBar.addFilter(DropDownColumn.GEN, i18next.t("filterBar:genFilter"), genDropDown); + filterBar.addFilter(DropDownColumn.GEN, i18next.t("filterBar:genFilter"), genDropDown); // type filter const typeKeys = Object.keys(PokemonType).filter(v => Number.isNaN(Number(v))); @@ -457,7 +410,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { typeSprite.setFrame(type.toLowerCase()); typeOptions.push(new DropDownOption(index, new DropDownLabel("", typeSprite))); }); - this.filterBar.addFilter( + filterBar.addFilter( DropDownColumn.TYPES, i18next.t("filterBar:typeFilter"), new DropDown(0, 0, typeOptions, this.updateStarters, DropDownType.HYBRID, 0.5), @@ -491,7 +444,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { new DropDownOption("UNCAUGHT", new DropDownLabel(i18next.t("filterBar:uncaught"))), ]; - this.filterBar.addFilter( + filterBar.addFilter( DropDownColumn.CAUGHT, i18next.t("filterBar:caughtFilter"), new DropDown(0, 0, caughtOptions, this.updateStarters, DropDownType.HYBRID), @@ -519,7 +472,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { new DropDownOption("COST_REDUCTION", costReductionLabels), ]; - this.filterBar.addFilter( + filterBar.addFilter( DropDownColumn.UNLOCKS, i18next.t("filterBar:unlocksFilter"), new DropDown(0, 0, unlocksOptions, this.updateStarters, DropDownType.RADIAL), @@ -556,7 +509,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { new DropDownOption("EGG", eggLabels), new DropDownOption("POKERUS", pokerusLabels), ]; - this.filterBar.addFilter( + filterBar.addFilter( DropDownColumn.MISC, i18next.t("filterBar:miscFilter"), new DropDown(0, 0, miscOptions, this.updateStarters, DropDownType.RADIAL), @@ -575,130 +528,27 @@ export class StarterSelectUiHandler extends MessageUiHandler { new DropDownOption(SortCriteria.CAUGHT, new DropDownLabel(i18next.t("filterBar:sortByNumCaught"))), new DropDownOption(SortCriteria.HATCHED, new DropDownLabel(i18next.t("filterBar:sortByNumHatched"))), ]; - this.filterBar.addFilter( + filterBar.addFilter( DropDownColumn.SORT, i18next.t("filterBar:sortFilter"), new DropDown(0, 0, sortOptions, this.updateStarters, DropDownType.SINGLE), ); - this.filterBarContainer.add(this.filterBar); // Offset the generation filter dropdown to avoid covering the filtered pokemon - this.filterBar.offsetHybridFilters(); + filterBar.offsetHybridFilters(); + + return filterBar; + } + + setupPartyColumn(): GameObjects.Container { + const partyColumn = globalScene.add.container(0, 0); + + const starterContainerWindow = addWindow(speciesContainerX, filterBarHeight + 1, 175, 161); if (globalScene.uiTheme === UiTheme.DEFAULT) { starterContainerWindow.setVisible(false); } - this.iconAnimHandler = new PokemonIconAnimHelper(); - this.iconAnimHandler.setup(); - - 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.pokemonNumberText = addTextObject(17, 1, "0000", TextStyle.SUMMARY_DEX_NUM).setOrigin(0); - - this.pokemonNameText = addTextObject(6, 112, "", TextStyle.SUMMARY).setOrigin(0); - - this.pokemonGrowthRateLabelText = addTextObject( - 8, - 106, - i18next.t("starterSelectUiHandler:growthRate"), - TextStyle.SUMMARY_ALT, - { fontSize: "36px" }, - ) - .setOrigin(0) - .setVisible(false); - - this.pokemonGrowthRateText = addTextObject(34, 106, "", TextStyle.GROWTH_RATE_TYPE, { fontSize: "36px" }).setOrigin( - 0, - ); - - this.pokemonGenderText = addTextObject(96, 112, "", TextStyle.SUMMARY_ALT).setOrigin(0); - - this.pokemonUncaughtText = addTextObject( - 6, - 127, - i18next.t("starterSelectUiHandler:uncaught"), - TextStyle.SUMMARY_ALT, - { fontSize: "56px" }, - ).setOrigin(0); - - // The position should be set per language - const starterInfoXPos = textSettings?.starterInfoXPos || 31; - const starterInfoYOffset = textSettings?.starterInfoYOffset || 0; - - // The font size should be set per language - const starterInfoTextSize = textSettings?.starterInfoTextSize || 56; - - this.pokemonAbilityLabelText = addTextObject( - 6, - 127 + starterInfoYOffset, - i18next.t("starterSelectUiHandler:ability"), - TextStyle.SUMMARY_ALT, - { fontSize: starterInfoTextSize }, - ) - .setOrigin(0) - .setVisible(false); - - this.pokemonAbilityText = addTextObject(starterInfoXPos, 127 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { - fontSize: starterInfoTextSize, - }) - .setOrigin(0) - .setInteractive(new Phaser.Geom.Rectangle(0, 0, 250, 55), Phaser.Geom.Rectangle.Contains); - - this.pokemonPassiveLabelText = addTextObject( - 6, - 136 + starterInfoYOffset, - i18next.t("starterSelectUiHandler:passive"), - TextStyle.SUMMARY_ALT, - { fontSize: starterInfoTextSize }, - ) - .setOrigin(0) - .setVisible(false); - - this.pokemonPassiveText = addTextObject(starterInfoXPos, 136 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { - fontSize: starterInfoTextSize, - }) - .setOrigin(0) - .setInteractive(new Phaser.Geom.Rectangle(0, 0, 250, 55), Phaser.Geom.Rectangle.Contains); - - this.pokemonPassiveDisabledIcon = globalScene.add - .sprite(starterInfoXPos, 137 + starterInfoYOffset, "icon_stop") - .setOrigin(0, 0.5) - .setScale(0.35) - .setVisible(false); - - this.pokemonPassiveLockedIcon = globalScene.add - .sprite(starterInfoXPos, 137 + starterInfoYOffset, "icon_lock") - .setOrigin(0, 0.5) - .setScale(0.42, 0.38) - .setVisible(false); - - this.pokemonNatureLabelText = addTextObject( - 6, - 145 + starterInfoYOffset, - i18next.t("starterSelectUiHandler:nature"), - TextStyle.SUMMARY_ALT, - { fontSize: starterInfoTextSize }, - ) - .setOrigin(0) - .setVisible(false); - - this.pokemonNatureText = addBBCodeTextObject(starterInfoXPos, 145 + starterInfoYOffset, "", TextStyle.SUMMARY_ALT, { - fontSize: starterInfoTextSize, - }).setOrigin(0); - - this.pokemonMoveContainers = []; - this.pokemonMoveBgs = []; - this.pokemonMoveLabels = []; - - this.pokemonEggMoveContainers = []; - this.pokemonEggMoveBgs = []; - this.pokemonEggMoveLabels = []; - this.valueLimitLabel = addTextObject(teamWindowX + 17, 150, "0/10", TextStyle.STARTER_VALUE_LIMIT).setOrigin( 0.5, 0, @@ -728,57 +578,12 @@ export class StarterSelectUiHandler extends MessageUiHandler { .setVisible(false) .setOrigin(0); - const starterSpecies: SpeciesId[] = []; - - const starterBoxContainer = globalScene.add.container(speciesContainerX + 6, 9); //115 - - this.starterSelectScrollBar = new ScrollBar(161, 12, 5, starterContainerWindow.height - 6, 9); - - starterBoxContainer.add(this.starterSelectScrollBar); - - this.pokerusCursorObjs = []; - for (let i = 0; i < POKERUS_STARTER_COUNT; i++) { - const cursorObj = globalScene.add.image(0, 0, "select_cursor_pokerus"); - cursorObj.setVisible(false); - cursorObj.setOrigin(0); - starterBoxContainer.add(cursorObj); - this.pokerusCursorObjs.push(cursorObj); - } - - this.starterCursorObjs = []; - for (let i = 0; i < 6; i++) { - const cursorObj = globalScene.add.image(0, 0, "select_cursor_highlight"); - cursorObj.setVisible(false); - cursorObj.setOrigin(0); - starterBoxContainer.add(cursorObj); - this.starterCursorObjs.push(cursorObj); - } - - this.cursorObj = globalScene.add.image(0, 0, "select_cursor").setOrigin(0); this.starterIconsCursorObj = globalScene.add .image(289, 64, "select_gen_cursor") .setName("starter-icons-cursor") .setVisible(false) .setOrigin(0); - starterBoxContainer.add(this.cursorObj); - - // TODO: Apply the same logic done in the pokedex to only have 81 containers whose sprites are cycled - for (const species of allSpecies) { - if (!speciesStarterCosts.hasOwnProperty(species.speciesId) || !species.isObtainable()) { - continue; - } - - starterSpecies.push(species.speciesId); - this.speciesLoaded.set(species.speciesId, false); - this.allSpecies.push(species); - - const starterContainer = new StarterContainer(species).setVisible(false); - this.iconAnimHandler.addOrUpdate(starterContainer.icon, PokemonIconAnimMode.NONE); - this.starterContainers.push(starterContainer); - starterBoxContainer.add(starterContainer); - } - this.starterIcons = []; for (let i = 0; i < 6; i++) { const icon = globalScene.add @@ -790,128 +595,34 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.starterIcons.push(icon); } - this.type1Icon = globalScene.add.sprite(8, 98, getLocalizedSpriteKey("types")).setScale(0.5).setOrigin(0); - - this.type2Icon = globalScene.add.sprite(26, 98, getLocalizedSpriteKey("types")).setScale(0.5).setOrigin(0); - - this.pokemonLuckLabelText = addTextObject(8, 89, i18next.t("common:luckIndicator"), TextStyle.WINDOW_ALT, { - fontSize: "56px", - }).setOrigin(0); - - this.pokemonLuckText = addTextObject( - 8 + this.pokemonLuckLabelText.displayWidth + 2, - 89, - "0", - TextStyle.LUCK_VALUE, - { fontSize: "56px" }, - ).setOrigin(0); - - // Candy icon and count - this.pokemonCandyContainer = globalScene.add - .container(4.5, 18) - .setInteractive(new Phaser.Geom.Rectangle(0, 0, 30, 20), Phaser.Geom.Rectangle.Contains); - this.pokemonCandyIcon = globalScene.add.sprite(0, 0, "candy").setScale(0.5).setOrigin(0); - this.pokemonCandyOverlayIcon = globalScene.add.sprite(0, 0, "candy_overlay").setScale(0.5).setOrigin(0); - this.pokemonCandyDarknessOverlay = globalScene.add - .sprite(0, 0, "candy") - .setScale(0.5) - .setOrigin(0) - .setTint(0x000000) - .setAlpha(0.5); - - this.pokemonCandyCountText = addTextObject(9.5, 0, "x0", TextStyle.WINDOW_ALT, { fontSize: "56px" }).setOrigin(0); - this.pokemonCandyContainer.add([ - this.pokemonCandyIcon, - this.pokemonCandyOverlayIcon, - this.pokemonCandyDarknessOverlay, - this.pokemonCandyCountText, + partyColumn.add([ + addWindow( + teamWindowX, + teamWindowY - randomSelectionWindowHeight, + teamWindowWidth, + randomSelectionWindowHeight, + true, + ), + addWindow(teamWindowX, teamWindowY, teamWindowWidth, teamWindowHeight), + addWindow(teamWindowX, teamWindowY + teamWindowHeight, teamWindowWidth, teamWindowWidth, true), + starterContainerWindow, + this.valueLimitLabel, + startLabel, + this.startCursorObj, + randomSelectLabel, + this.randomCursorObj, + this.starterIconsCursorObj, + ...this.starterIcons, ]); - this.pokemonFormText = addTextObject(6, 42, "Form", TextStyle.WINDOW_ALT, { - fontSize: "42px", - }).setOrigin(0); - - this.pokemonCaughtHatchedContainer = globalScene.add.container(2, 25).setScale(0.5); - - const pokemonCaughtIcon = globalScene.add.sprite(1, 0, "items", "pb").setOrigin(0).setScale(0.75); - - this.pokemonCaughtCountText = addTextObject(24, 4, "0", TextStyle.SUMMARY_ALT).setOrigin(0); - this.pokemonHatchedIcon = globalScene.add.sprite(1, 14, "egg_icons").setOrigin(0.15, 0.2).setScale(0.8); - this.pokemonShinyIcon = globalScene.add.sprite(14, 76, "shiny_icons").setOrigin(0.15, 0.2).setScale(1); - this.pokemonHatchedCountText = addTextObject(24, 19, "0", TextStyle.SUMMARY_ALT).setOrigin(0); - this.pokemonMovesContainer = globalScene.add.container(102, 16).setScale(0.375); - this.pokemonCaughtHatchedContainer.add([ - pokemonCaughtIcon, - this.pokemonCaughtCountText, - this.pokemonHatchedIcon, - this.pokemonShinyIcon, - this.pokemonHatchedCountText, - ]); - - for (let m = 0; m < 4; m++) { - const moveContainer = globalScene.add.container(0, 14 * m); - - const moveBg = globalScene.add.nineslice(0, 0, "type_bgs", "unknown", 92, 14, 2, 2, 2, 2); - moveBg.setOrigin(1, 0); - - const moveLabel = addTextObject(-moveBg.width / 2, 0, "-", TextStyle.MOVE_LABEL); - moveLabel.setOrigin(0.5, 0); - - this.pokemonMoveBgs.push(moveBg); - this.pokemonMoveLabels.push(moveLabel); - - moveContainer.add([moveBg, moveLabel]); - - this.pokemonMoveContainers.push(moveContainer); - this.pokemonMovesContainer.add(moveContainer); - } - - this.pokemonAdditionalMoveCountLabel = addTextObject( - -this.pokemonMoveBgs[0].width / 2, - 56, - "(+0)", - TextStyle.MOVE_LABEL, - ).setOrigin(0.5, 0); - - this.pokemonMovesContainer.add(this.pokemonAdditionalMoveCountLabel); - - this.pokemonEggMovesContainer = globalScene.add.container(102, 85).setScale(0.375); - - this.eggMovesLabel = addTextObject( - -46, - 0, - i18next.t("starterSelectUiHandler:eggMoves"), - TextStyle.WINDOW_ALT, - ).setOrigin(0.5, 0); - - this.pokemonEggMovesContainer.add(this.eggMovesLabel); - - for (let m = 0; m < 4; m++) { - const eggMoveContainer = globalScene.add.container(0, 16 + 14 * m); - - const eggMoveBg = globalScene.add.nineslice(0, 0, "type_bgs", "unknown", 92, 14, 2, 2, 2, 2); - eggMoveBg.setOrigin(1, 0); - - const eggMoveLabel = addTextObject(-eggMoveBg.width / 2, 0, "???", TextStyle.MOVE_LABEL); - eggMoveLabel.setOrigin(0.5, 0); - - this.pokemonEggMoveBgs.push(eggMoveBg); - this.pokemonEggMoveLabels.push(eggMoveLabel); - - eggMoveContainer.add([eggMoveBg, eggMoveLabel]); - - this.pokemonEggMoveContainers.push(eggMoveContainer); - - this.pokemonEggMovesContainer.add(eggMoveContainer); - } - - this.teraIcon = globalScene.add.sprite(85, 63, "button_tera").setName("terastallize-icon").setFrame("fire"); + return partyColumn; + } + setupInstructionButtons(): void { // The font size should be set per language + const textSettings = getStarterSelectTextSettings(); const instructionTextSize = textSettings.instructionTextSize; - this.instructionsContainer = globalScene.add.container(4, 156).setVisible(true); - const iRowX = this.instructionRowX; const iRowY = this.instructionRowY; const iRowTextX = iRowX + this.instructionRowTextOffset; @@ -1013,120 +724,6 @@ export class StarterSelectUiHandler extends MessageUiHandler { TextStyle.INSTRUCTIONS_TEXT, { fontSize: instructionTextSize }, ).setName("text-goFilter-label"); - - /** TODO: Uncomment this and update `this.hideInstructions` once our testing infra supports mocks of `Phaser.GameObject.Group` */ - /* - this.instructionElemGroup = globalScene.add.group([ - this.shinyIconElement, - this.shinyLabel, - this.formIconElement, - this.formLabel, - this.genderIconElement, - this.genderLabel, - this.abilityIconElement, - this.abilityLabel, - this.natureIconElement, - this.natureLabel, - this.teraIconElement, - this.teraLabel, - this.goFilterIconElement, - this.goFilterLabel, - ]); - */ - - this.hideInstructions(); - - this.filterInstructionsContainer = globalScene.add.container(50, 5).setVisible(true); - - this.starterSelectMessageBoxContainer = globalScene.add.container(0, sHeight).setVisible(false); - - this.starterSelectMessageBox = addWindow(1, -1, 318, 28).setOrigin(0, 1); - this.starterSelectMessageBoxContainer.add(this.starterSelectMessageBox); - - this.message = addTextObject(8, 8, "", TextStyle.WINDOW, { maxLines: 2 }).setOrigin(0); - this.starterSelectMessageBoxContainer.add(this.message); - - // arrow icon for the message box - this.initPromptSprite(this.starterSelectMessageBoxContainer); - - this.statsContainer = new StatsContainer(6, 16).setVisible(false); - - globalScene.add.existing(this.statsContainer); - - // add the info overlay last to be the top most ui element and prevent the IVs from overlaying this - this.moveInfoOverlay = new MoveInfoOverlay({ - top: true, - x: 1, - y: globalScene.scaledCanvas.height - MoveInfoOverlay.getHeight() - 29, - }); - - this.starterSelectContainer.add([ - bgColor, - starterSelectBg, - starterDexNoLabel, - this.shinyOverlay, - starterContainerBg, - addWindow( - teamWindowX, - teamWindowY - randomSelectionWindowHeight, - teamWindowWidth, - randomSelectionWindowHeight, - true, - ), - addWindow(teamWindowX, teamWindowY, teamWindowWidth, teamWindowHeight), - addWindow(teamWindowX, teamWindowY + teamWindowHeight, teamWindowWidth, teamWindowWidth, true), - starterContainerWindow, - this.pokemonSprite, - this.pokemonNumberText, - this.pokemonNameText, - this.pokemonGrowthRateLabelText, - this.pokemonGrowthRateText, - this.pokemonGenderText, - this.pokemonUncaughtText, - this.pokemonAbilityLabelText, - this.pokemonAbilityText, - this.pokemonPassiveLabelText, - this.pokemonPassiveText, - this.pokemonPassiveDisabledIcon, - this.pokemonPassiveLockedIcon, - this.pokemonNatureLabelText, - this.pokemonNatureText, - this.valueLimitLabel, - startLabel, - this.startCursorObj, - randomSelectLabel, - this.randomCursorObj, - this.starterIconsCursorObj, - starterBoxContainer, - ...this.starterIcons, - this.type1Icon, - this.type2Icon, - this.pokemonLuckLabelText, - this.pokemonLuckText, - this.pokemonCandyContainer, - this.pokemonFormText, - this.pokemonCaughtHatchedContainer, - this.pokemonMovesContainer, - this.pokemonEggMovesContainer, - this.teraIcon, - this.instructionsContainer, - this.filterInstructionsContainer, - this.starterSelectMessageBoxContainer, - this.statsContainer, - this.moveInfoOverlay, - // Filter bar sits above everything, except the tutorial overlay and message box. - // Do not put anything below this unless it must appear below the filter bar. - this.filterBarContainer, - ]); - - this.initTutorialOverlay(this.starterSelectContainer); - this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer); - - globalScene.eventTarget.addEventListener(BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED, e => - this.onCandyUpgradeDisplayChanged(e), - ); - - this.updateInstructions(); } show(args: any[]): boolean { @@ -1145,38 +742,23 @@ export class StarterSelectUiHandler extends MessageUiHandler { // Deep copy the JSON (avoid re-loading from disk) this.originalStarterPreferences = deepCopy(this.starterPreferences); - this.allSpecies.forEach((species, s) => { - const icon = this.starterContainers[s].icon; - const { dexEntry } = this.getSpeciesData(species.speciesId); - - // Initialize the StarterAttributes for this species + this.allStarterSpecies.forEach(species => { + // Initialize the StarterPreferences for this species this.starterPreferences[species.speciesId] = this.initStarterPrefs(species, this.starterPreferences); this.originalStarterPreferences[species.speciesId] = this.initStarterPrefs( species, this.originalStarterPreferences, true, ); + }); - if (dexEntry.caughtAttr) { - icon.clearTint(); - } else if (dexEntry.seenAttr) { - icon.setTint(0x808080); - } - + this.starterContainers.forEach(container => { + const icon = container.icon; + const species = container.species; this.setUpgradeAnimation(icon, species); }); - const notFreshStart = !globalScene.gameMode.hasChallenge(Challenges.FRESH_START); - - for (const container of this.pokemonEggMoveContainers) { - container.setVisible(notFreshStart); - } - this.eggMovesLabel.setVisible(notFreshStart); - // This is not enough, we need individual checks in setStarterSpecies too! :) - this.pokemonPassiveDisabledIcon.setVisible(notFreshStart); - this.pokemonPassiveLabelText.setVisible(notFreshStart); - this.pokemonPassiveLockedIcon.setVisible(notFreshStart); - this.pokemonPassiveText.setVisible(notFreshStart); + this.starterSummary.applyChallengeVisibility(); this.resetFilters(); this.updateStarters(); @@ -1200,20 +782,20 @@ export class StarterSelectUiHandler extends MessageUiHandler { * 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 + * @returns StarterPreferences for the species */ initStarterPrefs( species: PokemonSpecies, - preferences: StarterPreferences, + preferences: AllStarterPreferences, ignoreChallenge = false, - ): StarterAttributes { + ): StarterPreferences { // if preferences for the species is undefined, set it to an empty object preferences[species.speciesId] ??= {}; - const starterAttributes = preferences[species.speciesId]; - const { dexEntry, starterDataEntry: starterData } = this.getSpeciesData(species.speciesId, !ignoreChallenge); + const starterPreferences = preferences[species.speciesId]; + const { dexEntry, starterDataEntry: starterData } = getSpeciesData(species.speciesId, !ignoreChallenge); // no preferences or Pokemon wasn't caught, return empty attribute - if (!starterAttributes || !dexEntry.caughtAttr) { + if (!starterPreferences || !dexEntry.caughtAttr) { return {}; } @@ -1221,40 +803,40 @@ export class StarterSelectUiHandler extends MessageUiHandler { const hasShiny = caughtAttr & DexAttr.SHINY; const hasNonShiny = caughtAttr & DexAttr.NON_SHINY; - if (starterAttributes.shiny && !hasShiny) { + if (starterPreferences.shiny && !hasShiny) { // shiny form wasn't unlocked, purging shiny and variant setting - starterAttributes.shiny = undefined; - starterAttributes.variant = undefined; - } else if (starterAttributes.shiny === false && !hasNonShiny) { + starterPreferences.shiny = undefined; + starterPreferences.variant = undefined; + } else if (starterPreferences.shiny === false && !hasNonShiny) { // non shiny form wasn't unlocked, purging shiny setting - starterAttributes.shiny = undefined; + starterPreferences.shiny = undefined; } - if (starterAttributes.variant !== undefined) { + if (starterPreferences.variant !== undefined) { const unlockedVariants = [ hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT, hasShiny && caughtAttr & DexAttr.VARIANT_2, hasShiny && caughtAttr & DexAttr.VARIANT_3, ]; if ( - Number.isNaN(starterAttributes.variant) - || starterAttributes.variant < 0 - || !unlockedVariants[starterAttributes.variant] + Number.isNaN(starterPreferences.variant) + || starterPreferences.variant < 0 + || !unlockedVariants[starterPreferences.variant] ) { // variant value is invalid or requested variant wasn't unlocked, purging setting - starterAttributes.variant = undefined; + starterPreferences.variant = undefined; } } if ( - starterAttributes.female !== undefined - && !(starterAttributes.female ? caughtAttr & DexAttr.FEMALE : caughtAttr & DexAttr.MALE) + starterPreferences.female !== undefined + && !(starterPreferences.female ? caughtAttr & DexAttr.FEMALE : caughtAttr & DexAttr.MALE) ) { // requested gender wasn't unlocked, purging setting - starterAttributes.female = undefined; + starterPreferences.female = undefined; } - if (starterAttributes.ability !== undefined) { + if (starterPreferences.abilityIndex !== undefined) { const speciesHasSingleAbility = species.ability2 === species.ability1; const abilityAttr = starterData.abilityAttr; const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1; @@ -1267,42 +849,42 @@ export class StarterSelectUiHandler extends MessageUiHandler { speciesHasSingleAbility ? hasAbility2 && !hasAbility1 : hasAbility2, hasHiddenAbility, ]; - if (!unlockedAbilities[starterAttributes.ability]) { + if (!unlockedAbilities[starterPreferences.abilityIndex]) { // requested ability wasn't unlocked, purging setting - starterAttributes.ability = undefined; + starterPreferences.abilityIndex = undefined; } } - const selectedForm = starterAttributes.form; + const selectedForm = starterPreferences.formIndex; if ( selectedForm !== undefined && (!species.forms[selectedForm]?.isStarterSelectable || !(caughtAttr & globalScene.gameData.getFormAttr(selectedForm))) ) { // requested form wasn't unlocked/isn't a starter form, purging setting - starterAttributes.form = undefined; + starterPreferences.formIndex = undefined; } - if (starterAttributes.nature !== undefined) { + if (starterPreferences.nature !== undefined) { const unlockedNatures = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr); - if (unlockedNatures.indexOf(starterAttributes.nature as unknown as Nature) < 0) { + if (unlockedNatures.indexOf(starterPreferences.nature as unknown as Nature) < 0) { // requested nature wasn't unlocked, purging setting - starterAttributes.nature = undefined; + starterPreferences.nature = undefined; } } - if (starterAttributes.tera !== undefined) { + if (starterPreferences.tera !== undefined) { // If somehow we have an illegal tera type, it is reset here - if (!(starterAttributes.tera === species.type1 || starterAttributes.tera === species?.type2)) { - starterAttributes.tera = species.type1; + if (!(starterPreferences.tera === species.type1 || starterPreferences.tera === species?.type2)) { + starterPreferences.tera = species.type1; } // In fresh start challenge, the tera type is always reset to the first one if (globalScene.gameMode.hasChallenge(Challenges.FRESH_START) && !ignoreChallenge) { - starterAttributes.tera = species.type1; + starterPreferences.tera = species.type1; } } - return starterAttributes; + return starterPreferences; } /** @@ -1358,63 +940,6 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.starterSelectMessageBoxContainer.setVisible(text?.length > 0); } - /** - * 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; - } - - /** - * Determines if a passive upgrade is available for the given species ID - * @param speciesId The ID of the species to check the passive of - * @returns true if the user has enough candies and a passive has not been unlocked already - */ - isPassiveAvailable(speciesId: number): boolean { - // Get this species ID's starter data - const starterData = globalScene.gameData.starterData[speciesId]; - - return ( - starterData.candyCount >= getPassiveCandyCount(speciesStarterCosts[speciesId]) - && !(starterData.passiveAttr & PassiveAttr.UNLOCKED) - ); - } - - /** - * Determines if a value reduction upgrade is available for the given species ID - * @param speciesId The ID of the species to check the value reduction of - * @returns true if the user has enough candies and all value reductions have not been unlocked already - */ - isValueReductionAvailable(speciesId: number): boolean { - // Get this species ID's starter data - const starterData = globalScene.gameData.starterData[speciesId]; - - return ( - starterData.candyCount >= getValueReductionCandyCounts(speciesStarterCosts[speciesId])[starterData.valueReduction] - && starterData.valueReduction < valueReductionMax - ); - } - - /** - * Determines if an same species egg can be bought for the given species ID - * @param speciesId The ID of the species to check the value reduction of - * @returns true if the user has enough candies - */ - isSameSpeciesEggAvailable(speciesId: number): boolean { - // Get this species ID's starter data - const starterData = globalScene.gameData.starterData[speciesId]; - - return starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[speciesId]); - } - /** * Sets a bounce animation if enabled and the Pokemon has an upgrade * @param icon {@linkcode Phaser.GameObjects.GameObject} to animate @@ -1456,9 +981,9 @@ export class StarterSelectUiHandler extends MessageUiHandler { }; if ( - this.isPassiveAvailable(species.speciesId) + isPassiveAvailable(species.speciesId) || (globalScene.candyUpgradeNotification === 2 - && (this.isValueReductionAvailable(species.speciesId) || this.isSameSpeciesEggAvailable(species.speciesId))) + && (isValueReductionAvailable(species.speciesId) || isSameSpeciesEggAvailable(species.speciesId))) ) { const chain = globalScene.tweens.chain(tweenChain); if (!startPaused) { @@ -1484,19 +1009,19 @@ export class StarterSelectUiHandler extends MessageUiHandler { return; } - const isPassiveAvailable = this.isPassiveAvailable(species.speciesId); - const isValueReductionAvailable = this.isValueReductionAvailable(species.speciesId); - const isSameSpeciesEggAvailable = this.isSameSpeciesEggAvailable(species.speciesId); + const passiveAvailable = isPassiveAvailable(species.speciesId); + const valueReductionAvailable = isValueReductionAvailable(species.speciesId); + const sameSpeciesEggAvailable = isSameSpeciesEggAvailable(species.speciesId); // 'Passive Only' mode if (globalScene.candyUpgradeNotification === 1) { - starter.candyUpgradeIcon.setVisible(slotVisible && isPassiveAvailable); + starter.candyUpgradeIcon.setVisible(slotVisible && passiveAvailable); starter.candyUpgradeOverlayIcon.setVisible(slotVisible && starter.candyUpgradeIcon.visible); // 'On' mode } else if (globalScene.candyUpgradeNotification === 2) { starter.candyUpgradeIcon.setVisible( - slotVisible && (isPassiveAvailable || isValueReductionAvailable || isSameSpeciesEggAvailable), + slotVisible && (passiveAvailable || valueReductionAvailable || sameSpeciesEggAvailable), ); starter.candyUpgradeOverlayIcon.setVisible(slotVisible && starter.candyUpgradeIcon.visible); } @@ -1507,10 +1032,10 @@ export class StarterSelectUiHandler extends MessageUiHandler { * @param starterContainer the container for the Pokemon to update */ updateCandyUpgradeDisplay(starterContainer: StarterContainer) { - if (this.isUpgradeIconEnabled()) { + if (isUpgradeIconEnabled()) { this.setUpgradeIcon(starterContainer); } - if (this.isUpgradeAnimationEnabled()) { + if (isUpgradeAnimationEnabled()) { this.setUpgradeAnimation(starterContainer.icon, this.lastSpecies, true); } } @@ -1527,18 +1052,1058 @@ export class StarterSelectUiHandler extends MessageUiHandler { // Loop through all visible candy icons when set to 'Icon' mode if (globalScene.candyUpgradeDisplay === 0) { - this.filteredStarterContainers.forEach(starter => { - this.setUpgradeIcon(starter); + this.filteredStarterIds.forEach((_, i) => { + this.setUpgradeIcon(this.starterContainers[i]); }); return; } // Loop through all animations when set to 'Animation' mode - this.filteredStarterContainers.forEach((starter, s) => { - const icon = this.filteredStarterContainers[s].icon; + this.filteredStarterIds.forEach((id, i) => { + const icon = this.starterContainers[i].icon; - this.setUpgradeAnimation(icon, starter.species); + this.setUpgradeAnimation(icon, getPokemonSpecies(id)); + }); + } + + showRandomCursor() { + this.randomCursorObj.setVisible(true); + this.setNoSpecies(); + } + + toBoxCursor(cursor: number) { + const numberOfStarters = this.filteredStarterIds.length; + const numOfRows = Math.ceil(numberOfStarters / COLUMNS); + return numOfRows < ROWS ? cursor : cursor - (numOfRows - ROWS) * COLUMNS; + } + + processFilterModeInput(button: Button) { + let success = false; + + const numberOfStarters = this.filteredStarterIds.length; + const numOfRows = Math.ceil(numberOfStarters / COLUMNS); + + switch (button) { + case Button.CANCEL: + if (this.filterBar.openDropDown) { + // CANCEL with a filter menu open > close it + this.filterBar.toggleDropDown(this.filterBarCursor); + success = true; + } else if (!this.filterBar.getFilter(this.filterBar.getColumn(this.filterBarCursor)).hasDefaultValues()) { + if (this.filterBar.getColumn(this.filterBarCursor) === DropDownColumn.CAUGHT) { + this.resetCaughtDropdown(); + } else { + this.filterBar.resetSelection(this.filterBarCursor); + } + this.updateStarters(); + success = true; + } else if (this.statsMode) { + this.toggleStatsMode(false); + success = true; + } else if (this.starterSpecies.length > 0) { + this.popStarter(this.starterSpecies.length - 1); + success = true; + this.updateInstructions(); + } else { + this.tryExit(); + success = true; + } + break; + case Button.LEFT: + if (this.filterBarCursor > 0) { + success = this.setCursor(this.filterBarCursor - 1); + } else { + success = this.setCursor(this.filterBar.numFilters - 1); + } + break; + case Button.RIGHT: + if (this.filterBarCursor < this.filterBar.numFilters - 1) { + success = this.setCursor(this.filterBarCursor + 1); + } else { + success = this.setCursor(0); + } + break; + case Button.UP: + if (this.filterBar.openDropDown) { + success = this.filterBar.decDropDownCursor(); + } else if (this.filterBarCursor === this.filterBar.numFilters - 1) { + // UP from the last filter, move to start button + this.setFilterMode(false); + this.cursorObj.setVisible(false); + if (this.starterSpecies.length > 0) { + this.startCursorObj.setVisible(true); + } else { + this.showRandomCursor(); + } + success = true; + } else if (numberOfStarters > 0) { + // UP from filter bar to bottom of Pokemon list + this.setFilterMode(false); + this.scrollCursor = Math.max(0, numOfRows - 9); + this.updateScroll(); + const proportion = (this.filterBarCursor + 0.5) / this.filterBar.numFilters; + const targetCol = Math.min(8, Math.floor(proportion * 11)); + if (numberOfStarters % 9 > targetCol) { + this.setCursor(numberOfStarters - (numberOfStarters % 9) + targetCol - this.scrollCursor * 9); + } else { + this.setCursor( + Math.max(numberOfStarters - (numberOfStarters % 9) + targetCol - 9 - this.scrollCursor * 9, 0), + ); + } + success = true; + } + break; + case Button.DOWN: + if (this.filterBar.openDropDown) { + success = this.filterBar.incDropDownCursor(); + } else if (this.filterBarCursor === this.filterBar.numFilters - 1) { + // DOWN from the last filter, move to random selection label + this.setFilterMode(false); + this.cursorObj.setVisible(false); + this.showRandomCursor(); + success = true; + } else if (numberOfStarters > 0) { + // DOWN from filter bar to top of Pokemon list + this.setFilterMode(false); + this.scrollCursor = 0; + this.updateScroll(); + const proportion = this.filterBarCursor / Math.max(1, this.filterBar.numFilters - 1); + const targetCol = Math.min(8, Math.floor(proportion * 11)); + this.setCursor(Math.min(targetCol, numberOfStarters - 1)); + success = true; + } + break; + case Button.ACTION: + if (!this.filterBar.openDropDown) { + this.filterBar.toggleDropDown(this.filterBarCursor); + } else { + this.filterBar.toggleOptionState(); + } + success = true; + break; + } + + return success; + } + + processStartCursorInput(button: Button) { + let success = false; + let error = false; + + const numberOfStarters = this.filteredStarterIds.length; + const onScreenFirstIndex = this.scrollCursor * COLUMNS; + const onScreenLastIndex = Math.min(this.filteredStarterIds.length - onScreenFirstIndex - 1, ROWS * COLUMNS - 1); // this is the last starter index on the screen + const onScreenNumberOfRows = Math.ceil(onScreenLastIndex / COLUMNS); + + switch (button) { + case Button.ACTION: + if (this.tryStart(true)) { + success = true; + } else { + error = true; + } + break; + case Button.UP: + // UP from start button: go to pokemon in team if any, otherwise filter + this.startCursorObj.setVisible(false); + if (this.starterSpecies.length > 0) { + this.starterIconsCursorIndex = this.starterSpecies.length - 1; + this.moveStarterIconsCursor(this.starterIconsCursorIndex); + } else { + // TODO: how can we get here if start button can't be selected? this appears to be redundant + this.startCursorObj.setVisible(false); + this.showRandomCursor(); + this.setNoSpecies(); + } + success = true; + break; + case Button.DOWN: + // DOWN from start button: Go to filters + this.startCursorObj.setVisible(false); + this.filterBarCursor = Math.max(1, this.filterBar.numFilters - 1); + this.setFilterMode(true); + success = true; + break; + case Button.LEFT: + if (numberOfStarters > 0) { + this.startCursorObj.setVisible(false); + this.cursorObj.setVisible(true); + this.setCursor(onScreenLastIndex); // set last column + success = true; + } + break; + case Button.RIGHT: + if (numberOfStarters > 0) { + this.startCursorObj.setVisible(false); + this.cursorObj.setVisible(true); + this.setCursor((onScreenNumberOfRows - 1) * 9); // set first column + success = true; + } + break; + } + + return [success, error]; + } + + processRandomCursorInput(button: Button) { + let success = false; + let error = false; + + const numberOfStarters = this.filteredStarterIds.length; + + switch (button) { + case Button.ACTION: { + if (this.starterSpecies.length >= 6) { + error = true; + break; + } + const currentPartyValue = this.starterSpecies + .map(s => s.generation) + .reduce( + (total: number, _gen: number, i: number) => + total + globalScene.gameData.getSpeciesStarterValue(this.starterSpecies[i].speciesId), + 0, + ); + // Filter valid starters + const validStarters = this.filteredStarterIds.filter(speciesId => { + const species = getPokemonSpecies(speciesId); + const [isDupe] = this.isInParty(species); + const starterCost = globalScene.gameData.getSpeciesStarterValue(species.speciesId); + const isValidForChallenge = checkStarterValidForChallenge( + species, + this.getSpeciesPropsFromPreferences(species), + this.isPartyValid(), + ); + const isCaught = getSpeciesData(species.speciesId).dexEntry.caughtAttr; + return !isDupe && isValidForChallenge && currentPartyValue + starterCost <= getRunValueLimit() && isCaught; + }); + if (validStarters.length === 0) { + error = true; // No valid starters available + break; + } + // Select random starter + const randomStarterId = validStarters[Math.floor(Math.random() * validStarters.length)]; + const randomSpecies = getPokemonSpecies(randomStarterId); + // Set species and prepare attributes + this.setSpecies(randomSpecies); + // TODO: this might not be needed if we change .addToParty + const dexAttr = getDexAttrFromPreferences( + randomSpecies.speciesId, + this.starterPreferences[randomSpecies.speciesId], + ); + const props = this.getSpeciesPropsFromPreferences(randomSpecies); + const abilityIndex = this.abilityCursor; + const nature = this.natureCursor as unknown as Nature; + const teraType = this.teraCursor; + const moveset = this.starterMoveset?.slice(0) as StarterMoveset; + const starterCost = globalScene.gameData.getSpeciesStarterValue(randomSpecies.speciesId); + const speciesForm = getPokemonSpeciesForm(randomSpecies.speciesId, props.formIndex); + // Load assets and add to party + speciesForm.loadAssets(props.female, props.formIndex, props.shiny, props.variant, true).then(() => { + if (this.tryUpdateValue(starterCost, true)) { + this.addToParty(randomSpecies, dexAttr, abilityIndex, nature, moveset, teraType); + this.getUi().playSelect(); + } + }); + break; + } + case Button.UP: + this.randomCursorObj.setVisible(false); + this.filterBarCursor = this.filterBar.numFilters - 1; + this.setFilterMode(true); + success = true; + break; + case Button.DOWN: + this.randomCursorObj.setVisible(false); + if (this.starterSpecies.length > 0) { + this.starterIconsCursorIndex = 0; + this.moveStarterIconsCursor(this.starterIconsCursorIndex); + } else { + this.filterBarCursor = this.filterBar.numFilters - 1; + this.setFilterMode(true); + } + success = true; + break; + case Button.LEFT: + if (numberOfStarters > 0) { + this.randomCursorObj.setVisible(false); + this.cursorObj.setVisible(true); + this.setCursor(Math.min(8, numberOfStarters - 1)); // set last column + success = true; + } + break; + case Button.RIGHT: + if (numberOfStarters > 0) { + this.randomCursorObj.setVisible(false); + this.cursorObj.setVisible(true); + this.setCursor(0); // set first column + success = true; + } + break; + } + + return [success, error]; + } + + processCycleButtonsInput(button: Button) { + let success = false; + + const props = this.getSpeciesPropsFromPreferences(this.lastSpecies); + + const speciesId = this.lastSpecies.speciesId; + + const starterPreferences = (this.starterPreferences[this.lastSpecies.speciesId] ??= {}); + + switch (button) { + case Button.CYCLE_SHINY: + if (this.canCycleShiny) { + console.log(starterPreferences); + if (starterPreferences.shiny === false) { + // If not shiny, we change to shiny and get the proper default variant + const newVariant = (starterPreferences.variant as Variant) ?? props.variant; + this.setShinyAndVariant(speciesId, true, newVariant); + globalScene.playSound("se/sparkle"); + } else { + // If shiny, we update the variant + let newVariant = starterPreferences.variant ?? props.variant; + do { + newVariant = (newVariant + 1) % 3; + if (newVariant === 0) { + if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.DEFAULT_VARIANT) { + // TODO: is this bang correct? + break; + } + } else if (newVariant === 1) { + if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_2) { + // TODO: is this bang correct? + break; + } + } else if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_3) { + // TODO: is this bang correct? + break; + } + } while (newVariant !== props.variant); + this.setShinyAndVariant(speciesId, true, newVariant); + if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.NON_SHINY && newVariant <= props.variant) { + // If we have run out of variants, go back to non shiny + this.setShinyAndVariant(speciesId, false, newVariant); + success = true; + } else { + // If going to a higher variant, or only shiny forms are caught, go to next variant + success = true; + } + } + console.log("AFTER", starterPreferences); + } + break; + case Button.CYCLE_FORM: + if (this.canCycleForm) { + const formCount = this.lastSpecies.forms.length; + let newFormIndex = props.formIndex; + do { + newFormIndex = (newFormIndex + 1) % formCount; + if ( + this.lastSpecies.forms[newFormIndex].isStarterSelectable + && this.speciesStarterDexEntry!.caughtAttr! & globalScene.gameData.getFormAttr(newFormIndex) + ) { + // TODO: are those bangs correct? + break; + } + } while (newFormIndex !== props.formIndex); + this.setNewFormIndex(speciesId, newFormIndex); + success = true; + } + break; + case Button.CYCLE_GENDER: + if (this.canCycleGender) { + this.setNewGender(speciesId, !starterPreferences.female); + success = true; + } + break; + case Button.CYCLE_ABILITY: + if (this.canCycleAbility) { + const abilityCount = this.lastSpecies.getAbilityCount(); + const abilityAttr = getSpeciesData(this.lastSpecies.speciesId).starterDataEntry.abilityAttr; + const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1; + let newAbilityIndex = this.abilityCursor; + do { + newAbilityIndex = (newAbilityIndex + 1) % abilityCount; + if (newAbilityIndex === 0) { + if (hasAbility1) { + break; + } + } else if (newAbilityIndex === 1) { + // If ability 1 and 2 are the same and ability 1 is unlocked, skip over ability 2 + if (this.lastSpecies.ability1 === this.lastSpecies.ability2 && hasAbility1) { + newAbilityIndex = (newAbilityIndex + 1) % abilityCount; + } + break; + } else if (abilityAttr & AbilityAttr.ABILITY_HIDDEN) { + break; + } + } while (newAbilityIndex !== this.abilityCursor); + this.setNewAbilityIndex(speciesId, newAbilityIndex); + success = true; + } + break; + case Button.CYCLE_NATURE: + if (this.canCycleNature) { + const natures = globalScene.gameData.getNaturesForAttr(this.speciesStarterDexEntry?.natureAttr); + const natureIndex = natures.indexOf(this.natureCursor); + const newNature = natures[natureIndex < natures.length - 1 ? natureIndex + 1 : 0]; + // store cycled nature as default + this.setNewNature(speciesId, newNature); + success = true; + } + break; + case Button.CYCLE_TERA: + if (this.canCycleTera) { + const speciesForm = getPokemonSpeciesForm(this.lastSpecies.speciesId, starterPreferences.formIndex ?? 0); + const newTera = + speciesForm.type1 === this.teraCursor && speciesForm.type2 != null ? speciesForm.type2 : speciesForm.type1; + this.setNewTeraType(speciesId, newTera); + success = true; + } + break; + } + + this.setSpeciesDetails(this.lastSpecies); + + return success; + } + + setShinyAndVariant(speciesId: SpeciesId, shiny: boolean, variant: number) { + (this.starterPreferences[speciesId] ??= {}).shiny = shiny; + (this.originalStarterPreferences[speciesId] ??= {}).shiny = shiny; + (this.starterPreferences[speciesId] ??= {}).variant = variant; + (this.originalStarterPreferences[speciesId] ??= {}).variant = variant; + } + + setNewFormIndex(speciesId: SpeciesId, formIndex: number) { + (this.starterPreferences[speciesId] ??= {}).formIndex = formIndex; + (this.originalStarterPreferences[speciesId] ??= {}).formIndex = formIndex; + // Updating tera type for new form + this.setNewTeraType(speciesId, this.lastSpecies.forms[formIndex].type1); + // Updating gender for gendered forms + if (getPokemonSpecies[speciesId]?.forms?.find(f => f.formKey === "female")) { + const newFemale = formIndex === 1; + if (this.starterPreferences[speciesId].female !== newFemale) { + this.setNewGender(speciesId, newFemale); + } + } + } + + setNewGender(speciesId: SpeciesId, female: boolean) { + (this.starterPreferences[speciesId] ??= {}).female = female; + (this.originalStarterPreferences[speciesId] ??= {}).female = female; + // Updating form for gendered forms + if (getPokemonSpecies[speciesId]?.forms?.find(f => f.formKey === "female")) { + const newFormIndex = female ? 1 : 0; + if (this.starterPreferences[speciesId].formIndex !== newFormIndex) { + this.setNewFormIndex(speciesId, newFormIndex); + } + } + } + + setNewAbilityIndex(speciesId: SpeciesId, abilityIndex: number) { + (this.starterPreferences[speciesId] ??= {}).abilityIndex = abilityIndex; + (this.originalStarterPreferences[speciesId] ??= {}).abilityIndex = abilityIndex; + } + + setNewNature(speciesId: SpeciesId, nature: number) { + (this.starterPreferences[speciesId] ??= {}).nature = nature; + (this.originalStarterPreferences[speciesId] ??= {}).nature = nature; + } + + setNewTeraType(speciesId: SpeciesId, teraType: PokemonType) { + (this.starterPreferences[speciesId] ??= {}).tera = teraType; + (this.originalStarterPreferences[speciesId] ??= {}).tera = teraType; + } + + processPartyIconInput(button: Button) { + let success = false; + + const numberOfStarters = this.filteredStarterIds.length; + const onScreenLastIndex = Math.min(this.filteredStarterIds.length - 1, ROWS * COLUMNS - 1); // this is the last starter index on the screen + + switch (button) { + case Button.UP: + if (this.starterIconsCursorIndex === 0) { + // Up from first Pokemon in the team > go to Random selection + this.starterIconsCursorObj.setVisible(false); + this.showRandomCursor(); + } else { + this.starterIconsCursorIndex--; + this.moveStarterIconsCursor(this.starterIconsCursorIndex); + } + success = true; + break; + case Button.DOWN: + if (this.starterIconsCursorIndex <= this.starterSpecies.length - 2) { + this.starterIconsCursorIndex++; + this.moveStarterIconsCursor(this.starterIconsCursorIndex); + } else { + this.starterIconsCursorObj.setVisible(false); + this.setNoSpecies(); + this.startCursorObj.setVisible(true); + } + success = true; + break; + case Button.LEFT: + if (numberOfStarters > 0) { + // LEFT from team > Go to closest filtered Pokemon + const closestRowIndex = this.starterIconsCursorIndex + 1; + this.starterIconsCursorObj.setVisible(false); + this.cursorObj.setVisible(true); + this.setCursor(Math.min(closestRowIndex * 9 + 8, onScreenLastIndex)); + success = true; + } else { + // LEFT from team and no Pokemon in filter > do nothing + success = false; + } + break; + case Button.RIGHT: + if (numberOfStarters > 0) { + // RIGHT from team > Go to closest filtered Pokemon + const closestRowIndex = this.starterIconsCursorIndex + 1; + this.starterIconsCursorObj.setVisible(false); + this.cursorObj.setVisible(true); + this.setCursor(Math.min(closestRowIndex * 9, onScreenLastIndex - (onScreenLastIndex % 9))); + success = true; + } else { + // RIGHT from team and no Pokemon in filter > do nothing + success = false; + } + break; + } + + return success; + } + + openPokemonMenu() { + const ui = this.getUi(); + let options: any[] = []; // TODO: add proper type + + let starterContainer: StarterContainer; + // The temporary, duplicated starter data to show info + const starterData = getSpeciesData(this.lastSpecies.speciesId).starterDataEntry; + // The persistent starter data to apply e.g. candy upgrades + const persistentStarterData = globalScene.gameData.starterData[this.lastSpecies.speciesId]; + // The sanitized starter preferences + const starterPreferences = (this.starterPreferences[this.lastSpecies.speciesId] ??= {}); + // The original starter preferences + const originalStarterPreferences = (this.originalStarterPreferences[this.lastSpecies.speciesId] ??= {}); + + // this gets the correct pokemon cursor depending on whether you're in the starter screen or the party icons + if (!this.starterIconsCursorObj.visible) { + starterContainer = this.starterContainers[this.cursor]; + } else { + // if species is in filtered starters, get the starter container from the filtered starters, it can be undefined if the species is not in the filtered starters + starterContainer = + this.starterContainers[this.starterContainers.findIndex(container => container.species === this.lastSpecies)]; + } + + const [isDupe, removeIndex]: [boolean, number] = this.isInParty(this.lastSpecies); + + const isPartyValid = this.isPartyValid(); + const isValidForChallenge = checkStarterValidForChallenge( + this.lastSpecies, + this.getSpeciesPropsFromPreferences(this.lastSpecies), + isPartyValid, + ); + + const currentPartyValue = this.starterSpecies + .map(s => s.generation) + .reduce( + (total: number, _gen: number, i: number) => + (total += globalScene.gameData.getSpeciesStarterValue(this.starterSpecies[i].speciesId)), + 0, + ); + const newCost = globalScene.gameData.getSpeciesStarterValue(this.lastSpecies.speciesId); + if ( + !isDupe + && isValidForChallenge + && currentPartyValue + newCost <= getRunValueLimit() + && this.starterSpecies.length < PLAYER_PARTY_MAX_SIZE + ) { + options = [ + { + label: i18next.t("starterSelectUiHandler:addToParty"), + handler: () => { + ui.setMode(UiMode.STARTER_SELECT); + const isOverValueLimit = this.tryUpdateValue( + globalScene.gameData.getSpeciesStarterValue(this.lastSpecies.speciesId), + true, + ); + if (!isDupe && isValidForChallenge && isOverValueLimit) { + this.starterCursorObjs[this.starterSpecies.length] + .setVisible(true) + .setPosition(this.cursorObj.x, this.cursorObj.y); + this.addToParty( + this.lastSpecies, + this.dexAttrCursor, + this.abilityCursor, + this.natureCursor as unknown as Nature, + this.starterMoveset?.slice(0) as StarterMoveset, + this.teraCursor, + ); + ui.playSelect(); + } else { + ui.playError(); // this should be redundant as there is now a trigger for when a pokemon can't be added to party + } + return true; + }, + overrideSound: true, + }, + ]; + } else if (isDupe) { + // if it already exists in your party, it will give you the option to remove from your party + options = [ + { + label: i18next.t("starterSelectUiHandler:removeFromParty"), + handler: () => { + this.popStarter(removeIndex); + ui.setMode(UiMode.STARTER_SELECT); + return true; + }, + }, + ]; + } + + options.push( + // this shows the IVs for the pokemon + { + label: i18next.t("starterSelectUiHandler:toggleIVs"), + handler: () => { + this.toggleStatsMode(); + ui.setMode(UiMode.STARTER_SELECT); + return true; + }, + }, + ); + if (this.speciesStarterMoves.length > 1) { + // this lets you change the pokemon moves + const showSwapOptions = (moveset: StarterMoveset) => { + this.blockInput = true; + + ui.setMode(UiMode.STARTER_SELECT).then(() => { + ui.showText(i18next.t("starterSelectUiHandler:selectMoveSwapOut"), null, () => { + this.moveInfoOverlay.show(allMoves[moveset[0]]); + + ui.setModeWithoutClear(UiMode.OPTION_SELECT, { + options: moveset + .map((m: MoveId, i: number) => { + const option: OptionSelectItem = { + label: allMoves[m].name, + handler: () => { + this.blockInput = true; + ui.setMode(UiMode.STARTER_SELECT).then(() => { + ui.showText( + `${i18next.t("starterSelectUiHandler:selectMoveSwapWith")} ${allMoves[m].name}.`, + null, + () => { + const possibleMoves = this.speciesStarterMoves.filter((sm: MoveId) => sm !== m); + this.moveInfoOverlay.show(allMoves[possibleMoves[0]]); + + ui.setModeWithoutClear(UiMode.OPTION_SELECT, { + options: possibleMoves + .map(sm => { + // make an option for each available starter move + const option = { + label: allMoves[sm].name, + handler: () => { + this.switchMoveHandler(i, sm, m); + showSwapOptions(this.starterMoveset!); // TODO: is this bang correct? + return true; + }, + onHover: () => { + this.moveInfoOverlay.show(allMoves[sm]); + }, + }; + return option; + }) + .concat({ + label: i18next.t("menu:cancel"), + handler: () => { + showSwapOptions(this.starterMoveset!); // TODO: is this bang correct? + return true; + }, + onHover: () => { + this.moveInfoOverlay.clear(); + }, + }), + supportHover: true, + maxOptions: 8, + yOffset: 19, + }); + this.blockInput = false; + }, + ); + }); + return true; + }, + onHover: () => { + this.moveInfoOverlay.show(allMoves[m]); + }, + }; + return option; + }) + .concat({ + label: i18next.t("menu:cancel"), + handler: () => { + this.moveInfoOverlay.clear(); + this.clearText(); + // Only saved if moves were actually swapped + if (this.hasSwappedMoves) { + globalScene.gameData.saveSystem().then(success => { + if (!success) { + return globalScene.reset(true); + } + }); + } + ui.setMode(UiMode.STARTER_SELECT); + return true; + }, + onHover: () => { + this.moveInfoOverlay.clear(); + }, + }), + supportHover: true, + maxOptions: 8, + yOffset: 19, + }); + this.blockInput = false; + }); + }); + }; + options.push({ + label: i18next.t("starterSelectUiHandler:manageMoves"), + handler: () => { + this.hasSwappedMoves = false; + showSwapOptions(this.starterMoveset!); // TODO: is this bang correct? + return true; + }, + }); + } + if (this.canCycleNature) { + // if we could cycle natures, enable the improved nature menu + const showNatureOptions = () => { + this.blockInput = true; + + ui.setMode(UiMode.STARTER_SELECT).then(() => { + ui.showText(i18next.t("starterSelectUiHandler:selectNature"), null, () => { + const natures = globalScene.gameData.getNaturesForAttr(this.speciesStarterDexEntry?.natureAttr); + ui.setModeWithoutClear(UiMode.OPTION_SELECT, { + options: natures + .map((n: Nature, _i: number) => { + const option: OptionSelectItem = { + label: getNatureName(n, true, true, true), + handler: () => { + this.setNewNature(this.lastSpecies.speciesId, n); + this.clearText(); + ui.setMode(UiMode.STARTER_SELECT); + // set nature for starter + this.setSpeciesDetails(this.lastSpecies); + this.blockInput = false; + return true; + }, + }; + return option; + }) + .concat({ + label: i18next.t("menu:cancel"), + handler: () => { + this.clearText(); + ui.setMode(UiMode.STARTER_SELECT); + this.blockInput = false; + return true; + }, + }), + maxOptions: 8, + yOffset: 19, + }); + }); + }); + }; + options.push({ + label: i18next.t("starterSelectUiHandler:manageNature"), + handler: () => { + showNatureOptions(); + return true; + }, + }); + } + + const passiveAttr = starterData.passiveAttr; + if (passiveAttr & PassiveAttr.UNLOCKED) { + // this is for enabling and disabling the passive + const label = i18next.t( + passiveAttr & PassiveAttr.ENABLED + ? "starterSelectUiHandler:disablePassive" + : "starterSelectUiHandler:enablePassive", + ); + options.push({ + label, + handler: () => { + starterData.passiveAttr ^= PassiveAttr.ENABLED; + persistentStarterData.passiveAttr ^= PassiveAttr.ENABLED; + ui.setMode(UiMode.STARTER_SELECT); + this.setSpeciesDetails(this.lastSpecies); + return true; + }, + }); + } + // if container.favorite is false, show the favorite option + const isFavorite = starterPreferences?.favorite ?? false; + if (!isFavorite) { + options.push({ + label: i18next.t("starterSelectUiHandler:addToFavorites"), + handler: () => { + starterPreferences.favorite = true; + originalStarterPreferences.favorite = true; + // if the starter container not exists, it means the species is not in the filtered starters + if (starterContainer) { + starterContainer.favoriteIcon.setVisible(starterPreferences.favorite); + } + ui.setMode(UiMode.STARTER_SELECT); + return true; + }, + }); + } else { + options.push({ + label: i18next.t("starterSelectUiHandler:removeFromFavorites"), + handler: () => { + starterPreferences.favorite = false; + originalStarterPreferences.favorite = false; + // if the starter container not exists, it means the species is not in the filtered starters + if (starterContainer) { + starterContainer.favoriteIcon.setVisible(starterPreferences.favorite); + } + ui.setMode(UiMode.STARTER_SELECT); + return true; + }, + }); + } + options.push({ + label: i18next.t("menu:rename"), + handler: () => { + ui.playSelect(); + let nickname = starterPreferences.nickname ? String(starterPreferences.nickname) : ""; + nickname = decodeURIComponent(escape(atob(nickname))); + ui.setModeWithoutClear( + UiMode.RENAME_POKEMON, + { + buttonActions: [ + (sanitizedName: string) => { + ui.playSelect(); + starterPreferences.nickname = sanitizedName; + originalStarterPreferences.nickname = sanitizedName; + const name = decodeURIComponent(escape(atob(starterPreferences.nickname))); + this.starterSummary.updateName(name.length > 0 ? name : this.lastSpecies.name); + ui.setMode(UiMode.STARTER_SELECT); + }, + () => { + ui.setMode(UiMode.STARTER_SELECT); + }, + ], + }, + nickname, + ); + return true; + }, + }); + + // Purchases with Candy + const candyCount = starterData.candyCount; + const showUseCandies = () => { + const options: any[] = []; // TODO: add proper type + + // Unlock passive option + if (!(passiveAttr & PassiveAttr.UNLOCKED) && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START)) { + const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.lastSpecies.speciesId]); + options.push({ + label: `×${passiveCost} ${i18next.t("starterSelectUiHandler:unlockPassive")}`, + handler: () => { + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= passiveCost) { + persistentStarterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED; + starterData.passiveAttr = persistentStarterData.passiveAttr; + if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { + persistentStarterData.candyCount -= passiveCost; + starterData.candyCount = persistentStarterData.candyCount; + } + this.starterSummary.updateCandyCount(starterData.candyCount); + globalScene.gameData.saveSystem().then(success => { + if (!success) { + return globalScene.reset(true); + } + }); + ui.setMode(UiMode.STARTER_SELECT); + this.setSpeciesDetails(this.lastSpecies); + globalScene.playSound("se/buy"); + + // update the passive background and icon/animation for available upgrade + if (starterContainer) { + this.updateCandyUpgradeDisplay(starterContainer); + starterContainer.starterPassiveBgs.setVisible(!!starterData.passiveAttr); + } + return true; + } + return false; + }, + item: "candy", + itemArgs: starterColors[this.lastSpecies.speciesId], + }); + } + + // Reduce cost option + const valueReduction = starterData.valueReduction; + if (valueReduction < VALUE_REDUCTION_MAX && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START)) { + const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.lastSpecies.speciesId])[ + valueReduction + ]; + options.push({ + label: `×${reductionCost} ${i18next.t("starterSelectUiHandler:reduceCost")}`, + handler: () => { + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= reductionCost) { + persistentStarterData.valueReduction++; + starterData.valueReduction = persistentStarterData.valueReduction; + if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { + persistentStarterData.candyCount -= reductionCost; + starterData.candyCount = persistentStarterData.candyCount; + } + this.starterSummary.updateCandyCount(starterData.candyCount); + globalScene.gameData.saveSystem().then(success => { + if (!success) { + return globalScene.reset(true); + } + }); + this.tryUpdateValue(0); + ui.setMode(UiMode.STARTER_SELECT); + globalScene.playSound("se/buy"); + + // update the value label and icon/animation for available upgrade + if (starterContainer) { + this.updateStarterValueLabel(starterContainer); + this.updateCandyUpgradeDisplay(starterContainer); + } + return true; + } + return false; + }, + item: "candy", + itemArgs: starterColors[this.lastSpecies.speciesId], + }); + } + + // Same species egg menu option. + const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.lastSpecies.speciesId]); + options.push({ + label: `×${sameSpeciesEggCost} ${i18next.t("starterSelectUiHandler: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) { + persistentStarterData.candyCount -= sameSpeciesEggCost; + starterData.candyCount = persistentStarterData.candyCount; + } + this.starterSummary.updateCandyCount(starterData.candyCount); + + const egg = new Egg({ + species: this.lastSpecies.speciesId, + sourceType: EggSourceType.SAME_SPECIES_EGG, + }); + egg.addEggToGameData(); + + globalScene.gameData.saveSystem().then(success => { + if (!success) { + return globalScene.reset(true); + } + }); + ui.setMode(UiMode.STARTER_SELECT); + globalScene.playSound("se/buy"); + + // update the icon/animation for available upgrade + if (starterContainer) { + this.updateCandyUpgradeDisplay(starterContainer); + } + + return true; + } + return false; + }, + item: "candy", + itemArgs: starterColors[this.lastSpecies.speciesId], + }); + options.push({ + label: i18next.t("menu:cancel"), + handler: () => { + ui.setMode(UiMode.STARTER_SELECT); + return true; + }, + }); + ui.setModeWithoutClear(UiMode.OPTION_SELECT, { + options, + yOffset: 47, + }); + }; + options.push({ + label: i18next.t("menuUiHandler:pokedex"), + handler: () => { + ui.setMode(UiMode.STARTER_SELECT).then(() => { + const attributes = { + shiny: starterPreferences.shiny, + variant: starterPreferences.variant, + form: starterPreferences.formIndex, + female: starterPreferences.female, + }; + ui.setOverlayMode(UiMode.POKEDEX_PAGE, this.lastSpecies, attributes, null, null, () => { + if (this.lastSpecies) { + starterContainer = this.starterContainers[this.cursor]; + const persistentStarterData = globalScene.gameData.starterData[this.lastSpecies.speciesId]; + this.updateCandyUpgradeDisplay(starterContainer); + this.updateStarterValueLabel(starterContainer); + starterContainer.starterPassiveBgs.setVisible( + !!persistentStarterData.passiveAttr && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START), + ); + this.setSpecies(this.lastSpecies); + } + }); + }); + return true; + }, + }); + if (!pokemonPrevolutions.hasOwnProperty(this.lastSpecies.speciesId)) { + options.push({ + label: i18next.t("starterSelectUiHandler:useCandies"), + handler: () => { + ui.setMode(UiMode.STARTER_SELECT).then(() => showUseCandies()); + return true; + }, + }); + } + options.push({ + label: i18next.t("menu:cancel"), + handler: () => { + ui.setMode(UiMode.STARTER_SELECT); + return true; + }, + }); + ui.setModeWithoutClear(UiMode.OPTION_SELECT, { + options, + yOffset: 47, }); } @@ -1547,19 +2112,12 @@ export class StarterSelectUiHandler extends MessageUiHandler { return false; } - const maxColumns = 9; - const maxRows = 9; - const numberOfStarters = this.filteredStarterContainers.length; - const numOfRows = Math.ceil(numberOfStarters / maxColumns); - const currentRow = Math.floor(this.cursor / maxColumns); - const onScreenFirstIndex = this.scrollCursor * maxColumns; // this is first starter index on the screen - const onScreenLastIndex = Math.min( - this.filteredStarterContainers.length - 1, - onScreenFirstIndex + maxRows * maxColumns - 1, - ); // this is the last starter index on the screen - const onScreenNumberOfStarters = onScreenLastIndex - onScreenFirstIndex + 1; - const onScreenNumberOfRows = Math.ceil(onScreenNumberOfStarters / maxColumns); - const onScreenCurrentRow = Math.floor((this.cursor - onScreenFirstIndex) / maxColumns); + const numberOfStarters = this.filteredStarterIds.length; + const numOfRows = Math.ceil(numberOfStarters / COLUMNS); + const onScreenFirstIndex = this.scrollCursor * COLUMNS; // this is first starter index on the screen + const onScreenLastIndex = Math.min(this.filteredStarterIds.length - onScreenFirstIndex - 1, ROWS * COLUMNS - 1); // this is the last starter index on the screen + const currentRow = Math.floor((onScreenFirstIndex + this.cursor) / COLUMNS); + const onScreenCurrentRow = Math.floor(this.cursor / COLUMNS); const ui = this.getUi(); @@ -1572,23 +2130,10 @@ export class StarterSelectUiHandler extends MessageUiHandler { } else { error = true; } + } else if (this.filterMode) { + success = this.processFilterModeInput(button); } else if (button === Button.CANCEL) { - if (this.filterMode && this.filterBar.openDropDown) { - // CANCEL with a filter menu open > close it - this.filterBar.toggleDropDown(this.filterBarCursor); - success = true; - } else if ( - this.filterMode - && !this.filterBar.getFilter(this.filterBar.getColumn(this.filterBarCursor)).hasDefaultValues() - ) { - if (this.filterBar.getColumn(this.filterBarCursor) === DropDownColumn.CAUGHT) { - this.resetCaughtDropdown(); - } else { - this.filterBar.resetSelection(this.filterBarCursor); - } - this.updateStarters(); - success = true; - } else if (this.statsMode) { + if (this.statsMode) { this.toggleStatsMode(false); success = true; } else if (this.starterSpecies.length > 0) { @@ -1604,1096 +2149,131 @@ export class StarterSelectUiHandler extends MessageUiHandler { if (!this.filterMode) { this.startCursorObj.setVisible(false); this.starterIconsCursorObj.setVisible(false); - this.randomCursorObj.setVisible(false); - this.setSpecies(null); this.filterBarCursor = 0; this.setFilterMode(true); this.filterBar.toggleDropDown(this.filterBarCursor); } + } else if (this.starterIconsCursorObj.visible) { + success = this.processPartyIconInput(button); } else if (this.startCursorObj.visible) { // this checks to see if the start button is selected - switch (button) { - case Button.ACTION: - if (this.tryStart(true)) { - success = true; - } else { - error = true; - } - break; - case Button.UP: - // UP from start button: go to pokemon in team if any, otherwise filter - this.startCursorObj.setVisible(false); - if (this.starterSpecies.length > 0) { - this.starterIconsCursorIndex = this.starterSpecies.length - 1; - this.moveStarterIconsCursor(this.starterIconsCursorIndex); - } else { - // TODO: how can we get here if start button can't be selected? this appears to be redundant - this.startCursorObj.setVisible(false); - this.randomCursorObj.setVisible(true); - } - success = true; - break; - case Button.DOWN: - // DOWN from start button: Go to filters - this.startCursorObj.setVisible(false); - this.filterBarCursor = Math.max(1, this.filterBar.numFilters - 1); - this.setFilterMode(true); - success = true; - break; - case Button.LEFT: - if (numberOfStarters > 0) { - this.startCursorObj.setVisible(false); - this.cursorObj.setVisible(true); - this.setCursor(onScreenFirstIndex + (onScreenNumberOfRows - 1) * 9 + 8); // set last column - success = true; - } - break; - case Button.RIGHT: - if (numberOfStarters > 0) { - this.startCursorObj.setVisible(false); - this.cursorObj.setVisible(true); - this.setCursor(onScreenFirstIndex + (onScreenNumberOfRows - 1) * 9); // set first column - success = true; - } - break; - } - } else if (this.filterMode) { - switch (button) { - case Button.LEFT: - if (this.filterBarCursor > 0) { - success = this.setCursor(this.filterBarCursor - 1); - } else { - success = this.setCursor(this.filterBar.numFilters - 1); - } - break; - case Button.RIGHT: - if (this.filterBarCursor < this.filterBar.numFilters - 1) { - success = this.setCursor(this.filterBarCursor + 1); - } else { - success = this.setCursor(0); - } - break; - case Button.UP: - if (this.filterBar.openDropDown) { - success = this.filterBar.decDropDownCursor(); - } else if (this.filterBarCursor === this.filterBar.numFilters - 1) { - // UP from the last filter, move to start button - this.setFilterMode(false); - this.cursorObj.setVisible(false); - if (this.starterSpecies.length > 0) { - this.startCursorObj.setVisible(true); - } else { - this.randomCursorObj.setVisible(true); - } - success = true; - } else if (numberOfStarters > 0) { - // UP from filter bar to bottom of Pokemon list - this.setFilterMode(false); - this.scrollCursor = Math.max(0, numOfRows - 9); - this.updateScroll(); - const proportion = (this.filterBarCursor + 0.5) / this.filterBar.numFilters; - const targetCol = Math.min(8, Math.floor(proportion * 11)); - if (numberOfStarters % 9 > targetCol) { - this.setCursor(numberOfStarters - (numberOfStarters % 9) + targetCol); - } else { - this.setCursor(Math.max(numberOfStarters - (numberOfStarters % 9) + targetCol - 9, 0)); - } - success = true; - } - break; - case Button.DOWN: - if (this.filterBar.openDropDown) { - success = this.filterBar.incDropDownCursor(); - } else if (this.filterBarCursor === this.filterBar.numFilters - 1) { - // DOWN from the last filter, move to random selection label - this.setFilterMode(false); - this.cursorObj.setVisible(false); - this.randomCursorObj.setVisible(true); - success = true; - } else if (numberOfStarters > 0) { - // DOWN from filter bar to top of Pokemon list - this.setFilterMode(false); - this.scrollCursor = 0; - this.updateScroll(); - const proportion = this.filterBarCursor / Math.max(1, this.filterBar.numFilters - 1); - const targetCol = Math.min(8, Math.floor(proportion * 11)); - this.setCursor(Math.min(targetCol, numberOfStarters)); - success = true; - } - break; - case Button.ACTION: - if (!this.filterBar.openDropDown) { - this.filterBar.toggleDropDown(this.filterBarCursor); - } else { - this.filterBar.toggleOptionState(); - } - success = true; - break; - } + [success, error] = this.processStartCursorInput(button); } else if (this.randomCursorObj.visible) { - switch (button) { - case Button.ACTION: { - if (this.starterSpecies.length >= 6) { - error = true; - break; - } - const currentPartyValue = this.starterSpecies - .map(s => s.generation) - .reduce( - (total: number, _gen: number, i: number) => - total + globalScene.gameData.getSpeciesStarterValue(this.starterSpecies[i].speciesId), - 0, - ); - // Filter valid starters - const validStarters = this.filteredStarterContainers.filter(starter => { - const species = starter.species; - const [isDupe] = this.isInParty(species); - const starterCost = globalScene.gameData.getSpeciesStarterValue(species.speciesId); - const isValidForChallenge = checkStarterValidForChallenge( - species, - globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), - this.isPartyValid(), - ); - const isCaught = this.getSpeciesData(species.speciesId).dexEntry.caughtAttr; - return ( - !isDupe && isValidForChallenge && currentPartyValue + starterCost <= this.getValueLimit() && isCaught - ); - }); - if (validStarters.length === 0) { - error = true; // No valid starters available - break; - } - // Select random starter - const randomStarter = validStarters[Math.floor(Math.random() * validStarters.length)]; - const randomSpecies = randomStarter.species; - // Set species and prepare attributes - this.setSpecies(randomSpecies); - const dexAttr = this.getCurrentDexProps(randomSpecies.speciesId); - const props = globalScene.gameData.getSpeciesDexAttrProps(randomSpecies, dexAttr); - const abilityIndex = this.abilityCursor; - const nature = this.natureCursor as unknown as Nature; - const teraType = this.teraCursor; - const moveset = this.starterMoveset?.slice(0) as StarterMoveset; - const starterCost = globalScene.gameData.getSpeciesStarterValue(randomSpecies.speciesId); - const speciesForm = getPokemonSpeciesForm(randomSpecies.speciesId, props.formIndex); - // Load assets and add to party - speciesForm.loadAssets(props.female, props.formIndex, props.shiny, props.variant, true).then(() => { - if (this.tryUpdateValue(starterCost, true)) { - this.addToParty(randomSpecies, dexAttr, abilityIndex, nature, moveset, teraType, true); - ui.playSelect(); - } - }); - break; - } - case Button.UP: - this.randomCursorObj.setVisible(false); - this.filterBarCursor = this.filterBar.numFilters - 1; - this.setFilterMode(true); - success = true; - break; - case Button.DOWN: - this.randomCursorObj.setVisible(false); - if (this.starterSpecies.length > 0) { - this.starterIconsCursorIndex = 0; - this.moveStarterIconsCursor(this.starterIconsCursorIndex); - } else { - this.filterBarCursor = this.filterBar.numFilters - 1; - this.setFilterMode(true); - } - success = true; - break; - case Button.LEFT: - if (numberOfStarters > 0) { - this.randomCursorObj.setVisible(false); - this.cursorObj.setVisible(true); - this.setCursor(onScreenFirstIndex + 8); // set last column - success = true; - } - break; - case Button.RIGHT: - if (numberOfStarters > 0) { - this.randomCursorObj.setVisible(false); - this.cursorObj.setVisible(true); - this.setCursor(onScreenFirstIndex); // set first column - success = true; - } - break; + [success, error] = this.processRandomCursorInput(button); + } else if (button === Button.ACTION) { + if (!this.speciesStarterDexEntry?.caughtAttr) { + error = true; + } else if (this.starterSpecies.length <= 6) { + // checks to see if the party has 6 or fewer pokemon + this.openPokemonMenu(); + success = true; } } else { - let starterContainer: StarterContainer; - // The temporary, duplicated starter data to show info - const starterData = this.getSpeciesData(this.lastSpecies.speciesId).starterDataEntry; - // The persistent starter data to apply e.g. candy upgrades - const persistentStarterData = globalScene.gameData.starterData[this.lastSpecies.speciesId]; - // The sanitized starter preferences - if (this.starterPreferences[this.lastSpecies.speciesId] === undefined) { - this.starterPreferences[this.lastSpecies.speciesId] = {}; - } - if (this.originalStarterPreferences[this.lastSpecies.speciesId] === undefined) { - this.originalStarterPreferences[this.lastSpecies.speciesId] = {}; - } - // Bangs are safe here due to the above check - const starterAttributes = this.starterPreferences[this.lastSpecies.speciesId]!; - const originalStarterAttributes = this.originalStarterPreferences[this.lastSpecies.speciesId]!; - - // this gets the correct pokemon cursor depending on whether you're in the starter screen or the party icons - if (!this.starterIconsCursorObj.visible) { - starterContainer = this.filteredStarterContainers[this.cursor]; - } else { - // if species is in filtered starters, get the starter container from the filtered starters, it can be undefined if the species is not in the filtered starters - starterContainer = - this.filteredStarterContainers[ - this.filteredStarterContainers.findIndex(container => container.species === this.lastSpecies) - ]; + if ( + [ + Button.CYCLE_SHINY, + Button.CYCLE_FORM, + Button.CYCLE_GENDER, + Button.CYCLE_ABILITY, + Button.CYCLE_NATURE, + Button.CYCLE_TERA, + ].includes(button) + ) { + success = this.processCycleButtonsInput(button); } - if (button === Button.ACTION) { - if (!this.speciesStarterDexEntry?.caughtAttr) { - error = true; - } else if (this.starterSpecies.length <= 6) { - // checks to see if the party has 6 or fewer pokemon - const ui = this.getUi(); - let options: any[] = []; // TODO: add proper type - - const [isDupe, removeIndex]: [boolean, number] = this.isInParty(this.lastSpecies); - - const isPartyValid = this.isPartyValid(); - const isValidForChallenge = checkStarterValidForChallenge( - this.lastSpecies, - globalScene.gameData.getSpeciesDexAttrProps( - this.lastSpecies, - this.getCurrentDexProps(this.lastSpecies.speciesId), - ), - isPartyValid, - ); - - const currentPartyValue = this.starterSpecies - .map(s => s.generation) - .reduce( - (total: number, _gen: number, i: number) => - (total += globalScene.gameData.getSpeciesStarterValue(this.starterSpecies[i].speciesId)), - 0, - ); - const newCost = globalScene.gameData.getSpeciesStarterValue(this.lastSpecies.speciesId); - if ( - !isDupe - && isValidForChallenge - && currentPartyValue + newCost <= this.getValueLimit() - && this.starterSpecies.length < PLAYER_PARTY_MAX_SIZE - ) { - options = [ - { - label: i18next.t("starterSelectUiHandler:addToParty"), - handler: () => { - ui.setMode(UiMode.STARTER_SELECT); - const isOverValueLimit = this.tryUpdateValue( - globalScene.gameData.getSpeciesStarterValue(this.lastSpecies.speciesId), - true, - ); - if (!isDupe && isValidForChallenge && isOverValueLimit) { - this.starterCursorObjs[this.starterSpecies.length] - .setVisible(true) - .setPosition(this.cursorObj.x, this.cursorObj.y); - this.addToParty( - this.lastSpecies, - this.dexAttrCursor, - this.abilityCursor, - this.natureCursor as unknown as Nature, - this.starterMoveset?.slice(0) as StarterMoveset, - this.teraCursor, - ); - ui.playSelect(); - } else { - ui.playError(); // this should be redundant as there is now a trigger for when a pokemon can't be added to party - } - return true; - }, - overrideSound: true, - }, - ]; - } else if (isDupe) { - // if it already exists in your party, it will give you the option to remove from your party - options = [ - { - label: i18next.t("starterSelectUiHandler:removeFromParty"), - handler: () => { - this.popStarter(removeIndex); - ui.setMode(UiMode.STARTER_SELECT); - return true; - }, - }, - ]; - } - - options.push( - // this shows the IVs for the pokemon - { - label: i18next.t("starterSelectUiHandler:toggleIVs"), - handler: () => { - this.toggleStatsMode(); - ui.setMode(UiMode.STARTER_SELECT); - return true; - }, - }, - ); - if (this.speciesStarterMoves.length > 1) { - // this lets you change the pokemon moves - const showSwapOptions = (moveset: StarterMoveset) => { - this.blockInput = true; - - ui.setMode(UiMode.STARTER_SELECT).then(() => { - ui.showText(i18next.t("starterSelectUiHandler:selectMoveSwapOut"), null, () => { - this.moveInfoOverlay.show(allMoves[moveset[0]]); - - ui.setModeWithoutClear(UiMode.OPTION_SELECT, { - options: moveset - .map((m: MoveId, i: number) => { - const option: OptionSelectItem = { - label: allMoves[m].name, - handler: () => { - this.blockInput = true; - ui.setMode(UiMode.STARTER_SELECT).then(() => { - ui.showText( - `${i18next.t("starterSelectUiHandler:selectMoveSwapWith")} ${allMoves[m].name}.`, - null, - () => { - const possibleMoves = this.speciesStarterMoves.filter((sm: MoveId) => sm !== m); - this.moveInfoOverlay.show(allMoves[possibleMoves[0]]); - - ui.setModeWithoutClear(UiMode.OPTION_SELECT, { - options: possibleMoves - .map(sm => { - // make an option for each available starter move - const option = { - label: allMoves[sm].name, - handler: () => { - this.switchMoveHandler(i, sm, m); - showSwapOptions(this.starterMoveset!); // TODO: is this bang correct? - return true; - }, - onHover: () => { - this.moveInfoOverlay.show(allMoves[sm]); - }, - }; - return option; - }) - .concat({ - label: i18next.t("menu:cancel"), - handler: () => { - showSwapOptions(this.starterMoveset!); // TODO: is this bang correct? - return true; - }, - onHover: () => { - this.moveInfoOverlay.clear(); - }, - }), - supportHover: true, - maxOptions: 8, - yOffset: 19, - }); - this.blockInput = false; - }, - ); - }); - return true; - }, - onHover: () => { - this.moveInfoOverlay.show(allMoves[m]); - }, - }; - return option; - }) - .concat({ - label: i18next.t("menu:cancel"), - handler: () => { - this.moveInfoOverlay.clear(); - this.clearText(); - // Only saved if moves were actually swapped - if (this.hasSwappedMoves) { - globalScene.gameData.saveSystem().then(success => { - if (!success) { - return globalScene.reset(true); - } - }); - } - ui.setMode(UiMode.STARTER_SELECT); - return true; - }, - onHover: () => { - this.moveInfoOverlay.clear(); - }, - }), - supportHover: true, - maxOptions: 8, - yOffset: 19, - }); - this.blockInput = false; - }); - }); - }; - options.push({ - label: i18next.t("starterSelectUiHandler:manageMoves"), - handler: () => { - this.hasSwappedMoves = false; - showSwapOptions(this.starterMoveset!); // TODO: is this bang correct? - return true; - }, - }); - } - if (this.canCycleNature) { - // if we could cycle natures, enable the improved nature menu - const showNatureOptions = () => { - this.blockInput = true; - - ui.setMode(UiMode.STARTER_SELECT).then(() => { - ui.showText(i18next.t("starterSelectUiHandler:selectNature"), null, () => { - const natures = globalScene.gameData.getNaturesForAttr(this.speciesStarterDexEntry?.natureAttr); - ui.setModeWithoutClear(UiMode.OPTION_SELECT, { - options: natures - .map((n: Nature, _i: number) => { - const option: OptionSelectItem = { - label: getNatureName(n, true, true, true), - handler: () => { - starterAttributes.nature = n; - originalStarterAttributes.nature = starterAttributes.nature; - this.clearText(); - ui.setMode(UiMode.STARTER_SELECT); - // set nature for starter - this.setSpeciesDetails(this.lastSpecies, { - natureIndex: n, - }); - this.blockInput = false; - return true; - }, - }; - return option; - }) - .concat({ - label: i18next.t("menu:cancel"), - handler: () => { - this.clearText(); - ui.setMode(UiMode.STARTER_SELECT); - this.blockInput = false; - return true; - }, - }), - maxOptions: 8, - yOffset: 19, - }); - }); - }); - }; - options.push({ - label: i18next.t("starterSelectUiHandler:manageNature"), - handler: () => { - showNatureOptions(); - return true; - }, - }); - } - - const passiveAttr = starterData.passiveAttr; - if (passiveAttr & PassiveAttr.UNLOCKED) { - // this is for enabling and disabling the passive - const label = i18next.t( - passiveAttr & PassiveAttr.ENABLED - ? "starterSelectUiHandler:disablePassive" - : "starterSelectUiHandler:enablePassive", - ); - options.push({ - label, - handler: () => { - starterData.passiveAttr ^= PassiveAttr.ENABLED; - persistentStarterData.passiveAttr ^= PassiveAttr.ENABLED; - ui.setMode(UiMode.STARTER_SELECT); - this.setSpeciesDetails(this.lastSpecies); - return true; - }, - }); - } - // if container.favorite is false, show the favorite option - const isFavorite = starterAttributes?.favorite ?? false; - if (!isFavorite) { - options.push({ - label: i18next.t("starterSelectUiHandler:addToFavorites"), - handler: () => { - starterAttributes.favorite = true; - originalStarterAttributes.favorite = true; - // if the starter container not exists, it means the species is not in the filtered starters - if (starterContainer) { - starterContainer.favoriteIcon.setVisible(starterAttributes.favorite); - } - ui.setMode(UiMode.STARTER_SELECT); - return true; - }, - }); + switch (button) { + case Button.UP: + if (currentRow > 0) { + if (this.scrollCursor > 0 && currentRow - this.scrollCursor === 0) { + this.scrollCursor--; + this.updateScroll(); + success = this.setCursor(this.cursor); + } else { + success = this.setCursor(this.cursor - 9); + } } else { - options.push({ - label: i18next.t("starterSelectUiHandler:removeFromFavorites"), - handler: () => { - starterAttributes.favorite = false; - originalStarterAttributes.favorite = false; - // if the starter container not exists, it means the species is not in the filtered starters - if (starterContainer) { - starterContainer.favoriteIcon.setVisible(starterAttributes.favorite); - } - ui.setMode(UiMode.STARTER_SELECT); - return true; - }, - }); + this.filterBarCursor = this.filterBar.getNearestFilter(this.starterContainers[this.cursor]); + this.setFilterMode(true); + success = true; } - options.push({ - label: i18next.t("menu:rename"), - handler: () => { - ui.playSelect(); - let nickname = starterAttributes.nickname ? String(starterAttributes.nickname) : ""; - nickname = decodeURIComponent(escape(atob(nickname))); - ui.setModeWithoutClear( - UiMode.RENAME_POKEMON, - { - buttonActions: [ - (sanitizedName: string) => { - ui.playSelect(); - starterAttributes.nickname = sanitizedName; - originalStarterAttributes.nickname = sanitizedName; - const name = decodeURIComponent(escape(atob(starterAttributes.nickname))); - if (name.length > 0) { - this.pokemonNameText.setText(name); - } else { - this.pokemonNameText.setText(this.lastSpecies.name); - } - ui.setMode(UiMode.STARTER_SELECT); - }, - () => { - ui.setMode(UiMode.STARTER_SELECT); - }, - ], - }, - nickname, - ); - return true; - }, - }); - - // Purchases with Candy - const candyCount = starterData.candyCount; - const showUseCandies = () => { - const options: any[] = []; // TODO: add proper type - - // Unlock passive option - if (!(passiveAttr & PassiveAttr.UNLOCKED) && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START)) { - const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.lastSpecies.speciesId]); - options.push({ - label: `×${passiveCost} ${i18next.t("starterSelectUiHandler:unlockPassive")}`, - handler: () => { - if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= passiveCost) { - persistentStarterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED; - starterData.passiveAttr = persistentStarterData.passiveAttr; - if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { - persistentStarterData.candyCount -= passiveCost; - starterData.candyCount = persistentStarterData.candyCount; - } - this.pokemonCandyCountText.setText(`×${starterData.candyCount}`); - globalScene.gameData.saveSystem().then(success => { - if (!success) { - return globalScene.reset(true); - } - }); - ui.setMode(UiMode.STARTER_SELECT); - this.setSpeciesDetails(this.lastSpecies); - globalScene.playSound("se/buy"); - - // update the passive background and icon/animation for available upgrade - if (starterContainer) { - this.updateCandyUpgradeDisplay(starterContainer); - starterContainer.starterPassiveBgs.setVisible(!!starterData.passiveAttr); - } - return true; - } - return false; - }, - item: "candy", - itemArgs: starterColors[this.lastSpecies.speciesId], - }); + break; + case Button.DOWN: + if (currentRow < numOfRows - 1 && this.cursor + 9 < this.filteredStarterIds.length) { + // not last row + if (currentRow - this.scrollCursor === 8) { + // last row of visible starters + this.scrollCursor++; + this.updateScroll(); + success = this.setCursor(this.cursor); + } else { + success = this.setCursor(this.cursor + 9); } - - // Reduce cost option - const valueReduction = starterData.valueReduction; - if (valueReduction < valueReductionMax && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START)) { - const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.lastSpecies.speciesId])[ - valueReduction - ]; - options.push({ - label: `×${reductionCost} ${i18next.t("starterSelectUiHandler:reduceCost")}`, - handler: () => { - if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= reductionCost) { - persistentStarterData.valueReduction++; - starterData.valueReduction = persistentStarterData.valueReduction; - if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { - persistentStarterData.candyCount -= reductionCost; - starterData.candyCount = persistentStarterData.candyCount; - } - this.pokemonCandyCountText.setText(`×${starterData.candyCount}`); - globalScene.gameData.saveSystem().then(success => { - if (!success) { - return globalScene.reset(true); - } - }); - this.tryUpdateValue(0); - ui.setMode(UiMode.STARTER_SELECT); - globalScene.playSound("se/buy"); - - // update the value label and icon/animation for available upgrade - if (starterContainer) { - this.updateStarterValueLabel(starterContainer); - this.updateCandyUpgradeDisplay(starterContainer); - } - return true; - } - return false; - }, - item: "candy", - itemArgs: starterColors[this.lastSpecies.speciesId], - }); - } - - // Same species egg menu option. - const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.lastSpecies.speciesId]); - options.push({ - label: `×${sameSpeciesEggCost} ${i18next.t("starterSelectUiHandler: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) { - persistentStarterData.candyCount -= sameSpeciesEggCost; - starterData.candyCount = persistentStarterData.candyCount; - } - this.pokemonCandyCountText.setText(`×${starterData.candyCount}`); - - const egg = new Egg({ - species: this.lastSpecies.speciesId, - sourceType: EggSourceType.SAME_SPECIES_EGG, - }); - egg.addEggToGameData(); - - globalScene.gameData.saveSystem().then(success => { - if (!success) { - return globalScene.reset(true); - } - }); - ui.setMode(UiMode.STARTER_SELECT); - globalScene.playSound("se/buy"); - - // update the icon/animation for available upgrade - if (starterContainer) { - this.updateCandyUpgradeDisplay(starterContainer); - } - - return true; - } - return false; - }, - item: "candy", - itemArgs: starterColors[this.lastSpecies.speciesId], - }); - options.push({ - label: i18next.t("menu:cancel"), - handler: () => { - ui.setMode(UiMode.STARTER_SELECT); - return true; - }, - }); - ui.setModeWithoutClear(UiMode.OPTION_SELECT, { - options, - yOffset: 47, - }); - }; - options.push({ - label: i18next.t("menuUiHandler:pokedex"), - handler: () => { - ui.setMode(UiMode.STARTER_SELECT).then(() => { - const attributes = { - shiny: starterAttributes.shiny, - variant: starterAttributes.variant, - form: starterAttributes.form, - female: starterAttributes.female, - }; - ui.setOverlayMode(UiMode.POKEDEX_PAGE, this.lastSpecies, attributes, null, null, () => { - if (this.lastSpecies) { - starterContainer = this.filteredStarterContainers[this.cursor]; - const persistentStarterData = globalScene.gameData.starterData[this.lastSpecies.speciesId]; - this.updateCandyUpgradeDisplay(starterContainer); - this.updateStarterValueLabel(starterContainer); - starterContainer.starterPassiveBgs.setVisible( - !!persistentStarterData.passiveAttr && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START), - ); - this.setSpecies(this.lastSpecies); - } - }); - }); - return true; - }, - }); - if (!pokemonPrevolutions.hasOwnProperty(this.lastSpecies.speciesId)) { - options.push({ - label: i18next.t("starterSelectUiHandler:useCandies"), - handler: () => { - ui.setMode(UiMode.STARTER_SELECT).then(() => showUseCandies()); - return true; - }, - }); + } else if (numOfRows > 1) { + // DOWN from last row of Pokemon > Wrap around to first row + this.scrollCursor = 0; + this.updateScroll(); + success = this.setCursor(this.cursor % 9); + } else { + // DOWN from single row of Pokemon > Go to filters + this.filterBarCursor = this.filterBar.getNearestFilter(this.starterContainers[this.cursor]); + this.setFilterMode(true); + success = true; } - options.push({ - label: i18next.t("menu:cancel"), - handler: () => { - ui.setMode(UiMode.STARTER_SELECT); - return true; - }, - }); - ui.setModeWithoutClear(UiMode.OPTION_SELECT, { - options, - yOffset: 47, - }); - success = true; - } - } else { - const props = globalScene.gameData.getSpeciesDexAttrProps( - this.lastSpecies, - this.getCurrentDexProps(this.lastSpecies.speciesId), - ); - switch (button) { - case Button.CYCLE_SHINY: - if (this.canCycleShiny) { - if (starterAttributes.shiny === false) { - // If not shiny, we change to shiny and get the proper default variant - const newProps = globalScene.gameData.getSpeciesDexAttrProps( - this.lastSpecies, - this.getCurrentDexProps(this.lastSpecies.speciesId), - ); - const newVariant = starterAttributes.variant - ? (starterAttributes.variant as Variant) - : newProps.variant; - starterAttributes.shiny = true; - originalStarterAttributes.shiny = true; - starterAttributes.variant = newVariant; - originalStarterAttributes.variant = newVariant; - this.setSpeciesDetails(this.lastSpecies, { - shiny: true, - variant: newVariant, - }); - - globalScene.playSound("se/sparkle"); - // Cycle tint based on current sprite tint - const tint = getVariantTint(newVariant); - this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant)).setTint(tint).setVisible(true); - } else { - // If shiny, we update the variant - let newVariant = props.variant; - do { - newVariant = (newVariant + 1) % 3; - if (newVariant === 0) { - if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.DEFAULT_VARIANT) { - // TODO: is this bang correct? - break; - } - } else if (newVariant === 1) { - if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_2) { - // TODO: is this bang correct? - break; - } - } else if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.VARIANT_3) { - // TODO: is this bang correct? - break; - } - } while (newVariant !== props.variant); - starterAttributes.variant = newVariant; // store the selected variant - originalStarterAttributes.variant = newVariant; - if (this.speciesStarterDexEntry!.caughtAttr & DexAttr.NON_SHINY && newVariant <= props.variant) { - // If we have run out of variants, go back to non shiny - starterAttributes.shiny = false; - originalStarterAttributes.shiny = false; - this.setSpeciesDetails(this.lastSpecies, { - shiny: false, - variant: 0, - }); - this.pokemonShinyIcon.setVisible(false); - success = true; - } else { - // If going to a higher variant, or only shiny forms are caught, go to next variant - this.setSpeciesDetails(this.lastSpecies, { - variant: newVariant as Variant, - }); - // Cycle tint based on current sprite tint - const tint = getVariantTint(newVariant as Variant); - this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant as Variant)).setTint(tint); - success = true; - } - } - } - break; - case Button.CYCLE_FORM: - if (this.canCycleForm) { - const formCount = this.lastSpecies.forms.length; - let newFormIndex = props.formIndex; - do { - newFormIndex = (newFormIndex + 1) % formCount; - if ( - this.lastSpecies.forms[newFormIndex].isStarterSelectable - && this.speciesStarterDexEntry!.caughtAttr! & globalScene.gameData.getFormAttr(newFormIndex) - ) { - // TODO: are those bangs correct? - break; - } - } while (newFormIndex !== props.formIndex); - starterAttributes.form = newFormIndex; // store the selected form - originalStarterAttributes.form = newFormIndex; - starterAttributes.tera = this.lastSpecies.forms[newFormIndex].type1; - originalStarterAttributes.tera = starterAttributes.tera; - this.setSpeciesDetails(this.lastSpecies, { - formIndex: newFormIndex, - teraType: starterAttributes.tera, - }); - success = true; - } - break; - case Button.CYCLE_GENDER: - if (this.canCycleGender) { - starterAttributes.female = !props.female; - originalStarterAttributes.female = starterAttributes.female; - this.setSpeciesDetails(this.lastSpecies, { - female: !props.female, - }); - success = true; - } - break; - case Button.CYCLE_ABILITY: - if (this.canCycleAbility) { - const abilityCount = this.lastSpecies.getAbilityCount(); - const abilityAttr = starterData.abilityAttr; - const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1; - let newAbilityIndex = this.abilityCursor; - do { - newAbilityIndex = (newAbilityIndex + 1) % abilityCount; - if (newAbilityIndex === 0) { - if (hasAbility1) { - break; - } - } else if (newAbilityIndex === 1) { - // If ability 1 and 2 are the same and ability 1 is unlocked, skip over ability 2 - if (this.lastSpecies.ability1 === this.lastSpecies.ability2 && hasAbility1) { - newAbilityIndex = (newAbilityIndex + 1) % abilityCount; - } - break; - } else if (abilityAttr & AbilityAttr.ABILITY_HIDDEN) { - break; - } - } while (newAbilityIndex !== this.abilityCursor); - starterAttributes.ability = newAbilityIndex; // store the selected ability - originalStarterAttributes.ability = newAbilityIndex; - - const { visible: tooltipVisible } = globalScene.ui.getTooltip(); - - if (tooltipVisible && this.activeTooltip === "ABILITY") { - const newAbility = allAbilities[this.lastSpecies.getAbility(newAbilityIndex)]; - globalScene.ui.editTooltip(`${newAbility.name}`, `${newAbility.description}`); - } - - this.setSpeciesDetails(this.lastSpecies, { - abilityIndex: newAbilityIndex, - }); - success = true; - } - break; - case Button.CYCLE_NATURE: - if (this.canCycleNature) { - const natures = globalScene.gameData.getNaturesForAttr(this.speciesStarterDexEntry?.natureAttr); - const natureIndex = natures.indexOf(this.natureCursor); - const newNature = natures[natureIndex < natures.length - 1 ? natureIndex + 1 : 0]; - // store cycled nature as default - starterAttributes.nature = newNature as unknown as number; - originalStarterAttributes.nature = starterAttributes.nature; - this.setSpeciesDetails(this.lastSpecies, { - natureIndex: newNature, - }); - success = true; - } - break; - case Button.CYCLE_TERA: - if (this.canCycleTera) { - const speciesForm = getPokemonSpeciesForm(this.lastSpecies.speciesId, starterAttributes.form ?? 0); - if (speciesForm.type1 === this.teraCursor && speciesForm.type2 != null) { - starterAttributes.tera = speciesForm.type2; - originalStarterAttributes.tera = starterAttributes.tera; - this.setSpeciesDetails(this.lastSpecies, { - teraType: speciesForm.type2, - }); - } else { - starterAttributes.tera = speciesForm.type1; - originalStarterAttributes.tera = starterAttributes.tera; - this.setSpeciesDetails(this.lastSpecies, { - teraType: speciesForm.type1, - }); - } - success = true; - } - break; - case Button.UP: - if (!this.starterIconsCursorObj.visible) { - if (currentRow > 0) { - if (this.scrollCursor > 0 && currentRow - this.scrollCursor === 0) { - this.scrollCursor--; - this.updateScroll(); - } - success = this.setCursor(this.cursor - 9); - } else { - this.filterBarCursor = this.filterBar.getNearestFilter(this.filteredStarterContainers[this.cursor]); - this.setFilterMode(true); - success = true; - } + break; + case Button.LEFT: + if (this.cursor % 9 !== 0) { + success = this.setCursor(this.cursor - 1); + } else { + // LEFT from filtered Pokemon, on the left edge + if (onScreenCurrentRow === 0) { + // from the first row of starters we go to the random selection + this.cursorObj.setVisible(false); + this.showRandomCursor(); + } else if (this.starterSpecies.length === 0) { + // no starter in team and not on first row > wrap around to the last column + success = this.setCursor(this.cursor + Math.min(8, onScreenLastIndex - this.cursor)); + } else if (onScreenCurrentRow < 7) { + // at least one pokemon in team > for the first 7 rows, go to closest starter + this.cursorObj.setVisible(false); + this.starterIconsCursorIndex = findClosestStarterIndex(this.cursorObj.y - 1, this.starterSpecies.length); + this.moveStarterIconsCursor(this.starterIconsCursorIndex); } else { - if (this.starterIconsCursorIndex === 0) { - // Up from first Pokemon in the team > go to Random selection - this.starterIconsCursorObj.setVisible(false); - this.setSpecies(null); - this.randomCursorObj.setVisible(true); - } else { - this.starterIconsCursorIndex--; - this.moveStarterIconsCursor(this.starterIconsCursorIndex); - } - success = true; + // at least one pokemon in team > from the bottom 2 rows, go to start run button + this.cursorObj.setVisible(false); + this.setNoSpecies(); + this.startCursorObj.setVisible(true); } - break; - case Button.DOWN: - if (!this.starterIconsCursorObj.visible) { - if (currentRow < numOfRows - 1) { - // not last row - if (currentRow - this.scrollCursor === 8) { - // last row of visible starters - this.scrollCursor++; - } - success = this.setCursor(this.cursor + 9); - this.updateScroll(); - } else if (numOfRows > 1) { - // DOWN from last row of Pokemon > Wrap around to first row - this.scrollCursor = 0; - this.updateScroll(); - success = this.setCursor(this.cursor % 9); - } else { - // DOWN from single row of Pokemon > Go to filters - this.filterBarCursor = this.filterBar.getNearestFilter(this.filteredStarterContainers[this.cursor]); - this.setFilterMode(true); - success = true; - } + success = true; + } + break; + case Button.RIGHT: + // is not right edge + if (this.cursor % 9 < (currentRow < numOfRows - 1 ? 8 : (numberOfStarters - 1) % 9)) { + success = this.setCursor(this.cursor + 1); + } else { + // RIGHT from filtered Pokemon, on the right edge + if (onScreenCurrentRow === 0) { + // from the first row of starters we go to the random selection + this.cursorObj.setVisible(false); + this.showRandomCursor(); + } else if (this.starterSpecies.length === 0) { + // no selected starter in team > wrap around to the first column + success = this.setCursor(this.cursor - Math.min(8, this.cursor % 9)); + } else if (onScreenCurrentRow < 7) { + // at least one pokemon in team > for the first 7 rows, go to closest starter + this.cursorObj.setVisible(false); + this.starterIconsCursorIndex = findClosestStarterIndex(this.cursorObj.y - 1, this.starterSpecies.length); + this.moveStarterIconsCursor(this.starterIconsCursorIndex); } else { - if (this.starterIconsCursorIndex <= this.starterSpecies.length - 2) { - this.starterIconsCursorIndex++; - this.moveStarterIconsCursor(this.starterIconsCursorIndex); - } else { - this.starterIconsCursorObj.setVisible(false); - this.setSpecies(null); - this.startCursorObj.setVisible(true); - } - success = true; + // at least one pokemon in team > from the bottom 2 rows, go to start run button + this.cursorObj.setVisible(false); + this.setNoSpecies(); + this.startCursorObj.setVisible(true); } - break; - case Button.LEFT: - if (!this.starterIconsCursorObj.visible) { - if (this.cursor % 9 !== 0) { - success = this.setCursor(this.cursor - 1); - } else { - // LEFT from filtered Pokemon, on the left edge - if (onScreenCurrentRow === 0) { - // from the first row of starters we go to the random selection - this.cursorObj.setVisible(false); - this.randomCursorObj.setVisible(true); - } else if (this.starterSpecies.length === 0) { - // no starter in team and not on first row > wrap around to the last column - success = this.setCursor(this.cursor + Math.min(8, numberOfStarters - this.cursor)); - } else if (onScreenCurrentRow < 7) { - // at least one pokemon in team > for the first 7 rows, go to closest starter - this.cursorObj.setVisible(false); - this.starterIconsCursorIndex = findClosestStarterIndex( - this.cursorObj.y - 1, - this.starterSpecies.length, - ); - this.moveStarterIconsCursor(this.starterIconsCursorIndex); - } else { - // at least one pokemon in team > from the bottom 2 rows, go to start run button - this.cursorObj.setVisible(false); - this.setSpecies(null); - this.startCursorObj.setVisible(true); - } - success = true; - } - } else if (numberOfStarters > 0) { - // LEFT from team > Go to closest filtered Pokemon - const closestRowIndex = findClosestStarterRow(this.starterIconsCursorIndex, onScreenNumberOfRows); - this.starterIconsCursorObj.setVisible(false); - this.cursorObj.setVisible(true); - this.setCursor(Math.min(onScreenFirstIndex + closestRowIndex * 9 + 8, onScreenLastIndex)); - success = true; - } else { - // LEFT from team and no Pokemon in filter > do nothing - success = false; - } - break; - case Button.RIGHT: - if (!this.starterIconsCursorObj.visible) { - // is not right edge - if (this.cursor % 9 < (currentRow < numOfRows - 1 ? 8 : (numberOfStarters - 1) % 9)) { - success = this.setCursor(this.cursor + 1); - } else { - // RIGHT from filtered Pokemon, on the right edge - if (onScreenCurrentRow === 0) { - // from the first row of starters we go to the random selection - this.cursorObj.setVisible(false); - this.randomCursorObj.setVisible(true); - } else if (this.starterSpecies.length === 0) { - // no selected starter in team > wrap around to the first column - success = this.setCursor(this.cursor - Math.min(8, this.cursor % 9)); - } else if (onScreenCurrentRow < 7) { - // at least one pokemon in team > for the first 7 rows, go to closest starter - this.cursorObj.setVisible(false); - this.starterIconsCursorIndex = findClosestStarterIndex( - this.cursorObj.y - 1, - this.starterSpecies.length, - ); - this.moveStarterIconsCursor(this.starterIconsCursorIndex); - } else { - // at least one pokemon in team > from the bottom 2 rows, go to start run button - this.cursorObj.setVisible(false); - this.setSpecies(null); - this.startCursorObj.setVisible(true); - } - success = true; - } - } else if (numberOfStarters > 0) { - // RIGHT from team > Go to closest filtered Pokemon - const closestRowIndex = findClosestStarterRow(this.starterIconsCursorIndex, onScreenNumberOfRows); - this.starterIconsCursorObj.setVisible(false); - this.cursorObj.setVisible(true); - this.setCursor( - Math.min(onScreenFirstIndex + closestRowIndex * 9, onScreenLastIndex - (onScreenLastIndex % 9)), - ); - success = true; - } else { - // RIGHT from team and no Pokemon in filter > do nothing - success = false; - } - break; - } + success = true; + } + break; } } @@ -2726,7 +2306,6 @@ export class StarterSelectUiHandler extends MessageUiHandler { nature: Nature, moveset: StarterMoveset, teraType: PokemonType, - randomSelection = false, ) { const props = globalScene.gameData.getSpeciesDexAttrProps(species, dexAttr); this.starterIcons[this.starterSpecies.length].setTexture( @@ -2744,7 +2323,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { props.variant, ); - const { dexEntry, starterDataEntry } = this.getSpeciesData(species.speciesId); + const { dexEntry, starterDataEntry } = getSpeciesData(species.speciesId); const starter = { speciesId: species.speciesId, @@ -2764,14 +2343,12 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.starters.push(starter); this.starterSpecies.push(species); - if (this.speciesLoaded.get(species.speciesId) || randomSelection) { - getPokemonSpeciesForm(species.speciesId, props.formIndex).cry(); - } + getPokemonSpeciesForm(species.speciesId, props.formIndex).cry(); this.updateInstructions(); } updatePartyIcon(species: PokemonSpecies, index: number) { - const props = globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)); + const props = this.getSpeciesPropsFromPreferences(species); this.starterIcons[index].setTexture(species.getIconAtlasKey(props.formIndex, props.shiny, props.variant)); this.starterIcons[index].setFrame(species.getIconId(props.female, props.formIndex, props.shiny, props.variant)); this.checkIconId(this.starterIcons[index], species, props.female, props.formIndex, props.shiny, props.variant); @@ -2814,7 +2391,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { starterDataEntry.moveset = updatedMoveset; } this.hasSwappedMoves = true; - this.setSpeciesDetails(this.lastSpecies, { forSeen: false }); + // TODO: we shouldn't need to call setSpeciesDetails here, since only the moveset is changing + this.setSpeciesDetails(this.lastSpecies); this.updateSelectedStarterMoveset(speciesId); } @@ -2991,108 +2569,32 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } - getValueLimit(): number { - const valueLimit = new NumberHolder(0); - switch (globalScene.gameMode.modeId) { - case GameModes.ENDLESS: - case GameModes.SPLICED_ENDLESS: - valueLimit.value = 15; - break; - default: - valueLimit.value = 10; - } - - applyChallenges(ChallengeType.STARTER_POINTS, valueLimit); - - return valueLimit.value; - } - updateStarters = () => { this.scrollCursor = 0; - this.filteredStarterContainers = []; - this.validStarterContainers = []; - - // biome-ignore-start lint/suspicious/useIterableCallbackReturn: benign - this.pokerusCursorObjs.forEach(cursor => cursor.setVisible(false)); - this.starterCursorObjs.forEach(cursor => cursor.setVisible(false)); - // biome-ignore-end lint/suspicious/useIterableCallbackReturn: benign this.filterBar.updateFilterLabels(); - // pre filter for challenges - if (globalScene.gameMode.modeId === GameModes.CHALLENGE) { - this.starterContainers.forEach(container => { - const species = container.species; - let allFormsValid = false; - if (species.forms?.length > 0) { - for (let i = 0; i < species.forms.length; i++) { - /* Here we are making a fake form index dex props for challenges - * Since some pokemon rely on forms to be valid (i.e. blaze tauros for fire challenges), we make a fake form and dex props to use in the challenge - */ - if (!species.forms[i].isStarterSelectable) { - continue; - } - const tempFormProps = BigInt(Math.pow(2, i)) * DexAttr.DEFAULT_FORM; - const isValidForChallenge = checkStarterValidForChallenge( - container.species, - globalScene.gameData.getSpeciesDexAttrProps(species, tempFormProps), - true, - ); - allFormsValid ||= isValidForChallenge; - } - } else { - const isValidForChallenge = checkStarterValidForChallenge( - container.species, - globalScene.gameData.getSpeciesDexAttrProps( - species, - globalScene.gameData.getSpeciesDefaultDexAttr(container.species, false, true), - ), - true, - ); - allFormsValid = isValidForChallenge; - } - if (allFormsValid) { - this.validStarterContainers.push(container); - } else { - container.setVisible(false); - } - }); - } else { - this.validStarterContainers = this.starterContainers; - } - - // this updates icons for previously saved pokemon - for (const currentFilteredContainer of this.validStarterContainers) { - const starterSprite = currentFilteredContainer.icon as Phaser.GameObjects.Sprite; - - const currentDexAttr = this.getCurrentDexProps(currentFilteredContainer.species.speciesId); - const props = globalScene.gameData.getSpeciesDexAttrProps(currentFilteredContainer.species, currentDexAttr); - - starterSprite.setTexture( - currentFilteredContainer.species.getIconAtlasKey(props.formIndex, props.shiny, props.variant), - currentFilteredContainer.species.getIconId(props.female!, props.formIndex, props.shiny, props.variant), - ); - currentFilteredContainer.checkIconId(props.female, props.formIndex, props.shiny, props.variant); - } + this.filteredStarterIds = []; // filter - this.validStarterContainers.forEach(container => { - container.setVisible(false); - - container.cost = globalScene.gameData.getSpeciesStarterValue(container.species.speciesId); + this.allStarterSpecies.forEach(species => { + // Exclude starters which are not valid for the challenge + if (globalScene.gameMode.modeId === GameModes.CHALLENGE && !isStarterValidForChallenge(species)) { + // TODO: figure out what to put here + } // First, ensure you have the caught attributes for the species else default to bigint 0 - const { dexEntry, starterDataEntry: starterData } = this.getSpeciesData(container.species.speciesId); + const { dexEntry, starterDataEntry: starterData } = getSpeciesData(species.speciesId); const caughtAttr = dexEntry?.caughtAttr ?? BigInt(0); - const isStarterProgressable = speciesEggMoves.hasOwnProperty(container.species.speciesId); + const isStarterProgressable = speciesEggMoves.hasOwnProperty(species.speciesId); // Gen filter - const fitsGen = this.filterBar.getVals(DropDownColumn.GEN).includes(container.species.generation); + const fitsGen = this.filterBar.getVals(DropDownColumn.GEN).includes(species.generation); // Type filter const fitsType = this.filterBar .getVals(DropDownColumn.TYPES) - .some(type => container.species.isOfType((type as number) - 1)); + .some(type => species.isOfType((type as number) - 1)); // Caught / Shiny filter const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY); @@ -3121,7 +2623,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { // Passive Filter const isPassiveUnlocked = starterData.passiveAttr > 0; - const isPassiveUnlockable = this.isPassiveAvailable(container.species.speciesId) && !isPassiveUnlocked; + const isPassiveUnlockable = isPassiveAvailable(species.speciesId) && !isPassiveUnlocked; const fitsPassive = this.filterBar.getVals(DropDownColumn.UNLOCKS).some(unlocks => { if (unlocks.val === "PASSIVE" && unlocks.state === DropDownState.ON) { return isPassiveUnlocked; @@ -3140,7 +2642,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { // Cost Reduction Filter const isCostReducedByOne = starterData.valueReduction === 1; const isCostReducedByTwo = starterData.valueReduction === 2; - const isCostReductionUnlockable = this.isValueReductionAvailable(container.species.speciesId); + const isCostReductionUnlockable = isValueReductionAvailable(species.speciesId); const fitsCostReduction = this.filterBar.getVals(DropDownColumn.UNLOCKS).some(unlocks => { if (unlocks.val === "COST_REDUCTION" && unlocks.state === DropDownState.ON) { return isCostReducedByOne || isCostReducedByTwo; @@ -3163,7 +2665,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { }); // Favorite Filter - const isFavorite = this.starterPreferences[container.species.speciesId]?.favorite ?? false; + const isFavorite = this.starterPreferences[species.speciesId]?.favorite ?? false; const fitsFavorite = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { if (misc.val === "FAVORITE" && misc.state === DropDownState.ON) { return isFavorite; @@ -3194,8 +2696,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { // HA Filter const speciesHasHiddenAbility = - container.species.abilityHidden !== container.species.ability1 - && container.species.abilityHidden !== AbilityId.NONE; + species.abilityHidden !== species.ability1 && species.abilityHidden !== AbilityId.NONE; const hasHA = starterData.abilityAttr & AbilityAttr.ABILITY_HIDDEN; const fitsHA = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { if (misc.val === "HIDDEN_ABILITY" && misc.state === DropDownState.ON) { @@ -3210,7 +2711,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { }); // Egg Purchasable Filter - const isEggPurchasable = this.isSameSpeciesEggAvailable(container.species.speciesId); + const isEggPurchasable = isSameSpeciesEggAvailable(species.speciesId); const fitsEgg = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { if (misc.val === "EGG" && misc.state === DropDownState.ON) { return isEggPurchasable; @@ -3226,10 +2727,10 @@ export class StarterSelectUiHandler extends MessageUiHandler { // Pokerus Filter const fitsPokerus = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { if (misc.val === "POKERUS" && misc.state === DropDownState.ON) { - return this.pokerusSpecies.includes(container.species); + return this.pokerusSpecies.includes(species); } if (misc.val === "POKERUS" && misc.state === DropDownState.EXCLUDE) { - return !this.pokerusSpecies.includes(container.species); + return !this.pokerusSpecies.includes(species); } if (misc.val === "POKERUS" && misc.state === DropDownState.OFF) { return true; @@ -3248,157 +2749,154 @@ export class StarterSelectUiHandler extends MessageUiHandler { && fitsEgg && fitsPokerus ) { - this.filteredStarterContainers.push(container); + this.filteredStarterIds.push(species.speciesId); } }); - this.starterSelectScrollBar.setTotalRows(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1)); + this.starterSelectScrollBar.setTotalRows(Math.max(Math.ceil(this.filteredStarterIds.length / 9), 1)); this.starterSelectScrollBar.setScrollCursor(0); // sort const sort = this.filterBar.getVals(DropDownColumn.SORT)[0]; - this.filteredStarterContainers.sort((a, b) => { + this.filteredStarterIds.sort((a, b) => { switch (sort.val) { case SortCriteria.NUMBER: - return (a.species.speciesId - b.species.speciesId) * -sort.dir; + return (a - b) * -sort.dir; case SortCriteria.COST: - return (a.cost - b.cost) * -sort.dir; + return ( + (globalScene.gameData.getSpeciesStarterValue(a) - globalScene.gameData.getSpeciesStarterValue(b)) + * -sort.dir + ); case SortCriteria.CANDY: { - const candyCountA = globalScene.gameData.starterData[a.species.speciesId].candyCount; - const candyCountB = globalScene.gameData.starterData[b.species.speciesId].candyCount; + const candyCountA = globalScene.gameData.starterData[a].candyCount; + const candyCountB = globalScene.gameData.starterData[b].candyCount; return (candyCountA - candyCountB) * -sort.dir; } case SortCriteria.IV: { const avgIVsA = - globalScene.gameData.dexData[a.species.speciesId].ivs.reduce((a, b) => a + b, 0) - / globalScene.gameData.dexData[a.species.speciesId].ivs.length; + globalScene.gameData.dexData[a].ivs.reduce((a, b) => a + b, 0) / globalScene.gameData.dexData[a].ivs.length; const avgIVsB = - globalScene.gameData.dexData[b.species.speciesId].ivs.reduce((a, b) => a + b, 0) - / globalScene.gameData.dexData[b.species.speciesId].ivs.length; + globalScene.gameData.dexData[b].ivs.reduce((a, b) => a + b, 0) / globalScene.gameData.dexData[b].ivs.length; return (avgIVsA - avgIVsB) * -sort.dir; } case SortCriteria.NAME: - return a.species.name.localeCompare(b.species.name) * -sort.dir; + return getPokemonSpecies(a).name.localeCompare(getPokemonSpecies(b).name) * -sort.dir; case SortCriteria.CAUGHT: return ( - (globalScene.gameData.dexData[a.species.speciesId].caughtCount - - globalScene.gameData.dexData[b.species.speciesId].caughtCount) - * -sort.dir + (globalScene.gameData.dexData[a].caughtCount - globalScene.gameData.dexData[b].caughtCount) * -sort.dir ); case SortCriteria.HATCHED: return ( - (globalScene.gameData.dexData[a.species.speciesId].hatchedCount - - globalScene.gameData.dexData[b.species.speciesId].hatchedCount) - * -sort.dir + (globalScene.gameData.dexData[a].hatchedCount - globalScene.gameData.dexData[b].hatchedCount) * -sort.dir ); } return 0; }); this.updateScroll(); + this.tryUpdateValue(); }; - override destroy(): void { - // Without this the reference gets hung up and no startercontainers get GCd - this.starterContainers = []; - /* TODO: Uncomment this once our testing infra supports mocks of `Phaser.GameObject.Group` - this.instructionElemGroup.destroy(true); - */ - } - updateScroll = () => { - const maxColumns = 9; - const maxRows = 9; - const onScreenFirstIndex = this.scrollCursor * maxColumns; - const onScreenLastIndex = Math.min( - this.filteredStarterContainers.length - 1, - onScreenFirstIndex + maxRows * maxColumns - 1, - ); + const onScreenFirstIndex = this.scrollCursor * COLUMNS; this.starterSelectScrollBar.setScrollCursor(this.scrollCursor); - let pokerusCursorIndex = 0; - this.filteredStarterContainers.forEach((container, i) => { - const { dexEntry, starterDataEntry } = this.getSpeciesData(container.species.speciesId); + this.pokerusCursorObjs.forEach(cursor => cursor.setVisible(false)); + this.starterCursorObjs.forEach(cursor => cursor.setVisible(false)); - const pos = calcStarterPosition(i, this.scrollCursor); - container.setPosition(pos.x, pos.y); - if (i < onScreenFirstIndex || i > onScreenLastIndex) { + let pokerusCursorIndex = 0; + this.starterContainers.forEach((container, i) => { + const offset_i = i + onScreenFirstIndex; + if (offset_i >= this.filteredStarterIds.length) { container.setVisible(false); + } else { + container.setVisible(true); + + const speciesId = this.filteredStarterIds[offset_i]; + const species = getPokemonSpecies(speciesId); + const { dexEntry, starterDataEntry } = getSpeciesData(species.speciesId); + const props = this.getSpeciesPropsFromPreferences(species); + + container.setSpecies(species, props); + + const starterSprite = container.icon as Phaser.GameObjects.Sprite; + starterSprite.setTexture( + species.getIconAtlasKey(props.formIndex, props.shiny, props.variant), + container.species.getIconId(props.female!, props.formIndex, props.shiny, props.variant), + ); + container.checkIconId(props.female, props.formIndex, props.shiny, props.variant); + + const caughtAttr = dexEntry.caughtAttr; + + if (caughtAttr & species.getFullUnlocksData() || globalScene.dexForDevs) { + container.icon.clearTint(); + } else if (dexEntry.seenAttr) { + container.icon.setTint(0x808080); + } else { + container.icon.setTint(0); + } if (this.pokerusSpecies.includes(container.species)) { - this.pokerusCursorObjs[pokerusCursorIndex].setPosition(pos.x - 1, pos.y + 1).setVisible(false); + this.pokerusCursorObjs[pokerusCursorIndex].setPosition(container.x - 1, container.y + 1).setVisible(true); pokerusCursorIndex++; } if (this.starterSpecies.includes(container.species)) { this.starterCursorObjs[this.starterSpecies.indexOf(container.species)] - .setPosition(pos.x - 1, pos.y + 1) - .setVisible(false); - } - return; - } - container.setVisible(true); - - if (this.pokerusSpecies.includes(container.species)) { - this.pokerusCursorObjs[pokerusCursorIndex].setPosition(pos.x - 1, pos.y + 1).setVisible(true); - pokerusCursorIndex++; - } - - if (this.starterSpecies.includes(container.species)) { - this.starterCursorObjs[this.starterSpecies.indexOf(container.species)] - .setPosition(pos.x - 1, pos.y + 1) - .setVisible(true); - } - - const speciesId = container.species.speciesId; - this.updateStarterValueLabel(container); - - container.label.setVisible(true); - const speciesVariants = - speciesId && dexEntry.caughtAttr & DexAttr.SHINY - ? [DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3].filter(v => !!(dexEntry.caughtAttr & v)) - : []; - for (let v = 0; v < 3; v++) { - const hasVariant = speciesVariants.length > v; - container.shinyIcons[v].setVisible(hasVariant); - if (hasVariant) { - container.shinyIcons[v].setTint( - getVariantTint( - speciesVariants[v] === DexAttr.DEFAULT_VARIANT ? 0 : speciesVariants[v] === DexAttr.VARIANT_2 ? 1 : 2, - ), - ); - } - } - - container.starterPassiveBgs.setVisible(!!starterDataEntry.passiveAttr); - container.hiddenAbilityIcon.setVisible(!!dexEntry.caughtAttr && !!(starterDataEntry.abilityAttr & 4)); - container.classicWinIcon - .setVisible(starterDataEntry.classicWinCount > 0) - .setTexture(dexEntry.ribbons.has(RibbonData.NUZLOCKE) ? "champion_ribbon_emerald" : "champion_ribbon"); - container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false); - - // 'Candy Icon' mode - if (globalScene.candyUpgradeDisplay === 0) { - if (!starterColors[speciesId]) { - // Default to white if no colors are found - starterColors[speciesId] = ["ffffff", "ffffff"]; + .setPosition(container.x - 1, container.y + 1) + .setVisible(true); } - // Set the candy colors - container.candyUpgradeIcon.setTint(argbFromRgba(rgbHexToRgba(starterColors[speciesId][0]))); - container.candyUpgradeOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(starterColors[speciesId][1]))); + this.updateStarterValueLabel(container); - this.setUpgradeIcon(container); - } else if (globalScene.candyUpgradeDisplay === 1) { - container.candyUpgradeIcon.setVisible(false); - container.candyUpgradeOverlayIcon.setVisible(false); + container.label.setVisible(true); + const speciesVariants = + speciesId && dexEntry.caughtAttr & DexAttr.SHINY + ? [DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3].filter(v => !!(dexEntry.caughtAttr & v)) + : []; + for (let v = 0; v < 3; v++) { + const hasVariant = speciesVariants.length > v; + container.shinyIcons[v].setVisible(hasVariant); + if (hasVariant) { + container.shinyIcons[v].setTint( + getVariantTint( + speciesVariants[v] === DexAttr.DEFAULT_VARIANT ? 0 : speciesVariants[v] === DexAttr.VARIANT_2 ? 1 : 2, + ), + ); + } + } + + container.starterPassiveBgs.setVisible(!!starterDataEntry.passiveAttr); + container.hiddenAbilityIcon.setVisible(!!dexEntry.caughtAttr && !!(starterDataEntry.abilityAttr & 4)); + container.classicWinIcon + .setVisible(starterDataEntry.classicWinCount > 0) + .setTexture(dexEntry.ribbons.has(RibbonData.NUZLOCKE) ? "champion_ribbon_emerald" : "champion_ribbon"); + container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false); + + // 'Candy Icon' mode + if (globalScene.candyUpgradeDisplay === 0) { + if (!starterColors[speciesId]) { + // Default to white if no colors are found + starterColors[speciesId] = ["ffffff", "ffffff"]; + } + + // Set the candy colors + container.candyUpgradeIcon.setTint(argbFromRgba(rgbHexToRgba(starterColors[speciesId][0]))); + container.candyUpgradeOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(starterColors[speciesId][1]))); + + this.setUpgradeIcon(container); + } else if (globalScene.candyUpgradeDisplay === 1) { + container.candyUpgradeIcon.setVisible(false); + container.candyUpgradeOverlayIcon.setVisible(false); + } } }); }; setCursor(cursor: number): boolean { let changed = false; + this.oldCursor = this.cursor; if (this.filterMode) { changed = this.filterBarCursor !== cursor; @@ -3406,25 +2904,19 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.filterBar.setCursor(cursor); } else { - cursor = Math.max(Math.min(this.filteredStarterContainers.length - 1, cursor), 0); + cursor = Math.max(Math.min(this.starterContainers.length - 1, cursor), 0); changed = super.setCursor(cursor); - const pos = calcStarterPosition(cursor, this.scrollCursor); + const pos = calcStarterContainerPosition(cursor); this.cursorObj.setPosition(pos.x - 1, pos.y + 1); - const species = this.filteredStarterContainers[cursor]?.species; + const species = this.starterContainers[cursor]?.species; // TODO: why is there a "?" if (species) { - const defaultDexAttr = this.getCurrentDexProps(species.speciesId); - const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); - // Bang is correct due to the `?` before variant - const variant = this.starterPreferences[species.speciesId]?.variant - ? (this.starterPreferences[species.speciesId]!.variant as Variant) - : defaultProps.variant; - const tint = getVariantTint(variant); - this.pokemonShinyIcon.setFrame(getVariantIcon(variant)).setTint(tint); this.setSpecies(species); this.updateInstructions(); + } else { + this.setNoSpecies(); } } @@ -3439,7 +2931,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.filterMode = filterMode; this.setCursor(filterMode ? this.filterBarCursor : this.cursor); if (filterMode) { - this.setSpecies(null); + this.setNoSpecies(); //TODO: this probably needs to go somewhere else this.updateInstructions(); } @@ -3452,748 +2944,199 @@ export class StarterSelectUiHandler extends MessageUiHandler { moveStarterIconsCursor(index: number): void { this.starterIconsCursorObj.setPositionRelative( this.starterIcons[index], - this.starterIconsCursorXOffset, - this.starterIconsCursorYOffset, + STARTER_ICONS_CURSOR_X_OFFSET, + STARTER_ICONS_CURSOR_Y_OFFSET, ); if (this.starterSpecies.length > 0) { this.starterIconsCursorObj.setVisible(true); this.setSpecies(this.starterSpecies[index]); } else { this.starterIconsCursorObj.setVisible(false); - this.setSpecies(null); + this.setNoSpecies(); } } - getFriendship(speciesId: number) { - let currentFriendship = globalScene.gameData.starterData[speciesId].friendship; - if (!currentFriendship || currentFriendship === undefined) { - currentFriendship = 0; - } - - const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[speciesId]); - - return { currentFriendship, friendshipCap }; - } - - setSpecies(species: PokemonSpecies | null) { + setNoSpecies() { this.speciesStarterDexEntry = null; this.dexAttrCursor = 0n; this.abilityCursor = 0; this.natureCursor = 0; this.teraCursor = PokemonType.UNKNOWN; - if (species) { - const { dexEntry } = this.getSpeciesData(species.speciesId); - this.speciesStarterDexEntry = dexEntry; - this.dexAttrCursor = this.getCurrentDexProps(species.speciesId); - this.abilityCursor = globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); - this.natureCursor = globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); - this.teraCursor = species.type1; + if (this.lastSpecies) { + this.stopIconAnimation(this.oldCursor); } - if (!species && globalScene.ui.getTooltip().visible) { - globalScene.ui.hideTooltip(); - } + this.starterSummary.setNoSpecies(); + } - this.pokemonAbilityText.off("pointerover"); - this.pokemonPassiveText.off("pointerover"); + setSpecies(species: PokemonSpecies) { + const { dexEntry } = getSpeciesData(species.speciesId); + // This stuff is probably redundant. + this.speciesStarterDexEntry = dexEntry; + this.dexAttrCursor = getDexAttrFromPreferences(species.speciesId, this.starterPreferences[species.speciesId]); + this.abilityCursor = globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); + this.natureCursor = globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); + this.teraCursor = species.type1; - const starterAttributes: StarterAttributes | null = species - ? { ...this.starterPreferences[species.speciesId] } - : null; - - if (starterAttributes?.nature) { - // load default nature from stater save data, if set - this.natureCursor = starterAttributes.nature; + // Then, we override with preferences, if they exist + const starterPreferences = this.starterPreferences[species.speciesId]; + if (starterPreferences?.nature) { + this.natureCursor = starterPreferences.nature; } - if (starterAttributes?.ability && !Number.isNaN(starterAttributes.ability)) { - // load default ability from stater save data, if set - this.abilityCursor = starterAttributes.ability; + if (starterPreferences?.abilityIndex && !Number.isNaN(starterPreferences.abilityIndex)) { + this.abilityCursor = starterPreferences.abilityIndex; } - if (starterAttributes?.tera) { - // load default tera from starter save data, if set - this.teraCursor = starterAttributes.tera; - } - - if (this.statsMode) { - if (this.speciesStarterDexEntry?.caughtAttr) { - this.statsContainer.setVisible(true); - this.showStats(); - } else { - this.statsContainer.setVisible(false); - //@ts-expect-error - this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. what. how? huh? - } + if (starterPreferences?.tera) { + this.teraCursor = starterPreferences.tera; } if (this.lastSpecies) { - const dexAttr = this.getCurrentDexProps(this.lastSpecies.speciesId); - const props = globalScene.gameData.getSpeciesDexAttrProps(this.lastSpecies, dexAttr); - const speciesIndex = this.allSpecies.indexOf(this.lastSpecies); - const lastSpeciesIcon = this.starterContainers[speciesIndex].icon; - this.checkIconId(lastSpeciesIcon, this.lastSpecies, props.female, props.formIndex, props.shiny, props.variant); - this.iconAnimHandler.addOrUpdate(lastSpeciesIcon, PokemonIconAnimMode.NONE); - - // Resume the animation for the previously selected species - const icon = this.starterContainers[speciesIndex].icon; - globalScene.tweens.getTweensOf(icon).forEach(tween => tween.play()); + this.stopIconAnimation(this.oldCursor); } - this.lastSpecies = species!; // TODO: is this bang correct? + this.lastSpecies = species; - if (species && (this.speciesStarterDexEntry?.seenAttr || this.speciesStarterDexEntry?.caughtAttr)) { - this.pokemonNumberText.setText(padInt(species.speciesId, 4)); - if (starterAttributes?.nickname) { - const name = decodeURIComponent(escape(atob(starterAttributes.nickname))); - this.pokemonNameText.setText(name); - } else { - this.pokemonNameText.setText(species.name); + this.starterSummary.setSpecies(species, starterPreferences ?? {}); + + if (dexEntry?.caughtAttr) { + this.startIconAnimation(this.cursor); + + const props = this.getSpeciesPropsFromPreferences(species); + + this.setSpeciesDetails(species, false); + + if (props.formIndex != null) { + // If switching forms while the pokemon is in the team, update its moveset + this.updateSelectedStarterMoveset(species.speciesId); } - if (this.speciesStarterDexEntry?.caughtAttr) { - const colorScheme = starterColors[species.speciesId]; - - const luck = globalScene.gameData.getDexAttrLuck(this.speciesStarterDexEntry.caughtAttr); - this.pokemonLuckText - .setVisible(!!luck) - .setText(luck.toString()) - .setTint(getVariantTint(Math.min(luck - 1, 2) as Variant)); - this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible); - - //Growth translate - let growthReadable = toTitleCase(GrowthRate[species.growthRate]); - const growthAux = toCamelCase(growthReadable); - if (i18next.exists("growth:" + growthAux)) { - growthReadable = i18next.t(("growth:" + growthAux) as any); - } - this.pokemonGrowthRateText - .setText(growthReadable) - .setColor(getGrowthRateColor(species.growthRate)) - .setShadowColor(getGrowthRateColor(species.growthRate, true)); - this.pokemonGrowthRateLabelText.setVisible(true); - this.pokemonUncaughtText.setVisible(false); - this.pokemonAbilityLabelText.setVisible(true); - this.pokemonPassiveLabelText.setVisible(true); - this.pokemonNatureLabelText.setVisible(true); - this.pokemonCaughtCountText.setText(`${this.speciesStarterDexEntry.caughtCount}`); - if (species.speciesId === SpeciesId.MANAPHY || species.speciesId === SpeciesId.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; - const tint = getVariantTint(variant); - this.pokemonShinyIcon.setFrame(getVariantIcon(variant)).setTint(tint).setVisible(defaultProps.shiny); - this.pokemonCaughtHatchedContainer.setVisible(true); - this.pokemonFormText.setVisible(true); - - if (pokemonPrevolutions.hasOwnProperty(species.speciesId)) { - this.pokemonCaughtHatchedContainer.setY(16); - this.pokemonShinyIcon.setY(135).setFrame(getVariantIcon(variant)); - [this.pokemonCandyContainer, this.pokemonHatchedIcon, this.pokemonHatchedCountText].map(c => - c.setVisible(false), - ); - this.pokemonFormText.setY(25); - } else { - this.pokemonCaughtHatchedContainer.setY(25); - this.pokemonShinyIcon.setY(117); - this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0]))); - this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1]))); - this.pokemonCandyCountText.setText(`×${globalScene.gameData.starterData[species.speciesId].candyCount}`); - this.pokemonFormText.setY(42); - this.pokemonHatchedIcon.setVisible(true); - this.pokemonHatchedCountText.setVisible(true); - - const { currentFriendship, friendshipCap } = this.getFriendship(this.lastSpecies.speciesId); - const candyCropY = 16 - 16 * (currentFriendship / friendshipCap); - this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY); - - this.pokemonCandyContainer - .setVisible(true) - .on("pointerover", () => { - globalScene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true); - this.activeTooltip = "CANDY"; - }) - .on("pointerout", () => { - globalScene.ui.hideTooltip(); - this.activeTooltip = undefined; - }); - } - - // Pause the animation when the species is selected - const speciesIndex = this.allSpecies.indexOf(species); - const icon = this.starterContainers[speciesIndex].icon; - - if (this.isUpgradeAnimationEnabled()) { - globalScene.tweens.getTweensOf(icon).forEach(tween => tween.pause()); - // Reset the position of the icon - icon.x = -2; - icon.y = 2; - } - - // Initiates the small up and down idle animation - this.iconAnimHandler.addOrUpdate(icon, PokemonIconAnimMode.PASSIVE); - - const starterIndex = this.starterSpecies.indexOf(species); - - const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); - - if (starterIndex > -1) { - const starter = this.starters[starterIndex]; - this.setSpeciesDetails( - species, - { - shiny: starter.shiny, - formIndex: starter.formIndex, - female: starter.female, - variant: starter.variant, - abilityIndex: starter.abilityIndex, - natureIndex: starter.nature, - teraType: starter.teraType, - }, - false, - ); - } else { - const defaultAbilityIndex = - starterAttributes?.ability ?? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); - // load default nature from stater save data, if set - const { dexEntry } = this.getSpeciesData(species.speciesId); - const defaultNature = - starterAttributes?.nature || globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); - if (starterAttributes?.variant && !Number.isNaN(starterAttributes.variant) && props.shiny) { - props.variant = starterAttributes.variant as Variant; - } - props.formIndex = starterAttributes?.form ?? props.formIndex; - props.female = starterAttributes?.female ?? props.female; - - this.setSpeciesDetails( - species, - { - shiny: props.shiny, - formIndex: props.formIndex, - female: props.female, - variant: props.variant, - abilityIndex: defaultAbilityIndex, - natureIndex: defaultNature, - teraType: starterAttributes?.tera, - }, - false, - ); - } - - if (props.formIndex != null) { - // If switching forms while the pokemon is in the team, update its moveset - this.updateSelectedStarterMoveset(species.speciesId); - } - - const speciesForm = getPokemonSpeciesForm(species.speciesId, props.formIndex); - this.setTypeIcons(speciesForm.type1, speciesForm.type2); - - this.pokemonSprite.clearTint(); - if (this.pokerusSpecies.includes(species)) { - handleTutorial(Tutorial.Pokerus); - } - } else { - this.pokemonGrowthRateText.setText(""); - this.pokemonGrowthRateLabelText.setVisible(false); - this.type1Icon.setVisible(false); - this.type2Icon.setVisible(false); - this.pokemonLuckLabelText.setVisible(false); - this.pokemonLuckText.setVisible(false); - this.pokemonShinyIcon.setVisible(false); - this.pokemonUncaughtText.setVisible(true); - this.pokemonAbilityLabelText.setVisible(false); - this.pokemonPassiveLabelText.setVisible(false); - this.pokemonNatureLabelText.setVisible(false); - this.pokemonCaughtHatchedContainer.setVisible(false); - this.pokemonCandyContainer.setVisible(false); - this.pokemonFormText.setVisible(false); - this.teraIcon.setVisible(false); - - const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true); - const defaultAbilityIndex = globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); - const defaultNature = globalScene.gameData.getSpeciesDefaultNature(species); - const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); - - this.setSpeciesDetails( - species, - { - shiny: props.shiny, - formIndex: props.formIndex, - female: props.female, - variant: props.variant, - abilityIndex: defaultAbilityIndex, - natureIndex: defaultNature, - forSeen: true, - }, - false, - ); - this.pokemonSprite.setTint(0x808080); + if (this.pokerusSpecies.includes(species)) { + handleTutorial(Tutorial.Pokerus); } + } else if (dexEntry?.seenAttr) { + this.resetSpeciesDetails(); } else { - this.pokemonNumberText.setText(padInt(0, 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); - this.pokemonShinyIcon.setVisible(false); - this.pokemonUncaughtText.setVisible(!!species); - this.pokemonAbilityLabelText.setVisible(false); - this.pokemonPassiveLabelText.setVisible(false); - this.pokemonNatureLabelText.setVisible(false); - this.pokemonCaughtHatchedContainer.setVisible(false); - this.pokemonCandyContainer.setVisible(false); - this.pokemonFormText.setVisible(false); - this.teraIcon.setVisible(false); - - this.setSpeciesDetails( - species!, - { - // TODO: is this bang correct? - shiny: false, - formIndex: 0, - female: false, - variant: 0, - abilityIndex: 0, - natureIndex: 0, - }, - false, - ); - this.pokemonSprite.clearTint(); + this.resetSpeciesDetails(); } } - getSpeciesData( - speciesId: SpeciesId, - applyChallenge = true, - ): { dexEntry: DexEntry; starterDataEntry: StarterDataEntry } { - const dexEntry = globalScene.gameData.dexData[speciesId]; - const starterDataEntry = globalScene.gameData.starterData[speciesId]; - - // Unpacking to make a copy by values, not references - const copiedDexEntry = { ...dexEntry }; - copiedDexEntry.ivs = [...dexEntry.ivs]; - const copiedStarterDataEntry = { ...starterDataEntry }; - if (applyChallenge) { - applyChallenges(ChallengeType.STARTER_SELECT_MODIFY, speciesId, copiedDexEntry, copiedStarterDataEntry); + startIconAnimation(cursor: number) { + const container = this.starterContainers[cursor]; + const icon = container.icon; + if (isUpgradeAnimationEnabled()) { + globalScene.tweens.getTweensOf(icon).forEach(tween => tween.pause()); + // Reset the position of the icon + icon.x = -2; + icon.y = 2; } - return { dexEntry: { ...copiedDexEntry }, starterDataEntry: { ...copiedStarterDataEntry } }; + // Initiates the small up and down idle animation + this.iconAnimHandler.addOrUpdate(icon, PokemonIconAnimMode.PASSIVE); } - setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, save = true): void { - let { shiny, formIndex, female, variant, abilityIndex, natureIndex, teraType } = options; - const forSeen: boolean = options.forSeen ?? false; - const oldProps = species ? globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null; - const oldAbilityIndex = - this.abilityCursor > -1 ? this.abilityCursor : globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); - let oldNatureIndex = -1; - if (species) { - const { dexEntry } = this.getSpeciesData(species.speciesId); - oldNatureIndex = - this.natureCursor > -1 ? this.natureCursor : globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); + stopIconAnimation(cursor: number) { + const container = this.starterContainers[cursor]; + if (container) { + const lastSpeciesIcon = container.icon; + const props = this.getSpeciesPropsFromPreferences(container.species); + this.checkIconId(lastSpeciesIcon, container.species, props.female, props.formIndex, props.shiny, props.variant); + this.iconAnimHandler.addOrUpdate(lastSpeciesIcon, PokemonIconAnimMode.NONE); + // Resume the animation for the previously selected species + globalScene.tweens.getTweensOf(lastSpeciesIcon).forEach(tween => tween.resume()); } - const oldTeraType = this.teraCursor > -1 ? this.teraCursor : species ? species.type1 : PokemonType.UNKNOWN; + } + + resetSpeciesDetails() { this.dexAttrCursor = 0n; this.abilityCursor = -1; this.natureCursor = -1; this.teraCursor = PokemonType.UNKNOWN; - // 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 && female != null) || formIndex != null || shiny != null || variant != null; - - const isFreshStartChallenge = globalScene.gameMode.hasChallenge(Challenges.FRESH_START); - - if (this.activeTooltip === "CANDY") { - if (this.lastSpecies && this.pokemonCandyContainer.visible) { - const { currentFriendship, friendshipCap } = this.getFriendship(this.lastSpecies.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) { - this.dexAttrCursor |= (shiny !== undefined ? !shiny : !(shiny = oldProps?.shiny)) - ? DexAttr.NON_SHINY - : DexAttr.SHINY; - this.dexAttrCursor |= (female !== undefined ? !female : !(female = oldProps?.female)) - ? DexAttr.MALE - : DexAttr.FEMALE; - this.dexAttrCursor |= (variant !== undefined ? !variant : !(variant = oldProps?.variant)) - ? DexAttr.DEFAULT_VARIANT - : variant === 1 - ? DexAttr.VARIANT_2 - : DexAttr.VARIANT_3; - this.dexAttrCursor |= globalScene.gameData.getFormAttr( - formIndex !== undefined ? formIndex : (formIndex = oldProps!.formIndex), - ); // TODO: is this bang correct? - this.abilityCursor = abilityIndex !== undefined ? abilityIndex : (abilityIndex = oldAbilityIndex); - this.natureCursor = natureIndex !== undefined ? natureIndex : (natureIndex = oldNatureIndex); - this.teraCursor = teraType != null ? teraType : (teraType = oldTeraType); - const [isInParty, partyIndex]: [boolean, number] = this.isInParty(species); // we use this to firstly check if the pokemon is in the party, and if so, to get the party index in order to update the icon image - if (isInParty) { - this.updatePartyIcon(species, partyIndex); - } - } - - this.pokemonSprite.setVisible(false); - this.pokemonPassiveLabelText.setVisible(false); - this.pokemonPassiveText.setVisible(false); - this.pokemonPassiveDisabledIcon.setVisible(false); - this.pokemonPassiveLockedIcon.setVisible(false); - this.teraIcon.setVisible(false); - - if (this.assetLoadCancelled) { - this.assetLoadCancelled.value = true; - this.assetLoadCancelled = null; - } this.starterMoveset = null; this.speciesStarterMoves = []; - if (species) { - const { dexEntry, starterDataEntry } = this.getSpeciesData(species.speciesId); - const caughtAttr = dexEntry.caughtAttr || BigInt(0); - const abilityAttr = starterDataEntry.abilityAttr; + this.tryUpdateValue(); + this.updateInstructions(); + } - if (!caughtAttr) { - const props = globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)); - const defaultAbilityIndex = globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); - const defaultNature = globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); + setSpeciesDetails(species: PokemonSpecies, save = true): void { + // Here we pass some options to override everything else - if (shiny === undefined || shiny !== props.shiny) { - shiny = props.shiny; - } - if (formIndex === undefined || formIndex !== props.formIndex) { - formIndex = props.formIndex; - } - if (female === undefined || female !== props.female) { - female = props.female; - } - if (variant === undefined || variant !== props.variant) { - variant = props.variant; - } - if (abilityIndex === undefined || abilityIndex !== defaultAbilityIndex) { - abilityIndex = defaultAbilityIndex; - } - if (natureIndex === undefined || natureIndex !== defaultNature) { - natureIndex = defaultNature; - } - } + const speciesDetails = getSpeciesDetailsFromPreferences(species, this.starterPreferences[species.speciesId]); + let { shiny, formIndex, female, variant, abilityIndex, natureIndex, teraType } = speciesDetails; - this.shinyOverlay.setVisible(shiny ?? false); // TODO: is false the correct default? - this.pokemonNumberText.setColor( - getTextColor(shiny ? TextStyle.SUMMARY_DEX_NUM_GOLD : TextStyle.SUMMARY_DEX_NUM, false), + this.starterSummary.setSpeciesDetails(species, speciesDetails); + + // Storing old cursor values... + const oldProps = globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor); + const oldAbilityIndex = + this.abilityCursor > -1 ? this.abilityCursor : globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); + let oldNatureIndex = -1; + const { dexEntry } = getSpeciesData(species.speciesId); + oldNatureIndex = + this.natureCursor > -1 ? this.natureCursor : globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); + const oldTeraType = this.teraCursor > -1 ? this.teraCursor : species.type1; + + // Before we reset them to null values + this.dexAttrCursor = 0n; + this.abilityCursor = -1; + this.natureCursor = -1; + this.teraCursor = PokemonType.UNKNOWN; + + // Update cursors + this.dexAttrCursor |= (shiny !== undefined ? !shiny : !(shiny = oldProps?.shiny)) + ? DexAttr.NON_SHINY + : DexAttr.SHINY; + this.dexAttrCursor |= (female !== undefined ? !female : !(female = oldProps?.female)) + ? DexAttr.MALE + : DexAttr.FEMALE; + this.dexAttrCursor |= (variant !== undefined ? !variant : !(variant = oldProps?.variant)) + ? DexAttr.DEFAULT_VARIANT + : variant === 1 + ? DexAttr.VARIANT_2 + : DexAttr.VARIANT_3; + this.dexAttrCursor |= globalScene.gameData.getFormAttr( + formIndex !== undefined ? formIndex : (formIndex = oldProps!.formIndex), + ); // TODO: is this bang correct? + this.abilityCursor = abilityIndex !== undefined ? abilityIndex : (abilityIndex = oldAbilityIndex); + this.natureCursor = natureIndex !== undefined ? natureIndex : (natureIndex = oldNatureIndex); + this.teraCursor = teraType != null ? teraType : (teraType = oldTeraType); + + const [isInParty, partyIndex]: [boolean, number] = this.isInParty(species); // we use this to firstly check if the pokemon is in the party, and if so, to get the party index in order to update the icon image + if (isInParty) { + this.updatePartyIcon(species, partyIndex); + } + + const starterIndex = this.starterSpecies.indexOf(species); + + if (starterIndex > -1) { + const starter = this.starters[starterIndex]; + const props = globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor); + starter.shiny = props.shiny; + starter.variant = props.variant; + starter.female = props.female; + starter.formIndex = props.formIndex; + starter.abilityIndex = this.abilityCursor; + starter.nature = this.natureCursor; + starter.teraType = this.teraCursor; + } + + female ??= false; + + // Update the starter container (is this it?) + const currentContainer = this.starterContainers.find(p => p.species.speciesId === species.speciesId); + if (currentContainer) { + const starterSprite = currentContainer.icon as Phaser.GameObjects.Sprite; + starterSprite.setTexture( + species.getIconAtlasKey(formIndex, shiny, variant), + species.getIconId(female, formIndex, shiny, variant), ); - this.pokemonNumberText.setShadowColor( - getTextColor(shiny ? TextStyle.SUMMARY_DEX_NUM_GOLD : TextStyle.SUMMARY_DEX_NUM, true), - ); - - if (forSeen ? this.speciesStarterDexEntry?.seenAttr : this.speciesStarterDexEntry?.caughtAttr) { - const starterIndex = this.starterSpecies.indexOf(species); - - if (starterIndex > -1) { - const starter = this.starters[starterIndex]; - const props = globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor); - starter.shiny = props.shiny; - starter.variant = props.variant; - starter.female = props.female; - starter.formIndex = props.formIndex; - starter.abilityIndex = this.abilityCursor; - starter.nature = this.natureCursor; - starter.teraType = this.teraCursor; - } - - const assetLoadCancelled = new BooleanHolder(false); - this.assetLoadCancelled = assetLoadCancelled; - - female ??= false; - if (shouldUpdateSprite) { - species.loadAssets(female, formIndex, shiny, variant, true).then(() => { - if (assetLoadCancelled.value) { - return; - } - this.assetLoadCancelled = null; - this.speciesLoaded.set(species.speciesId, true); - // Note: Bangs are correct due to `female ??= false` above - this.pokemonSprite - .play(species.getSpriteKey(female!, formIndex, shiny, variant)) - .setPipelineData("shiny", shiny) - .setPipelineData("variant", variant) - .setPipelineData("spriteKey", species.getSpriteKey(female!, formIndex, shiny, variant)) - .setVisible(!this.statsMode); - }); - } else { - this.pokemonSprite.setVisible(!this.statsMode); - } - - const currentFilteredContainer = this.filteredStarterContainers.find( - p => p.species.speciesId === species.speciesId, - ); - if (currentFilteredContainer) { - const starterSprite = currentFilteredContainer.icon as Phaser.GameObjects.Sprite; - starterSprite.setTexture( - species.getIconAtlasKey(formIndex, shiny, variant), - species.getIconId(female, formIndex, shiny, variant), - ); - currentFilteredContainer.checkIconId(female, formIndex, shiny, variant); - } - - 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; - - const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1; - let hasAbility2 = abilityAttr & AbilityAttr.ABILITY_2; - const hasHiddenAbility = abilityAttr & AbilityAttr.ABILITY_HIDDEN; - - /* - * Check for Pokemon with a single ability (at some point it was possible to catch them with their ability 2 attribute) - * This prevents cycling between ability 1 and 2 if they are both unlocked and the same - * but we still need to account for the possibility ability 1 was never unlocked and fallback on ability 2 in this case - */ - if (hasAbility1 && hasAbility2 && species.ability1 === species.ability2) { - hasAbility2 = 0; - } - - this.canCycleAbility = [hasAbility1, hasAbility2, hasHiddenAbility].filter(a => a).length > 1; - - this.canCycleForm = - species.forms - .filter(f => f.isStarterSelectable || !pokemonFormChanges[species.speciesId]?.find(fc => fc.formKey)) - .map((_, f) => dexEntry.caughtAttr & globalScene.gameData.getFormAttr(f)) - .filter(f => f).length > 1; - this.canCycleNature = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr).length > 1; - this.canCycleTera = - !this.statsMode - && this.allowTera - && getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2 != null - && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START); - } - - if (dexEntry.caughtAttr && species.malePercent !== null) { - const gender = !female ? Gender.MALE : Gender.FEMALE; - this.pokemonGenderText - .setText(getGenderSymbol(gender)) - .setColor(getGenderColor(gender)) - .setShadowColor(getGenderColor(gender, true)); - } else { - this.pokemonGenderText.setText(""); - } - - if (dexEntry.caughtAttr) { - let ability: Ability; - if (this.lastSpecies.forms?.length > 1) { - ability = allAbilities[this.lastSpecies.forms[formIndex ?? 0].getAbility(abilityIndex!)]; - } else { - ability = allAbilities[this.lastSpecies.getAbility(abilityIndex!)]; // TODO: is this bang correct? - } - - const isHidden = abilityIndex === (this.lastSpecies.ability2 ? 2 : 1); - this.pokemonAbilityText - .setText(ability.name) - .setColor(getTextColor(!isHidden ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GOLD)) - .setShadowColor(getTextColor(!isHidden ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GOLD, true)); - - const passiveAttr = starterDataEntry.passiveAttr; - const passiveAbility = allAbilities[this.lastSpecies.getPassiveAbility(formIndex)]; - - if (this.pokemonAbilityText.visible) { - if (this.activeTooltip === "ABILITY") { - globalScene.ui.editTooltip(`${ability.name}`, `${ability.description}`); - } - - this.pokemonAbilityText.on("pointerover", () => { - globalScene.ui.showTooltip(`${ability.name}`, `${ability.description}`, true); - this.activeTooltip = "ABILITY"; - }); - this.pokemonAbilityText.on("pointerout", () => { - globalScene.ui.hideTooltip(); - this.activeTooltip = undefined; - }); - } - - if (passiveAbility) { - const isUnlocked = !!(passiveAttr & PassiveAttr.UNLOCKED); - const isEnabled = !!(passiveAttr & PassiveAttr.ENABLED); - - const textStyle = isUnlocked && isEnabled ? TextStyle.SUMMARY_ALT : TextStyle.SUMMARY_GRAY; - const textAlpha = isUnlocked && isEnabled ? 1 : 0.5; - - this.pokemonPassiveLabelText - .setVisible(!isFreshStartChallenge) - .setColor(getTextColor(TextStyle.SUMMARY_ALT)) - .setShadowColor(getTextColor(TextStyle.SUMMARY_ALT, true)); - this.pokemonPassiveText - .setVisible(!isFreshStartChallenge) - .setText(passiveAbility.name) - .setColor(getTextColor(textStyle)) - .setAlpha(textAlpha) - .setShadowColor(getTextColor(textStyle, true)); - - if (this.activeTooltip === "PASSIVE") { - globalScene.ui.editTooltip(`${passiveAbility.name}`, `${passiveAbility.description}`); - } - - if (this.pokemonPassiveText.visible) { - this.pokemonPassiveText.on("pointerover", () => { - globalScene.ui.showTooltip(`${passiveAbility.name}`, `${passiveAbility.description}`, true); - this.activeTooltip = "PASSIVE"; - }); - this.pokemonPassiveText.on("pointerout", () => { - globalScene.ui.hideTooltip(); - this.activeTooltip = undefined; - }); - } - - const iconPosition = { - x: this.pokemonPassiveText.x + this.pokemonPassiveText.displayWidth + 1, - y: this.pokemonPassiveText.y + this.pokemonPassiveText.displayHeight / 2, - }; - this.pokemonPassiveDisabledIcon - .setVisible(isUnlocked && !isEnabled && !isFreshStartChallenge) - .setPosition(iconPosition.x, iconPosition.y); - this.pokemonPassiveLockedIcon - .setVisible(!isUnlocked && !isFreshStartChallenge) - .setPosition(iconPosition.x, iconPosition.y); - } else if (this.activeTooltip === "PASSIVE") { - // No passive and passive tooltip is active > hide it - globalScene.ui.hideTooltip(); - } - - this.pokemonNatureText.setText(getNatureName(natureIndex as unknown as Nature, true, true, false)); - - let levelMoves: LevelMoves; - if ( - pokemonFormLevelMoves.hasOwnProperty(species.speciesId) - && formIndex - && pokemonFormLevelMoves[species.speciesId].hasOwnProperty(formIndex) - ) { - levelMoves = pokemonFormLevelMoves[species.speciesId][formIndex]; - } else { - levelMoves = pokemonSpeciesLevelMoves[species.speciesId]; - } - this.speciesStarterMoves.push(...levelMoves.filter(lm => lm[0] > 0 && lm[0] <= 5).map(lm => lm[1])); - if (speciesEggMoves.hasOwnProperty(species.speciesId)) { - for (let em = 0; em < 4; em++) { - if (starterDataEntry.eggMoves & (1 << em)) { - this.speciesStarterMoves.push(speciesEggMoves[species.speciesId][em]); - } - } - } - - const speciesMoveData = starterDataEntry.moveset; - const moveData: StarterMoveset | null = speciesMoveData - ? Array.isArray(speciesMoveData) - ? speciesMoveData - : speciesMoveData[formIndex!] // TODO: is this bang correct? - : null; - const availableStarterMoves = this.speciesStarterMoves.concat( - speciesEggMoves.hasOwnProperty(species.speciesId) - ? speciesEggMoves[species.speciesId].filter((_: any, em: number) => starterDataEntry.eggMoves & (1 << em)) - : [], - ); - this.starterMoveset = (moveData || (this.speciesStarterMoves.slice(0, 4) as StarterMoveset)).filter(m => - availableStarterMoves.find(sm => sm === m), - ) as StarterMoveset; - // Consolidate move data if it contains an incompatible move - if (this.starterMoveset.length < 4 && this.starterMoveset.length < availableStarterMoves.length) { - this.starterMoveset.push( - ...availableStarterMoves - .filter(sm => this.starterMoveset?.indexOf(sm) === -1) - .slice(0, 4 - this.starterMoveset.length), - ); - } - - // Remove duplicate moves - this.starterMoveset = this.starterMoveset.filter((move, i) => { - return this.starterMoveset?.indexOf(move) === i; - }) as StarterMoveset; - - const speciesForm = getPokemonSpeciesForm(species.speciesId, formIndex!); // TODO: is the bang correct? - const formText = species.getFormNameToDisplay(formIndex); - this.pokemonFormText.setText(formText); - - this.setTypeIcons(speciesForm.type1, speciesForm.type2); - - this.teraIcon.setFrame(PokemonType[this.teraCursor].toLowerCase()); - this.teraIcon.setVisible(!this.statsMode && this.allowTera); - } else { - this.pokemonAbilityText.setText(""); - this.pokemonPassiveText.setText(""); - this.pokemonNatureText.setText(""); - this.teraIcon.setVisible(false); - this.setTypeIcons(null, null); - } - } else { - this.shinyOverlay.setVisible(false); - this.pokemonNumberText - .setColor(getTextColor(TextStyle.SUMMARY)) - .setShadowColor(getTextColor(TextStyle.SUMMARY, true)); - this.pokemonGenderText.setText(""); - this.pokemonAbilityText.setText(""); - this.pokemonPassiveText.setText(""); - this.pokemonNatureText.setText(""); - this.teraIcon.setVisible(false); - this.setTypeIcons(null, null); + currentContainer.checkIconId(female, formIndex, shiny, variant); } - if (!this.starterMoveset) { - this.starterMoveset = this.speciesStarterMoves.slice(0, 4) as StarterMoveset; - } + this.updateCanCycle(species.speciesId, formIndex); - for (let m = 0; m < 4; m++) { - const move = m < this.starterMoveset.length ? allMoves[this.starterMoveset[m]] : null; - this.pokemonMoveBgs[m].setFrame(PokemonType[move ? move.type : PokemonType.UNKNOWN].toString().toLowerCase()); - this.pokemonMoveLabels[m].setText(move ? move.name : "-"); - this.pokemonMoveContainers[m].setVisible(!!move); - } - - const hasEggMoves = species && speciesEggMoves.hasOwnProperty(species.speciesId); - let eggMoves = 0; - if (species) { - const { starterDataEntry } = this.getSpeciesData(this.lastSpecies.speciesId); - eggMoves = starterDataEntry.eggMoves; - } - - for (let em = 0; em < 4; em++) { - const eggMove = hasEggMoves ? allMoves[speciesEggMoves[species.speciesId][em]] : null; - const eggMoveUnlocked = eggMove && eggMoves & (1 << em); - this.pokemonEggMoveBgs[em].setFrame( - PokemonType[eggMove ? eggMove.type : PokemonType.UNKNOWN].toString().toLowerCase(), - ); - this.pokemonEggMoveLabels[em].setText(eggMove && eggMoveUnlocked ? eggMove.name : "???"); - } - - this.pokemonEggMovesContainer.setVisible(!!this.speciesStarterDexEntry?.caughtAttr && hasEggMoves); - - this.pokemonAdditionalMoveCountLabel - .setText(`(+${Math.max(this.speciesStarterMoves.length - 4, 0)})`) - .setVisible(this.speciesStarterMoves.length > 4); + this.updateSpeciesMoves(species.speciesId, formIndex); this.tryUpdateValue(); @@ -4204,17 +3147,110 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } - setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void { - if (type1 !== null) { - this.type1Icon.setVisible(true).setFrame(PokemonType[type1].toLowerCase()); - } else { - this.type1Icon.setVisible(false); + updateCanCycle(speciesId: SpeciesId, formIndex = 0) { + const { dexEntry, starterDataEntry } = getSpeciesData(speciesId); + const caughtAttr = dexEntry.caughtAttr || BigInt(0); + const abilityAttr = starterDataEntry.abilityAttr; + const species = getPokemonSpecies(speciesId); + + 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; + + const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1; + let hasAbility2 = abilityAttr & AbilityAttr.ABILITY_2; + const hasHiddenAbility = abilityAttr & AbilityAttr.ABILITY_HIDDEN; + + /* + * Check for Pokemon with a single ability (at some point it was possible to catch them with their ability 2 attribute) + * This prevents cycling between ability 1 and 2 if they are both unlocked and the same + * but we still need to account for the possibility ability 1 was never unlocked and fallback on ability 2 in this case + */ + if (hasAbility1 && hasAbility2 && species.ability1 === species.ability2) { + hasAbility2 = 0; } - if (type2 !== null) { - this.type2Icon.setVisible(true).setFrame(PokemonType[type2].toLowerCase()); + + this.canCycleAbility = [hasAbility1, hasAbility2, hasHiddenAbility].filter(a => a).length > 1; + + this.canCycleForm = + species.forms + .filter(f => f.isStarterSelectable || !pokemonFormChanges[species.speciesId]?.find(fc => fc.formKey)) + .map((_, f) => dexEntry.caughtAttr & globalScene.gameData.getFormAttr(f)) + .filter(f => f).length > 1; + + this.canCycleNature = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr).length > 1; + + this.canCycleTera = + !this.statsMode + && this.allowTera + && getPokemonSpeciesForm(species.speciesId, formIndex).type2 != null + && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START); + } + + updateSpeciesMoves(speciesId: SpeciesId, formIndex = 0) { + const { starterDataEntry } = getSpeciesData(speciesId); + + this.starterMoveset = null; + this.speciesStarterMoves = []; + + let levelMoves: LevelMoves; + if ( + pokemonFormLevelMoves.hasOwnProperty(speciesId) + && formIndex + && pokemonFormLevelMoves[speciesId].hasOwnProperty(formIndex) + ) { + levelMoves = pokemonFormLevelMoves[speciesId][formIndex]; } else { - this.type2Icon.setVisible(false); + levelMoves = pokemonSpeciesLevelMoves[speciesId]; } + this.speciesStarterMoves.push(...levelMoves.filter(lm => lm[0] > 0 && lm[0] <= 5).map(lm => lm[1])); + if (speciesEggMoves.hasOwnProperty(speciesId)) { + for (let em = 0; em < 4; em++) { + if (starterDataEntry.eggMoves & (1 << em)) { + this.speciesStarterMoves.push(speciesEggMoves[speciesId][em]); + } + } + } + + const speciesMoveData = starterDataEntry.moveset; + const moveData: StarterMoveset | null = speciesMoveData + ? Array.isArray(speciesMoveData) + ? speciesMoveData + : speciesMoveData[formIndex!] // TODO: is this bang correct? + : null; + const availableStarterMoves = this.speciesStarterMoves.concat( + speciesEggMoves.hasOwnProperty(speciesId) + ? speciesEggMoves[speciesId].filter((_: any, em: number) => starterDataEntry.eggMoves & (1 << em)) + : [], + ); + this.starterMoveset = (moveData || (this.speciesStarterMoves.slice(0, 4) as StarterMoveset)).filter(m => + availableStarterMoves.find(sm => sm === m), + ) as StarterMoveset; + // Consolidate move data if it contains an incompatible move + if (this.starterMoveset.length < 4 && this.starterMoveset.length < availableStarterMoves.length) { + this.starterMoveset.push( + ...availableStarterMoves + .filter(sm => this.starterMoveset?.indexOf(sm) === -1) + .slice(0, 4 - this.starterMoveset.length), + ); + } + + // Remove duplicate moves + this.starterMoveset = this.starterMoveset.filter((move, i) => { + return this.starterMoveset?.indexOf(move) === i; + }) as StarterMoveset; + + if (!this.starterMoveset) { + this.starterMoveset = this.speciesStarterMoves.slice(0, 4) as StarterMoveset; + } + this.starterSummary.updateMoveset(this.starterMoveset, this.speciesStarterMoves.length); + this.starterSummary.updateEggMoves(starterDataEntry.eggMoves); } popStarter(index: number): void { @@ -4223,8 +3259,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { for (let s = 0; s < this.starterSpecies.length; s++) { const species = this.starterSpecies[s]; - const currentDexAttr = this.getCurrentDexProps(species.speciesId); - const props = globalScene.gameData.getSpeciesDexAttrProps(species, currentDexAttr); + const props = this.getSpeciesPropsFromPreferences(species); this.starterIcons[s] .setTexture(species.getIconAtlasKey(props.formIndex, props.shiny, props.variant)) .setFrame(species.getIconId(props.female, props.formIndex, props.shiny, props.variant)); @@ -4245,7 +3280,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { } else { // No more Pokemon selected, go back to filters this.starterIconsCursorObj.setVisible(false); - this.setSpecies(null); + this.setNoSpecies(); this.filterBarCursor = Math.max(1, this.filterBar.numFilters - 1); this.setFilterMode(true); } @@ -4254,7 +3289,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { } else if (this.startCursorObj.visible && this.starterSpecies.length === 0) { // On the start button and no more Pokemon in party this.startCursorObj.setVisible(false); - if (this.filteredStarterContainers.length > 0) { + if (this.filteredStarterIds.length > 0) { // Back to the first Pokemon if there is one this.cursorObj.setVisible(true); this.setCursor(this.scrollCursor * 9); @@ -4303,7 +3338,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { 0, ); const newValue = value + (add || 0); - const valueLimit = this.getValueLimit(); + const valueLimit = getRunValueLimit(); const overLimit = newValue > valueLimit; let newValueStr = newValue.toString(); if (newValueStr.startsWith("0.")) { @@ -4319,10 +3354,10 @@ export class StarterSelectUiHandler extends MessageUiHandler { } let isPartyValid = this.isPartyValid(); if (addingToParty) { - const species = this.filteredStarterContainers[this.cursor].species; + const species = this.starterContainers[this.cursor].species; const isNewPokemonValid = checkStarterValidForChallenge( species, - globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), + this.getSpeciesPropsFromPreferences(species), false, ); isPartyValid ||= isNewPokemonValid; @@ -4332,9 +3367,11 @@ export class StarterSelectUiHandler extends MessageUiHandler { * this loop is used to set the Sprite's alpha value and check if the user can select other pokemon more. */ const remainValue = valueLimit - newValue; - for (let s = 0; s < this.allSpecies.length; s++) { + for (let s = 0; s < this.starterContainers.length; s++) { /** Cost of pokemon species */ - const speciesStarterValue = globalScene.gameData.getSpeciesStarterValue(this.allSpecies[s].speciesId); + const speciesStarterValue = globalScene.gameData.getSpeciesStarterValue( + this.starterContainers[s].species.speciesId, + ); /** {@linkcode Phaser.GameObjects.Sprite} object of Pokémon for setting the alpha value */ const speciesSprite = this.starterContainers[s].icon; @@ -4350,17 +3387,14 @@ export class StarterSelectUiHandler extends MessageUiHandler { * we change to can AddParty value to true since the user has enough cost to choose this pokemon and this pokemon registered too. */ const isValidForChallenge = checkStarterValidForChallenge( - this.allSpecies[s], - globalScene.gameData.getSpeciesDexAttrProps( - this.allSpecies[s], - this.getCurrentDexProps(this.allSpecies[s].speciesId), - ), + this.allStarterSpecies[s], + this.getSpeciesPropsFromPreferences(this.allStarterSpecies[s]), isPartyValid, ); const canBeChosen = remainValue >= speciesStarterValue && isValidForChallenge; - const isPokemonInParty = this.isInParty(this.allSpecies[s])[0]; // this will get the valud of isDupe from isInParty. This will let us see if the pokemon in question is in our party already so we don't grey out the sprites if they're invalid + const isPokemonInParty = this.isInParty(this.allStarterSpecies[s])[0]; // this will get the valud of isDupe from isInParty. This will let us see if the pokemon in question is in our party already so we don't grey out the sprites if they're invalid /* This code does a check to tell whether or not a sprite should be lit up or greyed out. There are 3 ways a pokemon's sprite should be lit up: * 1) If it's in your party, it's a valid pokemon (i.e. for challenge) and you have enough points to have it @@ -4383,42 +3417,6 @@ export class StarterSelectUiHandler extends MessageUiHandler { return true; } - /** - * Attempt to back out of the starter selection screen into the appropriate parent modal - */ - tryExit(): void { - this.blockInput = true; - const ui = this.getUi(); - - const cancel = () => { - ui.setMode(UiMode.STARTER_SELECT); - this.clearText(); - this.blockInput = false; - }; - ui.showText(i18next.t("starterSelectUiHandler:confirmExit"), null, () => { - ui.setModeWithoutClear( - UiMode.CONFIRM, - () => { - ui.setMode(UiMode.STARTER_SELECT); - // Non-challenge modes go directly back to title, while challenge modes go to the selection screen. - if (!globalScene.gameMode.isChallenge) { - globalScene.phaseManager.toTitleScreen(); - } else { - globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.pushNew("SelectChallengePhase"); - globalScene.phaseManager.pushNew("EncounterPhase"); - } - this.clearText(); - globalScene.phaseManager.getCurrentPhase().end(); - }, - cancel, - null, - null, - 19, - ); - }); - } - tryStart(manualTrigger = false): boolean { if (this.starterSpecies.length === 0) { return false; @@ -4494,87 +3492,19 @@ export class StarterSelectUiHandler extends MessageUiHandler { return canStart; } - /** - * Creates a temporary dex attr props that will be used to check whether a pokemon is valid for a challenge - * and to display the correct shiny, variant, and form based on the StarterPreferences - * - * @param speciesId the id of the species to get props for - * @returns the dex props - */ - getCurrentDexProps(speciesId: number): bigint { - let props = 0n; - const { dexEntry } = this.getSpeciesData(speciesId); - const caughtAttr = dexEntry.caughtAttr; - - /* 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.starterPreferences[speciesId]?.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.starterPreferences[speciesId]?.shiny - || ((caughtAttr & DexAttr.SHINY) > 0n && this.starterPreferences[speciesId]?.shiny !== false) - ) { - props += DexAttr.SHINY; - if (this.starterPreferences[speciesId]?.variant !== undefined) { - props += BigInt(Math.pow(2, this.starterPreferences[speciesId]?.variant)) * DexAttr.DEFAULT_VARIANT; - } else if ((caughtAttr & DexAttr.VARIANT_3) > 0) { - /* 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 - */ - 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.starterPreferences[speciesId]?.form) { - // this checks for the form of the pokemon - props += BigInt(Math.pow(2, this.starterPreferences[speciesId]?.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); - this.teraIcon.setVisible(false); + this.starterSummary.showIvs(); this.canCycleTera = false; this.updateInstructions(); } else { this.statsMode = false; - this.statsContainer.setVisible(false); - this.pokemonSprite.setVisible(!!this.speciesStarterDexEntry?.caughtAttr); - //@ts-expect-error - this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. !?!? - this.teraIcon.setVisible(this.allowTera); - const props = globalScene.gameData.getSpeciesDexAttrProps( - this.lastSpecies, - this.getCurrentDexProps(this.lastSpecies.speciesId), - ); + this.starterSummary.hideIvs(!!this.speciesStarterDexEntry?.caughtAttr); + const props = this.getSpeciesPropsFromPreferences(this.lastSpecies); const formIndex = props.formIndex; this.canCycleTera = !this.statsMode @@ -4585,14 +3515,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } - showStats(): void { - if (!this.speciesStarterDexEntry) { - return; - } - - this.statsContainer.setVisible(true); - - this.statsContainer.updateIvs(this.speciesStarterDexEntry.ivs); + getSpeciesPropsFromPreferences(species: PokemonSpecies): DexAttrProps { + return getSpeciesPropsFromPreferences(species, this.starterPreferences[species.speciesId]); } clearText() { @@ -4619,6 +3543,42 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.goFilterLabel.setVisible(false); } + /** + * Attempt to back out of the starter selection screen into the appropriate parent modal + */ + tryExit(): void { + this.blockInput = true; + const ui = this.getUi(); + + const cancel = () => { + ui.setMode(UiMode.STARTER_SELECT); + this.clearText(); + this.blockInput = false; + }; + ui.showText(i18next.t("starterSelectUiHandler:confirmExit"), null, () => { + ui.setModeWithoutClear( + UiMode.CONFIRM, + () => { + ui.setMode(UiMode.STARTER_SELECT); + // Non-challenge modes go directly back to title, while challenge modes go to the selection screen. + if (!globalScene.gameMode.isChallenge) { + globalScene.phaseManager.toTitleScreen(); + } else { + globalScene.phaseManager.clearPhaseQueue(); + globalScene.phaseManager.pushNew("SelectChallengePhase"); + globalScene.phaseManager.pushNew("EncounterPhase"); + } + this.clearText(); + globalScene.phaseManager.getCurrentPhase()?.end(); + }, + cancel, + null, + null, + 19, + ); + }); + } + clear(): void { super.clear(); @@ -4626,9 +3586,10 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.clearStarterPreferences(); this.cursor = -1; + this.oldCursor = -1; this.hideInstructions(); - this.activeTooltip = undefined; - globalScene.ui.hideTooltip(); + + this.starterSummary.clear(); this.starterSelectContainer.setVisible(false); this.blockInput = false; @@ -4669,4 +3630,12 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.starterPreferences = {}; this.originalStarterPreferences = {}; } + + override destroy(): void { + // Without this the reference gets hung up and no startercontainers get GCd + this.starterContainers = []; + /* TODO: Uncomment this once our testing infra supports mocks of `Phaser.GameObject.Group` + this.instructionElemGroup.destroy(true); + */ + } } diff --git a/src/ui/utils/starter-select-ui-utils.ts b/src/ui/utils/starter-select-ui-utils.ts new file mode 100644 index 00000000000..d86e59e9a48 --- /dev/null +++ b/src/ui/utils/starter-select-ui-utils.ts @@ -0,0 +1,351 @@ +import { VALUE_REDUCTION_MAX } from "#app/constants"; +import { globalScene } from "#app/global-scene"; +import { + getPassiveCandyCount, + getSameSpeciesEggCandyCounts, + getStarterValueFriendshipCap, + getValueReductionCandyCounts, + speciesStarterCosts, +} from "#balance/starters"; +import type { PokemonSpecies } from "#data/pokemon-species"; +import { ChallengeType } from "#enums/challenge-type"; +import { DexAttr } from "#enums/dex-attr"; +import { GameModes } from "#enums/game-modes"; +import { Passive } from "#enums/passive"; +import type { PokemonType } from "#enums/pokemon-type"; +import type { SpeciesId } from "#enums/species-id"; +import type { Variant } from "#sprites/variant"; +import type { DexEntry } from "#types/dex-data"; +import type { DexAttrProps, StarterDataEntry, StarterPreferences } from "#types/save-data"; +import { applyChallenges, checkStarterValidForChallenge } from "#utils/challenge-utils"; +import { NumberHolder } from "#utils/common"; +import i18next from "i18next"; + +export interface SpeciesDetails { + shiny?: boolean; + formIndex?: number; + female?: boolean; + variant?: Variant; + abilityIndex?: number; + natureIndex?: number; + teraType?: PokemonType; +} + +/** + * Determines if a passive upgrade is available for the given species ID + * @param speciesId The ID of the species to check the passive of + * @returns true if the user has enough candies and a passive has not been unlocked already + */ +export function isPassiveAvailable(speciesId: number): boolean { + // Get this species ID's starter data + const starterData = globalScene.gameData.starterData[speciesId]; + + return ( + starterData.candyCount >= getPassiveCandyCount(speciesStarterCosts[speciesId]) + && !(starterData.passiveAttr & Passive.UNLOCKED) + ); +} + +/** + * Determines if a value reduction upgrade is available for the given species ID + * @param speciesId The ID of the species to check the value reduction of + * @returns true if the user has enough candies and all value reductions have not been unlocked already + */ +export function isValueReductionAvailable(speciesId: number): boolean { + // Get this species ID's starter data + const starterData = globalScene.gameData.starterData[speciesId]; + + return ( + starterData.candyCount >= getValueReductionCandyCounts(speciesStarterCosts[speciesId])[starterData.valueReduction] + && starterData.valueReduction < VALUE_REDUCTION_MAX + ); +} + +/** + * Determines if an same species egg can be bought for the given species ID + * @param speciesId The ID of the species to check the value reduction of + * @returns true if the user has enough candies + */ +export function isSameSpeciesEggAvailable(speciesId: number): boolean { + // Get this species ID's starter data + const starterData = globalScene.gameData.starterData[speciesId]; + + return starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[speciesId]); +} + +export function isStarterValidForChallenge(species: PokemonSpecies) { + let allFormsValid = false; + if (species.forms?.length > 0) { + for (let i = 0; i < species.forms.length; i++) { + /* Here we are making a fake form index dex props for challenges + * Since some pokemon rely on forms to be valid (i.e. blaze tauros for fire challenges), we make a fake form and dex props to use in the challenge + */ + if (!species.forms[i].isStarterSelectable) { + continue; + } + const tempFormProps = BigInt(Math.pow(2, i)) * DexAttr.DEFAULT_FORM; + const isValidForChallenge = checkStarterValidForChallenge( + species, + globalScene.gameData.getSpeciesDexAttrProps(species, tempFormProps), + true, + ); + allFormsValid ||= isValidForChallenge; + } + } else { + const isValidForChallenge = checkStarterValidForChallenge( + species, + globalScene.gameData.getSpeciesDexAttrProps( + species, + globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true), + ), + true, + ); + allFormsValid = isValidForChallenge; + } + + return allFormsValid; +} + +/** + * Determines if 'Icon' based upgrade notifications should be shown + * @returns true if upgrade notifications are enabled and set to display an 'Icon' + */ +export function 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' + */ +export function isUpgradeAnimationEnabled(): boolean { + return globalScene.candyUpgradeNotification !== 0 && globalScene.candyUpgradeDisplay === 1; +} + +interface StarterSelectLanguageSetting { + starterInfoTextSize: string; + instructionTextSize: string; + starterInfoXPos?: number; + starterInfoYOffset?: number; +} + +const languageSettings: { [key: string]: StarterSelectLanguageSetting } = { + en: { + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, + de: { + starterInfoTextSize: "54px", + instructionTextSize: "35px", + starterInfoXPos: 35, + }, + "es-ES": { + starterInfoTextSize: "50px", + instructionTextSize: "38px", + starterInfoYOffset: 0.5, + starterInfoXPos: 38, + }, + "es-419": { + starterInfoTextSize: "50px", + instructionTextSize: "38px", + starterInfoYOffset: 0.5, + starterInfoXPos: 38, + }, + fr: { + starterInfoTextSize: "54px", + instructionTextSize: "38px", + }, + it: { + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, + "pt-BR": { + starterInfoTextSize: "48px", + instructionTextSize: "42px", + starterInfoYOffset: 0.5, + starterInfoXPos: 33, + }, + zh: { + starterInfoTextSize: "56px", + instructionTextSize: "36px", + starterInfoXPos: 26, + }, + ko: { + starterInfoTextSize: "60px", + instructionTextSize: "38px", + starterInfoYOffset: -0.5, + starterInfoXPos: 30, + }, + ja: { + starterInfoTextSize: "48px", + instructionTextSize: "40px", + starterInfoYOffset: 1, + starterInfoXPos: 32, + }, + ca: { + starterInfoTextSize: "48px", + instructionTextSize: "38px", + starterInfoYOffset: 0.5, + starterInfoXPos: 29, + }, + da: { + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, + tr: { + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, + ro: { + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, + ru: { + starterInfoTextSize: "46px", + instructionTextSize: "38px", + starterInfoYOffset: 0.5, + starterInfoXPos: 26, + }, + tl: { + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, +}; + +export function getStarterSelectTextSettings(): StarterSelectLanguageSetting { + const currentLanguage = i18next.resolvedLanguage ?? "en"; + const langSettingKey = Object.keys(languageSettings).find(lang => currentLanguage.includes(lang)) ?? "en"; + const textSettings = languageSettings[langSettingKey]; + return textSettings; +} + +export function getSpeciesData( + speciesId: SpeciesId, + applyChallenge = true, +): { dexEntry: DexEntry; starterDataEntry: StarterDataEntry } { + const dexEntry = globalScene.gameData.dexData[speciesId]; + const starterDataEntry = globalScene.gameData.starterData[speciesId]; + + // Unpacking to make a copy by values, not references + const copiedDexEntry = { ...dexEntry }; + copiedDexEntry.ivs = [...dexEntry.ivs]; + const copiedStarterDataEntry = { ...starterDataEntry }; + if (applyChallenge) { + applyChallenges(ChallengeType.STARTER_SELECT_MODIFY, speciesId, copiedDexEntry, copiedStarterDataEntry); + } + return { dexEntry: { ...copiedDexEntry }, starterDataEntry: { ...copiedStarterDataEntry } }; +} + +export function getFriendship(speciesId: number) { + let currentFriendship = globalScene.gameData.starterData[speciesId].friendship; + if (!currentFriendship || currentFriendship === undefined) { + currentFriendship = 0; + } + + const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[speciesId]); + + return { currentFriendship, friendshipCap }; +} + +/** + * Creates a temporary dex attr props that will be used to check whether a pokemon is valid for a challenge + * and to display the correct shiny, variant, and form based on the AllStarterPreferences + * + * @param speciesId the id of the species to get props for + * @returns the dex props + */ +export function getDexAttrFromPreferences(speciesId: number, starterPreferences: StarterPreferences = {}): bigint { + let props = 0n; + const { dexEntry } = getSpeciesData(speciesId); + const caughtAttr = dexEntry.caughtAttr; + + /* 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 ( + starterPreferences[speciesId]?.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 ( + starterPreferences[speciesId]?.shiny + || ((caughtAttr & DexAttr.SHINY) > 0n && starterPreferences[speciesId]?.shiny !== false) + ) { + props += DexAttr.SHINY; + if (starterPreferences[speciesId]?.variant !== undefined) { + props += BigInt(Math.pow(2, starterPreferences[speciesId]?.variant)) * DexAttr.DEFAULT_VARIANT; + } else 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 (starterPreferences[speciesId]?.form) { + // this checks for the form of the pokemon + props += BigInt(Math.pow(2, starterPreferences[speciesId]?.form)) * DexAttr.DEFAULT_FORM; + } else { + // Get the first unlocked form + props += globalScene.gameData.getFormAttr(globalScene.gameData.getFormIndex(caughtAttr)); + } + + return props; +} + +export function getSpeciesPropsFromPreferences( + species: PokemonSpecies, + starterPreferences: StarterPreferences = {}, +): DexAttrProps { + const defaults = globalScene.gameData.getSpeciesDefaultDexAttrProps(species); + return { + shiny: starterPreferences.shiny != null ? starterPreferences.shiny : defaults.shiny, + variant: starterPreferences.variant != null ? (starterPreferences.variant as Variant) : defaults.variant, + female: starterPreferences.female ?? defaults.female, + formIndex: starterPreferences.formIndex ?? defaults.formIndex, + }; +} + +// TODO: Do we actually need props? +export function getSpeciesDetailsFromPreferences(species: PokemonSpecies, starterPreferences: StarterPreferences = {}) { + const props = getSpeciesPropsFromPreferences(species, starterPreferences); + const abilityIndex = + starterPreferences.abilityIndex ?? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); + const nature = starterPreferences.nature ?? globalScene.gameData.getSpeciesDefaultNature(species); + const teraType = starterPreferences.tera ?? species.type1; + return { + shiny: props.shiny, + formIndex: props.formIndex, + female: props.female, + variant: props.variant, + abilityIndex, + natureIndex: nature, + teraType, + }; +} + +export function getRunValueLimit(): number { + const valueLimit = new NumberHolder(0); + switch (globalScene.gameMode.modeId) { + case GameModes.ENDLESS: + case GameModes.SPLICED_ENDLESS: + valueLimit.value = 15; + break; + default: + valueLimit.value = 10; + } + + applyChallenges(ChallengeType.STARTER_POINTS, valueLimit); + + return valueLimit.value; +} diff --git a/src/utils/data.ts b/src/utils/data.ts index 1383d8e6ff2..296b97bd5ef 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -1,6 +1,6 @@ import { loggedInUser } from "#app/account"; import { saveKey } from "#app/constants"; -import type { StarterAttributes } from "#types/save-data"; +import type { StarterPreferences } from "#types/save-data"; import { AES, enc } from "crypto-js"; /** @@ -78,19 +78,19 @@ export function isBareObject(obj: any): boolean { const StarterPrefers_DEFAULT: string = "{}"; let StarterPrefers_private_latest: string = StarterPrefers_DEFAULT; -export interface StarterPreferences { - [key: number]: StarterAttributes | undefined; +export interface AllStarterPreferences { + [key: number]: StarterPreferences | undefined; } -// called on starter selection show once -export function loadStarterPreferences(): StarterPreferences { +// called on starter selection show once +export function loadStarterPreferences(): AllStarterPreferences { return JSON.parse( (StarterPrefers_private_latest = localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT), ); } -export function saveStarterPreferences(prefs: StarterPreferences): void { +export function saveStarterPreferences(prefs: AllStarterPreferences): void { // Fastest way to check if an object has any properties (does no allocation) if (isBareObject(prefs)) { console.warn("Refusing to save empty starter preferences"); diff --git a/test/ui/pokedex.test.ts b/test/ui/pokedex.test.ts index 6b84b253260..cab8264329b 100644 --- a/test/ui/pokedex.test.ts +++ b/test/ui/pokedex.test.ts @@ -7,7 +7,7 @@ import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { GameManager } from "#test/test-utils/game-manager"; -import type { StarterAttributes } from "#types/save-data"; +import type { StarterPreferences } from "#types/save-data"; import { FilterTextRow } from "#ui/filter-text"; import { PokedexPageUiHandler } from "#ui/pokedex-page-ui-handler"; import { PokedexUiHandler } from "#ui/pokedex-ui-handler"; @@ -84,12 +84,12 @@ describe("UI - Pokedex", () => { */ async function runToPokedexPage( species: PokemonSpecies, - starterAttributes: StarterAttributes = {}, + starterPreferences: StarterPreferences = {}, ): Promise { // Open the pokedex UI. await game.runToTitle(); - await game.scene.ui.setOverlayMode(UiMode.POKEDEX_PAGE, species, starterAttributes); + await game.scene.ui.setOverlayMode(UiMode.POKEDEX_PAGE, species, starterPreferences); // Get the handler for the current UI. const handler = game.scene.ui.getHandler(); @@ -514,7 +514,7 @@ describe("UI - Pokedex", () => { it("should show caught battle form as caught", async () => { await game.importData("./test/test-utils/saves/data_pokedex_tests_v2.prsv"); - const pageHandler = await runToPokedexPage(getPokemonSpecies(SpeciesId.VENUSAUR), { form: 1 }); + const pageHandler = await runToPokedexPage(getPokemonSpecies(SpeciesId.VENUSAUR), { formIndex: 1 }); // @ts-expect-error - `species` is private expect(pageHandler.species.speciesId).toEqual(SpeciesId.VENUSAUR); @@ -529,7 +529,7 @@ describe("UI - Pokedex", () => { //TODO: check tint of the sprite it("should show uncaught battle form as seen", async () => { await game.importData("./test/test-utils/saves/data_pokedex_tests_v2.prsv"); - const pageHandler = await runToPokedexPage(getPokemonSpecies(SpeciesId.VENUSAUR), { form: 2 }); + const pageHandler = await runToPokedexPage(getPokemonSpecies(SpeciesId.VENUSAUR), { formIndex: 2 }); // @ts-expect-error - `species` is private expect(pageHandler.species.speciesId).toEqual(SpeciesId.VENUSAUR);