From 312b38fda5363d2df34dfd1b9fb211ffead72873 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 5 Jun 2026 22:18:31 +0100 Subject: [PATCH] Disable game buttons (clan tag + username) (#4170) ## Description: disables buttons, instead of emitting a warning ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n --- src/client/GameModeSelector.ts | 39 ++++++++++++++++++++++++++++------ src/client/Main.ts | 2 +- src/client/UsernameInput.ts | 37 +++++++++++++------------------- 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/client/GameModeSelector.ts b/src/client/GameModeSelector.ts index 660dd222a..b1dc74cb0 100644 --- a/src/client/GameModeSelector.ts +++ b/src/client/GameModeSelector.ts @@ -34,6 +34,7 @@ const CARD_BG = "bg-surface"; export class GameModeSelector extends LitElement { @state() private lobbies: PublicGames | null = null; @state() private mapAspectRatios: Map = new Map(); + @state() private inputValid: boolean = true; private serverTimeOffset: number = 0; private defaultLobbyTime: number = 0; @@ -45,28 +46,44 @@ export class GameModeSelector extends LitElement { return this; } - /** - * Validates username input and shows error message if invalid. - * Returns true if valid, false otherwise. - */ + // Silent backstop; the buttons are already disabled while input is invalid. private validateUsername(): boolean { const usernameInput = document.querySelector( "username-input", ) as UsernameInput | null; - return usernameInput ? usernameInput.validateOrShowError() : true; + return usernameInput ? usernameInput.canPlay() : true; } connectedCallback() { super.connectedCallback(); this.lobbySocket.start(); this.defaultLobbyTime = ClientEnv.gameCreationRate() / 1000; + window.addEventListener( + "username-validity-change", + this.handleValidityChange, + ); + // Pick up the current value in case username-input validated before us. + const usernameInput = document.querySelector( + "username-input", + ) as UsernameInput | null; + if (usernameInput) { + this.inputValid = usernameInput.canPlay(); + } } disconnectedCallback() { this.stop(); + window.removeEventListener( + "username-validity-change", + this.handleValidityChange, + ); super.disconnectedCallback(); } + private handleValidityChange = (e: Event) => { + this.inputValid = (e as CustomEvent).detail?.isValid ?? true; + }; + public stop() { this.lobbySocket.stop(); } @@ -259,7 +276,11 @@ export class GameModeSelector extends LitElement { return html` ${title} @@ -305,7 +326,11 @@ export class GameModeSelector extends LitElement { return html` this.validateAndJoin(lobby)} - class="group relative w-full h-44 sm:h-full text-white uppercase rounded-2xl transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] bg-surface hover:shadow-[var(--shadow-lobby-card-hover)]" + ?disabled=${!this.inputValid} + class="group relative w-full h-44 sm:h-full text-white uppercase rounded-2xl transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] bg-surface hover:shadow-[var(--shadow-lobby-card-hover)] ${!this + .inputValid + ? "opacity-50 cursor-not-allowed pointer-events-none" + : ""}" > ) { const lobby = event.detail; this.mostRecentJoinEvent = event.timeStamp; - if (this.usernameInput && !this.usernameInput.validateOrShowError()) { + if (this.usernameInput && !this.usernameInput.canPlay()) { return; } diff --git a/src/client/UsernameInput.ts b/src/client/UsernameInput.ts index 50c6dba74..52d2a44ea 100644 --- a/src/client/UsernameInput.ts +++ b/src/client/UsernameInput.ts @@ -28,9 +28,8 @@ export class UsernameInput extends LitElement { @state() private clanTag: string = ""; @property({ type: String }) validationError: string = ""; - // Ownership-check feedback (i18n key) shown inline beneath the tag input. This - // is advisory only — it does not gate play; the tag is stripped on submit and - // the server re-checks authoritatively. + // Ownership-check feedback (i18n key) shown inline beneath the tag input. Only + // "not a member" gates the buttons (see emitValidity); the rest is advisory. @state() private clanTagOwnershipError: string = ""; @state() private clanCheckPending: boolean = false; private _isValid: boolean = true; @@ -71,6 +70,7 @@ export class UsernameInput extends LitElement { const gen = ++this.clanCheckGen; const tag = this.clanTag; this.clanTagOwnershipError = ""; + this.emitValidity(); if (tag.length === 0 || !validateClanTag(tag).isValid) { this.clanCheckPending = false; this.clanCheck = Promise.resolve(null); @@ -81,6 +81,7 @@ export class UsernameInput extends LitElement { if (gen === this.clanCheckGen) { this.clanTagOwnershipError = res.error ?? ""; this.clanCheckPending = false; + this.emitValidity(); } return res.tag; }); @@ -239,6 +240,7 @@ export class UsernameInput extends LitElement { if (!clanTagResult.isValid) { this._isValid = false; this.validationError = clanTagResult.error ?? ""; + this.emitValidity(); return; } @@ -251,32 +253,23 @@ export class UsernameInput extends LitElement { } else { this.validationError = result.error ?? ""; } + this.emitValidity(); } - public isValid(): boolean { - return this._isValid; - } - - public showValidationFeedback(): void { - const message = - this.validationError || translateText("username.invalid_chars"); + // Broadcast play-eligibility so action buttons can disable themselves. + private emitValidity() { window.dispatchEvent( - new CustomEvent("show-message", { - detail: { - message, - color: "red", - duration: 2500, - }, + new CustomEvent("username-validity-change", { + detail: { isValid: this.canPlay() }, }), ); } - public validateOrShowError(): boolean { - if (this.isValid()) { - return true; - } - this.showValidationFeedback(); - return false; + // Play-eligibility: syntax-valid and not blocked by clan membership. + public canPlay(): boolean { + return ( + this._isValid && this.clanTagOwnershipError !== "username.tag_not_member" + ); } }