import { System } from '../ecs/System.js'; import { Transform } from '../components/Transform.js'; import { ParticleEmitter } from '../components/ParticleEmitter.js'; import { GameConfig } from '../game/GameConfig.js'; /** * ParticleSystem - manages particle effects for visual feedback */ export class ParticleSystem extends System { constructor(scene) { super(); /** @type {import('three').Scene} */ this.scene = scene; /** @type {Array<{mesh: import('three').Mesh, velocity: import('three').Vector3, lifetime: number, maxLifetime: number}>} */ this.particles = []; } /** * Create particles at a position * @param {import('three').Vector3} position - Position to emit from * @param {number} count - Number of particles * @param {number} color - Color (hex) * @param {number} [speed=5] - Particle speed */ emit(position, count, color, speed = 5) { for (let i = 0; i < count; i++) { const geometry = new window.THREE.SphereGeometry(0.1, 8, 8); const material = new window.THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 1.0 }); const mesh = new window.THREE.Mesh(geometry, material); mesh.position.copy(position); this.scene.add(mesh); // Random velocity direction const velocity = new window.THREE.Vector3( (Math.random() - 0.5) * speed, Math.random() * speed * 0.5 + speed * 0.5, (Math.random() - 0.5) * speed ); this.particles.push({ mesh: mesh, velocity: velocity, lifetime: GameConfig.PARTICLE_LIFETIME, maxLifetime: GameConfig.PARTICLE_LIFETIME }); } } /** * Update particles * @param {number} deltaTime - Time since last frame in seconds */ update(deltaTime) { // Update existing particles for (let i = this.particles.length - 1; i >= 0; i--) { const particle = this.particles[i]; // Update position particle.mesh.position.add( particle.velocity.clone().multiplyScalar(deltaTime) ); // Apply gravity particle.velocity.y -= 9.8 * deltaTime; // Update lifetime particle.lifetime -= deltaTime; // Fade out const alpha = particle.lifetime / particle.maxLifetime; particle.mesh.material.opacity = alpha; // Remove dead particles if (particle.lifetime <= 0) { this.scene.remove(particle.mesh); particle.mesh.geometry.dispose(); particle.mesh.material.dispose(); this.particles.splice(i, 1); } } // Process particle emitters from entities const entities = this.getEntities(Transform, ParticleEmitter); for (const entityId of entities) { const transform = this.getComponent(entityId, Transform); const emitter = this.getComponent(entityId, ParticleEmitter); if (emitter && emitter.active) { this.emit( transform.position, emitter.count, emitter.color, emitter.speed ); // Deactivate after emitting once emitter.active = false; } } } }