edu-boardgame-generator/js/quiz.js

232 lines
16 KiB
JavaScript

/* 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=`<div class="card"><div class="no-quiz"><div class="nq-icon">❓</div><h3>Keine Quiz-Felder platziert</h3><p style="color:var(--muted);font-size:13px">In Schritt 3 hast du keine Quiz-Felder hinzugefügt.<br>Du kannst trotzdem weitermachen!</p></div></div>`;
document.getElementById('s4next').disabled=false;return;
}
const done=ST.quizData.filter(isQComplete).length;
const ov=document.createElement('div');ov.className='ov-bar';
ov.innerHTML=`<div class="ov-pill ${done===qidx.length?'good':'warn'}">✅ Fertig: <span>${done}/${qidx.length}</span></div><div class="ov-pill">❓ Quiz-Felder: <span>${qidx.length}</span></div>`;
area.appendChild(ov);
const af=document.createElement('div');af.className='af-bar';
af.innerHTML=`<div class="af-icon">✨</div><div class="af-text"><div class="af-title">Beispielfragen</div><div class="af-sub">Alle leeren Felder automatisch ausfüllen</div></div><button class="btn-sm" onclick="autoFill()">Ausfüllen</button>`;
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<qidx.length;
const done=ST.quizData.filter(isQComplete).length;
const p=document.querySelector('.ov-pill');if(p){p.className='ov-pill '+(done===qidx.length?'good':'warn');p.innerHTML=`✅ Fertig: <span>${done}/${qidx.length}</span>`;}
}
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 `<div class="ans-wrap ${q.correct===i?'correct':''}" id="aw_${fi}_${i}"><div class="ans-letter">${L}</div><input type="text" class="ans-input" maxlength="80" placeholder="Antwort ${L}..." value="${v}" oninput="updQ(${fi},'a${i}',this.value)"/></div>`;
}).join('');
const corr = LETTERS.map((L,i)=>`<button class="correct-btn ${q.correct===i?'selected':''}" onclick="setCorr(${fi},${i})">${L}</button>`).join('');
c.innerHTML=`
<div class="qc-head">
<div class="qc-num" id="qnum_${fi}">${idx+1}</div>
<div class="qc-title">Quiz-Frage ${idx+1}</div>
<div class="qc-status ${comp?'done':'todo'}" id="qst_${fi}">${comp?'✓ Fertig':'⏳ Ausfüllen'}</div>
</div>
<label class="lbl">Frage (📍 ${fl})</label>
<textarea class="q-input" rows="2" maxlength="200" placeholder="Schreib deine Frage hier..." oninput="updQ(${fi},'q',this.value)">${esc(q.question||'')}</textarea>
<label class="lbl">Die vier Antworten</label>
<div class="ans-grid" id="ag_${fi}">${ans}</div>
<div class="correct-row">
<span class="correct-lbl">✅ Richtige Antwort:</span>
<div class="correct-btns">${corr}</div>
</div>
<button class="tips-btn" onclick="this.nextElementSibling.classList.toggle('open')">💡 Tipps</button>
<div class="tips-box">• Stelle eine klare, eindeutige Frage<br>• Alle 4 Antworten sollten plausibel klingen<br>• Nur eine Antwort ist korrekt<br>• Beziehe die Frage auf dein Spielthema!</div>
`;
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=`<div class="hero-scene">${esc(bg.scene)}<span class="hero-fig">${esc(fig.e)}</span></div><div class="hero-info"><div class="hi-name">${esc(ST.name||'Mein Spiel')}</div><div class="hi-desc">${esc(ST.desc||'Keine Beschreibung.')}</div><div class="hero-tags"><div class="hero-tag">${esc(fig.e)} ${esc(fig.n)}</div><div class="hero-tag">🌍 ${esc(bg.n)}</div><div class="hero-tag">⬛ ${esc(String(ST.fieldCount))} Felder</div></div></div>`;
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=`<div class="card-title" style="margin-bottom:12px">⚙️ Spielregeln</div><div class="info-grid"><div class="info-block"><div class="ib-lbl">🎲 Bewegung</div><div class="ib-val">${ST.rules.movement==='dice'?'🎲 Würfeln':'👣 Ein Feld'}</div></div><div class="info-block"><div class="ib-lbl">${ST.rules.fail==='lives'?'❤️ Leben':'⭐ Punkte'}</div><div class="ib-val">${ST.rules.fail==='lives'?ST.rules.lives+' Leben':'+'+ST.rules.pts+' Pts'}</div></div><div class="info-block"><div class="ib-lbl">🕹️ Mini-Games</div><div class="ib-val">${filled} Felder</div></div><div class="info-block"><div class="ib-lbl">📖 Erzähltexte</div><div class="ib-val">${ST.storyItems.length}</div></div></div>`;
area.appendChild(rules);
// Timeline
const tlCard=document.createElement('div');tlCard.className='card';
tlCard.innerHTML='<div class="card-title" style="margin-bottom:12px">🗺️ Spielablauf</div>';
const tl=document.createElement('div');tl.className='tl';
for(let i=0;i<ST.fieldCount;i++){
const g=ST.fields[i];const mg=g?MINIGAMES.find(m=>m.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=`<div class="tl-dot st">${esc(s.emoji||'📖')}</div><div><div class="tl-lbl st">Erzähl-Text</div><div class="tl-sub">${esc(s.text||'(noch leer)')}</div><div class="tl-badge st">Vor Feld ${i}</div></div>`;
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=`<div class="tl-badge">${esc(mg.e)} ${esc(mg.n)}</div>`;}
else{dCls='em';lCls='em';lbl='Feld '+i;sub='Leeres Durchgangsfeld';}
it.innerHTML=`<div class="tl-dot ${esc(dCls)}">${isSt?'🟢':isEn?'🏁':mg?esc(mg.e):'⬜'}</div><div><div class="tl-lbl ${esc(lCls)}">${esc(lbl)}</div><div class="tl-sub">${esc(sub)}</div>${badge}</div>`;
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=`<div class="tl-dot st">${esc(s.emoji||'📖')}</div><div><div class="tl-lbl st">Erzähl-Text</div><div class="tl-sub">${esc(s.text||'(noch leer)')}</div><div class="tl-badge st">Nach Feld ${i}</div></div>`;
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='<div class="card-title" style="margin-bottom:10px">❓ Quiz-Fragen</div>';
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=`<div class="qp-q">Frage ${i+1}: ${esc(q.question||'(leer)')}</div><div class="qp-ans-grid">${LETTERS.map((L,j)=>`<div class="qp-ans ${q.correct===j?'cor':''}">${esc(L)} ${esc(q.answers[j]||'?')}${q.correct===j?' ✓':''}</div>`).join('')}</div>`;
ql.appendChild(qi);
});
qc.appendChild(ql);area.appendChild(qc);
}
// Checklist
const cc=document.createElement('div');cc.className='card';cc.innerHTML='<div class="card-title" style="margin-bottom:10px">🔍 Checkliste</div>';
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=`<span>${ch.ok?'✅':'⚠️'}</span>${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=[
`<div class="lh-pill">${fig.e} ${fig.n}</div>`,
`<div class="lh-pill">🌍 ${bg.n}</div>`,
`<div class="lh-pill">⬛ ${ST.fieldCount} Felder</div>`,
`<div class="lh-pill">${ST.rules.fail==='lives'?'❤️ '+ST.rules.lives+' Leben':'⭐ Punkte'}</div>`,
].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=>`<div class="lc-item"><span>${x.i}</span><span style="flex:1">${x.t}</span><span class="lc-check">✓</span></div>`).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=fbCur<FB_QS.length-1?'Weiter →':'Auswertung ✅';
const area=document.getElementById('fbArea');
area.innerHTML=`<div class="fb-qnum">Frage ${fbCur+1} von ${FB_QS.length}</div><div class="fb-q">${q.q}</div>${q.sub?`<div class="fb-sub">${q.sub}</div>`:''}`;
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=`<div class="mc-dot"></div>${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=`<span>${q.min}</span><span>${q.max}</span>`;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();}}