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