Add win/lose game-ending flow

Removing all 3 locked files while the timer is still running now marks
the session WON immediately (checked right after every successful rm)
and broadcasts a "game_finished" signal over Mercure so every
connected player gets redirected together, not just the one who
removed the last file. A new /won/{session} route + won.html.twig
mirrors the existing lost flow (victory narrative + the same feedback
form).

The existing timer-expiry path already set LOST but always redirected
to lostUrl regardless of actual status; it now picks won/lost based on
the status the server reports.

Also fixes a pre-existing bug on the lost page (and would-be bug on
the new won page): PlayerService::GetCurrentlyActiveAsPlayer() only
matches players in READY/PLAYING sessions, so by the time a session
has ended it always returned null there, silently breaking the
feedback form. Both pages now look the player up directly via
PlayerRepository instead.

Added a navigatingAway flag so the page's "confirm before leaving"
prompt doesn't block our own win/lose redirects.
This commit is contained in:
Frank
2026-07-11 21:41:16 +02:00
parent eee6c3a369
commit 3984a33282
5 changed files with 237 additions and 8 deletions
+27 -4
View File
@@ -3,8 +3,14 @@ import './styles/game1.css';
let sequenceFinished = false;
let stillPlayingSound = true;
let navigatingAway = false;
function subscribeToMercure(mercurePublicUrl, topic, myScreen) {
function goTo(url) {
navigatingAway = true;
window.location.href = url;
}
function subscribeToMercure(mercurePublicUrl, topic, myScreen, wonUrl, lostUrl) {
try {
const url = mercurePublicUrl + '?topic=' + encodeURIComponent(topic);
const es = new EventSource(url);
@@ -14,6 +20,14 @@ function subscribeToMercure(mercurePublicUrl, topic, myScreen) {
const data = JSON.parse(event.data);
console.log('[Mercure][game1] Update:', data);
if (data && !Array.isArray(data) && data.type === 'game_finished') {
const destination = data.status === 'won' ? wonUrl : lostUrl;
if (destination) {
goTo(destination);
}
return;
}
// data is [sendTo, message, messageType?] - messageType defaults to 'mainframe' (green)
if (Array.isArray(data) && data.length >= 2) {
const sendTo = parseInt(data[0]);
@@ -249,8 +263,11 @@ document.addEventListener('DOMContentLoaded', async () => {
// Look for config injected by Twig in the page
const cfgEl = document.getElementById('mercure-config');
// Prevent/warn on page reload
// Prevent/warn on page reload, except for our own win/lose redirects
window.addEventListener('beforeunload', (event) => {
if (navigatingAway) {
return;
}
// Standard way to trigger the browser's confirmation dialog
event.preventDefault();
// Included for compatibility with older browsers
@@ -269,6 +286,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const apiEchoUrl = cfgEl.dataset.apiEchoUrl;
const apiCheckFinishedUrl = cfgEl.dataset.apiCheckFinishedUrl;
const lostUrl = cfgEl.dataset.lostUrl;
const wonUrl = cfgEl.dataset.wonUrl;
const lockLockedAt = cfgEl.dataset.lockLockedAt;
const lockRevealAt = cfgEl.dataset.lockRevealAt;
const lockUnlockAt = cfgEl.dataset.lockUnlockAt;
@@ -280,7 +298,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}
if (mercurePublicUrl && topic) {
subscribeToMercure(mercurePublicUrl, topic, screen);
subscribeToMercure(mercurePublicUrl, topic, screen, wonUrl, lostUrl);
} else {
console.warn('[Mercure][game1] Missing data attributes on #mercure-config');
}
@@ -302,7 +320,7 @@ document.addEventListener('DOMContentLoaded', async () => {
try {
const response = await fetchJson(apiCheckFinishedUrl, { method: 'POST' });
if (response && response.finished) {
window.location.href = lostUrl;
goTo(response.status === 'won' && wonUrl ? wonUrl : lostUrl);
return; // Stop the timer loop
}
} catch (e) {
@@ -424,6 +442,11 @@ document.addEventListener('DOMContentLoaded', async () => {
window.scrollTo(0, document.body.scrollHeight);
}
if (response && response.result) {
if (response.result.gameWon === true && wonUrl) {
goTo(wonUrl);
return;
}
if (response.result.locked === true) {
applyLock({
lockedAt: response.result.lockedAt,