Skip to content

feat: make PlayerInfoPanel available in replays#3855

Open
mike-s-zaugg wants to merge 2 commits intoopenfrontio:mainfrom
mike-s-zaugg:feature/playerpanel-in-replay
Open

feat: make PlayerInfoPanel available in replays#3855
mike-s-zaugg wants to merge 2 commits intoopenfrontio:mainfrom
mike-s-zaugg:feature/playerpanel-in-replay

Conversation

@mike-s-zaugg
Copy link
Copy Markdown
Contributor

@mike-s-zaugg mike-s-zaugg commented May 6, 2026

Description:

Makes the PlayerInfoPanel accessible during replay playback, as suggested by @FloPinguin.

Previously the panel returned empty immediately when myPlayer() was null (which is always the case in replays since the viewer is not an active participant). The panel now renders in a read-only mode showing all
informational content: player name, flag, resources, alliances, betrayal count, and trading status.

The right-click radial menu was also gated on myPlayer !== null, so it never opened in replays. In replay mode the radial now opens with only the info wedge enabled when right-clicking on a player tile, which is the
entry point to the panel.

Action buttons that require sending intents to the server are hidden in replay mode since they are not applicable:

  • Chat, Emoji, Target
  • Donate Troops / Donate Gold
  • Alliance Request / Break Alliance
  • Embargo / Stop-Trade buttons
  • Moderation (Kick)
  • Rocket direction toggle

Read-only content (resources, alliances, stats) remains fully visible, which was the main motivation for this feature: being able to review alliances during a replay.

Screenshots

Screenshot_17

Radial menu only shows info option.

Screenshot_16

Displays all read-only content.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Tested locally with npm run dev:prod against a real prod replay (/w1/game/tvBd246s?live).

Please put your Discord username so you can be contacted if a bug or regression is found:

sxndrexe

Show player info (resources, alliances, stats) when clicking on a player
during replay playback. Action buttons are hidden as they are not applicable
in replay mode.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 6, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Review Change Stack

Walkthrough

Make UI safe for replay mode: allow nullable myPlayer in radial menus, show info-only radial when no local player, and render PlayerPanel read‑only using a viewer variable while hiding interactive controls in replay.

Changes

Replay Mode Support

Layer / File(s) Summary
Public param contract
src/client/graphics/layers/RadialMenuElements.ts
MenuElementParams.myPlayer changed to `PlayerView
Radial element guards & actions
src/client/graphics/layers/RadialMenuElements.ts
Element predicates now return false when myPlayer is null; cooldown uses optional chaining; action invocations assert myPlayer! where non-null is required; root submenu returns only info menu when myPlayer is null.
Context menu event & tick handling
src/client/graphics/layers/MainRadialMenu.ts
Added emptyPlayerActions() helper. ContextMenuEvent now shows info-only radial when myPlayer() is null, calls updatePlayerActions(null, emptyPlayerActions()), and tick() returns early in replay mode; friendly-target UI gated on real player.
PlayerPanel viewer rendering
src/client/graphics/layers/PlayerPanel.ts
Detect isReplay, compute viewer = my ?? other for read-only render. Use viewer for identity, stats, and modal bindings. Hide rocket toggle and actions UI in replay; actions call uses viewer when rendering read-only state.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

The viewer sits where players stood,
Quiet panels show what once was good,
Menus hush, actions take a bow,
Replay remembers then and now,
Silent hands, the game tells how.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main change: making PlayerInfoPanel accessible during replay mode when myPlayer is null.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the motivation, behavior in replay mode, hidden action buttons, and read-only content that remains visible.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/graphics/layers/PlayerPanel.ts (1)

954-990: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Block intent-capable modals in replay mode as well

Line 954 and Line 975 still allow modal rendering in replay when state is already set. Those modals can emit intent events, so replay is not fully read-only unless these branches are also gated by replay mode.

Suggested patch
+    const canInteract = !isReplay;
...
-                    ${this.sendTarget
+                    ${canInteract && this.sendTarget
                       ? html`
                           <send-resource-modal
...
-                    ${this.moderationTarget
+                    ${canInteract && this.moderationTarget
                       ? html`
                           <player-moderation-modal
🤖 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/PlayerPanel.ts` around lines 954 - 990,
PlayerPanel still renders intent-capable modals when sendTarget or
moderationTarget are set even in replay mode; update the template branches that
render <send-resource-modal> and <player-moderation-modal> to also check the
replay flag and skip rendering when in replay. Locate the modal conditional
expressions referencing this.sendTarget (and this.sendMode) and
this.moderationTarget inside PlayerPanel and add a guard such as &&
!this.isReplay (or the project's replay flag, e.g., this.g.isReplay) so the
modals are not instantiated in replay/read-only mode.
🤖 Prompt for all review comments with 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.

Outside diff comments:
In `@src/client/graphics/layers/PlayerPanel.ts`:
- Around line 954-990: PlayerPanel still renders intent-capable modals when
sendTarget or moderationTarget are set even in replay mode; update the template
branches that render <send-resource-modal> and <player-moderation-modal> to also
check the replay flag and skip rendering when in replay. Locate the modal
conditional expressions referencing this.sendTarget (and this.sendMode) and
this.moderationTarget inside PlayerPanel and add a guard such as &&
!this.isReplay (or the project's replay flag, e.g., this.g.isReplay) so the
modals are not instantiated in replay/read-only mode.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 38b69f60-1712-45f5-a37b-168a4f44109a

📥 Commits

Reviewing files that changed from the base of the PR and between b8a544a and 24974e6.

📒 Files selected for processing (1)
  • src/client/graphics/layers/PlayerPanel.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 6, 2026
@FloPinguin
Copy link
Copy Markdown
Contributor

Please test it locally by running npm run dev:prod

You can watch all prod replays that way

And a screenshot would be nice then

@FloPinguin FloPinguin modified the milestone: v32 May 6, 2026
@FloPinguin
Copy link
Copy Markdown
Contributor

And prettier is failing, run npm run format

The previous commit made PlayerPanel render when myPlayer is null, but
the panel is only opened from the right-click radial menu, which itself
short-circuited on myPlayer === null. So the feature was unreachable in
replays.

- MainRadialMenu: in replay mode, skip the server actions() lookup and
  open the radial with empty PlayerActions on player tiles only
- RadialMenuElements: widen MenuElementParams.myPlayer to PlayerView | null;
  root submenu returns [info] only when myPlayer is null; guard
  deleteUnit cooldown/disabled against null

Also applies prettier to PlayerPanel.ts (CI was failing on it).
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/client/graphics/layers/MainRadialMenu.ts`:
- Around line 118-126: The issue is that this.clickedTile is accessed inside
async callbacks in the methods around lines 118 and 188, leading to stale or
incorrect tile usage if it changes before the promise resolves. Fix this by
capturing the current clickedTile value into a local constant before calling
myPlayer.actions or other async functions, then use that local constant inside
the callback to update the player actions consistently for the intended tile.

In `@src/client/graphics/layers/PlayerPanel.ts`:
- Around line 1015-1022: renderAllianceExpiry() is still rendered when isReplay
is true, causing stale expiry text to persist because replay doesn't update the
live-only value; stop rendering or clear the underlying state when in replay:
update the template where renderAllianceExpiry() is used (near the current
conditional around renderActions) so it is not called when isReplay is true, and
additionally ensure show() and/or tick() detect entering replay mode and clear
the alliance-expiry state used by renderAllianceExpiry() (or set it to an
empty/undefined value) to be extra safe; reference renderAllianceExpiry(),
renderActions(), show(), and tick() when making the changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 323978f8-bb58-48ca-92b2-6972e512069b

📥 Commits

Reviewing files that changed from the base of the PR and between 24974e6 and 3fdcea9.

📒 Files selected for processing (3)
  • src/client/graphics/layers/MainRadialMenu.ts
  • src/client/graphics/layers/PlayerPanel.ts
  • src/client/graphics/layers/RadialMenuElements.ts

Comment on lines +118 to +126
myPlayer.actions(this.clickedTile).then((actions) => {
this.updatePlayerActions(
myPlayer,
actions,
this.clickedTile!,
event.x,
event.y,
);
});
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

Capture tile locally before async callbacks to avoid stale-menu updates

At Line 118 and Line 188, the callback reads this.clickedTile again after the async call. If the user right-clicks another tile before resolve, actions for tile A can be applied to tile B.

Proposed fix
-      this.clickedTile = this.game.ref(worldCoords.x, worldCoords.y);
+      this.clickedTile = this.game.ref(worldCoords.x, worldCoords.y);
+      const clickedTile = this.clickedTile;
...
-      myPlayer.actions(this.clickedTile).then((actions) => {
+      myPlayer.actions(clickedTile).then((actions) => {
         this.updatePlayerActions(
           myPlayer,
           actions,
-          this.clickedTile!,
+          clickedTile,
           event.x,
           event.y,
         );
       });
...
-    myPlayer.actions(this.clickedTile).then((actions) => {
-      this.updatePlayerActions(myPlayer, actions, this.clickedTile!);
+    const clickedTile = this.clickedTile;
+    myPlayer.actions(clickedTile).then((actions) => {
+      this.updatePlayerActions(myPlayer, actions, clickedTile);
     });

Also applies to: 188-190

🤖 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 118 - 126, The
issue is that this.clickedTile is accessed inside async callbacks in the methods
around lines 118 and 188, leading to stale or incorrect tile usage if it changes
before the promise resolves. Fix this by capturing the current clickedTile value
into a local constant before calling myPlayer.actions or other async functions,
then use that local constant inside the callback to update the player actions
consistently for the intended tile.

Comment on lines 1015 to +1022
${this.renderAllianceExpiry()}

<ui-divider></ui-divider>

<!-- Actions -->
${this.renderActions(my, other)}
${isReplay
? ""
: html`
<ui-divider></ui-divider>
<!-- Actions -->
${this.renderActions(viewer, other)}
`}
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 | 🟡 Minor | ⚡ Quick win

Hide or clear alliance-expiry state in replay mode

Line 1015 still renders alliance expiry in replay, but replay does not refresh that value (live-only update path), so stale text can leak from previous state.

Proposed fix
-                    <!-- Alliance time remaining -->
-                    ${this.renderAllianceExpiry()}
+                    <!-- Alliance time remaining -->
+                    ${isReplay ? "" : this.renderAllianceExpiry()}

Optionally also clear on replay entry in show()/tick() for extra safety.

🤖 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/PlayerPanel.ts` around lines 1015 - 1022,
renderAllianceExpiry() is still rendered when isReplay is true, causing stale
expiry text to persist because replay doesn't update the live-only value; stop
rendering or clear the underlying state when in replay: update the template
where renderAllianceExpiry() is used (near the current conditional around
renderActions) so it is not called when isReplay is true, and additionally
ensure show() and/or tick() detect entering replay mode and clear the
alliance-expiry state used by renderAllianceExpiry() (or set it to an
empty/undefined value) to be extra safe; reference renderAllianceExpiry(),
renderActions(), show(), and tick() when making the changes.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management May 7, 2026
@mike-s-zaugg
Copy link
Copy Markdown
Contributor Author

mike-s-zaugg commented May 7, 2026

@FloPinguin I tested it localy and needed to apply some changes.

I added the features and screenshots.
Should I implement the propsed fixes from coderabbit aswell?

@FloPinguin
Copy link
Copy Markdown
Contributor

Thank you, I think it would make sense to just show the info panel on right click, skip the info-only-radial?

Yeah please check if rabbit is correct, sometimes it has good ideas / finds bugs, you can reply to its comments

Comment on lines +100 to 102
if (myPlayer === null && !isReplay) {
return;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

so in this case we haven't spawned yet?


const my = this.g.myPlayer();
if (!my) return html``;
const isReplay = this.g.config().isReplay();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it maybe be cleaner to create:

this.g.isSpectator()

and it just does:

isSpectator() {
!(this.myPlayer()?.isAlive) || this.config.isReplay
}

const my = this.g.myPlayer();
if (!my) return html``;
const isReplay = this.g.config().isReplay();
if (!my && !isReplay) return html``;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

so in this case we are in a live match but haven'ts spawned in? so we don't render the player panel?


<!-- Actions -->
${this.renderActions(my, other)}
${isReplay
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same here, instead of isReplay, it's isSpectator because being dead is the equivalent of watching a replay

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

4 participants