662 lines
24 KiB
JavaScript
662 lines
24 KiB
JavaScript
/* ═══════════════════════════════════════════════════════════════
|
|
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;
|
|
}
|
|
}
|