import { ACTION_KEYS, VARIABLE_KEYS } from '../utils/decorators'; import type { RPGActions, RPGVariables } from '../types'; interface EntityEvent { target: Entity; data?: T; } interface WorldEvent { target: World; data?: T; } type Class = abstract new (...args: any[]) => T; type EntityEventHandler = (event: EntityEvent) => void; type WorldEventHandler = (event: WorldEvent) => void; export interface EvalContext { self: Entity | World; world: World; } /** Symbol used by Serialization to read component state. */ export const COMPONENT_STATE = Symbol('rpg.component.state'); /** Symbol used by Serialization to access World's entity counter. */ export const WORLD_ENTITY_COUNTER = Symbol('rpg.world.entityCounter'); export abstract class Component> { entity!: Entity; key!: string; protected state: TState; constructor(state: TState) { this.state = state; } [COMPONENT_STATE](): TState { return this.state; } protected emit(event: string, data?: unknown): void { this.entity.emit(`${this.key}.${event}`, data); } onAdd(): void { } onRemove(): void { } getVariables(): RPGVariables { const meta = (this.constructor as Function)[Symbol.metadata]; const keys = meta?.[VARIABLE_KEYS] as Map | undefined; if (!keys) return {}; const vars: RPGVariables = {}; for (const [methodKey, exportName] of keys) { const v = (this as Record)[String(methodKey)]; if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean') { vars[exportName] = v; } } return vars; } getActions(): RPGActions { const meta = (this.constructor as Function)[Symbol.metadata]; const keys = meta?.[ACTION_KEYS] as Set | undefined; if (!keys) return {}; const actions: RPGActions = {}; for (const key of keys) { const fn = (this as Record)[String(key)]; if (typeof fn === 'function') { actions[String(key)] = fn.bind(this); } } return actions; } get context(): EvalContext { return this.entity.context; } } export abstract class System { onAdd(_world: World): void { } onRemove(_world: World): void { } update(_world: World, _dt: number): void { }; } export class Entity { readonly #components = new Map(); constructor( readonly id: string, readonly world: World, ) { } get context(): EvalContext { return { self: this, world: this.world }; } add>(key: string, component: T): T { const existing = this.#components.get(key); if (existing) existing.onRemove(); component.entity = this; component.key = key; this.#components.set(key, component); component.onAdd(); return component; } get>(key: string): T | undefined; get>(ctor: Class): T | undefined; get>(ctor: Class, key: string): T | undefined; get>(ctorOrKey: Class | string, key?: string): T | undefined { if (typeof ctorOrKey === 'string') { return this.#components.get(ctorOrKey) as T | undefined; } if (key !== undefined) { const c = this.#components.get(key); return c instanceof ctorOrKey ? c as T : undefined; } for (const c of this.#components.values()) { if (c instanceof ctorOrKey) return c as T; } return undefined; } has(key: string): boolean; has>(ctor: Class): boolean; has>(ctor: Class, key: string): boolean; has>(ctorOrKey: Class | string, key?: string): boolean { if (typeof ctorOrKey === 'string') return this.#components.has(ctorOrKey); if (key !== undefined) return this.#components.get(key) instanceof ctorOrKey; for (const c of this.#components.values()) { if (c instanceof ctorOrKey) return true; } return false; } remove(key: string): void; remove>(ctor: Class): void; remove>(ctor: Class, key: string): void; remove>(ctorOrKey: Class | string, key?: string): void { if (typeof ctorOrKey === 'string') { this.#removeByKey(ctorOrKey); return; } if (key !== undefined) { if (this.#components.get(key) instanceof ctorOrKey) this.#removeByKey(key); return; } for (const [k, c] of this.#components) { if (c instanceof ctorOrKey) { this.#removeByKey(k); return; } } } #removeByKey(key: string): void { const c = this.#components.get(key); if (c) { c.onRemove(); this.#components.delete(key); } } emit(event: string, data?: unknown): void { this.world.emit(this.id, event, data); } emitGlobal(event: string, data?: unknown): void { this.world.emitGlobal(event, data); } on(event: string, handler: EntityEventHandler): () => void { return this.world.on(this.id, event, handler); } off(event: string, handler: EntityEventHandler): void { this.world.off(this.id, event, handler); } once(event: string, handler: EntityEventHandler): () => void { return this.world.once(this.id, event, handler); } destroy(): void { this.world.destroyEntity(this); } /** @internal */ [Symbol.iterator](): IterableIterator<[string, Component]> { return this.#components.entries(); } /** @internal */ _destroy(): void { for (const c of this.#components.values()) c.onRemove(); this.#components.clear(); } } export class World { /** World-level variables, accessible in conditions via the $. prefix */ readonly globals: RPGVariables = {}; readonly #entities = new Map(); readonly #handlers = new Map>(); readonly #globalHandlers = new Map>(); readonly #systems: System[] = []; #entityCounter = 0; get [WORLD_ENTITY_COUNTER](): number { return this.#entityCounter; } set [WORLD_ENTITY_COUNTER](n: number) { this.#entityCounter = n; } get id() { return 'world'; } get context(): EvalContext { return { self: this, world: this }; } /** * Create a new entity and add it to the world. * * @param id - How the entity ID is determined: * - Omitted → auto-generated: `entity_1`, `entity_2`, … * - Plain string → used as-is: `createEntity('player')` → `'player'` * - Template with `*` → `*` is replaced by the auto-incremented counter: * `createEntity('enemy_*')` → `'enemy_1'`, `'enemy_2'`, … * @throws If an entity with the resolved ID already exists. */ createEntity(id?: string): Entity { const entityId = id == null ? `entity_${++this.#entityCounter}` : id.includes('*') ? id.replace('*', String(++this.#entityCounter)) : id; if (this.#entities.has(entityId)) throw new Error(`Entity '${entityId}' already exists`); const entity = new Entity(entityId, this); this.#entities.set(entityId, entity); return entity; } getEntity(id: string): Entity | undefined { return this.#entities.get(id); } destroyEntity(entity: Entity): void { entity._destroy(); this.#entities.delete(entity.id); for (const key of this.#handlers.keys()) { if (key.startsWith(`${entity.id}\0`)) this.#handlers.delete(key); } } *query>(ctor: Class): Generator<[Entity, string, T]> { for (const entity of this.#entities.values()) { for (const [key, component] of entity) { if (component instanceof ctor) { yield [entity, key, component as T] } } } } addSystem(system: System): void { this.#systems.push(system); system.onAdd(this); } removeSystem(system: System): void { const idx = this.#systems.indexOf(system); if (idx !== -1) { this.#systems.splice(idx, 1); system.onRemove(this); } } update(dt: number): void { for (const system of this.#systems) system.update(this, dt); } emit(entityId: string, event: string, data?: unknown): void { const entity = this.getEntity(entityId); if (!entity) return; this.#handlers.get(`${entityId}\0${event}`)?.forEach(h => h({ target: entity, data })); } emitGlobal(event: string, data?: unknown): void { this.#globalHandlers.get(event)?.forEach(h => h({ target: this, data })); } on(event: string, handler: WorldEventHandler): () => void; on(entityId: string, event: string, handler: EntityEventHandler): () => void; on(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler): () => void { if (typeof arg2 === 'string') { return this.#addHandler(this.#handlers, `${arg1}\0${arg2}`, arg3!); } return this.#addHandler(this.#globalHandlers, arg1, arg2); } off(event: string, handler: WorldEventHandler): void; off(entityId: string, event: string, handler: EntityEventHandler): void; off(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler): void { if (typeof arg2 === 'string') { this.#handlers.get(`${arg1}\0${arg2}`)?.delete(arg3!); } else { this.#globalHandlers.get(arg1)?.delete(arg2); } } once(event: string, handler: WorldEventHandler): () => void; once(entityId: string, event: string, handler: EntityEventHandler): () => void; once(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler): () => void { if (typeof arg2 === 'string') { const wrapped: EntityEventHandler = data => { unsub(); arg3!(data); }; const unsub = this.on(arg1, arg2, wrapped); return unsub; } const h = arg2; const wrapped: WorldEventHandler = data => { unsub(); h(data); }; const unsub = this.on(arg1, wrapped); return unsub; } #addHandler( map: Map>, key: string, handler: T, ): () => void { if (!map.has(key)) map.set(key, new Set()); map.get(key)!.add(handler); return () => map.get(key)?.delete(handler); } *[Symbol.iterator]() { yield* this.#entities.values(); } } export function isEvalContext(v: unknown): v is EvalContext { return typeof v === 'object' && v != null && ( (v as EvalContext).self instanceof Entity || (v as EvalContext).self instanceof World ) && (v as EvalContext).world instanceof World; }