[Bug][Refactor] fix username-finder issues + code & visual improvements (#4055)

* fix username-finder issues & refactor `login-form-ui-handler`

- reduce redundancy
- add hover effect for interactable game objects
- add error handler for "No save files found!"
- Make user finder errors support i18n

* add `disableInteractive` to mockContainer
This commit is contained in:
flx-sta 2024-09-05 17:07:24 -07:00 committed by GitHub
parent 39f3572c1b
commit f3bdaa12ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 127 additions and 64 deletions

View File

@ -51,5 +51,7 @@
"renamePokemon": "Rename Pokémon",
"rename": "Rename",
"nickname": "Nickname",
"errorServerDown": "Oops! There was an issue contacting the server.\n\nYou may leave this window open,\nthe game will automatically reconnect."
"errorServerDown": "Oops! There was an issue contacting the server.\n\nYou may leave this window open,\nthe game will automatically reconnect.",
"noSaves": "You don't have any save files on record!",
"tooManySaves": "You have too many save files on record!"
}

View File

@ -208,4 +208,5 @@ export default class MockContainer implements MockGameObject {
return this.list;
}
disableInteractive = vi.fn();
}

View File

@ -8,7 +8,21 @@ import { addTextObject, TextStyle } from "./text";
import { addWindow } from "./ui-theme";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
interface BuildInteractableImageOpts {
scale?: number;
x?: number;
y?: number;
origin?: { x: number; y: number };
}
export default class LoginFormUiHandler extends FormModalUiHandler {
private readonly ERR_USERNAME: string = "invalid username";
private readonly ERR_PASSWORD: string = "invalid password";
private readonly ERR_ACCOUNT_EXIST: string = "account doesn't exist";
private readonly ERR_PASSWORD_MATCH: string = "password doesn't match";
private readonly ERR_NO_SAVES: string = "No save files found";
private readonly ERR_TOO_MANY_SAVES: string = "Too many save files found";
private googleImage: Phaser.GameObjects.Image;
private discordImage: Phaser.GameObjects.Image;
private usernameInfoImage: Phaser.GameObjects.Image;
@ -21,8 +35,23 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
}
setup(): void {
super.setup();
this.buildExternalPartyContainer();
this.infoContainer = this.scene.add.container(0, 0);
this.usernameInfoImage = this.buildInteractableImage("settings_icon", "username-info-icon", {
x: 20,
scale: 0.5
});
this.infoContainer.add(this.usernameInfoImage);
this.getUi().add(this.infoContainer);
this.infoContainer.setVisible(false);
this.infoContainer.disableInteractive();
}
private buildExternalPartyContainer() {
this.externalPartyContainer = this.scene.add.container(0, 0);
this.externalPartyContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 12, this.scene.game.canvas.height / 12), Phaser.Geom.Rectangle.Contains);
this.externalPartyTitle = addTextObject(this.scene, 0, 4, "", TextStyle.SETTINGS_LABEL);
@ -31,23 +60,8 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
this.externalPartyContainer.add(this.externalPartyBg);
this.externalPartyContainer.add(this.externalPartyTitle);
this.infoContainer = this.scene.add.container(0, 0);
this.infoContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 12, this.scene.game.canvas.height / 12), Phaser.Geom.Rectangle.Contains);
const googleImage = this.scene.add.image(0, 0, "google");
googleImage.setOrigin(0, 0);
googleImage.setScale(0.07);
googleImage.setInteractive();
googleImage.setName("google-icon");
this.googleImage = googleImage;
const discordImage = this.scene.add.image(20, 0, "discord");
discordImage.setOrigin(0, 0);
discordImage.setScale(0.07);
discordImage.setInteractive();
discordImage.setName("discord-icon");
this.discordImage = discordImage;
this.googleImage = this.buildInteractableImage("google", "google-icon");
this.discordImage = this.buildInteractableImage("discord", "discord-icon");
this.externalPartyContainer.add(this.googleImage);
this.externalPartyContainer.add(this.discordImage);
@ -55,59 +69,52 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
this.externalPartyContainer.add(this.googleImage);
this.externalPartyContainer.add(this.discordImage);
this.externalPartyContainer.setVisible(false);
const usernameInfoImage = this.scene.add.image(20, 0, "settings_icon");
usernameInfoImage.setOrigin(0, 0);
usernameInfoImage.setScale(0.5);
usernameInfoImage.setInteractive();
usernameInfoImage.setName("username-info-icon");
this.usernameInfoImage = usernameInfoImage;
this.infoContainer.add(this.usernameInfoImage);
this.getUi().add(this.infoContainer);
this.infoContainer.setVisible(false);
}
getModalTitle(config?: ModalConfig): string {
override getModalTitle(_config?: ModalConfig): string {
return i18next.t("menu:login");
}
getFields(config?: ModalConfig): string[] {
override getFields(_config?: ModalConfig): string[] {
return [ i18next.t("menu:username"), i18next.t("menu:password") ];
}
getWidth(config?: ModalConfig): number {
override getWidth(_config?: ModalConfig): number {
return 160;
}
getMargin(config?: ModalConfig): [number, number, number, number] {
override getMargin(_config?: ModalConfig): [number, number, number, number] {
return [ 0, 0, 48, 0 ];
}
getButtonLabels(config?: ModalConfig): string[] {
override getButtonLabels(_config?: ModalConfig): string[] {
return [ i18next.t("menu:login"), i18next.t("menu:register")];
}
getReadableErrorMessage(error: string): string {
override getReadableErrorMessage(error: string): string {
const colonIndex = error?.indexOf(":");
if (colonIndex > 0) {
error = error.slice(0, colonIndex);
}
switch (error) {
case "invalid username":
case this.ERR_USERNAME:
return i18next.t("menu:invalidLoginUsername");
case "invalid password":
case this.ERR_PASSWORD:
return i18next.t("menu:invalidLoginPassword");
case "account doesn't exist":
case this.ERR_ACCOUNT_EXIST:
return i18next.t("menu:accountNonExistent");
case "password doesn't match":
case this.ERR_PASSWORD_MATCH:
return i18next.t("menu:unmatchingPassword");
case this.ERR_NO_SAVES:
return i18next.t("menu:noSaves");
case this.ERR_TOO_MANY_SAVES:
return i18next.t("menu:tooManySaves");
}
return super.getReadableErrorMessage(error);
}
show(args: any[]): boolean {
override show(args: any[]): boolean {
if (super.show(args)) {
const config = args[0] as ModalConfig;
@ -148,17 +155,16 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
return false;
}
clear() {
override clear() {
super.clear();
this.externalPartyContainer.setVisible(false);
this.infoContainer.setVisible(false);
this.setMouseCursorStyle("default"); //reset cursor
this.discordImage.off("pointerdown");
this.googleImage.off("pointerdown");
this.usernameInfoImage.off("pointerdown");
[this.discordImage, this.googleImage, this.usernameInfoImage].forEach((img) => img.off("pointerdown"));
}
processExternalProvider(config: ModalConfig) : void {
private processExternalProvider(config: ModalConfig) : void {
this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? "");
this.externalPartyTitle.setX(20+this.externalPartyTitle.text.length);
this.externalPartyTitle.setVisible(true);
@ -205,6 +211,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
label: dataKeys[i].replace(keyToFind, ""),
handler: () => {
this.scene.ui.revertMode();
this.infoContainer.disableInteractive();
return true;
}
});
@ -213,8 +220,13 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
options: options,
delay: 1000
});
this.infoContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width, this.scene.game.canvas.height), Phaser.Geom.Rectangle.Contains);
} else {
return onFail("You have too many save files to use this");
if (dataKeys.length > 2) {
return onFail(this.ERR_TOO_MANY_SAVES);
} else {
return onFail(this.ERR_NO_SAVES);
}
}
});
@ -236,4 +248,21 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
alpha: 1
});
}
private buildInteractableImage(texture: string, name: string, opts: BuildInteractableImageOpts = {}) {
const {
scale = 0.07,
x = 0,
y = 0,
origin = { x: 0, y: 0 }
} = opts;
const img = this.scene.add.image(x, y, texture);
img.setName(name);
img.setOrigin(origin.x, origin.y);
img.setScale(scale);
img.setInteractive();
this.addInteractionHoverEffect(img);
return img;
}
}

View File

@ -57,29 +57,35 @@ export abstract class ModalUiHandler extends UiHandler {
const buttonLabels = this.getButtonLabels();
const buttonTopMargin = this.getButtonTopMargin();
for (const label of buttonLabels) {
const buttonLabel = addTextObject(this.scene, 0, 8, label, TextStyle.TOOLTIP_CONTENT);
buttonLabel.setOrigin(0.5, 0.5);
const buttonBg = addWindow(this.scene, 0, 0, buttonLabel.getBounds().width + 8, 16, false, false, 0, 0, WindowVariant.THIN);
buttonBg.setOrigin(0.5, 0);
buttonBg.setInteractive(new Phaser.Geom.Rectangle(0, 0, buttonBg.width, buttonBg.height), Phaser.Geom.Rectangle.Contains);
const buttonContainer = this.scene.add.container(0, buttonTopMargin);
this.buttonBgs.push(buttonBg);
this.buttonContainers.push(buttonContainer);
buttonContainer.add(buttonBg);
buttonContainer.add(buttonLabel);
this.modalContainer.add(buttonContainer);
this.addButton(label);
}
this.modalContainer.setVisible(false);
}
private addButton(label: string) {
const buttonTopMargin = this.getButtonTopMargin();
const buttonLabel = addTextObject(this.scene, 0, 8, label, TextStyle.TOOLTIP_CONTENT);
buttonLabel.setOrigin(0.5, 0.5);
const buttonBg = addWindow(this.scene, 0, 0, buttonLabel.getBounds().width + 8, 16, false, false, 0, 0, WindowVariant.THIN);
buttonBg.setOrigin(0.5, 0);
buttonBg.setInteractive(new Phaser.Geom.Rectangle(0, 0, buttonBg.width, buttonBg.height), Phaser.Geom.Rectangle.Contains);
const buttonContainer = this.scene.add.container(0, buttonTopMargin);
this.buttonBgs.push(buttonBg);
this.buttonContainers.push(buttonContainer);
buttonContainer.add(buttonBg);
buttonContainer.add(buttonLabel);
this.addInteractionHoverEffect(buttonBg);
this.modalContainer.add(buttonContainer);
}
show(args: any[]): boolean {
if (args.length >= 1 && "buttonActions" in args[0]) {
super.show(args);
@ -135,4 +141,20 @@ export abstract class ModalUiHandler extends UiHandler {
this.buttonBgs.map(bg => bg.off("pointerdown"));
}
/**
* Adds a hover effect to a game object which changes the cursor to a `pointer` and tints it slighly
* @param gameObject the game object to add hover events/effects to
*/
protected addInteractionHoverEffect(gameObject: Phaser.GameObjects.Image | Phaser.GameObjects.NineSlice | Phaser.GameObjects.Sprite) {
gameObject.on("pointerover", () => {
this.setMouseCursorStyle("pointer");
gameObject.setTint(0xbbbbbb);
});
gameObject.on("pointerout", () => {
this.setMouseCursorStyle("default");
gameObject.clearTint();
});
}
}

View File

@ -52,6 +52,15 @@ export default abstract class UiHandler {
return changed;
}
/**
* Changes the style of the mouse cursor.
* @see {@link https://developer.mozilla.org/en-US/docs/Web/CSS/cursor}
* @param cursorStyle cursor style to apply
*/
protected setMouseCursorStyle(cursorStyle: "pointer" | "default") {
this.scene.input.manager.canvas.style.cursor = cursorStyle;
}
clear() {
this.active = false;
}