326 lines
8.4 KiB
JavaScript
326 lines
8.4 KiB
JavaScript
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const scoreEl = document.getElementById('score');
|
|
const finalScoreEl = document.getElementById('finalScore');
|
|
const gameOverEl = document.getElementById('gameOver');
|
|
const restartBtn = document.getElementById('restartBtn');
|
|
|
|
// Game State
|
|
let score = 0;
|
|
let gameActive = true;
|
|
let animationId;
|
|
let frames = 0;
|
|
|
|
// Entities
|
|
let bullets = [];
|
|
let particles = [];
|
|
|
|
// Mouse
|
|
const mouse = {
|
|
x: innerWidth / 2,
|
|
y: innerHeight / 2
|
|
};
|
|
|
|
// Resize handling
|
|
function resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
window.addEventListener('resize', resize);
|
|
resize();
|
|
|
|
// Input handling
|
|
window.addEventListener('mousemove', (e) => {
|
|
mouse.x = e.clientX;
|
|
mouse.y = e.clientY;
|
|
});
|
|
|
|
window.addEventListener('touchmove', (e) => {
|
|
mouse.x = e.touches[0].clientX;
|
|
mouse.y = e.touches[0].clientY;
|
|
});
|
|
|
|
// Utility
|
|
function randomRange(min, max) {
|
|
return Math.random() * (max - min) + min;
|
|
}
|
|
|
|
// Classes
|
|
class Heart {
|
|
constructor() {
|
|
this.x = canvas.width / 2;
|
|
this.y = canvas.height / 2;
|
|
this.baseSize = 30;
|
|
this.size = this.baseSize;
|
|
this.pulseSpeed = 0.05;
|
|
this.angle = 0;
|
|
}
|
|
|
|
draw() {
|
|
// Update center position on resize
|
|
this.x = canvas.width / 2;
|
|
this.y = canvas.height / 2;
|
|
|
|
// Pulse animation
|
|
this.angle += this.pulseSpeed;
|
|
const scale = 1 + Math.sin(this.angle) * 0.1;
|
|
|
|
ctx.save();
|
|
ctx.translate(this.x, this.y);
|
|
ctx.scale(scale, scale);
|
|
|
|
// Draw Heart
|
|
ctx.beginPath();
|
|
ctx.fillStyle = '#ff4d4d';
|
|
ctx.shadowColor = '#ff4d4d';
|
|
ctx.shadowBlur = 20;
|
|
|
|
// Heart shape using bezier curves
|
|
// Starting from top center
|
|
ctx.moveTo(0, -10);
|
|
ctx.bezierCurveTo(15, -25, 35, -10, 0, 25);
|
|
ctx.bezierCurveTo(-35, -10, -15, -25, 0, -10);
|
|
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
class Shield {
|
|
constructor() {
|
|
this.radius = 80; // Distance from heart
|
|
this.angle = 0;
|
|
this.width = 60; // Arc length in radians (approx)
|
|
this.thickness = 10;
|
|
this.color = '#4d94ff';
|
|
}
|
|
|
|
update() {
|
|
const dx = mouse.x - canvas.width / 2;
|
|
const dy = mouse.y - canvas.height / 2;
|
|
this.angle = Math.atan2(dy, dx);
|
|
}
|
|
|
|
draw() {
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
|
|
ctx.save();
|
|
ctx.translate(centerX, centerY);
|
|
ctx.rotate(this.angle);
|
|
|
|
ctx.beginPath();
|
|
// Draw an arc for the shield
|
|
// We want the shield to be centered on the angle
|
|
// So we draw from -arcLength/2 to +arcLength/2
|
|
const arcLength = 0.8; // Radians
|
|
ctx.arc(0, 0, this.radius, -arcLength / 2, arcLength / 2);
|
|
|
|
ctx.lineWidth = this.thickness;
|
|
ctx.strokeStyle = this.color;
|
|
ctx.lineCap = 'round';
|
|
ctx.shadowColor = this.color;
|
|
ctx.shadowBlur = 15;
|
|
ctx.stroke();
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
class Bullet {
|
|
constructor() {
|
|
const edge = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left
|
|
|
|
if (edge === 0) { // Top
|
|
this.x = Math.random() * canvas.width;
|
|
this.y = -20;
|
|
} else if (edge === 1) { // Right
|
|
this.x = canvas.width + 20;
|
|
this.y = Math.random() * canvas.height;
|
|
} else if (edge === 2) { // Bottom
|
|
this.x = Math.random() * canvas.width;
|
|
this.y = canvas.height + 20;
|
|
} else { // Left
|
|
this.x = -20;
|
|
this.y = Math.random() * canvas.height;
|
|
}
|
|
|
|
this.radius = 5;
|
|
this.color = '#fff';
|
|
|
|
// Calculate velocity towards center
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
const angle = Math.atan2(centerY - this.y, centerX - this.x);
|
|
|
|
// Speed increases with score
|
|
const speedMultiplier = 1 + (score * 0.01);
|
|
this.velocity = {
|
|
x: Math.cos(angle) * (2 + Math.random()) * speedMultiplier,
|
|
y: Math.sin(angle) * (2 + Math.random()) * speedMultiplier
|
|
};
|
|
}
|
|
|
|
update() {
|
|
this.x += this.velocity.x;
|
|
this.y += this.velocity.y;
|
|
}
|
|
|
|
draw() {
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
|
ctx.fillStyle = this.color;
|
|
ctx.shadowColor = this.color;
|
|
ctx.shadowBlur = 5;
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
class Particle {
|
|
constructor(x, y, color) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.radius = Math.random() * 3;
|
|
this.color = color;
|
|
this.velocity = {
|
|
x: (Math.random() - 0.5) * 5,
|
|
y: (Math.random() - 0.5) * 5
|
|
};
|
|
this.alpha = 1;
|
|
}
|
|
|
|
update() {
|
|
this.x += this.velocity.x;
|
|
this.y += this.velocity.y;
|
|
this.alpha -= 0.02;
|
|
}
|
|
|
|
draw() {
|
|
ctx.save();
|
|
ctx.globalAlpha = this.alpha;
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
|
ctx.fillStyle = this.color;
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
// Game Objects
|
|
const heart = new Heart();
|
|
const shield = new Shield();
|
|
|
|
function spawnBullet() {
|
|
// Spawn rate increases with score
|
|
const spawnRate = Math.max(20, 100 - score);
|
|
if (frames % spawnRate === 0) {
|
|
bullets.push(new Bullet());
|
|
}
|
|
}
|
|
|
|
function checkCollisions() {
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
|
|
bullets.forEach((bullet, index) => {
|
|
// Distance to center (Heart)
|
|
const distToCenter = Math.hypot(bullet.x - centerX, bullet.y - centerY);
|
|
|
|
// 1. Check Shield Collision
|
|
// Shield is at a specific radius. If bullet is at that radius, check angle difference.
|
|
if (distToCenter < shield.radius + shield.thickness && distToCenter > shield.radius - shield.thickness) {
|
|
const angleToBullet = Math.atan2(bullet.y - centerY, bullet.x - centerX);
|
|
|
|
// Normalize angles to -PI to PI
|
|
let angleDiff = angleToBullet - shield.angle;
|
|
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
|
|
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
|
|
|
|
// Shield arc length is approx 0.8 radians. Half is 0.4.
|
|
// Add a bit of buffer for the bullet size
|
|
if (Math.abs(angleDiff) < 0.5) {
|
|
// Hit Shield
|
|
createExplosion(bullet.x, bullet.y, '#4d94ff');
|
|
bullets.splice(index, 1);
|
|
score += 10;
|
|
scoreEl.textContent = `Score: ${score}`;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 2. Check Heart Collision
|
|
if (distToCenter < 30) { // Heart approx radius
|
|
endGame();
|
|
}
|
|
});
|
|
}
|
|
|
|
function createExplosion(x, y, color) {
|
|
for (let i = 0; i < 8; i++) {
|
|
particles.push(new Particle(x, y, color));
|
|
}
|
|
}
|
|
|
|
function endGame() {
|
|
gameActive = false;
|
|
cancelAnimationFrame(animationId);
|
|
gameOverEl.classList.remove('hidden');
|
|
finalScoreEl.textContent = score;
|
|
}
|
|
|
|
function restartGame() {
|
|
score = 0;
|
|
scoreEl.textContent = `Score: 0`;
|
|
bullets = [];
|
|
particles = [];
|
|
gameActive = true;
|
|
gameOverEl.classList.add('hidden');
|
|
animate();
|
|
}
|
|
|
|
restartBtn.addEventListener('click', restartGame);
|
|
|
|
function animate() {
|
|
if (!gameActive) return;
|
|
|
|
animationId = requestAnimationFrame(animate);
|
|
frames++;
|
|
|
|
// Clear with trail effect
|
|
ctx.fillStyle = 'rgba(17, 17, 17, 0.2)';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
heart.draw();
|
|
shield.update();
|
|
shield.draw();
|
|
|
|
spawnBullet();
|
|
|
|
// Update and draw bullets
|
|
bullets.forEach((bullet, index) => {
|
|
bullet.update();
|
|
bullet.draw();
|
|
|
|
// Remove off-screen bullets (shouldn't happen often as they move to center)
|
|
if (bullet.x < -50 || bullet.x > canvas.width + 50 ||
|
|
bullet.y < -50 || bullet.y > canvas.height + 50) {
|
|
bullets.splice(index, 1);
|
|
}
|
|
});
|
|
|
|
// Update and draw particles
|
|
particles.forEach((particle, index) => {
|
|
if (particle.alpha <= 0) {
|
|
particles.splice(index, 1);
|
|
} else {
|
|
particle.update();
|
|
particle.draw();
|
|
}
|
|
});
|
|
|
|
checkCollisions();
|
|
}
|
|
|
|
// Start
|
|
animate();
|