import type { Mat } from '../model/matrix'; import type { Position, PropertyBag } from '../model/Properties'; import { type ChainTransform, composeAlongChain } from '../model/walk'; import { enabledAlongChain } from '../properties/enabled'; import { localMatrixOf, positionOf, transformOf } from '../properties/transform'; /** * Marks a live entity apart from a struct value: both reach a data edge as an object. A symbol * rather than a field, so no struct field name can impersonate it – and it never serializes, * which is right for a thing that exists only while the game runs. */ export const ENTITY_HANDLE: unique symbol = Symbol('entity handle'); /** * What a graph sees of a running entity; the runtime's Entity satisfies this shape. Mutating * `x`/`y`/`props` is how actions take effect. */ export interface EntityHandle { readonly [ENTITY_HANDLE]: true; id: string; assetId: string; name: string; /** Position local to `parent` (world position for top-level entities). */ x: number; y: number; z: number; props: PropertyBag; parent: EntityHandle | null; /** * Counts the Emit pulses a particle source has been given. Kept off `props` for the reason the * animation clock is: what the world writes back to the model must not grow an override per frame. */ emits?: number; } /** Position, rotation, scale and mirror of every ancestor applied, nearest last. */ export function worldTransformOf(handle: EntityHandle): ChainTransform { return composeAlongChain( handle, (h) => h.parent, (h) => ({ m: localMatrixOf(h, transformOf(h.props)), z: h.z }), ); } export function worldMatrixOf(handle: EntityHandle): Mat { return worldTransformOf(handle).m; } export function worldPositionOf(handle: EntityHandle): Position { return positionOf(worldTransformOf(handle)); } /** Whether a handle and every ancestor of it are switched on. */ export function isHandleEnabled(handle: EntityHandle): boolean { return enabledAlongChain( handle, (h) => h.props, (h) => h.parent, ); } /** Position in world units. */ export interface PointerState { x: number; y: number; inside: boolean; down: boolean; pressed: boolean; released: boolean; } /** * What is shared for the length of one run: the values of each Variables asset, keyed by its id, * and the current state of each `global` machine. Two Variables assets may declare the same name * without meeting, since a node names the asset it reads (see `graph/sharedState.ts`). */ export interface SharedState { variables: Record; states: Record; } /** * Everything the graph does to the world goes through here, so the core stays free of runtime * imports. Implemented by the runtime (BehaviorSystem) and by test doubles. */ export interface NodeContext { self: EntityHandle; /** The behavior's parent object – the one the graph acts on. */ owner: EntityHandle | null; variables: PropertyBag; /** * The current state of each state machine this instance has set, keyed by machine asset id, * as the state node's id. A machine not in here is in no state: `State branch` follows nothing. */ states: Record; shared: SharedState; /** Seconds since the world started and since the last tick. */ time: number; dt: number; findByName(name: string): EntityHandle | null; isKeyDown(key: string): boolean; pointer: PointerState; /** * Whether a world point lies on `target`: on its colliders when it has any, otherwise on its own * footprint or that of anything parented below it. */ hitTest(target: EntityHandle, x: number, y: number): boolean; /** A collider entity stands for itself; any other entity for its direct collider children. */ isTouching(a: EntityHandle, b: EntityHandle): boolean; /** * Moves `target` by (dx, dy) in local units, stopping solid colliders at other solid ones axis by * axis (x first, then y) so it slides along walls. */ moveAndSlide(target: EntityHandle, dx: number, dy: number): { blockedX: boolean; blockedY: boolean }; log(message: string): void; /** * The position is local to `parent` (world when none); null when the asset does not exist or * cannot be placed. A spawned behavior runs its `On start` on the next tick. */ spawn( assetId: string, position: { x: number; y: number; z: number }, parent: EntityHandle | null, ): EntityHandle | null; destroy(target: EntityHandle): void; stop(): void; /** Ends the run and starts it again from the saved scene (everything spawned is lost). */ restart(): void; }