Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Nuclear Reactor Simulator</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Roboto+Mono:wght@400;500;700&display=swap'); | |
| :root { | |
| --bg: #0a0a0f; | |
| --primary: #00b4ff; | |
| --caution: #ffab00; | |
| --alarm: #ff3b30; | |
| --coolant: #4df0ff; | |
| --graphite: #444; | |
| --glow: #00d1ff33; | |
| --success: #32d74b; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| background: var(--bg); | |
| color: var(--primary); | |
| font-family: 'Roboto Mono', monospace; | |
| overflow: hidden; | |
| height: 100vh; | |
| position: relative; | |
| } | |
| .container { | |
| display: flex; | |
| height: 100vh; | |
| } | |
| .control-panel { | |
| width: 35%; | |
| background: linear-gradient(135deg, #0d0d1a, #1a1a2e); | |
| border-right: 2px solid var(--primary); | |
| padding: 20px; | |
| overflow-y: auto; | |
| position: relative; | |
| } | |
| .reactor-cavity { | |
| width: 65%; | |
| background: radial-gradient(ellipse at center, #0f0f1f, var(--bg)); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .panel-header { | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 700; | |
| font-size: 1.2em; | |
| color: var(--primary); | |
| margin-bottom: 20px; | |
| text-align: center; | |
| text-shadow: 0 0 10px var(--glow); | |
| } | |
| .control-group { | |
| margin-bottom: 20px; | |
| background: rgba(0, 180, 255, 0.05); | |
| border: 1px solid rgba(0, 180, 255, 0.3); | |
| border-radius: 8px; | |
| padding: 15px; | |
| } | |
| .control-label { | |
| font-size: 0.9em; | |
| margin-bottom: 8px; | |
| color: var(--coolant); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .slider-container { | |
| position: relative; | |
| height: 30px; | |
| background: var(--graphite); | |
| border-radius: 15px; | |
| margin: 10px 0; | |
| } | |
| .slider { | |
| width: 100%; | |
| height: 100%; | |
| background: transparent; | |
| outline: none; | |
| border: none; | |
| border-radius: 15px; | |
| -webkit-appearance: none; | |
| appearance: none; | |
| cursor: pointer; | |
| } | |
| .slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 25px; | |
| height: 25px; | |
| background: var(--primary); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| box-shadow: 0 0 15px var(--glow); | |
| border: 2px solid var(--coolant); | |
| } | |
| .slider::-moz-range-thumb { | |
| width: 25px; | |
| height: 25px; | |
| background: var(--primary); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| box-shadow: 0 0 15px var(--glow); | |
| border: 2px solid var(--coolant); | |
| } | |
| .power-bar { | |
| height: 200px; | |
| width: 40px; | |
| background: var(--graphite); | |
| border-radius: 20px; | |
| position: relative; | |
| margin: 0 auto; | |
| border: 2px solid var(--primary); | |
| } | |
| .power-fill { | |
| position: absolute; | |
| bottom: 2px; | |
| left: 2px; | |
| right: 2px; | |
| background: linear-gradient(0deg, var(--primary), var(--coolant)); | |
| border-radius: 18px; | |
| transition: height 0.3s ease; | |
| box-shadow: 0 0 20px var(--glow); | |
| height: 0%; | |
| } | |
| .gauge { | |
| width: 100px; | |
| height: 100px; | |
| margin: 10px auto; | |
| position: relative; | |
| background: radial-gradient(circle, var(--graphite), #222); | |
| border-radius: 50%; | |
| border: 3px solid var(--primary); | |
| } | |
| .gauge-needle { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 2px; | |
| height: 40px; | |
| background: var(--alarm); | |
| transform-origin: bottom center; | |
| transform: translate(-50%, -100%) rotate(-90deg); | |
| transition: transform 0.3s ease; | |
| box-shadow: 0 0 10px var(--alarm); | |
| } | |
| .action-button { | |
| background: var(--primary); | |
| color: var(--bg); | |
| border: none; | |
| padding: 12px 20px; | |
| border-radius: 8px; | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| margin: 5px; | |
| font-size: 12px; | |
| display: inline-block; | |
| min-width: 120px; | |
| } | |
| .action-button:hover { | |
| background: var(--coolant); | |
| box-shadow: 0 0 20px var(--glow); | |
| transform: scale(1.02); | |
| } | |
| .action-button.caution { | |
| background: var(--caution); | |
| } | |
| .action-button.caution:hover { | |
| background: #ff8f00; | |
| } | |
| .action-button.success { | |
| background: var(--success); | |
| } | |
| .action-button.success:hover { | |
| background: #28a745; | |
| } | |
| .scram-button { | |
| background: var(--alarm); | |
| color: white; | |
| border: none; | |
| padding: 15px 25px; | |
| border-radius: 10px; | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| margin: 20px auto; | |
| display: block; | |
| width: 100%; | |
| font-size: 16px; | |
| } | |
| .scram-button:hover { | |
| background: #ff1a1a; | |
| box-shadow: 0 0 30px var(--alarm); | |
| transform: scale(1.05); | |
| } | |
| .emergency-controls { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| margin-top: 15px; | |
| } | |
| .annunciator-grid { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 5px; | |
| margin: 15px 0; | |
| } | |
| .annunciator { | |
| width: 25px; | |
| height: 25px; | |
| background: var(--graphite); | |
| border-radius: 3px; | |
| border: 1px solid var(--primary); | |
| transition: all 0.3s ease; | |
| position: relative; | |
| } | |
| .annunciator.active { | |
| background: var(--alarm); | |
| box-shadow: 0 0 10px var(--alarm); | |
| animation: blink 1s infinite; | |
| } | |
| .annunciator.caution { | |
| background: var(--caution); | |
| box-shadow: 0 0 10px var(--caution); | |
| } | |
| @keyframes blink { | |
| 0%, 50% { opacity: 1; } | |
| 51%, 100% { opacity: 0.3; } | |
| } | |
| .reactor-3d { | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .reactor-core { | |
| width: 300px; | |
| height: 400px; | |
| background: linear-gradient(45deg, var(--graphite), #666); | |
| border-radius: 20px; | |
| border: 2px solid var(--primary); | |
| overflow: hidden; | |
| box-shadow: 0 0 50px var(--glow); | |
| position: relative; | |
| } | |
| .fuel-assembly-grid { | |
| display: grid; | |
| grid-template-columns: repeat(17, 1fr); | |
| grid-template-rows: repeat(17, 1fr); | |
| height: 100%; | |
| padding: 10px; | |
| gap: 1px; | |
| } | |
| .fuel-pin { | |
| background: var(--primary); | |
| border-radius: 1px; | |
| transition: all 0.3s ease; | |
| opacity: 0.3; | |
| } | |
| .fuel-pin.hot { | |
| background: var(--coolant); | |
| box-shadow: 0 0 3px var(--coolant); | |
| opacity: 1; | |
| } | |
| .control-rod { | |
| position: absolute; | |
| width: 8px; | |
| height: 300px; | |
| background: linear-gradient(0deg, var(--graphite), #666); | |
| border: 1px solid var(--primary); | |
| left: 50%; | |
| top: 10px; | |
| transform: translateX(-50%); | |
| transition: top 0.5s ease; | |
| border-radius: 4px; | |
| } | |
| .coolant-loops { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| top: 0; | |
| left: 0; | |
| } | |
| .coolant-loop { | |
| position: absolute; | |
| width: 60px; | |
| height: 180px; | |
| border: 4px solid var(--coolant); | |
| border-radius: 30px; | |
| background: transparent; | |
| } | |
| .loop-1 { top: 50px; left: -40px; } | |
| .loop-2 { top: 50px; right: -40px; } | |
| .loop-3 { bottom: 50px; left: -40px; } | |
| .loop-4 { bottom: 50px; right: -40px; } | |
| .coolant-particle { | |
| position: absolute; | |
| width: 4px; | |
| height: 8px; | |
| background: var(--coolant); | |
| border-radius: 2px; | |
| opacity: 0.8; | |
| } | |
| .readout { | |
| font-family: 'Roboto Mono', monospace; | |
| font-size: 0.9em; | |
| color: var(--coolant); | |
| text-align: center; | |
| background: rgba(0, 0, 0, 0.8); | |
| padding: 5px 8px; | |
| border-radius: 5px; | |
| border: 1px solid var(--primary); | |
| margin-top: 10px; | |
| } | |
| .status-indicator { | |
| display: inline-block; | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| margin-right: 8px; | |
| animation: pulse 2s infinite; | |
| } | |
| .status-indicator.critical { | |
| background: var(--alarm); | |
| } | |
| .status-indicator.subcritical { | |
| background: var(--caution); | |
| } | |
| .status-indicator.shutdown { | |
| background: var(--success); | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| .tooltip { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: rgba(0, 0, 0, 0.95); | |
| border: 2px solid var(--primary); | |
| border-radius: 15px; | |
| padding: 30px; | |
| max-width: 500px; | |
| text-align: center; | |
| font-family: 'Orbitron', sans-serif; | |
| z-index: 1000; | |
| box-shadow: 0 0 30px var(--glow); | |
| } | |
| .close-tooltip { | |
| margin-top: 20px; | |
| padding: 10px 20px; | |
| background: var(--primary); | |
| color: var(--bg); | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 700; | |
| } | |
| .particles { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| overflow: hidden; | |
| } | |
| .neutron { | |
| position: absolute; | |
| width: 3px; | |
| height: 3px; | |
| background: #00ffff; | |
| border-radius: 50%; | |
| box-shadow: 0 0 6px #00ffff; | |
| } | |
| .audio-controls { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin: 10px 0; | |
| } | |
| .volume-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .mute-button { | |
| background: var(--graphite); | |
| color: var(--primary); | |
| border: 1px solid var(--primary); | |
| padding: 8px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-family: 'Roboto Mono', monospace; | |
| font-size: 12px; | |
| } | |
| .mute-button.muted { | |
| background: var(--alarm); | |
| color: white; | |
| } | |
| .scanlines { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: repeating-linear-gradient( | |
| 0deg, | |
| transparent, | |
| transparent 2px, | |
| rgba(0, 180, 255, 0.02) 2px, | |
| rgba(0, 180, 255, 0.02) 4px | |
| ); | |
| pointer-events: none; | |
| z-index: 10; | |
| } | |
| @media (max-width: 900px) { | |
| .container { | |
| flex-direction: column; | |
| } | |
| .control-panel { | |
| width: 100%; | |
| height: 40%; | |
| } | |
| .reactor-cavity { | |
| width: 100%; | |
| height: 60%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="scanlines"></div> | |
| <div class="tooltip" id="welcomeTooltip"> | |
| <h2>Welcome, Dr. Voss</h2> | |
| <p>Reactor is cold and shutdown. Bring power to 100% without exceeding DNBR < 1.3.</p> | |
| <p style="color: var(--caution); margin-top: 15px;">Good hunting.</p> | |
| <button class="close-tooltip" onclick="closeTooltip()">Initialize Reactor</button> | |
| </div> | |
| <div class="container"> | |
| <div class="control-panel"> | |
| <div class="panel-header">CONTROL ROD DRIVE PANEL</div> | |
| <div class="control-group"> | |
| <div class="control-label">Reactor Power</div> | |
| <div class="power-bar"> | |
| <div class="power-fill" id="powerFill"></div> | |
| </div> | |
| <div class="readout" id="powerReadout"> | |
| <span class="status-indicator shutdown" id="statusIndicator"></span> | |
| 0 MWth (0%) | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label">Core ΔT</div> | |
| <div class="gauge"> | |
| <div class="gauge-needle" id="tempNeedle"></div> | |
| </div> | |
| <div class="readout" id="tempReadout">0°C</div> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label">Control Rod Position</div> | |
| <div class="slider-container"> | |
| <input type="range" class="slider" id="rodPosition" min="0" max="100" value="0"> | |
| </div> | |
| <div class="readout" id="rodReadout">0% Withdrawn</div> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label">Boron Concentration</div> | |
| <div class="slider-container"> | |
| <input type="range" class="slider" id="boronLevel" min="0" max="2000" value="2000" step="50"> | |
| </div> | |
| <div class="readout" id="boronReadout">2000 ppm</div> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label">RCS Pressure</div> | |
| <div class="gauge"> | |
| <div class="gauge-needle" id="pressureNeedle"></div> | |
| </div> | |
| <div class="readout" id="pressureReadout">15.5 MPa</div> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label">Audio System</div> | |
| <div class="audio-controls"> | |
| <button class="mute-button" id="muteButton">🔊 Audio On</button> | |
| <div class="volume-control"> | |
| <span style="font-size: 12px;">Vol:</span> | |
| <input type="range" class="slider" id="volumeControl" min="0" max="100" value="50" style="width: 80px; height: 20px;"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label">Emergency Procedures</div> | |
| <div class="emergency-controls"> | |
| <button class="action-button success" id="startupBtn">Startup</button> | |
| <button class="action-button caution" id="cooldownBtn">Cooldown</button> | |
| <button class="action-button" id="criticalityBtn">To Critical</button> | |
| <button class="action-button caution" id="resetBtn">Reset All</button> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label">Annunciators</div> | |
| <div class="annunciator-grid" id="annunciatorGrid"></div> | |
| </div> | |
| <button class="scram-button" id="scramButton">SCRAM</button> | |
| <div class="control-group"> | |
| <div class="control-label">Time Scale</div> | |
| <div class="slider-container"> | |
| <input type="range" class="slider" id="timeScale" min="0.1" max="5" value="1" step="0.1"> | |
| </div> | |
| <div class="readout" id="timeReadout">1.0x Real Time</div> | |
| </div> | |
| </div> | |
| <div class="reactor-cavity"> | |
| <div class="reactor-3d"> | |
| <div class="reactor-core" id="reactorCore"> | |
| <div class="fuel-assembly-grid" id="fuelGrid"></div> | |
| <div class="control-rod" id="controlRod"></div> | |
| <div class="coolant-loops"> | |
| <div class="coolant-loop loop-1"></div> | |
| <div class="coolant-loop loop-2"></div> | |
| <div class="coolant-loop loop-3"></div> | |
| <div class="coolant-loop loop-4"></div> | |
| </div> | |
| </div> | |
| <div class="particles" id="particles"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Global variables | |
| let reactor; | |
| // Utility functions (defined first to avoid reference errors) | |
| function closeTooltip() { | |
| const tooltip = document.getElementById('welcomeTooltip'); | |
| if (tooltip) { | |
| tooltip.style.display = 'none'; | |
| } | |
| // Start the reactor simulation | |
| if (reactor) { | |
| reactor.start(); | |
| } | |
| } | |
| // Enhanced Audio System | |
| class ReactorAudio { | |
| constructor() { | |
| this.audioContext = null; | |
| this.enabled = false; | |
| this.muted = false; | |
| this.volume = 0.5; | |
| this.alarmInterval = null; | |
| this.backgroundHum = null; | |
| this.init(); | |
| } | |
| init() { | |
| try { | |
| this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| this.enabled = true; | |
| console.log('Audio system initialized successfully'); | |
| } catch (e) { | |
| console.log('Audio not available:', e); | |
| this.enabled = false; | |
| } | |
| } | |
| async ensureAudioContext() { | |
| if (this.audioContext && this.audioContext.state === 'suspended') { | |
| try { | |
| await this.audioContext.resume(); | |
| } catch (e) { | |
| console.log('Failed to resume audio context:', e); | |
| } | |
| } | |
| } | |
| playTone(frequency, duration = 0.1, type = 'sine', volume = null) { | |
| if (!this.enabled || !this.audioContext || this.muted) return; | |
| this.ensureAudioContext(); | |
| try { | |
| const oscillator = this.audioContext.createOscillator(); | |
| const gainNode = this.audioContext.createGain(); | |
| oscillator.type = type; | |
| oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime); | |
| const vol = (volume !== null ? volume : this.volume) * 0.3; | |
| gainNode.gain.setValueAtTime(vol, this.audioContext.currentTime); | |
| gainNode.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + duration); | |
| oscillator.connect(gainNode); | |
| gainNode.connect(this.audioContext.destination); | |
| oscillator.start(); | |
| oscillator.stop(this.audioContext.currentTime + duration); | |
| } catch (e) { | |
| console.log('Audio playback failed:', e); | |
| } | |
| } | |
| playClick() { | |
| this.playTone(800, 0.05, 'square', 0.2); | |
| } | |
| playAlarm() { | |
| if (this.alarmInterval) return; // Already playing | |
| this.playTone(1000, 0.8, 'sine', 0.8); | |
| // Continue alarm pattern | |
| this.alarmInterval = setInterval(() => { | |
| this.playTone(1000, 0.4, 'sine', 0.6); | |
| setTimeout(() => { | |
| this.playTone(1200, 0.4, 'sine', 0.6); | |
| }, 500); | |
| }, 1500); | |
| // Stop after 10 seconds | |
| setTimeout(() => { | |
| this.stopAlarm(); | |
| }, 10000); | |
| } | |
| stopAlarm() { | |
| if (this.alarmInterval) { | |
| clearInterval(this.alarmInterval); | |
| this.alarmInterval = null; | |
| } | |
| } | |
| playCriticalityAlarm() { | |
| this.stopAlarm(); | |
| this.playTone(1500, 0.2, 'sawtooth', 0.7); | |
| setTimeout(() => { | |
| this.playTone(1200, 0.2, 'sawtooth', 0.7); | |
| }, 300); | |
| setTimeout(() => { | |
| this.playTone(1800, 0.3, 'sawtooth', 0.7); | |
| }, 600); | |
| } | |
| playSubcriticalBeep() { | |
| this.playTone(600, 0.15, 'triangle', 0.4); | |
| } | |
| startBackgroundHum(powerLevel = 0) { | |
| if (!this.enabled || this.muted) return; | |
| this.ensureAudioContext(); | |
| try { | |
| if (this.backgroundHum) { | |
| this.backgroundHum.frequency.setValueAtTime( | |
| 50 + (powerLevel * 100), | |
| this.audioContext.currentTime | |
| ); | |
| this.backgroundHum.gain.gain.setValueAtTime( | |
| Math.min(0.1, powerLevel * 0.05) * this.volume, | |
| this.audioContext.currentTime | |
| ); | |
| return; | |
| } | |
| const oscillator = this.audioContext.createOscillator(); | |
| const gainNode = this.audioContext.createGain(); | |
| const filterNode = this.audioContext.createBiquadFilter(); | |
| oscillator.type = 'sawtooth'; | |
| oscillator.frequency.setValueAtTime(50 + (powerLevel * 100), this.audioContext.currentTime); | |
| filterNode.type = 'lowpass'; | |
| filterNode.frequency.setValueAtTime(200, this.audioContext.currentTime); | |
| gainNode.gain.setValueAtTime(Math.min(0.1, powerLevel * 0.05) * this.volume, this.audioContext.currentTime); | |
| oscillator.connect(filterNode); | |
| filterNode.connect(gainNode); | |
| gainNode.connect(this.audioContext.destination); | |
| oscillator.start(); | |
| this.backgroundHum = { | |
| oscillator: oscillator, | |
| gain: gainNode, | |
| frequency: oscillator.frequency | |
| }; | |
| } catch (e) { | |
| console.log('Background hum failed:', e); | |
| } | |
| } | |
| stopBackgroundHum() { | |
| if (this.backgroundHum) { | |
| try { | |
| this.backgroundHum.oscillator.stop(); | |
| } catch (e) {} | |
| this.backgroundHum = null; | |
| } | |
| } | |
| setMute(muted) { | |
| this.muted = muted; | |
| if (muted) { | |
| this.stopAlarm(); | |
| this.stopBackgroundHum(); | |
| } | |
| } | |
| setVolume(volume) { | |
| this.volume = Math.max(0, Math.min(1, volume)); | |
| } | |
| } | |
| // Reactor Physics Engine | |
| class ReactorPhysics { | |
| constructor() { | |
| this.power = 0; // MWth | |
| this.rodPosition = 0; // % withdrawn | |
| this.boronConc = 2000; // ppm | |
| this.pressure = 15.5; // MPa | |
| this.temperature = 291; // °C | |
| this.timeScale = 1.0; | |
| this.scrammed = false; | |
| this.running = false; | |
| this.lastState = 'shutdown'; | |
| // Physics constants | |
| this.maxPower = 3000; // MWth | |
| this.criticalBoron = 1000; // ppm for criticality | |
| this.lastUpdate = Date.now(); | |
| } | |
| update() { | |
| if (!this.running) return; | |
| const now = Date.now(); | |
| const dt = Math.min((now - this.lastUpdate) / 1000 * this.timeScale, 0.1); | |
| this.lastUpdate = now; | |
| if (this.scrammed) { | |
| this.power = Math.max(0, this.power - (this.power * 2 * dt)); | |
| return; | |
| } | |
| // Simple reactivity calculation | |
| const reactivity = this.calculateReactivity(); | |
| // Power change based on reactivity | |
| if (reactivity > 0) { | |
| this.power = Math.min(this.maxPower, this.power + (reactivity * 1000 * dt)); | |
| } else { | |
| this.power = Math.max(0, this.power + (reactivity * 1000 * dt)); | |
| } | |
| // Temperature response | |
| if (this.power > 0) { | |
| const targetTemp = 291 + (this.power / this.maxPower) * 50; | |
| this.temperature += (targetTemp - this.temperature) * 2 * dt; | |
| } else { | |
| this.temperature += (291 - this.temperature) * 1 * dt; | |
| } | |
| // Pressure response | |
| const tempRatio = this.temperature / 291; | |
| this.pressure = 15.5 * tempRatio; | |
| } | |
| calculateReactivity() { | |
| // Rod reactivity | |
| const rodReactivity = (this.rodPosition / 100) * 0.2; | |
| // Boron reactivity | |
| const boronReactivity = -(this.boronConc - this.criticalBoron) * 0.0002; | |
| // Temperature feedback | |
| const tempReactivity = -(this.temperature - 291) * 0.0001; | |
| // Base subcritical | |
| return rodReactivity + boronReactivity + tempReactivity - 0.05; | |
| } | |
| getState() { | |
| if (this.scrammed) return 'scrammed'; | |
| if (this.power > 1000) return 'critical'; | |
| if (this.power > 10) return 'subcritical'; | |
| return 'shutdown'; | |
| } | |
| scram() { | |
| this.scrammed = true; | |
| this.rodPosition = 0; | |
| } | |
| reset() { | |
| this.scrammed = false; | |
| this.power = 0; | |
| this.temperature = 291; | |
| this.pressure = 15.5; | |
| this.rodPosition = 0; | |
| this.boronConc = 2000; | |
| this.lastState = 'shutdown'; | |
| } | |
| start() { | |
| this.running = true; | |
| this.lastUpdate = Date.now(); | |
| } | |
| } | |
| // Main Reactor Simulator | |
| class ReactorSimulator { | |
| constructor() { | |
| this.physics = new ReactorPhysics(); | |
| this.audio = new ReactorAudio(); | |
| this.keyBuffer = ''; | |
| this.cherenkovMode = false; | |
| this.animationId = null; | |
| this.lastReactorState = 'shutdown'; | |
| this.initializeElements(); | |
| this.createFuelGrid(); | |
| this.createAnnunciators(); | |
| this.bindEvents(); | |
| } | |
| initializeElements() { | |
| this.elements = { | |
| powerFill: document.getElementById('powerFill'), | |
| powerReadout: document.getElementById('powerReadout'), | |
| statusIndicator: document.getElementById('statusIndicator'), | |
| tempNeedle: document.getElementById('tempNeedle'), | |
| tempReadout: document.getElementById('tempReadout'), | |
| pressureNeedle: document.getElementById('pressureNeedle'), | |
| pressureReadout: document.getElementById('pressureReadout'), | |
| rodPosition: document.getElementById('rodPosition'), | |
| rodReadout: document.getElementById('rodReadout'), | |
| boronLevel: document.getElementById('boronLevel'), | |
| boronReadout: document.getElementById('boronReadout'), | |
| timeScale: document.getElementById('timeScale'), | |
| timeReadout: document.getElementById('timeReadout'), | |
| scramButton: document.getElementById('scramButton'), | |
| controlRod: document.getElementById('controlRod'), | |
| reactorCore: document.getElementById('reactorCore'), | |
| particles: document.getElementById('particles'), | |
| fuelGrid: document.getElementById('fuelGrid'), | |
| muteButton: document.getElementById('muteButton'), | |
| volumeControl: document.getElementById('volumeControl'), | |
| startupBtn: document.getElementById('startupBtn'), | |
| cooldownBtn: document.getElementById('cooldownBtn'), | |
| criticalityBtn: document.getElementById('criticalityBtn'), | |
| resetBtn: document.getElementById('resetBtn') | |
| }; | |
| } | |
| createFuelGrid() { | |
| const grid = this.elements.fuelGrid; | |
| grid.innerHTML = ''; | |
| for (let i = 0; i < 289; i++) { // 17x17 grid | |
| const pin = document.createElement('div'); | |
| pin.className = 'fuel-pin'; | |
| grid.appendChild(pin); | |
| } | |
| } | |
| createAnnunciators() { | |
| const grid = document.getElementById('annunciatorGrid'); | |
| grid.innerHTML = ''; | |
| const labels = [ | |
| 'PWR HIGH', 'TEMP HIGH', 'PRESS HIGH', 'FLOW LOW', | |
| 'ROD DROP', 'SCRAM', 'DNBR LOW', 'CRITICAL', | |
| 'COOLING', 'CONTAIN', 'RADIAT', 'TURBINE', | |
| 'BACKUP', 'DIESEL', 'BATTERY', 'ALARM' | |
| ]; | |
| for (let i = 0; i < 16; i++) { | |
| const annunciator = document.createElement('div'); | |
| annunciator.className = 'annunciator'; | |
| annunciator.title = labels[i] || `ANN ${i+1}`; | |
| grid.appendChild(annunciator); | |
| } | |
| } | |
| bindEvents() { | |
| // Control sliders | |
| this.elements.rodPosition.addEventListener('input', (e) => { | |
| if (!this.physics.scrammed) { | |
| this.physics.rodPosition = parseFloat(e.target.value); | |
| this.audio.playClick(); | |
| } | |
| }); | |
| this.elements.boronLevel.addEventListener('input', (e) => { | |
| this.physics.boronConc = parseFloat(e.target.value); | |
| this.audio.playClick(); | |
| }); | |
| this.elements.timeScale.addEventListener('input', (e) => { | |
| this.physics.timeScale = parseFloat(e.target.value); | |
| }); | |
| // SCRAM button | |
| this.elements.scramButton.addEventListener('click', () => { | |
| if (this.physics.scrammed) { | |
| this.resetReactor(); | |
| } else { | |
| this.physics.scram(); | |
| this.elements.scramButton.textContent = 'RESET'; | |
| this.elements.scramButton.style.background = 'var(--caution)'; | |
| this.audio.playAlarm(); | |
| } | |
| }); | |
| // Audio controls | |
| this.elements.muteButton.addEventListener('click', () => { | |
| this.audio.setMute(!this.audio.muted); | |
| this.elements.muteButton.textContent = this.audio.muted ? '🔇 Audio Off' : '🔊 Audio On'; | |
| this.elements.muteButton.classList.toggle('muted', this.audio.muted); | |
| }); | |
| this.elements.volumeControl.addEventListener('input', (e) => { | |
| this.audio.setVolume(parseFloat(e.target.value) / 100); | |
| }); | |
| // Emergency procedure buttons | |
| this.elements.startupBtn.addEventListener('click', () => { | |
| this.startupProcedure(); | |
| this.audio.playClick(); | |
| }); | |
| this.elements.cooldownBtn.addEventListener('click', () => { | |
| this.cooldownProcedure(); | |
| this.audio.playClick(); | |
| }); | |
| this.elements.criticalityBtn.addEventListener('click', () => { | |
| this.toCriticality(); | |
| this.audio.playClick(); | |
| }); | |
| this.elements.resetBtn.addEventListener('click', () => { | |
| this.resetReactor(); | |
| this.audio.playClick(); | |
| }); | |
| // Cherenkov easter egg | |
| document.addEventListener('keydown', (e) => { | |
| if (e.shiftKey) { | |
| this.keyBuffer += e.key.toLowerCase(); | |
| if (this.keyBuffer.includes('cherenkov')) { | |
| this.toggleCherenkovMode(); | |
| this.keyBuffer = ''; | |
| } | |
| } | |
| }); | |
| document.addEventListener('keyup', (e) => { | |
| if (!e.shiftKey) { | |
| this.keyBuffer = ''; | |
| } | |
| }); | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| if (e.target.tagName === 'INPUT') return; | |
| switch(e.key.toLowerCase()) { | |
| case ' ': | |
| e.preventDefault(); | |
| this.elements.scramButton.click(); | |
| break; | |
| case 'r': | |
| if (e.ctrlKey) { | |
| e.preventDefault(); | |
| this.resetReactor(); | |
| } | |
| break; | |
| case 's': | |
| if (e.ctrlKey) { | |
| e.preventDefault(); | |
| this.startupProcedure(); | |
| } | |
| break; | |
| case 'c': | |
| if (e.ctrlKey) { | |
| e.preventDefault(); | |
| this.cooldownProcedure(); | |
| } | |
| break; | |
| case '0': | |
| case '1': | |
| case '2': | |
| case '3': | |
| case '4': | |
| case '5': | |
| case '6': | |
| case '7': | |
| case '8': | |
| case '9': | |
| if (!this.physics.scrammed) { | |
| const position = parseInt(e.key) * 10; | |
| this.physics.rodPosition = position; | |
| this.elements.rodPosition.value = position; | |
| this.audio.playClick(); | |
| } | |
| break; | |
| } | |
| }); | |
| } | |
| toggleCherenkovMode() { | |
| this.cherenkovMode = !this.cherenkovMode; | |
| if (this.cherenkovMode) { | |
| this.elements.reactorCore.style.filter = 'hue-rotate(200deg) brightness(1.5) saturate(2)'; | |
| console.log('Cherenkov debug mode activated'); | |
| } else { | |
| this.elements.reactorCore.style.filter = ''; | |
| console.log('Cherenkov debug mode deactivated'); | |
| } | |
| } | |
| startupProcedure() { | |
| if (this.physics.scrammed) { | |
| console.log('Cannot start: Reactor is scrammed'); | |
| return; | |
| } | |
| console.log('PROCEDURE: Reactor startup sequence initiated'); | |
| // Step 1: Ensure high boron concentration | |
| this.physics.boronConc = 1800; | |
| this.elements.boronLevel.value = 1800; | |
| // Step 2: Gradual rod withdrawal | |
| let step = 0; | |
| const withdrawStep = () => { | |
| if (step < 40 && this.physics.power < 100 && !this.physics.scrammed) { | |
| this.physics.rodPosition = Math.min(80, this.physics.rodPosition + 2); | |
| this.elements.rodPosition.value = this.physics.rodPosition; | |
| step++; | |
| setTimeout(withdrawStep, 800); | |
| } | |
| }; | |
| setTimeout(withdrawStep, 1000); | |
| } | |
| cooldownProcedure() { | |
| console.log('PROCEDURE: Initiating controlled cooldown'); | |
| this.physics.rodPosition = Math.max(0, this.physics.rodPosition - 30); | |
| this.physics.boronConc = Math.min(2000, this.physics.boronConc + 300); | |
| this.updateControls(); | |
| } | |
| toCriticality() { | |
| if (this.physics.scrammed) { | |
| console.log('Cannot achieve criticality: Reactor is scrammed'); | |
| return; | |
| } | |
| console.log('PROCEDURE: Approaching criticality'); | |
| // Reduce boron to critical level | |
| this.physics.boronConc = 1100; | |
| this.elements.boronLevel.value = 1100; | |
| // Set rods to critical position | |
| let targetRods = 60; | |
| const adjustRods = () => { | |
| if (Math.abs(this.physics.rodPosition - targetRods) > 2 && !this.physics.scrammed) { | |
| if (this.physics.rodPosition < targetRods) { | |
| this.physics.rodPosition = Math.min(targetRods, this.physics.rodPosition + 3); | |
| } else { | |
| this.physics.rodPosition = Math.max(targetRods, this.physics.rodPosition - 3); | |
| } | |
| this.elements.rodPosition.value = this.physics.rodPosition; | |
| setTimeout(adjustRods, 500); | |
| } | |
| }; | |
| adjustRods(); | |
| } | |
| resetReactor() { | |
| this.physics.reset(); | |
| this.elements.scramButton.textContent = 'SCRAM'; | |
| this.elements.scramButton.style.background = 'var(--alarm)'; | |
| this.updateControls(); | |
| this.audio.stopAlarm(); | |
| this.audio.stopBackgroundHum(); | |
| console.log('Reactor reset to cold shutdown'); | |
| } | |
| updateControls() { | |
| this.elements.rodPosition.value = this.physics.rodPosition; | |
| this.elements.boronLevel.value = this.physics.boronConc; | |
| this.elements.timeScale.value = this.physics.timeScale; | |
| } | |
| updateDisplay() { | |
| // Power display | |
| const powerPercent = Math.min((this.physics.power / 3000) * 100, 120); | |
| this.elements.powerFill.style.height = powerPercent + '%'; | |
| // Status indicator and reactor state monitoring | |
| const currentState = this.physics.getState(); | |
| if (currentState !== this.lastReactorState) { | |
| this.onStateChange(this.lastReactorState, currentState); | |
| this.lastReactorState = currentState; | |
| } | |
| // Update status indicator | |
| this.elements.statusIndicator.className = `status-indicator ${currentState}`; | |
| this.elements.powerReadout.innerHTML = | |
| `<span class="status-indicator ${currentState}"></span>${this.physics.power.toFixed(0)} MWth (${(this.physics.power/30).toFixed(1)}%)`; | |
| if (powerPercent > 100) { | |
| this.elements.powerFill.style.background = 'linear-gradient(0deg, var(--alarm), var(--caution))'; | |
| } else { | |
| this.elements.powerFill.style.background = 'linear-gradient(0deg, var(--primary), var(--coolant))'; | |
| } | |
| // Temperature | |
| const tempDelta = this.physics.temperature - 291; | |
| const tempAngle = Math.min((tempDelta / 50) * 180, 180); | |
| this.elements.tempNeedle.style.transform = | |
| `translate(-50%, -100%) rotate(${tempAngle - 90}deg)`; | |
| this.elements.tempReadout.textContent = `+${tempDelta.toFixed(1)}°C`; | |
| // Pressure | |
| const pressureAngle = Math.min(((this.physics.pressure - 15.5) / 2) * 180, 180); | |
| this.elements.pressureNeedle.style.transform = | |
| `translate(-50%, -100%) rotate(${pressureAngle - 90}deg)`; | |
| this.elements.pressureReadout.textContent = `${this.physics.pressure.toFixed(1)} MPa`; | |
| // Control displays | |
| this.elements.rodReadout.textContent = `${this.physics.rodPosition.toFixed(0)}% Withdrawn`; | |
| this.elements.boronReadout.textContent = `${this.physics.boronConc.toFixed(0)} ppm`; | |
| this.elements.timeReadout.textContent = `${this.physics.timeScale.toFixed(1)}x Real Time`; | |
| // Control rod visual position | |
| const rodTop = 10 + (100 - this.physics.rodPosition) * 2.9; | |
| this.elements.controlRod.style.top = rodTop + 'px'; | |
| // Update fuel pins | |
| this.updateFuelPins(); | |
| // Update annunciators | |
| this.updateAnnunciators(); | |
| // Update background hum based on power level | |
| if (this.physics.power > 50) { | |
| this.audio.startBackgroundHum(this.physics.power / 3000); | |
| } else { | |
| this.audio.stopBackgroundHum(); | |
| } | |
| // Create neutron particles in Cherenkov mode | |
| if (this.cherenkovMode && this.physics.power > 50 && Math.random() < 0.1) { | |
| this.createNeutronParticle(); | |
| } | |
| } | |
| onStateChange(oldState, newState) { | |
| console.log(`Reactor state changed: ${oldState} → ${newState}`); | |
| switch(newState) { | |
| case 'critical': | |
| if (oldState === 'subcritical') { | |
| console.log('🔥 REACTOR IS NOW CRITICAL'); | |
| this.audio.playCriticalityAlarm(); | |
| } | |
| break; | |
| case 'subcritical': | |
| if (oldState === 'shutdown') { | |
| console.log('⚡ Reactor is now subcritical'); | |
| this.audio.playSubcriticalBeep(); | |
| } | |
| break; | |
| case 'scrammed': | |
| console.log('🚨 REACTOR SCRAMMED - EMERGENCY SHUTDOWN'); | |
| this.audio.playAlarm(); | |
| break; | |
| case 'shutdown': | |
| console.log('✅ Reactor shutdown complete'); | |
| this.audio.stopAlarm(); | |
| break; | |
| } | |
| } | |
| updateFuelPins() { | |
| const pins = this.elements.fuelGrid.querySelectorAll('.fuel-pin'); | |
| const powerLevel = this.physics.power / 3000; | |
| pins.forEach((pin, index) => { | |
| const row = Math.floor(index / 17); | |
| const col = index % 17; | |
| const centerDistance = Math.sqrt((row - 8) ** 2 + (col - 8) ** 2); | |
| const peaking = Math.max(0, 1 - centerDistance / 12); | |
| const localPower = powerLevel * peaking; | |
| if (localPower > 0.1) { | |
| pin.classList.add('hot'); | |
| pin.style.opacity = Math.min(1, localPower * 2); | |
| } else { | |
| pin.classList.remove('hot'); | |
| pin.style.opacity = 0.3; | |
| } | |
| }); | |
| } | |
| updateAnnunciators() { | |
| const annunciators = document.querySelectorAll('.annunciator'); | |
| // Clear all | |
| annunciators.forEach(ann => { | |
| ann.classList.remove('active', 'caution'); | |
| }); | |
| // Power high | |
| if (this.physics.power > 2500) { | |
| annunciators[0].classList.add('active'); | |
| } | |
| // Temperature high | |
| if (this.physics.temperature > 330) { | |
| annunciators[1].classList.add('caution'); | |
| } | |
| // Pressure high | |
| if (this.physics.pressure > 17.0) { | |
| annunciators[2].classList.add('caution'); | |
| } | |
| // SCRAM | |
| if (this.physics.scrammed) { | |
| annunciators[5].classList.add('active'); | |
| } | |
| // Critical | |
| if (this.physics.power > 1000) { | |
| annunciators[7].classList.add('caution'); | |
| } | |
| // DNBR low | |
| if (this.physics.power > 2800 && this.physics.temperature > 335) { | |
| annunciators[6].classList.add('active'); | |
| } | |
| } | |
| createNeutronParticle() { | |
| const neutron = document.createElement('div'); | |
| neutron.className = 'neutron'; | |
| neutron.style.left = (40 + Math.random() * 20) + '%'; | |
| neutron.style.top = (30 + Math.random() * 40) + '%'; | |
| this.elements.particles.appendChild(neutron); | |
| // Animate neutron | |
| let y = 0; | |
| const animate = () => { | |
| y += 2; | |
| neutron.style.transform = `translateY(${y}px)`; | |
| neutron.style.opacity = Math.max(0, 1 - y / 200); | |
| if (y < 200 && neutron.parentNode) { | |
| requestAnimationFrame(animate); | |
| } else if (neutron.parentNode) { | |
| neutron.parentNode.removeChild(neutron); | |
| } | |
| }; | |
| animate(); | |
| } | |
| start() { | |
| this.physics.start(); | |
| const animate = () => { | |
| this.physics.update(); | |
| this.updateDisplay(); | |
| this.animationId = requestAnimationFrame(animate); | |
| }; | |
| animate(); | |
| } | |
| stop() { | |
| if (this.animationId) { | |
| cancelAnimationFrame(this.animationId); | |
| this.animationId = null; | |
| } | |
| this.audio.stopAlarm(); | |
| this.audio.stopBackgroundHum(); | |
| } | |
| } | |
| // Initialize everything when page loads | |
| window.addEventListener('load', () => { | |
| console.log('Initializing Nuclear Reactor Simulator...'); | |
| try { | |
| // Initialize reactor simulator | |
| reactor = new ReactorSimulator(); | |
| console.log('Reactor simulator initialized successfully'); | |
| console.log('Controls:'); | |
| console.log('- Numbers 0-9: Set rod position (0%, 10%, 20%, etc.)'); | |
| console.log('- Space: Emergency SCRAM'); | |
| console.log('- Ctrl+R: Reset reactor'); | |
| console.log('- Ctrl+S: Startup procedure'); | |
| console.log('- Ctrl+C: Cooldown procedure'); | |
| console.log('- Shift+CHERENKOV: Debug mode'); | |
| } catch (error) { | |
| console.error('Failed to initialize reactor simulator:', error); | |
| } | |
| }); | |
| // Handle page visibility changes | |
| document.addEventListener('visibilitychange', () => { | |
| if (reactor) { | |
| if (document.hidden) { | |
| reactor.stop(); | |
| } else { | |
| reactor.start(); | |
| } | |
| } | |
| }); | |
| // Prevent accidental page reload | |
| window.addEventListener('beforeunload', (e) => { | |
| if (reactor && reactor.physics.power > 500) { | |
| e.preventDefault(); | |
| e.returnValue = 'Reactor is at power. Are you sure you want to leave?'; | |
| return e.returnValue; | |
| } | |
| }); | |
| // Add status monitoring | |
| setInterval(() => { | |
| if (reactor && reactor.physics.running) { | |
| const status = { | |
| power: reactor.physics.power.toFixed(1) + ' MWth', | |
| temperature: reactor.physics.temperature.toFixed(1) + '°C', | |
| pressure: reactor.physics.pressure.toFixed(1) + ' MPa', | |
| rods: reactor.physics.rodPosition.toFixed(1) + '% withdrawn', | |
| boron: reactor.physics.boronConc.toFixed(0) + ' ppm', | |
| status: reactor.physics.scrammed ? 'SCRAMMED' : | |
| reactor.physics.power > 1000 ? 'CRITICAL' : | |
| reactor.physics.power > 10 ? 'SUBCRITICAL' : 'SHUTDOWN' | |
| }; | |
| // Update page title with reactor status | |
| document.title = `Reactor: ${status.power} | ${status.status}`; | |
| // Log critical conditions | |
| if (reactor.physics.power > 2500) { | |
| console.warn('WARNING: Reactor power exceeding 2500 MWth'); | |
| } | |
| if (reactor.physics.temperature > 340) { | |
| console.warn('WARNING: Core temperature exceeding 340°C'); | |
| } | |
| if (reactor.physics.pressure > 17) { | |
| console.warn('WARNING: RCS pressure exceeding 17 MPa'); | |
| } | |
| } | |
| }, 5000); | |
| console.log('Dr. Elara Voss Nuclear Reactor Simulator v2.0'); | |
| console.log('======================================================'); | |
| console.log('Enhanced with improved audio and button controls'); | |
| console.log('Reactor systems nominal. Awaiting initialization...'); | |
| console.log('======================================================'); | |
| </script> | |
| </body> | |
| </html> |