mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-15 20:35:45 +00:00
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:
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user