`

Левая полусистема (Дв. 1,2)

ОТБОР ВОЗД. 1
ОТБОР ВОЗД. 2
II КОНТУР 1
II КОНТУР 2
РАСХОД (Лев)
${panelState.tempL}°
Т°С ХОЛ. ЛИНИЯ (Лев)

Показания приборов

0.0
УРВК (Лев)
0.0
УРВК (Прав)
КОЛЬЦЕВАНИЕ
Температура в кабинах
+${panelState.tempCrew}°
КЭ
+${panelState.tempCargo}°
Гр. каб.

Правая полусистема (Дв. 3,4)

ОТБОР ВОЗД. 3
ОТБОР ВОЗД. 4
II КОНТУР 3
II КОНТУР 4
РАСХОД (Прав)
${panelState.tempR}°
Т°С ХОЛ. ЛИНИЯ (Прав)
Система: Ожидание действий экипажа. Переключите тумблеры, чтобы увидеть реакцию системы кондиционирования.

Пояснения к панели

ОТБОР ВОЗДУХА — открывает клапан отбора горячего воздуха от двигателя (за XI ступенью компрессора).

II КОНТУР — управляет заслонкой продува воздуховоздушного радиатора (ВВР). В положении «Авт» заслонка открывается автоматически для охлаждения. В положении «Откр» — принудительно открыта (используется на больших высотах, чтобы не раскрутить турбохолодильник).

РАСХОД — переключатель режимов регулятора расхода воздуха (АРВП). «Автомат» — нормальный расход (3800 кг/ч), «Мен/Бол» — ручное уменьшение/увеличение (Форсаж — 4750 кг/ч).

УРВК — указатель расхода воздуха в кабине (в условных единицах). Норма: 4,3–5,1.

КОЛЬЦЕВАНИЕ — объединяет левую и правую полусистемы. Позволяет работать одной полусистеме на обе кабины в случае отказа другой.

`; updateURVK(); } /* =============== ЛОГИКА ПРИЛОЖЕНИЯ =============== */ let state = { tab:'panel', section:null, query:'', tool:'calc', calc:{h:10, pAf:760, mode:'normal'}, timer:{scenario:'cargo_main', height:10, active:false, startTs:0, targets:null, intervalId:null}, quiz:{ started:false, idx:0, selected:null, score:0, order:[] } }; const app = document.getElementById('app'); function setTab(tab){ state.tab = tab; state.section = null; render(); window.scrollTo(0,0); } function openSection(id){ state.tab = 'sections'; state.section = id; render(); window.scrollTo(0,0); } function backToList(){ state.section = null; render(); window.scrollTo(0,0); } function render(){ if(state.tab === 'panel'){ renderPanel(); } else if(state.tab === 'sections'){ state.section ? renderSection() : renderList(); } else if(state.tab === 'tools'){ renderTools(); } else if(state.tab === 'quiz'){ renderQuiz(); } document.querySelectorAll('nav button').forEach(b=>{ b.classList.toggle('active', b.dataset.tab === state.tab); }); } /* ---- РАЗДЕЛЫ ---- */ function renderList(){ app.innerHTML = `
${sections.map(s=>`
${s.icon}

${s.title}

${s.desc}

`).join('')}
`; } function renderSection(){ const s = sections.find(x=>x.id === state.section); if(!s) return backToList(); app.innerHTML = `
${s.html}
`; } /* ---- ИНСТРУМЕНТЫ (Калькулятор / Таймеры) ---- */ function renderTools(){ app.innerHTML = `
`; state.tool === 'calc' ? renderCalc() : renderTimer(); } function setTool(t){ state.tool = t; renderTools(); } function renderCalc(){ const c = state.calc; const pOut = pressureAt(c.h * 1000); let cab; if(c.mode === 'normal') cab = cabinNormal(c.h * 1000, c.pAf); else if(c.mode === 'bolevoy') cab = cabinBolevoy(c.h * 1000, c.pAf); else cab = cabinAft(c.h * 1000); const cabAlt = altitudeAt(cab.P); const perepadKg = cab.perepad / KGCM2_MMHG; let badge = `НОРМА`; if(perepadKg > 0.6) badge = `ОПАСНЫЙ ПЕРЕПАД`; else if(perepadKg > 0.55) badge = `ПОВЫШ. ПЕРЕПАД`; let oxy = ''; if(cabAlt > 4500) oxy = `
ВЫСОТА В КАБИНЕ > 4,5 км — мигает «ВЫСОТА В КАБ. 4,5 км».
`; else if(cabAlt > 3870) oxy = `
Высота в кабине > 3,87 км — загорается «ДЫШИ КИСЛОРОД.».
`; app.querySelector('#tool-body').innerHTML = `
Атмосферное P
${pOut.toFixed(0)} мм
P в кабине
${cab.P.toFixed(0)} мм
Перепад
${perepadKg.toFixed(2)} кгс/см²
${badge}
Высота в кабине
${cabAlt.toFixed(0)} м
${oxy}

График 6.8-2 / 6.8-3

`; drawChart(c.h, c.pAf, c.mode); } function drawChart(hl, pAf, mode){ const cv = document.getElementById('chart'); if(!cv) return; const W = cv.width, H = cv.height; const ctx = cv.getContext('2d'); const L=54, R=20, T=14, B=36; const pw = W-L-R, ph = H-T-B; const xMax = 14, yMax = 800; const xS = h => L + (h/xMax)*pw; const yS = p => T + (1 - p/yMax)*ph; ctx.clearRect(0,0,W,H); ctx.strokeStyle = '#dbe3ea'; ctx.lineWidth = 1; for(let p=0;p<=yMax;p+=100){ ctx.beginPath();ctx.moveTo(L,yS(p));ctx.lineTo(W-R,yS(p));ctx.stroke(); ctx.fillStyle='#657786';ctx.font='11px sans-serif';ctx.textAlign='right'; ctx.fillText(p, L-4, yS(p)+3); } for(let h=0;h<=xMax;h+=2){ ctx.beginPath();ctx.moveTo(xS(h),T);ctx.lineTo(xS(h),H-B);ctx.stroke(); ctx.fillStyle='#657786';ctx.textAlign='center'; ctx.fillText(h, xS(h), H-B+14); } ctx.fillStyle='#657786';ctx.textAlign='center'; ctx.fillText('Высота полёта, км', W/2, H-4); ctx.save();ctx.translate(12,(T+ph/2));ctx.rotate(-Math.PI/2); ctx.fillText('Давление, мм рт.ст.',0,0);ctx.restore(); function plot(fn,color){ ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.beginPath(); let first = true; for(let i=0;i<=280;i++){ const h = i*0.05, P = fn(h), x = xS(h), y = yS(P); if(first){ctx.moveTo(x,y);first=false;} else ctx.lineTo(x,y); } ctx.stroke(); } plot(h=>pressureAt(h*1000), '#15202b'); plot(h=>cabinNormal(h*1000, pAf).P, '#0b5cad'); plot(h=>cabinBolevoy(h*1000, pAf).P, '#c62828'); plot(h=>cabinAft(h*1000).P, '#2e7d32'); const curP = mode==='normal' ? cabinNormal(hl*1000,pAf).P : mode==='bolevoy' ? cabinBolevoy(hl*1000,pAf).P : cabinAft(hl*1000).P; ctx.fillStyle='#ff9800'; ctx.beginPath();ctx.arc(xS(hl), yS(curP), 5, 0, Math.PI*2);ctx.fill(); ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.stroke(); } function renderTimer(){ const t = state.timer; const target = interpolateTimes(t.scenario, t.height); const channels = Object.keys(target).filter(k=>k!=='h'); const tbody = app.querySelector('#tool-body'); tbody.innerHTML = `
${t.active ? `` : ``}
`; const bars = tbody.querySelector('#timer-bars'); bars.innerHTML = channels.map(ch=>{ const val = target[ch]; const label = {t:'Кабина', ke:'КЭ', gr:'Грузовая', aft:'Кормовая'}[ch] || ch; return `
${label}
${val == null ? '—' : val.toFixed(0)+' с'}
`; }).join(''); if(t.active) tickTimer(); } function startTimer(){ const t = state.timer; const target = interpolateTimes(t.scenario, t.height); const targets = {}; Object.keys(target).forEach(k=>{ if(k !== 'h' && target[k] != null) targets[k] = target[k]; }); t.targets = targets; t.active = true; t.startTs = Date.now(); t.intervalId = setInterval(tickTimer, 100); renderTimer(); } function tickTimer(){ const t = state.timer; if(!t.active) return; const elapsed = (Date.now() - t.startTs) / 1000; let allDone = true; Object.entries(t.targets).forEach(([ch, sec])=>{ const remain = Math.max(0, sec - elapsed); if(remain > 0) allDone = false; const bar = document.getElementById('bar_'+ch); const num = document.getElementById('num_'+ch); const row = document.getElementById('tr_'+ch); if(bar){ bar.style.width = (sec > 0 ? (remain/sec)*100 : 0) + '%'; } if(num) num.textContent = remain.toFixed(1) + ' с'; if(row && remain === 0) row.classList.add('done'); }); if(allDone){ clearInterval(t.intervalId); t.active = false; beep(); if(navigator.vibrate) navigator.vibrate([300,120,300,120,300]); renderTimer(); } } function stopTimer(){ const t = state.timer; if(t.intervalId) clearInterval(t.intervalId); t.active = false; renderTimer(); } function beep(){ try{ const AC = window.AudioContext || window.webkitAudioContext; if(!AC) return; const ctx = new AC(); [880,1100,880].forEach((f,i)=>{ const o = ctx.createOscillator(), g = ctx.createGain(); o.connect(g); g.connect(ctx.destination); o.frequency.value = f; g.gain.value = 0.25; o.start(ctx.currentTime + i*0.25); o.stop(ctx.currentTime + i*0.25 + 0.2); }); }catch(e){} } const timers = { cargo_main:{name:"Разгерметизация грузовой (основная)",times:[{h:5.5,t:25},{h:10,t:70}]}, cockpit_cargo:{name:"КЭ + грузовая (основная)",times:[{h:5.5,ke:40,gr:48},{h:10,ke:70,gr:82}]}, bolevoy:{name:"Включение боевого перепада (0,5→0,2)",times:[{h:8,ke:40,gr:60,aft:32}]} }; function interpolateTimes(scenarioId,h){ const s=timers[scenarioId],times=s.times; if(times.length===1) return times[0]; if(h<=times[0].h) return times[0]; if(h>=times[times.length-1].h) return times[times.length-1]; for(let i=0;i=times[i].h && h<=times[i+1].h){ const a=times[i],b=times[i+1],t=(h-a.h)/(b.h-a.h),out={h}; const keys=new Set([...Object.keys(a),...Object.keys(b)]); keys.forEach(k=>{ if(k==='h')return; const va=a[k],vb=b[k]; if(va==null||vb==null) out[k]=va??vb; else out[k]=va+(vb-va)*t; }); return out; } } return times[0]; } /* ---- КВИЗ ---- */ function renderQuiz(){ const q = state.quiz; if(!q.started) return renderQuizIntro(); if(q.idx >= q.order.length) return renderQuizResult(); return renderQuizQuestion(); } function renderQuizIntro(){ app.innerHTML = `
🎯

Тренажёр по действиям при отказах

${quizQuestions.length} вопросов по СКВ и САРД.
Выберите правильное действие — получите объяснение.

`; } function startQuiz(){ const order = quizQuestions.map((_,i)=>i).sort(()=>Math.random()-0.5); state.quiz = { started:true, idx:0, selected:null, score:0, order }; renderQuiz(); } function renderQuizQuestion(){ const q = state.quiz; const total = q.order.length; const qi = q.order[q.idx]; const question = quizQuestions[qi]; const progress = ((q.idx) / total) * 100; const selected = q.selected; const answered = selected !== null; app.innerHTML = `
Вопрос ${q.idx+1} из ${total}

${question.q}

${question.options.map((opt,i)=>{ let cls = 'quiz-opt'; if(answered){ if(i === question.correct) cls += ' correct'; else if(i === selected) cls += ' wrong'; } return ``; }).join('')} ${answered ? `
${selected === question.correct ? '✓ Верно.' : '✗ Неверно.'} ${question.explain}
` : ''}
`; } function selectAnswer(i){ const q = state.quiz; if(q.selected !== null) return; q.selected = i; const qi = q.order[q.idx]; if(i === quizQuestions[qi].correct) q.score++; renderQuizQuestion(); } function nextQuestion(){ const q = state.quiz; q.idx++; q.selected = null; if(q.idx >= q.order.length){ renderQuizResult(); } else renderQuizQuestion(); } function renderQuizResult(){ const q = state.quiz; const total = q.order.length; const pct = Math.round((q.score / total) * 100); let cat, cls; if(pct >= 90){ cat = 'Отлично'; cls = 'ok'; } else if(pct >= 70){ cat = 'Хорошо'; cls = 'ok'; } else if(pct >= 50){ cat = 'Удовлетворительно'; cls = 'warn'; } else { cat = 'Требуется повторение'; cls = 'bad'; } app.innerHTML = `
${pct >= 70 ? '🎉' : '📚'}
${q.score}/${total}
правильных ответов
${cat} (${pct}%)
`; } /* ---- ШРИФТ ---- */ function changeFont(d){ const cur = parseFloat(localStorage.getItem('il76_font') || '16'); const next = Math.min(22, Math.max(13, cur + d)); localStorage.setItem('il76_font', next); document.documentElement.style.setProperty('--font-size', next + 'px'); } const sf = localStorage.getItem('il76_font'); if(sf) document.documentElement.style.setProperty('--font-size', sf + 'px'); /* старт */ render(); ДАВЛЕНИЯ» — 0,18 мм рт.ст./с"}, {id:"sard_boev",text:"Регулятор боевого перепада — 0,2 кгс/см²"}, {id:"sard_vyp",text:"«УПРАВЛ. ВЫПУСКН. КЛАПАНАМИ» — «АВТОМАТ»"}, {id:"sard_pred",text:"«УПРАВЛ. ПРЕДОХР. КЛАПАНОМ» — «ОСНОВН. ДАТЧ.» (чёрные колпачки)"}, {id:"sard_boev_vyk",text:"«БОЕВОЙ ПЕРЕПАД» — «ОТКЛ.» (красный колпачок)"}, {id:"sard_razg",text:"«КАБИНЫ ЭКИПАЖ. ГРУЗОВАЯ РАЗГЕРМЕТИЗ.» — «ОТКЛ.»"}, {id:"sard_avar",text:"«АВАРИЙНАЯ РАЗГЕРМ. САМОЛЕТА» и «АВАРИЙНАЯ РАЗГЕРМ. ГРУЗОВОЙ КАБИНЫ» — «ОТКЛ.»"}, {id:"sard_mezh",text:"«МЕЖКАБ. ЗАСЛОН.» — «ОТКЛ.»"}, {id:"sard_priv",text:"«ГЕРМЕТ. ПРИВОДН.» — нейтраль (красный колпачок)"}, {id:"sard_uvpd",text:"УВПД: КЭ/грузовая — «ВЫСОТА» = аэродром, «ПЕРЕПАД» = «0»; кормовая — ΔР ≤ 0,035"}, {id:"sard_var",text:"Вариометр кабины (грузовой) — на «0»"} ]}, { title:"Перед рулением / взлётом", items:[ {id:"pre_otb",text:"«ОТБОР ВОЗДУХА» 1,2 (левая) и 3,4 (правая) — «ВКЛ.»"}, {id:"pre_urvk",text:"УРВК: КЭ 4,3–5,1; кормовая 5,0–7,5 усл.ед."}, {id:"pre_doors",text:"Люки, двери, форточки во всех гермокабинах закрыты; люк в полу кабины летчиков закрыт"}, {id:"pre_dok",text:"Доклад стрелка о поступлении воздуха и расходе по УРВК"}, {id:"pre_off",text:"Перед взлётом отключить отбор от двух симметричных двигателей (или всех без обледенения)"}, {id:"pre_hot",text:"При +30 °С и выше: руление с 2 полусистемами от всех, продув ВВР II контура открыт; перед взлётом отбор отключить"} ]} ]; /* =============== ТАЙМЕРЫ =============== */ const timers = { cargo_main:{name:"Разгерметизация грузовой (основная)",times:[{h:5.5,t:25},{h:10,t:70}]}, cockpit_cargo:{name:"КЭ + грузовая (основная)",times:[{h:5.5,ke:40,gr:48},{h:10,ke:70,gr:82}]}, cargo_emerg:{name:"Аварийная разгерметизация грузовой",times:[{h:5.5,t:17},{h:10,t:40}]}, aircraft_emerg:{name:"Аварийная разгерметизация самолёта",times:[{h:5.5,ke:14,gr:15},{h:10,ke:22,aft:18}]}, bolevoy:{name:"Включение боевого перепада (0,5→0,2)",times:[{h:8,ke:40,gr:60,aft:32}]}, aft_depress:{name:"Разгерметизация кормовой",times:[{h:10,t:18}]} }; function interpolateTimes(scenarioId,h){ const s=timers[scenarioId],times=s.times; if(times.length===1) return times[0]; if(h<=times[0].h) return times[0]; if(h>=times[times.length-1].h) return times[times.length-1]; for(let i=0;i=times[i].h && h<=times[i+1].h){ const a=times[i],b=times[i+1],t=(h-a.h)/(b.h-a.h),out={h}; const keys=new Set([...Object.keys(a),...Object.keys(b)]); keys.forEach(k=>{ if(k==='h')return; const va=a[k],vb=b[k]; if(va==null||vb==null) out[k]=va??vb; else out[k]=va+(vb-va)*t; }); return out; } } return times[0]; } /* =============== ВОПРОСЫ КВИЗА =============== */ const quizQuestions = [ { q:"Загорелось «ОТКАЗ ОТБОРА» + речевая «КОНДИЦИОНИРОВАНИЕ ПРОВЕРИТЬ!». Первое действие?", options:["Отключить отбор от двигателя с неисправной системой","Открыть заслонку кольцевания","Продолжать полёт без изменений"], correct:0, explain:"Сначала отключают отбор от неисправного двигателя («ОТКЛ.»). Затем проверяют, что табло погасло, и при необходимости открывают кольцевание." }, { q:"После отключения одной полусистемы СКВ можно ли открывать кольцевание?", options:["Нет, запрещается","Да, обязательно","Да, только если упал расход"], correct:0, explain:"После отключения одной полусистемы открывать заслонку кольцевания запрещается — это прямое указание в ИПЭ." }, { q:"В кабине появился дым и запах гари из коробов. Первое действие?", options:["Перейти на кислородное питание","Открыть кольцевание","Включить вентиляцию забортным воздухом"], correct:0, explain:"Первое — перейти на кислородное питание. Затем поочерёдным отключением отбора ищут двигатель-источник и действуют по таблице отказов." }, { q:"Мигает «ОПАСНЫЙ ПЕРЕПАД» (ΔР > 0,59 кгс/см²). Первое действие?", options:["Снизить перепад до 0,4–0,45, включив на 4–5 с разгерметизацию","Включить боевой перепад","Открыть межкабинные заслонки"], correct:0, explain:"Сначала кратковременно (4–5 с) включают разгерметизацию, чтобы снизить ΔР до 0,4–0,45 кгс/см², и отключают обе полусистемы подачи воздуха." }, { q:"Разгерметизация кабины экипажа, грузовая загерметизирована. Срочное действие?", options:["Снизить ΔР грузовой до ≤ 0,3 кгс/см² или разгерметизировать её","Просто продолжать полёт","Ничего не делать"], correct:0, explain:"Прочность гермостенки шп. № 14 рассчитана на перепад 0,5 кгс/см² только на время принятия мер. Запрещён длительный полёт с ΔР ≤ 0,3 кгс/см² со стороны грузовой." }, { q:"Как включается боевой перепад в КЭ и грузовой кабине?", options:["Одновременно выключателем «БОЕВОЙ ПЕРЕПАД» на правой внешней панели ПД","Только в грузовой","Только в кормовой"], correct:0, explain:"Выключатель «БОЕВОЙ ПЕРЕПАД» (прав. внешн. панель ПД, закрыт красным колпачком) включает перепад одновременно в КЭ и грузовой. Кормовая — отдельным переключателем на левом пульте с разрешения КВС." }, { q:"Полёт с одним неработающим двигателем. Действия по СКВ?", options:["Заслонка «КОЛЬЦЕВАНИЕ», отбор от отказавшего «ОТКЛ.»","Отключить оба двигателя полусистемы","Ничего не делать"], correct:0, explain:"Открывают кольцевание и отключают отбор от отказавшего двигателя. Загораются мнемознаки «КОЛЬЦЕВАНИЕ» и отбора отказавшего." }, { q:"УРВК > 5,1 усл.ед., расход вручную не регулируется. Действия?", options:["Отключить неисправную полусистему, исправную — в «ФОРСАЖ»","Продолжать как есть","Отключить отбор от всех двигателей"], correct:0, explain:"Неисправную полусистему отключают («РАСХОД» → «ОТКЛ.», открыть колпачок), исправную переводят в «ФОРСАЖ» (5,4–6,4 усл.ед.)." }, { q:"Вынужденная посадка на воду. Как выполняется разгерметизация?", options:["На Н < 2000 м нажать «ГЕРМЕТ. ПРИВОДН.» вверх, удерживать 0,5–6 с","Включить аварийную разгерметизацию самолёта","Открыть все двери вручную"], correct:0, explain:"Нажимной переключатель «ГЕРМЕТ. ПРИВОДН.» на правой внешней панели ПД: вверх (открыть красный колпачок), удерживать 0,5–6 с, отпустить — должен вернуться в нейтраль." }, { q:"На земле при неработающих двигателях продув станции 4ДК заслонкой «забортного» воздуха:", options:["Невозможен — заблокировано концевым «ЗЕМЛЯ-ВОЗДУХ»","Возможен","Возможен только с ВСУ"], correct:0, explain:"На главной задней левой ноге шасси стоит концевой выключатель «ЗЕМЛЯ-ВОЗДУХ», при обжатии амортизатора цепь разорвана — заслонку на земле открыть нельзя." }, { q:"Правильный порядок включения АЗР СКВ на земле?", options:["Сначала РУ21, РУ22 (переменный), затем РУ23, РУ24 (постоянный)","Наоборот","В любом порядке"], correct:0, explain:"Сначала включают АЗР переменного тока (РУ21, РУ22), затем постоянного (РУ23, РУ24). Иначе возможны ложные срабатывания защиты ТХУ." }, { q:"t в кабинах и МСК > 35 °С. Первое действие?", options:["Переключатели t горячего воздуха в полусистемах — «ХОЛ.»","Отключить отбор от всех двигателей","Включить вентиляцию забортным воздухом"], correct:0, explain:"Сначала переключатели регулирования температуры горячего воздуха — в «ХОЛ.». При 20–25 °С — в нейтраль. Если не помогает — кислород, отключение отбора от всех двигателей, снижение, вентиляция забортным." }, { q:"Нормальный эксплуатационный перепад в кабинах:", options:["0,5 ± 0,02 кгс/см²","0,2 кгс/см²","0,7 кгс/см²"], correct:0, explain:"0,5 ± 0,02 кгс/см². Боевой — 0,2 кгс/см². При 760 мм рт.ст. нормальный перепад достигается на высоте 5200 ± 300 м." }, { q:"Полёт на 12 км. «Высота» в кабинах по САРД:", options:["3200 ± 300 м","5000 м","12000 м"], correct:0, explain:"При нормально работающих СКВ и САРД «высота» в кабинах на высоте полёта 12 км соответствует 3200 ± 300 м." }, { q:"«ПЕРЕГРЕВ» + «ЗАЩИТА ТХ» одновременно. Действия?", options:["«II КОНТУР» → «ОТКР.», затем после погасания — обратно в «АВТ.»","Включить вентиляцию забортным","Отключить СКВ полностью"], correct:0, explain:"Переключатели «II КОНТУР» соответствующих двигателей — в «ОТКР.» (открыть чёрные колпачки). После погасания ламп и мнемосигнализаторов вернуть в «АВТ.»." }, { q:"Перед взлётом отбор воздуха:", options:["Отключают от двух симметрично расположенных двигателей","Включают от всех двигателей","Оставляют без изменений"], correct:0, explain:"Перед взлётом отключают отбор от двух симметричных двигателей (внутренних или внешних). При отсутствии обледенения разрешается отключать от всех." }, { q:"Разгерметизация КЭ + грузовой одновременно. Действия?", options:["Кислород, разгерметизация самолёта, контроль сигнализации, полёт по решению КВС","Закрыть все двери","Включить боевой перепад"], correct:0, explain:"Переходят на кислород, производят разгерметизацию самолёта, убеждаются в срабатывании табло. Продолжают полёт по решению КВС; при необходимости — снижение." }, { q:"Полёт с двумя неработающими двигателями. Действия по СКВ?", options:["«КОЛЬЦЕВАНИЕ», «РАСХОД» одной полусистемы «ОТКЛ.», отбор от отказавших «ОТКЛ.»","Отключить всё","Ничего"], correct:0, explain:"Открывают кольцевание, отключают «РАСХОД» одной полусистемы (открыть колпачок), отключают отбор от отказавших двигателей." }, { q:"Перепад в кабине ≥ 0,55 кгс/см². Что происходит?", options:["Мигает «ПОВЫШ. ПЕРЕПАД», автоматически включаются предохранительные устройства","Ничего","Отключается СКВ"], correct:0, explain:"При ΔР ≥ 0,55 кгс/см² начинают мигать табло «ПОВЫШ. ПЕРЕПАД», автоматически вступают в работу предохранительные устройства от перенаддува." }, { q:"Задатчик «НАЧАЛО ГЕРМЕТИЗАЦИИ» перед снижением:", options:["На 30–40 мм рт.ст. ниже давления аэродрома посадки","Оставить как было","Установить максимально возможное"], correct:0, explain:"Устанавливают на 30–40 мм рт.ст. ниже барометрического давления аэродрома посадки. Если давление аэродрома ниже 590 мм рт.ст. — в крайнее левое положение." } ]; /* =============== СОСТОЯНИЕ =============== */ let state = { tab:'sections', section:null, query:'', tool:'calc', calc:{h:10, pAf:760, mode:'normal'}, timer:{scenario:'cargo_main', height:10, active:false, startTs:0, targets:null, intervalId:null}, quiz:{ started:false, idx:0, selected:null, score:0, order:[] } }; const app = document.getElementById('app'); function stripHtml(html){ const d = document.createElement('div'); d.innerHTML = html; return d.textContent || ''; } function setTab(tab){ state.tab = tab; state.section = null; render(); window.scrollTo(0,0); } function openSection(id){ state.tab = 'sections'; state.section = id; render(); window.scrollTo(0,0); } function backToList(){ state.section = null; render(); window.scrollTo(0,0); } function render(){ if(state.tab === 'sections'){ state.section ? renderSection() : renderList(); } else if(state.tab === 'tools'){ renderTools(); } else if(state.tab === 'search'){ renderSearch(); } else if(state.tab === 'checklists'){ renderChecklists(); } else { renderQuiz(); } document.querySelectorAll('nav button').forEach(b=>{ b.classList.toggle('active', b.dataset.tab === state.tab); }); } /* ---- РАЗДЕЛЫ ---- */ function renderList(){ app.innerHTML = `
${sections.map(s=>`
${s.icon}

${s.title}

${s.desc}

`).join('')}
`; } function renderSection(){ const s = sections.find(x=>x.id === state.section); if(!s) return backToList(); app.innerHTML = `
${s.html}
`; } /* ---- ПОИСК ---- */ function renderSearch(){ app.innerHTML = `
`; doSearch(state.query); } function doSearch(q){ state.query = q; const query = q.trim().toLowerCase(); const box = document.getElementById('results'); if(!box) return; if(!query){ box.innerHTML = `

Введите слово или цифру.

`; return; } const res = sections.map(s=>{ const raw = stripHtml(s.html); const text = (s.title + ' ' + s.desc + ' ' + raw).toLowerCase(); if(text.indexOf(query) === -1) return null; const idx = raw.toLowerCase().indexOf(query); let snippet = ''; if(idx !== -1){ const a = Math.max(0, idx - 60); const b = Math.min(raw.length, idx + query.length + 100); snippet = (a>0?'…':'') + raw.slice(a,b) + (b`

${r.s.icon} ${r.s.title}

${snippet.replace(/

`).join('') : `

Ничего не найдено.

`; } /* ---- ЧЕК-ЛИСТЫ ---- */ function renderChecklists(){ let total = 0, done = 0; checkGroups.forEach(g=>g.items.forEach(i=>{ total++; if(localStorage.getItem('il76_'+i.id)==='1') done++; })); const pct = total ? Math.round(done/total*100) : 0; app.innerHTML = `

Чек-листы

Отметки сохраняются локально.

Выполнено: ${done} из ${total} (${pct}%)

${checkGroups.map(g=>`

${g.title}

${g.items.map(i=>`
`).join('')}
`).join('')}`; } function toggleCheck(id, checked){ if(checked) localStorage.setItem('il76_'+id, '1'); else localStorage.removeItem('il76_'+id); renderChecklists(); } function resetChecks(){ if(!confirm('Сбросить все отметки?')) return; checkGroups.forEach(g=>g.items.forEach(i=>localStorage.removeItem('il76_'+i.id))); renderChecklists(); } /* ---- ИНСТРУМЕНТЫ ---- */ function renderTools(){ app.innerHTML = `
`; state.tool === 'calc' ? renderCalc() : renderTimer(); } function setTool(t){ state.tool = t; renderTools(); } function renderCalc(){ const c = state.calc; const pOut = pressureAt(c.h * 1000); let cab; if(c.mode === 'normal') cab = cabinNormal(c.h * 1000, c.pAf); else if(c.mode === 'bolevoy') cab = cabinBolevoy(c.h * 1000, c.pAf); else cab = cabinAft(c.h * 1000); const cabAlt = altitudeAt(cab.P); const perepadKg = cab.perepad / KGCM2_MMHG; let badge = `НОРМА`; if(perepadKg > 0.6) badge = `ОПАСНЫЙ ПЕРЕПАД`; else if(perepadKg > 0.55) badge = `ПОВЫШ. ПЕРЕПАД`; else if(perepadKg > 0.52) badge = `выше нормы`; let oxy = ''; if(cabAlt > 4500) oxy = `
ВЫСОТА В КАБИНЕ > 4,5 км — мигает «ВЫСОТА В КАБ. 4,5 км». Опасная разгерметизация! РИ: «ОПАСНАЯ РАЗГЕРМЕТИЗАЦИЯ!»
`; else if(cabAlt > 3870) oxy = `
Высота в кабине > 3,87 км — загорается «ДЫШИ КИСЛОРОД.».
`; app.querySelector('#tool-body').innerHTML = `
Атмосферное P
${pOut.toFixed(0)} мм
P в кабине
${cab.P.toFixed(0)} мм
Перепад
${perepadKg.toFixed(2)} кгс/см²
${badge}
Высота в кабине
${cabAlt.toFixed(0)} м
${oxy}

График 6.8-2 / 6.8-3

`; drawChart(c.h, c.pAf, c.mode); } function drawChart(hl, pAf, mode){ const cv = document.getElementById('chart'); if(!cv) return; const W = cv.width, H = cv.height; const ctx = cv.getContext('2d'); const L=54, R=20, T=14, B=36; const pw = W-L-R, ph = H-T-B; const xMax = 14, yMax = 800; const xS = h => L + (h/xMax)*pw; const yS = p => T + (1 - p/yMax)*ph; ctx.clearRect(0,0,W,H); ctx.strokeStyle = '#dbe3ea'; ctx.lineWidth = 1; for(let p=0;p<=yMax;p+=100){ ctx.beginPath();ctx.moveTo(L,yS(p));ctx.lineTo(W-R,yS(p));ctx.stroke(); ctx.fillStyle='#657786';ctx.font='11px sans-serif';ctx.textAlign='right'; ctx.fillText(p, L-4, yS(p)+3); } for(let h=0;h<=xMax;h+=2){ ctx.beginPath();ctx.moveTo(xS(h),T);ctx.lineTo(xS(h),H-B);ctx.stroke(); ctx.fillStyle='#657786';ctx.textAlign='center'; ctx.fillText(h, xS(h), H-B+14); } ctx.fillStyle='#657786';ctx.textAlign='center'; ctx.fillText('Высота полёта, км', W/2, H-4); ctx.save();ctx.translate(12,(T+ph/2));ctx.rotate(-Math.PI/2); ctx.fillText('Давление, мм рт.ст.',0,0);ctx.restore(); function plot(fn,color){ ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.beginPath(); let first = true; for(let i=0;i<=280;i++){ const h = i*0.05, P = fn(h), x = xS(h), y = yS(P); if(first){ctx.moveTo(x,y);first=false;} else ctx.lineTo(x,y); } ctx.stroke(); } plot(h=>pressureAt(h*1000), '#15202b'); plot(h=>cabinNormal(h*1000, pAf).P, '#0b5cad'); plot(h=>cabinBolevoy(h*1000, pAf).P, '#c62828'); plot(h=>cabinAft(h*1000).P, '#2e7d32'); ctx.setLineDash([4,4]);ctx.strokeStyle='#9fb0c0'; [595, 470, 430, 267].forEach(p=>{ ctx.beginPath();ctx.moveTo(L,yS(p));ctx.lineTo(W-R,yS(p));ctx.stroke(); }); ctx.setLineDash([]); const legend = [ ['#15202b','Атмосфера'], ['#0b5cad','Нормальный 0,5'], ['#c62828','Боевой 0,2'], ['#2e7d32','Кормовая'] ]; ctx.font='11px sans-serif';ctx.textAlign='left'; let lx = L+10, ly = T+12; legend.forEach(([c,t])=>{ ctx.fillStyle=c;ctx.fillRect(lx,ly-3,12,3); ctx.fillStyle='#15202b';ctx.fillText(t,lx+16,ly); ly += 14; }); const curP = mode==='normal' ? cabinNormal(hl*1000,pAf).P : mode==='bolevoy' ? cabinBolevoy(hl*1000,pAf).P : cabinAft(hl*1000).P; ctx.fillStyle='#ff9800'; ctx.beginPath();ctx.arc(xS(hl), yS(curP), 5, 0, Math.PI*2);ctx.fill(); ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.stroke(); } /* ---- ТАЙМЕРЫ ---- */ function renderTimer(){ const t = state.timer; const target = interpolateTimes(t.scenario, t.height); const channels = Object.keys(target).filter(k=>k!=='h'); const tbody = app.querySelector('#tool-body'); tbody.innerHTML = `

Интерполированные значения для ${t.height} км:

${t.active ? `` : ``}

Значения по таблицам 1–4 раздела 6.8.4 / 6.8.5 (интерполированы).

`; const bars = tbody.querySelector('#timer-bars'); bars.innerHTML = channels.map(ch=>{ const val = target[ch]; const label = {t:'Кабина', ke:'КЭ', gr:'Грузовая', aft:'Кормовая'}[ch] || ch; const shown = val == null ? '—' : (val.toFixed(0)+' с'); return `
${label}
${shown}
`; }).join(''); if(t.active) tickTimer(); } function startTimer(){ const t = state.timer; const target = interpolateTimes(t.scenario, t.height); const targets = {}; Object.keys(target).forEach(k=>{ if(k === 'h') return; if(target[k] != null) targets[k] = target[k]; }); t.targets = targets; t.active = true; t.startTs = Date.now(); t.intervalId = setInterval(tickTimer, 100); renderTimer(); } function tickTimer(){ const t = state.timer; if(!t.active) return; const elapsed = (Date.now() - t.startTs) / 1000; let allDone = true; Object.entries(t.targets).forEach(([ch, sec])=>{ const remain = Math.max(0, sec - elapsed); if(remain > 0) allDone = false; const bar = document.getElementById('bar_'+ch); const num = document.getElementById('num_'+ch); const row = document.getElementById('tr_'+ch); if(bar){ const pct = sec > 0 ? (remain/sec)*100 : 0; bar.style.width = pct + '%'; } if(num) num.textContent = remain.toFixed(1) + ' с'; if(row && remain === 0) row.classList.add('done'); }); if(allDone){ clearInterval(t.intervalId); t.active = false; beep(); if(navigator.vibrate) navigator.vibrate([300,120,300,120,300]); renderTimer(); } } function stopTimer(){ const t = state.timer; if(t.intervalId) clearInterval(t.intervalId); t.active = false; renderTimer(); } function beep(){ try{ const AC = window.AudioContext || window.webkitAudioContext; if(!AC) return; const ctx = new AC(); [880,1100,880].forEach((f,i)=>{ const o = ctx.createOscillator(), g = ctx.createGain(); o.connect(g); g.connect(ctx.destination); o.frequency.value = f; g.gain.value = 0.25; const start = ctx.currentTime + i*0.25; o.start(start); o.stop(start + 0.2); }); }catch(e){} } /* ---- КВИЗ ---- */ function renderQuiz(){ const q = state.quiz; if(!q.started) return renderQuizIntro(); if(q.idx >= q.order.length) return renderQuizResult(); return renderQuizQuestion(); } function renderQuizIntro(){ app.innerHTML = `
🎯

Тренажёр по действиям при отказах

${quizQuestions.length} вопросов по СКВ и САРД.
Выберите правильное действие — получите объяснение.

`; } function startQuiz(){ const order = quizQuestions.map((_,i)=>i).sort(()=>Math.random()-0.5); state.quiz = { started:true, idx:0, selected:null, score:0, order }; renderQuiz(); } function renderQuizQuestion(){ const q = state.quiz; const total = q.order.length; const qi = q.order[q.idx]; const question = quizQuestions[qi]; const progress = ((q.idx) / total) * 100; const selected = q.selected; const answered = selected !== null; app.innerHTML = `
Вопрос ${q.idx+1} из ${total}

${question.q}

${question.options.map((opt,i)=>{ let cls = 'quiz-opt'; if(answered){ if(i === question.correct) cls += ' correct'; else if(i === selected) cls += ' wrong'; } return ``; }).join('')} ${answered ? `
${selected === question.correct ? '✓ Верно.' : '✗ Неверно.'} ${question.explain}
` : ''}
`; } function selectAnswer(i){ const q = state.quiz; if(q.selected !== null) return; q.selected = i; const qi = q.order[q.idx]; if(i === quizQuestions[qi].correct) q.score++; renderQuizQuestion(); } function nextQuestion(){ const q = state.quiz; q.idx++; q.selected = null; if(q.idx >= q.order.length){ renderQuizResult(); } else renderQuizQuestion(); } function renderQuizResult(){ const q = state.quiz; const total = q.order.length; const pct = Math.round((q.score / total) * 100); let cat, cls; if(pct >= 90){ cat = 'Отлично'; cls = 'ok'; } else if(pct >= 70){ cat = 'Хорошо'; cls = 'ok'; } else if(pct >= 50){ cat = 'Удовлетворительно'; cls = 'warn'; } else { cat = 'Требуется повторение'; cls = 'bad'; } app.innerHTML = `
${pct >= 70 ? '🎉' : '📚'}
${q.score}/${total}
правильных ответов
${cat} (${pct}%)
`; } /* ---- ШРИФТ ---- */ function changeFont(d){ const cur = parseFloat(localStorage.getItem('il76_font') || '16'); const next = Math.min(22, Math.max(13, cur + d)); localStorage.setItem('il76_font', next); document.documentElement.style.setProperty('--font-size', next + 'px'); } const sf = localStorage.getItem('il76_font'); if(sf) document.documentElement.style.setProperty('--font-size', sf + 'px'); /* ---- PWA ---- */ let deferredPrompt = null; window.addEventListener('beforeinstallprompt', (e)=>{ e.preventDefault(); deferredPrompt = e; const btn = document.getElementById('install-btn'); if(btn) btn.classList.add('show'); }); async function installPWA(){ if(!deferredPrompt) return; deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; deferredPrompt = null; const btn = document.getElementById('install-btn'); if(btn) btn.classList.remove('show'); } window.addEventListener('appinstalled', ()=>{ const btn = document.getElementById('install-btn'); if(btn) btn.classList.remove('show'); }); if('serviceWorker' in navigator && location.protocol !== 'file:'){ window.addEventListener('load', ()=>{ navigator.serviceWorker.register('sw.js').catch(err=>console.warn('SW:', err)); }); } render();