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
This commit is contained in:
evanpelle
2025-07-15 11:54:33 -07:00
committed by GitHub
parent 6efc37e97d
commit d4794840f6
2 changed files with 112 additions and 76 deletions
+22 -14
View File
@@ -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!,
);
});
}
}
+90 -62
View File
@@ -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
}