diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index 0984367f8..0d901a82e 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -21,6 +21,9 @@ import { getMapLandTiles } from "./MapLandTiles"; const log = logger.child({}); const ARCADE_MAPS = new Set(mapCategories.arcade); +// Hard cap on player count for performance. Applied after compact-map reduction. +const MAX_PLAYER_COUNT = 125; + // How many times each map should appear in the playlist. // Note: The Partial should eventually be removed for better type safety. const frequency: Partial> = { @@ -164,7 +167,7 @@ export class MapPlaylist { } // 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) + // set player count to MAX_PLAYER_COUNT (or 60 if compact map is also enabled) let crowdedMaxPlayers: number | undefined; if (isCrowded) { crowdedMaxPlayers = await this.getCrowdedMaxPlayers(map, isCompact); @@ -537,12 +540,15 @@ export class MapPlaylist { const [l, , s] = this.calculateMapPlayerCounts(landTiles); // Worst case: smallest tier with team mode 1.5x multiplier, capped at l let p = Math.min(Math.ceil(s * 1.5), l); - // Apply compact 75% player reduction - p = Math.max(3, Math.floor(p * 0.25)); + // Apply compact 75% player reduction, then cap for performance + p = Math.min(Math.max(3, Math.floor(p * 0.25)), MAX_PLAYER_COUNT); // Apply team adjustment p = this.adjustForTeams(p, playerTeams); - // Check at least 2 players per team - return this.playersPerTeam(p, playerTeams) >= 2; + // Check at least 2 players per team AND at least 2 teams + return ( + this.playersPerTeam(p, playerTeams) >= 2 && + this.numberOfTeams(p, playerTeams) >= 2 + ); } private playersPerTeam( @@ -563,6 +569,24 @@ export class MapPlaylist { } } + private numberOfTeams( + adjustedPlayerCount: number, + playerTeams: TeamCountConfig, + ): number { + switch (playerTeams) { + case Duos: + return Math.floor(adjustedPlayerCount / 2); + case Trios: + return Math.floor(adjustedPlayerCount / 3); + case Quads: + return Math.floor(adjustedPlayerCount / 4); + case HumansVsNations: + return 2; // always 2 teams + default: + return playerTeams; // numeric value IS the team count + } + } + /** * Centralised spawn-immunity duration logic. * - HumansVsNations: always 5s (nations can't benefit from longer PVP immunity) @@ -586,9 +610,10 @@ export class MapPlaylist { isCompact: boolean, ): Promise { const landTiles = await getMapLandTiles(map); - const [firstPlayerCount] = this.calculateMapPlayerCounts(landTiles); + const [rawFirstPlayerCount] = this.calculateMapPlayerCounts(landTiles); + const firstPlayerCount = Math.min(rawFirstPlayerCount, MAX_PLAYER_COUNT); if (firstPlayerCount <= 60) { - return isCompact ? 60 : 125; + return isCompact ? 60 : MAX_PLAYER_COUNT; } return undefined; } @@ -608,6 +633,8 @@ export class MapPlaylist { if (isCompactMap) { p = Math.max(3, Math.floor(p * 0.25)); } + // Cap for performance + p = Math.min(p, MAX_PLAYER_COUNT); return this.adjustForTeams(p, numPlayerTeams); } @@ -641,7 +668,6 @@ export class MapPlaylist { /** * Calculate player counts from land tiles * For every 1,000,000 land tiles, take 50 players - * Limit to max 125 players for performance * Second value is 75% of calculated value, third is 50% * All values are rounded to the nearest 5 */ @@ -650,12 +676,7 @@ export class MapPlaylist { ): [number, number, number] { const roundToNearest5 = (n: number) => Math.round(n / 5) * 5; - const base = roundToNearest5((landTiles / 1_000_000) * 50); - const limitedBase = Math.min(Math.max(base, 5), 125); - return [ - limitedBase, - roundToNearest5(limitedBase * 0.75), - roundToNearest5(limitedBase * 0.5), - ]; + const base = Math.max(roundToNearest5((landTiles / 1_000_000) * 50), 5); + return [base, roundToNearest5(base * 0.75), roundToNearest5(base * 0.5)]; } }