6363.hud-tl { top : 44px ; left : 8px ; min-width : 140px ; }
6464.hud-tr { top : 44px ; right : 8px ; text-align : right; min-width : 140px ; }
6565.hud-bl { bottom : 20px ; left : 8px ; min-width : 140px ; }
66- .hud-br { bottom : 20 px ; right : 8 px ; text-align : right; min-width : 140px ; }
66+ .hud-br { bottom : 28 px ; right : 210 px ; text-align : right; min-width : 140px ; }
6767.hud b { color : # 4090E0 ; font-size : 0.85em ; letter-spacing : 1px ; }
6868.hud .val { color : # 40E090 ; font-weight : bold; }
6969.hud .warn { color : # FFD040 ; }
7070.hud .q-warn { color : # FF6040 ; font-weight : bold; }
7171
72- .navball { position : absolute; bottom : 30 px ; left : 50 % ; transform : translateX ( -50 % ); width : 220 px ; height : 220 px ; z-index : 10 ; pointer-events : none; border-radius : 50% ; overflow : hidden; box-shadow : 0 0 0 3px # 304060, inset 0 0 24 px rgba (0 , 0 , 0 , 0.6 ), 0 6 px 24 px rgba (0 , 0 , 0 , 0.7 ); }
73- .navball canvas { display : block; width : 220 px ; height : 220 px ; }
74- .navball ::before { content : '' ; position : absolute; top : 50% ; left : 50% ; width : 26 px ; height : 26 px ; margin : -13 px 0 0 -13 px ; border : 2.5 px solid # 40E090 ; border-radius : 50% ; box-sizing : border-box; z-index : 2 ; box-shadow : 0 0 4 px rgba (64 , 224 , 144 , 0.6 ); }
75- .navball ::after { content : '' ; position : absolute; top : 50% ; left : 50% ; width : 90 px ; height : 2.5 px ; margin : -1px 0 0 -45 px ; background : linear-gradient (to right, # 40E090 0 , # 40E090 18% , transparent 18% , transparent 82% , # 40E090 82% , # 40E090 100% ); z-index : 2 ; box-shadow : 0 0 4 px rgba (64 , 224 , 144 , 0.5 ); }
72+ .navball { position : absolute; bottom : 22 px ; right : 22 px ; width : 170 px ; height : 170 px ; z-index : 10 ; pointer-events : none; border-radius : 50% ; overflow : hidden; box-shadow : 0 0 0 3px # 304060, inset 0 0 18 px rgba (0 , 0 , 0 , 0.5 ), 0 4 px 18 px rgba (0 , 0 , 0 , 0.6 ); }
73+ .navball canvas { display : block; width : 170 px ; height : 170 px ; }
74+ .navball ::before { content : '' ; position : absolute; top : 50% ; left : 50% ; width : 22 px ; height : 22 px ; margin : -11 px 0 0 -11 px ; border : 2 px solid # 40E090 ; border-radius : 50% ; box-sizing : border-box; z-index : 2 ; box-shadow : 0 0 3 px rgba (64 , 224 , 144 , 0.6 ); }
75+ .navball ::after { content : '' ; position : absolute; top : 50% ; left : 50% ; width : 70 px ; height : 2 px ; margin : -1px 0 0 -35 px ; background : linear-gradient (to right, # 40E090 0 , # 40E090 18% , transparent 18% , transparent 82% , # 40E090 82% , # 40E090 100% ); z-index : 2 ; box-shadow : 0 0 3 px rgba (64 , 224 , 144 , 0.5 ); }
7676
77- .throttle-bar { position : absolute; bottom : 30px ; left : calc ( 50 % - 155 px ) ; width : 28 px ; height : 220 px ; background : rgba (5 , 10 , 25 , 0.7 ); border : 2px solid # 304060 ; border-radius : 6px ; z-index : 10 ; box-shadow : 0 4px 16px rgba (0 , 0 , 0 , 0.6 ); }
77+ .throttle-bar { position : absolute; bottom : 30px ; left : 20 px ; width : 24 px ; height : 200 px ; background : rgba (5 , 10 , 25 , 0.7 ); border : 2px solid # 304060 ; border-radius : 6px ; z-index : 10 ; box-shadow : 0 4px 16px rgba (0 , 0 , 0 , 0.6 ); }
7878.throttle-fill { position : absolute; bottom : 2px ; left : 2px ; right : 2px ; background : linear-gradient (to top, # D06020 0% , # E0A040 40% , # 40E080 100% ); border-radius : 0 0 4px 4px ; transition : height 0.1s ; }
7979.throttle-bar ::before { content : 'THR' ; position : absolute; top : -20px ; left : 50% ; transform : translateX (-50% ); font-size : 0.7em ; color : # 6090C0 ; letter-spacing : 2px ; font-family : 'Courier New' , monospace; font-weight : bold; }
8080
@@ -227,7 +227,7 @@ <h3>First mission</h3>
227227 < div class ="hud hud-bl " id ="hudBL "> </ div >
228228 < div class ="hud hud-br " id ="hudBR "> </ div >
229229 < div class ="throttle-bar " id ="throttleBar "> < div class ="throttle-fill " id ="throttleFill "> </ div > </ div >
230- < div class ="navball " id ="navball "> < canvas id ="navballCanvas " width ="220 " height ="220 "> </ canvas > </ div >
230+ < div class ="navball " id ="navball "> < canvas id ="navballCanvas " width ="170 " height ="170 "> </ canvas > </ div >
231231 < div class ="sas-panel " id ="sasPanel ">
232232 < h4 > SAS</ h4 >
233233 < button class ="sas-btn active " data-sas ="off " onclick ="setSAS('off') "> OFF< span class ="sas-key "> 0</ span > </ button >
@@ -331,7 +331,7 @@ <h2 id="resultTitle">Orbit Achieved!</h2>
331331var timeWarp = 1 ;
332332var lastTime = 0 ;
333333var accumulator = 0 ;
334- var PHYSICS_DT = 0.02 ;
334+ var PHYSICS_DT = 0.01 ;
335335var viewMode = 'flight' ;
336336var sasMode = 'off' ;
337337var SAS_KEYS = { '0' :'off' , '1' :'stability' , '2' :'prograde' , '3' :'retrograde' , '4' :'radial-out' , '5' :'radial-in' , '6' :'normal' , '7' :'antinormal' , '8' :'maneuver' , '9' :'target' } ;
@@ -1104,6 +1104,30 @@ <h2 id="resultTitle">Orbit Achieved!</h2>
11041104 if ( stageFuel ( s . parts , s . currentStage ) <= 0.001 && computeThrust ( s . parts , s . currentStage , 1 ) . thrust <= 0 ) doStage ( s ) ;
11051105}
11061106
1107+ function buildFlameGroup ( nominalThrust , radius ) {
1108+ var grp = new THREE . Group ( ) ;
1109+ var r = radius * PART_SCALE ;
1110+ // Outer plume (red)
1111+ var outer = new THREE . Mesh ( new THREE . ConeGeometry ( r * 1.8 , 8 , 12 , 1 , true ) , new THREE . MeshBasicMaterial ( { color : 0xFF5020 , transparent : true , opacity : 0.45 , blending : THREE . AdditiveBlending , depthWrite : false , depthTest : false , side : THREE . DoubleSide } ) ) ;
1112+ outer . rotation . x = Math . PI ;
1113+ outer . position . y = - 4 ;
1114+ grp . add ( outer ) ;
1115+ // Mid plume (orange)
1116+ var mid = new THREE . Mesh ( new THREE . ConeGeometry ( r * 1.25 , 6 , 12 , 1 , true ) , new THREE . MeshBasicMaterial ( { color : 0xFFAA30 , transparent : true , opacity : 0.7 , blending : THREE . AdditiveBlending , depthWrite : false , depthTest : false , side : THREE . DoubleSide } ) ) ;
1117+ mid . rotation . x = Math . PI ;
1118+ mid . position . y = - 3 ;
1119+ grp . add ( mid ) ;
1120+ // Inner core (white-yellow)
1121+ var inner = new THREE . Mesh ( new THREE . ConeGeometry ( r * 0.85 , 4 , 10 , 1 , true ) , new THREE . MeshBasicMaterial ( { color : 0xFFFFC0 , transparent : true , opacity : 0.9 , blending : THREE . AdditiveBlending , depthWrite : false , depthTest : false , side : THREE . DoubleSide } ) ) ;
1122+ inner . rotation . x = Math . PI ;
1123+ inner . position . y = - 2 ;
1124+ grp . add ( inner ) ;
1125+ // Flame length scales linearly with thrust magnitude
1126+ grp . userData = { isFlame : true , nominalThrust : nominalThrust , baseLen : Math . max ( 0.6 , Math . sqrt ( nominalThrust ) * 0.08 ) } ;
1127+ grp . visible = false ;
1128+ return grp ;
1129+ }
1130+
11071131/* ===== 3D ROCKET MESH ===== */
11081132function buildRocketMeshes ( parts ) {
11091133 while ( rocketGroup . children . length > 0 ) {
@@ -1123,18 +1147,23 @@ <h2 id="resultTitle">Orbit Achieved!</h2>
11231147 var N = def . radial ;
11241148 var boosterR = def . w / 2 * PART_SCALE ;
11251149 var offset = STACK_R + boosterR * 1.15 ;
1150+ var perUnitThrust = def . thrust / N ;
11261151 for ( var j = 0 ; j < N ; j ++ ) {
11271152 var ang = ( j / N ) * Math . PI * 2 ;
11281153 var bm = new THREE . Mesh ( new THREE . CylinderGeometry ( boosterR , boosterR , h , 8 ) , new THREE . MeshPhongMaterial ( { color : def . color } ) ) ;
11291154 bm . position . set ( offset * Math . cos ( ang ) , y + h / 2 , offset * Math . sin ( ang ) ) ;
11301155 rocketGroup . add ( bm ) ;
1131- // Tiny nozzle cone
11321156 var noz = new THREE . Mesh ( new THREE . ConeGeometry ( boosterR * 0.8 , boosterR , 8 ) , new THREE . MeshPhongMaterial ( { color : 0x303030 } ) ) ;
1133- noz . rotation . x = Math . PI ; // cone apex down
1157+ noz . rotation . x = Math . PI ;
11341158 noz . position . set ( offset * Math . cos ( ang ) , y - boosterR * 0.5 , offset * Math . sin ( ang ) ) ;
11351159 rocketGroup . add ( noz ) ;
1160+ var flame = buildFlameGroup ( perUnitThrust , def . w / 2 ) ;
1161+ flame . position . set ( offset * Math . cos ( ang ) , y - boosterR , offset * Math . sin ( ang ) ) ;
1162+ flame . userData . stage = parts [ i ] . stage ;
1163+ flame . userData . partIdx = i ;
1164+ rocketGroup . add ( flame ) ;
11361165 }
1137- continue ; // radials don't advance y
1166+ continue ;
11381167 }
11391168 if ( def . type === 'leg' ) {
11401169 var legLen = def . h * PART_SCALE ;
@@ -1155,23 +1184,26 @@ <h2 id="resultTitle">Orbit Achieved!</h2>
11551184 continue ;
11561185 }
11571186 if ( def . type === 'abort' ) {
1158- // Central tapered spire
11591187 var spireH = h , spireR = def . w / 2 * PART_SCALE ;
11601188 var spire = new THREE . Mesh ( new THREE . CylinderGeometry ( spireR * 0.2 , spireR * 0.9 , spireH * 0.55 , 8 ) , new THREE . MeshPhongMaterial ( { color : def . color } ) ) ;
11611189 spire . position . y = y + spireH * 0.725 ;
11621190 rocketGroup . add ( spire ) ;
1163- // Thruster block at base
1164- var base = new THREE . Mesh ( new THREE . CylinderGeometry ( spireR * 0.95 , spireR * 1.05 , spireH * 0.35 , 8 ) , new THREE . MeshPhongMaterial ( { color : 0x803030 } ) ) ;
1165- base . position . y = y + spireH * 0.25 ;
1166- rocketGroup . add ( base ) ;
1167- // 4 radial nozzles canted downward (pulling pod up)
1191+ var basePart = new THREE . Mesh ( new THREE . CylinderGeometry ( spireR * 0.95 , spireR * 1.05 , spireH * 0.35 , 8 ) , new THREE . MeshPhongMaterial ( { color : 0x803030 } ) ) ;
1192+ basePart . position . y = y + spireH * 0.25 ;
1193+ rocketGroup . add ( basePart ) ;
11681194 for ( var j = 0 ; j < 4 ; j ++ ) {
11691195 var ang = j * Math . PI / 2 ;
11701196 var noz = new THREE . Mesh ( new THREE . ConeGeometry ( spireR * 0.3 , spireR * 0.9 , 6 ) , new THREE . MeshPhongMaterial ( { color : 0x202428 } ) ) ;
11711197 noz . position . set ( spireR * 1.1 * Math . cos ( ang ) , y + spireH * 0.1 , spireR * 1.1 * Math . sin ( ang ) ) ;
11721198 noz . rotation . z = Math . cos ( ang ) * 0.5 ;
11731199 noz . rotation . x = Math . sin ( ang ) * 0.5 ;
11741200 rocketGroup . add ( noz ) ;
1201+ var fl = buildFlameGroup ( def . thrust / 4 , def . w / 4 ) ;
1202+ fl . position . set ( spireR * 1.3 * Math . cos ( ang ) , y + spireH * 0.05 , spireR * 1.3 * Math . sin ( ang ) ) ;
1203+ // Nozzles are canted outward; flame along their axis but simplification: point downward
1204+ fl . userData . stage = parts [ i ] . stage ;
1205+ fl . userData . partIdx = i ;
1206+ rocketGroup . add ( fl ) ;
11751207 }
11761208 y += spireH ;
11771209 continue ;
@@ -1235,6 +1267,14 @@ <h2 id="resultTitle">Orbit Achieved!</h2>
12351267 mesh . position . y = y + h / 2 ;
12361268 }
12371269 rocketGroup . add ( mesh ) ;
1270+ // Attach a flame to engines and SRBs (stack-mounted)
1271+ if ( def . type === 'engine' || def . type === 'srb' ) {
1272+ var fl2 = buildFlameGroup ( def . thrust , def . w / 2 ) ;
1273+ fl2 . position . y = y ;
1274+ fl2 . userData . stage = parts [ i ] . stage ;
1275+ fl2 . userData . partIdx = i ;
1276+ rocketGroup . add ( fl2 ) ;
1277+ }
12381278 y += h ;
12391279 }
12401280 flameLight . position . y = - 2 ;
@@ -1683,6 +1723,23 @@ <h2 id="resultTitle">Orbit Achieved!</h2>
16831723
16841724 // Flame light
16851725 flameLight . intensity = sim . thrustN > 0 ? sim . throttle * 3 : 0 ;
1726+ // Engine flame animation: scale length by throttle, jitter for life
1727+ for ( var fi = 0 ; fi < rocketGroup . children . length ; fi ++ ) {
1728+ var ch = rocketGroup . children [ fi ] ;
1729+ if ( ! ch . userData || ! ch . userData . isFlame ) continue ;
1730+ var isCurStage = ch . userData . stage === sim . currentStage ;
1731+ var firing = isCurStage && sim . throttle > 0 && sim . thrustN > 0 ;
1732+ if ( firing ) {
1733+ var throttle = sim . throttle ;
1734+ var jitter = 0.85 + Math . random ( ) * 0.3 ;
1735+ var lenScale = ch . userData . baseLen * throttle * jitter ;
1736+ var widthScale = ( 0.7 + throttle * 0.4 ) * ( 0.9 + Math . random ( ) * 0.2 ) ;
1737+ ch . scale . set ( widthScale , lenScale , widthScale ) ;
1738+ ch . visible = true ;
1739+ } else {
1740+ ch . visible = false ;
1741+ }
1742+ }
16861743
16871744 // Exhaust particles
16881745 if ( sim . thrustN > 0 ) {
@@ -1762,18 +1819,18 @@ <h2 id="resultTitle">Orbit Achieved!</h2>
17621819 var offsetDir = rotated . multiplyScalar ( cp ) . add ( radial . clone ( ) . multiplyScalar ( sp ) ) ;
17631820 var camTarget = focusPoint . clone ( ) . add ( offsetDir . multiplyScalar ( camDist ) ) ;
17641821
1765- // Screen shake
1822+ // Screen shake only at real g-loads (> 2g) and high throttle — prevents constant jitter at launch
17661823 var gForce = sim . accel / ENV . g0 ;
1767- if ( sim . thrustN > 0 && gForce > 1 ) {
1768- var shake = Math . min ( gForce * 0.3 , 3 ) ;
1824+ if ( sim . thrustN > 0 && gForce > 2 && sim . throttle > 0.5 ) {
1825+ var shake = Math . min ( ( gForce - 2 ) * 0.25 , 1.5 ) ;
17691826 camTarget . add ( new THREE . Vector3 ( ( Math . random ( ) - 0.5 ) * shake , ( Math . random ( ) - 0.5 ) * shake , ( Math . random ( ) - 0.5 ) * shake ) ) ;
17701827 }
17711828
17721829 if ( ! camera . _initialized ) {
17731830 camera . position . copy ( camTarget ) ;
17741831 camera . _initialized = true ;
17751832 } else {
1776- camera . position . lerp ( camTarget , 0.15 ) ;
1833+ camera . position . lerp ( camTarget , 0.08 ) ;
17771834 }
17781835 camera . up . copy ( radial ) ;
17791836 camera . lookAt ( focusPoint ) ;
@@ -2707,26 +2764,58 @@ <h2 id="resultTitle">Orbit Achieved!</h2>
27072764 try {
27082765 audio . ctx = new ( window . AudioContext || window . webkitAudioContext ) ( ) ;
27092766 } catch ( e ) { return ; }
2767+ // Layered sine oscillators for a deep chest-rumble instead of a buzzy sawtooth
27102768 audio . engineOsc = audio . ctx . createOscillator ( ) ;
2711- audio . engineOsc . type = 'sawtooth' ;
2712- audio . engineOsc . frequency . value = 48 ;
2713- var noiseBuf = audio . ctx . createBuffer ( 1 , audio . ctx . sampleRate * 2 , audio . ctx . sampleRate ) ;
2769+ audio . engineOsc . type = 'sine' ;
2770+ audio . engineOsc . frequency . value = 42 ;
2771+ audio . engineOsc2 = audio . ctx . createOscillator ( ) ;
2772+ audio . engineOsc2 . type = 'sine' ;
2773+ audio . engineOsc2 . frequency . value = 63 ;
2774+ audio . engineOsc3 = audio . ctx . createOscillator ( ) ;
2775+ audio . engineOsc3 . type = 'triangle' ;
2776+ audio . engineOsc3 . frequency . value = 98 ;
2777+ // Pink noise for natural turbulence
2778+ var noiseBuf = audio . ctx . createBuffer ( 1 , audio . ctx . sampleRate * 3 , audio . ctx . sampleRate ) ;
27142779 var nd = noiseBuf . getChannelData ( 0 ) ;
2715- for ( var i = 0 ; i < nd . length ; i ++ ) nd [ i ] = ( Math . random ( ) * 2 - 1 ) * 0.4 ;
2780+ var b0 = 0 , b1 = 0 , b2 = 0 , b3 = 0 , b4 = 0 , b5 = 0 , b6 = 0 ;
2781+ for ( var i = 0 ; i < nd . length ; i ++ ) {
2782+ var w = Math . random ( ) * 2 - 1 ;
2783+ b0 = 0.99886 * b0 + w * 0.0555179 ;
2784+ b1 = 0.99332 * b1 + w * 0.0750759 ;
2785+ b2 = 0.96900 * b2 + w * 0.1538520 ;
2786+ b3 = 0.86650 * b3 + w * 0.3104856 ;
2787+ b4 = 0.55000 * b4 + w * 0.5329522 ;
2788+ b5 = - 0.7616 * b5 - w * 0.0168980 ;
2789+ nd [ i ] = ( b0 + b1 + b2 + b3 + b4 + b5 + b6 + w * 0.5362 ) * 0.09 ;
2790+ b6 = w * 0.115926 ;
2791+ }
27162792 audio . noiseSrc = audio . ctx . createBufferSource ( ) ;
27172793 audio . noiseSrc . buffer = noiseBuf ;
27182794 audio . noiseSrc . loop = true ;
27192795 audio . engineFilter = audio . ctx . createBiquadFilter ( ) ;
27202796 audio . engineFilter . type = 'lowpass' ;
27212797 audio . engineFilter . frequency . value = 300 ;
2722- audio . engineFilter . Q . value = 1 ;
2798+ audio . engineFilter . Q . value = 0.7 ;
2799+ // A touch of distortion via waveshaper at the tail
2800+ audio . engineShaper = audio . ctx . createWaveShaper ( ) ;
2801+ var curve = new Float32Array ( 1024 ) ;
2802+ for ( var si = 0 ; si < 1024 ; si ++ ) {
2803+ var x = si / 512 - 1 ;
2804+ curve [ si ] = Math . tanh ( x * 1.4 ) ;
2805+ }
2806+ audio . engineShaper . curve = curve ;
27232807 audio . engineGain = audio . ctx . createGain ( ) ;
27242808 audio . engineGain . gain . value = 0 ;
27252809 audio . noiseSrc . connect ( audio . engineFilter ) ;
27262810 audio . engineOsc . connect ( audio . engineFilter ) ;
2727- audio . engineFilter . connect ( audio . engineGain ) ;
2811+ audio . engineOsc2 . connect ( audio . engineFilter ) ;
2812+ audio . engineOsc3 . connect ( audio . engineFilter ) ;
2813+ audio . engineFilter . connect ( audio . engineShaper ) ;
2814+ audio . engineShaper . connect ( audio . engineGain ) ;
27282815 audio . engineGain . connect ( audio . ctx . destination ) ;
27292816 audio . engineOsc . start ( ) ;
2817+ audio . engineOsc2 . start ( ) ;
2818+ audio . engineOsc3 . start ( ) ;
27302819 audio . noiseSrc . start ( ) ;
27312820 // Reentry wind: separate noise + bandpass chain, driven by Q
27322821 audio . windSrc = audio . ctx . createBufferSource ( ) ;
@@ -2753,11 +2842,14 @@ <h2 id="resultTitle">Orbit Achieved!</h2>
27532842 }
27542843 var thrFrac = s . thrustN > 0 ? s . throttle : 0 ;
27552844 var rho = s . parent === 'earth' ? atmDensity ( s . altitude ) : 0 ;
2756- var atmFactor = Math . min ( 1 , rho / 0.4 + 0.15 ) ;
2757- var vol = thrFrac * 0.35 * atmFactor ;
2758- audio . engineGain . gain . setTargetAtTime ( vol , audio . ctx . currentTime , 0.05 ) ;
2759- audio . engineFilter . frequency . setTargetAtTime ( 100 + atmFactor * 450 + thrFrac * 250 , audio . ctx . currentTime , 0.05 ) ;
2760- // Reentry wind: Q-driven, pitched by speed
2845+ var atmFactor = Math . min ( 1 , rho / 0.4 ) ;
2846+ // Audible but muffled in vacuum (cinematic convention) — 30% volume, hard low-pass
2847+ var vacFactor = 0.3 + atmFactor * 0.7 ;
2848+ var vol = thrFrac * 0.3 * vacFactor ;
2849+ audio . engineGain . gain . setTargetAtTime ( vol , audio . ctx . currentTime , 0.08 ) ;
2850+ // Cutoff: vacuum muffled (~200Hz), atm full range (~1800Hz). Throttle adds brightness.
2851+ var cutoff = 180 + atmFactor * 1500 + thrFrac * 350 ;
2852+ audio . engineFilter . frequency . setTargetAtTime ( cutoff , audio . ctx . currentTime , 0.08 ) ;
27612853 if ( audio . windGain && audio . windFilter ) {
27622854 var q = s . q || 0 ;
27632855 var windVol = Math . min ( 0.35 , q / 80000 ) ;
@@ -2862,7 +2954,7 @@ <h2 id="resultTitle">Orbit Achieved!</h2>
28622954 var canvas = document . getElementById ( 'navballCanvas' ) ;
28632955 if ( ! canvas ) return ;
28642956 navRenderer = new THREE . WebGLRenderer ( { canvas : canvas , alpha : true , antialias : true } ) ;
2865- navRenderer . setSize ( 220 , 220 ) ;
2957+ navRenderer . setSize ( 170 , 170 ) ;
28662958 navRenderer . setPixelRatio ( window . devicePixelRatio || 1 ) ;
28672959 navScene = new THREE . Scene ( ) ;
28682960 navCamera = new THREE . PerspectiveCamera ( 65 , 1 , 0.01 , 10 ) ;
0 commit comments