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:
+208
-261
@@ -1256,165 +1256,147 @@
|
|||||||
</div><!-- /app -->
|
</div><!-- /app -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// DATA
|
// PART 1: API-HELPERS & STATE-SYNC
|
||||||
// ══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
const quests = [
|
const API = '/api';
|
||||||
{
|
|
||||||
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. 1–2 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 (2–3 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.`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
|
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);
|
const itemCollected = new Array(8).fill(false);
|
||||||
let drinks = 0;
|
let drinks = 0;
|
||||||
const questDone = new Array(quests.length).fill(false);
|
const questDone = new Array(8).fill(false);
|
||||||
|
|
||||||
// ══════════════════════════════════════════
|
// State vom Server laden
|
||||||
// STARS
|
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() {
|
(function createStars() {
|
||||||
const c = document.getElementById('stars');
|
const c = document.getElementById('stars');
|
||||||
|
if (!c) return;
|
||||||
for (let i = 0; i < 120; i++) {
|
for (let i = 0; i < 120; i++) {
|
||||||
const s = document.createElement('div');
|
const s = document.createElement('div');
|
||||||
s.className = 'star';
|
s.className = 'star';
|
||||||
s.style.cssText = `
|
s.style.cssText = `left:${Math.random()*100}%;top:${Math.random()*100}%;
|
||||||
left:${Math.random()*100}%;
|
--d:${2+Math.random()*4}s;--delay:${Math.random()*5}s;
|
||||||
top:${Math.random()*100}%;
|
--bright:${0.4+Math.random()*0.6};width:${Math.random()<0.3?3:2}px;height:${Math.random()<0.3?3:2}px;`;
|
||||||
--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);
|
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) {
|
function showTab(id) {
|
||||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||||
document.querySelectorAll('.nav-tab').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');
|
event.target.classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════
|
function startApp() {
|
||||||
// QUESTS
|
document.getElementById('cover').style.display = 'none';
|
||||||
// ══════════════════════════════════════════
|
document.getElementById('app').style.display = 'block';
|
||||||
|
buildQuests();
|
||||||
|
buildDice();
|
||||||
|
syncState();
|
||||||
|
syncSpieler();
|
||||||
|
updateQuestStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// PART 4: QUEST-FUNKTIONEN
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
function buildQuests() {
|
function buildQuests() {
|
||||||
const grid = document.getElementById('questGrid');
|
const grid = document.getElementById('questGrid');
|
||||||
|
if (!grid) return;
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
quests.forEach((q, i) => {
|
quests.forEach((q, i) => {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
@@ -1443,12 +1436,12 @@ function buildQuests() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="quest-expand">${q.details}<br><br>
|
<div class="quest-expand">${q.details}<br><br>
|
||||||
<button onclick="event.stopPropagation(); toggleDone(${i})"
|
<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;">
|
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'}
|
${questDone[i] ? '↩ Als offen markieren' : '✓ Als erledigt markieren'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="expand-hint">▼ Aufklappen für Details</div>
|
<div class="expand-hint">▼ Aufklappen für Details</div>`;
|
||||||
`;
|
|
||||||
card.addEventListener('click', () => toggleExpand(i));
|
card.addEventListener('click', () => toggleExpand(i));
|
||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
});
|
});
|
||||||
@@ -1456,57 +1449,61 @@ function buildQuests() {
|
|||||||
|
|
||||||
function toggleExpand(i) {
|
function toggleExpand(i) {
|
||||||
const card = document.getElementById('qcard-' + 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];
|
questDone[i] = !questDone[i];
|
||||||
saveState();
|
const sv = await apiGet('/game');
|
||||||
|
if (!sv) return;
|
||||||
|
const s = {...sv.state, questDone: [...questDone]};
|
||||||
|
await updateAndSync(s);
|
||||||
buildQuests();
|
buildQuests();
|
||||||
updateQuestStatus();
|
updateQuestStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// INVENTORY
|
// PART 5: INVENTORY & DRINKS
|
||||||
// ══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
function toggleItem(i) {
|
async function toggleItem(i) {
|
||||||
itemCollected[i] = !itemCollected[i];
|
itemCollected[i] = !itemCollected[i];
|
||||||
const slot = document.getElementById('inv-' + i);
|
const slot = document.getElementById('inv-' + i);
|
||||||
slot.classList.toggle('collected', itemCollected[i]);
|
if (slot) slot.classList.toggle('collected', itemCollected[i]);
|
||||||
saveState();
|
const sv = await apiGet('/game');
|
||||||
|
if (!sv) return;
|
||||||
|
const s = {...sv.state, itemCollected: [...itemCollected]};
|
||||||
|
await updateAndSync(s);
|
||||||
updateQuestStatus();
|
updateQuestStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════
|
async function changeDrinks(n) {
|
||||||
// DRINKS
|
const res = await apiPost('/game/drink', {amount: n});
|
||||||
// ══════════════════════════════════════════
|
if (res && res.drinks !== undefined) {
|
||||||
function changeDrinks(n) {
|
drinks = res.drinks;
|
||||||
drinks = Math.max(0, drinks + n);
|
document.getElementById('drinkCount').textContent = drinks;
|
||||||
document.getElementById('drinkCount').textContent = drinks;
|
}
|
||||||
saveState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// STATUS
|
// PART 6: STATUS
|
||||||
// ══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
function updateQuestStatus() {
|
function updateQuestStatus() {
|
||||||
const count = itemCollected.filter(Boolean).length;
|
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');
|
const list = document.getElementById('questStatusList');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
list.innerHTML = quests.map((q, i) => `
|
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>${questDone[i] ? '✓' : '○'}</span>
|
||||||
<span>${q.name}</span>
|
<span>${q.name}</span>
|
||||||
<span style="margin-left:auto; font-size:0.8rem; opacity:0.7">${q.item}</span>
|
<span style="margin-left:auto;font-size:0.8rem;opacity:0.7">${q.item}</span>
|
||||||
</div>
|
</div>`).join('');
|
||||||
`).join('');
|
|
||||||
}
|
}
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
// ══════════════════════════════════════════
|
// PART 7: WÜRFEL
|
||||||
// DICE
|
// ═══════════════════════════════════════════
|
||||||
// ══════════════════════════════════════════
|
|
||||||
function buildDice() {
|
function buildDice() {
|
||||||
const btn = document.getElementById('diceBtns');
|
const btn = document.getElementById('diceBtns');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
@@ -1523,6 +1520,7 @@ function rollDice(sides) {
|
|||||||
const result = Math.floor(Math.random() * sides) + 1;
|
const result = Math.floor(Math.random() * sides) + 1;
|
||||||
const el = document.getElementById('diceResult');
|
const el = document.getElementById('diceResult');
|
||||||
const log = document.getElementById('diceLog');
|
const log = document.getElementById('diceLog');
|
||||||
|
if (!el || !log) return;
|
||||||
el.classList.remove('rolling');
|
el.classList.remove('rolling');
|
||||||
void el.offsetWidth;
|
void el.offsetWidth;
|
||||||
el.classList.add('rolling');
|
el.classList.add('rolling');
|
||||||
@@ -1533,10 +1531,9 @@ function rollDice(sides) {
|
|||||||
result >= Math.ceil(sides * 0.4) ? '~ Mittelmaß...' : '✗ Schwach. Trinken?';
|
result >= Math.ceil(sides * 0.4) ? '~ Mittelmaß...' : '✗ Schwach. Trinken?';
|
||||||
log.textContent = `W${sides} → ${result} · ${reactions}`;
|
log.textContent = `W${sides} → ${result} · ${reactions}`;
|
||||||
}
|
}
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
// ══════════════════════════════════════════
|
// PART 8: SPIELER (API-Version)
|
||||||
// SPIELER
|
// ═══════════════════════════════════════════
|
||||||
// ══════════════════════════════════════════
|
|
||||||
let spieler = [];
|
let spieler = [];
|
||||||
|
|
||||||
const rolleLabels = {
|
const rolleLabels = {
|
||||||
@@ -1548,6 +1545,13 @@ const rolleLabels = {
|
|||||||
gast: '🍺 Gast',
|
gast: '🍺 Gast',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function syncSpieler() {
|
||||||
|
const data = await apiGet('/spieler');
|
||||||
|
if (data) spieler = data;
|
||||||
|
populateQuestSelect();
|
||||||
|
renderSpieler();
|
||||||
|
}
|
||||||
|
|
||||||
function populateQuestSelect() {
|
function populateQuestSelect() {
|
||||||
const sel = document.getElementById('sQuest');
|
const sel = document.getElementById('sQuest');
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
@@ -1562,7 +1566,7 @@ function populateQuestSelect() {
|
|||||||
sel.value = current;
|
sel.value = current;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSpieler() {
|
async function addSpieler() {
|
||||||
const nameEl = document.getElementById('sName');
|
const nameEl = document.getElementById('sName');
|
||||||
const rolleEl = document.getElementById('sRolle');
|
const rolleEl = document.getElementById('sRolle');
|
||||||
const questEl = document.getElementById('sQuest');
|
const questEl = document.getElementById('sQuest');
|
||||||
@@ -1570,32 +1574,32 @@ function addSpieler() {
|
|||||||
if (!name) { nameEl.focus(); return; }
|
if (!name) { nameEl.focus(); return; }
|
||||||
const rolle = rolleEl.value;
|
const rolle = rolleEl.value;
|
||||||
const questIdx = questEl.value !== '' ? parseInt(questEl.value) : null;
|
const questIdx = questEl.value !== '' ? parseInt(questEl.value) : null;
|
||||||
spieler.push({ id: Date.now(), name, rolle, questIdx });
|
const res = await apiPost('/spieler', {name, rolle, quest_id: questIdx});
|
||||||
nameEl.value = '';
|
if (res && res.spieler) {
|
||||||
questEl.value = '';
|
spieler.push(res.spieler);
|
||||||
saveSpieler();
|
nameEl.value = '';
|
||||||
renderSpieler();
|
questEl.value = '';
|
||||||
|
renderSpieler();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSpieler(id) {
|
async function deleteSpieler(id) {
|
||||||
|
await apiDelete('/spieler/' + id);
|
||||||
spieler = spieler.filter(s => s.id !== id);
|
spieler = spieler.filter(s => s.id !== id);
|
||||||
saveSpieler();
|
|
||||||
renderSpieler();
|
renderSpieler();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeRolle(id, rolle) {
|
async function changeRolle(id, rolle) {
|
||||||
const s = spieler.find(s => s.id === id);
|
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);
|
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) {
|
function getInitial(name) { return name.charAt(0).toUpperCase(); }
|
||||||
return name.charAt(0).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSpieler() {
|
function renderSpieler() {
|
||||||
const list = document.getElementById('spielerList');
|
const list = document.getElementById('spielerList');
|
||||||
@@ -1605,8 +1609,7 @@ function renderSpieler() {
|
|||||||
} else {
|
} else {
|
||||||
list.innerHTML = spieler.map(s => {
|
list.innerHTML = spieler.map(s => {
|
||||||
const questName = s.questIdx !== null ? quests[s.questIdx]?.name : null;
|
const questName = s.questIdx !== null ? quests[s.questIdx]?.name : null;
|
||||||
return `
|
return `<div class="spieler-item">
|
||||||
<div class="spieler-item">
|
|
||||||
<div class="spieler-avatar">${getInitial(s.name)}</div>
|
<div class="spieler-avatar">${getInitial(s.name)}</div>
|
||||||
<div class="spieler-info">
|
<div class="spieler-info">
|
||||||
<div class="spieler-name-display">${s.name}</div>
|
<div class="spieler-name-display">${s.name}</div>
|
||||||
@@ -1617,15 +1620,13 @@ function renderSpieler() {
|
|||||||
<option value="">– Keine Quest –</option>
|
<option value="">– Keine Quest –</option>
|
||||||
${quests.map((q,i) => `<option value="${i}" ${s.questIdx===i?'selected':''}>${q.name}</option>`).join('')}
|
${quests.map((q,i) => `<option value="${i}" ${s.questIdx===i?'selected':''}>${q.name}</option>`).join('')}
|
||||||
</select>
|
</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('')}
|
${Object.entries(rolleLabels).map(([k,v]) => `<option value="${k}" ${s.rolle===k?'selected':''}>${v}</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
<button class="btn-del" onclick="deleteSpieler(${s.id})">✕</button>
|
<button class="btn-del" onclick="deleteSpieler(${s.id})">✕</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary
|
|
||||||
const summary = document.getElementById('spielerSummary');
|
const summary = document.getElementById('spielerSummary');
|
||||||
if (summary) {
|
if (summary) {
|
||||||
summary.style.display = spieler.length ? 'flex' : 'none';
|
summary.style.display = spieler.length ? 'flex' : 'none';
|
||||||
@@ -1636,60 +1637,6 @@ function renderSpieler() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSpieler() {
|
// Start bei Page Load
|
||||||
try { localStorage.setItem('jga_spieler', JSON.stringify(spieler)); } catch(e) {}
|
document.addEventListener('DOMContentLoaded', startApp);
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.static(path.join(__dirname, '../')));
|
||||||
|
|
||||||
|
const DB_PATH = path.join(__dirname, 'data.db');
|
||||||
|
|
||||||
|
function getDb() {
|
||||||
|
const db = new Database(DB_PATH);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS state (
|
||||||
|
key TEXT PRIMARY KEY, value TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS spieler (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT, rolle TEXT, quest_id INTEGER,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS notes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
body TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS drinks_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
amount INTEGER, note TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = getDb();
|
||||||
|
|
||||||
|
function initState() {
|
||||||
|
const row = db.prepare("SELECT value FROM state WHERE key = 'game'").get();
|
||||||
|
if (!row) {
|
||||||
|
const s = {
|
||||||
|
gameName: '', gameStarted: false, drinks: 0,
|
||||||
|
items: [false,false,false,false,false,false,false,false],
|
||||||
|
quests: [false,false,false,false,false,false,false,false],
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
db.prepare('INSERT OR REPLACE INTO state (key,value) VALUES (?,?)').run('game', JSON.stringify(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initState();
|
||||||
|
|
||||||
|
// GET state
|
||||||
|
app.get('/api/game', (req, res) => {
|
||||||
|
initState();
|
||||||
|
const sv = db.prepare("SELECT value FROM state WHERE key='game'").get();
|
||||||
|
res.json({
|
||||||
|
state: JSON.parse(sv.value),
|
||||||
|
spieler: db.prepare('SELECT * FROM spieler ORDER BY id').all(),
|
||||||
|
notes: db.prepare('SELECT * FROM notes ORDER BY created_at DESC').all(),
|
||||||
|
stats: db.prepare('SELECT SUM(amount) as total, COUNT(*) as entries FROM drinks_log').get() || {total:0,entries:0}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start
|
||||||
|
app.post('/api/game/start', (req, res) => {
|
||||||
|
initState();
|
||||||
|
const {name} = req.body;
|
||||||
|
const sv = db.prepare("SELECT value FROM state WHERE key='game'").get();
|
||||||
|
const s = JSON.parse(sv.value);
|
||||||
|
s.gameName = name || 'Der letzte Auftrag';
|
||||||
|
s.gameStarted = true; s.timestamp = Date.now();
|
||||||
|
db.prepare("UPDATE state SET value=? WHERE key='game'").run(JSON.stringify(s));
|
||||||
|
res.json({success:true, state:s});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drink
|
||||||
|
app.post('/api/game/drink', (req, res) => {
|
||||||
|
initState();
|
||||||
|
const amt = req.body.amount || 1;
|
||||||
|
const sv = db.prepare("SELECT value FROM state WHERE key='game'").get();
|
||||||
|
const s = JSON.parse(sv.value);
|
||||||
|
s.drinks = Math.max(0, s.drinks + amt);
|
||||||
|
s.timestamp = Date.now();
|
||||||
|
db.prepare("UPDATE state SET value=? WHERE key='game'").run(JSON.stringify(s));
|
||||||
|
db.prepare('INSERT INTO drinks_log (amount) VALUES (?)').run(amt);
|
||||||
|
res.json({success:true, drinks:s.drinks});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset drink
|
||||||
|
app.post('/api/game/drink/reset', (req, res) => {
|
||||||
|
initState();
|
||||||
|
const sv = db.prepare("SELECT value FROM state WHERE key='game'").get();
|
||||||
|
const s = JSON.parse(sv.value);
|
||||||
|
s.drinks = 0; s.timestamp = Date.now();
|
||||||
|
db.prepare("UPDATE state SET value=? WHERE key='game'").run(JSON.stringify(s));
|
||||||
|
res.json({success:true, drinks:0});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle item
|
||||||
|
app.post('/api/game/toggle-item', (req, res) => {
|
||||||
|
initState();
|
||||||
|
const idx = parseInt(req.body.index);
|
||||||
|
if (idx<0||idx>7) return res.status(400).json({error:'Index 0-7'});
|
||||||
|
const sv = db.prepare("SELECT value FROM state WHERE key='game'").get();
|
||||||
|
const s = JSON.parse(sv.value);
|
||||||
|
s.items[idx] = !s.items[idx];
|
||||||
|
s.timestamp = Date.now();
|
||||||
|
db.prepare("UPDATE state SET value=? WHERE key='game'").run(JSON.stringify(s));
|
||||||
|
res.json({success:true, state:s, collected:s.items.filter(Boolean).length});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle quest
|
||||||
|
app.post('/api/game/toggle-quest', (req, res) => {
|
||||||
|
initState();
|
||||||
|
const idx = parseInt(req.body.index);
|
||||||
|
if (idx<0||idx>7) return res.status(400).json({error:'Index 0-7'});
|
||||||
|
const sv = db.prepare("SELECT value FROM state WHERE key='game'").get();
|
||||||
|
const s = JSON.parse(sv.value);
|
||||||
|
s.quests[idx] = !s.quests[idx];
|
||||||
|
s.timestamp = Date.now();
|
||||||
|
db.prepare("UPDATE state SET value=? WHERE key='game'").run(JSON.stringify(s));
|
||||||
|
res.json({success:true, state:s});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save full state
|
||||||
|
app.post('/api/game/save-state', (req, res) => {
|
||||||
|
initState();
|
||||||
|
const s = req.body; s.timestamp = Date.now();
|
||||||
|
db.prepare("UPDATE state SET value=? WHERE key='game'").run(JSON.stringify(s));
|
||||||
|
res.json({success:true, state:s});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset all
|
||||||
|
app.post('/api/game/reset', (req, res) => {
|
||||||
|
initState();
|
||||||
|
const sv = db.prepare("SELECT value FROM state WHERE key='game'").get();
|
||||||
|
const s = JSON.parse(sv.value);
|
||||||
|
s.drinks=0; s.items=[false,false,false,false,false,false,false,false];
|
||||||
|
s.quests=[false,false,false,false,false,false,false,false];
|
||||||
|
s.gameStarted=false; s.gameName=''; s.timestamp=Date.now();
|
||||||
|
db.prepare("UPDATE state SET value=? WHERE key='game'").run(JSON.stringify(s));
|
||||||
|
db.prepare('DELETE FROM notes').run();
|
||||||
|
db.prepare('DELETE FROM drinks_log').run();
|
||||||
|
res.json({success:true, state:s});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spieler CRUD
|
||||||
|
app.get('/api/spieler', (req, res) => res.json(db.prepare('SELECT * FROM spieler ORDER BY id').all()));
|
||||||
|
app.post('/api/spieler', (req, res) => {
|
||||||
|
const {name, rolle, quest_id} = req.body;
|
||||||
|
const r = db.prepare('INSERT INTO spieler (name,rolle,quest_id) VALUES (?,?,?)').run(name, rolle, quest_id||null);
|
||||||
|
res.json({success:true, spieler:db.prepare('SELECT * FROM spieler WHERE id=?').get(r.lastInsertRowid)});
|
||||||
|
});
|
||||||
|
app.put('/api/spieler/:id', (req, res) => {
|
||||||
|
const {name, rolle, quest_id} = req.body;
|
||||||
|
db.prepare('UPDATE spieler SET name=COALESCE(?,name),rolle=COALESCE(?,rolle),quest_id=COALESCE(?,quest_id) WHERE id=?')
|
||||||
|
.run(name||null, rolle||null, quest_id||null, req.params.id);
|
||||||
|
res.json({success:true, spieler:db.prepare('SELECT * FROM spieler WHERE id=?').get(req.params.id)});
|
||||||
|
});
|
||||||
|
app.delete('/api/spieler/:id', (req, res) => {
|
||||||
|
db.prepare('DELETE FROM spieler WHERE id=?').run(req.params.id);
|
||||||
|
res.json({success:true});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notes
|
||||||
|
app.post('/api/notes', (req, res) => {
|
||||||
|
const {body} = req.body;
|
||||||
|
const r = db.prepare('INSERT INTO notes (body) VALUES (?)').run(body);
|
||||||
|
res.json({success:true, note:db.prepare('SELECT * FROM notes WHERE id=?').get(r.lastInsertRowid)});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
app.get('/api/game/stats', (req, res) => {
|
||||||
|
initState();
|
||||||
|
const sv = db.prepare("SELECT value FROM state WHERE key='game'").get();
|
||||||
|
const s = JSON.parse(sv.value);
|
||||||
|
res.json({
|
||||||
|
collected: s.items.filter(Boolean).length, total:8,
|
||||||
|
questsDone: s.quests.filter(Boolean).length, questsTotal:8,
|
||||||
|
drinks: s.drinks, gameName: s.gameName,
|
||||||
|
spielerCount: db.prepare('SELECT COUNT(*) as c FROM spieler').get().c,
|
||||||
|
spieler: db.prepare('SELECT * FROM spieler').all()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 8082;
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`🏰 Der letzte Auftrag server on port ${PORT}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user