/* ═══════════════════════════════════════════════════════════════ MAIN GAME ENGINE ═══════════════════════════════════════════════════════════════ */ // ═══════════════════════════════════════════════════════════════ // CANVAS & CONTEXT // ═══════════════════════════════════════════════════════════════ const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); // ═══════════════════════════════════════════════════════════════ // SAVE SYSTEM // ═══════════════════════════════════════════════════════════════ const SaveSystem = { saveKey: 'platformer_save', scoresKey: 'platformer_scores', saveGame: function(data) { try { localStorage.setItem(this.saveKey, JSON.stringify(data)); } catch(e) { console.log('Save failed:', e); } }, loadGame: function() { try { const data = localStorage.getItem(this.saveKey); return data ? JSON.parse(data) : null; } catch(e) { return null; } }, // ═══════════════════════════════════════════════════════════════ // ACHIEVEMENTS SYSTEM // ═══════════════════════════════════════════════════════════════ achievementsKey: 'platformer_achievements', getAchievements: function() { try { const data = localStorage.getItem(this.achievementsKey); return data ? JSON.parse(data) : {}; } catch(e) { return {}; } }, unlockAchievement: function(id) { const achievements = this.getAchievements(); if (!achievements[id]) { achievements[id] = true; localStorage.setItem(this.achievementsKey, JSON.stringify(achievements)); return true; } return false; }, saveScore: function(score) { try { const scores = this.getScores(); scores.push({ score: score, date: Date.now() }); scores.sort((a, b) => b.score - a.score); scores = scores.slice(0, 10); localStorage.setItem(this.scoresKey, JSON.stringify(scores)); } catch(e) {} }, getScores: function() { try { const data = localStorage.getItem(this.scoresKey); return data ? JSON.parse(data) : []; } catch(e) { return []; } }, clearSave: function() { localStorage.removeItem(this.saveKey); } }; // Achievements list const ACHIEVEMENTS = [ { id: 'first_coin', name: 'Первая монетка', icon: '🪙', desc: 'Соберите 1 монету' }, { id: 'coins_100', name: 'Коллекционер', icon: '💰', desc: 'Соберите 100 монет' }, { id: 'level_5', name: 'Продвинутый', icon: '⭐', desc: 'Пройдите 5 уровней' }, { id: 'level_10', name: 'Мастер', icon: '🏆', desc: 'Пройдите все уровни' }, { id: 'no_hit', name: 'Неуязвимый', icon: '🛡️', desc: 'Пройдите уровень без урона' }, { id: 'speedrun', name: 'Спидраннер', icon: '⚡', desc: 'Пройдите уровень за 30 сек' } ]; // Combo system let comboCount = 0; let comboTimer = 0; const COMBO_MAX_TIME = 120; // 2 seconds at 60fps function addCombo(points) { comboTimer = COMBO_MAX_TIME; comboCount++; return points * Math.min(comboCount, 5); // Max 5x multiplier } function updateCombo() { if (comboTimer > 0) comboTimer--; else comboCount = 0; } // ═══════════════════════════════════════════════════════════════ let gameRunning = true; let paused = false; let frameCount = 0; let gameTime = 0; let totalScore = 0; let currentLevelScore = 0; let lives = 3; let screenShake = 0; let maxUnlockedLevel = 1; let hasSave = false; // ═══════════════════════════════════════════════════════════════ // UI ELEMENTS // ═══════════════════════════════════════════════════════════════ const messageDiv = document.getElementById('message'); const messageTitle = document.getElementById('messageTitle'); const messageStats = document.getElementById('messageStats'); const scoreDisplay = document.getElementById('score'); const timerDisplay = document.getElementById('timer'); const livesContainer = document.getElementById('lives'); const levelDisplay = document.getElementById('level'); // ═══════════════════════════════════════════════════════════════ // INPUT // ═══════════════════════════════════════════════════════════════ const keys = { left: false, right: false, up: false }; document.addEventListener('keydown', (e) => { if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true; if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true; if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { keys.up = true; e.preventDefault(); } if (e.code === 'KeyR') restartGame(); if (e.code === 'Escape') togglePause(); }); document.addEventListener('keyup', (e) => { if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false; if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false; if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') keys.up = false; }); // Continue to next level with Enter or Space document.addEventListener('keydown', (e) => { // Only continue if game is not running AND message is shown AND it's a win if (!gameRunning && messageDiv.style.display !== 'none') { if (messageTitle.textContent.includes('ПРОЙДЕН') || messageTitle.textContent.includes('ПОБЕДА')) { if (e.code === 'Space' || e.code === 'Enter' || e.code === 'ArrowRight' || e.code === 'ArrowDown' || e.code === 'KeyA' || e.code === 'KeyD') { e.preventDefault(); continueToNextLevel(); } } } }); // ═══════════════════════════════════════════════════════════════ // PARTICLES SYSTEM // ═══════════════════════════════════════════════════════════════ let particles = []; function createParticles(x, y, color, count = 10, speed = 8) { for (let i = 0; i < count; i++) { particles.push({ x: x, y: y, vx: (Math.random() - 0.5) * speed, vy: (Math.random() - 0.5) * speed - 2, size: Math.random() * 6 + 2, color: color, life: 1, decay: Math.random() * 0.025 + 0.015 }); } } // Trail particles for player movement let playerTrail = []; function createPlayerTrail() { if (Math.abs(Player.velocityX) > 2 || Math.abs(Player.velocityY) > 2) { playerTrail.push({ x: Player.x + Player.width / 2, y: Player.y + Player.height / 2, size: Math.random() * 8 + 4, color: Player.color, life: 1, decay: 0.05 }); } } function updatePlayerTrail() { for (let i = playerTrail.length - 1; i >= 0; i--) { playerTrail[i].life -= playerTrail[i].decay; if (playerTrail[i].life <= 0) playerTrail.splice(i, 1); } } function drawPlayerTrail() { for (const p of playerTrail) { ctx.globalAlpha = p.life * 0.5; ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; } // Landing particles let wasGrounded = true; function checkLanding() { if (!wasGrounded && Player.grounded) { createParticles(Player.x + Player.width / 2, Player.y + Player.height, '#aaa', 8, 4); } wasGrounded = Player.grounded; } function updateParticles() { for (let i = particles.length - 1; i >= 0; i--) { const p = particles[i]; p.x += p.vx; p.y += p.vy; p.vy += 0.15; p.life -= p.decay; if (p.life <= 0) particles.splice(i, 1); } } function drawParticles() { for (const p of particles) { ctx.globalAlpha = p.life; ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; } // ═══════════════════════════════════════════════════════════════ // BACKGROUND - Parallax Stars // ═══════════════════════════════════════════════════════════════ const stars = []; for (let i = 0; i < 150; i++) { stars.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, size: Math.random() * 2.5 + 0.5, speed: Math.random() * 0.4 + 0.05, brightness: Math.random(), twinkleSpeed: Math.random() * 0.1 + 0.02 }); } function drawBackground() { // Animated gradient background const time = frameCount * 0.01; const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); gradient.addColorStop(0, `hsl(${260 + Math.sin(time) * 10}, 50%, 5%)`); gradient.addColorStop(0.3, `hsl(${240 + Math.sin(time) * 5}, 60%, 8%)`); gradient.addColorStop(0.6, `hsl(${270 + Math.sin(time) * 8}, 40%, 10%)`); gradient.addColorStop(1, `hsl(${250 + Math.sin(time) * 6}, 50%, 12%)`); ctx.fillStyle = gradient; ctx.fillRect(0, 0, canvas.width, canvas.height); // Animated nebula clouds for (let i = 0; i < 3; i++) { const nebulaX = 200 + i * 250 + Math.sin(time + i) * 50; const nebulaY = 300 + i * 100 + Math.cos(time * 0.7 + i) * 30; const nebulaSize = 150 + Math.sin(time * 0.5 + i * 2) * 30; const nebulaGradient = ctx.createRadialGradient(nebulaX, nebulaY, 0, nebulaX, nebulaY, nebulaSize); nebulaGradient.addColorStop(0, `hsla(${i * 60 + 280}, 70%, 30%, 0.08)`); nebulaGradient.addColorStop(0.5, `hsla(${i * 60 + 260}, 60%, 20%, 0.04)`); nebulaGradient.addColorStop(1, 'transparent'); ctx.fillStyle = nebulaGradient; ctx.beginPath(); ctx.arc(nebulaX, nebulaY, nebulaSize, 0, Math.PI * 2); ctx.fill(); } // Shooting stars (rare) if (Math.random() < 0.002) { const shootingStar = { x: Math.random() * canvas.width, y: Math.random() * canvas.height * 0.5, length: 50 + Math.random() * 50, speed: 15 + Math.random() * 10 }; const gradient2 = ctx.createLinearGradient( shootingStar.x, shootingStar.y, shootingStar.x + shootingStar.length, shootingStar.y + shootingStar.length * 0.3 ); gradient2.addColorStop(0, 'rgba(255, 255, 255, 0.8)'); gradient2.addColorStop(1, 'transparent'); ctx.strokeStyle = gradient2; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(shootingStar.x, shootingStar.y); ctx.lineTo(shootingStar.x + shootingStar.length, shootingStar.y + shootingStar.length * 0.3); ctx.stroke(); } // Stars with parallax and twinkle for (const star of stars) { const twinkle = Math.sin(frameCount * star.twinkleSpeed + star.brightness * 10) * 0.4 + 0.6; const starColor = star.brightness > 0.7 ? `rgba(100, 200, 255, ${twinkle * star.brightness})` : `rgba(255, 255, 255, ${twinkle * star.brightness})`; ctx.fillStyle = starColor; ctx.beginPath(); ctx.arc(star.x, star.y, star.size * twinkle, 0, Math.PI * 2); ctx.fill(); // Subtle movement (parallax) if (frameCount % 60 === 0) { star.x -= star.speed; if (star.x < 0) star.x = canvas.width; } } // Weather effects - rain if (frameCount % 2 === 0) { for (let i = 0; i < 3; i++) { const rainX = Math.random() * canvas.width; const rainY = Math.random() * canvas.height; ctx.strokeStyle = 'rgba(150, 200, 255, 0.3)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(rainX, rainY); ctx.lineTo(rainX - 2, rainY + 15); ctx.stroke(); } } // Vignette effect const vignetteGradient = ctx.createRadialGradient( canvas.width / 2, canvas.height / 2, canvas.height * 0.3, canvas.width / 2, canvas.height / 2, canvas.height * 0.8 ); vignetteGradient.addColorStop(0, 'transparent'); vignetteGradient.addColorStop(1, 'rgba(0, 0, 0, 0.5)'); ctx.fillStyle = vignetteGradient; ctx.fillRect(0, 0, canvas.width, canvas.height); } // ═══════════════════════════════════════════════════════════════ // COLLISION DETECTION // ═══════════════════════════════════════════════════════════════ function checkCollision(rect1, rect2) { return rect1.x < rect2.x + rect2.width && rect1.x + rect1.width > rect2.x && rect1.y < rect2.y + rect2.height && rect1.y + rect1.height > rect2.y; } // ═══════════════════════════════════════════════════════════════ // PLAYER DRAWING - Animated Sprite-like // ═══════════════════════════════════════════════════════════════ function drawPlayer() { const p = Player; // Skip drawing during invincibility flicker if (p.invincible && Math.floor(frameCount / 4) % 2 === 0) return; // Determine animation frame based on state let bobOffset = 0; let legSpread = 0; let armAngle = 0; if (!p.grounded) { // Jumping/falling pose bobOffset = p.velocityY > 0 ? 2 : -2; armAngle = p.velocityY > 0 ? -0.5 : 0.5; } else if (Math.abs(p.velocityX) > 0.5) { // Running animation legSpread = Math.sin(frameCount * 0.3) * 4; bobOffset = Math.sin(frameCount * 0.3) * 2; armAngle = Math.sin(frameCount * 0.3) * 0.3; } else { // Idle breathing bobOffset = Math.sin(frameCount * 0.05) * 1; } // Shadow with glow ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; ctx.beginPath(); ctx.ellipse(p.x + p.width / 2, p.y + p.height + 3 + bobOffset, p.width / 2, 4, 0, 0, Math.PI * 2); ctx.fill(); // Dynamic glow color based on powerups let glowColor = '#00ffff'; let bodyColor1 = '#00ffff'; let bodyColor2 = '#00d9ff'; if (p.hasSpeedBoost) { glowColor = '#ff00ff'; bodyColor1 = '#ff00ff'; bodyColor2 = '#aa00ff'; } if (p.invincible) { glowColor = '#ffff00'; bodyColor1 = '#ffff00'; bodyColor2 = '#ffaa00'; } // Draw arms (behind body) ctx.fillStyle = bodyColor2; // Left arm ctx.save(); ctx.translate(p.x + 4, p.y + 15 + bobOffset); ctx.rotate(-armAngle); ctx.fillRect(-3, 0, 6, 15); ctx.restore(); // Right arm ctx.save(); ctx.translate(p.x + p.width - 4, p.y + 15 + bobOffset); ctx.rotate(armAngle); ctx.fillRect(-3, 0, 6, 15); ctx.restore(); // Body with gradient const bodyGradient = ctx.createLinearGradient(p.x, p.y + bobOffset, p.x + p.width, p.y + p.height + bobOffset); bodyGradient.addColorStop(0, bodyColor1); bodyGradient.addColorStop(0.5, bodyColor2); bodyGradient.addColorStop(1, '#006688'); ctx.fillStyle = bodyGradient; // Main body const r = 6; ctx.beginPath(); ctx.moveTo(p.x + r, p.y + bobOffset); ctx.lineTo(p.x + p.width - r, p.y + bobOffset); ctx.quadraticCurveTo(p.x + p.width, p.y + bobOffset, p.x + p.width, p.y + r + bobOffset); ctx.lineTo(p.x + p.width, p.y + p.height - r + bobOffset); ctx.quadraticCurveTo(p.x + p.width, p.y + p.height + bobOffset, p.x + p.width - r, p.y + p.height + bobOffset); ctx.lineTo(p.x + r, p.y + p.height + bobOffset); ctx.quadraticCurveTo(p.x, p.y + p.height + bobOffset, p.x, p.y + p.height - r + bobOffset); ctx.lineTo(p.x, p.y + r + bobOffset); ctx.quadraticCurveTo(p.x, p.y + bobOffset, p.x + r, p.y + bobOffset); ctx.fill(); // Enhanced glow effect with pulse const pulseIntensity = Math.sin(frameCount * 0.1) * 5 + 15; ctx.shadowColor = glowColor; ctx.shadowBlur = pulseIntensity; ctx.strokeStyle = glowColor; ctx.lineWidth = 2; ctx.stroke(); ctx.shadowBlur = 0; // Draw legs ctx.fillStyle = bodyColor2; // Left leg ctx.fillRect(p.x + 6 - legSpread/2, p.y + p.height - 5 + bobOffset, 7, 10); // Right leg ctx.fillRect(p.x + p.width - 13 + legSpread/2, p.y + p.height - 5 + bobOffset, 7, 10); // Inner highlight ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(p.x + 4, p.y + 4 + bobOffset); ctx.lineTo(p.x + p.width - 4, p.y + 4 + bobOffset); ctx.stroke(); // Eyes with direction const eyeY = p.y + 12 + bobOffset; const eyeOffset = p.facingRight ? 2 : -2; // Eye whites ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(p.x + 8 + eyeOffset, eyeY, 5, 0, Math.PI * 2); ctx.arc(p.x + 20 + eyeOffset, eyeY, 5, 0, Math.PI * 2); ctx.fill(); // Pupils ctx.fillStyle = '#000'; const pupilOffset = p.facingRight ? 2 : 0; ctx.beginPath(); ctx.arc(p.x + 9 + pupilOffset + eyeOffset, eyeY + 1, 2.5, 0, Math.PI * 2); ctx.arc(p.x + 21 + pupilOffset + eyeOffset, eyeY + 1, 2.5, 0, Math.PI * 2); ctx.fill(); // Eye shine ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(p.x + 7 + pupilOffset + eyeOffset, eyeY - 1, 1.5, 0, Math.PI * 2); ctx.arc(p.x + 19 + pupilOffset + eyeOffset, eyeY - 1, 1.5, 0, Math.PI * 2); ctx.fill(); // Double jump indicator if (Player.hasDoubleJump && !Player.hasUsedDoubleJump) { ctx.fillStyle = 'rgba(0, 255, 136, 0.8)'; ctx.beginPath(); ctx.arc(p.x + p.width / 2, p.y - 10, 5, 0, Math.PI * 2); ctx.fill(); } } // ═══════════════════════════════════════════════════════════════ // SCREEN SHAKE // ═══════════════════════════════════════════════════════════════ function applyScreenShake() { if (screenShake > 0) { const shakeX = (Math.random() - 0.5) * screenShake; const shakeY = (Math.random() - 0.5) * screenShake; ctx.translate(shakeX, shakeY); screenShake *= 0.9; if (screenShake < 0.5) screenShake = 0; } } function triggerScreenShake(intensity = 10) { screenShake = intensity; canvas.classList.add('shake'); setTimeout(() => canvas.classList.remove('shake'), 400); } // ═══════════════════════════════════════════════════════════════ // GAME LOGIC // ═══════════════════════════════════════════════════════════════ let levelManager = new LevelManager(); let lastCheckpoint = null; function initLevel(levelIndex) { if (!levelManager.loadLevel(levelIndex)) { // Game completed gameComplete(); return; } // Reset player position const level = LEVELS[levelIndex]; Player.reset(level.playerStart.x, level.playerStart.y); // Reset checkpoint lastCheckpoint = null; // Update level display if (levelDisplay) { levelDisplay.textContent = `УРОВЕНЬ ${levelIndex + 1}: ${level.name}`; } // Reset level score currentLevelScore = 0; } function update() { if (!gameRunning || paused) return; frameCount++; if (frameCount % 60 === 0) { gameTime++; timerDisplay.textContent = gameTime; } // Update player Player.update(); // Update level objects levelManager.update(); // Check landing checkLanding(); // Create player trail createPlayerTrail(); updatePlayerTrail(); // Update particles updateParticles(); // Player movement if (keys.left) { Player.velocityX = -Player.speed; Player.facingRight = false; } else if (keys.right) { Player.velocityX = Player.speed; Player.facingRight = true; } else { Player.velocityX = 0; } Player.x += Player.velocityX; // Screen boundaries (horizontal) if (Player.x < 30) Player.x = 30; if (Player.x + Player.width > 870) Player.x = 870 - Player.width; // Jump if (keys.up) { const wasGrounded = Player.grounded; if (Player.jump()) { createParticles(Player.x + Player.width / 2, Player.y + Player.height, '#00ffff', 8); if (!wasGrounded && Player.hasDoubleJump && Player.hasUsedDoubleJump) { AudioSystem.play('doubleJump'); } else { AudioSystem.play('jump'); } } keys.up = false; // Prevent hold-to-fly } // Gravity Player.velocityY += Player.gravity; Player.y += Player.velocityY; // Platform collisions Player.grounded = false; const allPlatforms = levelManager.getAllPlatforms(); for (const platform of allPlatforms) { if (checkCollision(Player, platform)) { // Landing on top if (Player.velocityY > 0 && Player.y + Player.height - Player.velocityY <= platform.y + 10) { Player.y = platform.y - Player.height; Player.velocityY = 0; Player.grounded = true; Player.hasUsedDoubleJump = false; // Move with platform if (platform.isMoving) { Player.x += platform.speed * platform.direction; } } // Hitting from below else if (Player.velocityY < 0 && Player.y - Player.velocityY >= platform.y + platform.height - 10) { Player.y = platform.y + platform.height; Player.velocityY = 0; } // Side collisions else if (Player.velocityX > 0) { Player.x = platform.x - Player.width; } else if (Player.velocityX < 0) { Player.x = platform.x + platform.width; } } } // Fall off screen if (Player.y > canvas.height + 50) { takeDamage(); } // Coin collection for (const coin of levelManager.coins) { if (!coin.collected && checkCollision(Player, coin)) { coin.collected = true; currentLevelScore += 10; totalScore += 10; scoreDisplay.textContent = totalScore; // Enhanced sparkle effect with golden trail createParticles(coin.x + coin.width / 2, coin.y + coin.height / 2, '#ffd700', 15, 6); createParticles(coin.x + coin.width / 2, coin.y + coin.height / 2, '#fff8dc', 10, 8); createParticles(coin.x + coin.width / 2, coin.y + coin.height / 2, '#ffec8b', 8, 10); AudioSystem.play('coin'); } } // Spike collision for (const spike of levelManager.spikes) { if (checkCollision(Player, spike)) { takeDamage(); } } // Patrol enemy collision for (let i = levelManager.patrolEnemies.length - 1; i >= 0; i--) { const enemy = levelManager.patrolEnemies[i]; if (checkCollision(Player, enemy)) { // Check if player is falling onto enemy (stomp kill) if (Player.velocityY > 0 && Player.y + Player.height < enemy.y + enemy.height / 2) { // Enemy defeated - remove and create explosion levelManager.patrolEnemies.splice(i, 1); // Death explosion - red/orange particles createParticles(enemy.x + enemy.width / 2, enemy.y + enemy.height / 2, '#ff6b6b', 20, 8); createParticles(enemy.x + enemy.width / 2, enemy.y + enemy.height / 2, '#ff4444', 15, 10); createParticles(enemy.x + enemy.width / 2, enemy.y + enemy.height / 2, '#ffaa00', 12, 12); // Small bounce Player.velocityY = -8; // Bonus score currentLevelScore += 25; totalScore += 25; scoreDisplay.textContent = totalScore; screenShake = 8; AudioSystem.play('powerUp'); // Reuse power-up sound for victory } else { takeDamage(); } } } // Power-up collection for (const powerUp of levelManager.powerUps) { if (!powerUp.collected && checkCollision(Player, powerUp)) { powerUp.collected = true; Player.applyPowerUp(powerUp.type); createParticles(powerUp.x + powerUp.width / 2, powerUp.y + powerUp.height / 2, powerUp.color, 20); triggerScreenShake(5); AudioSystem.play('powerUp'); } } // Checkpoint activation for (const checkpoint of levelManager.checkpoints) { if (!checkpoint.activated && checkCollision(Player, checkpoint)) { checkpoint.activated = true; lastCheckpoint = { x: checkpoint.x, y: checkpoint.y }; createParticles(checkpoint.x + checkpoint.width / 2, checkpoint.y + checkpoint.height / 2, '#00ff88', 25); AudioSystem.play('checkpoint'); } } // Flag (goal) collision if (levelManager.flag && checkCollision(Player, levelManager.flag)) { levelComplete(); } // Portal collision (next level) if (levelManager.portal && checkCollision(Player, levelManager.portal)) { levelComplete(); } } function draw() { ctx.save(); // Apply screen shake applyScreenShake(); // Draw background drawBackground(); // Draw level objects levelManager.draw(ctx, frameCount); // Draw player trail drawPlayerTrail(); // Draw player drawPlayer(); // Draw particles drawParticles(); ctx.restore(); } function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); } // ═══════════════════════════════════════════════════════════════ // GAME EVENTS // ═══════════════════════════════════════════════════════════════ function takeDamage() { if (!Player.takeDamage()) return; AudioSystem.play('hurt'); lives--; updateLivesDisplay(); createParticles(Player.x + Player.width / 2, Player.y + Player.height / 2, '#ff4757', 20); triggerScreenShake(15); if (lives <= 0) { gameOver(); } else { respawnAtCheckpoint(); } } function respawnAtCheckpoint() { if (lastCheckpoint) { Player.reset(lastCheckpoint.x, lastCheckpoint.y); } else { const level = LEVELS[levelManager.currentLevel]; Player.reset(level.playerStart.x, level.playerStart.y); } Player.velocityX = 0; Player.velocityY = 0; } function updateLivesDisplay() { livesContainer.innerHTML = ''; for (let i = 0; i < 3; i++) { const life = document.createElement('div'); life.className = 'life-icon' + (i >= lives ? ' lost' : ''); livesContainer.appendChild(life); } } let levelCompleteCalled = false; function levelComplete() { if (levelCompleteCalled) return; levelCompleteCalled = true; AudioSystem.play('levelComplete'); // Confetti explosion! for (let i = 0; i < 50; i++) { const colors = ['#ff4757', '#2ed573', '#1e90ff', '#ffd700', '#ff6b81', '#00ff88']; createParticles( canvas.width / 2 + (Math.random() - 0.5) * 200, canvas.height / 2, colors[Math.floor(Math.random() * colors.length)], 3, 15 ); } // Screen flash effect triggerScreenShake(20); gameRunning = false; totalScore += currentLevelScore; totalScore += Math.max(0, 100 - gameTime) * 5; // Time bonus messageTitle.textContent = '🎉 УРОВЕНЬ ПРОЙДЕН! 🎉'; messageTitle.style.color = '#00ff88'; messageStats.innerHTML = ` Монеты: ${currentLevelScore}
Бонус за время: ${Math.max(0, 100 - gameTime) * 5}
Всего очков: ${totalScore} `; messageDiv.querySelector('button').textContent = 'Следующий уровень →'; messageDiv.className = 'win'; messageDiv.style.display = 'block'; // Auto advance after delay - only when user presses a key // setTimeout(() => { // nextLevel(); // }, 3000); } function continueToNextLevel() { console.log('continueToNextLevel called, current level:', levelManager.currentLevel, 'total levels:', LEVELS.length); if (levelManager.currentLevel < LEVELS.length - 1) { nextLevel(); } else { // Game completed - reset to level 1 console.log('Game completed, restarting from level 1'); restartGame(); } } function handleMessageButton() { if (messageTitle.textContent.includes('ИГРА ОКОНЧЕНА')) { restartGame(); } else { continueToNextLevel(); } } function nextLevel() { levelCompleteCalled = false; messageDiv.style.display = 'none'; gameTime = 0; frameCount = 0; lives = 3; updateLivesDisplay(); Player.hasDoubleJump = false; Player.hasSpeedBoost = false; initLevel(levelManager.currentLevel + 1); gameRunning = true; } function gameOver() { AudioSystem.play('gameOver'); gameRunning = false; messageDiv.querySelector('button').textContent = 'Играть снова'; messageTitle.textContent = '💀 ИГРА ОКОНЧЕНА'; messageTitle.style.color = '#ff4757'; messageStats.innerHTML = ` Уровень: ${levelManager.currentLevel + 1}
Счет: ${totalScore} `; messageDiv.className = 'lose'; messageDiv.style.display = 'block'; } function gameComplete() { gameRunning = false; messageTitle.textContent = '🏆 ПОБЕДА! 🏆'; messageTitle.style.color = '#ffd700'; messageStats.innerHTML = ` Поздравляем! Вы прошли все уровни!
Итоговый счет: ${totalScore} `; messageDiv.className = 'win'; messageDiv.style.display = 'block'; } function restartGame() { levelCompleteCalled = false; messageDiv.querySelector('button').textContent = 'Играть снова'; totalScore = 0; lives = 3; gameTime = 0; frameCount = 0; particles = []; screenShake = 0; Player.hasDoubleJump = false; Player.hasSpeedBoost = false; scoreDisplay.textContent = '0'; timerDisplay.textContent = '0'; updateLivesDisplay(); levelManager.reset(); initLevel(0); messageDiv.style.display = 'none'; gameRunning = true; } function togglePause() { if (!gameRunning) return; paused = !paused; const pauseMenu = document.getElementById('pause-menu'); if (pauseMenu) { pauseMenu.style.display = paused ? 'block' : 'none'; } } // ═══════════════════════════════════════════════════════════════ // START GAME // ═══════════════════════════════════════════════════════════════ updateLivesDisplay(); initLevel(0); gameLoop(); // ═══════════════════════════════════════════════════════════════ // MENU FUNCTIONS // ═══════════════════════════════════════════════════════════════ function startGame() { document.getElementById('main-menu').style.display = 'none'; document.getElementById('loading').style.display = 'none'; document.getElementById('game-wrapper').style.display = 'block'; AudioSystem.init(); if (AudioSystem.muted) { AudioSystem.toggleMute(); } gameRunning = true; } function showSettings() { document.getElementById('settings-modal').classList.add('show'); } function hideSettings() { document.getElementById('settings-modal').classList.remove('show'); } function showAbout() { document.getElementById('about-modal').classList.add('show'); } function hideAbout() { document.getElementById('about-modal').classList.remove('show'); } function toggleSound() { const muted = AudioSystem.toggleMute(); document.getElementById('sound-toggle').textContent = muted ? 'ВЫКЛ' : 'ВКЛ'; } function toggleFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); } else { document.exitFullscreen(); } } function exitToMenu() { document.getElementById('game-wrapper').style.display = 'none'; document.getElementById('main-menu').style.display = 'flex'; document.getElementById('pause-menu').style.display = 'none'; messageDiv.style.display = 'none'; gameRunning = false; }