import type { PropertyBag } from '../model/Properties'; import { sumAlongChain } from '../model/walk'; import { enabledAlongChain } from '../properties/enabled'; /** * 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; } 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. */ 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; /** Went down / up during this frame. */ pressed: boolean; released: boolean; } /** * 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; /** 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; }