import { TypedFastBitSet } from "typedfastbitset"; //#region src/Archetype.d.ts type Archetype = { readonly mask: TypedFastBitSet; readonly tagMask: TypedFastBitSet; readonly entities: Set; readonly adjacent: Map; readonly tagAdjacent: Map; /** * The id of the archetype is a hexadecimal representation of a set of unique bits for all of the `componentIds` and `tagIds` */ readonly id: string; /** * Check if an entity is currently included in this archetype */ hasEntity(entity: string): boolean; /** * Check if this archetype has a `componentId`. * This is typically much faster than checking if `componentIds` includes a given componentId */ hasComponent(component: number): boolean; /** * Check if this archetype has a `tagId` */ hasTag(tag: number): boolean; /** * All the `componentIds` constituting this archetype */ componentIds(): number[]; /** * All the `tagIds` constituting this archetype */ tagIds(): number[]; }; declare function archetypeId(mask: TypedFastBitSet, tagMask: TypedFastBitSet): string; declare function createArchetype(id: string, mask: TypedFastBitSet, tagMask?: TypedFastBitSet): Archetype; /** * Find (or create) the archetype reached by toggling `componentId` on `archetype`. * * The `index` is a per-ECS `Map` keyed by archetype id. Passing * it lets discovery of a novel archetype be an O(1) map lookup instead of an * O(n) `traverseArchetypeGraph` rescan — turning warmup from O(n²) to O(n) * (proposal 15). The live archetype's mask is never mutated: we clone first and * flip the clone, so the function is re-entrant and the "frozen" mask is never * transiently corrupted (proposal 19). */ declare function transformArchetype(archetype: Archetype, componentId: number, index?: Map): Archetype; /** * Find (or create) the archetype reached by toggling `tagId` on `archetype`. * Same id-index lookup and clone-first (non-mutating) contract as * {@link transformArchetype}. */ declare function transformArchetypeTag(archetype: Archetype, tagId: number, index?: Map): Archetype; declare function traverseArchetypeGraph(archetype: Archetype, callback: (archetype: Archetype) => boolean | void, traversed?: Set): boolean; //#endregion //#region src/clone.d.ts /** * Deep-clone a plain entity value: JSON-shaped data (objects, arrays, * primitives) plus `Uint8Array` leaves. This is what entity/component data is * made of (Yjs `toJSON()` output, bebop-decoded records), and on that shape it * is ~8× faster than `structuredClone` — which matters on the per-scope * reconcile drain and the per-frame client store mirror, where a clone runs * for every remotely-changed entity. * * Not a general clone: class instances, Maps/Sets, Dates, and cyclic values * are not supported (cycles recurse forever). Entity data never contains them. */ declare function clonePlainValue(value: T): T; //#endregion //#region src/delta.d.ts /** * Canonical delta algebra for entity components: set/add/remove on array fields, * additive merge on pool (`Record`) fields, and last-write-wins * replace on fields such as behavior trees whose values must not be summed. * * This is the SINGLE source of truth for those semantics. Generated code * (`materializeDelta`, `mergeDelta`, `accumulateDelta` in each app's * `game.generated.ts`) and any client replica route through these functions * instead of re-inlining the loops, so the three operations can never drift. */ /** A delta over an array field: replace wholesale (`set`), or `add`/`remove` members. */ interface ArrayDelta { set?: T[]; add?: T[]; remove?: T[]; } /** * Resolve an array delta against a base array (entity-level). `set` replaces; * otherwise `add` appends de-duplicated members and `remove` filters them out. * Returns a new array and never mutates `base`. */ declare function applyArrayDelta(base: T[], d?: ArrayDelta): T[]; /** A numeric pool field: a string-keyed map of numbers (health, position, ...). */ interface NumberPool { [key: string]: number; } /** * Additively merge a pool delta onto a base pool (entity-level), returning a new * object: `result[k] = (base[k] ?? 0) + delta[k]` for each key present in `delta`. */ declare function applyPoolDelta(base: T, delta: Record): T; /** * Accumulate an array delta INTO another array delta (delta-on-delta, used when * coalescing successive updates in a mutation scope). A `set` wins outright; * otherwise the `add`/`remove` lists concatenate. Returns the merged delta * (mutates `to` when present, else builds a fresh object). */ declare function accumulateArrayDelta(to: ArrayDelta | undefined, from: ArrayDelta): ArrayDelta; /** * Accumulate a pool delta INTO another pool delta (delta-on-delta). Additive on * overlapping keys. Returns a fresh object when `to` is absent, else mutates and * returns `to`. */ declare function accumulatePoolDelta(to: Record | undefined, from: Record): NumberPool; /** * Last-write-wins merge of a replace delta onto a base component (entity-level): * each field the delta defines overwrites the base's, including whole arrays. * Returns a new object and never mutates `base`. */ declare function applyReplaceDelta(base: T, delta: Partial): T; /** * Accumulate a replace delta INTO another (delta-on-delta): the later delta's * defined fields win. Returns a fresh object when `to` is absent, else mutates * and returns `to`. */ declare function accumulateReplaceDelta(to: D | undefined, from: D): D; //#endregion //#region src/Actions.d.ts type GenericAction = { tag: number; value: any; }; /** * Lightweight action class for ECS action system * Replaces CustomEvent to eliminate DOM overhead */ declare class CustomAction { readonly detail: T; defaultPrevented: boolean; constructor(detail: T); preventDefault(): void; } //#endregion //#region src/types.d.ts /** * The minimum shape an entity must have for the ECS to manage hierarchy. * Consumer entity types should extend this interface. * Fields are optional to remain compatible with message/schema-generated types * where all fields are optional by convention. */ type BaseEntity = { id?: string; sk?: string; parent?: string; children?: Array; tags?: Tags[]; }; declare enum MutationType { Insert = 1, Update = 2, Delete = 3 } type InsertMutation = { tag: 1; value: { entity: E; }; }; type UpdateMutation = { tag: 2; value: { delta: D; }; }; type DeleteMutation = { tag: 3; value: { entity: E; }; }; type MutationRecord = InsertMutation | UpdateMutation | DeleteMutation; declare const MutationRecord: Readonly<{ fromInsert(value: { entity: E; }): MutationRecord; fromUpdate(value: { delta: D; }): MutationRecord; fromDelete(value: { entity: E; }): MutationRecord; }>; type EntityMutator = (id: string, mutation: MutationRecord) => void; //#endregion //#region src/MutationScope.d.ts type MergeDeltaFn = (entity: E, delta: D) => void; type AccumulateDeltaFn = (from: D, to: D) => D; declare class MutationScope { private readonly _mergeDelta; private readonly _accumulateDelta; readonly mutations: Map>; readonly shadowEntities: Map; readonly deletedIds: Set; /** Async work the scope waits for before it commits (see `ECS.waitUntil`). */ readonly pending: Promise[]; constructor(_mergeDelta: MergeDeltaFn, _accumulateDelta: AccumulateDeltaFn); /** * Initialize shadow state from a parent scope for nested scope support. */ initializeFromParent(parentScope: MutationScope): void; /** * Get entity from shadow, checking deletion state. * Returns null if explicitly deleted, undefined if not in scope. */ getShadowEntity(entityId: string): E | undefined | null; insert(entityId: string, entity: E): void; update(entityId: string, delta: D): void; delete(entityId: string, entity: E): void; /** * Merge a coalesced mutation record from a nested (inner) scope into this * (parent) scope, routing it through this scope's own insert/update/delete * coalescing so the parent's shadow + mutation map stay consistent. Used when * a nested `withScope` exits: inner writes become visible to the parent's * shadow but only commit when the OUTERMOST scope flushes (transactional * atomicity). */ applyMutation(id: string, record: MutationRecord): void; clear(): void; } //#endregion //#region src/Query.d.ts type QueryMatcher = (target: TypedFastBitSet, archetype: Archetype) => boolean; type Query = { readonly tryAdd: (archetype: Archetype, add?: boolean) => boolean; /** * All archetypes matching the query */ readonly archetypes: Set; }; type QueryBuilder = { /** * Archetypes that has *every* componentId of `componentIds` will be included in the result */ every(...cids: number[]): QueryBuilder; /** * Archetypes that has *some* of the `componentIds` will be included in the result */ some(...cids: number[]): QueryBuilder; /** * Archetypes that has *some* of the `componentIds` will *not* be included in the result */ not(...cids: number[]): QueryBuilder; /** * Archetypes that has *every* componentId of `componentIds` will *not* be included in the result */ none(...cids: number[]): QueryBuilder; /** * Archetypes that has *every* tagId of `tagIds` will be included in the result */ everyTag(...tags: number[]): QueryBuilder; /** * Archetypes that has *some* of the `tagIds` will be included in the result */ someTag(...tags: number[]): QueryBuilder; /** * Archetypes that has *some* of the `tagIds` will *not* be included in the result */ notTag(...tags: number[]): QueryBuilder; /** * Archetypes that has *every* tagId of `tagIds` will *not* be included in the result */ noneTag(...tags: number[]): QueryBuilder; /** * Build a subquery to match a different set of Archetypes. * You may combine as many or subqueries and nested or subqueries as you need */ or(callback: (builder: QueryBuilder) => QueryBuilder): QueryBuilder; /** * Add a custom query matcher. * The matcher function receives a `BitSet` that indicates the presence of `componentIds`, and the `Archetype` associated with the `BitSet` */ custom(matcher: QueryMatcher): QueryBuilder; /** * Query for a prefabricated `archetype`. * May match descendant archetypes, ie archetypes with all of the component ids in the prefab *and* additional component ids added to entities in the prefabricated archetype or descendant archetypes */ prefabricated(archetype: Archetype): QueryBuilder; toQuery(): Query; readonly matchers: ReadonlyArray; }; declare function createBuilder(): QueryBuilder; /** * Build a {@link Query} from a {@link QueryBuilder} callback, e.g. * `query((q) => q.every(Position, Velocity).someTag(Hostile))`. Pass the result * to {@link ECS.query} (or a system factory) to match entities by their * component + tag set. */ declare function query(callback: (builder: QueryBuilder) => QueryBuilder): Query; //#endregion //#region src/System.d.ts /** * Discriminator for the {@link System} union. Numeric values are load-bearing * (the update loop and registration dispatch on them); they are NOT serialized, * so the names — not the numbers — are the API. Use these constants instead of * raw `0..4` literals everywhere a system's `type` is read. */ declare enum SystemType { Entity = 0, Archetype = 1, Event = 2, Lifecycle = 3, Behavior = 4 } type BaseSystem = { readonly query: Query; }; type EntitySystem, UpdateArguments extends unknown[], Actions extends GenericAction, Tags extends number = number, E extends BaseEntity = BaseEntity, D = unknown> = BaseSystem & { readonly type: SystemType.Entity; execute(entities: Array, world: ECS, ...args: UpdateArguments): void; }; type ArchetypeSystem, UpdateArguments extends unknown[], Actions extends GenericAction, Tags extends number = number, E extends BaseEntity = BaseEntity, D = unknown> = BaseSystem & { readonly type: SystemType.Archetype; execute(archetypes: Set, world: ECS, ...args: UpdateArguments): void; }; type EventSystem = BaseSystem & { readonly type: SystemType.Event; execute(entities: Array): void; }; type LifecycleSystem = BaseSystem & { readonly type: SystemType.Lifecycle; execute(entity: string): void; }; type Behavior, UpdateArguments extends unknown[], Actions extends GenericAction, Tags extends number = number, E extends BaseEntity = BaseEntity, D = unknown> = BaseSystem & { readonly type: SystemType.Behavior; tag: number; handler: (world: ECS, entity: E, event: CustomAction) => void | Promise; priority: number | undefined; }; /** * The handler {@link createBehavior} accepts: `event.detail` is narrowed to the * `Actions` member whose tag is `Tag`. */ type BehaviorHandler, UpdateArguments extends unknown[], Actions extends GenericAction, Tags extends number = number, E extends BaseEntity = BaseEntity, D = unknown, Tag extends Actions["tag"] = Actions["tag"]> = (world: ECS, entity: E, event: CustomAction>) => void | Promise; type System, UpdateArguments extends unknown[], Actions extends GenericAction, Tags extends number = number, E extends BaseEntity = BaseEntity, D = unknown> = EntitySystem | ArchetypeSystem | EventSystem | LifecycleSystem | Behavior; /** * An entity system is a system that will be executed for each archetype matching the query. * In other words, it may be executed multiple times in each update. * If you need the system to only execute once in each update, use the `ArchetypeSystem` created by `createArchetypeSystem` * @param execute * @param query * @returns */ declare function createEntitySystem, UpdateArguments extends unknown[], Actions extends GenericAction, Tags extends number = number, E extends BaseEntity = BaseEntity, D = unknown>(execute: (entities: Array, world: ECS, ...args: UpdateArguments) => void, query: Query | ((buildQuery: QueryBuilder) => QueryBuilder)): EntitySystem; /** * An archetype system is a system that that will only execute once in each update with all the archetypes matching the query. * This is usefull when your query potentially matches 2 or more archetypes and you need to check for the presence of a componentId on entities. * The differing components can be checked for once for each archetype instead of for each entity. * @param execute * @param queryParams * @returns */ declare function createArchetypeSystem, UpdateArguments extends unknown[], ReturnArguments, Actions extends GenericAction, Tags extends number = number, E extends BaseEntity = BaseEntity, D = unknown>(execute: (archetypes: Set, world: ECS, ...args: UpdateArguments) => ReturnArguments, query: Query | ((buildQuery: QueryBuilder) => QueryBuilder)): ArchetypeSystem; /** * An event system runs reactively (via {@link ECS.subscribe}) whenever an entity * matching `query` changes, rather than every update cycle. The executor receives * the matching entity ids. * @param execute * @param query * @returns */ declare function createEventSystem(execute: (entities: Array) => void, query: Query | ((buildQuery: QueryBuilder) => QueryBuilder)): EventSystem; /** * A lifecycle system runs once per entity when it is created ({@link ECS.onCreate}) * or deleted ({@link ECS.onDelete}) and matches `query`. The executor receives the * single entity id. * @param execute * @param query * @returns */ declare function createLifecycleSystem(execute: (entity: string) => void, query: Query | ((buildQuery: QueryBuilder) => QueryBuilder)): LifecycleSystem; /** * Create an event-driven {@link Behavior} for {@link ECS.registerBehavior}, keyed * by an action `tag`. When {@link ECS.act} dispatches that tag to an entity * matching `query`, `handler(world, entity, event)` runs with `event.detail` * narrowed to the `Actions` member for `tag`. An optional `priority` orders * behaviors sharing a tag. * @param tag action tag this behavior responds to * @param handler runs against the world + struck entity + action payload * @param query which entities this behavior applies to * @param priority higher runs first among behaviors for the same tag */ declare function createBehavior, UpdateArguments extends unknown[], Actions extends GenericAction, Tags extends number = number, E extends BaseEntity = BaseEntity, D = unknown, Tag extends Actions["tag"] = Actions["tag"]>(tag: Tag, handler: BehaviorHandler, query: Query | ((buildQuery: QueryBuilder) => QueryBuilder), priority?: number): Behavior; //#endregion //#region src/ECS.d.ts interface ECSOptions { createId: () => string; components: Record; materializeDelta: (delta: D, base?: Partial) => E; mergeDelta: MergeDeltaFn; accumulateDelta: AccumulateDeltaFn; } type StateWithScope, E extends BaseEntity, D> = C & { scope?: MutationScope; }; /** A coalesced set of mutations keyed by entity id (same shape the flush handler gets). */ type MutationBatch = Map>; /** * A remote observer of committed mutations (interest-managed broadcast). The game * supplies `interested` (its subjective area-of-interest policy) and `deliver` * (push the filtered batch to a transport, e.g. a framed `ws.send`). Both are * called by {@link ECS.routeMutations} after a scope commits. * * This is the per-observer routing mechanism that sits beside `setFlushHandler`: * the flush handler is the single authoritative write, mutation observers are the * many filtered readers. Both consume the same coalesced `Map`. */ interface MutationObserver, UpdateArguments extends Array, Actions extends GenericAction, Tags extends number, E extends BaseEntity, D> { /** * Is this single committed mutation relevant to this observer? Called once per * (observer × mutation) per routed flush. Close over the observer's * per-connection interest state (its viewer entity id, last-known position, * room…). Keep it cheap; back it with the game's own spatial index for O(1) * when scaling. * * For DELETE mutations the entity is already gone from the world, so read its * last state from `mutation.value.entity`, NOT from `world.entity(id)`. * * MUST be read-only: routing runs in the commit tail, so neither `interested` * nor `deliver` may mutate the world. */ interested(entityId: string, mutation: MutationRecord, world: ECS): boolean; /** Deliver the non-empty interested subset. Called at most once per routed flush. */ deliver(batch: MutationBatch): void; } /** * The core Entity-Component-System world. * * The generic parameters are a deliberate, stable part of the public API and are * NOT reducible to plain strings: entities are addressed by string id internally, * but consumers parametrize the world with their own `State`, `UpdateArguments`, * `Actions`, `Tags`, entity (`E`) and delta (`D`) types so that systems, * behaviors, queries and mutations are statically type-checked against the * consumer's domain model. Treat this signature as locked; downstream packages * depend on it. * * @typeParam State - Per-world context object (carries the active mutation scope). * @typeParam UpdateArguments - Extra arguments threaded into every `update()` call. * @typeParam Actions - The action union dispatched via `act`/`actWithBubbling`. * @typeParam Tags - Numeric tag id space for tag archetypes. * @typeParam E - The consumer's entity shape (must extend {@link BaseEntity}). * @typeParam D - The delta shape used by `put`/`upsert` and mutation coalescing. */ declare class ECS, UpdateArguments extends Array, Actions extends GenericAction, Tags extends number = number, E extends BaseEntity = BaseEntity, D = unknown> { protected readonly rootArchetype: Archetype; private readonly _archetypeIndex; readonly entityArchetype: Map; readonly deletedEntities: Set; readonly systems: System[]; readonly subscriptions: EventSystem[]; readonly behaviors: Map[]>; readonly entityBehaviorCache: Map[]>>; private readonly archetypeBehaviorCache; private readonly _deferredCacheRebuilds; private readonly _queryCache; private readonly _systemScratch; readonly handleCreate: LifecycleSystem[]; readonly handleDelete: LifecycleSystem[]; readonly deferred: (() => void)[]; private _initialized; get initialized(): boolean; private _scopeOpenCallbacks; private _flushHandler; private readonly _observers; /** * Register a mutation observer for interest-managed broadcast. Returns an * unsubscribe function. Per-world (per DO). See {@link MutationObserver}. */ observeMutations(observer: MutationObserver): () => void; /** Number of registered mutation observers (e.g. to gate work when zero). */ get observerCount(): number; /** * Route a committed batch to every interested observer. Builds a per-observer * subset via `interested` and calls `deliver` only when the subset is non-empty * (so an uninterested observer pays no send). O(observers × mutations) predicate * evaluations; zero cost when no observers are registered. * * Called automatically on every outermost-scope commit (see {@link withScope}), * after the authoritative flush, so observers never see uncommitted state. * Also public as an escape hatch for apps that route only certain scopes. */ routeMutations(mutations: MutationBatch): void; /** * Build insert-mutations for every entity matching `predicate`. Use for the * initial interest-filtered snapshot an `observe` RPC yields first, and for * re-sync frames when a viewer's interest set changes. */ snapshotMutations(predicate: (id: string, entity: E) => boolean): MutationBatch; /** * Ingest a coalesced batch of mutations into this world — the inverse of * {@link snapshotMutations}/{@link routeMutations}. Each record is dispatched * through the public, archetype-aware ops so queries/systems stay consistent: * Insert replaces any existing entity (so a re-sync snapshot is authoritative), * Update applies its delta via `put` (running the configured additive * `mergeDelta`), Delete removes the entity. * * This is the consumer-side counterpart to the producer/broadcast pair: a * remote replica (e.g. a UI client draining an `observe` stream, or a DO * rehydrating from persisted batches) applies what the server snapshotted and * routed. Call inside {@link withScope} to coalesce the batch into one commit * and capture the committed result. Does not itself open a scope, so it * composes inside an existing one. */ applyMutations(mutations: MutationBatch): void; /** * Register a callback invoked when a mutation scope is opened (before the scope function runs). * Returns an unsubscribe function. */ onScopeOpen(cb: (scope: MutationScope) => void): () => void; /** * Set a handler that replaces the default per-mutation flush loop. * The handler receives all coalesced mutations from the scope and is responsible for * applying them (to entities, Yjs, etc.). Pass `null` to restore default behavior. */ setFlushHandler(handler: ((mutations: Map>) => void) | null): void; private static readonly DELETED_RING_CAP; readonly context: StateWithScope; protected readonly _entities: Map; protected readonly _baseMutate: EntityMutator; protected readonly _mutate: EntityMutator; protected readonly options: ECSOptions; constructor(entities: Map, mutate: EntityMutator, context: State, options: ECSOptions); private _wrapMutator; /** * Read an entity by id, or `undefined` if it does not exist. Inside an open * mutation scope this returns the scope's shadow copy — so reads observe * staged, not-yet-committed writes — and `undefined` if it was deleted in the * scope. */ entity(id: string): E | undefined; /** Live `Map` of all committed entities. Treat as read-only. */ get entities(): Map; /** * Find the first entity matching `filter` among those carrying every component * present in `delta`, then apply `delta` to it via {@link put}; if none match, * {@link insert} a new entity materialized from `delta`. Repeated calls with the * same component shape reuse a cached query. */ upsert(delta: D, filter: (e: E | undefined, i: number, obj: string[]) => boolean, mutate?: boolean): E; /** * Collect every entity id from the archetypes a (kept-current) Query already * matches, in the same reverse-archetype order `query()` uses. Used by * `upsert` to avoid re-traversing the whole archetype graph per call. */ private _collectQueryEntities; /** * Insert a new entity. If `entity.id` is unset a fresh id is generated and a * copy is committed (the caller's object is never mutated); the committed entity * is returned. The insert mutation is recorded before create/event systems fire * so observers never see a torn read. */ insert(entity: E): E; /** * Patch an existing entity by `id` with `delta`, or {@link insert} a new one * (materialized from `delta`) when `id` is missing/unknown. With `mutating:true`, * `undefined` delta keys remove their component (changing the archetype); * otherwise absent keys are left untouched. Returns the pre-change values of the * touched keys. */ put(id: string | undefined, delta: D, mutating?: boolean): E; /** * Remove an entity (looked up by its `.id`), recording a delete mutation. * No-op (with a warning) if the id is missing or unknown. */ delete(entity: E): E | undefined; /** * Return the ids of every entity matching the query — either a {@link Query} or * a builder callback, e.g. `query((q) => q.every(A, B).someTag(T))`. The result * is a fresh array owned by the caller (never a recycled/pooled buffer). * * Cost note: every call re-walks the full archetype graph and allocates the * result array. For a query evaluated every frame, register a system * ({@link registerSystem}) or a membership tracker * ({@link createQueryMembershipTracker}) instead — both maintain their match * set incrementally as archetypes appear. */ query(_query: Query | ((builder: QueryBuilder) => QueryBuilder)): string[]; /** * Register an event {@link System} that runs whenever a matching entity * changes. Pass `emit:true` to also run it once immediately over current * matches. Returns an unsubscribe function. */ subscribe(system: EventSystem, emit?: boolean): () => void; /** Register a lifecycle system run when a matching entity is created. Returns an unsubscribe function. */ onCreate(system: LifecycleSystem): () => LifecycleSystem; /** Register a lifecycle system run when a matching entity is deleted. Returns an unsubscribe function. */ onDelete(system: LifecycleSystem): () => LifecycleSystem; private _executeEventSystems; private _executeEventSystem; private _executeDeferred; private _executeDeferredCacheRebuilds; private _tryAddArchetypeToQueries; private _assertEntity; private _transformEntityForComponent; private _transformEntityForTag; private _reconcileTags; /** * Provide a known combination of `componentIds` constituting an archetype. * The component ids can be of your choosing, but be carefull not to use the same id for different components. * You should either create all `componentIds` using `createComponentId` first and use the created component ids in the prefacbricate, * Or make all of you prefabricates before creating new component ids using `createComponentId` */ prefabricate(components: number[], tags?: number[]): Archetype; /** * Registers a system to be executed for each update cycle. * Use the `createEntitySystem` or `createArchetypeSystem` helpers to create the system. * A system may not be executed if it's `Query` does not match any `Archetype`s. */ registerSystem(system: System): () => System; /** * Register an event-driven {@link Behavior}, keyed by its action tag and run by * {@link act}. Use the `createBehavior` helper to build one. Registering after * {@link initialize} invalidates the behavior caches so the new behavior is * picked up by already-seen archetypes. */ registerBehavior(behavior: Behavior): void; private rebuildAllBehaviorCaches; rebuildBehaviorCache(entityId: string): void; /** * Initialize the world, must be done before the first update. * Subsequent calls to initialize will be voided. */ initialize(): void; /** * Update the world, executing all registered systems with queries matching 1 or more `Archetype`. * Typically you want to call `update` on each animation frame (`window.requestAnimationFrame`). * @throws {WorldNotInitializedError} if `initialized` has not been called */ update(...args: UpdateArguments): void; /** Dispatch the same `action` to every id in `entityIds` concurrently (see {@link act}). */ actBatch(entityIds: string[], action: Ac): Promise; /** * Return an entity's behavior cache, materializing it first if a deferred * rebuild is pending. Mutators only enqueue rebuilds (drained by `update()`), * so a reactive caller that dispatches via `act`/`actWithBubbling` outside an * update tick would otherwise read a stale or absent cache. Rebuilding here * and clearing the pending flag keeps those reads correct and avoids a * redundant rebuild at the next `update()`. */ private _ensureBehaviorCache; /** * Dispatch `payload` to the entity's matching behaviors, then propagate the * same action down to its children (recursive), so acting on a parent cascades * to its whole subtree. Resolves `false` if the entity does not exist. */ act(entityId: string, payload: Ac, action?: CustomAction): Promise; actWithBubbling(entityId: string, action: Ac): Promise; /** * Defer execution of an action until the end of the update cycle (after all systems has been executed) * For best performance you try to defer a batched action instead of many small actions, or avoid defering if possbile */ defer(action: () => void): void; /** * Hold the active {@link withScope} open until `work` settles, so async work a * synchronous system starts during `update()` (such as dispatching `act`) * commits in that scope's batch instead of landing after it closed. A * rejection aborts the scope. Outside a scope nothing waits for `work`. */ waitUntil(work: Promise): void; /** * Create a new mutation scope for tracking entity changes. * Mutations recorded in a scope can be coalesced and flushed together. */ createScope(): MutationScope; /** * Execute a function within a mutation scope. * All entity mutations (insert/put/delete) within the function are tracked * and deferred until scope completion. Entity reads within the scope see * pending mutations via shadow entities. * Returns the result and coalesced mutations after execution. * Supports nested scopes - inner scopes inherit parent's shadow state. */ withScope(fn: () => T | Promise): Promise<{ result: T; mutations: Map>; }>; /** * Check if the entity exists in the world */ hasEntity(entity: string): boolean; /** * Create an entity. * An entity is just an Id. * Previously deleted entity id's will be reused * Optionally supply a prefacbricated archetype */ createEntity(prefabricate?: Archetype, id?: string, executeSystems?: boolean): string; protected _handleCreate(entity: string): void; /** * Delete an entity, removing it from its current archetype (loosing all of its components). * @throws {EntityUndefinedError | EntityDeletedError | EntityNotExistError} */ deleteEntity(entity: string): void; private _handleDelete; /** * Transform the entity to that of a prefabricated archetype. * Any components added to the entity that does not exist in the prefabricate will be removed. * This is a sligthly faster operation than adding/subtracting components * @throws {EntityUndefinedError | EntityDeletedError | EntityNotExistError} */ transformEntity(entity: string, prefabricate: Archetype): void; /** * Check if the entity has a componentId */ hasComponent(entity: string, _component: Exclude): boolean; /** * Adds the componentId to the entity. * The entity will be moved to a different archetype * @throws {EntityUndefinedError | EntityDeletedError | EntityNotExistError} */ addComponent(entity: string, _component: Exclude, executeSystems?: boolean): void; private _addComponent; /** * Removes the componentId from the entity. * The entity will be moved to a different archetype * @throws {EntityUndefinedError | EntityDeletedError | EntityNotExistError} */ removeComponent(entity: string, _component: Exclude, executeSystems?: boolean): void; private _removeComponent; /** * Check if the entity has a tag */ hasTag(entity: string, tag: Tags): boolean; /** * All tags currently assigned to the entity */ getTags(entity: string): Tags[]; /** * Adds a tag to the entity. * The entity will be moved to a different archetype in the tag graph */ addTag(entity: string, tag: Tags, executeSystems?: boolean): void; /** * Removes a tag from the entity. * The entity will be moved to a different archetype in the tag graph */ removeTag(entity: string, tag: Tags, executeSystems?: boolean): void; executeEventSystems(id: string): void; parent(id: string, childId: string): Pick | undefined; unparent(id: string, childId: string): E | undefined; } //#endregion //#region src/mutator.d.ts /** * The slice of `ECSOptions` a base mutator needs to apply a coalesced mutation * to a plain entity store. `ECSOptions` satisfies this structurally, so callers * pass their full options object directly. */ interface BaseMutatorOptions { mergeDelta: (entity: E, delta: D) => void; materializeDelta: (delta: D, base?: Partial) => E; } interface BaseMutatorConfig { /** * When an Update arrives for an id not present in the store, materialize the * delta into a fresh entity and insert it instead of no-op'ing. Client replicas * want this (a delta may be the first frame they see for an entity); the server, * which authored the Insert, does not. */ materializeOnMissingUpdate?: boolean; } /** * Apply a single coalesced mutation to a flat entity store. The one canonical * Insert/Update/Delete switch, shared by every base mutator (server working * store, server flush handler, client read-replica) so they can never drift. */ declare function applyMutation(entities: Map, id: string, mutation: MutationRecord, options: BaseMutatorOptions, config?: BaseMutatorConfig): void; /** * Build the standard base mutator (`Insert` → set, `Update` → `mergeDelta`, * `Delete` → remove) over an entity store, wrapping {@link applyMutation}. */ declare function createBaseMutator(entities: Map, options: BaseMutatorOptions, config?: BaseMutatorConfig): EntityMutator; //#endregion //#region src/QueryMembership.d.ts /** * A query whose matching entity-id set is kept current as mutations commit. The * `members` set is owned by the tracker and mutated in place by `update`. */ interface TrackedQuery { readonly query: Query; readonly members: Set; } /** * The minimal slice of an ECS world the membership tracker reads: the * entity → archetype index and a snapshot `query`. `ECS` satisfies this * structurally, so any world (or a test double) can be passed directly. */ interface QueryMembershipWorld { readonly entityArchetype: ReadonlyMap; query(q: Query): Iterable; } interface QueryMembershipTracker { /** * Start tracking `query`, seeding its member set from the world's current * matches. Idempotent per `Query` reference — re-tracking returns the existing * {@link TrackedQuery}. */ track(query: Query): TrackedQuery; /** Stop tracking `query` and drop its member set. */ untrack(query: Query): void; /** * Apply a committed mutation batch: re-test ONLY the ids it touched against * every tracked query (via the O(1), non-mutating `query.tryAdd(archetype, * false)`) and update membership in place. Returns the tracked queries whose * membership actually changed, so a host can notify exactly those. * * This closes the gap a `subscribe(Q)` event system cannot: it also fires when * an entity transitions OUT of `Q` (its new archetype isn't in `Q`'s set, so * no event system keyed on `Q` would ever see it). */ update(batch: ReadonlyMap): TrackedQuery[]; } /** * Framework-agnostic per-query membership tracking, driven by committed mutation * batches rather than ECS event systems. UI bindings (Solid, React, Vue, …) wrap * this with their own reactivity; the diff logic lives here once. */ declare function createQueryMembershipTracker(world: QueryMembershipWorld): QueryMembershipTracker; //#endregion //#region src/behavior-tree/types.d.ts /** * Structural mirrors of the `@vampgg/utils/schema/behavior.bop` messages, so the * evaluator runs against any app's generated bebop types without importing them. */ declare const BehaviorNodeKind: { readonly Selector: 1; readonly Sequence: 2; readonly Weighted: 3; readonly Condition: 4; readonly Task: 5; readonly Invert: 6; readonly Chance: 7; readonly Cooldown: 8; }; type BehaviorNodeKind = (typeof BehaviorNodeKind)[keyof typeof BehaviorNodeKind]; interface BehaviorNode { kind?: number; children?: number[]; weight?: number; leaf?: number; args?: number[]; } /** A flat, index-linked tree; `nodes[0]` is the root. */ interface BehaviorTree { nodes?: BehaviorNode[]; } /** * Per-agent state: which tree it runs and when each cooldown node is ready again. * `readyAt` and `last` index into the tree whose fingerprint is `fingerprint`. */ interface Brain { tree?: string; readyAt?: number[]; last?: number; fingerprint?: number; } /** * The randomness a tree consumes. A seeded `@vampgg/rot` `RNG` satisfies it, so * a world that owns its RNG replays the same choices. */ interface BehaviorRandom { getUniform(): number; getWeightedValue(data: Record): string | number | undefined; } type BehaviorCondition = (world: W, entity: E, args: readonly number[]) => boolean; /** A task succeeds when it returns intents to dispatch, and fails on `undefined`. */ type BehaviorTask = (world: W, entity: E, args: readonly number[]) => A | A[] | undefined; type BehaviorStatus = "success" | "failure"; //#endregion //#region src/behavior-tree/builder.d.ts /** A nested node, before {@link tree} flattens it into index-linked form. */ interface BehaviorSpec { kind: BehaviorNodeKind; children: BehaviorSpec[]; weight?: number; leaf?: number; args?: number[]; } /** Succeeds with the first child that succeeds. */ declare const selector: (...children: BehaviorSpec[]) => BehaviorSpec; /** Succeeds when every child succeeds, in order. */ declare const seq: (...children: BehaviorSpec[]) => BehaviorSpec; /** Runs every branch, then picks one of those that succeeded by relative weight. */ declare const weighted: (...branches: [weight: number, child: BehaviorSpec][]) => BehaviorSpec; /** A registered predicate, e.g. `cond(Condition.PlayerNear, 6)`. */ declare const cond: (leaf: number, ...args: number[]) => BehaviorSpec; /** A registered task that returns action intents. */ declare const task: (leaf: number, ...args: number[]) => BehaviorSpec; declare const invert: (child: BehaviorSpec) => BehaviorSpec; /** Passes `percent`% of the time, then runs `child` if given. */ declare const chance: (percent: number, child?: BehaviorSpec | undefined) => BehaviorSpec; /** Fails until `ticks` have passed since it last succeeded, then runs `child` if given. */ declare const cooldown: (ticks: number, child?: BehaviorSpec | undefined) => BehaviorSpec; /** Flatten a nested spec into a {@link BehaviorTree} with the root at `nodes[0]`. */ declare function tree(root: BehaviorSpec): Required; //#endregion //#region src/behavior-tree/evaluate.d.ts interface BehaviorContext { world: W; entity: E; conditions: Readonly>>; tasks: Readonly>>; random: BehaviorRandom; /** Current tick, compared against `Brain.readyAt` by cooldown nodes. */ now: number; } interface BehaviorResult { status: BehaviorStatus; /** Actions the winning branch's tasks asked for, in tree order. Not yet dispatched. */ intents: A[]; /** Brain fields that changed; empty when nothing did. */ brain: Brain; } /** * Evaluate `tree` from the root for one agent. Pure apart from the RNG: tasks * only return intents, and a subtree's intents and cooldowns take effect only * when that subtree succeeds, so a failed sequence or an unpicked weighted * branch leaves no trace. A brain whose `fingerprint` differs from the tree's was * built against another tree, so it is evaluated as fresh and fully rewritten. */ declare function evaluate(tree: BehaviorTree, brain: Brain, ctx: BehaviorContext): BehaviorResult; //#endregion //#region src/behavior-tree/system.d.ts interface BehaviorTreeSystemOptions, UpdateArguments extends unknown[], Actions extends GenericAction, Tags extends number, E extends BaseEntity, D, C extends number = number, T extends number = number> { /** The agents to run: entities carrying the `brain` field. */ query: Query | ((buildQuery: QueryBuilder) => QueryBuilder); /** Entity field holding each agent's {@link Brain}. */ brain: keyof E & string; /** Entity field holding the {@link BehaviorTree} on the entity a brain points at. */ tree: keyof E & string; conditions: Readonly, E>>>; tasks: Readonly, E, Actions>>>; /** The world's seeded RNG, so replays make the same choices. */ random: (world: ECS) => BehaviorRandom; /** The world's current tick, for cooldowns. */ now: (world: ECS) => number; /** Entity an intent is dispatched to via `act`; defaults to the agent. `undefined` drops it. */ target?: (intent: Actions, agentId: string) => string | undefined; } /** * Run every agent's behavior tree once per `update()`. Brain changes are `put` * straight away; the intents are dispatched through `act` after all systems ran, * in agent order, and the surrounding `withScope` waits for them (see * `ECS.waitUntil`) so the resulting mutations commit in the same batch. */ declare function createBehaviorTreeSystem, UpdateArguments extends unknown[], Actions extends GenericAction, Tags extends number, E extends BaseEntity, D, C extends number = number, T extends number = number>(options: BehaviorTreeSystemOptions): ArchetypeSystem; //#endregion export { type AccumulateDeltaFn, type Archetype, type ArchetypeSystem, type ArrayDelta, type BaseEntity, type BaseMutatorConfig, type BaseMutatorOptions, type Behavior, type BehaviorCondition, type BehaviorContext, type BehaviorHandler, type BehaviorNode, BehaviorNodeKind, type BehaviorRandom, type BehaviorResult, type BehaviorSpec, type BehaviorStatus, type BehaviorTask, type BehaviorTree, type BehaviorTreeSystemOptions, type Brain, CustomAction, type DeleteMutation, ECS, type ECSOptions, type EntityMutator, type EntitySystem, GenericAction, type InsertMutation, type MergeDeltaFn, type MutationBatch, type MutationObserver, MutationRecord, type MutationScope, MutationType, type Query, type QueryBuilder, type QueryMembershipTracker, type QueryMembershipWorld, type System, SystemType, type TrackedQuery, type UpdateMutation, accumulateArrayDelta, accumulatePoolDelta, accumulateReplaceDelta, applyArrayDelta, applyMutation, applyPoolDelta, applyReplaceDelta, archetypeId, chance, clonePlainValue, cond, cooldown, createArchetype, createArchetypeSystem, createBaseMutator, createBehavior, createBehaviorTreeSystem, createBuilder, createEntitySystem, createEventSystem, createLifecycleSystem, createQueryMembershipTracker, evaluate, invert, query, selector, seq, task, transformArchetype, transformArchetypeTag, traverseArchetypeGraph, tree, weighted };