Bei neuem Service Worker wird die Seite automatisch neu geladen. Kein Popup mehr für die User.
809 lines
33 KiB
HTML
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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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> |