diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index ab4d0198f..119c7e1a3 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -11,6 +11,16 @@ import { } from "./RadialMenuElements"; import backIcon from "/images/BackIconWhite.svg?url"; +function resolveColor( + item: MenuElement, + params: MenuElementParams | null, +): string | undefined { + if (typeof item.color === "function") { + return params ? item.color(params) : undefined; + } + return item.color; +} + export class CloseRadialMenuEvent implements GameEvent { constructor() {} } @@ -322,7 +332,7 @@ export class RadialMenu implements Layer { const disabled = this.params === null || d.data.disabled(this.params); const color = disabled ? this.config.disabledColor - : (d.data.color ?? "#333333"); + : (resolveColor(d.data, this.params) ?? "#333333"); const opacity = disabled ? 0.5 : 0.7; if (d.data.id === this.selectedItemId && this.currentLevel > level) { @@ -365,7 +375,7 @@ export class RadialMenu implements Layer { const color = this.params === null || d.data.disabled(this.params) ? this.config.disabledColor - : (d.data.color ?? "#333333"); + : (resolveColor(d.data, this.params) ?? "#333333"); path.attr("fill", color); } }); @@ -431,7 +441,7 @@ export class RadialMenu implements Layer { path.attr("stroke-width", "2"); const color = disabled ? this.config.disabledColor - : (d.data.color ?? "#333333"); + : (resolveColor(d.data, this.params) ?? "#333333"); const opacity = disabled ? 0.5 : 0.7; path.attr( "fill", @@ -848,10 +858,7 @@ export class RadialMenu implements Layer { public disableAllButtons() { this.updateCenterButtonState("default"); - - for (const item of this.currentMenuItems) { - item.color = this.config.disabledColor; - } + this.refresh(); } public updateCenterButtonState(state: CenterButtonState) { @@ -1043,7 +1050,7 @@ export class RadialMenu implements Layer { const disabled = this.isItemDisabled(item); const color = disabled ? this.config.disabledColor - : (item.color ?? "#333333"); + : (resolveColor(item, this.params) ?? "#333333"); const opacity = disabled ? 0.5 : 0.7; // Update path appearance diff --git a/src/client/graphics/layers/RadialMenuElements.ts b/src/client/graphics/layers/RadialMenuElements.ts index 4ced271d0..67ef1d05a 100644 --- a/src/client/graphics/layers/RadialMenuElements.ts +++ b/src/client/graphics/layers/RadialMenuElements.ts @@ -46,7 +46,7 @@ export interface MenuElement { id: string; name: string; displayed?: boolean | ((params: MenuElementParams) => boolean); - color?: string; + color?: string | ((params: MenuElementParams) => string); icon?: string; text?: string; fontSize?: string; @@ -76,6 +76,7 @@ export const COLORS = { boat: "#3f6ab1", ally: "#53ac75", breakAlly: "#c74848", + breakAllyNoDebuff: "#d4882b", delete: "#ff0000", info: "#64748B", target: "#ff0000", @@ -216,7 +217,10 @@ const allyBreakElement: MenuElement = { !params.playerActions?.interaction?.canBreakAlliance, displayed: (params: MenuElementParams) => !!params.playerActions?.interaction?.canBreakAlliance, - color: COLORS.breakAlly, + color: (params: MenuElementParams) => + params.selected?.isTraitor() || params.selected?.isDisconnected() + ? COLORS.breakAllyNoDebuff + : COLORS.breakAlly, icon: traitorIcon, action: (params: MenuElementParams) => { params.playerActionHandler.handleBreakAlliance( diff --git a/tests/client/graphics/RadialMenuElements.test.ts b/tests/client/graphics/RadialMenuElements.test.ts index e1162f4eb..8f645737a 100644 --- a/tests/client/graphics/RadialMenuElements.test.ts +++ b/tests/client/graphics/RadialMenuElements.test.ts @@ -92,6 +92,8 @@ describe("RadialMenuElements", () => { id: () => 1, isAlliedWith: vi.fn(() => false), isPlayer: vi.fn(() => true), + isTraitor: vi.fn(() => false), + isDisconnected: vi.fn(() => false), } as unknown as PlayerView; mockGame = { @@ -339,6 +341,8 @@ describe("RadialMenuElements", () => { id: () => 2, isAlliedWith: vi.fn(() => true), isPlayer: vi.fn(() => true), + isTraitor: vi.fn(() => false), + isDisconnected: vi.fn(() => false), } as unknown as PlayerView; mockParams.selected = allyPlayer; mockGame.owner = vi.fn(() => allyPlayer); diff --git a/tests/radialMenuElements.test.ts b/tests/radialMenuElements.test.ts index 15a8885fc..e5ac9d34f 100644 --- a/tests/radialMenuElements.test.ts +++ b/tests/radialMenuElements.test.ts @@ -19,13 +19,18 @@ import { } from "../src/client/graphics/layers/RadialMenuElements"; // Minimal stubs to satisfy types used in rootMenuElement.subMenu and allyBreak actions -const makePlayer = (id: string) => +const makePlayer = ( + id: string, + opts?: { isTraitor?: boolean; isDisconnected?: boolean }, +) => ({ id: () => id, isAlliedWith: (other: any) => other && typeof other.id === "function" && other.id() !== id ? true : true, + isTraitor: () => opts?.isTraitor ?? false, + isDisconnected: () => opts?.isDisconnected ?? false, }) as unknown as import("../src/core/game/GameView").PlayerView; const makeParams = (opts?: Partial): MenuElementParams => { @@ -82,7 +87,26 @@ describe("RadialMenuElements ally break", () => { const ally = findAllyBreak(items)!; expect(ally).toBeTruthy(); expect(ally.name).toBe("break"); - expect(ally.color).toBe(COLORS.breakAlly); + expect(typeof ally.color).toBe("function"); + expect(ally.color(params)).toBe(COLORS.breakAlly); + }); + + test("shows break option with orange color when allied to traitor", () => { + const params = makeParams({ + selected: makePlayer("p2", { isTraitor: true }), + }); + const items = rootMenuElement.subMenu!(params); + const ally = findAllyBreak(items)!; + expect(ally.color(params)).toBe(COLORS.breakAllyNoDebuff); + }); + + test("shows boat button instead of break when allied to disconnected player", () => { + const params = makeParams({ + selected: makePlayer("p2", { isDisconnected: true }), + }); + const items = rootMenuElement.subMenu!(params); + expect(findAllyBreak(items)).toBeUndefined(); + expect(items.find((i) => i.id === "boat")).toBeDefined(); }); test("break action calls handleBreakAlliance and closes menu", () => {