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; } /** * 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; 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 }; /** 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; destroy(target: EntityHandle): void; stop(): void; /** Ends the run and starts it again from the saved scene (everything spawned is lost). */ restart(): void; }