Asteroidsv1 / index.html
awacke1's picture
Update index.html
13f01f5 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Asteroids</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
body {
margin: 0;
padding: 0;
background-color: #000;
color: #fff;
font-family: 'Press Start 2P', cursive;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
overflow: hidden;
}
#game-container {
position: relative;
width: 100%;
max-width: 800px;
aspect-ratio: 4 / 3;
border: 2px solid #fff;
box-shadow: 0 0 20px #fff;
}
canvas {
display: block;
background-color: #000;
width: 100%;
height: 100%;
}
#score-container {
position: absolute;
top: 10px;
left: 0;
width: 100%;
display: flex;
justify-content: space-between;
padding: 0 20px;
box-sizing: border-box;
font-size: 16px;
text-shadow: 0 0 5px #fff;
}
#message-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
font-size: 24px;
display: none; /* Hidden by default */
}
#message-overlay p {
margin: 0;
padding: 10px;
}
</style>
</head>
<body>
<div id="game-container">
<div id="score-container">
<span id="score">SCORE: 0</span>
<span id="high-score">HIGH: 0</span>
</div>
<canvas id="gameCanvas"></canvas>
<div id="message-overlay">
<p id="message-title">ASTEROIDS</p>
<p id="message-subtitle" style="font-size: 14px;">PRESS ENTER TO START</p>
</div>
</div>
<script>
// --- DOM ELEMENTS ---
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const highScoreEl = document.getElementById('high-score');
const messageOverlay = document.getElementById('message-overlay');
const messageTitle = document.getElementById('message-title');
const messageSubtitle = document.getElementById('message-subtitle');
// --- GAME CONSTANTS ---
const SHIP_SIZE = 15;
const SHIP_THRUST = 0.1;
const SHIP_TURN_SPEED = 0.1; // radians
const FRICTION = 0.99;
const BULLET_SPEED = 5;
const BULLET_MAX = 10;
const ASTEROID_NUM = 3;
const ASTEROID_SPEED = 1;
const ASTEROID_SIZE_LARGE = 50;
const ASTEROID_SIZE_MEDIUM = 25;
const ASTEROID_SIZE_SMALL = 12;
const ASTEROID_VERTICES = 10;
const ASTEROID_JAG = 0.4; // Jaggedness of the asteroids
const SAUCER_SPEED = 2;
const SAUCER_SIZE = 15;
const SAUCER_FIRE_RATE = 0.03; // ~ every second at 30fps
const SAUCER_SPAWN_TIME = 15000; // 15 seconds
// --- GAME STATE ---
let ship;
let asteroids = [];
let bullets = [];
let saucer = null;
let score = 0;
let highScore = localStorage.getItem('asteroidsHighScore') || 0;
let lives = 3;
let isPlaying = false;
let keys = {};
let saucerTimer;
// --- UTILITY FUNCTIONS ---
const degToRad = (deg) => deg * Math.PI / 180;
const radToDeg = (rad) => rad * 180 / Math.PI;
function resizeCanvas() {
const container = document.getElementById('game-container');
const { width, height } = container.getBoundingClientRect();
canvas.width = width;
canvas.height = height;
}
// --- CLASSES ---
class Ship {
constructor() {
this.x = canvas.width / 2;
this.y = canvas.height / 2;
this.radius = SHIP_SIZE / 2;
this.angle = degToRad(270); // Pointing up
this.vel = { x: 0, y: 0 };
this.isThrusting = false;
this.canShoot = true;
this.isInvincible = true;
this.invincibilityTime = 3000; // 3 seconds
setTimeout(() => this.isInvincible = false, this.invincibilityTime);
}
draw() {
ctx.strokeStyle = this.isInvincible ? 'grey' : 'white';
ctx.lineWidth = SHIP_SIZE / 10;
ctx.beginPath();
// Nose of the ship
ctx.moveTo(
this.x + this.radius * Math.cos(this.angle),
this.y + this.radius * Math.sin(this.angle)
);
// Left wing
ctx.lineTo(
this.x - this.radius * (Math.cos(this.angle) + Math.sin(this.angle)),
this.y - this.radius * (Math.sin(this.angle) - Math.cos(this.angle))
);
// Right wing
ctx.lineTo(
this.x - this.radius * (Math.cos(this.angle) - Math.sin(this.angle)),
this.y - this.radius * (Math.sin(this.angle) + Math.cos(this.angle))
);
ctx.closePath();
ctx.stroke();
// Draw thrust flame
if (this.isThrusting) {
ctx.fillStyle = "red";
ctx.strokeStyle = "yellow";
ctx.lineWidth = SHIP_SIZE / 15;
ctx.beginPath();
// Flame point
ctx.moveTo(
this.x - this.radius * (1.5 * Math.cos(this.angle) - 0.5 * Math.sin(this.angle)),
this.y - this.radius * (1.5 * Math.sin(this.angle) + 0.5 * Math.cos(this.angle))
);
// Flame base center
ctx.lineTo(
this.x - this.radius * 2.5 * Math.cos(this.angle),
this.y - this.radius * 2.5 * Math.sin(this.angle)
);
// Flame point
ctx.lineTo(
this.x - this.radius * (1.5 * Math.cos(this.angle) + 0.5 * Math.sin(this.angle)),
this.y - this.radius * (1.5 * Math.sin(this.angle) - 0.5 * Math.cos(this.angle))
);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
}
update() {
// Rotate ship
if (keys['a'] || keys['A']) {
this.angle -= SHIP_TURN_SPEED;
}
if (keys['d'] || keys['D']) {
this.angle += SHIP_TURN_SPEED;
}
// Thrust
this.isThrusting = (keys['w'] || keys['W'] || keys['e'] || keys['E']);
if (this.isThrusting) {
this.vel.x += SHIP_THRUST * Math.cos(this.angle);
this.vel.y += SHIP_THRUST * Math.sin(this.angle);
}
// Apply friction
this.vel.x *= FRICTION;
this.vel.y *= FRICTION;
// Move ship
this.x += this.vel.x;
this.y += this.vel.y;
// Handle screen wrapping
this.handleScreenWrap();
this.draw();
}
shoot() {
if (this.canShoot && bullets.length < BULLET_MAX) {
const bullet = new Bullet(
this.x + this.radius * Math.cos(this.angle),
this.y + this.radius * Math.sin(this.angle),
this.angle
);
bullets.push(bullet);
this.canShoot = false;
setTimeout(() => this.canShoot = true, 250); // Cooldown
}
}
handleScreenWrap() {
if (this.x < 0 - this.radius) this.x = canvas.width + this.radius;
if (this.x > canvas.width + this.radius) this.x = 0 - this.radius;
if (this.y < 0 - this.radius) this.y = canvas.height + this.radius;
if (this.y > canvas.height + this.radius) this.y = 0 - this.radius;
}
destroy() {
if (this.isInvincible) return;
lives--;
if (lives > 0) {
ship = new Ship();
} else {
gameOver();
}
}
}
class Bullet {
constructor(x, y, angle) {
this.x = x;
this.y = y;
this.vel = {
x: BULLET_SPEED * Math.cos(angle),
y: BULLET_SPEED * Math.sin(angle)
};
this.radius = 2;
this.lifespan = 80; // frames
}
draw() {
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
}
update() {
this.x += this.vel.x;
this.y += this.vel.y;
this.lifespan--;
this.draw();
}
}
class Asteroid {
constructor(x, y, radius) {
this.x = x || Math.random() * canvas.width;
this.y = y || Math.random() * canvas.height;
this.radius = radius || ASTEROID_SIZE_LARGE;
this.vel = {
x: (Math.random() * ASTEROID_SPEED * 2 - ASTEROID_SPEED),
y: (Math.random() * ASTEROID_SPEED * 2 - ASTEROID_SPEED)
};
this.angle = 0;
this.angleVel = (Math.random() - 0.5) * 0.02;
// Create a jagged shape
this.vertices = [];
for (let i = 0; i < ASTEROID_VERTICES; i++) {
this.vertices.push(Math.random() * ASTEROID_JAG * 2 + 1 - ASTEROID_JAG);
}
}
draw() {
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.beginPath();
let vertAngle = ((Math.PI * 2) / ASTEROID_VERTICES);
ctx.moveTo(
this.x + this.radius * this.vertices[0] * Math.cos(this.angle),
this.y + this.radius * this.vertices[0] * Math.sin(this.angle)
);
for (let i = 1; i < ASTEROID_VERTICES; i++) {
ctx.lineTo(
this.x + this.radius * this.vertices[i] * Math.cos(this.angle + i * vertAngle),
this.y + this.radius * this.vertices[i] * Math.sin(this.angle + i * vertAngle)
);
}
ctx.closePath();
ctx.stroke();
}
update() {
this.x += this.vel.x;
this.y += this.vel.y;
this.angle += this.angleVel;
// Handle screen wrapping
if (this.x < 0 - this.radius) this.x = canvas.width + this.radius;
if (this.x > canvas.width + this.radius) this.x = 0 - this.radius;
if (this.y < 0 - this.radius) this.y = canvas.height + this.radius;
if (this.y > canvas.height + this.radius) this.y = 0 - this.radius;
this.draw();
}
breakup() {
if (this.radius === ASTEROID_SIZE_LARGE) {
asteroids.push(new Asteroid(this.x, this.y, ASTEROID_SIZE_MEDIUM));
asteroids.push(new Asteroid(this.x, this.y, ASTEROID_SIZE_MEDIUM));
updateScore(20);
} else if (this.radius === ASTEROID_SIZE_MEDIUM) {
asteroids.push(new Asteroid(this.x, this.y, ASTEROID_SIZE_SMALL));
asteroids.push(new Asteroid(this.x, this.y, ASTEROID_SIZE_SMALL));
updateScore(50);
} else {
updateScore(100);
}
}
}
class Saucer {
constructor() {
this.x = Math.random() > 0.5 ? 0 - SAUCER_SIZE : canvas.width + SAUCER_SIZE;
this.y = Math.random() * canvas.height;
this.radius = SAUCER_SIZE;
this.vel = {
x: this.x < 0 ? SAUCER_SPEED : -SAUCER_SPEED,
y: 0
};
this.bullets = [];
}
draw() {
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(this.x - this.radius, this.y);
ctx.lineTo(this.x + this.radius, this.y);
ctx.moveTo(this.x - this.radius / 2, this.y - this.radius / 2);
ctx.lineTo(this.x + this.radius / 2, this.y - this.radius / 2);
ctx.moveTo(this.x - this.radius, this.y);
ctx.quadraticCurveTo(this.x, this.y - this.radius, this.x + this.radius, this.y);
ctx.closePath();
ctx.stroke();
}
update() {
this.x += this.vel.x;
this.y += this.vel.y;
// Saucer shoots at player
if (Math.random() < SAUCER_FIRE_RATE && ship) {
const angleToShip = Math.atan2(ship.y - this.y, ship.x - this.x);
const bullet = new Bullet(this.x, this.y, angleToShip);
this.bullets.push(bullet);
}
// Update saucer bullets
for (let i = this.bullets.length - 1; i >= 0; i--) {
this.bullets[i].update();
if (this.bullets[i].lifespan <= 0) {
this.bullets.splice(i, 1);
}
}
this.draw();
}
}
// --- GAME LOGIC ---
function init() {
resizeCanvas();
highScoreEl.textContent = `HIGH: ${highScore}`;
showMessage("ASTEROIDS", "PRESS ENTER TO START");
}
function startGame() {
isPlaying = true;
score = 0;
lives = 3;
updateScore(0);
messageOverlay.style.display = 'none';
ship = new Ship();
// Create initial asteroids
asteroids = [];
for (let i = 0; i < ASTEROID_NUM; i++) {
asteroids.push(new Asteroid());
}
// Start saucer timer
clearTimeout(saucerTimer);
saucerTimer = setTimeout(spawnSaucer, SAUCER_SPAWN_TIME);
gameLoop();
}
function gameOver() {
isPlaying = false;
ship = null;
if (score > highScore) {
highScore = score;
localStorage.setItem('asteroidsHighScore', highScore);
highScoreEl.textContent = `HIGH: ${highScore}`;
}
clearTimeout(saucerTimer);
saucer = null;
showMessage("GAME OVER", "PRESS ENTER TO RESTART");
}
function showMessage(title, subtitle) {
messageTitle.textContent = title;
messageSubtitle.textContent = subtitle;
messageOverlay.style.display = 'block';
}
function spawnSaucer() {
if (isPlaying) {
saucer = new Saucer();
clearTimeout(saucerTimer);
saucerTimer = setTimeout(spawnSaucer, SAUCER_SPAWN_TIME);
}
}
function updateScore(points) {
score += points;
scoreEl.textContent = `SCORE: ${score}`;
}
function checkCollisions() {
// Ship with asteroids
if (ship) {
for (let i = asteroids.length - 1; i >= 0; i--) {
const ast = asteroids[i];
if (isColliding(ship, ast)) {
ship.destroy();
ast.breakup();
asteroids.splice(i, 1);
break; // Prevent multiple collisions in one frame
}
}
}
// Bullets with asteroids
for (let i = bullets.length - 1; i >= 0; i--) {
const bullet = bullets[i];
for (let j = asteroids.length - 1; j >= 0; j--) {
const ast = asteroids[j];
if (isColliding(bullet, ast)) {
ast.breakup();
asteroids.splice(j, 1);
bullets.splice(i, 1);
break; // Bullet can only hit one asteroid
}
}
}
// Saucer logic
if (saucer) {
// Ship bullets with saucer
for (let i = bullets.length - 1; i >= 0; i--) {
if (isColliding(bullets[i], saucer)) {
updateScore(200);
saucer = null;
bullets.splice(i, 1);
break;
}
}
if (saucer) {
// Ship with saucer
if (ship && isColliding(ship, saucer)) {
ship.destroy();
saucer = null;
}
// Saucer bullets with ship
else if (ship) {
for (let i = saucer.bullets.length - 1; i >= 0; i--) {
if(isColliding(ship, saucer.bullets[i])) {
ship.destroy();
saucer.bullets.splice(i, 1);
break;
}
}
}
}
}
}
function isColliding(obj1, obj2) {
const dist = Math.sqrt(Math.pow(obj1.x - obj2.x, 2) + Math.pow(obj1.y - obj2.y, 2));
return dist < obj1.radius + obj2.radius;
}
function drawLives() {
let startX = canvas.width - 60;
for (let i = 0; i < lives; i++) {
ctx.save();
ctx.translate(startX - i * (SHIP_SIZE + 5), 30);
ctx.rotate(degToRad(-90));
ctx.strokeStyle = 'white';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, -SHIP_SIZE / 2);
ctx.lineTo(SHIP_SIZE / 2, SHIP_SIZE / 2);
ctx.lineTo(-SHIP_SIZE / 2, SHIP_SIZE / 2);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
}
function gameLoop() {
if (!isPlaying) return;
// Clear canvas
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Update and draw ship
if (ship) {
ship.update();
}
// Update and draw asteroids
for (let i = 0; i < asteroids.length; i++) {
asteroids[i].update();
}
// Update and draw bullets
for (let i = bullets.length - 1; i >= 0; i--) {
bullets[i].update();
if (bullets[i].lifespan <= 0) {
bullets.splice(i, 1);
}
}
// Update and draw saucer
if (saucer) {
saucer.update();
// Remove saucer if it goes off-screen
if (saucer.x < 0 - saucer.radius || saucer.x > canvas.width + saucer.radius) {
saucer = null;
}
}
// Check for collisions
checkCollisions();
// Draw lives
drawLives();
// Check for level clear
if (asteroids.length === 0) {
lives++;
startGame();
}
requestAnimationFrame(gameLoop);
}
// --- EVENT LISTENERS ---
window.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !isPlaying) {
startGame();
}
keys[e.key] = true;
if (e.key === ' ' && ship) { // Spacebar for shooting
e.preventDefault();
ship.shoot();
}
});
window.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
window.addEventListener('resize', resizeCanvas);
// --- INITIALIZE ---
init();
</script>
</body>
</html>