mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:00:43 +00:00
Add Split radial menu into separate attack and build buttons (#1598)
## Description: This PR implements a new radial menu system that separates attack and construction functionalities into distinct buttons. Previously, all units (both attack and construction) were grouped together in a single orange button, making the interface confusing and inefficient. ## 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 have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: Kipstzz <img width="420" height="345" alt="image" src="https://github.com/user-attachments/assets/73d67fe1-d5d2-4c7e-8894-360877fa7004" /> <img width="422" height="345" alt="image" src="https://github.com/user-attachments/assets/e576d543-4156-48f4-81ac-e7a06d26b25b" />
This commit is contained in:
@@ -70,6 +70,7 @@
|
||||
"radial_title": "Radial menu",
|
||||
"radial_desc": "Right clicking (or touch on mobile) opens the Radial menu. Right click outside it to close it. From the menu you can:",
|
||||
"radial_build": "Open the Build menu.",
|
||||
"radial_attack": "Open the Attack menu.",
|
||||
"radial_info": "Open the Info menu.",
|
||||
"radial_boat": "Send a Boat (transport ship) to attack at the selected location. Only available if you have access to water.",
|
||||
"radial_close": "Close the menu.",
|
||||
|
||||
@@ -19,6 +19,7 @@ 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 { EventBus } from "../../../core/EventBus";
|
||||
@@ -66,6 +67,7 @@ export const COLORS = {
|
||||
breakAlly: "#c74848",
|
||||
info: "#64748B",
|
||||
target: "#ff0000",
|
||||
attack: "#ff0000",
|
||||
infoDetails: "#7f8c8d",
|
||||
infoEmoji: "#f1c40f",
|
||||
trade: "#008080",
|
||||
@@ -89,6 +91,7 @@ export enum Slot {
|
||||
Info = "info",
|
||||
Boat = "boat",
|
||||
Build = "build",
|
||||
Attack = "attack",
|
||||
Ally = "ally",
|
||||
Back = "back",
|
||||
}
|
||||
@@ -319,6 +322,88 @@ function getAllEnabledUnits(myPlayer: boolean, config: Config): Set<UnitType> {
|
||||
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<UnitType> = 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 buildMenuElement: MenuElement = {
|
||||
id: Slot.Build,
|
||||
name: "build",
|
||||
@@ -328,53 +413,7 @@ export const buildMenuElement: MenuElement = {
|
||||
|
||||
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;
|
||||
return createMenuElements(params, "build", "build");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -444,6 +483,24 @@ export const rootMenuElement: MenuElement = {
|
||||
if (params.selected?.isAlliedWith(params.myPlayer)) {
|
||||
ally = allyBreakElement;
|
||||
}
|
||||
return [infoMenuElement, boatMenuElement, ally, buildMenuElement];
|
||||
|
||||
const tileOwner = params.game.owner(params.tile);
|
||||
const isOwnTerritory =
|
||||
tileOwner.isPlayer() &&
|
||||
(tileOwner as PlayerView).id() === params.myPlayer.id();
|
||||
|
||||
const menuItems: (MenuElement | null)[] = [
|
||||
infoMenuElement,
|
||||
boatMenuElement,
|
||||
ally,
|
||||
];
|
||||
|
||||
if (isOwnTerritory) {
|
||||
menuItems.push(buildMenuElement);
|
||||
} else {
|
||||
menuItems.push(attackMenuElement);
|
||||
}
|
||||
|
||||
return menuItems.filter((item): item is MenuElement => item !== null);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import {
|
||||
attackMenuElement,
|
||||
buildMenuElement,
|
||||
COLORS,
|
||||
MenuElementParams,
|
||||
rootMenuElement,
|
||||
Slot,
|
||||
} from "../../../src/client/graphics/layers/RadialMenuElements";
|
||||
import { UnitType } from "../../../src/core/game/Game";
|
||||
import { TileRef } from "../../../src/core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../src/core/game/GameView";
|
||||
|
||||
jest.mock("../../../src/client/Utils", () => ({
|
||||
translateText: jest.fn((key: string) => key),
|
||||
renderNumber: jest.fn((num: number) => num.toString()),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/client/graphics/layers/BuildMenu", () => {
|
||||
const { UnitType } = jest.requireActual("../../../src/core/game/Game");
|
||||
return {
|
||||
flattenedBuildTable: [
|
||||
{
|
||||
unitType: UnitType.City,
|
||||
key: "unit_type.city",
|
||||
description: "unit_type.city_desc",
|
||||
icon: "city-icon",
|
||||
countable: true,
|
||||
},
|
||||
{
|
||||
unitType: UnitType.Factory,
|
||||
key: "unit_type.factory",
|
||||
description: "unit_type.factory_desc",
|
||||
icon: "factory-icon",
|
||||
countable: true,
|
||||
},
|
||||
{
|
||||
unitType: UnitType.AtomBomb,
|
||||
key: "unit_type.atom_bomb",
|
||||
description: "unit_type.atom_bomb_desc",
|
||||
icon: "atom-bomb-icon",
|
||||
countable: false,
|
||||
},
|
||||
{
|
||||
unitType: UnitType.Warship,
|
||||
key: "unit_type.warship",
|
||||
description: "unit_type.warship_desc",
|
||||
icon: "warship-icon",
|
||||
countable: true,
|
||||
},
|
||||
{
|
||||
unitType: UnitType.HydrogenBomb,
|
||||
key: "unit_type.hydrogen_bomb",
|
||||
description: "unit_type.hydrogen_bomb_desc",
|
||||
icon: "hydrogen-bomb-icon",
|
||||
countable: false,
|
||||
},
|
||||
{
|
||||
unitType: UnitType.MIRV,
|
||||
key: "unit_type.mirv",
|
||||
description: "unit_type.mirv_desc",
|
||||
icon: "mirv-icon",
|
||||
countable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("nanoid", () => ({
|
||||
customAlphabet: jest.fn(() => jest.fn(() => "mock-id")),
|
||||
}));
|
||||
|
||||
jest.mock("dompurify", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
sanitize: jest.fn((str: string) => str),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("twemoji", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
parse: jest.fn((str: string) => str),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("RadialMenuElements", () => {
|
||||
let mockParams: MenuElementParams;
|
||||
let mockPlayer: PlayerView;
|
||||
let mockGame: GameView;
|
||||
let mockBuildMenu: any;
|
||||
let mockPlayerActions: any;
|
||||
let mockTile: TileRef;
|
||||
|
||||
beforeEach(() => {
|
||||
mockPlayer = {
|
||||
id: () => 1,
|
||||
isAlliedWith: jest.fn(() => false),
|
||||
isPlayer: jest.fn(() => true),
|
||||
} as unknown as PlayerView;
|
||||
|
||||
mockGame = {
|
||||
inSpawnPhase: jest.fn(() => false),
|
||||
owner: jest.fn(() => mockPlayer),
|
||||
isLand: jest.fn(() => true),
|
||||
config: jest.fn(() => ({
|
||||
theme: () => ({
|
||||
territoryColor: () => ({
|
||||
lighten: () => ({ alpha: () => ({ toRgbString: () => "#fff" }) }),
|
||||
}),
|
||||
}),
|
||||
isUnitDisabled: jest.fn(() => false),
|
||||
})),
|
||||
} as unknown as GameView;
|
||||
|
||||
mockBuildMenu = {
|
||||
canBuildOrUpgrade: jest.fn(() => true),
|
||||
cost: jest.fn(() => 100),
|
||||
count: jest.fn(() => 5),
|
||||
sendBuildOrUpgrade: jest.fn(),
|
||||
};
|
||||
|
||||
mockPlayerActions = {
|
||||
buildableUnits: [
|
||||
{ type: UnitType.City, canBuild: true },
|
||||
{ type: UnitType.Factory, canBuild: true },
|
||||
{ type: UnitType.AtomBomb, canBuild: true },
|
||||
{ type: UnitType.Warship, canBuild: true },
|
||||
{ type: UnitType.HydrogenBomb, canBuild: true },
|
||||
{ type: UnitType.MIRV, canBuild: true },
|
||||
{ type: UnitType.TransportShip, canBuild: true },
|
||||
],
|
||||
canAttack: true,
|
||||
interaction: {
|
||||
canSendAllianceRequest: true,
|
||||
canBreakAlliance: false,
|
||||
canDonate: true,
|
||||
},
|
||||
};
|
||||
|
||||
mockTile = {} as TileRef;
|
||||
|
||||
mockParams = {
|
||||
myPlayer: mockPlayer,
|
||||
selected: mockPlayer,
|
||||
tile: mockTile,
|
||||
playerActions: mockPlayerActions,
|
||||
game: mockGame,
|
||||
buildMenu: mockBuildMenu,
|
||||
emojiTable: {} as any,
|
||||
playerActionHandler: {} as any,
|
||||
playerPanel: {} as any,
|
||||
chatIntegration: {} as any,
|
||||
eventBus: {} as any,
|
||||
closeMenu: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("attackMenuElement", () => {
|
||||
it("should have correct basic properties", () => {
|
||||
expect(attackMenuElement.id).toBe(Slot.Attack);
|
||||
expect(attackMenuElement.name).toBe("radial_attack");
|
||||
expect(attackMenuElement.icon).toBeDefined();
|
||||
expect(attackMenuElement.color).toBe(COLORS.attack);
|
||||
});
|
||||
|
||||
it("should be disabled during spawn phase", () => {
|
||||
mockGame.inSpawnPhase = jest.fn(() => true);
|
||||
expect(attackMenuElement.disabled(mockParams)).toBe(true);
|
||||
});
|
||||
|
||||
it("should be enabled when not in spawn phase", () => {
|
||||
mockGame.inSpawnPhase = jest.fn(() => false);
|
||||
expect(attackMenuElement.disabled(mockParams)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return attack submenu with attack units only", () => {
|
||||
const enemyPlayer = {
|
||||
id: () => 2,
|
||||
isPlayer: jest.fn(() => true),
|
||||
} as unknown as PlayerView;
|
||||
mockParams.selected = enemyPlayer;
|
||||
|
||||
const subMenu = attackMenuElement.subMenu!(mockParams);
|
||||
|
||||
expect(subMenu).toBeDefined();
|
||||
expect(subMenu.length).toBeGreaterThan(0);
|
||||
|
||||
const attackUnitTypes = [
|
||||
UnitType.AtomBomb,
|
||||
UnitType.MIRV,
|
||||
UnitType.HydrogenBomb,
|
||||
UnitType.Warship,
|
||||
];
|
||||
const returnedUnitTypes = subMenu.map((item) => {
|
||||
const unitTypeStr = item.id.replace("attack_", "");
|
||||
return Object.values(UnitType).find(
|
||||
(type) => type.toString() === unitTypeStr,
|
||||
);
|
||||
});
|
||||
|
||||
returnedUnitTypes.forEach((unitType) => {
|
||||
expect(attackUnitTypes).toContain(unitType);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not include construction units in attack menu", () => {
|
||||
const enemyPlayer = {
|
||||
id: () => 2,
|
||||
isPlayer: jest.fn(() => true),
|
||||
} as unknown as PlayerView;
|
||||
mockParams.selected = enemyPlayer;
|
||||
|
||||
const subMenu = attackMenuElement.subMenu!(mockParams);
|
||||
|
||||
const constructionUnitTypes = [UnitType.City, UnitType.Factory];
|
||||
const returnedUnitTypes = subMenu.map((item) => {
|
||||
const unitTypeStr = item.id.replace("attack_", "");
|
||||
return Object.values(UnitType).find(
|
||||
(type) => type.toString() === unitTypeStr,
|
||||
);
|
||||
});
|
||||
|
||||
constructionUnitTypes.forEach((unitType) => {
|
||||
expect(returnedUnitTypes).not.toContain(unitType);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle undefined params in submenu", () => {
|
||||
const subMenu = attackMenuElement.subMenu!(undefined as any);
|
||||
expect(subMenu).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildMenuElement", () => {
|
||||
it("should have correct basic properties", () => {
|
||||
expect(buildMenuElement.id).toBe(Slot.Build);
|
||||
expect(buildMenuElement.name).toBe("build");
|
||||
expect(buildMenuElement.icon).toBeDefined();
|
||||
expect(buildMenuElement.color).toBe(COLORS.build);
|
||||
});
|
||||
|
||||
it("should be disabled during spawn phase", () => {
|
||||
mockGame.inSpawnPhase = jest.fn(() => true);
|
||||
expect(buildMenuElement.disabled(mockParams)).toBe(true);
|
||||
});
|
||||
|
||||
it("should be enabled when not in spawn phase", () => {
|
||||
mockGame.inSpawnPhase = jest.fn(() => false);
|
||||
expect(buildMenuElement.disabled(mockParams)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return build submenu with construction units only", () => {
|
||||
const subMenu = buildMenuElement.subMenu!(mockParams);
|
||||
|
||||
expect(subMenu).toBeDefined();
|
||||
expect(subMenu.length).toBeGreaterThan(0);
|
||||
|
||||
const constructionUnitTypes = [UnitType.City, UnitType.Factory];
|
||||
const returnedUnitTypes = subMenu.map((item) => {
|
||||
const unitTypeStr = item.id.replace("build_", "");
|
||||
return Object.values(UnitType).find(
|
||||
(type) => type.toString() === unitTypeStr,
|
||||
);
|
||||
});
|
||||
|
||||
returnedUnitTypes.forEach((unitType) => {
|
||||
expect(constructionUnitTypes).toContain(unitType);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not include attack units in build menu", () => {
|
||||
const subMenu = buildMenuElement.subMenu!(mockParams);
|
||||
|
||||
const attackUnitTypes = [
|
||||
UnitType.AtomBomb,
|
||||
UnitType.MIRV,
|
||||
UnitType.HydrogenBomb,
|
||||
UnitType.Warship,
|
||||
];
|
||||
const returnedUnitTypes = subMenu.map((item) => {
|
||||
const unitTypeStr = item.id.replace("build_", "");
|
||||
return Object.values(UnitType).find(
|
||||
(type) => type.toString() === unitTypeStr,
|
||||
);
|
||||
});
|
||||
|
||||
attackUnitTypes.forEach((unitType) => {
|
||||
expect(returnedUnitTypes).not.toContain(unitType);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle undefined params in submenu", () => {
|
||||
const subMenu = buildMenuElement.subMenu!(undefined as any);
|
||||
expect(subMenu).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rootMenuElement", () => {
|
||||
it("should have correct basic properties", () => {
|
||||
expect(rootMenuElement.id).toBe("root");
|
||||
expect(rootMenuElement.name).toBe("root");
|
||||
expect(rootMenuElement.disabled(mockParams)).toBe(false);
|
||||
});
|
||||
|
||||
it("should show build menu on own territory", () => {
|
||||
const subMenu = rootMenuElement.subMenu!(mockParams);
|
||||
const buildMenu = subMenu.find((item) => item.id === Slot.Build);
|
||||
const attackMenu = subMenu.find((item) => item.id === Slot.Attack);
|
||||
|
||||
expect(buildMenu).toBeDefined();
|
||||
expect(attackMenu).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should show attack menu on enemy territory", () => {
|
||||
const enemyPlayer = {
|
||||
id: () => 2,
|
||||
isPlayer: jest.fn(() => true),
|
||||
} as unknown as PlayerView;
|
||||
mockGame.owner = jest.fn(() => enemyPlayer);
|
||||
|
||||
const subMenu = rootMenuElement.subMenu!(mockParams);
|
||||
const buildMenu = subMenu.find((item) => item.id === Slot.Build);
|
||||
const attackMenu = subMenu.find((item) => item.id === Slot.Attack);
|
||||
|
||||
expect(attackMenu).toBeDefined();
|
||||
expect(buildMenu).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should include info and boat menus in both cases", () => {
|
||||
const subMenu = rootMenuElement.subMenu!(mockParams);
|
||||
const infoMenu = subMenu.find((item) => item.id === Slot.Info);
|
||||
const boatMenu = subMenu.find((item) => item.id === Slot.Boat);
|
||||
|
||||
expect(infoMenu).toBeDefined();
|
||||
expect(boatMenu).toBeDefined();
|
||||
});
|
||||
|
||||
it("should handle ally menu correctly", () => {
|
||||
const allyPlayer = {
|
||||
id: () => 2,
|
||||
isAlliedWith: jest.fn(() => true),
|
||||
isPlayer: jest.fn(() => true),
|
||||
} as unknown as PlayerView;
|
||||
mockParams.selected = allyPlayer;
|
||||
|
||||
const subMenu = rootMenuElement.subMenu!(mockParams);
|
||||
const allyMenu = subMenu.find((item) => item.id === "ally_break");
|
||||
|
||||
expect(allyMenu).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Menu element actions", () => {
|
||||
it("should execute build action correctly", () => {
|
||||
const subMenu = buildMenuElement.subMenu!(mockParams);
|
||||
const cityElement = subMenu.find((item) => item.id === "build_City");
|
||||
|
||||
expect(cityElement).toBeDefined();
|
||||
expect(cityElement!.action).toBeDefined();
|
||||
|
||||
if (cityElement!.action) {
|
||||
cityElement!.action(mockParams);
|
||||
expect(mockBuildMenu.sendBuildOrUpgrade).toHaveBeenCalled();
|
||||
expect(mockParams.closeMenu).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("should execute attack action correctly", () => {
|
||||
const enemyPlayer = {
|
||||
id: () => 2,
|
||||
isPlayer: jest.fn(() => true),
|
||||
} as unknown as PlayerView;
|
||||
mockParams.selected = enemyPlayer;
|
||||
|
||||
const subMenu = attackMenuElement.subMenu!(mockParams);
|
||||
|
||||
const atomBombElement = subMenu.find(
|
||||
(item) => item.id === "attack_Atom Bomb",
|
||||
);
|
||||
|
||||
expect(atomBombElement).toBeDefined();
|
||||
expect(atomBombElement!.action).toBeDefined();
|
||||
|
||||
if (atomBombElement!.action) {
|
||||
atomBombElement!.action(mockParams);
|
||||
expect(mockBuildMenu.sendBuildOrUpgrade).toHaveBeenCalled();
|
||||
expect(mockParams.closeMenu).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("should not execute action when buildable unit is not found", () => {
|
||||
mockPlayerActions.buildableUnits = [];
|
||||
mockBuildMenu.canBuildOrUpgrade = jest.fn(() => false);
|
||||
|
||||
const subMenu = buildMenuElement.subMenu!(mockParams);
|
||||
const cityElement = subMenu.find((item) => item.id === "build_City");
|
||||
|
||||
if (cityElement!.action) {
|
||||
cityElement!.action(mockParams);
|
||||
expect(mockBuildMenu.sendBuildOrUpgrade).not.toHaveBeenCalled();
|
||||
expect(mockParams.closeMenu).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Menu element tooltips", () => {
|
||||
it("should generate correct tooltip items for build elements", () => {
|
||||
const subMenu = buildMenuElement.subMenu!(mockParams);
|
||||
const cityElement = subMenu.find((item) => item.id === "build_City");
|
||||
|
||||
expect(cityElement!.tooltipItems).toBeDefined();
|
||||
expect(cityElement!.tooltipItems!.length).toBeGreaterThan(0);
|
||||
|
||||
const tooltipTexts = cityElement!.tooltipItems!.map((item) => item.text);
|
||||
expect(tooltipTexts).toContain("unit_type.city");
|
||||
expect(tooltipTexts).toContain("unit_type.city_desc");
|
||||
expect(tooltipTexts.some((text) => text.includes("100"))).toBe(true);
|
||||
expect(tooltipTexts.some((text) => text.includes("5x"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should generate correct tooltip items for attack elements", () => {
|
||||
const enemyPlayer = {
|
||||
id: () => 2,
|
||||
isPlayer: jest.fn(() => true),
|
||||
} as unknown as PlayerView;
|
||||
mockParams.selected = enemyPlayer;
|
||||
|
||||
const subMenu = attackMenuElement.subMenu!(mockParams);
|
||||
const atomBombElement = subMenu.find(
|
||||
(item) => item.id === "attack_Atom Bomb",
|
||||
);
|
||||
|
||||
expect(atomBombElement!.tooltipItems).toBeDefined();
|
||||
expect(atomBombElement!.tooltipItems!.length).toBeGreaterThan(0);
|
||||
|
||||
const tooltipTexts = atomBombElement!.tooltipItems!.map(
|
||||
(item) => item.text,
|
||||
);
|
||||
expect(tooltipTexts).toContain("unit_type.atom_bomb");
|
||||
expect(tooltipTexts).toContain("unit_type.atom_bomb_desc");
|
||||
expect(tooltipTexts.some((text) => text.includes("100"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Menu element colors", () => {
|
||||
it("should use correct colors for build elements", () => {
|
||||
const subMenu = buildMenuElement.subMenu!(mockParams);
|
||||
const cityElement = subMenu.find((item) => item.id === "build_City");
|
||||
|
||||
expect(cityElement!.color).toBe(COLORS.building);
|
||||
});
|
||||
|
||||
it("should use correct colors for attack elements", () => {
|
||||
const enemyPlayer = {
|
||||
id: () => 2,
|
||||
isPlayer: jest.fn(() => true),
|
||||
} as unknown as PlayerView;
|
||||
mockParams.selected = enemyPlayer;
|
||||
|
||||
const subMenu = attackMenuElement.subMenu!(mockParams);
|
||||
const atomBombElement = subMenu.find(
|
||||
(item) => item.id === "attack_Atom Bomb",
|
||||
);
|
||||
|
||||
expect(atomBombElement!.color).toBe(COLORS.attack);
|
||||
});
|
||||
|
||||
it("should not set color when element is disabled", () => {
|
||||
mockBuildMenu.canBuildOrUpgrade = jest.fn(() => false);
|
||||
|
||||
const subMenu = buildMenuElement.subMenu!(mockParams);
|
||||
const cityElement = subMenu.find((item) => item.id === "build_City");
|
||||
|
||||
expect(cityElement!.color).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Translation integration", () => {
|
||||
it("should use translateText for tooltip items in build menu", () => {
|
||||
const { translateText } = jest.requireMock("../../../src/client/Utils");
|
||||
|
||||
(translateText as jest.Mock).mockClear();
|
||||
|
||||
buildMenuElement.subMenu!(mockParams);
|
||||
|
||||
expect(translateText).toHaveBeenCalledWith("unit_type.city");
|
||||
expect(translateText).toHaveBeenCalledWith("unit_type.city_desc");
|
||||
expect(translateText).toHaveBeenCalledWith("unit_type.factory");
|
||||
expect(translateText).toHaveBeenCalledWith("unit_type.factory_desc");
|
||||
});
|
||||
|
||||
it("should use translateText for tooltip items in attack menu", () => {
|
||||
const { translateText } = jest.requireMock("../../../src/client/Utils");
|
||||
|
||||
(translateText as jest.Mock).mockClear();
|
||||
|
||||
const enemyPlayer = {
|
||||
id: () => 2,
|
||||
isPlayer: jest.fn(() => true),
|
||||
} as unknown as PlayerView;
|
||||
mockParams.selected = enemyPlayer;
|
||||
|
||||
attackMenuElement.subMenu!(mockParams);
|
||||
|
||||
expect(translateText).toHaveBeenCalledWith("unit_type.atom_bomb");
|
||||
expect(translateText).toHaveBeenCalledWith("unit_type.atom_bomb_desc");
|
||||
expect(translateText).toHaveBeenCalledWith("unit_type.hydrogen_bomb");
|
||||
expect(translateText).toHaveBeenCalledWith(
|
||||
"unit_type.hydrogen_bomb_desc",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user