rename UILayer/StructureIconsLayer to controllers, move to src/client/controllers/

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.
This commit is contained in:
evanpelle
2026-05-16 22:45:02 -07:00
parent bac29448c2
commit a708a8c984
5 changed files with 43 additions and 49 deletions
@@ -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<number> = new Set();
@@ -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;
+4 -7
View File
@@ -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,
@@ -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);
@@ -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 },