Add new public game modifiers 🙂 (#3500)

## Description:

Adds 5 new public game modifiers to the Special game mode modifier pool:

- **Ports Disabled** - disables port construction, focus on factories
- **Nukes Disabled** - disables atom bombs, hydrogen bombs, MIRVs,
missile silos and SAM launchers
- **SAMs Disabled** - disables SAM launchers (thats funny, you cant
protect against nukes, have to space out your stuff) (mutually exclusive
with nukes disabled)
- **1M Starting Gold** - gives all players 1M starting gold (was
requested by people, new chill tier alongside existing 5M and 25M)
- **4min Peace Time** - grants 4 minutes of PVP spawn immunity

All `PublicGameModifiers` boolean fields are now optional - inactive
modifiers are omitted from game JSON instead of being serialized as
`false` (stop bloating the JSON size)

I think we have enough modifiers now :) Good variety

## 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
This commit is contained in:
FloPinguin
2026-03-24 12:57:08 -07:00
committed by GitHub
parent 6e67c2bf0d
commit cf0cf14a1f
5 changed files with 140 additions and 45 deletions
+9 -1
View File
@@ -500,7 +500,15 @@
"starting_gold_label": "Starting Gold", "starting_gold_label": "Starting Gold",
"gold_multiplier": "x{amount} Gold Multiplier", "gold_multiplier": "x{amount} Gold Multiplier",
"disable_alliances": "Alliances Disabled", "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": { "select_lang": {
"title": "Select Language" "title": "Select Language"
+24
View File
@@ -186,6 +186,30 @@ export function getActiveModifiers(
formattedValue: translateText("common.disabled"), 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; return result;
} }
+9 -5
View File
@@ -225,13 +225,17 @@ export const GameConfigSchema = z.object({
gameMapSize: z.enum(GameMapSize), gameMapSize: z.enum(GameMapSize),
publicGameModifiers: z publicGameModifiers: z
.object({ .object({
isCompact: z.boolean(), isCompact: z.boolean().optional(),
isRandomSpawn: z.boolean(), isRandomSpawn: z.boolean().optional(),
isCrowded: z.boolean(), isCrowded: z.boolean().optional(),
isHardNations: z.boolean(), isHardNations: z.boolean().optional(),
startingGold: z.number().int().min(0).optional(), startingGold: z.number().int().min(0).optional(),
goldMultiplier: z.number().min(0.1).max(1000).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(), .optional(),
nations: z nations: z
+9 -5
View File
@@ -248,13 +248,17 @@ export enum GameMapSize {
} }
export interface PublicGameModifiers { export interface PublicGameModifiers {
isCompact: boolean; isCompact?: boolean;
isRandomSpawn: boolean; isRandomSpawn?: boolean;
isCrowded: boolean; isCrowded?: boolean;
isHardNations: boolean; isHardNations?: boolean;
startingGold?: number; startingGold?: number;
goldMultiplier?: number; goldMultiplier?: number;
isAlliancesDisabled: boolean; isAlliancesDisabled?: boolean;
isPortsDisabled?: boolean;
isNukesDisabled?: boolean;
isSAMsDisabled?: boolean;
isPeaceTime?: boolean;
} }
export interface UnitInfo { export interface UnitInfo {
+89 -34
View File
@@ -12,6 +12,7 @@ import {
Quads, Quads,
RankedType, RankedType,
Trios, Trios,
UnitType,
mapCategories, mapCategories,
} from "../core/game/Game"; } from "../core/game/Game";
import { PseudoRandom } from "../core/PseudoRandom"; import { PseudoRandom } from "../core/PseudoRandom";
@@ -103,27 +104,40 @@ type ModifierKey =
| "isCompact" | "isCompact"
| "isCrowded" | "isCrowded"
| "isHardNations" | "isHardNations"
| "startingGold" | "startingGold1M"
| "startingGoldHigh" | "startingGold5M"
| "startingGold25M"
| "goldMultiplier" | "goldMultiplier"
| "isAlliancesDisabled"; | "isAlliancesDisabled"
| "isPortsDisabled"
| "isNukesDisabled"
| "isSAMsDisabled"
| "isPeaceTime";
// Each entry represents one "ticket" in the pool. More tickets = higher chance of selection. // Each entry represents one "ticket" in the pool. More tickets = higher chance of selection.
const SPECIAL_MODIFIER_POOL: ModifierKey[] = [ const SPECIAL_MODIFIER_POOL: ModifierKey[] = [
...Array<ModifierKey>(2).fill("isRandomSpawn"), ...Array<ModifierKey>(2).fill("isRandomSpawn"),
...Array<ModifierKey>(8).fill("isCompact"), ...Array<ModifierKey>(5).fill("isCompact"),
...Array<ModifierKey>(1).fill("isCrowded"), ...Array<ModifierKey>(2).fill("isCrowded"),
...Array<ModifierKey>(1).fill("isHardNations"), ...Array<ModifierKey>(1).fill("isHardNations"),
...Array<ModifierKey>(8).fill("startingGold"), ...Array<ModifierKey>(3).fill("startingGold1M"),
...Array<ModifierKey>(1).fill("startingGoldHigh"), ...Array<ModifierKey>(5).fill("startingGold5M"),
...Array<ModifierKey>(1).fill("startingGold25M"),
...Array<ModifierKey>(4).fill("goldMultiplier"), ...Array<ModifierKey>(4).fill("goldMultiplier"),
...Array<ModifierKey>(1).fill("isAlliancesDisabled"), ...Array<ModifierKey>(1).fill("isAlliancesDisabled"),
...Array<ModifierKey>(1).fill("isPortsDisabled"),
...Array<ModifierKey>(1).fill("isNukesDisabled"),
...Array<ModifierKey>(1).fill("isSAMsDisabled"),
...Array<ModifierKey>(1).fill("isPeaceTime"),
]; ];
// Modifiers that cannot be active at the same time. // Modifiers that cannot be active at the same time.
const MUTUALLY_EXCLUSIVE_MODIFIERS: [ModifierKey, ModifierKey][] = [ const MUTUALLY_EXCLUSIVE_MODIFIERS: [ModifierKey, ModifierKey][] = [
["startingGold", "startingGoldHigh"], ["startingGold5M", "startingGold25M"],
["isHardNations", "startingGoldHigh"], ["startingGold5M", "startingGold1M"],
["startingGold25M", "startingGold1M"],
["isHardNations", "startingGold25M"],
["isNukesDisabled", "isSAMsDisabled"],
]; ];
export class MapPlaylist { export class MapPlaylist {
@@ -144,13 +158,14 @@ export class MapPlaylist {
const playerTeams = const playerTeams =
mode === GameMode.Team ? this.getTeamCount(map) : undefined; 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 ( if (
isCompact && isCompact &&
mode === GameMode.Team && mode === GameMode.Team &&
!(await this.supportsCompactMapForTeams(map, playerTeams!)) !(await this.supportsCompactMapForTeams(map, playerTeams!))
) { ) {
isCompact = false; isCompact = undefined;
} }
return { return {
@@ -162,10 +177,6 @@ export class MapPlaylist {
gameMapSize: isCompact ? GameMapSize.Compact : GameMapSize.Normal, gameMapSize: isCompact ? GameMapSize.Compact : GameMapSize.Normal,
publicGameModifiers: { publicGameModifiers: {
isCompact, isCompact,
isRandomSpawn: false,
isCrowded: false,
isHardNations: false,
isAlliancesDisabled: false,
}, },
difficulty: difficulty:
playerTeams === HumansVsNations ? Difficulty.Hard : Difficulty.Medium, playerTeams === HumansVsNations ? Difficulty.Hard : Difficulty.Medium,
@@ -216,7 +227,8 @@ export class MapPlaylist {
excludedModifiers.push("isHardNations"); excludedModifiers.push("isHardNations");
} }
if (playerTeams === HumansVsNations) { 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); const poolResult = this.getRandomSpecialGameModifiers(excludedModifiers);
@@ -228,26 +240,34 @@ export class MapPlaylist {
goldMultiplier, goldMultiplier,
isAlliancesDisabled, isAlliancesDisabled,
isHardNations, isHardNations,
isPortsDisabled,
isNukesDisabled,
isSAMsDisabled,
isPeaceTime,
} = poolResult; } = poolResult;
// Crowded modifier: if the map's biggest player count (first number of calculateMapPlayerCounts) is 60 or lower (small maps), // 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) // set player count to MAX_PLAYER_COUNT (or 60 if compact map is also enabled)
let crowdedMaxPlayers: number | undefined; let crowdedMaxPlayers: number | undefined;
if (isCrowded) { if (isCrowded) {
crowdedMaxPlayers = await this.getCrowdedMaxPlayers(map, isCompact); crowdedMaxPlayers = await this.getCrowdedMaxPlayers(map, !!isCompact);
if (crowdedMaxPlayers !== undefined) { if (crowdedMaxPlayers !== undefined) {
crowdedMaxPlayers = this.adjustForTeams(crowdedMaxPlayers, playerTeams); crowdedMaxPlayers = this.adjustForTeams(crowdedMaxPlayers, playerTeams);
} else { } else {
// Map doesn't support crowded. Drop it and pick one replacement only // 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. // if it was the sole modifier, so the lobby always has at least one.
isCrowded = false; isCrowded = undefined;
if ( if (
!isRandomSpawn && !isRandomSpawn &&
!isCompact && !isCompact &&
!isHardNations && !isHardNations &&
startingGold === undefined && startingGold === undefined &&
goldMultiplier === undefined && goldMultiplier === undefined &&
!isAlliancesDisabled !isAlliancesDisabled &&
!isPortsDisabled &&
!isNukesDisabled &&
!isSAMsDisabled &&
!isPeaceTime
) { ) {
excludedModifiers.push("isCrowded"); excludedModifiers.push("isCrowded");
const fallback = this.getRandomSpecialGameModifiers( const fallback = this.getRandomSpecialGameModifiers(
@@ -260,6 +280,10 @@ export class MapPlaylist {
startingGold, startingGold,
goldMultiplier, goldMultiplier,
isAlliancesDisabled, isAlliancesDisabled,
isPortsDisabled,
isNukesDisabled,
isSAMsDisabled,
isPeaceTime,
} = fallback); } = fallback);
({ isHardNations } = fallback); ({ isHardNations } = fallback);
} }
@@ -279,6 +303,28 @@ export class MapPlaylist {
? "disabled" ? "disabled"
: "default"; : "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 { return {
donateGold: mode === GameMode.Team, donateGold: mode === GameMode.Team,
donateTroops: mode === GameMode.Team, donateTroops: mode === GameMode.Team,
@@ -294,10 +340,14 @@ export class MapPlaylist {
startingGold, startingGold,
goldMultiplier, goldMultiplier,
isAlliancesDisabled, isAlliancesDisabled,
isPortsDisabled,
isNukesDisabled,
isSAMsDisabled,
isPeaceTime,
}, },
startingGold, startingGold,
goldMultiplier, goldMultiplier,
disableAlliances: isAlliancesDisabled, disableAlliances: isAlliancesDisabled ? true : undefined,
difficulty: difficulty:
isHardNations || playerTeams === HumansVsNations isHardNations || playerTeams === HumansVsNations
? Difficulty.Hard ? Difficulty.Hard
@@ -306,16 +356,15 @@ export class MapPlaylist {
infiniteTroops: false, infiniteTroops: false,
maxTimerValue: undefined, maxTimerValue: undefined,
instantBuild: false, instantBuild: false,
randomSpawn: isRandomSpawn, randomSpawn: isRandomSpawn ? true : false,
nations, nations,
gameMode: mode, gameMode: mode,
playerTeams, playerTeams,
bots: isCompact ? 100 : 400, bots: isCompact ? 100 : 400,
spawnImmunityDuration: this.getSpawnImmunityDuration( spawnImmunityDuration:
playerTeams, peaceTimeDuration ??
startingGold, this.getSpawnImmunityDuration(playerTeams, startingGold),
), disabledUnits,
disabledUnits: [],
} satisfies GameConfig; } satisfies GameConfig;
} }
@@ -476,17 +525,23 @@ export class MapPlaylist {
} }
return { return {
isRandomSpawn: selected.has("isRandomSpawn"), isRandomSpawn: selected.has("isRandomSpawn") || undefined,
isCompact: selected.has("isCompact"), isCompact: selected.has("isCompact") || undefined,
isCrowded: selected.has("isCrowded"), isCrowded: selected.has("isCrowded") || undefined,
isHardNations: selected.has("isHardNations"), isHardNations: selected.has("isHardNations") || undefined,
startingGold: selected.has("startingGoldHigh") startingGold: selected.has("startingGold25M")
? 25_000_000 ? 25_000_000
: selected.has("startingGold") : selected.has("startingGold5M")
? 5_000_000 ? 5_000_000
: undefined, : selected.has("startingGold1M")
? 1_000_000
: undefined,
goldMultiplier: selected.has("goldMultiplier") ? 2 : 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,
}; };
} }