[UI] Make Egg List and Egg Summary scrollable (#4391)

This commit is contained in:
MokaStitcher 2024-09-26 10:51:49 +02:00 committed by GitHub
parent 06331ccdf6
commit a25ccbcde6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 496 additions and 211 deletions

View File

@ -1,16 +1,21 @@
import BattleScene from "../battle-scene"; import BattleScene from "#app/battle-scene";
import { Mode } from "./ui"; import { Mode } from "#app/ui/ui";
import PokemonIconAnimHandler, { PokemonIconAnimMode } from "./pokemon-icon-anim-handler"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler";
import { TextStyle, addTextObject } from "./text"; import { TextStyle, addTextObject } from "#app/ui/text";
import MessageUiHandler from "./message-ui-handler"; import MessageUiHandler from "#app/ui/message-ui-handler";
import { Egg } from "../data/egg"; import { addWindow } from "#app/ui/ui-theme";
import { addWindow } from "./ui-theme";
import {Button} from "#enums/buttons"; import {Button} from "#enums/buttons";
import i18next from "i18next"; import i18next from "i18next";
import ScrollableGridUiHandler from "#app/ui/scrollable-grid-handler";
import { ScrollBar } from "#app/ui/scroll-bar";
export default class EggListUiHandler extends MessageUiHandler { export default class EggListUiHandler extends MessageUiHandler {
private readonly ROWS = 9;
private readonly COLUMNS = 11;
private eggListContainer: Phaser.GameObjects.Container; private eggListContainer: Phaser.GameObjects.Container;
private eggListIconContainer: Phaser.GameObjects.Container; private eggListIconContainer: Phaser.GameObjects.Container;
private eggIcons: Phaser.GameObjects.Sprite[];
private eggSprite: Phaser.GameObjects.Sprite; private eggSprite: Phaser.GameObjects.Sprite;
private eggNameText: Phaser.GameObjects.Text; private eggNameText: Phaser.GameObjects.Text;
private eggDateText: Phaser.GameObjects.Text; private eggDateText: Phaser.GameObjects.Text;
@ -19,6 +24,7 @@ export default class EggListUiHandler extends MessageUiHandler {
private eggListMessageBoxContainer: Phaser.GameObjects.Container; private eggListMessageBoxContainer: Phaser.GameObjects.Container;
private cursorObj: Phaser.GameObjects.Image; private cursorObj: Phaser.GameObjects.Image;
private scrollGridHandler : ScrollableGridUiHandler;
private iconAnimHandler: PokemonIconAnimHandler; private iconAnimHandler: PokemonIconAnimHandler;
@ -64,7 +70,7 @@ export default class EggListUiHandler extends MessageUiHandler {
this.eggGachaInfoText.setWordWrapWidth(540); this.eggGachaInfoText.setWordWrapWidth(540);
this.eggListContainer.add(this.eggGachaInfoText); this.eggListContainer.add(this.eggGachaInfoText);
this.eggListIconContainer = this.scene.add.container(115, 9); this.eggListIconContainer = this.scene.add.container(113, 5);
this.eggListContainer.add(this.eggListIconContainer); this.eggListContainer.add(this.eggListIconContainer);
this.cursorObj = this.scene.add.image(0, 0, "select_cursor"); this.cursorObj = this.scene.add.image(0, 0, "select_cursor");
@ -74,6 +80,14 @@ export default class EggListUiHandler extends MessageUiHandler {
this.eggSprite = this.scene.add.sprite(54, 37, "egg"); this.eggSprite = this.scene.add.sprite(54, 37, "egg");
this.eggListContainer.add(this.eggSprite); this.eggListContainer.add(this.eggSprite);
const scrollBar = new ScrollBar(this.scene, 310, 5, 4, 170, this.ROWS);
this.eggListContainer.add(scrollBar);
this.scrollGridHandler = new ScrollableGridUiHandler(this, this.ROWS, this.COLUMNS)
.withScrollBar(scrollBar)
.withUpdateGridCallBack(() => this.updateEggIcons())
.withUpdateSingleElementCallback((i:number) => this.setEggDetails(i));
this.eggListMessageBoxContainer = this.scene.add.container(0, this.scene.game.canvas.height / 6); this.eggListMessageBoxContainer = this.scene.add.container(0, this.scene.game.canvas.height / 6);
this.eggListMessageBoxContainer.setVisible(false); this.eggListMessageBoxContainer.setVisible(false);
this.eggListContainer.add(this.eggListMessageBoxContainer); this.eggListContainer.add(this.eggListMessageBoxContainer);
@ -92,76 +106,63 @@ export default class EggListUiHandler extends MessageUiHandler {
show(args: any[]): boolean { show(args: any[]): boolean {
super.show(args); super.show(args);
this.initEggIcons();
this.getUi().bringToTop(this.eggListContainer); this.getUi().bringToTop(this.eggListContainer);
this.eggListContainer.setVisible(true); this.eggListContainer.setVisible(true);
let e = 0; this.scrollGridHandler.setTotalElements(this.scene.gameData.eggs.length);
for (const egg of this.scene.gameData.eggs) {
const x = (e % 11) * 18;
const y = Math.floor(e / 11) * 18;
const icon = this.scene.add.sprite(x - 2, y + 2, "egg_icons");
icon.setScale(0.5);
icon.setOrigin(0, 0);
icon.setFrame(egg.getKey());
this.eggListIconContainer.add(icon);
this.iconAnimHandler.addOrUpdate(icon, PokemonIconAnimMode.NONE);
e++;
}
this.updateEggIcons();
this.setCursor(0); this.setCursor(0);
return true; return true;
} }
processInput(button: Button): boolean { /**
const ui = this.getUi(); * Create the grid of egg icons to display
*/
let success = false; private initEggIcons() {
const error = false; this.eggIcons = [];
for (let i = 0; i < Math.min(this.ROWS * this.COLUMNS, this.scene.gameData.eggs.length); i++) {
if (button === Button.CANCEL) { const x = (i % this.COLUMNS) * 18;
ui.revertMode(); const y = Math.floor(i / this.COLUMNS) * 18;
success = true; const icon = this.scene.add.sprite(x - 2, y + 2, "egg_icons");
} else { icon.setScale(0.5);
const eggCount = this.eggListIconContainer.getAll().length; icon.setOrigin(0, 0);
const rows = Math.ceil(eggCount / 11); this.eggListIconContainer.add(icon);
const row = Math.floor(this.cursor / 11); this.eggIcons.push(icon);
switch (button) {
case Button.UP:
if (row) {
success = this.setCursor(this.cursor - 11);
}
break;
case Button.DOWN:
if (row < rows - 2 || (row < rows - 1 && this.cursor % 11 <= (eggCount - 1) % 11)) {
success = this.setCursor(this.cursor + 11);
}
break;
case Button.LEFT:
if (this.cursor % 11) {
success = this.setCursor(this.cursor - 1);
}
break;
case Button.RIGHT:
if (this.cursor % 11 < (row < rows - 1 ? 10 : (eggCount - 1) % 11)) {
success = this.setCursor(this.cursor + 1);
}
break;
}
} }
if (success) {
ui.playSelect();
} else if (error) {
ui.playError();
}
return success || error;
} }
setEggDetails(egg: Egg): void { /**
* Show the grid of egg icons
*/
private updateEggIcons() {
const indexOffset = this.scrollGridHandler.getItemOffset();
const eggsToShow = Math.min(this.eggIcons.length, this.scene.gameData.eggs.length - indexOffset);
this.eggIcons.forEach((icon, i) => {
if (i !== this.cursor) {
this.iconAnimHandler.addOrUpdate(icon, PokemonIconAnimMode.NONE);
}
if (i < eggsToShow) {
const egg = this.scene.gameData.eggs[i + indexOffset];
icon.setFrame(egg.getKey());
icon.setVisible(true);
} else {
icon.setVisible(false);
}
});
}
/**
* Update the information panel with the information of the given egg
* @param index which egg in the list to display the info for
*/
private setEggDetails(index: number): void {
const egg = this.scene.gameData.eggs[index];
this.eggSprite.setFrame(`egg_${egg.getKey()}`); this.eggSprite.setFrame(`egg_${egg.getKey()}`);
this.eggNameText.setText(`${i18next.t("egg:egg")} (${egg.getEggDescriptor()})`); this.eggNameText.setText(`${i18next.t("egg:egg")} (${egg.getEggDescriptor()})`);
this.eggDateText.setText( this.eggDateText.setText(
@ -176,7 +177,29 @@ export default class EggListUiHandler extends MessageUiHandler {
this.eggGachaInfoText.setText(egg.getEggTypeDescriptor(this.scene)); this.eggGachaInfoText.setText(egg.getEggTypeDescriptor(this.scene));
} }
setCursor(cursor: integer): boolean { processInput(button: Button): boolean {
const ui = this.getUi();
let success = false;
const error = false;
if (button === Button.CANCEL) {
ui.revertMode();
success = true;
} else {
success = this.scrollGridHandler.processInput(button);
}
if (success) {
ui.playSelect();
} else if (error) {
ui.playError();
}
return success || error;
}
setCursor(cursor: number): boolean {
let changed = false; let changed = false;
const lastCursor = this.cursor; const lastCursor = this.cursor;
@ -184,14 +207,15 @@ export default class EggListUiHandler extends MessageUiHandler {
changed = super.setCursor(cursor); changed = super.setCursor(cursor);
if (changed) { if (changed) {
this.cursorObj.setPosition(114 + 18 * (cursor % 11), 10 + 18 * Math.floor(cursor / 11)); const icon = this.eggIcons[cursor];
this.cursorObj.setPositionRelative(icon, 114, 5);
if (lastCursor > -1) { if (lastCursor > -1) {
this.iconAnimHandler.addOrUpdate(this.eggListIconContainer.getAt(lastCursor) as Phaser.GameObjects.Sprite, PokemonIconAnimMode.NONE); this.iconAnimHandler.addOrUpdate(this.eggIcons[lastCursor], PokemonIconAnimMode.NONE);
} }
this.iconAnimHandler.addOrUpdate(this.eggListIconContainer.getAt(cursor) as Phaser.GameObjects.Sprite, PokemonIconAnimMode.ACTIVE); this.iconAnimHandler.addOrUpdate(icon, PokemonIconAnimMode.ACTIVE);
this.setEggDetails(this.scene.gameData.eggs[cursor]); this.setEggDetails(cursor + this.scrollGridHandler.getItemOffset());
} }
return changed; return changed;
@ -199,9 +223,11 @@ export default class EggListUiHandler extends MessageUiHandler {
clear(): void { clear(): void {
super.clear(); super.clear();
this.scrollGridHandler.reset();
this.cursor = -1; this.cursor = -1;
this.eggListContainer.setVisible(false); this.eggListContainer.setVisible(false);
this.iconAnimHandler.removeAll(); this.iconAnimHandler.removeAll();
this.eggListIconContainer.removeAll(true); this.eggListIconContainer.removeAll(true);
this.eggIcons = [];
} }
} }

View File

@ -3,17 +3,17 @@ import { Mode } from "./ui";
import PokemonIconAnimHandler, { PokemonIconAnimMode } from "./pokemon-icon-anim-handler"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "./pokemon-icon-anim-handler";
import MessageUiHandler from "./message-ui-handler"; import MessageUiHandler from "./message-ui-handler";
import { getEggTierForSpecies } from "../data/egg"; import { getEggTierForSpecies } from "../data/egg";
import {Button} from "#enums/buttons"; import { Button } from "#enums/buttons";
import { Gender } from "#app/data/gender";
import { getVariantTint } from "#app/data/variant";
import { EggTier } from "#app/enums/egg-type";
import PokemonHatchInfoContainer from "./pokemon-hatch-info-container"; import PokemonHatchInfoContainer from "./pokemon-hatch-info-container";
import { EggSummaryPhase } from "#app/phases/egg-summary-phase"; import { EggSummaryPhase } from "#app/phases/egg-summary-phase";
import { DexAttr } from "#app/system/game-data";
import { EggHatchData } from "#app/data/egg-hatch-data"; import { EggHatchData } from "#app/data/egg-hatch-data";
import ScrollableGridUiHandler from "./scrollable-grid-handler";
import { HatchedPokemonContainer } from "./hatched-pokemon-container";
import { ScrollBar } from "#app/ui/scroll-bar";
const iconContainerX = 115; const iconContainerX = 112;
const iconContainerY = 9; const iconContainerY = 9;
const numRows = 9;
const numCols = 11; const numCols = 11;
const iconSize = 18; const iconSize = 18;
@ -27,20 +27,20 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
private eggHatchContainer: Phaser.GameObjects.Container; private eggHatchContainer: Phaser.GameObjects.Container;
/** holds the icon containers and info container */ /** holds the icon containers and info container */
private summaryContainer: Phaser.GameObjects.Container; private summaryContainer: Phaser.GameObjects.Container;
/** container for the mini pokemon sprites */ /** container for the each pokemon sprites and icons */
private pokemonIconSpritesContainer: Phaser.GameObjects.Container;
/** container for the icons displayed on top of the mini pokemon sprites (e.g. shiny, HA capsule) */
private pokemonIconsContainer: Phaser.GameObjects.Container; private pokemonIconsContainer: Phaser.GameObjects.Container;
/** container for the elements displayed behind the mini pokemon sprites (e.g. egg rarity bg) */ /** list of the containers added to pokemonIconsContainer for easier access */
private pokemonBackgroundContainer: Phaser.GameObjects.Container; private pokemonContainers: HatchedPokemonContainer[];
/** hatch info container that displays the current pokemon / hatch (main element on left hand side) */ /** hatch info container that displays the current pokemon / hatch (main element on left hand side) */
private infoContainer: PokemonHatchInfoContainer; private infoContainer: PokemonHatchInfoContainer;
/** handles jumping animations for the pokemon sprite icons */ /** handles jumping animations for the pokemon sprite icons */
private iconAnimHandler: PokemonIconAnimHandler; private iconAnimHandler: PokemonIconAnimHandler;
private eggHatchBg: Phaser.GameObjects.Image; private eggHatchBg: Phaser.GameObjects.Image;
private cursorObj: Phaser.GameObjects.Image;
private eggHatchData: EggHatchData[]; private eggHatchData: EggHatchData[];
private scrollGridHandler : ScrollableGridUiHandler;
private cursorObj: Phaser.GameObjects.Image;
/** /**
* Allows subscribers to listen for events * Allows subscribers to listen for events
@ -54,7 +54,6 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
super(scene, Mode.EGG_HATCH_SUMMARY); super(scene, Mode.EGG_HATCH_SUMMARY);
} }
setup() { setup() {
const ui = this.getUi(); const ui = this.getUi();
@ -77,11 +76,8 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
this.cursorObj.setOrigin(0, 0); this.cursorObj.setOrigin(0, 0);
this.summaryContainer.add(this.cursorObj); this.summaryContainer.add(this.cursorObj);
this.pokemonIconSpritesContainer = this.scene.add.container(iconContainerX, iconContainerY); this.pokemonContainers = [];
this.pokemonIconsContainer = this.scene.add.container(iconContainerX, iconContainerY); this.pokemonIconsContainer = this.scene.add.container(iconContainerX, iconContainerY);
this.pokemonBackgroundContainer = this.scene.add.container(iconContainerX, iconContainerY);
this.summaryContainer.add(this.pokemonBackgroundContainer);
this.summaryContainer.add(this.pokemonIconSpritesContainer);
this.summaryContainer.add(this.pokemonIconsContainer); this.summaryContainer.add(this.pokemonIconsContainer);
this.infoContainer = new PokemonHatchInfoContainer(this.scene, this.summaryContainer); this.infoContainer = new PokemonHatchInfoContainer(this.scene, this.summaryContainer);
@ -90,16 +86,24 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
this.infoContainer.setVisible(true); this.infoContainer.setVisible(true);
this.summaryContainer.add(this.infoContainer); this.summaryContainer.add(this.infoContainer);
const scrollBar = new ScrollBar(this.scene, iconContainerX + numCols * iconSize, iconContainerY + 3, 4, this.scene.game.canvas.height / 6 - 20, numRows);
this.summaryContainer.add(scrollBar);
this.scrollGridHandler = new ScrollableGridUiHandler(this, numRows, numCols)
.withScrollBar(scrollBar)
.withUpdateGridCallBack(() => this.updatePokemonIcons())
.withUpdateSingleElementCallback((i: number) => this.infoContainer.showHatchInfo(this.eggHatchData[i]));
this.cursor = -1; this.cursor = -1;
} }
clear() { clear() {
super.clear(); super.clear();
this.cursor = -1; this.cursor = -1;
this.scrollGridHandler.reset();
this.summaryContainer.setVisible(false); this.summaryContainer.setVisible(false);
this.pokemonIconSpritesContainer.removeAll(true);
this.pokemonIconsContainer.removeAll(true); this.pokemonIconsContainer.removeAll(true);
this.pokemonBackgroundContainer.removeAll(true); this.pokemonContainers = [];
this.eggHatchBg.setVisible(false); this.eggHatchBg.setVisible(false);
this.getUi().hideTooltip(); this.getUi().hideTooltip();
@ -149,111 +153,51 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
return 0; return 0;
} }
} }
} });
);
} }
this.getUi().bringToTop(this.summaryContainer); this.getUi().bringToTop(this.summaryContainer);
this.summaryContainer.setVisible(true); this.summaryContainer.setVisible(true);
this.eggHatchContainer.setVisible(true); this.eggHatchContainer.setVisible(true);
this.pokemonIconsContainer.setVisible(true);
this.eggHatchBg.setVisible(true); this.eggHatchBg.setVisible(true);
this.infoContainer.hideDisplayPokemon(); this.infoContainer.hideDisplayPokemon();
this.eggHatchData.forEach( (value: EggHatchData, i: number) => { this.scrollGridHandler.setTotalElements(this.eggHatchData.length);
const x = (i % numCols) * iconSize; this.updatePokemonIcons();
const y = Math.floor(i / numCols) * iconSize;
const displayPokemon = value.pokemon;
const offset = 2;
const rightSideX = 12;
const rarityBg = this.scene.add.image(x + 2, y + 5, "passive_bg");
rarityBg.setOrigin(0, 0);
rarityBg.setScale(0.75);
rarityBg.setVisible(true);
this.pokemonBackgroundContainer.add(rarityBg);
// set tint for passive bg
switch (getEggTierForSpecies(displayPokemon.species)) {
case EggTier.COMMON:
rarityBg.setVisible(false);
break;
case EggTier.GREAT:
rarityBg.setTint(0xabafff);
break;
case EggTier.ULTRA:
rarityBg.setTint(0xffffaa);
break;
case EggTier.MASTER:
rarityBg.setTint(0xdfffaf);
break;
}
const species = displayPokemon.species;
const female = displayPokemon.gender === Gender.FEMALE;
const formIndex = displayPokemon.formIndex;
const variant = displayPokemon.variant;
const isShiny = displayPokemon.shiny;
// set pokemon icon (and replace with base sprite if there is a mismatch)
const pokemonIcon = this.scene.add.sprite(x - offset, y + offset, species.getIconAtlasKey(formIndex, isShiny, variant));
pokemonIcon.setScale(0.5);
pokemonIcon.setOrigin(0, 0);
pokemonIcon.setFrame(species.getIconId(female, formIndex, isShiny, variant));
if (pokemonIcon.frame.name !== species.getIconId(female, formIndex, isShiny, variant)) {
console.log(`${species.name}'s variant icon does not exist. Replacing with default.`);
pokemonIcon.setTexture(species.getIconAtlasKey(formIndex, false, variant));
pokemonIcon.setFrame(species.getIconId(female, formIndex, false, variant));
}
this.pokemonIconSpritesContainer.add(pokemonIcon);
const shinyIcon = this.scene.add.image(x + rightSideX, y + offset, "shiny_star_small");
shinyIcon.setOrigin(0, 0);
shinyIcon.setScale(0.5);
shinyIcon.setVisible(displayPokemon.shiny);
shinyIcon.setTint(getVariantTint(displayPokemon.variant));
this.pokemonIconsContainer.add(shinyIcon);
const haIcon = this.scene.add.image(x + rightSideX, y + offset * 4, "ha_capsule");
haIcon.setOrigin(0, 0);
haIcon.setScale(0.5);
haIcon.setVisible(displayPokemon.abilityIndex === 2);
this.pokemonIconsContainer.add(haIcon);
const dexEntry = value.dexEntryBeforeUpdate;
const caughtAttr = dexEntry.caughtAttr;
const newShiny = BigInt(1 << (displayPokemon.shiny ? 1 : 0));
const newVariant = BigInt(1 << (displayPokemon.variant + 4));
const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0));
const newForm = (BigInt(1 << displayPokemon.formIndex) * DexAttr.DEFAULT_FORM & caughtAttr) === BigInt(0);
const pokeballIcon = this.scene.add.image(x + rightSideX, y + offset * 7, "icon_owned");
pokeballIcon.setOrigin(0, 0);
pokeballIcon.setScale(0.5);
pokeballIcon.setVisible(!caughtAttr || newForm);
this.pokemonIconsContainer.add(pokeballIcon);
const eggMoveIcon = this.scene.add.image(x, y + offset, "icon_egg_move");
eggMoveIcon.setOrigin(0, 0);
eggMoveIcon.setScale(0.5);
eggMoveIcon.setVisible(value.eggMoveUnlocked);
this.pokemonIconsContainer.add(eggMoveIcon);
// add animation to the Pokemon sprite for new unlocks (new catch, new shiny or new form)
if (!caughtAttr || newShinyOrVariant || newForm) {
this.iconAnimHandler.addOrUpdate(pokemonIcon, PokemonIconAnimMode.PASSIVE);
} else {
this.iconAnimHandler.addOrUpdate(pokemonIcon, PokemonIconAnimMode.NONE);
}
});
this.setCursor(0); this.setCursor(0);
this.scene.playSoundWithoutBgm("evolution_fanfare"); this.scene.playSoundWithoutBgm("evolution_fanfare");
return true; return true;
} }
/**
* Show the grid of Pokemon icons
*/
private updatePokemonIcons(): void {
const itemOffset = this.scrollGridHandler.getItemOffset();
const eggsToShow = Math.min(numRows * numCols, this.eggHatchData.length - itemOffset);
for (let i = 0; i < numRows * numCols; i++) {
const hatchData = this.eggHatchData[i + itemOffset];
let hatchContainer = this.pokemonContainers[i];
if (i < eggsToShow) {
if (!hatchContainer) {
const x = (i % numCols) * iconSize;
const y = Math.floor(i / numCols) * iconSize;
hatchContainer = new HatchedPokemonContainer(this.scene, x, y, hatchData).setVisible(false);
this.pokemonContainers.push(hatchContainer);
this.pokemonIconsContainer.add(hatchContainer);
}
hatchContainer.setVisible(true);
hatchContainer.updateAndAnimate(hatchData, this.iconAnimHandler);
} else if (hatchContainer) {
hatchContainer.setVisible(false);
this.iconAnimHandler.addOrUpdate(hatchContainer.icon, PokemonIconAnimMode.NONE);
}
}
}
processInput(button: Button): boolean { processInput(button: Button): boolean {
const ui = this.getUi(); const ui = this.getUi();
@ -266,31 +210,7 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
} }
success = true; success = true;
} else { } else {
const count = this.eggHatchData.length; this.scrollGridHandler.processInput(button);
const rows = Math.ceil(count / numCols);
const row = Math.floor(this.cursor / numCols);
switch (button) {
case Button.UP:
if (row) {
success = this.setCursor(this.cursor - numCols);
}
break;
case Button.DOWN:
if (row < rows - 2 || (row < rows - 1 && this.cursor % numCols <= (count - 1) % numCols)) {
success = this.setCursor(this.cursor + numCols);
}
break;
case Button.LEFT:
if (this.cursor % numCols) {
success = this.setCursor(this.cursor - 1);
}
break;
case Button.RIGHT:
if (this.cursor % numCols < (row < rows - 1 ? 10 : (count - 1) % numCols)) {
success = this.setCursor(this.cursor + 1);
}
break;
}
} }
if (success) { if (success) {
@ -313,12 +233,11 @@ export default class EggSummaryUiHandler extends MessageUiHandler {
this.cursorObj.setPosition(iconContainerX - 1 + iconSize * (cursor % numCols), iconContainerY + 1 + iconSize * Math.floor(cursor / numCols)); this.cursorObj.setPosition(iconContainerX - 1 + iconSize * (cursor % numCols), iconContainerY + 1 + iconSize * Math.floor(cursor / numCols));
if (lastCursor > -1) { if (lastCursor > -1) {
this.iconAnimHandler.addOrUpdate(this.pokemonIconSpritesContainer.getAt(lastCursor) as Phaser.GameObjects.Sprite, PokemonIconAnimMode.NONE); this.iconAnimHandler.addOrUpdate(this.pokemonContainers[lastCursor].icon, PokemonIconAnimMode.NONE);
} }
this.iconAnimHandler.addOrUpdate(this.pokemonIconSpritesContainer.getAt(cursor) as Phaser.GameObjects.Sprite, PokemonIconAnimMode.ACTIVE); this.iconAnimHandler.addOrUpdate(this.pokemonContainers[cursor].icon, PokemonIconAnimMode.ACTIVE);
this.infoContainer.showHatchInfo(this.eggHatchData[cursor]);
this.infoContainer.showHatchInfo(this.eggHatchData[cursor + this.scrollGridHandler.getItemOffset()]);
} }
return changed; return changed;

View File

@ -0,0 +1,136 @@
import { EggHatchData } from "#app/data/egg-hatch-data";
import { Gender } from "#app/data/gender";
import { getVariantTint } from "#app/data/variant";
import { DexAttr } from "#app/system/game-data";
import BattleScene from "#app/battle-scene";
import PokemonSpecies from "#app/data/pokemon-species";
import PokemonIconAnimHandler, { PokemonIconAnimMode } from "./pokemon-icon-anim-handler";
/**
* A container for a Pokemon's sprite and icons to get displayed in the egg summary screen
* Shows the Pokemon's sprite, surrounded by icons for:
* shiny variant, hidden ability, new egg move, new catch
*/
export class HatchedPokemonContainer extends Phaser.GameObjects.Container {
public scene: BattleScene;
public species: PokemonSpecies;
public icon: Phaser.GameObjects.Sprite;
public shinyIcon: Phaser.GameObjects.Image;
public hiddenAbilityIcon: Phaser.GameObjects.Image;
public pokeballIcon: Phaser.GameObjects.Image;
public eggMoveIcon: Phaser.GameObjects.Image;
/**
* @param scene the current {@linkcode BattleScene}
* @param x x position
* @param y y position
* @param hatchData the {@linkcode EggHatchData} to load the icons and sprites for
*/
constructor(scene: BattleScene, x: number, y: number, hatchData: EggHatchData) {
super(scene, x, y);
const displayPokemon = hatchData.pokemon;
this.species = displayPokemon.species;
const offset = 2;
const rightSideX = 12;
const species = displayPokemon.species;
const female = displayPokemon.gender === Gender.FEMALE;
const formIndex = displayPokemon.formIndex;
const variant = displayPokemon.variant;
const isShiny = displayPokemon.shiny;
// Pokemon sprite
const pokemonIcon = this.scene.add.sprite(-offset, offset, species.getIconAtlasKey(formIndex, isShiny, variant));
pokemonIcon.setScale(0.5);
pokemonIcon.setOrigin(0, 0);
pokemonIcon.setFrame(species.getIconId(female, formIndex, isShiny, variant));
this.icon = pokemonIcon;
this.checkIconId(female, formIndex, isShiny, variant);
this.add(this.icon);
// Shiny icon
this.shinyIcon = this.scene.add.image(rightSideX, offset, "shiny_star_small");
this.shinyIcon.setOrigin(0, 0);
this.shinyIcon.setScale(0.5);
this.add(this.shinyIcon);
// Hidden ability icon
const haIcon = this.scene.add.image(rightSideX, offset * 4, "ha_capsule");
haIcon.setOrigin(0, 0);
haIcon.setScale(0.5);
this.hiddenAbilityIcon = haIcon;
this.add(this.hiddenAbilityIcon);
// Pokeball icon
const pokeballIcon = this.scene.add.image(rightSideX, offset * 7, "icon_owned");
pokeballIcon.setOrigin(0, 0);
pokeballIcon.setScale(0.5);
this.pokeballIcon = pokeballIcon;
this.add(this.pokeballIcon);
// Egg move icon
const eggMoveIcon = this.scene.add.image(0, offset, "icon_egg_move");
eggMoveIcon.setOrigin(0, 0);
eggMoveIcon.setScale(0.5);
this.eggMoveIcon = eggMoveIcon;
this.add(this.eggMoveIcon);
}
/**
* Update the Pokemon's sprite and icons based on new hatch data
* Animates the pokemon icon if it has a new form or shiny variant
*
* @param hatchData the {@linkcode EggHatchData} to base the icons on
* @param iconAnimHandler the {@linkcode PokemonIconAnimHandler} to use to animate the sprites
*/
updateAndAnimate(hatchData: EggHatchData, iconAnimHandler: PokemonIconAnimHandler) {
const displayPokemon = hatchData.pokemon;
this.species = displayPokemon.species;
const dexEntry = hatchData.dexEntryBeforeUpdate;
const caughtAttr = dexEntry.caughtAttr;
const newShiny = BigInt(1 << (displayPokemon.shiny ? 1 : 0));
const newVariant = BigInt(1 << (displayPokemon.variant + 4));
const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0));
const newForm = (BigInt(1 << displayPokemon.formIndex) * DexAttr.DEFAULT_FORM & caughtAttr) === BigInt(0);
const female = displayPokemon.gender === Gender.FEMALE;
const formIndex = displayPokemon.formIndex;
const variant = displayPokemon.variant;
const isShiny = displayPokemon.shiny;
this.icon.setTexture(this.species.getIconAtlasKey(formIndex, isShiny, variant));
this.icon.setFrame(this.species.getIconId(female, formIndex, isShiny, variant));
this.checkIconId(female, formIndex, isShiny, variant);
this.shinyIcon.setVisible(displayPokemon.shiny);
this.shinyIcon.setTint(getVariantTint(displayPokemon.variant));
this.eggMoveIcon.setVisible(hatchData.eggMoveUnlocked);
this.hiddenAbilityIcon.setVisible(displayPokemon.abilityIndex === 2);
this.pokeballIcon.setVisible(!caughtAttr || newForm);
// add animation to the Pokemon sprite for new unlocks (new catch, new shiny or new form)
if (!caughtAttr || newShinyOrVariant || newForm) {
iconAnimHandler.addOrUpdate(this.icon, PokemonIconAnimMode.PASSIVE);
} else {
iconAnimHandler.addOrUpdate(this.icon, PokemonIconAnimMode.NONE);
}
}
/**
* Check if the given Pokemon icon exists, otherwise replace it with a default one
* @param female `true` to get the female icon
* @param formIndex the form index
* @param shiny whether the Pokemon is shiny
* @param variant the shiny variant
*/
private checkIconId(female: boolean, formIndex: number, shiny: boolean, variant: number) {
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));
}
}
}

View File

@ -22,6 +22,8 @@ export class ScrollBar extends Phaser.GameObjects.Container {
super(scene, x, y); super(scene, x, y);
this.maxRows = maxRows; this.maxRows = maxRows;
this.totalRows = maxRows;
this.currentRow = 0;
const borderSize = 2; const borderSize = 2;
width = Math.max(width, 4); width = Math.max(width, 4);
@ -46,8 +48,7 @@ export class ScrollBar extends Phaser.GameObjects.Container {
*/ */
setScrollCursor(scrollCursor: number): void { setScrollCursor(scrollCursor: number): void {
this.currentRow = scrollCursor; this.currentRow = scrollCursor;
this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.totalRows * this.currentRow; this.updateHandlePosition();
this.handleBottom.y = this.handleBody.y + this.handleBody.displayHeight;
} }
/** /**
@ -59,7 +60,13 @@ export class ScrollBar extends Phaser.GameObjects.Container {
setTotalRows(rows: number): void { setTotalRows(rows: number): void {
this.totalRows = rows; this.totalRows = rows;
this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * this.maxRows / this.totalRows; this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * this.maxRows / this.totalRows;
this.updateHandlePosition();
this.setVisible(this.totalRows > this.maxRows); this.setVisible(this.totalRows > this.maxRows);
} }
private updateHandlePosition(): void {
this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.totalRows * this.currentRow;
this.handleBottom.y = this.handleBody.y + this.handleBody.displayHeight;
}
} }

View File

@ -0,0 +1,197 @@
import { Button } from "#enums/buttons";
import UiHandler from "#app/ui/ui-handler";
import { ScrollBar } from "#app/ui/scroll-bar";
type UpdateGridCallbackFunction = () => void;
type UpdateDetailsCallbackFunction = (index: number) => void;
/**
* A helper class to handle navigation through a grid of elements that can scroll vertically
* Needs to be used by a {@linkcode UiHandler}
* How to use:
* - in `UiHandler.setup`: Initialize with the {@linkcode UiHandler} that handles the grid,
* the number of rows and columns that can be shown at once,
* an optional {@linkcode ScrollBar}, and optional callbacks that will get called after scrolling
* - in `UiHandler.show`: Set `setTotalElements` to the total number of elements in the list to display
* - in `UiHandler.processInput`: call `processNavigationInput` to have it handle the cursor updates while calling the defined callbacks
* - in `UiHandler.clear`: call `reset`
*/
export default class ScrollableGridUiHandler {
private readonly ROWS: number;
private readonly COLUMNS: number;
private handler: UiHandler;
private totalElements: number;
private cursor: number;
private scrollCursor: number;
private scrollBar?: ScrollBar;
private updateGridCallback?: UpdateGridCallbackFunction;
private updateDetailsCallback?: UpdateDetailsCallbackFunction;
/**
* @param scene the {@linkcode UiHandler} that needs its cursor updated based on the scrolling
* @param rows the maximum number of rows shown at once
* @param columns the maximum number of columns shown at once
* @param updateGridCallback optional function that will get called if the whole grid needs to get updated
* @param updateDetailsCallback optional function that will get called if a single element's information needs to get updated
*/
constructor(handler: UiHandler, rows: number, columns: number) {
this.handler = handler;
this.ROWS = rows;
this.COLUMNS = columns;
this.scrollCursor = 0;
this.cursor = 0;
this.totalElements = rows * columns; // default value for the number of elements
}
/**
* Set a scrollBar to get updated with the scrolling
* @param scrollBar {@linkcode ScrollBar}
* @returns this
*/
withScrollBar(scrollBar: ScrollBar): ScrollableGridUiHandler {
this.scrollBar = scrollBar;
this.scrollBar.setTotalRows(Math.ceil(this.totalElements / this.COLUMNS));
return this;
}
/**
* Set function that will get called if the whole grid needs to get updated
* @param callback {@linkcode UpdateGridCallbackFunction}
* @returns this
*/
withUpdateGridCallBack(callback: UpdateGridCallbackFunction): ScrollableGridUiHandler {
this.updateGridCallback = callback;
return this;
}
/**
* Set function that will get called if a single element in the grid needs to get updated
* @param callback {@linkcode UpdateDetailsCallbackFunction}
* @returns this
*/
withUpdateSingleElementCallback(callback: UpdateDetailsCallbackFunction): ScrollableGridUiHandler {
this.updateDetailsCallback = callback;
return this;
}
/**
* @param totalElements the total number of elements that the grid needs to display
*/
setTotalElements(totalElements: number) {
this.totalElements = totalElements;
if (this.scrollBar) {
this.scrollBar.setTotalRows(Math.ceil(this.totalElements / this.COLUMNS));
}
this.setScrollCursor(0);
}
/**
* @returns how many elements are hidden due to scrolling
*/
getItemOffset(): number {
return this.scrollCursor * this.COLUMNS;
}
/**
* Update the cursor and scrollCursor based on user input
* @param button the button that was pressed
* @returns `true` if either the cursor or scrollCursor was updated
*/
processInput(button: Button): boolean {
let success = false;
const onScreenRows = Math.min(this.ROWS, Math.ceil(this.totalElements / this.COLUMNS));
const maxScrollCursor = Math.max(0, Math.ceil(this.totalElements / this.COLUMNS) - onScreenRows);
const currentRowIndex = Math.floor(this.cursor / this.COLUMNS);
const currentColumnIndex = this.cursor % this.COLUMNS;
const itemOffset = this.scrollCursor * this.COLUMNS;
const lastVisibleIndex = Math.min(this.totalElements - 1, this.totalElements - maxScrollCursor * this.COLUMNS - 1);
switch (button) {
case Button.UP:
if (currentRowIndex > 0) {
success = this.setCursor(this.cursor - this.COLUMNS);
} else if (this.scrollCursor > 0) {
success = this.setScrollCursor(this.scrollCursor - 1);
} else {
// wrap around to the last row
let newCursor = this.cursor + (onScreenRows - 1) * this.COLUMNS;
if (newCursor > lastVisibleIndex) {
newCursor -= this.COLUMNS;
}
success = this.setScrollCursor(maxScrollCursor, newCursor);
}
break;
case Button.DOWN:
if (currentRowIndex < onScreenRows - 1) {
// Go down one row
success = this.setCursor(Math.min(this.cursor + this.COLUMNS, this.totalElements - itemOffset - 1));
} else if (this.scrollCursor < maxScrollCursor) {
// Scroll down one row
success = this.setScrollCursor(this.scrollCursor + 1);
} else {
// Wrap around to the top row
success = this.setScrollCursor(0, this.cursor % this.COLUMNS);
}
break;
case Button.LEFT:
if (currentColumnIndex > 0) {
success = this.setCursor(this.cursor - 1);
} else if (this.scrollCursor === maxScrollCursor && currentRowIndex === onScreenRows - 1) {
success = this.setCursor(lastVisibleIndex);
} else {
success = this.setCursor(this.cursor + this.COLUMNS - 1);
}
break;
case Button.RIGHT:
if (currentColumnIndex < this.COLUMNS - 1 && this.cursor + itemOffset < this.totalElements - 1) {
success = this.setCursor(this.cursor + 1);
} else {
success = this.setCursor(this.cursor - currentColumnIndex);
}
break;
}
return success;
}
/**
* Reset the scrolling
*/
reset(): void {
this.setScrollCursor(0);
this.setCursor(0);
}
private setCursor(cursor: number): boolean {
this.cursor = cursor;
return this.handler.setCursor(cursor);
}
private setScrollCursor(scrollCursor: number, cursor?: number): boolean {
const scrollChanged = scrollCursor !== this.scrollCursor;
// update the scrolling cursor
if (scrollChanged) {
this.scrollCursor = scrollCursor;
if (this.scrollBar) {
this.scrollBar.setScrollCursor(scrollCursor);
}
if (this.updateGridCallback) {
this.updateGridCallback();
}
}
let cursorChanged = false;
const newElementIndex = this.cursor + this.scrollCursor * this.COLUMNS;
if (cursor !== undefined) {
cursorChanged = this.setCursor(cursor);
} else if (newElementIndex >= this.totalElements) {
// make sure the cursor does not go past the end of the list
cursorChanged = this.setCursor(this.totalElements - this.scrollCursor * this.COLUMNS - 1);
} else if (scrollChanged && this.updateDetailsCallback) {
// scroll was changed but not the normal cursor, update the selected element
this.updateDetailsCallback(newElementIndex);
}
return scrollChanged || cursorChanged;
}
}