From d07f84f80175f9c2dc8f9e2654f9196d14b318a8 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Sat, 8 Nov 2025 05:09:03 +0100 Subject: [PATCH] Move warship by a touch (of magic) (#2408) ## Description: This adds moving warships by tapping them on a touchscreen. Now you can steer them just like you already could with a mouse. Also has some earlier returns, doing checks only when needed, prevent as much duplicate checks and a bugfix. In onMouseUp: - early return if no OceanTile. Before, function findWarshipsNearCell would still go look for warships even if ocean wasn't clicked. - Move const nearbyWarShips down. It isn't needed when this.selectedUnit is true. - Remove unnecessary const clickedWarship. - Move getting clickRef from function findWarshipsNearCell into onMouseUp. Because it is needed in case of this.selectedUnit too, within onMouseUp. Getting a valid clickRef for this.selectedUnit fixes: Runtime error when clicking outside the map after selecting a warship. The isValidCoord/Ref check was missing for this.selectedUnit. For findWarshipsNearCell: - moved the cell/tile checks out to onMouseUp, the only caller of the function. - did NOT rename findWarshipsNearCell. Although it now uses tileRef as input. Renaming can cause merge issues so i only do this when needed. Added onTouch: - Tests if we need to look for warships to select/move or if we can open Radial Menu. - Prevent as much duplicated checks as possible. So if no there's no Ocean Tile found, just send the radial menu event, which checks isValidCoord anyway. isOceanTile itself works fine even if it's no valid cell (proven by this.selectedUnit working all this time in onMouseUp without an isValidCoord test). Screencap on mobile, shows selecting and moving warships, no runtime error when clicking outside the map after selecting a warship, while radial menu still opens as normal: https://github.com/user-attachments/assets/1300d557-ae2f-46e3-92bd-d434c523aae7 ## 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 --- src/client/InputHandler.ts | 8 ++- src/client/graphics/layers/UnitLayer.ts | 91 ++++++++++++++++++------- 2 files changed, 74 insertions(+), 25 deletions(-) diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 9c55e837a..26d8f6c27 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -18,6 +18,12 @@ export class MouseOverEvent implements GameEvent { public readonly y: number, ) {} } +export class TouchEvent implements GameEvent { + constructor( + public readonly x: number, + public readonly y: number, + ) {} +} /** * Event emitted when a unit is selected or deselected @@ -476,7 +482,7 @@ export class InputHandler { Math.abs(event.y - this.lastPointerDownY); if (dist < 10) { if (event.pointerType === "touch") { - this.eventBus.emit(new ContextMenuEvent(event.clientX, event.clientY)); + this.eventBus.emit(new TouchEvent(event.x, event.y)); event.preventDefault(); return; } diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index d0c5f8dd9..e7f69089d 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -7,7 +7,9 @@ import { GameView, UnitView } from "../../../core/game/GameView"; import { BezenhamLine } from "../../../core/utilities/Line"; import { AlternateViewEvent, + ContextMenuEvent, MouseUpEvent, + TouchEvent, UnitSelectionEvent, } from "../../InputHandler"; import { MoveWarshipIntentEvent } from "../../Transport"; @@ -73,6 +75,7 @@ export class UnitLayer implements Layer { init() { this.eventBus.on(AlternateViewEvent, (e) => this.onAlternativeViewEvent(e)); this.eventBus.on(MouseUpEvent, (e) => this.onMouseUp(e)); + this.eventBus.on(TouchEvent, (e) => this.onTouch(e)); this.eventBus.on(UnitSelectionEvent, (e) => this.onUnitSelectionChange(e)); this.redraw(); @@ -81,16 +84,10 @@ export class UnitLayer implements Layer { /** * Find player-owned warships near the given cell within a configurable radius - * @param cell The cell to check + * @param clickRef The tile to check * @returns Array of player's warships in range, sorted by distance (closest first) */ - private findWarshipsNearCell(cell: { x: number; y: number }): UnitView[] { - if (!this.game.isValidCoord(cell.x, cell.y)) { - // The cell coordinate were invalid (user probably clicked outside the map), therefore no warships can be found - return []; - } - const clickRef = this.game.ref(cell.x, cell.y); - + private findWarshipsNearCell(clickRef: TileRef): UnitView[] { // Only select warships owned by the player return this.game .units(UnitType.Warship) @@ -109,29 +106,75 @@ export class UnitLayer implements Layer { }); } - private onMouseUp(event: MouseUpEvent) { - // Convert screen coordinates to world coordinates + private onMouseUp( + event: MouseUpEvent, + clickRef?: TileRef, + nearbyWarships?: UnitView[], + ) { + if (clickRef === undefined) { + // Convert screen coordinates to world coordinates + const cell = this.transformHandler.screenToWorldCoordinates( + event.x, + event.y, + ); + if (!this.game.isValidCoord(cell.x, cell.y)) return; + + clickRef = this.game.ref(cell.x, cell.y); + } + if (!this.game.isOcean(clickRef)) return; + + if (this.selectedUnit) { + this.eventBus.emit( + new MoveWarshipIntentEvent(this.selectedUnit.id(), clickRef), + ); + // Deselect + this.eventBus.emit(new UnitSelectionEvent(this.selectedUnit, false)); + return; + } + + // Find warships near this tile, sorted by distance + nearbyWarships ??= this.findWarshipsNearCell(clickRef); + if (nearbyWarships.length > 0) { + // Toggle selection of the closest warship + this.eventBus.emit(new UnitSelectionEvent(nearbyWarships[0], true)); + } + } + + private onTouch(event: TouchEvent) { const cell = this.transformHandler.screenToWorldCoordinates( event.x, event.y, ); - // Find warships near this cell, sorted by distance - const nearbyWarships = this.findWarshipsNearCell(cell); + 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)); + return; + } + + if (!this.game.isValidRef(clickRef)) { + return; + } if (this.selectedUnit) { - const clickRef = this.game.ref(cell.x, cell.y); - if (this.game.isOcean(clickRef)) { - this.eventBus.emit( - new MoveWarshipIntentEvent(this.selectedUnit.id(), clickRef), - ); - } - // Deselect - this.eventBus.emit(new UnitSelectionEvent(this.selectedUnit, false)); - } else if (nearbyWarships.length > 0) { - // Toggle selection of the closest warship - const clickedUnit = nearbyWarships[0]; - this.eventBus.emit(new UnitSelectionEvent(clickedUnit, true)); + // Reuse the mouse logic, send clickRef to avoid fetching it again + this.onMouseUp(new MouseUpEvent(event.x, event.y), clickRef); + return; + } + + const nearbyWarships = this.findWarshipsNearCell(clickRef); + + if (nearbyWarships.length > 0) { + this.onMouseUp( + new MouseUpEvent(event.x, event.y), + clickRef, + nearbyWarships, + ); + } else { + // No warships selected or nearby, open Radial Menu + this.eventBus.emit(new ContextMenuEvent(event.x, event.y)); } }