Skip to content

Conversation

Dimitrije-V
Copy link

@Dimitrije-V Dimitrije-V commented Aug 14, 2025

Description:

This PR adds interactive nuke previews (Atom, Hydrogen, MIRV) to the build & radial menus, and dims the UI while previewing so players can aim with confidence.

What’s included

  • New render layer: NukePreview (src/client/graphics/layers/NukePreview.ts)
    • Reads from uiState.nukePreview (active + nuke type) and uiState.nukeAnchor (world tile).
    • Atom/Hydrogen:
    • Draws inner-radius ring + soft fill, probabilistic band to outer radius, and two animated dashed rings outside the band.
    • Uses UnitType.AtomBomb / Unit.Type.HydrogenBomb magnitudes (from the config) for ring sizes.
    • MIRV: draws mini targets spread across the target nation:
      • Uniform sampling within 1500-tile radius (matches MirvExecution.mirvRange).
      • Owner-gated (same owner as the anchor) and land-only tiles.
      • Min spacing of 25 tiles Manhattan (mirrors proximityCheck).
      • Uses UnitType.MIRVWarhead magnitudes (from the config) for ring sizes.
      • Deterministic per (type, anchor, owner) with cached results until signature changes.
  • UIState
    • UIState extended to contain:
nukePreview?: { active: boolean; nukeType: string }; 
nukeAnchor?: { x: number; y: number };
  • Wiring & UX:
    • GameRenderer: registers NukePreview layer and provides uiState.
    • BuildMenu:
      • Sets uiState.nukeAnchor to the clicked world tile when the menu opens.
      • On hover over enabled Atom/Hydrogen/MIRV items: sets uiState.nukePreview and adds a subtle “dim” state to the menu; clears on leave/close.
    • RadialMenu:
      • Receives uiState in MenuElementParams.
      • createMenuElements: marks nukes (isNuke) and provides onHoverEnter/onHoverLeave to set/clear uiState.nukePreview.
      • Adds a small helper to dim the radial container while previewing.
      • Stores uiState.nukeAnchor when opening on a tile.
    • PlayerActionHandler: small helpers to start/stop preview (used internally where helpful).

You can see what this looks like here:

Atom.mp4

Hydro.mp4

MIRV.mp4

Build.Menu.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:

.dimitrije

@CLAassistant
Copy link

CLAassistant commented Aug 14, 2025

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

coderabbitai bot commented Aug 14, 2025

Walkthrough

Adds a new NukePreview rendering layer and UIState fields (nukePreview, nukeAnchor), wires UIState through GameRenderer, BuildMenu, MainRadialMenu, RadialMenu, and RadialMenuElements, introduces hover-driven dimming and preview hooks, PlayerActionHandler helpers, and unit tests for rendering and hover behavior.

Changes

Cohort / File(s) Summary
Render wiring
src/client/graphics/GameRenderer.ts
Import and instantiate NukePreview; extend and pass shared uiState; register layer and assign uiState to BuildMenu.
UI state type
src/client/graphics/UIState.ts
Add nukePreview?: { active: boolean; nukeType: NukeType } and nukeAnchor?: { x:number; y:number }; import NukeType.
Build menu
src/client/graphics/layers/BuildMenu.ts
Add public uiState!: UIState; detect nuke unit hover, manage _dimForNuke, set/clear uiState.nukePreview, store nukeAnchor on show/hide, and apply dim styling.
Radial menu wiring
src/client/graphics/layers/MainRadialMenu.ts, src/client/graphics/layers/RadialMenu.ts, src/client/graphics/layers/RadialMenuElements.ts
Record nukeAnchor on context events; pass uiState into menu params; add isNuke, onHoverEnter/onHoverLeave hooks; dim/undim overlay on nuke hover and ensure reset on hide/click.
New preview layer
src/client/graphics/layers/NukePreview.ts
Add NukePreview layer implementing Layer; render deterministic MIRV and non‑MIRV previews using seeded PRNG, cache MIRV targets per anchor, read uiState.nukePreview/nukeAnchor.
Player actions
src/client/graphics/layers/PlayerActionHandler.ts
Add startNukePreview(t: UnitType) and stopNukePreview() to toggle uiState.nukePreview for nuke unit types.
Menu element types
src/client/graphics/layers/RadialMenuElements.ts
Extend MenuElement with isNuke?: boolean, onHoverEnter?, onHoverLeave?; add uiState: UIState to MenuElementParams.
Tests
tests/client/graphics/NukePreview.test.ts, tests/client/graphics/RadialMenuElements.test.ts
Add tests for NukePreview rendering (MIRV vs non‑MIRV) and radial menu nuke-hover behavior; include mocked canvas, game, transform and assert draw/callback behavior.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Menu as RadialMenu/BuildMenu
  participant State as UIState
  participant Renderer as GameRenderer
  participant NP as NukePreview

  User->>Menu: hover nuclear menu item
  Menu->>State: set nukePreview = {active:true, nukeType}
  Menu->>Menu: dim overlay
  Renderer->>NP: renderLayer(ctx)
  NP->>State: read nukePreview and nukeAnchor
  NP->>NP: compute radii or MIRV targets (seeded)
  NP->>Renderer: draw preview visuals
  User-->>Menu: hover leave / hide
  Menu->>State: nukePreview = undefined
  Menu->>Menu: undim overlay
Loading
sequenceDiagram
  participant GameLoop
  participant Layers
  participant NukePreview

  loop each frame
    GameLoop->>Layers: renderLayer(ctx)
    alt nukePreview active
      Layers->>NukePreview: renderLayer(ctx)
      NukePreview->>NukePreview: compute & cache targets/radii
      NukePreview->>GameLoop: draw previews
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related issues

Possibly related PRs

Suggested labels

Feature - Frontend, Feature - New

Suggested reviewers

  • scottanderson
  • evanpelle

Poem

A ring on the map, a seeded bright flare,
Menus dim softly when nukes hover there.
Targets align by deterministic art,
Canvas draws markers — stable from the start.
Tests nod and pass; the preview plays its part.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

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

38-40: Optional: Clear nukeAnchor on stop to avoid stale anchors

If the menu closes while previewing, you can end up with a leftover anchor. Consider also clearing nukeAnchor when stopping the preview. If other flows rely on keeping the last anchor, ignore this.

For clarity, update as follows (outside the shown segment):

   stopNukePreview() {
     this.uiState.nukePreview = undefined;
+    this.uiState.nukeAnchor = undefined;
   }
src/client/graphics/GameRenderer.ts (1)

52-52: Annotate uiState and drop redundant undefined

Prefer an explicit type and omit optional fields when not set. This makes the intent clearer and avoids accidental shape drift.

Apply this diff:

-  const uiState = { attackRatio: 20, nukePreview: undefined };
+  const uiState: UIState = { attackRatio: 20 };
tests/client/graphics/RadialMenuElements.test.ts (1)

515-579: Great coverage for hover-driven nuke previews

The tests assert isNuke, set/clear on hover, and exclude non-nukes. Consider adding a Hydrogen Bomb case to cover the full nuclear set.

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

372-378: Duplicate helper function across files.

The isNukeType helper is duplicated in both BuildMenu.ts and RadialMenuElements.ts. Consider extracting this to a shared utility.

Would you like me to create a shared utility file for nuclear-related helpers to avoid duplication?

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

189-189: Type assertion could be improved.

The code uses (anchorOwner as any).id() which bypasses TypeScript's type checking. Consider using a proper type guard.

-    const sig = `MIRV|${ax}|${ay}|${anchorOwner.isPlayer() ? (anchorOwner as any).id() : "TN"}`;
+    const sig = `MIRV|${ax}|${ay}|${anchorOwner.isPlayer() ? (anchorOwner as PlayerView).id() : "TN"}`;

Note: You'll need to import PlayerView from the appropriate module.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2ed0cef and 59eff9f.

📒 Files selected for processing (10)
  • src/client/graphics/GameRenderer.ts (4 hunks)
  • src/client/graphics/UIState.ts (1 hunks)
  • src/client/graphics/layers/BuildMenu.ts (8 hunks)
  • src/client/graphics/layers/MainRadialMenu.ts (2 hunks)
  • src/client/graphics/layers/NukePreview.ts (1 hunks)
  • src/client/graphics/layers/PlayerActionHandler.ts (2 hunks)
  • src/client/graphics/layers/RadialMenu.ts (6 hunks)
  • src/client/graphics/layers/RadialMenuElements.ts (6 hunks)
  • tests/client/graphics/NukePreview.test.ts (1 hunks)
  • tests/client/graphics/RadialMenuElements.test.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-05-18T23:36:12.847Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#784
File: src/core/game/StatsImpl.ts:143-159
Timestamp: 2025-05-18T23:36:12.847Z
Learning: In this codebase, NukeType is a union type derived from UnitType values (specifically bomb-related values like AtomBomb, HydrogenBomb, MIRV, and MIRVWarhead) rather than a separate enum. This means comparing NukeType values against UnitType values in switch statements is valid and intentional.

Applied to files:

  • src/client/graphics/layers/PlayerActionHandler.ts
  • src/client/graphics/layers/BuildMenu.ts
  • src/client/graphics/layers/RadialMenuElements.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#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:

  • tests/client/graphics/RadialMenuElements.test.ts
  • src/client/graphics/layers/RadialMenu.ts
  • src/client/graphics/layers/MainRadialMenu.ts
📚 Learning: 2025-05-18T23:36:12.847Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#784
File: src/core/game/StatsImpl.ts:143-159
Timestamp: 2025-05-18T23:36:12.847Z
Learning: In this codebase, NukeType is a union type derived from specific UnitType enum values (AtomBomb, HydrogenBomb, MIRV, MIRVWarhead). This means that comparisons in switch statements between a NukeType parameter and UnitType enum values are valid and will work correctly at runtime.

Applied to files:

  • src/client/graphics/layers/BuildMenu.ts
  • src/client/graphics/layers/RadialMenuElements.ts
🧬 Code Graph Analysis (7)
tests/client/graphics/NukePreview.test.ts (1)
src/client/graphics/layers/NukePreview.ts (1)
  • NukePreview (8-292)
src/client/graphics/layers/NukePreview.ts (6)
src/client/graphics/layers/Layer.ts (1)
  • Layer (1-7)
src/core/game/GameView.ts (1)
  • GameView (360-692)
src/client/graphics/TransformHandler.ts (1)
  • TransformHandler (15-296)
src/client/graphics/UIState.ts (1)
  • UIState (1-8)
src/core/StatsSchemas.ts (1)
  • NukeType (11-15)
src/core/game/Game.ts (1)
  • Cell (285-307)
tests/client/graphics/RadialMenuElements.test.ts (2)
src/core/game/GameView.ts (1)
  • PlayerView (180-358)
src/client/graphics/layers/RadialMenuElements.ts (1)
  • attackMenuElement (428-439)
src/client/graphics/layers/RadialMenu.ts (1)
src/client/graphics/layers/RadialMenuElements.ts (1)
  • MenuElementParams (29-43)
src/client/graphics/layers/BuildMenu.ts (1)
src/client/graphics/UIState.ts (1)
  • UIState (1-8)
src/client/graphics/layers/RadialMenuElements.ts (2)
src/client/graphics/UIState.ts (1)
  • UIState (1-8)
src/client/graphics/layers/BuildMenu.ts (1)
  • isNukeType (372-378)
src/client/graphics/GameRenderer.ts (1)
src/client/graphics/layers/NukePreview.ts (1)
  • NukePreview (8-292)
🔇 Additional comments (16)
src/client/graphics/layers/MainRadialMenu.ts (1)

133-134: Passing uiState to radial params is correct

This enables hover-driven preview toggles in menu items and keeps state shared across layers. Looks good.

src/client/graphics/GameRenderer.ts (3)

26-26: Adding NukePreview to the renderer is correct

Import and registration look good and match the new layer’s constructor.


76-77: Wiring uiState into BuildMenu is correct

Ensures build menu can dim and drive previews. Good.


274-275: Layer order: NukePreview after world layers is sensible

Rendering the preview after terrain/units ensures it overlays the map while DOM-based UI stays above the canvas. Good placement.

tests/client/graphics/RadialMenuElements.test.ts (1)

150-155: Test params shape for uiState is fine

Stubbed UI state matches what the radial elements expect.

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

378-382: LGTM! Clean dimming implementation for nuclear previews.

The setMenuDim method provides a simple, focused way to dim the radial menu during nuclear hover states. The opacity value and transition timing align well with the build menu's dimming behavior.


408-409: Good integration with nuclear preview system.

The hover enter logic correctly triggers dimming and invokes the item's hover callback when isNuke is true, enabling coordinated preview rendering.


789-789: Good cleanup of dim state on menu hide.

Properly resetting the dim state when hiding the menu ensures the UI remains consistent.

tests/client/graphics/NukePreview.test.ts (1)

1-144: Well-structured test coverage for nuclear preview rendering.

The tests effectively verify both Atom Bomb and MIRV rendering paths with proper mocking and assertions. The deterministic time mocking ensures stable dash pattern calculations across test runs.

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

347-353: Clean nuclear type detection helper.

The isNukeType function provides a clear, centralized way to identify nuclear units. This matches the pattern used in BuildMenu.ts.


415-424: Well-implemented hover handlers for nuclear previews.

The hover enter/leave callbacks properly manage the UI state for nuclear previews, setting the active state and nuke type on hover and clearing it on leave.

src/client/graphics/layers/BuildMenu.ts (3)

157-158: Good anchor position storage for nuclear previews.

Setting nukeAnchor when showing the build menu ensures the preview system has the correct reference point for rendering.


380-399: Clean nuclear hover implementation with proper UI state management.

The hover handlers correctly manage both the dimming state and the UI state's nuclear preview, ensuring coordinated behavior across components.


470-475: Efficient conditional hover handling for nuclear units.

The pointer event handlers only trigger nuclear preview logic when the unit type is nuclear and enabled, avoiding unnecessary processing.

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

1-24: Clean layer implementation with proper TypeScript patterns.

The class structure follows good TypeScript practices with typed imports, clear interface implementation, and proper constructor parameter typing.


156-291: Excellent MIRV preview implementation with deterministic targeting.

The MIRV rendering logic is well-structured with:

  • Deterministic target generation using seeded random sampling
  • Proper owner and terrain validation
  • Efficient caching with signature-based invalidation
  • Clear spacing constraints between targets

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Aug 14, 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: 2

♻️ Duplicate comments (3)
src/client/graphics/layers/RadialMenu.ts (3)

443-443: Guard onHoverLeave call; remove non-null assertion

Non-null assertion is unnecessary and flagged by ESLint. Also aligns with earlier feedback to ensure params is non-null before invoking callbacks.

Apply:

-      d.data.onHoverLeave?.(this.params!);
+      if (this.params) {
+        d.data.onHoverLeave?.(this.params);
+      }

455-457: Guard onHoverLeave on click; remove non-null assertion

Same issue as Line 443. Also satisfies the ESLint hint (“This assertion is unnecessary…”).

Apply:

-      if (d.data.isNuke) this.setMenuDim(false);
-      d.data.onHoverLeave?.(this.params!);
+      if (d.data.isNuke) this.setMenuDim(false);
+      if (this.params) {
+        d.data.onHoverLeave?.(this.params);
+      }

408-410: Guard params before invoking onHoverEnter; avoid the cast

Avoid relying on the earlier disabled check; make the callback call explicit and remove the unnecessary type assertion.

Apply:

-      if (d.data.isNuke) this.setMenuDim(true);
-      d.data.onHoverEnter?.(this.params as MenuElementParams);
+      if (d.data.isNuke) this.setMenuDim(true);
+      if (this.params) {
+        d.data.onHoverEnter?.(this.params);
+      }
🧹 Nitpick comments (4)
tests/client/graphics/RadialMenuElements.test.ts (1)

516-580: Broaden hover-preview tests to cover edge cases

Two useful additions:

  • Add a Hydrogen Bomb case to ensure parity with Atom/MIRV.
  • Verify that hovering a non-nuke does not clear an active preview set by a prior nuke hover (to prevent accidental preview reset due to out-of-order mouse events).

Example to add after your current tests:

it("does not clear active nukePreview on non-nuke hover leave", () => {
  const enemyPlayer = { id: () => 2, isPlayer: jest.fn(() => true) } as unknown as PlayerView;
  mockParams.selected = enemyPlayer;

  const subMenu = attackMenuElement.subMenu!(mockParams);
  const atom = subMenu.find((i) => i.id === "attack_Atom Bomb");
  const warship = subMenu.find((i) => i.id === "attack_Warship");

  atom!.onHoverEnter?.(mockParams);
  expect(mockParams.uiState.nukePreview?.nukeType).toBe(UnitType.AtomBomb);

  warship!.onHoverLeave?.(mockParams);
  // still active since the leave event was from a non-nuke item
  expect(mockParams.uiState.nukePreview?.nukeType).toBe(UnitType.AtomBomb);
});

it("sets and clears nukePreview on hover for Hydrogen Bomb", () => {
  const enemyPlayer = { id: () => 2, isPlayer: jest.fn(() => true) } as unknown as PlayerView;
  mockParams.selected = enemyPlayer;

  const subMenu = attackMenuElement.subMenu!(mockParams);
  const hydro = subMenu.find((i) => i.id === "attack_Hydrogen Bomb");

  expect(hydro).toBeDefined();
  expect(hydro!.isNuke).toBe(true);

  hydro!.onHoverEnter?.(mockParams);
  expect(mockParams.uiState.nukePreview).toEqual({ active: true, nukeType: UnitType.HydrogenBomb });

  hydro!.onHoverLeave?.(mockParams);
  expect(mockParams.uiState.nukePreview).toBeUndefined();
});
src/client/graphics/layers/RadialMenu.ts (1)

378-383: Minor nit: comment typo and style

The trailing parenthesis in the comment is stray.

Apply:

-      .style("transition", "opacity 120ms ease"); // smooth like the build menu)
+      .style("transition", "opacity 120ms ease"); // smooth like the build menu
src/client/graphics/layers/RadialMenuElements.ts (2)

347-354: Avoid duplicate isNukeType definitions; centralize in one helper

BuildMenu.ts already defines isNukeType. Duplicating here risks drift. Prefer a shared helper (e.g., src/client/graphics/Nuke.ts) and import it in both places.

Example:

// src/client/graphics/Nuke.ts
import { UnitType } from "../../core/game/Game";
export const isNukeType = (t: UnitType) =>
  t === UnitType.AtomBomb || t === UnitType.HydrogenBomb || t === UnitType.MIRV;

Then:

// here and in BuildMenu.ts
import { isNukeType } from "../Nuke";

27-27: Consider tightening UIState.nukePreview typing to NukeType (union of UnitType values)

UIState defines nukeType as string, but you consistently use UnitType.* values (tests included). Prefer a precise union type to catch errors at compile time.

Additional change outside this file (UIState.ts):

// src/client/graphics/UIState.ts
import { UnitType } from "../../core/game/Game";

export type NukeType =
  | UnitType.AtomBomb
  | UnitType.HydrogenBomb
  | UnitType.MIRV
  | UnitType.MIRVWarhead;

export type UIState = {
  attackRatio: number;
  nukePreview?: {
    active: boolean;
    nukeType: NukeType;
  };
  nukeAnchor?: { x: number; y: number };
};

This keeps types aligned with how the rest of the code uses nuke types.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 59eff9f and 98e7c60.

📒 Files selected for processing (6)
  • src/client/graphics/GameRenderer.ts (4 hunks)
  • src/client/graphics/layers/BuildMenu.ts (8 hunks)
  • src/client/graphics/layers/MainRadialMenu.ts (2 hunks)
  • src/client/graphics/layers/RadialMenu.ts (6 hunks)
  • src/client/graphics/layers/RadialMenuElements.ts (6 hunks)
  • tests/client/graphics/RadialMenuElements.test.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/client/graphics/layers/MainRadialMenu.ts
  • src/client/graphics/GameRenderer.ts
  • src/client/graphics/layers/BuildMenu.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#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:

  • tests/client/graphics/RadialMenuElements.test.ts
  • src/client/graphics/layers/RadialMenu.ts
📚 Learning: 2025-05-18T23:36:12.847Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#784
File: src/core/game/StatsImpl.ts:143-159
Timestamp: 2025-05-18T23:36:12.847Z
Learning: In this codebase, NukeType is a union type derived from UnitType values (specifically bomb-related values like AtomBomb, HydrogenBomb, MIRV, and MIRVWarhead) rather than a separate enum. This means comparing NukeType values against UnitType values in switch statements is valid and intentional.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
📚 Learning: 2025-05-18T23:36:12.847Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#784
File: src/core/game/StatsImpl.ts:143-159
Timestamp: 2025-05-18T23:36:12.847Z
Learning: In this codebase, NukeType is a union type derived from specific UnitType enum values (AtomBomb, HydrogenBomb, MIRV, MIRVWarhead). This means that comparisons in switch statements between a NukeType parameter and UnitType enum values are valid and will work correctly at runtime.

Applied to files:

  • src/client/graphics/layers/RadialMenuElements.ts
🧬 Code Graph Analysis (3)
tests/client/graphics/RadialMenuElements.test.ts (2)
src/core/game/GameView.ts (1)
  • PlayerView (180-358)
src/client/graphics/layers/RadialMenuElements.ts (1)
  • attackMenuElement (428-439)
src/client/graphics/layers/RadialMenu.ts (1)
src/client/graphics/layers/RadialMenuElements.ts (1)
  • MenuElementParams (29-43)
src/client/graphics/layers/RadialMenuElements.ts (1)
src/client/graphics/UIState.ts (1)
  • UIState (1-8)
🪛 GitHub Check: 🔍 ESLint
src/client/graphics/layers/RadialMenu.ts

[failure] 456-456:
This assertion is unnecessary since it does not change the type of the expression.

src/client/graphics/layers/RadialMenuElements.ts

[failure] 62-62:
Missing semicolon.

🔇 Additional comments (4)
tests/client/graphics/RadialMenuElements.test.ts (1)

151-155: LGTM: uiState scaffolding in test params is correct

Providing attackRatio and the optional nukePreview/nukeAnchor aligns with the new MenuElementParams shape. This keeps tests realistic and future-proof.

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

789-790: LGTM: reset dimming on hide

Ensures the overlay is undimmed on menu close, keeping UI state consistent.

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

41-41: LGTM: uiState added to MenuElementParams

Plumbing shared UI state into menu callbacks is the right surface for hover-driven previews.


386-386: LGTM: emit isNuke flag on menu items

This pairs cleanly with RadialMenu’s dimming and hover hooks.

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/RadialMenu.ts (2)

378-382: Solid helper; minor polish on transition and comment.

The dimming helper is clean and easy to reuse. Two small tweaks:

  • Set the transition once during element creation to avoid resetting it on every call.
  • Fix the stray parenthesis in the comment.

Apply this diff here:

   private setMenuDim(dim: boolean) {
     this.menuElement
       .style("opacity", dim ? "0.1" : "1")
-      .style("transition", "opacity 120ms ease"); // smooth like the build menu)
+      .style("transition", "opacity 120ms ease"); // smooth like the build menu
   }

Optionally set the transition once in createMenuElement (outside the selected range):

// after creating this.menuElement in createMenuElement()
this.menuElement.style("transition", "opacity 120ms ease");

408-410: Guard onHoverEnter with an explicit params check to avoid the cast.

You already gate earlier when disabled, but a direct guard keeps the types honest and matches our explicit-null-check style.

Apply this diff:

-      if (d.data.isNuke) this.setMenuDim(true);
-      d.data.onHoverEnter?.(this.params as MenuElementParams);
+      if (d.data.isNuke) this.setMenuDim(true);
+      if (this.params) {
+        d.data.onHoverEnter?.(this.params);
+      }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 98e7c60 and 64e5435.

📒 Files selected for processing (6)
  • src/client/graphics/UIState.ts (1 hunks)
  • src/client/graphics/layers/MainRadialMenu.ts (3 hunks)
  • src/client/graphics/layers/NukePreview.ts (1 hunks)
  • src/client/graphics/layers/PlayerActionHandler.ts (2 hunks)
  • src/client/graphics/layers/RadialMenu.ts (6 hunks)
  • src/client/graphics/layers/RadialMenuElements.ts (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/client/graphics/layers/PlayerActionHandler.ts
  • src/client/graphics/layers/MainRadialMenu.ts
  • src/client/graphics/UIState.ts
  • src/client/graphics/layers/RadialMenuElements.ts
  • src/client/graphics/layers/NukePreview.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#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/RadialMenu.ts
🧬 Code Graph Analysis (1)
src/client/graphics/layers/RadialMenu.ts (1)
src/client/graphics/layers/RadialMenuElements.ts (1)
  • MenuElementParams (29-43)
🔇 Additional comments (4)
src/client/graphics/layers/RadialMenu.ts (4)

423-423: Good: undim on mouseout before early returns.

Resetting dim state even when the handler bails keeps the UI consistent.


443-445: Nice fix: hover-leave callback now guarded by params.

This addresses the prior null assertion risk and aligns with our explicit checks preference.


457-461: Good consistency: clear dim and guard onHoverLeave on click.

Prevents lingering dim state and avoids null assertions during click handling.


793-794: Correct reset of dim state when hiding the menu.

Ensures the radial always returns to a clean state across close paths.

coderabbitai[bot]
coderabbitai bot previously approved these changes Aug 14, 2025
@TheGiraffe3 TheGiraffe3 linked an issue Aug 14, 2025 that may be closed by this pull request
Copy link
Contributor

@scottanderson scottanderson left a comment

Choose a reason for hiding this comment

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

  1. Please embed the videos/images directly into the PR description, instead of linking to them.
  2. This ticket is not associated with its related issue. Please see https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue for details on how to link tickets properly.
  3. The UI here is overly complex and visually busy, which I think will confuse players and lead to negative feedback. I’m not sure players are asking for all of this information.

@Dimitrije-V
Copy link
Author

@TheGiraffe3
Thanks for linking the issue to the PR.

@scottanderson
The first two points are addressed. Regarding point 3:

  • As stated in the issue, I think players need to know both where the bomb will hit, and where it might hit. Experienced players might have a good sense of this, but I've seen Youtube videos, like ones from EnzoPlays, where even he gets it wrong. I definitely do, and I've seen players lose as a result of an alliance break from a poorly placed nuke.
  • With that said, for players who don't want this, I would happily place this feature behind a setting; this seems like it would be easy to do, and should only add a few lines of code.
  • I personally feel that the spinning rings make the preview look good at the expense of very little screen space, but can remove them if others don't feel the same.

Please let me know if I've misunderstood your point, or you have another suggestion.
Thanks for having a look at the PR.

Copy link
Contributor

@scottanderson scottanderson left a comment

Choose a reason for hiding this comment

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

@Dimitrije-V sorry I thought this was for the ticket in the current release related to showing SAM protection radius. Please come talk to us in discord about this feature.

@scottanderson
Copy link
Contributor

This feature is similar to #1157 although they are a little bit different in what information they provide.

@iQiexie
Copy link

iQiexie commented Aug 20, 2025

i agree with @Dimitrije-V. Nuke preview feature really removes the depth of the game a little

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

🧹 Nitpick comments (8)
src/client/graphics/layers/NukePreview.ts (8)

49-56: Make MIRV seed deterministic per (type, anchor, owner) as designed

Right now _npSeed changes only with (type, anchor). The MIRV target set is re-generated when owner changes, but it still uses the old seed. To fully match “deterministic per (type, anchor, owner)”, include owner in the seed signature when MIRV is active.

-    const sig = `${p.nukeType}|${anchor.x}|${anchor.y}`;
+    const sig =
+      p.nukeType === UnitType.MIRV
+        ? (() => {
+            const o = this.game.owner(this.game.ref(anchor.x, anchor.y));
+            const ownerKey = o.isPlayer() ? (o as any).id() : "TN";
+            return `${p.nukeType}|${anchor.x}|${anchor.y}|${ownerKey}`;
+          })()
+        : `${p.nukeType}|${anchor.x}|${anchor.y}`;
     if (this._npSig !== sig) {
       this._npSig = sig;
       this._npSeed = this.game.ticks();
     }

8-10: Upgrade helper to a type guard for better narrowing

Turn isNukeType into a type guard so callers get narrowed types (clean, idiomatic TS).

-export const isNukeType = (t: UnitType) =>
-  t === UnitType.AtomBomb || t === UnitType.HydrogenBomb || t === UnitType.MIRV;
+export const isNukeType = (
+  t: UnitType,
+): t is Exclude<NukeType, UnitType.MIRVWarhead> =>
+  t === UnitType.AtomBomb || t === UnitType.HydrogenBomb || t === UnitType.MIRV;

97-119: Reduce per-frame work in probabilistic band (cache tiles per signature)

This double loop does thousands of worldToScreenCoordinates calls and fillRects every frame. Since the band is stable per (type, anchor, seed), precompute the list of band tiles once per _npSig and reuse during render.

Happy to sketch a small cache like _bandFill: Array<{px:number;py:number;size:number}> keyed by _npSig.


183-185: Compare owner by ID, not by object identity

owner !== anchorOwner may fail even for the same player if different object instances are returned. Compare stable IDs (or small IDs).

-        const owner = this.game.owner(tile);
-        if (owner !== anchorOwner) continue;
+        const owner = this.game.owner(tile);
+        const anchorOwnerId = anchorOwner.isPlayer() ? (anchorOwner as any).id() : -1;
+        const ownerId = owner.isPlayer() ? (owner as any).id() : -1;
+        if (ownerId !== anchorOwnerId) continue;

146-150: Avoid magic numbers: derive MIRV constants from config or shared constants

Hardcoding MIRV_RANGE = 1500 and MIN_SPACING = 25 may drift from server/game logic. Prefer reading from config, or import a shared constant (e.g., MirvExecution.mirvRange, proximity spacing).

Example:

  • Read mirvRange from config.
  • Export a single source of truth for proximity/spacings.

112-117: Pixel snapping for crisper rectangles

When tileStep > 1 or at fractional scales, fillRect at fractional px/py can blur. Snap to device pixels.

-          const px = pt.x - rect.left;
-          const py = pt.y - rect.top;
+          const px = Math.round(pt.x - rect.left) + 0.5;
+          const py = Math.round(pt.y - rect.top) + 0.5;

Note: also consider rounding size to avoid gaps.


237-255: Draw once, fill and stroke the same path

You create two arcs for the same circle. Use one path, fill then stroke for a small perf win.

-    ctx.beginPath();
-    ctx.setLineDash([]);
-    ctx.lineWidth = opts.strokeWidth;
-    ctx.strokeStyle = opts.stroke;
-    ctx.arc(cx, cy, r, 0, Math.PI * 2);
-    ctx.stroke();
-
-    ctx.beginPath();
-    ctx.fillStyle = opts.fill;
-    ctx.arc(cx, cy, r, 0, Math.PI * 2);
-    ctx.fill();
+    ctx.beginPath();
+    ctx.setLineDash([]);
+    ctx.arc(cx, cy, r, 0, Math.PI * 2);
+    ctx.fillStyle = opts.fill;
+    ctx.fill();
+    ctx.lineWidth = opts.strokeWidth;
+    ctx.strokeStyle = opts.stroke;
+    ctx.stroke();

141-145: Consistent visual padding for outer boundary

The single-blast safety line uses a padding based on tileStep; MIRV uses a fixed -1. Consider harmonizing the padding logic for visual consistency at different zoom levels.

Also applies to: 221-229

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fe47720 and 394beaa.

📒 Files selected for processing (4)
  • src/client/graphics/layers/NukePreview.ts (1 hunks)
  • src/client/graphics/layers/PlayerActionHandler.ts (3 hunks)
  • src/client/graphics/layers/RadialMenu.ts (6 hunks)
  • src/client/graphics/layers/RadialMenuElements.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/client/graphics/layers/PlayerActionHandler.ts
  • src/client/graphics/layers/RadialMenuElements.ts
  • src/client/graphics/layers/RadialMenu.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/client/graphics/layers/NukePreview.ts (6)
src/client/graphics/layers/Layer.ts (1)
  • Layer (1-7)
src/core/game/GameView.ts (1)
  • GameView (360-692)
src/client/graphics/TransformHandler.ts (1)
  • TransformHandler (15-296)
src/client/graphics/UIState.ts (1)
  • UIState (3-10)
src/core/StatsSchemas.ts (1)
  • NukeType (11-15)
src/core/game/Game.ts (1)
  • Cell (285-307)
🔇 Additional comments (2)
src/client/graphics/layers/NukePreview.ts (2)

31-43: Nice: deterministic PRNG and small, typed helpers

The h32 + rand01 approach is clean and deterministic, with unsigned return. Good foundation for stable previews.


65-128: Overall: solid composition, clear separation of MIRV vs single-blast, correct transform usage

Layer stays untransformed, converts world→screen precisely, and restores context. Readability is good and sticks to simple TypeScript.

Also applies to: 130-234

Comment on lines +57 to +63
if (p.nukeType === "MIRV") {
this.renderMirvPreview(ctx, anchor.x, anchor.y, seed);
return;
}

this.renderSingleBlastPreview(ctx, anchor.x, anchor.y, p.nukeType as NukeType, seed);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Bug: MIRV type compared to string instead of enum

This branch will never trigger if UnitType is an enum (very likely), so MIRV previews won’t render. Compare against UnitType.MIRV.

-    if (p.nukeType === "MIRV") {
+    if (p.nukeType === UnitType.MIRV) {
       this.renderMirvPreview(ctx, anchor.x, anchor.y, seed);
       return;
     }

Run to find any other string comparisons:


🏁 Script executed:

#!/bin/bash
rg -nP -C2 '===\s*["'\''](?:MIRV|AtomBomb|HydrogenBomb)["'\'']' src -g '!**/node_modules/**'

Length of output: 459


Use the UnitType.MIRV enum constant instead of the string literal

The check p.nukeType === "MIRV" will never succeed when UnitType is an enum (whether numeric or string-backed). Swap it for the enum value to ensure the MIRV preview renders correctly.

• File: src/client/graphics/layers/NukePreview.ts
Line: 57
• Diff to apply:

-    if (p.nukeType === "MIRV") {
+    if (p.nukeType === UnitType.MIRV) {
       this.renderMirvPreview(ctx, anchor.x, anchor.y, seed);
       return;
     }

• I searched for any other literal comparisons of MIRV, AtomBomb, or HydrogenBomb via:

rg -nP -C2 '===\s*["'"'"'](?:MIRV|AtomBomb|HydrogenBomb)["'"'"']' src -g '!**/node_modules/**'

and found no additional instances in src.

🤖 Prompt for AI Agents
In src/client/graphics/layers/NukePreview.ts around lines 57 to 63, the check
currently compares p.nukeType to the string literal "MIRV", which will never
match when UnitType is an enum; replace the string literal with the enum
constant UnitType.MIRV, ensure UnitType is imported in this file (add an import
if missing), and keep the rest of the conditional flow the same so
renderMirvPreview is called when p.nukeType === UnitType.MIRV.

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.

Preview where nukes will hit
5 participants