Files
Learn_System/frontend/js/flagships/phys9_flag_F19_rocket.js
Maxim Dolgolyov 4d53919e9a feat(phys9 flagships): F9 мост + F11 бильярд + F19 ракета (Wave C+D+финал)
F9. Конструктор моста (§28 в ch3):
- Canvas 700×380: балка на 2 опорах
- Палитра грузов 50/100/200/500 кг (drag&drop) + тест 1000 кг
- Расчёт реакций опор через ΣF=0 + ΣM=0
- При перегрузке (>8000 Н) балка ломается визуально

F11. Бильярдная физика (§32 в ch4):
- Canvas 700×380, зелёный стол, 4 шара
- Тяни мышью от битка → прицельный вектор, отпусти → удар
- Реальные упругие столкновения по нормали
- Трение поля, отскоки от бортов, trails
- Stats: Σ p, Σ Ek

F19. Полёт ракеты (final4, финальный босс):
- Canvas 700×420 — космос со звёздами, Земля внизу
- 4 slider'а: m₀, m_f, v_газов, расход q
- Реальная физика: тяга F = q·u, g(h), сопротивление ρ(h)e^(-h/8000)
- Анимация ракеты с пламенем + перемещение по высоте
- Цель: 400 км (МКС)
- При успехе: +150 XP, localStorage 'phys9_F19_success'

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 10:19:55 +03:00

228 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// F19. Полёт ракеты (финал курса в ch4) — Циолковский + гравитация + сопротивление.
(function(){
'use strict';
const B = () => window.PHYS9_FLAG_BASE;
const C = () => window.PHYS9_COLORS || {};
function init(secId){
if (!B()) return false;
const body = ''
+ '<div class="flag-sliders">'
+ '<label>Сухая масса $m_0$, т: <b id="F19-m0v">5</b><input type="range" id="F19-m0" min="1" max="20" step="0.5" value="5"></label>'
+ '<label>Топливо $m_f$, т: <b id="F19-mfv">95</b><input type="range" id="F19-mf" min="10" max="500" step="5" value="95"></label>'
+ '<label>$v_{газов}$, м/с: <b id="F19-uv">3000</b><input type="range" id="F19-u" min="1000" max="5000" step="100" value="3000"></label>'
+ '<label>Расход топлива, т/с: <b id="F19-qv">2</b><input type="range" id="F19-q" min="0.2" max="10" step="0.1" value="2"></label>'
+ '</div>'
+ '<canvas id="F19-cv" class="flag-canvas" width="700" height="420" style="height:420px"></canvas>'
+ '<div class="flag-controls">'
+ '<button class="flag-btn primary" id="F19-go">ЗАПУСК!</button>'
+ '<button class="flag-btn" id="F19-reset">Сброс</button>'
+ '</div>'
+ '<div class="flag-stats">'
+ '<div class="flag-stat"><span class="lbl">Время</span><span class="val" id="F19-t">0 с</span></div>'
+ '<div class="flag-stat"><span class="lbl">Высота</span><span class="val" id="F19-h">0 м</span></div>'
+ '<div class="flag-stat"><span class="lbl">Скорость</span><span class="val" id="F19-v">0 м/с</span></div>'
+ '<div class="flag-stat"><span class="lbl">Топливо</span><span class="val" id="F19-fuel">100%</span></div>'
+ '<div class="flag-stat"><span class="lbl">Перегрузка</span><span class="val" id="F19-g">0 g</span></div>'
+ '</div>'
+ '<div class="flag-feedback" id="F19-fb"></div>';
const card = B().makeCard(secId,
'F19. Полёт ракеты <span class="flag-medal">МАГИСТР</span>',
'Запусти ракету с Земли на орбиту 400 км (МКС). Физика: тяга по Циолковскому, сила тяжести g(h), сопротивление атмосферы. Финальный босс курса!',
body);
if (!card) return false;
const cv = document.getElementById('F19-cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
let st = { t: 0, h: 0, v: 0, m: 0, mf: 0, running: false, ended: false, history: [] };
function readSliders(){
document.getElementById('F19-m0v').textContent = (+document.getElementById('F19-m0').value).toFixed(1);
document.getElementById('F19-mfv').textContent = (+document.getElementById('F19-mf').value).toFixed(0);
document.getElementById('F19-uv').textContent = (+document.getElementById('F19-u').value).toFixed(0);
document.getElementById('F19-qv').textContent = (+document.getElementById('F19-q').value).toFixed(1);
}
function reset(){
const m0 = +document.getElementById('F19-m0').value * 1000; /* кг */
const mf = +document.getElementById('F19-mf').value * 1000;
st = { t: 0, h: 0, v: 0, m: m0 + mf, mf: mf, m0: m0, mf0: mf, running: false, ended: false, history: [] };
document.getElementById('F19-go').textContent = 'ЗАПУСК!';
document.getElementById('F19-fb').className = 'flag-feedback';
draw();
}
function tick(dt){
if (!st.running || st.ended) { draw(); return; }
const u = +document.getElementById('F19-u').value;
const q = +document.getElementById('F19-q').value * 1000; /* кг/с */
const G = 6.674e-11, M = 5.972e24, R = 6.371e6;
const N = 4;
const ddt = dt / N;
for (let i = 0; i < N; i++){
const dm = Math.min(st.mf, q * ddt);
const thrust = (dm > 1e-3) ? q * u : 0;
const g = G * M / Math.pow(R + st.h, 2);
/* атм сопротивление: плотность падает с высотой, упрощённо */
const rho = st.h < 80000 ? 1.225 * Math.exp(-st.h/8000) : 0;
const drag = 0.3 * rho * st.v * Math.abs(st.v) * 5; /* k*ρ*v²*A */
const F_net = thrust - st.m * g - Math.sign(st.v) * drag;
const a = F_net / st.m;
st.v += a * ddt;
st.h += st.v * ddt;
st.mf = Math.max(0, st.mf - dm);
st.m = st.m0 + st.mf;
st.t += ddt;
st.history.push({ t: st.t, h: st.h, v: st.v });
if (st.history.length > 1000) st.history.shift();
/* окончание */
if (st.h < 0){ st.ended = true; st.running = false; document.getElementById('F19-go').textContent='ЗАПУСК!'; break; }
if (st.t > 600){ st.ended = true; st.running = false; document.getElementById('F19-go').textContent='ЗАПУСК!'; break; }
}
/* stats */
document.getElementById('F19-t').textContent = st.t.toFixed(1) + ' с';
document.getElementById('F19-h').textContent = (st.h/1000).toFixed(1) + ' км';
document.getElementById('F19-v').textContent = st.v.toFixed(0) + ' м/с';
document.getElementById('F19-fuel').textContent = (st.mf/st.mf0*100).toFixed(0) + '%';
/* перегрузка во время тяги */
const u2 = +document.getElementById('F19-u').value;
const q2 = +document.getElementById('F19-q').value * 1000;
const thrust2 = (st.mf > 1e-3) ? q2 * u2 : 0;
const G2 = 6.674e-11, M2 = 5.972e24, R2 = 6.371e6;
const g2 = G2 * M2 / Math.pow(R2 + st.h, 2);
const a2 = (thrust2 - st.m * g2) / st.m;
document.getElementById('F19-g').textContent = ((a2/9.8) + 1).toFixed(2) + ' g';
/* feedback */
const fb = document.getElementById('F19-fb');
if (st.ended && st.h < 1){
fb.className = 'flag-feedback fail show';
fb.innerHTML = '&#10007; КРАШ. Ракета упала на Землю — не хватило топлива/тяги.';
} else if (st.h >= 400000 && Math.abs(st.v) < 100 && st.ended){
fb.className = 'flag-feedback ok show';
fb.innerHTML = '&#10003; УСПЕХ! Ракета на орбите МКС (400 км). Магистр Физики 9 — Вы!';
try { localStorage.setItem('phys9_F19_success', '1'); if(window.addXp) window.addXp(150, 'F19-magistr'); } catch(e){}
} else if (st.h > 400000){
fb.className = 'flag-feedback warn show';
fb.innerHTML = 'Высоко! Но нужна ещё горизонтальная скорость для орбиты ($\\sim 7.7$ км/с). Это упрощённая 1D модель.';
}
draw();
}
function draw(){
const col = C();
ctx.fillStyle = '#0f172a'; /* космос */
ctx.fillRect(0, 0, W, H);
/* звёзды */
ctx.fillStyle = '#fff';
for (let i = 0; i < 80; i++) ctx.fillRect((i*37+11)%W, (i*73+19)%H, 1, 1);
/* Земля внизу */
const earthY = H - 30;
ctx.fillStyle = '#16a34a';
ctx.fillRect(0, earthY, W, 30);
ctx.fillStyle = col.forceGravity || '#2563eb';
ctx.beginPath(); ctx.arc(W/2, earthY + 100, 80, 0, Math.PI*2); ctx.fill();
/* шкала высоты по правому краю */
ctx.fillStyle = '#fff';
ctx.font = '10px Inter,sans-serif';
const maxH = Math.max(450000, st.h * 1.1);
for (let k = 0; k <= 4; k++){
const h = maxH * k/4;
const py = earthY - (h/maxH) * (earthY - 40);
ctx.fillText((h/1000).toFixed(0) + ' км', W - 60, py + 3);
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.beginPath(); ctx.moveTo(40, py); ctx.lineTo(W - 65, py); ctx.stroke();
}
/* 400 км — целевая орбита */
const targetY = earthY - (400000/maxH) * (earthY - 40);
ctx.strokeStyle = '#fbbf24';
ctx.setLineDash([8, 5]);
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, targetY); ctx.lineTo(W, targetY); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#fbbf24';
ctx.font = 'bold 11px Inter,sans-serif';
ctx.fillText('★ цель: 400 км (МКС)', 10, targetY - 4);
/* Ракета */
const rocketX = 100;
const rocketY = earthY - (st.h/maxH) * (earthY - 40);
ctx.save();
ctx.translate(rocketX, rocketY);
/* корпус */
ctx.fillStyle = '#e5e7eb';
ctx.beginPath();
ctx.moveTo(0, -22);
ctx.lineTo(7, -16);
ctx.lineTo(7, 14);
ctx.lineTo(-7, 14);
ctx.lineTo(-7, -16);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#0f172a';
ctx.stroke();
/* окошко */
ctx.fillStyle = '#0891b2';
ctx.beginPath(); ctx.arc(0, -8, 3, 0, Math.PI*2); ctx.fill();
/* плавники */
ctx.fillStyle = '#dc2626';
ctx.beginPath();
ctx.moveTo(-7, 14); ctx.lineTo(-12, 22); ctx.lineTo(-7, 18); ctx.fill();
ctx.beginPath();
ctx.moveTo(7, 14); ctx.lineTo(12, 22); ctx.lineTo(7, 18); ctx.fill();
/* выхлоп */
if (st.running && st.mf > 0){
ctx.fillStyle = '#fbbf24';
ctx.beginPath();
ctx.moveTo(-5, 18);
ctx.lineTo(0, 18 + 10 + Math.random()*8);
ctx.lineTo(5, 18);
ctx.fill();
ctx.fillStyle = '#dc2626';
ctx.beginPath();
ctx.moveTo(-3, 18);
ctx.lineTo(0, 18 + 6 + Math.random()*4);
ctx.lineTo(3, 18);
ctx.fill();
}
ctx.restore();
/* график высота(t) справа */
if (st.history.length > 1){
ctx.strokeStyle = '#fbbf24';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < st.history.length; i++){
const p = st.history[i];
const py = earthY - (p.h/maxH) * (earthY - 40);
const px = 200 + (p.t/Math.max(60, st.t)) * (W - 290);
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.stroke();
}
}
document.getElementById('F19-go').addEventListener('click', ()=>{
if (st.ended) reset();
st.running = !st.running;
document.getElementById('F19-go').textContent = st.running ? 'ПАУЗА' : 'ЗАПУСК!';
});
document.getElementById('F19-reset').addEventListener('click', reset);
['F19-m0','F19-mf','F19-u','F19-q'].forEach(id => document.getElementById(id).addEventListener('input', ()=>{
readSliders();
if (!st.running) reset();
}));
readSliders();
reset();
B().startLoop('F19', cv, tick);
return true;
}
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F19', { init: init, cleanup: function(){} });
else document.addEventListener('DOMContentLoaded', ()=>{
if (window.PHYS9_FLAG_BASE) window.PHYS9_FLAG_BASE.register('F19', { init: init, cleanup: function(){} });
});
})();