SpotifyRoulette/src/main/resources/public/js/game.js

533 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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;