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;
+};