Meteor Dodge
by SolarScout643419 lines115.0 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Meteor Dodge</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
```
body {
width: 100vw;
height: 100vh;
overflow: hidden;
background: #000;
font-family: 'Arial', sans-serif;
touch-action: none;
}
#gameContainer {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
#gameCanvas {
display: block;
width: 100%;
height: 100%;
touch-action: none;
}
#hud {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
display: flex;
justify-content: space-between;
align-items: flex-start;
pointer-events: none;
z-index: 10;
}
.stat-box {
background: rgba(0, 0, 0, 0.8);
padding: 15px 25px;
border-radius: 15px;
border: 3px solid #ff6600;
box-shadow: 0 0 20px rgba(255, 102, 0, 0.6);
}
.stat-label {
font-size: 16px;
color: #ff6600;
margin-bottom: 5px;
text-transform: uppercase;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #ffffff;
}
#health-bar-container {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
pointer-events: none;
}
#hearts-display {
display: flex;
gap: 15px;
font-size: 48px;
filter: drop-shadow(0 0 10px rgba(255, 0, 0, 0.8));
}
.heart {
transition: opacity 0.3s, transform 0.3s;
}
.heart.lost {
opacity: 0.2;
transform: scale(0.8);
}
@keyframes pulse {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
}
#startScreen, #gameOver {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #000000 0%, #1a0a00 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 25;
color: white;
}
#gameOver {
display: none;
}
h1 {
font-size: 56px;
margin-bottom: 15px;
color: #ff6600;
text-shadow: 0 0 40px rgba(255, 102, 0, 1);
}
h2 {
font-size: 32px;
margin-bottom: 20px;
color: #ffaa00;
text-shadow: 0 0 20px rgba(255, 170, 0, 0.8);
}
p {
font-size: 20px;
margin-bottom: 10px;
text-align: center;
max-width: 600px;
line-height: 1.5;
color: #cccccc;
padding: 0 20px;
}
.btn {
background: linear-gradient(135deg, #ff6600 0%, #ff3300 100%);
color: white;
border: none;
padding: 20px 50px;
font-size: 28px;
border-radius: 50px;
cursor: pointer;
margin-top: 20px;
font-weight: bold;
box-shadow: 0 8px 25px rgba(255, 102, 0, 0.6);
transition: transform 0.2s;
}
.btn:active {
transform: scale(0.95);
}
#instructions {
position: absolute;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
font-size: 22px;
color: #ffaa00;
text-align: center;
text-shadow: 0 0 10px rgba(255, 170, 0, 0.6);
pointer-events: none;
z-index: 10;
background: rgba(0, 0, 0, 0.7);
padding: 15px 30px;
border-radius: 15px;
}
#weaponDisplay {
position: absolute;
top: 100px;
left: 50%;
transform: translateX(-50%);
font-size: 24px;
font-weight: bold;
color: #fff;
text-align: center;
text-shadow: 0 0 15px rgba(255, 255, 255, 0.8);
pointer-events: none;
z-index: 10;
background: rgba(0, 0, 0, 0.85);
padding: 15px 30px;
border-radius: 15px;
border: 3px solid #ffaa00;
display: none;
}
#weaponNotification {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 48px;
font-weight: bold;
text-align: center;
pointer-events: none;
z-index: 20;
opacity: 0;
transition: opacity 0.3s;
}
.star {
position: absolute;
background: white;
border-radius: 50%;
animation: twinkle 2s infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
#upgradeShop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
display: none;
flex-direction: column;
align-items: center;
padding: 40px 20px;
overflow-y: auto;
z-index: 30;
color: white;
}
#upgradeShop h2 {
font-size: 48px;
margin-bottom: 10px;
color: #ff6600;
text-shadow: 0 0 20px rgba(255, 102, 0, 0.8);
}
#upgradeShop .credits-display {
font-size: 28px;
margin-bottom: 30px;
color: #ffaa00;
}
.upgrade-container {
display: flex;
flex-direction: column;
gap: 20px;
max-width: 600px;
width: 100%;
}
.upgrade-card {
background: rgba(255, 255, 255, 0.1);
border: 3px solid #ff6600;
border-radius: 20px;
padding: 25px;
box-shadow: 0 4px 20px rgba(255, 102, 0, 0.3);
}
.upgrade-card h3 {
font-size: 32px;
margin-bottom: 10px;
color: #ffaa00;
}
.upgrade-card p {
font-size: 18px;
margin-bottom: 15px;
color: #ccc;
}
.upgrade-level {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
font-size: 20px;
}
.level-bar {
flex: 1;
height: 30px;
background: rgba(0, 0, 0, 0.5);
border-radius: 15px;
overflow: hidden;
border: 2px solid #666;
}
.level-fill {
height: 100%;
background: linear-gradient(90deg, #ff6600, #ffaa00);
transition: width 0.3s;
box-shadow: 0 0 10px rgba(255, 102, 0, 0.6);
}
.upgrade-btn {
background: linear-gradient(135deg, #00ff00 0%, #00cc00 100%);
color: #000;
border: none;
padding: 15px 30px;
font-size: 22px;
border-radius: 25px;
cursor: pointer;
font-weight: bold;
box-shadow: 0 4px 15px rgba(0, 255, 0, 0.4);
transition: transform 0.2s;
}
.upgrade-btn:active {
transform: scale(0.95);
}
.upgrade-btn:disabled {
background: #666;
cursor: not-allowed;
box-shadow: none;
}
.upgrade-btn.maxed {
background: linear-gradient(135deg, #ffaa00 0%, #ff6600 100%);
color: white;
}
.close-shop-btn {
background: linear-gradient(135deg, #ff6600 0%, #ff3300 100%);
color: white;
border: none;
padding: 15px 40px;
font-size: 24px;
border-radius: 50px;
cursor: pointer;
margin-top: 30px;
font-weight: bold;
box-shadow: 0 6px 20px rgba(255, 102, 0, 0.6);
transition: transform 0.2s;
}
.close-shop-btn:active {
transform: scale(0.95);
}
</style>
```
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas"></canvas>
```
<div id="hud">
<div class="stat-box">
<div class="stat-label">Score</div>
<div class="stat-value" id="score">0</div>
</div>
<div class="stat-box">
<div class="stat-label">Time</div>
<div class="stat-value" id="time">0s</div>
</div>
<div class="stat-box">
<div class="stat-label">Credits</div>
<div class="stat-value" id="credits">0</div>
</div>
</div>
<div id="health-bar-container">
<div id="hearts-display"></div>
</div>
<div id="instructions">Drag to move • Tap to shoot</div>
<div id="weaponDisplay"></div>
<div id="weaponNotification"></div>
<div id="startScreen">
<h1>☄️ METEOR DODGE ☄️</h1>
<h2>🚀 SURVIVAL MODE 🚀</h2>
<p>
<strong>MISSION BRIEFING:</strong><br><br>
Drag your finger to pilot your spaceship<br>
Tap anywhere to shoot laser bullets<br>
Destroy meteors for bonus points!<br>
Collect power-ups for shield repairs<br>
Survive as long as possible!<br><br>
The longer you survive, the harder it gets!
</p>
<button class="btn" id="startBtn">LAUNCH MISSION</button>
<button class="btn" id="shopBtnStart" style="background: linear-gradient(135deg, #ffaa00 0%, #ff6600 100%); margin-top: 10px;">⬆️ UPGRADES</button>
</div>
<div id="gameOver">
<h1>💥 SHIP DESTROYED 💥</h1>
<p id="finalStats"></p>
<p id="survivalTime"></p>
<p id="creditsEarned" style="color: #ffaa00; font-size: 24px; margin-top: 15px;"></p>
<button class="btn" id="upgradeBtn">⬆️ UPGRADES</button>
<button class="btn" id="restartBtn">RETRY MISSION</button>
</div>
<div id="upgradeShop">
<h2>🛠️ UPGRADE SHOP 🛠️</h2>
<div class="credits-display">Credits: <span id="shopCredits">0</span> 💰</div>
<div class="upgrade-container">
<div class="upgrade-card">
<h3>🛡️ SHIELD UPGRADE</h3>
<p>Increase maximum hearts! Start each mission with more lives!</p>
<div class="upgrade-level">
<span>Level:</span>
<div class="level-bar">
<div class="level-fill" id="shieldLevelBar" style="width: 20%"></div>
</div>
<span id="shieldLevelText">1/5</span>
</div>
<p style="font-size: 16px; color: #ffaa00;">Current Max: <span id="currentMaxHealth">3 Hearts</span></p>
<button class="upgrade-btn" id="upgradeShieldBtn">
UPGRADE (500 Credits)
</button>
</div>
<div class="upgrade-card">
<h3>🔫 GUN UPGRADE</h3>
<p>Upgrade your weapons! Faster fire rate and more powerful shots!</p>
<div class="upgrade-level">
<span>Level:</span>
<div class="level-bar">
<div class="level-fill" id="gunLevelBar" style="width: 20%"></div>
</div>
<span id="gunLevelText">1/5</span>
</div>
<p style="font-size: 16px; color: #ffaa00;">Fire Rate: <span id="currentFireRate">Normal</span></p>
<button class="upgrade-btn" id="upgradeGunBtn">
UPGRADE (300 Credits)
</button>
</div>
</div>
<button class="close-shop-btn" id="closeShopBtn">CLOSE SHOP</button>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Game state
let gameRunning = false;
let score = 0;
let survivalTime = 0;
let health = 100;
let meteors = [];
let powerups = [];
let particles = [];
let confetti = [];
let stars = [];
let highScore = localStorage.getItem('meteorDodgeHighScore') || 0;
let gameStartTime = 0;
let difficulty = 1;
let bullets = [];
let canShoot = true;
let shootCooldown = 200; // milliseconds between shots
let engineParticles = []; // Engine trail particles
let shipDebris = []; // Ship pieces when destroyed
let playerDestroyed = false;
let ejectedAstronaut = null; // The pilot ejecting from ship
let feedbackMessages = []; // Cool action feedback
let consecutiveKills = 0;
let lastKillTime = 0;
// Minigame system
let inMinigame = false;
let minigameType = null;
let minigameStartTime = 0;
let minigameScore = 0;
let minigameTimeLimit = 50; // 50 seconds for minigame
let normalGameState = {}; // Store normal game state
let minigameTriggered = false; // Prevent multiple triggers
// Audio context for sound effects
let audioContext = null;
// Initialize audio on first user interaction
function initAudio() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
}
let screenShake = 0;
let shakeIntensity = 0;
// Weapon system
let activeWeapon = 'normal';
let weaponTimeLeft = 0;
const weaponDuration = 15000; // 15 seconds per weapon powerup
// Shield system
let shieldActive = false;
let shieldTimeLeft = 0;
const shieldDuration = 10000; // 10 seconds of invincibility
// Heart system (replaces health bar)
let hearts = 3;
let maxHearts = 3;
// Upgrade system
let credits = parseInt(localStorage.getItem('meteorCredits')) || 0;
let shieldLevel = parseInt(localStorage.getItem('shieldLevel')) || 1;
let gunLevel = parseInt(localStorage.getItem('gunLevel')) || 1;
// Shield level now adds extra hearts
maxHearts = 3 + (shieldLevel - 1); // +1 heart per shield level
const upgradeCosts = {
shield: [0, 500, 1000, 2000, 3500],
gun: [0, 300, 750, 1500, 2500]
};
// Player ship
const player = {
x: 0,
y: 0,
width: 50,
height: 50,
targetX: 0,
targetY: 0,
speed: 0.15
};
// Create starfield
function createStars() {
stars = [];
for (let i = 0; i < 150; i++) {
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
size: Math.random() * 2 + 0.5,
speed: Math.random() * 0.5 + 0.2
});
}
}
createStars();
class Meteor {
constructor() {
this.x = Math.random() * canvas.width;
this.y = -50;
this.size = Math.random() * 40 + 30;
this.speed = (Math.random() * 2 + 2) * difficulty;
this.rotation = Math.random() * Math.PI * 2;
this.rotationSpeed = (Math.random() - 0.5) * 0.1;
this.type = Math.random() > 0.7 ? 'large' : 'normal';
if (this.type === 'large') {
this.size *= 1.5;
this.speed *= 0.7;
}
}
update() {
this.y += this.speed;
this.rotation += this.rotationSpeed;
return this.y < canvas.height + 100;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
// Meteor gradient
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size);
gradient.addColorStop(0, '#ff6600');
gradient.addColorStop(0.4, '#cc3300');
gradient.addColorStop(1, '#661100');
ctx.fillStyle = gradient;
ctx.beginPath();
// Irregular shape
const points = 8;
for (let i = 0; i < points; i++) {
const angle = (Math.PI * 2 / points) * i;
const radius = this.size * (0.8 + Math.random() * 0.4);
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.fill();
// Glowing edge
ctx.strokeStyle = '#ff9933';
ctx.lineWidth = 2;
ctx.stroke();
// Hot spots
ctx.fillStyle = '#ffaa00';
for (let i = 0; i < 3; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * this.size * 0.5;
ctx.beginPath();
ctx.arc(Math.cos(angle) * dist, Math.sin(angle) * dist, 3, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
checkCollision(px, py, pw, ph) {
const dist = Math.sqrt((this.x - px) ** 2 + (this.y - py) ** 2);
return dist < (this.size + Math.min(pw, ph) / 2);
}
}
class Asteroid {
constructor() {
this.x = Math.random() * canvas.width;
this.y = -60;
this.size = Math.random() * 25 + 35;
this.speed = (Math.random() * 1.5 + 3) * difficulty;
this.rotation = Math.random() * Math.PI * 2;
this.rotationSpeed = (Math.random() - 0.5) * 0.15;
}
update() {
this.y += this.speed;
this.rotation += this.rotationSpeed;
return this.y < canvas.height + 100;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
// Gray rocky asteroid
ctx.fillStyle = '#8B8680';
ctx.strokeStyle = '#696969';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (Math.PI * 2 / 6) * i;
const radius = this.size * (0.7 + Math.random() * 0.3);
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
// Craters
ctx.fillStyle = '#696969';
for (let i = 0; i < 3; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * this.size * 0.4;
ctx.beginPath();
ctx.arc(Math.cos(angle) * dist, Math.sin(angle) * dist, 4, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
checkCollision(px, py, pw, ph) {
const dist = Math.sqrt((this.x - px) ** 2 + (this.y - py) ** 2);
return dist < (this.size + Math.min(pw, ph) / 2);
}
}
class SpaceDebris {
constructor() {
// Spawn from sides sometimes
if (Math.random() > 0.5) {
this.x = Math.random() > 0.5 ? -30 : canvas.width + 30;
this.y = Math.random() * canvas.height * 0.6;
this.speedX = this.x < 0 ? 3 : -3;
this.speedY = (Math.random() * 2 + 1) * difficulty;
} else {
this.x = Math.random() * canvas.width;
this.y = -40;
this.speedX = (Math.random() - 0.5) * 4;
this.speedY = (Math.random() * 2 + 2.5) * difficulty;
}
this.size = Math.random() * 20 + 20;
this.rotation = Math.random() * Math.PI * 2;
this.rotationSpeed = (Math.random() - 0.5) * 0.2;
this.type = Math.random() > 0.5 ? 'metal' : 'panel';
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.rotation += this.rotationSpeed;
return this.y < canvas.height + 60 && this.x > -60 && this.x < canvas.width + 60;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
if (this.type === 'metal') {
// Metal debris
ctx.fillStyle = '#C0C0C0';
ctx.strokeStyle = '#888';
ctx.lineWidth = 2;
ctx.fillRect(-this.size/2, -this.size/2, this.size, this.size);
ctx.strokeRect(-this.size/2, -this.size/2, this.size, this.size);
// Rivets
ctx.fillStyle = '#666';
ctx.beginPath();
ctx.arc(-this.size/3, -this.size/3, 2, 0, Math.PI * 2);
ctx.arc(this.size/3, -this.size/3, 2, 0, Math.PI * 2);
ctx.arc(-this.size/3, this.size/3, 2, 0, Math.PI * 2);
ctx.arc(this.size/3, this.size/3, 2, 0, Math.PI * 2);
ctx.fill();
} else {
// Solar panel
ctx.fillStyle = '#1a3d5c';
ctx.strokeStyle = '#4a7ba7';
ctx.lineWidth = 2;
ctx.fillRect(-this.size, -this.size/3, this.size * 2, this.size * 0.6);
ctx.strokeRect(-this.size, -this.size/3, this.size * 2, this.size * 0.6);
// Grid lines
ctx.strokeStyle = '#6a9bc7';
ctx.lineWidth = 1;
for (let i = -this.size; i < this.size; i += 10) {
ctx.beginPath();
ctx.moveTo(i, -this.size/3);
ctx.lineTo(i, this.size/3);
ctx.stroke();
}
}
ctx.restore();
}
checkCollision(px, py, pw, ph) {
const dist = Math.sqrt((this.x - px) ** 2 + (this.y - py) ** 2);
return dist < (this.size + Math.min(pw, ph) / 2);
}
}
class IceCrystal {
constructor() {
this.x = Math.random() * canvas.width;
this.y = -50;
this.size = Math.random() * 20 + 25;
this.speed = (Math.random() * 1 + 2) * difficulty;
this.rotation = Math.random() * Math.PI * 2;
this.rotationSpeed = (Math.random() - 0.5) * 0.12;
this.shimmer = 0;
}
update() {
this.y += this.speed;
this.rotation += this.rotationSpeed;
this.shimmer += 0.1;
return this.y < canvas.height + 80;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
// Ice crystal
const shimmerAlpha = 0.5 + Math.sin(this.shimmer) * 0.3;
ctx.fillStyle = `rgba(173, 216, 230, ${shimmerAlpha})`;
ctx.strokeStyle = `rgba(135, 206, 250, ${shimmerAlpha})`;
ctx.lineWidth = 2;
ctx.shadowBlur = 15;
ctx.shadowColor = '#00ffff';
// Star shape
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i;
const outerX = Math.cos(angle) * this.size;
const outerY = Math.sin(angle) * this.size;
const innerX = Math.cos(angle + Math.PI / 6) * (this.size * 0.5);
const innerY = Math.sin(angle + Math.PI / 6) * (this.size * 0.5);
if (i === 0) {
ctx.moveTo(outerX, outerY);
} else {
ctx.lineTo(outerX, outerY);
}
ctx.lineTo(innerX, innerY);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
ctx.restore();
}
checkCollision(px, py, pw, ph) {
const dist = Math.sqrt((this.x - px) ** 2 + (this.y - py) ** 2);
return dist < (this.size + Math.min(pw, ph) / 2);
}
}
class RocketShip {
constructor() {
// Spawn from left or right side
this.fromLeft = Math.random() > 0.5;
this.x = this.fromLeft ? -80 : canvas.width + 80;
this.y = Math.random() * (canvas.height * 0.7) + 50;
this.width = 60;
this.height = 30;
this.speedX = (this.fromLeft ? 4 : -4) * difficulty;
this.speedY = (Math.random() - 0.5) * 1;
this.engineFlicker = 0;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.engineFlicker += 0.3;
// Keep in vertical bounds
if (this.y < 50) this.y = 50;
if (this.y > canvas.height - 100) this.y = canvas.height - 100;
return this.fromLeft ? this.x < canvas.width + 100 : this.x > -100;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
// Flip if coming from right
if (!this.fromLeft) {
ctx.scale(-1, 1);
}
// Rocket body
ctx.fillStyle = '#e74c3c';
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 2;
// Main body
ctx.fillRect(-25, -12, 45, 24);
ctx.strokeRect(-25, -12, 45, 24);
// Nose cone
ctx.fillStyle = '#bdc3c7';
ctx.beginPath();
ctx.moveTo(20, -12);
ctx.lineTo(35, 0);
ctx.lineTo(20, 12);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Window
ctx.fillStyle = '#3498db';
ctx.shadowBlur = 10;
ctx.shadowColor = '#3498db';
ctx.beginPath();
ctx.arc(5, 0, 6, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Wings
ctx.fillStyle = '#95a5a6';
ctx.strokeStyle = '#7f8c8d';
ctx.beginPath();
ctx.moveTo(-15, -12);
ctx.lineTo(-15, -20);
ctx.lineTo(-5, -12);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-15, 12);
ctx.lineTo(-15, 20);
ctx.lineTo(-5, 12);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Engine flame
const flameSize = 15 + Math.sin(this.engineFlicker) * 5;
ctx.fillStyle = '#ff6600';
ctx.shadowBlur = 20;
ctx.shadowColor = '#ff6600';
ctx.beginPath();
ctx.moveTo(-25, -8);
ctx.lineTo(-25 - flameSize, 0);
ctx.lineTo(-25, 8);
ctx.closePath();
ctx.fill();
// Inner flame
ctx.fillStyle = '#ffaa00';
ctx.beginPath();
ctx.moveTo(-25, -5);
ctx.lineTo(-25 - flameSize * 0.7, 0);
ctx.lineTo(-25, 5);
ctx.closePath();
ctx.fill();
ctx.shadowBlur = 0;
ctx.restore();
}
checkCollision(px, py, pw, ph) {
return px + pw/2 > this.x - this.width/2 &&
px - pw/2 < this.x + this.width/2 &&
py + ph/2 > this.y - this.height/2 &&
py - ph/2 < this.y + this.height/2;
}
}
class SpaceSurfer {
constructor() {
// Spawn from left or right, moving diagonally
this.fromLeft = Math.random() > 0.5;
this.x = this.fromLeft ? -60 : canvas.width + 60;
this.y = Math.random() * (canvas.height * 0.5);
this.width = 40;
this.height = 50;
this.speedX = (this.fromLeft ? 3.5 : -3.5) * difficulty;
this.speedY = Math.random() * 1.5 + 1;
this.wobble = 0;
this.sparkle = 0;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.wobble += 0.1;
this.sparkle += 0.2;
return (this.fromLeft ? this.x < canvas.width + 80 : this.x > -80) &&
this.y < canvas.height + 80;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
// Flip if coming from right
if (!this.fromLeft) {
ctx.scale(-1, 1);
}
const wobbleOffset = Math.sin(this.wobble) * 3;
// Surfboard
ctx.fillStyle = '#ff1493';
ctx.strokeStyle = '#c71585';
ctx.lineWidth = 2;
ctx.shadowBlur = 15;
ctx.shadowColor = '#ff1493';
ctx.beginPath();
ctx.ellipse(wobbleOffset, 15, 25, 8, Math.PI / 12, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
// Surfboard sparkles
if (Math.sin(this.sparkle) > 0.5) {
ctx.fillStyle = '#ffffff';
ctx.shadowBlur = 10;
ctx.shadowColor = '#ffffff';
ctx.beginPath();
ctx.arc(wobbleOffset - 10, 15, 2, 0, Math.PI * 2);
ctx.arc(wobbleOffset + 10, 15, 2, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
// Surfer body
ctx.fillStyle = '#ffa500';
ctx.fillRect(-5 + wobbleOffset, -5, 10, 15);
// Head
ctx.fillStyle = '#ffdbac';
ctx.beginPath();
ctx.arc(wobbleOffset, -12, 6, 0, Math.PI * 2);
ctx.fill();
// Helmet
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.shadowBlur = 10;
ctx.shadowColor = '#00ffff';
ctx.beginPath();
ctx.arc(wobbleOffset, -12, 7, Math.PI, 0);
ctx.stroke();
ctx.shadowBlur = 0;
// Arms
ctx.strokeStyle = '#ffa500';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-5 + wobbleOffset, 0);
ctx.lineTo(-12 + wobbleOffset, -5);
ctx.moveTo(5 + wobbleOffset, 0);
ctx.lineTo(12 + wobbleOffset, 5);
ctx.stroke();
// Energy trail from board
ctx.fillStyle = 'rgba(0, 255, 255, 0.3)';
ctx.shadowBlur = 20;
ctx.shadowColor = '#00ffff';
for (let i = 0; i < 3; i++) {
ctx.beginPath();
ctx.arc(-30 - i * 10 + wobbleOffset, 15, 4 - i, 0, Math.PI * 2);
ctx.fill();
}
ctx.shadowBlur = 0;
ctx.restore();
}
checkCollision(px, py, pw, ph) {
return px + pw/2 > this.x - this.width/2 &&
px - pw/2 < this.x + this.width/2 &&
py + ph/2 > this.y - this.height/2 &&
py - ph/2 < this.y + this.height/2;
}
}
class Satellite {
constructor() {
this.x = Math.random() * canvas.width;
this.y = -60;
this.width = 50;
this.height = 30;
this.speed = (Math.random() * 1.5 + 1.5) * difficulty;
this.rotation = 0;
this.panelRotation = 0;
this.panelRotationSpeed = 0.02;
this.blink = 0;
}
update() {
this.y += this.speed;
this.rotation += 0.01;
this.panelRotation += this.panelRotationSpeed;
this.blink += 0.1;
return this.y < canvas.height + 80;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
// Solar panels (rotating)
ctx.save();
ctx.rotate(this.panelRotation);
// Left panel
ctx.fillStyle = '#1a3d5c';
ctx.strokeStyle = '#4a7ba7';
ctx.lineWidth = 2;
ctx.fillRect(-45, -8, 25, 16);
ctx.strokeRect(-45, -8, 25, 16);
// Panel grid
ctx.strokeStyle = '#6a9bc7';
ctx.lineWidth = 1;
for (let i = -40; i < -20; i += 5) {
ctx.beginPath();
ctx.moveTo(i, -8);
ctx.lineTo(i, 8);
ctx.stroke();
}
// Right panel
ctx.fillStyle = '#1a3d5c';
ctx.strokeStyle = '#4a7ba7';
ctx.lineWidth = 2;
ctx.fillRect(20, -8, 25, 16);
ctx.strokeRect(20, -8, 25, 16);
// Panel grid
ctx.strokeStyle = '#6a9bc7';
ctx.lineWidth = 1;
for (let i = 25; i < 45; i += 5) {
ctx.beginPath();
ctx.moveTo(i, -8);
ctx.lineTo(i, 8);
ctx.stroke();
}
ctx.restore();
// Main satellite body
ctx.fillStyle = '#bdc3c7';
ctx.strokeStyle = '#95a5a6';
ctx.lineWidth = 2;
ctx.fillRect(-15, -10, 30, 20);
ctx.strokeRect(-15, -10, 30, 20);
// Body details
ctx.fillStyle = '#7f8c8d';
ctx.fillRect(-12, -7, 24, 3);
ctx.fillRect(-12, 4, 24, 3);
// Antenna
ctx.strokeStyle = '#7f8c8d';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, -10);
ctx.lineTo(0, -18);
ctx.stroke();
// Antenna dish
ctx.fillStyle = '#ecf0f1';
ctx.strokeStyle = '#bdc3c7';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.ellipse(0, -18, 5, 3, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Blinking lights
const blinkOn = Math.sin(this.blink) > 0;
if (blinkOn) {
ctx.fillStyle = '#ff0000';
ctx.shadowBlur = 10;
ctx.shadowColor = '#ff0000';
ctx.beginPath();
ctx.arc(-10, 0, 2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#00ff00';
ctx.shadowColor = '#00ff00';
ctx.beginPath();
ctx.arc(10, 0, 2, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
// Camera lens
ctx.fillStyle = '#2c3e50';
ctx.strokeStyle = '#34495e';
ctx.lineWidth = 2;
ctx.shadowBlur = 8;
ctx.shadowColor = '#3498db';
ctx.beginPath();
ctx.arc(0, 0, 4, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
ctx.restore();
}
checkCollision(px, py, pw, ph) {
// Include solar panels in collision
const totalWidth = 90; // Including panels
return px + pw/2 > this.x - totalWidth/2 &&
px - pw/2 < this.x + totalWidth/2 &&
py + ph/2 > this.y - this.height/2 &&
py - ph/2 < this.y + this.height/2;
}
}
class Powerup {
constructor() {
this.x = Math.random() * (canvas.width - 80) + 40;
this.y = -50;
this.size = 35; // Increased from 30 to 35
this.speed = 2.5; // Slightly slower so easier to catch
this.rotation = 0;
this.pulse = 0;
this.type = 'health'; // Mark as health powerup
}
update() {
this.y += this.speed;
this.rotation += 0.08;
this.pulse += 0.15;
return this.y < canvas.height + 60;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
// Pulsing glow effect
const glowSize = this.size + Math.sin(this.pulse) * 5;
// Outer glow
ctx.strokeStyle = '#00ffff';
ctx.lineWidth = 5;
ctx.shadowBlur = 25;
ctx.shadowColor = '#00ffff';
ctx.beginPath();
ctx.arc(0, 0, glowSize, 0, Math.PI * 2);
ctx.stroke();
// Inner circle
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 3;
ctx.shadowBlur = 20;
ctx.beginPath();
ctx.arc(0, 0, this.size - 5, 0, Math.PI * 2);
ctx.stroke();
// Plus sign (shield/health symbol)
ctx.strokeStyle = '#00ffff';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(-18, 0);
ctx.lineTo(18, 0);
ctx.moveTo(0, -18);
ctx.lineTo(0, 18);
ctx.stroke();
ctx.shadowBlur = 0;
ctx.restore();
}
checkCollision(px, py, pw, ph) {
const dist = Math.sqrt((this.x - px) ** 2 + (this.y - py) ** 2);
return dist < (this.size + Math.min(pw, ph) / 2);
}
}
class WeaponPowerup {
constructor() {
this.x = Math.random() * (canvas.width - 80) + 40;
this.y = -50;
this.size = 40;
this.speed = 2.5;
this.rotation = 0;
this.pulse = 0;
// Different weapon types
const types = ['spread', 'laser', 'rapid', 'missile'];
this.weaponType = types[Math.floor(Math.random() * types.length)];
// Colors and symbols for each weapon
this.config = {
spread: { color: '#ff6600', symbol: '⚡', name: 'SPREAD SHOT' },
laser: { color: '#00ff00', symbol: '━', name: 'LASER BEAM' },
rapid: { color: '#ffff00', symbol: '※', name: 'RAPID FIRE' },
missile: { color: '#ff00ff', symbol: '◈', name: 'MISSILES' }
};
}
update() {
this.y += this.speed;
this.rotation += 0.1;
this.pulse += 0.2;
return this.y < canvas.height + 60;
}
draw() {
const config = this.config[this.weaponType];
ctx.save();
ctx.translate(this.x, this.y);
// Pulsing glow
const glowSize = this.size + Math.sin(this.pulse) * 8;
ctx.strokeStyle = config.color;
ctx.lineWidth = 6;
ctx.shadowBlur = 30;
ctx.shadowColor = config.color;
// Outer hexagon
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i + this.rotation;
const x = Math.cos(angle) * glowSize;
const y = Math.sin(angle) * glowSize;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.stroke();
// Inner shape
ctx.fillStyle = config.color;
ctx.globalAlpha = 0.3;
ctx.fill();
ctx.globalAlpha = 1;
// Weapon symbol
ctx.shadowBlur = 15;
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 30px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(config.symbol, 0, 0);
ctx.shadowBlur = 0;
ctx.restore();
}
checkCollision(px, py, pw, ph) {
const dist = Math.sqrt((this.x - px) ** 2 + (this.y - py) ** 2);
return dist < (this.size + Math.min(pw, ph) / 2);
}
}
class ShieldPowerup {
constructor() {
this.x = Math.random() * (canvas.width - 80) + 40;
this.y = -50;
this.size = 40;
this.speed = 2.5;
this.rotation = 0;
this.pulse = 0;
this.type = 'shield';
}
update() {
this.y += this.speed;
this.rotation += 0.12;
this.pulse += 0.25;
return this.y < canvas.height + 60;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
// Pulsing outer glow
const glowSize = this.size + Math.sin(this.pulse) * 10;
// Shield shape - diamond
ctx.strokeStyle = '#4da6ff';
ctx.lineWidth = 6;
ctx.shadowBlur = 35;
ctx.shadowColor = '#4da6ff';
ctx.beginPath();
ctx.moveTo(0, -glowSize);
ctx.lineTo(glowSize * 0.7, 0);
ctx.lineTo(0, glowSize);
ctx.lineTo(-glowSize * 0.7, 0);
ctx.closePath();
ctx.stroke();
// Inner fill
ctx.fillStyle = '#4da6ff';
ctx.globalAlpha = 0.4;
ctx.fill();
ctx.globalAlpha = 1;
// Shield icon
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 4;
ctx.shadowBlur = 20;
ctx.shadowColor = '#ffffff';
// Shield emblem
ctx.beginPath();
ctx.moveTo(0, -this.size * 0.5);
ctx.lineTo(this.size * 0.4, 0);
ctx.lineTo(0, this.size * 0.5);
ctx.lineTo(-this.size * 0.4, 0);
ctx.closePath();
ctx.stroke();
// Center dot
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(0, 0, 5, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.restore();
}
checkCollision(px, py, pw, ph) {
const dist = Math.sqrt((this.x - px) ** 2 + (this.y - py) ** 2);
return dist < (this.size + Math.min(pw, ph) / 2);
}
}
class Bullet {
constructor(x, y, weaponType = 'normal', angle = 0) {
this.x = x;
this.y = y;
this.width = 6;
this.height = 20;
this.speed = 12;
this.weaponType = weaponType;
this.angle = angle;
// Weapon-specific properties
if (weaponType === 'laser') {
this.width = 8;
this.speed = 20;
}
if (weaponType === 'missile') {
this.width = 10;
this.height = 25;
this.speed = 10;
}
}
update() {
if (this.weaponType === 'spread') {
this.x += Math.sin(this.angle) * this.speed;
this.y -= Math.cos(this.angle) * this.speed;
} else {
this.y -= this.speed;
}
return this.y > -50;
}
draw() {
ctx.save();
if (this.weaponType === 'laser') {
// Green laser beam
ctx.fillStyle = 'rgba(0, 255, 0, 0.5)';
ctx.fillRect(this.x - this.width/2, this.y, this.width, this.height + 15);
ctx.fillStyle = '#00ff00';
ctx.shadowBlur = 20;
ctx.shadowColor = '#00ff00';
ctx.fillRect(this.x - this.width/2, this.y, this.width, this.height);
} else if (this.weaponType === 'rapid') {
// Yellow rapid fire
ctx.fillStyle = 'rgba(255, 255, 0, 0.4)';
ctx.fillRect(this.x - this.width/2, this.y, this.width, this.height + 8);
ctx.fillStyle = '#ffff00';
ctx.shadowBlur = 15;
ctx.shadowColor = '#ffff00';
ctx.fillRect(this.x - this.width/2, this.y, this.width, this.height);
} else if (this.weaponType === 'missile') {
// Purple missiles
ctx.translate(this.x, this.y + this.height/2);
// Missile body
ctx.fillStyle = '#ff00ff';
ctx.shadowBlur = 20;
ctx.shadowColor = '#ff00ff';
ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height);
// Missile tip
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.moveTo(0, -this.height/2 - 5);
ctx.lineTo(-this.width/2, -this.height/2);
ctx.lineTo(this.width/2, -this.height/2);
ctx.closePath();
ctx.fill();
// Flame trail
ctx.fillStyle = '#ff6600';
ctx.beginPath();
ctx.moveTo(-3, this.height/2);
ctx.lineTo(0, this.height/2 + 8);
ctx.lineTo(3, this.height/2);
ctx.closePath();
ctx.fill();
} else if (this.weaponType === 'spread') {
// Orange spread shot
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.fillStyle = 'rgba(255, 102, 0, 0.4)';
ctx.fillRect(-this.width/2, 0, this.width, this.height + 8);
ctx.fillStyle = '#ff6600';
ctx.shadowBlur = 15;
ctx.shadowColor = '#ff6600';
ctx.fillRect(-this.width/2, 0, this.width, this.height);
} else {
// Normal yellow bullet
ctx.fillStyle = 'rgba(255, 255, 0, 0.5)';
ctx.fillRect(this.x - this.width/2, this.y, this.width, this.height + 10);
ctx.fillStyle = '#ffff00';
ctx.shadowBlur = 15;
ctx.shadowColor = '#ffff00';
ctx.fillRect(this.x - this.width/2, this.y, this.width, this.height);
}
ctx.shadowBlur = 0;
ctx.restore();
// Bright tip
ctx.fillStyle = '#ffffff';
if (this.weaponType !== 'spread' && this.weaponType !== 'missile') {
ctx.fillRect(this.x - this.width/2, this.y, this.width, 5);
}
}
checkMeteorHit(obstacle) {
// For obstacles with size property (circular collision)
if (obstacle.size) {
const dist = Math.sqrt((this.x - obstacle.x) ** 2 + (this.y - obstacle.y) ** 2);
return dist < obstacle.size;
}
// For obstacles with width/height (rectangular collision)
if (obstacle.width && obstacle.height) {
return this.x > obstacle.x - obstacle.width/2 &&
this.x < obstacle.x + obstacle.width/2 &&
this.y > obstacle.y - obstacle.height/2 &&
this.y < obstacle.y + obstacle.height/2;
}
return false;
}
}
class Particle {
constructor(x, y, color) {
this.x = x;
this.y = y;
this.speedX = (Math.random() - 0.5) * 12;
this.speedY = (Math.random() - 0.5) * 12;
this.life = 1;
this.decay = Math.random() * 0.015 + 0.015;
this.size = Math.random() * 10 + 5;
this.color = color;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.speedY += 0.2;
this.life -= this.decay;
return this.life > 0;
}
draw() {
ctx.save();
ctx.globalAlpha = this.life;
ctx.fillStyle = this.color;
ctx.shadowBlur = 15;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
class Confetti {
constructor(x, y) {
this.x = x;
this.y = y;
this.speedX = (Math.random() - 0.5) * 8;
this.speedY = Math.random() * -8 - 4; // Shoot upward
this.gravity = 0.3;
this.rotation = Math.random() * Math.PI * 2;
this.rotationSpeed = (Math.random() - 0.5) * 0.3;
this.life = 1;
this.decay = 0.008;
this.width = Math.random() * 10 + 5;
this.height = Math.random() * 15 + 8;
const colors = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff', '#ff6600', '#ff1493'];
this.color = colors[Math.floor(Math.random() * colors.length)];
}
update() {
this.speedY += this.gravity;
this.x += this.speedX;
this.y += this.speedY;
this.rotation += this.rotationSpeed;
this.life -= this.decay;
return this.life > 0 && this.y < canvas.height + 50;
}
draw() {
ctx.save();
ctx.globalAlpha = this.life;
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.fillStyle = this.color;
ctx.fillRect(-this.width/2, -this.height/2, this.width, this.height);
ctx.restore();
}
}
class EngineParticle {
constructor(x, y) {
this.x = x;
this.y = y;
this.speedX = (Math.random() - 0.5) * 2;
this.speedY = Math.random() * 2 + 1; // Move downward/backward
this.life = 1;
this.decay = Math.random() * 0.03 + 0.02;
this.size = Math.random() * 6 + 3;
// Random fire colors
const colors = ['#ff6600', '#ff9900', '#ffaa00', '#ff3300', '#ffcc00'];
this.color = colors[Math.floor(Math.random() * colors.length)];
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.life -= this.decay;
this.size *= 0.97; // Shrink over time
return this.life > 0;
}
draw() {
ctx.save();
ctx.globalAlpha = this.life;
// Outer glow
ctx.fillStyle = this.color;
ctx.shadowBlur = 20;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
// Inner bright core
ctx.fillStyle = '#ffffff';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size * 0.4, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
class ShipDebris {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type; // 'nose', 'body', 'cockpit', 'fin', 'engine', 'stripe'
this.speedX = (Math.random() - 0.5) * 8;
this.speedY = (Math.random() - 0.5) * 8;
this.rotation = Math.random() * Math.PI * 2;
this.rotationSpeed = (Math.random() - 0.5) * 0.3;
this.life = 1;
this.decay = 0.015;
this.size = 25; // Increased from 15 to 25
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.speedY += 0.1; // Gravity
this.rotation += this.rotationSpeed;
this.life -= this.decay;
return this.life > 0;
}
draw() {
ctx.save();
ctx.globalAlpha = this.life;
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
switch(this.type) {
case 'nose':
// Red nose cone piece
ctx.fillStyle = '#e74c3c';
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, -this.size);
ctx.lineTo(-this.size/2, this.size/2);
ctx.lineTo(this.size/2, this.size/2);
ctx.closePath();
ctx.fill();
ctx.stroke();
break;
case 'body':
// Red body panel
ctx.fillStyle = '#e74c3c';
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 2;
ctx.fillRect(-this.size/2, -this.size/2, this.size, this.size);
ctx.strokeRect(-this.size/2, -this.size/2, this.size, this.size);
break;
case 'cockpit':
// Blue glowing cockpit window
ctx.fillStyle = '#3498db';
ctx.strokeStyle = '#2980b9';
ctx.lineWidth = 2;
ctx.shadowBlur = 15;
ctx.shadowColor = '#3498db';
ctx.beginPath();
ctx.arc(0, 0, this.size/2, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
break;
case 'fin':
// Gray fin piece
ctx.fillStyle = '#95a5a6';
ctx.strokeStyle = '#7f8c8d';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(-this.size, this.size/2);
ctx.lineTo(-this.size, this.size);
ctx.lineTo(0, this.size/2);
ctx.closePath();
ctx.fill();
ctx.stroke();
break;
case 'engine':
// Dark engine nozzle
ctx.fillStyle = '#34495e';
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 2;
ctx.fillRect(-this.size/3, -this.size/2, this.size * 0.6, this.size);
ctx.strokeRect(-this.size/3, -this.size/2, this.size * 0.6, this.size);
// Engine glow
ctx.fillStyle = '#ff6600';
ctx.shadowBlur = 10;
ctx.shadowColor = '#ff6600';
ctx.beginPath();
ctx.arc(0, this.size/3, 3, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
break;
case 'stripe':
// White accent stripe
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#ecf0f1';
ctx.lineWidth = 1;
ctx.fillRect(-this.size/2, -2, this.size, 4);
ctx.strokeRect(-this.size/2, -2, this.size, 4);
break;
}
ctx.restore();
}
}
class FeedbackMessage {
constructor(x, y, text, color, size = 36) {
this.x = x;
this.y = y;
this.text = text;
this.color = color;
this.size = size;
this.life = 1;
this.decay = 0.02;
this.speedY = -2; // Float upward
this.scale = 0.5;
this.targetScale = 1.2;
}
update() {
this.y += this.speedY;
this.life -= this.decay;
// Grow then shrink
if (this.scale < this.targetScale) {
this.scale += 0.1;
}
return this.life > 0;
}
draw() {
ctx.save();
ctx.globalAlpha = this.life;
ctx.translate(this.x, this.y);
ctx.scale(this.scale, this.scale);
ctx.font = `bold ${this.size}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Outline
ctx.strokeStyle = '#000';
ctx.lineWidth = 4;
ctx.strokeText(this.text, 0, 0);
// Fill
ctx.fillStyle = this.color;
ctx.shadowBlur = 20;
ctx.shadowColor = this.color;
ctx.fillText(this.text, 0, 0);
ctx.restore();
}
}
class EjectedAstronaut {
constructor(x, y) {
this.x = x;
this.y = y;
this.speedX = (Math.random() - 0.5) * 3; // Small sideways motion
this.speedY = 1; // Start falling immediately (positive = down)
this.rotation = Math.random() * Math.PI * 2;
this.rotationSpeed = (Math.random() - 0.5) * 0.15;
this.life = 1;
this.decay = 0.005; // Slow decay so visible longer
this.armWave = 0;
this.parachuteOpen = false;
this.parachuteTimer = 0;
}
update() {
this.parachuteTimer++;
// Open parachute immediately (after 10 frames)
if (this.parachuteTimer > 10) {
this.parachuteOpen = true;
}
this.x += this.speedX;
this.y += this.speedY;
if (this.parachuteOpen) {
// Gentle fall with parachute (faster than before)
this.speedY += 0.015; // Gentle gravity
this.speedY = Math.min(this.speedY, 2); // Moderate fall speed
this.speedX *= 0.96; // Slow down horizontal movement
this.rotation = 0; // Upright when parachute is open
this.rotationSpeed = 0;
} else {
// Brief tumble before parachute opens
this.speedY += 0.15;
this.rotation += this.rotationSpeed;
}
this.life -= this.decay;
this.armWave += 0.2;
return this.life > 0 && this.y < canvas.height + 50;
}
draw() {
ctx.save();
ctx.globalAlpha = this.life;
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
// Parachute (if open)
if (this.parachuteOpen) {
const swayOffset = Math.sin(this.armWave * 0.5) * 3;
// Parachute canopy
ctx.fillStyle = '#ff6600';
ctx.strokeStyle = '#cc5500';
ctx.lineWidth = 2;
ctx.shadowBlur = 10;
ctx.shadowColor = '#ff6600';
// Main canopy (dome shape)
ctx.beginPath();
ctx.moveTo(-25 + swayOffset, -25);
ctx.quadraticCurveTo(0 + swayOffset, -40, 25 + swayOffset, -25);
ctx.lineTo(20 + swayOffset, -20);
ctx.quadraticCurveTo(0 + swayOffset, -30, -20 + swayOffset, -20);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
// Parachute panels
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1;
for (let i = -1; i <= 1; i++) {
ctx.beginPath();
ctx.moveTo(i * 10 + swayOffset, -25);
ctx.lineTo(i * 8 + swayOffset, -20);
ctx.stroke();
}
// Parachute strings
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-20 + swayOffset, -20);
ctx.lineTo(-3, -8);
ctx.moveTo(20 + swayOffset, -20);
ctx.lineTo(3, -8);
ctx.moveTo(0 + swayOffset, -20);
ctx.lineTo(0, -8);
ctx.stroke();
}
// Helmet (space suit)
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 2;
ctx.shadowBlur = 10;
ctx.shadowColor = '#00ffff';
ctx.beginPath();
ctx.arc(0, -6, 8, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
// Visor
ctx.fillStyle = '#3498db';
ctx.shadowBlur = 8;
ctx.shadowColor = '#3498db';
ctx.beginPath();
ctx.arc(0, -6, 5, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Visor reflection
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
ctx.beginPath();
ctx.arc(-2, -8, 2, 0, Math.PI * 2);
ctx.fill();
// Body (orange spacesuit)
ctx.fillStyle = '#ff6600';
ctx.strokeStyle = '#cc5500';
ctx.lineWidth = 1.5;
ctx.fillRect(-4, 2, 8, 10);
ctx.strokeRect(-4, 2, 8, 10);
// Life support backpack
ctx.fillStyle = '#555555';
ctx.fillRect(-3, 3, 6, 4);
// Arms (waving)
ctx.strokeStyle = '#ff6600';
ctx.lineWidth = 2;
const armOffset = Math.sin(this.armWave) * 2;
// Left arm
ctx.beginPath();
ctx.moveTo(-4, 4);
ctx.lineTo(-7, 6 + armOffset);
ctx.stroke();
// Right arm
ctx.beginPath();
ctx.moveTo(4, 4);
ctx.lineTo(7, 6 - armOffset);
ctx.stroke();
// Legs
ctx.strokeStyle = '#ff6600';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-2, 12);
ctx.lineTo(-3, 16);
ctx.moveTo(2, 12);
ctx.lineTo(3, 16);
ctx.stroke();
// Boots
ctx.fillStyle = '#333333';
ctx.fillRect(-4, 15, 2, 3);
ctx.fillRect(2, 15, 2, 3);
ctx.restore();
}
}
function createExplosion(x, y, color) {
for (let i = 0; i < 25; i++) {
particles.push(new Particle(x, y, color));
}
}
function playExplosionSound() {
if (!audioContext) return;
const now = audioContext.currentTime;
// Create multiple oscillators for a rich explosion sound
// Low rumble
const rumble = audioContext.createOscillator();
const rumbleGain = audioContext.createGain();
rumble.type = 'sawtooth';
rumble.frequency.setValueAtTime(50, now);
rumble.frequency.exponentialRampToValueAtTime(20, now + 0.5);
rumbleGain.gain.setValueAtTime(0.3, now);
rumbleGain.gain.exponentialRampToValueAtTime(0.01, now + 0.5);
rumble.connect(rumbleGain);
rumbleGain.connect(audioContext.destination);
rumble.start(now);
rumble.stop(now + 0.5);
// Mid explosion crack
const crack = audioContext.createOscillator();
const crackGain = audioContext.createGain();
crack.type = 'square';
crack.frequency.setValueAtTime(200, now);
crack.frequency.exponentialRampToValueAtTime(50, now + 0.3);
crackGain.gain.setValueAtTime(0.4, now);
crackGain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
crack.connect(crackGain);
crackGain.connect(audioContext.destination);
crack.start(now);
crack.stop(now + 0.3);
// High frequency blast
const blast = audioContext.createOscillator();
const blastGain = audioContext.createGain();
blast.type = 'square';
blast.frequency.setValueAtTime(800, now);
blast.frequency.exponentialRampToValueAtTime(100, now + 0.2);
blastGain.gain.setValueAtTime(0.3, now);
blastGain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
blast.connect(blastGain);
blastGain.connect(audioContext.destination);
blast.start(now);
blast.stop(now + 0.2);
// White noise for debris
const bufferSize = audioContext.sampleRate * 0.4;
const noiseBuffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate);
const output = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
const whiteNoise = audioContext.createBufferSource();
const noiseGain = audioContext.createGain();
const noiseFilter = audioContext.createBiquadFilter();
whiteNoise.buffer = noiseBuffer;
noiseFilter.type = 'lowpass';
noiseFilter.frequency.setValueAtTime(2000, now);
noiseFilter.frequency.exponentialRampToValueAtTime(200, now + 0.4);
noiseGain.gain.setValueAtTime(0.2, now);
noiseGain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
whiteNoise.connect(noiseFilter);
noiseFilter.connect(noiseGain);
noiseGain.connect(audioContext.destination);
whiteNoise.start(now);
whiteNoise.stop(now + 0.4);
}
function explodeShip(x, y) {
playerDestroyed = true;
// If in minigame, end it immediately
if (inMinigame) {
setTimeout(() => {
endMinigame(true); // Died in minigame
}, 2000); // Wait 2 seconds to show explosion
}
// Play explosion sound
playExplosionSound();
// Screen shake effect
screenShake = 30;
shakeIntensity = 15;
// Giant BOOM text
feedbackMessages.push(new FeedbackMessage(x, y, '💥 BOOM! 💥', '#ff0000', 72));
// Eject the pilot!
ejectedAstronaut = new EjectedAstronaut(x, y - 10);
// Create realistic ship debris pieces matching the rocket design
shipDebris.push(new ShipDebris(x, y - 15, 'nose')); // Red nose cone
shipDebris.push(new ShipDebris(x - 8, y - 5, 'body')); // Left body panel
shipDebris.push(new ShipDebris(x + 8, y - 5, 'body')); // Right body panel
shipDebris.push(new ShipDebris(x, y + 5, 'body')); // Center body
shipDebris.push(new ShipDebris(x, y - 5, 'cockpit')); // Blue cockpit window
shipDebris.push(new ShipDebris(x - 18, y + 12, 'fin')); // Left fin
shipDebris.push(new ShipDebris(x + 18, y + 12, 'fin')); // Right fin
shipDebris.push(new ShipDebris(x - 7, y + 18, 'engine')); // Left engine nozzle
shipDebris.push(new ShipDebris(x + 7, y + 18, 'engine')); // Right engine nozzle
shipDebris.push(new ShipDebris(x - 5, y, 'stripe')); // White stripe piece
shipDebris.push(new ShipDebris(x + 5, y, 'stripe')); // White stripe piece
// MASSIVE EXPLOSION - Much bigger and more dramatic!
// First wave - Bright orange/yellow fire core (150 particles)
for (let i = 0; i < 150; i++) {
particles.push(new Particle(x, y, '#ff6600'));
particles.push(new Particle(x, y, '#ffaa00'));
particles.push(new Particle(x, y, '#ff8800'));
}
// Second wave - Bright white hot center (100 particles)
for (let i = 0; i < 100; i++) {
particles.push(new Particle(x, y, '#ffffff'));
particles.push(new Particle(x, y, '#ffffcc'));
}
// Third wave - Red/orange outer ring (80 particles)
for (let i = 0; i < 80; i++) {
particles.push(new Particle(x, y, '#ff0000'));
particles.push(new Particle(x, y, '#ff4400'));
}
// Fourth wave - Dark smoke particles (60 particles)
for (let i = 0; i < 60; i++) {
particles.push(new Particle(x, y, '#555555'));
particles.push(new Particle(x, y, '#333333'));
}
// Shockwave ring effect
for (let angle = 0; angle < Math.PI * 2; angle += 0.2) {
const dist = 50;
const px = x + Math.cos(angle) * dist;
const py = y + Math.sin(angle) * dist;
particles.push(new Particle(px, py, '#ffff00'));
particles.push(new Particle(px, py, '#ffffff'));
}
}
function shoot() {
if (!gameRunning || !canShoot) return;
const baseY = player.y - player.height/2;
// Different shooting patterns based on active weapon
switch(activeWeapon) {
case 'spread':
// 5 bullets in a spread pattern
for (let i = -2; i <= 2; i++) {
const angle = i * 0.25; // Spread angle
bullets.push(new Bullet(player.x, baseY, 'spread', angle));
}
break;
case 'laser':
// Single powerful laser
bullets.push(new Bullet(player.x, baseY, 'laser'));
break;
case 'rapid':
// Triple shot
bullets.push(new Bullet(player.x - 10, baseY, 'rapid'));
bullets.push(new Bullet(player.x, baseY, 'rapid'));
bullets.push(new Bullet(player.x + 10, baseY, 'rapid'));
break;
case 'missile':
// Dual homing missiles
bullets.push(new Bullet(player.x - 15, baseY, 'missile'));
bullets.push(new Bullet(player.x + 15, baseY, 'missile'));
break;
default:
// Normal gun with upgrade levels
const bulletCount = gunLevel >= 3 ? 2 : 1;
if (bulletCount === 2) {
bullets.push(new Bullet(player.x - 15, baseY));
bullets.push(new Bullet(player.x + 15, baseY));
} else {
bullets.push(new Bullet(player.x, baseY));
}
}
canShoot = false;
// Weapon affects fire rate
let cooldown;
if (activeWeapon === 'rapid') {
cooldown = 100; // Very fast
} else if (activeWeapon === 'spread') {
cooldown = 400; // Slower for balance
} else if (activeWeapon === 'missile') {
cooldown = 600; // Slowest but powerful
} else {
const cooldowns = [200, 175, 150, 120, 90];
cooldown = cooldowns[gunLevel - 1];
}
setTimeout(() => {
canShoot = true;
}, cooldown);
}
function updateShopDisplay() {
document.getElementById('shopCredits').textContent = credits;
document.getElementById('credits').textContent = credits;
// Shield upgrade
document.getElementById('shieldLevelText').textContent = `${shieldLevel}/5`;
document.getElementById('shieldLevelBar').style.width = (shieldLevel * 20) + '%';
document.getElementById('currentMaxHealth').textContent = maxHearts + ' Hearts';
const shieldBtn = document.getElementById('upgradeShieldBtn');
if (shieldLevel >= 5) {
shieldBtn.textContent = 'MAX LEVEL';
shieldBtn.disabled = true;
shieldBtn.classList.add('maxed');
} else {
const cost = upgradeCosts.shield[shieldLevel];
shieldBtn.textContent = `UPGRADE (${cost} Credits)`;
shieldBtn.disabled = credits < cost;
}
// Gun upgrade
document.getElementById('gunLevelText').textContent = `${gunLevel}/5`;
document.getElementById('gunLevelBar').style.width = (gunLevel * 20) + '%';
const fireRates = ['Normal', 'Fast', 'Rapid', 'Very Rapid', 'EXTREME'];
document.getElementById('currentFireRate').textContent = fireRates[gunLevel - 1];
const gunBtn = document.getElementById('upgradeGunBtn');
if (gunLevel >= 5) {
gunBtn.textContent = 'MAX LEVEL';
gunBtn.disabled = true;
gunBtn.classList.add('maxed');
} else {
const cost = upgradeCosts.gun[gunLevel];
gunBtn.textContent = `UPGRADE (${cost} Credits)`;
gunBtn.disabled = credits < cost;
}
}
function upgradeShield() {
if (shieldLevel >= 5) return;
const cost = upgradeCosts.shield[shieldLevel];
if (credits >= cost) {
credits -= cost;
shieldLevel++;
maxHearts = 3 + (shieldLevel - 1); // +1 heart per level
localStorage.setItem('meteorCredits', credits);
localStorage.setItem('shieldLevel', shieldLevel);
updateShopDisplay();
}
}
function upgradeGun() {
if (gunLevel >= 5) return;
const cost = upgradeCosts.gun[gunLevel];
if (credits >= cost) {
credits -= cost;
gunLevel++;
localStorage.setItem('meteorCredits', credits);
localStorage.setItem('gunLevel', gunLevel);
updateShopDisplay();
}
}
function openShop() {
updateShopDisplay();
document.getElementById('upgradeShop').style.display = 'flex';
document.getElementById('startScreen').style.display = 'none';
document.getElementById('gameOver').style.display = 'none';
}
function closeShop() {
document.getElementById('upgradeShop').style.display = 'none';
document.getElementById('startScreen').style.display = 'flex';
}
function updateHeartsDisplay() {
const heartsContainer = document.getElementById('hearts-display');
heartsContainer.innerHTML = '';
for (let i = 0; i < maxHearts; i++) {
const heart = document.createElement('span');
heart.className = 'heart';
heart.textContent = '❤️';
if (i >= hearts) {
heart.classList.add('lost');
}
heartsContainer.appendChild(heart);
}
}
function showWeaponNotification(weaponName) {
const notification = document.getElementById('weaponNotification');
notification.textContent = `🎯 ${weaponName} EQUIPPED! 🎯`;
notification.style.opacity = '1';
setTimeout(() => {
notification.style.opacity = '0';
}, 2000);
}
function updateWeaponDisplay() {
const display = document.getElementById('weaponDisplay');
if (weaponTimeLeft > 0 && !playerDestroyed) {
const seconds = Math.ceil(weaponTimeLeft / 1000);
const weaponNames = {
spread: 'SPREAD SHOT',
laser: 'LASER BEAM',
rapid: 'RAPID FIRE',
missile: 'MISSILES'
};
display.textContent = `${weaponNames[activeWeapon]} - ${seconds}s`;
display.style.display = 'block';
weaponTimeLeft -= 16; // Approximately 60 FPS
if (weaponTimeLeft <= 0) {
activeWeapon = 'normal';
display.style.display = 'none';
}
} else {
display.style.display = 'none';
}
// Update shield timer (only when player is alive)
if (shieldTimeLeft > 0 && !playerDestroyed) {
shieldTimeLeft -= 16;
if (shieldTimeLeft <= 0) {
shieldActive = false;
}
}
}
function drawPlayer() {
ctx.save();
ctx.translate(player.x, player.y);
// Shield bubble when active
if (shieldActive) {
const shieldPulse = Math.sin(Date.now() / 100) * 5;
const shieldRadius = player.width/2 + 25 + shieldPulse;
ctx.strokeStyle = '#4da6ff';
ctx.lineWidth = 4;
ctx.shadowBlur = 30;
ctx.shadowColor = '#4da6ff';
ctx.beginPath();
ctx.arc(0, 0, shieldRadius, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = 'rgba(77, 166, 255, 0.15)';
ctx.fill();
// Energy particles around shield
for (let i = 0; i < 8; i++) {
const angle = (Date.now() / 500 + i * Math.PI / 4) % (Math.PI * 2);
const px = Math.cos(angle) * shieldRadius;
const py = Math.sin(angle) * shieldRadius;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(px, py, 3, 0, Math.PI * 2);
ctx.fill();
}
}
// Enhanced shield visual at higher levels (only when not in active shield mode)
// REMOVED - no permanent glow around ship
// ROCKET SHIP DESIGN
// Nose cone (pointed front)
ctx.fillStyle = '#e74c3c';
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 2;
ctx.shadowBlur = 15;
ctx.shadowColor = '#e74c3c';
ctx.beginPath();
ctx.moveTo(0, -player.height/2 - 10); // Sharp point
ctx.lineTo(-12, -player.height/2 + 10);
ctx.lineTo(12, -player.height/2 + 10);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
// Main rocket body
ctx.fillStyle = '#e74c3c';
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 2;
ctx.fillRect(-12, -player.height/2 + 10, 24, player.height - 10);
ctx.strokeRect(-12, -player.height/2 + 10, 24, player.height - 10);
// Body accent stripe
ctx.fillStyle = '#ffffff';
ctx.fillRect(-10, -player.height/2 + 12, 20, 3);
// Cockpit window
ctx.fillStyle = '#3498db';
ctx.strokeStyle = '#2980b9';
ctx.lineWidth = 2;
ctx.shadowBlur = 15;
ctx.shadowColor = '#3498db';
ctx.beginPath();
ctx.arc(0, -player.height/2 + 20, 8, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Window reflection
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.beginPath();
ctx.arc(-2, -player.height/2 + 18, 3, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Side fins (wings)
ctx.fillStyle = '#95a5a6';
ctx.strokeStyle = '#7f8c8d';
ctx.lineWidth = 2;
// Left fin
ctx.beginPath();
ctx.moveTo(-12, player.height/2 - 15);
ctx.lineTo(-25, player.height/2 - 5);
ctx.lineTo(-25, player.height/2);
ctx.lineTo(-12, player.height/2 - 5);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Right fin
ctx.beginPath();
ctx.moveTo(12, player.height/2 - 15);
ctx.lineTo(25, player.height/2 - 5);
ctx.lineTo(25, player.height/2);
ctx.lineTo(12, player.height/2 - 5);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Fin details
ctx.strokeStyle = '#ecf0f1';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-22, player.height/2 - 3);
ctx.lineTo(-15, player.height/2 - 8);
ctx.moveTo(22, player.height/2 - 3);
ctx.lineTo(15, player.height/2 - 8);
ctx.stroke();
// Engine nozzles
ctx.fillStyle = '#34495e';
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 2;
// Left engine
ctx.fillRect(-10, player.height/2 - 8, 6, 10);
ctx.strokeRect(-10, player.height/2 - 8, 6, 10);
// Right engine
ctx.fillRect(4, player.height/2 - 8, 6, 10);
ctx.strokeRect(4, player.height/2 - 8, 6, 10);
// Engine glow (already handled by engine particles)
ctx.fillStyle = '#ff6600';
ctx.shadowBlur = 20;
ctx.shadowColor = '#ff6600';
ctx.beginPath();
ctx.arc(-7, player.height/2 + 2, 4, 0, Math.PI * 2);
ctx.arc(7, player.height/2 + 2, 4, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Weapon indicators based on gun level
if (gunLevel >= 3) {
// Advanced weapons - dual laser ports
ctx.fillStyle = '#ffff00';
ctx.shadowBlur = 10;
ctx.shadowColor = '#ffff00';
ctx.fillRect(-15, -player.height/2 + 5, 4, 8);
ctx.fillRect(11, -player.height/2 + 5, 4, 8);
ctx.shadowBlur = 0;
}
if (gunLevel >= 4) {
// Elite weapons - extra emitters
ctx.fillStyle = '#ff00ff';
ctx.beginPath();
ctx.arc(-13, -player.height/2 + 2, 2, 0, Math.PI * 2);
ctx.arc(13, -player.height/2 + 2, 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
let touchStartX = 0;
let touchStartY = 0;
let isDragging = false;
function handleTouchStart(e) {
if (!gameRunning) return;
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const touch = e.touches ? e.touches[0] : e;
touchStartX = touch.clientX - rect.left;
touchStartY = touch.clientY - rect.top;
isDragging = false;
}
function handleTouchMove(e) {
if (!gameRunning) return;
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const touch = e.touches ? e.touches[0] : e;
const currentX = touch.clientX - rect.left;
const currentY = touch.clientY - rect.top;
// If moved more than 10 pixels, consider it a drag
const dist = Math.sqrt((currentX - touchStartX) ** 2 + (currentY - touchStartY) ** 2);
if (dist > 10) {
isDragging = true;
player.targetX = currentX;
player.targetY = currentY;
}
}
function handleTouchEnd(e) {
if (!gameRunning) return;
e.preventDefault();
// If it wasn't a drag, it was a tap - shoot!
if (!isDragging) {
shoot();
}
}
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', handleTouchEnd);
canvas.addEventListener('mousedown', handleTouchStart);
canvas.addEventListener('mousemove', handleTouchMove);
canvas.addEventListener('mouseup', handleTouchEnd);
// Spacebar to shoot on desktop
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
shoot();
}
});
function gameLoop() {
if (!gameRunning) return;
// Update survival time and score (only if alive)
if (!playerDestroyed) {
survivalTime = Math.floor((Date.now() - gameStartTime) / 1000);
document.getElementById('time').textContent = survivalTime + 's';
// Update score based on mode
if (inMinigame) {
const minigameTime = Math.floor((Date.now() - minigameStartTime) / 1000);
const timeRemaining = minigameTimeLimit - minigameTime;
document.getElementById('time').textContent = timeRemaining + 's MINIGAME';
document.getElementById('score').textContent = minigameScore + ' kills';
// Check if minigame time is up
if (timeRemaining <= 0) {
endMinigame(false); // Time's up, return to normal game
}
} else {
score = survivalTime * 10;
document.getElementById('score').textContent = score;
// Check for WIN at 100 seconds
if (survivalTime >= 100) {
winGame();
return; // Stop game loop
}
// Trigger minigame at 50 seconds (only once)
if (survivalTime === 50 && !minigameTriggered) {
minigameTriggered = true;
startMinigame('survival50');
}
}
}
// Increase difficulty over time
difficulty = 1 + (survivalTime / 30);
// Clear canvas
ctx.fillStyle = '#000510';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw planets in the background
ctx.save();
// Planet 1 - Large Gas Giant (Jupiter-like) in top right
const planet1X = canvas.width - 150;
const planet1Y = 120;
const planet1Radius = 80;
// Gas giant gradient
const gasGiant = ctx.createRadialGradient(
planet1X - 20, planet1Y - 20, 20,
planet1X, planet1Y, planet1Radius
);
gasGiant.addColorStop(0, '#e8c4a0');
gasGiant.addColorStop(0.4, '#d4a574');
gasGiant.addColorStop(0.7, '#b87f4f');
gasGiant.addColorStop(1, '#8a5c3a');
ctx.fillStyle = gasGiant;
ctx.shadowBlur = 25;
ctx.shadowColor = '#d4a574';
ctx.beginPath();
ctx.arc(planet1X, planet1Y, planet1Radius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Gas giant bands
ctx.strokeStyle = 'rgba(139, 92, 58, 0.4)';
ctx.lineWidth = 8;
for (let i = -2; i <= 2; i++) {
ctx.beginPath();
ctx.ellipse(planet1X, planet1Y, planet1Radius * 0.95, planet1Radius * 0.7, 0, 0, Math.PI * 2);
ctx.stroke();
}
// Planet 2 - Blue Ice Planet (Neptune-like) in top left
const planet2X = 100;
const planet2Y = 100;
const planet2Radius = 60;
const icePlanet = ctx.createRadialGradient(
planet2X - 15, planet2Y - 15, 15,
planet2X, planet2Y, planet2Radius
);
icePlanet.addColorStop(0, '#a8d8ff');
icePlanet.addColorStop(0.5, '#5ba3d8');
icePlanet.addColorStop(1, '#2d5f8f');
ctx.fillStyle = icePlanet;
ctx.shadowBlur = 20;
ctx.shadowColor = '#5ba3d8';
ctx.beginPath();
ctx.arc(planet2X, planet2Y, planet2Radius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Ice planet spots
ctx.fillStyle = 'rgba(140, 190, 220, 0.3)';
ctx.beginPath();
ctx.arc(planet2X + 15, planet2Y - 10, 18, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(planet2X - 20, planet2Y + 15, 15, 0, Math.PI * 2);
ctx.fill();
// Planet 3 - Red Rocky Planet (Mars-like) bottom left
const planet3X = 120;
const planet3Y = canvas.height - 100;
const planet3Radius = 50;
const rockyPlanet = ctx.createRadialGradient(
planet3X - 12, planet3Y - 12, 12,
planet3X, planet3Y, planet3Radius
);
rockyPlanet.addColorStop(0, '#e89b7a');
rockyPlanet.addColorStop(0.5, '#c85a3a');
rockyPlanet.addColorStop(1, '#8f3a24');
ctx.fillStyle = rockyPlanet;
ctx.shadowBlur = 18;
ctx.shadowColor = '#c85a3a';
ctx.beginPath();
ctx.arc(planet3X, planet3Y, planet3Radius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Rocky craters
ctx.fillStyle = 'rgba(100, 50, 30, 0.4)';
ctx.beginPath();
ctx.arc(planet3X + 15, planet3Y - 5, 12, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(planet3X - 10, planet3Y + 15, 10, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(planet3X - 20, planet3Y - 10, 8, 0, Math.PI * 2);
ctx.fill();
// Planet 4 - Purple/Pink Planet (Exotic) bottom right
const planet4X = canvas.width - 100;
const planet4Y = canvas.height - 120;
const planet4Radius = 55;
const exoticPlanet = ctx.createRadialGradient(
planet4X - 13, planet4Y - 13, 13,
planet4X, planet4Y, planet4Radius
);
exoticPlanet.addColorStop(0, '#e89bff');
exoticPlanet.addColorStop(0.5, '#b85fd8');
exoticPlanet.addColorStop(1, '#7a3a8f');
ctx.fillStyle = exoticPlanet;
ctx.shadowBlur = 22;
ctx.shadowColor = '#b85fd8';
ctx.beginPath();
ctx.arc(planet4X, planet4Y, planet4Radius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Exotic swirls
ctx.strokeStyle = 'rgba(200, 120, 220, 0.3)';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.ellipse(planet4X, planet4Y, planet4Radius * 0.8, planet4Radius * 0.5, 0.3, 0, Math.PI * 2);
ctx.stroke();
// Planet 5 - Small Green Planet (Earth-like) middle left
const planet5X = 80;
const planet5Y = canvas.height / 2;
const planet5Radius = 35;
const greenPlanet = ctx.createRadialGradient(
planet5X - 8, planet5Y - 8, 8,
planet5X, planet5Y, planet5Radius
);
greenPlanet.addColorStop(0, '#7dd87d');
greenPlanet.addColorStop(0.5, '#4a9b4a');
greenPlanet.addColorStop(1, '#2d5f2d');
ctx.fillStyle = greenPlanet;
ctx.shadowBlur = 15;
ctx.shadowColor = '#4a9b4a';
ctx.beginPath();
ctx.arc(planet5X, planet5Y, planet5Radius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Green continents
ctx.fillStyle = 'rgba(80, 150, 80, 0.5)';
ctx.beginPath();
ctx.arc(planet5X - 8, planet5Y - 8, 12, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(planet5X + 10, planet5Y + 8, 10, 0, Math.PI * 2);
ctx.fill();
// Planet 6 - Ringed Planet (Saturn-like) middle right
const planet6X = canvas.width - 120;
const planet6Y = canvas.height / 2 + 50;
const planet6Radius = 45;
// Draw rings first (behind planet)
ctx.strokeStyle = 'rgba(210, 180, 140, 0.6)';
ctx.lineWidth = 8;
ctx.shadowBlur = 10;
ctx.shadowColor = 'rgba(210, 180, 140, 0.4)';
ctx.beginPath();
ctx.ellipse(planet6X, planet6Y, planet6Radius * 1.8, planet6Radius * 0.3, -0.2, 0, Math.PI);
ctx.stroke();
ctx.shadowBlur = 0;
// Planet body
const ringedPlanet = ctx.createRadialGradient(
planet6X - 10, planet6Y - 10, 10,
planet6X, planet6Y, planet6Radius
);
ringedPlanet.addColorStop(0, '#f5e6c8');
ringedPlanet.addColorStop(0.5, '#d4b896');
ringedPlanet.addColorStop(1, '#a38a6a');
ctx.fillStyle = ringedPlanet;
ctx.shadowBlur = 18;
ctx.shadowColor = '#d4b896';
ctx.beginPath();
ctx.arc(planet6X, planet6Y, planet6Radius, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Rings in front
ctx.strokeStyle = 'rgba(180, 155, 120, 0.5)';
ctx.lineWidth = 8;
ctx.beginPath();
ctx.ellipse(planet6X, planet6Y, planet6Radius * 1.8, planet6Radius * 0.3, -0.2, Math.PI, Math.PI * 2);
ctx.stroke();
ctx.restore();
// Draw and update stars
stars.forEach(star => {
star.y += star.speed * difficulty;
if (star.y > canvas.height) {
star.y = 0;
star.x = Math.random() * canvas.width;
}
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.beginPath();
ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
ctx.fill();
});
// Smooth player movement
player.x += (player.targetX - player.x) * player.speed;
player.y += (player.targetY - player.y) * player.speed;
// Keep player in bounds
player.x = Math.max(player.width/2, Math.min(canvas.width - player.width/2, player.x));
player.y = Math.max(player.height/2, Math.min(canvas.height - player.height/2, player.y));
// Update and draw particles
particles = particles.filter(p => {
const alive = p.update();
if (alive) p.draw();
return alive;
});
// Create engine particles from ship's thrusters
if (gameRunning && !playerDestroyed) {
// Left engine
engineParticles.push(new EngineParticle(
player.x - player.width/4,
player.y + player.height/2 - 5
));
// Right engine
engineParticles.push(new EngineParticle(
player.x + player.width/4,
player.y + player.height/2 - 5
));
}
// Update and draw engine particles (draw before player so they appear behind)
engineParticles = engineParticles.filter(p => {
const alive = p.update();
if (alive) p.draw();
return alive;
});
// Update and draw bullets
bullets = bullets.filter(b => {
const alive = b.update();
if (alive) {
b.draw();
// Check bullet-meteor collision
for (let i = meteors.length - 1; i >= 0; i--) {
if (b.checkMeteorHit(meteors[i])) {
// Different explosion colors matching each obstacle's actual color
let explosionColor = '#ff6600';
let killMessage = 'DESTROYED!';
if (meteors[i].constructor.name === 'Satellite') {
explosionColor = '#bdc3c7'; // Gray/silver (satellite body color)
killMessage = 'SATELLITE DOWN!';
} else if (meteors[i].constructor.name === 'RocketShip') {
explosionColor = '#e74c3c'; // Red (rocket body color)
killMessage = 'SHIP DESTROYED!';
} else if (meteors[i].constructor.name === 'SpaceSurfer') {
explosionColor = '#ff1493'; // Pink (surfboard color)
killMessage = 'SURFER HIT!';
} else if (meteors[i].constructor.name === 'IceCrystal') {
explosionColor = '#add8e6'; // Light blue (ice color)
} else if (meteors[i].constructor.name === 'SpaceDebris') {
explosionColor = '#C0C0C0'; // Silver (metal color)
} else if (meteors[i].constructor.name === 'Asteroid') {
explosionColor = '#8B8680'; // Gray (asteroid color)
} else if (meteors[i].constructor.name === 'Meteor') {
explosionColor = '#ff6600'; // Orange (meteor fire color)
}
createExplosion(meteors[i].x, meteors[i].y, explosionColor);
// Track consecutive kills for combos
const now = Date.now();
if (now - lastKillTime < 1000) {
consecutiveKills++;
} else {
consecutiveKills = 1;
}
lastKillTime = now;
// Show feedback based on combo
if (consecutiveKills >= 5) {
feedbackMessages.push(new FeedbackMessage(meteors[i].x, meteors[i].y, '🔥 ON FIRE! 🔥', '#ff6600', 48));
score += 100; // Bonus
} else if (consecutiveKills >= 3) {
feedbackMessages.push(new FeedbackMessage(meteors[i].x, meteors[i].y, '⚡ STREAK! ⚡', '#ffff00', 40));
score += 50; // Bonus
} else if (consecutiveKills === 2) {
feedbackMessages.push(new FeedbackMessage(meteors[i].x, meteors[i].y, 'DOUBLE KILL!', '#00ff00', 36));
} else {
// Random cool feedback for single kills with special messages
const messages = ['NICE!', 'BOOM!', 'GOT IT!', killMessage];
if (Math.random() > 0.7) {
feedbackMessages.push(new FeedbackMessage(meteors[i].x, meteors[i].y, messages[Math.floor(Math.random() * messages.length)], '#ffffff', 28));
}
}
meteors.splice(i, 1);
// Track score
if (inMinigame) {
minigameScore++; // Count kills in minigame
} else {
score += 50; // Base points for shooting meteors
}
document.getElementById('score').textContent = inMinigame ? minigameScore + ' kills' : score;
return false; // Remove bullet
}
}
}
return alive;
});
// Spawn meteors and other obstacles (only when alive)
if (!playerDestroyed) {
// Always spawn meteors
if (Math.random() < 0.02 * difficulty) {
meteors.push(new Meteor()); // Fire meteors
}
// Spawn other obstacles
if (Math.random() < 0.015 * difficulty) {
const obstacleType = Math.random();
if (obstacleType < 0.4) {
meteors.push(new Asteroid()); // 40% gray asteroids
} else if (obstacleType < 0.7) {
meteors.push(new SpaceDebris()); // 30% metal debris
} else if (obstacleType < 0.9) {
meteors.push(new IceCrystal()); // 20% ice crystals
} else {
meteors.push(new Satellite()); // 10% satellites
}
}
// Spawn rocket ships occasionally (horizontal movement)
if (Math.random() < 0.005 * difficulty) {
meteors.push(new RocketShip());
}
// Spawn space surfers occasionally
if (Math.random() < 0.004 * difficulty) {
meteors.push(new SpaceSurfer());
}
// Spawn powerups less frequently
if (Math.random() < 0.006) { // Reduced from 0.015 to 0.006
powerups.push(new Powerup());
}
// Spawn weapon powerups rarely
if (Math.random() < 0.003) { // Reduced from 0.008 to 0.003
powerups.push(new WeaponPowerup());
}
// Spawn shield powerups very rarely
if (Math.random() < 0.0015) { // Reduced from 0.004 to 0.0015 (62% less)
powerups.push(new ShieldPowerup());
}
}
// Update and draw meteors
meteors = meteors.filter(m => {
// Don't update meteors when player is destroyed (they freeze in place)
const alive = !playerDestroyed ? m.update() : true;
if (alive) {
m.draw();
// Check collision only if player is alive
if (!playerDestroyed && m.checkCollision(player.x, player.y, player.width, player.height)) {
// Determine explosion color based on obstacle type
let explosionColor = '#ff6600';
if (m.constructor.name === 'Satellite') {
explosionColor = '#bdc3c7'; // Gray/silver
} else if (m.constructor.name === 'RocketShip') {
explosionColor = '#e74c3c'; // Red
} else if (m.constructor.name === 'SpaceSurfer') {
explosionColor = '#ff1493'; // Pink
} else if (m.constructor.name === 'IceCrystal') {
explosionColor = '#add8e6'; // Light blue
} else if (m.constructor.name === 'SpaceDebris') {
explosionColor = '#C0C0C0'; // Silver
} else if (m.constructor.name === 'Asteroid') {
explosionColor = '#8B8680'; // Gray
} else if (m.constructor.name === 'Meteor') {
explosionColor = '#ff6600'; // Orange
}
createExplosion(m.x, m.y, explosionColor);
// Only take damage if shield is not active
if (!shieldActive) {
hearts -= 1; // Lose 1 heart per hit
updateHeartsDisplay();
if (hearts <= 0) {
explodeShip(player.x, player.y);
// Game will end when astronaut falls off screen
}
} else {
// Shield deflected it - cool feedback!
feedbackMessages.push(new FeedbackMessage(m.x, m.y, 'DEFLECTED!', '#4da6ff', 32));
}
return false;
}
// Check for close dodge (within 60 pixels but not hit)
if (!playerDestroyed) {
const dist = Math.sqrt((m.x - player.x) ** 2 + (m.y - player.y) ** 2);
if (dist < 60 && Math.random() > 0.95 && !m.closeDodge) {
feedbackMessages.push(new FeedbackMessage(m.x, m.y, 'CLOSE!', '#ffaa00', 28));
m.closeDodge = true; // Mark so we don't spam
}
}
}
return alive;
});
// Update and draw powerups
powerups = powerups.filter(p => {
const alive = p.update();
if (alive) {
p.draw();
// Check collision only if player is alive
if (!playerDestroyed && p.checkCollision(player.x, player.y, player.width, player.height)) {
if (p.type === 'health') {
// Health powerup - restore 1 heart
hearts = Math.min(maxHearts, hearts + 1);
updateHeartsDisplay();
createExplosion(p.x, p.y, '#00ffff');
score += 25;
feedbackMessages.push(new FeedbackMessage(p.x, p.y, '❤️ HEALTH +1', '#00ffff', 32));
} else if (p.type === 'shield') {
// Shield powerup - replaces weapon if active
const hadShield = shieldActive;
const hadWeapon = activeWeapon !== 'normal';
// Deactivate any weapon
if (hadWeapon) {
feedbackMessages.push(new FeedbackMessage(p.x, p.y, `❌ ${p.config[activeWeapon].name} REMOVED!`, '#ff6600', 28));
activeWeapon = 'normal';
weaponTimeLeft = 0;
}
shieldActive = true;
shieldTimeLeft = shieldDuration;
createExplosion(p.x, p.y, '#4da6ff');
score += 50;
if (hadShield) {
// Refreshing existing shield
feedbackMessages.push(new FeedbackMessage(p.x, p.y + (hadWeapon ? 40 : 0), '🔄 SHIELD REFRESHED!', '#4da6ff', 38));
} else {
// New shield
feedbackMessages.push(new FeedbackMessage(p.x, p.y + (hadWeapon ? 40 : 0), '🛡️ SHIELD ACTIVE!', '#4da6ff', 42));
}
showWeaponNotification('INVINCIBILITY SHIELD');
} else {
// Weapon powerup - replaces shield if active
const previousWeapon = activeWeapon;
const hadWeapon = previousWeapon !== 'normal';
const hadShield = shieldActive;
// Deactivate shield
if (hadShield) {
feedbackMessages.push(new FeedbackMessage(p.x, p.y, '❌ SHIELD REMOVED!', '#ff6600', 28));
shieldActive = false;
shieldTimeLeft = 0;
}
// Set new weapon
activeWeapon = p.weaponType;
weaponTimeLeft = weaponDuration;
createExplosion(p.x, p.y, p.config[p.weaponType].color);
score += 50; // More points for weapon
// Show notification
if (hadWeapon) {
// Replacing existing weapon
feedbackMessages.push(new FeedbackMessage(p.x, p.y + (hadShield ? 40 : 0), `❌ ${p.config[previousWeapon].name} REPLACED!`, '#ff6600', 28));
feedbackMessages.push(new FeedbackMessage(p.x, p.y + (hadShield ? 80 : 40), `✅ ${p.config[p.weaponType].name} EQUIPPED!`, p.config[p.weaponType].color, 38));
} else {
// New weapon
feedbackMessages.push(new FeedbackMessage(p.x, p.y + (hadShield ? 40 : 0), `✅ ${p.config[p.weaponType].name} EQUIPPED!`, p.config[p.weaponType].color, 38));
}
showWeaponNotification(p.config[p.weaponType].name);
}
document.getElementById('score').textContent = score;
return false;
}
}
return alive;
});
// Update weapon timer
updateWeaponDisplay();
// Update and draw ship debris if destroyed
shipDebris = shipDebris.filter(d => {
const alive = d.update();
if (alive) d.draw();
return alive;
});
// Update and draw ejected astronaut
if (ejectedAstronaut) {
const alive = ejectedAstronaut.update();
if (alive) {
ejectedAstronaut.draw();
} else {
// Astronaut has fallen off screen
ejectedAstronaut = null;
// Only end normal game if not in minigame
if (playerDestroyed && !inMinigame) {
endGame();
}
}
}
// Update and draw feedback messages
feedbackMessages = feedbackMessages.filter(f => {
const alive = f.update();
if (alive) f.draw();
return alive;
});
// Update and draw confetti
confetti = confetti.filter(c => {
const alive = c.update();
if (alive) c.draw();
return alive;
});
// Draw player (only if not destroyed)
if (!playerDestroyed) {
drawPlayer();
}
requestAnimationFrame(gameLoop);
}
function startMinigame(type) {
inMinigame = true;
minigameType = type;
minigameStartTime = Date.now();
minigameScore = 0;
// Save current game state
normalGameState = {
survivalTime: survivalTime,
score: score,
hearts: hearts,
difficulty: difficulty
};
// Reset for minigame
meteors = [];
bullets = [];
particles = [];
powerups = [];
feedbackMessages = [];
// Show minigame notification
feedbackMessages.push(new FeedbackMessage(canvas.width/2, canvas.height/2, '🎮 MINIGAME START! 🎮', '#ffff00', 64));
feedbackMessages.push(new FeedbackMessage(canvas.width/2, canvas.height/2 + 60, 'SURVIVE 50 SECONDS!', '#00ff00', 42));
feedbackMessages.push(new FeedbackMessage(canvas.width/2, canvas.height/2 + 110, 'SHOOT AS MANY AS YOU CAN!', '#ffffff', 36));
}
function endMinigame(died) {
const finalScore = minigameScore;
// Clear everything first
meteors = [];
bullets = [];
powerups = [];
particles = [];
engineParticles = [];
feedbackMessages = [];
shipDebris = [];
ejectedAstronaut = null;
// Show results
if (died) {
feedbackMessages.push(new FeedbackMessage(canvas.width/2, canvas.height/2, '💥 MINIGAME OVER! 💥', '#ff0000', 56));
feedbackMessages.push(new FeedbackMessage(canvas.width/2, canvas.height/2 + 60, `You destroyed ${finalScore} obstacles!`, '#ffaa00', 38));
} else {
feedbackMessages.push(new FeedbackMessage(canvas.width/2, canvas.height/2, '✨ MINIGAME COMPLETE! ✨', '#00ff00', 56));
feedbackMessages.push(new FeedbackMessage(canvas.width/2, canvas.height/2 + 60, `${finalScore} obstacles destroyed!`, '#ffaa00', 38));
}
// Restore normal game state
survivalTime = normalGameState.survivalTime;
score = normalGameState.score;
hearts = normalGameState.hearts;
difficulty = normalGameState.difficulty;
// Reset player state
playerDestroyed = false;
player.x = canvas.width / 2;
player.y = canvas.height - 100;
player.targetX = player.x;
player.targetY = player.y;
updateHeartsDisplay();
// Reset weapon states
activeWeapon = 'normal';
weaponTimeLeft = 0;
shieldActive = false;
shieldTimeLeft = 0;
canShoot = true;
// Reset minigame flags
inMinigame = false;
minigameType = null;
minigameScore = 0;
// Update display and recalculate game time
gameStartTime = Date.now() - (survivalTime * 1000);
document.getElementById('score').textContent = score;
document.getElementById('time').textContent = survivalTime + 's';
}
function startGame() {
// Initialize audio context
initAudio();
gameRunning = true;
score = 0;
survivalTime = 0;
hearts = maxHearts; // Start with max hearts
difficulty = 1;
meteors = [];
powerups = [];
particles = [];
confetti = [];
bullets = [];
engineParticles = [];
shipDebris = [];
feedbackMessages = [];
ejectedAstronaut = null;
playerDestroyed = false;
consecutiveKills = 0;
lastKillTime = 0;
canShoot = true;
activeWeapon = 'normal';
weaponTimeLeft = 0;
shieldActive = false;
shieldTimeLeft = 0;
// Reset minigame flags
inMinigame = false;
minigameTriggered = false;
minigameScore = 0;
player.x = canvas.width / 2;
player.y = canvas.height - 100;
player.targetX = player.x;
player.targetY = player.y;
gameStartTime = Date.now();
document.getElementById('score').textContent = '0';
document.getElementById('time').textContent = '0s';
document.getElementById('credits').textContent = credits;
document.getElementById('startScreen').style.display = 'none';
document.getElementById('gameOver').style.display = 'none';
document.getElementById('instructions').style.display = 'block';
updateHeartsDisplay();
setTimeout(() => {
document.getElementById('instructions').style.display = 'none';
}, 4000);
gameLoop();
}
function endGame() {
if (!gameRunning) return;
gameRunning = false;
// Award credits based on survival time (1 credit per second)
const earnedCredits = survivalTime;
credits += earnedCredits;
localStorage.setItem('meteorCredits', credits);
if (survivalTime > highScore) {
highScore = survivalTime;
localStorage.setItem('meteorDodgeHighScore', highScore);
}
document.getElementById('finalStats').innerHTML =
`<strong>Final Score: ${score}</strong>`;
document.getElementById('survivalTime').innerHTML =
`Survived: ${survivalTime} seconds<br>Best: ${highScore} seconds`;
document.getElementById('creditsEarned').innerHTML =
`💰 +${earnedCredits} Credits Earned! (1 credit/second)`;
document.getElementById('gameOver').style.display = 'flex';
updateShopDisplay();
}
function winGame() {
if (!gameRunning) return;
gameRunning = false;
// CONFETTI EXPLOSION! 🎉
for (let i = 0; i < 200; i++) {
confetti.push(new Confetti(canvas.width / 2, canvas.height / 2));
}
// More confetti bursts from different positions
setTimeout(() => {
for (let i = 0; i < 100; i++) {
confetti.push(new Confetti(canvas.width * 0.25, canvas.height * 0.3));
}
}, 200);
setTimeout(() => {
for (let i = 0; i < 100; i++) {
confetti.push(new Confetti(canvas.width * 0.75, canvas.height * 0.3));
}
}, 400);
// Bonus credits for winning!
const earnedCredits = survivalTime + 100; // Survival credits + 100 bonus
credits += earnedCredits;
localStorage.setItem('meteorCredits', credits);
if (survivalTime > highScore) {
highScore = survivalTime;
localStorage.setItem('meteorDodgeHighScore', highScore);
}
// Show WIN screen
document.getElementById('finalStats').innerHTML =
`<strong style="color: #00ff00; font-size: 72px;">🏆 YOU WIN! 🏆</strong><br><strong>Final Score: ${score}</strong>`;
document.getElementById('survivalTime').innerHTML =
`You survived 100 seconds!<br>Best: ${highScore} seconds`;
document.getElementById('creditsEarned').innerHTML =
`💰 +${earnedCredits} Credits Earned!<br>(100 seconds + 100 BONUS!)`;
document.getElementById('gameOver').style.display = 'flex';
updateShopDisplay();
}
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('restartBtn').addEventListener('click', startGame);
document.getElementById('shopBtnStart').addEventListener('click', openShop);
document.getElementById('upgradeBtn').addEventListener('click', openShop);
document.getElementById('upgradeShieldBtn').addEventListener('click', upgradeShield);
document.getElementById('upgradeGunBtn').addEventListener('click', upgradeGun);
document.getElementById('closeShopBtn').addEventListener('click', closeShop);
// Initialize shop display
updateShopDisplay();
// Background animation when not playing
function backgroundLoop() {
if (gameRunning) {
requestAnimationFrame(backgroundLoop);
return;
}
ctx.fillStyle = '#000510';
ctx.fillRect(0, 0, canvas.width, canvas.height);
stars.forEach(star => {
star.y += star.speed * 0.5;
if (star.y > canvas.height) {
star.y = 0;
star.x = Math.random() * canvas.width;
}
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.beginPath();
ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
ctx.fill();
});
requestAnimationFrame(backgroundLoop);
}
backgroundLoop();
</script>
```
</body>
</html>Game Source: Meteor Dodge
Creator: SolarScout64
Libraries: none
Complexity: complex (3419 lines, 115.0 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a KIDHUBB header at the top with "remix_of: meteor-dodge-solarscout64" to link back to the original. Then publish at kidhubb.com/publish.