/
veranda
/
TypeGame
Обзор
Документация
Войти
/
veranda
/
TypeGame
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
script.js
236 строк
36 KB
veranda
Improve tutorial and optimize image loading
04 авг 2026, 13:59
04 авг 2026, 13:59
0cb3cc5
Код
Авторство
О чём код?
(() => { "use strict"; const $ = (q, root = document) => root.querySelector(q); const app = $("#app"); const STORE_KEY = "treasureIslandTyping_v1"; const islandMeta = [ { id: "animals", icon: "🦁", image: "assets/animals-3d.webp", treasure: "🧭" }, { id: "sea", icon: "🐬", image: "assets/sea-3d.webp", treasure: "🦪" }, { id: "jungle", icon: "🦜", image: "assets/jungle-3d.webp", treasure: "🪶" }, { id: "dino", icon: "🦕", image: "assets/dino-3d.webp", treasure: "🦷" }, { id: "sport", icon: "⚽", image: "assets/sport-3d.webp", treasure: "🏆" }, { id: "pirate", icon: "🏴☠️", image: "assets/pirate-3d.webp", treasure: "🪙" } ]; const ru = { title: "Остров сокровищ", subtitle: "Печатай слова — открывай острова!", play: "Играть", continue: "Продолжить", collection: "Коллекция", settings: "Настройки", how: "Как играть", chooseLang: "Выбери язык", chooseGrade: "Выбери класс", layout: "Перед игрой включи русскую раскладку клавиатуры", back: "Назад", grade: "класс", map: "Карта островов", treasures: "Сокровища", stars: "Звёзды", open: "Открыт", locked: "Сначала пройди предыдущий остров", words: "слов", pause: "Пауза", sound: "Звук", stageTrail: "Тропа к отметке", stageBridge: "Построй мост", stageChest: "Открой сундук", checkLayout: "Проверь раскладку клавиатуры", correct: "Отлично!", tryAgain: "Почти! Исправь букву с помощью Backspace", result: "Остров пройден!", resultPartial: "Хорошая тренировка!", support: "Каждое слово делает тебя увереннее!", typed: "Напечатано слов", accuracy: "Точность", speed: "Знаков в минуту", errors: "Исправлено ошибок", repeat: "Стоит повторить", again: "Ещё раз", toMap: "На карту", found: "Новое сокровище", reset: "Сбросить прогресс", resetAsk: "Точно удалить весь прогресс?", resetDone: "Прогресс сброшен", noSave: "Продолжать пока нечего", calm: "Спокойный", challenge: "На скорость", mode: "Режим игры", soundSet: "Звуковые эффекты", music: "Музыка", keyboard: "Виртуальная клавиатура", nextKey: "Подсветка следующей клавиши", translation: "Перевод английских слов", acceptYo: "Принимать «е» вместо «ё»", reduced: "Уменьшить анимации", close: "Готово", resume: "Продолжить", restart: "Начать заново", time: "Время", timeUp: "Время вышло — отличный повод попробовать ещё!", mobile: "Удобнее играть с физической клавиатурой", emptyCollection: "Проходи острова, чтобы найти сокровища!", tutorialTitle: "Как стать искателем сокровищ", tutorial: ["Посмотри на слово и картинку.","Печатай буквы по порядку.","Исправляй ошибки клавишей Backspace.","Собери части карты.","Открой сундук с сокровищем!"], grades: ["Короткие слова и подсказки", "Слова становятся длиннее", "Больше разных сочетаний", "Настоящее испытание мастера"], islands: { animals:"Остров животных",sea:"Остров моря",jungle:"Остров джунглей",dino:"Остров динозавров",sport:"Остров спорта",pirate:"Пиратский остров" }, treasureNames: { animals:"Золотой компас",sea:"Волшебная жемчужина",jungle:"Перо попугая",dino:"Зуб динозавра",sport:"Кубок чемпиона",pirate:"Старинная монета" } }; const en = { title: "Treasure Island", subtitle: "Type words — discover islands!", play: "Play", continue: "Continue", collection: "Collection", settings: "Settings", how: "How to play", chooseLang: "Choose a language", chooseGrade: "Choose your grade", layout: "Switch to the English keyboard before playing", back: "Back", grade: "Grade", map: "Island Map", treasures: "Treasures", stars: "Stars", open: "Open", locked: "Complete the previous island first", words: "words", pause: "Pause", sound: "Sound", stageTrail: "Follow the trail", stageBridge: "Build the bridge", stageChest: "Open the chest", checkLayout: "Check your keyboard language", correct: "Great!", tryAgain: "Almost! Fix the letter with Backspace", result: "Island complete!", resultPartial: "Great practice!", support: "Every word makes you a stronger typist!", typed: "Words typed", accuracy: "Accuracy", speed: "Characters per minute", errors: "Mistakes fixed", repeat: "Words to practise", again: "Play again", toMap: "Back to map", found: "New treasure", reset: "Reset progress", resetAsk: "Delete all progress?", resetDone: "Progress reset", noSave: "There is no journey to continue yet", calm: "Calm", challenge: "Speed challenge", mode: "Game mode", soundSet: "Sound effects", music: "Music", keyboard: "On-screen keyboard", nextKey: "Highlight the next key", translation: "Show Russian translation", acceptYo: "Accept е instead of ё", reduced: "Reduce animations", close: "Done", resume: "Resume", restart: "Restart round", time: "Time", timeUp: "Time is up — you can always try again!", mobile: "A physical keyboard is best for playing", emptyCollection: "Complete islands to discover treasures!", tutorialTitle: "How to become a treasure hunter", tutorial: ["Look at the word and picture.","Type the letters in order.","Use Backspace to fix mistakes.","Collect pieces of the map.","Open the treasure chest!"], grades: ["Short words with helpful hints", "Words get a little longer", "More letter combinations", "A true typing master challenge"], islands: { animals:"Animal Island",sea:"Sea Island",jungle:"Jungle Island",dino:"Dinosaur Island",sport:"Sports Island",pirate:"Pirate Island" }, treasureNames: { animals:"Golden Compass",sea:"Magic Pearl",jungle:"Parrot Feather",dino:"Dinosaur Tooth",sport:"Champion Cup",pirate:"Ancient Coin" } }; const rawWords = { animals: { ru:"кот|🐱|cat|1,пёс|🐶|dog|1,лиса|🦊|fox|1,лев|🦁|lion|1,слон|🐘|elephant|1,зебра|🦓|zebra|2,белка|🐿️|squirrel|2,тигр|🐯|tiger|2,панда|🐼|panda|2,коала|🐨|koala|2,жираф|🦒|giraffe|3,кролик|🐰|rabbit|3,обезьяна|🐒|monkey|3,леопард|🐆|leopard|3,хомячок|🐹|hamster|3,носорог|🦏|rhinoceros|4,кенгуру|🦘|kangaroo|4,медведь|🐻|bear|4,дикобраз|🦔|porcupine|4,крокодил|🐊|crocodile|4", en:"cat|🐱|кот|1,dog|🐶|пёс|1,fox|🦊|лиса|1,lion|🦁|лев|1,bear|🐻|медведь|1,zebra|🦓|зебра|2,tiger|🐯|тигр|2,panda|🐼|панда|2,koala|🐨|коала|2,mouse|🐭|мышь|2,rabbit|🐰|кролик|3,monkey|🐒|обезьяна|3,giraffe|🦒|жираф|3,hamster|🐹|хомяк|3,dolphin|🐬|дельфин|3,elephant|🐘|слон|4,kangaroo|🦘|кенгуру|4,crocodile|🐊|крокодил|4,rhinoceros|🦏|носорог|4,porcupine|🦔|дикобраз|4" }, sea: { ru:"море|🌊|sea|1,краб|🦀|crab|1,кит|🐋|whale|1,скат|🐟|ray|1,порт|⚓|port|1,волна|🌊|wave|2,акула|🦈|shark|2,лодка|⛵|boat|2,рыбка|🐠|fish|2,песок|🏖️|sand|2,дельфин|🐬|dolphin|3,медуза|🪼|jellyfish|3,кораллы|🪸|corals|3,ракушка|🐚|shell|3,маяк|💡|lighthouse|3,осьминог|🐙|octopus|4,мореплаватель|🧭|navigator|4,побережье|🏝️|coast|4,жемчужина|🦪|pearl|4,подлодка|🚤|submarine|4", en:"sea|🌊|море|1,crab|🦀|краб|1,fish|🐟|рыба|1,ship|🚢|корабль|1,sand|🏖️|песок|1,wave|🌊|волна|2,shark|🦈|акула|2,whale|🐋|кит|2,shell|🐚|ракушка|2,boat|⛵|лодка|2,dolphin|🐬|дельфин|3,jellyfish|🪼|медуза|3,coral|🪸|коралл|3,anchor|⚓|якорь|3,island|🏝️|остров|3,octopus|🐙|осьминог|4,lighthouse|💡|маяк|4,submarine|🚤|подлодка|4,seashore|🏖️|побережье|4,navigator|🧭|мореплаватель|4" }, jungle: { ru:"лист|🍃|leaf|1,лиана|🌿|vine|1,тукан|🐦|toucan|1,удав|🐍|boa|1,мох|🌱|moss|1,пальма|🌴|palm|2,банан|🍌|banana|2,тапир|🐗|tapir|2,попугай|🦜|parrot|2,бамбук|🎋|bamboo|2,водопад|💦|waterfall|3,орхидея|🌸|orchid|3,ягуар|🐆|jaguar|3,муравей|🐜|ant|3,тропики|🌴|tropics|3,путешествие|🗺️|journey|4,исследователь|🔎|explorer|4,растительность|🌿|vegetation|4,приключение|🎒|adventure|4,хамелеон|🦎|chameleon|4", en:"leaf|🍃|лист|1,vine|🌿|лиана|1,boa|🐍|удав|1,moss|🌱|мох|1,palm|🌴|пальма|1,toucan|🐦|тукан|2,banana|🍌|банан|2,tapir|🐗|тапир|2,bamboo|🎋|бамбук|2,parrot|🦜|попугай|2,jaguar|🐆|ягуар|3,orchid|🌸|орхидея|3,tropics|🌴|тропики|3,monkey|🐒|обезьяна|3,waterfall|💦|водопад|3,chameleon|🦎|хамелеон|4,adventure|🎒|приключение|4,explorer|🔎|исследователь|4,vegetation|🌿|растительность|4,rainforest|🌧️|тропический лес|4" }, dino: { ru:"яйцо|🥚|egg|1,след|🐾|track|1,кость|🦴|bone|1,ящер|🦎|lizard|1,хвост|🦕|tail|1,раптор|🦖|raptor|2,коготь|🦖|claw|2,лава|🌋|lava|2,скала|🪨|rock|2,динозавр|🦕|dinosaur|2,трицератопс|🦕|triceratops|3,скелет|🦴|skeleton|3,раскопки|⛏️|dig|3,вулкан|🌋|volcano|3,древний|⌛|ancient|3,палеонтолог|🔎|paleontologist|4,тираннозавр|🦖|tyrannosaurus|4,окаменелость|🪨|fossil|4,растительноядный|🌿|herbivore|4,доисторический|⌛|prehistoric|4", en:"egg|🥚|яйцо|1,bone|🦴|кость|1,tail|🦕|хвост|1,claw|🦖|коготь|1,rock|🪨|скала|1,raptor|🦖|раптор|2,lizard|🦎|ящер|2,track|🐾|след|2,lava|🌋|лава|2,fossil|🪨|окаменелость|2,dinosaur|🦕|динозавр|3,skeleton|🦴|скелет|3,volcano|🌋|вулкан|3,ancient|⌛|древний|3,digging|⛏️|раскопки|3,triceratops|🦕|трицератопс|4,herbivore|🌿|травоядный|4,prehistoric|⌛|доисторический|4,tyrannosaurus|🦖|тираннозавр|4,paleontologist|🔎|палеонтолог|4" }, sport: { ru:"мяч|⚽|ball|1,гол|🥅|goal|1,бег|🏃|run|1,матч|🏟️|match|1,корт|🎾|court|1,кубок|🏆|cup|2,лыжи|🎿|skis|2,теннис|🎾|tennis|2,шайба|🏒|puck|2,форма|👕|uniform|2,прыжок|🤸|jump|3,победа|🥇|victory|3,команда|👥|team|3,стадион|🏟️|stadium|3,тренер|📣|coach|3,соревнование|🏅|competition|4,баскетбол|🏀|basketball|4,волейбол|🏐|volleyball|4,гимнастика|🤸|gymnastics|4,чемпионат|🏆|championship|4", en:"run|🏃|бег|1,ball|⚽|мяч|1,goal|🥅|гол|1,team|👥|команда|1,skis|🎿|лыжи|1,match|🏟️|матч|2,tennis|🎾|теннис|2,coach|📣|тренер|2,jump|🤸|прыжок|2,race|🏁|гонка|2,victory|🥇|победа|3,stadium|🏟️|стадион|3,hockey|🏒|хоккей|3,football|⚽|футбол|3,training|💪|тренировка|3,basketball|🏀|баскетбол|4,volleyball|🏐|волейбол|4,gymnastics|🤸|гимнастика|4,competition|🏅|соревнование|4,championship|🏆|чемпионат|4" }, pirate: { ru:"клад|🎁|treasure|1,меч|🗡️|sword|1,ромб|💎|gem|1,карта|🗺️|map|1,грот|🪨|grotto|1,пират|🏴☠️|pirate|2,сундук|🧰|chest|2,мачта|⛵|mast|2,якорь|⚓|anchor|2,штурвал|☸️|wheel|2,компас|🧭|compass|3,капитан|🧑✈️|captain|3,сокровище|💰|treasure|3,корабль|🚢|ship|3,попугай|🦜|parrot|3,приключение|🎒|adventure|4,мореплаватель|🧭|navigator|4,таинственный|❓|mysterious|4,необитаемый|🏝️|deserted|4,путешественник|🗺️|traveller|4", en:"map|🗺️|карта|1,gold|🪙|золото|1,ship|🚢|корабль|1,mast|⛵|мачта|1,cave|🪨|пещера|1,pirate|🏴☠️|пират|2,chest|🧰|сундук|2,sword|🗡️|меч|2,anchor|⚓|якорь|2,parrot|🦜|попугай|2,compass|🧭|компас|3,captain|🧑✈️|капитан|3,treasure|💰|сокровище|3,island|🏝️|остров|3,sailor|⛵|моряк|3,adventure|🎒|приключение|4,navigator|🧭|мореплаватель|4,mysterious|❓|таинственный|4,shipwreck|🚢|кораблекрушение|4,traveller|🗺️|путешественник|4" } }; const words = Object.fromEntries(Object.entries(rawWords).map(([id, langs]) => [id, Object.fromEntries(Object.entries(langs).map(([lang, list]) => [lang, list.split(",").map(row => { const [word, hint, translation, grade] = row.split("|"); return { word, hint, translation, grade:+grade }; })])) ])); const defaults = () => ({ version: 1, lang: "ru", grade: 1, lastIsland: null, tutorialSeen: false, settings: { sound:true, music:false, keyboard:true, nextKey:true, translation:false, acceptYo:false, mode:"calm", reduced:false }, profiles: { ru:{ grades:{} }, en:{ grades:{} } } }); let data = load(); let currentScreen = "home"; let screenHistory = []; let game = null; let timerId = null; let audioCtx = null; let musicId = null; function load() { try { const saved = JSON.parse(localStorage.getItem(STORE_KEY)); if (!saved || saved.version !== 1) return defaults(); const base = defaults(); return { ...base, ...saved, settings:{...base.settings,...saved.settings}, profiles:{...base.profiles,...saved.profiles} }; } catch { return defaults(); } } function save() { try { localStorage.setItem(STORE_KEY, JSON.stringify(data)); } catch {} } function t() { return data.lang === "ru" ? ru : en; } function gradeProgress(lang=data.lang, grade=data.grade) { const profile = data.profiles[lang] || (data.profiles[lang] = {grades:{}}); return profile.grades[grade] || (profile.grades[grade] = { unlocked:1, results:{}, treasures:{}, mistakes:{} }); } function esc(value) { return String(value).replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c])); } function setScreen(name, html) { currentScreen = name; app.innerHTML = `<section class="screen">${html}</section>`; app.focus(); applyMotion(); } function header(title) { return `<div class="topbar"><button class="btn ghost" data-back>← ${t().back}</button><h1>${title}</h1><span class="nav-mascot" aria-hidden="true"><img src="assets/parrot-mascot-3d.webp" alt="" draggable="false"></span></div>`; } function bindNav() { app.querySelectorAll("[data-go]").forEach(b => b.addEventListener("click", () => navigate(b.dataset.go))); app.querySelectorAll("[data-back]").forEach(b => b.addEventListener("click", goBack)); } function renderNamed(go) { ({home:renderHome,language:renderLanguage,grade:renderGrade,map:renderMap,collection:renderCollection,settings:renderSettings}[go] || renderHome)(); } function navigate(go, record=true) { if(go==="how"){showTutorial();return;} stopTimer(); if(record && currentScreen && currentScreen!==go) screenHistory.push(currentScreen); renderNamed(go); } function goBack() { stopTimer(); const target=screenHistory.pop() || "home"; renderNamed(target); } function returnToMap() { stopTimer(); while(screenHistory.length && screenHistory[screenHistory.length-1]!=="map") screenHistory.pop(); if(screenHistory[screenHistory.length-1]==="map") screenHistory.pop(); renderMap(); } function renderHome() { const canContinue = !!data.lastIsland; setScreen("home", `<div class="panel home-panel"><div class="home-layout"><div class="home-visual" aria-hidden="true"><div class="home-sun"></div><img src="assets/pirate-3d.webp" alt="" draggable="false"><span class="home-coin coin-one">🪙</span><span class="home-coin coin-two">⭐</span><span class="home-coin coin-three">💎</span><div class="home-wave"></div></div><div class="home-content"><div class="home-kicker">🧭 ${data.lang==="ru"?"Большое приключение":"A grand adventure"}</div><h1 class="logo">${t().title}</h1><p class="subtitle">${t().subtitle}</p><div class="home-badges"><span>⌨️ ${data.lang==="ru"?"1–4 классы":"Grades 1–4"}</span><span>🌐 RU · EN</span><span>🏝️ 6 ${data.lang==="ru"?"островов":"islands"}</span></div><div class="button-row home-actions"><button id="play" class="btn primary">▶ ${t().play}</button>${canContinue?`<button id="continue" class="btn secondary">⛵ ${t().continue}</button>`:""}</div><div class="button-row home-tools"><button class="btn ghost" data-go="collection">🎁 ${t().collection}</button><button class="btn ghost" data-go="settings">⚙️ ${t().settings}</button><button class="btn ghost" data-go="how">❓ ${t().how}</button></div></div></div></div>`); $("#play").onclick=()=>navigate("language"); if (canContinue) $("#continue").onclick=()=>navigate("map"); bindNav(); if (!data.tutorialSeen) setTimeout(showTutorial, 250); } function renderLanguage() { setScreen("language", `<div class="panel">${header(t().chooseLang)}<div class="cards"><button class="card language-card" data-lang="ru"><span class="emoji">🇷🇺</span>Русский</button><button class="card language-card" data-lang="en"><span class="emoji">🇬🇧</span>English</button></div><p class="pill">⌨️ ${t().layout}</p></div>`); bindNav(); app.querySelectorAll("[data-lang]").forEach(b=>b.onclick=()=>{data.lang=b.dataset.lang;save();navigate("grade");}); } function renderGrade() { const gradeScenes=["assets/animals-3d.webp","assets/sea-3d.webp","assets/jungle-3d.webp","assets/pirate-3d.webp"]; setScreen("grade", `<div class="panel grade-panel">${header(t().chooseGrade,"language")}<p class="grade-lead">${data.lang==="ru"?"Выбери подходящую сложность и отправляйся в приключение!":"Choose your challenge and begin the adventure!"}</p><div class="cards grade-cards">${[1,2,3,4].map((g,i)=>`<button class="card grade-card grade-${g}" data-grade="${g}"><span class="grade-art-wrap"><img class="grade-art" src="${gradeScenes[i]}" alt="" draggable="false"><span class="grade-number"><b>${g}</b><small>${data.lang==="ru"?"КЛАСС":"GRADE"}</small></span><span class="grade-shine"></span></span><span class="grade-copy"><h2>${data.lang==="ru"?`${g} ${t().grade}`:`${t().grade} ${g}`}</h2><p>${t().grades[i]}</p><span class="grade-arrow" aria-hidden="true">→</span></span></button>`).join("")}</div></div>`); bindNav(); app.querySelectorAll("[data-grade]").forEach(b=>b.onclick=()=>{data.grade=+b.dataset.grade;data.lastIsland=null;gradeProgress();save();navigate("map");}); } function renderMap() { const p=gradeProgress(); const totalStars=Object.values(p.results).reduce((n,r)=>n+(r.stars||0),0); setScreen("map", `<div class="panel">${header(t().map)}<div class="summary-strip"><span class="pill">${data.lang==="ru"?`${data.grade} ${t().grade}`:`${t().grade} ${data.grade}`}</span><span class="pill">⭐ ${t().stars}: ${totalStars}</span><button class="pill treasure-pill" data-go="collection" aria-label="${t().collection}">🎁 ${t().treasures}: ${Object.keys(p.treasures).length}/6 <span aria-hidden="true">→</span></button></div><div class="map">${islandMeta.map((island,i)=>{const unlocked=i<p.unlocked;const result=p.results[island.id];return `<button class="card island island-${island.id} ${unlocked?"open":""} ${i===p.unlocked-1?"current":""}" data-island="${island.id}" ${unlocked?"":`disabled title="${t().locked}"`}><span class="island-art-wrap"><img class="island-art" src="${island.image}" alt="" draggable="false"><span class="art-glow"></span></span><h3>${t().islands[island.id]}</h3><div class="stars">${result?"★".repeat(result.stars)+"☆".repeat(3-result.stars):"☆☆☆"}</div>${unlocked?`<p>${t().open}</p>`:`<span class="lock">🔒</span>`}</button>`}).join("")}</div><div class="button-row"><button class="btn ghost" data-go="collection">🎁 ${t().collection}</button><button class="btn ghost" data-go="settings">⚙️ ${t().settings}</button></div></div>`); bindNav(); app.querySelectorAll("[data-island]").forEach(b=>b.onclick=()=>startGame(b.dataset.island)); } function chooseWords(id) { const pool=[...words[id][data.lang]], p=gradeProgress(), mistakes=p.mistakes; const desired={1:8,2:10,3:10,4:12}[data.grade]; pool.sort((a,b)=>(Math.abs(a.grade-data.grade)-Math.abs(b.grade-data.grade)) || ((mistakes[b.word]||0)-(mistakes[a.word]||0)) || Math.random()-.5); const exact=pool.filter(w=>w.grade===data.grade).sort((a,b)=>(mistakes[b.word]||0)-(mistakes[a.word]||0)||Math.random()-.5); const rest=pool.filter(w=>!exact.includes(w)).sort(()=>Math.random()-.5); return [...exact,...rest].slice(0,desired); } function startGame(id) { stopTimer(); if(currentScreen==="map") screenHistory.push("map"); const selected=chooseWords(id); game={id,words:selected,index:0,totalTyped:0,correctTyped:0,errors:0,wordErrors:{},start:Date.now(),pausedMs:0,pauseStart:0,timeLeft:120,finished:false,composing:false}; data.lastIsland=id; save(); renderGame(); if(data.settings.mode==="challenge") startTimer(); } function renderGame() { const w=game.words[game.index], total=game.words.length, pct=Math.round(game.index/total*100); setScreen("game", `<div class="panel game-panel"><div class="game-head"><h1>${t().islands[game.id]}</h1><div class="button-row game-nav"><button id="gameBackBtn" class="btn ghost game-text-btn">← <span>${t().back}</span></button><button id="gameMapBtn" class="btn ghost game-text-btn"><span>${t().toMap}</span></button><button id="soundBtn" class="btn ghost game-text-btn" aria-label="${t().sound}">${data.settings.sound?(data.lang==="ru"?"Звук: вкл":"Sound: on"):(data.lang==="ru"?"Звук: выкл":"Sound: off")}</button><button id="pauseBtn" class="btn ghost game-text-btn" aria-label="${t().pause}">${t().pause}</button></div></div><div class="journey"><div class="rope"></div><div class="hero" style="left:${Math.min(82,pct*.8+2)}%">🏃</div></div><div class="game-meta"><span>${game.index+1} / ${total}</span><span id="timerText">${data.settings.mode==="challenge"?`${t().time}: ${game.timeLeft}`:t().calm}</span></div><div class="progress"><span style="width:${pct}%"></span></div><div class="stage">${stageName()}</div><div class="hint">${w.hint}</div><div class="word-display" aria-label="${esc(w.word)}">${wordLetters(w.word,0)}</div><div class="translation">${data.lang==="en"&&data.settings.translation?esc(w.translation):" "}</div><label class="sr-only" for="typeInput">${w.word}</label><input id="typeInput" class="type-input" autocomplete="off" autocapitalize="off" spellcheck="false" inputmode="text" maxlength="${w.word.length+3}" aria-describedby="feedback"><div id="feedback" class="feedback">${t().layout}</div>${data.settings.keyboard?keyboardHtml():""}<p class="mobile-note">${t().mobile}</p></div>`); const input=$("#typeInput"); setTimeout(()=>input.focus(),30); input.addEventListener("keydown", handleKeydown); input.addEventListener("input",handleInput); input.addEventListener("paste",e=>{e.preventDefault();toast(data.lang==="ru"?"Печатай сам — так полезнее!":"Type it yourself — it helps you learn!");}); input.addEventListener("compositionstart",()=>game.composing=true); input.addEventListener("compositionend",()=>{game.composing=false;handleInput({target:input});}); $("#pauseBtn").onclick=pauseGame; $("#gameBackBtn").onclick=goBack; $("#gameMapBtn").onclick=returnToMap; $("#soundBtn").onclick=()=>{data.settings.sound=!data.settings.sound;save();renderGame();}; } function wordLetters(word,done) { return [...word].map((c,i)=>`<span class="letter ${i<done?"done":i===done?"current":""}">${esc(c)}</span>`).join(""); } function normalize(s){s=s.toLocaleLowerCase(data.lang==="ru"?"ru":"en");return data.settings.acceptYo?s.replaceAll("ё","е"):s;} function handleKeydown(e) { if(e.repeat&&e.key.length===1){e.preventDefault();return;} if(e.key==="Escape"){e.preventDefault();pauseGame();return;} if(e.key.length===1&&!e.ctrlKey&&!e.metaKey&&!e.altKey){game.totalTyped++;const target=normalize(game.words[game.index].word);const pos=e.target.value.length;if(normalize(e.key)===target[pos]){game.correctTyped++;tone(520,.035);}else if(wrongAlphabet(e.key)){game.totalTyped--;toast(t().checkLayout);tone(180,.08);}else{game.errors++;const word=game.words[game.index].word;game.wordErrors[word]=(game.wordErrors[word]||0)+1;tone(210,.07);}} } function wrongAlphabet(ch){return data.lang==="ru"?/^[a-z]$/i.test(ch):/^[а-яё]$/i.test(ch);} function handleInput(e) { if(game.composing)return; const input=e.target,w=game.words[game.index], value=normalize(input.value), target=normalize(w.word);let good=0; while(good<value.length&&good<target.length&&value[good]===target[good])good++; $(".word-display").innerHTML=wordLetters(w.word,good); if(!target.startsWith(value)){$("#feedback").textContent=t().tryAgain;input.classList.remove("error");void input.offsetWidth;input.classList.add("error");return;} input.classList.remove("error");$("#feedback").textContent=value?t().correct:"";highlightKey(target[value.length]||""); if(value===target) completeWord(); } function completeWord(){tone(700,.11);game.index++;if(game.index>=game.words.length){setTimeout(()=>finishGame(true),350);}else{setTimeout(renderGame,data.settings.reduced?0:430);}} function stageName(){const f=game.index/game.words.length;return f<.5?`🗺️ ${t().stageTrail}`:f<.8?`🌉 ${t().stageBridge}`:`🎁 ${t().stageChest}`;} function keyboardHtml(){const keys=data.lang==="ru"?["йцукенгшщзхъ","фывапролджэ","ячсмитьбю"]:["qwertyuiop","asdfghjkl","zxcvbnm"];return `<div class="keyboard" aria-hidden="true">${keys.map(r=>`<div class="key-row">${[...r].map(k=>`<span class="key" data-key="${k}">${k}</span>`).join("")}</div>`).join("")}</div>`;} function highlightKey(key){app.querySelectorAll(".key").forEach(k=>k.classList.toggle("next",data.settings.nextKey&&normalize(k.dataset.key)===normalize(key)));} function pauseGame(){if(!game||game.finished)return;game.pauseStart=Date.now();stopTimer();showModal(`<h2>⏸️ ${t().pause}</h2><div class="button-row"><button id="resume" class="btn primary">▶ ${t().resume}</button><button id="restart" class="btn secondary">↻ ${t().restart}</button><button id="mapBtn" class="btn ghost">🗺️ ${t().toMap}</button><button id="settingsBtn" class="btn ghost">⚙️ ${t().settings}</button></div>`);$("#resume").onclick=resumeGame;$("#restart").onclick=()=>{closeModal();startGame(game.id)};$("#mapBtn").onclick=()=>{closeModal();returnToMap()};$("#settingsBtn").onclick=()=>{closeModal();renderSettings("map")};} function resumeGame(){game.pausedMs+=Date.now()-game.pauseStart;closeModal();renderGame();if(data.settings.mode==="challenge")startTimer();} function startTimer(){stopTimer();timerId=setInterval(()=>{game.timeLeft--;const el=$("#timerText");if(el)el.textContent=`⏱️ ${t().time}: ${game.timeLeft}`;if(game.timeLeft<=0){stopTimer();toast(t().timeUp);finishGame(false);}},1000);} function stopTimer(){if(timerId){clearInterval(timerId);timerId=null;}} function finishGame(completed){if(!game||game.finished)return;game.finished=true;stopTimer();const elapsed=Math.max(1,(Date.now()-game.start-game.pausedMs)/60000);const accuracy=Math.round(game.correctTyped/Math.max(1,game.totalTyped)*100);const speed=Math.round(game.correctTyped/elapsed);const stars=completed?(accuracy>=95?3:accuracy>=85?2:1):0;const p=gradeProgress(); Object.entries(game.wordErrors).forEach(([w,n])=>p.mistakes[w]=(p.mistakes[w]||0)+n); if(completed){const old=p.results[game.id]||{};p.results[game.id]={stars:Math.max(stars,old.stars||0),accuracy:Math.max(accuracy,old.accuracy||0),speed:Math.max(speed,old.speed||0)};const idx=islandMeta.findIndex(x=>x.id===game.id);p.unlocked=Math.max(p.unlocked,Math.min(6,idx+2));if(!p.treasures[game.id])p.treasures[game.id]={date:new Date().toLocaleDateString(data.lang==="ru"?"ru-RU":"en-GB")};data.lastIsland=null;tone(880,.25);confetti();} save();renderResults({completed,accuracy,speed,stars}); } function renderResults(r){const repeat=Object.entries(game.wordErrors).sort((a,b)=>b[1]-a[1]).slice(0,3).map(x=>x[0]);const island=islandMeta.find(x=>x.id===game.id); setScreen("results",`<div class="panel"><div class="treasure">${r.completed?island.treasure:"🗺️"}</div><h1>${r.completed?t().result:t().resultPartial}</h1>${r.completed?`<div class="result-stars">${"★".repeat(r.stars)}${"☆".repeat(3-r.stars)}</div><h2>${t().found}: ${t().treasureNames[game.id]}</h2>`:""}<p>${t().support}</p><div class="stats"><div class="stat"><strong>${game.index}</strong>${t().typed}</div><div class="stat"><strong>${r.accuracy}%</strong>${t().accuracy}</div><div class="stat"><strong>${r.speed}</strong>${t().speed}</div><div class="stat"><strong>${game.errors}</strong>${t().errors}</div></div><p><b>${t().repeat}:</b> ${repeat.length?repeat.map(esc).join(", "):(data.lang==="ru"?"всё получилось!":"you did great!")}</p><div class="button-row"><button id="again" class="btn secondary">↻ ${t().again}</button><button id="toMap" class="btn primary">🗺️ ${t().toMap}</button></div></div>`);$("#again").onclick=()=>startGame(game.id);$("#toMap").onclick=returnToMap;} function renderCollection(){const p=gradeProgress();setScreen("collection",`<div class="panel collection-panel">${header(t().collection)}<p class="collection-lead">${data.lang==="ru"?"Каждый пройденный остров открывает новое волшебное сокровище":"Every completed island reveals a magical treasure"}</p><div class="summary-strip"><span class="pill">🇷🇺 ${Object.keys(gradeProgress("ru",data.grade).treasures).length}/6</span><span class="pill">🇬🇧 ${Object.keys(gradeProgress("en",data.grade).treasures).length}/6</span></div><div class="cards collection-grid">${islandMeta.map((island,i)=>{const found=p.treasures[island.id];return `<article class="card treasure-card ${found?"found":"locked-treasure"}"><span class="treasure-scene"><img src="${island.image}" alt="" draggable="false"><span class="treasure-badge">${found?island.treasure:"🔒"}</span><span class="scene-shine"></span></span><span class="treasure-info"><span class="treasure-index">${String(i+1).padStart(2,"0")}</span><h3>${found?t().treasureNames[island.id]:"???"}</h3><p>${t().islands[island.id]}</p><small>${found?`📅 ${found.date}`:`🔐 ${t().locked}`}</small></span></article>`}).join("")}</div>${!Object.keys(p.treasures).length?`<p>${t().emptyCollection}</p>`:""}</div>`);bindNav();} function renderSettings(back="home"){const s=data.settings;const toggle=(key,label)=>`<label class="setting"><span>${label}</span><input type="checkbox" data-setting="${key}" ${s[key]?"checked":""}></label>`;setScreen("settings",`<div class="panel settings">${header(t().settings,back)}${toggle("sound",`🔊 ${t().soundSet}`)}${toggle("music",`🎵 ${t().music}`)}${toggle("keyboard",`⌨️ ${t().keyboard}`)}${toggle("nextKey",`💡 ${t().nextKey}`)}${toggle("translation",`🌐 ${t().translation}`)}${toggle("acceptYo",`Ё ${t().acceptYo}`)}${toggle("reduced",`🐢 ${t().reduced}`)}<label class="setting"><span>⏱️ ${t().mode}</span><select data-setting="mode"><option value="calm" ${s.mode==="calm"?"selected":""}>${t().calm}</option><option value="challenge" ${s.mode==="challenge"?"selected":""}>${t().challenge}</option></select></label><div class="button-row" style="margin-top:20px"><button id="reset" class="btn danger">🗑️ ${t().reset}</button></div></div>`);bindNav();app.querySelectorAll("[data-setting]").forEach(el=>el.onchange=()=>{data.settings[el.dataset.setting]=el.type==="checkbox"?el.checked:el.value;save();applyMotion();syncMusic();});$("#reset").onclick=()=>{if(confirm(t().resetAsk)){const keepLang=data.lang;data=defaults();data.lang=keepLang;save();syncMusic();toast(t().resetDone);renderSettings();}};} function showTutorial(){ const labels=data.lang==="ru"?["СМОТРИ","ПЕЧАТАЙ","ИСПРАВЛЯЙ","СОБИРАЙ","ОТКРЫВАЙ"]:["LOOK","TYPE","FIX","COLLECT","DISCOVER"]; showModal(`<div class="tutorial-layout"><div class="tutorial-visual" aria-hidden="true"><img src="assets/parrot-mascot-3d.webp" alt="" draggable="false"><div class="tutorial-path"><i></i><i></i><i></i><i></i><i></i></div><span class="tutorial-caption">${data.lang==="ru"?"Твой проводник уже готов!":"Your guide is ready!"}</span></div><div class="tutorial-content"><span class="tutorial-kicker">${data.lang==="ru"?"ПЯТЬ ПРОСТЫХ ШАГОВ":"FIVE EASY STEPS"}</span><h2>${t().tutorialTitle}</h2><div class="tutorial-steps">${t().tutorial.map((s,i)=>`<div class="tutorial-step"><span class="step-number">${i+1}</span><span class="step-copy"><b>${labels[i]}</b><small>${s}</small></span></div>`).join("")}</div><button id="closeTutorial" class="btn primary tutorial-ready">${data.lang==="ru"?"Начать приключение":"Start the adventure"} →</button></div></div>`,"tutorial-modal"); $("#closeTutorial").onclick=()=>{data.tutorialSeen=true;save();closeModal();}; } function showModal(html,extraClass=""){closeModal();const d=document.createElement("div");d.className="modal-backdrop";d.id="modal";d.innerHTML=`<div class="panel modal ${extraClass}" role="dialog" aria-modal="true">${html}</div>`;document.body.append(d);setTimeout(()=>$("button",d)?.focus(),0);} function closeModal(){$("#modal")?.remove();} function tone(freq,duration){if(!data.settings.sound)return;try{audioCtx ||= new (window.AudioContext||window.webkitAudioContext)();const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.frequency.value=freq;o.type="sine";g.gain.setValueAtTime(.06,audioCtx.currentTime);g.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+duration);o.connect(g).connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+duration);}catch{}} function musicNote(freq){try{audioCtx ||= new (window.AudioContext||window.webkitAudioContext)();const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.frequency.value=freq;o.type="sine";g.gain.setValueAtTime(.012,audioCtx.currentTime);g.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+.8);o.connect(g).connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+.8);}catch{}} function syncMusic(){if(musicId){clearInterval(musicId);musicId=null;}if(data.settings.music){let i=0;const notes=[262,330,392,330];musicNote(notes[0]);musicId=setInterval(()=>musicNote(notes[++i%notes.length]),1800);}} function confetti(){if(data.settings.reduced)return;["⭐","🎉","🪙","✨","🎊"].forEach((x,i)=>setTimeout(()=>{const e=document.createElement("span");e.className="confetti";e.textContent=x;e.style.left=(10+Math.random()*80)+"%";document.body.append(e);setTimeout(()=>e.remove(),2500)},i*120));} function toast(msg){const el=$("#toast");el.textContent=msg;el.classList.add("show");clearTimeout(toast.id);toast.id=setTimeout(()=>el.classList.remove("show"),2200);} function applyMotion(){document.body.classList.toggle("reduce-motion",data.settings.reduced);} window.addEventListener("beforeunload",save); document.addEventListener("visibilitychange",()=>{if(document.hidden&¤tScreen==="game"&&!$("#modal"))pauseGame();}); renderHome(); })();