Skip to content

feat: multi-warship selection with Shift+drag box#3677

Open
baculinivan-web wants to merge 13 commits intoopenfrontio:mainfrom
baculinivan-web:feature/warship-selection-and-move
Open

feat: multi-warship selection with Shift+drag box#3677
baculinivan-web wants to merge 13 commits intoopenfrontio:mainfrom
baculinivan-web:feature/warship-selection-and-move

Conversation

@baculinivan-web
Copy link
Copy Markdown
Contributor

@baculinivan-web baculinivan-web commented Apr 15, 2026

Resolves #3666

Description:

Adds RTS-style box selection for warships. Hold Shift and drag (desktop) or long-press and drag (touch/mobile) to draw a selection rectangle — all player-owned warships inside get selected at once. A subsequent click/tap on water sends them all to that location.

  • SelectionBoxLayer — pixel-dashed rectangle in world-space, player territory color; shared between desktop and touch
  • UILayer — same pulsing selection outline on each box-selected warship; clears correctly when switching between single/multi selection
  • UnitLayer — finds warships in screen rect, filters inactive ships before sending; touch support included
  • InputHandler — Shift+drag and touch long-press+drag both emit selection box events; cursor becomes crosshair on Shift; discards active ghost structure on Shift press; configurable via shiftKey keybind
  • Transport — single atomic move_multiple_warships intent (no split on socket drop)
  • Schemas + ExecutionManager + MoveMultipleWarshipsExecution — server fans out atomic intent into individual MoveWarshipExecution per ship
  • DynamicUILayerMoveIndicatorUI chevron animation on target tile for both single and multi move
  • UnitDisplay — warship tooltip Shift hint via translateText
  • HelpModal — new hotkey row: Shift + drag → select multiple warships

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

UI update

Mouse + Keyboard

CleanShot.2026-04-15.at.14.48.02.mp4

Touch

IMG_2237.2.MOV

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

fghjk_60845

Resolves openfrontio#3666

- Shift+drag draws a pixel-dashed selection rectangle in player color
- All player-owned warships inside the box get selected at once
- Click on water sends all selected warships to that location
- Same pulsing selection outline as single-warship selection
- MoveIndicatorUI chevron animation shown on target tile
- Shift keydown discards active ghost structure (build mode)
- Crosshair cursor on Shift hold, resets on release/blur
- Warship toolbar tooltip includes Shift hint (en.json)
- Tests: WarshipMultiSelection.test.ts + InputHandler selection box tests
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds Shift/long-press drag multi-selection for warships: input events and cursor handling, selection-box rendering layer, multi-selection visuals and controls, transport/schema/execution support for batch move intents, localization/help additions, and tests.

Changes

Cohort / File(s) Summary
Localization & Help UI
resources/lang/en.json, src/client/HelpModal.ts
Added translation keys (help_modal.action_warship_multiselect, help_modal.action_warship_selectall, help_modal.drag, build_menu.desc.warship_shift_hint) and appended Shift+drag and Select-All hotkey rows in the help modal.
Input Handling & Events
src/client/InputHandler.ts, tests/InputHandler.test.ts
Added selection-box/touch events (WarshipSelectionBoxUpdateEvent, WarshipSelectionBoxCompleteEvent, WarshipSelectionBoxCancelEvent, SelectAllWarshipsEvent, WarshipMultiSelectionEvent, TouchLongPressStartEvent), long-press & Shift logic, cursor updates, selection threshold, and unit tests for Shift/long-press flows and cancellation.
Renderer & Selection Layer
src/client/graphics/GameRenderer.ts, src/client/graphics/layers/SelectionBoxLayer.ts, src/client/graphics/layers/UILayer.ts
Inserted SelectionBoxLayer into renderer; new layer draws dashed/fill selection rectangle to off-screen canvas and blits it; UILayer updated to animate/manage per-unit multi-selection boxes and cleanup.
Dynamic Move Indicators
src/client/graphics/layers/DynamicUILayer.ts
Subscribed to MoveMultipleWarshipsIntentEvent and enqueued move indicator UI for batch move targets.
Unit Display & UnitLayer
src/client/graphics/layers/UnitDisplay.ts, src/client/graphics/layers/UnitLayer.ts
Tooltip placement tweak and added warship Shift+drag hint; UnitLayer added multi-warship selection state and handlers to compute/filter selected warships, emit WarshipMultiSelectionEvent, and emit MoveMultipleWarshipsIntentEvent when moving multiple selected warships.
Transport / Network Intent
src/client/Transport.ts
Added MoveMultipleWarshipsIntentEvent and handler to send {type:"move_multiple_warships", unitIds, tile} to server.
Core Intent & Execution
src/core/Schemas.ts, src/core/execution/ExecutionManager.ts, src/core/execution/MoveMultipleWarshipsExecution.ts
Added MoveMultipleWarshipsIntentSchema/type; ExecutionManager routes "move_multiple_warships" to MoveMultipleWarshipsExecution, which fans out per-unit MoveWarshipExecution during init.
User Settings
src/core/game/UserSettings.ts
Added default keybind selectAllWarships: "KeyF".
Tests
tests/WarshipMultiSelection.test.ts, tests/InputHandler.test.ts
New tests for multi-move execution (batched moves, ownership isolation, deleted-unit handling) and extended InputHandler tests for Shift/long-press box behavior.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant IH as InputHandler
    participant SBL as SelectionBoxLayer
    participant UL as UnitLayer
    participant T as Transport
    participant Server as Server

    User->>IH: Press Shift / long-press + Drag
    IH->>IH: mark selectionBoxActive\nset cursor = "crosshair"
    IH->>SBL: WarshipSelectionBoxUpdateEvent(start,end)
    SBL->>SBL: render dashed selection rect

    User->>IH: Release Pointer
    IH->>IH: compute drag distance
    alt distance >= threshold
        IH->>UL: WarshipSelectionBoxCompleteEvent(start,end)
        UL->>UL: filter player-owned warships in rect
        UL->>UL: emit WarshipMultiSelectionEvent(units)
    else
        IH->>UL: WarshipSelectionBoxCancelEvent
        UL->>UL: emit WarshipMultiSelectionEvent([])
    end

    User->>UL: Click water tile (with multi-selection)
    UL->>T: MoveMultipleWarshipsIntentEvent(unitIds, tile)
    T->>Server: send intent {type: "move_multiple_warships", unitIds, tile}
    Server->>Server: ExecutionManager -> create MoveMultipleWarshipsExecution -> spawn per-unit MoveWarshipExecution
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🚢 Hold Shift, draw a dashed bright frame,

Many hulls chosen, each with a single name,
One click, one intent, the orders fly,
Fleets fan out together beneath the sky,
A tiny box, a captain's new game.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and specifically summarizes the main feature: multi-warship selection using Shift+drag, which is the primary focus of all code changes.
Description check ✅ Passed Description comprehensively relates to the changeset, detailing the multi-warship selection feature, affected components, implementation approach, and testing/translations completed.
Linked Issues check ✅ Passed PR fully implements the linked issue #3666 requirements: RTS-style box selection (Shift+drag on desktop, long-press+drag on touch), visual feedback for multi-selected warships, grouped move commands, and configurable keybinding with default Shift.
Out of Scope Changes check ✅ Passed All changes directly support the multi-warship selection feature with no unrelated modifications; touch long-press, selection box rendering, UI feedback, intent routing, and execution handling are all in-scope components.

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


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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

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

1-702: ⚠️ Potential issue | 🟡 Minor

Formatting issue flagged by CI pipeline.

The Prettier check reported a formatting/style issue for this file. Please run your formatter to fix it before merge.

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

In `@src/client/graphics/layers/UnitLayer.ts` around lines 1 - 702, The file
UnitLayer.ts fails the Prettier/style check; run your formatter (e.g., npm run
format or npx prettier --write) on UnitLayer.ts (the class UnitLayer and its
methods like onMouseUp, onUnitSelectionChange, drawSprite, paintCell, etc.) and
commit the formatted file so the CI Prettier check passes—do not change logic,
only apply formatting and stage the updated file for the PR.
🤖 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/UILayer.ts`:
- Around line 211-225: onMultiSelection currently only clears
multiSelectionBoxCenters, so any existing single-selection state (selectedUnit
and lastSelectionBoxCenter) continues animating; update onMultiSelection to also
clear the single-selection outline by calling
clearSelectionBox(lastSelectionBoxCenter.x, lastSelectionBoxCenter.y,
lastSelectionBoxCenter.size) if lastSelectionBoxCenter is set, then null out
selectedUnit and lastSelectionBoxCenter before proceeding to set
multiSelectedWarships and drawSelectionBoxMulti for active units.

In `@src/client/Transport.ts`:
- Around line 637-645: The onMoveMultipleWarshipsEvent currently loops and calls
sendIntent repeatedly, which can split group moves if the socket drops; change
it to send a single batched intent (e.g. sendIntent with type
"move_multiple_warships" and payload { unitIds: event.unitIds, tile: event.tile
}) so the client emits one atomic intent and let the server/local executor (or
existing intent handler) fan out into individual move_warship actions; update
any handlers expecting multiple individual intents to accept and expand the new
batched intent (references: onMoveMultipleWarshipsEvent, sendIntent,
MoveMultipleWarshipsIntentEvent).

---

Outside diff comments:
In `@src/client/graphics/layers/UnitLayer.ts`:
- Around line 1-702: The file UnitLayer.ts fails the Prettier/style check; run
your formatter (e.g., npm run format or npx prettier --write) on UnitLayer.ts
(the class UnitLayer and its methods like onMouseUp, onUnitSelectionChange,
drawSprite, paintCell, etc.) and commit the formatted file so the CI Prettier
check passes—do not change logic, only apply formatting and stage the updated
file for the PR.
🪄 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: 108a19e0-761b-426a-96c9-9d1c20bc3fda

📥 Commits

Reviewing files that changed from the base of the PR and between 3a49b9a and 60a202f.

📒 Files selected for processing (11)
  • resources/lang/en.json
  • src/client/InputHandler.ts
  • src/client/Transport.ts
  • src/client/graphics/GameRenderer.ts
  • src/client/graphics/layers/DynamicUILayer.ts
  • src/client/graphics/layers/SelectionBoxLayer.ts
  • src/client/graphics/layers/UILayer.ts
  • src/client/graphics/layers/UnitDisplay.ts
  • src/client/graphics/layers/UnitLayer.ts
  • tests/InputHandler.test.ts
  • tests/WarshipMultiSelection.test.ts

Comment thread src/client/graphics/layers/UILayer.ts
Comment thread src/client/Transport.ts
@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Apr 15, 2026
- Replace hardcoded ShiftLeft/ShiftRight with this.keybinds.shiftKey
- Add warship multi-select row to HelpModal hotkeys table
- Add help_modal.action_warship_multiselect and drag keys to en.json
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

230-271: Extract the shared selection-box painter.

drawSelectionBoxMulti() now copies almost all of drawSelectionBox(). One helper for the pulse/color/border drawing would keep single- and multi-select visuals from drifting on the next style change.

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

In `@src/client/graphics/layers/UILayer.ts` around lines 230 - 271, Extract the
duplicated selection-box drawing from drawSelectionBoxMulti() and
drawSelectionBox() into a single helper (e.g., paintSelectionBox or
drawSelectionBoxAt) that accepts centerX, centerY, size, ownerColor (or a
computed selectionColor), and uses this.selectionAnimTime / SELECTION_BOX_SIZE
to compute pulse opacity and paint the border cells via this.paintCell; update
both drawSelectionBoxMulti and drawSelectionBox to call the new helper, keep
clearSelectionBox and multiSelectionBoxCenters logic in the callers (only the
painting/pulse/color loop moves to the helper) and reuse
this.SELECTION_BOX_SIZE, this.paintCell, and this.selectionAnimTime symbols to
preserve behavior.
🤖 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/UnitLayer.ts`:
- Around line 159-169: The current multi-move branch emits
MoveMultipleWarshipsIntentEvent using this.selectedWarships without filtering
out ships that may have been destroyed or deactivated; before emitting, filter
this.selectedWarships to only include currently active/player-owned warships
(e.g., check each warship.isActive() and warship.ownerId === currentPlayerId or
equivalent), then map to ids and emit MoveMultipleWarshipsIntentEvent with that
filtered id list; if the filtered list is empty, do not emit the move intent but
still clear this.selectedWarships and emit WarshipMultiSelectionEvent([]).

---

Nitpick comments:
In `@src/client/graphics/layers/UILayer.ts`:
- Around line 230-271: Extract the duplicated selection-box drawing from
drawSelectionBoxMulti() and drawSelectionBox() into a single helper (e.g.,
paintSelectionBox or drawSelectionBoxAt) that accepts centerX, centerY, size,
ownerColor (or a computed selectionColor), and uses this.selectionAnimTime /
SELECTION_BOX_SIZE to compute pulse opacity and paint the border cells via
this.paintCell; update both drawSelectionBoxMulti and drawSelectionBox to call
the new helper, keep clearSelectionBox and multiSelectionBoxCenters logic in the
callers (only the painting/pulse/color loop moves to the helper) and reuse
this.SELECTION_BOX_SIZE, this.paintCell, and this.selectionAnimTime symbols to
preserve behavior.
🪄 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: c08444b4-2d2b-4217-a30c-2f7b0d74560e

📥 Commits

Reviewing files that changed from the base of the PR and between 60a202f and 7b44813.

📒 Files selected for processing (5)
  • src/client/graphics/layers/DynamicUILayer.ts
  • src/client/graphics/layers/UILayer.ts
  • src/client/graphics/layers/UnitLayer.ts
  • tests/InputHandler.test.ts
  • tests/WarshipMultiSelection.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/client/graphics/layers/DynamicUILayer.ts
  • tests/WarshipMultiSelection.test.ts
  • tests/InputHandler.test.ts

Comment thread src/client/graphics/layers/UnitLayer.ts
onMultiSelection now clears selectedUnit and lastSelectionBoxCenter
before applying multi-selection, preventing ghost outlines
…onBoxAt helper

- UnitLayer: filter selectedWarships to active+owned before emitting
  MoveMultipleWarshipsIntentEvent; skip emit if none remain active
- UILayer: extract shared paintSelectionBoxAt() helper used by both
  drawSelectionBox and drawSelectionBoxMulti to remove duplication
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

238-279: Extract one shared selection-box painter.

drawSelectionBoxMulti() now copies most of drawSelectionBox(). Keeping two copies of the same pulse/border logic will drift fast. Please move the shared paint logic into one helper and keep only the per-mode state tracking separate.

Small refactor sketch
+  private drawAnimatedSelectionBox(
+    centerX: number,
+    centerY: number,
+    selectionSize: number,
+    selectionColor: Colord,
+  ) {
+    const baseOpacity = 200;
+    const pulseAmount = 55;
+    const opacity =
+      baseOpacity + Math.sin(this.selectionAnimTime * 0.1) * pulseAmount;
+
+    for (let x = centerX - selectionSize; x <= centerX + selectionSize; x++) {
+      for (let y = centerY - selectionSize; y <= centerY + selectionSize; y++) {
+        if (
+          (x === centerX - selectionSize ||
+            x === centerX + selectionSize ||
+            y === centerY - selectionSize ||
+            y === centerY + selectionSize) &&
+          (x + y) % 2 === 0
+        ) {
+          this.paintCell(x, y, selectionColor, opacity);
+        }
+      }
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/UILayer.ts` around lines 238 - 279,
drawSelectionBoxMulti duplicates the pulse/border painting logic from
drawSelectionBox; extract that shared logic into a new helper (e.g.,
paintSelectionBox or paintSelectionBorder) that accepts centerX, centerY,
selectionSize, selectionColor and opacity and performs the nested x/y loop and
paintCell calls. In drawSelectionBoxMulti and drawSelectionBox compute
mode-specific state (selectionSize, centerX/centerY, selectionColor, opacity)
and then call the new helper; keep each function's per-mode state tracking
(multiSelectionBoxCenters vs the single-selection center/clear logic) unchanged.
Ensure helper references the same this.paintCell and
this.selectionAnimTime/SELECTION_BOX_SIZE/theme checks as needed and update
callers to remove duplicated logic.
🤖 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/UILayer.ts`:
- Around line 220-225: Update UILayer to clear multi-selection state when a
single unit is selected: in the UILayer.onUnitSelectionChange handler (the
method that processes UnitSelectionEvent), detect when the event represents a
single unit selection and then clear this.multiSelectedWarships and
this.multiSelectionBoxCenters (and remove any rendered boxes via the same
clearSelectionBox logic used in the WarshipMultiSelectionEvent handling). This
mirrors the WarshipMultiSelectionEvent flow (which clears boxes and centers) so
ensure you call the same clearing logic before setting single selection state.

---

Nitpick comments:
In `@src/client/graphics/layers/UILayer.ts`:
- Around line 238-279: drawSelectionBoxMulti duplicates the pulse/border
painting logic from drawSelectionBox; extract that shared logic into a new
helper (e.g., paintSelectionBox or paintSelectionBorder) that accepts centerX,
centerY, selectionSize, selectionColor and opacity and performs the nested x/y
loop and paintCell calls. In drawSelectionBoxMulti and drawSelectionBox compute
mode-specific state (selectionSize, centerX/centerY, selectionColor, opacity)
and then call the new helper; keep each function's per-mode state tracking
(multiSelectionBoxCenters vs the single-selection center/clear logic) unchanged.
Ensure helper references the same this.paintCell and
this.selectionAnimTime/SELECTION_BOX_SIZE/theme checks as needed and update
callers to remove duplicated logic.
🪄 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: ca104888-b7c1-4c1f-be67-ab596db346e5

📥 Commits

Reviewing files that changed from the base of the PR and between 5f23c20 and 72b2b4c.

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

Comment thread src/client/graphics/layers/UILayer.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/client/graphics/layers/UILayer.ts (1)

189-205: ⚠️ Potential issue | 🟠 Major

Clear box-selection state on normal single selection.

A single UnitSelectionEvent sets selectedUnit, but it does not clear multiSelectedWarships or the already painted multi-selection boxes. After box-selecting warships, a normal click can therefore leave stale group outlines on screen until some later WarshipMultiSelectionEvent([]) happens.

Suggested fix
   private onUnitSelection(event: UnitSelectionEvent) {
     if (event.isSelected) {
+      for (const [, center] of this.multiSelectionBoxCenters) {
+        this.clearSelectionBox(center.x, center.y, center.size);
+      }
+      this.multiSelectionBoxCenters.clear();
+      this.multiSelectedWarships = [];
+
       this.selectedUnit = event.unit;
       if (event.unit && event.unit.type() === UnitType.Warship) {
         this.drawSelectionBox(event.unit);
       }
     } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/UILayer.ts` around lines 189 - 205, When handling
a normal single selection in onUnitSelection, you must also clear any prior
multi-selection state and visuals so stale group outlines aren't left on screen;
update onUnitSelection to reset this.multiSelectedWarships (set to empty array)
and remove any painted multi-selection boxes — either call an existing
clearMultiSelectionBoxes() helper if present or iterate the stored
multi-selection centers and call clearSelectionBox(x,y,size) for each before
setting this.selectedUnit; ensure any stored multi-selection center/state is
nulled after clearing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/client/graphics/layers/UILayer.ts`:
- Around line 189-205: When handling a normal single selection in
onUnitSelection, you must also clear any prior multi-selection state and visuals
so stale group outlines aren't left on screen; update onUnitSelection to reset
this.multiSelectedWarships (set to empty array) and remove any painted
multi-selection boxes — either call an existing clearMultiSelectionBoxes()
helper if present or iterate the stored multi-selection centers and call
clearSelectionBox(x,y,size) for each before setting this.selectedUnit; ensure
any stored multi-selection center/state is nulled after clearing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f032545b-bc27-4586-a3ec-ceeb986e9cb3

📥 Commits

Reviewing files that changed from the base of the PR and between 72b2b4c and edf42ea.

📒 Files selected for processing (3)
  • src/client/graphics/layers/UILayer.ts
  • src/client/graphics/layers/UnitLayer.ts
  • tests/InputHandler.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/client/graphics/layers/UnitLayer.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/InputHandler.test.ts

@baculinivan-web
Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

✅ Actions performed

Reviews resumed.

@baculinivan-web
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@VariableVince
Copy link
Copy Markdown
Contributor

VariableVince commented Apr 15, 2026

Nice. Have you thought about how the mobile equivalent would work? The goal is to have mobile on par with desktop as much as possible, even if hotkeys etc aren't possible.

Maybe in a follow-up PR if you'd be up for it and this one gets merged.

Perhaps something like this (but water instead of ground in this case ofc)

Screenshot_20260415_162453_Vivaldi

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

271-293: Nice extraction of shared logic.

paintSelectionBoxAt consolidates the duplicated box-painting code. Clean and readable.

One small note: the nested loop iterates all cells in the box but only paints border pixels. You could iterate just the border for a tiny speedup, but with SELECTION_BOX_SIZE = 6 (13×13 = 169 iterations), this is fine as-is.

♻️ Optional: iterate only border cells
 private paintSelectionBoxAt(
   centerX: number,
   centerY: number,
   selectionColor: Colord,
 ) {
   const size = this.SELECTION_BOX_SIZE;
   const opacity = 200 + Math.sin(this.selectionAnimTime * 0.1) * 55;
 
-  for (let x = centerX - size; x <= centerX + size; x++) {
-    for (let y = centerY - size; y <= centerY + size; y++) {
-      if (
-        x === centerX - size ||
-        x === centerX + size ||
-        y === centerY - size ||
-        y === centerY + size
-      ) {
-        if ((x + y) % 2 === 0) {
-          this.paintCell(x, y, selectionColor, opacity);
-        }
-      }
-    }
-  }
+  // Top and bottom edges
+  for (let x = centerX - size; x <= centerX + size; x++) {
+    if ((x + centerY - size) % 2 === 0) {
+      this.paintCell(x, centerY - size, selectionColor, opacity);
+    }
+    if ((x + centerY + size) % 2 === 0) {
+      this.paintCell(x, centerY + size, selectionColor, opacity);
+    }
+  }
+  // Left and right edges (skip corners, already painted)
+  for (let y = centerY - size + 1; y < centerY + size; y++) {
+    if ((centerX - size + y) % 2 === 0) {
+      this.paintCell(centerX - size, y, selectionColor, opacity);
+    }
+    if ((centerX + size + y) % 2 === 0) {
+      this.paintCell(centerX + size, y, selectionColor, opacity);
+    }
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/UILayer.ts` around lines 271 - 293,
paintSelectionBoxAt currently loops over the entire square but only paints
border cells; refactor it to iterate only the four border ranges to avoid
unnecessary iterations: compute size = this.SELECTION_BOX_SIZE and opacity as
before, then loop x from centerX - size to centerX + size for the top (y =
centerY - size) and bottom (y = centerY + size) edges and loop y from centerY -
size + 1 to centerY + size - 1 for the left (x = centerX - size) and right (x =
centerX + size) edges, calling this.paintCell(x,y, selectionColor, opacity);
keep using the same function name paintSelectionBoxAt and the same opacity
calculation so behavior is unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/client/graphics/layers/UILayer.ts`:
- Around line 271-293: paintSelectionBoxAt currently loops over the entire
square but only paints border cells; refactor it to iterate only the four border
ranges to avoid unnecessary iterations: compute size = this.SELECTION_BOX_SIZE
and opacity as before, then loop x from centerX - size to centerX + size for the
top (y = centerY - size) and bottom (y = centerY + size) edges and loop y from
centerY - size + 1 to centerY + size - 1 for the left (x = centerX - size) and
right (x = centerX + size) edges, calling this.paintCell(x,y, selectionColor,
opacity); keep using the same function name paintSelectionBoxAt and the same
opacity calculation so behavior is unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09065808-86a5-4bd0-b271-c5b5bf224c63

📥 Commits

Reviewing files that changed from the base of the PR and between 72b2b4c and cf5c50a.

📒 Files selected for processing (3)
  • src/client/graphics/layers/UILayer.ts
  • src/client/graphics/layers/UnitLayer.ts
  • tests/InputHandler.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/InputHandler.test.ts
  • src/client/graphics/layers/UnitLayer.ts

- Add MoveMultipleWarshipsIntentSchema to Schemas.ts
- Add MoveMultipleWarshipsExecution that fans out to individual MoveWarshipExecutions
- Transport now sends a single batched intent instead of N individual ones
- Prevents split group moves if socket drops mid-loop
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/client/Transport.ts (1)

637-643: Consider cloning unitIds before sending to keep payload immutable.

At Line 640, passing the same array reference works today, but a defensive copy avoids accidental mutation side effects in future listeners/refactors.

🛡️ Small defensive tweak
   private onMoveMultipleWarshipsEvent(event: MoveMultipleWarshipsIntentEvent) {
     this.sendIntent({
       type: "move_multiple_warships",
-      unitIds: event.unitIds,
+      unitIds: [...event.unitIds],
       tile: event.tile,
     });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/Transport.ts` around lines 637 - 643, The
onMoveMultipleWarshipsEvent handler passes the original
MoveMultipleWarshipsIntentEvent.unitIds array to sendIntent, risking accidental
mutation of the payload; update onMoveMultipleWarshipsEvent to send a shallow
copy of unitIds (e.g., via slice() or spread) when calling sendIntent so the
intent payload is immutable — locate the onMoveMultipleWarshipsEvent function
and change the unitIds argument to a cloned array before sending.
src/core/Schemas.ts (1)

413-417: Constrain unitIds in the schema to avoid malformed or oversized intents.

At Line 415, z.array(z.number()) allows empty lists, duplicates, and non-integer IDs. Tightening this will prevent no-op/duplicate fan-out and reduce abuse risk from oversized payloads.

♻️ Suggested schema hardening
 export const MoveMultipleWarshipsIntentSchema = z.object({
   type: z.literal("move_multiple_warships"),
-  unitIds: z.array(z.number()),
+  unitIds: z
+    .array(z.number().int().nonnegative())
+    .min(1)
+    // pick a game-appropriate cap constant
+    .max(1000)
+    .refine((ids) => new Set(ids).size === ids.length, {
+      message: "unitIds must be unique",
+    }),
   tile: z.number(),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/Schemas.ts` around lines 413 - 417, The
MoveMultipleWarshipsIntentSchema's unitIds currently allows empty, non-integer,
duplicate or overly large arrays; update the unitIds definition to be an array
of integers (use z.number().int()), require at least one entry (.nonempty()),
enforce a sensible maximum length to prevent oversized payloads (e.g., .max(N)
with N chosen by domain limits), and enforce uniqueness (either via .refine(...)
or .superRefine(...) checking new Set(size) equals array length). Keep the
schema name MoveMultipleWarshipsIntentSchema and field unitIds when making these
validations so downstream code still matches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/client/Transport.ts`:
- Around line 637-643: The onMoveMultipleWarshipsEvent handler passes the
original MoveMultipleWarshipsIntentEvent.unitIds array to sendIntent, risking
accidental mutation of the payload; update onMoveMultipleWarshipsEvent to send a
shallow copy of unitIds (e.g., via slice() or spread) when calling sendIntent so
the intent payload is immutable — locate the onMoveMultipleWarshipsEvent
function and change the unitIds argument to a cloned array before sending.

In `@src/core/Schemas.ts`:
- Around line 413-417: The MoveMultipleWarshipsIntentSchema's unitIds currently
allows empty, non-integer, duplicate or overly large arrays; update the unitIds
definition to be an array of integers (use z.number().int()), require at least
one entry (.nonempty()), enforce a sensible maximum length to prevent oversized
payloads (e.g., .max(N) with N chosen by domain limits), and enforce uniqueness
(either via .refine(...) or .superRefine(...) checking new Set(size) equals
array length). Keep the schema name MoveMultipleWarshipsIntentSchema and field
unitIds when making these validations so downstream code still matches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0354f116-d367-446a-bc09-71a34d361b4b

📥 Commits

Reviewing files that changed from the base of the PR and between cf5c50a and 19a5c75.

📒 Files selected for processing (4)
  • src/client/Transport.ts
  • src/core/Schemas.ts
  • src/core/execution/ExecutionManager.ts
  • src/core/execution/MoveMultipleWarshipsExecution.ts

coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 15, 2026
@baculinivan-web
Copy link
Copy Markdown
Contributor Author

@coderabbitai resolve

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

✅ Actions performed

Comments resolved and changes approved.

@baculinivan-web
Copy link
Copy Markdown
Contributor Author

@VariableVince Is mobile support nessesary to. be merged? idk how such a complex gesture can be implemented for mobile. two finger drag might work, but on a small screen gust selecting by hand would probably be simpler for the end user

@baculinivan-web
Copy link
Copy Markdown
Contributor Author

@VariableVince i looked at how mobile interactions are handled (first time playing on touchscreen....) and figured Hold + Drag would work.

flowchart TD
    A[User holds finger on map for ~1 second] --> B[Cross target appears under finger]
    B --> C[User drags]
    C --> D[Selection box is created]
    D --> E[User lets go]
    E --> F[Warships are selected]
    F --> G[User taps]
    G --> H[Warships go to tapped area]
Loading

@baculinivan-web
Copy link
Copy Markdown
Contributor Author

@VariableVince did it. loved it. wanna do more. works perfectly on ipad mini.

IMG_2237.2.MOV

- Hold finger ~800ms to activate selection mode
- Drag to draw selection box (same pixel-dashed style as desktop)
- Release to select warships, tap water to send them
- Long-press cancelled if finger moves >10px before timer fires
- Single SelectionBoxLayer handles both Shift+drag and touch long-press
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/client/InputHandler.ts (2)

593-612: Inconsistent use of event.x vs event.clientX.

Line 597 uses event.x for distance calculation, but line 604-605 uses event.clientX/event.clientY for the event coordinates. While these are typically equivalent, using them inconsistently can be confusing. Pick one and stick with it.

💡 Suggested fix
     if (this.selectionBoxActive) {
       this.selectionBoxActive = false;
       const dist =
-        Math.abs(event.x - this.lastPointerDownX) +
-        Math.abs(event.y - this.lastPointerDownY);
+        Math.abs(event.clientX - this.lastPointerDownX) +
+        Math.abs(event.clientY - this.lastPointerDownY);
       if (dist >= 10) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/InputHandler.ts` around lines 593 - 612, The distance calculation
uses event.x/event.y while the emitted WarshipSelectionBoxCompleteEvent passes
event.clientX/event.clientY, causing inconsistency; update the distance
computation in the this.selectionBoxActive block (where lastPointerDownX and
lastPointerDownY are used) to reference event.clientX and event.clientY so the
same coordinate properties are used throughout, leaving
WarshipSelectionBoxCompleteEvent and WarshipSelectionBoxCancelEvent usage
unchanged.

264-278: Consider resetting selectionBoxActive on blur.

The blur handler clears longPressActive and the timer, but selectionBoxActive remains set. If the user tabs away mid-selection, returning focus could leave the state inconsistent.

💡 Suggested fix
       if (this.longPressTimer !== null) {
         clearTimeout(this.longPressTimer);
         this.longPressTimer = null;
       }
       this.longPressActive = false;
+      if (this.selectionBoxActive) {
+        this.selectionBoxActive = false;
+        this.eventBus.emit(new WarshipSelectionBoxCancelEvent());
+      }
       this.canvas.style.cursor = "";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/InputHandler.ts` around lines 264 - 278, The blur handler
registered in window.addEventListener("blur") fails to reset selectionBoxActive,
leaving selection state inconsistent when focus is lost; update that handler to
set this.selectionBoxActive = false (and clear any selection box state such as
this.selectionBox coordinates or visual indicators) and, if you have a
SelectionBoxEvent or similar, emit an event via this.eventBus to notify others
of the change; ensure this runs alongside the existing resets
(this.longPressActive, this.longPressTimer, this.pointers, this.pointerDown,
this.alternateView) so returning focus starts from a clean state.
🤖 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/InputHandler.ts`:
- Around line 519-523: The current Shift-release handler in InputHandler
(checking this.keybinds.shiftKey and this.selectionBoxActive) can clear the
crosshair too early; update the handler so it only clears the cursor when
neither a selection box nor an ongoing multi-selection is active — e.g., add a
guard that checks a multi-selection flag or whether warships remain selected
(use whatever existing state you have, or introduce this.multiSelectionActive)
before setting this.canvas.style.cursor = ""; alternatively remove this
cursor-reset here and let WarshipSelectionBoxCompleteEvent /
WarshipMultiSelectionEvent handlers manage cursor state so the crosshair set by
WarshipMultiSelectionEvent isn't immediately cleared.

---

Nitpick comments:
In `@src/client/InputHandler.ts`:
- Around line 593-612: The distance calculation uses event.x/event.y while the
emitted WarshipSelectionBoxCompleteEvent passes event.clientX/event.clientY,
causing inconsistency; update the distance computation in the
this.selectionBoxActive block (where lastPointerDownX and lastPointerDownY are
used) to reference event.clientX and event.clientY so the same coordinate
properties are used throughout, leaving WarshipSelectionBoxCompleteEvent and
WarshipSelectionBoxCancelEvent usage unchanged.
- Around line 264-278: The blur handler registered in
window.addEventListener("blur") fails to reset selectionBoxActive, leaving
selection state inconsistent when focus is lost; update that handler to set
this.selectionBoxActive = false (and clear any selection box state such as
this.selectionBox coordinates or visual indicators) and, if you have a
SelectionBoxEvent or similar, emit an event via this.eventBus to notify others
of the change; ensure this runs alongside the existing resets
(this.longPressActive, this.longPressTimer, this.pointers, this.pointerDown,
this.alternateView) so returning focus starts from a clean state.
🪄 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: c42308cc-4bcd-4f19-aad2-fb5049314df9

📥 Commits

Reviewing files that changed from the base of the PR and between 19a5c75 and de07436.

📒 Files selected for processing (2)
  • src/client/InputHandler.ts
  • src/client/graphics/layers/SelectionBoxLayer.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/graphics/layers/SelectionBoxLayer.ts

Comment thread src/client/InputHandler.ts
- Keep crosshair after Shift release while multi-selection is active
- Use event.clientX/Y consistently in selection box distance check
- Reset selectionBoxActive and emit cancel on window blur
coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 15, 2026
@VariableVince
Copy link
Copy Markdown
Contributor

@VariableVince did it. loved it. wanna do more. works perfectly on ipad mini.

IMG_2237.2.MOV

Wow way to go! You asked if it was necessary to merge, maybe not but an Issue would surely have been created for someone to built it later on.

There are other things that could use 'getting more on par' like nuke trajectory preview, which could work with single tap, drag and double tap for launch or something.

We probably do need an Issue to have improved instructions for mobile control in the HelpModal.

- New keybind selectAllWarships (default KeyF) in UserSettings
- SelectAllWarshipsEvent emitted on keyup
- UnitLayer selects all active player-owned warships
- HelpModal row + en.json translation key
@baculinivan-web
Copy link
Copy Markdown
Contributor Author

@VariableVince did it. loved it. wanna do more. works perfectly on ipad mini.
IMG_2237.2.MOV

Wow way to go! You asked if it was necessary to merge, maybe not but an Issue would surely have been created for someone to built it later on.

There are other things that could use 'getting more on par' like nuke trajectory preview, which could work with single tap, drag and double tap for launch or something.

We probably do need an Issue to have improved instructions for mobile control in the HelpModal.

Oh for sure, but i am already a bit overwhelmed by 6 of my PRs being bombarded by coderabbit.... I would probably take this after a few of them would be merged @evanpelle :)

Снимок экрана 2026-04-15 в 21 03 04

@VariableVince
Copy link
Copy Markdown
Contributor

@baculinivan-web thought about it shortly yesterday and was like 'nah enough for now' but low and behold you have addressed keyboard+mouse and touch screen, but some use touchpad/trackpad :p #3666 (comment). Follow-up PR? I think touchpad is especially tricky to handle correctly without messing up other gestures, if possible at all in a good way btw.

@baculinivan-web
Copy link
Copy Markdown
Contributor Author

@baculinivan-web thought about it shortly yesterday and was like 'nah enough for now' but low and behold you have addressed keyboard+mouse and touch screen, but some use touchpad/trackpad :p #3666 (comment). Follow-up PR? I think touchpad is especially tricky to handle correctly without messing up other gestures, if possible at all in a good way btw.

Touchpad works well. It has same inputs as mouse. Tested. Also tasted with Apple Magic Mouse

@VariableVince
Copy link
Copy Markdown
Contributor

VariableVince commented Apr 16, 2026

⁷> > @baculinivan-web thought about it shortly yesterday and was like 'nah enough for now' but low and behold you have addressed keyboard+mouse and touch screen, but some use touchpad/trackpad :p #3666 (comment). Follow-up PR? I think touchpad is especially tricky to handle correctly without messing up other gestures, if possible at all in a good way btw.

Touchpad works well. It has same inputs as mouse. Tested. Also tasted with Apple Magic Mouse

It reminded of issues with zooming/panning, each one never working nicely when it was tried to have both (#1956 was reverted later on). This one apparently didn't have such issues, good to know

evanpelle pushed a commit that referenced this pull request Apr 17, 2026
## Description:

This PR adds support for `Shift+<key>` keybind combinations across the
entire keybind system.

Previously, keybinds only supported a single key (e.g. `KeyB` for boat
attack). Now any keybind can be configured as `Shift+KeyB`, which will
only trigger when Shift is held down simultaneously.

Enables to use Shift + A for "select all" feature from #3677 

**Changes:**
- `InputHandler.ts`: Added `parseKeybind()` helper that parses
`"Shift+KeyB"` → `{ shift: true, code: "KeyB" }`. Added
`keybindMatchesEvent()` for consistent matching across all keyup/keydown
handlers. Updated `resolveBuildKeybind()` and all keybind comparisons to
respect the shift modifier.
- `SettingKeybind.ts`: When recording a keybind, lone modifier keys
(Shift, Ctrl, etc.) are skipped — the component waits for the actual
key. If Shift is held when the key is pressed, the value is stored as
`"Shift+<code>"`.
- `Utils.ts`: `formatKeyForDisplay()` now handles the `Shift+` prefix,
displaying e.g. `"Shift+B"`.
- `tests/InputHandler.test.ts`: Added 6 tests covering Shift+ keybind
matching, negative cases (plain key not triggering Shift-bound action),
coexistence of `Digit1` and `Shift+Digit1` on different actions, and
Numpad alias support with Shift.

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

## UI changes:

<img width="2255" height="2070" alt="CleanShot 2026-04-15 at 20 23
25@2x"
src="https://github.com/user-attachments/assets/96c19fc3-6294-40b7-82eb-3fde52b71618"
/>


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

fghjk_60845
@baculinivan-web
Copy link
Copy Markdown
Contributor Author

@evanpelle Review, please.

Comment thread src/core/Schemas.ts
Comment on lines +413 to +417
export const MoveMultipleWarshipsIntentSchema = z.object({
type: z.literal("move_multiple_warships"),
unitIds: z.array(z.number()),
tile: z.number(),
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

could we reuse movewarshipintent?


/** Emitted when the selection box is cancelled (e.g. Escape or no drag) */
export class WarshipSelectionBoxCancelEvent implements GameEvent {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

could this share the same event with single warship selection cancelled?

Comment on lines +127 to +130
/** Emitted when multiple warships are selected via box selection */
export class WarshipMultiSelectionEvent implements GameEvent {
constructor(public readonly units: UnitView[]) {}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

would we reuse the warship selected event, just have it be an array instead?

Comment thread src/client/Transport.ts
Comment on lines +168 to +173
export class MoveMultipleWarshipsIntentEvent implements GameEvent {
constructor(
public readonly unitIds: number[],
public readonly tile: number,
) {}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same here, just reuse the existing move warship but have it be an array instead

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here, remove this file and just have MmoveWarshipExecution handle it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

so i basically forgot warships can move already.... fix incoming

* Renders the shift+drag / touch long-press warship selection rectangle
* in world-space, using the same pixel-dashed style as UILayer.
*/
export class SelectionBoxLayer implements Layer {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think this should implement Layer since it's not registered by the GameRenderer

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.

Feature Allow selecting multiple warships with keybinding (default shift)

3 participants