"use strict"; const DEBUG_HITBOX = false; 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; } // --- OPTIMASI 1: Resolusi Tetap (HD 720p) --- var canvasWidth = 1280; var canvasHeight = 720; var c, ctx; var gameStarted = false; var musicMuted = false; // --- OPTIMASI 2: Cache Vignette --- let vignetteCanvas = null; let lastFrameTime = 0; const frameInterval = 1000 / 60; 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(); }; function init() { c = document.getElementById("canvas"); ctx = c.getContext("2d", { alpha: false }); c.width = canvasWidth; c.height = canvasHeight; 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); } 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) { // --- FIX DI SINI: Tambahkan pengecekan !player1.dead --- if (abilityCharges > 0 && !game.gameOver && !gamePaused && !player1.dead) { 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" ); } function clearGame() { ctx.clearRect(0, 0, canvasWidth, canvasHeight); } function updateGame() { game.frames++; game.level = 1 + Math.floor(player1.score / 500); game.speed = 1 + game.level * 0.1; updateStarField(); addShips(); maybeSpawnAbilityToken(); if (keys.fire && !player1.dead && game.frames % 8 === 0) { fireBullet(); } 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(); drawParticles(); for (let i = 0; i < abilityTokens.length; i++) { const t = abilityTokens[i]; t.draw(); t.update(); 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--; } } if (!player1.dead) { player1.draw(); if (DEBUG_HITBOX) drawDebugHitbox(player1.getHitbox(), "lime"); } 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; } // BALANCING TEMBAKAN (Low Rate) let shootChance = 0.005 + game.level * 0.0012; if (shootChance > 0.04) shootChance = 0.04; if (!player1.dead && Math.random() < shootChance && 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)); } 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; } } 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]; if (Tabrakan(m.getHitbox(), en.getHitbox())) { // --- BALANCING DAMAGE PLAYER --- let playerDamage = 100 + game.level * 5; en.health -= playerDamage; createParticles( en.x + en.width / 2, en.y + en.height / 2, 5, "#ff9900" ); missilesArray.splice(i, 1); hit = true; if (en.health <= 0) { player1.score += 100 + game.level * 10; explosion_enemy.currentTime = 0; explosion_enemy.play(); explosions.push( new Explosion(en.x + en.width / 2, en.y + en.height / 2) ); enemyShipArray.splice(j, 1); } break; } } if (hit) { i--; continue; } if (m.x > canvasWidth + 50) { missilesArray.splice(i, 1); i--; } } for (let i = 0; i < enemyBulletsArray.length; i++) { let b = enemyBulletsArray[i]; b.draw(); b.update(); if (DEBUG_HITBOX) drawDebugHitbox(b.getHitbox(), "orange"); 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--; } } 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( "Score: " + player1.score, canvasWidth - 200, canvasHeight - 50, "white" ); drawNewText("LVL " + game.level, canvasWidth - 150, 50, "#00ff00"); let livesText = "Lives: "; for (let i = 0; i < player1.lives; i++) { livesText += "♥ "; } drawNewText(livesText, 30, canvasHeight - 50, "#ff3366"); drawNewText("Bombs: " + abilityCharges, 30, 50, "#ffffff"); } 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; // --- SIZE PLAYER: Middle Ground --- this.scale = 1.0; 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; } 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(); } 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 = 13; 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; // --- SIZE MUSUH: Middle Ground --- this.width = 145; this.height = 90; 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++; // --- RANDOM SPACING --- let randomSpacing = currentWave.spacing + Math.floor(Math.random() * 30); currentWave.spawnTimer = randomSpacing; } else { currentWave.spawnTimer--; } } else { if (enemyShipArray.length === 0) { currentWave = null; waveCooldown = Math.max(60, 120 - game.level * 2); } } } else { if (waveCooldown > 0) { waveCooldown--; } else { startNewWave(); } } } function startNewWave() { // --- CHAOS WAVE MODE --- // Tidak ada pattern 'line', 'v', 'zigzag'. // Kita hanya menentukan jumlah musuh yang akan muncul di wave ini. let baseCount = 3; let scalingCount = Math.floor(game.level / 2); let count = Math.min( 20, // Max musuh ditingkatkan sedikit untuk kompensasi sebaran baseCount + scalingCount + Math.floor(Math.random() * 5) ); // Spacing dasar (nanti diacak lagi per musuh) let spacing = Math.max(15, 40 - game.level); currentWave = { count: count, spacing: spacing, spawned: 0, spawnTimer: 0, }; } function spawnEnemyFromWave(wave) { // --- POSISI BENAR-BENAR ACAK --- // Tentukan Y sembarang di area layar // Area aman: 60px dari atas, 100px dari bawah const minY = 60; const maxY = canvasHeight - 120; const y = Math.random() * (maxY - minY) + minY; // Random X Offset biar tidak muncul dalam satu garis lurus sempurna const xOffset = Math.random() * 200; const randomShip = Math.floor(Math.random() * enemyImgArray.length); // Scaling Speed per 5 Level (0.2 factor) let rawSpeed = 3.5 + Math.random() * 2 + game.level * 0.2; const speed = Math.min(rawSpeed, 8); // --- RANDOM MOVEMENT TYPE --- // Setiap musuh melempar dadu sendiri untuk menentukan tipe gerakannya // 30% kemungkinan gerak gelombang (sine), sisanya lurus. let movementType = Math.random() < 0.3 ? "sine" : "straight"; let enemy = new EnemyObj( canvasWidth + 50 + xOffset, // X position + random offset y, speed, enemyImgArray[randomShip], movementType ); // BALANCING HEALTH enemy.health = 60 + game.level * 10; enemyShipArray.push(enemy); } 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(); 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; } 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; } } 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() { if (!vignetteCanvas) { vignetteCanvas = document.createElement("canvas"); vignetteCanvas.width = canvasWidth; vignetteCanvas.height = canvasHeight; const vCtx = vignetteCanvas.getContext("2d"); let grd = vCtx.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)"); vCtx.fillStyle = grd; vCtx.fillRect(0, 0, canvasWidth, canvasHeight); } ctx.drawImage(vignetteCanvas, 0, 0); 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); }