mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-18 23:02:48 +00:00
feat: multi-warship selection with Shift+drag box (#3677)
Resolves #3666 ## Description: Adds RTS-style box selection for warships. Hold Shift and drag (desktop) or long-press and drag (touch/mobile) to draw a selection rectangle — all player-owned warships inside get selected at once. A subsequent click/tap on water sends them all to that location. - `SelectionBoxLayer` — pixel-dashed rectangle in world-space, player territory color; shared between desktop and touch - `UILayer` — same pulsing selection outline on each box-selected warship; clears correctly when switching between single/multi selection - `UnitLayer` — finds warships in screen rect, filters inactive ships before sending; touch support included - `InputHandler` — Shift+drag and touch long-press+drag both emit selection box events; cursor becomes crosshair on Shift; discards active ghost structure on Shift press; configurable via `shiftKey` keybind - `Transport` — single atomic `move_multiple_warships` intent (no split on socket drop) - `Schemas` + `ExecutionManager` + `MoveMultipleWarshipsExecution` — server fans out atomic intent into individual `MoveWarshipExecution` per ship - `DynamicUILayer` — `MoveIndicatorUI` chevron animation on target tile for both single and multi move - `UnitDisplay` — warship tooltip Shift hint via `translateText` - `HelpModal` — new hotkey row: Shift + drag → select multiple warships ## 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 ## UI update ### Mouse + Keyboard https://github.com/user-attachments/assets/3f35ab5e-1f3c-4c5d-bc4f-aabccf64dc60 ### Touch https://github.com/user-attachments/assets/0d6aec3f-44fa-4fee-b5c6-b267b9b14d79 ## ## Please put your Discord username so you can be contacted if a bug or regression is found: fghjk_60845
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
import { colord, Colord } from "colord";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { UnitType } from "../../../core/game/Game";
|
||||
import { Cell, UnitType } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { BezenhamLine } from "../../../core/utilities/Line";
|
||||
import {
|
||||
AlternateViewEvent,
|
||||
CloseViewEvent,
|
||||
ContextMenuEvent,
|
||||
MouseUpEvent,
|
||||
SelectAllWarshipsEvent,
|
||||
TouchEvent,
|
||||
UnitSelectionEvent,
|
||||
WarshipSelectionBoxCancelEvent,
|
||||
WarshipSelectionBoxCompleteEvent,
|
||||
} from "../../InputHandler";
|
||||
import { MoveWarshipIntentEvent } from "../../Transport";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
@@ -48,6 +52,9 @@ export class UnitLayer implements Layer {
|
||||
// Selected unit property as suggested in the review comment
|
||||
private selectedUnit: UnitView | null = null;
|
||||
|
||||
// Multi-selected warships (from selection box)
|
||||
private selectedWarships: UnitView[] = [];
|
||||
|
||||
// Configuration for unit selection
|
||||
private readonly WARSHIP_SELECTION_RADIUS = 10; // Radius in game cells for warship selection hit zone
|
||||
|
||||
@@ -93,6 +100,14 @@ export class UnitLayer implements Layer {
|
||||
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.eventBus.on(WarshipSelectionBoxCompleteEvent, (e) =>
|
||||
this.onSelectionBoxComplete(e),
|
||||
);
|
||||
this.eventBus.on(WarshipSelectionBoxCancelEvent, () =>
|
||||
this.onSelectionBoxCancel(),
|
||||
);
|
||||
this.eventBus.on(CloseViewEvent, () => this.onSelectionBoxCancel());
|
||||
this.eventBus.on(SelectAllWarshipsEvent, () => this.onSelectAllWarships());
|
||||
this.redraw();
|
||||
|
||||
loadAllSprites();
|
||||
@@ -139,9 +154,24 @@ export class UnitLayer implements Layer {
|
||||
}
|
||||
if (!this.game.isWater(clickRef)) return;
|
||||
|
||||
// If we have multi-selected warships, send them all to this tile
|
||||
if (this.selectedWarships.length > 0) {
|
||||
const myPlayer = this.game.myPlayer();
|
||||
const activeIds = this.selectedWarships
|
||||
.filter((u) => u.isActive() && u.owner() === myPlayer)
|
||||
.map((u) => u.id());
|
||||
|
||||
if (activeIds.length > 0) {
|
||||
this.eventBus.emit(new MoveWarshipIntentEvent(activeIds, clickRef));
|
||||
}
|
||||
this.selectedWarships = [];
|
||||
this.eventBus.emit(new UnitSelectionEvent(null, false));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedUnit) {
|
||||
this.eventBus.emit(
|
||||
new MoveWarshipIntentEvent(this.selectedUnit.id(), clickRef),
|
||||
new MoveWarshipIntentEvent([this.selectedUnit.id()], clickRef),
|
||||
);
|
||||
// Deselect
|
||||
this.eventBus.emit(new UnitSelectionEvent(this.selectedUnit, false));
|
||||
@@ -187,6 +217,12 @@ export class UnitLayer implements Layer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Also delegate if we have multi-selected warships
|
||||
if (this.selectedWarships.length > 0) {
|
||||
this.onMouseUp(new MouseUpEvent(event.x, event.y), clickRef);
|
||||
return;
|
||||
}
|
||||
|
||||
const nearbyWarships = this.findWarshipsNearCell(clickRef);
|
||||
|
||||
if (nearbyWarships.length > 0) {
|
||||
@@ -212,6 +248,66 @@ export class UnitLayer implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle completion of shift+drag selection box.
|
||||
* Finds all player-owned warships within the screen rectangle.
|
||||
*/
|
||||
private onSelectionBoxComplete(event: WarshipSelectionBoxCompleteEvent) {
|
||||
const x1 = Math.min(event.startX, event.endX);
|
||||
const y1 = Math.min(event.startY, event.endY);
|
||||
const x2 = Math.max(event.startX, event.endX);
|
||||
const y2 = Math.max(event.startY, event.endY);
|
||||
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (!myPlayer) return;
|
||||
|
||||
this.selectedWarships = this.game.units(UnitType.Warship).filter((unit) => {
|
||||
if (!unit.isActive() || unit.owner() !== myPlayer) return false;
|
||||
const screen = this.transformHandler.worldToScreenCoordinates(
|
||||
new Cell(this.game.x(unit.tile()), this.game.y(unit.tile())),
|
||||
);
|
||||
return (
|
||||
screen.x >= x1 && screen.x <= x2 && screen.y >= y1 && screen.y <= y2
|
||||
);
|
||||
});
|
||||
|
||||
// Clear single selection if we got a box selection
|
||||
if (this.selectedWarships.length > 0 && this.selectedUnit) {
|
||||
this.eventBus.emit(new UnitSelectionEvent(this.selectedUnit, false));
|
||||
}
|
||||
|
||||
// Notify UILayer to draw selection boxes for all selected warships
|
||||
this.eventBus.emit(
|
||||
new UnitSelectionEvent(null, true, this.selectedWarships),
|
||||
);
|
||||
}
|
||||
|
||||
private onSelectionBoxCancel() {
|
||||
this.selectedWarships = [];
|
||||
this.eventBus.emit(new UnitSelectionEvent(null, false));
|
||||
}
|
||||
|
||||
private onSelectAllWarships() {
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (!myPlayer) return;
|
||||
|
||||
const allWarships = this.game
|
||||
.units(UnitType.Warship)
|
||||
.filter((u) => u.isActive() && u.owner() === myPlayer);
|
||||
|
||||
if (allWarships.length === 0) return;
|
||||
|
||||
// Clear single selection if active
|
||||
if (this.selectedUnit) {
|
||||
this.eventBus.emit(new UnitSelectionEvent(this.selectedUnit, false));
|
||||
}
|
||||
|
||||
this.selectedWarships = allWarships;
|
||||
this.eventBus.emit(
|
||||
new UnitSelectionEvent(null, true, this.selectedWarships),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle unit deactivation or destruction
|
||||
* If the selected unit is removed from the game, deselect it
|
||||
|
||||
Reference in New Issue
Block a user