Initial commit
This commit is contained in:
@@ -0,0 +1,542 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Asteroids</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; touch-action: none; font-family: Arial, sans-serif; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="game"></canvas>
|
||||
<script>
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
let audioCtx = null;
|
||||
let musicEnabled = true;
|
||||
let sfxEnabled = true;
|
||||
let musicPlaying = false;
|
||||
|
||||
function initAudio() {
|
||||
if (!audioCtx) audioCtx = new AudioContext();
|
||||
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||
}
|
||||
|
||||
// Space adventure melody
|
||||
const melody = [
|
||||
{n: 330, d: 0.3}, {n: 392, d: 0.3}, {n: 440, d: 0.3}, {n: 523, d: 0.6},
|
||||
{n: 440, d: 0.3}, {n: 392, d: 0.3}, {n: 330, d: 0.6},
|
||||
{n: 294, d: 0.3}, {n: 330, d: 0.3}, {n: 392, d: 0.3}, {n: 440, d: 0.6},
|
||||
{n: 392, d: 0.3}, {n: 330, d: 0.3}, {n: 294, d: 0.6}, {n: 0, d: 0.3}
|
||||
];
|
||||
|
||||
const bass = [
|
||||
{n: 82, d: 0.6}, {n: 98, d: 0.6}, {n: 110, d: 0.6}, {n: 131, d: 0.6},
|
||||
{n: 73, d: 0.6}, {n: 82, d: 0.6}, {n: 98, d: 0.6}, {n: 110, d: 0.6}
|
||||
];
|
||||
|
||||
let melodyIdx = 0, bassIdx = 0, nextMelody = 0, nextBass = 0;
|
||||
|
||||
function playTone(freq, dur, type = 'square', vol = 0.1) {
|
||||
if (!audioCtx || !musicEnabled || freq === 0) return;
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur * 0.9);
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.start();
|
||||
osc.stop(audioCtx.currentTime + dur);
|
||||
}
|
||||
|
||||
function playSfx(type) {
|
||||
if (!audioCtx || !sfxEnabled) return;
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
if (type === 'shoot') {
|
||||
osc.frequency.setValueAtTime(800, audioCtx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(200, audioCtx.currentTime + 0.1);
|
||||
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
|
||||
} else if (type === 'explode') {
|
||||
osc.frequency.setValueAtTime(150, audioCtx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(50, audioCtx.currentTime + 0.3);
|
||||
gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
|
||||
osc.type = 'sawtooth';
|
||||
} else if (type === 'die') {
|
||||
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.5);
|
||||
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
|
||||
osc.type = 'sawtooth';
|
||||
}
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.start();
|
||||
osc.stop(audioCtx.currentTime + 0.6);
|
||||
}
|
||||
|
||||
function updateMusic() {
|
||||
if (!audioCtx || !musicEnabled || !musicPlaying) return;
|
||||
const now = audioCtx.currentTime;
|
||||
if (now >= nextMelody) {
|
||||
const note = melody[melodyIdx];
|
||||
playTone(note.n, note.d, 'square', 0.08);
|
||||
nextMelody = now + note.d;
|
||||
melodyIdx = (melodyIdx + 1) % melody.length;
|
||||
}
|
||||
if (now >= nextBass) {
|
||||
const note = bass[bassIdx];
|
||||
playTone(note.n, note.d, 'triangle', 0.12);
|
||||
nextBass = now + note.d;
|
||||
bassIdx = (bassIdx + 1) % bass.length;
|
||||
}
|
||||
}
|
||||
|
||||
function startMusic() {
|
||||
if (!musicPlaying && musicEnabled) {
|
||||
initAudio();
|
||||
musicPlaying = true;
|
||||
nextMelody = audioCtx.currentTime;
|
||||
nextBass = audioCtx.currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = document.getElementById('game');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let W, H, safeTop, safeBottom;
|
||||
|
||||
function resize() {
|
||||
W = window.innerWidth;
|
||||
H = window.innerHeight;
|
||||
canvas.width = W;
|
||||
canvas.height = H;
|
||||
safeTop = H * 0.1;
|
||||
safeBottom = H * 0.9;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
let gameRunning = false;
|
||||
let gameOver = false;
|
||||
let score = 0;
|
||||
let lives = 3;
|
||||
let level = 1;
|
||||
let gameTime = 0;
|
||||
|
||||
const ship = { x: 0, y: 0, angle: 0, dx: 0, dy: 0, size: 20, invincible: 0 };
|
||||
let asteroids = [];
|
||||
let bullets = [];
|
||||
let particles = [];
|
||||
|
||||
let keys = { left: false, right: false, up: false, shoot: false };
|
||||
let lastShot = 0;
|
||||
let touchTarget = null;
|
||||
|
||||
function spawnAsteroids(count, size = 'large') {
|
||||
for (let i = 0; i < count; i++) {
|
||||
let x, y;
|
||||
do {
|
||||
x = Math.random() * W;
|
||||
y = safeTop + Math.random() * (safeBottom - safeTop);
|
||||
} while (Math.hypot(x - ship.x, y - ship.y) < 150);
|
||||
|
||||
const speed = size === 'large' ? 0.8 : size === 'medium' ? 1.2 : 1.8;
|
||||
const r = size === 'large' ? 40 : size === 'medium' ? 25 : 15;
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
asteroids.push({
|
||||
x, y, r, size,
|
||||
dx: Math.cos(angle) * speed,
|
||||
dy: Math.sin(angle) * speed,
|
||||
vertices: generateAsteroidShape(r)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function generateAsteroidShape(r) {
|
||||
const vertices = [];
|
||||
const points = 8 + Math.floor(Math.random() * 4);
|
||||
for (let i = 0; i < points; i++) {
|
||||
const angle = (i / points) * Math.PI * 2;
|
||||
const dist = r * (0.7 + Math.random() * 0.3);
|
||||
vertices.push({ x: Math.cos(angle) * dist, y: Math.sin(angle) * dist });
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
|
||||
function init() {
|
||||
ship.x = W / 2;
|
||||
ship.y = (safeTop + safeBottom) / 2;
|
||||
ship.angle = -Math.PI / 2;
|
||||
ship.dx = 0;
|
||||
ship.dy = 0;
|
||||
ship.invincible = 180;
|
||||
|
||||
asteroids = [];
|
||||
bullets = [];
|
||||
particles = [];
|
||||
score = 0;
|
||||
lives = 3;
|
||||
level = 1;
|
||||
gameTime = 0;
|
||||
|
||||
spawnAsteroids(3);
|
||||
gameOver = false;
|
||||
gameRunning = true;
|
||||
startMusic();
|
||||
}
|
||||
|
||||
function shoot() {
|
||||
if (Date.now() - lastShot < 250) return;
|
||||
lastShot = Date.now();
|
||||
bullets.push({
|
||||
x: ship.x + Math.cos(ship.angle) * ship.size,
|
||||
y: ship.y + Math.sin(ship.angle) * ship.size,
|
||||
dx: Math.cos(ship.angle) * 8,
|
||||
dy: Math.sin(ship.angle) * 8,
|
||||
life: 60
|
||||
});
|
||||
playSfx('shoot');
|
||||
}
|
||||
|
||||
function addParticles(x, y, count, color) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = 1 + Math.random() * 3;
|
||||
particles.push({
|
||||
x, y,
|
||||
dx: Math.cos(angle) * speed,
|
||||
dy: Math.sin(angle) * speed,
|
||||
life: 30 + Math.random() * 20,
|
||||
color
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
initAudio();
|
||||
if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = true;
|
||||
if (e.key === 'ArrowRight' || e.key === 'd') keys.right = true;
|
||||
if (e.key === 'ArrowUp' || e.key === 'w') keys.up = true;
|
||||
if (e.key === ' ') { keys.shoot = true; e.preventDefault(); }
|
||||
if ((e.key === ' ' || e.key === 'Enter') && gameOver) init();
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = false;
|
||||
if (e.key === 'ArrowRight' || e.key === 'd') keys.right = false;
|
||||
if (e.key === 'ArrowUp' || e.key === 'w') keys.up = false;
|
||||
if (e.key === ' ') keys.shoot = false;
|
||||
});
|
||||
|
||||
canvas.addEventListener('click', e => {
|
||||
initAudio();
|
||||
if (gameOver) { init(); return; }
|
||||
shoot();
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', e => {
|
||||
if (!gameRunning || gameOver) return;
|
||||
touchTarget = { x: e.clientX, y: e.clientY };
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault();
|
||||
initAudio();
|
||||
if (gameOver) { init(); return; }
|
||||
touchTarget = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||
shoot();
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
touchTarget = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchend', () => {
|
||||
touchTarget = null;
|
||||
});
|
||||
|
||||
function update() {
|
||||
if (!gameRunning || gameOver) return;
|
||||
|
||||
gameTime++;
|
||||
|
||||
// Ship rotation (keyboard)
|
||||
if (keys.left) ship.angle -= 0.08;
|
||||
if (keys.right) ship.angle += 0.08;
|
||||
|
||||
// Touch controls - point toward target
|
||||
if (touchTarget) {
|
||||
const targetAngle = Math.atan2(touchTarget.y - ship.y, touchTarget.x - ship.x);
|
||||
let diff = targetAngle - ship.angle;
|
||||
while (diff > Math.PI) diff -= Math.PI * 2;
|
||||
while (diff < -Math.PI) diff += Math.PI * 2;
|
||||
ship.angle += diff * 0.1;
|
||||
keys.up = true;
|
||||
} else if (!keys.up) {
|
||||
keys.up = false;
|
||||
}
|
||||
|
||||
// Thrust
|
||||
if (keys.up) {
|
||||
ship.dx += Math.cos(ship.angle) * 0.15;
|
||||
ship.dy += Math.sin(ship.angle) * 0.15;
|
||||
}
|
||||
|
||||
// Friction
|
||||
ship.dx *= 0.99;
|
||||
ship.dy *= 0.99;
|
||||
|
||||
// Move ship
|
||||
ship.x += ship.dx;
|
||||
ship.y += ship.dy;
|
||||
|
||||
// Wrap around
|
||||
if (ship.x < 0) ship.x = W;
|
||||
if (ship.x > W) ship.x = 0;
|
||||
if (ship.y < safeTop) ship.y = safeBottom;
|
||||
if (ship.y > safeBottom) ship.y = safeTop;
|
||||
|
||||
// Auto-shoot on keyboard
|
||||
if (keys.shoot) shoot();
|
||||
|
||||
// Update invincibility
|
||||
if (ship.invincible > 0) ship.invincible--;
|
||||
|
||||
// Update bullets
|
||||
for (let i = bullets.length - 1; i >= 0; i--) {
|
||||
const b = bullets[i];
|
||||
b.x += b.dx;
|
||||
b.y += b.dy;
|
||||
b.life--;
|
||||
|
||||
// Wrap
|
||||
if (b.x < 0) b.x = W;
|
||||
if (b.x > W) b.x = 0;
|
||||
if (b.y < safeTop) b.y = safeBottom;
|
||||
if (b.y > safeBottom) b.y = safeTop;
|
||||
|
||||
if (b.life <= 0) bullets.splice(i, 1);
|
||||
}
|
||||
|
||||
// Update asteroids
|
||||
for (let i = asteroids.length - 1; i >= 0; i--) {
|
||||
const a = asteroids[i];
|
||||
a.x += a.dx;
|
||||
a.y += a.dy;
|
||||
|
||||
// Wrap
|
||||
if (a.x < -a.r) a.x = W + a.r;
|
||||
if (a.x > W + a.r) a.x = -a.r;
|
||||
if (a.y < safeTop - a.r) a.y = safeBottom + a.r;
|
||||
if (a.y > safeBottom + a.r) a.y = safeTop - a.r;
|
||||
|
||||
// Check bullet collision
|
||||
for (let j = bullets.length - 1; j >= 0; j--) {
|
||||
const b = bullets[j];
|
||||
if (Math.hypot(b.x - a.x, b.y - a.y) < a.r) {
|
||||
bullets.splice(j, 1);
|
||||
addParticles(a.x, a.y, 10, '#ff0');
|
||||
playSfx('explode');
|
||||
|
||||
// Split asteroid
|
||||
if (a.size === 'large') {
|
||||
score += 20;
|
||||
for (let k = 0; k < 2; k++) {
|
||||
asteroids.push({
|
||||
x: a.x, y: a.y, r: 25, size: 'medium',
|
||||
dx: (Math.random() - 0.5) * 2.5,
|
||||
dy: (Math.random() - 0.5) * 2.5,
|
||||
vertices: generateAsteroidShape(25)
|
||||
});
|
||||
}
|
||||
} else if (a.size === 'medium') {
|
||||
score += 50;
|
||||
for (let k = 0; k < 2; k++) {
|
||||
asteroids.push({
|
||||
x: a.x, y: a.y, r: 15, size: 'small',
|
||||
dx: (Math.random() - 0.5) * 3,
|
||||
dy: (Math.random() - 0.5) * 3,
|
||||
vertices: generateAsteroidShape(15)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
score += 100;
|
||||
}
|
||||
|
||||
asteroids.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check ship collision (generous for children)
|
||||
if (ship.invincible <= 0) {
|
||||
for (const a of asteroids) {
|
||||
if (Math.hypot(ship.x - a.x, ship.y - a.y) < a.r + ship.size - 15) {
|
||||
lives--;
|
||||
ship.invincible = 180;
|
||||
addParticles(ship.x, ship.y, 20, '#0ff');
|
||||
playSfx('die');
|
||||
|
||||
if (lives <= 0) {
|
||||
gameOver = true;
|
||||
gameRunning = false;
|
||||
musicPlaying = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update particles
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const p = particles[i];
|
||||
p.x += p.dx;
|
||||
p.y += p.dy;
|
||||
p.life--;
|
||||
if (p.life <= 0) particles.splice(i, 1);
|
||||
}
|
||||
|
||||
// Spawn more asteroids when cleared
|
||||
if (asteroids.length === 0) {
|
||||
level++;
|
||||
// Progressive difficulty - more asteroids, but capped for fairness
|
||||
const count = Math.min(3 + level, 8);
|
||||
spawnAsteroids(count);
|
||||
}
|
||||
|
||||
updateMusic();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// Background
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// Safe zones
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.fillRect(0, 0, W, safeTop);
|
||||
ctx.fillRect(0, safeBottom, W, H - safeBottom);
|
||||
|
||||
// Stars
|
||||
ctx.fillStyle = '#fff';
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const x = (i * 137) % W;
|
||||
const y = safeTop + ((i * 251) % (safeBottom - safeTop));
|
||||
ctx.fillRect(x, y, 2, 2);
|
||||
}
|
||||
|
||||
// Header
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 22px Arial';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`Score: ${score}`, 20, 35);
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`Level ${level}`, W / 2, 35);
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(`Lives: ${'*'.repeat(lives)}`, W - 20, 35);
|
||||
|
||||
// Particles
|
||||
for (const p of particles) {
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.globalAlpha = p.life / 50;
|
||||
ctx.fillRect(p.x - 2, p.y - 2, 4, 4);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Asteroids
|
||||
ctx.strokeStyle = '#888';
|
||||
ctx.lineWidth = 2;
|
||||
for (const a of asteroids) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(a.x + a.vertices[0].x, a.y + a.vertices[0].y);
|
||||
for (let i = 1; i < a.vertices.length; i++) {
|
||||
ctx.lineTo(a.x + a.vertices[i].x, a.y + a.vertices[i].y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Bullets
|
||||
ctx.fillStyle = '#ff0';
|
||||
for (const b of bullets) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(b.x, b.y, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Ship
|
||||
if (ship.invincible <= 0 || Math.floor(ship.invincible / 5) % 2 === 0) {
|
||||
ctx.save();
|
||||
ctx.translate(ship.x, ship.y);
|
||||
ctx.rotate(ship.angle);
|
||||
|
||||
ctx.strokeStyle = '#0ff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(ship.size, 0);
|
||||
ctx.lineTo(-ship.size * 0.7, -ship.size * 0.6);
|
||||
ctx.lineTo(-ship.size * 0.4, 0);
|
||||
ctx.lineTo(-ship.size * 0.7, ship.size * 0.6);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
|
||||
// Thrust flame
|
||||
if (keys.up) {
|
||||
ctx.fillStyle = '#f80';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-ship.size * 0.5, -ship.size * 0.3);
|
||||
ctx.lineTo(-ship.size * 0.5, ship.size * 0.3);
|
||||
ctx.lineTo(-ship.size - Math.random() * 10, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
if (gameOver) {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.85)';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 40px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('GAME OVER', W / 2, H / 2 - 50);
|
||||
ctx.font = '24px Arial';
|
||||
ctx.fillText(`Final Score: ${score}`, W / 2, H / 2);
|
||||
|
||||
ctx.fillStyle = '#0ff';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(W / 2 - 100, H / 2 + 30, 200, 50, 10);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = 'bold 18px Arial';
|
||||
ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 62);
|
||||
}
|
||||
}
|
||||
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
window.gameScore = 0;
|
||||
Object.defineProperty(window, 'gameScore', { get: () => score });
|
||||
window.pauseGame = () => { gameRunning = false; musicPlaying = false; };
|
||||
window.resumeGame = () => { if (!gameOver) { gameRunning = true; startMusic(); } };
|
||||
window.setMusicEnabled = (v) => { musicEnabled = v; if (!v) musicPlaying = false; };
|
||||
window.setSfxEnabled = (v) => { sfxEnabled = v; };
|
||||
|
||||
init();
|
||||
gameLoop();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user