diff --git a/public/images/ui/legacy/mystery_egg.png b/public/images/ui/legacy/mystery_egg.png new file mode 100644 index 00000000000..bb117a137b0 Binary files /dev/null and b/public/images/ui/legacy/mystery_egg.png differ diff --git a/public/images/ui/legacy/normal_memory.png b/public/images/ui/legacy/normal_memory.png new file mode 100644 index 00000000000..ddc22d1d4ab Binary files /dev/null and b/public/images/ui/legacy/normal_memory.png differ diff --git a/public/images/ui/legacy/pokedex_summary_bg.png b/public/images/ui/legacy/pokedex_summary_bg.png new file mode 100644 index 00000000000..690df1547c0 Binary files /dev/null and b/public/images/ui/legacy/pokedex_summary_bg.png differ diff --git a/public/images/ui/mystery_egg.png b/public/images/ui/mystery_egg.png new file mode 100644 index 00000000000..bb117a137b0 Binary files /dev/null and b/public/images/ui/mystery_egg.png differ diff --git a/public/images/ui/normal_memory.png b/public/images/ui/normal_memory.png new file mode 100644 index 00000000000..ddc22d1d4ab Binary files /dev/null and b/public/images/ui/normal_memory.png differ diff --git a/public/images/ui/pokedex_summary_bg.png b/public/images/ui/pokedex_summary_bg.png new file mode 100644 index 00000000000..92e70bbee27 Binary files /dev/null and b/public/images/ui/pokedex_summary_bg.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 42bc76ff535..c986eb4eee7 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -161,6 +161,7 @@ export default class BattleScene extends SceneBase { public reroll: boolean = false; public shopCursorTarget: number = ShopCursorTarget.REWARDS; public commandCursorMemory: boolean = false; + public dexForDevs: boolean = false; public showMovesetFlyout: boolean = true; public showArenaFlyout: boolean = true; public showTimeOfDayWidget: boolean = true; diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index 240881ad580..f975f1d995d 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -102,6 +102,18 @@ export interface BiomePokemonPools { [key: integer]: BiomeTierPokemonPools } +export interface BiomeTierTod { + biome: Biome, + tier: BiomePoolTier, + tod: TimeOfDay[] +} + +export interface CatchableSpecies{ + [key: integer]: BiomeTierTod[] +} + +export const catchableSpecies: CatchableSpecies = {}; + export interface BiomeTierTrainerPools { [key: integer]: TrainerType[] } @@ -7716,6 +7728,10 @@ export function initBiomes() { uncatchableSpecies.push(speciesId); } + // prepares new array in catchableSpecies to host available biomes + //TODO: this must be improved to only make arrays for starters + catchableSpecies[speciesId] = []; + for (const b of biomeEntries) { const biome = b[0]; const tier = b[1]; @@ -7725,6 +7741,12 @@ export function initBiomes() { : [ b[2] ] : [ TimeOfDay.ALL ]; + catchableSpecies[speciesId].push({ + biome: biome as Biome, + tier: tier as BiomePoolTier, + tod: timesOfDay as TimeOfDay[] + }); + for (const tod of timesOfDay) { if (!biomePokemonPools.hasOwnProperty(biome) || !biomePokemonPools[biome].hasOwnProperty(tier) || !biomePokemonPools[biome][tier].hasOwnProperty(tod)) { continue; diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index a8fe3b5f4ab..b6a7aedda9d 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -92,6 +92,7 @@ export class SpeciesFormEvolution { public item: EvolutionItem | null; public condition: SpeciesEvolutionCondition | null; public wildDelay: SpeciesWildEvolutionDelay; + public description: string = ""; constructor(speciesId: Species, preFormKey: string | null, evoFormKey: string | null, level: integer, item: EvolutionItem | null, condition: SpeciesEvolutionCondition | null, wildDelay?: SpeciesWildEvolutionDelay) { this.speciesId = speciesId; @@ -101,6 +102,23 @@ export class SpeciesFormEvolution { this.item = item || EvolutionItem.NONE; this.condition = condition; this.wildDelay = wildDelay ?? SpeciesWildEvolutionDelay.NONE; + + const strings: string[] = []; + if (this.level > 1) { + strings.push(i18next.t("pokemonEvolutions:level") + ` ${this.level}`); + } + if (this.item) { + const itemDescription = i18next.t(`modifierType:EvolutionItem.${EvolutionItem[this.item].toUpperCase()}`); + const rarity = this.item > 50 ? i18next.t("pokemonEvolutions:ULTRA") : i18next.t("pokemonEvolutions:GREAT"); + strings.push(i18next.t("pokemonEvolutions:using") + itemDescription + ` (${rarity})`); + } + if (this.condition) { + strings.push(this.condition.description); + } + this.description = strings + .filter(str => str !== "") + .map((str, index) => index > 0 ? str[0].toLowerCase() + str.slice(1) : str) + .join(i18next.t("pokemonEvolutions:connector")); } } @@ -237,6 +255,7 @@ class WeatherEvolutionCondition extends SpeciesEvolutionCondition { constructor(weatherTypes: WeatherType[]) { super(() => weatherTypes.indexOf(globalScene.arena.weather?.weatherType || WeatherType.NONE) > -1); this.weatherTypes = weatherTypes; + this.description = i18next.t("pokemonEvolutions:weather"); } } @@ -1377,7 +1396,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [Species.TANDEMAUS]: [ new SpeciesFormEvolution(Species.MAUSHOLD, "", "three", 25, null, new TandemausEvolutionCondition()), - new SpeciesEvolution(Species.MAUSHOLD, 25, null, null) + new SpeciesFormEvolution(Species.MAUSHOLD, "", "four", 25, null, null) ], [Species.FIDOUGH]: [ new SpeciesEvolution(Species.DACHSBUN, 26, null, null) @@ -1540,7 +1559,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [Species.DUNSPARCE]: [ new SpeciesFormEvolution(Species.DUDUNSPARCE, "", "three-segment", 32, null, new DunsparceEvolutionCondition(), SpeciesWildEvolutionDelay.LONG), - new SpeciesEvolution(Species.DUDUNSPARCE, 32, null, new MoveEvolutionCondition(Moves.HYPER_DRILL), SpeciesWildEvolutionDelay.LONG) + new SpeciesFormEvolution(Species.DUDUNSPARCE, "", "two-segment", 32, null, new MoveEvolutionCondition(Moves.HYPER_DRILL), SpeciesWildEvolutionDelay.LONG) ], [Species.GLIGAR]: [ new SpeciesEvolution(Species.GLISCOR, 1, EvolutionItem.RAZOR_FANG, new TimeOfDayEvolutionCondition("night") /* Razor fang at night*/, SpeciesWildEvolutionDelay.VERY_LONG) diff --git a/src/data/balance/tms.ts b/src/data/balance/tms.ts index da900768987..12e402af4ed 100644 --- a/src/data/balance/tms.ts +++ b/src/data/balance/tms.ts @@ -68433,6 +68433,31 @@ export const tmSpecies: TmSpecies = { ], }; +interface SpeciesTmMoves { + [key: integer]: Moves[] +} + +function flipTmSpecies(tmSpecies: TmSpecies): SpeciesTmMoves { + const flipped: SpeciesTmMoves = {}; + + for (const move in tmSpecies) { + const moveKey = Number(move); + const speciesList = tmSpecies[move]; + + for (const species of speciesList) { + const speciesKey = Number(species); + if (!flipped[speciesKey]) { + flipped[speciesKey] = []; + } + flipped[speciesKey].push(moveKey); + } + } + return flipped; +} + +export const speciesTmMoves: SpeciesTmMoves = flipTmSpecies(tmSpecies); + + interface TmPoolTiers { [key: integer]: ModifierTier } diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 035cd6f1369..ede5907876d 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -328,7 +328,8 @@ export class SpeciesFormChangeMoveLearnedTrigger extends SpeciesFormChangeTrigge this.move = move; this.known = known; const moveKey = Moves[this.move].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join("") as unknown as string; - this.description = i18next.t("pokemonEvolutions:Forms.moveLearned", { move: i18next.t(`move:${moveKey}.name`) }); + this.description = known ? i18next.t("pokemonEvolutions:Forms.moveLearned", { move: i18next.t(`move:${moveKey}.name`) }) : + i18next.t("pokemonEvolutions:Forms.moveForgotten", { move: i18next.t(`move:${moveKey}.name`) }); } canChange(pokemon: Pokemon): boolean { diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 574c2a67f65..07cfdc6daf8 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -298,8 +298,8 @@ export abstract class PokemonSpeciesForm { return ret; } - getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string { - const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/"); + getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number, back?: boolean): string { + const spriteId = this.getSpriteId(female, formIndex, shiny, variant, back).replace(/\_{2}/g, "/"); return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`; } @@ -320,8 +320,8 @@ export abstract class PokemonSpeciesForm { return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`; } - getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string { - return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`; + getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number, back?: boolean): string { + return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant, back)}`; } abstract getFormSpriteKey(formIndex?: number): string; @@ -494,10 +494,10 @@ export abstract class PokemonSpeciesForm { return true; } - loadAssets(female: boolean, formIndex?: number, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise { + loadAssets(female: boolean, formIndex?: number, shiny?: boolean, variant?: Variant, startLoad?: boolean, back?: boolean): Promise { return new Promise(resolve => { - const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant); - globalScene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant)); + const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant, back); + globalScene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant, back)); globalScene.load.audio(`${this.getCryKey(formIndex)}`, `audio/${this.getCryKey(formIndex)}.m4a`); globalScene.load.once(Phaser.Loader.Events.COMPLETE, () => { const originalWarn = console.warn; @@ -507,7 +507,7 @@ export abstract class PokemonSpeciesForm { console.warn = originalWarn; if (!(globalScene.anims.exists(spriteKey))) { globalScene.anims.create({ - key: this.getSpriteKey(female, formIndex, shiny, variant), + key: this.getSpriteKey(female, formIndex, shiny, variant, back), frames: frameNames, frameRate: 10, repeat: -1 @@ -515,7 +515,7 @@ export abstract class PokemonSpeciesForm { } else { globalScene.anims.get(spriteKey).frameRate = 10; } - const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, ""); + const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant, back).replace("variant/", "").replace(/_[1-3]$/, ""); globalScene.loadPokemonVariantAssets(spriteKey, spritePath, variant).then(() => resolve()); }); if (startLoad) { @@ -2637,18 +2637,10 @@ export function initSpecies() { new PokemonSpecies(Species.ROARING_MOON, 9, false, false, false, "Paradox Pokémon", Type.DRAGON, Type.DARK, 2, 380, Abilities.PROTOSYNTHESIS, Abilities.NONE, Abilities.NONE, 590, 105, 139, 71, 55, 101, 119, 10, 0, 295, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.IRON_VALIANT, 9, false, false, false, "Paradox Pokémon", Type.FAIRY, Type.FIGHTING, 1.4, 35, Abilities.QUARK_DRIVE, Abilities.NONE, Abilities.NONE, 590, 74, 130, 90, 120, 60, 116, 10, 0, 295, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.KORAIDON, 9, false, true, false, "Paradox Pokémon", Type.FIGHTING, Type.DRAGON, 2.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, GrowthRate.SLOW, null, false, false, - new PokemonForm("Apex Build", "apex-build", Type.FIGHTING, Type.DRAGON, 2.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true), - new PokemonForm("Limited Build", "limited-build", Type.FIGHTING, Type.DRAGON, 3.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true), - new PokemonForm("Sprinting Build", "sprinting-build", Type.FIGHTING, Type.DRAGON, 3.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true), - new PokemonForm("Swimming Build", "swimming-build", Type.FIGHTING, Type.DRAGON, 3.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true), - new PokemonForm("Gliding Build", "gliding-build", Type.FIGHTING, Type.DRAGON, 3.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true), + new PokemonForm("Apex Build", "apex-build", Type.FIGHTING, Type.DRAGON, 2.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true) ), new PokemonSpecies(Species.MIRAIDON, 9, false, true, false, "Paradox Pokémon", Type.ELECTRIC, Type.DRAGON, 3.5, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, GrowthRate.SLOW, null, false, false, - new PokemonForm("Ultimate Mode", "ultimate-mode", Type.ELECTRIC, Type.DRAGON, 3.5, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true), - new PokemonForm("Low-Power Mode", "low-power-mode", Type.ELECTRIC, Type.DRAGON, 2.8, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true), - new PokemonForm("Drive Mode", "drive-mode", Type.ELECTRIC, Type.DRAGON, 2.8, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true), - new PokemonForm("Aquatic Mode", "aquatic-mode", Type.ELECTRIC, Type.DRAGON, 2.8, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true), - new PokemonForm("Glide Mode", "glide-mode", Type.ELECTRIC, Type.DRAGON, 2.8, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true), + new PokemonForm("Ultimate Mode", "ultimate-mode", Type.ELECTRIC, Type.DRAGON, 3.5, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true) ), new PokemonSpecies(Species.WALKING_WAKE, 9, false, false, false, "Paradox Pokémon", Type.WATER, Type.DRAGON, 3.5, 280, Abilities.PROTOSYNTHESIS, Abilities.NONE, Abilities.NONE, 590, 99, 83, 91, 125, 83, 109, 10, 0, 295, GrowthRate.SLOW, null, false), //Custom Catchrate, matching Gouging Fire and Raging Bolt new PokemonSpecies(Species.IRON_LEAVES, 9, false, false, false, "Paradox Pokémon", Type.GRASS, Type.PSYCHIC, 1.5, 125, Abilities.QUARK_DRIVE, Abilities.NONE, Abilities.NONE, 590, 90, 130, 88, 70, 108, 104, 10, 0, 295, GrowthRate.SLOW, null, false), //Custom Catchrate, matching Iron Boulder and Iron Crown diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 023f907a30d..60a0513f608 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -5,7 +5,7 @@ import { SceneBase } from "#app/scene-base"; import { WindowVariant, getWindowVariantSuffix } from "#app/ui/ui-theme"; import { isMobile } from "#app/touch-controls"; import * as Utils from "#app/utils"; -import { initPokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; +import { initPokemonPrevolutions, initPokemonStarters } from "#app/data/balance/pokemon-evolutions"; import { initBiomes } from "#app/data/balance/biomes"; import { initEggMoves } from "#app/data/balance/egg-moves"; import { initPokemonForms } from "#app/data/pokemon-forms"; @@ -103,6 +103,8 @@ export class LoadingScene extends SceneBase { this.loadImage("icon_tera", "ui"); this.loadImage("type_tera", "ui"); this.loadAtlas("type_bgs", "ui"); + this.loadImage("mystery_egg", "ui"); + this.loadImage("normal_memory", "ui"); this.loadImage("dawn_icon_fg", "ui"); this.loadImage("dawn_icon_mg", "ui"); @@ -154,6 +156,7 @@ export class LoadingScene extends SceneBase { this.loadImage("scroll_bar_handle", "ui"); this.loadImage("starter_container_bg", "ui"); this.loadImage("starter_select_bg", "ui"); + this.loadImage("pokedex_summary_bg", "ui"); this.loadImage("select_cursor", "ui"); this.loadImage("select_cursor_highlight", "ui"); this.loadImage("select_cursor_highlight_thick", "ui"); @@ -354,6 +357,7 @@ export class LoadingScene extends SceneBase { initVouchers(); initStatsKeys(); initPokemonPrevolutions(); + initPokemonStarters(); initBiomes(); initEggMoves(); initPokemonForms(); diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index cc798bc8585..904b51c6dc7 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -193,6 +193,7 @@ export async function initI18n(): Promise { "egg", "fightUiHandler", "filterBar", + "filterText", "gameMode", "gameStatsUiHandler", "growth", @@ -203,6 +204,7 @@ export async function initI18n(): Promise { "move", "nature", "pokeball", + "pokedexUiHandler", "pokemon", "pokemonEvolutions", "pokemonForm", diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index ed8e49ffe37..35bd97c151d 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -9,6 +9,7 @@ import { EaseType } from "#enums/ease-type"; import { MoneyFormat } from "#enums/money-format"; import { PlayerGender } from "#enums/player-gender"; import { ShopCursorTarget } from "#enums/shop-cursor-target"; +import * as Utils from "../../utils"; const VOLUME_OPTIONS: SettingOption[] = new Array(11).fill(null).map((_, i) => i ? { value: (i * 10).toString(), @@ -150,6 +151,7 @@ export const SettingKeys = { Show_Stats_on_Level_Up: "SHOW_LEVEL_UP_STATS", Shop_Cursor_Target: "SHOP_CURSOR_TARGET", Command_Cursor_Memory: "COMMAND_CURSOR_MEMORY", + Dex_For_Devs: "DEX_FOR_DEVS", Candy_Upgrade_Notification: "CANDY_UPGRADE_NOTIFICATION", Candy_Upgrade_Display: "CANDY_UPGRADE_DISPLAY", Move_Info: "MOVE_INFO", @@ -691,6 +693,16 @@ export const Setting: Array = [ } ]; +if (Utils.isLocal) { + Setting.push({ + key: SettingKeys.Dex_For_Devs, + label: i18next.t("settings:dexForDevs"), + options: OFF_ON, + default: 0, + type: SettingType.GENERAL + }); +} + /** * Return the index of a Setting * @param key SettingKey @@ -828,6 +840,9 @@ export function setSetting(setting: string, value: integer): boolean { case SettingKeys.Command_Cursor_Memory: globalScene.commandCursorMemory = Setting[index].options[value].value === "On"; break; + case SettingKeys.Dex_For_Devs: + globalScene.dexForDevs = Setting[index].options[value].value === "On"; + break; case SettingKeys.EXP_Gains_Speed: globalScene.expGainsSpeed = value; break; diff --git a/src/test/evolution.test.ts b/src/test/evolution.test.ts index 10748899d59..d198049801c 100644 --- a/src/test/evolution.test.ts +++ b/src/test/evolution.test.ts @@ -174,7 +174,7 @@ describe("Evolution", () => { for (let f = 1; f < 4; f++) { vi.spyOn(Utils, "randSeedInt").mockReturnValue(f); // setting the random generator to 1, 2 and 3 to force 4 family mausholds const fourForm = playerPokemon.getEvolution()!; - expect(fourForm.evoFormKey).toBe(null); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is null + expect(fourForm.evoFormKey).toBe("four"); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is "four" } }); }); diff --git a/src/tutorial.ts b/src/tutorial.ts index b5f688c11fb..6890075a642 100644 --- a/src/tutorial.ts +++ b/src/tutorial.ts @@ -10,6 +10,7 @@ export enum Tutorial { Access_Menu = "ACCESS_MENU", Menu = "MENU", Starter_Select = "STARTER_SELECT", + Pokedex = "POKEDEX", Pokerus = "POKERUS", Stat_Change = "STAT_CHANGE", Select_Item = "SELECT_ITEM", diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index 25ad9d87701..e6a0ed7a69c 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -12,6 +12,8 @@ import { globalScene } from "#app/global-scene"; import SettingsDisplayUiHandler from "./ui/settings/settings-display-ui-handler"; import SettingsAudioUiHandler from "./ui/settings/settings-audio-ui-handler"; import RunInfoUiHandler from "./ui/run-info-ui-handler"; +import PokedexUiHandler from "./ui/pokedex-ui-handler"; +import PokedexPageUiHandler from "./ui/pokedex-page-ui-handler"; type ActionKeys = Record void>; @@ -140,7 +142,7 @@ export class UiInputs { } buttonGoToFilter(button: Button): void { - const whitelist = [ StarterSelectUiHandler ]; + const whitelist = [ StarterSelectUiHandler, PokedexUiHandler, PokedexPageUiHandler ]; const uiHandler = globalScene.ui?.getHandler(); if (whitelist.some(handler => uiHandler instanceof handler)) { globalScene.ui.processInput(button); @@ -178,6 +180,7 @@ export class UiInputs { globalScene.ui.setOverlayMode(Mode.MENU); break; case Mode.STARTER_SELECT: + case Mode.POKEDEX_PAGE: this.buttonTouch(); break; case Mode.MENU: @@ -190,7 +193,7 @@ export class UiInputs { } buttonCycleOption(button: Button): void { - const whitelist = [ StarterSelectUiHandler, SettingsUiHandler, RunInfoUiHandler, SettingsDisplayUiHandler, SettingsAudioUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler ]; + const whitelist = [ StarterSelectUiHandler, PokedexUiHandler, PokedexPageUiHandler, SettingsUiHandler, RunInfoUiHandler, SettingsDisplayUiHandler, SettingsAudioUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler ]; const uiHandler = globalScene.ui?.getHandler(); if (whitelist.some(handler => uiHandler instanceof handler)) { globalScene.ui.processInput(button); diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index df592fc45b1..4a2792e525a 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -1,11 +1,12 @@ import { globalScene } from "#app/global-scene"; -import { TextStyle, addTextObject, getTextStyleOptions } from "./text"; +import { TextStyle, addBBCodeTextObject, getTextColor, getTextStyleOptions } from "./text"; import { Mode } from "./ui"; import UiHandler from "./ui-handler"; import { addWindow } from "./ui-theme"; import * as Utils from "../utils"; import { argbFromRgba } from "@material/material-color-utilities"; import { Button } from "#enums/buttons"; +import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; export interface OptionSelectConfig { xOffset?: number; @@ -21,8 +22,10 @@ export interface OptionSelectItem { label: string; handler: () => boolean; onHover?: () => void; + skip?: boolean; keepOpen?: boolean; overrideSound?: boolean; + style?: TextStyle; item?: string; itemArgs?: any[]; } @@ -33,7 +36,7 @@ const scrollDownLabel = "↓"; export default abstract class AbstractOptionSelectUiHandler extends UiHandler { protected optionSelectContainer: Phaser.GameObjects.Container; protected optionSelectBg: Phaser.GameObjects.NineSlice; - protected optionSelectText: Phaser.GameObjects.Text; + protected optionSelectText: BBCodeText; protected optionSelectIcons: Phaser.GameObjects.Sprite[]; protected config: OptionSelectConfig | null; @@ -41,11 +44,17 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { protected blockInput: boolean; protected scrollCursor: integer = 0; + protected fullCursor: integer = 0; protected scale: number = 0.1666666667; private cursorObj: Phaser.GameObjects.Image | null; + protected unskippedIndices: number[] = []; + + protected defaultTextStyle: TextStyle = TextStyle.WINDOW; + + constructor(mode: Mode | null) { super(mode); } @@ -79,44 +88,54 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { protected setupOptions() { const configOptions = this.config?.options ?? []; - let options: OptionSelectItem[]; + const options: OptionSelectItem[] = configOptions; - // for performance reasons, this limits how many options we can see at once. Without this, it would try to make text options for every single options - // which makes the performance take a hit. If there's not enough options to do this (set to 10 at the moment) and the ui mode !== Mode.AUTO_COMPLETE, - // this is ignored and the original code is untouched, with the options array being all the options from the config - if (configOptions.length >= 10 && globalScene.ui.getMode() === Mode.AUTO_COMPLETE) { - const optionsScrollTotal = configOptions.length; - const optionStartIndex = this.scrollCursor; - const optionEndIndex = Math.min(optionsScrollTotal, optionStartIndex + (!optionStartIndex || this.scrollCursor + (this.config?.maxOptions! - 1) >= optionsScrollTotal ? this.config?.maxOptions! - 1 : this.config?.maxOptions! - 2)); - options = configOptions.slice(optionStartIndex, optionEndIndex + 2); - } else { - options = configOptions; - } + this.unskippedIndices = this.getUnskippedIndices(configOptions); if (this.optionSelectText) { - this.optionSelectText.destroy(); + if (this.optionSelectText instanceof BBCodeText) { + try { + this.optionSelectText.destroy(); + } catch (error) { + console.error("Error while destroying optionSelectText:", error); + } + } else { + console.warn("optionSelectText is not an instance of BBCodeText."); + } } + if (this.optionSelectIcons?.length) { this.optionSelectIcons.map(i => i.destroy()); this.optionSelectIcons.splice(0, this.optionSelectIcons.length); } - this.optionSelectText = addTextObject(0, 0, options.map(o => o.item ? ` ${o.label}` : o.label).join("\n"), TextStyle.WINDOW, { maxLines: options.length }); - this.optionSelectText.setLineSpacing(this.scale * 72); + const optionsWithScroll = (this.config?.options && this.config?.options.length > (this.config?.maxOptions!)) ? this.getOptionsWithScroll() : options; + + // Setting the initial text to establish the width of the select object. We consider all options, even ones that are not displayed, + // Except in the case of autocomplete, where we don't want to set up a text element with potentially hundreds of lines. + const optionsForWidth = globalScene.ui.getMode() === Mode.AUTO_COMPLETE ? optionsWithScroll : options; + this.optionSelectText = addBBCodeTextObject( + 0, 0, optionsForWidth.map(o => o.item + ? `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}] ${o.label}[/color][/shadow]` + : `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}]${o.label}[/color][/shadow]` + ).join("\n"), + TextStyle.WINDOW, { maxLines: options.length, lineSpacing: 12 } + ); + this.optionSelectText.setOrigin(0, 0); this.optionSelectText.setName("text-option-select"); - this.optionSelectText.setLineSpacing(12); this.optionSelectContainer.add(this.optionSelectText); this.optionSelectContainer.setPosition((globalScene.game.canvas.width / 6) - 1 - (this.config?.xOffset || 0), -48 + (this.config?.yOffset || 0)); - this.optionSelectBg.width = Math.max(this.optionSelectText.displayWidth + 24, this.getWindowWidth()); - - if (this.config?.options && this.config?.options.length > (this.config?.maxOptions!)) { // TODO: is this bang correct? - this.optionSelectText.setText(this.getOptionsWithScroll().map(o => o.label).join("\n")); - } - this.optionSelectBg.height = this.getWindowHeight(); + this.optionSelectText.setPosition(this.optionSelectBg.x - this.optionSelectBg.width + 12 + 24 * this.scale, this.optionSelectBg.y - this.optionSelectBg.height + 2 + 42 * this.scale); - this.optionSelectText.setPositionRelative(this.optionSelectBg, 12 + 24 * this.scale, 2 + 42 * this.scale); + // Now that the container and background widths are established, we can set up the proper text restricted to visible options + this.optionSelectText.setText(optionsWithScroll.map(o => o.item + ? `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}] ${o.label}[/color][/shadow]` + : `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}]${o.label}[/color][/shadow]` + ).join("\n") + + ); options.forEach((option: OptionSelectItem, i: integer) => { if (option.item) { @@ -160,6 +179,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { this.optionSelectContainer.setVisible(true); this.scrollCursor = 0; + this.fullCursor = 0; this.setCursor(0); if (this.config.delay) { @@ -169,6 +189,11 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { globalScene.time.delayedCall(Utils.fixedInt(this.config.delay), () => this.unblockInput()); } + if (this.config?.supportHover) { + // handle hover code if the element supports hover-handlers and the option has the optional hover-handler set. + this.config?.options[this.unskippedIndices[this.fullCursor]]?.onHover?.(); + } + return true; } @@ -177,8 +202,6 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { let success = false; - const options = this.getOptionsWithScroll(); - let playSound = true; if (button === Button.ACTION || button === Button.CANCEL) { @@ -190,15 +213,14 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { success = true; if (button === Button.CANCEL) { if (this.config?.maxOptions && this.config.options.length > this.config.maxOptions) { - this.scrollCursor = (this.config.options.length - this.config.maxOptions) + 1; - this.cursor = options.length - 1; + this.setCursor(this.unskippedIndices.length - 1); } else if (!this.config?.noCancel) { - this.setCursor(options.length - 1); + this.setCursor(this.unskippedIndices.length - 1); } else { return false; } } - const option = this.config?.options[this.cursor + (this.scrollCursor - (this.scrollCursor ? 1 : 0))]; + const option = this.config?.options[this.unskippedIndices[this.fullCursor]]; if (option?.handler()) { if (!option.keepOpen) { this.clear(); @@ -211,7 +233,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { // this is here to differentiate between a Button.SUBMIT vs Button.ACTION within the autocomplete handler // this is here because Button.ACTION is picked up as z on the keyboard, meaning if you're typing and hit z, it'll select the option you've chosen success = true; - const option = this.config?.options[this.cursor + (this.scrollCursor - (this.scrollCursor ? 1 : 0))]; + const option = this.config?.options[this.unskippedIndices[this.fullCursor]]; if (option?.handler()) { if (!option.keepOpen) { this.clear(); @@ -223,15 +245,15 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { } else { switch (button) { case Button.UP: - if (this.cursor) { - success = this.setCursor(this.cursor - 1); - } else if (this.cursor === 0) { - success = this.setCursor(options.length - 1); + if (this.fullCursor === 0) { + success = this.setCursor(this.unskippedIndices.length - 1); + } else if (this.fullCursor) { + success = this.setCursor(this.fullCursor - 1); } break; case Button.DOWN: - if (this.cursor < options.length - 1) { - success = this.setCursor(this.cursor + 1); + if (this.fullCursor < this.unskippedIndices.length - 1) { + success = this.setCursor(this.fullCursor + 1); } else { success = this.setCursor(0); } @@ -239,7 +261,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { } if (this.config?.supportHover) { // handle hover code if the element supports hover-handlers and the option has the optional hover-handler set. - this.config?.options[this.cursor + (this.scrollCursor - (this.scrollCursor ? 1 : 0))]?.onHover?.(); + this.config?.options[this.unskippedIndices[this.fullCursor]]?.onHover?.(); } } @@ -273,7 +295,9 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { const optionsScrollTotal = options.length; const optionStartIndex = this.scrollCursor; - const optionEndIndex = Math.min(optionsScrollTotal, optionStartIndex + (!optionStartIndex || this.scrollCursor + (this.config.maxOptions - 1) >= optionsScrollTotal ? this.config.maxOptions - 1 : this.config.maxOptions - 2)); + const optionEndIndex = Math.min(optionsScrollTotal, optionStartIndex + + (!optionStartIndex || this.scrollCursor + (this.config.maxOptions - 1) >= optionsScrollTotal ? this.config.maxOptions - 1 : this.config.maxOptions - 2) + ); if (this.config?.maxOptions && options.length > this.config.maxOptions) { options.splice(optionEndIndex, optionsScrollTotal); @@ -281,13 +305,15 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { if (optionStartIndex) { options.unshift({ label: scrollUpLabel, - handler: () => true + handler: () => true, + style: this.defaultTextStyle }); } if (optionEndIndex < optionsScrollTotal) { options.push({ label: scrollDownLabel, - handler: () => true + handler: () => true, + style: this.defaultTextStyle }); } } @@ -295,42 +321,64 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { return options; } - setCursor(cursor: integer): boolean { - const changed = this.cursor !== cursor; + getUnskippedIndices(options: OptionSelectItem[]) { + const unskippedIndices = options + .map((option, index) => (option.skip ? null : index)) // Map to index or null if skipped + .filter(index => index !== null) as number[]; + return unskippedIndices; + } + + setCursor(fullCursor: integer): boolean { + const changed = this.fullCursor !== fullCursor; - let isScroll = false; - const options = this.getOptionsWithScroll(); if (changed && this.config?.maxOptions && this.config.options.length > this.config.maxOptions) { - if (Math.abs(cursor - this.cursor) === options.length - 1) { - // Wrap around the list - const optionsScrollTotal = this.config.options.length; - this.scrollCursor = cursor ? optionsScrollTotal - (this.config.maxOptions - 1) : 0; - this.setupOptions(); + + // If the fullCursor is the last possible value, we go to the bottom + if (fullCursor === this.unskippedIndices.length - 1) { + this.fullCursor = fullCursor; + this.cursor = this.config.maxOptions - (this.config.options.length - this.unskippedIndices[fullCursor]); + this.scrollCursor = this.config.options.length - this.config.maxOptions + 1; + // If the fullCursor is the first possible value, we go to the top + } else if (fullCursor === 0) { + this.fullCursor = fullCursor; + this.cursor = this.unskippedIndices[fullCursor]; + this.scrollCursor = 0; } else { - // Move the cursor up or down by 1 - const isDown = cursor && cursor > this.cursor; + const isDown = fullCursor && fullCursor > this.fullCursor; + if (isDown) { - if (options[cursor].label === scrollDownLabel) { - isScroll = true; - this.scrollCursor++; + // If there are skipped options under the next selection, we show them + const jumpFromCurrent = this.unskippedIndices[fullCursor] - this.unskippedIndices[this.fullCursor]; + const skipsFromNext = this.unskippedIndices[fullCursor + 1] - this.unskippedIndices[fullCursor] - 1; + + if (this.cursor + jumpFromCurrent + skipsFromNext >= this.config.maxOptions - 1) { + this.fullCursor = fullCursor; + this.cursor = this.config.maxOptions - 2 - skipsFromNext; + this.scrollCursor = this.unskippedIndices[this.fullCursor] - this.cursor + 1; + } else { + this.fullCursor = fullCursor; + this.cursor = this.unskippedIndices[fullCursor] - this.scrollCursor + (this.scrollCursor ? 1 : 0); } } else { - if (!cursor && this.scrollCursor) { - isScroll = true; - this.scrollCursor--; + const jumpFromPrevious = this.unskippedIndices[fullCursor] - this.unskippedIndices[fullCursor - 1]; + + if (this.cursor - jumpFromPrevious < 1) { + this.fullCursor = fullCursor; + this.cursor = 1; + this.scrollCursor = this.unskippedIndices[this.fullCursor] - this.cursor + 1; + } else { + this.fullCursor = fullCursor; + this.cursor = this.unskippedIndices[fullCursor] - this.scrollCursor + (this.scrollCursor ? 1 : 0); } } - if (isScroll && this.scrollCursor === 1) { - this.scrollCursor += isDown ? 1 : -1; - } } - } - if (isScroll) { - this.setupOptions(); } else { - this.cursor = cursor; + this.fullCursor = fullCursor; + this.cursor = this.unskippedIndices[fullCursor]; } + this.setupOptions(); + if (!this.cursorObj) { this.cursorObj = globalScene.add.image(0, 0, "cursor"); this.optionSelectContainer.add(this.cursorObj); @@ -346,6 +394,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { super.clear(); this.config = null; this.optionSelectContainer.setVisible(false); + this.fullCursor = 0; this.scrollCursor = 0; this.eraseCursor(); } diff --git a/src/ui/base-stats-overlay.ts b/src/ui/base-stats-overlay.ts new file mode 100644 index 00000000000..30520bdf3ec --- /dev/null +++ b/src/ui/base-stats-overlay.ts @@ -0,0 +1,124 @@ +import type { InfoToggle } from "../battle-scene"; +import { TextStyle, addTextObject } from "./text"; +import { addWindow } from "./ui-theme"; +import * as Utils from "../utils"; +import i18next from "i18next"; +import { globalScene } from "#app/global-scene"; + +export interface BaseStatsOverlaySettings { + scale?:number; // scale the box? A scale of 0.5 is recommended + x?: number; + y?: number; + /** Default is always half the screen, regardless of scale */ + width?: number; +} + +const HEIGHT = 120; +const BORDER = 8; +const GLOBAL_SCALE = 6; +const shortStats = [ "HP", "ATK", "DEF", "SPATK", "SPDEF", "SPD" ]; + +export default class BaseStatsOverlay extends Phaser.GameObjects.Container implements InfoToggle { + + public active: boolean = false; + + private statsLabels: Phaser.GameObjects.Text[] = []; + private statsRectangles: Phaser.GameObjects.Rectangle[] = []; + private statsShadows: Phaser.GameObjects.Rectangle[] = []; + private statsTotalLabel: Phaser.GameObjects.Text; + + private statsBg: Phaser.GameObjects.NineSlice; + + private options: BaseStatsOverlaySettings; + + public scale: number; + public width: number; + + constructor(options?: BaseStatsOverlaySettings) { + super(globalScene, options?.x, options?.y); + this.scale = options?.scale || 1; // set up the scale + this.setScale(this.scale); + this.options = options || {}; + + // prepare the description box + this.width = (options?.width || BaseStatsOverlay.getWidth(this.scale)) / this.scale; // divide by scale as we always want this to be half a window wide + this.statsBg = addWindow(0, 0, this.width, HEIGHT); + this.statsBg.setOrigin(0, 0); + this.add(this.statsBg); + + for (let i = 0; i < 6; i++) { + const shadow = globalScene.add.rectangle(this.width - BORDER + 1, BORDER + 3 + i * 15, 100, 5, 0x006860); + shadow.setOrigin(1, 0); + this.statsShadows.push(shadow); + this.add(shadow); + + const rectangle = globalScene.add.rectangle(this.width - BORDER, BORDER + 2 + i * 15, 100, 5, 0x66aa99); + rectangle.setOrigin(1, 0); + this.statsRectangles.push(rectangle); + this.add(rectangle); + + const label = addTextObject(BORDER, BORDER - 2 + i * 15, "A", TextStyle.BATTLE_INFO); + this.statsLabels.push(label); + this.add(label); + } + + this.statsTotalLabel = addTextObject(BORDER, BORDER + 6 * 15, "A", TextStyle.MONEY_WINDOW); + this.add(this.statsTotalLabel); + + // hide this component for now + this.setVisible(false); + } + + // show this component with infos for the specific move + show(values: number[], total: number):boolean { + + for (let i = 0; i < 6; i++) { + this.statsLabels[i].setText(i18next.t(`pokemonInfo:Stat.${shortStats[i]}shortened`) + ": " + `${values[i]}`); + // This accounts for base stats up to 200, might not be enough. + // TODO: change color based on value. + this.statsShadows[i].setSize(values[i] / 2, 5); + this.statsRectangles[i].setSize(values[i] / 2, 5); + } + + this.statsTotalLabel.setText(i18next.t("pokedexUiHandler:baseTotal") + ": " + `${total}`); + + + this.setVisible(true); + this.active = true; + return true; + } + + clear() { + this.setVisible(false); + this.active = false; + } + + toggleInfo(visible: boolean): void { + if (visible) { + this.setVisible(true); + } + globalScene.tweens.add({ + targets: this.statsLabels, + duration: Utils.fixedInt(125), + ease: "Sine.easeInOut", + alpha: visible ? 1 : 0 + }); + if (!visible) { + this.setVisible(false); + } + } + + isActive(): boolean { + return this.active; + } + + // width of this element + static getWidth(scale:number):number { + return globalScene.game.canvas.width / GLOBAL_SCALE / 2; + } + + // height of this element + static getHeight(scale:number, onSide?: boolean):number { + return HEIGHT * scale; + } +} diff --git a/src/ui/dropdown.ts b/src/ui/dropdown.ts index 8c318b29d64..f8824e8bef0 100644 --- a/src/ui/dropdown.ts +++ b/src/ui/dropdown.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; import { addTextObject, TextStyle } from "./text"; import { addWindow, WindowVariant } from "./ui-theme"; +import { ScrollBar } from "#app/ui/scroll-bar"; import i18next from "i18next"; export enum DropDownState { @@ -293,21 +294,37 @@ export class DropDown extends Phaser.GameObjects.Container { private onChange: () => void; private lastDir: SortDirection = SortDirection.ASC; private defaultSettings: any[]; + private dropDownScrollBar: ScrollBar; + private totalOptions: number = 0; + private maxOptions: number = 0; + private shownOptions: number = 0; + private tooManyOptions: Boolean = false; + private firstShown: number = 0; + private optionHeight: number = 0; + private optionSpacing: number = 0; + private optionPaddingX: number = 4; + private optionPaddingY: number = 6; + private optionWidth: number = 100; + private cursorOffset: number = 0; constructor(x: number, y: number, options: DropDownOption[], onChange: () => void, type: DropDownType = DropDownType.MULTI, optionSpacing: number = 2) { const windowPadding = 5; - const optionHeight = 7; - const optionPaddingX = 4; - const optionPaddingY = 6; const cursorOffset = 7; - const optionWidth = 100; super(globalScene, x - cursorOffset - windowPadding, y); + + this.optionWidth = 100; + this.optionHeight = 7; + this.optionSpacing = optionSpacing; + this.optionPaddingX = 4; + this.optionPaddingY = 6; + this.cursorOffset = cursorOffset; + this.options = options; this.dropDownType = type; this.onChange = onChange; - this.cursorObj = globalScene.add.image(optionPaddingX + 3, 0, "cursor"); + this.cursorObj = globalScene.add.image(this.optionPaddingX + 3, 0, "cursor"); this.cursorObj.setScale(0.5); this.cursorObj.setOrigin(0, 0.5); this.cursorObj.setVisible(false); @@ -317,31 +334,51 @@ export class DropDown extends Phaser.GameObjects.Container { this.options.unshift(new DropDownOption("ALL", new DropDownLabel(i18next.t("filterBar:all"), undefined, this.checkForAllOn() ? DropDownState.ON : DropDownState.OFF))); } + this.maxOptions = 19; + this.totalOptions = this.options.length; + this.tooManyOptions = this.totalOptions > this.maxOptions; + this.shownOptions = this.tooManyOptions ? this.maxOptions : this.totalOptions; + this.defaultSettings = this.getSettings(); // Place ui elements in the correct spot options.forEach((option, index) => { + const toggleVisibility = type !== DropDownType.SINGLE || option.state === DropDownState.ON; option.setupToggleIcon(type, toggleVisibility); - option.width = optionWidth; - option.y = index * optionHeight + index * optionSpacing + optionPaddingY; + option.width = this.optionWidth; + option.y = index * this.optionHeight + index * optionSpacing + this.optionPaddingY; - const baseX = cursorOffset + optionPaddingX + 3; - const baseY = optionHeight / 2; + const baseX = cursorOffset + this.optionPaddingX + 3; + const baseY = this.optionHeight / 2; option.setLabelPosition(baseX + 8, baseY); if (type === DropDownType.SINGLE) { option.setTogglePosition(baseX + 3, baseY + 1); } else { option.setTogglePosition(baseX, baseY); } + + if (index >= this.shownOptions) { + option.visible = false; + } + + this.firstShown = 0; }); - this.window = addWindow(0, 0, optionWidth, options[options.length - 1].y + optionHeight + optionPaddingY, false, false, undefined, undefined, WindowVariant.XTHIN); + this.window = addWindow(0, 0, this.optionWidth, options[this.shownOptions - 1].y + this.optionHeight + this.optionPaddingY, false, false, undefined, undefined, WindowVariant.XTHIN); this.add(this.window); this.add(options); this.add(this.cursorObj); this.setVisible(false); + + if (this.tooManyOptions) { + // Setting the last parameter to 1 turns out to be optimal in all cases. + this.dropDownScrollBar = new ScrollBar(this.window.width - 3, 5, 5, this.window.height - 10, 1); + this.add(this.dropDownScrollBar); + this.dropDownScrollBar.setTotalRows(this.totalOptions); + this.dropDownScrollBar.setScrollCursor(0); + } } getWidth(): number { @@ -371,6 +408,11 @@ export class DropDown extends Phaser.GameObjects.Container { } setCursor(cursor: integer): boolean { + + if (this.tooManyOptions) { + this.setLabels(cursor); + } + this.cursor = cursor; if (cursor < 0) { cursor = 0; @@ -393,6 +435,41 @@ export class DropDown extends Phaser.GameObjects.Container { return true; } + setLabels(cursor: integer) { + + if ((cursor === 0) && (this.lastCursor === this.totalOptions - 1)) { + this.firstShown = 0; + } else if ((cursor === this.totalOptions - 1) && (this.lastCursor === 0)) { + this.firstShown = this.totalOptions - this.shownOptions; + } else if ((cursor - this.firstShown >= this.shownOptions) && (cursor > this.lastCursor)) { + this.firstShown += 1; + } else if ((cursor < this.firstShown) && (cursor < this.lastCursor)) { + this.firstShown -= 1; + } + + this.options.forEach((option, index) => { + + option.y = (index - this.firstShown) * (this.optionHeight + this.optionSpacing) + this.optionPaddingY; + + const baseX = this.cursorOffset + this.optionPaddingX + 3; + const baseY = this.optionHeight / 2; + option.setLabelPosition(baseX + 8, baseY); + if (this.dropDownType === DropDownType.SINGLE) { + option.setTogglePosition(baseX + 3, baseY + 1); + } else { + option.setTogglePosition(baseX, baseY); + } + + if ((index < this.firstShown) || ( index >= this.firstShown + this.shownOptions)) { + option.visible = false; + } else { + option.visible = true; + } + }); + + this.dropDownScrollBar.setScrollCursor(cursor); + } + /** * Switch the option at the provided index to its next state and update visuals * Update accordingly the other options if needed: @@ -597,7 +674,12 @@ export class DropDown extends Phaser.GameObjects.Container { x = this.options[i].getCurrentLabelX() ?? 0; } } - this.window.width = maxWidth + x - this.window.x + 6; + this.window.width = maxWidth + x - this.window.x + 9; + + if (this.tooManyOptions) { + this.window.width += 6; + this.dropDownScrollBar.x = this.window.width - 9; + } if (this.x + this.window.width > this.parentContainer.width) { this.x = this.parentContainer.width - this.window.width; diff --git a/src/ui/filter-bar.ts b/src/ui/filter-bar.ts index a6f9f66efe2..1eba81247d4 100644 --- a/src/ui/filter-bar.ts +++ b/src/ui/filter-bar.ts @@ -9,6 +9,7 @@ import { globalScene } from "#app/global-scene"; export enum DropDownColumn { GEN, TYPES, + BIOME, CAUGHT, UNLOCKS, MISC, @@ -25,13 +26,20 @@ export class FilterBar extends Phaser.GameObjects.Container { public openDropDown: boolean = false; private lastCursor: number = -1; private uiTheme: UiTheme; + private leftPaddingX: number; + private rightPaddingX: number; + private cursorOffset: number; - constructor(x: number, y: number, width: number, height: number) { + constructor(x: number, y: number, width: number, height: number, leftPaddingX: number = 6, rightPaddingX: number = 6, cursorOffset: number = 8) { super(globalScene, x, y); this.width = width; this.height = height; + this.leftPaddingX = leftPaddingX; + this.rightPaddingX = rightPaddingX; + this.cursorOffset = cursorOffset; + this.window = addWindow(0, 0, width, height, false, false, undefined, undefined, WindowVariant.THIN); this.add(this.window); @@ -40,8 +48,6 @@ export class FilterBar extends Phaser.GameObjects.Container { this.cursorObj.setVisible(false); this.cursorObj.setOrigin(0, 0); this.add(this.cursorObj); - - this.uiTheme = globalScene.uiTheme; } /** @@ -86,9 +92,9 @@ export class FilterBar extends Phaser.GameObjects.Container { updateFilterLabels(): void { for (let i = 0; i < this.numFilters; i++) { if (this.dropDowns[i].hasDefaultValues()) { - this.labels[i].setColor(getTextColor(TextStyle.TOOLTIP_CONTENT, false, this.uiTheme)); + this.labels[i].setColor(getTextColor(TextStyle.TOOLTIP_CONTENT, false, globalScene.uiTheme)); } else { - this.labels[i].setColor(getTextColor(TextStyle.STATS_LABEL, false, this.uiTheme)); + this.labels[i].setColor(getTextColor(TextStyle.STATS_LABEL, false, globalScene.uiTheme)); } } } @@ -97,23 +103,21 @@ export class FilterBar extends Phaser.GameObjects.Container { * Position the filter dropdowns evenly across the width of the container */ private calcFilterPositions(): void { - const paddingX = 6; - const cursorOffset = 8; - let totalWidth = paddingX * 2 + cursorOffset; + let totalWidth = this.leftPaddingX + this.rightPaddingX + this.cursorOffset; this.labels.forEach(label => { - totalWidth += label.displayWidth + cursorOffset; + totalWidth += label.displayWidth + this.cursorOffset; }); const spacing = (this.width - totalWidth) / (this.labels.length - 1); for (let i = 0; i < this.labels.length; i++) { if (i === 0) { - this.labels[i].x = paddingX + cursorOffset; + this.labels[i].x = this.leftPaddingX + this.cursorOffset; } else { const lastRight = this.labels[i - 1].x + this.labels[i - 1].displayWidth; - this.labels[i].x = lastRight + spacing + cursorOffset; + this.labels[i].x = lastRight + spacing + this.cursorOffset; } - this.dropDowns[i].x = this.labels[i].x - cursorOffset - paddingX; + this.dropDowns[i].x = this.labels[i].x - this.cursorOffset - this.leftPaddingX; this.dropDowns[i].y = this.height; } } @@ -140,8 +144,7 @@ export class FilterBar extends Phaser.GameObjects.Container { } } - const cursorOffset = 8; - this.cursorObj.setPosition(this.labels[cursor].x - cursorOffset + 2, 6); + this.cursorObj.setPosition(this.labels[cursor].x - this.cursorOffset + 2, 6); this.lastCursor = cursor; } diff --git a/src/ui/filter-text.ts b/src/ui/filter-text.ts new file mode 100644 index 00000000000..22f8b6e8ac5 --- /dev/null +++ b/src/ui/filter-text.ts @@ -0,0 +1,230 @@ +import type { StarterContainer } from "./starter-container"; +import { addTextObject, getTextColor, TextStyle } from "./text"; +import type { UiTheme } from "#enums/ui-theme"; +import { addWindow, WindowVariant } from "./ui-theme"; +import i18next from "i18next"; +import type AwaitableUiHandler from "./awaitable-ui-handler"; +import type UI from "./ui"; +import { Mode } from "./ui"; +import { globalScene } from "#app/global-scene"; + +export enum FilterTextRow{ + NAME, + MOVE_1, + MOVE_2, + ABILITY_1, + ABILITY_2, +} + +export class FilterText extends Phaser.GameObjects.Container { + private window: Phaser.GameObjects.NineSlice; + private labels: Phaser.GameObjects.Text[] = []; + private selections: Phaser.GameObjects.Text[] = []; + private selectionStrings: string[] = []; + // private dropDowns: DropDown[] = []; + private rows: FilterTextRow[] = []; + public cursorObj: Phaser.GameObjects.Image; + public numFilters: number = 0; + // public openDropDown: boolean = false; + private lastCursor: number = -1; + private uiTheme: UiTheme; + + private menuMessageBoxContainer: Phaser.GameObjects.Container; + private dialogueMessageBox: Phaser.GameObjects.NineSlice; + message: any; + private readonly textPadding = 8; + private readonly defaultWordWrapWidth = 1224; + + private onChange: () => void; + + public defaultText: string = "---"; + + constructor(x: number, y: number, width: number, height: number, onChange: () => void,) { + super(globalScene, x, y); + + this.onChange = onChange; + + this.width = width; + this.height = height; + + this.window = addWindow(0, 0, width, height, false, false, undefined, undefined, WindowVariant.THIN); + this.add(this.window); + + this.cursorObj = globalScene.add.image(1, 1, "cursor"); + this.cursorObj.setScale(0.5); + this.cursorObj.setVisible(false); + this.cursorObj.setOrigin(0, 0); + this.add(this.cursorObj); + + this.menuMessageBoxContainer = globalScene.add.container(0, 130); + this.menuMessageBoxContainer.setName("menu-message-box"); + this.menuMessageBoxContainer.setVisible(false); + + // Full-width window used for testing dialog messages in debug mode + this.dialogueMessageBox = addWindow(-this.textPadding, 0, globalScene.game.canvas.width / 6 + this.textPadding * 2, 49, false, false, 0, 0, WindowVariant.THIN); + this.dialogueMessageBox.setOrigin(0, 0); + this.menuMessageBoxContainer.add(this.dialogueMessageBox); + + const menuMessageText = addTextObject(this.textPadding, this.textPadding, "", TextStyle.WINDOW, { maxLines: 2 }); + menuMessageText.setName("menu-message"); + menuMessageText.setOrigin(0, 0); + this.menuMessageBoxContainer.add(menuMessageText); + + // this.initTutorialOverlay(this.menuContainer); + // this.initPromptSprite(this.menuMessageBoxContainer); + + this.message = menuMessageText; + + } + + /** + * Add a new filter to the FilterBar, as long that a unique DropDownColumn is provided + * @param column the DropDownColumn that will be used to access the filter values + * @param title the string that will get displayed in the filter bar + * @param dropDown the DropDown with all options for this filter + * @returns true if successful, false if the provided column was already in use for another filter + */ + addFilter(row: FilterTextRow, title: string): boolean { + // The column should be unique to each filter, + + const paddingX = 6; + const cursorOffset = 8; + const extraSpaceX = 40; + + if (this.rows.includes(row)) { + return false; + } + + this.rows.push(row); + + const filterTypesLabel = addTextObject(paddingX + cursorOffset, 3, title, TextStyle.TOOLTIP_CONTENT); + this.labels.push(filterTypesLabel); + this.add(filterTypesLabel); + + const filterTypesSelection = addTextObject(paddingX + cursorOffset + extraSpaceX, 3, this.defaultText, TextStyle.TOOLTIP_CONTENT); + this.selections.push(filterTypesSelection); + this.add(filterTypesSelection); + + this.selectionStrings.push(""); + + this.calcFilterPositions(); + this.numFilters++; + + return true; + } + + resetSelection(index: number): void { + this.selections[index].setText(this.defaultText); + this.selectionStrings[index] = ""; + this.onChange(); + } + + setValsToDefault(): void { + for (let i = 0; i < this.numFilters; i++) { + this.resetSelection(i); + } + } + + startSearch(index: number, ui: UI): void { + + ui.playSelect(); + const prefilledText = ""; + const buttonAction: any = {}; + buttonAction["buttonActions"] = [ + (sanitizedName: string) => { + // ui.revertMode(); + ui.playSelect(); + const dialogueTestName = sanitizedName; + //TODO: Is it really necessary to encode and decode? + const dialogueName = decodeURIComponent(escape(atob(dialogueTestName))); + const handler = ui.getHandler() as AwaitableUiHandler; + handler.tutorialActive = true; + // Switch to the dialog test window + this.selections[index].setText(String(i18next.t(dialogueName))); + ui.revertMode(); + this.onChange(); + }, + () => { + ui.revertMode(); + this.onChange; + } + ]; + ui.setOverlayMode(Mode.POKEDEX_SCAN, buttonAction, prefilledText, index); + } + + + setCursor(cursor: number): void { + const cursorOffset = 8; + + this.cursorObj.setPosition(cursorOffset, this.labels[cursor].y + 3); + this.lastCursor = cursor; + } + + + /////////////////From here down changes must be made + /** + * Highlight the labels of the FilterBar if the filters are different from their default values + */ + updateFilterLabels(): void { + for (let i = 0; i < this.numFilters; i++) { + if (this.selections[i].text === this.defaultText) { + this.labels[i].setColor(getTextColor(TextStyle.TOOLTIP_CONTENT, false, globalScene.uiTheme)); + } else { + this.labels[i].setColor(getTextColor(TextStyle.STATS_LABEL, false, globalScene.uiTheme)); + } + } + } + + /** + * Position the filter dropdowns evenly across the width of the container + */ + private calcFilterPositions(): void { + const paddingY = 8; + + let totalHeight = paddingY * 2; + this.labels.forEach(label => { + totalHeight += label.displayHeight; + }); + const spacing = (this.height - totalHeight) / (this.labels.length - 1); + for (let i = 0; i < this.labels.length; i++) { + if (i === 0) { + this.labels[i].y = paddingY; + this.selections[i].y = paddingY; + } else { + const lastBottom = this.labels[i - 1].y + this.labels[i - 1].displayHeight; + this.labels[i].y = lastBottom + spacing; + this.selections[i].y = lastBottom + spacing; + } + // Uncomment and adjust if necessary to position dropdowns vertically + // this.dropDowns[i].y = this.labels[i].y + this.labels[i].displayHeight + paddingY; + // this.dropDowns[i].x = this.width; + } + } + + getValue(row: number): string { + return this.selections[row].getWrappedText()[0]; + } + + /** + * Find the nearest filter to the provided container on the y-axis + * @param container the StarterContainer to compare position against + * @returns the index of the closest filter + */ + getNearestFilter(container: StarterContainer): number { + + const midy = container.y + container.icon.displayHeight / 2; + let nearest = 0; + let nearestDist = 1000; + for (let i = 0; i < this.labels.length; i++) { + const dist = Math.abs(midy - (this.labels[i].y + this.labels[i].displayHeight / 3)); + if (dist < nearestDist) { + nearest = i; + nearestDist = dist; + } + } + + return nearest; + } + + +} diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 7ea8dbe7a4e..76d58d319b0 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -24,6 +24,7 @@ enum MenuOptions { RUN_HISTORY, EGG_LIST, EGG_GACHA, + POKEDEX, MANAGE_DATA, COMMUNITY, SAVE_AND_QUIT, @@ -527,6 +528,11 @@ export default class MenuUiHandler extends MessageUiHandler { ui.setOverlayMode(Mode.EGG_GACHA); success = true; break; + case MenuOptions.POKEDEX: + ui.revertMode(); + ui.setOverlayMode(Mode.POKEDEX); + success = true; + break; case MenuOptions.MANAGE_DATA: if (!bypassLogin && !this.manageDataConfig.options.some(o => o.label === i18next.t("menuUiHandler:linkDiscord") || o.label === i18next.t("menuUiHandler:unlinkDiscord"))) { this.manageDataConfig.options.splice(this.manageDataConfig.options.length - 1, 0, diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 4a7716f7e62..605d907a4d0 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -8,7 +8,7 @@ import { Mode } from "#app/ui/ui"; import * as Utils from "#app/utils"; import { PokemonFormChangeItemModifier, PokemonHeldItemModifier, SwitchEffectTransferModifier } from "#app/modifier/modifier"; import { allMoves, ForceSwitchOutAttr } from "#app/data/move"; -import { getGenderColor, getGenderSymbol } from "#app/data/gender"; +import { Gender, getGenderColor, getGenderSymbol } from "#app/data/gender"; import { StatusEffect } from "#enums/status-effect"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler"; import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; @@ -109,6 +109,7 @@ export enum PartyOption { TEACH, TRANSFER, SUMMARY, + POKEDEX, UNPAUSE_EVOLUTION, SPLICE, UNSPLICE, @@ -218,7 +219,7 @@ export default class PartyUiHandler extends MessageUiHandler { public static NoEffectMessage = i18next.t("partyUiHandler:anyEffect"); - private localizedOptions = [ PartyOption.SEND_OUT, PartyOption.SUMMARY, PartyOption.CANCEL, PartyOption.APPLY, PartyOption.RELEASE, PartyOption.TEACH, PartyOption.SPLICE, PartyOption.UNSPLICE, PartyOption.REVIVE, PartyOption.TRANSFER, PartyOption.UNPAUSE_EVOLUTION, PartyOption.PASS_BATON, PartyOption.RENAME, PartyOption.SELECT ]; + private localizedOptions = [ PartyOption.SEND_OUT, PartyOption.SUMMARY, PartyOption.POKEDEX, PartyOption.CANCEL, PartyOption.APPLY, PartyOption.RELEASE, PartyOption.TEACH, PartyOption.SPLICE, PartyOption.UNSPLICE, PartyOption.REVIVE, PartyOption.TRANSFER, PartyOption.UNPAUSE_EVOLUTION, PartyOption.PASS_BATON, PartyOption.RENAME, PartyOption.SELECT ]; constructor() { super(Mode.PARTY); @@ -397,7 +398,7 @@ export default class PartyUiHandler extends MessageUiHandler { } ui.playSelect(); return true; - } else if ((option !== PartyOption.SUMMARY && option !== PartyOption.UNPAUSE_EVOLUTION && option !== PartyOption.UNSPLICE && option !== PartyOption.RELEASE && option !== PartyOption.CANCEL && option !== PartyOption.RENAME) + } else if ((option !== PartyOption.SUMMARY && option !== PartyOption.POKEDEX && option !== PartyOption.UNPAUSE_EVOLUTION && option !== PartyOption.UNSPLICE && option !== PartyOption.RELEASE && option !== PartyOption.CANCEL && option !== PartyOption.RENAME) || (option === PartyOption.RELEASE && this.partyUiMode === PartyUiMode.RELEASE)) { let filterResult: string | null; const getTransferrableItemsFromPokemon = (pokemon: PlayerPokemon) => @@ -466,6 +467,16 @@ export default class PartyUiHandler extends MessageUiHandler { ui.playSelect(); ui.setModeWithoutClear(Mode.SUMMARY, pokemon).then(() => this.clearOptions()); return true; + } else if (option === PartyOption.POKEDEX) { + ui.playSelect(); + const attributes = { + shiny: pokemon.shiny, + variant: pokemon.variant, + form: pokemon.formIndex, + female: pokemon.gender === Gender.FEMALE ? true : false + }; + ui.setOverlayMode(Mode.POKEDEX_PAGE, pokemon.species, pokemon.formIndex, attributes).then(() => this.clearOptions()); + return true; } else if (option === PartyOption.UNPAUSE_EVOLUTION) { this.clearOptions(); ui.playSelect(); @@ -892,6 +903,7 @@ export default class PartyUiHandler extends MessageUiHandler { } this.options.push(PartyOption.SUMMARY); + this.options.push(PartyOption.POKEDEX); this.options.push(PartyOption.RENAME); if ((pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) || (pokemon.isFusion() && pokemon.fusionSpecies && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId)))) { diff --git a/src/ui/pokedex-info-overlay.ts b/src/ui/pokedex-info-overlay.ts new file mode 100644 index 00000000000..fe0b47b57e0 --- /dev/null +++ b/src/ui/pokedex-info-overlay.ts @@ -0,0 +1,174 @@ +import type { InfoToggle } from "../battle-scene"; +import { TextStyle, addTextObject } from "./text"; +import { addWindow } from "./ui-theme"; +import * as Utils from "../utils"; +import i18next from "i18next"; +import { globalScene } from "#app/global-scene"; + +export interface PokedexInfoOverlaySettings { + delayVisibility?: boolean; // if true, showing the overlay will only set it to active and populate the fields and the handler using this field has to manually call setVisible later. + scale?:number; // scale the box? A scale of 0.5 is recommended + //location and width of the component; unaffected by scaling + x?: number; + y?: number; + /** Default is always half the screen, regardless of scale */ + width?: number; + /** Determines whether to display the small secondary box */ + hideEffectBox?: boolean; + hideBg?: boolean; +} + +const DESC_HEIGHT = 48; +const BORDER = 8; +const GLOBAL_SCALE = 6; + +export default class PokedexInfoOverlay extends Phaser.GameObjects.Container implements InfoToggle { + public active: boolean = false; + + private desc: Phaser.GameObjects.Text; + private descScroll : Phaser.Tweens.Tween | null = null; + + private descBg: Phaser.GameObjects.NineSlice; + + private options: PokedexInfoOverlaySettings; + + private textMaskRect: Phaser.GameObjects.Graphics; + + private maskPointOriginX: number; + private maskPointOriginY: number; + public scale: number; + public width: number; + + constructor(options?: PokedexInfoOverlaySettings) { + super(globalScene, options?.x, options?.y); + this.scale = options?.scale || 1; // set up the scale + this.setScale(this.scale); + this.options = options || {}; + + // prepare the description box + this.width = (options?.width || PokedexInfoOverlay.getWidth(this.scale)) / this.scale; // divide by scale as we always want this to be half a window wide + this.descBg = addWindow(0, 0, this.width, DESC_HEIGHT); + this.descBg.setOrigin(0, 0); + this.add(this.descBg); + + // set up the description; wordWrap uses true pixels, unaffected by any scaling, while other values are affected + this.desc = addTextObject(BORDER, BORDER - 2, "", TextStyle.BATTLE_INFO, { wordWrap: { width: (this.width - (BORDER - 2) * 2) * GLOBAL_SCALE }}); + this.desc.setLineSpacing(i18next.resolvedLanguage === "ja" ? 25 : 5); + + // limit the text rendering, required for scrolling later on + this.maskPointOriginX = options?.x || 0; + this.maskPointOriginY = options?.y || 0; + + if (this.maskPointOriginX < 0) { + this.maskPointOriginX += globalScene.game.canvas.width / GLOBAL_SCALE; + } + if (this.maskPointOriginY < 0) { + this.maskPointOriginY += globalScene.game.canvas.height / GLOBAL_SCALE; + } + + this.textMaskRect = globalScene.make.graphics(); + this.textMaskRect.fillStyle(0xFF0000); + this.textMaskRect.fillRect( + this.maskPointOriginX + BORDER * this.scale, this.maskPointOriginY + (BORDER - 2) * this.scale, + this.width - (BORDER * 2) * this.scale, (DESC_HEIGHT - (BORDER - 2) * 2) * this.scale); + this.textMaskRect.setScale(6); + const textMask = this.createGeometryMask(this.textMaskRect); + + this.add(this.desc); + this.desc.setMask(textMask); + + if (options?.hideBg) { + this.descBg.setVisible(false); + } + + // hide this component for now + this.setVisible(false); + } + + // show this component with infos for the specific move + show(text: string):boolean { + if (!globalScene.enableMoveInfo) { + return false; // move infos have been disabled // TODO:: is `false` correct? i used to be `undeefined` + } + + this.desc.setText(text ?? ""); + + // stop previous scrolling effects and reset y position + if (this.descScroll) { + this.descScroll.remove(); + this.descScroll = null; + this.desc.y = BORDER - 2; + } + + // determine if we need to add new scrolling effects + const lineCount = Math.floor(this.desc.displayHeight * (96 / 72) / 14.83); + + const newHeight = lineCount >= 3 ? 48 : (lineCount === 2 ? 36 : 24); + this.textMaskRect.clear(); + this.textMaskRect.fillStyle(0xFF0000); + this.textMaskRect.fillRect( + this.maskPointOriginX + BORDER * this.scale, + this.maskPointOriginY + (BORDER - 2) * this.scale + (48 - newHeight), + this.width - (BORDER * 2) * this.scale, + (newHeight - (BORDER - 2) * 2) * this.scale + ); + const updatedMask = this.createGeometryMask(this.textMaskRect); + this.desc.setMask(updatedMask); + + this.descBg.setSize(this.descBg.width, newHeight); + this.descBg.setY(48 - newHeight); + this.desc.setY(BORDER - 2 + (48 - newHeight)); + + if (lineCount > 3) { + // generate scrolling effects + this.descScroll = globalScene.tweens.add({ + targets: this.desc, + delay: Utils.fixedInt(2000), + loop: -1, + hold: Utils.fixedInt(2000), + duration: Utils.fixedInt((lineCount - 3) * 2000), + y: `-=${14.83 * (72 / 96) * (lineCount - 3)}` + }); + } + + if (!this.options.delayVisibility) { + this.setVisible(true); + } + this.active = true; + return true; + } + + clear() { + this.setVisible(false); + this.active = false; + } + + toggleInfo(visible: boolean): void { + if (visible) { + this.setVisible(true); + } + globalScene.tweens.add({ + targets: this.desc, + duration: Utils.fixedInt(125), + ease: "Sine.easeInOut", + alpha: visible ? 1 : 0 + }); + if (!visible) { + this.setVisible(false); + } + } + + isActive(): boolean { + return this.active; + } + + // width of this element + static getWidth(scale:number):number { + return globalScene.game.canvas.width / GLOBAL_SCALE / 2; + } + + // height of this element + static getHeight(scale:number, onSide?: boolean):number { + return DESC_HEIGHT * scale; + } +} diff --git a/src/ui/pokedex-mon-container.ts b/src/ui/pokedex-mon-container.ts new file mode 100644 index 00000000000..f3932aa90c8 --- /dev/null +++ b/src/ui/pokedex-mon-container.ts @@ -0,0 +1,164 @@ +import { globalScene } from "#app/global-scene"; +import type PokemonSpecies from "../data/pokemon-species"; +import { addTextObject, TextStyle } from "./text"; + +export class PokedexMonContainer extends Phaser.GameObjects.Container { + public species: PokemonSpecies; + public icon: Phaser.GameObjects.Sprite; + public shinyIcons: Phaser.GameObjects.Image[] = []; + public label: Phaser.GameObjects.Text; + public starterPassiveBgs: Phaser.GameObjects.Image; + public hiddenAbilityIcon: Phaser.GameObjects.Image; + public favoriteIcon: Phaser.GameObjects.Image; + public classicWinIcon: Phaser.GameObjects.Image; + public candyUpgradeIcon: Phaser.GameObjects.Image; + public candyUpgradeOverlayIcon: Phaser.GameObjects.Image; + public eggMove1Icon: Phaser.GameObjects.Image; + public tmMove1Icon: Phaser.GameObjects.Image; + public eggMove2Icon: Phaser.GameObjects.Image; + public tmMove2Icon: Phaser.GameObjects.Image; + public passive1Icon: Phaser.GameObjects.Image; + public passive2Icon: Phaser.GameObjects.Image; + public cost: number = 0; + + 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); + + // starter passive bg + const starterPassiveBg = globalScene.add.image(2, 5, "passive_bg"); + starterPassiveBg.setOrigin(0, 0); + starterPassiveBg.setScale(0.75); + starterPassiveBg.setVisible(false); + 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"); + shinyIcon.setScale(0.5); + shinyIcon.setOrigin(0, 0); + shinyIcon.setVisible(false); + this.shinyIcons.push(shinyIcon); + } + this.add(this.shinyIcons); + + // value label + const label = addTextObject(1, 2, "0", TextStyle.WINDOW, { fontSize: "32px" }); + label.setShadowOffset(2, 2); + label.setOrigin(0, 0); + label.setVisible(false); + this.add(label); + this.label = label; + + // hidden ability icon + const abilityIcon = globalScene.add.image(12, 7, "ha_capsule"); + abilityIcon.setOrigin(0, 0); + abilityIcon.setScale(0.5); + abilityIcon.setVisible(false); + this.add(abilityIcon); + this.hiddenAbilityIcon = abilityIcon; + + // favorite icon + const favoriteIcon = globalScene.add.image(0, 7, "favorite"); + favoriteIcon.setOrigin(0, 0); + favoriteIcon.setScale(0.5); + favoriteIcon.setVisible(false); + this.add(favoriteIcon); + this.favoriteIcon = favoriteIcon; + + // classic win icon + const classicWinIcon = globalScene.add.image(0, 12, "champion_ribbon"); + classicWinIcon.setOrigin(0, 0); + classicWinIcon.setScale(0.5); + classicWinIcon.setVisible(false); + this.add(classicWinIcon); + this.classicWinIcon = classicWinIcon; + + // candy upgrade icon + const candyUpgradeIcon = globalScene.add.image(12, 12, "candy"); + candyUpgradeIcon.setOrigin(0, 0); + candyUpgradeIcon.setScale(0.25); + candyUpgradeIcon.setVisible(false); + this.add(candyUpgradeIcon); + this.candyUpgradeIcon = candyUpgradeIcon; + + // candy upgrade overlay icon + const candyUpgradeOverlayIcon = globalScene.add.image(12, 12, "candy_overlay"); + candyUpgradeOverlayIcon.setOrigin(0, 0); + candyUpgradeOverlayIcon.setScale(0.25); + candyUpgradeOverlayIcon.setVisible(false); + this.add(candyUpgradeOverlayIcon); + this.candyUpgradeOverlayIcon = candyUpgradeOverlayIcon; + + // move icons + const eggMove1Icon = globalScene.add.image(0, 12, "mystery_egg"); + eggMove1Icon.setOrigin(0, 0); + eggMove1Icon.setScale(0.25); + eggMove1Icon.setVisible(false); + this.add(eggMove1Icon); + this.eggMove1Icon = eggMove1Icon; + + // move icons + const tmMove1Icon = globalScene.add.image(0, 12, "normal_memory"); + tmMove1Icon.setOrigin(0, 0); + tmMove1Icon.setScale(0.25); + tmMove1Icon.setVisible(false); + this.add(tmMove1Icon); + this.tmMove1Icon = tmMove1Icon; + + // move icons + const eggMove2Icon = globalScene.add.image(7, 12, "mystery_egg"); + eggMove2Icon.setOrigin(0, 0); + eggMove2Icon.setScale(0.25); + eggMove2Icon.setVisible(false); + this.add(eggMove2Icon); + this.eggMove2Icon = eggMove2Icon; + + // move icons + const tmMove2Icon = globalScene.add.image(7, 12, "normal_memory"); + tmMove2Icon.setOrigin(0, 0); + tmMove2Icon.setScale(0.25); + tmMove2Icon.setVisible(false); + this.add(tmMove2Icon); + this.tmMove2Icon = tmMove2Icon; + + + // move icons + const passive1Icon = globalScene.add.image(3, 3, "candy"); + passive1Icon.setOrigin(0, 0); + passive1Icon.setScale(0.25); + passive1Icon.setVisible(false); + this.add(passive1Icon); + this.passive1Icon = passive1Icon; + + // move icons + const passive2Icon = globalScene.add.image(12, 3, "candy"); + passive2Icon.setOrigin(0, 0); + passive2Icon.setScale(0.25); + passive2Icon.setVisible(false); + this.add(passive2Icon); + this.passive2Icon = passive2Icon; + } + + 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.`); + this.icon.setTexture(this.species.getIconAtlasKey(formIndex, false, variant)); + this.icon.setFrame(this.species.getIconId(female, formIndex, false, variant)); + } + } +} diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts new file mode 100644 index 00000000000..c11a0b9a99a --- /dev/null +++ b/src/ui/pokedex-page-ui-handler.ts @@ -0,0 +1,2425 @@ +import type { SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; +import { pokemonEvolutions, pokemonPrevolutions, pokemonStarters } from "#app/data/balance/pokemon-evolutions"; +import type { Variant } from "#app/data/variant"; +import { getVariantTint, getVariantIcon } from "#app/data/variant"; +import { argbFromRgba } from "@material/material-color-utilities"; +import i18next from "i18next"; +import { starterColors } from "#app/battle-scene"; +import { allAbilities } from "#app/data/ability"; +import { speciesEggMoves } from "#app/data/balance/egg-moves"; +import { GrowthRate, getGrowthRateColor } from "#app/data/exp"; +import { Gender, getGenderColor, getGenderSymbol } from "#app/data/gender"; +import { allMoves } from "#app/data/move"; +import { getNatureName } from "#app/data/nature"; +import type { SpeciesFormChange } from "#app/data/pokemon-forms"; +import { pokemonFormChanges } from "#app/data/pokemon-forms"; +import type { LevelMoves } from "#app/data/balance/pokemon-level-moves"; +import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; +import type { PokemonForm } from "#app/data/pokemon-species"; +import type PokemonSpecies from "#app/data/pokemon-species"; +import { allSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; +import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; +import { starterPassiveAbilities } from "#app/data/balance/passives"; +import { Type } from "#enums/type"; +import { GameModes } from "#app/game-mode"; +import type { DexEntry, StarterAttributes } from "#app/system/game-data"; +import { AbilityAttr, DexAttr } from "#app/system/game-data"; +import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import { StatsContainer } from "#app/ui/stats-container"; +import { TextStyle, addTextObject, getTextStyleOptions } from "#app/ui/text"; +import { Mode } from "#app/ui/ui"; +import { addWindow } from "#app/ui/ui-theme"; +import { Egg } from "#app/data/egg"; +import Overrides from "#app/overrides"; +import { SettingKeyboard } from "#app/system/settings/settings-keyboard"; +import { Passive as PassiveAttr } from "#enums/passive"; +import * as Challenge from "#app/data/challenge"; +import MoveInfoOverlay from "#app/ui/move-info-overlay"; +import PokedexInfoOverlay from "#app/ui/pokedex-info-overlay"; +import { getEggTierForSpecies } from "#app/data/egg"; +import { Device } from "#enums/devices"; +import type { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Button } from "#enums/buttons"; +import { EggSourceType } from "#enums/egg-source-types"; +import { StarterContainer } from "#app/ui/starter-container"; +import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters"; +import { BooleanHolder, capitalizeString, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, rgbHexToRgba, toReadableString } from "#app/utils"; +import type { Nature } from "#enums/nature"; +import BgmBar from "./bgm-bar"; +import * as Utils from "../utils"; +import { speciesTmMoves } from "#app/data/balance/tms"; +import type { BiomeTierTod } from "#app/data/balance/biomes"; +import { BiomePoolTier, catchableSpecies } from "#app/data/balance/biomes"; +import { Biome } from "#app/enums/biome"; +import { TimeOfDay } from "#app/enums/time-of-day"; +import type { Abilities } from "#app/enums/abilities"; +import BaseStatsOverlay from "./base-stats-overlay"; +import { globalScene } from "#app/global-scene"; + + +interface LanguageSetting { + starterInfoTextSize: string, + instructionTextSize: string, + starterInfoXPos?: integer, + starterInfoYOffset?: integer +} + +const languageSettings: { [key: string]: LanguageSetting } = { + "en":{ + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, + "de":{ + starterInfoTextSize: "48px", + instructionTextSize: "35px", + starterInfoXPos: 33, + }, + "es-ES":{ + starterInfoTextSize: "56px", + instructionTextSize: "35px", + }, + "fr":{ + starterInfoTextSize: "54px", + instructionTextSize: "38px", + }, + "it":{ + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, + "pt_BR":{ + starterInfoTextSize: "47px", + instructionTextSize: "38px", + starterInfoXPos: 33, + }, + "zh":{ + starterInfoTextSize: "47px", + instructionTextSize: "38px", + starterInfoYOffset: 1, + starterInfoXPos: 24, + }, + "pt":{ + starterInfoTextSize: "48px", + instructionTextSize: "42px", + starterInfoXPos: 33, + }, + "ko":{ + starterInfoTextSize: "52px", + instructionTextSize: "38px", + }, + "ja":{ + starterInfoTextSize: "51px", + instructionTextSize: "38px", + }, + "ca-ES":{ + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, +}; + +const valueReductionMax = 2; + +// Position of UI elements +const speciesContainerX = 109; // if team on the RIGHT: 109 / if on the LEFT: 143 + +interface SpeciesDetails { + shiny?: boolean, + formIndex?: integer + female?: boolean, + variant?: integer, + forSeen?: boolean, // default = false +} + +enum MenuOptions { + BASE_STATS, + ABILITIES, + LEVEL_MOVES, + EGG_MOVES, + TM_MOVES, + BIOMES, + NATURES, + TOGGLE_IVS, + EVOLUTIONS +} + + +export default class PokedexPageUiHandler extends MessageUiHandler { + private starterSelectContainer: Phaser.GameObjects.Container; + private shinyOverlay: Phaser.GameObjects.Image; + private starterContainers: StarterContainer[] = []; + private filteredStarterContainers: 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 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 activeTooltip: "ABILITY" | "PASSIVE" | "CANDY" | undefined; + private instructionsContainer: Phaser.GameObjects.Container; + private filterInstructionsContainer: Phaser.GameObjects.Container; + private shinyIconElement: Phaser.GameObjects.Sprite; + private formIconElement: Phaser.GameObjects.Sprite; + private genderIconElement: Phaser.GameObjects.Sprite; + private variantIconElement: Phaser.GameObjects.Sprite; + private shinyLabel: Phaser.GameObjects.Text; + private formLabel: Phaser.GameObjects.Text; + private genderLabel: Phaser.GameObjects.Text; + private variantLabel: Phaser.GameObjects.Text; + private candyUpgradeIconElement: Phaser.GameObjects.Sprite; + private candyUpgradeLabel: Phaser.GameObjects.Text; + private showBackSpriteIconElement: Phaser.GameObjects.Sprite; + private showBackSpriteLabel: Phaser.GameObjects.Text; + + private starterSelectMessageBox: Phaser.GameObjects.NineSlice; + private starterSelectMessageBoxContainer: Phaser.GameObjects.Container; + private statsContainer: StatsContainer; + private moveInfoOverlay: MoveInfoOverlay; + private infoOverlay: PokedexInfoOverlay; + private baseStatsOverlay: BaseStatsOverlay; + + private statsMode: boolean; + + private allSpecies: PokemonSpecies[] = []; + private species: PokemonSpecies; + private formIndex: number; + private speciesLoaded: Map = new Map(); + private levelMoves: LevelMoves; + private eggMoves: Moves[] = []; + private hasEggMoves: boolean[] = []; + private tmMoves: Moves[] = []; + private ability1: Abilities; + private ability2: Abilities | undefined; + private abilityHidden: Abilities | undefined; + private passive: Abilities; + private hasPassive: boolean; + private hasAbilities: number[]; + private biomes: BiomeTierTod[]; + private preBiomes: BiomeTierTod[]; + private baseStats: number[]; + private baseTotal: number; + private evolutions: SpeciesFormEvolution[]; + private battleForms: SpeciesFormChange[]; + private prevolutions: SpeciesFormEvolution[]; + + private speciesStarterDexEntry: DexEntry | null; + private canCycleShiny: boolean; + private canCycleForm: boolean; + private canCycleGender: boolean; + + private assetLoadCancelled: BooleanHolder | null; + public cursorObj: Phaser.GameObjects.Image; + + // variables to keep track of the dynamically rendered list of instruction prompts for starter select + private instructionRowX = 0; + private instructionRowY = 0; + private instructionRowTextOffset = 9; + private filterInstructionRowX = 0; + private filterInstructionRowY = 0; + + private starterAttributes: StarterAttributes; + private savedStarterAttributes: StarterAttributes; + + protected blockInput: boolean = false; + protected blockInputOverlay: boolean = false; + + private showBackSprite: boolean = false; + + // Menu + private menuContainer: Phaser.GameObjects.Container; + private menuBg: Phaser.GameObjects.NineSlice; + protected optionSelectText: Phaser.GameObjects.Text; + public bgmBar: BgmBar; + private menuOptions: MenuOptions[]; + protected scale: number = 0.1666666667; + private menuDescriptions: string[]; + + constructor() { + super(Mode.POKEDEX_PAGE); + } + + setup() { + const ui = this.getUi(); + const currentLanguage = i18next.resolvedLanguage ?? "en"; + const langSettingKey = Object.keys(languageSettings).find(lang => currentLanguage.includes(lang)) ?? "en"; + const textSettings = languageSettings[langSettingKey]; + + this.starterSelectContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6); + this.starterSelectContainer.setVisible(false); + ui.add(this.starterSelectContainer); + + const bgColor = globalScene.add.rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6, 0x006860); + bgColor.setOrigin(0, 0); + this.starterSelectContainer.add(bgColor); + + const starterSelectBg = globalScene.add.image(0, 0, "pokedex_summary_bg"); + starterSelectBg.setOrigin(0, 0); + this.starterSelectContainer.add(starterSelectBg); + + this.shinyOverlay = globalScene.add.image(6, 6, "summary_overlay_shiny"); + this.shinyOverlay.setOrigin(0, 0); + this.shinyOverlay.setVisible(false); + this.starterSelectContainer.add(this.shinyOverlay); + + this.pokemonNumberText = addTextObject(17, 1, "0000", TextStyle.SUMMARY); + this.pokemonNumberText.setOrigin(0, 0); + this.starterSelectContainer.add(this.pokemonNumberText); + + this.pokemonNameText = addTextObject(6, 112, "", TextStyle.SUMMARY); + this.pokemonNameText.setOrigin(0, 0); + this.starterSelectContainer.add(this.pokemonNameText); + + this.pokemonGrowthRateLabelText = addTextObject(8, 106, i18next.t("pokedexUiHandler:growthRate"), TextStyle.SUMMARY_ALT, { fontSize: "36px" }); + this.pokemonGrowthRateLabelText.setOrigin(0, 0); + this.pokemonGrowthRateLabelText.setVisible(false); + this.starterSelectContainer.add(this.pokemonGrowthRateLabelText); + + this.pokemonGrowthRateText = addTextObject(34, 106, "", TextStyle.SUMMARY_PINK, { fontSize: "36px" }); + this.pokemonGrowthRateText.setOrigin(0, 0); + this.starterSelectContainer.add(this.pokemonGrowthRateText); + + this.pokemonGenderText = addTextObject(96, 112, "", TextStyle.SUMMARY_ALT); + this.pokemonGenderText.setOrigin(0, 0); + this.starterSelectContainer.add(this.pokemonGenderText); + + this.pokemonUncaughtText = addTextObject(6, 127, i18next.t("pokedexUiHandler:uncaught"), TextStyle.WINDOW, { fontSize: "56px" }); + this.pokemonUncaughtText.setOrigin(0, 0); + this.starterSelectContainer.add(this.pokemonUncaughtText); + + const starterBoxContainer = globalScene.add.container(speciesContainerX + 6, 9); //115 + + for (const species of allSpecies) { + if (!speciesStarterCosts.hasOwnProperty(species.speciesId) || !species.isObtainable()) { + continue; + } + + this.speciesLoaded.set(species.speciesId, false); + this.allSpecies.push(species); + + const starterContainer = new StarterContainer(species).setVisible(false); + this.starterContainers.push(starterContainer); + starterBoxContainer.add(starterContainer); + } + + this.starterSelectContainer.add(starterBoxContainer); + + this.pokemonSprite = globalScene.add.sprite(53, 63, "pkmn__sub"); + this.pokemonSprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true }); + this.starterSelectContainer.add(this.pokemonSprite); + + this.type1Icon = globalScene.add.sprite(8, 98, getLocalizedSpriteKey("types")); + this.type1Icon.setScale(0.5); + this.type1Icon.setOrigin(0, 0); + this.starterSelectContainer.add(this.type1Icon); + + this.type2Icon = globalScene.add.sprite(26, 98, getLocalizedSpriteKey("types")); + this.type2Icon.setScale(0.5); + this.type2Icon.setOrigin(0, 0); + this.starterSelectContainer.add(this.type2Icon); + + this.pokemonLuckLabelText = addTextObject(8, 89, i18next.t("common:luckIndicator"), TextStyle.WINDOW_ALT, { fontSize: "56px" }); + this.pokemonLuckLabelText.setOrigin(0, 0); + this.starterSelectContainer.add(this.pokemonLuckLabelText); + + this.pokemonLuckText = addTextObject(8 + this.pokemonLuckLabelText.displayWidth + 2, 89, "0", TextStyle.WINDOW, { fontSize: "56px" }); + this.pokemonLuckText.setOrigin(0, 0); + this.starterSelectContainer.add(this.pokemonLuckText); + + // Candy icon and count + this.pokemonCandyContainer = globalScene.add.container(4.5, 18); + + this.pokemonCandyIcon = globalScene.add.sprite(0, 0, "candy"); + this.pokemonCandyIcon.setScale(0.5); + this.pokemonCandyIcon.setOrigin(0, 0); + this.pokemonCandyContainer.add(this.pokemonCandyIcon); + + this.pokemonCandyOverlayIcon = globalScene.add.sprite(0, 0, "candy_overlay"); + this.pokemonCandyOverlayIcon.setScale(0.5); + this.pokemonCandyOverlayIcon.setOrigin(0, 0); + this.pokemonCandyContainer.add(this.pokemonCandyOverlayIcon); + + this.pokemonCandyDarknessOverlay = globalScene.add.sprite(0, 0, "candy"); + this.pokemonCandyDarknessOverlay.setScale(0.5); + this.pokemonCandyDarknessOverlay.setOrigin(0, 0); + this.pokemonCandyDarknessOverlay.setTint(0x000000); + this.pokemonCandyDarknessOverlay.setAlpha(0.50); + this.pokemonCandyContainer.add(this.pokemonCandyDarknessOverlay); + + this.pokemonCandyCountText = addTextObject(9.5, 0, "x0", TextStyle.WINDOW_ALT, { fontSize: "56px" }); + this.pokemonCandyCountText.setOrigin(0, 0); + this.pokemonCandyContainer.add(this.pokemonCandyCountText); + + this.pokemonCandyContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 30, 20), Phaser.Geom.Rectangle.Contains); + this.starterSelectContainer.add(this.pokemonCandyContainer); + + this.pokemonFormText = addTextObject(6, 42, "Form", TextStyle.WINDOW_ALT, { fontSize: "42px" }); + this.pokemonFormText.setOrigin(0, 0); + this.starterSelectContainer.add(this.pokemonFormText); + + this.pokemonCaughtHatchedContainer = globalScene.add.container(2, 25); + this.pokemonCaughtHatchedContainer.setScale(0.5); + this.starterSelectContainer.add(this.pokemonCaughtHatchedContainer); + + const pokemonCaughtIcon = globalScene.add.sprite(1, 0, "items", "pb"); + pokemonCaughtIcon.setOrigin(0, 0); + pokemonCaughtIcon.setScale(0.75); + this.pokemonCaughtHatchedContainer.add(pokemonCaughtIcon); + + this.pokemonCaughtCountText = addTextObject(24, 4, "0", TextStyle.SUMMARY_ALT); + this.pokemonCaughtCountText.setOrigin(0, 0); + this.pokemonCaughtHatchedContainer.add(this.pokemonCaughtCountText); + + this.pokemonHatchedIcon = globalScene.add.sprite(1, 14, "egg_icons"); + this.pokemonHatchedIcon.setOrigin(0.15, 0.2); + this.pokemonHatchedIcon.setScale(0.8); + this.pokemonCaughtHatchedContainer.add(this.pokemonHatchedIcon); + + this.pokemonShinyIcon = globalScene.add.sprite(14, 76, "shiny_icons"); + this.pokemonShinyIcon.setOrigin(0.15, 0.2); + this.pokemonShinyIcon.setScale(1); + this.pokemonCaughtHatchedContainer.add(this.pokemonShinyIcon); + + this.pokemonHatchedCountText = addTextObject(24, 19, "0", TextStyle.SUMMARY_ALT); + this.pokemonHatchedCountText.setOrigin(0, 0); + this.pokemonCaughtHatchedContainer.add(this.pokemonHatchedCountText); + + // The font size should be set per language + const instructionTextSize = textSettings.instructionTextSize; + + this.instructionsContainer = globalScene.add.container(4, 128); + this.instructionsContainer.setVisible(true); + this.starterSelectContainer.add(this.instructionsContainer); + + this.candyUpgradeIconElement = new Phaser.GameObjects.Sprite(globalScene, this.instructionRowX, this.instructionRowY, "keyboard", "C.png"); + this.candyUpgradeIconElement.setName("sprite-candyUpgrade-icon-element"); + this.candyUpgradeIconElement.setScale(0.675); + this.candyUpgradeIconElement.setOrigin(0.0, 0.0); + this.candyUpgradeLabel = addTextObject(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY, i18next.t("pokedexUiHandler:candyUpgrade"), TextStyle.PARTY, { fontSize: instructionTextSize }); + this.candyUpgradeLabel.setName("text-candyUpgrade-label"); + + // instruction rows that will be pushed into the container dynamically based on need + // creating new sprites since they will be added to the scene later + this.shinyIconElement = new Phaser.GameObjects.Sprite(globalScene, this.instructionRowX, this.instructionRowY, "keyboard", "R.png"); + this.shinyIconElement.setName("sprite-shiny-icon-element"); + this.shinyIconElement.setScale(0.675); + this.shinyIconElement.setOrigin(0.0, 0.0); + this.shinyLabel = addTextObject(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY, i18next.t("pokedexUiHandler:cycleShiny"), TextStyle.PARTY, { fontSize: instructionTextSize }); + this.shinyLabel.setName("text-shiny-label"); + + this.formIconElement = new Phaser.GameObjects.Sprite(globalScene, this.instructionRowX, this.instructionRowY, "keyboard", "F.png"); + this.formIconElement.setName("sprite-form-icon-element"); + this.formIconElement.setScale(0.675); + this.formIconElement.setOrigin(0.0, 0.0); + this.formLabel = addTextObject(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY, i18next.t("pokedexUiHandler:cycleForm"), TextStyle.PARTY, { fontSize: instructionTextSize }); + this.formLabel.setName("text-form-label"); + + this.genderIconElement = new Phaser.GameObjects.Sprite(globalScene, this.instructionRowX, this.instructionRowY, "keyboard", "G.png"); + this.genderIconElement.setName("sprite-gender-icon-element"); + this.genderIconElement.setScale(0.675); + this.genderIconElement.setOrigin(0.0, 0.0); + this.genderLabel = addTextObject(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY, i18next.t("pokedexUiHandler:cycleGender"), TextStyle.PARTY, { fontSize: instructionTextSize }); + this.genderLabel.setName("text-gender-label"); + + this.variantIconElement = new Phaser.GameObjects.Sprite(globalScene, this.instructionRowX, this.instructionRowY, "keyboard", "V.png"); + this.variantIconElement.setName("sprite-variant-icon-element"); + this.variantIconElement.setScale(0.675); + this.variantIconElement.setOrigin(0.0, 0.0); + this.variantLabel = addTextObject(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY, i18next.t("pokedexUiHandler:cycleVariant"), TextStyle.PARTY, { fontSize: instructionTextSize }); + this.variantLabel.setName("text-variant-label"); + + this.showBackSpriteIconElement = new Phaser.GameObjects.Sprite(globalScene, 50, 7, "keyboard", "E.png"); + this.showBackSpriteIconElement.setName("show-backSprite-icon-element"); + this.showBackSpriteIconElement.setScale(0.675); + this.showBackSpriteIconElement.setOrigin(0.0, 0.0); + this.showBackSpriteLabel = addTextObject(60, 7, i18next.t("pokedexUiHandler:showBackSprite"), TextStyle.PARTY, { fontSize: instructionTextSize }); + this.showBackSpriteLabel.setName("show-backSprite-label"); + this.starterSelectContainer.add(this.showBackSpriteIconElement); + this.starterSelectContainer.add(this.showBackSpriteLabel); + + this.hideInstructions(); + + this.filterInstructionsContainer = globalScene.add.container(50, 5); + this.filterInstructionsContainer.setVisible(true); + this.starterSelectContainer.add(this.filterInstructionsContainer); + + this.starterSelectMessageBoxContainer = globalScene.add.container(0, globalScene.game.canvas.height / 6); + this.starterSelectMessageBoxContainer.setVisible(false); + this.starterSelectContainer.add(this.starterSelectMessageBoxContainer); + + this.starterSelectMessageBox = addWindow(1, -1, 318, 28); + this.starterSelectMessageBox.setOrigin(0, 1); + this.starterSelectMessageBoxContainer.add(this.starterSelectMessageBox); + + this.message = addTextObject(8, 8, "", TextStyle.WINDOW, { maxLines: 2 }); + this.message.setOrigin(0, 0); + this.starterSelectMessageBoxContainer.add(this.message); + + // arrow icon for the message box + this.initPromptSprite(this.starterSelectMessageBoxContainer); + + this.statsContainer = new StatsContainer(6, 16); + + globalScene.add.existing(this.statsContainer); + + this.statsContainer.setVisible(false); + + this.starterSelectContainer.add(this.statsContainer); + + + // Adding menu container + this.menuContainer = globalScene.add.container(-130, 0); + this.menuContainer.setName("menu"); + this.menuContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); + + this.bgmBar = new BgmBar(); + this.bgmBar.setup(); + ui.bgmBar = this.bgmBar; + this.menuContainer.add(this.bgmBar); + this.menuContainer.setVisible(false); + + this.menuOptions = Utils.getEnumKeys(MenuOptions).map(m => parseInt(MenuOptions[m]) as MenuOptions); + + this.optionSelectText = addTextObject(0, 0, this.menuOptions.map(o => `${i18next.t(`pokedexUiHandler:${MenuOptions[o]}`)}`).join("\n"), TextStyle.WINDOW, { maxLines: this.menuOptions.length }); + this.optionSelectText.setLineSpacing(12); + + this.menuDescriptions = [ + i18next.t("pokedexUiHandler:showBaseStats"), + i18next.t("pokedexUiHandler:showAbilities"), + i18next.t("pokedexUiHandler:showLevelMoves"), + i18next.t("pokedexUiHandler:showEggMoves"), + i18next.t("pokedexUiHandler:showTmMoves"), + i18next.t("pokedexUiHandler:showBiomes"), + i18next.t("pokedexUiHandler:showNatures"), + i18next.t("pokedexUiHandler:toggleIVs"), + i18next.t("pokedexUiHandler:showEvolutions") + ]; + + this.scale = getTextStyleOptions(TextStyle.WINDOW, globalScene.uiTheme).scale; + this.menuBg = addWindow( + (globalScene.game.canvas.width / 6) - (this.optionSelectText.displayWidth + 25), + 0, + this.optionSelectText.displayWidth + 19 + 24 * this.scale, + (globalScene.game.canvas.height / 6) - 2 + ); + this.menuBg.setOrigin(0, 0); + + this.optionSelectText.setPositionRelative(this.menuBg, 10 + 24 * this.scale, 6); + + this.menuContainer.add(this.menuBg); + + this.menuContainer.add(this.optionSelectText); + + ui.add(this.menuContainer); + + this.starterSelectContainer.add(this.menuContainer); + + + // adding base stats + this.baseStatsOverlay = new BaseStatsOverlay({ x: 317, y: 0, width:133 }); + this.menuContainer.add(this.baseStatsOverlay); + this.menuContainer.bringToTop(this.baseStatsOverlay); + + // add the info overlay last to be the top most ui element and prevent the IVs from overlaying this + const overlayScale = 1; + this.moveInfoOverlay = new MoveInfoOverlay({ + scale: overlayScale, + top: true, + x: 1, + y: globalScene.game.canvas.height / 6 - MoveInfoOverlay.getHeight(overlayScale) - 29, + }); + this.starterSelectContainer.add(this.moveInfoOverlay); + + this.infoOverlay = new PokedexInfoOverlay({ + scale: overlayScale, + x: 1, + y: globalScene.game.canvas.height / 6 - PokedexInfoOverlay.getHeight(overlayScale) - 29, + }); + this.starterSelectContainer.add(this.infoOverlay); + + // Filter bar sits above everything, except the message box + this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer); + + this.updateInstructions(); + } + + show(args: any[]): boolean { + + if (args.length >= 1 && args[0] === "refresh") { + return false; + } else { + this.species = args[0]; + this.formIndex = args[1] ?? 0; + this.savedStarterAttributes = args[2] ?? { shiny:false, female:true, variant:0, form:0 }; + this.starterSetup(); + } + + this.moveInfoOverlay.clear(); // clear this when removing a menu; the cancel button doesn't seem to trigger this automatically on controllers + this.infoOverlay.clear(); + + super.show(args); + + this.starterSelectContainer.setVisible(true); + this.getUi().bringToTop(this.starterSelectContainer); + + this.starterAttributes = this.initStarterPrefs(); + + this.menuOptions = Utils.getEnumKeys(MenuOptions).map(m => parseInt(MenuOptions[m]) as MenuOptions); + + this.menuContainer.setVisible(true); + + this.speciesStarterDexEntry = this.species ? globalScene.gameData.dexData[this.species.speciesId] : null; + this.setSpecies(); + this.updateInstructions(); + + this.setCursor(0); + + return true; + + } + + starterSetup(): void { + + this.evolutions = []; + this.prevolutions = []; + this.battleForms = []; + + const species = this.species; + const formIndex = this.formIndex ?? 0; + + const allEvolutions = pokemonEvolutions.hasOwnProperty(species.speciesId) ? pokemonEvolutions[species.speciesId] : []; + + if (species.forms.length > 0) { + const form = species.forms[formIndex]; + + // If this form has a specific set of moves, we get them. + this.levelMoves = (formIndex > 0 && pokemonFormLevelMoves.hasOwnProperty(formIndex)) ? pokemonFormLevelMoves[species.speciesId][formIndex] : pokemonSpeciesLevelMoves[species.speciesId]; + this.ability1 = form.ability1; + this.ability2 = (form.ability2 === form.ability1) ? undefined : form.ability2; + this.abilityHidden = (form.abilityHidden === form.ability1) ? undefined : form.abilityHidden; + + this.evolutions = allEvolutions.filter(e => (e.preFormKey === form.formKey || e.preFormKey === null)); + this.baseStats = form.baseStats; + this.baseTotal = form.baseTotal; + + } else { + this.levelMoves = pokemonSpeciesLevelMoves[species.speciesId]; + this.ability1 = species.ability1; + this.ability2 = (species.ability2 === species.ability1) ? undefined : species.ability2; + this.abilityHidden = (species.abilityHidden === species.ability1) ? undefined : species.abilityHidden; + + this.evolutions = allEvolutions; + this.baseStats = species.baseStats; + this.baseTotal = species.baseTotal; + } + + this.eggMoves = speciesEggMoves[this.getStarterSpeciesId(species.speciesId)] ?? []; + this.hasEggMoves = Array.from({ length: 4 }, (_, em) => (globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)].eggMoves & (1 << em)) !== 0); + + this.tmMoves = (speciesTmMoves[species.speciesId] ?? []).sort((a, b) => allMoves[a].name > allMoves[b].name ? 1 : -1); + + this.passive = starterPassiveAbilities[this.getStarterSpeciesId(species.speciesId)]; + + const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)]; + const abilityAttr = starterData.abilityAttr; + this.hasPassive = starterData.passiveAttr > 0; + + const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1; + const hasAbility2 = abilityAttr & AbilityAttr.ABILITY_2; + const hasHiddenAbility = abilityAttr & AbilityAttr.ABILITY_HIDDEN; + + this.hasAbilities = [ + hasAbility1, + hasAbility2, + hasHiddenAbility + ]; + + const allBiomes = catchableSpecies[species.speciesId] ?? []; + this.preBiomes = this.sanitizeBiomes( + (catchableSpecies[this.getStarterSpeciesId(species.speciesId)] ?? []) + .filter(b => !allBiomes.some(bm => (b.biome === bm.biome && b.tier === bm.tier)) && !(b.biome === Biome.TOWN)), + this.getStarterSpeciesId(species.speciesId)); + this.biomes = this.sanitizeBiomes(allBiomes, species.speciesId); + + const allFormChanges = pokemonFormChanges.hasOwnProperty(species.speciesId) ? pokemonFormChanges[species.speciesId] : []; + this.battleForms = allFormChanges.filter(f => (f.preFormKey === this.species.forms[this.formIndex].formKey)); + + const preSpecies = pokemonPrevolutions.hasOwnProperty(this.species.speciesId) ? allSpecies.find(sp => sp.speciesId === pokemonPrevolutions[this.species.speciesId]) : null; + if (preSpecies) { + const preEvolutions = pokemonEvolutions.hasOwnProperty(preSpecies.speciesId) ? pokemonEvolutions[preSpecies.speciesId] : []; + this.prevolutions = preEvolutions.filter( + e => e.speciesId === species.speciesId && ( + ( + (e.evoFormKey === "" || e.evoFormKey === null) && + ( + // This takes care of Cosplay Pikachu (Pichu is not shown) + (preSpecies.forms.some(form => form.formKey === species.forms[formIndex]?.formKey)) || + // This takes care of Gholdengo + (preSpecies.forms.length > 0 && species.forms.length === 0) || + // This takes care of everything else + (preSpecies.forms.length === 0 && (species.forms.length === 0 || species.forms[formIndex]?.formKey === "")) + ) + ) + // This takes care of Burmy, Shellos etc + || e.evoFormKey === species.forms[formIndex]?.formKey + ) + ); + } + } + + // Function to ensure that forms appear in the appropriate biome and tod + sanitizeBiomes(biomes: BiomeTierTod[], speciesId: number): BiomeTierTod[] { + + if (speciesId === Species.BURMY || speciesId === Species.WORMADAM) { + return biomes.filter(b => { + const formIndex = (() => { + switch (b.biome) { + case Biome.BEACH: + return 1; + case Biome.SLUM: + return 2; + default: + return 0; + } + })(); + return this.formIndex === formIndex; + }); + + } else if (speciesId === Species.ROTOM) { + return biomes.filter(b => { + const formIndex = (() => { + switch (b.biome) { + case Biome.VOLCANO: + return 1; + case Biome.SEA: + return 2; + case Biome.ICE_CAVE: + return 3; + case Biome.MOUNTAIN: + return 4; + case Biome.TALL_GRASS: + return 5; + default: + return 0; + } + })(); + return this.formIndex === formIndex; + }); + + } else if (speciesId === Species.LYCANROC) { + return biomes.filter(b => { + const formIndex = (() => { + switch (b.tod[0]) { + case TimeOfDay.DAY: + case TimeOfDay.DAWN: + return 0; + case TimeOfDay.DUSK: + return 2; + case TimeOfDay.NIGHT: + return 1; + default: + return 0; + } + })(); + return this.formIndex === formIndex; + }); + } + + return biomes; + } + + isCaught(otherSpeciesDexEntry?: DexEntry): bigint { + if (globalScene.dexForDevs) { + return 255n; + } + + const dexEntry = otherSpeciesDexEntry ? otherSpeciesDexEntry : this.speciesStarterDexEntry; + + return dexEntry?.caughtAttr ?? 0n; + } + /** + * Check whether a given form is caught for a given species. + * All forms that can be reached through a form change during battle are considered caught and show up in the dex as such. + * + * @param otherSpecies The species to check; defaults to current species + * @param otherFormIndex The form index of the form to check; defaults to current form + * @returns StarterAttributes for the species + */ + isFormCaught(otherSpecies?: PokemonSpecies, otherFormIndex?: integer | undefined): boolean { + + if (globalScene.dexForDevs) { + return true; + } + const species = otherSpecies ? otherSpecies : this.species; + const formIndex = otherFormIndex !== undefined ? otherFormIndex : this.formIndex; + const dexEntry = globalScene.gameData.dexData[species.speciesId]; + + const isFormCaught = dexEntry ? + (dexEntry.caughtAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n + : false; + return isFormCaught; + } + + /** + * Get the starter attributes for the given PokemonSpecies, after sanitizing them. + * If somehow a preference is set for a form, variant, gender, ability or nature + * that wasn't actually unlocked or is invalid it will be cleared here + * + * @param species The species to get Starter Preferences for + * @returns StarterAttributes for the species + */ + initStarterPrefs(): StarterAttributes { + const starterAttributes : StarterAttributes | null = this.species ? { ...this.savedStarterAttributes } : null; + const dexEntry = globalScene.gameData.dexData[this.species.speciesId]; + const caughtAttr = this.isCaught(dexEntry); + + // no preferences or Pokemon wasn't caught, return empty attribute + if (!starterAttributes || !caughtAttr) { + return {}; + } + + const hasShiny = caughtAttr & DexAttr.SHINY; + const hasNonShiny = caughtAttr & DexAttr.NON_SHINY; + if (starterAttributes.shiny && !hasShiny) { + // shiny form wasn't unlocked, purging shiny and variant setting + starterAttributes.shiny = false; + starterAttributes.variant = 0; + } else if (starterAttributes.shiny === false && !hasNonShiny) { + // non shiny form wasn't unlocked, purging shiny setting + starterAttributes.shiny = false; + } + + if (starterAttributes.variant !== undefined) { + const unlockedVariants = [ + hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT, + hasShiny && caughtAttr & DexAttr.VARIANT_2, + hasShiny && caughtAttr & DexAttr.VARIANT_3 + ]; + if (isNaN(starterAttributes.variant) || starterAttributes.variant < 0) { + starterAttributes.variant = 0; + } else if (!unlockedVariants[starterAttributes.variant]) { + let highestValidIndex = -1; + for (let i = 0; i <= starterAttributes.variant && i < unlockedVariants.length; i++) { + if (unlockedVariants[i] !== 0n) { + highestValidIndex = i; + } + } + // Set to the highest valid index found or default to 0 + starterAttributes.variant = highestValidIndex !== -1 ? highestValidIndex : 0; + } + } + + if (starterAttributes.female !== undefined) { + if ((starterAttributes.female && !(caughtAttr & DexAttr.FEMALE)) || (!starterAttributes.female && !(caughtAttr & DexAttr.MALE))) { + starterAttributes.female = !starterAttributes.female; + } + } + + return starterAttributes; + } + + showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer, moveToTop?: boolean) { + super.showText(text, delay, callback, callbackDelay, prompt, promptDelay); + + const singleLine = text?.indexOf("\n") === -1; + + this.starterSelectMessageBox.setSize(318, singleLine ? 28 : 42); + + if (moveToTop) { + this.starterSelectMessageBox.setOrigin(0, 0); + this.starterSelectMessageBoxContainer.setY(0); + this.message.setY(4); + } else { + this.starterSelectMessageBoxContainer.setY(globalScene.game.canvas.height / 6); + this.starterSelectMessageBox.setOrigin(0, 1); + this.message.setY(singleLine ? -22 : -37); + } + + this.starterSelectMessageBoxContainer.setVisible(!!text?.length); + } + + /** + * Determines if 'Icon' based upgrade notifications should be shown + * @returns true if upgrade notifications are enabled and set to display an 'Icon' + */ + isUpgradeIconEnabled(): boolean { + return globalScene.candyUpgradeNotification !== 0 && globalScene.candyUpgradeDisplay === 0; + } + /** + * Determines if 'Animation' based upgrade notifications should be shown + * @returns true if upgrade notifications are enabled and set to display an 'Animation' + */ + isUpgradeAnimationEnabled(): boolean { + return globalScene.candyUpgradeNotification !== 0 && globalScene.candyUpgradeDisplay === 1; + } + + /** + * If the pokemon is an evolution, find speciesId of its starter. + * @param speciesId the id of the species to check + * @returns the id of the corresponding starter + */ + getStarterSpeciesId(speciesId): number { + if (globalScene.gameData.starterData.hasOwnProperty(speciesId)) { + return speciesId; + } else { + return pokemonStarters[speciesId]; + } + } + + getStarterSpecies(species): PokemonSpecies { + if (globalScene.gameData.starterData.hasOwnProperty(species.speciesId)) { + return species; + } else { + return allSpecies.find(sp => sp.speciesId === pokemonStarters[species.speciesId]) ?? species; + } + } + + /** + * Assign a form string to a given species and form + * @param formKey the form to format + * @param species the species to format + * @param speciesId whether the name of the species should be shown at the end + * @returns the formatted string + */ + getFormString(formKey: string, species: PokemonSpecies, append: boolean = false): string { + let label: string; + const formText = capitalizeString(formKey, "-", false, false) ?? ""; + const speciesName = capitalizeString(this.getStarterSpecies(species).name, "_", true, false) ?? ""; + if (species.speciesId === Species.ARCEUS) { + label = i18next.t(`pokemonInfo:Type.${formText?.toUpperCase()}`); + return label; + } + label = formText ? i18next.t(`pokemonForm:${speciesName}${formText}`) : ""; + if (label === `${speciesName}${formText}`) { + label = i18next.t(`battlePokemonForm:${formKey}`, { pokemonName:species.name }); + } else { + // If the label is only the form, we can append the name of the pokemon + label += append ? ` ${species.name}` : ""; + } + return label; + } + + /** + * Find the name of the region for regional species + * @param species the species to check + * @returns a string with the region name + */ + getRegionName(species: PokemonSpecies): string { + const name = species.name; + const label = Species[species.speciesId]; + const suffix = label.includes("_") ? " (" + label.split("_")[0].toLowerCase() + ")" : ""; + return name + suffix; + } + + processInput(button: Button): boolean { + if (this.blockInput) { + return false; + } + + const ui = this.getUi(); + + let success = false; + let error = false; + + const isCaught = this.isCaught(); + const isFormCaught = this.isFormCaught(); + + if (this.blockInputOverlay) { + if (button === Button.CANCEL || button === Button.ACTION) { + this.blockInputOverlay = false; + this.baseStatsOverlay.clear(); + ui.showText(""); + return true; + } else if (button === Button.UP || button === Button.DOWN) { + this.blockInputOverlay = false; + this.baseStatsOverlay.clear(); + ui.showText(""); + } else { + return false; + } + } + + if (button === Button.SUBMIT) { + success = true; + } else if (button === Button.CANCEL) { + if (this.statsMode) { + this.toggleStatsMode(false); + success = true; + } else { + this.getUi().revertMode(); + success = true; + } + } else { + + const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(this.species.speciesId)]; + // prepare persistent starter data to store changes + const starterAttributes = this.starterAttributes; + + if (button === Button.ACTION) { + + switch (this.cursor) { + + case MenuOptions.BASE_STATS: + + if (!isCaught || !isFormCaught) { + error = true; + } else { + + this.blockInput = true; + + ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => { + ui.showText(i18next.t("pokedexUiHandler:showBaseStats"), null, () => { + + this.baseStatsOverlay.show(this.baseStats, this.baseTotal); + + this.blockInput = false; + this.blockInputOverlay = true; + + return true; + }); + success = true; + }); + } + break; + + case MenuOptions.LEVEL_MOVES: + + if (!isCaught || !isFormCaught) { + error = true; + } else { + + this.blockInput = true; + + ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => { + ui.showText(i18next.t("pokedexUiHandler:showLevelMoves"), null, () => { + + this.moveInfoOverlay.show(allMoves[this.levelMoves[0][1]]); + + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: this.levelMoves.map(m => { + const option: OptionSelectItem = { + label: String(m[0]).padEnd(4, " ") + allMoves[m[1]].name, + handler: () => { + return false; + }, + onHover: () => { + this.moveInfoOverlay.show(allMoves[m[1]]); + }, + }; + return option; + }).concat({ + label: i18next.t("menu:cancel"), + handler: () => { + this.moveInfoOverlay.clear(); + this.clearText(); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + return true; + }, + onHover: () => { + this.moveInfoOverlay.clear(); + }, + }), + supportHover: true, + maxOptions: 8, + yOffset: 19 + }); + + this.blockInput = false; + }); + }); + success = true; + } + break; + + case MenuOptions.EGG_MOVES: + + + if (!isCaught || !isFormCaught) { + error = true; + } else { + + this.blockInput = true; + + ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => { + + if (this.eggMoves.length === 0) { + ui.showText(i18next.t("pokedexUiHandler:noEggMoves")); + this.blockInput = false; + return true; + } + + ui.showText(i18next.t("pokedexUiHandler:showEggMoves"), null, () => { + + this.moveInfoOverlay.show(allMoves[this.eggMoves[0]]); + + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: [ + { + label: i18next.t("pokedexUiHandler:common"), + skip: true, + style: TextStyle.MONEY_WINDOW, + handler: () => false, // Non-selectable, but handler is required + onHover: () => this.moveInfoOverlay.clear() // No hover behavior for titles + }, + ...this.eggMoves.slice(0, 3).map((m, i) => ({ + label: allMoves[m].name, + style: this.hasEggMoves[i] ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT, + handler: () => false, + onHover: () => this.moveInfoOverlay.show(allMoves[m]) + })), + { + label: i18next.t("pokedexUiHandler:rare"), + skip: true, + style: TextStyle.MONEY_WINDOW, + handler: () => false, + onHover: () => this.moveInfoOverlay.clear() + }, + { + label: allMoves[this.eggMoves[3]].name, + style: this.hasEggMoves[3] ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT, + handler: () => false, + onHover: () => this.moveInfoOverlay.show(allMoves[this.eggMoves[3]]) + }, + { + label: i18next.t("menu:cancel"), + handler: () => { + this.moveInfoOverlay.clear(); + this.clearText(); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + return true; + }, + onHover: () => this.moveInfoOverlay.clear() + } + ], + supportHover: true, + maxOptions: 8, + yOffset: 19 + }); + + this.blockInput = false; + }); + }); + success = true; + } + break; + + case MenuOptions.TM_MOVES: + + if (!isCaught || !isFormCaught) { + error = true; + } else { + this.blockInput = true; + + ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => { + ui.showText(i18next.t("pokedexUiHandler:showTmMoves"), null, () => { + + this.moveInfoOverlay.show(allMoves[this.tmMoves[0]]); + + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: this.tmMoves.map(m => { + const option: OptionSelectItem = { + label: allMoves[m].name, + handler: () => { + return false; + }, + onHover: () => { + this.moveInfoOverlay.show(allMoves[m]); + }, + }; + return option; + }).concat({ + label: i18next.t("menu:cancel"), + handler: () => { + this.moveInfoOverlay.clear(); + this.clearText(); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + return true; + }, + onHover: () => { + this.moveInfoOverlay.clear(); + }, + }), + supportHover: true, + maxOptions: 8, + yOffset: 19 + }); + + this.blockInput = false; + }); + }); + success = true; + } + break; + + case MenuOptions.ABILITIES: + + if (!isCaught || !isFormCaught) { + error = true; + } else { + + this.blockInput = true; + + ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => { + + ui.showText(i18next.t("pokedexUiHandler:showAbilities"), null, () => { + + this.infoOverlay.show(allAbilities[this.ability1].description); + + const options: any[] = []; + + if (this.ability1) { + options.push({ + label: allAbilities[this.ability1].name, + style: this.hasAbilities[0] > 0 ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT, + handler: () => false, + onHover: () => this.infoOverlay.show(allAbilities[this.ability1].description) + }); + } + if (this.ability2) { + const ability = allAbilities[this.ability2]; + options.push({ + label: ability?.name, + style: this.hasAbilities[1] > 0 ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT, + handler: () => false, + onHover: () => this.infoOverlay.show(ability?.description) + }); + } + + if (this.abilityHidden) { + options.push({ + label: i18next.t("pokedexUiHandler:hidden"), + skip: true, + style: TextStyle.MONEY_WINDOW, + handler: () => false, + onHover: () => this.infoOverlay.clear() + }); + const ability = allAbilities[this.abilityHidden]; + options.push({ + label: allAbilities[this.abilityHidden].name, + style: this.hasAbilities[2] > 0 ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT, + handler: () => false, + onHover: () => this.infoOverlay.show(ability?.description) + }); + } + + if (this.passive) { + options.push({ + label: i18next.t("pokedexUiHandler:passive"), + skip: true, + style: TextStyle.MONEY_WINDOW, + handler: () => false, + onHover: () => this.infoOverlay.clear() + }); + options.push({ + label: allAbilities[this.passive].name, + style: this.hasPassive ? TextStyle.SETTINGS_VALUE : TextStyle.SHADOW_TEXT, + handler: () => false, + onHover: () => this.infoOverlay.show(allAbilities[this.passive].description) + }); + } + + options.push({ + label: i18next.t("menu:cancel"), + handler: () => { + this.infoOverlay.clear(); + this.clearText(); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + return true; + }, + onHover: () => this.infoOverlay.clear() + }); + + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: options, + supportHover: true, + maxOptions: 8, + yOffset: 19 + }); + + this.blockInput = false; + }); + }); + success = true; + } + break; + + case MenuOptions.BIOMES: + + if (!(this.isCaught() || this.speciesStarterDexEntry?.seenAttr)) { + error = true; + } else { + this.blockInput = true; + + ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => { + + if ((!this.biomes || this.biomes?.length === 0) && + (!this.preBiomes || this.preBiomes?.length === 0)) { + ui.showText(i18next.t("pokedexUiHandler:noBiomes")); + ui.playError(); + this.blockInput = false; + return true; + } + + const options: any[] = []; + + ui.showText(i18next.t("pokedexUiHandler:showBiomes"), null, () => { + + this.biomes.map(b => { + options.push({ + label: i18next.t(`biome:${Biome[b.biome].toUpperCase()}`) + " - " + + i18next.t(`biome:${BiomePoolTier[b.tier].toUpperCase()}`) + + ( b.tod.length === 1 && b.tod[0] === -1 ? "" : " (" + b.tod.map(tod => i18next.t(`biome:${TimeOfDay[tod].toUpperCase()}`)).join(", ") + ")"), + handler: () => false + }); + }); + + + if (this.preBiomes.length > 0) { + options.push({ + label: i18next.t("pokedexUiHandler:preBiomes"), + skip: true, + handler: () => false + }); + this.preBiomes.map(b => { + options.push({ + label: i18next.t(`biome:${Biome[b.biome].toUpperCase()}`) + " - " + + i18next.t(`biome:${BiomePoolTier[b.tier].toUpperCase()}`) + + ( b.tod.length === 1 && b.tod[0] === -1 ? "" : " (" + b.tod.map(tod => i18next.t(`biome:${TimeOfDay[tod].toUpperCase()}`)).join(", ") + ")"), + handler: () => false + }); + }); + } + + options.push({ + label: i18next.t("menu:cancel"), + handler: () => { + this.moveInfoOverlay.clear(); + this.clearText(); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + return true; + }, + onHover: () => this.moveInfoOverlay.clear() + }); + + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: options, + supportHover: true, + maxOptions: 8, + yOffset: 19 + }); + + this.blockInput = false; + }); + }); + success = true; + } + break; + + case MenuOptions.EVOLUTIONS: + + if (!isCaught || !isFormCaught) { + error = true; + } else { + + this.blockInput = true; + + ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => { + + const options: any[] = []; + + if ((!this.prevolutions || this.prevolutions?.length === 0) && + (!this.evolutions || this.evolutions?.length === 0) && + (!this.battleForms || this.battleForms?.length === 0)) { + ui.showText(i18next.t("pokedexUiHandler:noEvolutions")); + ui.playError(); + this.blockInput = false; + return true; + } + + ui.showText(i18next.t("pokedexUiHandler:showEvolutions"), null, () => { + + if (this.prevolutions?.length > 0) { + options.push({ + label: i18next.t("pokedexUiHandler:prevolutions"), + style: TextStyle.MONEY_WINDOW, + skip: true, + handler: () => false + }); + this.prevolutions.map(pre => { + const preSpecies = allSpecies.find(species => species.speciesId === pokemonPrevolutions[this.species.speciesId]); + + const conditionText: string = pre.description; + + options.push({ + label: pre.preFormKey ? + this.getFormString(pre.preFormKey, preSpecies ?? this.species, true) : + this.getRegionName(preSpecies ?? this.species), + handler: () => { + const newSpecies = allSpecies.find(species => species.speciesId === pokemonPrevolutions[pre.speciesId]); + // Attempts to find the formIndex of the evolved species + const newFormKey = pre.preFormKey ? pre.preFormKey : (this.species.forms.length > 0 ? this.species.forms[this.formIndex].formKey : ""); + const matchingForm = newSpecies?.forms.find(form => form.formKey === newFormKey); + const newFormIndex = matchingForm ? matchingForm.formIndex : 0; + this.starterAttributes.form = newFormIndex; + this.savedStarterAttributes.form = newFormIndex; + this.moveInfoOverlay.clear(); + this.clearText(); + ui.setMode(Mode.POKEDEX_PAGE, newSpecies, newFormIndex, this.savedStarterAttributes); + return true; + }, + onHover: () => this.showText(conditionText) + }); + }); + } + + if (this.evolutions.length > 0) { + options.push({ + label: i18next.t("pokedexUiHandler:evolutions"), + style: TextStyle.MONEY_WINDOW, + skip: true, + handler: () => false + }); + this.evolutions.map(evo => { + const evoSpecies = allSpecies.find(species => species.speciesId === evo.speciesId); + const evoSpeciesStarterDexEntry = evoSpecies ? globalScene.gameData.dexData[evoSpecies.speciesId] : undefined; + const isCaughtEvo = this.isCaught(evoSpeciesStarterDexEntry) ? true : false; + // Attempts to find the formIndex of the evolved species + const newFormKey = evo.evoFormKey ? evo.evoFormKey : (this.species.forms.length > 0 ? this.species.forms[this.formIndex].formKey : ""); + const matchingForm = evoSpecies?.forms.find(form => form.formKey === newFormKey); + const newFormIndex = matchingForm ? matchingForm.formIndex : 0; + const isFormCaughtEvo = this.isFormCaught(evoSpecies, newFormIndex); + + const conditionText: string = evo.description; + + options.push({ + label: evo.evoFormKey ? + this.getFormString(evo.evoFormKey, evoSpecies ?? this.species, true) : + this.getRegionName(evoSpecies ?? this.species), + style: isCaughtEvo && isFormCaughtEvo ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, + handler: () => { + this.starterAttributes.form = newFormIndex; + this.savedStarterAttributes.form = newFormIndex; + this.moveInfoOverlay.clear(); + this.clearText(); + ui.setMode(Mode.POKEDEX_PAGE, evoSpecies, newFormIndex, this.savedStarterAttributes); + return true; + }, + onHover: () => this.showText(conditionText) + }); + }); + } + + if (this.battleForms.length > 0) { + options.push({ + label: i18next.t("pokedexUiHandler:forms"), + style: TextStyle.MONEY_WINDOW, + skip: true, + handler: () => false + }); + this.battleForms.map(bf => { + + let conditionText:string = ""; + if (bf.trigger) { + conditionText = bf.trigger.description; + } else { + conditionText = ""; + } + let label: string = this.getFormString(bf.formKey, this.species); + if (label === "") { + label = this.species.name; + } + const matchingForm = this.species?.forms.find(form => form.formKey === bf.formKey); + const newFormIndex = matchingForm ? matchingForm.formIndex : 0; + const isFormCaught = this.isFormCaught(this.species, newFormIndex); + + if (conditionText) { + options.push({ + label: label, + style: isFormCaught ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, + handler: () => { + const newSpecies = this.species; + const newFormIndex = this.species.forms.find(f => f.formKey === bf.formKey)?.formIndex; + this.starterAttributes.form = newFormIndex; + this.savedStarterAttributes.form = newFormIndex; + this.moveInfoOverlay.clear(); + this.clearText(); + ui.setMode(Mode.POKEDEX_PAGE, newSpecies, newFormIndex, this.savedStarterAttributes); + return true; + }, + onHover: () => this.showText(conditionText) + }); + } + }); + } + + options.push({ + label: i18next.t("menu:cancel"), + handler: () => { + this.moveInfoOverlay.clear(); + this.clearText(); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + return true; + }, + onHover: () => this.moveInfoOverlay.clear() + }); + + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: options, + supportHover: true, + maxOptions: 8, + yOffset: 19 + }); + + this.blockInput = false; + }); + }); + success = true; + } + break; + + case MenuOptions.TOGGLE_IVS: + + if (!isCaught || !isFormCaught) { + error = true; + } else { + this.toggleStatsMode(); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + success = true; + } + break; + + case MenuOptions.NATURES: + + if (!isCaught || !isFormCaught) { + error = true; + } else { + this.blockInput = true; + ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => { + ui.showText(i18next.t("pokedexUiHandler:showNature"), null, () => { + const natures = globalScene.gameData.getNaturesForAttr(this.speciesStarterDexEntry?.natureAttr); + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: natures.map((n: Nature, i: number) => { + const option: OptionSelectItem = { + label: getNatureName(n, true, true, true, globalScene.uiTheme), + handler: () => { + return false; + } + }; + return option; + }).concat({ + label: i18next.t("menu:cancel"), + handler: () => { + this.clearText(); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + this.blockInput = false; + return true; + } + }), + maxOptions: 8, + yOffset: 19 + }); + }); + }); + success = true; + } + break; + } + + } else { + const props = globalScene.gameData.getSpeciesDexAttrProps(this.species, this.getCurrentDexProps(this.species.speciesId)); + switch (button) { + case Button.CYCLE_SHINY: + if (this.canCycleShiny) { + + if (!starterAttributes.shiny) { + // Change to shiny, we need to get the proper default variant + const newVariant = starterAttributes.variant ? starterAttributes.variant as Variant : 0; + this.setSpeciesDetails(this.species, { shiny: true, variant: newVariant }); + + globalScene.playSound("se/sparkle"); + // Set the variant label to the shiny tint + const tint = getVariantTint(newVariant); + this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant)); + this.pokemonShinyIcon.setTint(tint); + this.pokemonShinyIcon.setVisible(true); + + starterAttributes.shiny = true; + } else { + let newVariant = props.variant; + do { + newVariant = (newVariant + 1) % 3; + if (newVariant === 0) { + if (this.isCaught() & DexAttr.DEFAULT_VARIANT) { // TODO: is this bang correct? + break; + } + } else if (newVariant === 1) { + if (this.isCaught() & DexAttr.VARIANT_2) { // TODO: is this bang correct? + break; + } + } else { + if (this.isCaught() & DexAttr.VARIANT_3) { // TODO: is this bang correct? + break; + } + } + } while (newVariant !== props.variant); + + starterAttributes.variant = newVariant; // store the selected variant + this.savedStarterAttributes.variant = starterAttributes.variant; + if (newVariant > props.variant) { + this.setSpeciesDetails(this.species, { variant: newVariant as Variant }); + // Cycle tint based on current sprite tint + const tint = getVariantTint(newVariant as Variant); + this.pokemonShinyIcon.setFrame(getVariantIcon(newVariant as Variant)); + this.pokemonShinyIcon.setTint(tint); + success = true; + } else { + this.setSpeciesDetails(this.species, { shiny: false, variant: 0 }); + this.pokemonShinyIcon.setVisible(false); + success = true; + + starterAttributes.shiny = false; + this.savedStarterAttributes.shiny = starterAttributes.shiny; + } + } + } + break; + case Button.CYCLE_FORM: + if (this.canCycleForm) { + const formCount = this.species.forms.length; + let newFormIndex = this.formIndex; + do { + newFormIndex = (newFormIndex + 1) % formCount; + if (this.species.forms[newFormIndex].isStarterSelectable || globalScene.dexForDevs) { // TODO: are those bangs correct? + break; + } + } while (newFormIndex !== props.formIndex); + // TODO: Is this still needed? + starterAttributes.form = newFormIndex; // store the selected form + this.savedStarterAttributes.form = starterAttributes.form; + this.formIndex = newFormIndex; + this.starterSetup(); + this.setSpeciesDetails(this.species, { formIndex: newFormIndex }); + success = this.setCursor(this.cursor); + } + break; + case Button.CYCLE_GENDER: + if (this.canCycleGender) { + starterAttributes.female = !props.female; + this.savedStarterAttributes.female = starterAttributes.female; + this.setSpeciesDetails(this.species, { female: !props.female }); + success = true; + } + break; + case Button.STATS: + if (!isCaught || !isFormCaught) { + error = true; + } else { // checks to see if the party has 6 or fewer pokemon + const ui = this.getUi(); + const options: any[] = []; // TODO: add proper type + + const passiveAttr = starterData.passiveAttr; + const candyCount = starterData.candyCount; + + if (!pokemonPrevolutions.hasOwnProperty(this.species.speciesId)) { + if (!(passiveAttr & PassiveAttr.UNLOCKED)) { + const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)]); + options.push({ + label: `x${passiveCost} ${i18next.t("pokedexUiHandler:unlockPassive")} (${allAbilities[starterPassiveAbilities[this.getStarterSpeciesId(this.species.speciesId)]].name})`, + handler: () => { + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= passiveCost) { + starterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED; + if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { + starterData.candyCount -= passiveCost; + } + this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); + globalScene.gameData.saveSystem().then(success => { + if (!success) { + return globalScene.reset(true); + } + }); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + this.setSpeciesDetails(this.species); + globalScene.playSound("se/buy"); + + return true; + } + return false; + }, + item: "candy", + itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)] + }); + } + + // Reduce cost option + const valueReduction = starterData.valueReduction; + if (valueReduction < valueReductionMax) { + const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)])[valueReduction]; + options.push({ + label: `x${reductionCost} ${i18next.t("pokedexUiHandler:reduceCost")}`, + handler: () => { + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= reductionCost) { + starterData.valueReduction++; + if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { + starterData.candyCount -= reductionCost; + } + this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); + globalScene.gameData.saveSystem().then(success => { + if (!success) { + return globalScene.reset(true); + } + }); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + globalScene.playSound("se/buy"); + + return true; + } + return false; + }, + item: "candy", + itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)] + }); + } + + // Same species egg menu option. + const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)]); + options.push({ + label: `x${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`, + handler: () => { + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost) { + if (globalScene.gameData.eggs.length >= 99 && !Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) { + // Egg list full, show error message at the top of the screen and abort + this.showText(i18next.t("egg:tooManyEggs"), undefined, () => this.showText("", 0, () => this.tutorialActive = false), 2000, false, undefined, true); + return false; + } + if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { + starterData.candyCount -= sameSpeciesEggCost; + } + this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); + + const egg = new Egg({ scene: globalScene, species: this.species.speciesId, sourceType: EggSourceType.SAME_SPECIES_EGG }); + egg.addEggToGameData(); + + globalScene.gameData.saveSystem().then(success => { + if (!success) { + return globalScene.reset(true); + } + }); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + globalScene.playSound("se/buy"); + + return true; + } + return false; + }, + item: "candy", + itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)] + }); + options.push({ + label: i18next.t("menu:cancel"), + handler: () => { + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + return true; + } + }); + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: options, + yOffset: 47 + }); + success = true; + } else { + error = true; + } + } + break; + case Button.CYCLE_ABILITY: + this.showBackSprite = !this.showBackSprite; + if (this.showBackSprite) { + this.showBackSpriteLabel.setText(i18next.t("pokedexUiHandler:showFrontSprite")); + } else { + this.showBackSpriteLabel.setText(i18next.t("pokedexUiHandler:showBackSprite")); + } + this.setSpeciesDetails(this.species, {}, true); + success = true; + break; + case Button.UP: + if (this.cursor) { + success = this.setCursor(this.cursor - 1); + } else { + success = this.setCursor(this.menuOptions.length - 1); + } + break; + case Button.DOWN: + if (this.cursor + 1 < this.menuOptions.length) { + success = this.setCursor(this.cursor + 1); + } else { + success = this.setCursor(0); + } + break; + case Button.LEFT: + this.blockInput = true; + ui.setModeWithoutClear(Mode.OPTION_SELECT).then(() => { + const index = allSpecies.findIndex(species => species.speciesId === this.species.speciesId); + const newIndex = index <= 0 ? allSpecies.length - 1 : index - 1; + const newSpecies = allSpecies[newIndex]; + // Attempts to find the formIndex of the evolved species + const matchingForm = newSpecies?.forms.find(form => form.formKey === this.species?.forms[this.formIndex]?.formKey); + const newFormIndex = matchingForm ? matchingForm.formIndex : 0; + this.starterAttributes.form = newFormIndex; + this.savedStarterAttributes.form = newFormIndex; + this.moveInfoOverlay.clear(); + this.clearText(); + ui.setModeForceTransition(Mode.POKEDEX_PAGE, newSpecies, newFormIndex, this.savedStarterAttributes); + }); + this.blockInput = false; + break; + case Button.RIGHT: + ui.setModeWithoutClear(Mode.OPTION_SELECT).then(() => { + const index = allSpecies.findIndex(species => species.speciesId === this.species.speciesId); + const newIndex = index >= allSpecies.length - 1 ? 0 : index + 1; + const newSpecies = allSpecies[newIndex]; + // Attempts to find the formIndex of the evolved species + const matchingForm = newSpecies?.forms.find(form => form.formKey === this.species?.forms[this.formIndex]?.formKey); + const newFormIndex = matchingForm ? matchingForm.formIndex : 0; + this.starterAttributes.form = newFormIndex; + this.savedStarterAttributes.form = newFormIndex; + this.moveInfoOverlay.clear(); + this.clearText(); + ui.setModeForceTransition(Mode.POKEDEX_PAGE, newSpecies, newFormIndex, this.savedStarterAttributes); + }); + break; + } + } + } + + if (success) { + ui.playSelect(); + } else if (error) { + ui.playError(); + } + + return success || error; + } + + updateButtonIcon(iconSetting, gamepadType, iconElement, controlLabel): void { + let iconPath; + // touch controls cannot be rebound as is, and are just emulating a keyboard event. + // Additionally, since keyboard controls can be rebound (and will be displayed when they are), we need to have special handling for the touch controls + if (gamepadType === "touch") { + gamepadType = "keyboard"; + switch (iconSetting) { + case SettingKeyboard.Button_Cycle_Shiny: + iconPath = "R.png"; + break; + case SettingKeyboard.Button_Cycle_Form: + iconPath = "F.png"; + break; + case SettingKeyboard.Button_Cycle_Gender: + iconPath = "G.png"; + break; + case SettingKeyboard.Button_Cycle_Ability: + iconPath = "E.png"; + break; + default: + break; + } + } else { + iconPath = globalScene.inputController?.getIconForLatestInputRecorded(iconSetting); + } + iconElement.setTexture(gamepadType, iconPath); + iconElement.setPosition(this.instructionRowX, this.instructionRowY); + controlLabel.setPosition(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY); + iconElement.setVisible(true); + controlLabel.setVisible(true); + this.instructionsContainer.add([ iconElement, controlLabel ]); + this.instructionRowY += 8; + if (this.instructionRowY >= 24) { + this.instructionRowY = 8; + this.instructionRowX += 50; + } + } + + updateInstructions(): void { + this.instructionRowX = 0; + this.instructionRowY = 0; + this.filterInstructionRowX = 0; + this.filterInstructionRowY = 0; + this.hideInstructions(); + this.instructionsContainer.removeAll(); + this.filterInstructionsContainer.removeAll(); + let gamepadType; + if (globalScene.inputMethod === "gamepad") { + gamepadType = globalScene.inputController.getConfig(globalScene.inputController.selectedDevice[Device.GAMEPAD]).padType; + } else { + gamepadType = globalScene.inputMethod; + } + + if (!gamepadType) { + return; + } + + const isFormCaught = this.isFormCaught(); + + if (this.isCaught()) { + if (isFormCaught) { + if (!pokemonPrevolutions.hasOwnProperty(this.species.speciesId)) { + this.updateButtonIcon(SettingKeyboard.Button_Stats, gamepadType, this.candyUpgradeIconElement, this.candyUpgradeLabel); + } + if (this.canCycleShiny) { + this.updateButtonIcon(SettingKeyboard.Button_Cycle_Shiny, gamepadType, this.shinyIconElement, this.shinyLabel); + } + if (this.canCycleGender) { + this.updateButtonIcon(SettingKeyboard.Button_Cycle_Gender, gamepadType, this.genderIconElement, this.genderLabel); + } + } + if (this.canCycleForm) { + this.updateButtonIcon(SettingKeyboard.Button_Cycle_Form, gamepadType, this.formIconElement, this.formLabel); + } + } + } + + 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; + } + + Challenge.applyChallenges(globalScene.gameMode, Challenge.ChallengeType.STARTER_POINTS, valueLimit); + + return valueLimit.value; + } + + + setCursor(cursor: integer): boolean { + const ret = super.setCursor(cursor); + + if (!this.cursorObj) { + this.cursorObj = globalScene.add.image(0, 0, "cursor"); + this.cursorObj.setOrigin(0, 0); + this.menuContainer.add(this.cursorObj); + } + + this.cursorObj.setScale(this.scale * 6); + this.cursorObj.setPositionRelative(this.menuBg, 7, 6 + (18 + this.cursor * 96) * this.scale); + + const ui = this.getUi(); + + const isFormCaught = this.isFormCaught(); + + if ((this.isCaught() && isFormCaught) || (this.speciesStarterDexEntry?.seenAttr && cursor === 5)) { + ui.showText(this.menuDescriptions[cursor]); + } else { + ui.showText(""); + } + + return ret; + } + + getFriendship(speciesId: number) { + let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship; + if (!currentFriendship || currentFriendship === undefined) { + currentFriendship = 0; + } + + const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]); + + return { currentFriendship, friendshipCap }; + } + + setSpecies() { + const species = this.species; + const starterAttributes : StarterAttributes | null = species ? { ...this.starterAttributes } : null; + + if (!species && globalScene.ui.getTooltip().visible) { + globalScene.ui.hideTooltip(); + } + + if (this.statsMode) { + if (this.isCaught()) { + this.statsContainer.setVisible(true); + this.showStats(); + } else { + this.statsContainer.setVisible(false); + //@ts-ignore + this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. what. how? huh? + } + } + + if (species && (this.speciesStarterDexEntry?.seenAttr || this.isCaught())) { + 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); + } + + if (this.isCaught()) { + const colorScheme = starterColors[species.speciesId]; + + const luck = globalScene.gameData.getDexAttrLuck(this.isCaught()); + this.pokemonLuckText.setVisible(!!luck); + this.pokemonLuckText.setText(luck.toString()); + this.pokemonLuckText.setTint(getVariantTint(Math.min(luck - 1, 2) as Variant)); + this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible); + + //Growth translate + let growthReadable = toReadableString(GrowthRate[species.growthRate]); + const growthAux = growthReadable.replace(" ", "_"); + if (i18next.exists("growth:" + growthAux)) { + growthReadable = i18next.t("growth:" + growthAux as any); + } + this.pokemonGrowthRateText.setText(growthReadable); + + this.pokemonGrowthRateText.setColor(getGrowthRateColor(species.growthRate)); + this.pokemonGrowthRateText.setShadowColor(getGrowthRateColor(species.growthRate, true)); + this.pokemonGrowthRateLabelText.setVisible(true); + this.pokemonUncaughtText.setVisible(false); + this.pokemonCaughtCountText.setText(`${this.speciesStarterDexEntry?.caughtCount}`); + if (species.speciesId === Species.MANAPHY || species.speciesId === Species.PHIONE) { + this.pokemonHatchedIcon.setFrame("manaphy"); + } else { + this.pokemonHatchedIcon.setFrame(getEggTierForSpecies(species)); + } + this.pokemonHatchedCountText.setText(`${this.speciesStarterDexEntry?.hatchedCount}`); + + const defaultDexAttr = this.getCurrentDexProps(species.speciesId); + const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); + const variant = defaultProps.variant; + const tint = getVariantTint(variant); + this.pokemonShinyIcon.setFrame(getVariantIcon(variant)); + this.pokemonShinyIcon.setTint(tint); + this.pokemonShinyIcon.setVisible(defaultProps.shiny); + this.pokemonCaughtHatchedContainer.setVisible(true); + this.pokemonFormText.setVisible(true); + + if (pokemonPrevolutions.hasOwnProperty(species.speciesId)) { + this.pokemonCaughtHatchedContainer.setY(16); + this.pokemonShinyIcon.setY(135); + this.pokemonShinyIcon.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(`x${globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)].candyCount}`); + this.pokemonCandyContainer.setVisible(true); + this.pokemonFormText.setY(42); + this.pokemonHatchedIcon.setVisible(true); + this.pokemonHatchedCountText.setVisible(true); + + const { currentFriendship, friendshipCap } = this.getFriendship(this.species.speciesId); + const candyCropY = 16 - (16 * (currentFriendship / friendshipCap)); + this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY); + + this.pokemonCandyContainer.on("pointerover", () => { + globalScene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true); + this.activeTooltip = "CANDY"; + }); + this.pokemonCandyContainer.on("pointerout", () => { + globalScene.ui.hideTooltip(); + this.activeTooltip = undefined; + }); + + } + + // 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 && !isNaN(starterAttributes.variant)) { + if (props.shiny) { + props.variant = starterAttributes.variant as Variant; + } + } + props.form = starterAttributes?.form ?? props.form; + props.female = starterAttributes?.female ?? props.female; + + this.setSpeciesDetails(species, { + shiny: props.shiny, + formIndex: props.form, + female: props.female, + variant: props.variant ?? 0, + }); + + if (this.isFormCaught(this.species, props.form)) { + const speciesForm = getPokemonSpeciesForm(species.speciesId, props.form ?? 0); + this.setTypeIcons(speciesForm.type1, speciesForm.type2); + this.pokemonSprite.clearTint(); + } + } else { + this.pokemonGrowthRateText.setText(""); + this.pokemonGrowthRateLabelText.setVisible(false); + this.type1Icon.setVisible(true); + this.type2Icon.setVisible(true); + this.pokemonLuckLabelText.setVisible(false); + this.pokemonLuckText.setVisible(false); + this.pokemonShinyIcon.setVisible(false); + this.pokemonUncaughtText.setVisible(true); + this.pokemonCaughtHatchedContainer.setVisible(true); + this.pokemonCandyContainer.setVisible(false); + this.pokemonFormText.setVisible(false); + + const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true); + const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); + + this.setSpeciesDetails(species, { + shiny: props.shiny, + formIndex: props.formIndex, + female: props.female, + variant: props.variant, + forSeen: true + }); + this.pokemonSprite.setTint(0x808080); + } + } else { + this.pokemonNumberText.setText(species ? padInt(species.speciesId, 4) : ""); + this.pokemonNameText.setText(species ? "???" : ""); + this.pokemonGrowthRateText.setText(""); + this.pokemonGrowthRateLabelText.setVisible(false); + this.type1Icon.setVisible(false); + this.type2Icon.setVisible(false); + this.pokemonLuckLabelText.setVisible(false); + this.pokemonLuckText.setVisible(false); + this.pokemonShinyIcon.setVisible(false); + this.pokemonUncaughtText.setVisible(!!species); + this.pokemonCaughtHatchedContainer.setVisible(false); + this.pokemonCandyContainer.setVisible(false); + this.pokemonFormText.setVisible(false); + + this.setSpeciesDetails(species!, { // TODO: is this bang correct? + shiny: false, + formIndex: 0, + female: false, + variant: 0, + }); + this.pokemonSprite.setTint(0x000000); + } + } + + setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, forceUpdate?: boolean): void { + let { shiny, formIndex, female, variant } = options; + const forSeen: boolean = options.forSeen ?? false; + const oldProps = species ? this.starterAttributes : null; + + // We will only update the sprite if there is a change to form, shiny/variant + // or gender for species with gender sprite differences + const shouldUpdateSprite = (species?.genderDiffs && !isNullOrUndefined(female)) + || !isNullOrUndefined(formIndex) || !isNullOrUndefined(shiny) || !isNullOrUndefined(variant) || forceUpdate; + + if (this.activeTooltip === "CANDY") { + if (this.species && this.pokemonCandyContainer.visible) { + const { currentFriendship, friendshipCap } = this.getFriendship(this.species.speciesId); + globalScene.ui.editTooltip("", `${currentFriendship}/${friendshipCap}`); + } else { + globalScene.ui.hideTooltip(); + } + } + + if (species?.forms?.find(f => f.formKey === "female")) { + if (female !== undefined) { + formIndex = female ? 1 : 0; + } else if (formIndex !== undefined) { + female = formIndex === 1; + } + } + + if (species) { + // Only assign shiny, female, and variant if they are undefined + if (shiny === undefined) { + shiny = oldProps?.shiny ?? false; + } + if (female === undefined) { + female = oldProps?.female ?? false; + } + if (variant === undefined) { + variant = oldProps?.variant ?? 0; + } + if (formIndex === undefined) { + formIndex = oldProps?.form ?? 0; + } + } + + this.pokemonSprite.setVisible(false); + + if (this.assetLoadCancelled) { + this.assetLoadCancelled.value = true; + this.assetLoadCancelled = null; + } + + if (species) { + const dexEntry = globalScene.gameData.dexData[species.speciesId]; + + const caughtAttr = this.isCaught(dexEntry); + + if (!caughtAttr) { + const props = this.starterAttributes; + + if (shiny === undefined || shiny !== props.shiny) { + shiny = props.shiny; + } + if (formIndex === undefined || formIndex !== props.form) { + formIndex = props.form; + } + if (female === undefined || female !== props.female) { + female = props.female; + } + if (variant === undefined || variant !== props.variant) { + variant = props.variant; + } + } + + const isFormCaught = this.isFormCaught(); + + this.shinyOverlay.setVisible(shiny ?? false); // TODO: is false the correct default? + this.pokemonNumberText.setColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, false)); + this.pokemonNumberText.setShadowColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, true)); + + + const assetLoadCancelled = new BooleanHolder(false); + this.assetLoadCancelled = assetLoadCancelled; + + if (shouldUpdateSprite) { + const back = this.showBackSprite ? true : false; + species.loadAssets(female!, formIndex, shiny, variant as Variant, true, back).then(() => { // TODO: is this bang correct? + if (assetLoadCancelled.value) { + return; + } + this.assetLoadCancelled = null; + this.speciesLoaded.set(species.speciesId, true); + this.pokemonSprite.play(species.getSpriteKey(female!, formIndex, shiny, variant, back)); // TODO: is this bang correct? + this.pokemonSprite.setPipelineData("shiny", shiny); + this.pokemonSprite.setPipelineData("variant", variant); + this.pokemonSprite.setPipelineData("spriteKey", species.getSpriteKey(female!, formIndex, shiny, variant, back)); // TODO: is this bang correct? + this.pokemonSprite.setVisible(!this.statsMode); + }); + } else { + this.pokemonSprite.setVisible(!this.statsMode); + } + + const 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); + + this.canCycleShiny = isNonShinyCaught && isShinyCaught; + + const isMaleCaught = !!(caughtAttr & DexAttr.MALE); + const isFemaleCaught = !!(caughtAttr & DexAttr.FEMALE); + this.canCycleGender = isMaleCaught && isFemaleCaught; + + // If the dev option for the dex is selected, all forms can be cycled through + this.canCycleForm = globalScene.dexForDevs ? species.forms.length > 1 : + species.forms.filter(f => f.isStarterSelectable).filter(f => f).length > 1; + + if (caughtAttr && species.malePercent !== null) { + const gender = !female ? Gender.MALE : Gender.FEMALE; + this.pokemonGenderText.setText(getGenderSymbol(gender)); + this.pokemonGenderText.setColor(getGenderColor(gender)); + this.pokemonGenderText.setShadowColor(getGenderColor(gender, true)); + } else { + this.pokemonGenderText.setText(""); + } + + if (caughtAttr) { + if (isFormCaught) { + this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => { + const crier = (this.species.forms && this.species.forms.length > 0) ? this.species.forms[formIndex ?? this.formIndex] : this.species; + crier.cry(); + }); + + this.pokemonSprite.clearTint(); + } else { + this.pokemonSprite.setTint(0x000000); + } + } + + if (caughtAttr || forSeen) { + const speciesForm = getPokemonSpeciesForm(species.speciesId, formIndex!); // TODO: is the bang correct? + this.setTypeIcons(speciesForm.type1, speciesForm.type2); + this.pokemonFormText.setText(this.getFormString((speciesForm as PokemonForm).formKey, species)); + + } else { + this.setTypeIcons(null, null); + this.pokemonFormText.setText(""); + } + } else { + this.shinyOverlay.setVisible(false); + this.pokemonNumberText.setColor(this.getTextColor(TextStyle.SUMMARY)); + this.pokemonNumberText.setShadowColor(this.getTextColor(TextStyle.SUMMARY, true)); + this.pokemonGenderText.setText(""); + this.setTypeIcons(null, null); + } + + this.updateInstructions(); + } + + setTypeIcons(type1: Type | null, type2: Type | null): void { + if (type1 !== null) { + this.type1Icon.setVisible(true); + this.type1Icon.setFrame(Type[type1].toLowerCase()); + } else { + this.type1Icon.setVisible(false); + } + if (type2 !== null) { + this.type2Icon.setVisible(true); + this.type2Icon.setFrame(Type[type2].toLowerCase()); + } else { + this.type2Icon.setVisible(false); + } + } + + + /** + * Creates a temporary dex attr props that will be used to display the correct shiny, variant, and form based on this.starterAttributes + * + * @param speciesId the id of the species to get props for + * @returns the dex props + */ + getCurrentDexProps(speciesId: number): bigint { + let props = 0n; + const caughtAttr = globalScene.gameData.dexData[speciesId].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.starterAttributes?.female || ((caughtAttr & DexAttr.FEMALE) > 0n && (caughtAttr & DexAttr.MALE) === 0n)) { + props += DexAttr.FEMALE; + } else { + props += DexAttr.MALE; + } + /* This part is very similar to above, but instead of for gender, it checks for shiny within starter preferences. + * If they're not there, it enables shiny state by default if any shiny was caught + */ + if (this.starterAttributes?.shiny || ((caughtAttr & DexAttr.SHINY) > 0n && this.starterAttributes?.shiny !== false)) { + props += DexAttr.SHINY; + if (this.starterAttributes?.variant !== undefined) { + props += BigInt(Math.pow(2, this.starterAttributes?.variant)) * DexAttr.DEFAULT_VARIANT; + } else { + /* This calculates the correct variant if there's no starter preferences for it. + * This gets the highest tier variant that you've caught and adds it to the temp props + */ + if ((caughtAttr & DexAttr.VARIANT_3) > 0) { + props += DexAttr.VARIANT_3; + } else if ((caughtAttr & DexAttr.VARIANT_2) > 0) { + props += DexAttr.VARIANT_2; + } else { + props += DexAttr.DEFAULT_VARIANT; + } + } + } else { + props += DexAttr.NON_SHINY; + props += DexAttr.DEFAULT_VARIANT; // we add the default variant here because non shiny versions are listed as default variant + } + if (this.starterAttributes?.form) { // this checks for the form of the pokemon + props += BigInt(Math.pow(2, this.starterAttributes?.form)) * DexAttr.DEFAULT_FORM; + } else { + // Get the first unlocked form + props += globalScene.gameData.getFormAttr(globalScene.gameData.getFormIndex(caughtAttr)); + } + + return props; + } + + toggleStatsMode(on?: boolean): void { + if (on === undefined) { + on = !this.statsMode; + } + if (on) { + this.showStats(); + this.statsMode = true; + this.pokemonSprite.setVisible(false); + } else { + this.statsMode = false; + this.statsContainer.setVisible(false); + this.pokemonSprite.setVisible(true); + //@ts-ignore + this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. !?!? + } + } + + showStats(): void { + if (!this.speciesStarterDexEntry) { + return; + } + + this.statsContainer.setVisible(true); + + this.statsContainer.updateIvs(this.speciesStarterDexEntry.ivs); + } + + clearText() { + this.starterSelectMessageBoxContainer.setVisible(false); + super.clearText(); + } + + hideInstructions(): void { + this.candyUpgradeIconElement.setVisible(false); + this.candyUpgradeLabel.setVisible(false); + this.shinyIconElement.setVisible(false); + this.shinyLabel.setVisible(false); + this.formIconElement.setVisible(false); + this.formLabel.setVisible(false); + this.genderIconElement.setVisible(false); + this.genderLabel.setVisible(false); + this.variantIconElement.setVisible(false); + this.variantLabel.setVisible(false); + } + + clear(): void { + super.clear(); + + this.cursor = -1; + this.hideInstructions(); + this.activeTooltip = undefined; + globalScene.ui.hideTooltip(); + + this.starterSelectContainer.setVisible(false); + this.blockInput = false; + + this.showBackSprite = false; + this.showBackSpriteLabel.setText(i18next.t("pokedexUiHandler:showBackSprite")); + + if (this.statsMode) { + this.toggleStatsMode(false); + } + } + + checkIconId(icon: Phaser.GameObjects.Sprite, species: PokemonSpecies, female: boolean, formIndex: number, shiny: boolean, variant: number) { + if (icon.frame.name !== species.getIconId(female, formIndex, shiny, variant)) { + console.log(`${species.name}'s icon ${icon.frame.name} does not match getIconId with female: ${female}, formIndex: ${formIndex}, shiny: ${shiny}, variant: ${variant}`); + icon.setTexture(species.getIconAtlasKey(formIndex, false, variant)); + icon.setFrame(species.getIconId(female, formIndex, false, variant)); + } + } +} diff --git a/src/ui/pokedex-scan-ui-handler.ts b/src/ui/pokedex-scan-ui-handler.ts new file mode 100644 index 00000000000..0fc7171842a --- /dev/null +++ b/src/ui/pokedex-scan-ui-handler.ts @@ -0,0 +1,191 @@ +import type { InputFieldConfig } from "./form-modal-ui-handler"; +import { FormModalUiHandler } from "./form-modal-ui-handler"; +import type { ModalConfig } from "./modal-ui-handler"; +import type { PlayerPokemon } from "#app/field/pokemon"; +import type { OptionSelectItem } from "./abstact-option-select-ui-handler"; +import { isNullOrUndefined } from "#app/utils"; +import { Mode } from "./ui"; +import { FilterTextRow } from "./filter-text"; +import { allAbilities } from "#app/data/ability"; +import { allMoves } from "#app/data/move"; +import { allSpecies } from "#app/data/pokemon-species"; +import i18next from "i18next"; + +export default class PokedexScanUiHandler extends FormModalUiHandler { + + keys: string[]; + reducedKeys: string[]; + parallelKeys: string[]; + nameKeys: string[]; + moveKeys: string[]; + abilityKeys: string[]; + row: number; + + constructor(mode) { + super(mode); + } + + setup() { + super.setup(); + + this.nameKeys = allSpecies.map(a => a.name).filter((value, index, self) => self.indexOf(value) === index); + this.moveKeys = allMoves.map(a => a.name); + this.abilityKeys = allAbilities.map(a => a.name); + } + + getModalTitle(config?: ModalConfig): string { + return i18next.t("pokedexUiHandler:scanChooseOption"); + } + + getWidth(config?: ModalConfig): number { + return 300; + } + + getMargin(config?: ModalConfig): [number, number, number, number] { + return [ 0, 0, 48, 0 ]; + } + + getButtonLabels(config?: ModalConfig): string[] { + return [ i18next.t("pokedexUiHandler:scanSelect"), i18next.t("pokedexUiHandler:scanCancel") ]; + } + + getReadableErrorMessage(error: string): string { + const colonIndex = error?.indexOf(":"); + if (colonIndex > 0) { + error = error.slice(0, colonIndex); + } + + return super.getReadableErrorMessage(error); + } + + override getInputFieldConfigs(): InputFieldConfig[] { + switch (this.row) { + case FilterTextRow.NAME: { + return [{ label: i18next.t("pokedexUiHandler:scanLabelName") }]; + } + case FilterTextRow.MOVE_1: + case FilterTextRow.MOVE_2: { + return [{ label: i18next.t("pokedexUiHandler:scanLabelMove") }]; + } + case FilterTextRow.ABILITY_1:{ + return [{ label: i18next.t("pokedexUiHandler:scanLabelAbility") }]; + } + case FilterTextRow.ABILITY_2: { + return [{ label: i18next.t("pokedexUiHandler:scanLabelPassive") }]; + } + default: { + return [{ label: "" }]; + } + } + + } + + reduceKeys(): void { + switch (this.row) { + case FilterTextRow.NAME: { + this.reducedKeys = this.nameKeys; + break; + } + case FilterTextRow.MOVE_1: + case FilterTextRow.MOVE_2: { + this.reducedKeys = this.moveKeys; + break; + } + case FilterTextRow.ABILITY_1: + case FilterTextRow.ABILITY_2: { + this.reducedKeys = this.abilityKeys; + break; + } + default: { + this.reducedKeys = this.keys; + } + } + } + + + // args[2] is an index of FilterTextRow + show(args: any[]): boolean { + this.row = args[2]; + const ui = this.getUi(); + const hasTitle = !!this.getModalTitle(); + this.updateFields(this.getInputFieldConfigs(), hasTitle); + this.updateContainer(args[0] as ModalConfig); + const input = this.inputs[0]; + input.setMaxLength(255); + + this.reduceKeys(); + + input.on("keydown", (inputObject, evt: KeyboardEvent) => { + if ([ "escape", "space" ].some((v) => v === evt.key.toLowerCase() || v === evt.code.toLowerCase()) && ui.getMode() === Mode.AUTO_COMPLETE) { + // Delete autocomplete list and recovery focus. + inputObject.on("blur", () => inputObject.node.focus(), { once: true }); + ui.revertMode(); + } + }); + + input.on("textchange", (inputObject, evt: InputEvent) => { + // Delete autocomplete. + if (ui.getMode() === Mode.AUTO_COMPLETE) { + ui.revertMode(); + } + + let options: OptionSelectItem[] = []; + const filteredKeys = this.reducedKeys.filter((command) => command.toLowerCase().includes(inputObject.text.toLowerCase())); + if (inputObject.text !== "" && filteredKeys.length > 0) { + options = filteredKeys.slice(0).map((value) => { + return { + label: value, + handler: () => { + if (!isNullOrUndefined(evt.data) || evt.inputType?.toLowerCase() === "deletecontentbackward") { + inputObject.setText(value); + } + ui.revertMode(); + return true; + } + }; + }); + } + + if (options.length > 0) { + const modalOpts = { + options: options, + maxOptions: 5, + modalContainer: this.modalContainer + }; + ui.setOverlayMode(Mode.AUTO_COMPLETE, modalOpts); + } + + }); + + if (super.show(args)) { + const config = args[0] as ModalConfig; + this.inputs[0].resize(1150, 116); + this.inputContainers[0].list[0].width = 200; + if (args[1] && typeof (args[1] as PlayerPokemon).getNameToRender === "function") { + this.inputs[0].text = (args[1] as PlayerPokemon).getNameToRender(); + } else { + this.inputs[0].text = args[1]; + } + this.submitAction = (_) => { + if (ui.getMode() === Mode.POKEDEX_SCAN) { + this.sanitizeInputs(); + const sanitizedName = btoa(unescape(encodeURIComponent(this.inputs[0].text))); + config.buttonActions[0](sanitizedName); + return true; + } + return false; + }; + return true; + } + return false; + } + + clear(): void { + super.clear(); + + // Clearing the labels so they don't appear again and overlap + this.formLabels.forEach(label => { + label.destroy(); + }); + } +} diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts new file mode 100644 index 00000000000..c2e3731cf0c --- /dev/null +++ b/src/ui/pokedex-ui-handler.ts @@ -0,0 +1,1907 @@ +import type { Variant } from "#app/data/variant"; +import { getVariantTint, getVariantIcon } from "#app/data/variant"; +import { argbFromRgba } from "@material/material-color-utilities"; +import i18next from "i18next"; +import { starterColors } from "#app/battle-scene"; +import { speciesEggMoves } from "#app/data/balance/egg-moves"; +import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; +import type { PokemonForm } from "#app/data/pokemon-species"; +import type PokemonSpecies from "#app/data/pokemon-species"; +import { allSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#app/data/pokemon-species"; +import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; +import { catchableSpecies } from "#app/data/balance/biomes"; +import { Type } from "#enums/type"; +import type { DexAttrProps, DexEntry, StarterMoveset, StarterAttributes, StarterPreferences } from "#app/system/game-data"; +import { AbilityAttr, DexAttr, StarterPrefs } from "#app/system/game-data"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler"; +import { TextStyle, addTextObject } from "#app/ui/text"; +import { Mode } from "#app/ui/ui"; +import { SettingKeyboard } from "#app/system/settings/settings-keyboard"; +import { Passive as PassiveAttr } from "#enums/passive"; +import type { Moves } from "#enums/moves"; +import type { Species } from "#enums/species"; +import { Button } from "#enums/buttons"; +import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#app/ui/dropdown"; +import { PokedexMonContainer } from "#app/ui/pokedex-mon-container"; +import { DropDownColumn, FilterBar } from "#app/ui/filter-bar"; +import { ScrollBar } from "#app/ui/scroll-bar"; +import { Abilities } from "#enums/abilities"; +import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters"; +import { BooleanHolder, fixedInt, getLocalizedSpriteKey, padInt, randIntRange, rgbHexToRgba } from "#app/utils"; +import type { Nature } from "#enums/nature"; +import { addWindow } from "./ui-theme"; +import type { OptionSelectConfig } from "./abstact-option-select-ui-handler"; +import { FilterText, FilterTextRow } from "./filter-text"; +import { allAbilities } from "#app/data/ability"; +import { starterPassiveAbilities } from "#app/data/balance/passives"; +import { allMoves } from "#app/data/move"; +import { speciesTmMoves } from "#app/data/balance/tms"; +import { pokemonStarters } from "#app/data/balance/pokemon-evolutions"; +import { Biome } from "#enums/biome"; +import { globalScene } from "#app/global-scene"; + + +// We don't need this interface here +export interface Starter { + species: PokemonSpecies; + starter: PokemonSpecies; + dexAttr: bigint; + abilityIndex: integer, + passive: boolean; + nature: Nature; + moveset?: StarterMoveset; + pokerus: boolean; + nickname?: string; +} + + +interface LanguageSetting { + starterInfoTextSize: string, + instructionTextSize: string, + starterInfoXPos?: integer, + starterInfoYOffset?: integer +} + +const languageSettings: { [key: string]: LanguageSetting } = { + "en":{ + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, + "de":{ + starterInfoTextSize: "48px", + instructionTextSize: "35px", + starterInfoXPos: 33, + }, + "es-ES":{ + starterInfoTextSize: "56px", + instructionTextSize: "35px", + }, + "fr":{ + starterInfoTextSize: "54px", + instructionTextSize: "38px", + }, + "it":{ + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, + "pt_BR":{ + starterInfoTextSize: "47px", + instructionTextSize: "38px", + starterInfoXPos: 33, + }, + "zh":{ + starterInfoTextSize: "47px", + instructionTextSize: "38px", + starterInfoYOffset: 1, + starterInfoXPos: 24, + }, + "pt":{ + starterInfoTextSize: "48px", + instructionTextSize: "42px", + starterInfoXPos: 33, + }, + "ko":{ + starterInfoTextSize: "52px", + instructionTextSize: "38px", + }, + "ja":{ + starterInfoTextSize: "51px", + instructionTextSize: "38px", + }, + "ca-ES":{ + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, +}; + + +enum FilterTextOptions{ + NAME, + MOVE_1, + MOVE_2, + ABILITY_1, + ABILITY_2, +} + + +const valueReductionMax = 2; + +// Position of UI elements +const filterBarHeight = 17; +const speciesContainerX = 143; // if team on the RIGHT: 109 / if on the LEFT: 143 + +/** + * Calculates the starter position for a Pokemon of a given UI index + * @param index UI index to calculate the starter position of + * @returns An interface with an x and y property + */ +function calcStarterPosition(index: number, scrollCursor:number = 0): {x: number, y: number} { + const yOffset = 13; + const height = 17; + const x = (index % 9) * 18; + const y = yOffset + (Math.floor(index / 9) - scrollCursor) * height; + + return { x: x, y: y }; +} + +interface SpeciesDetails { + shiny?: boolean, + formIndex?: integer + female?: boolean, + variant?: Variant, + abilityIndex?: integer, + natureIndex?: integer, + forSeen?: boolean, // default = false +} + +export default class PokedexUiHandler extends MessageUiHandler { + private starterSelectContainer: Phaser.GameObjects.Container; + private starterSelectScrollBar: ScrollBar; + private filterBarContainer: Phaser.GameObjects.Container; + private filterBar: FilterBar; + private pokemonContainers: PokedexMonContainer[] = []; + private filteredPokemonContainers: PokedexMonContainer[] = []; + private validPokemonContainers: PokedexMonContainer[] = []; + private pokemonNumberText: Phaser.GameObjects.Text; + private pokemonSprite: Phaser.GameObjects.Sprite; + private pokemonNameText: Phaser.GameObjects.Text; + private type1Icon: Phaser.GameObjects.Sprite; + private type2Icon: Phaser.GameObjects.Sprite; + + private starterSelectMessageBox: Phaser.GameObjects.NineSlice; + private starterSelectMessageBoxContainer: Phaser.GameObjects.Container; + + private filterMode: boolean; + private filterBarCursor: integer = 0; + private starterMoveset: StarterMoveset | null; + private scrollCursor: number; + + private allSpecies: PokemonSpecies[] = []; + private lastSpecies: PokemonSpecies; + private speciesLoaded: Map = new Map(); + private pokerusSpecies: PokemonSpecies[] = []; + private speciesStarterDexEntry: DexEntry | null; + private speciesStarterMoves: Moves[]; + + private assetLoadCancelled: BooleanHolder | null; + public cursorObj: Phaser.GameObjects.Image; + private pokerusCursorObjs: Phaser.GameObjects.Image[]; + + private iconAnimHandler: PokemonIconAnimHandler; + + private starterPreferences: StarterPreferences; + + protected blockInput: boolean = false; + + // for text filters + + + private readonly textPadding = 8; + private readonly defaultMessageBoxWidth = 220; + private readonly defaultWordWrapWidth = 1224; + private menuMessageBoxContainer: Phaser.GameObjects.Container; + private menuMessageBox: Phaser.GameObjects.NineSlice; + private dialogueMessageBox: Phaser.GameObjects.NineSlice; + protected manageDataConfig: OptionSelectConfig; + private filterTextOptions: FilterTextOptions[]; + protected optionSelectText: Phaser.GameObjects.Text; + protected scale: number = 0.1666666667; + private menuBg: Phaser.GameObjects.NineSlice; + + private filterTextContainer: Phaser.GameObjects.Container; + private filterText: FilterText; + private filterTextMode: boolean; + private filterTextCursor: integer = 0; + + private showDecorations: boolean = false; + private goFilterIconElement1: Phaser.GameObjects.Sprite; + private goFilterIconElement2: Phaser.GameObjects.Sprite; + private goFilterLabel: Phaser.GameObjects.Text; + private toggleDecorationsIconElement: Phaser.GameObjects.Sprite; + private toggleDecorationsLabel: Phaser.GameObjects.Text; + + constructor() { + super(Mode.POKEDEX); + } + + setup() { + const ui = this.getUi(); + const currentLanguage = i18next.resolvedLanguage ?? "en"; + const langSettingKey = Object.keys(languageSettings).find(lang => currentLanguage.includes(lang)) ?? "en"; + const textSettings = languageSettings[langSettingKey]; + + this.starterSelectContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6); + this.starterSelectContainer.setVisible(false); + ui.add(this.starterSelectContainer); + + const bgColor = globalScene.add.rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6, 0x006860); + bgColor.setOrigin(0, 0); + this.starterSelectContainer.add(bgColor); + + const pokemonContainerWindow = addWindow(speciesContainerX, filterBarHeight + 1, 175, 161); + const pokemonContainerBg = globalScene.add.image(speciesContainerX + 1, filterBarHeight + 2, "starter_container_bg"); + pokemonContainerBg.setOrigin(0, 0); + this.starterSelectContainer.add(pokemonContainerBg); + this.starterSelectContainer.add(pokemonContainerWindow); + + + // Create and initialise filter text fields + this.filterTextContainer = globalScene.add.container(0, 0); + this.filterText = new FilterText(1, filterBarHeight + 2, 140, 100, this.updateStarters); + + this.filterText.addFilter(FilterTextRow.NAME, i18next.t("filterText:nameField")); + this.filterText.addFilter(FilterTextRow.MOVE_1, i18next.t("filterText:move1Field")); + this.filterText.addFilter(FilterTextRow.MOVE_2, i18next.t("filterText:move2Field")); + this.filterText.addFilter(FilterTextRow.ABILITY_1, i18next.t("filterText:ability1Field")); + this.filterText.addFilter(FilterTextRow.ABILITY_2, i18next.t("filterText:ability2Field")); + + this.filterTextContainer.add(this.filterText); + this.starterSelectContainer.add(this.filterTextContainer); + + + // Create and initialise filter bar + this.filterBarContainer = globalScene.add.container(0, 0); + this.filterBar = new FilterBar(speciesContainerX, 1, 175, filterBarHeight, 2, 0, 6); + + // gen filter + const genOptions: DropDownOption[] = [ + new DropDownOption(1, new DropDownLabel(i18next.t("pokedexUiHandler:gen1"))), + new DropDownOption(2, new DropDownLabel(i18next.t("pokedexUiHandler:gen2"))), + new DropDownOption(3, new DropDownLabel(i18next.t("pokedexUiHandler:gen3"))), + new DropDownOption(4, new DropDownLabel(i18next.t("pokedexUiHandler:gen4"))), + new DropDownOption(5, new DropDownLabel(i18next.t("pokedexUiHandler:gen5"))), + new DropDownOption(6, new DropDownLabel(i18next.t("pokedexUiHandler:gen6"))), + new DropDownOption(7, new DropDownLabel(i18next.t("pokedexUiHandler:gen7"))), + new DropDownOption(8, new DropDownLabel(i18next.t("pokedexUiHandler:gen8"))), + new DropDownOption(9, new DropDownLabel(i18next.t("pokedexUiHandler:gen9"))), + ]; + const genDropDown: DropDown = new DropDown(0, 0, genOptions, this.updateStarters, DropDownType.HYBRID); + this.filterBar.addFilter(DropDownColumn.GEN, i18next.t("filterBar:genFilter"), genDropDown); + + // type filter + const typeKeys = Object.keys(Type).filter(v => isNaN(Number(v))); + const typeOptions: DropDownOption[] = []; + typeKeys.forEach((type, index) => { + if (index === 0 || index === 19) { + return; + } + const typeSprite = globalScene.add.sprite(0, 0, getLocalizedSpriteKey("types")); + typeSprite.setScale(0.5); + typeSprite.setFrame(type.toLowerCase()); + typeOptions.push(new DropDownOption( index, new DropDownLabel("", typeSprite))); + }); + this.filterBar.addFilter(DropDownColumn.TYPES, i18next.t("filterBar:typeFilter"), new DropDown(0, 0, typeOptions, this.updateStarters, DropDownType.HYBRID, 0.5)); + + // biome filter, making an entry in the dropdown for each biome + const biomeOptions = Object.values(Biome) + .filter((value) => typeof value === "number") // Filter numeric values from the enum + .map((biomeValue, index) => + new DropDownOption( index, new DropDownLabel(i18next.t(`biome:${Biome[biomeValue].toUpperCase()}`))) + ); + biomeOptions.push(new DropDownOption( biomeOptions.length, new DropDownLabel(i18next.t("filterBar:uncatchable")))); + const biomeDropDown: DropDown = new DropDown(0, 0, biomeOptions, this.updateStarters, DropDownType.HYBRID); + this.filterBar.addFilter(DropDownColumn.BIOME, i18next.t("filterBar:biomeFilter"), biomeDropDown); + + // caught filter + const shiny1Sprite = globalScene.add.sprite(0, 0, "shiny_icons"); + shiny1Sprite.setOrigin(0.15, 0.2); + shiny1Sprite.setScale(0.6); + shiny1Sprite.setFrame(getVariantIcon(0)); + shiny1Sprite.setTint(getVariantTint(0)); + const shiny2Sprite = globalScene.add.sprite(0, 0, "shiny_icons"); + shiny2Sprite.setOrigin(0.15, 0.2); + shiny2Sprite.setScale(0.6); + shiny2Sprite.setFrame(getVariantIcon(1)); + shiny2Sprite.setTint(getVariantTint(1)); + const shiny3Sprite = globalScene.add.sprite(0, 0, "shiny_icons"); + shiny3Sprite.setOrigin(0.15, 0.2); + shiny3Sprite.setScale(0.6); + shiny3Sprite.setFrame(getVariantIcon(2)); + shiny3Sprite.setTint(getVariantTint(2)); + + const caughtOptions = [ + new DropDownOption("SHINY3", new DropDownLabel("", shiny3Sprite)), + new DropDownOption("SHINY2", new DropDownLabel("", shiny2Sprite)), + new DropDownOption("SHINY", new DropDownLabel("", shiny1Sprite)), + new DropDownOption("NORMAL", new DropDownLabel(i18next.t("filterBar:normal"))), + new DropDownOption("UNCAUGHT", new DropDownLabel(i18next.t("filterBar:uncaught"))) + ]; + + this.filterBar.addFilter(DropDownColumn.CAUGHT, i18next.t("filterBar:caughtFilter"), new DropDown(0, 0, caughtOptions, this.updateStarters, DropDownType.HYBRID)); + + // unlocks filter + const passiveLabels = [ + new DropDownLabel(i18next.t("filterBar:passive"), undefined, DropDownState.OFF), + new DropDownLabel(i18next.t("filterBar:passiveUnlocked"), undefined, DropDownState.ON), + new DropDownLabel(i18next.t("filterBar:passiveUnlockable"), undefined, DropDownState.UNLOCKABLE), + new DropDownLabel(i18next.t("filterBar:passiveLocked"), undefined, DropDownState.EXCLUDE), + ]; + + const costReductionLabels = [ + new DropDownLabel(i18next.t("filterBar:costReduction"), undefined, DropDownState.OFF), + new DropDownLabel(i18next.t("filterBar:costReductionUnlocked"), undefined, DropDownState.ON), + new DropDownLabel(i18next.t("filterBar:costReductionUnlockable"), undefined, DropDownState.UNLOCKABLE), + new DropDownLabel(i18next.t("filterBar:costReductionLocked"), undefined, DropDownState.EXCLUDE), + ]; + + const unlocksOptions = [ + new DropDownOption("PASSIVE", passiveLabels), + new DropDownOption("COST_REDUCTION", costReductionLabels), + ]; + + this.filterBar.addFilter(DropDownColumn.UNLOCKS, i18next.t("filterBar:unlocksFilter"), new DropDown(0, 0, unlocksOptions, this.updateStarters, DropDownType.RADIAL)); + + // misc filter + const starters = [ + new DropDownLabel(i18next.t("filterBar:starter"), undefined, DropDownState.OFF), + new DropDownLabel(i18next.t("filterBar:isStarter"), undefined, DropDownState.ON), + new DropDownLabel(i18next.t("filterBar:notStarter"), undefined, DropDownState.EXCLUDE), + ]; + const favoriteLabels = [ + new DropDownLabel(i18next.t("filterBar:favorite"), undefined, DropDownState.OFF), + new DropDownLabel(i18next.t("filterBar:isFavorite"), undefined, DropDownState.ON), + new DropDownLabel(i18next.t("filterBar:notFavorite"), undefined, DropDownState.EXCLUDE), + ]; + const winLabels = [ + new DropDownLabel(i18next.t("filterBar:ribbon"), undefined, DropDownState.OFF), + new DropDownLabel(i18next.t("filterBar:hasWon"), undefined, DropDownState.ON), + new DropDownLabel(i18next.t("filterBar:hasNotWon"), undefined, DropDownState.EXCLUDE), + ]; + const hiddenAbilityLabels = [ + new DropDownLabel(i18next.t("filterBar:hiddenAbility"), undefined, DropDownState.OFF), + new DropDownLabel(i18next.t("filterBar:hasHiddenAbility"), undefined, DropDownState.ON), + new DropDownLabel(i18next.t("filterBar:noHiddenAbility"), undefined, DropDownState.EXCLUDE), + ]; + const eggLabels = [ + new DropDownLabel(i18next.t("filterBar:egg"), undefined, DropDownState.OFF), + new DropDownLabel(i18next.t("filterBar:eggPurchasable"), undefined, DropDownState.ON), + ]; + const pokerusLabels = [ + new DropDownLabel(i18next.t("filterBar:pokerus"), undefined, DropDownState.OFF), + new DropDownLabel(i18next.t("filterBar:hasPokerus"), undefined, DropDownState.ON), + ]; + const miscOptions = [ + new DropDownOption("STARTER", starters), + new DropDownOption("FAVORITE", favoriteLabels), + new DropDownOption("WIN", winLabels), + new DropDownOption("HIDDEN_ABILITY", hiddenAbilityLabels), + new DropDownOption("EGG", eggLabels), + new DropDownOption("POKERUS", pokerusLabels), + ]; + this.filterBar.addFilter(DropDownColumn.MISC, i18next.t("filterBar:miscFilter"), new DropDown(0, 0, miscOptions, this.updateStarters, DropDownType.RADIAL)); + + // sort filter + const sortOptions = [ + new DropDownOption(SortCriteria.NUMBER, new DropDownLabel(i18next.t("filterBar:sortByNumber"), undefined, DropDownState.ON)), + new DropDownOption(SortCriteria.COST, new DropDownLabel(i18next.t("filterBar:sortByCost"))), + new DropDownOption(SortCriteria.CANDY, new DropDownLabel(i18next.t("filterBar:sortByCandies"))), + new DropDownOption(SortCriteria.IV, new DropDownLabel(i18next.t("filterBar:sortByIVs"))), + new DropDownOption(SortCriteria.NAME, new DropDownLabel(i18next.t("filterBar:sortByName"))), + new DropDownOption(SortCriteria.CAUGHT, new DropDownLabel(i18next.t("filterBar:sortByNumCaught"))), + new DropDownOption(SortCriteria.HATCHED, new DropDownLabel(i18next.t("filterBar:sortByNumHatched"))) + ]; + this.filterBar.addFilter(DropDownColumn.SORT, i18next.t("filterBar:sortFilter"), new DropDown(0, 0, sortOptions, this.updateStarters, DropDownType.SINGLE)); + this.filterBarContainer.add(this.filterBar); + + this.starterSelectContainer.add(this.filterBarContainer); + + // Offset the generation filter dropdown to avoid covering the filtered pokemon + this.filterBar.offsetHybridFilters(); + + if (!globalScene.uiTheme) { + pokemonContainerWindow.setVisible(false); + } + + this.iconAnimHandler = new PokemonIconAnimHandler(); + this.iconAnimHandler.setup(); + + this.pokemonNumberText = addTextObject(6, 140, "", TextStyle.SUMMARY); + this.pokemonNumberText.setOrigin(0, 0); + this.starterSelectContainer.add(this.pokemonNumberText); + + this.pokemonNameText = addTextObject(6, 127, "", TextStyle.SUMMARY); + this.pokemonNameText.setOrigin(0, 0); + this.starterSelectContainer.add(this.pokemonNameText); + + const starterBoxContainer = globalScene.add.container(speciesContainerX + 6, 9); //115 + + this.starterSelectScrollBar = new ScrollBar(161, 12, 5, pokemonContainerWindow.height - 6, 9); + + starterBoxContainer.add(this.starterSelectScrollBar); + + this.pokerusCursorObjs = new Array(POKERUS_STARTER_COUNT).fill(null).map(() => { + const cursorObj = globalScene.add.image(0, 0, "select_cursor_pokerus"); + cursorObj.setVisible(false); + cursorObj.setOrigin(0, 0); + starterBoxContainer.add(cursorObj); + return cursorObj; + }); + + this.cursorObj = globalScene.add.image(0, 0, "select_cursor"); + this.cursorObj.setOrigin(0, 0); + + starterBoxContainer.add(this.cursorObj); + + for (const species of allSpecies) { + this.speciesLoaded.set(species.speciesId, false); + this.allSpecies.push(species); + + const pokemonContainer = new PokedexMonContainer(species).setVisible(false); + this.iconAnimHandler.addOrUpdate(pokemonContainer.icon, PokemonIconAnimMode.NONE); + this.pokemonContainers.push(pokemonContainer); + starterBoxContainer.add(pokemonContainer); + } + + this.starterSelectContainer.add(starterBoxContainer); + + this.pokemonSprite = globalScene.add.sprite(96, 143, "pkmn__sub"); + this.pokemonSprite.setPipeline(globalScene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true }); + this.starterSelectContainer.add(this.pokemonSprite); + + this.type1Icon = globalScene.add.sprite(10, 158, getLocalizedSpriteKey("types")); + this.type1Icon.setScale(0.5); + this.type1Icon.setOrigin(0, 0); + this.starterSelectContainer.add(this.type1Icon); + + this.type2Icon = globalScene.add.sprite(10, 166, getLocalizedSpriteKey("types")); + this.type2Icon.setScale(0.5); + this.type2Icon.setOrigin(0, 0); + this.starterSelectContainer.add(this.type2Icon); + + this.starterSelectMessageBoxContainer = globalScene.add.container(0, globalScene.game.canvas.height / 6); + this.starterSelectMessageBoxContainer.setVisible(false); + this.starterSelectContainer.add(this.starterSelectMessageBoxContainer); + + this.starterSelectMessageBox = addWindow(1, -1, 318, 28); + this.starterSelectMessageBox.setOrigin(0, 1); + this.starterSelectMessageBoxContainer.add(this.starterSelectMessageBox); + + // Instruction for "C" button to toggle showDecorations + const instructionTextSize = textSettings.instructionTextSize; + + this.goFilterIconElement1 = new Phaser.GameObjects.Sprite(globalScene, 10, 2, "keyboard", "C.png"); + this.goFilterIconElement1.setName("sprite-goFilter1-icon-element"); + this.goFilterIconElement1.setScale(0.675); + this.goFilterIconElement1.setOrigin(0.0, 0.0); + this.goFilterIconElement2 = new Phaser.GameObjects.Sprite(globalScene, 20, 2, "keyboard", "V.png"); + this.goFilterIconElement2.setName("sprite-goFilter2-icon-element"); + this.goFilterIconElement2.setScale(0.675); + this.goFilterIconElement2.setOrigin(0.0, 0.0); + this.goFilterLabel = addTextObject(30, 2, i18next.t("pokedexUiHandler:goFilters"), TextStyle.PARTY, { fontSize: instructionTextSize }); + this.goFilterLabel.setName("text-goFilter-label"); + this.starterSelectContainer.add(this.goFilterIconElement1); + this.starterSelectContainer.add(this.goFilterIconElement2); + this.starterSelectContainer.add(this.goFilterLabel); + + this.toggleDecorationsIconElement = new Phaser.GameObjects.Sprite(globalScene, 10, 10, "keyboard", "R.png"); + this.toggleDecorationsIconElement.setName("sprite-toggleDecorations-icon-element"); + this.toggleDecorationsIconElement.setScale(0.675); + this.toggleDecorationsIconElement.setOrigin(0.0, 0.0); + this.toggleDecorationsLabel = addTextObject(20, 10, i18next.t("pokedexUiHandler:toggleDecorations"), TextStyle.PARTY, { fontSize: instructionTextSize }); + this.toggleDecorationsLabel.setName("text-toggleDecorations-label"); + this.starterSelectContainer.add(this.toggleDecorationsIconElement); + this.starterSelectContainer.add(this.toggleDecorationsLabel); + + this.message = addTextObject(8, 8, "", TextStyle.WINDOW, { maxLines: 2 }); + this.message.setOrigin(0, 0); + this.starterSelectMessageBoxContainer.add(this.message); + + // arrow icon for the message box + this.initPromptSprite(this.starterSelectMessageBoxContainer); + + // Filter bar sits above everything, except the tutorial overlay and message box + this.starterSelectContainer.bringToTop(this.filterBarContainer); + this.initTutorialOverlay(this.starterSelectContainer); + this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer); + } + + show(args: any[]): boolean { + + if (!this.starterPreferences) { + this.starterPreferences = StarterPrefs.load(); + } + + this.pokerusSpecies = getPokerusStarters(); + + // When calling with "refresh", we do not reset the cursor and filters + if (args.length >= 1 && args[0] === "refresh") { + return false; + } + + super.show(args); + + this.starterSelectContainer.setVisible(true); + + this.getUi().bringToTop(this.starterSelectContainer); + + // Making caught pokemon visible icons, etc + this.allSpecies.forEach((species, s) => { + const icon = this.pokemonContainers[s].icon; + const dexEntry = globalScene.gameData.dexData[species.speciesId]; + + this.starterPreferences[species.speciesId] = this.initStarterPrefs(species); + + if (dexEntry.caughtAttr) { + icon.clearTint(); + } else if (dexEntry.seenAttr) { + icon.setTint(0x808080); + } + + this.setUpgradeAnimation(icon, species); + }); + + this.resetFilters(); + this.updateStarters(); + + this.setFilterMode(false); + this.filterBarCursor = 0; + this.setFilterTextMode(false); + this.filterTextCursor = 0; + this.setCursor(0); + + this.filterTextContainer.setVisible(true); + + return true; + } + + /** + * Get the starter attributes for the given PokemonSpecies, after sanitizing them. + * If somehow a preference is set for a form, variant, gender, ability or nature + * that wasn't actually unlocked or is invalid it will be cleared here + * + * @param species The species to get Starter Preferences for + * @returns StarterAttributes for the species + */ + initStarterPrefs(species: PokemonSpecies): StarterAttributes { + const starterAttributes = 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) { + return {}; + } + + const caughtAttr = dexEntry.caughtAttr; + + const hasShiny = caughtAttr & DexAttr.SHINY; + const hasNonShiny = caughtAttr & DexAttr.NON_SHINY; + if (starterAttributes.shiny && !hasShiny) { + // shiny form wasn't unlocked, purging shiny and variant setting + delete starterAttributes.shiny; + delete starterAttributes.variant; + } else if (starterAttributes.shiny === false && !hasNonShiny) { + // non shiny form wasn't unlocked, purging shiny setting + delete starterAttributes.shiny; + } + + if (starterAttributes.variant !== undefined) { + const unlockedVariants = [ + hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT, + hasShiny && caughtAttr & DexAttr.VARIANT_2, + hasShiny && caughtAttr & DexAttr.VARIANT_3 + ]; + if (isNaN(starterAttributes.variant) || starterAttributes.variant < 0 || !unlockedVariants[starterAttributes.variant]) { + // variant value is invalid or requested variant wasn't unlocked, purging setting + delete starterAttributes.variant; + } + } + + if (starterAttributes.female !== undefined) { + if (!(starterAttributes.female ? caughtAttr & DexAttr.FEMALE : caughtAttr & DexAttr.MALE)) { + // requested gender wasn't unlocked, purging setting + delete starterAttributes.female; + } + } + + if (starterAttributes.ability !== undefined) { + const speciesHasSingleAbility = species.ability2 === species.ability1; + const abilityAttr = starterData.abilityAttr; + const hasAbility1 = abilityAttr & AbilityAttr.ABILITY_1; + const hasAbility2 = abilityAttr & AbilityAttr.ABILITY_2; + const hasHiddenAbility = abilityAttr & AbilityAttr.ABILITY_HIDDEN; + // Due to a past bug it is possible that some Pokemon with a single ability have the ability2 flag + // In this case, we only count ability2 as valid if ability1 was not unlocked, otherwise we ignore it + const unlockedAbilities = [ + hasAbility1, + speciesHasSingleAbility ? hasAbility2 && !hasAbility1 : hasAbility2, + hasHiddenAbility + ]; + if (!unlockedAbilities[starterAttributes.ability]) { + // requested ability wasn't unlocked, purging setting + delete starterAttributes.ability; + } + } + + const selectedForm = starterAttributes.form; + if (selectedForm !== undefined && (!species.forms[selectedForm]?.isStarterSelectable || !(caughtAttr & globalScene.gameData.getFormAttr(selectedForm)))) { + // requested form wasn't unlocked/isn't a starter form, purging setting + delete starterAttributes.form; + } + + if (starterAttributes.nature !== undefined) { + const unlockedNatures = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr); + if (unlockedNatures.indexOf(starterAttributes.nature as unknown as Nature) < 0) { + // requested nature wasn't unlocked, purging setting + delete starterAttributes.nature; + } + } + + return starterAttributes; + } + + /** + * Set the selections for all filters to their default starting value + */ + resetFilters() : void { + this.filterBar.setValsToDefault(); + this.filterText.setValsToDefault(); + } + + showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer, moveToTop?: boolean) { + super.showText(text, delay, callback, callbackDelay, prompt, promptDelay); + + const singleLine = text?.indexOf("\n") === -1; + + this.starterSelectMessageBox.setSize(318, singleLine ? 28 : 42); + + if (moveToTop) { + this.starterSelectMessageBox.setOrigin(0, 0); + this.starterSelectMessageBoxContainer.setY(0); + this.message.setY(4); + } else { + this.starterSelectMessageBoxContainer.setY(globalScene.game.canvas.height / 6); + this.starterSelectMessageBox.setOrigin(0, 1); + this.message.setY(singleLine ? -22 : -37); + } + + this.starterSelectMessageBoxContainer.setVisible(!!text?.length); + } + + /** + * Determines if 'Icon' based upgrade notifications should be shown + * @returns true if upgrade notifications are enabled and set to display an 'Icon' + */ + isUpgradeIconEnabled(): boolean { + return globalScene.candyUpgradeNotification !== 0 && globalScene.candyUpgradeDisplay === 0; + } + /** + * Determines if 'Animation' based upgrade notifications should be shown + * @returns true if upgrade notifications are enabled and set to display an 'Animation' + */ + isUpgradeAnimationEnabled(): boolean { + return globalScene.candyUpgradeNotification !== 0 && globalScene.candyUpgradeDisplay === 1; + } + + getStarterSpeciesId(speciesId): number { + if (speciesStarterCosts.hasOwnProperty(speciesId)) { + return speciesId; + } else { + 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 + * @param species {@linkcode PokemonSpecies} of the icon used to check for upgrades + * @param startPaused Should this animation be paused after it is added? + */ + setUpgradeAnimation(icon: Phaser.GameObjects.Sprite, species: PokemonSpecies, startPaused: boolean = false): void { + globalScene.tweens.killTweensOf(icon); + // Skip animations if they are disabled + if (globalScene.candyUpgradeDisplay === 0 || species.speciesId !== species.getRootSpeciesId(false)) { + return; + } + + icon.y = 2; + + const tweenChain: Phaser.Types.Tweens.TweenChainBuilderConfig = { + targets: icon, + loop: -1, + // Make the initial bounce a little randomly delayed + delay: randIntRange(0, 50) * 5, + loopDelay: 1000, + tweens: [ + { + targets: icon, + y: 2 - 5, + duration: fixedInt(125), + ease: "Cubic.easeOut", + yoyo: true + }, + { + targets: icon, + y: 2 - 3, + duration: fixedInt(150), + ease: "Cubic.easeOut", + yoyo: true + } + ], }; + + const isPassiveAvailable = this.isPassiveAvailable(species.speciesId); + const isValueReductionAvailable = this.isValueReductionAvailable(species.speciesId); + const isSameSpeciesEggAvailable = this.isSameSpeciesEggAvailable(species.speciesId); + + // 'Passives Only' mode + if (globalScene.candyUpgradeNotification === 1) { + if (isPassiveAvailable) { + globalScene.tweens.chain(tweenChain).paused = startPaused; + } + // 'On' mode + } else if (globalScene.candyUpgradeNotification === 2) { + if (isPassiveAvailable || isValueReductionAvailable || isSameSpeciesEggAvailable) { + globalScene.tweens.chain(tweenChain).paused = startPaused; + } + } + } + + /** + * Sets the visibility of a Candy Upgrade Icon + */ + setUpgradeIcon(starter: PokedexMonContainer): void { + const species = starter.species; + const slotVisible = !!species?.speciesId; + + if (!species || globalScene.candyUpgradeNotification === 0 || species.speciesId !== species.getRootSpeciesId(false)) { + starter.candyUpgradeIcon.setVisible(false); + starter.candyUpgradeOverlayIcon.setVisible(false); + return; + } + + const isPassiveAvailable = this.isPassiveAvailable(species.speciesId); + const isValueReductionAvailable = this.isValueReductionAvailable(species.speciesId); + const isSameSpeciesEggAvailable = this.isSameSpeciesEggAvailable(species.speciesId); + + // 'Passive Only' mode + if (globalScene.candyUpgradeNotification === 1) { + starter.candyUpgradeIcon.setVisible(slotVisible && isPassiveAvailable); + starter.candyUpgradeOverlayIcon.setVisible(slotVisible && starter.candyUpgradeIcon.visible); + + // 'On' mode + } else if (globalScene.candyUpgradeNotification === 2) { + starter.candyUpgradeIcon.setVisible( + slotVisible && ( isPassiveAvailable || isValueReductionAvailable || isSameSpeciesEggAvailable )); + starter.candyUpgradeOverlayIcon.setVisible(slotVisible && starter.candyUpgradeIcon.visible); + } + } + + /** + * Update the display of candy upgrade icons or animations for the given PokedexMonContainer + * @param pokemonContainer the container for the Pokemon to update + */ + updateCandyUpgradeDisplay(pokemonContainer: PokedexMonContainer) { + if (this.isUpgradeIconEnabled() ) { + this.setUpgradeIcon(pokemonContainer); + } + if (this.isUpgradeAnimationEnabled()) { + this.setUpgradeAnimation(pokemonContainer.icon, this.lastSpecies, true); + } + } + + processInput(button: Button): boolean { + if (this.blockInput) { + return false; + } + + const maxColumns = 9; + // const maxRows = 9; + const numberOfStarters = this.filteredPokemonContainers.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.filteredPokemonContainers.length - 1, onScreenFirstIndex + maxRows * maxColumns - 1); // this is the last starter index on the screen + // const onScreenNumberOfStarters = onScreenLastIndex - onScreenFirstIndex + 1; + + // TODO: use the above to let the cursor go to the correct position when switching back. + + const ui = this.getUi(); + + let success = false; + let error = false; + + if (button === Button.SUBMIT) { + error = true; + } else if (button === Button.CANCEL) { + if (this.filterMode && this.filterBar.openDropDown) { + // CANCEL with a filter menu open > close it + this.filterBar.toggleDropDown(this.filterBarCursor); + + // if there are possible starters go the first one of the list + if (numberOfStarters > 0) { + this.setFilterMode(false); + this.scrollCursor = 0; + this.updateScroll(); + this.setCursor(0); + } + success = true; + + } else if (this.filterTextMode && !(this.filterText.getValue(this.filterTextCursor) === this.filterText.defaultText)) { + this.filterText.resetSelection(this.filterTextCursor); + success = true; + } else { + this.tryExit(); + success = true; + } + } else if (button === Button.STATS) { + if (!this.filterMode) { + this.cursorObj.setVisible(false); + this.setSpecies(null); + this.filterText.cursorObj.setVisible(false); + this.filterTextMode = false; + this.filterBarCursor = 0; + this.setFilterMode(true); + } + } else if (button === Button.V) { + if (!this.filterTextMode) { + this.cursorObj.setVisible(false); + this.setSpecies(null); + this.filterBar.cursorObj.setVisible(false); + this.filterMode = false; + this.filterTextCursor = 0; + this.setFilterTextMode(true); + } + } else if (button === Button.CYCLE_SHINY) { + this.showDecorations = !this.showDecorations; + this.updateScroll(); + success = true; + } 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); + 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 Pokemon in party if any + this.setFilterMode(false); + this.cursorObj.setVisible(false); + 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; + } + } else if (this.filterTextMode) { + switch (button) { + case Button.LEFT: + // LEFT from filter bar, move to right of Pokemon list + if (numberOfStarters > 0) { + this.setFilterTextMode(false); + const rowIndex = this.filterTextCursor; + this.setCursor(onScreenFirstIndex + (rowIndex < numOfRows - 1 ? (rowIndex + 1) * maxColumns - 1 : numberOfStarters - 1)); + success = true; + } + break; + case Button.RIGHT: + // RIGHT from filter bar, move to left of Pokemon list + if (numberOfStarters > 0) { + this.setFilterTextMode(false); + const rowIndex = this.filterTextCursor; + this.setCursor(onScreenFirstIndex + (rowIndex < numOfRows ? rowIndex * maxColumns : (numOfRows - 1) * maxColumns)); + success = true; + } + break; + case Button.UP: + if (this.filterTextCursor > 0) { + success = this.setCursor(this.filterTextCursor - 1); + } else { + success = this.setCursor(this.filterText.numFilters - 1); + } + break; + case Button.DOWN: + if (this.filterTextCursor < this.filterText.numFilters - 1) { + success = this.setCursor(this.filterTextCursor + 1); + } else { + success = this.setCursor(0); + } + break; + case Button.ACTION: + this.filterText.startSearch(this.filterTextCursor, this.getUi()); + success = true; + break; + } + } else { + + if (button === Button.ACTION) { + ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, 0); + success = true; + } else { + 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 - 9); + } else { + this.filterBarCursor = this.filterBar.getNearestFilter(this.filteredPokemonContainers[this.cursor]); + this.setFilterMode(true); + success = true; + } + break; + case Button.DOWN: + 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.filteredPokemonContainers[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 + this.filterTextCursor = this.filterText.getNearestFilter(this.filteredPokemonContainers[this.cursor]); + this.setFilterTextMode(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 + this.filterTextCursor = this.filterText.getNearestFilter(this.filteredPokemonContainers[this.cursor]); + this.setFilterTextMode(true); + success = true; + } + break; + } + } + } + + if (success) { + ui.playSelect(); + } else if (error) { + ui.playError(); + } + + return success || error; + } + + updateButtonIcon(iconSetting, gamepadType, iconElement, controlLabel): void { + let iconPath; + // touch controls cannot be rebound as is, and are just emulating a keyboard event. + // Additionally, since keyboard controls can be rebound (and will be displayed when they are), we need to have special handling for the touch controls + if (gamepadType === "touch") { + gamepadType = "keyboard"; + switch (iconSetting) { + case SettingKeyboard.Button_Cycle_Shiny: + iconPath = "R.png"; + break; + case SettingKeyboard.Button_Cycle_Variant: + iconPath = "V.png"; + break; + case SettingKeyboard.Button_Stats: + iconPath = "C.png"; + break; + default: + break; + } + } else { + iconPath = globalScene.inputController?.getIconForLatestInputRecorded(iconSetting); + } + iconElement.setTexture(gamepadType, iconPath); + iconElement.setVisible(true); + controlLabel.setVisible(true); + } + + updateFilterButtonIcon(iconSetting, gamepadType, iconElement, controlLabel): void { + let iconPath; + // touch controls cannot be rebound as is, and are just emulating a keyboard event. + // Additionally, since keyboard controls can be rebound (and will be displayed when they are), we need to have special handling for the touch controls + if (gamepadType === "touch") { + gamepadType = "keyboard"; + iconPath = "C.png"; + } else { + iconPath = globalScene.inputController?.getIconForLatestInputRecorded(iconSetting); + } + iconElement.setTexture(gamepadType, iconPath); + iconElement.setVisible(true); + controlLabel.setVisible(true); + } + + // TODO: add a toggle to return props (show shinies in dex or not) + getSanitizedProps(props: DexAttrProps): DexAttrProps { + const sanitizedProps: DexAttrProps = { + shiny: false, + female: props.female, + variant: 0, + formIndex: 0, + }; + return sanitizedProps; + } + + // Returns true if one of the forms has the requested move + hasFormLevelMove(form: PokemonForm, selectedMove: string): boolean { + if (!pokemonFormLevelMoves.hasOwnProperty(form.speciesId) || !pokemonFormLevelMoves[form.speciesId].hasOwnProperty(form.formIndex)) { + return false; + } else { + const levelMoves = pokemonFormLevelMoves[form.speciesId][form.formIndex].map(m => allMoves[m[1]].name); + return levelMoves.includes(selectedMove); + } + } + + updateStarters = () => { + this.scrollCursor = 0; + this.filteredPokemonContainers = []; + this.validPokemonContainers = []; + + this.pokerusCursorObjs.forEach(cursor => cursor.setVisible(false)); + + this.filterBar.updateFilterLabels(); + this.filterText.updateFilterLabels(); + + this.validPokemonContainers = this.pokemonContainers; + + // this updates icons for previously saved pokemon + for (let i = 0; i < this.validPokemonContainers.length; i++) { + const currentFilteredContainer = this.validPokemonContainers[i]; + const starterSprite = currentFilteredContainer.icon as Phaser.GameObjects.Sprite; + + const currentDexAttr = this.getCurrentDexProps(currentFilteredContainer.species.speciesId); + const props = this.getSanitizedProps(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); + } + + // filter + this.validPokemonContainers.forEach(container => { + container.setVisible(false); + + container.cost = globalScene.gameData.getSpeciesStarterValue(this.getStarterSpeciesId(container.species.speciesId)); + + // First, ensure you have the caught attributes for the species else default to bigint 0 + // TODO: This might be removed depending on how accessible we want the pokedex function to be + const caughtAttr = globalScene.gameData.dexData[container.species.speciesId]?.caughtAttr || BigInt(0); + const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(container.species.speciesId)]; + const isStarterProgressable = speciesEggMoves.hasOwnProperty(this.getStarterSpeciesId(container.species.speciesId)); + + // Name filter + const selectedName = this.filterText.getValue(FilterTextRow.NAME); + const fitsName = container.species.name === selectedName || selectedName === this.filterText.defaultText; + + // Move filter + // TODO: There can be fringe cases where the two moves belong to mutually exclusive forms, these must be handled separately (Pikachu); + // On the other hand, in some cases it is possible to switch between different forms and combine (Deoxys) + const levelMoves = pokemonSpeciesLevelMoves[container.species.speciesId].map(m => allMoves[m[1]].name); + // This always gets egg moves from the starter + const eggMoves = speciesEggMoves[this.getStarterSpeciesId(container.species.speciesId)]?.map(m => allMoves[m].name) ?? []; + const tmMoves = speciesTmMoves[container.species.speciesId]?.map(m => allMoves[m].name) ?? []; + const selectedMove1 = this.filterText.getValue(FilterTextRow.MOVE_1); + const selectedMove2 = this.filterText.getValue(FilterTextRow.MOVE_2); + + const fitsFormMove1 = container.species.forms.some(form => this.hasFormLevelMove(form, selectedMove1)); + const fitsFormMove2 = container.species.forms.some(form => this.hasFormLevelMove(form, selectedMove2)); + const fitsLevelMove1 = levelMoves.includes(selectedMove1) || fitsFormMove1; + const fitsEggMove1 = eggMoves.includes(selectedMove1); + const fitsTmMove1 = tmMoves.includes(selectedMove1); + const fitsLevelMove2 = levelMoves.includes(selectedMove2) || fitsFormMove2; + const fitsEggMove2 = eggMoves.includes(selectedMove2); + const fitsTmMove2 = tmMoves.includes(selectedMove2); + const fitsMove1 = fitsLevelMove1 || fitsEggMove1 || fitsTmMove1 || selectedMove1 === this.filterText.defaultText; + const fitsMove2 = fitsLevelMove2 || fitsEggMove2 || fitsTmMove2 || selectedMove2 === this.filterText.defaultText; + const fitsMoves = fitsMove1 && fitsMove2; + + container.eggMove1Icon.setVisible(false); + container.tmMove1Icon.setVisible(false); + container.eggMove2Icon.setVisible(false); + container.tmMove2Icon.setVisible(false); + if (fitsEggMove1 && !fitsLevelMove1) { + container.eggMove1Icon.setVisible(true); + } else if (fitsTmMove1 && !fitsLevelMove1) { + container.tmMove1Icon.setVisible(true); + } + if (fitsEggMove2 && !fitsLevelMove2) { + container.eggMove2Icon.setVisible(true); + } else if (fitsTmMove2 && !fitsLevelMove2) { + container.tmMove2Icon.setVisible(true); + } + + // Ability filter + const abilities = [ container.species.ability1, container.species.ability2, container.species.abilityHidden ].map(a => allAbilities[a].name); + const passive = starterPassiveAbilities[this.getStarterSpeciesId(container.species.speciesId)] ?? 0; + + const selectedAbility1 = this.filterText.getValue(FilterTextRow.ABILITY_1); + const fitsFormAbility = container.species.forms.some(form => allAbilities[form.ability1].name === selectedAbility1); + const fitsAbility1 = abilities.includes(selectedAbility1) || fitsFormAbility || selectedAbility1 === this.filterText.defaultText; + const fitsPassive1 = allAbilities[passive].name === selectedAbility1; + + const selectedAbility2 = this.filterText.getValue(FilterTextRow.ABILITY_2); + const fitsAbility2 = abilities.includes(selectedAbility2) || fitsFormAbility || selectedAbility2 === this.filterText.defaultText; + const fitsPassive2 = allAbilities[passive].name === selectedAbility2; + + // If both fields have been set to the same ability, show both ability and passive + const fitsAbilities = (fitsAbility1 && (fitsPassive2 || selectedAbility2 === this.filterText.defaultText)) || + (fitsAbility2 && (fitsPassive1 || selectedAbility1 === this.filterText.defaultText)); + + container.passive1Icon.setVisible(false); + container.passive2Icon.setVisible(false); + if (fitsPassive1) { + container.passive1Icon.setVisible(true); + } + if (fitsPassive2) { + container.passive2Icon.setVisible(true); + } + + // Gen filter + const fitsGen = this.filterBar.getVals(DropDownColumn.GEN).includes(container.species.generation); + + // Type filter + const fitsType = this.filterBar.getVals(DropDownColumn.TYPES).some(type => container.species.isOfType((type as number) - 1)); + + // Biome filter + const indexToBiome = new Map( + Object.values(Biome) + .map((value, index) => (typeof value === "string" ? [ index, value ] : undefined)) + .filter((entry): entry is [number, string] => entry !== undefined) + ); + indexToBiome.set(35, "Uncatchable"); + + // We get biomes for both the mon and its starters to ensure that evolutions get the correct filters. + // TODO: We might also need to do it the other way around. + const biomes = catchableSpecies[container.species.speciesId].concat(catchableSpecies[this.getStarterSpeciesId(container.species.speciesId)]).map(b => Biome[b.biome]); + //const biomes = catchableSpecies[container.species.speciesId].map(b => Biome[b.biome]); + // if (uncatchableSpecies.includes(container.species.speciesId) && biomes.length === 0) { + if (biomes.length === 0) { + biomes.push("Uncatchable"); + } + // Only show uncatchable mons if all biomes are selected. + // TODO: Have an entry for uncatchable mons. + const showNoBiome = (biomes.length === 0 && this.filterBar.getVals(DropDownColumn.BIOME).length === 36) ? true : false; + const fitsBiome = this.filterBar.getVals(DropDownColumn.BIOME).some(item => biomes.includes(indexToBiome.get(item) ?? "")) || showNoBiome; + + + // Caught / Shiny filter + const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY); + const isShinyCaught = !!(caughtAttr & DexAttr.SHINY); + const isVariant1Caught = isShinyCaught && !!(caughtAttr & DexAttr.DEFAULT_VARIANT); + const isVariant2Caught = isShinyCaught && !!(caughtAttr & DexAttr.VARIANT_2); + const isVariant3Caught = isShinyCaught && !!(caughtAttr & DexAttr.VARIANT_3); + const isUncaught = !isNonShinyCaught && !isVariant1Caught && !isVariant2Caught && !isVariant3Caught; + const fitsCaught = this.filterBar.getVals(DropDownColumn.CAUGHT).some(caught => { + if (caught === "SHINY3") { + return isVariant3Caught; + } else if (caught === "SHINY2") { + return isVariant2Caught && !isVariant3Caught; + } else if (caught === "SHINY") { + return isVariant1Caught && !isVariant2Caught && !isVariant3Caught; + } else if (caught === "NORMAL") { + return isNonShinyCaught && !isVariant1Caught && !isVariant2Caught && !isVariant3Caught; + } else if (caught === "UNCAUGHT") { + return isUncaught; + } + }); + + // Passive Filter + const isPassiveUnlocked = starterData.passiveAttr > 0; + const isPassiveUnlockable = this.isPassiveAvailable(container.species.speciesId) && !isPassiveUnlocked; + const fitsPassive = this.filterBar.getVals(DropDownColumn.UNLOCKS).some(unlocks => { + if (unlocks.val === "PASSIVE" && unlocks.state === DropDownState.ON) { + return isPassiveUnlocked; + } else if (unlocks.val === "PASSIVE" && unlocks.state === DropDownState.EXCLUDE) { + return isStarterProgressable && !isPassiveUnlocked; + } else if (unlocks.val === "PASSIVE" && unlocks.state === DropDownState.UNLOCKABLE) { + return isPassiveUnlockable; + } else if (unlocks.val === "PASSIVE" && unlocks.state === DropDownState.OFF) { + return true; + } + }); + + // Cost Reduction Filter + const isCostReducedByOne = starterData.valueReduction === 1; + const isCostReducedByTwo = starterData.valueReduction === 2; + const isCostReductionUnlockable = this.isValueReductionAvailable(container.species.speciesId); + const fitsCostReduction = this.filterBar.getVals(DropDownColumn.UNLOCKS).some(unlocks => { + if (unlocks.val === "COST_REDUCTION" && unlocks.state === DropDownState.ON) { + return isCostReducedByOne || isCostReducedByTwo; + } else if (unlocks.val === "COST_REDUCTION" && unlocks.state === DropDownState.ONE) { + return isCostReducedByOne; + } else if (unlocks.val === "COST_REDUCTION" && unlocks.state === DropDownState.TWO) { + return isCostReducedByTwo; + } else if (unlocks.val === "COST_REDUCTION" && unlocks.state === DropDownState.EXCLUDE) { + return isStarterProgressable && !(isCostReducedByOne || isCostReducedByTwo); + } else if (unlocks.val === "COST_REDUCTION" && unlocks.state === DropDownState.UNLOCKABLE) { + return isCostReductionUnlockable; + } else if (unlocks.val === "COST_REDUCTION" && unlocks.state === DropDownState.OFF) { + return true; + } + }); + + // Starter Filter + const isStarter = this.getStarterSpeciesId(container.species.speciesId) === container.species.speciesId; + const fitsStarter = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { + if (misc.val === "STARTER" && misc.state === DropDownState.ON) { + return isStarter; + } + if (misc.val === "STARTER" && misc.state === DropDownState.EXCLUDE) { + return !isStarter; + } + if (misc.val === "STARTER" && misc.state === DropDownState.OFF) { + return true; + } + }); + + // Favorite Filter + const isFavorite = this.starterPreferences[container.species.speciesId]?.favorite ?? false; + const fitsFavorite = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { + if (misc.val === "FAVORITE" && misc.state === DropDownState.ON) { + return isFavorite; + } + if (misc.val === "FAVORITE" && misc.state === DropDownState.EXCLUDE) { + return !isFavorite; + } + if (misc.val === "FAVORITE" && misc.state === DropDownState.OFF) { + return true; + } + }); + + // Ribbon / Classic Win Filter + const hasWon = starterData.classicWinCount > 0; + const hasNotWon = starterData.classicWinCount === 0; + const isUndefined = starterData.classicWinCount === undefined; + const fitsWin = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { + if (misc.val === "WIN" && misc.state === DropDownState.ON) { + return hasWon; + } else if (misc.val === "WIN" && misc.state === DropDownState.EXCLUDE) { + return hasNotWon || isUndefined; + } else if (misc.val === "WIN" && misc.state === DropDownState.OFF) { + return true; + } + }); + + // HA Filter + const speciesHasHiddenAbility = container.species.abilityHidden !== container.species.ability1 && container.species.abilityHidden !== Abilities.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) { + return hasHA; + } else if (misc.val === "HIDDEN_ABILITY" && misc.state === DropDownState.EXCLUDE) { + return speciesHasHiddenAbility && !hasHA; + } else if (misc.val === "HIDDEN_ABILITY" && misc.state === DropDownState.OFF) { + return true; + } + }); + + // Egg Purchasable Filter + const isEggPurchasable = this.isSameSpeciesEggAvailable(container.species.speciesId); + const fitsEgg = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { + if (misc.val === "EGG" && misc.state === DropDownState.ON) { + return isEggPurchasable; + } else if (misc.val === "EGG" && misc.state === DropDownState.EXCLUDE) { + return isStarterProgressable && !isEggPurchasable; + } else if (misc.val === "EGG" && misc.state === DropDownState.OFF) { + return true; + } + }); + + // 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); + } else if (misc.val === "POKERUS" && misc.state === DropDownState.EXCLUDE) { + return !this.pokerusSpecies.includes(container.species); + } else if (misc.val === "POKERUS" && misc.state === DropDownState.OFF) { + return true; + } + }); + + if (fitsName && fitsAbilities && fitsMoves && fitsGen && fitsBiome && fitsType && fitsCaught && fitsPassive && fitsCostReduction && fitsStarter && fitsFavorite && fitsWin && fitsHA && fitsEgg && fitsPokerus) { + this.filteredPokemonContainers.push(container); + } + }); + + this.starterSelectScrollBar.setTotalRows(Math.max(Math.ceil(this.filteredPokemonContainers.length / 9), 1)); + this.starterSelectScrollBar.setScrollCursor(0); + + // sort + const sort = this.filterBar.getVals(DropDownColumn.SORT)[0]; + this.filteredPokemonContainers.sort((a, b) => { + switch (sort.val) { + default: + break; + case SortCriteria.NUMBER: + return (a.species.speciesId - b.species.speciesId) * -sort.dir; + case SortCriteria.COST: + return (a.cost - b.cost) * -sort.dir; + case SortCriteria.CANDY: + const candyCountA = globalScene.gameData.starterData[a.species.speciesId].candyCount; + const candyCountB = globalScene.gameData.starterData[b.species.speciesId].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; + const avgIVsB = globalScene.gameData.dexData[b.species.speciesId].ivs.reduce((a, b) => a + b, 0) / globalScene.gameData.dexData[b.species.speciesId].ivs.length; + return (avgIVsA - avgIVsB) * -sort.dir; + case SortCriteria.NAME: + return a.species.name.localeCompare(b.species.name) * -sort.dir; + case SortCriteria.CAUGHT: + return (globalScene.gameData.dexData[a.species.speciesId].caughtCount - globalScene.gameData.dexData[b.species.speciesId].caughtCount) * -sort.dir; + case SortCriteria.HATCHED: + return (globalScene.gameData.dexData[this.getStarterSpeciesId(a.species.speciesId)].hatchedCount - globalScene.gameData.dexData[this.getStarterSpeciesId(b.species.speciesId)].hatchedCount) * -sort.dir; + } + return 0; + }); + + this.updateScroll(); + }; + + updateScroll = () => { + const maxColumns = 9; + const maxRows = 9; + const onScreenFirstIndex = this.scrollCursor * maxColumns; + const onScreenLastIndex = Math.min(this.filteredPokemonContainers.length - 1, onScreenFirstIndex + maxRows * maxColumns - 1); + + this.starterSelectScrollBar.setScrollCursor(this.scrollCursor); + + this.pokerusCursorObjs.forEach(cursorObj => cursorObj.setVisible(false)); + + let pokerusCursorIndex = 0; + this.filteredPokemonContainers.forEach((container, i) => { + const pos = calcStarterPosition(i, this.scrollCursor); + container.setPosition(pos.x, pos.y); + if (i < onScreenFirstIndex || i > onScreenLastIndex) { + container.setVisible(false); + return; + } else { + container.setVisible(true); + + if (this.showDecorations) { + + if (this.pokerusSpecies.includes(container.species)) { + this.pokerusCursorObjs[pokerusCursorIndex].setPosition(pos.x - 1, pos.y + 1); + this.pokerusCursorObjs[pokerusCursorIndex].setVisible(true); + pokerusCursorIndex++; + } + + const speciesId = container.species.speciesId; + this.updateStarterValueLabel(container); + + container.label.setVisible(true); + const speciesVariants = speciesId && globalScene.gameData.dexData[speciesId].caughtAttr & DexAttr.SHINY + ? [ DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3 ].filter(v => !!(globalScene.gameData.dexData[speciesId].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(!!globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].passiveAttr); + container.hiddenAbilityIcon.setVisible(!!globalScene.gameData.dexData[speciesId].caughtAttr && !!(globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].abilityAttr & 4)); + container.classicWinIcon.setVisible(globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].classicWinCount > 0); + container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false); + + // 'Candy Icon' mode + if (globalScene.candyUpgradeDisplay === 0) { + + if (!starterColors[this.getStarterSpeciesId(speciesId)]) { + // Default to white if no colors are found + starterColors[this.getStarterSpeciesId(speciesId)] = [ "ffffff", "ffffff" ]; + } + + // Set the candy colors + container.candyUpgradeIcon.setTint(argbFromRgba(rgbHexToRgba(starterColors[this.getStarterSpeciesId(speciesId)][0]))); + container.candyUpgradeOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(starterColors[this.getStarterSpeciesId(speciesId)][1]))); + + } else if (globalScene.candyUpgradeDisplay === 1) { + container.candyUpgradeIcon.setVisible(false); + container.candyUpgradeOverlayIcon.setVisible(false); + } + } else { + container.label.setVisible(false); + for (let v = 0; v < 3; v++) { + container.shinyIcons[v].setVisible(false); + } + container.starterPassiveBgs.setVisible(false); + container.hiddenAbilityIcon.setVisible(false); + container.classicWinIcon.setVisible(false); + container.favoriteIcon.setVisible(false); + + container.candyUpgradeIcon.setVisible(false); + container.candyUpgradeOverlayIcon.setVisible(false); + } + } + }); + }; + + setCursor(cursor: integer): boolean { + let changed = false; + + if (this.filterMode) { + changed = this.filterBarCursor !== cursor; + this.filterBarCursor = cursor; + this.filterBar.setCursor(cursor); + } else if (this.filterTextMode) { + changed = this.filterTextCursor !== cursor; + this.filterTextCursor = cursor; + this.filterText.setCursor(cursor); + } else { + cursor = Math.max(Math.min(this.filteredPokemonContainers.length - 1, cursor), 0); + changed = super.setCursor(cursor); + + const pos = calcStarterPosition(cursor, this.scrollCursor); + this.cursorObj.setPosition(pos.x - 1, pos.y + 1); + + const species = this.filteredPokemonContainers[cursor]?.species; + + if (species) { + this.setSpecies(species); + } + } + + return changed; + } + + setFilterMode(filterMode: boolean): boolean { + this.cursorObj.setVisible(!filterMode); + this.filterBar.cursorObj.setVisible(filterMode); + this.pokemonSprite.setVisible(false); + + if (filterMode !== this.filterMode) { + this.filterMode = filterMode; + this.setCursor(filterMode ? this.filterBarCursor : this.cursor); + if (filterMode) { + this.setSpecies(null); + } + return true; + } + return false; + } + + setFilterTextMode(filterTextMode: boolean): boolean { + this.cursorObj.setVisible(!filterTextMode); + this.filterText.cursorObj.setVisible(filterTextMode); + this.pokemonSprite.setVisible(false); + + if (filterTextMode !== this.filterTextMode) { + this.filterTextMode = filterTextMode; + this.setCursor(filterTextMode ? this.filterTextCursor : this.cursor); + if (filterTextMode) { + this.setSpecies(null); + } + return true; + } + return false; + } + + getFriendship(speciesId: number) { + let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship; + if (!currentFriendship || currentFriendship === undefined) { + currentFriendship = 0; + } + + const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[speciesId]); + + return { currentFriendship, friendshipCap }; + } + + setSpecies(species: PokemonSpecies | null) { + + this.speciesStarterDexEntry = species ? globalScene.gameData.dexData[species.speciesId] : null; + + if (!species && globalScene.ui.getTooltip().visible) { + globalScene.ui.hideTooltip(); + } + + if (this.lastSpecies) { + const dexAttr = this.getCurrentDexProps(this.lastSpecies.speciesId); + const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(this.lastSpecies, dexAttr)); + const speciesIndex = this.allSpecies.indexOf(this.lastSpecies); + const lastSpeciesIcon = this.pokemonContainers[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.pokemonContainers[speciesIndex].icon; + globalScene.tweens.getTweensOf(icon).forEach(tween => tween.resume()); + } + + this.lastSpecies = species!; // TODO: is this bang correct? + + if (species && (this.speciesStarterDexEntry?.seenAttr || this.speciesStarterDexEntry?.caughtAttr)) { + + this.pokemonNumberText.setText(i18next.t("pokedexUiHandler:pokemonNumber") + padInt(species.speciesId, 4)); + + this.pokemonNameText.setText(species.name); + + if (this.speciesStarterDexEntry?.caughtAttr) { + + // Pause the animation when the species is selected + const speciesIndex = this.allSpecies.indexOf(species); + const icon = this.pokemonContainers[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 speciesForm = getPokemonSpeciesForm(species.speciesId, 0); + this.setTypeIcons(speciesForm.type1, speciesForm.type2); + + this.setSpeciesDetails(species, {}); + + this.pokemonSprite.clearTint(); + + this.type1Icon.clearTint(); + this.type2Icon.clearTint(); + } else { + this.type1Icon.setVisible(true); + this.type2Icon.setVisible(true); + + this.setSpeciesDetails(species, { + forSeen: true + }); + this.pokemonSprite.setTint(0x808080); + } + } else { + this.pokemonNumberText.setText(species ? i18next.t("pokedexUiHandler:pokemonNumber") + padInt(species.speciesId, 4) : ""); + this.pokemonNameText.setText(species ? "???" : ""); + this.type1Icon.setVisible(false); + this.type2Icon.setVisible(false); + if (species) { + this.pokemonSprite.setTint(0x000000); + this.setSpeciesDetails(species, {}); + } + } + } + + setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void { + let { shiny, formIndex, female, variant } = options; + const forSeen: boolean = options.forSeen ?? false; + + // 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 = true; + + if (species?.forms?.find(f => f.formKey === "female")) { + if (female !== undefined) { + formIndex = female ? 1 : 0; + } else if (formIndex !== undefined) { + female = formIndex === 1; + } + } + + this.pokemonSprite.setVisible(false); + + if (this.assetLoadCancelled) { + this.assetLoadCancelled.value = true; + this.assetLoadCancelled = null; + } + + this.starterMoveset = null; + this.speciesStarterMoves = []; + + if (species) { + const dexEntry = globalScene.gameData.dexData[species.speciesId]; + + if (!dexEntry.caughtAttr) { + const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId))); + + 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; + } + } + + const assetLoadCancelled = new BooleanHolder(false); + this.assetLoadCancelled = assetLoadCancelled; + + if (shouldUpdateSprite) { + + species.loadAssets(female!, formIndex, shiny, variant, true).then(() => { // TODO: is this bang correct? + if (assetLoadCancelled.value) { + return; + } + this.assetLoadCancelled = null; + this.speciesLoaded.set(species.speciesId, true); + this.pokemonSprite.play(species.getSpriteKey(female!, formIndex, shiny, variant)); // TODO: is this bang correct? + this.pokemonSprite.setPipelineData("shiny", shiny); + this.pokemonSprite.setPipelineData("variant", variant); + this.pokemonSprite.setPipelineData("spriteKey", species.getSpriteKey(female!, formIndex, shiny, variant)); // TODO: is this bang correct? + this.pokemonSprite.setVisible(true); + }); + } else { + this.pokemonSprite.setVisible(!(this.filterMode || this.filterTextMode)); + } + + if (dexEntry.caughtAttr || forSeen) { + + const speciesForm = getPokemonSpeciesForm(species.speciesId, 0); // TODO: always selecting the first form + + this.setTypeIcons(speciesForm.type1, speciesForm.type2); + } else { + this.setTypeIcons(null, null); + } + } else { + this.setTypeIcons(null, null); + } + + if (!this.starterMoveset) { + this.starterMoveset = this.speciesStarterMoves.slice(0, 4) as StarterMoveset; + } + } + + setTypeIcons(type1: Type | null, type2: Type | null): void { + if (type1 !== null) { + this.type1Icon.setVisible(true); + this.type1Icon.setFrame(Type[type1].toLowerCase()); + } else { + this.type1Icon.setVisible(false); + } + if (type2 !== null) { + this.type2Icon.setVisible(true); + this.type2Icon.setFrame(Type[type2].toLowerCase()); + } else { + this.type2Icon.setVisible(false); + } + } + + updateStarterValueLabel(starter: PokedexMonContainer): void { + const speciesId = starter.species.speciesId; + const baseStarterValue = speciesStarterCosts[speciesId]; + const starterValue = globalScene.gameData.getSpeciesStarterValue(this.getStarterSpeciesId(speciesId)); + starter.cost = starterValue; + let valueStr = starterValue.toString(); + if (valueStr.startsWith("0.")) { + valueStr = valueStr.slice(1); + } + starter.label.setText(valueStr); + let textStyle: TextStyle; + switch (baseStarterValue - starterValue) { + case 0: + textStyle = TextStyle.WINDOW; + break; + case 1: + case 0.5: + textStyle = TextStyle.SUMMARY_BLUE; + break; + default: + textStyle = TextStyle.SUMMARY_GOLD; + break; + } + if (baseStarterValue - starterValue > 0) { + starter.label.setColor(this.getTextColor(textStyle)); + starter.label.setShadowColor(this.getTextColor(textStyle, true)); + } + } + + tryExit(): boolean { + this.blockInput = true; + const ui = this.getUi(); + + const cancel = () => { + ui.setMode(Mode.POKEDEX, "refresh"); + this.clearText(); + this.blockInput = false; + }; + ui.showText(i18next.t("pokedexUiHandler:confirmExit"), null, () => { + ui.setModeWithoutClear(Mode.CONFIRM, () => { + ui.setMode(Mode.POKEDEX, "refresh"); + globalScene.clearPhaseQueue(); + this.clearText(); + this.clear(); + ui.revertMode(); + }, cancel, null, null, 19); + }); + + return true; + } + + + /** + * Creates a temporary dex attr props that will be used 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 caughtAttr = globalScene.gameData.dexData[speciesId].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 { + /* This calculates the correct variant if there's no starter preferences for it. + * This gets the highest tier variant that you've caught and adds it to the temp props + */ + if ((caughtAttr & DexAttr.VARIANT_3) > 0) { + props += DexAttr.VARIANT_3; + } else if ((caughtAttr & DexAttr.VARIANT_2) > 0) { + props += DexAttr.VARIANT_2; + } else { + props += DexAttr.DEFAULT_VARIANT; + } + } + } else { + props += DexAttr.NON_SHINY; + props += DexAttr.DEFAULT_VARIANT; // we add the default variant here because non shiny versions are listed as default variant + } + if (this.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; + } + + clearText() { + this.starterSelectMessageBoxContainer.setVisible(false); + super.clearText(); + } + + clear(): void { + super.clear(); + + // StarterPrefs.save(this.starterPreferences); + this.cursor = -1; + globalScene.ui.hideTooltip(); + + this.starterSelectContainer.setVisible(false); + this.blockInput = false; + } + + checkIconId(icon: Phaser.GameObjects.Sprite, species: PokemonSpecies, female: boolean, formIndex: number, shiny: boolean, variant: number) { + if (icon.frame.name !== species.getIconId(female, formIndex, shiny, variant)) { + console.log(`${species.name}'s icon ${icon.frame.name} does not match getIconId with female: ${female}, formIndex: ${formIndex}, shiny: ${shiny}, variant: ${variant}`); + icon.setTexture(species.getIconAtlasKey(formIndex, false, variant)); + icon.setFrame(species.getIconId(female, formIndex, false, variant)); + } + } +} diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index dd427802083..c7b33321089 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -1981,6 +1981,21 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } }); } + options.push({ + label: i18next.t("menuUiHandler:POKEDEX"), + handler: () => { + ui.setMode(Mode.STARTER_SELECT).then(() => { + const attributes = { + shiny: starterAttributes.shiny, + variant: starterAttributes.variant, + form: starterAttributes.form, + female: starterAttributes.female + }; + ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, starterAttributes.form, attributes); + return true; + }); + } + }); options.push({ label: i18next.t("menu:cancel"), handler: () => { diff --git a/src/ui/text.ts b/src/ui/text.ts index cdd1142e7ee..6e28a486660 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -42,6 +42,7 @@ export enum TextStyle { PERFECT_IV, ME_OPTION_DEFAULT, // Default style for choices in ME ME_OPTION_SPECIAL, // Style for choices with special requirements in ME + SHADOW_TEXT // To obscure unavailable options } export interface TextStyleOptions { @@ -359,6 +360,12 @@ export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: Ui return !shadow ? "#f8b050" : "#c07800"; // Gold } return !shadow ? "#78c850" : "#306850"; // Green + // Leaving the logic in place, in case someone wants to pick an even darker hue for the shadow down the line + case TextStyle.SHADOW_TEXT: + if (isLegacyTheme) { + return !shadow ? "#d0d0c8" : "#d0d0c8"; + } + return !shadow ? "#6b5a73" : "#6b5a73"; } } diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 9e8c52b1d24..98ac33a1226 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -23,6 +23,7 @@ import OptionSelectUiHandler from "./settings/option-select-ui-handler"; import EggHatchSceneHandler from "./egg-hatch-scene-handler"; import EggListUiHandler from "./egg-list-ui-handler"; import EggGachaUiHandler from "./egg-gacha-ui-handler"; +import PokedexUiHandler from "./pokedex-ui-handler"; import { addWindow } from "./ui-theme"; import LoginFormUiHandler from "./login-form-ui-handler"; import RegistrationFormUiHandler from "./registration-form-ui-handler"; @@ -53,6 +54,8 @@ import TestDialogueUiHandler from "#app/ui/test-dialogue-ui-handler"; import AutoCompleteUiHandler from "./autocomplete-ui-handler"; import { Device } from "#enums/devices"; import MysteryEncounterUiHandler from "./mystery-encounter-ui-handler"; +import PokedexScanUiHandler from "./pokedex-scan-ui-handler"; +import PokedexPageUiHandler from "./pokedex-page-ui-handler"; import { NavigationManager } from "./settings/navigationMenu"; export enum Mode { @@ -85,6 +88,9 @@ export enum Mode { GAME_STATS, EGG_LIST, EGG_GACHA, + POKEDEX, + POKEDEX_SCAN, + POKEDEX_PAGE, LOGIN_FORM, REGISTRATION_FORM, LOADING, @@ -109,6 +115,8 @@ const transitionModes = [ Mode.EGG_HATCH_SCENE, Mode.EGG_LIST, Mode.EGG_GACHA, + Mode.POKEDEX, + Mode.POKEDEX_PAGE, Mode.CHALLENGE_SELECT, Mode.RUN_HISTORY, ]; @@ -128,6 +136,7 @@ const noTransitionModes = [ Mode.SETTINGS_KEYBOARD, Mode.ACHIEVEMENTS, Mode.GAME_STATS, + Mode.POKEDEX_SCAN, Mode.LOGIN_FORM, Mode.REGISTRATION_FORM, Mode.LOADING, @@ -193,6 +202,9 @@ export default class UI extends Phaser.GameObjects.Container { new GameStatsUiHandler(), new EggListUiHandler(), new EggGachaUiHandler(), + new PokedexUiHandler(), + new PokedexScanUiHandler(Mode.TEST_DIALOGUE), + new PokedexPageUiHandler(), new LoginFormUiHandler(), new RegistrationFormUiHandler(), new LoadingModalUiHandler(), @@ -558,6 +570,7 @@ export default class UI extends Phaser.GameObjects.Container { revertMode(): Promise { return new Promise(resolve => { + if (!this?.modeChain?.length) { return resolve(false); }