/
veranda
/
TypeGame
Обзор
Документация
Войти
/
veranda
/
TypeGame
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
script.js
256 строк
57 KB
veranda
Обновить игры и карту приключений
09 авг 2026, 00:59
09 авг 2026, 00:59
47e89c0
Код
Авторство
О чём код?
(() => { "use strict"; const $ = (s, root=document) => root.querySelector(s); const app = $("#app"), toastEl = $("#toast"); const VK = "https://vk.ru/interactiveenglishzone"; const games = [ {id:"animals",icon:pic("lion","game-icon","лев"),island:"Остров животных",title:"Слияние зверят",genre:"Мерж",image:"assets/animals-3d.webp",rules:["Создавай зверят кнопкой — они появляются в случайных клетках","Кликни по одному зверьку, затем по такому же","В каждом наборе своя цепочка из шести животных"]}, {id:"sea",icon:pic("dolphin","game-icon","дельфин"),island:"Остров моря",title:"Пузырьковый риф",genre:"Шутер-пузыри",image:"assets/sea-3d.webp",rules:["Наводи пушку и запускай пузыри","Собирай рядом три одинаковые картинки","Собери восемь групп и освободи дельфина"]}, {id:"jungle",icon:pic("parrot","game-icon","попугай"),island:"Остров джунглей",title:"Лианы и слова",genre:"Раннер",image:"assets/jungle-3d.webp",rules:["Прочитай вопрос и лети к верному ответу","Уклоняйся от лиан и собирай бонусы","Пройди джунгли и доберись до древнего храма"]}, {id:"dino",icon:pic("dino","game-icon","динозавр"),island:"Остров динозавров",title:"Дино-сканер",genre:"Экспедиция",image:"assets/dino-3d.webp",rules:["Прочитай задание исследователя","Наведи сканер на кристалл с верным ответом","Активируй десять кристаллов и открой древнюю долину"]}, {id:"sport",comingSoon:true,icon:pic("football","game-icon","мяч"),island:"Остров спорта",title:"Аэрохоккей слов",genre:"Аэрохоккей",image:"assets/sport-3d.webp",rules:["Прочитай слово на шайбе","Выбери верные ворота","Забей восемь голов"]}, {id:"pirate",comingSoon:true,icon:pic("pirate-ship","game-icon","пиратский корабль"),island:"Пиратский остров",title:"Пушки против ошибок",genre:"Артиллерия",image:"assets/pirate-3d.webp",rules:["Выбери корабль без ошибки","Настрой угол и силу","Сделай точный выстрел"]} ]; const mergeSetOptions=[{id:"cute",name:"Милые зверята",preview:"7"},{id:"water",name:"Водный мир",preview:"1"},{id:"wild",name:"Дикие животные",preview:"18"},{id:"exotic",name:"Экзотические",preview:"13"}]; const vocab = { animals:[["cat","кот","cat"],["dog","собака","dog"],["fox","лиса","fox"],["lion","лев","lion"],["bear","медведь","bear"],["tiger","тигр","tiger"],["rabbit","кролик","rabbit"],["monkey","обезьяна","monkey"]], sea:[["fish","рыба","fish"],["crab","краб","crab"],["dolphin","дельфин","dolphin"],["shark","акула","shark"],["shell","ракушка","shell"],["anchor","якорь","anchor"],["octopus","осьминог","octopus"],["whale","кит","dolphin"]], jungle:[["leaf","лист","leaf"],["tree","дерево","tree"],["snake","змея","snake"],["parrot","попугай","parrot"],["banana","банан","banana"],["flower","цветок","flower"],["monkey","обезьяна","monkey"],["jungle","джунгли","tree"]], sport:[["ball","мяч","football"],["run","бежать","runner"],["swim","плавать","swimmer"],["goal","гол","goal"],["tennis","теннис","tennis"],["hockey","хоккей","hockey"],["skates","коньки","skates"],["winner","победитель","trophy"]], pirate:[["map","карта","map"],["gold","золото","coin"],["ship","корабль","pirate-ship"],["mast","мачта","pirate-ship"],["pirate","пират","cannon"],["chest","сундук","treasure-chest"],["anchor","якорь","anchor"],["parrot","попугай","parrot"]] }; const dinoQs = [ ["I ___ seven.",["am","is","are"],"am"],["She ___ a cat.",["have got","has got","got"],"has got"], ["They ___ football.",["plays","play","playing"],"play"],["Tom ___ to school.",["go","goes","going"],"goes"], ["We ___ reading now.",["am","is","are"],"are"],["Yesterday I ___ home.",["go","went","gone"],"went"], ["Two ___.",["mouse","mouses","mice"],"mice"],["A lion is ___ than a cat.",["big","bigger","biggest"],"bigger"] ]; const topicGroups = { "Лексика":[["colours","Цвета"],["numbers","Цифры и числа"],["alphabet","Алфавит"],["family","Семья"],["weekdays","Дни недели"],["seasons","Месяцы и времена года"],["weather","Погода"],["clothes","Одежда"],["food","Еда"],["body","Тело человека"],["jobs","Профессии"],["school","Школьные принадлежности"],["home","Дом и мебель"],["transport","Транспорт"],["animals","Животные"]], "Грамматика":[["present_simple","Present Simple"],["present_continuous","Present Continuous"],["plural","Множественное число"],["articles","Артикли a/an/the"],["demonstratives","This / These / That / Those"],["prepositions","Предлоги места"],["possessives","Притяжательные местоимения"],["questions","Вопросительные слова"],["have_got","Have got / Has got"],["can","Can / Can't"],["comparison","Степени сравнения"]] }; const lessonTemplates = { colours:["What colour is the sun?",["yellow","blue","green"],"yellow"], numbers:["What comes after nine?",["ten","eight","six"],"ten"], alphabet:["Which letter comes after B?",["C","D","A"],"C"], family:["My mother's mother is my ___.",["grandmother","sister","aunt"],"grandmother"], weekdays:["What day comes after Monday?",["Tuesday","Sunday","Friday"],"Tuesday"], seasons:["It is snowy in ___.",["winter","summer","spring"],"winter"], weather:["☔ It is ___.",["rainy","sunny","windy"],"rainy"], clothes:["We wear it on the head.",["hat","shoe","sock"],"hat"], food:["Which one is a fruit?",["apple","bread","cheese"],"apple"], body:["We see with our ___.",["eyes","ears","hands"],"eyes"], jobs:["A ___ helps sick people.",["doctor","pilot","cook"],"doctor"], school:["We write with a ___.",["pencil","chair","door"],"pencil"], home:["We sleep in a ___.",["bed","table","lamp"],"bed"], transport:["It flies in the sky.",["plane","train","bike"],"plane"], animals:["Which animal says meow?",["cat","dog","duck"],"cat"], present_simple:["She ___ to school every day.",["goes","go","going"],"goes"], present_continuous:["They ___ playing now.",["are","is","do"],"are"], plural:["One child — two ___.",["children","childs","childes"],"children"], articles:["I see ___ apple.",["an","a","the"],"an"], demonstratives:["___ books here are new.",["These","This","That"],"These"], prepositions:["The cat is ___ the box.",["in","at","from"],"in"], possessives:["Anna has a dog. It is ___ dog.",["her","his","my"],"her"], questions:["___ do you live? — In Moscow.",["Where","Who","When"],"Where"], have_got:["He ___ a bike.",["has got","have got","got has"],"has got"], can:["A bird ___ fly.",["can","can't","is"],"can"], comparison:["An elephant is ___ than a mouse.",["bigger","big","biggest"],"bigger"] }; const TASK_LEVEL=4; const defaults = () => ({version:4,settings:{sound:true,reduced:false},lesson:{active:false,topics:["colours"],frequency:"off"},player:{pearls:0,petSkin:"default",unlockedSkins:["default"],badges:[],mapFlags:Object.fromEntries(games.map(g=>[g.id,false])),treasures:{}},games:Object.fromEntries(games.map(g=>[g.id,{results:{}}])),lessonStats:{correct:{},wrong:{}}}); function load(){try{const d=JSON.parse(localStorage.getItem("igrovoyKvartal_v3"));return merge(defaults(),d||{});}catch{return defaults();}} function merge(a,b){for(const k in b){if(b[k]&&typeof b[k]==="object"&&!Array.isArray(b[k])&&a[k])merge(a[k],b[k]);else a[k]=b[k];}return a;} let state=load(), current=null, lastTask="", dev=new URLSearchParams(location.search).get("dev")==="1"||sessionStorage.getItem("ik_dev")==="1"; delete state.lesson.grade;Object.values(state.games||{}).forEach(game=>delete game.grade); if(dev) sessionStorage.setItem("ik_dev","1"); function save(){localStorage.setItem("igrovoyKvartal_v3",JSON.stringify(state));} function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));} function pic(name,cls="item-art",alt=""){return `<img class="${cls}" src="assets/game-items/${name}.png" alt="${esc(alt)}" draggable="false">`;} function replaceEmojiArt(root){ const art={"🏝️":"map","⚙️":"map","🫧":"pearl","👩🏫":"map","🏆":"trophy","📚":"map","⭐":"coin","⛵":"pirate-ship","🚩":"map","❔":"map","🦜":"parrot","🧰":"treasure-chest","🔊":"cannon","🐢":"turtle","🔒":"treasure-chest","🛠":"cannon"}; const walker=document.createTreeWalker(root,NodeFilter.SHOW_TEXT),nodes=[];while(walker.nextNode())nodes.push(walker.currentNode); nodes.forEach(node=>{let html=esc(node.nodeValue),changed=false;Object.entries(art).forEach(([symbol,name])=>{if(html.includes(symbol)){const safe=name==="turtle"?"dino":name;html=html.split(symbol).join(pic(safe,"inline-art"));changed=true;}});if(changed){const span=document.createElement("span");span.innerHTML=html;node.replaceWith(...span.childNodes);}}); } function toast(s){toastEl.textContent=s;toastEl.classList.add("show");setTimeout(()=>toastEl.classList.remove("show"),1800);} function header(){return `<header class="site-head"><div class="brand-wrap"><a class="logo-link" href="${VK}" target="_blank" rel="noopener" aria-label="Открыть сообщество Игровой квартал ВКонтакте"><img src="logo.png" alt="Логотип Игрового квартала"></a><button class="brand" data-about aria-label="О проекте"><span><b>Игровой квартал</b><small>английский играючи</small></span></button></div><nav><button class="nav-btn" data-go="island">🏝️ Мой остров</button><button class="nav-btn" data-go="settings">⚙️</button><a class="vk" href="${VK}" target="_blank" rel="noopener" aria-label="Сообщество ВКонтакте">VK</a><button class="pearls" data-go="island">🫧 ${state.player.pearls}</button></nav></header>`;} function footer(){return `<footer><span>© Игровой квартал · автор — Вера Ива</span><a href="${VK}" target="_blank" rel="noopener">Сообщество ВКонтакте ↗</a></footer>`;} function screen(html,kind=""){app.innerHTML=`<div class="screen ${kind}">${html}</div>`;replaceEmojiArt(app);app.focus();bindCommon();} function bindCommon(){app.querySelectorAll("[data-go]").forEach(x=>x.onclick=()=>({library:renderLibrary,island:renderIsland,settings:renderSettings,lesson:renderLesson}[x.dataset.go]||renderLibrary)());app.querySelectorAll("[data-about]").forEach(x=>x.onclick=about);} function about(){modal(`<div class="about"><a class="about-logo" href="${VK}" target="_blank" rel="noopener"><img src="logo.png" alt="Игровой квартал — открыть сообщество ВКонтакте"></a><h2>Игровой квартал</h2><p>Добрые игры помогают детям 7–11 лет тренировать английские слова и грамматику без страха ошибки.</p><p><b>Автор проекта — Вера Ива.</b></p><a class="btn primary" href="${VK}" target="_blank" rel="noopener">Открыть сообщество ВКонтакте ↗</a></div>`);} function modal(html,cls=""){const d=document.createElement("div");d.className="modal-bg";d.id="modal";d.innerHTML=`<section class="modal ${cls}" role="dialog" aria-modal="true"><button class="modal-x" aria-label="Закрыть">×</button>${html}</section>`;document.body.append(d);$(".modal-x",d).onclick=()=>d.remove();d.onclick=e=>{if(e.target===d)d.remove();};return d;} function renderLibrary(filter="all"){ const active=state.lesson.active?`<div class="lesson-banner">📚 Урок активен: ${state.lesson.topics.map(topicName).join(", ")} <button data-go="lesson">Изменить</button></div>`:""; screen(`${header()}<section class="hero"><div><span class="eyebrow">6 приключений · английский язык</span><h1>Выбери остров и играй!</h1><p>Слова, грамматика и море открытий для юных исследователей.</p></div><img src="assets/parrot-mascot-3d.webp" alt="Попугай-помощник"></section>${active}<div class="library-tools"><button class="btn teacher" data-go="lesson">👩🏫 Настроить урок</button></div><section class="game-grid">${games.map(g=>{const r=state.games[g.id].results;const best=Math.max(0,...Object.values(r).map(x=>x.score||0));return `<article class="game-card ${g.comingSoon?"coming-soon":""}"><div class="cover"><img src="${g.image}" alt=""><span>${g.icon}</span>${g.comingSoon?'<div class="coming-badge">Скоро будет готово</div>':""}</div><div class="card-copy"><span class="genre">${g.genre}</span><h2>${g.title}</h2><p>${g.island}</p><div class="score">🏆 Лучший результат: <b>${best}</b></div>${g.comingSoon?'<div class="soon-button">Скоро будет готово</div>':`<button class="btn primary play" data-game="${g.id}">Играть →</button>`}</div></article>`}).join("")}</section>${dev?`<button class="dev-fab" data-dev>DEV</button>`:""}${footer()}`,"library"); app.querySelectorAll("[data-game]").forEach(b=>b.onclick=()=>renderLaunch(b.dataset.game));if(dev)$("[data-dev]").onclick=renderDev; } function topicName(id){for(const a of Object.values(topicGroups)){const x=a.find(t=>t[0]===id);if(x)return x[1];}return id;} function renderLaunch(id){if(id==="animals")return renderMergeLaunch();if(id==="jungle")return renderJungleLaunch();const g=games.find(x=>x.id===id);screen(`${header()}<section class="launch"><img src="${g.image}" alt="${g.island}"><div><button class="back" data-go="library">← Все игры</button><span class="genre">${g.genre}</span><h1>${g.icon} ${g.title}</h1><ul>${g.rules.map(x=>`<li>✓ ${x}</li>`).join("")}</ul><button class="btn primary big" id="start">Начать игру</button></div></section>${footer()}`);$("#start").onclick=()=>startGame(id);} function renderJungleLaunch(){const g=games.find(x=>x.id==="jungle"),topic=state.lesson.topics?.[0]||"animals";screen(`${header()}<section class="launch jungle-launch"><img src="${g.image}" alt="${g.island}"><div><button class="back" data-go="library">← Все игры</button><span class="genre">Путешествие-раннер</span><h1>${g.icon} ${g.title}</h1><p>Помоги попугаю долететь через джунгли до древнего храма.</p><div class="runner-mode-picker"><button type="button" class="runner-mode active" data-runner-mode="quiz"><b>QUIZ FLIGHT</b><small>Выбирай правильные ответы</small></button><button type="button" class="runner-mode" data-runner-mode="word"><b>SPELLING FLIGHT</b><small>Смотри на слово и лови пропущенные буквы</small></button></div><label class="label" for="jungleTopic">Учебная тема</label><select id="jungleTopic" class="launch-select">${Object.values(topicGroups).flat().map(([id,name])=>`<option value="${id}" ${id===topic?"selected":""}>${name}</option>`).join("")}</select><ul><li>✓ Удерживай, чтобы подняться; отпусти, чтобы снизиться</li><li>✓ Пролетай через правильные ответы</li><li>✓ Облетай лианы и доберись до храма</li></ul><button class="btn primary big" id="start">Начать путешествие</button></div></section>${footer()}`);let mode="quiz";app.querySelectorAll("[data-runner-mode]").forEach(b=>b.onclick=()=>{mode=b.dataset.runnerMode;app.querySelectorAll("[data-runner-mode]").forEach(x=>x.classList.toggle("active",x===b));$("#jungleTopic").disabled=mode==="word";});$("#start").onclick=()=>{state.lesson={...state.lesson,active:true,topics:[$("#jungleTopic").value]};save();startGame("jungle",mode);};} function renderMergeLaunch(){const g=games[0],selectedTopic=state.lesson.topics?.[0]||"colours",selectedSet=state.games.animals.mergeSet||"cute";screen(`${header()}<section class="launch merge-launch"><img src="${g.image}" alt="${g.island}"><div><button class="back" data-go="library">← Все игры</button><span class="genre">${g.genre}</span><h1>${g.icon} ${g.title}</h1><p>Выбери учебную тему и набор животных.</p><label class="label" for="mergeTopic">Учебная тема</label><select id="mergeTopic" class="launch-select">${Object.values(topicGroups).flat().map(([topic,name])=>`<option value="${topic}" ${topic===selectedTopic?"selected":""}>${name}</option>`).join("")}</select><label class="label">Набор животных</label><div class="merge-set-picker">${mergeSetOptions.map(set=>`<button type="button" class="merge-set ${set.id===selectedSet?"active":""}" data-merge-set="${set.id}"><img src="assets/new-animals/${set.preview}.png" alt=""><span>${set.name}</span></button>`).join("")}</div><button class="btn primary big" id="start">Начать игру</button></div></section>${footer()}`);let mergeSet=selectedSet;app.querySelectorAll("[data-merge-set]").forEach(b=>b.onclick=()=>{mergeSet=b.dataset.mergeSet;app.querySelectorAll("[data-merge-set]").forEach(x=>x.classList.toggle("active",x===b));});$("#start").onclick=()=>{state.lesson={...state.lesson,active:true,topics:[$("#mergeTopic").value],frequency:"frequent"};state.games.animals.mergeSet=mergeSet;save();startGame("animals");};} function renderLesson(){return renderLessonUnified();} function startGame(id,runnerMode="quiz"){current={id,runnerMode,mergeSet:state.games.animals.mergeSet||"cute",step:0,score:0,correct:0,wrong:0,lessonCorrect:0,lessonWrong:0,target:null,usedTaskSignatures:new Set(),recentTemplateIds:[]};renderGame();} function gameTotal(id){return id==="jungle"||id==="dino"?10:8;} function gameHead(g){const merge=g.id==="animals",total=gameTotal(g.id);return `<div class="ingame-head"><button class="nav-btn" id="quit">← Выйти</button><div><b>${g.icon} ${g.title}</b><span>${merge?"Цель: собери последнее животное":g.id==="jungle"?`Полёт к храму · ${current.step}/${total}`:`Задание ${Math.min(current.step+1,total)} из ${total}`}</span></div><div class="game-score">⭐ ${current.score}</div></div><div class="round-progress"><i style="width:${merge?0:current.step/total*100}%"></i></div>`;} function renderGame(){ if(current.id!=="animals"&¤t.id!=="jungle"&¤t.step>=gameTotal(current.id))return finishGame(); const g=games.find(x=>x.id===current.id); screen(`${gameHead(g)}<section class="game-stage canvas-stage ${g.id}"><div id="mechanic"></div></section>`,`playing`); $("#quit").onclick=()=>{if(confirm("Выйти из игры? Текущий раунд не сохранится.")){GameArcade.destroy();renderLibrary();}}; GameArcade.mount($("#mechanic"),g.id,{ mergeSet:current.mergeSet, runnerMode:current.runnerMode, soundEnabled:state.settings.sound, getTask:()=>makeTask(pickGrammar(state.lesson.topics?.length?state.lesson.topics:["present_simple"])), onSuccess:label=>{current.step++;current.correct++;current.score+=10;state.player.pearls+=2;save();updateGameHud();const interval={frequent:1,normal:2,rare:3}[state.lesson.frequency]||Infinity,integrated=["jungle","dino","sport","pirate"].includes(g.id),lessonDue=g.id==="animals"||(!integrated&&state.lesson.active&¤t.step%interval===0);current.lessonPending=lessonDue;if(lessonDue){GameArcade.pause();setTimeout(()=>showLesson(),500);}else if(g.id!=="animals"&¤t.step>=gameTotal(g.id))setTimeout(finishGame,900);}, onProgress:(rank,targetRank,target)=>{current.mergeRank=Math.max(current.mergeRank||0,rank);current.mergeTargetRank=targetRank;current.mergeTarget=target;updateGameHud();}, onRunnerAnswer:(good,runner)=>{current.step=runner.done;current.correct+=good?1:0;current.wrong+=good?0:1;current.score=runner.coins+current.correct*10;current.lessonCorrect=current.correct;current.lessonWrong=current.wrong;updateGameHud();}, onComplete:()=>{current.completeAfterLesson=!!current.lessonPending;if(!current.completeAfterLesson)setTimeout(finishGame,700);}, onError:()=>{current.wrong++;updateGameHud();} }); } function updateGameHud(){const step=$(".ingame-head span"),score=$(".game-score"),bar=$(".round-progress i"),merge=current?.id==="animals",runner=current?.id==="jungle",target=current?.mergeTargetRank||5,total=gameTotal(current?.id);if(step)step.textContent=merge?`До цели: ${Math.max(0,target-(current.mergeRank||0))} этапов`:runner?`Полёт к храму · ${current.step}/${total}`:`Задание ${Math.min(current.step+1,total)} из ${total}`;if(score)score.innerHTML=`${pic("coin","inline-art")} ${current.score}`;if(bar)bar.style.width=`${merge?(current.mergeRank||0)/target*100:current.step/total*100}%`;} function wordFor(id){const list=vocab[id]||vocab.animals;return list[current.step%list.length];} function shuffled(a){return [...a].sort(()=>Math.random()-.5);} function renderMechanic(){const box=$("#mechanic"),id=current.id,w=wordFor(id); if(id==="animals"){ const levels=[ {word:"cat",translation:"котёнок",art:"cat"}, {word:"fox",translation:"лиса",art:"fox"}, {word:"lion",translation:"лев",art:"lion"}, {word:"tiger",translation:"тигр",art:"tiger"}, {word:"legend",translation:"легендарный зверь",art:"bear"} ]; if(!current.merge){ let uid=0; current.merge={uid,selected:null,board:shuffled([0,0,0,0,1,1,1,1,2,2]).map(rank=>({uid:++uid,rank}))}; current.merge.uid=uid; } const m=current.merge,counts=m.board.reduce((a,x)=>(a[x.rank]=(a[x.rank]||0)+1,a),{}); let goalRank=+Object.keys(counts).find(rank=>counts[rank]>1&&+rank<levels.length-1); if(!Number.isFinite(goalRank)){ goalRank=0;while(m.board.length<16&&m.board.filter(x=>x.rank===0).length<2)m.board.push({uid:++m.uid,rank:0}); } const goal=levels[goalRank],reward=levels[goalRank+1]; box.innerHTML=`<div class="merge-goal"><span class="goal-label">Цель слияния</span><div>${pic(goal.art,"goal-art",goal.translation)}<b>${goal.word}</b></div><i>+</i><div>${pic(goal.art,"goal-art",goal.translation)}<b>${goal.word}</b></div><i>=</i><div class="reward">${pic(reward.art,"goal-art",reward.translation)}<b>${reward.word}</b></div></div><p class="merge-help">Выбери на поле две одинаковые карточки. Они превратятся в нового зверя.</p><div class="merge-board">${Array.from({length:16},(_,i)=>{const item=m.board[i];if(!item)return `<span class="merge-cell empty" aria-hidden="true"></span>`;const level=levels[item.rank];return `<button class="merge-cell ${m.selected===item.uid?"selected":""}" data-merge="${item.uid}" aria-label="${level.word}">${pic(level.art,"merge-art",level.translation)}<b>${level.word}</b><small>${level.translation}</small><span class="rank">${item.rank+1}</span></button>`;}).join("")}</div>`; box.querySelectorAll("[data-merge]").forEach(btn=>btn.onclick=()=>{ const uid=+btn.dataset.merge,item=m.board.find(x=>x.uid===uid); if(m.selected===null){m.selected=uid;renderMechanic();return;} if(m.selected===uid){m.selected=null;renderMechanic();return;} const first=m.board.find(x=>x.uid===m.selected); if(first&&first.rank===item.rank&&item.rank<levels.length-1){ const firstIndex=m.board.indexOf(first),itemIndex=m.board.indexOf(item); m.board.splice(Math.max(firstIndex,itemIndex),1);m.board.splice(Math.min(firstIndex,itemIndex),1); m.board.push({uid:++m.uid,rank:item.rank+1}); while(m.board.length<Math.min(16,11+current.step)&&m.board.length<16)m.board.push({uid:++m.uid,rank:0}); m.selected=null;current.step++;current.correct++;current.score+=15;state.player.pearls+=2;save();toast(`${levels[item.rank+1].word}: новое животное! +2 жемчужины`); const n={frequent:1,normal:2,rare:3}[state.lesson.frequency]||999; if(state.lesson.active&¤t.step%n===0)setTimeout(showLesson,450);else setTimeout(renderGame,500); }else{m.selected=uid;current.wrong++;toast("Можно соединить только одинаковых зверят");renderMechanic();} }); box.querySelectorAll("[data-merge]").forEach(btn=>{ btn.draggable=true; btn.ondragstart=e=>{m.selected=+btn.dataset.merge;e.dataTransfer.setData("text/plain",btn.dataset.merge);btn.classList.add("dragging");}; btn.ondragover=e=>e.preventDefault(); btn.ondrop=e=>{e.preventDefault();btn.click();}; }); } if(id==="sea"){const opts=shuffled([w,...shuffled(vocab.sea.filter(x=>x!==w)).slice(0,4)]);box.innerHTML=`<h1>Пушка заряжена словом <em>${w[0]}</em></h1><div class="bubble-field">${opts.map(x=>`<button data-answer="${x[1]}">${pic(x[2],"bubble-art",x[1])}<small>${x[1]}</small></button>`).join("")}</div><div class="cannon-graphic">${pic("pearl","projectile-art","снаряд")}${pic("cannon","cannon-art","пушка")}</div>`;bindAnswers(w[1]);} if(id==="jungle"){current.target=current.target||{word:w[0],pos:0};const t=current.target,letter=t.word[t.pos],opts=shuffled([letter,String.fromCharCode(97+Math.floor(Math.random()*26)),String.fromCharCode(97+Math.floor(Math.random()*26))]);box.innerHTML=`<h1>${pic(w[2],"prompt-art",w[1])} Собери слово: <em>${t.word}</em></h1><div class="built">${t.word.split("").map((x,i)=>`<span class="${i<t.pos?"done":""}">${i<t.pos?x:"_"}</span>`).join("")}</div><div class="lanes">${opts.map(x=>`<button data-letter="${x}"><span>${x.toUpperCase()}</span><small>лиана</small></button>`).join("")}</div><div class="runner-graphic">${pic("parrot","runner-art","попугай")}<i></i>${pic("treasure-chest","finish-art","финиш")}</div>`;box.querySelectorAll("[data-letter]").forEach(b=>b.onclick=()=>{if(b.dataset.letter===letter){t.pos++;if(t.pos===t.word.length){current.target=null;action(true,"Слово собрано!");}else renderMechanic();}else{current.wrong++;toast("Попробуй другую букву");}});} if(id==="dino"){const q=dinoQs[current.step%dinoQs.length];box.innerHTML=`<h1>${q[0]}</h1><p>Выстрели в яйцо с верным ответом.</p><div class="eggs">${shuffled(q[1]).map(x=>`<button data-answer="${x}"><span class="egg-art"></span><small>${x}</small></button>`).join("")}</div><div class="blaster-graphic">${pic("dino","dino-art","динозавр")}${pic("cannon","mini-cannon-art","бластер")}</div>`;bindAnswers(q[2]);} if(id==="sport"){const wrong=shuffled(vocab.sport.filter(x=>x!==w))[0];box.innerHTML=`<h1>Шайба: <em>${w[0]}</em> ${pic(w[2],"prompt-art",w[1])}</h1><div class="rink"><button class="goal left" data-answer="${w[1]}">${pic("goal","goal-net",w[1])}<b>${w[1]}</b></button><div class="puck">${pic("hockey","hockey-art","хоккей")}</div><button class="goal right" data-answer="${wrong[1]}"><b>${wrong[1]}</b>${pic("goal","goal-net",wrong[1])}</button></div>`;bindAnswers(w[1]);} if(id==="pirate"){const wrong1=w[0].replace(/[aeiou]/i,"e"),wrong2=w[0].slice(0,-1)+"t";box.innerHTML=`<h1>Потопи корабль со словом «${w[1]}» ${pic(w[2],"prompt-art",w[1])}</h1><div class="ships">${shuffled([w[0],wrong1,wrong2]).map(x=>`<button data-answer="${x}">${pic("pirate-ship","ship-art","корабль")}<small>${x}</small></button>`).join("")}</div><div class="aim"><label>Угол <input id="angle" type="range" min="15" max="75" value="45"></label><label>Сила <input id="power" type="range" min="1" max="10" value="7"></label></div><div class="pirate-cannon">${pic("cannon","cannon-art","пушка")}</div>`;bindAnswers(w[0]);} } function bindAnswers(answer){app.querySelectorAll("[data-answer]").forEach(b=>b.onclick=()=>{if(b.dataset.answer===answer)action(true,"Верно!");else{current.wrong++;b.classList.add("wrong");b.disabled=true;toast("Почти! Попробуй ещё");}});} function action(ok,msg){current.step++;current.correct++;current.score+=10;state.player.pearls+=2;save();toast(msg+" +2 🫧");const n={frequent:1,normal:2,rare:3}[state.lesson.frequency]||999;if(state.lesson.active&¤t.step%n===0)setTimeout(showLesson,350);else setTimeout(renderGame,500);} function makeTask(topic){const t=lessonTemplates[topic]||lessonTemplates.colours;return{topic,type:"choice",prompt:t[0],options:shuffled(t[1]),answer:t[2]};} function showLesson(forcedTopic){let topics=forcedTopic?[forcedTopic]:state.lesson.topics;let topic=topics[Math.floor(Math.random()*topics.length)]||"colours";if(topic===lastTask&&topics.length>1)topic=topics[(topics.indexOf(topic)+1)%topics.length];lastTask=topic;const task=makeTask(topic),d=modal(`<div class="lesson-mark">📚 Учебная остановка · ${topicName(topic)}</div><h2>${task.prompt}</h2><div class="task-area ${task.type}">${task.options.map(x=>`<button class="task-option" data-task="${esc(x)}">${esc(x)}</button>`).join("")}</div><p class="task-hint">Выбери ответ. Ошибаться можно!</p>`,`lesson-modal`);let order=[];d.querySelectorAll("[data-task]").forEach(b=>b.onclick=()=>{if(task.type==="wordorder"){order.push(b.dataset.task);b.disabled=true;$(".task-hint",d).textContent=order.join(" ");if(order.length<task.options.length)return;if(order.join(" ")!==task.answer){order=[];d.querySelectorAll("[data-task]").forEach(x=>x.disabled=false);wrong();return;}right();}else if(b.dataset.task===task.answer)right();else wrong(b);});function wrong(b){current&&(current.lessonWrong++);state.lessonStats.wrong[topic]=(state.lessonStats.wrong[topic]||0)+1;save();if(b){b.classList.add("wrong");b.disabled=true;}$(".task-hint",d).textContent="Подумай ещё — правильный ответ рядом 🌱";}function right(){current&&(current.lessonCorrect++);state.lessonStats.correct[topic]=(state.lessonStats.correct[topic]||0)+1;state.player.pearls+=5;save();d.querySelector(".modal").classList.add("success");$(".task-hint",d).textContent="Отлично! +5 жемчужин 🫧";setTimeout(()=>{d.remove();if(current)renderGame();},850);}} function finishGame(){GameArcade.destroy();const g=games.find(x=>x.id===current.id),runnerMode=current.runnerMode,jungle=g.id==="jungle",accuracy=Math.round(current.correct/(current.correct+current.wrong||1)*100),stars=accuracy>=90?3:accuracy>=70?2:1,bonus=stars*3,resultKey="default";state.player.pearls+=bonus;state.games[g.id].results[resultKey]={score:Math.max(current.score,state.games[g.id].results[resultKey]?.score||0),accuracy};if(!state.player.mapFlags[g.id]){state.player.mapFlags[g.id]=true;state.player.treasures[g.id]={date:new Date().toLocaleDateString("ru-RU")};state.player.badges.push(g.id);}save();screen(`${header()}<section class="result"><div class="result-icon">${g.icon}</div><h1>${jungle?"ХРАМ ДОСТИГНУТ!":"Экспедиция завершена!"}</h1><div class="stars">${"★".repeat(stars)}${"☆".repeat(3-stars)}</div><div class="stats"><div><b>${current.score}</b><span>игровых очков</span></div><div><b>${accuracy}%</b><span>игровая точность</span></div><div><b>${current.lessonCorrect}</b><span>${jungle?"верных ответов":"языковых ответов"}</span></div><div><b>+${bonus}</b><span>жемчужин</span></div></div><p>${jungle?"Попугай добрался до древнего храма!":`Открыта новая часть коллекции «${g.island}».`}</p><div class="actions"><button class="btn primary" id="again">${jungle?"Пройти ещё раз":"Новая экспедиция"}</button><button class="btn teacher" data-go="island">Мой остров</button><button class="btn ghost" data-go="library">Все игры</button></div></section>${footer()}`);$("#again").onclick=()=>startGame(g.id,runnerMode);current=null;} function renderIsland(){const flags=Object.values(state.player.mapFlags).filter(Boolean).length,correct=Object.values(state.lessonStats.correct).reduce((a,b)=>a+b,0);screen(`${header()}<section class="panel island-panel"><button class="back" data-go="library">← К играм</button><div class="map-title"><div><span class="eyebrow">Карта приключений</span><h1>Мой волшебный архипелаг</h1><p>Проходи игры, зажигай маршрут и собирай сокровища каждого острова.</p></div><div class="map-total"><b>${flags}/6</b><span>островов исследовано</span></div></div><div class="meta-map premium-map">${games.map((g,i)=>{const complete=!!state.player.mapFlags[g.id],soon=!!g.comingSoon;return `<button type="button" class="map-stop m${i} ${complete?"complete":""} ${soon?"unavailable":""}" ${soon?'disabled aria-disabled="true"':`data-map-game="${g.id}"`}><span class="map-stop-icon">${g.icon}</span><span class="map-stop-copy"><b>${g.title}</b><small>${soon?"Скоро":complete?"Исследовано ✓":"Начать экспедицию"}</small></span></button>`}).join("")}<div class="map-ship" aria-hidden="true">${pic("pirate-ship","map-ship-art")}</div></div><div class="stats island-stats"><div><b>${state.player.pearls}</b><span>жемчужин</span></div><div><b>${flags}/6</b><span>островов</span></div><div><b>${Object.keys(state.player.treasures).length}</b><span>сокровищ</span></div><div><b>${correct}</b><span>верных заданий</span></div></div><div class="treasure-head"><div><span class="eyebrow">Коллекция</span><h2>Сокровища экспедиций</h2></div><p>Каждая завершённая игра открывает новую реликвию.</p></div><div class="treasures premium-treasures">${games.map(g=>{const found=!!state.player.treasures[g.id];return `<div class="${found?"found":""}"><span>${found?g.icon:pic("treasure-chest","treasure-lock")}</span><b>${g.title}</b><small>${found?"Сокровище найдено":"Пока не найдено"}</small></div>`}).join("")}</div></section>${footer()}`,"island-screen");app.querySelectorAll("[data-map-game]").forEach(b=>b.onclick=()=>renderLaunch(b.dataset.mapGame));} function renderSettings(){screen(`${header()}<section class="panel narrow"><button class="back" data-go="library">← Назад</button><h1>⚙️ Настройки</h1><label class="setting"><span>🔊 Звуковые эффекты</span><input type="checkbox" data-set="sound" ${state.settings.sound?"checked":""}></label><label class="setting"><span>🐢 Меньше анимации</span><input type="checkbox" data-set="reduced" ${state.settings.reduced?"checked":""}></label><button class="btn danger" id="reset">Сбросить весь прогресс</button><p class="privacy">🔒 Данные остаются только в этом браузере. Мы не собираем персональные данные.</p></section>${footer()}`);app.querySelectorAll("[data-set]").forEach(x=>x.onchange=()=>{state.settings[x.dataset.set]=x.checked;save();document.body.classList.toggle("reduced",state.settings.reduced);});$("#reset").onclick=()=>{if(confirm("Удалить весь прогресс и настройки?")){state=defaults();save();renderLibrary();}};} function renderDev(){screen(`${header()}<section class="panel"><button class="back" data-go="library">← Назад</button><h1>🛠 Панель автора</h1><div class="dev-grid"><div><h2>Быстрый запуск</h2>${games.map(g=>`<button class="chip" data-quick="${g.id}">${g.icon} ${g.title}</button>`).join("")}</div><div><h2>Генератор заданий</h2><select id="devTopic">${Object.values(topicGroups).flat().map(([id,n])=>`<option value="${id}">${n}</option>`).join("")}</select><button class="btn teacher" id="forceTask">Показать задание</button><button class="btn primary" id="generate30">Generate 30 tasks</button><h2>Прогресс</h2><button class="btn teacher" id="pearls">+100 жемчужин</button><button class="btn teacher" id="unlock">Открыть всё</button></div></div><h2>Проверка генератора</h2><textarea id="taskAudit" rows="16" readonly></textarea><h2>Состояние localStorage</h2><textarea id="json" rows="12">${esc(JSON.stringify(state,null,2))}</textarea><div class="actions"><button class="btn ghost" id="copy">Копировать</button><button class="btn primary" id="import">Импортировать JSON</button></div><h2>Контент</h2><p>Игры: 6 · темы: 26 · единый уровень A1/A1+</p></section>${footer()}`);app.querySelectorAll("[data-quick]").forEach(b=>b.onclick=()=>startGame(b.dataset.quick));$("#forceTask").onclick=()=>showLesson($("#devTopic").value);$("#generate30").onclick=()=>{const history={usedTaskSignatures:new Set(),recentTemplateIds:[]},topic=$("#devTopic").value,tasks=Array.from({length:30},()=>generateTask(topic,history));$("#taskAudit").value=tasks.map((t,i)=>`${i+1}. [${t.templateId}] ${t.prompt}\n ${t.options.join(" / ")} → ${t.answer}`).join("\n\n");};$("#pearls").onclick=()=>{state.player.pearls+=100;save();renderDev();};$("#unlock").onclick=()=>{games.forEach(g=>{state.player.mapFlags[g.id]=true;state.player.treasures[g.id]={date:"DEV"};});save();renderDev();};$("#copy").onclick=()=>navigator.clipboard?.writeText($("#json").value).then(()=>toast("Скопировано"));$("#import").onclick=()=>{try{state=merge(defaults(),JSON.parse($("#json").value));delete state.lesson.grade;Object.values(state.games||{}).forEach(game=>delete game.grade);save();renderDev();}catch{toast("Ошибка в JSON");}};} // Procedural Spotlight-oriented task factory: controlled combinations, not a short question list. const grammarLexicon={ subjects:[{s:"I",be:"am",have:"have got",v3:false},{s:"You",be:"are",have:"have got",v3:false},{s:"We",be:"are",have:"have got",v3:false},{s:"They",be:"are",have:"have got",v3:false},{s:"Tom",be:"is",have:"has got",v3:true},{s:"Kate",be:"is",have:"has got",v3:true},{s:"My brother",be:"is",have:"has got",v3:true},{s:"The children",be:"are",have:"have got",v3:false}], verbs:[{base:"play",third:"plays",ing:"playing",objects:["football","tennis","games"]},{base:"read",third:"reads",ing:"reading",objects:["books","a story","comics"]},{base:"watch",third:"watches",ing:"watching",objects:["TV","a film","cartoons"]},{base:"eat",third:"eats",ing:"eating",objects:["an apple","lunch","breakfast"]},{base:"go",third:"goes",ing:"going",objects:["to school","home","to the park"]},{base:"draw",third:"draws",ing:"drawing",objects:["a picture","an animal","a house"]}], times:["every day","on Mondays","after school","in the morning","at weekends"], things:["a red bike","a little dog","two sisters","a new book","a blue school bag","a big bedroom"], places:["in the box","on the table","under the chair","next to the bed"] }; const recentGenerated=[]; function proceduralTask(topic,level=TASK_LEVEL,opts={}){ let task,tries=0; do{ const S=pickGrammar(grammarLexicon.subjects),V=pickGrammar(grammarLexicon.verbs),O=pickGrammar(V.objects),T=pickGrammar(grammarLexicon.times),mode=opts.sentenceMode||state.lesson.sentenceMode||"mixed"; if(topic==="present_simple"){ if(level===1){task={topic,type:"gapfill",prompt:`${S.s} ___ happy.`,options:shuffled([S.be,S.be==="is"?"are":"is","do"]),answer:S.be};} else{ const levelModes=level===2?["affirmative"]:level===3?["affirmative","negative"]:["affirmative","negative","question"],chosen=mode==="mixed"?pickGrammar(levelModes):mode; if(chosen==="negative"){const aux=S.v3?"doesn't":"don't";task={topic,type:"gapfill",prompt:`${S.s} ___ ${V.base} ${O} ${T}.`,options:shuffled([aux,S.v3?"don't":"doesn't","isn't"]),answer:aux};} else if(chosen==="question"){const aux=S.v3?"Does":"Do";task={topic,type:"gapfill",prompt:`___ ${S.s.toLowerCase()} ${V.base} ${O} ${T}?`,options:shuffled([aux,aux==="Do"?"Does":"Do","Is"]),answer:aux};} else{const answer=S.v3?V.third:V.base,wrong=[S.v3?V.base:V.third,V.ing];task={topic,type:"gapfill",prompt:`${S.s} ___ ${O} ${T}.`,options:shuffled([answer,...wrong]),answer};} } }else if(topic==="present_continuous"){ task={topic,type:"gapfill",prompt:`${S.s} ___ ${V.ing} ${O} now.`,options:shuffled([S.be,"do","does"]),answer:S.be}; }else if(topic==="have_got"){ const thing=pickGrammar(grammarLexicon.things);task={topic,type:"gapfill",prompt:`${S.s} ___ ${thing}.`,options:shuffled([S.have,S.v3?"have got":"has got","is got"]),answer:S.have}; }else if(topic==="can"){ const fact=pickGrammar([["A bird","fly",true],["A fish","swim",true],["A monkey","climb",true],["A cat","jump",true],["A fish","fly",false],["A bird","swim under water",false]]),answer=fact[2]?"can":"can't";task={topic,type:"gapfill",prompt:`${fact[0]} ___ ${fact[1]}.`,options:shuffled([answer,answer==="can"?"can't":"can","is"]),answer}; }else if(topic==="articles"){ const noun=pickGrammar([["apple","an"],["orange","an"],["book","a"],["pencil","a"],["elephant","an"]]);task={topic,type:"gapfill",prompt:`I can see ___ ${noun[0]}.`,options:shuffled([noun[1],noun[1]==="a"?"an":"a","are"]),answer:noun[1]}; }else if(topic==="prepositions"){ const place=pickGrammar(grammarLexicon.places),prep=place.split(" ")[0];task={topic,type:"gapfill",prompt:`The cat is ___ ${place.slice(prep.length+1)}.`,options:shuffled([prep,"from","at"]),answer:prep}; }else if(topic==="plural"){ const n=pickGrammar([["child","children"],["mouse","mice"],["box","boxes"],["toy","toys"],["baby","babies"]]);task={topic,type:"gapfill",prompt:`One ${n[0]} — two ___.`,options:shuffled([n[1],n[0]+"s",n[0]+"es"]),answer:n[1]}; }else if(topic==="demonstratives"){ const near=Math.random()<.5,plural=Math.random()<.5,answer=near?(plural?"These":"This"):(plural?"Those":"That"),noun=plural?pickGrammar(["books","toys","apples"]):pickGrammar(["book","toy","apple"]);task={topic,type:"gapfill",prompt:`___ ${noun} ${near?"here":"over there"} ${plural?"are":"is"} new.`,options:shuffled([answer,plural?(near?"Those":"These"):(near?"That":"This"),plural?"This":"These"]),answer}; }else if(topic==="possessives"){ const p=pickGrammar([["Anna","her"],["Tom","his"],["I","my"],["You","your"]]);task={topic,type:"gapfill",prompt:`${p[0]} has got a dog. It is ___ dog.`,options:shuffled([p[1],"my","his","her"].filter((x,i,a)=>a.indexOf(x)===i).slice(0,3)),answer:p[1]}; }else if(topic==="questions"){ const q=pickGrammar([["Where","___ do you live? — In London."],["Who","___ is your teacher? — Mr Brown."],["When","___ is your birthday? — In May."],["What","___ is your favourite colour? — Blue."]]);task={topic,type:"gapfill",prompt:q[1],options:shuffled([q[0],...pickGrammar([["Who","Why"],["What","Where"],["When","Who"]])].filter((x,i,a)=>a.indexOf(x)===i)),answer:q[0]}; }else if(topic==="comparison"){ const q=pickGrammar([["An elephant","a mouse","bigger","big"],["A train","a bike","faster","fast"],["A giraffe","a dog","taller","tall"]]);task={topic,type:"gapfill",prompt:`${q[0]} is ___ than ${q[1]}.`,options:shuffled([q[2],q[3],"the "+q[2]]),answer:q[2]}; }else task=makeTaskLegacy(topic); task.signature=`${topic}|${task.prompt}|${task.answer}`;tries++; }while(recentGenerated.includes(task.signature)&&tries<30); recentGenerated.push(task.signature);if(recentGenerated.length>150)recentGenerated.shift();return task; } function pickGrammar(a){return a[Math.floor(Math.random()*a.length)];} const makeTaskLegacy=makeTask; const lexicalTerms={ colours:[["red","красный"],["blue","синий"],["green","зелёный"],["yellow","жёлтый"],["black","чёрный"],["white","белый"],["orange","оранжевый"],["purple","фиолетовый"],["pink","розовый"],["brown","коричневый"]], numbers:[["one","один"],["two","два"],["three","три"],["four","четыре"],["five","пять"],["six","шесть"],["seven","семь"],["eight","восемь"],["nine","девять"],["ten","десять"]], alphabet:[["A","эй"],["B","би"],["C","си"],["D","ди"],["E","и"],["F","эф"],["G","джи"],["H","эйч"],["I","ай"],["J","джей"]], family:[["mother","мама"],["father","папа"],["sister","сестра"],["brother","брат"],["grandmother","бабушка"],["grandfather","дедушка"],["aunt","тётя"],["uncle","дядя"],["cousin","двоюродный брат или сестра"],["parents","родители"]], weekdays:[["Monday","понедельник"],["Tuesday","вторник"],["Wednesday","среда"],["Thursday","четверг"],["Friday","пятница"],["Saturday","суббота"],["Sunday","воскресенье"],["week","неделя"],["weekday","будний день"],["weekend","выходные"]], seasons:[["spring","весна"],["summer","лето"],["autumn","осень"],["winter","зима"],["January","январь"],["March","март"],["June","июнь"],["September","сентябрь"],["November","ноябрь"],["December","декабрь"]], weather:[["sunny","солнечно"],["rainy","дождливо"],["windy","ветрено"],["cloudy","облачно"],["snowy","снежно"],["hot","жарко"],["cold","холодно"],["warm","тепло"],["storm","гроза"],["foggy","туманно"]], clothes:[["hat","шапка"],["shirt","рубашка"],["dress","платье"],["skirt","юбка"],["trousers","брюки"],["shoes","туфли"],["socks","носки"],["coat","пальто"],["jacket","куртка"],["scarf","шарф"]], food:[["apple","яблоко"],["banana","банан"],["bread","хлеб"],["cheese","сыр"],["milk","молоко"],["water","вода"],["egg","яйцо"],["fish","рыба"],["soup","суп"],["cake","торт"]], body:[["head","голова"],["eye","глаз"],["ear","ухо"],["nose","нос"],["mouth","рот"],["hand","кисть руки"],["arm","рука"],["leg","нога"],["foot","ступня"],["hair","волосы"]], jobs:[["doctor","врач"],["teacher","учитель"],["pilot","пилот"],["cook","повар"],["driver","водитель"],["farmer","фермер"],["nurse","медсестра"],["police officer","полицейский"],["artist","художник"],["builder","строитель"]], school:[["pencil","карандаш"],["pen","ручка"],["book","книга"],["ruler","линейка"],["rubber","ластик"],["school bag","портфель"],["desk","парта"],["chair","стул"],["notebook","тетрадь"],["classroom","классная комната"]], home:[["bed","кровать"],["table","стол"],["chair","стул"],["sofa","диван"],["lamp","лампа"],["door","дверь"],["window","окно"],["kitchen","кухня"],["bedroom","спальня"],["bathroom","ванная комната"]], transport:[["car","машина"],["bus","автобус"],["train","поезд"],["plane","самолёт"],["bike","велосипед"],["ship","корабль"],["boat","лодка"],["taxi","такси"],["underground","метро"],["helicopter","вертолёт"]], animals:[["cat","кошка"],["dog","собака"],["fox","лиса"],["lion","лев"],["bear","медведь"],["tiger","тигр"],["rabbit","кролик"],["monkey","обезьяна"],["elephant","слон"],["giraffe","жираф"]] }; const grammarTopics=new Set(Object.values(topicGroups)[1].map(x=>x[0])); const lexicalTemplateIds={1:["translate","recognise","spelling"],2:["recognise","spelling","category","odd"],3:["category","odd","spelling","meaning"],4:["odd","meaning","spelling","category"]}; function otherTopicWord(topic){const keys=Object.keys(lexicalTerms).filter(x=>x!==topic),terms=lexicalTerms[pickGrammar(keys)];return pickGrammar(terms)[0];} function misspell(word){if(word.length<3)return word+word.at(-1);const i=Math.max(1,Math.floor(word.length/2));return word.slice(0,i)+word[i+1]+word[i]+word.slice(i+2);} function lexicalTask(topic,level,templateId){const terms=lexicalTerms[topic]||lexicalTerms.colours,index=Math.floor(Math.random()*terms.length),[answer,ru]=terms[index],same=[terms[(index+3)%terms.length][0],terms[(index+7)%terms.length][0]],outsider=otherTopicWord(topic);let prompt,options; if(templateId==="translate"){prompt=`How do you say «${ru}» in English?`;options=[answer,...same];} else if(templateId==="recognise"){prompt=`Choose ${answer}.`;options=[answer,...same];} else if(templateId==="spelling"){prompt="Choose the correctly spelt word.";options=[answer,misspell(answer),answer+answer.at(-1)];} else if(templateId==="odd"){prompt=`Find the odd one out. Topic: ${topicName(topic)}.`;options=[outsider,answer,same[0]];return{topic,level,templateId,type:"odd",prompt,options,answer:outsider};} else if(templateId==="meaning"){prompt=`Read and choose. Which word means «${ru}»?`;options=[answer,...same];} else{prompt=`Which word belongs to “${topicName(topic)}”?`;options=[answer,outsider,otherTopicWord(topic)];} return{topic,level,templateId,type:"choice",prompt,options,answer,optionStyle:topic==="colours"&&templateId==="recognise"?"colour":null}; } function generateTask(topic,history=current){const level=TASK_LEVEL,used=history?.usedTaskSignatures||new Set(),recent=history?.recentTemplateIds||[],isGrammar=grammarTopics.has(topic);let task; for(let attempt=0;attempt<50;attempt++){ if(isGrammar){task=proceduralTask(topic,level,state.lesson);const forms=["complete","choose","form","context"],available=forms.filter(form=>!recent.slice(-2).includes(`grammar-${topic}-${form}`)),form=pickGrammar(available.length?available:forms);task.templateId=`grammar-${topic}-${form}`;task.level=level;if(form==="choose")task.prompt=`Choose the correct answer. ${task.prompt}`;else if(form==="form")task.prompt=`Choose the correct form. ${task.prompt}`;else if(form==="context")task.prompt=`Read and complete. ${task.prompt}`;} else{const allowed=lexicalTemplateIds[TASK_LEVEL],available=allowed.filter(id=>!recent.slice(-2).includes(id));task=lexicalTask(topic,level,pickGrammar(available.length?available:allowed));} task.options=[...new Set(task.options)];if(!task.options.includes(task.answer))task.options.unshift(task.answer);if(task.options.length<2)continue; task.signature=[topic,task.templateId,task.prompt,task.answer].join("|"); if(!used.has(task.signature)&&!recent.slice(-2).includes(task.templateId)){used.add(task.signature);recent.push(task.templateId);if(recent.length>3)recent.shift();return{...task,options:shuffled(task.options)};} } task.signature=[topic,task.templateId,task.prompt,task.answer,Date.now()].join("|");used.add(task.signature);recent.push(task.templateId);return{...task,options:shuffled(task.options)}; } makeTask=function(topic){return generateTask(topic,current);}; const colourValues={red:"#e53935",blue:"#2878e3",green:"#35a853",yellow:"#ffd632",black:"#20242b",white:"#ffffff",orange:"#f28c20",purple:"#854ec7",pink:"#f174ad",brown:"#86502d"}; function taskOptionsHtml(task){return task.options.map(x=>task.optionStyle==="colour"?`<button class="task-option colour-option" data-task="${esc(x)}" aria-label="Цвет ${esc(x)}" title="Выбрать цвет" style="--option-colour:${colourValues[x]||"#ccc"}"><span aria-hidden="true"></span></button>`:`<button class="task-option" data-task="${esc(x)}">${esc(x)}</button>`).join("");} showLesson=function(forcedTopic){ GameArcade.pause();let topics=forcedTopic?[forcedTopic]:state.lesson.topics,topic=pickGrammar(topics.length?topics:["present_simple"]),task=generateTask(topic,current); const d=modal(`<div class="lesson-mark">Учебная остановка · ${topicName(topic)}</div><h2>${task.prompt}</h2><div class="task-area ${task.optionStyle||""}">${taskOptionsHtml(task)}</div><p class="task-hint">${task.optionStyle==="colour"?"Choose the colour.":"Choose the correct answer."}</p>`,`lesson-modal`); const closeLesson=()=>{d.remove();if(current)current.lessonPending=false;if(current?.completeAfterLesson||current?.id!=="animals"&¤t?.step>=8)finishGame();else GameArcade.resume();}; $(".modal-x",d).onclick=closeLesson;d.onclick=e=>{if(e.target===d)closeLesson();}; d.querySelectorAll("[data-task]").forEach(b=>b.onclick=()=>{if(b.dataset.task===task.answer){current&&(current.lessonCorrect++);state.lessonStats.correct[topic]=(state.lessonStats.correct[topic]||0)+1;state.player.pearls+=5;save();d.querySelector(".modal").classList.add("success");$(".task-hint",d).textContent="Correct!";setTimeout(closeLesson,650);}else{current&&(current.lessonWrong++);state.lessonStats.wrong[topic]=(state.lessonStats.wrong[topic]||0)+1;save();b.classList.add("wrong");b.disabled=true;$(".task-hint",d).textContent=state.lesson.hintLevel==="full"?`Try again. First letter: ${task.answer[0]}`:"Try again.";}}); }; function renderLessonUnified(){ const selected=new Set(state.lesson.topics),l=state.lesson; screen(`${header()}<section class="panel lesson-config"><button class="back" data-go="library">← Назад</button><h1>Настроить урок</h1><p class="lead">Выберите темы. Задания создаются без повторов на едином уровне A1/A1+.</p><div class="lesson-layout"><div>${Object.entries(topicGroups).map(([group,items])=>`<h2>${group}</h2><div class="topics">${items.map(([id,name])=>`<label><input type="checkbox" value="${id}" ${selected.has(id)?"checked":""}><span>${name}</span></label>`).join("")}</div>`).join("")}</div><aside><h2>Параметры генерации</h2><label class="config-field">Тип предложений<select id="sentenceMode"><option value="mixed">Смешанный</option><option value="affirmative">Утверждения</option><option value="negative">Отрицания</option><option value="question">Вопросы</option></select></label><label class="config-field">Подсказки<select id="hintLevel"><option value="normal">Обычные</option><option value="full">Полные</option><option value="minimal">Минимальные</option></select></label><label class="config-field">Длина сессии<select id="duration"><option value="5">5 минут</option><option value="10">10 минут</option><option value="15">15 минут</option><option value="20">20 минут</option></select></label><h2>Режим</h2>${[["rare","Редко"],["normal","Обычно"],["frequent","Часто"],["off","Свободная игра"]].map(([v,n])=>`<label class="frequency"><input type="radio" name="freq" value="${v}" ${l.frequency===v?"checked":""}><span><b>${n}</b></span></label>`).join("")}<div class="task-preview"><b>Пример генерации</b><textarea id="previewText" rows="3" aria-label="Пример задания"></textarea><button type="button" class="btn ghost" id="newPreview">Сгенерировать другой</button></div><button type="button" class="btn primary big" id="saveLesson">Сохранить и начать</button></aside></div></section>${footer()}`); $("#sentenceMode").value=l.sentenceMode||"mixed";$("#hintLevel").value=l.hintLevel||"normal";$("#duration").value=l.duration||10; const preview=()=>{const checked=[...app.querySelectorAll('.topics input:checked')].map(x=>x.value),history={usedTaskSignatures:new Set(),recentTemplateIds:[]};$("#previewText").value=generateTask(checked[0]||"present_simple",history).prompt;};preview();$("#newPreview").onclick=preview;app.querySelectorAll('.topics input').forEach(x=>x.onchange=preview); $("#saveLesson").onclick=()=>{const topics=[...app.querySelectorAll('.topics input:checked')].map(x=>x.value),frequency=$('input[name="freq"]:checked').value;if(frequency!=="off"&&!topics.length){toast("Выберите тему");return;}state.lesson={...state.lesson,active:frequency!=="off",topics:topics.length?topics:state.lesson.topics,frequency,sentenceMode:$("#sentenceMode").value,hintLevel:$("#hintLevel").value,duration:+$("#duration").value};save();renderLibrary();}; }; document.body.classList.toggle("reduced",state.settings.reduced); const previewGame=new URLSearchParams(location.search).get("game"); if(dev&&games.some(g=>g.id===previewGame))startGame(previewGame);else renderLibrary(); })();