mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
controllers push to the WebGL view directly, drop ClientGameRunner relays
BuildPreviewController and WarshipSelectionController now take the WebGL view in their constructor and call view.updateGhostPreview / view.setSelectedUnits themselves instead of emitting bus events that ClientGameRunner forwarded. Splits the old mountWebGLDebugRenderer in two — createWebGLView builds the view up front so the renderer can wire controllers to it, mountWebGLDebugRenderer does the per-frame plumbing after the transformHandler exists. GhostPreviewUpdatedEvent had no remaining consumers and is removed.
This commit is contained in:
@@ -39,12 +39,10 @@ import {
|
||||
DoGroundAttackEvent,
|
||||
DoRequestAllianceEvent,
|
||||
DoRetaliateAttackEvent,
|
||||
GhostPreviewUpdatedEvent,
|
||||
InputHandler,
|
||||
MouseMoveEvent,
|
||||
MouseUpEvent,
|
||||
TickMetricsEvent,
|
||||
UnitSelectionEvent,
|
||||
} from "./InputHandler";
|
||||
import { endGame, startGame, startTime } from "./LocalPersistantStats";
|
||||
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
|
||||
@@ -231,12 +229,13 @@ export function joinLobby(
|
||||
};
|
||||
}
|
||||
|
||||
function mountWebGLDebugRenderer(
|
||||
terrainMap: TerrainMapData,
|
||||
transformHandler: import("./graphics/TransformHandler").TransformHandler,
|
||||
gameView: GameView,
|
||||
eventBus: EventBus,
|
||||
): { builder: WebGLFrameBuilder } {
|
||||
// Build the WebGL view + its glCanvas. Must run before createRenderer so the
|
||||
// controllers can be wired directly to the view.
|
||||
function createWebGLView(terrainMap: TerrainMapData): {
|
||||
view: WebGLGameView;
|
||||
glCanvas: HTMLCanvasElement;
|
||||
cachedWebGLFrameCallback: { current: FrameRequestCallback | null };
|
||||
} {
|
||||
const gameMap = terrainMap.gameMap;
|
||||
const mapWidth = gameMap.width();
|
||||
const mapHeight = gameMap.height();
|
||||
@@ -259,13 +258,15 @@ function mountWebGLDebugRenderer(
|
||||
// because its RAF fires before canvas2D's RAF (which would have synced the
|
||||
// camera). Driving WebGL's draw synchronously from canvas2D's onPreRender
|
||||
// hook locks them to the same frame.
|
||||
let cachedWebGLFrameCallback: FrameRequestCallback | null = null;
|
||||
const cachedWebGLFrameCallback: { current: FrameRequestCallback | null } = {
|
||||
current: null,
|
||||
};
|
||||
const captureRaf = (cb: FrameRequestCallback): number => {
|
||||
cachedWebGLFrameCallback = cb;
|
||||
cachedWebGLFrameCallback.current = cb;
|
||||
return 0;
|
||||
};
|
||||
const captureCaf = (_id: number): void => {
|
||||
cachedWebGLFrameCallback = null;
|
||||
cachedWebGLFrameCallback.current = null;
|
||||
};
|
||||
|
||||
const palette = new Float32Array(4096 * 2 * 4);
|
||||
@@ -288,6 +289,24 @@ function mountWebGLDebugRenderer(
|
||||
captureCaf,
|
||||
);
|
||||
|
||||
(window as unknown as { __webglView?: unknown }).__webglView = view;
|
||||
|
||||
return { view, glCanvas, cachedWebGLFrameCallback };
|
||||
}
|
||||
|
||||
function mountWebGLDebugRenderer(
|
||||
terrainMap: TerrainMapData,
|
||||
view: WebGLGameView,
|
||||
glCanvas: HTMLCanvasElement,
|
||||
cachedWebGLFrameCallback: { current: FrameRequestCallback | null },
|
||||
transformHandler: import("./graphics/TransformHandler").TransformHandler,
|
||||
gameView: GameView,
|
||||
eventBus: EventBus,
|
||||
): { builder: WebGLFrameBuilder } {
|
||||
const gameMap = terrainMap.gameMap;
|
||||
const mapWidth = gameMap.width();
|
||||
const mapHeight = gameMap.height();
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key === "\\") {
|
||||
glCanvas.style.display =
|
||||
@@ -326,13 +345,11 @@ function mountWebGLDebugRenderer(
|
||||
// Invoke the WebGL renderer's frame callback synchronously, with the just-
|
||||
// updated camera state. The callback re-arms itself via captureRaf, so
|
||||
// we'll get a fresh callback ready for the next canvas2D frame.
|
||||
const cb = cachedWebGLFrameCallback;
|
||||
cachedWebGLFrameCallback = null;
|
||||
const cb = cachedWebGLFrameCallback.current;
|
||||
cachedWebGLFrameCallback.current = null;
|
||||
cb?.(performance.now());
|
||||
};
|
||||
|
||||
(window as unknown as { __webglView?: unknown }).__webglView = view;
|
||||
|
||||
// Move-target chevrons: when the player issues a warship move, show the
|
||||
// animated chevron pass at the target tile. The renderer needs the target's
|
||||
// tile x/y and the warship's owner smallID (so the chevrons use the right
|
||||
@@ -347,27 +364,6 @@ function mountWebGLDebugRenderer(
|
||||
view.showMoveIndicator(tx, ty, firstUnit.owner().smallID());
|
||||
});
|
||||
|
||||
// Build-mode ghost preview: forward the per-frame state to the renderer's
|
||||
// ghost passes (structure outline, range circle, rail snap, crosshair).
|
||||
eventBus.on(GhostPreviewUpdatedEvent, (e) => {
|
||||
view.updateGhostPreview(e.data);
|
||||
});
|
||||
|
||||
// Warship selection boxes: forward UnitSelectionEvent to the renderer's
|
||||
// SelectionBoxPass for both single and multi selections.
|
||||
eventBus.on(UnitSelectionEvent, (e) => {
|
||||
if (!e.isSelected) {
|
||||
view.setSelectedUnits([]);
|
||||
return;
|
||||
}
|
||||
const multi = e.units ?? [];
|
||||
if (multi.length > 0) {
|
||||
view.setSelectedUnits(multi.map((u) => u.id()));
|
||||
return;
|
||||
}
|
||||
view.setSelectedUnits(e.unit ? [e.unit.id()] : []);
|
||||
});
|
||||
|
||||
// Self-driving RAF: syncCamera reads the latest camera state from
|
||||
// TransformHandler, pushes it to WebGL, and synchronously invokes the
|
||||
// renderer's captured frame callback (which draws). One RAF = one
|
||||
@@ -437,15 +433,22 @@ async function createClientGame(
|
||||
|
||||
const soundManager = new SoundManager(eventBus, userSettings);
|
||||
try {
|
||||
const { view, glCanvas, cachedWebGLFrameCallback } =
|
||||
createWebGLView(gameMap);
|
||||
|
||||
const gameRenderer = createRenderer(
|
||||
inputOverlay,
|
||||
gameView,
|
||||
eventBus,
|
||||
lobbyConfig.playerRole,
|
||||
view,
|
||||
);
|
||||
|
||||
const { builder: webglBuilder } = mountWebGLDebugRenderer(
|
||||
gameMap,
|
||||
view,
|
||||
glCanvas,
|
||||
cachedWebGLFrameCallback,
|
||||
gameRenderer.transformHandler,
|
||||
gameView,
|
||||
eventBus,
|
||||
|
||||
@@ -96,17 +96,6 @@ export class GhostStructureChangedEvent implements GameEvent {
|
||||
constructor(public readonly ghostStructure: PlayerBuildableUnitType | null) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-frame ghost preview state for the WebGL renderer. Emitted by the
|
||||
* canvas2D ghost layer; consumed in ClientGameRunner.mountWebGLDebugRenderer
|
||||
* to push to view.updateGhostPreview. `data` is null when no ghost is active.
|
||||
*/
|
||||
export class GhostPreviewUpdatedEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly data: import("./render/types").GhostPreviewData | null,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class ConfirmGhostStructureEvent implements GameEvent {}
|
||||
|
||||
export class SwapRocketDirectionEvent implements GameEvent {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* All rendering for the build ghost (outline, range circle, rail snap,
|
||||
* 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.
|
||||
* is valid, and pushes preview data straight to the WebGL view.
|
||||
*/
|
||||
|
||||
import { EventBus } from "../../core/EventBus";
|
||||
@@ -21,11 +21,11 @@ import { TransformHandler } from "../graphics/TransformHandler";
|
||||
import { UIState } from "../graphics/UIState";
|
||||
import {
|
||||
ConfirmGhostStructureEvent,
|
||||
GhostPreviewUpdatedEvent,
|
||||
GhostStructureChangedEvent,
|
||||
MouseMoveEvent,
|
||||
MouseUpEvent,
|
||||
} from "../InputHandler";
|
||||
import { GameView as WebGLGameView } from "../render/gl";
|
||||
import type { GhostPreviewData } from "../render/types";
|
||||
import {
|
||||
BuildUnitIntentEvent,
|
||||
@@ -50,6 +50,7 @@ export class BuildPreviewController implements Controller {
|
||||
private eventBus: EventBus,
|
||||
public uiState: UIState,
|
||||
private transformHandler: TransformHandler,
|
||||
private view: WebGLGameView,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
@@ -183,15 +184,12 @@ export class BuildPreviewController implements Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a GhostPreviewData snapshot from the current ghost state and emit
|
||||
* it for the WebGL renderer to consume (StructurePass / RangeCirclePass /
|
||||
* RailroadPass / CrosshairPass all read it via view.updateGhostPreview).
|
||||
* Emits null when the ghost can't be placed.
|
||||
* Push a GhostPreviewData snapshot to the WebGL view (StructurePass /
|
||||
* RangeCirclePass / RailroadPass / CrosshairPass all read it). null when
|
||||
* the ghost can't be placed.
|
||||
*/
|
||||
private emitGhostPreview(tileRef: TileRef | undefined): void {
|
||||
this.eventBus.emit(
|
||||
new GhostPreviewUpdatedEvent(this.buildGhostPreviewData(tileRef)),
|
||||
);
|
||||
this.view.updateGhostPreview(this.buildGhostPreviewData(tileRef));
|
||||
}
|
||||
|
||||
private buildGhostPreviewData(
|
||||
@@ -310,7 +308,7 @@ export class BuildPreviewController implements Controller {
|
||||
this.pendingConfirm = null;
|
||||
this.ghostUnit = null;
|
||||
this.uiState.ghostRailPaths = [];
|
||||
this.eventBus.emit(new GhostPreviewUpdatedEvent(null));
|
||||
this.view.updateGhostPreview(null);
|
||||
}
|
||||
|
||||
private removeGhostStructure() {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
WarshipSelectionBoxCompleteEvent,
|
||||
WarshipSelectionBoxUpdateEvent,
|
||||
} from "../InputHandler";
|
||||
import { GameView as WebGLGameView } from "../render/gl";
|
||||
import { MoveWarshipIntentEvent } from "../Transport";
|
||||
|
||||
const WARSHIP_SELECTION_RADIUS = 10;
|
||||
@@ -47,6 +48,7 @@ export class WarshipSelectionController implements Controller {
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
private transformHandler: TransformHandler,
|
||||
private view: WebGLGameView,
|
||||
) {}
|
||||
|
||||
tick() {
|
||||
@@ -295,17 +297,20 @@ export class WarshipSelectionController implements Controller {
|
||||
* When event.isSelected is false it clears all selection state.
|
||||
*/
|
||||
private onUnitSelection(event: UnitSelectionEvent) {
|
||||
// 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.isSelected) {
|
||||
this.view.setSelectedUnits([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event.units ?? []).length > 0) {
|
||||
this.multiSelectedWarships = event.units;
|
||||
this.view.setSelectedUnits(event.units.map((u) => u.id()));
|
||||
} else {
|
||||
this.selectedUnit = event.unit;
|
||||
this.view.setSelectedUnits(event.unit ? [event.unit.id()] : []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { UserSettings } from "../../core/game/UserSettings";
|
||||
import { GameStartingModal } from "../GameStartingModal";
|
||||
import { BuildPreviewController } from "../controllers/BuildPreviewController";
|
||||
import { WarshipSelectionController } from "../controllers/WarshipSelectionController";
|
||||
import { GameView as WebGLGameView } from "../render/gl";
|
||||
import { FrameProfiler } from "./FrameProfiler";
|
||||
import { TransformHandler } from "./TransformHandler";
|
||||
import { UIState } from "./UIState";
|
||||
@@ -40,6 +41,7 @@ export function createRenderer(
|
||||
game: GameView,
|
||||
eventBus: EventBus,
|
||||
playerRole: string | null,
|
||||
view: WebGLGameView,
|
||||
): GameRenderer {
|
||||
const transformHandler = new TransformHandler(game, eventBus, inputEl);
|
||||
const userSettings = new UserSettings();
|
||||
@@ -257,8 +259,8 @@ export function createRenderer(
|
||||
inGamePromo.game = game;
|
||||
|
||||
const layers: Controller[] = [
|
||||
new WarshipSelectionController(game, eventBus, transformHandler),
|
||||
new BuildPreviewController(game, eventBus, uiState, transformHandler),
|
||||
new WarshipSelectionController(game, eventBus, transformHandler, view),
|
||||
new BuildPreviewController(game, eventBus, uiState, transformHandler, view),
|
||||
new AttackingTroopsOverlay(game, transformHandler, eventBus, userSettings),
|
||||
eventsDisplay,
|
||||
attacksDisplay,
|
||||
|
||||
@@ -5,6 +5,7 @@ describe("WarshipSelectionController", () => {
|
||||
let game: any;
|
||||
let eventBus: any;
|
||||
let transformHandler: any;
|
||||
let view: any;
|
||||
|
||||
beforeEach(() => {
|
||||
game = {
|
||||
@@ -26,11 +27,18 @@ describe("WarshipSelectionController", () => {
|
||||
};
|
||||
eventBus = { on: vi.fn() };
|
||||
transformHandler = {};
|
||||
view = { setSelectedUnits: vi.fn() };
|
||||
});
|
||||
|
||||
it("tracks the selected unit on single-unit selection (rendering is WebGL)", () => {
|
||||
const ui = new WarshipSelectionController(game, eventBus, transformHandler);
|
||||
const ui = new WarshipSelectionController(
|
||||
game,
|
||||
eventBus,
|
||||
transformHandler,
|
||||
view,
|
||||
);
|
||||
const unit = {
|
||||
id: () => 1,
|
||||
type: () => "Warship",
|
||||
isActive: () => true,
|
||||
tile: () => ({}),
|
||||
@@ -45,8 +53,14 @@ describe("WarshipSelectionController", () => {
|
||||
});
|
||||
|
||||
it("clears selection on deselect", () => {
|
||||
const ui = new WarshipSelectionController(game, eventBus, transformHandler);
|
||||
const ui = new WarshipSelectionController(
|
||||
game,
|
||||
eventBus,
|
||||
transformHandler,
|
||||
view,
|
||||
);
|
||||
const unit = {
|
||||
id: () => 1,
|
||||
type: () => "Warship",
|
||||
isActive: () => true,
|
||||
tile: () => ({}),
|
||||
@@ -61,7 +75,12 @@ describe("WarshipSelectionController", () => {
|
||||
});
|
||||
|
||||
it("tracks multi-selection list", () => {
|
||||
const ui = new WarshipSelectionController(game, eventBus, transformHandler);
|
||||
const ui = new WarshipSelectionController(
|
||||
game,
|
||||
eventBus,
|
||||
transformHandler,
|
||||
view,
|
||||
);
|
||||
const units = [
|
||||
{ id: () => 1, isActive: () => true },
|
||||
{ id: () => 2, isActive: () => true },
|
||||
|
||||
Reference in New Issue
Block a user