Files
der_letzte_auftrag/index.html
T
Clawdia ddaf5839a5 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
2026-05-08 10:49:05 +02:00

1643 lines
58 KiB
HTML
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.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light only">
<meta name="supported-color-schemes" content="light">
<title>Der letzte Auftrag JGA Abenteuer</title>
<link href="https://fonts.googleapis.com/css2?family=MedievalSharp&family=Cinzel+Decorative:wght@700&family=Cinzel:wght@400;600&family=Crimson+Pro:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
<style>
:root {
--parchment: #f4e8c1;
--parchment-dark: #e8d5a0;
--ink: #1a0f00;
--ink-light: #3d2200;
--red: #8b1a1a;
--gold: #c9a227;
--gold-light: #f0c94a;
--green: #1a4a1a;
--blue: #1a2a5a;
--purple: #4a1a6a;
--shadow: rgba(0,0,0,0.4);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
/* Force light mode verhindert iOS/Koder Dark Mode Override */
:root { color-scheme: light only; }
@media (prefers-color-scheme: dark) {
body { background: #0d0800 !important; }
.parchment { background: #f4e8c1 !important; }
.parchment p, .parchment li, .parchment div { color: #3d2200 !important; }
.quest-card { background: #e8d5a0 !important; }
.rules-box { background: rgba(138,101,32,0.15) !important; }
input, textarea { background: #f4e8c1 !important; color: #1a0f00 !important; }
}
body {
background: #0d0800;
font-family: 'Crimson Pro', Georgia, serif;
color: #1a0f00;
min-height: 100vh;
background-image:
radial-gradient(ellipse at 50% 0%, #2a1500 0%, #0d0800 60%);
}
/* ── COVER ─────────────────────────────────── */
#cover {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
position: relative;
overflow: hidden;
}
#cover::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at 50% 40%, rgba(201,162,39,0.15) 0%, transparent 70%);
pointer-events: none;
}
.stars {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.star {
position: absolute;
width: 2px; height: 2px;
background: #fff;
border-radius: 50%;
animation: twinkle var(--d, 3s) ease-in-out infinite var(--delay, 0s);
opacity: 0;
}
@keyframes twinkle {
0%,100% { opacity: 0; }
50% { opacity: var(--bright, 0.8); }
}
.cover-emblem {
font-size: 5rem;
margin-bottom: 1rem;
filter: drop-shadow(0 0 20px rgba(201,162,39,0.6));
animation: float 4s ease-in-out infinite;
}
@keyframes float {
0%,100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.cover-subtitle {
font-family: 'Cinzel', serif;
font-size: 0.9rem;
letter-spacing: 0.4em;
color: var(--gold);
text-transform: uppercase;
margin-bottom: 0.8rem;
}
.cover-title {
font-family: 'Cinzel Decorative', serif;
font-size: clamp(2rem, 6vw, 4.5rem);
color: var(--gold-light);
text-shadow:
0 0 30px rgba(201,162,39,0.8),
2px 2px 0 #5a3a00,
4px 4px 0 #3a2000;
line-height: 1.15;
margin-bottom: 1.5rem;
}
.cover-divider {
width: 300px;
height: 2px;
background: linear-gradient(90deg, transparent, var(--gold), transparent);
margin: 1.5rem auto;
position: relative;
}
.cover-divider::before, .cover-divider::after {
content: '◆';
position: absolute;
top: 50%;
transform: translateY(-50%);
color: var(--gold);
font-size: 1rem;
}
.cover-divider::before { left: -10px; }
.cover-divider::after { right: -10px; }
.cover-desc {
color: #c8a86a;
font-size: 1.15rem;
font-style: italic;
max-width: 500px;
line-height: 1.7;
margin-bottom: 2.5rem;
}
.btn-primary {
font-family: 'Cinzel', serif;
font-size: 1rem;
letter-spacing: 0.15em;
color: var(--ink);
background: linear-gradient(135deg, var(--gold-light), var(--gold));
border: none;
padding: 0.9rem 2.5rem;
cursor: pointer;
clip-path: polygon(8px 0%, 100% 0%, calc(100% - 8px) 100%, 0% 100%);
transition: all 0.2s;
font-weight: 600;
box-shadow: 0 4px 15px rgba(201,162,39,0.4);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 25px rgba(201,162,39,0.6);
background: linear-gradient(135deg, #ffe070, var(--gold-light));
}
/* ── MAIN APP ───────────────────────────────── */
#app { display: none; }
.page-wrap {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1rem 4rem;
background: #f0e4b8;
min-height: 100vh;
}
/* ── NAV ───────────────────────────────────── */
.top-nav {
background: linear-gradient(90deg, #1a0800, #2d1500, #1a0800);
border-bottom: 2px solid var(--gold);
padding: 0.8rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
flex-wrap: wrap;
gap: 0.5rem;
}
.nav-title {
font-family: 'Cinzel', serif;
color: var(--gold);
font-size: 1rem;
letter-spacing: 0.1em;
}
.nav-tabs {
display: flex;
gap: 0.3rem;
flex-wrap: wrap;
}
.nav-tab {
font-family: 'Cinzel', serif;
font-size: 0.75rem;
letter-spacing: 0.08em;
color: #a07840;
background: transparent;
border: 1px solid #4a2800;
padding: 0.35rem 0.8rem;
cursor: pointer;
transition: all 0.2s;
}
.nav-tab:hover, .nav-tab.active {
color: var(--gold-light);
border-color: var(--gold);
background: rgba(201,162,39,0.1);
}
/* ── PARCHMENT SECTIONS ────────────────────── */
.parchment {
background: #f4e8c1 !important;
border: 3px solid #8a6520;
border-radius: 4px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow:
0 0 0 1px #c9a227,
0 8px 30px rgba(0,0,0,0.5),
inset 0 1px 0 rgba(255,255,255,0.3);
position: relative;
}
.parchment::before {
content: '';
position: absolute;
inset: 6px;
border: 1px solid rgba(138,101,32,0.3);
pointer-events: none;
}
.section-title {
font-family: 'Cinzel Decorative', serif;
font-size: 1.4rem;
color: var(--red);
text-align: center;
margin-bottom: 0.5rem;
text-shadow: 1px 1px 0 rgba(0,0,0,0.1);
}
.section-subtitle {
font-family: 'Cinzel', serif;
font-size: 0.8rem;
letter-spacing: 0.2em;
color: #8a6520;
text-align: center;
text-transform: uppercase;
margin-bottom: 1.5rem;
}
.ornament {
text-align: center;
color: var(--gold);
font-size: 1.2rem;
letter-spacing: 0.5em;
margin: 1rem 0;
opacity: 0.7;
}
p {
font-size: 1.05rem;
line-height: 1.8;
margin-bottom: 0.8rem;
color: var(--ink-light);
}
.flavor-text {
font-style: italic;
background: rgba(138,101,32,0.08);
border-left: 3px solid var(--gold);
padding: 0.8rem 1rem;
margin: 1rem 0;
border-radius: 0 4px 4px 0;
font-size: 1rem;
}
/* ── QUEST CARDS ───────────────────────────── */
.quest-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.2rem;
margin-top: 1.5rem;
}
.quest-card {
background: #e8d5a0;
border: 2px solid #8a6520;
border-radius: 4px;
padding: 1.2rem;
position: relative;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.quest-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
}
.quest-card.done {
opacity: 0.6;
filter: saturate(0.4);
}
.quest-card.done::after {
content: '✓ ERLEDIGT';
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%,-50%) rotate(-15deg);
font-family: 'Cinzel', serif;
font-size: 1.5rem;
color: var(--green);
border: 3px solid var(--green);
padding: 0.3rem 0.8rem;
opacity: 0.8;
white-space: nowrap;
}
.quest-badge {
display: inline-block;
font-family: 'Cinzel', serif;
font-size: 0.65rem;
letter-spacing: 0.1em;
padding: 0.2rem 0.6rem;
border-radius: 2px;
margin-bottom: 0.5rem;
font-weight: 600;
}
.badge-easy { background: #2a5a1a; color: #a8ff80; }
.badge-medium { background: #5a4a00; color: #ffd060; }
.badge-hard { background: #5a1a00; color: #ff8060; }
.badge-tabletop { background: #2a1a5a; color: #a080ff; }
.quest-name {
font-family: 'Cinzel', serif;
font-size: 1rem;
color: var(--red);
margin-bottom: 0.4rem;
font-weight: 600;
}
.quest-holder {
font-size: 0.85rem;
color: #5a3a10;
margin-bottom: 0.5rem;
}
.quest-holder strong { color: var(--ink); }
.quest-desc {
font-size: 0.9rem;
line-height: 1.5;
color: var(--ink-light);
margin-bottom: 0.8rem;
}
.quest-rewards {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
border-top: 1px solid #c9a22760;
padding-top: 0.6rem;
margin-top: 0.5rem;
}
.reward-success {
color: var(--green);
font-weight: 600;
}
.reward-fail {
color: var(--red);
font-weight: 600;
}
.quest-expand {
display: none;
margin-top: 0.8rem;
padding-top: 0.8rem;
border-top: 1px dashed #c9a22780;
font-size: 0.9rem;
line-height: 1.6;
color: var(--ink);
}
.quest-card.expanded .quest-expand { display: block; }
.expand-hint {
font-family: 'Cinzel', serif;
font-size: 0.7rem;
color: #8a6520;
text-align: right;
margin-top: 0.5rem;
}
/* ── INVENTORY TRACKER ─────────────────────── */
.inventory-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.item-slot {
border: 2px dashed #8a6520;
border-radius: 4px;
padding: 1rem;
text-align: center;
min-height: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.4rem;
transition: all 0.3s;
cursor: pointer;
position: relative;
background: rgba(138,101,32,0.05);
}
.item-slot.collected {
border: 2px solid var(--gold);
background: rgba(201,162,39,0.1);
box-shadow: 0 0 10px rgba(201,162,39,0.3);
}
.item-emoji { font-size: 2.5rem; }
.item-name {
font-family: 'Cinzel', serif;
font-size: 0.75rem;
color: var(--ink-light);
line-height: 1.3;
}
.item-slot.collected .item-name { color: var(--gold); }
.item-check {
position: absolute;
top: 5px; right: 8px;
font-size: 1rem;
color: var(--gold-light);
opacity: 0;
transition: opacity 0.3s;
}
.item-slot.collected .item-check { opacity: 1; }
/* ── DRINKS TRACKER ────────────────────────── */
.drink-tracker {
text-align: center;
padding: 1rem;
}
.drink-count {
font-family: 'Cinzel Decorative', serif;
font-size: 4rem;
color: var(--red);
text-shadow: 2px 2px 0 rgba(0,0,0,0.2);
line-height: 1;
}
.drink-label {
font-family: 'Cinzel', serif;
font-size: 0.8rem;
letter-spacing: 0.3em;
color: #8a6520;
margin-top: 0.3rem;
text-transform: uppercase;
}
.drink-btns {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1.2rem;
}
.btn-drink {
font-family: 'Cinzel', serif;
font-size: 1.1rem;
border: 2px solid;
padding: 0.5rem 1.5rem;
cursor: pointer;
transition: all 0.2s;
background: transparent;
border-radius: 3px;
}
.btn-drink.add { border-color: var(--red); color: var(--red); }
.btn-drink.add:hover { background: var(--red); color: #fff; }
.btn-drink.sub { border-color: #8a6520; color: #8a6520; }
.btn-drink.sub:hover { background: #8a6520; color: #fff; }
/* ── TABLETOP SECTION ──────────────────────── */
.encounter-box {
background: #0d0500;
border: 2px solid var(--gold);
border-radius: 4px;
padding: 1.5rem;
margin: 1rem 0;
color: #e8d5a0;
position: relative;
overflow: hidden;
}
.encounter-box::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at 50% 0%, rgba(201,162,39,0.08), transparent 60%);
pointer-events: none;
}
.encounter-title {
font-family: 'Cinzel', serif;
font-size: 1.1rem;
color: var(--gold-light);
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.encounter-box p { color: #c8a86a; }
.encounter-box .flavor-text {
border-left-color: var(--gold);
background: rgba(201,162,39,0.05);
color: #e8d5a0;
}
.stat-block {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin: 1rem 0;
background: #1a0800;
border: 1px solid #4a2800;
padding: 0.8rem;
border-radius: 4px;
}
.stat-item {
text-align: center;
font-family: 'Cinzel', serif;
}
.stat-val {
font-size: 1.4rem;
color: var(--gold-light);
}
.stat-name {
font-size: 0.6rem;
letter-spacing: 0.15em;
color: #8a6520;
text-transform: uppercase;
}
/* ── DICE ROLLER ───────────────────────────── */
.dice-roller {
background: #1a0800;
border: 1px solid #4a2800;
border-radius: 4px;
padding: 1rem;
margin-top: 1rem;
}
.dice-roller-title {
font-family: 'Cinzel', serif;
font-size: 0.8rem;
letter-spacing: 0.2em;
color: var(--gold);
text-transform: uppercase;
margin-bottom: 0.8rem;
}
.dice-btns {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.8rem;
}
.dice-btn {
font-family: 'Cinzel', serif;
font-size: 0.85rem;
color: #c8a86a;
background: #2a1000;
border: 1px solid #4a2800;
padding: 0.4rem 0.8rem;
cursor: pointer;
border-radius: 3px;
transition: all 0.2s;
}
.dice-btn:hover {
border-color: var(--gold);
color: var(--gold-light);
background: #3a1800;
}
.dice-result {
font-family: 'Cinzel Decorative', serif;
font-size: 2.5rem;
color: var(--gold-light);
text-align: center;
min-height: 3rem;
display: flex;
align-items: center;
justify-content: center;
text-shadow: 0 0 15px rgba(201,162,39,0.5);
transition: all 0.3s;
}
.dice-result.rolling {
animation: diceRoll 0.4s ease-out;
}
@keyframes diceRoll {
0%,20%,40%,60%,80% { transform: rotate(-5deg) scale(1.1); }
10%,30%,50%,70%,90% { transform: rotate(5deg) scale(1.1); }
100% { transform: none; }
}
.dice-log {
font-size: 0.8rem;
color: #5a3a10;
min-height: 1.2rem;
text-align: center;
font-style: italic;
}
/* ── TIMELINE ──────────────────────────────── */
.timeline {
position: relative;
padding-left: 2rem;
margin-top: 1rem;
}
.timeline::before {
content: '';
position: absolute;
left: 0.5rem;
top: 0; bottom: 0;
width: 2px;
background: linear-gradient(180deg, var(--gold), var(--gold) 80%, transparent);
}
.timeline-item {
position: relative;
margin-bottom: 1.5rem;
}
.timeline-item::before {
content: '◆';
position: absolute;
left: -1.7rem;
color: var(--gold);
font-size: 0.8rem;
top: 0.1rem;
}
.timeline-time {
font-family: 'Cinzel', serif;
font-size: 0.7rem;
letter-spacing: 0.15em;
color: var(--gold);
text-transform: uppercase;
margin-bottom: 0.2rem;
}
.timeline-event {
font-size: 1rem;
color: var(--ink-light);
font-weight: 600;
}
.timeline-note {
font-size: 0.9rem;
color: #5a3a10;
font-style: italic;
}
/* ── RULES BOX ─────────────────────────────── */
.rules-box {
background: rgba(138,101,32,0.08);
border: 1px solid #c9a22740;
border-radius: 4px;
padding: 1rem 1.2rem;
margin: 0.8rem 0;
}
.rules-title {
font-family: 'Cinzel', serif;
font-size: 0.8rem;
letter-spacing: 0.15em;
color: var(--red);
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.rules-list {
list-style: none;
padding: 0;
}
.rules-list li {
font-size: 0.95rem;
padding: 0.25rem 0 0.25rem 1.2rem;
position: relative;
color: var(--ink-light);
}
.rules-list li::before {
content: '⚔';
position: absolute;
left: 0;
font-size: 0.75rem;
color: var(--gold);
top: 0.35rem;
}
/* ── TABS ──────────────────────────────────── */
.tab-content { display: none; }
.tab-content.active { display: block; }
/* ── SPIELER ───────────────────────────────── */
.spieler-add-row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1.2rem;
}
.spieler-input {
font-family: 'Crimson Pro', serif;
font-size: 1rem;
background: #fdf5dc;
border: 1px solid #8a6520;
border-radius: 3px;
padding: 0.5rem 0.8rem;
color: #1a0f00;
outline: none;
flex: 1;
min-width: 120px;
}
.spieler-input:focus { border-color: #c9a227; box-shadow: 0 0 0 2px rgba(201,162,39,0.2); }
.spieler-select {
font-family: 'Crimson Pro', serif;
font-size: 0.95rem;
background: #fdf5dc;
border: 1px solid #8a6520;
border-radius: 3px;
padding: 0.5rem 0.6rem;
color: #1a0f00;
cursor: pointer;
outline: none;
}
.btn-add {
font-family: 'Cinzel', serif;
font-size: 0.8rem;
letter-spacing: 0.1em;
background: #2a5a1a;
color: #a8ff80;
border: none;
padding: 0.5rem 1rem;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-add:hover { background: #1a4a0a; }
.spieler-list {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.spieler-item {
display: flex;
align-items: center;
gap: 0.7rem;
background: #fdf5dc;
border: 1px solid #c9a22760;
border-radius: 4px;
padding: 0.7rem 1rem;
flex-wrap: wrap;
}
.spieler-avatar {
width: 38px; height: 38px;
border-radius: 50%;
background: linear-gradient(135deg, #8a6520, #c9a227);
display: flex; align-items: center; justify-content: center;
font-size: 1.1rem;
flex-shrink: 0;
color: #fff;
font-weight: bold;
font-family: 'Cinzel', serif;
}
.spieler-info { flex: 1; min-width: 100px; }
.spieler-name-display {
font-family: 'Cinzel', serif;
font-size: 0.95rem;
color: #1a0f00;
font-weight: 600;
}
.spieler-rolle-display {
font-size: 0.8rem;
color: #5a3a10;
margin-top: 0.1rem;
font-style: italic;
}
.spieler-rolle-badge {
font-family: 'Cinzel', serif;
font-size: 0.65rem;
letter-spacing: 0.08em;
padding: 0.15rem 0.5rem;
border-radius: 2px;
white-space: nowrap;
flex-shrink: 0;
}
.rolle-held { background: #5a3a00; color: #ffd060; }
.rolle-hüter { background: #2a1a5a; color: #a080ff; }
.rolle-ratsmitglied { background: #4a1a1a; color: #ff9060; }
.rolle-einsteiger { background: #1a4a1a; color: #a8ff80; }
.rolle-spielleiter { background: #1a1a4a; color: #80c0ff; }
.rolle-gast { background: #3a3a1a; color: #e0e080; }
.spieler-quest-sel {
font-family: 'Crimson Pro', serif;
font-size: 0.8rem;
background: #f4e8c1;
border: 1px solid #8a652060;
border-radius: 3px;
padding: 0.25rem 0.4rem;
color: #3d2200;
cursor: pointer;
max-width: 160px;
}
.btn-del {
background: transparent;
border: 1px solid #8b1a1a60;
color: #8b1a1a;
border-radius: 3px;
padding: 0.25rem 0.6rem;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
flex-shrink: 0;
}
.btn-del:hover { background: #8b1a1a; color: #fff; }
.spieler-empty {
text-align: center;
font-style: italic;
color: #8a6520;
padding: 2rem;
font-size: 0.95rem;
}
.spieler-summary {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #c9a22740;
}
.summary-chip {
font-family: 'Cinzel', serif;
font-size: 0.7rem;
letter-spacing: 0.08em;
padding: 0.2rem 0.6rem;
border-radius: 20px;
border: 1px solid #c9a22760;
color: #5a3a10;
}
/* ── PRINT HINT ────────────────────────────── */
.print-hint {
text-align: center;
font-family: 'Cinzel', serif;
font-size: 0.75rem;
letter-spacing: 0.15em;
color: #5a3a10;
padding: 1rem;
}
/* ── RESPONSIVE ────────────────────────────── */
@media (max-width: 600px) {
.cover-title { font-size: 1.8rem; }
.parchment { padding: 1.2rem; }
.quest-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- ══════════════════ COVER ══════════════════ -->
<div id="cover">
<div class="stars" id="stars"></div>
<div class="cover-emblem">⚔️</div>
<div class="cover-subtitle">Ein Junggesellenabschied der Legenden</div>
<div class="cover-title">Der letzte<br>Auftrag</div>
<div class="cover-divider"></div>
<div class="cover-desc">
Eine Heldengeschichte, Trinkspiele & tabletop Quests <br>
für den tapfersten Krieger, der je eine Braut zu retten wagte.
</div>
<button class="btn-primary" onclick="startApp()">📜 Abenteuer beginnen</button>
</div>
<!-- ══════════════════ APP ══════════════════ -->
<div id="app">
<nav class="top-nav">
<span class="nav-title">⚔ Der letzte Auftrag</span>
<div class="nav-tabs">
<button class="nav-tab active" onclick="showTab('intro')">Intro</button>
<button class="nav-tab" onclick="showTab('quests')">Quests</button>
<button class="nav-tab" onclick="showTab('tabletop')">Tabletop</button>
<button class="nav-tab" onclick="showTab('tracker')">Tracker</button>
<button class="nav-tab" onclick="showTab('spieler')">Spieler</button>
<button class="nav-tab" onclick="showTab('ablauf')">Ablauf</button>
</div>
</nav>
<div class="page-wrap">
<!-- ═══ INTRO ═══ -->
<div id="tab-intro" class="tab-content active">
<div class="parchment">
<div class="section-title">Das Abenteuer</div>
<div class="section-subtitle">Für Spielleiter & Organisatoren</div>
<div class="ornament">✦ ✦ ✦</div>
<div class="flavor-text">
„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."
</div>
<p>Das Konzept ist simpel: <strong>Der Bräutigam ist der Held.</strong> 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.</p>
<div class="rules-box">
<div class="rules-title">Grundregeln</div>
<ul class="rules-list">
<li>Der Held löst Aufgaben bei den verschiedenen Gästen (= Hüter).</li>
<li>Erfolg → Hüter übergibt seinen Gegenstand (oder einen Hinweis-Zettel).</li>
<li>Misserfolg → Der Held trinkt. Der Gegenstand bleibt beim Hüter.</li>
<li>Er kann es später nochmal versuchen mit erhöhtem Schwierigkeitsgrad.</li>
<li>Hat er am Ende ALLE 8 Gegenstände, darf er die Braut symbolisch freikaufen.</li>
<li>Fehlt noch etwas? Er trinkt für jeden fehlenden Gegenstand nochmal.</li>
<li>Einsteiger-Quests brauchen keine Vorkenntnisse. Der Tabletop-Block ist optional für RPG-Enthusiasten.</li>
</ul>
</div>
<div class="rules-box">
<div class="rules-title">Was braucht ihr?</div>
<ul class="rules-list">
<li>8 physische Gegenstände (Fotos, kleine Dinge oder Zettel mit Clue-Beschreibungen)</li>
<li>Diese App / Ausgedrucktes für den Spielleiter</li>
<li>Getränke in ausreichender Menge 🍺</li>
<li>Gute Laune & keine Scham</li>
<li>Für den Tabletop-Block: W6, W10 oder diese App als Würfel</li>
</ul>
</div>
<div class="ornament">✦ ✦ ✦</div>
<p style="text-align:center; font-style:italic; color: #5a3a10;">Der Held weiß am Anfang NUR, dass er Quests lösen muss.<br>Die Hüter kennen ihre Aufgabe, er nicht.</p>
</div>
<div class="parchment">
<div class="section-title">Die 8 Besitztümer</div>
<div class="section-subtitle">Für die Braut vorbereiten</div>
<p>Bereite diese 8 Gegenstände (oder Zettel mit ihrer Beschreibung) vor. Die Hüter bekommen sie vorab zugewiesen:</p>
<div class="inventory-grid">
<div class="item-slot" id="inv-0" onclick="toggleItem(0)">
<span class="item-emoji">💍</span>
<span class="item-name">Der Ring der Verbindung</span>
<span class="item-check"></span>
</div>
<div class="item-slot" id="inv-1" onclick="toggleItem(1)">
<span class="item-emoji">👠</span>
<span class="item-name">Der Schuh der Flucht</span>
<span class="item-check"></span>
</div>
<div class="item-slot" id="inv-2" onclick="toggleItem(2)">
<span class="item-emoji">💌</span>
<span class="item-name">Der Brief des ersten Dates</span>
<span class="item-check"></span>
</div>
<div class="item-slot" id="inv-3" onclick="toggleItem(3)">
<span class="item-emoji">🌹</span>
<span class="item-name">Die Blume der Ewigkeit</span>
<span class="item-check"></span>
</div>
<div class="item-slot" id="inv-4" onclick="toggleItem(4)">
<span class="item-emoji">🔑</span>
<span class="item-name">Der Schlüssel zur Zukunft</span>
<span class="item-check"></span>
</div>
<div class="item-slot" id="inv-5" onclick="toggleItem(5)">
<span class="item-emoji">📸</span>
<span class="item-name">Das Foto der gemeinsamen Zeit</span>
<span class="item-check"></span>
</div>
<div class="item-slot" id="inv-6" onclick="toggleItem(6)">
<span class="item-emoji">🧸</span>
<span class="item-name">Das Maskottchen des Heils</span>
<span class="item-check"></span>
</div>
<div class="item-slot" id="inv-7" onclick="toggleItem(7)">
<span class="item-emoji">🏆</span>
<span class="item-name">Die Urkunde der Heldentaten</span>
<span class="item-check"></span>
</div>
</div>
</div>
</div>
<!-- ═══ QUESTS ═══ -->
<div id="tab-quests" class="tab-content">
<div class="parchment">
<div class="section-title">Die Quests</div>
<div class="section-subtitle">Aufgaben der Hüter — Klicken zum Aufklappen</div>
<div class="rules-box">
<div class="rules-title">Farbcode</div>
<ul class="rules-list" style="columns:2; gap:1rem;">
<li><span class="quest-badge badge-easy">EINSTEIGER</span> Keine Vorkenntnisse</li>
<li><span class="quest-badge badge-medium">MITTEL</span> Etwas Geschick</li>
<li><span class="quest-badge badge-hard">HERAUSFORDERUNG</span> Echte Prüfung</li>
<li><span class="quest-badge badge-tabletop">TABLETOP</span> RPG-Erfahrene</li>
</ul>
</div>
<div class="quest-grid" id="questGrid"></div>
</div>
</div>
<!-- ═══ TABLETOP ═══ -->
<div id="tab-tabletop" class="tab-content">
<div class="parchment">
<div class="section-title">Tabletop-Block</div>
<div class="section-subtitle">Für erfahrene Rollenspieler · W6/W10-System</div>
<div class="flavor-text">
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. 3060 Minuten.
</div>
<div class="encounter-box">
<div class="encounter-title">🐉 Szene I Der Wächter des Tores</div>
<div class="flavor-text">
„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."
</div>
<p>Der Wächter stellt <strong>3 Fragen</strong> echte Fragen über die Beziehung (Spielleiter vorbereiten!). Für jede richtige Antwort: kein Würfelwurf nötig. Für jede falsche: Würfelduell.</p>
<div class="stat-block">
<div class="stat-item">
<div class="stat-val">W10</div>
<div class="stat-name">System</div>
</div>
<div class="stat-item">
<div class="stat-val">7+</div>
<div class="stat-name">Erfolg</div>
</div>
<div class="stat-item">
<div class="stat-val">3</div>
<div class="stat-name">Fragen</div>
</div>
</div>
<p style="font-size:0.9rem;"><strong>Beispielfragen:</strong> Wann hatten sie ihr erstes Date? Was ist ihr Lieblingsessen? Was war ihr erster gemeinsamer Urlaub?</p>
</div>
<div class="encounter-box">
<div class="encounter-title">⚔ Szene II Die Prüfung des Mutes</div>
<div class="flavor-text">
„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."
</div>
<p>Jeder RPG-erfahrene Spieler ist ein <strong>Ratsmitglied</strong> und beschreibt eine kurze Szene. Der Held muss reagieren (Roleplay + Würfelwurf). Das Ratsmitglied entscheidet, ob der Wurf ausreicht.</p>
<div class="rules-box" style="background: rgba(201,162,39,0.05); border-color: #4a2800;">
<div class="rules-title" style="color: var(--gold);">Beispiel-Szenen für Ratsmitglieder</div>
<ul class="rules-list">
<li>„Du stehst vor einem Drachen, der dich nach ihrem schlimmsten Eigenschaft fragt. Antworte weise." (Charisma-Check)</li>
<li>„Ein alter Rivale erscheint. Er behauptet, ihr erster Kuss war mit ihm." (Einschüchterungs-Check)</li>
<li>„Du musst in 30 Sekunden 3 Dinge nennen, die du an ihr liebst ohne zu stocken." (Wisdom-Check)</li>
<li>„Der Magier bietet dir: ENTWEDER ihre Schuhe ODER ein ewiges Leben ohne sie. Was wählst du?" (Roleplay kein Würfelwurf)</li>
</ul>
</div>
</div>
<div class="encounter-box">
<div class="encounter-title">🎲 Szene III Der finale Würfelkampf</div>
<div class="flavor-text">
„Ein letztes Tor. Dahinter wartet der Wächter der Wächter der härteste Gegner des Abends. Er fordert den Held zum entscheidenden Duell."
</div>
<p>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.</p>
<div class="stat-block">
<div class="stat-item">
<div class="stat-val">W10</div>
<div class="stat-name">Würfel</div>
</div>
<div class="stat-item">
<div class="stat-val">3/5</div>
<div class="stat-name">Best-of</div>
</div>
<div class="stat-item">
<div class="stat-val">🍺</div>
<div class="stat-name">Einsatz</div>
</div>
</div>
<p style="font-size:0.9rem;">Jede verlorene Runde = ein Schluck. Wer das Duell verliert, ERZÄHLT außerdem einer Anekdote über die Braut.</p>
</div>
<div class="dice-roller">
<div class="dice-roller-title">🎲 Würfel-Assistent</div>
<div class="dice-btns" id="diceBtns"></div>
<div class="dice-result" id="diceResult"></div>
<div class="dice-log" id="diceLog"></div>
</div>
</div>
</div>
<!-- ═══ TRACKER ═══ -->
<div id="tab-tracker" class="tab-content">
<div class="parchment">
<div class="section-title">Helden-Tracker</div>
<div class="section-subtitle">Live-Übersicht während des Abends</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom:1.5rem;">
<div class="drink-tracker">
<div class="drink-count" id="drinkCount">0</div>
<div class="drink-label">Getrunken 🍺</div>
<div class="drink-btns">
<button class="btn-drink add" onclick="changeDrinks(1)">+1</button>
<button class="btn-drink sub" onclick="changeDrinks(-1)">1</button>
</div>
</div>
<div class="drink-tracker">
<div class="drink-count" id="questCount" style="color: var(--green);">0/8</div>
<div class="drink-label">Besitztümer ⚔</div>
<div style="margin-top:1rem; font-family: 'Cinzel', serif; font-size:0.75rem; color:#8a6520; letter-spacing:0.1em;">
Im Intro-Tab verwalten
</div>
</div>
</div>
<div class="rules-box">
<div class="rules-title">Quest-Status</div>
<div id="questStatusList" style="display:flex; flex-direction:column; gap:0.4rem; margin-top:0.5rem;"></div>
</div>
<div class="rules-box" style="margin-top:1rem;">
<div class="rules-title">Notizen</div>
<textarea id="notes" placeholder="Hier Notizen eingeben..."
style="width:100%; background:rgba(138,101,32,0.05); border:1px solid #c9a22740; border-radius:4px; padding:0.6rem; font-family:'Crimson Pro',serif; font-size:0.95rem; color:var(--ink-light); resize:vertical; min-height:80px; outline:none;"
oninput="saveNotes()"></textarea>
</div>
<div style="text-align:center; margin-top:1.5rem;">
<button class="btn-primary" onclick="resetAll()">↺ Neustart</button>
</div>
</div>
</div>
<!-- ═══ SPIELER ═══ -->
<div id="tab-spieler" class="tab-content">
<div class="parchment">
<div class="section-title">Die Gefährten</div>
<div class="section-subtitle">Spieler, Rollen & Hüter-Zuteilung</div>
<div class="flavor-text">
Trage hier alle Teilnehmer des Abends ein und weise ihnen ihre Rolle zu. So behältst du den Überblick, wer welche Quest hütet.
</div>
<!-- Hinzufügen -->
<div class="rules-box">
<div class="rules-title">Spieler hinzufügen</div>
<div class="spieler-add-row" style="margin-top:0.8rem;">
<input class="spieler-input" id="sName" placeholder="Name..." maxlength="30" onkeydown="if(event.key==='Enter')addSpieler()">
<select class="spieler-select" id="sRolle">
<option value="held">⚔ Held (Bräutigam)</option>
<option value="spielleiter">📜 Spielleiter</option>
<option value="hüter" selected>🔒 Hüter</option>
<option value="ratsmitglied">🧙 Ratsmitglied</option>
<option value="einsteiger">🌱 Einsteiger</option>
<option value="gast">🍺 Gast</option>
</select>
<select class="spieler-select" id="sQuest">
<option value=""> Quest zuweisen </option>
</select>
<button class="btn-add" onclick="addSpieler()">+ Hinzufügen</button>
</div>
</div>
<!-- Liste -->
<div id="spielerList" class="spieler-list">
<div class="spieler-empty">Keine Teilnehmer eingetragen.</div>
</div>
<!-- Zusammenfassung -->
<div class="spieler-summary" id="spielerSummary" style="display:none;">
<span class="summary-chip" id="sumTotal">0 Teilnehmer</span>
<span class="summary-chip" id="sumHüter">0 Hüter</span>
<span class="summary-chip" id="sumRat">0 Ratsmitglieder</span>
<span class="summary-chip" id="sumEinsteiger">0 Einsteiger</span>
</div>
</div>
<!-- Rollen-Legende -->
<div class="parchment">
<div class="section-title" style="font-size:1.1rem;">Rollen-Übersicht</div>
<div class="ornament">✦ ✦ ✦</div>
<div class="rules-box">
<ul class="rules-list">
<li><span class="spieler-rolle-badge rolle-held">⚔ HELD</span> &nbsp;Der Bräutigam löst alle Quests.</li>
<li><span class="spieler-rolle-badge rolle-spielleiter">📜 SPIELLEITER</span> &nbsp;Organisator kennt alle Aufgaben, leitet den Abend.</li>
<li><span class="spieler-rolle-badge rolle-hüter">🔒 HÜTER</span> &nbsp;Bewacht ein Besitztum & stellt die zugewiesene Quest.</li>
<li><span class="spieler-rolle-badge rolle-ratsmitglied">🧙 RATSMITGLIED</span> &nbsp;Erfahrener RPG-Spieler nimmt am Tabletop-Block teil.</li>
<li><span class="spieler-rolle-badge rolle-einsteiger">🌱 EINSTEIGER</span> &nbsp;Tabletop-Neuling bekommt einfache Quests.</li>
<li><span class="spieler-rolle-badge rolle-gast">🍺 GAST</span> &nbsp;Schaut zu, feuert an und trinkt mit.</li>
</ul>
</div>
</div>
</div>
<!-- ═══ ABLAUF ═══ -->
<div id="tab-ablauf" class="tab-content">
<div class="parchment">
<div class="section-title">Ablaufplan</div>
<div class="section-subtitle">Empfohlener Zeitplan für den Abend</div>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-time">Beginn · Ankunft</div>
<div class="timeline-event">Versammlung & Einstimmung</div>
<div class="timeline-note">Alle Hüter bekommen ihren Gegenstand & ihre Aufgabe erklärt. Der Held weiß noch nichts.</div>
</div>
<div class="timeline-item">
<div class="timeline-time">~30 Min. nach Beginn</div>
<div class="timeline-event">Die Verkündigung 📜</div>
<div class="timeline-note">Der Spielleiter liest dem Held feierlich die Quest vor (Intro-Text). Dann beginnt das Abenteuer.</div>
</div>
<div class="timeline-item">
<div class="timeline-time">Phase 1 · 45-60 Min.</div>
<div class="timeline-event">Einsteiger-Quests (15)</div>
<div class="timeline-note">Der Held absolviert die ersten Aufgaben und erbeutet die ersten Besitztümer.</div>
</div>
<div class="timeline-item">
<div class="timeline-time">Pause · ~Mitte des Abends</div>
<div class="timeline-event">Inventur & Halftime-Ritual</div>
<div class="timeline-note">Für jeden fehlenden Gegenstand trinkt der Held einen kleinen Schluck. Dann gehts weiter.</div>
</div>
<div class="timeline-item">
<div class="timeline-time">Phase 2 · 45-60 Min.</div>
<div class="timeline-event">Herausforderungs-Quests (68)</div>
<div class="timeline-note">Anspruchsvollere Aufgaben mit höherem Einsatz.</div>
</div>
<div class="timeline-item">
<div class="timeline-time">Optional · nach Phase 2</div>
<div class="timeline-event">⚔ Tabletop-Block</div>
<div class="timeline-note">Die RPG-erfahrenen Spieler ziehen sich für 30-60 Min. in die Ratssitzung zurück. Andere trinken weiter.</div>
</div>
<div class="timeline-item">
<div class="timeline-time">Finale · Abend-Ende</div>
<div class="timeline-event">Die Befreiung der Braut 🏆</div>
<div class="timeline-note">Der Held präsentiert alle Besitztümer. Für jeden fehlenden: Straftrunk. Dann: Feier ohne Quests!</div>
</div>
</div>
<div class="rules-box" style="margin-top:1.5rem;">
<div class="rules-title">Hinweise für den Spielleiter</div>
<ul class="rules-list">
<li>Hüter vorab briefen! Aufgabe kennen, Gegenstand bereit haben.</li>
<li>Zweite Chance immer erlauben aber mit Extratrinken.</li>
<li>Atmosphäre hat Vorrang vor strikter Regelauslegung.</li>
<li>Wasser & Snacks bereitstellen, wenn es länger geht.</li>
</ul>
</div>
</div>
</div>
</div><!-- /page-wrap -->
</div><!-- /app -->
<script>
// ═══════════════════════════════════════════
// 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(8).fill(false);
// 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;`;
c.appendChild(s);
}
})();
function showTab(id) {
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
document.getElementById('tab-' + id).classList.add('active');
event.target.classList.add('active');
}
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');
card.className = 'quest-card' + (questDone[i] ? ' done' : '');
card.id = 'qcard-' + i;
card.innerHTML = `
<span class="quest-badge badge-${q.badge}">${q.badgeLabel}</span>
<div class="quest-name">${q.name}</div>
<div class="quest-holder">Hüter: ${q.holder}</div>
<div class="quest-desc">${q.desc}</div>
<div class="quest-rewards">
<span class="reward-success">${q.success}</span>
<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;">
${questDone[i] ? '↩ Als offen markieren' : '✓ Als erledigt markieren'}
</button>
</div>
<div class="expand-hint">▼ Aufklappen für Details</div>`;
card.addEventListener('click', () => toggleExpand(i));
grid.appendChild(card);
});
}
function toggleExpand(i) {
const card = document.getElementById('qcard-' + i);
if (card) card.classList.toggle('expanded');
}
async function toggleDone(i) {
questDone[i] = !questDone[i];
const sv = await apiGet('/game');
if (!sv) return;
const s = {...sv.state, questDone: [...questDone]};
await updateAndSync(s);
buildQuests();
updateQuestStatus();
}
// ═══════════════════════════════════════════
// PART 5: INVENTORY & DRINKS
// ═══════════════════════════════════════════
async function toggleItem(i) {
itemCollected[i] = !itemCollected[i];
const slot = document.getElementById('inv-' + i);
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();
}
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;
}
}
// ═══════════════════════════════════════════
// PART 6: STATUS
// ═══════════════════════════════════════════
function updateQuestStatus() {
const count = itemCollected.filter(Boolean).length;
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'};">
<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('');
}
// ═══════════════════════════════════════════
// PART 7: WÜRFEL
// ═══════════════════════════════════════════
function buildDice() {
const btn = document.getElementById('diceBtns');
if (!btn) return;
[4,6,8,10,12,20,100].forEach(d => {
const b = document.createElement('button');
b.className = 'dice-btn';
b.textContent = 'W' + d;
b.onclick = () => rollDice(d);
btn.appendChild(b);
});
}
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');
el.textContent = result;
const reactions = result === sides ? '⭐ KRITISCH! Legendär!' :
result === 1 ? '💀 Kritischer Fehlschlag! Trinken!' :
result >= Math.ceil(sides * 0.7) ? '✓ Starkes Ergebnis!' :
result >= Math.ceil(sides * 0.4) ? '~ Mittelmaß...' : '✗ Schwach. Trinken?';
log.textContent = `W${sides}${result} · ${reactions}`;
}
// ═══════════════════════════════════════════
// PART 8: SPIELER (API-Version)
// ═══════════════════════════════════════════
let spieler = [];
const rolleLabels = {
held: '⚔ Held',
spielleiter: '📜 Spielleiter',
hüter: '🔒 Hüter',
ratsmitglied: '🧙 Ratsmitglied',
einsteiger: '🌱 Einsteiger',
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;
const current = sel.value;
sel.innerHTML = '<option value=""> Quest zuweisen </option>';
quests.forEach((q, i) => {
const opt = document.createElement('option');
opt.value = i;
opt.textContent = q.name;
sel.appendChild(opt);
});
sel.value = current;
}
async function addSpieler() {
const nameEl = document.getElementById('sName');
const rolleEl = document.getElementById('sRolle');
const questEl = document.getElementById('sQuest');
const name = nameEl.value.trim();
if (!name) { nameEl.focus(); return; }
const rolle = rolleEl.value;
const questIdx = questEl.value !== '' ? parseInt(questEl.value) : null;
const res = await apiPost('/spieler', {name, rolle, quest_id: questIdx});
if (res && res.spieler) {
spieler.push(res.spieler);
nameEl.value = '';
questEl.value = '';
renderSpieler();
}
}
async function deleteSpieler(id) {
await apiDelete('/spieler/' + id);
spieler = spieler.filter(s => s.id !== id);
renderSpieler();
}
async function changeRolle(id, rolle) {
const s = spieler.find(s => s.id === id);
if (s) { s.rolle = rolle; await apiPut('/spieler/' + id, {rolle}); renderSpieler(); }
}
async function changeQuest(id, val) {
const s = spieler.find(s => s.id === id);
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 renderSpieler() {
const list = document.getElementById('spielerList');
if (!list) return;
if (spieler.length === 0) {
list.innerHTML = '<div class="spieler-empty">Keine Teilnehmer eingetragen.</div>';
} else {
list.innerHTML = spieler.map(s => {
const questName = s.questIdx !== null ? quests[s.questIdx]?.name : null;
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>
<div class="spieler-rolle-display">${questName ? '🔒 ' + questName : rolleLabels[s.rolle]}</div>
</div>
<span class="spieler-rolle-badge rolle-${s.rolle}">${rolleLabels[s.rolle]}</span>
<select class="spieler-quest-sel" onchange="changeQuest(${s.id}, this.value)">
<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)">
${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('');
}
const summary = document.getElementById('spielerSummary');
if (summary) {
summary.style.display = spieler.length ? 'flex' : 'none';
document.getElementById('sumTotal').textContent = spieler.length + ' Teilnehmer';
document.getElementById('sumHüter').textContent = spieler.filter(s=>s.rolle==='hüter'||s.questIdx!==null).length + ' Hüter';
document.getElementById('sumRat').textContent = spieler.filter(s=>s.rolle==='ratsmitglied').length + ' Ratsmitglieder';
document.getElementById('sumEinsteiger').textContent = spieler.filter(s=>s.rolle==='einsteiger').length + ' Einsteiger';
}
}
// Start bei Page Load
document.addEventListener('DOMContentLoaded', startApp);
</script>