import { System } from '../core/System.ts'; import { Events } from '../core/EventBus.ts'; import { Palette } from '../core/Palette.ts'; import { SystemName, ComponentType } from '../core/Constants.ts'; import type { Entity } from '../core/Entity.ts'; import type { Health } from '../components/Health.ts'; import type { Position } from '../components/Position.ts'; import type { Velocity } from '../components/Velocity.ts'; import type { VFXSystem } from './VFXSystem.ts'; /** * System responsible for managing projectile movement, range limits, lifetimes, and collisions. */ export class ProjectileSystem extends System { constructor() { super(SystemName.PROJECTILE); this.requiredComponents = [ComponentType.POSITION, ComponentType.VELOCITY]; this.priority = 18; } /** * Process logic for all projectiles, checking for range, lifetime, and target collisions. * @param deltaTime - Time elapsed since last frame in seconds * @param entities - Entities matching system requirements */ process(deltaTime: number, entities: Entity[]): void { entities.forEach((entity) => { const health = entity.getComponent(ComponentType.HEALTH); if (!health || !health.isProjectile) return; const position = entity.getComponent(ComponentType.POSITION); if (!position) return; if ( entity.startX !== undefined && entity.startY !== undefined && entity.maxRange !== undefined ) { const dx = position.x - entity.startX; const dy = position.y - entity.startY; const distanceTraveled = Math.sqrt(dx * dx + dy * dy); if (distanceTraveled >= entity.maxRange) { this.engine.removeEntity(entity); return; } } if (entity.lifetime !== undefined) { entity.lifetime -= deltaTime; if (entity.lifetime <= 0) { this.engine.removeEntity(entity); return; } } const allEntities = this.engine.getEntities(); allEntities.forEach((target) => { if (target.id === entity.owner) return; if (target.id === entity.id) return; if (!target.hasComponent(ComponentType.HEALTH)) return; const targetHealth = target.getComponent(ComponentType.HEALTH); if (targetHealth && targetHealth.isProjectile) return; const targetPos = target.getComponent(ComponentType.POSITION); if (!targetPos) return; const dx = targetPos.x - position.x; const dy = targetPos.y - position.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 8) { const targetHealthComp = target.getComponent(ComponentType.HEALTH); const damage = entity.damage || 10; if (targetHealthComp) { targetHealthComp.takeDamage(damage); const vfxSystem = this.engine.systems.find((s) => s.name === SystemName.VFX) as | VFXSystem | undefined; const velocity = entity.getComponent(ComponentType.VELOCITY); if (vfxSystem) { const angle = velocity ? Math.atan2(velocity.vy, velocity.vx) : null; vfxSystem.createImpact(position.x, position.y, Palette.CYAN, angle); } if (targetHealthComp.isDead()) { this.engine.emit(Events.ENTITY_DIED, { entity: target }); } } this.engine.removeEntity(entity); } }); const canvas = this.engine.canvas; if ( position.x < 0 || position.x > canvas.width || position.y < 0 || position.y > canvas.height ) { this.engine.removeEntity(entity); } }); } }