[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", "renamePokemon": "Rename Pokémon",
"rename": "Rename", "rename": "Rename",
"nickname": "Nickname", "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; return this.list;
} }
disableInteractive = vi.fn();
} }

View File

@ -8,7 +8,21 @@ import { addTextObject, TextStyle } from "./text";
import { addWindow } from "./ui-theme"; import { addWindow } from "./ui-theme";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; 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 { 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 googleImage: Phaser.GameObjects.Image;
private discordImage: Phaser.GameObjects.Image; private discordImage: Phaser.GameObjects.Image;
private usernameInfoImage: Phaser.GameObjects.Image; private usernameInfoImage: Phaser.GameObjects.Image;
@ -21,8 +35,23 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
} }
setup(): void { setup(): void {
super.setup(); 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 = 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.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); 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.externalPartyBg);
this.externalPartyContainer.add(this.externalPartyTitle); this.externalPartyContainer.add(this.externalPartyTitle);
this.infoContainer = this.scene.add.container(0, 0); this.googleImage = this.buildInteractableImage("google", "google-icon");
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); this.discordImage = this.buildInteractableImage("discord", "discord-icon");
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.externalPartyContainer.add(this.googleImage); this.externalPartyContainer.add(this.googleImage);
this.externalPartyContainer.add(this.discordImage); this.externalPartyContainer.add(this.discordImage);
@ -55,59 +69,52 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
this.externalPartyContainer.add(this.googleImage); this.externalPartyContainer.add(this.googleImage);
this.externalPartyContainer.add(this.discordImage); this.externalPartyContainer.add(this.discordImage);
this.externalPartyContainer.setVisible(false); 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"); return i18next.t("menu:login");
} }
getFields(config?: ModalConfig): string[] { override getFields(_config?: ModalConfig): string[] {
return [ i18next.t("menu:username"), i18next.t("menu:password") ]; return [ i18next.t("menu:username"), i18next.t("menu:password") ];
} }
getWidth(config?: ModalConfig): number { override getWidth(_config?: ModalConfig): number {
return 160; return 160;
} }
getMargin(config?: ModalConfig): [number, number, number, number] { override getMargin(_config?: ModalConfig): [number, number, number, number] {
return [ 0, 0, 48, 0 ]; return [ 0, 0, 48, 0 ];
} }
getButtonLabels(config?: ModalConfig): string[] { override getButtonLabels(_config?: ModalConfig): string[] {
return [ i18next.t("menu:login"), i18next.t("menu:register")]; return [ i18next.t("menu:login"), i18next.t("menu:register")];
} }
getReadableErrorMessage(error: string): string { override getReadableErrorMessage(error: string): string {
const colonIndex = error?.indexOf(":"); const colonIndex = error?.indexOf(":");
if (colonIndex > 0) { if (colonIndex > 0) {
error = error.slice(0, colonIndex); error = error.slice(0, colonIndex);
} }
switch (error) { switch (error) {
case "invalid username": case this.ERR_USERNAME:
return i18next.t("menu:invalidLoginUsername"); return i18next.t("menu:invalidLoginUsername");
case "invalid password": case this.ERR_PASSWORD:
return i18next.t("menu:invalidLoginPassword"); return i18next.t("menu:invalidLoginPassword");
case "account doesn't exist": case this.ERR_ACCOUNT_EXIST:
return i18next.t("menu:accountNonExistent"); return i18next.t("menu:accountNonExistent");
case "password doesn't match": case this.ERR_PASSWORD_MATCH:
return i18next.t("menu:unmatchingPassword"); 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); return super.getReadableErrorMessage(error);
} }
show(args: any[]): boolean { override show(args: any[]): boolean {
if (super.show(args)) { if (super.show(args)) {
const config = args[0] as ModalConfig; const config = args[0] as ModalConfig;
@ -148,17 +155,16 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
return false; return false;
} }
clear() { override clear() {
super.clear(); super.clear();
this.externalPartyContainer.setVisible(false); this.externalPartyContainer.setVisible(false);
this.infoContainer.setVisible(false); this.infoContainer.setVisible(false);
this.setMouseCursorStyle("default"); //reset cursor
this.discordImage.off("pointerdown"); [this.discordImage, this.googleImage, this.usernameInfoImage].forEach((img) => img.off("pointerdown"));
this.googleImage.off("pointerdown");
this.usernameInfoImage.off("pointerdown");
} }
processExternalProvider(config: ModalConfig) : void { private processExternalProvider(config: ModalConfig) : void {
this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? ""); this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? "");
this.externalPartyTitle.setX(20+this.externalPartyTitle.text.length); this.externalPartyTitle.setX(20+this.externalPartyTitle.text.length);
this.externalPartyTitle.setVisible(true); this.externalPartyTitle.setVisible(true);
@ -205,6 +211,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
label: dataKeys[i].replace(keyToFind, ""), label: dataKeys[i].replace(keyToFind, ""),
handler: () => { handler: () => {
this.scene.ui.revertMode(); this.scene.ui.revertMode();
this.infoContainer.disableInteractive();
return true; return true;
} }
}); });
@ -213,8 +220,13 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
options: options, options: options,
delay: 1000 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 { } 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 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 buttonLabels = this.getButtonLabels();
const buttonTopMargin = this.getButtonTopMargin();
for (const label of buttonLabels) { for (const label of buttonLabels) {
const buttonLabel = addTextObject(this.scene, 0, 8, label, TextStyle.TOOLTIP_CONTENT); this.addButton(label);
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.modalContainer.setVisible(false); 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 { show(args: any[]): boolean {
if (args.length >= 1 && "buttonActions" in args[0]) { if (args.length >= 1 && "buttonActions" in args[0]) {
super.show(args); super.show(args);
@ -135,4 +141,20 @@ export abstract class ModalUiHandler extends UiHandler {
this.buttonBgs.map(bg => bg.off("pointerdown")); 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; 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() { clear() {
this.active = false; this.active = false;
} }