From 643a00c1c3bde38271a7170ed9a8fee80018fc69 Mon Sep 17 00:00:00 2001 From: Ramon Date: Sun, 19 Jul 2026 14:03:58 +0200 Subject: [PATCH] feat: personage-aanmoediging uitgebreid naar klok, geld, plaatswaarde en spellen (v0.4.32-beta) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - De math.js-pilot (v0.4.23-beta: eigen personage kort naast "Goed!" bij een juist antwoord) hergebruikt in vier andere widgets, zoals destijds al voorzien in de roadmap: klok (mountClock), geld (mountMoney), plaatswaarde (mountPlacevalue) en de wereldoriëntatie-spelopdracht (mountWorldQuiz - dekt bomen/vogels/planten in één keer - en mountTopography). - Zelfde patroon overal: bij S.feedback==='good' (of S.spellSolved) en een ingesteld personage verschijnt renderAvatarFace(...,"md") met de av-cheer-animatie naast het bestaande feedbackbericht - puur visueel, geen nieuw feedbacktype. Zonder personage blijft het gedrag exact ongewijzigd. - Geverifieerd met echte oplos-interacties (niet alleen mounten): klok (typemodus + toetsenbord), geld (typemodus + toetsenbord), plaatswaarde (blokken plaatsen via toetsenbord-Enter) en de spelopdracht (letters plaatsen via toetsenbord-Enter) - in alle vier verschijnt het personage daadwerkelijk in de feedback-HTML na een echt juist antwoord. Volledige testsuite: 112/112. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014EPxzBVRXZZnBPvSAaPAbJ --- VERSION | 2 +- public/js/core.js | 2 +- public/js/widgets/learning.js | 6 +++--- public/js/widgets/world.js | 2 ++ 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index 48e07bc..fdacb98 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.31-beta \ No newline at end of file +0.4.32-beta \ No newline at end of file diff --git a/public/js/core.js b/public/js/core.js index 1241f27..d16404b 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -2,7 +2,7 @@ "use strict"; /* version — shown until /api/version resolves (or if the fetch fails, e.g. offline). Kept in sync by hand with the VERSION file at the repo root on every release. */ -const VERSION = "0.4.31-beta"; +const VERSION = "0.4.32-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; diff --git a/public/js/widgets/learning.js b/public/js/widgets/learning.js index 9ffab62..a77f51b 100644 --- a/public/js/widgets/learning.js +++ b/public/js/widgets/learning.js @@ -710,7 +710,7 @@ function mountClock(root,init){ if(S.task&&S.game==='set'){const prompt=document.createElement('div');prompt.className='clock-target';const label=document.createElement('span');label.textContent=T('clockYourTask');prompt.append(label,assignment(S.targetH,S.targetM,true));card.append(prompt);const work=document.createElement('div');work.className='clock-work';const answer=document.createElement('div');answer.className='clock-set-answer';if(uses24())answer.append(periodControl());const live=document.createElement('div');live.className='clock-live-answer';live.append(timeBlocks(S.h,S.m));answer.append(live);work.append(makeClock(S.h,S.m,true),answer);card.append(work);} if(S.task&&S.game==='read'){const layout=document.createElement('div');layout.className='clock-read-layout';layout.append(assignment(S.targetH,S.targetM),S.amode==='blocks'?blockAnswer():typeAnswer());card.append(layout)} if(S.task&&S.game==='choose'){card.append(assignment(S.targetH,S.targetM));if(!S.solved){const choices=document.createElement('div');choices.className='clock-choices '+(S.amode==='blocks'?'wood':'plain');const asWords=['digital','both'].includes(S.style);S.choices.forEach(option=>{const choose=()=>setFeedback(option.h===S.targetH&&option.m===S.targetM);if(asWords){const button=btn(words(option.h,option.m),choose);button.classList.add('clock-word-choice');choices.append(button)}else if(S.amode==='blocks')choices.append(timeBlocks(option.h,option.m,choose));else{const button=btn(time(option.h,option.m),choose);button.classList.add('clock-digital-choice');choices.append(button)}});card.append(choices)}} - const feedback=document.createElement('p');feedback.className='clock-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'clockCorrect':S.feedback==='incomplete'?'clockIncomplete':'clockTryAgain');card.append(feedback);const actions=document.createElement('div');actions.className='clock-actions';if(S.task&&S.game!=='choose'&&!S.solved&&!(S.game==='read'&&S.amode==='type'))actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T('clockNextTask'),newTask,'ghost'));card.append(actions);if(S.task&&S.game==='set'){const hint=document.createElement('small');hint.className='clock-drag-hint';hint.textContent=T('clockDragHint');card.append(hint)}stage.append(card)}; + const feedback=document.createElement('p');feedback.className='clock-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'clockCorrect':S.feedback==='incomplete'?'clockIncomplete':'clockTryAgain');if(S.feedback==='good'&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,'md');cheer.classList.add('av-cheer');feedback.append(cheer)}card.append(feedback);const actions=document.createElement('div');actions.className='clock-actions';if(S.task&&S.game!=='choose'&&!S.solved&&!(S.game==='read'&&S.amode==='type'))actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T('clockNextTask'),newTask,'ghost'));card.append(actions);if(S.task&&S.game==='set'){const hint=document.createElement('small');hint.className='clock-drag-hint';hint.textContent=T('clockDragHint');card.append(hint)}stage.append(card)}; style.onchange=draw;game.onchange=()=>{syncSettings();newTask()};learningShell(root,[field(T('clockLearningStyle'),style),field(T('clockGame'),game),linkSetting,hour24Setting],stage);syncSettings();newTask();return{getState:()=>({...S,choices:S.choices.map(x=>({...x})),answerDigits:[...S.answerDigits]})}; } const PLACE_COLUMNS=['HD','TD','D','H','T','E']; @@ -759,7 +759,7 @@ function mountPlacevalue(root,init){ const target=document.createElement('div');target.className='place-target';const targetLabel=document.createElement('span');targetLabel.textContent=T('placeYourTask'); const targetValue=document.createElement('output');targetValue.className='place-task-digital';targetValue.textContent=String(S.target);target.append(targetLabel,targetValue);card.append(target,buildAnswer()); const feedback=document.createElement('p');feedback.className='place-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status'); - if(S.feedback)feedback.textContent=T(S.feedback==='good'?'placeCorrect':S.feedback==='incomplete'?'placeIncomplete':'placeTryAgain');card.append(feedback); + if(S.feedback)feedback.textContent=T(S.feedback==='good'?'placeCorrect':S.feedback==='incomplete'?'placeIncomplete':'placeTryAgain');if(S.feedback==='good'&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,'md');cheer.classList.add('av-cheer');feedback.append(cheer)}card.append(feedback); const actions=document.createElement('div');actions.className='place-actions';if(!S.solved)actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T('placeNextTask'),newTask,'ghost'));card.append(actions);stage.append(card); }; input.oninput=drawExplore;game.onchange=()=>{S.game=game.value;syncSettings();if(S.game!=='build')drawExplore();else if(needsNewTask())newTask();else drawBuild()}; @@ -809,7 +809,7 @@ function mountMoney(root,init){ if(S.game==='make'){const target=document.createElement('div');target.className='money-target';const label=document.createElement('span');label.textContent=T('moneyYourTask');target.append(label,amountDisplay(S.targetCents,true));card.append(target);const wallet=document.createElement('div');wallet.className='money-wallet';const walletHead=document.createElement('div');const walletLabel=document.createElement('b');walletLabel.textContent=T('moneyWallet');const total=document.createElement('output');total.textContent=format(S.selected.reduce((sum,item)=>sum+item,0));walletHead.append(walletLabel,total);wallet.append(walletHead);if(S.selected.length)wallet.append(pile(S.selected,true,wallet));else{const hint=document.createElement('p');hint.textContent=T('moneyWalletHint');wallet.append(hint)}card.append(wallet,moneyTray(wallet))} if(S.game==='count'){const target=document.createElement('div');target.className='money-count-task';target.append(pile(S.given));card.append(target,S.amode==='blocks'?digitAnswer():typeAnswer())} if(S.game==='change'){const item=currentProduct(),shop=document.createElement('div');shop.className='money-shop-task';const product=document.createElement('div');product.className='money-product';const emoji=document.createElement('span'),productName=document.createElement('strong');emoji.className='money-product-icon';emoji.textContent=item.icon;productName.className='money-product-name';productName.textContent=LANG==='nl'?item.nl:item.en;const priceLabel=document.createElement('b');priceLabel.textContent=T('moneyPrice');product.append(emoji,productName,priceLabel,amountDisplay(S.targetCents,true));const paid=document.createElement('div');paid.className='money-paid';const paidLabel=document.createElement('b');paidLabel.textContent=T('moneyPaid');const paidAmount=document.createElement('output');paidAmount.textContent=format(S.paidCents);paid.append(paidLabel,paidAmount,pile([S.paidCents]));shop.append(product,paid);card.append(shop,S.amode==='blocks'?digitAnswer():typeAnswer())} - const feedback=document.createElement('p');feedback.className='money-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');feedback.textContent=S.feedback?feedbackText():'';card.append(feedback);const actions=document.createElement('div');actions.className='money-actions';if(!S.solved)actions.append(btn(T('numberlineCheck'),check,''));if(!S.solved)actions.append(btn(T('moneyClear'),resetAnswer,'ghost'));actions.append(btn(T('moneyNextTask'),newTask,'ghost'));card.append(actions);stage.append(card)}; + const feedback=document.createElement('p');feedback.className='money-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');feedback.textContent=S.feedback?feedbackText():'';if(S.feedback==='good'&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,'md');cheer.classList.add('av-cheer');feedback.append(cheer)}card.append(feedback);const actions=document.createElement('div');actions.className='money-actions';if(!S.solved)actions.append(btn(T('numberlineCheck'),check,''));if(!S.solved)actions.append(btn(T('moneyClear'),resetAnswer,'ghost'));actions.append(btn(T('moneyNextTask'),newTask,'ghost'));card.append(actions);stage.append(card)}; style.onchange=draw;game.onchange=()=>{S.game=game.value;syncSettings();newTask()};category.onchange=()=>{S.category=category.value;newTask()};learningShell(root,[styleSetting,gameSetting,categorySetting],stage);syncSettings();newTask();return{getState:()=>({...S,selected:[...S.selected],given:[...S.given],answerDigits:[...S.answerDigits]})}; } diff --git a/public/js/widgets/world.js b/public/js/widgets/world.js index 872b8da..07b6d56 100644 --- a/public/js/widgets/world.js +++ b/public/js/widgets/world.js @@ -663,6 +663,7 @@ function mountWorldQuiz(root,init,config){ spellFeedback.className="world-feedback "+(S.spellSolved?"ok":""); spellFeedback.setAttribute("role","status"); if(S.spellSolved)spellFeedback.textContent=T("worldSpellComplete"); + if(S.spellSolved&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,"md");cheer.classList.add("av-cheer");spellFeedback.append(cheer)} card.append(spellFeedback); if(S.spellSolved){ const fact=document.createElement("div"); @@ -1054,6 +1055,7 @@ function mountTopography(root,init){ spellFeedback.className="world-feedback "+(S.spellSolved?"ok":""); spellFeedback.setAttribute("role","status"); if(S.spellSolved)spellFeedback.textContent=T("worldSpellComplete"); + if(S.spellSolved&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,"md");cheer.classList.add("av-cheer");spellFeedback.append(cheer)} card.append(spellFeedback); const spellActions=document.createElement("div"); spellActions.className="world-actions";