533 lines
19 KiB
JavaScript
533 lines
19 KiB
JavaScript
// 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 = `
|
||
<iframe
|
||
src="https://open.spotify.com/embed/track/${trackId}"
|
||
width="100%" height="80" frameborder="0"
|
||
allow="encrypted-media">
|
||
</iframe>`;
|
||
|
||
// 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 = `
|
||
<div class="tcover"></div>
|
||
<div class="meta"><b>${esc(title ?? t)}</b><span>${esc(artists ?? "")}</span></div>`;
|
||
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 = `
|
||
<div class="left">
|
||
<div class="rank">${medal}</div>
|
||
<div class="name">${user}</div>
|
||
</div>
|
||
<div class="pts">${pts} Punkte</div>
|
||
`;
|
||
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;
|