From aa4b490e68b2963588cd0e7ea25728d0abb2f901 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 12 Jun 2026 14:21:24 -0700 Subject: [PATCH] Simplify WebGL renderer integration: remove dead extension code, untangle GameView naming (#4240) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- src/client/ClientGameRunner.ts | 14 +- src/client/InputHandler.ts | 2 +- src/client/TransformHandler.ts | 2 +- src/client/Transport.ts | 2 +- src/client/WebGLFrameBuilder.ts | 10 +- .../controllers/AttackingTroopsController.ts | 6 +- .../controllers/BuildPreviewController.ts | 6 +- .../controllers/HoverHighlightController.ts | 6 +- .../controllers/SoundEffectController.ts | 2 +- .../StructureHighlightController.ts | 4 +- src/client/controllers/ViewModeController.ts | 4 +- .../controllers/WarshipSelectionController.ts | 6 +- src/client/hud/GameRenderer.ts | 6 +- src/client/hud/PlayerIcons.ts | 2 +- src/client/hud/SpriteLoader.ts | 2 +- src/client/hud/layers/ActionableEvents.ts | 2 +- src/client/hud/layers/AlertFrame.ts | 2 +- src/client/hud/layers/AttacksDisplay.ts | 2 +- src/client/hud/layers/BuildMenu.ts | 2 +- src/client/hud/layers/ChatDisplay.ts | 2 +- src/client/hud/layers/ChatIntegration.ts | 2 +- src/client/hud/layers/ChatModal.ts | 2 +- src/client/hud/layers/ControlPanel.ts | 2 +- src/client/hud/layers/EmojiTable.ts | 2 +- src/client/hud/layers/EventsDisplay.ts | 2 +- src/client/hud/layers/GameLeftSidebar.ts | 2 +- src/client/hud/layers/GameRightSidebar.ts | 2 +- src/client/hud/layers/HeadsUpMessage.ts | 2 +- src/client/hud/layers/ImmunityTimer.ts | 2 +- src/client/hud/layers/InGamePromo.ts | 2 +- src/client/hud/layers/Leaderboard.ts | 2 +- src/client/hud/layers/MainRadialMenu.ts | 2 +- src/client/hud/layers/MultiTabModal.ts | 2 +- src/client/hud/layers/PlayerActionHandler.ts | 2 +- src/client/hud/layers/PlayerInfoOverlay.ts | 2 +- .../hud/layers/PlayerModerationModal.ts | 2 +- src/client/hud/layers/PlayerPanel.ts | 2 +- src/client/hud/layers/RadialMenuElements.ts | 2 +- src/client/hud/layers/ReplayPanel.ts | 2 +- src/client/hud/layers/SendResourceModal.ts | 2 +- src/client/hud/layers/SpawnTimer.ts | 2 +- src/client/hud/layers/TeamStats.ts | 2 +- src/client/hud/layers/UnitDisplay.ts | 2 +- src/client/hud/layers/WinModal.ts | 2 +- src/client/render/CLAUDE.md | 6 +- src/client/render/frame/Upload.ts | 59 +- src/client/render/frame/index.ts | 2 +- src/client/render/gl/Events.ts | 98 --- .../render/gl/{GameView.ts => MapRenderer.ts} | 211 +------ src/client/render/gl/Renderer.ts | 230 +------ src/client/render/gl/index.ts | 10 +- src/client/render/gl/passes/RadialMenuPass.ts | 577 ------------------ src/client/render/gl/passes/TerritoryPass.ts | 67 +- src/client/render/gl/passes/TrailPass.ts | 19 - .../gl/shaders/radial-menu/arcs.frag.glsl | 132 ---- .../gl/shaders/radial-menu/arcs.vert.glsl | 24 - .../gl/shaders/radial-menu/icon.frag.glsl | 27 - .../gl/shaders/radial-menu/icon.vert.glsl | 74 --- src/client/render/gl/utils/HeatManager.ts | 79 +-- src/client/render/types/FrameData.ts | 26 +- src/client/render/types/FrameEvents.ts | 90 +-- src/client/render/types/FrameSource.ts | 38 -- src/client/render/types/Game.ts | 17 - src/client/render/types/GameUpdates.ts | 161 ----- src/client/render/types/Replay.ts | 144 ----- src/client/render/types/index.ts | 62 +- src/client/theme/ThemeProvider.ts | 2 +- src/client/view/GameView.ts | 16 +- src/client/view/index.ts | 3 + src/core/configuration/Config.ts | 2 +- src/core/execution/Util.ts | 2 +- src/core/game/GameImpl.ts | 2 +- src/core/game/GameView.ts | 12 - src/core/game/UnitGrid.ts | 2 +- tests/InputHandler.test.ts | 2 +- .../graphics/RadialMenuElements.test.ts | 2 +- .../graphics/layers/PlayerPanelKick.test.ts | 2 +- tests/client/view/GameView.test.ts | 5 - tests/radialMenuElements.test.ts | 2 +- 79 files changed, 142 insertions(+), 2197 deletions(-) delete mode 100644 src/client/render/gl/Events.ts rename src/client/render/gl/{GameView.ts => MapRenderer.ts} (56%) delete mode 100644 src/client/render/gl/passes/RadialMenuPass.ts delete mode 100644 src/client/render/gl/shaders/radial-menu/arcs.frag.glsl delete mode 100644 src/client/render/gl/shaders/radial-menu/arcs.vert.glsl delete mode 100644 src/client/render/gl/shaders/radial-menu/icon.frag.glsl delete mode 100644 src/client/render/gl/shaders/radial-menu/icon.vert.glsl delete mode 100644 src/client/render/types/FrameSource.ts delete mode 100644 src/client/render/types/Game.ts delete mode 100644 src/client/render/types/GameUpdates.ts delete mode 100644 src/client/render/types/Replay.ts create mode 100644 src/client/view/index.ts delete mode 100644 src/core/game/GameView.ts diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index e8947f011..6229de04e 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -27,7 +27,6 @@ import { HashUpdate, WinUpdate, } from "../core/game/GameUpdates"; -import { GameView, PlayerView } from "../core/game/GameView"; import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader"; import { DARK_MODE_KEY, @@ -73,12 +72,13 @@ import { applyGraphicsOverrides, createRenderSettings, deepAssign, + MapRenderer, preloadAtlasData, - GameView as WebGLGameView, } from "./render/gl"; import { ALL_UNIT_TYPES, UnitState } from "./render/types"; import { SoundManager } from "./sound/SoundManager"; import { themeProvider } from "./theme/ThemeProvider"; +import { GameView, PlayerView } from "./view"; export interface LobbyConfig { cosmetics: PlayerCosmeticRefs; @@ -257,7 +257,7 @@ function createWebGLView( terrainMap: TerrainMapData, config: Config, ): { - view: WebGLGameView; + view: MapRenderer; glCanvas: HTMLCanvasElement; cachedWebGLFrameCallback: { current: FrameRequestCallback | null }; } { @@ -295,7 +295,7 @@ function createWebGLView( }; const palette = new Float32Array(4096 * 2 * 4); - const view = new WebGLGameView( + const view = new MapRenderer( glCanvas, { mapWidth, @@ -322,7 +322,7 @@ function createWebGLView( function mountWebGLFrameLoop( terrainMap: TerrainMapData, - view: WebGLGameView, + view: MapRenderer, glCanvas: HTMLCanvasElement, cachedWebGLFrameCallback: { current: FrameRequestCallback | null }, transformHandler: import("./TransformHandler").TransformHandler, @@ -397,7 +397,7 @@ function mountWebGLFrameLoop( // When context is lost and restored, WebGL loses all textures and geometry. // Force a full re-upload of the simulation state. - view.on("contextrestored", () => { + view.onContextRestored = () => { builder.clearCaches(); // Full upload of terrain, territory & trail state @@ -418,7 +418,7 @@ function mountWebGLFrameLoop( view.uploadRailroadState(frameData.railroadState); builder.update(gameView); - }); + }; return { builder }; } diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 774ddbdcb..d0b164bf8 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -1,10 +1,10 @@ import { EventBus, GameEvent } from "../core/EventBus"; import { PlayerBuildableUnitType, UnitType } from "../core/game/Game"; -import { GameView, UnitView } from "../core/game/GameView"; import { UserSettings } from "../core/game/UserSettings"; import { Platform } from "./Platform"; import { UIState } from "./UIState"; import { ReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier"; +import { GameView, UnitView } from "./view"; export class MouseUpEvent implements GameEvent { constructor( diff --git a/src/client/TransformHandler.ts b/src/client/TransformHandler.ts index abab40838..41122c90d 100644 --- a/src/client/TransformHandler.ts +++ b/src/client/TransformHandler.ts @@ -1,7 +1,7 @@ import { EventBus, GameEvent } from "../core/EventBus"; import { Cell } from "../core/game/Game"; -import { GameView, PlayerView, UnitView } from "../core/game/GameView"; import { CenterCameraEvent, DragEvent, ZoomEvent } from "./InputHandler"; +import { GameView, PlayerView, UnitView } from "./view"; export class GoToPlayerEvent implements GameEvent { constructor( diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 8115f3241..c1307af69 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -10,7 +10,6 @@ import { UnitType, } from "../core/game/Game"; import { TileRef } from "../core/game/GameMap"; -import { PlayerView } from "../core/game/GameView"; import { AllPlayersStats, ClientHashMessage, @@ -30,6 +29,7 @@ import { replacer } from "../core/Util"; import { getPlayToken } from "./Auth"; import { LobbyConfig } from "./ClientGameRunner"; import { LocalServer } from "./LocalServer"; +import { PlayerView } from "./view"; export class PauseGameIntentEvent implements GameEvent { constructor(public readonly paused: boolean) {} diff --git a/src/client/WebGLFrameBuilder.ts b/src/client/WebGLFrameBuilder.ts index 041d8ab8e..6a293f15a 100644 --- a/src/client/WebGLFrameBuilder.ts +++ b/src/client/WebGLFrameBuilder.ts @@ -3,15 +3,11 @@ import { base64url } from "jose"; import { assetUrl } from "../core/AssetUrls"; import { decodePatternData } from "../core/PatternDecoder"; import { PlayerType } from "../core/game/Game"; -import { GameView } from "../core/game/GameView"; import { uploadFrameData } from "./render/frame/Upload"; // Type-only: a value import would pull GPURenderer and its `.glsl?raw` shader // imports into any non-Vite consumer (e.g. the Node perf harness). -import type { - PlayerStatic, - SpawnCenter, - GameView as WebGLGameView, -} from "./render/gl"; +import type { MapRenderer, PlayerStatic, SpawnCenter } from "./render/gl"; +import type { GameView } from "./view"; const PALETTE_SIZE = 4096; @@ -47,7 +43,7 @@ export class WebGLFrameBuilder { // Scratch buffer for terrain-delta uploads (parallel to the refs list). private terrainDeltaBytes: Uint8Array = new Uint8Array(0); - constructor(private readonly view: WebGLGameView) { + constructor(private readonly view: MapRenderer) { this.palette = new Float32Array(PALETTE_SIZE * 2 * 4); this.patternMeta = new Float32Array(PALETTE_SIZE * 4); this.patternData = new Uint8Array(PALETTE_SIZE * 1024); diff --git a/src/client/controllers/AttackingTroopsController.ts b/src/client/controllers/AttackingTroopsController.ts index f105368c9..d88740041 100644 --- a/src/client/controllers/AttackingTroopsController.ts +++ b/src/client/controllers/AttackingTroopsController.ts @@ -10,13 +10,13 @@ */ import { EventBus } from "../../core/EventBus"; import { Cell, PlayerType } from "../../core/game/Game"; -import { GameView } from "../../core/game/GameView"; import { UserSettings } from "../../core/game/UserSettings"; import { Controller } from "../Controller"; import { AlternateViewEvent } from "../InputHandler"; -import { GameView as WebGLGameView } from "../render/gl"; +import { MapRenderer } from "../render/gl"; import type { AttackTroopLabel } from "../render/gl/passes/WorldTextPass"; import { renderTroops } from "../Utils"; +import { GameView } from "../view"; // Aquarius (#3fa9f5) for outgoing, red-400 (#f87171) for incoming. const OUTGOING_R = 0x3f / 255; @@ -75,7 +75,7 @@ export class AttackingTroopsController implements Controller { private readonly game: GameView, private readonly eventBus: EventBus, private readonly userSettings: UserSettings, - private readonly view: WebGLGameView, + private readonly view: MapRenderer, ) {} init() { diff --git a/src/client/controllers/BuildPreviewController.ts b/src/client/controllers/BuildPreviewController.ts index 19a70ab32..3dfaa22ca 100644 --- a/src/client/controllers/BuildPreviewController.ts +++ b/src/client/controllers/BuildPreviewController.ts @@ -18,7 +18,6 @@ import { UnitType, } from "../../core/game/Game"; import { TileRef } from "../../core/game/GameMap"; -import { GameView } from "../../core/game/GameView"; import { UserSettings } from "../../core/game/UserSettings"; import { Controller } from "../Controller"; import { @@ -26,7 +25,7 @@ import { MouseMoveEvent, MouseUpEvent, } from "../InputHandler"; -import { GameView as WebGLGameView, buildNukeTrajectory } from "../render/gl"; +import { MapRenderer, buildNukeTrajectory } from "../render/gl"; import type { SAMInfo } from "../render/gl/utils/NukeTrajectory"; import type { GhostPreviewData } from "../render/types"; import { TransformHandler } from "../TransformHandler"; @@ -35,6 +34,7 @@ import { SendUpgradeStructureIntentEvent, } from "../Transport"; import { UIState } from "../UIState"; +import { GameView } from "../view"; /** 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 { @@ -86,7 +86,7 @@ export class BuildPreviewController implements Controller { private eventBus: EventBus, public uiState: UIState, private transformHandler: TransformHandler, - private view: WebGLGameView, + private view: MapRenderer, private userSettings: UserSettings, ) {} diff --git a/src/client/controllers/HoverHighlightController.ts b/src/client/controllers/HoverHighlightController.ts index e647bd659..e33a68f6a 100644 --- a/src/client/controllers/HoverHighlightController.ts +++ b/src/client/controllers/HoverHighlightController.ts @@ -9,12 +9,12 @@ */ import { EventBus } from "../../core/EventBus"; -import { GameView } from "../../core/game/GameView"; import { Controller } from "../Controller"; import { MouseMoveEvent } from "../InputHandler"; -import { GameView as WebGLGameView } from "../render/gl"; +import { MapRenderer } from "../render/gl"; import { OWNER_MASK } from "../render/gl/utils/TileCodec"; import { TransformHandler } from "../TransformHandler"; +import { GameView } from "../view"; export class HoverHighlightController implements Controller { private lastOwnerID = 0; @@ -23,7 +23,7 @@ export class HoverHighlightController implements Controller { private game: GameView, private eventBus: EventBus, private transformHandler: TransformHandler, - private view: WebGLGameView, + private view: MapRenderer, ) {} init() { diff --git a/src/client/controllers/SoundEffectController.ts b/src/client/controllers/SoundEffectController.ts index fc4bb91fd..9fc443f86 100644 --- a/src/client/controllers/SoundEffectController.ts +++ b/src/client/controllers/SoundEffectController.ts @@ -1,9 +1,9 @@ import { EventBus } from "../../core/EventBus"; import { UnitType } from "../../core/game/Game"; import { GameUpdateType } from "../../core/game/GameUpdates"; -import { GameView, UnitView } from "../../core/game/GameView"; import { Controller } from "../Controller"; import { PlaySoundEffectEvent, SoundEffect } from "../sound/Sounds"; +import { GameView, UnitView } from "../view"; export class SoundEffectController implements Controller { constructor( diff --git a/src/client/controllers/StructureHighlightController.ts b/src/client/controllers/StructureHighlightController.ts index e3d8c6333..95e2f8c73 100644 --- a/src/client/controllers/StructureHighlightController.ts +++ b/src/client/controllers/StructureHighlightController.ts @@ -12,12 +12,12 @@ import { EventBus } from "../../core/EventBus"; import { Controller } from "../Controller"; import { ToggleStructureEvent } from "../InputHandler"; -import { GameView as WebGLGameView } from "../render/gl"; +import { MapRenderer } from "../render/gl"; export class StructureHighlightController implements Controller { constructor( private eventBus: EventBus, - private view: WebGLGameView, + private view: MapRenderer, ) {} init() { diff --git a/src/client/controllers/ViewModeController.ts b/src/client/controllers/ViewModeController.ts index 5ffa021fc..e9c86efbd 100644 --- a/src/client/controllers/ViewModeController.ts +++ b/src/client/controllers/ViewModeController.ts @@ -10,12 +10,12 @@ import { EventBus } from "../../core/EventBus"; import { Controller } from "../Controller"; import { AlternateViewEvent, ToggleCoordinateGridEvent } from "../InputHandler"; -import { GameView as WebGLGameView } from "../render/gl"; +import { MapRenderer } from "../render/gl"; export class ViewModeController implements Controller { constructor( private eventBus: EventBus, - private view: WebGLGameView, + private view: MapRenderer, ) {} init() { diff --git a/src/client/controllers/WarshipSelectionController.ts b/src/client/controllers/WarshipSelectionController.ts index fdaa9294b..fe8c852f2 100644 --- a/src/client/controllers/WarshipSelectionController.ts +++ b/src/client/controllers/WarshipSelectionController.ts @@ -2,7 +2,6 @@ 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 { Controller } from "../Controller"; import { CloseViewEvent, @@ -15,9 +14,10 @@ import { WarshipSelectionBoxCompleteEvent, WarshipSelectionBoxUpdateEvent, } from "../InputHandler"; -import { GameView as WebGLGameView } from "../render/gl"; +import { MapRenderer } from "../render/gl"; import { TransformHandler } from "../TransformHandler"; import { MoveWarshipIntentEvent } from "../Transport"; +import { GameView, UnitView } from "../view"; const WARSHIP_SELECTION_RADIUS = 10; @@ -48,7 +48,7 @@ export class WarshipSelectionController implements Controller { private game: GameView, private eventBus: EventBus, private transformHandler: TransformHandler, - private view: WebGLGameView, + private view: MapRenderer, ) {} tick() { diff --git a/src/client/hud/GameRenderer.ts b/src/client/hud/GameRenderer.ts index 43aa826f5..24a12315e 100644 --- a/src/client/hud/GameRenderer.ts +++ b/src/client/hud/GameRenderer.ts @@ -1,5 +1,4 @@ import { EventBus } from "../../core/EventBus"; -import { GameView } from "../../core/game/GameView"; import { UserSettings } from "../../core/game/UserSettings"; import { Controller } from "../Controller"; import { AttackingTroopsController } from "../controllers/AttackingTroopsController"; @@ -10,9 +9,10 @@ import { StructureHighlightController } from "../controllers/StructureHighlightC import { ViewModeController } from "../controllers/ViewModeController"; import { WarshipSelectionController } from "../controllers/WarshipSelectionController"; import { GameStartingModal } from "../GameStartingModal"; -import { GameView as WebGLGameView } from "../render/gl"; +import { MapRenderer } from "../render/gl"; import { TransformHandler } from "../TransformHandler"; import { UIState } from "../UIState"; +import { GameView } from "../view"; import { FrameProfiler } from "./FrameProfiler"; import { ActionableEvents } from "./layers/ActionableEvents"; import { AlertFrame } from "./layers/AlertFrame"; @@ -48,7 +48,7 @@ export function createRenderer( game: GameView, eventBus: EventBus, playerRole: string | null, - view: WebGLGameView, + view: MapRenderer, ): GameRenderer { const transformHandler = new TransformHandler(game, eventBus, inputEl); const userSettings = new UserSettings(); diff --git a/src/client/hud/PlayerIcons.ts b/src/client/hud/PlayerIcons.ts index 935de9388..65666231a 100644 --- a/src/client/hud/PlayerIcons.ts +++ b/src/client/hud/PlayerIcons.ts @@ -1,6 +1,6 @@ import { assetUrl } from "../../core/AssetUrls"; import { AllPlayers, Nukes } from "../../core/game/Game"; -import { GameView, PlayerView } from "../../core/game/GameView"; +import { GameView, PlayerView } from "../view"; const allianceIcon = assetUrl("images/AllianceIcon.svg"); const allianceIconFaded = assetUrl("images/AllianceIconFaded.svg"); const allianceRequestBlackIcon = assetUrl( diff --git a/src/client/hud/SpriteLoader.ts b/src/client/hud/SpriteLoader.ts index 4facdfab0..1e03d0b3e 100644 --- a/src/client/hud/SpriteLoader.ts +++ b/src/client/hud/SpriteLoader.ts @@ -1,8 +1,8 @@ import { Colord } from "colord"; import { assetUrl } from "../../core/AssetUrls"; import { TrainType, UnitType } from "../../core/game/Game"; -import { UnitView } from "../../core/game/GameView"; import { Theme } from "../theme/ThemeProvider"; +import { UnitView } from "../view"; const atomBombSprite = assetUrl("sprites/atombomb.png"); const hydrogenBombSprite = assetUrl("sprites/hydrogenbomb.png"); const mirvSprite = assetUrl("sprites/mirv2.png"); diff --git a/src/client/hud/layers/ActionableEvents.ts b/src/client/hud/layers/ActionableEvents.ts index ae2c604a2..4211ff5ea 100644 --- a/src/client/hud/layers/ActionableEvents.ts +++ b/src/client/hud/layers/ActionableEvents.ts @@ -9,7 +9,6 @@ import { BrokeAllianceUpdate, GameUpdateType, } from "../../../core/game/GameUpdates"; -import { GameView, PlayerView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { PlaySoundEffectEvent } from "../../sound/Sounds"; import { GoToPlayerEvent } from "../../TransformHandler"; @@ -20,6 +19,7 @@ import { } from "../../Transport"; import { UIState } from "../../UIState"; import { getMessageTypeClasses, translateText } from "../../Utils"; +import { GameView, PlayerView } from "../../view"; interface ActionableEvent { description: string; diff --git a/src/client/hud/layers/AlertFrame.ts b/src/client/hud/layers/AlertFrame.ts index fcadb4969..2f0c53662 100644 --- a/src/client/hud/layers/AlertFrame.ts +++ b/src/client/hud/layers/AlertFrame.ts @@ -5,9 +5,9 @@ import { BrokeAllianceUpdate, GameUpdateType, } from "../../../core/game/GameUpdates"; -import { GameView, PlayerView } from "../../../core/game/GameView"; import { UserSettings } from "../../../core/game/UserSettings"; import { Controller } from "../../Controller"; +import { GameView, PlayerView } from "../../view"; // Parameters for the alert animation const ALERT_SPEED = 1.6; diff --git a/src/client/hud/layers/AttacksDisplay.ts b/src/client/hud/layers/AttacksDisplay.ts index 04f736d83..0f83d1cde 100644 --- a/src/client/hud/layers/AttacksDisplay.ts +++ b/src/client/hud/layers/AttacksDisplay.ts @@ -8,7 +8,6 @@ import { GameUpdateType, UnitIncomingUpdate, } from "../../../core/game/GameUpdates"; -import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { themeProvider } from "../../theme/ThemeProvider"; import { @@ -23,6 +22,7 @@ import { } from "../../Transport"; import { UIState } from "../../UIState"; import { renderTroops, translateText } from "../../Utils"; +import { GameView, PlayerView, UnitView } from "../../view"; import { getColoredSprite } from "../SpriteLoader"; const soldierIcon = assetUrl("images/SoldierIcon.svg"); const swordIcon = assetUrl("images/SwordIcon.svg"); diff --git a/src/client/hud/layers/BuildMenu.ts b/src/client/hud/layers/BuildMenu.ts index 46d1a8f41..0094f3ec3 100644 --- a/src/client/hud/layers/BuildMenu.ts +++ b/src/client/hud/layers/BuildMenu.ts @@ -11,7 +11,6 @@ import { UnitType, } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; -import { GameView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { CloseViewEvent, @@ -26,6 +25,7 @@ import { } from "../../Transport"; import { UIState } from "../../UIState"; import { renderNumber } from "../../Utils"; +import { GameView } from "../../view"; const warshipIcon = assetUrl("images/BattleshipIconWhite.svg"); const cityIcon = assetUrl("images/CityIconWhite.svg"); const factoryIcon = assetUrl("images/FactoryIconWhite.svg"); diff --git a/src/client/hud/layers/ChatDisplay.ts b/src/client/hud/layers/ChatDisplay.ts index 04018b2cf..3190f9d94 100644 --- a/src/client/hud/layers/ChatDisplay.ts +++ b/src/client/hud/layers/ChatDisplay.ts @@ -8,9 +8,9 @@ import { DisplayMessageUpdate, GameUpdateType, } from "../../../core/game/GameUpdates"; -import { GameView } from "../../../core/game/GameView"; import { onlyImages } from "../../../core/Util"; import { Controller } from "../../Controller"; +import { GameView } from "../../view"; interface ChatEvent { description: string; diff --git a/src/client/hud/layers/ChatIntegration.ts b/src/client/hud/layers/ChatIntegration.ts index b81e5caca..639aba86e 100644 --- a/src/client/hud/layers/ChatIntegration.ts +++ b/src/client/hud/layers/ChatIntegration.ts @@ -1,7 +1,7 @@ import { EventBus } from "../../../core/EventBus"; -import { GameView, PlayerView } from "../../../core/game/GameView"; import { SendQuickChatEvent } from "../../Transport"; import { translateText } from "../../Utils"; +import { GameView, PlayerView } from "../../view"; import { ChatModal, QuickChatPhrase, quickChatPhrases } from "./ChatModal"; import { COLORS, MenuElement, MenuElementParams } from "./RadialMenuElements"; diff --git a/src/client/hud/layers/ChatModal.ts b/src/client/hud/layers/ChatModal.ts index 0237b17ac..7d8f4d123 100644 --- a/src/client/hud/layers/ChatModal.ts +++ b/src/client/hud/layers/ChatModal.ts @@ -2,7 +2,7 @@ import { LitElement, html } from "lit"; import { customElement, query } from "lit/decorators.js"; import { PlayerType } from "../../../core/game/Game"; -import { GameView, PlayerView } from "../../../core/game/GameView"; +import { GameView, PlayerView } from "../../view"; import quickChatData from "resources/QuickChat.json"; import { EventBus } from "../../../core/EventBus"; diff --git a/src/client/hud/layers/ControlPanel.ts b/src/client/hud/layers/ControlPanel.ts index f8416017f..f77684486 100644 --- a/src/client/hud/layers/ControlPanel.ts +++ b/src/client/hud/layers/ControlPanel.ts @@ -8,7 +8,6 @@ import { Config } from "../../../core/configuration/Config"; import { GameMode, GameType, Gold } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; import { GameUpdateType } from "../../../core/game/GameUpdates"; -import { GameView } from "../../../core/game/GameView"; import { UserSettings } from "../../../core/game/UserSettings"; import { Controller } from "../../Controller"; import { AttackRatioEvent } from "../../InputHandler"; @@ -19,6 +18,7 @@ import { renderTroops, translateText, } from "../../Utils"; +import { GameView } from "../../view"; import { PlayerView } from "../../view/PlayerView"; const goldCoinIcon = assetUrl("images/GoldCoinIcon.svg"); const soldierIcon = assetUrl("images/SoldierIcon.svg"); diff --git a/src/client/hud/layers/EmojiTable.ts b/src/client/hud/layers/EmojiTable.ts index 787b83725..f03e16f36 100644 --- a/src/client/hud/layers/EmojiTable.ts +++ b/src/client/hud/layers/EmojiTable.ts @@ -2,12 +2,12 @@ import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { EventBus } from "../../../core/EventBus"; import { AllPlayers } from "../../../core/game/Game"; -import { GameView, PlayerView } from "../../../core/game/GameView"; import { TerraNulliusImpl } from "../../../core/game/TerraNulliusImpl"; import { Emoji, flattenedEmojiTable } from "../../../core/Util"; import { CloseViewEvent, ShowEmojiMenuEvent } from "../../InputHandler"; import { TransformHandler } from "../../TransformHandler"; import { SendEmojiIntentEvent } from "../../Transport"; +import { GameView, PlayerView } from "../../view"; @customElement("emoji-table") export class EmojiTable extends LitElement { diff --git a/src/client/hud/layers/EventsDisplay.ts b/src/client/hud/layers/EventsDisplay.ts index 1e4d8dacd..46958f0e9 100644 --- a/src/client/hud/layers/EventsDisplay.ts +++ b/src/client/hud/layers/EventsDisplay.ts @@ -19,9 +19,9 @@ import { import { Controller } from "../../Controller"; import { SendAllianceRequestIntentEvent } from "../../Transport"; -import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; import { onlyImages } from "../../../core/Util"; import { GoToPlayerEvent, GoToUnitEvent } from "../../TransformHandler"; +import { GameView, PlayerView, UnitView } from "../../view"; import { PlaySoundEffectEvent } from "../../sound/Sounds"; import { UIState } from "../../UIState"; diff --git a/src/client/hud/layers/GameLeftSidebar.ts b/src/client/hud/layers/GameLeftSidebar.ts index 65a13ab4e..a96311bfb 100644 --- a/src/client/hud/layers/GameLeftSidebar.ts +++ b/src/client/hud/layers/GameLeftSidebar.ts @@ -4,11 +4,11 @@ import { customElement, state } from "lit/decorators.js"; import { assetUrl } from "../../../core/AssetUrls"; import { EventBus } from "../../../core/EventBus"; import { GameMode, Team } from "../../../core/game/Game"; -import { GameView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { Platform } from "../../Platform"; import { themeProvider } from "../../theme/ThemeProvider"; import { getTranslatedPlayerTeamLabel, translateText } from "../../Utils"; +import { GameView } from "../../view"; import { ImmunityBarVisibleEvent } from "./ImmunityTimer"; import { SpawnBarVisibleEvent } from "./SpawnTimer"; const leaderboardRegularIcon = assetUrl( diff --git a/src/client/hud/layers/GameRightSidebar.ts b/src/client/hud/layers/GameRightSidebar.ts index 12bc818fe..3752c5e70 100644 --- a/src/client/hud/layers/GameRightSidebar.ts +++ b/src/client/hud/layers/GameRightSidebar.ts @@ -3,12 +3,12 @@ import { customElement, state } from "lit/decorators.js"; import { assetUrl } from "../../../core/AssetUrls"; import { EventBus } from "../../../core/EventBus"; import { GameType } from "../../../core/game/Game"; -import { GameView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { crazyGamesSDK } from "../../CrazyGamesSDK"; import { TogglePauseIntentEvent } from "../../InputHandler"; import { PauseGameIntentEvent, SendWinnerEvent } from "../../Transport"; import { translateText } from "../../Utils"; +import { GameView } from "../../view"; import { ImmunityBarVisibleEvent } from "./ImmunityTimer"; import { ShowReplayPanelEvent } from "./ReplayPanel"; import { ShowSettingsModalEvent } from "./SettingsModal"; diff --git a/src/client/hud/layers/HeadsUpMessage.ts b/src/client/hud/layers/HeadsUpMessage.ts index 33d5e2e73..4c633820e 100644 --- a/src/client/hud/layers/HeadsUpMessage.ts +++ b/src/client/hud/layers/HeadsUpMessage.ts @@ -2,9 +2,9 @@ import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { GameMode, GameType } from "../../../core/game/Game"; import { GameUpdateType } from "../../../core/game/GameUpdates"; -import { GameView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { translateText } from "../../Utils"; +import { GameView } from "../../view"; const COLLUSION_WARNING_CLOSED_KEY = "hasClosedCollusionWarning"; diff --git a/src/client/hud/layers/ImmunityTimer.ts b/src/client/hud/layers/ImmunityTimer.ts index 4cfefb99c..1aa264ac2 100644 --- a/src/client/hud/layers/ImmunityTimer.ts +++ b/src/client/hud/layers/ImmunityTimer.ts @@ -2,8 +2,8 @@ import { LitElement, html } from "lit"; import { customElement } from "lit/decorators.js"; import { EventBus, GameEvent } from "../../../core/EventBus"; import { GameMode } from "../../../core/game/Game"; -import { GameView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; +import { GameView } from "../../view"; export class ImmunityBarVisibleEvent implements GameEvent { constructor(public readonly visible: boolean) {} diff --git a/src/client/hud/layers/InGamePromo.ts b/src/client/hud/layers/InGamePromo.ts index d26efb85d..60f9cff32 100644 --- a/src/client/hud/layers/InGamePromo.ts +++ b/src/client/hud/layers/InGamePromo.ts @@ -1,8 +1,8 @@ import { LitElement, html } from "lit"; import { customElement } from "lit/decorators.js"; -import { GameView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { crazyGamesSDK } from "../../CrazyGamesSDK"; +import { GameView } from "../../view"; const AD_TYPES = [ { type: "standard_iab_left1", selectorId: "in-game-bottom-left-ad" }, diff --git a/src/client/hud/layers/Leaderboard.ts b/src/client/hud/layers/Leaderboard.ts index 4a805ca20..7e292bb3e 100644 --- a/src/client/hud/layers/Leaderboard.ts +++ b/src/client/hud/layers/Leaderboard.ts @@ -3,10 +3,10 @@ import { customElement, property, state } from "lit/decorators.js"; import { repeat } from "lit/directives/repeat.js"; import { renderTroops, translateText } from "../../../client/Utils"; import { EventBus } from "../../../core/EventBus"; -import { GameView, PlayerView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { GoToPlayerEvent } from "../../TransformHandler"; import { formatPercentage, renderNumber } from "../../Utils"; +import { GameView, PlayerView } from "../../view"; interface Entry { name: string; diff --git a/src/client/hud/layers/MainRadialMenu.ts b/src/client/hud/layers/MainRadialMenu.ts index c615db0d0..021fb8cf7 100644 --- a/src/client/hud/layers/MainRadialMenu.ts +++ b/src/client/hud/layers/MainRadialMenu.ts @@ -4,10 +4,10 @@ import { assetUrl } from "../../../core/AssetUrls"; import { EventBus } from "../../../core/EventBus"; import { PlayerActions } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; -import { GameView, PlayerView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { TransformHandler } from "../../TransformHandler"; import { UIState } from "../../UIState"; +import { GameView, PlayerView } from "../../view"; import { BuildMenu } from "./BuildMenu"; import { ChatIntegration } from "./ChatIntegration"; import { EmojiTable } from "./EmojiTable"; diff --git a/src/client/hud/layers/MultiTabModal.ts b/src/client/hud/layers/MultiTabModal.ts index 1ea12914b..ebf5dc67b 100644 --- a/src/client/hud/layers/MultiTabModal.ts +++ b/src/client/hud/layers/MultiTabModal.ts @@ -3,10 +3,10 @@ import { customElement, property, state } from "lit/decorators.js"; import { ClientEnv } from "src/client/ClientEnv"; import { GameEnv } from "../../../core/configuration/Config"; import { GameType } from "../../../core/game/Game"; -import { GameView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { MultiTabDetector } from "../../MultiTabDetector"; import { translateText } from "../../Utils"; +import { GameView } from "../../view"; @customElement("multi-tab-modal") export class MultiTabModal extends LitElement implements Controller { diff --git a/src/client/hud/layers/PlayerActionHandler.ts b/src/client/hud/layers/PlayerActionHandler.ts index 085e4d1ce..bb9c6613d 100644 --- a/src/client/hud/layers/PlayerActionHandler.ts +++ b/src/client/hud/layers/PlayerActionHandler.ts @@ -1,6 +1,5 @@ import { EventBus } from "../../../core/EventBus"; import { TileRef } from "../../../core/game/GameMap"; -import { PlayerView } from "../../../core/game/GameView"; import { SendAllianceExtensionIntentEvent, SendAllianceRequestIntentEvent, @@ -16,6 +15,7 @@ import { SendTargetPlayerIntentEvent, } from "../../Transport"; import { UIState } from "../../UIState"; +import { PlayerView } from "../../view"; export class PlayerActionHandler { constructor( diff --git a/src/client/hud/layers/PlayerInfoOverlay.ts b/src/client/hud/layers/PlayerInfoOverlay.ts index 44497143f..6ea92478a 100644 --- a/src/client/hud/layers/PlayerInfoOverlay.ts +++ b/src/client/hud/layers/PlayerInfoOverlay.ts @@ -11,7 +11,6 @@ import { } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; import { AllianceView } from "../../../core/game/GameUpdates"; -import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { ContextMenuEvent, @@ -27,6 +26,7 @@ import { renderTroops, translateText, } from "../../Utils"; +import { GameView, PlayerView, UnitView } from "../../view"; import { EMOJI_ICON_KIND, getFirstPlacePlayer, diff --git a/src/client/hud/layers/PlayerModerationModal.ts b/src/client/hud/layers/PlayerModerationModal.ts index dcf8b833a..acccf5053 100644 --- a/src/client/hud/layers/PlayerModerationModal.ts +++ b/src/client/hud/layers/PlayerModerationModal.ts @@ -3,10 +3,10 @@ import { customElement, property } from "lit/decorators.js"; import { assetUrl } from "../../../core/AssetUrls"; import { EventBus } from "../../../core/EventBus"; import { PlayerType } from "../../../core/game/Game"; -import { PlayerView } from "../../../core/game/GameView"; import { actionButton } from "../../components/ui/ActionButton"; import { SendKickPlayerIntentEvent } from "../../Transport"; import { translateText } from "../../Utils"; +import { PlayerView } from "../../view"; const kickIcon = assetUrl("images/ExitIconWhite.svg"); const shieldIcon = assetUrl("images/ShieldIconWhite.svg"); diff --git a/src/client/hud/layers/PlayerPanel.ts b/src/client/hud/layers/PlayerPanel.ts index f872f8140..494c96978 100644 --- a/src/client/hud/layers/PlayerPanel.ts +++ b/src/client/hud/layers/PlayerPanel.ts @@ -11,7 +11,6 @@ import { Relation, } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; -import { GameView, PlayerView } from "../../../core/game/GameView"; import { Emoji, flattenedEmojiTable } from "../../../core/Util"; import { actionButton } from "../../components/ui/ActionButton"; import "../../components/ui/Divider"; @@ -36,6 +35,7 @@ import { renderTroops, translateText, } from "../../Utils"; +import { GameView, PlayerView } from "../../view"; import { ChatModal } from "./ChatModal"; import { EmojiTable } from "./EmojiTable"; import "./PlayerModerationModal"; diff --git a/src/client/hud/layers/RadialMenuElements.ts b/src/client/hud/layers/RadialMenuElements.ts index b29ea6791..c27ff668d 100644 --- a/src/client/hud/layers/RadialMenuElements.ts +++ b/src/client/hud/layers/RadialMenuElements.ts @@ -9,10 +9,10 @@ import { UnitType, } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; -import { GameView, PlayerView } from "../../../core/game/GameView"; import { Emoji, findClosestBy, flattenedEmojiTable } from "../../../core/Util"; import { UIState } from "../../UIState"; import { renderNumber, translateText } from "../../Utils"; +import { GameView, PlayerView } from "../../view"; import { BuildItemDisplay, BuildMenu, flattenedBuildTable } from "./BuildMenu"; import { ChatIntegration } from "./ChatIntegration"; import { EmojiTable } from "./EmojiTable"; diff --git a/src/client/hud/layers/ReplayPanel.ts b/src/client/hud/layers/ReplayPanel.ts index 390e57092..f7a55cb5e 100644 --- a/src/client/hud/layers/ReplayPanel.ts +++ b/src/client/hud/layers/ReplayPanel.ts @@ -1,7 +1,6 @@ import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { EventBus } from "../../../core/EventBus"; -import { GameView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { ReplaySpeedChangeEvent } from "../../InputHandler"; import { @@ -9,6 +8,7 @@ import { ReplaySpeedMultiplier, } from "../../utilities/ReplaySpeedMultiplier"; import { translateText } from "../../Utils"; +import { GameView } from "../../view"; export class ShowReplayPanelEvent { constructor( diff --git a/src/client/hud/layers/SendResourceModal.ts b/src/client/hud/layers/SendResourceModal.ts index 0ce7db8d1..5865d5e9e 100644 --- a/src/client/hud/layers/SendResourceModal.ts +++ b/src/client/hud/layers/SendResourceModal.ts @@ -1,7 +1,6 @@ import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { EventBus } from "../../../core/EventBus"; -import { GameView, PlayerView } from "../../../core/game/GameView"; import { within } from "../../../core/Util"; import { SendDonateGoldIntentEvent, @@ -9,6 +8,7 @@ import { } from "../../Transport"; import { UIState } from "../../UIState"; import { renderTroops, translateText } from "../../Utils"; +import { GameView, PlayerView } from "../../view"; @customElement("send-resource-modal") export class SendResourceModal extends LitElement { diff --git a/src/client/hud/layers/SpawnTimer.ts b/src/client/hud/layers/SpawnTimer.ts index 45fd1938f..8f712562f 100644 --- a/src/client/hud/layers/SpawnTimer.ts +++ b/src/client/hud/layers/SpawnTimer.ts @@ -2,10 +2,10 @@ import { LitElement, html } from "lit"; import { customElement } from "lit/decorators.js"; import { EventBus, GameEvent } from "../../../core/EventBus"; import { GameMode, GameType, Team } from "../../../core/game/Game"; -import { GameView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { themeProvider } from "../../theme/ThemeProvider"; import { TransformHandler } from "../../TransformHandler"; +import { GameView } from "../../view"; export class SpawnBarVisibleEvent implements GameEvent { constructor(public readonly visible: boolean) {} diff --git a/src/client/hud/layers/TeamStats.ts b/src/client/hud/layers/TeamStats.ts index 3df550093..92ee40c64 100644 --- a/src/client/hud/layers/TeamStats.ts +++ b/src/client/hud/layers/TeamStats.ts @@ -2,7 +2,6 @@ import { LitElement, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { EventBus } from "../../../core/EventBus"; import { GameMode, Team, UnitType } from "../../../core/game/Game"; -import { GameView, PlayerView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; import { formatPercentage, @@ -10,6 +9,7 @@ import { renderTroops, translateText, } from "../../Utils"; +import { GameView, PlayerView } from "../../view"; interface TeamEntry { teamName: string; diff --git a/src/client/hud/layers/UnitDisplay.ts b/src/client/hud/layers/UnitDisplay.ts index 8714d51b6..e3e9f1334 100644 --- a/src/client/hud/layers/UnitDisplay.ts +++ b/src/client/hud/layers/UnitDisplay.ts @@ -9,12 +9,12 @@ import { PlayerBuildableUnitType, UnitType, } from "../../../core/game/Game"; -import { GameView } from "../../../core/game/GameView"; import { UserSettings } from "../../../core/game/UserSettings"; import { Controller } from "../../Controller"; import { ToggleStructureEvent } from "../../InputHandler"; import { UIState } from "../../UIState"; import { renderNumber, translateText } from "../../Utils"; +import { GameView } from "../../view"; const warshipIcon = assetUrl("images/BattleshipIconWhite.svg"); const cityIcon = assetUrl("images/CityIconWhite.svg"); const factoryIcon = assetUrl("images/FactoryIconWhite.svg"); diff --git a/src/client/hud/layers/WinModal.ts b/src/client/hud/layers/WinModal.ts index 7c7b8fdd6..2728db546 100644 --- a/src/client/hud/layers/WinModal.ts +++ b/src/client/hud/layers/WinModal.ts @@ -9,7 +9,6 @@ import { import { EventBus } from "../../../core/EventBus"; import { RankedType } from "../../../core/game/Game"; import { GameUpdateType } from "../../../core/game/GameUpdates"; -import { GameView } from "../../../core/game/GameView"; import { getUserMe } from "../../Api"; import "../../components/CosmeticButton"; import { Controller } from "../../Controller"; @@ -21,6 +20,7 @@ import { import { crazyGamesSDK } from "../../CrazyGamesSDK"; import { Platform } from "../../Platform"; import { SendWinnerEvent } from "../../Transport"; +import { GameView } from "../../view"; @customElement("win-modal") export class WinModal extends LitElement implements Controller { diff --git a/src/client/render/CLAUDE.md b/src/client/render/CLAUDE.md index 8991f264c..b3f9dc4e9 100644 --- a/src/client/render/CLAUDE.md +++ b/src/client/render/CLAUDE.md @@ -19,7 +19,7 @@ WebGLFrameBuilder.update ← syncs palette, local-player ID, spawn uploadFrameData(view, frame) ← frame/Upload.ts — dispatches to view.update*() │ ▼ -GameView.update*() methods ← gl/GameView.ts — public facade +MapRenderer.update*() methods ← gl/MapRenderer.ts — public facade │ ▼ GPURenderer (gl/Renderer.ts) ← owns all passes @@ -44,7 +44,7 @@ each frame (and animate from local time, e.g. the spawn-overlay breath). | `frame/TrailManager.ts` | Mutates the per-tile trail texture; emits dirty row range | | `frame/RailroadCache.ts` | Maintains the railroad tile state buffer | | `gl/` | WebGL2 renderer internals | -| `gl/GameView.ts` | Public facade — what `WebGLFrameBuilder` and the client talk to | +| `gl/MapRenderer.ts` | Public facade — what `WebGLFrameBuilder` and the client talk to | | `gl/Renderer.ts` | Owns all passes, runs them in order each frame, manages FBOs | | `gl/Camera.ts` | World↔screen math; mutated externally each frame via `setCameraState` | | `gl/RenderSettings.ts` | Typed view of `render-settings.json` (tuning knobs) | @@ -163,6 +163,6 @@ builds its allocators and color derivations from the same theme JSONs — see defaults to `render-settings.json`. 4. Instantiate it in `GPURenderer`'s constructor and call its `draw` from the appropriate phase of `Renderer.render`. -5. Expose any needed setters on `GameView` (gl/GameView.ts). +5. Expose any needed setters on `MapRenderer` (gl/MapRenderer.ts). 6. Wire the data push from `WebGLFrameBuilder` or a controller — without this step the pass is dead code. diff --git a/src/client/render/frame/Upload.ts b/src/client/render/frame/Upload.ts index cc0774beb..2a835cec5 100644 --- a/src/client/render/frame/Upload.ts +++ b/src/client/render/frame/Upload.ts @@ -24,8 +24,6 @@ export interface FrameUploadTarget { dirtyRowMin: number, dirtyRowMax: number, ): void; - applyFullTiles(tileState: Uint16Array, trailState: Uint8Array): void; - applyDelta(changedTiles: TilePair[], trailState: Uint8Array): void; uploadRailroadState(data: Uint8Array): void; applyRailroadDust(tileRefs: number[]): void; updateUnits(units: ReadonlyMap, gameTick: number): void; @@ -45,54 +43,33 @@ export interface FrameUploadTarget { setSAMAllianceClusters(clusters: ReadonlyMap): void; } -export interface UploadOptions { - /** Snap name positions instantly (seek mode). Default: false. */ - snap?: boolean; - /** Skip tile upload — caller already handled tiles (e.g. seek with bloom reset). */ - skipTileUpload?: boolean; -} - /** * Upload a FrameData snapshot to the GPU view. * - * Handles tile upload mode switching, all view update calls, and conditional - * railroad/ephemeral uploads. The FrameData itself carries semantic differences - * (seek sets deadUnits=[], conquestEvents=[] etc.) — this function is a - * straightforward dispatch loop. + * A straightforward dispatch loop: pushes tile/trail deltas, then all the + * conditional railroad/ephemeral uploads, to the view's update*() methods. */ export function uploadFrameData( view: FrameUploadTarget, frame: FrameData, - opts?: UploadOptions, ): void { - const snap = opts?.snap ?? false; - const skipTileUpload = opts?.skipTileUpload ?? false; - // --- Tiles + Trails --- - // Live mode: changedTiles[] means "only these tiles changed" (empty = nothing changed, skip upload). - // changedTiles null/undefined means "no delta info" (first tick — full upload needed). - // Copy mode: changedTiles[] = delta playback, null = full seek. - if (!skipTileUpload) { - if (frame.tileMode === "live" && frame.changedTiles) { - // Live delta path — tiles and trails uploaded independently - if (frame.changedTiles.length > 0) { - view.uploadLiveDelta(frame.tileState, frame.changedTiles); - } - // Trail dirty rows come from TrailManager, independent of tile deltas - if (frame.trailDirtyRowMax >= 0) { - view.uploadLiveTrailDelta( - frame.trailState, - frame.trailDirtyRowMin, - frame.trailDirtyRowMax, - ); - } - } else if (frame.tileMode === "live") { - view.uploadTileAndTrailState(frame.tileState, frame.trailState); - } else if (!frame.changedTiles) { - view.applyFullTiles(frame.tileState, frame.trailState); - } else { - view.applyDelta(frame.changedTiles, frame.trailState); + // changedTiles[] means "only these tiles changed" (empty = nothing changed, + // skip upload). null means "no delta info" (first tick — full upload needed). + if (frame.changedTiles) { + if (frame.changedTiles.length > 0) { + view.uploadLiveDelta(frame.tileState, frame.changedTiles); } + // Trail dirty rows come from TrailManager, independent of tile deltas + if (frame.trailDirtyRowMax >= 0) { + view.uploadLiveTrailDelta( + frame.trailState, + frame.trailDirtyRowMin, + frame.trailDirtyRowMax, + ); + } + } else { + view.uploadTileAndTrailState(frame.tileState, frame.trailState); } // --- Railroads --- @@ -125,7 +102,7 @@ export function uploadFrameData( view.updateNukeTelegraphs(frame.nukeTelegraphs); // --- Names + player status --- - view.updateNames(frame.names, frame.players, snap, frame.playerStatus); + view.updateNames(frame.names, frame.players, false, frame.playerStatus); // --- Relations --- view.updateRelations(frame.relationMatrix, frame.relationSize); diff --git a/src/client/render/frame/index.ts b/src/client/render/frame/index.ts index 86b43b0ef..c754c78cc 100644 --- a/src/client/render/frame/index.ts +++ b/src/client/render/frame/index.ts @@ -4,4 +4,4 @@ export type { FrameData } from "../types"; // Upload export type { RelationMatrixResult } from "./derive/RelationMatrix"; export { uploadFrameData } from "./Upload"; -export type { FrameUploadTarget, UploadOptions } from "./Upload"; +export type { FrameUploadTarget } from "./Upload"; diff --git a/src/client/render/gl/Events.ts b/src/client/render/gl/Events.ts deleted file mode 100644 index 2953d6add..000000000 --- a/src/client/render/gl/Events.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { UnitState } from "../types"; - -/** Event data emitted by GameView for map interactions. */ -export interface MapPointerEvent { - /** CSS pixel X relative to viewport (clientX). */ - screenX: number; - /** CSS pixel Y relative to viewport (clientY). */ - screenY: number; - /** World-space X (fractional; floor for tile column). */ - worldX: number; - /** World-space Y (fractional; floor for tile row). */ - worldY: number; - /** Tile column (integer, -1 if out of bounds). */ - tileX: number; - /** Tile row (integer, -1 if out of bounds). */ - tileY: number; - /** Territory owner at this tile (0 = unowned/OOB). */ - ownerID: number; - /** Nearest mobile unit under cursor, or null. */ - unit: UnitState | null; - /** Nearest structure under cursor, or null. */ - structure: UnitState | null; - /** Mouse button: 0 = left, 1 = middle, 2 = right. */ - button: number; - /** Shift key held. */ - shiftKey: boolean; - /** Ctrl/Meta key held. */ - ctrlKey: boolean; - /** Alt key held. */ - altKey: boolean; -} - -/** Scroll event data emitted by GameView. */ -export interface MapScrollEvent { - deltaX: number; - deltaY: number; - shiftKey: boolean; - ctrlKey: boolean; - altKey: boolean; -} - -/** Alt-view temporarily peeked (space hold — enables altview + gridview). */ -export interface AltViewPeekEvent { - active: boolean; -} - -/** Grid-view default toggled (persistent resting state changed via 'M'). */ -export interface GridViewToggleEvent { - active: boolean; -} - -/** Map of event names to their payload types. */ -export interface GameViewEventMap { - /** Left-click (pointerdown + pointerup with < 10px movement). */ - click: MapPointerEvent; - /** Double-click. */ - dblclick: MapPointerEvent; - /** Middle-click (auxclick with button 1). */ - middleclick: MapPointerEvent; - /** Right-click / context menu. */ - contextmenu: MapPointerEvent; - /** Hovered entity changed (owner, unit, or structure differs from previous). */ - hover: MapPointerEvent; - /** Scroll with modifier keys (unmodified scroll is consumed by zoom). */ - scroll: MapScrollEvent; - /** User selected a radial menu item. */ - menuselect: RadialMenuSelectEvent; - /** Alt-view temporarily peeked (space hold — enables altview + gridview). */ - altviewpeek: AltViewPeekEvent; - /** Grid-view default toggled (M key). */ - gridviewtoggle: GridViewToggleEvent; - /** WebGL Context successfully restored after a loss. (Requires full state re-upload) */ - contextrestored: { type: "restored" }; -} - -/** A single item in the radial context menu. */ -export interface RadialMenuItem { - /** Unique identifier for this action. */ - id: string; - /** Emoji key into the atlas (e.g. "⚔️"), or empty string for no icon. */ - icon: string; - /** RGB color [0–1]. */ - color: [number, number, number]; - /** Whether this action is currently available. */ - enabled: boolean; - /** If present, clicking this item opens a submenu with these items. */ - subItems?: RadialMenuItem[]; -} - -/** Emitted when the user selects a radial menu item. */ -export interface RadialMenuSelectEvent { - /** Index of the selected segment. */ - index: number; - /** The item's id. */ - id: string; -} - -export type GameViewEventType = keyof GameViewEventMap; diff --git a/src/client/render/gl/GameView.ts b/src/client/render/gl/MapRenderer.ts similarity index 56% rename from src/client/render/gl/GameView.ts rename to src/client/render/gl/MapRenderer.ts index 8e11807c6..4ecc9c31b 100644 --- a/src/client/render/gl/GameView.ts +++ b/src/client/render/gl/MapRenderer.ts @@ -1,11 +1,14 @@ /** - * GameView — public facade for the openfront-gl renderer. + * MapRenderer — public facade for the WebGL map renderer. * - * Wraps GPURenderer (rendering) and Camera (viewport math) as private - * implementation details. Handles all user interaction: drag-to-pan, - * wheel-to-zoom, click detection, hover tracking, and hit-testing. + * Wraps GPURenderer as a private implementation detail and survives WebGL + * context loss: when the context is lost the renderer is disposed, and on + * restore a fresh GPURenderer is created and `onContextRestored` fires so + * the owner can re-upload all simulation state. * - * Consumers only touch GameView — they never import GPURenderer or Camera. + * This is a pure data sink. Input handling lives in InputHandler/EventBus; + * camera state is pushed in each frame via setCameraState. Consumers only + * touch MapRenderer — they never import GPURenderer or Camera. */ import type { Config } from "../../../core/configuration/Config"; @@ -25,27 +28,21 @@ import type { TilePair, UnitState, } from "../types"; -import type { - GameViewEventMap, - GameViewEventType, - RadialMenuItem, -} from "./Events"; import type { SpawnCenter } from "./passes/SpawnOverlayPass"; import type { AttackTroopLabel } from "./passes/WorldTextPass"; import { GPURenderer } from "./Renderer"; import type { RenderSettings } from "./RenderSettings"; -export class GameView { +export class MapRenderer { private renderer: GPURenderer | null = null; private resizeObs: ResizeObserver | null = null; - private listeners = new Map void>>(); - private cachedIcons: { key: string; img: CanvasImageSource }[] = []; - - // Stored for context recreation - private cachedOnFrame: ((ms: number) => void) | null = null; - private cachedAfterRender: ((canvas: HTMLCanvasElement) => void) | null = - null; + /** + * Called after a lost WebGL context is restored and the renderer has been + * recreated. The owner must re-upload all simulation state (textures and + * geometry are gone). + */ + onContextRestored: (() => void) | null = null; constructor( private canvas: HTMLCanvasElement, @@ -66,10 +63,10 @@ export class GameView { }); this.resizeObs.observe(canvas); - canvas.addEventListener("webglcontextlost", this.onContextLost, false); + canvas.addEventListener("webglcontextlost", this.handleContextLost, false); canvas.addEventListener( "webglcontextrestored", - this.onContextRestored, + this.handleContextRestored, false, ); } @@ -85,18 +82,11 @@ export class GameView { this.caf, ); - // Restore cached state - if (this.cachedIcons.length > 0) { - this.renderer.registerRadialMenuIcons(this.cachedIcons); - } - this.renderer.onFrame = this.cachedOnFrame; - this.renderer.afterRender = this.cachedAfterRender; - const rect = this.canvas.getBoundingClientRect(); if (rect.width > 0) this.renderer.resize(rect.width, rect.height); }; - private onContextLost = (e: Event) => { + private handleContextLost = (e: Event) => { e.preventDefault(); if (this.renderer) { this.renderer.dispose(); @@ -104,141 +94,19 @@ export class GameView { } }; - private onContextRestored = () => { + private handleContextRestored = () => { this.initRenderer(); - this.emit("contextrestored", { type: "restored" }); + this.onContextRestored?.(); }; - // ---- Event system ---- - - on( - event: K, - handler: (e: GameViewEventMap[K]) => void, - ): void { - let set = this.listeners.get(event); - if (!set) { - set = new Set(); - this.listeners.set(event, set); - } - set.add(handler as (e: unknown) => void); - } - - off( - event: K, - handler: (e: GameViewEventMap[K]) => void, - ): void { - this.listeners.get(event)?.delete(handler as (e: unknown) => void); - } - - private emit( - event: K, - data: GameViewEventMap[K], - ): void { - const set = this.listeners.get(event); - if (set) - for (const fn of set) (fn as (e: GameViewEventMap[K]) => void)(data); - } - - // ---- Radial menu ---- - - showRadialMenu( - screenX: number, - screenY: number, - items: RadialMenuItem[], - centerItem?: RadialMenuItem, - ): void { - this.renderer?.showRadialMenu(screenX, screenY, items, centerItem); - } - - hideRadialMenu(): void { - this.renderer?.hideRadialMenu(); - } - - openRadialSubMenu(subItems: RadialMenuItem[]): void { - this.renderer?.openRadialSubMenu(subItems); - } - - goBackRadialMenu(): void { - this.renderer?.goBackRadialMenu(); - } - - get radialMenuVisible(): boolean { - return this.renderer?.radialMenuVisible ?? false; - } - registerRadialMenuIcons( - icons: { key: string; img: CanvasImageSource }[], - ): void { - this.cachedIcons = icons; - this.renderer?.registerRadialMenuIcons(icons); - } - // ---- Camera ---- - screenToWorld(screenX: number, screenY: number): { x: number; y: number } { - return this.renderer?.screenToWorld(screenX, screenY) ?? { x: 0, y: 0 }; - } - - worldToScreen(worldX: number, worldY: number): { x: number; y: number } { - return this.renderer?.worldToScreen(worldX, worldY) ?? { x: 0, y: 0 }; - } - - panTo(worldX: number, worldY: number): void { - this.renderer?.panTo(worldX, worldY); - } - zoomTo(level: number): void { - this.renderer?.zoomTo(level); - } - fitMap(): void { - this.renderer?.fitMap(); - } - focusOwner(ownerID: number): void { - this.renderer?.focusOwner(ownerID); - } - - focusBBox( - minX: number, - minY: number, - maxX: number, - maxY: number, - padding?: number, - ): void { - this.renderer?.focusBBox(minX, minY, maxX, maxY, padding); - } - - getCameraState(): { x: number; y: number; z: number } { - return this.renderer?.getCameraState() ?? { x: 0, y: 0, z: 1 }; - } - setCameraState(x: number, y: number, z: number): void { this.renderer?.setCameraState(x, y, z); } - getOwnerAtWorld(worldX: number, worldY: number): number { - return this.renderer?.getOwnerAtWorld(worldX, worldY) ?? 0; - } - // ---- Data upload ---- - applyFullFrame( - tileState: Uint16Array, - trailState: Uint8Array, - nukeEvents?: Array<{ tick: number; tiles: number[] }>, - currentTick?: number, - ): void { - this.renderer?.applyFullFrame( - tileState, - trailState, - nukeEvents, - currentTick, - ); - } - - applyFullTiles(tileState: Uint16Array, trailState: Uint8Array): void { - this.renderer?.applyFullTiles(tileState, trailState); - } - applyDelta(changedTiles: TilePair[], trailState: Uint8Array): void { - this.renderer?.applyDelta(changedTiles, trailState); - } uploadLiveDelta(tileState: Uint16Array, changedTiles: TilePair[]): void { this.renderer?.uploadLiveDelta(tileState, changedTiles); } @@ -318,12 +186,6 @@ export class GameView { updateAttackRings(rings: AttackRingInput[]): void { this.renderer?.updateAttackRings(rings); } - clearFx(): void { - this.renderer?.clearFx(); - } - setFxTimeFn(fn: () => number): void { - this.renderer?.setFxTimeFn(fn); - } /** Update ghost structure preview (build-mode visualization). null = clear. */ updateGhostPreview(data: GhostPreviewData | null): void { @@ -349,11 +211,6 @@ export class GameView { // ---- Selection box ---- - /** Show/hide the stippled selection box around a unit (warship selection). */ - setSelectedUnit(unitId: number | null): void { - this.renderer?.setSelectedUnit(unitId); - } - /** Set multiple selected units (multi-select). Pass [] to clear. */ setSelectedUnits(unitIds: readonly number[]): void { this.renderer?.setSelectedUnits(unitIds); @@ -364,17 +221,8 @@ export class GameView { this.renderer?.showMoveIndicator(tileX, tileY, ownerID); } - // ---- SAM radius (replay) ---- + // ---- SAM radius ---- - setSAMRadiusVisible(visible: boolean): void { - this.renderer?.setSAMRadiusVisible(visible); - } - setSAMPerspective(playerID: number, allies: Set): void { - this.renderer?.setSAMPerspective(playerID, allies); - } - setSAMColorMode(mode: "perspective" | "owner"): void { - this.renderer?.setSAMColorMode(mode); - } setSAMAllianceClusters(clusters: Map): void { this.renderer?.setSAMAllianceClusters(clusters); } @@ -409,29 +257,18 @@ export class GameView { getSettings(): RenderSettings { return this.renderer?.getSettings() ?? ({} as RenderSettings); } - get fps(): number { - return this.renderer?.fps ?? 0; - } - set onFrame(cb: ((ms: number) => void) | null) { - this.cachedOnFrame = cb; - if (this.renderer) this.renderer.onFrame = cb; - } - set afterRender(cb: ((canvas: HTMLCanvasElement) => void) | null) { - this.cachedAfterRender = cb; - if (this.renderer) this.renderer.afterRender = cb; - } // ---- Lifecycle ---- dispose(): void { this.resizeObs?.disconnect(); this.resizeObs = null; - this.listeners.clear(); + this.onContextRestored = null; this.renderer?.dispose(); - this.canvas.removeEventListener("webglcontextlost", this.onContextLost); + this.canvas.removeEventListener("webglcontextlost", this.handleContextLost); this.canvas.removeEventListener( "webglcontextrestored", - this.onContextRestored, + this.handleContextRestored, ); } } diff --git a/src/client/render/gl/Renderer.ts b/src/client/render/gl/Renderer.ts index b10fdaa4d..7a3b918e7 100644 --- a/src/client/render/gl/Renderer.ts +++ b/src/client/render/gl/Renderer.ts @@ -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 = new Map(); private lastStructures: Map = 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): 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): 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(); diff --git a/src/client/render/gl/index.ts b/src/client/render/gl/index.ts index 64a77c057..8f742922a 100644 --- a/src/client/render/gl/index.ts +++ b/src/client/render/gl/index.ts @@ -1,17 +1,9 @@ export type { AttackRingInput } from "../types"; // createDebugGui is intentionally not re-exported here — it pulls lil-gui and // the debug GUI into the main bundle; dynamically import "./debug/index". -export type { - GameViewEventMap, - GameViewEventType, - MapPointerEvent, - MapScrollEvent, - RadialMenuItem, - RadialMenuSelectEvent, -} from "./Events"; -export { GameView } from "./GameView"; export { GraphicsOverridesSchema } from "./GraphicsOverrides"; export type { GraphicsOverrides } from "./GraphicsOverrides"; +export { MapRenderer } from "./MapRenderer"; export { preloadAtlasData } from "./passes/name-pass/AtlasData"; export type { SpawnCenter } from "./passes/SpawnOverlayPass"; export { diff --git a/src/client/render/gl/passes/RadialMenuPass.ts b/src/client/render/gl/passes/RadialMenuPass.ts deleted file mode 100644 index 2518298ec..000000000 --- a/src/client/render/gl/passes/RadialMenuPass.ts +++ /dev/null @@ -1,577 +0,0 @@ -/** - * RadialMenuPass — renders a radial (pie-wheel) context menu as screen-space - * arc segments with emoji icons. - * - * Supports one level of submenus: when a submenu is open, the parent items - * shrink into a smaller inner ring, a back button appears in the center, and - * the submenu items take the outer ring. - * - * Rendering elements (reused for each ring via drawRing): - * 1. Arcs: single quad with SDF annulus + angular segment masking + borders - * 2. Center button: filled circle drawn by the innermost ring - * 3. Icons: instanced quads sampling the emoji atlas - */ - -import type { RadialMenuItem } from "../Events"; -import { createProgram } from "../utils/GlUtils"; - -import arcFragSrc from "../shaders/radial-menu/arcs.frag.glsl?raw"; -import arcVertSrc from "../shaders/radial-menu/arcs.vert.glsl?raw"; -import iconFragSrc from "../shaders/radial-menu/icon.frag.glsl?raw"; -import iconVertSrc from "../shaders/radial-menu/icon.vert.glsl?raw"; - -import emojiAtlasMeta from "resources/atlases/emoji-atlas-meta.json"; -import { assetUrl } from "src/core/AssetUrls"; - -const emojiAtlasUrl = assetUrl("atlases/emoji-atlas.png"); - -// --------------------------------------------------------------------------- -// Ring layout configs (CSS pixels) -// --------------------------------------------------------------------------- - -interface RingConfig { - outerR: number; - innerR: number; - /** Icon half-size; if a function, receives the segment count. */ - iconHalf: number | ((n: number) => number); - /** Opacity multiplier applied to colors (1 = full, <1 = dimmed). */ - dim: number; -} - -/** Normal top-level ring (game: innerRadius 40, arcWidth 55). */ -const RING_NORMAL: RingConfig = { - outerR: 95, - innerR: 40, - iconHalf: (n) => (n <= 4 ? 20 : n <= 6 ? 17 : 14), - dim: 1.0, -}; - -/** Submenu active ring (game: innerRadius 75, arcWidth 65). */ -const RING_SUBMENU: RingConfig = { - outerR: 140, - innerR: 75, - iconHalf: (n) => (n <= 4 ? 22 : n <= 6 ? 18 : 14), - dim: 1.0, -}; - -/** Parent ring when submenu is open (game: scales to 0.65). */ -const RING_PARENT: RingConfig = { - outerR: 70, - innerR: 32, - iconHalf: 12, - dim: 0.5, -}; -const MAX_SEGMENTS = 8; - -/** Hit-test return value for the center button. */ -export const CENTER_INDEX = -2; - -const BACK_ITEM: RadialMenuItem = { - id: "__back__", - icon: "back-icon", - color: [0.45, 0.45, 0.45], - enabled: true, -}; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function buildEmojiMap(): Map { - const map = new Map(); - const emojis = (emojiAtlasMeta as { emojis: Record }).emojis; - for (const [key, idx] of Object.entries(emojis)) { - map.set(key, idx); - } - return map; -} - -// --------------------------------------------------------------------------- -// RadialMenuPass -// --------------------------------------------------------------------------- - -export class RadialMenuPass { - private gl: WebGL2RenderingContext; - - // Programs - private arcProg: WebGLProgram; - private iconProg: WebGLProgram; - private vao: WebGLVertexArrayObject; - - // Arc uniform locations - private arcU: { - anchor: WebGLUniformLocation; - viewport: WebGLUniformLocation; - outerR: WebGLUniformLocation; - innerR: WebGLUniformLocation; - segCount: WebGLUniformLocation; - hoveredSeg: WebGLUniformLocation; - segColors: WebGLUniformLocation; - hasCenterBtn: WebGLUniformLocation; - centerColor: WebGLUniformLocation; - centerHovered: WebGLUniformLocation; - }; - - // Icon uniform locations - private iconU: { - anchor: WebGLUniformLocation; - viewport: WebGLUniformLocation; - outerR: WebGLUniformLocation; - innerR: WebGLUniformLocation; - segCount: WebGLUniformLocation; - iconHalf: WebGLUniformLocation; - emojiIndices: WebGLUniformLocation; - centerEmojiIdx: WebGLUniformLocation; - segOpacity: WebGLUniformLocation; - emojiAtlas: WebGLUniformLocation; - emojiCell: WebGLUniformLocation; - emojiCols: WebGLUniformLocation; - emojiAtlasW: WebGLUniformLocation; - emojiAtlasH: WebGLUniformLocation; - }; - - // Emoji + icon atlas - private emojiTex: WebGLTexture | null = null; - private emojiReady = false; - private emojiMap: Map; - private atlasImg: HTMLImageElement | null = null; - private pendingIcons: { key: string; img: CanvasImageSource }[] = []; - - // ---- State ---- - private visible = false; - private anchorX = 0; - private anchorY = 0; - private items: RadialMenuItem[] = []; - private centerItem: RadialMenuItem | null = null; - private hoveredIndex = -1; // -1 = none, 0..n-1 = segment, CENTER_INDEX = center - - // Submenu (one level) - private _inSubmenu = false; - private savedItems: RadialMenuItem[] = []; - private savedCenterItem: RadialMenuItem | null = null; - - constructor(gl: WebGL2RenderingContext) { - this.gl = gl; - this.emojiMap = buildEmojiMap(); - - // Shared quad VAO - this.vao = gl.createVertexArray()!; - gl.bindVertexArray(this.vao); - const quadBuf = gl.createBuffer()!; - gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1]), - gl.STATIC_DRAW, - ); - gl.enableVertexAttribArray(0); - gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); - gl.bindVertexArray(null); - - // Arc program - this.arcProg = createProgram(gl, arcVertSrc, arcFragSrc); - this.arcU = { - anchor: gl.getUniformLocation(this.arcProg, "uAnchor")!, - viewport: gl.getUniformLocation(this.arcProg, "uViewport")!, - outerR: gl.getUniformLocation(this.arcProg, "uOuterR")!, - innerR: gl.getUniformLocation(this.arcProg, "uInnerR")!, - segCount: gl.getUniformLocation(this.arcProg, "uSegCount")!, - hoveredSeg: gl.getUniformLocation(this.arcProg, "uHoveredSeg")!, - segColors: gl.getUniformLocation(this.arcProg, "uSegColors")!, - hasCenterBtn: gl.getUniformLocation(this.arcProg, "uHasCenterBtn")!, - centerColor: gl.getUniformLocation(this.arcProg, "uCenterColor")!, - centerHovered: gl.getUniformLocation(this.arcProg, "uCenterHovered")!, - }; - - // Icon program - this.iconProg = createProgram(gl, iconVertSrc, iconFragSrc); - gl.useProgram(this.iconProg); - gl.uniform1i(gl.getUniformLocation(this.iconProg, "uEmojiAtlas"), 0); - const em = emojiAtlasMeta as { - width: number; - height: number; - cellSize: number; - cols: number; - }; - gl.uniform1f( - gl.getUniformLocation(this.iconProg, "uEmojiCell")!, - em.cellSize, - ); - gl.uniform1f(gl.getUniformLocation(this.iconProg, "uEmojiCols")!, em.cols); - gl.uniform1f( - gl.getUniformLocation(this.iconProg, "uEmojiAtlasW")!, - em.width, - ); - gl.uniform1f( - gl.getUniformLocation(this.iconProg, "uEmojiAtlasH")!, - em.height, - ); - - this.iconU = { - anchor: gl.getUniformLocation(this.iconProg, "uAnchor")!, - viewport: gl.getUniformLocation(this.iconProg, "uViewport")!, - outerR: gl.getUniformLocation(this.iconProg, "uOuterR")!, - innerR: gl.getUniformLocation(this.iconProg, "uInnerR")!, - segCount: gl.getUniformLocation(this.iconProg, "uSegCount")!, - iconHalf: gl.getUniformLocation(this.iconProg, "uIconHalf")!, - emojiIndices: gl.getUniformLocation(this.iconProg, "uEmojiIndices")!, - centerEmojiIdx: gl.getUniformLocation(this.iconProg, "uCenterEmojiIdx")!, - segOpacity: gl.getUniformLocation(this.iconProg, "uSegOpacity")!, - emojiAtlas: gl.getUniformLocation(this.iconProg, "uEmojiAtlas")!, - emojiCell: gl.getUniformLocation(this.iconProg, "uEmojiCell")!, - emojiCols: gl.getUniformLocation(this.iconProg, "uEmojiCols")!, - emojiAtlasW: gl.getUniformLocation(this.iconProg, "uEmojiAtlasW")!, - emojiAtlasH: gl.getUniformLocation(this.iconProg, "uEmojiAtlasH")!, - }; - - this.loadEmojiAtlas(); - } - - private loadEmojiAtlas(): void { - const img = new Image(); - img.crossOrigin = "anonymous"; - img.onload = () => { - this.atlasImg = img; - this.rebuildAtlasTexture(); - }; - img.src = emojiAtlasUrl; - } - - /** - * Register additional icon images to append to the atlas texture. - * Call from the adapter after loading game SVG icons. - */ - registerIcons(icons: { key: string; img: CanvasImageSource }[]): void { - this.pendingIcons = icons; - if (this.atlasImg) this.rebuildAtlasTexture(); - } - - private rebuildAtlasTexture(): void { - if (!this.atlasImg) return; - - const gl = this.gl; - const meta = emojiAtlasMeta as { - width: number; - height: number; - cellSize: number; - cols: number; - emojis: Record; - }; - const baseCount = Object.keys(meta.emojis).length; - const totalCount = baseCount + this.pendingIcons.length; - const rows = Math.ceil(totalCount / meta.cols); - const height = Math.max(meta.height, rows * meta.cellSize); - - const canvas = document.createElement("canvas"); - canvas.width = meta.width; - canvas.height = height; - const ctx = canvas.getContext("2d")!; - - // Draw existing emoji atlas - ctx.drawImage(this.atlasImg, 0, 0); - - // Append extra icons into new cells (preserving aspect ratio) - // Minimal padding — SVGs are already clean vectors, maximize resolution - const pad = Math.floor(meta.cellSize * 0.04); - const size = meta.cellSize - pad * 2; - for (let i = 0; i < this.pendingIcons.length; i++) { - const idx = baseCount + i; - const col = idx % meta.cols; - const row = Math.floor(idx / meta.cols); - const img = this.pendingIcons[i].img; - const nw = (img as HTMLImageElement).naturalWidth || size; - const nh = (img as HTMLImageElement).naturalHeight || size; - const aspect = nw / nh; - let dw = size, - dh = size; - if (aspect > 1) dh = size / aspect; - else dw = size * aspect; - const ox = (size - dw) / 2; - const oy = (size - dh) / 2; - ctx.drawImage( - img, - col * meta.cellSize + pad + ox, - row * meta.cellSize + pad + oy, - dw, - dh, - ); - this.emojiMap.set(this.pendingIcons[i].key, idx); - } - - // Upload texture - this.emojiTex ??= gl.createTexture()!; - gl.bindTexture(gl.TEXTURE_2D, this.emojiTex); - gl.texParameteri( - gl.TEXTURE_2D, - gl.TEXTURE_MIN_FILTER, - gl.LINEAR_MIPMAP_LINEAR, - ); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); - gl.generateMipmap(gl.TEXTURE_2D); - this.emojiReady = true; - - // Update atlas height uniform (texture may be taller now) - gl.useProgram(this.iconProg); - gl.uniform1f(this.iconU.emojiAtlasH, height); - } - - resolveEmoji(icon: string): number { - return this.emojiMap.get(icon) ?? -1; - } - - // --------------------------------------------------------------------------- - // Public API - // --------------------------------------------------------------------------- - - show( - anchorX: number, - anchorY: number, - items: RadialMenuItem[], - centerItem?: RadialMenuItem, - ): void { - this.visible = true; - this.anchorX = anchorX; - this.anchorY = anchorY; - this.items = items.slice(0, MAX_SEGMENTS); - this.centerItem = centerItem ?? null; - // Cursor is at the anchor — center button starts hovered - this.hoveredIndex = this.centerItem ? CENTER_INDEX : -1; - this._inSubmenu = false; - this.savedItems = []; - this.savedCenterItem = null; - } - - openSubMenu(subItems: RadialMenuItem[]): void { - this.savedItems = this.items; - this.savedCenterItem = this.centerItem; - this.items = subItems.slice(0, MAX_SEGMENTS); - this.centerItem = BACK_ITEM; - this._inSubmenu = true; - this.hoveredIndex = -1; - } - - goBack(): void { - if (!this._inSubmenu) return; - this.items = this.savedItems; - this.centerItem = this.savedCenterItem; - this._inSubmenu = false; - this.savedItems = []; - this.savedCenterItem = null; - this.hoveredIndex = -1; - } - - hide(): void { - this.visible = false; - this.hoveredIndex = -1; - this._inSubmenu = false; - this.savedItems = []; - this.savedCenterItem = null; - } - - setHover(index: number): void { - this.hoveredIndex = index; - } - - get isVisible(): boolean { - return this.visible; - } - get inSubmenu(): boolean { - return this._inSubmenu; - } - getItems(): readonly RadialMenuItem[] { - return this.items; - } - getCenterItem(): RadialMenuItem | null { - return this.centerItem; - } - - /** Look up an item by hit-test index. */ - getItemAt(index: number): RadialMenuItem | null { - if (index === CENTER_INDEX) return this.centerItem; - if (index >= 0 && index < this.items.length) return this.items[index]; - return null; - } - - // --------------------------------------------------------------------------- - // Hit testing - // --------------------------------------------------------------------------- - - hitTest(screenX: number, screenY: number): number { - if (!this.visible) return -1; - const dx = screenX - this.anchorX; - const dy = screenY - this.anchorY; - const dist = Math.sqrt(dx * dx + dy * dy); - - const active = this._inSubmenu ? RING_SUBMENU : RING_NORMAL; - const centerR = this._inSubmenu ? RING_PARENT.innerR : RING_NORMAL.innerR; - const ringInner = active.innerR; - const ringOuter = active.outerR; - - // Center button - if (dist < centerR) return this.centerItem ? CENTER_INDEX : -1; - - // Gap / parent ring zone (non-interactive) - if (dist < ringInner) return -1; - - // Active ring - if (dist > ringOuter || this.items.length === 0) return -1; - - let angle = Math.atan2(dx, -dy); // 0 = top, CW positive - if (angle < 0) angle += Math.PI * 2; - const n = this.items.length; - const segArc = (Math.PI * 2) / n; - // Rotate so first segment is centered at top (game: startAngle = -π/n) - const shifted = (angle + Math.PI / n) % (Math.PI * 2); - return Math.min(Math.floor(shifted / segArc), n - 1); - } - - // --------------------------------------------------------------------------- - // Rendering - // --------------------------------------------------------------------------- - - draw(): void { - if (!this.visible) return; - if (this.items.length === 0 && !this.centerItem) return; - - const gl = this.gl; - const dpr = window.devicePixelRatio || 1; - const vw = gl.drawingBufferWidth; - const vh = gl.drawingBufferHeight; - const ax = this.anchorX * dpr; - const ay = this.anchorY * dpr; - - gl.enable(gl.BLEND); - gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); - gl.bindVertexArray(this.vao); - - // Parent ring (dimmed, non-interactive) — drawn first so active ring overlays - if (this._inSubmenu && this.savedItems.length > 0) { - const p = RING_PARENT; - this.drawRing( - ax, - ay, - vw, - vh, - p, - this.savedItems, - -1, - BACK_ITEM, - this.hoveredIndex === CENTER_INDEX, - ); - } - - // Active ring — expands when in submenu - const active = this._inSubmenu ? RING_SUBMENU : RING_NORMAL; - this.drawRing( - ax, - ay, - vw, - vh, - active, - this.items, - this.hoveredIndex >= 0 ? this.hoveredIndex : -1, - this._inSubmenu ? null : this.centerItem, - !this._inSubmenu && this.hoveredIndex === CENTER_INDEX, - ); - } - - /** Draw a single ring (arcs + icons) using a RingConfig. */ - private drawRing( - ax: number, - ay: number, - vw: number, - vh: number, - cfg: RingConfig, - items: readonly RadialMenuItem[], - hoveredSeg: number, - centerItem: RadialMenuItem | null, - centerHovered: boolean, - ): void { - const gl = this.gl; - const dpr = window.devicePixelRatio || 1; - const n = items.length; - const hasCenter = centerItem !== null; - const outerR = cfg.outerR * dpr; - const innerFrac = cfg.innerR / cfg.outerR; - const dim = cfg.dim; - const ih = - typeof cfg.iconHalf === "function" ? cfg.iconHalf(n) : cfg.iconHalf; - const iconHalf = ih * dpr; - - // --- Arcs --- - gl.useProgram(this.arcProg); - gl.uniform2f(this.arcU.anchor, ax, ay); - gl.uniform2f(this.arcU.viewport, vw, vh); - gl.uniform1f(this.arcU.outerR, outerR); - gl.uniform1f(this.arcU.innerR, innerFrac); - gl.uniform1i(this.arcU.segCount, n); - gl.uniform1i(this.arcU.hoveredSeg, hoveredSeg); - - gl.uniform1i(this.arcU.hasCenterBtn, hasCenter ? 1 : 0); - if (hasCenter) { - const cc = centerItem.color; - gl.uniform3f( - this.arcU.centerColor, - cc[0] * dim, - cc[1] * dim, - cc[2] * dim, - ); - gl.uniform1i(this.arcU.centerHovered, centerHovered ? 1 : 0); - } - - const colors = new Float32Array(MAX_SEGMENTS * 4); - for (let i = 0; i < n; i++) { - const c = items[i].color; - colors[i * 4 + 0] = c[0] * dim; - colors[i * 4 + 1] = c[1] * dim; - colors[i * 4 + 2] = c[2] * dim; - colors[i * 4 + 3] = items[i].enabled ? 1 : 0; - } - gl.uniform4fv(this.arcU.segColors, colors); - gl.drawArrays(gl.TRIANGLES, 0, 6); - - // --- Icons --- - if (!this.emojiReady || (n === 0 && !hasCenter)) return; - - gl.useProgram(this.iconProg); - gl.uniform2f(this.iconU.anchor, ax, ay); - gl.uniform2f(this.iconU.viewport, vw, vh); - gl.uniform1f(this.iconU.outerR, outerR); - gl.uniform1f(this.iconU.innerR, innerFrac); - gl.uniform1i(this.iconU.segCount, n); - gl.uniform1f(this.iconU.iconHalf, iconHalf); - - const indices = new Float32Array(MAX_SEGMENTS); - const opacities = new Float32Array(MAX_SEGMENTS); - indices.fill(-1); - opacities.fill(1); - for (let i = 0; i < n; i++) { - indices[i] = this.resolveEmoji(items[i].icon); - opacities[i] = items[i].enabled ? 1.0 : 0.3; - } - gl.uniform1fv(this.iconU.emojiIndices, indices); - gl.uniform1fv(this.iconU.segOpacity, opacities); - - const centerIdx = hasCenter ? this.resolveEmoji(centerItem.icon) : -1; - gl.uniform1f(this.iconU.centerEmojiIdx, centerIdx); - - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this.emojiTex!); - gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, n + 1); - } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - dispose(): void { - const gl = this.gl; - gl.deleteProgram(this.arcProg); - gl.deleteProgram(this.iconProg); - gl.deleteVertexArray(this.vao); - if (this.emojiTex) gl.deleteTexture(this.emojiTex); - } -} diff --git a/src/client/render/gl/passes/TerritoryPass.ts b/src/client/render/gl/passes/TerritoryPass.ts index 3139eaf74..e83e07c41 100644 --- a/src/client/render/gl/passes/TerritoryPass.ts +++ b/src/client/render/gl/passes/TerritoryPass.ts @@ -16,7 +16,7 @@ import type { TilePair } from "../../types"; import type { RenderSettings } from "../RenderSettings"; import { getPaletteSize } from "../utils/ColorUtils"; import { createMapQuad, createProgram, shaderSrc } from "../utils/GlUtils"; -import { OWNER_MASK, TILE_DEFINES } from "../utils/TileCodec"; +import { TILE_DEFINES } from "../utils/TileCodec"; import overlayVertSrc from "../shaders/map-overlay/overlay.vert.glsl?raw"; import territoryFragSrc from "../shaders/map-overlay/territory.frag.glsl?raw"; @@ -193,15 +193,6 @@ export class TerritoryPass { // Tile data upload // --------------------------------------------------------------------------- - /** Full tile state upload (on seek). */ - uploadFullTileState(tileState: Uint16Array): void { - this.cpuTileState.set(tileState); - this.clearDripBuckets(); - this.scatter.clear(); - this.fullUploadPending = true; - this.tilesDirty = true; - } - /** Live-game path: snapshot the initial tile state and clear pending drip. */ setLiveRef(tileState: Uint16Array): void { this.cpuTileState.set(tileState); @@ -221,25 +212,6 @@ export class TerritoryPass { this.borderPatchConsumer = fn; } - /** Apply tile deltas (during playback). */ - uploadDeltaTiles(changedTiles: TilePair[]): void { - const ts = this.cpuTileState; - const w = this.mapW; - const pending = this.fullUploadPending; - const borderFn = this.borderPatchConsumer; - for (let i = 0; i < changedTiles.length; i++) { - const tp = changedTiles[i]; - ts[tp.ref] = tp.state; - if (!pending) { - const x = tp.ref % w; - const y = (tp.ref - x) / w; - this.scatter.push(x, y, tp.state); - if (borderFn) borderFn(x, y); - } - } - this.tilesDirty = true; - } - /** * Live delta: dispatch each changed tile into a round-robin drip bucket. * Stable per-ref hash means repeated updates to the same tile stay in @@ -317,43 +289,6 @@ export class TerritoryPass { this.currentBucket = 0; } - // --------------------------------------------------------------------------- - // Queries - // --------------------------------------------------------------------------- - - /** - * Get ownerID at a tile reference. Returns 0 for unowned. - * Reads display state (post-drip), so queries match what's visible. - */ - getOwnerAt(tileRef: number): number { - const ts = this.cpuTileState; - if (tileRef < 0 || tileRef >= ts.length) return 0; - return ts[tileRef] & OWNER_MASK; - } - - /** AABB of all tiles owned by ownerID. */ - getBBoxForOwner( - ownerID: number, - ): { minX: number; minY: number; maxX: number; maxY: number } | null { - let minX = Infinity, - minY = Infinity, - maxX = -Infinity, - maxY = -Infinity; - const w = this.mapW; - const ts = this.cpuTileState; - for (let i = 0; i < ts.length; i++) { - if ((ts[i] & OWNER_MASK) === ownerID) { - const x = i % w; - const y = (i - x) / w; - if (x < minX) minX = x; - if (x > maxX) maxX = x; - if (y < minY) minY = y; - if (y > maxY) maxY = y; - } - } - return minX === Infinity ? null : { minX, minY, maxX, maxY }; - } - // --------------------------------------------------------------------------- // GPU flush + draw // --------------------------------------------------------------------------- diff --git a/src/client/render/gl/passes/TrailPass.ts b/src/client/render/gl/passes/TrailPass.ts index c98d8198c..6bb658ec1 100644 --- a/src/client/render/gl/passes/TrailPass.ts +++ b/src/client/render/gl/passes/TrailPass.ts @@ -115,25 +115,6 @@ export class TrailPass { this.trailsDirty = true; } - /** Full trail state upload (on seek). */ - uploadFullState(trailState: Uint8Array): void { - this.liveTrailRef = null; - this.cpuTrailState.set(trailState); - this.trailsDirty = true; - } - - /** Set a single trail tile (during playback advance). */ - setTile(ref: number, ownerID: number): void { - this.cpuTrailState[ref] = ownerID; - this.trailsDirty = true; - } - - /** Clear all trails (on seek before rebuilding). */ - clear(): void { - this.cpuTrailState.fill(0); - this.trailsDirty = true; - } - /** Flush trail texture to GPU. Called once per render frame in uploadTextures. */ flushTexture(): void { if (!this.trailsDirty) return; diff --git a/src/client/render/gl/shaders/radial-menu/arcs.frag.glsl b/src/client/render/gl/shaders/radial-menu/arcs.frag.glsl deleted file mode 100644 index 12481b223..000000000 --- a/src/client/render/gl/shaders/radial-menu/arcs.frag.glsl +++ /dev/null @@ -1,132 +0,0 @@ -#version 300 es -precision highp float; - -in vec2 vLocal; // [-1, +1], distance 1.0 = outerR - -uniform float uInnerR; // inner radius as fraction of outerR [0,1] -uniform int uSegCount; // number of segments (1..8) -uniform int uHoveredSeg; // hovered segment index (-1 = none) -uniform vec4 uSegColors[8]; // per-segment: rgb + enabled (a: 1 = enabled, 0 = disabled) - -// Center button -uniform int uHasCenterBtn; // 1 = show center button -uniform vec3 uCenterColor; // center button RGB -uniform int uCenterHovered; // 1 = center button hovered - -out vec4 fragColor; - -const float GAP = 0.03; // radians gap between segments (game: padAngle 0.03) -const float AA = 0.010; // anti-alias width (normalized coords) -const float BORDER_W = 0.024; // border width, non-hovered -const float BORDER_W_HOV = 0.034; // border width, hovered (thicker) -const float PI = 3.14159265359; -const float TWO_PI = 6.28318530718; - -void main() { - float dist = length(vLocal); - - // --- Center button zone --- - if (dist < uInnerR - AA) { - if (uHasCenterBtn == 0) discard; - - // Solid center fill — fade alpha only at outer edge - float centerAlpha = 1.0 - smoothstep(uInnerR - AA * 3.0, uInnerR - AA, dist); - - bool cHov = uCenterHovered > 0; - float cbw = cHov ? BORDER_W_HOV : BORDER_W; - vec3 cbCol = cHov ? vec3(1.0) : vec3(0.88); - - // Crisp border at outer edge of center circle - float borderDist = uInnerR - AA - dist; - float border = 1.0 - smoothstep(cbw - AA, cbw + AA, borderDist); - - vec3 color = uCenterColor; - if (cHov) color = mix(color, vec3(1.0), 0.2); - color = mix(color, cbCol, border); - - float cAlpha = cHov ? 0.92 : 0.6; - fragColor = vec4(color, cAlpha * centerAlpha); - return; - } - - // --- Ring zone --- - if (uSegCount == 0) discard; // center-only mode - - // Annulus mask - float outer = 1.0 - smoothstep(1.0 - AA, 1.0, dist); - float inner = smoothstep(uInnerR - AA, uInnerR + AA, dist); - float ring = outer * inner; - if (ring < 0.01) discard; - - // Angle: 0 at top, increasing clockwise [0, 2π] - float angle = atan(vLocal.x, -vLocal.y); - if (angle < 0.0) angle += TWO_PI; - - // Rotate so first segment is centered at top (game: startAngle = -π/n) - float segArc = TWO_PI / float(uSegCount); - float offset = PI / float(uSegCount); - float shifted = mod(angle + offset, TWO_PI); - - // Segment index (in rotated space) - int segIdx = int(floor(shifted / segArc)); - segIdx = min(segIdx, uSegCount - 1); - - // Gap mask between segments - float segStart = float(segIdx) * segArc; - float segEnd = segStart + segArc; - float halfGap = GAP * 0.5; - - float gap = 1.0; - if (uSegCount > 1) { - gap = smoothstep(segStart + halfGap - AA, segStart + halfGap + AA, shifted) - * (1.0 - smoothstep(segEnd - halfGap - AA, segEnd - halfGap + AA, shifted)); - } - - float alpha = ring * gap; - if (alpha < 0.01) discard; - - // Segment color + hover state - vec4 seg = uSegColors[segIdx]; - vec3 color = seg.rgb; - bool enabled = seg.a > 0.5; - bool hovered = (segIdx == uHoveredSeg && enabled); - - // Pick border width & color based on hover - float bw = hovered ? BORDER_W_HOV : BORDER_W; - vec3 borderCol = hovered ? vec3(1.0) : vec3(0.88); - - // --- Borders --- - // Outer edge - float outerBorder = 1.0 - smoothstep(bw - AA, bw + AA, 1.0 - dist); - // Inner edge - float innerBorder = 1.0 - smoothstep(bw - AA, bw + AA, dist - uInnerR); - // Radial lines at gap edges - float angBorder = 0.0; - if (uSegCount > 1) { - float angleInSeg = shifted - segStart; - float distToStart = angleInSeg - halfGap; - float distToEnd = (segArc - halfGap) - angleInSeg; - // Convert angular distance to approximate normalized arc-length - float nearestAng = min(distToStart, distToEnd) * dist; - angBorder = 1.0 - smoothstep(bw - AA, bw + AA, nearestAng); - } - float border = max(max(outerBorder, innerBorder), angBorder); - - // Disabled segments: desaturate + darken - if (!enabled) { - float lum = dot(color, vec3(0.3, 0.6, 0.1)); - color = vec3(lum) * 0.4; - } - - // Hover highlight: brighten fill - if (hovered) { - color = mix(color, vec3(1.0), 0.2); - } - - // Blend border on top - color = mix(color, borderCol, border); - - // Opacity: hovered → nearly opaque, default → slightly transparent, disabled → dim - float segAlpha = enabled ? (hovered ? 0.92 : 0.6) : 0.4; - fragColor = vec4(color, alpha * segAlpha); -} diff --git a/src/client/render/gl/shaders/radial-menu/arcs.vert.glsl b/src/client/render/gl/shaders/radial-menu/arcs.vert.glsl deleted file mode 100644 index 5e5b379ab..000000000 --- a/src/client/render/gl/shaders/radial-menu/arcs.vert.glsl +++ /dev/null @@ -1,24 +0,0 @@ -#version 300 es -precision highp float; - -layout(location = 0) in vec2 aPos; // [0,1] quad - -uniform vec2 uAnchor; // anchor in device pixels -uniform float uOuterR; // outer radius in device pixels -uniform vec2 uViewport; // drawingBuffer width, height - -out vec2 vLocal; // [-1, +1] square pixel-space - -void main() { - vLocal = aPos * 2.0 - 1.0; - - // Expand quad to [-outerR, +outerR] in device pixels around anchor - vec2 pos = uAnchor + vLocal * uOuterR; - - // Device pixels → NDC - gl_Position = vec4( - pos.x / uViewport.x * 2.0 - 1.0, - 1.0 - pos.y / uViewport.y * 2.0, - 0.0, 1.0 - ); -} diff --git a/src/client/render/gl/shaders/radial-menu/icon.frag.glsl b/src/client/render/gl/shaders/radial-menu/icon.frag.glsl deleted file mode 100644 index 79862e014..000000000 --- a/src/client/render/gl/shaders/radial-menu/icon.frag.glsl +++ /dev/null @@ -1,27 +0,0 @@ -#version 300 es -precision highp float; - -in vec2 vUV; -flat in float vAtlasIdx; -flat in float vOpacity; - -uniform sampler2D uEmojiAtlas; -uniform float uEmojiCell; -uniform float uEmojiCols; -uniform float uEmojiAtlasW; -uniform float uEmojiAtlasH; - -out vec4 fragColor; - -void main() { - if (vAtlasIdx < 0.0) discard; - - float col = mod(vAtlasIdx, uEmojiCols); - float row = floor(vAtlasIdx / uEmojiCols); - - vec2 cellOrigin = vec2(col * uEmojiCell / uEmojiAtlasW, row * uEmojiCell / uEmojiAtlasH); - vec2 cellSize = vec2(uEmojiCell / uEmojiAtlasW, uEmojiCell / uEmojiAtlasH); - - vec4 texel = texture(uEmojiAtlas, cellOrigin + vUV * cellSize); - fragColor = vec4(texel.rgb, texel.a * vOpacity); -} diff --git a/src/client/render/gl/shaders/radial-menu/icon.vert.glsl b/src/client/render/gl/shaders/radial-menu/icon.vert.glsl deleted file mode 100644 index ee6fc4899..000000000 --- a/src/client/render/gl/shaders/radial-menu/icon.vert.glsl +++ /dev/null @@ -1,74 +0,0 @@ -#version 300 es -precision highp float; - -layout(location = 0) in vec2 aPos; // [0,1] quad - -uniform vec2 uAnchor; // anchor in device pixels -uniform float uOuterR; // outer radius in device pixels -uniform float uInnerR; // inner radius as fraction of outerR [0,1] -uniform vec2 uViewport; // drawingBuffer width, height -uniform int uSegCount; // number of segments -uniform float uIconHalf; // icon half-size in device pixels -uniform float uEmojiIndices[8]; // atlas index per segment (-1 = none) -uniform float uCenterEmojiIdx; // atlas index for center icon (-1 = none) -uniform float uSegOpacity[8]; // per-segment opacity (0..1) - -out vec2 vUV; -flat out float vAtlasIdx; -flat out float vOpacity; - -const float PI = 3.14159265359; -const float TWO_PI = 6.28318530718; - -void main() { - int segIdx = gl_InstanceID; - - // Center icon: last instance (index == uSegCount) - if (segIdx == uSegCount) { - vAtlasIdx = uCenterEmojiIdx; - vOpacity = 1.0; // center icon always full opacity - if (vAtlasIdx < 0.0) { - gl_Position = vec4(2.0, 2.0, 0.0, 1.0); - vUV = vec2(0.0); - return; - } - // Position at anchor center — always upright - vec2 local = aPos * 2.0 - 1.0; - vec2 pos = uAnchor + local * uIconHalf; - gl_Position = vec4( - pos.x / uViewport.x * 2.0 - 1.0, - 1.0 - pos.y / uViewport.y * 2.0, - 0.0, 1.0 - ); - vUV = aPos; - return; - } - - vAtlasIdx = uEmojiIndices[segIdx]; - vOpacity = uSegOpacity[segIdx]; - - if (vAtlasIdx < 0.0 || segIdx >= uSegCount) { - gl_Position = vec4(2.0, 2.0, 0.0, 1.0); - vUV = vec2(0.0); - return; - } - - // Arc center position — rotated so first segment is centered at top - float segArc = TWO_PI / float(uSegCount); - float offset = PI / float(uSegCount); - float angle = (float(segIdx) + 0.5) * segArc - offset; - float midR = (uInnerR + 1.0) * 0.5 * uOuterR; - vec2 center = uAnchor + vec2(sin(angle), -cos(angle)) * midR; - - // Quad corners — always axis-aligned (upright icons) - vec2 local = aPos * 2.0 - 1.0; - vec2 pos = center + local * uIconHalf; - - gl_Position = vec4( - pos.x / uViewport.x * 2.0 - 1.0, - 1.0 - pos.y / uViewport.y * 2.0, - 0.0, 1.0 - ); - - vUV = aPos; -} diff --git a/src/client/render/gl/utils/HeatManager.ts b/src/client/render/gl/utils/HeatManager.ts index 119175769..ce4a335e7 100644 --- a/src/client/render/gl/utils/HeatManager.ts +++ b/src/client/render/gl/utils/HeatManager.ts @@ -15,7 +15,7 @@ import { createTexture2D, shaderSrc, } from "./GlUtils"; -import { FALLOUT_BIT, TILE_DEFINES } from "./TileCodec"; +import { TILE_DEFINES } from "./TileCodec"; import heatDecayFragSrc from "../shaders/fallout-bloom/heat-decay.frag.glsl?raw"; import fullscreenNoUvVertSrc from "../shaders/shared/fullscreen-no-uv.vert.glsl?raw"; @@ -39,12 +39,11 @@ export class HeatManager { private prevTileTex: WebGLTexture; private prevTileFbo: WebGLFramebuffer; private tileTexReadFbo: WebGLFramebuffer; - /** True on first frame and after seek — blit tileTex→prevTileTex without transitions. */ + /** True on first frame — blit tileTex→prevTileTex without transitions. */ private needsPrevTileCopy = true; // Pending CPU → GPU writes private pendingDecay = 0; - private pendingFullHeat: Uint8Array | null = null; /** * True when heat may be non-zero anywhere — gates the decay pass. * Set true on each game tick (shader may detect new fallout transitions). @@ -157,25 +156,7 @@ export class HeatManager { const mw = this.mapW; const mh = this.mapH; - // 1. Upload reconstructed heat on seek - if (this.pendingFullHeat) { - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this.heatReadTex); - gl.texSubImage2D( - gl.TEXTURE_2D, - 0, - 0, - 0, - mw, - mh, - gl.RED, - gl.UNSIGNED_BYTE, - this.pendingFullHeat, - ); - this.pendingFullHeat = null; - } - - // 2. First frame / seek: copy tileTex → prevTileTex, skip transitions + // 1. First frame: copy tileTex → prevTileTex, skip transitions if (this.needsPrevTileCopy) { this.blitTileToPrev(); this.needsPrevTileCopy = false; @@ -183,7 +164,7 @@ export class HeatManager { return; } - // 3. Skip decay pass when nothing to do — no pending decay and heat already settled. + // 2. Skip decay pass when nothing to do — no pending decay and heat already settled. // Still blit tileTex→prevTileTex when a tick fired (pendingDecay > 0) so transition // detection stays accurate if heat activates later. if (!this.heatActive && this.pendingDecay === 0) return; @@ -195,7 +176,7 @@ export class HeatManager { return; } - // 4. Combined transition detection + decay (GPU ping-pong) + // 3. Combined transition detection + decay (GPU ping-pong) gl.bindFramebuffer(gl.FRAMEBUFFER, this.heatWriteFbo); gl.viewport(0, 0, mw, mh); gl.disable(gl.BLEND); @@ -242,30 +223,6 @@ export class HeatManager { ); } - /** - * Reset heat state on seek. Reconstructs heat from nuke history and - * masks out recaptured tiles. - */ - resetForSeek( - tileState: Uint16Array, - nukeEvents?: Array<{ tick: number; tiles: number[] }>, - currentTick?: number, - ): void { - let hasHeat = false; - if (nukeEvents && nukeEvents.length > 0 && currentTick !== undefined) { - const heat = this.reconstructHeat(nukeEvents, currentTick); - this.maskHeat(heat, tileState); - this.pendingFullHeat = heat; - hasHeat = heat.some((v) => v > 0); - } else { - this.pendingFullHeat = new Uint8Array(this.mapW * this.mapH); - } - this.pendingDecay = 0; - this.decayAccumulated = 0; - this.heatActive = hasHeat; - this.needsPrevTileCopy = true; - } - /** Accumulate heat decay for one game tick. */ decayHeat(): void { this.pendingDecay += this.settings.falloutBloom.heatDecayPerTick; @@ -280,32 +237,6 @@ export class HeatManager { // Internals // --------------------------------------------------------------------------- - private reconstructHeat( - nukeEvents: Array<{ tick: number; tiles: number[] }>, - currentTick: number, - ): Uint8Array { - const heat = new Uint8Array(this.mapW * this.mapH); - const decay = this.settings.falloutBloom.heatDecayPerTick; - for (const evt of nukeEvents) { - if (evt.tick > currentTick) continue; - const elapsed = currentTick - evt.tick; - const h = Math.round(255 - elapsed * decay); - if (h <= 0) continue; - for (const ref of evt.tiles) { - if (heat[ref] < h) heat[ref] = h; - } - } - return heat; - } - - private maskHeat(heat: Uint8Array, tileState: Uint16Array): void { - for (let i = 0; i < heat.length; i++) { - if (heat[i] > 0 && (tileState[i] & FALLOUT_BIT) === 0) { - heat[i] = 0; - } - } - } - dispose(): void { const gl = this.gl; gl.deleteProgram(this.decayProg); diff --git a/src/client/render/types/FrameData.ts b/src/client/render/types/FrameData.ts index 04890f591..bb931e0af 100644 --- a/src/client/render/types/FrameData.ts +++ b/src/client/render/types/FrameData.ts @@ -10,17 +10,17 @@ import type { } from "./Renderer"; /** - * FrameData — the boundary contract between game integration and features. + * FrameData — the boundary contract between game integration and the + * renderer. Built once per tick by GameView; the renderer reads from this + * interface and never touches game internals directly. * - * Produced once per frame by a driver (shim for live, codec for replay). - * All feature consumers (renderer, minimap, stats) read from this interface. - * They never touch game internals directly. + * Arrays are long-lived and mutated in place each tick (zero-copy refs). */ export interface FrameData { // ── Core accumulated state ──────────────────────────────────────────── readonly tick: number; - /** True during spawn phase (before gameplay begins). Always false for replay. */ + /** True during spawn phase (before gameplay begins). */ readonly inSpawnPhase: boolean; readonly tileState: Uint16Array; readonly trailState: Uint8Array; @@ -38,10 +38,10 @@ export interface FrameData { /** * Changed tiles this frame for delta uploads. - * - `null` or `undefined` → full upload needed (live mode or keyframe seek) - * - array → delta upload (replay sequential advance) + * - `null` → no delta info; full upload needed (first tick) + * - array → only these tiles changed (empty = skip upload) */ - readonly changedTiles?: TilePair[] | null; + readonly changedTiles: TilePair[] | null; readonly railroadDirty: boolean; readonly revealedRailTiles: number[]; @@ -49,7 +49,6 @@ export interface FrameData { * Trail dirty row range for partial GPU upload. * - `dirtyRowMin > dirtyRowMax` → no trail changes (skip upload) * - Otherwise → upload rows [min, max] from trailState - * Only meaningful in `tileMode: "live"`. */ readonly trailDirtyRowMin: number; readonly trailDirtyRowMax: number; @@ -64,13 +63,4 @@ export interface FrameData { readonly attackRings: AttackRingInput[]; /** True when structures changed this tick (added/removed/level change). */ readonly structuresDirty: boolean; - - // ── Upload semantics ────────────────────────────────────────────────── - - /** - * How tile data should reach the GPU: - * - `"live"` — arrays are mutated in-place by shim each tick (zero-copy refs) - * - `"copy"` — arrays may be swapped/reconstructed (renderer must copy) - */ - readonly tileMode: "live" | "copy"; } diff --git a/src/client/render/types/FrameEvents.ts b/src/client/render/types/FrameEvents.ts index 9ae6abb66..832dc63ba 100644 --- a/src/client/render/types/FrameEvents.ts +++ b/src/client/render/types/FrameEvents.ts @@ -1,38 +1,7 @@ -import type { - ConquestFx, - DeadUnitFx, - PlayerState, - UnitState, -} from "./Renderer"; +import type { ConquestFx, DeadUnitFx } from "./Renderer"; // ── Supporting event types ────────────────────────────────────────────── -export interface AllianceFormedEvent { - requestorID: number; - recipientID: number; -} - -export interface AllianceBrokenEvent { - traitorID: number; - betrayedID: number; -} - -export interface AllianceExpiredEvent { - player1ID: number; - player2ID: number; -} - -export interface EmbargoEvent { - type: "start" | "stop"; - playerID: number; - embargoedID: number; -} - -export interface TargetEvent { - playerID: number; - targetID: number; -} - export interface BonusEvent { playerID: string; smallID: number; @@ -41,48 +10,6 @@ export interface BonusEvent { troops: number; } -export interface NukeIncomingEvent { - playerID: number; -} - -export interface EmojiEvent { - senderID: number; - message: string; -} - -export interface DisplayMessageEvent { - messageType: number; - playerID: number | null; - goldAmount?: number; - params?: Record; -} - -export interface WinEvent { - /** Tuple: ["player", ...playerIds] or ["team"|"nation", name, ...playerIds] */ - winner: string[]; -} - -// ── Empty events constant ─────────────────────────────────────────────── - -/** Shared empty-events object. Safe to reuse — all arrays are empty and never mutated. */ -export const EMPTY_FRAME_EVENTS: FrameEvents = { - deadUnits: [], - conquestEvents: [], - unitUpdates: [], - playerUpdates: [], - allianceFormed: [], - allianceBroken: [], - allianceExpired: [], - embargoEvents: [], - targetEvents: [], - bonusEvents: [], - nukeIncoming: [], - emojis: [], - displayMessages: [], - wins: [], - gamePaused: null, -}; - // ── FrameEvents ───────────────────────────────────────────────────────── /** @@ -93,22 +20,7 @@ export const EMPTY_FRAME_EVENTS: FrameEvents = { * field (no undefined — consumers shouldn't need null checks). */ export interface FrameEvents { - // Rendering events readonly deadUnits: DeadUnitFx[]; readonly conquestEvents: ConquestFx[]; - - // Stats events - readonly unitUpdates: UnitState[]; - readonly playerUpdates: PlayerState[]; - readonly allianceFormed: AllianceFormedEvent[]; - readonly allianceBroken: AllianceBrokenEvent[]; - readonly allianceExpired: AllianceExpiredEvent[]; - readonly embargoEvents: EmbargoEvent[]; - readonly targetEvents: TargetEvent[]; readonly bonusEvents: BonusEvent[]; - readonly nukeIncoming: NukeIncomingEvent[]; - readonly emojis: EmojiEvent[]; - readonly displayMessages: DisplayMessageEvent[]; - readonly wins: WinEvent[]; - readonly gamePaused: boolean | null; } diff --git a/src/client/render/types/FrameSource.ts b/src/client/render/types/FrameSource.ts deleted file mode 100644 index b2a2a4e50..000000000 --- a/src/client/render/types/FrameSource.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { FrameData } from "./FrameData"; -import type { PlayerStatic } from "./Renderer"; - -/** - * Static per-session metadata. Set once at game-start, never changes. - */ -export interface GameStartConfig { - gameID: string; - mapWidth: number; - mapHeight: number; - /** 0 for spectator/replay. */ - localPlayerSmallID: number; - players: PlayerStatic[]; - gameMode?: string; - difficulty?: string; - numLandTiles?: number; -} - -/** - * Mode-agnostic frame source. Features subscribe here and don't care - * whether data comes from a live game or a replay file. - * - * All subscription methods return an unsubscribe function. - * - * Late-join: `onGameStart` fires immediately with cached config if - * subscribed after game-start. `onFrame` does NOT late-fire — subscriber - * waits for the next real tick. - * - * Game-end: `onGameEnd` fires on win detection. `onFrame` continues - * emitting — the simulation runs past game-end. - */ -export interface FrameSource { - onFrame(handler: (frame: FrameData) => void): () => void; - onGameStart(handler: (config: GameStartConfig) => void): () => void; - onGameEnd(handler: () => void): () => void; - /** null before game-start. Stays valid after game-end (same session). */ - readonly config: GameStartConfig | null; -} diff --git a/src/client/render/types/Game.ts b/src/client/render/types/Game.ts deleted file mode 100644 index 5a6809240..000000000 --- a/src/client/render/types/Game.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * The frame data type that both the live game and encoder consume. - * This matches the GameUpdateViewData from the live game's update loop. - */ -export interface GameUpdateViewData { - tick: number; - updates: Record; - packedTileUpdates: unknown; - packedMotionPlans?: Uint32Array; - playerNameViewData: Record; -} - -/** - * Minimal GameStartInfo for the encoder's finish() call. - * The actual object is opaque JSON — we just need it to be serializable. - */ -export type GameStartInfo = Record; diff --git a/src/client/render/types/GameUpdates.ts b/src/client/render/types/GameUpdates.ts deleted file mode 100644 index 438378c6b..000000000 --- a/src/client/render/types/GameUpdates.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Game update type constants and typed event payloads. - * - * Shared contract between shim (live game) and codec (replay). - * Values must match the LIVE deployed game's GameUpdates.ts. - */ - -// --------------------------------------------------------------------------- -// GameUpdateType constants -// --------------------------------------------------------------------------- - -export const GameUpdateType = { - Tile: 0, - Unit: 1, - Player: 2, - DisplayEvent: 3, - DisplayChatEvent: 4, - AllianceRequest: 5, - AllianceRequestReply: 6, - BrokeAlliance: 7, - AllianceExpired: 8, - AllianceExtension: 9, - TargetPlayer: 10, - Emoji: 11, - Win: 12, - Hash: 13, - UnitIncoming: 14, - BonusEvent: 15, - RailroadDestructionEvent: 16, - RailroadConstructionEvent: 17, - RailroadSnapEvent: 18, - ConquestEvent: 19, - EmbargoEvent: 20, - GamePaused: 21, - NukeDetonation: 22, -} as const; - -// --------------------------------------------------------------------------- -// Typed update payloads (keyed by GameUpdateType values) -// --------------------------------------------------------------------------- - -export type PlayerType = "HUMAN" | "NATION" | "BOT"; - -export interface UnitEventUpdate { - id: number; - unitType: string; - ownerID: number; - pos: number; - lastPos?: number; - isActive: boolean; - level: number; - underConstruction?: boolean; - markedForDeletion: number | false; - lastOwnerID?: number; - trainType?: string; - loaded?: boolean; - targetUnitId?: number; - targetTile?: number; - health?: number; - troops?: number; - reachedTarget?: boolean; - retreating?: boolean; - targetable?: boolean; - hasTrainStation?: boolean; - missileTimerQueue?: number[]; -} - -export interface PlayerEventUpdate { - id: string; - clientID?: string | null; - smallID: number; - displayName: string; - playerType: PlayerType; - team?: string | null; - isAlive: boolean; - troops: number; - gold: bigint; - tilesOwned: number; - outgoingAttacks?: AttackEventUpdate[]; - incomingAttacks?: AttackEventUpdate[]; - allies?: number[]; - betrayals?: number; -} - -export interface AttackEventUpdate { - troops: number; -} - -export interface WinUpdate { - /** Winner tuple: ["player", ...playerIds] or ["team"|"nation", name, ...playerIds] */ - winner?: [string, ...string[]]; -} - -export interface AllianceReplyUpdate { - accepted: boolean; - request?: { requestorID: number; recipientID: number }; -} - -export interface BrokeAllianceUpdate { - traitorID: number; - betrayedID: number; -} - -export interface AllianceExpiredUpdate { - player1ID: number; - player2ID: number; -} - -export interface EmbargoUpdate { - event: "start" | "stop"; - playerID: number; - embargoedID: number; -} - -export interface TargetPlayerUpdate { - playerID: number; - targetID: number; -} - -export interface BonusUpdate { - player: string; - tile?: number; - gold: number; - troops: number; -} - -export interface UnitIncomingUpdate { - playerID: number; -} - -export interface EmojiUpdate { - emoji?: { senderID: number; message: string }; -} - -export interface DisplayMessageUpdate { - messageType: number; - playerID: number | null; - goldAmount?: bigint | number; - params?: Record; -} - -export interface GamePausedUpdate { - paused: boolean; -} - -export interface RailroadConstructionUpdate { - id: number; - tiles: number[]; -} - -export interface RailroadDestructionUpdate { - id: number; -} - -export interface RailroadSnapUpdate { - originalId: number; - newId1: number; - newId2: number; - tiles1: number[]; - tiles2: number[]; -} diff --git a/src/client/render/types/Replay.ts b/src/client/render/types/Replay.ts deleted file mode 100644 index a6018a78c..000000000 --- a/src/client/render/types/Replay.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { - ConquestFx, - DeadUnitFx, - NameEntry, - PlayerState, - PlayerStatic, - RendererConfig, - TilePair, - UnitState, -} from "./Renderer"; - -/** Chunk index entry — one per chunk in the file */ -export interface ChunkIndexEntry { - compressedOffset: number; - compressedSize: number; - decompressedSize: number; - frameCount: number; -} - -/** Subset of header available after streaming preamble (before full file download). */ -export interface StreamableReplayInfo extends RendererConfig { - totalFrames: number; - keyframeInterval: number; - numLandTiles: number; - gameStartInfo: unknown; - chunks: ChunkIndexEntry[]; -} - -/** Parsed v6 file header + dictionaries + chunk index + trailer sections */ -export interface ReplayHeader extends StreamableReplayInfo { - magic: number; - version: number; - gameID: string; - totalFrames: number; - keyframeInterval: number; - numLandTiles: number; - processedAt: number; - processingDurationMs: number; - gameStartInfo: unknown; - players: PlayerStatic[]; - /** Chunk index — per-chunk offsets and sizes */ - chunks: ChunkIndexEntry[]; - /** Nuke detonation events — top-level index for seek-time heat reconstruction */ - nukeEvents: Array<{ tick: number; tiles: number[] }>; - /** Railroad events — top-level index for seek-time railroad reconstruction */ - railroadEvents: Array<{ tick: number; type: number; data: unknown }>; - /** Motion plan events — top-level index for plan-driven unit positions and trails */ - motionPlanEvents: MotionPlanRecord[]; - /** Construction start events — top-level index for seek-time construction progress */ - constructionStarts: Array<{ unitId: number; startTick: number }>; - /** Conquest events — top-level index for seek-time gold popup + sword sprite */ - conquestEvents: Array<{ tick: number; x: number; y: number; gold: number }>; - /** Dead unit events — top-level index for seek-time explosion/death FX */ - deadUnitEvents: Array<{ - tick: number; - unitType: string; - pos: number; - reachedTarget: boolean; - }>; - /** Player elimination events — tick when each player's isAlive transitioned to false */ - eliminationEvents: Array<{ tick: number; smallID: number }>; -} - -/** Raw decoded v4 keyframe data — tile data is a raw Uint16Array blob */ -export interface RawKeyframe { - type: 0; - tick: number; - /** Raw tile blob: Uint16Array[mapWidth x mapHeight]. Direct GPU upload. */ - tileBlob: Uint16Array; - players: Map; - units: Map; - names: Map; - miscUpdates: Record | null; -} - -/** Raw decoded delta frame data */ -export interface RawDelta { - type: 1; - tick: number; - tiles: TilePair[]; - playerDeltas: Map; // new or changed players (full state after applying delta) - playersRemoved: number[]; - unitDeltas: Map; - unitsRemoved: number[]; - nameChanges: Map; - miscUpdates: Record | null; -} - -export type RawFrame = RawKeyframe | RawDelta; - -/** Full accumulated game state at a given tick */ -export interface FrameSnapshot { - tick: number; - players: Map; - units: Map; - names: Map; - /** Tiles changed in this frame only (for incremental rendering). null = full upload needed. */ - changedTiles: TilePair[] | null; - /** Units that died this frame (FX-only data). Empty on keyframes. */ - deadUnits: DeadUnitFx[]; - /** Conquest events active at this tick (from global index). */ - conquestEvents: ConquestFx[]; - /** Per-frame misc updates (alliances, donations, trades, etc.). null = none. */ - miscUpdates: Record | null; -} - -/** - * Inflate function type — platform provides its implementation. - * Node: zlib.inflateSync, Browser: pako.inflate - */ -export type InflateFn = (data: Uint8Array) => Uint8Array; - -/** - * Gzip function type — platform provides its implementation. - * Node: zlib.gzipSync, Browser: pako.gzip - */ -export type GzipFn = (data: Uint8Array) => Uint8Array | Promise; - -// --------------------------------------------------------------------------- -// Motion plan records — stored as a file-level index for plan-driven units -// (transport ships, trade ships, trains). -// --------------------------------------------------------------------------- - -export interface GridPlanRecord { - kind: "grid"; - unitId: number; - planId: number; - startTick: number; - ticksPerStep: number; - path: Uint32Array; -} - -export interface TrainPlanRecord { - kind: "train"; - engineUnitId: number; - carUnitIds: Uint32Array; - planId: number; - startTick: number; - speed: number; - spacing: number; - path: Uint32Array; -} - -export type MotionPlanRecord = GridPlanRecord | TrainPlanRecord; diff --git a/src/client/render/types/index.ts b/src/client/render/types/index.ts index 5e4b1783c..122620e17 100644 --- a/src/client/render/types/index.ts +++ b/src/client/render/types/index.ts @@ -22,66 +22,8 @@ export type { // Frame data — boundary contract between game integration and features export type { FrameData } from "./FrameData"; -// Frame events — per-frame ephemeral events (rendering FX + stats events) -export { EMPTY_FRAME_EVENTS } from "./FrameEvents"; -export type { - AllianceBrokenEvent, - AllianceExpiredEvent, - AllianceFormedEvent, - BonusEvent, - DisplayMessageEvent, - EmbargoEvent, - EmojiEvent, - FrameEvents, - NukeIncomingEvent, - TargetEvent, - WinEvent, -} from "./FrameEvents"; - -// Frame source — mode-agnostic subscription interface -export type { FrameSource, GameStartConfig } from "./FrameSource"; - -// Game update types -export type { GameStartInfo, GameUpdateViewData } from "./Game"; - -// Replay types (header, frames, codec helpers) -export type { - ChunkIndexEntry, - FrameSnapshot, - GridPlanRecord, - GzipFn, - InflateFn, - MotionPlanRecord, - RawDelta, - RawFrame, - RawKeyframe, - ReplayHeader, - StreamableReplayInfo, - TrainPlanRecord, -} from "./Replay"; - -// Game update type constants and event payloads (shared between shim + codec) -export { GameUpdateType } from "./GameUpdates"; -export type { - AllianceExpiredUpdate, - AllianceReplyUpdate, - AttackEventUpdate, - BonusUpdate, - BrokeAllianceUpdate, - DisplayMessageUpdate, - EmbargoUpdate, - EmojiUpdate, - GamePausedUpdate, - PlayerEventUpdate, - PlayerType, - RailroadConstructionUpdate, - RailroadDestructionUpdate, - RailroadSnapUpdate, - TargetPlayerUpdate, - UnitEventUpdate, - UnitIncomingUpdate, - WinUpdate, -} from "./GameUpdates"; +// Frame events — per-frame ephemeral events (rendering FX) +export type { BonusEvent, FrameEvents } from "./FrameEvents"; // Unit type string constants and derived sets export { diff --git a/src/client/theme/ThemeProvider.ts b/src/client/theme/ThemeProvider.ts index 52b2abb09..20a4f5b70 100644 --- a/src/client/theme/ThemeProvider.ts +++ b/src/client/theme/ThemeProvider.ts @@ -1,12 +1,12 @@ import { Colord, colord, LabaColor } from "colord"; import { PlayerType, Team } from "../../core/game/Game"; -import { PlayerView } from "../../core/game/GameView"; import { UserSettings } from "../../core/game/UserSettings"; import { simpleHash } from "../../core/Util"; import { createThemeSettings, ThemeSettings, } from "../render/gl/RenderSettings"; +import { PlayerView } from "../view"; import { ColorAllocator } from "./ColorAllocator"; /** diff --git a/src/client/view/GameView.ts b/src/client/view/GameView.ts index 67ed744e1..6dd2d9fcc 100644 --- a/src/client/view/GameView.ts +++ b/src/client/view/GameView.ts @@ -156,8 +156,7 @@ export class GameView implements GameMap { // buffers (tileState, trailState, etc.); some (_changedTilesScratch, // derived arrays) are reused each tick. Properties marked `readonly` on // FrameData only prevent reassignment, not mutation through the reference. - // events: fresh arrays we own; cleared and repopulated each tick. (Don't - // spread EMPTY_FRAME_EVENTS — that would share the module-level arrays.) + // events: fresh arrays we own; cleared and repopulated each tick. this._frame = { tick: 0, inSpawnPhase: true, @@ -170,19 +169,7 @@ export class GameView implements GameMap { events: { deadUnits: [], conquestEvents: [], - unitUpdates: [], - playerUpdates: [], - allianceFormed: [], - allianceBroken: [], - allianceExpired: [], - embargoEvents: [], - targetEvents: [], bonusEvents: [], - nukeIncoming: [], - emojis: [], - displayMessages: [], - wins: [], - gamePaused: null, }, changedTiles: this._changedTilesScratch, railroadDirty: false, @@ -198,7 +185,6 @@ export class GameView implements GameMap { nukeTelegraphs: [], attackRings: [], structuresDirty: false, - tileMode: "live", }; } diff --git a/src/client/view/index.ts b/src/client/view/index.ts new file mode 100644 index 000000000..2c44c49c6 --- /dev/null +++ b/src/client/view/index.ts @@ -0,0 +1,3 @@ +export { GameView } from "./GameView"; +export { PlayerView } from "./PlayerView"; +export { UnitView } from "./UnitView"; diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 920f632de..4d147bce8 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { PlayerView } from "../../client/view"; import { AssetManifest } from "../AssetUrls"; import { Difficulty, @@ -16,7 +17,6 @@ import { UnitType, } from "../game/Game"; import { TileRef } from "../game/GameMap"; -import { PlayerView } from "../game/GameView"; import { UserSettings } from "../game/UserSettings"; import { GameConfig, TeamCountConfig } from "../Schemas"; import { NukeType } from "../StatsSchemas"; diff --git a/src/core/execution/Util.ts b/src/core/execution/Util.ts index 1b51a41fe..d3e062a6a 100644 --- a/src/core/execution/Util.ts +++ b/src/core/execution/Util.ts @@ -1,7 +1,7 @@ +import { GameView } from "../../client/view"; import { NukeMagnitude } from "../configuration/Config"; import { Game, Player, Structures } from "../game/Game"; import { euclDistFN, GameMap, TileRef } from "../game/GameMap"; -import { GameView } from "../game/GameView"; export interface NukeBlastParams { gm: GameMap; diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 84e893671..397d454ab 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -1,4 +1,5 @@ import { renderNumber } from "../../client/Utils"; +import { UnitView } from "../../client/view"; import { Config } from "../configuration/Config"; import { SharedWaterCache } from "../execution/nation/SharedWaterCache"; import { AbstractGraph } from "../pathfinding/algorithms/AbstractGraph"; @@ -40,7 +41,6 @@ import { } from "./Game"; import { GameMap, TileRef } from "./GameMap"; import { GameUpdate, GameUpdateType } from "./GameUpdates"; -import { UnitView } from "./GameView"; import { MotionPlanRecord, packMotionPlans } from "./MotionPlans"; import { PlayerImpl } from "./PlayerImpl"; import { RailNetwork } from "./RailNetwork"; diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts deleted file mode 100644 index da408cfac..000000000 --- a/src/core/game/GameView.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Back-compat re-export shim. -// The view classes physically live in src/client/view/ — this re-export keeps -// the older `import { GameView } from "src/core/game/GameView"` path working. -// -// TODO: remove this shim once all 50+ importers have been updated to point at -// src/client/view/ directly, and the 6 core files that reference PlayerView / -// UnitView / GameView as union types (Player | PlayerView etc.) are refactored -// to use Player / Unit / Game interfaces instead. - -export { GameView } from "../../client/view/GameView"; -export { PlayerView } from "../../client/view/PlayerView"; -export { UnitView } from "../../client/view/UnitView"; diff --git a/src/core/game/UnitGrid.ts b/src/core/game/UnitGrid.ts index 835c55819..7a3a42c10 100644 --- a/src/core/game/UnitGrid.ts +++ b/src/core/game/UnitGrid.ts @@ -1,6 +1,6 @@ +import { UnitView } from "../../client/view"; import { PlayerID, Unit, UnitType } from "./Game"; import { GameMap, TileRef } from "./GameMap"; -import { UnitView } from "./GameView"; export type UnitPredicate = (value: { unit: Unit | UnitView; diff --git a/tests/InputHandler.test.ts b/tests/InputHandler.test.ts index b89e2f46d..5e84043c3 100644 --- a/tests/InputHandler.test.ts +++ b/tests/InputHandler.test.ts @@ -7,9 +7,9 @@ import { WarshipSelectionBoxUpdateEvent, } from "../src/client/InputHandler"; import { UIState } from "../src/client/UIState"; +import { GameView, PlayerView } from "../src/client/view"; import { EventBus } from "../src/core/EventBus"; import { UnitType } from "../src/core/game/Game"; -import { GameView, PlayerView } from "../src/core/game/GameView"; import { KEYBINDS_KEY, UserSettings } from "../src/core/game/UserSettings"; class MockPointerEvent { diff --git a/tests/client/graphics/RadialMenuElements.test.ts b/tests/client/graphics/RadialMenuElements.test.ts index ff27eec69..649c6fc50 100644 --- a/tests/client/graphics/RadialMenuElements.test.ts +++ b/tests/client/graphics/RadialMenuElements.test.ts @@ -7,9 +7,9 @@ import { rootMenuElement, Slot, } from "../../../src/client/hud/layers/RadialMenuElements"; +import { GameView, PlayerView } from "../../../src/client/view"; import { UnitType } from "../../../src/core/game/Game"; import { TileRef } from "../../../src/core/game/GameMap"; -import { GameView, PlayerView } from "../../../src/core/game/GameView"; vi.mock("../../../src/client/Utils", () => ({ translateText: vi.fn((key: string) => key), diff --git a/tests/client/graphics/layers/PlayerPanelKick.test.ts b/tests/client/graphics/layers/PlayerPanelKick.test.ts index 9c491f0ed..ca19c9b89 100644 --- a/tests/client/graphics/layers/PlayerPanelKick.test.ts +++ b/tests/client/graphics/layers/PlayerPanelKick.test.ts @@ -30,8 +30,8 @@ import { actionButton } from "../../../../src/client/components/ui/ActionButton" import { PlayerModerationModal } from "../../../../src/client/hud/layers/PlayerModerationModal"; import { PlayerPanel } from "../../../../src/client/hud/layers/PlayerPanel"; import { SendKickPlayerIntentEvent } from "../../../../src/client/Transport"; +import { PlayerView } from "../../../../src/client/view"; import { PlayerType } from "../../../../src/core/game/Game"; -import { PlayerView } from "../../../../src/core/game/GameView"; describe("PlayerPanel - kick player moderation", () => { let panel: PlayerPanel; diff --git a/tests/client/view/GameView.test.ts b/tests/client/view/GameView.test.ts index 39347d4d8..6ed1c1f72 100644 --- a/tests/client/view/GameView.test.ts +++ b/tests/client/view/GameView.test.ts @@ -454,11 +454,6 @@ describe("GameView.frameData() — renderer contract", () => { expect(game.frameData().events.deadUnits).toBe(a1); }); - it("frame.tileMode is 'live'", () => { - const game = makeGameView(); - expect(game.frameData().tileMode).toBe("live"); - }); - it("frame.structuresDirty is true on first populate (force initial upload)", () => { const game = makeGameView(); game.update(makeEmptyGu(1)); diff --git a/tests/radialMenuElements.test.ts b/tests/radialMenuElements.test.ts index bfa823a1a..dc3ee59ba 100644 --- a/tests/radialMenuElements.test.ts +++ b/tests/radialMenuElements.test.ts @@ -31,7 +31,7 @@ const makePlayer = ( : true, isTraitor: () => opts?.isTraitor ?? false, isDisconnected: () => opts?.isDisconnected ?? false, - }) as unknown as import("../src/core/game/GameView").PlayerView; + }) as unknown as import("../src/client/view").PlayerView; const makeParams = (opts?: Partial): MenuElementParams => { const myPlayer = (opts?.myPlayer as any) ?? makePlayer("p1");