import type { PropertyBag } from '../model/Properties'; import { sumAlongChain } from '../model/walk'; import { enabledAlongChain } from '../properties/enabled'; /** * What a graph sees of a running entity. The runtime's Entity satisfies this shape; unit * tests use plain objects. Mutating `x`/`y`/`props` is how actions take effect. */ export interface EntityHandle { id: string; /** Asset this entity was instantiated from (what `Is asset` compares against). */ assetId: string; name: string; /** Position local to `parent` (world position for top-level entities). */ x: number; y: number; z: number; props: PropertyBag; /** The entity this one is parented to; children move with their parent. */ parent: EntityHandle | null; } /** World position of a handle: its local position plus those of all ancestors. */ export function worldPositionOf(handle: EntityHandle): { x: number; y: number; z: number } { return sumAlongChain( handle, (h) => h.parent, (h) => h, ); } /** Whether a handle and every ancestor of it are switched on (see `properties/enabled.ts`). */ export function isHandleEnabled(handle: EntityHandle): boolean { return enabledAlongChain( handle, (h) => h.props, (h) => h.parent, ); } /** Where the mouse is (world units) and what it did this frame. */ export interface PointerState { x: number; y: number; /** Whether the pointer is over the game view at all. */ inside: boolean; down: boolean; /** Went down / up during this frame. */ pressed: boolean; released: boolean; } /** * Services a graph may use while running. Implemented by the runtime (BehaviorSystem) and * by test doubles. Everything the graph does to the world goes through here, so the core * stays free of runtime imports. */ export interface NodeContext { /** The behavior entity itself. */ self: EntityHandle; /** The behavior's parent object (the one the graph acts on), if any. */ owner: EntityHandle | null; /** Mutable variable state of this behavior instance. */ variables: PropertyBag; /** Seconds since the world started and since the last tick. */ time: number; dt: number; findByName(name: string): EntityHandle | null; isKeyDown(key: string): boolean; /** Mouse state of this frame, in world units. */ 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 (so a button made of a container * with a sprite and a label is hit as a whole). */ hitTest(target: EntityHandle, x: number, y: number): boolean; /** * Whether any collider of `a` overlaps any collider of `b` right now. A collider entity * stands for itself; any other entity stands for its direct collider children. */ isTouching(a: EntityHandle, b: EntityHandle): boolean; /** * Moves `target` by (dx, dy) in local units, stopping its solid colliders at other solid * colliders axis by axis (x first, then y) so it slides along walls. Reports which axes hit. */ moveAndSlide(target: EntityHandle, dx: number, dy: number): { blockedX: boolean; blockedY: boolean }; log(message: string): void; /** * Places a new instance of an asset into the running scene and returns it. The position is * local to `parent` (world when none). Returns null when the asset does not exist or cannot * be placed (a scene). Spawned behaviors run their `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; }