diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 934e5bb4d..db807385f 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -445,13 +445,6 @@ async function createClientGame( (e) => applyDayNightMode((e as CustomEvent).detail === "true"), ); - // The WebGL canvas has pointer-events: none so input flows through the - // overlay div. Forward pointermove to the WebGL view's MapInteraction so - // hover-driven features (highlight owner, etc.) still work. - inputOverlay.addEventListener("pointermove", (e) => - view.handlePointerMove(e), - ); - const gameRenderer = createRenderer( inputOverlay, gameView, diff --git a/src/client/controllers/HoverHighlightController.ts b/src/client/controllers/HoverHighlightController.ts new file mode 100644 index 000000000..9a0e35aa2 --- /dev/null +++ b/src/client/controllers/HoverHighlightController.ts @@ -0,0 +1,44 @@ +/** + * HoverHighlightController — pushes the cursor's tile-owner to the WebGL + * view so the territory + border passes can highlight the hovered player. + * + * Replaces the hover path inside the renderer's MapInteraction class (which + * was bound to the WebGL canvas; that canvas has pointer-events: none in the + * current input architecture so its listeners never fired). All input flows + * through InputHandler → MouseMoveEvent on the EventBus, so we just listen. + */ + +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 { OWNER_MASK } from "../render/gl/utils/tile-codec"; +import { TransformHandler } from "../TransformHandler"; + +export class HoverHighlightController implements Controller { + private lastOwnerID = 0; + + constructor( + private game: GameView, + private eventBus: EventBus, + private transformHandler: TransformHandler, + private view: WebGLGameView, + ) {} + + init() { + this.eventBus.on(MouseMoveEvent, (e) => this.onMouseMove(e)); + } + + private onMouseMove(e: MouseMoveEvent): void { + const cell = this.transformHandler.screenToWorldCoordinates(e.x, e.y); + let ownerID = 0; + if (this.game.isValidCoord(cell.x, cell.y)) { + const ref = this.game.ref(cell.x, cell.y); + ownerID = this.game.tileState(ref) & OWNER_MASK; + } + if (ownerID === this.lastOwnerID) return; + this.lastOwnerID = ownerID; + this.view.setHighlightOwner(ownerID); + } +} diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index c8c854a67..1beae5d95 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -6,6 +6,7 @@ import { GameStartingModal } from "../GameStartingModal"; import { TransformHandler } from "../TransformHandler"; import { UIState } from "../UIState"; import { BuildPreviewController } from "../controllers/BuildPreviewController"; +import { HoverHighlightController } from "../controllers/HoverHighlightController"; import { WarshipSelectionController } from "../controllers/WarshipSelectionController"; import { GameView as WebGLGameView } from "../render/gl"; import { FrameProfiler } from "./FrameProfiler"; @@ -261,6 +262,7 @@ export function createRenderer( const layers: Controller[] = [ new WarshipSelectionController(game, eventBus, transformHandler, view), new BuildPreviewController(game, eventBus, uiState, transformHandler, view), + new HoverHighlightController(game, eventBus, transformHandler, view), new AttackingTroopsOverlay(game, transformHandler, eventBus, userSettings), eventsDisplay, attacksDisplay, diff --git a/src/client/render/gl/game-view.ts b/src/client/render/gl/game-view.ts index 48d7f0306..21562ce81 100644 --- a/src/client/render/gl/game-view.ts +++ b/src/client/render/gl/game-view.ts @@ -29,8 +29,6 @@ import type { GameViewEventType, RadialMenuItem, } from "./events"; -import type { MapKeyBindings } from "./map-interaction"; -import { MapInteraction } from "./map-interaction"; import type { SpawnCenter } from "./passes/spawn-overlay-pass"; import type { RenderSettings } from "./render-settings"; import { GPURenderer } from "./renderer"; @@ -38,7 +36,6 @@ import { GPURenderer } from "./renderer"; export class GameView { private renderer: GPURenderer; private resizeObs: ResizeObserver | null = null; - private interaction: MapInteraction; private listeners = new Map void>>(); @@ -49,7 +46,6 @@ export class GameView { paletteData: Float32Array, raf?: typeof requestAnimationFrame, caf?: typeof cancelAnimationFrame, - keyBindings?: MapKeyBindings, ) { this.renderer = new GPURenderer( canvas, @@ -60,44 +56,6 @@ export class GameView { caf, ); - // Create interaction handler and wire DOM events - this.interaction = new MapInteraction({ - renderer: this.renderer, - emit: this.emit.bind(this), - raf: raf ?? requestAnimationFrame.bind(window), - caf: caf ?? cancelAnimationFrame.bind(window), - keyBindings, - }); - - canvas.addEventListener("pointerdown", (e) => - this.interaction.handlePointerDown(e), - ); - canvas.addEventListener("pointermove", (e) => - this.interaction.handlePointerMove(e), - ); - canvas.addEventListener("pointerup", (e) => - this.interaction.handlePointerUp(e), - ); - canvas.addEventListener("pointercancel", (e) => - this.interaction.handlePointerUp(e), - ); - canvas.addEventListener("wheel", (e) => this.interaction.handleWheel(e), { - passive: false, - }); - canvas.addEventListener("contextmenu", (e) => - this.interaction.handleContextMenu(e), - ); - canvas.addEventListener("dblclick", (e) => - this.interaction.handleDblClick(e), - ); - canvas.addEventListener("auxclick", (e) => - this.interaction.handleAuxClick(e), - ); - document.addEventListener("keydown", (e) => - this.interaction.handleKeyDown(e), - ); - document.addEventListener("keyup", (e) => this.interaction.handleKeyUp(e)); - this.resizeObs = new ResizeObserver((entries) => { for (const entry of entries) { const { width, height } = entry.contentRect; @@ -110,18 +68,6 @@ export class GameView { if (rect.width > 0) this.renderer.resize(rect.width, rect.height); } - /** - * Forward a pointermove event into the MapInteraction handler. The WebGL - * canvas itself has pointer-events: none (input flows through a separate - * overlay div in the main client), so the listener bound to `canvas` in - * the constructor never actually fires for game-mode input. Callers that - * own the active input element forward pointermove events here so hover - * tracking + setHighlightOwner still work. - */ - handlePointerMove(e: PointerEvent): void { - this.interaction.handlePointerMove(e); - } - // ---- Event system ---- on( @@ -161,25 +107,18 @@ export class GameView { centerItem?: RadialMenuItem, ): void { this.renderer.showRadialMenu(screenX, screenY, items, centerItem); - // Cursor is at anchor — center starts hovered (synced with RadialMenuPass) - this.interaction.setMenuHoveredSeg( - this.renderer.radialMenuHitTest(screenX, screenY), - ); } hideRadialMenu(): void { this.renderer.hideRadialMenu(); - this.interaction.setMenuHoveredSeg(-1); } openRadialSubMenu(subItems: RadialMenuItem[]): void { this.renderer.openRadialSubMenu(subItems); - this.interaction.setMenuHoveredSeg(-1); } goBackRadialMenu(): void { this.renderer.goBackRadialMenu(); - this.interaction.setMenuHoveredSeg(-1); } get radialMenuVisible(): boolean { @@ -325,7 +264,6 @@ export class GameView { /** Update ghost structure preview (build-mode visualization). null = clear. */ updateGhostPreview(data: GhostPreviewData | null): void { - this.interaction.setHasGhostPreview(data !== null); this.renderer.updateGhostPreview(data); } @@ -380,21 +318,8 @@ export class GameView { // ---- Other ---- - setFitZoomOnDoubleClick(v: boolean): void { - this.interaction.fitZoomOnDoubleClick = v; - } - setDefaultGridView(v: boolean): void { - this.interaction.setDefaultGridView(v); - } setLocalPlayerID(id: number): void { this.renderer.setLocalPlayerID(id); - this.interaction.setLocalPlayerID(id); - } - setPanSpeed(speed: number): void { - this.interaction.setPanSpeed(speed); - } - setZoomSpeed(speed: number): void { - this.interaction.setZoomSpeed(speed); } setHighlightOwner(ownerID: number): void { this.renderer.setHighlightOwner(ownerID); @@ -418,7 +343,6 @@ export class GameView { // ---- Lifecycle ---- dispose(): void { - this.interaction.dispose(); this.resizeObs?.disconnect(); this.resizeObs = null; this.listeners.clear(); diff --git a/src/client/render/gl/index.ts b/src/client/render/gl/index.ts index 93e4ee05a..2b0942f03 100644 --- a/src/client/render/gl/index.ts +++ b/src/client/render/gl/index.ts @@ -9,8 +9,6 @@ export type { RadialMenuSelectEvent, } from "./events"; export { GameView } from "./game-view"; -export { REPLAY_KEY_BINDINGS } from "./map-interaction"; -export type { MapKeyBindings } from "./map-interaction"; export type { SpawnCenter } from "./passes/spawn-overlay-pass"; export { createRenderSettings, dumpSettings } from "./render-settings"; export type { RenderSettings } from "./render-settings"; diff --git a/src/client/render/gl/keyboard-pan.ts b/src/client/render/gl/keyboard-pan.ts deleted file mode 100644 index 797e9e0f5..000000000 --- a/src/client/render/gl/keyboard-pan.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * KeyboardPan — WASD camera panning, Q/E smooth zoom, and C fit-zoom. - * - * Tracks held keys and runs a requestAnimationFrame loop while any - * direction or zoom key is pressed. All movement is frame-rate - * independent. Pan speed is zoom-adaptive (faster when zoomed out). - * - * Skips all input when the user is typing in an input/textarea. - */ - -const WASD = new Set(["w", "a", "s", "d"]); -const ZOOM_KEYS = new Set(["q", "e"]); -const DEFAULT_PAN_SPEED = 800; // tiles per second at zoom = 1 -const DEFAULT_ZOOM_SPEED = 2.0; // zoom multiplier per second (e.g. 2× per second held) - -interface KeyboardPanDeps { - panBy(dx: number, dy: number): void; - zoomBy(factor: number): void; - focusOwner(ownerID: number): void; - fitMap(): void; - readonly zoom: number; -} - -export class KeyboardPan { - private deps: KeyboardPanDeps; - private raf: typeof requestAnimationFrame; - private caf: typeof cancelAnimationFrame; - - private held = new Set(); - private animId: number | null = null; - private lastTime = 0; - private localPlayerID = 0; - private panSpeed = DEFAULT_PAN_SPEED; - private zoomSpeed = DEFAULT_ZOOM_SPEED; - - constructor( - deps: KeyboardPanDeps, - raf: typeof requestAnimationFrame = requestAnimationFrame.bind(window), - caf: typeof cancelAnimationFrame = cancelAnimationFrame.bind(window), - ) { - this.deps = deps; - this.raf = raf; - this.caf = caf; - } - - handleKeyDown(e: KeyboardEvent): boolean { - if (isTyping()) return false; - - const key = e.key.toLowerCase(); - - if (key === "c" && !e.repeat) { - if (this.localPlayerID > 0) this.deps.focusOwner(this.localPlayerID); - else this.deps.fitMap(); - return true; - } - - if (WASD.has(key) || ZOOM_KEYS.has(key)) { - this.held.add(key); - if (this.animId === null) this.startLoop(); - return true; - } - - return false; - } - - handleKeyUp(e: KeyboardEvent): boolean { - const key = e.key.toLowerCase(); - if (WASD.has(key) || ZOOM_KEYS.has(key)) { - this.held.delete(key); - if (this.held.size === 0) this.stopLoop(); - return true; - } - return false; - } - - setLocalPlayerID(id: number): void { - this.localPlayerID = id; - } - setPanSpeed(speed: number): void { - this.panSpeed = speed; - } - setZoomSpeed(speed: number): void { - this.zoomSpeed = speed; - } - - dispose(): void { - this.stopLoop(); - this.held.clear(); - } - - // ---- Animation loop ---- - - private startLoop(): void { - this.lastTime = performance.now(); - this.animId = this.raf(this.loop); - } - - private stopLoop(): void { - if (this.animId !== null) { - this.caf(this.animId); - this.animId = null; - } - } - - private loop = (): void => { - const now = performance.now(); - const dt = Math.min((now - this.lastTime) / 1000, 0.1); // cap at 100ms - this.lastTime = now; - - const speed = this.panSpeed / this.deps.zoom; - let dx = 0; - let dy = 0; - if (this.held.has("a")) dx -= speed * dt; - if (this.held.has("d")) dx += speed * dt; - if (this.held.has("w")) dy -= speed * dt; - if (this.held.has("s")) dy += speed * dt; - - if (dx !== 0 || dy !== 0) this.deps.panBy(dx, dy); - - // Q/E smooth zoom: compute multiplicative factor for this frame - let zoomDir = 0; - if (this.held.has("e")) zoomDir += 1; - if (this.held.has("q")) zoomDir -= 1; - if (zoomDir !== 0) { - const factor = this.zoomSpeed ** (zoomDir * dt); - this.deps.zoomBy(factor); - } - - if (this.held.size > 0) this.animId = this.raf(this.loop); - else this.animId = null; - }; -} - -function isTyping(): boolean { - const el = document.activeElement; - if (!el) return false; - const tag = el.tagName; - if (tag === "INPUT" || tag === "TEXTAREA") return true; - if ((el as HTMLElement).isContentEditable) return true; - return false; -} diff --git a/src/client/render/gl/map-interaction.ts b/src/client/render/gl/map-interaction.ts deleted file mode 100644 index 4e353206e..000000000 --- a/src/client/render/gl/map-interaction.ts +++ /dev/null @@ -1,418 +0,0 @@ -/** - * MapInteraction — handles all DOM pointer and keyboard events for GameView. - * - * Owns: - * - Drag state: dragging, lastX/Y, downX/Y - * - Menu hover state: menuHoveredSeg - * - Timing guards: lastMenuDismissAt, lastGhostClickAt - * - Ghost preview flag: hasGhostPreview - * - Alt-view flag: altView (affiliation recoloring, configurable hold key) - * - Grid-view flag: gridView (coordinate grid, configurable toggle key) - * - Hover tracking: lastHoverOwner, lastHoverUnitId, lastHoverStructureId, lastHoverTileX/Y - * - * All handler methods (pointerdown, pointermove, pointerup, keydown, keyup, wheel, contextmenu, auxclick, dblclick) - * are defined here and bound by GameView. - */ - -import type { - GameViewEventMap, - GameViewEventType, - MapPointerEvent, -} from "./events"; -import { KeyboardPan } from "./keyboard-pan"; -import type { GPURenderer } from "./renderer"; - -const HIT_RADIUS_PX = 16; -const CLICK_THRESHOLD_SQ = 100; - -/** Describes a hold-key binding (key held = active, released = inactive). */ -export interface HoldKeyBinding { - /** KeyboardEvent.code to match (e.g. "Space", "KeyM"). */ - code: string; - /** Require shift modifier. Default false. */ - shift?: boolean; -} - -/** Describes a toggle-key binding (each press toggles). */ -export interface ToggleKeyBinding { - /** KeyboardEvent.key to match (e.g. "m", "g"). */ - key: string; -} - -/** Configurable keybindings for MapInteraction. */ -export interface MapKeyBindings { - /** Hold to peek alt-view (affiliation recoloring) + grid. */ - altViewPeek: HoldKeyBinding; - /** Toggle grid overlay on/off. */ - gridToggle: ToggleKeyBinding; -} - -/** Extension default: Space hold for altView peek, 'm' toggle for grid. */ -export const DEFAULT_KEY_BINDINGS: MapKeyBindings = { - altViewPeek: { code: "Space" }, - gridToggle: { key: "m" }, -}; - -/** Replay default: Shift+M hold for altView peek, 'm' toggle for grid. */ -export const REPLAY_KEY_BINDINGS: MapKeyBindings = { - altViewPeek: { code: "KeyG", shift: true }, - gridToggle: { key: "g" }, -}; - -interface InteractionDeps { - renderer: GPURenderer; - emit: ( - event: K, - payload: GameViewEventMap[K], - ) => void; - raf: typeof requestAnimationFrame; - caf: typeof cancelAnimationFrame; - keyBindings?: MapKeyBindings; -} - -export class MapInteraction { - private deps: InteractionDeps; - private keys: MapKeyBindings; - - // Drag state - private dragging = false; - private lastX = 0; - private lastY = 0; - private downX = 0; - private downY = 0; - - // Hover tracking - private lastHoverOwner = 0; - private lastHoverUnitId: number | null = null; - private lastHoverStructureId: number | null = null; - private lastHoverTileX = -1; - private lastHoverTileY = -1; - - // Timing guards - private hasGhostPreview = false; - private lastGhostClickAt = 0; - private lastMenuDismissAt = 0; - - // Menu hover - private menuHoveredSeg = -1; - - // Grid-view: coordinate grid overlay. Toggled by configured key, persisted. - private gridViewBase = false; - private gridView = false; - - // Alt-view: affiliation recoloring (no persistent toggle). - private altView = false; - - // Alt-view peek hold state. - private peekHeld = false; - - // Interaction settings (mutable — updated live by extension) - fitZoomOnDoubleClick = true; - - // Keyboard camera control (WASD pan + C fit-zoom) - private keyboardPan: KeyboardPan; - - constructor(deps: InteractionDeps) { - this.deps = deps; - this.keys = deps.keyBindings ?? DEFAULT_KEY_BINDINGS; - this.keyboardPan = new KeyboardPan(deps.renderer, deps.raf, deps.caf); - } - - // ---- Pointer event handlers ---- - - handlePointerDown(e: PointerEvent): void { - if (e.button !== 0) return; - - // If radial menu is open, clicking outside dismisses it - if (this.deps.renderer.radialMenuVisible) { - const hit = this.deps.renderer.radialMenuHitTest(e.clientX, e.clientY); - if (hit === -1) { - this.deps.renderer.hideRadialMenu(); - this.menuHoveredSeg = -1; - this.lastMenuDismissAt = performance.now(); - } - return; // consume the event either way — don't start dragging - } - - if (this.hasGhostPreview) this.lastGhostClickAt = performance.now(); - this.dragging = true; - this.lastX = e.clientX; - this.lastY = e.clientY; - this.downX = e.clientX; - this.downY = e.clientY; - (e.target as HTMLElement).setPointerCapture(e.pointerId); - } - - handlePointerMove(e: PointerEvent): void { - // Update radial menu hover - if (this.deps.renderer.radialMenuVisible) { - const hit = this.deps.renderer.radialMenuHitTest(e.clientX, e.clientY); - if (hit !== this.menuHoveredSeg) { - this.menuHoveredSeg = hit; - this.deps.renderer.setRadialMenuHover(hit); - } - return; // don't pan or update game hover while menu is open - } - - if (this.dragging) { - const dx = e.clientX - this.lastX; - const dy = e.clientY - this.lastY; - this.lastX = e.clientX; - this.lastY = e.clientY; - const dpr = window.devicePixelRatio || 1; - this.deps.renderer.panBy( - -(dx * dpr) / this.deps.renderer.zoom, - -(dy * dpr) / this.deps.renderer.zoom, - ); - return; - } - this.updateHover(e); - } - - handlePointerUp(e: PointerEvent): void { - if (e.button !== 0) return; - - // If radial menu is open, a click on a segment or center selects it. - // Don't hide the menu here — the menuselect handler decides whether to - // close or open a submenu. - if (this.deps.renderer.radialMenuVisible) { - if (this.menuHoveredSeg !== -1) { - const item = this.deps.renderer.getRadialMenuItemAt( - this.menuHoveredSeg, - ); - if (item && item.enabled) { - this.deps.emit("menuselect", { - index: this.menuHoveredSeg, - id: item.id, - }); - } - if (!this.deps.renderer.radialMenuVisible) { - this.lastMenuDismissAt = performance.now(); - } - this.menuHoveredSeg = -1; - } - return; - } - - if (!this.dragging) return; - this.dragging = false; - (e.target as HTMLElement).releasePointerCapture(e.pointerId); - const dx = e.clientX - this.downX; - const dy = e.clientY - this.downY; - if (dx * dx + dy * dy < CLICK_THRESHOLD_SQ) { - this.deps.emit("click", this.buildEvent(e, 0)); - } - } - - // ---- Keyboard event handlers ---- - - handleKeyDown(e: KeyboardEvent): void { - if (e.key === "Escape" && this.deps.renderer.radialMenuVisible) { - this.deps.renderer.hideRadialMenu(); - this.menuHoveredSeg = -1; - this.lastMenuDismissAt = performance.now(); - } - if ( - this.matchesHold(e, this.keys.altViewPeek) && - !e.repeat && - !this.peekHeld - ) { - e.preventDefault(); - this.peekHeld = true; - this.applyAltView(true); - this.applyGridView(true); - this.deps.emit("altviewpeek", { active: true }); - } - if (e.key === this.keys.gridToggle.key && !e.shiftKey && !e.repeat) { - this.gridViewBase = !this.gridViewBase; - this.applyGridView(this.gridViewBase); - this.deps.emit("gridviewtoggle", { active: this.gridViewBase }); - } - this.keyboardPan.handleKeyDown(e); - } - - handleKeyUp(e: KeyboardEvent): void { - if (e.code === this.keys.altViewPeek.code && this.peekHeld) { - e.preventDefault(); - this.peekHeld = false; - this.applyAltView(false); - this.applyGridView(this.gridViewBase); - this.deps.emit("altviewpeek", { active: false }); - } - this.keyboardPan.handleKeyUp(e); - } - - private matchesHold(e: KeyboardEvent, binding: HoldKeyBinding): boolean { - return e.code === binding.code && (!binding.shift || e.shiftKey); - } - - // ---- Other event handlers ---- - - handleWheel(e: WheelEvent): void { - e.preventDefault(); - if (e.shiftKey || e.ctrlKey || e.altKey) { - this.deps.emit("scroll", { - deltaX: e.deltaX, - deltaY: e.deltaY, - shiftKey: e.shiftKey, - ctrlKey: e.ctrlKey, - altKey: e.altKey, - }); - return; - } - const factor = e.deltaY < 0 ? 1.1 : 1 / 1.1; - this.deps.renderer.zoomAtScreen(factor, e.clientX, e.clientY); - } - - handleContextMenu(e: MouseEvent): void { - e.preventDefault(); - // Dismiss any open menu first — the external manager will decide whether to reopen - if (this.deps.renderer.radialMenuVisible) { - this.deps.renderer.hideRadialMenu(); - this.menuHoveredSeg = -1; - this.lastMenuDismissAt = performance.now(); - } - this.deps.emit("contextmenu", this.buildEvent(e, 2)); - } - - handleAuxClick(e: MouseEvent): void { - if (e.button !== 1) return; - e.preventDefault(); - this.deps.emit("middleclick", this.buildEvent(e, 1)); - } - - handleDblClick(e: MouseEvent): void { - // Suppress fitzoom if menu is open or was recently open - if (this.deps.renderer.radialMenuVisible) return; - const now = performance.now(); - if (now - this.lastMenuDismissAt < 500) return; - - const evt = this.buildEvent(e, 0); - if (this.fitZoomOnDoubleClick && now - this.lastGhostClickAt > 500) { - if (evt.ownerID !== 0) this.deps.renderer.focusOwner(evt.ownerID); - else this.deps.renderer.fitMap(); - } - this.deps.emit("dblclick", evt); - } - - // ---- Hover tracking ---- - - private updateHover(e: PointerEvent): void { - const world = this.deps.renderer.screenToWorld(e.clientX, e.clientY); - const tileX = Math.floor(world.x); - const tileY = Math.floor(world.y); - const ownerID = this.deps.renderer.getOwnerAtWorld(world.x, world.y); - const hitRadius = HIT_RADIUS_PX / this.deps.renderer.zoom; - const unit = this.deps.renderer.getUnitAtWorld(world.x, world.y, hitRadius); - const structure = this.deps.renderer.getStructureAtWorld( - world.x, - world.y, - hitRadius, - ); - const unitId = unit?.id ?? null; - const structureId = structure?.id ?? null; - - if ( - ownerID !== this.lastHoverOwner || - unitId !== this.lastHoverUnitId || - structureId !== this.lastHoverStructureId || - tileX !== this.lastHoverTileX || - tileY !== this.lastHoverTileY - ) { - this.lastHoverOwner = ownerID; - this.lastHoverUnitId = unitId; - this.lastHoverStructureId = structureId; - this.lastHoverTileX = tileX; - this.lastHoverTileY = tileY; - this.deps.renderer.setHighlightOwner(ownerID); - this.deps.emit("hover", { - screenX: e.clientX, - screenY: e.clientY, - worldX: world.x, - worldY: world.y, - tileX, - tileY, - ownerID, - unit, - structure, - button: 0, - shiftKey: e.shiftKey, - ctrlKey: e.ctrlKey || e.metaKey, - altKey: e.altKey, - }); - } - } - - private buildEvent(e: MouseEvent, button: number): MapPointerEvent { - const world = this.deps.renderer.screenToWorld(e.clientX, e.clientY); - const hitRadius = HIT_RADIUS_PX / this.deps.renderer.zoom; - return { - screenX: e.clientX, - screenY: e.clientY, - worldX: world.x, - worldY: world.y, - tileX: Math.floor(world.x), - tileY: Math.floor(world.y), - ownerID: this.deps.renderer.getOwnerAtWorld(world.x, world.y), - unit: this.deps.renderer.getUnitAtWorld(world.x, world.y, hitRadius), - structure: this.deps.renderer.getStructureAtWorld( - world.x, - world.y, - hitRadius, - ), - button, - shiftKey: e.shiftKey, - ctrlKey: e.ctrlKey || e.metaKey, - altKey: e.altKey, - }; - } - - // ---- View helpers ---- - - private applyAltView(active: boolean): void { - if (active === this.altView) return; - this.altView = active; - this.deps.renderer.setAltView(active); - } - - private applyGridView(active: boolean): void { - if (active === this.gridView) return; - this.gridView = active; - this.deps.renderer.setGridView(active); - } - - // ---- Public API ---- - - setDefaultGridView(v: boolean): void { - this.gridViewBase = v; - if (!this.peekHeld) this.applyGridView(v); - } - - setHasGhostPreview(v: boolean): void { - this.hasGhostPreview = v; - } - - getMenuHoveredSeg(): number { - return this.menuHoveredSeg; - } - - setMenuHoveredSeg(v: number): void { - this.menuHoveredSeg = v; - } - - setLocalPlayerID(id: number): void { - this.keyboardPan.setLocalPlayerID(id); - } - - setPanSpeed(speed: number): void { - this.keyboardPan.setPanSpeed(speed); - } - - setZoomSpeed(speed: number): void { - this.keyboardPan.setZoomSpeed(speed); - } - - dispose(): void { - this.keyboardPan.dispose(); - } -}