338 lines
11 KiB
JavaScript
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;
|