import { Config } from "../../../core/configuration/Config"; import { AllPlayers, PlayerActions, UnitType } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; import { GameView, PlayerView } from "../../../core/game/GameView"; import { Emoji, 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 swordIcon from "../../../../resources/images/SwordIconWhite.svg"; 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"; export interface MenuElementParams { myPlayer: PlayerView; selected: PlayerView | null; tile: TileRef; playerActions: PlayerActions; game: GameView; buildMenu: BuildMenu; emojiTable: EmojiTable; playerActionHandler: PlayerActionHandler; playerPanel: PlayerPanel; chatIntegration: ChatIntegration; eventBus: EventBus; closeMenu: () => void; } export interface MenuElement { id: string; name: string; displayed?: boolean | ((params: MenuElementParams) => boolean); color?: string; icon?: string; text?: string; fontSize?: string; tooltipItems?: TooltipItem[]; tooltipKeys?: TooltipKey[]; cooldown?: (params: MenuElementParams) => number; disabled: (params: MenuElementParams) => boolean; action?: (params: MenuElementParams) => void; // For leaf items that perform actions subMenu?: (params: MenuElementParams) => MenuElement[]; // For non-leaf items that open submenus } export interface TooltipKey { key: string; className: string; params?: Record; } export interface CenterButtonElement { disabled: (params: MenuElementParams) => boolean; action: (params: MenuElementParams) => void; } export const COLORS = { build: "#ebe250", building: "#2c2c2c", boat: "#3f6ab1", ally: "#53ac75", breakAlly: "#c74848", delete: "#ff0000", info: "#64748B", target: "#ff0000", attack: "#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", Attack = "attack", Ally = "ally", Back = "back", Delete = "delete", } // eslint-disable-next-line @typescript-eslint/no-unused-vars const infoChatElement: MenuElement = { 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, })), }; // eslint-disable-next-line @typescript-eslint/no-unused-vars const allyTargetElement: MenuElement = { id: "ally_target", name: "target", disabled: (params: MenuElementParams): boolean => { if (params.selected === null) return true; return !params.playerActions.interaction?.canTarget; }, color: COLORS.target, icon: targetIcon, action: (params: MenuElementParams) => { params.playerActionHandler.handleTargetPlayer(params.selected!.id()); params.closeMenu(); }, }; // eslint-disable-next-line @typescript-eslint/no-unused-vars const allyTradeElement: MenuElement = { id: "ally_trade", name: "trade", disabled: (params: MenuElementParams) => !!params.playerActions?.interaction?.canEmbargo, displayed: (params: MenuElementParams) => !params.playerActions?.interaction?.canEmbargo, color: COLORS.trade, text: translateText("player_panel.start_trade"), action: (params: MenuElementParams) => { params.playerActionHandler.handleEmbargo(params.selected!, "stop"); params.closeMenu(); }, }; // eslint-disable-next-line @typescript-eslint/no-unused-vars const allyEmbargoElement: MenuElement = { id: "ally_embargo", name: "embargo", disabled: (params: MenuElementParams) => !params.playerActions?.interaction?.canEmbargo, displayed: (params: MenuElementParams) => !!params.playerActions?.interaction?.canEmbargo, color: COLORS.embargo, text: translateText("player_panel.stop_trade"), action: (params: MenuElementParams) => { params.playerActionHandler.handleEmbargo(params.selected!, "start"); params.closeMenu(); }, }; const allyRequestElement: MenuElement = { id: "ally_request", name: "request", disabled: (params: MenuElementParams) => !params.playerActions?.interaction?.canSendAllianceRequest, displayed: (params: MenuElementParams) => !params.playerActions?.interaction?.canBreakAlliance, color: COLORS.ally, icon: allianceIcon, action: (params: MenuElementParams) => { params.playerActionHandler.handleAllianceRequest( params.myPlayer, params.selected!, ); params.closeMenu(); }, }; const allyBreakElement: MenuElement = { id: "ally_break", name: "break", disabled: (params: MenuElementParams) => !params.playerActions?.interaction?.canBreakAlliance, displayed: (params: MenuElementParams) => !!params.playerActions?.interaction?.canBreakAlliance, color: COLORS.breakAlly, icon: traitorIcon, action: (params: MenuElementParams) => { params.playerActionHandler.handleBreakAlliance( params.myPlayer, params.selected!, ); params.closeMenu(); }, }; // eslint-disable-next-line @typescript-eslint/no-unused-vars const allyDonateGoldElement: MenuElement = { id: "ally_donate_gold", name: "donate gold", disabled: (params: MenuElementParams) => !params.playerActions?.interaction?.canDonateGold, color: COLORS.ally, icon: donateGoldIcon, action: (params: MenuElementParams) => { params.playerActionHandler.handleDonateGold(params.selected!); params.closeMenu(); }, }; // eslint-disable-next-line @typescript-eslint/no-unused-vars const allyDonateTroopsElement: MenuElement = { id: "ally_donate_troops", name: "donate troops", disabled: (params: MenuElementParams) => !params.playerActions?.interaction?.canDonateTroops, color: COLORS.ally, icon: donateTroopIcon, action: (params: MenuElementParams) => { params.playerActionHandler.handleDonateTroops(params.selected!); params.closeMenu(); }, }; // eslint-disable-next-line @typescript-eslint/no-unused-vars const infoPlayerElement: MenuElement = { id: "info_player", name: "player", disabled: () => false, color: COLORS.info, icon: infoIcon, action: (params: MenuElementParams) => { params.playerPanel.show(params.playerActions, params.tile); }, }; // eslint-disable-next-line @typescript-eslint/no-unused-vars const infoEmojiElement: MenuElement = { id: "info_emoji", name: "emoji", disabled: () => false, color: COLORS.infoEmoji, icon: emojiIcon, subMenu: (params: MenuElementParams) => { const emojiElements: MenuElement[] = [ { 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 as Emoji), ); params.emojiTable.hideTable(); }); }, }, ]; const emojiCount = 8; 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(); }, }); } return emojiElements; }, }; export const infoMenuElement: MenuElement = { id: Slot.Info, name: "info", disabled: (params: MenuElementParams) => !params.selected || params.game.inSpawnPhase(), icon: infoIcon, color: COLORS.info, action: (params: MenuElementParams) => { params.playerPanel.show(params.playerActions, params.tile); }, }; function getAllEnabledUnits(myPlayer: boolean, config: Config): Set { const Units: Set = new Set(); const addStructureIfEnabled = (unitType: UnitType) => { if (!config.isUnitDisabled(unitType)) { Units.add(unitType); } }; if (myPlayer) { addStructureIfEnabled(UnitType.City); addStructureIfEnabled(UnitType.DefensePost); addStructureIfEnabled(UnitType.Port); addStructureIfEnabled(UnitType.MissileSilo); addStructureIfEnabled(UnitType.SAMLauncher); addStructureIfEnabled(UnitType.Factory); } else { addStructureIfEnabled(UnitType.Warship); addStructureIfEnabled(UnitType.HydrogenBomb); addStructureIfEnabled(UnitType.MIRV); addStructureIfEnabled(UnitType.AtomBomb); } return Units; } const ATTACK_UNIT_TYPES: UnitType[] = [ UnitType.AtomBomb, UnitType.MIRV, UnitType.HydrogenBomb, UnitType.Warship, ]; function createMenuElements( params: MenuElementParams, filterType: "attack" | "build", elementIdPrefix: string, ): MenuElement[] { const unitTypes: Set = getAllEnabledUnits( params.selected === params.myPlayer, params.game.config(), ); return flattenedBuildTable .filter( (item) => unitTypes.has(item.unitType) && (filterType === "attack" ? ATTACK_UNIT_TYPES.includes(item.unitType) : !ATTACK_UNIT_TYPES.includes(item.unitType)), ) .map((item: BuildItemDisplay) => ({ id: `${elementIdPrefix}_${item.unitType}`, name: item.key ? item.key.replace("unit_type.", "") : item.unitType.toString(), disabled: (params: MenuElementParams) => !params.buildMenu.canBuildOrUpgrade(item), color: params.buildMenu.canBuildOrUpgrade(item) ? filterType === "attack" ? COLORS.attack : 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( (tooltipItem): tooltipItem is TooltipItem => tooltipItem !== null, ), action: (params: MenuElementParams) => { const buildableUnit = params.playerActions.buildableUnits.find( (bu) => bu.type === item.unitType, ); if (buildableUnit === undefined) { return; } if (params.buildMenu.canBuildOrUpgrade(item)) { params.buildMenu.sendBuildOrUpgrade(buildableUnit, params.tile); } params.closeMenu(); }, })); } export const attackMenuElement: MenuElement = { id: Slot.Attack, name: "radial_attack", disabled: (params: MenuElementParams) => params.game.inSpawnPhase(), icon: swordIcon, color: COLORS.attack, subMenu: (params: MenuElementParams) => { if (params === undefined) return []; return createMenuElements(params, "attack", "attack"); }, }; export const deleteUnitElement: MenuElement = { id: Slot.Delete, name: "delete", cooldown: (params: MenuElementParams) => params.myPlayer.deleteUnitCooldown(), disabled: (params: MenuElementParams) => { const tileOwner = params.game.owner(params.tile); const isLand = params.game.isLand(params.tile); if (!tileOwner.isPlayer() || tileOwner.id() !== params.myPlayer.id()) { return true; } if (!isLand) { return true; } if (params.game.inSpawnPhase()) { return true; } if (params.myPlayer.deleteUnitCooldown() > 0) { return true; } const DELETE_SELECTION_RADIUS = 5; const myUnits = params.myPlayer .units() .filter( (unit) => !unit.isUnderConstruction() && unit.markedForDeletion() === false && params.game.manhattanDist(unit.tile(), params.tile) <= DELETE_SELECTION_RADIUS, ); return myUnits.length === 0; }, icon: xIcon, color: COLORS.delete, tooltipKeys: [ { key: "radial_menu.delete_unit_title", className: "title", }, { key: "radial_menu.delete_unit_description", className: "description", }, ], action: (params: MenuElementParams) => { const DELETE_SELECTION_RADIUS = 5; const myUnits = params.myPlayer .units() .filter( (unit) => params.game.manhattanDist(unit.tile(), params.tile) <= DELETE_SELECTION_RADIUS, ); if (myUnits.length > 0) { myUnits.sort( (a, b) => params.game.manhattanDist(a.tile(), params.tile) - params.game.manhattanDist(b.tile(), params.tile), ); params.playerActionHandler.handleDeleteUnit(myUnits[0].id()); } params.closeMenu(); }, }; export const buildMenuElement: MenuElement = { id: Slot.Build, name: "build", disabled: (params: MenuElementParams) => params.game.inSpawnPhase(), icon: buildIcon, color: COLORS.build, subMenu: (params: MenuElementParams) => { if (params === undefined) return []; return createMenuElements(params, "build", "build"); }, }; export const boatMenuElement: MenuElement = { id: Slot.Boat, name: "boat", disabled: (params: MenuElementParams) => !params.playerActions.buildableUnits.some( (unit) => unit.type === UnitType.TransportShip && unit.canBuild, ), icon: boatIcon, color: COLORS.boat, action: async (params: MenuElementParams) => { const spawn = await params.playerActionHandler.findBestTransportShipSpawn( params.myPlayer, params.tile, ); params.playerActionHandler.handleBoatAttack( params.myPlayer, params.selected?.id() ?? null, params.tile, spawn !== false ? spawn : null, ); params.closeMenu(); }, }; export const centerButtonElement: CenterButtonElement = { disabled: (params: MenuElementParams): boolean => { const tileOwner = params.game.owner(params.tile); const isLand = params.game.isLand(params.tile); if (!isLand) { return true; } if (params.game.inSpawnPhase()) { if (tileOwner.isPlayer()) { return true; } return false; } return !params.playerActions.canAttack; }, action: (params: MenuElementParams) => { if (params.game.inSpawnPhase()) { params.playerActionHandler.handleSpawn(params.tile); } else { params.playerActionHandler.handleAttack( params.myPlayer, params.selected?.id() ?? null, ); } params.closeMenu(); }, }; export const rootMenuElement: MenuElement = { id: "root", name: "root", disabled: () => false, icon: infoIcon, color: COLORS.info, subMenu: (params: MenuElementParams) => { let ally = allyRequestElement; if (params.selected?.isAlliedWith(params.myPlayer)) { ally = allyBreakElement; } const tileOwner = params.game.owner(params.tile); const isOwnTerritory = tileOwner.isPlayer() && (tileOwner as PlayerView).id() === params.myPlayer.id(); const menuItems: (MenuElement | null)[] = [ infoMenuElement, ...(isOwnTerritory ? [deleteUnitElement, ally, buildMenuElement] : [boatMenuElement, ally, attackMenuElement]), ]; return menuItems.filter((item): item is MenuElement => item !== null); }, };