app.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. 'use strict';
  2. // ---------- ДАННЫЕ ----------
  3. const ACTS = {
  4. 1: {
  5. title: 'Акт I. Идентификация',
  6. scenes: [
  7. {
  8. system: 'Приветствую. Для начала работы мне нужно понять, кто вы. Представьтесь, пожалуйста.',
  9. choices: [
  10. { text: 'Назвать свое имя: «Я — Алексей Иванов, стажер».', correct: true,
  11. feedback: 'Принято. Алексей Иванов. Я зафиксировала ваше имя в журнале посещений.',
  12. edu: 'ИДЕНТИФИКАЦИЯ — это присвоение субъекту доступа уникального имени (логина, ID, номера пропуска). Это заявление: «Я — Вася». Важно: На этом этапе система еще не доверяет вам, она просто знает, как вас зовут.'
  13. },
  14. { text: 'Попытаться пройти молча.', correct: false,
  15. feedback: 'Ошибка. Идентификация не пройдена. Я не могу работать с анонимом. Доступ запрещен.'
  16. },
  17. { text: 'Сказать: «Я — директор банка».', correct: false,
  18. feedback: 'Стоп. Вы назвались директором, но в базе нет такого стажера. Это ложная идентификация. Вызов охраны!'
  19. }
  20. ]
  21. }
  22. ]
  23. },
  24. 2: {
  25. title: 'Акт II. Аутентификация',
  26. scenes: [
  27. {
  28. system: 'Алексей, я вижу ваше имя, но мне нужно убедиться, что вы — это действительно вы. У вас есть 3 попытки. Ваш ключ доступа — это дата вашего рождения + код из СМС.',
  29. choices: [
  30. { text: 'Ввести пароль, который вы придумали сами (например, «12345»).', correct: false,
  31. feedback: 'Проверка... Пароль слишком простой. Взломщик подобрал его за 2 секунды. Это плохая аутентификация. Доступ заблокирован.'
  32. },
  33. { text: 'Приложить отпечаток пальца к сканеру (Биометрия).', correct: true,
  34. feedback: 'Отпечаток совпадает с базой. Отлично! Вы доказали, что вы — это вы. Процесс АУТЕНТИФИКАЦИИ пройден. Теперь я доверяю этому сеансу.',
  35. edu: 'АУТЕНТИФИКАЦИЯ — это проверка подлинности. Доказательство того, что субъект действительно тот, за кого себя выдает. Факторы аутентификации: Знание (пароль, PIN). Владение (токен, телефон, смарт-карта). Биометрия (палец, лицо, сетчатка).'
  36. },
  37. { text: 'Попросить коллегу приложить свой пропуск.', correct: false,
  38. feedback: 'Чужой пропуск? Это попытка обмана. Аутентификация не пройдена.'
  39. }
  40. ]
  41. }
  42. ]
  43. },
  44. 3: {
  45. title: 'Акт III. Авторизация',
  46. scenes: [
  47. {
  48. system: 'Алексей, вы успешно вошли. Но теперь мне нужно определить ваши права. Кто вы в системе?',
  49. choices: [
  50. { text: 'Администратор (все права).', correct: false,
  51. feedback: 'Попытка повышения привилегий! Вы стажер, но пытаетесь получить права админа. Это нарушение политики безопасности. Инцидент записан в лог.'
  52. },
  53. { text: 'Кассир (доступ к деньгам).', correct: false,
  54. feedback: 'Кассир? Ваша роль — стажер. Несоответствие прав. Доступ к деньгам запрещен.'
  55. },
  56. { text: 'Стажер (только просмотр).', correct: true,
  57. feedback: 'Роль подтверждена. Вам разрешен только просмотр курса валют. Кнопка перевода денег для вас неактивна (серая). Это АВТОРИЗАЦИЯ.',
  58. edu: 'АВТОРИЗАЦИЯ — это предоставление субъекту прав на доступ к ресурсам и определение разрешенных действий. Простыми словами: Аутентификация говорит «Ты — Вася», а Авторизация говорит «Васе можно только читать, но нельзя писать».'
  59. }
  60. ]
  61. },
  62. {
  63. system: 'Вдруг экран мигает. Появляется «Тень»: «Я украл логин Алексея (Идентификация) и подобрал пароль (Аутентификация). Теперь я войду в систему!»',
  64. choices: [
  65. { text: 'Ничего, он войдет.', correct: false,
  66. feedback: 'Нет. Даже зная пароль, без второго фактора Хакер не пройдет Аутентификацию.'
  67. },
  68. { text: 'Двухфакторная аутентификация (2FA).', correct: true,
  69. feedback: 'Верно! Даже зная пароль, без второго фактора (СМС или отпечатка) Хакер не пройдет Аутентификацию. А если бы и прошел — Авторизация ограничила бы его права только просмотром, и он не смог бы украсть деньги.',
  70. edu: 'Двухфакторная аутентификация (2FA) — метод защиты, при котором для входа требуется два разных фактора: например, пароль (знание) и код из СМС (владение). Это значительно усложняет взлом.'
  71. },
  72. { text: 'Авторизация.', correct: false,
  73. feedback: 'Авторизация происходит после аутентификации. Но без аутентификации хакер не получит даже прав.'
  74. }
  75. ]
  76. }
  77. ]
  78. }
  79. };
  80. const QUIZ = [
  81. {
  82. q: 'Что такое идентификация?',
  83. options: [
  84. 'Присвоение субъекту уникального имени (логина, ID)',
  85. 'Проверка подлинности субъекта',
  86. 'Предоставление прав доступа к ресурсам',
  87. 'Шифрование данных'
  88. ],
  89. correct: 0,
  90. explain: 'Идентификация — это заявление «Я — Вася». Система просто знает, как вас зовут.'
  91. },
  92. {
  93. q: 'Какой из перечисленных факторов НЕ относится к аутентификации?',
  94. options: [
  95. 'Знание (пароль, PIN)',
  96. 'Владение (токен, телефон)',
  97. 'Биометрия (отпечаток пальца)',
  98. 'Роль в системе (стажёр, администратор)'
  99. ],
  100. correct: 3,
  101. explain: 'Роль в системе — это авторизация. Аутентификация проверяет, что вы — это вы.'
  102. },
  103. {
  104. q: 'Что означает авторизация?',
  105. options: [
  106. 'Проверка подлинности пользователя',
  107. 'Присвоение пользователю уникального имени',
  108. 'Предоставление прав на доступ к ресурсам и определение разрешённых действий',
  109. 'Двухфакторная проверка'
  110. ],
  111. correct: 2,
  112. explain: 'Авторизация говорит: «Васе можно только читать, но нельзя писать».'
  113. },
  114. {
  115. q: 'Хакер украл логин и пароль. Что помешает ему войти в систему?',
  116. options: [
  117. 'Ничего, он войдет',
  118. 'Двухфакторная аутентификация (2FA)',
  119. 'Авторизация',
  120. 'Идентификация'
  121. ],
  122. correct: 1,
  123. explain: '2FA требует второй фактор (СМС, отпечаток), которого у хакера нет.'
  124. },
  125. {
  126. q: 'В какой последовательности происходит доступ к защищённой системе?',
  127. options: [
  128. 'Авторизация → Аутентификация → Идентификация',
  129. 'Идентификация → Аутентификация → Авторизация',
  130. 'Аутентификация → Идентификация → Авторизация',
  131. 'Идентификация → Авторизация → Аутентификация'
  132. ],
  133. correct: 1,
  134. explain: 'Сначала система узнаёт имя (идентификация), затем проверяет подлинность (аутентификация), затем даёт права (авторизация).'
  135. }
  136. ];
  137. // ---------- СОСТОЯНИЕ ----------
  138. const state = {
  139. screen: 'title',
  140. act: 1,
  141. scene: 0,
  142. actCompleted: { 1: false, 2: false, 3: false },
  143. quizAnswers: [],
  144. quizIndex: 0,
  145. quizScore: 0,
  146. quizDone: false,
  147. sound: true,
  148. voice: true,
  149. journal: [],
  150. audioCtx: null,
  151. };
  152. // ---------- DOM ----------
  153. const $ = (sel, ctx = document) => ctx.querySelector(sel);
  154. const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
  155. const hud = $('#hud');
  156. const hudAct = $('#hudAct');
  157. const hudFill = $('#hudFill');
  158. const tSound = $('#tSound');
  159. const tVoice = $('#tVoice');
  160. const tJournal = $('#tJournal');
  161. const journal = $('#journal');
  162. const journalBody = $('#journalBody');
  163. const jClose = $('#jClose');
  164. const scrim = $('#scrim');
  165. const screens = {
  166. title: $('#s-title'),
  167. brief: $('#s-brief'),
  168. act1: $('#s-act1'),
  169. act2: $('#s-act2'),
  170. act3: $('#s-act3'),
  171. final: $('#s-final'),
  172. quiz: $('#s-quiz'),
  173. result: $('#s-result'),
  174. };
  175. // ---------- УТИЛИТЫ ----------
  176. function showScreen(id) {
  177. Object.values(screens).forEach(s => s.classList.remove('is-active'));
  178. screens[id].classList.add('is-active');
  179. state.screen = id;
  180. hud.hidden = (id === 'title');
  181. updateHUD();
  182. window.scrollTo(0, 0);
  183. }
  184. function updateHUD() {
  185. let label = `АКТ ${state.act}`;
  186. if (state.screen === 'quiz') label = 'ТЕСТ';
  187. if (state.screen === 'result') label = 'ИТОГ';
  188. if (state.screen === 'final') label = 'ФИНАЛ';
  189. hudAct.textContent = label;
  190. const total = 4; // 3 акта + тест
  191. let completed = 0;
  192. if (state.actCompleted[1]) completed++;
  193. if (state.actCompleted[2]) completed++;
  194. if (state.actCompleted[3]) completed++;
  195. if (state.quizDone) completed++;
  196. const pct = (completed / total) * 100;
  197. hudFill.style.width = pct + '%';
  198. }
  199. function logJournal(text, type = 'info') {
  200. state.journal.push({ text, type, time: new Date().toLocaleTimeString() });
  201. if (state.journal.length > 50) state.journal.shift();
  202. renderJournal();
  203. }
  204. function renderJournal() {
  205. if (state.journal.length === 0) {
  206. journalBody.innerHTML = '<p class="journal__empty">Записей пока нет.</p>';
  207. return;
  208. }
  209. journalBody.innerHTML = state.journal.map(e => {
  210. const cls = e.type === 'ok' ? 'journal__entry--ok' : e.type === 'err' ? 'journal__entry--err' : '';
  211. return `<div class="journal__entry ${cls}">[${e.time}] ${e.text}</div>`;
  212. }).join('');
  213. journalBody.scrollTop = journalBody.scrollHeight;
  214. }
  215. // ---------- ЗВУК ----------
  216. function ensureAudio() {
  217. if (!state.audioCtx) {
  218. state.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  219. }
  220. if (state.audioCtx.state === 'suspended') state.audioCtx.resume();
  221. }
  222. function playTone(freq, duration = 0.15, type = 'sine') {
  223. if (!state.sound) return;
  224. ensureAudio();
  225. const ctx = state.audioCtx;
  226. const osc = ctx.createOscillator();
  227. const gain = ctx.createGain();
  228. osc.type = type;
  229. osc.frequency.value = freq;
  230. gain.gain.setValueAtTime(0.15, ctx.currentTime);
  231. gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
  232. osc.connect(gain).connect(ctx.destination);
  233. osc.start();
  234. osc.stop(ctx.currentTime + duration);
  235. }
  236. function speak(text) {
  237. if (!state.voice || !window.speechSynthesis) return;
  238. window.speechSynthesis.cancel();
  239. const utter = new SpeechSynthesisUtterance(text);
  240. utter.lang = 'ru-RU';
  241. utter.rate = 1;
  242. utter.pitch = 1;
  243. window.speechSynthesis.speak(utter);
  244. }
  245. // ---------- РЕНДЕР АКТОВ ----------
  246. function renderActScreen(screenId, actNum, sceneIndex) {
  247. const act = ACTS[actNum];
  248. const scene = act.scenes[sceneIndex];
  249. const screen = screens[`act${actNum}`];
  250. screen.innerHTML = `
  251. <div class="page">
  252. <div class="act-head">
  253. <span class="act-num">Акт ${actNum}</span>
  254. <h2>${act.title}</h2>
  255. </div>
  256. <div class="scene">
  257. <div class="system-bubble">
  258. <div class="system-avatar">С</div>
  259. <div class="system-text" id="sysText">${scene.system}</div>
  260. </div>
  261. <div class="choices" id="choices">
  262. ${scene.choices.map((c, i) => `
  263. <button class="choice" data-index="${i}">${c.text}</button>
  264. `).join('')}
  265. </div>
  266. <div class="feedback" id="feedback" hidden></div>
  267. <div class="edu" id="eduBlock" hidden>
  268. <div class="note">
  269. <div class="note__head"><span class="note__mark">§</span>Обучающий блок</div>
  270. <div class="note__body" id="eduText"></div>
  271. </div>
  272. <button class="btn" id="continueBtn">Продолжить</button>
  273. </div>
  274. </div>
  275. </div>
  276. `;
  277. screen.querySelectorAll('.choice').forEach(btn => {
  278. btn.addEventListener('click', () => {
  279. handleChoice(actNum, sceneIndex, parseInt(btn.dataset.index));
  280. });
  281. });
  282. speak(scene.system);
  283. }
  284. function handleChoice(actNum, sceneIndex, choiceIndex) {
  285. const act = ACTS[actNum];
  286. const scene = act.scenes[sceneIndex];
  287. const choice = scene.choices[choiceIndex];
  288. const screen = screens[`act${actNum}`];
  289. const sysText = screen.querySelector('#sysText');
  290. const choicesDiv = screen.querySelector('#choices');
  291. const feedbackDiv = screen.querySelector('#feedback');
  292. const eduBlock = screen.querySelector('#eduBlock');
  293. if (choice.correct) {
  294. choicesDiv.querySelectorAll('.choice').forEach(b => b.disabled = true);
  295. sysText.textContent = choice.feedback;
  296. playTone(880, 0.12, 'sine');
  297. logJournal(`✅ ${choice.feedback}`, 'ok');
  298. if (choice.edu) {
  299. eduBlock.hidden = false;
  300. screen.querySelector('#eduText').innerHTML = choice.edu;
  301. speak(choice.edu);
  302. const contBtn = screen.querySelector('#continueBtn');
  303. contBtn.addEventListener('click', () => nextScene());
  304. } else {
  305. feedbackDiv.hidden = false;
  306. feedbackDiv.innerHTML = `<button class="btn" id="continueBtn">Продолжить</button>`;
  307. feedbackDiv.querySelector('#continueBtn').addEventListener('click', () => nextScene());
  308. }
  309. } else {
  310. playTone(220, 0.25, 'sawtooth');
  311. sysText.textContent = choice.feedback;
  312. speak(choice.feedback);
  313. logJournal(`❌ ${choice.feedback}`, 'err');
  314. choicesDiv.querySelectorAll('.choice').forEach(b => b.disabled = true);
  315. feedbackDiv.hidden = false;
  316. feedbackDiv.innerHTML = `<button class="btn btn--small" id="retryBtn">Попробовать снова</button>`;
  317. feedbackDiv.querySelector('#retryBtn').addEventListener('click', () => {
  318. renderActScreen(`s-act${actNum}`, actNum, sceneIndex);
  319. });
  320. }
  321. }
  322. function nextScene() {
  323. const actNum = state.act;
  324. const act = ACTS[actNum];
  325. if (state.scene < act.scenes.length - 1) {
  326. state.scene++;
  327. renderActScreen(`s-act${actNum}`, actNum, state.scene);
  328. } else {
  329. state.actCompleted[actNum] = true;
  330. updateHUD();
  331. if (actNum < 3) {
  332. state.act = actNum + 1;
  333. state.scene = 0;
  334. showScreen(`act${state.act}`);
  335. renderActScreen(`s-act${state.act}`, state.act, 0);
  336. } else {
  337. showFinal();
  338. }
  339. }
  340. }
  341. // ---------- ФИНАЛЬНЫЙ ЭКРАН (ИТОГИ) ----------
  342. function showFinal() {
  343. showScreen('final');
  344. screens.final.innerHTML = `
  345. <div class="page">
  346. <div class="act-head">
  347. <span class="act-num">Финал</span>
  348. <h2>Цепочка безопасности</h2>
  349. </div>
  350. <div class="cards">
  351. <article class="card">
  352. <h3>Идентификация</h3>
  353. <p>«Кто ты?» — ввод логина, предъявление имени. Система узнаёт, как вас зовут.</p>
  354. </article>
  355. <article class="card">
  356. <h3>Аутентификация</h3>
  357. <p>«Докажи!» — ввод пароля, отпечаток, СМС. Система убеждается, что это действительно вы.</p>
  358. </article>
  359. <article class="card">
  360. <h3>Авторизация</h3>
  361. <p>«Что тебе можно?» — права доступа, роль, чтение/запись. Система решает, что вам разрешено.</p>
  362. </article>
  363. </div>
  364. <div class="note">
  365. <div class="note__head"><span class="note__mark">§</span>Главный вывод</div>
  366. <div class="note__body">
  367. <p><b>Безопасность строится на цепочке: Идентификация → Аутентификация → Авторизация.</b> Разрыв любого звена ведёт к уязвимости.</p>
  368. </div>
  369. </div>
  370. <div class="actions">
  371. <button class="btn btn--big" id="toQuiz">Итоговое Тестирование</button>
  372. </div>
  373. </div>
  374. `;
  375. screens.final.querySelector('#toQuiz').addEventListener('click', startQuiz);
  376. }
  377. // ---------- ТЕСТ ----------
  378. function startQuiz() {
  379. state.quizAnswers = [];
  380. state.quizIndex = 0;
  381. state.quizScore = 0;
  382. state.quizDone = false;
  383. showScreen('quiz');
  384. renderQuizQuestion();
  385. }
  386. function renderQuizQuestion() {
  387. const q = QUIZ[state.quizIndex];
  388. screens.quiz.innerHTML = `
  389. <div class="page">
  390. <div class="act-head">
  391. <span class="act-num">Тест</span>
  392. <h2>Вопрос ${state.quizIndex + 1} из ${QUIZ.length}</h2>
  393. </div>
  394. <div class="quiz-question">
  395. <h3>${q.q}</h3>
  396. <div class="quiz-options" id="quizOptions">
  397. ${q.options.map((opt, i) => `
  398. <button class="quiz-option" data-index="${i}">${opt}</button>
  399. `).join('')}
  400. </div>
  401. <div class="quiz-feedback" id="quizFeedback" hidden></div>
  402. </div>
  403. </div>
  404. `;
  405. screens.quiz.querySelectorAll('.quiz-option').forEach(btn => {
  406. btn.addEventListener('click', () => handleQuizAnswer(parseInt(btn.dataset.index)));
  407. });
  408. }
  409. function handleQuizAnswer(index) {
  410. const q = QUIZ[state.quizIndex];
  411. const buttons = screens.quiz.querySelectorAll('.quiz-option');
  412. buttons.forEach(b => b.disabled = true);
  413. const feedback = screens.quiz.querySelector('#quizFeedback');
  414. const isCorrect = index === q.correct;
  415. if (isCorrect) {
  416. state.quizScore++;
  417. playTone(880, 0.12, 'sine');
  418. buttons[index].classList.add('correct');
  419. } else {
  420. playTone(220, 0.2, 'sawtooth');
  421. buttons[index].classList.add('wrong');
  422. buttons[q.correct].classList.add('correct');
  423. }
  424. feedback.hidden = false;
  425. feedback.innerHTML = `<b>${isCorrect ? 'Верно!' : 'Неверно.'}</b> ${q.explain}`;
  426. state.quizAnswers.push({ question: q.q, correct: isCorrect });
  427. const nextBtn = document.createElement('button');
  428. nextBtn.className = 'btn';
  429. nextBtn.textContent = state.quizIndex < QUIZ.length - 1 ? 'Следующий вопрос' : 'Показать результат';
  430. nextBtn.style.marginTop = '1rem';
  431. nextBtn.addEventListener('click', () => {
  432. if (state.quizIndex < QUIZ.length - 1) {
  433. state.quizIndex++;
  434. renderQuizQuestion();
  435. } else {
  436. showResult();
  437. }
  438. });
  439. feedback.appendChild(nextBtn);
  440. }
  441. function showResult() {
  442. state.quizDone = true;
  443. updateHUD();
  444. showScreen('result');
  445. const score = state.quizScore;
  446. const total = QUIZ.length;
  447. const percent = Math.round((score / total) * 100);
  448. let rank = 'Стажёр';
  449. if (percent >= 90) rank = 'Агент безопасности';
  450. else if (percent >= 70) rank = 'Сотрудник';
  451. else if (percent >= 50) rank = 'Практикант';
  452. else rank = 'Требуется переподготовка';
  453. screens.result.innerHTML = `
  454. <div class="page">
  455. <div class="act-head">
  456. <span class="act-num">Результат</span>
  457. <h2>Золотой пропуск</h2>
  458. </div>
  459. <div class="result-pass">
  460. <p>Банк «Кристалл» · отдел информационной безопасности</p>
  461. <h3>ПРОПУСК №1957-К</h3>
  462. <p>Выдан: Алексею Иванову</p>
  463. <p class="chain">Идентификация → Аутентификация → Авторизация</p>
  464. <p class="score">${score} из ${total} (${percent}%)</p>
  465. <p>Звание: <b>${rank}</b></p>
  466. </div>
  467. <div class="note">
  468. <div class="note__head"><span class="note__mark">§</span>Шпаргалка</div>
  469. <div class="note__body">
  470. <p><b>Идентификация</b> — «Кто ты?» (Ввод логина, предъявление имени).</p>
  471. <p><b>Аутентификация</b> — «Докажи!» (Ввод пароля, отпечаток, СМС).</p>
  472. <p><b>Авторизация</b> — «Что тебе можно?» (Права доступа, роль, чтение/запись).</p>
  473. <p><b>Главный вывод:</b> Безопасность строится на цепочке: Идентификация → Аутентификация → Авторизация. Разрыв любого звена ведёт к уязвимости.</p>
  474. </div>
  475. </div>
  476. <div class="actions">
  477. <button class="btn" id="restartBtn">Пройти заново</button>
  478. </div>
  479. </div>
  480. `;
  481. screens.result.querySelector('#restartBtn').addEventListener('click', () => {
  482. location.reload();
  483. });
  484. logJournal(`📋 Тест завершён: ${score}/${total}`, 'ok');
  485. }
  486. // ---------- ИНИЦИАЛИЗАЦИЯ ----------
  487. function init() {
  488. hud.hidden = true;
  489. showScreen('title');
  490. $('#startBtn').addEventListener('click', () => {
  491. ensureAudio();
  492. showScreen('brief');
  493. });
  494. $('#toAct1').addEventListener('click', () => {
  495. state.act = 1;
  496. state.scene = 0;
  497. showScreen('act1');
  498. renderActScreen('s-act1', 1, 0);
  499. });
  500. tSound.addEventListener('click', () => {
  501. state.sound = !state.sound;
  502. tSound.setAttribute('aria-pressed', state.sound);
  503. tSound.textContent = state.sound ? 'звук' : 'звук ✕';
  504. });
  505. tVoice.addEventListener('click', () => {
  506. state.voice = !state.voice;
  507. tVoice.setAttribute('aria-pressed', state.voice);
  508. tVoice.textContent = state.voice ? 'голос' : 'голос ✕';
  509. if (!state.voice) window.speechSynthesis.cancel();
  510. });
  511. tJournal.addEventListener('click', () => {
  512. journal.classList.add('is-open');
  513. scrim.hidden = false;
  514. });
  515. jClose.addEventListener('click', closeJournal);
  516. scrim.addEventListener('click', closeJournal);
  517. function closeJournal() {
  518. journal.classList.remove('is-open');
  519. scrim.hidden = true;
  520. }
  521. updateHUD();
  522. }
  523. document.addEventListener('DOMContentLoaded', init);