Disable Radial Menu during spawn phase, just left click or tap to pick spawn point (#3611)

## Description:

During spawn phase, disable Radial Menu further. Its options where
already greyed out or non-responding on purpose, except for the attack
button (middle button) which could only be used to select a spawn point
but two clicks for that is convoluted.

It was mostly a nuisance, especially on mobile where you where forced to
go through the Radial Menu, so tap and then tap again to pick a spawn
point.

- Now, left click directly places a spawn point. Even if "Left click
opens menu" is enabled.
- And right click does not open Radial menu anymore. Which had no use
anyway. And also makes touch screen and mouse players more alike in that
they now both have no access to the Radial Menu (which didn't have a
purpose anyway except picking spawn point in a convoluted way with two
clicks).
- On touch screen during spawn phase, the Radial Menu also doesn't open
anymore. Instead of two taps (open Radial Menu > tap attack button), now
one tap suffices to pick a spawn point just like one left mouse click
now does.

Fixes https://github.com/openfrontio/OpenFrontIO/issues/3609

Also from UnitLayer > onTouch:
- remove redundant isValidRef check. Since isValidCoord check was added
in PR 3226 above it, we know it is a correct coord and with that correct
ref, already.
- remove redundant comment about isValidCoord/Ref not being checked
there yet intentionally, because it is actually being checked there
since PR 3226.

## 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

## Please put your Discord username so you can be contacted if a bug or
regression is found:

tryout33

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
This commit is contained in:
VariableVince
2026-04-08 23:49:51 +02:00
committed by GitHub
parent 105404ca50
commit 646d7ecaf6
4 changed files with 115 additions and 14 deletions
+1 -1
View File
@@ -270,7 +270,7 @@ async function createClientGame(
clientID,
eventBus,
gameRenderer,
new InputHandler(gameRenderer.uiState, canvas, eventBus),
new InputHandler(gameView, gameRenderer.uiState, canvas, eventBus),
transport,
worker,
gameView,
+10 -2
View File
@@ -1,6 +1,6 @@
import { EventBus, GameEvent } from "../core/EventBus";
import { PlayerBuildableUnitType, UnitType } from "../core/game/Game";
import { UnitView } from "../core/game/GameView";
import { GameView, UnitView } from "../core/game/GameView";
import { UserSettings } from "../core/game/UserSettings";
import { UIState } from "./graphics/UIState";
import { Platform } from "./Platform";
@@ -177,6 +177,7 @@ export class InputHandler {
private readonly userSettings: UserSettings = new UserSettings();
constructor(
private gameView: GameView,
public uiState: UIState,
private canvas: HTMLCanvasElement,
private eventBus: EventBus,
@@ -571,7 +572,11 @@ export class InputHandler {
return;
}
if (!this.userSettings.leftClickOpensMenu() || event.shiftKey) {
if (
!this.userSettings.leftClickOpensMenu() ||
event.shiftKey ||
this.gameView.inSpawnPhase() // No Radial Menu during spawn phase, only spawn point selection
) {
this.eventBus.emit(new MouseUpEvent(event.x, event.y));
} else {
this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY));
@@ -658,6 +663,9 @@ export class InputHandler {
private onContextMenu(event: MouseEvent) {
event.preventDefault();
if (this.gameView.inSpawnPhase()) {
return;
}
if (this.uiState.ghostStructure !== null) {
this.setGhostStructure(null);
return;
+8 -5
View File
@@ -167,14 +167,17 @@ export class UnitLayer implements Layer {
}
const clickRef = this.game.ref(cell.x, cell.y);
if (!this.game.isOcean(clickRef)) {
// No isValidCoord/Ref check yet, that is done for ContextMenuEvent later
// No warship to find because no Ocean tile, open Radial Menu
this.eventBus.emit(new ContextMenuEvent(event.x, event.y));
if (this.game.inSpawnPhase()) {
// No Radial Menu during spawn phase, only spawn point selection
if (!this.game.isOcean(clickRef)) {
this.eventBus.emit(new MouseUpEvent(event.x, event.y));
}
return;
}
if (!this.game.isValidRef(clickRef)) {
if (!this.game.isOcean(clickRef)) {
// No warship to find because no Ocean tile, open Radial Menu
this.eventBus.emit(new ContextMenuEvent(event.x, event.y));
return;
}
+96 -6
View File
@@ -6,13 +6,17 @@ import {
import { UIState } from "../src/client/graphics/UIState";
import { EventBus } from "../src/core/EventBus";
import { UnitType } from "../src/core/game/Game";
import { GameView } from "../src/core/game/GameView";
class MockPointerEvent {
button: number;
clientX: number;
clientY: number;
x: number;
y: number;
pointerId: number;
type: string;
pointerType: string;
preventDefault: () => void;
constructor(type: string, init: any) {
@@ -20,7 +24,10 @@ class MockPointerEvent {
this.button = init.button;
this.clientX = init.clientX;
this.clientY = init.clientY;
this.x = init.x ?? init.clientX;
this.y = init.y ?? init.clientY;
this.pointerId = init.pointerId;
this.pointerType = init.pointerType ?? "mouse";
this.preventDefault = vi.fn();
}
}
@@ -29,10 +36,12 @@ global.PointerEvent = MockPointerEvent as any;
describe("InputHandler AutoUpgrade", () => {
let inputHandler: InputHandler;
let mockGameView: GameView;
let eventBus: EventBus;
let mockCanvas: HTMLCanvasElement;
beforeEach(() => {
mockGameView = { inSpawnPhase: () => false } as GameView;
mockCanvas = document.createElement("canvas");
mockCanvas.width = 800;
mockCanvas.height = 600;
@@ -40,6 +49,7 @@ describe("InputHandler AutoUpgrade", () => {
eventBus = new EventBus();
inputHandler = new InputHandler(
mockGameView,
{
attackRatio: 20,
ghostStructure: null,
@@ -218,6 +228,56 @@ describe("InputHandler AutoUpgrade", () => {
});
});
describe("Spawn Phase Handling", () => {
test("should emit MouseUpEvent and not ContextMenuEvent on left click release during spawn phase", () => {
mockGameView.inSpawnPhase = () => true;
const mockEmit = vi.spyOn(eventBus, "emit");
inputHandler["userSettings"].leftClickOpensMenu = () => true;
const pointerEvent = new PointerEvent("pointerup", {
button: 0,
clientX: 150,
clientY: 250,
});
inputHandler["lastPointerDownX"] = 149;
inputHandler["lastPointerDownY"] = 249;
inputHandler["onPointerUp"](pointerEvent);
expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
x: 150,
y: 250,
}),
);
const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).toContain("MouseUpEvent");
expect(emittedTypes).not.toContain("ContextMenuEvent");
});
test("should suppress/ignore context menu events during spawn phase", () => {
mockGameView.inSpawnPhase = () => true;
const mockEmit = vi.spyOn(eventBus, "emit");
const mouseEvent = new MouseEvent("contextmenu", {
clientX: 150,
clientY: 250,
});
const preventDefaultSpy = vi.spyOn(mouseEvent, "preventDefault");
inputHandler["onContextMenu"](mouseEvent);
expect(preventDefaultSpy).toHaveBeenCalled();
const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).not.toContain("ContextMenuEvent");
});
});
describe("Pointer Event Handling", () => {
test("should handle pointer events with different pointer IDs", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
@@ -481,7 +541,12 @@ describe("InputHandler AutoUpgrade", () => {
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
});
@@ -533,7 +598,12 @@ describe("InputHandler AutoUpgrade", () => {
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
});
@@ -570,7 +640,12 @@ describe("InputHandler AutoUpgrade", () => {
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
});
@@ -590,7 +665,12 @@ describe("InputHandler AutoUpgrade", () => {
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
window.dispatchEvent(
@@ -616,7 +696,12 @@ describe("InputHandler AutoUpgrade", () => {
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
window.dispatchEvent(
@@ -639,7 +724,12 @@ describe("InputHandler AutoUpgrade", () => {
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(uiState, mockCanvas, eventBus);
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
window.dispatchEvent(