import { $answerResult, $guessArtist, $guessTitle, $npArtist, $npTitle, $npYear, } from "./dom.js"; import { state } from "./state.js"; import { cacheLastRoomId, cacheSessionId, clearSessionId, sendMsg } from "./ws.js"; import { renderRoom } from "./render.js"; import { applySync, loadTrack, getSound, stopAudioPlayback } from "./audio.js"; import { playCorrect, playWrong, playWinner, playTurnStart, playCoinEarned } from "./sfx.js"; import { showReaction, celebrateCorrect } from "./reactions.js"; function updatePlayerIdFromRoom(r) { try { if (r?.players?.length === 1) { const only = r.players[0]; if (only && only.id && only.id !== state.playerId) { state.playerId = only.id; try { localStorage.setItem("playerId", only.id); } catch { } } } } catch { } } function shortName(id) { if (!id) return "-"; const p = state.room?.players.find((x) => x.id === id); return p ? p.name : id.slice(0, 4); } export function handleConnected(msg) { state.playerId = msg.playerId; try { if (msg.playerId) localStorage.setItem("playerId", msg.playerId); } catch { } if (msg.sessionId) { const existing = localStorage.getItem("sessionId"); if (!existing) cacheSessionId(msg.sessionId); } // lazy import to avoid cycle import("./session.js").then(({ reusePlayerName, reconnectLastRoom }) => { reusePlayerName(); reconnectLastRoom(); }); if (state.room) { try { updatePlayerIdFromRoom(state.room); renderRoom(state.room); } catch { } } } export function handleRoomUpdate(msg) { if (msg?.room?.id) cacheLastRoomId(msg.room.id); const r = msg.room; updatePlayerIdFromRoom(r); renderRoom(r); } export function handlePlayTrack(msg) { const t = msg.track; state.lastTrack = t; state.revealed = false; $npTitle.textContent = "???"; $npArtist.textContent = ""; $npYear.textContent = ""; if ($guessTitle) $guessTitle.value = ""; if ($guessArtist) $guessArtist.value = ""; if ($answerResult) { $answerResult.textContent = ""; $answerResult.className = "mt-1 text-sm"; } // Load track with Howler, passing the filename for format detection const sound = loadTrack(t.url, t.file); const pf = document.getElementById("progressFill"); if (pf) pf.style.width = "0%"; const rd = document.getElementById("recordDisc"); if (rd) { rd.classList.remove("spin-record"); rd.src = "/hitstar.png"; } const { startAt, serverNow } = msg; const now = Date.now(); const offsetMs = startAt - serverNow; const localStart = now + offsetMs; const delay = Math.max(0, localStart - now); setTimeout(() => { if (sound && sound === getSound() && !sound.playing()) { sound.play(); const disc = document.getElementById("recordDisc"); if (disc) disc.classList.add("spin-record"); } }, delay); // Play turn start sound if it's the current player's turn if (state.room?.state?.currentGuesser === state.playerId) { playTurnStart(); } if (state.room) renderRoom(state.room); } export function handleSync(msg) { applySync(msg.startAt, msg.serverNow); } export function handleControl(msg) { const { action, startAt, serverNow } = msg; const sound = getSound(); if (!sound) return; if (action === "pause") { sound.pause(); const disc = document.getElementById("recordDisc"); if (disc) disc.classList.remove("spin-record"); sound.rate(1.0); } else if (action === "play") { if (startAt && serverNow) { const now = Date.now(); const elapsed = (now - startAt) / 1000; sound.seek(Math.max(0, elapsed)); } sound.play(); const disc = document.getElementById("recordDisc"); if (disc) disc.classList.add("spin-record"); } } export function handleReveal(msg) { const { result, track } = msg; $npTitle.textContent = track.title || track.id || "Track"; $npArtist.textContent = track.artist ? ` – ${track.artist}` : ""; $npYear.textContent = track.year ? ` (${track.year})` : ""; state.revealed = true; const $rb = document.getElementById("revealBanner"); if ($rb) { if (result.correct) { $rb.textContent = "Richtig!"; $rb.className = "inline-block rounded-md bg-emerald-600 text-white px-3 py-1 text-sm font-medium"; playCorrect(); celebrateCorrect(); // Auto 🎉 animation } else { $rb.textContent = "Falsch!"; $rb.className = "inline-block rounded-md bg-rose-600 text-white px-3 py-1 text-sm font-medium"; playWrong(); } } // Note: placeArea visibility is now controlled by renderRoom() based on game phase const rd = document.getElementById("recordDisc"); if (rd && track?.file) { // Use track.file instead of track.id to include playlist folder prefix const coverUrl = `/cover/${encodeURIComponent(track.file)}`; const coverUrlWithTimestamp = `${coverUrl}?t=${Date.now()}`; const img = new Image(); img.onload = () => { rd.src = coverUrlWithTimestamp; }; img.onerror = () => { /* keep default logo */ }; img.src = coverUrlWithTimestamp; } } export function handleGameEnded(msg) { // Play winner fanfare playWinner(); // Create and show the winner popup with confetti showWinnerPopup(msg); } function showWinnerPopup(msg) { const winnerName = msg.winnerName || shortName(msg.winner); const score = msg.score ?? 0; const timeline = msg.timeline || []; // Create the popup overlay const overlay = document.createElement('div'); overlay.id = 'winnerOverlay'; overlay.className = 'fixed inset-0 z-50 flex items-center justify-center p-4'; overlay.style.cssText = 'background: rgba(0,0,0,0.75); backdrop-filter: blur(4px);'; // Create timeline cards HTML const timelineHtml = timeline.length > 0 ? timeline.map(t => `
${t.year ?? '?'}
${escapeHtmlSimple(t.title || 'Unknown')}
${escapeHtmlSimple(t.artist || '')}
`).join('') : '

Keine Karten

'; overlay.innerHTML = `
🏆

Gewinner!

${escapeHtmlSimple(winnerName)}

Score: ${score} Karten

Zeitleiste

${timelineHtml}
`; document.body.appendChild(overlay); // Start confetti animation createConfetti(); // Rematch button handler document.getElementById('rematchBtn').addEventListener('click', () => { stopAudioPlayback(); // Stop the music sendMsg({ type: 'rematch' }); overlay.remove(); }); // Close button handler document.getElementById('closeWinnerBtn').addEventListener('click', () => { overlay.remove(); }); // Close on overlay click (outside popup) overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); } }); } function createConfetti() { const container = document.getElementById('confettiContainer'); if (!container) return; const colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8']; const confettiCount = 100; for (let i = 0; i < confettiCount; i++) { const confetti = document.createElement('div'); confetti.className = 'confetti-piece'; confetti.style.cssText = ` position: absolute; width: ${Math.random() * 10 + 5}px; height: ${Math.random() * 10 + 5}px; background: ${colors[Math.floor(Math.random() * colors.length)]}; left: ${Math.random() * 100}%; top: -20px; border-radius: ${Math.random() > 0.5 ? '50%' : '2px'}; animation: confetti-fall ${Math.random() * 3 + 2}s linear forwards; animation-delay: ${Math.random() * 0.5}s; transform: rotate(${Math.random() * 360}deg); `; container.appendChild(confetti); } // Clean up confetti after animation setTimeout(() => { container.innerHTML = ''; }, 4000); } function escapeHtmlSimple(s) { return String(s).replace( /[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ); } export function onMessage(ev) { const msg = JSON.parse(ev.data); switch (msg.type) { case "resume_result": { if (msg.ok) { if (msg.playerId) { state.playerId = msg.playerId; try { localStorage.setItem("playerId", msg.playerId); } catch { } } const code = msg.roomId || state.room?.id || localStorage.getItem("lastRoomId"); if (code) sendMsg({ type: "join_room", roomId: code }); if (state.room) { try { renderRoom(state.room); } catch { } } // Restore player name after successful resume import("./session.js").then(({ reusePlayerName }) => { reusePlayerName(); }); } else { // Resume failed - session expired or server restarted // Clear stale session ID so next connect gets a fresh one clearSessionId(); // Still try to join the last room const code = state.room?.id || localStorage.getItem("lastRoomId"); if (code) sendMsg({ type: "join_room", roomId: code }); // Restore player name even on failed resume (new player, same name) import("./session.js").then(({ reusePlayerName }) => { reusePlayerName(); }); } return; } case "connected": return handleConnected(msg); case "room_update": return handleRoomUpdate(msg); case "play_track": return handlePlayTrack(msg); case "sync": return handleSync(msg); case "control": return handleControl(msg); case "reveal": return handleReveal(msg); case "game_ended": return handleGameEnded(msg); case "answer_result": { if ($answerResult) { if (!msg.ok) { $answerResult.textContent = "⛔ Eingabe ungültig oder gerade nicht möglich"; $answerResult.className = "mt-1 text-sm text-rose-600"; } else { const okBoth = !!(msg.correctTitle && msg.correctArtist); const parts = []; parts.push(msg.correctTitle ? "Titel ✓" : "Titel ✗"); parts.push(msg.correctArtist ? "Künstler ✓" : "Künstler ✗"); let coin = ""; if (msg.awarded) { coin = " +1 Token"; playCoinEarned(); } else if (msg.alreadyAwarded) { coin = " (bereits erhalten)"; } $answerResult.textContent = `${parts.join(" · ")}${coin}`; $answerResult.className = okBoth ? "mt-1 text-sm text-emerald-600" : "mt-1 text-sm text-amber-600"; } } return; } default: return; case "reaction": { // Show reaction from another player showReaction(msg.emoji, msg.playerName); return; } } }