mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-30 21:52:04 +00:00
Merge v25 into main
This commit is contained in:
@@ -41,9 +41,8 @@ export async function handlePurchase(priceId: string) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
priceId: priceId,
|
||||
|
||||
successUrl: `${window.location.href}purchase-success`,
|
||||
cancelUrl: `${window.location.href}purchase-cancel`,
|
||||
successUrl: `${window.location.origin}#purchase-completed=true`,
|
||||
cancelUrl: `${window.location.origin}#purchase-completed=false`,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -11,6 +11,21 @@ export class DarkModeButton extends LitElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("dark-mode-changed", this.handleDarkModeChanged);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("dark-mode-changed", this.handleDarkModeChanged);
|
||||
}
|
||||
|
||||
private handleDarkModeChanged = (e: Event) => {
|
||||
const event = e as CustomEvent<{ darkMode: boolean }>;
|
||||
this.darkMode = event.detail.darkMode;
|
||||
};
|
||||
|
||||
toggleDarkMode() {
|
||||
this.userSettings.toggleDarkMode();
|
||||
this.darkMode = this.userSettings.darkMode();
|
||||
|
||||
@@ -47,7 +47,7 @@ export class HostLobbyModal extends LitElement {
|
||||
@state() private copySuccess = false;
|
||||
@state() private clients: ClientInfo[] = [];
|
||||
@state() private useRandomMap: boolean = false;
|
||||
@state() private disabledUnits: UnitType[] = [UnitType.Factory];
|
||||
@state() private disabledUnits: UnitType[] = [];
|
||||
@state() private lobbyCreatorClientID: string = "";
|
||||
@state() private lobbyIdVisible: boolean = true;
|
||||
|
||||
|
||||
@@ -423,8 +423,25 @@ class Client {
|
||||
|
||||
private handleHash() {
|
||||
const { hash } = window.location;
|
||||
|
||||
const alertAndStrip = (message: string) => {
|
||||
alert(message);
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
window.location.pathname + window.location.search,
|
||||
);
|
||||
};
|
||||
|
||||
if (hash.startsWith("#")) {
|
||||
const params = new URLSearchParams(hash.slice(1));
|
||||
if (params.get("purchase-completed") === "true") {
|
||||
alertAndStrip("purchase succeeded");
|
||||
return;
|
||||
} else if (params.get("purchase-completed") === "false") {
|
||||
alertAndStrip("purchase failed");
|
||||
return;
|
||||
}
|
||||
const lobbyId = params.get("join");
|
||||
if (lobbyId && ID.safeParse(lobbyId).success) {
|
||||
this.joinModal.open(lobbyId);
|
||||
@@ -475,6 +492,7 @@ class Client {
|
||||
"territory-patterns-modal",
|
||||
"language-modal",
|
||||
"news-modal",
|
||||
"flag-input-modal",
|
||||
].forEach((tag) => {
|
||||
const modal = document.querySelector(tag) as HTMLElement & {
|
||||
close?: () => void;
|
||||
|
||||
@@ -43,7 +43,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
@state() private gameMode: GameMode = GameMode.FFA;
|
||||
@state() private teamCount: TeamCountConfig = 2;
|
||||
|
||||
@state() private disabledUnits: UnitType[] = [UnitType.Factory];
|
||||
@state() private disabledUnits: UnitType[] = [];
|
||||
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
|
||||
@@ -95,6 +95,14 @@ export class UserSettingModal extends LitElement {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("dark-mode-changed", {
|
||||
detail: { darkMode: enabled },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
console.log("🌙 Dark Mode:", enabled ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ export class OModal extends LitElement {
|
||||
}
|
||||
|
||||
.c-modal__wrapper {
|
||||
background: #23232382;
|
||||
border-radius: 8px;
|
||||
min-width: 340px;
|
||||
max-width: 860px;
|
||||
@@ -62,6 +61,7 @@ export class OModal extends LitElement {
|
||||
}
|
||||
|
||||
.c-modal__content {
|
||||
background: #23232382;
|
||||
position: relative;
|
||||
color: #fff;
|
||||
padding: 1.4rem;
|
||||
|
||||
@@ -442,10 +442,6 @@
|
||||
document.documentElement.classList.remove("preload");
|
||||
});
|
||||
});
|
||||
window.addEventListener("beforeunload", function (e) {
|
||||
e.preventDefault();
|
||||
e.returnValue = "Are you sure you want to leave?";
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Playwire ads -->
|
||||
|
||||
@@ -57,6 +57,7 @@ export class BotExecution implements Execution {
|
||||
}
|
||||
|
||||
this.behavior.handleAllianceRequests();
|
||||
this.behavior.handleAllianceExtensionRequests();
|
||||
this.maybeAttack();
|
||||
}
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
this.updateRelationsFromEmbargos();
|
||||
this.behavior.handleAllianceRequests();
|
||||
this.behavior.handleAllianceExtensionRequests();
|
||||
this.handleUnits();
|
||||
this.handleEmbargoesToHostileNations();
|
||||
this.maybeAttack();
|
||||
|
||||
@@ -39,7 +39,7 @@ export class AllianceExtensionExecution implements Execution {
|
||||
// Mark this player's intent to extend
|
||||
alliance.addExtensionRequest(this.from);
|
||||
|
||||
if (alliance.canExtend()) {
|
||||
if (alliance.bothAgreedToExtend()) {
|
||||
alliance.extend();
|
||||
|
||||
mg.displayMessage(
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../game/Game";
|
||||
import { PseudoRandom } from "../../PseudoRandom";
|
||||
import { flattenedEmojiTable } from "../../Util";
|
||||
import { AllianceExtensionExecution } from "../alliance/AllianceExtensionExecution";
|
||||
import { AttackExecution } from "../AttackExecution";
|
||||
import { EmojiExecution } from "../EmojiExecution";
|
||||
|
||||
@@ -37,6 +38,28 @@ export class BotBehavior {
|
||||
}
|
||||
}
|
||||
|
||||
handleAllianceExtensionRequests() {
|
||||
for (const alliance of this.player.alliances()) {
|
||||
// Alliance expiration tracked by Events Panel, only human ally can click Request to Renew
|
||||
// Skip if no expiration yet/ ally didn't request extension yet/ bot already agreed to extend
|
||||
if (!alliance.onlyOneAgreedToExtend()) continue;
|
||||
|
||||
// Nation is either Friendly or Neutral as an ally. Bot has no attitude
|
||||
// If Friendly or Bot, always agree to extend. If Neutral, have random chance decide
|
||||
const human = alliance.other(this.player);
|
||||
if (
|
||||
this.player.type() === PlayerType.FakeHuman &&
|
||||
this.player.relation(human) === Relation.Neutral
|
||||
) {
|
||||
if (!this.random.chance(1.5)) continue;
|
||||
}
|
||||
|
||||
this.game.addExecution(
|
||||
new AllianceExtensionExecution(this.player, human.id()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private emoji(player: Player, emoji: number) {
|
||||
if (player.type() !== PlayerType.Human) return;
|
||||
this.game.addExecution(new EmojiExecution(this.player, player.id(), emoji));
|
||||
|
||||
@@ -47,12 +47,21 @@ export class AllianceImpl implements MutableAlliance {
|
||||
}
|
||||
}
|
||||
|
||||
canExtend(): boolean {
|
||||
bothAgreedToExtend(): boolean {
|
||||
return (
|
||||
this.extensionRequestedRequestor_ && this.extensionRequestedRecipient_
|
||||
);
|
||||
}
|
||||
|
||||
onlyOneAgreedToExtend(): boolean {
|
||||
// Requestor / Recipient of the original alliance request, not of the extension request
|
||||
// False if: no expiration or neither requested extension yet (both false), or both agreed to extend (both true)
|
||||
// True if: one requested extension, other didn't yet or actively ignored (one true, one false)
|
||||
return (
|
||||
this.extensionRequestedRequestor_ !== this.extensionRequestedRecipient_
|
||||
);
|
||||
}
|
||||
|
||||
public id(): number {
|
||||
return this.id_;
|
||||
}
|
||||
|
||||
@@ -364,10 +364,11 @@ export interface Alliance {
|
||||
export interface MutableAlliance extends Alliance {
|
||||
expire(): void;
|
||||
other(player: Player): Player;
|
||||
canExtend(): boolean;
|
||||
bothAgreedToExtend(): boolean;
|
||||
addExtensionRequest(player: Player): void;
|
||||
id(): number;
|
||||
extend(): void;
|
||||
onlyOneAgreedToExtend(): boolean;
|
||||
}
|
||||
|
||||
export class PlayerInfo {
|
||||
|
||||
@@ -229,10 +229,7 @@ export class Cluster {
|
||||
availableForTrade(player: Player): Set<TrainStation> {
|
||||
const tradingStations = new Set<TrainStation>();
|
||||
for (const station of this.stations) {
|
||||
if (
|
||||
station.unit.owner() === player ||
|
||||
station.unit.owner().isFriendly(player)
|
||||
) {
|
||||
if (station.tradeAvailable(player)) {
|
||||
tradingStations.add(station);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
GameType,
|
||||
Quads,
|
||||
Trios,
|
||||
UnitType,
|
||||
} from "../core/game/Game";
|
||||
import { PseudoRandom } from "../core/PseudoRandom";
|
||||
import { GameConfig, TeamCountConfig } from "../core/Schemas";
|
||||
@@ -89,7 +88,7 @@ export class MapPlaylist {
|
||||
gameMode: mode,
|
||||
playerTeams,
|
||||
bots: 400,
|
||||
disabledUnits: [UnitType.Train, UnitType.Factory],
|
||||
disabledUnits: [],
|
||||
} satisfies GameConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { playerInfo, setup } from "./util/Setup";
|
||||
let game: Game;
|
||||
let player1: Player;
|
||||
let player2: Player;
|
||||
let player3: Player;
|
||||
|
||||
describe("AllianceExtensionExecution", () => {
|
||||
beforeEach(async () => {
|
||||
@@ -20,18 +21,20 @@ describe("AllianceExtensionExecution", () => {
|
||||
[
|
||||
playerInfo("player1", PlayerType.Human),
|
||||
playerInfo("player2", PlayerType.Human),
|
||||
playerInfo("player3", PlayerType.FakeHuman),
|
||||
],
|
||||
);
|
||||
|
||||
player1 = game.player("player1");
|
||||
player2 = game.player("player2");
|
||||
player3 = game.player("player3");
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
});
|
||||
|
||||
test("Successfully extends existing alliance", () => {
|
||||
test("Successfully extends existing alliance between Humans", () => {
|
||||
jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
|
||||
jest.spyOn(player2, "isAlive").mockReturnValue(true);
|
||||
jest.spyOn(player1, "isAlive").mockReturnValue(true);
|
||||
@@ -51,8 +54,8 @@ describe("AllianceExtensionExecution", () => {
|
||||
|
||||
const allianceBefore = player1.allianceWith(player2)!;
|
||||
const allianceSpy = jest.spyOn(allianceBefore, "extend");
|
||||
const expirationBefore =
|
||||
allianceBefore.createdAt() + game.config().allianceDuration();
|
||||
|
||||
const expirationBefore = allianceBefore.expiresAt();
|
||||
|
||||
game.addExecution(new AllianceExtensionExecution(player1, player2.id()));
|
||||
game.executeNextTick();
|
||||
@@ -64,10 +67,9 @@ describe("AllianceExtensionExecution", () => {
|
||||
|
||||
expect(allianceAfter.id()).toBe(allianceBefore.id());
|
||||
|
||||
const expirationAfter =
|
||||
allianceAfter.createdAt() + game.config().allianceDuration();
|
||||
const expirationAfter = allianceAfter.expiresAt();
|
||||
|
||||
expect(expirationAfter).toBeGreaterThanOrEqual(expirationBefore);
|
||||
expect(expirationAfter).toBeGreaterThan(expirationBefore);
|
||||
expect(allianceSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -78,4 +80,43 @@ describe("AllianceExtensionExecution", () => {
|
||||
expect(player1.allianceWith(player2)).toBeFalsy();
|
||||
expect(player2.allianceWith(player1)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("Successfully extends existing alliance between Human and non-Human", () => {
|
||||
//test of handleAllianceExtensions is done in BotBehavior tests
|
||||
jest.spyOn(player1, "canSendAllianceRequest").mockReturnValue(true);
|
||||
jest.spyOn(player3, "isAlive").mockReturnValue(true);
|
||||
jest.spyOn(player1, "isAlive").mockReturnValue(true);
|
||||
|
||||
game.addExecution(new AllianceRequestExecution(player1, player3.id()));
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
|
||||
game.addExecution(
|
||||
new AllianceRequestReplyExecution(player1.id(), player3, true),
|
||||
);
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
|
||||
expect(player1.allianceWith(player3)).toBeTruthy();
|
||||
expect(player3.allianceWith(player1)).toBeTruthy();
|
||||
|
||||
const allianceBefore = player1.allianceWith(player3)!;
|
||||
const allianceSpy = jest.spyOn(allianceBefore, "extend");
|
||||
const expirationBefore = allianceBefore.expiresAt();
|
||||
|
||||
game.addExecution(new AllianceExtensionExecution(player1, player3.id()));
|
||||
game.executeNextTick();
|
||||
expect(allianceSpy).toHaveBeenCalledTimes(0); // both players must agree to extend
|
||||
game.addExecution(new AllianceExtensionExecution(player3, player1.id()));
|
||||
game.executeNextTick();
|
||||
|
||||
const allianceAfter = player1.allianceWith(player3)!;
|
||||
|
||||
expect(allianceAfter.id()).toBe(allianceBefore.id());
|
||||
|
||||
const expirationAfter = allianceAfter.expiresAt();
|
||||
|
||||
expect(expirationAfter).toBeGreaterThan(expirationBefore);
|
||||
expect(allianceSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AllianceExtensionExecution } from "../src/core/execution/alliance/AllianceExtensionExecution";
|
||||
import { BotBehavior } from "../src/core/execution/utils/BotBehavior";
|
||||
import {
|
||||
AllianceRequest,
|
||||
@@ -5,6 +6,7 @@ import {
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
Relation,
|
||||
Tick,
|
||||
} from "../src/core/game/Game";
|
||||
import { PseudoRandom } from "../src/core/PseudoRandom";
|
||||
@@ -149,3 +151,79 @@ describe("BotBehavior.handleAllianceRequests", () => {
|
||||
expect(request.reject).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("BotBehavior.handleAllianceExtensionRequests", () => {
|
||||
let mockGame: any;
|
||||
let mockPlayer: any;
|
||||
let mockAlliance: any;
|
||||
let mockHuman: any;
|
||||
let mockRandom: any;
|
||||
let botBehavior: BotBehavior;
|
||||
|
||||
beforeEach(() => {
|
||||
mockGame = { addExecution: jest.fn() };
|
||||
mockHuman = { id: jest.fn(() => "human_id") };
|
||||
mockAlliance = {
|
||||
onlyOneAgreedToExtend: jest.fn(() => true),
|
||||
other: jest.fn(() => mockHuman),
|
||||
};
|
||||
mockRandom = { chance: jest.fn() };
|
||||
|
||||
mockPlayer = {
|
||||
alliances: jest.fn(() => [mockAlliance]),
|
||||
relation: jest.fn(),
|
||||
id: jest.fn(() => "bot_id"),
|
||||
type: jest.fn(() => PlayerType.FakeHuman),
|
||||
};
|
||||
|
||||
botBehavior = new BotBehavior(
|
||||
mockRandom,
|
||||
mockGame,
|
||||
mockPlayer,
|
||||
0.5,
|
||||
0.5,
|
||||
0.2,
|
||||
);
|
||||
});
|
||||
|
||||
it("should NOT request extension if onlyOneAgreedToExtend is false (no expiration yet or both already agreed)", () => {
|
||||
mockAlliance.onlyOneAgreedToExtend.mockReturnValue(false);
|
||||
botBehavior.handleAllianceExtensionRequests();
|
||||
expect(mockGame.addExecution).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should always extend if type Bot", () => {
|
||||
mockPlayer.type.mockReturnValue(PlayerType.Bot);
|
||||
botBehavior.handleAllianceExtensionRequests();
|
||||
expect(mockGame.addExecution).toHaveBeenCalledTimes(1);
|
||||
expect(mockGame.addExecution.mock.calls[0][0]).toBeInstanceOf(
|
||||
AllianceExtensionExecution,
|
||||
);
|
||||
});
|
||||
|
||||
it("should always extend if Nation and relation is Friendly", () => {
|
||||
mockPlayer.relation.mockReturnValue(Relation.Friendly);
|
||||
botBehavior.handleAllianceExtensionRequests();
|
||||
expect(mockGame.addExecution).toHaveBeenCalledTimes(1);
|
||||
expect(mockGame.addExecution.mock.calls[0][0]).toBeInstanceOf(
|
||||
AllianceExtensionExecution,
|
||||
);
|
||||
});
|
||||
|
||||
it("should extend if Nation, relation is Neutral and random chance is true", () => {
|
||||
mockPlayer.relation.mockReturnValue(Relation.Neutral);
|
||||
mockRandom.chance.mockReturnValue(true);
|
||||
botBehavior.handleAllianceExtensionRequests();
|
||||
expect(mockGame.addExecution).toHaveBeenCalledTimes(1);
|
||||
expect(mockGame.addExecution.mock.calls[0][0]).toBeInstanceOf(
|
||||
AllianceExtensionExecution,
|
||||
);
|
||||
});
|
||||
|
||||
it("should NOT extend if Nation, relation is Neutral and random chance is false", () => {
|
||||
mockPlayer.relation.mockReturnValue(Relation.Neutral);
|
||||
mockRandom.chance.mockReturnValue(false);
|
||||
botBehavior.handleAllianceExtensionRequests();
|
||||
expect(mockGame.addExecution).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user