// public/js/game.js import { getParam, renderList } from "./utils.js"; import { setupStartRound } from "./start-round.js"; const gameId = getParam("gameId"); const username = getParam("username"); // --- kleine Helper --- function showToast(msg, ms = 2200) { const t = document.getElementById("toast"); if (!t) return; t.textContent = msg; t.classList.add("show"); setTimeout(() => t.classList.remove("show"), ms); } function esc(s) { const d = document.createElement("div"); d.textContent = String(s ?? ""); return d.innerHTML; } // 1) Parameter prüfen if (!gameId || !username) { alert("Ungültige oder fehlende URL-Parameter!"); throw new Error("Missing gameId or username"); } // 2) Copy to clipboard (unverändert, aber mit Toast) (function copyCodeToClipboard(code) { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(code) .then(() => showToast(`Spiel-Code ${code} kopiert`)) .catch(err => console.error("Clipboard write failed:", err)); } else { const ta = document.createElement("textarea"); ta.value = code; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.focus(); ta.select(); try { document.execCommand("copy"); showToast(`Spiel-Code ${code} kopiert`); } catch (err) { console.error("Fallback copy failed:", err); } document.body.removeChild(ta); } })(gameId); // 3) Spiel-Code ins DOM schreiben document.getElementById("gameId").textContent = gameId; // 4) Drawer toggles für Songliste const drawer = document.getElementById("songDrawer"); document.getElementById("toggleSongList")?.addEventListener("click", () => drawer?.classList.toggle("open")); document.getElementById("closeDrawer")?.addEventListener("click", () => drawer?.classList.remove("open")); // 5) WebSocket mit Reconnect-Logik let socket; function connectWebSocket() { const protocol = location.protocol === "https:" ? "wss" : "ws"; socket = new WebSocket( `${protocol}://${location.host}/ws/${gameId}?username=${encodeURIComponent(username)}` ); socket.addEventListener("open", () => { console.log("WebSocket connected. Requesting player list..."); socket.send(JSON.stringify({ type: "requestPlayers" })); setupStartRound(socket); // deaktiviert den Start-Button beim Klicken }); socket.addEventListener("message", async ({ data }) => { console.log("WS-Rohdaten:", data); const msg = JSON.parse(data); switch (msg.type) { case "players": renderList("#playersList", msg.players, username); break; case "reload": window.location.reload(); break; case "round-start": await handleRoundStart(msg); break; case "round-result": handleRoundResult(msg); break; case "game-end": handleGameEnd(msg); break; default: console.warn("Unknown WS message type:", msg.type); } }); socket.addEventListener("close", () => { console.warn("WebSocket geschlossen, versuche erneut zu verbinden..."); setTimeout(connectWebSocket, 2000); }); socket.addEventListener("error", (e) => { console.error("WebSocket Fehler:", e); socket.close(); }); } connectWebSocket(); // Zugriff auf DOM-Elemente const startBtn = document.getElementById("startRound"); const roundArea = document.getElementById("roundArea"); const songEmbed = document.getElementById("songEmbed"); const optionsDiv = document.getElementById("options"); const resultP = document.getElementById("result"); const scoreboard = document.getElementById("scoreboard"); // --- SVG helpers für Pie-Slices --- const SVGNS = "http://www.w3.org/2000/svg"; const VB = 1000; // viewBox 0..1000 const CX = 500, CY = 500, R = 480; function polar(cx, cy, r, deg){ const rad = (deg - 90) * Math.PI/180; // 0° oben return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }; } function wedgePath(cx, cy, r, a0, a1){ const p0 = polar(cx, cy, r, a0); const p1 = polar(cx, cy, r, a1); const largeArc = ((a1 - a0 + 360) % 360) > 180 ? 1 : 0; return `M ${cx} ${cy} L ${p0.x} ${p0.y} A ${r} ${r} 0 ${largeArc} 1 ${p1.x} ${p1.y} Z`; } // Auswahl-Set für Multi-Guess (pro Runde zurückgesetzt) let selectedGuesses = new Set(); let lastScores = null; function ensureSubmitBtn(optionsDiv) { let btn = document.getElementById("submitGuesses"); if (!btn) { btn = document.createElement("button"); btn.id = "submitGuesses"; btn.className = "btn btn-primary"; btn.style.marginTop = "8px"; btn.textContent = "Tipps abgeben"; // Button direkt unter den Optionen platzieren optionsDiv.parentElement.appendChild(btn); } return btn; } // 6) Neue Runde anzeigen async function handleRoundStart({ ownerOptions, songUri, trackInfos }) { // UI zurücksetzen resultP.textContent = ""; optionsDiv.innerHTML = ""; songEmbed.innerHTML = ""; //scoreboard zurücksetzen scoreboard.innerHTML = ""; selectedGuesses = new Set(); ownerOptions.forEach(user => { const li = document.createElement("li"); li.textContent = `${user}: 0 Punkte`; scoreboard.appendChild(li); }); // Song einbetten const trackId = songUri.split(":")[2]; songEmbed.innerHTML = ` `; // Spotify-Playback (optional, wenn vorhanden) if (window.playOnSpotify && typeof window.playOnSpotify === "function") { await window.playOnSpotify(songUri, username); } // Optionen als Tortenstücke (SVG) rendern const svg = document.createElementNS(SVGNS, "svg"); svg.setAttribute("viewBox", `0 0 ${VB} ${VB}`); svg.setAttribute("class", "options-svg"); optionsDiv.innerHTML = ""; optionsDiv.appendChild(svg); const n = ownerOptions.length; ownerOptions.forEach((user, i) => { const a0 = (360 / n) * i; const a1 = (360 / n) * (i + 1); const path = document.createElementNS(SVGNS, "path"); path.setAttribute("d", wedgePath(CX, CY, R, a0, a1)); path.setAttribute("class", "wedge"); path.setAttribute("data-user", user); // path.addEventListener("click", () => { // socket.send(JSON.stringify({ // type: "guess", // username: username, // guess: user // })); // svg.querySelectorAll(".wedge").forEach(w => w.classList.add("disabled")); // path.classList.add("selected"); // }); // Toggle-Selection statt sofort zu senden path.addEventListener("click", () => { const u = path.getAttribute("data-user"); if (path.classList.contains("disabled")) return; if (path.classList.toggle("selected")) selectedGuesses.add(u); else selectedGuesses.delete(u); //submitBtn.disabled = selectedGuesses.size === 0; const hasSelection = selectedGuesses.size > 0; submitBtn.hidden = !hasSelection; // ohne Auswahl: versteckt submitBtn.disabled = !hasSelection; // mit Auswahl: enabled }); // Label mittig im Segment const mid = (a0 + a1) / 2; const P = polar(CX, CY, R * 0.58, mid); const text = document.createElementNS(SVGNS, "text"); const span = a1 - a0; // Winkelbreite des Segments const font = Math.max(18, Math.min(42, 26 * (span / 60))); text.setAttribute("font-size", font); text.setAttribute("font-weight", "800"); // Bessere Lesbarkeit auf dunklem Slice text.setAttribute("paint-order", "stroke"); text.setAttribute("stroke", "#000"); text.setAttribute("stroke-width", "2"); text.setAttribute("fill", "#fff"); // überschreibt CSS-Größe text.setAttribute("x", P.x); text.setAttribute("y", P.y); text.setAttribute("class", "wedge-label"); text.setAttribute("text-anchor", "middle"); text.setAttribute("dominant-baseline", "middle"); text.textContent = user; svg.appendChild(path); svg.appendChild(text); }); // Submit-Button dynamisch hinzufügen let submitBtn = document.getElementById("submitGuesses"); if (!submitBtn) { submitBtn = document.createElement("button"); submitBtn.id = "submitGuesses"; submitBtn.className = "btn btn-primary"; submitBtn.style.marginTop = "8px"; submitBtn.textContent = "Submit"; optionsDiv.parentElement.appendChild(submitBtn); } //submitBtn.hidden = false; // warum disabled? //submitBtn.disabled = true; // Zu Beginn der Runde: Button verstecken, bis eine Auswahl existiert submitBtn.hidden = true; submitBtn.disabled = true; submitBtn.onclick = () => { if (!selectedGuesses.size) return; const guesses = Array.from(selectedGuesses); socket.send(JSON.stringify({ type: "submit-guesses", username, guesses })); //submitBtn.disabled = true; // Nach Abgabe sichtbar, aber disabled (ausgegraut) submitBtn.hidden = false; submitBtn.disabled = true; // Sperre Eingaben nach Abgabe optionsDiv.querySelectorAll(".wedge").forEach(w => w.classList.add("disabled")); }; // Start-Button ausblenden + Rundensektion einblenden startBtn.hidden = true; startBtn.disabled = true; roundArea.hidden = false; // Geladene Songs (beide Orte befüllen: Aside + Drawer) const songList = document.getElementById("songList"); const songListArea = document.getElementById("songListArea"); const userTracks = trackInfos?.[username] ?? []; if (songList) { songList.innerHTML = ""; userTracks.forEach(t => { const li = document.createElement("li"); li.textContent = t; songList.appendChild(li); }); } if (songListArea) { songListArea.innerHTML = ""; userTracks.forEach(t => { // Erwartetes Format "Titel - Künstler" const [title, artists] = String(t).split(" - "); const row = document.createElement("div"); row.className = "track"; row.innerHTML = `
${esc(title ?? t)}${esc(artists ?? "")}
`; songListArea.appendChild(row); }); } } // 7) Ergebnis + Weiter-Button function renderScoreboard(scores) { scoreboard.innerHTML = ""; if (!scores) return; const entries = Object.entries(scores) .sort((a, b) => (b[1] - a[1]) || a[0].localeCompare(b[0])); // Punkte absteigend for (const [user, pts] of entries) { const li = document.createElement("li"); li.textContent = `${user}: ${pts} Punkte`; scoreboard.append(li); } } //let lastScores = null; function handleRoundResult({ scores, guesses, owner }) { renderScoreboard(scores); lastScores = scores; (() => { // Button verstecken/disable und Eingaben sperren const sb = document.getElementById('submitGuesses'); if (sb) { sb.disabled = true; sb.hidden = true; } document.querySelectorAll('#options .wedge').forEach(w => w.classList.add('disabled')); // lokale Auswahl leeren (optional) try { selectedGuesses.clear?.(); } catch(_) {} })(); try { const wedges = document.querySelectorAll("#options .wedge"); // Owner-Slice immer grün wedges.forEach(w => { if (w.getAttribute("data-user") === owner) w.classList.add("correct"); }); // Nur die EIGENE Auswahl einfärben: rot wenn falsch, sonst grün // const myGuess = guesses?.[username]; // if (myGuess) { // const myWedge = Array.from(wedges).find(w => w.getAttribute("data-user") === myGuess); // if (myWedge) { // if (myGuess === owner) { // myWedge.classList.add("correct"); // } else { // myWedge.classList.add("wrong"); // nur dieser wird rot // } // } // } // } catch (e) {} const my = guesses?.[username]; const myArr = Array.isArray(my) ? my : (typeof my === "string" ? [my] : []); if (myArr.length) { myArr.forEach(sel => { const w = Array.from(wedges).find(x => x.getAttribute("data-user") === sel); if (!w) return; if (sel === owner) w.classList.add("correct"); else w.classList.add("wrong"); }); } } catch (_) {} // resultP.innerHTML = ""; // Object.entries(guesses || {}).forEach(([user, guess]) => { // const correct = guess === owner; // const icon = correct ? "✅" : "❌"; // const delta = correct ? "+3" : "-1"; // const p = document.createElement("p"); // p.textContent = `${icon} ${user} hat auf ${guess} getippt${correct ? " (richtig!)" : " (falsch)"} [${delta}]`; // resultP.appendChild(p); // }); resultP.innerHTML = ""; Object.entries(guesses || {}).forEach(([user, g]) => { const list = Array.isArray(g) ? g : (typeof g === "string" ? [g] : []); const correct = list.includes(owner); const wrongCount = list.length - (correct ? 1 : 0); //const delta = (correct ? 3 : 0) - wrongCount; const bonus = wrongCount === 0 ? 1 : 0; // fehlerfrei-Bonus const delta = (correct ? 3 : 0) - wrongCount + bonus; const icon = correct ? "✅" : "❌"; const picks = list.length ? list.join(", ") : "—"; const p = document.createElement("p"); //p.textContent = `${icon} ${user} hat auf ${picks} getippt${correct ? " (richtig!)" : ""} [${delta >= 0 ? "+" : ""}${delta}]`; p.textContent = `${icon} ${user} hat auf ${picks} getippt${correct ? " (richtig!)" : ""}${bonus ? " (+1 Bonus)" : ""} [${delta >= 0 ? "+" : ""}${delta}]`; resultP.appendChild(p); }); const nextBtn = document.getElementById("nextRound"); nextBtn.hidden = false; nextBtn.disabled = false; nextBtn.onclick = () => { socket.send(JSON.stringify({ type: "next-round" })); nextBtn.hidden = true; nextBtn.disabled = true; resultP.textContent = ""; startBtn.hidden = true; startBtn.disabled = true; roundArea.hidden = true; //const submitBtn = document.getElementById("submitGuesses"); //if (submitBtn) submitBtn.hidden = true; const submitBtn = document.getElementById("submitGuesses"); if (submitBtn) { submitBtn.hidden = true; submitBtn.disabled = true; } }; } // 8) Spielende -> Start-Button zurück function handleGameEnd({ winner }) { const overlay = document.getElementById("winnerOverlay"); const nameEl = document.getElementById("winName"); const ptsEl = document.getElementById("winPoints"); //const btnNext = document.getElementById("winNext"); const btnClose= document.getElementById("winClose"); const btnShare= document.getElementById("winShare"); nameEl.textContent = winner; const pts = lastScores && typeof lastScores[winner] !== "undefined" ? lastScores[winner] : null; ptsEl.textContent = pts != null ? `${winner} gewinnt mit ${pts} Punkten.` : ""; // Leaderboard im Overlay rendern (aus Cache lastScores) const boardEl = document.getElementById("winBoard"); if (boardEl) { boardEl.innerHTML = ""; const entries = lastScores ? Object.entries(lastScores).sort((a,b)=> (b[1]-a[1]) || a[0].localeCompare(b[0])) : []; entries.forEach(([user, pts], i) => { const li = document.createElement("li"); if (user === winner) li.classList.add("winner"); const medal = i===0 ? "🥇" : i===1 ? "🥈" : i===2 ? "🥉" : String(i+1); li.innerHTML = `
${medal}
${user}
${pts} Punkte
`; boardEl.appendChild(li); }); } // Konfetti erzeugen (einmalig pro Anzeige) Array.from(overlay.querySelectorAll(".confetti")).forEach(n => n.remove()); const colors = [ "var(--accent)", "#21d07a", "#b3b3b3", "#ffffff" ]; for (let i=0;i<24;i++){ const c = document.createElement("div"); c.className = "confetti"; c.style.left = Math.random()*100 + "vw"; c.style.background = colors[i % colors.length]; c.style.animationDelay = (Math.random()*1.2) + "s"; c.style.transform = `translateY(-10vh) rotate(${Math.random()*360}deg)`; overlay.appendChild(c); } // Buttons //btnNext.onclick = () => { // socket.send(JSON.stringify({ type: "next-round" })); // overlay.style.display = "none"; // roundArea.hidden = true; // resultP.textContent = ""; //}; btnClose.onclick = () => { overlay.style.display = "none"; }; btnShare.onclick = async () => { const text = `🏆 ${winner} hat Spotify Roulette gewonnen!`; try { if (navigator.share) await navigator.share({ text }); else { await navigator.clipboard.writeText(text); showToast("Text kopiert – teilen!"); } } catch (_) {} }; const nextBtn = document.getElementById("nextRound"); //resultP.textContent = `🎉 ${winner} hat gewonnen!`; //nextBtn.hidden = true; //nextBtn.disabled = true; if (nextBtn) { nextBtn.hidden = true; nextBtn.disabled = true; } startBtn.hidden = false; startBtn.disabled = false; // Overlay zeigen overlay.style.display = "flex"; //setTimeout(() => { // startBtn.hidden = false; // startBtn.disabled = false; // roundArea.hidden = true; // scoreboard.innerHTML = ""; //}, 6000); } // Spotify-Playback Funktion (unverändert) async function playOnSpotify(trackUri, username) { const deviceId = document.getElementById("deviceSelect")?.value; if (deviceId === "") { //alert("Bitte ein Wiedergabegerät auswählen!"); //keine warnung i guess return; } try { const response = await fetch("/api/spotify/play", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, device_id: deviceId, track_uri: trackUri }) }); if (!response.ok) { const error = await response.text(); alert(`Fehler beim Abspielen: ${error}`); } } catch (err) { alert(`Netzwerkfehler: ${err.message}`); } } window.playOnSpotify = playOnSpotify;