Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 36 additions & 25 deletions src/client/graphics/layers/MainRadialMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
);
});
});
}

Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
});
Comment on lines +179 to +184
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close stale radial when viewer becomes spectator.

At Line 180, early return keeps an already-open radial menu visible after spectator transition. Close it before returning so read-only mode is enforced consistently.

Proposed fix
   async tick() {
     if (!this.radialMenu.isMenuVisible() || this.clickedTile === null) return;
     const myPlayer = this.game.myPlayer();
-    if (myPlayer === null) return;
+    if (myPlayer === null || this.game.isSpectator()) {
+      this.closeMenu();
+      return;
+    }
     const tile = this.clickedTile;
     myPlayer.actions(tile).then((actions) => {
       this.updatePlayerActions(myPlayer, actions, tile);
     });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const myPlayer = this.game.myPlayer();
if (myPlayer === null) return;
const tile = this.clickedTile;
myPlayer.actions(tile).then((actions) => {
this.updatePlayerActions(myPlayer, actions, tile);
});
const myPlayer = this.game.myPlayer();
if (myPlayer === null || this.game.isSpectator()) {
this.closeMenu();
return;
}
const tile = this.clickedTile;
myPlayer.actions(tile).then((actions) => {
this.updatePlayerActions(myPlayer, actions, tile);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/graphics/layers/MainRadialMenu.ts` around lines 179 - 184, The
early return when myPlayer is null leaves the radial open; before returning from
MainRadialMenu's logic, explicitly close the menu (e.g. call this.close() or
this.hide() on the radial instance) so spectator/read-only mode is enforced;
update the code around the myPlayer null check (the block that uses
this.clickedTile and calls this.updatePlayerActions) to first close the radial
then return.

}

renderLayer(context: CanvasRenderingContext2D) {
Expand Down
39 changes: 24 additions & 15 deletions src/client/graphics/layers/PlayerPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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`
<style>
Expand Down Expand Up @@ -946,9 +949,11 @@ export class PlayerPanel extends LitElement implements Layer {
class="p-6 flex flex-col gap-2 font-sans antialiased text-[14.5px] leading-relaxed"
>
<!-- Identity (flag, name, type, traitor, relation) -->
<div class="mb-1">${this.renderIdentityRow(other, my)}</div>
<div class="mb-1">
${this.renderIdentityRow(other, viewer)}
</div>

${this.sendTarget
${this.sendTarget && !isSpectator
? html`
<send-resource-modal
.open=${this.sendMode !== "none"}
Expand All @@ -957,7 +962,7 @@ export class PlayerPanel extends LitElement implements Layer {
? myTroopsNum
: myGoldNum}
.uiState=${this.uiState}
.myPlayer=${my}
.myPlayer=${viewer}
.target=${this.sendTarget}
.gameView=${this.g}
.eventBus=${this.eventBus}
Expand All @@ -969,11 +974,11 @@ export class PlayerPanel extends LitElement implements Layer {
></send-resource-modal>
`
: ""}
${this.moderationTarget
${this.moderationTarget && !isSpectator
? html`
<player-moderation-modal
.open=${true}
.myPlayer=${my}
.myPlayer=${viewer}
.target=${this.moderationTarget}
.eventBus=${this.eventBus}
.isAdmin=${this.isAdminRole}
Expand All @@ -992,12 +997,14 @@ export class PlayerPanel extends LitElement implements Layer {
${this.renderResources(other)}

<!-- Rocket direction toggle -->
${other === my ? this.renderRocketDirectionToggle() : ""}
${other === viewer && !isSpectator
? this.renderRocketDirectionToggle()
: ""}

<ui-divider></ui-divider>

<!-- Stats: betrayals / trading -->
${this.renderStats(other, my)}
${this.renderStats(other, viewer)}

<ui-divider></ui-divider>

Expand All @@ -1006,11 +1013,13 @@ export class PlayerPanel extends LitElement implements Layer {

<!-- Alliance time remaining -->
${this.renderAllianceExpiry()}

<ui-divider></ui-divider>

<!-- Actions -->
${this.renderActions(my, other)}
${isSpectator
? ""
: html`
<ui-divider></ui-divider>
<!-- Actions -->
${this.renderActions(viewer, other)}
`}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/core/game/GameView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,9 @@ export class GameView implements GameMap {
config(): Config {
return this._config;
}
isSpectator(): boolean {
return !this.myPlayer()?.isAlive() || this._config.isReplay();
}
units(...types: UnitType[]): UnitView[] {
if (types.length === 0) {
return Array.from(this._units.values()).filter((u) => u.isActive());
Expand Down
Loading