MagicPot / js /particles.js
zhanglei
fix voice
ab5909a
Raw
History Blame Contribute Delete
16.8 kB
// 粒子系统类
class ParticleSystem {
constructor() {
this.particles = [];
this.maxParticles = 150; // 降低最大粒子数
this.gravity = 0.5;
this.bounce = 0.6;
this.friction = 0.98;
this.lastCleanupTime = 0;
}
update(deltaTime) {
const dt = deltaTime * 0.001; // 转换为秒
for (let i = this.particles.length - 1; i >= 0; i--) {
const particle = this.particles[i];
this.updateParticle(particle, dt);
// 移除生命周期结束的粒子
if (particle.life <= 0) {
this.particles.splice(i, 1);
}
}
}
updateParticle(particle, dt) {
// 更新位置
particle.x += particle.vx * dt;
particle.y += particle.vy * dt;
// 应用重力
particle.vy += this.gravity * dt * 60; // 60fps基准
// 应用摩擦力
particle.vx *= this.friction;
// 边界碰撞检测
this.handleBoundaryCollision(particle);
// 更新生命周期
particle.life -= dt / particle.maxLife;
particle.opacity = Math.max(0, particle.life);
// 更新大小(某些粒子会变化)
if (particle.sizeChange) {
particle.size += particle.sizeChange * dt;
particle.size = Math.max(1, particle.size);
}
// 更新旋转
if (particle.rotation !== undefined) {
particle.rotation += particle.rotationSpeed * dt;
}
}
handleBoundaryCollision(particle) {
const canvas = document.getElementById('gameCanvas');
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
// 底部碰撞
if (particle.y + particle.size > canvasHeight) {
particle.y = canvasHeight - particle.size;
particle.vy *= -this.bounce;
// 减少水平速度
particle.vx *= 0.8;
// 如果速度太小,停止弹跳
if (Math.abs(particle.vy) < 10) {
particle.vy = 0;
particle.isGrounded = true;
}
}
// 左右边界碰撞
if (particle.x - particle.size < 0) {
particle.x = particle.size;
particle.vx *= -this.bounce;
} else if (particle.x + particle.size > canvasWidth) {
particle.x = canvasWidth - particle.size;
particle.vx *= -this.bounce;
}
// 顶部边界(防止粒子飞出屏幕)
if (particle.y - particle.size < 0) {
particle.y = particle.size;
particle.vy *= -this.bounce;
}
}
createFoodParticle(x, y, foodType) {
if (this.particles.length >= this.maxParticles) {
// 移除最老的粒子
this.particles.shift();
}
const particle = {
x: x + (Math.random() - 0.5) * 20,
y: y + (Math.random() - 0.5) * 10,
vx: (Math.random() - 0.5) * 200,
vy: -Math.random() * 150 - 50,
size: Math.random() * 15 + 8,
color: this.getFoodColor(foodType),
emoji: this.getFoodEmoji(foodType),
life: 1,
maxLife: Math.random() * 3 + 2,
opacity: 1,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 5,
type: 'food',
foodType: foodType,
isGrounded: false,
bounceCount: 0,
maxBounces: Math.floor(Math.random() * 3) + 2
};
this.particles.push(particle);
return particle;
}
createExplosionParticles(x, y, count = 10, color = '#FFD700') {
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const speed = Math.random() * 100 + 50;
const particle = {
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: Math.random() * 8 + 4,
color: color,
life: 1,
maxLife: Math.random() * 1 + 0.5,
opacity: 1,
type: 'explosion',
sizeChange: -5 // 粒子逐渐变小
};
this.particles.push(particle);
}
}
createSparkleParticles(x, y, count = 5) {
for (let i = 0; i < count; i++) {
const particle = {
x: x + (Math.random() - 0.5) * 50,
y: y + (Math.random() - 0.5) * 50,
vx: (Math.random() - 0.5) * 30,
vy: -Math.random() * 30 - 10,
size: Math.random() * 4 + 2,
color: Math.random() < 0.5 ? '#FFD700' : '#FF69B4',
life: 1,
maxLife: Math.random() * 1.5 + 0.5,
opacity: 1,
type: 'sparkle',
twinkle: true
};
this.particles.push(particle);
}
}
render(ctx) {
this.particles.forEach(particle => {
this.renderParticle(ctx, particle);
});
}
renderParticle(ctx, particle) {
ctx.save();
ctx.globalAlpha = particle.opacity;
// 移动到粒子位置
ctx.translate(particle.x, particle.y);
// 应用旋转
if (particle.rotation !== undefined) {
ctx.rotate(particle.rotation);
}
switch (particle.type) {
case 'food':
this.renderFoodParticle(ctx, particle);
break;
case 'explosion':
this.renderExplosionParticle(ctx, particle);
break;
case 'sparkle':
this.renderSparkleParticle(ctx, particle);
break;
default:
this.renderDefaultParticle(ctx, particle);
break;
}
ctx.restore();
}
renderFoodParticle(ctx, particle) {
// 绘制食物表情符号
if (particle.emoji) {
ctx.font = `${particle.size}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(particle.emoji, 0, 0);
} else {
// 备用:绘制彩色圆形
ctx.fillStyle = particle.color;
ctx.beginPath();
ctx.arc(0, 0, particle.size / 2, 0, Math.PI * 2);
ctx.fill();
// 添加高光
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.beginPath();
ctx.arc(-particle.size / 6, -particle.size / 6, particle.size / 4, 0, Math.PI * 2);
ctx.fill();
}
}
renderExplosionParticle(ctx, particle) {
ctx.fillStyle = particle.color;
ctx.shadowBlur = 10;
ctx.shadowColor = particle.color;
ctx.beginPath();
ctx.arc(0, 0, particle.size, 0, Math.PI * 2);
ctx.fill();
}
renderSparkleParticle(ctx, particle) {
ctx.fillStyle = particle.color;
ctx.shadowBlur = 8;
ctx.shadowColor = particle.color;
// 绘制星形
const spikes = 4;
const outerRadius = particle.size;
const innerRadius = particle.size * 0.4;
ctx.beginPath();
for (let i = 0; i < spikes * 2; i++) {
const angle = (i / (spikes * 2)) * Math.PI * 2;
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.fill();
// 闪烁效果
if (particle.twinkle) {
const twinkleAlpha = Math.sin(Date.now() * 0.01) * 0.5 + 0.5;
ctx.globalAlpha *= twinkleAlpha;
}
}
renderDefaultParticle(ctx, particle) {
ctx.fillStyle = particle.color || '#FFD700';
ctx.beginPath();
ctx.arc(0, 0, particle.size, 0, Math.PI * 2);
ctx.fill();
}
getFoodColor(foodType) {
const colorMap = {
'apple': '#FF6B6B',
'banana': '#FFE66D',
'orange': '#FF8E53',
'strawberry': '#FF6B9D',
'watermelon': '#C44569',
'grape': '#9B59B6',
'pizza': '#F39C12',
'burger': '#D35400',
'cake': '#F8C471',
'cookie': '#D2691E',
'bread': '#DEB887',
'cheese': '#F1C40F',
'fish': '#3498DB',
'chicken': '#E67E22',
'rice': '#ECF0F1',
'noodles': '#F4D03F'
};
return colorMap[foodType] || '#FFD700';
}
getFoodEmoji(foodType) {
const emojiMap = {
'apple': '🍎',
'banana': '🍌',
'orange': '🍊',
'strawberry': '🍓',
'watermelon': '🍉',
'grape': '🍇',
'pizza': '🍕',
'burger': '🍔',
'cake': '🍰',
'cookie': '🍪',
'bread': '🍞',
'cheese': '🧀',
'fish': '🐟',
'chicken': '🍗',
'rice': '🍚',
'noodles': '🍜',
'carrot': '🥕',
'tomato': '🍅',
'corn': '🌽',
'broccoli': '🥦',
'potato': '🥔',
'onion': '🧅'
};
return emojiMap[foodType] || '🍽️';
}
// 清理所有粒子
clear() {
this.particles = [];
}
// 获取指定类型的粒子数量
getParticleCount(type = null) {
if (type) {
return this.particles.filter(p => p.type === type).length;
}
return this.particles.length;
}
// 移除指定类型的粒子
removeParticlesByType(type) {
this.particles = this.particles.filter(p => p.type !== type);
}
// 获取地面上的食物粒子数量
getGroundedFoodCount() {
return this.particles.filter(p => p.type === 'food' && p.isGrounded).length;
}
// 在指定区域内创建粒子爆炸效果
createAreaExplosion(x, y, radius, particleCount = 20) {
for (let i = 0; i < particleCount; i++) {
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * radius;
const particleX = x + Math.cos(angle) * distance;
const particleY = y + Math.sin(angle) * distance;
this.createExplosionParticles(particleX, particleY, 3);
}
}
// 清理无效粒子
cleanupInvalidParticles() {
const beforeCount = this.particles.length;
// 移除生命周期结束的粒子
this.particles = this.particles.filter(particle => particle.life > 0);
// 移除超出边界太远的粒子
const canvas = document.getElementById('gameCanvas');
if (canvas) {
const margin = 100; // 边界外100像素
this.particles = this.particles.filter(particle => {
return particle.x > -margin &&
particle.x < canvas.width + margin &&
particle.y > -margin &&
particle.y < canvas.height + margin;
});
}
const afterCount = this.particles.length;
return beforeCount - afterCount; // 返回清理的粒子数量
}
// 优化粒子更新
updateParticle(particle, dt) {
// 更新生命周期
particle.life -= dt / particle.maxLife;
if (particle.life <= 0) {
return; // 粒子死亡,将在下次清理时移除
}
// 更新位置
particle.x += particle.vx * dt;
particle.y += particle.vy * dt;
// 应用重力
particle.vy += this.gravity * dt * 60; // 60fps基准
// 应用摩擦力
particle.vx *= this.friction;
// 边界碰撞检测
this.handleBoundaryCollision(particle);
// 更新透明度
particle.opacity = Math.max(0, particle.life);
// 更新大小(某些粒子会变化)
if (particle.sizeChange) {
particle.size += particle.sizeChange * dt;
particle.size = Math.max(1, particle.size);
}
// 更新旋转
if (particle.rotation !== undefined) {
particle.rotation += particle.rotationSpeed * dt;
}
// 更新生命时间(用于清理)
particle.lifeTime = (particle.lifeTime || 0) + dt;
}
// 获取特定状态的粒子统计
getParticleStats() {
const stats = {
total: this.particles.length,
food: 0,
explosion: 0,
sparkle: 0,
grounded: 0,
moving: 0
};
this.particles.forEach(particle => {
if (particle.type === 'food') {
stats.food++;
if (particle.isGrounded) stats.grounded++;
else stats.moving++;
} else if (particle.type === 'explosion') {
stats.explosion++;
} else if (particle.type === 'sparkle') {
stats.sparkle++;
}
});
return stats;
}
// 强制清理最老的粒子
forceCleanupOldest(count) {
if (count <= 0) return 0;
// 按生命时间排序,移除最老的
const sortedParticles = this.particles
.map((particle, index) => ({ particle, index, lifeTime: particle.lifeTime || 0 }))
.sort((a, b) => b.lifeTime - a.lifeTime);
const toRemove = Math.min(count, sortedParticles.length);
const indicesToRemove = sortedParticles
.slice(0, toRemove)
.map(item => item.index)
.sort((a, b) => b - a); // 从后往前删除
indicesToRemove.forEach(index => {
this.particles.splice(index, 1);
});
return toRemove;
}
}
// 食物粒子特殊效果类
class FoodParticleEffects {
static createBurstEffect(particleSystem, x, y, foodType, count = 15) {
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const speed = Math.random() * 80 + 40;
const particle = {
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 30,
size: Math.random() * 12 + 6,
color: particleSystem.getFoodColor(foodType),
emoji: particleSystem.getFoodEmoji(foodType),
life: 1,
maxLife: Math.random() * 2 + 1,
opacity: 1,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 8,
type: 'food',
foodType: foodType,
isGrounded: false
};
particleSystem.particles.push(particle);
}
}
static createFountainEffect(particleSystem, x, y, foodType) {
const particleCount = 8;
for (let i = 0; i < particleCount; i++) {
const angle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI / 3;
const speed = Math.random() * 120 + 80;
const particle = {
x: x + (Math.random() - 0.5) * 30,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: Math.random() * 18 + 10,
color: particleSystem.getFoodColor(foodType),
emoji: particleSystem.getFoodEmoji(foodType),
life: 1,
maxLife: Math.random() * 4 + 2,
opacity: 1,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 6,
type: 'food',
foodType: foodType,
isGrounded: false
};
particleSystem.particles.push(particle);
}
}
}