Инородное тело — обучающая игра TurboText
/* ============================================================ ДАННЫЕ: ЭТАП 1 — Инородное тело в глазу ============================================================ */ const fb_stage1 = [ { q: "В глаз попала соринка. Что делают ПЕРВЫМ делом?", opts: [ { t: "Тереть глаз, чтобы вышла", ok: false, why: "Тереть нельзя — можно поцарапать роговицу." }, { t: "Промыть чистой водой или физраствором", ok: true, why: "Промывание — первый и главный шаг." }, { t: "Закапать перекись водорода", ok: false, why: "Перекись в глаз — ожог роговицы." }, { t: "Заклеить глаз пластырем и ждать", ok: false, why: "Если соринка на веке — это бесполезно." } ] }, { q: "Соринка под верхним веком. Как её достать?", opts: [ { t: "Оттянуть веко и попросить посмотреть вниз — вымыть", ok: true, why: "Так соринка выходит сама или смывается." }, { t: "Выковырять ватной палочкой", ok: false, why: "Есть риск занести инфекцию и поцарапать." }, { t: "Подуть в глаз", ok: false, why: "Не поможет, только раздражит." }, { t: "Помассировать глаз через веко", ok: false, why: "Можно втереть соринку в роговицу." } ] }, { q: "Что делать, если соринка НЕ выходит после промывания?", opts: [ { t: "Наложить стерильную салфетку и ехать к офтальмологу", ok: true, why: "Дальше — только врач." }, { t: "Капать в глаз антибиотик самостоятельно", ok: false, why: "Не по назначению — вредно." }, { t: "Ночью само выйдет, спать", ok: false, why: "За ночь может быть травма." }, { t: "Промывать каждый час до утра", ok: false, why: "Избыточное промывание сушит роговицу." } ] }, { q: "Осколок стекла в глазу. Опасно ли промывать?", opts: [ { t: "Промывать нельзя — только закрыть салфеткой и в стационар", ok: true, why: "Промывание может загнать осколок глубже." }, { t: "Да, промыть обязательно большим объёмом", ok: false, why: "При проникающем ранении — нельзя." }, { t: "Помассировать глаз", ok: false, why: "Опасно, двигает осколок." }, { t: "Ничего не делать, само заживёт", ok: false, why: "Опасно, нужен врач срочно." } ] }, { q: "Через сколько часов после травмы глаза нужно обратиться к врачу, если симптомов почти нет?", opts: [ { t: "Незамедлительно — в течение ближайшего часа", ok: true, why: "Глаз — орган, где счёт на часы." }, { t: "В течение недели", ok: false, why: "Слишком поздно — риск потери зрения." }, { t: "Только если заболит", ok: false, why: "Может быть травма без боли." }, { t: "Никогда, само пройдёт", ok: false, why: "Опасно, риск потери зрения." } ] } ]; /* ============================================================ ДАННЫЕ: ЭТАП 2 — Химия в глазу ============================================================ */ const fb_stage2 = [ { q: "В глаз попала кислота. Что делаем?", opts: [ { t: "Промывать чистой водой 15–20 минут непрерывно", ok: true, why: "При химии промывают долго и обильно." }, { t: "Нейтрализовать щёлочью", ok: false, why: "Нейтрализация в глазу — ожог ещё хуже." }, { t: "Закапать успокаивающие капли", ok: false, why: "Сначала промыть, потом врач." }, { t: "Закрыть глаз и терпеть", ok: false, why: "Химия продолжает разрушать." } ] }, { q: "Какой водой промывать глаз при химическом ожоге?", opts: [ { t: "Любой чистой: из-под крана, бутилированной", ok: true, why: "При химии лучше любая вода, чем никакой." }, { t: "Только стерильным физраствором", ok: false, why: "Идеально, но время дороже — годится любая." }, { t: "Дистиллированной", ok: false, why: "Не обязательно." }, { t: "Перекисью, чтобы обеззаразить", ok: false, why: "Перекись в глаз — запрещено." } ] }, { q: "Известь попала в глаз. Что НЕЛЬЗЯ делать?", opts: [ { t: "Удалять частицы извести пинцетом", ok: true, why: "Остатки извести дадут вторичный ожог." }, { t: "Промывать большим объёмом воды", ok: false, why: "Это нужно делать." }, { t: "Ехать к врачу после промывания", ok: false, why: "Это правильно." }, { t: "Держать веко открытым при промывании", ok: false, why: "Это правильно." } ] } ]; /* ============================================================ ДАННЫЕ: ЭТАП 3 — Инородное тело в ухе ============================================================ */ const fb_stage3 = [ { q: "В ухо поп/* ============================================================ ДАННЫЕ: ЭТАП 3 — Инородное тело в ухе ============================================================ */ const fb_stage3 = [ { q: "В ухо попал живой жук. Что делаем?", opts: [ { t: "Закапать тёплое масло или воду — жук погибнет и перестанет шевелиться", ok: true, why: "Классика: залить тёплым маслом, потом к врачу." }, { t: "Трясти головой, чтобы выпал", ok: false, why: "Может залезть глубже и повредить барабанную перепонку." }, { t: "Ковырять ватной палочкой", ok: false, why: "Протолкнёт жука глубже." }, { t: "Закапать перекись", ok: false, why: "Не поможет от жука, только навредит." } ] }, { q: "В ухо попала горошина (растительное тело). Что НЕЛЬЗЯ?", opts: [ { t: "Промывать водой", ok: true, why: "Горох разбухнет и заклинит — станет хуже." }, { t: "Ехать к ЛОРу", ok: false, why: "Это правильно." }, { t: "Наклонить голову в сторону поражённого уха", ok: false, why: "Если не мешает — попробовать можно." }, { t: "Не ковырять палочками", ok: false, why: "Это правильно." } ] }, { q: "У ребёнка в ухе бусина, сидит глубоко. Что делать?", opts: [ { t: "Не трогать, срочно к ЛОРу", ok: true, why: "Глубоко — только врач со спец-инструментом." }, { t: "Пинцетом достать", ok: false, why: "Протолкнёт глубже, риск перфорации." }, { t: "Промыть спринцовкой", ok: false, why: "Без врача — опасно." }, { t: "Подождать, само выйдет", ok: false, why: "Не выйдет, будет воспаление." } ] }, { q: "После удаления инородного тела из уха что важно проверить?", opts: [ { t: "Целостность барабанной перепонки и слух", ok: true, why: "Обязательный контроль у ЛОРа." }, { t: "Температуру тела", ok: false, why: "Может быть, но не в первую очередь." }, { t: "Артериальное давление", ok: false, why: "Не связано." }, { t: "Уровень сахара", ok: false, why: "Не связано." } ] } ]; /* ============================================================ ДАННЫЕ: ЭТАП 4 — Инородное тело в ране ============================================================ */ const fb_stage4 = [ { q: "В ране торчит гвоздь. Что делаем?", opts: [ { t: "Не вынимать! Фиксируем, накрываем, вызываем скорую", ok: true, why: "Инородное в ране — не трогать, оно может тампонировать сосуд." }, { t: "Аккуратно вытянуть пинцетом", ok: false, why: "Может открыться сильное кровотечение." }, { t: "Обрезать гвоздь кусачками", ok: false, why: "Дёргает рану, риск кровотечения." }, { t: "Налить антисептик и ждать", ok: false, why: "Не поможет от кровотечения." } ] }, { q: "В ране осколок стекла. Как правильно?", opts: [ { t: "Не вынимать, обложить валиками вокруг и забинтовать", ok: true, why: "Фиксация — ключ." }, { t: "Промыть под напором", ok: false, why: "Может загнать осколок глубже." }, { t: "Намазать йодом", ok: false, why: "Йод в глубокую рану — дополнительный ожог." }, { t: "Зажать пальцами и вести в больницу", ok: false, why: "Дёргает осколок, опасно." } ] }, { q: "Осколок в ране крупный, торчит на 5 см. Как фиксировать?", opts: [ { t: "Сделать «бублик» из бинта вокруг, закрепить пластырем", ok: true, why: "Классика: бублик, чтобы предмет не двигался." }, { t: "Забинтовать прямо поверх, прижав", ok: false, why: "Может загнать глубже и сместить." }, { t: "Обмотать скотчем", ok: false, why: "Не стерильно и неудобно." }, { t: "Ничего не делать", ok: false, why: "Обязательно нужна фиксация." } ] }, { q: "Инородное тело в животе после падения на арматуру. Что НЕЛЬЗЯ?", opts: [ { t: "Давать пить и есть", ok: true, why: "Может быть операция — нельзя есть и пить." }, { t: "Вызвать скорую", ok: false, why: "Это правильно." }, { t: "Зафиксировать предмет", ok: false, why: "Это правильно." }, { t: "Уложить пострадавшего", ok: false, why: "Это правильно." } ] } ]; /* ============================================================ ДАННЫЕ: ЭТАП 5 — Заноза под ногтем ============================================================ */ const fb_stage5 = [ { q: "Заноза под ногтем, кончик видно. Что делаем?", opts: [ { t: "Пинцетом вытянуть в направлении входа, потом обработать", ok: true, why: "Тянут в сторону входа, не ломают." }, { t: "Выковырнуть иголкой с усилием", ok: false, why: "Можно расщепить и загнать глубже." }, { t: "Расковырять ноготь", ok: false, why: "Травма ногтевого ложа." }, { t: "Оставить — само выйдет", ok: false, why: "Риск нагноения, панариция." } ] }, { q: "Заноза вросла глубоко, кончика не видно. Что делать?", opts: [ { t: "Срочно к хирургу — самому не ковырять", ok: true, why: "Глубоко — только врач, иначе панариций." }, { t: "Распарить руку и давить", ok: false, why: "Раздавишь, загноится." }, { t: "Йод прямо под ноготь", ok: false, why: "Ожог ногтевого ложа." }, { t: "Иглой подцепить как получится", ok: false, why: "Травма и инфекция." } ] }, { q: "После удаления занозы ноготь болит и пульсирует. Что это?", opts: [ { t: "Возможный признак инфекции — нужен врач", ok: true, why: "Пульсация и боль — красный флаг." }, { t: "Норма, само пройдёт", ok: false, why: "Так можно потерять ноготь." }, { t: "Значит, ещё одна заноза", ok: false, why: "Не обязательно." }, { t: "Показатель высокого давления", ok: false, why: "Не связано." } ] } ]; /* ============================================================ ДАННЫЕ: ЭТАП 6 — Мифы и правда ============================================================ */ const fb_stage6_myths = [ { myth: "Перекисью можно промывать глаз, чтобы обеззаразить", truth: false, why: "Перекись в глаз — химический ожог. Никогда." }, { myth: "Если инородное тело в ране — надо сразу вытащить", truth: false, why: "Не вынимать! Сначала фиксация." }, { myth: "Ухо при инородном теле нужно промывать большим объёмом воды", truth: false, why: "Если это горошина — разбухнет и заклинит." }, { myth: "Занозу под ногтем лучше сразу выковырять иголкой", truth: false, why: "Опасно, часто вредит сильнее самой занозы." }, { myth: "Глаз при мелкой соринке промывают чистой водой", truth: true, why: "Да, промывание — первое и главное." }, { myth: "Жука в ухе заливают тёплым маслом", truth: true, why: "Да, чтобы обездвижить, потом к врачу." }, { myth: "Если глазник не болит — к врачу можно не идти", truth: false, why: "Многие травмы без боли. Счёт на часы." }, { myth: "При инородном теле в животе — нельзя пить и есть", truth: true, why: "Возможна операция под наркозом." } ]; /* ============================================================ ДАННЫЕ: ЭТАП 7 — Тест на закрепление ============================================================ */ const fb_stage7_quiz = [ { q: "В глаз попала щёлочь (моющее средство). Что делаем?", opts: [ { t: "Промывать чистой водой 15–20 минут и немедленно к офтальмологу", ok: true, why: "Щёлочь проникает глубоко — время критично." }, { t: "Нейтрализовать слабой кислотой", ok: false, why: "Нейтрализация в глазу — ожог." }, { t: "Закапать антибиотик", ok: false, why: "Не первично." }, { t: "Промывать 10 секунд и звонить врачу", ok: false, why: "Слишком мало, нужно 15–20 минут." } ] }, { q: "В ране большой осколок. Что НЕЛЬЗЯ делать?", opts: [ { t: "Пытаться вытащить своими руками", ok: true, why: "Может открыться кровотечение." }, { t: "Фиксировать валиками", ok: false, why: "Это правильно." }, { t: "Прижать салфеткой вокруг", ok: false, why: "Это правильно." }, { t: "Вызвать скорую", ok: false, why: "Это правильно." } ] }, { q: "У ребёнка в носу батарейка-таблетка. Что делать?", opts: [ { t: "Срочно в стационар — батарейка прожигает ткани за минуты", ok: true, why: "Это одна из опаснейших ситуаций, счёт на минуты." }, { t: "Достать пинцетом", ok: false, why: "Опасно протолкнуть и получить химический ожог." }, { t: "Промыть спринцовкой", ok: false, why: "Не поможет и опасно." }, { t: "Покапать сосудосуживающим", ok: false, why: "Не про это." } ] }, { q: "Чужой предмет в ране груди. Какое положение безопаснее?", opts: [ { t: "Полусидя, с наклоном в сторону раны", ok: true, why: "Меньше нагрузки на грудную клетку." }, { t: "Лежа на спине", ok: false, why: "Затрудняет дыхание." }, { t: "Сидя ровно, 90°", ok: false, why: "Тоже допустимо, но лучше с наклоном." }, { t: "Стоя", ok: false, why: "Может упасть." } ] }, { q: "При любом инородном теле, если сомневаешься — что главное правило?", opts: [ { t: "Не навреди: фиксируй, не ковыряй, вызывай врача", ok: true, why: "Универсальное правило первой помощи." }, { t: "Скорее вытащить самому", ok: false, why: "Часто вредит." }, { t: "Ждать до утра", ok: false, why: "Можно потерять время." }, { t: "Довериться советам из интернета", ok: false, why: "Только врач." } ] } ]; /* ============================================================ УНИВЕРСАЛЬНЫЙ РЕНДЕР ВОПРОСА ============================================================ */ function fb_renderQuestion(item, onDone, stageNum) { fb_stopTimer(); const lvl = fb_levels[fb_state.levelKey]; const app = fb_state.app; app.innerHTML = ""; fb_mountProgress(); const card = fb_h("div", "fb-card"); card.appendChild(fb_h("div", "fb-q-num", "Вопрос " + (fb_state.qIndex + 1))); card.appendChild(fb_h("div", "fb-q-text", item.q)); const opts = fb_h("div", "fb-opts"); const letters = ["А", "Б", "В", "Г", "Д", "Е"]; item.opts.forEach(function(opt, idx) { const btn = fb_h("button", "fb-opt"); btn.appendChild(fb_h("span", "fb-opt-letter", letters[idx] + ".")); btn.appendChild(fb_h("span", "", opt.t)); btn.disabled = false; btn.onclick = function() { fb_answer(btn, opt, item, opts, onDone, stageNum); }; opts.appendChild(btn); }); card.appendChild(opts); app.appendChild(card); // таймер if (lvl.timerSec > 0) { fb_startTimer(lvl.timerSec, function() { fb_penalizeTimeout(item, opts, onDone, stageNum); }); } } /* ============================================================ ОТВЕТ ============================================================ */ function fb_answer(btn, opt, item, optsWrap, onDone, stageNum) { fb_stopTimer(); const allBtns = optsWrap.querySelectorAll(".fb-opt"); allBtns.forEach(function(b) { b.disabled = true; }); const lvl = fb_levels[fb_state.levelKey]; const correctIdx = item.opts.findIndex(function(x) { return x.ok; }); if (opt.ok) { btn.classList.add("fb-opt-right"); fb_state.score += lvl.score.correct; fb_state.streak++; if (fb_state.streak >= 3) fb_state.score += lvl.score.streakBonus; fb_state.correctInStage++; fb_beepOk(); fb_vibro(30); let msg = "✅ Верно! " + opt.why; if (fb_state.streak >= 3) msg += " 🔥 Серия " + fb_state.streak + " — бонус!"; fb_showFeedback("ok", msg); } else { btn.classList.add("fb-opt-wrong"); if (allBtns[correctIdx]) allBtns[correctIdx].classList.add("fb-opt-right"); fb_state.score += lvl.score.wrong; fb_state.lives--; fb_state.streak = 0; fb_state.wrongInStage++; fb_beepErr(); fb_vibro([40, 60, 40]); fb_showFeedback("err", "❌ Не так. " + opt.why); } fb_afterAnswer(onDone, stageNum); } /* ============================================================ ПЕНАЛЬТИ ПО ТАЙМАУТУ ============================================================ */ function fb_penalizeTimeout(item, optsWrap, onDone, stageNum) { const lvl = fb_levels[fb_state.levelKey]; fb_state.score += lvl.score.wrong; fb_state.lives--; fb_state.streak = 0; fb_state.wrongInStage++; fb_beepErr(); fb_vibro([80, 60, 80]); const allBtns = optsWrap.querySelectorAll(".fb-opt"); const correctText = item.opts.find(function(x) { return x.ok; }).t; allBtns.forEach(function(b) { if (b.textContent.indexOf(correctText) !== -1) b.classList.add("fb-opt-right"); b.disabled = true; }); fb_showFeedback("err", "⏱ Время вышло! Правильный ответ: «" + correctText + "»."); fb_afterAnswer(onDone, stageNum); } /* ============================================================ ПОСЛЕ ОТВЕТА ============================================================ */ function fb_afterAnswer(onDone, stageNum) { fb_renderProgress(); if (fb_state.lives <= 0) { setTimeout(function() { fb/** ============================================================ ЗАВЕРШЕНИЕ ЭТАПА ============================================================ */ function fb_finishStage() { fb_stopTimer(); const lvl = fb_levels[fb_state.levelKey]; fb_state.stageScores = fb_state.stageScores || {}; fb_state.stageScores[fb_state.stage] = { correct: fb_state.correctInStage, wrong: fb_state.wrongInStage }; if (fb_state.stage >= 7) { fb_showFinal(); return; } fb_showStageBetween(); } /** ============================================================ ЭКРАН МЕЖДУ ЭТАПАМИ ============================================================ */ function fb_showStageBetween() { const app = fb_state.app; app.innerHTML = ""; const card = fb_h("div", "fb-card fb-card-center"); const nextStage = fb_state.stage + 1; card.appendChild(fb_h("div", "fb-big-emoji", "✅")); card.appendChild(fb_h("div", "fb-h2", "Этап " + fb_state.stage + " пройден!")); const stat = fb_h("div", "fb-stage-stat"); stat.appendChild(fb_h("span", "fb-stat-ok", "✔ " + fb_state.correctInStage + " верно")); stat.appendChild(fb_h("span", "fb-stat-err", "✖ " + fb_state.wrongInStage + " ошибок")); card.appendChild(stat); card.appendChild(fb_h("div", "fb-muted", "Следующий: " + (fb_stageNames[nextStage - 1] || ""))); const btn = fb_h("button", "fb-btn fb-btn-primary", "➡️ Дальше"); btn.style.marginTop = "18px"; btn.onclick = function() { fb_startStage(nextStage); }; card.appendChild(btn); app.appendChild(card); } /** ============================================================ ФИНАЛЬНЫЙ ЭКРАН ============================================================ */ function fb_showFinal() { const app = fb_state.app; app.innerHTML = ""; const lvl = fb_levels[fb_state.levelKey]; let totalCorrect = 0, totalWrong = 0; Object.keys(fb_state.stageScores).forEach(function(k) { totalCorrect += fb_state.stageScores[k].correct; totalWrong += fb_state.stageScores[k].wrong; }); const maxScore = fb_state.score; const best = fb_getBest(lvl.id); const isRecord = maxScore > best; if (isRecord) fb_setBest(lvl.id, maxScore); const tier = fb_getTier(totalCorrect, totalWrong, fb_state.lives, lvl.lives); const card = fb_h("div", "fb-card fb-card-center"); card.appendChild(fb_h("div", "fb-big-emoji", tier.emoji)); card.appendChild(fb_h("div", "fb-h1", tier.title)); card.appendChild(fb_h("div", "fb-muted", tier.text)); const scoreRow = fb_h("div", "fb-final-score"); scoreRow.appendChild(fb_h("span", "fb-final-num", String(maxScore))); scoreRow.appendChild(fb_h("span", "fb-final-lbl", "очков")); card.appendChild(scoreRow); if (isRecord) { const rec = fb_h("div", "fb-record", "🏆 Новый рекорд!"); card.appendChild(rec); } else { card.appendChild(fb_h("div", "fb-muted", "Личный рекорд: " + best)); } const stat = fb_h("div", "fb-stage-stat"); stat.appendChild(fb_h("span", "fb-stat-ok", "✔ " + totalCorrect + " верно")); stat.appendChild(fb_h("span", "fb-stat-err", "✖ " + totalWrong + " ошибок")); stat.appendChild(fb_h("span", "fb-stat-lives", fb_hearts(fb_state.lives, lvl.lives))); card.appendChild(stat); const again = fb_h("button", "fb-btn fb-btn-primary", "🔄 Играть заново"); again.style.marginTop = "20px"; again.onclick = function() { fb_startGame(fb_state.levelKey); }; card.appendChild(again); const toMenu = fb_h("button", "fb-btn fb-btn-ghost", "🏠 В меню"); toMenu.style.marginTop = "10px"; toMenu.onclick = function() { fb_showStart(); }; card.appendChild(toMenu); app.appendChild(card); } /** ============================================================ ТИР (ранг) ============================================================ */ function fb_getTier(correct, wrong, livesLeft, livesMax) { const total = correct + wrong || 1; const acc = correct / total; const survived = livesLeft; if (survived === livesMax && acc >= 0.9) { return { emoji: "🥇", title: "Эксперт первой помощи", text: "Ты не потерял ни одной жизни и почти не ошибался. Так держать!" }; } if (acc >= 0.8) { return { emoji: "🥈", title: "Уверенный уровень", text: "Хороший результат. Пара слабых мест — их можно подтянуть." }; } if (acc >= 0.6) { return { emoji: "🥉", title: "Базовый уровень", text: "Основы знаешь. Стоит повторить ключевые правила." }; } return { emoji: "📚", title: "Новичок", text: "Материал пока не отложился. Пройди заново — и всё встанет на места." }; } /** ============================================================ ГЕЙМОВЕР ============================================================ */ function fb_showGameOver() { fb_stopTimer(); const app = fb_state.app; app.innerHTML = ""; const card = fb_h("div", "fb-card fb-card-center"); card.appendChild(fb_h("div", "fb-big-emoji", "💔")); card.appendChild(fb_h("div", "fb-h1", "Игра окончена")); card.appendChild(fb_h("div", "fb-muted", "Жизни закончились. Но ты всегда можешь попробовать снова.")); const scoreRow = fb_h("div", "fb-final-score"); scoreRow.appendChild(fb_h("span", "fb-final-num", String(fb_state.score))); scoreRow.appendChild(fb_h("span", "fb-final-lbl", "очков")); card.appendChild(scoreRow); const again = fb_h("button", "fb-btn fb-btn-primary", "🔄 Ещё раз"); again.style.marginTop = "20px"; again.onclick = function() { fb_startGame(fb_state.levelKey); }; card.appendChild(again); const toMenu = fb_h("button", "fb-btn fb-btn-ghost", "🏠 В меню"); toMenu.style.marginTop = "10px"; toMenu.onclick = function() { fb_showStart(); }; card.appendChild(toMenu); app.appendChild(card); } /** ============================================================ СТАРТОВЫЙ ЭКРАН ============================================================ */ function fb_showStart() { fb_stopTimer(); const app = fb_state.app; app.innerHTML = ""; const hero = fb_h("div", "fb-hero"); hero.appendChild(fb_h("div", "fb-hero-emoji", "🚑")); hero.appendChild(fb_h("div", "fb-h1", "Первая помощь")); hero.appendChild(fb_h("div", "fb-hero-sub", "Инородные тела. 7 этапов. Правила, которые спасают зрение, слух и жизнь.")); app.appendChild(hero); const lvlBox = fb_h("div", "fb-levels"); Object.keys(fb_levels).forEach(function(key) { const lvl = fb_levels[key]; const card = fb_h("div", "fb-level-card" + (fb_state.levelKey === key ? " fb-level-active" : "")); card.appendChild(fb_h("div", "fb-level-title", lvl.title)); card.appendChild(fb_h("div", "fb-level-desc", lvl.desc)); card.appendChild(fb_h("div", "fb-level-meta", "❤️ " + lvl.lives + " жизней • ⏱ " + lvl.timerSec + " сек/вопрос")); const best = fb_getBest(lvl.id); if (best > 0) card.appendChild(fb_h("div", "fb-level-best", "🏆 Рекорд: " + best)); card.onclick = function() { fb_state.levelKey = key; fb_saveLevel(key); fb_showStart(); }; lvlBox.appendChild(card); }); app.appendChild(lvlBox); const start = fb_h("button", "fb-btn fb-btn-primary", "🚀 Начать игру"); start.style.marginTop = "18px"; start.onclick = function() { fb_startGame(fb_state.levelKey); }; app.appendChild(start); } /** ============================================================ ЗАПУСК ИГРЫ ============================================================ */ function fb_startGame(levelKey) { const lvl = fb_levels[levelKey]; fb_state.levelKey = levelKey; fb_state.stage = 1; fb_state.qIndex = 0; fb_state.score = 0; fb_state.lives = lvl.lives; fb_state.streak = 0; fb_state.correctInStage = 0; fb_state.wrongInStage = 0; fb_state.stageScores = {}; fb_saveLevel(levelKey); fb_startStage(1); } /** ============================================================ STORAGE (рекорды и уровень) ============================================================ */ function fb_getBest(id) { try { return parseInt(localStorage.getItem("fb_best_" + id) || "0", 10) || 0; } catch(e) { return 0; } } function fb_setBest(id, val) { try { localStorage.setItem("fb_best_" + id, String(val)); } catch(e) {} } function fb_saveLevel(key) { try { localStorage.setItem("fb_level", key); } catch(e) {} } function fb_loadLevel() { try { return localStorage.getItem("fb_level") || "easy"; } catch(e) { return "easy"; } } /** ============================================================ HAPTICS / ЗВУК ============================================================ */ function fb_vibro(pattern) { if (navigator.vibrate) { try { navigator.vibrate(pattern); } catch(e) {} } } let fb_audioCtx = null; function fb_beep(freq, dur) { try { if (!fb_audioCtx) { const Ctx = window.AudioContext || window.webkitAudioContext; if (!Ctx) return; fb_audioCtx = new Ctx(); } const osc = fb_audioCtx.createOscillator(); const gain = fb_audioCtx.createGain(); osc.type = "sine"; osc.frequency.value = freq; gain.gain.value = 0.08; osc.connect(gain); gain.connect(fb_audioCtx.destination); osc.start(); osc.stop(fb_audioCtx.currentTime + dur / 1000); } catch(e) {} } function fb_beepOk() { fb_beep(880, 90); } function fb_beepErr() { fb_beep(220, 160); } /** ============================================================ УТИЛИТЫ ============================================================ */ function fb_h(tag, cls, text) { const el = document.createElement(tag); if (cls) el.className = cls; if (text !== undefined && text !== null) el.textContent = text; return el; } function fb_shuffle(arr) { const a = arr.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const t = a[i]; a[i] = a[j]; a[j] = t; } return a; } function fb_mmss(sec) { const m = Math.floor(sec / 60); const s = sec % 60; return (m > 0 ? m + ":" + (s < 10 ? "0" : "") : "") + s + "с"; } function fb_hearts(current, max) { let out = ""; for (let i = 0; i < max; i++) out += (i < current ? "❤️" : "🖤"); return out; }