mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:20:46 +00:00
513fcb0944
## Description: When building a structure in the same location as a nearby structure, it will update the existing structure instead of creating a new one. Also fix ctrl+click shortcut to bring up the build menu. ## 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: evan
477 lines
14 KiB
TypeScript
477 lines
14 KiB
TypeScript
import { Config } from "../../../core/configuration/Config";
|
|
import {
|
|
AllPlayers,
|
|
Cell,
|
|
PlayerActions,
|
|
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";
|
|
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[];
|
|
|
|
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 CenterButtonElement {
|
|
disabled: (params: MenuElementParams) => boolean;
|
|
action: (params: MenuElementParams) => 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",
|
|
}
|
|
|
|
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,
|
|
})),
|
|
};
|
|
|
|
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();
|
|
},
|
|
};
|
|
|
|
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();
|
|
},
|
|
};
|
|
|
|
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();
|
|
},
|
|
};
|
|
|
|
const allyDonateGoldElement: MenuElement = {
|
|
id: "ally_donate_gold",
|
|
name: "donate gold",
|
|
disabled: (params: MenuElementParams) =>
|
|
!params.playerActions?.interaction?.canDonate,
|
|
color: COLORS.ally,
|
|
icon: donateGoldIcon,
|
|
action: (params: MenuElementParams) => {
|
|
params.playerActionHandler.handleDonateGold(params.selected!);
|
|
params.closeMenu();
|
|
},
|
|
};
|
|
|
|
const allyDonateTroopsElement: MenuElement = {
|
|
id: "ally_donate_troops",
|
|
name: "donate troops",
|
|
disabled: (params: MenuElementParams) =>
|
|
!params.playerActions?.interaction?.canDonate,
|
|
color: COLORS.ally,
|
|
icon: donateTroopIcon,
|
|
action: (params: MenuElementParams) => {
|
|
params.playerActionHandler.handleDonateTroops(params.selected!);
|
|
params.closeMenu();
|
|
},
|
|
};
|
|
|
|
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);
|
|
},
|
|
};
|
|
|
|
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),
|
|
);
|
|
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,
|
|
|
|
subMenu: (params: MenuElementParams) => {
|
|
if (!params.selected || params.game.inSpawnPhase()) return [];
|
|
|
|
if (params.selected === params.myPlayer) {
|
|
return [infoPlayerElement, infoEmojiElement];
|
|
}
|
|
|
|
const elements: MenuElement[] = [
|
|
infoPlayerElement,
|
|
infoEmojiElement,
|
|
infoChatElement,
|
|
];
|
|
if (params.myPlayer.isAlliedWith(params.selected)) {
|
|
elements.push(
|
|
allyBreakElement,
|
|
allyDonateGoldElement,
|
|
allyDonateTroopsElement,
|
|
);
|
|
} else {
|
|
elements.push(allyTargetElement, allyRequestElement);
|
|
}
|
|
if (params.myPlayer.hasEmbargoAgainst(params.selected)) {
|
|
elements.push(allyTradeElement);
|
|
} else {
|
|
elements.push(allyEmbargoElement);
|
|
}
|
|
|
|
return elements;
|
|
},
|
|
};
|
|
|
|
function getAllEnabledUnits(myPlayer: boolean, config: Config): Set<UnitType> {
|
|
const Units: Set<UnitType> = new Set<UnitType>();
|
|
|
|
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;
|
|
}
|
|
|
|
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 [];
|
|
|
|
const unitTypes: Set<UnitType> = getAllEnabledUnits(
|
|
params.selected === params.myPlayer,
|
|
params.game.config(),
|
|
);
|
|
const buildElements: MenuElement[] = flattenedBuildTable
|
|
.filter((item) => unitTypes.has(item.unitType))
|
|
.map((item: BuildItemDisplay) => ({
|
|
id: `build_${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)
|
|
? 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) => {
|
|
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();
|
|
},
|
|
}));
|
|
|
|
return buildElements;
|
|
},
|
|
};
|
|
|
|
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 false;
|
|
},
|
|
action: (params: MenuElementParams) => {
|
|
if (params.game.inSpawnPhase()) {
|
|
const cell = new Cell(
|
|
params.game.x(params.tile),
|
|
params.game.y(params.tile),
|
|
);
|
|
params.playerActionHandler.handleSpawn(cell);
|
|
} else {
|
|
params.playerActionHandler.handleAttack(
|
|
params.myPlayer,
|
|
params.selected?.id() ?? null,
|
|
);
|
|
}
|
|
params.closeMenu();
|
|
},
|
|
};
|
|
|
|
export const rootMenuItems: MenuElement[] = [
|
|
infoMenuElement,
|
|
boatMenuElement,
|
|
buildMenuElement,
|
|
];
|