Add alliance renewal action to Radial Menu (#3148)

## Description:

The following PR replaces the (disabled) alliance request button with an
alliance extension/renewal button when the alliance with the target
player is expiring.

Agreeing to renewal via radial menu also hides the message in the
EventsDisplay.

<img width="369" height="364" alt="image"
src="https://github.com/user-attachments/assets/d8040f5c-ad7b-47d0-852f-925ecbf273a8"
/>


https://github.com/user-attachments/assets/aa589edf-6505-46bf-88a3-aa4c2df9137f

Icon size adjusted:

<img width="294" height="252" alt="image"
src="https://github.com/user-attachments/assets/7ca63500-b1fb-427b-965c-cf121a5213da"
/>

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

## Please put your Discord username so you can be contacted if a bug or
regression is found:

deshack_82603

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Mattia Migliorini
2026-02-20 02:47:57 +01:00
committed by GitHub
parent f6a08e16db
commit 90204f6628
10 changed files with 491 additions and 36 deletions
+1 -4
View File
@@ -223,11 +223,8 @@ export class GameRunner {
canDonateGold: player.canDonateGold(other),
canDonateTroops: player.canDonateTroops(other),
canEmbargo: !player.hasEmbargoAgainst(other),
allianceInfo: player.allianceInfo(other) ?? undefined,
};
const alliance = player.allianceWith(other as Player);
if (alliance) {
actions.interaction.allianceExpiresAt = alliance.expiresAt();
}
}
return actions;
+13
View File
@@ -1,4 +1,5 @@
import { Game, MutableAlliance, Player, Tick } from "./Game";
import { GameUpdateType } from "./GameUpdates";
export class AllianceImpl implements MutableAlliance {
private extensionRequestedRequestor_: boolean = false;
@@ -45,6 +46,11 @@ export class AllianceImpl implements MutableAlliance {
} else if (this.recipient_ === player) {
this.extensionRequestedRecipient_ = true;
}
this.mg.addUpdate({
type: GameUpdateType.AllianceExtension,
playerID: player.smallID(),
allianceID: this.id_,
});
}
bothAgreedToExtend(): boolean {
@@ -62,6 +68,13 @@ export class AllianceImpl implements MutableAlliance {
);
}
agreedToExtend(player: Player): boolean {
return (
(this.requestor_ === player && this.extensionRequestedRequestor_) ||
(this.recipient_ === player && this.extensionRequestedRecipient_)
);
}
public id(): number {
return this.id_;
}
+12 -1
View File
@@ -454,6 +454,8 @@ export interface MutableAlliance extends Alliance {
id(): number;
extend(): void;
onlyOneAgreedToExtend(): boolean;
agreedToExtend(player: Player): boolean;
}
export class PlayerInfo {
@@ -661,6 +663,7 @@ export interface Player {
allies(): Player[];
isAlliedWith(other: Player): boolean;
allianceWith(other: Player): MutableAlliance | null;
allianceInfo(other: Player): AllianceInfo | null;
canSendAllianceRequest(other: Player): boolean;
breakAlliance(alliance: Alliance): void;
removeAllAlliances(): void;
@@ -862,6 +865,14 @@ export interface PlayerBorderTiles {
borderTiles: ReadonlySet<TileRef>;
}
export interface AllianceInfo {
expiresAt: Tick;
inExtensionWindow: boolean;
myPlayerAgreedToExtend: boolean;
otherAgreedToExtend: boolean;
canExtend: boolean;
}
export interface PlayerInteraction {
sharedBorder: boolean;
canSendEmoji: boolean;
@@ -871,7 +882,7 @@ export interface PlayerInteraction {
canDonateGold: boolean;
canDonateTroops: boolean;
canEmbargo: boolean;
allianceExpiresAt?: Tick;
allianceInfo?: AllianceInfo;
}
export interface EmojiMessage {
+25
View File
@@ -12,6 +12,7 @@ import {
import { AttackImpl } from "./AttackImpl";
import {
Alliance,
AllianceInfo,
AllianceRequest,
AllPlayers,
Attack,
@@ -450,6 +451,30 @@ export class PlayerImpl implements Player {
);
}
allianceInfo(other: Player): AllianceInfo | null {
const alliance = this.allianceWith(other);
if (!alliance) {
return null;
}
const inExtensionWindow =
alliance.expiresAt() <=
this.mg.ticks() + this.mg.config().allianceExtensionPromptOffset();
const canExtend =
!this.isDisconnected() &&
!other.isDisconnected() &&
this.isAlive() &&
other.isAlive() &&
inExtensionWindow &&
!alliance.agreedToExtend(this);
return {
expiresAt: alliance.expiresAt(),
inExtensionWindow,
myPlayerAgreedToExtend: alliance.agreedToExtend(this),
otherAgreedToExtend: alliance.agreedToExtend(other),
canExtend,
};
}
canSendAllianceRequest(other: Player): boolean {
if (other === this) {
return false;