diff --git a/resources/lang/en.json b/resources/lang/en.json index 1f1f0674a..2fbb59b57 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -500,7 +500,15 @@ "starting_gold_label": "Starting Gold", "gold_multiplier": "x{amount} Gold Multiplier", "disable_alliances": "Alliances Disabled", - "disable_alliances_label": "Alliances" + "disable_alliances_label": "Alliances", + "ports_disabled": "Ports Disabled", + "ports_disabled_label": "Ports", + "nukes_disabled": "Nukes Disabled", + "nukes_disabled_label": "Nukes", + "sams_disabled": "SAMs Disabled", + "sams_disabled_label": "SAMs", + "peace_time": "4min Peace", + "peace_time_label": "PVP Immunity" }, "select_lang": { "title": "Select Language" diff --git a/src/client/Utils.ts b/src/client/Utils.ts index e3f61d00d..3677bbbeb 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -186,6 +186,30 @@ export function getActiveModifiers( formattedValue: translateText("common.disabled"), }); } + if (modifiers.isPortsDisabled) { + result.push({ + labelKey: "public_game_modifier.ports_disabled_label", + badgeKey: "public_game_modifier.ports_disabled", + }); + } + if (modifiers.isNukesDisabled) { + result.push({ + labelKey: "public_game_modifier.nukes_disabled_label", + badgeKey: "public_game_modifier.nukes_disabled", + }); + } + if (modifiers.isSAMsDisabled) { + result.push({ + labelKey: "public_game_modifier.sams_disabled_label", + badgeKey: "public_game_modifier.sams_disabled", + }); + } + if (modifiers.isPeaceTime) { + result.push({ + labelKey: "public_game_modifier.peace_time_label", + badgeKey: "public_game_modifier.peace_time", + }); + } return result; } diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 8b5ed10fc..4382e0c2e 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -225,13 +225,17 @@ export const GameConfigSchema = z.object({ gameMapSize: z.enum(GameMapSize), publicGameModifiers: z .object({ - isCompact: z.boolean(), - isRandomSpawn: z.boolean(), - isCrowded: z.boolean(), - isHardNations: z.boolean(), + isCompact: z.boolean().optional(), + isRandomSpawn: z.boolean().optional(), + isCrowded: z.boolean().optional(), + isHardNations: z.boolean().optional(), startingGold: z.number().int().min(0).optional(), goldMultiplier: z.number().min(0.1).max(1000).optional(), - isAlliancesDisabled: z.boolean(), + isAlliancesDisabled: z.boolean().optional(), + isPortsDisabled: z.boolean().optional(), + isNukesDisabled: z.boolean().optional(), + isSAMsDisabled: z.boolean().optional(), + isPeaceTime: z.boolean().optional(), }) .optional(), nations: z diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index b6bc4b52e..73f5168a2 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -248,13 +248,17 @@ export enum GameMapSize { } export interface PublicGameModifiers { - isCompact: boolean; - isRandomSpawn: boolean; - isCrowded: boolean; - isHardNations: boolean; + isCompact?: boolean; + isRandomSpawn?: boolean; + isCrowded?: boolean; + isHardNations?: boolean; startingGold?: number; goldMultiplier?: number; - isAlliancesDisabled: boolean; + isAlliancesDisabled?: boolean; + isPortsDisabled?: boolean; + isNukesDisabled?: boolean; + isSAMsDisabled?: boolean; + isPeaceTime?: boolean; } export interface UnitInfo { diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index 273c9cddf..6d05c77ee 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -12,6 +12,7 @@ import { Quads, RankedType, Trios, + UnitType, mapCategories, } from "../core/game/Game"; import { PseudoRandom } from "../core/PseudoRandom"; @@ -103,27 +104,40 @@ type ModifierKey = | "isCompact" | "isCrowded" | "isHardNations" - | "startingGold" - | "startingGoldHigh" + | "startingGold1M" + | "startingGold5M" + | "startingGold25M" | "goldMultiplier" - | "isAlliancesDisabled"; + | "isAlliancesDisabled" + | "isPortsDisabled" + | "isNukesDisabled" + | "isSAMsDisabled" + | "isPeaceTime"; // Each entry represents one "ticket" in the pool. More tickets = higher chance of selection. const SPECIAL_MODIFIER_POOL: ModifierKey[] = [ ...Array(2).fill("isRandomSpawn"), - ...Array(8).fill("isCompact"), - ...Array(1).fill("isCrowded"), + ...Array(5).fill("isCompact"), + ...Array(2).fill("isCrowded"), ...Array(1).fill("isHardNations"), - ...Array(8).fill("startingGold"), - ...Array(1).fill("startingGoldHigh"), + ...Array(3).fill("startingGold1M"), + ...Array(5).fill("startingGold5M"), + ...Array(1).fill("startingGold25M"), ...Array(4).fill("goldMultiplier"), ...Array(1).fill("isAlliancesDisabled"), + ...Array(1).fill("isPortsDisabled"), + ...Array(1).fill("isNukesDisabled"), + ...Array(1).fill("isSAMsDisabled"), + ...Array(1).fill("isPeaceTime"), ]; // Modifiers that cannot be active at the same time. const MUTUALLY_EXCLUSIVE_MODIFIERS: [ModifierKey, ModifierKey][] = [ - ["startingGold", "startingGoldHigh"], - ["isHardNations", "startingGoldHigh"], + ["startingGold5M", "startingGold25M"], + ["startingGold5M", "startingGold1M"], + ["startingGold25M", "startingGold1M"], + ["isHardNations", "startingGold25M"], + ["isNukesDisabled", "isSAMsDisabled"], ]; export class MapPlaylist { @@ -144,13 +158,14 @@ export class MapPlaylist { const playerTeams = mode === GameMode.Team ? this.getTeamCount(map) : undefined; - let isCompact = this.playlists[type].length % 3 === 0; + let isCompact: boolean | undefined = + this.playlists[type].length % 3 === 0 || undefined; if ( isCompact && mode === GameMode.Team && !(await this.supportsCompactMapForTeams(map, playerTeams!)) ) { - isCompact = false; + isCompact = undefined; } return { @@ -162,10 +177,6 @@ export class MapPlaylist { gameMapSize: isCompact ? GameMapSize.Compact : GameMapSize.Normal, publicGameModifiers: { isCompact, - isRandomSpawn: false, - isCrowded: false, - isHardNations: false, - isAlliancesDisabled: false, }, difficulty: playerTeams === HumansVsNations ? Difficulty.Hard : Difficulty.Medium, @@ -216,7 +227,8 @@ export class MapPlaylist { excludedModifiers.push("isHardNations"); } if (playerTeams === HumansVsNations) { - excludedModifiers.push("startingGoldHigh"); // Nations are disabled if that modifier is active + excludedModifiers.push("startingGold25M"); // Nations are disabled if that modifier is active (Because of PVP immunity) + excludedModifiers.push("isPeaceTime"); // Nations don't have PVP immunity } const poolResult = this.getRandomSpecialGameModifiers(excludedModifiers); @@ -228,26 +240,34 @@ export class MapPlaylist { goldMultiplier, isAlliancesDisabled, isHardNations, + isPortsDisabled, + isNukesDisabled, + isSAMsDisabled, + isPeaceTime, } = poolResult; // Crowded modifier: if the map's biggest player count (first number of calculateMapPlayerCounts) is 60 or lower (small maps), // 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); + crowdedMaxPlayers = await this.getCrowdedMaxPlayers(map, !!isCompact); if (crowdedMaxPlayers !== undefined) { crowdedMaxPlayers = this.adjustForTeams(crowdedMaxPlayers, playerTeams); } else { // Map doesn't support crowded. Drop it and pick one replacement only // if it was the sole modifier, so the lobby always has at least one. - isCrowded = false; + isCrowded = undefined; if ( !isRandomSpawn && !isCompact && !isHardNations && startingGold === undefined && goldMultiplier === undefined && - !isAlliancesDisabled + !isAlliancesDisabled && + !isPortsDisabled && + !isNukesDisabled && + !isSAMsDisabled && + !isPeaceTime ) { excludedModifiers.push("isCrowded"); const fallback = this.getRandomSpecialGameModifiers( @@ -260,6 +280,10 @@ export class MapPlaylist { startingGold, goldMultiplier, isAlliancesDisabled, + isPortsDisabled, + isNukesDisabled, + isSAMsDisabled, + isPeaceTime, } = fallback); ({ isHardNations } = fallback); } @@ -279,6 +303,28 @@ export class MapPlaylist { ? "disabled" : "default"; + // Build disabledUnits from modifiers + const disabledUnits: UnitType[] = []; + if (isPortsDisabled) { + disabledUnits.push(UnitType.Port); + } + if (isNukesDisabled) { + disabledUnits.push( + UnitType.MissileSilo, + UnitType.AtomBomb, + UnitType.HydrogenBomb, + UnitType.MIRV, + UnitType.SAMLauncher, + ); + } + if (isSAMsDisabled) { + disabledUnits.push(UnitType.SAMLauncher); + } + + // 3min peace = 180s = 1800 ticks + // 4min peace = 240s = 2400 ticks + const peaceTimeDuration = isPeaceTime ? 240 * 10 : undefined; + return { donateGold: mode === GameMode.Team, donateTroops: mode === GameMode.Team, @@ -294,10 +340,14 @@ export class MapPlaylist { startingGold, goldMultiplier, isAlliancesDisabled, + isPortsDisabled, + isNukesDisabled, + isSAMsDisabled, + isPeaceTime, }, startingGold, goldMultiplier, - disableAlliances: isAlliancesDisabled, + disableAlliances: isAlliancesDisabled ? true : undefined, difficulty: isHardNations || playerTeams === HumansVsNations ? Difficulty.Hard @@ -306,16 +356,15 @@ export class MapPlaylist { infiniteTroops: false, maxTimerValue: undefined, instantBuild: false, - randomSpawn: isRandomSpawn, + randomSpawn: isRandomSpawn ? true : false, nations, gameMode: mode, playerTeams, bots: isCompact ? 100 : 400, - spawnImmunityDuration: this.getSpawnImmunityDuration( - playerTeams, - startingGold, - ), - disabledUnits: [], + spawnImmunityDuration: + peaceTimeDuration ?? + this.getSpawnImmunityDuration(playerTeams, startingGold), + disabledUnits, } satisfies GameConfig; } @@ -476,17 +525,23 @@ export class MapPlaylist { } return { - isRandomSpawn: selected.has("isRandomSpawn"), - isCompact: selected.has("isCompact"), - isCrowded: selected.has("isCrowded"), - isHardNations: selected.has("isHardNations"), - startingGold: selected.has("startingGoldHigh") + isRandomSpawn: selected.has("isRandomSpawn") || undefined, + isCompact: selected.has("isCompact") || undefined, + isCrowded: selected.has("isCrowded") || undefined, + isHardNations: selected.has("isHardNations") || undefined, + startingGold: selected.has("startingGold25M") ? 25_000_000 - : selected.has("startingGold") + : selected.has("startingGold5M") ? 5_000_000 - : undefined, + : selected.has("startingGold1M") + ? 1_000_000 + : undefined, goldMultiplier: selected.has("goldMultiplier") ? 2 : undefined, - isAlliancesDisabled: selected.has("isAlliancesDisabled"), + isAlliancesDisabled: selected.has("isAlliancesDisabled") || undefined, + isPortsDisabled: selected.has("isPortsDisabled") || undefined, + isNukesDisabled: selected.has("isNukesDisabled") || undefined, + isSAMsDisabled: selected.has("isSAMsDisabled") || undefined, + isPeaceTime: selected.has("isPeaceTime") || undefined, }; }