mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:00:42 +00:00
## Description: About 30s before an alliance is about to expire, both players receive a prompt to extend the alliance. If both players agree the alliance is extended. ## 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: evan
This commit is contained in:
@@ -432,7 +432,12 @@
|
||||
},
|
||||
"events_display": {
|
||||
"retreating": "retreating",
|
||||
"boat": "Boat"
|
||||
"boat": "Boat",
|
||||
"about_to_expire": "Your alliance with {name} is about to expire!",
|
||||
"renew_alliance": "Request to renew",
|
||||
"focus": "Focus",
|
||||
"alliance_renewed": "Your alliance with {name} has been renewed",
|
||||
"ignore": "Ignore"
|
||||
},
|
||||
"unit_info_modal": {
|
||||
"structure_info": "Structure Info",
|
||||
|
||||
@@ -67,6 +67,10 @@ export class SendAllianceReplyIntentEvent implements GameEvent {
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendAllianceExtensionIntentEvent implements GameEvent {
|
||||
constructor(public readonly recipient: PlayerView) {}
|
||||
}
|
||||
|
||||
export class SendSpawnIntentEvent implements GameEvent {
|
||||
constructor(public readonly cell: Cell) {}
|
||||
}
|
||||
@@ -194,6 +198,9 @@ export class Transport {
|
||||
this.eventBus.on(SendAllianceReplyIntentEvent, (e) =>
|
||||
this.onAllianceRequestReplyUIEvent(e),
|
||||
);
|
||||
this.eventBus.on(SendAllianceExtensionIntentEvent, (e) =>
|
||||
this.onSendAllianceExtensionIntent(e),
|
||||
);
|
||||
this.eventBus.on(SendBreakAllianceIntentEvent, (e) =>
|
||||
this.onBreakAllianceRequestUIEvent(e),
|
||||
);
|
||||
@@ -419,6 +426,16 @@ export class Transport {
|
||||
});
|
||||
}
|
||||
|
||||
private onSendAllianceExtensionIntent(
|
||||
event: SendAllianceExtensionIntentEvent,
|
||||
) {
|
||||
this.sendIntent({
|
||||
type: "allianceExtension",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
recipient: event.recipient.id(),
|
||||
});
|
||||
}
|
||||
|
||||
private onSendSpawnIntentEvent(event: SendSpawnIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "spawn",
|
||||
|
||||
@@ -141,6 +141,7 @@ export function getMessageTypeClasses(type: MessageType): string {
|
||||
case MessageType.SAM_MISS:
|
||||
case MessageType.ALLIANCE_EXPIRED:
|
||||
case MessageType.NAVAL_INVASION_INBOUND:
|
||||
case MessageType.RENEW_ALLIANCE:
|
||||
return severityColors["warn"];
|
||||
case MessageType.CHAT:
|
||||
case MessageType.ALLIANCE_REQUEST:
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import {
|
||||
CancelAttackIntentEvent,
|
||||
CancelBoatIntentEvent,
|
||||
SendAllianceExtensionIntentEvent,
|
||||
SendAllianceReplyIntentEvent,
|
||||
} from "../../Transport";
|
||||
import { Layer } from "./Layer";
|
||||
@@ -45,7 +46,6 @@ import {
|
||||
GoToUnitEvent,
|
||||
} from "./Leaderboard";
|
||||
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { getMessageTypeClasses, translateText } from "../../Utils";
|
||||
|
||||
interface GameEvent {
|
||||
@@ -73,9 +73,11 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
public eventBus: EventBus;
|
||||
public game: GameView;
|
||||
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
private active: boolean = false;
|
||||
private events: GameEvent[] = [];
|
||||
|
||||
// allianceID -> last checked at tick
|
||||
private alliancesCheckedAt = new Map<number, Tick>();
|
||||
@state() private incomingAttacks: AttackUpdate[] = [];
|
||||
@state() private outgoingAttacks: AttackUpdate[] = [];
|
||||
@state() private outgoingLandAttacks: AttackUpdate[] = [];
|
||||
@@ -182,6 +184,8 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
return;
|
||||
}
|
||||
|
||||
this.checkForAllianceExpirations();
|
||||
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
if (updates) {
|
||||
for (const [ut, fn] of this.updateMap) {
|
||||
@@ -235,6 +239,64 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private checkForAllianceExpirations() {
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (!myPlayer) return;
|
||||
|
||||
for (const alliance of myPlayer.alliances()) {
|
||||
if (
|
||||
alliance.expiresAt >
|
||||
this.game.ticks() + this.game.config().allianceExtensionPromptOffset()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
(this.alliancesCheckedAt.get(alliance.id) ?? 0) >=
|
||||
this.game.ticks() - this.game.config().allianceExtensionPromptOffset()
|
||||
) {
|
||||
// We've already displayed a message for this alliance.
|
||||
continue;
|
||||
}
|
||||
|
||||
this.alliancesCheckedAt.set(alliance.id, this.game.ticks());
|
||||
|
||||
const other = this.game.player(alliance.other) as PlayerView;
|
||||
|
||||
this.addEvent({
|
||||
description: translateText("events_display.about_to_expire", {
|
||||
name: other.name(),
|
||||
}),
|
||||
type: MessageType.RENEW_ALLIANCE,
|
||||
duration: this.game.config().allianceExtensionPromptOffset() - 3 * 10, // 3 second buffer
|
||||
buttons: [
|
||||
{
|
||||
text: translateText("events_display.focus"),
|
||||
className: "btn-gray",
|
||||
action: () => this.eventBus.emit(new GoToPlayerEvent(other)),
|
||||
preventClose: true,
|
||||
},
|
||||
{
|
||||
text: translateText("events_display.renew_alliance", {
|
||||
name: other.name(),
|
||||
}),
|
||||
className: "btn",
|
||||
action: () =>
|
||||
this.eventBus.emit(new SendAllianceExtensionIntentEvent(other)),
|
||||
},
|
||||
{
|
||||
text: translateText("events_display.ignore"),
|
||||
className: "btn-info",
|
||||
action: () => {},
|
||||
},
|
||||
],
|
||||
highlight: true,
|
||||
createdAt: this.game.ticks(),
|
||||
focusID: other.smallID(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private addEvent(event: GameEvent) {
|
||||
this.events = [...this.events, event];
|
||||
if (this._hidden === true) {
|
||||
|
||||
@@ -176,11 +176,9 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
if (myPlayer !== null && myPlayer.isAlive()) {
|
||||
this.actions = await myPlayer.actions(this.tile);
|
||||
|
||||
if (this.actions?.interaction?.allianceCreatedAtTick !== undefined) {
|
||||
const createdAt = this.actions.interaction.allianceCreatedAtTick;
|
||||
const durationTicks = this.g.config().allianceDuration();
|
||||
const expiryTick = createdAt + durationTicks;
|
||||
const remainingTicks = expiryTick - this.g.ticks();
|
||||
if (this.actions?.interaction?.allianceExpiresAt !== undefined) {
|
||||
const expiresAt = this.actions.interaction.allianceExpiresAt;
|
||||
const remainingTicks = expiresAt - this.g.ticks();
|
||||
|
||||
if (remainingTicks > 0) {
|
||||
const remainingSeconds = Math.max(
|
||||
|
||||
@@ -196,12 +196,13 @@ export class GameRunner {
|
||||
};
|
||||
const alliance = player.allianceWith(other as Player);
|
||||
if (alliance) {
|
||||
actions.interaction.allianceCreatedAtTick = alliance.createdAt();
|
||||
actions.interaction.allianceExpiresAt = alliance.expiresAt();
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
public playerProfile(playerID: number): PlayerProfile {
|
||||
const player = this.game.playerBySmallID(playerID);
|
||||
if (!player.isPlayer()) {
|
||||
|
||||
@@ -25,6 +25,7 @@ export type Intent =
|
||||
| CancelBoatIntent
|
||||
| AllianceRequestIntent
|
||||
| AllianceRequestReplyIntent
|
||||
| AllianceExtensionIntent
|
||||
| BreakAllianceIntent
|
||||
| TargetPlayerIntent
|
||||
| EmojiIntent
|
||||
@@ -67,6 +68,9 @@ export type QuickChatIntent = z.infer<typeof QuickChatIntentSchema>;
|
||||
export type MarkDisconnectedIntent = z.infer<
|
||||
typeof MarkDisconnectedIntentSchema
|
||||
>;
|
||||
export type AllianceExtensionIntent = z.infer<
|
||||
typeof AllianceExtensionIntentSchema
|
||||
>;
|
||||
|
||||
export type Turn = z.infer<typeof TurnSchema>;
|
||||
export type GameConfig = z.infer<typeof GameConfigSchema>;
|
||||
@@ -213,6 +217,11 @@ const BaseIntentSchema = z.object({
|
||||
clientID: ID,
|
||||
});
|
||||
|
||||
export const AllianceExtensionIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("allianceExtension"),
|
||||
recipient: ID,
|
||||
});
|
||||
|
||||
export const AttackIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("attack"),
|
||||
targetID: ID.nullable(),
|
||||
@@ -354,6 +363,7 @@ const IntentSchema = z.discriminatedUnion("type", [
|
||||
EmbargoIntentSchema,
|
||||
MoveWarshipIntentSchema,
|
||||
QuickChatIntentSchema,
|
||||
AllianceExtensionIntentSchema,
|
||||
]);
|
||||
|
||||
//
|
||||
|
||||
@@ -158,6 +158,7 @@ export interface Config {
|
||||
nukeDeathFactor(humans: number, tilesOwned: number): number;
|
||||
structureMinDist(): number;
|
||||
isReplay(): boolean;
|
||||
allianceExtensionPromptOffset(): number;
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
|
||||
@@ -839,4 +839,8 @@ export class DefaultConfig implements Config {
|
||||
defensePostTargettingRange(): number {
|
||||
return 75;
|
||||
}
|
||||
|
||||
allianceExtensionPromptOffset(): number {
|
||||
return 300; // 30 seconds before expiration
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Execution, Game } from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { ClientID, GameID, Intent, Turn } from "../Schemas";
|
||||
import { simpleHash } from "../Util";
|
||||
import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution";
|
||||
import { AllianceRequestExecution } from "./alliance/AllianceRequestExecution";
|
||||
import { AllianceRequestReplyExecution } from "./alliance/AllianceRequestReplyExecution";
|
||||
import { BreakAllianceExecution } from "./alliance/BreakAllianceExecution";
|
||||
@@ -111,6 +112,10 @@ export class Executor {
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
intent.unit,
|
||||
);
|
||||
case "allianceExtension": {
|
||||
return new AllianceExtensionExecution(player, intent.recipient);
|
||||
}
|
||||
|
||||
case "upgrade_structure":
|
||||
return new UpgradeStructureExecution(player, intent.unitId);
|
||||
case "create_station":
|
||||
|
||||
@@ -72,10 +72,7 @@ export class PlayerExecution implements Execution {
|
||||
|
||||
const alliances = Array.from(this.player.alliances());
|
||||
for (const alliance of alliances) {
|
||||
if (
|
||||
this.mg.ticks() - alliance.createdAt() >
|
||||
this.mg.config().allianceDuration()
|
||||
) {
|
||||
if (alliance.expiresAt() <= this.mg.ticks()) {
|
||||
alliance.expire();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
Execution,
|
||||
Game,
|
||||
MessageType,
|
||||
Player,
|
||||
PlayerID,
|
||||
} from "../../game/Game";
|
||||
|
||||
export class AllianceExtensionExecution implements Execution {
|
||||
constructor(
|
||||
private readonly from: Player,
|
||||
private readonly toID: PlayerID,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.toID)) {
|
||||
console.warn(
|
||||
`[AllianceExtensionExecution] Player ${this.toID} not found`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const to = mg.player(this.toID);
|
||||
const alliance = this.from.allianceWith(to);
|
||||
if (!alliance) {
|
||||
console.warn(
|
||||
`[AllianceExtensionExecution] No alliance to extend between ${this.from.id()} and ${this.toID}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark this player's intent to extend
|
||||
alliance.addExtensionRequest(this.from);
|
||||
|
||||
if (alliance.canExtend()) {
|
||||
alliance.extend();
|
||||
|
||||
mg.displayMessage(
|
||||
"alliance.renewed",
|
||||
MessageType.ALLIANCE_ACCEPTED,
|
||||
this.from.id(),
|
||||
);
|
||||
mg.displayMessage(
|
||||
"alliance.renewed",
|
||||
MessageType.ALLIANCE_ACCEPTED,
|
||||
this.toID,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
// No-op
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
import { Game, MutableAlliance, Player, Tick } from "./Game";
|
||||
|
||||
export class AllianceImpl implements MutableAlliance {
|
||||
private extensionRequestedRequestor_: boolean = false;
|
||||
private extensionRequestedRecipient_: boolean = false;
|
||||
|
||||
private expiresAt_: Tick;
|
||||
|
||||
constructor(
|
||||
private readonly mg: Game,
|
||||
readonly requestor_: Player,
|
||||
readonly recipient_: Player,
|
||||
readonly createdAtTick_: Tick,
|
||||
) {}
|
||||
private readonly createdAt_: Tick,
|
||||
private readonly id_: number,
|
||||
) {
|
||||
this.expiresAt_ = createdAt_ + mg.config().allianceDuration();
|
||||
}
|
||||
|
||||
other(player: Player): Player {
|
||||
if (this.requestor_ === player) {
|
||||
@@ -24,10 +32,38 @@ export class AllianceImpl implements MutableAlliance {
|
||||
}
|
||||
|
||||
createdAt(): Tick {
|
||||
return this.createdAtTick_;
|
||||
return this.createdAt_;
|
||||
}
|
||||
|
||||
expire(): void {
|
||||
this.mg.expireAlliance(this);
|
||||
}
|
||||
|
||||
addExtensionRequest(player: Player): void {
|
||||
if (this.requestor_ === player) {
|
||||
this.extensionRequestedRequestor_ = true;
|
||||
} else if (this.recipient_ === player) {
|
||||
this.extensionRequestedRecipient_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
canExtend(): boolean {
|
||||
return (
|
||||
this.extensionRequestedRequestor_ && this.extensionRequestedRecipient_
|
||||
);
|
||||
}
|
||||
|
||||
public id(): number {
|
||||
return this.id_;
|
||||
}
|
||||
|
||||
extend(): void {
|
||||
this.extensionRequestedRequestor_ = false;
|
||||
this.extensionRequestedRecipient_ = false;
|
||||
this.expiresAt_ = this.mg.ticks() + this.mg.config().allianceDuration();
|
||||
}
|
||||
|
||||
expiresAt(): Tick {
|
||||
return this.expiresAt_;
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -339,12 +339,17 @@ export interface Alliance {
|
||||
requestor(): Player;
|
||||
recipient(): Player;
|
||||
createdAt(): Tick;
|
||||
expiresAt(): Tick;
|
||||
other(player: Player): Player;
|
||||
}
|
||||
|
||||
export interface MutableAlliance extends Alliance {
|
||||
expire(): void;
|
||||
other(player: Player): Player;
|
||||
canExtend(): boolean;
|
||||
addExtensionRequest(player: Player): void;
|
||||
id(): number;
|
||||
extend(): void;
|
||||
}
|
||||
|
||||
export class PlayerInfo {
|
||||
@@ -536,6 +541,7 @@ export interface Player {
|
||||
incomingAllianceRequests(): AllianceRequest[];
|
||||
outgoingAllianceRequests(): AllianceRequest[];
|
||||
alliances(): MutableAlliance[];
|
||||
expiredAlliances(): Alliance[];
|
||||
allies(): Player[];
|
||||
isAlliedWith(other: Player): boolean;
|
||||
allianceWith(other: Player): MutableAlliance | null;
|
||||
@@ -591,7 +597,6 @@ export interface Player {
|
||||
}
|
||||
|
||||
export interface Game extends GameMap {
|
||||
expireAlliance(alliance: Alliance);
|
||||
// Map & Dimensions
|
||||
isOnMap(cell: Cell): boolean;
|
||||
width(): number;
|
||||
@@ -613,6 +618,10 @@ export interface Game extends GameMap {
|
||||
|
||||
teams(): Team[];
|
||||
|
||||
// Alliances
|
||||
alliances(): MutableAlliance[];
|
||||
expireAlliance(alliance: Alliance): void;
|
||||
|
||||
// Game State
|
||||
ticks(): Tick;
|
||||
inSpawnPhase(): boolean;
|
||||
@@ -694,7 +703,7 @@ export interface PlayerInteraction {
|
||||
canTarget: boolean;
|
||||
canDonate: boolean;
|
||||
canEmbargo: boolean;
|
||||
allianceCreatedAtTick?: Tick;
|
||||
allianceExpiresAt?: Tick;
|
||||
}
|
||||
|
||||
export interface EmojiMessage {
|
||||
@@ -729,6 +738,7 @@ export enum MessageType {
|
||||
SENT_TROOPS_TO_PLAYER,
|
||||
RECEIVED_TROOPS_FROM_PLAYER,
|
||||
CHAT,
|
||||
RENEW_ALLIANCE,
|
||||
}
|
||||
|
||||
// Message categories used for filtering events in the EventsDisplay
|
||||
@@ -759,6 +769,7 @@ export const MESSAGE_TYPE_CATEGORIES: Record<MessageType, MessageCategory> = {
|
||||
[MessageType.ALLIANCE_REQUEST]: MessageCategory.ALLIANCE,
|
||||
[MessageType.ALLIANCE_BROKEN]: MessageCategory.ALLIANCE,
|
||||
[MessageType.ALLIANCE_EXPIRED]: MessageCategory.ALLIANCE,
|
||||
[MessageType.RENEW_ALLIANCE]: MessageCategory.ALLIANCE,
|
||||
[MessageType.SENT_GOLD_TO_PLAYER]: MessageCategory.TRADE,
|
||||
[MessageType.RECEIVED_GOLD_FROM_PLAYER]: MessageCategory.TRADE,
|
||||
[MessageType.RECEIVED_GOLD_FROM_TRADE]: MessageCategory.TRADE,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
GameMode,
|
||||
GameUpdates,
|
||||
MessageType,
|
||||
MutableAlliance,
|
||||
Nation,
|
||||
Player,
|
||||
PlayerID,
|
||||
@@ -77,6 +78,9 @@ export class GameImpl implements Game {
|
||||
private botTeam: Team = ColoredTeams.Bot;
|
||||
private _railNetwork: RailNetwork = createRailNetwork(this);
|
||||
|
||||
// Used to assign unique IDs to each new alliance
|
||||
private nextAllianceID: number = 0;
|
||||
|
||||
constructor(
|
||||
private _humans: PlayerInfo[],
|
||||
private _nations: Nation[],
|
||||
@@ -146,6 +150,11 @@ export class GameImpl implements Game {
|
||||
owner(ref: TileRef): Player | TerraNullius {
|
||||
return this.playerBySmallID(this.ownerID(ref));
|
||||
}
|
||||
|
||||
alliances(): MutableAlliance[] {
|
||||
return this.alliances_;
|
||||
}
|
||||
|
||||
playerBySmallID(id: number): Player | TerraNullius {
|
||||
if (id === 0) {
|
||||
return this.terraNullius();
|
||||
@@ -231,11 +240,20 @@ export class GameImpl implements Game {
|
||||
const requestor = request.requestor();
|
||||
const recipient = request.recipient();
|
||||
|
||||
const existing = requestor.allianceWith(recipient);
|
||||
if (existing) {
|
||||
throw new Error(
|
||||
`cannot accept alliance request, already allied with ${recipient.name()}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Create and register the new alliance
|
||||
const alliance = new AllianceImpl(
|
||||
this,
|
||||
requestor as PlayerImpl,
|
||||
recipient as PlayerImpl,
|
||||
this._ticks,
|
||||
this.nextAllianceID++,
|
||||
);
|
||||
this.alliances_.push(alliance);
|
||||
(request.requestor() as PlayerImpl).pastOutgoingAllianceRequests.push(
|
||||
|
||||
@@ -36,6 +36,7 @@ export enum GameUpdateType {
|
||||
AllianceRequestReply,
|
||||
BrokeAlliance,
|
||||
AllianceExpired,
|
||||
AllianceExtension,
|
||||
TargetPlayer,
|
||||
Emoji,
|
||||
Win,
|
||||
@@ -60,6 +61,7 @@ export type GameUpdate =
|
||||
| WinUpdate
|
||||
| HashUpdate
|
||||
| UnitIncomingUpdate
|
||||
| AllianceExtensionUpdate
|
||||
| BonusEventUpdate
|
||||
| RailroadUpdate;
|
||||
|
||||
@@ -155,10 +157,18 @@ export interface PlayerUpdate {
|
||||
outgoingAttacks: AttackUpdate[];
|
||||
incomingAttacks: AttackUpdate[];
|
||||
outgoingAllianceRequests: PlayerID[];
|
||||
alliances: AllianceView[];
|
||||
hasSpawned: boolean;
|
||||
betrayals?: bigint;
|
||||
}
|
||||
|
||||
export interface AllianceView {
|
||||
id: number;
|
||||
other: PlayerID;
|
||||
createdAt: Tick;
|
||||
expiresAt: Tick;
|
||||
}
|
||||
|
||||
export interface AllianceRequestUpdate {
|
||||
type: GameUpdateType.AllianceRequest;
|
||||
requestorID: number;
|
||||
@@ -184,6 +194,12 @@ export interface AllianceExpiredUpdate {
|
||||
player2ID: number;
|
||||
}
|
||||
|
||||
export interface AllianceExtensionUpdate {
|
||||
type: GameUpdateType.AllianceExtension;
|
||||
playerID: number;
|
||||
allianceID: number;
|
||||
}
|
||||
|
||||
export interface TargetPlayerUpdate {
|
||||
type: GameUpdateType.TargetPlayer;
|
||||
playerID: number;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "./Game";
|
||||
import { GameMap, TileRef, TileUpdate } from "./GameMap";
|
||||
import {
|
||||
AllianceView,
|
||||
AttackUpdate,
|
||||
GameUpdateType,
|
||||
GameUpdateViewData,
|
||||
@@ -291,6 +292,10 @@ export class PlayerView {
|
||||
return this.data.outgoingAllianceRequests.some((id) => other.id() === id);
|
||||
}
|
||||
|
||||
alliances(): AllianceView[] {
|
||||
return this.data.alliances;
|
||||
}
|
||||
|
||||
hasEmbargoAgainst(other: PlayerView): boolean {
|
||||
return this.data.embargoes.has(other.id());
|
||||
}
|
||||
|
||||
@@ -42,7 +42,12 @@ import {
|
||||
} from "./Game";
|
||||
import { GameImpl } from "./GameImpl";
|
||||
import { andFN, manhattanDistFN, TileRef } from "./GameMap";
|
||||
import { AttackUpdate, GameUpdateType, PlayerUpdate } from "./GameUpdates";
|
||||
import {
|
||||
AllianceView,
|
||||
AttackUpdate,
|
||||
GameUpdateType,
|
||||
PlayerUpdate,
|
||||
} from "./GameUpdates";
|
||||
import {
|
||||
bestShoreDeploymentSource,
|
||||
canBuildTransportShip,
|
||||
@@ -85,6 +90,7 @@ export class PlayerImpl implements Player {
|
||||
private _displayName: string;
|
||||
|
||||
public pastOutgoingAllianceRequests: AllianceRequest[] = [];
|
||||
private _expiredAlliances: Alliance[] = [];
|
||||
|
||||
private targets_: Target[] = [];
|
||||
|
||||
@@ -166,6 +172,15 @@ export class PlayerImpl implements Player {
|
||||
} satisfies AttackUpdate;
|
||||
}),
|
||||
outgoingAllianceRequests: outgoingAllianceRequests,
|
||||
alliances: this.alliances().map(
|
||||
(a) =>
|
||||
({
|
||||
id: a.id(),
|
||||
other: a.other(this).id(),
|
||||
createdAt: a.createdAt(),
|
||||
expiresAt: a.expiresAt(),
|
||||
}) satisfies AllianceView,
|
||||
),
|
||||
hasSpawned: this.hasSpawned(),
|
||||
betrayals: stats?.betrayals,
|
||||
};
|
||||
@@ -342,6 +357,10 @@ export class PlayerImpl implements Player {
|
||||
);
|
||||
}
|
||||
|
||||
expiredAlliances(): Alliance[] {
|
||||
return [...this._expiredAlliances];
|
||||
}
|
||||
|
||||
allies(): Player[] {
|
||||
return this.alliances().map((a) => a.other(this));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { AllianceExtensionExecution } from "../src/core/execution/alliance/AllianceExtensionExecution";
|
||||
import { AllianceRequestExecution } from "../src/core/execution/alliance/AllianceRequestExecution";
|
||||
import { AllianceRequestReplyExecution } from "../src/core/execution/alliance/AllianceRequestReplyExecution";
|
||||
import { Game, Player, PlayerType } from "../src/core/game/Game";
|
||||
import { playerInfo, setup } from "./util/Setup";
|
||||
|
||||
let game: Game;
|
||||
let player1: Player;
|
||||
let player2: Player;
|
||||
|
||||
describe("AllianceExtensionExecution", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup(
|
||||
"ocean_and_land",
|
||||
{
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
infiniteTroops: true,
|
||||
},
|
||||
[
|
||||
playerInfo("player1", PlayerType.Human),
|
||||
playerInfo("player2", PlayerType.Human),
|
||||
],
|
||||
);
|
||||
|
||||
player1 = game.player("player1");
|
||||
player2 = game.player("player2");
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
test("Successfully extends existing alliance", () => {
|
||||
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
|
||||
game.addExecution(
|
||||
new AllianceRequestReplyExecution(player1.id(), player2, true),
|
||||
);
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
|
||||
expect(player1.allianceWith(player2)).toBeTruthy();
|
||||
expect(player2.allianceWith(player1)).toBeTruthy();
|
||||
|
||||
const allianceBefore = player1.allianceWith(player2)!;
|
||||
const expirationBefore =
|
||||
allianceBefore.createdAt() + game.config().allianceDuration();
|
||||
|
||||
game.addExecution(new AllianceExtensionExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
|
||||
const allianceAfter = player1.allianceWith(player2)!;
|
||||
|
||||
expect(allianceAfter.id()).toBe(allianceBefore.id());
|
||||
|
||||
const expirationAfter =
|
||||
allianceAfter.createdAt() + game.config().allianceDuration();
|
||||
|
||||
expect(expirationAfter).toBeGreaterThanOrEqual(expirationBefore);
|
||||
});
|
||||
|
||||
test("Fails gracefully if no alliance exists", () => {
|
||||
game.addExecution(new AllianceExtensionExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
|
||||
expect(player1.allianceWith(player2)).toBeFalsy();
|
||||
expect(player2.allianceWith(player1)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user