mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:30:43 +00:00
Quads (#1347)
## Description: - Add trios and quads - Add translations for duos, trios, quads - Add duos, trios, quads to the public lobby rotation - Increase the frequency of team games   ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors
This commit is contained in:
@@ -178,6 +178,9 @@
|
||||
"public_lobby": {
|
||||
"join": "Join next Game",
|
||||
"waiting": "players waiting",
|
||||
"teams_Duos": "Duos (teams of 2)",
|
||||
"teams_Trios": "Trios (teams of 3)",
|
||||
"teams_Quads": "Quads (teams of 4)",
|
||||
"teams": "{num} teams"
|
||||
},
|
||||
"username": {
|
||||
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
Duos,
|
||||
GameMapType,
|
||||
GameMode,
|
||||
Quads,
|
||||
Trios,
|
||||
UnitType,
|
||||
mapCategories,
|
||||
} from "../core/game/Game";
|
||||
import { GameConfig, GameInfo } from "../core/Schemas";
|
||||
import { GameConfig, GameInfo, TeamCountConfig } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import "./components/baseComponents/Modal";
|
||||
import "./components/Difficulties";
|
||||
@@ -30,7 +32,7 @@ export class HostLobbyModal extends LitElement {
|
||||
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
|
||||
@state() private disableNPCs = false;
|
||||
@state() private gameMode: GameMode = GameMode.FFA;
|
||||
@state() private teamCount: number | typeof Duos = 2;
|
||||
@state() private teamCount: TeamCountConfig = 2;
|
||||
@state() private bots: number = 400;
|
||||
@state() private infiniteGold: boolean = false;
|
||||
@state() private infiniteTroops: boolean = false;
|
||||
@@ -196,7 +198,7 @@ export class HostLobbyModal extends LitElement {
|
||||
${translateText("host_modal.team_count")}
|
||||
</div>
|
||||
<div class="option-cards">
|
||||
${[Duos, 2, 3, 4, 5, 6, 7].map(
|
||||
${[2, 3, 4, 5, 6, 7, Quads, Trios, Duos].map(
|
||||
(o) => html`
|
||||
<div
|
||||
class="option-card ${this.teamCount === o
|
||||
@@ -204,7 +206,13 @@ export class HostLobbyModal extends LitElement {
|
||||
: ""}"
|
||||
@click=${() => this.handleTeamCountSelection(o)}
|
||||
>
|
||||
<div class="option-card-title">${o}</div>
|
||||
<div class="option-card-title">
|
||||
${typeof o === "string"
|
||||
? translateText(`public_lobby.teams_${o}`)
|
||||
: translateText("public_lobby.teams", {
|
||||
num: o,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
@@ -465,8 +473,8 @@ export class HostLobbyModal extends LitElement {
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async handleTeamCountSelection(value: number | typeof Duos) {
|
||||
this.teamCount = value === Duos ? Duos : Number(value);
|
||||
private async handleTeamCountSelection(value: TeamCountConfig) {
|
||||
this.teamCount = value;
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,11 @@ export class PublicLobby extends LitElement {
|
||||
: "text-blue-600"} bg-white rounded-sm px-1"
|
||||
>
|
||||
${lobby.gameConfig.gameMode === GameMode.Team
|
||||
? translateText("public_lobby.teams", { num: teamCount ?? 0 })
|
||||
? typeof teamCount === "string"
|
||||
? translateText(`public_lobby.teams_${teamCount}`)
|
||||
: translateText("public_lobby.teams", {
|
||||
num: teamCount ?? 0,
|
||||
})
|
||||
: translateText("game_mode.ffa")}</span
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -8,10 +8,13 @@ import {
|
||||
GameMapType,
|
||||
GameMode,
|
||||
GameType,
|
||||
Quads,
|
||||
Trios,
|
||||
UnitType,
|
||||
mapCategories,
|
||||
} from "../core/game/Game";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import { TeamCountConfig } from "../core/Schemas";
|
||||
import { generateID } from "../core/Util";
|
||||
import "./components/baseComponents/Button";
|
||||
import "./components/baseComponents/Modal";
|
||||
@@ -38,7 +41,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
@state() private instantBuild: boolean = false;
|
||||
@state() private useRandomMap: boolean = false;
|
||||
@state() private gameMode: GameMode = GameMode.FFA;
|
||||
@state() private teamCount: number | typeof Duos = 2;
|
||||
@state() private teamCount: TeamCountConfig = 2;
|
||||
|
||||
@state() private disabledUnits: UnitType[] = [UnitType.Factory];
|
||||
|
||||
@@ -171,7 +174,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
${translateText("host_modal.team_count")}
|
||||
</div>
|
||||
<div class="option-cards">
|
||||
${["Duos", 2, 3, 4, 5, 6, 7].map(
|
||||
${[2, 3, 4, 5, 6, 7, Quads, Trios, Duos].map(
|
||||
(o) => html`
|
||||
<div
|
||||
class="option-card ${this.teamCount === o
|
||||
@@ -179,7 +182,11 @@ export class SinglePlayerModal extends LitElement {
|
||||
: ""}"
|
||||
@click=${() => this.handleTeamCountSelection(o)}
|
||||
>
|
||||
<div class="option-card-title">${o}</div>
|
||||
<div class="option-card-title">
|
||||
${typeof o === "string"
|
||||
? translateText(`public_lobby.teams_${o}`)
|
||||
: translateText(`public_lobby.teams`, { num: o })}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
@@ -358,8 +365,8 @@ export class SinglePlayerModal extends LitElement {
|
||||
this.gameMode = value;
|
||||
}
|
||||
|
||||
private handleTeamCountSelection(value: number | string) {
|
||||
this.teamCount = value === "Duos" ? Duos : Number(value);
|
||||
private handleTeamCountSelection(value: TeamCountConfig) {
|
||||
this.teamCount = value;
|
||||
}
|
||||
|
||||
private getRandomMap(): GameMapType {
|
||||
|
||||
+11
-1
@@ -9,6 +9,8 @@ import {
|
||||
GameMode,
|
||||
GameType,
|
||||
PlayerType,
|
||||
Quads,
|
||||
Trios,
|
||||
UnitType,
|
||||
} from "./game/Game";
|
||||
import { PatternDecoder } from "./PatternDecoder";
|
||||
@@ -131,6 +133,14 @@ export enum LogSeverity {
|
||||
// Utility types
|
||||
//
|
||||
|
||||
const TeamCountConfigSchema = z.union([
|
||||
z.number(),
|
||||
z.literal(Duos),
|
||||
z.literal(Trios),
|
||||
z.literal(Quads),
|
||||
]);
|
||||
export type TeamCountConfig = z.infer<typeof TeamCountConfigSchema>;
|
||||
|
||||
export const GameConfigSchema = z.object({
|
||||
gameMap: z.enum(GameMapType),
|
||||
difficulty: z.enum(Difficulty),
|
||||
@@ -143,7 +153,7 @@ export const GameConfigSchema = z.object({
|
||||
instantBuild: z.boolean(),
|
||||
maxPlayers: z.number().optional(),
|
||||
disabledUnits: z.enum(UnitType).array().optional(),
|
||||
playerTeams: z.union([z.number(), z.literal(Duos)]).optional(),
|
||||
playerTeams: TeamCountConfigSchema.optional(),
|
||||
});
|
||||
|
||||
export const TeamSchema = z.string();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Colord } from "colord";
|
||||
import { JWK } from "jose";
|
||||
import { GameConfig, GameID } from "../Schemas";
|
||||
import { GameConfig, GameID, TeamCountConfig } from "../Schemas";
|
||||
import {
|
||||
Difficulty,
|
||||
Duos,
|
||||
Game,
|
||||
GameMapType,
|
||||
GameMode,
|
||||
@@ -32,7 +31,7 @@ export interface ServerConfig {
|
||||
lobbyMaxPlayers(
|
||||
map: GameMapType,
|
||||
mode: GameMode,
|
||||
numPlayerTeams: number | undefined,
|
||||
numPlayerTeams: TeamCountConfig | undefined,
|
||||
): number;
|
||||
numWorkers(): number;
|
||||
workerIndex(gameID: GameID): number;
|
||||
@@ -87,7 +86,7 @@ export interface Config {
|
||||
instantBuild(): boolean;
|
||||
numSpawnPhaseTurns(): number;
|
||||
userSettings(): UserSettings;
|
||||
playerTeams(): number | typeof Duos;
|
||||
playerTeams(): TeamCountConfig;
|
||||
|
||||
startManpower(playerInfo: PlayerInfo): number;
|
||||
populationIncreaseRate(player: Player | PlayerView): number;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { JWK } from "jose";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
Difficulty,
|
||||
Duos,
|
||||
Game,
|
||||
GameMapType,
|
||||
GameMode,
|
||||
@@ -20,7 +19,7 @@ import {
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { PlayerView } from "../game/GameView";
|
||||
import { UserSettings } from "../game/UserSettings";
|
||||
import { GameConfig, GameID } from "../Schemas";
|
||||
import { GameConfig, GameID, TeamCountConfig } from "../Schemas";
|
||||
import { assertNever, simpleHash, within } from "../Util";
|
||||
import { Config, GameEnv, NukeMagnitude, ServerConfig, Theme } from "./Config";
|
||||
import { PastelTheme } from "./PastelTheme";
|
||||
@@ -167,13 +166,13 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
lobbyMaxPlayers(
|
||||
map: GameMapType,
|
||||
mode: GameMode,
|
||||
numPlayerTeams: number | undefined,
|
||||
numPlayerTeams: TeamCountConfig | undefined,
|
||||
): number {
|
||||
const [l, m, s] = numPlayersConfig[map] ?? [50, 30, 20];
|
||||
const r = Math.random();
|
||||
const base = r < 0.3 ? l : r < 0.6 ? m : s;
|
||||
let p = Math.min(mode === GameMode.Team ? Math.ceil(base * 1.5) : base, l);
|
||||
if (numPlayerTeams !== undefined) {
|
||||
if (typeof numPlayerTeams === "number") {
|
||||
p -= p % numPlayerTeams;
|
||||
}
|
||||
return p;
|
||||
@@ -279,7 +278,7 @@ export class DefaultConfig implements Config {
|
||||
defensePostDefenseBonus(): number {
|
||||
return 5;
|
||||
}
|
||||
playerTeams(): number | typeof Duos {
|
||||
playerTeams(): TeamCountConfig {
|
||||
return this._gameConfig.playerTeams ?? 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ export enum Difficulty {
|
||||
export type Team = string;
|
||||
|
||||
export const Duos = "Duos" as const;
|
||||
export const Trios = "Trios" as const;
|
||||
export const Quads = "Quads" as const;
|
||||
|
||||
export const ColoredTeams: Record<string, Team> = {
|
||||
Red: "Red",
|
||||
|
||||
+28
-14
@@ -21,9 +21,11 @@ import {
|
||||
PlayerID,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
Quads,
|
||||
Team,
|
||||
TerrainType,
|
||||
TerraNullius,
|
||||
Trios,
|
||||
Unit,
|
||||
UnitInfo,
|
||||
UnitType,
|
||||
@@ -74,7 +76,7 @@ export class GameImpl implements Game {
|
||||
private updates: GameUpdates = createGameUpdatesMap();
|
||||
private unitGrid: UnitGrid;
|
||||
|
||||
private playerTeams: Team[] = [ColoredTeams.Red, ColoredTeams.Blue];
|
||||
private playerTeams: Team[];
|
||||
private botTeam: Team = ColoredTeams.Bot;
|
||||
private _railNetwork: RailNetwork = createRailNetwork(this);
|
||||
|
||||
@@ -101,25 +103,37 @@ export class GameImpl implements Game {
|
||||
}
|
||||
|
||||
private populateTeams() {
|
||||
if (this._config.playerTeams() === Duos) {
|
||||
this.playerTeams = [];
|
||||
const numTeams = Math.ceil(
|
||||
(this._humans.length + this._nations.length) / 2,
|
||||
);
|
||||
for (let i = 0; i < numTeams; i++) {
|
||||
this.playerTeams.push("Team " + (i + 1));
|
||||
let numPlayerTeams = this._config.playerTeams();
|
||||
if (typeof numPlayerTeams !== "number") {
|
||||
const players = this._humans.length + this._nations.length;
|
||||
switch (numPlayerTeams) {
|
||||
case Duos:
|
||||
numPlayerTeams = Math.ceil(players / 2);
|
||||
break;
|
||||
case Trios:
|
||||
numPlayerTeams = Math.ceil(players / 3);
|
||||
break;
|
||||
case Quads:
|
||||
numPlayerTeams = Math.ceil(players / 4);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown TeamCountConfig ${numPlayerTeams}`);
|
||||
}
|
||||
} else {
|
||||
const numPlayerTeams = this._config.playerTeams() as number;
|
||||
if (numPlayerTeams < 2)
|
||||
throw new Error(`Too few teams: ${numPlayerTeams}`);
|
||||
}
|
||||
if (numPlayerTeams < 2) {
|
||||
throw new Error(`Too few teams: ${numPlayerTeams}`);
|
||||
} else if (numPlayerTeams < 8) {
|
||||
this.playerTeams = [ColoredTeams.Red, ColoredTeams.Blue];
|
||||
if (numPlayerTeams >= 3) this.playerTeams.push(ColoredTeams.Yellow);
|
||||
if (numPlayerTeams >= 4) this.playerTeams.push(ColoredTeams.Green);
|
||||
if (numPlayerTeams >= 5) this.playerTeams.push(ColoredTeams.Purple);
|
||||
if (numPlayerTeams >= 6) this.playerTeams.push(ColoredTeams.Orange);
|
||||
if (numPlayerTeams >= 7) this.playerTeams.push(ColoredTeams.Teal);
|
||||
if (numPlayerTeams >= 8)
|
||||
throw new Error(`Too many teams: ${numPlayerTeams}`);
|
||||
} else {
|
||||
this.playerTeams = [];
|
||||
for (let i = 1; i <= numPlayerTeams; i++) {
|
||||
this.playerTeams.push(`Team ${i}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
||||
import {
|
||||
Difficulty,
|
||||
Duos,
|
||||
GameMapType,
|
||||
GameMode,
|
||||
GameType,
|
||||
Quads,
|
||||
Trios,
|
||||
UnitType,
|
||||
} from "../core/game/Game";
|
||||
import { PseudoRandom } from "../core/PseudoRandom";
|
||||
import { GameConfig } from "../core/Schemas";
|
||||
import { GameConfig, TeamCountConfig } from "../core/Schemas";
|
||||
import { logger } from "./Logger";
|
||||
|
||||
const log = logger.child({});
|
||||
@@ -45,19 +48,31 @@ interface MapWithMode {
|
||||
mode: GameMode;
|
||||
}
|
||||
|
||||
const TEAM_COUNTS = [
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
Duos,
|
||||
Trios,
|
||||
Quads,
|
||||
] as const satisfies TeamCountConfig[];
|
||||
|
||||
export class MapPlaylist {
|
||||
private mapsPlaylist: MapWithMode[] = [];
|
||||
|
||||
public gameConfig(): GameConfig {
|
||||
const { map, mode } = this.getNextMap();
|
||||
|
||||
const numPlayerTeams =
|
||||
mode === GameMode.Team ? 2 + Math.floor(Math.random() * 5) : undefined;
|
||||
const playerTeams =
|
||||
mode === GameMode.Team ? this.getTeamCount() : undefined;
|
||||
|
||||
// Create the default public game config (from your GameManager)
|
||||
return {
|
||||
gameMap: map,
|
||||
maxPlayers: config.lobbyMaxPlayers(map, mode, numPlayerTeams),
|
||||
maxPlayers: config.lobbyMaxPlayers(map, mode, playerTeams),
|
||||
gameType: GameType.Public,
|
||||
difficulty: Difficulty.Medium,
|
||||
infiniteGold: false,
|
||||
@@ -65,12 +80,16 @@ export class MapPlaylist {
|
||||
instantBuild: false,
|
||||
disableNPCs: mode === GameMode.Team,
|
||||
gameMode: mode,
|
||||
playerTeams: numPlayerTeams,
|
||||
playerTeams,
|
||||
bots: 400,
|
||||
disabledUnits: [UnitType.Train, UnitType.Factory],
|
||||
} satisfies GameConfig;
|
||||
}
|
||||
|
||||
private getTeamCount(): TeamCountConfig {
|
||||
return TEAM_COUNTS[Math.floor(Math.random() * TEAM_COUNTS.length)];
|
||||
}
|
||||
|
||||
private getNextMap(): MapWithMode {
|
||||
if (this.mapsPlaylist.length === 0) {
|
||||
const numAttempts = 10000;
|
||||
@@ -98,7 +117,6 @@ export class MapPlaylist {
|
||||
|
||||
const ffa1: GameMapType[] = rand.shuffleArray([...maps]);
|
||||
const ffa2: GameMapType[] = rand.shuffleArray([...maps]);
|
||||
const ffa3: GameMapType[] = rand.shuffleArray([...maps]);
|
||||
const team: GameMapType[] = rand.shuffleArray([...maps]);
|
||||
|
||||
this.mapsPlaylist = [];
|
||||
@@ -109,9 +127,6 @@ export class MapPlaylist {
|
||||
if (!this.addNextMap(this.mapsPlaylist, ffa2, GameMode.FFA)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.addNextMap(this.mapsPlaylist, ffa3, GameMode.FFA)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.addNextMap(this.mapsPlaylist, team, GameMode.Team)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user