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