pokerogue/src/ui/ui.ts
flx-sta a07d2c57a4
[Refactor] use typescript strict-null (#3259)
* TS: enable strict-null

* fix battle-scene.ts

* fix voucher.ts

* adapt more files to strict-null

* adapt more files to strict-null ( 2)

* adapt ability.ts to strict-null

* adapt `arena.ts` to strict-null

* adapt TagAddedEvent constructor to strict-null

* adapt phases.ts.to strict-null

* adapt status-effect.ts to strict-null

* adapt `account.ts` to strict-null

* adapt `configHandler.ts` to strict-null

* adapt `ability.ts` to strict-null

* adapt `biomes.ts` to strict-null

* adapt `challenge.ts` to strict-null

* adapt `daily-run.ts` to strict-null

* adapt `nature.ts` to strict-null

* adapt `pokemon-forms.ts` to strict-null

* adapt `tainer-names.ts` to strict-null

* adapt `types.ts` to strict-null

* adapt `weather.ts` to strict-null

* adapt `egg-hatch-phase.ts` to strict-null

* adapt `evolution-phase.ts` to strict-null

* adapt `pokemon-sprite-sparkle-handler.ts` to strict-null

* adapt `evolution-phase.ts` to strict-null

* adapt `game-mode.ts` to strict-null

* adapt `utils.ts` to strict-null

* adapt `voucher-ui-handler.ts` to strict-null

* adapt `src/ui/unavailable-modal-ui-handler.ts` to strict-null

* adapt `src/ui/ui.ts` to strict-null

* adapt `src/ui/ui-theme.ts` to strict-null

* adapt `src/ui/title-ui-handler.ts` to strict-null

* adapt `src/ui/time-of-day-widget.ts` to strict-null

* adapt `src/ui/text.ts` to strict-null

* adapt `src/ui/target-select-ui-handler.ts` to strict-null

* adapt `src/ui/settings/settings-keyboard-ui-handler.ts` to strict-null

* adapt more files to strict-null (3)

* adapt more files to strict-null (4)

* adapt more files (mostly tests) to strict-null (5)

* adapt more files to strict-null (6)

* adapt more files to strict-null (7)

* Update `src/data/pokemon-evolutions.ts` for strict-null

Partial update `src/data/pokemon-species.ts` for strict-null

* adapt more files to strict-null (8)

* adapt more files to strict-null (9)

* Strict some more nulls (still a few errors remaining)

* adapt rest of the files to strict-null (9)

* fix tests (check for null instead of undefined)

* repalce a lot of `??` with bangs

And added TODO notice as usual

* fix more tests

* all tests pass now

* fix broken game-loop after trainer battle

add some console.warn for missing cases and falling back to default

* remove guessed fallback from utils.rgbHexToRgba

* add TODO for this.currentBattle = null

* adjust   getPokemonById() return to include `null`

* fix compilation errors

* add test for pokemon.trySetStatus

* `chanceMultiplier` shouldn't be optional

* allow `null` for currentPhase

* adjust hasExpSprite logic for no keymatch found

* reduce bang usage in account.updateUserInfo()

* fix new strict-null issues after merge

* fix `strict-null` issues in dropdown.ts

and sand_spit.test.ts

* fix egg-gacha

* adapt gul_missile.test.ts to strict-null

* fix move.ts strict-null

* fix i18n.ts strict-null

* fix strict-null issues

* fix baton_pass test

after accidentially breaking it

* chore: fix compiler errors

* revert accidential changes in baton_pass.test.ts

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2024-08-07 17:23:12 +01:00

544 lines
18 KiB
TypeScript

import {default as BattleScene} from "../battle-scene";
import UiHandler from "./ui-handler";
import BattleMessageUiHandler from "./battle-message-ui-handler";
import CommandUiHandler from "./command-ui-handler";
import PartyUiHandler from "./party-ui-handler";
import FightUiHandler from "./fight-ui-handler";
import MessageUiHandler from "./message-ui-handler";
import ConfirmUiHandler from "./confirm-ui-handler";
import ModifierSelectUiHandler from "./modifier-select-ui-handler";
import BallUiHandler from "./ball-ui-handler";
import SummaryUiHandler from "./summary-ui-handler";
import StarterSelectUiHandler from "./starter-select-ui-handler";
import EvolutionSceneHandler from "./evolution-scene-handler";
import TargetSelectUiHandler from "./target-select-ui-handler";
import SettingsUiHandler from "./settings/settings-ui-handler";
import SettingsGamepadUiHandler from "./settings/settings-gamepad-ui-handler";
import GameChallengesUiHandler from "./challenges-select-ui-handler";
import { TextStyle, addTextObject } from "./text";
import AchvBar from "./achv-bar";
import MenuUiHandler from "./menu-ui-handler";
import AchvsUiHandler from "./achvs-ui-handler";
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 VouchersUiHandler from "./vouchers-ui-handler";
import {addWindow} from "./ui-theme";
import LoginFormUiHandler from "./login-form-ui-handler";
import RegistrationFormUiHandler from "./registration-form-ui-handler";
import LoadingModalUiHandler from "./loading-modal-ui-handler";
import * as Utils from "../utils";
import GameStatsUiHandler from "./game-stats-ui-handler";
import AwaitableUiHandler from "./awaitable-ui-handler";
import SaveSlotSelectUiHandler from "./save-slot-select-ui-handler";
import TitleUiHandler from "./title-ui-handler";
import SavingIconHandler from "./saving-icon-handler";
import UnavailableModalUiHandler from "./unavailable-modal-ui-handler";
import OutdatedModalUiHandler from "./outdated-modal-ui-handler";
import SessionReloadModalUiHandler from "./session-reload-modal-ui-handler";
import {Button} from "#enums/buttons";
import i18next, {ParseKeys} from "i18next";
import GamepadBindingUiHandler from "./settings/gamepad-binding-ui-handler";
import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler";
import KeyboardBindingUiHandler from "#app/ui/settings/keyboard-binding-ui-handler";
import SettingsDisplayUiHandler from "./settings/settings-display-ui-handler";
import SettingsAudioUiHandler from "./settings/settings-audio-ui-handler";
import { PlayerGender } from "#enums/player-gender";
import BgmBar from "#app/ui/bgm-bar";
import RenameFormUiHandler from "./rename-form-ui-handler";
export enum Mode {
MESSAGE,
TITLE,
COMMAND,
FIGHT,
BALL,
TARGET_SELECT,
MODIFIER_SELECT,
SAVE_SLOT,
PARTY,
SUMMARY,
STARTER_SELECT,
EVOLUTION_SCENE,
EGG_HATCH_SCENE,
CONFIRM,
OPTION_SELECT,
MENU,
MENU_OPTION_SELECT,
SETTINGS,
SETTINGS_DISPLAY,
SETTINGS_AUDIO,
SETTINGS_GAMEPAD,
GAMEPAD_BINDING,
SETTINGS_KEYBOARD,
KEYBOARD_BINDING,
ACHIEVEMENTS,
GAME_STATS,
VOUCHERS,
EGG_LIST,
EGG_GACHA,
LOGIN_FORM,
REGISTRATION_FORM,
LOADING,
SESSION_RELOAD,
UNAVAILABLE,
OUTDATED,
CHALLENGE_SELECT,
RENAME_POKEMON
}
const transitionModes = [
Mode.SAVE_SLOT,
Mode.PARTY,
Mode.SUMMARY,
Mode.STARTER_SELECT,
Mode.EVOLUTION_SCENE,
Mode.EGG_HATCH_SCENE,
Mode.EGG_LIST,
Mode.EGG_GACHA,
Mode.CHALLENGE_SELECT
];
const noTransitionModes = [
Mode.TITLE,
Mode.CONFIRM,
Mode.OPTION_SELECT,
Mode.MENU,
Mode.MENU_OPTION_SELECT,
Mode.GAMEPAD_BINDING,
Mode.KEYBOARD_BINDING,
Mode.SETTINGS,
Mode.SETTINGS_AUDIO,
Mode.SETTINGS_DISPLAY,
Mode.SETTINGS_GAMEPAD,
Mode.SETTINGS_KEYBOARD,
Mode.ACHIEVEMENTS,
Mode.GAME_STATS,
Mode.VOUCHERS,
Mode.LOGIN_FORM,
Mode.REGISTRATION_FORM,
Mode.LOADING,
Mode.SESSION_RELOAD,
Mode.UNAVAILABLE,
Mode.OUTDATED,
Mode.RENAME_POKEMON
];
export default class UI extends Phaser.GameObjects.Container {
private mode: Mode;
private modeChain: Mode[];
public handlers: UiHandler[];
private overlay: Phaser.GameObjects.Rectangle;
public achvBar: AchvBar;
public bgmBar: BgmBar;
public savingIcon: SavingIconHandler;
private tooltipContainer: Phaser.GameObjects.Container;
private tooltipBg: Phaser.GameObjects.NineSlice;
private tooltipTitle: Phaser.GameObjects.Text;
private tooltipContent: Phaser.GameObjects.Text;
private overlayActive: boolean;
constructor(scene: BattleScene) {
super(scene, 0, scene.game.canvas.height / 6);
this.mode = Mode.MESSAGE;
this.modeChain = [];
this.handlers = [
new BattleMessageUiHandler(scene),
new TitleUiHandler(scene),
new CommandUiHandler(scene),
new FightUiHandler(scene),
new BallUiHandler(scene),
new TargetSelectUiHandler(scene),
new ModifierSelectUiHandler(scene),
new SaveSlotSelectUiHandler(scene),
new PartyUiHandler(scene),
new SummaryUiHandler(scene),
new StarterSelectUiHandler(scene),
new EvolutionSceneHandler(scene),
new EggHatchSceneHandler(scene),
new ConfirmUiHandler(scene),
new OptionSelectUiHandler(scene),
new MenuUiHandler(scene),
new OptionSelectUiHandler(scene, Mode.MENU_OPTION_SELECT),
// settings
new SettingsUiHandler(scene),
new SettingsDisplayUiHandler(scene),
new SettingsAudioUiHandler(scene),
new SettingsGamepadUiHandler(scene),
new GamepadBindingUiHandler(scene),
new SettingsKeyboardUiHandler(scene),
new KeyboardBindingUiHandler(scene),
new AchvsUiHandler(scene),
new GameStatsUiHandler(scene),
new VouchersUiHandler(scene),
new EggListUiHandler(scene),
new EggGachaUiHandler(scene),
new LoginFormUiHandler(scene),
new RegistrationFormUiHandler(scene),
new LoadingModalUiHandler(scene),
new SessionReloadModalUiHandler(scene),
new UnavailableModalUiHandler(scene),
new OutdatedModalUiHandler(scene),
new GameChallengesUiHandler(scene),
new RenameFormUiHandler(scene),
];
}
setup(): void {
this.setName(`ui-${Mode[this.mode]}`);
for (const handler of this.handlers) {
handler.setup();
}
this.overlay = this.scene.add.rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6, 0);
this.overlay.setName("rect-ui-overlay");
this.overlay.setOrigin(0, 0);
(this.scene as BattleScene).uiContainer.add(this.overlay);
this.overlay.setVisible(false);
this.setupTooltip();
this.achvBar = new AchvBar(this.scene as BattleScene);
this.achvBar.setup();
(this.scene as BattleScene).uiContainer.add(this.achvBar);
this.savingIcon = new SavingIconHandler(this.scene as BattleScene);
this.savingIcon.setup();
(this.scene as BattleScene).uiContainer.add(this.savingIcon);
}
private setupTooltip() {
this.tooltipContainer = this.scene.add.container(0, 0);
this.tooltipContainer.setName("tooltip");
this.tooltipContainer.setVisible(false);
this.tooltipBg = addWindow(this.scene as BattleScene, 0, 0, 128, 31);
this.tooltipBg.setName("window-tooltip-bg");
this.tooltipBg.setOrigin(0, 0);
this.tooltipTitle = addTextObject(this.scene, 64, 4, "", TextStyle.TOOLTIP_TITLE);
this.tooltipTitle.setName("text-tooltip-title");
this.tooltipTitle.setOrigin(0.5, 0);
this.tooltipContent = addTextObject(this.scene, 6, 16, "", TextStyle.TOOLTIP_CONTENT);
this.tooltipContent.setName("text-tooltip-content");
this.tooltipContent.setWordWrapWidth(696);
this.tooltipContainer.add(this.tooltipBg);
this.tooltipContainer.add(this.tooltipTitle);
this.tooltipContainer.add(this.tooltipContent);
(this.scene as BattleScene).uiContainer.add(this.tooltipContainer);
}
getHandler<H extends UiHandler = UiHandler>(): H {
return this.handlers[this.mode] as H;
}
getMessageHandler(): BattleMessageUiHandler {
return this.handlers[Mode.MESSAGE] as BattleMessageUiHandler;
}
processInfoButton(pressed: boolean) {
if (this.overlayActive) {
return false;
}
const battleScene = this.scene as BattleScene;
if ([Mode.CONFIRM, Mode.COMMAND, Mode.FIGHT, Mode.MESSAGE].includes(this.mode)) {
battleScene?.processInfoButton(pressed);
return true;
}
battleScene?.processInfoButton(false);
return true;
}
processInput(button: Button): boolean {
if (this.overlayActive) {
return false;
}
const handler = this.getHandler();
if (handler instanceof AwaitableUiHandler && handler.tutorialActive) {
return handler.processTutorialInput(button);
}
return handler.processInput(button);
}
showText(text: string, delay?: integer | null, callback?: Function | null, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null): void {
if (prompt && text.indexOf("$") > -1) {
const messagePages = text.split(/\$/g).map(m => m.trim());
let showMessageAndCallback = () => callback && callback();
for (let p = messagePages.length - 1; p >= 0; p--) {
const originalFunc = showMessageAndCallback;
showMessageAndCallback = () => this.showText(messagePages[p], null, originalFunc, null, true);
}
showMessageAndCallback();
} else {
const handler = this.getHandler();
if (handler instanceof MessageUiHandler) {
(handler as MessageUiHandler).showText(text, delay, callback, callbackDelay, prompt, promptDelay);
} else {
this.getMessageHandler().showText(text, delay, callback, callbackDelay, prompt, promptDelay);
}
}
}
showDialogue(text: string, name: string | undefined, delay: integer | null = 0, callback: Function, callbackDelay?: integer, promptDelay?: integer): void {
// First get the gender of the player (default male) (also used if UNSET)
let playerGenderPrefix = "PGM";
if ((this.scene as BattleScene).gameData.gender === PlayerGender.FEMALE) {
playerGenderPrefix = "PGF";
}
// Add the prefix to the text
const localizationKey: string = playerGenderPrefix + text;
// Get localized dialogue (if available)
let hasi18n = false;
if (i18next.exists(localizationKey) ) {
text = i18next.t(localizationKey as ParseKeys);
hasi18n = true;
// Skip dialogue if the player has enabled the option and the dialogue has been already seen
if ((this.scene as BattleScene).skipSeenDialogues && (this.scene as BattleScene).gameData.getSeenDialogues()[localizationKey] === true) {
console.log(`Dialogue ${localizationKey} skipped`);
callback();
return;
}
}
let showMessageAndCallback = () => {
hasi18n && (this.scene as BattleScene).gameData.saveSeenDialogue(localizationKey);
callback();
};
if (text.indexOf("$") > -1) {
const messagePages = text.split(/\$/g).map(m => m.trim());
for (let p = messagePages.length - 1; p >= 0; p--) {
const originalFunc = showMessageAndCallback;
showMessageAndCallback = () => this.showDialogue(messagePages[p], name, null, originalFunc);
}
showMessageAndCallback();
} else {
const handler = this.getHandler();
if (handler instanceof MessageUiHandler) {
(handler as MessageUiHandler).showDialogue(text, name, delay, showMessageAndCallback, callbackDelay, true, promptDelay);
} else {
this.getMessageHandler().showDialogue(text, name, delay, showMessageAndCallback, callbackDelay, true, promptDelay);
}
}
}
shouldSkipDialogue(text): boolean {
let playerGenderPrefix = "PGM";
if ((this.scene as BattleScene).gameData.gender === PlayerGender.FEMALE) {
playerGenderPrefix = "PGF";
}
const key = playerGenderPrefix + text;
if (i18next.exists(key) ) {
if ((this.scene as BattleScene).skipSeenDialogues && (this.scene as BattleScene).gameData.getSeenDialogues()[key] === true) {
return true;
}
}
return false;
}
showTooltip(title: string, content: string, overlap?: boolean): void {
this.tooltipContainer.setVisible(true);
this.tooltipTitle.setText(title || "");
const wrappedContent = this.tooltipContent.runWordWrap(content);
this.tooltipContent.setText(wrappedContent);
this.tooltipContent.y = title ? 16 : 4;
this.tooltipBg.width = Math.min(Math.max(this.tooltipTitle.displayWidth, this.tooltipContent.displayWidth) + 12, 684);
this.tooltipBg.height = (title ? 31 : 19) + 10.5 * (wrappedContent.split("\n").length - 1);
if (overlap) {
(this.scene as BattleScene).uiContainer.moveAbove(this.tooltipContainer, this);
} else {
(this.scene as BattleScene).uiContainer.moveBelow(this.tooltipContainer, this);
}
}
hideTooltip(): void {
this.tooltipContainer.setVisible(false);
this.tooltipTitle.clearTint();
}
update(): void {
if (this.tooltipContainer.visible) {
const reverse = this.scene.game.input.mousePointer && this.scene.game.input.mousePointer.x >= this.scene.game.canvas.width - this.tooltipBg.width * 6 - 12;
this.tooltipContainer.setPosition(!reverse ? this.scene.game.input.mousePointer!.x / 6 + 2 : this.scene.game.input.mousePointer!.x / 6 - this.tooltipBg.width - 2, this.scene.game.input.mousePointer!.y / 6 + 2); // TODO: are these bangs correct?
}
}
clearText(): void {
const handler = this.getHandler();
if (handler instanceof MessageUiHandler) {
(handler as MessageUiHandler).clearText();
} else {
this.getMessageHandler().clearText();
}
}
setCursor(cursor: integer): boolean {
const changed = this.getHandler().setCursor(cursor);
if (changed) {
this.playSelect();
}
return changed;
}
playSelect(): void {
(this.scene as BattleScene).playSound("select");
}
playError(): void {
(this.scene as BattleScene).playSound("error");
}
fadeOut(duration: integer): Promise<void> {
return new Promise(resolve => {
if (this.overlayActive) {
return resolve();
}
this.overlayActive = true;
this.overlay.setAlpha(0);
this.overlay.setVisible(true);
this.scene.tweens.add({
targets: this.overlay,
alpha: 1,
duration: duration,
ease: "Sine.easeOut",
onComplete: () => resolve()
});
});
}
fadeIn(duration: integer): Promise<void> {
return new Promise(resolve => {
if (!this.overlayActive) {
return resolve();
}
this.scene.tweens.add({
targets: this.overlay,
alpha: 0,
duration: duration,
ease: "Sine.easeIn",
onComplete: () => {
this.overlay.setVisible(false);
resolve();
}
});
this.overlayActive = false;
});
}
private setModeInternal(mode: Mode, clear: boolean, forceTransition: boolean, chainMode: boolean, args: any[]): Promise<void> {
return new Promise(resolve => {
if (this.mode === mode && !forceTransition) {
resolve();
return;
}
const doSetMode = () => {
if (this.mode !== mode) {
if (clear) {
this.getHandler().clear();
}
if (chainMode && this.mode && !clear) {
this.modeChain.push(this.mode);
}
this.mode = mode;
const touchControls = document?.getElementById("touchControls");
if (touchControls) {
touchControls.dataset.uiMode = Mode[mode];
}
this.getHandler().show(args);
}
resolve();
};
if (((!chainMode && ((transitionModes.indexOf(this.mode) > -1 || transitionModes.indexOf(mode) > -1)
&& (noTransitionModes.indexOf(this.mode) === -1 && noTransitionModes.indexOf(mode) === -1)))
|| (chainMode && noTransitionModes.indexOf(mode) === -1))) {
this.fadeOut(250).then(() => {
this.scene.time.delayedCall(100, () => {
doSetMode();
this.fadeIn(250);
});
});
} else {
doSetMode();
}
});
}
getMode(): Mode {
return this.mode;
}
setMode(mode: Mode, ...args: any[]): Promise<void> {
return this.setModeInternal(mode, true, false, false, args);
}
setModeForceTransition(mode: Mode, ...args: any[]): Promise<void> {
return this.setModeInternal(mode, true, true, false, args);
}
setModeWithoutClear(mode: Mode, ...args: any[]): Promise<void> {
return this.setModeInternal(mode, false, false, false, args);
}
setOverlayMode(mode: Mode, ...args: any[]): Promise<void> {
return this.setModeInternal(mode, false, false, true, args);
}
revertMode(): Promise<boolean> {
return new Promise<boolean>(resolve => {
if (!this?.modeChain?.length) {
return resolve(false);
}
const lastMode = this.mode;
const doRevertMode = () => {
this.getHandler().clear();
this.mode = this.modeChain.pop()!; // TODO: is this bang correct?
const touchControls = document.getElementById("touchControls");
if (touchControls) {
touchControls.dataset.uiMode = Mode[this.mode];
}
resolve(true);
};
if (noTransitionModes.indexOf(lastMode) === -1) {
this.fadeOut(250).then(() => {
this.scene.time.delayedCall(100, () => {
doRevertMode();
this.fadeIn(250);
});
});
} else {
doRevertMode();
}
});
}
revertModes(): Promise<void> {
return new Promise<void>(resolve => {
if (!this?.modeChain?.length) {
return resolve();
}
this.revertMode().then(success => Utils.executeIf(success, this.revertModes).then(() => resolve()));
});
}
public getModeChain(): Mode[] {
return this.modeChain;
}
}