ORBITAL / index.html
KyleHessling1's picture
ORBITAL — gravity-arcade demo built by Qwopus3.6-27B-Fusion
ab98535 verified
Raw
History Blame Contribute Delete
48.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ORBITAL</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:'Orbitron',sans-serif;color:#fff;user-select:none}
canvas{display:block;position:fixed;top:0;left:0}
.screen{position:fixed;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:10;pointer-events:none;opacity:0;transition:opacity .4s}
.screen.active{opacity:1;pointer-events:auto}
#title-screen .logo{font-size:clamp(48px,10vw,120px);font-weight:900;letter-spacing:.15em;text-shadow:0 0 40px #0af,0 0 80px #06f,0 0 120px #04a;margin-bottom:20px}
#title-screen .subtitle{font-size:clamp(12px,2vw,18px);opacity:.7;margin-bottom:30px}
#title-screen .prompt{font-size:clamp(14px,2vw,20px);animation:pulse 2s ease-in-out infinite}
#title-screen .controls{font-size:clamp(10px,1.5vw,14px);opacity:.5;margin-top:30px;text-align:center;line-height:1.8}
#title-screen .highscore-display{font-size:clamp(12px,1.5vw,16px);opacity:.6;margin-top:15px}
#hud{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5}
#hud-top{position:absolute;top:10px;left:10px;right:10px;display:flex;justify-content:space-between;align-items:flex-start}
#hud-left,#hud-right{display:flex;flex-direction:column;gap:6px}
#hud-right{text-align:right}
.hud-label{font-size:10px;opacity:.5;text-transform:uppercase;letter-spacing:.1em}
.hud-value{font-size:16px;font-weight:700}
.hud-bar{width:120px;height:8px;background:rgba(255,255,255,.1);border-radius:4px;overflow:hidden}
.hud-bar-fill{height:100%;border-radius:4px;transition:width .15s}
.health-fill{background:linear-gradient(90deg,#f44,#ff4)}
.fuel-fill{background:linear-gradient(90deg,#4af,#4f8)}
#combo-display{font-size:24px;font-weight:900;color:#ff4;text-shadow:0 0 20px #fa0}
#wave-display{font-size:20px;font-weight:700;color:#0af;text-shadow:0 0 15px #06f}
#score-display{font-size:20px;font-weight:700;text-shadow:0 0 10px #fff}
#upgrades-display{position:absolute;bottom:10px;left:50%;transform:translateX(-50%);display:flex;gap:8px}
.upgrade-icon{width:32px;height:32px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:14px;border:1px solid rgba(255,255,255,.2);background:rgba(0,0,0,.5)}
#radar{position:absolute;bottom:10px;right:10px;width:80px;height:80px;border-radius:50%;border:1px solid rgba(255,255,255,.15);background:rgba(0,0,0,.3)}
#mute-btn{position:absolute;top:10px;right:10px;pointer-events:auto;cursor:pointer;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.2);color:#fff;padding:4px 8px;border-radius:4px;font-size:12px;font-family:inherit}
#wave-card{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:15;text-align:center;pointer-events:none;opacity:0;transition:opacity .3s}
#wave-card.active{opacity:1}
#wave-card .wave-num{font-size:clamp(36px,6vw,72px);font-weight:900;text-shadow:0 0 30px #0af,0 0 60px #06f}
#wave-card .wave-sub{font-size:clamp(12px,2vw,18px);opacity:.6;margin-top:8px}
#upgrade-screen .upgrade-title{font-size:clamp(20px,3vw,36px);font-weight:700;margin-bottom:30px;text-shadow:0 0 20px #0af}
#upgrade-cards{display:flex;gap:20px;flex-wrap:wrap;justify-content:center}
.upgrade-card{width:140px;padding:20px 15px;background:rgba(0,0,0,.7);border:1px solid rgba(255,255,255,.15);border-radius:10px;cursor:pointer;text-align:center;transition:all .2s;pointer-events:auto}
.upgrade-card:hover{border-color:#0af;box-shadow:0 0 20px rgba(0,170,255,.3);transform:translateY(-4px)}
.upgrade-card .uc-icon{font-size:28px;margin-bottom:8px}
.upgrade-card .uc-name{font-size:12px;font-weight:700;margin-bottom:4px}
.upgrade-card .uc-desc{font-size:9px;opacity:.5;line-height:1.4}
#gameover-screen .go-title{font-size:clamp(36px,6vw,72px);font-weight:900;color:#f44;text-shadow:0 0 40px #f00;margin-bottom:20px}
#gameover-screen .go-stats{font-size:clamp(12px,1.5vw,16px);line-height:2;text-align:center;margin-bottom:20px;opacity:.8}
#gameover-screen .go-new{font-size:clamp(14px,2vw,20px);color:#ff4;text-shadow:0 0 20px #fa0;margin-bottom:20px}
#gameover-screen .go-prompt{font-size:clamp(12px,1.5vw,16px);opacity:.5;animation:pulse 2s ease-in-out infinite}
#pause-screen .pause-title{font-size:clamp(36px,6vw,72px);font-weight:900;text-shadow:0 0 30px #fff}
#pause-screen .pause-prompt{font-size:clamp(12px,1.5vw,16px);opacity:.5;margin-top:15px}
@keyframes pulse{0%,100%{opacity:.5}50%{opacity:1}}
</style>
</head>
<body>
<canvas id="game-canvas"></canvas>
<div id="hud" class="screen">
<div id="hud-top">
<div id="hud-left">
<div><span class="hud-label">Health</span><div class="hud-bar"><div id="health-bar" class="hud-bar-fill health-fill" style="width:100%"></div></div></div>
<div><span class="hud-label">Fuel</span><div class="hud-bar"><div id="fuel-bar" class="hud-bar-fill fuel-fill" style="width:100%"></div></div></div>
<div id="upgrades-display"></div>
</div>
<div id="hud-center" style="text-align:center">
<div id="wave-display">WAVE 1</div>
<div id="combo-display"></div>
</div>
<div id="hud-right">
<div id="score-display">0</div>
<div><span class="hud-label">High Score</span><div class="hud-value" id="highscore-hud">0</div></div>
</div>
</div>
<div id="radar"><canvas id="radar-canvas" width="80" height="80"></canvas></div>
<button id="mute-btn">MUTE</button>
</div>
<div id="wave-card"><div class="wave-num" id="wave-num-text"></div><div class="wave-sub" id="wave-sub-text"></div></div>
<div id="title-screen" class="screen active">
<div class="logo">ORBITAL</div>
<div class="subtitle">Gravity is your weapon</div>
<div class="prompt">CLICK or ENTER to launch</div>
<div class="highscore-display" id="highscore-title"></div>
<div class="controls">W/UP thrust | A/D or LEFT/RIGHT rotate | SPACE/CLICK fire | SHIFT boost | ESC pause</div>
</div>
<div id="upgrade-screen" class="screen">
<div class="upgrade-title">CHOOSE UPGRADE</div>
<div id="upgrade-cards"></div>
</div>
<div id="gameover-screen" class="screen">
<div class="go-title">GAME OVER</div>
<div class="go-stats" id="go-stats"></div>
<div class="go-new" id="go-new" style="display:none">NEW HIGH SCORE!</div>
<div class="go-prompt">CLICK or ENTER to restart</div>
</div>
<div id="pause-screen" class="screen">
<div class="pause-title">PAUSED</div>
<div class="pause-prompt">ESC to resume</div>
</div>
<script>
var canvas, ctx, radarCanvas, radarCtx;
var W, H, DPR;
var audioCtx, masterGain, isMuted = false;
var gameState = 'TITLE';
var score = 0, highScore = 0, combo = 0, comboTimer = 0;
var wave = 0, waveTimer = 0, waveDelay = 3;
var gameTime = 0, maxSpeed = 0, totalKills = 0;
var lastTime = 0, dt = 0, slowMo = 1, hitStop = 0;
var screenShake = {x:0,y:0,intensity:0,duration:0};
var keys = {}, mouse = {x:0,y:0,down:false};
var camera = {x:0,y:0,zoom:1,targetZoom:1,shakeX:0,shakeY:0};
var bullets = [], particles = [], enemies = [], pickups = [], popups = [], shockwaves = [];
var stars = [], nebulae = [];
var celestialBodies = [];
var ship = null;
var blackHole = null;
var titleShip = null;
(function init() {
canvas = document.getElementById('game-canvas');
ctx = canvas.getContext('2d');
radarCanvas = document.getElementById('radar-canvas');
radarCtx = radarCanvas.getContext('2d');
resize();
window.addEventListener('resize', resize);
highScore = parseInt(localStorage.getItem('orbital_hs') || '0');
document.getElementById('highscore-hud').textContent = highScore;
document.getElementById('highscore-title').textContent = highScore ? 'HIGH SCORE: ' + highScore : '';
setupInput();
setupAudio();
generateStars();
generateNebulae();
titleShip = {x:W/2,y:H/2+100,vx:200,vy:-100,angle:0,trail:[]};
requestAnimationFrame(loop);
})();
function resize() {
DPR = window.devicePixelRatio || 1;
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * DPR;
canvas.height = H * DPR;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
function setupInput() {
window.addEventListener('keydown', function(e) {
keys[e.code] = true;
if (e.code === 'Enter' || e.code === 'Space') {
if (gameState === 'TITLE') startGame();
else if (gameState === 'GAMEOVER') restartGame();
}
if (e.code === 'Escape') {
if (gameState === 'PLAYING') { gameState = 'PAUSED'; showScreen('pause-screen'); }
else if (gameState === 'PAUSED') { gameState = 'PLAYING'; showScreen(null); }
}
});
window.addEventListener('keyup', function(e) { keys[e.code] = false; });
canvas.addEventListener('mousemove', function(e) { mouse.x = e.clientX; mouse.y = e.clientY; });
canvas.addEventListener('mousedown', function(e) {
mouse.down = true;
if (gameState === 'TITLE') startGame();
else if (gameState === 'GAMEOVER') restartGame();
});
canvas.addEventListener('mouseup', function() { mouse.down = false; });
document.getElementById('mute-btn').addEventListener('click', function() {
isMuted = !isMuted;
masterGain.gain.value = isMuted ? 0 : 1;
this.textContent = isMuted ? 'UNMUTE' : 'MUTE';
});
}
function setupAudio() {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = audioCtx.createGain();
masterGain.gain.value = 1;
masterGain.connect(audioCtx.destination);
} catch(e) { audioCtx = null; }
}
function playSound(type) {
if (!audioCtx || isMuted) return;
try {
var t = audioCtx.currentTime, osc, g;
if (type === 'thrust') {
osc = audioCtx.createOscillator(); g = audioCtx.createGain();
osc.type = 'sawtooth'; osc.frequency.value = 60;
g.gain.setValueAtTime(0.04, t); g.gain.exponentialRampToValueAtTime(0.001, t+0.15);
osc.connect(g); g.connect(masterGain); osc.start(t); osc.stop(t+0.15);
} else if (type === 'fire') {
osc = audioCtx.createOscillator(); g = audioCtx.createGain();
osc.type = 'square'; osc.frequency.setValueAtTime(880, t); osc.frequency.exponentialRampToValueAtTime(220, t+0.1);
g.gain.setValueAtTime(0.08, t); g.gain.exponentialRampToValueAtTime(0.001, t+0.1);
osc.connect(g); g.connect(masterGain); osc.start(t); osc.stop(t+0.1);
} else if (type === 'explosion') {
var buf = audioCtx.createBuffer(1, audioCtx.sampleRate*0.3, audioCtx.sampleRate);
var d = buf.getChannelData(0);
for (var i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.exp(-i/(audioCtx.sampleRate*0.08));
var src = audioCtx.createBufferSource(); src.buffer = buf;
g = audioCtx.createGain(); g.gain.value = 0.15;
src.connect(g); g.connect(masterGain); src.start(t);
} else if (type === 'pickup') {
osc = audioCtx.createOscillator(); g = audioCtx.createGain();
osc.type = 'sine'; osc.frequency.setValueAtTime(523, t); osc.frequency.setValueAtTime(659, t+0.08); osc.frequency.setValueAtTime(784, t+0.16);
g.gain.setValueAtTime(0.08, t); g.gain.exponentialRampToValueAtTime(0.001, t+0.3);
osc.connect(g); g.connect(masterGain); osc.start(t); osc.stop(t+0.3);
} else if (type === 'wave') {
osc = audioCtx.createOscillator(); g = audioCtx.createGain();
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(110, t); osc.frequency.linearRampToValueAtTime(220, t+0.5);
g.gain.setValueAtTime(0.1, t); g.gain.exponentialRampToValueAtTime(0.001, t+0.6);
osc.connect(g); g.connect(masterGain); osc.start(t); osc.stop(t+0.6);
} else if (type === 'boss') {
osc = audioCtx.createOscillator(); g = audioCtx.createGain();
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(55, t); osc.frequency.linearRampToValueAtTime(33, t+1);
g.gain.setValueAtTime(0.12, t); g.gain.exponentialRampToValueAtTime(0.001, t+1.2);
osc.connect(g); g.connect(masterGain); osc.start(t); osc.stop(t+1.2);
} else if (type === 'death') {
osc = audioCtx.createOscillator(); g = audioCtx.createGain();
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(440, t); osc.frequency.exponentialRampToValueAtTime(55, t+1.5);
g.gain.setValueAtTime(0.15, t); g.gain.exponentialRampToValueAtTime(0.001, t+1.5);
osc.connect(g); g.connect(masterGain); osc.start(t); osc.stop(t+1.5);
} else if (type === 'graze') {
osc = audioCtx.createOscillator(); g = audioCtx.createGain();
osc.type = 'sine'; osc.frequency.value = 1200;
g.gain.setValueAtTime(0.03, t); g.gain.exponentialRampToValueAtTime(0.001, t+0.05);
osc.connect(g); g.connect(masterGain); osc.start(t); osc.stop(t+0.05);
}
} catch(e) {}
}
function showScreen(id) {
var screens = ['title-screen','upgrade-screen','gameover-screen','pause-screen'];
for (var i=0;i<screens.length;i++) {
var el = document.getElementById(screens[i]);
if (id === screens[i]) el.classList.add('active');
else el.classList.remove('active');
}
var hud = document.getElementById('hud');
if (id === 'title-screen') hud.classList.remove('active');
else if (id === null) hud.classList.add('active');
else hud.classList.toggle('active', id !== 'title-screen');
}
function startGame() {
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
score = 0; combo = 0; comboTimer = 0; wave = 0; gameTime = 0; maxSpeed = 0; totalKills = 0;
slowMo = 1; hitStop = 0;
bullets.length = 0; particles.length = 0; enemies.length = 0; pickups.length = 0;
popups.length = 0; shockwaves.length = 0;
celestialBodies.length = 0; blackHole = null;
ship = {x:W/2+200,y:H/2+100,vx:-120,vy:0,angle:-Math.PI/2,health:200,maxHealth:200,fuel:100,maxFuel:100,
fireRate:0.15,fireTimer:0,boostFuel:3,boostActive:false,
upgrades:{spread:0,rapid:0,shield:0,overdrive:0},trail:[],invuln:5,alive:true};
camera.x = W/2; camera.y = H/2; camera.zoom = 1; camera.targetZoom = 1;
buildSolarSystem();
gameState = 'PLAYING';
showScreen(null);
nextWave();
}
function restartGame() {
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
startGame();
}
function buildSolarSystem() {
celestialBodies.push({x:W/2,y:H/2,mass:8000,radius:50,type:'star',orbitAngle:0,orbitDist:0,orbitSpeed:0,pulse:0});
var pd = [{mass:1200,radius:28,orbitDist:220,orbitSpeed:0.3,color:'#4af'},
{mass:2000,radius:38,orbitDist:340,orbitSpeed:0.2,color:'#f84'},
{mass:600,radius:20,orbitDist:160,orbitSpeed:0.5,color:'#4f8'},
{mass:3000,radius:48,orbitDist:440,orbitSpeed:0.15,color:'#a4f'}];
for (var i=0;i<pd.length;i++) {
var p = pd[i];
celestialBodies.push({x:W/2+Math.cos(i*1.5)*p.orbitDist,y:H/2+Math.sin(i*1.5)*p.orbitDist,
mass:p.mass,radius:p.radius,type:'planet',orbitAngle:i*1.5,orbitDist:p.orbitDist,
orbitSpeed:p.orbitSpeed,color:p.color,pulse:Math.random()*Math.PI*2});
}
}
function nextWave() {
wave++;
waveTimer = waveDelay;
showWaveCard(wave);
playSound('wave');
if (wave % 5 === 0) {
if (wave === 10) { spawnBlackHole(); playSound('boss'); }
else { spawnBoss(); playSound('boss'); }
} else { spawnWaveEnemies(wave); }
}
function showWaveCard(n) {
var el = document.getElementById('wave-card');
document.getElementById('wave-num-text').textContent = 'WAVE ' + n;
document.getElementById('wave-sub-text').textContent = n % 5 === 0 ? 'BOSS WAVE' : '';
el.classList.add('active');
setTimeout(function() { el.classList.remove('active'); }, 2000);
}
function spawnWaveEnemies(w) {
var count = 2 + w;
for (var i=0;i<count;i++) {
var type = 'drone';
if (w >= 3 && Math.random() < 0.3) type = 'mine';
if (w >= 5 && Math.random() < 0.2) type = 'gunship';
spawnEnemy(type);
}
}
function spawnBoss() {
enemies.push({type:'boss',x:W/2+(Math.random()-0.5)*200,y:-80,vx:0,vy:0,
health:80+wave*20,maxHealth:80+wave*20,radius:40,fireRate:0.8,fireTimer:0,alive:true,angle:0,speed:0.5});
}
function spawnBlackHole() {
blackHole = {x:W/2,y:H/2-100,mass:15000,radius:60,life:30,pulse:0,health:200,maxHealth:200};
for (var i=0;i<8;i++) spawnEnemy('drone');
}
function spawnEnemy(type) {
var side = Math.floor(Math.random()*4), x, y;
if (side===0) { x=-30; y=Math.random()*H; }
else if (side===1) { x=W+30; y=Math.random()*H; }
else if (side===2) { x=Math.random()*W; y=-30; }
else { x=Math.random()*W; y=H+30; }
if (type==='drone') enemies.push({type:'drone',x:x,y:y,vx:0,vy:0,health:1,maxHealth:1,radius:12,alive:true,angle:0,speed:1.5});
else if (type==='mine') enemies.push({type:'mine',x:x,y:y,vx:0,vy:0,health:1,maxHealth:1,radius:14,alive:true,angle:0,speed:0.8,accel:0});
else if (type==='gunship') enemies.push({type:'gunship',x:x,y:y,vx:0,vy:0,health:4,maxHealth:4,radius:22,alive:true,angle:0,speed:0.6,fireRate:1.2,fireTimer:0});
}
function spawnPickup(x, y) {
var types = ['fuel','shield','spread','rapid','bomb','overdrive'];
var weights = [30,15,15,15,10,15];
var total = 0;
for (var i=0;i<weights.length;i++) total += weights[i];
var r = Math.random()*total, cum = 0, chosen = 'fuel';
for (var i=0;i<types.length;i++) { cum += weights[i]; if (r < cum) { chosen = types[i]; break; } }
var bodyIdx = 1 + Math.floor(Math.random()*(celestialBodies.length-1));
var body = celestialBodies[bodyIdx];
var angle = Math.random()*Math.PI*2;
var dist = body.orbitDist ? body.orbitDist + body.radius + 30 + Math.random()*40 : 200;
pickups.push({type:chosen,x:body.x+Math.cos(angle)*dist,y:body.y+Math.sin(angle)*dist,
radius:10,alive:true,orbitBody:bodyIdx,orbitAngle:angle,orbitDist:dist});
}
function applyGravity(obj, bodies) {
for (var i=0;i<bodies.length;i++) {
var b = bodies[i];
var dx = b.x - obj.x, dy = b.y - obj.y;
var distSq = dx*dx + dy*dy;
var softDist = distSq + b.radius*b.radius*4;
var force = b.mass / softDist;
obj.vx += (dx/softDist) * force * dt * 300;
obj.vy += (dy/softDist) * force * dt * 300;
}
}
function spawnParticle(x, y, vx, vy, color, life) {
particles.push({x:x,y:y,vx:vx,vy:vy,color:color,life:life,maxLife:life});
}
function addPopup(text, x, y) {
popups.push({text:text,x:x,y:y,life:1.5});
}
function addShockwave(x, y, maxRadius) {
shockwaves.push({x:x,y:y,radius:0,maxRadius:maxRadius,speed:maxRadius/0.5,life:0.5});
}
function update() {
if (gameState !== 'PLAYING') return;
var effectiveDt = dt * slowMo;
if (hitStop > 0) { hitStop -= effectiveDt; return; }
gameTime += effectiveDt;
if (slowMo < 1) slowMo = Math.min(1, slowMo + effectiveDt * 0.5);
if (screenShake.duration > 0) {
screenShake.duration -= effectiveDt;
camera.shakeX = (Math.random()-0.5) * screenShake.intensity;
camera.shakeY = (Math.random()-0.5) * screenShake.intensity;
screenShake.intensity *= 0.95;
} else { camera.shakeX = 0; camera.shakeY = 0; }
for (var i=0;i<celestialBodies.length;i++) {
var b = celestialBodies[i];
if (b.type === 'planet') {
b.orbitAngle += b.orbitSpeed * effectiveDt * 0.3;
b.x = W/2 + Math.cos(b.orbitAngle) * b.orbitDist;
b.y = H/2 + Math.sin(b.orbitAngle) * b.orbitDist;
b.pulse += effectiveDt * 2;
} else if (b.type === 'star') { b.pulse += effectiveDt * 3; }
}
if (blackHole) {
blackHole.life -= effectiveDt;
blackHole.pulse += effectiveDt * 4;
if (blackHole.life <= 0) {
for (var i=0;i<80;i++) spawnParticle(blackHole.x,blackHole.y,(Math.random()-0.5)*600,(Math.random()-0.5)*600,'#fff',1);
addShockwave(blackHole.x,blackHole.y,300);
screenShake.intensity = 20; screenShake.duration = 1; hitStop = 0.3;
playSound('explosion'); blackHole = null;
}
}
if (ship && ship.alive) updateShip(effectiveDt);
// Bullets
for (var i=bullets.length-1;i>=0;i--) {
var b = bullets[i];
applyGravity(b, celestialBodies);
if (blackHole) applyGravity(b, [blackHole]);
b.x += b.vx * effectiveDt; b.y += b.vy * effectiveDt; b.life -= effectiveDt;
if (b.life <= 0 || b.x<-100 || b.x>W+100 || b.y<-100 || b.y>H+100) { bullets.splice(i,1); continue; }
if (b.owner === 'player') {
for (var j=enemies.length-1;j>=0;j--) {
var e = enemies[j]; if (!e.alive) continue;
var dx = b.x-e.x, dy = b.y-e.y;
if (dx*dx+dy*dy < (b.radius+e.radius)*(b.radius+e.radius)) {
e.health -= b.damage;
spawnParticle(b.x,b.y,(Math.random()-0.5)*200,(Math.random()-0.5)*200,'#ff4',0.3);
bullets.splice(i,1);
if (e.health <= 0) killEnemy(e);
break;
}
}
} else if (b.owner === 'enemy') {
if (ship && ship.alive) {
var dx = b.x-ship.x, dy = b.y-ship.y;
if (dx*dx+dy*dy < (b.radius+12)*(b.radius+12)) {
if (ship.invuln <= 0) { ship.health -= b.damage; screenShake.intensity=5; screenShake.duration=0.2; playSound('graze'); }
bullets.splice(i,1);
if (ship.health <= 0) killShip();
}
}
}
}
// Enemies
for (var i=enemies.length-1;i>=0;i--) {
var e = enemies[i];
if (!e.alive) { enemies.splice(i,1); continue; }
applyGravity(e, celestialBodies);
if (blackHole) applyGravity(e, [blackHole]);
if (ship && ship.alive) {
var sdx = ship.x-e.x, sdy = ship.y-e.y;
var sdist = Math.sqrt(sdx*sdx+sdy*sdy);
if (e.type==='drone') { e.vx+=(sdx/sdist)*e.speed*60*effectiveDt; e.vy+=(sdy/sdist)*e.speed*60*effectiveDt; }
else if (e.type==='mine') {
if (sdist<250) e.accel=Math.min(1,e.accel+effectiveDt*2);
e.vx+=(sdx/sdist)*e.speed*e.accel*60*effectiveDt; e.vy+=(sdy/sdist)*e.speed*e.accel*60*effectiveDt;
} else if (e.type==='gunship') {
e.vx+=(sdx/sdist)*e.speed*30*effectiveDt; e.vy+=(sdy/sdist)*e.speed*30*effectiveDt;
e.fireTimer -= effectiveDt;
if (e.fireTimer <= 0) {
e.fireTimer = e.fireRate;
var ba = Math.atan2(sdy,sdx);
bullets.push({x:e.x+Math.cos(ba)*e.radius,y:e.y+Math.sin(ba)*e.radius,
vx:Math.cos(ba)*300,vy:Math.sin(ba)*300,radius:4,damage:10,life:3,owner:'enemy',color:'#f44'});
}
} else if (e.type==='boss') {
e.vx+=(sdx/sdist)*e.speed*20*effectiveDt; e.vy+=(sdy/sdist)*e.speed*20*effectiveDt;
e.fireTimer -= effectiveDt;
if (e.fireTimer <= 0) {
e.fireTimer = 0.5;
for (var a=0;a<8;a++) {
var aa = a*Math.PI/4+gameTime;
bullets.push({x:e.x+Math.cos(aa)*e.radius,y:e.y+Math.sin(aa)*e.radius,
vx:Math.cos(aa)*200,vy:Math.sin(aa)*200,radius:5,damage:15,life:3,owner:'enemy',color:'#f84'});
}
}
}
}
e.x += e.vx*effectiveDt; e.y += e.vy*effectiveDt; e.angle = Math.atan2(e.vy,e.vx);
// Screen wrapping for enemies
if (e.x < -100) e.x = W+100;
if (e.x > W+100) e.x = -100;
if (e.y < -100) e.y = H+100;
if (e.y > H+100) e.y = -100;
if (ship && ship.alive) {
var dx = e.x-ship.x, dy = e.y-ship.y;
if (dx*dx+dy*dy < (e.radius+12)*(e.radius+12)) {
if (ship.invuln <= 0) { ship.health-=10; screenShake.intensity=8; screenShake.duration=0.3; playSound('graze');
spawnParticle(e.x,e.y,(Math.random()-0.5)*300,(Math.random()-0.5)*300,'#f44',0.5); }
if (e.type!=='gunship' && e.type!=='boss') { e.alive=false; killEnemy(e); }
if (ship.health <= 0) killShip();
}
}
for (var ci=0;ci<celestialBodies.length;ci++) {
var cb = celestialBodies[ci];
var dx2 = e.x-cb.x, dy2 = e.y-cb.y;
if (dx2*dx2+dy2*dy2 < (e.radius+cb.radius)*(e.radius+cb.radius)) { e.alive=false; killEnemy(e); break; }
}
}
// Pickups
for (var i=pickups.length-1;i>=0;i--) {
var p = pickups[i]; if (!p.alive) { pickups.splice(i,1); continue; }
var body = celestialBodies[p.orbitBody];
p.orbitAngle += 0.5*effectiveDt;
p.x = body.x+Math.cos(p.orbitAngle)*p.orbitDist; p.y = body.y+Math.sin(p.orbitAngle)*p.orbitDist;
if (ship && ship.alive) {
var dx = p.x-ship.x, dy = p.y-ship.y;
if (dx*dx+dy*dy < (p.radius+15)*(p.radius+15)) {
applyPickup(p.type); p.alive=false; playSound('pickup');
spawnParticle(p.x,p.y,(Math.random()-0.5)*200,(Math.random()-0.5)*200,'#4f8',0.5);
}
}
}
for (var i=particles.length-1;i>=0;i--) {
var p = particles[i];
p.x+=p.vx*effectiveDt; p.y+=p.vy*effectiveDt; p.vx*=0.97; p.vy*=0.97; p.life-=effectiveDt;
if (p.life<=0) particles.splice(i,1);
}
for (var i=shockwaves.length-1;i>=0;i--) {
var s = shockwaves[i]; s.radius+=s.speed*effectiveDt; s.life-=effectiveDt;
if (s.life<=0) shockwaves.splice(i,1);
}
for (var i=popups.length-1;i>=0;i--) {
var p = popups[i]; p.y-=40*effectiveDt; p.life-=effectiveDt;
if (p.life<=0) popups.splice(i,1);
}
if (comboTimer > 0) { comboTimer -= effectiveDt; if (comboTimer <= 0) combo = 0; }
if (enemies.length === 0 && !blackHole) {
waveTimer -= effectiveDt;
if (waveTimer <= 0) {
if (wave % 5 === 0) showUpgradeScreen();
else nextWave();
}
}
updateHUD();
}
function updateShip(dt) {
var rotSpeed = 3;
if (keys['ArrowLeft'] || keys['KeyA']) ship.angle -= rotSpeed*dt;
if (keys['ArrowRight'] || keys['KeyD']) ship.angle += rotSpeed*dt;
var thrusting = false;
if ((keys['ArrowUp'] || keys['KeyW']) && ship.fuel > 0) {
var thrust = ship.boostActive ? 800 : 400;
ship.vx += Math.cos(ship.angle)*thrust*dt; ship.vy += Math.sin(ship.angle)*thrust*dt;
ship.fuel = Math.max(0, ship.fuel - 25*dt); thrusting = true;
if (Math.random() < 0.3) playSound('thrust');
for (var i=0;i<2;i++) {
spawnParticle(ship.x-Math.cos(ship.angle)*15,ship.y-Math.sin(ship.angle)*15,
-Math.cos(ship.angle)*200+(Math.random()-0.5)*100,
-Math.sin(ship.angle)*200+(Math.random()-0.5)*100,
ship.boostActive?'#f84':'#4af',0.4);
}
}
ship.boostActive = keys['ShiftLeft'] || keys['ShiftRight'];
if (ship.boostActive && ship.boostFuel > 0) ship.boostFuel = Math.max(0, ship.boostFuel-dt);
else if (!ship.boostActive) ship.boostFuel = Math.min(3, ship.boostFuel+dt*0.3);
if (!thrusting) ship.fuel = Math.min(ship.maxFuel, ship.fuel+5*dt);
if (ship.upgrades.overdrive > 0) ship.fuel = Math.min(ship.maxFuel, ship.fuel+3*ship.upgrades.overdrive*dt);
applyGravity(ship, celestialBodies);
if (blackHole) applyGravity(ship, [blackHole]);
ship.x += ship.vx*dt; ship.y += ship.vy*dt;
// Screen wrapping
if (ship.x < -50) ship.x = W+50;
if (ship.x > W+50) ship.x = -50;
if (ship.y < -50) ship.y = H+50;
if (ship.y > H+50) ship.y = -50;
var speed = Math.sqrt(ship.vx*ship.vx+ship.vy*ship.vy);
if (speed > maxSpeed) maxSpeed = speed;
ship.trail.push({x:ship.x,y:ship.y,t:gameTime});
while (ship.trail.length > 60) ship.trail.shift();
if (ship.invuln > 0) ship.invuln -= dt;
ship.fireTimer -= dt;
var rate = ship.fireRate * Math.pow(0.7, ship.upgrades.rapid);
if ((keys['Space'] || mouse.down) && ship.fireTimer <= 0) {
ship.fireTimer = rate; fireBullet(); playSound('fire');
}
for (var i=0;i<celestialBodies.length;i++) {
var b = celestialBodies[i];
var dx = ship.x-b.x, dy = ship.y-b.y;
var dist = Math.sqrt(dx*dx+dy*dy);
if (dist < 12+b.radius) {
if (ship.invuln<=0){ship.health-=10;screenShake.intensity=8;screenShake.duration=0.3;playSound('graze');}
// Bounce ship away
var overlap = (12+b.radius) - dist;
ship.x += (dx/dist)*overlap*2;
ship.y += (dy/dist)*overlap*2;
ship.vx += (dx/dist)*200;
ship.vy += (dy/dist)*200;
if(ship.health<=0)killShip(); break;
}
}
if (blackHole) {
var dx = ship.x-blackHole.x, dy = ship.y-blackHole.y;
var dist = Math.sqrt(dx*dx+dy*dy);
if (dist < 12+blackHole.radius) {
if (ship.invuln<=0){ship.health-=10;screenShake.intensity=8;screenShake.duration=0.3;playSound('graze');}
var overlap = (12+blackHole.radius) - dist;
ship.x += (dx/dist)*overlap*2;
ship.y += (dy/dist)*overlap*2;
ship.vx += (dx/dist)*200;
ship.vy += (dy/dist)*200;
if(ship.health<=0)killShip();
}
}
camera.x += (ship.x-camera.x)*0.08; camera.y += (ship.y-camera.y)*0.08;
}
function fireBullet() {
var bx = ship.x+Math.cos(ship.angle)*18, by = ship.y+Math.sin(ship.angle)*18;
if (ship.upgrades.spread > 0) {
for (var i=0;i<1+ship.upgrades.spread;i++) {
var sa = ship.angle+(i-ship.upgrades.spread/2)*0.15;
bullets.push({x:bx,y:by,vx:Math.cos(sa)*600+ship.vx*0.3,vy:Math.sin(sa)*600+ship.vy*0.3,
radius:3,damage:1,life:2,owner:'player',color:'#4ff'});
}
} else {
bullets.push({x:bx,y:by,vx:Math.cos(ship.angle)*600+ship.vx*0.3,vy:Math.sin(ship.angle)*600+ship.vy*0.3,
radius:3,damage:1,life:2,owner:'player',color:'#4ff'});
}
}
function killEnemy(e) {
e.alive = false; totalKills++;
var pts = (e.type==='boss'?500:e.type==='gunship'?100:e.type==='mine'?50:25)*(1+combo*0.5);
score += Math.floor(pts); combo++; comboTimer = 3;
addPopup(Math.floor(pts), e.x, e.y);
addShockwave(e.x,e.y,e.type==='boss'?200:80);
screenShake.intensity = e.type==='boss'?15:5; screenShake.duration = e.type==='boss'?0.5:0.15; hitStop = e.type==='boss'?0.2:0.05;
playSound('explosion');
var pCount = e.type==='boss'?40:15;
for (var i=0;i<pCount;i++) spawnParticle(e.x,e.y,(Math.random()-0.5)*400,(Math.random()-0.5)*400,e.type==='boss'?'#f84':'#f44',0.8);
if (Math.random() < 0.3) spawnPickup(e.x, e.y);
if (enemies.length === 0 && !blackHole) waveTimer = waveDelay;
}
function killShip() {
if (!ship.alive) return;
ship.alive = false; slowMo = 0.2; hitStop = 0.5;
screenShake.intensity = 25; screenShake.duration = 1; playSound('death');
for (var i=0;i<60;i++) spawnParticle(ship.x,ship.y,(Math.random()-0.5)*500,(Math.random()-0.5)*500,'#ff4',1.5);
addShockwave(ship.x,ship.y,200);
setTimeout(function() { gameState = 'GAMEOVER'; showGameOver(); }, 2000);
}
function applyPickup(type) {
if (type==='fuel') { ship.fuel=Math.min(ship.maxFuel,ship.fuel+40); addPopup('+FUEL',ship.x,ship.y-30); }
else if (type==='shield') { ship.health=Math.min(ship.maxHealth,ship.health+30); ship.invuln=5; addPopup('+SHIELD',ship.x,ship.y-30); }
else if (type==='spread') { ship.upgrades.spread=Math.min(3,ship.upgrades.spread+1); addPopup('SPREAD+',ship.x,ship.y-30); }
else if (type==='rapid') { ship.upgrades.rapid=Math.min(3,ship.upgrades.rapid+1); addPopup('RAPID+',ship.x,ship.y-30); }
else if (type==='bomb') {
addShockwave(ship.x,ship.y,350);
for (var i=enemies.length-1;i>=0;i--) {
var e = enemies[i];
var dx = e.x-ship.x, dy = e.y-ship.y;
var dist = Math.sqrt(dx*dx+dy*dy);
if (dist < 250) { e.alive = false; killEnemy(e); }
}
addPopup('BOOM!',ship.x,ship.y-30);
}
else if (type==='overdrive') { ship.upgrades.overdrive=Math.min(3,ship.upgrades.overdrive+1); addPopup('OVERDRIVE+',ship.x,ship.y-30); }
}
function updateHUD() {
if (!ship) return;
document.getElementById('health-bar').style.width = (ship.health/ship.maxHealth*100)+'%';
document.getElementById('fuel-bar').style.width = (ship.fuel/ship.maxFuel*100)+'%';
document.getElementById('wave-display').textContent = 'WAVE ' + wave;
document.getElementById('score-display').textContent = score;
document.getElementById('combo-display').textContent = combo > 1 ? 'x' + combo : '';
var ud = document.getElementById('upgrades-display');
ud.innerHTML = '';
if (ship.upgrades.spread > 0) ud.innerHTML += '<div class="upgrade-icon" title="Spread">S</div>';
if (ship.upgrades.rapid > 0) ud.innerHTML += '<div class="upgrade-icon" title="Rapid">R</div>';
if (ship.upgrades.overdrive > 0) ud.innerHTML += '<div class="upgrade-icon" title="Overdrive">O</div>';
document.getElementById('highscore-hud').textContent = highScore;
}
function showGameOver() {
showScreen('gameover-screen');
var newHS = score > highScore;
if (newHS) { highScore = score; localStorage.setItem('orbital_hs', highScore); }
document.getElementById('go-stats').innerHTML =
'Time: ' + Math.floor(gameTime) + 's<br>Wave: ' + wave + '<br>Kills: ' + totalKills +
'<br>Max Speed: ' + Math.floor(maxSpeed) + '<br>Score: ' + score;
document.getElementById('go-new').style.display = newHS ? 'block' : 'none';
}
function showUpgradeScreen() {
gameState = 'UPGRADE';
showScreen('upgrade-screen');
var allUpgrades = [
{id:'spread',icon:'S',name:'SPREAD SHOT',desc:'Fire multiple projectiles'},
{id:'rapid',icon:'R',name:'RAPID FIRE',desc:'Increase fire rate'},
{id:'overdrive',icon:'O',name:'OVERDRIVE',desc:'Faster fuel regen'},
{id:'health',icon:'H',name:'REPAIR',desc:'+30 health'},
{id:'fuel',icon:'F',name:'FUEL CELL',desc:'Full fuel restore'},
{id:'boost',icon:'B',name:'BOOST',desc:'Restore boost charge'}
];
var cards = document.getElementById('upgrade-cards');
cards.innerHTML = '';
var choices = [];
var pool = allUpgrades.slice();
for (var i=0;i<3;i++) {
var idx = Math.floor(Math.random()*pool.length);
choices.push(pool.splice(idx,1)[0]);
}
for (var i=0;i<choices.length;i++) {
var c = choices[i];
var card = document.createElement('div');
card.className = 'upgrade-card';
card.innerHTML = '<div class="uc-icon">' + c.icon + '</div><div class="uc-name">' + c.name + '</div><div class="uc-desc">' + c.desc + '</div>';
card.addEventListener('click', (function(ch) {
return function() { applyUpgradeCard(ch); };
})(c));
cards.appendChild(card);
}
}
function applyUpgradeCard(c) {
if (c.id === 'spread') ship.upgrades.spread = Math.min(3, ship.upgrades.spread+1);
else if (c.id === 'rapid') ship.upgrades.rapid = Math.min(3, ship.upgrades.rapid+1);
else if (c.id === 'overdrive') ship.upgrades.overdrive = Math.min(3, ship.upgrades.overdrive+1);
else if (c.id === 'health') ship.health = Math.min(ship.maxHealth, ship.health+30);
else if (c.id === 'fuel') ship.fuel = ship.maxFuel;
else if (c.id === 'boost') ship.boostFuel = 3;
gameState = 'PLAYING';
showScreen(null);
nextWave();
}
// ─── RENDERING ───
function generateStars() {
stars = [];
for (var i=0;i<300;i++) {
stars.push({x:Math.random()*2000-500,y:Math.random()*2000-500,
size:Math.random()*2+0.5,brightness:Math.random(),twinkleSpeed:Math.random()*3+1,
layer:Math.floor(Math.random()*3)});
}
}
function generateNebulae() {
nebulae = [];
for (var i=0;i<5;i++) {
nebulae.push({x:Math.random()*W,y:Math.random()*H,radius:Math.random()*200+100,
color:['#1a0a3a','#0a1a3a','#1a0a2a','#0a2a1a','#2a0a1a'][i],opacity:0.08});
}
}
function render() {
ctx.clearRect(0,0,W,H);
// Background gradient
var bg = ctx.createRadialGradient(W/2,H/2,0,W/2,H/2,Math.max(W,H));
bg.addColorStop(0,'#0a0a1a'); bg.addColorStop(1,'#000005');
ctx.fillStyle = bg; ctx.fillRect(0,0,W,H);
// Nebulae
for (var i=0;i<nebulae.length;i++) {
var n = nebulae[i];
var ng = ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,n.radius);
ng.addColorStop(0,n.color); ng.addColorStop(1,'transparent');
ctx.fillStyle = ng; ctx.fillRect(n.x-n.radius,n.y-n.radius,n.radius*2,n.radius*2);
}
// Stars with parallax
for (var i=0;i<stars.length;i++) {
var s = stars[i];
var px = (s.x - camera.x*0.01*s.layer) % W;
var py = (s.y - camera.y*0.01*s.layer) % H;
if (px < 0) px += W; if (py < 0) py += H;
var twinkle = 0.5 + 0.5*Math.sin(gameTime*s.twinkleSpeed);
ctx.globalAlpha = s.brightness * twinkle;
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(px,py,s.size,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
if (gameState === 'TITLE') {
renderTitleScene();
return;
}
// Camera transform
ctx.save();
ctx.translate(camera.shakeX, camera.shakeY);
// Gravity field lines
renderGravityField();
// Celestial bodies
for (var i=0;i<celestialBodies.length;i++) renderCelestial(celestialBodies[i]);
// Black hole
if (blackHole) renderBlackHole(blackHole);
// Ship trail
if (ship && ship.trail.length > 1) {
ctx.beginPath();
ctx.moveTo(ship.trail[0].x, ship.trail[0].y);
for (var i=1;i<ship.trail.length;i++) {
ctx.lineTo(ship.trail[i].x, ship.trail[i].y);
}
ctx.strokeStyle = 'rgba(0,170,255,0.3)';
ctx.lineWidth = 2;
ctx.stroke();
}
// Ship
if (ship && ship.alive) renderShip();
// Trajectory prediction
if (ship && ship.alive) renderTrajectory();
// Enemies
for (var i=0;i<enemies.length;i++) renderEnemy(enemies[i]);
// Bullets
for (var i=0;i<bullets.length;i++) renderBullet(bullets[i]);
// Pickups
for (var i=0;i<pickups.length;i++) renderPickup(pickups[i]);
// Particles
ctx.globalCompositeOperation = 'lighter';
for (var i=0;i<particles.length;i++) {
var p = particles[i];
var alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath(); ctx.arc(p.x,p.y,2+alpha*2,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
// Shockwaves
for (var i=0;i<shockwaves.length;i++) {
var s = shockwaves[i];
var alpha = s.life / 0.5;
ctx.strokeStyle = 'rgba(255,255,255,' + alpha*0.5 + ')';
ctx.lineWidth = 3*alpha;
ctx.beginPath(); ctx.arc(s.x,s.y,s.radius,0,Math.PI*2); ctx.stroke();
}
// Popups
for (var i=0;i<popups.length;i++) {
var p = popups[i];
ctx.globalAlpha = p.life / 1.5;
ctx.fillStyle = '#ff4';
ctx.font = 'bold 16px Orbitron';
ctx.textAlign = 'center';
ctx.fillText(p.text, p.x, p.y);
}
ctx.globalAlpha = 1;
ctx.restore();
// Radar
renderRadar();
}
function renderTitleScene() {
// Draw a mini solar system for the title
var star = {x:W/2,y:H/2,mass:8000,radius:50,type:'star',pulse:gameTime*3};
renderCelestial(star);
var planet = {x:W/2+150*Math.cos(gameTime*0.3),y:H/2+150*Math.sin(gameTime*0.3),
mass:1200,radius:28,type:'planet',color:'#4af',pulse:gameTime*2};
renderCelestial(planet);
if (titleShip) {
applyGravity(titleShip, [star, planet]);
titleShip.x += titleShip.vx*dt; titleShip.y += titleShip.vy*dt;
titleShip.angle = Math.atan2(titleShip.vy, titleShip.vx);
titleShip.trail.push({x:titleShip.x,y:titleShip.y});
while (titleShip.trail.length > 40) titleShip.trail.shift();
if (titleShip.trail.length > 1) {
ctx.beginPath(); ctx.moveTo(titleShip.trail[0].x,titleShip.trail[0].y);
for (var i=1;i<titleShip.trail.length;i++) ctx.lineTo(titleShip.trail[i].x,titleShip.trail[i].y);
ctx.strokeStyle = 'rgba(0,170,255,0.3)'; ctx.lineWidth = 2; ctx.stroke();
}
ctx.save();
ctx.translate(titleShip.x, titleShip.y);
ctx.rotate(titleShip.angle);
ctx.fillStyle = '#4af';
ctx.beginPath(); ctx.moveTo(12,0); ctx.lineTo(-8,-6); ctx.lineTo(-8,6); ctx.closePath(); ctx.fill();
ctx.restore();
// Reset if too far
var dx = titleShip.x-W/2, dy = titleShip.y-H/2;
if (dx*dx+dy*dy > 100000) {
titleShip.x = W/2; titleShip.y = H/2+100;
titleShip.vx = 200; titleShip.vy = -100;
titleShip.trail = [];
}
}
}
function renderGravityField() {
ctx.strokeStyle = 'rgba(0,100,255,0.04)';
ctx.lineWidth = 1;
var spacing = 60;
for (var x=0;x<W;x+=spacing) {
ctx.beginPath();
for (var y=0;y<H;y+=10) {
var fx = 0, fy = 0;
for (var i=0;i<celestialBodies.length;i++) {
var b = celestialBodies[i];
var dx = b.x-x, dy = b.y-y;
var d2 = dx*dx+dy*dy+b.radius*b.radius*4;
var f = b.mass/d2;
fx += (dx/d2)*f*500; fy += (dy/d2)*f*500;
}
if (blackHole) {
var dx = blackHole.x-x, dy = blackHole.y-y;
var d2 = dx*dx+dy*dy+blackHole.radius*blackHole.radius*4;
var f = blackHole.mass/d2;
fx += (dx/d2)*f*500; fy += (dy/d2)*f*500;
}
if (y===0) ctx.moveTo(x+fx,y+fy);
else ctx.lineTo(x+fx,y+fy);
}
ctx.stroke();
}
}
function renderCelestial(b) {
if (b.type === 'star') {
var pulse = 1 + 0.1*Math.sin(b.pulse);
var r = b.radius * pulse;
// Outer glow
var g1 = ctx.createRadialGradient(b.x,b.y,r*0.5,b.x,b.y,r*4);
g1.addColorStop(0,'rgba(255,200,50,0.3)'); g1.addColorStop(1,'transparent');
ctx.fillStyle = g1; ctx.beginPath(); ctx.arc(b.x,b.y,r*4,0,Math.PI*2); ctx.fill();
// Core
var g2 = ctx.createRadialGradient(b.x,b.y,0,b.x,b.y,r);
g2.addColorStop(0,'#fff'); g2.addColorStop(0.3,'#ffa'); g2.addColorStop(1,'#f80');
ctx.fillStyle = g2; ctx.beginPath(); ctx.arc(b.x,b.y,r,0,Math.PI*2); ctx.fill();
} else {
var pulse = 1 + 0.05*Math.sin(b.pulse);
var r = b.radius * pulse;
// Atmospheric rim
var g1 = ctx.createRadialGradient(b.x,b.y,r*0.8,b.x,b.y,r*1.5);
g1.addColorStop(0,b.color); g1.addColorStop(1,'transparent');
ctx.fillStyle = g1; ctx.beginPath(); ctx.arc(b.x,b.y,r*1.5,0,Math.PI*2); ctx.fill();
// Body
var g2 = ctx.createRadialGradient(b.x-r*0.3,b.y-r*0.3,0,b.x,b.y,r);
g2.addColorStop(0,'#fff'); g2.addColorStop(0.5,b.color); g2.addColorStop(1,'#111');
ctx.fillStyle = g2; ctx.beginPath(); ctx.arc(b.x,b.y,r,0,Math.PI*2); ctx.fill();
}
}
function renderBlackHole(bh) {
// Accretion disk
ctx.save();
ctx.translate(bh.x, bh.y);
ctx.rotate(bh.pulse*0.2);
for (var i=0;i<3;i++) {
var r = bh.radius + 20 + i*15;
ctx.strokeStyle = 'rgba(100,0,255,' + (0.3-i*0.08) + ')';
ctx.lineWidth = 8-i*2;
ctx.beginPath();
ctx.ellipse(0,0,r,r*0.3,0,0,Math.PI*2);
ctx.stroke();
}
ctx.restore();
// Core
var g = ctx.createRadialGradient(bh.x,bh.y,0,bh.x,bh.y,bh.radius);
g.addColorStop(0,'#000'); g.addColorStop(0.7,'#100020'); g.addColorStop(1,'#400080');
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(bh.x,bh.y,bh.radius,0,Math.PI*2); ctx.fill();
// Lensing ring
ctx.strokeStyle = 'rgba(150,100,255,0.5)';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(bh.x,bh.y,bh.radius+5,0,Math.PI*2); ctx.stroke();
}
function renderShip() {
ctx.save();
ctx.translate(ship.x, ship.y);
ctx.rotate(ship.angle);
if (ship.invuln > 0 && Math.sin(gameTime*20) > 0) { ctx.globalAlpha = 0.5; }
// Ship body
ctx.fillStyle = '#4af';
ctx.beginPath(); ctx.moveTo(14,0); ctx.lineTo(-10,-7); ctx.lineTo(-6,0); ctx.lineTo(-10,7); ctx.closePath(); ctx.fill();
// Cockpit
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(4,0,2,0,Math.PI*2); ctx.fill();
// Engine glow
if (keys['ArrowUp'] || keys['KeyW']) {
ctx.globalCompositeOperation = 'lighter';
var eg = ctx.createRadialGradient(-10,0,0,-10,0,15);
eg.addColorStop(0,ship.boostActive?'#f84':'#4af'); eg.addColorStop(1,'transparent');
ctx.fillStyle = eg; ctx.beginPath(); ctx.arc(-10,0,15,0,Math.PI*2); ctx.fill();
ctx.globalCompositeOperation = 'source-over';
}
// Shield indicator
if (ship.invuln > 0) {
ctx.strokeStyle = 'rgba(0,255,200,0.5)';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(0,0,18,0,Math.PI*2); ctx.stroke();
}
ctx.globalAlpha = 1;
ctx.restore();
}
function renderTrajectory() {
var px = ship.x, py = ship.y, pvx = ship.vx, pvy = ship.vy;
ctx.setLineDash([3,6]);
ctx.strokeStyle = 'rgba(0,170,255,0.4)';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(px,py);
for (var i=0;i<30;i++) {
var tvx = pvx, tvy = pvy;
for (var j=0;j<celestialBodies.length;j++) {
var b = celestialBodies[j];
var dx = b.x-px, dy = b.y-py;
var d2 = dx*dx+dy*dy+b.radius*b.radius*4;
var f = b.mass/d2;
tvx += (dx/d2)*f*0.016*60; tvy += (dy/d2)*f*0.016*60;
}
if (blackHole) {
var dx = blackHole.x-px, dy = blackHole.y-py;
var d2 = dx*dx+dy*dy+blackHole.radius*blackHole.radius*4;
var f = blackHole.mass/d2;
tvx += (dx/d2)*f*0.016*60; tvy += (dy/d2)*f*0.016*60;
}
px += tvx*0.016; py += tvy*0.016; pvx = tvx; pvy = tvy;
ctx.lineTo(px,py);
}
ctx.stroke();
ctx.setLineDash([]);
}
function renderEnemy(e) {
ctx.save();
ctx.translate(e.x, e.y);
if (e.type === 'drone') {
ctx.rotate(e.angle);
ctx.fillStyle = '#f44';
ctx.beginPath(); ctx.moveTo(12,0); ctx.lineTo(-6,-5); ctx.lineTo(-6,5); ctx.closePath(); ctx.fill();
} else if (e.type === 'mine') {
ctx.fillStyle = '#f84';
var pulse = 1 + 0.2*Math.sin(gameTime*5);
ctx.beginPath(); ctx.arc(0,0,e.radius*pulse,0,Math.PI*2); ctx.fill();
ctx.fillStyle = '#fa0';
ctx.beginPath(); ctx.arc(0,0,e.radius*0.4,0,Math.PI*2); ctx.fill();
} else if (e.type === 'gunship') {
ctx.rotate(e.angle);
ctx.fillStyle = '#a44';
ctx.beginPath(); ctx.moveTo(22,0); ctx.lineTo(-14,-10); ctx.lineTo(-10,0); ctx.lineTo(-14,10); ctx.closePath(); ctx.fill();
// HP bar
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(-15,-18,30,4);
ctx.fillStyle = '#f44';
ctx.fillRect(-15,-18,30*(e.health/e.maxHealth),4);
} else if (e.type === 'boss') {
ctx.rotate(gameTime*0.3);
ctx.fillStyle = '#f84';
for (var i=0;i<6;i++) {
var a = i*Math.PI/3;
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(Math.cos(a)*e.radius, Math.sin(a)*e.radius);
ctx.lineTo(Math.cos(a+0.3)*e.radius*0.6, Math.sin(a+0.3)*e.radius*0.6);
ctx.closePath(); ctx.fill();
}
// HP bar
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(-30,-50,60,6);
ctx.fillStyle = '#f84';
ctx.fillRect(-30,-50,60*(e.health/e.maxHealth),6);
}
ctx.restore();
}
function renderBullet(b) {
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = b.color;
ctx.beginPath(); ctx.arc(b.x,b.y,b.radius,0,Math.PI*2); ctx.fill();
// Glow
var g = ctx.createRadialGradient(b.x,b.y,0,b.x,b.y,b.radius*3);
g.addColorStop(0,b.color); g.addColorStop(1,'transparent');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x,b.y,b.radius*3,0,Math.PI*2); ctx.fill();
ctx.globalCompositeOperation = 'source-over';
}
function renderPickup(p) {
ctx.save();
ctx.translate(p.x, p.y);
var pulse = 1 + 0.15*Math.sin(gameTime*4);
var colors = {fuel:'#4f8',shield:'#4ff',spread:'#f44',rapid:'#ff4',bomb:'#f84',overdrive:'#a4f'};
var c = colors[p.type] || '#fff';
// Glow
ctx.globalCompositeOperation = 'lighter';
var g = ctx.createRadialGradient(0,0,0,0,0,p.radius*2.5);
g.addColorStop(0,c); g.addColorStop(1,'transparent');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0,0,p.radius*2.5,0,Math.PI*2); ctx.fill();
ctx.globalCompositeOperation = 'source-over';
// Icon
ctx.fillStyle = c;
ctx.beginPath(); ctx.arc(0,0,p.radius*pulse,0,Math.PI*2); ctx.fill();
ctx.fillStyle = '#000';
ctx.font = 'bold 10px Orbitron';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
var labels = {fuel:'F',shield:'S',spread:'SP',rapid:'RP',bomb:'B',overdrive:'O'};
ctx.fillText(labels[p.type]||'?',0,0);
ctx.restore();
}
function renderRadar() {
if (gameState !== 'PLAYING') return;
radarCtx.clearRect(0,0,80,80);
radarCtx.fillStyle = 'rgba(0,0,0,0.3)';
radarCtx.beginPath(); radarCtx.arc(40,40,38,0,Math.PI*2); radarCtx.fill();
// Ship
if (ship) {
var sx = 40 + (ship.x-W/2)/W*70;
var sy = 40 + (ship.y-H/2)/H*70;
sx = Math.max(5,Math.min(75,sx)); sy = Math.max(5,Math.min(75,sy));
radarCtx.fillStyle = '#4af';
radarCtx.beginPath(); radarCtx.arc(sx,sy,2,0,Math.PI*2); radarCtx.fill();
}
// Enemies
for (var i=0;i<enemies.length;i++) {
var e = enemies[i];
var ex = 40+(e.x-W/2)/W*70, ey = 40+(e.y-H/2)/H*70;
ex = Math.max(5,Math.min(75,ex)); ey = Math.max(5,Math.min(75,ey));
radarCtx.fillStyle = '#f44';
radarCtx.beginPath(); radarCtx.arc(ex,ey,1.5,0,Math.PI*2); radarCtx.fill();
}
// Pickups
for (var i=0;i<pickups.length;i++) {
var p = pickups[i];
var px = 40+(p.x-W/2)/W*70, py = 40+(p.y-H/2)/H*70;
px = Math.max(5,Math.min(75,px)); py = Math.max(5,Math.min(75,py));
radarCtx.fillStyle = '#4f8';
radarCtx.beginPath(); radarCtx.arc(px,py,1.5,0,Math.PI*2); radarCtx.fill();
}
}
// ─── GAME LOOP ───
function loop(timestamp) {
dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
update();
render();
requestAnimationFrame(loop);
}
</script>
</body>
</html>