import { System } from '../core/System.ts'; import { Palette } from '../core/Palette.ts'; import { SystemName, ComponentType, VFXType } from '../core/Constants.ts'; import type { Entity } from '../core/Entity.ts'; import type { Position } from '../components/Position.ts'; import type { PlayerControllerSystem } from './PlayerControllerSystem.ts'; interface Particle { x: number; y: number; vx: number; vy: number; lifetime: number; size: number; color: string; type: VFXType; } /** * System responsible for creating and updating visual effects particles. */ export class VFXSystem extends System { particles: Particle[]; constructor() { super(SystemName.VFX); this.requiredComponents = []; this.priority = 40; this.particles = []; } /** * Update all active particles, applying movement, attraction, and drag. * @param deltaTime - Time elapsed since last frame in seconds * @param _entities - Filtered entities */ process(deltaTime: number, _entities: Entity[]): void { const playerController = this.engine.systems.find( (s) => s.name === SystemName.PLAYER_CONTROLLER ) as PlayerControllerSystem | undefined; const player = playerController ? playerController.getPlayerEntity() : null; const playerPos = player ? player.getComponent(ComponentType.POSITION) : null; for (let i = this.particles.length - 1; i >= 0; i--) { const p = this.particles[i]; p.lifetime -= deltaTime; if (p.lifetime <= 0) { this.particles.splice(i, 1); continue; } if (p.type === VFXType.ABSORPTION && playerPos) { const dx = playerPos.x - p.x; const dy = playerPos.y - p.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist > 5) { p.vx += (dx / dist) * 800 * deltaTime; p.vy += (dy / dist) * 800 * deltaTime; p.vx *= 0.95; p.vy *= 0.95; } else { this.particles.splice(i, 1); continue; } } else { p.vx *= 0.9; p.vy *= 0.9; } p.x += p.vx * deltaTime; p.y += p.vy * deltaTime; } } /** * Create an impact particle explosion at a specific location. * @param x - Horizontal coordinate * @param y - Vertical coordinate * @param color - The color of particles * @param angle - The direction of the impact splash */ createImpact(x: number, y: number, color = Palette.WHITE, angle: number | null = null): void { const count = 8; for (let i = 0; i < count; i++) { let vx: number, vy: number; if (angle !== null) { const spread = (Math.random() - 0.5) * 2; const speed = 50 + Math.random() * 150; vx = Math.cos(angle + spread) * speed; vy = Math.sin(angle + spread) * speed; } else { vx = (Math.random() - 0.5) * 150; vy = (Math.random() - 0.5) * 150; } this.particles.push({ x, y, vx, vy, lifetime: 0.2 + Math.random() * 0.3, size: 1 + Math.random() * 2, color: color, type: VFXType.IMPACT, }); } } /** * Create absorption particles that fly towards the player. * @param x - Starting horizontal coordinate * @param y - Starting vertical coordinate * @param color - The color of particles */ createAbsorption(x: number, y: number, color = Palette.CYAN): void { for (let i = 0; i < 12; i++) { this.particles.push({ x, y, vx: (Math.random() - 0.5) * 100, vy: (Math.random() - 0.5) * 100, lifetime: 1.5, size: 2, color: color, type: VFXType.ABSORPTION, }); } } /** * Get all currently active particles. * @returns Array of particle data objects */ getParticles(): Particle[] { return this.particles; } }