import { System } from "../System"; import { type ECSWorld } from "../World"; import { type ComponentType } from "../components"; import { type EntityId } from "../../../core/types"; import { applyStatusEffect } from "./StatusEffectSystem"; /** * Processes trigger entities when other entities step on them. * Handles traps (damage), status effects, and one-shot triggers. * * @example * registry.register(new TriggerSystem()); * * // Create a spike trap * world.addComponent(trapId, "trigger", { * onEnter: true, * damage: 15 * }); */ export class TriggerSystem extends System { readonly name = "Trigger"; readonly requiredComponents: readonly ComponentType[] = ["trigger", "position"]; readonly priority = 5; // Run before status effects /** * Track which entities are currently on which triggers. * Used to detect enter/exit events. */ private entityPositions: Map = new Map(); update(entities: EntityId[], world: ECSWorld, _dt?: number): void { // Get all entities with positions (potential activators) const allWithPosition = world.getEntitiesWith("position"); for (const triggerId of entities) { const trigger = world.getComponent(triggerId, "trigger"); const triggerPos = world.getComponent(triggerId, "position"); if (!trigger || !triggerPos) continue; if (trigger.triggered && trigger.oneShot) continue; // Already triggered one-shot // Check for entities at this trigger's position for (const entityId of allWithPosition) { if (entityId === triggerId) continue; // Skip self const entityPos = world.getComponent(entityId, "position"); if (!entityPos) continue; const isOnTrigger = entityPos.x === triggerPos.x && entityPos.y === triggerPos.y; const wasOnTrigger = this.wasEntityOnTrigger(entityId, triggerPos); // Handle enter if (trigger.onEnter && isOnTrigger && !wasOnTrigger) { this.activateTrigger(triggerId, entityId, trigger, world); } // Handle exit if (trigger.onExit && !isOnTrigger && wasOnTrigger) { this.eventBus?.emit({ type: "trigger_activated", triggerId, activatorId: entityId }); } } } // Update entity positions for next frame this.updateEntityPositions(allWithPosition, world); } /** * Activate a trigger on an entity. */ private activateTrigger( triggerId: EntityId, activatorId: EntityId, trigger: { damage?: number; effect?: string; effectDuration?: number; oneShot?: boolean; triggered?: boolean; }, world: ECSWorld ): void { // Emit trigger event this.eventBus?.emit({ type: "trigger_activated", triggerId, activatorId }); // Apply damage if trap if (trigger.damage && trigger.damage > 0) { const stats = world.getComponent(activatorId, "stats"); if (stats) { stats.hp = Math.max(0, stats.hp - trigger.damage); this.eventBus?.emit({ type: "damage", entityId: activatorId, amount: trigger.damage, source: triggerId }); } } // Apply status effect if specified if (trigger.effect) { applyStatusEffect(world, activatorId, { type: trigger.effect, duration: trigger.effectDuration ?? 3, source: triggerId }); this.eventBus?.emit({ type: "status_applied", entityId: activatorId, status: trigger.effect, duration: trigger.effectDuration ?? 3 }); } // Mark as triggered for one-shot triggers and update sprite if (trigger.oneShot) { trigger.triggered = true; // Change sprite to triggered appearance (dungeon sprite 23) const sprite = world.getComponent(triggerId, "sprite"); if (sprite) { sprite.index = 23; // Triggered/spent trap appearance } } } /** * Check if an entity was previously on a trigger position. */ private wasEntityOnTrigger(entityId: EntityId, triggerPos: { x: number; y: number }): boolean { const lastPos = this.entityPositions.get(entityId); if (!lastPos) return false; return lastPos.x === triggerPos.x && lastPos.y === triggerPos.y; } /** * Update cached entity positions for next frame comparison. */ private updateEntityPositions(entities: EntityId[], world: ECSWorld): void { this.entityPositions.clear(); for (const entityId of entities) { const pos = world.getComponent(entityId, "position"); if (pos) { this.entityPositions.set(entityId, { x: pos.x, y: pos.y }); } } } /** * Called when the system is registered - initialize position tracking. */ onRegister(world: ECSWorld): void { const allWithPosition = world.getEntitiesWith("position"); this.updateEntityPositions(allWithPosition, world); } }