From 055b6468d44287dc08d1e13a7537d0e417d7d598 Mon Sep 17 00:00:00 2001 From: "maxim.dolgolyov" Date: Tue, 24 Feb 2026 18:20:59 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?js=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/entities.js | 661 +++++++++++++++++++++++++++++++++ js/game.js | 990 +++++++++++++++++++++++++++++++++++++++++++++++++ js/levels.js | 689 ++++++++++++++++++++++++++++++++++ js/sound.js | 212 +++++++++++ 4 files changed, 2552 insertions(+) create mode 100644 js/entities.js create mode 100644 js/game.js create mode 100644 js/levels.js create mode 100644 js/sound.js diff --git a/js/entities.js b/js/entities.js new file mode 100644 index 0000000..3954fd2 --- /dev/null +++ b/js/entities.js @@ -0,0 +1,661 @@ +/* ═══════════════════════════════════════════════════════════════ + GAME ENTITIES - Player, Platforms, Coins, Enemies, etc. + ═══════════════════════════════════════════════════════════════ */ + +// ═══════════════════════════════════════════════════════════════ +// PLAYER +// ═══════════════════════════════════════════════════════════════ +const Player = { + x: 60, + y: 400, + width: 28, + height: 38, + color: '#00d9ff', + velocityX: 0, + velocityY: 0, + speed: 6, + baseSpeed: 6, + sprintSpeed: 9, + jumpPower: -14, + baseJumpPower: -14, + doubleJumpPower: -12, + gravity: 0.65, + grounded: false, + facingRight: true, + invincible: false, + invincibleTimer: 0, + hasDoubleJump: false, + hasUsedDoubleJump: false, + hasSpeedBoost: false, + speedBoostTimer: 0, + + reset(x = 60, y = 400) { + this.x = x; + this.y = y; + this.velocityX = 0; + this.velocityY = 0; + this.grounded = false; + this.invincible = false; + this.invincibleTimer = 0; + this.hasUsedDoubleJump = false; + this.hasSpeedBoost = false; + this.speedBoostTimer = 0; + }, + + update() { + // Speed boost timer + if (this.hasSpeedBoost) { + this.speedBoostTimer--; + if (this.speedBoostTimer <= 0) { + this.hasSpeedBoost = false; + this.speed = this.baseSpeed; + } + } + + // Invincibility timer + if (this.invincible) { + this.invincibleTimer--; + if (this.invincibleTimer <= 0) { + this.invincible = false; + } + } + }, + + jump() { + if (this.grounded) { + this.velocityY = this.jumpPower; + this.grounded = false; + this.hasUsedDoubleJump = false; + return true; + } else if (this.hasDoubleJump && !this.hasUsedDoubleJump) { + this.velocityY = this.doubleJumpPower; + this.hasUsedDoubleJump = true; + return true; + } + return false; + }, + + takeDamage() { + if (this.invincible) return false; + this.invincible = true; + this.invincibleTimer = 90; // 1.5 seconds at 60fps + return true; + }, + + applyPowerUp(type) { + switch(type) { + case 'doubleJump': + this.hasDoubleJump = true; + break; + case 'speedBoost': + this.hasSpeedBoost = true; + this.speed = this.sprintSpeed; + this.speedBoostTimer = 300; // 5 seconds + break; + case 'invincibility': + this.invincible = true; + this.invincibleTimer = 300; + break; + } + } +}; + +// Boss Enemy +const Boss = { + x: 0, y: 0, width: 60, height: 60, + health: 5, maxHealth: 5, + velocityX: 2, velocityY: 0, gravity: 0.5, + patrolLeft: 0, patrolRight: 0, + hitTimer: 0, + + init(x, y, left, right) { + this.x = x; this.y = y; + this.patrolLeft = left; this.patrolRight = right; + this.health = this.maxHealth; + }, + + update() { + if (this.hitTimer > 0) this.hitTimer--; + this.x += this.velocityX; + if (this.x <= this.patrolLeft || this.x + this.width >= this.patrolRight) this.velocityX *= -1; + }, + + draw() { + const pulse = Math.sin(frameCount * 0.1) * 0.2 + 0.8; + ctx.shadowColor = this.hitTimer > 0 ? '#ff0000' : '#ff00ff'; + ctx.shadowBlur = 20 * pulse; + ctx.fillStyle = this.hitTimer > 0 ? '#ff4444' : '#ff00ff'; + ctx.beginPath(); + ctx.arc(this.x + 30, this.y + 30, 25, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#fff'; + ctx.beginPath(); ctx.arc(this.x + 20, this.y + 25, 8, 0, Math.PI * 2); + ctx.arc(this.x + 40, this.y + 25, 8, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + }, + + checkCollision(player) { + return player.x < this.x + this.width && player.x + player.width > this.x && + player.y < this.y + this.height && player.y + player.height > this.y; + }, + + takeDamage() { + this.health--; + this.hitTimer = 15; + screenShake = 10; + createParticles(this.x + 30, this.y + 30, '#ff00ff', 15); + return this.health <= 0; + } +}; + +// Secret Gem +const SecretGem = { + x: 0, y: 0, width: 20, height: 20, collected: false, bobOffset: 0, + + init(x, y) { this.x = x; this.y = y; this.collected = false; }, + update() { this.bobOffset = Math.sin(frameCount * 0.08) * 5; }, + + draw() { + if (this.collected) return; + const y = this.y + this.bobOffset; + const glow = Math.sin(frameCount * 0.15) * 0.3 + 0.7; + ctx.shadowColor = '#ffd700'; ctx.shadowBlur = 20 * glow; + ctx.fillStyle = '#ffd700'; + ctx.beginPath(); + ctx.moveTo(this.x + 10, y); + ctx.lineTo(this.x + 20, y + 10); + ctx.lineTo(this.x + 10, y + 20); + ctx.lineTo(this.x, y + 10); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = '#fff'; + ctx.beginPath(); ctx.arc(this.x + 7, y + 7, 3, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + }, + + checkCollision(player) { + if (this.collected) return false; + return player.x < this.x + this.width && player.x + player.width > this.x && + player.y < this.y + this.height && player.y + player.height > this.y; + } +}; + +// ═══════════════════════════════════════════════════════════════ +// PLATFORMS +// ═══════════════════════════════════════════════════════════════ +class Platform { + constructor(x, y, width, height, color = '#3a3a5a', isMoving = false, moveData = null) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.color = color; + this.isMoving = isMoving; + + if (isMoving && moveData) { + this.startX = moveData.startX; + this.endX = moveData.endX; + this.startY = moveData.startY; + this.endY = moveData.endY; + this.speed = moveData.speed || 2; + this.direction = 1; + this.moveType = moveData.moveType || 'horizontal'; // 'horizontal', 'vertical', 'circular' + } + } + + update() { + if (!this.isMoving) return; + + switch(this.moveType) { + case 'horizontal': + this.x += this.speed * this.direction; + if (this.x >= this.endX || this.x <= this.startX) { + this.direction *= -1; + } + break; + case 'vertical': + this.y += this.speed * this.direction; + if (this.y >= this.endY || this.y <= this.startY) { + this.direction *= -1; + } + break; + case 'circular': + const time = Date.now() / 1000; + this.x = this.startX + Math.sin(time * this.speed) * (this.endX - this.startX) / 2; + this.y = this.startY + Math.cos(time * this.speed) * (this.endY - this.startY) / 2; + break; + } + } + + draw(ctx, frameCount) { + // Main platform body with gradient + const platformGradient = ctx.createLinearGradient(this.x, this.y, this.x, this.y + this.height); + platformGradient.addColorStop(0, this.color); + platformGradient.addColorStop(1, this.darkenColor(this.color, 30)); + ctx.fillStyle = platformGradient; + ctx.fillRect(this.x, this.y, this.width, this.height); + + // Top highlight (bevel effect) + ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; + ctx.fillRect(this.x, this.y, this.width, 4); + + // Bottom shadow + ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'; + ctx.fillRect(this.x, this.y + this.height - 3, this.width, 3); + + // Side highlights + ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; + ctx.fillRect(this.x, this.y, 2, this.height); + ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; + ctx.fillRect(this.x + this.width - 2, this.y, 2, this.height); + + // Edge glow for moving platforms + if (this.isMoving) { + ctx.shadowColor = this.color; + ctx.shadowBlur = 15; + ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.lineWidth = 2; + ctx.strokeRect(this.x, this.y, this.width, this.height); + ctx.shadowBlur = 0; + } + } + + darkenColor(color, percent) { + // Simple color darkening + const num = parseInt(color.replace('#', ''), 16); + const amt = Math.round(2.55 * percent); + const R = Math.max((num >> 16) - amt, 0); + const G = Math.max((num >> 8 & 0x00FF) - amt, 0); + const B = Math.max((num & 0x0000FF) - amt, 0); + return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1); + } +} + +// ═══════════════════════════════════════════════════════════════ +// COINS +// ═══════════════════════════════════════════════════════════════ +class Coin { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = 20; + this.height = 20; + this.collected = false; + this.rotation = Math.random() * Math.PI * 2; + } + + update(frameCount) { + if (!this.collected) { + this.rotation += 0.05; + } + } + + draw(ctx, frameCount) { + if (this.collected) return; + + const bobOffset = Math.sin(frameCount * 0.08) * 3; + const scale = Math.abs(Math.cos(this.rotation)); + + ctx.save(); + ctx.translate(this.x + this.width / 2, this.y + this.height / 2 + bobOffset); + ctx.scale(scale, 1); + + // Glow + ctx.shadowColor = '#ffd700'; + ctx.shadowBlur = 20; + + // Coin body + const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, 10); + gradient.addColorStop(0, '#fff8dc'); + gradient.addColorStop(0.5, '#ffd700'); + gradient.addColorStop(1, '#b8860b'); + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.arc(0, 0, 10, 0, Math.PI * 2); + ctx.fill(); + + // Inner detail + ctx.fillStyle = '#daa520'; + ctx.beginPath(); + ctx.arc(0, 0, 6, 0, Math.PI * 2); + ctx.fill(); + + ctx.restore(); + ctx.shadowBlur = 0; + } +} + +// ═══════════════════════════════════════════════════════════════ +// ENEMIES - Spikes +// ═══════════════════════════════════════════════════════════════ +class Spike { + constructor(x, y, width = 30, height = 22) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + draw(ctx) { + ctx.fillStyle = '#ff4757'; + const spikeCount = Math.floor(this.width / 15); + const spikeWidth = this.width / spikeCount; + + for (let i = 0; i < spikeCount; i++) { + ctx.beginPath(); + ctx.moveTo(this.x + i * spikeWidth, this.y + this.height); + ctx.lineTo(this.x + i * spikeWidth + spikeWidth / 2, this.y); + ctx.lineTo(this.x + (i + 1) * spikeWidth, this.y + this.height); + ctx.fill(); + } + + // Glow effect + ctx.shadowColor = '#ff4757'; + ctx.shadowBlur = 10; + ctx.fillStyle = 'rgba(255, 71, 87, 0.5)'; + for (let i = 0; i < spikeCount; i++) { + ctx.beginPath(); + ctx.moveTo(this.x + i * spikeWidth + spikeWidth / 2, this.y); + ctx.lineTo(this.x + i * spikeWidth + spikeWidth / 2, this.y + 5); + ctx.stroke(); + } + ctx.shadowBlur = 0; + } +} + +// ═══════════════════════════════════════════════════════════════ +// ENEMIES - Moving Patrol Enemy +// ═══════════════════════════════════════════════════════════════ +class PatrolEnemy { + constructor(x, y, patrolRange = 100, speed = 1.5) { + this.x = x; + this.y = y; + this.width = 25; + this.height = 30; + this.startX = x; + this.endX = x + patrolRange; + this.speed = speed; + this.direction = 1; + this.color = '#ff6b6b'; + } + + update() { + this.x += this.speed * this.direction; + if (this.x >= this.endX || this.x <= this.startX) { + this.direction *= -1; + } + } + + draw(ctx, frameCount) { + // Body + ctx.fillStyle = this.color; + ctx.fillRect(this.x, this.y, this.width, this.height); + + // Gradient overlay + const gradient = ctx.createLinearGradient(this.x, this.y, this.x, this.y + this.height); + gradient.addColorStop(0, 'rgba(255, 255, 255, 0.2)'); + gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)'); + ctx.fillStyle = gradient; + ctx.fillRect(this.x, this.y, this.width, this.height); + + // Eyes + const eyeY = this.y + 8; + ctx.fillStyle = '#fff'; + ctx.fillRect(this.x + 4, eyeY, 6, 6); + ctx.fillRect(this.x + 15, eyeY, 6, 6); + + // Pupils (follow player direction) + ctx.fillStyle = '#000'; + const pupilOffset = this.direction > 0 ? 2 : 0; + ctx.fillRect(this.x + 5 + pupilOffset, eyeY + 2, 3, 3); + ctx.fillRect(this.x + 16 + pupilOffset, eyeY + 2, 3, 3); + + // Angry eyebrows + ctx.fillStyle = '#333'; + ctx.beginPath(); + ctx.moveTo(this.x + 3, eyeY - 3); + ctx.lineTo(this.x + 10, eyeY); + ctx.lineTo(this.x + 10, eyeY - 2); + ctx.lineTo(this.x + 5, eyeY - 4); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(this.x + 22, eyeY - 3); + ctx.lineTo(this.x + 15, eyeY); + ctx.lineTo(this.x + 15, eyeY - 2); + ctx.lineTo(this.x + 20, eyeY - 4); + ctx.fill(); + + // Glow + ctx.shadowColor = this.color; + ctx.shadowBlur = 10; + ctx.strokeStyle = 'rgba(255, 107, 107, 0.5)'; + ctx.lineWidth = 2; + ctx.strokeRect(this.x, this.y, this.width, this.height); + ctx.shadowBlur = 0; + } +} + +// ═══════════════════════════════════════════════════════════════ +// FLAG (Goal) +// ═══════════════════════════════════════════════════════════════ +class Flag { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = 30; + this.height = 40; + } + + draw(ctx, frameCount) { + // Pole + ctx.fillStyle = '#8b4513'; + ctx.fillRect(this.x, this.y, 6, this.height); + + // Pole gradient + const poleGradient = ctx.createLinearGradient(this.x, this.y, this.x + 6, this.y); + poleGradient.addColorStop(0, '#a0522d'); + poleGradient.addColorStop(0.5, '#8b4513'); + poleGradient.addColorStop(1, '#5d3a1a'); + ctx.fillStyle = poleGradient; + ctx.fillRect(this.x, this.y, 6, this.height); + + // Flag cloth with wave animation + const waveOffset = Math.sin(frameCount * 0.08) * 8; + + ctx.fillStyle = '#ff6b6b'; + ctx.beginPath(); + ctx.moveTo(this.x + 6, this.y + 2); + ctx.quadraticCurveTo(this.x + 20 + waveOffset, this.y + 8, this.x + 36, this.y + 12); + ctx.quadraticCurveTo(this.x + 20 + waveOffset, this.y + 20, this.x + 36, this.y + 28); + ctx.quadraticCurveTo(this.x + 20 + waveOffset, this.y + 32, this.x + 6, this.y + 35); + ctx.closePath(); + ctx.fill(); + + // Flag highlight + const flagGradient = ctx.createLinearGradient(this.x + 6, this.y, this.x + 36, this.y + 35); + flagGradient.addColorStop(0, 'rgba(255, 255, 255, 0.3)'); + flagGradient.addColorStop(1, 'rgba(0, 0, 0, 0.2)'); + ctx.fillStyle = flagGradient; + ctx.beginPath(); + ctx.moveTo(this.x + 6, this.y + 2); + ctx.quadraticCurveTo(this.x + 20 + waveOffset, this.y + 8, this.x + 36, this.y + 12); + ctx.quadraticCurveTo(this.x + 20 + waveOffset, this.y + 20, this.x + 36, this.y + 28); + ctx.quadraticCurveTo(this.x + 20 + waveOffset, this.y + 32, this.x + 6, this.y + 35); + ctx.closePath(); + ctx.fill(); + + // Gold orb on top + ctx.fillStyle = '#ffd700'; + ctx.beginPath(); + ctx.arc(this.x + 3, this.y, 8, 0, Math.PI * 2); + ctx.fill(); + + // Orb shine + ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; + ctx.beginPath(); + ctx.arc(this.x + 1, this.y - 2, 3, 0, Math.PI * 2); + ctx.fill(); + + // Glow + ctx.shadowColor = '#ffd700'; + ctx.shadowBlur = 20; + ctx.strokeStyle = 'rgba(255, 215, 0, 0.5)'; + ctx.lineWidth = 2; + ctx.strokeRect(this.x + 6, this.y + 2, 30, 33); + ctx.shadowBlur = 0; + } +} + +// ═══════════════════════════════════════════════════════════════ +// POWER-UPS +// ═══════════════════════════════════════════════════════════════ +class PowerUp { + constructor(x, y, type) { + this.x = x; + this.y = y; + this.width = 24; + this.height = 24; + this.type = type; // 'doubleJump', 'speedBoost', 'invincibility' + this.collected = false; + + switch(type) { + case 'doubleJump': + this.color = '#00ff88'; + this.icon = '⬆️'; + break; + case 'speedBoost': + this.color = '#ff00ff'; + this.icon = '⚡'; + break; + case 'invincibility': + this.color = '#ffff00'; + this.icon = '🛡️'; + break; + } + } + + update(frameCount) { + if (this.collected) return; + } + + draw(ctx, frameCount) { + if (this.collected) return; + + const floatOffset = Math.sin(frameCount * 0.1) * 5; + const rotation = frameCount * 0.02; + + ctx.save(); + ctx.translate(this.x + this.width / 2, this.y + this.height / 2 + floatOffset); + ctx.rotate(rotation); + + // Glow + ctx.shadowColor = this.color; + ctx.shadowBlur = 20; + + // Body + ctx.fillStyle = this.color; + ctx.beginPath(); + ctx.arc(0, 0, 12, 0, Math.PI * 2); + ctx.fill(); + + // Inner circle + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.beginPath(); + ctx.arc(0, 0, 8, 0, Math.PI * 2); + ctx.fill(); + + ctx.restore(); + + // Icon (non-rotating) + ctx.font = '14px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(this.icon, this.x + this.width / 2, this.y + this.height / 2 + floatOffset + 5); + + ctx.shadowBlur = 0; + } +} + +// ═══════════════════════════════════════════════════════════════ +// CHECKPOINTS +// ═══════════════════════════════════════════════════════════════ +class Checkpoint { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = 30; + this.height = 40; + this.activated = false; + } + + draw(ctx, frameCount) { + // Pole + ctx.fillStyle = this.activated ? '#00ff88' : '#666'; + ctx.fillRect(this.x + 12, this.y, 6, this.height); + + // Flag + const waveOffset = Math.sin(frameCount * 0.1) * 5; + ctx.fillStyle = this.activated ? '#00ff88' : '#888'; + ctx.beginPath(); + ctx.moveTo(this.x + 18, this.y + 2); + ctx.quadraticCurveTo(this.x + 28 + waveOffset, this.y + 10, this.x + 35, this.y + 12); + ctx.quadraticCurveTo(this.x + 28 + waveOffset, this.y + 22, this.x + 18, this.y + 25); + ctx.closePath(); + ctx.fill(); + + // Glow when activated + if (this.activated) { + ctx.shadowColor = '#00ff88'; + ctx.shadowBlur = 15; + ctx.strokeStyle = 'rgba(0, 255, 136, 0.5)'; + ctx.lineWidth = 2; + ctx.strokeRect(this.x, this.y, this.width, this.height); + ctx.shadowBlur = 0; + } + } +} + +// ═══════════════════════════════════════════════════════════════ +// PORTAL (Level Exit) +// ═══════════════════════════════════════════════════════════════ +class Portal { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = 50; + this.height = 60; + } + + draw(ctx, frameCount) { + const rotation = frameCount * 0.03; + + // Outer ring + ctx.save(); + ctx.translate(this.x + this.width / 2, this.y + this.height / 2); + ctx.rotate(rotation); + + // Glow + ctx.shadowColor = '#aa00ff'; + ctx.shadowBlur = 30; + + // Multiple rings + for (let i = 0; i < 3; i++) { + ctx.strokeStyle = `rgba(170, 0, 255, ${0.8 - i * 0.2})`; + ctx.lineWidth = 3 - i; + ctx.beginPath(); + ctx.arc(0, 0, 25 - i * 5, 0, Math.PI * 2); + ctx.stroke(); + } + + // Inner swirl + ctx.fillStyle = 'rgba(100, 0, 200, 0.5)'; + ctx.beginPath(); + ctx.arc(0, 0, 15, 0, Math.PI * 2); + ctx.fill(); + + ctx.restore(); + ctx.shadowBlur = 0; + } +} diff --git a/js/game.js b/js/game.js new file mode 100644 index 0000000..47f0419 --- /dev/null +++ b/js/game.js @@ -0,0 +1,990 @@ +/* ═══════════════════════════════════════════════════════════════ + 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; +} diff --git a/js/levels.js b/js/levels.js new file mode 100644 index 0000000..8283fe0 --- /dev/null +++ b/js/levels.js @@ -0,0 +1,689 @@ +/* ═══════════════════════════════════════════════════════════════ + GAME LEVELS - Level definitions for multi-level platformer + ═══════════════════════════════════════════════════════════════ */ + +const LEVELS = [ + // ═══════════════════════════════════════════════════════════════ + // LEVEL 1 - Tutorial + // ═══════════════════════════════════════════════════════════════ + { + name: "НАЧАЛО", + playerStart: { x: 60, y: 420 }, + platforms: [ + // Ground + { x: 0, y: 510, width: 900, height: 40, color: '#2a2a4a', isGround: true }, + // Wall + { x: 0, y: 0, width: 30, height: 550, color: '#1a1a3a', isGround: true }, + // Platform + { x: 870, y: 0, width: 30, height: 550, color: '#1a1a3a', }, + // Platform + { x: 150, y: 420, width: 120, height: 20, color: '#4a3a6a', }, + // Platform + { x: 320, y: 340, width: 100, height: 20, color: '#4a3a6a', }, + // Platform + { x: 480, y: 260, width: 120, height: 20, color: '#4a3a6a', }, + // Platform + { x: 280, y: 180, width: 100, height: 20, color: '#4a3a6a', }, + // Platform + { x: 550, y: 120, width: 150, height: 20, color: '#4a3a6a', }, + // Platform + { x: 100, y: 280, width: 80, height: 20, color: '#4a3a6a', }, + ], + movingPlatforms: [ + { x: 200, y: 300, width: 70, height: 18, color: '#6a5aaa', startX: 180, endX: 280, speed: 1.5, direction: 1, moveType: 'horizontal' }, + ], + coins: [ + { x: 200, y: 380 }, + { x: 360, y: 300 }, + { x: 530, y: 220 }, + { x: 320, y: 140 }, + { x: 620, y: 80 }, + { x: 140, y: 240 }, + ], + spikes: [ + { x: 400, y: 488, width: 30, height: 20 }, + { x: 600, y: 488, width: 30, height: 20 }, + ], + patrolEnemies: [ + ], + powerUps: [ + ], + checkpoints: [ + { x: 50, y: 470 }, + ], + flag: { x: 680, y: 60 }, + portal: null + }, + + // ═══════════════════════════════════════════════════════════════ + // LEVEL 2 - Moving Platforms + // ═══════════════════════════════════════════════════════════════ + { + name: "НЕБО", + playerStart: { x: 60, y: 420 }, + platforms: [ + { x: 0, y: 510, width: 200, height: 40, color: '#2a2a4a' }, + { x: 0, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 870, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + // Islands + { x: 80, y: 380, width: 80, height: 20, color: '#4a3a6a' }, + { x: 300, y: 320, width: 80, height: 20, color: '#4a3a6a' }, + { x: 550, y: 250, width: 80, height: 20, color: '#4a3a6a' }, + { x: 750, y: 350, width: 100, height: 20, color: '#4a3a6a' }, + { x: 700, y: 150, width: 100, height: 20, color: '#4a3a6a' } + ], + movingPlatforms: [ + { x: 220, y: 380, width: 70, height: 18, color: '#6a5aaa', startX: 200, endX: 320, speed: 2, direction: 1, moveType: 'horizontal' }, + { x: 420, y: 300, width: 70, height: 18, color: '#6a5aaa', startX: 400, endX: 520, speed: 1.8, direction: 1, moveType: 'horizontal' }, + { x: 650, y: 250, width: 60, height: 18, color: '#6a5aaa', startX: 620, endX: 720, speed: 2.2, direction: 1, moveType: 'horizontal' }, + { x: 450, y: 150, width: 70, height: 18, color: '#6a5aaa', startX: 400, endY: 150, endX: 600, speed: 2, direction: 1, moveType: 'horizontal' } + ], + coins: [ + { x: 110, y: 340 }, + { x: 330, y: 280 }, + { x: 580, y: 210 }, + { x: 790, y: 310 }, + { x: 740, y: 110 }, + { x: 500, y: 110 } + ], + spikes: [ + { x: 280, y: 488, width: 100, height: 22 }, + { x: 500, y: 488, width: 80, height: 22 } + ], + patrolEnemies: [ + { x: 700, y: 320, patrolRange: 80, speed: 1.5 } + ], + powerUps: [ + { x: 340, y: 100, type: 'doubleJump' } + ], + checkpoints: [ + { x: 50, y: 470 }, + { x: 450, y: 270 } + ], + flag: { x: 780, y: 110 }, + portal: null + }, + + // ═══════════════════════════════════════════════════════════════ + // LEVEL 3 - Enemies & Power-ups + // ═══════════════════════════════════════════════════════════════ + { + name: "ОПАСНОСТЬ", + playerStart: { x: 60, y: 420 }, + platforms: [ + { x: 0, y: 510, width: 250, height: 40, color: '#2a2a4a' }, + { x: 0, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 870, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + // Platforms with enemies + { x: 100, y: 400, width: 100, height: 20, color: '#4a3a6a' }, + { x: 280, y: 350, width: 120, height: 20, color: '#4a3a6a' }, + { x: 500, y: 280, width: 100, height: 20, color: '#4a3a6a' }, + { x: 300, y: 200, width: 100, height: 20, color: '#4a3a6a' }, + { x: 550, y: 150, width: 120, height: 20, color: '#4a3a6a' }, + { x: 750, y: 250, width: 100, height: 20, color: '#4a3a6a' }, + { x: 700, y: 400, width: 150, height: 20, color: '#4a3a6a' } + ], + movingPlatforms: [ + { x: 450, y: 180, width: 70, height: 18, color: '#6a5aaa', startX: 420, endX: 540, speed: 2.5, direction: 1, moveType: 'horizontal' } + ], + coins: [ + { x: 140, y: 360 }, + { x: 330, y: 310 }, + { x: 540, y: 240 }, + { x: 340, y: 160 }, + { x: 600, y: 110 }, + { x: 790, y: 210 }, + { x: 770, y: 360 } + ], + spikes: [ + { x: 350, y: 328, width: 40, height: 22 }, + { x: 600, y: 488, width: 60, height: 22 } + ], + patrolEnemies: [ + { x: 300, y: 170, patrolRange: 80, speed: 2 }, + { x: 560, y: 120, patrolRange: 90, speed: 1.8 }, + { x: 720, y: 370, patrolRange: 100, speed: 2.2 } + ], + powerUps: [ + { x: 560, y: 110, type: 'speedBoost' }, + { x: 780, y: 370, type: 'invincibility' } + ], + checkpoints: [ + { x: 50, y: 470 }, + { x: 330, y: 170 } + ], + flag: { x: 820, y: 60 }, + portal: { x: 800, y: 60 } + }, + + // ═══════════════════════════════════════════════════════════════ + // LEVEL 4 - Vertical Challenge + // ═══════════════════════════════════════════════════════════════ + { + name: "ВЕРШИНА", + playerStart: { x: 60, y: 450 }, + platforms: [ + { x: 0, y: 510, width: 250, height: 40, color: '#2a2a4a' }, + { x: 0, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 870, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + // Vertical climbing + { x: 180, y: 420, width: 80, height: 18, color: '#4a3a6a' }, + { x: 80, y: 350, width: 80, height: 18, color: '#4a3a6a' }, + { x: 250, y: 320, width: 80, height: 18, color: '#4a3a6a' }, + { x: 450, y: 380, width: 80, height: 18, color: '#4a3a6a' }, + { x: 350, y: 280, width: 80, height: 18, color: '#4a3a6a' }, + { x: 550, y: 220, width: 80, height: 18, color: '#4a3a6a' }, + { x: 700, y: 280, width: 80, height: 18, color: '#4a3a6a' }, + { x: 600, y: 150, width: 100, height: 18, color: '#4a3a6a' }, + { x: 400, y: 100, width: 100, height: 18, color: '#4a3a6a' }, + { x: 700, y: 80, width: 100, height: 18, color: '#4a3a6a' } + ], + movingPlatforms: [ + { x: 120, y: 280, width: 60, height: 18, color: '#6a5aaa', startX: 100, endX: 200, speed: 2, direction: 1, moveType: 'horizontal' }, + { x: 450, y: 120, width: 60, height: 18, color: '#6a5aaa', startX: 430, endX: 530, speed: 1.5, direction: 1, moveType: 'horizontal' } + ], + coins: [ + { x: 210, y: 420 }, + { x: 110, y: 340 }, + { x: 280, y: 280 }, + { x: 480, y: 340 }, + { x: 380, y: 240 }, + { x: 580, y: 180 }, + { x: 730, y: 240 }, + { x: 640, y: 110 }, + { x: 440, y: 60 }, + { x: 740, y: 40 } + ], + spikes: [ + { x: 300, y: 488, width: 60, height: 22 }, + { x: 500, y: 488, width: 80, height: 22 } + ], + patrolEnemies: [ + { x: 370, y: 70, patrolRange: 70, speed: 2.5 } + ], + powerUps: [ + { x: 420, y: 250, type: 'doubleJump' }, + { x: 600, y: 50, type: 'speedBoost' } + ], + checkpoints: [ + { x: 50, y: 490 }, + { x: 350, y: 80 } + ], + flag: { x: 770, y: 40 }, + portal: null + }, + + // ═══════════════════════════════════════════════════════════════ + // LEVEL 5 - Final Boss Stage + // ═══════════════════════════════════════════════════════════════ + { + name: "ФИНАЛ", + playerStart: { x: 60, y: 450 }, + platforms: [ + { x: 0, y: 510, width: 200, height: 40, color: '#2a2a4a' }, + { x: 0, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 870, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + // Gauntlet + { x: 150, y: 440, width: 60, height: 18, color: '#4a3a6a' }, + { x: 280, y: 400, width: 60, height: 18, color: '#4a3a6a' }, + { x: 400, y: 360, width: 60, height: 18, color: '#4a3a6a' }, + { x: 520, y: 320, width: 60, height: 18, color: '#4a3a6a' }, + { x: 650, y: 280, width: 60, height: 18, color: '#4a3a6a' }, + { x: 750, y: 350, width: 100, height: 18, color: '#4a3a6a' }, + { x: 700, y: 200, width: 80, height: 18, color: '#4a3a6a' }, + { x: 500, y: 150, width: 80, height: 18, color: '#4a3a6a' }, + { x: 300, y: 120, width: 100, height: 18, color: '#4a3a6a' }, + { x: 100, y: 180, width: 100, height: 18, color: '#4a3a6a' }, + { x: 750, y: 80, width: 100, height: 18, color: '#4a3a6a' } + ], + movingPlatforms: [ + { x: 230, y: 350, width: 50, height: 18, color: '#6a5aaa', startX: 220, endX: 300, speed: 3, direction: 1, moveType: 'horizontal' }, + { x: 580, y: 250, width: 50, height: 18, color: '#6a5aaa', startX: 560, endX: 660, speed: 2.5, direction: 1, moveType: 'horizontal' }, + { x: 400, y: 80, width: 60, height: 18, color: '#6a5aaa', startX: 380, endX: 480, speed: 2, direction: 1, moveType: 'horizontal' } + ], + coins: [ + { x: 170, y: 400 }, + { x: 300, y: 360 }, + { x: 420, y: 320 }, + { x: 540, y: 280 }, + { x: 670, y: 240 }, + { x: 780, y: 310 }, + { x: 730, y: 160 }, + { x: 530, y: 110 }, + { x: 340, y: 80 }, + { x: 140, y: 140 }, + { x: 790, y: 40 } + ], + spikes: [ + { x: 250, y: 488, width: 80, height: 22 }, + { x: 450, y: 488, width: 80, height: 22 }, + { x: 650, y: 488, width: 80, height: 22 } + ], + patrolEnemies: [ + { x: 170, y: 150, patrolRange: 60, speed: 2 }, + { x: 320, y: 90, patrolRange: 70, speed: 2.5 }, + { x: 510, y: 120, patrolRange: 60, speed: 3 }, + { x: 760, y: 50, patrolRange: 60, speed: 2 } + ], + powerUps: [ + { x: 730, y: 320, type: 'doubleJump' }, + { x: 510, y: 80, type: 'speedBoost' }, + { x: 130, y: 140, type: 'invincibility' } + ], + checkpoints: [ + { x: 50, y: 470 }, + { x: 730, y: 50 } + ], + flag: { x: 800, y: 40 }, + portal: { x: 780, y: 40 } + }, + + // ═══════════════════════════════════════════════════════════════ + // LEVEL 6 - The Gauntlet + // ═══════════════════════════════════════════════════════════════ + { + name: "ИСПЫТАНИЕ", + playerStart: { x: 60, y: 450 }, + platforms: [ + { x: 0, y: 510, width: 150, height: 40, color: '#2a2a4a' }, + { x: 0, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 870, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 180, y: 450, width: 60, height: 18, color: '#4a3a6a' }, + { x: 300, y: 400, width: 60, height: 18, color: '#4a3a6a' }, + { x: 420, y: 350, width: 60, height: 18, color: '#4a3a6a' }, + { x: 540, y: 300, width: 60, height: 18, color: '#4a3a6a' }, + { x: 660, y: 250, width: 60, height: 18, color: '#4a3a6a' }, + { x: 750, y: 350, width: 100, height: 18, color: '#4a3a6a' }, + { x: 600, y: 150, width: 80, height: 18, color: '#4a3a6a' }, + { x: 400, y: 100, width: 80, height: 18, color: '#4a3a6a' }, + { x: 200, y: 150, width: 80, height: 18, color: '#4a3a6a' }, + { x: 50, y: 220, width: 80, height: 18, color: '#4a3a6a' } + ], + movingPlatforms: [ + { x: 250, y: 320, width: 50, height: 18, color: '#6a5aaa', startX: 230, endX: 310, speed: 2.5, direction: 1, moveType: 'horizontal' }, + { x: 480, y: 220, width: 50, height: 18, color: '#6a5aaa', startX: 460, endX: 540, speed: 2.2, direction: 1, moveType: 'horizontal' } + ], + coins: [ + { x: 200, y: 410 }, { x: 320, y: 360 }, { x: 440, y: 310 }, + { x: 560, y: 260 }, { x: 680, y: 210 }, { x: 790, y: 310 }, + { x: 630, y: 110 }, { x: 430, y: 60 }, { x: 230, y: 110 }, + { x: 80, y: 180 } + ], + spikes: [ + { x: 200, y: 488, width: 80, height: 22 }, { x: 400, y: 488, width: 80, height: 22 }, + { x: 600, y: 488, width: 80, height: 22 } + ], + patrolEnemies: [ + { x: 380, y: 70, patrolRange: 60, speed: 3 } + ], + powerUps: [ + { x: 750, y: 310, type: 'speedBoost' } + ], + checkpoints: [ + { x: 50, y: 470 }, { x: 400, y: 70 } + ], + flag: { x: 80, y: 180 }, + portal: null + }, + + // ═══════════════════════════════════════════════════════════════ + // LEVEL 7 - Speed Run + // ═══════════════════════════════════════════════════════════════ + { + name: "ГОНКА", + playerStart: { x: 60, y: 420 }, + platforms: [ + { x: 0, y: 510, width: 300, height: 40, color: '#2a2a4a' }, + { x: 0, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 870, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 350, y: 450, width: 80, height: 18, color: '#4a3a6a' }, + { x: 500, y: 380, width: 80, height: 18, color: '#4a3a6a' }, + { x: 650, y: 310, width: 80, height: 18, color: '#4a3a6a' }, + { x: 750, y: 400, width: 100, height: 18, color: '#4a3a6a' }, + { x: 700, y: 200, width: 80, height: 18, color: '#4a3a6a' }, + { x: 500, y: 150, width: 80, height: 18, color: '#4a3a6a' }, + { x: 300, y: 120, width: 80, height: 18, color: '#4a3a6a' }, + { x: 100, y: 180, width: 80, height: 18, color: '#4a3a6a' } + ], + movingPlatforms: [ + { x: 200, y: 350, width: 60, height: 18, color: '#6a5aaa', startX: 180, endX: 280, speed: 3.5, direction: 1, moveType: 'horizontal' }, + { x: 400, y: 250, width: 60, height: 18, color: '#6a5aaa', startX: 380, endX: 480, speed: 3, direction: 1, moveType: 'horizontal' }, + { x: 600, y: 100, width: 60, height: 18, color: '#6a5aaa', startX: 580, endX: 680, speed: 2.5, direction: 1, moveType: 'horizontal' } + ], + coins: [ + { x: 380, y: 410 }, { x: 530, y: 340 }, { x: 680, y: 270 }, + { x: 790, y: 360 }, { x: 730, y: 160 }, { x: 530, y: 110 }, + { x: 330, y: 80 }, { x: 130, y: 140 } + ], + spikes: [ + { x: 300, y: 488, width: 60, height: 22 }, { x: 500, y: 488, width: 60, height: 22 } + ], + patrolEnemies: [ + { x: 500, y: 120, patrolRange: 60, speed: 3.5 }, + { x: 300, y: 90, patrolRange: 60, speed: 3 } + ], + powerUps: [ + { x: 330, y: 80, type: 'doubleJump' }, + { x: 130, y: 140, type: 'speedBoost' } + ], + checkpoints: [ + { x: 50, y: 470 }, { x: 500, y: 120 } + ], + flag: { x: 130, y: 140 }, + portal: null + }, + + // ═══════════════════════════════════════════════════════════════ + // LEVEL 8 - The Tower + // ═══════════════════════════════════════════════════════════════ + { + name: "БАШНЯ", + playerStart: { x: 60, y: 480 }, + platforms: [ + { x: 0, y: 530, width: 120, height: 20, color: '#2a2a4a' }, + { x: 0, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 870, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + // Tower ascending + { x: 150, y: 470, width: 60, height: 16, color: '#4a3a6a' }, + { x: 80, y: 400, width: 60, height: 16, color: '#4a3a6a' }, + { x: 180, y: 340, width: 60, height: 16, color: '#4a3a6a' }, + { x: 100, y: 280, width: 60, height: 16, color: '#4a3a6a' }, + { x: 200, y: 220, width: 60, height: 16, color: '#4a3a6a' }, + { x: 350, y: 300, width: 60, height: 16, color: '#4a3a6a' }, + { x: 500, y: 250, width: 60, height: 16, color: '#4a3a6a' }, + { x: 650, y: 200, width: 60, height: 16, color: '#4a3a6a' }, + { x: 750, y: 280, width: 80, height: 16, color: '#4a3a6a' }, + { x: 700, y: 150, width: 80, height: 16, color: '#4a3a6a' }, + { x: 550, y: 80, width: 80, height: 16, color: '#4a3a6a' }, + { x: 350, y: 60, width: 100, height: 16, color: '#4a3a6a' } + ], + movingPlatforms: [ + { x: 280, y: 250, width: 50, height: 16, color: '#6a5aaa', startX: 260, endX: 340, speed: 2, direction: 1, moveType: 'horizontal' }, + { x: 450, y: 160, width: 50, height: 16, color: '#6a5aaa', startX: 430, endX: 510, speed: 1.8, direction: 1, moveType: 'horizontal' } + ], + coins: [ + { x: 170, y: 430 }, { x: 100, y: 360 }, { x: 200, y: 300 }, + { x: 120, y: 240 }, { x: 220, y: 180 }, { x: 370, y: 260 }, + { x: 520, y: 210 }, { x: 670, y: 160 }, { x: 780, y: 240 }, + { x: 730, y: 110 }, { x: 580, y: 40 }, { x: 380, y: 20 } + ], + spikes: [ + { x: 150, y: 508, width: 40, height: 22 }, { x: 300, y: 508, width: 50, height: 22 }, + { x: 500, y: 508, width: 50, height: 22 }, { x: 700, y: 508, width: 50, height: 22 } + ], + patrolEnemies: [ + { x: 340, y: 30, patrolRange: 80, speed: 2 } + ], + powerUps: [ + { x: 580, y: 40, type: 'doubleJump' } + ], + checkpoints: [ + { x: 30, y: 490 }, { x: 350, y: 30 } + ], + flag: { x: 380, y: 20 }, + portal: null + }, + + // ═══════════════════════════════════════════════════════════════ + // LEVEL 9 - Chaos + // ═══════════════════════════════════════════════════════════════ + { + name: "ХАОС", + playerStart: { x: 60, y: 400 }, + platforms: [ + { x: 0, y: 510, width: 200, height: 40, color: '#2a2a4a' }, + { x: 0, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 870, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 100, y: 420, width: 50, height: 16, color: '#4a3a6a' }, + { x: 200, y: 380, width: 50, height: 16, color: '#4a3a6a' }, + { x: 300, y: 340, width: 50, height: 16, color: '#4a3a6a' }, + { x: 400, y: 300, width: 50, height: 16, color: '#4a3a6a' }, + { x: 500, y: 260, width: 50, height: 16, color: '#4a3a6a' }, + { x: 600, y: 220, width: 50, height: 16, color: '#4a3a6a' }, + { x: 700, y: 280, width: 50, height: 16, color: '#4a3a6a' }, + { x: 780, y: 350, width: 70, height: 16, color: '#4a3a6a' }, + { x: 750, y: 180, width: 80, height: 16, color: '#4a3a6a' }, + { x: 600, y: 120, width: 80, height: 16, color: '#4a3a6a' }, + { x: 400, y: 100, width: 80, height: 16, color: '#4a3a6a' }, + { x: 200, y: 150, width: 80, height: 16, color: '#4a3a6a' }, + { x: 50, y: 220, width: 80, height: 16, color: '#4a3a6a' } + ], + movingPlatforms: [ + { x: 250, y: 300, width: 40, height: 16, color: '#6a5aaa', startX: 230, endX: 290, speed: 3, direction: 1, moveType: 'horizontal' }, + { x: 450, y: 200, width: 40, height: 16, color: '#6a5aaa', startX: 430, endX: 510, speed: 2.5, direction: 1, moveType: 'horizontal' }, + { x: 650, y: 150, width: 40, height: 16, color: '#6a5aaa', startX: 630, endX: 710, speed: 2, direction: 1, moveType: 'horizontal' } + ], + coins: [ + { x: 115, y: 380 }, { x: 215, y: 340 }, { x: 315, y: 300 }, + { x: 415, y: 260 }, { x: 515, y: 220 }, { x: 615, y: 180 }, + { x: 730, y: 240 }, { x: 810, y: 310 }, { x: 780, y: 140 }, + { x: 630, y: 80 }, { x: 430, y: 60 }, { x: 230, y: 110 }, + { x: 80, y: 180 } + ], + spikes: [ + { x: 220, y: 488, width: 60, height: 22 }, { x: 400, y: 488, width: 60, height: 22 }, + { x: 580, y: 488, width: 60, height: 22 }, { x: 760, y: 488, width: 60, height: 22 } + ], + patrolEnemies: [ + { x: 200, y: 120, patrolRange: 60, speed: 3.5 }, + { x: 400, y: 70, patrolRange: 60, speed: 3 }, + { x: 600, y: 90, patrolRange: 60, speed: 3.5 } + ], + powerUps: [ + { x: 80, y: 180, type: 'invincibility' }, + { x: 630, y: 80, type: 'doubleJump' } + ], + checkpoints: [ + { x: 30, y: 470 }, { x: 200, y: 120 }, { x: 80, y: 180 } + ], + flag: { x: 80, y: 180 }, + portal: null + }, + + // ═══════════════════════════════════════════════════════════════ + // LEVEL 10 - Final Challenge + // ═══════════════════════════════════════════════════════════════ + { + name: "ФИНАЛ", + playerStart: { x: 60, y: 450 }, + platforms: [ + { x: 0, y: 510, width: 180, height: 40, color: '#2a2a4a' }, + { x: 0, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + { x: 870, y: 0, width: 30, height: 550, color: '#1a1a3a' }, + // Challenging platforms + { x: 200, y: 460, width: 50, height: 16, color: '#4a3a6a' }, + { x: 100, y: 390, width: 50, height: 16, color: '#4a3a6a' }, + { x: 220, y: 330, width: 50, height: 16, color: '#4a3a6a' }, + { x: 120, y: 270, width: 50, height: 16, color: '#4a3a6a' }, + { x: 250, y: 220, width: 50, height: 16, color: '#4a3a6a' }, + { x: 400, y: 280, width: 50, height: 16, color: '#4a3a6a' }, + { x: 550, y: 220, width: 50, height: 16, color: '#4a3a6a' }, + { x: 700, y: 280, width: 50, height: 16, color: '#4a3a6a' }, + { x: 800, y: 200, width: 60, height: 16, color: '#4a3a6a' }, + { x: 700, y: 120, width: 60, height: 16, color: '#4a3a6a' }, + { x: 500, y: 80, width: 80, height: 16, color: '#4a3a6a' }, + { x: 300, y: 60, width: 80, height: 16, color: '#4a3a6a' }, + { x: 100, y: 100, width: 80, height: 16, color: '#4a3a6a' } + ], + movingPlatforms: [ + { x: 170, y: 320, width: 40, height: 16, color: '#6a5aaa', startX: 150, endX: 210, speed: 2.5, direction: 1, moveType: 'horizontal' }, + { x: 350, y: 180, width: 40, height: 16, color: '#6a5aaa', startX: 330, endX: 410, speed: 3, direction: 1, moveType: 'horizontal' }, + { x: 600, y: 160, width: 40, height: 16, color: '#6a5aaa', startX: 580, endX: 660, speed: 2, direction: 1, moveType: 'horizontal' }, + { x: 400, y: 150, width: 40, height: 16, color: '#6a5aaa', startX: 380, endX: 460, speed: 2.2, direction: 1, moveType: 'horizontal' } + ], + coins: [ + { x: 215, y: 420 }, { x: 115, y: 350 }, { x: 235, y: 290 }, + { x: 135, y: 230 }, { x: 265, y: 180 }, { x: 415, y: 240 }, + { x: 565, y: 180 }, { x: 715, y: 240 }, { x: 825, y: 160 }, + { x: 725, y: 80 }, { x: 530, y: 40 }, { x: 330, y: 20 }, + { x: 130, y: 60 } + ], + spikes: [ + { x: 220, y: 488, width: 50, height: 22 }, { x: 400, y: 488, width: 50, height: 22 }, + { x: 580, y: 488, width: 50, height: 22 }, { x: 760, y: 488, width: 50, height: 22 } + ], + patrolEnemies: [ + { x: 300, y: 30, patrolRange: 70, speed: 4 }, + { x: 500, y: 50, patrolRange: 70, speed: 3.5 } + ], + powerUps: [ + { x: 130, y: 60, type: 'doubleJump' }, + { x: 530, y: 40, type: 'speedBoost' }, + { x: 825, y: 160, type: 'invincibility' } + ], + checkpoints: [ + { x: 30, y: 470 }, { x: 300, y: 30 }, { x: 130, y: 60 } + ], + flag: { x: 130, y: 60 }, + portal: null + } +]; + +// ═══════════════════════════════════════════════════════════════ +// LEVEL MANAGER +// ═══════════════════════════════════════════════════════════════ +class LevelManager { + constructor() { + this.currentLevel = 0; + this.maxLevel = LEVELS.length; + this.platforms = []; + this.movingPlatforms = []; + this.coins = []; + this.spikes = []; + this.patrolEnemies = []; + this.powerUps = []; + this.checkpoints = []; + this.flag = null; + this.portal = null; + } + + loadLevel(levelIndex) { + if (levelIndex >= this.maxLevel) { + return false; // Game complete + } + + this.currentLevel = levelIndex; + const level = LEVELS[levelIndex]; + + // Create platforms + this.platforms = level.platforms.map(p => new Platform(p.x, p.y, p.width, p.height, p.color, false, p.isMoving)); + + // Create moving platforms + this.movingPlatforms = level.movingPlatforms.map(mp => + new Platform(mp.x, mp.y, mp.width, mp.height, mp.color, true, { + startX: mp.startX, + endX: mp.endX, + startY: mp.startY || mp.y, + endY: mp.endY || mp.y, + speed: mp.speed, + moveType: mp.moveType + }) + ); + + // Create coins + this.coins = level.coins.map(c => new Coin(c.x, c.y)); + + // Create spikes + this.spikes = level.spikes.map(s => new Spike(s.x, s.y, s.width, s.height)); + + // Create patrol enemies + this.patrolEnemies = level.patrolEnemies.map(e => + new PatrolEnemy(e.x, e.y, e.patrolRange, e.speed) + ); + + // Create power-ups + this.powerUps = level.powerUps.map(p => new PowerUp(p.x, p.y, p.type)); + + // Create checkpoints + this.checkpoints = level.checkpoints.map(c => new Checkpoint(c.x, c.y)); + + // Create flag + this.flag = new Flag(level.flag.x, level.flag.y); + + // Create portal + if (level.portal) { + this.portal = new Portal(level.portal.x, level.portal.y); + } else { + this.portal = null; + } + + return true; + } + + update() { + // Update moving platforms + for (const mp of this.movingPlatforms) { + mp.update(); + } + + // Update coins + for (const coin of this.coins) { + coin.update(frameCount); + } + + // Update patrol enemies + for (const enemy of this.patrolEnemies) { + enemy.update(); + } + + // Update power-ups + for (const powerUp of this.powerUps) { + powerUp.update(frameCount); + } + } + + draw(ctx, frameCount) { + // Draw platforms + for (const platform of this.platforms) { + platform.draw(ctx, frameCount); + } + + // Draw moving platforms + for (const mp of this.movingPlatforms) { + mp.draw(ctx, frameCount); + } + + // Draw checkpoints + for (const checkpoint of this.checkpoints) { + checkpoint.draw(ctx, frameCount); + } + + // Draw coins + for (const coin of this.coins) { + coin.draw(ctx, frameCount); + } + + // Draw spikes + for (const spike of this.spikes) { + spike.draw(ctx); + } + + // Draw patrol enemies + for (const enemy of this.patrolEnemies) { + enemy.draw(ctx, frameCount); + } + + // Draw power-ups + for (const powerUp of this.powerUps) { + powerUp.draw(ctx, frameCount); + } + + // Draw portal + if (this.portal) { + this.portal.draw(ctx, frameCount); + } + + // Draw flag + if (this.flag) { + this.flag.draw(ctx, frameCount); + } + } + + getAllPlatforms() { + return [...this.platforms, ...this.movingPlatforms]; + } + + reset() { + this.currentLevel = 0; + } +} diff --git a/js/sound.js b/js/sound.js new file mode 100644 index 0000000..96d6186 --- /dev/null +++ b/js/sound.js @@ -0,0 +1,212 @@ +/* ═══════════════════════════════════════════════════════════════ + AUDIO SYSTEM - Web Audio API Sound Effects + ═══════════════════════════════════════════════════════════════ */ + +const AudioSystem = { + ctx: null, + masterVolume: 0.3, + sounds: {}, + muted: false, + + init() { + try { + this.ctx = new (window.AudioContext || window.webkitAudioContext)(); + this.createSounds(); + } catch (e) { + console.log('Audio not supported'); + } + }, + + createSounds() { + // Jump sound - rising pitch + this.sounds.jump = () => { + if (!this.ctx || this.muted) return; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.connect(gain); + gain.connect(this.ctx.destination); + osc.type = 'sine'; + osc.frequency.setValueAtTime(200, this.ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(600, this.ctx.currentTime + 0.1); + gain.gain.setValueAtTime(this.masterVolume * 0.5, this.ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.15); + osc.start(); + osc.stop(this.ctx.currentTime + 0.15); + }; + + // Double jump sound - higher pitch + this.sounds.doubleJump = () => { + if (!this.ctx || this.muted) return; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.connect(gain); + gain.connect(this.ctx.destination); + osc.type = 'sine'; + osc.frequency.setValueAtTime(400, this.ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(800, this.ctx.currentTime + 0.1); + gain.gain.setValueAtTime(this.masterVolume * 0.4, this.ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.12); + osc.start(); + osc.stop(this.ctx.currentTime + 0.12); + }; + + // Coin collect - bright chime + this.sounds.coin = () => { + if (!this.ctx || this.muted) return; + const frequencies = [880, 1100, 1320]; + frequencies.forEach((freq, i) => { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.connect(gain); + gain.connect(this.ctx.destination); + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.05); + gain.gain.setValueAtTime(0, this.ctx.currentTime); + gain.gain.linearRampToValueAtTime(this.masterVolume * 0.3, this.ctx.currentTime + i * 0.05); + gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.2 + i * 0.05); + osc.start(this.ctx.currentTime + i * 0.05); + osc.stop(this.ctx.currentTime + 0.25 + i * 0.05); + }); + }; + + // Damage/hurt - low thud + this.sounds.hurt = () => { + if (!this.ctx || this.muted) return; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.connect(gain); + gain.connect(this.ctx.destination); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(150, this.ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(50, this.ctx.currentTime + 0.2); + gain.gain.setValueAtTime(this.masterVolume * 0.4, this.ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.2); + osc.start(); + osc.stop(this.ctx.currentTime + 0.2); + }; + + // Power-up collected - magical chime + this.sounds.powerUp = () => { + if (!this.ctx || this.muted) return; + const frequencies = [523, 659, 784, 1047]; + frequencies.forEach((freq, i) => { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.connect(gain); + gain.connect(this.ctx.destination); + osc.type = 'triangle'; + osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.08); + gain.gain.setValueAtTime(0, this.ctx.currentTime); + gain.gain.linearRampToValueAtTime(this.masterVolume * 0.25, this.ctx.currentTime + i * 0.08); + gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.3 + i * 0.08); + osc.start(this.ctx.currentTime + i * 0.08); + osc.stop(this.ctx.currentTime + 0.4 + i * 0.08); + }); + }; + + // Checkpoint - celebratory sound + this.sounds.checkpoint = () => { + if (!this.ctx || this.muted) return; + const frequencies = [392, 523, 659, 784]; + frequencies.forEach((freq, i) => { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.connect(gain); + gain.connect(this.ctx.destination); + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.1); + gain.gain.setValueAtTime(0, this.ctx.currentTime); + gain.gain.linearRampToValueAtTime(this.masterVolume * 0.3, this.ctx.currentTime + i * 0.1); + gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.3 + i * 0.1); + osc.start(this.ctx.currentTime + i * 0.1); + osc.stop(this.ctx.currentTime + 0.5 + i * 0.1); + }); + }; + + // Level complete - victory fanfare + this.sounds.levelComplete = () => { + if (!this.ctx || this.muted) return; + const notes = [523, 587, 659, 698, 784, 880, 988, 1047]; + notes.forEach((freq, i) => { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.connect(gain); + gain.connect(this.ctx.destination); + osc.type = 'square'; + osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.12); + gain.gain.setValueAtTime(0, this.ctx.currentTime); + gain.gain.linearRampToValueAtTime(this.masterVolume * 0.15, this.ctx.currentTime + i * 0.12 + 0.02); + gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.4 + i * 0.12); + osc.start(this.ctx.currentTime + i * 0.12); + osc.stop(this.ctx.currentTime + 0.5 + i * 0.12); + }); + }; + + // Game over - sad descending + this.sounds.gameOver = () => { + if (!this.ctx || this.muted) return; + const frequencies = [440, 415, 392, 370, 349, 330, 311, 294]; + frequencies.forEach((freq, i) => { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.connect(gain); + gain.connect(this.ctx.destination); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(freq, this.ctx.currentTime + i * 0.15); + gain.gain.setValueAtTime(0, this.ctx.currentTime); + gain.gain.linearRampToValueAtTime(this.masterVolume * 0.2, this.ctx.currentTime + i * 0.15); + gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.3 + i * 0.15); + osc.start(this.ctx.currentTime + i * 0.15); + osc.stop(this.ctx.currentTime + 0.4 + i * 0.15); + }); + }; + + // Step/land sound + this.sounds.land = () => { + if (!this.ctx || this.muted) return; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.connect(gain); + gain.connect(this.ctx.destination); + osc.type = 'sine'; + osc.frequency.setValueAtTime(80, this.ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(40, this.ctx.currentTime + 0.05); + gain.gain.setValueAtTime(this.masterVolume * 0.2, this.ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.05); + osc.start(); + osc.stop(this.ctx.currentTime + 0.05); + }; + }, + + play(soundName) { + if (this.sounds[soundName]) { + // Resume audio context if suspended (browser autoplay policy) + if (this.ctx && this.ctx.state === 'suspended') { + this.ctx.resume(); + } + this.sounds[soundName](); + } + }, + + toggleMute() { + this.muted = !this.muted; + return this.muted; + }, + + setVolume(vol) { + this.masterVolume = Math.max(0, Math.min(1, vol)); + } +}; + +// Auto-initialize on first user interaction +document.addEventListener('click', () => { + if (!AudioSystem.ctx) { + AudioSystem.init(); + } +}, { once: true }); + +document.addEventListener('keydown', () => { + if (!AudioSystem.ctx) { + AudioSystem.init(); + } +}, { once: true });