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:
Adarsh Das
2025-10-28 16:40:30 -07:00
committed by GitHub
co-authored by Evan
parent 02bdaaa2c6
commit 0789f0d7f8
10 changed files with 154 additions and 53 deletions
+65 -33
View File
@@ -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>),
+47 -18
View File
@@ -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,