mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:40:44 +00:00
Multi-level radial menu (#1018)
## Description: - Refactored the radial menu to enable multi-level functionality. - Organized the actions into submenus. <img width="192" alt="Знімок екрана 2025-06-03 о 16 33 24" src="https://github.com/user-attachments/assets/6dae9792-bcae-4fc9-8ce4-1203d0efbfac" /> <img width="313" alt="Знімок екрана 2025-06-03 о 16 34 17" src="https://github.com/user-attachments/assets/5d78098f-b05b-40c4-bd70-8f2e3c08da2b" /> <img width="308" alt="Знімок екрана 2025-06-03 о 16 40 22" src="https://github.com/user-attachments/assets/01b00906-9e8b-47e9-8f97-cfd3c023c352" /> <img width="277" alt="Знімок екрана 2025-06-03 о 16 37 04" src="https://github.com/user-attachments/assets/60718c5b-8544-43e6-b891-2833d7fb789a" /> <img width="353" alt="Знімок екрана 2025-06-03 о 16 36 32" src="https://github.com/user-attachments/assets/8c35a0f8-5588-470f-8af4-8e6d4ba66d88" /> ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: oleksandr037617_47021 --------- Co-authored-by: Oleksandr Shysh <oleksandr.s@develops.today> Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<svg fill="#fff" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 26.676 26.676" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M26.105,21.891c-0.229,0-0.439-0.131-0.529-0.346l0,0c-0.066-0.156-1.716-3.857-7.885-4.59
|
||||
c-1.285-0.156-2.824-0.236-4.693-0.25v4.613c0,0.213-0.115,0.406-0.304,0.508c-0.188,0.098-0.413,0.084-0.588-0.033L0.254,13.815
|
||||
C0.094,13.708,0,13.528,0,13.339c0-0.191,0.094-0.365,0.254-0.477l11.857-7.979c0.175-0.121,0.398-0.129,0.588-0.029
|
||||
c0.19,0.102,0.303,0.295,0.303,0.502v4.293c2.578,0.336,13.674,2.33,13.674,11.674c0,0.271-0.191,0.508-0.459,0.562
|
||||
C26.18,21.891,26.141,21.891,26.105,21.891z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 764 B |
@@ -14,13 +14,13 @@ import { FxLayer } from "./layers/FxLayer";
|
||||
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
|
||||
import { Layer } from "./layers/Layer";
|
||||
import { Leaderboard } from "./layers/Leaderboard";
|
||||
import { MainRadialMenu } from "./layers/MainRadialMenu";
|
||||
import { MultiTabModal } from "./layers/MultiTabModal";
|
||||
import { NameLayer } from "./layers/NameLayer";
|
||||
import { OptionsMenu } from "./layers/OptionsMenu";
|
||||
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
|
||||
import { PlayerPanel } from "./layers/PlayerPanel";
|
||||
import { PlayerTeamLabel } from "./layers/PlayerTeamLabel";
|
||||
import { RadialMenu } from "./layers/RadialMenu";
|
||||
import { SpawnTimer } from "./layers/SpawnTimer";
|
||||
import { StructureLayer } from "./layers/StructureLayer";
|
||||
import { TeamStats } from "./layers/TeamStats";
|
||||
@@ -199,7 +199,7 @@ export function createRenderer(
|
||||
eventsDisplay,
|
||||
chatDisplay,
|
||||
buildMenu,
|
||||
new RadialMenu(
|
||||
new MainRadialMenu(
|
||||
eventBus,
|
||||
game,
|
||||
transformHandler,
|
||||
|
||||
@@ -19,7 +19,7 @@ import { BuildUnitIntentEvent } from "../../Transport";
|
||||
import { renderNumber } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
interface BuildItemDisplay {
|
||||
export interface BuildItemDisplay {
|
||||
unitType: UnitType;
|
||||
icon: string;
|
||||
description?: string;
|
||||
@@ -27,7 +27,7 @@ interface BuildItemDisplay {
|
||||
countable?: boolean;
|
||||
}
|
||||
|
||||
const buildTable: BuildItemDisplay[][] = [
|
||||
export const buildTable: BuildItemDisplay[][] = [
|
||||
[
|
||||
{
|
||||
unitType: UnitType.AtomBomb,
|
||||
@@ -96,12 +96,14 @@ const buildTable: BuildItemDisplay[][] = [
|
||||
],
|
||||
];
|
||||
|
||||
export const flattenedBuildTable = buildTable.flat();
|
||||
|
||||
@customElement("build-menu")
|
||||
export class BuildMenu extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public eventBus: EventBus;
|
||||
private clickedTile: TileRef;
|
||||
private playerActions: PlayerActions | null;
|
||||
public playerActions: PlayerActions | null;
|
||||
private filteredBuildTable: BuildItemDisplay[][] = buildTable;
|
||||
|
||||
tick() {
|
||||
@@ -302,7 +304,7 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
@state()
|
||||
private _hidden = true;
|
||||
|
||||
private canBuild(item: BuildItemDisplay): boolean {
|
||||
public canBuild(item: BuildItemDisplay): boolean {
|
||||
if (this.game?.myPlayer() === null || this.playerActions === null) {
|
||||
return false;
|
||||
}
|
||||
@@ -314,7 +316,7 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
return unit[0].canBuild !== false;
|
||||
}
|
||||
|
||||
private cost(item: BuildItemDisplay): Gold {
|
||||
public cost(item: BuildItemDisplay): Gold {
|
||||
for (const bu of this.playerActions?.buildableUnits ?? []) {
|
||||
if (bu.type === item.unitType) {
|
||||
return bu.cost;
|
||||
@@ -323,7 +325,7 @@ export class BuildMenu extends LitElement implements Layer {
|
||||
return 0n;
|
||||
}
|
||||
|
||||
private count(item: BuildItemDisplay): string {
|
||||
public count(item: BuildItemDisplay): string {
|
||||
const player = this.game?.myPlayer();
|
||||
if (!player) {
|
||||
return "?";
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { SendQuickChatEvent } from "../../Transport";
|
||||
import { translateText } from "../../Utils";
|
||||
import { ChatModal, QuickChatPhrase, quickChatPhrases } from "./ChatModal";
|
||||
import { COLORS, MenuElement } from "./RadialMenuElements";
|
||||
|
||||
export class ChatIntegration {
|
||||
private ctModal: ChatModal;
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
) {
|
||||
this.ctModal = document.querySelector("chat-modal") as ChatModal;
|
||||
|
||||
if (!this.ctModal) {
|
||||
throw new Error(
|
||||
"Chat modal element not found. Ensure chat-modal element exists in DOM before initializing ChatIntegration",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setupChatModal(sender: PlayerView, recipient: PlayerView) {
|
||||
this.ctModal.setSender(sender);
|
||||
this.ctModal.setRecipient(recipient);
|
||||
}
|
||||
|
||||
createQuickChatMenu(recipient: PlayerView): MenuElement[] {
|
||||
if (!this.ctModal) {
|
||||
throw new Error("Chat modal not set");
|
||||
}
|
||||
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (!myPlayer) {
|
||||
throw new Error("Current player not found");
|
||||
}
|
||||
|
||||
return this.ctModal.categories.map((category) => {
|
||||
const categoryTranslation = translateText(`chat.cat.${category.id}`);
|
||||
|
||||
const categoryColor =
|
||||
COLORS.chat[category.id as keyof typeof COLORS.chat] ||
|
||||
COLORS.chat.default;
|
||||
const phrases = quickChatPhrases[category.id] || [];
|
||||
|
||||
const phraseItems: MenuElement[] = phrases.map(
|
||||
(phrase: QuickChatPhrase) => {
|
||||
const phraseText = translateText(`chat.${category.id}.${phrase.key}`);
|
||||
|
||||
return {
|
||||
id: `phrase-${category.id}-${phrase.key}`,
|
||||
name: phraseText,
|
||||
disabled: false,
|
||||
text: this.shortenText(phraseText),
|
||||
fontSize: "10px",
|
||||
color: categoryColor,
|
||||
tooltipItems: [
|
||||
{
|
||||
text: phraseText,
|
||||
className: "description",
|
||||
},
|
||||
],
|
||||
action: () => {
|
||||
if (phrase.requiresPlayer) {
|
||||
this.ctModal.openWithSelection(
|
||||
category.id,
|
||||
phrase.key,
|
||||
myPlayer,
|
||||
recipient,
|
||||
);
|
||||
} else {
|
||||
this.eventBus.emit(
|
||||
new SendQuickChatEvent(
|
||||
recipient,
|
||||
`${category.id}.${phrase.key}`,
|
||||
{},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
id: `chat-category-${category.id}`,
|
||||
name: categoryTranslation,
|
||||
disabled: false,
|
||||
text: categoryTranslation,
|
||||
color: categoryColor,
|
||||
_action: () => {}, // Empty action placeholder for RadialMenu
|
||||
subMenu: () => phraseItems,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
shortenText(text: string, maxLength = 15): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,14 @@ import { EventBus } from "../../../core/EventBus";
|
||||
import { SendQuickChatEvent } from "../../Transport";
|
||||
import { translateText } from "../../Utils";
|
||||
|
||||
type QuickChatPhrase = {
|
||||
export type QuickChatPhrase = {
|
||||
key: string;
|
||||
requiresPlayer: boolean;
|
||||
};
|
||||
|
||||
type QuickChatPhrases = Record<string, QuickChatPhrase[]>;
|
||||
export type QuickChatPhrases = Record<string, QuickChatPhrase[]>;
|
||||
|
||||
const quickChatPhrases: QuickChatPhrases = quickChatData;
|
||||
export const quickChatPhrases: QuickChatPhrases = quickChatData;
|
||||
|
||||
@customElement("chat-modal")
|
||||
export class ChatModal extends LitElement {
|
||||
@@ -57,7 +57,7 @@ export class ChatModal extends LitElement {
|
||||
misc: [{ text: "Let's go!", requiresPlayer: false }],
|
||||
};
|
||||
|
||||
private categories = [
|
||||
public categories = [
|
||||
{ id: "help" },
|
||||
{ id: "attack" },
|
||||
{ id: "defend" },
|
||||
@@ -71,17 +71,6 @@ export class ChatModal extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const sortedPlayers = [...this.players].sort((a, b) => a.localeCompare(b));
|
||||
|
||||
const filteredPlayers = sortedPlayers.filter((player) =>
|
||||
player.toLowerCase().includes(this.playerSearchQuery),
|
||||
);
|
||||
|
||||
const otherPlayers = sortedPlayers.filter(
|
||||
(player) => !player.toLowerCase().includes(this.playerSearchQuery),
|
||||
);
|
||||
|
||||
const displayPlayers = [...filteredPlayers, ...otherPlayers];
|
||||
return html`
|
||||
<o-modal title="${translateText("chat.title")}">
|
||||
<div class="chat-columns">
|
||||
@@ -306,4 +295,35 @@ export class ChatModal extends LitElement {
|
||||
public setSender(value: PlayerView) {
|
||||
this.sender = value;
|
||||
}
|
||||
|
||||
public openWithSelection(
|
||||
categoryId: string,
|
||||
phraseKey: string,
|
||||
sender?: PlayerView,
|
||||
recipient?: PlayerView,
|
||||
) {
|
||||
if (sender && recipient) {
|
||||
const alivePlayerNames = this.g
|
||||
.players()
|
||||
.filter((p) => p.isAlive() && !(p.data.playerType === PlayerType.Bot))
|
||||
.map((p) => p.data.name);
|
||||
|
||||
this.players = alivePlayerNames;
|
||||
this.recipient = recipient;
|
||||
this.sender = sender;
|
||||
}
|
||||
|
||||
this.selectCategory(categoryId);
|
||||
|
||||
const phrase = this.getPhrasesForCategory(categoryId).find(
|
||||
(p) => p.key === phraseKey,
|
||||
);
|
||||
|
||||
if (phrase) {
|
||||
this.selectPhrase(phrase);
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
this.modalEl?.open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import { LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { PlayerActions, UnitType } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { UIState } from "../UIState";
|
||||
import { BuildMenu } from "./BuildMenu";
|
||||
import { ChatIntegration } from "./ChatIntegration";
|
||||
import { EmojiTable } from "./EmojiTable";
|
||||
import { Layer } from "./Layer";
|
||||
import { MenuEventManager } from "./MenuEventManager";
|
||||
import { PlayerActionHandler } from "./PlayerActionHandler";
|
||||
import { PlayerInfoOverlay } from "./PlayerInfoOverlay";
|
||||
import { PlayerPanel } from "./PlayerPanel";
|
||||
import { RadialMenu, RadialMenuConfig } from "./RadialMenu";
|
||||
import {
|
||||
COLORS,
|
||||
MenuElementParams,
|
||||
Slot,
|
||||
createRadialMenuItems,
|
||||
getRootMenuItems,
|
||||
updateCenterButton,
|
||||
} from "./RadialMenuElements";
|
||||
|
||||
import boatIcon from "../../../../resources/images/BoatIconWhite.svg";
|
||||
import buildIcon from "../../../../resources/images/BuildIconWhite.svg";
|
||||
import infoIcon from "../../../../resources/images/InfoIcon.svg";
|
||||
import swordIcon from "../../../../resources/images/SwordIconWhite.svg";
|
||||
|
||||
@customElement("main-radial-menu")
|
||||
export class MainRadialMenu extends LitElement implements Layer {
|
||||
private radialMenu: RadialMenu;
|
||||
private lastTickRefresh: number = 0;
|
||||
private tickRefreshInterval: number = 500;
|
||||
private needsRefresh: boolean = false;
|
||||
|
||||
private playerActionHandler: PlayerActionHandler;
|
||||
private menuEventManager: MenuEventManager;
|
||||
private chatIntegration: ChatIntegration;
|
||||
|
||||
constructor(
|
||||
private eventBus: EventBus,
|
||||
private game: GameView,
|
||||
private transformHandler: TransformHandler,
|
||||
private emojiTable: EmojiTable,
|
||||
private buildMenu: BuildMenu,
|
||||
private uiState: UIState,
|
||||
private playerInfoOverlay: PlayerInfoOverlay,
|
||||
private playerPanel: PlayerPanel,
|
||||
) {
|
||||
super();
|
||||
|
||||
const menuConfig: RadialMenuConfig = {
|
||||
centerButtonIcon: swordIcon,
|
||||
tooltipStyle: `
|
||||
.radial-tooltip .cost {
|
||||
margin-top: 4px;
|
||||
color: ${COLORS.tooltip.cost};
|
||||
}
|
||||
.radial-tooltip .count {
|
||||
color: ${COLORS.tooltip.count};
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
this.radialMenu = new RadialMenu(menuConfig);
|
||||
|
||||
this.playerActionHandler = new PlayerActionHandler(
|
||||
this.eventBus,
|
||||
this.uiState,
|
||||
);
|
||||
|
||||
this.menuEventManager = new MenuEventManager(
|
||||
this.eventBus,
|
||||
this.game,
|
||||
this.transformHandler,
|
||||
this.radialMenu,
|
||||
this.buildMenu,
|
||||
this.emojiTable,
|
||||
this.playerInfoOverlay,
|
||||
this.playerPanel,
|
||||
);
|
||||
|
||||
this.chatIntegration = new ChatIntegration(this.game, this.eventBus);
|
||||
|
||||
this.radialMenu.setRootMenuItems(getRootMenuItems());
|
||||
}
|
||||
|
||||
init() {
|
||||
this.radialMenu.init();
|
||||
|
||||
this.menuEventManager.setContextMenuCallback((myPlayer, tile, actions) => {
|
||||
this.handlePlayerActions(myPlayer, actions, tile);
|
||||
});
|
||||
|
||||
this.menuEventManager.init();
|
||||
}
|
||||
|
||||
private async handlePlayerActions(
|
||||
myPlayer: PlayerView,
|
||||
actions: PlayerActions,
|
||||
tile: TileRef,
|
||||
) {
|
||||
this.buildMenu.playerActions = actions;
|
||||
|
||||
const tileOwner = this.game.owner(tile);
|
||||
const recipient = tileOwner.isPlayer() ? (tileOwner as PlayerView) : null;
|
||||
|
||||
if (myPlayer && recipient) {
|
||||
this.chatIntegration.setupChatModal(myPlayer, recipient);
|
||||
}
|
||||
|
||||
const params: MenuElementParams = {
|
||||
myPlayer,
|
||||
selected: recipient,
|
||||
tileOwner,
|
||||
tile,
|
||||
playerActions: actions,
|
||||
game: this.game,
|
||||
buildMenu: this.buildMenu,
|
||||
emojiTable: this.emojiTable,
|
||||
playerActionHandler: this.playerActionHandler,
|
||||
playerPanel: this.playerPanel,
|
||||
chatIntegration: this.chatIntegration,
|
||||
closeMenu: () => this.menuEventManager.closeMenu(),
|
||||
};
|
||||
|
||||
const menuItems = createRadialMenuItems(params);
|
||||
|
||||
this.radialMenu.setRootMenuItems(menuItems);
|
||||
|
||||
updateCenterButton(params, (enabled, action) => {
|
||||
this.radialMenu.enableCenterButton(enabled, action);
|
||||
});
|
||||
}
|
||||
|
||||
async tick() {
|
||||
const clickedCell = this.menuEventManager.getClickedCell();
|
||||
if (!this.radialMenu.isMenuVisible() || clickedCell === null) return;
|
||||
|
||||
const currentTime = new Date().getTime();
|
||||
if (
|
||||
currentTime - this.lastTickRefresh < this.tickRefreshInterval &&
|
||||
!this.needsRefresh
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (myPlayer === null || !myPlayer.isAlive()) return;
|
||||
|
||||
const tile = this.game.ref(clickedCell.x, clickedCell.y);
|
||||
|
||||
const isSpawnPhase = this.game.inSpawnPhase();
|
||||
const wasInSpawnPhase = this.menuEventManager.getWasInSpawnPhase();
|
||||
|
||||
if (wasInSpawnPhase !== isSpawnPhase) {
|
||||
if (wasInSpawnPhase && !isSpawnPhase) {
|
||||
this.needsRefresh = true;
|
||||
this.menuEventManager.setWasInSpawnPhase(isSpawnPhase);
|
||||
|
||||
const actions = await this.playerActionHandler.getPlayerActions(
|
||||
myPlayer,
|
||||
tile,
|
||||
);
|
||||
this.updateMenuState(myPlayer, actions, tile);
|
||||
this.radialMenu.refreshMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
this.menuEventManager.closeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if tile ownership has changed
|
||||
const originalTileOwner = this.menuEventManager.getOriginalTileOwner();
|
||||
if (originalTileOwner && originalTileOwner.isPlayer()) {
|
||||
if (this.game.owner(tile) !== originalTileOwner) {
|
||||
this.menuEventManager.closeMenu();
|
||||
return;
|
||||
}
|
||||
} else if (originalTileOwner) {
|
||||
if (
|
||||
this.game.owner(tile).isPlayer() ||
|
||||
this.game.owner(tile) === myPlayer
|
||||
) {
|
||||
this.menuEventManager.closeMenu();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.lastTickRefresh = currentTime;
|
||||
this.needsRefresh = false;
|
||||
|
||||
const actions = await this.playerActionHandler.getPlayerActions(
|
||||
myPlayer,
|
||||
tile,
|
||||
);
|
||||
this.updateMenuState(myPlayer, actions, tile);
|
||||
}
|
||||
|
||||
private updateMenuState(
|
||||
myPlayer: PlayerView,
|
||||
actions: PlayerActions,
|
||||
tile: TileRef,
|
||||
) {
|
||||
if (!this.radialMenu.isMenuVisible()) return;
|
||||
|
||||
const tileOwner = this.game.owner(tile);
|
||||
const recipient = tileOwner.isPlayer() ? (tileOwner as PlayerView) : null;
|
||||
|
||||
const params: MenuElementParams = {
|
||||
myPlayer,
|
||||
selected: recipient,
|
||||
tileOwner,
|
||||
tile,
|
||||
playerActions: actions,
|
||||
game: this.game,
|
||||
buildMenu: this.buildMenu,
|
||||
emojiTable: this.emojiTable,
|
||||
playerActionHandler: this.playerActionHandler,
|
||||
playerPanel: this.playerPanel,
|
||||
chatIntegration: this.chatIntegration,
|
||||
closeMenu: () => this.menuEventManager.closeMenu(),
|
||||
};
|
||||
|
||||
if (this.radialMenu.getCurrentLevel() === 0) {
|
||||
updateCenterButton(params, (enabled, action) => {
|
||||
this.radialMenu.enableCenterButton(enabled, action);
|
||||
});
|
||||
}
|
||||
|
||||
const canBuildTransport = actions.buildableUnits.find(
|
||||
(bu) => bu.type === UnitType.TransportShip,
|
||||
)?.canBuild;
|
||||
|
||||
this.radialMenu.updateMenuItem(
|
||||
Slot.Build,
|
||||
!this.game.inSpawnPhase(),
|
||||
COLORS.build,
|
||||
buildIcon,
|
||||
);
|
||||
|
||||
if (actions?.interaction?.canSendAllianceRequest) {
|
||||
this.radialMenu.updateMenuItem(Slot.Ally, true, COLORS.ally, undefined);
|
||||
} else if (actions?.interaction?.canBreakAlliance) {
|
||||
this.radialMenu.updateMenuItem(
|
||||
Slot.Ally,
|
||||
true,
|
||||
COLORS.breakAlly,
|
||||
undefined,
|
||||
);
|
||||
} else {
|
||||
this.radialMenu.updateMenuItem(Slot.Ally, false, undefined, undefined);
|
||||
}
|
||||
|
||||
this.radialMenu.updateMenuItem(
|
||||
Slot.Boat,
|
||||
!!canBuildTransport,
|
||||
COLORS.boat,
|
||||
boatIcon,
|
||||
);
|
||||
|
||||
this.radialMenu.updateMenuItem(
|
||||
Slot.Info,
|
||||
this.game.hasOwner(tile),
|
||||
COLORS.info,
|
||||
infoIcon,
|
||||
);
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
this.radialMenu.renderLayer(context);
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return this.radialMenu.shouldTransform();
|
||||
}
|
||||
|
||||
redraw() {
|
||||
// No redraw implementation needed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { Cell, PlayerActions, TerraNullius } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import {
|
||||
CloseViewEvent,
|
||||
ContextMenuEvent,
|
||||
MouseUpEvent,
|
||||
ShowBuildMenuEvent,
|
||||
} from "../../InputHandler";
|
||||
import { SendSpawnIntentEvent } from "../../Transport";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { BuildMenu } from "./BuildMenu";
|
||||
import { EmojiTable } from "./EmojiTable";
|
||||
import { PlayerInfoOverlay } from "./PlayerInfoOverlay";
|
||||
import { PlayerPanel } from "./PlayerPanel";
|
||||
import { RadialMenu } from "./RadialMenu";
|
||||
|
||||
export type ContextMenuCallback = (
|
||||
myPlayer: PlayerView,
|
||||
tile: TileRef,
|
||||
actions: PlayerActions,
|
||||
) => void;
|
||||
|
||||
export class MenuEventManager {
|
||||
private clickedCell: Cell | null = null;
|
||||
private lastClosed: number = 0;
|
||||
private originalTileOwner: PlayerView | TerraNullius | null = null;
|
||||
private wasInSpawnPhase: boolean = false;
|
||||
private onContextMenuCallback: ContextMenuCallback | null = null;
|
||||
|
||||
constructor(
|
||||
private eventBus: EventBus,
|
||||
private game: GameView,
|
||||
private transformHandler: TransformHandler,
|
||||
private radialMenu: RadialMenu,
|
||||
private buildMenu: BuildMenu,
|
||||
private emojiTable: EmojiTable,
|
||||
private playerInfoOverlay: PlayerInfoOverlay,
|
||||
private playerPanel: PlayerPanel,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
this.eventBus.on(ContextMenuEvent, (e) => this.onContextMenu(e));
|
||||
this.eventBus.on(MouseUpEvent, (e) => this.onPointerUp(e));
|
||||
this.eventBus.on(CloseViewEvent, () => this.closeMenu());
|
||||
this.eventBus.on(ShowBuildMenuEvent, (e) => this.onShowBuildMenu(e));
|
||||
}
|
||||
|
||||
setContextMenuCallback(callback: ContextMenuCallback) {
|
||||
this.onContextMenuCallback = callback;
|
||||
}
|
||||
|
||||
onContextMenu(event: ContextMenuEvent): Cell | null {
|
||||
if (this.lastClosed + 200 > new Date().getTime()) return null;
|
||||
|
||||
this.closeMenu();
|
||||
|
||||
if (this.radialMenu.isMenuVisible()) {
|
||||
this.radialMenu.hideRadialMenu();
|
||||
return null;
|
||||
} else {
|
||||
this.radialMenu.showRadialMenu(event.x, event.y);
|
||||
}
|
||||
|
||||
this.radialMenu.disableAllButtons();
|
||||
this.clickedCell = this.transformHandler.screenToWorldCoordinates(
|
||||
event.x,
|
||||
event.y,
|
||||
);
|
||||
|
||||
if (
|
||||
!this.clickedCell ||
|
||||
!this.game.isValidCoord(this.clickedCell.x, this.clickedCell.y)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tile = this.game.ref(this.clickedCell.x, this.clickedCell.y);
|
||||
this.originalTileOwner = this.game.owner(tile);
|
||||
this.wasInSpawnPhase = this.game.inSpawnPhase();
|
||||
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (myPlayer === null) {
|
||||
throw new Error("my player not found");
|
||||
}
|
||||
|
||||
if (myPlayer && !myPlayer.isAlive() && !this.game.inSpawnPhase()) {
|
||||
this.radialMenu.hideRadialMenu();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.game.inSpawnPhase()) {
|
||||
if (this.game.isLand(tile) && !this.game.hasOwner(tile)) {
|
||||
this.radialMenu.enableCenterButton(true, () => {
|
||||
if (this.clickedCell === null) return;
|
||||
this.eventBus.emit(new SendSpawnIntentEvent(this.clickedCell));
|
||||
this.radialMenu.hideRadialMenu();
|
||||
});
|
||||
|
||||
return this.clickedCell;
|
||||
}
|
||||
}
|
||||
|
||||
myPlayer.actions(tile).then((actions) => {
|
||||
if (this.onContextMenuCallback) {
|
||||
this.onContextMenuCallback(myPlayer, tile, actions);
|
||||
}
|
||||
});
|
||||
|
||||
return this.clickedCell;
|
||||
}
|
||||
|
||||
getClickedCell(): Cell | null {
|
||||
return this.clickedCell;
|
||||
}
|
||||
|
||||
getOriginalTileOwner(): PlayerView | TerraNullius | null {
|
||||
return this.originalTileOwner;
|
||||
}
|
||||
|
||||
getWasInSpawnPhase(): boolean {
|
||||
return this.wasInSpawnPhase;
|
||||
}
|
||||
|
||||
setWasInSpawnPhase(value: boolean) {
|
||||
this.wasInSpawnPhase = value;
|
||||
}
|
||||
|
||||
onPointerUp(event: MouseUpEvent) {
|
||||
this.playerInfoOverlay.hide();
|
||||
this.hideEverything();
|
||||
}
|
||||
|
||||
onShowBuildMenu(e: ShowBuildMenuEvent): TileRef | null {
|
||||
const clickedCell = this.transformHandler.screenToWorldCoordinates(
|
||||
e.x,
|
||||
e.y,
|
||||
);
|
||||
if (clickedCell === null) {
|
||||
return null;
|
||||
}
|
||||
if (!this.game.isValidCoord(clickedCell.x, clickedCell.y)) {
|
||||
return null;
|
||||
}
|
||||
const tile = this.game.ref(clickedCell.x, clickedCell.y);
|
||||
const p = this.game.myPlayer();
|
||||
if (p === null) {
|
||||
return null;
|
||||
}
|
||||
this.buildMenu.showMenu(tile);
|
||||
return tile;
|
||||
}
|
||||
|
||||
closeMenu() {
|
||||
if (this.radialMenu.isMenuVisible()) {
|
||||
this.radialMenu.hideRadialMenu();
|
||||
}
|
||||
|
||||
if (this.buildMenu.isVisible) {
|
||||
this.buildMenu.hideMenu();
|
||||
}
|
||||
|
||||
if (this.emojiTable.isVisible) {
|
||||
this.emojiTable.hideTable();
|
||||
}
|
||||
|
||||
if (this.playerPanel.isVisible) {
|
||||
this.playerPanel.hide();
|
||||
}
|
||||
}
|
||||
|
||||
hideEverything() {
|
||||
if (this.radialMenu.isMenuVisible()) {
|
||||
this.radialMenu.hideRadialMenu();
|
||||
this.lastClosed = new Date().getTime();
|
||||
}
|
||||
this.emojiTable.hideTable();
|
||||
this.buildMenu.hideMenu();
|
||||
}
|
||||
|
||||
enableCenterButton(enabled: boolean, action: () => void) {
|
||||
this.radialMenu.enableCenterButton(enabled, action);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { Cell, PlayerActions, UnitType } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { PlayerView } from "../../../core/game/GameView";
|
||||
import {
|
||||
BuildUnitIntentEvent,
|
||||
SendAllianceRequestIntentEvent,
|
||||
SendAttackIntentEvent,
|
||||
SendBoatAttackIntentEvent,
|
||||
SendBreakAllianceIntentEvent,
|
||||
SendDonateGoldIntentEvent,
|
||||
SendDonateTroopsIntentEvent,
|
||||
SendEmbargoIntentEvent,
|
||||
SendEmojiIntentEvent,
|
||||
SendQuickChatEvent,
|
||||
SendSpawnIntentEvent,
|
||||
SendTargetPlayerIntentEvent,
|
||||
} from "../../Transport";
|
||||
import { UIState } from "../UIState";
|
||||
|
||||
export class PlayerActionHandler {
|
||||
constructor(
|
||||
private eventBus: EventBus,
|
||||
private uiState: UIState,
|
||||
) {}
|
||||
|
||||
async getPlayerActions(
|
||||
player: PlayerView,
|
||||
tile: TileRef,
|
||||
): Promise<PlayerActions> {
|
||||
return await player.actions(tile);
|
||||
}
|
||||
|
||||
handleAttack(player: PlayerView, targetId: string | null) {
|
||||
this.eventBus.emit(
|
||||
new SendAttackIntentEvent(
|
||||
targetId,
|
||||
this.uiState.attackRatio * player.troops(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
handleBoatAttack(
|
||||
player: PlayerView,
|
||||
targetId: string,
|
||||
targetCell: Cell,
|
||||
spawnTile: Cell | null,
|
||||
) {
|
||||
this.eventBus.emit(
|
||||
new SendBoatAttackIntentEvent(
|
||||
targetId,
|
||||
targetCell,
|
||||
this.uiState.attackRatio * player.troops(),
|
||||
spawnTile,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async findBestTransportShipSpawn(
|
||||
player: PlayerView,
|
||||
tile: TileRef,
|
||||
): Promise<TileRef | false> {
|
||||
return await player.bestTransportShipSpawn(tile);
|
||||
}
|
||||
|
||||
handleBuildUnit(unitType: UnitType, cellX: number, cellY: number) {
|
||||
this.eventBus.emit(
|
||||
new BuildUnitIntentEvent(unitType, new Cell(cellX, cellY)),
|
||||
);
|
||||
}
|
||||
|
||||
handleSpawn(spawnCell: Cell) {
|
||||
this.eventBus.emit(new SendSpawnIntentEvent(spawnCell));
|
||||
}
|
||||
|
||||
handleAllianceRequest(player: PlayerView, recipient: PlayerView) {
|
||||
this.eventBus.emit(new SendAllianceRequestIntentEvent(player, recipient));
|
||||
}
|
||||
|
||||
handleBreakAlliance(player: PlayerView, recipient: PlayerView) {
|
||||
this.eventBus.emit(new SendBreakAllianceIntentEvent(player, recipient));
|
||||
}
|
||||
|
||||
handleTargetPlayer(targetId: string | null) {
|
||||
if (!targetId) return;
|
||||
|
||||
this.eventBus.emit(new SendTargetPlayerIntentEvent(targetId));
|
||||
}
|
||||
|
||||
handleDonateGold(recipient: PlayerView) {
|
||||
this.eventBus.emit(new SendDonateGoldIntentEvent(recipient, null));
|
||||
}
|
||||
|
||||
handleDonateTroops(recipient: PlayerView) {
|
||||
this.eventBus.emit(new SendDonateTroopsIntentEvent(recipient, null));
|
||||
}
|
||||
|
||||
handleEmbargo(recipient: PlayerView, action: "start" | "stop") {
|
||||
this.eventBus.emit(new SendEmbargoIntentEvent(recipient, action));
|
||||
}
|
||||
|
||||
handleEmoji(targetPlayer: PlayerView | "AllPlayers", emojiIndex: number) {
|
||||
this.eventBus.emit(new SendEmojiIntentEvent(targetPlayer, emojiIndex));
|
||||
}
|
||||
|
||||
handleQuickChat(recipient: PlayerView, chatKey: string, params: any = {}) {
|
||||
this.eventBus.emit(new SendQuickChatEvent(recipient, chatKey, params));
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
private tile: TileRef | null = null;
|
||||
|
||||
@state()
|
||||
private isVisible: boolean = false;
|
||||
public isVisible: boolean = false;
|
||||
|
||||
@state()
|
||||
private allianceExpiryText: string | null = null;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,471 @@
|
||||
import {
|
||||
AllPlayers,
|
||||
Cell,
|
||||
PlayerActions,
|
||||
TerraNullius,
|
||||
UnitType,
|
||||
} from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { flattenedEmojiTable } from "../../../core/Util";
|
||||
import { renderNumber, translateText } from "../../Utils";
|
||||
import { BuildItemDisplay, BuildMenu, flattenedBuildTable } from "./BuildMenu";
|
||||
import { ChatIntegration } from "./ChatIntegration";
|
||||
import { EmojiTable } from "./EmojiTable";
|
||||
import { PlayerActionHandler } from "./PlayerActionHandler";
|
||||
import { PlayerPanel } from "./PlayerPanel";
|
||||
import { TooltipItem } from "./RadialMenu";
|
||||
|
||||
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
|
||||
import boatIcon from "../../../../resources/images/BoatIconWhite.svg";
|
||||
import buildIcon from "../../../../resources/images/BuildIconWhite.svg";
|
||||
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
|
||||
import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg";
|
||||
import donateTroopIcon from "../../../../resources/images/DonateTroopIconWhite.svg";
|
||||
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
|
||||
import infoIcon from "../../../../resources/images/InfoIcon.svg";
|
||||
import targetIcon from "../../../../resources/images/TargetIconWhite.svg";
|
||||
import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
|
||||
|
||||
export interface MenuElementParams {
|
||||
myPlayer: PlayerView;
|
||||
selected: PlayerView | null;
|
||||
tileOwner: PlayerView | TerraNullius;
|
||||
tile: TileRef;
|
||||
playerActions: PlayerActions;
|
||||
game: GameView;
|
||||
buildMenu: BuildMenu;
|
||||
emojiTable: EmojiTable;
|
||||
playerActionHandler: PlayerActionHandler;
|
||||
playerPanel: PlayerPanel;
|
||||
chatIntegration: ChatIntegration;
|
||||
closeMenu: () => void;
|
||||
}
|
||||
|
||||
export interface MenuElement {
|
||||
id: string;
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
displayed?: boolean;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
text?: string;
|
||||
fontSize?: string;
|
||||
tooltipItems?: TooltipItem[];
|
||||
|
||||
action?: (params: MenuElementParams) => void; // For leaf items that perform actions
|
||||
subMenu?: (params: MenuElementParams) => MenuElement[]; // For non-leaf items that open submenus
|
||||
|
||||
// Runtime properties used by RadialMenu (not to be set by menu element creators)
|
||||
children?: MenuElement[];
|
||||
_action?: () => void;
|
||||
}
|
||||
|
||||
export const COLORS = {
|
||||
build: "#ebe250",
|
||||
building: "#2c2c2c",
|
||||
boat: "#3f6ab1",
|
||||
ally: "#53ac75",
|
||||
breakAlly: "#c74848",
|
||||
info: "#64748B",
|
||||
target: "#ff0000",
|
||||
infoDetails: "#7f8c8d",
|
||||
infoEmoji: "#f1c40f",
|
||||
trade: "#008080",
|
||||
embargo: "#6600cc",
|
||||
tooltip: {
|
||||
cost: "#ffd700",
|
||||
count: "#aaa",
|
||||
},
|
||||
chat: {
|
||||
default: "#66c",
|
||||
help: "#4caf50",
|
||||
attack: "#f44336",
|
||||
defend: "#2196f3",
|
||||
greet: "#ff9800",
|
||||
misc: "#9c27b0",
|
||||
warnings: "#e3c532",
|
||||
},
|
||||
};
|
||||
|
||||
export enum Slot {
|
||||
Info = "info",
|
||||
Boat = "boat",
|
||||
Build = "build",
|
||||
Ally = "ally",
|
||||
Back = "back",
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a MenuElement tree to a version usable by the RadialMenu
|
||||
* by resolving subMenu functions and setting up actions
|
||||
*/
|
||||
export function prepareMenuElementsForRadialMenu(
|
||||
elements: MenuElement[],
|
||||
params: MenuElementParams,
|
||||
): MenuElement[] {
|
||||
return elements.map((element) => {
|
||||
const prepared: MenuElement = { ...element };
|
||||
|
||||
// If the element has a subMenu function, execute it to get the children
|
||||
if (element.subMenu) {
|
||||
prepared.children = prepareMenuElementsForRadialMenu(
|
||||
element.subMenu(params),
|
||||
params,
|
||||
);
|
||||
// We don't need the subMenu function anymore
|
||||
prepared.subMenu = undefined;
|
||||
}
|
||||
|
||||
// Set up the action function to call the element's action with params
|
||||
if (element.action) {
|
||||
prepared._action = () => element.action!(params);
|
||||
} else {
|
||||
prepared._action = () => {};
|
||||
}
|
||||
|
||||
return prepared;
|
||||
});
|
||||
}
|
||||
|
||||
export const buildMenuElement: MenuElement = {
|
||||
id: Slot.Build,
|
||||
name: "build",
|
||||
disabled: false,
|
||||
icon: buildIcon,
|
||||
color: COLORS.build,
|
||||
|
||||
subMenu: (params: MenuElementParams) => {
|
||||
const buildElements: MenuElement[] = flattenedBuildTable.map(
|
||||
(item: BuildItemDisplay) => ({
|
||||
id: `build_${item.unitType}`,
|
||||
name: item.key
|
||||
? item.key.replace("unit_type.", "")
|
||||
: item.unitType.toString(),
|
||||
disabled: !params.buildMenu.canBuild(item),
|
||||
color: params.buildMenu.canBuild(item) ? COLORS.building : undefined,
|
||||
icon: item.icon,
|
||||
tooltipItems: [
|
||||
{ text: translateText(item.key || ""), className: "title" },
|
||||
{
|
||||
text: translateText(item.description || ""),
|
||||
className: "description",
|
||||
},
|
||||
{
|
||||
text: `${renderNumber(params.buildMenu.cost(item))} ${translateText("player_panel.gold")}`,
|
||||
className: "cost",
|
||||
},
|
||||
item.countable
|
||||
? { text: `${params.buildMenu.count(item)}x`, className: "count" }
|
||||
: null,
|
||||
].filter((item): item is TooltipItem => item !== null),
|
||||
action: (params: MenuElementParams) => {
|
||||
params.playerActionHandler.handleBuildUnit(
|
||||
item.unitType,
|
||||
params.game.x(params.tile),
|
||||
params.game.y(params.tile),
|
||||
);
|
||||
params.closeMenu();
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
buildElements.push({
|
||||
id: "build_menu",
|
||||
name: "build",
|
||||
disabled: false,
|
||||
color: COLORS.build,
|
||||
icon: buildIcon,
|
||||
action: (params: MenuElementParams) => {
|
||||
params.buildMenu.showMenu(params.tile);
|
||||
},
|
||||
});
|
||||
|
||||
return buildElements;
|
||||
},
|
||||
};
|
||||
|
||||
export const boatMenuElement: MenuElement = {
|
||||
id: Slot.Boat,
|
||||
name: "boat",
|
||||
disabled: false,
|
||||
icon: boatIcon,
|
||||
color: COLORS.boat,
|
||||
|
||||
action: async (params: MenuElementParams) => {
|
||||
if (!params.selected) return;
|
||||
|
||||
const spawn = await params.playerActionHandler.findBestTransportShipSpawn(
|
||||
params.myPlayer,
|
||||
params.tile,
|
||||
);
|
||||
|
||||
let spawnTile: Cell | null = null;
|
||||
if (spawn !== false) {
|
||||
spawnTile = new Cell(params.game.x(spawn), params.game.y(spawn));
|
||||
}
|
||||
|
||||
params.playerActionHandler.handleBoatAttack(
|
||||
params.myPlayer,
|
||||
params.selected.id(),
|
||||
new Cell(params.game.x(params.tile), params.game.y(params.tile)),
|
||||
spawnTile,
|
||||
);
|
||||
|
||||
params.closeMenu();
|
||||
},
|
||||
};
|
||||
|
||||
export const infoMenuElement: MenuElement = {
|
||||
id: Slot.Info,
|
||||
name: "info",
|
||||
disabled: false,
|
||||
icon: infoIcon,
|
||||
color: COLORS.info,
|
||||
|
||||
subMenu: (params: MenuElementParams) => {
|
||||
if (!params.selected) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
id: "info_chat",
|
||||
name: "chat",
|
||||
disabled: false,
|
||||
color: COLORS.chat.default,
|
||||
icon: chatIcon,
|
||||
subMenu: (params: MenuElementParams) =>
|
||||
params.chatIntegration
|
||||
.createQuickChatMenu(params.selected!)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
action: item.action
|
||||
? (_params: MenuElementParams) => item.action!(params)
|
||||
: undefined,
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: "ally_target",
|
||||
name: "target",
|
||||
disabled: false,
|
||||
color: COLORS.target,
|
||||
icon: targetIcon,
|
||||
action: (params: MenuElementParams) => {
|
||||
params.playerActionHandler.handleTargetPlayer(params.selected!.id());
|
||||
params.closeMenu();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ally_trade",
|
||||
name: "trade",
|
||||
disabled: !!params.playerActions?.interaction?.canEmbargo,
|
||||
displayed: !params.playerActions?.interaction?.canEmbargo,
|
||||
color: COLORS.trade,
|
||||
text: translateText("player_panel.start_trade"),
|
||||
action: (params: MenuElementParams) => {
|
||||
params.playerActionHandler.handleEmbargo(params.selected!, "start");
|
||||
params.closeMenu();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ally_embargo",
|
||||
name: "embargo",
|
||||
disabled: !params.playerActions?.interaction?.canEmbargo,
|
||||
displayed: !!params.playerActions?.interaction?.canEmbargo,
|
||||
color: COLORS.embargo,
|
||||
text: translateText("player_panel.stop_trade"),
|
||||
action: (params: MenuElementParams) => {
|
||||
params.playerActionHandler.handleEmbargo(params.selected!, "stop");
|
||||
params.closeMenu();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ally_request",
|
||||
name: "request",
|
||||
disabled: !params.playerActions?.interaction?.canSendAllianceRequest,
|
||||
displayed: !params.playerActions?.interaction?.canBreakAlliance,
|
||||
color: COLORS.ally,
|
||||
icon: allianceIcon,
|
||||
action: (params: MenuElementParams) => {
|
||||
params.playerActionHandler.handleAllianceRequest(
|
||||
params.myPlayer,
|
||||
params.selected!,
|
||||
);
|
||||
params.closeMenu();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ally_break",
|
||||
name: "break",
|
||||
disabled: !params.playerActions?.interaction?.canBreakAlliance,
|
||||
displayed: !!params.playerActions?.interaction?.canBreakAlliance,
|
||||
color: COLORS.breakAlly,
|
||||
icon: traitorIcon,
|
||||
action: (params: MenuElementParams) => {
|
||||
params.playerActionHandler.handleBreakAlliance(
|
||||
params.myPlayer,
|
||||
params.selected!,
|
||||
);
|
||||
params.closeMenu();
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: "ally_donate_gold",
|
||||
name: "donate gold",
|
||||
disabled: !params.playerActions?.interaction?.canDonate,
|
||||
color: COLORS.ally,
|
||||
icon: donateGoldIcon,
|
||||
action: (params: MenuElementParams) => {
|
||||
params.playerActionHandler.handleDonateGold(params.selected!);
|
||||
params.closeMenu();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ally_donate_troops",
|
||||
name: "donate troops",
|
||||
disabled: !params.playerActions?.interaction?.canDonate,
|
||||
color: COLORS.ally,
|
||||
icon: donateTroopIcon,
|
||||
action: (params: MenuElementParams) => {
|
||||
params.playerActionHandler.handleDonateTroops(params.selected!);
|
||||
params.closeMenu();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "info_player",
|
||||
name: "player",
|
||||
disabled: false,
|
||||
color: COLORS.info,
|
||||
icon: infoIcon,
|
||||
action: (params: MenuElementParams) => {
|
||||
params.playerPanel.show(params.playerActions, params.tile);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "info_emoji",
|
||||
name: "emoji",
|
||||
disabled: false,
|
||||
color: COLORS.infoEmoji,
|
||||
icon: emojiIcon,
|
||||
subMenu: () => {
|
||||
const emojiElements: MenuElement[] = [];
|
||||
|
||||
const emojiCount = 15;
|
||||
for (let i = 0; i < emojiCount; i++) {
|
||||
emojiElements.push({
|
||||
id: `emoji_${i}`,
|
||||
name: flattenedEmojiTable[i],
|
||||
text: flattenedEmojiTable[i],
|
||||
disabled: false,
|
||||
fontSize: "25px",
|
||||
action: (params: MenuElementParams) => {
|
||||
const targetPlayer =
|
||||
params.selected === params.game.myPlayer()
|
||||
? AllPlayers
|
||||
: params.selected;
|
||||
params.playerActionHandler.handleEmoji(targetPlayer!, i);
|
||||
params.closeMenu();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
emojiElements.push({
|
||||
id: "emoji_more",
|
||||
name: "more",
|
||||
disabled: false,
|
||||
color: COLORS.infoEmoji,
|
||||
icon: emojiIcon,
|
||||
action: (params: MenuElementParams) => {
|
||||
params.emojiTable.showTable((emoji) => {
|
||||
const targetPlayer =
|
||||
params.selected === params.game.myPlayer()
|
||||
? AllPlayers
|
||||
: params.selected;
|
||||
params.playerActionHandler.handleEmoji(
|
||||
targetPlayer!,
|
||||
flattenedEmojiTable.indexOf(emoji),
|
||||
);
|
||||
params.emojiTable.hideTable();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return emojiElements;
|
||||
},
|
||||
},
|
||||
].filter((item) => item.displayed !== false);
|
||||
},
|
||||
};
|
||||
|
||||
export function createMenuItems(params: MenuElementParams): MenuElement[] {
|
||||
const canBuildTransport = params.playerActions.buildableUnits.find(
|
||||
(bu) => bu.type === UnitType.TransportShip,
|
||||
)?.canBuild;
|
||||
|
||||
return [
|
||||
{
|
||||
...boatMenuElement,
|
||||
disabled: !canBuildTransport || !params.selected,
|
||||
},
|
||||
{
|
||||
...buildMenuElement,
|
||||
disabled: params.game.inSpawnPhase(),
|
||||
},
|
||||
{
|
||||
...infoMenuElement,
|
||||
disabled: !params.game.hasOwner(params.tile),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function createRadialMenuItems(
|
||||
params: MenuElementParams,
|
||||
): MenuElement[] {
|
||||
const elements = createMenuItems(params);
|
||||
return prepareMenuElementsForRadialMenu(elements, params);
|
||||
}
|
||||
|
||||
export function getRootMenuItems(): MenuElement[] {
|
||||
return [
|
||||
{
|
||||
id: Slot.Boat,
|
||||
name: "boat",
|
||||
disabled: true,
|
||||
_action: () => {},
|
||||
icon: boatIcon,
|
||||
},
|
||||
{
|
||||
id: Slot.Build,
|
||||
name: "build",
|
||||
disabled: true,
|
||||
_action: () => {},
|
||||
icon: buildIcon,
|
||||
},
|
||||
{
|
||||
id: Slot.Info,
|
||||
name: "info",
|
||||
disabled: true,
|
||||
_action: () => {},
|
||||
icon: infoIcon,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function updateCenterButton(
|
||||
params: MenuElementParams,
|
||||
enableCenterButton: (enabled: boolean, action?: (() => void) | null) => void,
|
||||
) {
|
||||
if (params.playerActions.canAttack) {
|
||||
enableCenterButton(true, () => {
|
||||
if (params.tileOwner !== params.myPlayer) {
|
||||
params.playerActionHandler.handleAttack(
|
||||
params.myPlayer,
|
||||
params.tileOwner.id(),
|
||||
);
|
||||
}
|
||||
params.closeMenu();
|
||||
});
|
||||
} else {
|
||||
enableCenterButton(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user