From 4367bacf710e8472c5f9f899113c7bb9e10bb5bb Mon Sep 17 00:00:00 2001 From: tnhnblgl <51187395+tnhnblgl@users.noreply.github.com> Date: Sat, 7 Jun 2025 00:11:39 +0300 Subject: [PATCH] Add Boat hotkey (#1060) ## Description: Pressing B to fast boat ![Screenshot_2025-06-06-09-32-53-865_com android chrome](https://github.com/user-attachments/assets/cdd4aefe-0088-4334-bf69-7cf3b32b6db6) ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: dovg --- resources/lang/en.json | 3 + src/client/ClientGameRunner.ts | 111 +++++++++++++++++++++++---------- src/client/InputHandler.ts | 8 +++ src/client/UserSettingModal.ts | 13 ++++ 4 files changed, 101 insertions(+), 34 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index d059c9261..d8f49b2de 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -257,6 +257,9 @@ "attack_ratio_up_desc": "Increase attack ratio by 10%", "attack_ratio_down": "Decrease Attack Ratio", "attack_ratio_down_desc": "Decrease attack ratio by 10%", + "attack_keybinds": "Attack Keybinds", + "boat_attack": "Boat Attack", + "boat_attack_desc": "Send a boat attack to the tile under your cursor.", "zoom_controls": "Zoom Controls", "zoom_out": "Zoom Out", "zoom_out_desc": "Zoom out the map", diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 65ea5be59..1bde53319 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -12,7 +12,7 @@ import { import { createGameRecord } from "../core/Util"; import { ServerConfig } from "../core/configuration/Config"; import { getConfig } from "../core/configuration/ConfigLoader"; -import { Cell, UnitType } from "../core/game/Game"; +import { Cell, PlayerActions, UnitType } from "../core/game/Game"; import { TileRef } from "../core/game/GameMap"; import { ErrorUpdate, @@ -25,7 +25,12 @@ import { GameView, PlayerView } from "../core/game/GameView"; import { loadTerrainMap, TerrainMapData } from "../core/game/TerrainMapLoader"; import { UserSettings } from "../core/game/UserSettings"; import { WorkerClient } from "../core/worker/WorkerClient"; -import { InputHandler, MouseMoveEvent, MouseUpEvent } from "./InputHandler"; +import { + DoBoatAttackEvent, + InputHandler, + MouseMoveEvent, + MouseUpEvent, +} from "./InputHandler"; import { endGame, startGame, startTime } from "./LocalPersistantStats"; import { getPersistentID } from "./Main"; import { @@ -231,6 +236,7 @@ export class ClientGameRunner { }, 20000); this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e)); this.eventBus.on(MouseMoveEvent, (e) => this.onMouseMove(e)); + this.eventBus.on(DoBoatAttackEvent, (e) => this.doBoatAttackUnderCursor()); this.renderer.initialize(); this.input.initialize(); @@ -363,13 +369,6 @@ export class ClientGameRunner { } this.myPlayer.actions(tile).then((actions) => { if (this.myPlayer === null) return; - const bu = actions.buildableUnits.find( - (bu) => bu.type === UnitType.TransportShip, - ); - if (bu === undefined) { - console.warn(`no transport ship buildable units`); - return; - } if (actions.canAttack) { this.eventBus.emit( new SendAttackIntentEvent( @@ -377,31 +376,8 @@ export class ClientGameRunner { this.myPlayer.troops() * this.renderer.uiState.attackRatio, ), ); - } else if ( - bu.canBuild !== false && - this.shouldBoat(tile, bu.canBuild) && - this.gameView.isLand(tile) - ) { - this.myPlayer - .bestTransportShipSpawn(this.gameView.ref(cell.x, cell.y)) - .then((spawn: number | false) => { - if (this.myPlayer === null) throw new Error("not initialized"); - let spawnCell: Cell | null = null; - if (spawn !== false) { - spawnCell = new Cell( - this.gameView.x(spawn), - this.gameView.y(spawn), - ); - } - this.eventBus.emit( - new SendBoatAttackIntentEvent( - this.gameView.owner(tile).id(), - cell, - this.myPlayer.troops() * this.renderer.uiState.attackRatio, - spawnCell, - ), - ); - }); + } else if (this.canBoatAttack(actions, tile)) { + this.sendBoatAttackIntent(tile, cell); } const owner = this.gameView.owner(tile); @@ -413,6 +389,73 @@ export class ClientGameRunner { }); } + private doBoatAttackUnderCursor(): void { + if (!this.isActive || !this.lastMousePosition) { + return; + } + const cell = this.renderer.transformHandler.screenToWorldCoordinates( + this.lastMousePosition.x, + this.lastMousePosition.y, + ); + if (!this.gameView.isValidCoord(cell.x, cell.y)) { + return; + } + + const tile = this.gameView.ref(cell.x, cell.y); + if (this.gameView.inSpawnPhase()) { + return; + } + + if (this.myPlayer === null) { + const myPlayer = this.gameView.playerByClientID(this.lobby.clientID); + if (myPlayer === null) return; + this.myPlayer = myPlayer; + } + + this.myPlayer.actions(tile).then((actions) => { + if (!actions.canAttack && this.canBoatAttack(actions, tile)) { + this.sendBoatAttackIntent(tile, cell); + } + }); + } + + private canBoatAttack(actions: PlayerActions, tile: TileRef): boolean { + const bu = actions.buildableUnits.find( + (bu) => bu.type === UnitType.TransportShip, + ); + if (bu === undefined) { + console.warn(`no transport ship buildable units`); + return false; + } + return ( + bu.canBuild !== false && + this.shouldBoat(tile, bu.canBuild) && + this.gameView.isLand(tile) + ); + } + + private sendBoatAttackIntent(tile: TileRef, cell: Cell) { + if (!this.myPlayer) return; + + this.myPlayer + .bestTransportShipSpawn(this.gameView.ref(cell.x, cell.y)) + .then((spawn: number | false) => { + if (this.myPlayer === null) throw new Error("not initialized"); + let spawnCell: Cell | null = null; + if (spawn !== false) { + spawnCell = new Cell(this.gameView.x(spawn), this.gameView.y(spawn)); + } + this.eventBus.emit( + new SendBoatAttackIntentEvent( + this.gameView.owner(tile).id(), + cell, + this.myPlayer.troops() * this.renderer.uiState.attackRatio, + spawnCell, + ), + ); + }); + } + private shouldBoat(tile: TileRef, src: TileRef) { // TODO: Global enable flag // TODO: Global limit autoboat to nearby shore flag diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 47f57ae7a..597733705 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -76,6 +76,8 @@ export class ShowEmojiMenuEvent implements GameEvent { ) {} } +export class DoBoatAttackEvent implements GameEvent {} + export class AttackRatioEvent implements GameEvent { constructor(public readonly attackRatio: number) {} } @@ -124,6 +126,7 @@ export class InputHandler { zoomIn: "KeyE", attackRatioDown: "Digit1", attackRatioUp: "Digit2", + boatAttack: "KeyB", ...JSON.parse(localStorage.getItem("settings.keybinds") ?? "{}"), }; this.canvas.addEventListener("pointerdown", (e) => this.onPointerDown(e)); @@ -242,6 +245,11 @@ export class InputHandler { this.eventBus.emit(new RefreshGraphicsEvent()); } + if (e.code === keybinds.boatAttack) { + e.preventDefault(); + this.eventBus.emit(new DoBoatAttackEvent()); + } + if (e.code === keybinds.attackRatioDown) { e.preventDefault(); this.eventBus.emit(new AttackRatioEvent(-10)); diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 405e24145..aca371552 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -367,6 +367,19 @@ export class UserSettingModal extends LitElement { @change=${this.handleKeybindChange} > +
+ ${translateText("user_setting.attack_keybinds")} +
+ + +
${translateText("user_setting.zoom_controls")}