From ede0fb766804ce450d2289626d7d328cd757f9c5 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sat, 16 May 2026 19:53:13 -0700 Subject: [PATCH] move single-unit warship selection box to WebGL SelectionBoxPass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UnitSelectionEvent now forwards to view.setSelectedUnit(unit.id()) in mountWebGLDebugRenderer; the renderer's SelectionBoxPass draws the animated stippled outline on the GPU. UILayer still tracks selectedUnit for game-logic readers (the click handlers) but no longer paints to canvas2D for it. Drops drawSelectionBox + lastSelectionBoxCenter (~50 LOC) plus the per-tick single-unit redraw in tick(). Multi-selection stays on canvas2D — SelectionBoxPass is single-unit only. Test update: replaces the now-dead drawSelectionBox spy with a selectedUnit state assertion + a deselect case. --- src/client/ClientGameRunner.ts | 18 +++++ src/client/graphics/layers/UILayer.ts | 109 +++++--------------------- tests/client/graphics/UILayer.test.ts | 25 +++++- 3 files changed, 61 insertions(+), 91 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 09047f371..93de7a16e 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -43,6 +43,7 @@ import { MouseMoveEvent, MouseUpEvent, TickMetricsEvent, + UnitSelectionEvent, } from "./InputHandler"; import { endGame, startGame, startTime } from "./LocalPersistantStats"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; @@ -345,6 +346,23 @@ function mountWebGLDebugRenderer( view.showMoveIndicator(tx, ty, firstUnit.owner().smallID()); }); + // Single-unit warship selection box: forward UnitSelectionEvent to the + // renderer's SelectionBoxPass. Multi-selection (event.units.length > 0) + // stays canvas2D for now — SelectionBoxPass only supports one unit. + eventBus.on(UnitSelectionEvent, (e) => { + if (!e.isSelected) { + view.setSelectedUnit(null); + return; + } + if ((e.units ?? []).length > 0) { + // Multi-selection: drop any prior single highlight; canvas2D draws + // the multi outlines in UILayer. + view.setSelectedUnit(null); + return; + } + view.setSelectedUnit(e.unit?.id() ?? null); + }); + return { builder: new WebGLFrameBuilder(view), syncCamera }; } diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts index 3e2888781..3cb75baf1 100644 --- a/src/client/graphics/layers/UILayer.ts +++ b/src/client/graphics/layers/UILayer.ts @@ -44,13 +44,6 @@ export class UILayer implements Layer { { x: number; y: number; size: number } > = new Map(); - // Keep track of previous selection box position for cleanup - private lastSelectionBoxCenter: { - x: number; - y: number; - size: number; - } | null = null; - // Visual settings for selection private readonly SELECTION_BOX_SIZE = 6; // Size of the selection box (should be larger than the warship) @@ -71,14 +64,10 @@ export class UILayer implements Layer { } tick() { - // Update the selection animation time + // Update the selection animation time (only used by the multi-selection + // boxes — the single-unit box is now drawn by the WebGL SelectionBoxPass). this.selectionAnimTime = (this.selectionAnimTime + 1) % 60; - // If there's a selected warship, redraw to update the selection box animation - if (this.selectedUnit && this.selectedUnit.type() === UnitType.Warship) { - this.drawSelectionBox(this.selectedUnit); - } - // Animate multi-selected warships for (const unit of this.multiSelectedWarships) { if (unit.isActive()) { @@ -354,50 +343,29 @@ export class UILayer implements Layer { * When event.isSelected is false it clears all selection state. */ private onUnitSelection(event: UnitSelectionEvent) { - if (event.isSelected) { - // Always clear single-selection outline first - if (this.lastSelectionBoxCenter) { - const { x, y, size } = this.lastSelectionBoxCenter; - this.clearSelectionBox(x, y, size); - this.lastSelectionBoxCenter = null; - } - // selectedUnit is always reset regardless of lastSelectionBoxCenter - this.selectedUnit = null; - // Always clear previous multi-selection boxes - for (const [, center] of this.multiSelectionBoxCenters) { - this.clearSelectionBox(center.x, center.y, center.size); - } - this.multiSelectionBoxCenters.clear(); - this.multiSelectedWarships = []; + // Clear previous multi-selection boxes (the single-unit box is now drawn + // by the WebGL SelectionBoxPass — see ClientGameRunner.mountWebGLDebugRenderer + // which forwards this event to view.setSelectedUnit). + for (const [, center] of this.multiSelectionBoxCenters) { + this.clearSelectionBox(center.x, center.y, center.size); + } + this.multiSelectionBoxCenters.clear(); + this.multiSelectedWarships = []; + this.selectedUnit = null; - if ((event.units ?? []).length > 0) { - // Multi-selection - this.multiSelectedWarships = event.units; - for (const unit of this.multiSelectedWarships) { - if (unit.isActive()) { - this.drawSelectionBoxMulti(unit); - } - } - } else { - // Single selection - this.selectedUnit = event.unit; - if (event.unit && event.unit.type() === UnitType.Warship) { - this.drawSelectionBox(event.unit); + if (!event.isSelected) return; + + if ((event.units ?? []).length > 0) { + // Multi-selection — canvas2D draws the per-unit outlines. + this.multiSelectedWarships = event.units; + for (const unit of this.multiSelectedWarships) { + if (unit.isActive()) { + this.drawSelectionBoxMulti(unit); } } } else { - // Deselect everything - if (this.lastSelectionBoxCenter) { - const { x, y, size } = this.lastSelectionBoxCenter; - this.clearSelectionBox(x, y, size); - this.lastSelectionBoxCenter = null; - } - this.selectedUnit = null; - for (const [, center] of this.multiSelectionBoxCenters) { - this.clearSelectionBox(center.x, center.y, center.size); - } - this.multiSelectionBoxCenters.clear(); - this.multiSelectedWarships = []; + // Single selection — state only; WebGL draws the box. + this.selectedUnit = event.unit; } } @@ -471,41 +439,6 @@ export class UILayer implements Layer { } } - /** - * Draw a selection box around the given unit - */ - public drawSelectionBox(unit: UnitView) { - if (!unit || !unit.isActive()) { - return; - } - - if (this.theme === null) throw new Error("missing theme"); - const selectionColor = unit.owner().territoryColor().lighten(0.2); - const centerX = this.game.x(unit.tile()); - const centerY = this.game.y(unit.tile()); - - // Clear previous box if unit moved - if ( - this.lastSelectionBoxCenter && - (this.lastSelectionBoxCenter.x !== centerX || - this.lastSelectionBoxCenter.y !== centerY) - ) { - this.clearSelectionBox( - this.lastSelectionBoxCenter.x, - this.lastSelectionBoxCenter.y, - this.lastSelectionBoxCenter.size, - ); - } - - this.paintSelectionBoxAt(centerX, centerY, selectionColor); - - this.lastSelectionBoxCenter = { - x: centerX, - y: centerY, - size: this.SELECTION_BOX_SIZE, - }; - } - paintCell(x: number, y: number, color: Colord, alpha: number) { if (this.context === null) throw new Error("null context"); this.clearCell(x, y); diff --git a/tests/client/graphics/UILayer.test.ts b/tests/client/graphics/UILayer.test.ts index 7ada98699..63d0501a3 100644 --- a/tests/client/graphics/UILayer.test.ts +++ b/tests/client/graphics/UILayer.test.ts @@ -36,7 +36,7 @@ describe("UILayer", () => { expect(ui["context"]).not.toBeNull(); }); - it("should handle unit selection event", () => { + it("tracks the selected unit on single-unit selection (rendering is WebGL)", () => { const ui = new UILayer(game, eventBus, transformHandler); ui.redraw(); const unit = { @@ -46,8 +46,27 @@ describe("UILayer", () => { owner: () => ({}), }; const event = { isSelected: true, unit }; - ui.drawSelectionBox = vi.fn(); ui["onUnitSelection"](event as UnitSelectionEvent); - expect(ui.drawSelectionBox).toHaveBeenCalledWith(unit); + // selectedUnit is held for game-logic callers (the click handlers). The + // visual selection box is now drawn by WebGL SelectionBoxPass — wired + // from ClientGameRunner via view.setSelectedUnit(unit.id()). + expect(ui["selectedUnit"]).toBe(unit); + }); + + it("clears selection on deselect", () => { + const ui = new UILayer(game, eventBus, transformHandler); + ui.redraw(); + const unit = { + type: () => "Warship", + isActive: () => true, + tile: () => ({}), + owner: () => ({}), + }; + ui["onUnitSelection"]({ isSelected: true, unit } as UnitSelectionEvent); + ui["onUnitSelection"]({ + isSelected: false, + unit: null, + } as unknown as UnitSelectionEvent); + expect(ui["selectedUnit"]).toBeNull(); }); });