diff --git a/frontend/js/flagships/phys9_flag_F18_master.js b/frontend/js/flagships/phys9_flag_F18_master.js
new file mode 100644
index 0000000..2204573
--- /dev/null
+++ b/frontend/js/flagships/phys9_flag_F18_master.js
@@ -0,0 +1,271 @@
+// F18. Магистр-симулятор (final5 в ch5) — конструктор сценария движения тела.
+(function(){
+'use strict';
+const B = () => window.PHYS9_FLAG_BASE;
+const C = () => window.PHYS9_COLORS || {};
+
+const STAGE_TYPES = {
+ uniform: { name: 'Равномерное', color: '#0891b2' },
+ accel: { name: 'Равноуск.', color: '#ea580c' },
+ fall: { name: 'Своб. падение', color: '#2563eb' },
+ stop: { name: 'Стоп / покой', color: '#475569' }
+};
+
+function init(secId){
+ if (!B()) return false;
+ let typeBtns = '';
+ for (const t in STAGE_TYPES){
+ const s = STAGE_TYPES[t];
+ typeBtns += '';
+ }
+ const body = ''
+ + '
Собери цепочку этапов движения как LEGO. Программа рассчитает $x(t)$, $v(t)$, $a(t)$.
'
+ + ''
+ + typeBtns
+ + ''
+ + ''
+ + '
'
+ + ''
+ + ''
+ + ''
+ + '
Этапов0
'
+ + '
Общая длительность0 с
'
+ + '
Итог $x$0 м
'
+ + '
Итог $v$0 м/с
'
+ + '
'
+ + ''
+ + ''
+ + ''
+ + '
'
+ + '';
+
+ const card = B().makeCard(secId,
+ 'F18. Магистр-симулятор КОНСТРУКТОР',
+ 'Финальный конструктор сценариев. Каждый этап — кусок движения. Программа считает $x(t)$, $v(t)$, $a(t)$ и проверяет согласованность.',
+ body);
+ if (!card) return false;
+
+ const cv = document.getElementById('F18-cv');
+ const ctx = cv.getContext('2d');
+ const W = cv.width, H = cv.height;
+
+ let stages = []; /* {type, duration, param} */
+
+ function makeStageCard(idx){
+ const s = stages[idx];
+ const type = STAGE_TYPES[s.type];
+ const paramLabel = {
+ uniform: 'v, м/с',
+ accel: 'a, м/с²',
+ fall: '— (g)',
+ stop: '—'
+ }[s.type];
+ const div = document.createElement('div');
+ div.style.cssText = 'background:var(--card);border:2px solid '+type.color+';border-radius:9px;padding:8px 10px;display:flex;gap:8px;align-items:center;font-size:.85rem';
+ div.innerHTML = ''
+ + '' + (idx+1) + '. ' + type.name + ''
+ + ''
+ + (paramLabel === '—' || paramLabel === '— (g)' ? ''+paramLabel+'' :
+ '')
+ + '';
+ return div;
+ }
+
+ function refreshStages(){
+ const host = document.getElementById('F18-stages');
+ host.innerHTML = '';
+ if (stages.length === 0){
+ host.innerHTML = 'Добавь этапы кнопками сверху…';
+ } else {
+ stages.forEach((s, i) => host.appendChild(makeStageCard(i)));
+ }
+ host.querySelectorAll('input').forEach(inp => {
+ inp.addEventListener('input', e => {
+ const i = +e.target.dataset.i, f = e.target.dataset.f;
+ const val = +e.target.value;
+ if (!isNaN(val)) { stages[i][f] = val; recalculate(); }
+ });
+ });
+ host.querySelectorAll('[data-rm]').forEach(btn => {
+ btn.addEventListener('click', e => {
+ const i = +e.target.dataset.rm;
+ stages.splice(i, 1);
+ refreshStages();
+ recalculate();
+ });
+ });
+ try { if(window.renderMathInElement) window.renderMathInElement(host, { delimiters:[{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){}
+ recalculate();
+ }
+
+ function simulate(){
+ /* Возвращает массив точек {t, x, v, a} */
+ const pts = [];
+ let t = 0, x = 0, v = 0, a = 0;
+ pts.push({ t, x, v, a: 0 });
+ for (const s of stages){
+ const dt_step = 0.05;
+ const end = t + s.duration;
+ while (t < end - 1e-6){
+ const step = Math.min(dt_step, end - t);
+ if (s.type === 'uniform') { a = 0; v = s.param; }
+ else if (s.type === 'accel') { a = s.param; }
+ else if (s.type === 'fall') { a = -9.8; }
+ else if (s.type === 'stop') { a = 0; v = 0; }
+ v += a * step;
+ x += v * step;
+ t += step;
+ pts.push({ t, x, v, a });
+ }
+ }
+ return pts;
+ }
+
+ function recalculate(){
+ const pts = simulate();
+ const last = pts[pts.length-1];
+ document.getElementById('F18-n').textContent = stages.length;
+ document.getElementById('F18-tt').textContent = last.t.toFixed(1) + ' с';
+ document.getElementById('F18-xt').textContent = last.x.toFixed(2) + ' м';
+ document.getElementById('F18-vt').textContent = last.v.toFixed(2) + ' м/с';
+ /* проверки */
+ const fb = document.getElementById('F18-fb');
+ if (stages.length === 0){
+ fb.className = 'flag-feedback';
+ } else {
+ /* для разрывов: проверяем согласованность v между этапами */
+ let warning = '';
+ for (let i = 0; i < stages.length - 1; i++){
+ const a = stages[i], b = stages[i+1];
+ /* в реальности v сохраняется при переходе. Если b.type=uniform с фикс. v, может быть разрыв */
+ if (b.type === 'uniform'){
+ /* считаем v на конце a */
+ let v_end = 0;
+ if (a.type === 'uniform') v_end = a.param;
+ else if (a.type === 'stop') v_end = 0;
+ /* для accel и fall нужно знать v на начало a — пропускаем для упрощения */
+ if (Math.abs(v_end - b.param) > 0.1){
+ warning = '⚠ После этапа '+(i+1)+' скорость '+v_end.toFixed(1)+' м/с, но '+(i+2)+'-й этап задаёт '+b.param.toFixed(1)+' м/с — это рывок.';
+ break;
+ }
+ }
+ }
+ if (warning){
+ fb.className = 'flag-feedback warn show';
+ fb.innerHTML = warning;
+ } else {
+ fb.className = 'flag-feedback ok show';
+ fb.innerHTML = '✓ Сценарий согласован. Этапов: '+stages.length+', итог: x = '+last.x.toFixed(1)+' м, v = '+last.v.toFixed(1)+' м/с.';
+ }
+ }
+ draw(pts);
+ }
+
+ function draw(pts){
+ const col = C();
+ ctx.fillStyle = col.bg || '#fafafa';
+ ctx.fillRect(0, 0, W, H);
+ if (pts.length < 2){
+ ctx.fillStyle = col.textMuted || '#64748b';
+ ctx.font = '15px Inter,sans-serif';
+ ctx.fillText('Добавь этапы движения — увидь графики x(t), v(t), a(t)', 90, H/2);
+ return;
+ }
+ /* 3 графика горизонтально */
+ const tMax = pts[pts.length-1].t || 1;
+ let xMin = 0, xMax = 0, vMin = 0, vMax = 0, aMin = 0, aMax = 0;
+ for (const p of pts){
+ xMin = Math.min(xMin, p.x); xMax = Math.max(xMax, p.x);
+ vMin = Math.min(vMin, p.v); vMax = Math.max(vMax, p.v);
+ aMin = Math.min(aMin, p.a); aMax = Math.max(aMax, p.a);
+ }
+ if (xMax - xMin < 0.5) xMax = xMin + 1;
+ if (vMax - vMin < 0.5) vMax = vMin + 1;
+ if (aMax - aMin < 0.5) aMax = aMin + 1;
+ function plot(yOff, hOff, vals, vMin0, vMax0, color, label){
+ ctx.fillStyle = col.bgSubtle || '#f8fafc';
+ ctx.fillRect(40, yOff, W - 60, hOff);
+ ctx.strokeStyle = col.axis || '#1e293b';
+ ctx.lineWidth = 1.5;
+ ctx.strokeRect(40, yOff, W - 60, hOff);
+ /* zero-line */
+ const zeroY = yOff + hOff - (0 - vMin0)/(vMax0 - vMin0) * hOff;
+ ctx.strokeStyle = col.grid || '#e5e7eb';
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.moveTo(40, zeroY); ctx.lineTo(W - 20, zeroY);
+ ctx.stroke();
+ /* линия */
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 2.5;
+ ctx.beginPath();
+ for (let i = 0; i < pts.length; i++){
+ const t = pts[i].t;
+ const v = vals[i];
+ const px = 40 + (t / tMax) * (W - 60);
+ const py = yOff + hOff - (v - vMin0)/(vMax0 - vMin0) * hOff;
+ if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
+ }
+ ctx.stroke();
+ /* label */
+ ctx.fillStyle = color;
+ ctx.font = 'bold 13px Inter,sans-serif';
+ ctx.fillText(label, 8, yOff + 16);
+ ctx.fillStyle = col.textMuted || '#64748b';
+ ctx.font = '10px Inter,sans-serif';
+ ctx.fillText(vMax0.toFixed(1), 8, yOff + 12);
+ ctx.fillText(vMin0.toFixed(1), 8, yOff + hOff - 2);
+ }
+ const blockH = 130, gap = 10;
+ plot(10, blockH, pts.map(p => p.x), xMin, xMax, col.displacement || '#2563eb', 'x(t), м');
+ plot(10 + blockH + gap, blockH, pts.map(p => p.v), vMin, vMax, col.velocity || '#0891b2', 'v(t), м/с');
+ plot(10 + 2*(blockH + gap), blockH, pts.map(p => p.a), aMin, aMax, col.acceleration || '#ea580c', 'a(t), м/с²');
+ /* x-axis label */
+ ctx.fillStyle = col.text || '#0f172a';
+ ctx.font = '11px Inter,sans-serif';
+ ctx.fillText('t = '+tMax.toFixed(1)+' с', W - 70, H - 4);
+ }
+
+ card.querySelectorAll('[data-add]').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const type = btn.dataset.add;
+ const def = { uniform: 5, accel: 1, fall: 0, stop: 0 }[type];
+ stages.push({ type, duration: 3, param: def });
+ refreshStages();
+ });
+ });
+ document.getElementById('F18-clear').addEventListener('click', () => { stages = []; refreshStages(); });
+ document.getElementById('F18-preset').addEventListener('click', () => {
+ stages = [
+ { type:'accel', duration:5, param: 2 },
+ { type:'uniform', duration:8, param:10 },
+ { type:'accel', duration:4, param:-2.5 }
+ ];
+ refreshStages();
+ });
+ document.getElementById('F18-save').addEventListener('click', () => {
+ try {
+ localStorage.setItem('phys9_F18_scenario', JSON.stringify(stages));
+ const fb = document.getElementById('F18-fb');
+ fb.className = 'flag-feedback ok show';
+ fb.innerHTML = '✓ Сценарий сохранён в браузере.';
+ } catch(e){}
+ });
+ document.getElementById('F18-load').addEventListener('click', () => {
+ try {
+ const s = JSON.parse(localStorage.getItem('phys9_F18_scenario') || 'null');
+ if (Array.isArray(s)) { stages = s; refreshStages(); }
+ } catch(e){}
+ });
+
+ refreshStages();
+ draw([{t:0,x:0,v:0,a:0}]);
+ return true;
+}
+
+if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F18', { init: init, cleanup: function(){} });
+else document.addEventListener('DOMContentLoaded', ()=>{
+ if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F18', { init: init, cleanup: function(){} });
+});
+
+})();
diff --git a/frontend/textbooks/physics_9_ch5.html b/frontend/textbooks/physics_9_ch5.html
index 5d1bea5..15dfc8f 100644
--- a/frontend/textbooks/physics_9_ch5.html
+++ b/frontend/textbooks/physics_9_ch5.html
@@ -9,6 +9,7 @@
+
@@ -797,7 +798,7 @@ function _injectTasks(id){
var body = document.getElementById(id + '-body');
if(!body || body.querySelector('.legacy-tasks')) return;
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
- setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH5_WIDGETS && window.PHYS9_CH5_WIDGETS[id]) window.PHYS9_CH5_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60);
+ setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH5_WIDGETS && window.PHYS9_CH5_WIDGETS[id]) window.PHYS9_CH5_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } try { if(window.PHYS9_FLAG_BASE){ if(id==='final5') window.PHYS9_FLAG_BASE.mount('F18','final5'); } } catch(e){ console.warn('phys9 flag init:', e.message); } }, 60);
}
var _origEnsureBuilt = ensureBuilt;
ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };