| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Patriot Intercept Simulator • v5</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script> |
| <style> |
| body { margin: 0; overflow: hidden; font-family: system-ui; } |
| canvas { display: block; } |
| .sidebar { |
| transition: width 0.4s cubic-bezier(0.32, 0.72, 0, 1), padding 0.4s cubic-bezier(0.32, 0.72, 0, 1); |
| } |
| .sidebar.collapsed { |
| width: 0 !important; |
| padding: 0 !important; |
| overflow: hidden; |
| } |
| </style> |
| </head> |
| <body class="bg-zinc-950 text-white"> |
| <div class="flex h-screen"> |
| |
| <div id="sidebar" class="w-80 flex flex-col border-r border-zinc-800 sidebar p-6 overflow-y-auto bg-gradient-to-b from-[#111827] to-[#1f2937]"> |
| <div class="flex items-center justify-between mb-8"> |
| <div class="flex items-center gap-3"> |
| <div class="w-9 h-9 bg-gradient-to-br from-red-500 to-amber-500 rounded-2xl flex items-center justify-center text-2xl shadow-lg">🛡️</div> |
| <div> |
| <h1 class="text-3xl font-black tracking-[-2px]">PATRIOT SIM</h1> |
| <p class="text-emerald-400 text-sm -mt-1">v5 • PERFECT HOMING </p> |
| </div> |
| </div> |
| <button onclick="toggleSidebar()" class="text-zinc-400 hover:text-white p-2 rounded-xl transition-colors"> |
| <svg id="toggleIcon" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M11 19l-7-7 7-7" /> |
| </svg> |
| </button> |
| </div> |
|
|
| <div class="space-y-6"> |
| <div> |
| <h2 class="uppercase text-xs tracking-widest text-zinc-400 mb-3">THREAT SCENARIO</h2> |
| <div class="space-y-5"> |
| <div> |
| <label class="text-xs text-zinc-400 block mb-1">NUMBER OF INCOMING</label> |
| <input id="numThreats" type="range" min="1" max="12" value="5" class="w-full accent-red-500"> |
| <div class="flex justify-between text-[10px] text-zinc-400 mt-1"><span>1</span><span id="numThreatsVal" class="font-mono text-white">5</span><span>12</span></div> |
| </div> |
| <div> |
| <label class="text-xs text-zinc-400 block mb-1">THREAT SPEED (km/s)</label> |
| <input id="vm" type="range" min="1" max="3" value="1.4" step="0.1" class="w-full accent-red-500"> |
| <div class="flex justify-between text-[10px] text-zinc-400 mt-1"><span>1.0</span><span id="vmVal" class="font-mono text-white">1.4</span><span>3.0</span></div> |
| </div> |
| <div> |
| <label class="text-xs text-zinc-400 block mb-1">INTERCEPTOR ACCEL (g)</label> |
| <input id="accelG" type="range" min="8" max="60" value="35" step="1" class="w-full accent-sky-500"> |
| <div class="flex justify-between text-[10px] text-zinc-400 mt-1"><span>8g</span><span id="accelGVal" class="font-mono text-white">35g</span><span>60g</span></div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="grid grid-cols-3 gap-3"> |
| <button onclick="launchScenario()" class="col-span-2 bg-red-600 hover:bg-red-700 py-4 rounded-2xl font-bold text-lg flex items-center justify-center gap-2 transition-all active:scale-95"> |
| 🚀 LAUNCH WAVE |
| </button> |
| <button onclick="launchInterceptors()" class="bg-sky-600 hover:bg-sky-700 py-4 rounded-2xl font-bold flex items-center justify-center gap-2 transition-all active:scale-95"> |
| 🛡️ ENGAGE |
| </button> |
| </div> |
|
|
| <div class="flex gap-2"> |
| <button onclick="toggleAutoEngage()" id="autoBtn" class="flex-1 py-3 bg-amber-600 hover:bg-amber-700 rounded-2xl text-sm font-semibold">AUTO ENGAGE: OFF</button> |
| <button onclick="togglePause()" id="pauseBtn" class="flex-1 py-3 bg-zinc-700 hover:bg-zinc-600 rounded-2xl text-sm font-semibold">PAUSE</button> |
| <button onclick="resetSim()" class="flex-1 py-3 bg-zinc-700 hover:bg-zinc-600 rounded-2xl text-sm font-semibold">RESET</button> |
| </div> |
| </div> |
|
|
| <div class="mt-8 bg-zinc-900/70 rounded-3xl p-5 border border-zinc-700"> |
| <div class="flex justify-between items-baseline mb-4"> |
| <div class="text-emerald-400 text-5xl font-mono tabular-nums" id="intercepts">0</div> |
| <div class="text-right"> |
| <div class="text-xs text-zinc-400">INTERCEPTS</div> |
| <div class="text-3xl font-mono text-amber-400" id="misses">0</div> |
| <div class="text-xs text-zinc-400">MISSES</div> |
| </div> |
| <div class="text-right"> |
| <div class="text-5xl font-mono text-red-400" id="baseHits">0</div> |
| <div class="text-xs text-zinc-400">BASE HITS</div> |
| </div> |
| </div> |
| <div id="successRate" class="text-center text-2xl font-bold text-emerald-400">100%</div> |
| <div id="timeDisplay" class="text-center font-mono text-xl mt-1 text-white/60">T+ 00:00</div> |
| </div> |
|
|
| <div class="mt-auto pt-8 text-[10px] text-zinc-500"> |
| v5 • PERFECT HOMING • Collapsible sidebar<br> |
| Production-ready Flask app |
| </div> |
| </div> |
|
|
| |
| <div class="flex-1 relative" id="viewport"> |
| <canvas id="canvas"></canvas> |
| <div class="absolute top-6 left-1/2 -translate-x-1/2 bg-black/70 backdrop-blur-xl px-8 py-2 rounded-3xl font-mono text-sm flex items-center gap-6 shadow-2xl"> |
| <div id="status" class="px-4 py-1 bg-emerald-500/20 text-emerald-400 rounded-2xl">READY — PRESS LAUNCH</div> |
| </div> |
| <div class="absolute top-6 right-6 bg-black/60 backdrop-blur-md px-5 py-2.5 rounded-3xl font-mono text-xs flex flex-col gap-1 text-right"> |
| <div>THREATS: <span id="threatCount" class="text-red-400">0</span></div> |
| <div>INTERCEPTORS: <span id="intCount" class="text-sky-400">0</span></div> |
| </div> |
| <div id="hitFlash" class="absolute inset-0 bg-red-600 pointer-events-none opacity-0 transition-all duration-200"></div> |
|
|
| |
| <button onclick="toggleSidebar()" id="floatingToggle" |
| class="hidden fixed left-4 top-6 bg-zinc-800 hover:bg-zinc-700 text-white p-3 rounded-2xl shadow-2xl z-50 transition-all"> |
| <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"> |
| <path stroke-linecap="round" stroke-linejoin="round" d="M13 5l7 7-7 7" /> |
| </svg> |
| </button> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let scene, camera, renderer, clock; |
| let threats = [], interceptors = [], particles = [], launchSmoke = []; |
| let baseMesh; |
| let isPaused = false, autoEngage = false, autoTimer = null; |
| let simTime = 0, intercepts = 0, baseHits = 0, misses = 0; |
| let sidebarCollapsed = false; |
| |
| const BASE_POS = new THREE.Vector3(0, 0, 0); |
| const GRAVITY = -0.00981; |
| const G = 0.00981; |
| |
| let threatSpeed = 1.4; |
| let interceptorAccelG = 35; |
| |
| let theta = 2.4, phi = 0.9, radius = 65; |
| let isDragging = false, prevMouse = {x:0, y:0}; |
| |
| class Threat { |
| constructor(pos, vel) { |
| this.pos = pos.clone(); |
| this.vel = vel.clone(); |
| this.mesh = createMissile(0xff3300); |
| this.mesh.position.copy(this.pos); |
| scene.add(this.mesh); |
| this.trail = createTrail(0xff8855); |
| scene.add(this.trail); |
| this.trailPoints = []; |
| this.alive = true; |
| this.assigned = false; |
| } |
| update(dt) { |
| this.vel.y += GRAVITY * dt; |
| this.pos.addScaledVector(this.vel, dt); |
| this.mesh.position.copy(this.pos); |
| this.mesh.lookAt(this.pos.clone().add(this.vel)); |
| this.trailPoints.push(this.pos.clone()); |
| if (this.trailPoints.length > 180) this.trailPoints.shift(); |
| this.trail.geometry.setFromPoints(this.trailPoints); |
| } |
| remove() { |
| scene.remove(this.mesh); |
| scene.remove(this.trail); |
| this.alive = false; |
| } |
| } |
| |
| class Interceptor { |
| constructor(target) { |
| this.pos = BASE_POS.clone().add(new THREE.Vector3(0, 1.5, 0)); |
| this.target = target; |
| this.accel = interceptorAccelG * G; |
| const d = this.target.pos.clone().sub(this.pos).length(); |
| const vm = this.target.vel.length(); |
| let t = 3.0; |
| if (vm > 0.01) { |
| const disc = vm * vm + 2 * this.accel * d; |
| if (disc > 0) t = (-vm + Math.sqrt(disc)) / this.accel; |
| t = Math.max(t, 0.8); |
| } |
| const predicted = this.target.pos.clone().addScaledVector(this.target.vel, t); |
| const launchDir = predicted.sub(this.pos).normalize(); |
| this.vel = launchDir.multiplyScalar(0.95); |
| this.mesh = createMissile(0x00ddff); |
| this.mesh.position.copy(this.pos); |
| scene.add(this.mesh); |
| this.trail = createTrail(0x88eeff, 0.85); |
| scene.add(this.trail); |
| this.trailPoints = []; |
| this.alive = true; |
| } |
| update(dt) { |
| if (!this.target || !this.target.alive) return; |
| const toTarget = this.target.pos.clone().sub(this.pos); |
| const d = toTarget.length(); |
| const vm = this.target.vel.length(); |
| let t = 1.2; |
| if (vm > 0.01) { |
| const disc = vm * vm + 2 * this.accel * d; |
| if (disc > 0) t = (-vm + Math.sqrt(disc)) / this.accel; |
| t = Math.max(t, 0.4); |
| } |
| const predicted = this.target.pos.clone().addScaledVector(this.target.vel, t); |
| const desiredDir = predicted.sub(this.pos).normalize(); |
| const accelVec = desiredDir.multiplyScalar(this.accel * 1.8); |
| this.vel.addScaledVector(accelVec, dt); |
| if (this.vel.length() > 2.2) this.vel.setLength(2.2); |
| this.pos.addScaledVector(this.vel, dt); |
| this.mesh.position.copy(this.pos); |
| this.mesh.lookAt(this.pos.clone().add(this.vel)); |
| this.trailPoints.push(this.pos.clone()); |
| if (this.trailPoints.length > 110) this.trailPoints.shift(); |
| this.trail.geometry.setFromPoints(this.trailPoints); |
| } |
| remove() { |
| scene.remove(this.mesh); |
| scene.remove(this.trail); |
| this.alive = false; |
| } |
| } |
| |
| function createMissile(color) { |
| const group = new THREE.Group(); |
| const body = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.09, 2.4, 12), new THREE.MeshPhongMaterial({color, emissive: color, emissiveIntensity: 0.95})); |
| body.rotation.x = Math.PI / 2; |
| group.add(body); |
| const nose = new THREE.Mesh(new THREE.ConeGeometry(0.09, 1.0, 12), new THREE.MeshPhongMaterial({color: 0x111111, emissive: color})); |
| nose.position.z = 1.7; nose.rotation.x = Math.PI / 2; |
| group.add(nose); |
| const glow = new THREE.PointLight(color, 3, 12); |
| glow.position.z = 0.8; |
| group.add(glow); |
| return group; |
| } |
| |
| function createTrail(color, opacity = 0.9) { |
| return new THREE.Line(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({color, linewidth: 7, transparent: true, opacity})); |
| } |
| |
| function createExplosion(pos, big = false) { |
| const count = big ? 48 : 24; |
| for (let i = 0; i < count; i++) { |
| const p = new THREE.Mesh(new THREE.SphereGeometry(big ? 0.7 : 0.4, 8, 8), new THREE.MeshBasicMaterial({color: Math.random() > 0.5 ? 0xffaa00 : 0xffff44})); |
| p.position.copy(pos); |
| scene.add(p); |
| const vel = new THREE.Vector3((Math.random()-0.5)*28, (Math.random()-0.5)*28 + (big?15:8), (Math.random()-0.5)*28); |
| particles.push({mesh: p, vel, life: big ? 2.4 : 1.5}); |
| } |
| } |
| |
| function launchSmokeEffect(pos) { |
| for (let i = 0; i < 12; i++) { |
| const s = new THREE.Mesh(new THREE.SphereGeometry(0.5 + Math.random()*0.6, 6, 6), new THREE.MeshBasicMaterial({color: 0xcccccc, transparent: true, opacity: 0.7})); |
| s.position.copy(pos); |
| scene.add(s); |
| launchSmoke.push({mesh: s, life: 2.2, vel: new THREE.Vector3((Math.random()-0.5)*4, 6 + Math.random()*8, (Math.random()-0.5)*4)}); |
| } |
| } |
| |
| function init() { |
| clock = new THREE.Clock(); |
| const canvas = document.getElementById('canvas'); |
| renderer = new THREE.WebGLRenderer({canvas, antialias: true}); |
| renderer.setSize(window.innerWidth - 320, window.innerHeight); |
| renderer.setPixelRatio(Math.min(2, window.devicePixelRatio)); |
| |
| scene = new THREE.Scene(); |
| scene.fog = new THREE.FogExp2(0x0a1421, 0.011); |
| scene.background = new THREE.Color(0x0a1421); |
| |
| camera = new THREE.PerspectiveCamera(52, (window.innerWidth-320)/window.innerHeight, 0.1, 500); |
| updateCamera(); |
| |
| scene.add(new THREE.AmbientLight(0x6688aa, 0.7)); |
| const sun = new THREE.DirectionalLight(0xffeecc, 1.5); |
| sun.position.set(90, 140, 70); |
| scene.add(sun); |
| |
| const ground = new THREE.Mesh(new THREE.PlaneGeometry(400, 400), new THREE.MeshPhongMaterial({color: 0x1a2a1a})); |
| ground.rotation.x = -Math.PI / 2; |
| scene.add(ground); |
| |
| baseMesh = new THREE.Group(); |
| const bunker = new THREE.Mesh(new THREE.BoxGeometry(8, 3, 12), new THREE.MeshPhongMaterial({color: 0x444444})); |
| bunker.position.y = 1.5; |
| baseMesh.add(bunker); |
| const radar = new THREE.Mesh(new THREE.CylinderGeometry(2.6, 2.6, 0.7, 48), new THREE.MeshPhongMaterial({color: 0x99bbdd})); |
| radar.rotation.x = Math.PI / 2; |
| radar.position.y = 5.2; |
| baseMesh.add(radar); |
| scene.add(baseMesh); |
| |
| const grid = new THREE.GridHelper(300, 60, 0x334422, 0x223311); |
| grid.position.y = 0.02; |
| scene.add(grid); |
| |
| window.addEventListener('resize', onResize); |
| const vp = document.getElementById('viewport'); |
| vp.addEventListener('mousedown', e => { if (e.button === 0) { isDragging = true; prevMouse = {x: e.clientX, y: e.clientY}; } }); |
| vp.addEventListener('mousemove', onMouseMove); |
| vp.addEventListener('mouseup', () => isDragging = false); |
| vp.addEventListener('wheel', e => { radius = Math.max(35, Math.min(160, radius - e.deltaY * 0.09)); updateCamera(); }); |
| |
| updateUI(); |
| animate(); |
| } |
| |
| function updateCamera() { |
| const x = radius * Math.sin(phi) * Math.cos(theta); |
| const z = radius * Math.sin(phi) * Math.sin(theta); |
| camera.position.set(x, radius * Math.cos(phi) * 0.65 + 20, z); |
| camera.lookAt(6, 14, 0); |
| } |
| |
| function onMouseMove(e) { |
| if (!isDragging) return; |
| theta -= (e.clientX - prevMouse.x) * 0.0058; |
| phi = Math.max(0.25, Math.min(1.75, phi - (e.clientY - prevMouse.y) * 0.0058)); |
| updateCamera(); |
| prevMouse = {x: e.clientX, y: e.clientY}; |
| } |
| |
| function onResize() { |
| const w = window.innerWidth - (sidebarCollapsed ? 0 : 320); |
| camera.aspect = w / window.innerHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(w, window.innerHeight); |
| } |
| |
| function launchScenario() { |
| resetSim(false); |
| const n = parseInt(document.getElementById('numThreats').value); |
| threatSpeed = parseFloat(document.getElementById('vm').value); |
| for (let i = 0; i < n; i++) { |
| const spread = (i - (n-1)/2) * 0.72; |
| const range = 52 + Math.random() * 18; |
| const elev = 0.38 + Math.random() * 0.32; |
| const px = range * Math.cos(elev) * Math.cos(spread); |
| const pz = range * Math.cos(elev) * Math.sin(spread); |
| const py = range * Math.sin(elev) + 6; |
| const start = new THREE.Vector3(px, py, pz); |
| const dir = start.clone().negate().normalize(); |
| const vel = dir.multiplyScalar(threatSpeed); |
| threats.push(new Threat(start, vel)); |
| } |
| document.getElementById('status').innerHTML = 'INCOMING — ENGAGE!'; |
| document.getElementById('status').className = 'px-4 py-1 bg-amber-500/30 text-amber-300 rounded-2xl'; |
| if (autoEngage) autoTimer = setTimeout(launchInterceptors, 900); |
| } |
| |
| function launchInterceptors() { |
| interceptorAccelG = parseFloat(document.getElementById('accelG').value); |
| threats.forEach(th => { |
| if (th.alive && !th.assigned) { |
| const intc = new Interceptor(th); |
| interceptors.push(intc); |
| th.assigned = true; |
| launchSmokeEffect(intc.pos); |
| } |
| }); |
| document.getElementById('status').innerHTML = 'MISSILES AWAY — HOMING'; |
| document.getElementById('status').className = 'px-4 py-1 bg-sky-500/30 text-sky-300 rounded-2xl'; |
| } |
| |
| function checkCollisions() { |
| for (let i = interceptors.length - 1; i >= 0; i--) { |
| const intc = interceptors[i]; |
| for (let j = threats.length - 1; j >= 0; j--) { |
| const th = threats[j]; |
| if (th.alive && intc.pos.distanceTo(th.pos) < 3.2) { |
| createExplosion(intc.pos, true); |
| th.remove(); |
| intc.remove(); |
| threats.splice(j, 1); |
| interceptors.splice(i, 1); |
| intercepts++; |
| updateStats(); |
| cameraShake(); |
| return; |
| } |
| } |
| } |
| for (let j = threats.length - 1; j >= 0; j--) { |
| const th = threats[j]; |
| if (th.alive && th.pos.length() < 8 && th.pos.y < 9) { |
| createExplosion(th.pos, true); |
| th.remove(); |
| threats.splice(j, 1); |
| baseHits++; |
| updateStats(); |
| document.getElementById('hitFlash').style.opacity = '0.8'; |
| setTimeout(() => document.getElementById('hitFlash').style.opacity = '0', 260); |
| cameraShake(1.8); |
| } |
| } |
| } |
| |
| function updateStats() { |
| document.getElementById('intercepts').textContent = intercepts; |
| document.getElementById('baseHits').textContent = baseHits; |
| document.getElementById('misses').textContent = misses; |
| const total = intercepts + baseHits + misses; |
| document.getElementById('successRate').textContent = total ? Math.round(intercepts / total * 100) + '%' : '100%'; |
| document.getElementById('threatCount').textContent = threats.length; |
| document.getElementById('intCount').textContent = interceptors.length; |
| } |
| |
| function cameraShake(intensity = 1.0) { |
| const orig = camera.position.clone(); |
| let t = 0; |
| const intv = setInterval(() => { |
| t += 0.22; |
| camera.position.copy(orig).add(new THREE.Vector3((Math.random()-0.5)*intensity*2, (Math.random()-0.5)*intensity, (Math.random()-0.5)*intensity*2)); |
| if (t > 0.9) { clearInterval(intv); camera.position.copy(orig); updateCamera(); } |
| }, 35); |
| } |
| |
| function updateUI() { |
| document.getElementById('numThreatsVal').textContent = document.getElementById('numThreats').value; |
| document.getElementById('vmVal').textContent = document.getElementById('vm').value; |
| document.getElementById('accelGVal').textContent = document.getElementById('accelG').value + 'g'; |
| } |
| ['numThreats','vm','accelG'].forEach(id => document.getElementById(id).addEventListener('input', updateUI)); |
| |
| function togglePause() { isPaused = !isPaused; document.getElementById('pauseBtn').textContent = isPaused ? 'RESUME' : 'PAUSE'; } |
| function toggleAutoEngage() { |
| autoEngage = !autoEngage; |
| const btn = document.getElementById('autoBtn'); |
| btn.textContent = 'AUTO ENGAGE: ' + (autoEngage ? 'ON' : 'OFF'); |
| btn.classList.toggle('bg-emerald-600', autoEngage); |
| } |
| |
| function resetSim(full = true) { |
| if (autoTimer) clearTimeout(autoTimer); |
| threats.forEach(t => t.remove()); |
| interceptors.forEach(i => i.remove()); |
| particles.forEach(p => scene.remove(p.mesh)); |
| launchSmoke.forEach(s => scene.remove(s.mesh)); |
| threats = []; interceptors = []; particles = []; launchSmoke = []; |
| if (full) { intercepts = baseHits = misses = 0; simTime = 0; } |
| updateStats(); |
| document.getElementById('status').innerHTML = 'READY'; |
| document.getElementById('status').className = 'px-4 py-1 bg-emerald-500/20 text-emerald-400 rounded-2xl'; |
| } |
| |
| function toggleSidebar() { |
| sidebarCollapsed = !sidebarCollapsed; |
| const sb = document.getElementById('sidebar'); |
| const float = document.getElementById('floatingToggle'); |
| if (sidebarCollapsed) { |
| sb.classList.add('collapsed'); |
| float.classList.remove('hidden'); |
| } else { |
| sb.classList.remove('collapsed'); |
| float.classList.add('hidden'); |
| } |
| setTimeout(onResize, 420); |
| } |
| |
| function animate() { |
| requestAnimationFrame(animate); |
| const dt = Math.min(clock.getDelta(), 0.1); |
| if (!isPaused) { |
| simTime += dt; |
| const m = Math.floor(simTime/60), s = Math.floor(simTime%60); |
| document.getElementById('timeDisplay').textContent = `T+ ${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`; |
| |
| threats.forEach(t => { if (t.alive) t.update(dt); }); |
| interceptors.forEach(i => { if (i.alive) i.update(dt); }); |
| |
| checkCollisions(); |
| |
| for (let i = particles.length - 1; i >= 0; i--) { |
| const p = particles[i]; |
| p.life -= dt; |
| p.vel.y += GRAVITY * 1.6 * dt; |
| p.mesh.position.addScaledVector(p.vel, dt); |
| p.mesh.scale.setScalar(Math.max(0.1, p.life * 1.8)); |
| if (p.life <= 0) { scene.remove(p.mesh); particles.splice(i,1); } |
| } |
| for (let i = launchSmoke.length - 1; i >= 0; i--) { |
| const s = launchSmoke[i]; |
| s.life -= dt * 1.3; |
| s.mesh.position.addScaledVector(s.vel, dt); |
| s.mesh.material.opacity = s.life * 0.65; |
| s.mesh.scale.setScalar(s.life * 1.6); |
| if (s.life <= 0) { scene.remove(s.mesh); launchSmoke.splice(i,1); } |
| } |
| } |
| renderer.render(scene, camera); |
| } |
| |
| window.addEventListener('keydown', e => { |
| if (e.key === ' ') togglePause(); |
| if (e.key.toLowerCase() === 'r') resetSim(); |
| if (e.key.toLowerCase() === 'l') launchScenario(); |
| if (e.key.toLowerCase() === 'e') launchInterceptors(); |
| if (e.key.toLowerCase() === 'a') toggleAutoEngage(); |
| if (e.key === 's' || e.key === 'S') toggleSidebar(); |
| }); |
| |
| window.onload = () => { |
| init(); |
| setTimeout(() => { |
| document.getElementById('numThreats').value = 5; |
| updateUI(); |
| launchScenario(); |
| setTimeout(() => { |
| launchInterceptors(); |
| toggleAutoEngage(); |
| }, 950); |
| }, 500); |
| }; |
| </script> |
| </body> |
| </html> |