refactor: improve game start logic to ensure all active players are ready before starting
All checks were successful
Build and Push Docker Image / docker (push) Successful in 8s
All checks were successful
Build and Push Docker Image / docker (push) Successful in 8s
This commit is contained in:
480
public/client.js
480
public/client.js
@@ -1,480 +0,0 @@
|
||||
import { badgeColorForYear } from './utils/colors.js';
|
||||
|
||||
let ws;
|
||||
let reconnectAttempts = 0;
|
||||
let reconnectTimer = null;
|
||||
const outbox = [];
|
||||
let sessionId = localStorage.getItem('sessionId') || null;
|
||||
let lastRoomId = localStorage.getItem('lastRoomId') || null;
|
||||
function wsIsOpen() { return ws && ws.readyState === WebSocket.OPEN; }
|
||||
function sendMsg(obj) {
|
||||
if (wsIsOpen()) ws.send(JSON.stringify(obj));
|
||||
else outbox.push(obj);
|
||||
}
|
||||
function scheduleReconnect() {
|
||||
if (reconnectTimer) return;
|
||||
const delay = Math.min(30000, 1000 * Math.pow(2, reconnectAttempts));
|
||||
reconnectAttempts++;
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
connectWS();
|
||||
}, delay);
|
||||
}
|
||||
function connectWS() {
|
||||
const url = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host;
|
||||
ws = new WebSocket(url);
|
||||
ws.addEventListener('open', () => {
|
||||
reconnectAttempts = 0;
|
||||
// Try to resume session immediately
|
||||
if (sessionId) {
|
||||
try { ws.send(JSON.stringify({ type: 'resume', sessionId })); } catch {}
|
||||
}
|
||||
// Flush queued messages
|
||||
setTimeout(() => {
|
||||
while (outbox.length && wsIsOpen()) {
|
||||
try { ws.send(JSON.stringify(outbox.shift())); } catch { break; }
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
ws.addEventListener('message', (ev) => handleMessage(ev));
|
||||
ws.addEventListener('close', () => { scheduleReconnect(); });
|
||||
ws.addEventListener('error', () => { try { ws.close(); } catch {} });
|
||||
}
|
||||
|
||||
let state = {
|
||||
playerId: null,
|
||||
room: null,
|
||||
lastTrack: null,
|
||||
revealed: false,
|
||||
pendingReady: null,
|
||||
isBuffering: false,
|
||||
};
|
||||
|
||||
// Elements
|
||||
const el = (id) => document.getElementById(id);
|
||||
const $lobby = el('lobby');
|
||||
const $room = el('room');
|
||||
// Removed old players chip list; using dashboard only
|
||||
const $dashboardList = el('dashboardList');
|
||||
const $roomId = el('roomId');
|
||||
const $status = el('status');
|
||||
const $guesser = el('guesser');
|
||||
const $timeline = el('timeline');
|
||||
const $tokens = el('tokens');
|
||||
const $audio = el('audio');
|
||||
if ($audio) { try { $audio.preload = 'none'; } catch {} }
|
||||
const $np = document.getElementById('nowPlaying');
|
||||
const $npTitle = el('npTitle');
|
||||
const $npArtist = el('npArtist');
|
||||
const $npYear = el('npYear');
|
||||
const $readyChk = document.getElementById('readyChk');
|
||||
const $startGame = document.getElementById('startGame');
|
||||
const $revealBanner = document.getElementById('revealBanner');
|
||||
const $placeArea = document.getElementById('placeArea');
|
||||
const $slotSelect = document.getElementById('slotSelect');
|
||||
const $placeBtn = document.getElementById('placeBtn');
|
||||
const $mediaControls = document.getElementById('mediaControls');
|
||||
const $playBtn = document.getElementById('playBtn');
|
||||
const $pauseBtn = document.getElementById('pauseBtn');
|
||||
const $nextArea = document.getElementById('nextArea');
|
||||
const $nextBtn = document.getElementById('nextBtn');
|
||||
// Custom player UI
|
||||
const $recordDisc = document.getElementById('recordDisc');
|
||||
const $progressFill = document.getElementById('progressFill');
|
||||
const $volumeSlider = document.getElementById('volumeSlider');
|
||||
const $bufferBadge = document.getElementById('bufferBadge');
|
||||
// Copy Room Code button
|
||||
const $copyRoomCode = document.getElementById('copyRoomCode');
|
||||
// Name (lobby input + room display)
|
||||
const $nameLobby = document.getElementById('name');
|
||||
const $setNameLobby = document.getElementById('setName');
|
||||
const $nameDisplay = document.getElementById('nameDisplay');
|
||||
|
||||
function showLobby() { $lobby.classList.remove('hidden'); $room.classList.add('hidden'); }
|
||||
function showRoom() { $lobby.classList.add('hidden'); $room.classList.remove('hidden'); }
|
||||
|
||||
function renderRoom(room) {
|
||||
state.room = room;
|
||||
try { if (room?.id) { lastRoomId = room.id; localStorage.setItem('lastRoomId', room.id); } } catch {}
|
||||
if (!room) { showLobby(); return; }
|
||||
showRoom();
|
||||
$roomId.textContent = room.id;
|
||||
// Ensure copy button is visible and set up
|
||||
if ($copyRoomCode) {
|
||||
$copyRoomCode.style.display = 'inline-block';
|
||||
$copyRoomCode.onclick = function() {
|
||||
if (room.id) {
|
||||
navigator.clipboard.writeText(room.id).then(() => {
|
||||
$copyRoomCode.textContent = '✔️';
|
||||
showToast('Code kopiert!');
|
||||
setTimeout(()=>{$copyRoomCode.textContent = '📋';}, 1200);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
// Also allow clicking the room code itself to copy
|
||||
if ($roomId) {
|
||||
$roomId.onclick = function() {
|
||||
if (room.id) {
|
||||
navigator.clipboard.writeText(room.id).then(() => {
|
||||
$roomId.title = 'Kopiert!';
|
||||
showToast('Code kopiert!');
|
||||
setTimeout(()=>{$roomId.title = 'Klicken zum Kopieren';}, 1200);
|
||||
});
|
||||
}
|
||||
};
|
||||
$roomId.style.cursor = 'pointer';
|
||||
}
|
||||
const $toast = document.getElementById('toast');
|
||||
function showToast(msg) {
|
||||
if (!$toast) return;
|
||||
$toast.textContent = msg;
|
||||
$toast.style.opacity = '1';
|
||||
setTimeout(() => {
|
||||
$toast.style.opacity = '0';
|
||||
}, 1200);
|
||||
}
|
||||
$status.textContent = room.state.status;
|
||||
$guesser.textContent = shortName(room.state.currentGuesser);
|
||||
// Show my current name (from server if available) or fallback to stored value
|
||||
const me = room.players.find(p=>p.id===state.playerId);
|
||||
if ($nameDisplay) $nameDisplay.textContent = (me?.name || localStorage.getItem('playerName') || '-');
|
||||
// Old players chip list removed
|
||||
// Dashboard rows
|
||||
if ($dashboardList) {
|
||||
$dashboardList.innerHTML = room.players.map(p => {
|
||||
const connected = p.connected ? '<span class="text-emerald-600">online</span>' : '<span class="text-rose-600">offline</span>';
|
||||
const ready = p.ready ? '<span class="text-emerald-600">bereit</span>' : '<span class="text-slate-400">-</span>';
|
||||
const score = (room.state.timeline?.[p.id]?.length) ?? 0;
|
||||
const isMe = p.id === state.playerId;
|
||||
return `
|
||||
<tr class="align-top">
|
||||
<td class="py-2 pr-3">
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<span>${escapeHtml(p.name)}</span>${p.spectator ? ' <span title="Zuschauer">👻</span>' : ''}
|
||||
${p.id===room.hostId ? '<span title="Host" class="text-amber-600">\u2B50</span>' : ''}
|
||||
${isMe ? '<span title="Du" class="text-indigo-600">(du)</span>' : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 pr-3">${connected}</td>
|
||||
<td class="py-2 pr-3">${ready}</td>
|
||||
<td class="py-2 pr-3 font-semibold tabular-nums">${score}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
const myTl = room.state.timeline?.[state.playerId] || [];
|
||||
$timeline.innerHTML = myTl.map(t => {
|
||||
const title = escapeHtml(t.title || t.trackId || 'Unbekannt');
|
||||
const artist = t.artist ? escapeHtml(t.artist) : '';
|
||||
const year = (t.year ?? '?');
|
||||
const badgeStyle = badgeColorForYear(year);
|
||||
return `
|
||||
<div class="flex items-center gap-2 border border-slate-200 dark:border-slate-800 rounded-lg px-3 py-2 bg-white text-slate-900 dark:bg-slate-800 dark:text-slate-100 shadow-sm" title="${title}${artist? ' — '+artist : ''} (${year})">
|
||||
<div class="font-bold tabular-nums text-white rounded-md px-2 py-0.5 min-w-[3ch] text-center" style="${badgeStyle}">${year}</div>
|
||||
<div class="leading-tight">
|
||||
<div class="font-semibold">${title}</div>
|
||||
<div class="text-sm text-slate-600 dark:text-slate-300">${artist}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
$tokens.textContent = room.state.tokens?.[state.playerId] ?? 0;
|
||||
// Ready control visibility
|
||||
if ($readyChk) {
|
||||
const serverReady = !!me?.ready;
|
||||
// If user recently toggled, keep local visual state until server matches
|
||||
if (state.pendingReady === null || state.pendingReady === undefined) {
|
||||
$readyChk.checked = serverReady;
|
||||
} else {
|
||||
$readyChk.checked = !!state.pendingReady;
|
||||
// Clear pending once it matches server
|
||||
if (serverReady === state.pendingReady) state.pendingReady = null;
|
||||
}
|
||||
$readyChk.parentElement.classList.toggle('hidden', room.state.status !== 'lobby');
|
||||
}
|
||||
// Host start button when all ready
|
||||
const isHost = state.playerId === room.hostId;
|
||||
const allReady = room.players.length>0 && room.players.every(p=>p.ready);
|
||||
if ($startGame) $startGame.classList.toggle('hidden', !(isHost && room.state.status==='lobby' && allReady));
|
||||
// Show guess buttons only when it's my turn and a track is active
|
||||
const isMyTurn = room.state.status==='playing' && room.state.phase==='guess' && room.state.currentGuesser===state.playerId && room.state.currentTrack;
|
||||
const canGuess = isMyTurn;
|
||||
// Build slot options: 0..n
|
||||
if ($placeArea && $slotSelect) {
|
||||
if (canGuess) {
|
||||
const tl = room.state.timeline?.[state.playerId] || [];
|
||||
$slotSelect.innerHTML = '';
|
||||
for (let i = 0; i <= tl.length; i++) {
|
||||
const left = i>0 ? (tl[i-1]?.year ?? '?') : null;
|
||||
const right = i<tl.length ? (tl[i]?.year ?? '?') : null;
|
||||
let label = '';
|
||||
if (tl.length === 0) label = 'Einsetzen';
|
||||
else if (i === 0) label = `Vor (${right})`;
|
||||
else if (i === tl.length) label = `Nach (${left})`;
|
||||
else label = `Zwischen (${left} / ${right})`;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = String(i);
|
||||
opt.textContent = label;
|
||||
$slotSelect.appendChild(opt);
|
||||
}
|
||||
}
|
||||
$placeArea.classList.toggle('hidden', !canGuess);
|
||||
}
|
||||
// Show now playing if track exists
|
||||
$np.classList.toggle('hidden', !room.state.currentTrack);
|
||||
// Keep reveal banner visible during reveal; otherwise hide/reset it
|
||||
if ($revealBanner) {
|
||||
const inReveal = room.state.phase === 'reveal';
|
||||
if (!inReveal) { $revealBanner.className = 'hidden'; $revealBanner.textContent=''; }
|
||||
}
|
||||
// Media controls only for current guesser during guess phase
|
||||
if ($mediaControls) $mediaControls.classList.toggle('hidden', !isMyTurn);
|
||||
// Next button visible in reveal to host or current guesser
|
||||
const canNext = room.state.status==='playing' && room.state.phase==='reveal' && (isHost || room.state.currentGuesser===state.playerId);
|
||||
if ($nextArea) $nextArea.classList.toggle('hidden', !canNext);
|
||||
}
|
||||
|
||||
function shortName(id) {
|
||||
if (!id) return '-';
|
||||
const p = state.room?.players.find(x=>x.id===id);
|
||||
return p ? p.name : id.slice(0,4);
|
||||
}
|
||||
|
||||
function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
|
||||
// color helper imported from utils/colors.js
|
||||
|
||||
function handleMessage(ev) {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.type === 'connected') {
|
||||
state.playerId = msg.playerId;
|
||||
if (msg.sessionId && !sessionId) {
|
||||
sessionId = msg.sessionId;
|
||||
try { localStorage.setItem('sessionId', sessionId); } catch {}
|
||||
}
|
||||
// Try to auto-apply stored name
|
||||
const stored = localStorage.getItem('playerName');
|
||||
if (stored) {
|
||||
if ($nameLobby && $nameLobby.value !== stored) {
|
||||
$nameLobby.value = stored;
|
||||
}
|
||||
if ($nameDisplay) {
|
||||
$nameDisplay.textContent = stored;
|
||||
}
|
||||
sendMsg({ type: 'set_name', name: stored });
|
||||
}
|
||||
// If we already have a sessionId, we'll resume instead of auto-joining here
|
||||
if (!sessionId) {
|
||||
const code = state.room?.id || lastRoomId || localStorage.getItem('lastRoomId');
|
||||
if (code) sendMsg({ type: 'join_room', code });
|
||||
}
|
||||
}
|
||||
if (msg.type === 'resume_result') {
|
||||
if (msg.ok) {
|
||||
if (msg.playerId) state.playerId = msg.playerId;
|
||||
// If we have a known room, ensure we rejoin it
|
||||
if (msg.roomId) {
|
||||
// Update local roomId (we will receive a room_update shortly)
|
||||
sendMsg({ type: 'join_room', code: msg.roomId });
|
||||
} else {
|
||||
const code = state.room?.id || lastRoomId || localStorage.getItem('lastRoomId');
|
||||
if (code) sendMsg({ type: 'join_room', code });
|
||||
}
|
||||
} else {
|
||||
// If resume failed, try to rejoin by last room code
|
||||
const code = state.room?.id || lastRoomId || localStorage.getItem('lastRoomId');
|
||||
if (code) sendMsg({ type: 'join_room', code });
|
||||
}
|
||||
}
|
||||
if (msg.type === 'room_update') {
|
||||
renderRoom(msg.room);
|
||||
}
|
||||
if (msg.type === 'play_track') {
|
||||
const t = msg.track;
|
||||
state.lastTrack = t;
|
||||
state.revealed = false;
|
||||
// Hide metadata until a guess is placed
|
||||
$npTitle.textContent = '???';
|
||||
$npArtist.textContent = '';
|
||||
$npYear.textContent = '';
|
||||
try { $audio.preload = 'auto'; } catch {}
|
||||
$audio.src = t.url;
|
||||
// Reset custom UI
|
||||
if ($progressFill) $progressFill.style.width = '0%';
|
||||
if ($recordDisc) $recordDisc.classList.remove('spin-record');
|
||||
// Sync start using server-provided timestamp
|
||||
const { startAt, serverNow } = msg;
|
||||
if (startAt && serverNow) {
|
||||
const now = Date.now();
|
||||
const offsetMs = startAt - serverNow; // server delay until start
|
||||
const localStart = now + offsetMs;
|
||||
const delay = Math.max(0, localStart - now);
|
||||
setTimeout(() => {
|
||||
$audio.currentTime = 0;
|
||||
$audio.play().catch(()=>{});
|
||||
if ($recordDisc) $recordDisc.classList.add('spin-record');
|
||||
}, delay);
|
||||
} else {
|
||||
$audio.play().catch(()=>{});
|
||||
if ($recordDisc) $recordDisc.classList.add('spin-record');
|
||||
}
|
||||
if (state.room) renderRoom(state.room);
|
||||
}
|
||||
if (msg.type === 'sync') {
|
||||
const { startAt, serverNow } = msg;
|
||||
if (!state.room?.state?.currentTrack || !startAt || !serverNow) return;
|
||||
if (state.room?.state?.paused) return; // don't auto-resume while paused
|
||||
if (state.isBuffering) return; // avoid corrections while buffering
|
||||
const now = Date.now();
|
||||
const elapsed = (now - startAt) / 1000; // seconds
|
||||
const drift = ($audio.currentTime || 0) - elapsed;
|
||||
// Soft sync via playbackRate adjustments; hard seek if way off
|
||||
const abs = Math.abs(drift);
|
||||
if (abs > 1.0) {
|
||||
// Hard correct when over 1s
|
||||
$audio.currentTime = Math.max(0, elapsed);
|
||||
if ($audio.paused) $audio.play().catch(()=>{});
|
||||
$audio.playbackRate = 1.0;
|
||||
} else if (abs > 0.12) {
|
||||
// Gently nudge speed up to +/-3%
|
||||
const maxNudge = 0.03;
|
||||
const sign = drift > 0 ? -1 : 1; // if ahead (positive drift), slow down
|
||||
const rate = 1 + sign * Math.min(maxNudge, abs * 0.5);
|
||||
$audio.playbackRate = Math.max(0.8, Math.min(1.2, rate));
|
||||
} else {
|
||||
// Close enough
|
||||
if (Math.abs($audio.playbackRate - 1) > 0.001) {
|
||||
$audio.playbackRate = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg.type === 'control') {
|
||||
const { action, startAt, serverNow } = msg;
|
||||
if (action === 'pause') {
|
||||
$audio.pause();
|
||||
if ($recordDisc) $recordDisc.classList.remove('spin-record');
|
||||
$audio.playbackRate = 1.0;
|
||||
} else if (action === 'play') {
|
||||
if (startAt && serverNow) {
|
||||
const now = Date.now();
|
||||
const elapsed = (now - startAt) / 1000;
|
||||
$audio.currentTime = Math.max(0, elapsed);
|
||||
}
|
||||
$audio.play().catch(()=>{});
|
||||
if ($recordDisc) $recordDisc.classList.add('spin-record');
|
||||
}
|
||||
}
|
||||
if (msg.type === 'reveal') {
|
||||
const { result, track } = msg;
|
||||
// Reveal metadata
|
||||
$npTitle.textContent = track.title || track.id || 'Track';
|
||||
$npArtist.textContent = track.artist ? ` – ${track.artist}` : '';
|
||||
$npYear.textContent = track.year ? ` (${track.year})` : '';
|
||||
state.revealed = true;
|
||||
// Show banner
|
||||
if ($revealBanner) {
|
||||
if (result.correct) {
|
||||
$revealBanner.textContent = 'Richtig!';
|
||||
$revealBanner.className = 'inline-block rounded-md bg-emerald-600 text-white px-3 py-1 text-sm font-medium';
|
||||
} else {
|
||||
$revealBanner.textContent = 'Falsch!';
|
||||
$revealBanner.className = 'inline-block rounded-md bg-rose-600 text-white px-3 py-1 text-sm font-medium';
|
||||
}
|
||||
}
|
||||
// Hide placement during reveal
|
||||
if ($placeArea) $placeArea.classList.add('hidden');
|
||||
}
|
||||
if (msg.type === 'game_ended') {
|
||||
alert(`Gewinner: ${shortName(msg.winner)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Start connection
|
||||
connectWS();
|
||||
window.addEventListener('online', () => {
|
||||
if (!wsIsOpen()) {
|
||||
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
||||
connectWS();
|
||||
}
|
||||
});
|
||||
|
||||
// UI events
|
||||
el('setName').onclick = () => {
|
||||
const name = $nameLobby.value.trim();
|
||||
if (!name) return;
|
||||
localStorage.setItem('playerName', name);
|
||||
if ($nameDisplay) $nameDisplay.textContent = name;
|
||||
sendMsg({ type: 'set_name', name });
|
||||
};
|
||||
el('createRoom').onclick = () => { sendMsg({ type: 'create_room' }); };
|
||||
el('joinRoom').onclick = () => {
|
||||
const code = el('roomCode').value.trim();
|
||||
if (code) sendMsg({ type: 'join_room', code });
|
||||
};
|
||||
el('leaveRoom').onclick = () => {
|
||||
sendMsg({ type: 'leave_room' });
|
||||
state.room = null; showLobby();
|
||||
};
|
||||
el('startGame').onclick = () => sendMsg({ type: 'start_game' });
|
||||
document.getElementById('readyChk').onchange = (e) => {
|
||||
const val = !!e.target.checked;
|
||||
state.pendingReady = val;
|
||||
sendMsg({ type: 'set_ready', ready: val });
|
||||
};
|
||||
el('earnToken').onclick = () => sendMsg({ type: 'earn_token' });
|
||||
|
||||
if ($placeBtn) {
|
||||
$placeBtn.onclick = () => {
|
||||
const slot = parseInt($slotSelect.value, 10);
|
||||
sendMsg({ type: 'place_guess', slot });
|
||||
};
|
||||
}
|
||||
|
||||
if ($playBtn) $playBtn.onclick = () => sendMsg({ type: 'player_control', action: 'play' });
|
||||
if ($pauseBtn) $pauseBtn.onclick = () => sendMsg({ type: 'player_control', action: 'pause' });
|
||||
if ($nextBtn) $nextBtn.onclick = () => sendMsg({ type: 'next_track' });
|
||||
|
||||
// Progress + volume updates
|
||||
if ($audio) {
|
||||
// Try to preserve pitch during slight playbackRate changes if supported
|
||||
try {
|
||||
if ('preservesPitch' in $audio) $audio.preservesPitch = true;
|
||||
if ('mozPreservesPitch' in $audio) $audio.mozPreservesPitch = true;
|
||||
if ('webkitPreservesPitch' in $audio) $audio.webkitPreservesPitch = true;
|
||||
} catch {}
|
||||
$audio.addEventListener('timeupdate', () => {
|
||||
const dur = $audio.duration || 0;
|
||||
if (!dur || !$progressFill) return;
|
||||
const pct = Math.min(100, Math.max(0, ($audio.currentTime / dur) * 100));
|
||||
$progressFill.style.width = pct + '%';
|
||||
});
|
||||
const showBuffer = (v) => {
|
||||
state.isBuffering = v;
|
||||
if ($bufferBadge) $bufferBadge.classList.toggle('hidden', !v);
|
||||
if ($recordDisc) $recordDisc.classList.toggle('spin-record', !v && !$audio.paused);
|
||||
};
|
||||
$audio.addEventListener('waiting', () => showBuffer(true));
|
||||
$audio.addEventListener('stalled', () => showBuffer(true));
|
||||
$audio.addEventListener('canplay', () => showBuffer(false));
|
||||
$audio.addEventListener('playing', () => showBuffer(false));
|
||||
$audio.addEventListener('ended', () => {
|
||||
if ($recordDisc) $recordDisc.classList.remove('spin-record');
|
||||
$audio.playbackRate = 1.0;
|
||||
});
|
||||
}
|
||||
if ($volumeSlider && $audio) {
|
||||
// Initialize from current volume
|
||||
try { $volumeSlider.value = String($audio.volume ?? 1); } catch {}
|
||||
$volumeSlider.addEventListener('input', () => {
|
||||
$audio.volume = parseFloat($volumeSlider.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Try to restore room view if server restarts won't preserve sessions; basic behavior only
|
||||
(() => {
|
||||
const saved = localStorage.getItem('playerName');
|
||||
if (saved) {
|
||||
if ($nameLobby && $nameLobby.value !== saved) $nameLobby.value = saved;
|
||||
if ($nameDisplay) $nameDisplay.textContent = saved;
|
||||
}
|
||||
})();
|
||||
@@ -56,8 +56,10 @@ export function renderRoom(room) {
|
||||
$readyChk.parentElement.classList.toggle('hidden', room.state.status !== 'lobby');
|
||||
}
|
||||
const isHost = state.playerId === room.hostId;
|
||||
const allReady = room.players.length>0 && room.players.every(p=>p.ready);
|
||||
if ($startGame) $startGame.classList.toggle('hidden', !(isHost && room.state.status==='lobby' && allReady));
|
||||
const activePlayers = room.players.filter(p => !p.spectator && p.connected);
|
||||
const allReady = activePlayers.length>0 && activePlayers.every(p=>p.ready);
|
||||
const canStart = room.state.status==='lobby' && isHost && allReady;
|
||||
if ($startGame) $startGame.classList.toggle('hidden', !canStart);
|
||||
const isMyTurn = room.state.status==='playing' && room.state.phase==='guess' && room.state.currentGuesser===state.playerId && room.state.currentTrack;
|
||||
const canGuess = isMyTurn;
|
||||
// Media controls (play/pause) only for current guesser while guessing and a track is active
|
||||
|
||||
Reference in New Issue
Block a user