mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
Add Nations Vs Players Game Mode (#2233)
## Description: Fixes: #676 This PR adds Players Vs Nations as a game mode in the menu. For this change I have added two mutually exclusive option for this mode: 1. Match number of nations to number of players who have joined 2. Set the number of nations to a fixed value ### Screenshots: #### Options in Single player mode <img width="1025" height="790" alt="image" src="https://github.com/user-attachments/assets/c0685ea5-94f5-43c7-a9e5-390835fc94e9" /> <img width="1005" height="795" alt="image" src="https://github.com/user-attachments/assets/dddba015-a424-40dd-a0fe-2571fd7b0fba" /> #### Options in lobby mode <img width="1015" height="888" alt="image" src="https://github.com/user-attachments/assets/45bc865b-c6a8-4b6a-9062-4eb499c1ea36" /> #### Example gameplay (1 Human Vs 90 Nations) <img width="1888" height="912" alt="image" src="https://github.com/user-attachments/assets/38faec75-171f-4358-a3be-93630cca1587" /> ## 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: saphereye --------- Co-authored-by: Evan <evanpelle@gmail.com>
This commit is contained in:
@@ -139,6 +139,7 @@
|
||||
"options_title": "Options",
|
||||
"bots": "Bots: ",
|
||||
"bots_disabled": "Disabled",
|
||||
"nations": "Nations: ",
|
||||
"disable_nations": "Disable Nations",
|
||||
"instant_build": "Instant build",
|
||||
"infinite_gold": "Infinite gold",
|
||||
@@ -146,6 +147,7 @@
|
||||
"compact_map": "Mini Map",
|
||||
"max_timer": "Game length (minutes)",
|
||||
"disable_nukes": "Disable Nukes",
|
||||
"automatic_difficulty": "Automatic Difficulty",
|
||||
"enables_title": "Enable Settings",
|
||||
"start": "Start Game"
|
||||
},
|
||||
@@ -222,6 +224,7 @@
|
||||
"teams_Duos": "Duos (teams of 2)",
|
||||
"teams_Trios": "Trios (teams of 3)",
|
||||
"teams_Quads": "Quads (teams of 4)",
|
||||
"teams_hvn": "Humans Vs Nations",
|
||||
"teams": "{num} teams"
|
||||
},
|
||||
"matchmaking_modal": {
|
||||
@@ -244,6 +247,7 @@
|
||||
"options_title": "Options",
|
||||
"bots": "Bots: ",
|
||||
"bots_disabled": "Disabled",
|
||||
"nations": "Nations: ",
|
||||
"disable_nations": "Disable Nations",
|
||||
"max_timer": "Game length (minutes)",
|
||||
"instant_build": "Instant build",
|
||||
@@ -252,6 +256,7 @@
|
||||
"infinite_troops": "Infinite troops",
|
||||
"donate_troops": "Donate troops",
|
||||
"compact_map": "Mini Map",
|
||||
"automatic_difficulty": "Automatic Difficulty",
|
||||
"enables_title": "Enable Settings",
|
||||
"player": "Player",
|
||||
"players": "Players",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
GameMapSize,
|
||||
GameMapType,
|
||||
GameMode,
|
||||
HumansVsNations,
|
||||
Quads,
|
||||
Trios,
|
||||
UnitType,
|
||||
@@ -284,7 +285,18 @@ export class HostLobbyModal extends LitElement {
|
||||
${translateText("host_modal.team_count")}
|
||||
</div>
|
||||
<div class="option-cards">
|
||||
${[2, 3, 4, 5, 6, 7, Quads, Trios, Duos].map(
|
||||
${[
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
Quads,
|
||||
Trios,
|
||||
Duos,
|
||||
HumansVsNations,
|
||||
].map(
|
||||
(o) => html`
|
||||
<div
|
||||
class="option-card ${this.teamCount === o
|
||||
@@ -294,7 +306,9 @@ export class HostLobbyModal extends LitElement {
|
||||
>
|
||||
<div class="option-card-title">
|
||||
${typeof o === "string"
|
||||
? translateText(`public_lobby.teams_${o}`)
|
||||
? o === HumansVsNations
|
||||
? translateText("public_lobby.teams_hvn")
|
||||
: translateText(`public_lobby.teams_${o}`)
|
||||
: translateText("public_lobby.teams", {
|
||||
num: o,
|
||||
})}
|
||||
@@ -313,42 +327,53 @@ export class HostLobbyModal extends LitElement {
|
||||
${translateText("host_modal.options_title")}
|
||||
</div>
|
||||
<div class="option-cards">
|
||||
<label for="bots-count" class="option-card">
|
||||
<input
|
||||
type="range"
|
||||
id="bots-count"
|
||||
min="0"
|
||||
max="400"
|
||||
step="1"
|
||||
@input=${this.handleBotsChange}
|
||||
@change=${this.handleBotsChange}
|
||||
.value="${String(this.bots)}"
|
||||
/>
|
||||
<div class="option-card-title">
|
||||
<span>${translateText("host_modal.bots")}</span>${
|
||||
this.bots === 0
|
||||
? translateText("host_modal.bots_disabled")
|
||||
: this.bots
|
||||
}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
for="disable-npcs"
|
||||
class="option-card ${this.disableNPCs ? "selected" : ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<label for="bots-count" class="option-card">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-npcs"
|
||||
@change=${this.handleDisableNPCsChange}
|
||||
.checked=${this.disableNPCs}
|
||||
type="range"
|
||||
id="bots-count"
|
||||
min="0"
|
||||
max="400"
|
||||
step="1"
|
||||
@input=${this.handleBotsChange}
|
||||
@change=${this.handleBotsChange}
|
||||
.value="${String(this.bots)}"
|
||||
/>
|
||||
<div class="option-card-title">
|
||||
${translateText("host_modal.disable_nations")}
|
||||
<span>${translateText("host_modal.bots")}</span>${
|
||||
this.bots === 0
|
||||
? translateText("host_modal.bots_disabled")
|
||||
: this.bots
|
||||
}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
${
|
||||
!(
|
||||
this.gameMode === GameMode.Team &&
|
||||
this.teamCount === HumansVsNations
|
||||
)
|
||||
? html`
|
||||
<label
|
||||
for="disable-npcs"
|
||||
class="option-card ${this.disableNPCs
|
||||
? "selected"
|
||||
: ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-npcs"
|
||||
@change=${this.handleDisableNPCsChange}
|
||||
.checked=${this.disableNPCs}
|
||||
/>
|
||||
<div class="option-card-title">
|
||||
${translateText("host_modal.disable_nations")}
|
||||
</div>
|
||||
</label>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<label
|
||||
for="instant-build"
|
||||
class="option-card ${this.instantBuild ? "selected" : ""}"
|
||||
@@ -718,7 +743,6 @@ export class HostLobbyModal extends LitElement {
|
||||
? GameMapSize.Compact
|
||||
: GameMapSize.Normal,
|
||||
difficulty: this.selectedDifficulty,
|
||||
disableNPCs: this.disableNPCs,
|
||||
bots: this.bots,
|
||||
infiniteGold: this.infiniteGold,
|
||||
donateGold: this.donateGold,
|
||||
@@ -728,6 +752,14 @@ export class HostLobbyModal extends LitElement {
|
||||
gameMode: this.gameMode,
|
||||
disabledUnits: this.disabledUnits,
|
||||
playerTeams: this.teamCount,
|
||||
...(this.gameMode === GameMode.Team &&
|
||||
this.teamCount === HumansVsNations
|
||||
? {
|
||||
disableNPCs: false,
|
||||
}
|
||||
: {
|
||||
disableNPCs: this.disableNPCs,
|
||||
}),
|
||||
maxTimerValue:
|
||||
this.maxTimer === true ? this.maxTimerValue : undefined,
|
||||
} satisfies Partial<GameConfig>),
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
GameMapType,
|
||||
GameMode,
|
||||
GameType,
|
||||
HumansVsNations,
|
||||
Quads,
|
||||
Trios,
|
||||
UnitType,
|
||||
@@ -195,7 +196,18 @@ export class SinglePlayerModal extends LitElement {
|
||||
${translateText("host_modal.team_count")}
|
||||
</div>
|
||||
<div class="option-cards">
|
||||
${[2, 3, 4, 5, 6, 7, Quads, Trios, Duos].map(
|
||||
${[
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
Quads,
|
||||
Trios,
|
||||
Duos,
|
||||
HumansVsNations,
|
||||
].map(
|
||||
(o) => html`
|
||||
<div
|
||||
class="option-card ${this.teamCount === o
|
||||
@@ -205,7 +217,9 @@ export class SinglePlayerModal extends LitElement {
|
||||
>
|
||||
<div class="option-card-title">
|
||||
${typeof o === "string"
|
||||
? translateText(`public_lobby.teams_${o}`)
|
||||
? o === HumansVsNations
|
||||
? translateText("public_lobby.teams_hvn")
|
||||
: translateText(`public_lobby.teams_${o}`)
|
||||
: translateText(`public_lobby.teams`, { num: o })}
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,21 +254,29 @@ export class SinglePlayerModal extends LitElement {
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
for="singleplayer-modal-disable-npcs"
|
||||
class="option-card ${this.disableNPCs ? "selected" : ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="singleplayer-modal-disable-npcs"
|
||||
@change=${this.handleDisableNPCsChange}
|
||||
.checked=${this.disableNPCs}
|
||||
/>
|
||||
<div class="option-card-title">
|
||||
${translateText("single_modal.disable_nations")}
|
||||
</div>
|
||||
</label>
|
||||
${!(
|
||||
this.gameMode === GameMode.Team &&
|
||||
this.teamCount === HumansVsNations
|
||||
)
|
||||
? html`
|
||||
<label
|
||||
for="singleplayer-modal-disable-npcs"
|
||||
class="option-card ${this.disableNPCs ? "selected" : ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="singleplayer-modal-disable-npcs"
|
||||
@change=${this.handleDisableNPCsChange}
|
||||
.checked=${this.disableNPCs}
|
||||
/>
|
||||
<div class="option-card-title">
|
||||
${translateText("single_modal.disable_nations")}
|
||||
</div>
|
||||
</label>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<label
|
||||
for="singleplayer-modal-instant-build"
|
||||
class="option-card ${this.instantBuild ? "selected" : ""}"
|
||||
@@ -534,7 +556,6 @@ export class SinglePlayerModal extends LitElement {
|
||||
gameMode: this.gameMode,
|
||||
playerTeams: this.teamCount,
|
||||
difficulty: this.selectedDifficulty,
|
||||
disableNPCs: this.disableNPCs,
|
||||
maxTimerValue: this.maxTimer ? this.maxTimerValue : undefined,
|
||||
bots: this.bots,
|
||||
infiniteGold: this.infiniteGold,
|
||||
@@ -545,6 +566,14 @@ export class SinglePlayerModal extends LitElement {
|
||||
disabledUnits: this.disabledUnits
|
||||
.map((u) => Object.values(UnitType).find((ut) => ut === u))
|
||||
.filter((ut): ut is UnitType => ut !== undefined),
|
||||
...(this.gameMode === GameMode.Team &&
|
||||
this.teamCount === HumansVsNations
|
||||
? {
|
||||
disableNPCs: false,
|
||||
}
|
||||
: {
|
||||
disableNPCs: this.disableNPCs,
|
||||
}),
|
||||
},
|
||||
},
|
||||
} satisfies JoinLobbyEvent,
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
GameMapType,
|
||||
GameMode,
|
||||
GameType,
|
||||
HumansVsNations,
|
||||
Quads,
|
||||
Trios,
|
||||
UnitType,
|
||||
@@ -149,6 +150,7 @@ const TeamCountConfigSchema = z.union([
|
||||
z.literal(Duos),
|
||||
z.literal(Trios),
|
||||
z.literal(Quads),
|
||||
z.literal(HumansVsNations),
|
||||
]);
|
||||
export type TeamCountConfig = z.infer<typeof TeamCountConfigSchema>;
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ export class ColorAllocator {
|
||||
return greenTeamColors;
|
||||
case ColoredTeams.Bot:
|
||||
return botTeamColors;
|
||||
case ColoredTeams.Humans:
|
||||
return blueTeamColors;
|
||||
case ColoredTeams.Nations:
|
||||
return redTeamColors;
|
||||
default:
|
||||
return [this.assignColor(team)];
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
GameMode,
|
||||
GameType,
|
||||
Gold,
|
||||
HumansVsNations,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
@@ -195,6 +196,9 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
case Quads:
|
||||
p -= p % 4;
|
||||
break;
|
||||
case HumansVsNations:
|
||||
// For HumansVsNations, return the base team player count
|
||||
break;
|
||||
default:
|
||||
p -= p % numPlayerTeams;
|
||||
break;
|
||||
|
||||
@@ -53,6 +53,7 @@ export type Team = string;
|
||||
export const Duos = "Duos" as const;
|
||||
export const Trios = "Trios" as const;
|
||||
export const Quads = "Quads" as const;
|
||||
export const HumansVsNations = "Humans Vs Nations" as const;
|
||||
|
||||
export const ColoredTeams: Record<string, Team> = {
|
||||
Red: "Red",
|
||||
@@ -63,6 +64,8 @@ export const ColoredTeams: Record<string, Team> = {
|
||||
Orange: "Orange",
|
||||
Green: "Green",
|
||||
Bot: "Bot",
|
||||
Humans: "Humans",
|
||||
Nations: "Nations",
|
||||
} as const;
|
||||
|
||||
export enum GameMapType {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Game,
|
||||
GameMode,
|
||||
GameUpdates,
|
||||
HumansVsNations,
|
||||
MessageType,
|
||||
MutableAlliance,
|
||||
Nation,
|
||||
@@ -105,6 +106,13 @@ export class GameImpl implements Game {
|
||||
|
||||
private populateTeams() {
|
||||
let numPlayerTeams = this._config.playerTeams();
|
||||
|
||||
// HumansVsNations mode always has exactly 2 teams
|
||||
if (numPlayerTeams === HumansVsNations) {
|
||||
this.playerTeams = [ColoredTeams.Humans, ColoredTeams.Nations];
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof numPlayerTeams !== "number") {
|
||||
const players = this._humans.length + this._nations.length;
|
||||
switch (numPlayerTeams) {
|
||||
@@ -139,11 +147,21 @@ export class GameImpl implements Game {
|
||||
}
|
||||
|
||||
private addPlayers() {
|
||||
if (this.config().gameConfig().gameMode !== GameMode.Team) {
|
||||
if (this.config().gameConfig().gameMode === GameMode.FFA) {
|
||||
this._humans.forEach((p) => this.addPlayer(p));
|
||||
this._nations.forEach((n) => this.addPlayer(n.playerInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._config.playerTeams() === HumansVsNations) {
|
||||
this._humans.forEach((p) => this.addPlayer(p, ColoredTeams.Humans));
|
||||
this._nations.forEach((n) =>
|
||||
this.addPlayer(n.playerInfo, ColoredTeams.Nations),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Team mode
|
||||
const allPlayers = [
|
||||
...this._humans,
|
||||
...this._nations.map((n) => n.playerInfo),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
GameMapType,
|
||||
GameMode,
|
||||
GameType,
|
||||
HumansVsNations,
|
||||
Quads,
|
||||
Trios,
|
||||
} from "../core/game/Game";
|
||||
@@ -67,6 +68,7 @@ const TEAM_COUNTS = [
|
||||
Duos,
|
||||
Trios,
|
||||
Quads,
|
||||
HumansVsNations,
|
||||
] as const satisfies TeamCountConfig[];
|
||||
|
||||
export class MapPlaylist {
|
||||
@@ -93,7 +95,7 @@ export class MapPlaylist {
|
||||
infiniteTroops: false,
|
||||
maxTimerValue: undefined,
|
||||
instantBuild: false,
|
||||
disableNPCs: mode === GameMode.Team,
|
||||
disableNPCs: mode === GameMode.Team && playerTeams !== HumansVsNations,
|
||||
gameMode: mode,
|
||||
playerTeams,
|
||||
bots: 400,
|
||||
|
||||
@@ -90,6 +90,8 @@ describe("ColorAllocator", () => {
|
||||
expect(allocator.assignTeamColor(ColoredTeams.Orange)).toEqual(orange);
|
||||
expect(allocator.assignTeamColor(ColoredTeams.Green)).toEqual(green);
|
||||
expect(allocator.assignTeamColor(ColoredTeams.Bot)).toEqual(botColor);
|
||||
expect(allocator.assignTeamColor(ColoredTeams.Humans)).toEqual(blue);
|
||||
expect(allocator.assignTeamColor(ColoredTeams.Nations)).toEqual(red);
|
||||
});
|
||||
|
||||
test("assignTeamPlayerColor always returns the same color for the same playerID", () => {
|
||||
|
||||
Reference in New Issue
Block a user