Files
NeonPlaformer/js/game.js

991 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ═══════════════════════════════════════════════════════════════
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 = `
Монеты: <span style="color: #ffd700">${currentLevelScore}</span><br>
Бонус за время: <span style="color: #00ffff">${Math.max(0, 100 - gameTime) * 5}</span><br>
Всего очков: <span style="color: #00ff88">${totalScore}</span>
`;
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 = `
Уровень: <span style="color: #00ffff">${levelManager.currentLevel + 1}</span><br>
Счет: <span style="color: #ffd700">${totalScore}</span>
`;
messageDiv.className = 'lose';
messageDiv.style.display = 'block';
}
function gameComplete() {
gameRunning = false;
messageTitle.textContent = '🏆 ПОБЕДА! 🏆';
messageTitle.style.color = '#ffd700';
messageStats.innerHTML = `
Поздравляем! Вы прошли все уровни!<br>
Итоговый счет: <span style="color: #ffd700; font-size: 24px">${totalScore}</span>
`;
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;
}