Skip to content

Conversation

@bijx
Copy link
Contributor

@bijx bijx commented Dec 27, 2025

If this PR fixes an issue, link it below. If not, delete these two lines.
Resolves #2705

Description:

Introduces a quick donation feature in games where the canDonateTroops option is enabled. It works by converting the center button in the radial menu from a disabled attack button to a troop donate button when the player right clicked on is friendly (teammate or ally).

Also adds donate gold button to Attack slot on radial menu when right clicking friendly troops.

Video Example

Recording.2025-12-26.214041.mp4

New Icon

image

Donate Gold

Recording.2025-12-27.000313.mp4

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

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

bijx

@bijx bijx requested a review from a team as a code owner December 27, 2025 02:42
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 27, 2025

Walkthrough

Radial menu detects friendly targets and switches the center button to a donate appearance and size; clicking donates troops using uiState.attackRatio. MenuElementParams accepts uiState. RadialMenu exposes setCenterButtonAppearance. PlayerActionHandler.handleDonateTroops accepts an optional troops amount.

Changes

Cohort / File(s) Summary
Radial menu core
src/client/graphics/layers/RadialMenu.ts, src/client/graphics/layers/RadialMenuElements.ts
Added mutable center button color/size and getDefaultCenterIconSize(); new setCenterButtonAppearance(icon, color?, iconSize?). MenuElementParams gains optional uiState?: UIState. Added isFriendlyTarget() and swap center-button action to donate (uses uiState.attackRatio) when target is friendly; adjusted disabled rules.
Main radial wiring
src/client/graphics/layers/MainRadialMenu.ts
Import donateTroopIcon; update updateActions to compute friendly target and call setCenterButtonAppearance with donate icon/green (and reduced icon size) or attack icon/default.
Player action handler
src/client/graphics/layers/PlayerActionHandler.ts
handleDonateTroops(recipient, troops?) now accepts optional troops, computes/validates amount, and emits SendDonateTroopsIntentEvent with number or null.
Player UI panel
src/client/graphics/layers/PlayerPanel.ts
Added suppressNextHide flag; MouseUp handler may ignore the next hide. Added openSendGoldModal(actions, tile, target) and set suppressNextHide when opening send modals.

Sequence Diagram

sequenceDiagram
    autonumber
    actor Player
    participant RadialMenu
    participant RadialMenuElements
    participant PlayerActionHandler
    Note over Player,RadialMenuElements: hover / open radial menu
    Player->>RadialMenu: updateActions(recipient, uiState)
    RadialMenu->>RadialMenuElements: updateActions(recipient, uiState)
    RadialMenuElements->>RadialMenuElements: isFriendlyTarget(params)
    alt Friendly target
        RadialMenuElements->>RadialMenu: setCenterButtonAppearance(donateTroopIcon, "#34D399", 0.75)
    else Enemy target
        RadialMenuElements->>RadialMenu: setCenterButtonAppearance(swordIcon, "#2c3e50")
    end
    Note over Player: click center button
    Player->>RadialMenuElements: center button action
    alt Donate (friendly)
        RadialMenuElements->>PlayerActionHandler: handleDonateTroops(recipient, computedAmount)
        PlayerActionHandler->>PlayerActionHandler: validate amount
        PlayerActionHandler->>PlayerActionHandler: emit SendDonateTroopsIntentEvent(amount|null)
    else Attack (enemy)
        RadialMenuElements->>PlayerActionHandler: performAttack(recipient)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

Feature - Frontend

Suggested reviewers

  • evanpelle

Poem

🟢 Sword or helping hand, a button turns,
A smaller green that quietly returns.
Troops traverse from friend to friend,
A tiny aid the game can send. ✨

Pre-merge checks

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: adding a quick troop donation via radial menu center button for friendly targets.
Description check ✅ Passed The description clearly explains the feature, links to the resolved issue (#2705), and includes supporting materials like video examples and screenshots.
Linked Issues check ✅ Passed The PR implements the core objective from #2705: converting the radial menu center button to a quick troop donation action when targeting friendly players, eliminating the three-click flow.
Out of Scope Changes check ✅ Passed All changes directly support the troop donation feature: radial menu modifications, center button styling/behavior, player action handling, and supporting gold donations for friendly targets.

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
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: 0

Caution

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

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

1-1: Fix code formatting.

The pipeline reports a Prettier formatting issue. Run the suggested command to fix:

npx prettier --write src/client/graphics/layers/MainRadialMenu.ts
src/client/graphics/layers/RadialMenuElements.ts (1)

1-1: Fix code formatting.

The pipeline reports a Prettier formatting issue. Run the suggested command to fix:

npx prettier --write src/client/graphics/layers/RadialMenuElements.ts
🧹 Nitpick comments (2)
src/client/graphics/layers/MainRadialMenu.ts (1)

136-144: Refactor: Extract duplicated friendly-target logic.

The isFriendlyTarget logic here duplicates the helper function defined in RadialMenuElements.ts (lines 112-119). Consider importing and reusing the helper from RadialMenuElements.ts to maintain a single source of truth.

🔎 Suggested refactor

Import the helper from RadialMenuElements:

 import {
   centerButtonElement,
   COLORS,
+  isFriendlyTarget,
   MenuElementParams,
   rootMenuElement,
 } from "./RadialMenuElements";

Then replace the local computation:

-    const isFriendlyTarget =
-      recipient !== null &&
-      recipient.id() !== myPlayer.id() &&
-      recipient.isFriendly(myPlayer);
+    const friendly = isFriendlyTarget(params);

     this.radialMenu.setCenterButtonAppearance(
-      isFriendlyTarget ? donateTroopIcon : swordIcon,
-      isFriendlyTarget ? "#34D399" : "#2c3e50",
+      friendly ? donateTroopIcon : swordIcon,
+      friendly ? "#34D399" : "#2c3e50",
     );

Note: You'll need to export isFriendlyTarget from RadialMenuElements.ts first.

src/client/graphics/layers/RadialMenuElements.ts (1)

112-119: Consider exporting this helper for reuse.

The isFriendlyTarget helper encapsulates the friendly-player detection logic cleanly. Consider exporting it so MainRadialMenu.ts (lines 136-139) can reuse it instead of duplicating the same logic.

🔎 Suggested change
-function isFriendlyTarget(params: MenuElementParams): boolean {
+export function isFriendlyTarget(params: MenuElementParams): boolean {
   const selectedPlayer = params.selected;
   return (
     selectedPlayer !== null &&
     selectedPlayer.id() !== params.myPlayer.id() &&
     selectedPlayer.isFriendly(params.myPlayer)
   );
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d52f73 and 7e39668.

📒 Files selected for processing (4)
  • src/client/graphics/layers/MainRadialMenu.ts
  • src/client/graphics/layers/PlayerActionHandler.ts
  • src/client/graphics/layers/RadialMenu.ts
  • src/client/graphics/layers/RadialMenuElements.ts
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-05-31T18:15:03.445Z
Learnt from: 1brucben
Repo: openfrontio/OpenFrontIO PR: 977
File: src/core/execution/AttackExecution.ts:123-125
Timestamp: 2025-05-31T18:15:03.445Z
Learning: The removeTroops function in PlayerImpl.ts already prevents negative troop counts by using minInt(this._troops, toInt(troops)) to ensure it never removes more troops than available.

Applied to files:

  • src/client/graphics/layers/PlayerActionHandler.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/graphics/layers/PlayerActionHandler.ts
  • src/client/graphics/layers/MainRadialMenu.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/client/graphics/layers/PlayerActionHandler.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/graphics/layers/MainRadialMenu.ts
  • src/client/graphics/layers/RadialMenuElements.ts
  • src/client/graphics/layers/RadialMenu.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
📚 Learning: 2025-06-10T09:56:44.473Z
Learnt from: Ble4Ch
Repo: openfrontio/OpenFrontIO PR: 1063
File: src/core/configuration/PastelThemeDark.ts:53-53
Timestamp: 2025-06-10T09:56:44.473Z
Learning: In ColorAllocator class in src/core/configuration/Colors.ts, the correct method names are assignColor(id: string): Colord for general color assignment and assignTeamColor(team: Team): Colord for team-specific colors. There are no assignPlayerColor() or assignBotColor() methods.

Applied to files:

  • src/client/graphics/layers/RadialMenu.ts
🧬 Code graph analysis (3)
src/client/graphics/layers/PlayerActionHandler.ts (3)
src/core/game/GameView.ts (3)
  • PlayerView (185-578)
  • troops (101-103)
  • troops (493-495)
src/core/game/PlayerImpl.ts (1)
  • troops (832-834)
src/client/Transport.ts (1)
  • SendDonateTroopsIntentEvent (115-120)
src/client/graphics/layers/MainRadialMenu.ts (1)
src/core/game/GameView.ts (1)
  • myPlayer (724-726)
src/client/graphics/layers/RadialMenuElements.ts (2)
src/client/graphics/UIState.ts (1)
  • UIState (3-6)
src/core/game/GameView.ts (1)
  • PlayerView (185-578)
🪛 GitHub Actions: 🧪 CI
src/client/graphics/layers/MainRadialMenu.ts

[warning] 1-1: Code style issues found in file. Run 'npx prettier --write src/client/graphics/layers/MainRadialMenu.ts' to fix.

src/client/graphics/layers/RadialMenuElements.ts

[warning] 1-1: Code style issues found in file. Run 'npx prettier --write src/client/graphics/layers/RadialMenuElements.ts' to fix.

🔇 Additional comments (4)
src/client/graphics/layers/PlayerActionHandler.ts (1)

87-93: LGTM! Clean parameter-driven donation logic.

The updated signature accepts an optional troops parameter, properly guards against non-positive amounts, and emits the event with the computed value. The guard on line 89 correctly prevents donating zero or negative troops.

src/client/graphics/layers/RadialMenuElements.ts (1)

574-589: LGTM! Proper donation flow with good safeguards.

The center button action correctly:

  • Detects friendly targets using the helper
  • Uses uiState?.attackRatio ?? 1 with proper fallback
  • Applies Math.floor to ensure whole troop counts
  • Guards against zero-troop donations before calling the handler

The logic integrates well with the existing attack flow.

src/client/graphics/layers/RadialMenu.ts (2)

81-82: Good color state management.

The introduction of defaultCenterButtonColor as a readonly constant and centerButtonColor as mutable state provides clean separation between the default and current appearance. This pattern works well with the new dynamic appearance API.


959-981: LGTM! Clean API for dynamic center button appearance.

The setCenterButtonAppearance method properly:

  • Updates both icon and color state
  • Provides sensible fallback to defaultCenterButtonColor
  • Guards against missing menuElement
  • Applies changes immediately to the visible button via updateCenterButtonState

The implementation integrates well with the existing center button state management.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 27, 2025
coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 27, 2025
@evanpelle evanpelle added this to the v29 milestone Dec 27, 2025
coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 27, 2025
Copy link
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: 0

🧹 Nitpick comments (1)
src/client/graphics/layers/MainRadialMenu.ts (1)

136-142: Extract hardcoded colors to the COLORS constant for consistency.

The file already uses COLORS.tooltip.cost and COLORS.tooltip.count. Consider adding COLORS.centerButton.donate and COLORS.centerButton.attack to keep all colors in one place, then update the setCenterButtonAppearance call to use them instead of "#34D399" and "#2c3e50".

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb8b816 and ee72a5c.

📒 Files selected for processing (2)
  • src/client/graphics/layers/MainRadialMenu.ts
  • src/client/graphics/layers/RadialMenuElements.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/graphics/layers/RadialMenuElements.ts
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/graphics/layers/MainRadialMenu.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/graphics/layers/MainRadialMenu.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/graphics/layers/MainRadialMenu.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/client/graphics/layers/MainRadialMenu.ts
🧬 Code graph analysis (1)
src/client/graphics/layers/MainRadialMenu.ts (1)
src/core/game/GameView.ts (1)
  • myPlayer (728-730)
🔇 Additional comments (2)
src/client/graphics/layers/MainRadialMenu.ts (2)

23-23: LGTM!

The import follows the same pattern as the existing swordIcon import.


131-131: LGTM!

Correctly passes uiState to the params, enabling the radial menu to access UI state for the donate feature.

Copy link
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: 0

🧹 Nitpick comments (2)
src/client/graphics/layers/PlayerPanel.ts (1)

155-160: Consider resetting suppressNextHide in the hide() method.

Currently, if the panel is closed via the X button or CloseViewEvent before the MouseUpEvent fires, suppressNextHide remains true. While not breaking (since hide() is idempotent), this creates stale state that could suppress an unrelated future mouse event.

Suggested state cleanup
 public hide() {
   this.isVisible = false;
   this.sendMode = "none";
   this.sendTarget = null;
+  this.suppressNextHide = false;
   this.requestUpdate();
 }

Also applies to: 188-188, 194-194

src/client/graphics/layers/RadialMenuElements.ts (1)

433-449: Use a COLORS constant for the donate gold color.

Line 440 uses a hardcoded color "#EAB308". For consistency with the rest of the codebase, define this in the COLORS object.

🔎 Proposed refactor

Add to the COLORS object around line 85:

   trade: "#008080",
   embargo: "#6600cc",
+  donateGold: "#EAB308",
   tooltip: {

Then update line 440:

-  color: "#EAB308",
+  color: COLORS.donateGold,

Note: The id: Slot.Attack on line 434 matches attackMenuElement, but this is intentional since they occupy the same radial position conditionally. The menu composition logic on lines 633-635 ensures only one renders based on whether the target is friendly.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee72a5c and 5bbf5ce.

📒 Files selected for processing (2)
  • src/client/graphics/layers/PlayerPanel.ts
  • src/client/graphics/layers/RadialMenuElements.ts
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
  • src/client/graphics/layers/PlayerPanel.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
  • src/client/graphics/layers/PlayerPanel.ts
🧬 Code graph analysis (2)
src/client/graphics/layers/RadialMenuElements.ts (2)
src/client/graphics/UIState.ts (1)
  • UIState (3-6)
src/core/game/GameView.ts (1)
  • PlayerView (185-582)
src/client/graphics/layers/PlayerPanel.ts (4)
src/client/InputHandler.ts (1)
  • MouseUpEvent (8-13)
src/core/game/GameView.ts (2)
  • actions (406-412)
  • tile (110-112)
src/core/game/Game.ts (1)
  • PlayerActions (772-778)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
🪛 GitHub Actions: 🧪 CI
src/client/graphics/layers/RadialMenuElements.ts

[error] 114-114: TypeError: selectedPlayer.isFriendly is not a function. isFriendlyTarget calls selectedPlayer.isFriendly but the selected player object does not implement that method.

🔇 Additional comments (9)
src/client/graphics/layers/PlayerPanel.ts (4)

65-65: State flag added to control panel hiding behavior.

The suppressNextHide flag is properly declared with clean initialization. This prevents the panel from closing immediately after opening a modal when the user releases the mouse button.


82-88: MouseUpEvent handler correctly implements suppression logic.

The conditional check properly consumes the suppressNextHide flag on the next mouse-up event, preventing premature panel closure when modals are opened. Clean early return pattern.


141-153: New public API for opening gold donation modal directly.

The openSendGoldModal method properly sets up all necessary state for direct modal invocation, likely from the radial menu. Setting suppressNextHide = true correctly prevents the release-click from immediately closing the panel.


141-153: API is properly integrated with the radial menu.

The openSendGoldModal method is called from RadialMenuElements.ts (line 443) with the correct parameters when a player selects the donation option from the radial menu.

src/client/graphics/layers/RadialMenuElements.ts (5)

7-7: LGTM! UIState integration is clean.

Adding the optional uiState parameter to MenuElementParams allows menu elements to access attackRatio for donation calculations. The optional field is appropriate since not all menu contexts require UI state.

Also applies to: 41-41


577-580: Conditional gating logic looks correct.

The center button correctly disables for friendly targets when troop donation is not permitted, and for non-friendly targets when attack is not permitted. This provides appropriate access control for the dual-purpose button.

Note: This depends on fixing the isFriendlyTarget function (lines 112-115) which currently has a pipeline failure.


630-636: Clean conditional menu composition.

The ternary operator clearly switches between donateGoldRadialElement and attackMenuElement based on target type, making the dual-purpose slot behavior explicit and maintainable.

Note: This depends on fixing the isFriendlyTarget function (lines 112-115) which currently has a pipeline failure.


112-115: The isFriendly method exists and is correctly implemented on PlayerView (line 520 in src/core/game/GameView.ts). The code in RadialMenuElements.ts:112-115 is correct and requires no changes.

Likely an incorrect or invalid review comment.


588-603: No issues found. The code correctly prevents donations with zero or negative troop amounts through the guard clause at line 592 (if (troopsToDonate > 0)). The handler is never called with an invalid amount, and doing nothing when the calculated donation rounds to zero is the expected behavior—not a silent failure. The menu item is already disabled at the UI level if donations are not permitted. No changes needed.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 27, 2025
Copy link
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: 0

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/RadialMenuElements.ts (1)

587-610: Consider adding user feedback when troop donation cannot proceed.

The code uses params.uiState?.attackRatio ?? 1 as fallback, but uiState is always initialized in GameRenderer and passed through MainRadialMenu, so this default should not occur in practice. The actual concern is the silent no-op: when troopsToDonate is 0 (from low troop count or small ratio), the menu closes without any feedback to the player. Add a notification or disable the action more explicitly to guide players.

🧹 Nitpick comments (2)
src/client/graphics/layers/RadialMenuElements.ts (2)

112-118: Simplify method invocation.

The defensive checks are good (aligns with commit message about test safety), but the .call() usage is unnecessarily verbose.

🔎 Simpler equivalent
 function isFriendlyTarget(params: MenuElementParams): boolean {
   const selectedPlayer = params.selected;
   if (selectedPlayer === null) return false;
-  const isFriendly = (selectedPlayer as PlayerView).isFriendly;
-  if (typeof isFriendly !== "function") return false;
-  return isFriendly.call(selectedPlayer, params.myPlayer);
+  const playerView = selectedPlayer as PlayerView;
+  if (typeof playerView.isFriendly !== "function") return false;
+  return playerView.isFriendly(params.myPlayer);
 }

436-452: Use COLORS constant for consistency.

The hardcoded color "#EAB308" (line 443) breaks the pattern used elsewhere in the file. Consider adding a named constant to the COLORS object for better maintainability.

🔎 Consistent with existing patterns

At the COLORS definition (around line 73), add:

 export const COLORS = {
   build: "#ebe250",
   building: "#2c2c2c",
   boat: "#3f6ab1",
   ally: "#53ac75",
+  donate: "#EAB308",
   breakAlly: "#c74848",
   // ...

Then use it:

   icon: donateGoldIcon,
-  color: "#EAB308",
+  color: COLORS.donate,
   action: (params: MenuElementParams) => {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5bbf5ce and 9dc370d.

📒 Files selected for processing (1)
  • src/client/graphics/layers/RadialMenuElements.ts
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
🧬 Code graph analysis (1)
src/client/graphics/layers/RadialMenuElements.ts (3)
src/client/graphics/UIState.ts (1)
  • UIState (3-6)
src/core/game/GameView.ts (2)
  • isFriendly (520-522)
  • PlayerView (185-582)
src/core/game/PlayerImpl.ts (1)
  • isFriendly (805-810)
🔇 Additional comments (3)
src/client/graphics/layers/RadialMenuElements.ts (3)

41-41: LGTM! Optional field maintains backward compatibility.

The optional uiState field is a clean addition to the interface that allows existing code to continue working while new donation features can access the UI state.


568-586: LGTM! Correct gating logic.

The center button correctly disables based on whether the target is friendly and whether troop donation is allowed.


633-639: LGTM! Correct conditional rendering.

The radial menu appropriately swaps between donation and attack elements based on target friendliness.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 27, 2025
Copy link
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: 0

🧹 Nitpick comments (1)
src/client/graphics/layers/MainRadialMenu.ts (1)

136-145: LGTM! Clean logic for switching center button appearance.

The friendly target detection is null-safe, and the conditional appearance update correctly switches between donate and attack modes. The 0.75 size multiplier aligns with your commit message about visual appearance next to the donate gold icon.

Optional: Extract colors to COLORS constant for consistency

The file already uses a COLORS object for tooltip styling. For consistency, consider extracting the hardcoded hex values:

  const menuConfig: RadialMenuConfig = {
    centerButtonIcon: swordIcon,
    tooltipStyle: `
      .radial-tooltip .cost {
        margin-top: 4px;
        color: ${COLORS.tooltip.cost};
      }
      .radial-tooltip .count {
        color: ${COLORS.tooltip.count};
      }
    `,
  };

In RadialMenuElements.ts, you could add:

centerButton: {
  friendly: "#34D399",  // green for donate
  attack: "#2c3e50",    // gray for attack
}

Then use COLORS.centerButton.friendly and COLORS.centerButton.attack here.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9dc370d and f9af360.

📒 Files selected for processing (2)
  • src/client/graphics/layers/MainRadialMenu.ts
  • src/client/graphics/layers/RadialMenu.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/graphics/layers/RadialMenu.ts
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/graphics/layers/MainRadialMenu.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/graphics/layers/MainRadialMenu.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/graphics/layers/MainRadialMenu.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/client/graphics/layers/MainRadialMenu.ts
🧬 Code graph analysis (1)
src/client/graphics/layers/MainRadialMenu.ts (1)
src/core/game/GameView.ts (1)
  • myPlayer (728-730)
🔇 Additional comments (2)
src/client/graphics/layers/MainRadialMenu.ts (2)

23-23: LGTM!

The donate troop icon import is correctly placed and used in the center button appearance logic below.


131-131: LGTM!

Adding uiState to params enables radial menu elements to access the attack ratio for troop donations, which is necessary for the quick donate feature.

Copy link
Collaborator

@evanpelle evanpelle left a comment

Choose a reason for hiding this comment

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

thanks!

@evanpelle evanpelle merged commit 7284ded into openfrontio:main Dec 27, 2025
7 checks passed
@bijx bijx mentioned this pull request Dec 31, 2025
4 tasks
evanpelle pushed a commit that referenced this pull request Dec 31, 2025
## Description:

I believe this bug came after the introduction of the new donation
button in the radial menu (#2708) causing the rectangular SVGs to get
squished/skewed into square aspect ratios. This fix adds a little check
to fallback onto original image dimensions when an SVG's aspect ratio
isn't available. I tried a hardcoded fix earlier but if we ever decide
to add different scaled icons into the radial menu again, this will
automatically ensure the correct aspect ratio is used and the icon is
centered properly.

### Before Fix
<img width="1133" height="473" alt="image"
src="https://github.com/user-attachments/assets/5f06b19c-7072-4650-a1b7-4cb2bf1200dc"
/>

### After Fix
<img width="840" height="421" alt="image"
src="https://github.com/user-attachments/assets/176f1e7b-84bf-4b06-9ad6-4031e516d4ff"
/>

## 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:

bijx
github-merge-queue bot pushed a commit that referenced this pull request Jan 2, 2026
…2769)

## Description:

Updates the instructions Help Menu to update the ally radial menu
screenshot to show the new gold and troop donation icons, as well as
what they do. Related to #2708

<img width="1656" height="974" alt="image"
src="https://github.com/user-attachments/assets/365e0fe5-6854-4cac-8288-039a05cf4905"
/>


## 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:

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add shortcut to send troops to teammate

2 participants