diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index 0f4926cf7c7..adeb4de5054 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -101,6 +101,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[] } @@ -7715,6 +7727,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]; @@ -7724,6 +7740,12 @@ export function initBiomes() { : [ b[2] ] : [ TimeOfDay.ALL ]; + catchableSpecies[speciesId].push({ + biome: biome, + tier: tier, + tod: timesOfDay + }); + for (const tod of timesOfDay) { if (!biomePokemonPools.hasOwnProperty(biome) || !biomePokemonPools[biome].hasOwnProperty(tier) || !biomePokemonPools[biome][tier].hasOwnProperty(tod)) { continue; diff --git a/src/ui/dropdown.ts b/src/ui/dropdown.ts index e16efe17036..81526722073 100644 --- a/src/ui/dropdown.ts +++ b/src/ui/dropdown.ts @@ -2,6 +2,7 @@ import BattleScene from "#app/battle-scene"; import { SceneBase } from "#app/scene-base"; 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 { @@ -282,21 +283,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(scene: BattleScene, 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(scene, 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 = scene.add.image(optionPaddingX + 3, 0, "cursor"); + this.cursorObj = scene.add.image(this.optionPaddingX + 3, 0, "cursor"); this.cursorObj.setScale(0.5); this.cursorObj.setOrigin(0, 0.5); this.cursorObj.setVisible(false); @@ -306,31 +323,51 @@ export class DropDown extends Phaser.GameObjects.Container { this.options.unshift(new DropDownOption(scene, "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(scene, 0, 0, optionWidth, options[options.length - 1].y + optionHeight + optionPaddingY, false, false, undefined, undefined, WindowVariant.XTHIN); + this.window = addWindow(scene, 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(scene, 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 { @@ -360,6 +397,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; @@ -382,6 +424,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: @@ -586,7 +663,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 bcf7409fce0..eeee7b90793 100644 --- a/src/ui/filter-bar.ts +++ b/src/ui/filter-bar.ts @@ -8,6 +8,7 @@ import { addWindow, WindowVariant } from "./ui-theme"; export enum DropDownColumn { GEN, TYPES, + BIOME, CAUGHT, UNLOCKS, MISC, @@ -96,7 +97,7 @@ 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 paddingX = 0; const cursorOffset = 8; let totalWidth = paddingX * 2 + cursorOffset; diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index ec2894148f6..01a1fd7aea5 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -1,4 +1,4 @@ -import { pokemonPrevolutions, pokemonStarters } from "#app/data/balance/pokemon-evolutions"; +import { EvolutionItem, pokemonEvolutions, pokemonPrevolutions, pokemonStarters, SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import { Variant, getVariantTint, getVariantIcon } from "#app/data/variant"; import { argbFromRgba } from "@material/material-color-utilities"; import i18next from "i18next"; @@ -44,6 +44,9 @@ import type { Nature } from "#enums/nature"; import BgmBar from "./bgm-bar"; import * as Utils from "../utils"; import { speciesTmMoves } from "#app/data/balance/tms"; +import { BiomePoolTier, BiomeTierTod, catchableSpecies } from "#app/data/balance/biomes"; +import { Biome } from "#app/enums/biome"; +import { TimeOfDay } from "#app/enums/time-of-day"; export type StarterSelectCallback = (starters: Starter[]) => void; @@ -235,6 +238,10 @@ export default class PokedexPageUiHandler extends MessageUiHandler { private passive: Ability; private hasPassive: boolean; private hasAbilities: number[]; + private biomes: BiomeTierTod[]; + private baseStats: number[]; + private baseTotal: number; + private evolutions: SpeciesFormEvolution[]; private speciesStarterDexEntry: DexEntry | null; private speciesStarterMoves: Moves[]; @@ -498,7 +505,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { // The font size should be set per language const instructionTextSize = textSettings.instructionTextSize; - this.instructionsContainer = this.scene.add.container(4, 156); + this.instructionsContainer = this.scene.add.container(4, 128); this.instructionsContainer.setVisible(true); this.starterSelectContainer.add(this.instructionsContainer); @@ -724,7 +731,13 @@ export default class PokedexPageUiHandler extends MessageUiHandler { hasAbility2, hasHiddenAbility ]; - console.log(this.hasAbilities); + + this.biomes = catchableSpecies[species.speciesId]; + + this.baseStats = species.baseStats; + this.baseTotal = species.baseTotal; + + this.evolutions = pokemonEvolutions[species.speciesId]; } /** @@ -1036,6 +1049,55 @@ export default class PokedexPageUiHandler extends MessageUiHandler { console.log("Cursor", this.cursor); switch (this.cursor) { + + case MenuOptions.BASE_STATS: + + this.blockInput = true; + console.log("level moves", MenuOptions.LEVEL_MOVES); + + ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => { + ui.showText(i18next.t("pokedexUiHandler:baseStats"), null, () => { + const options: any[] = []; + const shortStats = [ "HP", "ATK", "DEF", "SPATK", "SPDEF", "SPD" ]; + + this.baseStats.map((bst, index) => { + options.push({ + label: i18next.t(`pokemonInfo:Stat.${shortStats[index]}shortened`).padEnd(5, " ") + ": " + `${bst}`, + handler: () => { + return false; + } + }); + }); + options.push({ + label: i18next.t("pokedexUiHandler:baseTotal") + ": " + `${this.baseTotal}`, + color: "#ccbe00", + handler: () => { + return 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; + }); + }); + break; + case MenuOptions.LEVEL_MOVES: this.blockInput = true; @@ -1278,6 +1340,105 @@ export default class PokedexPageUiHandler extends MessageUiHandler { }); break; + case MenuOptions.BIOMES: + + this.blockInput = true; + + ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => { + + const options: any[] = []; + + ui.showText(i18next.t("pokedexUiHandler:abilities"), 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 + }); + }); + + 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; + }); + }); + break; + + case MenuOptions.EVOLUTIONS: + + this.blockInput = true; + + ui.setMode(Mode.POKEDEX_PAGE, "refresh").then(() => { + + const options: any[] = []; + + ui.showText(i18next.t("pokedexUiHandler:abilities"), null, () => { + + if (!this.evolutions) { + this.blockInput = false; + return true; + } + if (this.evolutions.length === 0) { + this.blockInput = false; + return true; + } + + this.evolutions.map(evo => { + console.log(evo); + console.log(Species[evo.speciesId]); + options.push({ + label: i18next.t(`pokemon:${Species[evo.speciesId].toUpperCase()}`), + color: "#ccbe00", + handler: () => false + }); + options.push({ + label: evo.level > 1 ? `${evo.level}` : (evo.item ? i18next.t(`modifier-type:EvolutionItem.${EvolutionItem[evo.item].toUpperCase()}`) : ""), + skip: true, + 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; + }); + }); + break; + case MenuOptions.TOGGLE_IVS: this.toggleStatsMode(); ui.setMode(Mode.POKEDEX_PAGE, "refresh"); diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index 2bfe7afa06f..335c21fac54 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -7,6 +7,7 @@ import { speciesEggMoves } from "#app/data/balance/egg-moves"; import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import PokemonSpecies, { allSpecies, getPokemonSpeciesForm, getPokerusStarters, PokemonForm } 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 { AbilityAttr, DexAttr, DexAttrProps, DexEntry, StarterMoveset, StarterAttributes, StarterPreferences, StarterPrefs } from "#app/system/game-data"; import { Tutorial, handleTutorial } from "#app/tutorial"; @@ -38,6 +39,7 @@ 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"; // We don't need this interface here @@ -352,7 +354,7 @@ export default class PokedexUiHandler extends MessageUiHandler { // Create and initialise filter bar this.filterBarContainer = this.scene.add.container(0, 0); - this.filterBar = new FilterBar(this.scene, speciesContainerX, 1, 175, filterBarHeight); + this.filterBar = new FilterBar(this.scene, speciesContainerX - 10, 1, 175 + 10, filterBarHeight); // gen filter const genOptions: DropDownOption[] = [ @@ -383,6 +385,15 @@ export default class PokedexUiHandler extends MessageUiHandler { }); this.filterBar.addFilter(DropDownColumn.TYPES, i18next.t("filterBar:typeFilter"), new DropDown(this.scene, 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(this.scene, index, new DropDownLabel(i18next.t(`biome:${Biome[biomeValue].toUpperCase()}`))) + ); + const biomeDropDown: DropDown = new DropDown(this.scene, 0, 0, biomeOptions, this.updateStarters, DropDownType.HYBRID); + this.filterBar.addFilter(DropDownColumn.BIOME, i18next.t("filterBar:biomeFilter"), biomeDropDown); + // caught filter const shiny1Sprite = this.scene.add.sprite(0, 0, "shiny_icons"); shiny1Sprite.setOrigin(0.15, 0.2); @@ -1361,6 +1372,18 @@ export default class PokedexUiHandler extends MessageUiHandler { // 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) => [ index, value ]) + ); + + // 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]); + const fitsBiome = this.filterBar.getVals(DropDownColumn.BIOME).some(item => biomes.includes(indexToBiome.get(item))); + + // Caught / Shiny filter const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY); const isShinyCaught = !!(caughtAttr & DexAttr.SHINY); @@ -1476,7 +1499,7 @@ export default class PokedexUiHandler extends MessageUiHandler { } }); - if (fitsName && fitsAbilities && fitsMoves && fitsGen && fitsType && fitsCaught && fitsPassive && fitsCostReduction && fitsFavorite && fitsWin && fitsHA && fitsEgg && fitsPokerus) { + if (fitsName && fitsAbilities && fitsMoves && fitsGen && fitsBiome && fitsType && fitsCaught && fitsPassive && fitsCostReduction && fitsFavorite && fitsWin && fitsHA && fitsEgg && fitsPokerus) { this.filteredStarterContainers.push(container); } });