Skip to content

Commit de07436

Browse files
feat: touch long-press + drag for warship multi-select on mobile
- Hold finger ~800ms to activate selection mode - Drag to draw selection box (same pixel-dashed style as desktop) - Release to select warships, tap water to send them - Long-press cancelled if finger moves >10px before timer fires - Single SelectionBoxLayer handles both Shift+drag and touch long-press
1 parent 19a5c75 commit de07436

File tree

2 files changed

+64
-15
lines changed

2 files changed

+64
-15
lines changed

src/client/InputHandler.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ export class WarshipMultiSelectionEvent implements GameEvent {
126126
constructor(public readonly units: UnitView[]) {}
127127
}
128128

129+
/** Emitted when a touch long-press is detected (shows crosshair indicator) */
130+
export class TouchLongPressStartEvent implements GameEvent {
131+
constructor(
132+
public readonly x: number,
133+
public readonly y: number,
134+
) {}
135+
}
136+
129137
export class ShowBuildMenuEvent implements GameEvent {
130138
constructor(
131139
public readonly x: number,
@@ -197,6 +205,11 @@ export class InputHandler {
197205
// Warship selection box state
198206
private selectionBoxActive: boolean = false;
199207

208+
// Touch long-press state
209+
private longPressTimer: ReturnType<typeof setTimeout> | null = null;
210+
private longPressActive: boolean = false;
211+
private readonly LONG_PRESS_MS = 800;
212+
200213
private moveInterval: NodeJS.Timeout | null = null;
201214
private activeKeys = new Set<string>();
202215
private keybinds: Record<string, string> = {};
@@ -256,6 +269,11 @@ export class InputHandler {
256269
}
257270
this.pointerDown = false;
258271
this.pointers.clear();
272+
if (this.longPressTimer !== null) {
273+
clearTimeout(this.longPressTimer);
274+
this.longPressTimer = null;
275+
}
276+
this.longPressActive = false;
259277
this.canvas.style.cursor = "";
260278
});
261279
this.pointers.clear();
@@ -528,6 +546,22 @@ export class InputHandler {
528546
this.lastPointerDownY = event.clientY;
529547

530548
this.eventBus.emit(new MouseDownEvent(event.clientX, event.clientY));
549+
550+
// Start long-press timer for touch devices
551+
if (event.pointerType === "touch") {
552+
this.longPressActive = false;
553+
this.longPressTimer = setTimeout(() => {
554+
this.longPressTimer = null;
555+
this.longPressActive = true;
556+
this.canvas.style.cursor = "crosshair";
557+
this.eventBus.emit(
558+
new TouchLongPressStartEvent(
559+
this.lastPointerDownX,
560+
this.lastPointerDownY,
561+
),
562+
);
563+
}, this.LONG_PRESS_MS);
564+
}
531565
} else if (this.pointers.size === 2) {
532566
this.lastPinchDistance = this.getPinchDistance();
533567
}
@@ -545,6 +579,17 @@ export class InputHandler {
545579
this.pointerDown = false;
546580
this.pointers.clear();
547581

582+
// Clean up long-press state
583+
if (this.longPressTimer !== null) {
584+
clearTimeout(this.longPressTimer);
585+
this.longPressTimer = null;
586+
}
587+
const wasLongPress = this.longPressActive;
588+
this.longPressActive = false;
589+
if (wasLongPress) {
590+
this.canvas.style.cursor = "";
591+
}
592+
548593
// Complete selection box if it was active
549594
if (this.selectionBoxActive) {
550595
this.selectionBoxActive = false;
@@ -656,8 +701,19 @@ export class InputHandler {
656701
const deltaX = event.clientX - this.lastPointerX;
657702
const deltaY = event.clientY - this.lastPointerY;
658703

659-
// If shift is held, draw selection box instead of panning
660-
if (this.activeKeys.has(this.keybinds.shiftKey)) {
704+
// Cancel long-press if finger moved significantly before timer fires
705+
if (this.longPressTimer !== null) {
706+
const moveDist =
707+
Math.abs(event.clientX - this.lastPointerDownX) +
708+
Math.abs(event.clientY - this.lastPointerDownY);
709+
if (moveDist >= 10) {
710+
clearTimeout(this.longPressTimer);
711+
this.longPressTimer = null;
712+
}
713+
}
714+
715+
// If shift is held OR touch long-press is active, draw selection box
716+
if (this.activeKeys.has(this.keybinds.shiftKey) || this.longPressActive) {
661717
this.selectionBoxActive = true;
662718
this.eventBus.emit(
663719
new WarshipSelectionBoxUpdateEvent(

src/client/graphics/layers/SelectionBoxLayer.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { TransformHandler } from "../TransformHandler";
1010
import { Layer } from "./Layer";
1111

1212
/**
13-
* Renders the shift+drag warship selection rectangle in world-space,
14-
* using the same pixel-dashed style as the warship selection box in UILayer.
13+
* Renders the shift+drag / touch long-press warship selection rectangle
14+
* in world-space, using the same pixel-dashed style as UILayer.
1515
*/
1616
export class SelectionBoxLayer implements Layer {
1717
private active = false;
@@ -63,7 +63,6 @@ export class SelectionBoxLayer implements Layer {
6363
renderLayer(context: CanvasRenderingContext2D) {
6464
if (!this.active || !this.ctx) return;
6565

66-
// Convert screen corners to world coordinates
6766
const topLeft = this.transformHandler.screenToWorldCoordinates(
6867
Math.min(this.startX, this.endX),
6968
Math.min(this.startY, this.endY),
@@ -78,31 +77,26 @@ export class SelectionBoxLayer implements Layer {
7877
const wx2 = Math.floor(bottomRight.x);
7978
const wy2 = Math.floor(bottomRight.y);
8079

81-
// Clamp to canvas bounds to avoid out-of-bounds fillRect
8280
const cx1 = Math.max(0, wx1);
8381
const cy1 = Math.max(0, wy1);
8482
const cx2 = Math.min(this.canvas.width - 1, wx2);
8583
const cy2 = Math.min(this.canvas.height - 1, wy2);
8684

8785
if (cx2 <= cx1 || cy2 <= cy1) return;
8886

89-
// Player color — fall back to a neutral cyan if no player yet
9087
const myPlayer = this.game.myPlayer();
9188
const baseColor = myPlayer ? myPlayer.territoryColor().lighten(0.2) : null;
9289
const colorStr = baseColor
9390
? baseColor.alpha(0.85).toRgbString()
9491
: "rgba(100,200,255,0.85)";
9592

9693
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
97-
98-
// Draw dashed border using 4 line passes (O(n) not O(n²))
9994
this.ctx.fillStyle = colorStr;
100-
this.drawDashedLine(this.ctx, cx1, cy1, cx2, cy1); // top
101-
this.drawDashedLine(this.ctx, cx1, cy2, cx2, cy2); // bottom
102-
this.drawDashedLine(this.ctx, cx1, cy1, cx1, cy2); // left
103-
this.drawDashedLine(this.ctx, cx2, cy1, cx2, cy2); // right
95+
this.drawDashedLine(this.ctx, cx1, cy1, cx2, cy1);
96+
this.drawDashedLine(this.ctx, cx1, cy2, cx2, cy2);
97+
this.drawDashedLine(this.ctx, cx1, cy1, cx1, cy2);
98+
this.drawDashedLine(this.ctx, cx2, cy1, cx2, cy2);
10499

105-
// Subtle fill
106100
this.ctx.fillStyle = baseColor
107101
? baseColor.alpha(0.06).toRgbString()
108102
: "rgba(100,200,255,0.06)";
@@ -117,7 +111,6 @@ export class SelectionBoxLayer implements Layer {
117111
);
118112
}
119113

120-
/** Draw a dashed 1px line using the (x+y) % 2 pattern to match UILayer style */
121114
private drawDashedLine(
122115
ctx: CanvasRenderingContext2D,
123116
x1: number,

0 commit comments

Comments
 (0)