From d4794840f6d51383a2d1472ba7b093c9041f5c61 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 15 Jul 2025 11:54:33 -0700 Subject: [PATCH] Have radial menu refresh when open (#1437) ## Description: There was a regression where the radial menu would not refresh while open (eg open radial menu, then get enough gold to build something, the radial wouldn't update). Also refactored a bit ## 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 --- src/client/graphics/layers/MainRadialMenu.ts | 36 +++-- src/client/graphics/layers/RadialMenu.ts | 152 +++++++++++-------- 2 files changed, 112 insertions(+), 76 deletions(-) diff --git a/src/client/graphics/layers/MainRadialMenu.ts b/src/client/graphics/layers/MainRadialMenu.ts index ccec0c13c..3981c8986 100644 --- a/src/client/graphics/layers/MainRadialMenu.ts +++ b/src/client/graphics/layers/MainRadialMenu.ts @@ -1,7 +1,7 @@ import { LitElement } from "lit"; import { customElement } from "lit/decorators.js"; import { EventBus } from "../../../core/EventBus"; -import { PlayerActions, TerraNullius } from "../../../core/game/Game"; +import { PlayerActions } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; import { GameView, PlayerView } from "../../../core/game/GameView"; import { TransformHandler } from "../TransformHandler"; @@ -31,7 +31,6 @@ export class MainRadialMenu extends LitElement implements Layer { private chatIntegration: ChatIntegration; private clickedTile: TileRef | null = null; - private selectedPlayer: PlayerView | TerraNullius | null = null; constructor( private eventBus: EventBus, @@ -71,6 +70,7 @@ export class MainRadialMenu extends LitElement implements Layer { init() { this.radialMenu.init(); + this.radialMenu.setRootMenuItems(rootMenuItems, centerButtonElement); this.eventBus.on(ContextMenuEvent, (event) => { const worldCoords = this.transformHandler.screenToWorldCoordinates( event.x, @@ -83,12 +83,11 @@ export class MainRadialMenu extends LitElement implements Layer { return; } this.clickedTile = this.game.ref(worldCoords.x, worldCoords.y); - this.selectedPlayer = this.game.owner(this.clickedTile); this.game .myPlayer()! .actions(this.clickedTile) .then((actions) => { - this.handlePlayerActions( + this.updatePlayerActions( this.game.myPlayer()!, actions, this.clickedTile!, @@ -99,12 +98,12 @@ export class MainRadialMenu extends LitElement implements Layer { }); } - private async handlePlayerActions( + private async updatePlayerActions( myPlayer: PlayerView, actions: PlayerActions, tile: TileRef, - screenX: number, - screenY: number, + screenX: number | null = null, + screenY: number | null = null, ) { this.buildMenu.playerActions = actions; @@ -130,18 +129,27 @@ export class MainRadialMenu extends LitElement implements Layer { eventBus: this.eventBus, }; - this.radialMenu.setRootMenuItems(rootMenuItems, centerButtonElement); this.radialMenu.setParams(params); - this.radialMenu.showRadialMenu(screenX, screenY); + if (screenX !== null && screenY !== null) { + this.radialMenu.showRadialMenu(screenX, screenY); + } else { + this.radialMenu.refresh(); + } } async tick() { if (!this.radialMenu.isMenuVisible() || this.clickedTile === null) return; - if (this.selectedPlayer === null) return; - const currentPlayer = this.game.owner(this.clickedTile); - if (currentPlayer.id() !== this.selectedPlayer.id()) { - this.closeMenu(); - return; + if (this.game.ticks() % 5 === 0) { + this.game + .myPlayer()! + .actions(this.clickedTile) + .then((actions) => { + this.updatePlayerActions( + this.game.myPlayer()!, + actions, + this.clickedTile!, + ); + }); } } diff --git a/src/client/graphics/layers/RadialMenu.ts b/src/client/graphics/layers/RadialMenu.ts index 78bfcbaa3..ae2ce623e 100644 --- a/src/client/graphics/layers/RadialMenu.ts +++ b/src/client/graphics/layers/RadialMenu.ts @@ -437,6 +437,8 @@ export class RadialMenu implements Layer { this.updateCenterButtonState("back"); } else { d.data.action?.(this.params); + // Force transition state to false to ensure menu hides + this.isTransitioning = false; this.hideRadialMenu(); } }; @@ -479,6 +481,14 @@ export class RadialMenu implements Layer { }); } + private isItemDisabled(item: MenuElement): boolean { + return ( + this.params === null || + this.params.game.inSpawnPhase() || + item.disabled(this.params) + ); + } + private renderIconsAndText( arcs: d3.Selection< SVGGElement, @@ -496,10 +506,7 @@ export class RadialMenu implements Layer { .each((d) => { const contentId = d.data.id; const content = d3.select(`g[data-id="${contentId}"]`); - const disabled = - this.params === null || - this.params.game.inSpawnPhase() || - d.data.disabled(this.params); + const disabled = this.isItemDisabled(d.data); if (d.data.text) { content @@ -557,24 +564,43 @@ export class RadialMenu implements Layer { } private updateMenuGroupVisibility() { - // Hide all menus except the current and immediate previous one + this.updateMenuVisibility("forward"); + } + + private updateMenuVisibility(direction: "forward" | "backward" = "backward") { this.menuGroups.forEach((menuGroup, level) => { if (level === this.currentLevel) { + // Current level - always visible and interactive menuGroup.style("display", "block"); - } else if (level === this.currentLevel - 1) { - menuGroup.style("display", "block"); - menuGroup .transition() .duration(this.config.menuTransitionDuration * 0.8) - .style("transform", "scale(0.5)") + .style("transform", "scale(1)") + .style("opacity", 1); + + // Enable pointer events for current level + menuGroup.selectAll("path").style("pointer-events", "auto"); + } else if (level === this.currentLevel - 1 && this.currentLevel > 0) { + // Previous level - visible but scaled down + menuGroup.style("display", "block"); + menuGroup + .transition() + .duration(this.config.menuTransitionDuration * 0.8) + .style( + "transform", + `scale(${this.currentLevel === 1 ? "0.65" : "0.5"})`, + ) .style("opacity", 0.8); - menuGroup.selectAll("path").each(function () { - const pathElement = d3.select(this); - pathElement.style("pointer-events", "none"); - }); - } else { + // Disable pointer events for previous level when going forward + if (direction === "forward") { + menuGroup.selectAll("path").each(function () { + const pathElement = d3.select(this); + pathElement.style("pointer-events", "none"); + }); + } + } else if (level !== this.currentLevel + 1) { + // Hide all other levels menuGroup .transition() .duration(this.config.menuTransitionDuration * 0.5) @@ -612,7 +638,7 @@ export class RadialMenu implements Layer { this.updateMenuLevels(); this.clearSelectedItemHoverState(); - this.updateMenuVisibility(); + this.updateMenuVisibility("backward"); this.animateMenuTransitions(); } @@ -639,54 +665,10 @@ export class RadialMenu implements Layer { if (selectedPath) { selectedPath.attr("filter", null); selectedPath.attr("stroke-width", "2"); - - const item = this.findMenuItem(this.selectedItemId); - if (item) { - const disabled = this.params === null || item.disabled(this.params); - const color = disabled - ? this.config.disabledColor - : (item.color ?? "#333333"); - const opacity = disabled ? 0.5 : 0.7; - selectedPath.attr( - "fill", - d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color, - ); - } } } - } - - private updateMenuVisibility() { - this.menuGroups.forEach((menuGroup, level) => { - if (level === this.currentLevel) { - menuGroup.style("display", "block"); - menuGroup - .transition() - .duration(this.config.menuTransitionDuration * 0.8) - .style("transform", "scale(1)") - .style("opacity", 1); - - menuGroup.selectAll("path").style("pointer-events", "auto"); - } else if (level === this.currentLevel - 1 && this.currentLevel > 0) { - menuGroup.style("display", "block"); - menuGroup - .transition() - .duration(this.config.menuTransitionDuration * 0.8) - .style( - "transform", - `scale(${this.currentLevel === 1 ? "0.65" : "0.5"})`, - ) - .style("opacity", 0.8); - } else if (level !== this.currentLevel + 1) { - menuGroup - .transition() - .duration(this.config.menuTransitionDuration * 0.5) - .style("opacity", 0) - .on("end", function () { - d3.select(this).style("display", "none"); - }); - } - }); + // Use refresh() to update all item appearances consistently + this.refresh(); } private animateMenuTransitions() { @@ -767,10 +749,13 @@ export class RadialMenu implements Layer { } public hideRadialMenu() { - if (!this.isVisible || this.isTransitioning) { + if (!this.isVisible) { return; } + // Force transition state to false to ensure menu hides + this.isTransitioning = false; + this.menuElement.style("display", "none"); this.isVisible = false; this.selectedItemId = null; @@ -951,6 +936,49 @@ export class RadialMenu implements Layer { this.renderMenuItems(this.currentMenuItems, this.currentLevel); } + public refresh() { + if (!this.isVisible || !this.params) return; + + // Refresh the disabled state of all menu items + this.menuPaths.forEach((path, itemId) => { + const item = this.findMenuItem(itemId); + if (item) { + const disabled = this.isItemDisabled(item); + const color = disabled + ? this.config.disabledColor + : (item.color ?? "#333333"); + const opacity = disabled ? 0.5 : 0.7; + + // Update path appearance + path.attr( + "fill", + d3.color(color)?.copy({ opacity: opacity })?.toString() ?? color, + ); + path.style("opacity", disabled ? 0.5 : 1); + path.style("cursor", disabled ? "not-allowed" : "pointer"); + + // Update icon/text appearance using the same logic as renderIconsAndText + const icon = this.menuIcons.get(itemId); + if (icon) { + // Update text opacity + const textElement = icon.select("text"); + if (!textElement.empty()) { + textElement.style("opacity", disabled ? 0.5 : 1); + } + + // Update image opacity + const imageElement = icon.select("image"); + if (!imageElement.empty()) { + imageElement.attr("opacity", disabled ? 0.5 : 1); + } + } + } + }); + + // Refresh center button state + this.updateCenterButtonState(this.centerButtonState); + } + renderLayer(context: CanvasRenderingContext2D) { // No need to render anything on the canvas }