1/* Fox Hollow â Module 2/5 : moteurs de jeu, navigation, écrans enfant et espace professionnel. */ 2 3/* sélection d'items selon le niveau courant */ 4function quizPick(opts){ 5 var g=state.grade||'P1'; 6 if(opts.gen) return opts.gen(g); 7 var gi=GRADES.indexOf(g); 8 while(gi>0 && !opts.data[GRADES[gi]]){ gi--; } 9 return (opts.data[GRADES[gi]]||opts.data['P1']||[]).slice(); 10} 11/* moteur de quiz générique (Maths, Géographieâ¦) */ 12function playQuiz(host, opts){ 13 var items=quizPick(opts).slice(0,5); mission.total=items.length; var idx=0; 14 function step(){ 15 host.innerHTML=''; var it=items[idx]; 16 missionHeader(host,'Ãtape '+(idx+1)+' sur '+items.length,(idx/items.length)*100); 17 var big = String(it.stim||'').length>6 ? '30px' : '52px'; 18 var stimHtml = it.stim ? '<div class="stim" style="font-size:'+big+'">'+it.stim+'</div>' : ''; 19 var c=el('<div class="consigne"><div class="txt">'+it.q+'</div>'+stimHtml+'<button class="play-audio" title="Ãcouter">ð</button><div class="choices"></div><div class="feedback" aria-live="polite"></div><div class="helpbar"></div></div>'); 20 function say(){ speak(it.q.replace(/[Ãâ+=·]/g,' ')); } 21 c.querySelector('.play-audio').onclick=say; 22 var choicesBox=c.querySelector('.choices'), fb=c.querySelector('.feedback'), helpbar=c.querySelector('.helpbar'); 23 var attempts=0, choix=it.choix.slice(); 24 function draw(){ choicesBox.innerHTML=''; choix.forEach(function(ch){ var b=el('<button class="choice">'+ch+'</button>'); b.onclick=function(){ pick(ch,b); }; choicesBox.appendChild(b); }); } 25 function pick(ch,b){ 26 if(String(ch)===String(it.rep)){ 27 b.classList.add('good'); fb.className='feedback pos'; fb.textContent=it.expl||'Bravo, câest juste !'; speak('Bravo !'); 28 mission.correct++; Array.prototype.forEach.call(choicesBox.children,function(x){x.classList.add('dim');}); setTimeout(next,1200); 29 } else { 30 b.classList.add('bad'); attempts++; mission.errors++; 31 if(attempts===1){ fb.className='feedback neutre'; fb.textContent='Presque⦠regarde encore, puis réessaie.'; } 32 else if(attempts===2){ fb.className='feedback neutre'; fb.textContent='Prends ton temps, tu peux y arriver.'; 33 if(!helpbar.dataset.on){ helpbar.dataset.on='1'; var h=el('<button class="btn ghost">ð¡ Montre-moi</button>'); 34 h.onclick=function(){ mission.helpsUsed++; Array.prototype.forEach.call(choicesBox.children,function(x){ if(String(x.textContent)===String(it.rep)) x.classList.add('good'); }); fb.textContent='La bonne réponse est '+it.rep+'.'; }; helpbar.appendChild(h); } } 35 else { fb.textContent='On enlève un choix pour tâaider.'; var wrong=choix.filter(function(x){return String(x)!==String(it.rep) && String(x)!==String(ch);}); choix=shuffle([it.rep].concat(wrong.slice(0,1))); mission.helpsUsed++; draw(); } 36 setTimeout(function(){ b.classList.remove('bad'); },600); 37 } 38 } 39 draw(); host.appendChild(c); quitRow(host); say(); 40 } 41 function next(){ idx++; if(idx>=items.length) endMission(); else step(); } 42 step(); 43} 44/* séquence selon le niveau (Histoire) */ 45function playSequenceByGrade(host, map){ 46 var gi=GRADES.indexOf(state.grade||'P1'); 47 while(gi>0 && !map[GRADES[gi]]){ gi--; } 48 var data=map[GRADES[gi]]||map['P1']||map[Object.keys(map)[0]]; 49 playSequence(host,data); 50} 51 52function go(screen){ state.screen=screen; render(); window.scrollTo(0,0); } 53function setRole(r){ 54 state.role=r; 55 roleBadge.style.display = r? 'inline-block':'none'; 56 roleBadge.textContent = r==='enfant'?'ENFANT': r==='pro'?'PROFESSIONNEL': r==='admin'?'ADMIN':'PARENT'; 57 homeBtn.style.display = r? 'inline-flex':'none'; 58} 59homeBtn.onclick=function(){ setRole(null); go('accueil'); }; 60 61function screenAccueil(){ 62 setRole(null); app.innerHTML=''; 63 var wrap=el('<div class="center-narrow"></div>'); 64 var hero=el('<div class="hero"><h1>Le Vallon des Apprentissages</h1><p>Un prototype cliquable de la plateforme Fox Hollow : lâenfant apprend en aidant les habitants du vallon, les adultes préparent, suivent et impriment.</p><div style="margin-top:16px;display:flex;gap:10px;justify-content:center;flex-wrap:wrap"><button class="btn lg" id="goCompte">ð Sâinscrire / Se connecter</button></div><p class="form-note" style="margin-top:8px">Choisissez un espace. Les espaces adultes demandent une connexion (mot de passe, SSO Microsoft 365/Google, ou MFA pour lâadministration) ; lâenfant entre par code ou QR de séance.</p></div>'); 65 hero.querySelector('#goCompte').onclick=function(){ go('compte'); }; 66 wrap.appendChild(hero); 67 var grid=el('<div class="grid g3"></div>'); 68 var roles=[ 69 {r:'enfant',em:'ð¦',t:'Espace enfant',d:'Carte, missions, mini-jeux, carnet et badges.',pills:['5 à 9 ans','séances courtes','aides visuelles']}, 70 {r:'pro',em:'ð',t:'Espace professionnel',d:'Tableau de bord, créateur de séance, résultats.',pills:['enseignant','logopède','imprimables']},
71 {r:'parent',em:'ð¡',t:'Espace parent',d:'Progression simple, temps dâécran, abonnement.',pills:['usage familial','temps maîtrisé']}, 72 {r:'admin',em:'ð ï¸',t:'Administration',d:'Clients, domaines SSO, licences, audit et support.',pills:['Fox Hollow','sécurité','SSO 365']} 73 ]; 74 roles.forEach(function(o){ 75 var c=el('<button class="card role-card"></button>'); 76 var lockHint = o.r==='enfant' ? '<div style="font-size:11px;color:var(--brun);margin-top:6px">ð Accès par code / QR de séance</div>' : (isAuthorized(o.r)?'<div style="font-size:11px;color:var(--ok);margin-top:6px">ð Connecté</div>':'<div style="font-size:11px;color:var(--brun);margin-top:6px">ð Connexion requise'+(o.r==='admin'?' + MFA':'')+'</div>'); 77 c.innerHTML='<span class="role-emoji">'+o.em+'</span><h3>'+o.t+'</h3><small>'+o.d+'</small><div>'+o.pills.map(function(p){return '<span class="pill">'+p+'</span>';}).join('')+'</div>'+lockHint; 78 c.onclick=function(){ if(o.r==='enfant'){ state.compteTab='enfant'; go('compte'); } else { enterSpace(o.r); } }; 79 grid.appendChild(c); 80 }); 81 wrap.appendChild(grid); 82 wrap.appendChild(el('<div class="disclaimer">â ï¸ Prototype de démonstration UX (jalon J1). Contenus, illustrations et voix sont provisoires. Le produit nâest pas un dispositif de diagnostic ou de soin médical. Aucune donnée personnelle nâest collectée.</div>')); 83 app.appendChild(wrap); 84} 85 86function screenCarte(){ 87 setRole('enfant'); app.innerHTML=''; 88 var who = state.child ? (state.child.avatar+' '+state.child.pseudo) : 'Lina'; 89 90 var bar=el('<div class="card" style="margin-bottom:16px"></div>'); 91 bar.appendChild(el('<div style="font-weight:700;color:var(--foret);margin-bottom:6px">Choisis une matière</div>')); 92 var mrow=el('<div class="filters"></div>'); 93 MATIERES.forEach(function(m){ 94 var b=el('<button class="chip '+(state.matiere===m.nom?'active':'')+'">'+m.emoji+' '+m.nom+'</button>'); 95 b.onclick=function(){ state.matiere=m.nom; screenCarte(); }; 96 mrow.appendChild(b); 97 }); 98 bar.appendChild(mrow); 99 bar.appendChild(el('<div style="font-weight:700;color:var(--foret);margin:12px 0 6px">Niveau (année scolaire)</div>')); 100 var grow=el('<div class="filters"></div>'); 101 GRADES.forEach(function(g){ 102 var b=el('<button class="chip '+(state.grade===g?'active':'')+'">'+g+'</button>'); 103 b.onclick=function(){ state.grade=g; screenCarte(); }; 104 grow.appendChild(b); 105 }); 106 bar.appendChild(grow); 107 app.appendChild(bar); 108 109 var wrap=el('<div class="enfant-wrap"></div>'); 110 var map=el('<div class="map"><h2>Bonjour '+who+', où allons-nous aujourdâhui ?</h2><div class="map-path" id="mapPath"></div></div>'); 111 var keys=Object.keys(JEUX).filter(function(k){ return JEUX[k].matiere===state.matiere; }).slice(0,4); 112 var positions=[{x:'2%',y:'52%'},{x:'28%',y:'8%'},{x:'52%',y:'55%'},{x:'78%',y:'14%'}]; 113 var ptsAll=['8,60','34,18','58,63','84,24']; 114 var path=map.querySelector('#mapPath'); 115 path.appendChild(el('<svg class="link" viewBox="0 0 100 100" preserveAspectRatio="none"><polyline points="'+ptsAll.slice(0,Math.max(2,keys.length)).join(' ')+'" fill="none" stroke="#C99A3F" stroke-width="1.4" stroke-linejoin="round"/></svg>')); 116 keys.forEach(function(k,i){ 117 var j=JEUX[k]; var d=positions[i]; 118 var node=el('<div class="dest" style="left:'+d.x+';top:'+d.y+'"><button title="'+j.nom+'">'+j.emoji+'</button><span class="lieu">'+j.lieu+'</span><span class="dom">'+j.dom+'</span></div>'); 119 node.querySelector('button').onclick=function(){ startMission(k); }; 120 path.appendChild(node); 121 }); 122 wrap.appendChild(map); 123 var side=el('<div class="side-panel"></div>'); 124 var propoKey=keys[0]||'JEU-01'; var propo=JEUX[propoKey]; 125 var mp=el('<div class="mission-propo"><div style="font-weight:700;color:var(--brun)">Mission proposée</div><div class="titre">'+propo.nom+'</div><div class="mission-meta">'+propo.min+' minutes ⢠'+state.grade+' ⢠'+propo.dom+'</div><button class="btn lg" id="startPropo">â¶ Commencer</button></div>'); 126 mp.querySelector('#startPropo').onclick=function(){ startMission(propoKey); }; 127 side.appendChild(mp); 128 side.appendChild(el('<div class="today"><div style="font-weight:700;color:var(--foret)">Aujourdâhui</div><ul style="padding-left:0;margin:8px 0 0"><li>â '+state.missionsDone+' mission(s) terminée(s)</li><li>ð '+state.badges.length+' badge(s) gagné(s)</li></ul></div>')); 129 var carnetBtn=el('<button class="btn soft" style="width:100%">ð Mon carnet & mes badges</button>'); 130 carnetBtn.onclick=function(){ go('carnet'); }; 131 side.appendChild(carnetBtn); 132 wrap.appendChild(side); 133 var av=AVENTURES['AV-01']; 134 var banner=el('<div class="aventure-banner"><span class="av-em">'+av.emoji+'</span><div class="av-txt"><h3>Aventure : '+av.nom+'</h3><small>3 chapitres ⢠avec '+av.perso+'</small></div><button class="btn" id="startAv">Vivre lâaventure â¶</button></div>'); 135 banner.querySelector('#startAv').onclick=function(){ startAventure('AV-01'); }; 136 wrap.appendChild(banner); 137 app.appendChild(wrap); 138 app.appendChild(el('<div class="center-narrow disclaimer">Ãcran carte : 4 destinations maximum, durée et niveau lisibles, aucune donnée dâun autre enfant, ni prix ni achat. Mode contraste et réduction dâanimations disponibles (bouton â¿).</div>')); 139} 140 141var mission = { key:null, helpsUsed:0, errors:0, correct:0, total:0 }; 142state.adventure=null; 143state.account = JSON.parse(store.get('fh_account','null')); 144state.child = null; 145state.matiere = 'Langage'; 146state.grade = 'P1'; 147state.session = null; 148state.compteTab = null; 149state.authIntent = null; 150function launchGame(key){ mission.key=key; mission.helpsUsed=0; mission.errors=0; mission.correct=0; mission.total=0; go('jeu'); } 151function startMission(key){ state.adventure=null; state.session=null; launchGame(key); } 152function startSession(list){ state.adventure=null; state.session={list:list.slice(),idx:0,ok:0,tot:0}; if(list.length){ launchGame(list[0]); } } 153function startAventure(id){ state.adventure={id:id, idx:0}; state.session=null; go('aventureIntro'); } 154function playChapter(){ var ch=AVENTURES[state.adventure.id].chapitres[state.adventure.idx]; launchGame(ch.jeu); } 155 156function screenJeu(){ 157 setRole('enfant'); app.innerHTML='';
158 var stage=el('<div class="stage"></div>'); 159 var fil = state.session ? ('Séance ⢠activité '+(state.session.idx+1)+'/'+state.session.list.length+' : '+JEUX[mission.key].nom) 160 : state.adventure ? (AVENTURES[state.adventure.id].nom+' ⺠'+AVENTURES[state.adventure.id].chapitres[state.adventure.idx].titre) 161 : ('Mission : '+JEUX[mission.key].nom); 162 stage.appendChild(el('<div class="breadcrumb"><button id="bcCarte">Carte</button> ⺠'+fil+'</div>')); 163 var host=el('<div id="gameHost"></div>'); 164 stage.appendChild(host); app.appendChild(stage); 165 stage.querySelector('#bcCarte').onclick=function(){ go('carte'); }; 166 var engines={'JEU-01':playTelegraphe,'JEU-03':playPaniers,'JEU-05':playRecette,'JEU-06':playImagesTemps,'JEU-11':playFloriphone, 167 'MAT-01':function(h){ playQuiz(h,{gen:mathItems}); }, 168 'MAT-02':function(h){ playQuiz(h,{gen:suiteItems}); }, 169 'GEO-01':function(h){ playQuiz(h,{data:GEO}); }, 170 'GEO-02':function(h){ playQuiz(h,{data:GEO2}); }, 171 'HIS-01':function(h){ playSequenceByGrade(h,HIST); }, 172 'HIS-02':function(h){ playQuiz(h,{data:HIST2}); }}; 173 engines[mission.key](host); 174} 175function missionHeader(host, stepLabel, pct){ 176 host.appendChild(el('<div class="progress-row"><span class="step-label">'+stepLabel+'</span><div class="progress-track"><div class="progress-fill" style="width:'+pct+'%"></div></div></div>')); 177} 178function quitRow(host){ 179 var row=el('<div class="toolrow"><button class="quit">âµ Quitter</button><span class="hint-note">Les erreurs déclenchent une aide, jamais une sanction.</span></div>'); 180 row.querySelector('.quit').onclick=function(){ go('carte'); }; 181 host.appendChild(row); 182} 183 184function playTelegraphe(host){ 185 var items = shuffle(TELEGRAPHE).slice(0,4); 186 mission.total=items.length; var idx=0; 187 function step(){ 188 host.innerHTML=''; var it=items[idx]; 189 missionHeader(host,'Ãtape '+(idx+1)+' sur '+items.length, (idx/items.length)*100); 190 var c=el('<div class="consigne"><div class="txt">Ãcoute le mot et choisis le premier son.</div><div class="stim">'+it.emoji+'</div><button class="play-audio" title="Ãcouter">ð</button><div class="choices"></div><div class="feedback" aria-live="polite"></div><div class="helpbar"></div></div>'); 191 c.querySelector('.play-audio').onclick=function(){ speak(it.mot); }; 192 var choicesBox=c.querySelector('.choices'); var fb=c.querySelector('.feedback'); var helpbar=c.querySelector('.helpbar'); 193 var attempts=0; var choix=it.choix.slice(); 194 function drawChoices(){ 195 choicesBox.innerHTML=''; 196 choix.forEach(function(ch){ var b=el('<button class="choice">'+ch+'</button>'); b.onclick=function(){ pick(ch,b); }; choicesBox.appendChild(b); }); 197 } 198 function pick(ch,b){ 199 if(ch===it.rep){ 200 b.classList.add('good'); fb.className='feedback pos'; 201 fb.textContent='Bravo ! Tu as entendu /'+it.rep.toLowerCase()+'/ au début de « '+it.mot+' ».'; speak('Bravo !'); 202 mission.correct++; 203 Array.prototype.forEach.call(choicesBox.children,function(x){x.classList.add('dim');}); 204 setTimeout(next,1300); 205 }else{ 206 b.classList.add('bad'); attempts++; mission.errors++; 207 if(attempts===1){ fb.className='feedback neutre'; fb.textContent='Presque⦠écoute encore une fois, puis réessaie.'; speak(it.mot); } 208 else if(attempts===2){ fb.className='feedback neutre'; fb.textContent='Aide : écoute bien le tout premier son.'; speak(it.mot); offerHelp(); } 209 else { fb.textContent='On enlève un choix pour tâaider.'; var other=it.choix.find(function(x){return x!==it.rep && x!==ch;}); choix=[it.rep, other].filter(Boolean); mission.helpsUsed++; drawChoices(); } 210 setTimeout(function(){ b.classList.remove('bad'); },600); 211 } 212 } 213 function offerHelp(){ 214 if(helpbar.dataset.on) return; helpbar.dataset.on='1'; 215 var h=el('<button class="btn ghost">ð¡ Montre-moi</button>'); 216 h.onclick=function(){ mission.helpsUsed++; Array.prototype.forEach.call(choicesBox.children,function(x){ if(x.textContent===it.rep) x.classList.add('good'); }); fb.textContent='Le premier son est /'+it.rep.toLowerCase()+'/.'; }; 217 helpbar.appendChild(h); 218 } 219 drawChoices(); host.appendChild(c); quitRow(host); 220 speak('Ãcoute le mot et choisis le premier son.'); setTimeout(function(){ speak(it.mot); },1600); 221 } 222 function next(){ idx++; if(idx>=items.length) endMission(); else step(); } 223 step(); 224} 225 226function playPaniers(host){ 227 host.innerHTML=''; missionHeader(host,'Range chaque mot dans sa famille', 20); 228 mission.total=PANIERS.tokens.length; 229 var c=el('<div class="consigne"><div class="txt">'+PANIERS.titre+'</div><button class="play-audio" title="Ãcouter">ð</button><div class="items-tray"></div><div class="baskets"></div>
229<div class="feedback" aria-live="polite"></div></div>'); 230 c.querySelector('.play-audio').onclick=function(){ speak(PANIERS.titre); }; 231 var tray=c.querySelector('.items-tray'); var baskets=c.querySelector('.baskets'); var fb=c.querySelector('.feedback'); 232 var selected=null, placed=0; var tokenEls={}; 233 shuffle(PANIERS.tokens).forEach(function(tk){ 234 var t=el('<button class="token">'+tk.e+' '+tk.t+'</button>'); 235 t.onclick=function(){ selected=tk; Array.prototype.forEach.call(tray.children,function(x){x.classList.remove('selected');}); t.classList.add('selected'); speak(tk.t); }; 236 tokenEls[tk.t]=t; tray.appendChild(t); 237 }); 238 PANIERS.baskets.forEach(function(bk){ 239 var b=el('<div class="basket"><h4>'+bk.nom+'</h4><div class="drop"></div></div>'); 240 b.onclick=function(){ 241 if(!selected){ fb.className='feedback neutre'; fb.textContent='Choisis dâabord un mot en haut.'; return; } 242 var tk=selected; 243 if(tk.cat===bk.id){ 244 b.querySelector('.drop').appendChild(el('<span class="token placed" style="opacity:1">'+tk.e+'</span>')); 245 tokenEls[tk.t].classList.add('placed'); tokenEls[tk.t].classList.remove('selected'); 246 selected=null; placed++; mission.correct++; 247 fb.className='feedback pos'; fb.textContent='Bien rangé !'; 248 if(placed===PANIERS.tokens.length){ setTimeout(endMission,900); } 249 }else{ 250 mission.errors++; fb.className='feedback neutre'; fb.textContent='« '+tk.t+' » nâest pas dans cette famille. Regarde lâautre panier.'; 251 b.classList.add('hi'); setTimeout(function(){ b.classList.remove('hi'); },700); 252 } 253 }; 254 baskets.appendChild(b); 255 }); 256 host.appendChild(c); quitRow(host); speak(PANIERS.titre); 257} 258 259function playSequence(host,data){ 260 host.innerHTML=''; missionHeader(host,'Remets dans le bon ordre', 20); 261 mission.total=data.etapes.length; var correct=data.etapes; 262 var c=el('<div class="consigne"><div class="txt">'+data.titre+'</div><button class="play-audio" title="Ãcouter">ð</button><div class="seq-slots" style="margin:14px 0"></div><div class="seq-source"></div><div class="feedback" aria-live="polite"></div><div class="helpbar"></div></div>'); 263 c.querySelector('.play-audio').onclick=function(){ speak(data.titre); }; 264 var slots=c.querySelector('.seq-slots'); var source=c.querySelector('.seq-source'); var fb=c.querySelector('.feedback'); var helpbar=c.querySelector('.helpbar'); 265 var nextPos=0; 266 correct.forEach(function(_,i){ slots.appendChild(el('<div class="slot"><span class="num">'+(i+1)+'</span></div>')); }); 267 var cards={}; 268 shuffle(correct).forEach(function(stepData){ 269 var card=el('<button class="seq-card"><span class="big">'+stepData.e+'</span>'+stepData.t+'</button>'); 270 card.onclick=function(){ place(stepData,card); }; 271 cards[stepData.t]=card; source.appendChild(card); 272 }); 273 function place(stepData,card){ 274 var expected=correct[nextPos]; 275 if(stepData.t===expected.t){ 276 var slot=slots.children[nextPos]; slot.classList.add('filled'); slot.insertAdjacentHTML('beforeend','<span>'+stepData.e+'</span>'); 277 card.classList.add('used'); mission.correct++; nextPos++; 278 fb.className='feedback pos'; fb.textContent='Oui, câest la bonne étape !'; 279 if(nextPos===correct.length){ setTimeout(endMission,900); } 280 }else{ 281 mission.errors++; fb.className='feedback neutre'; fb.textContent='Ce nâest pas encore le moment. Quelle étape vient en numéro '+(nextPos+1)+' ?'; 282 card.style.borderColor='var(--alerte)'; setTimeout(function(){ card.style.borderColor=''; },700); 283 if(mission.errors>=2 && !helpbar.dataset.on){ 284 helpbar.dataset.on='1'; 285 var h=el('<button class="btn ghost">ð¡ Indice</button>'); 286 h.onclick=function(){ mission.helpsUsed++; cards[correct[nextPos].t].style.boxShadow='var(--focus)'; fb.textContent='Indice : cherche « '+correct[nextPos].t+' ».'; }; 287 helpbar.appendChild(h); 288 } 289 } 290 } 291 host.appendChild(c); quitRow(host); speak(data.titre); 292} 293function playRecette(host){ playSequence(host,RECETTE); } 294function playImagesTemps(host){ playSequence(host,IMAGES_TEMPS); } 295 296function playFloriphone(host){ 297 var items=shuffle(FLORIPHONE).slice(0,4); mission.total=items.length; var idx=0; 298 function step(){ 299 host.innerHTML=''; var it=items[idx]; 300 missionHeader(host,'Ãtape '+(idx+1)+' sur '+items.length,(idx/items.length)*100); 301 var c=el('<div class="consigne"><div class="txt">Ãcoute les deux mots. Sont-ils pareils ou différents ?</div><div class="stim">ð¸</div><button class="play-audio" title="Ãcouter">ð</button><div class="choices"><button class="choice" data-v="same">ð° Pareils</button><button class="choice" data-v="diff">â Différents</button></div>
301<div class="feedback" aria-live="polite"></div></div>'); 302 var fb=c.querySelector('.feedback'); 303 function playPair(){ speak(it.a); setTimeout(function(){ speak(it.b); },900); } 304 c.querySelector('.play-audio').onclick=playPair; 305 Array.prototype.forEach.call(c.querySelectorAll('.choice'),function(b){ 306 b.onclick=function(){ 307 var good=(b.dataset.v==='same')===it.same; 308 if(good){ b.classList.add('good'); fb.className='feedback pos'; 309 fb.textContent=it.same?('Oui, « '+it.a+' » et « '+it.b+' » sont pareils.'):('Oui, « '+it.a+' » et « '+it.b+' » sont différents.'); 310 mission.correct++; speak('Bravo !'); setTimeout(next,1300); 311 }else{ b.classList.add('bad'); mission.errors++; fb.className='feedback neutre'; fb.textContent='Ãcoute encore les deux mots, tout doucement.'; playPair(); setTimeout(function(){ b.classList.remove('bad'); },600); } 312 }; 313 }); 314 host.appendChild(c); quitRow(host); setTimeout(playPair,700); 315 } 316 function next(){ idx++; if(idx>=items.length) endMission(); else step(); } 317 step(); 318} 319 320function endMission(){ 321 if(state.session){ advanceSession(); return; } 322 if(state.adventure){ advanceAventure(); return; } 323 state.missionsDone++; 324 var j=JEUX[mission.key]; var badgeName=j.nom; 325 if(!state.badges.find(function(b){return b.nom===badgeName;})) state.badges.push({nom:badgeName,emoji:j.emoji}); 326 saveBadges(); go('resultat'); 327} 328function advanceSession(){ 329 state.session.ok += mission.correct; state.session.tot += mission.total; 330 state.session.idx++; 331 if(state.session.idx < state.session.list.length){ launchGame(state.session.list[state.session.idx]); } 332 else { state.missionsDone++; saveBadges(); go('sessionFin'); } 333} 334function screenSessionFin(){ 335 setRole('enfant'); app.innerHTML=''; 336 var s=state.session||{list:[],ok:0,tot:0}; 337 var stage=el('<div class="result card"></div>'); 338 stage.innerHTML='<div class="badge-big">ð</div><h2>Séance terminée !</h2><p style="color:var(--texte-doux)">Tu as fait '+s.list.length+' activité(s). Réussite : '+s.ok+'/'+s.tot+'.</p><p>Belle séance ! Chaque essai tâa fait progresser.</p><div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:8px"><button class="btn" id="sfCarnet">ð Mon carnet</button><button class="btn ghost" id="sfCarte">ðºï¸ Retour à la carte</button></div>'; 339 app.appendChild(stage); 340 state.session=null; 341 stage.querySelector('#sfCarnet').onclick=function(){ go('carnet'); }; 342 stage.querySelector('#sfCarte').onclick=function(){ go('carte'); }; 343 speak('Séance terminée, bravo !'); 344} 345function advanceAventure(){ 346 state.adventure.idx++; 347 var av=AVENTURES[state.adventure.id]; 348 if(state.adventure.idx < av.chapitres.length){ go('aventureChapitre'); } 349 else { 350 state.missionsDone++; 351 if(!state.badges.find(function(b){return b.nom===av.nom;})) state.badges.push({nom:av.nom,emoji:av.emoji}); 352 saveBadges(); go('aventureFin'); 353 } 354} 355function chapterDots(){ 356 if(!state.adventure) return ''; 357 var n=AVENTURES[state.adventure.id].chapitres.length, i=state.adventure.idx, s=''; 358 for(var k=0;k<n;k++){ s+='<span class="'+(k<i?'done':(k===i?'on':''))+'"></span>'; } 359 return '<div class="chapter-dots">'+s+'</div>'; 360} 361function screenAventureIntro(){ 362 setRole('enfant'); app.innerHTML=''; 363 var av=AVENTURES[state.adventure.id]; 364 var s=el('<div class="story-scene"><div class="story-perso">Aventure</div><div class="story-emoji">'+av.emoji+'</div><h2>'+av.nom+'</h2><p class="story-text">'+av.intro+'</p><p style="color:var(--dore-clair);font-weight:600">Avec '+av.perso+'</p><div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:6px"><button class="btn lg" id="goChap">Commencer lâaventure â¶</button><button class="btn ghost" id="goCarte" style="color:#fff;border-color:rgba(255,255,255,.5)">Plus tard</button></div></div>'); 365 app.appendChild(s); 366 s.querySelector('#goChap').onclick=function(){ go('aventureChapitre'); }; 367 s.querySelector('#goCarte').onclick=function(){ state.adventure=null; go('carte'); }; 368 speak(av.nom+'. '+av.intro.replace(/ /g,' ').replace(/<[^>]+>/g,'')); 369} 370function screenAventureChapitre(){ 371 setRole('enfant'); app.innerHTML='';
372 var av=AVENTURES[state.adventure.id]; var ch=av.chapitres[state.adventure.idx]; 373 var s=el('<div class="story-scene"><div class="story-perso">'+ch.perso+'</div><div class="story-emoji">'+ch.emoji+'</div><h2>'+ch.titre+'</h2>'+chapterDots()+'<p class="story-text">'+ch.texte+'</p><div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap"><button class="btn lg" id="playChap">Jouer â¶</button><button class="btn ghost" id="qChap" style="color:#fff;border-color:rgba(255,255,255,.5)">Quitter lâaventure</button></div></div>'); 374 app.appendChild(s); 375 s.querySelector('#playChap').onclick=function(){ playChapter(); }; 376 s.querySelector('#qChap').onclick=function(){ state.adventure=null; go('carte'); }; 377 speak(ch.titre.replace(/â/g,' ')+'. '+ch.texte.replace(/ /g,' ').replace(/<[^>]+>/g,'')); 378} 379function screenAventureFin(){ 380 setRole('enfant'); app.innerHTML=''; 381 var av=AVENTURES[state.adventure.id]; 382 var s=el('<div class="story-scene"><div class="story-emoji">ð'+av.emoji+'</div><h2>Aventure terminée !</h2><p class="story-text">'+av.final+'</p><div class="reward-row" style="justify-content:center"><div class="reward" style="background:rgba(255,255,255,.12);color:#fff"><div class="r-emoji">ð</div>Badge dâaventure</div><div class="reward" style="background:rgba(255,255,255,.12);color:#fff"><div class="r-emoji">ð</div>Nouvelle page de carnet</div></div><div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:6px"><button class="btn lg" id="finCarnet">ð Voir mon carnet</button><button class="btn ghost" id="finCarte" style="color:#fff;border-color:rgba(255,255,255,.5)">ðºï¸ Retour à la carte</button></div></div>'); 383 app.appendChild(s); 384 var advId=state.adventure.id; state.adventure=null; 385 s.querySelector('#finCarnet').onclick=function(){ go('carnet'); }; 386 s.querySelector('#finCarte').onclick=function(){ go('carte'); }; 387 speak('Aventure terminée ! '+av.final.replace(/ /g,' ').replace(/<[^>]+>/g,'')); 388} 389function screenResultat(){ 390 setRole('enfant'); app.innerHTML=''; 391 var j=JEUX[mission.key]; 392 var stage=el('<div class="result card"></div>'); 393 stage.innerHTML='<div class="badge-big">'+j.emoji+'</div><h2>Mystère résolu !</h2><p style="color:var(--texte-doux)">Tu as aidé les habitants du vallon avec la mission « '+j.nom+' ».</p><div class="reward-row"><div class="reward"><div class="r-emoji">ð </div>Badge dâenquête</div><div class="reward"><div class="r-emoji">ð</div>Nouvelle page de carnet</div><div class="reward"><div class="r-emoji">ð±</div>Une graine pour le jardin</div></div><p><strong>Ta stratégie :</strong> tu as réussi '+mission.correct+'/'+mission.total+' et demandé '+mission.helpsUsed+' aide(s). Demander de lâaide, câest une bonne idée !</p><div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:8px"><button class="btn" id="toCarnet">ð Voir mon carnet</button><button class="btn ghost" id="toCarte">ðºï¸ Retour à la carte</button></div>'; 394 app.appendChild(stage); 395 stage.querySelector('#toCarnet').onclick=function(){ go('carnet'); }; 396 stage.querySelector('#toCarte').onclick=function(){ go('carte'); }; 397 speak('Bravo, mystère résolu !'); 398} 399 400function screenCarnet(){ 401 setRole('enfant'); app.innerHTML=''; 402 var wrap=el('<div class="center-narrow"></div>'); 403 wrap.appendChild(el('<div class="breadcrumb"><button id="bcC">Carte</button> ⺠Mon carnet</div>')); 404 wrap.appendChild(el('<h2>ð Mon carnet & mes badges</h2>')); 405 var grid=el('<div class="carnet-grid"></div>'); 406 Object.keys(JEUX).forEach(function(k){ 407 var j=JEUX[k]; var owned=state.badges.find(function(b){return b.nom===j.nom;}); 408 grid.appendChild(el('<div class="badge-tile '+(owned?'':'lock')+'"><div class="em">'+(owned?j.emoji:'ð')+'</div><div style="font-weight:700;color:var(--foret);font-size:14px">'+j.nom+'</div><small style="color:var(--texte-doux)">'+(owned?'Badge obtenu':'à découvrir')+'</small></div>')); 409 }); 410 wrap.appendChild(grid); 411 wrap.appendChild(el('<h3 style="margin-top:24px">ð Aventures</h3>')); 412 var agrid=el('<div class="carnet-grid"></div>');
413 Object.keys(AVENTURES).forEach(function(k){ 414 var av=AVENTURES[k]; var owned=state.badges.find(function(b){return b.nom===av.nom;}); 415 agrid.appendChild(el('<div class="badge-tile '+(owned?'':'lock')+'"><div class="em">'+(owned?av.emoji:'ð')+'</div><div style="font-weight:700;color:var(--foret);font-size:14px">'+av.nom+'</div><small style="color:var(--texte-doux)">'+(owned?'Aventure terminée':'à vivre')+'</small></div>')); 416 }); 417 wrap.appendChild(agrid); 418 wrap.appendChild(el('<p style="text-align:center;color:var(--texte-doux);margin-top:18px">ð± Ton jardin pousse doucement : '+state.missionsDone+' graine(s) plantée(s).</p>')); 419 var back=el('<div style="text-align:center;margin-top:10px"><button class="btn ghost">ðºï¸ Retour à la carte</button></div>'); 420 back.querySelector('button').onclick=function(){ go('carte'); }; 421 wrap.appendChild(back); app.appendChild(wrap); 422 wrap.querySelector('#bcC').onclick=function(){ go('carte'); }; 423} 424 425function screenPro(){ 426 setRole('pro'); app.innerHTML=''; 427 var layout=el('<div class="pro-layout"></div>'); 428 var nav=el('<nav class="pro-nav"></nav>'); 429 var tabs=[['dash','ð Tableau de bord'],['creer','â Créer une séance'],['resultats','ð Résultats & notes'],['imprimables','ð¨ï¸ Imprimables']];
430 tabs.forEach(function(pair){ 431 var b=el('<button class="'+(state.proTab===pair[0]?'active':'')+'">'+pair[1]+'</button>'); 432 b.onclick=function(){ state.proTab=pair[0]; screenPro(); }; 433 nav.appendChild(b); 434 }); 435 layout.appendChild(nav); 436 var content=el('<div id="proContent"></div>'); 437 layout.appendChild(content); app.appendChild(layout); 438 ({dash:proDash,creer:proCreer,resultats:proResultats,imprimables:proImprimables}[state.proTab])(content); 439} 440function proDash(host){ 441 host.appendChild(el('<h2>Tableau de bord</h2>')); 442 host.appendChild(el('<div class="kpi-row"><div class="kpi"><div class="num">12</div><div class="lab">enfants actifs</div></div><div class="kpi"><div class="num">4</div><div class="lab">séances cette semaine</div></div><div class="kpi"><div class="num">78 %</div><div class="lab">missions terminées</div></div></div>')); 443 var g=el('<div class="grid g2" style="margin-top:18px"></div>'); 444 var comp=el('<div class="card"><h3>Compétences travaillées</h3></div>'); 445 [['Vocabulaire',72],['Sons & syllabes',64],['Compréhension',58],['Chronologie',40]].forEach(function(p){ 446 comp.appendChild(el('<div class="bar-line"><span class="name">'+p[0]+'</span><div class="bar-track"><div class="bar-fill" style="width:'+p[1]+'%"></div></div></div>')); 447 }); 448 var watch=el('<div class="card"><h3>à regarder</h3><div class="watch"><strong>Groupe Renardeine</strong><br><small>3 profils à revoir</small></div><div class="watch"><strong>Lina A.</strong><br><small>a utilisé 4 aides sur la dernière séance</small></div><div class="watch"><strong>Séance vendredi</strong><br><small>à préparer</small></div></div>'); 449 g.appendChild(comp); g.appendChild(watch); host.appendChild(g);
450 var cta=el('<div style="margin-top:18px"><button class="btn">â Créer une séance</button></div>'); 451 cta.querySelector('button').onclick=function(){ state.proTab='creer'; screenPro(); }; 452 host.appendChild(cta); 453 host.appendChild(el('<div class="disclaimer">Aucun classement entre enfants : la comparaison ne porte que sur la progression propre au profil et aux objectifs fixés.</div>')); 454} 455var PRO_FILTERS=['Tous','Langage','Maths','Géographie','Histoire']; 456var proFilter='Tous'; 457function proCreer(host){ 458 host.innerHTML=''; 459 host.appendChild(el('<h2>Créer une séance</h2>')); 460 var grid=el('<div class="pro-layout" style="grid-template-columns:1fr;gap:18px"></div>'); 461 var left=el('<div></div>'); 462 var filt=el('<div class="filters"></div>'); 463 PRO_FILTERS.forEach(function(f){ 464 var c=el('<button class="chip '+(proFilter===f?'active':'')+'">'+f+'</button>'); 465 c.onclick=function(){ proFilter=f; proCreer(host); }; 466 filt.appendChild(c); 467 }); 468 left.appendChild(el('<p style="color:var(--texte-doux);margin:0 0 8px">Choisissez 2 à 5 activités ⢠filtre par compétence, niveau et durée.</p>')); 469 left.appendChild(filt); 470 Object.keys(JEUX).forEach(function(id){ 471 var j=JEUX[id]; 472 if(proFilter!=='Tous' && j.matiere!==proFilter) return; 473 var inSess=state.proSession.indexOf(id)>=0; 474 var item=el('<div class="lib-item"><div><strong>'+j.emoji+' '+j.nom+'</strong><div class="meta">'+j.min+' min ⢠Niveaux '+j.niv+' ⢠'+j.obj+'</div></div><button class="add-mini '+(inSess?'on':'')+'" title="Ajouter/retirer">'+(inSess?'â':'+')+'</button></div>'); 475 item.querySelector('button').onclick=function(){ 476 var i=state.proSession.indexOf(id); 477 if(i>=0) state.proSession.splice(i,1); 478 else { if(state.proSession.length>=5){ flash('5 activités maximum'); return; } state.proSession.push(id); } 479 proCreer(host); 480 }; 481 left.appendChild(item); 482 }); 483 grid.appendChild(left); 484 var total=state.proSession.reduce(function(s,id){ return s+JEUX[id].min; },0); 485 var side=el('<div class="basket-side"><h3>Votre séance</h3><div class="meta" style="opacity:.8;font-size:13px">'+state.proSession.length+' activité(s) ⢠'+total+' min estimées</div><div id="sessList"></div></div>'); 486 var list=side.querySelector('#sessList'); 487 if(state.proSession.length===0) list.appendChild(el('<p style="opacity:.7;font-size:14px">Ajoutez des activités depuis la bibliothèque.</p>')); 488 state.proSession.forEach(function(id){ list.appendChild(el('<div class="li"><span>'+JEUX[id].emoji+' '+JEUX[id].nom+'</span><span>'+JEUX[id].min+'â²</span></div>')); }); 489 var cont=el('<button class="btn" style="width:100%;margin-top:10px" '+(state.proSession.length<2?'disabled':'')+'>Attribuer / lancer â¶</button>'); 490 cont.onclick=function(){ flash('Séance attribuée â un QR de séance serait généré pour lâenfant.'); }; 491 side.appendChild(cont); 492 var demo=el('<button class="btn soft" style="width:100%;margin-top:8px" '+(state.proSession.length<1?'disabled':'')+'>â¶ Lancer la séance (côté enfant)</button>'); 493 demo.onclick=function(){ setRole('enfant'); startSession(state.proSession); }; 494 side.appendChild(demo); 495 grid.appendChild(side); host.appendChild(grid); 496} 497function proResultats(host){ 498 host.appendChild(el('<h2>Résultats & notes</h2>')); 499 host.appendChild(el('<p style="color:var(--texte-doux);margin-top:0">Profil « Lina A. » â séance du jour. Observations dâusage, jamais un score diagnostique.</p>')); 500 host.appendChild(el('<div class="card"><table class="res-table"><thead><tr><th>Information</th><th>Affichage</th><th>Interprétation interdite</th></tr></thead><tbody><tr><td>Réussite</td><td>7 / 8 items réussis (Niveau 2)</td><td class="interdit">pas de comparaison normative</td></tr><tr><td>Aides</td><td>2 aides visuelles utilisées</td><td class="interdit">pas de conclusion sur une pathologie</td></tr><tr><td>Erreurs</td><td>Confusion son /p/â/b/ (2Ã)</td><td class="interdit">pas dâétiquette définitive</td></tr><tr><td>Durée</td><td>6 min actives, 1 pause</td><td class="interdit">pas de jugement de vitesse</td></tr></tbody></table></div>')); 501 var note=el('<div class="card" style="margin-top:16px"><h3>Note professionnelle (privée)</h3><textarea class="note-box" placeholder="Ex. : Bonne compréhension des consignes. à retravailler : discrimination /p/â/b/. Séance suivante : Floriphone niveau 2."></textarea><div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap"><button class="btn" id="saveNote">ð¾ Enregistrer la note</button><button class="btn ghost" id="exportPdf">ð Exporter le PDF de séance</button></div></div>'); 502 host.appendChild(note); 503 note.querySelector('#saveNote').onclick=function(){ flash('Note privée enregistrée et datée.'); }; 504 note.querySelector('#exportPdf').onclick=function(){ flash('PDF de séance généré (champs choisis, sans donnée superflue).'); }; 505 host.appendChild(el('<div class="disclaimer">Formulation imposée : « activité réussie », « aides utilisées », « type dâerreur observé ». Jamais « diagnostic » ni « retard confirmé ».</div>')); 506} 507function proImprimables(host){ 508 host.appendChild(el('<h2>Imprimables</h2>')); 509 host.appendChild(el('<p style="color:var(--texte-doux);margin-top:0">Supports hors écran, sans donnée personnelle par défaut. Version couleur, version à colorier, fiche adulte séparée.</p>')); 510 var g=el('<div class="grid g3"></div>'); 511 [['ð¨','Le télégraphe des sons','Fiche + version à colorier'],['ð§º','Paniers de Renardeine','Cartes à découper'],['ð¥§','La recette de Châtaigne','Ãtapes à remettre en ordre'],['â³','Images du temps','Histoire à raconter'],['ð¸','
511Le Floriphone','Paires de mots'],['ð','Fiche adulte','Objectifs, aides, critères']].forEach(function(p){ 512 var card=el('<div class="card" style="text-align:center"><div style="font-size:34px">'+p[0]+'</div><div style="font-weight:700;color:var(--foret)">'+p[1]+'</div><small style="color:var(--texte-doux)">'+p[2]+'</small><br><button class="btn soft" style="margin-top:10px">⬠PDF A4</button></div>'); 513 card.querySelector('button').onclick=function(){ flash('PDF « '+p[1]+' » généré (démo).'); }; 514 g.appendChild(card); 515 }); 516 host.appendChild(g); 517}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.