Инородное тело — обучающая игра 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;
}