feat: der letzte Auftrag - Vollständiges Deployment

- Frontend: HTML/CSS/JS Spielapp (1642 Zeilen)
- Backend: Node.js/Express + SQLite server
- REST API: game, spieler, notes
- AI_INSTRUCTIONS.md documentation
This commit is contained in:
Clawdia
2026-05-08 10:47:49 +02:00
parent 2cd6181492
commit df49ba3f63
2 changed files with 397 additions and 262 deletions
+209 -262
View File
@@ -1256,165 +1256,147 @@
</div><!-- /app -->
<script>
// ══════════════════════════════════════════
// DATA
// ══════════════════════════════════════════
const quests = [
{
id: 0,
badge: 'easy',
badgeLabel: 'EINSTEIGER',
name: 'Das Lied der Liebe',
holder: 'Hüter des <strong>Briefes</strong>',
item: '💌 Brief des ersten Dates',
desc: 'Der Held muss der Runde einen Liebesfilm-Titel nennen und ihn pantomimisch darstellen die anderen raten.',
success: '✓ Brief bekommt er',
fail: '✗ Ein Schluck',
details: `<strong>Ablauf:</strong> Der Hüter flüstert dem Held einen Filmtitel ins Ohr (z.B. Titanic, Die Schöne und das Biest, Notting Hill). Der Held darf nicht sprechen und muss den Titel pantomimisch darstellen. Zeitlimit: 60 Sekunden.`,
},
{
id: 1,
badge: 'easy',
badgeLabel: 'EINSTEIGER',
name: 'Der Wissenstest',
holder: 'Hüter des <strong>Rings</strong>',
item: '💍 Ring der Verbindung',
desc: 'Der Hüter stellt 5 Fragen über die Braut. Der Held muss mindestens 3 richtig beantworten.',
success: '✓ Ring bekommt er',
fail: '✗ Pro Falschantwort ein Schluck',
details: `<strong>Fragen vorbereiten (Hüter):</strong> Wähle 5 aus diesen Kategorien:<br>
• Lieblingsessen? • Lieblingsfilm? • Wo wollte sie immer hinreisen?<br>
• Was ist ihr größter Tick? • Wie heißt ihre beste Freundin?<br>
• Was war ihr erstes Haustier? • Was mag sie am Bräutigam am meisten?<br><br>
<strong>Hinweis:</strong> Fragen vorab mit der Braut abstimmen.`,
},
{
id: 2,
badge: 'easy',
badgeLabel: 'EINSTEIGER',
name: 'Das Duell der Münzen',
holder: 'Hüter des <strong>Schlüssels</strong>',
item: '🔑 Schlüssel zur Zukunft',
desc: 'Münze oder Kronkorken werfen: Wer 3 von 5 Würfen in einen Becher trifft, gewinnt.',
success: '✓ Schlüssel bekommt er',
fail: '✗ Ein ordentlicher Schluck',
details: `<strong>Setup:</strong> Einen Becher auf den Tisch stellen, ca. 12 Meter Abstand. Jeder wirft abwechselnd (Held vs. Hüter). Bester aus 5 Versuchen gewinnt.<br><br>
<strong>Regelung bei Gleichstand:</strong> Plötzlicher Tod wer als nächstes trifft, gewinnt.`,
},
{
id: 3,
badge: 'easy',
badgeLabel: 'EINSTEIGER',
name: 'Der Schwur der tausend Worte',
holder: 'Hüter der <strong>Blume</strong>',
item: '🌹 Blume der Ewigkeit',
desc: 'Der Held muss in 90 Sekunden einen Heiratsantrag improvisieren vor allen Gästen.',
success: '✓ Blume bekommt er',
fail: '✗ Kein Applaus = trinken',
details: `<strong>Ablauf:</strong> Der Held hat 90 Sekunden Zeit für einen improvisierten Heiratsantrag an die (nicht anwesende) Braut. Die Gäste klatschen am Ende.<br><br>
<strong>Bewertung:</strong> Lautester Applaus = Erfolg. Stille oder Widerspruch = Misserfolg. Der Hüter entscheidet im Zweifelsfall.`,
},
{
id: 4,
badge: 'medium',
badgeLabel: 'MITTEL',
name: 'Die Chronik der Scham',
holder: 'Hüter des <strong>Fotos</strong>',
item: '📸 Foto der gemeinsamen Zeit',
desc: 'Der Hüter erzählt eine peinliche Geschichte des Helden. Wer davon die meisten Details wusste, gewinnt.',
success: '✓ Foto und eine Geschichte über die Braut zurück',
fail: '✗ Der Held trinkt und erzählt selbst die Geschichte',
details: `<strong>Ablauf:</strong> Der Hüter hat vorab eine peinliche Geschichte über den Helden vorbereitet. Er liest sie vor und stellt danach 3 Detailfragen. Kann der Held 2 von 3 beantworten? → Erfolg.<br><br>
<strong>Empfehlung:</strong> Eine Person, die den Helden gut kennt, übernimmt diese Rolle.`,
},
{
id: 5,
badge: 'medium',
badgeLabel: 'MITTEL',
name: 'Das Blindverkostung',
holder: 'Hüter des <strong>Schuhs</strong>',
item: '👠 Schuh der Flucht',
desc: '5 Getränke blind verkosten und benennen. Mindestens 3 richtig.',
success: '✓ Schuh bekommt er',
fail: '✗ Alle falsch erkannten Getränke austrinken',
details: `<strong>Vorbereitung:</strong> 5 Getränke in nummerierten Bechern vorbereiten (Mischung aus Alkohol und Softdrinks). Augenbinde aufsetzen.<br><br>
<strong>Regeln:</strong> Riechen ist erlaubt. Geraten werden darf nur auf Basis der Verkostung.`,
},
{
id: 6,
badge: 'hard',
badgeLabel: 'HERAUSFORDERUNG',
name: 'Der Zweikampf der Weisheit',
holder: 'Hüter des <strong>Maskottchens</strong>',
item: '🧸 Maskottchen des Heils',
desc: 'Ein Trivia-Duell: Pop-Kultur, Warhammer & DnD-Fragen. Wer 3 von 5 beantwortet, gewinnt.',
success: '✓ Maskottchen und ein Ehrentitel',
fail: '✗ Zwei große Schlucke',
details: `<strong>Beispielfragen:</strong><br>
• RPG: „Wie viele Seiten hat ein W20?" (20)<br>
• Warhammer: „Welche Fraktion trägt Schädelornamente als Zeichen der Loyalität?" (Space Marines / diverse)<br>
• DnD: „Was ist ein nat. 20?" (Kritischer Erfolg)<br>
• Allgemein: „Wer schrieb 'Der Herr der Ringe'?" (Tolkien)<br>
• Persönlich: Eine Frage über das Paar.<br><br>
<strong>Hinweis:</strong> Schwierigkeitsgrad der Fragen an die Gruppe anpassen.`,
},
{
id: 7,
badge: 'hard',
badgeLabel: 'HERAUSFORDERUNG',
name: 'Die letzte Prüfung',
holder: 'Hüter der <strong>Urkunde</strong>',
item: '🏆 Urkunde der Heldentaten',
desc: 'Der Held muss einen Abend-Rückblick halten: Die 5 peinlichsten Momente des Abends, dramatisch vorgetragen.',
success: '✓ Urkunde + Applaus der Legende',
fail: '✗ Nochmal von vorne lauter!',
details: `<strong>Ablauf:</strong> Diese Quest steht am Ende des Abends. Der Held fasst in einer Rede (23 Min.) die Ereignisse des Abends zusammen pointiert und feierlich vorgetragen.<br><br>
<strong>Bewertung:</strong> Stehende Ovationen = Erfolg. Unzureichende Darbietung = Neuversuch.<br><br>
<strong>Abschluss:</strong> Nach bestandener Quest wird die Urkunde feierlich überreicht.`,
},
];
// ══════════════════════════════════════════
// PART 1: API-HELPERS & STATE-SYNC
// ══════════════════════════════════════════
const API = '/api';
async function apiGet(url) {
try {
const r = await fetch(API + url);
return r.ok ? r.json() : null;
} catch(e) { console.error('GET failed:', e); return null; }
}
async function apiPost(url, body) {
try {
const r = await fetch(API + url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body || {})
});
return r.ok ? r.json() : null;
} catch(e) { console.error('POST failed:', e); return null; }
}
async function apiPut(url, body) {
try {
const r = await fetch(API + url, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body || {})
});
return r.ok ? r.json() : null;
} catch(e) { console.error('PUT failed:', e); return null; }
}
async function apiDelete(url) {
try {
const r = await fetch(API + url, {method: 'DELETE'});
return r.ok ? r.json() : null;
} catch(e) { console.error('DELETE failed:', e); return null; }
}
// Lokale State-Variablen
const itemCollected = new Array(8).fill(false);
let drinks = 0;
const questDone = new Array(quests.length).fill(false);
const questDone = new Array(8).fill(false);
// ══════════════════════════════════════════
// STARS
// ══════════════════════════════════════════
// State vom Server laden
async function syncState() {
const data = await apiGet('/game');
if (!data) return;
const s = data.state;
if (s.itemCollected) s.itemCollected.forEach((v, i) => { itemCollected[i] = v; });
if (s.drinks !== undefined) drinks = s.drinks;
if (s.questDone) s.questDone.forEach((v, i) => { questDone[i] = v; });
document.getElementById('drinkCount').textContent = drinks;
}
// State zum Server speichern
async function saveStateToServer(state) {
await apiPost('/game/save-state', { ...state, timestamp: Date.now() });
}
// Lokale UI aktualisieren
function refreshUI() {
for (let i = 0; i < 8; i++) {
const slot = document.getElementById('inv-' + i);
if (slot) slot.classList.toggle('collected', itemCollected[i]);
}
const dc = document.getElementById('drinkCount');
if (dc) dc.textContent = drinks;
updateQuestStatus();
renderSpieler();
}
// Sync + Refresh
async function updateAndSync(state) {
await saveStateToServer(state);
refreshUI();
}
// ══════════════════════════════════════════════════════
// PART 2: QUEST-DATEN
// ═══════════════════════════════════════════
const quests = [
{ id:0, badge:'easy', badgeLabel:'EINSTEIGER', name:'Das Lied der Liebe',
holder:'Hüter des <strong>Briefes</strong>', item:'💌 Brief des ersten Dates',
desc:'Der Held muss der Runde einen Liebesfilm-Titel nennen und ihn pantomimisch darstellen.',
success:'✓ Brief bekommt er', fail:'✗ Ein Schluck',
details:'Der Hüter flüstert dem Held einen Filmtitel ins Ohr. Der Held stellt ihn pantomimisch dar (60s).' },
{ id:1, badge:'easy', badgeLabel:'EINSTEIGER', name:'Der Wissenstest',
holder:'Hüter des <strong>Rings</strong>', item:'💍 Ring der Verbindung',
desc:'Der Hüter stellt 5 Fragen über die Braut. Mindestens 3 richtig = Gewinn.',
success:'✓ Ring bekommt er', fail:'✗ Pro Falschantwort ein Schluck',
details:'Fragen: Lieblingsessen, Lieblingsfilm, Reisetraum, größter Tick, beste Freundin.' },
{ id:2, badge:'easy', badgeLabel:'EINSTEIGER', name:'Das Duell der Münzen',
holder:'Hüter des <strong>Schlüssels</strong>', item:'🔑 Schlüssel zur Zukunft',
desc:'Münze oder Kronkorken werfen: Wer 3 von 5 in einen Becher trifft, gewinnt.',
success:'✓ Schlüssel bekommt er', fail:'✗ Ein ordentlicher Schluck',
details:'Becher 1-2m entfernt. Bester aus 5 Versuchen. Bei Gleichstand: plötzlicher Tod.' },
{ id:3, badge:'easy', badgeLabel:'EINSTEIGER', name:'Der Schwur der tausend Worte',
holder:'Hüter der <strong>Blume</strong>', item:'🌹 Blume der Ewigkeit',
desc:'Der Held muss in 90 Sekunden einen Heiratsantrag improvisieren vor allen Gästen.',
success:'✓ Blume bekommt er', fail:'✗ Kein Applaus = trinken',
details:'90s improvisierter Heiratsantrag. Lautester Applaus = Erfolg.' },
{ id:4, badge:'medium', badgeLabel:'MITTEL', name:'Die Chronik der Scham',
holder:'Hüter des <strong>Fotos</strong>', item:'📸 Foto der gemeinsamen Zeit',
desc:'Der Hüter erzählt eine peinliche Geschichte. Wer die meisten Details wusste, gewinnt.',
success:'✓ Foto und Geschichte über die Braut', fail:'✗ Held trinkt & erzählt selbst',
details:'Hüter liest peinliche Geschichte. 3 Detailfragen. 2/3 richtig = Erfolg.' },
{ id:5, badge:'medium', badgeLabel:'MITTEL', name:'Das Blindverkostung',
holder:'Hüter des <strong>Schuhs</strong>', item:'👠 Schuh der Flucht',
desc:'5 Getränke blind verkosten und benennen. Mindestens 3 richtig.',
success:'✓ Schuh bekommt er', fail:'✗ Alle falsch erkannten austrinken',
details:'5 nummerierte Becher (Alkohol+Softdrinks). Augenbinde auf. Riechen erlaubt.' },
{ id:6, badge:'hard', badgeLabel:'HERAUSFORDERUNG', name:'Der Zweikampf der Weisheit',
holder:'Hüter des <strong>Maskottchens</strong>', item:'🧸 Maskottchen des Heils',
desc:'Trivia-Duell: Pop-Kultur, Warhammer & DnD. Wer 3 von 5 beantwortet, gewinnt.',
success:'✓ Maskottchen und Ehrentitel', fail:'✗ Zwei große Schlucke',
details:'RPG/Warhammer/DnD/Filmfragen. Schwierigkeitsgrad anpassen.' },
{ id:7, badge:'hard', badgeLabel:'HERAUSFORDERUNG', name:'Die letzte Prüfung',
holder:'Hüter der <strong>Urkunde</strong>', item:'🏆 Urkunde der Heldentaten',
desc:'Abend-Rückblick: Die 5 peinlichsten Momente, dramatisch vorgetragen.',
success:'✓ Urkunde + Applaus der Legende', fail:'✗ Nochmal von vorne lauter!',
details:'2-3 Min Rede. Stehende Ovationen = Erfolg.' },
];
// ═══════════════════════════════════════════
// PART 3: STARS, TABS & START
// ═══════════════════════════════════════════
(function createStars() {
const c = document.getElementById('stars');
if (!c) return;
for (let i = 0; i < 120; i++) {
const s = document.createElement('div');
s.className = 'star';
s.style.cssText = `
left:${Math.random()*100}%;
top:${Math.random()*100}%;
--d:${2+Math.random()*4}s;
--delay:${Math.random()*5}s;
--bright:${0.4+Math.random()*0.6};
width:${Math.random()<0.3?3:2}px;
height:${Math.random()<0.3?3:2}px;
`;
s.style.cssText = `left:${Math.random()*100}%;top:${Math.random()*100}%;
--d:${2+Math.random()*4}s;--delay:${Math.random()*5}s;
--bright:${0.4+Math.random()*0.6};width:${Math.random()<0.3?3:2}px;height:${Math.random()<0.3?3:2}px;`;
c.appendChild(s);
}
})();
// ══════════════════════════════════════════
// START
// ══════════════════════════════════════════
function startApp() {
document.getElementById('cover').style.display = 'none';
document.getElementById('app').style.display = 'block';
buildQuests();
buildDice();
loadState();
loadSpieler();
updateQuestStatus();
}
// ══════════════════════════════════════════
// TABS
// ══════════════════════════════════════════
function showTab(id) {
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
@@ -1422,11 +1404,22 @@ function showTab(id) {
event.target.classList.add('active');
}
// ══════════════════════════════════════════
// QUESTS
// ══════════════════════════════════════════
function startApp() {
document.getElementById('cover').style.display = 'none';
document.getElementById('app').style.display = 'block';
buildQuests();
buildDice();
syncState();
syncSpieler();
updateQuestStatus();
}
// ═══════════════════════════════════════════
// PART 4: QUEST-FUNKTIONEN
// ═══════════════════════════════════════════
function buildQuests() {
const grid = document.getElementById('questGrid');
if (!grid) return;
grid.innerHTML = '';
quests.forEach((q, i) => {
const card = document.createElement('div');
@@ -1442,13 +1435,13 @@ function buildQuests() {
<span class="reward-fail">${q.fail}</span>
</div>
<div class="quest-expand">${q.details}<br><br>
<button onclick="event.stopPropagation(); toggleDone(${i})"
style="font-family:'Cinzel',serif; font-size:0.75rem; background:transparent; border:1px solid #8a6520; padding:0.3rem 0.8rem; cursor:pointer; color:#8a6520; border-radius:3px;">
<button onclick="event.stopPropagation(); toggleDone(${i})"
style="font-family:'Cinzel',serif;font-size:0.75rem;background:transparent;border:1px solid #8a6520;
padding:0.3rem 0.8rem;cursor:pointer;color:#8a6520;border-radius:3px;">
${questDone[i] ? '↩ Als offen markieren' : '✓ Als erledigt markieren'}
</button>
</div>
<div class="expand-hint">▼ Aufklappen für Details</div>
`;
<div class="expand-hint">▼ Aufklappen für Details</div>`;
card.addEventListener('click', () => toggleExpand(i));
grid.appendChild(card);
});
@@ -1456,57 +1449,61 @@ function buildQuests() {
function toggleExpand(i) {
const card = document.getElementById('qcard-' + i);
card.classList.toggle('expanded');
if (card) card.classList.toggle('expanded');
}
function toggleDone(i) {
async function toggleDone(i) {
questDone[i] = !questDone[i];
saveState();
const sv = await apiGet('/game');
if (!sv) return;
const s = {...sv.state, questDone: [...questDone]};
await updateAndSync(s);
buildQuests();
updateQuestStatus();
}
// ══════════════════════════════════════════
// INVENTORY
// ══════════════════════════════════════════
function toggleItem(i) {
// ══════════════════════════════════════════
// PART 5: INVENTORY & DRINKS
// ══════════════════════════════════════════
async function toggleItem(i) {
itemCollected[i] = !itemCollected[i];
const slot = document.getElementById('inv-' + i);
slot.classList.toggle('collected', itemCollected[i]);
saveState();
if (slot) slot.classList.toggle('collected', itemCollected[i]);
const sv = await apiGet('/game');
if (!sv) return;
const s = {...sv.state, itemCollected: [...itemCollected]};
await updateAndSync(s);
updateQuestStatus();
}
// ══════════════════════════════════════════
// DRINKS
// ══════════════════════════════════════════
function changeDrinks(n) {
drinks = Math.max(0, drinks + n);
document.getElementById('drinkCount').textContent = drinks;
saveState();
async function changeDrinks(n) {
const res = await apiPost('/game/drink', {amount: n});
if (res && res.drinks !== undefined) {
drinks = res.drinks;
document.getElementById('drinkCount').textContent = drinks;
}
}
// ══════════════════════════════════════════
// STATUS
// ══════════════════════════════════════════
// ══════════════════════════════════════════
// PART 6: STATUS
// ══════════════════════════════════════════
function updateQuestStatus() {
const count = itemCollected.filter(Boolean).length;
document.getElementById('questCount').textContent = count + '/8';
const qc = document.getElementById('questCount');
if (qc) qc.textContent = count + '/8';
const list = document.getElementById('questStatusList');
if (!list) return;
list.innerHTML = quests.map((q, i) => `
<div style="display:flex; align-items:center; gap:0.5rem; font-size:0.9rem; color:${questDone[i]?'#2a5a1a':'#5a3a10'};">
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;
color:${questDone[i]?'#2a5a1a':'#5a3a10'};">
<span>${questDone[i] ? '✓' : '○'}</span>
<span>${q.name}</span>
<span style="margin-left:auto; font-size:0.8rem; opacity:0.7">${q.item}</span>
</div>
`).join('');
<span style="margin-left:auto;font-size:0.8rem;opacity:0.7">${q.item}</span>
</div>`).join('');
}
// ══════════════════════════════════════════
// DICE
// ══════════════════════════════════════════
// ═══════════════════════════════════════════
// PART 7: WÜRFEL
// ═══════════════════════════════════════════
function buildDice() {
const btn = document.getElementById('diceBtns');
if (!btn) return;
@@ -1523,6 +1520,7 @@ function rollDice(sides) {
const result = Math.floor(Math.random() * sides) + 1;
const el = document.getElementById('diceResult');
const log = document.getElementById('diceLog');
if (!el || !log) return;
el.classList.remove('rolling');
void el.offsetWidth;
el.classList.add('rolling');
@@ -1533,10 +1531,9 @@ function rollDice(sides) {
result >= Math.ceil(sides * 0.4) ? '~ Mittelmaß...' : '✗ Schwach. Trinken?';
log.textContent = `W${sides}${result} · ${reactions}`;
}
// ══════════════════════════════════════════
// SPIELER
// ══════════════════════════════════════════
// ═══════════════════════════════════════════
// PART 8: SPIELER (API-Version)
// ═══════════════════════════════════════════
let spieler = [];
const rolleLabels = {
@@ -1548,6 +1545,13 @@ const rolleLabels = {
gast: '🍺 Gast',
};
async function syncSpieler() {
const data = await apiGet('/spieler');
if (data) spieler = data;
populateQuestSelect();
renderSpieler();
}
function populateQuestSelect() {
const sel = document.getElementById('sQuest');
if (!sel) return;
@@ -1562,7 +1566,7 @@ function populateQuestSelect() {
sel.value = current;
}
function addSpieler() {
async function addSpieler() {
const nameEl = document.getElementById('sName');
const rolleEl = document.getElementById('sRolle');
const questEl = document.getElementById('sQuest');
@@ -1570,32 +1574,32 @@ function addSpieler() {
if (!name) { nameEl.focus(); return; }
const rolle = rolleEl.value;
const questIdx = questEl.value !== '' ? parseInt(questEl.value) : null;
spieler.push({ id: Date.now(), name, rolle, questIdx });
nameEl.value = '';
questEl.value = '';
saveSpieler();
renderSpieler();
const res = await apiPost('/spieler', {name, rolle, quest_id: questIdx});
if (res && res.spieler) {
spieler.push(res.spieler);
nameEl.value = '';
questEl.value = '';
renderSpieler();
}
}
function deleteSpieler(id) {
async function deleteSpieler(id) {
await apiDelete('/spieler/' + id);
spieler = spieler.filter(s => s.id !== id);
saveSpieler();
renderSpieler();
}
function changeRolle(id, rolle) {
async function changeRolle(id, rolle) {
const s = spieler.find(s => s.id === id);
if (s) { s.rolle = rolle; saveSpieler(); renderSpieler(); }
if (s) { s.rolle = rolle; await apiPut('/spieler/' + id, {rolle}); renderSpieler(); }
}
function changeQuest(id, val) {
async function changeQuest(id, val) {
const s = spieler.find(s => s.id === id);
if (s) { s.questIdx = val !== '' ? parseInt(val) : null; saveSpieler(); renderSpieler(); }
if (s) { s.questIdx = val !== '' ? parseInt(val) : null; await apiPut('/spieler/' + id, {quest_id: s.questIdx}); renderSpieler(); }
}
function getInitial(name) {
return name.charAt(0).toUpperCase();
}
function getInitial(name) { return name.charAt(0).toUpperCase(); }
function renderSpieler() {
const list = document.getElementById('spielerList');
@@ -1605,8 +1609,7 @@ function renderSpieler() {
} else {
list.innerHTML = spieler.map(s => {
const questName = s.questIdx !== null ? quests[s.questIdx]?.name : null;
return `
<div class="spieler-item">
return `<div class="spieler-item">
<div class="spieler-avatar">${getInitial(s.name)}</div>
<div class="spieler-info">
<div class="spieler-name-display">${s.name}</div>
@@ -1617,15 +1620,13 @@ function renderSpieler() {
<option value=""> Keine Quest </option>
${quests.map((q,i) => `<option value="${i}" ${s.questIdx===i?'selected':''}>${q.name}</option>`).join('')}
</select>
<select class="spieler-select" style="font-size:0.8rem; padding:0.25rem 0.4rem;" onchange="changeRolle(${s.id}, this.value)">
<select class="spieler-select" style="font-size:0.8rem;padding:0.25rem 0.4rem;" onchange="changeRolle(${s.id}, this.value)">
${Object.entries(rolleLabels).map(([k,v]) => `<option value="${k}" ${s.rolle===k?'selected':''}>${v}</option>`).join('')}
</select>
<button class="btn-del" onclick="deleteSpieler(${s.id})">✕</button>
</div>`;
}).join('');
}
// Summary
const summary = document.getElementById('spielerSummary');
if (summary) {
summary.style.display = spieler.length ? 'flex' : 'none';
@@ -1636,60 +1637,6 @@ function renderSpieler() {
}
}
function saveSpieler() {
try { localStorage.setItem('jga_spieler', JSON.stringify(spieler)); } catch(e) {}
}
function loadSpieler() {
try {
const s = JSON.parse(localStorage.getItem('jga_spieler') || 'null');
if (s) spieler = s;
} catch(e) {}
populateQuestSelect();
renderSpieler();
}
function saveState() {
try {
localStorage.setItem('jga_state', JSON.stringify({ itemCollected, drinks, questDone }));
} catch(e) {}
}
function loadState() {
try {
const s = JSON.parse(localStorage.getItem('jga_state') || 'null');
if (!s) return;
s.itemCollected?.forEach((v, i) => {
itemCollected[i] = v;
const slot = document.getElementById('inv-' + i);
if (slot && v) slot.classList.add('collected');
});
drinks = s.drinks || 0;
document.getElementById('drinkCount').textContent = drinks;
s.questDone?.forEach((v, i) => { questDone[i] = v; });
const notes = localStorage.getItem('jga_notes');
if (notes) document.getElementById('notes').value = notes;
} catch(e) {}
}
function saveNotes() {
try { localStorage.setItem('jga_notes', document.getElementById('notes').value); } catch(e) {}
}
function resetAll() {
if (!confirm('Wirklich alles zurücksetzen?')) return;
itemCollected.fill(false);
questDone.fill(false);
drinks = 0;
document.querySelectorAll('.item-slot').forEach(s => s.classList.remove('collected'));
document.getElementById('drinkCount').textContent = 0;
document.getElementById('notes').value = '';
spieler = [];
renderSpieler();
try { localStorage.clear(); } catch(e) {}
buildQuests();
updateQuestStatus();
}
// Start bei Page Load
document.addEventListener('DOMContentLoaded', startApp);
</script>
</body>
</html>