diff --git a/src/client/graphics/layers/MainRadialMenu.ts b/src/client/graphics/layers/MainRadialMenu.ts index b6adba9291..c58b4566ab 100644 --- a/src/client/graphics/layers/MainRadialMenu.ts +++ b/src/client/graphics/layers/MainRadialMenu.ts @@ -25,6 +25,14 @@ const swordIcon = assetUrl("images/SwordIconWhite.svg"); import { ContextMenuEvent } from "../../InputHandler"; +function emptyPlayerActions(): PlayerActions { + return { + canAttack: false, + buildableUnits: [], + canSendEmojiAllPlayers: false, + }; +} + @customElement("main-radial-menu") export class MainRadialMenu extends LitElement implements Layer { private radialMenu: RadialMenu; @@ -87,22 +95,29 @@ export class MainRadialMenu extends LitElement implements Layer { if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) { return; } - if (this.game.myPlayer() === null) { + const clickedTile = this.game.ref(worldCoords.x, worldCoords.y); + this.clickedTile = clickedTile; + + // Spectators (replay, dead, pre-spawn): skip the action radial and open + // the read-only PlayerPanel directly when right-clicking on a player. + if (this.game.isSpectator()) { + if (this.game.owner(clickedTile).isPlayer()) { + this.playerPanel.show(emptyPlayerActions(), clickedTile); + } return; } - this.clickedTile = this.game.ref(worldCoords.x, worldCoords.y); - this.game - .myPlayer()! - .actions(this.clickedTile) - .then((actions) => { - this.updatePlayerActions( - this.game.myPlayer()!, - actions, - this.clickedTile!, - event.x, - event.y, - ); - }); + + const myPlayer = this.game.myPlayer(); + if (myPlayer === null) return; + myPlayer.actions(clickedTile).then((actions) => { + this.updatePlayerActions( + myPlayer, + actions, + clickedTile, + event.x, + event.y, + ); + }); }); } @@ -118,7 +133,7 @@ export class MainRadialMenu extends LitElement implements Layer { const tileOwner = this.game.owner(tile); const recipient = tileOwner.isPlayer() ? (tileOwner as PlayerView) : null; - if (myPlayer && recipient) { + if (recipient) { this.chatIntegration.setupChatModal(myPlayer, recipient); } @@ -161,16 +176,12 @@ export class MainRadialMenu extends LitElement implements Layer { async tick() { if (!this.radialMenu.isMenuVisible() || this.clickedTile === null) return; - this.game - .myPlayer()! - .actions(this.clickedTile) - .then((actions) => { - this.updatePlayerActions( - this.game.myPlayer()!, - actions, - this.clickedTile!, - ); - }); + const myPlayer = this.game.myPlayer(); + if (myPlayer === null) return; + const tile = this.clickedTile; + myPlayer.actions(tile).then((actions) => { + this.updatePlayerActions(myPlayer, actions, tile); + }); } renderLayer(context: CanvasRenderingContext2D) { diff --git a/src/client/graphics/layers/PlayerPanel.ts b/src/client/graphics/layers/PlayerPanel.ts index 6a60675d20..6dae27295f 100644 --- a/src/client/graphics/layers/PlayerPanel.ts +++ b/src/client/graphics/layers/PlayerPanel.ts @@ -867,7 +867,8 @@ export class PlayerPanel extends LitElement implements Layer { if (!this.isVisible) return html``; const my = this.g.myPlayer(); - if (!my) return html``; + const isSpectator = this.g.isSpectator(); + if (!my && !isSpectator) return html``; if (!this.tile) return html``; const owner = this.g.owner(this.tile); @@ -877,8 +878,10 @@ export class PlayerPanel extends LitElement implements Layer { return html``; } const other = owner as PlayerView; - const myGoldNum = my.gold(); - const myTroopsNum = Number(my.troops()); + // Spectators (replay viewers, dead, or pre-spawn) have no live player; use other as a read-only stand-in + const viewer = my ?? other; + const myGoldNum = viewer.gold(); + const myTroopsNum = Number(viewer.troops()); return html`