Files
JGA-2026/trinkspiel.html
Hermes AI Bot 4e1691ac37 fix(cache): auto-reload bei SW-Update statt confirm-Prompt
Bei neuem Service Worker wird die Seite automatisch neu geladen.
Kein Popup mehr für die User.
2026-06-27 13:08:41 +00:00

1205 lines
38 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JGA-Trinkspiel — Prag 2026</title>
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#3b82f6">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="JGA Prag">
<meta name="mobile-web-app-capable" content="yes">
<script>
(function() {
const saved = localStorage.getItem('jga-theme');
document.documentElement.setAttribute('data-theme', saved || 'dark');
})();
</script>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🃏</text></svg>">
<style>
/* ====== Theme-Variablen ====== */
:root, [data-theme="dark"] {
--bg: #1c1c1f;
--card-bg: #26262a;
--card-bg-2: #2e2e33;
--card-border: #353539;
--text: #e8e8ea;
--text-muted: #999;
--text-strong: #fff;
--accent: #3b82f6;
--accent-text: #60a5fa;
--danger: #ef4444;
--success: #22c55e;
--warn: #f59e0b;
--shadow: 0 4px 16px rgba(0,0,0,0.35);
--shadow-strong: 0 8px 32px rgba(0,0,0,0.5);
--card-back: linear-gradient(135deg, #1e40af 0%, #3b82f6 50%, #1e40af 100%);
--card-back-pattern: rgba(255,255,255,0.08);
}
[data-theme="light"] {
--bg: #f5f5f7;
--card-bg: #ffffff;
--card-bg-2: #f8f8fb;
--card-border: #e0e0e3;
--text: #1a1a1a;
--text-muted: #666;
--text-strong: #000;
--accent: #2563eb;
--accent-text: #2563eb;
--danger: #dc2626;
--success: #16a34a;
--warn: #d97706;
--shadow: 0 4px 16px rgba(0,0,0,0.08);
--shadow-strong: 0 8px 32px rgba(0,0,0,0.15);
--card-back: linear-gradient(135deg, #1e40af 0%, #3b82f6 50%, #1e40af 100%);
--card-back-pattern: rgba(255,255,255,0.15);
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
transition: background 0.3s, color 0.3s;
}
.container { max-width: 1100px; margin: 0 auto; padding: 24px 24px 60px; }
h1 {
font-size: 36px;
margin: 0 0 8px 0;
color: var(--text-strong);
font-weight: 700;
}
.subtitle { color: var(--text-muted); margin-bottom: 28px; font-size: 15px; }
.back-link {
display: inline-block;
margin-bottom: 16px;
color: var(--accent-text);
text-decoration: none;
font-size: 14px;
}
.back-link:hover { text-decoration: underline; }
/* Theme-Toggle */
.home-btn {
position: fixed;
top: 16px; left: 16px;
z-index: 1000;
background: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text);
text-decoration: none;
padding: 10px 16px;
border-radius: 22px;
font-size: 14px;
font-weight: 600;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
}
@media (max-width: 768px) {
.home-btn { top: 10px; left: 10px; padding: 8px 12px; font-size: 13px; }
.theme-toggle { top: 10px; right: 10px; width: 38px; height: 38px; font-size: 18px; }
}
.home-btn:hover {
transform: translateX(-2px);
border-color: var(--accent);
color: var(--accent);
}
.theme-toggle {
position: fixed;
top: 16px;
right: 16px;
width: 44px; height: 44px;
border-radius: 50%;
background: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text);
font-size: 20px;
cursor: pointer;
z-index: 100;
box-shadow: var(--shadow);
transition: all 0.2s;
display: flex; align-items: center; justify-content: center;
}
.theme-toggle:hover {
transform: rotate(15deg) scale(1.1);
border-color: var(--accent);
}
/* ===== Layout ===== */
.layout {
display: grid;
grid-template-columns: 260px 1fr 280px;
gap: 20px;
}
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
}
.panel {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 18px;
}
.panel h2 {
margin: 0 0 14px;
font-size: 15px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
font-weight: 600;
}
/* ===== Spiele-Auswahl ===== */
.game-list { display: flex; flex-direction: column; gap: 8px; }
.game-btn {
text-align: left;
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-radius: 10px;
padding: 12px 14px;
color: var(--text);
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 10px;
}
.game-btn:hover {
border-color: var(--accent);
transform: translateX(2px);
}
.game-btn.active {
border-color: var(--accent);
background: rgba(59, 130, 246, 0.12);
color: var(--text-strong);
font-weight: 600;
}
.game-btn .icon { font-size: 22px; }
/* ===== Spieler ===== */
.player-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.player {
display: flex;
align-items: center;
gap: 8px;
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 8px 10px;
font-size: 14px;
}
.player .avatar { font-size: 22px; }
.player .name { flex: 1; font-weight: 500; }
.player .drinks {
background: var(--accent);
color: #fff;
border-radius: 12px;
padding: 2px 10px;
font-size: 13px;
font-weight: 700;
min-width: 32px;
text-align: center;
}
.player.drunk .drinks { background: var(--danger); }
.player .remove {
background: transparent;
border: 0;
color: var(--text-muted);
cursor: pointer;
font-size: 16px;
padding: 0 4px;
}
.player .remove:hover { color: var(--danger); }
.add-player {
display: flex;
gap: 6px;
}
.add-player input {
flex: 1;
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 8px 10px;
color: var(--text);
font-size: 14px;
}
.add-player input:focus { outline: 2px solid var(--accent); }
.add-player button {
background: var(--accent);
color: #fff;
border: 0;
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
font-size: 16px;
}
/* ===== Mitte: Karten-Deck ===== */
.center {
display: flex;
flex-direction: column;
align-items: center;
}
.game-info {
text-align: center;
margin-bottom: 24px;
max-width: 480px;
}
.game-info h3 {
margin: 0 0 6px;
font-size: 22px;
color: var(--text-strong);
}
.game-info p {
margin: 0;
color: var(--text-muted);
font-size: 14px;
line-height: 1.5;
}
.deck-area {
position: relative;
width: 240px;
height: 336px;
margin: 20px 0 32px;
perspective: 1200px;
}
.card-wrap {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
transform-style: preserve-3d;
transition: transform 0.7s cubic-bezier(0.4, 0.0, 0.2, 1);
cursor: pointer;
}
.card-wrap.flipped { transform: rotateY(180deg); }
.card-face {
position: absolute;
inset: 0;
border-radius: 16px;
backface-visibility: hidden;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-strong);
overflow: hidden;
}
.card-back {
background: var(--card-back);
border: 6px solid #fff;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.card-back::before, .card-back::after {
content: '';
position: absolute;
background: var(--card-back-pattern);
}
.card-back::before {
width: 70%; height: 70%;
border-radius: 50%;
}
.card-back::after {
width: 90%; height: 2px;
top: 50%; left: 5%;
transform: rotate(45deg);
box-shadow:
0 -80px 0 var(--card-back-pattern),
0 -160px 0 var(--card-back-pattern),
0 80px 0 var(--card-back-pattern),
0 160px 0 var(--card-back-pattern);
}
.card-back .logo {
font-size: 64px;
z-index: 1;
filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3));
}
.card-front {
transform: rotateY(180deg);
background: var(--card-bg);
border: 2px solid var(--card-border);
padding: 20px;
text-align: center;
}
.card-front.red { color: #dc2626; }
.card-front.black { color: #1a1a1a; }
[data-theme="dark"] .card-front.black { color: #fff; }
.card-front .suit {
font-size: 48px;
line-height: 1;
}
.card-front .rank {
font-size: 72px;
font-weight: 800;
line-height: 1;
margin: 8px 0;
}
.card-front .action {
font-size: 15px;
font-weight: 500;
line-height: 1.4;
color: var(--text);
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--card-border);
}
.card-front .who {
font-size: 12px;
color: var(--text-muted);
margin-top: 8px;
}
.deck-stack {
position: absolute;
inset: 0;
pointer-events: none;
}
.deck-stack::before, .deck-stack::after {
content: '';
position: absolute;
inset: 0;
background: var(--card-back);
border: 6px solid #fff;
border-radius: 16px;
box-shadow: var(--shadow);
}
.deck-stack::before {
transform: translate(8px, 8px);
opacity: 0.7;
}
.deck-stack::after {
transform: translate(4px, 4px);
opacity: 0.85;
}
.deck-controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.btn {
background: var(--accent);
color: #fff;
border: 0;
border-radius: 10px;
padding: 12px 22px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
box-shadow: var(--shadow);
}
.btn:hover { transform: translateY(-1px); }
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: var(--card-bg-2);
color: var(--text);
border: 1px solid var(--card-border);
}
.btn-danger { background: var(--danger); }
.draw-status {
margin-top: 14px;
color: var(--text-muted);
font-size: 13px;
}
/* ===== Regeln / Stats ===== */
.rules-list { list-style: none; padding: 0; margin: 0; }
.rules-list li {
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-left: 3px solid var(--warn);
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 8px;
font-size: 14px;
display: flex;
gap: 8px;
align-items: flex-start;
}
.rules-list li .rule-text { flex: 1; }
.rules-list li .rule-meta {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
.rules-list li button {
background: transparent;
border: 0;
color: var(--text-muted);
cursor: pointer;
font-size: 14px;
}
.rules-list li button:hover { color: var(--danger); }
.stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.stat {
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 10px;
text-align: center;
}
.stat .num {
font-size: 22px;
font-weight: 700;
color: var(--accent-text);
}
.stat .label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 2px;
}
/* History */
.history {
max-height: 220px;
overflow-y: auto;
margin-top: 10px;
}
.history-item {
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-radius: 6px;
padding: 6px 10px;
margin-bottom: 6px;
font-size: 13px;
display: flex;
gap: 8px;
align-items: center;
}
.history-item .mini-card {
font-size: 16px;
width: 28px;
text-align: center;
}
.history-item.red { color: #dc2626; }
.history-item.black { color: var(--text); }
[data-theme="dark"] .history-item.black { color: #fff; }
.history-item .who-text { color: var(--text-muted); margin-left: auto; font-size: 12px; }
/* King-Becher */
.king-meter {
margin: 16px 0 4px;
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 10px;
}
.king-meter .label {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 6px;
}
.king-bar {
height: 8px;
background: var(--card-border);
border-radius: 4px;
overflow: hidden;
}
.king-fill {
height: 100%;
background: linear-gradient(90deg, var(--warn) 0%, var(--danger) 100%);
transition: width 0.4s;
}
/* Modal für Spieler-Auswahl */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.6);
display: none;
align-items: center;
justify-content: center;
z-index: 200;
backdrop-filter: blur(4px);
}
.modal-overlay.open { display: flex; }
.modal {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 24px;
max-width: 400px;
width: 90%;
box-shadow: var(--shadow-strong);
}
.modal h3 { margin: 0 0 16px; color: var(--text-strong); }
.modal-actions { display: flex; gap: 10px; margin-top: 18px; }
.modal-actions .btn { flex: 1; }
/* Empty state */
.empty {
color: var(--text-muted);
font-size: 13px;
text-align: center;
padding: 16px;
font-style: italic;
}
/* Footer */
.footer-note {
text-align: center;
color: var(--text-muted);
font-size: 12px;
margin-top: 40px;
}
</style>
</head>
<body>
<a href="index.html" class="home-btn" title="Zurück zur Startseite">🏠 Startseite</a>
<button class="theme-toggle" id="themeToggle" aria-label="Theme wechseln">🌙</button>
<div class="container">
<a href="index.html" class="back-link">← Zurück zur Übersicht</a>
<h1>🃏 JGA-Trinkspiel</h1>
<p class="subtitle">Klassische Trinkspiele für unterwegs — fair, ohne Spielkarten, ohne Sucheritis</p>
<div class="layout">
<!-- Linke Spalte: Spiel-Auswahl -->
<aside class="panel">
<h2>Spiel wählen</h2>
<div class="game-list" id="gameList">
<button class="game-btn active" data-game="kings"><span class="icon">👑</span><span>Kings</span></button>
<button class="game-btn" data-game="ride"><span class="icon">🚌</span><span>Ride the Bus</span></button>
<button class="game-btn" data-game="ring"><span class="icon">🔥</span><span>Ring of Fire</span></button>
<button class="game-btn" data-game="never"><span class="icon">🚫</span><span>Ich hab noch nie</span></button>
<button class="game-btn" data-game="truth"><span class="icon">🎯</span><span>Wahrheit/Pflicht</span></button>
</div>
</aside>
<!-- Mitte: Karten-Deck -->
<main class="center">
<div class="game-info">
<h3 id="gameTitle">👑 Kings</h3>
<p id="gameDescription">Ziehe Karten, führe Aktionen aus. Wer alle 4 Könige zieht, trinkt den König-Becher leer.</p>
</div>
<div class="deck-area" id="deckArea">
<div class="deck-stack"></div>
<div class="card-wrap" id="cardWrap">
<div class="card-face card-back">
<span class="logo">🃏</span>
</div>
<div class="card-face card-front" id="cardFront">
<div class="suit" id="cardSuit"></div>
<div class="rank" id="cardRank">A</div>
<div class="action" id="cardAction">Trinke 1 Schluck</div>
<div class="who" id="cardWho"></div>
</div>
</div>
</div>
<!-- Ring of Fire: King-Becher Meter -->
<div class="king-meter" id="kingMeter" style="display:none;">
<div class="label">
<span>👑 König-Becher</span>
<span id="kingCount">0 / 4</span>
</div>
<div class="king-bar"><div class="king-fill" id="kingFill" style="width:0%"></div></div>
</div>
<div class="deck-controls">
<button class="btn" id="drawBtn">🎴 Karte ziehen</button>
<button class="btn btn-secondary" id="resetBtn">🔄 Neues Spiel</button>
</div>
<div class="draw-status" id="drawStatus">Karten im Deck: 52</div>
<!-- History -->
<div style="width:100%; max-width:600px;">
<h3 style="font-size:14px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-muted); margin:30px 0 8px;">📜 Gezogene Karten</h3>
<div class="history" id="history">
<div class="empty">Noch keine Karten gezogen</div>
</div>
</div>
</main>
<!-- Rechte Spalte: Spieler + Regeln + Stats -->
<aside>
<div class="panel" style="margin-bottom:16px;">
<h2>👥 Spieler</h2>
<div class="player-list" id="playerList"></div>
<div class="add-player">
<input type="text" id="playerInput" placeholder="Name..." maxlength="20">
<button id="addPlayerBtn">+</button>
</div>
</div>
<div class="panel" style="margin-bottom:16px;">
<h2>📜 Aktive Regeln</h2>
<ul class="rules-list" id="rulesList">
<li class="empty" style="border:0; padding:8px;">Noch keine Regeln — König ziehen!</li>
</ul>
</div>
<div class="panel">
<h2>📊 Statistik</h2>
<div class="stats">
<div class="stat"><div class="num" id="statCards">0</div><div class="label">Karten</div></div>
<div class="stat"><div class="num" id="statDrinks">0</div><div class="label">Schlucke</div></div>
<div class="stat"><div class="num" id="statRules">0</div><div class="label">Regeln</div></div>
<div class="stat"><div class="num" id="statBroken">0</div><div class="label">Verstöße</div></div>
</div>
</div>
</aside>
</div>
<p class="footer-note">Spiel wird automatisch in localStorage gespeichert · Nur für den JGA — bleibt unter uns 🤫</p>
</div>
<!-- Modal: Spieler-Auswahl für "Gib an einen Spieler"-Aktionen -->
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<h3 id="modalTitle">Wähle einen Spieler</h3>
<div class="player-list" id="modalPlayers"></div>
<div class="modal-actions">
<button class="btn btn-secondary" id="modalCancel">Abbrechen</button>
</div>
</div>
</div>
<script>
/* ===== Theme Toggle ===== */
const themeToggle = document.getElementById('themeToggle');
function setTheme(mode) {
document.documentElement.setAttribute('data-theme', mode);
localStorage.setItem('jga-theme', mode);
themeToggle.textContent = mode === 'dark' ? '🌙' : '☀️';
}
themeToggle.addEventListener('click', () => {
const cur = document.documentElement.getAttribute('data-theme');
setTheme(cur === 'dark' ? 'light' : 'dark');
});
// Init button icon
setTheme(localStorage.getItem('jga-theme') || 'dark');
/* ===== Daten ===== */
const SUITS = [
{ symbol: '♠', name: 'Pik', color: 'black' },
{ symbol: '♥', name: 'Herz', color: 'red' },
{ symbol: '♦', name: 'Karo', color: 'red' },
{ symbol: '♣', name: 'Kreuz', color: 'black' }
];
const RANKS = ['A','2','3','4','5','6','7','8','9','10','J','Q','K'];
const AVATARS = ['😎','🤠','🥳','🤩','😈','🧔','👨','👩','🧑','🦸','🥷','🤡','👻','🎅'];
/* ===== Spiel-Konfigurationen ===== */
const GAMES = {
kings: {
title: '👑 Kings',
desc: 'Ziehe Karten, führe Aktionen aus. Wer alle 4 Könige zieht, trinkt den König-Becher leer.',
cardActions: (rank, suit) => {
const base = {
'2': { who: 'self', text: 'Gib 2 Schluck an einen Spieler', count: 2, target: 'pick' },
'3': { who: 'self', text: 'Trinke 3 Schluck', count: 3, target: 'self' },
'4': { who: 'last', text: 'Letzter zum Boden-Berühren trinkt 4', count: 4, target: 'game' },
'5': { who: 'all', text: 'Kategorie wählen — wer nicht antwortet, trinkt', count: 1, target: 'category' },
'6': { who: 'last', text: 'Daumen auf den Tisch — Letzter trinkt 6', count: 6, target: 'game' },
'7': { who: 'all', text: 'Höchste Hand zeigen — Verlierer trinkt 7', count: 7, target: 'game' },
'8': { who: 'self', text: 'Wähle einen Trink-Partner — ab jetzt immer mit', count: 0, target: 'partner' },
'9': { who: 'self', text: 'Trink-Sprache wählen — alle reden 1 Min so', count: 0, target: 'language' },
'10': { who: 'self', text: 'Geschichte erzählen — wer stockt, trinkt 10', count: 10, target: 'self' },
'J': { who: 'self', text: 'Mach ein Tier-Geräusch — Gruppe entscheidet', count: 0, target: 'task' },
'Q': { who: 'self', text: 'Quizmaster — falsche Antwort = 5 Schluck', count: 0, target: 'quizmaster' },
'K': { who: 'self', text: '👑 Erfinde eine Regel (bis zum nächsten König)', count: 0, target: 'rule' },
'A': { who: 'all', text: '💧 Wasserfall — alle trinken gleichzeitig', count: 999, target: 'waterfall' }
};
return base[rank];
},
useKingMeter: true
},
ride: {
title: '🚌 Ride the Bus',
desc: '4 Runden, je mehr Karten du ziehst desto mehr riskierst du. Verlierer trinkt.',
cardActions: () => ({ who: 'self', text: 'Niedrigste Karte verliert — trinkt Schlucke', count: 1, target: 'round' }),
useKingMeter: false
},
ring: {
title: '🔥 Ring of Fire',
desc: '52 Karten im Kreis um den König-Becher. Jede Karte hat eine Regel.',
cardActions: (rank, suit) => {
const r = rank;
const c = suit.color;
if (c === 'red') {
if (r === 'A') return { who: 'self', text: 'Trinke 1 Schluck', count: 1 };
if (r === 'K') return { who: 'self', text: '👑 König-Becher füllen!', count: 0, target: 'king-fill' };
if (r === 'Q') return { who: 'all', text: 'Quizmaster — wer falsch antwortet trinkt', count: 0, target: 'quizmaster' };
if (r === 'J') return { who: 'self', text: 'Mach eine Regel auf', count: 0, target: 'rule' };
// Zahlenkarten
const n = parseInt(r);
return { who: 'self', text: `Trinke ${n} Schluck${n>1?'e':''}`, count: n };
} else {
// Schwarz
const map = {
'A': { who: 'across', text: 'Gegenüber trinkt 1', count: 1, target: 'across' },
'2': { who: 'self', text: 'Du trinkst 2', count: 2 },
'3': { who: 'right', text: 'Rechts trinkt 3', count: 3, target: 'right' },
'4': { who: 'left', text: 'Links trinkt 4', count: 4, target: 'left' },
'5': { who: 'all', text: 'Wasserfall — Reihe ums Trinken', count: 999, target: 'waterfall' },
'6': { who: 'last', text: 'Daumen auf Tisch — Letzter trinkt 6', count: 6, target: 'game' },
'7': { who: 'all', text: 'Wer zuletzt trinkt, trinkt nochmal 7', count: 7, target: 'reverse-game' },
'8': { who: 'self', text: 'Mach ein Foto von der Runde', count: 0, target: 'photo' },
'9': { who: 'all', text: 'Sozial — alle trinken mit', count: 1 },
'10':{ who: 'self', text: 'Quizmaster', count: 0, target: 'quizmaster' },
'J': { who: 'self', text: 'Erfand eine Regel', count: 0, target: 'rule' },
'Q': { who: 'self', text: 'Becher füllen', count: 0, target: 'king-fill' },
'K': { who: 'all', text: 'Trink aus dem König-Becher', count: 0, target: 'king-drink' }
};
return map[r];
}
},
useKingMeter: true
},
never: {
title: '🚫 Ich hab noch nie',
desc: 'Eine Aussage, alle die es schon gemacht haben trinken.',
cardActions: (rank, suit) => {
// Wir nutzen die Karten als Zufalls-Picker aus Datenbank
const prompts = [
'noch nie einen Stripclub besucht',
'noch nie im Casino gespielt',
'noch nie bei Rot über die Ampel',
'noch ne beim Schwimmen gepinkelt',
'noch nie in der Öffentlichkeit gesungen',
'noch nie beim Film im Kino eingeschlafen',
'noch nie fremdes Essen aus dem Kühlschrank',
'noch nie eine 1 auf dem Zeugnis',
'noch nie einen Strafzettel bekommen',
'noch ne im Bus die Notbremse gezogen',
'noch nie beim Date die Wohnung verwüstet',
'noch nie den falschen Namen gerufen',
'noch nie nach Alkohol im Bett eingeschlafen',
'noch nie beim Joggen das Handy verloren',
'noch nie beim Zahnarzt geweint',
'noch nie im Stau auf der Autobahn gepinkelt',
'noch ne einen Liebesbrief geschrieben',
'noch nie einen Joint geraucht',
'noch nie alleine im Restaurant gegessen',
'noch ne den Zug verpasst',
'noch nie ein Haustier zum Tierarzt',
'noch nie beim Kochen die Küche abgefackelt',
'noch nie auf einer Beerdigung gelacht',
'noch ne beim Wandern verlaufen',
'noch nie bei der Arbeit geweint'
];
const idx = parseInt(rank === 'A' ? 1 : rank === 'J' ? 11 : rank === 'Q' ? 12 : rank === 'K' ? 13 : rank) - 1;
return { who: 'all', text: `Ich hab noch nie ${prompts[idx % prompts.length]}`, count: 1, target: 'never' };
},
useKingMeter: false
},
truth: {
title: '🎯 Wahrheit oder Pflicht',
desc: 'Wähle Wahrheit (Frage beantworten) oder Pflicht (Aufgabe machen). Verweigerung = Trinken.',
cardActions: (rank, suit) => {
const truths = [
'Was ist dein peinlichstes Erlebnis auf einer Party?',
'Wer war dein erster Crush?',
'Was ist das Verrückteste, das du je getrunken hast?',
'Hast du jemals im Bett etwas gemacht, das du bereut hast?',
'Was ist dein geheimer Fetisch?',
'Was war dein dümmster Unfall?',
'Was war der letzte peinliche Moment?',
'Hast du jemals einen Falschtext geschickt?',
'Wer ist dein "Celebrity Free Pass"?',
'Was ist das Schlimmste, das du je gegessen hast?',
'Was war dein unangenehmster Arztbesuch?',
'Was war dein erster Kuss?',
'Wer war dein schlimmster Date?',
'Was ist dein geheimer Traum?'
];
const dares = [
'Imitate einen Porn-Star',
'Schicke deiner Chefin eine alberne SMS',
'Trinke ein Glas Wasser in 5 Sekunden',
'Tanze 30 Sekunden ohne Musik',
'Mach 10 Liegestütze',
'Lies den letzten Chat-Verlauf laut vor',
'Mach ein Selfie mit der absurdesten Grimasse',
'Trinke ein Getränk auf ex',
'Singe den Refrain deines Lieblingslieds',
'Laufe 10 Sekunden wie ein Model',
'Erzähle einen Witz',
'Mach einen Handstand',
'Spiele 30 Sekunden Luftgitarre',
'Iss einen Löffel Senf'
];
const idx = parseInt(rank === 'A' ? 1 : rank === 'J' ? 11 : rank === 'Q' ? 12 : rank === 'K' ? 13 : rank) - 1;
const isTruth = suit.color === 'red';
return { who: 'self', text: isTruth ? `Wahrheit: ${truths[idx % truths.length]}` : `Pflicht: ${dares[idx % dares.length]}`, count: 0, target: 'truth-dare', prompt: isTruth ? 'truth' : 'dare' };
},
useKingMeter: false
}
};
/* ===== State ===== */
const state = {
game: 'kings',
players: JSON.parse(localStorage.getItem('jga-players-v2') || '[]'),
rules: JSON.parse(localStorage.getItem('jga-rules-v2') || '[]'),
history: JSON.parse(localStorage.getItem('jga-history-v2') || '[]'),
kingCount: parseInt(localStorage.getItem('jga-king-count') || '0'),
brokenRules: parseInt(localStorage.getItem('jga-broken-rules') || '0')
};
// Default-Spieler wenn leer
if (state.players.length === 0) {
state.players = [
{ name: 'Bräutigam', avatar: '🤵', drinks: 0 },
{ name: 'Trauzeuge', avatar: '🤠', drinks: 0 }
];
savePlayers();
}
/* ===== Helpers ===== */
function save() {
localStorage.setItem('jga-players-v2', JSON.stringify(state.players));
localStorage.setItem('jga-rules-v2', JSON.stringify(state.rules));
localStorage.setItem('jga-history-v2', JSON.stringify(state.history));
localStorage.setItem('jga-king-count', state.kingCount.toString());
localStorage.setItem('jga-broken-rules', state.brokenRules.toString());
}
function savePlayers() { localStorage.setItem('jga-players-v2', JSON.stringify(state.players)); }
function randAvatar() { return AVATARS[Math.floor(Math.random() * AVATARS.length)]; }
function buildDeck() {
const deck = [];
for (const suit of SUITS) {
for (const rank of RANKS) {
deck.push({ rank, suit });
}
}
return deck.sort(() => Math.random() - 0.5);
}
let currentDeck = buildDeck();
let currentCard = null;
let currentPlayer = null;
/* ===== Render ===== */
function renderPlayers() {
const list = document.getElementById('playerList');
if (state.players.length === 0) {
list.innerHTML = '<div class="empty">Noch keine Spieler — füge welche hinzu</div>';
return;
}
list.innerHTML = state.players.map((p, i) => `
<div class="player ${p.drinks >= 10 ? 'drunk' : ''}">
<span class="avatar">${p.avatar}</span>
<span class="name">${escapeHtml(p.name)}</span>
<span class="drinks">${p.drinks}</span>
<button class="remove" data-idx="${i}" title="Entfernen">✕</button>
</div>
`).join('');
list.querySelectorAll('.remove').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx);
state.players.splice(idx, 1);
savePlayers();
renderPlayers();
renderStats();
});
});
}
function renderRules() {
const list = document.getElementById('rulesList');
if (state.rules.length === 0) {
list.innerHTML = '<li class="empty" style="border:0; padding:8px;">Noch keine Regeln — König ziehen!</li>';
return;
}
list.innerHTML = state.rules.map((r, i) => `
<li>
<span class="rule-text">
${escapeHtml(r.text)}
<div class="rule-meta">von ${escapeHtml(r.author)} · ${r.broken}x gebrochen</div>
</span>
<button data-idx="${i}" title="Regel aufheben">✕</button>
</li>
`).join('');
list.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => {
state.rules.splice(parseInt(btn.dataset.idx), 1);
save();
renderRules();
renderStats();
});
});
}
function renderStats() {
const totalDrinks = state.players.reduce((sum, p) => sum + p.drinks, 0);
document.getElementById('statCards').textContent = state.history.length;
document.getElementById('statDrinks').textContent = totalDrinks;
document.getElementById('statRules').textContent = state.rules.length;
document.getElementById('statBroken').textContent = state.brokenRules;
}
function renderHistory() {
const hist = document.getElementById('history');
if (state.history.length === 0) {
hist.innerHTML = '<div class="empty">Noch keine Karten gezogen</div>';
return;
}
hist.innerHTML = state.history.slice().reverse().slice(0, 30).map(h => {
const colorClass = h.suitColor === 'red' ? 'red' : 'black';
return `
<div class="history-item ${colorClass}">
<span class="mini-card">${h.suitSymbol}${h.rank}</span>
<span>${escapeHtml(h.action)}</span>
<span class="who-text">${escapeHtml(h.who)}</span>
</div>
`;
}).join('');
}
function renderKingMeter() {
const cfg = GAMES[state.game];
const meter = document.getElementById('kingMeter');
if (cfg.useKingMeter) {
meter.style.display = 'block';
document.getElementById('kingCount').textContent = `${state.kingCount} / 4`;
document.getElementById('kingFill').style.width = (state.kingCount / 4 * 100) + '%';
} else {
meter.style.display = 'none';
}
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c]));
}
function renderGameInfo() {
const cfg = GAMES[state.game];
document.getElementById('gameTitle').textContent = cfg.title;
document.getElementById('gameDescription').textContent = cfg.desc;
renderKingMeter();
}
/* ===== Karten-Logik ===== */
function drawCard() {
if (currentDeck.length === 0) {
currentDeck = buildDeck();
alert('Deck neu gemischt!');
}
currentCard = currentDeck.pop();
document.getElementById('cardSuit').textContent = currentCard.suit.symbol;
document.getElementById('cardRank').textContent = currentCard.rank;
const cardFront = document.getElementById('cardFront');
cardFront.classList.remove('red', 'black');
cardFront.classList.add(currentCard.suit.color);
// Action bestimmen
const cfg = GAMES[state.game];
const action = cfg.cardActions(currentCard.rank, currentCard.suit);
document.getElementById('cardAction').textContent = action.text;
document.getElementById('cardWho').textContent = action.who === 'self' ? 'DU' : (action.who === 'all' ? 'ALLE' : 'GRUPPE');
// Karten flippen
const wrap = document.getElementById('cardWrap');
wrap.classList.remove('flipped');
// Force reflow
void wrap.offsetWidth;
wrap.classList.add('flipped');
// Speichern in History
currentPlayer = state.players[Math.floor(Math.random() * state.players.length)];
state.history.push({
rank: currentCard.rank,
suitSymbol: currentCard.suit.symbol,
suitColor: currentCard.suit.color,
action: action.text,
who: currentPlayer ? currentPlayer.name : '—'
});
// Trink-Count auf Spieler
if (action.count > 0 && currentPlayer && action.who !== 'all') {
currentPlayer.drinks += action.count;
} else if (action.count > 0 && action.who === 'all') {
state.players.forEach(p => p.drinks += action.count);
}
// Spezial-Aktionen
if (action.target === 'rule') {
const ruleText = prompt('Welche Regel gilt bis zum nächsten König?');
if (ruleText && ruleText.trim()) {
state.rules.push({
text: ruleText.trim(),
author: currentPlayer ? currentPlayer.name : '—',
created: Date.now(),
broken: 0
});
}
}
if (action.target === 'pick') {
showPlayerModal('Wähle einen Spieler — der bekommt ' + action.count + ' Schluck(e)', (picked) => {
picked.drinks += action.count;
savePlayers();
renderPlayers();
});
}
if (action.target === 'partner') {
showPlayerModal('Wähle deinen Trink-Partner', (picked) => {
alert(`${picked.name} ist jetzt dein Trink-Partner!`);
});
}
// König-Meter (Kings-Spiel)
if (cfg.useKingMeter) {
if (currentCard.rank === 'K') {
state.kingCount++;
if (state.kingCount === 4 && currentPlayer) {
currentPlayer.drinks += 20; // König-Becher
alert(`👑👑👑👑 ${currentPlayer.name} hat den 4. König gezogen — König-Becher leertrinken! +20 Schlucke`);
state.kingCount = 0;
}
}
save();
renderKingMeter();
}
save();
renderPlayers();
renderHistory();
renderStats();
document.getElementById('drawStatus').textContent = `Karten im Deck: ${currentDeck.length}`;
}
/* ===== Modal ===== */
function showPlayerModal(title, callback) {
const overlay = document.getElementById('modalOverlay');
document.getElementById('modalTitle').textContent = title;
const list = document.getElementById('modalPlayers');
list.innerHTML = state.players.map((p, i) => `
<div class="player" style="cursor:pointer;" data-idx="${i}">
<span class="avatar">${p.avatar}</span>
<span class="name">${escapeHtml(p.name)}</span>
<span class="drinks">${p.drinks}</span>
</div>
`).join('');
list.querySelectorAll('.player').forEach(el => {
el.addEventListener('click', () => {
const idx = parseInt(el.dataset.idx);
callback(state.players[idx]);
overlay.classList.remove('open');
});
});
overlay.classList.add('open');
}
document.getElementById('modalCancel').addEventListener('click', () => {
document.getElementById('modalOverlay').classList.remove('open');
});
/* ===== Events ===== */
document.querySelectorAll('.game-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.game-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.game = btn.dataset.game;
currentDeck = buildDeck();
state.kingCount = 0;
save();
renderGameInfo();
document.getElementById('cardWrap').classList.remove('flipped');
document.getElementById('drawStatus').textContent = `Karten im Deck: ${currentDeck.length}`;
});
});
document.getElementById('drawBtn').addEventListener('click', drawCard);
document.getElementById('cardWrap').addEventListener('click', drawCard);
document.getElementById('resetBtn').addEventListener('click', () => {
if (confirm('Wirklich neues Spiel starten? Spieler bleiben, Stats werden zurückgesetzt.')) {
currentDeck = buildDeck();
state.history = [];
state.kingCount = 0;
state.brokenRules = 0;
state.players.forEach(p => p.drinks = 0);
document.getElementById('cardWrap').classList.remove('flipped');
save();
renderPlayers();
renderHistory();
renderStats();
renderKingMeter();
document.getElementById('drawStatus').textContent = `Karten im Deck: ${currentDeck.length}`;
}
});
document.getElementById('addPlayerBtn').addEventListener('click', addPlayer);
document.getElementById('playerInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') addPlayer();
});
function addPlayer() {
const input = document.getElementById('playerInput');
const name = input.value.trim();
if (!name) return;
state.players.push({ name, avatar: randAvatar(), drinks: 0 });
input.value = '';
savePlayers();
renderPlayers();
renderStats();
}
/* ===== Init ===== */
renderPlayers();
renderRules();
renderStats();
renderHistory();
renderGameInfo();
document.getElementById('drawStatus').textContent = `Karten im Deck: ${currentDeck.length}`;
/* ===== Service Worker ===== */
// PWA Service Worker — Network-First Strategie (Updates kommen sofort durch)
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./sw.js')
.then(reg => {
console.log('[PWA] SW registered:', reg.scope);
// Update-Hook: wenn ein neuer SW installiert wurde, kurz reloaden
reg.addEventListener('updatefound', () => {
const newSw = reg.installing;
if (!newSw) return;
newSw.addEventListener('statechange', () => {
if (newSw.state === 'activated' && navigator.serviceWorker.controller) {
console.log('[PWA] Neue Version aktiviert — lade automatisch neu');
window.location.reload();
}
});
});
})
.catch(err => console.warn('[PWA] SW registration failed:', err));
});
}
</script>
</body>
</html>