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