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:
Evan
2026-06-12 14:21:24 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent aa22339f96
commit aa4b490e68
79 changed files with 142 additions and 2197 deletions
+1 -229
View File
@@ -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();