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> |