156 lines
4.6 KiB
TypeScript
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;
|
|
}
|