Files
filmdle/static/js/game.js
SimolZimol 526b0353e7 new file: .env.example
new file:   .gitignore
	new file:   Dockerfile
	new file:   README.md
	new file:   app.py
	new file:   app/__init__.py
	new file:   app/game.py
	new file:   app/models.py
	new file:   app/routes.py
	new file:   app/seed.py
	new file:   data/movies.json
	new file:   docker-compose.yml
	new file:   requirements.txt
	new file:   static/css/style.css
	new file:   static/js/game.js
	new file:   static/js/player.js
	new file:   templates/index.html
2026-08-07 20:48:08 +02:00

376 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
const mode = document.body.dataset.mode; // 'daily' | 'unlimited'
const searchInput = document.getElementById("search-input");
const autocompleteBox = document.getElementById("autocomplete");
const boardBody = document.getElementById("board-body");
const attemptsInfo = document.getElementById("attempts-info");
const resultBox = document.getElementById("result-box");
const nicknameModal = document.getElementById("nickname-modal");
const playerNameEl = document.getElementById("player-name");
const streakLabel = document.getElementById("streak-label");
let player = PlayerStore.load();
let sessionId = null;
let maxAttempts = 10;
let finished = false;
// ---------- Spieler ----------
function showNicknameModal() {
if (!player) {
nicknameModal.classList.remove("hidden");
} else {
nicknameModal.classList.add("hidden");
playerNameEl.textContent = player.nickname;
}
renderStreaks();
}
async function submitNickname() {
const input = document.getElementById("nickname-input");
const nickname = input.value.trim();
if (!nickname) return;
const res = await fetch("/api/player", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nickname }),
});
const data = await res.json();
if (data.player) {
player = data.player;
PlayerStore.save(player);
PlayerStore.setStreaks(data.streaks);
nicknameModal.classList.add("hidden");
playerNameEl.textContent = player.nickname;
renderStreaks();
startGame();
}
}
function renderStreaks() {
const streaks = PlayerStore.getStreaks();
if (!streaks) { streakLabel.textContent = ""; return; }
const s = mode === "daily" ? streaks.daily : streaks.unlimited;
if (!s) { streakLabel.textContent = ""; return; }
streakLabel.textContent = `🏆 ${s.win} Siege · 💀 ${s.loss} Niederlagen`;
}
// ---------- Spielstart ----------
async function startGame() {
if (!player) return;
const res = await fetch("/api/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ player_id: player.id, mode }),
});
const data = await res.json();
if (data.error) { alert(data.error); return; }
sessionId = data.session_id;
maxAttempts = data.max_attempts;
attemptsInfo.textContent = `Versuche: 0 / ${maxAttempts}`;
if (data.already_played) {
finished = true;
showResult(data.status, data.target, data.attempts_used);
}
}
// ---------- Suche / Autocomplete ----------
let searchTimer = null;
let suggestions = [];
let selectedIndex = -1;
searchInput.addEventListener("input", () => {
clearTimeout(searchTimer);
const q = searchInput.value.trim();
if (!q) { closeAutocomplete(); return; }
searchTimer = setTimeout(async () => {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
suggestions = await res.json();
renderAutocomplete();
}, 200);
});
function renderAutocomplete() {
autocompleteBox.innerHTML = "";
selectedIndex = -1;
if (!suggestions.length) { closeAutocomplete(); return; }
suggestions.forEach((s, i) => {
const div = document.createElement("div");
div.className = "autocomplete-item";
div.innerHTML = `<span>${escapeHtml(s.title)}</span><span class="year">${s.year}</span>`;
div.addEventListener("mousedown", (e) => {
e.preventDefault();
selectSuggestion(i);
});
div.addEventListener("mouseenter", () => { selectedIndex = i; highlight(); });
autocompleteBox.appendChild(div);
});
autocompleteBox.classList.add("open");
}
function highlight() {
[...autocompleteBox.children].forEach((el, i) => {
el.classList.toggle("selected", i === selectedIndex);
});
}
function closeAutocomplete() {
autocompleteBox.classList.remove("open");
autocompleteBox.innerHTML = "";
}
function selectSuggestion(i) {
const s = suggestions[i];
if (!s) return;
closeAutocomplete();
submitGuess(s);
}
searchInput.addEventListener("keydown", (e) => {
if (!autocompleteBox.classList.contains("open")) return;
if (e.key === "ArrowDown") { e.preventDefault(); selectedIndex = Math.min(selectedIndex + 1, suggestions.length - 1); highlight(); }
else if (e.key === "ArrowUp") { e.preventDefault(); selectedIndex = Math.max(selectedIndex - 1, 0); highlight(); }
else if (e.key === "Enter") { e.preventDefault(); selectSuggestion(selectedIndex); }
else if (e.key === "Escape") { closeAutocomplete(); }
});
document.addEventListener("click", (e) => {
if (!searchWrapperContains(e.target)) closeAutocomplete();
});
function searchWrapperContains(t) {
const w = document.getElementById("search-wrapper");
return w.contains(t);
}
// ---------- Tipp abgeben ----------
async function submitGuess(movie) {
if (finished || !sessionId) return;
searchInput.value = "";
const res = await fetch("/api/guess", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: sessionId, movie_id: movie.id }),
});
const data = await res.json();
if (data.error) {
if (data.status) {
finished = true;
showResult(data.status, null, data.attempts_used || 0);
} else {
alert(data.error);
}
return;
}
attemptsInfo.textContent = `Versuche: ${data.attempts_used} / ${data.max_attempts}`;
addRow(data.row);
if (data.finished) {
finished = true;
if (data.streaks) {
PlayerStore.setStreaks(data.streaks);
renderStreaks();
}
showResult(data.status, data.target, data.attempts_used);
}
}
// ---------- Icons (Gamedle-/Steam-Stil) ----------
const ICONS = {
platforms: {
"Netflix": ["📺", "#e50914"],
"Disney+": ["🐭", "#3f6fc9"],
"Prime Video": ["📦", "#00a8e1"],
"Max": ["🌊", "#6a4fa3"],
"Apple TV+": ["🍎", "#9aa0a6"],
"Paramount+": ["🏔️", "#0f5cab"],
"Blu-ray": ["💿", "#2f6fbe"],
"DVD": ["💿", "#8f959b"],
"4K UHD": ["🖥️", "#7f5cd6"],
"Cinema": ["🍿", "#e53935"],
},
genres: {
"Action": ["💥", "#ff7043"],
"Abenteuer": ["🗺️", "#a1887f"],
"Animation": ["🎨", "#ab47bc"],
"Komödie": ["😂", "#f6c445"],
"Krimi": ["🕵️", "#5c6bc0"],
"Drama": ["🎭", "#7e8b98"],
"Fantasy": ["🐉", "#7e57c2"],
"Horror": ["👻", "#546e7a"],
"Mystery": ["🔍", "#26a69a"],
"Romantik": ["💘", "#ec407a"],
"Sci-Fi": ["🚀", "#29b6f6"],
"Thriller": ["🔪", "#8d2f2f"],
"Western": ["🤠", "#a1887f"],
"Musical": ["🎵", "#f06292"],
"Krieg": ["🪖", "#6d4c41"],
"Biografie": ["📖", "#b0bec5"],
},
studios: {
"Warner Bros": ["🎞️", "#3f6fc9"],
"Disney": ["🏰", "#2f6fbe"],
"Universal": ["🌐", "#1d8aaa"],
"Paramount": ["🏔️", "#3c5a93"],
"Sony Pictures": ["🎥", "#7f5cd6"],
"20th Century": ["🦊", "#e0892f"],
"MGM": ["🦁", "#c2a845"],
"Lionsgate": ["🦁", "#9aa0a6"],
"A24": ["⏳", "#e2a11f"],
"DreamWorks": ["🌙", "#5b6b8c"],
"New Line": ["🟦", "#2f6fbe"],
"Orion": ["⭐", "#c9c9c9"],
"Neon": ["⚡", "#1fb8e0"],
"CJ Entertainment": ["🎬", "#c93030"],
"Show East": ["🎥", "#666"],
"Dimension": ["🌀", "#9c27b0"],
"Netflix": ["📺", "#e50914"],
},
universes: {
"MCU": ["🛡️", "#d63031"],
"Star Wars": ["🎆", "#3a3f47"],
"DC": ["🦇", "#1f2937"],
"Wizarding World": ["⚡", "#7e57c2"],
"Fast & Furious": ["🏎️", "#37474f"],
"Herr der Ringe": ["💍", "#a1887f"],
"Spider-Man": ["🕷️", "#c62828"],
},
};
function iconFor(map, name) {
if (!name || !map[name]) return "▪️";
return map[name][0];
}
function colorFor(map, name) {
if (!map || !name || !map[name]) return null;
return map[name][1];
}
// Badge mit Icon + Label
function badge(map, name) {
if (!name) return "";
const color = colorFor(map, name);
const style = color ? `style="--tag:${color}"` : "";
return `<span class="badge" ${style}><span class="bicon">${iconFor(map, name)}</span>${escapeHtml(name)}</span>`;
}
function addRow(row) {
const tr = document.createElement("tr");
const titleTd = document.createElement("td");
titleTd.className = "title-cell";
titleTd.textContent = row.title;
tr.appendChild(titleTd);
tr.appendChild(cell(row.platforms.value.map(v => badge(ICONS.platforms, v)).join(""), row.platforms.status));
tr.appendChild(cell(row.genres.value.map(v => badge(ICONS.genres, v)).join(""), row.genres.status));
tr.appendChild(cell(yearHtml(row.year), row.year.status));
tr.appendChild(cell(fskHtml(row.fsk.value), row.fsk.status));
tr.appendChild(cell(badge(ICONS.studios, row.studio.value), row.studio.status));
tr.appendChild(cell(row.director.value ? `<span class="director">${escapeHtml(row.director.value)}</span>` : `<span class="none"></span>`, row.director.status));
tr.appendChild(cell(budgetHtml(row.budget), row.budget.status));
tr.appendChild(cell(badge(ICONS.universes, row.universe.value), row.universe.status));
boardBody.prepend(tr);
}
function yearHtml(row) {
return `<span class="num">${row.value}</span>${arrowDiff(row.direction, row.diff)}`;
}
function fskHtml(fsk) {
if (fsk == null) return `<span class="none"></span>`;
return `<span class="fsk fsk-${fsk}">${fsk}</span>`;
}
function budgetHtml(row) {
if (row.value == null) return `<span class="none"></span>`;
return `<span class="num">${formatMoney(row.value)}</span>${arrowMoney(row.direction, row.diff)}`;
}
// Pfeil mit Zahl (z.B. Jahr): ▲ 5 / ▼ 3
function arrowDiff(dir, diff) {
if (dir === "equal" || diff == null) return "";
const cls = dir === "up" ? "up" : "down";
const gly = dir === "up" ? "▲" : "▼";
return `<span class="dir-pill ${cls}">${gly} ${Math.abs(diff)}</span>`;
}
// Pfeil mit Differenz in Geld (Budget)
function arrowMoney(dir, diff) {
if (dir === "equal" || diff == null) return "";
const cls = dir === "up" ? "up" : "down";
const gly = dir === "up" ? "▲" : "▼";
const n = Math.abs(diff);
const txt = n >= 1000000 ? (n / 1000000).toFixed(0) + " Mio $" : Math.round(n / 1000) + " Tsd $";
return `<span class="dir-pill ${cls}">${gly} ${txt}</span>`;
}
function cell(inner, status) {
const td = document.createElement("td");
td.innerHTML = inner;
td.classList.add("cell", status);
return td;
}
function formatMoney(n) {
if (n >= 1000000) return (n / 1000000).toFixed(0) + " Mio $";
return n.toLocaleString("de-DE") + " $";
}
// ---------- Ergebnis ----------
function showResult(status, target, attemptsUsed) {
resultBox.classList.remove("hidden", "won", "lost");
if (status === "won") {
resultBox.classList.add("won");
resultBox.innerHTML = `<div>🎉 <strong>Gewonnen!</strong> Du hast den Film in ${attemptsUsed} Versuch(en) erraten.</div>${target ? `<div class="target-title">${escapeHtml(target.title)} (${target.year})</div>` : ""}${newGameButton()}`;
} else {
resultBox.classList.add("lost");
resultBox.innerHTML = `<div>😞 <strong>Verloren!</strong> Du hattest ${attemptsUsed} Versuch(e).</div>${target ? `Der gesuchte Film war: <div class="target-title">${escapeHtml(target.title)} (${target.year})</div>` : ""}${newGameButton()}`;
}
}
function newGameButton() {
const label = mode === "unlimited" ? "Neues Spiel" : "Zurück zu Täglich";
if (mode === "unlimited") {
return `<button id="new-game-btn">${label}</button>`;
}
return "";
}
document.addEventListener("click", async (e) => {
if (e.target.id === "nickname-submit") await submitNickname();
if (e.target.id === "new-game-btn") {
finished = false;
resultBox.classList.add("hidden");
resultBox.innerHTML = "";
boardBody.innerHTML = "";
const res = await fetch("/api/new-game", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ player_id: player.id }),
});
const data = await res.json();
sessionId = data.session_id;
maxAttempts = data.max_attempts;
attemptsInfo.textContent = `Versuche: 0 / ${maxAttempts}`;
}
});
document.getElementById("nickname-input").addEventListener("keydown", (e) => {
if (e.key === "Enter") submitNickname();
});
// ---------- Init ----------
function escapeHtml(s) {
if (s == null) return "";
const div = document.createElement("div");
div.textContent = String(s);
return div.innerHTML;
}
window.addEventListener("load", () => {
showNicknameModal();
if (player) startGame();
});
})();