diff --git a/assets b/assets index a36741a2112..9398703b8f0 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit a36741a2112217eaf067248d7d1917266339a56d +Subproject commit 9398703b8f0e437ef6d5212373e2ae8465b3bfda diff --git a/locales b/locales index 6b5e2130256..fe7025942cf 160000 --- a/locales +++ b/locales @@ -1 +1 @@ -Subproject commit 6b5e2130256dd521908f15a485d045fb36baca41 +Subproject commit fe7025942cf5ad1391380cc4e52695535c56c986 diff --git a/src/phases/login-phase.ts b/src/phases/login-phase.ts index aebdcdae152..9747eb2e9c7 100644 --- a/src/phases/login-phase.ts +++ b/src/phases/login-phase.ts @@ -79,51 +79,14 @@ export class LoginPhase extends Phase { } private showLoginRegister(): void { - const { gameData, phaseManager, ui } = globalScene; - - const backButton = () => { - phaseManager.unshiftNew("LoginPhase", false); - this.end(); - }; - - const checkUserInfo = async (): Promise => { - ui.playSelect(); - const success = await updateUserInfo(); - if (!success[0]) { - removeCookie(sessionIdKey); - globalScene.reset(true, true); - return false; - } - return true; - }; - - const loginButton = async () => { - const success = await checkUserInfo(); - if (!success) { - return; - } - await gameData.loadSystem(); - this.end(); - }; - - const registerButton = async () => { - const success = await checkUserInfo(); - if (!success) { - return; - } - this.end(); - }; + const { ui } = globalScene; const goToLoginButton = () => { - globalScene.playSound("ui/menu_open"); - - ui.setMode(UiMode.LOGIN_FORM, { buttonActions: [loginButton, backButton] }); + this.goToLogin(); }; const goToRegistrationButton = () => { - globalScene.playSound("ui/menu_open"); - - ui.setMode(UiMode.REGISTRATION_FORM, { buttonActions: [registerButton, backButton] }); + this.goToRegister(); }; if (this.showText) { @@ -134,4 +97,56 @@ export class LoginPhase extends Phase { ui.setMode(UiMode.LOGIN_OR_REGISTER, { buttonActions: [goToLoginButton, goToRegistrationButton] }); } + + private async checkUserInfo(): Promise { + globalScene.ui.playSelect(); + const success = await updateUserInfo(); + if (!success[0]) { + removeCookie(sessionIdKey); + globalScene.reset(true, true); + return false; + } + return true; + } + + public goToLogin(): void { + const { gameData, ui, phaseManager } = globalScene; + + const backButton = () => { + phaseManager.unshiftNew("LoginPhase", false); + this.end(); + }; + + const loginButton = async () => { + const success = await this.checkUserInfo(); + if (!success) { + return; + } + await gameData.loadSystem(); + this.end(); + }; + globalScene.playSound("ui/menu_open"); + + ui.setMode(UiMode.LOGIN_FORM, { buttonActions: [loginButton, backButton] }); + } + + public goToRegister(): void { + const { phaseManager, ui } = globalScene; + + const backButton = () => { + phaseManager.unshiftNew("LoginPhase", false); + this.end(); + }; + + const registerButton = async () => { + const success = await this.checkUserInfo(); + if (!success) { + return; + } + this.end(); + }; + globalScene.playSound("ui/menu_open"); + + ui.setMode(UiMode.REGISTRATION_FORM, { buttonActions: [registerButton, backButton] }); + } } diff --git a/src/plugins/api/api-base.ts b/src/plugins/api/api-base.ts index 842b16d5ce0..94d7737d2c0 100644 --- a/src/plugins/api/api-base.ts +++ b/src/plugins/api/api-base.ts @@ -74,6 +74,7 @@ export abstract class ApiBase { console.log(`Sending ${config.method ?? "GET"} request to: `, this.base + path, config); } + // TODO: need some sort of error handling here? return await fetch(this.base + path, config); } diff --git a/src/plugins/api/pokerogue-account-api.ts b/src/plugins/api/pokerogue-account-api.ts index fa9414210bc..8d328abea68 100644 --- a/src/plugins/api/pokerogue-account-api.ts +++ b/src/plugins/api/pokerogue-account-api.ts @@ -52,7 +52,7 @@ export class PokerogueAccountApi extends ApiBase { console.warn("Register failed!", err); } - return "Unknown error!"; + return "Unknown registration error!"; } /** @@ -76,7 +76,7 @@ export class PokerogueAccountApi extends ApiBase { console.warn("Login failed!", err); } - return "Unknown error!"; + return "Unknown login error!"; } /** diff --git a/src/system/settings/settings-language.ts b/src/system/settings/settings-language.ts index b1df2444507..0c7cbb400aa 100644 --- a/src/system/settings/settings-language.ts +++ b/src/system/settings/settings-language.ts @@ -1,4 +1,5 @@ import { globalScene } from "#app/global-scene"; +import type { LoginRegisterInfoContainerUiHandler } from "#ui/login-register-info-container-ui-handler"; import type { SettingsDisplayUiHandler } from "#ui/settings-display-ui-handler"; import i18next from "i18next"; @@ -8,6 +9,8 @@ const cancelHandler = () => { // Reset the cursor to the current language, if in the settings menu if (handler && typeof (handler as SettingsDisplayUiHandler).setOptionCursor === "function") { (handler as SettingsDisplayUiHandler).setOptionCursor(-1, 0, true); + } else if (handler && typeof (handler as LoginRegisterInfoContainerUiHandler).setInteractive === "function") { + (handler as LoginRegisterInfoContainerUiHandler).setInteractive(true); } }; diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 4a811f45c41..fcb2a85a15d 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -708,7 +708,7 @@ export class TimedEventManager { } export class TimedEventDisplay extends Phaser.GameObjects.Container { - private event: TimedEvent | nil; + private readonly event: TimedEvent | nil; private eventTimerText: Phaser.GameObjects.Text; private banner: Phaser.GameObjects.Image; private availableWidth: number; @@ -725,7 +725,7 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container { * Set the width that can be used to display the event timer and banner. By default * these elements get centered horizontally in that space, in the bottom left of the screen */ - setWidth(width: number) { + public setWidth(width: number): void { if (width !== this.availableWidth) { this.availableWidth = width; const xPosition = this.availableWidth / 2 + (this.event?.xOffset ?? 0); @@ -738,7 +738,7 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container { } } - setup() { + public setup(): void { const lang = i18next.resolvedLanguage; if (this.event?.bannerKey) { let key = this.event.bannerKey; @@ -775,7 +775,7 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container { } } - show() { + public show(): void { this.setVisible(true); this.updateCountdown(); @@ -784,13 +784,13 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container { }, 1000); } - clear() { + public clear(): void { this.setVisible(false); this.eventTimer && clearInterval(this.eventTimer); this.eventTimer = null; } - private timeToGo(date: Date) { + private timeToGo(date: Date): string { // Utility to add leading zero function z(n) { return (n < 10 ? "0" : "") + n; @@ -816,8 +816,8 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container { }); } - updateCountdown() { - if (this.event && this.event.eventType !== EventType.NO_TIMER_DISPLAY) { + private updateCountdown(): void { + if (this.event && this.event.eventType !== EventType.NO_TIMER_DISPLAY && this.eventTimerText.visible) { this.eventTimerText.setText(this.timeToGo(this.event.endDate)); } } diff --git a/src/ui/handlers/login-or-register-ui-handler.ts b/src/ui/handlers/login-or-register-ui-handler.ts index b5733a9eaee..e3bcc27afea 100644 --- a/src/ui/handlers/login-or-register-ui-handler.ts +++ b/src/ui/handlers/login-or-register-ui-handler.ts @@ -29,7 +29,6 @@ export class LoginOrRegisterUiHandler extends LoginRegisterInfoContainerUiHandle return [i18next.t("menu:login"), i18next.t("menu:register")]; } - // TODO: use mixins so it's not necessary to inherit from `FormModalUiHandler` public override getInputFieldConfigs(): InputFieldConfig[] { return []; } diff --git a/src/ui/handlers/login-register-info-container-ui-handler.ts b/src/ui/handlers/login-register-info-container-ui-handler.ts index 556ab94e15f..89a166da73b 100644 --- a/src/ui/handlers/login-register-info-container-ui-handler.ts +++ b/src/ui/handlers/login-register-info-container-ui-handler.ts @@ -7,6 +7,7 @@ import type { ModalConfig } from "#ui/modal-ui-handler"; import { fixedInt } from "#utils/common"; import i18next from "i18next"; import JSZip from "jszip"; +import type InputText from "phaser3-rex-plugins/plugins/inputtext"; interface BuildInteractableImageOpts { scale?: number; @@ -30,12 +31,12 @@ const MAX_SAVES_FOR_USERNAME_PANEL = 7; const ERR_NO_SAVES: string = "No save files found"; const ERR_TOO_MANY_SAVES: string = "Too many save files found"; -// TODO: use mixins export abstract class LoginRegisterInfoContainerUiHandler extends FormModalUiHandler { private usernameInfoImage: Phaser.GameObjects.Image; private saveDownloadImage: Phaser.GameObjects.Image; private changeLanguageImage: Phaser.GameObjects.Image; private infoContainer: Phaser.GameObjects.Container; + private lastFocusedInput: InputText | null = null; public override getReadableErrorMessage(error: string): string { if (!error) { @@ -109,6 +110,7 @@ export abstract class LoginRegisterInfoContainerUiHandler extends FormModalUiHan this.changeLanguageImage // .setPositionRelative(this.infoContainer, 40, 0) .on("pointerdown", () => { + this.setInteractive(false); globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, { options: languageOptions, maxOptions: 7, @@ -154,6 +156,7 @@ export abstract class LoginRegisterInfoContainerUiHandler extends FormModalUiHan const handler = () => { globalScene.ui.revertMode(); this.infoContainer.disableInteractive(); + this.setInteractive(true); return true; }; @@ -162,6 +165,7 @@ export abstract class LoginRegisterInfoContainerUiHandler extends FormModalUiHan } globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, { options, delay: 1000 }); + this.setInteractive(false); this.infoContainer.setInteractive( new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width, globalScene.game.canvas.height), Phaser.Geom.Rectangle.Contains, @@ -230,4 +234,45 @@ export abstract class LoginRegisterInfoContainerUiHandler extends FormModalUiHan return img; } + + /** + * Enable or disable interactivity on all interactive objects. + * @param active - Whether to enable or disable interactivity + */ + public setInteractive(active: boolean): void { + const objects = [...this.buttonBgs, this.usernameInfoImage, this.saveDownloadImage, this.changeLanguageImage]; + + for (const obj of objects) { + if (active) { + obj.setInteractive(); + } else { + obj.disableInteractive(); + if (obj instanceof Phaser.GameObjects.Image) { + obj.clearTint(); + } + } + } + + this.setInteractiveInputs(active); + this.setMouseCursorStyle("default"); + } + + /** + * Enable or disable interactivity on all input fields. + * @param active - Whether to enable or disable interactivity + */ + private setInteractiveInputs(active: boolean): void { + if (active) { + // `setFocus` doesn't focus without a timeout + setTimeout(() => { + this.lastFocusedInput?.setFocus(); + this.lastFocusedInput = null; + }, 50); + } else { + this.lastFocusedInput = this.inputs.find(input => input.isFocused) ?? null; + } + for (const input of this.inputs) { + (input.node as HTMLInputElement).disabled = !active; + } + } } diff --git a/src/ui/handlers/oauth-providers-ui-handler.ts b/src/ui/handlers/oauth-providers-ui-handler.ts index 8839f6880a1..4b02741fdf5 100644 --- a/src/ui/handlers/oauth-providers-ui-handler.ts +++ b/src/ui/handlers/oauth-providers-ui-handler.ts @@ -6,7 +6,6 @@ import { addWindow } from "#ui/ui-theme"; import { fixedInt } from "#utils/common"; import i18next from "i18next"; -// TODO: use mixins export abstract class OAuthProvidersUiHandler extends LoginRegisterInfoContainerUiHandler { private discordImage: Phaser.GameObjects.Image; private googleImage: Phaser.GameObjects.Image; @@ -56,6 +55,18 @@ export abstract class OAuthProvidersUiHandler extends LoginRegisterInfoContainer this.getUi().add(this.externalPartyContainer); } + public override setInteractive(active: boolean): void { + super.setInteractive(active); + const externalPartyIcons = this.externalPartyContainer.list.filter(obj => obj instanceof Phaser.GameObjects.Image); + for (const obj of externalPartyIcons) { + if (active) { + obj.setInteractive(); + } else { + obj.disableInteractive(); + } + } + } + protected processExternalProvider(): void { const titleX = 22; this.externalPartyTitle diff --git a/src/ui/handlers/party-ui-handler.ts b/src/ui/handlers/party-ui-handler.ts index 477ef2bf1ef..afb5dad29eb 100644 --- a/src/ui/handlers/party-ui-handler.ts +++ b/src/ui/handlers/party-ui-handler.ts @@ -1515,9 +1515,6 @@ export class PartyUiHandler extends MessageUiHandler { ); } this.addCommonOptions(pokemon); - if (this.partyUiMode === PartyUiMode.SWITCH && pokemon.isFusion()) { - this.options.push(PartyOption.UNSPLICE); - } break; case PartyUiMode.REVIVAL_BLESSING: this.options.push(PartyOption.REVIVE); @@ -1551,6 +1548,9 @@ export class PartyUiHandler extends MessageUiHandler { case PartyUiMode.CHECK: this.addCommonOptions(pokemon); if (globalScene.phaseManager.getCurrentPhase().is("SelectModifierPhase")) { + if (pokemon.isFusion()) { + this.options.push(PartyOption.UNSPLICE); + } this.options.push(PartyOption.RELEASE); const formChangeItemModifiers = this.getFormChangeItemsModifiers(pokemon); for (let i = 0; i < formChangeItemModifiers.length; i++) { diff --git a/src/ui/handlers/registration-form-ui-handler.ts b/src/ui/handlers/registration-form-ui-handler.ts index 250a243e494..8a92fc3563b 100644 --- a/src/ui/handlers/registration-form-ui-handler.ts +++ b/src/ui/handlers/registration-form-ui-handler.ts @@ -2,9 +2,11 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { globalScene } from "#app/global-scene"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; +import type { LoginPhase } from "#phases/login-phase"; import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; import type { ModalConfig } from "#ui/modal-ui-handler"; import { addTextObject } from "#ui/text"; +import { fixedInt } from "#utils/common"; import i18next from "i18next"; import { LoginRegisterInfoContainerUiHandler } from "./login-register-info-container-ui-handler"; @@ -86,7 +88,7 @@ export class RegistrationFormUiHandler extends LoginRegisterInfoContainerUiHandl this.submitAction = originalRegistrationAction; this.sanitizeInputs(); globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); - const onFail = error => { + const onFail = (error: string) => { globalScene.ui.setMode(UiMode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() })); globalScene.ui.playError(); }; @@ -109,18 +111,25 @@ export class RegistrationFormUiHandler extends LoginRegisterInfoContainerUiHandl if (registerError) { onFail(registerError); } else { - pokerogueApi.account - .login({ - username: usernameInput.text, - password: passwordInput.text, - }) - .then(loginError => { - if (loginError) { - onFail(loginError); - } else { - originalRegistrationAction?.(); - } - }); + const username = usernameInput.text; + const password = passwordInput.text; + pokerogueApi.account.login({ username, password }).then(loginError => { + if (loginError) { + // retry once if the first attempt fails + const retryLogin = () => { + pokerogueApi.account.login({ username, password }).then(error => { + if (error) { + (globalScene.phaseManager.getCurrentPhase() as LoginPhase).goToLogin(); + } else { + originalRegistrationAction?.(); + } + }); + }; + globalScene.time.delayedCall(fixedInt(2000), retryLogin); + } else { + originalRegistrationAction?.(); + } + }); } }); } diff --git a/src/ui/handlers/title-ui-handler.ts b/src/ui/handlers/title-ui-handler.ts index de3bbecb0ce..5a55495edea 100644 --- a/src/ui/handlers/title-ui-handler.ts +++ b/src/ui/handlers/title-ui-handler.ts @@ -261,6 +261,7 @@ export class TitleUiHandler extends OptionSelectUiHandler { private getSnow(): void { const width = globalScene.scaledCanvas.width; const height = globalScene.scaledCanvas.height; + this.snow?.destroy(); // Ensures no duplicate snow layers this.snow = globalScene.add.tileSprite(width, height, width, height, "snow"); this.snow.setOrigin(1, 1); diff --git a/test/plugins/api/pokerogue-account-api.test.ts b/test/plugins/api/pokerogue-account-api.test.ts index ec309b87069..91351d248df 100644 --- a/test/plugins/api/pokerogue-account-api.test.ts +++ b/test/plugins/api/pokerogue-account-api.test.ts @@ -85,12 +85,12 @@ describe("Pokerogue Account API", () => { expect(error).toBe("Username is already taken"); }); - it('should return "Unknown error" and report a warning on ERROR', async () => { + it('should return "Unknown registration error!" and report a warning on ERROR', async () => { server.use(http.post(`${apiBase}/account/register`, () => HttpResponse.error())); const error = await accountApi.register(registerParams); - expect(error).toBe("Unknown error!"); + expect(error).toBe("Unknown registration error!"); expect(console.warn).toHaveBeenCalledWith("Register failed!", expect.any(Error)); }); }); @@ -119,12 +119,12 @@ describe("Pokerogue Account API", () => { expect(console.warn).toHaveBeenCalledWith("Login failed!", 401, "Unauthorized"); }); - it('should return "Unknown error" and report a warning on ERROR', async () => { + it('should return "Unknown login error!" and report a warning on ERROR', async () => { server.use(http.post(`${apiBase}/account/login`, () => HttpResponse.error())); const error = await accountApi.login(loginParams); - expect(error).toBe("Unknown error!"); + expect(error).toBe("Unknown login error!"); expect(console.warn).toHaveBeenCalledWith("Login failed!", expect.any(Error)); }); });