diff --git a/src/client/graphics/layers/PlayerActionHandler.ts b/src/client/graphics/layers/PlayerActionHandler.ts index 54714cadbf..2ccaef0459 100644 --- a/src/client/graphics/layers/PlayerActionHandler.ts +++ b/src/client/graphics/layers/PlayerActionHandler.ts @@ -3,6 +3,7 @@ import { PlayerActions } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; import { PlayerView } from "../../../core/game/GameView"; import { + SendAllianceExtensionIntentEvent, SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, @@ -63,6 +64,10 @@ export class PlayerActionHandler { this.eventBus.emit(new SendAllianceRequestIntentEvent(player, recipient)); } + handleExtendAlliance(recipient: PlayerView) { + this.eventBus.emit(new SendAllianceExtensionIntentEvent(recipient)); + } + handleBreakAlliance(player: PlayerView, recipient: PlayerView) { this.eventBus.emit(new SendBreakAllianceIntentEvent(player, recipient)); } diff --git a/src/client/graphics/layers/RadialMenuElements.ts b/src/client/graphics/layers/RadialMenuElements.ts index aeafdf8a10..d7ced8acb2 100644 --- a/src/client/graphics/layers/RadialMenuElements.ts +++ b/src/client/graphics/layers/RadialMenuElements.ts @@ -209,6 +209,20 @@ const allyRequestElement: MenuElement = { }, }; +const allyExtendElement: MenuElement = { + id: "ally_extend", + name: "extend", + disabled: (params: MenuElementParams) => false, + displayed: (params: MenuElementParams) => + !!params.playerActions?.interaction?.canExtendAlliance, + color: COLORS.boat, + icon: allianceIcon, + action: (params: MenuElementParams) => { + params.playerActionHandler.handleExtendAlliance(params.selected!); + params.closeMenu(); + }, +}; + const allyBreakElement: MenuElement = { id: "ally_break", name: "break", @@ -624,13 +638,16 @@ export const rootMenuElement: MenuElement = { tileOwner.isPlayer() && (tileOwner as PlayerView).id() === params.myPlayer.id(); + const canExtendAlliance = + params.playerActions.interaction?.canExtendAlliance; + const menuItems: (MenuElement | null)[] = [ infoMenuElement, ...(isOwnTerritory ? [deleteUnitElement, allyRequestElement, buildMenuElement] : [ isAllied ? allyBreakElement : boatMenuElement, - allyRequestElement, + canExtendAlliance ? allyExtendElement : allyRequestElement, isFriendlyTarget(params) && !isDisconnectedTarget(params) ? donateGoldRadialElement : attackMenuElement, diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 0f93a94f68..e9be9a768f 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -207,6 +207,7 @@ export class GameRunner { canSendEmoji: player.canSendEmoji(other), canTarget: player.canTarget(other), canSendAllianceRequest: player.canSendAllianceRequest(other), + canExtendAlliance: player.canExtendAlliance(other), canBreakAlliance: player.isAlliedWith(other), canDonateGold: player.canDonateGold(other), canDonateTroops: player.canDonateTroops(other), diff --git a/src/core/game/AllianceImpl.ts b/src/core/game/AllianceImpl.ts index fa74ca766c..5b60bd59a9 100644 --- a/src/core/game/AllianceImpl.ts +++ b/src/core/game/AllianceImpl.ts @@ -62,6 +62,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_; } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index f291e11214..9c8e5730e4 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -450,6 +450,8 @@ export interface MutableAlliance extends Alliance { id(): number; extend(): void; onlyOneAgreedToExtend(): boolean; + + agreedToExtend(player: Player): boolean; } export class PlayerInfo { @@ -658,6 +660,7 @@ export interface Player { isAlliedWith(other: Player): boolean; allianceWith(other: Player): MutableAlliance | null; canSendAllianceRequest(other: Player): boolean; + canExtendAlliance(other: Player): boolean; breakAlliance(alliance: Alliance): void; createAllianceRequest(recipient: Player): AllianceRequest | null; betrayals(): number; @@ -858,6 +861,7 @@ export interface PlayerInteraction { sharedBorder: boolean; canSendEmoji: boolean; canSendAllianceRequest: boolean; + canExtendAlliance: boolean; canBreakAlliance: boolean; canTarget: boolean; canDonateGold: boolean; diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index fae23df8eb..c603bbca81 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -400,6 +400,38 @@ export class PlayerImpl implements Player { ); } + canExtendAlliance(other: Player): boolean { + if (other === this) { + return false; + } + + if (this.isDisconnected() || other.isDisconnected()) { + // Disconnected players are marked as not-friendly even if they are allies, + // so we need to return early if either player is disconnected. + // Otherwise we could end up sending an alliance extension to someone + // we are already allied with. + return false; + } + if (!this.allianceWith(other) || !this.isAlive() || !other.isAlive()) { + return false; + } + + const alliance = this.allianceWith(other); + + if (!alliance) { + return false; + } + + if ( + alliance.expiresAt() > + this.mg.ticks() + this.mg.config().allianceExtensionPromptOffset() + ) { + return false; + } + + return !alliance.agreedToExtend(this); + } + canSendAllianceRequest(other: Player): boolean { if (other === this) { return false;