Handle Nation win condition (#2824)

Resolves #2823

## Description:

When playing in single-player mode, if an NPC reaches 80% land control
before the player, the game enters a broken state where:

- The game clock stops
- Win checking stops permanently
- Even if the player later conquers 100% of land, victory is never
awarded
- The game becomes "stuck" in a zombie state.

This PR addresses this allowing Nations to be set as winners in single
mode, and in this case showing a "Nation {nation} has won" modal to the
user. This WinModal is the same as the "{player} has won", with the only
change being the title.

Nation wins in FFA, from the human player perspective:

<img width="1457" height="837" alt="image"
src="https://github.com/user-attachments/assets/1ce569bd-6616-4a23-b4a4-afedad2c64f8"
/>

## 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:

deshack_82603
This commit is contained in:
Mattia Migliorini
2026-01-08 22:51:23 +01:00
committed by GitHub
parent b090f2f624
commit 2dada6f516
5 changed files with 299 additions and 2 deletions
+6
View File
@@ -303,6 +303,12 @@ export class WinModal extends LitElement implements Layer {
this.isWin = false;
}
this.show();
} else if (wu.winner[0] === "nation") {
this._title = translateText("win_modal.nation_won", {
nation: wu.winner[1],
});
this.isWin = false;
this.show();
} else {
const winner = this.game.playerByClientID(wu.winner[1]);
if (!winner?.isPlayer()) return;
+1
View File
@@ -472,6 +472,7 @@ export const WinnerSchema = z
.union([
z.tuple([z.literal("player"), ID]).rest(ID),
z.tuple([z.literal("team"), SafeString]).rest(ID),
z.tuple([z.literal("nation"), SafeString]).rest(ID),
])
.optional();
export type Winner = z.infer<typeof WinnerSchema>;
+3 -1
View File
@@ -718,7 +718,9 @@ export class GameImpl implements Game {
];
} else {
const clientId = winner.clientID();
if (clientId === null) return;
if (clientId === null) {
return ["nation", winner.name()];
}
return [
"player",
clientId,