Skip to content

Conversation

@evanpelle
Copy link
Collaborator

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

Description:

Describe the PR.

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:

DISCORD_USERNAME

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Walkthrough

Adds a new AttacksDisplay component and integrates it into the renderer; removes attack/boat UI and related logic from EventsDisplay; updates markup to mount the new element and constrain the bottom-right panel width.

Changes

Cohort / File(s) Summary
Markup + renderer
index.html, src/client/graphics/GameRenderer.ts
Inserted <attacks-display> in the bottom-right panel and added a large-screen max-width class. GameRenderer imports and instantiates AttacksDisplay, validates its type, wires eventBus, game, and uiState, and registers it as a UI layer.
New attacks UI layer
src/client/graphics/layers/AttacksDisplay.ts
New LitElement AttacksDisplay implementing Layer: tracks visibility/activity, incoming/outgoing attacks and boats, resolves unit views each tick, filters bots, derives attack categories, renders interactive sections (warn, retaliate, cancel, go-to), emits intent/navigation events, and caches generated boat sprites.
EventsDisplay pruning
src/client/graphics/layers/EventsDisplay.ts
Removed attack/boat state properties, render helpers, event handlers, and related imports. Simplified EventsDisplay rendering flow to exclude combat/boat UI rows and interactions.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer as GameRenderer
    participant UI as AttacksDisplay
    participant EB as EventBus
    participant Game as GameState
    participant UIState as UIState

    Renderer->>UI: instantiate & attach (eventBus, game, uiState)
    EB-->>UI: sends event updates (tick, UnitIncoming, attack events)
    UI->>Game: query units/players (resolve unit views)
    UI->>UIState: read current player / spawn phase
    UI->>EB: emit intents (CancelAttack, CancelBoat, SendAttackIntent, GoTo events)
    EB->>Game: forward intents for processing
    Game-->>Renderer: state changes -> re-render pipeline
    Renderer->>UI: tick/render cycle continues
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

New lines for clashes now neatly display,
Boats, warnings, counters find their own bay.
Events grown slimmer, battles clearer to see,
Click, steer, or cancel — combat set free. ⚔️🛶✨

🚥 Pre-merge checks | ❌ 3
❌ Failed checks (1 warning, 2 inconclusive)
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.
Title check ❓ Inconclusive The title 'attack panel' is vague and generic. While it relates to the attack-related changes, it doesn't clearly convey what was added or changed—whether it's a new feature, refactor, or bug fix. Use a more descriptive title like 'Extract attack panel to dedicated AttacksDisplay component' or 'Add AttacksDisplay layer for rendering attack information' to clarify the main change.
Description check ❓ Inconclusive The description is an unmodified PR template with placeholders and checkboxes, containing no meaningful information about the changeset or implementation details. Replace the template with a clear description of the new AttacksDisplay component, why it was extracted from EventsDisplay, and what functionality it provides.

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

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Reviews will stop working after February 8, 2026 if the new IP is not added to your allowlist.


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

🤖 Fix all issues with AI agents
In `@src/client/graphics/GameRenderer.ts`:
- Around line 127-136: The code logs when the "attacks-display" element is
missing but then dereferences attacksDisplay anyway; update the block around
attacksDisplay in GameRenderer.ts to guard before use: after querying const
attacksDisplay = ... check if (!(attacksDisplay instanceof AttacksDisplay)) then
handle the missing element (e.g., log error and return or skip assigning) so the
subsequent lines attacksDisplay.eventBus = eventBus, attacksDisplay.game = game,
and attacksDisplay.uiState = uiState only run when attacksDisplay is a valid
AttacksDisplay instance.

In `@src/client/graphics/layers/AttacksDisplay.ts`:
- Around line 229-409: Replace the Tailwind utility backdrop-blur-sm with
backdrop-blur-xs in all attack/boat display renderers in this file: update the
blur class in the incoming attacks block (inside the method that renders
incoming attacks), renderOutgoingAttacks, renderOutgoingLandAttacks, and
renderBoats so each div using "bg-gray-800/70 backdrop-blur-sm ..." becomes
"bg-gray-800/70 backdrop-blur-xs ..."; search for backdrop-blur-sm to find each
occurrence and ensure className strings passed to renderButton (and the boat
div) are updated consistently.
🧹 Nitpick comments (1)
src/client/graphics/layers/AttacksDisplay.ts (1)

49-109: Add a tick interval to avoid per-frame UI work.

This layer updates every tick and always calls requestUpdate(). Consider adding getTickIntervalMs() so the renderer can throttle it on wall-clock time.

🔧 Suggested change
  tick() {
    this.active = true;
    ...
    this.requestUpdate();
  }

+  getTickIntervalMs(): number {
+    return 200;
+  }

Based on learnings: In this TypeScript game codebase, UI/layer updates should be wall-clock throttled (independent of simulation speed). Do not use tick-modulus gating like if (game.ticks()%N===0) inside layer.tick() methods. Instead, layers should expose a getTickIntervalMs() method and let the renderer schedule ticks based on wall-clock time.

Comment on lines +127 to +136
const attacksDisplay = document.querySelector(
"attacks-display",
) as AttacksDisplay;
if (!(attacksDisplay instanceof AttacksDisplay)) {
console.error("attacks display not found");
}
attacksDisplay.eventBus = eventBus;
attacksDisplay.game = game;
attacksDisplay.uiState = uiState;

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

Guard against missing attacks-display before use.

Right now it logs an error but still dereferences the element. That can throw if the DOM tag is missing.

🛡️ Suggested fix
-  const attacksDisplay = document.querySelector(
-    "attacks-display",
-  ) as AttacksDisplay;
-  if (!(attacksDisplay instanceof AttacksDisplay)) {
-    console.error("attacks display not found");
-  }
-  attacksDisplay.eventBus = eventBus;
-  attacksDisplay.game = game;
-  attacksDisplay.uiState = uiState;
+  const attacksDisplay = document.querySelector(
+    "attacks-display",
+  ) as AttacksDisplay | null;
+  if (!(attacksDisplay instanceof AttacksDisplay)) {
+    console.error("attacks display not found");
+  } else {
+    attacksDisplay.eventBus = eventBus;
+    attacksDisplay.game = game;
+    attacksDisplay.uiState = uiState;
+  }
🤖 Prompt for AI Agents
In `@src/client/graphics/GameRenderer.ts` around lines 127 - 136, The code logs
when the "attacks-display" element is missing but then dereferences
attacksDisplay anyway; update the block around attacksDisplay in GameRenderer.ts
to guard before use: after querying const attacksDisplay = ... check if
(!(attacksDisplay instanceof AttacksDisplay)) then handle the missing element
(e.g., log error and return or skip assigning) so the subsequent lines
attacksDisplay.eventBus = eventBus, attacksDisplay.game = game, and
attacksDisplay.uiState = uiState only run when attacksDisplay is a valid
AttacksDisplay instance.

Comment on lines +229 to +409
<div class="flex flex-col gap-1">
${this.incomingAttacks.map(
(attack) => html`
<div
class="flex items-center gap-1 w-full bg-gray-800/70 backdrop-blur-sm rounded-lg px-2 py-1"
>
${this.renderButton({
content: html`<img
src="${swordIcon}"
class="h-4 w-4 inline-block"
style="filter: invert(1)"
/>
<span class="inline-block min-w-[3rem] text-right"
>${renderTroops(attack.troops)}</span
>
${(
this.game.playerBySmallID(attack.attackerID) as PlayerView
)?.name()}
${attack.retreating
? `(${translateText("events_display.retreating")}...)`
: ""} `,
onClick: () => this.attackWarningOnClick(attack),
className:
"text-left text-red-400 inline-flex items-center gap-1",
translate: false,
})}
${!attack.retreating
? this.renderButton({
content: html`<img
src="${swordIcon}"
class="h-4 w-4"
style="filter: brightness(0) saturate(100%) invert(27%) sepia(91%) saturate(4551%) hue-rotate(348deg) brightness(89%) contrast(97%)"
/>`,
onClick: () => this.handleRetaliate(attack),
className:
"ml-auto inline-flex items-center justify-center cursor-pointer bg-red-900/50 hover:bg-red-800/70 rounded px-1.5 py-1 border border-red-700/50",
translate: false,
})
: ""}
</div>
`,
)}
</div>
`;
}

private renderOutgoingAttacks() {
if (this.outgoingAttacks.length === 0) return html``;

return html`
<div class="flex flex-col gap-1">
${this.outgoingAttacks.map(
(attack) => html`
<div
class="flex items-center gap-1 w-full bg-gray-800/70 backdrop-blur-sm rounded-lg px-2 py-1"
>
${this.renderButton({
content: html`<img
src="${swordIcon}"
class="h-4 w-4 inline-block"
style="filter: invert(1)"
/>
<span class="inline-block min-w-[3rem] text-right"
>${renderTroops(attack.troops)}</span
>
${(
this.game.playerBySmallID(attack.targetID) as PlayerView
)?.name()} `,
onClick: async () => this.attackWarningOnClick(attack),
className:
"text-left text-blue-400 inline-flex items-center gap-1",
translate: false,
})}
${!attack.retreating
? this.renderButton({
content: "❌",
onClick: () => this.emitCancelAttackIntent(attack.id),
className: "ml-auto text-left shrink-0",
disabled: attack.retreating,
})
: html`<span class="ml-auto shrink-0 text-blue-400"
>(${translateText("events_display.retreating")}...)</span
>`}
</div>
`,
)}
</div>
`;
}

private renderOutgoingLandAttacks() {
if (this.outgoingLandAttacks.length === 0) return html``;

return html`
<div class="flex flex-col gap-1">
${this.outgoingLandAttacks.map(
(landAttack) => html`
<div
class="flex items-center gap-1 w-full bg-gray-800/70 backdrop-blur-sm rounded-lg px-2 py-1"
>
${this.renderButton({
content: html`<img
src="${swordIcon}"
class="h-4 w-4 inline-block"
style="filter: invert(1)"
/>
<span class="inline-block min-w-[3rem] text-right"
>${renderTroops(landAttack.troops)}</span
>
${translateText("help_modal.ui_wilderness")}`,
className:
"text-left text-gray-400 inline-flex items-center gap-1",
translate: false,
})}
${!landAttack.retreating
? this.renderButton({
content: "❌",
onClick: () => this.emitCancelAttackIntent(landAttack.id),
className: "ml-auto text-left shrink-0",
disabled: landAttack.retreating,
})
: html`<span class="ml-auto shrink-0 text-blue-400"
>(${translateText("events_display.retreating")}...)</span
>`}
</div>
`,
)}
</div>
`;
}

private getBoatTargetName(boat: UnitView): string {
const target = boat.targetTile();
if (target === undefined) return "";
const ownerID = this.game.ownerID(target);
if (ownerID === 0) return "";
const player = this.game.playerBySmallID(ownerID) as PlayerView;
return player?.name() ?? "";
}

private renderBoatIcon(boat: UnitView) {
const dataURL = this.getBoatSpriteDataURL(boat);
if (!dataURL) return html``;
return html`<img
src="${dataURL}"
class="h-5 w-5 inline-block"
style="image-rendering: pixelated"
/>`;
}

private renderBoats() {
if (this.outgoingBoats.length === 0) return html``;

return html`
<div class="flex flex-col gap-1">
${this.outgoingBoats.map(
(boat) => html`
<div
class="flex items-center gap-1 w-full bg-gray-800/70 backdrop-blur-sm rounded-lg px-2 py-1"
>
${this.renderButton({
content: html`${this.renderBoatIcon(boat)}
<span class="inline-block min-w-[3rem] text-right"
>${renderTroops(boat.troops())}</span
>
${this.getBoatTargetName(boat)}`,
onClick: () => this.emitGoToUnitEvent(boat),
className:
"text-left text-blue-400 inline-flex items-center gap-1",
translate: false,
})}
${!boat.retreating()
? this.renderButton({
content: "❌",
onClick: () => this.emitBoatCancelIntent(boat.id()),
className: "ml-auto text-left shrink-0",
disabled: boat.retreating(),
})
: html`<span class="ml-auto shrink-0 text-blue-400"
>(${translateText("events_display.retreating")}...)</span
>`}
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

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and get its line count
if [ -f "src/client/graphics/layers/AttacksDisplay.ts" ]; then
  wc -l "src/client/graphics/layers/AttacksDisplay.ts"
else
  echo "File not found"
fi

Repository: openfrontio/OpenFrontIO

Length of output: 115


🏁 Script executed:

# Find all occurrences of backdrop-blur-sm in the file
rg "backdrop-blur-sm" src/client/graphics/layers/AttacksDisplay.ts -n

Repository: openfrontio/OpenFrontIO

Length of output: 616


🏁 Script executed:

# Check Tailwind CSS configuration
fd -e "tailwind" -e "tailwindrc" -e "tailwind.config" . --type f | head -20

Repository: openfrontio/OpenFrontIO

Length of output: 49


🏁 Script executed:

# Look for package.json to check Tailwind version
fd "package.json" . --type f | head -5 | xargs -I {} bash -c 'echo "=== {} ===" && cat {}'

Repository: openfrontio/OpenFrontIO

Length of output: 4540


🏁 Script executed:

# Check for any other blur utilities that might need updating
rg "(blur-sm|backdrop-blur[^-]|\sblur\s)" src/client/graphics/layers/AttacksDisplay.ts -n

Repository: openfrontio/OpenFrontIO

Length of output: 616


🏁 Script executed:

# Verify no other blur utilities besides backdrop-blur-sm in the file
rg "blur" src/client/graphics/layers/AttacksDisplay.ts -n | grep -v "backdrop-blur-sm"

Repository: openfrontio/OpenFrontIO

Length of output: 49


Update Tailwind blur utility names for v4.

The code uses backdrop-blur-sm in five places (lines 233, 283, 327, 387, 425). In Tailwind v4, this maps to backdrop-blur-xs. Update all occurrences.

Suggested replacements
-              class="flex items-center gap-1 w-full bg-gray-800/70 backdrop-blur-sm rounded-lg px-2 py-1"
+              class="flex items-center gap-1 w-full bg-gray-800/70 backdrop-blur-xs rounded-lg px-2 py-1"
🤖 Prompt for AI Agents
In `@src/client/graphics/layers/AttacksDisplay.ts` around lines 229 - 409, Replace
the Tailwind utility backdrop-blur-sm with backdrop-blur-xs in all attack/boat
display renderers in this file: update the blur class in the incoming attacks
block (inside the method that renders incoming attacks), renderOutgoingAttacks,
renderOutgoingLandAttacks, and renderBoats so each div using "bg-gray-800/70
backdrop-blur-sm ..." becomes "bg-gray-800/70 backdrop-blur-xs ..."; search for
backdrop-blur-sm to find each occurrence and ensure className strings passed to
renderButton (and the boat div) are updated consistently.

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Feb 4, 2026
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.

2 participants