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

338 lines
11 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`;
}
// 6) Neue Runde anzeigen
async function handleRoundStart({ ownerOptions, songUri, trackInfos }) {
// UI zurücksetzen
resultP.textContent = "";
optionsDiv.innerHTML = "";
songEmbed.innerHTML = "";
// 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");
});
// 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);
});
// 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 = "";
Object.entries(scores).forEach(([user, pts]) => {
const li = document.createElement("li");
li.textContent = `${user}: ${pts} Punkte`;
scoreboard.append(li);
});
}
function handleRoundResult({ scores, guesses, owner }) {
renderScoreboard(scores);
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) {}
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);
});
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;
};
}
// 8) Spielende -> Start-Button zurück
function handleGameEnd({ winner }) {
const nextBtn = document.getElementById("nextRound");
resultP.textContent = `🎉 ${winner} hat gewonnen!`;
nextBtn.hidden = true;
nextBtn.disabled = true;
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!");
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;