diff --git a/resources/lang/en.json b/resources/lang/en.json index 44ebe941a4..2e412e3d32 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -572,6 +572,11 @@ "boat_attack_desc": "Send a boat attack to the tile under your cursor.", "ground_attack": "Ground Attack", "ground_attack_desc": "Send a ground attack to the tile under your cursor.", + "ally_keybinds": "Ally Keybinds", + "request_alliance": "Request Alliance", + "request_alliance_desc": "Send an alliance request to the player whose tile is under your cursor.", + "break_alliance": "Break Alliance (Betray)", + "break_alliance_desc": "Break alliance with the player whose tile is under your cursor.", "swap_direction": "Swap Rocket Direction", "swap_direction_desc": "Toggle rocket launch direction (up/down).", "zoom_controls": "Zoom Controls", @@ -760,6 +765,7 @@ "sent_emoji": "Sent {name}: {emoji}", "renew_alliance": "Request to renew", "request_alliance": "{name} requests an alliance!", + "alliance_request_sent": "Alliance request sent to {name}.", "focus": "Focus", "accept_alliance": "Accept", "reject_alliance": "Reject", diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index ef0981bebf..4eb8891d6d 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -31,7 +31,9 @@ import { getPersistentID } from "./Auth"; import { AutoUpgradeEvent, DoBoatAttackEvent, + DoBreakAllianceEvent, DoGroundAttackEvent, + DoRequestAllianceEvent, InputHandler, MouseMoveEvent, MouseUpEvent, @@ -40,8 +42,10 @@ import { import { endGame, startGame, startTime } from "./LocalPersistantStats"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { + SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, + SendBreakAllianceIntentEvent, SendHashEvent, SendSpawnIntentEvent, SendUpgradeStructureIntentEvent, @@ -348,6 +352,14 @@ export class ClientGameRunner { DoGroundAttackEvent, this.doGroundAttackUnderCursor.bind(this), ); + this.eventBus.on( + DoRequestAllianceEvent, + this.doRequestAllianceUnderCursor.bind(this), + ); + this.eventBus.on( + DoBreakAllianceEvent, + this.doBreakAllianceUnderCursor.bind(this), + ); this.renderer.initialize(); this.input.initialize(); @@ -687,6 +699,50 @@ export class ClientGameRunner { }); } + private doRequestAllianceUnderCursor(): void { + const tile = this.getTileUnderCursor(); + if (tile === null) return; + + const myPlayer = + this.myPlayer ?? this.gameView.playerByClientID(this.lobby.clientID); + if (myPlayer === null) return; + this.myPlayer = myPlayer; + + const tileOwner = this.gameView.owner(tile); + if (!tileOwner.isPlayer()) return; + const recipient = tileOwner as PlayerView; + + myPlayer.actions(tile).then((actions) => { + if (actions.interaction?.canSendAllianceRequest) { + this.eventBus.emit( + new SendAllianceRequestIntentEvent(myPlayer, recipient), + ); + } + }); + } + + private doBreakAllianceUnderCursor(): void { + const tile = this.getTileUnderCursor(); + if (tile === null) return; + + const myPlayer = + this.myPlayer ?? this.gameView.playerByClientID(this.lobby.clientID); + if (myPlayer === null) return; + this.myPlayer = myPlayer; + + const tileOwner = this.gameView.owner(tile); + if (!tileOwner.isPlayer()) return; + const recipient = tileOwner as PlayerView; + + myPlayer.actions(tile).then((actions) => { + if (actions.interaction?.canBreakAlliance) { + this.eventBus.emit( + new SendBreakAllianceIntentEvent(myPlayer, recipient), + ); + } + }); + } + private getTileUnderCursor(): TileRef | null { if (!this.isActive || !this.lastMousePosition) { return null; diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 45d1188a3b..42968e441a 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -110,6 +110,10 @@ export class DoBoatAttackEvent implements GameEvent {} export class DoGroundAttackEvent implements GameEvent {} +export class DoRequestAllianceEvent implements GameEvent {} + +export class DoBreakAllianceEvent implements GameEvent {} + export class AttackRatioEvent implements GameEvent { constructor(public readonly attackRatio: number) {} } @@ -225,6 +229,8 @@ export class InputHandler { buildAtomBomb: "Digit8", buildHydrogenBomb: "Digit9", buildMIRV: "Digit0", + requestAlliance: "KeyK", + breakAlliance: "KeyL", ...saved, }; @@ -441,6 +447,16 @@ export class InputHandler { this.setGhostStructure(UnitType.MIRV); } + if (e.code === this.keybinds.requestAlliance) { + e.preventDefault(); + this.eventBus.emit(new DoRequestAllianceEvent()); + } + + if (e.code === this.keybinds.breakAlliance) { + e.preventDefault(); + this.eventBus.emit(new DoBreakAllianceEvent()); + } + if (e.code === this.keybinds.swapDirection) { e.preventDefault(); const nextDirection = !this.uiState.rocketDirectionUp; diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 70f4251f80..d6048e14b7 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -35,6 +35,8 @@ const DefaultKeybinds: Record = { attackRatioUp: "KeyY", boatAttack: "KeyB", groundAttack: "KeyG", + requestAlliance: "KeyK", + breakAlliance: "KeyL", swapDirection: "KeyU", zoomOut: "KeyQ", zoomIn: "KeyE", @@ -669,6 +671,32 @@ export class UserSettingModal extends BaseModal { @change=${this.handleKeybindChange} > +

+ ${translateText("user_setting.ally_keybinds")} +

+ + + + +

diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index db68fe987a..2e090d6f86 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -30,6 +30,7 @@ import { CancelBoatIntentEvent, SendAllianceExtensionIntentEvent, SendAllianceReplyIntentEvent, + SendAllianceRequestIntentEvent, SendAttackIntentEvent, } from "../../Transport"; import { Layer } from "./Layer"; @@ -199,7 +200,26 @@ export class EventsDisplay extends LitElement implements Layer { this.outgoingBoats = []; } - init() {} + init() { + this.eventBus.on( + SendAllianceRequestIntentEvent, + this.onAllianceRequestSentConfirmation.bind(this), + ); + } + + private onAllianceRequestSentConfirmation(e: SendAllianceRequestIntentEvent) { + const myPlayer = this.game.myPlayer(); + if (!myPlayer || e.requestor.id() !== myPlayer.id()) { + return; + } + this.addEvent({ + description: translateText("events_display.alliance_request_sent", { + name: e.recipient.name(), + }), + type: MessageType.ALLIANCE_REQUEST, + createdAt: this.game.ticks(), + }); + } tick() { this.active = true;