From b5ac552029439ae5b2393e2938b5efd181752f6b Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 4 Aug 2025 19:57:00 -0700 Subject: [PATCH 1/9] remove unload warning. (#1704) ## Description: The unload alert is triggered when purchasing items causing bad ux. We already have unload alert in-game if you are still alive so this seems unnecessary. reverts: https://github.com/openfrontio/OpenFrontIO/commit/ac8a841e40d0e9d290877b1fe71a0f9456758fa5 ## 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 have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/client/index.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/client/index.html b/src/client/index.html index 0892848f8..33b280951 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -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?"; - }); From fe948cb65e6bf8f7cdee24d5a2666a6e27434b7b Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 4 Aug 2025 20:06:55 -0700 Subject: [PATCH 2/9] Alert on payment success or failure. (#1705) ## Description: Alert on payment success or failure. ## 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 have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/client/Cosmetics.ts | 5 ++--- src/client/Main.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/client/Cosmetics.ts b/src/client/Cosmetics.ts index 01a4a5de0..bfd991c4a 100644 --- a/src/client/Cosmetics.ts +++ b/src/client/Cosmetics.ts @@ -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`, }), }, ); diff --git a/src/client/Main.ts b/src/client/Main.ts index 732a58c12..03d04c777 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -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); From 86466d0690d7ca452b9517dcaab29ab36d3ac931 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Tue, 5 Aug 2025 17:06:17 +0900 Subject: [PATCH 3/9] Fix: Automatically Close Flag Input Modal on Game Start (#1709) ## Description: This PR ensures that the flag selection modal is automatically closed when a game starts. Fix: https://github.com/openfrontio/OpenFrontIO/issues/1047 ## 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 have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri --- src/client/Main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/Main.ts b/src/client/Main.ts index 03d04c777..97cca212c 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -492,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; From 70e5cd83a3224eebd4086162be2fb3a71b6459ed Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:28:27 -0400 Subject: [PATCH 4/9] Disable strict mode (#1713) ## Description: Strict mode is causing docker deployments to fail. This commit reverts only the strict mode setting itself, while keeping the code changes, so that we can re-enable it more easily in the future. ## 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 - [ ] I have read and accepted the CLA agreement (only required once). --- tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 6de45daab..7c73290f3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,8 +21,7 @@ "resolveJsonModule": true, "strictNullChecks": true, "useDefineForClassFields": false, - "strictPropertyInitialization": false, - "strict": true + "strictPropertyInitialization": false }, "include": [ "src/**/*", From b48c5472c57dee13b7c8222fe3c439f4a2addf14 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Wed, 6 Aug 2025 10:04:19 +0900 Subject: [PATCH 5/9] Fix: Modal background color does not cover entire content when alwaysMaximized is set (#1710) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: This PR fixes a visual bug where, when the `alwaysMaximized` property is enabled on the modal component (`o-modal`), the background color does not cover the entire content area, especially when the content is scrollable or overflows. before It is noticeable that a black border appears between the first and second lines from the bottom スクリーンショット 2025-08-05 17 03 16 after スクリーンショット 2025-08-05 17 06 54 ## 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 have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri --- src/client/components/baseComponents/Modal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/components/baseComponents/Modal.ts b/src/client/components/baseComponents/Modal.ts index 0841b4ba4..ae58d9078 100644 --- a/src/client/components/baseComponents/Modal.ts +++ b/src/client/components/baseComponents/Modal.ts @@ -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; From b7e07208c8d08fa75c00733250ec78f214b00937 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Wed, 6 Aug 2025 10:26:20 +0900 Subject: [PATCH 6/9] Fix: Sync Dark Mode Button State (#1708) ## Description: This PR updates the dark mode toggle logic to synchronize the state of the component. Fix: https://github.com/openfrontio/OpenFrontIO/issues/1244 ## 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 have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri --- src/client/DarkModeButton.ts | 15 +++++++++++++++ src/client/UserSettingModal.ts | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/src/client/DarkModeButton.ts b/src/client/DarkModeButton.ts index 65071408f..5284ca323 100644 --- a/src/client/DarkModeButton.ts +++ b/src/client/DarkModeButton.ts @@ -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(); diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 14f79c190..d55f644ea 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -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"); } From a71ac7a218e76da254fda463a2fb5ffc0ab47542 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Wed, 6 Aug 2025 12:14:34 +0200 Subject: [PATCH 7/9] Fix oversight: non-human player never responds to alliance renewal request (#1536) ## Description: **--Fix oversight in v24--** In v24, alliance renewal was introduced. But a Bot or Nation never answers to it. So the Event Panel expiration message + clicking to Renew and waiting, is all in vain if the other player is not a human. Like in Single player in all cases. The message after ~30 seconds is always "Alliance with xxx expired". This feels illogical and there's no purpose for showing a Request to Renew button if it then always expires. Also reported by players like here: https://discord.com/channels/1284581928254701718/1284581928833388619/1398249123093676094 This PR fixes it by having the non-human reciever of the request, say yes to depending on attiude towards the human and chance. This feels more realistic. The requestor is always the human player because they click a button in EventsDisplay. So there is always already an extension request which the bot can react to with another extension request to have the alliance be extended. **--Add tests--** It adds tests for extending alliance between human and non-human player. One for AllianceExtensionExecution simply testing if alliance between human and non-human can be extended. And one in BotBehavior, testing if it correctly handles an extension request by adding a new AllianceExtensionExecution. **--Fix silent bug in existing test--** Adding the new test for human and non-human for AllianceExtensionExecution, i ran into a bug in the existing test for extending alliance between humans. Which made the test always pass because expirationAt wasn't fetched correctly. Had to fix that too, without intending that for this PR beforehand. And then had to include the bugfix from PR #1582 (v25) in it too to have the alliance actually extended. More details below: (-- The existing test would always return 'all passed' because it did not get the expiresAt() but got the createdAt + config.AllianceDuration() for both expirationBefore and expirationAfter. createdAt is immutable so before and after would be the same. And then it did not test for toBeGreaterThan, which would have failed. But it tested wrongfully for toBeGreaterThanOrEqual which was a pass even when the expirationBefore and expirationAfter would be the same and no extension had taken place. -- The bugfix from PR 1582 needed to be included now too. Because only with those changes, the existing test has its alliance truly extended and only with that the expirationAt actually changed. Actually, checking if extend() was called isn't needed anymore, since we now check the expirationAt correctly which on its own tells us if extend() was succesful. But left this addition from PR 1582 in since it can't do any harm.) ## 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 - [ ] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 --------- Co-authored-by: evanpelle Co-authored-by: Drills Kibo <59177241+drillskibo@users.noreply.github.com> --- src/core/execution/BotExecution.ts | 1 + src/core/execution/FakeHumanExecution.ts | 1 + .../alliance/AllianceExtensionExecution.ts | 2 +- src/core/execution/utils/BotBehavior.ts | 23 ++++++ src/core/game/AllianceImpl.ts | 11 ++- src/core/game/Game.ts | 3 +- tests/AllianceExtensionExecution.test.ts | 53 +++++++++++-- tests/BotBehavior.test.ts | 78 +++++++++++++++++++ 8 files changed, 163 insertions(+), 9 deletions(-) diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index 5915b6223..ddd635cc8 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -57,6 +57,7 @@ export class BotExecution implements Execution { } this.behavior.handleAllianceRequests(); + this.behavior.handleAllianceExtensionRequests(); this.maybeAttack(); } diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 828631757..0f41f7e86 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -155,6 +155,7 @@ export class FakeHumanExecution implements Execution { this.updateRelationsFromEmbargos(); this.behavior.handleAllianceRequests(); + this.behavior.handleAllianceExtensionRequests(); this.handleUnits(); this.handleEmbargoesToHostileNations(); this.maybeAttack(); diff --git a/src/core/execution/alliance/AllianceExtensionExecution.ts b/src/core/execution/alliance/AllianceExtensionExecution.ts index 2469d9038..df24a9fa8 100644 --- a/src/core/execution/alliance/AllianceExtensionExecution.ts +++ b/src/core/execution/alliance/AllianceExtensionExecution.ts @@ -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( diff --git a/src/core/execution/utils/BotBehavior.ts b/src/core/execution/utils/BotBehavior.ts index 4525c4ad9..3cf85c249 100644 --- a/src/core/execution/utils/BotBehavior.ts +++ b/src/core/execution/utils/BotBehavior.ts @@ -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)); diff --git a/src/core/game/AllianceImpl.ts b/src/core/game/AllianceImpl.ts index 6d2782595..fa74ca766 100644 --- a/src/core/game/AllianceImpl.ts +++ b/src/core/game/AllianceImpl.ts @@ -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_; } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 6272d4622..5e89f06bd 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -362,10 +362,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 { diff --git a/tests/AllianceExtensionExecution.test.ts b/tests/AllianceExtensionExecution.test.ts index 8e4cbcebb..6ae6340ed 100644 --- a/tests/AllianceExtensionExecution.test.ts +++ b/tests/AllianceExtensionExecution.test.ts @@ -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); + }); }); diff --git a/tests/BotBehavior.test.ts b/tests/BotBehavior.test.ts index 51084b509..71b14ac0b 100644 --- a/tests/BotBehavior.test.ts +++ b/tests/BotBehavior.test.ts @@ -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(); + }); +}); From fd2612ce19e1daca53842eda07df12cecdd92460 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 6 Aug 2025 15:21:39 -0700 Subject: [PATCH 8/9] enable factories by default (#1718) ## Description: Enable factories by default in singplayer private & public games. ## 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 have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/client/HostLobbyModal.ts | 2 +- src/client/SinglePlayerModal.ts | 2 +- src/server/MapPlaylist.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 07212ef6b..8b5e256e3 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -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; diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index d819a0ccf..0ed5ff179 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -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(); diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index 9f41c89e0..8fdc7612b 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -8,7 +8,6 @@ import { GameType, Quads, Trios, - UnitType, } from "../core/game/Game"; import { PseudoRandom } from "../core/PseudoRandom"; import { GameConfig, TeamCountConfig } from "../core/Schemas"; @@ -88,7 +87,7 @@ export class MapPlaylist { gameMode: mode, playerTeams, bots: 400, - disabledUnits: [UnitType.Train, UnitType.Factory], + disabledUnits: [], } satisfies GameConfig; } From 25805141b7288dc16f875184cb5f1588f19e334c Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 6 Aug 2025 15:43:01 -0700 Subject: [PATCH 9/9] allow trains & railroads between non-allies (#1719) ## Description: I think it makes sense to allow trains between non-allies, but give a bonus to land trade between players who are allies. ## 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 have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/core/game/TrainStation.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core/game/TrainStation.ts b/src/core/game/TrainStation.ts index a0e78fa01..8eb3bacee 100644 --- a/src/core/game/TrainStation.ts +++ b/src/core/game/TrainStation.ts @@ -229,10 +229,7 @@ export class Cluster { availableForTrade(player: Player): Set { const tradingStations = new Set(); for (const station of this.stations) { - if ( - station.unit.owner() === player || - station.unit.owner().isFriendly(player) - ) { + if (station.tradeAvailable(player)) { tradingStations.add(station); } }