diff --git a/resources/images/PingIcon.svg b/resources/images/PingIcon.svg new file mode 100644 index 000000000..5e20950a0 --- /dev/null +++ b/resources/images/PingIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 4731cd4a6..1b840aa72 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -1,10 +1,10 @@ import { EventBus, GameEvent } from "../core/EventBus"; import { UnitType } from "../core/game/Game"; import { UnitView } from "../core/game/GameView"; -import { UserSettings } from "../core/game/UserSettings"; import { PingType } from "../core/game/Ping"; -import { UIState } from "./graphics/UIState"; +import { UserSettings } from "../core/game/UserSettings"; import { TransformHandler } from "./graphics/TransformHandler"; +import { UIState } from "./graphics/UIState"; import { ReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier"; export class MouseUpEvent implements GameEvent { @@ -501,26 +501,24 @@ export class InputHandler { this.eventBus.emit(new PingSelectedEvent(null)); return; } - { - const localX = event.clientX - rect.left; - const localY = event.clientY - rect.top; - const worldCoords = this.transformHandler.screenToWorldCoordinates( - localX, - localY, - ); - this.eventBus.emit( - new PingPlacedEvent( - this.uiState.currentPingType, - worldCoords.x, - worldCoords.y, - ), - ); - } + const localX = event.clientX - rect.left; + const localY = event.clientY - rect.top; + const worldCoords = this.transformHandler.screenToWorldCoordinates( + localX, + localY, + ); + this.eventBus.emit( + new PingPlacedEvent( + this.uiState.currentPingType, + worldCoords.x, + worldCoords.y, + ), + ); this.uiState.currentPingType = null; - this.eventBus.emit(new PingSelectedEvent(null)); // Clear ping preview + this.eventBus.emit(new PingSelectedEvent(null)); return; } - + if (event.pointerType === "touch") { this.eventBus.emit(new TouchEvent(event.x, event.y)); event.preventDefault(); diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index f6c82bfec..a793bd59f 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -298,6 +298,10 @@ export function createRenderer( export class GameRenderer { private context: CanvasRenderingContext2D; + private rafId?: number; + private resizeListener?: () => void; + private contextLostListener?: () => void; + private contextRestoredListener?: () => void; constructor( private game: GameView, @@ -316,33 +320,59 @@ export class GameRenderer { private redrawEventCleanup?: () => void; initialize() { - this.redrawEventCleanup = this.eventBus.on(RedrawGraphicsEvent, () => + this.redrawEventCleanup = this.eventBus.on(RedrawGraphicsEvent, () => this.redraw(), ); this.layers.forEach((l) => l.init?.()); document.body.appendChild(this.canvas); - window.addEventListener("resize", () => this.resizeCanvas()); + this.resizeListener = () => this.resizeCanvas(); + window.addEventListener("resize", this.resizeListener); this.resizeCanvas(); //show whole map on startup this.transformHandler.centerAll(0.9); - let rafId = requestAnimationFrame(() => this.renderGame()); - this.canvas.addEventListener("contextlost", () => { - cancelAnimationFrame(rafId); - }); - this.canvas.addEventListener("contextrestored", () => { + this.contextLostListener = () => { + if (this.rafId !== undefined) { + cancelAnimationFrame(this.rafId); + this.rafId = undefined; + } + }; + this.canvas.addEventListener("contextlost", this.contextLostListener); + + this.contextRestoredListener = () => { this.redraw(); - rafId = requestAnimationFrame(() => this.renderGame()); - }); + this.rafId = requestAnimationFrame(() => this.renderGame()); + }; + this.canvas.addEventListener( + "contextrestored", + this.contextRestoredListener, + ); + + this.rafId = requestAnimationFrame(() => this.renderGame()); } - + destroy() { this.redrawEventCleanup?.(); + if (this.rafId !== undefined) { + cancelAnimationFrame(this.rafId); + } + if (this.resizeListener) { + window.removeEventListener("resize", this.resizeListener); + } + if (this.contextLostListener) { + this.canvas.removeEventListener("contextlost", this.contextLostListener); + } + if (this.contextRestoredListener) { + this.canvas.removeEventListener( + "contextrestored", + this.contextRestoredListener, + ); + } this.layers.forEach((l) => l.destroy?.()); } - + resizeCanvas() { this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; diff --git a/src/client/graphics/fx/PingFx.ts b/src/client/graphics/fx/PingFx.ts index b16be2471..cb1bd5e66 100644 --- a/src/client/graphics/fx/PingFx.ts +++ b/src/client/graphics/fx/PingFx.ts @@ -1,9 +1,8 @@ +import { TileRef } from "../../../core/game/GameMap"; import { GameView } from "../../../core/game/GameView"; import { PingType } from "../../../core/game/Ping"; -import { TileRef } from "../../../core/game/GameMap"; import { Fx } from "./Fx"; - export class PingFx implements Fx { private readonly durationMs: number = 3000; // Ping visible for 3 seconds private startTime: number; @@ -12,8 +11,6 @@ export class PingFx implements Fx { return PingFx.iconCache.get(this.pingType) ?? null; } - - constructor( private game: GameView, private pingType: PingType, @@ -57,9 +54,10 @@ export class PingFx implements Fx { return null; } } -private static iconCache = new Map(); + private static iconCache = new Map(); private static preloadIcon(pingType: PingType, iconPath: string): void { if (!PingFx.iconCache.has(pingType)) { + PingFx.iconCache.set(pingType, null); // Reserve spot immediately const img = new Image(); img.onload = () => { PingFx.iconCache.set(pingType, img); @@ -109,4 +107,4 @@ private static iconCache = new Map(); context.restore(); return true; // Fx is still active } -} \ No newline at end of file +} diff --git a/src/client/graphics/layers/FxLayer.ts b/src/client/graphics/layers/FxLayer.ts index d4203205d..56cffef94 100644 --- a/src/client/graphics/layers/FxLayer.ts +++ b/src/client/graphics/layers/FxLayer.ts @@ -353,6 +353,12 @@ export class FxLayer implements Layer { } private pingEventCleanup?: () => void; + dispose() { + if (this.pingEventCleanup) { + this.pingEventCleanup(); + this.pingEventCleanup = undefined; + } + } async init() { this.redraw(); try { diff --git a/src/client/graphics/layers/PingMenu.ts b/src/client/graphics/layers/PingMenu.ts index fd1ecf8e6..66a19e604 100644 --- a/src/client/graphics/layers/PingMenu.ts +++ b/src/client/graphics/layers/PingMenu.ts @@ -1,17 +1,13 @@ -import { - MenuElement, - MenuElementParams, - COLORS, -} from "./RadialMenuElements"; -import swordIcon from "../../../../resources/images/SwordIconWhite.svg"; import retreatIcon from "../../../../resources/images/BackIconWhite.svg"; -import defendIcon from "../../../../resources/images/ShieldIconWhite.svg"; +import pingIcon from "../../../../resources/images/PingIcon.svg"; import watchOutIcon from "../../../../resources/images/QuestionMarkIcon.svg"; +import defendIcon from "../../../../resources/images/ShieldIconWhite.svg"; import { EventBus } from "../../../core/EventBus"; import { PingType } from "../../../core/game/Ping"; import { PingSelectedEvent } from "../../InputHandler"; +import { COLORS, MenuElement, MenuElementParams } from "./RadialMenuElements"; -export const PING_ICON = swordIcon; +export const PING_ICON = pingIcon; export const PING_COLORS = { [PingType.Attack]: "#ff0000", @@ -34,9 +30,10 @@ function createPingElement( color: PING_COLORS[pingType], disabled: () => false, action: (params?: MenuElementParams) => { - if (!params) return; eventBus.emit(new PingSelectedEvent(pingType)); - params.closeMenu(); + if (params) { + params.closeMenu(); + } }, }; } @@ -84,4 +81,4 @@ export function createPingMenu(eventBus: EventBus): MenuElement { pingWatchOutElement, ], }; -} \ No newline at end of file +} diff --git a/src/client/graphics/layers/RadialMenuElements.ts b/src/client/graphics/layers/RadialMenuElements.ts index 2b1be2f24..20ed25084 100644 --- a/src/client/graphics/layers/RadialMenuElements.ts +++ b/src/client/graphics/layers/RadialMenuElements.ts @@ -572,6 +572,8 @@ export const rootMenuElement: MenuElement = { icon: infoIcon, color: COLORS.info, subMenu: (params: MenuElementParams) => { + if (params === undefined) return []; + let ally = allyRequestElement; if (params.selected?.isAlliedWith(params.myPlayer)) { ally = allyBreakElement; @@ -584,7 +586,7 @@ export const rootMenuElement: MenuElement = { const menuItems: (MenuElement | null)[] = [ infoMenuElement, - createPingMenu(params.eventBus), + createPingMenu(params.eventBus), ...(isOwnTerritory ? [deleteUnitElement, ally, buildMenuElement] : [boatMenuElement, ally, attackMenuElement]), diff --git a/src/core/game/Ping.ts b/src/core/game/Ping.ts index 81e3d4147..c0aec19c8 100644 --- a/src/core/game/Ping.ts +++ b/src/core/game/Ping.ts @@ -1,17 +1,8 @@ import { TileRef } from "./GameMap"; -export enum PingType { - Attack, - Retreat, - Defend, - WatchOut, -} +export type PingType = "attack" | "retreat" | "defend" | "watchOut"; -export class Ping { - constructor( - public type: PingType, - public tile: TileRef, - ) {} -} - -export class PingPlacedEvent extends Ping {} +export type Ping = { + type: PingType; + tile: TileRef; +};