"use strict"; const DEBUG_HITBOX = true; // ================== BGM SYSTEM ================== const bgmList = [ { normal: "music/Scary.mp3", gameover: "music/ScaryGO.mp3" }, { normal: "music/Fear.mp3", gameover: "music/FearGO.mp3" }, { normal: "music/Chill.mp3", gameover: "music/ChillGO.mp3" }, ]; let currentBGM = new Audio(); let gameOverBGM = new Audio(); currentBGM.loop = true; gameOverBGM.loop = true; function pickRandomBGM() { const bgm = bgmList[Math.floor(Math.random() * bgmList.length)]; currentBGM.src = bgm.normal; gameOverBGM.src = bgm.gameover; } // ================== GLOBALS ================== var canvasWidth = 1280; var canvasHeight = 650; var c, ctx; var gameStarted = false; var musicMuted = false; let lastFrameTime = 0; const frameInterval = 1000 / 80; let cameraY = 0; let respawnCounter = 0; let damageFlash = 0; let currentWave = null; let waveCooldown = 0; let abilityCharges = 0; var game = { level: 1, speed: 1, gameOver: false, frames: 0, timer: 0, }; var keys = { up: false, down: false, left: false, right: false, fire: false, }; var playerShipImg = new Image(); playerShipImg.src = "img/Player/pesawat22.png"; var bg0 = new Image(); bg0.src = "img/bg_0.png"; var bg1 = new Image(); bg1.src = "img/bg_1.png"; var bg2 = new Image(); bg2.src = "img/bg_2.png"; var enemyImgArray = []; enemyImgArray.length = 4; let audioStarted = false; window.addEventListener("keydown", () => { if (!audioStarted) { currentBGM.play().catch(() => {}); audioStarted = true; } }); window.addEventListener("click", () => { if (!audioStarted) { currentBGM.play().catch(() => {}); audioStarted = true; } }); for (var i = 0; i < enemyImgArray.length; i++) { enemyImgArray[i] = new Image(); enemyImgArray[i].src = "img/alien_" + [i] + ".png"; } var missilesArray = []; var enemyShipArray = []; var enemyBulletsArray = []; var explosions = []; var abilityTokens = []; var particles = []; var laser = document.createElement("audio"); laser.src = "music/laser2.mp3"; var explosion_enemy = document.createElement("audio"); explosion_enemy.src = "music/explosion-small.mp3"; var planetImages = []; for (let i = 1; i <= 4; i++) { let img = new Image(); img.src = `img/SpritesPlanet/planet_${i}.png`; planetImages.push(img); } let currentPlanet = null; window.onload = function () { init(); }; // Init function init() { c = document.getElementById("canvas"); ctx = c.getContext("2d"); c.width = window.innerWidth; c.height = window.innerHeight; canvasWidth = c.width; canvasHeight = c.height; document.addEventListener("keydown", keyDownPressed, false); document.addEventListener("keyup", keyUpPressed, false); gameStarted = true; pickRandomBGM(); currentBGM.volume = 1; requestAnimationFrame(gameLoop); } function gameLoop(timestamp) { if (!gameStarted) return; if (game.gameOver) { clearGame(); drawGameOver(); return; } if (timestamp - lastFrameTime >= frameInterval) { lastFrameTime = timestamp; if (!gamePaused) { clearGame(); updateGame(); drawGame(); } else { drawPauseOverlay(); } } requestAnimationFrame(gameLoop); } // ================== INPUT ================== let gamePaused = false; function keyDownPressed(e) { if (e.keyCode === 87 || e.keyCode === 38) keys.up = true; else if (e.keyCode === 83 || e.keyCode === 40) keys.down = true; if (e.keyCode === 65 || e.keyCode === 37) keys.left = true; if (e.keyCode === 68 || e.keyCode === 39) keys.right = true; if (e.keyCode === 32) { keys.fire = true; if (!player1.dead) { fireBullet(); } } if (e.keyCode === 80) togglePause(); if (e.keyCode === 16) { if (abilityCharges > 0 && !game.gameOver && !gamePaused) { useAbility(); abilityCharges--; } } } function keyUpPressed(e) { if (e.keyCode === 87 || e.keyCode === 38) keys.up = false; else if (e.keyCode === 83 || e.keyCode === 40) keys.down = false; if (e.keyCode === 65 || e.keyCode === 37) keys.left = false; if (e.keyCode === 68 || e.keyCode === 39) keys.right = false; if (e.keyCode === 32) keys.fire = false; } function fireBullet() { missilesArray.push( new LaserBullet(player1.x + player1.width, player1.y + player1.height / 2) ); laser.currentTime = 0; laser.volume = 0.4; laser.play(); createParticles( player1.x + player1.width, player1.y + player1.height / 2, 5, "#00e1ff" ); } // ================== CORE UPDATE / DRAW ================== function clearGame() { ctx.clearRect(0, 0, canvasWidth, canvasHeight); } function updateGame() { game.frames++; updateStarField(); addShips(); maybeSpawnAbilityToken(); // Auto-fire when holding space if (keys.fire && !player1.dead && game.frames % 8 === 0) { fireBullet(); } // Player update / respawn if (!player1.dead) { player1.update(); if (player1.invincible > 0) player1.invincible--; updateCamera(); } else { if (respawnCounter > 0) { respawnCounter--; if (respawnCounter <= 0 && player1.lives > 0) { player1.dead = false; player1.invincible = 120; player1.x = 100; player1.y = canvasHeight / 2 - player1.height / 2; player1.vx = 0; player1.vy = 0; } } } spawnPlanet(); if (currentPlanet) currentPlanet.update(); updateParticles(); } function drawGame() { ctx.save(); ctx.translate(0, cameraY); drawStarField(); if (currentPlanet) currentPlanet.draw(); // Particles (background layer) drawParticles(); // Ability tokens for (let i = 0; i < abilityTokens.length; i++) { const t = abilityTokens[i]; t.draw(); t.update(); // Hitbox token sederhana (tidak perlu .getHitbox karena bentuknya simple) // Tapi kita pakai logika bounding box manual di sini if ( !player1.dead && Tabrakan(player1.getHitbox(), { x: t.x, y: t.y, width: t.width, height: t.height, }) ) { abilityCharges++; abilityTokens.splice(i, 1); createParticles(t.x, t.y, 15, "#00ffea"); i--; continue; } if (t.x + t.width < 0) { abilityTokens.splice(i, 1); i--; } } // Player if (!player1.dead) { player1.draw(); // DEBUG: Lihat Hitbox Player if (DEBUG_HITBOX) drawDebugHitbox(player1.getHitbox(), "lime"); } // Enemies for (let i = 0; i < enemyShipArray.length; i++) { let s = enemyShipArray[i]; s.draw(); s.update(); if (DEBUG_HITBOX) drawDebugHitbox(s.getHitbox(), "red"); if (s.x < -200) { enemyShipArray.splice(i, 1); i--; continue; } // Enemy shooting (aimed, non-homing) if (!player1.dead && Math.random() < 0.01 && s.x > player1.x + 50) { const ex = s.x; const ey = s.y + s.height / 2; const px = player1.x + player1.width / 2; const py = player1.y + player1.height / 2; enemyBulletsArray.push(new EnemyBullet(ex, ey, px, py)); } // Collision with player (hitbox vs hitbox) if (!player1.dead && Tabrakan(player1.getHitbox(), s.getHitbox())) { explosions.push(new Explosion(s.x + s.width / 2, s.y + s.height / 2)); createParticles(s.x + s.width / 2, s.y + s.height / 2, 20, "#ff6600"); enemyShipArray.splice(i, 1); i--; handlePlayerHit(); continue; } } // Player bullets for (let i = 0; i < missilesArray.length; i++) { let m = missilesArray[i]; m.draw(); m.update(); if (DEBUG_HITBOX) drawDebugHitbox(m.getHitbox(), "cyan"); let hit = false; for (let j = 0; j < enemyShipArray.length; j++) { let en = enemyShipArray[j]; // PERBAIKAN: Gunakan .getHitbox() untuk kedua objek if (Tabrakan(m.getHitbox(), en.getHitbox())) { player1.score += 100; explosion_enemy.currentTime = 0; explosion_enemy.play(); explosions.push( new Explosion(en.x + en.width / 2, en.y + en.height / 2) ); createParticles( en.x + en.width / 2, en.y + en.height / 2, 15, "#ff9900" ); missilesArray.splice(i, 1); enemyShipArray.splice(j, 1); hit = true; break; } } if (hit) { i--; continue; } if (m.x > canvasWidth + 50) { missilesArray.splice(i, 1); i--; } } // Enemy bullets for (let i = 0; i < enemyBulletsArray.length; i++) { let b = enemyBulletsArray[i]; b.draw(); b.update(); if (DEBUG_HITBOX) drawDebugHitbox(b.getHitbox(), "orange"); // PERBAIKAN: Gunakan b.getHitbox() agar collision lebih akurat (tidak kena glow) if (!player1.dead && Tabrakan(b.getHitbox(), player1.getHitbox())) { explosions.push( new Explosion( player1.x + player1.width / 2, player1.y + player1.height / 2 ) ); createParticles( player1.x + player1.width / 2, player1.y + player1.height / 2, 12, "#ff3300" ); enemyBulletsArray.splice(i, 1); i--; handlePlayerHit(); continue; } if ( b.x + b.width < -100 || b.x > canvasWidth + 100 || b.y + b.height < -100 || b.y > canvasHeight + 100 ) { enemyBulletsArray.splice(i, 1); i--; } } // Explosions for (let i = 0; i < explosions.length; i++) { let ex = explosions[i]; ex.draw(); ex.update(); if (ex.done) { explosions.splice(i, 1); i--; } } ctx.restore(); drawScreenShading(); drawUI(); } function drawDebugHitbox(rect, color) { ctx.save(); ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.strokeRect(rect.x, rect.y, rect.width, rect.height); ctx.restore(); } function drawUI() { drawNewText(" " + player1.score, 1400, 760, "white"); let livesText = "Lives: "; for (let i = 0; i < player1.lives; i++) { livesText += "♥ "; } drawNewText(livesText, 60, 760, "#ff3366"); drawNewText("Bombs: " + abilityCharges, 50, 40, "#ffffffff"); } class PlayerObject { constructor(x, y) { this.x = x; this.y = y; this.width = 100; this.height = 64; this.image = playerShipImg; this.vx = 0; this.vy = 0; this.acceleration = 0.8; this.friction = 0.92; this.maxSpeed = 10; this.lives = 3; this.score = 0; this.health = 100; this.invincible = 0; this.dead = false; this.totalFrames = 5; this.frameIndex = 2; this.spriteWidth = 0; this.sourceHeight = 0; this.scale = 1.3; this.image.onload = () => { this.spriteWidth = this.image.width / this.totalFrames; this.sourceHeight = this.image.height; this.width = this.spriteWidth * this.scale; this.height = this.sourceHeight * this.scale; this.y = canvasHeight / 2 - this.height / 2; }; } getHitbox() { const h = this.height * 0.05; const w = this.width * 0.8; const x = this.x + (this.width - w) / 2; const y = this.y + (this.height - h) / 2; return { x, y, width: w, height: h }; } draw() { if (this.invincible > 0 && game.frames % 10 < 5) { return; } ctx.save(); if (this.spriteWidth > 0) { ctx.drawImage( this.image, this.frameIndex * this.spriteWidth, 0, this.spriteWidth, this.sourceHeight, this.x, this.y, this.width, this.height ); } else { ctx.fillStyle = "red"; ctx.fillRect(this.x, this.y, 50, 50); } ctx.restore(); } update() { if (keys.up) this.vy -= this.acceleration; if (keys.down) this.vy += this.acceleration; if (keys.left) this.vx -= this.acceleration; if (keys.right) this.vx += this.acceleration; this.vx *= this.friction; this.vy *= this.friction; const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy); if (speed > this.maxSpeed) { const scale = this.maxSpeed / speed; this.vx *= scale; this.vy *= scale; } this.x += this.vx; this.y += this.vy; const bleedY = this.height * 0.4; const bleedX = this.width * 0.4; if (this.y < -bleedY) { this.y = -bleedY; if (this.vy < 0) this.vy = 0; } if (this.y > canvasHeight - this.height + bleedY) { this.y = canvasHeight - this.height + bleedY; if (this.vy > 0) this.vy = 0; } if (this.x < -bleedX) { this.x = -bleedX; if (this.vx < 0) this.vx = 0; } if (this.x > canvasWidth - this.width + bleedX) { this.x = canvasWidth - this.width + bleedX; if (this.vx > 0) this.vx = 0; } if (this.vy < -2.5) { this.frameIndex = 4; } else if (this.vy < -0.5) { this.frameIndex = 3; } else if (this.vy > 2.5) { this.frameIndex = 0; } else if (this.vy > 0.5) { this.frameIndex = 1; } else { this.frameIndex = 2; } } } let player1 = new PlayerObject(100, 300); function handlePlayerHit() { if (player1.invincible > 0 || player1.dead || game.gameOver) return; explosion_enemy.currentTime = 0; explosion_enemy.play(); damageFlash = 20; player1.lives--; if (player1.lives <= 0) { game.gameOver = true; crossfadeToGameOver(); return; } player1.dead = true; respawnCounter = 80 * 3; } // ================== TEXT ================== function drawNewText(txt, x, y, color) { ctx.font = "20px Arial"; ctx.fillStyle = color; ctx.textAlign = "left"; ctx.fillText(txt, x, y); } class backgroundObj { constructor(img, x, y, speed) { this.x = x; this.y = y; this.width = 2000; this.height = 1200; this.img = img; this.speed = speed; } draw() { ctx.save(); ctx.drawImage(this.img, this.x, this.y, this.width, this.height); ctx.restore(); } update() { this.x -= this.speed; if (this.x < -2000) { this.x = 2000; } } } let background1 = new backgroundObj(bg0, 0, 0, game.speed * 3); let background1a = new backgroundObj(bg0, 2000, 0, game.speed * 3); let background2 = new backgroundObj(bg1, 0, 0, game.speed * 2); let background2a = new backgroundObj(bg1, 2000, 0, game.speed * 2); let background3 = new backgroundObj(bg2, 0, 0, game.speed * 1); let background3a = new backgroundObj(bg2, 2000, 0, game.speed * 1); function updateStarField() { background3.update(); background3a.update(); background2.update(); background2a.update(); background1.update(); background1a.update(); } function drawStarField() { background3.draw(); background3a.draw(); background2.draw(); background2a.draw(); background1.draw(); background1a.draw(); } // panning function updateCamera() { const offset = player1.y + player1.height / 2 - canvasHeight / 2; const target = -offset * 0.7; const bgHeight = 1200; const minY = canvasHeight - bgHeight; const maxY = 0; const clamped = Math.max(minY, Math.min(maxY, target)); cameraY += (clamped - cameraY) * 0.1; } // class LaserBullet { constructor(x, y) { this.x = x; this.y = y; this.width = 14; this.height = 4; this.speed = 16; } getHitbox() { return { x: this.x, y: this.y, width: this.width, height: this.height }; } draw() { let g = ctx.createLinearGradient( this.x, this.y, this.x + this.width, this.y ); g.addColorStop(0, "#00e1ff"); g.addColorStop(0.5, "#ffffff"); g.addColorStop(1, "#00e1ff"); ctx.fillStyle = g; ctx.shadowColor = "#00ffff"; ctx.shadowBlur = 15; ctx.fillRect(this.x, this.y, this.width, this.height); ctx.shadowBlur = 0; } update() { this.x += this.speed; } } // class EnemyObj { constructor(x, y, speed, img, pattern = "straight") { this.x = x; this.y = y; this.width = 170; this.height = 105; this.image = img; this.speed = speed; this.health = 100; this.damage = 10; this.pattern = pattern; this.angle = 0; } getHitbox() { const w = this.width * 0.55; const h = this.height * 0.55; const x = this.x + (this.width - w) / 2; const y = this.y + (this.height - h) / 2; return { x, y, width: w, height: h }; } draw() { ctx.save(); ctx.drawImage(this.image, this.x, this.y, this.width, this.height); ctx.restore(); } update() { this.x -= this.speed; if (this.pattern === "sine") { this.angle += 0.05 * this.speed; this.y += Math.sin(this.angle) * 3; } } } class EnemyBullet { constructor(x, y, targetX, targetY) { this.x = x; this.y = y; this.width = 10; this.height = 4; const dx = targetX - x; const dy = targetY - y; const len = Math.sqrt(dx * dx + dy * dy) || 1; const speed = 8; this.vx = (dx / len) * speed; this.vy = (dy / len) * speed; } getHitbox() { const padding = 1; return { x: this.x + padding, y: this.y + padding, width: this.width - padding * 2, height: this.height - padding * 2, }; } draw() { let g = ctx.createLinearGradient( this.x + this.width, this.y, this.x, this.y ); g.addColorStop(0, "#ff9900"); g.addColorStop(0.5, "#ffffff"); g.addColorStop(1, "#ff3300"); ctx.fillStyle = g; ctx.shadowColor = "#ff6600"; ctx.shadowBlur = 10; ctx.fillRect(this.x, this.y, this.width, this.height); ctx.shadowBlur = 0; } update() { this.x += this.vx; this.y += this.vy; } } class Planet { constructor(img) { this.image = img; this.width = 160; this.height = 160; this.x = canvasWidth + 50; this.y = Math.random() * 300 + 50; this.speed = 1.2; this.active = true; } draw() { ctx.drawImage(this.image, this.x, this.y, this.width, this.height); } update() { this.x -= this.speed; if (this.x < -this.width) { this.active = false; } } } function spawnPlanet() { if (currentPlanet == null || currentPlanet.active === false) { let randomImg = planetImages[Math.floor(Math.random() * planetImages.length)]; currentPlanet = new Planet(randomImg); } } function addShips() { if (game.frames < 200) return; if (currentWave) { if (currentWave.spawned < currentWave.count) { if (currentWave.spawnTimer <= 0) { spawnEnemyFromWave(currentWave); currentWave.spawned++; currentWave.spawnTimer = currentWave.spacing; } else { currentWave.spawnTimer--; } } else { if (enemyShipArray.length === 0) { currentWave = null; waveCooldown = 120; game.level++; game.speed += 0.1; } } } else { if (waveCooldown > 0) { waveCooldown--; } else { startNewWave(); } } } function startNewWave() { const patterns = ["line", "v", "sine"]; const pattern = patterns[Math.floor(Math.random() * patterns.length)]; const count = 4 + Math.floor(Math.random() * 4); const spacing = 30 + Math.floor(Math.random() * 20); currentWave = { pattern: pattern, count: count, spacing: spacing, spawned: 0, spawnTimer: 0, }; } function spawnEnemyFromWave(wave) { const baseY = canvasHeight / 2; const spread = 200; const index = wave.spawned; let y = Math.random() * (canvasHeight - 120) + 60; if (wave.pattern === "line") { const step = (spread * 2) / Math.max(1, wave.count - 1); y = baseY - spread + step * index; } else if (wave.pattern === "v") { const centerIndex = (wave.count - 1) / 2; const offset = (index - centerIndex) * 40; y = baseY + Math.abs(offset) * 1.5; } else if (wave.pattern === "sine") { const angle = (index / wave.count) * Math.PI * 2; y = baseY + Math.sin(angle) * spread; } y = Math.max(40, Math.min(canvasHeight - 140, y)); const randomShip = Math.floor(Math.random() * enemyImgArray.length); const speed = 5 + Math.random() * 4; enemyShipArray.push( new EnemyObj( canvasWidth + 50, y, speed + game.speed, enemyImgArray[randomShip], wave.pattern === "sine" ? "sine" : "straight" ) ); } // bombs class AbilityToken { constructor(x, y) { this.x = x; this.y = y; this.width = 32; this.height = 32; this.speed = 4; } draw() { ctx.save(); ctx.beginPath(); const cx = this.x + this.width / 2; const cy = this.y + this.height / 2; ctx.arc(cx, cy, 12, 0, Math.PI * 2); const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, 12); g.addColorStop(0, "#ffffff"); g.addColorStop(0.5, "#00ffea"); g.addColorStop(1, "#0066ff"); ctx.fillStyle = g; ctx.fill(); ctx.restore(); } update() { this.x -= this.speed; } } function maybeSpawnAbilityToken() { if (Math.random() < 0.002 && abilityTokens.length < 3) { const y = Math.random() * (canvasHeight - 120) + 60; abilityTokens.push(new AbilityToken(canvasWidth + 40, y)); } } function useAbility() { if (enemyShipArray.length === 0 && enemyBulletsArray.length === 0) return; explosion_enemy.currentTime = 0; explosion_enemy.play(); // Explosions for all enemies enemyShipArray.forEach((e) => { explosions.push(new Explosion(e.x + e.width / 2, e.y + e.height / 2)); createParticles(e.x + e.width / 2, e.y + e.height / 2, 20, "#ff9900"); }); enemyShipArray = []; enemyBulletsArray = []; missilesArray = []; damageFlash = 10; } // ================== PARTICLES ================== class Particle { constructor(x, y, color) { this.x = x; this.y = y; this.vx = (Math.random() - 0.5) * 8; this.vy = (Math.random() - 0.5) * 8; this.life = 30; this.maxLife = 30; this.color = color; this.size = Math.random() * 3 + 2; } update() { this.x += this.vx; this.y += this.vy; this.vx *= 0.95; this.vy *= 0.95; this.life--; } draw() { const alpha = this.life / this.maxLife; ctx.save(); ctx.globalAlpha = alpha; ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } get isDead() { return this.life <= 0; } } function createParticles(x, y, count, color) { for (let i = 0; i < count; i++) { particles.push(new Particle(x, y, color)); } } function updateParticles() { for (let i = particles.length - 1; i >= 0; i--) { particles[i].update(); if (particles[i].isDead) { particles.splice(i, 1); } } } function drawParticles() { particles.forEach((p) => p.draw()); } function Tabrakan(o, p) { if ( o.x + o.width > p.x && o.x < p.x + p.width && o.y + o.height > p.y && o.y < p.y + p.height ) { return true; } return false; } class Explosion { constructor(x, y, scale = 1) { this.x = x; this.y = y; this.frame = 0; this.maxFrames = 30; this.scale = scale; } update() { this.frame++; } draw() { let progress = this.frame / this.maxFrames; let radius = (20 + 60 * progress) * this.scale; ctx.save(); ctx.globalAlpha = 1 - progress; let gradient = ctx.createRadialGradient( this.x, this.y, 0, this.x, this.y, radius ); gradient.addColorStop(0, "#ffffff"); gradient.addColorStop(0.2, "#ffe066"); gradient.addColorStop(0.5, "#ff8c42"); gradient.addColorStop(1, "#ff0000"); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(this.x, this.y, radius, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } get done() { return this.frame >= this.maxFrames; } } // GAME OVER / PAUSE function drawGameOver() { ctx.fillStyle = "rgba(53, 0, 0, 0.7)"; ctx.fillRect(0, 0, canvasWidth, canvasHeight); ctx.font = "80px Arial"; ctx.fillStyle = "red"; ctx.textAlign = "center"; ctx.fillText("GAME OVER", canvasWidth / 2, canvasHeight / 2 - 50); ctx.font = "40px Arial"; ctx.fillStyle = "white"; ctx.fillText( "Final Score: " + player1.score, canvasWidth / 2, canvasHeight / 2 + 20 ); ctx.fillText("Refresh to Restart", canvasWidth / 2, canvasHeight / 2 + 70); ctx.textAlign = "left"; } function drawPauseOverlay() { ctx.fillStyle = "rgba(0,0,0,0.5)"; ctx.fillRect(0, 0, canvasWidth, canvasHeight); ctx.font = "60px Arial"; ctx.fillStyle = "white"; ctx.textAlign = "center"; ctx.fillText("PAUSED", canvasWidth / 2, canvasHeight / 2); ctx.font = "24px Arial"; ctx.fillText("Press P to Resume", canvasWidth / 2, canvasHeight / 2 + 50); ctx.textAlign = "left"; } function drawScreenShading() { let grd = ctx.createRadialGradient( canvasWidth / 2, canvasHeight / 2, 200, canvasWidth / 2, canvasHeight / 2, canvasWidth ); grd.addColorStop(0, "rgba(0,0,0,0)"); grd.addColorStop(1, "rgba(0,0,0,0.6)"); ctx.fillStyle = grd; ctx.fillRect(0, 0, canvasWidth, canvasHeight); if (damageFlash > 0) { let alpha = (damageFlash / 20) * 0.6; ctx.fillStyle = "rgba(255,0,0," + alpha.toFixed(2) + ")"; ctx.fillRect(0, 0, canvasWidth, canvasHeight); damageFlash--; } } function togglePause() { if (game.gameOver || !gameStarted) return; gamePaused = !gamePaused; if (gamePaused) { currentBGM.pause(); } else if (!musicMuted && audioStarted) { currentBGM.play().catch(() => {}); lastFrameTime = performance.now ? performance.now() : Date.now(); } } function crossfadeToGameOver() { let fadeSpeed = 0.02; gameOverBGM.volume = 0; gameOverBGM.play(); let fadeInterval = setInterval(() => { currentBGM.volume -= fadeSpeed; if (currentBGM.volume < 0) currentBGM.volume = 0; gameOverBGM.volume += fadeSpeed; if (gameOverBGM.volume > 1) gameOverBGM.volume = 1; if (currentBGM.volume === 0) { currentBGM.pause(); clearInterval(fadeInterval); } }, 1000 / 30); }