Files
rogue/src/engine/ecs/EventBus.ts

156 lines
4.6 KiB
TypeScript

import { type EntityId } from "../../core/types";
import { type ComponentType } from "./components";
/**
* Game events for cross-system communication.
* Systems can emit and subscribe to these events to react to gameplay changes.
*/
export type GameEvent =
// Combat events
| { type: "damage"; entityId: EntityId; amount: number; source?: EntityId }
| { type: "heal"; entityId: EntityId; amount: number; source?: EntityId }
| { type: "death"; entityId: EntityId; killedBy?: EntityId }
// Component lifecycle events
| { type: "component_added"; entityId: EntityId; componentType: ComponentType }
| { type: "component_removed"; entityId: EntityId; componentType: ComponentType }
| { type: "entity_created"; entityId: EntityId }
| { type: "entity_destroyed"; entityId: EntityId }
// Movement & trigger events
| { type: "stepped_on"; entityId: EntityId; x: number; y: number }
| { type: "trigger_activated"; triggerId: EntityId; activatorId: EntityId }
// Status effect events
| { type: "status_applied"; entityId: EntityId; status: string; duration: number }
| { type: "status_expired"; entityId: EntityId; status: string }
| { type: "status_tick"; entityId: EntityId; status: string; remaining: number }
// World events
| { type: "tile_changed"; x: number; y: number };
export type GameEventType = GameEvent["type"];
type EventHandler<T extends GameEvent = GameEvent> = (event: T) => void;
/**
* Lightweight event bus for cross-system communication.
* Enables reactive gameplay like status effects, triggers, and combat feedback.
*/
export class EventBus {
private listeners: Map<string, Set<EventHandler>> = new Map();
private onceListeners: Map<string, Set<EventHandler>> = new Map();
private eventQueue: GameEvent[] = [];
/**
* Subscribe to events of a specific type.
* @returns Unsubscribe function
*/
on<T extends GameEventType>(
eventType: T,
handler: EventHandler<Extract<GameEvent, { type: T }>>
): () => void {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, new Set());
}
this.listeners.get(eventType)!.add(handler as EventHandler);
// Return unsubscribe function
return () => {
this.listeners.get(eventType)?.delete(handler as EventHandler);
};
}
/**
* Subscribe to a single occurrence of an event type.
* The handler is automatically removed after being called once.
*/
once<T extends GameEventType>(
eventType: T,
handler: EventHandler<Extract<GameEvent, { type: T }>>
): void {
if (!this.onceListeners.has(eventType)) {
this.onceListeners.set(eventType, new Set());
}
this.onceListeners.get(eventType)!.add(handler as EventHandler);
}
/**
* Emit an event to all registered listeners.
*/
emit(event: GameEvent): void {
const eventType = event.type;
// Call regular listeners
const handlers = this.listeners.get(eventType);
if (handlers) {
for (const handler of handlers) {
handler(event);
}
}
// Call once listeners and remove them
const onceHandlers = this.onceListeners.get(eventType);
if (onceHandlers) {
for (const handler of onceHandlers) {
handler(event);
}
this.onceListeners.delete(eventType);
}
// Add to queue for drain()
this.eventQueue.push(event);
}
/**
* Drain all queued events and return them.
* Clears the internal queue.
*/
drain(): GameEvent[] {
const events = [...this.eventQueue];
this.eventQueue = [];
return events;
}
/**
* Remove all listeners for a specific event type.
*/
off(eventType: GameEventType): void {
this.listeners.delete(eventType);
this.onceListeners.delete(eventType);
}
/**
* Remove all listeners for all event types.
*/
clear(): void {
this.listeners.clear();
this.onceListeners.clear();
}
/**
* Check if there are any listeners for a specific event type.
*/
hasListeners(eventType: GameEventType): boolean {
return (
(this.listeners.get(eventType)?.size ?? 0) > 0 ||
(this.onceListeners.get(eventType)?.size ?? 0) > 0
);
}
}
// Singleton instance for global event bus (optional - can also create instances per world)
let globalEventBus: EventBus | null = null;
export function getEventBus(): EventBus {
if (!globalEventBus) {
globalEventBus = new EventBus();
}
return globalEventBus;
}
export function resetEventBus(): void {
globalEventBus?.clear();
globalEventBus = null;
}