Files
rogue/src/engine/ecs/systems/TriggerSystem.ts
Peter Stockings 9552364a60 feat: Add traps
2026-01-25 16:37:46 +11:00

167 lines
5.1 KiB
TypeScript

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<EntityId, { x: number; y: number }> = 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);
}
}