edu-boardgame-generator/game.html

1297 lines
55 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title id="pageTitle">Spiel</title>
<link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#0f0e17;--card:#1e1c30;--border:#2e2b4a;
--accent:#f5a623;--accent2:#7c3aed;--accent3:#10b981;
--danger:#ef4444;--text:#fffffe;--muted:#a7a3c2;
--field-size:72px;
--theme-primary:#f5a623;--theme-bg:#0f0e17;--theme-glow:rgba(245,166,35,0.3);
}
html,body{width:100%;height:100%;overflow:hidden;background:#000;}
canvas#bgCanvas{
position:fixed;inset:0;width:100%;height:100%;z-index:0;
}
/* ══ SCREENS ══ */
.screen{
position:fixed;inset:0;z-index:10;
display:flex;flex-direction:column;align-items:center;justify-content:center;
opacity:0;pointer-events:none;transition:opacity 0.6s ease;
overflow:hidden;
}
.screen.active{opacity:1;pointer-events:all;}
/* ══ INTRO ══ */
#introScreen{background:linear-gradient(180deg,rgba(0,0,0,0.85) 0%,rgba(0,0,0,0.6) 100%);}
.intro-inner{text-align:center;padding:32px;max-width:700px;position:relative;z-index:2;}
.intro-particles{position:absolute;inset:0;z-index:0;overflow:hidden;}
.game-badge{
display:inline-block;background:rgba(124,58,237,0.3);border:1px solid rgba(124,58,237,0.6);
color:#c4b5fd;font-size:11px;font-weight:800;letter-spacing:3px;text-transform:uppercase;
padding:6px 18px;border-radius:999px;margin-bottom:24px;
animation:fadeDown 0.8s 0.2s ease both;
}
.intro-title{
font-family:'Fredoka One',cursive;
font-size:clamp(2.8rem,8vw,5.5rem);
line-height:1.05;color:#fff;
animation:titleReveal 1.2s 0.4s cubic-bezier(0.16,1,0.3,1) both;
text-shadow:0 0 60px var(--theme-glow),0 4px 30px rgba(0,0,0,0.5);
position:relative;z-index:1;
}
.intro-title .highlight{color:var(--theme-primary);}
.intro-desc{
font-size:clamp(14px,2vw,17px);color:rgba(255,255,255,0.75);
line-height:1.8;margin:20px auto;max-width:480px;
animation:fadeUp 0.8s 0.8s ease both;opacity:0;
}
.intro-meta{
display:flex;gap:16px;justify-content:center;flex-wrap:wrap;
margin:24px 0;
animation:fadeUp 0.8s 1s ease both;opacity:0;
}
.intro-meta-pill{
background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.15);
border-radius:999px;padding:7px 18px;font-size:13px;font-weight:700;color:rgba(255,255,255,0.8);
display:flex;align-items:center;gap:6px;
}
.btn-start{
background:linear-gradient(135deg,var(--accent2),var(--accent));
color:#fff;font-family:'Fredoka One',cursive;font-size:1.3rem;
border:none;border-radius:16px;padding:18px 52px;cursor:pointer;
margin-top:8px;box-shadow:0 8px 40px rgba(124,58,237,0.5);
transition:transform 0.2s,box-shadow 0.2s;
animation:fadeUp 0.8s 1.2s ease both;opacity:0;
position:relative;overflow:hidden;
}
.btn-start::after{
content:'';position:absolute;inset:0;
background:linear-gradient(135deg,rgba(255,255,255,0.15),transparent);
}
.btn-start:hover{transform:translateY(-3px) scale(1.02);box-shadow:0 16px 50px rgba(124,58,237,0.6);}
.btn-start:active{transform:translateY(0);}
.intro-figure{font-size:5rem;display:block;margin-bottom:16px;animation:floatFig 3s ease-in-out infinite;}
/* ══ GAME SCREEN ══ */
#gameScreen{
background:transparent;
flex-direction:row;
gap:0;
align-items:stretch;
justify-content:stretch;
padding:0;
}
/* HUD TOP */
.hud{
position:fixed;top:0;left:0;right:0;z-index:50;
background:rgba(10,8,22,0.85);backdrop-filter:blur(12px);
border-bottom:1px solid rgba(255,255,255,0.08);
padding:10px 20px;
display:flex;align-items:center;gap:16px;
}
.hud-title{font-family:'Fredoka One',cursive;font-size:1rem;color:var(--text);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.hud-pill{
background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);
border-radius:999px;padding:5px 14px;font-size:12px;font-weight:700;color:var(--muted);
display:flex;align-items:center;gap:5px;white-space:nowrap;
}
.hud-pill .val{color:var(--text);}
.hud-lives{display:flex;gap:3px;font-size:1rem;}
#btnMenu{
background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);
color:var(--muted);border-radius:8px;padding:6px 12px;font-size:12px;font-weight:700;
cursor:pointer;transition:all 0.15s;
}
#btnMenu:hover{background:rgba(255,255,255,0.15);color:var(--text);}
/* BOARD AREA */
.board-area{
position:fixed;inset:0;top:56px;
display:flex;align-items:center;justify-content:center;
padding:20px;
}
#boardCanvas{
border-radius:20px;
box-shadow:0 0 80px rgba(0,0,0,0.6),0 0 0 1px rgba(255,255,255,0.05);
}
/* DICE PANEL */
.dice-panel{
position:fixed;bottom:24px;left:50%;transform:translateX(-50%);
background:rgba(10,8,22,0.9);backdrop-filter:blur(16px);
border:1px solid rgba(255,255,255,0.1);border-radius:20px;
padding:16px 28px;display:flex;align-items:center;gap:20px;
z-index:50;box-shadow:0 8px 40px rgba(0,0,0,0.5);
}
#diceEmoji{font-size:2.8rem;cursor:pointer;transition:transform 0.1s;user-select:none;filter:drop-shadow(0 0 12px var(--theme-glow));}
#diceEmoji:hover:not(.locked){transform:scale(1.15);}
#diceEmoji.rolling{animation:diceAnim 0.5s ease;}
.dice-label{font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:var(--muted);}
#btnRoll{
background:linear-gradient(135deg,var(--accent2),var(--accent));
color:#fff;font-family:'Fredoka One',cursive;font-size:1rem;
border:none;border-radius:12px;padding:12px 28px;cursor:pointer;
box-shadow:0 4px 20px rgba(124,58,237,0.4);
transition:transform 0.15s,box-shadow 0.15s,opacity 0.2s;
}
#btnRoll:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 28px rgba(124,58,237,0.5);}
#btnRoll:disabled{opacity:0.35;cursor:not-allowed;}
/* ══ OVERLAYS ══ */
.overlay{
position:fixed;inset:0;z-index:200;
display:flex;align-items:center;justify-content:center;
padding:20px;
opacity:0;pointer-events:none;transition:opacity 0.35s;
}
.overlay.open{opacity:1;pointer-events:all;}
.overlay-bg{position:absolute;inset:0;background:rgba(0,0,0,0.8);backdrop-filter:blur(8px);}
.overlay-box{
position:relative;z-index:1;
background:rgba(22,20,38,0.97);border:1px solid rgba(255,255,255,0.1);
border-radius:24px;width:100%;max-width:520px;
box-shadow:0 24px 80px rgba(0,0,0,0.7);
transform:scale(0.92);transition:transform 0.35s cubic-bezier(0.16,1,0.3,1);
overflow:hidden;
}
.overlay.open .overlay-box{transform:scale(1);}
/* STORY OVERLAY */
#storyOverlay .overlay-box{border-color:rgba(6,182,212,0.4);max-width:560px;}
.story-ov-header{
background:linear-gradient(135deg,rgba(14,116,144,0.3),rgba(6,182,212,0.1));
padding:28px 28px 20px;text-align:center;border-bottom:1px solid rgba(6,182,212,0.2);
}
.story-ov-emoji{font-size:3.5rem;display:block;margin-bottom:12px;animation:floatFig 3s ease-in-out infinite;}
.story-ov-title{font-family:'Fredoka One',cursive;font-size:1.2rem;color:#06b6d4;}
.story-ov-body{padding:24px 28px;}
.story-ov-text{font-size:15px;font-weight:600;line-height:1.8;color:rgba(255,255,255,0.9);}
.story-ov-btn{
display:block;width:calc(100% - 56px);margin:0 28px 28px;
background:linear-gradient(135deg,#0e7490,#06b6d4);color:#fff;
font-family:'Fredoka One',cursive;font-size:1rem;border:none;
border-radius:12px;padding:14px;cursor:pointer;transition:transform 0.15s;
}
.story-ov-btn:hover{transform:translateY(-2px);}
/* MINIGAME OVERLAY */
#mgOverlay .overlay-box{max-width:640px;}
.mg-ov-header{
padding:20px 24px 0;
display:flex;align-items:center;justify-content:space-between;
}
.mg-ov-title{font-family:'Fredoka One',cursive;font-size:1.4rem;display:flex;align-items:center;gap:10px;}
.mg-ov-close{
background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);
color:var(--muted);width:32px;height:32px;border-radius:8px;cursor:pointer;
font-size:1rem;display:flex;align-items:center;justify-content:center;transition:all 0.15s;
}
.mg-ov-close:hover{border-color:var(--danger);color:var(--danger);}
.mg-ov-body{padding:16px 24px 24px;}
.mg-ov-desc{color:var(--muted);font-size:13px;margin-bottom:12px;line-height:1.6;}
.mg-canvas-wrap{
background:#000;border-radius:12px;overflow:hidden;
display:flex;align-items:center;justify-content:center;
}
.mg-ov-controls{
margin-top:10px;background:rgba(255,255,255,0.04);border-radius:8px;
padding:8px 12px;font-size:12px;color:var(--muted);display:flex;align-items:center;gap:6px;
}
.mg-result-bar{
margin-top:12px;border-radius:12px;padding:14px 16px;text-align:center;display:none;
}
.mg-result-bar.win{background:rgba(16,185,129,0.15);border:1px solid var(--accent3);}
.mg-result-bar.lose{background:rgba(239,68,68,0.1);border:1px solid var(--danger);}
.mg-result-bar h3{font-family:'Fredoka One',cursive;font-size:1.2rem;}
.mg-result-bar.win h3{color:var(--accent3);}
.mg-result-bar.lose h3{color:var(--danger);}
.mg-result-bar p{font-size:13px;color:var(--muted);margin-top:4px;}
#btnMgContinue{
display:none;margin-top:10px;width:100%;
background:linear-gradient(135deg,var(--accent2),var(--accent));
color:#fff;font-family:'Fredoka One',cursive;font-size:1rem;border:none;
border-radius:10px;padding:13px;cursor:pointer;transition:transform 0.15s;
}
#btnMgContinue:hover{transform:translateY(-1px);}
/* RESULT OVERLAY */
#resultOverlay .overlay-box{text-align:center;padding:40px 32px;}
.res-emoji{font-size:5rem;display:block;margin-bottom:16px;}
.res-title{font-family:'Fredoka One',cursive;font-size:2.5rem;margin-bottom:8px;}
.res-sub{color:var(--muted);font-size:15px;margin-bottom:24px;line-height:1.6;}
.res-stats{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-bottom:28px;}
.rs-pill{background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:14px 20px;}
.rs-val{font-family:'Fredoka One',cursive;font-size:1.6rem;color:var(--accent);}
.rs-label{font-size:11px;font-weight:800;text-transform:uppercase;color:var(--muted);margin-top:2px;}
.res-btn{
background:linear-gradient(135deg,var(--accent3),#059669);
color:#fff;font-family:'Fredoka One',cursive;font-size:1.1rem;
border:none;border-radius:14px;padding:16px 40px;cursor:pointer;
box-shadow:0 6px 24px rgba(16,185,129,0.4);transition:transform 0.15s;
}
.res-btn:hover{transform:translateY(-2px);}
/* TRANSITION */
#transOverlay{
position:fixed;inset:0;z-index:500;
background:radial-gradient(ellipse at center,var(--accent2) 0%,#000 100%);
opacity:0;pointer-events:none;transition:opacity 0.4s;
}
#transOverlay.flash{opacity:1;}
/* FLOATING TOAST */
#toast{
position:fixed;top:72px;left:50%;transform:translateX(-50%) translateY(-10px);
background:rgba(22,20,38,0.95);border:1px solid rgba(255,255,255,0.12);
border-radius:12px;padding:10px 20px;font-size:14px;font-weight:700;
color:var(--text);z-index:300;opacity:0;transition:all 0.3s;white-space:nowrap;
backdrop-filter:blur(12px);
}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
@keyframes fadeDown{from{opacity:0;transform:translateY(-20px);}to{opacity:1;transform:translateY(0);}}
@keyframes fadeUp {from{opacity:0;transform:translateY(24px);}to{opacity:1;transform:translateY(0);}}
@keyframes titleReveal{from{opacity:0;transform:scale(0.85) translateY(30px);}to{opacity:1;transform:scale(1) translateY(0);}}
@keyframes floatFig{0%,100%{transform:translateY(0);}50%{transform:translateY(-12px);}}
@keyframes diceAnim{0%{transform:rotate(0) scale(1);}25%{transform:rotate(-30deg) scale(1.3);}75%{transform:rotate(30deg) scale(1.3);}100%{transform:rotate(0) scale(1);}}
@keyframes fieldPop{0%{transform:scale(1);}50%{transform:scale(1.3);}100%{transform:scale(1);}}
</style>
</head>
<body>
<canvas id="bgCanvas"></canvas>
<!-- INTRO -->
<div class="screen active" id="introScreen">
<div class="intro-inner">
<div class="intro-particles" id="introParticles"></div>
<span class="intro-figure" id="introFigure">🎮</span>
<div class="game-badge">Spiel-Generator Workshop</div>
<h1 class="intro-title" id="introTitle">Mein <span class="highlight">Spiel</span></h1>
<p class="intro-desc" id="introDesc">Beschreibung folgt...</p>
<div class="intro-meta" id="introMeta"></div>
<button class="btn-start" onclick="startGame()">🎮 Spiel starten!</button>
</div>
</div>
<!-- GAME -->
<div class="screen" id="gameScreen">
<div class="hud">
<div class="hud-title" id="hudTitle">Spiel</div>
<div class="hud-pill">⬛ Feld <span class="val" id="hudPos">1/10</span></div>
<div class="hud-pill" id="hudLifePill">❤️ <span class="val hud-lives" id="hudLives">❤️❤️❤️</span></div>
<div class="hud-pill" id="hudPtPill" style="display:none"><span class="val" id="hudPts">0</span></div>
<button id="btnMenu" onclick="showMenu()">☰ Menü</button>
</div>
<div class="board-area">
<canvas id="boardCanvas"></canvas>
</div>
<div class="dice-panel">
<div>
<div class="dice-label">Würfeln!</div>
<div id="diceEmoji">🎲</div>
</div>
<button id="btnRoll" onclick="doRoll()">🎲 Würfeln</button>
</div>
</div>
<!-- OVERLAYS -->
<!-- Story -->
<div class="overlay" id="storyOverlay">
<div class="overlay-bg"></div>
<div class="overlay-box">
<div class="story-ov-header">
<span class="story-ov-emoji" id="storyEmoji">📖</span>
<div class="story-ov-title">Erzähl-Text</div>
</div>
<div class="story-ov-body">
<p class="story-ov-text" id="storyText"></p>
</div>
<button class="story-ov-btn" onclick="closeStory()">▶ Weiter</button>
</div>
</div>
<!-- Minigame -->
<div class="overlay" id="mgOverlay">
<div class="overlay-bg"></div>
<div class="overlay-box">
<div class="mg-ov-header">
<div class="mg-ov-title" id="mgTitle">Mini-Game</div>
<button class="mg-ov-close" onclick="skipMinigame()"></button>
</div>
<div class="mg-ov-body">
<p class="mg-ov-desc" id="mgDesc"></p>
<div class="mg-canvas-wrap" id="mgCanvasWrap"></div>
<div class="mg-ov-controls" id="mgControls"></div>
<div class="mg-result-bar" id="mgResultBar">
<h3 id="mgResultTitle"></h3>
<p id="mgResultSub"></p>
</div>
<button id="btnMgContinue" onclick="afterMinigame()">Weiter →</button>
</div>
</div>
</div>
<!-- Result -->
<div class="overlay" id="resultOverlay">
<div class="overlay-bg"></div>
<div class="overlay-box">
<span class="res-emoji" id="resEmoji">🏆</span>
<h2 class="res-title" id="resTitle">Gewonnen!</h2>
<p class="res-sub" id="resSub"></p>
<div class="res-stats" id="resStats"></div>
<button class="res-btn" onclick="backToEditor()">📝 Zum Feedback</button>
</div>
</div>
<!-- Flash -->
<div id="transOverlay"></div>
<div id="toast"></div>
<script>
/* roundRect polyfill for older browsers */
if(!CanvasRenderingContext2D.prototype.roundRect){
CanvasRenderingContext2D.prototype.roundRect=function(x,y,w,h,r){
r=Math.min(r,w/2,h/2);
this.moveTo(x+r,y);this.lineTo(x+w-r,y);this.arcTo(x+w,y,x+w,y+r,r);
this.lineTo(x+w,y+h-r);this.arcTo(x+w,y+h,x+w-r,y+h,r);
this.lineTo(x+r,y+h);this.arcTo(x,y+h,x,y+h-r,r);
this.lineTo(x,y+r);this.arcTo(x,y,x+r,y,r);this.closePath();
return this;
};
}
/* ══════════ CONFIG ══════════ */
const cfg = JSON.parse(localStorage.getItem('gameConfig') || '{"name":"Mein Spiel","desc":"Ein tolles Brettspiel!","figure":"robot","background":"space","fieldCount":10,"fields":["snake","flappy","quiz","memory","reaction",null,"snake","quiz",null,""],"rules":{"movement":"dice","fail":"lives","lives":"3","pts":"10"}}');
const fields = (cfg.fields||[]).map(f=>f||null);
const fieldCount = cfg.fieldCount || fields.length || 10;
const rules = cfg.rules || {movement:'dice',fail:'lives',lives:'3',pts:'10'};
const storyItems = cfg.storyItems || [];
const quizData = cfg.quizData || [];
const FIGURES={robot:'🤖',ninja:'🥷',knight:'🧙‍♂️',cat:'🐱',rocket:'🚀',dino:'🦖',alien:'👾',superhero:'🦸',pirate:'🏴‍☠️',fox:'🦊',dragon:'🐲',astronaut:'👨‍🚀'};
const BG_THEMES={
jungle: {primary:'#22c55e',bg:'#052e16',glow:'rgba(34,197,94,0.35)',sky:'#064e3b',particle:'🌿'},
space: {primary:'#818cf8',bg:'#020617',glow:'rgba(129,140,248,0.4)',sky:'#0f0a2e',particle:'⭐'},
ocean: {primary:'#06b6d4',bg:'#083344',glow:'rgba(6,182,212,0.35)',sky:'#0c4a6e',particle:'🫧'},
fantasy:{primary:'#c084fc',bg:'#1e0a2e',glow:'rgba(192,132,252,0.4)',sky:'#2e1065',particle:'✨'},
school: {primary:'#fbbf24',bg:'#1c1508',glow:'rgba(251,191,36,0.3)',sky:'#1c1917',particle:'📚'},
volcano:{primary:'#f97316',bg:'#1c0a00',glow:'rgba(249,115,22,0.4)',sky:'#431407',particle:'🔥'},
snow: {primary:'#bae6fd',bg:'#0c1929',glow:'rgba(186,230,253,0.3)',sky:'#0f172a',particle:'❄️'},
city: {primary:'#94a3b8',bg:'#0a0a0f',glow:'rgba(148,163,184,0.3)',sky:'#1e293b',particle:'💡'},
candy: {primary:'#f472b6',bg:'#1a0010',glow:'rgba(244,114,182,0.4)',sky:'#500724',particle:'🍭'},
desert: {primary:'#fcd34d',bg:'#1c1000',glow:'rgba(252,211,77,0.3)',sky:'#292524',particle:'🌵'},
haunted:{primary:'#818cf8',bg:'#050308',glow:'rgba(129,140,248,0.3)',sky:'#0a0015',particle:'👻'},
future: {primary:'#34d399',bg:'#020c08',glow:'rgba(52,211,153,0.35)',sky:'#042f2e',particle:'🤖'},
};
const THEME = BG_THEMES[cfg.background] || BG_THEMES.space;
const MG_INFO={
snake: {emoji:'🐍',name:'Snake', desc:'Steuere die Schlange! Iss das Essen ohne gegen die Wand zu fahren.',controls:'Pfeiltasten oder WASD'},
flappy: {emoji:'🐦',name:'Flappy Bird', desc:'Klick oder Leertaste, um durch die Röhren zu fliegen!', controls:'Klick / Leertaste'},
memory: {emoji:'🃏',name:'Memory', desc:'Finde alle Paare — so schnell wie möglich!', controls:'Mausklick'},
quiz: {emoji:'❓',name:'Quiz', desc:'Beantworte die Frage richtig!', controls:'Mausklick'},
reaction: {emoji:'⚡',name:'Reaktion', desc:'Drück den Knopf so schnell du kannst wenn er erscheint!', controls:'Klick / Leertaste'},
basketball:{emoji:'🏀',name:'Basketball', desc:'Ziehe den Ball und lass ihn los um zu werfen!', controls:'Maus ziehen'},
catch: {emoji:'🍎',name:'Äpfel fangen',desc:'Bewege den Korb mit den Pfeiltasten!', controls:'← → oder A/D'},
maze: {emoji:'🌀',name:'Labyrinth', desc:'Finde den Ausgang!', controls:'Pfeiltasten / WASD'},
simon: {emoji:'🔴',name:'Simon Says', desc:'Merke und wiederhole die Farb-Sequenz!', controls:'Mausklick'},
puzzle: {emoji:'🧩',name:'Rätsel', desc:'Löse die Aufgabe!', controls:'Mausklick'},
spotdiff: {emoji:'🔍',name:'Unterschiede',desc:'Finde alle Unterschiede!', controls:'Mausklick'},
typing: {emoji:'⌨️',name:'Tipp-Rennen', desc:'Tippe das Wort so schnell wie möglich!', controls:'Tastatur'},
};
const figEmoji = FIGURES[cfg.figure] || '🎮';
const LETTERS = ['A','B','C','D'];
/* ══════════ APPLY THEME ══════════ */
document.documentElement.style.setProperty('--theme-primary', THEME.primary);
document.documentElement.style.setProperty('--theme-bg', THEME.bg);
document.documentElement.style.setProperty('--theme-glow', THEME.glow);
document.title = cfg.name || 'Spiel';
document.getElementById('pageTitle').textContent = cfg.name || 'Spiel';
/* ══════════ INTRO SETUP ══════════ */
document.getElementById('introFigure').textContent = figEmoji;
document.getElementById('introTitle').innerHTML =
(cfg.name||'Mein Spiel').replace(/(\S+)\s*$/, '<span class="highlight">$1</span>');
document.getElementById('introDesc').textContent = cfg.desc || '';
document.getElementById('hudTitle').textContent = cfg.name || 'Spiel';
const meta = document.getElementById('introMeta');
const BGNAMES={jungle:'Dschungel',space:'Weltraum',ocean:'Unterwasser',fantasy:'Fantasy',school:'Schule',volcano:'Vulkaninsel',snow:'Schneereich',city:'Großstadt',candy:'Süßigkeitenland',desert:'Wüste',haunted:'Geisterhaus',future:'Zukunft'};
const FIGNAMES={robot:'Roboter',ninja:'Ninja',knight:'Zauberer',cat:'Katze',rocket:'Rakete',dino:'Dino',alien:'Alien',superhero:'Superheld',pirate:'Pirat',fox:'Fuchs',dragon:'Drache',astronaut:'Astronaut'};
meta.innerHTML = [
`<div class="intro-meta-pill">${figEmoji} ${FIGNAMES[cfg.figure]||'Figur'}</div>`,
`<div class="intro-meta-pill">🌍 ${BGNAMES[cfg.background]||'Setting'}</div>`,
`<div class="intro-meta-pill">⬛ ${fieldCount} Felder</div>`,
`<div class="intro-meta-pill">${rules.fail==='lives'?'❤️ '+rules.lives+' Leben':'⭐ Punkte'}</div>`,
].join('');
/* ══════════ BG CANVAS (particle/ambient) ══════════ */
const bgCanvas = document.getElementById('bgCanvas');
const bgCtx = bgCanvas.getContext('2d');
let bgW, bgH, bgParticles=[];
function resizeBg() {
bgCanvas.width = bgW = window.innerWidth;
bgCanvas.height = bgH = window.innerHeight;
}
resizeBg(); window.addEventListener('resize', resizeBg);
function initParticles(count=60) {
bgParticles=[];
for(let i=0;i<count;i++) bgParticles.push({
x:Math.random()*bgW, y:Math.random()*bgH,
vx:(Math.random()-0.5)*0.3, vy:-Math.random()*0.4-0.1,
size:Math.random()*14+8, alpha:Math.random()*0.6+0.2,
emoji: THEME.particle
});
}
initParticles();
function drawBg() {
// Sky gradient
const grad = bgCtx.createLinearGradient(0,0,0,bgH);
grad.addColorStop(0, THEME.sky);
grad.addColorStop(1, THEME.bg);
bgCtx.fillStyle = grad;
bgCtx.fillRect(0,0,bgW,bgH);
// Ambient glow
const glow = bgCtx.createRadialGradient(bgW*0.5,bgH*0.3,0, bgW*0.5,bgH*0.3,bgW*0.5);
glow.addColorStop(0, THEME.glow);
glow.addColorStop(1, 'transparent');
bgCtx.fillStyle = glow;
bgCtx.fillRect(0,0,bgW,bgH);
// Particles
bgCtx.save();
bgParticles.forEach(p=>{
bgCtx.globalAlpha = p.alpha * (0.5+0.5*Math.sin(Date.now()*0.001+p.x));
bgCtx.font = p.size+'px serif';
bgCtx.fillText(p.emoji, p.x, p.y);
p.x += p.vx; p.y += p.vy;
if(p.y < -30) { p.y=bgH+20; p.x=Math.random()*bgW; }
if(p.x < -30 || p.x > bgW+30) p.vx *= -1;
});
bgCtx.globalAlpha=1;
bgCtx.restore();
requestAnimationFrame(drawBg);
}
drawBg();
/* ══════════ AUDIO (Web Audio API) ══════════ */
let audioCtx = null;
let musicNodes = [];
function startMusic() {
try {
audioCtx = new (window.AudioContext||window.webkitAudioContext)();
const master = audioCtx.createGain();
master.gain.value = 0.08;
master.connect(audioCtx.destination);
// Theme-specific chord progressions
const THEMES_MUSIC = {
jungle: [[220,277,330],[196,247,294],[165,208,247],[185,233,277]],
space: [[174,220,261],[155,196,233],[138,174,207],[130,164,196]],
ocean: [[261,329,392],[220,277,329],[196,247,294],[174,220,261]],
fantasy: [[220,277,370],[196,247,330],[174,220,294],[155,196,261]],
school: [[261,329,392],[294,370,440],[330,415,494],[261,329,392]],
volcano: [[164,207,247],[146,185,220],[130,164,196],[155,196,233]],
snow: [[293,370,440],[261,329,392],[220,277,329],[246,311,370]],
city: [[220,277,330],[196,247,294],[174,220,261],[185,233,294]],
candy: [[329,415,494],[294,370,440],[261,329,392],[277,349,415]],
desert: [[196,247,294],[174,220,261],[164,207,247],[155,196,233]],
haunted: [[174,207,247],[155,185,220],[138,164,196],[130,155,185]],
future: [[261,329,440],[220,277,370],[196,247,330],[174,220,294]],
};
const chords = THEMES_MUSIC[cfg.background] || THEMES_MUSIC.space;
let chord = 0;
function playChord() {
const now = audioCtx.currentTime;
const notes = chords[chord % chords.length];
notes.forEach(freq => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = cfg.background === 'haunted' ? 'sawtooth' : cfg.background === 'space' ? 'sine' : 'triangle';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0, now);
gain.gain.linearRampToValueAtTime(0.15, now+0.3);
gain.gain.linearRampToValueAtTime(0.05, now+1.5);
gain.gain.linearRampToValueAtTime(0, now+2.5);
osc.connect(gain); gain.connect(master);
osc.start(now); osc.stop(now+2.6);
musicNodes.push(osc);
});
chord++;
}
// Add subtle beat
function playBeat() {
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type='sine'; osc.frequency.value=80;
gain.gain.setValueAtTime(0.4,now);
gain.gain.linearRampToValueAtTime(0,now+0.1);
osc.connect(gain);gain.connect(master);
osc.start(now);osc.stop(now+0.12);
}
playChord();
const chordInterval = setInterval(playChord, 2800);
const beatInterval = setInterval(playBeat, 700);
musicNodes.push({ stop:()=>{ clearInterval(chordInterval); clearInterval(beatInterval); }});
} catch(e) { console.log('Audio not available:', e); }
}
function playSfx(type) {
if (!audioCtx) return;
try {
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
const sfx = {
roll: {freq:440, type:'sine', dur:0.15, vol:0.2},
land: {freq:330, type:'triangle',dur:0.2, vol:0.15},
win: {freq:660, type:'sine', dur:0.4, vol:0.25},
lose: {freq:110, type:'sawtooth',dur:0.5, vol:0.2},
story: {freq:528, type:'sine', dur:0.3, vol:0.15},
mg: {freq:550, type:'triangle',dur:0.25, vol:0.2},
};
const s = sfx[type] || sfx.land;
osc.type = s.type; osc.frequency.value = s.freq;
gain.gain.setValueAtTime(s.vol, now);
if(type==='win'){osc.frequency.setValueAtTime(660,now);osc.frequency.linearRampToValueAtTime(880,now+0.2);}
gain.gain.linearRampToValueAtTime(0, now+s.dur);
osc.connect(gain); gain.connect(audioCtx.destination);
osc.start(now); osc.stop(now+s.dur+0.05);
} catch(e){}
}
/* ══════════ BOARD CANVAS ══════════ */
const bc = document.getElementById('boardCanvas');
const bctx = bc.getContext('2d');
const COLS = Math.ceil(Math.sqrt(fieldCount));
const ROWS = Math.ceil(fieldCount / COLS);
const FS = Math.min(Math.floor((window.innerWidth*0.85)/COLS), Math.floor((window.innerHeight*0.72)/ROWS), 90);
const PAD = 16;
bc.width = COLS*FS + PAD*2;
bc.height = ROWS*FS + PAD*2 + 40;
// Snake-path field order
function fieldPos(i) {
const row = Math.floor(i/COLS);
const col = row%2===0 ? i%COLS : (COLS-1)-(i%COLS);
return {col, row};
}
// Game state
let pos=0, lives=parseInt(rules.lives)||3, maxLives=parseInt(rules.lives)||3, points=0;
let visited=new Set([0]), gamesPlayed=[], quizIdx=0;
let rolling=false, waitingForAction=false;
let pendingStories=[], pendingStoryIdx=0;
let currentMgId=null, mgActive=null, mgResult=null;
let gameEnded=false;
function fieldCenter(i) {
const {col,row} = fieldPos(i);
return { x: PAD + col*FS + FS/2, y: PAD + row*FS + FS/2 + 40 };
}
// Fig animation
let figX=0, figY=0, figTargX=0, figTargY=0, figScale=1;
function updateFig() {
const c = fieldCenter(pos);
figTargX=c.x; figTargY=c.y;
}
updateFig();
figX=figTargX; figY=figTargY;
function drawBoard() {
bctx.clearRect(0,0,bc.width,bc.height);
const t = Date.now();
// ── Board background with gradient
const bgGrad = bctx.createLinearGradient(0,0,bc.width,bc.height);
bgGrad.addColorStop(0,'rgba(10,8,30,0.82)');
bgGrad.addColorStop(1,'rgba(5,4,18,0.88)');
bctx.fillStyle=bgGrad;
bctx.beginPath();
bctx.roundRect(0,0,bc.width,bc.height,24);
bctx.fill();
// ── Subtle inner border glow
bctx.strokeStyle=`${THEME.primary}28`;
bctx.lineWidth=1.5;
bctx.beginPath();
bctx.roundRect(1,1,bc.width-2,bc.height-2,24);
bctx.stroke();
// ── Header bar
const hg=bctx.createLinearGradient(0,0,bc.width,0);
hg.addColorStop(0,`${THEME.primary}22`);
hg.addColorStop(0.5,`${THEME.primary}10`);
hg.addColorStop(1,`${THEME.primary}22`);
bctx.fillStyle=hg;
bctx.fillRect(0,0,bc.width,42);
bctx.strokeStyle=`${THEME.primary}30`;
bctx.lineWidth=1;
bctx.beginPath();bctx.moveTo(0,42);bctx.lineTo(bc.width,42);bctx.stroke();
bctx.fillStyle=THEME.primary;
bctx.font=`bold 14px 'Fredoka One',cursive`;
bctx.textAlign='center';
bctx.fillText((cfg.name||'Spiel').toUpperCase(), bc.width/2, 27);
if(cfg.devName){
bctx.fillStyle='rgba(255,255,255,0.35)';
bctx.font='bold 10px Nunito,sans-serif';
bctx.fillText('von '+cfg.devName, bc.width/2, 39);
}
// ── Path lines between fields (drawn under everything)
for(let i=0;i<fieldCount-1;i++) {
const a=fieldCenter(i), b=fieldCenter(i+1);
const vis=visited.has(i)&&visited.has(i+1);
// Glow line for visited path
if(vis){
bctx.shadowColor=THEME.primary;
bctx.shadowBlur=6;
bctx.strokeStyle=`${THEME.primary}60`;
bctx.lineWidth=3;
} else {
bctx.shadowBlur=0;
bctx.strokeStyle='rgba(255,255,255,0.07)';
bctx.lineWidth=2;
}
bctx.setLineDash(vis?[]:[5,5]);
bctx.beginPath(); bctx.moveTo(a.x,a.y); bctx.lineTo(b.x,b.y); bctx.stroke();
}
bctx.setLineDash([]);
bctx.shadowBlur=0;
// ── Fields
const MG_ICONS={snake:'🐍',flappy:'🐦',memory:'🃏',quiz:'❓',reaction:'⚡',basketball:'🏀',catch:'🍎',maze:'🌀',simon:'🔴',puzzle:'🧩',spotdiff:'🔍',typing:'⌨️'};
const MG_COLORS={snake:'#10b981',flappy:'#06b6d4',memory:'#8b5cf6',quiz:'#f59e0b',reaction:'#ef4444',basketball:'#f97316',catch:'#22c55e',maze:'#6366f1',simon:'#ec4899',puzzle:'#14b8a6',spotdiff:'#84cc16',typing:'#f43f5e'};
const r=FS*0.42;
for(let i=0;i<fieldCount;i++) {
const {x,y}=fieldCenter(i);
const gameId=fields[i];
const isStart=i===0, isEnd=i===fieldCount-1;
const isActive=i===pos, isVisited=visited.has(i);
const mgColor=gameId?MG_COLORS[gameId]||THEME.primary:null;
// ── Field glow (active pulse)
if(isActive){
const pulse=0.7+0.3*Math.sin(t*0.005);
bctx.shadowColor=THEME.primary;
bctx.shadowBlur=28*pulse;
const ag=bctx.createRadialGradient(x,y,0,x,y,r*1.5);
ag.addColorStop(0,`${THEME.primary}35`);
ag.addColorStop(1,'transparent');
bctx.fillStyle=ag;
bctx.beginPath();bctx.arc(x,y,r*1.5,0,Math.PI*2);bctx.fill();
} else if(isVisited&&mgColor){
bctx.shadowColor=mgColor;
bctx.shadowBlur=6;
} else {
bctx.shadowBlur=0;
}
// ── Field circle background
bctx.beginPath();
bctx.arc(x,y,r,0,Math.PI*2);
if(isStart){
const sg=bctx.createRadialGradient(x,y-r*0.3,0,x,y,r);
sg.addColorStop(0,'rgba(16,185,129,0.5)');
sg.addColorStop(1,'rgba(16,185,129,0.12)');
bctx.fillStyle=sg;
} else if(isEnd){
const eg=bctx.createRadialGradient(x,y-r*0.3,0,x,y,r);
eg.addColorStop(0,'rgba(245,166,35,0.5)');
eg.addColorStop(1,'rgba(245,166,35,0.12)');
bctx.fillStyle=eg;
} else if(gameId&&mgColor){
const mg2=bctx.createRadialGradient(x,y-r*0.3,0,x,y,r);
mg2.addColorStop(0,mgColor+'38');
mg2.addColorStop(1,mgColor+'0a');
bctx.fillStyle=mg2;
} else if(isVisited){
bctx.fillStyle='rgba(255,255,255,0.06)';
} else {
bctx.fillStyle='rgba(255,255,255,0.03)';
}
bctx.fill();
// ── Field border
bctx.lineWidth=isActive?2.5:1.5;
bctx.strokeStyle=isStart?'#10b981':isEnd?'#f5a623':isActive?THEME.primary:gameId&&mgColor?mgColor+'88':isVisited?'rgba(255,255,255,0.18)':'rgba(255,255,255,0.06)';
bctx.stroke();
bctx.shadowBlur=0;
// ── Field number label (small, top of circle)
const numLabel=isStart?'▶':isEnd?'★':String(i);
bctx.fillStyle=isStart?'#10b981':isEnd?'#f5a623':isVisited?'rgba(255,255,255,0.55)':'rgba(255,255,255,0.2)';
bctx.font=`bold ${Math.max(9,Math.floor(FS*0.13))}px Nunito,sans-serif`;
bctx.textAlign='center';
bctx.fillText(numLabel, x, y-r*0.52);
// ── Icon (emoji, center of circle)
const icon=isStart?'🟢':isEnd?'🏁':gameId?(MG_ICONS[gameId]||'🎮'):'';
if(icon){
bctx.font=`${Math.floor(FS*0.3)}px serif`;
bctx.fillText(icon, x, y+r*0.45);
}
// ── Game name label (below number, tiny)
if(gameId&&!isStart&&!isEnd){
const gname={snake:'Snake',flappy:'Flappy',memory:'Memory',quiz:'Quiz',reaction:'Reaktion',basketball:'Basketball',catch:'Fangen',maze:'Labyrinth',simon:'Simon',puzzle:'Rätsel',spotdiff:'Suche',typing:'Tippen'}[gameId]||gameId;
bctx.fillStyle=mgColor?mgColor+'cc':'rgba(255,255,255,0.35)';
bctx.font=`bold ${Math.max(8,Math.floor(FS*0.11))}px Nunito,sans-serif`;
bctx.fillText(gname, x, y+r*0.82);
}
// ── Story indicator badge
const hasStory=storyItems.some(s=>s.fieldIndex===i);
if(hasStory){
bctx.font=`${Math.floor(FS*0.16)}px serif`;
bctx.fillText('📖', x+r*0.7, y-r*0.65);
}
// ── Visited checkmark
if(isVisited&&!isActive&&i!==0){
bctx.fillStyle='rgba(255,255,255,0.25)';
bctx.font=`bold ${Math.floor(FS*0.14)}px Nunito,sans-serif`;
bctx.fillText('✓', x-r*0.7, y-r*0.65);
}
}
// ── Animate figure smoothly
figX += (figTargX-figX)*0.14;
figY += (figTargY-figY)*0.14;
// ── Figure shadow/glow
const glow=bctx.createRadialGradient(figX,figY+4,0,figX,figY,FS*0.55);
glow.addColorStop(0,THEME.glow);
glow.addColorStop(0.5,THEME.glow);
glow.addColorStop(1,'transparent');
bctx.fillStyle=glow;
bctx.fillRect(figX-FS*0.6,figY-FS*0.6,FS*1.2,FS*1.2);
// ── Figure emoji with bounce
const bounce=Math.sin(t*0.005)*4;
bctx.shadowColor=THEME.primary;
bctx.shadowBlur=20;
bctx.font=`${Math.floor(FS*0.44)}px serif`;
bctx.textAlign='center';
bctx.fillText(figEmoji, figX, figY+FS*0.17+bounce);
bctx.shadowBlur=0;
requestAnimationFrame(drawBoard);
}
/* ══════════ SCREEN TRANSITION ══════════ */
function switchScreen(from, to, cb) {
const t=document.getElementById('transOverlay');
t.classList.add('flash');
setTimeout(()=>{
document.getElementById(from).classList.remove('active');
document.getElementById(to).classList.add('active');
t.classList.remove('flash');
cb && cb();
}, 400);
}
/* ══════════ START ══════════ */
function startGame() {
startMusic();
switchScreen('introScreen','gameScreen',()=>{
updateHUD();
requestAnimationFrame(drawBoard);
setTimeout(()=>showToast(`${figEmoji} Viel Spaß, ${cfg.devName||'Spieler'}! Würfle zum Starten!`), 800);
});
}
/* ══════════ HUD ══════════ */
function updateHUD() {
document.getElementById('hudPos').textContent=`${pos+1}/${fieldCount}`;
if(rules.fail==='lives'){
document.getElementById('hudLifePill').style.display='';
document.getElementById('hudPtPill').style.display='none';
document.getElementById('hudLives').textContent='❤️'.repeat(lives)+'🖤'.repeat(Math.max(0,maxLives-lives));
} else {
document.getElementById('hudLifePill').style.display='none';
document.getElementById('hudPtPill').style.display='';
document.getElementById('hudPts').textContent=points;
}
}
/* ══════════ TOAST ══════════ */
let toastTimer=null;
function showToast(msg, dur=2500) {
const t=document.getElementById('toast');
t.textContent=msg; t.classList.add('show');
clearTimeout(toastTimer);
toastTimer=setTimeout(()=>t.classList.remove('show'), dur);
}
/* ══════════ DICE & MOVEMENT ══════════ */
function doRoll() {
if(rolling||waitingForAction||gameEnded) return;
rolling=true;
document.getElementById('btnRoll').disabled=true;
playSfx('roll');
const faces=['⚀','⚁','⚂','⚃','⚄','⚅'];
const de=document.getElementById('diceEmoji');
de.classList.add('rolling');
let t=0;
const iv=setInterval(()=>{
de.textContent=faces[Math.floor(Math.random()*6)];
t++;
if(t>10){
clearInterval(iv);
de.classList.remove('rolling');
const steps=rules.movement==='step'?1:(Math.floor(Math.random()*6)+1);
de.textContent=faces[steps-1];
showToast(`🎲 ${steps} ${steps===1?'Feld':'Felder'} vor!`);
setTimeout(()=>movePlayer(steps), 500);
}
}, 70);
}
function movePlayer(steps) {
const newPos=Math.min(pos+steps,fieldCount-1);
// Animate step by step
let step=0, cur=pos;
function doStep(){
if(cur>=newPos){
pos=newPos; visited.add(pos); rolling=false;
playSfx('land');
updateHUD();
updateFig();
processLanding();
return;
}
cur++;
visited.add(cur);
fieldPos(cur); // side effect - update
figTargX=fieldCenter(cur).x;
figTargY=fieldCenter(cur).y;
setTimeout(doStep, 220);
}
doStep();
}
/* ══════════ LANDING LOGIC ══════════ */
function processLanding() {
// Check stories BEFORE
const sBefore=storyItems.filter(s=>s.position==='before'&&s.fieldIndex===pos);
const sAfter =storyItems.filter(s=>s.position==='after' &&s.fieldIndex===pos);
// Win check
if(pos===fieldCount-1){ setTimeout(showWin, 400); return; }
if(sBefore.length>0){
pendingStories=[...sBefore, {_field:true}, ...sAfter];
pendingStoryIdx=0;
processNextStoryOrField();
} else {
triggerFieldEvent(sAfter);
}
}
function processNextStoryOrField(){
const item=pendingStories[pendingStoryIdx];
if(!item){ enableRoll(); return; }
if(item._field){
pendingStoryIdx++;
const remaining=pendingStories.slice(pendingStoryIdx);
triggerFieldEvent(remaining);
return;
}
pendingStoryIdx++;
openStory(item);
}
function openStory(s){
playSfx('story');
document.getElementById('storyEmoji').textContent=s.emoji||'📖';
document.getElementById('storyText').textContent=s.text||'...';
document.getElementById('storyOverlay').classList.add('open');
}
function closeStory(){
document.getElementById('storyOverlay').classList.remove('open');
processNextStoryOrField();
}
function triggerFieldEvent(afterStories){
const gameId=fields[pos];
if(!gameId||pos===0){
// Empty field — check after stories
if(afterStories&&afterStories.length>0){
pendingStories=afterStories; pendingStoryIdx=0; processNextStoryOrField();
} else { enableRoll(); showToast('⬜ Leeres Feld — kein Mini-Game.'); }
return;
}
// Store after stories for post-mg
pendingStories=afterStories||[];
pendingStoryIdx=0;
openMinigame(gameId);
}
function enableRoll(){
waitingForAction=false;
document.getElementById('btnRoll').disabled=false;
}
/* ══════════ MINIGAME SYSTEM ══════════ */
function openMinigame(id){
const info=MG_INFO[id]||{emoji:'🎮',name:id,desc:'Mini-Game!',controls:'Variiert'};
currentMgId=id;
document.getElementById('mgTitle').innerHTML=`${info.emoji} ${info.name}`;
document.getElementById('mgDesc').textContent=info.desc;
document.getElementById('mgControls').innerHTML=`🎮 ${info.controls}`;
document.getElementById('mgResultBar').style.display='none';
document.getElementById('btnMgContinue').style.display='none';
const wrap=document.getElementById('mgCanvasWrap');
wrap.innerHTML='';
if(mgActive&&mgActive.stop) mgActive.stop();
mgActive=null; mgResult=null;
playSfx('mg');
document.getElementById('mgOverlay').classList.add('open');
const W=Math.min(560, window.innerWidth-80)-48;
const H=260;
const launchers={
snake: launchSnake,
flappy: launchFlappy,
memory: launchMemory,
quiz: launchQuiz,
reaction:launchReaction,
};
if(launchers[id]){ mgActive=launchers[id](wrap,W,H); }
else {
// Placeholder for other games
wrap.style.minHeight=H+'px';
wrap.innerHTML=`<div style="text-align:center;padding:40px 20px">
<div style="font-size:3rem;margin-bottom:12px">${info.emoji}</div>
<div style="font-family:'Fredoka One',cursive;font-size:1.2rem;color:${THEME.primary};margin-bottom:8px">${info.name}</div>
<div style="color:#a7a3c2;font-size:13px;margin-bottom:20px">Dieses Mini-Game wird bald verfügbar sein!</div>
<button onclick="finishMinigame(true)" style="background:linear-gradient(135deg,#7c3aed,#f5a623);color:#fff;font-family:'Fredoka One',cursive;font-size:1rem;border:none;border-radius:10px;padding:12px 28px;cursor:pointer;">✅ Bestanden!</button>
</div>`;
}
}
function finishMinigame(won){
if(mgActive&&mgActive.stop) mgActive.stop();
mgActive=null;
const rb=document.getElementById('mgResultBar');
rb.className='mg-result-bar '+(won?'win':'lose');
rb.style.display='block';
document.getElementById('mgResultTitle').textContent=won?'🎉 Gewonnen!':'💀 Verloren!';
if(!won && rules.fail==='lives'){
lives=Math.max(0,lives-1);
document.getElementById('mgResultSub').textContent=`Du verlierst ein Leben. Noch ${lives} übrig.`;
playSfx('lose');
} else if(won && rules.fail==='points'){
const pts=parseInt(rules.pts)||10;
points+=pts;
document.getElementById('mgResultSub').textContent=`+${pts} Punkte! Insgesamt: ${points}`;
playSfx('win');
} else {
document.getElementById('mgResultSub').textContent=won?'Super gemacht!':'Nicht schlimm — weiter gehts!';
if(won) playSfx('win'); else playSfx('lose');
}
updateHUD();
document.getElementById('btnMgContinue').style.display='block';
if(rules.fail==='lives' && lives<=0){ setTimeout(showGameOver,1200); }
}
function afterMinigame(){
document.getElementById('mgOverlay').classList.remove('open');
updateHUD();
// Process after-stories
if(pendingStories.length>0){ processNextStoryOrField(); }
else { enableRoll(); }
}
function skipMinigame(){
if(mgActive&&mgActive.stop) mgActive.stop();
mgActive=null;
document.getElementById('mgOverlay').classList.remove('open');
enableRoll();
}
/* ══ SNAKE ══ */
function launchSnake(wrap,W,H){
const canvas=document.createElement('canvas');
canvas.width=W; canvas.height=H;
wrap.appendChild(canvas);
const ctx=canvas.getContext('2d');
const SZ=20, cols=Math.floor(W/SZ), rows=Math.floor(H/SZ);
let snake=[{x:5,y:5},{x:4,y:5},{x:3,y:5}],dir={x:1,y:0},food={x:10,y:8},score=0,dead=false,started=true;
let raf,stopped=false;
const onKey=e=>{
if(e.key==='ArrowUp'&&dir.y===0)dir={x:0,y:-1};
if(e.key==='ArrowDown'&&dir.y===0)dir={x:0,y:1};
if(e.key==='ArrowLeft'&&dir.x===0)dir={x:-1,y:0};
if(e.key==='ArrowRight'&&dir.x===0)dir={x:1,y:0};
if(e.key==='w')dir={x:0,y:-1}; if(e.key==='s')dir={x:0,y:1};
if(e.key==='a')dir={x:-1,y:0}; if(e.key==='d')dir={x:1,y:0};
};
document.addEventListener('keydown',onKey);
let last=0, endTimer=null;
function loop(ts){
if(stopped)return; raf=requestAnimationFrame(loop);
if(ts-last<140)return; last=ts;
if(!dead){
const h={x:snake[0].x+dir.x,y:snake[0].y+dir.y};
if(h.x<0||h.x>=cols||h.y<0||h.y>=rows||snake.some(s=>s.x===h.x&&s.y===h.y)){
dead=true;
if(!endTimer) endTimer=setTimeout(()=>finishMinigame(score>=3),1200);
} else {
snake.unshift(h);
if(h.x===food.x&&h.y===food.y){score++;food={x:Math.floor(Math.random()*cols),y:Math.floor(Math.random()*rows)};}
else snake.pop();
}
}
ctx.fillStyle='#050508'; ctx.fillRect(0,0,W,H);
ctx.fillStyle=THEME.primary; ctx.beginPath(); ctx.arc(food.x*SZ+SZ/2,food.y*SZ+SZ/2,SZ*0.4,0,Math.PI*2); ctx.fill();
snake.forEach((s,i)=>{
ctx.fillStyle=i===0?THEME.primary:`${THEME.primary}99`;
ctx.fillRect(s.x*SZ+1,s.y*SZ+1,SZ-2,SZ-2);
});
ctx.fillStyle='#fff'; ctx.font='bold 13px Nunito,sans-serif'; ctx.textAlign='left'; ctx.fillText(`🍕 ${score} Ziel: 3`,8,18);
if(dead){
ctx.fillStyle='rgba(239,68,68,0.8)'; ctx.fillRect(0,H/2-28,W,56);
ctx.fillStyle='#fff'; ctx.font='bold 20px Fredoka One,cursive'; ctx.textAlign='center';
ctx.fillText(score>=3?'🎉 Geschafft!':'💀 Game Over',W/2,H/2+6);
}
}
raf=requestAnimationFrame(loop);
return{stop:()=>{stopped=true;cancelAnimationFrame(raf);document.removeEventListener('keydown',onKey);}};
}
/* ══ FLAPPY ══ */
function launchFlappy(wrap,W,H){
const canvas=document.createElement('canvas'); canvas.width=W; canvas.height=H; wrap.appendChild(canvas);
const ctx=canvas.getContext('2d');
let bird={y:H/2,vy:0},pipes=[{x:W,gap:Math.random()*(H-100)+30}],score=0,dead=false,started=false;
const GRAV=0.5,JUMP=-8,PW=38,GAP=85;
let raf,stopped=false,endTimer=null;
const jump=()=>{if(!dead){bird.vy=JUMP;started=true;}};
const onKey=e=>{if(e.code==='Space'){e.preventDefault();jump();}};
document.addEventListener('keydown',onKey); canvas.addEventListener('click',jump);
function loop(){
if(stopped)return; raf=requestAnimationFrame(loop);
ctx.fillStyle='#050508'; ctx.fillRect(0,0,W,H);
if(started&&!dead){
bird.vy+=GRAV; bird.y+=bird.vy;
pipes.forEach(p=>p.x-=2.8);
if(pipes[pipes.length-1].x<W-170) pipes.push({x:W,gap:Math.random()*(H-100)+30});
pipes=pipes.filter(p=>p.x>-PW);
pipes.forEach(p=>{if(p.x+PW<50&&!p.passed){p.passed=true;score++;}});
if(bird.y<0||bird.y+20>H)dead=true;
pipes.forEach(p=>{if(50<p.x+PW&&70>p.x&&(bird.y<p.gap||bird.y+20>p.gap+GAP))dead=true;});
if(dead&&!endTimer) endTimer=setTimeout(()=>finishMinigame(score>=3),1000);
}
pipes.forEach(p=>{
ctx.fillStyle=THEME.primary+'bb';
ctx.fillRect(p.x,0,PW,p.gap); ctx.fillRect(p.x,p.gap+GAP,PW,H-p.gap-GAP);
});
ctx.fillStyle=dead?'#ef4444':THEME.primary;
ctx.beginPath(); ctx.ellipse(50+14,bird.y+10,14,10,bird.vy*0.04,0,Math.PI*2); ctx.fill();
ctx.fillStyle='#fff'; ctx.font='bold 13px Nunito,sans-serif'; ctx.textAlign='left'; ctx.fillText(`🐦 ${score} Ziel: 3`,8,18);
if(!started){ctx.fillStyle='rgba(255,255,255,0.7)';ctx.textAlign='center';ctx.font='13px Nunito,sans-serif';ctx.fillText('Klicken oder Leertaste',W/2,H/2+40);}
if(dead){ctx.fillStyle='rgba(239,68,68,0.8)';ctx.fillRect(0,H/2-28,W,56);ctx.fillStyle='#fff';ctx.font='bold 20px Fredoka One,cursive';ctx.textAlign='center';ctx.fillText(score>=3?'🎉 Geschafft!':'💀 Daneben!',W/2,H/2+6);}
}
raf=requestAnimationFrame(loop);
return{stop:()=>{stopped=true;cancelAnimationFrame(raf);document.removeEventListener('keydown',onKey);canvas.removeEventListener('click',jump);}};
}
/* ══ MEMORY ══ */
function launchMemory(wrap,W,H){
const canvas=document.createElement('canvas'); canvas.width=W; canvas.height=H; wrap.appendChild(canvas);
const ctx=canvas.getContext('2d');
const emojis=['🐱','🐶','🦊','🐸','🦋','🐠'];
const all=[...emojis,...emojis].sort(()=>Math.random()-0.5);
let cards=all.map((e,i)=>({e,i,open:false}));
let flipped=[],matched=[],checking=false,moves=0;
let raf,stopped=false;
const C=4,R=3,cw=Math.floor((W-16)/(C+0.5)),ch=Math.floor((H-16)/R);
const ox=(W-C*cw)/2, oy=(H-R*ch)/2;
canvas.addEventListener('click',e=>{
if(checking)return;
const r=canvas.getBoundingClientRect();
const mx=e.clientX-r.left, my=e.clientY-r.top;
for(let ri=0;ri<R;ri++) for(let ci=0;ci<C;ci++){
const card=cards[ri*C+ci];
const x=ox+ci*cw, y=oy+ri*ch;
if(mx>=x&&mx<x+cw-4&&my>=y&&my<y+ch-4&&!card.open&&!matched.includes(card.i)){
card.open=true; flipped.push(card);
if(flipped.length===2){
moves++;checking=true;
setTimeout(()=>{
if(flipped[0].e===flipped[1].e) matched.push(flipped[0].i,flipped[1].i);
else flipped.forEach(c=>c.open=false);
flipped=[]; checking=false;
if(matched.length===cards.length) setTimeout(()=>finishMinigame(true),600);
},700);
}
break;
}
}
});
function loop(){
if(stopped)return; raf=requestAnimationFrame(loop);
ctx.fillStyle='#050508'; ctx.fillRect(0,0,W,H);
ctx.fillStyle='#fff'; ctx.font='bold 13px Nunito,sans-serif'; ctx.textAlign='left'; ctx.fillText(`🃏 ${matched.length/2}/${emojis.length} Paare Züge: ${moves}`,8,18);
for(let ri=0;ri<R;ri++) for(let ci=0;ci<C;ci++){
const card=cards[ri*C+ci];
const x=ox+ci*cw, y=oy+ri*ch, isM=matched.includes(card.i);
ctx.fillStyle=isM?'rgba(16,185,129,0.2)':card.open?'#2e2b4a':'rgba(255,255,255,0.04)';
ctx.strokeStyle=isM?'#10b981':card.open?THEME.primary:'rgba(255,255,255,0.1)';
ctx.lineWidth=2;
ctx.beginPath(); ctx.roundRect(x+2,y+2,cw-8,ch-8,8); ctx.fill(); ctx.stroke();
if(card.open||isM){ ctx.font=`${Math.min(cw,ch)*0.5}px serif`; ctx.textAlign='center'; ctx.fillText(card.e,x+cw/2,y+ch/2+10); }
else { ctx.fillStyle='rgba(255,255,255,0.15)'; ctx.font='bold 16px Nunito,sans-serif'; ctx.textAlign='center'; ctx.fillText('?',x+cw/2,y+ch/2+5); }
}
}
raf=requestAnimationFrame(loop);
return{stop:()=>{stopped=true;cancelAnimationFrame(raf);}};
}
/* ══ QUIZ ══ */
function launchQuiz(wrap,W,H){
const q=quizData[quizIdx]||null; quizIdx++;
if(!q||!q.question){ wrap.innerHTML=`<div style="text-align:center;padding:40px;font-family:'Nunito',sans-serif"><div style="font-size:2rem;margin-bottom:12px">❓</div><div style="color:#a7a3c2">Keine Quiz-Frage hinterlegt.</div><button onclick="finishMinigame(true)" style="margin-top:16px;background:#7c3aed;color:#fff;border:none;border-radius:10px;padding:10px 24px;cursor:pointer;font-family:'Fredoka One',cursive">OK</button></div>`; return{stop:()=>{}};}
let answered=false;
wrap.innerHTML=`
<div style="padding:20px;font-family:'Nunito',sans-serif;width:100%">
<div style="font-size:13px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:${THEME.primary};margin-bottom:10px">❓ Quiz-Frage</div>
<div style="font-size:15px;font-weight:700;margin-bottom:16px;line-height:1.5;color:#fff">${q.question}</div>
<div id="quizAnswers" style="display:grid;grid-template-columns:1fr 1fr;gap:8px"></div>
<div id="quizResult" style="margin-top:12px;text-align:center;font-family:'Fredoka One',cursive;font-size:1.1rem;display:none"></div>
</div>
`;
const grid=wrap.querySelector('#quizAnswers');
LETTERS.forEach((L,i)=>{
const btn=document.createElement('button');
btn.style.cssText=`background:rgba(255,255,255,0.06);border:1.5px solid rgba(255,255,255,0.12);border-radius:10px;padding:10px 12px;font-family:'Nunito',sans-serif;font-size:13px;font-weight:700;color:#fff;cursor:pointer;display:flex;align-items:center;gap:8px;transition:all 0.2s;text-align:left;`;
btn.innerHTML=`<span style="font-family:'Fredoka One',cursive;font-size:0.9rem;color:${THEME.primary}">${L}</span>${q.answers[i]||'?'}`;
btn.addEventListener('mouseenter',()=>{ if(!answered) btn.style.borderColor=THEME.primary; });
btn.addEventListener('mouseleave',()=>{ if(!answered) btn.style.borderColor='rgba(255,255,255,0.12)'; });
btn.addEventListener('click',()=>{
if(answered)return; answered=true;
const correct=i===q.correct;
btn.style.borderColor=correct?'#10b981':'#ef4444';
btn.style.background=correct?'rgba(16,185,129,0.15)':'rgba(239,68,68,0.1)';
if(!correct){
const cBtn=grid.children[q.correct];
cBtn.style.borderColor='#10b981'; cBtn.style.background='rgba(16,185,129,0.15)';
}
const res=wrap.querySelector('#quizResult');
res.style.display='block';
res.style.color=correct?'#10b981':'#ef4444';
res.textContent=correct?'✅ Richtig!':'❌ Falsch!';
setTimeout(()=>finishMinigame(correct),1200);
});
grid.appendChild(btn);
});
return{stop:()=>{}};
}
/* ══ REACTION ══ */
function launchReaction(wrap,W,H){
const canvas=document.createElement('canvas'); canvas.width=W; canvas.height=H; wrap.appendChild(canvas);
const ctx=canvas.getContext('2d');
let phase='wait',waitEnd=0,reactionStart=0,times=[],best=Infinity;
let raf,stopped=false;
function reset(){phase='wait';waitEnd=performance.now()+(2+Math.random()*3)*1000;}reset();
const react=()=>{
if(phase==='ready'){const t=performance.now()-reactionStart;times.push(t);best=Math.min(best,t);if(times.length>=3){setTimeout(()=>finishMinigame(best<600),800);}else reset();}
else if(phase==='wait'){phase='toosoon';setTimeout(reset,1000);}
};
const onKey=e=>{if(e.code==='Space')react();};
document.addEventListener('keydown',onKey); canvas.addEventListener('click',react);
function loop(ts){
if(stopped)return; raf=requestAnimationFrame(loop);
if(phase==='wait'&&ts>waitEnd){phase='ready';reactionStart=performance.now();}
ctx.fillStyle=phase==='ready'?'#16a34a':phase==='toosoon'?'#7f1d1d':'#050508';
ctx.fillRect(0,0,W,H);
ctx.textAlign='center';
if(phase==='wait'){ctx.fillStyle='#a7a3c2';ctx.font='16px Nunito,sans-serif';ctx.fillText('Warte...',W/2,H/2-20);ctx.fillText(`${times.length}/3 Runden`,W/2,H/2+10);}
if(phase==='ready'){ctx.fillStyle='#fff';ctx.font='bold 36px Fredoka One,cursive';ctx.fillText('JETZT!',W/2,H/2);ctx.font='14px Nunito,sans-serif';ctx.fillText('Klick oder Leertaste!',W/2,H/2+36);}
if(phase==='toosoon'){ctx.fillStyle='#fca5a5';ctx.font='bold 24px Fredoka One,cursive';ctx.fillText('Zu früh! 😅',W/2,H/2);}
if(times.length>0){const last=times[times.length-1];ctx.fillStyle='rgba(255,255,255,0.6)';ctx.font='13px Nunito,sans-serif';ctx.fillText(`Letzte: ${Math.round(last)}ms Beste: ${Math.round(best)}ms`,W/2,H-16);}
ctx.textAlign='left';
}
raf=requestAnimationFrame(loop);
return{stop:()=>{stopped=true;cancelAnimationFrame(raf);document.removeEventListener('keydown',onKey);canvas.removeEventListener('click',react);}};
}
/* ══════════ WIN / GAME OVER ══════════ */
function showWin(){
gameEnded=true;
playSfx('win');
const ov=document.getElementById('resultOverlay');
document.getElementById('resEmoji').textContent='🏆';
document.getElementById('resTitle').textContent='Du hast gewonnen!';
document.getElementById('resSub').textContent=`Super gemacht! Du hast alle ${fieldCount} Felder durchgespielt!`;
document.getElementById('resStats').innerHTML=`
<div class="rs-pill"><div class="rs-val">${gamesPlayed.length}</div><div class="rs-label">Mini-Games</div></div>
<div class="rs-pill"><div class="rs-val">${rules.fail==='points'?points:lives+'❤️'}</div><div class="rs-label">${rules.fail==='points'?'Punkte':'Leben übrig'}</div></div>
<div class="rs-pill"><div class="rs-val">${visited.size}</div><div class="rs-label">Felder besucht</div></div>
`;
ov.classList.add('open');
}
function showGameOver(){
gameEnded=true;
playSfx('lose');
const ov=document.getElementById('resultOverlay');
document.getElementById('resEmoji').textContent='💀';
document.getElementById('resTitle').textContent='Game Over!';
document.getElementById('resSub').textContent='Alle Leben aufgebraucht. Aber du kannst trotzdem Feedback geben!';
document.getElementById('resTitle').style.color='var(--danger)';
document.getElementById('resStats').innerHTML=`
<div class="rs-pill"><div class="rs-val">${gamesPlayed.length}</div><div class="rs-label">Mini-Games gespielt</div></div>
<div class="rs-pill"><div class="rs-val">${visited.size}/${fieldCount}</div><div class="rs-label">Felder besucht</div></div>
`;
ov.classList.add('open');
}
function showMenu(){ showToast('☰ Menü kommt in der finalen Version!'); }
function backToEditor(){ window.close(); }
</script>
</body>
</html>