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 {