mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 02:32:04 +00:00
ebcb654825
## Description: Added a public game modifier system. It causes that 5% of public games are played on the compact version of the map 10% of public games have "Random Spawn" activated Percentages can easily get changed via `DefaultConfig`. We can also easily add more modifiers. Modifiers can stack, so in rare cases you will play on a compact map with random spawn 😄 More variety! ### "Compact Map" modifier implementation - With the "Compact Map" modifier the lobby max player count gets reduced to 25% and only 25% of the regular bots and only 25% of the regular nations will spawn (because the map has only 25% of its regular size) - In private lobbies and singleplayer the nation reduction happens too (When "Compact Map" is enabled). ### Restrictions - Duos/Trios/Quads team modes do not get Random Spawn (defeats the purpose) - Maps with smallest player count < 50 do not get Compact Map in team games (not enough players after the reduction to 25%). I have calculated all the possible max player counts. ### How it looks like Random Spawn modifier: <img width="528" height="183" alt="Screenshot 2026-01-06 194959" src="https://github.com/user-attachments/assets/2f729da9-80c3-4548-8205-71129da2a76a" /> Very rare case: Two modifiers at the same time and only 10 max players have been chosen from `[GameMapType.FaroeIslands]: [20, 15, 10]`. Because of the 75% reduction in player count only 3 players are allowed (3 is the minimum). I think its funny that you can play a 1v1v1 in rare occasions 😄 <img width="526" height="184" alt="Screenshot 2026-01-06 194938" src="https://github.com/user-attachments/assets/834326eb-df03-41b7-b1db-1efa3f1013b5" /> ### Funny side-effect Team games with random spawn. That will be interesting. No more "Who is better in donating troops to the frontline". Instead you have to heavily coordinate with your teammates. ## 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
333 lines
7.9 KiB
TypeScript
333 lines
7.9 KiB
TypeScript
import { PseudoRandom } from "../PseudoRandom";
|
|
import { GameStartInfo } from "../Schemas";
|
|
import {
|
|
Cell,
|
|
GameMapSize,
|
|
GameMode,
|
|
GameType,
|
|
HumansVsNations,
|
|
Nation,
|
|
PlayerInfo,
|
|
PlayerType,
|
|
} from "./Game";
|
|
import { Nation as ManifestNation } from "./TerrainMapLoader";
|
|
|
|
/**
|
|
* Creates the nations array for a game, handling HumansVsNations mode specially.
|
|
* In HumansVsNations mode, the number of nations matches the number of human players to ensure fair gameplay.
|
|
* For compact maps, only 25% of the nations are used.
|
|
*/
|
|
export function createNationsForGame(
|
|
gameStart: GameStartInfo,
|
|
manifestNations: ManifestNation[],
|
|
numHumans: number,
|
|
random: PseudoRandom,
|
|
): Nation[] {
|
|
if (gameStart.config.disableNations) {
|
|
return [];
|
|
}
|
|
|
|
const toNation = (n: ManifestNation): Nation =>
|
|
new Nation(
|
|
new Cell(n.coordinates[0], n.coordinates[1]),
|
|
new PlayerInfo(n.name, PlayerType.Nation, null, random.nextID()),
|
|
);
|
|
|
|
const isCompactMap = gameStart.config.gameMapSize === GameMapSize.Compact;
|
|
|
|
const isHumansVsNations =
|
|
gameStart.config.gameMode === GameMode.Team &&
|
|
gameStart.config.playerTeams === HumansVsNations;
|
|
|
|
// For compact maps, use only 25% of nations (minimum 1)
|
|
let effectiveNations = manifestNations;
|
|
if (isCompactMap && !isHumansVsNations) {
|
|
const targetCount = getCompactMapNationCount(manifestNations.length, true);
|
|
const shuffled = random.shuffleArray(manifestNations);
|
|
effectiveNations = shuffled.slice(0, targetCount);
|
|
}
|
|
|
|
// For non-HumansVsNations modes, simply use the effective nations
|
|
if (!isHumansVsNations) {
|
|
return effectiveNations.map(toNation);
|
|
}
|
|
|
|
// HumansVsNations mode: balance nation count to match human count
|
|
const isSingleplayer = gameStart.config.gameType === GameType.Singleplayer;
|
|
const targetNationCount = isSingleplayer ? 1 : numHumans;
|
|
|
|
if (targetNationCount === 0) {
|
|
return [];
|
|
}
|
|
|
|
// If we have enough manifest nations, use a subset
|
|
if (manifestNations.length >= targetNationCount) {
|
|
// Shuffle manifest nations to add variety
|
|
const shuffled = random.shuffleArray(manifestNations);
|
|
return shuffled.slice(0, targetNationCount).map(toNation);
|
|
}
|
|
|
|
// If we need more nations than defined in manifest, create additional ones
|
|
const nations: Nation[] = manifestNations.map(toNation);
|
|
const additionalCount = targetNationCount - manifestNations.length;
|
|
for (let i = 0; i < additionalCount; i++) {
|
|
const name = generateNationName(random);
|
|
nations.push(
|
|
new Nation(
|
|
undefined,
|
|
new PlayerInfo(name, PlayerType.Nation, null, random.nextID()),
|
|
),
|
|
);
|
|
}
|
|
|
|
return nations;
|
|
}
|
|
|
|
// For compact maps, only 25% of nations are used (minimum 1).
|
|
export function getCompactMapNationCount(
|
|
manifestNationCount: number,
|
|
isCompactMap: boolean,
|
|
): number {
|
|
if (manifestNationCount === 0) {
|
|
return 0;
|
|
}
|
|
if (isCompactMap) {
|
|
return Math.max(1, Math.floor(manifestNationCount * 0.25));
|
|
}
|
|
return manifestNationCount;
|
|
}
|
|
|
|
const PLURAL_NOUN = Symbol("plural!");
|
|
const NOUN = Symbol("noun!");
|
|
|
|
type NameTemplate = (string | typeof PLURAL_NOUN | typeof NOUN)[];
|
|
|
|
const NAME_TEMPLATES: NameTemplate[] = [
|
|
["World Famous", NOUN],
|
|
["Famous", PLURAL_NOUN],
|
|
["Comically Large", NOUN],
|
|
["Comically Small", NOUN],
|
|
["Massive", PLURAL_NOUN],
|
|
["Friendly", NOUN],
|
|
["Evil", NOUN],
|
|
["Malicious", NOUN],
|
|
["Spiteful", NOUN],
|
|
["Suspicious", NOUN],
|
|
["Canonically Evil", NOUN],
|
|
["Limited Edition", NOUN],
|
|
["Patent Pending", NOUN],
|
|
["Patented", NOUN],
|
|
["Space", NOUN],
|
|
["Defend The", PLURAL_NOUN],
|
|
["Anarchist", NOUN],
|
|
["Republic of", PLURAL_NOUN],
|
|
["Slippery", NOUN],
|
|
["Wealthy", PLURAL_NOUN],
|
|
["Certified", NOUN],
|
|
["Dr", NOUN],
|
|
["Runaway", NOUN],
|
|
["Chrome", NOUN],
|
|
["All New", NOUN],
|
|
["Top Shelf", PLURAL_NOUN],
|
|
["Invading", PLURAL_NOUN],
|
|
["Loyal To", PLURAL_NOUN],
|
|
["United States of", NOUN],
|
|
["United States of", PLURAL_NOUN],
|
|
["Flowing Rivers of", NOUN],
|
|
["House of", PLURAL_NOUN],
|
|
["Certified Organic", NOUN],
|
|
["Unregulated", NOUN],
|
|
|
|
[NOUN, "For Hire"],
|
|
[PLURAL_NOUN, "That Bite"],
|
|
[PLURAL_NOUN, "Are Opps"],
|
|
[NOUN, "Hotel"],
|
|
[PLURAL_NOUN, "The Movie"],
|
|
[NOUN, "Corporation"],
|
|
[PLURAL_NOUN, "Inc"],
|
|
[NOUN, "Democracy"],
|
|
[NOUN, "Network"],
|
|
[NOUN, "Railway"],
|
|
[NOUN, "Congress"],
|
|
[NOUN, "Alliance"],
|
|
[NOUN, "Island"],
|
|
[NOUN, "Kingdom"],
|
|
[NOUN, "Empire"],
|
|
[NOUN, "Dynasty"],
|
|
[NOUN, "Cartel"],
|
|
[NOUN, "Cabal"],
|
|
[NOUN, "Land"],
|
|
[NOUN, "Oligarchy"],
|
|
[NOUN, "Nationalist"],
|
|
[NOUN, "State"],
|
|
[NOUN, "Duchy"],
|
|
[NOUN, "Ocean"],
|
|
|
|
["Alternate", NOUN, "Universe"],
|
|
["Famous", NOUN, "Collection"],
|
|
["Supersonic", NOUN, "Spaceship"],
|
|
["Secret", NOUN, "Agenda"],
|
|
["Ballistic", NOUN, "Missile"],
|
|
["The", PLURAL_NOUN, "are SPIES"],
|
|
["Traveling", NOUN, "Circus"],
|
|
["The", PLURAL_NOUN, "Lied"],
|
|
["Sacred", NOUN, "Knowledge"],
|
|
["Quantum", NOUN, "Computer"],
|
|
["Hadron", NOUN, "Collider"],
|
|
["Large", NOUN, "Obliterator"],
|
|
["Interstellar", NOUN, "Cabal"],
|
|
["Interstellar", NOUN, "Army"],
|
|
["Interstellar", NOUN, "Pirates"],
|
|
["Interstellar", NOUN, "Dynasty"],
|
|
["Interstellar", NOUN, "Clan"],
|
|
["Galactic", NOUN, "Smugglers"],
|
|
["Galactic", NOUN, "Cabal"],
|
|
["Galactic", NOUN, "Alliance"],
|
|
["Galactic", NOUN, "Empire"],
|
|
["Galactic", NOUN, "Army"],
|
|
["Galactic", NOUN, "Crown"],
|
|
["Galactic", NOUN, "Pirates"],
|
|
["Galactic", NOUN, "Dynasty"],
|
|
["Galactic", NOUN, "Clan"],
|
|
["Alien", NOUN, "Army"],
|
|
["Alien", NOUN, "Cabal"],
|
|
["Alien", NOUN, "Alliance"],
|
|
["Alien", NOUN, "Empire"],
|
|
["Alien", NOUN, "Pirates"],
|
|
["Alien", NOUN, "Clan"],
|
|
["Grand", NOUN, "Empire"],
|
|
["Grand", NOUN, "Dynasty"],
|
|
["Grand", NOUN, "Army"],
|
|
["Grand", NOUN, "Cabal"],
|
|
["Grand", NOUN, "Alliance"],
|
|
["Royal", NOUN, "Army"],
|
|
["Royal", NOUN, "Cabal"],
|
|
["Royal", NOUN, "Empire"],
|
|
["Royal", NOUN, "Dynasty"],
|
|
["Holy", NOUN, "Dynasty"],
|
|
["Holy", NOUN, "Empire"],
|
|
["Holy", NOUN, "Alliance"],
|
|
["Eternal", NOUN, "Empire"],
|
|
["Eternal", NOUN, "Cabal"],
|
|
["Eternal", NOUN, "Alliance"],
|
|
["Eternal", NOUN, "Dynasty"],
|
|
["Invading", NOUN, "Cabal"],
|
|
["Invading", NOUN, "Empire"],
|
|
["Invading", NOUN, "Alliance"],
|
|
["Immortal", NOUN, "Pirates"],
|
|
["Shadow", NOUN, "Cabal"],
|
|
["Secret", NOUN, "Dynasty"],
|
|
["The Great", NOUN, "Army"],
|
|
["The", NOUN, "Matrix"],
|
|
];
|
|
|
|
const NOUNS = [
|
|
"Snail",
|
|
"Cow",
|
|
"Giraffe",
|
|
"Donkey",
|
|
"Horse",
|
|
"Mushroom",
|
|
"Salad",
|
|
"Kitten",
|
|
"Fork",
|
|
"Apple",
|
|
"Pancake",
|
|
"Tree",
|
|
"Fern",
|
|
"Seashell",
|
|
"Turtle",
|
|
"Casserole",
|
|
"Gnome",
|
|
"Frog",
|
|
"Cheese",
|
|
"Mold",
|
|
"Clown",
|
|
"Boat",
|
|
"Robot",
|
|
"Millionaire",
|
|
"Billionaire",
|
|
"Pigeon",
|
|
"Fish",
|
|
"Bumblebee",
|
|
"Jelly",
|
|
"Wizard",
|
|
"Worm",
|
|
"Rat",
|
|
"Pumpkin",
|
|
"Zombie",
|
|
"Grass",
|
|
"Bear",
|
|
"Skunk",
|
|
"Sandwich",
|
|
"Butter",
|
|
"Soda",
|
|
"Pickle",
|
|
"Potato",
|
|
"Book",
|
|
"Friend",
|
|
"Feather",
|
|
"Flower",
|
|
"Oil",
|
|
"Train",
|
|
"Fan",
|
|
"Salmon",
|
|
"Cod",
|
|
"Sink",
|
|
"Villain",
|
|
"Bug",
|
|
"Car",
|
|
"Soup",
|
|
"Puppy",
|
|
"Rock",
|
|
"Stick",
|
|
"Succulent",
|
|
"Nerd",
|
|
"Mercenary",
|
|
"Ninja",
|
|
"Burger",
|
|
"Tomato",
|
|
"Penguin",
|
|
];
|
|
|
|
function generateNationName(random: PseudoRandom): string {
|
|
const template = NAME_TEMPLATES[random.nextInt(0, NAME_TEMPLATES.length)];
|
|
const noun = NOUNS[random.nextInt(0, NOUNS.length)];
|
|
|
|
const result: string[] = [];
|
|
|
|
for (const part of template) {
|
|
if (part === PLURAL_NOUN) {
|
|
result.push(pluralize(noun));
|
|
} else if (part === NOUN) {
|
|
result.push(noun);
|
|
} else {
|
|
result.push(part);
|
|
}
|
|
}
|
|
|
|
return result.join(" ");
|
|
}
|
|
|
|
// Words from NOUNS that need irregular "-oes" plural
|
|
const O_TO_OES = new Set(["Potato", "Tomato"]);
|
|
|
|
function pluralize(noun: string): string {
|
|
if (
|
|
noun.endsWith("s") ||
|
|
noun.endsWith("ch") ||
|
|
noun.endsWith("sh") ||
|
|
noun.endsWith("x") ||
|
|
noun.endsWith("z")
|
|
) {
|
|
return `${noun}es`;
|
|
}
|
|
if (noun.endsWith("y") && !"aeiou".includes(noun[noun.length - 2])) {
|
|
return `${noun.slice(0, -1)}ies`;
|
|
}
|
|
if (O_TO_OES.has(noun)) {
|
|
return `${noun}es`;
|
|
}
|
|
return `${noun}s`;
|
|
}
|