Handle confirmation on popstate event if player is active in a game (#2777)

Please merge it into v29, since in that version the back navigation out
of a game is currently **broken** after switching from hash-based to
path-based routing via #2740

## Description:

Protect against players accidentally leaving an active game by pressing
the browser back button. Uses the same confirmation dialog as the game
exit button.

Partially handles issue #1877 (protects against back button, not closing
tab or editing the URL directly).

<img width="861" height="373" alt="image"
src="https://github.com/user-attachments/assets/167cc137-6df3-44a7-a594-91ffd904857d"
/>

Partial credit to PR #2141

## 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-16 17:09:08 -08:00
committed by GitHub
parent 2fcca8ee26
commit b1d63533d5
2 changed files with 78 additions and 19 deletions
+36 -3
View File
@@ -69,7 +69,7 @@ export function joinLobby(
lobbyConfig: LobbyConfig,
onPrestart: () => void,
onJoin: () => void,
): () => void {
): (force?: boolean) => boolean {
console.log(
`joining lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}`,
);
@@ -79,6 +79,8 @@ export function joinLobby(
const transport = new Transport(lobbyConfig, eventBus);
let currentGameRunner: ClientGameRunner | null = null;
let hasJoined = false;
const onconnect = () => {
@@ -122,9 +124,15 @@ export function joinLobby(
terrainLoad,
terrainMapFileLoader,
)
.then((r) => r.start())
.then((r) => {
currentGameRunner = r;
r.start();
})
.catch((e) => {
console.error("error creating client game", e);
currentGameRunner = null;
const startingModal = document.querySelector(
"game-starting-modal",
) as HTMLElement;
@@ -165,9 +173,19 @@ export function joinLobby(
}
};
transport.connect(onconnect, onmessage);
return () => {
return (force: boolean = false) => {
if (!force && currentGameRunner?.shouldPreventWindowClose()) {
console.log("Player is active, prevent leaving game");
return false;
}
console.log("leaving game");
currentGameRunner = null;
transport.leaveGame();
return true;
};
}
@@ -256,6 +274,21 @@ export class ClientGameRunner {
this.lastMessageTime = Date.now();
}
/**
* Determines whether window closing should be prevented.
*
* Used to show a confirmation dialog when the user attempts to close
* the window or navigate away during an active game session.
*
* @returns {boolean} `true` if the window close should be prevented
* (when the player is alive in the game), `false` otherwise
* (when the player is not alive or doesn't exist)
*/
public shouldPreventWindowClose(): boolean {
// Show confirmation dialog if player is alive in the game
return !!this.myPlayer?.isAlive();
}
private async saveGame(update: WinUpdate) {
if (this.myPlayer === null) {
return;