mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
move multi-unit warship selection box to WebGL SelectionBoxPass
SelectionBoxPass now stores an array of selections and renders one quad per entry. GPURenderer gains setSelectedUnits(ids) — the single-unit setSelectedUnit becomes a wrapper. Position + color are rebuilt each frame from lastUnits; dead unit IDs get pruned in place. ClientGameRunner's UnitSelectionEvent listener forwards both single and multi to view.setSelectedUnits — no more single/multi split. UILayer drops everything canvas2D-related: the offscreen canvas + context, theme, selectionAnimTime, multiSelectionBoxCenters, SELECTION_BOX_SIZE, drawSelectionBoxMulti, paintSelectionBoxAt, clearSelectionBox, paintCell, clearCell, and renderLayer / redraw / shouldTransform. tick() now only prunes destroyed warships from the selection list; the layer is purely state + click handling. ~120 LOC gone. Tests: UILayer.test.ts updated — drops the canvas/redraw asserts, adds a multi-selection state assertion.
This commit is contained in:
@@ -346,21 +346,19 @@ 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.
|
||||
// Warship selection boxes: forward UnitSelectionEvent to the renderer's
|
||||
// SelectionBoxPass for both single and multi selections.
|
||||
eventBus.on(UnitSelectionEvent, (e) => {
|
||||
if (!e.isSelected) {
|
||||
view.setSelectedUnit(null);
|
||||
view.setSelectedUnits([]);
|
||||
return;
|
||||
}
|
||||
if ((e.units ?? []).length > 0) {
|
||||
// Multi-selection: drop any prior single highlight; canvas2D draws
|
||||
// the multi outlines in UILayer.
|
||||
view.setSelectedUnit(null);
|
||||
const multi = e.units ?? [];
|
||||
if (multi.length > 0) {
|
||||
view.setSelectedUnits(multi.map((u) => u.id()));
|
||||
return;
|
||||
}
|
||||
view.setSelectedUnit(e.unit?.id() ?? null);
|
||||
view.setSelectedUnits(e.unit ? [e.unit.id()] : []);
|
||||
});
|
||||
|
||||
return { builder: new WebGLFrameBuilder(view), syncCamera };
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Colord } from "colord";
|
||||
import { Theme } from "src/core/configuration/Theme";
|
||||
import { Cell } from "src/core/game/Game";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UnitType } from "../../../core/game/Game";
|
||||
@@ -23,65 +21,35 @@ import { Layer } from "./Layer";
|
||||
const WARSHIP_SELECTION_RADIUS = 10;
|
||||
|
||||
/**
|
||||
* Layer responsible for drawing UI elements that overlay the game.
|
||||
* Currently: warship selection boxes + drag-rectangle selection.
|
||||
* Health/progress bars are now drawn by the WebGL BarPass.
|
||||
* Layer responsible for warship selection state + click handling.
|
||||
*
|
||||
* Drawing for selection boxes (single + multi) lives in the WebGL
|
||||
* SelectionBoxPass; the drag-rectangle preview is a screen-space DOM
|
||||
* overlay (dragRectEl). This layer does not draw to canvas2D at all —
|
||||
* it stays in the Layer list for lifecycle hooks (init / tick / event
|
||||
* subscriptions).
|
||||
*/
|
||||
export class UILayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private context: CanvasRenderingContext2D | null;
|
||||
private theme: Theme | null = null;
|
||||
private selectionAnimTime = 0;
|
||||
// Keep track of currently selected unit
|
||||
// Currently selected single warship (game-logic readers use this; the
|
||||
// visual is drawn by WebGL SelectionBoxPass).
|
||||
private selectedUnit: UnitView | null = null;
|
||||
|
||||
// Keep track of multi-selected warships (box selection)
|
||||
// Currently multi-selected warships (shift+drag box select).
|
||||
private multiSelectedWarships: UnitView[] = [];
|
||||
|
||||
// Per-unit last selection box position for multi-select cleanup
|
||||
private multiSelectionBoxCenters: Map<
|
||||
number,
|
||||
{ x: number; y: number; size: number }
|
||||
> = new Map();
|
||||
|
||||
// Visual settings for selection
|
||||
private readonly SELECTION_BOX_SIZE = 6; // Size of the selection box (should be larger than the warship)
|
||||
|
||||
// Drag rectangle (shift+drag warship selection box) — a screen-space DOM
|
||||
// overlay positioned via inline style. Not part of the canvas2D draw path.
|
||||
// overlay positioned via inline style.
|
||||
private dragRectEl: HTMLDivElement | null = null;
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
private transformHandler: TransformHandler,
|
||||
) {
|
||||
this.theme = game.config().theme();
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
) {}
|
||||
|
||||
tick() {
|
||||
// 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;
|
||||
|
||||
// Animate multi-selected warships
|
||||
for (const unit of this.multiSelectedWarships) {
|
||||
if (unit.isActive()) {
|
||||
this.drawSelectionBoxMulti(unit);
|
||||
} else {
|
||||
// Unit was destroyed — clean up its box
|
||||
const prev = this.multiSelectionBoxCenters.get(unit.id());
|
||||
if (prev) {
|
||||
this.clearSelectionBox(prev.x, prev.y, prev.size);
|
||||
this.multiSelectionBoxCenters.delete(unit.id());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove destroyed units from the list
|
||||
// Prune any destroyed warships from the multi-selection so callers
|
||||
// (move-warship intent) don't try to act on dead units. The WebGL
|
||||
// SelectionBoxPass also drops them automatically.
|
||||
this.multiSelectedWarships = this.multiSelectedWarships.filter((u) =>
|
||||
u.isActive(),
|
||||
);
|
||||
@@ -106,8 +74,6 @@ export class UILayer implements Layer {
|
||||
this.onSelectionBoxComplete(e),
|
||||
);
|
||||
this.eventBus.on(SelectAllWarshipsEvent, () => this.onSelectAllWarships());
|
||||
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,23 +285,6 @@ export class UILayer implements Layer {
|
||||
this.eventBus.emit(new UnitSelectionEvent(null, true, allWarships));
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
}
|
||||
|
||||
redraw() {
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.context = this.canvas.getContext("2d");
|
||||
this.canvas.width = this.game.width();
|
||||
this.canvas.height = this.game.height();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the unit selection event (single or multi).
|
||||
* When event.units.length > 0 it's a multi-selection from box/select-all.
|
||||
@@ -343,111 +292,17 @@ export class UILayer implements Layer {
|
||||
* When event.isSelected is false it clears all selection state.
|
||||
*/
|
||||
private onUnitSelection(event: UnitSelectionEvent) {
|
||||
// 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();
|
||||
// Selection box visuals are drawn by the WebGL SelectionBoxPass; this
|
||||
// method just tracks selection state for the click-handler logic.
|
||||
this.multiSelectedWarships = [];
|
||||
this.selectedUnit = null;
|
||||
|
||||
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 {
|
||||
// Single selection — state only; WebGL draws the box.
|
||||
this.selectedUnit = event.unit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw selection box for a multi-selected warship, tracking position per unit id.
|
||||
*/
|
||||
private drawSelectionBoxMulti(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());
|
||||
|
||||
const prev = this.multiSelectionBoxCenters.get(unit.id());
|
||||
if (prev && (prev.x !== centerX || prev.y !== centerY)) {
|
||||
this.clearSelectionBox(prev.x, prev.y, prev.size);
|
||||
}
|
||||
|
||||
this.paintSelectionBoxAt(centerX, centerY, selectionColor);
|
||||
|
||||
this.multiSelectionBoxCenters.set(unit.id(), {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
size: this.SELECTION_BOX_SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared helper: paint the dashed pulsing border pixels for a selection box.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the selection box at a specific position
|
||||
*/
|
||||
private clearSelectionBox(x: number, y: number, size: number) {
|
||||
for (let px = x - size; px <= x + size; px++) {
|
||||
for (let py = y - size; py <= y + size; py++) {
|
||||
if (
|
||||
px === x - size ||
|
||||
px === x + size ||
|
||||
py === y - size ||
|
||||
py === y + size
|
||||
) {
|
||||
this.clearCell(px, py);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paintCell(x: number, y: number, color: Colord, alpha: number) {
|
||||
if (this.context === null) throw new Error("null context");
|
||||
this.clearCell(x, y);
|
||||
this.context.fillStyle = color.alpha(alpha / 255).toRgbString();
|
||||
this.context.fillRect(x, y, 1, 1);
|
||||
}
|
||||
|
||||
clearCell(x: number, y: number) {
|
||||
if (this.context === null) throw new Error("null context");
|
||||
this.context.clearRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +341,11 @@ export class GameView {
|
||||
this.renderer.setSelectedUnit(unitId);
|
||||
}
|
||||
|
||||
/** Set multiple selected units (multi-select). Pass [] to clear. */
|
||||
setSelectedUnits(unitIds: readonly number[]): void {
|
||||
this.renderer.setSelectedUnits(unitIds);
|
||||
}
|
||||
|
||||
/** Flash converging-chevron animation at a warship move target. */
|
||||
showMoveIndicator(tileX: number, tileY: number, ownerID: number): void {
|
||||
this.renderer.showMoveIndicator(tileX, tileY, ownerID);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* SelectionBoxPass — draws a stippled pulsating square border around a
|
||||
* selected warship, matching the game's native UILayer selection box.
|
||||
* SelectionBoxPass — draws stippled pulsating square borders around selected
|
||||
* warships. Supports any number of selections; renders one quad per selection.
|
||||
*
|
||||
* Single quad with tile-space SDF logic in the fragment shader.
|
||||
* Active only when a unit is selected via setSelectedUnit().
|
||||
* For typical use (1-50 selected units) the draw-call overhead is fine; if
|
||||
* this ever becomes hot we could swap to instanced rendering.
|
||||
*/
|
||||
|
||||
import { createProgram } from "../utils/gl-utils";
|
||||
@@ -14,6 +14,14 @@ import vertSrc from "../shaders/selection-box/selection-box.vert.glsl?raw";
|
||||
/** Half-size of the selection box in tiles (matches game's SELECTION_BOX_SIZE). */
|
||||
const HALF_SIZE = 6;
|
||||
|
||||
export interface SelectionEntry {
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
export class SelectionBoxPass {
|
||||
private gl: WebGL2RenderingContext;
|
||||
private program: WebGLProgram;
|
||||
@@ -25,12 +33,8 @@ export class SelectionBoxPass {
|
||||
private uTime: WebGLUniformLocation;
|
||||
private uColor: WebGLUniformLocation;
|
||||
|
||||
private active = false;
|
||||
private centerX = 0;
|
||||
private centerY = 0;
|
||||
private colorR = 1;
|
||||
private colorG = 1;
|
||||
private colorB = 1;
|
||||
/** Reusable buffer of selections — caller mutates via setSelections(). */
|
||||
private readonly selections: SelectionEntry[] = [];
|
||||
|
||||
constructor(gl: WebGL2RenderingContext) {
|
||||
this.gl = gl;
|
||||
@@ -58,8 +62,18 @@ export class SelectionBoxPass {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selection box center and color. Pass active=false to hide.
|
||||
* Replace the set of selections drawn this frame. Call with [] to hide.
|
||||
* Stored by reference — the renderer rebuilds the array each frame from
|
||||
* the current unit positions/colors, so we just swap pointers.
|
||||
*/
|
||||
setSelections(entries: readonly SelectionEntry[]): void {
|
||||
this.selections.length = 0;
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
this.selections.push(entries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Legacy single-selection API kept for callers that haven't migrated. */
|
||||
update(
|
||||
active: boolean,
|
||||
centerX: number,
|
||||
@@ -68,31 +82,33 @@ export class SelectionBoxPass {
|
||||
g: number,
|
||||
b: number,
|
||||
): void {
|
||||
this.active = active;
|
||||
this.centerX = centerX;
|
||||
this.centerY = centerY;
|
||||
this.colorR = r;
|
||||
this.colorG = g;
|
||||
this.colorB = b;
|
||||
this.selections.length = 0;
|
||||
if (active) this.selections.push({ centerX, centerY, r, g, b });
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.active = false;
|
||||
this.selections.length = 0;
|
||||
}
|
||||
|
||||
draw(cameraMatrix: Float32Array, frameTick: number): void {
|
||||
if (!this.active) return;
|
||||
if (this.selections.length === 0) return;
|
||||
|
||||
const gl = this.gl;
|
||||
gl.useProgram(this.program);
|
||||
gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix);
|
||||
gl.uniform2f(this.uCenter, this.centerX, this.centerY);
|
||||
gl.uniform1f(this.uHalfSize, HALF_SIZE);
|
||||
gl.uniform1f(this.uTime, frameTick);
|
||||
gl.uniform3f(this.uColor, this.colorR, this.colorG, this.colorB);
|
||||
|
||||
gl.bindVertexArray(this.vao);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
|
||||
// One draw call per selection — for the typical N=1..50, this is cheap.
|
||||
// (If profiling ever shows it matters, swap to instanced rendering with a
|
||||
// small per-instance VBO of {centerX, centerY, r, g, b}.)
|
||||
for (let i = 0; i < this.selections.length; i++) {
|
||||
const s = this.selections[i];
|
||||
gl.uniform2f(this.uCenter, s.centerX, s.centerY);
|
||||
gl.uniform3f(this.uColor, s.r, s.g, s.b);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
@@ -164,8 +164,11 @@ export class GPURenderer {
|
||||
private samGhostVisible = false;
|
||||
private samHighlightVisible = false;
|
||||
|
||||
// Warship selection
|
||||
private selectedUnitId: number | null = null;
|
||||
// Warship selection — supports any number of selections.
|
||||
private selectedUnitIds: number[] = [];
|
||||
/** Reusable scratch buffer of {x,y,r,g,b} for the selection-box pass. */
|
||||
private readonly selectionBoxEntries: import("./passes/selection-box-pass").SelectionEntry[] =
|
||||
[];
|
||||
|
||||
constructor(
|
||||
canvas: HTMLCanvasElement,
|
||||
@@ -884,40 +887,56 @@ export class GPURenderer {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
setSelectedUnit(unitId: number | null): void {
|
||||
this.selectedUnitId = unitId;
|
||||
if (unitId === null) {
|
||||
this.setSelectedUnits(unitId === null ? [] : [unitId]);
|
||||
}
|
||||
|
||||
setSelectedUnits(unitIds: readonly number[]): void {
|
||||
// Copy in (callers may mutate their array).
|
||||
this.selectedUnitIds.length = 0;
|
||||
for (let i = 0; i < unitIds.length; i++) {
|
||||
this.selectedUnitIds.push(unitIds[i]);
|
||||
}
|
||||
if (this.selectedUnitIds.length === 0) {
|
||||
this.selectionBoxPass.hide();
|
||||
}
|
||||
// Position + color are updated each frame in draw() from lastUnits.
|
||||
// Position + color are rebuilt each frame in updateSelectionBox() from
|
||||
// lastUnits — dead units get dropped automatically.
|
||||
}
|
||||
|
||||
private updateSelectionBox(): void {
|
||||
if (this.selectedUnitId === null) return;
|
||||
const unit = this.lastUnits.get(this.selectedUnitId);
|
||||
if (!unit || !unit.isActive) {
|
||||
this.selectedUnitId = null;
|
||||
this.selectionBoxPass.hide();
|
||||
return;
|
||||
if (this.selectedUnitIds.length === 0) return;
|
||||
|
||||
// Build the entries for this frame and prune dead unit IDs in place.
|
||||
const entries = this.selectionBoxEntries;
|
||||
entries.length = 0;
|
||||
let writeIdx = 0;
|
||||
for (let i = 0; i < this.selectedUnitIds.length; i++) {
|
||||
const id = this.selectedUnitIds[i];
|
||||
const unit = this.lastUnits.get(id);
|
||||
if (!unit || !unit.isActive) continue; // dead — drop
|
||||
this.selectedUnitIds[writeIdx++] = id;
|
||||
|
||||
const centerX = unit.pos % this.mapW;
|
||||
const centerY = Math.floor(unit.pos / this.mapW);
|
||||
// Lighten the owner's territory color by ~20% (mix toward white).
|
||||
const off = unit.ownerID * 4;
|
||||
const r = Math.min(
|
||||
1,
|
||||
this.paletteData[off] + (1 - this.paletteData[off]) * 0.3,
|
||||
);
|
||||
const g = Math.min(
|
||||
1,
|
||||
this.paletteData[off + 1] + (1 - this.paletteData[off + 1]) * 0.3,
|
||||
);
|
||||
const b = Math.min(
|
||||
1,
|
||||
this.paletteData[off + 2] + (1 - this.paletteData[off + 2]) * 0.3,
|
||||
);
|
||||
entries.push({ centerX, centerY, r, g, b });
|
||||
}
|
||||
const x = unit.pos % this.mapW;
|
||||
const y = Math.floor(unit.pos / this.mapW);
|
||||
this.selectedUnitIds.length = writeIdx;
|
||||
|
||||
// Lighten the owner's territory color by ~20% (mix toward white)
|
||||
const off = unit.ownerID * 4;
|
||||
const lr = Math.min(
|
||||
1,
|
||||
this.paletteData[off] + (1 - this.paletteData[off]) * 0.3,
|
||||
);
|
||||
const lg = Math.min(
|
||||
1,
|
||||
this.paletteData[off + 1] + (1 - this.paletteData[off + 1]) * 0.3,
|
||||
);
|
||||
const lb = Math.min(
|
||||
1,
|
||||
this.paletteData[off + 2] + (1 - this.paletteData[off + 2]) * 0.3,
|
||||
);
|
||||
|
||||
this.selectionBoxPass.update(true, x, y, lr, lg, lb);
|
||||
this.selectionBoxPass.setSelections(entries);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -28,17 +28,8 @@ describe("UILayer", () => {
|
||||
transformHandler = {};
|
||||
});
|
||||
|
||||
it("should initialize and redraw canvas", () => {
|
||||
const ui = new UILayer(game, eventBus, transformHandler);
|
||||
ui.redraw();
|
||||
expect(ui["canvas"].width).toBe(100);
|
||||
expect(ui["canvas"].height).toBe(100);
|
||||
expect(ui["context"]).not.toBeNull();
|
||||
});
|
||||
|
||||
it("tracks the selected unit on single-unit selection (rendering is WebGL)", () => {
|
||||
const ui = new UILayer(game, eventBus, transformHandler);
|
||||
ui.redraw();
|
||||
const unit = {
|
||||
type: () => "Warship",
|
||||
isActive: () => true,
|
||||
@@ -48,14 +39,13 @@ describe("UILayer", () => {
|
||||
const event = { isSelected: true, unit };
|
||||
ui["onUnitSelection"](event as UnitSelectionEvent);
|
||||
// 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()).
|
||||
// visual selection box is drawn by WebGL SelectionBoxPass — wired from
|
||||
// ClientGameRunner via view.setSelectedUnits([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,
|
||||
@@ -69,4 +59,19 @@ describe("UILayer", () => {
|
||||
} as unknown as UnitSelectionEvent);
|
||||
expect(ui["selectedUnit"]).toBeNull();
|
||||
});
|
||||
|
||||
it("tracks multi-selection list", () => {
|
||||
const ui = new UILayer(game, eventBus, transformHandler);
|
||||
const units = [
|
||||
{ id: () => 1, isActive: () => true },
|
||||
{ id: () => 2, isActive: () => true },
|
||||
];
|
||||
ui["onUnitSelection"]({
|
||||
isSelected: true,
|
||||
unit: null,
|
||||
units,
|
||||
} as unknown as UnitSelectionEvent);
|
||||
expect(ui["multiSelectedWarships"]).toEqual(units);
|
||||
expect(ui["selectedUnit"]).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user