|
|
@@ -1 +1,973 @@
|
|
|
-<head>
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="ru">
|
|
|
+<head>
|
|
|
+<meta charset="UTF-8">
|
|
|
+<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
|
+<meta name="theme-color" content="#1a1a1e">
|
|
|
+<meta name="apple-mobile-web-app-capable" content="yes">
|
|
|
+<title>Detector Master — Resanta DM-100</title>
|
|
|
+<link rel="manifest" href='data:application/manifest+json,{"name":"Detector Master DM-100","short_name":"DM-100","start_url":".","display":"standalone","background_color":"%231a1a1e","theme_color":"%231a1a1e","icons":[]}'>
|
|
|
+<style>
|
|
|
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
+ :root {
|
|
|
+ --bg: #1a1a1e;
|
|
|
+ --panel: #25252b;
|
|
|
+ --panel2: #2e2e35;
|
|
|
+ --border: #3a3a42;
|
|
|
+ --text: #e5e5e7;
|
|
|
+ --muted: #9a9aa3;
|
|
|
+ --yellow: #ffd23f;
|
|
|
+ --green: #4ade80;
|
|
|
+ --red: #ef4444;
|
|
|
+ --orange: #f97316;
|
|
|
+ --blue: #38bdf8;
|
|
|
+ }
|
|
|
+ html, body { height: 100%; }
|
|
|
+ body {
|
|
|
+ background: var(--bg);
|
|
|
+ color: var(--text);
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
|
+ line-height: 1.5;
|
|
|
+ overflow-x: hidden;
|
|
|
+ -webkit-font-smoothing: antialiased;
|
|
|
+ }
|
|
|
+ #progress-bar {
|
|
|
+ position: fixed; top: 0; left: 0; right: 0; height: 4px;
|
|
|
+ background: #0f0f12; z-index: 100;
|
|
|
+ }
|
|
|
+ #progress-fill {
|
|
|
+ height: 100%; width: 0%;
|
|
|
+ background: linear-gradient(90deg, var(--yellow), var(--green));
|
|
|
+ transition: width .4s ease;
|
|
|
+ }
|
|
|
+ #app { padding: 28px 16px 80px; max-width: 720px; margin: 0 auto; }
|
|
|
+ .screen { display: none; animation: fadeIn .4s ease; }
|
|
|
+ .screen.active { display: block; }
|
|
|
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
|
|
+
|
|
|
+ h1 { font-size: 1.7rem; line-height: 1.2; margin-bottom: 12px; }
|
|
|
+ h2 { font-size: 1.35rem; margin-bottom: 10px; color: var(--yellow); }
|
|
|
+ h3 { font-size: 1.05rem; margin: 16px 0 8px; }
|
|
|
+ p { margin-bottom: 12px; color: var(--text); }
|
|
|
+ .muted { color: var(--muted); font-size: .92rem; }
|
|
|
+ .tag {
|
|
|
+ display: inline-block; padding: 2px 10px; border-radius: 999px;
|
|
|
+ background: var(--panel2); font-size: .78rem; color: var(--muted);
|
|
|
+ margin-bottom: 12px; letter-spacing: .04em; text-transform: uppercase;
|
|
|
+ }
|
|
|
+ .tag.yellow { color: var(--yellow); }
|
|
|
+ .card {
|
|
|
+ background: var(--panel); border: 1px solid var(--border);
|
|
|
+ border-radius: 14px; padding: 16px; margin-bottom: 14px;
|
|
|
+ }
|
|
|
+ .card.hint { border-left: 3px solid var(--yellow); background: #2a281a; }
|
|
|
+ .card.warn { border-left: 3px solid var(--red); background: #2a1f1f; }
|
|
|
+ .card.ok { border-left: 3px solid var(--green); background: #1e2a22; }
|
|
|
+
|
|
|
+ button {
|
|
|
+ font-family: inherit; font-size: 1rem; cursor: pointer;
|
|
|
+ border: none; border-radius: 12px; padding: 14px 20px;
|
|
|
+ transition: transform .1s, opacity .2s, background .2s;
|
|
|
+ -webkit-tap-highlight-color: transparent;
|
|
|
+ }
|
|
|
+ button:active { transform: scale(.97); }
|
|
|
+ button:disabled { opacity: .35; cursor: not-allowed; transform: none; }
|
|
|
+ .btn-primary {
|
|
|
+ background: var(--yellow); color: #1a1a1e; font-weight: 700;
|
|
|
+ width: 100%; margin-top: 8px;
|
|
|
+ }
|
|
|
+ .btn-secondary {
|
|
|
+ background: var(--panel2); color: var(--text); border: 1px solid var(--border);
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ===== WELCOME ===== */
|
|
|
+ .hero { text-align: center; padding: 20px 0 10px; }
|
|
|
+ .hero-icon {
|
|
|
+ width: 110px; height: 110px; margin: 0 auto 20px;
|
|
|
+ border-radius: 24px;
|
|
|
+ background: linear-gradient(145deg, #35353d, #1c1c22);
|
|
|
+ border: 2px solid var(--border);
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ font-size: 2.6rem;
|
|
|
+ box-shadow: 0 0 0 0 rgba(255,210,63,.4);
|
|
|
+ animation: pulse 2.2s infinite;
|
|
|
+ }
|
|
|
+ @keyframes pulse {
|
|
|
+ 0% { box-shadow: 0 0 0 0 rgba(255,210,63,.4); }
|
|
|
+ 70% { box-shadow: 0 0 0 22px rgba(255,210,63,0); }
|
|
|
+ 100% { box-shadow: 0 0 0 0 rgba(255,210,63,0); }
|
|
|
+ }
|
|
|
+ .feature-list { list-style: none; margin: 20px 0; }
|
|
|
+ .feature-list li {
|
|
|
+ padding: 10px 0; border-bottom: 1px solid var(--border);
|
|
|
+ display: flex; gap: 10px; align-items: flex-start;
|
|
|
+ }
|
|
|
+ .feature-list li:last-child { border-bottom: none; }
|
|
|
+ .feature-list .ico { color: var(--yellow); flex-shrink: 0; }
|
|
|
+
|
|
|
+ /* ===== WORKBENCH / WALL ===== */
|
|
|
+ .workbench-slot { margin: 16px 0; min-height: 220px; }
|
|
|
+ #workbench { width: 100%; }
|
|
|
+ .wall {
|
|
|
+ position: relative; width: 100%; height: 180px;
|
|
|
+ background:
|
|
|
+ repeating-linear-gradient(90deg, #2c2c33 0 30px, #28282e 30px 60px),
|
|
|
+ linear-gradient(180deg, #2c2c33, #232328);
|
|
|
+ border-radius: 14px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ overflow: hidden;
|
|
|
+ touch-action: none;
|
|
|
+ user-select: none;
|
|
|
+ cursor: crosshair;
|
|
|
+ box-shadow: inset 0 4px 20px rgba(0,0,0,.5);
|
|
|
+ }
|
|
|
+ .wall-object {
|
|
|
+ position: absolute; top: 0; bottom: 0; width: 9%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ border-radius: 6px;
|
|
|
+ opacity: .18;
|
|
|
+ transition: opacity .3s, box-shadow .3s;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+ .wall.show-objects .wall-object { opacity: .35; }
|
|
|
+ .wall-object.found {
|
|
|
+ opacity: .9 !important;
|
|
|
+ box-shadow: 0 0 18px 4px currentColor;
|
|
|
+ }
|
|
|
+ .wall-object[data-id="armature"] { background: var(--orange); color: var(--orange); }
|
|
|
+ .wall-object[data-id="pipe"] { background: #f59e0b; color: #f59e0b; }
|
|
|
+ .wall-object[data-id="cable"] { background: var(--red); color: var(--red); }
|
|
|
+ .wall-object[data-id="wood"] { background: #a16207; color: #a16207; }
|
|
|
+ .wall-object .obj-label {
|
|
|
+ position: absolute; bottom: 6px; left: 50%; transform: translateX(-50%);
|
|
|
+ font-size: .7rem; color: #fff; white-space: nowrap;
|
|
|
+ background: rgba(0,0,0,.7); padding: 2px 6px; border-radius: 4px;
|
|
|
+ opacity: 0; transition: opacity .2s;
|
|
|
+ }
|
|
|
+ .wall.show-objects .wall-object .obj-label,
|
|
|
+ .wall-object.found .obj-label { opacity: 1; }
|
|
|
+
|
|
|
+ /* ===== DETECTOR ===== */
|
|
|
+ .detector {
|
|
|
+ position: absolute; top: 50%; left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ width: 130px;
|
|
|
+ background: linear-gradient(160deg, #3a3a44, #1e1e24);
|
|
|
+ border: 2px solid #4a4a55;
|
|
|
+ border-radius: 16px;
|
|
|
+ padding: 10px 8px 8px;
|
|
|
+ box-shadow: 0 8px 20px rgba(0,0,0,.5);
|
|
|
+ z-index: 10;
|
|
|
+ transition: left .06s linear;
|
|
|
+ }
|
|
|
+ .detector-display {
|
|
|
+ background: #0b1a0e;
|
|
|
+ border: 1px solid #1e3a24;
|
|
|
+ border-radius: 8px;
|
|
|
+ color: var(--green);
|
|
|
+ font-family: "SF Mono", Menlo, Consolas, monospace;
|
|
|
+ font-size: .72rem;
|
|
|
+ text-align: center;
|
|
|
+ padding: 6px 4px;
|
|
|
+ min-height: 42px;
|
|
|
+ display: flex; flex-direction: column; justify-content: center; gap: 2px;
|
|
|
+ box-shadow: inset 0 0 8px rgba(0,255,100,.15);
|
|
|
+ }
|
|
|
+ .detector-display .d-mode { font-size: .6rem; color: #6b9c74; letter-spacing: .05em; }
|
|
|
+ .detector-display .d-main { font-weight: 700; letter-spacing: .03em; }
|
|
|
+ .detector-display.alert { color: var(--yellow); border-color: #4a3d1a; background: #1a1608; }
|
|
|
+ .detector-display.danger { color: var(--red); border-color: #4a1e1e; background: #1a0808; }
|
|
|
+ .detector-buttons {
|
|
|
+ display: flex; gap: 6px; margin-top: 8px;
|
|
|
+ }
|
|
|
+ .det-btn {
|
|
|
+ flex: 1; padding: 8px 0; font-size: .78rem; font-weight: 700;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #2a2a32; color: var(--text);
|
|
|
+ border: 1px solid #444;
|
|
|
+ letter-spacing: .02em;
|
|
|
+ }
|
|
|
+ .det-btn:active { background: #1a1a20; transform: scale(.94); }
|
|
|
+ .det-btn[data-btn="power"] { color: var(--red); }
|
|
|
+ .det-btn[data-btn="cal"] { color: var(--yellow); }
|
|
|
+ .det-btn[data-btn="mode"] { color: var(--blue); }
|
|
|
+ .det-btn.active { box-shadow: 0 0 0 2px currentColor inset; }
|
|
|
+
|
|
|
+ /* ===== TOAST ===== */
|
|
|
+ #toast {
|
|
|
+ position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(120%);
|
|
|
+ background: var(--panel2); color: var(--text);
|
|
|
+ padding: 12px 20px; border-radius: 12px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ font-size: .9rem; z-index: 200;
|
|
|
+ box-shadow: 0 8px 24px rgba(0,0,0,.5);
|
|
|
+ transition: transform .35s ease;
|
|
|
+ max-width: 90vw; text-align: center;
|
|
|
+ }
|
|
|
+ #toast.show { transform: translateX(-50%) translateY(0); }
|
|
|
+ #toast.success { border-color: var(--green); color: var(--green); }
|
|
|
+ #toast.error { border-color: var(--red); color: var(--red); }
|
|
|
+ #toast.info { border-color: var(--yellow); color: var(--yellow); }
|
|
|
+
|
|
|
+ /* ===== QUIZ ===== */
|
|
|
+ .question { margin-bottom: 22px; }
|
|
|
+ .question h3 { font-size: 1rem; color: var(--text); margin-bottom: 12px; font-weight: 600; }
|
|
|
+ .options { display: flex; flex-direction: column; gap: 8px; }
|
|
|
+ .option {
|
|
|
+ display: flex; align-items: flex-start; gap: 10px;
|
|
|
+ padding: 12px 14px; border-radius: 10px;
|
|
|
+ background: var(--panel2); border: 1px solid var(--border);
|
|
|
+ cursor: pointer; transition: border-color .2s, background .2s;
|
|
|
+ font-size: .95rem;
|
|
|
+ }
|
|
|
+ .option:hover { border-color: #555; }
|
|
|
+ .option input { margin-top: 3px; accent-color: var(--yellow); flex-shrink: 0; }
|
|
|
+ .option.correct { border-color: var(--green); background: #1e2a22; }
|
|
|
+ .option.wrong { border-color: var(--red); background: #2a1f1f; }
|
|
|
+ .option.reveal { border-color: var(--green); background: #1e2a22; }
|
|
|
+ .explain {
|
|
|
+ margin-top: 8px; font-size: .85rem; color: var(--muted);
|
|
|
+ padding: 8px 12px; background: #1a1a20; border-radius: 8px;
|
|
|
+ border-left: 2px solid var(--yellow);
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+ .explain.show { display: block; }
|
|
|
+
|
|
|
+ /* ===== RESULTS ===== */
|
|
|
+ .result-score {
|
|
|
+ text-align: center; font-size: 3rem; font-weight: 800;
|
|
|
+ margin: 20px 0 8px;
|
|
|
+ }
|
|
|
+ .result-score.good { color: var(--green); }
|
|
|
+ .result-score.mid { color: var(--yellow); }
|
|
|
+ .result-score.bad { color: var(--red); }
|
|
|
+
|
|
|
+ .nav-btns { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; }
|
|
|
+ .task-indicator {
|
|
|
+ font-size: .85rem; color: var(--muted);
|
|
|
+ text-align: center; margin-top: 10px;
|
|
|
+ }
|
|
|
+ .task-indicator.done { color: var(--green); }
|
|
|
+
|
|
|
+ @media (max-width: 420px) {
|
|
|
+ #app { padding: 24px 12px 80px; }
|
|
|
+ h1 { font-size: 1.4rem; }
|
|
|
+ .detector { width: 116px; }
|
|
|
+ }
|
|
|
+</style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+
|
|
|
+<div id="progress-bar"><div id="progress-fill"></div></div>
|
|
|
+
|
|
|
+<main id="app">
|
|
|
+
|
|
|
+ <!-- ============ WELCOME ============ -->
|
|
|
+ <section class="screen active" id="screen-welcome">
|
|
|
+ <div class="hero">
|
|
|
+ <div class="hero-icon">📡</div>
|
|
|
+ <h1>Detector Master</h1>
|
|
|
+ <p class="muted">Resanta DM-100 — интерактивный симулятор</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card">
|
|
|
+ <p>Вы — мастер. Перед сверлением стены нужно проверить её на скрытые объекты. У вас в руках <b>Resanta DM-100</b> — детектор с тремя кнопками управления.</p>
|
|
|
+ <p class="muted">За 7 минут вы научитесь включать прибор, калибровать его, переключать режимы и находить арматуру, трубы, кабель под напряжением и дерево.</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <ul class="feature-list">
|
|
|
+ <li><span class="ico">⏻</span><span>Кнопка питания — включение и выключение</span></li>
|
|
|
+ <li><span class="ico">🎯</span><span>Кнопка CAL — калибровка под фон стены</span></li>
|
|
|
+ <li><span class="ico">🔄</span><span>Кнопка M — режимы: Металл → Проводка → Дерево</span></li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <div class="nav-btns">
|
|
|
+ <button class="btn-primary" onclick="go('m1')">Начать обучение</button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- ============ MODULE 1 ============ -->
|
|
|
+ <section class="screen" id="screen-m1">
|
|
|
+ <span class="tag yellow">Модуль 1 · 1 минута</span>
|
|
|
+ <h2>Включение прибора</h2>
|
|
|
+ <p>Детектор лежит на столе. На корпусе три кнопки. Чтобы начать работу, <b>нажмите кнопку питания ⏻</b>.</p>
|
|
|
+ <div class="card hint">
|
|
|
+ Короткое нажатие включает прибор. Долгое удержание (1,5 с) выключает его.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="workbench-slot" data-screen="m1"></div>
|
|
|
+
|
|
|
+ <div class="task-indicator" id="task-m1">Задача: включите прибор</div>
|
|
|
+ <div class="nav-btns">
|
|
|
+ <button class="btn-primary next-btn" disabled onclick="go('m2')">Далее</button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- ============ MODULE 2 ============ -->
|
|
|
+ <section class="screen" id="screen-m2">
|
|
|
+ <span class="tag yellow">Модуль 2 · 1,5 минуты</span>
|
|
|
+ <h2>Калибровка — обязательный шаг</h2>
|
|
|
+ <p>После включения прибор нужно откалибровать. Прижмите детектор к <b>пустому участку стены</b> (перетащите его мышью или пальцем) и <b>удерживайте кнопку CAL</b> ~1,5 секунды.</p>
|
|
|
+ <div class="card warn">
|
|
|
+ ⚠️ Не калибруйте над объектом — прибор «ослепнет» и перестанет видеть реальные цели. На стене видны подсказки, где спрятаны объекты.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="workbench-slot" data-screen="m2"></div>
|
|
|
+
|
|
|
+ <div class="task-indicator" id="task-m2">Задача: откалибруйте прибор на пустом участке</div>
|
|
|
+ <div class="nav-btns">
|
|
|
+ <button class="btn-primary next-btn" disabled onclick="go('m3')">Далее</button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- ============ MODULE 3 ============ -->
|
|
|
+ <section class="screen" id="screen-m3">
|
|
|
+ <span class="tag yellow">Модуль 3 · 2 минуты</span>
|
|
|
+ <h2>Переключение режимов</h2>
|
|
|
+ <p>Вы собираетесь сверлить стену. Чтобы убедиться, что под точкой нет <b>кабеля под напряжением</b>, переключите режим на <b>«Проводка»</b> кнопкой M.</p>
|
|
|
+ <div class="card hint">
|
|
|
+ 🔄 Кнопка M циклически переключает режимы: <b>Металл → Проводка → Дерево</b>. Режим отображается на экране прибора.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="workbench-slot" data-screen="m3"></div>
|
|
|
+
|
|
|
+ <div class="task-indicator" id="task-m3">Задача: включите режим «Проводка»</div>
|
|
|
+ <div class="nav-btns">
|
|
|
+ <button class="btn-primary next-btn" disabled onclick="go('m4')">Далее</button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- ============ MODULE 4 ============ -->
|
|
|
+ <section class="screen" id="screen-m4">
|
|
|
+ <span class="tag yellow">Модуль 4 · 2 минуты</span>
|
|
|
+ <h2>Поиск в действии</h2>
|
|
|
+ <p>Перетаскивайте детектор по стене. Частота звука растёт по мере приближения к объекту. Найдено: <b><span id="found-count">0</span> / 4</b>.</p>
|
|
|
+ <div class="card hint">
|
|
|
+ 💡 Не забудьте: арматура и труба ищутся в режиме «Металл», кабель — в «Проводка», дерево — в «Дерево». Переключайтесь кнопкой M.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="workbench-slot" data-screen="m4"></div>
|
|
|
+
|
|
|
+ <div class="task-indicator" id="task-m4">Задача: найдите все 4 объекта</div>
|
|
|
+ <div class="nav-btns">
|
|
|
+ <button class="btn-primary next-btn" disabled onclick="go('m5')">Далее</button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- ============ MODULE 5 ============ -->
|
|
|
+ <section class="screen" id="screen-m5">
|
|
|
+ <span class="tag yellow">Модуль 5 · 30 секунд</span>
|
|
|
+ <h2>Завершение работы</h2>
|
|
|
+ <p>Поиск завершён. Чтобы выключить прибор и сэкономить батарею, <b>удерживайте кнопку питания ⏻</b> около 1,5 секунды.</p>
|
|
|
+ <div class="card hint">
|
|
|
+ ⏻ Прибор автоматически выключится через 5 минут бездействия, но лучше выключать вручную.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="workbench-slot" data-screen="m5"></div>
|
|
|
+
|
|
|
+ <div class="task-indicator" id="task-m5">Задача: выключите прибор удержанием кнопки питания</div>
|
|
|
+ <div class="nav-btns">
|
|
|
+ <button class="btn-primary next-btn" disabled onclick="go('quiz')">Итоговое тестирование</button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- ============ QUIZ ============ -->
|
|
|
+ <section class="screen" id="screen-quiz">
|
|
|
+ <span class="tag yellow">Итоговое тестирование</span>
|
|
|
+ <h2>Проверка знаний</h2>
|
|
|
+ <p class="muted">7 вопросов. Выберите один вариант в каждом. После отправки вы увидите результат и пояснения.</p>
|
|
|
+ <form id="quiz-form"></form>
|
|
|
+ <div class="nav-btns">
|
|
|
+ <button class="btn-primary" id="submit-quiz">Проверить ответы</button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- ============ RESULTS ============ -->
|
|
|
+ <section class="screen" id="screen-results">
|
|
|
+ <span class="tag yellow">Результат</span>
|
|
|
+ <h2>Тестирование завершено</h2>
|
|
|
+ <div class="result-score" id="result-score">0/7</div>
|
|
|
+ <p class="muted" id="result-msg" style="text-align:center;"></p>
|
|
|
+ <div class="card" id="result-details"></div>
|
|
|
+ <div class="nav-btns">
|
|
|
+ <button class="btn-primary" onclick="restart()">Пройти заново</button>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+</main>
|
|
|
+
|
|
|
+<div id="toast"></div>
|
|
|
+
|
|
|
+<script>
|
|
|
+/* =========================================================
|
|
|
+ STATE
|
|
|
+ ========================================================= */
|
|
|
+const MODES = ['metal', 'ac', 'wood'];
|
|
|
+const MODE_LABEL = { metal: 'МЕТАЛЛ', ac: 'ПРОВОДКА', wood: 'ДЕРЕВО' };
|
|
|
+const MODE_ICON = { metal: 'Fe/Cu', ac: '⚡AC', wood: '🌳' };
|
|
|
+
|
|
|
+const OBJECTS = [
|
|
|
+ { id: 'armature', x: 18, depth: 100, label: 'Арматура Fe · 100 мм', needMode: 'metal' },
|
|
|
+ { id: 'pipe', x: 42, depth: 80, label: 'Труба Cu · 80 мм', needMode: 'metal' },
|
|
|
+ { id: 'cable', x: 66, depth: 50, label: 'Кабель AC · 50 мм', needMode: 'ac' },
|
|
|
+ { id: 'wood', x: 88, depth: 20, label: 'Дерево · 20 мм', needMode: 'wood' }
|
|
|
+];
|
|
|
+
|
|
|
+const state = {
|
|
|
+ poweredOn: false,
|
|
|
+ calibrated: false,
|
|
|
+ mode: 'metal',
|
|
|
+ found: new Set(),
|
|
|
+ currentScreen: 'welcome',
|
|
|
+ scanningActive: false
|
|
|
+};
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ AUDIO (Web Audio API)
|
|
|
+ ========================================================= */
|
|
|
+let audioCtx = null;
|
|
|
+function ensureAudio() {
|
|
|
+ if (!audioCtx) {
|
|
|
+ try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
|
|
|
+ catch(e) { /* no audio */ }
|
|
|
+ }
|
|
|
+ if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
|
|
|
+}
|
|
|
+function beep(freq = 800, dur = 0.05, vol = 0.06) {
|
|
|
+ ensureAudio();
|
|
|
+ if (!audioCtx) return;
|
|
|
+ const osc = audioCtx.createOscillator();
|
|
|
+ const gain = audioCtx.createGain();
|
|
|
+ osc.type = 'square';
|
|
|
+ osc.frequency.value = freq;
|
|
|
+ gain.gain.value = vol;
|
|
|
+ osc.connect(gain).connect(audioCtx.destination);
|
|
|
+ const t = audioCtx.currentTime;
|
|
|
+ osc.start(t);
|
|
|
+ gain.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
|
|
+ osc.stop(t + dur);
|
|
|
+}
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ DOM REFS
|
|
|
+ ========================================================= */
|
|
|
+const workbench = document.createElement('div');
|
|
|
+workbench.id = 'workbench';
|
|
|
+workbench.innerHTML = `
|
|
|
+ <div class="wall" id="wall">
|
|
|
+ ${OBJECTS.map(o => `
|
|
|
+ <div class="wall-object" data-id="${o.id}" style="left:${o.x}%">
|
|
|
+ <span class="obj-label">${o.label}</span>
|
|
|
+ </div>
|
|
|
+ `).join('')}
|
|
|
+ <div class="detector" id="detector">
|
|
|
+ <div class="detector-display" id="det-display">
|
|
|
+ <div class="d-mode" id="d-mode">OFF</div>
|
|
|
+ <div class="d-main" id="d-main">—</div>
|
|
|
+ </div>
|
|
|
+ <div class="detector-buttons">
|
|
|
+ <button class="det-btn" data-btn="power" title="Питание">⏻</button>
|
|
|
+ <button class="det-btn" data-btn="mode" title="Режим">M</button>
|
|
|
+ <button class="det-btn" data-btn="cal" title="Калибровка">CAL</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+`;
|
|
|
+document.body.appendChild(workbench);
|
|
|
+
|
|
|
+const wall = workbench.querySelector('#wall');
|
|
|
+const detectorEl = workbench.querySelector('#detector');
|
|
|
+const dMode = workbench.querySelector('#d-mode');
|
|
|
+const dMain = workbench.querySelector('#d-main');
|
|
|
+const dDisplay = workbench.querySelector('#det-display');
|
|
|
+const btnPower = workbench.querySelector('[data-btn="power"]');
|
|
|
+const btnMode = workbench.querySelector('[data-btn="mode"]');
|
|
|
+const btnCal = workbench.querySelector('[data-btn="cal"]');
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ HELPERS
|
|
|
+ ========================================================= */
|
|
|
+function showToast(msg, type = 'info', ms = 2200) {
|
|
|
+ const t = document.getElementById('toast');
|
|
|
+ t.textContent = msg;
|
|
|
+ t.className = 'show ' + type;
|
|
|
+ clearTimeout(t._timer);
|
|
|
+ t._timer = setTimeout(() => t.className = '', ms);
|
|
|
+}
|
|
|
+
|
|
|
+function getDetX() {
|
|
|
+ const left = parseFloat(detectorEl.style.left) || 50;
|
|
|
+ return left;
|
|
|
+}
|
|
|
+function setDetX(x) {
|
|
|
+ detectorEl.style.left = Math.max(6, Math.min(94, x)) + '%';
|
|
|
+}
|
|
|
+setDetX(50);
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ NAVIGATION
|
|
|
+ ========================================================= */
|
|
|
+const SCREENS = ['welcome','m1','m2','m3','m4','m5','quiz','results'];
|
|
|
+
|
|
|
+function go(id) {
|
|
|
+ document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
|
|
+ const scr = document.getElementById('screen-' + id);
|
|
|
+ if (!scr) return;
|
|
|
+ scr.classList.add('active');
|
|
|
+ state.currentScreen = id;
|
|
|
+ window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
|
+
|
|
|
+ // move workbench into active screen if needed
|
|
|
+ const slot = scr.querySelector('.workbench-slot');
|
|
|
+ if (slot) {
|
|
|
+ slot.appendChild(workbench);
|
|
|
+ // toggle object visibility
|
|
|
+ if (id === 'm2') wall.classList.add('show-objects');
|
|
|
+ else wall.classList.remove('show-objects');
|
|
|
+
|
|
|
+ // module-specific setup
|
|
|
+ if (id === 'm4') {
|
|
|
+ state.found.clear();
|
|
|
+ document.querySelectorAll('.wall-object').forEach(el => el.classList.remove('found'));
|
|
|
+ updateFoundCount();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ updateProgress();
|
|
|
+ checkAdvance();
|
|
|
+ updateDetectorUI();
|
|
|
+
|
|
|
+ // start/stop scanning loop
|
|
|
+ if (id === 'm4' && state.poweredOn && state.calibrated) startScanLoop();
|
|
|
+ else stopScanLoop();
|
|
|
+}
|
|
|
+
|
|
|
+function updateProgress() {
|
|
|
+ const idx = SCREENS.indexOf(state.currentScreen);
|
|
|
+ const pct = idx <= 0 ? 0 : (idx / (SCREENS.length - 1)) * 100;
|
|
|
+ document.getElementById('progress-fill').style.width = pct + '%';
|
|
|
+}
|
|
|
+
|
|
|
+function checkAdvance() {
|
|
|
+ const scr = document.getElementById('screen-' + state.currentScreen);
|
|
|
+ if (!scr) return;
|
|
|
+ const btn = scr.querySelector('.next-btn');
|
|
|
+ if (!btn) return;
|
|
|
+ let ok = false;
|
|
|
+ switch (state.currentScreen) {
|
|
|
+ case 'm1': ok = state.poweredOn; break;
|
|
|
+ case 'm2': ok = state.calibrated; break;
|
|
|
+ case 'm3': ok = state.mode === 'ac'; break;
|
|
|
+ case 'm4': ok = state.found.size === OBJECTS.length; break;
|
|
|
+ case 'm5': ok = !state.poweredOn; break;
|
|
|
+ }
|
|
|
+ btn.disabled = !ok;
|
|
|
+
|
|
|
+ // task indicator
|
|
|
+ const ind = document.getElementById('task-' + state.currentScreen);
|
|
|
+ if (ind) {
|
|
|
+ if (ok) { ind.classList.add('done'); ind.textContent = '✅ Выполнено'; }
|
|
|
+ else {
|
|
|
+ ind.classList.remove('done');
|
|
|
+ if (state.currentScreen === 'm1') ind.textContent = 'Задача: включите прибор';
|
|
|
+ if (state.currentScreen === 'm2') ind.textContent = 'Задача: откалибруйте прибор на пустом участке';
|
|
|
+ if (state.currentScreen === 'm3') ind.textContent = 'Задача: включите режим «Проводка»';
|
|
|
+ if (state.currentScreen === 'm4') ind.textContent = 'Задача: найдите все 4 объекта';
|
|
|
+ if (state.currentScreen === 'm5') ind.textContent = 'Задача: выключите прибор удержанием кнопки питания';
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ DETECTOR UI
|
|
|
+ ========================================================= */
|
|
|
+function updateDetectorUI(signal) {
|
|
|
+ if (!state.poweredOn) {
|
|
|
+ dDisplay.className = 'detector-display';
|
|
|
+ dMode.textContent = 'OFF';
|
|
|
+ dMain.textContent = '—';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ dMode.textContent = MODE_LABEL[state.mode];
|
|
|
+ dDisplay.className = 'detector-display';
|
|
|
+ if (!state.calibrated) {
|
|
|
+ dMain.textContent = 'CAL?';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (signal) {
|
|
|
+ dDisplay.className = 'detector-display alert';
|
|
|
+ if (signal.obj.id === 'cable') dDisplay.className = 'detector-display danger';
|
|
|
+ dMain.textContent = MODE_ICON[state.mode] + ' ' + Math.round(100 - signal.dist * 7) + '%';
|
|
|
+ } else {
|
|
|
+ dMain.textContent = '—';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ SIGNAL LOGIC
|
|
|
+ ========================================================= */
|
|
|
+function getSignal(detX) {
|
|
|
+ let best = null;
|
|
|
+ for (const obj of OBJECTS) {
|
|
|
+ if (state.mode !== obj.needMode) continue;
|
|
|
+ const dist = Math.abs(detX - obj.x);
|
|
|
+ if (dist < 12) {
|
|
|
+ if (!best || dist < best.dist) best = { obj, dist };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return best;
|
|
|
+}
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ POWER BUTTON
|
|
|
+ ========================================================= */
|
|
|
+let powerHoldTimer = null;
|
|
|
+let powerPressStart = 0;
|
|
|
+
|
|
|
+btnPower.addEventListener('pointerdown', (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ ensureAudio();
|
|
|
+ powerPressStart = Date.now();
|
|
|
+ if (state.poweredOn) {
|
|
|
+ // hold to turn off
|
|
|
+ clearTimeout(powerHoldTimer);
|
|
|
+ powerHoldTimer = setTimeout(() => {
|
|
|
+ togglePower(false);
|
|
|
+ }, 1500);
|
|
|
+ }
|
|
|
+});
|
|
|
+btnPower.addEventListener('pointerup', (e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ clearTimeout(powerHoldTimer);
|
|
|
+ if (!state.poweredOn && Date.now() - powerPressStart < 1500) {
|
|
|
+ togglePower(true);
|
|
|
+ }
|
|
|
+});
|
|
|
+btnPower.addEventListener('pointerleave', () => clearTimeout(powerHoldTimer));
|
|
|
+btnPower.addEventListener('pointercancel', () => clearTimeout(powerHoldTimer));
|
|
|
+
|
|
|
+function togglePower(on) {
|
|
|
+ state.poweredOn = on;
|
|
|
+ if (!on) {
|
|
|
+ state.calibrated = false;
|
|
|
+ beep(400, 0.15, 0.08);
|
|
|
+ showToast('Прибор выключен', 'info');
|
|
|
+ stopScanLoop();
|
|
|
+ } else {
|
|
|
+ beep(1000, 0.1, 0.08);
|
|
|
+ showToast('Прибор включён', 'success');
|
|
|
+ }
|
|
|
+ updateDetectorUI();
|
|
|
+ checkAdvance();
|
|
|
+}
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ MODE BUTTON
|
|
|
+ ========================================================= */
|
|
|
+btnMode.addEventListener('click', (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ if (!state.poweredOn) {
|
|
|
+ showToast('Сначала включите прибор', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const idx = MODES.indexOf(state.mode);
|
|
|
+ state.mode = MODES[(idx + 1) % MODES.length];
|
|
|
+ beep(500, 0.04, 0.05);
|
|
|
+ updateDetectorUI();
|
|
|
+ checkAdvance();
|
|
|
+});
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ CALIBRATION BUTTON
|
|
|
+ ========================================================= */
|
|
|
+let calHoldTimer = null;
|
|
|
+btnCal.addEventListener('pointerdown', (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ ensureAudio();
|
|
|
+ if (!state.poweredOn) {
|
|
|
+ showToast('Сначала включите прибор', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ clearTimeout(calHoldTimer);
|
|
|
+ dMain.textContent = 'CAL…';
|
|
|
+ calHoldTimer = setTimeout(() => {
|
|
|
+ const x = getDetX();
|
|
|
+ const overObject = OBJECTS.some(o => Math.abs(x - o.x) < 9);
|
|
|
+ if (overObject) {
|
|
|
+ beep(300, 0.25, 0.1);
|
|
|
+ showToast('Ошибка: калибровка над объектом!', 'error');
|
|
|
+ dMain.textContent = 'ERR';
|
|
|
+ } else {
|
|
|
+ state.calibrated = true;
|
|
|
+ beep(900, 0.08, 0.08);
|
|
|
+ setTimeout(() => beep(1200, 0.08, 0.08), 100);
|
|
|
+ showToast('Калибровка успешна', 'success');
|
|
|
+ dMain.textContent = 'OK';
|
|
|
+ checkAdvance();
|
|
|
+ if (state.currentScreen === 'm4') startScanLoop();
|
|
|
+ }
|
|
|
+ }, 1500);
|
|
|
+});
|
|
|
+btnCal.addEventListener('pointerup', (e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ clearTimeout(calHoldTimer);
|
|
|
+ if (state.poweredOn && !state.calibrated) dMain.textContent = 'CAL?';
|
|
|
+});
|
|
|
+btnCal.addEventListener('pointerleave', () => clearTimeout(calHoldTimer));
|
|
|
+btnCal.addEventListener('pointercancel', () => clearTimeout(calHoldTimer));
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ WALL DRAG
|
|
|
+ ========================================================= */
|
|
|
+let dragging = false;
|
|
|
+wall.addEventListener('pointerdown', (e) => {
|
|
|
+ if (e.target.closest('.detector')) return;
|
|
|
+ dragging = true;
|
|
|
+ wall.setPointerCapture(e.pointerId);
|
|
|
+ moveDetectorTo(e);
|
|
|
+});
|
|
|
+wall.addEventListener('pointermove', (e) => {
|
|
|
+ if (!dragging) return;
|
|
|
+ moveDetectorTo(e);
|
|
|
+});
|
|
|
+wall.addEventListener('pointerup', () => dragging = false);
|
|
|
+wall.addEventListener('pointercancel', () => dragging = false);
|
|
|
+
|
|
|
+function moveDetectorTo(e) {
|
|
|
+ const rect = wall.getBoundingClientRect();
|
|
|
+ const x = ((e.clientX - rect.left) / rect.width) * 100;
|
|
|
+ setDetX(x);
|
|
|
+}
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ SCAN LOOP (module 4)
|
|
|
+ ========================================================= */
|
|
|
+let scanTimeout = null;
|
|
|
+function startScanLoop() {
|
|
|
+ if (scanTimeout) return;
|
|
|
+ scanLoop();
|
|
|
+}
|
|
|
+function stopScanLoop() {
|
|
|
+ clearTimeout(scanTimeout);
|
|
|
+ scanTimeout = null;
|
|
|
+}
|
|
|
+function scanLoop() {
|
|
|
+ if (state.currentScreen !== 'm4' || !state.poweredOn || !state.calibrated) {
|
|
|
+ scanTimeout = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const x = getDetX();
|
|
|
+ const sig = getSignal(x);
|
|
|
+ let delay = 400;
|
|
|
+ if (sig) {
|
|
|
+ const rate = 1 - sig.dist / 12;
|
|
|
+ beep(500 + rate * 700, 0.04, 0.03 + rate * 0.08);
|
|
|
+ delay = Math.max(120, 400 - rate * 300);
|
|
|
+ updateDetectorUI(sig);
|
|
|
+ // mark found
|
|
|
+ if (!state.found.has(sig.obj.id)) {
|
|
|
+ state.found.add(sig.obj.id);
|
|
|
+ document.querySelector(`.wall-object[data-id="${sig.obj.id}"]`).classList.add('found');
|
|
|
+ updateFoundCount();
|
|
|
+ showToast('Найдено: ' + sig.obj.label, 'success', 1600);
|
|
|
+ checkAdvance();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ updateDetectorUI(null);
|
|
|
+ }
|
|
|
+ scanTimeout = setTimeout(scanLoop, delay);
|
|
|
+}
|
|
|
+
|
|
|
+function updateFoundCount() {
|
|
|
+ const el = document.getElementById('found-count');
|
|
|
+ if (el) el.textContent = state.found.size;
|
|
|
+}
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ QUIZ
|
|
|
+ ========================================================= */
|
|
|
+const QUIZ = [
|
|
|
+ {
|
|
|
+ q: "Что нужно сделать в первую очередь после включения детектора, прежде чем искать объекты?",
|
|
|
+ opts: [
|
|
|
+ "Переключить режим на «Проводка»",
|
|
|
+ "Прижать к пустому участку стены и нажать/удержать кнопку калибровки",
|
|
|
+ "Отключить звук"
|
|
|
+ ],
|
|
|
+ correct: 1,
|
|
|
+ explain: "Калибровка настраивает прибор под фон стены (бетон, кирпич) и устраняет ложные срабатывания."
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: "Вы ищете кабель под напряжением. Какой режим нужно выбрать кнопкой M?",
|
|
|
+ opts: [
|
|
|
+ "«Металл»",
|
|
|
+ "«Проводка» (AC)",
|
|
|
+ "«Дерево»"
|
|
|
+ ],
|
|
|
+ correct: 1,
|
|
|
+ explain: "Режим «Проводка» настроен на электромагнитное поле кабеля под напряжением."
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: "Прибор в режиме «Металл» пищит над арматурой. Вы собираетесь сверлить. Что нужно сделать?",
|
|
|
+ opts: [
|
|
|
+ "Сразу сверлить — это же арматура",
|
|
|
+ "Переключить режим на «Проводка» и проверить место снова",
|
|
|
+ "Нажать калибровку прямо над арматурой"
|
|
|
+ ],
|
|
|
+ correct: 1,
|
|
|
+ explain: "Чтобы убедиться в отсутствии кабеля под напряжением, нужен режим «Проводка». Калибровка над объектом — ошибка."
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: "Какова максимальная глубина обнаружения чёрных металлов (арматуры) детектором DM-100?",
|
|
|
+ opts: [
|
|
|
+ "20 мм",
|
|
|
+ "50 мм",
|
|
|
+ "100 мм"
|
|
|
+ ],
|
|
|
+ correct: 2,
|
|
|
+ explain: "Чёрные металлы обнаруживаются на глубине до 100 мм."
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: "Как отключить звуковой сигнал, если он мешает?",
|
|
|
+ opts: [
|
|
|
+ "Выключить и снова включить прибор",
|
|
|
+ "Использовать комбинацию нижних кнопок (удержание одной + нажатие другой)",
|
|
|
+ "Кнопкой M"
|
|
|
+ ],
|
|
|
+ correct: 1,
|
|
|
+ explain: "В DM-100 предусмотрено отключение звука комбинацией кнопок."
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: "Через сколько минут бездействия детектор автоматически выключается?",
|
|
|
+ opts: [
|
|
|
+ "1 минута",
|
|
|
+ "5 минут",
|
|
|
+ "30 минут"
|
|
|
+ ],
|
|
|
+ correct: 1,
|
|
|
+ explain: "Автовыключение предусмотрено через 5 минут отсутствия активности."
|
|
|
+ },
|
|
|
+ {
|
|
|
+ q: "Что означает учащающийся звуковой сигнал при поиске?",
|
|
|
+ opts: [
|
|
|
+ "Прибор разряжается",
|
|
|
+ "Детектор приближается к объекту",
|
|
|
+ "Произошла ошибка калибровки"
|
|
|
+ ],
|
|
|
+ correct: 1,
|
|
|
+ explain: "Частота сигнала возрастает по мере приближения к цели — это главный навигационный инструмент."
|
|
|
+ }
|
|
|
+];
|
|
|
+
|
|
|
+function renderQuiz() {
|
|
|
+ const form = document.getElementById('quiz-form');
|
|
|
+ form.innerHTML = QUIZ.map((q, i) => `
|
|
|
+ <div class="question" data-q="${i}">
|
|
|
+ <h3>${i + 1}. ${q.q}</h3>
|
|
|
+ <div class="options">
|
|
|
+ ${q.opts.map((opt, j) => `
|
|
|
+ <label class="option" data-opt="${j}">
|
|
|
+ <input type="radio" name="q${i}" value="${j}">
|
|
|
+ <span>${opt}</span>
|
|
|
+ </label>
|
|
|
+ `).join('')}
|
|
|
+ </div>
|
|
|
+ <div class="explain" id="explain-${i}"></div>
|
|
|
+ </div>
|
|
|
+ `).join('');
|
|
|
+}
|
|
|
+
|
|
|
+document.getElementById('submit-quiz').addEventListener('click', () => {
|
|
|
+ let score = 0;
|
|
|
+ QUIZ.forEach((q, i) => {
|
|
|
+ const selected = document.querySelector(`input[name="q${i}"]:checked`);
|
|
|
+ const qEl = document.querySelector(`.question[data-q="${i}"]`);
|
|
|
+ const explain = document.getElementById(`explain-${i}`);
|
|
|
+ qEl.querySelectorAll('.option').forEach(o => o.classList.remove('correct', 'wrong', 'reveal'));
|
|
|
+
|
|
|
+ if (selected) {
|
|
|
+ const val = parseInt(selected.value, 10);
|
|
|
+ const chosenEl = qEl.querySelector(`.option[data-opt="${val}"]`);
|
|
|
+ if (val === q.correct) {
|
|
|
+ score++;
|
|
|
+ chosenEl.classList.add('correct');
|
|
|
+ } else {
|
|
|
+ chosenEl.classList.add('wrong');
|
|
|
+ qEl.querySelector(`.option[data-opt="${q.correct}"]`).classList.add('reveal');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ qEl.querySelector(`.option[data-opt="${q.correct}"]`).classList.add('reveal');
|
|
|
+ }
|
|
|
+ explain.textContent = q.explain;
|
|
|
+ explain.classList.add('show');
|
|
|
+ });
|
|
|
+
|
|
|
+ showResults(score);
|
|
|
+});
|
|
|
+
|
|
|
+function showResults(score) {
|
|
|
+ const total = QUIZ.length;
|
|
|
+ const scoreEl = document.getElementById('result-score');
|
|
|
+ scoreEl.textContent = score + ' / ' + total;
|
|
|
+ scoreEl.className = 'result-score ' + (score >= 6 ? 'good' : score >= 4 ? 'mid' : 'bad');
|
|
|
+
|
|
|
+ const msg = document.getElementById('result-msg');
|
|
|
+ if (score === total) msg.textContent = 'Отлично! Вы полностью освоили прибор.';
|
|
|
+ else if (score >= 6) msg.textContent = 'Хороший результат. Ознакомьтесь с пояснениями к ошибкам.';
|
|
|
+ else if (score >= 4) msg.textContent = 'Есть пробелы. Рекомендуем повторить модули 2 и 4.';
|
|
|
+ else msg.textContent = 'Стоит пройти обучение заново.';
|
|
|
+
|
|
|
+ const details = document.getElementById('result-details');
|
|
|
+ details.innerHTML = QUIZ.map((q, i) => {
|
|
|
+ const sel = document.querySelector(`input[name="q${i}"]:checked`);
|
|
|
+ const chosen = sel ? parseInt(sel.value, 10) : -1;
|
|
|
+ const ok = chosen === q.correct;
|
|
|
+ return `<p style="margin-bottom:8px;font-size:.9rem;">
|
|
|
+ <b style="color:${ok ? 'var(--green)' : 'var(--red)'}">${ok ? '✓' : '✗'} Вопрос ${i + 1}.</b>
|
|
|
+ ${ok ? 'Верно.' : 'Правильный ответ: ' + q.opts[q.correct]}
|
|
|
+ </p>`;
|
|
|
+ }).join('');
|
|
|
+
|
|
|
+ go('results');
|
|
|
+}
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ RESTART
|
|
|
+ ========================================================= */
|
|
|
+function restart() {
|
|
|
+ state.poweredOn = false;
|
|
|
+ state.calibrated = false;
|
|
|
+ state.mode = 'metal';
|
|
|
+ state.found.clear();
|
|
|
+ setDetX(50);
|
|
|
+ document.querySelectorAll('.wall-object').forEach(el => el.classList.remove('found'));
|
|
|
+ document.querySelectorAll('.option').forEach(o => o.classList.remove('correct', 'wrong', 'reveal'));
|
|
|
+ document.querySelectorAll('.explain').forEach(e => e.classList.remove('show'));
|
|
|
+ document.getElementById('quiz-form').innerHTML = '';
|
|
|
+ renderQuiz();
|
|
|
+ stopScanLoop();
|
|
|
+ updateDetectorUI();
|
|
|
+ go('welcome');
|
|
|
+}
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ INIT
|
|
|
+ ========================================================= */
|
|
|
+renderQuiz();
|
|
|
+updateDetectorUI();
|
|
|
+updateProgress();
|
|
|
+
|
|
|
+// Prevent accidental page zoom on double tap of buttons
|
|
|
+document.addEventListener('dblclick', (e) => {
|
|
|
+ if (e.target.closest('button')) e.preventDefault();
|
|
|
+}, { passive: false });
|
|
|
+
|
|
|
+// Resume audio on first user gesture
|
|
|
+document.addEventListener('pointerdown', ensureAudio, { once: true });
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|