Hull
100
Orbit
Gravity
0%
Score
0
Lives
♥♥♥

// --- Game Loop --- function render() { ctx.fillStyle = '#0a0a12'; ctx.fillRect(0, 0, W(), H()); // Stars stars.forEach(s => { const twinkle = Math.sin(gameTime * s.twinkleSpeed) * 0.3 + 0.7; ctx.fillStyle = `rgba(184,168,255,${s.brightness * twinkle})`; ctx.fillRect(s.x + W()/2, s.y + H()/2, s.size, s.size); }); // Center gravity well const cx = W()/2, cy = H()/2; const gravR = 15 + gravity * 0.3; const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, gravR); grad.addColorStop(0, 'rgba(107,91,255,0.4)'); grad.addColorStop(0.5, 'rgba(107,91,255,0.1)'); grad.addColorStop(1, 'transparent'); ctx.fillStyle = grad; ctx.fillRect(cx - gravR, cy - gravR, gravR*2, gravR*2); // Orbit rings ctx.strokeStyle = 'rgba(184,168,255,0.08)'; ctx.lineWidth = 1; for (let r = 100; r < 300; r += 50) { ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); ctx.stroke(); } if (gameState === 'playing' || gameState === 'paused') { // Ship ctx.save(); ctx.translate(ship.x, ship.y); ctx.rotate(ship.angle); // Shield if (ship.shieldActive) { ctx.strokeStyle = `rgba(107,91,255,${0.4 + Math.sin(gameTime*8)*0.2})`; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, 14, 0, Math.PI*2); ctx.stroke(); } // Invulnerability blink if (ship.invulnTimer > 0 && Math.sin(gameTime * 15) > 0) { ctx.restore(); return; } // Ship body ctx.fillStyle = '#b8a8ff'; ctx.beginPath(); ctx.moveTo(12, 0); ctx.lineTo(-8, -6); ctx.lineTo(-4, 0); ctx.lineTo(-8, 6); ctx.closePath(); ctx.fill(); // Thrust flame if (keys['KeyW'] || keys['ArrowUp']) { ctx.fillStyle = '#ff6bbb'; ctx.beginPath(); ctx.moveTo(-4, -3); ctx.lineTo(-12 - Math.random()*5, 0); ctx.lineTo(-4, 3); ctx.closePath(); ctx.fill(); } ctx.restore(); // Orbitals orbitals.forEach(orb => { // HP bar const hpRatio = orb.hp / orb.maxHp; ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(orb.x - 10, orb.y - orb.radius - 6, 20, 3); ctx.fillStyle = hpRatio > 0.5 ? '#6b5bff' : '#ff4466'; ctx.fillRect(orb.x - 10, orb.y - orb.radius - 6, 20 * hpRatio, 3); // Orbital body ctx.fillStyle = orb.color; ctx.beginPath(); ctx.arc(orb.x, orb.y, orb.radius, 0, Math.PI*2); ctx.fill(); // Inner glow const gGrad = ctx.createRadialGradient(orb.x, orb.y, 0, orb.x, orb.y, orb.radius); gGrad.addColorStop(0, 'rgba(255,255,255,0.3)'); gGrad.addColorStop(1, 'transparent'); ctx.fillStyle = gGrad; ctx.beginPath(); ctx.arc(orb.x, orb.y, orb.radius, 0, Math.PI*2); ctx.fill(); }); // Projectiles projectiles.forEach(p => { ctx.fillStyle = '#6b5bff'; ctx.shadowColor = '#6b5bff'; ctx.shadowBlur = 8; ctx.fillRect(p.x-2, p.y-2, 4, 4); ctx.shadowBlur = 0; }); enemyProjectiles.forEach(p => { ctx.fillStyle = '#ff4466'; ctx.shadowColor = '#ff4466'; ctx.shadowBlur = 8; ctx.fillRect(p.x-2, p.y-2, 4, 4); ctx.shadowBlur = 0; }); // Particles particles.forEach(p => { ctx.fillStyle = p.color; ctx.globalAlpha = p.life; ctx.beginPath(); ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI*2); ctx.fill(); }); ctx.globalAlpha = 1; // Gravity well lines ctx.strokeStyle = 'rgba(107,91,255,0.03)'; for (let a = 0; a < Math.PI*2; a += 0.3) { ctx.beginPath(); const r1 = gravR; const r2 = gravR + 30 + Math.sin(gameTime*2 + a)*15; ctx.moveTo(cx + Math.cos(a)*r1, cy + Math.sin(a)*r1); ctx.lineTo(cx + Math.cos(a)*r2, cy + Math.sin(a)*r2); ctx.stroke(); } // HUD update document.getElementById('hull-fill').style.width = hull + '%'; document.getElementById('hull-text').textContent = Math.max(0, Math.round(hull)); document.getElementById('score-text').textContent = score; document.getElementById('grav-text').textContent = Math.round(gravity); document.getElementById('lives-text').textContent = '♥'.repeat(Math.max(0,lives)) + '♡'.repeat(Math.max(0, 3-lives)); const gravFill = document.getElementById('gravity-fill'); gravFill.style.width = gravity + '%'; // Pause overlay if (gameState === 'paused') { ctx.fillStyle = 'rgba(10,10,18,0.7)'; ctx.fillRect(0, 0, W(), H()); ctx.fillStyle = '#b8a8ff'; ctx.font = '32px Courier New'; ctx.textAlign = 'center'; ctx.fillText('PAUSED', W()/2, H()/2 - 20); ctx.font = '14px Courier New'; ctx.fillStyle = '#8a7acc'; ctx.fillText('Press P to resume', W()/2, H()/2 + 20); } } // Menu screen if (gameState === 'menu') { ctx.fillStyle = 'rgba(10,10,18,0.85)'; ctx.fillRect(0, 0, W(), H()); ctx.textAlign = 'center'; // Title glow ctx.shadowColor = '#b8a8ff'; ctx.shadowBlur = 20; ctx.fillStyle = '#b8a8ff'; ctx.font = 'bold 36px Courier New'; ctx.fillText('TWIN ORBITS', W()/2, H()/2 - 60); ctx.shadowBlur = 0; ctx.fillStyle = '#6b5bff'; ctx.font = '16px Courier New'; ctx.fillText('Gravity Well Piloting', W()/2, H()/2 - 30); ctx.fillStyle = '#8a7acc'; ctx.font = '12px Courier New'; ctx.fillText('W/S or Arrow Keys — Thrust/Brake', W()/2, H()/2 + 20); ctx.fillText('A/D or Mouse — Aim & Rotate', W()/2, H()/2 + 40); ctx.fillText('Space / Click — Fire', W()/2, H()/2 + 60); ctx.fillStyle = '#ff6bbb'; ctx.font = '14px Courier New'; const pulse = Math.sin(gameTime * 3) * 0.3 + 0.7; ctx.globalAlpha = pulse; ctx.fillText('[ Click or press Space to Start ]', W()/2, H()/2 + 110); ctx.globalAlpha = 1; } // Game Over screen if (gameState === 'gameover') { ctx.fillStyle = 'rgba(10,10,18,0.8)'; ctx.fillRect(0, 0, W(), H()); ctx.textAlign = 'center'; ctx.shadowColor = '#ff4466'; ctx.shadowBlur = 15; ctx.fillStyle = '#ff4466'; ctx.font = 'bold 32px Courier New'; ctx.fillText('GAME OVER', W()/2, H()/2 - 50); ctx.shadowBlur = 0; ctx.fillStyle = '#b8a8ff'; ctx.font = '16px Courier New'; ctx.fillText('Score: ' + score, W()/2, H()/2); ctx.fillText('Level: ' + level, W()/2, H()/2 + 25); ctx.fillStyle = '#8a7acc'; ctx.font = '14px Courier New'; const pulse = Math.sin(gameTime * 3) * 0.3 + 0.7; ctx.globalAlpha = pulse; ctx.fillText('[ Click or press Space to Retry ]', W()/2, H()/2 + 80); ctx.globalAlpha = 1; } // Screen flash if (flashAlpha > 0.01) { ctx.fillStyle = `rgba(255,255,255,${flashAlpha})`; ctx.fillRect(0, 0, W(), H()); } } function loop(timestamp) { const dt = Math.min((timestamp - lastTime) / 1000, 0.05); lastTime = timestamp || 0; update(dt); render(); requestAnimationFrame(loop); } lastTime = performance.now(); requestAnimationFrame(loop);