mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 23:23:37 +00:00
29a1e8dfda
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
145 lines
5.4 KiB
TypeScript
145 lines
5.4 KiB
TypeScript
import { Execution, Game } from "../game/Game";
|
|
import { PseudoRandom } from "../PseudoRandom";
|
|
import { ClientID, GameID, StampedIntent, Turn } from "../Schemas";
|
|
import { simpleHash } from "../Util";
|
|
import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution";
|
|
import { AllianceRejectExecution } from "./alliance/AllianceRejectExecution";
|
|
import { AllianceRequestExecution } from "./alliance/AllianceRequestExecution";
|
|
import { BreakAllianceExecution } from "./alliance/BreakAllianceExecution";
|
|
import { AttackExecution } from "./AttackExecution";
|
|
import { BoatRetreatExecution } from "./BoatRetreatExecution";
|
|
import { ConstructionExecution } from "./ConstructionExecution";
|
|
import { DeleteUnitExecution } from "./DeleteUnitExecution";
|
|
import { DonateGoldExecution } from "./DonateGoldExecution";
|
|
import { DonateTroopsExecution } from "./DonateTroopExecution";
|
|
import { EmbargoAllExecution } from "./EmbargoAllExecution";
|
|
import { EmbargoExecution } from "./EmbargoExecution";
|
|
import { EmojiExecution } from "./EmojiExecution";
|
|
import { MarkDisconnectedExecution } from "./MarkDisconnectedExecution";
|
|
import { MoveWarshipExecution } from "./MoveWarshipExecution";
|
|
import { NationExecution } from "./NationExecution";
|
|
import { NoOpExecution } from "./NoOpExecution";
|
|
import { PauseExecution } from "./PauseExecution";
|
|
import { QuickChatExecution } from "./QuickChatExecution";
|
|
import { RetreatExecution } from "./RetreatExecution";
|
|
import { SpawnExecution } from "./SpawnExecution";
|
|
import { TargetPlayerExecution } from "./TargetPlayerExecution";
|
|
import { TransportShipExecution } from "./TransportShipExecution";
|
|
import { TribeSpawner } from "./TribeSpawner";
|
|
import { UpgradeStructureExecution } from "./UpgradeStructureExecution";
|
|
import { PlayerSpawner } from "./utils/PlayerSpawner";
|
|
|
|
export class Executor {
|
|
// private random = new PseudoRandom(999)
|
|
private random: PseudoRandom;
|
|
|
|
constructor(
|
|
private mg: Game,
|
|
private gameID: GameID,
|
|
private clientID: ClientID | undefined,
|
|
) {
|
|
// Add one to avoid id collisions with tribes.
|
|
this.random = new PseudoRandom(simpleHash(gameID) + 1);
|
|
}
|
|
|
|
createExecs(turn: Turn): Execution[] {
|
|
return turn.intents.map((i) => this.createExec(i));
|
|
}
|
|
|
|
createExec(intent: StampedIntent): Execution {
|
|
const player = this.mg.playerByClientID(intent.clientID);
|
|
if (!player) {
|
|
console.warn(`player with clientID ${intent.clientID} not found`);
|
|
return new NoOpExecution();
|
|
}
|
|
|
|
// create execution
|
|
switch (intent.type) {
|
|
case "attack": {
|
|
return new AttackExecution(
|
|
intent.troops,
|
|
player,
|
|
intent.targetID,
|
|
null,
|
|
);
|
|
}
|
|
case "cancel_attack":
|
|
return new RetreatExecution(player, intent.attackID);
|
|
case "cancel_boat":
|
|
return new BoatRetreatExecution(player, intent.unitID);
|
|
case "move_warship":
|
|
return new MoveWarshipExecution(player, intent.unitIds, intent.tile);
|
|
case "spawn":
|
|
return new SpawnExecution(this.gameID, player.info(), intent.tile);
|
|
case "boat":
|
|
return new TransportShipExecution(player, intent.dst, intent.troops);
|
|
case "allianceRequest":
|
|
return new AllianceRequestExecution(player, intent.recipient);
|
|
case "allianceReject":
|
|
return new AllianceRejectExecution(intent.requestor, player);
|
|
case "breakAlliance":
|
|
return new BreakAllianceExecution(player, intent.recipient);
|
|
case "targetPlayer":
|
|
return new TargetPlayerExecution(player, intent.target);
|
|
case "emoji":
|
|
return new EmojiExecution(player, intent.recipient, intent.emoji);
|
|
case "donate_troops":
|
|
return new DonateTroopsExecution(
|
|
player,
|
|
intent.recipient,
|
|
intent.troops,
|
|
);
|
|
case "donate_gold":
|
|
return new DonateGoldExecution(player, intent.recipient, intent.gold);
|
|
case "embargo":
|
|
return new EmbargoExecution(player, intent.targetID, intent.action);
|
|
case "embargo_all":
|
|
return new EmbargoAllExecution(player, intent.action);
|
|
case "build_unit":
|
|
return new ConstructionExecution(
|
|
player,
|
|
intent.unit,
|
|
intent.tile,
|
|
intent.rocketDirectionUp,
|
|
);
|
|
case "allianceExtension": {
|
|
return new AllianceExtensionExecution(player, intent.recipient);
|
|
}
|
|
|
|
case "upgrade_structure":
|
|
return new UpgradeStructureExecution(player, intent.unitId);
|
|
case "delete_unit":
|
|
return new DeleteUnitExecution(player, intent.unitId);
|
|
case "quick_chat":
|
|
return new QuickChatExecution(
|
|
player,
|
|
intent.recipient,
|
|
intent.quickChatKey,
|
|
intent.target,
|
|
);
|
|
case "mark_disconnected":
|
|
return new MarkDisconnectedExecution(player, intent.isDisconnected);
|
|
case "toggle_pause":
|
|
return new PauseExecution(player, intent.paused);
|
|
default:
|
|
throw new Error(`intent type ${intent} not found`);
|
|
}
|
|
}
|
|
|
|
spawnTribes(numTribes: number): SpawnExecution[] {
|
|
return new TribeSpawner(this.mg, this.gameID).spawnTribes(numTribes);
|
|
}
|
|
|
|
spawnPlayers(): SpawnExecution[] {
|
|
return new PlayerSpawner(this.mg, this.gameID).spawnPlayers();
|
|
}
|
|
|
|
nationExecutions(): Execution[] {
|
|
const execs: Execution[] = [];
|
|
for (const nation of this.mg.nations()) {
|
|
execs.push(new NationExecution(this.gameID, nation));
|
|
}
|
|
return execs;
|
|
}
|
|
}
|