mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 13:29:45 +00:00
feat: Add ping feature
This commit introduces a new ping system that allows players to communicate on the map. - Adds a radial ping menu to select different ping types (Attack, Retreat, Defend, Watch Out). - Implements the logic to place pings on the map. - Adds visual effects for pings, including a fading circle and an icon. - Shows a preview of the ping location before placing it.
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Add PATH setup to ensure npx is found
|
||||
export PATH="/usr/local/bin:$HOME/.npm-global/bin:$HOME/.nvm/versions/node/$(node -v)/bin:$PATH"
|
||||
|
||||
Generated
+3
-3
@@ -3822,9 +3822,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,9 @@ 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 { TransformHandler } from "./graphics/TransformHandler";
|
||||
import { ReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier";
|
||||
|
||||
export class MouseUpEvent implements GameEvent {
|
||||
@@ -124,6 +126,9 @@ export class AutoUpgradeEvent implements GameEvent {
|
||||
public readonly y: number,
|
||||
) {}
|
||||
}
|
||||
export class PingSelectedEvent implements GameEvent {
|
||||
constructor(public readonly pingType: PingType | null) {}
|
||||
}
|
||||
|
||||
export class TickMetricsEvent implements GameEvent {
|
||||
constructor(
|
||||
@@ -131,6 +136,13 @@ export class TickMetricsEvent implements GameEvent {
|
||||
public readonly tickDelay?: number,
|
||||
) {}
|
||||
}
|
||||
export class PingPlacedEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly pingType: PingType,
|
||||
public readonly x: number,
|
||||
public readonly y: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class InputHandler {
|
||||
private lastPointerX: number = 0;
|
||||
@@ -160,6 +172,7 @@ export class InputHandler {
|
||||
public uiState: UIState,
|
||||
private canvas: HTMLCanvasElement,
|
||||
private eventBus: EventBus,
|
||||
private transformHandler: TransformHandler,
|
||||
) {}
|
||||
|
||||
initialize() {
|
||||
@@ -481,6 +494,33 @@ export class InputHandler {
|
||||
Math.abs(event.x - this.lastPointerDownX) +
|
||||
Math.abs(event.y - this.lastPointerDownY);
|
||||
if (dist < 10) {
|
||||
if (this.uiState.currentPingType !== null) {
|
||||
const rect = this.transformHandler.boundingRect();
|
||||
if (!rect) {
|
||||
this.uiState.currentPingType = null;
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
this.uiState.currentPingType = null;
|
||||
this.eventBus.emit(new PingSelectedEvent(null)); // Clear ping preview
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.pointerType === "touch") {
|
||||
this.eventBus.emit(new TouchEvent(event.x, event.y));
|
||||
event.preventDefault();
|
||||
|
||||
@@ -25,6 +25,7 @@ import { MultiTabModal } from "./layers/MultiTabModal";
|
||||
import { NameLayer } from "./layers/NameLayer";
|
||||
import { NukeTrajectoryPreviewLayer } from "./layers/NukeTrajectoryPreviewLayer";
|
||||
import { PerformanceOverlay } from "./layers/PerformanceOverlay";
|
||||
import { PingTargetPreviewLayer } from "./layers/PingTargetPreviewLayer";
|
||||
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
|
||||
import { PlayerPanel } from "./layers/PlayerPanel";
|
||||
import { RailroadLayer } from "./layers/RailroadLayer";
|
||||
@@ -210,7 +211,11 @@ export function createRenderer(
|
||||
transformHandler,
|
||||
uiState,
|
||||
);
|
||||
|
||||
const pingTargetPreviewLayer = new PingTargetPreviewLayer(
|
||||
game,
|
||||
eventBus,
|
||||
transformHandler,
|
||||
);
|
||||
const performanceOverlay = document.querySelector(
|
||||
"performance-overlay",
|
||||
) as PerformanceOverlay;
|
||||
@@ -243,9 +248,10 @@ export function createRenderer(
|
||||
structureLayer,
|
||||
samRadiusLayer,
|
||||
new UnitLayer(game, eventBus, transformHandler),
|
||||
new FxLayer(game),
|
||||
new FxLayer(game, eventBus),
|
||||
new UILayer(game, eventBus, transformHandler),
|
||||
new NukeTrajectoryPreviewLayer(game, eventBus, transformHandler),
|
||||
pingTargetPreviewLayer,
|
||||
new StructureIconsLayer(game, eventBus, uiState, transformHandler),
|
||||
new NameLayer(game, transformHandler, eventBus),
|
||||
eventsDisplay,
|
||||
@@ -307,8 +313,12 @@ export class GameRenderer {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private redrawEventCleanup?: () => void;
|
||||
|
||||
initialize() {
|
||||
this.eventBus.on(RedrawGraphicsEvent, () => this.redraw());
|
||||
this.redrawEventCleanup = this.eventBus.on(RedrawGraphicsEvent, () =>
|
||||
this.redraw(),
|
||||
);
|
||||
this.layers.forEach((l) => l.init?.());
|
||||
|
||||
document.body.appendChild(this.canvas);
|
||||
@@ -327,7 +337,12 @@ export class GameRenderer {
|
||||
rafId = requestAnimationFrame(() => this.renderGame());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
destroy() {
|
||||
this.redrawEventCleanup?.();
|
||||
this.layers.forEach((l) => l.destroy?.());
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { UnitType } from "../../core/game/Game";
|
||||
import { PingType } from "../../core/game/Ping";
|
||||
|
||||
export interface UIState {
|
||||
attackRatio: number;
|
||||
ghostStructure: UnitType | null;
|
||||
currentPingType: PingType | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
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;
|
||||
private readonly pingColor: string;
|
||||
private get icon(): HTMLImageElement | null {
|
||||
return PingFx.iconCache.get(this.pingType) ?? null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private pingType: PingType,
|
||||
private tile: TileRef,
|
||||
) {
|
||||
this.startTime = performance.now();
|
||||
this.pingColor = this.getPingColor(pingType);
|
||||
// Trigger preload but don't store the result
|
||||
const iconPath = this.getIconPath(pingType);
|
||||
if (iconPath) {
|
||||
PingFx.preloadIcon(pingType, iconPath);
|
||||
}
|
||||
}
|
||||
|
||||
private getPingColor(pingType: PingType): string {
|
||||
switch (pingType) {
|
||||
case PingType.Attack:
|
||||
return "rgba(255, 0, 0, 0.7)"; // Red
|
||||
case PingType.Retreat:
|
||||
return "rgba(0, 255, 0, 0.7)"; // Green
|
||||
case PingType.Defend:
|
||||
return "rgba(0, 0, 255, 0.7)"; // Blue
|
||||
case PingType.WatchOut:
|
||||
return "rgba(255, 255, 0, 0.7)"; // Yellow
|
||||
default:
|
||||
return "rgba(128, 128, 128, 0.7)"; // Default to gray
|
||||
}
|
||||
}
|
||||
|
||||
private getIconPath(pingType: PingType): string | null {
|
||||
switch (pingType) {
|
||||
case PingType.Attack:
|
||||
return "/resources/images/SwordIconWhite.svg";
|
||||
case PingType.Retreat:
|
||||
return "/resources/images/BackIconWhite.svg";
|
||||
case PingType.Defend:
|
||||
return "/resources/images/ShieldIconWhite.svg";
|
||||
case PingType.WatchOut:
|
||||
return "/resources/images/ExclamationMarkIcon.svg";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private static iconCache = new Map<PingType, HTMLImageElement | null>();
|
||||
private static preloadIcon(pingType: PingType, iconPath: string): void {
|
||||
if (!PingFx.iconCache.has(pingType)) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
PingFx.iconCache.set(pingType, img);
|
||||
};
|
||||
img.onerror = () => {
|
||||
console.error(`Failed to load ping icon: ${iconPath}`);
|
||||
PingFx.iconCache.set(pingType, null); // Mark as failed
|
||||
};
|
||||
img.src = iconPath;
|
||||
}
|
||||
}
|
||||
|
||||
renderTick(duration: number, context: CanvasRenderingContext2D): boolean {
|
||||
const elapsed = performance.now() - this.startTime;
|
||||
if (elapsed > this.durationMs) {
|
||||
return false; // Fx is finished
|
||||
}
|
||||
|
||||
const x = this.game.x(this.tile);
|
||||
const y = this.game.y(this.tile);
|
||||
|
||||
// Calculate offset to center coordinates (same as canvas drawing)
|
||||
const offsetX = -this.game.width() / 2;
|
||||
const offsetY = -this.game.height() / 2;
|
||||
|
||||
context.save();
|
||||
context.globalAlpha = 1 - elapsed / this.durationMs; // Fade out effect
|
||||
|
||||
// Draw colored circle
|
||||
context.fillStyle = this.pingColor;
|
||||
context.beginPath();
|
||||
context.arc(x + offsetX, y + offsetY, 15, 0, 2 * Math.PI);
|
||||
context.fill();
|
||||
|
||||
// Draw icon
|
||||
if (this.icon && this.icon.complete) {
|
||||
const iconSize = 20;
|
||||
context.drawImage(
|
||||
this.icon,
|
||||
x + offsetX - iconSize / 2,
|
||||
y + offsetY - iconSize / 2,
|
||||
iconSize,
|
||||
iconSize,
|
||||
);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
return true; // Fx is still active
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UnitType } from "../../../core/game/Game";
|
||||
import {
|
||||
BonusEventUpdate,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
RailroadUpdate,
|
||||
} from "../../../core/game/GameUpdates";
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { PingPlacedEvent } from "../../../core/game/Ping";
|
||||
import SoundManager, { SoundEffect } from "../../sound/SoundManager";
|
||||
import { renderNumber } from "../../Utils";
|
||||
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
|
||||
@@ -14,10 +16,12 @@ import { conquestFxFactory } from "../fx/ConquestFx";
|
||||
import { Fx, FxType } from "../fx/Fx";
|
||||
import { NukeAreaFx } from "../fx/NukeAreaFx";
|
||||
import { nukeFxFactory, ShockwaveFx } from "../fx/NukeFx";
|
||||
import { PingFx } from "../fx/PingFx";
|
||||
import { SpriteFx } from "../fx/SpriteFx";
|
||||
import { TargetFx } from "../fx/TargetFx";
|
||||
import { TextFx } from "../fx/TextFx";
|
||||
import { UnitExplosionFx } from "../fx/UnitExplosionFx";
|
||||
|
||||
import { Layer } from "./Layer";
|
||||
export class FxLayer implements Layer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
@@ -33,7 +37,10 @@ export class FxLayer implements Layer {
|
||||
private boatTargetFxByUnitId: Map<number, TargetFx> = new Map();
|
||||
private nukeTargetFxByUnitId: Map<number, NukeAreaFx> = new Map();
|
||||
|
||||
constructor(private game: GameView) {
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
) {
|
||||
this.theme = this.game.config().theme();
|
||||
}
|
||||
|
||||
@@ -345,6 +352,7 @@ export class FxLayer implements Layer {
|
||||
this.allFx.push(shockwave);
|
||||
}
|
||||
|
||||
private pingEventCleanup?: () => void;
|
||||
async init() {
|
||||
this.redraw();
|
||||
try {
|
||||
@@ -355,6 +363,19 @@ export class FxLayer implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.pingEventCleanup) {
|
||||
this.pingEventCleanup();
|
||||
if (this.pingEventCleanup) {
|
||||
this.pingEventCleanup();
|
||||
this.pingEventCleanup = undefined;
|
||||
}
|
||||
this.pingEventCleanup = this.eventBus.on(PingPlacedEvent, (event) => {
|
||||
const pingFx = new PingFx(this.game, event.type, event.tile);
|
||||
this.allFx.push(pingFx);
|
||||
});
|
||||
}
|
||||
this.pingEventCleanup?.();
|
||||
|
||||
redraw(): void {
|
||||
this.canvas = document.createElement("canvas");
|
||||
const context = this.canvas.getContext("2d");
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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 watchOutIcon from "../../../../resources/images/QuestionMarkIcon.svg";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { PingType } from "../../../core/game/Ping";
|
||||
import { PingSelectedEvent } from "../../InputHandler";
|
||||
|
||||
export const PING_ICON = swordIcon;
|
||||
|
||||
export const PING_COLORS = {
|
||||
[PingType.Attack]: "#ff0000",
|
||||
[PingType.Retreat]: "#ffa600",
|
||||
[PingType.Defend]: "#0000ff",
|
||||
[PingType.WatchOut]: "#ffff00",
|
||||
};
|
||||
|
||||
function createPingElement(
|
||||
id: string,
|
||||
name: string,
|
||||
icon: string,
|
||||
pingType: PingType,
|
||||
eventBus: EventBus,
|
||||
): MenuElement {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
icon,
|
||||
color: PING_COLORS[pingType],
|
||||
disabled: () => false,
|
||||
action: (params?: MenuElementParams) => {
|
||||
if (!params) return;
|
||||
eventBus.emit(new PingSelectedEvent(pingType));
|
||||
params.closeMenu();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createPingMenu(eventBus: EventBus): MenuElement {
|
||||
const pingAttackElement = createPingElement(
|
||||
"ping_attack",
|
||||
"Attack",
|
||||
swordIcon,
|
||||
PingType.Attack,
|
||||
eventBus,
|
||||
);
|
||||
const pingRetreatElement = createPingElement(
|
||||
"ping_retreat",
|
||||
"Retreat",
|
||||
retreatIcon,
|
||||
PingType.Retreat,
|
||||
eventBus,
|
||||
);
|
||||
const pingDefendElement = createPingElement(
|
||||
"ping_defend",
|
||||
"Defend",
|
||||
defendIcon,
|
||||
PingType.Defend,
|
||||
eventBus,
|
||||
);
|
||||
const pingWatchOutElement = createPingElement(
|
||||
"ping_watch_out",
|
||||
"Watch out",
|
||||
watchOutIcon,
|
||||
PingType.WatchOut,
|
||||
eventBus,
|
||||
);
|
||||
|
||||
return {
|
||||
id: "ping",
|
||||
name: "Pings",
|
||||
icon: PING_ICON,
|
||||
color: COLORS.ally,
|
||||
disabled: () => false,
|
||||
subMenu: () => [
|
||||
pingAttackElement,
|
||||
pingRetreatElement,
|
||||
pingDefendElement,
|
||||
pingWatchOutElement,
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { PingType } from "../../../core/game/Ping";
|
||||
import { MouseMoveEvent, PingSelectedEvent } from "../../InputHandler";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class PingTrajectoryPreviewLayer implements Layer {
|
||||
private mousePos = { x: 0, y: 0 };
|
||||
private pingTargetTile: TileRef | null = null;
|
||||
private currentPingType: PingType | null = null;
|
||||
private lastPingUpdate: number = 0;
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
private transformHandler: TransformHandler,
|
||||
) {}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.eventBus.off(MouseMoveEvent, this.handleMouseMove);
|
||||
this.eventBus.off(PingSelectedEvent, this.handlePingSelected);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.eventBus.on(MouseMoveEvent, this.handleMouseMove);
|
||||
this.eventBus.on(PingSelectedEvent, this.handlePingSelected);
|
||||
}
|
||||
|
||||
private handleMouseMove = (e: MouseMoveEvent) => {
|
||||
this.mousePos.x = e.x;
|
||||
this.mousePos.y = e.y;
|
||||
};
|
||||
|
||||
private handlePingSelected = (e: PingSelectedEvent) => {
|
||||
this.currentPingType = e.pingType;
|
||||
};
|
||||
|
||||
tick() {
|
||||
this.updatePingPreview();
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
this.drawPingPreview(context);
|
||||
}
|
||||
|
||||
private updatePingPreview() {
|
||||
if (this.currentPingType === null) {
|
||||
this.pingTargetTile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const now = performance.now();
|
||||
if (now - this.lastPingUpdate < 50) {
|
||||
return;
|
||||
}
|
||||
this.lastPingUpdate = now;
|
||||
|
||||
const rect = this.transformHandler.boundingRect();
|
||||
if (!rect) {
|
||||
this.pingTargetTile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const localX = this.mousePos.x - rect.left;
|
||||
const localY = this.mousePos.y - rect.top;
|
||||
const worldCoords = this.transformHandler.screenToWorldCoordinates(
|
||||
localX,
|
||||
localY,
|
||||
);
|
||||
|
||||
if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) {
|
||||
this.pingTargetTile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.pingTargetTile = this.game.ref(worldCoords.x, worldCoords.y);
|
||||
}
|
||||
|
||||
private getPingColor(): string {
|
||||
switch (this.currentPingType) {
|
||||
case PingType.Attack:
|
||||
return "rgba(255, 0, 0, 0.7)"; // Red
|
||||
case PingType.Retreat:
|
||||
return "rgba(0, 255, 0, 0.7)"; // Green
|
||||
case PingType.Defend:
|
||||
return "rgba(0, 0, 255, 0.7)"; // Blue
|
||||
case PingType.WatchOut:
|
||||
return "rgba(255, 255, 0, 0.7)"; // Yellow
|
||||
default:
|
||||
return "rgba(128, 128, 128, 0.7)"; // Gray fallback
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly PING_PREVIEW_RADIUS = 10;
|
||||
private drawPingPreview(context: CanvasRenderingContext2D) {
|
||||
if (this.currentPingType === null || this.pingTargetTile === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pingColor = this.getPingColor();
|
||||
|
||||
const offsetX = -this.game.width() / 2;
|
||||
const offsetY = -this.game.height() / 2;
|
||||
|
||||
const x = this.game.x(this.pingTargetTile) + offsetX;
|
||||
const y = this.game.y(this.pingTargetTile) + offsetY;
|
||||
|
||||
context.save();
|
||||
context.fillStyle = pingColor;
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
x,
|
||||
y,
|
||||
PingTrajectoryPreviewLayer.PING_PREVIEW_RADIUS,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.fill();
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import targetIcon from "../../../../resources/images/TargetIconWhite.svg";
|
||||
import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
|
||||
import xIcon from "../../../../resources/images/XIcon.svg";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { createPingMenu } from "./PingMenu";
|
||||
|
||||
export interface MenuElementParams {
|
||||
myPlayer: PlayerView;
|
||||
@@ -583,6 +584,7 @@ export const rootMenuElement: MenuElement = {
|
||||
|
||||
const menuItems: (MenuElement | null)[] = [
|
||||
infoMenuElement,
|
||||
createPingMenu(params.eventBus),
|
||||
...(isOwnTerritory
|
||||
? [deleteUnitElement, ally, buildMenuElement]
|
||||
: [boatMenuElement, ally, attackMenuElement]),
|
||||
|
||||
Reference in New Issue
Block a user