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 20:57:08 +01:00
committed by GitHub
parent 6e67c2bf0d
commit cf0cf14a1f
5 changed files with 140 additions and 45 deletions
+89 -34
View File
@@ -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<ModifierKey>(2).fill("isRandomSpawn"),
...Array<ModifierKey>(8).fill("isCompact"),
...Array<ModifierKey>(1).fill("isCrowded"),
...Array<ModifierKey>(5).fill("isCompact"),
...Array<ModifierKey>(2).fill("isCrowded"),
...Array<ModifierKey>(1).fill("isHardNations"),
...Array<ModifierKey>(8).fill("startingGold"),
...Array<ModifierKey>(1).fill("startingGoldHigh"),
...Array<ModifierKey>(3).fill("startingGold1M"),
...Array<ModifierKey>(5).fill("startingGold5M"),
...Array<ModifierKey>(1).fill("startingGold25M"),
...Array<ModifierKey>(4).fill("goldMultiplier"),
...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.
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,
};
}