From 0eb23c0c8c62a6d96fd64cc3ecd719ab1584541b Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:58:10 +0000 Subject: [PATCH 01/11] clientId replay bugfix (was picking first clientID in the array) (#3369) ## Description: clientId replay bugfix (was picking first clientID in the array) https://discord.com/channels/1359946986937258015/1479543573404844042 ## 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 - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n --- src/client/ClientGameRunner.ts | 10 +++++++--- src/client/LocalServer.ts | 5 +++-- src/core/GameRunner.ts | 2 +- src/core/Schemas.ts | 5 +++-- src/core/execution/ExecutionManager.ts | 2 +- src/core/game/GameView.ts | 8 +++++--- src/core/worker/WorkerClient.ts | 2 +- src/core/worker/WorkerMessages.ts | 2 +- 8 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 5cc88c3eb..09271afda 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -194,7 +194,7 @@ export function joinLobby( async function createClientGame( lobbyConfig: LobbyConfig, - clientID: ClientID, + clientID: ClientID | undefined, eventBus: EventBus, transport: Transport, userSettings: UserSettings, @@ -267,7 +267,7 @@ export class ClientGameRunner { constructor( private lobby: LobbyConfig, - private clientID: ClientID, + private clientID: ClientID | undefined, private eventBus: EventBus, private renderer: GameRenderer, private input: InputHandler, @@ -294,7 +294,7 @@ export class ClientGameRunner { } private async saveGame(update: WinUpdate) { - if (this.myPlayer === null) { + if (!this.clientID) { return; } const players: PlayerRecord[] = [ @@ -544,6 +544,7 @@ export class ClientGameRunner { return; } if (this.myPlayer === null) { + if (!this.clientID) return; const myPlayer = this.gameView.playerByClientID(this.clientID); if (myPlayer === null) return; this.myPlayer = myPlayer; @@ -578,6 +579,7 @@ export class ClientGameRunner { const tile = this.gameView.ref(cell.x, cell.y); if (this.myPlayer === null) { + if (!this.clientID) return; const myPlayer = this.gameView.playerByClientID(this.clientID); if (myPlayer === null) return; this.myPlayer = myPlayer; @@ -639,6 +641,7 @@ export class ClientGameRunner { } if (this.myPlayer === null) { + if (!this.clientID) return; const myPlayer = this.gameView.playerByClientID(this.clientID); if (myPlayer === null) return; this.myPlayer = myPlayer; @@ -664,6 +667,7 @@ export class ClientGameRunner { } if (this.myPlayer === null) { + if (!this.clientID) return; const myPlayer = this.gameView.playerByClientID(this.clientID); if (myPlayer === null) return; this.myPlayer = myPlayer; diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index 90712b868..58c43306c 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -113,7 +113,8 @@ export class LocalServer { gameStartInfo: this.lobbyConfig.gameStartInfo, turns: [], lobbyCreatedAt: this.lobbyConfig.gameStartInfo.lobbyCreatedAt, - myClientID: this.clientID, + // Don't send myClientID for replays — viewer has no player identity. + myClientID: this.lobbyConfig.gameRecord ? undefined : this.clientID, } satisfies ServerStartGameMessage); } @@ -127,7 +128,7 @@ export class LocalServer { gameStartInfo: this.lobbyConfig.gameStartInfo!, turns: this.turns, lobbyCreatedAt: this.lobbyConfig.gameStartInfo!.lobbyCreatedAt, - myClientID: this.clientID, + myClientID: this.lobbyConfig.gameRecord ? undefined : this.clientID, } satisfies ServerStartGameMessage); } if (clientMsg.type === "intent") { diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 9019a78e5..427a32781 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -33,7 +33,7 @@ import { simpleHash } from "./Util"; export async function createGameRunner( gameStart: GameStartInfo, - clientID: ClientID, + clientID: ClientID | undefined, mapLoader: GameMapLoader, callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, ): Promise { diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 067e83069..a42738774 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -552,8 +552,9 @@ export const ServerStartGameMessageSchema = z.object({ turns: TurnSchema.array(), gameStartInfo: GameStartInfoSchema, lobbyCreatedAt: z.number(), - // The clientID assigned to this connection by the server - myClientID: ID, + // The clientID assigned to this connection by the server. + // Absent for replays where the viewer has no player identity. + myClientID: ID.optional(), }); export const ServerDesyncSchema = z.object({ diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index d50aaadb7..9b61629d9 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -36,7 +36,7 @@ export class Executor { constructor( private mg: Game, private gameID: GameID, - private clientID: ClientID, + private clientID: ClientID | undefined, ) { // Add one to avoid id collisions with bots. this.random = new PseudoRandom(simpleHash(gameID) + 1); diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index bdd9f2092..d3e3ad87e 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -657,7 +657,7 @@ export class GameView implements GameMap { public worker: WorkerClient, private _config: Config, private _mapData: TerrainMapData, - private _myClientID: ClientID, + private _myClientID: ClientID | undefined, private _myUsername: string, private _gameID: GameID, private humans: Player[], @@ -785,7 +785,9 @@ export class GameView implements GameMap { } }); - this._myPlayer ??= this.playerByClientID(this._myClientID); + if (this._myClientID) { + this._myPlayer ??= this.playerByClientID(this._myClientID); + } for (const unit of this._units.values()) { unit._wasUpdated = false; @@ -1103,7 +1105,7 @@ export class GameView implements GameMap { ); } - myClientID(): ClientID { + myClientID(): ClientID | undefined { return this._myClientID; } diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index 7655f9050..372e4af96 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -23,7 +23,7 @@ export class WorkerClient { constructor( private gameStartInfo: GameStartInfo, - private clientID: ClientID, + private clientID: ClientID | undefined, ) { this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url), { type: "module", diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts index b8b04740d..7d75f1882 100644 --- a/src/core/worker/WorkerMessages.ts +++ b/src/core/worker/WorkerMessages.ts @@ -39,7 +39,7 @@ interface BaseWorkerMessage { export interface InitMessage extends BaseWorkerMessage { type: "init"; gameStartInfo: GameStartInfo; - clientID: ClientID; + clientID: ClientID | undefined; } export interface TurnMessage extends BaseWorkerMessage { From 815f1de67b71d83f8c15721d6d9b3074f5fdf450 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 6 Mar 2026 18:32:01 -0800 Subject: [PATCH 02/11] Update control panel UI (#3357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relates to #2260 ## Description: Inspired by https://github.com/openfrontio/OpenFrontIO/pull/3359 This PR centers the control panel and combines it with the units display. The reasoning is that the control panel contains the most critical info so it should be in the center of the screen. Combining it with the units display reduces the number of UI components on screen. Also made the attack ratio bar persistent on mobile Screenshot 2026-03-06 at 2 06 34 PM Screenshot 2026-03-06 at 2 06 55 PM Screenshot 2026-03-06 at 4 11 20 PM Screenshot 2026-03-06 at 4 11 32 PM ## 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 - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan Co-authored-by: hkio120 <111693579+hkio120@users.noreply.github.com> --- index.html | 29 +- src/client/graphics/layers/AttacksDisplay.ts | 12 +- src/client/graphics/layers/ControlPanel.ts | 334 +++++++++---------- src/client/graphics/layers/EventsDisplay.ts | 10 +- src/client/graphics/layers/UnitDisplay.ts | 168 +++++----- 5 files changed, 274 insertions(+), 279 deletions(-) diff --git a/index.html b/index.html index 90f2e229b..c5d47ffa5 100644 --- a/index.html +++ b/index.html @@ -264,23 +264,37 @@
+
+
- - + +
+ + +
+ +
@@ -290,7 +304,6 @@ -
diff --git a/src/client/graphics/layers/AttacksDisplay.ts b/src/client/graphics/layers/AttacksDisplay.ts index f77411e55..88e0f4c15 100644 --- a/src/client/graphics/layers/AttacksDisplay.ts +++ b/src/client/graphics/layers/AttacksDisplay.ts @@ -221,7 +221,7 @@ export class AttacksDisplay extends LitElement implements Layer { return this.incomingAttacks.map( (attack) => html`
${this.renderButton({ content: html` html`
${this.renderButton({ content: html` html`
${this.renderButton({ content: html` html`
${this.renderButton({ content: html`${this.renderBoatIcon(boat)} @@ -403,7 +403,7 @@ export class AttacksDisplay extends LitElement implements Layer { return this.incomingBoats.map( (boat) => html`
${this.renderButton({ content: html`${this.renderBoatIcon(boat)} @@ -441,7 +441,7 @@ export class AttacksDisplay extends LitElement implements Layer { return html`
${this.renderOutgoingAttacks()} ${this.renderOutgoingLandAttacks()} ${this.renderBoats()} ${this.renderIncomingAttacks()} diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index 8aeb8d415..5210a1995 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -40,9 +40,6 @@ export class ControlPanel extends LitElement implements Layer { @state() private _attackingTroops: number = 0; - @state() - private _touchDragging = false; - private _troopRateIsIncreasing: boolean = true; private _lastTroopIncreaseRate: number; @@ -127,73 +124,13 @@ export class ControlPanel extends LitElement implements Layer { this.requestUpdate(); } - private _outsideTouchHandler: ((ev: Event) => void) | null = null; - - private handleAttackTouchStart(e: TouchEvent) { - e.preventDefault(); - e.stopPropagation(); - - if (this._touchDragging) { - this.closeAttackBar(); - return; - } - - this._touchDragging = true; - - setTimeout(() => { - this._outsideTouchHandler = () => { - this.closeAttackBar(); - }; - document.addEventListener("touchstart", this._outsideTouchHandler); - }, 0); - } - - private closeAttackBar() { - this._touchDragging = false; - if (this._outsideTouchHandler) { - document.removeEventListener("touchstart", this._outsideTouchHandler); - this._outsideTouchHandler = null; - } - } - - private handleBarTouch(e: TouchEvent) { - e.preventDefault(); - e.stopPropagation(); - - this.setRatioFromTouch(e.touches[0]); - - const onMove = (ev: TouchEvent) => { - ev.preventDefault(); - this.setRatioFromTouch(ev.touches[0]); - }; - - const onEnd = () => { - document.removeEventListener("touchmove", onMove); - document.removeEventListener("touchend", onEnd); - }; - - document.addEventListener("touchmove", onMove, { passive: false }); - document.addEventListener("touchend", onEnd); - } - - private setRatioFromTouch(touch: Touch) { - const barEl = this.querySelector(".attack-drag-bar"); - if (!barEl) return; - - const rect = barEl.getBoundingClientRect(); - const ratio = (rect.bottom - touch.clientY) / (rect.bottom - rect.top); - this.attackRatio = - Math.round(Math.max(1, Math.min(100, ratio * 100))) / 100; - this.onAttackRatioChange(this.attackRatio); - } - private handleRatioSliderInput(e: Event) { const value = Number((e.target as HTMLInputElement).value); this.attackRatio = value / 100; this.onAttackRatioChange(this.attackRatio); } - private renderTroopBar() { + private calculateTroopBar(): { greenPercent: number; orangePercent: number } { const base = Math.max(this._maxTroops, 1); const greenPercentRaw = (this._troops / base) * 100; const orangePercentRaw = (this._attackingTroops / base) * 100; @@ -204,9 +141,14 @@ export class ControlPanel extends LitElement implements Layer { Math.min(100 - greenPercent, orangePercentRaw), ); + return { greenPercent, orangePercent }; + } + + private renderMobileTroopBar() { + const { greenPercent, orangePercent } = this.calculateTroopBar(); return html`
${greenPercent > 0 @@ -223,7 +165,7 @@ export class ControlPanel extends LitElement implements Layer { : ""}