From ddaf5839a51b55731549b306c879ebad8cbb6c55 Mon Sep 17 00:00:00 2001 From: Clawdia Date: Fri, 8 May 2026 10:47:49 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20der=20letzte=20Auftrag=20-=20Vollst?= =?UTF-8?q?=C3=A4ndiges=20Deployment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Frontend: HTML/CSS/JS Spielapp (1642 Zeilen) - Backend: Node.js/Express + SQLite server - REST API: game, spieler, notes - AI_INSTRUCTIONS.md documentation --- index.html | 1642 ++++++++++++++++++++++++++++++++++++++++++++++ server/server.js | 188 ++++++ 2 files changed, 1830 insertions(+) create mode 100644 index.html create mode 100644 server/server.js diff --git a/index.html b/index.html new file mode 100644 index 0000000..593b403 --- /dev/null +++ b/index.html @@ -0,0 +1,1642 @@ + + + + + + + +Der letzte Auftrag – JGA Abenteuer + + + + + + +
+
+
⚔️
+
Ein Junggesellenabschied der Legenden
+
Der letzte
Auftrag
+
+
+ Eine Heldengeschichte, Trinkspiele & tabletop Quests –
+ für den tapfersten Krieger, der je eine Braut zu retten wagte. +
+ +
+ + +
+ + +
+ + +
+
+
Das Abenteuer
+
Für Spielleiter & Organisatoren
+
✦ ✦ ✦
+ +
+ „Höre mich an, tapferer Held. Deine zukünftige Gemahlin wurde von den mächtigen Hütern ihrer Habseligkeiten gefangen gehalten. Acht Gegenstände von unschätzbarem Wert trägt sie bei sich – oder vielmehr: tragen sie ANDERE für sie. Nur wer alle acht erbeuten kann, verdient es, sie zum Traualtar zu führen." +
+ +

Das Konzept ist simpel: Der Bräutigam ist der Held. Jeder Gast (oder eine Gruppe) hütet ein Besitztum der Braut. Durch Quests und Aufgaben erkämpft er sich die Gegenstände – oder trinkt bei Misserfolg.

+ +
+
Grundregeln
+
    +
  • Der Held löst Aufgaben bei den verschiedenen Gästen (= Hüter).
  • +
  • Erfolg → Hüter übergibt seinen Gegenstand (oder einen Hinweis-Zettel).
  • +
  • Misserfolg → Der Held trinkt. Der Gegenstand bleibt beim Hüter.
  • +
  • Er kann es später nochmal versuchen – mit erhöhtem Schwierigkeitsgrad.
  • +
  • Hat er am Ende ALLE 8 Gegenstände, darf er die Braut symbolisch freikaufen.
  • +
  • Fehlt noch etwas? Er trinkt für jeden fehlenden Gegenstand nochmal.
  • +
  • Einsteiger-Quests brauchen keine Vorkenntnisse. Der Tabletop-Block ist optional für RPG-Enthusiasten.
  • +
+
+ +
+
Was braucht ihr?
+
    +
  • 8 physische Gegenstände (Fotos, kleine Dinge oder Zettel mit Clue-Beschreibungen)
  • +
  • Diese App / Ausgedrucktes für den Spielleiter
  • +
  • Getränke in ausreichender Menge 🍺
  • +
  • Gute Laune & keine Scham
  • +
  • Für den Tabletop-Block: W6, W10 oder diese App als Würfel
  • +
+
+ +
✦ ✦ ✦
+

Der Held weiß am Anfang NUR, dass er Quests lösen muss.
Die Hüter kennen ihre Aufgabe, er nicht.

+
+ +
+
Die 8 Besitztümer
+
Für die Braut vorbereiten
+

Bereite diese 8 Gegenstände (oder Zettel mit ihrer Beschreibung) vor. Die Hüter bekommen sie vorab zugewiesen:

+
+
+ 💍 + Der Ring der Verbindung + +
+
+ 👠 + Der Schuh der Flucht + +
+
+ 💌 + Der Brief des ersten Dates + +
+
+ 🌹 + Die Blume der Ewigkeit + +
+
+ 🔑 + Der Schlüssel zur Zukunft + +
+
+ 📸 + Das Foto der gemeinsamen Zeit + +
+
+ 🧸 + Das Maskottchen des Heils + +
+
+ 🏆 + Die Urkunde der Heldentaten + +
+
+ +
+
+ + +
+
+
Die Quests
+
Aufgaben der Hüter — Klicken zum Aufklappen
+ +
+
Farbcode
+
    +
  • EINSTEIGER Keine Vorkenntnisse
  • +
  • MITTEL Etwas Geschick
  • +
  • HERAUSFORDERUNG Echte Prüfung
  • +
  • TABLETOP RPG-Erfahrene
  • +
+
+ +
+ + +
+
+ + +
+
+
Tabletop-Block
+
Für erfahrene Rollenspieler · W6/W10-System
+ +
+ Dieser Block ist für den Moment gedacht, wenn die erfahrenen Rollenspieler in der Runde zusammenkommen. Eine kurze Kampf- und Entscheidungsszene, vollständig vorbereitet. Dauer: ca. 30–60 Minuten. +
+ +
+
🐉 Szene I – Der Wächter des Tores
+
+ „Der Held steht vor dem letzten Tor zur Festung der Braut. Der Wächter – ein alter Vertrauter – versperrt den Weg. Er verlangt den Beweis, dass der Held seiner Gemahlin würdig ist." +
+

Der Wächter stellt 3 Fragen – echte Fragen über die Beziehung (Spielleiter vorbereiten!). Für jede richtige Antwort: kein Würfelwurf nötig. Für jede falsche: Würfelduell.

+
+
+
W10
+
System
+
+
+
7+
+
Erfolg
+
+
+
3
+
Fragen
+
+
+

Beispielfragen: Wann hatten sie ihr erstes Date? Was ist ihr Lieblingsessen? Was war ihr erster gemeinsamer Urlaub?

+
+ +
+
⚔ Szene II – Die Prüfung des Mutes
+
+ „Der Rat der Weisen (= die erfahrenen Rollenspieler) sitzt im Kreis. Jeder hat eine Herausforderung für den Held: einen Skill-Check, eine moralische Entscheidung oder eine Improvisation." +
+

Jeder RPG-erfahrene Spieler ist ein Ratsmitglied und beschreibt eine kurze Szene. Der Held muss reagieren (Roleplay + Würfelwurf). Das Ratsmitglied entscheidet, ob der Wurf ausreicht.

+
+
Beispiel-Szenen für Ratsmitglieder
+
    +
  • „Du stehst vor einem Drachen, der dich nach ihrem schlimmsten Eigenschaft fragt. Antworte weise." (Charisma-Check)
  • +
  • „Ein alter Rivale erscheint. Er behauptet, ihr erster Kuss war mit ihm." (Einschüchterungs-Check)
  • +
  • „Du musst in 30 Sekunden 3 Dinge nennen, die du an ihr liebst – ohne zu stocken." (Wisdom-Check)
  • +
  • „Der Magier bietet dir: ENTWEDER ihre Schuhe ODER ein ewiges Leben ohne sie. Was wählst du?" (Roleplay – kein Würfelwurf)
  • +
+
+
+ +
+
🎲 Szene III – Der finale Würfelkampf
+
+ „Ein letztes Tor. Dahinter wartet der Wächter der Wächter – der härteste Gegner des Abends. Er fordert den Held zum entscheidenden Duell." +
+

Klassischer Würfelkampf: Beide würfeln W10. Wer höher würfelt, gewinnt die Runde. Beste 3 von 5 Runden. Bei Unentschieden: Stechershot trinkt der Verlierer.

+
+
+
W10
+
Würfel
+
+
+
3/5
+
Best-of
+
+
+
🍺
+
Einsatz
+
+
+

Jede verlorene Runde = ein Schluck. Wer das Duell verliert, ERZÄHLT außerdem einer Anekdote über die Braut.

+
+ +
+
🎲 Würfel-Assistent
+
+
+
+
+
+
+ + +
+
+
Helden-Tracker
+
Live-Übersicht während des Abends
+ +
+
+
0
+
Getrunken 🍺
+
+ + +
+
+
+
0/8
+
Besitztümer ⚔
+
+ Im Intro-Tab verwalten +
+
+
+ +
+
Quest-Status
+
+
+ +
+
Notizen
+ +
+ +
+ +
+
+
+ + +
+
+
Die Gefährten
+
Spieler, Rollen & Hüter-Zuteilung
+ +
+ Trage hier alle Teilnehmer des Abends ein und weise ihnen ihre Rolle zu. So behältst du den Überblick, wer welche Quest hütet. +
+ + +
+
Spieler hinzufügen
+
+ + + + +
+
+ + +
+
Keine Teilnehmer eingetragen.
+
+ + + +
+ + +
+
Rollen-Übersicht
+
✦ ✦ ✦
+
+
    +
  • ⚔ HELD  Der Bräutigam – löst alle Quests.
  • +
  • 📜 SPIELLEITER  Organisator – kennt alle Aufgaben, leitet den Abend.
  • +
  • 🔒 HÜTER  Bewacht ein Besitztum & stellt die zugewiesene Quest.
  • +
  • 🧙 RATSMITGLIED  Erfahrener RPG-Spieler – nimmt am Tabletop-Block teil.
  • +
  • 🌱 EINSTEIGER  Tabletop-Neuling – bekommt einfache Quests.
  • +
  • 🍺 GAST  Schaut zu, feuert an und trinkt mit.
  • +
+
+
+
+ + +
+
+
Ablaufplan
+
Empfohlener Zeitplan für den Abend
+ +
+
+
Beginn · Ankunft
+
Versammlung & Einstimmung
+
Alle Hüter bekommen ihren Gegenstand & ihre Aufgabe erklärt. Der Held weiß noch nichts.
+
+
+
~30 Min. nach Beginn
+
Die Verkündigung 📜
+
Der Spielleiter liest dem Held feierlich die Quest vor (Intro-Text). Dann beginnt das Abenteuer.
+
+
+
Phase 1 · 45-60 Min.
+
Einsteiger-Quests (1–5)
+
Der Held absolviert die ersten Aufgaben und erbeutet die ersten Besitztümer.
+
+
+
Pause · ~Mitte des Abends
+
Inventur & Halftime-Ritual
+
Für jeden fehlenden Gegenstand trinkt der Held einen kleinen Schluck. Dann gehts weiter.
+
+
+
Phase 2 · 45-60 Min.
+
Herausforderungs-Quests (6–8)
+
Anspruchsvollere Aufgaben mit höherem Einsatz.
+
+
+
Optional · nach Phase 2
+
⚔ Tabletop-Block
+
Die RPG-erfahrenen Spieler ziehen sich für 30-60 Min. in die Ratssitzung zurück. Andere trinken weiter.
+
+
+
Finale · Abend-Ende
+
Die Befreiung der Braut 🏆
+
Der Held präsentiert alle Besitztümer. Für jeden fehlenden: Straftrunk. Dann: Feier ohne Quests!
+
+
+ +
+
Hinweise für den Spielleiter
+
    +
  • Hüter vorab briefen! Aufgabe kennen, Gegenstand bereit haben.
  • +
  • Zweite Chance immer erlauben – aber mit Extratrinken.
  • +
  • Atmosphäre hat Vorrang vor strikter Regelauslegung.
  • +
  • Wasser & Snacks bereitstellen, wenn es länger geht.
  • +
+
+
+
+ +
+
+ + diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..609b8fe --- /dev/null +++ b/server/server.js @@ -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}`); +});