feat: Add traps
This commit is contained in:
166
src/engine/ecs/systems/TriggerSystem.ts
Normal file
166
src/engine/ecs/systems/TriggerSystem.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user