Files
NeonPlaformer/js/entities.js

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;
}
}