A player's own browser now starts a 60s timer the moment they check "ready" (with a visible countdown) and proactively tells the server when it's up via a new expire_ready action - idempotent, so it only actually clears the status if the deadline has genuinely passed. Everyone else finds out live through a Mercure player_ready broadcast that now carries which player and their new ready/not-ready state (previously the broadcast payload's `ready` flag was always false due to a stale-variable bug, though nothing consumed it yet). checkAllPlayersReady() still does the same expiry check server-side and broadcasts on anyone else's request in the meantime, so a stalled frontend timer doesn't leave a stale "ready" badge showing forever. This only affects the pre-PLAYING ready phase: checkAllPlayersReady() still flips the session to PLAYING and stops touching ready state the instant everyone is simultaneously ready, so the earlier fix (a reload should never send an already-started game back to the waiting room) is unaffected. Also fixed the ready checkbox itself: unchecking it submitted a POST without `toggle_ready` in the body (unchecked checkboxes aren't sent), so un-readying silently did nothing server-side. Added the standard hidden-fallback-input pattern to fix it.
164 lines
6.9 KiB
Twig
164 lines
6.9 KiB
Twig
{% extends 'layout/site.html.twig' %}
|
|
|
|
{% block title %}Waiting for players - {{ session.game.name }}{% endblock %}
|
|
|
|
{% block body %}
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-primary text-white">
|
|
<h3 class="card-title mb-0">Waiting for all players to be ready</h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<h4>{{ session.game.name }}</h4>
|
|
<p>Welcome to the game! Please wait for all players to join and signal they are ready to start.</p>
|
|
|
|
<div class="game-info">
|
|
Please keep the following things in mind:
|
|
<ul>
|
|
<li>This game is best played in full screen mode. For windows, press F11. For Mac, press Cmd+Ctrl+F.</li>
|
|
<li>There is no need to reload the page. There is even a chance this could break the game.</li>
|
|
<li>If your internet connection is lost, you can get back in the game after internet has been fixed.</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="alert alert-info">
|
|
<strong>Game Information:</strong>
|
|
<ul class="mb-0">
|
|
<li>Number of players: {{ session.game.numberOfPlayers }}</li>
|
|
<li>Current players: {{ session.players|length }} / {{ session.game.numberOfPlayers }}</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<h5>Players status:</h5>
|
|
<ul class="list-group">
|
|
{% for player in session.players %}
|
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
|
{{ player.user.username }}
|
|
{% set playerReady = false %}
|
|
{% set settingName = 'ReadyAtForPlayer' ~ player.screen %}
|
|
{% for setting in session.settings %}
|
|
{% if setting.name.value == settingName and setting.player == player %}
|
|
{% set playerReady = true %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if playerReady %}
|
|
<span class="badge bg-success">Ready</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">Not ready</span>
|
|
{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
{% if not app.user.verified %}
|
|
<div class="alert alert-warning">
|
|
You must verify your email address before you can mark yourself as ready.
|
|
Check your inbox for the confirmation link, or <a href="{{ path('app_verify_resend_email') }}">request a new one</a>.
|
|
</div>
|
|
{% endif %}
|
|
<form method="post" class="mt-4">
|
|
<input type="hidden" name="toggle_ready" value="0">
|
|
<div class="form-check form-switch mb-3">
|
|
<input class="form-check-input" type="checkbox" id="toggle_ready" name="toggle_ready" value="1" onchange="this.form.submit()" {{ isReady ? 'checked' : '' }} {{ not app.user.verified ? 'disabled' : '' }}>
|
|
<label class="form-check-label" for="toggle_ready">
|
|
<strong>I am ready to start!</strong>
|
|
</label>
|
|
</div>
|
|
<p class="text-muted small">
|
|
As soon as everyone has checked this box, the game starts automatically for everyone.
|
|
{% if isReady %}
|
|
Your ready status expires in <span id="ready-countdown">1:00</span> if not everyone else is ready by then.
|
|
{% endif %}
|
|
</p>
|
|
</form>
|
|
|
|
<div class="mt-3">
|
|
<a href="{{ path('game_dashboard') }}" class="btn btn-outline-secondary">Back to Dashboard</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form id="expire-ready-form" method="post" style="display:none">
|
|
<input type="hidden" name="expire_ready" value="1">
|
|
</form>
|
|
|
|
<div id="mercure-config"
|
|
data-mercure-public-url="{{ mercure_public_url|e('html_attr') }}"
|
|
data-topic="/game/hub/{{ session.id|e('html_attr') }}"
|
|
data-ready-at="{{ readyAt|e('html_attr') }}"
|
|
style="display:none">
|
|
</div>
|
|
|
|
<script>
|
|
const config = document.getElementById('mercure-config');
|
|
const publicUrl = config.dataset.mercurePublicUrl;
|
|
const topic = config.dataset.topic;
|
|
const readyAt = config.dataset.readyAt;
|
|
|
|
let reloading = false;
|
|
const reloadOnce = (eventSource) => {
|
|
if (reloading) {
|
|
return;
|
|
}
|
|
reloading = true;
|
|
if (eventSource) {
|
|
eventSource.close();
|
|
}
|
|
window.location.reload();
|
|
};
|
|
|
|
if (publicUrl && topic) {
|
|
const url = new URL(publicUrl);
|
|
url.searchParams.append('topic', topic);
|
|
|
|
const eventSource = new EventSource(url);
|
|
eventSource.onmessage = event => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'all_ready' || data.type === 'player_ready') {
|
|
reloadOnce(eventSource);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Our own ready status expires 60s after we set it - proactively tell the
|
|
// server as close to that deadline as possible, so the other players find
|
|
// out live instead of only whenever someone else's request happens to
|
|
// trigger the lazy check.
|
|
if (readyAt) {
|
|
const timeoutMs = 61000; // slightly more than the server-side 60s
|
|
const readyAtMs = readyAt * 1000;
|
|
const countdownEl = document.getElementById('ready-countdown');
|
|
|
|
const updateCountdown = () => {
|
|
const remaining = Math.max(0, Math.ceil((readyAtMs + timeoutMs - Date.now()) / 1000));
|
|
if (countdownEl) {
|
|
const m = Math.floor(remaining / 60);
|
|
const s = remaining % 60;
|
|
countdownEl.textContent = m + ':' + s.toString().padStart(2, '0');
|
|
}
|
|
return remaining;
|
|
};
|
|
|
|
const remaining = updateCountdown();
|
|
if (remaining <= 0) {
|
|
document.getElementById('expire-ready-form').submit();
|
|
} else {
|
|
const countdownInterval = setInterval(() => {
|
|
if (updateCountdown() <= 0) {
|
|
clearInterval(countdownInterval);
|
|
document.getElementById('expire-ready-form').submit();
|
|
}
|
|
}, 1000);
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|