pokerogue/src/ui/challenges-select-ui-handler.ts
Lugiad ca4df7233f
[UI/UX] [Localization] Japanese font change and custom size edits (#6026)
* Delete Japanese Galmuri11 font

* Delete Japanese Galmuri9 font

* Added pokemon-bw font for Japanese

* Changed Japanese Font + removed font size adjustment

* Added Japanese Custom Adjustments

* Slightly increased spacing for Ability name+desc labels

* Japanese starterInfoText adjustments

* Japanese custom lineSpacing adjustment

* Spacing for Ability desc labels adjusted

* Friendship count text adjustment

* Japanese former custom adjustments removal

* TextStyle adjustments

* TextStyle.PARTY adjustments

* Added new TextStyle

* Added new TextStyle

* Added new TextStyle

* Added new TextStyle

* Added new TextStyle

* Add new TextStyle

* Add new Text Style

* Add new TextStyle

* Add new TextStyle

* Add new TextStyle

* Add new TextStyle

* Add new TextStyle

* PARTY_CANCEL_BUTTON TextStyle Correction

* PARTY_CANCEL_BUTTON TextStyle Correction

* Removal of old Japanese line spacing parameter

* Removed old Japanese adjustments

* Apply Biome

* PARTY_CANCEL_BUTTON Adjustments

* partyCancelText position adjustment

* Update i18n.ts

* Change TextStyle of valueLimitLabel

* Added new TextStyle

* Add MOVE_LABEL TextStyle

* Add MOVE_LABEL TextStyle

* Line formatting correction

* MOVE_LABEL TextStyle padding correction

* Added GROWTH_RATE_TYPE TextStyle

* Add GROWTH_RATE_TYPE TextStyle

* Line formatting correction

* Egg Moves title text indepentent from pokemonEggMovesContainer

* Egg Moves title text indepentent from pokemonEggMovesContainer

* Update src/ui/starter-select-ui-handler.ts

* Correction to eggMovesLabel

* Update starter-select-ui-handler.ts

* Added SUMMARY_STATS_GOLD

* Added SUMMARY_STATS_GOLD

* Added proper multiplication symbol

* Added proper multiplication symbol

* Added proper multiplication symbol

* Added proper multiplication symbol

* Added GROWTH_RATE_TYPE

* Added INSTRUCTIONS_TEXT TextStyle

* Added INSTRUCTIONS_TEXT TextStyle

* Added INSTRUCTIONS_TEXT TextStyle

* Added INSTRUCTIONS_TEXT TextStyle

* Added INSTRUCTIONS_TEXT TextStyle

* INSTRUCTIONS_TEXT adjustments

* Added proper multiplication symbol

* Added SUMMARY_DEX_NUM TextStyle

* Added SUMMARY_DEX_NUM

* Revert SUMMARY_DEX_NUM to wrong text

* Add SUMMARY_DEX_NUM

* Removed outdated Japanese custom line spacing

* Removed outdated Japanese custom line spacing

* Removed outdated Japanese custom line spacing

* Correction outdated Japanese custom line spacing

* Added MOVE_LABEL TextStyle

* Fixed corped tooltipbox and tooltipbox scrolling

* Corrected ME descriptiuon scrolling

* Added MOVE_LABEL

* Apply HEADER_LABEL TextStyle

* Apply HEADER_LABEL

* Added custom values for SETTINGS_VALUE

* Apply MOVE_LABEL

* Added STATS_HEXAGON TextStyle

* Apply STATS_HEXAGON TextStyle

* Typo correction

* Delete outadated pokemon-bw font

* Add updated pokemon-bw font

* Update pokemon-bw format

* Added EGG_SUMMARY_NAME and EGG_SUMMARY_DEX TextStyles

* Apply EGG_SUMMARY_NAME and EGG_SUMMARY_DEX

* Add LUCK_VALUE TextStyle

* Apply LUCK_VALUE TextStyle

* Apply LUCK_VALUE TextStyle

* Adjusted LUCK_VALUE

* Apply LUCK_VALUE TextStyle

* Adjustments for Japanese

* Adjusted Japanese custom

* Added FILTER_BAR_MAIN TextStyle

* Apply FILTER_BAR_MAIN TextStyle

* Added japanese to custom TextStyle

* Added English language settings

* Apply Biome

* pokemon-bw font update

* pokemon-bw font updated

* pokemon-bw font update

* pokemon-bw font update

* pokemon-bw font update

* pokemon-bw font update

* pokemon-bw font update

* pokemon-bw font update

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>
2025-07-11 13:51:59 -04:00

539 lines
20 KiB
TypeScript

import { TextStyle, addTextObject } from "./text";
import type { UiMode } from "#enums/ui-mode";
import UiHandler from "./ui-handler";
import { addWindow } from "./ui-theme";
import { Button } from "#enums/buttons";
import i18next from "i18next";
import type { Challenge } from "#app/data/challenge";
import { getLocalizedSpriteKey } from "#app/utils/common";
import { Challenges } from "#app/enums/challenges";
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
import { Color, ShadowColor } from "#app/enums/color";
import { globalScene } from "#app/global-scene";
/**
* Handles all the UI for choosing optional challenges.
*/
export default class GameChallengesUiHandler extends UiHandler {
private challengesContainer: Phaser.GameObjects.Container;
private valuesContainer: Phaser.GameObjects.Container;
private scrollCursor: number;
private optionsBg: Phaser.GameObjects.NineSlice;
// private difficultyText: Phaser.GameObjects.Text;
private descriptionText: BBCodeText;
private challengeLabels: Array<{
label: Phaser.GameObjects.Text;
value: Phaser.GameObjects.Text;
leftArrow: Phaser.GameObjects.Image;
rightArrow: Phaser.GameObjects.Image;
}>;
private monoTypeValue: Phaser.GameObjects.Sprite;
private cursorObj: Phaser.GameObjects.NineSlice | null;
private startBg: Phaser.GameObjects.NineSlice;
private startCursor: Phaser.GameObjects.NineSlice;
private startText: Phaser.GameObjects.Text;
private hasSelectedChallenge: boolean;
private optionsWidth: number;
private widestTextBox: number;
private readonly leftArrowGap: number = 90; // distance from the label to the left arrow
private readonly arrowSpacing: number = 3; // distance between the arrows and the value area
constructor(mode: UiMode | null = null) {
super(mode);
}
setup() {
const ui = this.getUi();
this.widestTextBox = 0;
this.challengesContainer = globalScene.add.container(1, -(globalScene.game.canvas.height / 6) + 1);
this.challengesContainer.setName("challenges");
this.challengesContainer.setInteractive(
new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6),
Phaser.Geom.Rectangle.Contains,
);
const bgOverlay = globalScene.add.rectangle(
-1,
-1,
globalScene.scaledCanvas.width,
globalScene.scaledCanvas.height,
0x424242,
0.8,
);
bgOverlay.setName("rect-challenge-overlay");
bgOverlay.setOrigin(0, 0);
this.challengesContainer.add(bgOverlay);
// TODO: Change this back to /9 when adding in difficulty
const headerBg = addWindow(0, 0, globalScene.game.canvas.width / 6, 24);
headerBg.setName("window-header-bg");
headerBg.setOrigin(0, 0);
const headerText = addTextObject(0, 0, i18next.t("challenges:title"), TextStyle.HEADER_LABEL);
headerText.setName("text-header");
headerText.setOrigin(0, 0);
headerText.setPositionRelative(headerBg, 8, 4);
this.optionsWidth = globalScene.scaledCanvas.width * 0.6;
this.optionsBg = addWindow(
0,
headerBg.height,
this.optionsWidth,
globalScene.scaledCanvas.height - headerBg.height - 2,
);
this.optionsBg.setName("window-options-bg");
this.optionsBg.setOrigin(0, 0);
const descriptionBg = addWindow(
0,
headerBg.height,
globalScene.scaledCanvas.width - this.optionsWidth,
globalScene.scaledCanvas.height - headerBg.height - 26,
);
descriptionBg.setName("window-desc-bg");
descriptionBg.setOrigin(0, 0);
descriptionBg.setPositionRelative(this.optionsBg, this.optionsBg.width, 0);
this.descriptionText = new BBCodeText(globalScene, descriptionBg.x + 6, descriptionBg.y + 4, "", {
fontFamily: "emerald",
fontSize: 84,
color: Color.ORANGE,
padding: {
bottom: 6,
},
wrap: {
mode: "word",
width: (descriptionBg.width - 12) * 6,
},
});
this.descriptionText.setName("text-desc");
globalScene.add.existing(this.descriptionText);
this.descriptionText.setScale(1 / 6);
this.descriptionText.setShadow(4, 5, ShadowColor.ORANGE);
this.descriptionText.setOrigin(0, 0);
this.startBg = addWindow(0, 0, descriptionBg.width, 24);
this.startBg.setName("window-start-bg");
this.startBg.setOrigin(0, 0);
this.startBg.setPositionRelative(descriptionBg, 0, descriptionBg.height);
this.startText = addTextObject(0, 0, i18next.t("challenges:noneSelected"), TextStyle.SETTINGS_LABEL);
this.startText.setName("text-start");
this.startText.setOrigin(0, 0);
this.startText.setPositionRelative(this.startBg, (this.startBg.width - this.startText.displayWidth) / 2, 4);
this.startCursor = globalScene.add.nineslice(
0,
0,
"summary_moves_cursor",
undefined,
descriptionBg.width - 8,
16,
1,
1,
1,
1,
);
this.startCursor.setName("9s-start-cursor");
this.startCursor.setOrigin(0, 0);
this.startCursor.setPositionRelative(this.startBg, 4, 3);
this.startCursor.setVisible(false);
this.valuesContainer = globalScene.add.container(0, 0);
this.valuesContainer.setName("values");
this.challengeLabels = [];
for (let i = 0; i < 9; i++) {
const label = addTextObject(8, 28 + i * 16, "", TextStyle.SETTINGS_LABEL);
label.setName(`text-challenge-label-${i}`);
label.setOrigin(0, 0);
this.valuesContainer.add(label);
const leftArrow = globalScene.add.image(0, 0, "cursor_reverse");
leftArrow.setName(`challenge-left-arrow-${i}`);
leftArrow.setOrigin(0, 0);
leftArrow.setVisible(false);
leftArrow.setScale(0.75);
this.valuesContainer.add(leftArrow);
const rightArrow = globalScene.add.image(0, 0, "cursor");
rightArrow.setName(`challenge-right-arrow-${i}`);
rightArrow.setOrigin(0, 0);
rightArrow.setScale(0.75);
rightArrow.setVisible(false);
this.valuesContainer.add(rightArrow);
const value = addTextObject(0, 28 + i * 16, "", TextStyle.SETTINGS_LABEL);
value.setName(`challenge-value-text-${i}`);
value.setPositionRelative(label, 100, 0);
this.valuesContainer.add(value);
this.challengeLabels[i] = {
label: label,
value: value,
leftArrow: leftArrow,
rightArrow: rightArrow,
};
}
this.monoTypeValue = globalScene.add.sprite(8, 98, getLocalizedSpriteKey("types"));
this.monoTypeValue.setName("challenge-value-monotype-sprite");
this.monoTypeValue.setScale(0.86);
this.monoTypeValue.setVisible(false);
this.valuesContainer.add(this.monoTypeValue);
this.challengesContainer.add(headerBg);
this.challengesContainer.add(headerText);
// this.challengesContainer.add(difficultyBg);
// this.challengesContainer.add(this.difficultyText);
// this.challengesContainer.add(difficultyName);
this.challengesContainer.add(this.optionsBg);
this.challengesContainer.add(descriptionBg);
this.challengesContainer.add(this.descriptionText);
this.challengesContainer.add(this.startBg);
this.challengesContainer.add(this.startText);
this.challengesContainer.add(this.startCursor);
this.challengesContainer.add(this.valuesContainer);
ui.add(this.challengesContainer);
this.setCursor(0);
this.setScrollCursor(0);
this.challengesContainer.setVisible(false);
}
/**
* Adds the default text color to the description text
* @param text text to set to the BBCode description
*/
setDescription(text: string): void {
this.descriptionText.setText(`[color=${Color.ORANGE}][shadow=${ShadowColor.ORANGE}]${text}`);
}
/**
* initLabels
* init all challenge labels
*/
initLabels(): void {
this.setDescription(globalScene.gameMode.challenges[0].getDescription());
this.widestTextBox = 0;
for (let i = 0; i < 9; i++) {
if (i < globalScene.gameMode.challenges.length) {
this.challengeLabels[i].label.setVisible(true);
this.challengeLabels[i].value.setVisible(true);
this.challengeLabels[i].leftArrow.setVisible(true);
this.challengeLabels[i].rightArrow.setVisible(true);
const tempText = addTextObject(0, 0, "", TextStyle.SETTINGS_LABEL); // this is added here to get the widest text object for this language, which will be used for the arrow placement
for (let j = 0; j <= globalScene.gameMode.challenges[i].maxValue; j++) {
// this goes through each challenge's value to find out what the max width will be
if (globalScene.gameMode.challenges[i].id !== Challenges.SINGLE_TYPE) {
tempText.setText(globalScene.gameMode.challenges[i].getValue(j));
if (tempText.displayWidth > this.widestTextBox) {
this.widestTextBox = tempText.displayWidth;
}
}
}
tempText.destroy();
}
}
}
/**
* update the text the cursor is on
*/
updateText(): void {
this.setDescription(this.getActiveChallenge().getDescription());
let monoTypeVisible = false;
for (let i = 0; i < Math.min(9, globalScene.gameMode.challenges.length); i++) {
const challenge = globalScene.gameMode.challenges[this.scrollCursor + i];
const challengeLabel = this.challengeLabels[i];
challengeLabel.label.setText(challenge.getName());
challengeLabel.leftArrow.setPositionRelative(challengeLabel.label, this.leftArrowGap, 4.5);
challengeLabel.leftArrow.setVisible(challenge.value !== 0);
challengeLabel.rightArrow.setPositionRelative(
challengeLabel.leftArrow,
Math.max(this.monoTypeValue.width, this.widestTextBox) +
challengeLabel.leftArrow.displayWidth +
2 * this.arrowSpacing,
0,
);
challengeLabel.rightArrow.setVisible(challenge.value !== challenge.maxValue);
// this check looks to make sure that the arrows and value textbox don't take up too much space that they'll clip the right edge of the options background
if (
challengeLabel.rightArrow.x + challengeLabel.rightArrow.width + this.optionsBg.rightWidth + this.arrowSpacing >
this.optionsWidth
) {
// if we go out of bounds of the box, set the x position as far right as we can without going past the box, with this.arrowSpacing to allow a small gap between the arrow and border
challengeLabel.rightArrow.setX(this.optionsWidth - this.arrowSpacing - this.optionsBg.rightWidth);
}
// this line of code gets the center point between the left and right arrows from their left side (Arrow.x gives middle point), taking into account the width of the arrows
const xLocation = Math.round(
(challengeLabel.leftArrow.x + challengeLabel.rightArrow.x + challengeLabel.leftArrow.displayWidth) / 2,
);
if (challenge.id === Challenges.SINGLE_TYPE) {
this.monoTypeValue.setX(xLocation);
this.monoTypeValue.setY(challengeLabel.label.y + 8);
this.monoTypeValue.setFrame(challenge.getValue());
this.monoTypeValue.setVisible(true);
challengeLabel.value.setVisible(false);
monoTypeVisible = true;
} else {
challengeLabel.value.setText(challenge.getValue());
challengeLabel.value.setX(xLocation);
challengeLabel.value.setOrigin(0.5, 0);
challengeLabel.value.setVisible(true);
}
}
if (!monoTypeVisible) {
this.monoTypeValue.setVisible(false);
}
// This checks if a challenge has been selected by the user and updates the text/its opacity accordingly.
this.hasSelectedChallenge = globalScene.gameMode.challenges.some(c => c.value !== 0);
if (this.hasSelectedChallenge) {
this.startText.setText(i18next.t("common:start"));
this.startText.setAlpha(1);
this.startText.setPositionRelative(this.startBg, (this.startBg.width - this.startText.displayWidth) / 2, 4);
} else {
this.startText.setText(i18next.t("challenges:noneSelected"));
this.startText.setAlpha(0.5);
this.startText.setPositionRelative(this.startBg, (this.startBg.width - this.startText.displayWidth) / 2, 4);
}
this.challengesContainer.update();
}
show(args: any[]): boolean {
super.show(args);
this.startCursor.setVisible(false);
this.updateChallengeArrows(false);
this.challengesContainer.setVisible(true);
// Should always be false at the start
this.hasSelectedChallenge = globalScene.gameMode.challenges.some(c => c.value !== 0);
this.setCursor(0);
this.initLabels();
this.updateText();
this.getUi().moveTo(this.challengesContainer, this.getUi().length - 1);
this.getUi().hideTooltip();
return true;
}
/* This code updates the challenge starter arrows to be tinted/not tinted when the start button is selected to show they can't be changed
*/
updateChallengeArrows(tinted: boolean) {
for (let i = 0; i < Math.min(9, globalScene.gameMode.challenges.length); i++) {
const challengeLabel = this.challengeLabels[i];
if (tinted) {
challengeLabel.leftArrow.setTint(0x808080);
challengeLabel.rightArrow.setTint(0x808080);
} else {
challengeLabel.leftArrow.clearTint();
challengeLabel.rightArrow.clearTint();
}
}
}
/**
* Processes input from a specified button.
* This method handles navigation through a UI menu, including movement through menu items
* and handling special actions like cancellation. Each button press may adjust the cursor
* position or the menu scroll, and plays a sound effect if the action was successful.
*
* @param button - The button pressed by the user.
* @returns `true` if the action associated with the button was successfully processed, `false` otherwise.
*/
processInput(button: Button): boolean {
const ui = this.getUi();
// Defines the maximum number of rows that can be displayed on the screen.
const rowsToDisplay = 9;
let success = false;
if (button === Button.CANCEL) {
if (this.startCursor.visible) {
// If the user presses cancel when the start cursor has been activated, the game deactivates the start cursor and allows typical challenge selection behavior
this.startCursor.setVisible(false);
this.cursorObj?.setVisible(true);
this.updateChallengeArrows(this.startCursor.visible);
} else {
globalScene.phaseManager.clearPhaseQueue();
globalScene.phaseManager.pushNew("TitlePhase");
globalScene.phaseManager.getCurrentPhase()?.end();
}
success = true;
} else if (button === Button.SUBMIT || button === Button.ACTION) {
if (this.hasSelectedChallenge) {
if (this.startCursor.visible) {
globalScene.phaseManager.unshiftNew("SelectStarterPhase");
globalScene.phaseManager.getCurrentPhase()?.end();
} else {
this.startCursor.setVisible(true);
this.cursorObj?.setVisible(false);
this.updateChallengeArrows(this.startCursor.visible);
}
success = true;
} else {
success = false;
}
} else {
if (this.cursorObj?.visible && !this.startCursor.visible) {
switch (button) {
case Button.UP:
if (this.cursor === 0) {
if (this.scrollCursor === 0) {
// When at the top of the menu and pressing UP, move to the bottommost item.
if (globalScene.gameMode.challenges.length > rowsToDisplay) {
// If there are more than 9 challenges, scroll to the bottom
// First, set the cursor to the last visible element, preparing for the scroll to the end.
const successA = this.setCursor(rowsToDisplay - 1);
// Then, adjust the scroll to display the bottommost elements of the menu.
const successB = this.setScrollCursor(globalScene.gameMode.challenges.length - rowsToDisplay);
success = successA && successB; // success is just there to play the little validation sound effect
} else {
// If there are 9 or less challenges, just move to the bottom one
success = this.setCursor(globalScene.gameMode.challenges.length - 1);
}
} else {
success = this.setScrollCursor(this.scrollCursor - 1);
}
} else {
success = this.setCursor(this.cursor - 1);
}
if (success) {
this.updateText();
}
break;
case Button.DOWN:
if (this.cursor === rowsToDisplay - 1) {
if (this.scrollCursor < globalScene.gameMode.challenges.length - rowsToDisplay) {
// When at the bottom and pressing DOWN, scroll if possible.
success = this.setScrollCursor(this.scrollCursor + 1);
} else {
// When at the bottom of a scrolling menu and pressing DOWN, move to the topmost item.
// First, set the cursor to the first visible element, preparing for the scroll to the top.
const successA = this.setCursor(0);
// Then, adjust the scroll to display the topmost elements of the menu.
const successB = this.setScrollCursor(0);
success = successA && successB; // success is just there to play the little validation sound effect
}
} else if (
globalScene.gameMode.challenges.length < rowsToDisplay &&
this.cursor === globalScene.gameMode.challenges.length - 1
) {
// When at the bottom of a non-scrolling menu and pressing DOWN, move to the topmost item.
success = this.setCursor(0);
} else {
success = this.setCursor(this.cursor + 1);
}
if (success) {
this.updateText();
}
break;
case Button.LEFT:
// Moves the option cursor left, if possible.
success = this.getActiveChallenge().decreaseValue();
if (success) {
this.updateText();
}
break;
case Button.RIGHT:
// Moves the option cursor right, if possible.
success = this.getActiveChallenge().increaseValue();
if (success) {
this.updateText();
}
break;
}
}
}
// Plays a select sound effect if an action was successfully processed.
if (success) {
ui.playSelect();
}
return success;
}
setCursor(cursor: number): boolean {
let ret = super.setCursor(cursor);
if (!this.cursorObj) {
this.cursorObj = globalScene.add.nineslice(
0,
0,
"summary_moves_cursor",
undefined,
this.optionsWidth - 8,
16,
1,
1,
1,
1,
);
this.cursorObj.setOrigin(0, 0);
this.valuesContainer.add(this.cursorObj);
}
ret ||= !this.cursorObj.visible;
this.cursorObj.setVisible(true);
this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + (this.cursor + this.scrollCursor) * 16);
return ret;
}
setScrollCursor(scrollCursor: number): boolean {
if (scrollCursor === this.scrollCursor) {
return false;
}
this.scrollCursor = scrollCursor;
this.setCursor(this.cursor);
return true;
}
getActiveChallenge(): Challenge {
return globalScene.gameMode.challenges[this.cursor + this.scrollCursor];
}
clear() {
super.clear();
this.challengesContainer.setVisible(false);
this.eraseCursor();
}
eraseCursor() {
if (this.cursorObj) {
this.cursorObj.destroy();
}
this.cursorObj = null;
}
}