Загрузить файлы в «js»
This commit is contained in:
661
js/entities.js
Normal file
661
js/entities.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
990
js/game.js
Normal file
990
js/game.js
Normal file
@@ -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 = `
|
||||
Монеты: <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;
|
||||
}
|
||||
689
js/levels.js
Normal file
689
js/levels.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
212
js/sound.js
Normal file
212
js/sound.js
Normal file
@@ -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 });
|
||||
Reference in New Issue
Block a user