Skip to content

UI overhaul: split/classic layouts, control panel, events, attacks…#3359

Open
hkio120 wants to merge 2 commits intoopenfrontio:mainfrom
hkio120:pr-ui-global
Open

UI overhaul: split/classic layouts, control panel, events, attacks…#3359
hkio120 wants to merge 2 commits intoopenfrontio:mainfrom
hkio120:pr-ui-global

Conversation

@hkio120
Copy link
Contributor

@hkio120 hkio120 commented Mar 5, 2026

Summary
This PR introduces a full in-game UI rework focused on layout flexibility, clearer information hierarchy, and improved readability during combat-heavy gameplay.
What’s New (vs main)

  1. Dual in-game layout system
    • Added a switchable in-game layout:
    ◦ Layout: Split
    ◦ Layout: Classic
    • Added persistent layout setting.
    • Updated panel placement logic (control panel, attacks display, side event panels) for each layout.
    Files:
    • index.html
    • src/client/graphics/GameRenderer.ts
  2. Independent control panel style switch
    • Added a second toggle to switch control panel style independently of layout:
    ◦ Panel: Split
    ◦ Panel: Classic
    • Added persistent panel-style setting.
    • Enables mixed combinations (e.g. classic layout + split panel).
    Files:
    • index.html
    • src/client/graphics/layers/ControlPanel.ts
  3. Control panel redesign and info flow
    • Reworked control panel rendering for both styles.
    • Improved spacing/alignment for troops, attack ratio, gold, and economy info.
    • Added/adjusted gold-per-minute visibility where needed by mode.
    Files:
    • src/client/graphics/layers/ControlPanel.ts
  4. Unit display integration
    • Better integration of unit display with panel layout.
    • Size/spacing refinements for unit icons and fit behavior.
    Files:
    • src/client/graphics/layers/UnitDisplay.ts
    • index.html
  5. Events/chat panel restructuring
    • Added mode-based event display routing (trades, right, alliance_requests, etc.).
    • Improved filtering to reduce noisy messages.
    • Separated alliance request flow from general event stream where relevant.
    Files:
    • src/client/graphics/layers/EventsDisplay.ts
    • index.html
  6. Attacks display compaction and positioning
    • Made attacks display more compact/readable.
    • Improved vertical positioning relative to control panel, especially in classic layout.
    Files:
    • src/client/graphics/layers/AttacksDisplay.ts
    • index.html
  7. Mini hover
    • Added mini-hover overlay toggle and behavior refinements.
    Files:
    • src/client/graphics/layers/PlayerInfoOverlay.ts
    • index.html
  8. Localization updates
    • Added missing i18n entries used by new/updated event messages (including conquest gold messages) to avoid raw key rendering.
    Files:
    • resources/lang/en.json
    • resources/lang/fr.json
    Why
    These changes improve:
    • player customization of UI setup,
    • combat readability,
    • event signal-to-noise ratio,
    • consistency across in-game overlays.
    Testing
    Manual in-game verification performed for:
    • all Layout x Panel combinations,
    • control panel / attacks display alignment,
    • events panel routing and filtering,
    • mini-hover toggle and rendering,
    • translation rendering for updated event messages.

I DIDNT TESTED ON MOBILE !

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

HulKiora

@hkio120 hkio120 requested review from a team as code owners March 5, 2026 21:06
@hkio120 hkio120 changed the title UI overhaul: split/classic layouts, control panel, events, attacks, a… UI overhaul: split/classic layouts, control panel, events, attacks… Mar 5, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

Walkthrough

Adds a configurable Classic vs Split in-game layout, new DOM panels and persistent UI toggles, expanded localization, and multiple graphics-layer changes (events mode filtering, attacks anchors, control panel income/regen logic, hover overlays), plus related renderer and configuration adjustments.

Changes

Cohort / File(s) Summary
Layout & DOM
index.html
Adds alternate "layout-classic" structure, new panels (layout-trades-panel, layout-right-column, layout-attacks-container, layout-control-container), persistent control bar buttons, and client script to persist/apply layout, mini-hover, and control-panel-style via localStorage.
Localization
resources/lang/en.json, resources/lang/fr.json
Large addition of new UI keys, modals, overlays, event labels, auth/spawn text; removal/renames of some keys; French gains two conquest-related messages.
Renderer Core
src/client/graphics/GameRenderer.ts
Initializes and wires multiple EventsDisplay instances, spreads them into rendering layers, and assigns uiState to PlayerInfoOverlay.
Attacks Display
src/client/graphics/layers/AttacksDisplay.ts
Adds TransformHandler support and outgoing-attack anchor/angle resolution, renders outgoing markers, reorganizes incoming/outgoing blocks into responsive grid, and updates sizing/styling.
Control Panel
src/client/graphics/layers/ControlPanel.ts
Introduces gold-per-minute rolling window, optimal-regeneration computation, pointer-based attack-ratio slider, classic vs standard render branches, localStorage persistence for attack ratio, and many UI/behavior changes. (public: setVisibile signature updated)
Events Display
src/client/graphics/layers/EventsDisplay.ts
Adds mode property (all/trades/alerts/social/right/alliance_requests), mode-aware filtering, extends GameEvent with unitType and messageKey, and adds alliance-requests UI and mode-based rendering.
Player Info Overlay
src/client/graphics/layers/PlayerInfoOverlay.ts
Adds uiState property, hover coordinate tracking, compact hover overlay, troop bar coloring based on optimal regen, attack/troop preview logic, and collapsible detail rendering.
Unit Display
src/client/graphics/layers/UnitDisplay.ts
Introduces shared hover tooltip for units (type, hotkey, description, cost), hover tracking fields, layout tweaks, and icon sizing adjustments.
Configuration
src/core/configuration/DefaultConfig.ts
Refines attack speed/bonus formulas, adds attackerTilesOwned caching, singleplayer empty-land discount, and applies an attackSpeedMultiplier.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

Buttons toggle left and right,
Gold and regen hum through night,
Hover whispers unit lore,
Events filter, anchors soar,
LocalStorage keeps your light ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: a comprehensive UI overhaul introducing split/classic layout options and redesigned control panel, events, and attacks displays.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering all major components (layouts, control panel, unit display, events, attacks, mini-hover, and localization) with clear objectives and testing notes.

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

Important

Merge conflicts detected

  • Resolve merge conflict in branch pr-ui-global
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

You can enable review details to help with troubleshooting, context usage and more.

Enable the reviews.review_details setting to include review details such as the model used, the time taken for each step and more in the review comments.

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

Caution

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

⚠️ Outside diff range comments (1)
resources/lang/en.json (1)

8-25: ⚠️ Potential issue | 🟠 Major

Restore removed common keys still used by UI.

common.paste and common.enabled are still referenced in join-lobby flows, so their removal from the common block causes missing translation output at runtime.

🩹 Suggested key restoration
   "common": {
     "close": "Close",
     "copy": "Copy",
+    "paste": "Paste",
     "back": "Back",
     "available": "Available",
@@
     "target_dead_note": "You can't send resources to an eliminated player.",
     "none": "None",
+    "enabled": "Enabled",
     "copied": "Copied!",
     "click_to_copy": "Click to copy"
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/lang/en.json` around lines 8 - 25, Restore the missing translation
keys under the common block by re-adding "paste" and "enabled" (i.e.,
common.paste and common.enabled) with appropriate English strings (for example
"Paste" and "Enabled") so join-lobby flows can resolve translations; update the
resources/lang/en.json common object to include these keys alongside existing
entries and ensure JSON syntax remains valid (commas, quoting) after insertion.
🧹 Nitpick comments (4)
src/client/graphics/layers/UnitDisplay.ts (1)

281-287: Remove dead template output and duplicate class token.

Line 281 renders an empty template (hovered ? html\` : null) with no visible effect, and rounded-sm` is duplicated in the class string on Lines 285 and 287.

♻️ Proposed cleanup
-        ${hovered ? html`` : null}
         <div
           class="${this.canBuild(unitType)
             ? ""
-            : "opacity-40"} border border-slate-500 rounded-sm pr-1 pb-0.5 flex items-center gap-1 cursor-pointer
+            : "opacity-40"} border border-slate-500 pr-1 pb-0.5 flex items-center gap-1 cursor-pointer
              ${selected ? "hover:bg-gray-400/10" : "hover:bg-gray-800"}
              rounded-sm text-white text-xs ${selected ? "bg-slate-400/20" : ""}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/UnitDisplay.ts` around lines 281 - 287, In
UnitDisplay.ts (in the render template where hovered, selected, and
canBuild(unitType) are used) remove the dead template expression "hovered ?
html`` : null" and deduplicate the "rounded-sm" class token in the big class
string (used alongside the conditional from canBuild(unitType) and selected) so
the markup no longer outputs an empty template and the class attribute contains
"rounded-sm" only once; keep the rest of the conditional class logic
(opacity-40, hover:bg-*, bg-slate-400/20, text-white, text-xs, border classes)
intact.
src/client/graphics/layers/PlayerInfoOverlay.ts (1)

113-123: Gate reactive cursor updates to when compact hover is actually used.

Lines 114-115 update reactive state on every mouse move before the throttle branch, which can cause unnecessary re-renders on a very hot path.

⚡ Suggested gating
   private onMouseEvent(event: MouseMoveEvent) {
-    this.hoverScreenX = event.x;
-    this.hoverScreenY = event.y;
+    const shouldTrackPointer =
+      this._isInfoVisible &&
+      this.player !== null &&
+      this.unit === null &&
+      this.isDesktopViewport() &&
+      this.isMiniHoverOverlayEnabled();
+    if (shouldTrackPointer) {
+      this.hoverScreenX = event.x;
+      this.hoverScreenY = event.y;
+    }

     const now = Date.now();
     if (now - this.lastMouseUpdate < 100) {
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/PlayerInfoOverlay.ts` around lines 113 - 123, The
code updates hoverScreenX/hoverScreenY every mouse move before throttling;
change onMouseEvent so it first checks whether compact hover mode is active
(e.g. a flag/method like this.isCompactHoverEnabled or this.compactHoverEnabled)
and only then performs the reactive assignments and throttling logic; keep the
Date.now() throttle using lastMouseUpdate and call this.maybeShow(x,y) as
before, but move hoverScreenX/hoverScreenY assignments to occur inside the
compact-hover branch after the throttle check so non-compact cursor movement
does not trigger reactive updates.
src/client/graphics/layers/ControlPanel.ts (2)

624-644: Extract touch drag overlay into a helper method.

This block (lines 624-644) is nearly identical to lines 732-752 in render(). Extracting it to a private method like renderTouchDragOverlay() would reduce duplication and simplify both render paths.

♻️ Proposed extraction
+  private renderTouchDragOverlay() {
+    if (!this._touchDragging) return "";
+    return html`
+      <div
+        class="absolute bottom-full right-0 flex flex-col items-center pointer-events-auto z-[10000] bg-gray-800/70 backdrop-blur-xs rounded-tl-lg sm:rounded-lg p-2 w-12"
+        style="height: 50vh;"
+        `@touchstart`=${(e: TouchEvent) => this.handleBarTouch(e)}
+      >
+        <span class="text-red-400 text-sm font-bold mb-1" translate="no"
+          >${(this.attackRatio * 100).toFixed(0)}%</span
+        >
+        <div
+          class="attack-drag-bar flex-1 w-3 bg-white/20 rounded-full relative overflow-hidden"
+        >
+          <div
+            class="absolute bottom-0 w-full bg-red-500 rounded-full"
+            style="height: ${this.attackRatio * 100}%"
+          ></div>
+        </div>
+      </div>
+    `;
+  }

Then replace both occurrences with ${this.renderTouchDragOverlay()}.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/ControlPanel.ts` around lines 624 - 644, The
touch-drag overlay HTML is duplicated in render(); extract it into a private
helper like renderTouchDragOverlay() that returns the same template and uses the
same bindings (_touchDragging, attackRatio, handleBarTouch), then replace both
duplicated inline blocks in render() with ${this.renderTouchDragOverlay()} so
both paths reuse the single method and remove the duplicated markup.

52-52: Unused state property regenPct.

This property is declared but never read or written anywhere in the file. Remove it or wire it up if it was intended for something.

🧹 Proposed fix
-  `@state`() private regenPct: number = 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/ControlPanel.ts` at line 52, The property
decorated as `@state`() private regenPct: number = 0 in the ControlPanel class is
unused; remove this unused state field or implement its intended usage. Locate
the ControlPanel class in ControlPanel.ts, delete the regenPct declaration if
not needed, or if it should reflect some UI state, connect it to the
corresponding update logic (e.g., update it inside methods that compute regen
percentage or bind it to the template/render function) and ensure any references
use this.regenPct consistently; also remove any unused imports triggered by this
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@index.html`:
- Around line 384-401: The three control buttons (ids layout-switch-button,
mini-hover-toggle-button, control-panel-style-button) currently hardcode English
labels in HTML and are later updated in JS (around the code that updates them at
~417-453); change both the static HTML text and all JS updates to use the app's
established i18n mechanism (the same translation function/utility used
elsewhere) and reference new translation keys (e.g., controls.layout.split,
controls.miniHover.on, controls.panel.split) instead of literal strings so the
labels render localized text for all locales; ensure keys are added to locale
files and the JS that toggles/changes labels calls the translation function
rather than setting raw English text.

In `@resources/lang/en.json`:
- Around line 869-871: The locale entries "slider_tooltip", "aria_slider", and
"capacity_note" use double-brace placeholders (e.g., "{{percent}}") which is
inconsistent with the project's single-brace convention; update those keys to
use single braces (e.g., "{percent}") and also change the other occurrence
referenced at line 877 to single-brace placeholders so all locale interpolations
use the same "{...}" format.

In `@resources/lang/fr.json`:
- Around line 721-722: Revert the direct edits to resources/lang/fr.json for the
keys "received_gold_from_conquest" and "conquered_no_gold" (remove those two
added/changed entries) and instead add these translation updates via the
dedicated translation workflow: create or include them in a translation PR
titled "mls" authored by Aotumori (or submit the source strings in en.json if
missing) so that only the dedicated translation PR updates non-English locale
files.

In `@src/client/graphics/layers/EventsDisplay.ts`:
- Around line 955-957: The static mode headers "Alliance Requests" and "Trade
Information" in the EventsDisplay component are hardcoded; replace them with the
component's i18n translation calls (use the same translate function used
elsewhere in this file, e.g., t(...) or i18n.t(...)) and reference new keys like
events.allianceRequests and events.tradeInformation so the headers are localized
consistently with the rest of the component (update both occurrences that render
those header divs).

---

Outside diff comments:
In `@resources/lang/en.json`:
- Around line 8-25: Restore the missing translation keys under the common block
by re-adding "paste" and "enabled" (i.e., common.paste and common.enabled) with
appropriate English strings (for example "Paste" and "Enabled") so join-lobby
flows can resolve translations; update the resources/lang/en.json common object
to include these keys alongside existing entries and ensure JSON syntax remains
valid (commas, quoting) after insertion.

---

Nitpick comments:
In `@src/client/graphics/layers/ControlPanel.ts`:
- Around line 624-644: The touch-drag overlay HTML is duplicated in render();
extract it into a private helper like renderTouchDragOverlay() that returns the
same template and uses the same bindings (_touchDragging, attackRatio,
handleBarTouch), then replace both duplicated inline blocks in render() with
${this.renderTouchDragOverlay()} so both paths reuse the single method and
remove the duplicated markup.
- Line 52: The property decorated as `@state`() private regenPct: number = 0 in
the ControlPanel class is unused; remove this unused state field or implement
its intended usage. Locate the ControlPanel class in ControlPanel.ts, delete the
regenPct declaration if not needed, or if it should reflect some UI state,
connect it to the corresponding update logic (e.g., update it inside methods
that compute regen percentage or bind it to the template/render function) and
ensure any references use this.regenPct consistently; also remove any unused
imports triggered by this change.

In `@src/client/graphics/layers/PlayerInfoOverlay.ts`:
- Around line 113-123: The code updates hoverScreenX/hoverScreenY every mouse
move before throttling; change onMouseEvent so it first checks whether compact
hover mode is active (e.g. a flag/method like this.isCompactHoverEnabled or
this.compactHoverEnabled) and only then performs the reactive assignments and
throttling logic; keep the Date.now() throttle using lastMouseUpdate and call
this.maybeShow(x,y) as before, but move hoverScreenX/hoverScreenY assignments to
occur inside the compact-hover branch after the throttle check so non-compact
cursor movement does not trigger reactive updates.

In `@src/client/graphics/layers/UnitDisplay.ts`:
- Around line 281-287: In UnitDisplay.ts (in the render template where hovered,
selected, and canBuild(unitType) are used) remove the dead template expression
"hovered ? html`` : null" and deduplicate the "rounded-sm" class token in the
big class string (used alongside the conditional from canBuild(unitType) and
selected) so the markup no longer outputs an empty template and the class
attribute contains "rounded-sm" only once; keep the rest of the conditional
class logic (opacity-40, hover:bg-*, bg-slate-400/20, text-white, text-xs,
border classes) intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6b53ab94-67d1-4b0d-bda1-55c5fd5c5a64

📥 Commits

Reviewing files that changed from the base of the PR and between b3c01d4 and 3c14267.

📒 Files selected for processing (9)
  • index.html
  • resources/lang/en.json
  • resources/lang/fr.json
  • src/client/graphics/GameRenderer.ts
  • src/client/graphics/layers/AttacksDisplay.ts
  • src/client/graphics/layers/ControlPanel.ts
  • src/client/graphics/layers/EventsDisplay.ts
  • src/client/graphics/layers/PlayerInfoOverlay.ts
  • src/client/graphics/layers/UnitDisplay.ts

Comment on lines +384 to +401
<button
id="layout-switch-button"
class="px-3 py-1.5 rounded-md bg-gray-900/80 border border-white/20 text-white text-xs lg:text-sm hover:bg-gray-800/90 transition-colors"
>
Layout: Split
</button>
<button
id="mini-hover-toggle-button"
class="px-3 py-1.5 rounded-md bg-gray-900/80 border border-white/20 text-white text-xs lg:text-sm hover:bg-gray-800/90 transition-colors"
>
Mini Hover: On
</button>
<button
id="control-panel-style-button"
class="px-3 py-1.5 rounded-md bg-gray-900/80 border border-white/20 text-white text-xs lg:text-sm hover:bg-gray-800/90 transition-colors"
>
Panel: Split
</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

New layout-control labels are not localized.

Texts set on Lines 384-401 and updated in JS on Lines 417-453 are hardcoded in English. In other locales, these controls will still show English strings. Please route them through translation keys like the rest of the UI.

Also applies to: 417-453

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.html` around lines 384 - 401, The three control buttons (ids
layout-switch-button, mini-hover-toggle-button, control-panel-style-button)
currently hardcode English labels in HTML and are later updated in JS (around
the code that updates them at ~417-453); change both the static HTML text and
all JS updates to use the app's established i18n mechanism (the same translation
function/utility used elsewhere) and reference new translation keys (e.g.,
controls.layout.split, controls.miniHover.on, controls.panel.split) instead of
literal strings so the labels render localized text for all locales; ensure keys
are added to locale files and the JS that toggles/changes labels calls the
translation function rather than setting raw English text.

Comment on lines +869 to +871
"slider_tooltip": "{{percent}}% • {{amount}}",
"aria_slider": "Troops slider",
"capacity_note": "Receiver can accept only {amount} right now."
"capacity_note": "Receiver can accept only {{amount}} right now."
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use consistent single-brace placeholders in tooltips.

Lines 869-871 and 877 use {{...}}, but existing locale placeholders use {...}. This inconsistency can break interpolation and show raw braces to users.

🩹 Placeholder consistency fix
-    "slider_tooltip": "{{percent}}% • {{amount}}",
+    "slider_tooltip": "{percent}% • {amount}",
@@
-    "capacity_note": "Receiver can accept only {{amount}} right now."
+    "capacity_note": "Receiver can accept only {amount} right now."
@@
-    "slider_tooltip": "{{percent}}% • {{amount}}"
+    "slider_tooltip": "{percent}% • {amount}"

Also applies to: 877-877

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/lang/en.json` around lines 869 - 871, The locale entries
"slider_tooltip", "aria_slider", and "capacity_note" use double-brace
placeholders (e.g., "{{percent}}") which is inconsistent with the project's
single-brace convention; update those keys to use single braces (e.g.,
"{percent}") and also change the other occurrence referenced at line 877 to
single-brace placeholders so all locale interpolations use the same "{...}"
format.

Comment on lines +721 to +722
"received_gold_from_conquest": "{gold} ors reçu en conquérant {name}",
"conquered_no_gold": "{name} a été conquis, mais il n'y avait aucun or à récupérer",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move fr.json updates to the dedicated translation flow.

Adding keys in resources/lang/fr.json on Lines 721-722 should be done in the dedicated translation PR process, not in a regular feature PR.

Based on learnings: In OpenFrontIO, translation files in resources/lang/*.json (except en.json) should not be updated in regular PRs. Only dedicated translation PRs titled "mls" and made by Aotumori should update non-English locale files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/lang/fr.json` around lines 721 - 722, Revert the direct edits to
resources/lang/fr.json for the keys "received_gold_from_conquest" and
"conquered_no_gold" (remove those two added/changed entries) and instead add
these translation updates via the dedicated translation workflow: create or
include them in a translation PR titled "mls" authored by Aotumori (or submit
the source strings in en.json if missing) so that only the dedicated translation
PR updates non-English locale files.

Comment on lines +955 to +957
<div class="text-[11px] uppercase tracking-wide text-gray-300 mb-1">
Alliance Requests
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Mode headers are hardcoded instead of translated.

Alliance Requests and Trade Information (Lines 955-957, 1041-1049) should use translation keys to match the rest of this component’s i18n behavior.

Also applies to: 1041-1049

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/EventsDisplay.ts` around lines 955 - 957, The
static mode headers "Alliance Requests" and "Trade Information" in the
EventsDisplay component are hardcoded; replace them with the component's i18n
translation calls (use the same translate function used elsewhere in this file,
e.g., t(...) or i18n.t(...)) and reference new keys like events.allianceRequests
and events.tradeInformation so the headers are localized consistently with the
rest of the component (update both occurrences that render those header divs).

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Mar 5, 2026
@evanpelle evanpelle mentioned this pull request Mar 6, 2026
4 tasks
evanpelle added a commit that referenced this pull request Mar 7, 2026
Relates to #2260

## Description:

Inspired by #3359

This PR centers the control panel and combines it with the units
display. The reasoning is that the control panel contains the most
critical info so it should be in the center of the screen. Combining it
with the units display reduces the number of UI components on screen.

Also made the attack ratio bar persistent on mobile

<img width="618" height="216" alt="Screenshot 2026-03-06 at 2 06 34 PM"
src="https://github.com/user-attachments/assets/34b041c1-d78b-46b5-a7ab-f2a44145a7e2"
/>


<img width="941" height="343" alt="Screenshot 2026-03-06 at 2 06 55 PM"
src="https://github.com/user-attachments/assets/1e3b026c-8eb2-407c-be38-0e71e1ae426c"
/>

<img width="562" height="228" alt="Screenshot 2026-03-06 at 4 11 20 PM"
src="https://github.com/user-attachments/assets/56eac49f-c8a4-4ac1-a60a-f1bcb2fad2d0"
/>

<img width="939" height="357" alt="Screenshot 2026-03-06 at 4 11 32 PM"
src="https://github.com/user-attachments/assets/eb5591d5-3cc2-4182-944b-3a4b0b76852a"
/>


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

evan

Co-authored-by: hkio120 <111693579+hkio120@users.noreply.github.com>
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 (3)
src/core/configuration/DefaultConfig.ts (1)

630-644: Caching and formula changes look correct.

Good optimization to cache attackerTilesOwned instead of calling attacker.numTilesOwned() multiple times. The math for both largeAttackBonus and largeAttackerSpeedBonus is sound:

  • largeAttackBonus decreases as attacker grows larger (penalizes large attackers)
  • largeAttackerSpeedBonus decreases as attackerSpeed grows (slows expansion for large attackers)
  • Division by attackerSpeed is safe since it's always >= 1

Consider extracting the threshold 100_000 into a named constant (like the existing DEFENSE_DEBUFF_MIDPOINT) for clarity and easier tuning.

🔧 Optional: Extract magic number to named constant
 const DEFENSE_DEBUFF_MIDPOINT = 150_000;
 const DEFENSE_DEBUFF_DECAY_RATE = Math.LN2 / 50000;
 const DEFAULT_SPAWN_IMMUNITY_TICKS = 5 * 10;
+const LARGE_ATTACKER_THRESHOLD = 100_000;

Then replace 100_000 references with LARGE_ATTACKER_THRESHOLD.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/configuration/DefaultConfig.ts` around lines 630 - 644, The code
repeats the magic number 100_000 when computing largeAttackBonus and
largeAttackerSpeedBonus; extract that value into a named constant (e.g.,
LARGE_ATTACKER_THRESHOLD) and replace the hardcoded 100_000 uses in the
attackerTilesOwned check and the attackerSpeed formula; update any related
documentation/comments and preferably colocate the constant near
DEFENSE_DEBUFF_MIDPOINT so tuning is easier, while leaving the logic in the
functions/variables attacker.numTilesOwned(), attackerTilesOwned,
largeAttackBonus, and largeAttackerSpeedBonus unchanged.
src/client/graphics/layers/ControlPanel.ts (1)

145-148: Typo in method name: setVisibile should be setVisible.

The method name has a typo (double 'i'). Consider renaming to setVisible for clarity, though this would require updating callers.

🔤 Suggested fix
-  setVisibile(isVisible: boolean) {
+  setVisible(isVisible: boolean) {
     this._isVisible = isVisible;
     this.requestUpdate();
   }

And update the callers at lines 101 and 106.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/ControlPanel.ts` around lines 145 - 148, Rename
the misnamed method setVisibile to setVisible in the ControlPanel class and
update all its callers to the new name; inside the method keep the same behavior
(assign this._isVisible = isVisible; this.requestUpdate();) and ensure any
imports/usages referencing setVisibile are changed to setVisible so callers
continue to compile and the visibility property (_isVisible) and requestUpdate
usage remain intact.
src/client/graphics/layers/PlayerInfoOverlay.ts (1)

503-557: Duplicated troop optimization logic between PlayerInfoOverlay and ControlPanel.

The methods predictedTroopIncreaseRate, getOptimalRegenPoint, lerpColor, and troopFillColor are nearly identical in both files. Consider extracting these to a shared utility module to avoid drift and reduce maintenance burden.

Also note: ControlPanel caches the optimal regen point (_cachedOptimalMaxTroops, etc.), but PlayerInfoOverlay recalculates on every call. Since maxTroops changes infrequently, caching would improve performance here too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/PlayerInfoOverlay.ts` around lines 503 - 557, The
four nearly-identical functions predictedTroopIncreaseRate,
getOptimalRegenPoint, lerpColor, and troopFillColor should be extracted into a
shared utility (e.g., troopUtils) and PlayerInfoOverlay should call those
helpers instead of duplicating logic; move the implementations to the new module
and import them here, and add a caching mechanism similar to ControlPanel's
(_cachedOptimalMaxTroops / cached value + lastMaxTroops) so getOptimalRegenPoint
is not recomputed every render when maxTroops is unchanged. Ensure you preserve
the same function signatures (predictedTroopIncreaseRate(troops,maxTroops),
getOptimalRegenPoint(maxTroops), lerpColor(from,to,t),
troopFillColor(troops,maxTroops)) and update all references in PlayerInfoOverlay
to use the shared exports.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client/graphics/layers/AttacksDisplay.ts`:
- Around line 161-189: resolveOutgoingAttackAnchor starts an async
attackAveragePosition lookup and may write anchors after the attack has been
removed by refreshOutgoingAttackAnchors, causing stale entries; after the await
(before setting outgoingAttackAnchors) check that the attack is still active
(e.g. via the same source refreshOutgoingAttackAnchors uses—call/game lookup
like this.game.attackById(attack.id) or this.attacks.has(attack.id)) and only
write the anchor if that check passes; keep the existing
pendingOutgoingAnchorLookups removal and requestUpdate in finally.

---

Nitpick comments:
In `@src/client/graphics/layers/ControlPanel.ts`:
- Around line 145-148: Rename the misnamed method setVisibile to setVisible in
the ControlPanel class and update all its callers to the new name; inside the
method keep the same behavior (assign this._isVisible = isVisible;
this.requestUpdate();) and ensure any imports/usages referencing setVisibile are
changed to setVisible so callers continue to compile and the visibility property
(_isVisible) and requestUpdate usage remain intact.

In `@src/client/graphics/layers/PlayerInfoOverlay.ts`:
- Around line 503-557: The four nearly-identical functions
predictedTroopIncreaseRate, getOptimalRegenPoint, lerpColor, and troopFillColor
should be extracted into a shared utility (e.g., troopUtils) and
PlayerInfoOverlay should call those helpers instead of duplicating logic; move
the implementations to the new module and import them here, and add a caching
mechanism similar to ControlPanel's (_cachedOptimalMaxTroops / cached value +
lastMaxTroops) so getOptimalRegenPoint is not recomputed every render when
maxTroops is unchanged. Ensure you preserve the same function signatures
(predictedTroopIncreaseRate(troops,maxTroops), getOptimalRegenPoint(maxTroops),
lerpColor(from,to,t), troopFillColor(troops,maxTroops)) and update all
references in PlayerInfoOverlay to use the shared exports.

In `@src/core/configuration/DefaultConfig.ts`:
- Around line 630-644: The code repeats the magic number 100_000 when computing
largeAttackBonus and largeAttackerSpeedBonus; extract that value into a named
constant (e.g., LARGE_ATTACKER_THRESHOLD) and replace the hardcoded 100_000 uses
in the attackerTilesOwned check and the attackerSpeed formula; update any
related documentation/comments and preferably colocate the constant near
DEFENSE_DEBUFF_MIDPOINT so tuning is easier, while leaving the logic in the
functions/variables attacker.numTilesOwned(), attackerTilesOwned,
largeAttackBonus, and largeAttackerSpeedBonus unchanged.
🪄 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: efb17300-bd4f-40cd-939f-3587f7ca5de4

📥 Commits

Reviewing files that changed from the base of the PR and between 3c14267 and 2979e89.

📒 Files selected for processing (5)
  • src/client/graphics/GameRenderer.ts
  • src/client/graphics/layers/AttacksDisplay.ts
  • src/client/graphics/layers/ControlPanel.ts
  • src/client/graphics/layers/PlayerInfoOverlay.ts
  • src/core/configuration/DefaultConfig.ts

Comment on lines +161 to +189
private async resolveOutgoingAttackAnchor(attack: AttackUpdate) {
this.pendingOutgoingAnchorLookups.add(attack.id);
try {
const attacker = this.game.playerBySmallID(attack.attackerID);
if (!(attacker instanceof PlayerView)) {
this.outgoingAttackAnchors.set(attack.id, null);
return;
}

const averagePosition = await attacker.attackAveragePosition(
attack.attackerID,
attack.id,
);

if (averagePosition === null) {
this.outgoingAttackAnchors.set(attack.id, null);
} else {
this.outgoingAttackAnchors.set(attack.id, {
x: averagePosition.x,
y: averagePosition.y,
});
}
} catch {
this.outgoingAttackAnchors.set(attack.id, null);
} finally {
this.pendingOutgoingAnchorLookups.delete(attack.id);
this.requestUpdate();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Async anchor resolution can leave stale data if attacks end mid-flight.

The resolveOutgoingAttackAnchor method starts an async lookup, but if the attack ends before attackAveragePosition returns, the anchor is still written to outgoingAttackAnchors. The cleanup in refreshOutgoingAttackAnchors runs before the async call completes, so the next tick will re-add the stale entry to the map.

Consider checking if the attack is still active before writing to the map:

🛠️ Suggested fix
       const averagePosition = await attacker.attackAveragePosition(
         attack.attackerID,
         attack.id,
       );

+      // Check if attack still exists before storing anchor
+      const stillActive = this.outgoingAttacks.some((a) => a.id === attack.id);
+      if (!stillActive) {
+        return;
+      }
+
       if (averagePosition === null) {
         this.outgoingAttackAnchors.set(attack.id, null);
       } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/AttacksDisplay.ts` around lines 161 - 189,
resolveOutgoingAttackAnchor starts an async attackAveragePosition lookup and may
write anchors after the attack has been removed by refreshOutgoingAttackAnchors,
causing stale entries; after the await (before setting outgoingAttackAnchors)
check that the attack is still active (e.g. via the same source
refreshOutgoingAttackAnchors uses—call/game lookup like
this.game.attackById(attack.id) or this.attacks.has(attack.id)) and only write
the anchor if that check passes; keep the existing pendingOutgoingAnchorLookups
removal and requestUpdate in finally.

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.

1 participant