Files
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

809 lines
33 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-Strafen-Generator — 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>
: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;
--tier-common: #22c55e;
--tier-uncommon: #eab308;
--tier-rare: #f97316;
--tier-legendary: #ef4444;
--shadow: 0 4px 16px rgba(0,0,0,0.35);
--shadow-strong: 0 8px 32px rgba(0,0,0,0.5);
}
[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;
--shadow: 0 4px 16px rgba(0,0,0,0.08);
--shadow-strong: 0 8px 32px rgba(0,0,0,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: 1200px; margin: 0 auto; padding: 24px 24px 60px; }
h1 { font-size: 36px; margin: 0 0 8px; 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; }
.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: 1fr 320px;
gap: 20px;
}
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
}
/* Card display */
.stage {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
}
.target-picker {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 18px;
}
.target-btn {
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-radius: 999px;
padding: 8px 14px;
font-size: 13px;
color: var(--text);
cursor: pointer;
transition: all 0.2s;
}
.target-btn.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
font-weight: 600;
}
.target-btn:hover { border-color: var(--accent); }
.strafe-card {
width: 100%;
max-width: 480px;
min-height: 380px;
background: var(--card-bg);
border: 2px solid var(--card-border);
border-radius: 20px;
padding: 32px 28px;
text-align: center;
box-shadow: var(--shadow-strong);
position: relative;
transition: all 0.3s;
display: flex;
flex-direction: column;
justify-content: center;
}
.strafe-card.spinning { animation: spin 1s ease-in-out; }
@keyframes spin {
0% { transform: rotateY(0deg) scale(1); }
50% { transform: rotateY(720deg) scale(0.7); }
100% { transform: rotateY(1440deg) scale(1); }
}
.tier-badge {
display: inline-block;
padding: 6px 16px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 18px;
}
.tier-common { background: rgba(34,197,94,0.18); color: var(--tier-common); border: 1px solid var(--tier-common); }
.tier-uncommon { background: rgba(234,179,8,0.18); color: var(--tier-uncommon); border: 1px solid var(--tier-uncommon); }
.tier-rare { background: rgba(249,115,22,0.18); color: var(--tier-rare); border: 1px solid var(--tier-rare); }
.tier-legendary {
background: rgba(239,68,68,0.18);
color: var(--tier-legendary);
border: 1px solid var(--tier-legendary);
animation: glow 1.6s infinite;
}
@keyframes glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.4); }
50% { box-shadow: 0 0 20px 4px rgba(239,68,68,0.5); }
}
.strafe-title { font-size: 28px; font-weight: 800; margin: 0 0 12px; color: var(--text-strong); }
.strafe-desc { font-size: 17px; line-height: 1.5; margin: 0 0 18px; color: var(--text); }
.strafe-rules {
font-size: 13px;
color: var(--text-muted);
border-top: 1px solid var(--card-border);
padding-top: 14px;
margin-top: 14px;
line-height: 1.5;
}
.strafe-meta {
display: flex;
gap: 14px;
justify-content: center;
font-size: 12px;
color: var(--text-muted);
margin-top: 14px;
}
.empty-card {
color: var(--text-muted);
font-style: italic;
}
.actions {
display: flex;
gap: 10px;
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
}
.btn {
background: var(--accent);
color: #fff;
border: 0;
border-radius: 12px;
padding: 14px 28px;
font-size: 16px;
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, #ef4444); }
.btn-large { font-size: 18px; padding: 16px 36px; }
.counter {
margin-top: 16px;
color: var(--text-muted);
font-size: 14px;
}
.counter strong { color: var(--text-strong); font-size: 16px; }
/* Sidebar */
.sidebar { display: flex; flex-direction: column; gap: 16px; }
.panel {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 16px;
}
.panel h2 {
margin: 0 0 12px;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
font-weight: 600;
}
.pool-filters {
display: flex;
gap: 6px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.pool-filter {
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-radius: 6px;
padding: 4px 10px;
font-size: 12px;
cursor: pointer;
color: var(--text);
}
.pool-filter.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.pool-list {
max-height: 360px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 6px;
}
.pool-item {
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 8px 10px;
display: flex;
gap: 8px;
align-items: center;
font-size: 13px;
}
.pool-item.disabled { opacity: 0.4; }
.pool-item input[type="checkbox"] { cursor: pointer; }
.pool-item .tier-dot {
width: 8px; height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.pool-item .text { flex: 1; line-height: 1.3; }
.pool-item .delete {
background: transparent;
border: 0;
color: var(--text-muted);
cursor: pointer;
font-size: 14px;
padding: 0 2px;
}
.pool-item .delete:hover { color: var(--tier-legendary); }
.add-strafe {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.add-strafe input, .add-strafe select, .add-strafe textarea {
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 8px 10px;
color: var(--text);
font-size: 13px;
width: 100%;
font-family: inherit;
}
.add-strafe textarea { resize: vertical; min-height: 50px; }
.add-strafe button { margin-top: 4px; }
/* Stats */
.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: 10px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }
/* History */
.history-list {
max-height: 240px;
overflow-y: auto;
}
.history-item {
background: var(--card-bg-2);
border: 1px solid var(--card-border);
border-left: 3px solid var(--accent);
border-radius: 6px;
padding: 8px 10px;
margin-bottom: 6px;
font-size: 13px;
display: flex;
gap: 8px;
}
.history-item.tier-common { border-left-color: var(--tier-common); }
.history-item.tier-uncommon { border-left-color: var(--tier-uncommon); }
.history-item.tier-rare { border-left-color: var(--tier-rare); }
.history-item.tier-legendary { border-left-color: var(--tier-legendary); }
.history-item .h-title { font-weight: 600; color: var(--text-strong); }
.history-item .h-meta { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.empty { text-align: center; color: var(--text-muted); font-style: italic; padding: 16px; font-size: 13px; }
/* Tier-Legende */
.tier-legend {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
font-size: 11px;
color: var(--text-muted);
}
.tier-legend .row { display: flex; align-items: center; gap: 6px; }
.tier-legend .dot {
width: 10px; height: 10px; border-radius: 50%;
}
</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-Strafen-Generator</h1>
<p class="subtitle">Würfelt zufällig eine Strafe aus dem Pool — leicht versaut, mit Edit-Möglichkeit</p>
<div class="layout">
<!-- Stage -->
<div class="stage">
<!-- Ziel-Person -->
<div class="target-picker" id="targetPicker">
<button class="target-btn active" data-target="braeutigam">🤵 Bräutigam</button>
<button class="target-btn" data-target="random">🎲 Zufällig</button>
<button class="target-btn" data-target="gruppe">👥 Gruppe</button>
</div>
<!-- Strafen-Karte -->
<div class="strafe-card" id="strafeCard">
<div class="empty-card">
<div style="font-size:64px; margin-bottom:12px;">🎲</div>
<p style="font-size:18px;">Klicke <strong>Würfeln</strong> um zu starten</p>
</div>
</div>
<div class="actions">
<button class="btn btn-large" id="rollBtn">🎲 Würfeln</button>
<button class="btn btn-secondary" id="skipBtn" disabled>⏭️ Skip</button>
</div>
<div class="counter">
Gewürfelt: <strong id="rolledCount">0</strong> / <strong id="totalCount">0</strong> aktivierten Strafen
</div>
<!-- History -->
<div style="width:100%; max-width:600px; margin-top:32px;">
<h3 style="font-size:14px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-muted); margin:0 0 12px;">📜 Würfel-Historie</h3>
<div class="history-list" id="history">
<div class="empty">Noch nichts gewürfelt</div>
</div>
</div>
</div>
<!-- Sidebar -->
<aside class="sidebar">
<div class="panel">
<h2>📊 Statistik</h2>
<div class="stats">
<div class="stat"><div class="num" id="statTotal">0</div><div class="label">Gewürfelt</div></div>
<div class="stat"><div class="num" id="statSkipped">0</div><div class="label">Geskippt</div></div>
<div class="stat"><div class="num" id="statLegendary">0</div><div class="label">Legendär</div></div>
<div class="stat"><div class="num" id="statPoolSize">0</div><div class="label">Im Pool</div></div>
</div>
</div>
<div class="panel">
<h2>⚖️ Tier-Wahrscheinlichkeiten</h2>
<div class="tier-legend">
<div class="row"><span class="dot" style="background:var(--tier-common);"></span> 60% Common</div>
<div class="row"><span class="dot" style="background:var(--tier-uncommon);"></span> 25% Uncommon</div>
<div class="row"><span class="dot" style="background:var(--tier-rare);"></span> 10% Rare</div>
<div class="row"><span class="dot" style="background:var(--tier-legendary);"></span> 5% Legendary</div>
</div>
</div>
<div class="panel">
<h2>📦 Strafen-Pool</h2>
<div class="pool-filters" id="poolFilters">
<button class="pool-filter active" data-filter="all">Alle</button>
<button class="pool-filter" data-filter="common">🟢 Common</button>
<button class="pool-filter" data-filter="uncommon">🟡 Uncommon</button>
<button class="pool-filter" data-filter="rare">🟠 Rare</button>
<button class="pool-filter" data-filter="legendary">🔴 Legendary</button>
</div>
<div class="pool-list" id="poolList"></div>
<div class="add-strafe">
<input type="text" id="newTitle" placeholder="Titel (z.B. Brautschuh-Shot)">
<textarea id="newDesc" placeholder="Beschreibung (Was muss gemacht werden?)"></textarea>
<select id="newTier">
<option value="common">🟢 Common (60%)</option>
<option value="uncommon">🟡 Uncommon (25%)</option>
<option value="rare">🟠 Rare (10%)</option>
<option value="legendary">🔴 Legendary (5%)</option>
</select>
<button class="btn btn-secondary" id="addBtn">+ Eigene Strafe hinzufügen</button>
</div>
</div>
</aside>
</div>
</div>
<script>
/* ===== Theme ===== */
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');
});
setTheme(localStorage.getItem('jga-theme') || 'dark');
/* ===== Default-Strafen-Pool ===== */
const DEFAULT_STRAFEN = [
// Common (60%)
{ id: 's1', tier: 'common', title: 'Ein Schluck Bier', desc: 'Trinke einen ordentlichen Schluck Bier.', rules: 'Standard', forWhom: ['braeutigam','gruppe','random'], enabled: true },
{ id: 's2', tier: 'common', title: 'Wahrheitsfrage', desc: 'Beantworte eine peinliche Wahrheitsfrage aus der Gruppe.', rules: 'Verweigerung = 3 Schlucke', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's3', tier: 'common', title: 'Witz erzählen', desc: 'Erzähle einen Witz. Gruppe entscheidet ob er lustig war.', rules: 'Witz nicht lustig = 2 Schlucke', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's4', tier: 'common', title: 'Kompliment machen', desc: 'Mach einem Fremden auf der Straße ein ehrliches Kompliment.', rules: 'Video-Beweis erforderlich', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's5', tier: 'common', title: 'Tanzperformance', desc: 'Tanze 15 Sekunden zu einem zufälligen Lied.', rules: 'Gruppe darf Lied aussuchen', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's6', tier: 'common', title: 'Lied vorsingen', desc: 'Singe den Refrain eines Liedes laut vor.', rules: 'Refrain reicht — Strophe muss nicht', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's7', tier: 'common', title: 'Tiergeräusch', desc: 'Mach ein überzeugendes Tiergeräusch.', rules: 'Gruppen-Jury entscheidet', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's8', tier: 'common', title: 'Kniebeugen', desc: 'Mach 10 Kniebeugen.', rules: 'Nicht vollständig = nochmal', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's9', tier: 'common', title: 'Reim auf Prag', desc: 'Finde einen Reim auf "Prag" — Gruppe urteilt.', rules: 'Kein Reim = 2 Schlucke', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's10', tier: 'common', title: 'Selfie mit Passant', desc: 'Mach ein Selfie mit einem wildfremden Passanten.', rules: 'Nur ein Foto, kurze Interaktion', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's11', tier: 'common', title: 'Akzent-Wette', desc: 'Rede die nächsten 10 Minuten in einem lustigen Akzent.', rules: 'Vergessen = 1 Schluck', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's12', tier: 'common', title: 'Daumen-Duell', desc: 'Spiele Daumen-Duell gegen jeden in der Runde.', rules: 'Verlierer trinkt', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's13', tier: 'common', title: 'Kuss auf die Stirn', desc: 'Küsse einen fremden Mann auf die Stirn.', rules: 'Nur mit Einwilligung', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's14', tier: 'common', title: 'Schuh ausziehen', desc: 'Laufe die nächsten 5 Minuten barfuß.', rules: 'Öffentlich ok', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's15', tier: 'common', title: 'Bier-Yoga', desc: 'Mache 5 Yoga-Übungen während du ein volles Bier balancierst.', rules: 'Verschütten = 5 Schlucke', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's16', tier: 'common', title: 'Lügengeschichte', desc: 'Erzähle eine absurde Geschichte — Gruppe rät was gelogen ist.', rules: '2 falsch geraten = trinkst du', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's17', tier: 'common', title: 'Trink-Sprache', desc: 'Rede 5 Minuten in einer erfundenen Sprache.', rules: 'Nur Englisch/Deutsch verboten', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's18', tier: 'common', title: 'Hulahoop', desc: 'Hulahoop mit der Hüfte für 30 Sekunden.', rules: 'Unter 30s = nochmal', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's19', tier: 'common', title: 'Schwiegermutter-Witz', desc: 'Erzähle einen Schwiegermutter-Witz.', rules: 'Nicht lustig = 3 Schlucke', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's20', tier: 'common', title: 'Falsche Handynummer', desc: 'Gib einem Fremden eine falsche Handynummer.', rules: 'Drama vermeiden', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's21', tier: 'common', title: 'Straßenmusiker', desc: 'Sing ein Lied auf der Straße.', rules: 'Nur 30 Sekunden', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's22', tier: 'common', title: 'Unterhose ausziehen', desc: 'Zieh deine Unterhose aus und steck sie in deine Hose.', rules: 'Disco-Effekt', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's23', tier: 'common', title: 'Pantomime', desc: 'Spiele einen Film in Pantomime — Gruppe rät.', rules: '30 Sek. Zeit', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's24', tier: 'common', title: 'Bräutigams-Hymne', desc: 'Singe die Bräutigams-Hymne (Gruppe erfindet Text).', rules: 'Mind. 30 Sek.', forWhom: ['braeutigam'], enabled: true },
// Uncommon (25%)
{ id: 's25', tier: 'uncommon', title: 'Zwei Shots', desc: 'Trinke zwei Shots hintereinander.', rules: 'Auf ex', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's26', tier: 'uncommon', title: 'Wette', desc: 'Gruppe wettet — du musst annehmen.', rules: 'Verlierer trinkt 3 Schlucke', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's27', tier: 'uncommon', title: 'Plank-Challenge', desc: 'Halte 1 Min Plank.', rules: 'Unter 60s = 5 Schlucke', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's28', tier: 'uncommon', title: 'Brautjungfern-Slip kaufen', desc: 'Geh in einen Laden und kauf einen roten Damen-Slip.', rules: 'Quittung erforderlich', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's29', tier: 'uncommon', title: 'T-Shirt-Wechsel', desc: 'Tausche dein Shirt mit einem Fremden.', rules: 'Video-Beweis', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's30', tier: 'uncommon', title: 'Anruf bei den Eltern', desc: 'Ruf deine Mutter an und sag "Ich bin stolz auf dich".', rules: 'Echter Anruf, keine Sprachnachricht', forWhom: ['braeutigam'], enabled: true },
{ id: 's31', tier: 'uncommon', title: 'Peinliches Geständnis', desc: 'Erzähle ein peinliches Erlebnis aus deiner Vergangenheit.', rules: 'Muss wahr sein', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's32', tier: 'uncommon', title: 'Strip-Poker light', desc: 'Spiele eine Runde 17+4 — Verlierer zieht ein Kleidungsstück aus.', rules: 'Maximal bis Unterhose', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's33', tier: 'uncommon', title: 'Baby-Stimme', desc: 'Rede die nächsten 10 Minuten in Baby-Stimme.', rules: 'Vergessen = Schluck', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's34', tier: 'uncommon', title: 'Liebesbrief', desc: 'Schreibe einen 30-Sek-Liebesbrief an eine fremde Person.', rules: 'Muss vorgelesen werden', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's35', tier: 'uncommon', title: 'Strip-Dart', desc: 'Spiele eine Runde Dart — bei jeder Niete ein Kleidungsstück.', rules: 'Maximal bis Unterhose', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's36', tier: 'uncommon', title: 'Karaoke-Trinkspiel', desc: 'Wähle ein peinliches Lied — singe mit vollem Einsatz.', rules: 'Mitsingen Pflicht', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's37', tier: 'uncommon', title: 'Schleim-Curling', desc: 'Curling spielen mit Schleim auf dem Tisch.', rules: 'Selbst gebastelt', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's38', tier: 'uncommon', title: 'Wahrheits-5er-Pack', desc: 'Beantworte 5 Wahrheitsfragen hintereinander.', rules: 'Verweigerung erlaubt = 1 Schluck pro Verweigerung', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's39', tier: 'uncommon', title: 'Liebes-Selfie', desc: 'Mach ein Kuss-Selfie mit einem männlichen Passanten.', rules: 'Wangen reicht', forWhom: ['braeutigam','random'], enabled: true },
// Rare (10%)
{ id: 's40', tier: 'rare', title: 'Fünf Shots', desc: 'Trinke 5 Shots in unter 10 Minuten.', rules: 'Auf ex', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's41', tier: 'rare', title: 'Stadtrallye', desc: 'Löse 3 Aufgaben in der Stadt in unter 30 Minuten.', rules: 'Foto-Beweis pro Aufgabe', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's42', tier: 'rare', title: 'Karaoke-Solo', desc: 'Sing einen ganzen Song alleine vor der Gruppe.', rules: 'Mind. 3 Min', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's43', tier: 'rare', title: 'Straßenkünstler', desc: 'Mach 5 Minuten Straßenunterhaltung (Musik, Comedy, Pantomime).', rules: 'Passanten müssen reagieren', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's44', tier: 'rare', title: 'Sünder-T-Shirt', desc: 'Trage für den Rest des Abends ein selbstgemaltes "Sünder"-Shirt.', rules: 'Sichtbar für alle', forWhom: ['braeutigam'], enabled: true },
{ id: 's45', tier: 'rare', title: 'Liebes-Tattoo-Wette', desc: 'Wette: Wenn du verlierst, lässt du dir ein "I love X" Tattoo stechen.', rules: 'Wette muss angenommen werden', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's46', tier: 'rare', title: 'Strip-Photobombing', desc: 'Schleich dich in 3 fremde Urlaubsfotos.', rules: 'Foto-Beweis', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's47', tier: 'rare', title: 'Wetten-dass-Song', desc: 'Wette mit der Gruppe: Ich kann ein 10-Min-Lied singen ohne Witz.', rules: 'Verloren = 10 Schlucke', forWhom: ['braeutigam'], enabled: true },
// Legendary (5%)
{ id: 's48', tier: 'legendary', title: 'Brautschuh-Shot', desc: 'Trinke einen Shot aus dem Brautschuh des Bräutigams.', rules: 'Wird vom Bräutigam befüllt — auf ex runtertrinken', forWhom: ['braeutigam'], enabled: true },
{ id: 's49', tier: 'legendary', title: 'Strip-Engel', desc: 'Zieh dich bis auf die Unterhose aus und mach den "Engel" im Schnee/auf der Straße.', rules: 'Video-Beweis', forWhom: ['braeutigam','random'], enabled: true },
{ id: 's50', tier: 'legendary', title: 'Public Proposal', desc: 'Mach einem fremden Mädchen auf der Straße einen Heiratsantrag (in einer Fremdsprache).', rules: 'Video-Beweis', forWhom: ['braeutigam','random'], enabled: true }
];
const TIER_WEIGHTS = { common: 0.60, uncommon: 0.25, rare: 0.10, legendary: 0.05 };
const TIER_LABEL = { common: '🟢 COMMON', uncommon: '🟡 UNCOMMON', rare: '🟠 RARE', legendary: '🔴 LEGENDÄR' };
/* ===== State ===== */
const state = {
pool: JSON.parse(localStorage.getItem('jga-strafen-pool') || 'null') || DEFAULT_STRAFEN.map(s => ({...s})),
history: JSON.parse(localStorage.getItem('jga-strafen-history') || '[]'),
filter: 'all',
target: 'braeutigam',
rolled: 0,
skipped: 0,
legendaryRolled: 0,
currentStrafe: null
};
function save() {
localStorage.setItem('jga-strafen-pool', JSON.stringify(state.pool));
localStorage.setItem('jga-strafen-history', JSON.stringify(state.history));
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
/* ===== Würfeln ===== */
function pickTier() {
const r = Math.random();
let acc = 0;
for (const tier of ['common','uncommon','rare','legendary']) {
acc += TIER_WEIGHTS[tier];
if (r < acc) return tier;
}
return 'common';
}
function roll() {
const enabled = state.pool.filter(s => s.enabled && s.forWhom.includes(state.target));
if (enabled.length === 0) {
alert('Keine Strafen aktiviert für diese Ziel-Person!');
return;
}
const tier = pickTier();
let candidates = enabled.filter(s => s.tier === tier);
if (candidates.length === 0) {
// Fallback zu enabled
candidates = enabled;
}
const chosen = candidates[Math.floor(Math.random() * candidates.length)];
state.currentStrafe = chosen;
state.rolled++;
if (chosen.tier === 'legendary') state.legendaryRolled++;
state.history.push({ id: chosen.id, title: chosen.title, tier: chosen.tier, target: state.target, at: Date.now() });
save();
renderCard();
renderStats();
renderHistory();
document.getElementById('skipBtn').disabled = false;
}
function skip() {
state.skipped++;
state.currentStrafe = null;
renderCard();
renderStats();
document.getElementById('skipBtn').disabled = true;
}
/* ===== Render ===== */
function renderCard() {
const card = document.getElementById('strafeCard');
const s = state.currentStrafe;
if (!s) {
card.classList.remove('spinning');
card.innerHTML = `
<div class="empty-card">
<div style="font-size:64px; margin-bottom:12px;">🎲</div>
<p style="font-size:18px;">Klicke <strong>Würfeln</strong> um zu starten</p>
</div>`;
return;
}
card.innerHTML = `
<div class="tier-badge tier-${s.tier}">${TIER_LABEL[s.tier]}</div>
<h2 class="strafe-title">${escapeHtml(s.title)}</h2>
<p class="strafe-desc">${escapeHtml(s.desc)}</p>
<div class="strafe-rules">📋 ${escapeHtml(s.rules || 'Keine weiteren Regeln')}</div>
<div class="strafe-meta">
<span>👤 ${targetLabel(state.target)}</span>
</div>
`;
// Animation kurz
card.classList.remove('spinning');
void card.offsetWidth;
card.classList.add('spinning');
}
function targetLabel(t) {
return { braeutigam: 'Bräutigam', random: 'Zufällig', gruppe: 'Gruppe' }[t] || t;
}
function renderPool() {
const list = document.getElementById('poolList');
const filtered = state.pool.filter(s => state.filter === 'all' || s.tier === state.filter);
if (filtered.length === 0) {
list.innerHTML = '<div class="empty">Keine Strafen in diesem Tier</div>';
return;
}
list.innerHTML = filtered.map(s => `
<div class="pool-item ${s.enabled ? '' : 'disabled'}">
<input type="checkbox" ${s.enabled ? 'checked' : ''} data-id="${s.id}">
<span class="tier-dot" style="background:var(--tier-${s.tier});"></span>
<span class="text">${escapeHtml(s.title)}</span>
<button class="delete" data-del="${s.id}" title="Löschen">✕</button>
</div>
`).join('');
list.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', () => {
const id = cb.dataset.id;
const item = state.pool.find(x => x.id === id);
if (item) {
item.enabled = cb.checked;
save();
renderStats();
}
});
});
list.querySelectorAll('.delete').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.del;
state.pool = state.pool.filter(x => x.id !== id);
save();
renderPool();
renderStats();
});
});
}
function renderStats() {
document.getElementById('rolledCount').textContent = state.rolled;
document.getElementById('totalCount').textContent = state.pool.filter(s => s.enabled).length;
document.getElementById('statTotal').textContent = state.rolled;
document.getElementById('statSkipped').textContent = state.skipped;
document.getElementById('statLegendary').textContent = state.legendaryRolled;
document.getElementById('statPoolSize').textContent = state.pool.length;
}
function renderHistory() {
const hist = document.getElementById('history');
if (state.history.length === 0) {
hist.innerHTML = '<div class="empty">Noch nichts gewürfelt</div>';
return;
}
hist.innerHTML = state.history.slice().reverse().slice(0, 20).map(h => `
<div class="history-item tier-${h.tier}">
<div>
<div class="h-title">${escapeHtml(h.title)}</div>
<div class="h-meta">${TIER_LABEL[h.tier]} · ${targetLabel(h.target)} · ${new Date(h.at).toLocaleTimeString('de-DE', {hour:'2-digit',minute:'2-digit'})}</div>
</div>
</div>
`).join('');
}
/* ===== Events ===== */
document.getElementById('rollBtn').addEventListener('click', roll);
document.getElementById('skipBtn').addEventListener('click', skip);
document.querySelectorAll('.target-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.target-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.target = btn.dataset.target;
if (state.currentStrafe) renderCard();
});
});
document.querySelectorAll('.pool-filter').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.pool-filter').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.filter = btn.dataset.filter;
renderPool();
});
});
document.getElementById('addBtn').addEventListener('click', () => {
const title = document.getElementById('newTitle').value.trim();
const desc = document.getElementById('newDesc').value.trim();
const tier = document.getElementById('newTier').value;
if (!title || !desc) {
alert('Titel und Beschreibung erforderlich');
return;
}
state.pool.push({
id: 'custom-' + Date.now(),
tier,
title,
desc,
rules: 'Selbst erstellt',
forWhom: ['braeutigam','gruppe','random'],
enabled: true
});
document.getElementById('newTitle').value = '';
document.getElementById('newDesc').value = '';
save();
renderPool();
renderStats();
});
/* ===== Init ===== */
renderCard();
renderPool();
renderStats();
renderHistory();
// 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>