From 3838de1d302fefbade0a9b3d37f71f085dfb8dde Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Tue, 10 Mar 2026 05:13:13 +0100 Subject: [PATCH] =?UTF-8?q?Option=20to=20disable=20alliances=20+=202=20new?= =?UTF-8?q?=20modifiers=20for=20variety=20=F0=9F=98=84=20(#3392)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Rex had this idea: "It would be funny to have an option in private lobbies to disable alliances." I added it as an option. Now people can choose to live in constant fear of their neighbors 😆 Also added two new public game modifiers for variety (only for the special rotation): - Alliances disabled (low probability) - x2 gold multiplier (low probability) Would be nice to squeeze this into v30, last minute? ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin --- resources/lang/en.json | 11 ++++++-- src/client/HostLobbyModal.ts | 23 +++++++++++----- src/client/SinglePlayerModal.ts | 12 +++++++++ src/client/Utils.ts | 17 ++++++++++++ src/core/Schemas.ts | 3 +++ src/core/configuration/Config.ts | 1 + src/core/configuration/DefaultConfig.ts | 3 +++ src/core/game/Game.ts | 2 ++ src/core/game/PlayerImpl.ts | 3 +++ src/server/GameServer.ts | 3 +++ src/server/MapPlaylist.ts | 35 ++++++++++++++++++++++--- tests/util/TestServerConfig.ts | 1 + 12 files changed, 102 insertions(+), 12 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 95d232a03..e80177ffc 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -193,6 +193,7 @@ "infinite_gold": "Infinite gold", "infinite_troops": "Infinite troops", "compact_map": "Compact Map", + "disable_alliances": "Disable alliances", "max_timer": "Game length (minutes)", "max_timer_placeholder": "Mins", "max_timer_invalid": "Please enter a valid max timer value (1-120 minutes)", @@ -414,6 +415,7 @@ "infinite_troops": "Infinite troops", "donate_troops": "Donate troops", "compact_map": "Compact Map", + "disable_alliances": "Disable alliances", "enables_title": "Enable Settings", "player": "Player", "players": "Players", @@ -431,9 +433,12 @@ "teams_Trios": "Trios (teams of 3)", "teams_Quads": "Quads (teams of 4)", "teams_Humans Vs Nations": "Humans vs Nations", - "starting_gold": "Starting gold", "crowded": "Crowded modifier", "hard_nations": "Hard Nations", + "gold_multiplier": "Gold multiplier", + "gold_multiplier_placeholder": "2.0x", + "starting_gold": "Starting Gold (Millions)", + "starting_gold_placeholder": "5", "leave_confirmation": "Are you sure you want to leave the lobby?" }, "team_colors": { @@ -478,7 +483,9 @@ "compact_map": "Compact Map", "crowded": "Crowded", "hard_nations": "Hard Nations", - "starting_gold": "{amount}M Starting Gold" + "starting_gold": "{amount}M Starting Gold", + "gold_multiplier": "x{amount} Gold Multiplier", + "disable_alliances": "Alliances Disabled" }, "select_lang": { "title": "Select Language" diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 7c24ae97c..401097317 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -71,6 +71,7 @@ export class HostLobbyModal extends BaseModal { @state() private goldMultiplierValue: number | undefined = undefined; @state() private startingGold: boolean = false; @state() private startingGoldValue: number | undefined = undefined; + @state() private disableAlliances: boolean = false; @state() private lobbyId = ""; @state() private lobbyUrlSuffix = ""; @state() private clients: ClientInfo[] = []; @@ -174,16 +175,16 @@ export class HostLobbyModal extends BaseModal { .onKeyDown=${this.handleSpawnImmunityDurationKeyDown} >`, html``, html`, }, bubbles: true, diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 3df1f1a31..37f023ca4 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -56,6 +56,7 @@ const DEFAULT_OPTIONS = { startingGold: false, startingGoldValue: undefined as number | undefined, disabledUnits: [] as UnitType[], + disableAlliances: false, } as const; @customElement("single-player-modal") @@ -90,6 +91,7 @@ export class SinglePlayerModal extends BaseModal { @state() private disabledUnits: UnitType[] = [ ...DEFAULT_OPTIONS.disabledUnits, ]; + @state() private disableAlliances: boolean = DEFAULT_OPTIONS.disableAlliances; private mapLoader = terrainMapFileLoader; @@ -313,6 +315,10 @@ export class SinglePlayerModal extends BaseModal { labelKey: "single_modal.compact_map", checked: this.compactMap, }, + { + labelKey: "single_modal.disable_alliances", + checked: this.disableAlliances, + }, ], inputCards, }, @@ -383,6 +389,7 @@ export class SinglePlayerModal extends BaseModal { this.gameMode !== DEFAULT_OPTIONS.gameMode || this.goldMultiplier !== DEFAULT_OPTIONS.goldMultiplier || this.startingGold !== DEFAULT_OPTIONS.startingGold || + this.disableAlliances !== DEFAULT_OPTIONS.disableAlliances || this.disabledUnits.length > 0 ); } @@ -409,6 +416,7 @@ export class SinglePlayerModal extends BaseModal { this.goldMultiplierValue = DEFAULT_OPTIONS.goldMultiplierValue; this.startingGold = DEFAULT_OPTIONS.startingGold; this.startingGoldValue = DEFAULT_OPTIONS.startingGoldValue; + this.disableAlliances = DEFAULT_OPTIONS.disableAlliances; } protected onOpen(): void { @@ -488,6 +496,9 @@ export class SinglePlayerModal extends BaseModal { case "single_modal.compact_map": this.handleCompactMapChange(checked); break; + case "single_modal.disable_alliances": + this.disableAlliances = checked; + break; default: break; } @@ -696,6 +707,7 @@ export class SinglePlayerModal extends BaseModal { ), } : {}), + ...(this.disableAlliances ? { disableAlliances: true } : {}), }, lobbyCreatedAt: Date.now(), // ms; server should be authoritative in MP }, diff --git a/src/client/Utils.ts b/src/client/Utils.ts index 159e8cfb3..c9323bead 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -168,6 +168,23 @@ export function getActiveModifiers( formattedValue: `${millions}M`, }); } + if (modifiers.goldMultiplier) { + result.push({ + labelKey: "host_modal.gold_multiplier", + badgeKey: "public_game_modifier.gold_multiplier", + badgeParams: { + amount: modifiers.goldMultiplier, + }, + value: modifiers.goldMultiplier, + formattedValue: `x${modifiers.goldMultiplier}`, + }); + } + if (modifiers.isAlliancesDisabled) { + result.push({ + labelKey: "host_modal.disable_alliances", + badgeKey: "public_game_modifier.disable_alliances", + }); + } return result; } diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index a42738774..07b7f263e 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -217,6 +217,8 @@ export const GameConfigSchema = z.object({ isCrowded: z.boolean(), isHardNations: z.boolean(), startingGold: z.number().int().min(0).optional(), + goldMultiplier: z.number().min(0.1).max(1000).optional(), + isAlliancesDisabled: z.boolean(), }) .optional(), nations: z @@ -230,6 +232,7 @@ export const GameConfigSchema = z.object({ infiniteTroops: z.boolean(), instantBuild: z.boolean(), disableNavMesh: z.boolean().optional(), + disableAlliances: z.boolean().optional(), randomSpawn: z.boolean(), maxPlayers: z.number().optional(), maxTimerValue: z.number().int().min(1).max(120).optional(), // In minutes diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 55fbab613..5a7a90c30 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -74,6 +74,7 @@ export interface Config { donateTroops(): boolean; instantBuild(): boolean; disableNavMesh(): boolean; + disableAlliances(): boolean; isRandomSpawn(): boolean; numSpawnPhaseTurns(): number; userSettings(): UserSettings; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 290175c6b..2b0fb96df 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -240,6 +240,9 @@ export class DefaultConfig implements Config { disableNavMesh(): boolean { return this._gameConfig.disableNavMesh ?? false; } + disableAlliances(): boolean { + return this._gameConfig.disableAlliances ?? false; + } isRandomSpawn(): boolean { return this._gameConfig.randomSpawn; } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index c5f318f5c..475d0e4a6 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -243,6 +243,8 @@ export interface PublicGameModifiers { isCrowded: boolean; isHardNations: boolean; startingGold?: number; + goldMultiplier?: number; + isAlliancesDisabled: boolean; } export interface UnitInfo { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 027560215..975d5a70f 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -477,6 +477,9 @@ export class PlayerImpl implements Player { } canSendAllianceRequest(other: Player): boolean { + if (this.mg.config().disableAlliances()) { + return false; + } if (other === this) { return false; } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 48cf120ef..4bfe1217d 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -157,6 +157,9 @@ export class GameServer { if (gameConfig.startingGold !== undefined) { this.gameConfig.startingGold = gameConfig.startingGold; } + if (gameConfig.disableAlliances !== undefined) { + this.gameConfig.disableAlliances = gameConfig.disableAlliances; + } } private isKicked(clientID: ClientID): boolean { diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index 1425d968c..44c0f706d 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -103,7 +103,9 @@ type ModifierKey = | "isCrowded" | "isHardNations" | "startingGold" - | "startingGoldHigh"; + | "startingGoldHigh" + | "goldMultiplier" + | "isAlliancesDisabled"; // Each entry represents one "ticket" in the pool. More tickets = higher chance of selection. const SPECIAL_MODIFIER_POOL: ModifierKey[] = [ @@ -113,6 +115,8 @@ const SPECIAL_MODIFIER_POOL: ModifierKey[] = [ ...Array(1).fill("isHardNations"), ...Array(8).fill("startingGold"), ...Array(1).fill("startingGoldHigh"), + ...Array(1).fill("goldMultiplier"), + ...Array(1).fill("isAlliancesDisabled"), ]; // Modifiers that cannot be active at the same time. @@ -197,6 +201,7 @@ export class MapPlaylist { isCrowded, isHardNations, startingGold, + isAlliancesDisabled: false, }, startingGold, difficulty: isHardNations ? Difficulty.Hard : Difficulty.Medium, @@ -263,7 +268,14 @@ export class MapPlaylist { undefined, poolCountReduction, ); - let { isCrowded, startingGold, isCompact, isRandomSpawn } = poolResult; + let { + isCrowded, + startingGold, + isCompact, + isRandomSpawn, + goldMultiplier, + isAlliancesDisabled, + } = poolResult; let isHardNations = hardNationsFromIndependentRoll ?? poolResult.isHardNations; @@ -280,7 +292,9 @@ export class MapPlaylist { !isRandomSpawn && !isCompact && !isHardNations && - startingGold === undefined + startingGold === undefined && + goldMultiplier === undefined && + !isAlliancesDisabled ) { excludedModifiers.push("isCrowded"); const fallback = this.getRandomSpecialGameModifiers( @@ -288,7 +302,13 @@ export class MapPlaylist { 1, poolCountReduction, ); - ({ isRandomSpawn, isCompact, startingGold } = fallback); + ({ + isRandomSpawn, + isCompact, + startingGold, + goldMultiplier, + isAlliancesDisabled, + } = fallback); isHardNations = hardNationsFromIndependentRoll ?? fallback.isHardNations; } @@ -321,8 +341,12 @@ export class MapPlaylist { isCrowded, isHardNations, startingGold, + goldMultiplier, + isAlliancesDisabled, }, startingGold, + goldMultiplier, + disableAlliances: isAlliancesDisabled, difficulty: isHardNations ? Difficulty.Hard : Difficulty.Medium, infiniteGold: false, infiniteTroops: false, @@ -482,6 +506,7 @@ export class MapPlaylist { playerTeams === HumansVsNations ? Math.random() < HARD_NATIONS_HVN_PROBABILITY : Math.random() < 0.025, // 2.5% chance + isAlliancesDisabled: false, }; } @@ -530,6 +555,8 @@ export class MapPlaylist { : selected.has("startingGold") ? 5_000_000 : undefined, + goldMultiplier: selected.has("goldMultiplier") ? 2 : undefined, + isAlliancesDisabled: selected.has("isAlliancesDisabled"), }; } diff --git a/tests/util/TestServerConfig.ts b/tests/util/TestServerConfig.ts index c4b4b8d67..4b34f133f 100644 --- a/tests/util/TestServerConfig.ts +++ b/tests/util/TestServerConfig.ts @@ -85,6 +85,7 @@ export class TestServerConfig implements ServerConfig { isRandomSpawn: false, isCrowded: false, isHardNations: false, + isAlliancesDisabled: false, }; } async supportsCompactMapForTeams(): Promise {