From be7998e864ce2d7b35f97c2080d51306723d07cb Mon Sep 17 00:00:00 2001 From: Stefan Franke Date: Tue, 7 Jul 2026 08:36:44 +0200 Subject: [PATCH] =?UTF-8?q?Aktueller=20Stand:=20Editor=20+=20Player=20+=20?= =?UTF-8?q?Short-Codes=20+=20XSS-H=C3=A4rtung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 20 + codegen.js | 313 ++++++++++ editor.css | 616 ++++++++++++++++++++ editor.html | 597 +++++++++++++++++++ game.css | 270 +++++++++ game.html | 140 +++++ index.html | 14 + js/board.js | 307 ++++++++++ js/data.js | 54 ++ js/game.js | 1001 ++++++++++++++++++++++++++++++++ js/minigame-test.js | 155 +++++ js/quiz.js | 232 ++++++++ js/share.js | 362 ++++++++++++ js/state.js | 98 ++++ js/tour.js | 278 +++++++++ js/wizard.js | 340 +++++++++++ js/world.js | 723 +++++++++++++++++++++++ logo.png | Bin 0 -> 90563 bytes minigames/_api.js | 98 ++++ minigames/basketball.js | 176 ++++++ minigames/catch.js | 142 +++++ minigames/flappy.js | 268 +++++++++ minigames/flappy.js.bak_084309 | 174 ++++++ minigames/flappy2p.js | 457 +++++++++++++++ minigames/maze.js | 194 +++++++ minigames/memory.js | 142 +++++ minigames/puzzle.js | 137 +++++ minigames/quiz.js | 116 ++++ minigames/reaction.js | 144 +++++ minigames/simon.js | 172 ++++++ minigames/snake.js | 204 +++++++ minigames/snake2p.js | 353 +++++++++++ minigames/spotdiff.js | 392 +++++++++++++ minigames/typing.js | 170 ++++++ phw_logo.png | Bin 0 -> 73293 bytes play/codes/4KVF7E.json | 1 + play/codes/4PN6WA.json | 1 + play/codes/4QMP7Z.json | 1 + play/codes/58LPMQ.json | 1 + play/codes/5VL3YY.json | 1 + play/codes/7DR474.json | 1 + play/codes/7N5VM4.json | 1 + play/codes/9BXACW.json | 1 + play/codes/AA7BB5.json | 1 + play/codes/B56Q2K.json | 1 + play/codes/CFTWPL.json | 1 + play/codes/D2FVAZ.json | 1 + play/codes/G8X49H.json | 1 + play/codes/HGPBEE.json | 1 + play/codes/J732HQ.json | 1 + play/codes/K8ENHR.json | 1 + play/codes/K8RZCV.json | 1 + play/codes/KQZJMW.json | 1 + play/codes/M62QKS.json | 1 + play/codes/NJMCNF.json | 1 + play/codes/PGJWC7.json | 1 + play/codes/QCE29W.json | 1 + play/codes/QEE88J.json | 1 + play/codes/SMK7DD.json | 1 + play/codes/T9B7XH.json | 1 + play/codes/UU6CZ4.json | 1 + play/codes/VZUA7B.json | 1 + play/codes/W53XAM.json | 1 + play/codes/WYSG2Q.json | 1 + play/codes/XSCYZN.json | 1 + play/index.html | 125 ++++ 66 files changed, 9014 insertions(+) create mode 100644 .gitignore create mode 100644 codegen.js create mode 100644 editor.css create mode 100644 editor.html create mode 100644 game.css create mode 100644 game.html create mode 100644 index.html create mode 100644 js/board.js create mode 100644 js/data.js create mode 100644 js/game.js create mode 100644 js/minigame-test.js create mode 100644 js/quiz.js create mode 100644 js/share.js create mode 100644 js/state.js create mode 100644 js/tour.js create mode 100644 js/wizard.js create mode 100644 js/world.js create mode 100644 logo.png create mode 100644 minigames/_api.js create mode 100644 minigames/basketball.js create mode 100644 minigames/catch.js create mode 100644 minigames/flappy.js create mode 100644 minigames/flappy.js.bak_084309 create mode 100644 minigames/flappy2p.js create mode 100644 minigames/maze.js create mode 100644 minigames/memory.js create mode 100644 minigames/puzzle.js create mode 100644 minigames/quiz.js create mode 100644 minigames/reaction.js create mode 100644 minigames/simon.js create mode 100644 minigames/snake.js create mode 100644 minigames/snake2p.js create mode 100644 minigames/spotdiff.js create mode 100644 minigames/typing.js create mode 100644 phw_logo.png create mode 100644 play/codes/4KVF7E.json create mode 100644 play/codes/4PN6WA.json create mode 100644 play/codes/4QMP7Z.json create mode 100644 play/codes/58LPMQ.json create mode 100644 play/codes/5VL3YY.json create mode 100644 play/codes/7DR474.json create mode 100644 play/codes/7N5VM4.json create mode 100644 play/codes/9BXACW.json create mode 100644 play/codes/AA7BB5.json create mode 100644 play/codes/B56Q2K.json create mode 100644 play/codes/CFTWPL.json create mode 100644 play/codes/D2FVAZ.json create mode 100644 play/codes/G8X49H.json create mode 100644 play/codes/HGPBEE.json create mode 100644 play/codes/J732HQ.json create mode 100644 play/codes/K8ENHR.json create mode 100644 play/codes/K8RZCV.json create mode 100644 play/codes/KQZJMW.json create mode 100644 play/codes/M62QKS.json create mode 100644 play/codes/NJMCNF.json create mode 100644 play/codes/PGJWC7.json create mode 100644 play/codes/QCE29W.json create mode 100644 play/codes/QEE88J.json create mode 100644 play/codes/SMK7DD.json create mode 100644 play/codes/T9B7XH.json create mode 100644 play/codes/UU6CZ4.json create mode 100644 play/codes/VZUA7B.json create mode 100644 play/codes/W53XAM.json create mode 100644 play/codes/WYSG2Q.json create mode 100644 play/codes/XSCYZN.json create mode 100644 play/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a16220 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Backups +backups/ +backup_*/ +_backup_*/ +*.bak +*.bak.* +*.bak? +editor_backup_* +game.html.bak2 + +# Build & Deps +node_modules/ +dist/ +.env +.env.* +*.log + +# OS +.DS_Store +Thumbs.db diff --git a/codegen.js b/codegen.js new file mode 100644 index 0000000..a19edf6 --- /dev/null +++ b/codegen.js @@ -0,0 +1,313 @@ +/* codegen.js — Live-Python-Code-Vorschau (EINZIGE Quelle, aus editor.html ausgelagert) */ +/* ══════════════════════════════════════════════ + PYTHON CODE GENERATOR — Live-Vorschau (kompakt) +══════════════════════════════════════════════ */ + +// ── Progressive unlock tracking ───────────────────────────── +const unlocked = new Set(); +let lastKey = null; // welcher Bereich wurde zuletzt angefasst → gelbes Highlight + +function setCodeFocus(key){ + unlocked.add(key); + lastKey = key; + scheduleCodeUpdate(50); +} + +// Focus-Listener für Text-Eingaben +document.addEventListener('focusin', e=>{ + const id = e.target.id || ''; + if(id==='s1devname') setCodeFocus('dev'); + if(id==='s1name') setCodeFocus('gamename'); + if(id==='s1desc') setCodeFocus('gamedesc'); + if(id && id.startsWith('qq')) setCodeFocus('quiz'); +}); + +// Mini-Game IDs → Python-Funktionsnamen +const MG_PY = { + snake:'schlangen_spiel', flappy:'flug_spiel', memory:'memory', quiz:'quiz', + reaction:'reaktionstest', basketball:'basketball', catch:'fangen', maze:'labyrinth', + simon:'simon_says', puzzle:'raetsel', spotdiff:'unterschiede', typing:'tipp_rennen', + snake2p:'snake_duell', flappy2p:'flappy_duell', +}; + +// ───────────────────────────────────────────── +function buildPythonCode(){ + const s = ST; + const fig = FIGURES.find(f=>f.id===s.figure); + const bg = BACKGROUNDS.find(b=>b.id===s.background); + + // Leer bis zur ersten Interaktion + if(unlocked.size===0 && !s.devName && !s.name && !s.figure) return []; + + const lines = []; + const hi = key => lastKey === key; + + // ── Imports ── + lines.push({t:'kw', v:'import', rest:' boardgame_engine as engine'}); + lines.push({t:'kw', v:'import', rest:' minigames'}); + lines.push({t:'kw', v:'from', rest:' config import GameConfig, Field, StoryText'}); + lines.push({t:'bl'}); + + // ── Grundeinstellungen ── + const anyBasic = s.devName||s.name||s.desc||s.figure||s.background + ||unlocked.has('dev')||unlocked.has('gamename')||unlocked.has('gamedesc') + ||unlocked.has('figure')||unlocked.has('background')||unlocked.has('movement') + ||unlocked.has('failmode')||unlocked.has('lives')||unlocked.has('pts')||unlocked.has('fieldcount'); + if(anyBasic){ + lines.push({t:'cm', v:'# ── Grundeinstellungen ──'}); + + if(unlocked.has('dev') || s.devName){ + if(s.devName) lines.push({t:'ass', var:'developer_name', val:`"${s.devName}"`, hi:hi('dev')}); + else lines.push({t:'ph', var:'developer_name', hint:'← tippe deinen Namen'}); + } + if(unlocked.has('gamename') || s.name){ + if(s.name) lines.push({t:'ass', var:'game_name', val:`"${s.name}"`, hi:hi('gamename')}); + else lines.push({t:'ph', var:'game_name', hint:'← tippe den Spielnamen'}); + } + if(unlocked.has('gamedesc') || s.desc){ + if(s.desc) lines.push({t:'ass', var:'game_description', val:`"${s.desc}"`, hi:hi('gamedesc')}); + else lines.push({t:'ph', var:'game_description', hint:'← tippe eine Beschreibung'}); + } + if(unlocked.has('figure') || s.figure){ + if(s.figure) lines.push({t:'ass', var:'player_figure', + val:`"${s.figure}" # ${fig?fig.e+' '+fig.n:''}`, hi:hi('figure')}); + else lines.push({t:'ph', var:'player_figure', hint:'← klicke eine Figur an'}); + } + if(unlocked.has('background') || s.background){ + if(s.background) lines.push({t:'ass', var:'world_setting', + val:`"${s.background}" # ${bg?bg.e+' '+bg.n:''}`, hi:hi('background')}); + else lines.push({t:'ph', var:'world_setting', hint:'← klicke eine Welt an'}); + } + if(unlocked.has('movement')){ + lines.push({t:'ass', var:'movement_type', + val: s.rules.movement==='step' ? '"schritt" # 1 Feld pro Runde' + : '"wuerfeln" # 1–6 Felder', + hi:hi('movement')}); + } + if(unlocked.has('failmode')){ + if(s.rules.fail==='lives'){ + lines.push({t:'ass', var:'verlust_system', val:'"leben"', hi:hi('failmode')}); + lines.push({t:'ass', var:'anzahl_leben', val:String(s.rules.lives||3), hi:hi('lives')}); + } else { + lines.push({t:'ass', var:'verlust_system', val:'"punkte"', hi:hi('failmode')}); + lines.push({t:'ass', var:'punkte_pro_sieg',val:String(s.rules.pts||10), hi:hi('pts')}); + } + } + if(unlocked.has('fieldcount')){ + lines.push({t:'ass', var:'anzahl_felder', val:String(s.fieldCount), hi:hi('fieldcount')}); + } + lines.push({t:'bl'}); + } + + // ── Konsequenz nach Mini-Game ── + if(unlocked.has('consequences')){ + const c = s.consequences || {}; + const win = c.win || 'nothing'; + const lose = c.lose || 'nothing'; + if(win !== 'nothing' || lose !== 'nothing'){ + lines.push({t:'cm', v:'# ── Konsequenz nach Mini-Game ──'}); + lines.push({t:'kw', v:'if', rest:' ergebnis == "gewonnen":'}); + if(win === 'forward') lines.push({t:'ind', v:`spieler.position += ${c.winVal??2} # ${c.winVal??2} Felder vor`, hi:hi('consequences')}); + else if(win === 'points') lines.push({t:'ind', v:`spieler.punkte += ${c.winPts??10} # +${c.winPts??10} Punkte`, hi:hi('consequences')}); + else if(win === 'again') lines.push({t:'ind', v:'spieler.nochmal_wuerfeln = True', hi:hi('consequences')}); + else lines.push({t:'ind', v:'pass', hi:hi('consequences')}); + lines.push({t:'kw', v:'else', rest:':'}); + if(lose === 'back') lines.push({t:'ind', v:`spieler.position -= ${c.loseVal??1} # ${c.loseVal??1} Felder zurück`, hi:hi('consequences')}); + else if(lose === 'life') lines.push({t:'ind', v:'spieler.leben -= 1', hi:hi('consequences')}); + else if(lose === 'points') lines.push({t:'ind', v:`spieler.punkte -= ${c.losePts??5} # −${c.losePts??5} Punkte`, hi:hi('consequences')}); + else if(lose === 'skip') lines.push({t:'ind', v:'spieler.aussetzen = True', hi:hi('consequences')}); + else lines.push({t:'ind', v:'pass', hi:hi('consequences')}); + lines.push({t:'bl'}); + } + } + + // ── Spielfeld aufbauen ── + const hasAnyField = (s.fields||[]).some(Boolean); + if(unlocked.has('fields') || hasAnyField){ + lines.push({t:'cm', v:'# ── Spielfeld aufbauen ──'}); + lines.push({t:'fn-def', name:'setup_spielfeld', args:''}); + lines.push({t:'ind', v:'felder = [Field(index=0, typ="start")]'}); + for(let i=1;i0){ + lines.push({t:'cm', v:'# ── Mini-Game Funktionen ──'}); + usedGames.forEach(id=>{ + const pyName=MG_PY[id]||id; + lines.push({t:'fn-def', name:`spiele_${pyName}`, args:'spieler'}); + lines.push({t:'ind', v:`return minigames.${pyName}.starten(spieler).gewonnen`}); + }); + lines.push({t:'bl'}); + } + + // ── Quiz-Fragen ── + const quizItems=(s.quizData||[]).filter(q=>q&&q.question); + if(quizItems.length>0 || unlocked.has('quiz')){ + lines.push({t:'cm', v:'# ── Quiz-Fragen ──'}); + lines.push({t:'ass', var:'fragen', val:'[', hi:false}); + quizItems.forEach(q=>{ + const clean=q.question.replace(/"/g,"'").slice(0,50); + const ans=q.answers.map(a=>`"${(a||'').replace(/"/g,"'")}"`).join(', '); + const cor=q.correct!=null?`"${(q.answers[q.correct]||'').replace(/"/g,"'")}"`:'None'; + lines.push({t:'ind', v:`{"frage": "${clean}", "antworten": [${ans}], "richtig": ${cor}},`}); + }); + if(quizItems.length===0) lines.push({t:'ind-cm', v:'# noch keine Fragen eingetragen...'}); + lines.push({t:'ass-end', v:']'}); + lines.push({t:'bl'}); + } + + // ── Erzähltexte ── + const storyList = (s.storyItems||[]).filter(st=>st&&st.text&&st.text.trim()); + if(storyList.length>0){ + lines.push({t:'cm', v:'# ── Erzähltexte ──'}); + lines.push({t:'ass', var:'erzaehltexte', val:'[', hi:false}); + storyList.forEach(st=>{ + const txt = st.text.replace(/"/g,"'").slice(0,60); + const pos = st.position==='before' ? `vor Feld ${st.fieldIndex}` : `nach Feld ${st.fieldIndex}`; + lines.push({t:'ind', v:`{"text": "${txt}", "position": "${pos}"},`}); + }); + lines.push({t:'ass-end', v:']'}); + lines.push({t:'bl'}); + } + + // ── Spiel starten ── + lines.push({t:'cm', v:'# ── Spiel starten ──'}); + lines.push({t:'kw', v:'if', rest:" __name__ == '__main__':"}); + lines.push({t:'ind', v:'config = GameConfig('}); + lines.push({t:'ind2', v:`name=${s.name?`"${s.name}"`:'""'},`}); + lines.push({t:'ind2', v:`developer=${s.devName?`"${s.devName}"`:'""'},`}); + if(s.figure) lines.push({t:'ind2', v:`figure="${s.figure}",`}); + if(s.background) lines.push({t:'ind2', v:`setting="${s.background}",`}); + if(hasAnyField) lines.push({t:'ind2', v:'board=setup_spielfeld(),'}); + if(quizItems.length) lines.push({t:'ind2', v:'questions=fragen,'}); + if(storyList.length) lines.push({t:'ind2', v:'stories=erzaehltexte,'}); + lines.push({t:'ind', v:')'}); + lines.push({t:'ind', v:'engine.run(config)'}); + + return lines; +} + +function renderToken(line){ + const esc = s => String(s).replace(/&/g,'&').replace(//g,'>'); + let r=''; + switch(line.t){ + case 'cm': r=`${esc(line.v)}`; break; + case 'bl': return ''; + case 'ph': r=`${esc(line.var)} = "???" ${esc(line.hint||'')}`; break; + case 'kw': r=`${esc(line.v)}${esc(line.rest||'')}`; break; + case 'ass': r=`${esc(line.var)} = ${esc(String(line.val))}`; break; + case 'ass-end':r=`${esc(line.v)}`; break; + case 'fn-def': r=`def ${esc(line.name)}(${esc(line.args||'')}):`; break; + case 'ind': r=` ${esc(line.v)}`; break; + case 'ind2': r=` ${esc(line.v)}`; break; + case 'ind-cm': r=` ${esc(line.v)}`; break; + default: return esc(line.v||''); + } + return line.hi ? `${r}` : r; +} + +let codeTypingTimer = null; +let lastLineCount = 0; +let lastKey_scroll = null; + +function updateCodePane(){ + const lines = buildPythonCode(); + const codeEl = document.getElementById('codeContent'); + const numsEl = document.getElementById('codeLineNums'); + const linesEl= document.getElementById('codeLines'); + const scroll = document.getElementById('codeScroll'); + if(!codeEl) return; + + // Dateiname + const dev = ST.devName ? ST.devName.toLowerCase().replace(/[^a-z0-9]/g,'_') : 'dev'; + const gn = ST.name ? ST.name.toLowerCase().replace(/[^a-z0-9]/g,'_').slice(0,20) : 'boardgame'; + const fnEl = document.getElementById('codeFilename'); + if(fnEl) fnEl.textContent = (ST.name||ST.devName) ? `${gn}_von_${dev}.py` : 'boardgame.py'; + + if(lines.length===0){ + codeEl.innerHTML='# Klicke ein Feld an — dein Code erscheint hier ✨'; + numsEl.innerHTML='
1
'; + if(linesEl) linesEl.textContent='1 Zeile'; + if(scroll) scroll.scrollTop = 0; + lastLineCount=0; return; + } + + let html='', lineNum=1; + const lineNums=[]; + lines.forEach(line=>{ + if(line.t==='bl'){ html+='\n'; } + else { html+=renderToken(line)+'\n'; } + lineNums.push(lineNum++); + }); + + const newCount = lines.length; + const grew = newCount > lastLineCount; + lastLineCount = newCount; + + codeEl.innerHTML = html; + numsEl.innerHTML = lineNums.map(n=>`
${n}
`).join(''); + if(linesEl) linesEl.textContent = lineNum+' Zeilen'; + + const status = document.getElementById('codeStatus'); + if(status){ status.textContent='● Live'; setTimeout(()=>{ status.textContent='● Bereit'; },800); } + + // Nach Layout: scrollTop auf gültigen Bereich begrenzen (verhindert "ins Leere gescrollt") + // UND optional zum Highlight scrollen (nur bei grow + neuem Key, damit nichts thrasht) + if(scroll){ + requestAnimationFrame(()=>{ + const maxScroll = Math.max(0, scroll.scrollHeight - scroll.clientHeight); + if(scroll.scrollTop > maxScroll) scroll.scrollTop = maxScroll; + if(grew && lastKey !== lastKey_scroll){ + lastKey_scroll = lastKey; + const hiEl = codeEl.querySelector('.py-hi'); + if(hiEl){ + const preTop = codeEl.getBoundingClientRect().top; + const hiTop = hiEl.getBoundingClientRect().top; + const offset = hiTop - preTop; + let target = scroll.scrollTop + offset - 80; + target = Math.max(0, Math.min(maxScroll, target)); + scroll.scrollTo({top: target, behavior:'smooth'}); + } + } + }); + } +} + +function scheduleCodeUpdate(delay=80){ + clearTimeout(codeTypingTimer); + codeTypingTimer = setTimeout(updateCodePane, delay); +} + +function downloadPy(){ + const codeEl=document.getElementById('codeContent'); + if(!codeEl)return; + const text=codeEl.innerText||codeEl.textContent||''; + const dev=(ST.devName||'dev').toLowerCase().replace(/[^a-z0-9]/g,'_'); + const gn=(ST.name||'boardgame').toLowerCase().replace(/[^a-z0-9]/g,'_').slice(0,20); + const fname=`${gn}_von_${dev}.py`; + const blob=new Blob([text],{type:'text/plain'}); + const a=document.createElement('a'); + a.href=URL.createObjectURL(blob); + a.download=fname; + a.click(); + URL.revokeObjectURL(a.href); +} + +document.addEventListener('input', ()=>scheduleCodeUpdate(50)); +document.addEventListener('change', ()=>scheduleCodeUpdate(50)); + +// Initial render +setTimeout(updateCodePane, 200); diff --git a/editor.css b/editor.css new file mode 100644 index 0000000..8e0b601 --- /dev/null +++ b/editor.css @@ -0,0 +1,616 @@ +*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} +:root{ + --bg:#0f0e17;--surface:#1a1827;--card:#22203a; + --accent:#f5a623;--accent2:#7c3aed;--accent3:#10b981; + --danger:#ef4444;--text:#fffffe;--muted:#a7a3c2;--border:#2e2b4a;--radius:16px; +} +html,body{height:100%;margin:0;padding:0} +body{ + color:var(--text);font-family:'Nunito',sans-serif; + background:var(--bg); + background-image:radial-gradient(ellipse at 20% 10%,rgba(124,58,237,0.18) 0%,transparent 50%),radial-gradient(ellipse at 80% 90%,rgba(245,166,35,0.10) 0%,transparent 50%); + background-attachment:fixed; + display:flex;flex-direction:column;height:100vh;overflow:hidden; +} +/* ─── SPLIT LAYOUT ─── */ +#splitWrap{ + display:flex;flex-direction:row;flex:1 1 0;overflow:hidden;min-height:0;width:100%; +} +#editorPane{ + flex:0 0 65%;width:65%;overflow-y:auto;overflow-x:hidden;min-width:0; + border-right:1px solid var(--border); + order:1; + scrollbar-width:thin;scrollbar-color:rgba(124,58,237,0.3) transparent; +} +#editorPane::-webkit-scrollbar{width:5px} +#editorPane::-webkit-scrollbar-thumb{background:rgba(124,58,237,0.25);border-radius:3px} +#editorPane::-webkit-scrollbar-thumb:hover{background:rgba(124,58,237,0.5)} +#resizerBar{ + flex:0 0 5px;width:5px;background:var(--border);cursor:col-resize; + transition:background 0.2s;user-select:none; + display:flex;align-items:center;justify-content:center; + position:relative;z-index:10;order:2; +} +#resizerBar:hover,#resizerBar.dragging{background:var(--accent2);} +#resizerBar::after{ + content:'⋮';color:rgba(255,255,255,0.25);font-size:1.1rem;letter-spacing:-2px; + pointer-events:none; +} +#codePane{ + flex:1 1 0;min-width:0;overflow:hidden;display:flex;flex-direction:column; + background:#0d0c1a;order:3; +} + +/* ─── PROGRESS BAR ─── */ +#progressBar{position:relative;z-index:200;flex-shrink:0;width:100%;box-sizing:border-box;background:rgba(15,14,23,0.95);backdrop-filter:blur(14px);border-bottom:1px solid rgba(255,255,255,0.07);padding:10px 24px} +.pb-inner{max-width:100%;margin:0 auto} +.pb-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px} +.pb-logo{font-family:'Fredoka One',cursive;font-size:0.95rem;color:var(--accent);display:flex;align-items:center;gap:8px} +.pb-phwg{display:flex;align-items:center;gap:10px} +.pb-phwg img{height:32px;width:auto;object-fit:contain;opacity:0.92} +.pb-phwg-text{display:flex;flex-direction:column;gap:1px} +.pb-phwg-studiengaenge{font-size:9px;font-weight:700;color:var(--muted);letter-spacing:0.3px;line-height:1.4} +.pb-phwg-studiengaenge span{color:rgba(245,166,35,0.7)} +@media(max-width:640px){.pb-phwg-text{display:none}} +.pb-steps{display:flex;align-items:center;gap:0} +.pb-step{display:flex;align-items:center;gap:7px;flex:1;cursor:default;opacity:0.38;transition:opacity 0.3s} +.pb-step.done{opacity:0.7;cursor:pointer} +.pb-step.done:hover{opacity:1} +.pb-step.active{opacity:1} +.pb-dot{width:28px;height:28px;border-radius:50%;border:2.5px solid var(--muted);display:flex;align-items:center;justify-content:center;font-family:'Fredoka One',cursive;font-size:0.85rem;flex-shrink:0;transition:all 0.3s} +.pb-step.active .pb-dot{border-color:var(--accent);background:var(--accent);color:#000;box-shadow:0 0 12px rgba(245,166,35,0.5)} +.pb-step.done .pb-dot{border-color:var(--accent3);background:var(--accent3);color:#fff} +.pb-label{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:0.5px;white-space:nowrap} +.pb-step.active .pb-label{color:var(--accent)} +.pb-step.done .pb-label{color:var(--accent3)} +.pb-connector{flex:1;height:2px;background:var(--border);margin:0 6px;transition:background 0.4s;min-width:12px} +.pb-connector.done{background:var(--accent3)} +@media(max-width:580px){.pb-label{display:none}} +.pb-track{height:3px;background:var(--border);border-radius:999px;margin-top:10px;overflow:hidden} +.pb-fill{height:100%;background:linear-gradient(90deg,var(--accent3),var(--accent));border-radius:999px;transition:width 0.5s cubic-bezier(0.34,1.56,0.64,1)} + +/* ─── SCREENS ─── */ +.screen{display:none;max-width:680px;margin:0 auto;padding:32px 16px 80px} +.screen.active{display:block} +.slide-in{animation:slideIn 0.38s cubic-bezier(0.16,1,0.3,1) both} +.slide-out{animation:slideOut 0.22s ease both} +@keyframes slideIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:translateX(0)}} +@keyframes slideOut{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(-30px)}} +@keyframes fadeUp{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}} +@keyframes popIn{from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:scale(1)}} +@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}} + +/* ─── SCREEN HEADER ─── */ +.sh{text-align:center;margin-bottom:28px} +.sh-badge{display:inline-block;background:var(--accent2);color:#fff;font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;padding:4px 14px;border-radius:999px;margin-bottom:12px} +.sh-title{font-family:'Fredoka One',cursive;font-size:clamp(1.8rem,4vw,2.5rem)} +.sh-title em{color:var(--accent);font-style:normal} +.sh-sub{color:var(--muted);font-size:14px;margin-top:8px;line-height:1.6} + +/* ─── STEP SUMMARY BANNER ─── */ +.summary-banner{background:linear-gradient(135deg,rgba(16,185,129,0.12),rgba(16,185,129,0.04));border:1.5px solid rgba(16,185,129,0.4);border-radius:14px;padding:18px 22px;margin-bottom:20px;display:none;animation:fadeUp 0.4s ease both} +.summary-banner.visible{display:block} +.sb-title{font-family:'Fredoka One',cursive;font-size:1.1rem;color:var(--accent3);margin-bottom:8px;display:flex;align-items:center;gap:8px} +.sb-chips{display:flex;gap:8px;flex-wrap:wrap} +.sb-chip{background:rgba(16,185,129,0.15);border:1px solid rgba(16,185,129,0.3);border-radius:8px;padding:4px 12px;font-size:12px;font-weight:700;color:var(--accent3)} + +/* ─── CARD ─── */ +.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:26px;margin-bottom:18px} +.card-title{font-family:'Fredoka One',cursive;font-size:1.15rem;margin-bottom:5px;display:flex;align-items:center;gap:10px} +.card-sub{color:var(--muted);font-size:13px;margin-bottom:18px;line-height:1.6} +.divider{height:1px;background:var(--border);margin:20px 0} + +/* ─── FORM ─── */ +label.lbl{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:0.8px;color:var(--muted);margin-bottom:7px;display:block} +.finput,.ftarea{width:100%;background:var(--surface);border:1.5px solid var(--border);border-radius:10px;color:var(--text);font-family:'Nunito',sans-serif;font-size:15px;font-weight:600;padding:12px 16px;outline:none;transition:border-color 0.2s,box-shadow 0.2s} +.finput:focus,.ftarea:focus{border-color:var(--accent2);box-shadow:0 0 0 3px rgba(124,58,237,0.14)} +.finput::placeholder,.ftarea::placeholder{color:#4a476a} +.ftarea{resize:none} +.char-row{display:flex;justify-content:flex-end;font-size:11px;color:var(--muted);margin-top:4px} +.char-row.warn{color:var(--accent)} + +/* ─── NAV BUTTONS ─── */ +.btn-row{display:flex;justify-content:space-between;gap:12px;margin-top:28px} +.btn-back{background:var(--surface);color:var(--muted);font-family:'Fredoka One',cursive;font-size:1rem;border:1.5px solid var(--border);border-radius:12px;padding:13px 24px;cursor:pointer;transition:all 0.2s} +.btn-back:hover{border-color:var(--muted);color:var(--text)} +.btn-next{background:linear-gradient(135deg,var(--accent2),var(--accent));color:#fff;font-family:'Fredoka One',cursive;font-size:1rem;border:none;border-radius:12px;padding:14px 32px;cursor:pointer;display:flex;align-items:center;gap:8px;transition:transform 0.15s,box-shadow 0.15s,opacity 0.2s;box-shadow:0 4px 20px rgba(124,58,237,0.35)} +.btn-next:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 8px 28px rgba(124,58,237,0.45)} +.btn-next:disabled{opacity:0.32;cursor:not-allowed;transform:none} + +/* ─── PREVIEW CARD ─── */ +.preview-card{background:linear-gradient(135deg,rgba(124,58,237,0.15),rgba(245,166,35,0.08));border:1px solid rgba(124,58,237,0.35);border-radius:14px;padding:18px 22px;margin-bottom:18px;animation:fadeUp 0.3s ease both} +.preview-card .pv-label{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:var(--accent2);margin-bottom:6px} +.preview-card .pv-name{font-family:'Fredoka One',cursive;font-size:1.5rem;color:var(--accent)} +.preview-card .pv-desc{font-size:13px;color:var(--muted);margin-top:4px;line-height:1.5} + +/* ─── SUGGESTIONS ─── */ +.suggestions{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px} +.sug{background:var(--surface);border:1px solid var(--border);border-radius:7px;padding:4px 12px;font-size:12px;color:var(--muted);cursor:pointer;transition:all 0.15s} +.sug:hover{border-color:var(--accent2);color:var(--text)} + +/* ─── SELECTION GRID (step 2) ─── */ +.sel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:9px;margin-bottom:14px} +.sel-card{background:var(--surface);border:2px solid var(--border);border-radius:12px;padding:14px 6px;text-align:center;cursor:pointer;transition:all 0.2s;user-select:none} +.sel-card:hover{border-color:var(--accent2);transform:translateY(-2px)} +.sel-card.selected{border-color:var(--accent);background:rgba(245,166,35,0.07);box-shadow:0 0 14px rgba(245,166,35,0.2)} +.sel-card.disabled{opacity:0.35;cursor:not-allowed;filter:grayscale(0.7)} +.sel-card.disabled:hover{transform:none;border-color:var(--border)} +.sel-card .se{font-size:1.9rem;display:block;margin-bottom:5px} +.sel-card .sl{font-size:11px;font-weight:700;color:var(--muted)} +.sel-card.selected .sl{color:var(--accent)} +.combo-preview{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px;text-align:center;margin-top:10px;animation:popIn 0.3s ease both} +.combo-preview .cp-scene{font-size:3rem;display:block;margin-bottom:6px;animation:float 3s ease-in-out infinite} +.combo-preview .cp-label{font-size:12px;color:var(--muted);font-weight:700} + +/* ─── RULES (step 3) ─── */ +.rules-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:4px} +@media(max-width:560px){.rules-grid{grid-template-columns:1fr}} +.rule-block{background:var(--surface);border:1.5px solid var(--border);border-radius:12px;padding:15px} +.rule-block-title{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:0.8px;color:var(--muted);margin-bottom:11px} +.toggle-group{display:flex;flex-direction:column;gap:8px} +.toggle-option{display:flex;align-items:center;gap:9px;padding:10px 12px;border-radius:9px;border:2px solid var(--border);cursor:pointer;transition:all 0.2s;user-select:none} +.toggle-option:hover{border-color:var(--accent2)} +.toggle-option.selected{border-color:var(--accent);background:rgba(245,166,35,0.07)} +.tog-emoji{font-size:1.2rem;flex-shrink:0} +.tog-text{font-size:13px;font-weight:700} +.tog-sub{font-size:11px;color:var(--muted)} +.tog-check{margin-left:auto;width:18px;height:18px;border-radius:50%;border:2px solid var(--border);transition:all 0.2s;flex-shrink:0} +.toggle-option.selected .tog-check{background:var(--accent);border-color:var(--accent)} +.sub-opts{display:none;gap:7px;margin-top:9px;flex-wrap:wrap} +.sub-opts.visible{display:flex} +.sub-opt{background:var(--card);border:1.5px solid var(--border);border-radius:8px;padding:6px 13px;font-size:13px;font-weight:700;cursor:pointer;transition:all 0.15s;color:var(--muted)} +.sub-opt:hover{border-color:var(--accent2);color:var(--text)} +.sub-opt.selected{border-color:var(--accent);color:var(--accent);background:rgba(245,166,35,0.07)} +.count-row{display:flex;align-items:center;gap:14px;background:var(--surface);border:1.5px solid var(--border);border-radius:12px;padding:14px 18px;margin-bottom:18px} +.count-row label{font-size:13px;font-weight:800;text-transform:uppercase;letter-spacing:0.5px;color:var(--muted)} +.count-ctrl{display:flex;align-items:center;gap:10px;margin-left:auto} +.cnt-btn{width:36px;height:36px;border-radius:8px;background:var(--card);border:1.5px solid var(--border);color:var(--text);font-size:1.2rem;font-weight:800;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.15s} +.cnt-btn:hover:not(:disabled){border-color:var(--accent);color:var(--accent)} +.cnt-btn:disabled{opacity:0.3;cursor:not-allowed} +.cnt-val{font-family:'Fredoka One',cursive;font-size:1.8rem;color:var(--accent);min-width:36px;text-align:center} + +/* ─── PALETTE ─── */ +.pal-tabs{display:flex;gap:8px;margin-bottom:12px} +.pal-tab{background:var(--surface);border:1.5px solid var(--border);border-radius:9px;padding:7px 16px;font-size:12px;font-weight:800;cursor:pointer;color:var(--muted);transition:all 0.2s;text-transform:uppercase;letter-spacing:0.5px} +.pal-tab.active{background:var(--accent2);border-color:var(--accent2);color:#fff} +.pal-panel{display:none} +.pal-panel.active{display:grid;grid-template-columns:repeat(auto-fill,minmax(105px,1fr));gap:9px;margin-bottom:18px} +.mg-card{background:var(--surface);border:2px solid var(--border);border-radius:11px;padding:11px 7px 8px;display:flex;flex-direction:column;align-items:center;gap:4px;cursor:grab;transition:all 0.2s;user-select:none;position:relative} +.mg-card:hover:not(.disabled){border-color:var(--accent2);transform:translateY(-2px);box-shadow:0 5px 18px rgba(124,58,237,0.2)} +.mg-card.click-sel{outline:3px solid var(--accent);outline-offset:2px;background:rgba(245,166,35,0.06)} +.mg-card.disabled{opacity:0.28;cursor:not-allowed;filter:grayscale(0.5);pointer-events:none} +.mg-emoji{font-size:1.6rem;pointer-events:none} +.mg-lbl{font-size:11px;font-weight:700;color:var(--muted);text-align:center;line-height:1.3;pointer-events:none} +.mg-tag{font-size:9px;font-weight:800;letter-spacing:0.5px;text-transform:uppercase;padding:2px 7px;border-radius:999px;pointer-events:none} +.mg-tag.once{background:rgba(239,68,68,0.2);color:#f87171} +.mg-tag.multi{background:rgba(16,185,129,0.2);color:var(--accent3)} +.mg-tag.unlim{background:rgba(245,166,35,0.2);color:var(--accent)} +.mg-tag.story-tag{background:rgba(6,182,212,0.2);color:#22d3ee} +.mg-tag.requires2p{background:rgba(124,58,237,0.25);color:#c4b5fd} +.mg-card.disabled .mg-test-btn{pointer-events:none;} +.mg-lock{position:absolute;bottom:4px;left:4px;right:4px;font-size:9px;font-weight:700;color:#a78bfa;background:rgba(124,58,237,0.15);border:1px solid rgba(124,58,237,0.35);border-radius:6px;padding:2px 4px;text-align:center;pointer-events:none;} +.use-count{position:absolute;top:4px;right:4px;background:var(--accent);color:#000;font-size:10px;font-weight:900;width:17px;height:17px;border-radius:50%;display:none;align-items:center;justify-content:center;pointer-events:none} +.mg-card.used .use-count{display:flex} +.story-counter{display:inline-flex;align-items:center;gap:5px;background:rgba(6,182,212,0.12);border:1px solid rgba(6,182,212,0.4);border-radius:999px;padding:3px 10px;font-size:11px;font-weight:800;color:#22d3ee} +.story-counter.maxed{background:rgba(239,68,68,0.12);border-color:var(--danger);color:var(--danger)} +.sec-label{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-bottom:10px;display:flex;align-items:center;justify-content:space-between} + +/* ─── BOARD ─── */ +.board-builder{display:flex;flex-direction:column;gap:2px} +.drop-zone{height:10px;border-radius:7px;border:2px dashed transparent;transition:all 0.2s;display:flex;align-items:center;justify-content:center;font-size:11px;color:transparent;font-weight:700;cursor:pointer} +.drop-zone.story-hoverable{height:28px;border-color:rgba(6,182,212,0.3);color:rgba(6,182,212,0.4)} +.drop-zone.drag-over{height:38px;background:rgba(6,182,212,0.12);border-color:#22d3ee;color:#22d3ee} +.field-row{display:flex;align-items:center;gap:10px;background:var(--surface);border:1.5px solid var(--border);border-radius:9px;padding:9px 13px;transition:all 0.2s;cursor:pointer} +.field-row:hover:not(.start-f):not(.end-f){border-color:var(--accent2)} +.field-row.filled{border-color:rgba(124,58,237,0.5)} +.field-row.start-f{border-color:var(--accent3);cursor:default} +.field-row.end-f{border-color:var(--accent);cursor:default} +.field-row.drag-target{background:rgba(124,58,237,0.1);border-color:var(--accent)!important;border-style:dashed} +.f-num{font-family:'Fredoka One',cursive;font-size:0.85rem;color:var(--muted);min-width:54px;flex-shrink:0} +.field-row.start-f .f-num{color:var(--accent3)} +.field-row.end-f .f-num{color:var(--accent)} +.f-icon{font-size:1.2rem;min-width:26px;text-align:center;flex-shrink:0} +.f-name{font-size:13px;font-weight:700;flex:1} +.f-empty{color:var(--muted);font-style:italic;font-weight:400} +.f-clear{background:none;border:none;color:var(--muted);cursor:pointer;font-size:0.9rem;padding:4px 6px;border-radius:6px;transition:all 0.15s;display:none} +.field-row.filled .f-clear{display:block} +.f-clear:hover{color:var(--danger);background:rgba(239,68,68,0.1)} +.story-row{display:flex;align-items:flex-start;gap:10px;background:rgba(6,182,212,0.07);border:1.5px solid rgba(6,182,212,0.4);border-radius:9px;padding:11px 13px;animation:fadeUp 0.25s ease both} +.story-icon{font-size:1.3rem;cursor:pointer;transition:transform 0.2s;flex-shrink:0;margin-top:2px} +.story-icon:hover{transform:scale(1.2)} +.story-content{flex:1} +.story-lbl{font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:#22d3ee;margin-bottom:4px} +.story-ta{width:100%;background:rgba(6,182,212,0.06);border:1.5px solid rgba(6,182,212,0.3);border-radius:7px;color:var(--text);font-family:'Nunito',sans-serif;font-size:13px;font-weight:600;padding:7px 10px;resize:none;outline:none;transition:border-color 0.2s} +.story-ta:focus{border-color:#22d3ee} +.story-ta::placeholder{color:#3a6e7a} +.story-meta{display:flex;align-items:center;gap:6px;margin-top:5px} +.story-pos-lbl{font-size:10px;font-weight:700;color:#22d3ee;background:rgba(6,182,212,0.12);padding:2px 8px;border-radius:999px} +.story-chars{font-size:10px;color:var(--muted);margin-left:auto} +.story-clear{background:none;border:none;color:var(--muted);cursor:pointer;font-size:0.85rem;padding:4px;border-radius:5px;transition:all 0.15s;flex-shrink:0} +.story-clear:hover{color:var(--danger);background:rgba(239,68,68,0.1)} +.emoji-picker{position:fixed;background:var(--card);border:1.5px solid var(--border);border-radius:12px;padding:10px;display:grid;grid-template-columns:repeat(6,1fr);gap:4px;z-index:1000;box-shadow:0 8px 30px rgba(0,0,0,0.5);display:none} +.emoji-picker.open{display:grid} +.ep-it{font-size:1.2rem;cursor:pointer;padding:4px;border-radius:6px;text-align:center;transition:background 0.15s} +.ep-it:hover{background:var(--surface)} +.board-stats{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px} +.stat-pill{background:var(--surface);border:1px solid var(--border);border-radius:999px;padding:5px 13px;font-size:12px;font-weight:700;color:var(--muted);display:flex;align-items:center;gap:5px} +.stat-pill span{color:var(--text)} +#dragGhost{position:fixed;pointer-events:none;z-index:9999;background:var(--card);border:2px solid var(--accent);border-radius:10px;padding:7px 14px;font-size:1rem;display:flex;align-items:center;gap:7px;box-shadow:0 8px 28px rgba(0,0,0,0.6);transform:translate(-50%,-50%);opacity:0;transition:opacity 0.1s;font-family:'Nunito',sans-serif;font-weight:700;color:var(--text)} + +/* ─── QUIZ (step 4) ─── */ +.quiz-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:22px;margin-bottom:14px;position:relative;overflow:hidden} +.quiz-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:4px;background:linear-gradient(180deg,var(--accent2),var(--accent))} +.quiz-card.complete::before{background:linear-gradient(180deg,var(--accent3),#059669)} +.qc-head{display:flex;align-items:center;gap:10px;margin-bottom:14px} +.qc-num{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--accent2),var(--accent));color:#fff;font-family:'Fredoka One',cursive;font-size:0.9rem;display:flex;align-items:center;justify-content:center;flex-shrink:0} +.quiz-card.complete .qc-num{background:linear-gradient(135deg,var(--accent3),#059669)} +.qc-title{font-family:'Fredoka One',cursive;font-size:1rem;flex:1} +.qc-status{font-size:11px;font-weight:800;text-transform:uppercase;padding:3px 9px;border-radius:999px} +.qc-status.done{background:rgba(16,185,129,0.12);color:var(--accent3);border:1px solid var(--accent3)} +.qc-status.todo{background:rgba(245,166,35,0.08);color:var(--accent);border:1px solid var(--accent)} +.q-input{width:100%;background:var(--surface);border:1.5px solid var(--border);border-radius:9px;color:var(--text);font-family:'Nunito',sans-serif;font-size:14px;font-weight:600;padding:11px 13px;outline:none;transition:border-color 0.2s,box-shadow 0.2s;margin-bottom:12px;resize:none} +.q-input:focus{border-color:var(--accent2);box-shadow:0 0 0 3px rgba(124,58,237,0.12)} +.q-input::placeholder{color:#4a476a} +.ans-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-bottom:12px} +@media(max-width:480px){.ans-grid{grid-template-columns:1fr}} +.ans-wrap{position:relative} +.ans-letter{position:absolute;left:10px;top:50%;transform:translateY(-50%);font-family:'Fredoka One',cursive;font-size:0.85rem;width:22px;height:22px;border-radius:6px;display:flex;align-items:center;justify-content:center;background:var(--border);color:var(--muted);pointer-events:none;z-index:1;transition:all 0.2s} +.ans-wrap.correct .ans-letter{background:var(--accent3);color:#fff} +.ans-input{width:100%;background:var(--surface);border:1.5px solid var(--border);border-radius:9px;color:var(--text);font-family:'Nunito',sans-serif;font-size:13px;font-weight:600;padding:9px 11px 9px 40px;outline:none;transition:border-color 0.2s} +.ans-input:focus{border-color:var(--accent2)} +.ans-wrap.correct .ans-input{border-color:var(--accent3);background:rgba(16,185,129,0.06)} +.correct-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap} +.correct-lbl{font-size:12px;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px} +.correct-btns{display:flex;gap:7px} +.correct-btn{width:33px;height:33px;border-radius:8px;background:var(--surface);border:1.5px solid var(--border);font-family:'Fredoka One',cursive;font-size:0.85rem;color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s} +.correct-btn:hover{border-color:var(--accent3);color:var(--accent3)} +.correct-btn.selected{background:var(--accent3);border-color:var(--accent3);color:#fff;box-shadow:0 0 10px rgba(16,185,129,0.35)} +.tips-btn{background:none;border:none;color:var(--accent2);font-size:12px;font-weight:800;cursor:pointer;padding:0;margin-top:10px;display:flex;align-items:center;gap:5px;text-transform:uppercase;letter-spacing:0.5px} +.tips-box{background:rgba(124,58,237,0.07);border:1px solid rgba(124,58,237,0.25);border-radius:9px;padding:11px 13px;margin-top:7px;font-size:13px;color:var(--muted);line-height:1.7;display:none} +.tips-box.open{display:block} +.no-quiz{text-align:center;padding:36px 20px} +.no-quiz .nq-icon{font-size:3rem;margin-bottom:10px} +.no-quiz h3{font-family:'Fredoka One',cursive;font-size:1.2rem;color:var(--muted);margin-bottom:6px} +.af-bar{display:flex;align-items:center;gap:12px;background:var(--surface);border:1px solid var(--border);border-radius:11px;padding:13px 16px;margin-bottom:14px} +.af-icon{font-size:1.4rem;flex-shrink:0} +.af-text .af-title{font-family:'Fredoka One',cursive;font-size:0.95rem} +.af-text .af-sub{font-size:12px;color:var(--muted)} +.btn-sm{background:linear-gradient(135deg,var(--accent2),var(--accent));color:#fff;font-family:'Fredoka One',cursive;font-size:0.85rem;border:none;border-radius:8px;padding:9px 16px;cursor:pointer;transition:transform 0.15s;white-space:nowrap;flex-shrink:0} +.btn-sm:hover{transform:translateY(-1px)} +.ov-bar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px} +.ov-pill{background:var(--card);border:1px solid var(--border);border-radius:999px;padding:5px 13px;font-size:12px;font-weight:700;color:var(--muted);display:flex;align-items:center;gap:5px} +.ov-pill span{color:var(--text)} +.ov-pill.good span{color:var(--accent3)} +.ov-pill.warn span{color:var(--accent)} + +/* ─── REVIEW + TEST (step 5) ─── */ +.hero-card{background:linear-gradient(135deg,rgba(124,58,237,0.18),rgba(245,166,35,0.08));border:1px solid rgba(124,58,237,0.35);border-radius:var(--radius);padding:26px;margin-bottom:18px;display:flex;align-items:center;gap:18px} +.hero-scene{font-size:3rem;line-height:1;flex-shrink:0;position:relative} +.hero-fig{position:absolute;bottom:-4px;right:-14px;font-size:2rem} +.hero-info .hi-name{font-family:'Fredoka One',cursive;font-size:1.7rem;color:var(--accent)} +.hero-info .hi-desc{color:var(--muted);font-size:13px;margin-top:4px;line-height:1.5} +.hero-tags{display:flex;gap:7px;flex-wrap:wrap;margin-top:9px} +.hero-tag{background:rgba(124,58,237,0.18);border:1px solid rgba(124,58,237,0.3);border-radius:999px;padding:3px 11px;font-size:11px;font-weight:700;color:#c4b5fd} +.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px} +@media(max-width:500px){.info-grid{grid-template-columns:1fr}} +.info-block{background:var(--surface);border:1px solid var(--border);border-radius:11px;padding:13px} +.ib-lbl{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:var(--muted);margin-bottom:4px} +.ib-val{font-family:'Fredoka One',cursive;font-size:1.05rem} +.tl{display:flex;flex-direction:column} +.tl-item{display:flex;align-items:flex-start;gap:11px;padding:7px 0;position:relative} +.tl-item:not(:last-child)::after{content:'';position:absolute;left:16px;top:38px;bottom:-7px;width:2px;background:var(--border)} +.tl-item.story-it:not(:last-child)::after{background:rgba(6,182,212,0.3)} +.tl-dot{width:34px;height:34px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:0.95rem;border:2px solid var(--border);background:var(--surface)} +.tl-dot.s{background:rgba(16,185,129,0.12);border-color:var(--accent3)} +.tl-dot.e{background:rgba(245,166,35,0.12);border-color:var(--accent)} +.tl-dot.st{background:rgba(6,182,212,0.12);border-color:#22d3ee} +.tl-dot.g{background:rgba(124,58,237,0.12);border-color:var(--accent2)} +.tl-dot.em{opacity:0.35} +.tl-lbl{font-size:13px;font-weight:800;padding-top:6px} +.tl-lbl.s{color:var(--accent3)}.tl-lbl.e{color:var(--accent)}.tl-lbl.st{color:#22d3ee}.tl-lbl.g,.tl-lbl.em{color:var(--muted)} +.tl-sub{font-size:12px;color:var(--muted);margin-top:1px;line-height:1.5} +.tl-badge{display:inline-block;background:rgba(124,58,237,0.12);border:1px solid var(--accent2);border-radius:6px;padding:2px 7px;font-size:10px;font-weight:800;color:var(--accent2);margin-top:2px;text-transform:uppercase;letter-spacing:0.5px} +.tl-badge.st{background:rgba(6,182,212,0.1);border-color:#22d3ee;color:#22d3ee} +.qp-list{display:flex;flex-direction:column;gap:7px} +.qp-item{background:var(--surface);border:1px solid var(--border);border-radius:9px;padding:11px 13px} +.qp-q{font-size:13px;font-weight:700;margin-bottom:6px} +.qp-ans-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px} +.qp-ans{font-size:11px;padding:4px 8px;border-radius:6px;background:var(--card);border:1px solid var(--border);color:var(--muted);display:flex;align-items:center;gap:4px} +.qp-ans.cor{background:rgba(16,185,129,0.1);border-color:var(--accent3);color:var(--accent3);font-weight:700} +.checklist{display:flex;flex-direction:column;gap:6px} +.chk-item{background:var(--surface);border:1px solid var(--border);border-radius:9px;padding:9px 13px;display:flex;align-items:center;gap:8px;font-size:13px} +.chk-item.ok{border-color:rgba(16,185,129,0.3);color:var(--accent3)} +.chk-item.warn{border-color:rgba(245,166,35,0.3);color:var(--accent)} + +/* ─── LAUNCH CARD ─── */ +.launch-hero{background:linear-gradient(135deg,rgba(124,58,237,0.22),rgba(245,166,35,0.1));border-bottom:1px solid var(--border);padding:30px;text-align:center;position:relative;overflow:hidden} +.launch-hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at 50% 0%,rgba(124,58,237,0.28),transparent 70%)} +.lh-fig{font-size:3.8rem;display:block;margin-bottom:9px;animation:float 3s ease-in-out infinite;position:relative;z-index:1} +.lh-name{font-family:'Fredoka One',cursive;font-size:1.7rem;color:var(--accent);position:relative;z-index:1} +.lh-desc{color:var(--muted);font-size:13px;margin-top:4px;max-width:360px;margin-left:auto;margin-right:auto;line-height:1.5;position:relative;z-index:1} +.lh-pills{display:flex;gap:7px;justify-content:center;flex-wrap:wrap;margin-top:11px;position:relative;z-index:1} +.lh-pill{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.13);border-radius:999px;padding:4px 12px;font-size:12px;font-weight:700;color:rgba(255,255,255,0.72)} +.launch-body{padding:22px 26px} +.launch-chk{display:flex;flex-direction:column;gap:6px;margin-bottom:18px} +.lc-item{display:flex;align-items:center;gap:9px;background:var(--surface);border:1px solid var(--border);border-radius:9px;padding:9px 13px;font-size:13px;font-weight:700} +.lc-check{color:var(--accent3);margin-left:auto} +.btn-launch{width:100%;background:linear-gradient(135deg,var(--accent2),var(--accent));color:#fff;font-family:'Fredoka One',cursive;font-size:1.15rem;border:none;border-radius:13px;padding:17px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:9px;box-shadow:0 5px 26px rgba(124,58,237,0.4);transition:transform 0.15s,box-shadow 0.15s,opacity 0.2s;position:relative;overflow:hidden} +.btn-launch::after{content:'';position:absolute;inset:0;background:linear-gradient(135deg,rgba(255,255,255,0.1),transparent)} +.btn-launch:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 9px 32px rgba(124,58,237,0.5)} +.btn-launch:disabled{opacity:0.5;cursor:not-allowed;transform:none} +.btn-launch-sub{text-align:center;font-size:12px;color:var(--muted);margin-top:7px;font-weight:700} +.played-badge{display:none;background:rgba(16,185,129,0.1);border:1px solid var(--accent3);border-radius:9px;padding:11px 15px;margin-top:12px;font-size:13px;font-weight:700;color:var(--accent3);align-items:center;gap:7px} +.played-badge.visible{display:flex} + +/* ─── FEEDBACK ─── */ +.fb-prog{height:4px;background:var(--border);border-radius:999px;margin-bottom:22px;overflow:hidden} +.fb-prog-fill{height:100%;background:linear-gradient(90deg,var(--accent2),var(--accent));border-radius:999px;transition:width 0.4s ease} +.fb-qnum{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:var(--accent2);margin-bottom:5px} +.fb-q{font-family:'Fredoka One',cursive;font-size:1.1rem;margin-bottom:11px;line-height:1.4} +.fb-sub{font-size:13px;color:var(--muted);margin-bottom:11px;margin-top:-5px} +.mc-opts{display:flex;flex-direction:column;gap:7px} +.mc-opt{background:var(--surface);border:2px solid var(--border);border-radius:9px;padding:11px 15px;cursor:pointer;transition:all 0.2s;display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600} +.mc-opt:hover{border-color:var(--accent2)} +.mc-opt.sel{border-color:var(--accent);background:rgba(245,166,35,0.07);color:var(--accent)} +.mc-dot{width:19px;height:19px;border-radius:50%;border:2px solid var(--border);flex-shrink:0;transition:all 0.2s} +.mc-opt.sel .mc-dot{background:var(--accent);border-color:var(--accent)} +.scale-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap} +.scale-btn{width:46px;height:46px;border-radius:9px;border:2px solid var(--border);background:var(--surface);font-family:'Fredoka One',cursive;font-size:1.05rem;cursor:pointer;color:var(--muted);transition:all 0.2s;display:flex;align-items:center;justify-content:center} +.scale-btn:hover{border-color:var(--accent2);color:var(--text)} +.scale-btn.sel{background:var(--accent);border-color:var(--accent);color:#000} +.scale-lbls{display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-top:5px;font-weight:700} +.fb-ta{width:100%;background:var(--surface);border:1.5px solid var(--border);border-radius:9px;color:var(--text);font-family:'Nunito',sans-serif;font-size:14px;font-weight:600;padding:11px 13px;outline:none;resize:none;transition:border-color 0.2s} +.fb-ta:focus{border-color:var(--accent2)} +.fb-ta::placeholder{color:#4a476a} +.fb-nav{display:flex;gap:9px;margin-top:18px} +.btn-fb-prev{background:var(--surface);color:var(--muted);font-family:'Fredoka One',cursive;font-size:0.9rem;border:1.5px solid var(--border);border-radius:10px;padding:11px 20px;cursor:pointer;transition:all 0.2s} +.btn-fb-prev:hover{border-color:var(--muted);color:var(--text)} +.btn-fb-next{flex:1;background:linear-gradient(135deg,var(--accent2),var(--accent));color:#fff;font-family:'Fredoka One',cursive;font-size:1rem;border:none;border-radius:10px;padding:12px;cursor:pointer;transition:transform 0.15s,opacity 0.2s;box-shadow:0 4px 14px rgba(124,58,237,0.3)} +.btn-fb-next:hover:not(:disabled){transform:translateY(-1px)} +.btn-fb-next:disabled{opacity:0.32;cursor:not-allowed} + +/* ─── SUMMARY ─── */ +.sum-hero{text-align:center;padding:30px 22px 18px} +.sum-icon{font-size:3.5rem;display:block;margin-bottom:10px} +.sum-hero h2{font-family:'Fredoka One',cursive;font-size:1.9rem;color:var(--accent3)} +.sum-hero p{color:var(--muted);font-size:13px;margin-top:5px;line-height:1.6} +.sum-scores{display:flex;gap:9px;justify-content:center;flex-wrap:wrap;margin-top:14px} +.sum-sc{background:var(--surface);border:1px solid var(--border);border-radius:11px;padding:11px 16px;text-align:center} +.sum-val{font-family:'Fredoka One',cursive;font-size:1.5rem;color:var(--accent)} +.sum-lbl{font-size:10px;font-weight:800;text-transform:uppercase;color:var(--muted);margin-top:2px} +.sum-notes{display:flex;flex-direction:column;gap:6px;margin-bottom:18px} +.sn-item{display:flex;align-items:flex-start;gap:8px;font-size:13px;padding:8px 0;border-bottom:1px solid var(--border)} +.sn-item:last-child{border-bottom:none} +.final-btns{display:flex;gap:10px;justify-content:center;flex-wrap:wrap} +.btn-revise{background:var(--surface);color:var(--text);font-family:'Fredoka One',cursive;font-size:1rem;border:1.5px solid var(--border);border-radius:11px;padding:13px 22px;cursor:pointer;transition:all 0.2s} +.btn-revise:hover{border-color:var(--accent2)} +.btn-publish{background:linear-gradient(135deg,var(--accent3),#059669);color:#fff;font-family:'Fredoka One',cursive;font-size:1.05rem;border:none;border-radius:11px;padding:14px 28px;cursor:pointer;transition:transform 0.15s,box-shadow 0.15s;box-shadow:0 4px 18px rgba(16,185,129,0.38)} +.btn-publish:hover{transform:translateY(-2px);box-shadow:0 8px 26px rgba(16,185,129,0.48)} + +/* ─── TOAST ─── */ +#toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(16px);background:rgba(16,185,129,0.96);color:#fff;font-family:'Fredoka One',cursive;font-size:0.95rem;padding:11px 26px;border-radius:11px;z-index:500;opacity:0;transition:all 0.3s;pointer-events:none;white-space:nowrap;box-shadow:0 4px 18px rgba(0,0,0,0.3)} +#toast.show{opacity:1;transform:translateX(-50%) translateY(0)} + +/* ─── MINI-GAME TEST POPUP ─── */ +.mg-card{position:relative;} +.mg-test-btn{display:block;width:100%;margin-top:6px;background:rgba(124,58,237,0.25);border:1px solid rgba(124,58,237,0.5);border-radius:6px;color:#c4b5fd;font-size:10px;font-weight:800;padding:4px 0;cursor:pointer;text-transform:uppercase;letter-spacing:0.5px;transition:all 0.15s;z-index:2;} +.mg-test-btn:hover{background:rgba(124,58,237,0.55);color:#fff;border-color:#7c3aed;} +.mg-card.disabled .mg-test-btn{display:none;} +/* ── Spielmodus-Auswahl (Schritt 2) ── */ +.pmode-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:8px;} +.pmode-card{cursor:pointer;border:2px solid #2e2b4a;background:#13111f;border-radius:14px;padding:18px 16px;text-align:center;transition:all 0.15s;user-select:none;} +.pmode-card:hover{border-color:#5b21b6;background:#1a162e;transform:translateY(-2px);} +.pmode-card.active{border-color:#7c3aed;background:linear-gradient(135deg,rgba(124,58,237,0.20),rgba(124,58,237,0.08));box-shadow:0 6px 22px rgba(124,58,237,0.30);} +.pmode-icon{font-size:2.4rem;margin-bottom:4px;} +.pmode-label{font-family:'Fredoka One',cursive;font-size:1.05rem;color:#e2e0f0;margin-bottom:2px;} +.pmode-card.active .pmode-label{color:#c4b5fd;} +.pmode-desc{font-size:11px;color:var(--muted);} + +/* ══════════ TOUR-OVERLAY (Schritt 3) ══════════ */ +.tour-root{position:fixed;inset:0;z-index:8800;pointer-events:none;} +.tour-blocker{position:fixed;inset:0;background:rgba(0,0,0,0.05);pointer-events:auto;z-index:8810;cursor:not-allowed;transition:background 0.35s ease;} +.tour-root.no-anchor .tour-blocker{background:rgba(8,6,20,0.78);} +.tour-highlight{ + position:fixed;border-radius:14px;pointer-events:none; + box-shadow:0 0 0 9999px rgba(8,6,20,0.78),0 0 0 2px rgba(167,139,250,0.55),0 0 30px rgba(167,139,250,0.45) inset; + transition:top 0.45s cubic-bezier(0.25,1,0.5,1),left 0.45s cubic-bezier(0.25,1,0.5,1),width 0.45s ease,height 0.45s ease; + z-index:8820; +} +.tour-character{ + position:fixed;display:flex;align-items:flex-start;gap:14px; + z-index:8830;pointer-events:auto;width:380px; + transition:left 0.5s cubic-bezier(0.34,1.3,0.5,1),top 0.5s cubic-bezier(0.34,1.3,0.5,1); +} +.tour-character[data-side="left"]{flex-direction:row-reverse;} +.tour-figure{ + font-size:3.4rem;line-height:1;flex-shrink:0;margin-top:6px; + filter:drop-shadow(0 6px 18px rgba(124,58,237,0.55)); + animation:tourBob 1.8s ease-in-out infinite; +} +@keyframes tourBob{ + 0%,100%{transform:translateY(0) rotate(-2deg);} + 50%{transform:translateY(-6px) rotate(2deg);} +} +.tour-bubble{ + background:#1a1827;border:2px solid #7c3aed;border-radius:16px;padding:16px 18px 12px; + color:#e2e0f0;font-family:'Nunito',sans-serif;font-size:14px;line-height:1.55; + box-shadow:0 14px 50px rgba(0,0,0,0.7),0 0 0 1px rgba(167,139,250,0.2); + position:relative;flex:1;min-width:0; + animation:tourBubblePop 0.35s cubic-bezier(0.34,1.5,0.5,1); +} +@keyframes tourBubblePop{from{transform:scale(0.92);opacity:0;}to{transform:scale(1);opacity:1;}} +/* Sprechblasen-Schwanz */ +.tour-character[data-side="right"] .tour-bubble::before, +.tour-character[data-side="right"] .tour-bubble::after{content:'';position:absolute;top:24px;border:10px solid transparent;} +.tour-character[data-side="right"] .tour-bubble::before{left:-11px;border-right-color:#7c3aed;} +.tour-character[data-side="right"] .tour-bubble::after {left:-8px;border-right-color:#1a1827;border-width:9px;top:25px;} +.tour-character[data-side="left"] .tour-bubble::before, +.tour-character[data-side="left"] .tour-bubble::after{content:'';position:absolute;top:24px;border:10px solid transparent;} +.tour-character[data-side="left"] .tour-bubble::before{right:-11px;border-left-color:#7c3aed;} +.tour-character[data-side="left"] .tour-bubble::after {right:-8px;border-left-color:#1a1827;border-width:9px;top:25px;} +.tour-bubble-text{min-height:60px;cursor:pointer;user-select:none;white-space:pre-line;} +.tour-bubble-text::after{content:'▌';opacity:0.6;animation:tourCaret 0.9s steps(2) infinite;margin-left:1px;} +@keyframes tourCaret{0%,49%{opacity:0.65;}50%,100%{opacity:0;}} +.tour-bubble-progress{font-size:10px;color:#7c7596;text-align:right;margin-top:6px;font-weight:700;letter-spacing:1px;} +.tour-bubble-actions{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px;} +.tour-btn-next{ + background:linear-gradient(135deg,#7c3aed,#a78bfa);color:#fff; + font-family:'Fredoka One',cursive;font-size:14px;border:none;border-radius:10px; + padding:9px 18px;cursor:pointer;transition:transform 0.15s,box-shadow 0.15s; +} +.tour-btn-next:hover{transform:translateY(-2px);box-shadow:0 8px 22px rgba(124,58,237,0.55);} +.tour-btn-skip{ + background:transparent;color:#7c7596;border:1px solid #2e2b4a;border-radius:8px; + padding:6px 12px;font-size:12px;cursor:pointer;font-family:'Nunito',sans-serif; +} +.tour-btn-skip:hover{color:#cdc9e6;border-color:#3a3658;background:rgba(255,255,255,0.04);} +.tour-btn-back{ + background:transparent;color:#cdc9e6;border:1px solid #2e2b4a;border-radius:8px; + padding:8px 14px;font-size:13px;font-family:'Nunito',sans-serif;font-weight:700;cursor:pointer; +} +.tour-btn-back:hover{background:rgba(255,255,255,0.06);border-color:#3a3658;} +/* Während Scroll/Resize: keine Transitions — sonst hinkt der Anker hinterher */ +.tour-character.tour-no-anim, +.tour-highlight.tour-no-anim{transition:none !important;} +/* Diskreter Re-Trigger-Button oben rechts in Schritt 3 */ +.tour-restart-btn{ + position:absolute;top:14px;right:14px; + background:rgba(124,58,237,0.12);border:1px solid rgba(124,58,237,0.40);color:#c4b5fd; + font-family:'Nunito',sans-serif;font-size:11px;font-weight:700;letter-spacing:0.3px; + border-radius:999px;padding:6px 12px;cursor:pointer;transition:all 0.15s; + user-select:none; +} +.tour-restart-btn:hover{background:rgba(124,58,237,0.26);border-color:rgba(124,58,237,0.65);color:#e2e0f0;transform:translateY(-1px);} + +/* ── App-Dialog (ersetzt confirm/alert) ── */ +.app-dialog{position:fixed;inset:0;z-index:9000;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.78);animation:appDlgFade 0.15s ease;} +.app-dialog.open{display:flex;} +.app-dialog-box{background:#1a1827;border:1.5px solid #2e2b4a;border-radius:18px;padding:26px 30px;width:min(440px,92vw);box-shadow:0 24px 60px rgba(0,0,0,0.7);animation:appDlgIn 0.22s cubic-bezier(0.34,1.56,0.64,1);} +.app-dialog-icon{font-size:2.6rem;text-align:center;margin-bottom:10px;} +.app-dialog-msg{color:#e2e0f0;font-family:'Nunito',sans-serif;font-size:15px;line-height:1.5;text-align:center;margin-bottom:22px;white-space:pre-line;} +.app-dialog-btns{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;} +.app-dialog-btns button{font-family:'Fredoka One',cursive;font-size:0.95rem;border:none;border-radius:10px;padding:11px 24px;cursor:pointer;transition:transform 0.15s,box-shadow 0.15s;} +.app-dialog-cancel{background:#2e2b4a;color:#cdc9e6;} +.app-dialog-cancel:hover{background:#3a3658;} +.app-dialog-ok{background:linear-gradient(135deg,#7c3aed,#a78bfa);color:#fff;} +.app-dialog-ok:hover{transform:translateY(-2px);box-shadow:0 6px 18px rgba(124,58,237,0.5);} +.app-dialog-ok.danger{background:linear-gradient(135deg,#dc2626,#ef4444);} +.app-dialog-ok.danger:hover{box-shadow:0 6px 18px rgba(239,68,68,0.5);} +@keyframes appDlgIn{from{transform:scale(0.92);opacity:0;}to{transform:scale(1);opacity:1;}} +@keyframes appDlgFade{from{opacity:0;}to{opacity:1;}} + +/* ── Welt-Editor (Schritt 3) ── */ +.world-toolbar{display:flex;gap:8px;align-items:center;margin:8px 0 10px;flex-wrap:wrap;} +.world-toolbar button{background:linear-gradient(135deg,#7c3aed,#a78bfa);color:#fff;border:none;border-radius:10px;padding:9px 16px;font-family:'Fredoka One',cursive;font-size:14px;cursor:pointer;transition:transform 0.15s,box-shadow 0.15s,filter 0.15s;} +.world-toolbar button:hover{transform:translateY(-2px);box-shadow:0 6px 18px rgba(124,58,237,0.4);} +.world-toolbar button.locked{background:linear-gradient(135deg,#059669,#10b981);} +.world-toolbar button.dim{filter:grayscale(0.4) opacity(0.85);} +.world-status{font-size:12px;color:var(--muted);margin-left:6px;} +.world-canvas{display:block;width:100%;height:min(60vh,560px);min-height:380px;border-radius:14px;border:1px solid #2e2b4a;background:#050314;cursor:pointer;} +.story-list{display:flex;flex-direction:column;gap:8px;margin-top:12px;} + +#mgTestOverlay{position:fixed;inset:0;background:rgba(0,0,0,0.82);z-index:5000;display:none;align-items:center;justify-content:center;} +#mgTestOverlay.open{display:flex;} +#mgTestBox{background:#1a1827;border:1.5px solid #2e2b4a;border-radius:20px;padding:0;width:min(1040px,95vw);max-height:92vh;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 24px 60px rgba(0,0,0,0.7);} +#mgTestHeader{display:flex;align-items:center;gap:12px;padding:18px 22px;border-bottom:1px solid #2e2b4a;background:#22203a;} +#mgTestEmoji{font-size:2rem;} +#mgTestTitle{font-family:'Fredoka One',cursive;font-size:1.3rem;color:#f5a623;flex:1;} +#mgTestClose{background:none;border:none;color:#a7a3c2;font-size:1.4rem;cursor:pointer;padding:4px 8px;border-radius:8px;transition:all 0.15s;line-height:1;} +#mgTestClose:hover{background:rgba(239,68,68,0.15);color:#ef4444;} +#mgTestBody{padding:20px;overflow-y:auto;} +#mgTestCanvas{display:block;margin:0 auto;border-radius:10px;border:1px solid #2e2b4a;} +#mgTestMsg{text-align:center;font-family:'Fredoka One',cursive;font-size:1.1rem;margin-top:14px;min-height:28px;} +#mgTestMsg.win{color:#10b981;}#mgTestMsg.lose{color:#ef4444;} +#mgTestControls{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:14px;} +.mg-ctrl-btn{background:linear-gradient(135deg,#7c3aed,#f5a623);color:#fff;font-family:'Fredoka One',cursive;font-size:0.9rem;border:none;border-radius:9px;padding:10px 20px;cursor:pointer;transition:transform 0.15s;} +.mg-ctrl-btn:hover{transform:translateY(-1px);} +.mg-ctrl-btn.secondary{background:#22203a;border:1.5px solid #2e2b4a;color:#a7a3c2;} +#mgTestDesc{font-size:13px;color:#a7a3c2;text-align:center;line-height:1.6;margin-bottom:14px;} + +/* ─── CODE PANE ─── */ +#codePane{font-family:'Fira Code','Cascadia Code','Consolas','Monaco',monospace} +#codeHeader{ + display:flex;align-items:center;justify-content:space-between; + background:#131221;border-bottom:1px solid rgba(255,255,255,0.07); + padding:9px 16px;flex-shrink:0; +} +#codeHeaderLeft{display:flex;align-items:center;gap:7px} +.code-dot{width:11px;height:11px;border-radius:50%;display:inline-block} +.code-dot.red{background:#ff5f57}.code-dot.yellow{background:#febc2e}.code-dot.green{background:#28c840} +#codeFilename{color:#a7a3c2;font-size:12px;font-weight:600;margin-left:6px;font-family:'Nunito',sans-serif} +#codeHeaderRight{display:flex;align-items:center;gap:12px} +#codeLang{font-size:11px;font-weight:700;color:var(--accent2);font-family:'Nunito',sans-serif; + background:rgba(124,58,237,0.15);border:1px solid rgba(124,58,237,0.3);border-radius:5px;padding:2px 8px} +#codeLines{font-size:10px;color:rgba(255,255,255,0.25);font-family:'Nunito',sans-serif} +#codeScroll{ + display:flex;flex:1;overflow-y:auto;overflow-x:hidden;min-height:0; + scrollbar-width:thin;scrollbar-color:rgba(124,58,237,0.3) transparent; +} +#codeScroll::-webkit-scrollbar{width:5px} +#codeScroll::-webkit-scrollbar-thumb{background:rgba(124,58,237,0.3);border-radius:3px} +#codeLineNums{ + padding:16px 12px 16px 10px;text-align:right; + color:rgba(255,255,255,0.15);font-size:12px;line-height:1.65; + user-select:none;flex-shrink:0;min-width:38px;border-right:1px solid rgba(255,255,255,0.05); + background:#0a0917; +} +#codeContent{ + flex:1;padding:16px 16px 80px 14px;margin:0;white-space:pre; + font-size:12.5px;line-height:1.65;color:#e2e0f0;overflow-x:hidden; + background:transparent;border:none;outline:none; +} +/* ── Syntax colors (applied via spans) */ +.py-kw{color:#c678dd} /* keywords: def, class, if, for, return, import */ +.py-fn{color:#61afef} /* function names */ +.py-str{color:#98c379} /* strings */ +.py-num{color:#d19a66} /* numbers */ +.py-cm{color:#5c6370;font-style:italic} /* comments */ +.py-var{color:#e06c75} /* variables (left of =) */ +.py-val{color:#abb2bf} /* plain values */ +.py-punc{color:#abb2bf} /* punctuation */ +.py-cls{color:#e5c07b} /* class names */ +.py-dec{color:#56b6c2} /* decorators / special */ +.py-hi{ /* active/focused line highlight */ + background:rgba(245,166,35,0.13);display:inline-block;width:100%; + border-left:3px solid var(--accent);padding-left:6px;margin-left:-6px; + animation:codeFlash 0.4s ease both; +} +@keyframes codeFlash{ + 0% {background:rgba(245,166,35,0.35);border-left-color:var(--accent)} + 100%{background:rgba(245,166,35,0.10);border-left-color:var(--accent)} +} +#codeFooter{ + display:flex;align-items:center;justify-content:space-between; + background:#0d0c1a;border-top:1px solid rgba(255,255,255,0.05); + padding:5px 14px;flex-shrink:0; +} +#codeStatus{font-size:10px;color:var(--accent3);font-weight:700;font-family:'Nunito',sans-serif;display:flex;align-items:center;gap:5px} +#codeInfo{font-size:10px;color:rgba(255,255,255,0.2);font-family:'Nunito',sans-serif} +/* typing cursor in code */ +.py-cursor{display:inline-block;width:2px;height:14px;background:var(--accent2);vertical-align:middle;animation:blink 1s step-end infinite;margin-left:1px} +@keyframes blink{0%,100%{opacity:1}50%{opacity:0}} +/* Responsive: hide code pane on small screens */ +@media(max-width:900px){ + #resizerBar,#codePane{display:none} + #editorPane{flex:1;border-right:none} +} +/* Save indicator */ +#saveIndicator{ + position:fixed;top:12px;right:16px;z-index:9000; + background:rgba(16,185,129,0.92);color:#fff; + font-family:'Fredoka One',cursive;font-size:0.85rem; + padding:6px 14px;border-radius:8px; + opacity:0;transition:opacity 0.3s;pointer-events:none; + box-shadow:0 3px 12px rgba(0,0,0,0.3); +} + +/* ── Konsequenz-Wert-Buttons ── */ +.val-btn{background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2); + color:#fff;border-radius:4px;padding:1px 7px;cursor:pointer;font-size:12px; + line-height:1.4;vertical-align:middle;} +.val-btn:hover{background:rgba(255,255,255,0.2);} +/* Ausgeblendete Optionen je nach Spielregel */ +/* Konsequenz-Optionen: alle sichtbar ausser wenn explizit ausgeblendet */ +.cq-mode-step .cq-dice-only{display:none;} +.cq-mode-pts .cq-lives-only{display:none;} +.cq-mode-lives .cq-pts-only{display:none;} diff --git a/editor.html b/editor.html new file mode 100644 index 0000000..0c91472 --- /dev/null +++ b/editor.html @@ -0,0 +1,597 @@ + + + + + +Spiel-Generator + + + + +
Gespeichert ✓
+ + + +
+
+
+ +
+ PH Weingarten +
+
Medien- und Bildungsmanagement
+
Informatik (Lehramt)
+
+
+
+
+
1
Grundinfos
+
+
2
Gestaltung
+
+
3
Spielfeld
+
+
4
Quiz
+
+
5
Testen
+
+
+
+
+ +
+
+ + +
+
+
✅ Gespeichert!
+ + +
+
+
Schritt 1 von 5
+

Dein Spiel beginnt hier ✨

+

Erstmal wichtig: Wer bist du? Dann gib deinem Spiel einen Namen.

+
+
+
👋 Hallo! Wie heißt du?
+

Dein Name oder Nickname — er erscheint auf deinem Spiel als Entwickler:in!

+ +
+
+
+ + +
0 / 40
+
+
🌴 Abenteuer im Dschungel
+
🚀 Mission Weltraum
+
❓ Das große Quiz-Turnier
+
🐲 Schatz des Drachen
+
🌀 Flucht aus dem Labyrinth
+
+
+ + +
0 / 120
+
+ +
+
+
+ + +
+
+
+ + +
+
+
Schritt 2 von 5
+

Figur & Setting 🎨

+

Wähle deine Spielfigur und die Spielwelt.

+
+
+
✅ Schritt 1 abgeschlossen
+
+
+
+
👥 Spielmodus
+

Allein oder zu zweit gegeneinander?

+
+
+
👤
+
1 Spieler
+
Allein spielen
+
+
+
👥
+
2 Spieler
+
Wettrennen — wer zuerst am Ziel?
+
+
+
+
+
👤 Spielfigur
+

Das bist du im Spiel!

+
+
+ +
+
🌍 Spielwelt
+

Wo findet das Abenteuer statt?

+
+ +
+
+ + +
+
+ + +
+
+
Schritt 3 von 5
+

Bau dein Spielfeld 🎲

+

Lege Regeln fest und bestücke die Felder mit Mini-Games.

+ +
+
+
✅ Schritt 2 abgeschlossen
+
+
+
+
⚙️ Spielregeln
+

Wie soll dein Spiel funktionieren?

+
+
+
🎲 Bewegung
+
+
🎲
Würfeln
Zufällig 1–6 Felder
+
👣
Ein Feld pro Runde
Jedes Feld wird besucht
+
+
+
+
🎮 Spielmodus
+
+
❤️
Ich spiele mit Leben
Bei 0 Leben: Game Over
+
Ich spiele mit Punkten
Kein Verlieren, nur Score
+
+
+ Anzahl Leben: + + 3 + + ❤️❤️❤️ +
+
+
+
+
+
Konsequenzen nach Mini-Game
+

Was passiert wenn ein Mini-Game gewonnen oder verloren wird?

+
+ +
+
Bei Sieg
+
+
+
Nichts passiert
Spiel läuft normal weiter
+
+
+
Felder vor
+
2 Felder
+
+
+
Punkte +
+
10 Punkte
+
+
+ 🎲
Nochmal würfeln
Sofort ein zweites Mal
+
+
+
+ +
+
Bei Niederlage
+
+
+
Nichts passiert
Spiel läuft normal weiter
+
+
+
Felder zurück
+
1 Felder
+
+
+ ❤️
Leben verlieren
Zusätzlich zu Spielregel
+
+
+
Punkte −
+
5 Punkte
+
+
+ ⏸️
Runde aussetzen
Nächste Runde pausieren
+
+
+
+
+
+
+ +
+ +
10
+ + (6–50) +
+
+
+
🕹️ Bausteine
📖 0 / 5
+
+
🕹️ Mini-Games
+
📖 Erzähltexte
+
+
+
+
+
0
+
📖
+
Erzähl-Text
+ +
Vor/nach Feld
+
+
+
+
🗺️ Spielwelt Generieren bis es passt, dann Bausteine auf die Felder ziehen
+
+ + + 🎲 Welt frei generierbar +
+ +
+
+
+ + +
+ +
+ + +
+
+
+ + +
+
+
Schritt 4 von 5
+

Deine Quiz-Fragen ❓

+

Für jedes Quiz-Feld brauchst du eine Frage mit vier Antworten.

+
+
+
✅ Schritt 3 abgeschlossen
+
+
+
+
+ + +
+
+ + +
+
+
Schritt 5 von 5
+

Testen & Feedback 🎮

+

Überblick über dein Spiel — dann starten und ausprobieren!

+
+
+
✅ Schritt 4 abgeschlossen
+
+
+ +
+ +
+
+ 🎮 +
+
+
+
+
+
+ +
Öffnet das Spiel in einem neuen Fenster
+
✅ Du hast gespielt! Jetzt bitte Feedback geben ↓
+
+
+ + + + +
+ +
+
+
+ + +
+
+
+
+ 🎉 Dein Spiel ist fertig! +
+ +
+
+
+
+
+ 🔗 Dein Spiel-Link +
+
+ + +
+
+
+
+
+ 📱 QR-Code zum Scannen +
+
+
+ Handy-Kamera auf den QR-Code richten → öffnet das Spiel direkt +
+
+ + + +
+ 📤 Direkt teilen +
+
+ + + + +
+
+ ℹ️ Der Link enthält dein komplettes Spiel. Jeder mit dem Link kann es direkt spielen – keine Anmeldung nötig! +
+
+
+
+ + + + + + + + + + + + +
+
+
+ 🎮 +
Mini-Game Test
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ + + + boardgame.py +
+
+ 🐍 Python + 0 Zeilen +
+
+
+
+

+  
+
+ ● Bereit + Live-Vorschau deines Spielcodes + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
⚠️
+
+
+ + +
+
+
+ + + diff --git a/game.css b/game.css new file mode 100644 index 0000000..70e2eff --- /dev/null +++ b/game.css @@ -0,0 +1,270 @@ +*,*::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;} +.hud-2p{display:flex;gap:8px;flex:1;} +.hud-player{display:flex;gap:6px;align-items:center;background:rgba(255,255,255,0.06);border:1.5px solid rgba(255,255,255,0.10);border-radius:999px;padding:5px 12px;font-size:12px;font-weight:700;color:var(--muted);transition:all 0.25s;} +.hud-player.active{border-color:var(--theme-primary,#a78bfa);box-shadow:0 0 16px var(--theme-glow,rgba(167,139,250,0.5));background:rgba(255,255,255,0.10);color:var(--text);} +.hud-player.eliminated{opacity:0.45;} +.hud-player.eliminated .hp-stat{text-decoration:line-through;} +.hp-fig{font-size:1.15rem;line-height:1;} +.hp-pos{color:var(--text);} +.hp-stat{font-size:0.85rem;} +#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.84);} +.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:1100px;width:min(1100px,95vw);} +.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; + width:100%;min-height:min(560px,70vh); +} +.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);}} diff --git a/game.html b/game.html new file mode 100644 index 0000000..4bb348f --- /dev/null +++ b/game.html @@ -0,0 +1,140 @@ + + + + + +Spiel + + + + + + + + +
+
+
+ 🎮 +
Spiel-Generator Workshop
+

Mein Spiel

+

Beschreibung folgt...

+
+ +
+
+ + +
+
+
Spiel
+ +
⬛ Feld 1/10
+
❤️ ❤️❤️❤️
+ + + + +
+
+ +
+
+
+
Würfeln!
+
🎲
+
+ +
+
+ + + +
+
+
+
+ 📖 +
Erzähl-Text
+
+
+

+
+ +
+
+ + +
+
+
+
+
Mini-Game
+ +
+
+

+
+
+
+

+

+
+ +
+
+
+ + +
+
+
+ 🏆 +

Gewonnen!

+

+
+
+ + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..b691ff5 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + +Edu Boardgame Generator + + + + + +

Weiterleitung zum Editor… Hier klicken, falls es nicht automatisch geht.

+ + diff --git a/js/board.js b/js/board.js new file mode 100644 index 0000000..f6ba7f7 --- /dev/null +++ b/js/board.js @@ -0,0 +1,307 @@ +/* board.js — Mini-Game-Palette, Spielfeld-Aufbau, Story Drag&Drop */ +function palTab(t){ + document.querySelectorAll('.pal-tab').forEach((x,i)=>x.classList.toggle('active',(i===0&&t==='games')||(i===1&&t==='story'))); + document.getElementById('panGames').classList.toggle('active',t==='games'); + document.getElementById('panStory').classList.toggle('active',t==='story'); +} +function getUC(id){return ST.fields.filter(f=>f===id).length;} +function mgDis(mg){ + if(mg.requires==='2p' && ST.playerCount!==2) return true; // nur in 2P verfügbar + const c=getUC(mg.id); + return mg.multi===1?c>=1:mg.multi===3?c>=MULTI_MAX:false; +} +function mgLockReason(mg){ + if(mg.requires==='2p' && ST.playerCount!==2) return '2-Spieler-Modus nötig'; + return ''; +} +function sCnt(){return ST.storyItems.length;} +function sMaxed(){return ST.storyItems.length>=STORY_MAX;} + +function buildPalette(){ + const pan=document.getElementById('panGames');pan.innerHTML=''; + MINIGAMES.forEach(mg=>{ + const tagCls=mg.requires==='2p'?'requires2p':mg.multi==='∞'?'unlim':mg.multi===3?'multi':'once'; + const tagTxt=mg.requires==='2p'?'2P':mg.multi==='∞'?'∞':mg.multi===3?'max.3×':'1×'; + const c=document.createElement('div');c.className='mg-card';c.id='mgc_'+mg.id;c.draggable=true; + const lockHint=mg.requires==='2p'?`
🔒 2-Spieler-Modus
`:''; + c.innerHTML=`
0
${mg.e}
${mg.n}
${tagTxt}
${lockHint}`; + c.addEventListener('click',()=>{if(c.classList.contains('disabled'))return;if(selGame===mg.id){selGame=null;c.classList.remove('click-sel');}else{clearSel();selGame=mg.id;c.classList.add('click-sel');}}); + c.addEventListener('dragstart',e=>{ + if(c.classList.contains('disabled')){e.preventDefault();return;} + dragging='game'; dragId=mg.id; clearSel(); + e.dataTransfer.effectAllowed='copy'; + e.dataTransfer.setData('text/plain','game:'+mg.id); + // Use the card itself as drag image — offset so cursor is in center + e.dataTransfer.setDragImage(c, c.offsetWidth/2, c.offsetHeight/2); + ghost(mg.e+' '+mg.n); + }); + c.addEventListener('dragend',()=>{dragging=null;dragId=null;unghost();cleanDropTargets();}); + pan.appendChild(c); + }); + const sc=document.getElementById('storyCard'); + sc.ondragstart=e=>{ + if(sMaxed()){e.preventDefault();return;} + dragging='story'; dragId='story'; clearSel(); selGame='story'; + e.dataTransfer.effectAllowed='copy'; + e.dataTransfer.setData('text/plain','story'); + e.dataTransfer.setDragImage(sc, sc.offsetWidth/2, sc.offsetHeight/2); + ghost('📖 Erzähl-Text'); + }; + sc.ondragend=()=>{dragging=null;dragId=null;unghost();cleanDropTargets();}; + updPal(); +} +function clearSel(){selGame=null;document.querySelectorAll('.mg-card').forEach(c=>c.classList.remove('click-sel'));} +function cleanDropTargets(){ + document.querySelectorAll('.drop-zone').forEach(z=>{z.classList.remove('story-hoverable','drag-over');}); + document.querySelectorAll('.field-row').forEach(r=>r.classList.remove('drag-target')); +} +function updPal(){ + MINIGAMES.forEach(mg=>{ + const c=document.getElementById('mgc_'+mg.id);const uc=document.getElementById('uc_'+mg.id);if(!c)return; + const cnt=getUC(mg.id);uc.textContent=cnt; + c.classList.toggle('used',cnt>0);c.classList.toggle('disabled',mgDis(mg)); + const lockNeeded = mg.requires==='2p' && ST.playerCount!==2; + const lockEl=document.getElementById('mglk_'+mg.id); + if(lockEl) lockEl.style.display = lockNeeded ? '' : 'none'; + if(mgDis(mg)&&selGame===mg.id)clearSel(); + }); + const sc=document.getElementById('storyCard');if(sc){sc.classList.toggle('disabled',sMaxed());const suc=document.getElementById('storyUC');if(suc)suc.textContent=sCnt();sc.classList.toggle('used',sCnt()>0);} + const badge=document.getElementById('storyCtr');badge.textContent='📖 '+sCnt()+' / '+STORY_MAX;badge.classList.toggle('maxed',sMaxed()); + const filled=ST.fields.slice(1,ST.fieldCount-1).filter(Boolean).length; + document.getElementById('s3next').disabled=filled===0; + const s3hint=document.getElementById('s3hint');if(s3hint)s3hint.style.display=filled===0?'block':'none'; +} +let dragId=null; +function ghost(txt){const g=document.getElementById('dragGhost');g.textContent=txt;g.style.opacity='1';} +function unghost(){document.getElementById('dragGhost').style.opacity='0';} +// Track mouse position for ghost element +document.addEventListener('dragover',e=>{ + e.preventDefault(); + const g=document.getElementById('dragGhost'); + if(dragging){g.style.left=e.clientX+'px';g.style.top=e.clientY+'px';} + if(dragging==='story'){ + document.querySelectorAll('.drop-zone').forEach(z=>z.classList.add('story-hoverable')); + } +}); +document.addEventListener('dragend',()=>cleanDropTargets()); + +/* ── Welt-Editor: Canvas mit rAF-Loop + Drag&Drop direkt auf Pads ── */ +let worldEdit=null, worldRaf=null, worldHover=-1, worldCanvasInit=false; + +function ensureWorld(){ + if(!ST.worldSeed) ST.worldSeed=World.randomSeed(); + worldEdit=World.generate(ST.background||'space', ST.worldSeed, ST.fieldCount); +} + +function worldReroll(){ + const doReroll=()=>{ + ST.worldSeed=World.randomSeed(); + ensureWorld(); setCodeFocus('fields'); save(); updateWorldUI(); + }; + if(ST.worldLocked){ + appConfirm('Spielbrett ist fixiert. Willst du wirklich eine neue Welt generieren?\n\nFelder und Storys bleiben erhalten.', doReroll, {icon:'🔒', okText:'Neu generieren'}); + } else { + doReroll(); + } +} +function worldToggleLock(){ + ST.worldLocked=!ST.worldLocked; + save(); + updateWorldUI(); +} +function updateWorldUI(){ + const lb=document.getElementById('wbLock'); const st=document.getElementById('wbStatus'); const rb=document.getElementById('wbReroll'); + if(lb){lb.textContent=ST.worldLocked?'🔓 Wieder bearbeiten':'✓ Spielbrett verwenden'; lb.classList.toggle('locked',ST.worldLocked);} + if(st){st.textContent=ST.worldLocked?'🔒 Spielbrett ist fixiert':'🎲 Welt frei generierbar';} + if(rb)rb.classList.toggle('dim',ST.worldLocked); +} + +function resizeWorldCanvas(canvas){ + const r=canvas.getBoundingClientRect(); + const dpr=window.devicePixelRatio||1; + const w=Math.round(r.width), h=Math.round(r.height); + if(w<=0||h<=0) return false; + if(canvas.width!==Math.round(w*dpr)){canvas.width=Math.round(w*dpr);canvas.height=Math.round(h*dpr);} + canvas._w=w; canvas._h=h; canvas._dpr=dpr; + return true; +} + +function worldLoop(){ + const canvas=document.getElementById('worldCanvas'); + if(!canvas){worldRaf=null;return;} + // Pause wenn Schritt 3 nicht aktiv (cur!==2) + if(typeof cur==='number' && cur!==2){worldRaf=requestAnimationFrame(worldLoop);return;} + if(!resizeWorldCanvas(canvas)){worldRaf=requestAnimationFrame(worldLoop);return;} + if(!worldEdit || worldEdit.theme!==World.resolveTheme(ST.background||'space') || worldEdit.fieldCount!==ST.fieldCount || worldEdit.seed!==(ST.worldSeed>>>0)){ + ensureWorld(); + } + const ctx=canvas.getContext('2d'); + ctx.setTransform(canvas._dpr,0,0,canvas._dpr,0,0); + ctx.clearRect(0,0,canvas._w,canvas._h); + const fig1=(FIGURES.find(f=>f.id===ST.figure)||{}).e||'🎮'; + const fig2=ST.playerCount===2 ? ((FIGURES.find(f=>f.id===ST.figure2)||{}).e||'🎮') : null; + const c0=World.padCenter(worldEdit,0,canvas._w,canvas._h); + const figures = [{ emoji:fig1, x:c0.x, y:c0.y, active:true, dimmed:false }]; + if(fig2) figures.push({ emoji:fig2, x:c0.x, y:c0.y, active:false, dimmed:false }); + World.render(ctx, worldEdit, { + W:canvas._w, H:canvas._h, + pos:0, visited:new Set([0]), + fields:ST.fields, storyItems:ST.storyItems, + figures, + figEmoji:fig1, figX:c0.x, figY:c0.y, + gameName:ST.name, devName:ST.devName, + hover:worldHover, + }, performance.now()); + worldRaf=requestAnimationFrame(worldLoop); +} + +function worldEventToPad(e){ + const canvas=document.getElementById('worldCanvas'); + if(!canvas||!worldEdit) return -1; + const r=canvas.getBoundingClientRect(); + return World.hitTestPad(worldEdit, e.clientX-r.left, e.clientY-r.top, canvas._w||r.width, canvas._h||r.height); +} + +function initWorldCanvas(){ + if(worldCanvasInit) return; + const canvas=document.getElementById('worldCanvas'); if(!canvas) return; + worldCanvasInit=true; + canvas.addEventListener('dragover',e=>{if(!dragging)return; e.preventDefault(); e.dataTransfer.dropEffect='copy'; worldHover=worldEventToPad(e);}); + canvas.addEventListener('dragleave',()=>{worldHover=-1;}); + canvas.addEventListener('drop',e=>{ + if(!dragging) return; + e.preventDefault(); + const idx=worldEventToPad(e); + const d=e.dataTransfer.getData('text/plain'); + worldHover=-1; + if(idx<0) return; + if(d==='story'){ + const r=canvas.getBoundingClientRect(); + const pc=World.padCenter(worldEdit,idx,canvas._w,canvas._h); + const above=(e.clientY-r.top)0; after idx0?'before':'after') : (idx0?idx:idx) : (idx{ + const idx=worldEventToPad(e); + if(idx<0) return; + if(selGame==='story'){ + const r=canvas.getBoundingClientRect(); + const pc=World.padCenter(worldEdit,idx,canvas._w,canvas._h); + const above=(e.clientY-r.top)0?'before':'after') : (idx0 && idx{ + ST.fields[idx]=null; buildBoard(); save(); setCodeFocus('fields'); + }, {danger:true, okText:'Entfernen', icon:'🗑️'}); + } + }); + canvas.addEventListener('mousemove',e=>{worldHover=worldEventToPad(e);}); + canvas.addEventListener('mouseleave',()=>{worldHover=-1;}); +} + +function buildStoryList(){ + const list=document.getElementById('storyList'); if(!list) return; + list.innerHTML=''; + // sortiert: nach fieldIndex, before vor after + const sorted=[...ST.storyItems].sort((a,b)=>(a.fieldIndex-b.fieldIndex)||(a.position==='before'?-1:1)); + sorted.forEach(s=>list.appendChild(mkStory(s))); +} + +function buildBoard(){ + ensureWorld(); + initWorldCanvas(); + updateWorldUI(); + if(!worldRaf) worldRaf=requestAnimationFrame(worldLoop); + buildStoryList(); + updPal(); + const filled=ST.fields.slice(1,ST.fieldCount-1).filter(Boolean).length; + const stEl=document.getElementById('boardStats'); + if(stEl) stEl.innerHTML=`
🎯 Belegt: ${filled}/${ST.fieldCount-2}
🕹️ Games: ${new Set(ST.fields.filter(Boolean)).size}
📖 Texte: ${sCnt()}
`; +} +function mkDrop(pos,fi){ + const z=document.createElement('div');z.className='drop-zone'; + z.textContent = pos==='before' ? `↗ Vor Feld ${fi}` : `↘ Nach Feld ${fi}`; + z.addEventListener('dragenter',e=>{if(dragging==='story'){e.preventDefault();z.classList.add('drag-over');}}); + z.addEventListener('dragover',e=>{if(dragging==='story'){e.preventDefault();e.dataTransfer.dropEffect='copy';z.classList.add('drag-over');}}); + z.addEventListener('dragleave',e=>{if(!z.contains(e.relatedTarget))z.classList.remove('drag-over');}); + z.addEventListener('drop',e=>{ + e.preventDefault();z.classList.remove('drag-over'); + if(e.dataTransfer.getData('text/plain')==='story')dropStory(pos,fi); + }); + z.addEventListener('click',()=>{if(selGame==='story')dropStory(pos,fi);}); + return z; +} +function mkStory(s){ + // XSS-sicher: DOM-API + textContent/value statt innerHTML — s.id wird via JS-Closure übergeben, + // also kein HTML-Inject-Vektor mehr. + const row=document.createElement('div'); row.className='story-row'; row.dataset.sid=s.id; + const posLbl = s.position==='before' ? `Vor Feld ${s.fieldIndex|0}` : `Nach Feld ${s.fieldIndex|0}`; + const icon = document.createElement('div'); icon.className='story-icon'; + icon.textContent = String(s.emoji||'📖').slice(0,8); + icon.addEventListener('click', () => openEP(s.id, icon)); + const content = document.createElement('div'); content.className='story-content'; + const lbl = document.createElement('div'); lbl.className='story-lbl'; lbl.textContent='📖 Erzähl-Text'; + const ta = document.createElement('textarea'); ta.className='story-ta'; ta.rows=2; ta.maxLength=300; + ta.placeholder='Schreib deine Geschichte...'; + ta.value = String(s.text||''); + ta.addEventListener('input', () => updStory(s.id, ta.value)); + const meta = document.createElement('div'); meta.className='story-meta'; + const posEl = document.createElement('div'); posEl.className='story-pos-lbl'; posEl.textContent=posLbl; + const chars = document.createElement('div'); chars.className='story-chars'; chars.textContent=(s.text||'').length+'/300'; + meta.appendChild(posEl); meta.appendChild(chars); + content.appendChild(lbl); content.appendChild(ta); content.appendChild(meta); + const clear = document.createElement('button'); clear.className='story-clear'; clear.textContent='✕'; + clear.addEventListener('click', () => rmStory(s.id)); + row.appendChild(icon); row.appendChild(content); row.appendChild(clear); + return row; +} +function clickStory(){if(sMaxed())return;if(selGame==='story'){selGame=null;document.getElementById('storyCard').classList.remove('click-sel');}else{clearSel();selGame='story';document.getElementById('storyCard').classList.add('click-sel');}} +function dropStory(pos,fi){ + if(sMaxed()){showToast('⚠️ Maximal 5 Erzähltexte!');return;} + const id='st_'+Date.now(); + ST.storyItems.push({id,emoji:'📖',text:'',position:pos,fieldIndex:fi}); + clearSel();buildBoard(); + setTimeout(()=>{const r=document.querySelector(`[data-sid="${id}"] textarea`);if(r)r.focus();},80); +} +function updStory(id,val){const s=ST.storyItems.find(x=>x.id===id);if(!s)return;s.text=val;const row=document.querySelector(`[data-sid="${id}"]`);if(row){const cc=row.querySelector('.story-chars');if(cc)cc.textContent=val.length+'/300';}} +function rmStory(id){appConfirm('Erzähltext wirklich löschen?', ()=>{ST.storyItems=ST.storyItems.filter(x=>x.id!==id);buildBoard();}, {danger:true, okText:'Löschen', icon:'🗑️'});} +function openEP(sid,iconEl){ + const picker=document.getElementById('emojiPicker'); + if(epFor===sid){picker.classList.remove('open');epFor=null;return;} + epFor=sid;picker.innerHTML=''; + STORY_EMOJIS.forEach(em=>{const btn=document.createElement('div');btn.className='ep-it';btn.textContent=em;btn.addEventListener('click',()=>{const s=ST.storyItems.find(x=>x.id===sid);if(s){s.emoji=em;buildBoard();}picker.classList.remove('open');epFor=null;});picker.appendChild(btn);}); + const r=iconEl.getBoundingClientRect(); + picker.style.left=Math.min(r.left,window.innerWidth-220)+'px'; + picker.style.top=(r.bottom+6)+'px'; + picker.classList.add('open'); +} +document.addEventListener('click',e=>{if(epFor&&!e.target.classList.contains('story-icon')&&!document.getElementById('emojiPicker').contains(e.target)){document.getElementById('emojiPicker').classList.remove('open');epFor=null;}}); +function handleFClick(idx){if(!selGame||selGame==='story')return;const mg=MINIGAMES.find(m=>m.id===selGame);if(!mg||mgDis(mg))return;ST.fields[idx]=selGame;setCodeFocus('fields');buildBoard();} +function dropGame(idx,id){ + const mg=MINIGAMES.find(m=>m.id===id);if(!mg)return; + const src=dragFromIdx;dragFromIdx=null; + if(src===idx){buildBoard();return;} + const tmp=[...ST.fields]; + if(src!==null)tmp[src]=null; + tmp[idx]=null; + const cnt=tmp.filter(f=>f===id).length; + if((mg.multi===1&&cnt>=1)||(mg.multi===3&&cnt>=MULTI_MAX)){showToast('⚠️ Limit erreicht für '+mg.n+'!');return;} + if(src!==null)ST.fields[src]=null; + ST.fields[idx]=id; + setCodeFocus('fields'); + buildBoard(); +} +function clearF(idx,e){e&&e.stopPropagation();appConfirm('Mini-Game von Feld '+idx+' entfernen?', ()=>{ST.fields[idx]=null;buildBoard();}, {danger:true, okText:'Entfernen', icon:'🗑️'});} + diff --git a/js/data.js b/js/data.js new file mode 100644 index 0000000..aea07db --- /dev/null +++ b/js/data.js @@ -0,0 +1,54 @@ +/* data.js — Stammdaten: Figuren, Welten, Mini-Games, Quiz-Beispiele, Feedback-Fragen */ +/* ── XSS helper ── */ +function esc(s){return String(s??'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');} + +/* ══════════════════════════════════════════════ + DATA +══════════════════════════════════════════════ */ +const FIGURES=[ + {id:'robot',e:'🤖',n:'Roboter'},{id:'ninja',e:'🥷',n:'Ninja'},{id:'knight',e:'🧙‍♂️',n:'Zauberer'}, + {id:'cat',e:'🐱',n:'Katze'},{id:'rocket',e:'🚀',n:'Rakete'},{id:'dino',e:'🦖',n:'Dino'}, + {id:'alien',e:'👾',n:'Alien'},{id:'superhero',e:'🦸',n:'Superheld'},{id:'pirate',e:'🏴‍☠️',n:'Pirat'}, + {id:'fox',e:'🦊',n:'Fuchs'},{id:'dragon',e:'🐲',n:'Drache'},{id:'astronaut',e:'👨‍🚀',n:'Astronaut'}, +]; +const BACKGROUNDS=[ + {id:'underwater',e:'🌊',scene:'🌊🐠🐙',n:'Unterwasser',color:'#22d3ee',glow:'rgba(34,211,238,0.45)'}, + {id:'space', e:'🌌',scene:'🌌🪐⭐',n:'Weltall', color:'#a78bfa',glow:'rgba(167,139,250,0.5)'}, + {id:'haunted', e:'👻',scene:'👻🏚️🕸️',n:'Geisterhaus',color:'#86efac',glow:'rgba(134,239,172,0.45)'}, + {id:'fantasy', e:'🧙',scene:'🧙🐉✨',n:'Fantasy', color:'#f0abfc',glow:'rgba(240,171,252,0.5)'}, +]; +const MINIGAMES=[ + {id:'quiz',e:'❓',n:'Quiz',multi:'∞'},{id:'reaction',e:'⚡',n:'Reaktion',multi:3}, + {id:'puzzle',e:'🧩',n:'Rätsel',multi:3},{id:'spotdiff',e:'🔍',n:'Fehler finden',multi:3}, + {id:'typing',e:'⌨️',n:'Tipp-Rennen',multi:3},{id:'snake',e:'🐍',n:'Snake',multi:1}, + {id:'flappy',e:'🐦',n:'Flappy Bird',multi:1},{id:'catch',e:'🍎',n:'Äpfel fangen',multi:1}, + {id:'basketball',e:'🏀',n:'Basketball',multi:1},{id:'memory',e:'🃏',n:'Memory',multi:1}, + {id:'maze',e:'🌀',n:'Labyrinth',multi:1},{id:'simon',e:'🔴',n:'Simon Says',multi:1}, + {id:'snake2p',e:'🐍🐍',n:'Snake-Duell',multi:1,requires:'2p'}, + {id:'flappy2p',e:'🐦🐦',n:'Flappy-Duell',multi:1,requires:'2p'}, +]; +const STORY_EMOJIS=['📖','🗺️','⚔️','🧭','💬','🌟','🔥','💡','🎭','🏴‍☠️','🌈','🐉','👁️','🎶','🌙','⚡','🗝️','🌊','🏔️','🎪','🦋','🌺','💎','🎯']; +const LETTERS=['A','B','C','D']; +const QUIZ_EX=[ + {q:'Was ist die Hauptstadt von Deutschland?',a:['Paris','London','Berlin','Madrid'],c:2}, + {q:'Wie viele Seiten hat ein Hexagon?',a:['5','6','7','8'],c:1}, + {q:'Wer schrieb "Romeo und Julia"?',a:['Goethe','Schiller','Shakespeare','Kafka'],c:2}, + {q:'Was ist das größte Tier der Welt?',a:['Elefant','Blauwal','Hai','Giraffe'],c:1}, + {q:'Wie viele Minuten hat eine Stunde?',a:['50','70','100','60'],c:3}, + {q:'Chemische Formel für Wasser?',a:['CO2','H2O','O2','NaCl'],c:1}, + {q:'In welchem Kontinent liegt Ägypten?',a:['Asien','Europa','Amerika','Afrika'],c:3}, + {q:'Was ist 12 × 12?',a:['132','144','124','148'],c:1}, + {q:'Blau + Gelb = ?',a:['Rot','Orange','Grün','Lila'],c:2}, + {q:'Höchster Berg der Welt?',a:['K2','Mont Blanc','Everest','Kilimandscharo'],c:2}, +]; +const FB_QS=[ + {id:'fun',t:'scale',q:'Wie viel Spaß hat dein Spiel gemacht?',sub:'1 = gar kein Spaß · 5 = mega viel Spaß',min:'😴 Langweilig',max:'🔥 Super Spaß'}, + {id:'difficulty',t:'mc',q:'Wie schwierig war dein Spiel?',opts:['😴 Viel zu einfach','👍 Genau richtig','😅 Etwas zu schwer','💀 Viel zu schwer']}, + {id:'flow',t:'mc',q:'Wie gut passt das Spiel zusammen?',sub:'„Flow" — fühlt sich alles stimmig und flüssig an?',opts:['🔀 Wirkt zusammengewürfelt','🤔 Einigermaßen','👌 Passt gut','✨ Perfekt stimmig']}, + {id:'clarity',t:'mc',q:'Waren Regeln und Aufgaben klar?',opts:['❓ Oft unklar','🤔 Manchmal unklar','👍 Meistens klar','✅ Immer sofort klar']}, + {id:'minigames',t:'scale',q:'Wie gut passen die Mini-Games zum Thema?',sub:'Passen sie zur Geschichte und zum Setting?',min:'🚫 Passen nicht',max:'✨ Passen perfekt'}, + {id:'problems',t:'text',q:'Was hat noch nicht so gut funktioniert?',ph:'z.B. "Das Quiz war zu einfach" oder "zu viele leere Felder"...'}, + {id:'highlight',t:'text',q:'Was war der beste Teil deines Spiels?',ph:'z.B. "Die Geschichte war spannend" oder "Snake hat Spaß gemacht"...'}, + {id:'replay',t:'mc',q:'Würdest du das Spiel nochmal spielen?',opts:['❌ Eher nicht','🤷 Vielleicht','😊 Ja','🚀 Auf jeden Fall!']}, +]; + diff --git a/js/game.js b/js/game.js new file mode 100644 index 0000000..2eb9b8e --- /dev/null +++ b/js/game.js @@ -0,0 +1,1001 @@ +/* game.js — Spiel-Runtime: Config laden, Board, Würfel, Mini-Game-Ablauf */ +/* ── XSS helper ── */ +function esc(s){return String(s??'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');} + +/* 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 ══════════ */ +// 1. Versuche URL-Hash (geteilter Link), 2. Fallback localStorage, 3. Demo-Config +function loadGameConfig() { + const hash = window.location.hash.slice(1); // '#' entfernen + if (hash) { + try { + if (hash.startsWith('z:')) { + // Komprimiert: Base64 → binary → pako.inflate → JSON + const b64 = hash.slice(2); + const binary = atob(b64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + const json = pako.inflate(bytes, { to: 'string' }); + return JSON.parse(json); + } else if (hash.startsWith('j:')) { + // Unkomprimiert Base64 + return JSON.parse(decodeURIComponent(escape(atob(hash.slice(2))))); + } + } catch(e) { + console.warn('URL-Hash konnte nicht gelesen werden:', e); + } + } + // Fallback: localStorage (vom Editor) + try { + const stored = localStorage.getItem('gameConfig'); + if (stored) return JSON.parse(stored); + } catch(e) { + document.body.innerHTML = '
⚠️

Spielkonfiguration fehlerhaft

Die gespeicherten Spieldaten konnten nicht geladen werden.
Bitte gehe zurück zum Editor.

'; + return null; + } + // Demo-Config + return {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 cfg = loadGameConfig(); +if (!cfg) throw new Error('No config'); + +// ── Defense-in-Depth: Konfig aus URL/Hash strikt validieren (verhindert DOM-XSS) ── +(function sanitizeCfg(){ + const OK_FIG = ['robot','ninja','knight','cat','rocket','dino','alien','superhero','pirate','fox','dragon','astronaut']; + const OK_BG = ['underwater','space','haunted','fantasy','ocean','jungle','volcano','snow','city','candy','desert','school','future']; + const OK_MG = ['snake','flappy','memory','quiz','reaction','basketball','catch','maze','simon','puzzle','spotdiff','typing','snake2p','flappy2p']; + const clamp = (v,lo,hi,def)=>{const n=parseInt(v); return isFinite(n)?Math.max(lo,Math.min(hi,n)):def;}; + const str = (v,max)=>String(v??'').slice(0,max); + cfg.devName = str(cfg.devName, 60); + cfg.name = str(cfg.name, 80); + cfg.desc = str(cfg.desc, 200); + if(cfg.figure && !OK_FIG.includes(cfg.figure)) cfg.figure = ''; + if(cfg.figure2 && !OK_FIG.includes(cfg.figure2)) cfg.figure2 = ''; + if(cfg.background && !OK_BG.includes(cfg.background)) cfg.background = 'space'; + cfg.playerCount = (cfg.playerCount===2)?2:1; + cfg.fieldCount = clamp(cfg.fieldCount, 6, 50, 10); + cfg.fields = (Array.isArray(cfg.fields)?cfg.fields:[]).map(f => (typeof f==='string' && OK_MG.includes(f))?f:null); + while(cfg.fields.length < cfg.fieldCount) cfg.fields.push(null); + cfg.fields.length = cfg.fieldCount; + cfg.rules = cfg.rules || {}; + cfg.rules.movement = (cfg.rules.movement==='step')?'step':'dice'; + cfg.rules.fail = (cfg.rules.fail==='points')?'points':'lives'; + cfg.rules.lives = String(clamp(cfg.rules.lives, 1, 99, 3)); + cfg.rules.pts = String(clamp(cfg.rules.pts, 1, 999, 10)); + cfg.storyItems = (Array.isArray(cfg.storyItems)?cfg.storyItems:[]).filter(s=>s&&typeof s==='object').map(s=>({ + id: String(s.id||'').replace(/[^a-zA-Z0-9_]/g,'').slice(0,30) || ('s'+Math.random().toString(36).slice(2,8)), + emoji: str(s.emoji||'📖', 8), + text: str(s.text, 300), + position: (s.position==='after')?'after':'before', + fieldIndex: clamp(s.fieldIndex, 0, cfg.fieldCount-1, 0), + })); + cfg.quizData = (Array.isArray(cfg.quizData)?cfg.quizData:[]).filter(q=>q&&typeof q==='object').map(q=>({ + fieldIndex: clamp(q.fieldIndex, 0, cfg.fieldCount-1, 0), + question: str(q.question, 200), + answers: (Array.isArray(q.answers)?q.answers:[]).slice(0,4).map(a=>str(a,80)), + correct: (q.correct!=null && q.correct>=0 && q.correct<=3) ? (parseInt(q.correct)||0) : null, + })); + const cq = cfg.consequences || {}; + const OK_WIN = ['nothing','forward','points','again']; + const OK_LOSE = ['nothing','back','life','points','skip']; + cfg.consequences = { + win: OK_WIN.includes(cq.win)?cq.win:'nothing', + lose: OK_LOSE.includes(cq.lose)?cq.lose:'nothing', + winVal: clamp(cq.winVal, 1, 10, 2), + loseVal: clamp(cq.loseVal, 1, 10, 1), + winPts: clamp(cq.winPts, 1, 999, 10), + losePts: clamp(cq.losePts, 1, 999, 5), + }; + cfg.worldSeed = (parseInt(cfg.worldSeed)||0) >>> 0; + cfg.worldLocked = !!cfg.worldLocked; + cfg.mgSettings = (cfg.mgSettings && typeof cfg.mgSettings==='object') ? cfg.mgSettings : {}; +})(); + +const fields = (cfg.fields||[]).map(f=>f||null); +const fieldCount = cfg.fieldCount || fields.length || 10; +// Sanitize: Start- und Zielfeld dürfen NIE ein Minigame haben (sonst Sieg-Check feuert vorher → MG unerreichbar) +if (fields.length > 0) fields[0] = null; +while (fields.length < fieldCount) fields.push(null); +fields[fieldCount - 1] = null; +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={ + underwater:{primary:'#22d3ee',bg:'#082f49',glow:'rgba(34,211,238,0.4)',sky:'#0c4a6e',particle:'🫧'}, + space: {primary:'#a78bfa',bg:'#050314',glow:'rgba(167,139,250,0.45)',sky:'#1e1b4b',particle:'⭐'}, + haunted: {primary:'#86efac',bg:'#0a0a14',glow:'rgba(134,239,172,0.4)',sky:'#1a1a2e',particle:'👻'}, + fantasy: {primary:'#f0abfc',bg:'#1a0a2e',glow:'rgba(240,171,252,0.45)',sky:'#3b0764',particle:'✨'}, +}; +// Migration alter Welten-IDs auf die 4 verbliebenen (vor THEME-Lookup!) +const LEGACY_BG={ocean:'underwater',jungle:'fantasy',volcano:'space',snow:'space',city:'space',candy:'fantasy',desert:'fantasy',school:'fantasy',future:'space'}; +if(cfg.background && !BG_THEMES[cfg.background] && LEGACY_BG[cfg.background]) cfg.background=LEGACY_BG[cfg.background]; +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 = + esc(cfg.name||'Mein Spiel').replace(/(\S+)\s*$/, '$1'); +document.getElementById('introDesc').textContent = cfg.desc || ''; +document.getElementById('hudTitle').textContent = cfg.name || 'Spiel'; + +const meta = document.getElementById('introMeta'); +const BGNAMES={underwater:'Unterwasser',space:'Weltall',haunted:'Geisterhaus',fantasy:'Fantasy'}; +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 = [ + `
${figEmoji} ${FIGNAMES[cfg.figure]||'Figur'}
`, + `
🌍 ${BGNAMES[cfg.background]||'Setting'}
`, + `
⬛ ${fieldCount} Felder
`, + `
${rules.fail==='lives'?'❤️ '+rules.lives+' Leben':'⭐ Punkte'}
`, +].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{ + 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 (World-Engine) ══════════ */ +const bc = document.getElementById('boardCanvas'); +const bctx = bc.getContext('2d'); + +// DPR-aware sizing — Canvas füllt die .board-area +let bW = 0, bH = 0, bDpr = 1; +function resizeBoard() { + const ba = document.querySelector('.board-area'); + if (!ba) return; + const dpr = window.devicePixelRatio || 1; + const w = Math.max(640, ba.clientWidth - 24); + const h = Math.max(420, ba.clientHeight - 24); + bc.width = Math.round(w * dpr); + bc.height = Math.round(h * dpr); + bc.style.width = w + 'px'; + bc.style.height = h + 'px'; + bctx.setTransform(dpr, 0, 0, dpr, 0, 0); + bW = w; bH = h; bDpr = dpr; +} +resizeBoard(); +window.addEventListener('resize', resizeBoard); + +// Welt aus cfg.worldSeed (Fallback: deterministischer Hash aus Name+Dev) +const worldSeed = (cfg.worldSeed >>> 0) || World.hashSeed((cfg.name||'') + '|' + (cfg.devName||'') + '|' + (cfg.background||'')); +const world = World.generate(cfg.background, worldSeed, fieldCount); + +// ── Game state (Multiplayer-ready: players[]-Array + cur-Index) ── +let cur = 0; // aktiver Spieler-Index +function _makePlayer(idx, emoji){ + return { + id: idx, + figEmoji: emoji, + pos: 0, + lives: parseInt(rules.lives)||3, + maxLives: parseInt(rules.lives)||3, + points: 0, + visited: new Set([0]), + gamesPlayed: [], + figX: 0, figY: 0, figTargX: 0, figTargY: 0, + skipNext: false, + eliminated: false, + }; +} +const players = [_makePlayer(0, figEmoji)]; +if(cfg.playerCount === 2){ + const fig2Emoji = FIGURES[cfg.figure2] || '🦊'; + players.push(_makePlayer(1, fig2Emoji)); +} +function me() { return players[cur]; } +function nextPlayer(){ + if(players.length < 2) return; + for(let i=1; i<=players.length; i++){ + const next=(cur+i)%players.length; + if(!players[next].eliminated){ cur=next; return; } + } + // alle eliminiert: cur bleibt +} +function turnEnd(){ + if(gameEnded) return; + if(players.length < 2){ enableRoll(); return; } + nextPlayer(); + handleTurnStart(); +} +function handleTurnStart(){ + if(gameEnded) return; + // Aussetz-Flag prüfen (2P) + if(players.length >= 2 && me().skipNext){ + me().skipNext = false; + showToast(`⏭️ Spieler ${cur+1} ${me().figEmoji} setzt aus!`, 1500); + setTimeout(()=>{ nextPlayer(); handleTurnStart(); }, 1500); + return; + } + enableRoll(); + updateHUD(); + if(players.length >= 2){ + showToast(`▶ Spieler ${cur+1} ${me().figEmoji} ist dran!`, 1600); + } +} + +let quizIdx=0; +let rolling=false, waitingForAction=false; +let pendingStories=[], pendingStoryIdx=0; +let currentMgId=null, mgActive=null, mgResult=null; +let gameEnded=false; +let pendingExtraRoll=false, pendingSkipRoll=false; + +function fieldCenter(i) { return World.padCenter(world, i, bW, bH); } + +function updateFig() { + const p = me(); + const c = fieldCenter(p.pos); + p.figTargX = c.x; p.figTargY = c.y; +} +updateFig(); +players.forEach(p => { p.figX = p.figTargX; p.figY = p.figTargY; }); + +function drawBoard() { + // Pause während ein Mini-Game offen ist (Board liegt verdeckt hinter dem Overlay) + if (document.getElementById('mgOverlay')?.classList.contains('open')) { requestAnimationFrame(drawBoard); return; } + bctx.clearRect(0, 0, bW, bH); + + // Jede Spielfigur sanft Richtung Zielpad bewegen + for (const p of players) { + const tc = fieldCenter(p.pos); + p.figTargX = tc.x; p.figTargY = tc.y; + p.figX += (p.figTargX - p.figX) * 0.14; + p.figY += (p.figTargY - p.figY) * 0.14; + } + const meP = me(); + + World.render(bctx, world, { + W: bW, H: bH, + pos: meP.pos, visited: meP.visited, fields, storyItems, + // Neue Multi-Figuren-API + figures: players.map((p, i) => ({ emoji: p.figEmoji, x: p.figX, y: p.figY, active: i===cur, dimmed: p.eliminated })), + // Backwards-Compat (alter Single-Figure-Pfad in world.js) + figEmoji: meP.figEmoji, figX: meP.figX, figY: meP.figY, + gameName: cfg.name, devName: cfg.devName, + }, performance.now()); + + requestAnimationFrame(drawBoard); +} + +// Alte Render-Implementierung (durch World-Engine ersetzt) entfernt — Ende 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); + if(players.length >= 2){ + setTimeout(()=>showToast(`▶ Spieler 1 ${players[0].figEmoji} fängt an!`, 2200), 800); + } else { + setTimeout(()=>showToast(`${figEmoji} Viel Spaß, ${cfg.devName||'Spieler'}! Würfle zum Starten!`), 800); + } + }); +} + +/* ══════════ HUD ══════════ */ +function updateHUD() { + const is2P = players.length >= 2; + // 1P-Pillen ein/aus + document.getElementById('hudPosPill').style.display = is2P ? 'none' : ''; + document.getElementById('hudLifePill').style.display = is2P ? 'none' : (rules.fail==='lives' ? '' : 'none'); + document.getElementById('hudPtPill').style.display = is2P ? 'none' : (rules.fail==='points' ? '' : 'none'); + document.getElementById('hud2p').style.display = is2P ? '' : 'none'; + + if(is2P){ + // Beide Spieler-Pillen befüllen + for(let i=0; i<2; i++){ + const p = players[i]; + const fig = document.getElementById('hp'+(i+1)+'Fig'); + const pos = document.getElementById('hp'+(i+1)+'Pos'); + const stat= document.getElementById('hp'+(i+1)+'Stat'); + const box = document.getElementById('hudP'+(i+1)); + if(fig) fig.textContent = p.figEmoji; + if(pos) pos.textContent = `${p.pos+1}/${fieldCount}`; + if(stat){ + if(rules.fail==='lives'){ + stat.textContent = '❤️'.repeat(p.lives)+'🖤'.repeat(Math.max(0,p.maxLives-p.lives)); + } else { + stat.textContent = '⭐ '+p.points; + } + } + if(box){ + box.classList.toggle('active', i===cur && !p.eliminated); + box.classList.toggle('eliminated', !!p.eliminated); + } + } + } else { + const p = me(); + document.getElementById('hudPos').textContent=`${p.pos+1}/${fieldCount}`; + if(rules.fail==='lives'){ + document.getElementById('hudLives').textContent='❤️'.repeat(p.lives)+'🖤'.repeat(Math.max(0,p.maxLives-p.lives)); + } else { + document.getElementById('hudPts').textContent=p.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 ══════════ */ +const isStepMode = rules.movement === 'step'; + +// UI an Bewegungs-Modus anpassen (Schritt-Modus: kein Würfel, sondern "Nächster Zug") +(function setupMovementUI(){ + const lbl = document.getElementById('diceLabel'); + const em = document.getElementById('diceEmoji'); + const btn = document.getElementById('btnRoll'); + if(isStepMode){ + if(lbl) lbl.textContent = 'Nächster Zug'; + if(em) em.textContent = '👣'; + if(btn) btn.innerHTML = '👣 Weiter'; + } +})(); + +function doRoll() { + if(rolling||waitingForAction||gameEnded) return; + rolling=true; + document.getElementById('btnRoll').disabled=true; + playSfx('roll'); + + const de=document.getElementById('diceEmoji'); + de.classList.add('rolling'); + + if(isStepMode){ + // Schritt-Modus: kurze Animation, dann 1 Feld weiter + setTimeout(()=>{ + de.classList.remove('rolling'); + showToast('👣 1 Feld weiter!'); + setTimeout(()=>movePlayer(1), 300); + }, 400); + return; + } + + // Würfel-Modus: Faces rotieren, dann Zufallszahl + const faces=['⚀','⚁','⚂','⚃','⚄','⚅']; + 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=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 p = me(); + const newPos=Math.min(p.pos+steps,fieldCount-1); + // Animate step by step + let walkPos=p.pos; + function doStep(){ + if(walkPos>=newPos){ + p.pos=newPos; p.visited.add(newPos); rolling=false; + playSfx('land'); + updateHUD(); + updateFig(); + processLanding(); + return; + } + walkPos++; + p.visited.add(walkPos); + p.figTargX=fieldCenter(walkPos).x; + p.figTargY=fieldCenter(walkPos).y; + setTimeout(doStep, 220); + } + doStep(); +} + +/* ══════════ LANDING LOGIC ══════════ */ +function processLanding() { + const p = me(); + // Check stories BEFORE + const sBefore=storyItems.filter(s=>s.position==='before'&&s.fieldIndex===p.pos); + const sAfter =storyItems.filter(s=>s.position==='after' &&s.fieldIndex===p.pos); + + // Win check: letztes Feld — erst Erzähltexte zeigen, dann gewinnen + if(p.pos===fieldCount-1){ + if(sBefore.length>0){ + pendingStories=[...sBefore, {_win:true}, ...sAfter]; + pendingStoryIdx=0; + processNextStoryOrField(); + } else { + 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){ turnEnd(); return; } + if(item._win){ + setTimeout(showWin, 400); + 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 p = me(); + const gameId=fields[p.pos]; + if(!gameId||p.pos===0){ + // Empty field — check after stories + if(afterStories&&afterStories.length>0){ + pendingStories=afterStories; pendingStoryIdx=0; processNextStoryOrField(); + } else { showToast('⬜ Leeres Feld — kein Mini-Game.'); turnEnd(); } + 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'); + + // Größe aus dem mg-canvas-wrap ableiten (sobald das Overlay sichtbar ist), so groß wie der Test im Editor + let W = 1040, H = 560; + // Overlay ist gerade erst sichtbar — Browser muss Layout berechnen + // Im nächsten Frame lesen wir die echten Maße des Wraps und übergeben sie ans Modul + // (rAF-Verzögerung wird unten beim Aufruf abgewickelt) + + // Wire external MG modules to finishMinigame (oder finishDuelMinigame bei 2P-Duell) + const isDuel = (id === 'snake2p' || id === 'flappy2p'); + window._mgOnResult = isDuel ? finishDuelMinigame : finishMinigame; + + const mgCfg = { + theme: THEME, quizData, rules, + devName: cfg.devName, gameName: cfg.name, + figure: FIGURES[cfg.figure] || '🐦', + figure2: cfg.figure2 ? (FIGURES[cfg.figure2] || '🦊') : null, + fieldIndex: me().pos, // Quiz nutzt das, um die feldspezifische Frage zu finden + }; + if (cfg.mgSettings && cfg.mgSettings[id]) Object.assign(mgCfg, cfg.mgSettings[id]); // eingestellte Schwierigkeit übernehmen + + const launchers={ + snake: (w,W,H)=>window.MG_snake&&window.MG_snake.launch(w,W,H,mgCfg), + flappy: (w,W,H)=>window.MG_flappy&&window.MG_flappy.launch(w,W,H,mgCfg), + memory: (w,W,H)=>window.MG_memory&&window.MG_memory.launch(w,W,H,mgCfg), + quiz: (w,W,H)=>window.MG_quiz&&window.MG_quiz.launch(w,W,H,mgCfg), + reaction: (w,W,H)=>window.MG_reaction&&window.MG_reaction.launch(w,W,H,mgCfg), + basketball: (w,W,H)=>window.MG_basketball&&window.MG_basketball.launch(w,W,H,mgCfg), + catch: (w,W,H)=>window.MG_catch&&window.MG_catch.launch(w,W,H,mgCfg), + maze: (w,W,H)=>window.MG_maze&&window.MG_maze.launch(w,W,H,mgCfg), + simon: (w,W,H)=>window.MG_simon&&window.MG_simon.launch(w,W,H,mgCfg), + puzzle: (w,W,H)=>window.MG_puzzle&&window.MG_puzzle.launch(w,W,H,mgCfg), + spotdiff: (w,W,H)=>window.MG_spotdiff&&window.MG_spotdiff.launch(w,W,H,mgCfg), + typing: (w,W,H)=>window.MG_typing&&window.MG_typing.launch(w,W,H,mgCfg), + snake2p: (w,W,H)=>window.MG_snake2p&&window.MG_snake2p.launch(w,W,H,mgCfg), + flappy2p: (w,W,H)=>window.MG_flappy2p&&window.MG_flappy2p.launch(w,W,H,mgCfg), + }; + + // Layout des Wraps abwarten und ECHTE Maße verwenden (so wie's das Editor-Test-Popup macht) + requestAnimationFrame(()=>{ + const rect = wrap.getBoundingClientRect(); + const Wv = Math.max(360, Math.round(rect.width)) || W; + const Hv = Math.max(280, Math.round(rect.height)) || H; + if(launchers[id]){ + const result=launchers[id](wrap,Wv,Hv); + mgActive=result||{stop:()=>{}}; + } + else { + // Fallback placeholder + wrap.style.minHeight=Hv+'px'; + wrap.innerHTML=`
+
${esc(info.emoji)}
+
${esc(info.name)}
+
Dieses Mini-Game wird bald verfügbar sein!
+ +
`; + } + }); +} + +function finishMinigame(won){ + const p = me(); + if(mgActive&&mgActive.stop) mgActive.stop(); + mgActive=null; + p.gamesPlayed.push(currentMgId); + + const rb=document.getElementById('mgResultBar'); + rb.className='mg-result-bar '+(won?'win':'lose'); + rb.style.display='block'; + document.getElementById('mgResultTitle').textContent=won?'🎉 Gewonnen!':'💀 Verloren!'; + + // ── Basis-Spielregel-Konsequenz ── + let subMsg = won?'Super gemacht!':'Nicht schlimm — weiter gehts!'; + if(!won && rules.fail==='lives'){ + p.lives=Math.max(0,p.lives-1); + subMsg=`Du verlierst ein Leben. Noch ${p.lives} übrig.`; + playSfx('lose'); + } else if(won && rules.fail==='points'){ + const pts=parseInt(rules.pts)||10; + p.points+=pts; + subMsg=`+${pts} Punkte! Insgesamt: ${p.points}`; + playSfx('win'); + } else { + if(won) playSfx('win'); else playSfx('lose'); + } + + // ── Zusatz-Konsequenz (if/else aus Editor Schritt 3) ── + const cq = cfg.consequences || {}; + const action = won ? (cq.win||'nothing') : (cq.lose||'nothing'); + let cqMsg = ''; + if(action === 'forward'){ + const v = parseInt(won ? cq.winVal : cq.loseVal) || (won?2:1); + p.pos = Math.min(fieldCount-1, p.pos+v); + p.visited.add(p.pos); + updateFig(); + cqMsg = `+${v} Felder vor!`; + // Bug 3 Fix: Sieg prüfen wenn Konsequenz ans Ziel schiebt + if(p.pos === fieldCount-1){ setTimeout(showWin, 1600); } + } else if(action === 'back'){ + const v = parseInt(cq.loseVal) || 1; + p.pos = Math.max(0, p.pos-v); + p.visited.add(p.pos); + updateFig(); + cqMsg = `-${v} Felder zurück!`; + } else if(action === 'life' && !won && rules.fail !== 'lives'){ + p.lives = Math.max(0, p.lives-1); + cqMsg = `Noch ${p.lives} Leben.`; + } else if(action === 'points'){ + const v = parseInt(won ? cq.winPts : cq.losePts) || (won?10:5); + if(won){ p.points+=v; cqMsg=`+${v} Punkte!`; } + else { p.points=Math.max(0,p.points-v); cqMsg=`-${v} Punkte!`; } + } else if(action === 'again' && won){ + pendingExtraRoll = true; + cqMsg = 'Nochmal würfeln!'; + } else if(action === 'skip' && !won){ + pendingSkipRoll = true; + cqMsg = 'Nächste Runde aussetzen!'; + } + + document.getElementById('mgResultSub').textContent = + cqMsg ? `${subMsg} ${cqMsg}` : subMsg; + + updateHUD(); + document.getElementById('btnMgContinue').style.display='block'; + if(rules.fail==='lives' && p.lives<=0){ setTimeout(showGameOver,1200); } +} + +/* ══════════ DUEL-MINIGAME (snake2p) ══════════ */ +function finishDuelMinigame(winnerIdx){ + if(mgActive&&mgActive.stop) mgActive.stop(); + mgActive=null; + + const rb=document.getElementById('mgResultBar'); + rb.style.display='block'; + + // Beide Spieler protokollieren das gespielte Minispiel + players.forEach(p => p.gamesPlayed.push(currentMgId)); + + if(winnerIdx === -1){ + // Unentschieden — keine Konsequenzen + rb.className='mg-result-bar'; + document.getElementById('mgResultTitle').textContent = '🤝 Unentschieden!'; + document.getElementById('mgResultSub').textContent = 'Niemand gewinnt — der Zug geht normal weiter.'; + playSfx('mg'); + updateHUD(); + document.getElementById('btnMgContinue').style.display='block'; + return; + } + + const winner = players[winnerIdx]; + const loser = players[1 - winnerIdx]; + rb.className='mg-result-bar win'; + document.getElementById('mgResultTitle').textContent = + `🏆 Spieler ${winnerIdx+1} ${winner.figEmoji} gewinnt das Duell!`; + + const subParts = []; + + // ── Basis-Spielregel ── + if(rules.fail==='lives'){ + loser.lives = Math.max(0, loser.lives-1); + subParts.push(`${loser.figEmoji} −1 Leben (${loser.lives})`); + playSfx('lose'); + } else if(rules.fail==='points'){ + const pts = parseInt(rules.pts)||10; + winner.points += pts; + subParts.push(`+${pts} Punkte für ${winner.figEmoji}`); + playSfx('win'); + } else { + playSfx('win'); + } + + // ── Sieg-Konsequenz für Sieger ── + const cq = cfg.consequences || {}; + const winAction = cq.win || 'nothing'; + const loseAction = cq.lose || 'nothing'; + + if(winAction === 'forward'){ + const v = parseInt(cq.winVal)||2; + winner.pos = Math.min(fieldCount-1, winner.pos+v); + winner.visited.add(winner.pos); + subParts.push(`${winner.figEmoji} +${v} Felder`); + if(winner.pos === fieldCount-1){ setTimeout(()=>showWin(winnerIdx), 1800); } + } else if(winAction === 'points'){ + const v = parseInt(cq.winPts)||10; + winner.points += v; + subParts.push(`+${v} Punkte für ${winner.figEmoji}`); + } else if(winAction === 'again'){ + // Nur sinnvoll, wenn Sieger der aktive Spieler ist (er hat gewürfelt) + if(winnerIdx === cur){ pendingExtraRoll = true; subParts.push(`${winner.figEmoji} darf nochmal würfeln!`); } + } + + // ── Niederlage-Konsequenz für Verlierer ── + if(loseAction === 'back'){ + const v = parseInt(cq.loseVal)||1; + loser.pos = Math.max(0, loser.pos-v); + loser.visited.add(loser.pos); + subParts.push(`${loser.figEmoji} −${v} Felder`); + } else if(loseAction === 'points'){ + const v = parseInt(cq.losePts)||5; + loser.points = Math.max(0, loser.points-v); + subParts.push(`${loser.figEmoji} −${v} Punkte`); + } else if(loseAction === 'life' && rules.fail !== 'lives'){ + loser.lives = Math.max(0, loser.lives-1); + subParts.push(`${loser.figEmoji} −1 Leben (${loser.lives})`); + } else if(loseAction === 'skip'){ + loser.skipNext = true; + subParts.push(`${loser.figEmoji} setzt aus`); + } + + document.getElementById('mgResultSub').textContent = subParts.join(' · ') || 'Weiter geht\'s!'; + updateHUD(); + document.getElementById('btnMgContinue').style.display='block'; + + // Eliminierungs-Check (Race-Sieg bei Lebensverlust) + if(rules.fail==='lives' && loser.lives<=0){ + setTimeout(()=>{ + loser.eliminated = true; + const other = players.find(p => !p.eliminated); + if(other) showWin(other.id); + }, 1500); + } +} + +function afterMinigame(){ + document.getElementById('mgOverlay').classList.remove('open'); + updateHUD(); + // Bug 2 Fix: pendingExtraRoll / pendingSkipRoll auswerten + if(pendingExtraRoll){ + pendingExtraRoll=false; + if(pendingStories.length>0){ processNextStoryOrField(); } + else { enableRoll(); showToast('🎲 Nochmal würfeln!'); } + return; + } + if(pendingSkipRoll){ + pendingSkipRoll=false; + if(pendingStories.length>0){ processNextStoryOrField(); return; } + if(players.length >= 2){ + // 2P: skip-Flag setzen + sofort Turn-Switch + me().skipNext = true; + showToast('⏭️ Du setzt eine Runde aus!'); + setTimeout(turnEnd, 1200); + } else { + // 1P: alter 3s-Block + enableRoll(); showToast('⏭️ Nächste Runde aussetzen — warte auf deinen Zug!'); + waitingForAction=true; + setTimeout(()=>{ waitingForAction=false; showToast('▶️ Du darfst wieder würfeln!'); }, 3000); + } + return; + } + // Process after-stories + if(pendingStories.length>0){ processNextStoryOrField(); } + else { turnEnd(); } +} + +function skipMinigame(){ + if(mgActive&&mgActive.stop) mgActive.stop(); + mgActive=null; + document.getElementById('mgOverlay').classList.remove('open'); + enableRoll(); +} + +/* ══ SNAKE ══ */ +function showWin(winnerIdx){ + if(gameEnded) return; + gameEnded=true; + playSfx('win'); + const winner = (winnerIdx != null) ? players[winnerIdx] : me(); + const ov=document.getElementById('resultOverlay'); + document.getElementById('resEmoji').textContent = winner.figEmoji + '🏆'; + document.getElementById('resTitle').style.color = ''; + if(players.length >= 2){ + document.getElementById('resTitle').textContent = `Spieler ${winner.id+1} gewinnt!`; + document.getElementById('resSub').textContent = `${winner.figEmoji} hat das Ziel als Erster erreicht!`; + } else { + document.getElementById('resTitle').textContent='Du hast gewonnen!'; + document.getElementById('resSub').textContent=`Super gemacht! Du hast alle ${fieldCount} Felder durchgespielt!`; + } + document.getElementById('resStats').innerHTML=` +
${winner.gamesPlayed.length}
Mini-Games
+
${rules.fail==='points'?winner.points:winner.lives+'❤️'}
${rules.fail==='points'?'Punkte':'Leben übrig'}
+
${winner.visited.size}
Felder besucht
+ `; + ov.classList.add('open'); +} + +function showGameOver(){ + if(gameEnded) return; + // 2P: Aktiver Spieler verliert alle Leben → eliminiert; wenn anderer noch lebt → Race-Sieg + if(players.length >= 2){ + me().eliminated = true; + const other = players.find(p => !p.eliminated); + if(other){ + showWin(other.id); + return; + } + } + gameEnded=true; + playSfx('lose'); + const p = me(); + 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=` +
${p.gamesPlayed.length}
Mini-Games gespielt
+
${p.visited.size}/${fieldCount}
Felder besucht
+ `; + ov.classList.add('open'); +} + +function showMenu(){ showToast('☰ Menü kommt in der finalen Version!'); } +function backToEditor(){ if(window.opener&&!window.opener.closed) window.close(); else window.location.href='editor.html'; } diff --git a/js/minigame-test.js b/js/minigame-test.js new file mode 100644 index 0000000..5c0c6c5 --- /dev/null +++ b/js/minigame-test.js @@ -0,0 +1,155 @@ +/* minigame-test.js — Test-Popup (preview) der Mini-Game-Module */ +/* ══════════════════════════════════════════════ + INIT +══════════════════════════════════════════════ */ +// ── Init wird nach Modulen ausgeführt (siehe unten) ── + +/* ══════════════════════════════════════════════ + MINI-GAME TEST POPUP +══════════════════════════════════════════════ */ +let mgTestLoop=null; +let mgTestActive=null; + + +function openMGTest(id, e){ + e&&e.stopPropagation(); + const mg=MINIGAMES.find(m=>m.id===id);if(!mg)return; + closeMGTest(); + currentMGId=id; + document.getElementById('mgTestEmoji').textContent=mg.e; + document.getElementById('mgTestTitle').textContent=mg.n+' — Probespiel'; + // Beschreibung aus Modul oder MINIGAMES + const mod=window['MG_'+id]; + document.getElementById('mgTestDesc').textContent=mod?mod.desc:(mg.desc||''); + document.getElementById('mgTestMsg').textContent=''; + document.getElementById('mgTestMsg').className=''; + const ctrl=document.getElementById('mgTestControls'); + ctrl.innerHTML=renderMgSettings(id)+'
'; + document.getElementById('mgTestOverlay').classList.add('open'); + document.body.style.overflow='hidden'; + // Modul-System: preview() aufrufen + const wrap=document.getElementById('mgTestWrap'); + wrap.innerHTML=''; + if(mod && mod.preview){ + // Theme aus aktuellem ST-State + const bg=BACKGROUNDS.find(b=>b.id===ST.background); + const modCfg={ + theme: bg?{primary:bg.color||'#7c3aed',glow:bg.glow||'rgba(124,58,237,0.4)'}:{primary:'#7c3aed',glow:'rgba(124,58,237,0.4)'}, + quizData: ST.quizData||[], + devName: ST.devName, + gameName: ST.name, + figure: (FIGURES.find(f=>f.id===ST.figure)||{}).e || '🐦', + }; + Object.assign(modCfg, getMgSettings(id)); + // onResult-Handler + window._mgOnResult=(won)=>{ + setMsg(won?'🏆 Gewonnen! 🎉':'💀 Verloren! Versuch es nochmal.', won?'win':'lose'); + }; + if(window.MGAPI) MGAPI.onResult=window._mgOnResult; + // Bug 4 Fix: requestAnimationFrame stellt sicher dass der Browser das + // Overlay gerendert hat und wrap.offsetWidth korrekt ist + requestAnimationFrame(()=>{ + mgTestActive=mod.preview(wrap, Math.max(wrap.offsetWidth,360), Math.max(wrap.offsetHeight||0,560), modCfg); + }); + } else { + setMsg('Vorschau für dieses Mini-Game nicht verfügbar.', 'lose'); + } +} +function closeMGTest(){ + // Modul stoppen falls aktiv + if(mgTestActive&&mgTestActive.stop){mgTestActive.stop();mgTestActive=null;} + if(mgTestLoop){clearInterval(mgTestLoop);cancelAnimationFrame(mgTestLoop);mgTestLoop=null;} + document.removeEventListener('keydown',mgKeyHandler); + document.removeEventListener('mousemove',mgMouseHandler); + document.getElementById('mgTestOverlay').classList.remove('open'); + document.body.style.overflow=''; + // Wrap leeren + const wrap=document.getElementById('mgTestWrap'); + if(wrap)wrap.innerHTML=''; + const canvas=document.getElementById('mgTestCanvas'); + if(canvas){const ctx=canvas.getContext('2d');ctx.clearRect(0,0,canvas.width,canvas.height);} + currentMGId=null; +} +let currentMGId=null; +function restartMGTest(){ + if(currentMGId)openMGTest(currentMGId,null); +} +let mgKeyHandler=null,mgMouseHandler=null; +function setMsg(txt,cls=''){const m=document.getElementById('mgTestMsg');m.textContent=txt;m.className=cls;} + +/* ══════════════════════════════════════════════ + SCHWIERIGKEITS-EINSTELLUNGEN je Mini-Game + Werte landen in ST.mgSettings[id] → werden exportiert (Teilen-URL) + und im fertigen Spiel angewandt. +══════════════════════════════════════════════ */ +const MG_SETTINGS_DEF={ + flappy:[ + {key:'win', label:'Hindernisse bis Sieg', min:5, max:30, step:1, def:15}, + {key:'speed',label:'Start-Tempo', min:1.8,max:4.5, step:0.1, def:2.7, fmt:v=>(+v).toFixed(1)}, + {key:'gap', label:'Start-Größe Durchgang',min:110,max:210, step:5, def:168, fmt:v=>v+' px'}, + {key:'rampSpeed',type:'toggle',label:'Tempo steigt pro Durchgang',def:true}, + {key:'shrinkGap',type:'toggle',label:'Durchgänge werden enger', def:true}, + ], + flappy2p:[ + {key:'bestOf', label:'Best of', min:2, max:5, step:1, def:3, fmt:v=>'Best of '+v}, + {key:'speed', label:'Start-Tempo', min:1.8,max:4.5, step:0.1, def:2.7, fmt:v=>(+v).toFixed(1)}, + {key:'gap', label:'Start-Größe Durchgang',min:110,max:210, step:5, def:168, fmt:v=>v+' px'}, + {key:'rampSpeed',type:'toggle',label:'Tempo steigt pro Durchgang',def:true}, + {key:'shrinkGap',type:'toggle',label:'Durchgänge werden enger', def:true}, + ], +}; + +// Liefert (und initialisiert mit Defaults) die Einstellungen eines Mini-Games +function getMgSettings(id){ + if(!ST.mgSettings)ST.mgSettings={}; + const defs=MG_SETTINGS_DEF[id]; + if(!defs)return {}; + if(!ST.mgSettings[id])ST.mgSettings[id]={}; + defs.forEach(d=>{ if(ST.mgSettings[id][d.key]==null)ST.mgSettings[id][d.key]=d.def; }); + return ST.mgSettings[id]; +} + +// Einstellungs-Panel im Test-Popup (Slider = Startwerte, Toggles = Steigerung obendrauf) +function renderMgSettings(id){ + const defs=MG_SETTINGS_DEF[id]; if(!defs)return ''; + const s=getMgSettings(id); + const rows=defs.filter(d=>d.type!=='toggle').map(d=>{ + const v=s[d.key], disp=d.fmt?d.fmt(v):v; + return `
+ ${d.label} + + ${disp} +
`; + }).join(''); + const toggles=defs.filter(d=>d.type==='toggle'); + let tg=''; + if(toggles.length){ + tg='
'+toggles.map(d=>{ + const on=!!s[d.key]; + return `
+
${on?'✓':'○'}
${d.label}
`; + }).join('')+'
'; + } + return `
+
⚙️ Schwierigkeit — wird im geteilten Spiel übernommen
${rows}${tg}
`; +} + +// Live: nur Anzeige aktualisieren (beim Ziehen) +function mgSetLabel(id,key,val){ + const d=(MG_SETTINGS_DEF[id]||[]).find(x=>x.key===key); if(!d)return; + const el=document.getElementById('mgset-'+id+'-'+key); + if(el)el.textContent=d.fmt?d.fmt(val):val; +} +// Übernehmen (beim Loslassen): speichern + Vorschau neu starten +function mgSetApply(id,key,val){ + getMgSettings(id)[key]=Number(val); + save(); + restartMGTest(); +} +// Anklickbares Feld umschalten (Steigerung an/aus) +function mgToggle(id,key){ + const s=getMgSettings(id); s[key]=!s[key]; + save(); + restartMGTest(); +} diff --git a/js/quiz.js b/js/quiz.js new file mode 100644 index 0000000..d5df410 --- /dev/null +++ b/js/quiz.js @@ -0,0 +1,232 @@ +/* quiz.js — Quiz-Builder, Review-Screen, Feedback-Bogen */ +/* ══════════════════════════════════════════════ + STEP 4 — QUIZ +══════════════════════════════════════════════ */ +function isQComplete(q){return q.question.trim()&&q.answers.every(a=>a.trim())&&q.correct!==null;} +function getQ(fi){return ST.quizData.find(q=>q.fieldIndex===fi);} + +function buildQuiz(){ + const qidx=ST.fields.map((f,i)=>f==='quiz'?i:-1).filter(i=>i>=0); + qidx.forEach(fi=>{if(!ST.quizData.find(q=>q.fieldIndex===fi))ST.quizData.push({fieldIndex:fi,question:'',answers:['','','',''],correct:null});}); + ST.quizData=ST.quizData.filter(q=>qidx.includes(q.fieldIndex)); + const area=document.getElementById('quizArea');area.innerHTML=''; + if(qidx.length===0){ + area.innerHTML=`

Keine Quiz-Felder platziert

In Schritt 3 hast du keine Quiz-Felder hinzugefügt.
Du kannst trotzdem weitermachen!

`; + document.getElementById('s4next').disabled=false;return; + } + const done=ST.quizData.filter(isQComplete).length; + const ov=document.createElement('div');ov.className='ov-bar'; + ov.innerHTML=`
✅ Fertig: ${done}/${qidx.length}
❓ Quiz-Felder: ${qidx.length}
`; + area.appendChild(ov); + const af=document.createElement('div');af.className='af-bar'; + af.innerHTML=`
Beispielfragen
Alle leeren Felder automatisch ausfüllen
`; + area.appendChild(af); + ST.quizData.forEach((q,i)=>area.appendChild(mkQCard(q,i))); + updQNext(); +} +function updQNext(){ + const qidx=ST.fields.map((f,i)=>f==='quiz'?i:-1).filter(i=>i>=0); + document.getElementById('s4next').disabled=qidx.length>0&&ST.quizData.filter(isQComplete).length${done}/${qidx.length}`;} +} +function mkQCard(q,idx){ + // XSS-sicher: fieldIndex wird zu Integer geclampt, Frage/Antworten via esc() (entschärft <,>,&,",') + const comp=isQComplete(q); + const fi = parseInt(q.fieldIndex)|0; // hart casten + const c=document.createElement('div');c.className='card quiz-card'+(comp?' complete':'');c.id='qc_'+fi; + const fl=fi===0?'Start':fi===ST.fieldCount-1?'Ziel':'Feld '+fi; + const ans = LETTERS.map((L,i)=>{ + const v = esc(q.answers[i]||''); + return `
${L}
`; + }).join(''); + const corr = LETTERS.map((L,i)=>``).join(''); + c.innerHTML=` +
+
${idx+1}
+
Quiz-Frage ${idx+1}
+
${comp?'✓ Fertig':'⏳ Ausfüllen'}
+
+ + + +
${ans}
+
+ ✅ Richtige Antwort: +
${corr}
+
+ +
• Stelle eine klare, eindeutige Frage
• Alle 4 Antworten sollten plausibel klingen
• Nur eine Antwort ist korrekt
• Beziehe die Frage auf dein Spielthema!
+ `; + return c; +} +function updQ(fi,field,val){ + const q=getQ(fi);if(!q)return; + if(field==='q')q.question=val; + else q.answers[parseInt(field.slice(1))]=val; + refQCard(fi);updQNext(); +} +function setCorr(fi,idx){ + const q=getQ(fi);if(!q)return;q.correct=idx; + LETTERS.forEach((_,i)=>{const w=document.getElementById(`aw_${fi}_${i}`);if(w)w.className='ans-wrap '+(i===idx?'correct':'');}); + const card=document.getElementById('qc_'+fi);if(card)card.querySelectorAll('.correct-btn').forEach((b,i)=>b.classList.toggle('selected',i===idx)); + refQCard(fi);updQNext(); +} +function refQCard(fi){ + const q=getQ(fi);if(!q)return; + const comp=isQComplete(q); + const c=document.getElementById('qc_'+fi);if(!c)return; + c.classList.toggle('complete',comp); + const st=document.getElementById('qst_'+fi);if(st){st.className='qc-status '+(comp?'done':'todo');st.textContent=comp?'✓ Fertig':'⏳ Ausfüllen';} + const num=document.getElementById('qnum_'+fi);if(num)num.style.background=comp?'linear-gradient(135deg,#10b981,#059669)':'linear-gradient(135deg,#7c3aed,#f5a623)'; +} +function autoFill(){ + ST.quizData.forEach((q,i)=>{if(!isQComplete(q)){const ex=QUIZ_EX[i%QUIZ_EX.length];q.question=ex.q;q.answers=[...ex.a];q.correct=ex.c;}}); + buildQuiz();showToast('✅ Beispielfragen eingefügt!'); +} + +/* ══════════════════════════════════════════════ + STEP 5 — REVIEW + TEST +══════════════════════════════════════════════ */ +function buildReview(){ + const fig=FIGURES.find(f=>f.id===ST.figure)||{e:'🎮',n:'Figur'}; + const bg=BACKGROUNDS.find(b=>b.id===ST.background)||{scene:'🌍',n:'Welt'}; + const area=document.getElementById('reviewArea');area.innerHTML=''; + + // Hero card + const hero=document.createElement('div');hero.className='hero-card'; + hero.innerHTML=`
${esc(bg.scene)}${esc(fig.e)}
${esc(ST.name||'Mein Spiel')}
${esc(ST.desc||'Keine Beschreibung.')}
${esc(fig.e)} ${esc(fig.n)}
🌍 ${esc(bg.n)}
⬛ ${esc(String(ST.fieldCount))} Felder
`; + area.appendChild(hero); + + // Rules + const rules=document.createElement('div');rules.className='card'; + const filled=ST.fields.slice(1,ST.fieldCount-1).filter(Boolean).length; + rules.innerHTML=`
⚙️ Spielregeln
🎲 Bewegung
${ST.rules.movement==='dice'?'🎲 Würfeln':'👣 Ein Feld'}
${ST.rules.fail==='lives'?'❤️ Leben':'⭐ Punkte'}
${ST.rules.fail==='lives'?ST.rules.lives+' Leben':'+'+ST.rules.pts+' Pts'}
🕹️ Mini-Games
${filled} Felder
📖 Erzähltexte
${ST.storyItems.length}
`; + area.appendChild(rules); + + // Timeline + const tlCard=document.createElement('div');tlCard.className='card'; + tlCard.innerHTML='
🗺️ Spielablauf
'; + const tl=document.createElement('div');tl.className='tl'; + for(let i=0;im.id===g):null; + const isSt=i===0,isEn=i===ST.fieldCount-1; + ST.storyItems.filter(s=>s.position==='before'&&s.fieldIndex===i).forEach(s=>{ + const it=document.createElement('div');it.className='tl-item story-it'; + it.innerHTML=`
${esc(s.emoji||'📖')}
Erzähl-Text
${esc(s.text||'(noch leer)')}
Vor Feld ${i}
`; + tl.appendChild(it); + }); + const it=document.createElement('div');it.className='tl-item'; + let dCls,lCls,lbl,sub,badge=''; + if(isSt){dCls='s';lCls='s';lbl='🟢 Start';sub='Das Abenteuer beginnt!';} + else if(isEn){dCls='e';lCls='e';lbl='🏁 Ziel';sub='Hier gewinnt man!';} + else if(mg){dCls='g';lCls='g';lbl='Feld '+i;sub=mg.n;badge=`
${esc(mg.e)} ${esc(mg.n)}
`;} + else{dCls='em';lCls='em';lbl='Feld '+i;sub='Leeres Durchgangsfeld';} + it.innerHTML=`
${isSt?'🟢':isEn?'🏁':mg?esc(mg.e):'⬜'}
${esc(lbl)}
${esc(sub)}
${badge}
`; + tl.appendChild(it); + ST.storyItems.filter(s=>s.position==='after'&&s.fieldIndex===i).forEach(s=>{ + const it2=document.createElement('div');it2.className='tl-item story-it'; + it2.innerHTML=`
${esc(s.emoji||'📖')}
Erzähl-Text
${esc(s.text||'(noch leer)')}
Nach Feld ${i}
`; + tl.appendChild(it2); + }); + } + tlCard.appendChild(tl);area.appendChild(tlCard); + + // Quiz preview + if(ST.quizData.length>0){ + const qc=document.createElement('div');qc.className='card'; + qc.innerHTML='
❓ Quiz-Fragen
'; + const ql=document.createElement('div');ql.className='qp-list'; + ST.quizData.forEach((q,i)=>{ + const qi=document.createElement('div');qi.className='qp-item'; + qi.innerHTML=`
Frage ${i+1}: ${esc(q.question||'(leer)')}
${LETTERS.map((L,j)=>`
${esc(L)} ${esc(q.answers[j]||'?')}${q.correct===j?' ✓':''}
`).join('')}
`; + ql.appendChild(qi); + }); + qc.appendChild(ql);area.appendChild(qc); + } + + // Checklist + const cc=document.createElement('div');cc.className='card';cc.innerHTML='
🔍 Checkliste
'; + const cl=document.createElement('div');cl.className='checklist'; + const qDone=ST.quizData.filter(isQComplete).length; + [ + {ok:!!ST.name,t:ST.name?`Spielname: "${esc(ST.name)}"`:'Kein Spielname vergeben'}, + {ok:!!(ST.figure&&ST.background),t:ST.figure?`Figur & Setting: ${esc(fig.n)} / ${esc(bg.n)}`:'Figur oder Setting fehlt'}, + {ok:filled>0,t:`${filled} von ${ST.fieldCount-2} Spielfeldern belegt`}, + {ok:ST.quizData.length===0||qDone===ST.quizData.length,t:ST.quizData.length===0?'Keine Quiz-Felder (ok)':`${qDone}/${ST.quizData.length} Quiz-Fragen ausgefüllt`}, + ].forEach(ch=>{const it=document.createElement('div');it.className='chk-item '+(ch.ok?'ok':'warn');it.innerHTML=`${ch.ok?'✅':'⚠️'}${ch.t}`;cl.appendChild(it);}); + cc.appendChild(cl);area.appendChild(cc); + + // Fill launch card + document.getElementById('lhFig').textContent=fig.e; + document.getElementById('lhName').textContent=ST.name||'Mein Spiel'; + document.getElementById('lhDesc').textContent=ST.desc||''; + document.getElementById('lhPills').innerHTML=[ + `
${fig.e} ${fig.n}
`, + `
🌍 ${bg.n}
`, + `
⬛ ${ST.fieldCount} Felder
`, + `
${ST.rules.fail==='lives'?'❤️ '+ST.rules.lives+' Leben':'⭐ Punkte'}
`, + ].join(''); + const uniqueG=[...new Set(ST.fields.filter(Boolean))]; + document.getElementById('launchChk').innerHTML=[ + {i:'🎲',t:`${ST.fieldCount} Felder · ${filled} Mini-Games`}, + {i:'🕹️',t:`Games: ${uniqueG.map(g=>{const m=MINIGAMES.find(x=>x.id===g);return m?m.e:'❓';}).join(' ')||'—'}`}, + {i:'📖',t:`${ST.storyItems.length} Erzähltexte`}, + {i:'🎵',t:`Musik-Theme: ${bg.n}`}, + ].map(x=>`
${x.i}${x.t}
`).join(''); +} + +let gamePlayed=false; +function doLaunch(){ + save(); + const w=window.open('game.html','Spiel',`width=${Math.min(1100,screen.width-60)},height=${Math.min(800,screen.height-60)},left=40,top=30,resizable=yes,scrollbars=no`); + if(!w){showToast('⚠️ Popup blockiert! Bitte Popups erlauben.');return;} + document.getElementById('btnLaunch').textContent='⏳ Warte auf Spielende...'; + document.getElementById('btnLaunch').disabled=true; + const poll=setInterval(()=>{if(w.closed){clearInterval(poll);onGameDone();}},800); +} +function onGameDone(){ + gamePlayed=true; + document.getElementById('btnLaunch').textContent='🔄 Nochmal spielen'; + document.getElementById('btnLaunch').disabled=false; + document.getElementById('playedBadge').classList.add('visible'); + const fc=document.getElementById('fbCard');fc.style.display='block'; + fc.style.animation='slideIn 0.4s ease both'; + setTimeout(()=>fc.scrollIntoView({behavior:'smooth',block:'start'}),500); + fbCur=0;fbAnswers={};renderFB(); +} + +/* ══════════════════════════════════════════════ + FEEDBACK +══════════════════════════════════════════════ */ +let fbCur=0,fbAnswers={}; +function renderFB(){ + const q=FB_QS[fbCur]; + document.getElementById('fbFill').style.width=(fbCur/FB_QS.length*100)+'%'; + document.getElementById('fbPrev').style.display=fbCur>0?'':'none'; + document.getElementById('fbNext').textContent=fbCurFrage ${fbCur+1} von ${FB_QS.length}
${q.q}
${q.sub?`
${q.sub}
`:''}`; + if(q.t==='mc'){ + const opts=document.createElement('div');opts.className='mc-opts'; + q.opts.forEach((o,i)=>{const btn=document.createElement('div');btn.className='mc-opt'+(fbAnswers[q.id]===i?' sel':'');btn.innerHTML=`
${o}`;btn.addEventListener('click',()=>{fbAnswers[q.id]=i;opts.querySelectorAll('.mc-opt').forEach(b=>b.classList.remove('sel'));btn.classList.add('sel');document.getElementById('fbNext').disabled=false;});opts.appendChild(btn);}); + area.appendChild(opts); + } + if(q.t==='scale'){ + const row=document.createElement('div');row.className='scale-row'; + for(let i=1;i<=5;i++){const btn=document.createElement('button');btn.className='scale-btn'+(fbAnswers[q.id]===i?' sel':'');btn.textContent=i;btn.addEventListener('click',()=>{fbAnswers[q.id]=i;row.querySelectorAll('.scale-btn').forEach(b=>b.classList.remove('sel'));btn.classList.add('sel');document.getElementById('fbNext').disabled=false;});row.appendChild(btn);} + area.appendChild(row); + const lbl=document.createElement('div');lbl.className='scale-lbls';lbl.innerHTML=`${q.min}${q.max}`;area.appendChild(lbl); + } + if(q.t==='text'){ + const ta=document.createElement('textarea');ta.className='fb-ta';ta.rows=3;ta.placeholder=q.ph||'';ta.value=fbAnswers[q.id]||'';ta.addEventListener('input',()=>{fbAnswers[q.id]=ta.value;document.getElementById('fbNext').disabled=ta.value.trim().length<3;});area.appendChild(ta); + } + document.getElementById('fbNext').disabled=fbAnswers[q.id]===undefined; +} +function fbFwd(){ + fbCur++; + if(fbCur>=FB_QS.length){showSummaryCard();return;} + renderFB(); +} +function fbBack(){if(fbCur>0){fbCur--;renderFB();}} + diff --git a/js/share.js b/js/share.js new file mode 100644 index 0000000..ea60cbb --- /dev/null +++ b/js/share.js @@ -0,0 +1,362 @@ +/* share.js — Veröffentlichen: Link/QR/PDF, Teilen */ +/* ══════════════════════════════════════════════ + SUMMARY CARD +══════════════════════════════════════════════ */ +function showSummaryCard(){ + document.getElementById('fbCard').style.display='none'; + const sc=document.getElementById('sumCard');sc.style.display='block'; + sc.style.animation='slideIn 0.4s ease both'; + setTimeout(()=>sc.scrollIntoView({behavior:'smooth',block:'start'}),400); + const fun=fbAnswers.fun||3;const mg=fbAnswers.minigames||3; + const diff=fbAnswers.difficulty;const replay=fbAnswers.replay; + document.getElementById('sumText').textContent='Du hast dein Spiel getestet und Feedback gegeben. Hier ist deine Auswertung:'; + document.getElementById('sumScores').innerHTML=`
${fun}/5
Spaßfaktor
${mg}/5
Mini-Game Passung
${ST.fieldCount}
Felder
${ST.storyItems.length}
Erzähltexte
`; + const notes=[]; + if(fun<=2)notes.push({i:'🎯',t:'Der Spaßfaktor ist noch niedrig — überlege, spannendere Mini-Games oder Erzähltexte hinzuzufügen.'}); + else if(fun>=4)notes.push({i:'🔥',t:'Super Spaßfaktor! Dein Spiel macht offensichtlich Freude.'}); + if(diff===0)notes.push({i:'⚠️',t:'Das Spiel war zu einfach — probiere schwierigere Mini-Games oder weniger Leben.'}); + else if(diff===3)notes.push({i:'⚠️',t:'Das Spiel war zu schwer — erhöhe die Lebensanzahl oder wähle einfachere Mini-Games.'}); + if(ST.storyItems.length===0)notes.push({i:'📖',t:'Keine Erzähltexte vorhanden — sie machen das Spiel viel lebendiger!'}); + if(ST.fields.slice(1,ST.fieldCount-1).filter(Boolean).length<3)notes.push({i:'🕹️',t:'Nur wenige Felder sind belegt — füge mehr Mini-Games hinzu für mehr Abwechslung.'}); + if(replay>=2)notes.push({i:'🚀',t:'Das Spiel hat Wiederspielwert — großartig!'}); + if(fbAnswers.problems&&fbAnswers.problems.length>5)notes.push({i:'🔧',t:`Problem: "${esc(fbAnswers.problems)}" — geh zurück und überarbeite das.`}); + if(notes.length===0)notes.push({i:'✅',t:'Alles sieht gut aus! Dein Spiel ist bereit zum Teilen.'}); + document.getElementById('sumNotes').innerHTML=notes.map(n=>`
${n.i}${n.t}
`).join(''); + save();localStorage.setItem('testFeedback',JSON.stringify(fbAnswers)); +} + +// ── URL-Sharing System ───────────────────────────────────────────────────── +// Komprimiert die Spielkonfiguration und kodiert sie als URL-Hash +// JSON → pako.gzip (DEFLATE) → Base64 → URL-Hash + +function buildShareURL() { + // Nur die nötigen Felder exportieren (kein highestStep etc.) + const exportCfg = { + devName: ST.devName, + name: ST.name, + desc: ST.desc, + figure: ST.figure, + figure2: ST.figure2, + playerCount: ST.playerCount, + background: ST.background, + fieldCount: ST.fieldCount, + fields: ST.fields, + storyItems: ST.storyItems, + quizData: ST.quizData, + rules: ST.rules, + consequences:ST.consequences, + mgSettings: ST.mgSettings, + worldSeed: ST.worldSeed, + worldLocked: ST.worldLocked, + }; + const json = JSON.stringify(exportCfg); + // pako ist über CDN geladen – falls nicht verfügbar: direktes Base64 + let encoded; + try { + const compressed = pako.deflate(json, { level: 9 }); + // Uint8Array → binary string → btoa + const binary = Array.from(compressed).map(b => String.fromCharCode(b)).join(''); + encoded = 'z:' + btoa(binary); + } catch(e) { + // Fallback ohne Kompression + encoded = 'j:' + btoa(unescape(encodeURIComponent(json))); + } + const base = window.location.href.replace(/\/editor\.html.*$/, '') + '/game.html'; + return base + '#' + encoded; +} + +// ── Kurz-Code (6 Zeichen) zum manuellen Eintippen ── +const CODE_ALPHA = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; // ohne 0,O,1,I,L (Verwechslungsschutz) +function genShareCode(){ + let s = ''; + for(let i=0;i<6;i++) s += CODE_ALPHA[Math.floor(Math.random()*CODE_ALPHA.length)]; + return s; +} + +// URL der /play/-Landing-Page (für den zweiten QR) +function playPageURL(){ + return window.location.href.replace(/\/editor\.html.*$/, '') + '/play/'; +} + +// Spielkonfiguration zum Server hochladen → Kurz-Code reservieren +async function uploadCfgForCode(code, cfg){ + const res = await fetch('/api/boardgame-play/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code, cfg }), + }); + if(!res.ok) throw new Error('Server-Fehler ' + res.status); + return res.json(); +} + +function doPublish() { + const url = buildShareURL(); + // Spiel-Info füllen + const bg = BACKGROUNDS.find(b => b.id === ST.background); + const fig = FIGURES.find(f => f.id === ST.figure); + const infoEl = document.getElementById('shareGameInfo'); + infoEl.innerHTML = ` +
+
${fig ? fig.e : '🎮'}${bg ? bg.e : ''}
+
+
${esc(ST.name || 'Mein Spiel')}
+
von ${esc(ST.devName || 'Anonym')} · ${ST.fieldCount} Felder · ${ST.fields.filter(Boolean).length} Mini-Games
+
+
+ `; + // Link setzen + const input = document.getElementById('shareLinkInput'); + input.value = url; + const lenEl = document.getElementById('shareLinkLen'); + lenEl.textContent = `Link-Länge: ${url.length} Zeichen${url.length > 4000 ? ' ⚠️ zu lang für QR-Code!' : ' ✅ QR-Code-tauglich'}`; + // Haupt-QR-Code (zum direkten Spiel-Link) + const qrEl = document.getElementById('shareQR'); + qrEl.innerHTML = ''; + try { + new QRCode(qrEl, { + text: url, width: 180, height: 180, + colorDark: '#000000', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.M, + }); + } catch(e) { + qrEl.innerHTML = '
QR-Code konnte nicht erstellt werden.
Link zum Kopieren nutzen.
'; + } + // Overlay zeigen + const ov = document.getElementById('shareOverlay'); + ov.style.display = 'flex'; + document.body.style.overflow = 'hidden'; + + // ── Kurz-Code-Bereich ── + const codeBox = document.getElementById('shareCodeBox'); + const codeEl = document.getElementById('shareCodeText'); + const playUrlEl= document.getElementById('sharePlayUrl'); + const playQrEl = document.getElementById('sharePlayQR'); + const codeStatus = document.getElementById('shareCodeStatus'); + if(codeBox){ + codeBox.style.display = ''; + if(codeEl) codeEl.textContent = '······'; + if(codeStatus) codeStatus.textContent = 'wird erzeugt …'; + if(playUrlEl) playUrlEl.textContent = playPageURL().replace(/^https?:\/\//,''); + if(playQrEl){ + playQrEl.innerHTML = ''; + try{ + new QRCode(playQrEl, { text: playPageURL(), width:120, height:120, + colorDark:'#000000', colorLight:'#ffffff', correctLevel: QRCode.CorrectLevel.M }); + }catch(e){} + } + } + // Code reservieren (re-publish nutzt denselben Code) + const exportCfg = { + devName: ST.devName, name: ST.name, desc: ST.desc, figure: ST.figure, figure2: ST.figure2, + playerCount: ST.playerCount, background: ST.background, fieldCount: ST.fieldCount, + fields: ST.fields, storyItems: ST.storyItems, quizData: ST.quizData, rules: ST.rules, + consequences: ST.consequences, + mgSettings: ST.mgSettings, worldSeed: ST.worldSeed, worldLocked: ST.worldLocked, + }; + const useCode = ST.shareCode || genShareCode(); + uploadCfgForCode(useCode, exportCfg) + .then(() => { + ST.shareCode = useCode; + save(); + if(codeEl) codeEl.textContent = useCode; + if(codeStatus) codeStatus.textContent = '✓ bereit'; + }) + .catch(err => { + console.warn('Kurz-Code Upload fehlgeschlagen:', err); + if(codeEl) codeEl.textContent = '— —'; + if(codeStatus) codeStatus.innerHTML = 'offline?'; + }); +} + +function closeShare() { + document.getElementById('shareOverlay').style.display = 'none'; + document.body.style.overflow = ''; +} + +async function printAsPDF() { + // Heutiges Datum DD.MM.YYYY (dynamisch — kein Hardcode) + const _d = new Date(); + const todayStr = String(_d.getDate()).padStart(2,'0') + '.' + + String(_d.getMonth()+1).padStart(2,'0') + '.' + _d.getFullYear(); + const qrEl = document.getElementById('shareQR'); + const canvas = qrEl.querySelector('canvas'); + if (!canvas) { appAlert('QR-Code nicht gefunden.'); return; } + const qrDataUrl = canvas.toDataURL('image/png'); + const { jsPDF } = window.jspdf; + const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); + const pageW = 210, pageH = 297, margin = 14; + + await new Promise(resolve => { + const img = new Image(); + img.onload = () => { + const c = document.createElement('canvas'); + c.width = img.naturalWidth; c.height = img.naturalHeight; + c.getContext('2d').drawImage(img, 0, 0); + // Logo klein, zentriert + const logoW = 72, logoH = logoW * (img.naturalHeight / img.naturalWidth); + doc.addImage(c.toDataURL('image/png'), 'PNG', (pageW - logoW) / 2, margin, logoW, logoH); + let y = margin + logoH + 3; + + // Trennlinie grau + doc.setDrawColor(160, 160, 160); doc.setLineWidth(0.3); + doc.line(margin, y, pageW - margin, y); y += 4; + + // Fach-Bezeichnung + doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(90, 90, 90); + doc.text('Fach Informatik & Medien- und Bildungsmanagement', pageW / 2, y, { align: 'center' }); y += 5; + + // Datum + doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(40, 40, 40); + doc.text(todayStr, pageW / 2, y, { align: 'center' }); y += 6; + + // Spielname + doc.setFont('helvetica', 'bold'); doc.setFontSize(22); doc.setTextColor(10, 10, 10); + doc.text(ST.name || 'Mein Spiel', pageW / 2, y, { align: 'center' }); y += 5; + + // Autor + Info + doc.setFont('helvetica', 'normal'); doc.setFontSize(9); doc.setTextColor(110, 110, 110); + const miniCount = ST.fields.filter(Boolean).length; + doc.text('von ' + (ST.devName || 'Anonym') + ' · ' + ST.fieldCount + ' Felder · ' + miniCount + ' Mini-Games', pageW / 2, y, { align: 'center' }); y += 9; + + // ── Zwei Spalten: Haupt-QR (Direktstart) links, Kurz-Code-Block rechts ── + const colGap = 8; + const qrLarge = 70; + const colW = (pageW - 2*margin - colGap) / 2; + const leftX = margin; + const rightX = margin + colW + colGap; + + // ── LINKE SPALTE: QR direkt ── + // Überschrift links + doc.setFont('helvetica', 'bold'); doc.setFontSize(9); doc.setTextColor(40,40,40); + doc.text('📱 Mit Handy scannen', leftX + colW/2, y, { align: 'center' }); + const qrX = leftX + (colW - qrLarge) / 2; + const qrY = y + 4; + doc.setDrawColor(180,180,180); doc.setLineWidth(0.25); + doc.rect(qrX - 1, qrY - 1, qrLarge + 2, qrLarge + 2); + doc.addImage(qrDataUrl, 'PNG', qrX, qrY, qrLarge, qrLarge); + doc.setFont('helvetica','italic'); doc.setFontSize(7.5); doc.setTextColor(120,120,120); + doc.text('Kamera → öffnet Spiel direkt', leftX + colW/2, qrY + qrLarge + 4, { align: 'center' }); + + // ── RECHTE SPALTE: Kurz-Code + Play-Seite-QR ── + const shareCode = (ST.shareCode || '······').toUpperCase(); + const playUrl = playPageURL().replace(/^https?:\/\//, ''); + doc.setFont('helvetica', 'bold'); doc.setFontSize(9); doc.setTextColor(40,40,40); + doc.text('⌨️ Am PC eintippen', rightX + colW/2, y, { align: 'center' }); + + // Kleiner QR für /play/ Seite + const playQrEl = document.getElementById('sharePlayQR'); + const playQrCanvas = playQrEl ? playQrEl.querySelector('canvas') : null; + const smallQR = 28; + if (playQrCanvas) { + const playQrDataUrl = playQrCanvas.toDataURL('image/png'); + const playQrX = rightX + 2; + const playQrY = y + 4; + doc.setDrawColor(200,200,200); doc.setLineWidth(0.2); + doc.rect(playQrX - 0.5, playQrY - 0.5, smallQR + 1, smallQR + 1); + doc.addImage(playQrDataUrl, 'PNG', playQrX, playQrY, smallQR, smallQR); + doc.setFont('helvetica','normal'); doc.setFontSize(6); doc.setTextColor(120,120,120); + doc.text('zur Seite:', playQrX + smallQR/2, playQrY + smallQR + 3, { align: 'center' }); + } + + // URL-Text + Code + const txtX = rightX + smallQR + 8; + let codeY = y + 9; + doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.setTextColor(80,80,80); + doc.text('1. Im Browser öffnen:', txtX, codeY); codeY += 3.5; + doc.setFont('helvetica','bold'); doc.setFontSize(8); doc.setTextColor(30,30,30); + doc.text(playUrl, txtX, codeY); codeY += 7; + doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.setTextColor(80,80,80); + doc.text('2. Diesen Code eingeben:', txtX, codeY); codeY += 6; + doc.setFont('courier', 'bold'); doc.setFontSize(22); doc.setTextColor(20,20,20); + doc.text(shareCode, txtX, codeY); + + y = qrY + qrLarge + 12; + + // ---- Schnittlinie ---- + const stripW = 43, stripH = 23; + const stripX = (pageW - stripW) / 2; + const stripY = pageH - margin - stripH; + const cutY = stripY - 6; + + // Schnittlinie + doc.setDrawColor(150, 150, 150); doc.setLineWidth(0.25); + doc.setLineDashPattern([1.5, 1.5], 0); + doc.line(margin, cutY, pageW - margin, cutY); + doc.setLineDashPattern([], 0); + doc.setFont('helvetica', 'normal'); doc.setFontSize(5.5); doc.setTextColor(170, 170, 170); + doc.text('✂ Hier abschneiden', margin + 1, cutY - 1.2); + + // ---- Ausschneidstreifen 41 × 23 mm (QR 23×23 | Text ~18×23) ---- + // Rahmen + doc.setDrawColor(100, 100, 100); doc.setLineWidth(0.3); + doc.rect(stripX, stripY, stripW, stripH); + // Trennlinie bei 23mm (QR-Breite) + doc.setLineWidth(0.15); doc.setDrawColor(180,180,180); + doc.line(stripX + 23, stripY, stripX + 23, stripY + stripH); + + // QR links 23×23 + doc.addImage(qrDataUrl, 'PNG', stripX, stripY, 23, 23); + + // Text rechts — linksbündig mit 1.5mm Einzug, vertikal zentriert + const tx = stripX + 23 + 1.5; + doc.setFont('helvetica', 'bold'); doc.setFontSize(5.5); doc.setTextColor(20, 20, 20); + const gameName = (ST.name || 'Mein Spiel').substring(0, 18); + doc.text(gameName, tx, stripY + 5); + doc.setFont('helvetica', 'normal'); doc.setFontSize(4); doc.setTextColor(110,110,110); + doc.text(todayStr + ' · PH Weingarten', tx, stripY + 9); + doc.setFont('helvetica', 'normal'); doc.setFontSize(4); doc.setTextColor(120,120,120); + doc.text('Code:', tx, stripY + 14); + doc.setFont('courier', 'bold'); doc.setFontSize(7); doc.setTextColor(20,20,20); + doc.text((ST.shareCode || '······').toUpperCase(), tx, stripY + 18.5); + + resolve(); + }; + img.onerror = () => { resolve(); }; + img.src = 'phw_logo.png'; + }); + const filename = (ST.name || 'spiel').replace(/[^a-zA-Z0-9_\-]/g, '_') + '_qr.pdf'; + doc.save(filename); +} + +function copyShareLink() { + const input = document.getElementById('shareLinkInput'); + input.select(); + navigator.clipboard.writeText(input.value).then(() => { + const btn = document.getElementById('shareCopyBtn'); + btn.textContent = '✅ Kopiert!'; + setTimeout(() => btn.textContent = '📋 Kopieren', 2000); + }).catch(() => { + document.execCommand('copy'); + showToast('📋 Link kopiert!'); + }); +} + +function shareViaEmail() { + const url = document.getElementById('shareLinkInput').value; + const subject = encodeURIComponent('🎲 Mein Brettspiel: ' + (ST.name || 'Mein Spiel')); + const body = encodeURIComponent( + 'Hallo!\n\nIch habe ein Brettspiel erstellt und möchte es mit dir teilen:\n\n' + + (ST.name || 'Mein Spiel') + '\n' + url + '\n\nViel Spaß beim Spielen!\n' + (ST.devName || '') + ); + window.open('mailto:?subject=' + subject + '&body=' + body); +} + +function shareViaWhatsApp() { + const url = document.getElementById('shareLinkInput').value; + const text = encodeURIComponent('🎲 Mein Brettspiel "' + (ST.name || 'Mein Spiel') + '" – einfach anklicken und spielen! ' + url); + window.open('https://wa.me/?text=' + text, '_blank', 'noopener,noreferrer'); +} + +function shareNative() { + const url = document.getElementById('shareLinkInput').value; + if (navigator.share) { + navigator.share({ + title: ST.name || 'Mein Brettspiel', + text: 'Ich habe ein Brettspiel erstellt!', + url: url, + }).catch(() => {}); + } else { + copyShareLink(); + showToast('📋 Link in die Zwischenablage kopiert!'); + } +} + diff --git a/js/state.js b/js/state.js new file mode 100644 index 0000000..c9f8633 --- /dev/null +++ b/js/state.js @@ -0,0 +1,98 @@ +/* state.js — Zustand ST + localStorage + Reset */ +/* ══════════════════════════════════════════════ + STATE +══════════════════════════════════════════════ */ +const ST={ + devName:'',name:'',desc:'',figure:'',figure2:'',background:'', + playerCount:1, + fieldCount:10,fields:Array(10).fill(null), + storyItems:[],quizData:[], + rules:{movement:'dice',fail:'lives',lives:'3',pts:'10'}, + consequences:{win:'nothing',winVal:2,lose:'nothing',loseVal:1,winPts:10,losePts:5}, + mgSettings:{}, + worldSeed:0, + worldLocked:false, + shareCode:'', + highestStep:0, +}; +try{ + const sv=JSON.parse(localStorage.getItem('gameConfig')||'{}'); + if(sv.devName)ST.devName=sv.devName; + if(sv.name)ST.name=sv.name;if(sv.desc)ST.desc=sv.desc; + if(sv.figure)ST.figure=sv.figure;if(sv.figure2)ST.figure2=sv.figure2;if(sv.background)ST.background=sv.background; + if(sv.playerCount===1||sv.playerCount===2)ST.playerCount=sv.playerCount; + if(sv.fieldCount)ST.fieldCount=sv.fieldCount; + if(sv.fields){ST.fields=sv.fields;while(ST.fields.length>>0; + if(sv.worldLocked)ST.worldLocked=!!sv.worldLocked; + if(sv.shareCode)ST.shareCode=String(sv.shareCode); + if(sv.highestStep)ST.highestStep=sv.highestStep; +}catch(e){} + +let cur=0; + +function save(){ + try { + localStorage.setItem('gameConfig',JSON.stringify({ + devName:ST.devName,name:ST.name,desc:ST.desc,figure:ST.figure,figure2:ST.figure2,playerCount:ST.playerCount,background:ST.background, + fieldCount:ST.fieldCount,fields:ST.fields,storyItems:ST.storyItems, + quizData:ST.quizData,rules:ST.rules,consequences:ST.consequences,mgSettings:ST.mgSettings,worldSeed:ST.worldSeed,worldLocked:ST.worldLocked,shareCode:ST.shareCode,highestStep:ST.highestStep, + })); + } catch(e) { + if(e.name==='QuotaExceededError'){ + appAlert('Speicher voll. Bitte altes Spiel löschen.'); + } + } + showSaveIndicator(); + // Note: code update is triggered by input/change listeners, not here +} + +function showSaveIndicator(){ + const el=document.getElementById('saveIndicator'); + if(!el)return; + el.style.opacity='1'; + clearTimeout(el._hideTimer); + el._hideTimer=setTimeout(()=>{el.style.opacity='0';},2000); +} + +/* ── App-Dialog (ersetzt natives confirm/alert) ── */ +let _appOk=null; +function appConfirm(message, onOk, opts){ + opts=opts||{}; + const dlg=document.getElementById('appDialog'); if(!dlg){ if(confirm(message)&&onOk)onOk(); return; } + document.getElementById('appDialogIcon').textContent = opts.icon || (opts.danger?'⚠️':'❓'); + document.getElementById('appDialogMsg').textContent = message; + const yes=document.getElementById('appDialogYes'); const no=document.getElementById('appDialogNo'); + yes.textContent = opts.okText || 'Ja'; + no.textContent = opts.cancelText || 'Abbrechen'; + yes.classList.toggle('danger', !!opts.danger); + no.style.display = opts.noCancel ? 'none' : ''; + _appOk = onOk || null; + dlg.classList.add('open'); +} +function appAlert(message, onOk, opts){ + appConfirm(message, onOk, Object.assign({noCancel:true, okText:'OK', icon:'ℹ️'}, opts||{})); +} +function appDialogOk(){ + document.getElementById('appDialog').classList.remove('open'); + const fn=_appOk; _appOk=null; + if(fn){ try{ fn(); }catch(e){ console.error(e); } } +} +function appDialogCancel(){ + document.getElementById('appDialog').classList.remove('open'); + _appOk=null; +} + +function resetAll(){ + appConfirm('Wirklich alles zurücksetzen?\n\nAlle Eingaben gehen verloren!', ()=>{ + localStorage.removeItem('gameConfig'); + localStorage.removeItem('testFeedback'); + location.reload(); + }, {danger:true, okText:'Zurücksetzen', icon:'🗑️'}); +} + diff --git a/js/tour.js b/js/tour.js new file mode 100644 index 0000000..540ba63 --- /dev/null +++ b/js/tour.js @@ -0,0 +1,278 @@ +/** + * js/tour.js — Tour durch Schritt 3 (Spielfeld bauen) + * Triggert beim ersten Aufruf von goTo(2). Figur (cfg.figure) führt durch alle Bereiche. + * Während der Tour ist die Seite VOLLSTÄNDIG GESPERRT — nur die Buttons in der + * Sprechblase und ein Klick auf den Text (für Skip-Tippvorgang) sind aktiv. + */ + +(function () { + + // ── Storyboard ── + // selector: CSS-Selector des Anker-Elements (null = kein Anker, Figur in Mitte) + // text: Sprechblasen-Text (\n für Absatz) + // side: 'left'|'right'|'top'|'bottom'|'auto' — wo die Figur am Anker landet + const TOUR_STEPS = [ + { + id: 'welcome', selector: null, + text: "Hi! Ich bin {fig} und führe dich kurz durch dein Spielfeld. Los geht's!\n\nDer Board Game Emulator befindet sich übrigens noch in der Entwicklung. Wenn dir auffällt, dass was nicht funktioniert, gib das kurz deinem Lehrer oder deiner Lehrerin weiter — vielleicht kann er/sie dir direkt helfen.", + }, + { + id: 'bewegung', selector: '[data-tour="bewegung"]', side: 'bottom', + text: "Hier wählst du, wie sich deine Figur bewegt — würfeln für Spannung, oder genau ein Feld pro Runde für mehr Kontrolle.", + }, + { + id: 'spielmodus', selector: '[data-tour="spielmodus"]', side: 'bottom', + text: "Willst du lieber mit Leben spielen, die du verlieren kannst? Oder Punkte sammeln beim Gewinnen?", + }, + { + id: 'modus-counter', selector: '[data-tour="modus-counter"]:not([style*="display: none"])', side: 'bottom', + text: "Wie viele Leben jeder bekommt — oder mit welcher Punktzahl gestartet wird — stellst du hier ein.", + }, + { + id: 'code-pane', selector: '#codePane', side: 'left', + text: "Hier rechts siehst du den Programmcode deines Spiels — und zwar live! Während du dein Spiel baust, kannst du direkt mitverfolgen, wie der Code im Hintergrund entsteht. Das ist übrigens Python — eine sehr häufig benutzte Programmiersprache.", + }, + { + id: 'konsequenz-sieg', selector: '[data-tour="konsequenz-sieg"]', side: 'bottom', + text: "Wenn ein Mini-Game gewonnen wird, darf was Tolles passieren: ein paar Felder vor, Bonuspunkte oder nochmal würfeln.", + }, + { + id: 'konsequenz-niederlage', selector: '[data-tour="konsequenz-niederlage"]', side: 'bottom', + text: "Und wenn jemand verliert? Vielleicht ein paar Felder zurück, ein Leben weniger oder eine Runde aussetzen.", + }, + { + id: 'fieldcount', selector: '[data-tour="fieldcount"]', side: 'right', + text: "Wie groß soll dein Spielfeld sein? Zwischen 6 und 30 Feldern — je mehr, desto länger das Spiel.", + }, + { + id: 'palette', selector: '[data-tour="palette"]', side: 'right', + text: "Das hier sind deine Bausteine: 12 Mini-Games und Erzähltexte. Die ziehst du gleich aufs Spielfeld.", + }, + { + id: 'world-reroll', selector: '#wbReroll', side: 'right', + text: "Das ist dein Spielfeld! Mit '🎲 Welt neu generieren' würfelst du eine neue Welt aus — so lange, bis dir der Pfad gefällt.", + }, + { + id: 'world-canvas', selector: '#worldCanvas', side: 'right', + text: "Zieh die Bausteine einfach auf die runden Felder hier. Klick auf ein belegtes Feld, wenn du es wieder leeren willst.", + }, + { + id: 'world-lock', selector: '#wbLock', side: 'right', + text: "Wenn dein Spielbrett perfekt ist, klick auf '✓ Spielbrett verwenden' — dann bleibt es so.", + }, + { + id: 'next', selector: '#s3next', side: 'left', + text: "Fertig? Dann geht's mit 'Weiter' zum Quiz. Viel Spaß beim Bauen!", + }, + ]; + + // ── State ── + let stepIdx = 0; + let typingTimer = null; + let typingFull = ''; + let typingPos = 0; + let active = false; + let layoutHandler = null; + let suppressAnim = false; // Scroll/Resize → keine Position-Transition (sonst hinkt's hinterher) + + // ── Public API ── + window.tourStart = function () { + if (active) return; + active = true; + stepIdx = 0; + const root = document.getElementById('tourRoot'); + if (!root) return; + root.style.display = ''; + // Figur-Emoji aus ST.figure + const figId = typeof ST !== 'undefined' && ST.figure; + const fig = (typeof FIGURES !== 'undefined' && FIGURES.find(f => f.id === figId)) || null; + const figEmoji = fig ? fig.e : '🎮'; + document.getElementById('tourFigure').textContent = figEmoji; + // Layout neu-bauen bei Resize/Scroll (capture: fängt auch Scrolls auf inneren Containern wie #editorPane) + layoutHandler = () => { suppressAnim = true; layoutCurrent(); }; + window.addEventListener('resize', layoutHandler); + document.addEventListener('scroll', layoutHandler, { passive: true, capture: true }); + showStep(0); + }; + + window.tourNext = function () { + if (!active) return; + if (typingTimer) { skipTypewriter(); return; } + if (stepIdx >= TOUR_STEPS.length - 1) { tourFinish(); return; } + showStep(stepIdx + 1); + }; + + window.tourPrev = function () { + if (!active) return; + if (stepIdx > 0) showStep(stepIdx - 1); + }; + + window.tourSkipConfirm = function () { + if (!active) return; + appConfirm('Tour wirklich beenden?\n\nDu kannst sie später jederzeit nochmal starten.', + () => tourFinish(), + { okText: 'Ja, beenden', cancelText: 'Weiter ansehen', icon: '🚪' } + ); + }; + + function tourFinish() { + if (!active) return; + active = false; + if (typingTimer) { clearInterval(typingTimer); typingTimer = null; } + const root = document.getElementById('tourRoot'); + if (root) root.style.display = 'none'; + if (layoutHandler) { + window.removeEventListener('resize', layoutHandler); + document.removeEventListener('scroll', layoutHandler, { capture: true }); + layoutHandler = null; + } + try { localStorage.setItem('tourSchritt3Done', '1'); } catch (e) {} + } + + // ── Steps ── + function showStep(idx) { + stepIdx = idx; + const step = TOUR_STEPS[idx]; + // Progress-Indikator + const prog = document.getElementById('tourBubbleProgress'); + if (prog) prog.textContent = `${idx + 1} / ${TOUR_STEPS.length}`; + // Buttons aktualisieren + const btnNext = document.getElementById('tourBtnNext'); + if (btnNext) btnNext.textContent = (idx >= TOUR_STEPS.length - 1) ? '✓ Fertig' : 'Weiter →'; + const btnBack = document.getElementById('tourBtnBack'); + if (btnBack) btnBack.style.display = idx > 0 ? '' : 'none'; + // Bei echtem Step-Wechsel: Position animieren (suppressAnim = false) + suppressAnim = false; + layoutCurrent(); + // Nach kurzem Delay zurück in den Scroll-Follow-Modus (no anim) + setTimeout(() => { suppressAnim = true; }, 80); + // Text mit Typewriter starten (Figur-Token ersetzen) + const figEmoji = document.getElementById('tourFigure').textContent; + const text = step.text.replace(/\{fig\}/g, figEmoji); + startTypewriter(text); + } + + function layoutCurrent() { + if (!active) return; + const step = TOUR_STEPS[stepIdx]; + const root = document.getElementById('tourRoot'); + const highlight = document.getElementById('tourHighlight'); + const character = document.getElementById('tourCharacter'); + if (!step) return; + + // Transition-Klassen für scroll-follow vs. step-change + if (suppressAnim) { + character.classList.add('tour-no-anim'); + highlight.classList.add('tour-no-anim'); + } else { + character.classList.remove('tour-no-anim'); + highlight.classList.remove('tour-no-anim'); + } + + let targetEl = null; + if (step.selector) { + const cands = document.querySelectorAll(step.selector); + for (const c of cands) { + if (c.offsetWidth > 0 && c.offsetHeight > 0) { targetEl = c; break; } + } + } + + if (!targetEl) { + // Kein Anker → Hintergrund dunkel (no-anchor-Modus), Figur mittig, kein Highlight + root.classList.add('no-anchor'); + highlight.style.display = 'none'; + character.style.left = (window.innerWidth/2 - 200) + 'px'; + character.style.top = (window.innerHeight/2 - 120) + 'px'; + character.dataset.side = 'center'; + return; + } + root.classList.remove('no-anchor'); + + // Element ins Viewport scrollen, falls außerhalb + const r = targetEl.getBoundingClientRect(); + const PAD = 10; + const inView = r.top >= 0 && r.bottom <= window.innerHeight; + if (!inView) targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + // Highlight positionieren + highlight.style.display = ''; + highlight.style.top = (r.top - PAD) + 'px'; + highlight.style.left = (r.left - PAD) + 'px'; + highlight.style.width = (r.width + PAD * 2) + 'px'; + highlight.style.height = (r.height + PAD * 2) + 'px'; + + // Figur+Bubble platzieren — Seite je nach Anker + const charW = 380, charH = 200; + let side = step.side || 'right'; + let cx, cy; + if (side === 'right') { + cx = r.right + 24; + cy = r.top + r.height/2 - charH/2; + if (cx + charW > window.innerWidth - 12) side = 'left'; + } + if (side === 'left') { + cx = r.left - charW - 24; + cy = r.top + r.height/2 - charH/2; + if (cx < 12) side = 'bottom'; + } + if (side === 'top') { + cx = r.left + r.width/2 - charW/2; + cy = r.top - charH - 16; + if (cy < 12) side = 'bottom'; + } + if (side === 'bottom') { + cx = r.left + r.width/2 - charW/2; + cy = r.bottom + 16; + } + // Clamp ins Viewport + cx = Math.max(12, Math.min(window.innerWidth - charW - 12, cx)); + cy = Math.max(12, Math.min(window.innerHeight - charH - 12, cy)); + character.style.left = cx + 'px'; + character.style.top = cy + 'px'; + character.dataset.side = side; + } + + // ── Typewriter ── + function startTypewriter(text) { + if (typingTimer) clearInterval(typingTimer); + typingFull = text; + typingPos = 0; + const el = document.getElementById('tourBubbleText'); + el.textContent = ''; + typingTimer = setInterval(() => { + typingPos++; + el.textContent = typingFull.slice(0, typingPos); + if (typingPos >= typingFull.length) { + clearInterval(typingTimer); + typingTimer = null; + } + }, 38); + } + + function skipTypewriter() { + if (!typingTimer) return; + clearInterval(typingTimer); + typingTimer = null; + document.getElementById('tourBubbleText').textContent = typingFull; + } + + // Klick auf den Text-Bereich → Tippen überspringen + document.addEventListener('click', (e) => { + if (!active) return; + const text = document.getElementById('tourBubbleText'); + if (text && text.contains(e.target) && typingTimer) { + e.stopPropagation(); + skipTypewriter(); + } + }); + + // ── Auto-Trigger beim ersten Aufruf von Schritt 3 ── + window.maybeStartTour = function () { + try { + if (localStorage.getItem('tourSchritt3Done') === '1') return; + } catch (e) {} + // Tour erst nach kurzem Delay starten, damit Layout/Render fertig sind + setTimeout(() => tourStart(), 350); + }; + +})(); diff --git a/js/wizard.js b/js/wizard.js new file mode 100644 index 0000000..ed9c6a5 --- /dev/null +++ b/js/wizard.js @@ -0,0 +1,340 @@ +/* wizard.js — Schritt-Navigation + Screens 1-3 (Grundinfos, Gestaltung, Regeln) */ +/* ══════════════════════════════════════════════ + NAVIGATION +══════════════════════════════════════════════ */ +function goTo(step){ + if(step>cur&&!canLeave(cur))return; + save(); + const from=document.getElementById('s'+cur); + from.classList.remove('active'); + from.style.display='none'; + setTimeout(()=>{from.style.display='';},50); + if(step>cur)ST.highestStep=Math.max(ST.highestStep,step); + cur=step; + const to=document.getElementById('s'+cur); + to.style.display=''; + to.classList.add('active','slide-in'); + to.addEventListener('animationend',()=>to.classList.remove('slide-in'),{once:true}); + const ep=document.getElementById('editorPane');if(ep)ep.scrollTo({top:0,behavior:'smooth'});else window.scrollTo({top:0,behavior:'smooth'}); + updatePB(); + showSummary(step); + if(step===1)s2init(); + if(step===2){ s3init(); if(typeof maybeStartTour==='function') maybeStartTour(); } + if(step===3)buildQuiz(); + if(step===4)buildReview(); + showToast('✅ Schritt '+(step+1)+' geöffnet!'); +} + +function pbClick(step){if(step0; + if(step===1)return !!(ST.figure&&ST.background) && (ST.playerCount!==2 || (ST.figure2 && ST.figure2!==ST.figure)); + if(step===2)return ST.fields.slice(1,ST.fieldCount-1).filter(Boolean).length>0; + return true; +} + +function updatePB(){ + for(let i=0;i<5;i++){ + const d=document.getElementById('pb'+i); + d.className='pb-step'+(i===cur?' active':i👋 ${esc(ST.devName)}`;html+=`
🎮 ${esc(ST.name)}
`;if(ST.desc)html+=`
"${esc(ST.desc.slice(0,40))}${ST.desc.length>40?'…':''}"
`;} + if(idx===1){const f=FIGURES.find(x=>x.id===ST.figure);const b=BACKGROUNDS.find(x=>x.id===ST.background);if(f)html+=`
${f.e} ${f.n}
`;if(b)html+=`
🌍 ${b.n}
`;} + if(idx===2){const filled=ST.fields.slice(1,ST.fieldCount-1).filter(Boolean).length;html+=`
⬛ ${ST.fieldCount} Felder
🕹️ ${filled} Mini-Games
📖 ${ST.storyItems.length} Erzähltexte
`;} + if(idx===3){const done=ST.quizData.filter(isQComplete).length;html+=`
❓ ${done}/${ST.quizData.length||0} Quiz-Fragen
`;} + chips.innerHTML=html; +} + +function showToast(msg){ + const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show'); + setTimeout(()=>t.classList.remove('show'),2000); +} + +/* ══════════════════════════════════════════════ + STEP 1 — GRUNDINFOS +══════════════════════════════════════════════ */ +function s1init(){ + document.getElementById('s1devname').value=ST.devName; + document.getElementById('s1name').value=ST.name; + document.getElementById('s1desc').value=ST.desc; + s1update(); +} +function s1update(){ + ST.devName=document.getElementById('s1devname').value.trim(); + ST.name=document.getElementById('s1name').value.trim(); + ST.desc=document.getElementById('s1desc').value; + const nl=document.getElementById('s1name').value.length; + const dl=ST.desc.length; + document.getElementById('s1nc').textContent=nl+' / 40'; + document.getElementById('s1dc').textContent=dl+' / 120'; + document.getElementById('s1dc').className='char-row'+(dl>100?' warn':''); + const s1dis=!ST.name||!ST.devName; + document.getElementById('s1next').disabled=s1dis; + const s1hint=document.getElementById('s1hint');if(s1hint)s1hint.style.display=s1dis?'block':'none'; + // Persönliche Begrüßung + const grEl=document.getElementById('devGreeting'); + if(ST.devName){ + const greetings=[ + 'Hey '+ST.devName+' 👋 Schön, dass du dabei bist!', + 'Los geht\'s, '+ST.devName+'! Dein Spiel wartet 🎮', + 'Hi '+ST.devName+'! Du wirst ein tolles Spiel bauen ✨', + 'Willkommen, '+ST.devName+'! Game on! 🚀', + ]; + grEl.textContent=greetings[ST.devName.length%greetings.length]; + grEl.style.opacity='1'; + } else {grEl.style.opacity='0';} + // Vorschau + const show=ST.name.length>0&&ST.devName.length>0; + document.getElementById('s1preview').style.display=show?'':'none'; + if(show){ + document.getElementById('s1pvname').textContent=ST.name; + document.getElementById('s1pvdesc').textContent=ST.desc||'Keine Beschreibung.'; + document.getElementById('s1pvdev').textContent='👨‍💻 Entwickelt von '+ST.devName; + } +} +function suggest(txt){document.getElementById('s1name').value=txt;s1update();} + +/* ══════════════════════════════════════════════ + STEP 2 — GESTALTUNG +══════════════════════════════════════════════ */ +function buildFigureGrid(gridId, current, blocked, onPick){ + const g=document.getElementById(gridId); if(!g) return; + g.innerHTML=''; + FIGURES.forEach(f=>{ + const isBlocked = blocked && blocked===f.id; + const c=document.createElement('div'); + c.className='sel-card'+(current===f.id?' selected':'')+(isBlocked?' disabled':''); + c.innerHTML=`${f.e}
${f.n}
`; + if(isBlocked){ c.title='Bereits vom anderen Spieler gewählt'; } + else { c.onclick=()=>onPick(f.id); } + g.appendChild(c); + }); +} + +function setPlayerCount(n){ + ST.playerCount = n; + if(n===1){ ST.figure2=''; } + setCodeFocus('figure'); + save(); + s2update(); +} + +function s2init(){ + buildFigureGrid('figGrid', ST.figure, ST.figure2, id => { + ST.figure=id; setCodeFocus('figure'); + if(ST.figure2===id) ST.figure2=''; // Konflikt löst sich automatisch + s2update(); + }); + const bg=document.getElementById('bgGrid'); + bg.innerHTML=''; + BACKGROUNDS.forEach(b=>{ + const c=document.createElement('div');c.className='sel-card'+(ST.background===b.id?' selected':''); + c.innerHTML=`${b.e}
${b.n}
`; + c.onclick=()=>{ST.background=b.id;setCodeFocus('bg');s2update();bg.querySelectorAll('.sel-card').forEach(x=>x.classList.remove('selected'));c.classList.add('selected');}; + bg.appendChild(c); + }); + s2update(); +} +function s2update(){ + // Spielmodus-Karten markieren + document.getElementById('pmode1')?.classList.toggle('active', ST.playerCount!==2); + document.getElementById('pmode2')?.classList.toggle('active', ST.playerCount===2); + // Figur-Karten-Titel anpassen + const t = document.getElementById('figCardTitle'); + const s = document.getElementById('figCardSub'); + if(t){ t.textContent = ST.playerCount===2 ? '👤 Spielfigur Spieler 1' : '👤 Spielfigur'; } + if(s){ s.textContent = ST.playerCount===2 ? 'Wer bist du im Spiel?' : 'Das bist du im Spiel!'; } + // P2-Karte ein-/ausblenden + Grid neu bauen (für blocked-State) + const fig2Card = document.getElementById('fig2Card'); + if(fig2Card){ + fig2Card.style.display = ST.playerCount===2 ? '' : 'none'; + if(ST.playerCount===2){ + buildFigureGrid('fig2Grid', ST.figure2, ST.figure, id => { + ST.figure2=id; setCodeFocus('figure'); s2update(); + }); + } + } + // P1-Grid neu bauen (damit Block-State der gegenüberliegenden Figur stimmt) + buildFigureGrid('figGrid', ST.figure, ST.playerCount===2 ? ST.figure2 : '', id => { + ST.figure=id; setCodeFocus('figure'); + if(ST.figure2===id) ST.figure2=''; + s2update(); + }); + // Combo-Preview + const ok = !!(ST.figure&&ST.background) && (ST.playerCount!==2 || (ST.figure2 && ST.figure2!==ST.figure)); + document.getElementById('s2next').disabled=!ok; + const combo=document.getElementById('s2combo'); + if(ST.figure && ST.background){ + const f=FIGURES.find(x=>x.id===ST.figure);const b=BACKGROUNDS.find(x=>x.id===ST.background); + combo.style.display=''; + const f2 = ST.playerCount===2 && ST.figure2 ? FIGURES.find(x=>x.id===ST.figure2) : null; + document.getElementById('s2scene').textContent = b.scene + ' ' + f.e + (f2?' vs '+f2.e:''); + document.getElementById('s2clabel').textContent = f.n + (f2?' & '+f2.n:'') + ' im '+ b.n; + } else { combo.style.display='none'; } +} + +/* ══════════════════════════════════════════════ + STEP 3 — SPIELFELD +══════════════════════════════════════════════ */ +let selGame=null,dragging=null,epFor=null,dragFromIdx=null; +const STORY_MAX=5,MULTI_MAX=3; + +// unlockSection: Markiert eine Sektion als "gesehen" für den Live-Code-Preview +function unlockSection(key){ + if(typeof unlocked !== 'undefined') unlocked.add(key); + if(typeof setCodeFocus === 'function') setCodeFocus(key); +} + +function s3init(){ + unlockSection('movement'); unlockSection('failmode'); unlockSection('fieldcount'); unlockSection('fields'); unlockSection('consequences'); + document.getElementById('cntVal').textContent=ST.fieldCount; + document.getElementById('cntMinus').disabled=ST.fieldCount<=6; + document.getElementById('cntPlus').disabled=ST.fieldCount>=50; + applyRuleUI(); + buildPalette(); + buildBoard(); + updateConsequenceUI(); +} +function selRule(el){ + const g=el.dataset.g; + document.querySelectorAll(`[data-g="${g}"]`).forEach(x=>x.classList.remove('selected')); + el.classList.add('selected');ST.rules[g]=el.dataset.v; + // Map data-g to focus keys + const fkMap={'movement':'movement','fail':'failmode'}; + setCodeFocus(fkMap[g]||g); + if(g==='fail'){ + const sl = document.getElementById('sub_lives'); + const sp = document.getElementById('sub_points'); + if(ST.rules.fail==='lives'){ + sl.classList.add('visible'); sl.style.display='flex'; + sp.classList.remove('visible'); sp.style.display=''; + } else { + sl.classList.remove('visible'); sl.style.display=''; + sp.classList.add('visible'); sp.style.display=''; + } + } + updateConsequenceUI(); +} +function selSub(el){ + const g=el.dataset.g; + document.querySelectorAll(`[data-g="${g}"]`).forEach(x=>x.classList.remove('selected')); + el.classList.add('selected');ST.rules[g]=el.dataset.v; + // lives/pts sub-options + setCodeFocus(g==='lives'?'lives':g==='pts'?'pts':'failmode'); +} + +// ── Konsequenz-Funktionen ────────────────────────────────────────────────── + +function chgLives(delta){ + const cur = parseInt(ST.rules.lives) || 3; + const next = Math.min(5, Math.max(1, cur + delta)); + ST.rules.lives = String(next); + document.getElementById('livesDisplay').textContent = next; + document.getElementById('livesHearts').textContent = '❤️'.repeat(next); + setCodeFocus('lives'); + save(); +} +function selCQ(el,e){ + e&&e.stopPropagation&&e.stopPropagation(); + const side = el.dataset.cq; + const val = el.dataset.cv; + document.querySelectorAll(`[data-cq="${side}"]`) + .forEach(x=>x.classList.remove('selected')); + el.classList.add('selected'); + ST.consequences[side] = val; + setCodeFocus('consequences'); + save(); +} + +function chgCQVal(key, delta, e){ + e && e.stopPropagation(); + const mins = {winVal:0,loseVal:0,winPts:0,losePts:0}; + const maxs = {winVal:6,loseVal:6,winPts:100,losePts:100}; + if(!ST.consequences[key]) ST.consequences[key] = key.includes('Pts') ? 10 : 1; + let v = (ST.consequences[key]||0) + delta; + v = Math.max(mins[key]||0, Math.min(maxs[key]||99, v)); + ST.consequences[key] = v; + const elId = {winVal:'cqWinVal',loseVal:'cqLoseVal',winPts:'cqWinPts',losePts:'cqLosePts'}[key]; + if(elId) document.getElementById(elId).textContent = v; + setCodeFocus('consequences'); + save(); +} + +function updateConsequenceUI(){ + const grid = document.getElementById('consequenceGrid'); + if(!grid) return; + const isDice = ST.rules.movement === 'dice'; + const isLives = ST.rules.fail === 'lives'; + const isPts = ST.rules.fail === 'points'; + // CSS-Klassen für sichtbare Optionen + // Alle Modus-Klassen zuerst entfernen + grid.classList.remove('cq-mode-step','cq-mode-lives','cq-mode-pts'); + // Dann die passenden setzen + if(!isDice) grid.classList.add('cq-mode-step'); + if(isLives) grid.classList.add('cq-mode-lives'); + if(isPts) grid.classList.add('cq-mode-pts'); + // Falls aktuelle Auswahl nicht mehr sichtbar → auf 'nothing' zurücksetzen + ['win','lose'].forEach(side=>{ + const cur = ST.consequences[side]; + const diceOnly = ['forward','back','again','skip']; + const livesOnly = ['life']; + const ptsOnly = ['points']; + let invalid = false; + if(diceOnly.includes(cur) && !isDice) invalid = true; + if(livesOnly.includes(cur) && !isLives) invalid = true; + if(ptsOnly.includes(cur) && !isPts) invalid = true; + if(invalid){ + ST.consequences[side] = 'nothing'; + } + // UI aktualisieren + document.querySelectorAll(`[data-cq="${side}"]`) + .forEach(x=>x.classList.toggle('selected', x.dataset.cv === ST.consequences[side])); + }); + // Werte anzeigen + const c = ST.consequences; + const wv = document.getElementById('cqWinVal'); + const lv = document.getElementById('cqLoseVal'); + const wp = document.getElementById('cqWinPts'); + const lp = document.getElementById('cqLosePts'); + if(wv) wv.textContent = c.winVal ?? 2; + if(lv) lv.textContent = c.loseVal ?? 1; + const ld = document.getElementById('livesDisplay'); + const lh = document.getElementById('livesHearts'); + const ln = parseInt(ST.rules.lives) || 3; + if(ld) ld.textContent = ln; + if(lh) lh.textContent = '❤️'.repeat(ln); + if(wp) wp.textContent = c.winPts ?? 10; + if(lp) lp.textContent = c.losePts ?? 5; +} + +function applyRuleUI(){ + ['movement','fail'].forEach(g=>document.querySelectorAll(`[data-g="${g}"]`).forEach(x=>x.classList.toggle('selected',x.dataset.v===ST.rules[g]))); + document.getElementById('sub_lives').classList.toggle('visible',ST.rules.fail==='lives'); + document.getElementById('sub_points').classList.toggle('visible',ST.rules.fail==='points'); + ['lives','pts'].forEach(g=>document.querySelectorAll(`[data-g="${g}"]`).forEach(x=>x.classList.toggle('selected',x.dataset.v===ST.rules[g]))); + updateConsequenceUI(); +} +function chgCount(d){ + const n=ST.fieldCount+d;if(n<6||n>50)return; + ST.fieldCount=n;setCodeFocus('fieldcount');document.getElementById('cntVal').textContent=n; + document.getElementById('cntMinus').disabled=n<=6;document.getElementById('cntPlus').disabled=n>=50; + while(ST.fields.lengths.fieldIndex>> 0) || 1; + return function () { + s = (s + 0x6D2B79F5) | 0; + let t = s; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; + } + + function hashSeed(str) { + let h = 2166136261; + const s = String(str || ''); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + return (h >>> 0) || 1; + } + + function randomSeed() { return (Math.floor(Math.random() * 0x100000000)) >>> 0; } + + // ── Theme registry (4 Welten) ─────────────────────────────── + const THEMES = { + underwater: { + id: 'underwater', name: 'Unterwasser', emoji: '🌊', + primary: '#22d3ee', accent: '#0ea5e9', + bgGrad: ['#052236', '#083b56', '#0e567a'], + pad: { fill: '#0891b2', edge: '#155e75', glow: 'rgba(34,211,238,0.5)' }, + }, + space: { + id: 'space', name: 'Weltall', emoji: '🌌', + primary: '#a78bfa', accent: '#7c3aed', + bgGrad: ['#050314', '#0f0a2e', '#1e1b4b'], + pad: { fill: '#7c3aed', edge: '#4c1d95', glow: 'rgba(167,139,250,0.55)' }, + }, + haunted: { + id: 'haunted', name: 'Geisterhaus', emoji: '👻', + primary: '#86efac', accent: '#22d3ee', + bgGrad: ['#0a0a0e', '#16162a', '#1a1a2e'], + pad: { fill: '#475569', edge: '#1e293b', glow: 'rgba(134,239,172,0.45)' }, + }, + fantasy: { + id: 'fantasy', name: 'Fantasy', emoji: '🧙', + primary: '#f0abfc', accent: '#c084fc', + bgGrad: ['#1a0a2e', '#3b0764', '#581c87'], + pad: { fill: '#a855f7', edge: '#581c87', glow: 'rgba(240,171,252,0.55)' }, + }, + }; + + // Mapping für entfernte Welten (Migration alter Saves/URLs) + const LEGACY_MAP = { + ocean: 'underwater', jungle: 'fantasy', volcano: 'space', snow: 'space', + city: 'space', candy: 'fantasy', desert: 'fantasy', school: 'fantasy', future: 'space', + }; + + function resolveTheme(id) { return THEMES[id] ? id : (LEGACY_MAP[id] || 'space'); } + + // ── Pad-Platzierung (Snake-Grid + Jitter, normalisierte Koords) ───── + function placePads(rng, fieldCount) { + const aspect = 16 / 9; + const cols = Math.max(2, Math.min(fieldCount, Math.round(Math.sqrt(fieldCount * aspect)))); + const rows = Math.ceil(fieldCount / cols); + const marginX = 0.07, marginY = 0.13; // Platz oben für Header + const usableW = 1 - 2 * marginX; + const usableH = 1 - marginY - 0.10; + const cellW = usableW / cols; + const cellH = usableH / rows; + + const pads = []; + for (let i = 0; i < fieldCount; i++) { + const r = Math.floor(i / cols); + const c = r % 2 === 0 ? i % cols : (cols - 1) - (i % cols); + const cx = marginX + (c + 0.5) * cellW; + const cy = marginY + (r + 0.5) * cellH; + const jx = (rng() - 0.5) * cellW * 0.55; + const jy = (rng() - 0.5) * cellH * 0.45; + const ux = Math.max(marginX, Math.min(1 - marginX, cx + jx)); + const uy = Math.max(marginY, Math.min(1 - 0.06, cy + jy)); + pads.push({ ux, uy }); + } + return pads; + } + + // Catmull-Rom-Spline durch Pads (normalized space) + function spline(pads, samplesPerSeg) { + const out = []; + const n = pads.length; + if (n < 2) return pads.slice(); + for (let i = 0; i < n - 1; i++) { + const p0 = pads[Math.max(0, i - 1)]; + const p1 = pads[i]; + const p2 = pads[i + 1]; + const p3 = pads[Math.min(n - 1, i + 2)]; + for (let s = 0; s < samplesPerSeg; s++) { + const t = s / samplesPerSeg, t2 = t * t, t3 = t2 * t; + const ux = 0.5 * ((2 * p1.ux) + (-p0.ux + p2.ux) * t + (2 * p0.ux - 5 * p1.ux + 4 * p2.ux - p3.ux) * t2 + (-p0.ux + 3 * p1.ux - 3 * p2.ux + p3.ux) * t3); + const uy = 0.5 * ((2 * p1.uy) + (-p0.uy + p2.uy) * t + (2 * p0.uy - 5 * p1.uy + 4 * p2.uy - p3.uy) * t2 + (-p0.uy + 3 * p1.uy - 3 * p2.uy + p3.uy) * t3); + out.push({ ux, uy }); + } + } + out.push({ ux: pads[n - 1].ux, uy: pads[n - 1].uy }); + return out; + } + + // ── Theme-spezifische Dekoration deterministisch generieren ──── + function makeDecor(rng, themeId, fieldCount) { + const d = {}; + if (themeId === 'underwater') { + d.bubbles = []; for (let i = 0; i < 30; i++) d.bubbles.push({ ux: rng(), uy: rng(), r: 2 + rng() * 5, sp: 0.04 + rng() * 0.10, ph: rng() * 6.28 }); + d.grass = []; for (let i = 0; i < 14; i++) d.grass.push({ ux: rng(), h: 0.10 + rng() * 0.22, sw: 1.5 + rng() * 2.5, ph: rng() * 6.28 }); + d.rocks = []; for (let i = 0; i < 8; i++) d.rocks.push({ ux: rng(), w: 0.04 + rng() * 0.08, h: 0.05 + rng() * 0.08, col: rng() }); + d.rays = []; for (let i = 0; i < 5; i++) d.rays.push({ x: rng(), w: 60 + rng() * 80, sk: rng(), ph: rng() * 6.28 }); + } + else if (themeId === 'space') { + d.stars = []; for (let i = 0; i < 90; i++) d.stars.push({ ux: rng(), uy: rng(), r: 0.5 + rng() * 1.8, sp: 0.5 + rng() * 1.8, ph: rng() * 6.28 }); + d.planets = []; for (let i = 0; i < 3; i++) d.planets.push({ ux: 0.1 + rng() * 0.8, uy: rng() * 0.5, r: 0.03 + rng() * 0.045, hue: rng() * 360 }); + d.nebula = []; for (let i = 0; i < 2; i++) d.nebula.push({ ux: rng(), uy: rng() * 0.6, r: 0.18 + rng() * 0.12, hue: 250 + rng() * 80 }); + } + else if (themeId === 'haunted') { + d.trees = []; for (let i = 0; i < 7; i++) d.trees.push({ ux: rng(), h: 0.30 + rng() * 0.30, lean: (rng() - 0.5) * 0.4 }); + d.graves = []; for (let i = 0; i < 8; i++) d.graves.push({ ux: rng(), w: 0.03 + rng() * 0.04, h: 0.04 + rng() * 0.05 }); + d.wisps = []; for (let i = 0; i < 14; i++) d.wisps.push({ ux: rng(), uy: rng(), r: 4 + rng() * 5, sp: 0.02 + rng() * 0.05, ph: rng() * 6.28 }); + d.moonX = 0.78 + rng() * 0.12; + } + else if (themeId === 'fantasy') { + d.trees = []; for (let i = 0; i < 9; i++) d.trees.push({ ux: rng(), h: 0.25 + rng() * 0.30 }); + d.flies = []; for (let i = 0; i < 40; i++) d.flies.push({ ux: rng(), uy: rng(), r: 1.5 + rng() * 2, sp: 0.6 + rng() * 1.2, ph: rng() * 6.28 }); + d.runes = []; for (let i = 0; i < 5; i++) d.runes.push({ ux: 0.05 + rng() * 0.9, uy: 0.1 + rng() * 0.5, r: 0.018 + rng() * 0.020, ph: rng() * 6.28 }); + } + return d; + } + + // ── generate ──────────────────────────────────────────────── + function generate(themeIdRaw, seed, fieldCount) { + const themeId = resolveTheme(themeIdRaw); + const s = ((seed >>> 0) || 1) >>> 0; + const rng = mulberry32(s); + const pads = placePads(rng, fieldCount); + const path = spline(pads, 22); + const decor = makeDecor(rng, themeId, fieldCount); + return { theme: themeId, seed: s, fieldCount, pads, path, decor }; + } + + // ── Hilfen ────────────────────────────────────────────────── + function padR(W, H) { return Math.min(W, H) * 0.052; } + function padCenter(world, i, W, H) { const p = world.pads[i]; return { x: p.ux * W, y: p.uy * H }; } + + function hitTestPad(world, x, y, W, H) { + const r = padR(W, H) * 1.25; // etwas größer für Touch + for (let i = 0; i < world.pads.length; i++) { + const p = world.pads[i], px = p.ux * W, py = p.uy * H; + if ((x - px) * (x - px) + (y - py) * (y - py) < r * r) return i; + } + return -1; + } + + // ── Background-Dekoration zeichnen ────────────────────────── + function drawBgDecor(ctx, world, theme, state, t) { + const W = state.W, H = state.H, tt = t * 0.001; + + if (theme.id === 'underwater') { + // Caustic-Lichtstrahlen + ctx.save(); ctx.globalCompositeOperation = 'lighter'; + world.decor.rays.forEach(ry => { + const x = ((ry.x + Math.sin(tt * 0.2 + ry.ph) * 0.03) % 1 + 1) % 1 * W; + const g = ctx.createLinearGradient(x, 0, x + ry.w * 0.6, H); + g.addColorStop(0, 'rgba(125,211,252,0.10)'); + g.addColorStop(1, 'rgba(125,211,252,0)'); + ctx.fillStyle = g; + ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x + ry.w, 0); ctx.lineTo(x + ry.w * 1.2 + 60, H); ctx.lineTo(x + 40, H); ctx.closePath(); ctx.fill(); + }); + ctx.restore(); + // Felsen + world.decor.rocks.forEach(r => { + const x = r.ux * W, w = r.w * W, h = r.h * H; + ctx.fillStyle = r.col > 0.5 ? '#0e7490' : '#155e75'; + ctx.beginPath(); ctx.ellipse(x, H - h * 0.3, w, h * 0.7, 0, Math.PI, 2 * Math.PI); ctx.fill(); + }); + // Seetang + world.decor.grass.forEach(gr => { + const x = gr.ux * W, sway = Math.sin(tt * 1.4 + gr.ph) * 14; + ctx.strokeStyle = 'rgba(20,184,166,0.65)'; ctx.lineWidth = gr.sw; ctx.lineCap = 'round'; + ctx.beginPath(); ctx.moveTo(x, H); + ctx.quadraticCurveTo(x + sway * 0.3, H - gr.h * H * 0.5, x + sway, H - gr.h * H); + ctx.stroke(); + }); + // Blasen + world.decor.bubbles.forEach(b => { + const x = b.ux * W + Math.sin(tt + b.ph) * 12; + const y = ((b.uy - tt * b.sp) % 1 + 1) % 1 * H; + ctx.globalAlpha = 0.35 + 0.35 * Math.sin(tt * 2 + b.ph); + ctx.fillStyle = 'rgba(186,230,253,0.6)'; + ctx.beginPath(); ctx.arc(x, y, b.r, 0, Math.PI * 2); ctx.fill(); + ctx.strokeStyle = 'rgba(255,255,255,0.4)'; ctx.lineWidth = 0.5; ctx.stroke(); + }); + ctx.globalAlpha = 1; + } + else if (theme.id === 'space') { + // Nebel + world.decor.nebula.forEach(n => { + const x = n.ux * W, y = n.uy * H, r = n.r * Math.min(W, H); + const g = ctx.createRadialGradient(x, y, 0, x, y, r); + g.addColorStop(0, `hsla(${n.hue},80%,60%,0.20)`); + g.addColorStop(1, `hsla(${n.hue},80%,40%,0)`); + ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); + }); + // Sterne + world.decor.stars.forEach(s => { + const x = s.ux * W, y = s.uy * H; + ctx.globalAlpha = 0.4 + 0.5 * Math.sin(tt * s.sp + s.ph); + ctx.fillStyle = '#cdd6ff'; + ctx.fillRect(x, y, s.r, s.r); + }); + ctx.globalAlpha = 1; + // Planeten + world.decor.planets.forEach(p => { + const x = p.ux * W, y = p.uy * H, r = p.r * Math.min(W, H); + const g = ctx.createRadialGradient(x - r * 0.3, y - r * 0.3, 0, x, y, r); + g.addColorStop(0, `hsla(${p.hue},70%,65%,0.55)`); + g.addColorStop(1, `hsla(${p.hue},70%,30%,0.05)`); + ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); + }); + } + else if (theme.id === 'haunted') { + // Mond + const mx = world.decor.moonX * W, my = H * 0.16, mr = Math.min(W, H) * 0.06; + ctx.fillStyle = 'rgba(220,230,200,0.18)'; + ctx.beginPath(); ctx.arc(mx, my, mr * 2.2, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = 'rgba(220,230,200,0.92)'; + ctx.beginPath(); ctx.arc(mx, my, mr, 0, Math.PI * 2); ctx.fill(); + // Bäume + ctx.fillStyle = 'rgba(5,5,12,0.85)'; + world.decor.trees.forEach(tr => { + const x = tr.ux * W, h = tr.h * H; + ctx.beginPath(); ctx.moveTo(x - 8, H); + ctx.quadraticCurveTo(x + tr.lean * 30, H - h * 0.55, x + tr.lean * 50, H - h); + ctx.lineTo(x + tr.lean * 50 + 4, H - h * 0.97); + ctx.lineTo(x + 8, H); ctx.closePath(); ctx.fill(); + }); + // Grabsteine + world.decor.graves.forEach(g => { + const x = g.ux * W, w = g.w * W, h = g.h * H; + ctx.fillStyle = 'rgba(60,60,75,0.85)'; ctx.fillRect(x - w / 2, H - h, w, h); + ctx.beginPath(); ctx.arc(x, H - h, w / 2, Math.PI, 2 * Math.PI); ctx.fill(); + }); + // Wisps + world.decor.wisps.forEach(b => { + const x = b.ux * W + Math.sin(tt + b.ph) * 30; + const y = ((b.uy - tt * b.sp) % 1 + 1) % 1 * H; + const a = 0.3 + 0.35 * Math.sin(tt * 2 + b.ph); + const g2 = ctx.createRadialGradient(x, y, 0, x, y, b.r * 3.5); + g2.addColorStop(0, `rgba(134,239,172,${a * 0.7})`); + g2.addColorStop(1, 'rgba(134,239,172,0)'); + ctx.fillStyle = g2; + ctx.beginPath(); ctx.arc(x, y, b.r * 3.5, 0, Math.PI * 2); ctx.fill(); + }); + } + else if (theme.id === 'fantasy') { + // Doppelmond + ctx.fillStyle = 'rgba(240,171,252,0.85)'; + ctx.beginPath(); ctx.arc(W * 0.78, H * 0.14, Math.min(W, H) * 0.045, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = 'rgba(240,171,252,0.6)'; + ctx.beginPath(); ctx.arc(W * 0.87, H * 0.22, Math.min(W, H) * 0.025, 0, Math.PI * 2); ctx.fill(); + // Wald + ctx.fillStyle = 'rgba(15,5,30,0.85)'; + world.decor.trees.forEach(tr => { + const x = tr.ux * W, h = tr.h * H, w = h * 0.45; + ctx.beginPath(); ctx.moveTo(x - w / 2, H); ctx.lineTo(x, H - h); ctx.lineTo(x + w / 2, H); ctx.closePath(); ctx.fill(); + }); + // Glühwürmchen + world.decor.flies.forEach(b => { + const x = b.ux * W + Math.cos(tt * b.sp + b.ph) * 20; + const y = b.uy * H + Math.sin(tt * b.sp * 1.2 + b.ph) * 12; + const a = 0.4 + 0.55 * Math.sin(tt * 3 + b.ph); + const g = ctx.createRadialGradient(x, y, 0, x, y, b.r * 4); + g.addColorStop(0, `rgba(253,224,71,${a})`); + g.addColorStop(1, 'rgba(253,224,71,0)'); + ctx.fillStyle = g; + ctx.beginPath(); ctx.arc(x, y, b.r * 4, 0, Math.PI * 2); ctx.fill(); + }); + // Runen + world.decor.runes.forEach(r => { + const x = r.ux * W, y = r.uy * H + Math.sin(tt + r.ph) * 8; + const rr = r.r * Math.min(W, H); + ctx.save(); ctx.translate(x, y); ctx.rotate(tt * 0.4 + r.ph); + ctx.strokeStyle = 'rgba(240,171,252,0.65)'; ctx.lineWidth = 1.5; + ctx.strokeRect(-rr, -rr, rr * 2, rr * 2); + ctx.beginPath(); ctx.moveTo(-rr * 0.5, 0); ctx.lineTo(rr * 0.5, 0); ctx.moveTo(0, -rr * 0.5); ctx.lineTo(0, rr * 0.5); ctx.stroke(); + ctx.restore(); + }); + } + } + + // ── Pfad zeichnen ─────────────────────────────────────────── + function drawPath(ctx, world, theme, state, t) { + if (world.path.length < 2) return; + const W = state.W, H = state.H; + // Outer glow + ctx.lineCap = 'round'; ctx.lineJoin = 'round'; + ctx.strokeStyle = theme.pad.glow; + ctx.lineWidth = 16; + ctx.beginPath(); + ctx.moveTo(world.path[0].ux * W, world.path[0].uy * H); + for (let i = 1; i < world.path.length; i++) ctx.lineTo(world.path[i].ux * W, world.path[i].uy * H); + ctx.stroke(); + // Inner line + ctx.strokeStyle = theme.primary; ctx.lineWidth = 5; + ctx.beginPath(); + ctx.moveTo(world.path[0].ux * W, world.path[0].uy * H); + for (let i = 1; i < world.path.length; i++) ctx.lineTo(world.path[i].ux * W, world.path[i].uy * H); + ctx.stroke(); + // Animierte fließende Lichtpunkte + const tt = t * 0.0007; + for (let d = 0; d < 8; d++) { + const frac = ((tt + d / 8) % 1 + 1) % 1; + const idx = Math.min(world.path.length - 1, Math.floor(frac * (world.path.length - 1))); + const p = world.path[idx]; + ctx.fillStyle = '#fff'; ctx.globalAlpha = 0.55; + ctx.beginPath(); ctx.arc(p.ux * W, p.uy * H, 2.6, 0, Math.PI * 2); ctx.fill(); + } + ctx.globalAlpha = 1; + } + + // ── Pads zeichnen ─────────────────────────────────────────── + function drawPads(ctx, world, theme, state, t) { + const W = state.W, H = state.H, R = padR(W, H), tt = t * 0.001; + const MG_ICONS = { snake: '🐍', flappy: '🐦', memory: '🃏', quiz: '❓', reaction: '⚡', basketball: '🏀', catch: '🍎', maze: '🌀', simon: '🔴', puzzle: '🧩', spotdiff: '🔍', typing: '⌨️' }; + + for (let i = 0; i < world.pads.length; i++) { + const p = world.pads[i], x = p.ux * W, y = p.uy * H; + const isStart = i === 0, isEnd = i === world.pads.length - 1; + const isActive = state.pos === i; + const isVisited = state.visited && state.visited.has(i); + const isHover = state.hover === i; + const gameId = state.fields ? state.fields[i] : null; + + ctx.save(); + + // Glow ring + if (isActive) { + const pulse = 0.6 + 0.4 * Math.sin(tt * 4); + const g = ctx.createRadialGradient(x, y, R * 0.9, x, y, R * 1.9); + g.addColorStop(0, theme.pad.glow); g.addColorStop(1, 'transparent'); + ctx.fillStyle = g; + ctx.beginPath(); ctx.arc(x, y, R * 1.9 * pulse, 0, Math.PI * 2); ctx.fill(); + } else if (isHover) { + const g = ctx.createRadialGradient(x, y, R * 0.9, x, y, R * 1.6); + g.addColorStop(0, 'rgba(255,255,255,0.25)'); g.addColorStop(1, 'transparent'); + ctx.fillStyle = g; + ctx.beginPath(); ctx.arc(x, y, R * 1.6, 0, Math.PI * 2); ctx.fill(); + } + + // Pad fill + const grd = ctx.createRadialGradient(x - R * 0.35, y - R * 0.35, 0, x, y, R); + if (isStart) { grd.addColorStop(0, '#6ee7b7'); grd.addColorStop(1, '#065f46'); } + else if (isEnd) { grd.addColorStop(0, '#fde68a'); grd.addColorStop(1, '#78350f'); } + else if (gameId) { grd.addColorStop(0, theme.primary); grd.addColorStop(1, theme.pad.edge); } + else { grd.addColorStop(0, theme.pad.fill); grd.addColorStop(1, theme.pad.edge); } + ctx.fillStyle = grd; + + if (theme.id === 'haunted' && !isStart && !isEnd) { + // Grabstein-Form + ctx.beginPath(); + ctx.moveTo(x - R * 0.85, y + R * 0.95); + ctx.lineTo(x - R * 0.85, y - R * 0.35); + ctx.quadraticCurveTo(x - R * 0.85, y - R * 1.0, x, y - R * 1.0); + ctx.quadraticCurveTo(x + R * 0.85, y - R * 1.0, x + R * 0.85, y - R * 0.35); + ctx.lineTo(x + R * 0.85, y + R * 0.95); + ctx.closePath(); ctx.fill(); + } else { + ctx.beginPath(); ctx.arc(x, y, R, 0, Math.PI * 2); ctx.fill(); + } + + // Rim + ctx.strokeStyle = isActive ? '#fff' : 'rgba(255,255,255,0.35)'; + ctx.lineWidth = isActive ? 2.5 : 1.2; + ctx.beginPath(); ctx.arc(x, y, R, 0, Math.PI * 2); ctx.stroke(); + + // Glas-Highlight + ctx.beginPath(); + ctx.arc(x - R * 0.32, y - R * 0.32, R * 0.45, Math.PI * 1.05, Math.PI * 1.9); + ctx.lineWidth = 2.5; ctx.strokeStyle = 'rgba(255,255,255,0.35)'; + ctx.stroke(); + + ctx.restore(); + + // Icon + Nummer + const icon = isStart ? '▶' : isEnd ? '🏁' : gameId ? (MG_ICONS[gameId] || '🎮') : ''; + ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + if (icon) { + ctx.font = `bold ${Math.round(R * 0.9)}px ${gameId ? 'serif' : 'Nunito,sans-serif'}`; + ctx.fillStyle = '#fff'; + ctx.fillText(icon, x, y); + } else if (!isStart && !isEnd) { + ctx.font = `bold ${Math.round(R * 0.7)}px Nunito,sans-serif`; + ctx.fillStyle = 'rgba(255,255,255,0.85)'; + ctx.fillText(String(i), x, y); + } + + // Visited check + if (isVisited && !isActive && i > 0) { + ctx.font = `bold ${Math.round(R * 0.5)}px Nunito,sans-serif`; + ctx.fillStyle = '#86efac'; + ctx.fillText('✓', x + R * 0.78, y - R * 0.78); + } + // Story-Badge + const hasStory = state.storyItems && state.storyItems.some(s => s.fieldIndex === i); + if (hasStory) { + ctx.font = `${Math.round(R * 0.5)}px serif`; + ctx.fillText('📖', x - R * 0.8, y - R * 0.8); + } + } + } + + function drawFigure(ctx, world, theme, state, t) { + const W = state.W, H = state.H, R = padR(W, H); + // Neue Multi-Figuren-API: state.figures [{emoji,x,y,active,dimmed}] + // Backwards-Compat: alte Felder state.figEmoji/figX/figY → in 1-Element-Array umwandeln + let figs = state.figures; + if (!figs || !figs.length) { + if (state.figEmoji == null || state.figX == null) return; + figs = [{ emoji: state.figEmoji, x: state.figX, y: state.figY, active: true, dimmed: false }]; + } + // Same-Pad-Offset: wenn zwei Figuren ~auf gleicher Pixel-Position sind, leicht versetzen + const fsBase = R * 1.5; + const pts = figs.map(f => ({ ...f, dx: 0, dy: 0 })); + if (pts.length >= 2) { + for (let i = 0; i < pts.length; i++) { + for (let j = i+1; j < pts.length; j++) { + const dx = pts[j].x - pts[i].x, dy = pts[j].y - pts[i].y; + if (dx*dx + dy*dy < (R*0.6)*(R*0.6)) { + pts[i].dx -= R * 0.55; pts[j].dx += R * 0.55; + } + } + } + } + // Inaktive zuerst zeichnen (damit aktiver oben liegt) + pts.sort((a, b) => (a.active === b.active ? 0 : (a.active ? 1 : -1))); + pts.forEach(p => { + const fx = p.x + p.dx, fy = p.y + p.dy; + const active = !!p.active && !p.dimmed; + const fs = active ? fsBase : fsBase * 0.88; + const bounce = active ? Math.sin(t * 0.005) * 3 : 0; + // Aura + const g = ctx.createRadialGradient(fx, fy, 0, fx, fy, fs * 1.1); + g.addColorStop(0, p.dimmed ? 'rgba(100,100,100,0.25)' : theme.pad.glow); g.addColorStop(1, 'transparent'); + ctx.fillStyle = g; + ctx.beginPath(); ctx.arc(fx, fy, fs * 1.1, 0, Math.PI * 2); ctx.fill(); + // Emoji + ctx.font = `${Math.round(fs)}px serif`; + ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + if (active) { ctx.shadowColor = theme.primary; ctx.shadowBlur = 15; } + ctx.globalAlpha = p.dimmed ? 0.45 : 1; + ctx.fillText(p.emoji || '🎮', fx, fy - R * 0.05 + bounce); + ctx.shadowBlur = 0; ctx.globalAlpha = 1; + }); + } + + function drawHeader(ctx, state) { + if (!state.gameName) return; + const W = state.W, H = state.H; + ctx.textAlign = 'center'; + ctx.font = `bold ${Math.round(Math.min(W, H) * 0.038)}px 'Fredoka One',cursive`; + ctx.fillStyle = 'rgba(255,255,255,0.95)'; + ctx.shadowColor = 'rgba(0,0,0,0.8)'; ctx.shadowBlur = 14; + ctx.fillText(String(state.gameName).toUpperCase(), W / 2, H * 0.055); + if (state.devName) { + ctx.font = `${Math.round(Math.min(W, H) * 0.018)}px Nunito,sans-serif`; + ctx.fillStyle = 'rgba(255,255,255,0.65)'; + ctx.fillText('von ' + state.devName, W / 2, H * 0.085); + } + ctx.shadowBlur = 0; + } + + // ── Vordergrund: passierende Bewohner (Schwärme, Wal, Komet, Drache, Fledermaus, Blitz) ── + function drawFish(ctx, x, y, s, color) { + ctx.save(); ctx.translate(x, y); ctx.fillStyle = color; + ctx.beginPath(); ctx.ellipse(0, 0, s, s * 0.55, 0, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); ctx.moveTo(-s * 0.7, 0); ctx.lineTo(-s * 1.5, -s * 0.6); ctx.lineTo(-s * 1.5, s * 0.6); ctx.closePath(); ctx.fill(); + ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(s * 0.4, -s * 0.1, s * 0.16, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(s * 0.42, -s * 0.1, s * 0.08, 0, Math.PI * 2); ctx.fill(); + ctx.restore(); + } + function drawFishLayer(ctx, tt, W, H, color, count, yFrac, speed, size, bob) { + const cycle = W + 240; + for (let i = 0; i < count; i++) { + const x = ((tt * speed * 60 + i * (cycle / count)) % cycle) - 120; + const y = yFrac * H + Math.sin(tt * 1.4 + i * 0.7) * bob; + drawFish(ctx, x, y, size, color); + } + } + function drawWhale(ctx, W, H, c) { + const x = -260 + c * (W + 520); + const y = H * 0.42 + Math.sin(c * Math.PI * 2) * 22; + ctx.save(); ctx.translate(x, y); ctx.fillStyle = 'rgba(20,40,68,0.55)'; + ctx.beginPath(); ctx.ellipse(0, 0, 110, 36, 0, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); ctx.moveTo(-100, 0); ctx.lineTo(-140, -30); ctx.lineTo(-140, 30); ctx.closePath(); ctx.fill(); + ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.beginPath(); ctx.arc(55, -6, 4, 0, Math.PI * 2); ctx.fill(); + ctx.restore(); + } + function drawShootingStar(ctx, W, H, c, idx) { + // Position bewegt sich von oben-rechts nach unten-links → Kopf führt, Schweif trailt + const sx = W * (1.15 - c * 1.4); + const sy = H * (0.08 + idx * 0.08 + c * 0.55); + const len = 70 + 50 * Math.sin(c * Math.PI); + ctx.save(); ctx.translate(sx, sy); ctx.rotate(-0.45 - idx * 0.12); + // Kopf (hell) am Ursprung = aktuelle Position, Schweif erstreckt sich in +x = entgegen Flugrichtung + const g = ctx.createLinearGradient(0, 0, len, 0); + g.addColorStop(0, 'rgba(255,255,255,0.95)'); + g.addColorStop(1, 'rgba(255,255,255,0)'); + ctx.strokeStyle = g; ctx.lineWidth = 2; + ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(len, 0); ctx.stroke(); + ctx.fillStyle = '#fff'; ctx.shadowColor = '#fff'; ctx.shadowBlur = 8; + ctx.beginPath(); ctx.arc(0, 0, 3, 0, Math.PI * 2); ctx.fill(); + ctx.shadowBlur = 0; + ctx.restore(); + } + + function drawUFO(ctx, W, H, c, theme) { + // Sanft auf-und-ab fliegend, von links nach rechts + const x = -60 + c * (W + 120); + const y = H * 0.28 + Math.sin(c * Math.PI * 5) * 18; + ctx.save(); ctx.translate(x, y); + // Beam unter dem UFO + const beamA = 0.10 + 0.10 * Math.sin(c * Math.PI * 12); + ctx.fillStyle = `rgba(160,255,255,${beamA})`; + ctx.beginPath(); ctx.moveTo(-6, 5); ctx.lineTo(-20, 38); ctx.lineTo(20, 38); ctx.lineTo(6, 5); ctx.closePath(); ctx.fill(); + // Untertasse + const dish = ctx.createLinearGradient(0, -2, 0, 8); + dish.addColorStop(0, '#666c80'); dish.addColorStop(1, '#1f2433'); + ctx.fillStyle = dish; + ctx.beginPath(); ctx.ellipse(0, 2, 26, 8, 0, 0, Math.PI * 2); ctx.fill(); + // Glas-Dome + const dome = ctx.createRadialGradient(-3, -7, 1, 0, -3, 14); + dome.addColorStop(0, 'rgba(180,235,255,0.95)'); dome.addColorStop(1, 'rgba(80,150,200,0.7)'); + ctx.fillStyle = dome; + ctx.beginPath(); ctx.ellipse(0, -3, 13, 9, 0, Math.PI, 2 * Math.PI); ctx.fill(); + // Highlight im Dome + ctx.fillStyle = 'rgba(255,255,255,0.5)'; + ctx.beginPath(); ctx.ellipse(-4, -6, 3, 2, 0, 0, Math.PI * 2); ctx.fill(); + // Blinkende Lichter unten + for (let i = -2; i <= 2; i++) { + const on = Math.sin(c * Math.PI * 22 + i * 1.2) > 0; + ctx.fillStyle = on ? '#ffd166' : 'rgba(255,209,102,0.35)'; + ctx.shadowColor = '#ffd166'; ctx.shadowBlur = on ? 6 : 0; + ctx.beginPath(); ctx.arc(i * 7, 8, 1.8, 0, Math.PI * 2); ctx.fill(); + } + ctx.shadowBlur = 0; + ctx.restore(); + } + + function drawGhost(ctx, W, H, c, tt) { + // Schwebt in Wellenform durchs Bild + const x = -50 + c * (W + 100); + const y = H * 0.32 + Math.sin(c * Math.PI * 3) * 30; + ctx.save(); ctx.translate(x, y); + ctx.shadowColor = 'rgba(220,230,245,0.7)'; ctx.shadowBlur = 22; + ctx.fillStyle = 'rgba(230,235,245,0.88)'; + // Körper: runde Oberseite, geschwungene Unterkante + ctx.beginPath(); + ctx.arc(0, -8, 22, Math.PI, 2 * Math.PI); + ctx.lineTo(22, 16); + for (let i = 4; i >= -4; i--) { + const wx = (i / 4) * 22; + const wy = 16 + Math.sin(tt * 4 + i * 0.7 + c * 6) * 5; + ctx.lineTo(wx, wy); + } + ctx.lineTo(-22, 16); + ctx.closePath(); ctx.fill(); + ctx.shadowBlur = 0; + // Augen + ctx.fillStyle = '#0c0c14'; + ctx.beginPath(); ctx.ellipse(-7, -8, 3, 4, 0, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); ctx.ellipse(7, -8, 3, 4, 0, 0, Math.PI * 2); ctx.fill(); + // Mund (Oval) + ctx.beginPath(); ctx.ellipse(0, 2, 3.5, 5, 0, 0, Math.PI * 2); ctx.fill(); + ctx.restore(); + } + function drawComet(ctx, W, H, c, theme) { + const x = W * (1.1 - c * 1.3), y = H * (0.12 + c * 0.5); + ctx.save(); ctx.translate(x, y); + const g = ctx.createLinearGradient(0, 0, 70, 0); + g.addColorStop(0, theme.primary); g.addColorStop(1, 'transparent'); + ctx.fillStyle = g; + ctx.beginPath(); ctx.moveTo(0, -7); ctx.lineTo(70, 0); ctx.lineTo(0, 7); ctx.closePath(); ctx.fill(); + ctx.shadowColor = theme.primary; ctx.shadowBlur = 14; + ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI * 2); ctx.fill(); + ctx.shadowBlur = 0; ctx.restore(); + } + function drawBat(ctx, W, H, c, idx, tt) { + const x = c * (W + 120) - 60; + const y = H * (0.18 + idx * 0.08) + Math.sin(tt * 5 + idx) * 14; + const flap = Math.sin(tt * 14) * 0.4 + 0.7; + ctx.save(); ctx.translate(x, y); ctx.fillStyle = 'rgba(10,10,18,0.92)'; + ctx.beginPath(); ctx.ellipse(0, 0, 5, 4, 0, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(-12, -9 * flap, -20, -2); ctx.quadraticCurveTo(-10, 4, 0, 2); ctx.fill(); + ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(12, -9 * flap, 20, -2); ctx.quadraticCurveTo(10, 4, 0, 2); ctx.fill(); + ctx.restore(); + } + function drawMist(ctx, W, H, tt) { + for (let i = 0; i < 3; i++) { + const x = ((tt * (10 + i * 6) + i * 240) % (W + 480)) - 240; + const y = H - 50 - i * 10; + const g = ctx.createRadialGradient(x, y, 0, x, y, 220); + g.addColorStop(0, 'rgba(200,200,225,0.15)'); g.addColorStop(1, 'transparent'); + ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, 220, 0, Math.PI * 2); ctx.fill(); + } + } + function drawLeaves(ctx, W, H, tt) { + for (let i = 0; i < 7; i++) { + const x = ((tt * 22 + i * 240) % (W + 200)) - 100; + const y = ((tt * 14 + i * 130) % (H + 80)) - 40; + const rot = tt * 1.4 + i; + ctx.save(); ctx.translate(x, y); ctx.rotate(rot); + ctx.fillStyle = `hsla(${280 + i * 8},65%,55%,0.55)`; + ctx.beginPath(); ctx.ellipse(0, 0, 7, 3.5, 0, 0, Math.PI * 2); ctx.fill(); + ctx.restore(); + } + } + function drawDragon(ctx, W, H, c, theme) { + const x = -140 + c * (W + 320); + const y = H * 0.16 + Math.sin(c * Math.PI * 3) * 30; + ctx.save(); ctx.translate(x, y); + ctx.shadowColor = theme.primary; ctx.shadowBlur = 18; + ctx.fillStyle = 'rgba(30,10,40,0.92)'; + // S-Body + ctx.beginPath(); + ctx.moveTo(0, 0); ctx.quadraticCurveTo(-30, -16, -60, 0); + ctx.quadraticCurveTo(-90, 16, -120, 4); ctx.lineTo(-120, 12); + ctx.quadraticCurveTo(-90, 24, -60, 14); ctx.quadraticCurveTo(-30, 10, 0, 14); + ctx.closePath(); ctx.fill(); + // Kopf + ctx.beginPath(); ctx.ellipse(10, 8, 13, 9, 0, 0, Math.PI * 2); ctx.fill(); + // Flügel + const wing = Math.sin(c * Math.PI * 16) * 14; + ctx.beginPath(); ctx.moveTo(-22, 6); ctx.quadraticCurveTo(-42, -22 - wing, -60, -10); ctx.lineTo(-30, 4); ctx.fill(); + ctx.beginPath(); ctx.moveTo(-50, 6); ctx.quadraticCurveTo(-72, -28 - wing, -86, -8); ctx.lineTo(-58, 4); ctx.fill(); + ctx.shadowBlur = 0; + ctx.fillStyle = theme.primary; + ctx.beginPath(); ctx.arc(17, 6, 2.4, 0, Math.PI * 2); ctx.fill(); + ctx.restore(); + } + + function drawForeground(ctx, world, theme, state, t) { + const W = state.W, H = state.H, tt = t * 0.001; + if (theme.id === 'underwater') { + drawFishLayer(ctx, tt, W, H, 'rgba(252,211,77,0.85)', 6, 0.22, 0.5, 11, 7); + drawFishLayer(ctx, tt, W, H, 'rgba(125,211,252,0.7)', 5, 0.42, 0.7, 9, 5); + drawFishLayer(ctx, tt, W, H, 'rgba(74,222,128,0.7)', 7, 0.62, 0.4, 8, 6); + const wc = (tt % 22) / 14; if (wc <= 1) drawWhale(ctx, W, H, wc); + } + else if (theme.id === 'space') { + for (let i = 0; i < 3; i++) { + const c = ((tt + i * 4.3) % 11) / 3.5; + if (c <= 1) drawShootingStar(ctx, W, H, c, i); + } + const cc = (tt % 28) / 18; if (cc <= 1) drawComet(ctx, W, H, cc, theme); + // UFO sporadisch (alle ~36 s, fliegt ~14 s) + const uc = (tt % 36) / 14; if (uc <= 1) drawUFO(ctx, W, H, uc, theme); + } + else if (theme.id === 'haunted') { + // Blitz-Flicker: kurz, alle ~9 s + const flickerPhase = (tt * 0.5) % 9; + if (flickerPhase < 0.15) { + ctx.fillStyle = `rgba(200,210,230,${(0.15 - flickerPhase) * 1.8})`; + ctx.fillRect(0, 0, W, H); + } + drawMist(ctx, W, H, tt); + for (let i = 0; i < 2; i++) { + const c = ((tt + i * 6.5) % 12) / 7.5; + if (c <= 1) drawBat(ctx, W, H, c, i, tt); + } + // Gespenst sporadisch (alle ~28 s, schwebt ~16 s durchs Bild) + const gc = (tt % 28) / 16; if (gc <= 1) drawGhost(ctx, W, H, gc, tt); + } + else if (theme.id === 'fantasy') { + drawLeaves(ctx, W, H, tt); + // Drache sporadisch (alle ~38 s, fliegt ~17 s) — wirkt seltener/zufälliger + const dc = (tt % 38) / 17; if (dc <= 1) drawDragon(ctx, W, H, dc, theme); + } + } + + // ── render() ──────────────────────────────────────────────── + function render(ctx, world, state, t) { + const W = state.W, H = state.H; + const theme = THEMES[world.theme]; + // Background gradient + const bg = ctx.createLinearGradient(0, 0, 0, H); + bg.addColorStop(0, theme.bgGrad[0]); bg.addColorStop(0.55, theme.bgGrad[1]); bg.addColorStop(1, theme.bgGrad[2]); + ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H); + // Atmosphäre + drawBgDecor(ctx, world, theme, state, t); + // Pfad + Pads + Vordergrund-Bewohner + Figur + Header + drawPath(ctx, world, theme, state, t); + drawPads(ctx, world, theme, state, t); + drawForeground(ctx, world, theme, state, t); + drawFigure(ctx, world, theme, state, t); + drawHeader(ctx, state); + } + + return { THEMES, generate, render, hitTestPad, padCenter, resolveTheme, randomSeed, hashSeed }; + +})(); diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..be23eb1a46cbe9c8bb5aa0821d8201fa64dc073f GIT binary patch literal 90563 zcmeFY^-#@^pZAY@y?5<8j`KLAp!6@J0Vb98@UapjsKx2pPf2^XA)l&FUq_j%} z^o&o)i(c{FeNXT3Gjj;A{_+J3=IjhddpJA4fE9_S>U_@{?Th9k8IL6*qYEd(r+plX z$G+C`!)e_doIEj)Rm5Ci7A=z#EdRY<^W)!d3I`|RgGn2v06+yQ@Dh1__K=6~|2_YY zf&YIokj!-Q5gHoX*SktqMJ9$1pp8z(!?km-a|r(70m?HU`L>UyMWDktbSyQxSJ!!O z3e7aD&43I(b17%^)Ec=wCN%RW_Y>s#d5Vzx2KF9#HY{jX}P|s{1!{8N)82`U=8bC z)x9JxLYh{}um;Me>Z-XD0M#+414(@B@25{dIZT%BEbKsPN^Supt+%_i5jna@y;&g^ zfVnL_6K#;K`qWhPJ8&eE4!&!=J8ERfd$*5k*XCBP_yxBZNe`7ZBi;X=vq(W&0X`+M z7eu)Ib?`;suX}@&mG1YvowqDkb(UifSij21Zmk87+G$qfO)rW8s4*0Zx61WxsT$@H z9GU)Gsn-YpqeyLqu2eZU7N9>I24OqRG6Km#(asz%>|O`NO~4rXb!OTHC*n$Z0}cBV z1gIsuGIWISh&x_Vzh_0aPp-)1N^&eD^SR|OaiA}(2)+M`mz(+4lG;j`AJ-O0*Y)6H zKe89CSXzR!lpC?e2yvZF4Q z48o(dC6&!^!q%&!*=@MOzdV+8>(bm9ZdkNq{kAYbc#iw4jM3uX%kt&l!qtFu_w1FH zjgWrLveN4Xo9@3-C{;)g5p*g%ce|E_7|{oEK;rW>GJLe$GF5Jb4kyf}mD;`vVBbY$ zWS9q@;xiQZD-=AT8AGlqxy>+3ixes{v(-Ru;ZFkhx`pk zz!=d$B6~t_yO?r^y51ynwg=B@5O^fKX%H{UuIu)N*!H-QyD|%b7g{zua-;E4YzE|6 zvv5UvIoCB%bCl?xzam%rAER-@p_hqY9|76f5u=iIgr3$cm3~^$He6VaLyb+|VG7`B z_O5i~a1zM`Q?oyL{t`J?-}vi)k4Tb8CI<{g5DV$o#4<&bfAka!kR|Vq>pNL}TJM+j^rDT8Y0JAZgg0}vP}Zl zG^m&F_$f7bUi2n;jN9aaRW*=H>~Q|S5_oF(o8j-n5tp@N0)F>c^==K944Jy}9BH7~ zUGN4Mv`S3*zSDuDBBV;q_Dx9=AN%Aly`dG6q;zapuK!@3AMn6w=kom=KtG{S;+MK| zh3@ksjzqiL|6iZy9$;?2`sukqA$nj>sY6ZuDoF2soa4%qe!((WQ}aRQLwoJRV&ah> z&sp#noNTuJ^oODi#|sbZcv0J;VfCI;Ba0>Kw8J#`vNaYFf&Uu*Sv!T~@l9N6bPG|i zQL1klsG+X=L8w72&Z5yl(%1h6?#$Az->00Iq3&A~VsU9Ic4IVh)%L9^d!rDGk3-SD zFeVi4YIxG}>r)Y*n(>KoY281H1HSxCHsq{HKza+4yT=!GG|jvG-i=zXA-<`nk14En zw#`SAcn()R3q;ljo^t*dKzM)aDgP#Lasz#`nSUge2HE@jJc)1OCk1uc5dC3;uJOZf zw+PE8#BE{_yGvflpuTc%S}<%b&&+}RZ}^Cv%lrLOYd#51VUR!A>&IXt%v6_i*^uXP z+`HR;41bnt6u63LQ&;2q=f-Xd0(!f49S1@n=MM>3t-R2?-9Wi%F1l@pe9K`1Ami7uw_((r(J6PfvO^xFBtK%UK9O?0yVYY#Eu=T%|o^d=+ZnX5D#bf8Re!t!Wwh3{nUDRuU`_mL)#U-M< za;a`@UzyVGAi}6V^8B}xZN^xJ6#g6U_<1g&^oSfFJqua_a0wElU8CokMP{^-ill!% zl+X9KFq1A61dPwb-EwO?J!4IE8BJ0b$Q@4mcH6R#{xa{W=h^r7V{sf)P=)G&)G^jguLNz_Z9={W4J5rH{_{I$iv`NLef(=RHcGzdL?95(xhV34x+{sm z4f~Jx8(n|*|D>C)Wu(saIAn9q|AqVm=gcpx?x8{oI)4MOU2%eJiYEM_Nb>eN5G>Aa z2lYU_?EU19cugMhS6#i2Sv~lY-(|E12ZQ4e8bLh);nN1R2)QJO-Ykigzw`rbe}h&D zJ{xBmhEtn@G~5-_f_hIf`Tp-VD1L+F3#Q*1yL%uNckhn836H`>AQAF;!TrCXYSd8U z2GD|8l@d50hvHYw`;vF^=kGlb?uhxH%&v49MwPOEpr<6KODw5z;ahX5zxdYe=#^&1 z3o8)F?btqZ@5Oa1Zow&|*#BnL)K53~V&)iZtbr{<>YJqV38R9vVV`N!zFqxgfZ-}-;0e? z*eH4I-_eT-Rmdcuwk*)!9Zh{}z_lm%Tx&gLnEX#!m7jmaNQC*|i_k*n&-)aiSj; zAtQPRVHVcb0>Cu^-an)#Q_e@cz4^xC`2A$7#(`{g&mD272eoDNFI9vFDOWjQt@K>k zswFt_@FO(x4;KK5n{c!L&L4kETvAk4Ge;7y=R;wzJHl^8+wc$TYFtD&;gU<@#=T%a ze81ma7PPmnvQuSk4l7*X-gExXih9|47=axiaTnkVq7D0=YG;+$81odYU39~Td}lTVnlwc~j|z+AY9_Y{ri#s%V_ za{0`yD1Yk@Q`;?E ze;HH*>i#w*5dyE&@Lqa21#Mm_Wo;>O+9JxM9@O_2VqdTJZ;INP@7d{=sH6_+gEz>o z>Sl~gk|b6D>LhJ{wJV@=vcyLdmVwB3@CMpBduzQxu-4?*84Z0Xgg4v*`3|Q3%{nQ` z@i!JxTFJ*__m-4a?H&$*y;PInM;$|YChXILGp2gLtm1wM@nwvS;N2y* z;p*ac==~4R{~Uwc6H{mGe!$8L%3aODm-ek8oa5tn?O@URM`^nBzgY3)rf^(|nFBL( zm#*YHlQAng-oh488pwiW2;J^${w+`Z-EY#-8E?I95-jZG3SIQPp;+qZUZu@{+3I&C z-$zp|(0+^zjwzP@g>;vD$@M^WIp#_3gtP38eEt#`QS`s}$lO1@X1S$Tr$hQ^3C+0@ zbn-dJQR=Ti04%>D(pnH8Dl3+>^+YcXctrh@9d^-j!I^>41ytGD|Lr{Pe>bY~4W32k3>i)aDOIjSnbPoybC8_tihh;Y@| zrYi+{{8cbb^ly`O8_zy*ie~@hm@4;e^Ay={4+=UkEDs6Grp#H zSLo97xBu-ZLghw9+_qn|2Qyh&*(DY@kbzsr2{QGkrS1Qhfj?`ENV8Cva}DI@6za#b zmlSc~f>r-1yV-~mWj;~hg{a%CFE01yM7el+hT)r1&r9Cfm7N;HdR#G?#nPOU#rB=N zByG={9xX$%GGdanjIk@DhtE@RzI0oUlGK~9OJr3hN(+I&ry={*6vs6d7S2s@_z~WB z2OZL+BvwDYetsXA_62tcrar_bzaGF5ymk0GFr_cxsg6|)4c%b=a|uy4uEM8bT~qsS zH_y0Htwj*pQ{iDn7p3af9xWOE)V@~J&c)o)emZa%;P`lA)8urx?JT=43ow4Qgi&&V z7A&R-Gs=6eS1*wj@XCr|*5_i~wsv-*A)6NRT)=zRdl@0)2u_{p2hnn%V#e?}O z6c581N|V$-F&B^w4{5b%D(Y-EHd4{=ZfLp_o)MXAOWoR@Lyd7ltYHgpsNe^)KROyjF(M7)i;-FZj!Xp6l&OdW8ys(xy)L*GL zN?FjZ;!!?|v$pw|kf|?NUl(hI>7OS=aB$IP+5txc+xZIA0#H@6I{WhjG0Y+UaupH zo}E{p>p08djxC(A+ji9pVz~}lLiU&}n0Vlzle2UAp7l*?!-WC}eU4#|@>{BVkGJqJ zXhvuzNoe2aWUt_EFqXLv4sV%5ms8Yzy@oCQ0G8bPu&ll z2h?>mG?_)JyBZ&h;qrBpGE)kTLzS&ZzrHF-=I;uDa+T(~3`=U%grb7|vuYh9olMSp zP27T}N)HPXJ(u9$mmAZ_mY-PDd-%C>^#Xi3n1&TDb4xo~3v*P@NWbb&&9xxnjRlcw zXi1o83OL#nMd<=ueY7y)*uC~xX^|UGFD$RS;gw>tnVxF6VIOt5SEONP*0KcWWLIWs zJ8+{XHfA(re|mU|6_c5!)5m%fwshK;Ek8<5nPpFLb8zg^v>w;ev?yNJTVLenJ&3Bi z)6Ck#3{ME9bB`1S-%U8H?IOf+x8nYNQ6E_Ko}PWOa$jfzqOQw|rI>EFut5`MU?h<) z*?|L=8Mu=NQ|9+`=Vn*^IfT3$@I*n)G!3*3>4C^Or3D)a{}H{DFjoB1Jxec`oVK4f zOB8iz5{#TPZ1VV6uDZGC@`&z4pLi*sAaH%-uIO9?b2ZfqGpL1iuj!<52#O!LZ<7qFv|3yVBJK$d7x_iNd^c^!ulHNdnHWug0^oBp zI`OsS5~tGQ3}7B(9J2s8WCh*AY&f1E3@9yGea_Ws8D){;y1epdMT(pJ!!KIkJ#&)z zoJ)9aM}F?;(cO3SM*;x1L^>t60#6qIwA35M!HOpYJ z)rGqn@;;>EsfPI2CkP%q4aC| zDZ6vmO$%M6c&uG(?!cE0qv4;%Prwq$+u;tD8xvb+TmbyzvX)Iioo`+otN2NS1ZDkw zY77CwzjZ8WDr6%IyLkc#T$Rw{+8RsOxig8QG7)Ti0j+iI9vq6zsFyzD2L7wG%M0o! zb-Cva_S|LU(|1=+#m9(gFN7z>BtNCG$Fws`D9Z9T5p*z=)1D_8a6GOK$UBae;Q62u z7qt|aJK@x3bL7+`Adh<49UvO=aoCvadyE7vbJ#)I^7_s@+U$TrP5gt*ngAtVfKeaU zd~!d1#W=RFRXw6FGW~d`YzZ~9&~9-hyxHZ{3)9bE z0s^UXPs)pNZ3ZV@cK&`?_XsNMCWq87dbvUS#!c|2P`_rc@)EgGncB1;Q?v=-%m^fX zG0W(!PpqN}TGE!gnGw!O`jj=iQ_v1Wo-y||&cF_Ehs`SdUdXru?#QXBIhY$?&YJw$ z(*&blPhI;#*#O(P1g+jcbqZKP)%eNx^ruz@vZmzu()T_EiZueGWbN2 zg7PJMR8=pnUb{-|AnFlI`B8v3+{&jYc|_s3IBeMSPGv^xIV zM*>DH=5{H>)TnqEOO`cvEpCW}Srf^%C8L%VMTy}OS?2+_>PdBF4G~3lUzt}TSn)rb zsRL;yDaU(6E!l|E!uWv$zPGuBX^3jFr#zj7af`r*flqi-w2DqOWt!6#KXazaCCx~3 zVvVc|53-iVG=Jt;1^fM`n(*6gE%D|`0k>XTJzrI~ipBfyG;v?6(q@`EvldlY0urZ3 z6SrIbeT-n69UjkdDqOg953W|%wsi06Ax$&)JpSw%WgBI<@$CH46`aCtT%5Kzmnx2F zpTL7GpqFB)?tY2y>Wofp5c#HHCGzZ6ACiVSBI*?_MFQASk$GlRqV7lMOC^lO*tDWz z8}gBkl?@lWB{!*yYIS>a1hTdPji}efDhf$+6h)7WZ+)-{6V)(eu{*Y@s14dvt>f)l zO6{nQ?_`@hWYHb!TIFZXeuA-(r%|NJJL;}08036MRzUofCa*C1ZI5Ky7Wg2v`SQ>p zsKc@&ciyB9Tj=cJViE21Lb8wdCEA^cAj$a{vih8=eo_@i!6@wa<|)*`y^FwmNZ{uq z)TKMxDFmy(TP*h6ncIQqwNo&`8$Hf;)VGGU=!YM8CHIb7+mf@FK8r*!-F{jx_k$}? znd19?#Bu>;H>bxV9uqxMztN(dm@D-CuiutdUe-_b=6%5|{kS$7K0-R^OGA8gK{*9IO*r`q25gnC;)J^vMaiamKPI?AZ5_&vUoCI5%)uK`;n2zJ>F zvUER!XW1>xy^>4Kwz4ULes+gWJk3DS`BHFq@Cs5iN~=8` zDK4%x2xU$jyR=e@2=Sjv*nROkF2P-#&X8Lx0jerP+J&%zuMuqxtz9Q`X}2-E$ds?g z=79(9P^DHc%*0p@&gd%YpgAQ_%ZL!{GUh&Y)GUr#p1q8ao1Qix8>Vgl9#Bz%Sy?35 zGlhNY(_BA!0f};zUZ%nb8h<3(pIZa-o|2+Ty|&Ww-F!*zkI6Zv^aq^Sn=2UKyT`ra zh$_i3o9Gacf6Hk>c}Z&M&ot>-TvX^Z=Xof>seO=+(?=CjYVK<+?es?Ja&bfBZR)KB zI8ex1MsP#ApYm-yB~o%#9HYsOIr@dg7fM{snD=Di&JCxW4nEUE6*7-*8risy^A9`% zB01Z*J{Q{_wP8;f&1ZL}^RAi7mW>&XlLz2C`#A2WJHNK??N?cBZL%udm$7>Ao(L<( zJyp7zeXvEpe(@ijF z)@imRr~!f3TSqc;N=@6~Rx0N^0`z-le z9A}GEU44oHf+wM!LzIOU;p`Q`-lI|m0-NSc=v;PKJ-b8o+eOYaToc<~ugPtX+7;(D z&BMje}X(_mYoAeeBdf8GXZy$fuW&hvZfk}yY|Vp(CR>70oiygkFRRbZNgvGonB=jXN3Vb0 z`{s&SP4x)WtKt4`hkk6q5!b~tTsT;|%w8rdsCw^9K=*u(g_e=f=^~qqg71TxnF(GM zZv2{pha8U@G7<{$)g7}9AxcbCSmt^`gzK@vp-*#9_|6vaMb~#d;d;j|gXm4lkTkmE z&+`9{x>Mrph7ri@X&X2^nOj#w6qn%SA2EpXHVcP*v)3rVIgx;DK3c7ko>8t@R%RFCw@s|d!Q+AJ{imDOO2wBub=Sx4+S0l2m(}cFir=1~ns%3cf|PXOQ`l{NEc-c2suF+w z19loU2ka5bsbM2DCg)Yo=!pI}Hz512L~*dkZ+*`H>dg1WWjp3XYKqC-)#(F+_GZF4 zoHR3mJzILg%H9pQJkSAj1)vA6(lXrU7TCG%q6ZjkhFvN>9li5>@~6$sIuz`DLy}hj z88p;qup>_DGR(}VGgP1HE89kI_{3wDv_em^6XfPSUvIkdf){GJ&;+@Vea7lLmK$d5 z$!1)MeR4+4m<=RX1*eC8f?Eids>0~unDr%T{g`?c-R|K&`KB<#6R+BoL+ar>_6Cyly)Xw z5*~XNBa{E5C%E-sXA9KBo7EA=EUpPg63n|*n&N$PM}D0 zJFxeyoMG19zJURHWFQ*Sdz7b`y?XZ;tytOYoh95p$bE3!N?Jz{bbk00h8#=l9@v)< z|1~?5D@j5^a)$yQv=TV!LX#Mn<9?~6_*2{AxkrfR>m@u zjR94?UrZ|YEsi@TE#hs*)rVCv?FuwyHvLsFy4+H4n2}8{iUCM+e6c>JlM7n8k>6lK z(25NR+R_Sg<2aqk!B!pc)^&jE=goxk(yWryRLgsrmzNM95ZIA-|NQ7n zv-HCOm23R{n^r?oQ_E!6JR9nVrqqWs*JpQ>$a*6@Y9X+~#!C;j7Y~yqLZrHC>KIrM zLlwHl&Cr@L?(o2+f&9z97#5=uaMt_QZYoGB7o@{;oFkF!^==hKf zk6tTVrhE0IbJ+>Cfy0q6zf87owVHR{NY@sp~LO^EU&DiOI6DavCRU7qG2+uVu!z_ zM5Yf%i4M?%z`_D;p)h&&)hY1U`?QrH19}U$KJgE*rLkjXN7TvK^eLxJ+BD@4we`)7 z=0Z{FGecR@xHEe-k0j9t0Wf)4q-3S?GGXQ(>=isja(gSJHKwcyN{DNAI-P42NpE6h zCl>aJ#Y&0_T$EGZ^-S?cNxp#<<<)0lI*~K+u8SQ#x}bzIUh{6JyOu*COoh$N@eFjIIdX6sxr7@IMAGOAlt)t z6r;&gugzO;w_FZmTe~EM`Sa)J7PhVuR$KYcW}q@tpNbxgBOQM&hxe*1eBFyheRsZI zn9rlRun6JW(#xAe+cLbSp~*5Wn4jlQ%g>ZoQ7Xm;vT?6zSKKu^NJ0SC$C-**tH;!? zCJ^4Z^9OrQW77~96sSEP?<%o|J$El zgYv5|Uu4gyoOK&wux{KwItpF2EZzB#bJt%}CfvBv!YISc%6y54w&QALgIwuH%g}kE zqVx|PI7w03Dy1Y)^e#Rojc0KrLs|N z*1^4nM@w4E=NhbhoYL~u=y21iSz@-n(lR&(rN`Yo;;j@+yj3bK%Y>{SXznO_J>W!O-plGU=BDC9< z$b_^bRvN@`$~ZOb;&FfgG=R9Ujh{Dh3{z`aH?4EUgEE6@TI<<5%0gff21vo z`qt(heikQ!gCV?Iy_do8*TY8%$5I1iUg0w0jUo7eCIc!?;Jl zrn>%(RVQZ-j-N?*+L7x(IgkoNe@Bh)J2}}#n5)7PO{GtRJXDZyKED5ogh=G%cD0_-p z1T>tt@@#TODd6B)>pAa3A0W-KTb^vcQFWE#>}t?hRe;|UIGOFo&4Bb7CTQaLGmkx%^J6kUu1hJ$$8 z^KMn`GPCK{c}TgUWuG#{PSqc0pg-8+|LMmHN3k@S5jK+piM0jhbcSoVmC5yMtiHVj3}% zSyCy%#t(lLo-htJ7zT>V6OcVAd)GpjO^j8P!xTWvBi8yHG!(c}s6lfq$xoK)Lht}D zJDlsg$=jL{rEEZ#UiTS4p?(MJeacf-{2d52Fk@cI&*VW ztrEJ32d%T-(p#s8JnbgEOH~{#`s=~Fr6)a-IozJp{u;tx0DI$R!P_U-yZQC&8HC3m zgxkTLsG$}`L_=Rmd;*%}doeMgmXbhfRj>xT#;M^NPN*?XrwNV#cu@7GZVLDXGF*4yl=D4gtaFpO%pZF{kE&2rIycTIKxC{dI{P0`Z^3oARf5i6aJyQUje?_Zs(QQ~VuAc=;KO6@&9-Gi5DU%||$ zNA<{B>w>n@?R~zfw#zb*Tw7CD<5_O%uj+p!zQ~O-${d2dyp^B$5YZ$hc*iu{`_>fF zelVCKj0(Ih^TKvKLH3LRim|?qS8BjaE!}E(R<5tiB7rkFs=R~e-mvBqmnp&L!~cwmw4SX0SbsIORJZ5 zxu-{)B*$&d!&m;nQs~AV`iG{r$bbvM&gROnR#*yrEqpY=LgS=CM6Vd0T81%tHt5)| z35ltZgil8O_)@6+%KYk7R0&()7;4_KU`{z}mfIA@Tfe(w^bfY@p-$C9m&*OLgIuVu zvu{?SsARRyD~|af<4S+Y1Ti?dBv|LESY;<%`z1F0DTc+@bSd@#`ESo21?lG3c}aqmqxj-Q9fqON0l@b!c9l z=NquF+2e@8(Y{e@itK~S3WFxP`71RfxFF=+Pn$y0g+hCRYEu^Xa=LOt{O5*bIX_lMD%|vVlI!jk z!wp}>P8*aju5E* z*fPuA-uA1FU-zJIqrkt!>)>}BXMm|znz6xxG-YE)KZDxRhy66A1v?Dj?G7AS@0@bh z)$_cnp1Y;1-kB~ZW>D$cf*8L8QXOMmpi4aqW}DQq_)@>d;0!L5kvH??@}B>Ke`~xNc{-cJjv{P<69liYpOL=&3qB8d0ZCq7^AoXzB zQd!M~UIwyd^~4|Kjn>&)*jyP`$Kzv)b#(5AN3Ew>zsRd%((GObYAf%u9%wjI3TMIO zJSpLQE3nZ9;=!oYOW-mWB3)cBU#WkiDP!B@N8oJR3j&Exms zwx_$S9${>1Ly%8Yz09RpfbZ1Jltv@z@g(fa#rXOCFHJI2p&->&se)p12KP3mbI>AFcwxb~j!1^<7o9nCT8UXT{nZn#l zNxxsVFV!3^c}~13-%(*$e7_4xIA%Q$XeO~<&~ctV6d7}cmEQ?IEGs>mM_mtyv{mk0 zHxfUJ{^$tBCjqKBD{ey2WodFP8`CWH2?M+pI$1<|x_C(jlAUNytFo|cQD0L$W*cYD zhtFWSs74qAW_ZLhaQ5m-ss-zFCRf!~?GX+_Ho%X`WskE?J6XEuM*`3_D$e|g6Uwp$ z4)RbsyKb{yS(P37oC>BbtXVs5PqBp>N`O-+)ji;gN}W%5HfZmt6K*GUG1RYWBGJld zOX-Idx;vUtpyzxjD}zxxc8;dgYlx-`Lpf;#)wanU%@cOeCq?@Dc(vFVZ*&Kku#DDN z*?6t_mAkMEq07_YQ+JzLW9|eUX0mR7eu?yzm7I|ltDo81kGySs8F}!sbF9?F1KVoX zcJKmMdbHxDOv+iIoTVZdB~1*-S^^wM6i3pXu)T1L5Ln5#;p}i!4jFIljJ|^Lf`amY z-H&K;m!pe)V7}Wq6zm@VG00=Jl{ptmggdFP^B0nrE+xrjV=ch4eo#@^KP&@LH;~Fr zhB-i>LUIh@pe+veTj@FN)6@7M8U+MYt~bL`64y4@P+~JjxJyd36l$l)_OawJQ26b2 zFr{nTxM{9EQ^upCA6?+)ruEsHnpeS9@i0sqTxLgcPqA$HecfWtT%dM zNZb#FQo=hNVp_@amB6$AFK~hdj3fG z^u5_$mgkhD5Y=X@!z0)^z>E>=FNw7^~=(f}@`` zs?(^98OAGnZYgd9dP6d26fE1+QN-D`EO1u&qRkm~9tv5*v|n|u-v(x&bG;Q^ z?(mj*(@EO%I6*bhnt}$X}$ZmtVEcM(EuS*1|pV7%M9I zycbq>UR8ST)F%7ytW5?V6gakIM*t_wC*pi{!uivhOzt<69y8acpIJ(C4RgMHAvPFa zL=d#xu5jLZtmKt%=lkptp%mtPMkL<|4*PcNM^^+5ob>#rj+JZdNk+4RjC$V=k-gS% zACaQsWJOW8gqG7b^LyX2wh*9t23NG2WXJ_A5x(~6dn5KM!2MwTCJ=-bVgdup(VwU5 zZ>wa3gUdQFYEvlIZ^{5GuOa})B(W(mHZvn&e$%Sc!tuF{ke@M)&#O*irIyxy--p)q zH1`Jsxwf!?Jr?V!#sQQW%6f-i4g74OeCBBgW&1D`+RcWTZ%~{Y#h`z&y zdR3dxj+Bx$0rT<6v~%J+%u0pp;1rReR?(uHcm!~QJ-E%(Ywz88_%N7FQlnAe?$j#_ z-Ny6}BQ}`f-4kRGCclQxng~~~=epP9)$Xs|Aj$wKU_Gv7QQ<7Hc{C-)>!+Qd_Tas7Hl-O7 z#D{!|yq^|U-R|R2@%|=NOz67?T71$XR=QHl2HSMm4A07A2H#h<^lH@P`_Xh20V$46 zZgP4M>YzEoy}^VT-}vKZf+3HeCTxPUy9)r)g1+Wcvu3_8BI`L>Bb_a#GzIFfFRv9z zoz;bf@fP#|vhug0!C%2syKFH8!qn-_6O?B=p$1ZDR?pBW;s>lr`lyre2xQgsN~L>L z4dV>B?rxZv8Tw(aVn=O#CGwH!mR0vzLUDrcqvdZxVYv*P`sQ8Bq#g5tw{uxrSJ<3| zkJxNRe{HP26G=MdqG53jrsxq#x|*fe|2)ZlVw82$&4XIIoPK%;ut>P){e7A=-nH%S zOQaz2Gs?_CuVUim@W+$PmeC%BFJDv|nShtRKkBY?2w41nS?bn|tA_fn8q;`tqvwxH zmfeEbOb{ zZ+7qN4{{JD1-~ei2@93!n-=WZMYIRF$5kj5xzKxDiR$&J0z(=GL#DoC8zK3`xXqi1 z@w2w-46oE+)nVPE$|Z^DkOI@M^pcy>07mDpWLmNEmY1k`N}b8H*-`8CZAs zh;e9k(|%q${srjEB;8T#^9 zn6Jn)hNt}-s13!!wcTQYA_!ttNC`Z(*CywkU5b5~qeOVz1`N!8I9viSMkkE%+T}>{ zEo)n!2+_AZEXs7dWKQ7V3Doo%sS|^CHF&H5#gnKy9-7~s``5L1@y^roiAS8@duo>0 zX2xmZ%OXF25Mv3MY=GU7Eiq{d?pco0lPA5uB%uP$w?4_)h64!stEX9@_x5GDLoO8S zdmci~mJVEOa%srug4`0|lKZM5z(P^wo=`VFp@5QKDUeSi6YAl#{jRbI?E~_du9slLPH6F7WC>M~W%9|^#^(IO-rY&OUBQg;$Ej9ByW}b$ze}qk0)QJ9{xbN}h~k zwexvpRpQ0E$p-uJbTj*9^&A!&V{(91dHaFu;Jxg-TBtZ5-(f1fh zjk{w~kH-3`80O2p${S-h74qvlQNie2SE(6od-)-~2R0##aI$=D#V==}WR1i}jvBMS zCMJMKj~382)LZg`&s4_(2E-7~jD#L9Vi8YRkhj=OOf40BgcjL0-nfM+fQj5zKYy9O zMKTnNB=3m{pxe0=^RIP^!T zYt;Id>)lLf=bpVNfJsq1M$x4_%N-Iv(h2B-KjILrJeXO`6_y=<$;*_By%ql6r0{*x zb@%5vxqynzxG$0Lkw%}$RV$m=Mv#4_hnMJrrA===BQ1_*O3yj+#YzW!O-flPJA`~nz}dtGI*e2uV`~} zdZQH3RviWP9r3j(bF8Ck8)d(lNpF0@nwVA6xnrMY1-fH%z44Ez)ZVK%mR6C%njJOj zo#qHuw|tntIk(^{;Y3aB0^J-G9$oz(0DnM$zpwA^zVBxHb0^n$ zdVaujjmLL0VyG`r*Vl7a6Cf4C4dwsEJ31H1E(`%=k0(IMILox7y66>stP5Xp2rSak;a`1@1AwE@S_2Q9>!rR@rd+6&aR5tr-ml1U3L8__w)bs;d|2o0o^ z@ad4pg6Hl1J)Q--eVEBhU{6`#Y~M2ueSKfFf-k;47d*~8vN>iMI`7GCGxP>@_3!CD zP3y68Q2VP{APj-()OGLq8axa3mrkwoQb{~E?`n|J6;de_=gFnuqsNL8Quo(M!%o-HG!0EoOd_PNBgVE4I=C$^yVwBU52=X0p2t%$!cgu{PLyImt`mb} z!(pidd*-nQ%_dFD;k;)Q8XE8E`<`Ub5Q%|dzztxDN$Gg9pRhAp<|+1WnZgPT<69kO zu4aK*s_zTKHCTPVb!gPRW*)iv#xMjPkN-k{bOu;F?t4DQ(ZH9R+wk_}6K@W0{+?PR z{OO4ONNjknz$hJ3h6RMBrIv2m)n31`;n1LO8|IYZy$?z@8t0kd@j2o8c^!M=sin0g zvp(D7#kj=&(uCjc_Uxk^ZxD8L7*G#ziP|oP*AQ*f3dLyW^eV+2@|5`^s0j()Ap!G+eX7lnJ?+uy%5b-erhT z9zVzV=?zYmjwTmks8y7;j4IZK&qt9ANTY^VY1t1=?OT^KMmpjZs!ieUF z8^l70L(s_}8mx>~-*=lEu(o=Fw7<_zU$_xqhe$te7_YC>j3=b7$Ay}Ole@%lEJu}bJ36Z^0^Cq=dCyHcn-ht^iw>(w@cG!qHC~H zFzV0a0*QvVd)WMm&-2Z1eUnQi6Hab&rAv%M%fwIcrQwOsy~LHT|1nSZyKI((HNy(F zOLe6xsg5nwK>dg2IGDL?8jD@IDL3XOb1MwNX|oVF9(I%upSpk z$q`+oFL0w2QVd`{%f6xQ>xJ^nceqe{Op!~?xEwMoTJP}vJ{N-Hr%r71udiIO+XsL= z1NO6zJ;~D7z6NAcDva7l-zRVh=RAjT#OvcRCqDP%yz(!;LfY8kYU}92CS!B-De#yx zo`3ckUi-b@=Ry-{W*y_;iC7J|c$h}d{&;^sOl8Vk@Dh( zKk#ms*k9gY-vYsE)`D*mWuDk{4Qmue#c)32i{tTgXWw(5{|{DzeaL=`4AoYdZjKJ67AvjpJ%t>>s`G6~q#*g^?r+);#_43j0 z4ZykWO}O?JxI)MgaAenDu3#k087_O8;7PsJ*TfZWIL9UL>3oALFiDv+R@n3%-Vzb8 zA!7~a=#vpgkUQ9nJ=;F;*{yZHd2Q#8?eRIVpIlkud^e@(=fiWNmUa*Kq@DM?+h=y0 zhW5l+rp}XGz3Gd=u!95q^*y#R)~zR|IdSADL)Pe86KQ}O(-~jd+TvT+uD#D$>E10G zsC)10MA{)3=7B}Ud2fX?v88?MHJ%8LvR9AwCf6Fzf~(t|I-sWz0bl$0Q73s$@{FxK z!-x8M8$%M8tHT@IP#NOP^TN~5!*^f3F_HRD@3}>&r!zdq6d<`nZ+J+0G(1c+j z$iSt5a}E0`liN@)?Mla5E-b~Cp(q$nspk_{GZZtPTr$BQa(=wZ7k%X41p7ztadrOh zHO{^~YB<|Xas9lOe3#lEwNPk0q<$ETlnnzqbL_NBTq}hGSFqTSOTe4aI9PF+6?e#% zcZ>+QV!ZgmGw}77Z~5IXzVrh2_1`0eKpATKV+hpTvVx6(+}EKX9+D4qM0PV=v4%97 zF!h->7oLcb*e6EGXdR^JNUq@X(6JzbFPROSa+1L3H&^)f^;_?UK7VqP?F7qdiiMuJ z6()_xa$?c!z|50;WNMMiedZueIB*3QMp!c1;ArxkwPuwtJ7B{HiVGy)^U|kZo_aw? zF{ZR*m=~7JaWO`ghwGE3seSx%9;OTr{r}pMIf-$)c+LRdy=_~3_R~KC-}yR@TBhy& z&EQiV76mw8`{Lbgr@o8F&YZ&^+~D})5e$6_kG`uAW6N3-Yd!dFHrF=syEh0{j}3n; z`_&uxQhSD;KYIq=dHa2z&+}k^;n{PXzCNeTbA|+1T!d-g(FR9H;06;eyFlLDVm@w2 zxxNO4%yMX%z4jI-vgdR(Vw#btQ=^MzT9?`EXFTo#fA#st_&>4-4XE7_J?70#K1R!+$XMFxpLo+ z@!ju!m#4a}HhnE~?AQvcbM7eMQ9r@(I%xYZ?Ck8^QKBdxpEuuplT((C0>-s>lhF%n(=9PDS-#>9hNR=lslrv%(w71i0F~f1V59c;kEAzQX<0x8CB+ z`X*^@otJknVQX7_^Ltl$EnQ*f_3x3mc^V5W%8Xze`26yi7nWP*-IP-yu;c=yR5Jvc zz~L;>Mp#Nc;o4=MSz6}h!-G5ijxFGEGtLAUcWIdPI^4iT$i3A}8*dyofh!jH%JnJx z7tZkA8#l=FLryHOaCU8r?_9aeGmYo`c*4XwLO&Z;k8j=_4#`54Qg7zpZ&<^3!F{Eh z+renWb#GkiJ9b0EH(tNYyIj86%oymXfl&{v0vCZ#Z>{s9Z#mx zN648q5>KzMz+1Qe{-?p38&^2xU^1-Y7Ru2?+-YxI^^vzH6aM$V`fI!k90ISs`sN*( z4FCPF{RUqE{`SU%RZpEBTgoua&hX|q=n}v6jo10-9UK5x;f~3-fBCV~oZGuWoE_3O zj+A<^fOny0p?R?jE!CHLmxtf8&tZ{E(%!5hqWaCLZk3989^edIIYc zee}F@{qBLDC&0e2y2f*P&eD8_bv47W_I(?Ie?Sb85*rR&;PupVwHfhd3e3i1Tobu` z{W5*&**vkuO1p|XJYcmu;LIqnk}_|%V}AGRU%%ydd*GeR@9>#LMhB&CGlrnfVv*B( zrWQCHk9jwB+-Ts9l$foq^ZK=2mPSjQTixKT-RnH=j3-=XsZY??67diV(bxT~anvcV z-gxfZV{n&1++RGk&dIAcSn6i9K(b-LN6Hi+g@$*s(4Gv*t&d; z4NHvtP|7x(Tx;2})s_p+ktR#Ldyu(7WOV*KZ{D~@A+x!)hCkeAX`VUVEVG>FjI1X( zhU0EgcJ2wT5T<)P)vocU&!6Lu-+b$i%%f+(zPx>kC%PG{rH7IUt{#uUd14!&7#4wc ziEFuVg`OK>#487Lx|MZodBmI7u27QW)QM9pHx2vmzRA;@n~V+)*z^sTmX`R+w{8!& zz}LS1b$-ly=F^T*Z18ycJja5^x1sio#4!W*Mq@7Zg{#Hb2@MD1bzZ-I9k(>%!p14C zTz!{knue#Fi0cmMOR3pYAz-5_h#fGO+W;yq@J%lSbZ#c7>jZG%_mIDh+L(9AUoayQ+-`cx@Ke5fhJaBzFC3PL^ z%S)VCS!Jn9ygS(`icJu$eU=d4kNOvOb8mKB(Xz-}|) znh#v=J9gUG*Lo6Tl4 zO@pJ5a}5xbQdezCF`Q1L@2h*C^ndy29UL6sCIN@5=bpw>`iwV+BVZXU8QI<6>+)IP zXOcKr9 zJzqJP+1Y~`|IyYmlh_jHQ&7(j6sTcUw-~atP9_LT5Zu8j8Xe{Fz zN9wIkZ5=YBpx{HxUUs}{BmULa7XQWNSNP_u-*Zs=3cWVnaZg^^u_fTAmRo*$WrHs! zIMHRYr!v-iZXGMpe!CcIVxzj5OlzwtiLZ4pc} zus>Dcg!3#@d#gCBJprQ+OqvPH0rtBcz6Wi=>Yy9~7gnxR_-I6`_*%Mo=mBNHf>F3M~#F1kZ99>s%q2We?U%7tBzxaS{Gza@f zyHmCz4J6N!&pfj^=GU&zSTy+m9dK{G_LfCzLg?eb-xvpee0jvW z&$OkGT*ieuG1+^^K`QjE4eCO(0!|&gR%a?ct>@S^I<p9l_Co1Q0p%SGVj_j;iFlykm0r5jIgwNGI7X(%79 zx6IiwGy%PN{c3KgU>HJ*9`I!-nY6y%7<;RW3d4$qKN=NU0J~*T+JI@kbHV#5uuqTH zxX{mOsRK!FHh=d+Ib1D4+>0&W?u~yrYWST?d%Ur;cNC)ymv=DW%O_U&$sS$`OPnm3 zwO9xH^nDu2-wj`2B}BHn%(kVw_Gf*njcgZVtIM>mW>J$flg86KqaUVl9}?M3k#FMp z?a=biFWsn991kw>%JF?-|J9@aytsF0>%ga13X{n?m&)Xh-#_gOW{x%jE-}xB1y`%y zb0Vco#Iq9`zB69uE1~BfzjcXAOnH^v`g;%d;qp~p1p9@JWq#2cPq;uEYi)AL^>vNZ z*VPOrqlSy8Ps2BF9U`>6zDmA)1*|8QdakP(lJ#BGQB%9#Ci9=|AMnlH`r6!beZRj4 z@78RnCx;mOnem9vJ@y#8-JJ_{06%(ai*wnr(jDUIi9XZ&`h24e)zA*682i@nvTyiJ zm-w|euJA2z?A(@Veq#@)6Pup_+XP;0XZV#R-s)S}yZNm(3E%K-O-l~L^^MU}oUc7W zfm|Ycal+T)GJlwRe)Y;BdmK_0fB(S&7s0lGKOd(2#dgAl>{yzNna-y}VhBU_E+juE zIM2pi?k8v692b0T0TG}}b>XV->mUfT;e{->7uvqJ!^1=TWUXE#)ED#S1~|q!ch_qs zN4+1Weg96!-g^$FcecB@-H&c&fP3+ z{18eh!%Bu5CMMbYoDyVVvE4$r*SCqAnx?^RwB;2e`= z_@T@YS88^RcZ{Op_ik_+IKK9MI!2oLj3tI3Tm~ZK9ws3$ONIUMh?o02GkNajD!|Dz z>|VP}OmmjK!~2?nmaL~RW_fvo)t#L?f^Yy&EwyaS=WGYZ!Mvk)&^8?PWW&`gpfpaYsVgI z#XH)PNoj^}pc%5A`eGDw9Cn#mv&1z%xEcM+;8wxX#s>ZL5KBGIAQvNeN9!Y-A<*W` z?eqPP2f_jXnQ-_R(C& z9M6c$EV73jFbuCb>JT}6FUO~y7HXJ&cr5A-e{BtBwVZDFqH}d|A>oEcy#4tPn$WvQdcKCPebHY-|{c#iU0TYy}O>H0KR>7pYO15|Iz8Qtb_^8 z!F8gup~Tu~e4q@?-dD-2*o@C^Z}IIbw`N0q@u??Se&;ev#cIICL1-QGd4Z-*tL%v! zj+XfP-185wU%BVwVR5{EWq$_yuW{>Hty*4QrJE;W6X}O3(=Nt3T{va@(0lVLfq(Ye zoBTRA&#kxn8aTUgV4vSu;Uu2u9lkHPQfg@>9xM51s`bvhZ}TMB@jznd3WbDO8J1ia zvL%NSd}A1-;W$CP*}hxf(}l%je|I+LxBulIyyv$1)W%6x-hG=TjAR+}i)_!Kw`8dA z8CMH@GdBDeSFZ5i-{b4BIDh=h>w9p0?~bn*>Ty$YordhgqU^dZ76c&S*>{1f&4mBv z&3E{H?!IXE8o1Yhc!2%X%9t~F4*EHxCe;3PWzh$-4lSCrj&$SB?B(%zOfe(pOc;gQ zUa4v5`|j3?AntuSWz&pm(B0nqCl(oicMG){5W+o-8*}ci=YZp00Nb1<&K5^&8E?6k zb_}n>+!rnd&(~)2_YJ}={3~!Tcb%OdH%vT)oEX+;F%q$xangG}bLtfA-2FOPK3WRI zJ>tBtD+b=x#-jD&W5}fX5Xo$d>@MZO-J9At*~lg1z5l=z<=d4~$T`>Ph{N(x&V}IJ z9dAN%ciKvy9Pd3P5B-wDVv-q@P-sW>YaH(L)OdyeV0)eU#uodrv04)3aK+szpM=MBIYpL>RtH{T%556QVsF(hZ8-W+)Y*Lg1I!f!P_|J^+V zC;riVX6Lrfa_;gaw!01+`aty=%(eAqr#L3SD8UQo&+|KPzIpT*rJKXZQ$}_FalJ7P zj@irz2=Zi8>n zd;3>cclZwQUPA}o&of{v`}?d+BE^hZU0Ng#`)#s7a*^yJZ(GAZ@AvLG=x{G;M$XLv z0og4AUiItQK_5KV`oxtu=C|G_2)ED!-O37A_O3Gu#w650sCifS`Qm7RNet{?yLQL9 z;DuA$#A~~>rQpU5^E5je7SNZ%4ePjeYKz~w#}*qegL`*0va3F_#f&)5HN(u+_l1#< zO!}UeE<6q|zk2T*?|m$;PaV{(!eRPITk7kJGYVx9`5YEB7v-h(bKXH2E|7DsU(ZAN zXtYJ3G}O@BP}`c1vgwXbQT2C}tpa>%bBlF)M*WoBmZRtEiW>$3I7&8dM9u}17FE}e0k&LHUM+vnDZg$XxW!?-U z{-2kx-g6M{UO6HZPYh#v*O91M%2sN&Ru?>1UF6M4%QyJo%=sPQ)o#wI0NYJuHk~o@ zwWnzIH4yDf!IwE}D=T+gHyzGrHA}CFbh)e3#`6%EJ$5g9Kg00MyBgqix0a?ZLS~EO zHs^fL8@ITdKksbuzMgR(umJi*JK~HvPP;%JGUTlcZA1#VwJ}$V<6m9B_x|GFI_!91 zIdIk`CS48QwgE_qWnVa5A}?RP|5i!z;V2L%wMP8tzYFHw(1`f{LKJ1_`BF-`_oatN zT+nro(Kj=4swfy_0L)Yb)Iuj?OY*waGo; zO2&s+OP$Q)N~V;2&q3)YhQzwFe$3-1Pp~;^$n!%|N_Cru+Jnkx!^~lE3E$0Gx^b10 z-Z3hMx0>)1d>Arp9DQGV?KD2#@fI>!8Z)2I@xC@AFQsNv`T~9KS(>bJ{f)QSzQ?}- zpml}D8CXv)wa6q!N0*J+Y>ICh+PmE?1MU7kE*DBMmPX^+D<>IE8<>}Rb5?xA;b_8d zT)uiw&*xQezxDc?_Jwhaxqwms#y(}dk9Zd;IS~w&$0MFNxdq1paiMAHOHXMWU53$c zdK-;A^J0vf3Ez9=wR?V@mw_c0X=6j_r}!b0EkppL#nIxqGkkWF1DYn{L&Bwk6^Q0) z_BKYg+S|xdrU|~5S06h*zw_3c?60jbV?-!30)~}@4>jA*$2z5O(sw-VJ(~vyZ25-g zFP!D=0+(?1^{ZF-=Defl-b$*sfpe1)?R-`zms)LVo!!kIi^*UwI}TP?`R#Y^?e)D^ znMFHCPoefIs$s`t-{U;F8BxZ|?D4^awLEZjKIPCAEJRA~=?gXMs<;|pN||Qjh7km} zdS?3sYX=83eP))@QHf-2uTk4%xC+9sol8oD>^Zl6 z>LUrn-Tq|i0(4yna6=o9;uqO$5p#~5`?_z|s2U8-+KaCXERLaM)z{2=pB=7P9i~xJ zX5RNX-EHdPvu95e-hQ1BGRbcCsdG4jg__CdU@t~~|N8C+F2+DzkonDn-hOdooxS~z zWk2*eb*?tJkKm)Hn|Cx82s=BslqPQh%d|Zk+(~- zfBoh=yix9bvEXR}>sDwYbg2hdhc_9}_QvO*AJ7e>QN8z+ele*$ z*F*2N!|Pr*M$Q=@TFl_dp_WNd{>J`?lm(Gf^)Ia(c;Pt0Op zcf-w}&%QS9r2!7zvV*3ANATP#0agm6mz&{gyn=UJ#>jt^dwiahq`R| z(TKNm=3kFI|5d(yi2O~@fPE=?{>;;lv$DTO?B*<$MCd!bd5W9YauW=f3vFnb9q!`C zOF(9A?3he%upC{@rWi6j9FE38?hCQ;ERV+2Q35xuwH`O`FnV0E`sSQ#;KL886lTUe zwtQ{x;GVN*eGK>z@OT1+Y~c9+v-f8~mSx#_Cibm8oO1_v5BHd-jL6EdvSt7T$ff}V z!Kn$hI5eB=HZ~=inzDXWo9RU_>IbzRS}l`IMw1?7q{KE-O;7^F5d=UIMgkzJ01|~7 zfSPj7jCl?ZpYCwZ*?TWN>~lRPj|h)MRAdeND|5^8h`9Hjd&a&0{jYy165GC$C4CW9 z*w>`)3!s1X)C+vVTHNMMR>KCii&-2o21$;tUzBlbbBRt+DkCteIg~n9L&PXFdJJ6KYNt2m@moIFD#zI(b;+cpY@GGhti}upuB#hmxJil5z)1MwaU_(X6d8*{2W( z@ZPM&9y!8%J!c)-MOmE2-HE*>VIY>-mHSH?XT}n(5LGeKk&My?r5bo-o){VBn6Eg( zgme4YmLtIOG~wC?C18AhOo>2Dk(7k43y&4{&VfX7inyr3#DvuZG3Abc<7gm!fipAKP zNz?8tg!~pm|_{^EE-$bfJD_OM8WAl8GX#YTn7#i#N)*hvLk0JuifdkyyzQf2m zQrGs)DzdPbQXm=_jXi_;l%;!K`vP#dw%ArtFG$dQ&h*Y-Yhfz|)`y<2UAw%m=g8CI zjL>!+)*9+Qr!T1Hl42#_J5-Rc|0;*|*_8RPM!bPIBPGR|tS&C{D@g&X_M!XIJ9p;X zKDfe`I>shpiMyF%sW0@aj74G~>`XXad5)TnN!v2Vu-OIv)UkuCWB70$^-Fi|@DALc zqxS%3j~~X72qLKU7&JHKA$Muls03GY(|SJGd2;-~(_-jT1S}Y0zP44VQWgR4CL+4N z^U9NX9aOrAk;K3nXcEch&fK9F`Ln2PnKaEqJID2IgBWwy-n(U?Ij42Oz1>frL_&AR zsS6?ep*_(rChbLDEbb@Pd!YS~ILnikKTjV$f?m5u7gHVsN-lfOqgam%@Y>oMKQp<( zPaHi>9It4%v%|`;BF);Id+V$0l#Gd$E^vHngJUznJ5Q2C*BcTR9l@g}4jov-Z9X}_ zuJ8w^2yq3hHAobaroQY8p0=#fkSExS6}~JGJ?ky~!9(P(D=UPL-oG=KT+o-M#_Tk) zr9AaXl>PI3$*XW-w_BnmFXM^|##&N}7|F|XRyCLU=-o*SaChRakm4Niju0ZXcYP-+ z2r+>ZGzzg^;*!AS>hj^FM|HXPaNUZ1ZO8Zoah3_T%t}*?}qM= zNoq-I5iyt;u{OJ@FiNr@F=0b4-&BE^5)Mb|BGsS*3?asTU)}xuZQ$y3#+l&&)3umI z2SCZr3`re_UE<}+@)Pq;zx=BRF(hs!I4Tuq&Ya>WyyZ(5F6dib?gsqe=8gnx=!I$n z^unJ?pTlGz8DVZcTkYe8$d5vvOuF8YVGxbI>txZ%FAK*Z1O}t=zPkZ7vB6fJ6jEOF z5J+9$uuB!^Y|f38`-9CbaBwih>V&RU#9M-b^FA-Us!+wq8KaEris^idnH3rm#z2)U z%v(e(VhzoF%D^U$Og353NG*!BS#VS%i1q@NHG7IxA9=qnGUECiFN@?{FBS_Xu&svc z&aOYD{)h_2qwPcDiclSh5x4 zdtSRN5GGq7ktTF_1#eBCvzPCEt-{n<=Em{hBGa?Lknr9U+G!pi_$<&Y&W{ zjrVLx;=}DbA8}7Vngo5WZ2oN3dvYta7tf8wsKJTbSM#|BsKqi+kTx;&IVUobqmxon zP?MWW`N2c?+7JTXSR~~8bMDKK^8MuAW-CuQ){pVY_nk-m8>OGunaAtVQPUHvat<9> zVRG?(q%y?Rf%N0hep%Y9a$q*)Csvoatu4*Y2DK*a_6(QubFrIG>5M^)$ES|twdb%? zeCX(u>@+o+V`HpELqbC=g>BD?BE%I)DfO##nUCU#Tii-wjQwQ0l>9v{dzC3XiG`rC zcseZ_KSYg1WZDNM&+z&3pzC(g_C6UQYQQ*$N}}t7fTed5*xkIOwA)f%B`>;a3JffS zHfMU+iW~-Sgy;rb#q(RH;vau->5(}~e;M@89X`%$lL?2?jA3n{?Q#Z(vw46bN;C@* zanUW%U?k;oIirMryA2?k%dG{(+o5v7Igs1N>*cQHjE}i9 zp&A53N`bLgQnSrxea(M!a+Tk{x}jfD*y#6l-`j!Xz@J@O;m23ToNPA;?Hq?gQ+7F7 z7l{Z{5|hIRc<1WFqGt^Zy@NK=wk@_cq!f4W!?}~95D_+;ou|tF48Zkf$_ssN-B=?g z##|CA5JHp7oNa@3_ZLjP6(fH%3G)!~z4)6Fee^jkf+TZ zdGklWE5OejTIKVVaI6dXW{z>5NT5rKbB^s~x#cP@ru0MyObQB>$&Ml`geVxvrAG-+ zF?2w*c29TN|2T`{M~x25fZ8lG3ftHaaAMFTV1$l@+T5ThYQnn;qY89rgf((!$dl7e z&PMq3p+mfD9Y1&Zx_Rk`1 zzn^f=y)XN2EQ)sRzMo-&?emii5RU;XhybZ zBo^uV<13I98y$Cy<0}VO_|;37@GGOfIfB8cB|BmI4N|~D+eNBsK-ni-1YlO%xwEYbwCDGiod*Ww&ouzw-ws3&~sN0VZAh)F<8c#tiip zRc&bJTR9UAL6cHdkmk9Jy{_ziLR`*94AelCBUlL`q1KSEFhRYi3x?VJyVHq2r2vvh zqB!FT+7bJO0Wnz&4GKow{+&-TFIcDD1%kU-UP*P=@f6eE1%&_L_HEXeSNPMTnwLl{ zX~WP#40$22OF92GrHCjDhn{xc;lyFAC5FVo$}yD0sW9iIuHz4#JjS0gMt}a&)!k0i z3nX^bE|Nk3$?m8?{ztxdkhQU6xVFaj)jz<_e$@T~f;WVGciLS{>k_DgAWCNp1h7%| z_1rrLA~C9$eEw_9Uuexz=lYFY3bGVtZ z>>|^7M?I)WL5XeG=N7>vDYh132i34*(shsm4u|&+QY31gI9Zd}?Z7ALu2}wv!J9XN zV^NxycDt@cKs6!O(kZl0ZJafTSe&th6iI!#M;{gk2-=t4o#o+dr?a4c^Aj)f>dq#w zi!z>1bE%a9afYY??;TB-2u6^qB1MC7Lf17|Z%FjpZ*q{ehJf5V{*)}DiY~&;_a2=7 z&doa4g*=h1lJGe4tu)fKe?HMQM zQ))4|M`idv}=dB zGw;}H8b+f5hbzx`{U(RIIY*mq#;LMzQ?3~fF&Ky?hzMJ2qu3vyVrf*@fsApC-?SQ6E(YFLqFlyWSIM! zYfH=gVux^hzs|0^9VR~w`m+M3#R6G?QB2I163OaUJA+9;I(uV(u;!?3aGs zDmM+;1JgTC`>%WdYh%jS)^0UdpS!IIw-`T>JE`t>i&6@pckjD@P#ztIl6jnSa5wUz zcaPIw_ql_4z$6$Xs9nO$r(g^tQMzfsditzd`g6F+nZTr6X@bcO(6k?6^unXXK2>vz z!XJzx#1#sKLg8bfGC~R|yQDN{kBGp$NjU3rHp@bs0q7^5qIV~9)(BlnG(ibQn7bj@ z&49Ps$ooq}e(~akM>^qN2K@^!9OZ>(%E2^a=#(yWjEpBV5hO7fR>WzCTE|Qrt$18r zv!$RzPmB>49iB*qFtT8S?q;X}xny(K&0?^6|HJnTXeAz(3zlASMdPndLecl4zPqL6G}(%yqVx;o)PaSFsfW z(L0bt)+f z7xSdVG&5EPLpIiL(VE=(5>?!t8w^!hMX@Qc$XqkhJ0DX-ykpyp*ffq`d+*ww+pz;Q zy`;Kmh@hGsjOIkn3 zoQ*9ftz)ba4LKk`#)M6&pPU+yd@icST*7Q3#tAhopGcN!yo8L_;OfO)G0wQEm&AMV z2yzF$MchN`9TrI$)+5+?Dv#Vim?u-eLcTjFMqd$gjKCHdLuU&oLyipavW7 z5#c0)R#>%E-z`;v-mNCN6iy9@L|ReIXz7WseH0zJY!lP<7cAJAA8fi18F$63^>T;PU+Pk!kha~rj7G0~PE`O#a zFmIJeq*Bv6o-N*cVk`GGR(h6X5hj-j2gyFsMQlYDVtN7XDFjNFQWkmcISdQo{oZ3F z);vKsMFJd)Lm^sTRI_y0S5_Ypg`(rJSIpY`cJW3WUyO_&UHRX#?ALICc z5O3^mMx5!d;qM-gzh0Uy;*-Utyl5CTKbe&5fOJ(}@Nm{*QeHf2VxX!7O^fk!=!JWC z_ZkskcN}f{=YjMNU;!iFMM=GIFGYwe6bgmH$HF3)s0p!eI}hpvT|L12>s!RCCRv9` z>`ok2Aeul{w?M2Rps4pWM!22eosB8KJ)iR%Y(91f^*V4m1&(@U93rs^3@eLFRzSQ(HW_Y+^Z~TUt*!VgUSbpK(xKf zFW%=4x;t>nlf|bQAQ_VdO^pe$f)(j=bPRXvKXdog2YCtfbE|6{-ri!|&QO;{K+)`o zvld!0+_H{4#&ZY5dJ>utbD3M$;hZ5LxTK8Rj#pNeIpsSDP2LavKzHHxiH{kVUI6*$ z*RSizlc$NH`xsN~E-(>qNyI|-IVI2kHl#qkqH`Ev~r zoySTdH60jm&Z2F^TTh4`Y2Gl@ibK@^*!qZj@9~)3eVO;0sM%eRGYXhoy1HL>U-tlM zAx2AouazhFQv0P8<^0Z6-`|Kp1!nm5z~`W-Wcis49Wi{wB2JDdhXkv zlt>{yDdO$1JhEHr-7VjB06$KO+t`_8fip_35%14LBSPhcS(oVQyhz-5PZJZB_c_zJ zzyI2WqZ&gpi6ptS*=X;yQ@5L)sbXkUxal45Jo(X3cm@<9u23ix3Lgtma>$59NI`MK ziW>^=%sMtspJ8k55Ia7v?HYBUxg;kU1p-c>na)_>ns8%dgL7AJaErXMzn^@PKXUpw zN7^YyA~C^uP}5Eu#3s}^jH$S2EMHm~@&CDajTy6k7?itrEv8q7j)TCD0Y5u1e99~_ za1EWd{l9XE_C;jsQ0^aaUI3ZQqh=AOw~Op*6eb7St73^%-taV2Q;i8L(mPAC!^!kr z{uYD9g&_9e4tohWI-j#r3nnF^>Sk7Q83$Y>f<%PyWY7^cWVw>=!|HZf)s*b?OL15`)P$ z%L?LzCMc-ETSu27&Kg2&a^7ME^Vrfg;C+RrfOmXNfb=WxS+YzRqU3WuyQLD*n0@E1{W8ce-`?SqpkFw$#OrH^I6j&2LX4auXTli* zR&oiCGdbID(I6rSs8wv3azZRW^YR(~?OWg0^B_||su9vnaQ$Rc$+=OGCqZ=#98Zzc z#_}TUx>lYAXF*NMrLcA(R+Im~>8spx6Wz4CkN1P9$-(X>^$i2=mW9zzdiREsF7;daA@j=&cJ6K{h%#(HQ>HOPIs2881a#v8vzbB&PrU*eC zvj97HZawYp+c$~q?i~Oor7X-#r0brhy~_OU3z%()j4H=u)=>{EbqpNp;6(M^@nU7SmFeRP5=Q$j%nMhoNtpik{jw`W zKk%CSgpriXqxa+U`zFXYf!}!l9bE-};_w=0-GEP6;ox-6i6ktigbBj1QX0*!rn+~h z=Y|Kl^u01L>F80ovbUrA zqw*4HCD~aWCDPe^=kY1+mYG`uE*acYbteA!^D^ z2VzG`xk(J{Qm~!rFa- z{Xb7?p%FB0lNg_FD|rKe^S5%><$v9pWb)bpOQj%drfOmZeqRhr|vj%pE&SjZTf*6 z{q;?m%-73&BW-nQK=!%0RGhJ_Ic-wwITY)Af}9I!7l12ahuu>YuY&g1_sNStB~s*|`b^?>@;s_YQ)m7_NaeJNt@6$VmEYMvwmY zYsz7V^$aYyfPdWFDcbDAN~@9r^rSRDS*6plzprebk}V8SKQ&FHJa^n))$J5PB}ZrF zx4FlaLR-vl-M{cTeo3eW5GS-JzJs3Yh(AvIjLV(J6)ykH3!$Qlyymm}`TkrGaeDfp zwcdeojrp{HltI8@>ppeD=uHD7jel;sH2V@>#=#n>m1ar5t@7kYKBQH}8|Bb0s0VL3 zGC0-5_OaI{_4dLSRN_D)UrBrDnXVJMk#%HxCGwp(0p*c%8{F;u7&yUmw7$&#Y5B_*V%5N7McJu_wc(m&%j31Z86fVs|FYokst5MH`{D`?VR`WwAkF9 z&8I&6jd=8LNp41Xl7z)06_rJ_U{4*(!f#y1+Wd*qeXjA7 z*Hwj)prpLLCE)!l=a38K?W2mP6VV<12_s~MGmVV8(vYo@;67WDtXQTEzqyFXZ6-&# zte!Q;&eiep%+1Flf%yBOiRM)~!RxTp+FV-BWj~%RCo~_@%cY4c0rtw3HPMDOjK11o zgfZZ*dgELo6KYDz9IF1@_M0JJ3G!nIHigAF?P(-<`vOs=77ULNO-=Vu1U=rZjBV$Be z5g(N8F>^KK9lGgLqh0w4E=K^w&L;fdR^EX+r1^1gpa4D!&I-$hfjt%IetyT2n{mQ) z9V94&)lyr|^(md`2E$Ec+wes%`q$my%8k%R74L{U<}Zxh`r?8bFbf0X?RCe1oz5pc zaS|_#G30<<3))vSTH@K}tevdcS@S;q^n6lP!!gy>tI6_)I!7kmtQE&@%SQsvu@{w7*{(j(22GHg>Jy-XoIn+r6PR~BJTUmV0P+=%xzW5XN zni$1G=QfPIi6n2T%_$w`JW8oDV$WP$T1*&^X>ubNA^^-$5{Ex3Dj}S$Kr_*j>#lqRcwr_N--D1_fyf{tM!2 z<(YateM24vTF$uHc~p=1@yp|X6zrwqX!j2$N;xXM-tzfm;^3ZTLM&sh|NrCNv)%W>wCa3Ut__1*IcQ&HZ*7EZ0|lWIWliwBQVi3r4Py98z~=TY7DdS znvpw1Tqj=n5ni0yz{9}#I9DQyr2Y-H%SB^@$(g>mKOJh(h{IlISmpE;qT%PUza)Y-(m4U z!nljlb%u9ipDQ+B3K#7?Q3sZ^Ou-GH!&p|iRW~;uIxwIU0PJrsc_K zaRo@LUml&6`qmXf>I9JnF9{BvnT}Em@LvpdWzEHoIZpc};=EAcz`$GO)bRhA&NpH%P0$nLZxB|D6q9wNT@tl&j@b=#0S&$y^$oKp)J z&V-@{90lcJmJZh9@$!2=q4v0)v&-QbVs=0N5Yo!K2GqIM(${NrpI`8C=J2JI{BgxT zoPF*XFjAf(M9zRFk5JyDU-WDVYDFLs4QMY^A{)$AgSo{%^E0f7Ds^9T zK?|NjEZN$^TKGs>n~Ej+*StC>U_uDs&TJG1c1yt1{H}z}k2}JAwq)B!X{cJ+tt;}( zf$`eXOr}khg-}jv@LhMlxpwhl&#YHv1(sIV+s@NARfUQuNUI?LoE`DPR=p6CDtZ-C z4Jic1zq?1sVaeda!G`^aK_2-a&*k?xB;hyEYuXUq*ld}9=-(eiY9;CCFj1^ln^EOx zPL1sW!nFjA=oS7giP+3Fen0!&eTxQNv(s%K#ozF=Sb{JK3)YT9q>ZviA2w`&+?7)H zxk7CD$%F&QJ5K}Rn(cY+*Vj2KrqAPqLC0*y8)dL?ECD!5pcdd3h--)|wLxkq9-ijz-zHHL@=P=7^gM{v-3xv^-5j*j zjIVro$IfgNkY3%$uDj^5cO}@~)I5sC`-X2jCErDhT(XQ_T(v#a-X6LzY3oF$7`nZ?@=o{}L?fhpL zgRPrr%GicgT~~j(On`zPOP5y9Offa5#c0V(`i2pd2yAf|rcTqf=7A|4(Vs6t4TBLU z_vbOID>eQfV@bw*Wq#NoCs{l#PnDeO?0@2H zhmWOaaB&d>XMarhqE&S2llfde*?rtJm2?p4K>TWI9(HEirCX`ler$K0v5EM()h%h$ za_2IKWWN*5*=mAD!US2^E$tX5SL`Kzr~`cF&~g85qtiO7nYX5@a_rHS^Y1I-n$gG0 zt=CL!I|lDHy5`s#yArScU*=N6M2E?Yi-5I9#RecQlC)Svn~UN{d7GjsA_IA13N*yW z;u%=+f)pa&gBO7gWR+jaSHwjd)lC-uinFc@Hl{?V8f(*Iq_O=&)f!d~4g&P6p*)q@ z{8p6K-6DI~R=iPCNKK*H0=oPnUVgJUFgk`bhMN3P6nX)U-|h+Zq-JXtg{Y262ht14 z)yZu%Og}1P%Gr>5*RO4zj2MWvV^(9g-6bWXf}sqLXfSdLcs+);9l*Wf)Gw4YRwTkO z&(U9nLVg8|Fu$c8gsgTyTwn*n&xHJE=uS*NBVy?q4zvAUv z+qU@)k?PDEE*gNUaHd)1Q*IdZMCJcm@kHJkXjjtn`?aRFnt=1#qBB<|MOZEJG1C}26b%AFdTI_WkJO5_Kd z%oxH#iv2v|6x?-_bQ4%IS`uM4S9Y@A=yxW=hI}iI^i71Z)T8;qnB-PWN*^!Fkv_>9 zM6}vx*idvc$SLTeffSM#P>NF7(gk|)Ye)|}5jjm^?p8tgmx-==Q7$%{bLTQ6L8+@G z#8b%~;9F9;Fk}6|IsG9{+N8gA&Hu=n&K*KgYqPVc@|7OF&Y5&OhjV!CAyqXEI^r_M zLUUBBU z@T1h$GVO-B%h`A36)fvfqrV#4>leMr9nQ}2lM)thxAjw*)szWL&@Z>|Pd9V41p^_^ zM-ez&B~vUOl5{fIN=?Ap)cPM89jfNmv47G&9%Dqi-7&F*#o?Gps1UN!8*#Ae|L7CX zZGi0k6%_!eLrq|p!s~`gnIV{oq=ZaT1FjthJ`K(NueYL1Q+Ug-R-E4mYUrjTgFBUY zWLdvH*esEU`O|#ta9Kz1*F{(ZXlf5m%?>tNPu zN5Q2!tLulN_^vgIREMt$`&VB765bo0m3Oam`-2GZ_ZS+%;-Mq&S!vLt`o?_6AhgSR zoX)X*2GuZQP~o(vO{LRw_O{&>HtII}={U$fBOpSa;MhDJelG1~%ck14P~5!zdYqu# z6&t)>pGT)gf>gb+$vWV9Bm(a(>`q`uE+8DuFXZ*dlMau)$UX?`;zWt3pz&H!E(kWQ zUzjVs%t3R>ia^;usn%)@uZ{-YxI{qJcF+0yQjySX7)|vUCPd?7M~D zIUL#Bq07+~j%vP!SL?l-|I8hqi1wbtg@&+HpK$+>hFv(nD^8ITHRlvYTz2UF3W?a` z9nEVnxqnDPa8*|6Bu9sx0>*}F#XakV%h1$+UXZ?6d>D^GEPLpW3n zbW@NOgv!RLC9lb@4&zM?YDpm?Pe^(uor6(hRMeHmyJWY>oh@D=jga|>;+gqH!+)|k zC*`6?Hp?uub2vByl_BVOHZqq-=>WBxY$9+XW5!M@0s+Ufx>L`r<#C03c|847QE(*y zF2_RB5LW8aPE_H1`{+K;VO@`BC@P@tB2qp6zwLKLh#I^n!;!qjbi zX{PAKHIXb-DZv*`(4O1svMsjlAjEX8?SW~6$#0n~g=s&Dkk#-OIqOev_PZihH+!Dq zuAUo87go1LI+)z^v)tL&g0zQN3@eeVcakbAyz4gA!b9 zJ~N=qkP8}G0wDi~UIOxjbWkL6R}2foZWE8`)?>xys!iMH?E+4=(#G}=Yl_zc3%W$Q zj{&6NcO?3I*5gQ`tC^_1>;qT!^YCx4_Q8Ck-$#q2JM`awr4RXT{-BsSey~UDgmyRJ zJDD}gNHL!|HVRQZAWeXv+k_)LC$$ANt*lyVU>1lb7Qm>seH?ReeL?0;_2Bg*I8Ru$ zjgyP}HjxHKy=2v|^RNXbK2DIRR^)lNvYa&DuFjIi*EY}0EL=$AhW=dcMenbJWFo-5 zq4_>Xpha0(X-bX#k2@G{(+H-y<$IYu1*PnCu@;bf_~S#*5=sfQFOuIfXZuL9Wl{K( z*OSQ&6YmuoEZu=CE)_VWf=ceHoSC{M8~A9Ce7I7rvp6?5Kwu6bwH$Ex=Z^8ne#8$; z%hZ+fdvZuNj!Lj4A@3vyaB~j-HkbnIw)kM$C<9^u;B-`)Rc{%DQ6ZJ7p+TwBF%}2w zB7L~h{fQDN|@==fB`ha(@wyt=j6|alRgn%*9H_70Y*1?G<~5MSj!-SfVwq+1x_jkeKCz6+rCR z5u?2S%3|vd;-o{iKol)>IpF!D#tYYxniJ!(*U|g$IZ>{eC0bLz!nu2d@yqw1Ds8r%m{CFMWiJ#!uxZET3g$*MSZ@) zV+vI@4&2k61RBVBYDg$MsvcsQbAfXru$S2lrO?+CFsU|GhTm+E1QQdn>EwjM3Hh8l z8yg3A)YS8T*%K@}bRfKZ7>)0IjkOW9_jMM5{G)_YsRvJz%r9YN zvLskp-@|X-Ga;Ve|1~qNWK&w@PRN@9e$3;UvrwRgt?yYH6lGk0mrt)6twd@BwF=yXrwo5vu1jabZpD)o&$LWGfnY0wR`8_OPk*QE#CLXoIb zTre~FkX_KmrUY+>(b_kUn94|4T@yG( zCTPbaz{kF`!p|xcVHdQ$sYOA35k~|yxfALYM+7W`kPvN)+c? z`7$i}Z@{J2Z}O>VD>|E5jh^qmi&oG{cLXy+p1J!1qKnNIam8r>*I24GJIDLS$=$zl zuFr4(?rh00`8J|AVND&m)6^U?jUU)I=Rg<&3PU!v!9#D<0+1#MG@!<}P=>kV)!1`(Qr+C!`G|4hA{( zCJD*$N1m&{=gp(K!JNB&z*Hc*yrEiXhFn0x8#mpqf#&UFIV_%jzp(4qO`9Ez3UGonQP6se;&~=iy z{f0$KNDN*Xn_F@Qm zXLHslT_%#_Mn3~g!lZ4qi@WjybxtF-^^{_4%WxtT^o%gh_g@)19$CcM5i&VR^4QT{ zkiq2h*k(=hDKELRvc;v>swl#PgQR*|6u^ev;_KQHfDlY<1_GEi7<67ngQQWZn5UKB z{QhzDOimb>35SI0v0k^`pq7AftPN7yc@i>JCqF)>DF&`?_*Lb}z-bCjWh$C;l8vWPOfq^4WI8T(lbK;FW^pJ@@2`tz z>6QLjAs^&_BUq*wEDXJU=9^2A#*gy4Evd$7f82579J&YvvhK0+C@u-qV<+@d_K~&@ z3KPxK)UxRu4M*FO9h;a*?s_@%I+NyE1=RJImOlmB5m-=NcwRH+C`X{_kKvg!ae_~a zG+v&v?YzpRd@Nq4GdrsFh!~vY0dK*?z1|9gv!XJ~@Uhtba7q0+%{QnV5;t<|9ypO1 zdfS9qoNEB!+3n84_e^Yw;`hLS?z3cdWrUG`M>xUd4c!|;s3HK^5e+8P0~O8J)?24! z6}S-%2!G;Xcupq|n2>Ht7k~NjXYA4fs?9|5fH~jB9%a!j=I|uPyq(*m6 zC-i3=Z{L;`&Xe{rT}q7u=t*7&kwLp9>JePUbi=nP8J^P8`X(K>`O32H!rb|oMTA_~ zM7`TNjXcS?v<<8i(R_Uk)3C{9Tl}HohX6&KeeEZC_jZlV_TJrCL8fFjKsyI1<$9Uy z{j(C66%rc0;2&gwpin=T*rGUmIGYg8#sw&L^OQ865K!mh7uwME;#8x1;k^xRjm+Ed zXU-X^3APENk;zzw^e5uD)Ud;sR*s$JuanG1W9#~#sbGb-W}unyXb>3{j}EO&rgad;0GcYFCkk6K?NPEL zQUnz#*CXAU2tIkm-_df7X~rlnk2BnpswLfS+oLeXHWz`9ts;w;{xiYoMToaUDkn)j zkjTd4r}9_;1DwF=b=|~~-EbU59u>M@VR}%9cae4$x?w*ot3jHolvZT2kCQjh$^*^O z$}DobGrV|_N4js2H?EFM9G+gxbq?NSu-0u4f(i#5rgH#S3{jm~vl&}qP&K*P%1we- ztYB|R*uryf_Ggd#mGFB~Rc#Gi&E1M0wZT}q0htLN%^FhN@3^k4Sb%X$L?9O(v_fh@ z)^TJX<9;1({poksBZY2R37J8U4B=jxt5rPm(#tJZGRq2mFXJFxewV$xnRoP2lMGRm z7q9?RwPmZ|_*7?sNVSwKR)SdYqS2l*Dvg{CeP)>i4f<3xFv}1MK z9e>MJ5gzyI_4++0-f+%4ay`;g_Tfs^`;!{5?FT%hfu(C}mF4W-w>({xvd(hI$SbG2 z3DJ!dJ9*+J)v!5qKcsqW*93I3Tiodx{|7wFo0diiE9OSLH-qIxC_$TJ9&1L93BJsy zfa4qXes5n>v7Ro3Qy47m)96bLA+$@|L=`ZOo56Co(*_vazTeSAXED-;KXOYay+vZx zD|K#Yzj?sroNy|h?{d2-@~6OKdx0pZ&p_IFKODEv zMee*S&GOKC28Af(O95tg6uw*`Mex&b_|`X7WaH?z|6adBB5{#@aou*-UCGdU{l{Q+ zP+{Xdf~K#75bUGpx`AfnPW4Bp>!SZn21oVnSSswWp0)@?A?|Hg@3EEuStMemB%^Va z!M-H97SfXNL5`1yNx)2HC7@iavWN>pM#(WSHe<>a!AT*xt0?WlRsibY7r;q8^jYgX z;7B2~RFqZZ%#XV8jsD+Xmhx15Z_I4s|Jx3dh6IU`k)9;p9S}I|xQc-)kGQUva9siz?L(dmDqe_P zxA}Dz&3Li2mz?b5lk_;f7I!}SFd{*MCQCMLqVmPv{M#_i7paumbjmka>t zOs7T}J-xsN6<9l2JiPVQp9E?~>|#Q__z{Bg2dC|j`*2<4BNck?et${(F4-_QEjiYr zHww99)4vU)a~GjUh!>uN&T6SBSruiGM0 zDmXZ%HEm|8U0U%hM3NUxsXg}1l=HU3UsuwFUy9%q%&?2EYZ(}`n*JO>(J+kjK3olh0b2k*(sgW zBYM@2wfk4dtsCQNLZX@Y`yub?IBXo=mR3^bnx6>#W!Z6~=?qLIqV}^;q1Fon-<0bS zmf+WScCnA5!#1s0UhxZBUDqE!@7?oho>oI)yd_z zZ#MzckyG_W=o%B1p&BoxyJ$jGx!5$S73Wy1~xxXt_k~%zi zqCsk9Ew79fK3^2ru%=-~5SX}SJ}eTsO7Cgz?OiI`i4`a?zHDlcp@#}2M!j?V;^m;T z9yO4`<@;Yk+#1VcUOezShpL2OJF`u;{_vZ6oG6qeyIT83W=bYrSU_I14Ol^7Yt=fd zO82RBx?pvnHHpJ4EBCBycN=^WH@4`V4Msj^7>;V?L+W%f?d{n-*mnQ=J#2PXS(Kp6 zHZjy}P$Cxw24kD(mmnz(fYDHpx%vDC=Q~nxJV63Dia==^`|&x47a}*{f7R%gRZF(31tZO>c|gL`oOf*D1qsZ+#UGy8Y6 zwZ6~)QW9svyo=V{DVn)rbs8q5YTFizQT})neL>Ns4AuJv>Sz{I?&PWgDWcKUZY{cp zEV`x)MK&bi6qwKQIOsprA_xu~dORv>fXY;OiA(FSsDXzzcCJ`8oHRL)>lwTx9FFFt z>Z)slBk=_WQPZn$%O;9P@q)FYrUr~sXcpL=x~=sz{NAA2=#1zEEgua-hcAiUl&d=JqbWN+83ke}3(nY;z7-7(l*PUC zdIAemfb_>sapb3IHz5IkJAO_oR!#`zmD)qPbX?})TWS=B+vCg2U(JW!A9F)esSVUO zx4|sU$FUW#Q;~xF|Foqg_xpY4A^^+qWjVuziPUqV^{erbdeB(s06>`qqYA{w)G@S= z&FkFW{fVHp*isn&Y`9+gWoDJIGfZir&bWlyCBFaQrZhzapw>1^oav*mP$_?~x{uX% z(gRscUme_RH?1o2iAQr`;e0DvA7tymt5-o@;S4JzPSecp!X2W*4+B=zX(UuoZJ6`0>#J zJk|BugOc`dC@@xmQyODF(Sg+jE>Okl z7Q|C`s`m+C=U4w#%~>s(0Re{fVb$0iy}qenhu2r2vZ)KWPI`DEQD|?>^$Cug1PdcW z853ZokOi!hVz(C;EOdTGv8{#Wb&;>blLl#XG$zn{J86@+iJ8zlc-{R16~y)!PFMdV zCqvEB^aX~*tUo| zbVclwQ{yh^F^4Du?hK(aq`TvI)#RRV6(mcKt#i2gn^2}tao{RpvHQ$D#ck2h;#(Tc z>zRqLHhfYR%@|i$p$+L1{>o~v49@O%7MYPP3$aat>%sc@2D$e(*R(D=cQPb@?z|H7 z*>CVeK8K{Rvk~wMNcXE35Y$6|$vC>HCV4VjJ)^k#P|RVG@}Nv^`}GsIkv3=s))$-z za-1yhj!tUU7U(+j}kV%pGWHMeaz_2A~x93%2Cl~}mOgTl-gcWd4HplbKglp_xf z4dX(7?qk1gY69)Pn3omz-x|86(`#)1#_6w(2p&6e#>)?K2}O#q#!yBU_05N(5-O?S zQeV^DTN~-EbRp3kEj5V2+mY;;;cI_q+!GG5ICf3h+NyJDmPBQxTr}571z;;KsU19Y zKE4x)cPyRGk&mQZS~}SC{^e>!42k9h)<$%+N;zsT@KnEQC)K-X%p5G6F zI$gsnBXn98gtDTj_Rq3Hy>Mf+_qEdrK^GRJc+C6%=3}#7z5%HQfVcxG!bW1ltJVF# zOUYVuS4mP&W7YQNPwOi5d@R<-`M0yLooKCBvC@a-~uLF)RAra>$(GfRdbFM#hOOV{Gb> zA}Gz1eZIw6Q}+``{Y|G&RG+I5MZkILI1C1!6E;pv80l(7{3@n08la%5)v{T zubyPQTobdO@8?x6RMKDEFSFyc%&>)*4P+F}XQMEzGY4o-W0xaxf&eaN0}_^*g|+=fzGR5l}o zz}wI&RG!|6Z#d9{%kz{Y1jZiDJ+CBS09Z02YUC23B7Zq-;$M8rPptmmXfCr(9!?Kp zP2y=xvl|)yCZkFJv8-(i{GzbkdXRh>lJ?dht~{AL`9?U0El1UznNn?O_qAyl8A1;V zT@j^s=;CES<&VP(=Rl1?r@Bh<;9k=wghRrXt6#(VLnFvDb6VPq|8z76Ti$ z)}f!pvjny70>%XEJBo(+;MkzyP^3iGxm$@ulHb;$yBDSyDn#*~*Pg&gO!<&;^{OzOQhfNCm;Ymx&lZ(@o z=CHjYAsBE!MWHP|4u8bVM3gh3m$-pK{Sw1RDmc;V3*%=ZhS0 z;xskcft|6`V+ID@HAWn{AP^s$785cKA>-j|5TrYpmLL*3CQhJ65DcYaRIn(4BAYDynKCYDCJw^h*_IU*ntuAnkBK1PpR8hN?RXy#T(t5m1UcDcvZP#3!F?*}rRn|0ARNfes0W zjB|+Q73?5A`1!g_*j+mA+!P2Pl8}T4qzFg!;m0^6`_2vq0;0zkQz;zB>a3wX)uNtP zWJXq?qb1mSXH77|AuEhbl0yDci3cixfXtc2|kkXBwTJrm<5 z>qY|ccl*eHzKjKU+M5NDYY%WbKl$y-n@gynrDIDg&jkyZU?>IO=Wg~nv=M7cHq^k z+^^uhftxo$FB43B%@or~S{MDAT>EbtGjy;+PbaTQ-30c+%r z9+yjAa8iahD+xj>jV&P{&?0|84{-|@ftP))5aEYWmq_|%sr2D3c}B9_M>nPtecT-m z!VKp8T?D+3#+7A8kQ)-{E6#A&wVWZl$R!0A^STuE;#vyhvs@8ia-t-I46MM(8e&<} zJgwmL`!yEq%JHtLXEJxQe$_`}Z`x)*Y7Z6rD~?S51)ER1@MRmeuPRql^*SZsMFB01 zNzW__Bd3n4k?W^0I=>(YkBB!9KoGJZ$bgt}V#+je(B0ARDsS`s852y( zZW3aKy+=vA&CPyB%JKVAIrWZj^z-Nq>&w+^hWOX8j9-%VaOnd*5&?~Ks6P;DkKXo2 z(hSC27?reDoNSmH)c|S6uq_YOHr3^8<_?O(t^%7#`zSM|mSkaKgFpfWI#yE8%edpv2KQ@9`XO`dR5Y^5s4dM)!10y;3 zc~hL0^Q2a^4)C)vIFZ|51YEK)o2#Efy?D5eh>W<|Pq))4q2>eZ;f6n}wDWn->HYSL z&TuylR=vG;c&Zl}5$4w*uF#2tNMln1V8-=j`_d^K&}m>fQ~YFM#+e9NwsrfsUTZmG zNI3&r)-ad^FE-VwW=S{AG8tIXYO&t=^7wuNOQDMh>(|?ZRtaVA*$n+z6|xx}_6(8t z<`UXTmp!dSFsML15RPSzt_`o2BgLugAX>qZT^$k11{s*^C(x_JrnwMI7(}<^yVLF& zu@n9Z-1kC0wAoQRame*KIwiH{JW`WJ6q-0Q^$m-|<%TXGm4BI@gLmen;MU(h*@H&G zzfI;Os7*sWDNpC9*=h$i;OSW~AD@BXryS#l%81z7Fx#udw6JD|^?+9$C?LL3W#GLw zuysx_iGJ+vz)!r#^te^W&3ZlEh4?mG7d%b3r)VIdWHnok>kmq4V?n*JR?icK%Abs( zR@5Zfz)MEv8@7{^I2$Xg8;0GUr#mUn+kGVDmkSEH{M1AB*0h$$lWEr=25VkWhZol2 z>OIdHOq|?`uuA2SMZhEWT%gErPZ`JY#!Fl4!M1NHtN^!uK9A31?3|9Uk0TtEhV9;e zf>S?a)b7D~MLVFt0zd_%9#-I-Kmvq`7x9d7jo=~Q!>f$q8@6N?$S zREXU5LzLzw5z=#1#L4r!cSf~ub@yEu!6>nivs~!D0V6n^g53t6x5GBXGqCy)>@$bY z!GCGi?ynORkCm-eNL1V@mgjG+@>M{x13vymWQga#eylHQ+P4?#=&VIWsjoI6#1)1n zmn^Vw)OIpzEYgJ$$Z+w-H#+=~{IP~3cPl}xc8PaJ;i}#*GE_)^VL)=nCBb(zpFB;) z>WH{xD-aYQukH-bElj}}ZZwyOYsiI`WM!B$p7s1n($mAThc!DxfO}iO)FtikPRmq7 zXvAv+x1s5Jj!Jy1x2nFLYI}g?Pk7z;!tH+d<8yE8(XB^)#|Ng@;|VIZrDkWI-{lxe zmXsh=-M7lHf3SNa*U76OFmTaJT;bG~J)~@-_@MOu%L_nChOuty7FFbB)(mg?EmnBz zprLbHM`v%hqQrNK1*>>E&4`vuPF|Fa2RtR6X6v8u+w?2(^>WekmmML|v$nto z%8K<3+3C6AL`>HcT&s7491(AS>7{8)i!Ecw7&ZE|N};K(1vxtuqa1Z$K}F`y=I#d0 z$}@<(`2f^M-s$mznbKZ8BL#lh;R)Z?8S6gC!KejZeQB}L6RyYq{=M1}ZKDYnVKDHK zjr1^5B00Iu_Ld5$)7A`pzx^G{wB0&gBJer8C+rCtR9{`)om_|AU>#heK(MVIzZyYa z^q2?54!c=jSvoYBrA3K(zJe(EGWma>On7~fZ7wlN91E%)j0Aw)PSdkR(U=<0%2UN^ z-S*EfVjr2eBsST+#TfxOu|W;L9pEw~+CDUYS;X^G&6Tn1BDsF3IgG`5gYD5Cu4yVt3 z+!UGj`7zerf{m@yy1_Y`6+SMmm^jifoI_Nr`(TEub??!{>-vL@`pi0h&SCjbQpehhd>=v%ODv#oI{uwI2Sd=yen| zqd0Y<2YL-{;QC}1-P9XZ@oll$SvRD6Jmv{OVSX7@m&OOs9M`Wfa#Vargr5A%vR(vc zqw525dd7;KqD#3?NIGB_2-DrF4WdF zIprOGAat^j)3Ub|JrX^70vUPep7e1DblY8FGx>dt?m>8hMoG`I7<9C_f-N)+SWHd{ z@2YzlX~({w8CFOe?mraj;|p`P^>cjXnN3Y_xFux#4)mCQ6S%rw5%|n8LAhNY-op1= zy69F#l$f`6p1GAEd)YQdE740J&BMKUt9E)raVdUFzWRK7!aViyI{x8f$$p^3vO&m9 z1qNwJj1ybH@T!aL>`VL?vRQk|&AfZD&!?sBUTNja1>%A7ebF+4!?9nIW1QWHP~77Yedj;E|& zlHmZedg_xsEOSfm`rQ#q4Qd&Y{*~USTJB`h@4WZHVZB;aAjNN>S6&xjoBhwSpX%a6 zdi8VsLu?Oe_AA7YKz;A@Oi<;FH0hSBQj5*Rv?x%D3*`rDnYrA0HlW8gPOdE+BOVu! z#|~$gjY%f^ie=>D%fue`sc-UHw*{FqNG&kZE4Yg}3m%d@>R9iN|e42aoxkF zyA3|hIEL6xBR@CIfqgs-Rmjm7xvOMeK9+pLvd#3Aq0rN#yQux^V3L8{D!g6dvYQsc zgP`-T=8O6&(nBbwic;IqFu*Fptyi%VeB$Kgz1a!g4o~nc+0U41(OZDW#z7cbFRN4T z_`43V4LAd+>J!6?&+;D{V)>@@$Ovp?I3SzX4XwF zg8-^W*jx6EL>r~X7rE6#x7?S~PhJ~M-uaj?WApuL!}BqEVj|aKvkO{`3PDrGbj^%> zFWYL>k9gGX+C(5GKBsFUAk(XzCBia3rwyaqVvX1<~4HYeV3$7#NCOt&}w7LWDW_$Vuw5<9n0kD7*75_+WR0F*RPM<5R=udo%J~5 zBT^#9tOybQ50Lg@_n6|!aSre&lfpl)^{p8%j6)gU49({gnkUxN8Ri2M6vkV3HWKL) z(=%J_P_7R1Wy_yLghpm1p93+Y*#?Kx2jOzrND;S385AGSO;U3A>p`clD=1vS8)0mO zs4Y~HXIcPyY@v+X!%l}8h6S{2X8ACN3(v=yA&)gh@zqt&>W%BJVd2+8#ZyV@Zj-U> z6YYiLoS3*@H8X1{V&C4fM@0OdZ~3WE;4CEcUk03zywA72m_J>oU{I2lu6VankI22C z-)0GKfgItes$VVxU$Zff?39$@w;~Lt^Sq<*h!iF}V}}U*+h`|yv3`eFwJmMOqe@?5 zuV=)oX@=ul8Yy8Z3^G7}uON;9G*&`!-gD~UbWFdl>kZ|5WQ`ho|ML{mc8^yCg7!${ z=FPQ44gZ2i3B#_RAq+~wl@_?g&2U0HV?^(8ZvJXtEKkGt7$#YU|5WkShHXmy*T?M7 z56>9~*6F81$AAWWB>pULJKp{1FAM@@VR7n*wFU22RYmy4`e;5@MCS_h-lc~*3m=#; zRKa9xvAOA=a*B2hzz|*$sYHvXwMya1WUtF_!a9$MlKt4;7fHQ2SfD96VQ82otFX|@ zky)!Be_FJQa+;dDCrqz4H1rU~;1&vZwNNVysbo9>Eg4^4b6DHn?Hf-sst*1Qag#)( z?Sm9Xoc%t4jG8RU|L4f~e=5g>-?94t$4SJK;~+%FRMLCq8LlRLg2+-}DaXE+Y0bPq zU;G=Q{bz0jSitV@+O5RCDnjH~HTW=NbVdq)ig>?7OgiWgeX3|W^R{1je9ej*k9Y&t z({;s1Ma{oOLKjA*l1vPCACe}d#R*!l%xTyCNXJM{Oo~I{2kXRp^H@w6-T4u`vXdS1?%$F=qRu>ApG~&L!Gc8TH%{{Y>C=q zO##xKC+0ZM^HKQ3k1LG-eg03~`Tte6|G?~K3QfOOhH8inBA1U8H3#{~{6UNpxBf9J zgIDR>gi00)X4vo=h1Xu_`pv|Yn^H}VrSiGg{bOJn`4jXyiUgzgf_ziuuQEG?9AS8h zSndx;n7tV(G!z@3|0BPx!xPgs{kNQ9-aj&kpjAHyp#;PzHcOP@&XA)PYs;vX*MvC`JpyO0$ArR1Rp z(|VIUqEB1S(@b05586}R8=QdD-5Y4Ug?T4tR)La6>@OQZcr*pcLT_Rkx5sc;l_XhG zyk7rsf%?MTQ)spoN>DVex3NQrPx`;-mup=6?WIny7t?*^H3PLYS!txaaaFYi9aeOPx(DNBDS8QxXHuK;e>= zc6e1$!+oO!yI;eqlIFi8Fz@|m;}il=<-yP*=c~@CZ9zSJuHL?saBJk_Z(kU$t9QasBr#ws3Lo|1kHKUvVwZ_b|@j3_1aVyHAiH z!QGtynB^WsQ%_3o-&)w_&i z=pf?+<}A_{a@KJBbn8yv1rIzI^j}>Frq!ICtlymcVfxRi4F4-I`)gIVVK}uT9KK#D z@5SN~ESdQ_-)(R9@F-{Nn{;=NUH>@1j4^TV6kkGVhu-!?#O5%4A$K|~o^Ne#9uA+c zJ^Rz_zJlC+;m6wjYZMiDCwQ`hf}e*XG>B4q6u41YNhatO+BeE`lIMey$zO9mGuCKl zYpVHWF)o8}d68s80=3QGC+A|`@pNu$X&I@zx%&yRKK{Rf6n6N(34Vz36k9>|uS9lG zIUg6wren?`1-bnB$xp8%xtQBqeYMloMuG8?@#huI?mM!*E8W4|Mb-^UP0J{Q12&L4 znUM;2UV`o2|6A@j!vDA25eII}x|c3Cdxai$o*HWZdvh+!_Qp!!eJ{BhvSFsU295}#b;JyE(^9Z?={tVH~8Kwie- zapL8<6fQB~R>MeHCMxPs-1PAsl`CO1tEE(2ghdvohuIK+h+N`;{03`1#&Wy+LHX)N z2#zq+UN74$w@_@aOo{ z<=uWwkK!B5A4Q4Q;RZ*s9Xpq8KhaVvmTHUz8hWAEF8T@&`NV&Y0J7etGk;Db0$-vv z**`ctzTo=zTSc~yQ`nHWNL?C~AcA^r0Erw?A_(S0$7p0{)+X`q!gztW@eJzzr_Y6% z2AY?_6B;SJ!lLqMwrteFQ-X~n+$WK&*ICUf9J(J*z&5*?aY4cL^k4Iahg{VfbQef> z^!cR%U|wk4a4Ixb&(PDb=EBF_5p)?1C^j3lyyA1)W-GmruoV!M<74X@HH1fh+NdbmIbpM7<~@J zi-PFyY8f7Iz9P8F6};NBmrdO7LeIlGK;$kz^Vwj$0unX`vKVys(OriAZ}xhJ{_3w5 zB`c6zJgbxqn=Vd!=*cqWSNsgqnnJw^vl-i)E(4k6JqDvfT8U%9vcUu=0&DHco)^#P zJ{n{p<7sei^NvomV|;TSZcGWXZcVrC;z?#U??OzZd>%nY-5y7gC*HoQ7cU~&9E!YP zbQ@MNEHtO7-zYDF9PZh*%iaO2OLuZX?|FHh3__GBO{thFfZ?hPqgy=@DU+v#Cov}o zg}4xF(zppuY%cK#Nn|jkjT2gd`ymA4pl68iv=%Z#X$Glr* zh@Rh4SI}rLIY#U%rA@!RtR+q`IOCG8qvo>&BF_1TzI~*Ukc>hKy)z|Xi^tl%@Z$w%;!^(YC}&lGdT zFTLhLyE=}0IONox20B!}mw$5FXw$p7c?!^~*tW`53J&vj#&iZ zuh$WYKgEfqSrJhP8allZjvAu8M{q`2jKO-W1j}RelAen1R|@Yld`;4~PE3i4;>hZ0 zWimYWcGh6KXfs}jMf3?96??f@*PcC;ETu2Xk+RyM;W;(FKqgI__02JmcmGP zi_npO;Y*ci+0Ig(dlI6ALggnM28|DGcdc)m|G&TV0^YydoY`PGHN|JS%+h2pb<0xu zNMq<5ND6Y^&hIvV4IkE@l;%?nV4G3l0qbdFK`bxT!1qy;AHwIcUx)YsoJ$8KT4#fY zmuL~bG&Cteqvgp!ynX{p!0!2?a0~B9Qf0__+)+jkqpW(O0bdSH4!*@zV64ulw}(8F z$?y8d-}nr-lRACTjr`$eGK6}aHxXJdyoR8Z_7UEadawz`zYG5QzbNH&*^9J4TcGBB zjsAABGbF4G4LCr;iLQ*_hs2OJbc#gf0MT2gg`y+^)hWI(R%i}>ws18hWplmT#xg|l zdj=QRI0m*KxohKKT6ul}qhtbJ3Qo-VPVMgCTMBnF>$9K9ZQV{ErkRrRx0J&2!3)U) z38BWgtdSrr`_oRH_7b98*a0*T6wfbY%}1Z$oMV+GOH4f%j8EnMTFJ(GF`m5o-@(m4 z?UVjbA6oxz>`IT7f0Rn;7Fq@<5Kj)JGQr2>KE0pRa5|bZTRoUL$8+cwqxSSs&RKsa zYji9?(DQwVtT9z3F1+&@WO733BpC<-W+@z3zx0?G<7>kDZaQRVo)Qr7yO--1!+5aK z>PoWf5`XTrOADz71x4FK2ZEztd}Kt}i4fUZN2(eoX2&O7gfYwdcW+75nNR9$fO=&A zh*`bB&qMLQ?zUG=r>9TIc(+sNgq?)CBr+Y4Cn%7xm^Wc|;Z$IEDIPag80X`gSD)5p z6HkG(G6O*T2zu_|#ngP#^R^_0yDiVvfK87saE*>c0(}95b}-w(lC3;?#chKOpXDgm!zMA@#5WZO=-&`-@vIGd@9DK`_Ilb5|ioX420C^^jG zL}(81DO3Vnzh(Dx=)WE2p@7xo?^pe9b3x=peZN;@G$<){&qI}@mqRUD?X=8qNTKDs z2lTec(=qsc>KmT$1uAbTiWwmwi9g7smOLh=rKcSGE<8xadwuXIyAQ>VtMrX{62jOk z(-_Q8xg~oGGpXBa=sPurt1TCYbQ3&l$i_bbDQc8Ry_CwOd5L9+}uC?Wx`pP5B zU9mN8Cl^krviR0J!C%>O>J9^O(Xf`hGtWnYU8c12qX1dC-Dy+fDYx-f#VQ030w7 z6_*rY$8tL|25@t*liP9I74RIe8eoShQg)W|Cc^1`mmy+F5PEBSkQpD^BEk#HS0AI3 zZ^ovLImT}j%Q{E+w3~r5!n3xs)R|)hF9eqjW+RH(%DX+D@-nplQkaud`ir*wp#)2x zlP+Xrqe`_*9V!0%cs+4zgX!InSe`|k5vmY6+29)!%<2XhsEnHvkZ>p87Pdn_p>oo- zb#a&S+ustSq{_U?YO)sPlJ4AG%NhT6kIVO+<(rI`KmSSAfSWdKhc8Vf&s;oR-KuDG z&J0qYRHJVcx(rmY-0>PdZ2a2KC%xx_%EUPFF`8pC zGHLdN2PU~GN&%@81Vmp6<+SN^`*=}^bn{$Hq>lxX=Yh=jS1@0j0o=pbkSVCg#9NHa z&=tl7{5`fB6Xq`Tpf6%SAdVE|oB?F)4k9zg?$tu`*Cf;e{9-DU%5l#2&bf0KGIw`d zEcb;S7`~jnSdSUn*M1!+aC@QpXdB7 z3e=~{)h8Z95>lCpD3DE6vvDYhMxF1Y){h-mjK>|vuXLY72h#!_yM06Z%;h>(H$Jm4 zq4JmqDyua>n$eDeyBn8}$0nB=Wdcs7S@dTZi2J+I?`=!t&((j5Sy=FC>RzWJ=g$u? zr$bxo!TXDaHBxphZ9fm{Cz$!Y9?knR-Ly6P>O?8!1+-8Q$t7y-E<#50&c}t@PbWZM z;qUiXuuF~jMiS1Pw`+f`$#(QXkOV=h1-OKIe7-T5n&6{E-L+A}yyu>1j{PvfY5mT7 z{`o+ac`#1EQCfHL>Ox2SnSiLK>Bub|K+i2MEWYEDW8H#+FDo4#&Z-`~>^4efx~V?}+7obSMsN=wqi^6^x>yCg2;qzV8IF@lCcQ_i%aNHlQpJe(1#$wV5+gZnU(J=cQlO{CqBi z=~U6r2jf?8cMN(v61=BCarVjnA`6wiwW#>d^q#fAi`Y*B<=!W+1IS_cX!? zRf5f@7h}-=_{!9TIl4mZWB zL}5Sy7GwL_{VprqxL=Wri(L(fd%D3no_~wMr-#E=6{Y{2U0)!z1Ug&~oRZyFk?n_s z%=NYL_76xkF?#i(H@;^-R{~$&oS0acCO)6<6W@2xeDBklD17^MEleTLrT>BcomeC_ z<0oU|eAc}t!A|X2qzMUFo{&>un%2)+M1eg25y=iIYgT1c{p_wG3|P=;o^ut6lh;Jw zeUwm`xjq7wv}C5A*z`RJgWW3*4ue=^n_Hd}895cyW7Z}N9-cSk9}@BZE|7!tR9%m~ zy7G3QzFo!go>#++Eh_y>xrACQCAnoNGm@_(oChXF*s*W?NsN1|t5ptRL{;E*5f96fTktW>hBX8w}3*(ll={E%qC)r7yH`j*qz@s$DZ>t^{T5sXO< zK8)^g@TWxq@T{_O0ZJ7^mxqqs>&O;+{c`A>?dw zuCKJp4*y$uUlJBwRBk2gzop3m^g?x>ymAV{`(r}-WmD-^rvE)hn}xeFABfkH5l zAGkToe3GvMpU)!A9z|+=%kyF=RUaSB3ck9pQI@u2ki9o|G@zj}ZjJ|NlD{*?5VOtO zZoMvJu11I;>?RK+84(ADfs(Qm3wol0(Qv8Z0j`Zr(e)35r%xA_{4Q4--36(`4ryOp zZ~q{e>!^FLwoZ%2s?H}gC|c^1iuF$%u&rnRjzAX^GYIF{K0c%9_DZiMt$d!{nWxaP z&_Q^R_+gFJdB{=+l%mpAm52zE!ngk6VSSRb){&$^06stO4NOBLqcldle2Bg+gXnQ; zA(sRqcCYJK@vB~yW4-iEi^W%lQ%vez;%mpC0U3cESi?<=U~7KJpwPsHlXEG3gUi?F zFNA~lo>Vu{yP@qJzGG+vp>sX8%|3ZKGbnB5nlnNX1Q?W+`DS}4D!$V zi#<=yj>fwW9ij0sRKoArs$ddzMV4bz^-`(D4MsglGaUr(v2$_) z?c{BEEf|B}A~-rG6pn#cb1YRRgK2zp(sSom%O!|5{&5q$xY*c>^L6U9-UcUMgv%5mpIuD@K>KD%BS_NhJ_r6pC`UU8ho`K8 zLAs=Adp1%|SSS6fJyS2(CQO?Jg%`{FN~}jM31xDOh|8wVrFDR--i(+SC`A_PU9ll|zYqZTG6< zK3*G0czxn%vhy(+!9BFWzKr^^S-eWAhELAba%N2n%?2&sP%A$IPP#5E2n4fo5oZeB zjigc)7*z-}chnr2D78mXMb)*JAVichITJRH#S8B(OVrk+))Iojv90bX`e5p0GmtOz z1$f*{0zuCQh#L3Nb58WkIk>Oh#)*|Vh4t9NyqvJB2rKCA`qrG#No;%Js)ABoC9gEy zMaj~83r*1sDW%aPaj9(JmMr{#`|r8Oi*lkp04>~Ps${vUTBqExd}*+i zDD70_2#b02(**`DVvdKL-h&(Mx*3$78XFxGTy#W6WxormQ&wdO1&Lu1iPBUABm2^* z;+VepkUVQ-GXkOt6Mz!zP*7`!m*)@rdx6~mmq$8f&mI1ez$g1+Loz5b_bVpRB1S~Gs2%;KIk5@%SFO#CyX-+xO!#x+7*A;ibNuh#m2|HpLIxvJJq{37&kTwNh zE8XdnnO2@eAQN&aVm4Z+jVM8Qfh6J_nh6v{STH#=>ag~TMdAJ_N_*6WQZt*kH&3uB ze2=5_D!ze>{V#B4?4+{xo4?qm#b_H+gH_I0v^h130I-S+iFz2sK%kZ|AZ3`D8JN*l zJWr0G%f%I=hnuIbBV~4#Tcp7=yFOKdC7umxG_bdkkrj>o9vj`g(7fsc4Hc$?7M>eY< zlHCp>*8)~Xq$TQ)ea4rV#*DK0tE`o{eRhgj5IPX^7Od03&)I4DEO; z%#mdk!))na3TdTgOY&lapyf!qF>g-y%KMJ=`53d!>s#vf1+r0}Y%NusAII3n ze##17qZu%g0LMU#*@fh^`AP<;=;3cEiG0hPP<#!hj|oB{TqcxbW2b`)v>qOr&$}r} zkB-9Ko!jp!06poOrd(0Hm1XFrgyljT!j!@#_C#SM1E6S0cs-~t&=;@D&+IiOyl!#v z+cK+W1!kj-V3#I4uD4~kM_u^(9WAG-k-q)6$2LQs?Gd78rbubH zB!^%`TG<2FWK%=>jMnw%?3+4>kD#KS^ggKJ0^pUa*`r)~q?eoYx z&yW`swTqoi@T%IlEEc<~6emssY_Lp!RM=B%{LEjU#Ro(n+qsaxN^amJFg?|Z1uFEb z>nXSq*~(W1OEBgXY7exUWEj5L4ufCALXu-=uT+#ZHY&Sa>_H?mD)!m3o)GbTJj|GOw@~A(hC}6=Y1v#@YxF%$_Q5%Mk5g7IAb3kELk9_*#!LQCMV2XCDui)PJ8zK zRY5Qh$_yS#?!eu#O=XYKudng*$1CeI=`?eG1?XSQPuEWefj|_*_+}z^@l`LBVwZ8v z?*k+$2sCW+?{OuRFs&&LnpihN$Q`k%Yky^IhzQg<{)j7E0TICSDB-k6@b18oV&T0D zi(c@^)Ke|X$CWukEHgI6+p1y#P_^kRAW|VY#SMtXI3w|2BFTAX#Q)7 z1}Yx^;-TK{?lETVSFg~hKuMX1;Vz3T-D$B|OFLimE}yiEx*3VX`SIKj>tjlG3HM$) zCUOmcxrV?#Mn!ojTdEa;aG_(W^MiY=&*}_0U&;^jONf7a%Kqu3ufEyUW$*R&ZjA)_~@z)q_OR#B7{!!?e7)~|m zByGboo;o5#A||2CwzOFiJ~mTbJ;zw`3^;>GZyMVp4?n3&!zv(?7m*TNVO~~H0DiMV zQYvBmPSY*%GHhYR$pA-+rYjqZ*1A3m|Q?Bbz7Wdl2yBE zYG)VZ;#xmG&+5nbUp@*?J!8Sj2&)L)vrMer@*?Pp<{fF z!kmqqE+DH75Z&k%Hr?`N*5*M)(<@Ne6Y zO;iwPMCI0!^M6xutU3Dl{cj9U-{|u{_c9Cv=4P^oP5ppN9R=)(!UHm5j!9likDILF ztHT19v$i3l>bC+su`NnWI-%V#cP0Rp(-#9=BIdy;+PPT-d)xHvtgW=6l3O*;xDDvT z0nEQrR_nLfbmyw5>Hxn28DleWP&s$4E9<2hT84-gNn9)%Dlsa7dn{qGS(tz%u0-0m z&V$o($REgSkv6}ytYJ-wbGSLO4?(pmZns~YJGJAeEXX6H)+4AFKnpn8qDY}G4eL%LCWK4g`so$5_|hY#tTFrBjU2o!l|D?+fT6Y%>oYblbj zuPDu9%7)C(`JB?%gg^z(pdghR8f5C;=JxHA@$1|9$_O?Iv19u+%Pw6UMG`|#HhloW zWTBXK8y=X)GpL0)CE&NvTgaMaBL)szZD#uBTtDB1_HL{@NKyH8uR2RVE605M7}F05 zRYBhMrND?Gi2=o6f4fCr6j5ZCYk#^9Oojb;u}anPAz-rQAIr=gqItA+Qv_m|*U8lKQ7Xpze{WEwmY9Udo6 zwEl0r#QuKKmINzhpd(2m~jNOlj^# zZK>~pyvz$$w?62Vg|L_6-Q1HUg2&?Gol68u;Yx3m-#FilYN~6ayW5SN_pcp@E`5Pi z(TF@e?n@k;9!)c3wEIL85r!dFYZQ9mYwSwSl5d0voUg8AY*Exq;_OqOZiJENaX_DY z@_AQX>=)hh7~vUGdqsDz`4;z2Us`VDrLQH0A=d;xwm5bbqXuG6ApZ%M!I?S>0-LGx zmm>fQYGU>6$50XD_!9mz=Q0?jTwDn8bw;jigVh3_!9K z^2xO7mNm8vZDYpMiZYw;G(I1%_yD>zxbujJiQ^$^FGgF+rxW;vY0an>s%AR#a3b)yaZpXYKO zws$z|9Q{*6=HC3ap+(P+BM74-$=BXej?j0admfMHsA+pGJOP`=SgO;airK7;(<5=BA_fO>Xot@!@G5yQ^6S0SU zkFrq>iKXYsO`Gwoh)V!z>>KlojK9Y@`F*9=c%PDH1nuHf5d5e(eQSgW;jscNUj*gF zlh*v|8mjaeDu0MBcjGAO*2)1q_znysqSmz}odS_)e}* zeZTbEcqZ9R2=h5~jhYF{OeRzVEC9s94%|1{mVQ$-+(PQN{nkeM{l^qlTp1xYq;+8o z{hCv+%<{x-y8X`yzFw>#!>t%nsb`~ht&nW!f0g+h2rgg7Bb3C%OM;`9OvPi@7L%Ny zq$Hw47EZUwl}Z%#R$vt&8WQQ^`cVoC)9O#e5k-LxwF`~Z6FH`COysEqo73=?;}DEd zlQnINtLCNq!%%A#7J{ZjoK$5Okndv8|iv|RZ zT#wh_{{(m*tGtdaYU2VM^OQiRw;jui1n}x5HC%?0J6Y&%l*xaizGpuj(~rk^cX&?f z;(U*y$N+z7zUD|qx^em)Gf7QdysXGC2)CY(D0a(X$i=my?*xwG9c6pvO7a0p1U(e};zW`Jt(2{_>~p+Un)C;KQS9zNO4H zJ*bGEv9}axOiw;sOKyo$9=(&Zq{vaeCThzQ1>rBK8CEy1py z-e_Jwi}Dy^jI$n0(16@mKNAZ+z_)G)y{~*Q_^=byCuW_`f&k2Pkqe^i4c1+0cl{dJ zLiAqr&t`O%=2WkIkQVH%(rr?K^bmTEe_m20_xUqY(E)leU|V=7%fzdeLP|{z;n$*i zcH7CbRAx${T=FGn?I)2o&-j20q=@FQ*Prt|I=+9=yCjGqsN&u%51L6)s2`kg8Stxp zA2>3dFjF}OfiVv9X(Deden&O&xPn{I%E+bQ3>^GT7#RzrrA{wv@+%@F$KG0C#GJ^? zE~;P-zQL`ey*K5AOl6Y_pob4K@Snj-bXPKQXaZ+HbW}tK$8THQaU3siig@3=z8iAw zl5wa^(T8ut_@2U4Oc41ku@8^h!kXe_DY32L2PzHw!;ivn&WaMRj(4HcDrSxpm7&e)FYCH40VB4{e>o0P6D-X}Z{R1Jq!foF1J{n1^ z{i3Cx2Za)mc+J#pHqxaBJEA)Qip$-7D-EIt4(*dfxS{gNK48vY%&o7I#<)a;hus3O z^{z71B>;&9;IpRQzzYpJYH_uz$odjqs+1HPyzC07CjA>dg56t!<*W|JL(A)<($_tM zGa5FG4&IcIvbxqtIfvyOAE)N|O`F%dblR!cm@~pa-*mf>3)zpK2WK- z!w~jX=&%zhn6Sv}wxPcbVWw#CUUUMGIU*ktKJ|ss?ec_hspiA?CaZ9Gsj43C$Z~Q_ zaTfHajLDXt-V?-uG_cU125w0T@XLWu0J;qcyc;eP_2Ekh7AO57?9_}KfhackR@TJi z`IdanL$U4B7fwlNqpIYH*lsh<=z~M&+FaM{=o<#cF3?+2TF}rGPVJ+$aBIKxZ76y4}Z?z;FwT2`>sVS5 z-r!D3g`K63_@~_?NDoMkht6a#dPXF9I_z!%^>ucPyx;{ri4W9I*b9w0V+B>z%(CuB zWiAZF0ydf(J5^DaAIVVl?Cq)|Lr82fCsAX-NRg6}Wheqta%3}yk-5&6KpG{H#$+$% zSf>w?l+xLHesShujcVKj{@g%@aw9S#Yr&7gDUQM(=5wO&Z%@Q&`l}D~Ev&9nfMFBZ zb>oCh5;?rhJP@q{X>kBZ?~O&61l9Q{?7%Ec_mzVA0X;8pxV$uL!HxCunC&zL5jBDI ztyQzGrdEo4`787dIrt?m{4e}AoJ5pA2qmhBpOk$7 zx1PjRNB-ZO71Q6G)n6M>gMWVj|4>*;!>I=H&Cyp;gGIcdm%kZfGbHqC)0iOFyoJ+F zL@F7ZaQ)QrWpl?f6=Tu=&lcO*YkpZfo&5o1P3k#7PhSyAH&GazVnhkK7E6v9^)2Xh z4ove&s-p&$ekC$KfOaPv+y57qUn46aeeb0cbBhP9)%A-1~}l`bw%WlHOibh}b&% zD={^!QEG7-_1XLM%~=%Gba`v3d|W`mtu&n+dkrmZ2Bfro9`$y2P|0wmF%9xd!dHaJ zlU`M(*qoZ<)#zofeTg(^^Z=UF*y3HTh3i*DB<9P7jcBOB-)J4*ceGT=zVQHkUP++I zPKN!_dkcDRu2M;fX>qD$TxF$!#XQo`Z>uyj6M}7IW-8ThLbtAYRssy5R=`$?G0mou z9vGT!al?}knnbD+P#smBQ)30nsOHBVl>PJz)ZEUze;Z8oISz{y>kH6EDhc{QW|ehOi8ek8I1ZCuG)XVd53Qq*zFLufH#9A&Rs^riA_fdHkB+pAn`N~7J4si z{tPKXC)5q97pEL}vI(?Ggnl9EYk0};d1y5j4v~mkM@}{@9K^kI6o8AC8i3L|cit%o zeEPt65zA}&cMBN);6d>}gK$RPEI~PnYkB3yVndnVx@G4 z@ne9C`2=<#m^>{@Nsmn}%kf7V#wB)*d}ZkboO1<|itxqK^p+6)OL9_|0luIis+tc| zp{f$FscRn;mc#Fmk@ziIl$1|K569AjvP=AREeOR*J3MjHJ_^xG?Uwae1wxxJhG0Ir zPsrGUqedb(2+y0nSY3+DINgii;w;rI*{=};_Ab@NesD~b-gf?kwEwK~K3EYexX zC{{Y#_S+v*p78Ip2KZ`c2sz|_gx0DWiRF-)QLEl>PR+E(L#X8Y(<3Az++i9tgFqT9 zv-kRVzpa*E=1DPfezwJ(*)>Mt;)M`OeZ$9n^^P@{8pU|{2u0~Ld^v41Fmzf}oaS5X zm+#_aQ?=G65xOCaB|4gs`7qoAQ9&>4S9Kv#lv?sq*yWzNB&t|^Q_dB96%`7}vQ0{L z!>U?o5%PF=T}Begc9MH0a`&g-a1rzQhhmxqvqKZe(!fP)@g@{Pso7CV*)Ck{kgj*{ z{*5b+{})$iuXOSB%`1~rLZVUV_!K4rKYfFYLZ33bit8XZKn1+InUXA*tA4Q`Py?4~ zz)4lB+Mie7wAB2XugP9Bi%3c$g}-oTAre)_T6K(6*aIvBp!}D9m05O;nXHBNsEZP_ z+%I}9l*3qsa^KY{CF5^$r(X4o)*hQ-WRY36AO4BvU8@wWBoLpN`8pC9S&pAJXd+X$ ze`W(ywX&7uqoNmMldei1T)ku@aXt%rtNxm3Sk% z8qy}$>>OlpVA`(7@<_g^|IV@$za^3HrnY3>G+gDE*u3OB2F+GL>wO` zCQ8eD0n%8Id+lt>hkXANKe#AA%Hr`x$84>mgxV|R1B9V))UK; zswjUcJyeslr%cC%IEtxmWv#$lvURu*GlU{F>~!l!7mtmu#x~#%+p6 zFa}*=aF*22t8=@k8V9`Bniw(HxN?5EH8|a3X zm}?#1yu#%{+NT>z(IUsgsxs)BIr$A1BYd}ybVfv@`Vi)x}Cl}BFk7x33abp*URHiI$KC@6taYE`9h_4!F_<&vy!Kd~b>omHG zc1?PlnEWb4{N~cxdvR& z(Q!W{=*-rq$7M{en0suKJD9Ta&sF7P!xyT8U?k#za*Axig2o@+4ytD0#D0CpDFHor zaKok>KA~OZhc(0!x7*$66Ix`82tB3f*(g~0v z9qhh*jN|2b{ri0KMQ34?<4_P*fWBdl5sOI6v)+q}yHU4aPn&=P9~ng$5`mYqasIux zVgq|+-7dWSxG-^0i(6?uHY^CWK2DT{|48xtXK4BqcUPp+UUmUV;4K8j8q%Cy>WXW+sm6%?p|O=+MP!DSOPaT%;ICfqUe*exfd zl_bt24TGZ=uLkp%1+N!5?@$>N686y8Mo9;p3}R(02B;QiuPIUcNZ|0`0?HRTg4td- z-yE`_)007LOk*Nq2@;?g|@fk{(fQki)d<@`(mC%=39I- zyg?>hN6z9`ZNvr%i0F*Z89)b?eg`J>6p+E~ z0Zm6F1c37MHAJ zx)jG0JE!yd*D++dW2XGO6)Y+9@Qxk+@BvwJPPu8F?_u+1(o$n>g_h(Asigz~8VB)i z$TvfNj#J=~Z3n)x%$O$<*ad5W#0l$pDc2#DCLeLjB#I8!8ug|tg)J@W6&M(+wK1lR zMEi}a)o11Sa4}p<*7|<3TRi*UH&Zi~aEDfu)YVwGQ=om)RAyGH(qPW~un3F;Xks*2U@Vx@HBPHbvs27f;&DE8Ck$clD zhl?RUN>mYo6RFwj8un{)l6clIddz8w#LpSqUi{u~ z8vUZ+;7t7GB*iqf!Wi(ln{~#{Nu^ckef#5MJ4;qn{UJg_le9JN4%Kr$x14m zmhbX`<>bj5ok7U9d-1W(8^?W5y>Khfk0hvq$XWqG_3})MsXed_eHxxuFXtTZzWQwS zR(FJ6YI}X6=q5aM@-NUiJE>co_$*o4bBK^eP6dFx(-x8zA_~a!;`LRt&*!c5jKmO4 z{>b!G4Qr-%YFU9MmlF8U%Ik_{4Zv0lS6i*J-SAI}kndlT<%a)YZ#f%Isqqxly&NuN z_6hL_uaGASS2NNbI@=I|-ZKvY(%Q)H*by+JiF1m7CK8(89^Jib&^XoU=nHT@QxG;V zcfdh-Sf}17rqV=|Pdeu)Bovq3YiNo6jS3SXzkRqTs2OlP5-R(Z^)>>jN-jzdS4t9% zeZ z-mR5~bnBl}FIFK|p5gePWYH{06A^SEiO-XCjf~_eGaOEg`|zW3jYYyf(q`<+KmJbh zHb32ebg>8fjfVFc)NNtcdFqnRRvk4>pHAM!sg@|bn3wH16Kn7Q;XxT?W+wA zb*zd_ZVj<2-r=MlreRxwh|o8wuP|MudIoqxQz|iBzpIeClF{aDho^p{gwNwjqus13JG>;Kg3^AXF(-+H4=*M&y9Zt)vgyu5)yn8`C6Y4~0x zFhmF3-FkV`{Ne;z8VL+$s7_Y+h$>;qmk~Gzzf^G=B9{>E7f5lY2CDZHVx7lQL1ebj ziDq2dNKTVTxjJ84%q@?F!x8UmDle0+#UO`?y=Kg&`=rVEs2t!-3&ajAKuo9oG7^=z zoR3lY(_34i<#l@#WeOij%FGZ}YKymqaA)d~)M~b{ zya9dpPsnmkn^IOVH8#on-E-bIgU}i%MkRV7Y4i$1C)FHalK3%LJdlV&z8)o{|!s!sX=B2@(bt_=m zHokr#%Y=hTQVdBSUmjXfmOc|dMmsR9&~pT8^K!iU;{-<`W!@g><2RlR=1W?J_(MTp zf;p!WJ1#)3&cxToy#n!l_)>^!v{io49YIK)sW;;o^xve&SyX2U&^G8k=M4M@e=8h^(DtWN?oj)KFCZ>RLw zbw@8p8N3h1UJG}De=KYwJqWx;w}mllRb?DqrDt>vU5U9U3OhWq!7{-WAK~5U+#^6{ zRw+!XJ+Bld$#)Drue$vz*@bda`0bOerrf;dLkNW<|70iM$kE zDVU8@kNR5T#vvLSjFPcOloqW+!9CM-*OO&%Xo&9+E6?cb51ol2G)rPp|BnZsl$-U) zGjBinf+H%6v7RORFwpJE6 z95`@NvFvoxz2{#)5#rZ>de~ihxN+$wablk%2!YR02T3xbpN5EX^x7%hrcGaTzI{S- z#3JVZ`k7oMNVTZR&=w$>4dA9L$1`wqh{F(d%+;`OYdG4_yH5P8BB6Dj1xbFR0GTU8sBoQkn zIm_Tv&r5fo!&R98=LETgVo>PdqkQx?F@&85gFnO!ac4#bcj_ET;>g?@(}Ng3 zOUW8Cw;`49=e&NYYr5czBS^cK-xx9@#>Gbfb<8qYJ4C~4$w(B^)HU8`Fa0}l?~FcB z{ZnBm7##07?qqX9{8yvl7PPwegL>eF>kqUGWPW)g0!&=pIx9-Reii zuC>o^V(i$Bmyhp>)zd%zvCmrWILCR?jHSpMzhTyED-%DtH>U|`!T7xsw$&vQV+q-Q z%Nnlt^H2ofb&v$H)FzI>jCEx%D!D0Fd=rc&S%_5athC`@IAFH(IyD5*CW$=XnU=Tf zEG_D?Q?6^BeDmGQ5sP=Q%^cDg>mo)BUjq|r5Sa}dpF*=K`P)Qq8=F{2WyDQ#7+q)} z7868Le-om1Zrki)4Seb$F`axb`kWK8;k)YbqAg*LLpWLT#zZWUj11Xy`r=1j#lZwT z`@|(rB~Y!2sU9hjOPN~wgyX9rM_%xERcj-Oq+2jqho{X<6j{%85apl)rrp+VbKVu*xz=(6mtpm`ID;04+4}~r3nk(Y_p6JrVeKa zcDmdGh__J`RgQN;mfl`s8(0M9cP^p~1~IJ}n|R;B{NqkCU;%ghNvJm$+5s^837q#o z*?BWJv1e)HDvQXyMEBn>Bv;AcOS`CG%TZw^SUsxe$@EYz&;~2q<53F!m%|tSFNg1e zB%-Ik=_6xE=|Nn#j;6lKKr5wlOtp>0YV$#wI`6KT^byC~(t_xDef0Zv=}$d~>1Fv< zO3fqX<*)%kzIXy6YMbZ93QgJr-Dx@cL*efa9a37waGwgeLqAUmRNr(?cqq>BFz9{a zIQg{(gYGV}e-i-%kJ~!UGRD-U$0eOO$+=*gq8uNEqLjQBdkzaV$3p z&`-rq6=(WRlOz|8bsV&%si8E99>mFw!?PD3Pd0-0=CM{@-hmEJDpjz5@04P?sB&d| z6qak4h8QMwFXT%g8Cqba_W$Vm%BZ@QW!(f14go?SxVyW%ySoK7%kY^Y7 zvw7dmg-quG#R~LS(Zgo5K}?_PTSw&Ch)+5xd$RsnZGmrpT6mrHgc+)!uuJ|)i<9`3 zRgStJdFcDcny_FKxr;GN{3w-0cnz)--fjL$kr*fUB2HZfPCs=rO(SZjP+fNIIX*cf z(7PebG~HWS5d&;L=kIpFqA$SPqyH_^^0(I-CkgaS0MZw?pe=~W>@A0u5w&4+ax*R<$>WPQWdNP`|D4ch!*75}hKne-2u*ht6tX1?emQ^YnQfTjB|1C9 zrWNWr3`KPy0Xx$v7~=vzIA35205n@Gju&#K+x;ma^qC;wt=D?QjP>`hR^NI?4qbX5 zdA(POuEgdM{0B2d^9za#qGZL`0h3YHO~0`iW5{4W_B=6^jh@jADO0bw?ndnM8*YGbhzA~bSZiY0!Vd4c|0^C92q)cHvZA%Oe}jf$fvosaM(e!I9Zj8oLKDGAz#!<@G^U|eOF&5wqe4`dql@wM0 zA07r9JgITSl0*ekEMpSaB2|Qj_J7&g!8#`rv#zn$zqHCPu=k+o2Y?NL?U?rHPo!3C z94U3lL(ETYMXN;8UgX1;>zE$?<&8L!x(c)HUq?7~FXrFkbcr*x{`smrFK`6Sk#*WI zN|b;|r);EHKo)0t%h&AHEPT(qhO!+u#hV5M;|^(L9wt{ysvwL%b_Ym) ztBVQO448yNhHHmuVfKAbJeiP%!@6G_Qjc?nt{f1MpG$@I&8#R{M|u(io@bG)TX9%X z&G>8`arCFMEW~Xr)Lc&Zd?*6dhi1OdRuJF9D}E;`_Te~^5y9Aytk(+5l^S(?twRn} z=HOfErtHQ3fnrfeITSw#{*8HyBOl$s#X7O+QPQ>6bk)R9dGfp@lYBe){4_irBEN5} z#)N#hj`lVpSIxT@Ga7ZGxEC4@|F>~zUdOzc9y-2lOHMhT3{~2XFj3iXCEHcC${V91 zatLFgZen9asR!&dVg8Bj$4jOk;u0q%^*H|U7gCs40oLcj1+wc*xyLltBAhkGw6RFbLKsh=_o`Y} z={l~WTJ@_HI(jx;YagIPyJr#(`V#<-)5fHljPfrVkCGlsYQ5|kiA#P`iS6KE8S6NU z8|!ASEI+tb%M+8%VwE%Sow?%IhUa9I(=LoDN*K{mur(nmW`#gUo@R!Naoe5eUiZoQ z&TNP_Gi@?PiHa7dx>{0#;AosfOFJulm}-uzk)b5|S?ZC=YW)73taDs7OIwh)SG?P! zHKvFoIh4tSQZMyBcOf*{lSp)T=&Mj<<5zM$RV9JnK{(&gd}Pa|AnSsA_ERs}xYf~h zojQtK3t1$W7%O~zKJNG7*PlgRf8gAEXk{1ZG7#|1m?NNUeVTJkAh`SeP@l7ONQzee zt|kon79s(nBIyfdr!X8c37Ha-5J^2PwyL0c_=5Di-rQMKThf&6Y8D7}OkCx0;U;O@ zVhGnQT3MYL+QiDVluvnLj0wG)?CaF0FDz}pTTIjS_ud%y7GW-Hl8kRs5jUuT)|Iod4zFu?Z6bhV0oLYTCWb;C(`_s2}d zl#DzbyV$faZ0w1oIDyFlOfYMo3&gF9qQnXa%Qw-(;ngti@okjegj;lnOBw%w7xrrF z-xpqHlDKg8dUAED!lBMb5^C>FAqm)i@Ko$aPUr_>kr^%sc>oA3Xt?Tcz%s0g7K;~+^+aq6;l}v;h-G7He zC0RZeKqeYE*o7>K5=Ns~q;gP<1~ExmNgk?=AyI)`l2l)wOc2=WuoaEZ>64(Z#9Z)J za4-%u>f%JyP^DrehQ1U_lHP!vpU z>{ghs7@|y6rGOfNhu|YP>)lLFqZY;pqZroe8(m-1Fst~FeTlu4blJW;SyBkQL zGwDtgy%(17rDci52lnbg0_;49b#&~`MGalx7@3$gPOu)C8~b%W#irFOa!%FBg7z)Y zW9X^k(4|ujy|H4K?0z!Bd+}t=1_bC%kR+wj4&!lU2_?5O;vo$F?DTYslvqHuTE6e? zJ(%?s#fZaUDzfBL+J_>Nl%~cDhMVqHssrvv`Rdh^5^pyJXCRu=kOWl5Y#Z%Oi^aa& zFD7%l-eVxNiH)X+MPq$YSIbk&sSlt*ElF8-WaE5`DbsOknfU#7T?jHWDG%h0QRJ$5 zScHY?NL5tZ*4WQw%VZIC4!_?=6SS%z_-t?BQ`F&WA+Sc>$F-^wEk1^iYYE*6{rx`f zTc&LP@C-XDJ{6_t9Oh=h}*|)C&b5tW@3vdwh(DVD1~o% zN$ij&My*}uB%?+#g8Q|pD26`y7D1d0198+)_oJx2JXf;~AO8(9p#K|W!0Y-&n7nAh z&BD@^;vuWrO|8ynnkkz5MTx#-Y#J zHI+eXCPMUY>O|Bg?b`@e<*Ipio@QUN$Hr>LPpi$>CoH~u4^dV*zFeF1@G05#A8y?v zZg(_Zr9KA-hNiHQ_1Wfc(HcvQlCT!oN(&7^AqqztF1}Yq%crF5o+M^mrMK)!OQJ6O zk#oVeDj}L z9HQl%Y^y>&&#?uXJiq(9?ybgCULn-YmAK~MVZ2v~sZ+JUIp|Gm`MEuGu!J(wgBgnu z0ci`iMxMz+Rc;iq5f&&7U7Q_;-pLa>E%NFv+jb|uSm>d-Pevgy)P_fP{Zs(9u=&ah zbj>-CO?>>>qIdrQzHkhC^cV0rts3fpzX0=h^n!BM-n0W~CR=UvD!mhNaMy2efm=HD zV8i95&hP_?%X1{ zdG#XryPUx`N7V(|lFL0dAFQ=IbL)qi2~?#>jK^;7=Ns8%g2VlXK2VPT)2&ha9O!&A zC)w)>b?Tyk%7AF(MR_tSmLuSItF>b=mO$Z#{pe^JDCJRyLLLNazw&W&KOzuz@N_{sxzQcO`nE4~d+33!ol6hX6v{?j7yVK9U8)P}QHY@QO01%Jf z^|~}`HEehxA@GMfd+wkO@xImmt-O#?ddm?Xk57OFZ4ir1@cc_^4UQ(R`zlu z@8L|c2(LLeP&MkG^1GV#@FG`@ zj3s9~JoX7`Wa)$~?UU|*r)T+h;r~WC@V?pKv_vH*)4#Y?-7t^@QB|=2T+L8VS z`OV%J-^_5xOy=ud1UAD$pB&0h{2sC!^BwL4x&T+0^xia~Vw{W?^l^JGa%tJ}*!O%u zNEW208pjiKzr5KgxCS9jn%EuN*(-FJ4KHZ#B?GrQ{}T;w0%}V>w!)f2TO+n3V;m#D zfB&9Q;2yf-Ejc-PrCQw;51uwYvvvIad$CBCPkdfbyR+^j?$3{&!`}YVrSX+!kwT*4 z&NyZg#IQHVV-%gTW}%lj`~LTB_BG%OQ{WUlAKa-ZjG-Z08b5;AJ^!*}F#kt~|27ihV`=&>&#)_p`g$-lK7x<m z;7I&}Z{InZ31*Mj0zF6+an<|&DRk-zUknOpkM@o!o`ibCf66^Z@+}J03|^*?&wX~m z1Al$8KqjM{I>TA=Q;wr&uNbLqM1eu&4?`+QMjoCai>4T$*|}x^jmi`U*HTL#6M<$% z8Rx834dblY&8RIFYh?%J#Cd$yQm9G<6LJ_f0@JJ}@A1ONn_>q+T06!!TWBO0^`t?9 zO5FEQyJeM(?7eF*94}@q8hn2R0&rCj285TL@)DN=0ICPD(7!*F_B;x zg_Z#7s|9Or7sankw~T_m*^^qNqWP9L1yu)X(m^>cqWkM`jsZ0XydOZM}FQIqfM<5X3RVp8(PEIB9S@3h1=6l^jo7piVu7~KpayB zNi)g0S|$914hZNbe=BOGjVwH+Szb9FF>W7q-sJeG8|?39@Ash2`5*R_Fva*wn{k*# z(ynx4f1g~P98;)o&26D!!RaL>VhsvlGFuthGKmaAMMcvkDG80W(>bM)87Y;@Dv&iZ zCkeTVwN;(?u9A*Npb zXJ`~g3_bwxF$g;e&%w1!W>eS9wc3R7`@$V9L_;?b_VBd64OCGV8>S<@5jK6=!3te~0mEEyLuDMq8G3IZ5rQu9xmXc;wOZ>_Tpzl?n(jz4W=NyQt5 zCG6nwmFe)mhsySKOBDGzl22n9j00+sUHB>=7P$cN0kT^XBjV?KqC~2FZ&M;|nAwT< zrW5rk_H|9W*3A##b8iBJoyFl#$u)3)i%tL^YvD6s1a)c$LJ?VYvl=gY0dR{00Ni5r zAF?kNc@oVJs9J8Oo-f;XnMI`7if}|ZXr+m|S@+l%j{y1#%*%D+(!~@_!}S}7UfK&` z^RG{t@mmrKPC#SEu6Z&5BG9{poQK5}vQAP@`B62_Pn8^npkTfxnc?TJCLjBdA$Plr zDsXFdPs3kl52%U7iVto?WD*xmwwB87M&qD8Lm)CN8zJ*o_^sHji_-P%*ClS=wH}&7 z6d_9z`&A71M$K=sSef4n#sNJGsx@AF>=c4@auLBoJx`u1fdwz%%jD`Uy8p=F@xQr= z)M1vPMB4IK{DW6#B8$jL=ILJw{`1QWq5mJmj{iu{YcvA-Ki~h)rvUar{A)x0@1p^m zxBrxJ{nw+<^#QLTt)-xes|w0}_0P`-lRWmF>HneN?x9jeovV_0P zqWf(Ne)B)CK0LaasG&dcgA$Ht{MfC@X-9e#=dZ3dgZytUHO0gTJbTD(9M5PN%*62D zQzAH%INTg*K?7n_j9sG}yPaXei2XV4^$g8o`Im3QEdAhiFyB+4z-^$w#;-&bP{W_!86>UFV^#m@s@|Ig8@AZkGTEW;RjJ>D; z@~ooTVY46k3f%1CG5)Ko$je5dkTtDd5)8(d+^fgl`q}=g*y((;$NZ!<`}Kuag#Ps- zcYJO)LUr6ASqHr{dEDh0bQ@#s2LTqCMKt-`T_k0D#}EjV>pW%sWgg-{QPJxk3vn5| zo=!t{398v`-qj;+k)N3rhR(P8uBVGs##TEM7p`UHDd=wxa2D!*=|8Ws90)jVW-S#2?0<`X^13%&qZ%4K!BtsY8FHFxHr zWxO3Hq8E28H50%XG~7UM>#=T%-VyM2WOtyeSE1nr^G1B=Uzmtnc&FR$qvbU9U>;%b zvxN&%&sFE%clxGBe#t0;W=B^Y0i1ZH+fAG#aHtv?I8nmwU*|P36q!oKtXb9$?8jC| z?93W2bTgirNiJyh3CBi9?SR(@c3l5Ti~HPDS|W0@0{`81>IeNA68ajY3?6s8{7kA- z=T}n3Rx|tH;Bu-TUqr6H428h@kMeS{p?+@U>ABKm>W~3Cw?f~<3&b!l$*#h(h-CF3 zY&WcW(fEf0+sO5@HMsyn)4IVME>-VO&3+*upG#xq7@N#XU1ui!4jpfrufTiH)eL{d z8(4*v*2__zyJFTFqR4x()tcL217J0I0ij$_jr`eH+>*At^6zKOC@yq5u64Ay`ak1$ z6+US@8_WUUk1q}S%%0Ha*Nw<y7|P5Pph=Ga!f0ibC(Ch=CV3pXB2;9LJQSpTzetz(_F`=sZq zUjn{I3nxblHTMU-f{wuXI-u_4tM1#B6O2Z}*Q+hQN zDfb$pwr|h3nlqn{#WNq?Q^@Zp(r9FPWf(jiYPdQ%#aGjQF(sl164XlaI7LGP=oCNs zcz+M70DXak#XpP#uRAjG@r{i6KVMSlhdS?#TDGj1@jV>k+PCJOcDy{lZ`=F!p2Ee- z%1ZAP)-wChg`32`{>|ebhR3fb3pJsghJqzCn_$0Fu(;Rp<|c8+Ic`Vps(1Jg!l244 zPo24I-@OKd=N$um?i0f6T%Efn)@6N~F(bgNBzU>)Q)nGo^|_SuJ#7QeUf&zs7Z^+` z@psK_&H-4DGuR?}b284%t@u>o5Y}`Z4=o)J;};4UoVkEA5Lw;vsxcT0+A~ghS$LxL znpCo2pLi%xoK99V$%yQvZk4)}IaoHDVifRhZ2w=M7N~#myTI-Mg2O|BVua|Xo?dzR zo*dFFr!;|hStd}sdJ#0Ww?at%&Te*kecNl?j@kyNBj;)K%xYl5{PfVWdDLC`gM?wOVa*oFzcSQUfM0%Zl zg}IM{b6&fO2l|97o%l*amT2qJH=733hSX9wdLMM7ZO2HjXm2b|=iNyCRu9LnSyLY9 z^jCezJ(x^A#)I27!jgLYd2SAte~)`9+Neq672fMepIs#gvVQpsM5R8OCEpH*^*4uK z@|ns3QRo{$ISJEbj`;D0^YLO`+?x0P(IP#&a1bOmF78rT4{x_uFF2og*})$tm}Aw$ zM5RKVT{FcyZPKr3 zzdsB?U|wLWbLIk0O$gp&cK_=cjms4av%X5uKZSGqV{K+<7^KD)PUtV|^sL9B? z63V8;^KyiPW4o6ms1hw=0c*BKfUhLFWvefyqA4?*?A(;1DpOuvl586Hj@P4+^vO~y zroT-e@Em6!m$tN7c&%+r1$reiY22uBm8#wx47GiyP~dk4WiMnNX%pGiwG*U{h!__A z8mi2eC$E`O8el6*HIF!M4p_A_@2?q_cC^1Odse?FOkcRJoZvW1joLY!DY0HPvlS7;4^tv&YgPCz1Fh>?DnL*dpOQRAXG?nb67420%>V>w~*ePu0F}M~G&&;r$d5iRL{nE$kINn1uKNhHnL<3pd>4tmI zhJ?WVL`dtNnSqLJlFSC6aY-1!!>KG|SHWA_94YBl$Mqyiu6cu{1U)tTSc}NOa#j^R zDH>V#6xGcaGbp&FcLB!*Z!{nIl*kDc8~dp1Y&+H_u8 zn?S!EwYSfi<_h9O>C&;?EptOU1z)HSNM~*GRh}iXLGI`s7c?C|068u8JGM5M+PJf= z+pE(qV@s}DZ1Tdcy?7me{2dQ(xk46i(cWn06d-^gubAsHHz_+e!(sKBtjjYmPXFiD z_UC)heU!bAiN=V=?wwKXF60>YAE%f`%I1XGj|&BZTgZg*^pj#xYCW1Pe>f_&AO>t0 zn0l81b7Lce+f`cN;fUpKVa)GFwPlW@<4(>ORMv2!)0@g-KJ6XjY&W#RlYZxiyCHmc z)`imWZ7CD*iHiU}pe}`DkHXQggXP z)}G2Qc}uu2ZY@apW5cKrL_ri-N$$)bzR(OlZ*_sED-|uek$XTMakCDabg=7mpOqjk z9v>e?_5yNE9^1gHI`=DQjecpDXMqQGeK7=cZY@z^ifS{>qd@#HV;nGlnL>b&gH}=y z>Isk8n*WRMtCA?D?!}H|(rt7wk~or4s5Y=ye|H&3#{$T9E{H28=Hg@gfDL(za+nG} zJ;Ol!*zOO(hNZ501Y>`+nj{jtu-LNjR9kb3D}sR|{8f)FB;S`OuxXxMy9Nlh;UkBe zjxQ5;tLI8FSO-PHN&a==knQ34@AvcA=B!zQQkCrJIf}QPDUX-KU19_fctoWAh zujdTr`rhfhiV45fix-B%eKTMvop%vDJ4lROidS85;wMVrA$(xaPdLUf;fQz@67|H> z=f^z(gG*m_VNL=+Gl3J&zG%Fy&7KGu0fENnJin0go1I1Nqv^{fd938Q7KNi}u2Eir zBk90-Moz*|HXA(n+P*$GBHE_Slhbz}lAl|y9(?YHY*$TLo5kg23U9G#GhP%= zuyW_l{H52;(q#rUdDLs-$8kN5d(HBI@^XekaMucp=WJc2CsnZ&H~U1Wpi|$bWyGI4 z>{e|Z-1tQ}%Cs?De2OF(23wU7*4yrrhTsjp*B3uElQJBJsjWzFgPu@IoIPX&qPtuZ__mNaIL&4cW zBk-4ioAw=0bdgf!pOG6L zhs`HWxbyR;r@<=BcKkGEVSW}(FLpFOU-QXawtzprN%H%NNTxg?M%{J zdV)^S{ zq!0goWuq%p!t2<|7IAhH*qtN-V4>N|;Qor2`wIJex0QW|WC~MN@VyK}+m+~$K}?=) zov$ZL2ljk%K>Vw9ws;0!C`J7yYo_XE8^gJVZ|-%2EP;kN^dp^Jpva~U($d^Gva<

YKSciHoqpMgfX#h5~QfDcp+RQQmvIP_|w2G$iaT8#3HYYnC zOp?iKV2*N;B9AxnR&}9o>==v>KiI|RXyC6PSsCv8uvj9OR%y~qPi(dQU?M!0dfr|^ zsrTz2;{qKpIGwJx$$c7l`paCU4m1N41E2Gr5Qa-&8Jt*ISe)0!P67W(vI4ix$Gk*S zZn@d>a`$K3nz(;nM>-w^=s=_)?b4P<1ZTEO{y33y`~Dh~{)`Wza`o9s)To`YA|j4? znw&^+;p2Jy@iCsnvhH%mYg8`V=VG9LOi!WZ>r#Wgz*P(ESe-SKfPlb73gCCt#AQFQ zJ}&t`*mL-<`OQlE10+6(R_G`^gw0Gw5)fp8w?A<@7)~X%#Vf@GUsryVS_eP{=M#lL z0a-w;6b_FlAnb@BpM(^^UM=%UNTF{$szCo6PE{|ONq+g9eRey)GwcFKa0Ll7eNgP@ zv0ONJ11bls&4AwA8C^xU7QN^|eZ9V{g;OwKJrkirXlkymM`uwH2hi$xn~?kt z-e6Eg3r2+`609|Z|FT}l=Cn1tyW(P=g}+ySLUO2dV3Sw*k>ir5j`m1u&h=LWa^<}o zfWK`_5&DJKnwFj(@RLtWY9>F-O(EFVK%LFZmd$~pt`@Ja@Nr#JOR5r&(X+_J>7JIu zw=CI=yr<87!7Xbu1dr>mpv4Xj5kj4)5y_~PQ3fOCkMjiuMH+(q*_r10h^wqS{t&2W zo5}=5M`1F;vBf`%!}{-~aS06BJ&tZUS8)!-Ex-4c)wUvCvsc*T+Z8?j2+&Baw6ZgRwOFg3C~Fr7K`6IF(Ww#Yd11t+c(Zaa#72n2cmjd zw6vp}Pc^||wt?zc@=8A5<{FC>$J$_zRxhbr5t&CHtX@7LD%CdQI&|IeYOlq?#l`hi zM5)v>f}~k^S1_zF=;qrV>16;WO?-Ro=Dkt@L}l;E;$6BaQg% zI3h2U{s%?jDuXl+W@Qs`(nmR0jrpMUpfNSa+HXyIMola(`H zF=_*T^3qfSRe2v$V^+I2$cW-+eRT7S&D=R_am%yRJphT-<;2H31Ytv7gT(`S?*`da zzTQWk^vSVO*=I?P53mapvj>=Z$&5~%#$;xTxhI$poL1!^Xv8qJtI`;t*dpzN}oVtw86<8_T!OZ2Z1 znM;U)X`^Q8>tv+vy;o*(b<_6BughK{8cb9mpSPmI~~_04!eAiMGK z2wC{)i$2Mk@f~TCeMW0sIes5EO>y{TtZ@E{c1T0ouuKcxC4|FI!AU6A>vCj*Et>kP zPL=yRCUA&Oa3VeX=^R09RqH1$bX5_!Y1iK=E51m~7%rg?Gt!+JAv=5ITpK>0p7jfV zjMGVPPERMraDCTQaH8hbBB;L3FFKxvdpLH(u)*Xh+W!12EE~ipu6}V{;qTiwyeGdS zC0#k&Tf4 zu_j$#BC#xANbj*|7(`oY=0?(@9`=Bd@4OG!W+!OHNIIva7&fAPnt*U}Y^E8_y=}R> zbFrf3C6;a&o(s74aBL`7Awz$bpLoB$-|A%EG&&3HzjS;rK+6$B`=JQ=0TsT8R1Jrt z#aXyC(c%+=SZ80u+*$&A4WEr0LG_6A=1^p>ywy!B?r8ef*47eV&|cPcMh!cS?&hmY zrNRKVy#IErzUl=klUvX)(Sx+>z1xp@-jB($dR+4K^8`W_ZrkS9g`2wyeiz^IxSq?_ zEdw)Wr(RgTd#caZ34i*Xm}>b$6JFy-+9!i%VoRo8A6a`=f*H(+GAu|F!6bw)Vn`4& zmjc^OPojf1a6c^9zV19phYdXuLSqO)V?9Wh*r*g4Rk8F+tQv#eF;fLm%Q}cij>L6L?dZOa>vc? zcYK}~+bVqQ-jTQyc}IH!FL%*A9LRxQz_RIxAp&ik^h9fvv+s?o^h)OjZVUGWzR@@} zK=YU~<93#x016}ubait=mJ=q55u=?nLy%}Qv}kw&q2a~1N~^8fc2~e}3L}UlL>xC= zLG#@cC#cq-?8f)$4D|z=RRsna2H6z|XDw}AK5TipIoFK+%daPO|9+Z^)*lOKU>m2$ zTSvUQ?cERl=O0{KR^ZCitq)!_tR0???frE=eE5*;b-=l|@4`Lp+D89jbaQi491&ba zOG_L1xV#}ZKi7W4mYJE^gWWYfjUn)`Fea{F0k|joQwc}>oJk|QuOYrH^gD?v{qwEY z`2Gle$BKMb{je=tmWy{L5IYJL9tJ!kS7YO^;8kyh`kI6jz^e4x_yf)5xEX}c?Gh&N zG$pVy>ua>r>i2qbrE}Ojz5dtlu@S&OpRlUa1b(Oo2<+D6f7R%Tth(rXAG7J{>UMMW zP2@@30g+5f(4DLf1E{RobDGmlxX7Bk26ui&c-b}Bvx+uEvX4^~BkFBc4z;F97#D}5 z$Q@KZubZrSxzAvgIxK;g6i5{^3a%lZHqeY62|DU4lQf<(gJ|N{rHF{Do8v8E_RnVh zrbX!kcb9vvEGu@PrjPS>Xvp|$YZiIkJ3v6wTV~dEy0$fuHwvgN5^XH|oG02yVZ;&P zfZLO9-(Ex`gO}~%UFITkjA1^lXw&Yr6Rz9jWZDwyOdxREiITLt@3?mcyV7RxusG*3 z?cDzw>o4gZr?`tZENJutj5R}N@BZTiUm!O^TT>#V!VoZ=NOyu$SPIG3-Pk(WB(c1E zR>Qz=PMFjqlnB*GL?aD{sW{Lk!K@Gsp|mMdR3zhXf4`mjgAhV3Y?|2O*T_y&6CIv4 z`6X|)KAK$vIACE^bav&Bcmkk(*lA{*8_r9w0+*5Att$)$5}DQ1)R1(z(cpc}URd9=0JsDYQIrOe6_tn1&yDWGPau^XHA2ZTbVTpuw3^2SY@(md zitg{nq=}E*xM>>FVH+mw++F!>e;jYkA73U9@LC^v5uF6`xTnzPkh6u-yz!Av^HFa{ z@}6x(SU#a=wD=TSpSlYhh{K#3ML6h0e0sKLnAD@MCKiPXRZH9CXdmLe#5@U@S6e{4 zy@=aRsrd5+L(RfA?(u|Y(#hK_4T}9s^`_c|*CgM9*6Un&!RfQijNu}b-MRRmu}e}G z9C#V$>>PCxw2z*4iav8J$8MZZG>bv(OrEs2AU`CARvCF7C+rf_=VQz)a+imuEy%-m z3_1Z>hlLvRODWIL^L)9~8k#+$EKFE=U>(jVjeLG34xG9GGgD*m_fL^C+wYin*QDTQ z4ji$&&Eefqx1uof&HmIuVVn?=-NOTYa(#C4QfY*^n7u;eSlXI4#Z{9uuqt?$42MlL zSibgzKNK1wJ~ zPknuPv|mO0RBjej(a~;Xo=oAo%}15)Niq$(X185PmAC|AiAP@*TFcCTWdyPZO5lN( zl*l_4o;nS+443!yjnc_5?0UGINmiD+NarbhpBS0EY37Ts_M?nBvLl$s7{-FpFD5cu zt|Gfg2(hX{di>-j2G`{6vFwr7LugVeR6aULmR&QE!lrx{*%Yy{T{xlVxtcL#m1=Vo zLlL&73zF{y=3eSo|oNr|#fJo=$4@+d;p=7BRQ;?yyQGuEfsn$jXw!b*B63 zLapznRCh|5WHTOoBu`V&-S)tDuifE~!4KcX2(BN0d3Hq6`}-b`)%sp%RYL}C9bF$ zRtU&tvO28Q1&?#CtHV3A44|jY(n#x~vx;?2E2cmMZa`|B*+;kk_P-Hw)8i}oPmJhQ zmm+uI;pvMS0ExfXxD!#(^3@C17NESjT^fQ=lhV>rz{$3F%V8Nn5+{@crz?FN72-7r zRJwfX?v=Xu>PF{wnW(Y_n|;e4D=KR~;04CyKNvYzcLd6T@DO1DttK{_!IjtbY+5{M zp6yHi8sOerX4x}du+8B2)n?FZCIvifRcpH5lmgpoJmJAJzSn@Bn3Ccgo0yWrtIBsj z$v6pmGUYUny>UK$QqDfvI9>ZUZNdJz^wmFa=EwHKe~nr!g0Aa#77ZyI4Vqn~Ml*RD zYM+zo_2M(V?6jLsU*`+`w#L+3U|X+pQvoE_cAGc@q62|)SFdjxdp|vkuo-tnq%T=v zgg?8#LkF3#1UGin+Uvgjs_y}*5r_n1qD)Ex)hn{-@>zatS$fr|4ML^#-CEG^@d62b zrc<|HisSqleNUS>PyZ`ZZ?;!8bU3BHN8KM|x3<%J%bDnk4JnV9J4j}gJ}_p6)ufUn zi%X=53d!^pd#Q1=8b98XJH)h5L};O~vzPP_(mCDGAI;G8*^6IO;Cj=HziDJIr#Js* z?lb$HiMroDdW>%sYhq2KqqTx-waHqBFl%cXp)MA3HhgeUW1)F( zA%^tx;3Earu1F6XTDkbn)Lu7q2H-f25#z4Rooi9ajs7jGy7W0;hI1tNx_lhTQUT2m zr@5~-I?CJW8hjw2G@_Nmk{-qWuYtj&2mP<%i zcE}5{fQ|wy{C29IdlCQ)I257P)GVW1_MB*HQA%1S@yNzweEr>zeX-FRfyO;K#i9w6 zXpsR4w7p-)U@$+_9KF^>@T3`W0}0;Ov4`z;o0>P9-dq?p^c~BG+F?jbahx~*$d?oC zzhCOT$ZO)V6Yu&#m8(XMhAd>Fdw85U!27O?KTs%<+d~za2H6A=lM$D$A#Z2R$J0co zFZFfIzPvOB_+La_sko0F|I6f@$(0)G>;b<3{dS52?CbW=DYSZt!RVc zgSBl&2l{!T(azv8Q0cGxL|X$$zzF=KcCXM73+Ml2wEu_86y3gv7y1oD z$NOhJP!pV@%3;41J%-4NaK{90FJ$PG>AWzyrJ2YR(RNcud6AT+x@dw5gHXe;OP#N5 zJ3p8Xbfjdhp8UIlKpMFIzgLg~Si$E^Z{5;hTxDe~_w2Ku#S8Bm-aW!?*R-iD7jQ zQ^wo$ z45mVIjS?C<%s3v(sWRl3?M-vt0n^v2i%VI@ox#Mo(Ivocm*VgEr_{}J*MmNoGhhwG zaP~IN6Ozg7+Lp%S!}jbgcJ@8K>xL7h_-+yEZ!gLpM6>JGiV(iIjl1hANXttnLa*}t z4}Cyd|LY#$JH6Rn(NE?n|2%;O<%mN1^JfuS(lM)25<#pjyTYHfmq!`;hEIALsKla8 z{vUL?8ip>dJuEcZUf!jeXKXmSu9^9snHx3|h`xm`#by-^|6>dM9kQa%|2p{oI*QKAoV3Y#UIwcQz$c&T82;bUvLtsui&uq6mx?Y&X zy60_}2^fxG&n8gaDHFaC!WANCF9^lnoK&}Pn@jv^vHE6=b@EXnJygq4_Zz+ab#3aD zf)XM%DxxSX8=5OsC6RQfPTdw?-6Q8I-v7Sz_XQ)~IpJloSb$@JZakbdJbOon z++(hGTmNt(o&OBrga*+k+=@%%cP=F+#-A29rOA*sa1oE45RC#>zi*-X9bctq;K?;k zMyC86ETxr{BPiWoiDrtcr!CpOmoEAQ4&~5q2e*9MT=j;ZOJWfXv1(e>7{Vmh3%FDK zhGS)<3ctmbA3tx|tE;WG$3mfS8Gl#$x(o;SrtwU-bPNDhjlk5!bE-0Y@e+ty^jmMS#WUog*UJ19S%40e0EzAa9&?f3d|nZ=-d8ggJvm} z#W9grKR3j>X1#DOyI8YA?Pz~CGvExDA$!ZkVnK`N_pHn4y~iIPCCa*Zk%e?Jn5W;* zJ6sK7w|?%&=F~^lZw-n?##{xo;xhkpUVf#8OXff2<@}mt(wvM>f2sRe9bdWXz*A0TQ@s|~|KL}e% zW5>F6WNNRIx)?BqwRA?H`dyb@W)yzokS6U*A}7U)h}Nd56JEM8%FyAl&t7Y(9?3p+ zX-y)zJjbcgKDZc<$H_s?H7^t#$k(=DeAkxU;%9W)FIl~@r$WY52d>a!MZ>~# z(d+wxcW#D#eyz3rLEkGixef83ReTrNXur{f=Tq?aH^KIv3tqbE?cC*Z+Zgj@ ziDf@`MByG6Mv?s;OhG67u79Vo`y#aLewK{-{E+^EljY;37l$bpe3MS)EPIfrxzs4$ zMTAEE?Xu0_!ZOpCvvPmFA{EhOu<1~-`HLS`faI5PjqaIodqt!IqS-fWj0ku-MxC`% z8T7$>;)w<;Sx$>q|8lOM@I>#HI_TxDmrNh0%;EVrj7D0LSXe%4F1>=$GLJA`^bTH7 z`=+_S-RF?2V_@4Y>8P~pQd5|8E!hqclUYdyp#l)dJ{%NG?Ol%c@Tb0M_y|u zwk2_zkh$VcVK(eY>7neLSh)IpE@1|K*YLj(H~x{1B5#hw#=b|)v&+5u#@i#XBXL-{uUlAp28<4!U5^`&0)aDB*CIkH+7DaV57oH)lRIJx=#|Y@AcvHmrGk{|@eMA^$6yWsX{1 zEmzb*8eb5yAB3OI;m+eod^D`I)|&oUh1c2+8Ba|7lpIN&Xf#?K0k;bXb{u?f$2!O? z&c3maJX*RsIkNuB-)NZzA<~E`oDbsuIkk}Kf9Ae{9hB{VcYP1No=u=n;gTpJY-r0$ zybb7$FT&wp7&9uhZ-O0F@4q=A3XQi*SGMpP+HrlLtN+%z-w1e+a%+8nckW@v|H~3b zI8|w=?gOUqelp&X@7E2swRWq<9kxY(%5EXj#q+?N6!VSXN8Fz~i`)pM|OL@=5{nlIn+*j08jV z9MM8i;{(VDQ@A;IRjeG{5A7?O8}%K!Oh*rzO}^>c$upXDB?;P+Rk;a&j3-vIU>lM2 zL(Z7bZVF1S4G6gV!9CqmRm>;wLFF-vP90K-xi}>dOzLTq9nlbT#g%{RC=cxzmgY;~ z5WZw);$?lkKa>lSpE^jWw|$4<6e9YPrJj?pk+DqlHcZ6Y?cH4YP8+Bmf_08;I1b)r zPOaPZBY6qZN~?+R=l7gK8^dv-ckchEy)*xZa%}^+NEq37MhuZ9J7YbAk!XYonJ|tm zd$N_KlBR|sOVo^|L{qlOUX5j}$G(njl&lRJTS)f(ozpq*pYZ`+TqK z^E~%`!D{3Bzkp$CmRuFgdKAQXqwXQ0)ZjyVxOKY$zu4ssc{Sq~u6meCRLZIS8pObk zb*!VSkF~QsOTFzYU$!+M|H?t-PO<&|djg1?MEKmk^EDmxDP4QHAE?*)>1Kn!0o64# zwR>i4k2RhfP(y`3phMF|Zm>i=b(eD50e+ z8!9{BeKRYfR%FB7FA~+NlB3)4h2tMma0x%D-dW(1L`KfqB(d0MV$`fHd3|>junl7c zY0$zbxl{1lg=CW2kEO=#qX*1z*!0lov@%8KmP~z%F}Y-44?SJ2ULqzD`B3NMW3QFntV43F_PnnPT-%C`fGNQ*juL6H5et#`11N)moo8Ra@l|-jT zpz@z{E~TZE=sOFbrCS)Y%%PzQRvdH8wOgGDx@y}vrJ}U2wm!%}- zo;40V1-p)P85Ay6IuaH%oO?N)u1cJ?W6uk_BspSEAU6IfJ;yK!v`6p}73Un~@t#!y zx;Sc2UxthRj$pqN^KZf4XQ0Q;^f1_48#(&5Bs$uFdVpv*P|HrC)fN-u)pLe&jpV+Q z=I~gEraZDJu4wwLdMVmpsd(v#8T~pTeQ9^0rPrM*jra8Qevt&`@$b0J(#PtzI$NE8 zrFWh^uF7!qP?At{B8-8N`2oZ;di#-aYEaRZ2Mw;qH`N@!GsY(nzQ;K3<%I^WKL%y} z4UEOViO0Ej5v}pj_o#yfyvvWWuH-111alClFK{2EylriO7RAB}V~*N> zTnZ}F=cIa1lbQ_wP$fhch_5f=Ome@1{lyb6rzj>~NL~EI-II0gTd9hFbxs>V{xpnm zb~4|@$G}eXjGTDx>G;l7ADnGM)ABOQZcBpD-q5P@e@#I`@=S2}z!%9MNFBtpQ|X+= zzv98`t=*!E8_qWJSjGeZ?xBV6W4Fsr_|Icpzr1~})w7S=tsxp4v^dJtOE#pM&d)uZ<~Bdb)Ori2xEIM2}$9m$d*TR2jBpea?Z}0qg0={OPk4=V_H+7HctJ#+SFsET*08S6d;V zB}|U8=r@Tto-sXcTXH-aG-25r-S&j&ITZAk^4c_BdDDXnQsIcNU2A=q?MDB!rpqW< z9A0>eCQwRi8dnrMY^(c;doAzsU^&1_^dIenyrCofLV@Jrd_pbKz3O<oUDjFI-s^5M(Mf;#E#z*k-mH&!f_8Or|KY!kg%??y&y&%DY=V|{Rn zb&}Y_H(>^2eVMIipl{+)dc9*~78keso`zO5^&P0ARRRMx_?##?_qKhrC#L2+?tGfe ziXCHl)o!9)a=Nf$`g4jNtL}$5j#qBY+OU}z(~fkq^9EFG~cy?y>DId2L^6Q)} zycY9iHwYMa}|s1HnaU11v!@$l7}TYNCj@r;6y}eGPvGzvu(wlF9=U zR`29So0F+_fY2CEzosJ4Qre!xJc|?_ffeJ--J)?5N%Mzc4qn?gBloX!&>lkgRnBvVXc#`@nUd_QG5b0hXmtwGD}BQX}l-g3Yk-wNQ_ zHC-Dw_36C$)b0l=$q8(*)vD1-lf?-s#7oLFr`C2rTg^bC`^oXe@u#*Y4@Vr?K-|mR zBqgC>27>+VLeztum$A@M_F4Btenzdt{jhm>VNEnd{t}?3?4|`A@JVg(Zj7+oHHCNl zT{S7O?D@Qlet2KU{9)bMYq?=DvPMeW=C_C*R{5>k3U#W^KPSEi%s2f`P&PT~gLTS< zUTV;o-_@iK_^zt;^N8Z6cZx+oU$O2HCHpbK5Hp86nwBjF;hL7`=?V^qIeV#LZQ|8Z zY|s-Jqr6QNk`*P4FV8Y%fy?vz6;cMcCZh)m@?Sp=@(ab386U+nDLz;lFaauwvn)o) z*>R%Hhwo|;kZKHu<4ssTzI~z|G-tntocQU9pLg*`x&l zU}Q<*4h&eo+(mf_;&+b`waEFf!skK%63aWD`}%9~h7Y6E1PvW8hTp&*s=CP?&J}sCc|%Bf{SSEb+#qIe z>dCUf2V$2;AHce;lYUf-)-eAKY{Ogm&z@R#Ipy>;$&GbhFcUj7B^6hGSO%3cB52~5 zCF-CocL?vCVxe5O-6pbyKy}qbb|>#W7QUrtkRuSp!0G2M4J`3q`kd5Im^k4)6uo$h zyyR$+E`$bvT738yLfleOqlkg;XXUq6-r_e$g4{B%t&E2`#ofW@u`BHfT1E7Qy7#r# zKb!#&ms>O>2UaHi<2D7bd0aFvjTO10b$);MyKz}U z?UZ)`y1!vl&yaKLf`o4G4)BI|z(TMH!aV{r*Wk;Od*HewLULQpAg8w{M_3}kBGI{dKWOml5?nv2@j9BS4TD(c0QM^ zSggh7Wh+RufktzBpo0N(S@O&_fE0h1O#5^`YOHJN=EgX$cq((Md3B7E2#$x6CaSj- za^SyUNY665c=x;Oc_rH~`3O706?@s2XVdmp9}nDwC0=E_IxaxA5<)sS%-<^g0XYU` z>u&)%1Z^8ChylC*jm00CSn=jICTym!WgNw1W5;!npNeWu*7 zaLEgM0h6P^7ajo_IdD3sgEtSfg|*=RQt3vr#;-D%J|bDwf>WVY5VugU2Ut{1EGhDP z_w{@huJyyOp+KhNnF-St?cc1Ll73QsFc8X}Jw6fJ7m$tGUmp>G?2TSK#}WVJqd552 zVE3QGhhwHGvP^{v0>msc<%=Vn`Wy(rXv1Tf)QI%;QuH2qK?dT-Q9j`UC zR$3n^L+{sQtvmKWXL`ZiJViFi2fm;oMYHQz2+({ z+wKgq@o6a7DF!n9>;h%nC&eHB?0}Hp z-hVN&_xM6G$Q*(Vy%ej(i`VME(jt6q<{vkF7?08bW9uci#36NKGpc=1yAxge0Okaho_KD zht)tm_5zp0Hxm}lUvpGUb;zNPn3Kl2_oJ=ski6jjMfJ1LLKG~C#)ma#SocqABc z@vlK9zJ}$2Gu9G%O=GLrgbtTkA9j{kO>zi9c{Tk9Xv4aQ)DxXP?YrdY?Rc~09c95= z$a?5lUoMd+u#bjhICs+E9r@8IXmttualT5Y_AZXS>3UGtZey4|mXPEC5IN(sq|BgQ)0 zh)T4?SI+xe3e<^FC~sOOhN3%VQ+xmv;^v2?dtDR_3vQ+c98$y j|L=kSO%Dv!&z~?M-5KHMs!*<4 literal 0 HcmV?d00001 diff --git a/minigames/_api.js b/minigames/_api.js new file mode 100644 index 0000000..fe23a24 --- /dev/null +++ b/minigames/_api.js @@ -0,0 +1,98 @@ +/** + * minigames/_api.js + * Gemeinsame API und Hilfsfunktionen für alle Mini-Games + * + * Jedes Mini-Game muss folgendes exportieren (als globale Variable window.MG_): + * + * window.MG_snake = { + * id: 'snake', + * emoji: '🐍', + * name: 'Snake', + * desc: 'Steuere die Schlange...', + * controls: 'Pfeiltasten oder WASD', + * multi: 1, // 1 = einmalig, 3 = max 3x, 99 = unbegrenzt + * launch: function(wrap, W, H, cfg) { ... return { stop() {} }; }, + * preview: function(wrap, W, H, cfg) { ... return { stop() {} }; }, + * }; + * + * launch() → vollständiges Spiel, wird in game.html verwendet + * preview() → kompakte Demo, wird im Editor-Test-Popup verwendet + * Beide geben ein Objekt { stop() } zurück zum Aufräumen. + * + * cfg = { quizData, theme, rules, devName, gameName } + */ + +window.MGAPI = (function() { + + // ── Canvas-Setup-Helfer ────────────────────────────────────────────────── + function makeCanvas(wrap, W, H) { + const dpr = window.devicePixelRatio || 1; + const canvas = document.createElement('canvas'); + canvas.width = Math.round(W * dpr); + canvas.height = Math.round(H * dpr); + canvas.style.width = W + 'px'; + canvas.style.height = H + 'px'; + canvas.style.display = 'block'; + canvas.style.margin = '0 auto'; + canvas.style.borderRadius = '12px'; + canvas.style.background = '#0f0e17'; + wrap.appendChild(canvas); + const ctx = canvas.getContext('2d'); + ctx.scale(dpr, dpr); + return { canvas, ctx }; + } + + // ── Ergebnis-Anzeige (wird von game.html überschrieben) ────────────────── + // game.html setzt window.MGAPI.onResult = finishMinigame + // editor.html setzt window.MGAPI.onResult = previewResult + function onResult(won) { + if (typeof window._mgOnResult === 'function') { + window._mgOnResult(won); + } + } + + // ── Schrift-Helfer ─────────────────────────────────────────────────────── + function text(ctx, str, x, y, opts = {}) { + ctx.save(); + ctx.font = `${opts.weight || 'bold'} ${opts.size || 16}px ${opts.family || 'Nunito,sans-serif'}`; + ctx.fillStyle = opts.color || '#fff'; + ctx.textAlign = opts.align || 'center'; + ctx.textBaseline = opts.baseline || 'middle'; + if (opts.shadow) { + ctx.shadowColor = opts.shadow; + ctx.shadowBlur = opts.shadowBlur || 10; + } + ctx.fillText(str, x, y); + ctx.restore(); + } + + // ── Runde Rechtecke ────────────────────────────────────────────────────── + function roundRect(ctx, x, y, w, h, r, fill, stroke) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.arcTo(x + w, y, x + w, y + r, r); + ctx.lineTo(x + w, y + h - r); + ctx.arcTo(x + w, y + h, x + w - r, y + h, r); + ctx.lineTo(x + r, y + h); + ctx.arcTo(x, y + h, x, y + h - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + if (fill) { ctx.fillStyle = fill; ctx.fill(); } + if (stroke) { ctx.strokeStyle = stroke; ctx.stroke(); } + } + + // ── Game-Over / Win Screen ─────────────────────────────────────────────── + function resultScreen(ctx, W, H, won, msg) { + ctx.fillStyle = won ? 'rgba(16,185,129,0.85)' : 'rgba(239,68,68,0.85)'; + roundRect(ctx, W/2-120, H/2-50, 240, 100, 16, ctx.fillStyle, null); + text(ctx, won ? '🎉 Gewonnen!' : '💀 Verloren!', W/2, H/2-16, + { size: 22, weight: 'bold', family: "'Fredoka One',cursive", color: '#fff' }); + if (msg) text(ctx, msg, W/2, H/2+16, { size: 13, color: 'rgba(255,255,255,0.85)' }); + } + + // ── Öffentliche API ────────────────────────────────────────────────────── + return { makeCanvas, onResult, text, roundRect, resultScreen }; + +})(); diff --git a/minigames/basketball.js b/minigames/basketball.js new file mode 100644 index 0000000..4005822 --- /dev/null +++ b/minigames/basketball.js @@ -0,0 +1,176 @@ +/** + * minigames/basketball.js + * 🏀 Basketball — Wirf den Ball zum richtigen Zeitpunkt! + */ +window.MG_basketball = (function() { + + const ID = 'basketball'; + const EMOJI = '🏀'; + const NAME = 'Basketball'; + const DESC = 'Drücke zum richtigen Zeitpunkt, um den Ball zu werfen!'; + const CONTROLS = 'Klick / Leertaste'; + const MULTI = 3; + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + const theme = cfg.theme || { primary: '#f97316' }; + const WIN_BASKETS = cfg.winBaskets || 1; + + const HOOP_X = W * 0.72; + const HOOP_Y = H * 0.28; + const HOOP_W = 54; + const BALL_R = 18; + const BALL_START = { x: W * 0.18, y: H * 0.72 }; + + let ballX = BALL_START.x; + let ballY = BALL_START.y; + let vx = 0, vy = 0; + let flying = false; + let score = 0; + let misses = 0; + let stopped = false; + let raf, endTimer; + let result = null; + + let power = 0; + let powerDir = 1; + const powerMax = 100; + + function shoot() { + if (flying || result) return; + const dx = HOOP_X - BALL_START.x; + const dy = HOOP_Y - BALL_START.y - 20; + const speed = 8.5 + power * 0.12; // schneller: war 4 + 0.09 + const angle = Math.atan2(dy, dx) - 0.18 - (power - 50) * 0.005; + vx = Math.cos(angle) * speed; + vy = Math.sin(angle) * speed; + flying = true; + ballX = BALL_START.x; + ballY = BALL_START.y; + } + + const onAction = e => { + if (e.type === 'keydown' && e.code !== 'Space') return; + if (e.type === 'keydown') e.preventDefault(); + shoot(); + }; + document.addEventListener('keydown', onAction); + canvas.addEventListener('click', onAction); + canvas.addEventListener('touchstart', e => { e.preventDefault(); shoot(); }, { passive: false }); + + function reset() { + flying = false; + ballX = BALL_START.x; + ballY = BALL_START.y; + } + + function drawHoop() { + ctx.fillStyle = '#e5e7eb'; + ctx.fillRect(HOOP_X + HOOP_W * 0.4, HOOP_Y - 50, 8, 50); + ctx.strokeStyle = '#ef4444'; + ctx.lineWidth = 5; + ctx.beginPath(); + ctx.ellipse(HOOP_X + HOOP_W / 2, HOOP_Y, HOOP_W / 2, 8, 0, 0, Math.PI * 2); + ctx.stroke(); + ctx.strokeStyle = 'rgba(255,255,255,0.5)'; + ctx.lineWidth = 1.5; + for (let i = 0; i <= 4; i++) { + const nx = HOOP_X + (HOOP_W / 4) * i; + ctx.beginPath(); + ctx.moveTo(nx, HOOP_Y + 8); + ctx.lineTo(HOOP_X + HOOP_W / 2 + (i - 2) * 3, HOOP_Y + 36); + ctx.stroke(); + } + } + + function drawPower() { + if (flying || result) return; + const bx = 12, by = H - 28, bw = W * 0.45, bh = 14; + MGAPI.roundRect(ctx, bx, by, bw, bh, 6, 'rgba(255,255,255,0.1)', 'rgba(255,255,255,0.2)'); + const fill = Math.max(0, Math.min(1, power / powerMax)); + const color = fill < 0.4 ? '#22c55e' : fill < 0.7 ? '#f59e0b' : '#ef4444'; + MGAPI.roundRect(ctx, bx + 2, by + 2, (bw - 4) * fill, bh - 4, 4, color, null); + MGAPI.text(ctx, 'Kraft', bx + bw / 2, by + bh / 2, { size: 10, color: '#fff' }); + } + + function loop() { + if (stopped) return; + raf = requestAnimationFrame(loop); + + ctx.fillStyle = '#050508'; + ctx.fillRect(0, 0, W, H); + MGAPI.roundRect(ctx, 0, H - 10, W, 10, 0, '#1e293b', null); + + drawHoop(); + + if (!flying && !result) { + power += powerDir * 2.2; // Powerbar etwas schneller (war 1.8) + if (power >= powerMax) powerDir = -1; + if (power <= 0) powerDir = 1; + } + + if (flying) { + vy += 0.38; + ballX += vx; + ballY += vy; + + const inHoopX = ballX > HOOP_X && ballX < HOOP_X + HOOP_W; + const inHoopY = Math.abs(ballY - HOOP_Y) < 18; + if (inHoopX && inHoopY && vy > 0) { + score++; + if (score >= WIN_BASKETS && !endTimer) + endTimer = setTimeout(() => { result = 'win'; setTimeout(() => onDone(true), 900); }, 300); + reset(); + } + + if (ballY > H + 20 || ballX > W + 20 || ballX < -20) { + misses++; + if (misses >= 6 && score < WIN_BASKETS && !endTimer) + endTimer = setTimeout(() => { result = 'lose'; setTimeout(() => onDone(false), 900); }, 300); + reset(); + } + } + + // Ball + ctx.shadowColor = theme.primary; + ctx.shadowBlur = 15; + ctx.fillStyle = theme.primary; + ctx.beginPath(); + ctx.arc(ballX, ballY, BALL_R, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + ctx.strokeStyle = 'rgba(0,0,0,0.35)'; + ctx.lineWidth = 2; + ctx.beginPath(); ctx.arc(ballX, ballY, BALL_R, 0, Math.PI * 2); ctx.stroke(); + ctx.beginPath(); ctx.moveTo(ballX - BALL_R, ballY); ctx.lineTo(ballX + BALL_R, ballY); ctx.stroke(); + + drawPower(); + + MGAPI.text(ctx, `🏀 ${score} / ${WIN_BASKETS}`, W / 2, 18, { size: 14, color: theme.primary }); + if (!flying && !result) + MGAPI.text(ctx, 'Klick oder Leertaste zum Werfen', W / 2, H - 48, + { size: 11, color: 'rgba(255,255,255,0.45)' }); + + if (result) + MGAPI.resultScreen(ctx, W, H, result === 'win', + result === 'win' ? `${score} Körbe getroffen!` : `Nur ${score} — weiter üben!`); + } + + raf = requestAnimationFrame(loop); + return { + stop() { + stopped = true; + cancelAnimationFrame(raf); + clearTimeout(endTimer); + document.removeEventListener('keydown', onAction); + canvas.removeEventListener('click', onAction); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/catch.js b/minigames/catch.js new file mode 100644 index 0000000..83dcf30 --- /dev/null +++ b/minigames/catch.js @@ -0,0 +1,142 @@ +/** + * minigames/catch.js + * 🍎 Äpfel fangen — Bewege den Korb und fange fallende Früchte! + */ +window.MG_catch = (function() { + + const ID = 'catch'; + const EMOJI = '🍎'; + const NAME = 'Äpfel fangen'; + const DESC = 'Bewege den Korb mit ← → oder der Maus und fange 5 Äpfel!'; + const CONTROLS = '← → / A D / Maus'; + const MULTI = 3; + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + const theme = cfg.theme || { primary: '#22c55e' }; + const WIN = cfg.winCatch || 20; + const FRUITS = ['🍎','🍊','🍋','🍇','🍓','🍑']; + const BASKET_W = 70, BASKET_H = 24; + + let basket = { x: W / 2 - BASKET_W / 2 }; + let items = []; + let score = 0; + let missed = 0; + let frame = 0; + let stopped = false; + let raf, endTimer, result = null; + + const keys = {}; + let mouseX = null; + + const onKey = e => { keys[e.code] = e.type === 'keydown'; }; + const onMouse = e => { + const rect = canvas.getBoundingClientRect(); + mouseX = (e.clientX - rect.left) * (canvas.width / rect.width / (window.devicePixelRatio || 1)) - BASKET_W / 2; + }; + const onTouch = e => { + e.preventDefault(); + const rect = canvas.getBoundingClientRect(); + mouseX = (e.touches[0].clientX - rect.left) * (canvas.width / rect.width / (window.devicePixelRatio || 1)) - BASKET_W / 2; + }; + + document.addEventListener('keydown', onKey); + document.addEventListener('keyup', onKey); + canvas.addEventListener('mousemove', onMouse); + canvas.addEventListener('touchmove', onTouch, { passive: false }); + + function spawnItem() { + items.push({ + x: Math.random() * (W - 40) + 20, + y: -30, + vy: 3.2 + Math.random() * 1.2 + score * 0.22, // schneller + mehr Zuwachs + emoji: FRUITS[Math.floor(Math.random() * FRUITS.length)], + size: 26 + Math.random() * 10, + }); + } + + function loop() { + if (stopped) return; + raf = requestAnimationFrame(loop); + frame++; + + if (mouseX !== null) { + basket.x += (mouseX - basket.x) * 0.18; + } else { + if (keys['ArrowLeft'] || keys['KeyA']) basket.x -= 5.5; + if (keys['ArrowRight'] || keys['KeyD']) basket.x += 5.5; + } + basket.x = Math.max(0, Math.min(W - BASKET_W, basket.x)); + + const spawnRate = Math.max(22, 55 - score * 3); // häufiger spawnen + if (frame % spawnRate === 0 && !result) spawnItem(); + + items.forEach(it => { it.y += it.vy; }); + + items = items.filter(it => { + const basketTop = H - 50 - BASKET_H; + if (it.y + it.size / 2 > basketTop && + it.x > basket.x && it.x < basket.x + BASKET_W) { + score++; + if (score >= WIN && !endTimer) + endTimer = setTimeout(() => { result = 'win'; setTimeout(() => onDone(true), 900); }, 200); + return false; + } + if (it.y > H + 20) { + missed++; + if (missed >= 4 && score < WIN && !endTimer) + endTimer = setTimeout(() => { result = 'lose'; setTimeout(() => onDone(false), 900); }, 200); + return false; + } + return true; + }); + + ctx.fillStyle = '#050508'; + ctx.fillRect(0, 0, W, H); + + ctx.strokeStyle = 'rgba(255,255,255,0.08)'; + ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(0, H - 50); ctx.lineTo(W, H - 50); ctx.stroke(); + + ctx.font = '28px serif'; ctx.textAlign = 'center'; + items.forEach(it => ctx.fillText(it.emoji, it.x, it.y + it.size)); + + const bx = basket.x, by = H - 50 - BASKET_H; + MGAPI.roundRect(ctx, bx, by, BASKET_W, BASKET_H, 6, `${theme.primary}33`, theme.primary); + for (let i = 1; i < 4; i++) { + ctx.strokeStyle = `${theme.primary}66`; ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(bx+(BASKET_W/4)*i, by); ctx.lineTo(bx+(BASKET_W/4)*i, by+BASKET_H); ctx.stroke(); + } + ctx.font = '22px serif'; + ctx.fillText('🧺', bx + BASKET_W / 2, by + BASKET_H + 20); + + MGAPI.text(ctx, `🍎 ${score} / ${WIN} 💔 ${missed} / 4`, W / 2, 18, { size: 13, color: theme.primary }); + + if (!result) + MGAPI.text(ctx, '← → oder Maus bewegen', W / 2, H - 10, { size: 10, color: 'rgba(255,255,255,0.3)' }); + + if (result) + MGAPI.resultScreen(ctx, W, H, result === 'win', + result === 'win' ? `${score} Früchte gefangen!` : `${missed} verpasst — nächstes Mal!`); + } + + raf = requestAnimationFrame(loop); + return { + stop() { + stopped = true; + cancelAnimationFrame(raf); + clearTimeout(endTimer); + document.removeEventListener('keydown', onKey); + document.removeEventListener('keyup', onKey); + canvas.removeEventListener('mousemove', onMouse); + canvas.removeEventListener('touchmove', onTouch); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/flappy.js b/minigames/flappy.js new file mode 100644 index 0000000..5bc47d6 --- /dev/null +++ b/minigames/flappy.js @@ -0,0 +1,268 @@ +/** + * minigames/flappy.js + * 🐦 Flappy — Dark-Neon-Look, flüssig (Fixed-Timestep + Interpolation). + * Die Spielfigur ist die im Editor gewählte Welt-Figur (als leuchtender Komet mit Schweif). + * Eigene Vektor-/Canvas-Grafik, keine externen Assets. + */ +window.MG_flappy = (function () { + + const ID = 'flappy', EMOJI = '🐦', NAME = 'Flappy Bird'; + const DESC = '3 Leben, 15 Hindernisse. Klick oder Leertaste zum Fliegen!'; + const CONTROLS = 'Klick / Leertaste'; + const MULTI = 1; + + const lerp = (a, b, t) => a + (b - a) * t; + const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v); + const hx = (h, i) => parseInt(h.replace('#', '').substr(i, 2), 16); + function shade(hex, amt) { + let r = clamp(hx(hex, 0) + amt, 0, 255), g = clamp(hx(hex, 2) + amt, 0, 255), b = clamp(hx(hex, 4) + amt, 0, 255); + return '#' + [r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join(''); + } + function rgba(hex, a) { return `rgba(${hx(hex, 0)},${hx(hex, 2)},${hx(hex, 4)},${a})`; } + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + + let accent = (cfg && cfg.theme && cfg.theme.primary) || '#5ec5b8'; + if (!/^#[0-9a-f]{6}$/i.test(accent)) accent = '#5ec5b8'; + const accentLight = shade(accent, 70); + const accentDeep = shade(accent, -120); + const figEmoji = (cfg && cfg.figure) || '🐦'; + const WIN_PIPES = (cfg && (cfg.win || cfg.winPipes)) || 15; + const MAX_LIVES = (cfg && cfg.lives) || 3; + + // ── Physik (pro festem 1/60-Schritt) — einstellbar via cfg.speed / cfg.gap ── + const STEP = 1000 / 60; + const GRAV = 0.42, FLAP = -7.2, PW = 46; + const BASE_SPEED = (cfg && cfg.speed) || 2.7; + const SPEED_INC = (cfg && cfg.rampSpeed === false) ? 0 : 0.07; // Tempo steigt pro Durchgang (Toggle) + const BASE_GAP = (cfg && cfg.gap) || 168; + const GAP_DEC = (cfg && cfg.shrinkGap === false) ? 0 : 1.6; // Durchgänge werden enger (Toggle) + const MIN_GAP = Math.max(90, BASE_GAP - 36), minTop = 36; + const GROUND_H = Math.min(54, Math.round(H * 0.13)); + const PLAY_H = H - GROUND_H; + const BIRD_X = Math.round(W * 0.28); + const BR = 16; + + // ── Zustand ── + let state = 'ready'; + let bird = { y: PLAY_H / 2, py: PLAY_H / 2, vy: 0, angle: 0 }; + let pipes = [], trail = []; + let score = 0, lives = MAX_LIVES, speed = BASE_SPEED; + let worldScroll = 0, pWorldScroll = 0; + let simTime = 0, flapAnim = 0, shake = 0, flash = 0; + let stopped = false, raf = null, endTimer = null, respawnTimer = null; + let lastTime = 0, acc = 0; + let puffs = [], pops = [], confetti = []; + + const stars = []; + for (let i = 0; i < 34; i++) + stars.push({ x: Math.random() * W, y: Math.random() * PLAY_H, r: Math.random() < 0.3 ? 2 : 1, a: 0.3 + Math.random() * 0.5, p: Math.random() * 6.28 }); + + // Dunkler Himmel in Welt-Farbe (einmalig) + const skyGrad = ctx.createLinearGradient(0, 0, 0, H); + skyGrad.addColorStop(0, '#080810'); + skyGrad.addColorStop(0.6, shade(accentDeep, -40)); + skyGrad.addColorStop(1, accentDeep); + + // Figur einmal mit Neon-Glow als Sprite vorrendern → danach nur noch blitten + const SPR = 72, dpr = window.devicePixelRatio || 1; + const spr = document.createElement('canvas'); + spr.width = SPR * dpr; spr.height = SPR * dpr; + const sctx = spr.getContext('2d'); + sctx.scale(dpr, dpr); + sctx.textAlign = 'center'; sctx.textBaseline = 'middle'; + sctx.font = `${Math.round(SPR * 0.55)}px serif`; + sctx.shadowColor = accentLight; sctx.shadowBlur = 18; + sctx.fillText(figEmoji, SPR / 2, SPR / 2); + sctx.shadowBlur = 10; + sctx.fillText(figEmoji, SPR / 2, SPR / 2); + + function curGAP(sc) { return Math.max(MIN_GAP, BASE_GAP - sc * GAP_DEC); } + function rndGapY(sc) { + const g = curGAP(sc), maxTop = PLAY_H - g - 40; + return minTop + Math.random() * Math.max(10, maxTop - minTop); + } + function spawnPipe() { pipes.push({ x: W + PW, px: W + PW, gap: rndGapY(score), g: curGAP(score), passed: false }); } + + function flap() { + if (stopped || state === 'dying' || state === 'won') return; + if (state === 'ready') { state = 'play'; spawnPipe(); } + bird.vy = FLAP; flapAnim = 1; + for (let i = 0; i < 4; i++) puffs.push({ x: BIRD_X - BR, y: bird.y + (Math.random() - 0.5) * 14, r: 2, a: 0.6 }); + } + const onKey = e => { if (e.code === 'Space' || e.code === 'ArrowUp') { e.preventDefault(); flap(); } }; + document.addEventListener('keydown', onKey); + canvas.addEventListener('mousedown', flap); + canvas.addEventListener('touchstart', e => { e.preventDefault(); flap(); }, { passive: false }); + + function hitPipe(p) { + const r = BR - 4; + if (BIRD_X + r > p.x && BIRD_X - r < p.x + PW) + return bird.y - r < p.gap || bird.y + r > p.gap + p.g; + return false; + } + function loseLife() { + if (state === 'dying' || state === 'won') return; + shake = 14; flash = 0.55; lives--; state = 'dying'; + bird.vy = Math.min(bird.vy, 1.5); + if (lives <= 0) endTimer = setTimeout(() => onDone(false), 1400); + else respawnTimer = setTimeout(() => { + bird = { y: PLAY_H / 2, py: PLAY_H / 2, vy: 0, angle: 0 }; + pipes = pipes.filter(p => p.x > BIRD_X + 150); trail = []; state = 'play'; + }, 1100); + } + function win() { + if (state === 'won') return; + state = 'won'; + const cols = [accent, accentLight, '#ffffff', shade(accent, 40)]; + for (let i = 0; i < 70; i++) + confetti.push({ x: W / 2, y: PLAY_H * 0.4, vx: (Math.random() - 0.5) * 9, vy: -3 - Math.random() * 6, c: cols[i % cols.length], r: 3 + Math.random() * 3, a: 1 }); + endTimer = setTimeout(() => onDone(true), 1800); + } + + function step() { + pWorldScroll = worldScroll; + simTime += STEP; + flapAnim = Math.max(0, flapAnim - 0.08); + if (shake > 0) shake = Math.max(0, shake - 0.7); + if (flash > 0) flash = Math.max(0, flash - 0.04); + stars.forEach(s => { s.x -= speed * 0.12; if (s.x < -2) { s.x = W + 2; s.y = Math.random() * PLAY_H; } }); + + if (state === 'ready') { bird.py = bird.y; bird.y = PLAY_H / 2 + Math.sin(simTime * 0.005) * 8; return; } + if (state === 'won') { + bird.py = bird.y; + confetti.forEach(p => { p.x += p.vx; p.y += p.vy; p.vy += 0.25; p.a -= 0.009; }); + confetti = confetti.filter(p => p.a > 0); return; + } + + bird.py = bird.y; + bird.vy += GRAV; bird.y += bird.vy; + bird.angle = lerp(bird.angle, clamp(bird.vy * 0.06, -0.5, 1.4), 0.18); + trail.unshift(bird.y); if (trail.length > 8) trail.pop(); + + puffs.forEach(p => { p.r += 1.1; p.a -= 0.06; p.x -= speed; }); + puffs = puffs.filter(p => p.a > 0); + pops.forEach(p => { p.y -= 0.8; p.a -= 0.02; p.scale = Math.min(1.3, p.scale + 0.06); }); + pops = pops.filter(p => p.a > 0); + + if (state === 'dying') { bird.angle = Math.min(1.7, bird.angle + 0.12); return; } + + worldScroll += speed; + pipes.forEach(p => { p.px = p.x; p.x -= speed; }); + if (!pipes.length || pipes[pipes.length - 1].x < W - 200) spawnPipe(); + pipes = pipes.filter(p => p.x > -PW - 10); + pipes.forEach(p => { + if (!p.passed && p.x + PW < BIRD_X) { + p.passed = true; score++; speed = BASE_SPEED + score * SPEED_INC; + pops.push({ x: BIRD_X + 26, y: bird.y - 22, a: 1, scale: 0.6 }); + if (score >= WIN_PIPES) win(); + } + }); + if (bird.y + (BR - 4) > PLAY_H) { bird.y = PLAY_H - (BR - 4); loseLife(); } + else if (bird.y - (BR - 4) < 0) { bird.y = BR - 4; bird.vy = 0; } + if (state === 'play') for (const p of pipes) if (hitPipe(p)) { loseLife(); break; } + } + + function drawPipe(x, p) { + const topH = p.gap, botY = p.gap + p.g, glow = rgba(accent, 0.16); + const body = (yy, hh) => { + if (hh <= 0) return; + ctx.fillStyle = glow; ctx.fillRect(x - 6, yy, PW + 12, hh); + ctx.fillStyle = 'rgba(10,12,24,0.80)'; MGAPI.roundRect(ctx, x, yy, PW, hh, 6, 'rgba(10,12,24,0.80)', null); + ctx.fillStyle = accent; ctx.fillRect(x + 3, yy, 3, hh); ctx.fillRect(x + PW - 6, yy, 3, hh); + ctx.fillStyle = accentLight; ctx.fillRect(x + PW / 2 - 1, yy, 2, hh); + }; + const cap = yy => { + ctx.fillStyle = glow; ctx.fillRect(x - 9, yy - 2, PW + 18, 20); + MGAPI.roundRect(ctx, x - 5, yy, PW + 10, 16, 5, accent, null); + ctx.fillStyle = accentLight; ctx.fillRect(x - 2, yy + 3, PW + 4, 3); + }; + body(0, topH - 18); body(botY + 18, PLAY_H - botY - 18); + cap(topH - 18); cap(botY + 2); + } + + function drawFloor(scroll) { + const gy = PLAY_H; + ctx.fillStyle = rgba(accent, 0.20); ctx.fillRect(0, gy - 3, W, 6); + ctx.fillStyle = accentLight; ctx.fillRect(0, gy - 1, W, 2); + ctx.fillStyle = '#070710'; ctx.fillRect(0, gy, W, GROUND_H); + ctx.fillStyle = rgba(accent, 0.18); + const tile = 34, off = -(((scroll % tile) + tile) % tile); + for (let x = off; x < W; x += tile) ctx.fillRect(x, gy + 6, 16, 2); + } + + function render(alpha) { + let sx = 0, sy = 0; + if (shake > 0.2) { sx = (Math.random() - 0.5) * shake; sy = (Math.random() - 0.5) * shake; } + ctx.save(); ctx.translate(sx, sy); + + ctx.fillStyle = skyGrad; ctx.fillRect(-12, -12, W + 24, H + 24); + // Sterne + stars.forEach(s => { ctx.globalAlpha = s.a * (0.5 + 0.5 * Math.sin(simTime * 0.004 + s.p)); ctx.fillStyle = '#cdd6ff'; ctx.fillRect(s.x, s.y, s.r, s.r); }); + ctx.globalAlpha = 1; + + pipes.forEach(p => drawPipe(lerp(p.px, p.x, alpha), p)); + drawFloor(lerp(pWorldScroll, worldScroll, alpha)); + + // Flap-Püffchen + puffs.forEach(p => { ctx.globalAlpha = p.a; ctx.fillStyle = accentLight; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); }); + ctx.globalAlpha = 1; + + const headY = lerp(bird.py, bird.y, alpha); + // Komet-Schweif + for (let i = trail.length - 1; i >= 0; i--) { + const t = 1 - i / trail.length; + ctx.globalAlpha = 0.28 * t; + ctx.fillStyle = accentLight; + ctx.beginPath(); ctx.arc(BIRD_X - i * 7, trail[i], (BR * 0.55) * t + 1.5, 0, Math.PI * 2); ctx.fill(); + } + ctx.globalAlpha = 1; + // Figur (Sprite-Blit, rotiert) + ctx.save(); ctx.translate(BIRD_X, headY); ctx.rotate(bird.angle); + ctx.drawImage(spr, -SPR / 2, -SPR / 2, SPR, SPR); ctx.restore(); + + pops.forEach(p => { ctx.save(); ctx.globalAlpha = Math.max(0, p.a); MGAPI.text(ctx, '+1', p.x, p.y, { size: 18 * p.scale, color: accentLight, weight: 'bold' }); ctx.restore(); }); + confetti.forEach(p => { ctx.globalAlpha = Math.max(0, p.a); ctx.fillStyle = p.c; ctx.fillRect(p.x, p.y, p.r, p.r); }); + ctx.globalAlpha = 1; + + ctx.restore(); + + // HUD + MGAPI.text(ctx, `${score} / ${WIN_PIPES}`, W / 2, 28, { size: 22, color: '#fff', weight: 'bold', family: "'Fredoka One',cursive" }); + const hearts = '❤️'.repeat(Math.max(0, lives)) + '🤍'.repeat(Math.max(0, MAX_LIVES - lives)); + MGAPI.text(ctx, hearts, 10, 16, { align: 'left', size: 13 }); + if (state === 'ready') + MGAPI.text(ctx, '👆 Tippen oder Leertaste', W / 2, PLAY_H * 0.62, { size: 15, color: accentLight, weight: 'bold' }); + if (flash > 0) { ctx.fillStyle = `rgba(255,255,255,${flash})`; ctx.fillRect(0, 0, W, H); } + if (state === 'dying' && lives <= 0) MGAPI.resultScreen(ctx, W, H, false, `${score} Hindernisse — nochmal!`); + if (state === 'won') MGAPI.resultScreen(ctx, W, H, true, `Alle ${WIN_PIPES} geschafft! 🎉`); + } + + function loop(ts) { + if (stopped) return; + raf = requestAnimationFrame(loop); + if (!lastTime) lastTime = ts; + acc += Math.min(ts - lastTime, STEP * 5); + lastTime = ts; + while (acc >= STEP) { step(); acc -= STEP; } + render(acc / STEP); + } + + raf = requestAnimationFrame(loop); + return { + stop() { + stopped = true; cancelAnimationFrame(raf); + clearTimeout(endTimer); clearTimeout(respawnTimer); + document.removeEventListener('keydown', onKey); + canvas.removeEventListener('mousedown', flap); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/flappy.js.bak_084309 b/minigames/flappy.js.bak_084309 new file mode 100644 index 0000000..9e8d90b --- /dev/null +++ b/minigames/flappy.js.bak_084309 @@ -0,0 +1,174 @@ +/** + * minigames/flappy.js + * 🐦 Flappy Bird — Klicke oder Leertaste, um durch die Röhren zu fliegen! + */ +window.MG_flappy = (function() { + + const ID = 'flappy'; + const EMOJI = '🐦'; + const NAME = 'Flappy Bird'; + const DESC = '3 Leben, 15 Hindernisse. Klick oder Leertaste zum Fliegen!'; + const CONTROLS = 'Klick / Leertaste'; + const MULTI = 1; + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + const theme = cfg.theme || { primary: '#06b6d4' }; + const WIN_PIPES = cfg.winPipes || 15; + const MAX_LIVES = cfg.lives || 3; + + const GRAV = 0.35; + const JUMP = -7; + const PW = 38; + const BASE_SPEED = 4.5; // spürbar schneller von Anfang + const SPEED_INC = 0.16; + const BASE_GAP = 175; // großzügige Lücke zu Beginn + const MIN_GAP = 135; // bleibt auch am Ende gut spielbar + const GAP_DEC = 2.7; // sanfte Verkleinerung pro Hindernis + const minTop = 35; + + let bird = { y: H / 2, vy: 0 }; + let pipes = [{ x: W, gap: rndGap(0) }]; + let score = 0; + let speed = BASE_SPEED; + let lives = MAX_LIVES; + let dead = false; + let dying = false; + let started = false; + let stopped = false; + let raf, endTimer, respawnTimer; + + function curGAP(sc) { + return Math.max(MIN_GAP, BASE_GAP - sc * GAP_DEC); + } + function rndGap(sc) { + const gap = curGAP(sc); + const maxTop = H - gap - 35; + return minTop + Math.random() * Math.max(0, maxTop - minTop); + } + + 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); + canvas.addEventListener('touchstart', e => { e.preventDefault(); jump(); }, { passive: false }); + + function die() { + if (dying) return; + dying = true; + dead = true; + lives--; + if (lives <= 0) { + endTimer = setTimeout(() => onDone(false), 1100); + } else { + respawnTimer = setTimeout(() => { + bird = { y: H / 2, vy: 0 }; + pipes = pipes.filter(p => p.x > 120); + if (!pipes.length) pipes = [{ x: W, gap: rndGap(score) }]; + dead = false; + dying = false; + }, 900); + } + } + + function loop() { + if (stopped) return; + raf = requestAnimationFrame(loop); + + const grad = ctx.createLinearGradient(0, 0, 0, H); + grad.addColorStop(0, '#0a0a1a'); + grad.addColorStop(1, '#050508'); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, W, H); + + if (started && !dead) { + bird.vy += GRAV; + bird.y += bird.vy; + + pipes.forEach(p => p.x -= speed); + if (pipes[pipes.length - 1].x < W - 210) + pipes.push({ x: W, gap: rndGap(score) }); + pipes = pipes.filter(p => p.x > -PW); + + pipes.forEach(p => { + if (p.x + PW < 50 && !p.passed) { + p.passed = true; + score++; + speed = BASE_SPEED + score * SPEED_INC; + } + }); + + if (bird.y < 0 || bird.y + 20 > H) die(); + pipes.forEach(p => { + const g = curGAP(score); + if (50 < p.x + PW && 70 > p.x && + (bird.y < p.gap || bird.y + 20 > p.gap + g)) die(); + }); + + if (score >= WIN_PIPES && !endTimer) + endTimer = setTimeout(() => onDone(true), 400); + } + + // Röhren + pipes.forEach(p => { + const g = curGAP(score); + MGAPI.roundRect(ctx, p.x, 0, PW, p.gap - 8, 4, `${theme.primary}cc`, null); + MGAPI.roundRect(ctx, p.x - 3, p.gap - 12, PW + 6, 12, 4, `${theme.primary}ee`, null); + MGAPI.roundRect(ctx, p.x, p.gap + g + 8, PW, H - p.gap - g - 8, 4, `${theme.primary}cc`, null); + MGAPI.roundRect(ctx, p.x - 3, p.gap + g, PW + 6, 12, 4, `${theme.primary}ee`, null); + }); + + // Vogel + ctx.shadowColor = dead ? '#ef4444' : theme.primary; + ctx.shadowBlur = 12; + 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.shadowBlur = 0; + ctx.fillStyle = '#fff'; + ctx.beginPath(); ctx.arc(50 + 20, bird.y + 7, 4, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = '#000'; + ctx.beginPath(); ctx.arc(50 + 21, bird.y + 7, 2, 0, Math.PI * 2); ctx.fill(); + + // HUD + MGAPI.text(ctx, `🐦 ${score} / ${WIN_PIPES}`, 8, 14, + { align: 'left', size: 13, color: theme.primary }); + const hearts = '❤️'.repeat(lives) + '🖤'.repeat(Math.max(0, MAX_LIVES - lives)); + MGAPI.text(ctx, hearts, W / 2, 14, { align: 'center', size: 12 }); + MGAPI.text(ctx, `⚡ ${speed.toFixed(1)}`, W - 8, 14, + { align: 'right', size: 11, color: 'rgba(255,255,255,0.4)' }); + + if (!started) + MGAPI.text(ctx, 'Klicken oder Leertaste', W / 2, H / 2 + 40, + { size: 13, color: 'rgba(255,255,255,0.7)' }); + + if (dead && lives <= 0) + MGAPI.resultScreen(ctx, W, H, false, `Nur ${score} Hindernisse — nächstes Mal!`); + else if (dead && dying) + MGAPI.text(ctx, `${lives} ${lives === 1 ? 'Leben' : 'Leben'} übrig`, W / 2, H / 2, + { size: 16, color: '#ef4444' }); + + if (score >= WIN_PIPES && !dead) + MGAPI.resultScreen(ctx, W, H, true, `Alle ${WIN_PIPES} Hindernisse geschafft!`); + } + + raf = requestAnimationFrame(loop); + return { + stop() { + stopped = true; + cancelAnimationFrame(raf); + clearTimeout(endTimer); + clearTimeout(respawnTimer); + document.removeEventListener('keydown', onKey); + canvas.removeEventListener('click', jump); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, { ...cfg, winPipes: 15, lives: 3 }, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/flappy2p.js b/minigames/flappy2p.js new file mode 100644 index 0000000..bbd667e --- /dev/null +++ b/minigames/flappy2p.js @@ -0,0 +1,457 @@ +/** + * minigames/flappy2p.js + * 🐦🐦 Flappy-Duell — 2-Spieler "Best of 2-5", basierend auf flappy.js + * 🔵 P1 = ↑ Pfeil · 🔴 P2 = W. Beide fliegen durch dieselben Röhren. + * Stirbt einer (Wand/Röhre/Boden), gibt es einen Punkt für den anderen → neue Runde. + * Spielfiguren kollidieren NICHT miteinander. + */ +window.MG_flappy2p = (function () { + + const ID = 'flappy2p'; + const EMOJI = '🐦🐦'; + const NAME = 'Flappy-Duell'; + const DESC = 'Best of 2-5 · 🔵 ↑ Pfeil oben gegen 🔴 W. Wer abstürzt, gibt einen Punkt ab.'; + const CONTROLS = '↑ / W'; + const MULTI = 1; + const REQUIRES = '2p'; + + const lerp = (a, b, t) => a + (b - a) * t; + const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v); + const hx = (h, i) => parseInt(h.replace('#', '').substr(i, 2), 16); + function shade(hex, amt) { + const r = clamp(hx(hex, 0) + amt, 0, 255), g = clamp(hx(hex, 2) + amt, 0, 255), b = clamp(hx(hex, 4) + amt, 0, 255); + return '#' + [r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join(''); + } + + const P1_COL = '#3b82f6', P1_LIGHT = '#93c5fd'; + const P2_COL = '#ef4444', P2_LIGHT = '#fca5a5'; + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + + // ── Einstellungen (vom mgSettings durchgereicht) ── + const BEST_OF = clamp(parseInt(cfg.bestOf) || 3, 2, 5); // 2..5 + const TARGET = Math.ceil(BEST_OF / 2); // Runden bis Sieg + const BASE_SPEED = (cfg && cfg.speed) || 2.7; + const SPEED_INC = (cfg && cfg.rampSpeed === false) ? 0 : 0.07; + const BASE_GAP = (cfg && cfg.gap) || 168; + const GAP_DEC = (cfg && cfg.shrinkGap === false) ? 0 : 1.6; + const MIN_GAP = Math.max(90, BASE_GAP - 36); + const minTop = 36; + + // ── Physik (pro festem 1/60-Schritt) ── + const STEP = 1000 / 60; + const GRAV = 0.42, FLAP = -7.2, PW = 46; + const GROUND_H = Math.min(54, Math.round(H * 0.13)); + const PLAY_H = H - GROUND_H; + const BIRD_X = Math.round(W * 0.28); + const BR = 16; + + // ── Theme-Hintergrund ── + let accent = (cfg && cfg.theme && cfg.theme.primary) || '#5ec5b8'; + if (!/^#[0-9a-f]{6}$/i.test(accent)) accent = '#5ec5b8'; + const accentLight = shade(accent, 70); + const accentDeep = shade(accent, -120); + const fig1Emoji = (cfg && cfg.figure) || '🐦'; + const fig2Emoji = (cfg && cfg.figure2) || '🐤'; + + // ── Match-State ── + const scores = [0, 0]; + let roundIdx = 0; + let state = 'intro'; // intro | playing | roundEnd | duelEnd + let stateTimer = 0; + let roundResult = null; + let startBtnRect = null; + let stopped = false, raf = null; + let lastTime = 0, acc = 0; + + // Pre-rendered Glow-Sprites pro Spieler (einmalig) + const SPR = 72, dpr = window.devicePixelRatio || 1; + function makeSprite(emoji, glow) { + const s = document.createElement('canvas'); + s.width = SPR * dpr; s.height = SPR * dpr; + const sx = s.getContext('2d'); sx.scale(dpr, dpr); + sx.textAlign = 'center'; sx.textBaseline = 'middle'; + sx.font = `${Math.round(SPR * 0.55)}px serif`; + sx.shadowColor = glow; sx.shadowBlur = 18; sx.fillText(emoji, SPR/2, SPR/2); + sx.shadowBlur = 10; sx.fillText(emoji, SPR/2, SPR/2); + return s; + } + const sprP1 = makeSprite(fig1Emoji, P1_LIGHT); + const sprP2 = makeSprite(fig2Emoji, P2_LIGHT); + + // Sky einmal vorrendern + const skyGrad = ctx.createLinearGradient(0, 0, 0, H); + skyGrad.addColorStop(0, '#080810'); + skyGrad.addColorStop(0.6, shade(accentDeep, -40)); + skyGrad.addColorStop(1, accentDeep); + + // Deterministische Sterne im Hintergrund + const stars = []; + for (let i = 0; i < 34; i++) + stars.push({ x: Math.random()*W, y: Math.random()*PLAY_H, r: Math.random()<0.3?2:1, a: 0.3+Math.random()*0.5, p: Math.random()*6.28 }); + + // ── Runden-State ── + let birds, pipes, score, speed, worldScroll, pWorldScroll, simTime, shake, flash, puffs; + + function curGAP(sc) { return Math.max(MIN_GAP, BASE_GAP - sc * GAP_DEC); } + function rndGapY(sc) { + const g = curGAP(sc), maxTop = PLAY_H - g - 40; + return minTop + Math.random() * Math.max(10, maxTop - minTop); + } + function spawnPipe() { pipes.push({ x: W + PW, px: W + PW, gap: rndGapY(score), g: curGAP(score), passed: false }); } + + function makeBird(yFrac) { + const y0 = PLAY_H * yFrac; + return { y: y0, py: y0, vy: 0, angle: 0, alive: true, flapAnim: 0, trail: [] }; + } + + function newRound() { + birds = [ makeBird(0.4), makeBird(0.6) ]; + pipes = []; + spawnPipe(); + score = 0; + speed = BASE_SPEED; + worldScroll = 0; pWorldScroll = 0; + simTime = 0; shake = 0; flash = 0; + puffs = []; + acc = 0; + lastTime = 0; + state = 'playing'; + stateTimer = 0; + roundResult = null; + } + + function endRound(winnerIdx) { + if (winnerIdx === 0 || winnerIdx === 1) scores[winnerIdx]++; + roundResult = winnerIdx; + shake = 14; flash = 0.5; + state = 'roundEnd'; + stateTimer = 0; + } + + function advanceRound() { + roundIdx++; + if (scores[0] >= TARGET || scores[1] >= TARGET || roundIdx >= BEST_OF) { + state = 'duelEnd'; stateTimer = 0; + } else { + newRound(); + } + } + + function finalWinner() { + if (scores[0] > scores[1]) return 0; + if (scores[1] > scores[0]) return 1; + return -1; + } + + function finishUp() { + if (stopped) return; + stopped = true; + cancelAnimationFrame(raf); + document.removeEventListener('keydown', onKey); + canvas.removeEventListener('mousedown', onCanvasDown); + canvas.removeEventListener('touchstart', onCanvasTouch); + onDone(finalWinner()); + } + + // ── Eingabe ── + function flap(i) { + if (state !== 'playing') return; + const b = birds[i]; if (!b || !b.alive) return; + b.vy = FLAP; b.flapAnim = 1; + for (let k = 0; k < 3; k++) + puffs.push({ x: BIRD_X - BR + (Math.random()-0.5)*6, y: b.y + (Math.random()-0.5)*14, r: 2, a: 0.6, col: i===0 ? P1_LIGHT : P2_LIGHT }); + } + + function onKey(e) { + if (state === 'intro') return; // Start nur per Klick + if (state !== 'playing') return; + const k = e.key; + if (k === 'ArrowUp') { flap(0); e.preventDefault(); } + else if (k === 'w' || k === 'W') { flap(1); e.preventDefault(); } + } + + function canvasCoords(clientX, clientY) { + const r = canvas.getBoundingClientRect(); + const d = window.devicePixelRatio || 1; + const x = (clientX - r.left) * (canvas.width / r.width / d); + const y = (clientY - r.top) * (canvas.height / r.height / d); + return { x, y }; + } + function pointInBtn(x, y) { + return startBtnRect && x >= startBtnRect.x && x <= startBtnRect.x + startBtnRect.w + && y >= startBtnRect.y && y <= startBtnRect.y + startBtnRect.h; + } + function onCanvasDown(e) { + const p = canvasCoords(e.clientX, e.clientY); + if (state === 'intro') { + if (pointInBtn(p.x, p.y)) newRound(); + return; + } + if (state === 'playing') { + // Tap-Steuerung als Bonus (für Touch/Tablets): linke Hälfte = P2, rechte Hälfte = P1 + flap(p.x < W / 2 ? 1 : 0); + } + } + function onCanvasTouch(e) { + const t = e.changedTouches && e.changedTouches[0]; + if (!t) return; + const p = canvasCoords(t.clientX, t.clientY); + if (state === 'intro') { + if (pointInBtn(p.x, p.y)) { e.preventDefault(); newRound(); } + return; + } + if (state === 'playing') { + e.preventDefault(); + flap(p.x < W / 2 ? 1 : 0); + } + } + document.addEventListener('keydown', onKey); + canvas.addEventListener('mousedown', onCanvasDown); + canvas.addEventListener('touchstart', onCanvasTouch, { passive: false }); + + function hitPipe(b, p) { + const r = BR - 4; + if (BIRD_X + r > p.x && BIRD_X - r < p.x + PW) + return b.y - r < p.gap || b.y + r > p.gap + p.g; + return false; + } + + // ── Physik-Schritt (1/60) ── + function step() { + pWorldScroll = worldScroll; + simTime += STEP; + if (shake > 0) shake = Math.max(0, shake - 0.7); + if (flash > 0) flash = Math.max(0, flash - 0.04); + + // Birds bewegen + for (const b of birds) { + if (!b.alive) continue; + b.py = b.y; + b.vy += GRAV; + b.y += b.vy; + b.angle = lerp(b.angle, clamp(b.vy * 0.06, -0.5, 1.4), 0.18); + b.flapAnim = Math.max(0, b.flapAnim - 0.08); + b.trail.unshift(b.y); if (b.trail.length > 8) b.trail.pop(); + } + // Puff-Partikel + puffs.forEach(p => { p.r += 1.1; p.a -= 0.06; p.x -= speed; }); + puffs = puffs.filter(p => p.a > 0); + + // Röhren scrollen + worldScroll += speed; + pipes.forEach(p => { p.px = p.x; p.x -= speed; }); + if (!pipes.length || pipes[pipes.length - 1].x < W - 210) spawnPipe(); + pipes = pipes.filter(p => p.x > -PW - 10); + pipes.forEach(p => { + if (!p.passed && p.x + PW < BIRD_X) { + p.passed = true; score++; + speed = BASE_SPEED + score * SPEED_INC; + } + }); + + // Kollisionen pro Vogel (Vögel untereinander NICHT) + for (const b of birds) { + if (!b.alive) continue; + if (b.y + (BR - 4) > PLAY_H) { b.y = PLAY_H - (BR - 4); b.alive = false; continue; } + if (b.y - (BR - 4) < 0) { b.y = BR - 4; b.alive = false; continue; } + for (const p of pipes) { if (hitPipe(b, p)) { b.alive = false; break; } } + } + + // Round-Ende? + if (!birds[0].alive || !birds[1].alive) { + let winner; + if (!birds[0].alive && !birds[1].alive) winner = -1; + else if (!birds[0].alive) winner = 1; + else winner = 0; + endRound(winner); + } + } + + // ── Render ── + function drawPipe(x, p) { + const topH = p.gap, botY = p.gap + p.g, glow = `${accent}28`; + const body = (yy, hh) => { + if (hh <= 0) return; + ctx.fillStyle = glow; ctx.fillRect(x - 6, yy, PW + 12, hh); + MGAPI.roundRect(ctx, x, yy, PW, hh, 6, 'rgba(10,12,24,0.80)', null); + ctx.fillStyle = accent; ctx.fillRect(x + 3, yy, 3, hh); ctx.fillRect(x + PW - 6, yy, 3, hh); + ctx.fillStyle = accentLight; ctx.fillRect(x + PW / 2 - 1, yy, 2, hh); + }; + const cap = yy => { + ctx.fillStyle = glow; ctx.fillRect(x - 9, yy - 2, PW + 18, 20); + MGAPI.roundRect(ctx, x - 5, yy, PW + 10, 16, 5, accent, null); + ctx.fillStyle = accentLight; ctx.fillRect(x - 2, yy + 3, PW + 4, 3); + }; + body(0, topH - 18); body(botY + 18, PLAY_H - botY - 18); + cap(topH - 18); cap(botY + 2); + } + + function drawFloor(scroll) { + const gy = PLAY_H; + ctx.fillStyle = `${accent}33`; ctx.fillRect(0, gy - 3, W, 6); + ctx.fillStyle = accentLight; ctx.fillRect(0, gy - 1, W, 2); + ctx.fillStyle = '#070710'; ctx.fillRect(0, gy, W, GROUND_H); + ctx.fillStyle = `${accent}2e`; + const tile = 34, off = -(((scroll % tile) + tile) % tile); + for (let x = off; x < W; x += tile) ctx.fillRect(x, gy + 6, 16, 2); + } + + function drawStartButton() { + const bw = Math.min(320, W * 0.62), bh = 56; + const bx = (W - bw) / 2, by = H / 2 + 10; + startBtnRect = { x: bx, y: by, w: bw, h: bh }; + ctx.fillStyle = 'rgba(0,0,0,0.45)'; + MGAPI.roundRect(ctx, bx, by + 4, bw, bh, 14, 'rgba(0,0,0,0.45)', null); + const grd = ctx.createLinearGradient(0, by, 0, by + bh); + grd.addColorStop(0, '#7c3aed'); grd.addColorStop(1, '#5b21b6'); + MGAPI.roundRect(ctx, bx, by, bw, bh, 14, grd, null); + MGAPI.roundRect(ctx, bx + 4, by + 3, bw - 8, bh / 2 - 4, 12, 'rgba(255,255,255,0.18)', null); + MGAPI.text(ctx, '▶ Duell starten', bx + bw / 2, by + bh / 2, { + size: 22, color: '#fff', weight: 'bold', family: "'Fredoka One',cursive" + }); + } + + function drawOverlayText(title, sub, color) { + ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, 0, W, H); + MGAPI.text(ctx, title, W/2, H/2 - 22, { + size: 28, weight:'bold', color: color || '#fff', + family: "'Fredoka One',cursive", shadow:'rgba(0,0,0,0.7)', shadowBlur: 12 + }); + if (sub) MGAPI.text(ctx, sub, W/2, H/2 + 18, { size: 15, color:'rgba(255,255,255,0.88)' }); + } + + function render(alpha) { + // Shake + let sx = 0, sy = 0; + if (shake > 0.2) { sx = (Math.random()-0.5)*shake; sy = (Math.random()-0.5)*shake; } + ctx.save(); ctx.translate(sx, sy); + + // Himmel + ctx.fillStyle = skyGrad; ctx.fillRect(-12, -12, W + 24, H + 24); + // Sterne (twinkern via simTime) + stars.forEach(s => { + ctx.globalAlpha = s.a * (0.5 + 0.5*Math.sin((simTime||0)*0.004 + s.p)); + ctx.fillStyle = '#cdd6ff'; + ctx.fillRect(s.x, s.y, s.r, s.r); + }); + ctx.globalAlpha = 1; + + // Pipes + Boden + if (state !== 'intro') { + pipes.forEach(p => drawPipe(lerp(p.px, p.x, alpha), p)); + drawFloor(lerp(pWorldScroll || 0, worldScroll || 0, alpha)); + + // Puffs + puffs.forEach(p => { + ctx.globalAlpha = p.a; ctx.fillStyle = p.col || '#fff'; + ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); ctx.fill(); + }); + ctx.globalAlpha = 1; + + // Birds (mit Komet-Schweif pro Spieler) + const drawBird = (b, col, sprite, dimmed) => { + const headY = lerp(b.py, b.y, alpha); + // Schweif + for (let i = b.trail.length - 1; i >= 0; i--) { + const t = 1 - i / b.trail.length; + ctx.globalAlpha = 0.28 * t; + ctx.fillStyle = col; + ctx.beginPath(); ctx.arc(BIRD_X - i*7, b.trail[i], (BR * 0.55) * t + 1.5, 0, Math.PI*2); ctx.fill(); + } + ctx.globalAlpha = dimmed ? 0.45 : 1; + ctx.save(); ctx.translate(BIRD_X, headY); ctx.rotate(b.angle); + ctx.drawImage(sprite, -SPR/2, -SPR/2, SPR, SPR); + ctx.restore(); + ctx.globalAlpha = 1; + }; + if (birds) { + drawBird(birds[0], P1_LIGHT, sprP1, !birds[0].alive); + drawBird(birds[1], P2_LIGHT, sprP2, !birds[1].alive); + } + } else { + // Im Intro: Boden andeuten (statisch) + drawFloor(0); + } + + ctx.restore(); + + // HUD oben (Stil von 1P-Flappy: kompakt) + MGAPI.text(ctx, `🔵 ${scores[0]}`, 8, 14, { align:'left', size:13, color:P1_COL }); + const roundLabel = state === 'duelEnd' ? 'Best of ' + BEST_OF + : `Runde ${Math.min(roundIdx+1, BEST_OF)} / ${BEST_OF}`; + MGAPI.text(ctx, roundLabel, W/2, 14, { align:'center', size:12, color:'rgba(255,255,255,0.85)' }); + MGAPI.text(ctx, `${scores[1]} 🔴`, W-8, 14, { align:'right', size:13, color:P2_COL }); + + // Flash beim Crash + if (flash > 0) { ctx.fillStyle = `rgba(255,255,255,${flash})`; ctx.fillRect(0, 0, W, H); } + + // Overlays + if (state === 'intro') { + MGAPI.text(ctx, `Flappy-Duell · Best of ${BEST_OF}`, W/2, H/2 - 56, { + size: 26, weight:'bold', color:'#fff', family:"'Fredoka One',cursive", + shadow:'rgba(0,0,0,0.5)', shadowBlur: 10 + }); + MGAPI.text(ctx, '🔵 ↑ Pfeil oben · 🔴 W', + W/2, H/2 - 24, { size: 14, color:'rgba(255,255,255,0.88)' }); + drawStartButton(); + } else if (state === 'roundEnd') { + let t, c; + if (roundResult === 0) { t = '🔵 Runde für Spieler 1!'; c = P1_LIGHT; } + else if (roundResult === 1) { t = '🔴 Runde für Spieler 2!'; c = P2_LIGHT; } + else { t = '⚖️ Beide abgestürzt!'; c = '#fde68a'; } + drawOverlayText(t, `Stand: 🔵 ${scores[0]} : ${scores[1]} 🔴`, c); + } else if (state === 'duelEnd') { + const w = finalWinner(); + let t, c; + if (w === 0) { t = '🏆 Spieler 1 gewinnt!'; c = P1_LIGHT; } + else if (w === 1) { t = '🏆 Spieler 2 gewinnt!'; c = P2_LIGHT; } + else { t = '🤝 Unentschieden!'; c = '#fde68a'; } + drawOverlayText(t, `Endstand: 🔵 ${scores[0]} : ${scores[1]} 🔴`, c); + } + } + + function loop(ts) { + if (stopped) return; + raf = requestAnimationFrame(loop); + + if (state === 'playing') { + if (!lastTime) lastTime = ts; + acc += Math.min(ts - lastTime, STEP * 5); + lastTime = ts; + while (acc >= STEP) { step(); acc -= STEP; } + render(acc / STEP); + return; + } + + // Sonstige States: stateTimer mit dt zählen, render normal + if (!lastTime) lastTime = ts; + const dt = Math.min(50, ts - lastTime); + lastTime = ts; + stateTimer += dt; + if (state === 'roundEnd' && stateTimer >= 1800) advanceRound(); + else if (state === 'duelEnd' && stateTimer >= 2200) finishUp(); + render(0); + } + + raf = requestAnimationFrame(loop); + + return { + stop() { + if (stopped) return; + stopped = true; + cancelAnimationFrame(raf); + document.removeEventListener('keydown', onKey); + canvas.removeEventListener('mousedown', onCanvasDown); + canvas.removeEventListener('touchstart', onCanvasTouch); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, idx => MGAPI.onResult(idx)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, idx => MGAPI.onResult(idx)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, requires: REQUIRES, launch, preview }; + +})(); diff --git a/minigames/maze.js b/minigames/maze.js new file mode 100644 index 0000000..bb3705b --- /dev/null +++ b/minigames/maze.js @@ -0,0 +1,194 @@ +/** + * minigames/maze.js + * 🌀 Labyrinth — Finde den Ausgang in der Zeit! + */ +window.MG_maze = (function() { + + const ID = 'maze'; + const EMOJI = '🌀'; + const NAME = 'Labyrinth'; + const DESC = '2 Leben, 15 Sekunden pro Versuch. Pfeiltasten zum Steuern!'; + const CONTROLS = 'Pfeiltasten / WASD'; + const MULTI = 3; + + function generateMaze(COLS, ROWS) { + const cells = Array.from({ length: ROWS }, () => + Array.from({ length: COLS }, () => ({ n: true, s: true, e: true, w: true, visited: false })) + ); + const stack = []; + let cur = { c: 0, r: 0 }; + cells[0][0].visited = true; + stack.push(cur); + while (stack.length) { + const { c, r } = stack[stack.length - 1]; + const neighbors = []; + if (r > 0 && !cells[r-1][c].visited) neighbors.push({ c, r: r-1, dir: 'n' }); + if (r < ROWS-1 && !cells[r+1][c].visited) neighbors.push({ c, r: r+1, dir: 's' }); + if (c < COLS-1 && !cells[r][c+1].visited) neighbors.push({ c: c+1, r, dir: 'e' }); + if (c > 0 && !cells[r][c-1].visited) neighbors.push({ c: c-1, r, dir: 'w' }); + if (!neighbors.length) { stack.pop(); continue; } + const next = neighbors[Math.floor(Math.random() * neighbors.length)]; + cells[r][c][next.dir] = false; + cells[next.r][next.c][{ n:'s', s:'n', e:'w', w:'e' }[next.dir]] = false; + cells[next.r][next.c].visited = true; + stack.push({ c: next.c, r: next.r }); + } + return cells; + } + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + const theme = cfg.theme || { primary: '#6366f1' }; + const MAX_LIVES = 2; + const TIME_LIMIT = 15; + + const COLS = 9, ROWS = 7; + const cellW = Math.floor((W - 16) / COLS); + const cellH = Math.floor((H - 48) / ROWS); + const OX = (W - COLS * cellW) / 2; + const OY = 38; + const WALL = 2; + + let maze = generateMaze(COLS, ROWS); + let px = 0, py = 0; + let lives = MAX_LIVES; + let timeLeft = TIME_LIMIT; + let startTs = null; + let won = false; + let gameOver = false; + let stopped = false; + let raf, endTimer; + + function resetRound(ts) { + maze = generateMaze(COLS, ROWS); + px = 0; py = 0; + startTs = ts; + timeLeft = TIME_LIMIT; + } + + function tryMove(dc, dr) { + if (won || gameOver) return; + const cell = maze[py][px]; + const dir = dc === 1 ? 'e' : dc === -1 ? 'w' : dr === 1 ? 's' : 'n'; + if (cell[dir]) return; + px += dc; py += dr; + if (px === COLS - 1 && py === ROWS - 1 && !endTimer) { + won = true; + endTimer = setTimeout(() => onDone(true), 900); + } + } + + const held = {}; + const onKey = e => { + const map = { + ArrowUp:[0,-1], ArrowDown:[0,1], ArrowLeft:[-1,0], ArrowRight:[1,0], + w:[0,-1], s:[0,1], a:[-1,0], d:[1,0], + }; + const d = map[e.key]; + if (!d) return; + e.preventDefault(); + if (e.type === 'keydown') { if (!held[e.key]) tryMove(d[0], d[1]); held[e.key] = true; } + else held[e.key] = false; + }; + + let touchStart = null; + canvas.addEventListener('touchstart', e => { + touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY }; + }, { passive: true }); + canvas.addEventListener('touchend', e => { + if (!touchStart) return; + const dx = e.changedTouches[0].clientX - touchStart.x; + const dy = e.changedTouches[0].clientY - touchStart.y; + Math.abs(dx) > Math.abs(dy) ? tryMove(dx > 0 ? 1 : -1, 0) : tryMove(0, dy > 0 ? 1 : -1); + }, { passive: true }); + + document.addEventListener('keydown', onKey); + document.addEventListener('keyup', onKey); + + function drawMaze() { + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + const x = OX + c * cellW, y = OY + r * cellH; + const cell = maze[r][c]; + ctx.strokeStyle = `${theme.primary}88`; + ctx.lineWidth = WALL; + if (cell.n && r === 0) { ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x+cellW,y); ctx.stroke(); } + if (cell.w && c === 0) { ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x,y+cellH); ctx.stroke(); } + if (cell.s) { ctx.beginPath(); ctx.moveTo(x,y+cellH); ctx.lineTo(x+cellW,y+cellH); ctx.stroke(); } + if (cell.e) { ctx.beginPath(); ctx.moveTo(x+cellW,y); ctx.lineTo(x+cellW,y+cellH); ctx.stroke(); } + } + } + } + + function loop(ts) { + if (stopped) return; + raf = requestAnimationFrame(loop); + + if (startTs === null) startTs = ts; + + // Timer + if (!won && !gameOver) { + timeLeft = Math.max(0, TIME_LIMIT - (ts - startTs) / 1000); + if (timeLeft <= 0) { + lives--; + if (lives <= 0) { + gameOver = true; + if (!endTimer) endTimer = setTimeout(() => onDone(false), 1100); + } else { + resetRound(ts); + } + } + } + + ctx.fillStyle = '#050508'; + ctx.fillRect(0, 0, W, H); + + MGAPI.roundRect(ctx, OX, OY, cellW, cellH, 4, 'rgba(16,185,129,0.2)', null); + MGAPI.roundRect(ctx, OX+(COLS-1)*cellW, OY+(ROWS-1)*cellH, cellW, cellH, 4, 'rgba(245,166,35,0.25)', null); + + drawMaze(); + + ctx.font = '14px serif'; ctx.textAlign = 'center'; + ctx.fillText('🟢', OX + cellW/2, OY + cellH/2 + 5); + ctx.fillText('🏁', OX + (COLS-0.5)*cellW, OY + (ROWS-0.5)*cellH + 5); + + // Spieler + const plX = OX + px * cellW + cellW / 2; + const plY = OY + py * cellH + cellH / 2; + ctx.shadowColor = theme.primary; ctx.shadowBlur = 14; + ctx.fillStyle = theme.primary; + ctx.beginPath(); + ctx.arc(plX, plY, Math.min(cellW, cellH) * 0.32, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + + // HUD: Timer + Herzen + const tSec = Math.ceil(timeLeft); + const tColor = timeLeft < 5 ? '#ef4444' : timeLeft < 8 ? '#f59e0b' : theme.primary; + MGAPI.text(ctx, `⏱ ${tSec}s`, W / 2, 16, { size: 13, color: tColor }); + const hearts = '❤️'.repeat(lives) + '🖤'.repeat(Math.max(0, MAX_LIVES - lives)); + MGAPI.text(ctx, hearts, W - 10, 16, { align: 'right', size: 12 }); + MGAPI.text(ctx, '← ↑ → ↓', 10, 16, { align: 'left', size: 10, color: 'rgba(255,255,255,0.3)' }); + + if (won) MGAPI.resultScreen(ctx, W, H, true, 'Ausgang gefunden! ⏱ ' + (TIME_LIMIT - timeLeft).toFixed(1) + 's'); + if (gameOver) MGAPI.resultScreen(ctx, W, H, false, 'Zeit abgelaufen — nächstes Mal!'); + } + + raf = requestAnimationFrame(loop); + return { + stop() { + stopped = true; + cancelAnimationFrame(raf); + clearTimeout(endTimer); + document.removeEventListener('keydown', onKey); + document.removeEventListener('keyup', onKey); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/memory.js b/minigames/memory.js new file mode 100644 index 0000000..09a5a01 --- /dev/null +++ b/minigames/memory.js @@ -0,0 +1,142 @@ +/** + * minigames/memory.js + * 🃏 Memory — Finde alle Paare, so schnell wie möglich! + */ +window.MG_memory = (function() { + + const ID = 'memory'; + const EMOJI = '🃏'; + const NAME = 'Memory'; + const DESC = 'Finde alle Paare — so schnell wie möglich!'; + const CONTROLS = 'Mausklick / Tippen'; + const MULTI = 1; + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + const theme = cfg.theme || { primary: '#8b5cf6' }; + const EMOJIS = cfg.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 stopped = false, raf, startTime = null, elapsed = 0; + + // Layout: 4×3 + const COLS = 4, ROWS = Math.ceil(all.length / 4); + const PAD = 8; + const cw = Math.floor((W - PAD * (COLS + 1)) / COLS); + const ch = Math.floor((H - PAD * (ROWS + 2) - 24) / ROWS); + const ox = (W - COLS * cw - PAD * (COLS - 1)) / 2; + const oy = 30; + + function cardAt(mx, my) { + for (let ri = 0; ri < ROWS; ri++) { + for (let ci = 0; ci < COLS; ci++) { + const idx = ri * COLS + ci; + if (idx >= cards.length) continue; + const x = ox + ci * (cw + PAD); + const y = oy + ri * (ch + PAD); + if (mx >= x && mx < x + cw && my >= y && my < y + ch) return cards[idx]; + } + } + return null; + } + + function onClick(e) { + if (checking) return; + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width / (window.devicePixelRatio || 1); + const scaleY = canvas.height / rect.height / (window.devicePixelRatio || 1); + const mx = (e.clientX - rect.left) * scaleX; + const my = (e.clientY - rect.top) * scaleY; + const card = cardAt(mx, my); + if (!card || card.open || matched.includes(card.i)) return; + if (!startTime) startTime = performance.now(); + + 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); + if (matched.length === cards.length) { + setTimeout(() => onDone(true), 600); + } + } else { + flipped.forEach(c => c.open = false); + } + flipped = []; + checking = false; + }, 700); + } + } + + canvas.addEventListener('click', onClick); + + // Karten-Flip-Animation (progress 0–1) + const flipAnim = new Map(); // card.i → { prog, dir } + + function loop(ts) { + if (stopped) return; + raf = requestAnimationFrame(loop); + if (startTime) elapsed = (performance.now() - startTime) / 1000; + + ctx.fillStyle = '#050508'; + ctx.fillRect(0, 0, W, H); + + // HUD + MGAPI.text(ctx, `🃏 ${matched.length / 2} / ${EMOJIS.length} | Züge: ${moves}`, W / 2, 16, + { size: 12, color: theme.primary }); + + // Karten zeichnen + for (let ri = 0; ri < ROWS; ri++) { + for (let ci = 0; ci < COLS; ci++) { + const idx = ri * COLS + ci; + if (idx >= cards.length) continue; + const card = cards[idx]; + const x = ox + ci * (cw + PAD); + const y = oy + ri * (ch + PAD); + const isM = matched.includes(card.i); + const isOpen = card.open || isM; + + if (isM) { + MGAPI.roundRect(ctx, x, y, cw, ch, 8, + 'rgba(16,185,129,0.15)', '#10b981'); + } else if (isOpen) { + MGAPI.roundRect(ctx, x, y, cw, ch, 8, + `${theme.primary}22`, theme.primary); + } else { + MGAPI.roundRect(ctx, x, y, cw, ch, 8, + 'rgba(255,255,255,0.04)', 'rgba(255,255,255,0.1)'); + } + + if (isOpen) { + ctx.font = `${Math.min(cw, ch) * 0.55}px serif`; + ctx.textAlign = 'center'; + ctx.fillText(card.e, x + cw / 2, y + ch / 2 + ch * 0.15); + } else { + MGAPI.text(ctx, '?', x + cw / 2, y + ch / 2, + { size: Math.floor(ch * 0.35), color: 'rgba(255,255,255,0.2)' }); + } + } + } + } + + raf = requestAnimationFrame(loop); + return { + stop() { + stopped = true; + cancelAnimationFrame(raf); + canvas.removeEventListener('click', onClick); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/puzzle.js b/minigames/puzzle.js new file mode 100644 index 0000000..a1f99f9 --- /dev/null +++ b/minigames/puzzle.js @@ -0,0 +1,137 @@ +/** + * minigames/puzzle.js + * 🧩 Zahlen-Puzzle — Schiebe die Kacheln in die richtige Reihenfolge! (15-Puzzle) + */ +window.MG_puzzle = (function() { + + const ID = 'puzzle'; + const EMOJI = '🧩'; + const NAME = 'Zahlen-Puzzle'; + const DESC = 'Schiebe die Kacheln in die richtige Reihenfolge (1–8)!'; + const CONTROLS = 'Mausklick / Pfeiltasten'; + const MULTI = 1; + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + const theme = cfg.theme || { primary: '#14b8a6' }; + + const SIZE = 3; // 3×3 + const PAD = 20; + const cellW = Math.floor((W - PAD * 2) / SIZE); + const cellH = Math.floor((H - PAD * 2 - 30) / SIZE); + const OX = (W - SIZE * cellW) / 2; + const OY = 30 + (H - 30 - SIZE * cellH) / 2; + + // Ziel: 1 2 3 / 4 5 6 / 7 8 _ + const GOAL = [1,2,3,4,5,6,7,8,0]; + let board = [...GOAL]; + let moves = 0; + let stopped = false; + let raf, endTimer, solved = false; + + // Mischeln (100 zufällige Züge) + function shuffle() { + for (let i = 0; i < 100; i++) { + const ei = board.indexOf(0); + const er = Math.floor(ei / SIZE), ec = ei % SIZE; + const nbr = []; + if (er > 0) nbr.push(ei - SIZE); + if (er < SIZE-1) nbr.push(ei + SIZE); + if (ec > 0) nbr.push(ei - 1); + if (ec < SIZE-1) nbr.push(ei + 1); + const ni = nbr[Math.floor(Math.random() * nbr.length)]; + [board[ei], board[ni]] = [board[ni], board[ei]]; + } + } + shuffle(); + + function slideAt(idx) { + if (solved) return; + const ei = board.indexOf(0); + const er = Math.floor(ei / SIZE), ec = ei % SIZE; + const tr = Math.floor(idx / SIZE), tc = idx % SIZE; + const adj = (er === tr && Math.abs(ec - tc) === 1) || + (ec === tc && Math.abs(er - tr) === 1); + if (!adj) return; + [board[ei], board[idx]] = [board[idx], board[ei]]; + moves++; + if (board.join() === GOAL.join()) { + solved = true; + endTimer = setTimeout(() => onDone(true), 900); + } + } + + canvas.addEventListener('click', e => { + const rect = canvas.getBoundingClientRect(); + const mx = (e.clientX - rect.left) * (canvas.width / rect.width / (window.devicePixelRatio || 1)); + const my = (e.clientY - rect.top) * (canvas.height / rect.height); + for (let i = 0; i < SIZE * SIZE; i++) { + const c = i % SIZE, r = Math.floor(i / SIZE); + const x = OX + c * cellW, y = OY + r * cellH; + if (mx >= x && mx < x + cellW - 4 && my >= y && my < y + cellH - 4) + slideAt(i); + } + }); + + const onKey = e => { + const ei = board.indexOf(0); + const map = { ArrowUp: SIZE, ArrowDown: -SIZE, ArrowLeft: 1, ArrowRight: -1 }; + const di = map[e.key]; + if (di && ei + di >= 0 && ei + di < SIZE * SIZE) { + e.preventDefault(); + slideAt(ei + di); + } + }; + document.addEventListener('keydown', onKey); + + function loop() { + if (stopped) return; + raf = requestAnimationFrame(loop); + + ctx.fillStyle = '#050508'; + ctx.fillRect(0, 0, W, H); + + MGAPI.text(ctx, `🧩 Züge: ${moves}`, W / 2, 18, { size: 12, color: theme.primary }); + + for (let i = 0; i < SIZE * SIZE; i++) { + const val = board[i]; + const c = i % SIZE, r = Math.floor(i / SIZE); + const x = OX + c * cellW, y = OY + r * cellH; + + if (val === 0) { + // Leerfeld + MGAPI.roundRect(ctx, x+2, y+2, cellW-8, cellH-8, 8, 'rgba(255,255,255,0.03)', null); + continue; + } + + const isCorrect = val === GOAL[i]; + MGAPI.roundRect(ctx, x+2, y+2, cellW-8, cellH-8, 10, + isCorrect ? `${theme.primary}33` : 'rgba(255,255,255,0.07)', + isCorrect ? theme.primary : 'rgba(255,255,255,0.15)'); + + MGAPI.text(ctx, String(val), x + cellW/2 - 2, y + cellH/2 + 2, + { size: Math.floor(cellH * 0.38), family: "'Fredoka One',cursive", + color: isCorrect ? theme.primary : '#fff' }); + } + + if (solved) + MGAPI.resultScreen(ctx, W, H, true, `In ${moves} Zügen gelöst!`); + } + + raf = requestAnimationFrame(loop); + return { + stop() { + stopped = true; + clearTimeout(endTimer); + cancelAnimationFrame(raf); + document.removeEventListener('keydown', onKey); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/quiz.js b/minigames/quiz.js new file mode 100644 index 0000000..02eddf9 --- /dev/null +++ b/minigames/quiz.js @@ -0,0 +1,116 @@ +/** + * minigames/quiz.js + * ❓ Quiz — Beantworte die Frage richtig! + */ +window.MG_quiz = (function() { + + const ID = 'quiz'; + const EMOJI = '❓'; + const NAME = 'Quiz'; + const DESC = 'Beantworte die Frage richtig!'; + const CONTROLS = 'Mausklick / Tippen'; + const MULTI = 99; // unbegrenzt, je Quiz-Feld eine Frage + + function run(wrap, W, H, cfg, onDone) { + const theme = cfg.theme || { primary: '#f59e0b' }; + const quizData = cfg.quizData || []; + const fieldIdx = cfg.fieldIndex ?? null; + + // Passende Frage suchen + let q = null; + if (fieldIdx !== null) q = quizData.find(d => d.fieldIndex === fieldIdx); + if (!q && quizData.length > 0) q = quizData[Math.floor(Math.random() * quizData.length)]; + + // Fallback wenn keine Frage hinterlegt + if (!q || !q.question) { + wrap.innerHTML = ` +

+
+
Keine Quiz-Frage für dieses Feld hinterlegt.
+ +
`; + return { stop() {} }; + } + + // HTML-Quiz (kein Canvas — besser lesbar) + const answers = q.answers || []; + const correct = q.correct ?? 0; + const COLORS = ['#7c3aed','#2563eb','#059669','#d97706']; + const LABELS = ['A','B','C','D']; + + const container = document.createElement('div'); + container.style.cssText = ` + display:flex;flex-direction:column;gap:10px;padding:16px; + font-family:'Nunito',sans-serif;width:100%;box-sizing:border-box; + `; + + // Frage + const qEl = document.createElement('div'); + qEl.style.cssText = ` + background:rgba(255,255,255,0.05);border-radius:12px;padding:16px; + color:#fff;font-size:15px;font-weight:700;line-height:1.5;text-align:center; + `; + qEl.textContent = q.question; + container.appendChild(qEl); + + // Antworten + let answered = false; + answers.forEach((a, i) => { + if (!a) return; + const btn = document.createElement('button'); + btn.style.cssText = ` + background:${COLORS[i] || '#333'}22;border:2px solid ${COLORS[i] || '#333'}88; + color:#fff;border-radius:10px;padding:12px 16px;cursor:pointer; + font-family:'Nunito',sans-serif;font-size:13px;font-weight:700; + text-align:left;transition:all 0.2s;display:flex;gap:10px;align-items:center; + `; + // XSS-sicher: Label per DOM, Antwort als reiner Textknoten + const lbl = document.createElement('span'); + lbl.style.cssText = `background:${COLORS[i]};border-radius:6px;padding:2px 8px;font-size:12px`; + lbl.textContent = LABELS[i]; + btn.appendChild(lbl); + btn.appendChild(document.createTextNode(' ' + a)); + btn.addEventListener('click', () => { + if (answered) return; + answered = true; + const won = (i === correct); + // Feedback-Farben + answers.forEach((_, j) => { + const b = container.querySelectorAll('button')[j]; + if (!b) return; + if (j === correct) { + b.style.background = 'rgba(16,185,129,0.3)'; + b.style.borderColor = '#10b981'; + } else if (j === i && !won) { + b.style.background = 'rgba(239,68,68,0.3)'; + b.style.borderColor = '#ef4444'; + } + b.style.cursor = 'default'; + }); + // Ergebnis-Text + const result = document.createElement('div'); + result.style.cssText = ` + text-align:center;font-family:'Fredoka One',cursive;font-size:1.2rem; + padding:10px;color:${won ? '#10b981' : '#ef4444'}; + `; + result.textContent = won ? '🎉 Richtig!' : `❌ Falsch! Richtig wäre: ${answers[correct]}`; + container.appendChild(result); + setTimeout(() => onDone(won), 1400); + }); + container.appendChild(btn); + }); + + wrap.appendChild(container); + return { stop() { wrap.innerHTML = ''; } }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/reaction.js b/minigames/reaction.js new file mode 100644 index 0000000..c521119 --- /dev/null +++ b/minigames/reaction.js @@ -0,0 +1,144 @@ +/** + * minigames/reaction.js + * ⚡ Reaktionstest — Drück den Knopf so schnell wie möglich! + */ +window.MG_reaction = (function() { + + const ID = 'reaction'; + const EMOJI = '⚡'; + const NAME = 'Reaktionstest'; + const DESC = 'Wenn der Kreis GRÜN wird — so schnell wie möglich klicken!'; + const CONTROLS = 'Klick / Leertaste'; + const MULTI = 3; + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + const theme = cfg.theme || { primary: '#ef4444' }; + const ROUNDS = 3; + const WIN_MS = 600; + + let phase = 'wait'; + let waitEnd = 0; + let reStart = 0; + let times = []; + let best = Infinity; + let stopped = false; + let raf, endTimer; + + function nextWait() { + phase = 'wait'; + waitEnd = performance.now() + (2 + Math.random() * 3) * 1000; + } + nextWait(); + + const react = () => { + if (phase === 'ready') { + const t = performance.now() - reStart; + times.push(t); + best = Math.min(best, t); + if (times.length >= ROUNDS) { + clearTimeout(endTimer); + endTimer = setTimeout(() => onDone(best < WIN_MS), 800); + phase = 'done'; + } else { + nextWait(); + } + } else if (phase === 'wait') { + phase = 'toosoon'; + setTimeout(nextWait, 1000); + } + }; + + const onKey = e => { if (e.code === 'Space') { e.preventDefault(); 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'; + reStart = performance.now(); + } + + // Hintergrund + const bg = phase === 'ready' ? '#14532d' + : phase === 'toosoon' ? '#7f1d1d' + : phase === 'done' ? '#0f0e17' + : '#050508'; + ctx.fillStyle = bg; + ctx.fillRect(0, 0, W, H); + + const cx = W / 2, cy = H / 2; + + if (phase === 'wait') { + // Pulsierender Kreis (rot = warten) + const pulse = 0.85 + 0.15 * Math.sin(ts * 0.003); + ctx.beginPath(); + ctx.arc(cx, cy, 60 * pulse, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(239,68,68,0.2)'; + ctx.fill(); + ctx.strokeStyle = '#ef444488'; + ctx.lineWidth = 3; + ctx.stroke(); + MGAPI.text(ctx, '⏳', cx, cy - 6, { size: 36 }); + MGAPI.text(ctx, 'Warte...', cx, cy + 44, { size: 14, color: '#a7a3c2' }); + MGAPI.text(ctx, `${times.length} / ${ROUNDS} Runden`, cx, H - 20, { size: 12, color: '#64748b' }); + } + + if (phase === 'ready') { + // Leuchtender grüner Kreis + ctx.shadowColor = '#22c55e'; + ctx.shadowBlur = 40; + ctx.beginPath(); + ctx.arc(cx, cy, 65, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(22,163,74,0.4)'; + ctx.fill(); + ctx.strokeStyle = '#22c55e'; + ctx.lineWidth = 4; + ctx.stroke(); + ctx.shadowBlur = 0; + MGAPI.text(ctx, '⚡', cx, cy - 8, { size: 44 }); + MGAPI.text(ctx, 'JETZT!', cx, cy + 48, + { size: 22, family: "'Fredoka One',cursive", color: '#fff', shadow: '#22c55e', shadowBlur: 15 }); + MGAPI.text(ctx, 'Klick oder Leertaste!', cx, cy + 78, { size: 13, color: 'rgba(255,255,255,0.6)' }); + } + + if (phase === 'toosoon') { + MGAPI.text(ctx, '😅', cx, cy - 10, { size: 48 }); + MGAPI.text(ctx, 'Zu früh!', cx, cy + 46, + { size: 22, family: "'Fredoka One',cursive", color: '#fca5a5' }); + } + + if (phase === 'done') { + MGAPI.resultScreen(ctx, W, H, best < WIN_MS, + best < WIN_MS ? `${Math.round(best)} ms — super schnell!` : `${Math.round(best)} ms — noch etwas langsam`); + } + + // Statistik + if (times.length > 0 && phase !== 'done') { + const last = times[times.length - 1]; + MGAPI.text(ctx, `Letzte: ${Math.round(last)} ms | Beste: ${Math.round(best)} ms`, + cx, H - 14, { size: 11, color: 'rgba(255,255,255,0.4)' }); + } + } + + raf = requestAnimationFrame(loop); + return { + stop() { + stopped = true; + cancelAnimationFrame(raf); + clearTimeout(endTimer); + document.removeEventListener('keydown', onKey); + canvas.removeEventListener('click', react); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/simon.js b/minigames/simon.js new file mode 100644 index 0000000..b0227d1 --- /dev/null +++ b/minigames/simon.js @@ -0,0 +1,172 @@ +/** + * minigames/simon.js + * 🔴 Simon Says — Merke und wiederhole die Farb-Sequenz! + */ +window.MG_simon = (function() { + + const ID = 'simon'; + const EMOJI = '🔴'; + const NAME = 'Simon Says'; + const DESC = 'Merke die Farb-Reihenfolge und tippe sie nach!'; + const CONTROLS = 'Mausklick / Tippen'; + const MULTI = 3; + + const COLORS = [ + { id: 0, fill: '#22c55e', glow: 'rgba(34,197,94,0.6)', label: '🟢' }, + { id: 1, fill: '#ef4444', glow: 'rgba(239,68,68,0.6)', label: '🔴' }, + { id: 2, fill: '#3b82f6', glow: 'rgba(59,130,246,0.6)', label: '🔵' }, + { id: 3, fill: '#f59e0b', glow: 'rgba(245,158,11,0.6)', label: '🟡' }, + ]; + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + const WIN_ROUNDS = cfg.winRounds || 4; + + // Layout: 2×2 Grid + const PAD = 16; + const GW = (W - PAD * 3) / 2; + const GH = (H - PAD * 3 - 32) / 2; + const POS = [ + { x: PAD, y: 32 + PAD }, + { x: PAD*2+GW, y: 32 + PAD }, + { x: PAD, y: 32 + PAD*2 + GH }, + { x: PAD*2+GW, y: 32 + PAD*2 + GH }, + ]; + + let sequence = []; + let userSeq = []; + let pressedIdx = -1; + let showIdx = -1; // welcher Schritt der Anzeige läuft + let phase = 'show'; // 'show' | 'input' | 'result' + let litIdx = -1; + let stopped = false; + let raf, stepTimer, endTimer, result = null; + + function addStep() { sequence.push(Math.floor(Math.random() * 4)); } + function startShow() { + phase = 'show'; + litIdx = -1; + showIdx = 0; + userSeq = []; + stepTimer = setInterval(() => { + litIdx = (litIdx === -1) ? sequence[showIdx] : -1; + if (litIdx === -1) { + showIdx++; + if (showIdx >= sequence.length) { + clearInterval(stepTimer); + litIdx = -1; + phase = 'input'; + } + } + }, 500); + } + + function checkInput(colorId) { + if (phase !== 'input') return; + pressedIdx = colorId; + setTimeout(() => { pressedIdx = -1; }, 200); + userSeq.push(colorId); + const idx = userSeq.length - 1; + + if (userSeq[idx] !== sequence[idx]) { + // Falsch + clearTimeout(endTimer); + result = 'lose'; + endTimer = setTimeout(() => onDone(false), 1000); + phase = 'result'; + return; + } + + if (userSeq.length === sequence.length) { + if (sequence.length >= WIN_ROUNDS) { + clearTimeout(endTimer); + result = 'win'; + endTimer = setTimeout(() => onDone(true), 900); + phase = 'result'; + } else { + // Nächste Runde + phase = 'wait'; + setTimeout(() => { + addStep(); + startShow(); + }, 800); + } + } + } + + // Klick auf Farb-Button + canvas.addEventListener('click', e => { + if (phase !== 'input') return; + const rect = canvas.getBoundingClientRect(); + const mx = (e.clientX - rect.left) * (canvas.width / rect.width / (window.devicePixelRatio || 1)); + const my = (e.clientY - rect.top) * (canvas.height / rect.height / (window.devicePixelRatio || 1)); + POS.forEach((p, i) => { + if (mx >= p.x && mx < p.x + GW && my >= p.y && my < p.y + GH) + checkInput(i); + }); + }); + + // Starten + addStep(); + startShow(); + + function loop() { + if (stopped) return; + raf = requestAnimationFrame(loop); + + ctx.fillStyle = '#050508'; + ctx.fillRect(0, 0, W, H); + + // Felder zeichnen + POS.forEach((p, i) => { + const col = COLORS[i]; + const isLit = litIdx === i; + const inUser = pressedIdx === i; + + ctx.shadowColor = isLit || inUser ? col.glow : 'transparent'; + ctx.shadowBlur = isLit || inUser ? 30 : 0; + + MGAPI.roundRect(ctx, p.x, p.y, GW, GH, 12, + isLit || inUser ? col.fill : `${col.fill}44`, + isLit || inUser ? col.fill : `${col.fill}88`); + + ctx.shadowBlur = 0; + + // Emoji mittig + ctx.font = `${Math.min(GW, GH) * 0.4}px serif`; + ctx.textAlign = 'center'; + ctx.fillText(col.label, p.x + GW / 2, p.y + GH / 2 + GH * 0.14); + }); + + // HUD + const phaseText = phase === 'show' ? '👀 Schau zu...' + : phase === 'input' ? '👆 Deine Runde!' + : phase === 'wait' ? '✅ Richtig!' + : ''; + MGAPI.text(ctx, phaseText, W / 2, 18, + { size: 13, color: phase === 'input' ? '#f59e0b' : '#a7a3c2' }); + MGAPI.text(ctx, `Runde ${sequence.length} / ${WIN_ROUNDS}`, W / 2, H - 10, + { size: 11, color: 'rgba(255,255,255,0.35)' }); + + if (result) + MGAPI.resultScreen(ctx, W, H, result === 'win', + result === 'win' ? `${sequence.length} Runden gemeistert!` : 'Falsche Farbe!'); + } + + raf = requestAnimationFrame(loop); + return { + stop() { + stopped = true; + clearInterval(stepTimer); + clearTimeout(endTimer); + cancelAnimationFrame(raf); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/snake.js b/minigames/snake.js new file mode 100644 index 0000000..c722100 --- /dev/null +++ b/minigames/snake.js @@ -0,0 +1,204 @@ +/** + * minigames/snake.js + * 🐍 Snake — Steuere die Schlange, friss 10 Äpfel ohne gegen die Wand zu fahren + */ +window.MG_snake = (function() { + + const ID = 'snake'; + const EMOJI = '🐍'; + const NAME = 'Snake'; + const DESC = '3 Leben, 15 Äpfel sammeln. Pfeiltasten oder WASD.'; + const CONTROLS = 'Pfeiltasten / WASD'; + const MULTI = 1; + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + const SZ = 20; + const COLS = Math.floor(W / SZ); + const ROWS = Math.floor(H / SZ); + const WIN_SCORE = cfg.winScore || 15; + const MAX_LIVES = cfg.lives || 3; + const theme = cfg.theme || { primary: '#10b981', glow: 'rgba(16,185,129,0.4)' }; + + // Startgeschwindigkeit 160ms, pro Apfel -12ms, Minimum 55ms + const BASE_INTERVAL = 160; + const SPEED_INC = 12; + const MIN_INTERVAL = 55; + + function interval(sc) { + return Math.max(MIN_INTERVAL, BASE_INTERVAL - sc * SPEED_INC); + } + + function rndFood(snk) { + let f; + do { f = { x: Math.floor(Math.random() * COLS), y: Math.floor(Math.random() * ROWS) }; } + while (snk.some(s => s.x === f.x && s.y === f.y)); + return f; + } + + function startSnake() { + return [{ x: 5, y: 5 }, { x: 4, y: 5 }, { x: 3, y: 5 }]; + } + + let snake = startSnake(); + let dir = { x: 1, y: 0 }; + let nextDir = { x: 1, y: 0 }; + let food = rndFood(snake); + let score = 0; + let lives = MAX_LIVES; + let dead = false; + let dying = false; + let stopped = false; + let raf, endTimer, respawnTimer, last = 0; + + function onKey(e) { + const map = { + ArrowUp: { x: 0, y: -1 }, w: { x: 0, y: -1 }, + ArrowDown: { x: 0, y: 1 }, s: { x: 0, y: 1 }, + ArrowLeft: { x: -1, y: 0 }, a: { x: -1, y: 0 }, + ArrowRight: { x: 1, y: 0 }, d: { x: 1, y: 0 }, + }; + const d = map[e.key]; + if (d && (d.x !== -dir.x || d.y !== -dir.y)) { + e.preventDefault(); + nextDir = d; + } + } + document.addEventListener('keydown', onKey); + + let touchStart = null; + canvas.addEventListener('touchstart', e => { + touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY }; + }, { passive: true }); + canvas.addEventListener('touchend', e => { + if (!touchStart) return; + const dx = e.changedTouches[0].clientX - touchStart.x; + const dy = e.changedTouches[0].clientY - touchStart.y; + if (Math.abs(dx) > Math.abs(dy)) + nextDir = dx > 0 ? { x: 1, y: 0 } : { x: -1, y: 0 }; + else + nextDir = dy > 0 ? { x: 0, y: 1 } : { x: 0, y: -1 }; + }, { passive: true }); + + function die() { + if (dying) return; + dying = true; + dead = true; + lives--; + if (lives <= 0) { + if (!endTimer) endTimer = setTimeout(() => onDone(false), 1300); + } else { + respawnTimer = setTimeout(() => { + snake = startSnake(); + dir = { x: 1, y: 0 }; + nextDir = { x: 1, y: 0 }; + food = rndFood(snake); + dead = false; + dying = false; + last = 0; + }, 1000); + } + } + + function drawGrid() { + ctx.strokeStyle = 'rgba(255,255,255,0.04)'; + ctx.lineWidth = 0.5; + for (let x = 0; x < COLS; x++) + for (let y = 0; y < ROWS; y++) + ctx.strokeRect(x * SZ, y * SZ, SZ, SZ); + } + + function loop(ts) { + if (stopped) return; + raf = requestAnimationFrame(loop); + + // Zeichnen immer, Spiellogik nur im Takt + const tick = ts - last >= interval(score); + + if (tick && !dead) { + last = ts; + dir = nextDir; + const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y }; + if ( + head.x < 0 || head.x >= COLS || + head.y < 0 || head.y >= ROWS || + snake.some(s => s.x === head.x && s.y === head.y) + ) { + die(); + } else { + snake.unshift(head); + if (head.x === food.x && head.y === food.y) { + score++; + food = rndFood(snake); + if (score >= WIN_SCORE && !endTimer) + endTimer = setTimeout(() => onDone(true), 800); + // Schlange wächst automatisch (kein pop) + } else { + snake.pop(); + } + } + } + + // --- Zeichnen --- + ctx.fillStyle = '#050508'; + ctx.fillRect(0, 0, W, H); + drawGrid(); + + // Futter + ctx.shadowColor = theme.primary; + ctx.shadowBlur = 12; + ctx.font = `${SZ - 2}px serif`; + ctx.textAlign = 'center'; + ctx.fillText('🍎', food.x * SZ + SZ / 2, food.y * SZ + SZ / 1.2); + ctx.shadowBlur = 0; + + // Schlange + snake.forEach((s, i) => { + const alpha = i === 0 ? 'ff' : Math.max(0x44, 0x88 - i * 2).toString(16).padStart(2, '0'); + ctx.fillStyle = i === 0 ? theme.primary : `${theme.primary}${alpha}`; + if (i === 0) { ctx.shadowColor = theme.primary; ctx.shadowBlur = 8; } + MGAPI.roundRect(ctx, s.x * SZ + 2, s.y * SZ + 2, SZ - 4, SZ - 4, 4, ctx.fillStyle, null); + ctx.shadowBlur = 0; + }); + + // HUD + MGAPI.text(ctx, `🍎 ${score} / ${WIN_SCORE}`, 8, 14, + { align: 'left', size: 13, color: theme.primary }); + const hearts = '❤️'.repeat(lives) + '🖤'.repeat(Math.max(0, MAX_LIVES - lives)); + MGAPI.text(ctx, hearts, W / 2, 14, { align: 'center', size: 12 }); + // Geschwindigkeitsanzeige + const spd = Math.round((BASE_INTERVAL / Math.max(MIN_INTERVAL, interval(score))) * 10) / 10; + MGAPI.text(ctx, `⚡ ${spd.toFixed(1)}x`, W - 8, 14, + { align: 'right', size: 11, color: 'rgba(255,255,255,0.4)' }); + + if (dead && lives <= 0) { + MGAPI.resultScreen(ctx, W, H, false, + `Nur ${score} Äpfel — nächstes Mal!`); + } else if (dead && dying) { + MGAPI.text(ctx, `${lives} ${lives === 1 ? 'Leben' : 'Leben'} übrig`, W / 2, H / 2, + { size: 16, color: '#ef4444' }); + } + + if (score >= WIN_SCORE && !dead) + MGAPI.resultScreen(ctx, W, H, true, `${score} Äpfel gefressen — geschafft!`); + } + + raf = requestAnimationFrame(loop); + + return { + stop() { + stopped = true; + cancelAnimationFrame(raf); + clearTimeout(endTimer); + clearTimeout(respawnTimer); + document.removeEventListener('keydown', onKey); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, { ...cfg, winScore: 15, lives: 3 }, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/snake2p.js b/minigames/snake2p.js new file mode 100644 index 0000000..14c5123 --- /dev/null +++ b/minigames/snake2p.js @@ -0,0 +1,353 @@ +/** + * minigames/snake2p.js + * 🐍🐍 Snake-Duell — 2-Spieler Best-of-3 im Stil von snake.js + * Blau (Pfeiltasten) startet rechts · Rot (WASD) startet links. + * Ruft MGAPI.onResult(winnerIdx) mit 0 (P1), 1 (P2) oder -1 (Unentschieden). + */ +window.MG_snake2p = (function () { + + const ID = 'snake2p'; + const EMOJI = '🐍🐍'; + const NAME = 'Snake-Duell'; + const DESC = 'Best-of-3 · 🔵 Pfeile (rechts) gegen 🔴 WASD (links). 2 Äpfel auf dem Feld.'; + const CONTROLS = '← ↑ → ↓ / W A S D'; + const MULTI = 1; + const REQUIRES = '2p'; + + const TOTAL_ROUNDS = 3; + const STEP_INTERVAL = 160; // wie snake.js Start-Tempo, konstant (fair fürs Duell) + + const P1_COL = '#3b82f6'; // Pfeiltasten, startet rechts + const P2_COL = '#ef4444'; // WASD, startet links + const APPLE_COL = '#f59e0b'; + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + const SZ = 20; + const COLS = Math.floor(W / SZ); + const ROWS = Math.floor(H / SZ); + + const scores = [0, 0]; // [P1 (Pfeile), P2 (WASD)] + let roundIdx = 0; + let state = 'intro'; // intro | playing | roundEnd | duelEnd + let stateTimer = 0; + let lastTs = 0; + let lastStepTs = 0; + let stopped = false, raf = null; + let roundResult = null; + let startBtnRect = null; + + let s1 = null, s2 = null; + let apples = []; + + // ── Snake-Init ── + function startSnakeRight() { + // P1 (Pfeiltasten), Blau, startet rechts und zieht nach links + const my = Math.floor(ROWS / 2); + const x0 = Math.max(5, COLS - 6); + return { body: [{x:x0, y:my}, {x:x0+1, y:my}, {x:x0+2, y:my}], dir:{x:-1,y:0}, nextDir:{x:-1,y:0}, alive:true }; + } + function startSnakeLeft() { + // P2 (WASD), Rot, startet links und zieht nach rechts + const my = Math.floor(ROWS / 2); + const x0 = Math.min(COLS - 6, 5); + return { body: [{x:x0, y:my}, {x:x0-1, y:my}, {x:x0-2, y:my}], dir:{x:1,y:0}, nextDir:{x:1,y:0}, alive:true }; + } + + function rndApple() { + let safety = 200; + while (safety-- > 0) { + const f = { x: Math.floor(Math.random()*COLS), y: Math.floor(Math.random()*ROWS) }; + if (s1.body.some(c=>c.x===f.x&&c.y===f.y)) continue; + if (s2.body.some(c=>c.x===f.x&&c.y===f.y)) continue; + if (apples.some(a=>a.x===f.x&&a.y===f.y)) continue; + return f; + } + return null; + } + function spawnApples() { + while (apples.length < 2) { + const a = rndApple(); + if (!a) break; + apples.push(a); + } + } + + function newRound() { + s1 = startSnakeRight(); + s2 = startSnakeLeft(); + apples = []; + spawnApples(); + lastStepTs = 0; + roundResult = null; + state = 'playing'; + stateTimer = 0; + } + + function endRound(winner) { + if (winner === 0 || winner === 1) scores[winner]++; + roundResult = winner; + state = 'roundEnd'; + stateTimer = 0; + } + + function advanceRound() { + roundIdx++; + if (scores[0] >= 2 || scores[1] >= 2 || roundIdx >= TOTAL_ROUNDS) { + state = 'duelEnd'; + stateTimer = 0; + } else { + newRound(); + } + } + + function finalWinner() { + if (scores[0] > scores[1]) return 0; + if (scores[1] > scores[0]) return 1; + return -1; + } + + function finishUp() { + if (stopped) return; + stopped = true; + cancelAnimationFrame(raf); + document.removeEventListener('keydown', onKey); + canvas.removeEventListener('click', onClick); + canvas.removeEventListener('touchend', onTouch); + onDone(finalWinner()); + } + + // ── Steuerung ── + function setDir(s, dx, dy) { + if (s.dir.x === -dx && s.dir.y === -dy) return; // kein 180° + s.nextDir = { x: dx, y: dy }; + } + function onKey(e) { + if (state !== 'playing') return; + const k = e.key; + let handled = true; + if (k === 'ArrowUp') setDir(s1, 0, -1); + else if (k === 'ArrowDown') setDir(s1, 0, 1); + else if (k === 'ArrowLeft') setDir(s1, -1, 0); + else if (k === 'ArrowRight') setDir(s1, 1, 0); + else if (k === 'w' || k === 'W') setDir(s2, 0, -1); + else if (k === 's' || k === 'S') setDir(s2, 0, 1); + else if (k === 'a' || k === 'A') setDir(s2, -1, 0); + else if (k === 'd' || k === 'D') setDir(s2, 1, 0); + else handled = false; + if (handled) e.preventDefault(); + } + function pointInBtn(x, y) { + return startBtnRect && x >= startBtnRect.x && x <= startBtnRect.x + startBtnRect.w + && y >= startBtnRect.y && y <= startBtnRect.y + startBtnRect.h; + } + function canvasCoords(clientX, clientY) { + const r = canvas.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + const x = (clientX - r.left) * (canvas.width / r.width / dpr); + const y = (clientY - r.top) * (canvas.height / r.height / dpr); + return { x, y }; + } + function onClick(e) { + if (state !== 'intro') return; + const p = canvasCoords(e.clientX, e.clientY); + if (pointInBtn(p.x, p.y)) newRound(); + } + function onTouch(e) { + if (state !== 'intro') return; + const t = e.changedTouches && e.changedTouches[0]; + if (!t) return; + const p = canvasCoords(t.clientX, t.clientY); + if (pointInBtn(p.x, p.y)) { e.preventDefault(); newRound(); } + } + document.addEventListener('keydown', onKey); + canvas.addEventListener('click', onClick); + canvas.addEventListener('touchend', onTouch, { passive: false }); + + // ── Spielschritt ── + function stepGame() { + s1.dir = s1.nextDir; + s2.dir = s2.nextDir; + + const h1 = { x: s1.body[0].x + s1.dir.x, y: s1.body[0].y + s1.dir.y }; + const h2 = { x: s2.body[0].x + s2.dir.x, y: s2.body[0].y + s2.dir.y }; + + const out1 = h1.x<0||h1.x>=COLS||h1.y<0||h1.y>=ROWS; + const out2 = h2.x<0||h2.x>=COLS||h2.y<0||h2.y>=ROWS; + const self1 = s1.body.some(c=>c.x===h1.x&&c.y===h1.y); + const self2 = s2.body.some(c=>c.x===h2.x&&c.y===h2.y); + const cross1 = s2.body.some(c=>c.x===h1.x&&c.y===h1.y); + const cross2 = s1.body.some(c=>c.x===h2.x&&c.y===h2.y); + const headOn = h1.x===h2.x && h1.y===h2.y; + + let die1 = out1||self1||cross1; + let die2 = out2||self2||cross2; + if (headOn) { die1 = true; die2 = true; } + + if (die1) s1.alive = false; + if (die2) s2.alive = false; + + if (s1.alive) { + s1.body.unshift(h1); + const ai = apples.findIndex(a=>a.x===h1.x&&a.y===h1.y); + if (ai >= 0) apples.splice(ai,1); else s1.body.pop(); + } + if (s2.alive) { + s2.body.unshift(h2); + const ai = apples.findIndex(a=>a.x===h2.x&&a.y===h2.y); + if (ai >= 0) apples.splice(ai,1); else s2.body.pop(); + } + spawnApples(); + + if (!s1.alive || !s2.alive) { + let w; + if (!s1.alive && !s2.alive) w = -1; + else if (!s1.alive) w = 1; + else w = 0; + endRound(w); + } + } + + // ── Render (gleicher Stil wie snake.js) ── + function drawGrid() { + ctx.strokeStyle = 'rgba(255,255,255,0.04)'; + ctx.lineWidth = 0.5; + for (let x = 0; x < COLS; x++) + for (let y = 0; y < ROWS; y++) + ctx.strokeRect(x*SZ, y*SZ, SZ, SZ); + } + + function drawSnake(s, col) { + s.body.forEach((c, i) => { + const alpha = i === 0 ? 'ff' : Math.max(0x44, 0x88 - i*2).toString(16).padStart(2, '0'); + const fill = i === 0 ? col : `${col}${alpha}`; + if (i === 0) { ctx.shadowColor = col; ctx.shadowBlur = 8; } + MGAPI.roundRect(ctx, c.x*SZ + 2, c.y*SZ + 2, SZ-4, SZ-4, 4, fill, null); + ctx.shadowBlur = 0; + }); + } + + function drawApples() { + ctx.shadowColor = APPLE_COL; + ctx.shadowBlur = 12; + ctx.font = `${SZ-2}px serif`; + ctx.textAlign = 'center'; + apples.forEach(a => ctx.fillText('🍎', a.x*SZ + SZ/2, a.y*SZ + SZ/1.2)); + ctx.shadowBlur = 0; + } + + function drawHUD() { + MGAPI.text(ctx, `🔵 ${scores[0]}`, 8, 14, { align:'left', size:13, color:P1_COL }); + MGAPI.text(ctx, `Runde ${Math.min(roundIdx+1, TOTAL_ROUNDS)} / ${TOTAL_ROUNDS}`, + W/2, 14, { align:'center', size:12, color:'rgba(255,255,255,0.85)' }); + MGAPI.text(ctx, `${scores[1]} 🔴`, W-8, 14, { align:'right', size:13, color:P2_COL }); + } + + function drawStartButton() { + const bw = Math.min(320, W*0.62), bh = 56; + const bx = (W - bw)/2, by = H/2 + 8; + startBtnRect = { x: bx, y: by, w: bw, h: bh }; + // Schatten + ctx.fillStyle = 'rgba(0,0,0,0.45)'; + MGAPI.roundRect(ctx, bx, by+4, bw, bh, 14, 'rgba(0,0,0,0.45)', null); + // Button-Gradient + const grd = ctx.createLinearGradient(0, by, 0, by+bh); + grd.addColorStop(0, '#7c3aed'); grd.addColorStop(1, '#5b21b6'); + MGAPI.roundRect(ctx, bx, by, bw, bh, 14, grd, null); + // Glanz oben + MGAPI.roundRect(ctx, bx+4, by+3, bw-8, bh/2 - 4, 12, 'rgba(255,255,255,0.18)', null); + // Text + MGAPI.text(ctx, '▶ Duell starten', bx + bw/2, by + bh/2, { + size: 22, color:'#fff', weight:'bold', family: "'Fredoka One',cursive" + }); + } + + function drawOverlayText(title, sub, color) { + ctx.fillStyle = 'rgba(0,0,0,0.62)'; + ctx.fillRect(0, 0, W, H); + MGAPI.text(ctx, title, W/2, H/2 - 20, { + size: 28, weight:'bold', color: color||'#fff', + family: "'Fredoka One',cursive", shadow:'rgba(0,0,0,0.7)', shadowBlur: 12 + }); + if (sub) MGAPI.text(ctx, sub, W/2, H/2 + 18, { size: 15, color:'rgba(255,255,255,0.88)' }); + } + + function render() { + // Hintergrund + Grid (wie snake.js) + ctx.fillStyle = '#050508'; + ctx.fillRect(0, 0, W, H); + drawGrid(); + + if (state !== 'intro') { + drawApples(); + if (s1) drawSnake(s1, s1.alive ? P1_COL : 'rgba(80,80,120,0.5)'); + if (s2) drawSnake(s2, s2.alive ? P2_COL : 'rgba(80,80,120,0.5)'); + } + drawHUD(); + + if (state === 'intro') { + MGAPI.text(ctx, 'Snake-Duell · Best of 3', W/2, H/2 - 56, { + size: 26, weight:'bold', color:'#fff', family:"'Fredoka One',cursive", + shadow:'rgba(0,0,0,0.5)', shadowBlur: 10 + }); + MGAPI.text(ctx, '🔵 Pfeiltasten (Start rechts) · 🔴 WASD (Start links)', + W/2, H/2 - 24, { size: 13, color:'rgba(255,255,255,0.85)' }); + drawStartButton(); + } else if (state === 'roundEnd') { + let t, c; + if (roundResult === 0) { t = '🔵 Runde für Spieler 1!'; c = '#93c5fd'; } + else if (roundResult === 1) { t = '🔴 Runde für Spieler 2!'; c = '#fca5a5'; } + else { t = '⚖️ Unentschieden!'; c = '#fde68a'; } + drawOverlayText(t, `Stand: 🔵 ${scores[0]} : ${scores[1]} 🔴`, c); + } else if (state === 'duelEnd') { + const w = finalWinner(); + let t, c; + if (w === 0) { t = '🏆 Spieler 1 gewinnt!'; c = '#93c5fd'; } + else if (w === 1) { t = '🏆 Spieler 2 gewinnt!'; c = '#fca5a5'; } + else { t = '🤝 Unentschieden!'; c = '#fde68a'; } + drawOverlayText(t, `Endstand: 🔵 ${scores[0]} : ${scores[1]} 🔴`, c); + } + } + + function loop(ts) { + if (stopped) return; + raf = requestAnimationFrame(loop); + if (!lastTs) lastTs = ts; + const dt = ts - lastTs; + lastTs = ts; + + if (state === 'playing') { + if (!lastStepTs) lastStepTs = ts; + if (ts - lastStepTs >= STEP_INTERVAL) { + lastStepTs = ts; + stepGame(); + } + } else { + stateTimer += dt; + if (state === 'roundEnd' && stateTimer >= 1800) advanceRound(); + else if (state === 'duelEnd' && stateTimer >= 2200) finishUp(); + // intro: wartet auf Klick auf den Start-Button + } + render(); + } + + raf = requestAnimationFrame(loop); + + return { + stop() { + if (stopped) return; + stopped = true; + cancelAnimationFrame(raf); + document.removeEventListener('keydown', onKey); + canvas.removeEventListener('click', onClick); + canvas.removeEventListener('touchend', onTouch); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, idx => MGAPI.onResult(idx)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, idx => MGAPI.onResult(idx)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, requires: REQUIRES, launch, preview }; + +})(); diff --git a/minigames/spotdiff.js b/minigames/spotdiff.js new file mode 100644 index 0000000..f010079 --- /dev/null +++ b/minigames/spotdiff.js @@ -0,0 +1,392 @@ +/** + * minigames/spotdiff.js + * 🔍 Fehler finden — Finde alle Unterschiede zwischen den zwei Bildern! + */ +window.MG_spotdiff = (function() { + + const ID = 'spotdiff'; + const EMOJI = '🔍'; + const NAME = 'Fehler finden'; + const DESC = 'Finde alle Unterschiede zwischen den zwei Bildern!'; + const CONTROLS = 'Mausklick'; + const MULTI = 1; + + // ── Bild-Sets (canvas-gezeichnete Szenen) ──────────────────────────────── + // Jede Szene besteht aus drawA(ctx,W,H) und drawB(ctx,W,H), + // plus einer Liste von Unterschied-Hotspots { x, y, r } (relativ, 0–1) + const SCENES = [ + { + name: 'Bauernhof', + drawA(ctx, W, H) { + // Himmel + ctx.fillStyle = '#7dd3fc'; ctx.fillRect(0, 0, W, H * 0.55); + // Gras + ctx.fillStyle = '#4ade80'; ctx.fillRect(0, H * 0.55, W, H * 0.45); + // Sonne + ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(W*0.15, H*0.18, H*0.1, 0, Math.PI*2); ctx.fill(); + // Haus + ctx.fillStyle = '#f87171'; ctx.fillRect(W*0.3, H*0.3, W*0.25, H*0.28); + ctx.fillStyle = '#7f1d1d'; ctx.beginPath(); ctx.moveTo(W*0.27,H*0.3); ctx.lineTo(W*0.425,H*0.12); ctx.lineTo(W*0.58,H*0.3); ctx.fill(); + // Fenster (2) + ctx.fillStyle = '#bae6fd'; ctx.fillRect(W*0.34, H*0.38, W*0.06, H*0.07); + ctx.fillStyle = '#bae6fd'; ctx.fillRect(W*0.45, H*0.38, W*0.06, H*0.07); + // Tür + ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.39, H*0.46, W*0.05, H*0.12); + // Baum (3 Kreise) + ctx.fillStyle = '#16a34a'; ctx.beginPath(); ctx.arc(W*0.75, H*0.38, W*0.06, 0, Math.PI*2); ctx.fill(); + ctx.fillStyle = '#15803d'; ctx.beginPath(); ctx.arc(W*0.72, H*0.46, W*0.05, 0, Math.PI*2); ctx.fill(); + ctx.fillStyle = '#14532d'; ctx.beginPath(); ctx.arc(W*0.79, H*0.44, W*0.05, 0, Math.PI*2); ctx.fill(); + ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.74, H*0.5, W*0.02, H*0.08); + // Wolke + ctx.fillStyle = '#fff'; + ctx.beginPath(); ctx.arc(W*0.55, H*0.14, W*0.05, 0, Math.PI*2); ctx.fill(); + ctx.beginPath(); ctx.arc(W*0.62, H*0.11, W*0.06, 0, Math.PI*2); ctx.fill(); + ctx.beginPath(); ctx.arc(W*0.69, H*0.14, W*0.05, 0, Math.PI*2); ctx.fill(); + // Vogel (klein, V-Form im Himmel) + ctx.strokeStyle = '#1f2937'; ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(W*0.85, H*0.23); ctx.lineTo(W*0.88, H*0.20); ctx.lineTo(W*0.91, H*0.23); + ctx.stroke(); + }, + drawB(ctx, W, H) { + // Himmel — IDENTISCH zu A + ctx.fillStyle = '#7dd3fc'; ctx.fillRect(0, 0, W, H * 0.55); + // Gras + ctx.fillStyle = '#4ade80'; ctx.fillRect(0, H * 0.55, W, H * 0.45); + // Sonne (UNTERSCHIED 1: weiter oben) + ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(W*0.15, H*0.10, H*0.1, 0, Math.PI*2); ctx.fill(); + // Haus + ctx.fillStyle = '#f87171'; ctx.fillRect(W*0.3, H*0.3, W*0.25, H*0.28); + ctx.fillStyle = '#7f1d1d'; ctx.beginPath(); ctx.moveTo(W*0.27,H*0.3); ctx.lineTo(W*0.425,H*0.12); ctx.lineTo(W*0.58,H*0.3); ctx.fill(); + // Fenster (UNTERSCHIED 2: zweites Fenster ist ROT statt blau) + ctx.fillStyle = '#bae6fd'; ctx.fillRect(W*0.34, H*0.38, W*0.06, H*0.07); + ctx.fillStyle = '#f87171'; ctx.fillRect(W*0.45, H*0.38, W*0.06, H*0.07); + // Tür (UNTERSCHIED 3: fehlt — Wand bleibt durchgehend rot) + // Baum (UNTERSCHIED 4: kleiner) + ctx.fillStyle = '#16a34a'; ctx.beginPath(); ctx.arc(W*0.75, H*0.42, W*0.04, 0, Math.PI*2); ctx.fill(); + ctx.fillStyle = '#15803d'; ctx.beginPath(); ctx.arc(W*0.72, H*0.48, W*0.035, 0, Math.PI*2); ctx.fill(); + ctx.fillStyle = '#14532d'; ctx.beginPath(); ctx.arc(W*0.79, H*0.46, W*0.035, 0, Math.PI*2); ctx.fill(); + ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.74, H*0.5, W*0.02, H*0.08); + // Wolke (UNTERSCHIED 5: fehlt) + // Vogel — bleibt + ctx.strokeStyle = '#1f2937'; ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(W*0.85, H*0.23); ctx.lineTo(W*0.88, H*0.20); ctx.lineTo(W*0.91, H*0.23); + ctx.stroke(); + }, + spots: [ + { x: 0.15, y: 0.10, r: 0.10, label: 'Sonne (weiter oben)' }, + { x: 0.48, y: 0.41, r: 0.06, label: 'Rotes Fenster' }, + { x: 0.42, y: 0.52, r: 0.05, label: 'Fehlende Tür' }, + { x: 0.75, y: 0.44, r: 0.08, label: 'Kleinerer Baum' }, + { x: 0.62, y: 0.13, r: 0.10, label: 'Fehlende Wolke' }, + ], + }, + // ── Szene 2: STRAND ────────────────────────────────────────────────── + { + name: 'Strand', + drawA(ctx, W, H) { + // Himmel + ctx.fillStyle = '#7dd3fc'; ctx.fillRect(0, 0, W, H * 0.6); + // Meer + ctx.fillStyle = '#0e7490'; ctx.fillRect(0, H * 0.55, W, H * 0.12); + // Sand + ctx.fillStyle = '#fde68a'; ctx.fillRect(0, H * 0.67, W, H * 0.33); + // Sonne (oben rechts) + ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(W*0.8, H*0.18, H*0.08, 0, Math.PI*2); ctx.fill(); + // Palme (links) + ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.13, H*0.4, W*0.022, H*0.4); + ctx.fillStyle = '#15803d'; + for (let i = 0; i < 5; i++) { + ctx.save(); + ctx.translate(W*0.14, H*0.4); + ctx.rotate((i - 2) * 0.35); + ctx.beginPath(); ctx.ellipse(0, -H*0.04, W*0.09, H*0.025, 0, 0, Math.PI*2); ctx.fill(); + ctx.restore(); + } + // Kokosnüsse (2) + ctx.fillStyle = '#451a03'; + ctx.beginPath(); ctx.arc(W*0.155, H*0.41, W*0.012, 0, Math.PI*2); ctx.fill(); + ctx.beginPath(); ctx.arc(W*0.13, H*0.43, W*0.012, 0, Math.PI*2); ctx.fill(); + // Sandburg (Mitte unten) + ctx.fillStyle = '#d97706'; + ctx.fillRect(W*0.42, H*0.74, W*0.18, H*0.14); + // Türme links + rechts + Mitte + ctx.fillRect(W*0.41, H*0.7, W*0.04, H*0.04); + ctx.fillRect(W*0.49, H*0.66, W*0.04, H*0.08); + ctx.fillRect(W*0.57, H*0.7, W*0.04, H*0.04); + // Flagge am mittleren Turm + ctx.fillStyle = '#dc2626'; + ctx.beginPath(); ctx.moveTo(W*0.51, H*0.62); ctx.lineTo(W*0.55, H*0.64); ctx.lineTo(W*0.51, H*0.66); ctx.fill(); + ctx.strokeStyle = '#1f2937'; ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(W*0.51, H*0.62); ctx.lineTo(W*0.51, H*0.7); ctx.stroke(); + // Schiff am Horizont (rechts) + ctx.fillStyle = '#1f2937'; ctx.fillRect(W*0.78, H*0.58, W*0.08, H*0.025); + ctx.fillStyle = '#fff'; ctx.beginPath(); + ctx.moveTo(W*0.82, H*0.58); ctx.lineTo(W*0.82, H*0.5); ctx.lineTo(W*0.85, H*0.58); ctx.fill(); + // Möwe (V) + ctx.strokeStyle = '#1f2937'; ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(W*0.36, H*0.22); ctx.lineTo(W*0.4, H*0.24); ctx.lineTo(W*0.44, H*0.22); + ctx.stroke(); + }, + drawB(ctx, W, H) { + // Himmel — IDENTISCH zu A + ctx.fillStyle = '#7dd3fc'; ctx.fillRect(0, 0, W, H * 0.6); + // Meer + ctx.fillStyle = '#0e7490'; ctx.fillRect(0, H * 0.55, W, H * 0.12); + // Sand + ctx.fillStyle = '#fde68a'; ctx.fillRect(0, H * 0.67, W, H * 0.33); + // Sonne (UNTERSCHIED 1: weiter mittig) + ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(W*0.5, H*0.18, H*0.08, 0, Math.PI*2); ctx.fill(); + // Palme (links) + ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.13, H*0.4, W*0.022, H*0.4); + ctx.fillStyle = '#15803d'; + for (let i = 0; i < 5; i++) { + ctx.save(); + ctx.translate(W*0.14, H*0.4); + ctx.rotate((i - 2) * 0.35); + ctx.beginPath(); ctx.ellipse(0, -H*0.04, W*0.09, H*0.025, 0, 0, Math.PI*2); ctx.fill(); + ctx.restore(); + } + // Kokosnüsse (UNTERSCHIED 2: nur EINE statt zwei) + ctx.fillStyle = '#451a03'; + ctx.beginPath(); ctx.arc(W*0.155, H*0.41, W*0.012, 0, Math.PI*2); ctx.fill(); + // Sandburg + ctx.fillStyle = '#d97706'; + ctx.fillRect(W*0.42, H*0.74, W*0.18, H*0.14); + ctx.fillRect(W*0.41, H*0.7, W*0.04, H*0.04); + ctx.fillRect(W*0.49, H*0.66, W*0.04, H*0.08); + ctx.fillRect(W*0.57, H*0.7, W*0.04, H*0.04); + // Flagge fehlt (UNTERSCHIED 3) + ctx.strokeStyle = '#1f2937'; ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(W*0.51, H*0.62); ctx.lineTo(W*0.51, H*0.7); ctx.stroke(); + // Schiff fehlt (UNTERSCHIED 4) + // Möwe fehlt (UNTERSCHIED 5) + }, + spots: [ + { x: 0.50, y: 0.18, r: 0.10, label: 'Sonne (weiter mittig)' }, + { x: 0.155, y: 0.43, r: 0.04, label: 'Fehlende Kokosnuss' }, + { x: 0.52, y: 0.64, r: 0.05, label: 'Fehlende Flagge' }, + { x: 0.82, y: 0.55, r: 0.08, label: 'Fehlendes Schiff' }, + { x: 0.40, y: 0.23, r: 0.07, label: 'Fehlende Möwe' }, + ], + }, + // ── Szene 3: WELTRAUM ───────────────────────────────────────────────── + { + name: 'Weltraum', + drawA(ctx, W, H) { + // Hintergrund: tiefes Nachtblau + ctx.fillStyle = '#0c0a2e'; ctx.fillRect(0, 0, W, H); + // Sterne (10 Stück) + ctx.fillStyle = '#fff'; + const stars = [[0.10,0.15],[0.22,0.32],[0.35,0.10],[0.48,0.25],[0.60,0.12], + [0.72,0.30],[0.85,0.18],[0.18,0.55],[0.90,0.50],[0.05,0.75]]; + stars.forEach(([sx,sy])=>{ ctx.beginPath(); ctx.arc(sx*W, sy*H, 2, 0, Math.PI*2); ctx.fill(); }); + // Großer Planet rechts mit Ringen (Saturn-Style) + const px = W*0.78, py = H*0.45, pr = H*0.13; + ctx.fillStyle = '#fb923c'; ctx.beginPath(); ctx.arc(px, py, pr, 0, Math.PI*2); ctx.fill(); + // Ring + ctx.strokeStyle = '#fde68a'; ctx.lineWidth = 4; + ctx.beginPath(); ctx.ellipse(px, py, pr*1.7, pr*0.35, 0.3, 0, Math.PI*2); ctx.stroke(); + // Rakete (links, schräg) + ctx.save(); ctx.translate(W*0.2, H*0.6); ctx.rotate(-0.5); + ctx.fillStyle = '#e5e7eb'; + ctx.fillRect(-W*0.025, -H*0.08, W*0.05, H*0.16); + // Spitze + ctx.fillStyle = '#dc2626'; ctx.beginPath(); + ctx.moveTo(-W*0.025, -H*0.08); ctx.lineTo(0, -H*0.13); ctx.lineTo(W*0.025, -H*0.08); ctx.fill(); + // Fenster + ctx.fillStyle = '#3b82f6'; ctx.beginPath(); ctx.arc(0, -H*0.03, W*0.012, 0, Math.PI*2); ctx.fill(); + // Flossen + ctx.fillStyle = '#dc2626'; + ctx.beginPath(); ctx.moveTo(-W*0.025, H*0.04); ctx.lineTo(-W*0.05, H*0.08); ctx.lineTo(-W*0.025, H*0.08); ctx.fill(); + ctx.beginPath(); ctx.moveTo(W*0.025, H*0.04); ctx.lineTo(W*0.05, H*0.08); ctx.lineTo(W*0.025, H*0.08); ctx.fill(); + // Flamme + ctx.fillStyle = '#fbbf24'; ctx.beginPath(); + ctx.moveTo(-W*0.015, H*0.08); ctx.lineTo(0, H*0.16); ctx.lineTo(W*0.015, H*0.08); ctx.fill(); + ctx.restore(); + // Astronaut (Mitte unten) + ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(W*0.48, H*0.75, H*0.05, 0, Math.PI*2); ctx.fill(); // Helm + ctx.fillStyle = '#1f2937'; ctx.beginPath(); ctx.ellipse(W*0.48, H*0.75, H*0.03, H*0.025, 0, 0, Math.PI*2); ctx.fill(); // Visier + ctx.fillStyle = '#e5e7eb'; ctx.fillRect(W*0.45, H*0.78, W*0.06, H*0.1); // Körper + // UFO (oben links) + ctx.fillStyle = '#9ca3af'; ctx.beginPath(); ctx.ellipse(W*0.15, H*0.25, W*0.05, H*0.018, 0, 0, Math.PI*2); ctx.fill(); + ctx.fillStyle = '#67e8f9'; ctx.beginPath(); ctx.ellipse(W*0.15, H*0.225, W*0.025, H*0.018, 0, Math.PI, 2*Math.PI); ctx.fill(); + }, + drawB(ctx, W, H) { + // Hintergrund: GLEICH wie A (kein Hintergrund-Unterschied) + ctx.fillStyle = '#0c0a2e'; ctx.fillRect(0, 0, W, H); + // Sterne — eine fehlt (UNTERSCHIED 1: nur 9 statt 10, der bei 0.48/0.25 fehlt) + ctx.fillStyle = '#fff'; + const stars = [[0.10,0.15],[0.22,0.32],[0.35,0.10],[0.60,0.12], + [0.72,0.30],[0.85,0.18],[0.18,0.55],[0.90,0.50],[0.05,0.75]]; + stars.forEach(([sx,sy])=>{ ctx.beginPath(); ctx.arc(sx*W, sy*H, 2, 0, Math.PI*2); ctx.fill(); }); + // Planet (UNTERSCHIED 2: kein Ring) + const px = W*0.78, py = H*0.45, pr = H*0.13; + ctx.fillStyle = '#fb923c'; ctx.beginPath(); ctx.arc(px, py, pr, 0, Math.PI*2); ctx.fill(); + // Rakete (UNTERSCHIED 3: keine Flamme) + ctx.save(); ctx.translate(W*0.2, H*0.6); ctx.rotate(-0.5); + ctx.fillStyle = '#e5e7eb'; + ctx.fillRect(-W*0.025, -H*0.08, W*0.05, H*0.16); + ctx.fillStyle = '#dc2626'; ctx.beginPath(); + ctx.moveTo(-W*0.025, -H*0.08); ctx.lineTo(0, -H*0.13); ctx.lineTo(W*0.025, -H*0.08); ctx.fill(); + ctx.fillStyle = '#3b82f6'; ctx.beginPath(); ctx.arc(0, -H*0.03, W*0.012, 0, Math.PI*2); ctx.fill(); + ctx.fillStyle = '#dc2626'; + ctx.beginPath(); ctx.moveTo(-W*0.025, H*0.04); ctx.lineTo(-W*0.05, H*0.08); ctx.lineTo(-W*0.025, H*0.08); ctx.fill(); + ctx.beginPath(); ctx.moveTo(W*0.025, H*0.04); ctx.lineTo(W*0.05, H*0.08); ctx.lineTo(W*0.025, H*0.08); ctx.fill(); + ctx.restore(); + // Astronaut (UNTERSCHIED 4: ROTER Anzug statt grau) + ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(W*0.48, H*0.75, H*0.05, 0, Math.PI*2); ctx.fill(); + ctx.fillStyle = '#1f2937'; ctx.beginPath(); ctx.ellipse(W*0.48, H*0.75, H*0.03, H*0.025, 0, 0, Math.PI*2); ctx.fill(); + ctx.fillStyle = '#dc2626'; ctx.fillRect(W*0.45, H*0.78, W*0.06, H*0.1); + // UFO (UNTERSCHIED 5: an anderer Position - weiter rechts oben) + ctx.fillStyle = '#9ca3af'; ctx.beginPath(); ctx.ellipse(W*0.42, H*0.15, W*0.05, H*0.018, 0, 0, Math.PI*2); ctx.fill(); + ctx.fillStyle = '#67e8f9'; ctx.beginPath(); ctx.ellipse(W*0.42, H*0.125, W*0.025, H*0.018, 0, Math.PI, 2*Math.PI); ctx.fill(); + }, + spots: [ + { x: 0.48, y: 0.25, r: 0.05, label: 'Fehlender Stern' }, + { x: 0.78, y: 0.45, r: 0.13, label: 'Fehlender Ring' }, + { x: 0.21, y: 0.74, r: 0.06, label: 'Fehlende Flamme' }, + { x: 0.48, y: 0.83, r: 0.06, label: 'Anderer Anzug (rot)' }, + { x: 0.42, y: 0.15, r: 0.07, label: 'UFO an anderer Stelle' }, + ], + }, + ]; + + // Pro Aufruf andere Szene (rotiert) — so bekommt jeder Spielzug bei multi:3 eine neue Szene + let _sceneCounter = 0; + + function run(wrap, W, H, cfg, onDone) { + const scene = SCENES[_sceneCounter % SCENES.length]; + _sceneCounter++; + const theme = cfg.theme || { primary: '#84cc16' }; + + // Zwei Canvas nebeneinander + const halfW = Math.floor((W - 8) / 2); + const imgH = H - 64; + + const container = document.createElement('div'); + container.style.cssText = 'position:relative;width:100%;'; + wrap.appendChild(container); + + const canvasA = document.createElement('canvas'); + canvasA.width = halfW; canvasA.height = imgH; + canvasA.style.cssText = `display:inline-block;border-radius:8px;cursor:default;`; + const canvasB = document.createElement('canvas'); + canvasB.width = halfW; canvasB.height = imgH; + canvasB.style.cssText = `display:inline-block;border-radius:8px;cursor:crosshair;margin-left:8px;`; + + container.appendChild(canvasA); + container.appendChild(canvasB); + + // HUD-Canvas + const hud = document.createElement('canvas'); + hud.width = W; hud.height = 48; + hud.style.display = 'block'; + container.appendChild(hud); + + const ctxA = canvasA.getContext('2d'); + const ctxB = canvasB.getContext('2d'); + const ctxH = hud.getContext('2d'); + + scene.drawA(ctxA, halfW, imgH); + scene.drawB(ctxB, halfW, imgH); + + let found = new Set(); + let marks = []; // { x, y, ok } + let stopped = false; + let endTimer; + + function drawMarks() { + scene.drawB(ctxB, halfW, imgH); // neu zeichnen + marks.forEach(m => { + ctxB.strokeStyle = m.ok ? '#22c55e' : '#ef4444'; + ctxB.lineWidth = 3; + ctxB.beginPath(); + ctxB.arc(m.x, m.y, 18, 0, Math.PI * 2); + ctxB.stroke(); + if (m.ok) { + ctxB.fillStyle = 'rgba(34,197,94,0.25)'; + ctxB.fill(); + } + }); + } + + function drawHUD() { + ctxH.clearRect(0, 0, W, 48); + MGAPI.text(ctxH, `🔍 ${found.size} / ${scene.spots.length} Unterschiede gefunden`, W/2, 16, + { size: 13, color: theme.primary }); + MGAPI.text(ctxH, 'Klicke auf die Unterschiede im rechten Bild', W/2, 36, + { size: 11, color: 'rgba(255,255,255,0.4)' }); + } + + function hitsSpot(s, mx, my) { + if (s.shape === 'rect') { + const x = s.x * halfW, y = s.y * imgH; + const w = s.w * halfW, h = s.h * imgH; + return mx >= x && mx < x + w && my >= y && my < y + h; + } + const sx = s.x * halfW, sy = s.y * imgH; + const d = Math.sqrt((mx - sx) ** 2 + (my - sy) ** 2); + return d < s.r * Math.min(halfW, imgH); + } + + canvasB.addEventListener('click', e => { + if (found.size >= scene.spots.length) return; + const rect = canvasB.getBoundingClientRect(); + const mx = (e.clientX - rect.left) * (canvasB.width / rect.width / (window.devicePixelRatio || 1)); + const my = (e.clientY - rect.top) * (canvasB.height / rect.height); + + // Reihenfolge = Priorität: erste passende Spot gewinnt (radial-Spots vor rect-Catchall) + let hit = null; + for (let i = 0; i < scene.spots.length; i++) { + if (found.has(i)) continue; + if (hitsSpot(scene.spots[i], mx, my)) { hit = i; break; } + } + + if (hit !== null) { + found.add(hit); + // Treffer-Marker an Klick-Position (statt Spot-Mitte) — funktioniert für Kreis UND Rechteck + marks.push({ x: mx, y: my, ok: true }); + // Auch in Bild A markieren (gleiche Klick-Position) + ctxA.strokeStyle = '#22c55e'; + ctxA.lineWidth = 3; + ctxA.beginPath(); ctxA.arc(mx, my, 18, 0, Math.PI * 2); ctxA.stroke(); + ctxA.fillStyle = 'rgba(34,197,94,0.2)'; ctxA.fill(); + } else { + marks.push({ x: mx, y: my, ok: false }); + setTimeout(() => { + marks = marks.filter(m => m.ok); + drawMarks(); drawHUD(); + }, 600); + } + + drawMarks(); + drawHUD(); + + if (found.size >= scene.spots.length && !endTimer) { + endTimer = setTimeout(() => onDone(true), 900); + // Overlay + ctxH.fillStyle = 'rgba(16,185,129,0.9)'; + MGAPI.roundRect(ctxH, W/2-120, 4, 240, 38, 8, 'rgba(16,185,129,0.9)', null); + MGAPI.text(ctxH, '🎉 Alle Unterschiede gefunden!', W/2, 24, + { size: 14, family: "'Fredoka One',cursive", color: '#fff' }); + } + }); + + drawHUD(); + return { + stop() { + stopped = true; + clearTimeout(endTimer); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/minigames/typing.js b/minigames/typing.js new file mode 100644 index 0000000..461abc8 --- /dev/null +++ b/minigames/typing.js @@ -0,0 +1,170 @@ +/** + * minigames/typing.js + * ⌨️ Tipp-Rennen — Tippe den Text so schnell wie möglich! + */ +window.MG_typing = (function() { + + const ID = 'typing'; + const EMOJI = '⌨️'; + const NAME = 'Tipp-Rennen'; + const DESC = 'Tippe das angezeigte Wort so schnell wie möglich!'; + const CONTROLS = 'Tastatur'; + const MULTI = 3; + + const WORD_POOLS = { + easy: ['Katze','Hund','Baum','Haus','Ball','Buch','Schule','Spiel','Kind','Mond'], + medium: ['Abenteuer','Computer','Programm','Zauber','Kristall','Phantom','Roboter','Galaxie'], + hard: ['Dinosaurier','Wissenschaft','Programmierung','Abenteuerland','Weltentdecker'], + }; + + function run(wrap, W, H, cfg, onDone) { + const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); + const theme = cfg.theme || { primary: '#f43f5e' }; + const WIN_WPM = cfg.winWpm || 30; // mind. 30 WPM zum Gewinnen + const ROUNDS = 3; + + const allWords = [...WORD_POOLS.easy, ...WORD_POOLS.medium]; + let words = []; + for (let i = 0; i < ROUNDS; i++) { + const pool = i < 2 ? WORD_POOLS.easy : WORD_POOLS.medium; + words.push(pool[Math.floor(Math.random() * pool.length)]); + } + + let round = 0; + let typed = ''; + let startTs = null; + let times = []; + let stopped = false; + let raf, endTimer, result = null; + + // HTML-Input über dem Canvas + const input = document.createElement('input'); + input.type = 'text'; + input.autocomplete = 'off'; + input.autocorrect = 'off'; + input.autocapitalize = 'none'; + input.spellcheck = false; + input.style.cssText = ` + position:absolute;left:-9999px;top:0;opacity:0;width:1px;height:1px; + `; + wrap.style.position = 'relative'; + wrap.appendChild(input); + setTimeout(() => input.focus(), 100); + canvas.addEventListener('click', () => input.focus()); + + input.addEventListener('input', () => { + typed = input.value; + if (!startTs && typed.length > 0) startTs = performance.now(); + + const word = words[round]; + if (typed.toLowerCase() === word.toLowerCase()) { + const elapsed = (performance.now() - startTs) / 1000 / 60; // Minuten + const wpm = Math.round(word.length / 5 / elapsed); // Standard: 5 Zeichen = 1 Wort + times.push({ word, wpm }); + typed = ''; + input.value = ''; + startTs = null; + + round++; + if (round >= ROUNDS) { + const avgWpm = Math.round(times.reduce((a, b) => a + b.wpm, 0) / times.length); + result = avgWpm >= WIN_WPM ? 'win' : 'lose'; + endTimer = setTimeout(() => onDone(result === 'win'), 1200); + } + } + }); + + function loop(ts) { + if (stopped) return; + raf = requestAnimationFrame(loop); + + ctx.fillStyle = '#050508'; + ctx.fillRect(0, 0, W, H); + + if (round >= ROUNDS) { + const avgWpm = times.length + ? Math.round(times.reduce((a,b) => a+b.wpm, 0) / times.length) + : 0; + MGAPI.resultScreen(ctx, W, H, result === 'win', + result === 'win' ? `${avgWpm} WPM — richtig schnell!` : `${avgWpm} WPM — weiter üben!`); + return; + } + + const word = words[round]; + const elapsed = startTs ? (ts - startTs) / 1000 : 0; + + // Fortschrittsbalken (Timing-Druck) + const timeLimit = 10; + const prog = Math.min(1, elapsed / timeLimit); + if (prog >= 1 && !endTimer && !result) { + result = 'lose'; + endTimer = setTimeout(() => onDone(false), 800); + } + + const barW = W - 40; + MGAPI.roundRect(ctx, 20, H - 28, barW, 10, 4, 'rgba(255,255,255,0.07)', null); + const barColor = prog < 0.6 ? theme.primary : prog < 0.8 ? '#f59e0b' : '#ef4444'; + MGAPI.roundRect(ctx, 20, H - 28, barW * (1 - prog), 10, 4, barColor, null); + + // Rundenanzeige + MGAPI.text(ctx, `⌨️ Runde ${round + 1} / ${ROUNDS}`, W / 2, 20, { size: 12, color: theme.primary }); + + // Zu tippendes Wort + MGAPI.roundRect(ctx, W/2-140, H*0.2, 280, 60, 12, + 'rgba(255,255,255,0.05)', `${theme.primary}44`); + MGAPI.text(ctx, word, W / 2, H * 0.2 + 34, + { size: 28, family: "'Fredoka One',cursive", color: '#fff' }); + + // Eingabe-Anzeige (zeichenweiser Vergleich) + const charW = 26; + const startX = W / 2 - (word.length * charW) / 2; + const charY = H * 0.55; + MGAPI.text(ctx, 'Tippe:', W / 2, charY - 24, { size: 11, color: 'rgba(255,255,255,0.4)' }); + + for (let i = 0; i < word.length; i++) { + const cx = startX + i * charW + charW / 2; + const tc = (typed[i] || '').toLowerCase(); + const wc = word[i].toLowerCase(); + let color; + if (!typed[i]) color = 'rgba(255,255,255,0.2)'; + else if (tc === wc) color = '#22c55e'; + else color = '#ef4444'; + + MGAPI.roundRect(ctx, startX + i * charW, charY - 4, charW - 2, 32, 4, + 'rgba(255,255,255,0.04)', `${color}66`); + MGAPI.text(ctx, typed[i] || word[i], cx, charY + 12, { size: 18, color }); + } + + // Cursor-Blinken + if (startTs || typed.length === 0) { + const cx = startX + Math.min(typed.length, word.length) * charW + 2; + if (Math.floor(ts / 500) % 2 === 0) { + ctx.fillStyle = theme.primary; + ctx.fillRect(cx, charY, 3, 32); + } + } + + // letzten WPM anzeigen + if (times.length > 0) { + const last = times[times.length - 1]; + MGAPI.text(ctx, `Letztes: ${last.wpm} WPM`, W / 2, H - 42, { size: 11, color: 'rgba(255,255,255,0.35)' }); + } + } + + raf = requestAnimationFrame(loop); + return { + stop() { + stopped = true; + clearTimeout(endTimer); + cancelAnimationFrame(raf); + if (input.parentNode) input.parentNode.removeChild(input); + }, + }; + } + + function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } + + return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; + +})(); diff --git a/phw_logo.png b/phw_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5e5dfb30447eb3ed2e4f18aca57d4bc286605f7d GIT binary patch literal 73293 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 literal 0 HcmV?d00001 diff --git a/play/codes/4KVF7E.json b/play/codes/4KVF7E.json new file mode 100644 index 0000000..1cbab28 --- /dev/null +++ b/play/codes/4KVF7E.json @@ -0,0 +1 @@ +{"devName":"No_like","name":"der fluch der karibik","desc":"piraten bret spiel","figure":"pirate","figure2":"ninja","playerCount":2,"background":"underwater","fieldCount":30,"fields":[null,"typing","spotdiff","reaction","quiz","puzzle","quiz","basketball","reaction","memory","quiz","snake2p","typing","spotdiff","simon","puzzle","quiz","flappy2p","quiz","puzzle","reaction","typing","maze","quiz","catch","spotdiff","quiz","snake","flappy",null],"storyItems":[],"quizData":[{"fieldIndex":4,"question":"WER IST DER HAUPT KARAKTER IN ASSASIN CREED 2","answers":["edwart kenway","ezio","ezio Auditore ","heywen kenwey"],"correct":2},{"fieldIndex":6,"question":"was liebt edwart kenway","answers":["rum","sein schiff","geld","family"],"correct":2},{"fieldIndex":10,"question":"wie heiß DER GAME entwickler","answers":["FRANZ","no_LIKE","NO LIKE","ROBIN"],"correct":1},{"fieldIndex":16,"question":"WENN HAST DER GAME ENTWICKLE ","answers":["LEANDER ","MARKUS","AUGUSTUS","NICKITA"],"correct":3},{"fieldIndex":18,"question":"WER SIND DIE HAUPT CARAKTERE IN GTA","answers":["ALLE ","MICHAEL","TREVOR","FRANKLIN"],"correct":0},{"fieldIndex":23,"question":"HATT EZIO ","answers":["EIN BRUDER ","EINE SCHWESTER","EIN HAUSTIER","NICHTS"],"correct":1},{"fieldIndex":26,"question":"WELCHE FIRMA GEHÖRT ASSASINE CREED","answers":["UBISOF","ABSTERGO","VETER","NIEMANDEN"],"correct":0}],"rules":{"movement":"dice","fail":"lives","lives":"5","pts":"10"},"mgSettings":{"flappy":{"win":30,"speed":4.5,"gap":175,"rampSpeed":true,"shrinkGap":true}},"worldSeed":2214350745,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/4PN6WA.json b/play/codes/4PN6WA.json new file mode 100644 index 0000000..f1f8099 --- /dev/null +++ b/play/codes/4PN6WA.json @@ -0,0 +1 @@ +{"devName":"Jason","name":"JacksonTv","desc":"Spielspass und viel Freude ","figure":"astronaut","figure2":"","playerCount":1,"background":"haunted","fieldCount":10,"fields":[null,"reaction","typing","maze","basketball","snake","catch","flappy","puzzle",null],"storyItems":[],"quizData":[],"rules":{"movement":"step","fail":"lives","lives":"1","pts":"10"},"mgSettings":{"flappy":{"win":30,"speed":4.5,"gap":110,"rampSpeed":true,"shrinkGap":true}},"worldSeed":2734155088,"worldLocked":true} \ No newline at end of file diff --git a/play/codes/4QMP7Z.json b/play/codes/4QMP7Z.json new file mode 100644 index 0000000..40d3005 --- /dev/null +++ b/play/codes/4QMP7Z.json @@ -0,0 +1 @@ +{"devName":"Steini und GKBraun","name":"Fantasyrush","desc":"Ein cooles Duo Abenteuerspiel.","figure":"dino","figure2":"dragon","playerCount":2,"background":"fantasy","fieldCount":13,"fields":[null,"typing","reaction","spotdiff","snake2p","spotdiff","memory","flappy2p","basketball","maze","typing","spotdiff",null],"storyItems":[],"quizData":[],"rules":{"movement":"dice","fail":"lives","lives":"5","pts":"10"},"mgSettings":{"flappy":{"win":15,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":3718887036,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/58LPMQ.json b/play/codes/58LPMQ.json new file mode 100644 index 0000000..d3df2c3 --- /dev/null +++ b/play/codes/58LPMQ.json @@ -0,0 +1 @@ +{"devName":"xminister","name":"lebron james","desc":"","figure":"superhero","figure2":"ninja","playerCount":2,"background":"space","fieldCount":30,"fields":[null,"basketball","flappy2p",null,"memory",null,"puzzle",null,"spotdiff",null,"maze",null,"simon","quiz",null,"catch",null,"typing",null,null,"reaction","snake",null,"snake2p",null,null,"flappy",null,null,null],"storyItems":[],"quizData":[{"fieldIndex":13,"question":"Was bedeutet Layup auf Deutsch","answers":["hochlegen","hoch schmeißen","Korbleger","dribbeln"],"correct":2}],"rules":{"movement":"dice","fail":"lives","lives":"3","pts":"10"},"mgSettings":{},"worldSeed":675013668,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/5VL3YY.json b/play/codes/5VL3YY.json new file mode 100644 index 0000000..0e79a30 --- /dev/null +++ b/play/codes/5VL3YY.json @@ -0,0 +1 @@ +{"devName":"gefährlich","name":"UNTERWASSER","desc":"man lauft durch ein labyrinth (unterwasserwelt) und muss verschiedene fragen beantworten,sachen einsammeln und kämpfen.\n","figure":"ninja","figure2":"pirate","playerCount":2,"background":"underwater","fieldCount":20,"fields":[null,"maze","reaction","snake2p","puzzle","catch","reaction","puzzle","spotdiff","snake","reaction","flappy2p","spotdiff","basketball","flappy","puzzle","spotdiff","simon","memory",null],"storyItems":[],"quizData":[],"rules":{"movement":"dice","fail":"lives","lives":"5","pts":"10"},"mgSettings":{},"worldSeed":3490040687,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/7DR474.json b/play/codes/7DR474.json new file mode 100644 index 0000000..e1aea89 --- /dev/null +++ b/play/codes/7DR474.json @@ -0,0 +1 @@ +{"devName":"game master","name":"game of death","desc":"gewinne oder stirb","figure":"ninja","figure2":"","playerCount":1,"background":"fantasy","fieldCount":37,"fields":[null,null,"simon",null,"catch",null,"flappy","quiz",null,"typing",null,"reaction",null,null,null,"spotdiff",null,"reaction",null,"maze",null,null,"memory","spotdiff",null,"quiz",null,"puzzle",null,"spotdiff","snake","reaction",null,"basketball",null,null,null],"storyItems":[],"quizData":[{"fieldIndex":7,"question":"welche schule ist die beste","answers":["real","Gymnasium","werk real","keine"],"correct":0},{"fieldIndex":25,"question":"wie viele Menschen gibt es","answers":["8 Millionen","9 Milliarden","8 Milliarden","7 Millionen"],"correct":2}],"rules":{"movement":"dice","fail":"lives","lives":"5","pts":"10"},"mgSettings":{"flappy":{"win":15,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":3470773478,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/7N5VM4.json b/play/codes/7N5VM4.json new file mode 100644 index 0000000..f2e5bf1 --- /dev/null +++ b/play/codes/7N5VM4.json @@ -0,0 +1 @@ +{"devName":"Moto-King","name":"FAST-RUN","desc":"Der als erstes im Ziel ankommt gewinnt, desto höher die Zahl auf dem Würfel desto besser ist die Chance","figure":"cat","figure2":"fox","playerCount":2,"background":"underwater","fieldCount":18,"fields":[null,null,null,"reaction",null,null,"typing",null,"catch",null,"memory","reaction",null,"reaction",null,"basketball",null,null],"storyItems":[],"quizData":[],"rules":{"movement":"dice","fail":"lives","lives":"4","pts":"10"},"mgSettings":{},"worldSeed":226427537,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/9BXACW.json b/play/codes/9BXACW.json new file mode 100644 index 0000000..07b662c --- /dev/null +++ b/play/codes/9BXACW.json @@ -0,0 +1 @@ +{"devName":"ghaith67","name":"191919beta","desc":"","figure":"dragon","figure2":"","playerCount":1,"background":"space","fieldCount":20,"fields":[null,"reaction",null,null,"typing",null,"reaction","puzzle","memory","typing",null,"flappy","simon","spotdiff","maze","snake","basketball","reaction","catch",null],"storyItems":[],"quizData":[],"rules":{"movement":"step","fail":"lives","lives":"5","pts":"10"},"mgSettings":{"flappy":{"win":22,"speed":3.5,"gap":200,"rampSpeed":true,"shrinkGap":true}},"worldSeed":2225447356,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/AA7BB5.json b/play/codes/AA7BB5.json new file mode 100644 index 0000000..d2220a6 --- /dev/null +++ b/play/codes/AA7BB5.json @@ -0,0 +1 @@ +{"devName":"cubecraft","name":"Flucht aus dem Labyrinth","desc":"man soll das spiel gewinnen","figure":"alien","figure2":"ninja","playerCount":2,"background":"haunted","fieldCount":35,"fields":[null,"quiz","reaction","spotdiff","typing","puzzle","quiz","flappy2p","simon",null,null,"snake2p","quiz","reaction","maze","puzzle","quiz","catch","spotdiff",null,"puzzle","typing","flappy","spotdiff","basketball","quiz","typing","reaction","memory","quiz",null,"snake",null,null,null],"storyItems":[],"quizData":[{"fieldIndex":1,"question":"wann wurde minecraft erfunden","answers":["2009 ","2010","2013","2020"],"correct":0},{"fieldIndex":6,"question":"wie vielle texturepacks hat cubecraft","answers":["9","21","3","10"],"correct":3},{"fieldIndex":12,"question":"wie viele spieler spielen minecraft täglich","answers":["40,3m","13,7m","31,9m","500,1m"],"correct":2},{"fieldIndex":16,"question":"welches item macht den meisten schaden","answers":["axt","mase","speer","schwert"],"correct":1},{"fieldIndex":25,"question":"welcher spieler ist der beste in unstable smp","answers":["wemmbu","Clown Pierce","farah mc","parrotx2"],"correct":0},{"fieldIndex":29,"question":"was ist die stärkste machine","answers":["tnt launcher","orbital strike canon","schwert mit schärfe 10","mase mit die beste verzauberungen"],"correct":1}],"rules":{"movement":"dice","fail":"points","lives":"3","pts":"10"},"mgSettings":{},"worldSeed":2111293881,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/B56Q2K.json b/play/codes/B56Q2K.json new file mode 100644 index 0000000..dc7eef7 --- /dev/null +++ b/play/codes/B56Q2K.json @@ -0,0 +1 @@ +{"devName":"Andi","name":"Zanninja","desc":"es soll schwirig sein und spaß machen","figure":"ninja","figure2":"","playerCount":1,"background":"fantasy","fieldCount":10,"fields":[null,"reaction","catch","basketball","typing","maze","flappy","puzzle","snake",null],"storyItems":[],"quizData":[],"rules":{"movement":"step","fail":"lives","lives":"5","pts":"10"},"mgSettings":{},"worldSeed":382314214,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/CFTWPL.json b/play/codes/CFTWPL.json new file mode 100644 index 0000000..7a4f837 --- /dev/null +++ b/play/codes/CFTWPL.json @@ -0,0 +1 @@ +{"devName":"ahimet gaming","name":"gaming is good","desc":"","figure":"dino","figure2":"","playerCount":1,"background":"haunted","fieldCount":20,"fields":[null,"spotdiff","puzzle","typing","typing","reaction","basketball","simon",null,"typing","flappy","memory","catch","reaction",null,"puzzle",null,"reaction",null,null],"storyItems":[],"quizData":[],"rules":{"movement":"dice","fail":"lives","lives":"5","pts":"10"},"mgSettings":{"flappy":{"win":10,"speed":1.8,"gap":110,"rampSpeed":true,"shrinkGap":true}},"worldSeed":267994743,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/D2FVAZ.json b/play/codes/D2FVAZ.json new file mode 100644 index 0000000..b98a09b --- /dev/null +++ b/play/codes/D2FVAZ.json @@ -0,0 +1 @@ +{"devName":"Adem","name":"GoonerTV","desc":"Spielspaß ","figure":"ninja","figure2":"","playerCount":1,"background":"space","fieldCount":10,"fields":[null,"typing","snake","reaction","catch","flappy","basketball","spotdiff","maze",null],"storyItems":[],"quizData":[],"rules":{"movement":"step","fail":"lives","lives":"5","pts":"10"},"mgSettings":{"flappy":{"win":15,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":2606163505,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/G8X49H.json b/play/codes/G8X49H.json new file mode 100644 index 0000000..d8fc108 --- /dev/null +++ b/play/codes/G8X49H.json @@ -0,0 +1 @@ +{"devName":"vanille","name":"runnery","desc":"","figure":"fox","figure2":"","playerCount":1,"background":"fantasy","fieldCount":13,"fields":[null,"memory","spotdiff","simon","typing","flappy","typing","basketball","reaction","typing","snake","maze",null],"storyItems":[],"quizData":[],"rules":{"movement":"step","fail":"lives","lives":"3","pts":"10"},"mgSettings":{"flappy":{"win":17,"speed":2.3,"gap":210,"rampSpeed":true,"shrinkGap":true}},"worldSeed":2868277306,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/HGPBEE.json b/play/codes/HGPBEE.json new file mode 100644 index 0000000..7ee4c4f --- /dev/null +++ b/play/codes/HGPBEE.json @@ -0,0 +1 @@ +{"devName":"s1nd3yy-smn","name":"brawlnite","desc":"supper spiele","figure":"dragon","figure2":"","playerCount":1,"background":"underwater","fieldCount":20,"fields":[null,"snake","quiz",null,"reaction","catch","puzzle","basketball","spotdiff","memory","simon","maze","typing","spotdiff","puzzle","spotdiff","reaction","reaction","puzzle",null],"storyItems":[],"quizData":[{"fieldIndex":2,"question":"wie alt bin ich ","answers":["19","55","13","12"],"correct":3}],"rules":{"movement":"step","fail":"points","lives":"3","pts":"10"},"mgSettings":{"flappy":{"win":30,"speed":1.8,"gap":110,"rampSpeed":true,"shrinkGap":true}},"worldSeed":1743976196,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/J732HQ.json b/play/codes/J732HQ.json new file mode 100644 index 0000000..f858bd6 --- /dev/null +++ b/play/codes/J732HQ.json @@ -0,0 +1 @@ +{"devName":"RUMI","name":"Das KPOP Quiz","desc":"","figure":"ninja","figure2":"","playerCount":1,"background":"underwater","fieldCount":15,"fields":[null,"memory","quiz","spotdiff","typing","reaction","quiz","quiz","maze","quiz","puzzle","quiz","spotdiff","quiz",null],"storyItems":[],"quizData":[{"fieldIndex":2,"question":"WIE VIELE MEMBER HAT BTS","answers":["7","8","6","10"],"correct":0},{"fieldIndex":6,"question":"AUS WELCHEM LAND KOMMT DIE KPOP MUSIK","answers":["JAPAN","AMERIKA","KOREA","DEUTSCHLAND"],"correct":2},{"fieldIndex":9,"question":"WIE HEIST DER ÄLTESTE VON STRAYKIDS","answers":["FELIX","HAN","CHANGBIN","BANGCHANG"],"correct":3},{"fieldIndex":11,"question":"WIE HEISEN DIE MEMBER VON BLACKPINK","answers":["LISA; MELLY; ROSE; JISOO","LISA; JENNY; ROSA;JISOO","JENNY; ROSE; LISA; JISOO","ROSE; LISA; MISOO; JENNY"],"correct":2},{"fieldIndex":13,"question":"IN WELCHER FARBE LEUCHTET DER BLACKPINK LIGHTSTICK","answers":["LILA","PINK","ROT","BLAU"],"correct":1},{"fieldIndex":7,"question":"WIE HEIST DER KPOP FILM FÜR KINDER","answers":["STRAYKISD STEHT KOPF","BLACKPINK IM BÄLLEBAD","BTS IN DER SCHULE","KPOP DEMON HUNTERS"],"correct":3}],"rules":{"movement":"step","fail":"lives","lives":"3","pts":"10"},"mgSettings":{},"worldSeed":787496475,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/K8ENHR.json b/play/codes/K8ENHR.json new file mode 100644 index 0000000..a037582 --- /dev/null +++ b/play/codes/K8ENHR.json @@ -0,0 +1 @@ +{"devName":"hamster563","name":"kiselstein389","desc":"ein offliene game\n","figure":"dino","figure2":"ninja","playerCount":2,"background":"underwater","fieldCount":20,"fields":[null,"snake","basketball","catch","snake2p","spotdiff","flappy","flappy2p","puzzle","memory","simon","maze","reaction","quiz","spotdiff","puzzle","spotdiff","quiz","typing",null],"storyItems":[],"quizData":[{"fieldIndex":13,"question":"wer hat die wm 2014 gewonnen ","answers":["frankreich","argentinen","deutschland ","afrika"],"correct":2},{"fieldIndex":17,"question":"wer wurde in der wm 7 zu1 von deutschland besigt ","answers":["brasilien ","argentinien ","schweitz ","frankreich"],"correct":0}],"rules":{"movement":"dice","fail":"points","lives":"5","pts":"10"},"mgSettings":{},"worldSeed":445616089,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/K8RZCV.json b/play/codes/K8RZCV.json new file mode 100644 index 0000000..0d566c9 --- /dev/null +++ b/play/codes/K8RZCV.json @@ -0,0 +1 @@ +{"devName":"ABC","name":"DEF","desc":"","figure":"dino","figure2":"fox","playerCount":2,"background":"haunted","fieldCount":6,"fields":[null,"snake2p","flappy2p","catch","spotdiff","basketball"],"storyItems":[],"quizData":[],"rules":{"movement":"step","fail":"points","lives":"3","pts":"10"},"mgSettings":{"flappy2p":{"bestOf":3,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":772068203,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/KQZJMW.json b/play/codes/KQZJMW.json new file mode 100644 index 0000000..c187e26 --- /dev/null +++ b/play/codes/KQZJMW.json @@ -0,0 +1 @@ +{"devName":"Stefan","name":"Aulendorfer Adventure","desc":"","figure":"knight","figure2":"cat","playerCount":2,"background":"fantasy","fieldCount":10,"fields":[null,"snake2p","flappy2p",null,null,null,null,null,null,null],"storyItems":[{"id":"st_1781598703137","emoji":"📖","text":"","position":"before","fieldIndex":1}],"quizData":[],"rules":{"movement":"dice","fail":"lives","lives":"3","pts":"10"},"mgSettings":{},"worldSeed":1950088632,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/M62QKS.json b/play/codes/M62QKS.json new file mode 100644 index 0000000..31670b9 --- /dev/null +++ b/play/codes/M62QKS.json @@ -0,0 +1 @@ +{"devName":"khalemimi","name":"MEISTER ELIEN","desc":"MANN MUS VERSUCHEN DEN ELIEN ZU FANGEN","figure":"alien","figure2":"astronaut","playerCount":2,"background":"space","fieldCount":13,"fields":[null,"flappy","snake","catch","simon","reaction","basketball","memory","maze","typing","snake2p","flappy2p",null],"storyItems":[],"quizData":[],"rules":{"movement":"dice","fail":"lives","lives":"3","pts":"10"},"mgSettings":{"flappy2p":{"bestOf":3,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":160670247,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/NJMCNF.json b/play/codes/NJMCNF.json new file mode 100644 index 0000000..f484220 --- /dev/null +++ b/play/codes/NJMCNF.json @@ -0,0 +1 @@ +{"devName":"Jaroslav","name":"1000mini spile","desc":"","figure":"fox","figure2":"","playerCount":1,"background":"underwater","fieldCount":20,"fields":[null,"snake","simon","maze","basketball","flappy","spotdiff","reaction","memory","catch","spotdiff","typing","puzzle","spotdiff","puzzle","reaction","reaction","puzzle","typing","typing"],"storyItems":[],"quizData":[],"rules":{"movement":"step","fail":"lives","lives":"5","pts":"10"},"mgSettings":{"flappy":{"win":30,"speed":4.5,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":3114780664,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/PGJWC7.json b/play/codes/PGJWC7.json new file mode 100644 index 0000000..b15aa47 --- /dev/null +++ b/play/codes/PGJWC7.json @@ -0,0 +1 @@ +{"devName":"Adnan","name":"ninja dream","desc":"","figure":"ninja","figure2":"","playerCount":1,"background":"space","fieldCount":20,"fields":[null,"flappy","snake","basketball","reaction","spotdiff","typing","memory","spotdiff","puzzle","typing","maze","catch","simon","spotdiff","reaction","typing","puzzle",null,"puzzle"],"storyItems":[],"quizData":[],"rules":{"movement":"step","fail":"lives","lives":"4","pts":"10"},"mgSettings":{"flappy":{"win":15,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":2954399299,"worldLocked":true} \ No newline at end of file diff --git a/play/codes/QCE29W.json b/play/codes/QCE29W.json new file mode 100644 index 0000000..37db328 --- /dev/null +++ b/play/codes/QCE29W.json @@ -0,0 +1 @@ +{"devName":"derdcmodgamer","name":"Flucht aus dem Labyrinth","desc":"","figure":"rocket","figure2":"","playerCount":1,"background":"haunted","fieldCount":10,"fields":[null,"basketball","reaction","memory","maze","spotdiff","snake","puzzle","catch",null],"storyItems":[],"quizData":[],"rules":{"movement":"step","fail":"lives","lives":"1","pts":"10"},"mgSettings":{"flappy":{"win":15,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":3792878537,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/QEE88J.json b/play/codes/QEE88J.json new file mode 100644 index 0000000..cf5b73b --- /dev/null +++ b/play/codes/QEE88J.json @@ -0,0 +1 @@ +{"devName":"adrian123626","name":"beta version","desc":"","figure":"ninja","figure2":"","playerCount":1,"background":"space","fieldCount":20,"fields":[null,"flappy","basketball","reaction","maze","snake","simon","catch","memory","reaction","puzzle","spotdiff","typing","quiz","reaction","puzzle","spotdiff","typing","typing",null],"storyItems":[],"quizData":[{"fieldIndex":13,"question":"wie geht es dir\n","answers":["gut","ne","geht","deprission"],"correct":3}],"rules":{"movement":"step","fail":"lives","lives":"3","pts":"10"},"mgSettings":{"flappy":{"win":15,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":2237941060,"worldLocked":true} \ No newline at end of file diff --git a/play/codes/SMK7DD.json b/play/codes/SMK7DD.json new file mode 100644 index 0000000..74a77cf --- /dev/null +++ b/play/codes/SMK7DD.json @@ -0,0 +1 @@ +{"devName":"Meister Proppa","name":"ling Ling","desc":"Banghenigong","figure":"astronaut","figure2":"","playerCount":1,"background":"space","fieldCount":20,"fields":[null,"basketball","snake","spotdiff","memory","flappy","simon","maze","reaction","puzzle","reaction","puzzle","spotdiff","quiz","puzzle","quiz","reaction","quiz","spotdiff",null],"storyItems":[],"quizData":[{"fieldIndex":13,"question":"Was meint ihr wie alt ist der Spielentwikler","answers":["67","13","12","15"],"correct":2},{"fieldIndex":15,"question":"Wie alt ist Ling ling","answers":["67","32","56","leider tot "],"correct":1},{"fieldIndex":17,"question":"wie dumm ist Marco_SCM der beste streamer\n","answers":["dumm","DUMM","dumm","67"],"correct":0}],"rules":{"movement":"step","fail":"lives","lives":"3","pts":"10"},"mgSettings":{},"worldSeed":2820088432,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/T9B7XH.json b/play/codes/T9B7XH.json new file mode 100644 index 0000000..501b852 --- /dev/null +++ b/play/codes/T9B7XH.json @@ -0,0 +1 @@ +{"devName":"no_abo","name":"star wars","desc":"","figure":"ninja","figure2":"knight","playerCount":2,"background":"fantasy","fieldCount":30,"fields":[null,null,"spotdiff","simon","quiz",null,null,"quiz","reaction",null,"maze","quiz","flappy",null,"basketball","quiz",null,"memory","flappy2p",null,"snake","typing",null,"quiz","snake2p",null,"spotdiff",null,null,null],"storyItems":[],"quizData":[{"fieldIndex":4,"question":"Auf welchem Planeten Wurde Aniken von obi-wan besiegt","answers":["Nabu","exegol","mustarfar","corusant"],"correct":2},{"fieldIndex":7,"question":"wer wird ein Macht Geist","answers":["Mace windu","Kit Fisto","Plo Koon","Yoda"],"correct":3},{"fieldIndex":11,"question":"wer ist Anakin Skywalkers Frau","answers":["Leia Organa","Ahsoka Tano","Padmé Amidala","Rey"],"correct":2},{"fieldIndex":15,"question":"Wer ist Edward Kenways Sohn","answers":["Haythen","conner","Ezio","Altair"],"correct":0},{"fieldIndex":23,"question":"wann kam das erste Assassine creed","answers":["2007","2016","2000","2005"],"correct":0}],"rules":{"movement":"dice","fail":"points","lives":"3","pts":"10"},"mgSettings":{"flappy":{"win":20,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":198389643,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/UU6CZ4.json b/play/codes/UU6CZ4.json new file mode 100644 index 0000000..60ecec0 --- /dev/null +++ b/play/codes/UU6CZ4.json @@ -0,0 +1 @@ +{"devName":"LaraK","name":"1,2,3 RAUS","desc":"Wen du auf die zahlen Reihe komst must du ein Spiel spielen ob du raus fliegst oder nicht","figure":"knight","figure2":"astronaut","playerCount":2,"background":"underwater","fieldCount":50,"fields":[null,null,null,"snake",null,null,"flappy",null,null,"basketball",null,null,"maze",null,null,"simon",null,null,"catch",null,null,"reaction",null,null,"puzzle",null,null,"spotdiff",null,null,"typing",null,null,"spotdiff",null,null,"memory",null,null,"spotdiff",null,null,"typing",null,null,"reaction",null,null,"puzzle",null],"storyItems":[],"quizData":[],"rules":{"movement":"dice","fail":"lives","lives":"2","pts":"10"},"mgSettings":{"flappy":{"win":15,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":2188213609,"worldLocked":true} \ No newline at end of file diff --git a/play/codes/VZUA7B.json b/play/codes/VZUA7B.json new file mode 100644 index 0000000..5509e4d --- /dev/null +++ b/play/codes/VZUA7B.json @@ -0,0 +1 @@ +{"devName":"GambleMASTER","name":"GostRUN","desc":"","figure":"ninja","figure2":"dragon","playerCount":2,"background":"haunted","fieldCount":50,"fields":[null,null,"memory",null,null,"spotdiff",null,"puzzle","flappy2p",null,"spotdiff",null,"flappy",null,"puzzle",null,"snake2p",null,"reaction",null,"puzzle",null,"maze","simon",null,"quiz","typing",null,null,"spotdiff",null,null,"reaction",null,"typing",null,"basketball",null,"quiz","reaction",null,null,null,"snake",null,"typing","quiz",null,"catch",null],"storyItems":[],"quizData":[{"fieldIndex":25,"question":"Wie viele Weltwunder gibt es","answers":["6","7","19","27"],"correct":1},{"fieldIndex":38,"question":"Wann endete der 2. Weltkrieg","answers":["1933","2026","1820","1945"],"correct":3},{"fieldIndex":46,"question":"Wer hat Minecraft erfunden","answers":["Notch","Wotch","Lotch","Noch"],"correct":0}],"rules":{"movement":"dice","fail":"lives","lives":"3","pts":"10"},"mgSettings":{"flappy":{"win":15,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":3312271192,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/W53XAM.json b/play/codes/W53XAM.json new file mode 100644 index 0000000..cbe5dca --- /dev/null +++ b/play/codes/W53XAM.json @@ -0,0 +1 @@ +{"devName":"Stefan","name":"Aulendorfer Adventure","desc":"","figure":"knight","figure2":"cat","playerCount":2,"background":"space","fieldCount":10,"fields":[null,"snake2p","snake",null,null,null,null,null,null,null],"storyItems":[],"quizData":[],"rules":{"movement":"dice","fail":"lives","lives":"3","pts":"10"},"mgSettings":{},"worldSeed":1959313746,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/WYSG2Q.json b/play/codes/WYSG2Q.json new file mode 100644 index 0000000..73a0731 --- /dev/null +++ b/play/codes/WYSG2Q.json @@ -0,0 +1 @@ +{"devName":"Lohen und Varka","name":"Tempel des Adeps","desc":"Varka und Lohen sind in sumeru angekommen und müssen jetzt vom adep fliehen ","figure":"ninja","figure2":"superhero","playerCount":2,"background":"fantasy","fieldCount":20,"fields":[null,"basketball","puzzle",null,"maze","quiz",null,null,"catch","reaction","quiz",null,"puzzle",null,"reaction",null,null,"quiz","flappy2p",null],"storyItems":[],"quizData":[{"fieldIndex":5,"question":"wie heiß ittos vorname ","answers":["paki ","arata","arataki","kikata"],"correct":2},{"fieldIndex":10,"question":"wie heißt die göttin der wälder","answers":["diah","hida","navie","nahida"],"correct":3},{"fieldIndex":17,"question":"welcher supporter ist der beste","answers":["citlali","BENNET ","escoffie","Furina "],"correct":0}],"rules":{"movement":"dice","fail":"lives","lives":"3","pts":"10"},"mgSettings":{"flappy":{"win":30,"speed":3.3,"gap":155,"rampSpeed":false,"shrinkGap":true}},"worldSeed":2721658814,"worldLocked":false} \ No newline at end of file diff --git a/play/codes/XSCYZN.json b/play/codes/XSCYZN.json new file mode 100644 index 0000000..6614677 --- /dev/null +++ b/play/codes/XSCYZN.json @@ -0,0 +1 @@ +{"devName":"ORCAPIPER","name":"Goast Run","desc":"bezwinge das geister haus","figure":"alien","figure2":"ninja","playerCount":2,"background":"haunted","fieldCount":12,"fields":[null,"reaction","snake","flappy","simon","maze","catch","basketball","typing","flappy2p","snake2p",null],"storyItems":[],"quizData":[],"rules":{"movement":"step","fail":"points","lives":"3","pts":"10"},"mgSettings":{"flappy":{"win":15,"speed":2.7,"gap":168,"rampSpeed":true,"shrinkGap":true}},"worldSeed":4170920395,"worldLocked":true} \ No newline at end of file diff --git a/play/index.html b/play/index.html new file mode 100644 index 0000000..cce7c81 --- /dev/null +++ b/play/index.html @@ -0,0 +1,125 @@ + + + + + +Spiel laden — Edu Boardgame Generator + + + + +
+ +

Spiel laden

+

Gib den 6-stelligen Code ein, den du auf deinem Ausdruck findest.

+ +
+ +
+
Edu Boardgame Generator · PH Weingarten
+
+ + + + +