From a708a8c9847a29f8caf23ac79f3493504522917f Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sat, 16 May 2026 22:45:02 -0700 Subject: [PATCH] rename UILayer/StructureIconsLayer to controllers, move to src/client/controllers/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UILayer → WarshipSelectionController and StructureIconsLayer → BuildPreviewController. These are the two real Controller implementations (state + click handling, no rendering) — the new names + location reflect what they actually do now that all rendering lives in WebGL passes. --- .../BuildPreviewController.ts} | 28 ++++++++--------- .../WarshipSelectionController.ts} | 31 ++++++++++--------- src/client/graphics/GameRenderer.ts | 11 +++---- .../BuildPreviewController.test.ts} | 12 ++----- .../WarshipSelectionController.test.ts} | 10 +++--- 5 files changed, 43 insertions(+), 49 deletions(-) rename src/client/{graphics/layers/StructureIconsLayer.ts => controllers/BuildPreviewController.ts} (93%) rename src/client/{graphics/layers/UILayer.ts => controllers/WarshipSelectionController.ts} (91%) rename tests/client/{graphics/layers/StructureIconsLayer.test.ts => controllers/BuildPreviewController.test.ts} (70%) rename tests/client/{graphics/UILayer.test.ts => controllers/WarshipSelectionController.test.ts} (84%) diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/controllers/BuildPreviewController.ts similarity index 93% rename from src/client/graphics/layers/StructureIconsLayer.ts rename to src/client/controllers/BuildPreviewController.ts index e6df5c49b..ef29d3ca2 100644 --- a/src/client/graphics/layers/StructureIconsLayer.ts +++ b/src/client/controllers/BuildPreviewController.ts @@ -1,43 +1,43 @@ /** - * StructureIconsLayer — build-ghost state machine + click-to-build flow. + * BuildPreviewController — build-ghost state machine + click-to-build flow. * * All rendering for the build ghost (outline, range circle, rail snap, - * crosshair) lives in the WebGL renderer. This layer just owns the state: + * crosshair) lives in the WebGL renderer. This controller owns the state: * it queries buildables for the cursor tile, tracks whether the placement * is valid, and emits GhostPreviewUpdatedEvent to feed the renderer. */ -import { EventBus } from "../../../core/EventBus"; -import { wouldNukeBreakAlliance } from "../../../core/execution/Util"; +import { EventBus } from "../../core/EventBus"; +import { wouldNukeBreakAlliance } from "../../core/execution/Util"; import { BuildableUnit, PlayerBuildableUnitType, UnitType, -} from "../../../core/game/Game"; -import { TileRef } from "../../../core/game/GameMap"; -import { GameView } from "../../../core/game/GameView"; +} from "../../core/game/Game"; +import { TileRef } from "../../core/game/GameMap"; +import { GameView } from "../../core/game/GameView"; +import { Controller } from "../graphics/layers/Controller"; +import { TransformHandler } from "../graphics/TransformHandler"; +import { UIState } from "../graphics/UIState"; import { ConfirmGhostStructureEvent, GhostPreviewUpdatedEvent, GhostStructureChangedEvent, MouseMoveEvent, MouseUpEvent, -} from "../../InputHandler"; -import type { GhostPreviewData } from "../../render/types"; +} from "../InputHandler"; +import type { GhostPreviewData } from "../render/types"; import { BuildUnitIntentEvent, SendUpgradeStructureIntentEvent, -} from "../../Transport"; -import { TransformHandler } from "../TransformHandler"; -import { UIState } from "../UIState"; -import { Controller } from "./Controller"; +} from "../Transport"; /** True for nuke types (AtomBomb, HydrogenBomb): ghost is preserved after placement so user can place multiple or keep selection (Enter/key confirm). */ export function shouldPreserveGhostAfterBuild(unitType: UnitType): boolean { return unitType === UnitType.AtomBomb || unitType === UnitType.HydrogenBomb; } -export class StructureIconsLayer implements Controller { +export class BuildPreviewController implements Controller { /** Current ghost (null when no build type is active). */ private ghostUnit: { buildableUnit: BuildableUnit } | null = null; private readonly connectedAllySmallIds: Set = new Set(); diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/controllers/WarshipSelectionController.ts similarity index 91% rename from src/client/graphics/layers/UILayer.ts rename to src/client/controllers/WarshipSelectionController.ts index f8b67eef7..cdf51d431 100644 --- a/src/client/graphics/layers/UILayer.ts +++ b/src/client/controllers/WarshipSelectionController.ts @@ -1,8 +1,10 @@ import { Cell } from "src/core/game/Game"; -import { EventBus } from "../../../core/EventBus"; -import { UnitType } from "../../../core/game/Game"; -import { TileRef } from "../../../core/game/GameMap"; -import { GameView, UnitView } from "../../../core/game/GameView"; +import { EventBus } from "../../core/EventBus"; +import { UnitType } from "../../core/game/Game"; +import { TileRef } from "../../core/game/GameMap"; +import { GameView, UnitView } from "../../core/game/GameView"; +import { Controller } from "../graphics/layers/Controller"; +import { TransformHandler } from "../graphics/TransformHandler"; import { CloseViewEvent, ContextMenuEvent, @@ -13,23 +15,24 @@ import { WarshipSelectionBoxCancelEvent, WarshipSelectionBoxCompleteEvent, WarshipSelectionBoxUpdateEvent, -} from "../../InputHandler"; -import { MoveWarshipIntentEvent } from "../../Transport"; -import { TransformHandler } from "../TransformHandler"; -import { Controller } from "./Controller"; +} from "../InputHandler"; +import { MoveWarshipIntentEvent } from "../Transport"; const WARSHIP_SELECTION_RADIUS = 10; /** - * Layer responsible for warship selection state + click handling. + * Controller 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). + * SelectionBoxPass (forwarded via UnitSelectionEvent from ClientGameRunner). + * The drag-rectangle preview is a screen-space DOM overlay (dragRectEl) we + * own here. + * + * This class does not render anything to canvas2D — it's purely a state + + * click controller. The "Controller" pattern: main-thread analog of the + * worker's Execution (init + tick + event subscriptions). */ -export class UILayer implements Controller { +export class WarshipSelectionController implements Controller { // Currently selected single warship (game-logic readers use this; the // visual is drawn by WebGL SelectionBoxPass). private selectedUnit: UnitView | null = null; diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index a1aad6d7f..9e14f28bc 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -2,6 +2,8 @@ import { EventBus } from "../../core/EventBus"; import { GameView } from "../../core/game/GameView"; import { UserSettings } from "../../core/game/UserSettings"; import { GameStartingModal } from "../GameStartingModal"; +import { BuildPreviewController } from "../controllers/BuildPreviewController"; +import { WarshipSelectionController } from "../controllers/WarshipSelectionController"; import { FrameProfiler } from "./FrameProfiler"; import { TransformHandler } from "./TransformHandler"; import { UIState } from "./UIState"; @@ -29,9 +31,7 @@ import { PlayerPanel } from "./layers/PlayerPanel"; import { ReplayPanel } from "./layers/ReplayPanel"; import { SettingsModal } from "./layers/SettingsModal"; import { SpawnTimer } from "./layers/SpawnTimer"; -import { StructureIconsLayer } from "./layers/StructureIconsLayer"; import { TeamStats } from "./layers/TeamStats"; -import { UILayer } from "./layers/UILayer"; import { UnitDisplay } from "./layers/UnitDisplay"; import { WinModal } from "./layers/WinModal"; @@ -256,12 +256,9 @@ export function createRenderer( } inGamePromo.game = game; - // When updating these layers please be mindful of the order. - // Try to group layers by the return value of shouldTransform. - // Not grouping the layers may cause excessive calls to context.save() and context.restore(). const layers: Controller[] = [ - new UILayer(game, eventBus, transformHandler), - new StructureIconsLayer(game, eventBus, uiState, transformHandler), + new WarshipSelectionController(game, eventBus, transformHandler), + new BuildPreviewController(game, eventBus, uiState, transformHandler), new AttackingTroopsOverlay(game, transformHandler, eventBus, userSettings), eventsDisplay, attacksDisplay, diff --git a/tests/client/graphics/layers/StructureIconsLayer.test.ts b/tests/client/controllers/BuildPreviewController.test.ts similarity index 70% rename from tests/client/graphics/layers/StructureIconsLayer.test.ts rename to tests/client/controllers/BuildPreviewController.test.ts index 7cb8b557c..3dafc5469 100644 --- a/tests/client/graphics/layers/StructureIconsLayer.test.ts +++ b/tests/client/controllers/BuildPreviewController.test.ts @@ -1,14 +1,8 @@ import { describe, expect, test } from "vitest"; -import { shouldPreserveGhostAfterBuild } from "../../../../src/client/graphics/layers/StructureIconsLayer"; -import { UnitType } from "../../../../src/core/game/Game"; +import { shouldPreserveGhostAfterBuild } from "../../../src/client/controllers/BuildPreviewController"; +import { UnitType } from "../../../src/core/game/Game"; -/** - * Tests for StructureIconsLayer edge cases mentioned in comments: - * - Locked nuke / AtomBomb / HydrogenBomb: when confirming placement (Enter or key), - * the ghost is preserved so the user can place multiple nukes or keep the nuke - * selected. Other structure types clear the ghost after placement. - */ -describe("StructureIconsLayer ghost preservation (locked nuke / Enter confirm)", () => { +describe("BuildPreviewController ghost preservation (locked nuke / Enter confirm)", () => { describe("shouldPreserveGhostAfterBuild", () => { test("returns true for AtomBomb so ghost is not cleared after placement", () => { expect(shouldPreserveGhostAfterBuild(UnitType.AtomBomb)).toBe(true); diff --git a/tests/client/graphics/UILayer.test.ts b/tests/client/controllers/WarshipSelectionController.test.ts similarity index 84% rename from tests/client/graphics/UILayer.test.ts rename to tests/client/controllers/WarshipSelectionController.test.ts index aaafded00..6754d453c 100644 --- a/tests/client/graphics/UILayer.test.ts +++ b/tests/client/controllers/WarshipSelectionController.test.ts @@ -1,7 +1,7 @@ -import { UILayer } from "../../../src/client/graphics/layers/UILayer"; +import { WarshipSelectionController } from "../../../src/client/controllers/WarshipSelectionController"; import { UnitSelectionEvent } from "../../../src/client/InputHandler"; -describe("UILayer", () => { +describe("WarshipSelectionController", () => { let game: any; let eventBus: any; let transformHandler: any; @@ -29,7 +29,7 @@ describe("UILayer", () => { }); it("tracks the selected unit on single-unit selection (rendering is WebGL)", () => { - const ui = new UILayer(game, eventBus, transformHandler); + const ui = new WarshipSelectionController(game, eventBus, transformHandler); const unit = { type: () => "Warship", isActive: () => true, @@ -45,7 +45,7 @@ describe("UILayer", () => { }); it("clears selection on deselect", () => { - const ui = new UILayer(game, eventBus, transformHandler); + const ui = new WarshipSelectionController(game, eventBus, transformHandler); const unit = { type: () => "Warship", isActive: () => true, @@ -61,7 +61,7 @@ describe("UILayer", () => { }); it("tracks multi-selection list", () => { - const ui = new UILayer(game, eventBus, transformHandler); + const ui = new WarshipSelectionController(game, eventBus, transformHandler); const units = [ { id: () => 1, isActive: () => true }, { id: () => 2, isActive: () => true },