mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-02 20:28:14 +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:
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user