mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-21 11:58:42 +00:00
Simplify WebGL renderer integration: remove dead extension code, untangle GameView naming (#4240)
## Summary The WebGL renderer was adapted from an external extension and carried a lot of machinery this integration never uses (replay playback, its own input/event system, a GL radial menu). This PR is two mechanical cleanup passes with **no behavior change**: delete the dead code, then untangle the `GameView` naming collision. **78 files, +142 / −2,197.** ### Pass 1 — remove dead extension baggage - **Replay/copy mode**: `FrameData.tileMode` was hard-coded `"live"`; the copy branches in `frame/Upload.ts`, `UploadOptions` (never passed), `applyFullFrame`/`applyFullTiles`/`applyDelta` on the facade and `GPURenderer`, `HeatManager.resetForSeek`, and the seek-upload methods on `TerritoryPass`/`TrailPass` were all unreachable. Also deletes `types/Replay.ts`, `types/FrameSource.ts`, `types/GameUpdates.ts`, `types/Game.ts` (imported only by the types barrel). - **FrameEvents**: trimmed from 14 fields to the 3 actually populated and read (`deadUnits`, `conquestEvents`, `bonusEvents`). The other 11 fed the extension's stats system and were never written or read here. - **GL radial menu**: `RadialMenuPass`, its 4 shaders, and ~10 API methods on facade + renderer had zero callers — the game uses the DOM/d3 radial menu in `hud/layers/RadialMenu.ts`. The pass was constructed and drawn every frame for nothing. - **Facade event system**: `GameViewEventMap` defined 10 event types (`click`, `hover`, `scroll`, …) but only `contextrestored` was ever emitted — input actually flows through `InputHandler` → EventBus → controllers. Replaced the listener map with a single `onContextRestored` callback and deleted `Events.ts`. Also fixed the stale header comment claiming the facade handles user interaction. - **Unused API surface**: removed ~20 facade/renderer methods with zero callers (camera passthroughs like `panTo`/`zoomTo`/`fitMap`/`screenToWorld`, hit-testing queries, SAM replay setters, `setSelectedUnit`, `clearFx`/`setFxTimeFn`, `onFrame`/`afterRender`/fps tracking). Deliberately left alone: `Camera`'s pan/zoom primitives (building blocks for a possible future camera unification) and the `timeFn` plumbing inside the FX passes (deeply embedded as defaults; only the dead renderer-level wrappers were removed). ### Pass 2 — untangle the three GameViews - `render/gl/GameView.ts` → **`MapRenderer.ts`** (class `MapRenderer`). Every importer was already aliasing it as `WebGLGameView` to dodge the collision with the simulation-mirror `GameView` in `client/view/`, so this removes aliasing rather than adding churn. `render/CLAUDE.md` updated. - Deleted the `src/core/game/GameView.ts` back-compat shim (its own TODO asked for this). All 51 importers now import from `src/client/view/` directly via a new 3-line barrel `view/index.ts`. ## Test plan - `tsc --noEmit` clean, `eslint` clean - Full test suite passes (1,385 + 65 server tests) - Manual verification via headless Chromium: started a singleplayer game and confirmed the renderer works end-to-end — terrain draws, spawn-phase overlay shows, territories fill with borders after spawning, player names/flags render, no renderer console errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,6 @@ import type {
|
||||
UnitState,
|
||||
} from "../types";
|
||||
import { Camera } from "./Camera";
|
||||
import type { RadialMenuItem } from "./Events";
|
||||
import { BarPass } from "./passes/BarPass";
|
||||
import { BorderComputePass } from "./passes/BorderComputePass";
|
||||
import { BorderStampPass } from "./passes/BorderStampPass";
|
||||
@@ -44,7 +43,6 @@ import { NightCompositePass } from "./passes/NightCompositePass";
|
||||
import { NukeTelegraphPass } from "./passes/NukeTelegraphPass";
|
||||
import { NukeTrajectoryPass } from "./passes/NukeTrajectoryPass";
|
||||
import { PointLightPass } from "./passes/PointLightPass";
|
||||
import { RadialMenuPass } from "./passes/RadialMenuPass";
|
||||
import { RailroadPass } from "./passes/RailroadPass";
|
||||
import { RangeCirclePass } from "./passes/RangeCirclePass";
|
||||
import { SAMRadiusPass } from "./passes/SamRadiusPass";
|
||||
@@ -121,7 +119,6 @@ export class GPURenderer {
|
||||
private railroadPass: RailroadPass;
|
||||
private barPass: BarPass;
|
||||
private worldTextPass: WorldTextPass;
|
||||
private radialMenuPass: RadialMenuPass;
|
||||
private selectionBoxPass: SelectionBoxPass;
|
||||
private moveIndicatorPass: MoveIndicatorPass;
|
||||
private nukeTrajectoryPass: NukeTrajectoryPass;
|
||||
@@ -154,15 +151,7 @@ export class GPURenderer {
|
||||
private mapW = 0;
|
||||
private mapH = 0;
|
||||
|
||||
// FPS tracking
|
||||
private frameTimes: Float64Array = new Float64Array(60);
|
||||
private frameIdx = 0;
|
||||
private frameCount = 0;
|
||||
fps = 0;
|
||||
onFrame: ((ms: number) => void) | null = null;
|
||||
afterRender: ((canvas: HTMLCanvasElement) => void) | null = null;
|
||||
|
||||
// Hit-testing references
|
||||
// Last-uploaded unit/structure maps (selection box + bar pass inputs)
|
||||
private lastUnits: Map<number, UnitState> = new Map();
|
||||
private lastStructures: Map<number, UnitState> = new Map();
|
||||
|
||||
@@ -479,7 +468,6 @@ export class GPURenderer {
|
||||
this.barPass = new BarPass(gl, header, this.settings, config);
|
||||
this.worldTextPass = new WorldTextPass(gl, this.settings, config);
|
||||
this.worldTextPass.setMapWidth(this.mapW);
|
||||
this.radialMenuPass = new RadialMenuPass(gl);
|
||||
this.selectionBoxPass = new SelectionBoxPass(gl);
|
||||
this.moveIndicatorPass = new MoveIndicatorPass(gl, this.settings);
|
||||
this.nukeTrajectoryPass = new NukeTrajectoryPass(gl, this.settings);
|
||||
@@ -571,80 +559,14 @@ export class GPURenderer {
|
||||
this.camera.resize(cssWidth, cssHeight);
|
||||
}
|
||||
|
||||
screenToWorld(screenX: number, screenY: number): { x: number; y: number } {
|
||||
return this.camera.screenToWorld(screenX, screenY);
|
||||
}
|
||||
|
||||
worldToScreen(worldX: number, worldY: number): { x: number; y: number } {
|
||||
return this.camera.worldToScreen(worldX, worldY);
|
||||
}
|
||||
|
||||
panTo(worldX: number, worldY: number): void {
|
||||
this.camera.panTo(worldX, worldY);
|
||||
}
|
||||
panBy(dx: number, dy: number): void {
|
||||
this.camera.panBy(dx, dy);
|
||||
}
|
||||
zoomTo(level: number): void {
|
||||
this.camera.zoomTo(level);
|
||||
}
|
||||
zoomBy(factor: number): void {
|
||||
this.camera.zoomBy(factor);
|
||||
}
|
||||
zoomAtScreen(factor: number, screenX: number, screenY: number): void {
|
||||
this.camera.zoomAtScreen(factor, screenX, screenY);
|
||||
}
|
||||
fitMap(): void {
|
||||
this.camera.fitMap();
|
||||
}
|
||||
focusBBox(
|
||||
minX: number,
|
||||
minY: number,
|
||||
maxX: number,
|
||||
maxY: number,
|
||||
padding?: number,
|
||||
): void {
|
||||
this.camera.focusBBox(minX, minY, maxX, maxY, padding);
|
||||
}
|
||||
getCameraState(): { x: number; y: number; z: number } {
|
||||
return {
|
||||
x: this.camera.offsetX,
|
||||
y: this.camera.offsetY,
|
||||
z: this.camera.zoom,
|
||||
};
|
||||
}
|
||||
setCameraState(x: number, y: number, z: number): void {
|
||||
this.camera.setCameraState(x, y, z);
|
||||
}
|
||||
get zoom(): number {
|
||||
return this.camera.zoom;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data upload
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
applyFullFrame(
|
||||
tileState: Uint16Array,
|
||||
trailState: Uint8Array,
|
||||
nukeEvents?: Array<{ tick: number; tiles: number[] }>,
|
||||
currentTick?: number,
|
||||
): void {
|
||||
this.territoryPass.uploadFullTileState(tileState);
|
||||
this.trailPass.uploadFullState(trailState);
|
||||
this.heatManager.resetForSeek(tileState, nukeEvents, currentTick);
|
||||
}
|
||||
|
||||
applyFullTiles(tileState: Uint16Array, trailState: Uint8Array): void {
|
||||
this.territoryPass.uploadFullTileState(tileState);
|
||||
this.trailPass.uploadFullState(trailState);
|
||||
}
|
||||
|
||||
applyDelta(changedTiles: TilePair[], trailState: Uint8Array): void {
|
||||
this.territoryPass.uploadDeltaTiles(changedTiles);
|
||||
this.trailPass.uploadFullState(trailState);
|
||||
}
|
||||
|
||||
uploadTileAndTrailState(
|
||||
tileState: Uint16Array,
|
||||
trailState: Uint8Array,
|
||||
@@ -912,15 +834,6 @@ export class GPURenderer {
|
||||
this.fxPass.updateAttackRings(rings);
|
||||
}
|
||||
|
||||
clearFx(): void {
|
||||
this.fxPass.clear();
|
||||
this.worldTextPass.clear();
|
||||
}
|
||||
setFxTimeFn(fn: () => number): void {
|
||||
this.fxPass.setTimeFn(fn);
|
||||
this.worldTextPass.setTimeFn(fn);
|
||||
}
|
||||
|
||||
updateGhostPreview(data: GhostPreviewData | null): void {
|
||||
this.structurePass.updateGhostPreview(data);
|
||||
this.railroadPass.updateGhostPreview(data);
|
||||
@@ -980,64 +893,6 @@ export class GPURenderer {
|
||||
);
|
||||
}
|
||||
|
||||
focusOwner(ownerID: number): void {
|
||||
if (ownerID !== 0) {
|
||||
const bbox = this.territoryPass.getBBoxForOwner(ownerID);
|
||||
if (bbox) {
|
||||
this.camera.focusBBox(bbox.minX, bbox.minY, bbox.maxX, bbox.maxY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.camera.focusBBox(0, 0, this.mapW - 1, this.mapH - 1);
|
||||
}
|
||||
|
||||
getOwnerAtWorld(worldX: number, worldY: number): number {
|
||||
const tx = Math.floor(worldX);
|
||||
const ty = Math.floor(worldY);
|
||||
if (tx < 0 || ty < 0 || tx >= this.mapW || ty >= this.mapH) return 0;
|
||||
return this.territoryPass.getOwnerAt(ty * this.mapW + tx);
|
||||
}
|
||||
|
||||
getUnitAtWorld(
|
||||
worldX: number,
|
||||
worldY: number,
|
||||
radius: number,
|
||||
): UnitState | null {
|
||||
let best: UnitState | null = null;
|
||||
let bestDist = radius * radius;
|
||||
const w = this.mapW;
|
||||
for (const u of this.lastUnits.values()) {
|
||||
const dx = (u.pos % w) - worldX;
|
||||
const dy = Math.floor(u.pos / w) - worldY;
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 < bestDist) {
|
||||
bestDist = d2;
|
||||
best = u;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
getStructureAtWorld(
|
||||
worldX: number,
|
||||
worldY: number,
|
||||
radius: number,
|
||||
): UnitState | null {
|
||||
let best: UnitState | null = null;
|
||||
let bestDist = radius * radius;
|
||||
const w = this.mapW;
|
||||
for (const s of this.lastStructures.values()) {
|
||||
const dx = (s.pos % w) - worldX;
|
||||
const dy = Math.floor(s.pos / w) - worldY;
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 < bestDist) {
|
||||
bestDist = d2;
|
||||
best = s;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
setLocalPlayerID(id: number): void {
|
||||
if (id === this.localPlayerID) return;
|
||||
this.localPlayerID = id;
|
||||
@@ -1051,21 +906,6 @@ export class GPURenderer {
|
||||
this.railroadPass.setLocalRailColor(r, g, b);
|
||||
}
|
||||
|
||||
setSAMRadiusVisible(visible: boolean): void {
|
||||
this.samRadiusPass.setVisible(visible);
|
||||
}
|
||||
|
||||
setSAMPerspective(playerID: number, allies: Set<number>): void {
|
||||
this.samRadiusPass.setLocalPlayer(playerID);
|
||||
this.samRadiusPass.setAllies(allies);
|
||||
this.unitPass.setLocalPlayer(playerID);
|
||||
this.unitPass.setAllies(allies);
|
||||
}
|
||||
|
||||
setSAMColorMode(mode: "perspective" | "owner"): void {
|
||||
this.samRadiusPass.setColorMode(mode);
|
||||
}
|
||||
|
||||
setSAMAllianceClusters(clusters: Map<number, number>): void {
|
||||
this.samRadiusPass.setAllianceClusters(clusters);
|
||||
}
|
||||
@@ -1096,57 +936,10 @@ export class GPURenderer {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Radial menu
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
showRadialMenu(
|
||||
anchorX: number,
|
||||
anchorY: number,
|
||||
items: RadialMenuItem[],
|
||||
centerItem?: RadialMenuItem,
|
||||
): void {
|
||||
this.radialMenuPass.show(anchorX, anchorY, items, centerItem);
|
||||
}
|
||||
|
||||
hideRadialMenu(): void {
|
||||
this.radialMenuPass.hide();
|
||||
}
|
||||
openRadialSubMenu(subItems: RadialMenuItem[]): void {
|
||||
this.radialMenuPass.openSubMenu(subItems);
|
||||
}
|
||||
goBackRadialMenu(): void {
|
||||
this.radialMenuPass.goBack();
|
||||
}
|
||||
setRadialMenuHover(index: number): void {
|
||||
this.radialMenuPass.setHover(index);
|
||||
}
|
||||
radialMenuHitTest(screenX: number, screenY: number): number {
|
||||
return this.radialMenuPass.hitTest(screenX, screenY);
|
||||
}
|
||||
get radialMenuVisible(): boolean {
|
||||
return this.radialMenuPass.isVisible;
|
||||
}
|
||||
getRadialMenuItems(): readonly RadialMenuItem[] {
|
||||
return this.radialMenuPass.getItems();
|
||||
}
|
||||
getRadialMenuItemAt(index: number): RadialMenuItem | null {
|
||||
return this.radialMenuPass.getItemAt(index);
|
||||
}
|
||||
registerRadialMenuIcons(
|
||||
icons: { key: string; img: CanvasImageSource }[],
|
||||
): void {
|
||||
this.radialMenuPass.registerIcons(icons);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Selection box (warship selection)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
setSelectedUnit(unitId: number | null): void {
|
||||
this.setSelectedUnits(unitId === null ? [] : [unitId]);
|
||||
}
|
||||
|
||||
setSelectedUnits(unitIds: readonly number[]): void {
|
||||
// Copy in (callers may mutate their array).
|
||||
this.selectedUnitIds.length = 0;
|
||||
@@ -1222,27 +1015,9 @@ export class GPURenderer {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
draw(): void {
|
||||
const now = performance.now();
|
||||
this.trackFps(now);
|
||||
this.uploadTextures();
|
||||
this.computeTextures();
|
||||
this.renderFrame();
|
||||
if (this.onFrame) this.onFrame(performance.now() - now);
|
||||
if (this.afterRender) this.afterRender(this.canvas);
|
||||
}
|
||||
|
||||
private trackFps(now: number): void {
|
||||
this.frameTimes[this.frameIdx] = now;
|
||||
this.frameIdx = (this.frameIdx + 1) % this.frameTimes.length;
|
||||
if (this.frameCount < this.frameTimes.length) this.frameCount++;
|
||||
if (this.frameCount > 1) {
|
||||
const oldest =
|
||||
this.frameTimes[
|
||||
(this.frameIdx - this.frameCount + this.frameTimes.length) %
|
||||
this.frameTimes.length
|
||||
];
|
||||
this.fps = (this.frameCount - 1) / ((now - oldest) / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private uploadTextures(): void {
|
||||
@@ -1369,8 +1144,6 @@ export class GPURenderer {
|
||||
this.worldTextPass.tick(zoom);
|
||||
this.worldTextPass.draw(cam, zoom);
|
||||
|
||||
this.radialMenuPass.draw();
|
||||
|
||||
gl.disable(gl.BLEND);
|
||||
}
|
||||
|
||||
@@ -1405,7 +1178,6 @@ export class GPURenderer {
|
||||
this.namePass.dispose();
|
||||
this.fxPass.dispose();
|
||||
this.worldTextPass.dispose();
|
||||
this.radialMenuPass.dispose();
|
||||
this.selectionBoxPass.dispose();
|
||||
this.moveIndicatorPass.dispose();
|
||||
this.nukeTrajectoryPass.dispose();
|
||||
|
||||
Reference in New Issue
Block a user