Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Chaos Cannon | MAYHEM EDITION</title> <!-- Changed Title --> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
/* CSS remains the same as before */ | |
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Orbitron:wght@400;700&display=swap'); | |
body { | |
font-family: 'Orbitron', sans-serif; | |
background: linear-gradient(135deg, #111 0%, #222 100%); | |
overflow: hidden; | |
user-select: none; | |
margin: 0; padding: 0; display: flex; | |
min-height: 100vh; align-items: center; justify-content: center; | |
flex-direction: column; | |
} | |
#game-container { position: relative; width: 800px; height: 600px; margin-top: 1rem; } | |
#game { | |
background: radial-gradient(ellipse at center, #1a1a2e 0%, #16213e 100%); | |
box-shadow: 0 0 40px rgba(0, 180, 255, 0.4); /* Enhanced glow */ | |
border-radius: 8px; | |
border: 3px solid rgba(0, 180, 255, 0.6); /* Brighter border */ | |
display: block; cursor: crosshair; | |
} | |
.title-text { font-family: 'Press Start 2P', cursive; text-shadow: 0 0 15px #ff6600, 0 0 25px #ff6600, 0 0 5px #fff; /* Added white core */ letter-spacing: 3px; /* More spacing */ } | |
.btn-glow { box-shadow: 0 0 15px rgba(255, 102, 0, 0.7); transition: all 0.3s ease; } | |
.btn-glow:hover { box-shadow: 0 0 30px rgba(255, 102, 0, 1.0); /* Stronger hover */ transform: translateY(-3px) scale(1.05); /* Add scale */} | |
.btn-glow:active { transform: translateY(1px) scale(1.0); } | |
.score-display { background: rgba(0, 0, 0, 0.75); border: 2px solid rgba(0, 180, 255, 0.7); border-radius: 8px; text-shadow: 0 0 6px #00ff00, 0 0 10px #00ff00; /* Brighter score */ } | |
.game-over { background: rgba(0, 0, 0, 0.9); border: 3px solid rgba(255, 0, 0, 0.7); box-shadow: 0 0 40px rgba(255, 0, 0, 0.6); } | |
.star { position: absolute; background: white; border-radius: 50%; pointer-events: none; } | |
#titleScreen, #gameOverScreen { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background-color: rgba(0, 0, 0, 0.85); border-radius: 8px; z-index: 20; } | |
#titleScreen { opacity: 1; transition: opacity 0.5s ease-out; pointer-events: auto; } | |
#gameOverScreen { opacity: 0; transition: opacity 0.5s ease-in, background-color 0.5s ease-in; pointer-events: none; } | |
#gameOverScreen.visible { opacity: 1; pointer-events: auto; background-color: rgba(0, 0, 0, 0.9); } | |
/* Twinkle animation (no changes needed) */ | |
@keyframes twinkle { from { opacity: 0.1; } to { opacity: ${Math.random() * 0.5 + 0.5}; } } | |
</style> | |
</head> | |
<body class="p-4 text-white"> | |
<div class="absolute top-0 left-0 w-full h-full overflow-hidden z-0" id="stars"></div> | |
<div class="text-center mb-4 z-10"> | |
<!-- Title updated --> | |
<h1 class="title-text text-4xl md:text-5xl mb-2">CHAOS CANNON</h1> | |
<div class="flex justify-center gap-4"> | |
<div class="score-display px-4 py-2 text-xl"> | |
<i class="fas fa-trophy mr-2 text-yellow-400"></i> | |
<span id="highScore">0</span> | |
</div> | |
<div class="score-display px-4 py-2 text-xl"> | |
<i class="fas fa-bolt mr-2 text-orange-500"></i> | |
<span id="streak">0x</span> | |
</div> | |
</div> | |
</div> | |
<div id="game-container" class="z-10"> | |
<canvas id="game" width="800" height="600"></canvas> | |
<div id="titleScreen"> | |
<h2 class="title-text text-5xl mb-8">CHAOS CANNON</h2> | |
<p class="text-xl mb-8 text-orange-400">MAYHEM EDITION</p> <!-- Subtitle --> | |
<button id="startBtn" class="btn-glow bg-orange-600 hover:bg-orange-700 text-white font-bold py-3 px-8 rounded-full text-xl mb-4"> | |
<i class="fas fa-play mr-2"></i>START MAYHEM | |
</button> | |
<div class="text-gray-400 mt-4"> | |
<p class="mb-2"><i class="fas fa-mouse-pointer mr-2"></i>Aim with mouse</p> | |
<p><i class="fas fa-mouse mr-2"></i>Click to SHOOT A LOT</p> | |
</div> | |
</div> | |
<div id="gameOverScreen"> | |
<div class="game-over p-8 rounded-lg text-center max-w-md"> | |
<h2 class="title-text text-4xl text-red-500 mb-6">GAME OVER</h2> | |
<p class="text-2xl mb-2">Your Score:</p> | |
<p id="finalScore" class="text-4xl font-bold text-orange-500 mb-6">0</p> | |
<p class="text-lg mb-6 text-gray-300">The mayhem subsided... for now.</p> | |
<button id="restartBtn" class="btn-glow bg-orange-600 hover:bg-orange-700 text-white font-bold py-3 px-8 rounded-full text-xl"> | |
<i class="fas fa-redo mr-2"></i>RESTART MAYHEM | |
</button> | |
</div> | |
</div> | |
</div> | |
<div class="mt-6 text-gray-400 text-sm z-10"> | |
<p>Made with <i class="fas fa-burn text-orange-500"></i> <!-- Changed icon --> for particle lovers</p> | |
</div> | |
<script> | |
// --- Star Background (Same) --- | |
const starsContainer = document.getElementById('stars'); | |
for (let i = 0; i < 250; i++) { // More stars | |
const star = document.createElement('div'); | |
star.className = 'star'; | |
star.style.width = `${Math.random() * 3.5}px`; // Slightly bigger max | |
star.style.height = star.style.width; | |
star.style.left = `${Math.random() * 100}%`; | |
star.style.top = `${Math.random() * 100}%`; | |
const initialOpacity = Math.random() * 0.7 + 0.1; | |
star.style.opacity = initialOpacity; | |
star.style.animation = `twinkle ${1.5 + Math.random() * 5}s infinite alternate ease-in-out`; // Faster min twinkle | |
starsContainer.appendChild(star); | |
} | |
const style = document.createElement('style'); | |
style.textContent = ` @keyframes twinkle { from { opacity: 0.1; } to { opacity: ${Math.random() * 0.6 + 0.4}; } } `; | |
document.head.appendChild(style); | |
// --- Game Setup (Same) --- | |
const canvas = document.getElementById('game'); | |
const ctx = canvas.getContext('2d'); | |
let audioContext; | |
const titleScreen = document.getElementById('titleScreen'); | |
const gameOverScreen = document.getElementById('gameOverScreen'); | |
const startBtn = document.getElementById('startBtn'); | |
const restartBtn = document.getElementById('restartBtn'); | |
const finalScore = document.getElementById('finalScore'); | |
const highScoreDisplay = document.getElementById('highScore'); | |
const streakDisplay = document.getElementById('streak'); | |
let canvasRect = canvas.getBoundingClientRect(); | |
let highScore = localStorage.getItem('chaosCannonHighScore') || 0; | |
highScoreDisplay.textContent = highScore; | |
let game; | |
let lastTime = 0; | |
let animationFrameId = null; | |
let mousePos = { x: canvas.width / 2, y: 0 }; | |
// --- Audio Context Initialization (Same) --- | |
function initAudioContext() { if (!audioContext && (window.AudioContext || window.webkitAudioContext)) { try { audioContext = new (window.AudioContext || window.webkitAudioContext)(); if (audioContext.state === 'suspended') { audioContext.resume(); } } catch (e) { console.error("Audio Error", e); audioContext = null; } } } | |
document.addEventListener('DOMContentLoaded', initAudioContext); | |
// --- Particle Class (MAYHEM Edition) --- | |
class Particle { | |
constructor(x, y, vx, vy, color, lifespan, size, gravity = 0.05, friction = 0.99, blendMode = 'source-over') { // Added blendMode | |
this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.color = color; | |
this.initialLifespan = lifespan; this.lifespan = lifespan; | |
this.size = size; this.dead = false; this.opacity = 1; | |
this.gravity = gravity; this.friction = friction; | |
this.blendMode = blendMode; // Store blend mode | |
} | |
update(deltaTime) { | |
const dtFactor = Math.min(deltaTime / 16.67, 4); | |
this.x += this.vx * dtFactor; this.y += this.vy * dtFactor; | |
this.vy += this.gravity * dtFactor; | |
// Apply friction less aggressively for more hang time | |
const frictionFactor = Math.pow(this.friction, dtFactor); | |
this.vx *= frictionFactor; this.vy *= frictionFactor; | |
this.lifespan -= deltaTime; | |
// Fade out more slowly initially, then faster | |
this.opacity = Math.max(0, Math.pow(this.lifespan / this.initialLifespan, 0.5)); | |
if (this.lifespan <= 0) this.dead = true; | |
} | |
draw(ctx) { | |
ctx.globalCompositeOperation = this.blendMode; // Set blend mode | |
ctx.fillStyle = this.color; | |
ctx.globalAlpha = this.opacity; | |
ctx.beginPath(); | |
const currentSize = Math.max(0, this.size * (this.opacity * 1.1)); // Size shrinks slightly slower than opacity | |
ctx.arc(this.x, this.y, currentSize, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.globalAlpha = 1.0; | |
ctx.globalCompositeOperation = 'source-over'; // Reset blend mode | |
} | |
} | |
// --- Explosion Class (MAYHEM Edition) --- | |
class Explosion { | |
constructor(x, y, type, game) { | |
this.x = x; this.y = y; this.dead = false; this.type = type; | |
this.game = game; this.duration = 600; // Slightly longer duration | |
const isBoss = type === 'boss'; | |
// MORE POWER | |
const basePower = isBoss ? 15 : 9; | |
const particleMultiplier = isBoss ? 3.0 : 1.8; // MOOORE PARTICLES | |
// 0. Central Flash | |
const flashSize = isBoss ? 60 : 35; | |
game.particles.push(new Particle(this.x, this.y, 0, 0, 'rgba(255, 255, 255, 0.9)', 80, flashSize, 0, 1, 'lighter')); // Short life, big size, lighter blend | |
game.particles.push(new Particle(this.x, this.y, 0, 0, `hsl(${Math.random()*60}, 100%, 70%)`, 120, flashSize * 0.7, 0, 1, 'lighter')); // Colored flash under white | |
// 1. Fireball Core Particles | |
const coreCount = Math.floor(35 * particleMultiplier); // MORE | |
for (let i = 0; i < coreCount; i++) { | |
const angle = Math.random() * Math.PI * 2; | |
const speed = Math.random() * basePower * 1.8 + basePower * 0.6; // Faster max speed | |
const vx = Math.cos(angle) * speed; const vy = Math.sin(angle) * speed; | |
const color = `hsl(${Math.random() * 40 + 10}, 100%, ${65 + Math.random() * 35}%)`; // Brighter range | |
const lifespan = Math.random() * 300 + 150; // Longer short lifespan | |
const size = Math.random() * (isBoss ? 12 : 8) + 5; | |
game.particles.push(new Particle(this.x, this.y, vx, vy, color, lifespan, size, 0.03, 0.96, 'lighter')); // Lighter blend for core | |
} | |
// 2. Sparks / Debris Particles | |
const sparkCount = Math.floor(60 * particleMultiplier); // MORE | |
for (let i = 0; i < sparkCount; i++) { | |
const angle = Math.random() * Math.PI * 2; | |
const speed = Math.random() * basePower * 1.2; // Slightly faster debris | |
const vx = Math.cos(angle) * speed; const vy = Math.sin(angle) * speed - Math.random() * 1.5; | |
const color = `hsl(${Math.random() * 45}, 100%, ${55 + Math.random() * 25}%)`; // More vibrant reds/oranges | |
const lifespan = Math.random() * 600 + 400; // Longer medium lifespan | |
const size = Math.random() * (isBoss ? 5 : 4) + 1.5; | |
game.particles.push(new Particle(this.x, this.y, vx, vy, color, lifespan, size, 0.06, 0.97)); // More friction | |
} | |
// 3. Smoke Particles | |
const smokeCount = Math.floor(45 * particleMultiplier); // MORE | |
for (let i = 0; i < smokeCount; i++) { | |
const angle = Math.random() * Math.PI * 2; | |
const speed = Math.random() * basePower * 0.5 + 0.5; // Slightly faster smoke base | |
const vx = Math.cos(angle) * speed + (Math.random() - 0.5) * 1.5; // More drift | |
const vy = Math.sin(angle) * speed - Math.random() * 0.8 - 0.3; // Stronger upward drift | |
const gray = 10 + Math.random() * 50; // Darker range | |
const color = `rgba(${gray}, ${gray}, ${gray+5}, ${0.5 + Math.random() * 0.4})`; // More opaque max | |
const lifespan = Math.random() * 1500 + 1000; // Even longer lifespan | |
const size = Math.random() * (isBoss ? 18 : 12) + 8; // Bigger smoke | |
game.particles.push(new Particle(this.x, this.y, vx, vy, color, lifespan, size, -0.015, 0.95)); // More anti-grav, more friction | |
} | |
} | |
update(game, deltaTime) { this.duration -= deltaTime; if (this.duration <= 0) this.dead = true; } | |
draw(ctx) { /* Only particles draw */ } | |
} | |
// --- Game Class (MAYHEM Edition) --- | |
class Game { | |
// Constructor remains largely the same | |
constructor() { | |
this.state = 'title'; this.score = 0; this.shake = 0; this.turret = new Turret(); | |
this.projectiles = []; this.enemies = []; this.particles = []; this.explosions = []; this.powerUps = []; | |
this.streak = 0; this.lastHitTime = 0; this.stars = []; this.enemySpawnTimer = 0; this.powerUpSpawnTimer = 0; | |
this.enemySpawnInterval = 1600; // Slightly faster initial spawn | |
this.powerUpSpawnInterval = 9000; // Faster powerups | |
this.difficultyTimer = 0; | |
this.boundClickHandler = this.clickHandler.bind(this); | |
this.boundMouseMoveHandler = this.mouseMoveHandler.bind(this); | |
canvas.addEventListener('click', this.boundClickHandler); | |
canvas.addEventListener('mousemove', this.boundMouseMoveHandler); | |
startBtn.addEventListener('click', () => this.start()); | |
restartBtn.addEventListener('click', () => this.restart()); | |
for(let i = 0; i < 70; i++) { // More background stars | |
this.stars.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, size: Math.random() * 2.5 + 0.5, opacity: Math.random() * 0.5 + 0.1, speed: Math.random() * 0.15 + 0.05 }); // Slightly faster stars | |
} | |
} | |
// clickHandler, mouseMoveHandler, start, restart (remain the same) | |
clickHandler(e) { if (!audioContext) initAudioContext(); if (audioContext && audioContext.state === 'suspended') { audioContext.resume(); } if (this.state === 'playing') { this.turret.shoot(this); } } | |
mouseMoveHandler(e) { canvasRect = canvas.getBoundingClientRect(); mousePos.x = e.clientX - canvasRect.left; mousePos.y = e.clientY - canvasRect.top; const MIN_ANGLE = -Math.PI * 0.95; const MAX_ANGLE = -Math.PI * 0.05; let targetAngle = Math.atan2(mousePos.y - this.turret.y, mousePos.x - this.turret.x); this.turret.angle = Math.max(MIN_ANGLE, Math.min(MAX_ANGLE, targetAngle)); } | |
start() { if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } if (!audioContext) initAudioContext(); if (audioContext && audioContext.state === 'suspended') { audioContext.resume(); } this.state = 'playing'; this.score = 0; this.streak = 0; this.lastHitTime = 0; this.enemySpawnTimer = 0; this.powerUpSpawnTimer = 0; this.enemySpawnInterval = 1600; this.powerUpSpawnInterval = 9000; this.difficultyTimer = 0; this.shake = 0; this.turret = new Turret(); this.projectiles = []; this.enemies = []; this.particles = []; this.explosions = []; this.powerUps = []; this.updateScoreDisplay(); streakDisplay.textContent = `0x`; titleScreen.style.opacity = '0'; titleScreen.style.pointerEvents = 'none'; gameOverScreen.classList.remove('visible'); lastTime = performance.now(); this.animate(); } | |
restart() { this.start(); } | |
spawnEnemies(deltaTime) { | |
this.enemySpawnTimer += deltaTime; | |
if (this.enemySpawnTimer >= this.enemySpawnInterval) { | |
this.enemySpawnTimer -= this.enemySpawnInterval; | |
const isBoss = Math.random() < (0.12 + this.difficultyTimer / 250000); // Faster boss scaling | |
const type = isBoss ? 'boss' : 'normal'; | |
const edgeBuffer = type === 'boss' ? 50 : 30; | |
// Faster base speed, faster scaling | |
const speed = (isBoss ? 0.8 : 1.2) * (Math.random() * 1.8 + 1.0 + (this.difficultyTimer / 60000)); | |
const newEnemy = new Enemy( Math.random() * (canvas.width - edgeBuffer * 2) + edgeBuffer, -60, speed, type ); | |
this.enemies.push(newEnemy); | |
} | |
} | |
spawnPowerUps(deltaTime) { | |
this.powerUpSpawnTimer += deltaTime; | |
if (this.powerUpSpawnTimer >= this.powerUpSpawnInterval) { | |
this.powerUpSpawnTimer = 0; | |
if(Math.random() > 0.4) { // Even more powerups! | |
const edgeBuffer = 40; | |
this.powerUps.push(new PowerUp( Math.random() * (canvas.width - edgeBuffer * 2) + edgeBuffer, -30, ['rapid', 'explosive', 'multi'][Math.floor(Math.random() * 3)] )); | |
} | |
} | |
} | |
increaseDifficulty(deltaTime) { | |
this.difficultyTimer += deltaTime; | |
const decreaseFactor = 1 - (deltaTime / 200000); // Faster difficulty scaling (interval decrease) | |
this.enemySpawnInterval = Math.max(250, this.enemySpawnInterval * decreaseFactor); // Lower min interval (0.25s) | |
} | |
endGame() { if (this.state === 'gameover') return; this.state = 'gameover'; this.playGameOverSound(); if(this.score > highScore) { highScore = this.score; localStorage.setItem('chaosCannonHighScore', highScore); highScoreDisplay.textContent = highScore; } finalScore.textContent = this.score; gameOverScreen.classList.add('visible'); } | |
update(deltaTime) { | |
if(this.state !== 'playing') return; | |
this.increaseDifficulty(deltaTime); | |
this.spawnEnemies(deltaTime); | |
this.spawnPowerUps(deltaTime); | |
// Update entities (pass deltaTime) - Turret needs it for idle particles now | |
this.turret.update(deltaTime, this); // Pass game reference for particles | |
[...this.projectiles, ...this.enemies, ...this.particles, ...this.explosions, ...this.powerUps].forEach(e => e.update(this, deltaTime)); | |
streakDisplay.textContent = `${this.streak}x`; | |
if(this.streak > 0 && Date.now() - this.lastHitTime > 3000) this.streak = 0; | |
// --- Collision Detection --- | |
for (let i = this.projectiles.length - 1; i >= 0; i--) { | |
const projectile = this.projectiles[i]; | |
if (projectile.dead) continue; | |
for (let j = this.enemies.length - 1; j >= 0; j--) { | |
const enemy = this.enemies[j]; | |
const enemyRadius = enemy.type === 'boss' ? 40 : 20; | |
if (Math.hypot(projectile.x - enemy.x, projectile.y - enemy.y) < (enemyRadius + projectile.size)) { | |
enemy.health--; // Health reduced earlier, easier enemies! | |
if (enemy.health <= 0) { | |
// Enemy destroyed | |
if (Date.now() - this.lastHitTime < 3000) this.streak++; else this.streak = 1; | |
this.lastHitTime = Date.now(); | |
this.explosions.push(new Explosion(enemy.x, enemy.y, enemy.type, this)); // MAYHEM explosion | |
const basePoints = enemy.type === 'boss' ? 250 : 50; // Less points needed as they die faster | |
this.score += Math.round(basePoints * (1 + this.streak * 0.20)); // Higher streak bonus | |
this.updateScoreDisplay(); | |
this.enemies.splice(j, 1); | |
this.shake = enemy.type === 'boss' ? 25 : 12; // MORE SHAKE | |
this.playExplosionSound(enemy.type); | |
if(enemy.type === 'boss' && Math.random() > 0.3) { // Very high powerup drop chance | |
this.powerUps.push(new PowerUp( enemy.x, enemy.y, ['rapid', 'explosive', 'multi'][Math.floor(Math.random() * 3)] )); | |
} | |
} else { | |
// --- Enemy Hit Spark Particles --- | |
this.playHitSound(); | |
this.shake = 6; // Keep small hit shake | |
const impactAngle = Math.atan2(projectile.y - enemy.y, projectile.x - enemy.x); | |
for (let k = 0; k < 5 + Math.random() * 5; k++) { // 5-10 sparks | |
const angle = impactAngle + (Math.random() - 0.5) * 1.5; // Spread sparks | |
const speed = Math.random() * 3 + 1; | |
const vx = Math.cos(angle) * speed; | |
const vy = Math.sin(angle) * speed; | |
const color = `hsl(${Math.random()*20 + 20}, 80%, ${50 + Math.random()*20}%)`; // Orange/Yellow sparks | |
const lifespan = Math.random() * 150 + 50; // Short life | |
const size = Math.random() * 2 + 0.5; | |
this.particles.push(new Particle(projectile.x, projectile.y, vx, vy, color, lifespan, size, 0.1, 0.96)); // More gravity | |
} | |
} | |
if (projectile.explosive) { | |
this.explosions.push(new Explosion(projectile.x, projectile.y, 'normal', this)); // Explosive projectiles also get MAYHEM explosions | |
this.playExplosionSound('normal'); | |
} | |
projectile.dead = true; | |
break; | |
} | |
} | |
} | |
// Powerup vs Turret (Same logic) | |
for (let i = this.powerUps.length - 1; i >= 0; i--) { const powerUp = this.powerUps[i]; if (Math.hypot(powerUp.x - this.turret.x, powerUp.y - (this.turret.y + 10)) < (30 + powerUp.size / 2)) { this.turret.activatePowerUp(powerUp.type); this.powerUps.splice(i, 1); this.playPowerUpSound(); this.score += 100; this.updateScoreDisplay(); } } | |
// --- Cleanup --- Filter dead entities | |
this.projectiles = this.projectiles.filter(p => !p.dead && p.y > -150 && p.y < canvas.height + 150 && p.x > -150 && p.x < canvas.width + 150); // Even wider bounds | |
this.particles = this.particles.filter(p => !p.dead); | |
// Limit total particles if needed (optional performance safeguard) | |
// const MAX_PARTICLES = 3000; | |
// if (this.particles.length > MAX_PARTICLES) { | |
// this.particles.splice(0, this.particles.length - MAX_PARTICLES); | |
// } | |
this.explosions = this.explosions.filter(e => !e.dead); | |
this.powerUps = this.powerUps.filter(p => !p.dead); | |
// Update in-game stars (Same) | |
this.stars.forEach(star => { star.y += star.speed * (deltaTime / 16.67); if (star.y > canvas.height) { star.y = -star.size; star.x = Math.random() * canvas.width; } }); | |
} | |
// Draw method (minor adjustments for mayhem feel) | |
draw() { | |
ctx.save(); | |
if(this.shake > 0) { const shakeX = Math.random() * this.shake - this.shake / 2; const shakeY = Math.random() * this.shake - this.shake / 2; ctx.translate(shakeX, shakeY); this.shake *= 0.92; if (this.shake < 0.5) this.shake = 0; } | |
// Background Color | |
ctx.fillStyle = '#080814'; // Even darker | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
// Background Moving Stars | |
this.stars.forEach(star => { ctx.fillStyle = `rgba(255, 255, 255, ${star.opacity * 1.2})`; ctx.fillRect(star.x, star.y, star.size, star.size); }); // Brighter stars | |
// Grid Lines (Fainter) | |
ctx.strokeStyle = 'rgba(0, 150, 255, 0.06)'; ctx.lineWidth = 0.8; | |
for(let x = 0; x < canvas.width; x += 50) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); } | |
for(let y = 0; y < canvas.height; y += 50) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); } | |
// Game Entities (Order matters for layering) | |
this.powerUps.forEach(p => p.draw(ctx)); | |
this.turret.draw(ctx); | |
this.enemies.forEach(e => e.draw(ctx)); | |
this.projectiles.forEach(p => p.draw(ctx)); | |
this.particles.forEach(p => p.draw(ctx)); // Draw ALL particles last | |
ctx.restore(); // Restore before UI | |
// --- UI (Drawn after restoring context) --- | |
// Score Box | |
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.strokeStyle = 'rgba(0, 180, 255, 0.7)'; ctx.lineWidth = 2; | |
ctx.strokeRect(10, 10, 200, 40); ctx.fillRect(10, 10, 200, 40); | |
ctx.font = '18px Orbitron'; ctx.fillStyle = '#00ff00'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; | |
ctx.shadowColor = '#0f0'; ctx.shadowBlur = 5; // Add glow to score text | |
ctx.fillText(`SCORE: ${this.score}`, 20, 30); | |
ctx.shadowBlur = 0; // Reset shadow | |
// Powerup Timer (minor visual tweaks) | |
if(this.turret.powerUpActive) { const remaining = (this.turret.powerUpEnd - Date.now()) / 1000; if(remaining > 0) { const powerColor = this.turret.getPowerUpColor(); ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.strokeStyle = powerColor; ctx.lineWidth = 2.5; /* Thicker border */ const timerWidth = 220; const timerHeight = 30; const timerX = canvas.width - timerWidth - 10; const timerY = 10; ctx.strokeRect(timerX, timerY, timerWidth, timerHeight); ctx.fillRect(timerX, timerY, timerWidth, timerHeight); ctx.fillStyle = powerColor; ctx.font = '14px Orbitron'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.shadowColor = powerColor; ctx.shadowBlur = 8; /* Glow for timer text */ if (remaining < 3 && Math.floor(Date.now() / 150) % 2 === 0) { ctx.fillStyle = '#FFFFFF'; ctx.shadowColor = '#FFF'; } /* Faster flash */ ctx.fillText(`${this.turret.powerUpType.toUpperCase()} ACTIVE: ${remaining.toFixed(1)}s`, timerX + 10, timerY + timerHeight / 2); ctx.shadowBlur = 0; } } | |
} | |
// updateScoreDisplay, Sound Effects, animate, drawStaticBackground, drawUI (remain same structure) | |
updateScoreDisplay() { /* Optional direct DOM update */ } | |
_playSound(oscType, freqStart, freqEnd, gainVal, duration, filterType = null, filterFreqStart = 20000, filterFreqEnd = 20000) { if (!audioContext || audioContext.state !== 'running') return; const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); let lastNode = oscillator; if (filterType) { const filter = audioContext.createBiquadFilter(); filter.type = filterType; filter.frequency.setValueAtTime(filterFreqStart, audioContext.currentTime); filter.frequency.exponentialRampToValueAtTime(filterFreqEnd, audioContext.currentTime + duration); oscillator.connect(filter); lastNode = filter; } lastNode.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.type = oscType; oscillator.frequency.setValueAtTime(freqStart, audioContext.currentTime); if (freqStart !== freqEnd) { oscillator.frequency.exponentialRampToValueAtTime(freqEnd, audioContext.currentTime + duration * 0.8); } gainNode.gain.setValueAtTime(gainVal, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.0001, audioContext.currentTime + duration); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + duration); } | |
playShootSound() { this._playSound('triangle', 700, 120, 0.10, 0.12); } // Slightly adjusted pitch/vol | |
playHitSound() { this._playSound('square', 500, 400, 0.06, 0.08); } // Lower pitch hit | |
playPowerUpSound() { this._playSound('sine', 440, 1300, 0.18, 0.4); } | |
playGameOverSound() { this._playSound('sawtooth', 100, 40, 0.3, 1.2); this._playSound('square', 90, 35, 0.3, 1.2); } | |
playExplosionSound(type) { const isBoss = type === 'boss'; this._playSound(isBoss ? 'noise' : 'square', isBoss ? 50 : 80 + Math.random() * 50, isBoss ? 20 : 40, isBoss ? 0.35 : 0.18, isBoss ? 0.6 : 0.45, 'lowpass', isBoss ? 1800 : 3500, 80); } // Use 'noise' for boss, louder/longer, lower filter end | |
animate() { animationFrameId = requestAnimationFrame((ts) => this.animate(ts)); const now = performance.now(); const deltaTime = Math.min(now - lastTime, 100); lastTime = now; ctx.clearRect(0, 0, canvas.width, canvas.height); if (this.state === 'playing') { this.update(deltaTime); this.draw(); } else if (this.state === 'gameover') { this.drawStaticBackground(); this.particles.forEach(p => p.update(this, deltaTime)); this.particles = this.particles.filter(p => !p.dead); this.particles.forEach(p => p.draw(ctx)); this.turret.draw(ctx); this.drawUI(); } else if (this.state === 'title') { this.drawStaticBackground(); } } | |
drawStaticBackground() { ctx.save(); ctx.fillStyle = '#080814'; ctx.fillRect(0, 0, canvas.width, canvas.height); this.stars.forEach(star => { ctx.fillStyle = `rgba(255, 255, 255, ${star.opacity * 1.2})`; ctx.fillRect(star.x, star.y, star.size, star.size); }); ctx.strokeStyle = 'rgba(0, 150, 255, 0.06)'; ctx.lineWidth = 0.8; for (let x = 0; x < canvas.width; x += 50) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); } for (let y = 0; y < canvas.height; y += 50) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); } ctx.restore(); } | |
drawUI() { ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.strokeStyle = 'rgba(0, 180, 255, 0.7)'; ctx.lineWidth = 2; ctx.strokeRect(10, 10, 200, 40); ctx.fillRect(10, 10, 200, 40); ctx.font = '18px Orbitron'; ctx.fillStyle = '#00ff00'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.shadowColor = '#0f0'; ctx.shadowBlur = 5; ctx.fillText(`SCORE: ${this.score}`, 20, 30); ctx.shadowBlur = 0; } | |
} | |
// --- Turret Class (MAYHEM Edition) --- | |
class Turret { | |
// Constructor and angle setter/getter same | |
constructor() { this.x = canvas.width / 2; this.y = canvas.height - 30; this.angle = -Math.PI / 2; this.cooldown = 0; this.powerUpActive = false; this.powerUpType = null; this.powerUpEnd = 0; this.barrelLength = 45; this.recoil = 0; this.targetAngle = -Math.PI / 2; this.aimSpeed = 0.2; this._currentAngle = -Math.PI/2; this.idleParticleTimer = 0; } // Add idle timer | |
set angle(target) { this.targetAngle = target; } | |
get angle() { return this._currentAngle || -Math.PI / 2; } | |
update(deltaTime, game) { // Added game reference | |
const current = this.angle; const target = this.targetAngle; let diff = target - current; while (diff < -Math.PI) diff += Math.PI * 2; while (diff > Math.PI) diff -= Math.PI * 2; | |
this._currentAngle = current + diff * this.aimSpeed * (deltaTime / 16.67); | |
if (this.cooldown > 0) this.cooldown -= deltaTime; | |
if (this.recoil > 0) this.recoil -= deltaTime * 0.4; // Even faster recoil recovery | |
if (this.recoil < 0) this.recoil = 0; | |
if(this.powerUpActive && Date.now() > this.powerUpEnd) { this.powerUpActive = false; this.powerUpType = null; } | |
// --- Idle Turret Particles --- | |
this.idleParticleTimer += deltaTime; | |
const idleInterval = this.powerUpActive ? 30 : 150; // Faster sparks when powered up | |
if (this.idleParticleTimer > idleInterval) { | |
this.idleParticleTimer -= idleInterval; | |
const angle = Math.random() * Math.PI * 2; // Emit from base | |
const speed = Math.random() * 0.5 + 0.1; | |
const vx = Math.cos(angle) * speed; | |
const vy = Math.sin(angle) * speed; | |
const color = this.powerUpActive ? this.getPowerUpColor() + '80' : 'rgba(100, 150, 255, 0.5)'; // Use powerup color or default blue | |
const lifespan = Math.random() * 300 + 200; | |
const size = Math.random() * 1.5 + 0.5; | |
game.particles.push(new Particle(this.x + Math.cos(angle) * 20, this.y + Math.sin(angle) * 10, vx, vy, color, lifespan, size, 0.01, 0.98)); | |
} | |
} | |
shoot(game) { | |
if(this.cooldown <= 0) { | |
game.playShootSound(); | |
this.recoil = 10; // More recoil visual | |
const shootAngle = this.angle; | |
const muzzleOffsetX = Math.cos(shootAngle) * (this.barrelLength - this.recoil); | |
const muzzleOffsetY = Math.sin(shootAngle) * (this.barrelLength - this.recoil); | |
const startX = this.x + muzzleOffsetX; | |
const startY = this.y + muzzleOffsetY; | |
// --- Muzzle Flash Particles --- | |
const flashCount = this.powerUpActive && this.powerUpType === 'explosive' ? 25 : 15; // More flash for explosive | |
const flashPower = this.powerUpActive && this.powerUpType === 'explosive' ? 6 : 4; | |
const flashBaseColor = this.powerUpActive && this.powerUpType === 'explosive' ? 15 : 40; // Orange/Red for explosive, Yellow otherwise | |
for (let i = 0; i < flashCount; i++) { | |
const angle = shootAngle + (Math.random() - 0.5) * 0.8; // Cone shape | |
const speed = Math.random() * flashPower + 1.0; | |
const vx = Math.cos(angle) * speed; | |
const vy = Math.sin(angle) * speed; | |
const color = `hsl(${flashBaseColor + Math.random()*20 - 10}, 100%, ${70 + Math.random()*30}%)`; // Bright yellow/orange/white | |
const lifespan = Math.random() * 80 + 40; // Very short life | |
const size = Math.random() * 4 + 1; | |
game.particles.push(new Particle(startX, startY, vx, vy, color, lifespan, size, 0.02, 0.92, 'lighter')); // Lighter blend | |
} | |
// --- Projectile Spawning --- | |
const projectileSpeed = 14; // Faster projectiles | |
const vx = Math.cos(shootAngle) * projectileSpeed; | |
const vy = Math.sin(shootAngle) * projectileSpeed; | |
const isExplosive = this.powerUpActive && this.powerUpType === 'explosive'; | |
if(this.powerUpActive && this.powerUpType === 'multi') { | |
const spreadAngle = 0.2; // Even wider multi | |
for(let i = -1; i <= 1; i++) { | |
const angle = shootAngle + (i * spreadAngle); | |
game.projectiles.push(new Projectile( startX, startY, Math.cos(angle) * projectileSpeed, Math.sin(angle) * projectileSpeed, isExplosive )); | |
} | |
this.cooldown = 250; // Keep multi cooldown reasonable | |
} else { | |
game.projectiles.push(new Projectile(startX, startY, vx, vy, isExplosive)); | |
// Much faster firing rate! | |
this.cooldown = this.powerUpActive && this.powerUpType === 'rapid' ? 40 : 100; // RAPID FIRE! | |
} | |
} | |
} | |
// activatePowerUp, getPowerUpColor, draw (remain same structure, visuals already good) | |
activatePowerUp(type) { this.powerUpActive = true; this.powerUpType = type; this.powerUpEnd = Date.now() + 9000; } // Longer powerup duration | |
getPowerUpColor() { switch(this.powerUpType) { case 'rapid': return '#00ffff'; case 'explosive': return '#ff3300'; case 'multi': return '#ffff00'; default: return '#ffffff'; } } | |
draw(ctx) { ctx.save(); ctx.translate(this.x, this.y); ctx.fillStyle = 'rgba(60, 60, 80, 0.9)'; ctx.strokeStyle = 'rgba(100, 100, 120, 1)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-50, 20); ctx.lineTo(50, 20); ctx.arc(0, 20, 50, 0, Math.PI, false); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.rotate(this.angle); const barrelX = -this.recoil; const gradient = ctx.createLinearGradient(barrelX, -6, barrelX + this.barrelLength, 6); gradient.addColorStop(0, '#AAA'); gradient.addColorStop(0.5, '#888'); gradient.addColorStop(1, '#777'); ctx.fillStyle = gradient; ctx.strokeStyle = '#555'; ctx.lineWidth = 1; ctx.beginPath(); ctx.rect(barrelX, -6, this.barrelLength, 12); ctx.fill(); ctx.stroke(); const baseGradient = ctx.createRadialGradient(0, 0, 5, 0, 0, 25); baseGradient.addColorStop(0, '#888'); baseGradient.addColorStop(1, '#555'); ctx.fillStyle = baseGradient; ctx.strokeStyle = '#444'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, 25, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); const coreRadius = 12; let powerColor = '#555'; let glow = false; if(this.powerUpActive) { powerColor = this.getPowerUpColor(); glow = true; } ctx.fillStyle = powerColor; ctx.beginPath(); ctx.arc(0, 0, coreRadius, 0, Math.PI * 2); ctx.fill(); if (glow) { ctx.shadowColor = powerColor; ctx.shadowBlur = 30; /* More intense glow */ ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; ctx.beginPath(); ctx.arc(0, 0, coreRadius * 0.6, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; } ctx.restore(); } | |
} | |
// --- Projectile Class (MAYHEM Edition) --- | |
class Projectile { | |
constructor(x, y, vx, vy, explosive) { | |
this.x = x; this.y = y; this.vx = vx; this.vy = vy; | |
this.dead = false; this.explosive = explosive; | |
this.size = explosive ? 8 : 5; // Slightly larger base size | |
this.trailTimer = 0; | |
this.trailInterval = 8; // FASTER trail emission | |
} | |
update(game, deltaTime) { | |
const dtFactor = Math.min(deltaTime / 16.67, 4); | |
this.x += this.vx * dtFactor; this.y += this.vy * dtFactor; | |
if(this.y < -150 || this.y > canvas.height + 150 || this.x < -150 || this.x > canvas.width + 150) this.dead = true; | |
this.trailTimer += deltaTime; | |
if (this.trailTimer >= this.trailInterval) { | |
this.trailTimer -= this.trailInterval; | |
const trailColor = this.explosive ? | |
`hsl(${Math.random() * 25 + 2}, 100%, ${65 + Math.random() * 25}%)` : // More intense red/orange | |
`hsl(${Math.random() * 25 + 30}, 100%, ${75 + Math.random() * 25}%)`; // Brighter yellow/orange | |
game.particles.push(new Particle( | |
this.x + (Math.random()-0.5)*this.size, // Emit from within projectile area | |
this.y + (Math.random()-0.5)*this.size, | |
this.vx * -0.05 + Math.random() * 1 - 0.5, // Less opposing velocity | |
this.vy * -0.05 + Math.random() * 1 - 0.5, | |
trailColor, | |
200 + Math.random() * 150, // Longer trail lifespan | |
this.explosive ? 5 : 3, // Slightly larger trail particles | |
0.01, 0.97, 'lighter' // Use lighter blend for trails too | |
)); | |
} | |
} | |
// Draw method (same structure, lighter blend will make it brighter) | |
draw(ctx) { | |
const coreColor = this.explosive ? '#ffeecc' : '#ffffdd'; | |
const outerColor1 = this.explosive ? '#ff6600' : '#ffaa00'; | |
const outerColor2 = this.explosive ? '#cc2200' : '#dd6600'; | |
const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size); | |
gradient.addColorStop(0, coreColor); gradient.addColorStop(0.4, outerColor1); gradient.addColorStop(1, outerColor2); | |
ctx.globalCompositeOperation = 'lighter'; // Draw projectile with lighter blend | |
ctx.fillStyle = gradient; | |
ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); | |
ctx.shadowColor = this.explosive ? '#ff3300' : '#ff9900'; | |
ctx.shadowBlur = this.explosive ? 22 : 15; // More glow | |
ctx.fill(); // Draw again with shadow enabled | |
ctx.shadowBlur = 0; | |
ctx.globalCompositeOperation = 'source-over'; // Reset blend mode | |
} | |
} | |
// --- Enemy Class (MAYHEM Edition - Health Change!) --- | |
class Enemy { | |
constructor(x, y, speed, type) { | |
this.x = x; this.y = y; this.speed = speed; this.type = type || 'normal'; | |
// --- HEALTH CHANGE --- | |
this.initialHealth = type === 'boss' ? 3 : 1; // BOSS: 3 HP, NORMAL: 1 HP | |
this._health = this.initialHealth; // Initialize private health | |
// -------------------- | |
this.wobble = Math.random() * Math.PI * 2; this.wobbleSpeed = type === 'boss' ? 0.05 : 0.08; // Faster wobble | |
this.wobbleAmount = type === 'boss' ? 8 : 5; // More wobble | |
this.angle = 0; this.hitTimer = 0; this.hitDuration = 80; // Shorter hit flash | |
} | |
set health(value) { if (value < this._health) { this.hitTimer = this.hitDuration; } this._health = value; } | |
get health() { return this._health; } // No need for default here anymore | |
// Update method (same logic, checks remain) | |
update(game, deltaTime) { const dtFactor = Math.min(deltaTime / 16.67, 4); this.y += this.speed * dtFactor; this.wobble += this.wobbleSpeed * dtFactor; const wobbleOffset = Math.sin(this.wobble) * this.wobbleAmount; this.x += wobbleOffset * 0.1 * dtFactor; if (this.hitTimer > 0) { this.hitTimer -= deltaTime; } const radius = this.type === 'boss' ? 40 : 20; this.x = Math.max(radius, Math.min(canvas.width - radius, this.x)); if(this.y > canvas.height + radius * 2) { if (this.type !== 'boss') { game.endGame(); } else { const index = game.enemies.indexOf(this); if (index > -1) game.enemies.splice(index, 1); } } } | |
// Draw method (same structure, flash effect remains) | |
draw(ctx) { ctx.save(); const wobbleOffsetY = Math.sin(this.wobble) * this.wobbleAmount * 0.5; ctx.translate(this.x, this.y + wobbleOffsetY); ctx.rotate(this.angle); const isHit = this.hitTimer > 0; if(this.type === 'boss') { const width = 80; const height = 55; const gradient = ctx.createRadialGradient(0, 0, 10, 0, 0, width / 2); gradient.addColorStop(0, isHit ? '#ffffff' : '#ff4444'); gradient.addColorStop(0.6, '#cc0000'); gradient.addColorStop(1, '#880000'); ctx.fillStyle = gradient; ctx.strokeStyle = '#660000'; ctx.lineWidth = 2; ctx.beginPath(); ctx.ellipse(0, 0, width / 2, height / 2, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = isHit ? '#ffcccc' : '#ffff00'; ctx.beginPath(); ctx.ellipse(-18, -8, 10, 6, -0.2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(18, -8, 10, 6, 0.2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000000'; ctx.beginPath(); ctx.arc(-18, -8, 3, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(18, -8, 3, 0, Math.PI * 2); ctx.fill(); const healthBarWidth = 60; const healthBarHeight = 8; const healthBarY = -(height / 2) - 15; ctx.fillStyle = 'rgba(80, 80, 80, 0.7)'; ctx.fillRect(-healthBarWidth / 2, healthBarY, healthBarWidth, healthBarHeight); const healthPercent = Math.max(0, this.health / this.initialHealth); ctx.fillStyle = healthPercent > 0.66 ? '#00ff00' : healthPercent > 0.33 ? '#ffff00' : '#ff0000'; /* Adjusted thresholds for lower HP */ ctx.fillRect(-healthBarWidth / 2, healthBarY, healthBarWidth * healthPercent, healthBarHeight); ctx.strokeStyle = 'rgba(200, 200, 200, 0.8)'; ctx.lineWidth = 1; ctx.strokeRect(-healthBarWidth / 2, healthBarY, healthBarWidth, healthBarHeight); } else { const size = 20; const gradient = ctx.createLinearGradient(0, -size * 0.7, 0, size * 0.3); gradient.addColorStop(0, isHit ? '#ffffff' : '#33dd33'); gradient.addColorStop(1, '#008800'); ctx.fillStyle = gradient; ctx.strokeStyle = '#005500'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(0, -size * 0.7); ctx.lineTo(-size, size * 0.3); ctx.lineTo(size, size * 0.3); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = isHit ? 'rgba(255, 100, 100, 0.9)' : 'rgba(0, 200, 255, 0.6)'; ctx.shadowColor = isHit ? 'rgba(255, 0, 0, 1)' : 'rgba(0, 200, 255, 1)'; ctx.shadowBlur = 8; ctx.beginPath(); ctx.arc(0, -size * 0.1, size * 0.3, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; } ctx.restore(); } | |
} | |
// --- PowerUp Class (Same structure, visuals already good) --- | |
class PowerUp { constructor(x, y, type) { this.x = x; this.y = y; this.type = type; this.vy = 1.8; /* Slightly faster fall */ this.dead = false; this.size = 18; this.angle = Math.random() * Math.PI * 2; this.rotationSpeed = (Math.random() - 0.5) * 0.05; this.pulseTimer = Math.random() * 1000; } update(game, deltaTime) { const dtFactor = Math.min(deltaTime / 16.67, 4); this.y += this.vy * dtFactor; this.angle += this.rotationSpeed * dtFactor; this.pulseTimer += deltaTime; if(this.y > canvas.height + this.size * 2) this.dead = true; } draw(ctx) { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle); let color1, color2, symbolFunc; const iconSize = this.size * 0.8; switch(this.type) { case 'rapid': color1 = '#00ffff'; color2 = '#00aaff'; symbolFunc = () => { ctx.beginPath(); ctx.moveTo(-iconSize*0.2, -iconSize*0.5); ctx.lineTo(iconSize*0.3, 0); ctx.lineTo(-iconSize*0.3, 0); ctx.lineTo(iconSize*0.2, iconSize*0.5); ctx.strokeStyle=color2; ctx.lineWidth=3; ctx.stroke(); }; break; case 'explosive': color1 = '#ff6600'; color2 = '#ffaa00'; symbolFunc = () => { ctx.fillStyle=color2; ctx.beginPath(); ctx.arc(0, 0, iconSize*0.4, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle='#555'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(0, -iconSize*0.4); ctx.lineTo(iconSize*0.3, -iconSize*0.6); ctx.stroke(); ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(iconSize*0.3, -iconSize*0.6, 2, 0, Math.PI*2); ctx.fill(); }; break; case 'multi': color1 = '#ffff00'; color2 = '#ffcc00'; symbolFunc = () => { ctx.fillStyle=color2; const r = iconSize*0.2; ctx.beginPath(); ctx.arc(-iconSize*0.3, iconSize*0.15, r, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc( iconSize*0.3, iconSize*0.15, r, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc( 0, -iconSize*0.3, r, 0, Math.PI*2); ctx.fill(); }; break; } const pulseFactor = 1.0 + Math.sin(this.pulseTimer / 250) * 0.12; ctx.strokeStyle = color1; ctx.lineWidth = 2; ctx.globalAlpha = 0.6 + Math.sin(this.pulseTimer / 250) * 0.3; ctx.beginPath(); ctx.arc(0, 0, this.size * 0.9 * pulseFactor, 0, Math.PI * 2); ctx.stroke(); ctx.globalAlpha = 1.0; ctx.fillStyle = 'rgba(50, 50, 70, 0.8)'; ctx.strokeStyle = color1; ctx.lineWidth = 1.5; const backSize = this.size * 0.9; ctx.beginPath(); ctx.roundRect(-backSize, -backSize, backSize*2, backSize*2, 5); ctx.fill(); ctx.stroke(); symbolFunc(); ctx.restore(); } } | |
// --- Initialize and Start Game (Same) --- | |
function initGame() { game = new Game(); game.drawStaticBackground(); } | |
function resizeCanvas() { canvasRect = canvas.getBoundingClientRect(); } | |
window.addEventListener('resize', resizeCanvas); | |
resizeCanvas(); | |
initGame(); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LukasBe/chaos-cannon-turbo" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |