PageSourceSearch

https://lecomptoirdes3abeilles.eu/fh-2-jeux-ecrans.js

js lecomptoirdes3abeilles.eu collected 2026-10-05 21:24:08 UTC 38,425 bytes, 517 lines download raw bytes

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&nbsp;?</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&nbsp;?</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&nbsp;!</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&nbsp;! 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(/&nbsp;/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(/&nbsp;/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&nbsp;!</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(/&nbsp;/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&nbsp;!</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&nbsp;!</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.