import type { AnimationPlayer } from '../model/animation'; 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 players are: what the world writes back to the model must not grow an override per frame. */ emits?: number; /** What `Play animation` started on this object (see `model/animation.ts`). */ animations?: AnimationPlayer[]; } /** 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; } /** * What a graph can do to the played game's physics (#147). The object is the one a rigidbody sits * on, or the rigidbody itself; only a dynamic body is acted on or read, and each call answers * whether there was one. Vectors are world units, turning is degrees the way rotation counts. */ export interface PhysicsControl { velocityOf(target: EntityHandle): { x: number; y: number; angular: number } | null; setVelocity(target: EntityHandle, x: number, y: number): boolean; setAngularVelocity(target: EntityHandle, degrees: number): boolean; /** World units × kg per second: the velocity changes by impulse ÷ mass. */ applyImpulse(target: EntityHandle, x: number, y: number): boolean; /** World units per second squared, for the rest of the run. */ setGravity(x: number, y: number): void; } /** * What a move with collision met: which of the object's own axes something stood against, whether * it hit anything, and the world normal of the first contact (pointing away from what was hit). */ export interface MoveOutcome { blockedX: boolean; blockedY: boolean; hit: boolean; normal: { x: number; y: number }; } /** What a ray met (#153): the object and the collider (or tilemap), where, the face and how far. */ export interface RayOutcome { object: EntityHandle; collider: EntityHandle; point: { x: number; y: number }; normal: { x: number; y: number }; distance: number; } /** * 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; /** * A machine moved from one state to another – what `On state exited` / `On state entered` are * fired from. Reported rather than fired on the spot: the events belong after the exec flow that * caused them, never in the middle of it (see `BehaviorSystem`). */ stateChanged(machineId: string, from: string, to: string): void; /** Seconds since the world started and since the last tick. */ time: number; dt: number; findByName(name: string): EntityHandle | null; /** Whether the object is still in the game – false once destroyed, or once its world has ended. */ isAlive(target: EntityHandle): boolean; /** The object a reference on `from` names (#258, see `model/objectRefs.ts`); null when there is none. */ findReferenced(from: EntityHandle, ref: string): EntityHandle | null; /** The nearest object called `name` below `parent` – a child before a grandchild; null for none. */ childByName(parent: EntityHandle, 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; its solid colliders stop flush at other solid ones * and the rest of the move goes on along the surface, less what friction takes. */ moveAndSlide(target: EntityHandle, dx: number, dy: number): MoveOutcome; /** * The first solid a ray from (x, y) along (dx, dy) meets within `length`, all in world units; * null for none. `ignore` and whatever is parented below it are not met, and neither is anything * off the layers of `layers` (a 16-bit mask, #158). */ raycast( from: { x: number; y: number }, direction: { x: number; y: number }, length: number, options: { ignore: EntityHandle | null; triggers: boolean; layers: number }, ): RayOutcome | null; /** * Everything that ray meets, nearest first, rather than only the first of them (#157). One entry * per collider and one per tilemap cell; `maxHits` above 0 keeps that many of the nearest. */ raycastAll( from: { x: number; y: number }, direction: { x: number; y: number }, length: number, options: { ignore: EntityHandle | null; triggers: boolean; layers: number; maxHits: number }, ): RayOutcome[]; /** Lets `target` fall through one-way colliders and tiles for `seconds` (#155). */ ignoreOneWay(target: EntityHandle, seconds: number): void; /** Whether `target` is a placed tilemap – what the tile nodes act on (#135). */ isTilemap(target: EntityHandle): boolean; /** * The tile id in a cell of a placed tilemap, `''` for an empty one; null when `target` is no * tilemap, has no layer of that name (`''` = its first) or the cell lies outside its grid. */ tileAt(target: EntityHandle, layer: string, cell: { x: number; y: number }): string | null; /** * Puts a tile into that cell, `''` empties it; false where `tileAt` answers null or the id is no * tile. Only this placement changes, and only for the run (see `model/liveTilemap.ts`). */ setTile(target: EntityHandle, layer: string, cell: { x: number; y: number }, tileId: string): boolean; /** Absent where nothing is simulated: the editor run, and a test double that does not ask for it. */ physics?: PhysicsControl; log(message: string): void; /** * Starts a sound asset, named by its id. `volume` multiplies the asset's own (1 leaves it alone); * `loop` is the caller's answer to whether it repeats, and `undefined` leaves that to the asset, * because the asset says what a sound *is* and the node only colours it. Every call is a voice of * its own: two plays overlap, whether they loop or not, which is what makes one sound effect * fired twice in a row audible twice. * * The voice belongs to the behavior that started it and ends when that behavior is destroyed – * a sound is something a thing in the world is doing. `detached` gives that up on purpose, for * the one case where the rule is in the way: the death sound of an object that destroys itself. */ playSound(assetId: string, volume: number, loop: boolean | undefined, detached: boolean): void; /** Silences every voice of that sound; `''` silences everything this run started. */ stopSound(assetId: string): void; /** * Asks for another scene, by asset id. The swap happens once the frame is over – a graph must be * able to finish the flow it is in, and half a frame in the old world and half in the new one is * the kind of thing nobody can debug. The last request of a frame is the one that happens. */ loadScene(sceneId: 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; /** * Removes `target` with everything below it. With `keepChildren` only the object goes: its * children move up to its parent where they stand, and are answered, nearest level only. */ destroy(target: EntityHandle, keepChildren: boolean): EntityHandle[]; stop(): void; /** Ends the run and starts it again from the saved scene (everything spawned is lost). */ restart(): void; /** The global time scale the run goes at (#167): 1 is real time, 0 frozen. */ timeScale(): number; /** For the rest of the run, the next scene included; Play and Stop set it back to 1. */ setTimeScale(scale: number): void; /** Whether a behavior paused the game – not the editor's own pause, which runs no graph at all. */ isPaused(): boolean; /** From the next frame on, only behaviors that run while paused are ticked. */ setPaused(paused: boolean): void; }