/* widgets: Rekenen (+ - x : %) */
/* =========================================================
Math practice engine โ add / sub / mul / div / percentages
==========================================================*/
function mountMath(root, initState, mode, opts){
const ROUND = 10;
const onProgress = opts && opts.onProgress;
const S = { level:1, yearGroup:null, managedLearningLevel:false, snd:true, amode:"blocks", q:null, tries:0, done:0, good:0, input:"", busy:false };
if(initState){
S.level = initState.level||1;
/* yearGroup komt bij leerlingen van klassenmanagement (pupil.js). Op een
auteursbord bewaart board.js de aparte leerjaarkeuze rondom getState(). */
S.yearGroup = initState.yearGroup || null;
S.managedLearningLevel = !!initState.managedLearningLevel;
S.snd = initState.snd!==false;
S.amode = initState.amode||"blocks";
}
const play = f=>{ if(S.snd) f(); };
const rnd = n => Math.floor(Math.random()*n);
root.innerHTML = `
`;
const el = s => root.querySelector(s);
/* pilot voor "personage moedigt aan" (roadmap-gerelateerd punt): naast het
bestaande "Goed!"-bericht verschijnt kort het eigen personage, als de
leerling er รฉรฉn heeft ingesteld. Puur visueel, geen nieuw feedbacktype -
zonder personage blijft dit exact het oude gedrag. */
function showGoodFeedback(){
fbEl.innerHTML = "";
fbEl.appendChild(document.createTextNode(T("mGood")));
if(currentUser && currentUser.avatar){
const cheer = renderAvatarFace(currentUser.avatar, "md");
cheer.classList.add("av-cheer");
fbEl.appendChild(cheer);
}
}
const mtEl = el(".mt"), qEl = el(".mt-q"), aEl = el(".mt-a"), fbEl = el(".mt-fb"),
scoreEl = el(".mt-score"), doneEl = el(".mt-done"),
sndBtn = el(".lg-snd"), modeBtn = el(".lg-mode"),
padEl = el(".mt-pad"), slotsEl = el(".mt-slots"), trayEl = el(".mt-tray");
/* ---- digit blocks with dot patterns (like the letter blocks) ---- */
function makeDigitBlock(d){
const b = document.createElement("div");
b.className = "blok sm";
b.dataset.digit = d;
b.innerHTML = `${d}
`;
const dg = dotGrid(d); dg.classList.add("blok-dots");
b.appendChild(dg);
attachDigitDrag(b);
return b;
}
"0123456789".split("").forEach(d=>trayEl.appendChild(makeDigitBlock(d)));
function makeDigitSlot(d){
const s = document.createElement("div");
s.className = "slot sm";
s.dataset.digit = d;
s.innerHTML = ``;
const dg = dotGrid(d); dg.classList.add("slot-dots");
s.appendChild(dg);
s.addEventListener("click", ()=>{
const placed = s.querySelector(".blok");
if(placed && Date.now() - (+placed.dataset.placedAt||0) > 400){
placed.remove();
s.classList.remove("filled");
}
});
return s;
}
function attachDigitDrag(block){
block.addEventListener("pointerdown", e=>{
if(e.pointerType==="mouse" && e.button!==0) return;
if(S.busy || doneEl.classList.contains("show")) return;
e.preventDefault();
try{ block.releasePointerCapture(e.pointerId); }catch(_){}
const clone = block.cloneNode(true);
clone.id = "dragClone";
document.body.appendChild(clone);
const moveTo = ev=>{ const zf = VZ(); clone.style.left = (ev.clientX/zf)+"px"; clone.style.top = (ev.clientY/zf)+"px"; };
moveTo(e);
let hovered = null;
const hit = ev=>{
const pad = 8;
let found = null;
slotsEl.querySelectorAll(".slot:not(.filled)").forEach(s=>{
const r = s.getBoundingClientRect();
if(ev.clientX>=r.left-pad && ev.clientX<=r.right+pad &&
ev.clientY>=r.top-pad && ev.clientY<=r.bottom+pad) found = s;
});
return found;
};
const move = ev=>{
moveTo(ev);
const s = hit(ev);
if(hovered && hovered!==s) hovered.classList.remove("droptarget");
hovered = s;
if(hovered) hovered.classList.add("droptarget");
};
const up = ev=>{
document.removeEventListener("pointermove",move);
document.removeEventListener("pointerup",up);
document.removeEventListener("pointercancel",cancel);
clone.remove();
if(hovered) hovered.classList.remove("droptarget");
const target = hit(ev);
if(target){
if(target.dataset.digit === block.dataset.digit){
const copy = block.cloneNode(true);
copy.classList.add("in-slot");
copy.dataset.placedAt = Date.now();
target.appendChild(copy);
target.classList.add("filled");
play(sndGood);
if(!slotsEl.querySelector(".slot:not(.filled)")) blocksSolved();
}else{
target.classList.add("shake");
setTimeout(()=>target.classList.remove("shake"), 400);
play(sndBad);
}
}
};
const cancel = ()=>{
document.removeEventListener("pointermove",move);
document.removeEventListener("pointerup",up);
document.removeEventListener("pointercancel",cancel);
clone.remove();
if(hovered) hovered.classList.remove("droptarget");
};
document.addEventListener("pointermove",move);
document.addEventListener("pointerup",up);
document.addEventListener("pointercancel",cancel);
});
}
function blocksSolved(){
S.busy = true;
S.good++;
showGoodFeedback();
play(sndWin);
confetti(el(".mt-card"));
setTimeout(next, 1000);
}
function gen(){
const L = S.level;
/* klassenmanagement geeft een groep (leerjaar) mee via yearGroup - dan
sturen de SLO-gebaseerde getalgrenzen (data.js: mathRangeFor) de
inhoud, niet meer de vaste 10/20/100-klem. Zonder groep (geen
klassenmanagement ingesteld voor deze leerling/dit bord) blijft het
oude gedrag exact hetzelfde. */
const slo = mathRangeFor(S.yearGroup, L);
const rules = mathTaskRules(S.yearGroup, L);
let a, b, ans, txt;
if(mode==="add"){
const max = rules ? rules.addSub : (slo ? slo.addSub : (L===1 ? 10 : L===2 ? 20 : 100));
a = rnd(max+1); b = rnd(max-a+1); ans = a+b;
txt = `${a} + ${b} =`;
}else if(mode==="sub"){
const max = rules ? rules.addSub : (slo ? slo.addSub : (L===1 ? 10 : L===2 ? 20 : 100));
a = rnd(max+1); b = rnd(a+1); ans = a-b;
txt = `${a} โ ${b} =`;
}else if(mode==="mul"){
const tmax = rules ? rules.timesTable : (slo ? slo.timesTable : (L===1 ? 5 : L===2 ? 10 : 12));
a = 1+rnd(tmax); b = 1+rnd(rules ? rules.factorMax : (L===3 ? 12 : 10)); ans = a*b;
txt = `${a} ร ${b} =`;
}else if(mode==="div"){
const tmax = rules ? rules.timesTable : (slo ? slo.timesTable : (L===1 ? 5 : L===2 ? 10 : 12));
const t = 1+rnd(tmax), q = 1+rnd(rules ? rules.factorMax : (L===3 ? 12 : 10));
a = t*q; ans = q;
txt = `${a} ${S.lang==="nl" || LANG==="nl" ? ":" : "รท"} ${t} =`;
}else{ /* pct */
const sets = rules ? rules.percentSets : (L===1 ? [10,50,100] : L===2 ? [10,20,25,50,75,100] : [5,10,20,25,30,40,50,60,70,75,80,90]);
const p = sets[rnd(sets.length)];
const step = {5:20,10:10,15:20,20:5,25:4,30:10,40:5,50:2,60:5,70:10,75:4,80:5,90:10,100:1}[p];
const base = step*(1+rnd(12));
ans = p*base/100;
txt = `${p}% ${T("mOf")} ${base} =`;
}
return { txt, ans };
}
function newQuestion(){
let q = gen();
if(S.q && q.txt===S.q.txt) q = gen(); /* avoid the same sum twice in a row */
S.q = q; S.tries = 0; S.input = ""; S.busy = false;
qEl.textContent = q.txt;
aEl.textContent = "";
aEl.classList.remove("ok");
fbEl.textContent = ""; fbEl.classList.remove("ans");
slotsEl.innerHTML = "";
if(S.amode==="blocks"){
String(q.ans).split("").forEach(d=>slotsEl.appendChild(makeDigitSlot(d)));
}
updateScore();
}
function updateScore(){
scoreEl.textContent = `${S.good} ${T("mGoed")} ยท ${Math.min(S.done+1,ROUND)}/${ROUND}`;
}
function endRound(){
doneEl.querySelector(".dt").textContent = T("mDoneT");
doneEl.querySelector(".ds").textContent = `${S.good}/${ROUND} ${T("mGoed")}`;
doneEl.querySelector(".dg").textContent = S.good>=8 ? "๐" : S.good>=5 ? "๐" : "๐ช";
doneEl.classList.add("show");
if(S.good>=8) play(sndWin);
}
function next(){
S.done++;
if(S.done >= ROUND){ endRound(); }
else newQuestion();
}
function check(){
if(S.busy || S.input==="") return;
if(+S.input === S.q.ans){
S.busy = true;
S.good++;
aEl.classList.add("ok");
showGoodFeedback();
play(sndGood);
confetti(el(".mt-card"));
/* per som loggen (niet meer รฉรฉn keer per blok van 10 in endRound()) -
zo levert een sessie ook echt een groep aparte momenten op in de
voortgang, en weet de leerkracht welke som het was. */
if(onProgress) onProgress({ attempts: S.tries+1, correct: 1, stars: 0,
detail: { question: S.q.txt, answer: +S.input, expected: S.q.ans, correct: true } });
setTimeout(next, 900);
}else{
S.tries++;
aEl.classList.add("shake");
setTimeout(()=>aEl.classList.remove("shake"), 400);
play(sndBad);
const wrongAnswer = S.input;
S.input = ""; aEl.textContent = "";
if(S.tries >= 3){
S.busy = true;
fbEl.textContent = `${T("mAnsWas")} ${S.q.ans}`;
fbEl.classList.add("ans");
if(onProgress) onProgress({ attempts: S.tries, correct: 0, stars: 0,
detail: { question: S.q.txt, answer: +wrongAnswer, expected: S.q.ans, correct: false } });
setTimeout(next, 1800);
}
}
}
["7","8","9","โซ","4","5","6","0","1","2","3","โ"].forEach(k=>{
const b = document.createElement("button");
b.textContent = k;
if(k==="โ") b.className = "ok";
b.addEventListener("click", ()=>{
if(doneEl.classList.contains("show")) return;
if(k==="โซ"){ if(!S.busy){ S.input = S.input.slice(0,-1); aEl.textContent = S.input; } }
else if(k==="โ") check();
else if(!S.busy && S.input.length<8){ S.input += k; aEl.textContent = S.input; }
});
padEl.appendChild(b);
});
el(".mt-again").addEventListener("click", ()=>{
S.done = 0; S.good = 0;
doneEl.classList.remove("show");
newQuestion();
});
root.querySelectorAll(".lg-levels button").forEach(b=>{
b.disabled = S.managedLearningLevel;
b.addEventListener("click", ()=>{
S.level = +b.dataset.l;
S.done = 0; S.good = 0;
doneEl.classList.remove("show");
labels(); newQuestion();
});
});
sndBtn.addEventListener("click", ()=>{
S.snd = !S.snd;
sndBtn.textContent = S.snd ? "๐" : "๐";
sndBtn.classList.toggle("off", !S.snd);
});
modeBtn.addEventListener("click", ()=>{
S.amode = S.amode==="blocks" ? "type" : "blocks";
labels(); newQuestion();
});
function labels(){
root.querySelectorAll(".lg-levels button").forEach(b=>{
b.textContent = T("lvl"+b.dataset.l);
b.classList.toggle("active", +b.dataset.l===S.level);
});
sndBtn.textContent = S.snd ? "๐" : "๐";
sndBtn.classList.toggle("off", !S.snd);
sndBtn.title = T("sndWidget");
modeBtn.textContent = S.amode==="blocks" ? "๐งฑ" : "โจ๏ธ";
modeBtn.title = T("amode");
mtEl.classList.toggle("blocks", S.amode==="blocks");
el(".mt-again").textContent = T("mAgain");
updateScore();
}
document.addEventListener("langchange", ()=>{ labels(); newQuestion(); });
labels();
newQuestion();
root.classList.toggle("managed-learning-level",S.managedLearningLevel);
return {
getState: ()=>({ level:S.level, snd:S.snd, amode:S.amode }),
setState: st=>{ if(st){ S.level=st.level||1;S.yearGroup=st.yearGroup||null;S.managedLearningLevel=!!st.managedLearningLevel; S.snd=st.snd!==false; S.amode=st.amode||"blocks"; labels(); newQuestion(); } }
};
}