Feat: Alliance and betrayal hotkeys (#3110)

Original Feature request by @FloPinguin  
Resolves #3077 

## Description:

Adds hotkeys for Requesting alliances and breaking alliances. This
allows for players to send or break alliances whose tile is under the
cursor, without opening the radial menu.

Keybinds:
New "Ally Keybinds" section in Settings -> Keybinds
Request alliance: Default: K - sends an alliance request to the
player/bot/nation under the cursor
Break alliance: Default: L - breaks the alliance with the player at the
cursor

Behavior:
- Cursor must be over a tile owned by the target player. The action runs
only when the game allows it, following the same logic as the radial
menu. (canSendAllianceRequest and canBreakAlliance)
- When an alliance request is sent, the events log shows: "Alliance
request sent to [target]" for confirmation. No extra message for
breaking an alliance (betrayal/debuff message already exists and is sent
upon breaking an alliance)

## Screenshots:
Keybind menu:
<img width="739" height="595" alt="image"
src="https://github.com/user-attachments/assets/ee958eab-fd50-4971-85c5-dfd49c6f0bdc"
/>
In game logs:
<img width="373" height="232" alt="image"
src="https://github.com/user-attachments/assets/2cf6bb07-5f0d-425a-82d3-65a44fef99c5"
/>

## 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


Discord username:
_Dave9595_
This commit is contained in:
Jarifa
2026-04-21 23:34:51 +02:00
committed by GitHub
parent eedb90ffb5
commit 0801798fbd
6 changed files with 132 additions and 1 deletions
+6
View File
@@ -633,6 +633,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",
@@ -818,6 +823,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",
+64
View File
@@ -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,
@@ -387,6 +391,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();
@@ -760,6 +772,58 @@ export class ClientGameRunner {
});
}
private doRequestAllianceUnderCursor(): void {
const tile = this.getTileUnderCursor();
if (tile === null) return;
if (this.myPlayer === null) {
if (!this.clientID) return;
const myPlayer = this.gameView.playerByClientID(this.clientID);
if (myPlayer === null) return;
this.myPlayer = myPlayer;
}
const myPlayer = this.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;
if (this.myPlayer === null) {
if (!this.clientID) return;
const myPlayer = this.gameView.playerByClientID(this.clientID);
if (myPlayer === null) return;
this.myPlayer = myPlayer;
}
const myPlayer = this.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;
+14
View File
@@ -153,6 +153,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) {}
}
@@ -521,6 +525,16 @@ export class InputHandler {
this.setGhostStructure(matchedBuild);
}
if (this.keybindMatchesEvent(e, this.keybinds.requestAlliance)) {
e.preventDefault();
this.eventBus.emit(new DoRequestAllianceEvent());
}
if (this.keybindMatchesEvent(e, this.keybinds.breakAlliance)) {
e.preventDefault();
this.eventBus.emit(new DoBreakAllianceEvent());
}
if (this.keybindMatchesEvent(e, this.keybinds.swapDirection)) {
e.preventDefault();
const nextDirection = !this.uiState.rocketDirectionUp;
+26
View File
@@ -657,6 +657,32 @@ export class UserSettingModal extends BaseModal {
@change=${this.handleKeybindChange}
></setting-keybind>
<h2
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
>
${translateText("user_setting.ally_keybinds")}
</h2>
<setting-keybind
action="requestAlliance"
label=${translateText("user_setting.request_alliance")}
description=${translateText("user_setting.request_alliance_desc")}
defaultKey="KeyK"
.value=${this.getKeyValue("requestAlliance")}
.display=${this.getKeyChar("requestAlliance")}
@change=${this.handleKeybindChange}
></setting-keybind>
<setting-keybind
action="breakAlliance"
label=${translateText("user_setting.break_alliance")}
description=${translateText("user_setting.break_alliance_desc")}
defaultKey="KeyL"
.value=${this.getKeyValue("breakAlliance")}
.display=${this.getKeyChar("breakAlliance")}
@change=${this.handleKeybindChange}
></setting-keybind>
<h2
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
>
+20 -1
View File
@@ -190,7 +190,26 @@ export class EventsDisplay extends LitElement implements Layer {
this.events = [];
}
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;
+2
View File
@@ -19,6 +19,8 @@ export function getDefaultKeybinds(isMac: boolean): Record<string, string> {
attackRatioUp: "KeyY",
boatAttack: "KeyB",
groundAttack: "KeyG",
requestAlliance: "KeyK",
breakAlliance: "KeyL",
swapDirection: "KeyU",
zoomOut: "KeyQ",
zoomIn: "KeyE",