diff --git a/resources/lang/en.json b/resources/lang/en.json index a9663b2b0..5f76bd3b2 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -166,6 +166,7 @@ "infinite_gold": "Infinite gold", "infinite_troops": "Infinite troops", "compact_map": "Compact Map", + "crowded": "Crowded", "max_timer": "Game length (minutes)", "max_timer_placeholder": "Mins", "max_timer_invalid": "Please enter a valid max timer value (1-120 minutes)", @@ -421,6 +422,7 @@ "public_game_modifier": { "random_spawn": "Random Spawn", "compact_map": "Compact Map", + "crowded": "Crowded", "starting_gold": "5M Starting Gold" }, "select_lang": { diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index 4c895ab8f..e7610672e 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -374,6 +374,9 @@ export class PublicLobby extends LitElement { if (publicGameModifiers.isCompact) { labels.push(translateText("public_game_modifier.compact_map")); } + if (publicGameModifiers.isCrowded) { + labels.push(translateText("public_game_modifier.crowded")); + } if (publicGameModifiers.startingGold) { labels.push(translateText("public_game_modifier.starting_gold")); } diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index d225857c5..9255156c1 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -190,6 +190,7 @@ export const GameConfigSchema = z.object({ .object({ isCompact: z.boolean(), isRandomSpawn: z.boolean(), + isCrowded: z.boolean(), startingGold: z.number().int().min(0).optional(), }) .optional(), diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 1c56d5d46..7e613e0c5 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -211,6 +211,7 @@ export enum GameMapSize { export interface PublicGameModifiers { isCompact: boolean; isRandomSpawn: boolean; + isCrowded: boolean; startingGold?: number; } diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index 5bbc11b00..b54aa54ff 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -97,7 +97,7 @@ export class MapPlaylist { const modifiers = this.getRandomPublicGameModifiers(); const { startingGold } = modifiers; - let { isCompact, isRandomSpawn } = modifiers; + let { isCompact, isRandomSpawn, isCrowded } = modifiers; // Duos, Trios, and Quads should not get random spawn (as it defeats the purpose) if ( @@ -108,8 +108,8 @@ export class MapPlaylist { isRandomSpawn = false; } - // Maps with smallest player count < 50 don't support compact map in team games - // The smallest player count is the 3rd number in the player counts array + // Maps with smallest player count (third number of calculateMapPlayerCounts) < 50 don't support compact map in team games + // (not enough players after 75% player reduction for compact maps) if ( mode === GameMode.Team && !(await this.supportsCompactMapForTeams(map)) @@ -117,15 +117,34 @@ export class MapPlaylist { isCompact = false; } + // Crowded modifier: if the map's biggest player count (first number of calculateMapPlayerCounts) is 60 or lower (small maps), + // set player count to 125 (or 60 if compact map is also enabled) + let crowdedMaxPlayers: number | undefined; + if (isCrowded) { + crowdedMaxPlayers = await this.getCrowdedMaxPlayers(map, isCompact); + if (crowdedMaxPlayers === undefined) { + isCrowded = false; + } else { + crowdedMaxPlayers = this.adjustForTeams(crowdedMaxPlayers, playerTeams); + } + } + // Create the default public game config (from your GameManager) return { donateGold: mode === GameMode.Team, donateTroops: mode === GameMode.Team, gameMap: map, - maxPlayers: await this.lobbyMaxPlayers(map, mode, playerTeams, isCompact), + maxPlayers: + crowdedMaxPlayers ?? + (await this.lobbyMaxPlayers(map, mode, playerTeams, isCompact)), gameType: GameType.Public, gameMapSize: isCompact ? GameMapSize.Compact : GameMapSize.Normal, - publicGameModifiers: { isCompact, isRandomSpawn, startingGold }, + publicGameModifiers: { + isCompact, + isRandomSpawn, + isCrowded, + startingGold, + }, startingGold, difficulty: playerTeams === HumansVsNations ? Difficulty.Medium : Difficulty.Easy, @@ -209,18 +228,31 @@ export class MapPlaylist { return { isRandomSpawn: Math.random() < 0.1, // 10% chance isCompact: Math.random() < 0.05, // 5% chance + isCrowded: Math.random() < 0.05, // 5% chance startingGold: Math.random() < 0.05 ? 5_000_000 : undefined, // 5% chance }; } + // Maps with smallest player count (third number of calculateMapPlayerCounts) < 50 don't support compact map in team games + // (not enough players after 75% player reduction for compact maps) private async supportsCompactMapForTeams(map: GameMapType): Promise { - // Maps with smallest player count < 50 don't support compact map in team games - // The smallest player count is the 3rd number in the player counts array const landTiles = await getMapLandTiles(map); const [, , smallest] = this.calculateMapPlayerCounts(landTiles); return smallest >= 50; } + private async getCrowdedMaxPlayers( + map: GameMapType, + isCompact: boolean, + ): Promise { + const landTiles = await getMapLandTiles(map); + const [firstPlayerCount] = this.calculateMapPlayerCounts(landTiles); + if (firstPlayerCount <= 60) { + return isCompact ? 60 : 125; + } + return undefined; + } + private async lobbyMaxPlayers( map: GameMapType, mode: GameMode, @@ -236,7 +268,15 @@ export class MapPlaylist { if (isCompactMap) { p = Math.max(3, Math.floor(p * 0.25)); } - if (numPlayerTeams === undefined) return p; + return this.adjustForTeams(p, numPlayerTeams); + } + + private adjustForTeams( + playerCount: number, + numPlayerTeams: TeamCountConfig | undefined, + ): number { + if (numPlayerTeams === undefined) return playerCount; + let p = playerCount; switch (numPlayerTeams) { case Duos: p -= p % 2; diff --git a/tests/util/TestServerConfig.ts b/tests/util/TestServerConfig.ts index 6a879ccd5..94b625943 100644 --- a/tests/util/TestServerConfig.ts +++ b/tests/util/TestServerConfig.ts @@ -80,7 +80,7 @@ export class TestServerConfig implements ServerConfig { throw new Error("Method not implemented."); } getRandomPublicGameModifiers(): PublicGameModifiers { - return { isCompact: false, isRandomSpawn: false }; + return { isCompact: false, isRandomSpawn: false, isCrowded: false }; } async supportsCompactMapForTeams(): Promise { throw new Error("Method not implemented.");