import type { AnimationPlayer, EaseShare } from '../model/animation'; import type { CompositionPlayer } from '../model/composition'; import type { Mat } from '../model/matrix'; import type { Position, PropertyBag } from '../model/Properties'; import type { FxParams } from '../soundfx/nodes'; import type { HeardPosition, Quantize } from '../music/beats'; import type { RecordingSettings } from '../recording/recording'; import type { VideoReading, VideoSteer } from '../model/videoClock'; 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. */ /** * A sound, note or pattern started in time with a playing song or pattern (#338), a stinger: on the * spot `quantize` names of the playing numbered `playback`; 0 for none, which starts it at once. */ export interface MusicSync { playback: number; quantize: Quantize; } 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[]; /** What `Play composition` started on a placed composition (see `model/composition.ts`). */ composition?: CompositionPlayer; } /** 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; /** When it was pressed, in milliseconds of `performance.now()`; null where nobody said (#438). */ pressedAt?: number | null; released: boolean; /** How far the wheel turned this tick, in notches, positive downwards (see `wheelNotches`). */ wheel: number; } /** * 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; /** * How far a hinge has turned (degrees) or a slider has glided (world units) from how it was placed, * and how fast; null for any other joint, for one that holds nothing, and for anything else (#154). */ jointStateOf(target: EntityHandle): { value: number; speed: number } | null; } /** * 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; /** Every object in the game right now, in scene order – what the plural lookups filter (#259). */ objects(): readonly EntityHandle[]; isKeyDown(key: string): boolean; /** Whether the key went down / came up in this frame – the edges `On key down` and `On key up` fire on. */ wasKeyPressed(key: string): boolean; wasKeyReleased(key: string): boolean; pointer: PointerState; /** * The mouse as `camera` sees it (#304): world units through that camera, and whether the mouse is * on its picture. The main camera gives `pointer`; a camera not drawn is never under the mouse. */ pointerThrough(camera: EntityHandle): { x: number; y: number; inside: boolean }; /** * 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[]; /** * The number of the collision layer `target` is on (#211): a collider's or tilemap's own, any * other object's that of its first collider child; null for an object without a collider. */ collisionLayerOf(target: EntityHandle): number | null; /** * The number of the render layer `target` is drawn on (#305): its own choice, else its parent's, * at the top 0; null for an object that is not drawn. */ renderLayerOf(target: EntityHandle): number | null; /** 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; /** * Plays, pauses or seeks the video the sprite `target` wears (#410). A free one gets a player of its * own from then on; a driven one, and the editor run, are left alone and say so in the log. */ steerVideo(target: EntityHandle, steer: VideoSteer): void; /** Where the video the sprite `target` wears stands (#410); null for none. */ videoReading(target: EntityHandle): VideoReading | null; /** * 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. * `fx` names a sound FX asset the voice is played through (#311); `''` the one the sound asset * names (#352), `NO_FX` none. * `fxParams` are values for its parameters; one not given is the default the FX declares. * `tempo` is what a Delay synced to beats counts by (#332). `bus` is the bus it is heard on * (#325): a bus asset or `MASTER_BUS`, `''` for the one the sound asset names. `sync` starts it in * time with a playing (#338). * Returns the number of the voice for `tuneSound`, or 0 where nothing is played. */ playSound( assetId: string, volume: number, loop: boolean | undefined, detached: boolean, fx: string, fxParams: FxParams, tempo: number, bus: string, sync: MusicSync, ): number; /** * Plays one note of an instrument asset (#331): `note` as MIDI counts it (60 is C4), struck with * `velocity` (0 to 1, how loud it is next to `volume`) and released after `duration` seconds, or * held until it is stopped where that is 0. Owned like a `playSound` voice, `instrumentParams` are * values for the instrument's parameters, `tempo` as for `playSound`. `fx` sends the note through a * sound FX of its own (#346), `fxParams` with values for its parameters, apart from the instrument's; * `''` the one the instrument names, `NO_FX` none (#352). * `bus` as for `playSound`, `''` for the one the instrument names, and `sync` too. * Returns the number of the voice, 0 where nothing plays. */ playNote( instrumentId: string, note: number, velocity: number, volume: number, duration: number, detached: boolean, instrumentParams: FxParams, tempo: number, fx: string, fxParams: FxParams, bus: string, sync: MusicSync, ): number; /** * Plays a pattern asset through its instrument, at its own tempo, once or round and round, every * note at `volume`. Owned like a `playSound` voice. `fx` is a sound FX all of it goes through as * one chain (#346), `''` the one the pattern names, `NO_FX` none (#352); `fxParams` values for its * parameters, `bus` as for `playSound`. Synced to a playing (#338), it starts on its spot and plays * at its tempo. Returns its number, for `stopVoice` and `tuneSound`; 0 where there is nothing to play. */ playPattern( patternId: string, volume: number, loop: boolean, detached: boolean, fx: string, fxParams: FxParams, bus: string, sync: MusicSync, ): number; /** Stops every playing of that pattern, `''` every pattern; `letRing` lets its notes fall away in their release. */ stopPattern(patternId: string, letRing: boolean): void; /** * Plays a song asset (#336) at its tempo, its tracks as they are set: from its start, then round * the loop of its ruler with `loop`, else once to its end. `volume` (0 to 1) is the level of the * whole of it, rising there from silence over `fadeIn` seconds. Owned like a `playSound` voice. * `fx`, `fxParams` and `bus` as for `playPattern`, after its tracks. With `live` it plays its notes * even where it has a bake, so its tracks can be turned (#338). Returns its number; 0 where there is * nothing to play. */ playSong( songId: string, volume: number, fadeIn: number, loop: boolean, detached: boolean, fx: string, fxParams: FxParams, bus: string, live: boolean, ): number; /** * Turns one track of a playing song (#338) to `volumeDb` on top of its own level, over `fade` * seconds from where `quantize` lands; a playing of its bake has no tracks, which is logged. */ setSongTrack(playback: number, track: string, volumeDb: number, fade: number, quantize: Quantize): void; /** * Holds the lane `lane` of one track of a playing song (#338) on `value`, in the lane's units, * reached over `fade` seconds from where `quantize` lands; its points are not heard until it is * let go. A track without such a lane, or a playing of its bake, is logged. */ setSongLane( playback: number, track: string, lane: string, value: number, fade: number, quantize: Quantize, ): void; /** Lets such a held lane go (#338): back to its curve over `fade` seconds, as soon as can be heard. */ releaseSongLane(playback: number, track: string, lane: string, fade: number): void; /** * Plays `songId` in place of a playing song (#338), `''` the same one, from the marker `marker` * (`''` its start), where `quantize` lands: over `fade` seconds, else in a cut. It keeps its number. */ switchSong(playback: number, songId: string, marker: string, quantize: Quantize, fade: number): void; /** Stops every playing of that song, `''` every song, falling silent over `fadeOut` seconds. */ stopSong(songId: string, fadeOut: number): void; /** Stops one playing of a song or pattern by its number, falling silent over `fadeOut` seconds. */ stopPlayback(playback: number, fadeOut: number): void; /** * How many seconds one run of that sound asset lasts, so a change can span it; 0 where it does not * exist or has not been loaded yet. */ soundLength(assetId: string): number; /** How many seconds that voice rings on after its sound ends: its echoes (#324); 0 for none. */ soundTail(voice: number): number; /** * Silences every voice of that sound; `''` silences everything this run started. `letRing` stops * the sound and lets its echoes fade (#324). */ stopSound(assetId: string, letRing: boolean): void; /** Silences one voice, by the number `playSound` gave it. */ stopVoice(voice: number, letRing: boolean): void; /** * Hands new values to the sound FX parameters of one voice, by the number `playSound` gave it * (#315), so a running sound follows the game. A voice that has ended is not reached. `time` is * how many seconds the values take to get there, on the audio clock and along `ease`; 0 is a * quick glide. */ tuneSound(voice: number, fxParams: FxParams, time: number, ease: EaseShare): void; /** * The same for the sound FX of a bus (#325): a bus asset or `MASTER_BUS`. A bus that is not there * is not reached. */ tuneBus(bus: string, fxParams: FxParams, time: number, ease: EaseShare): void; /** * Sets the volume of a bus (#325), `''` the master, over `time` seconds along `ease`. It holds * for the rest of the run, over the volume of its asset and across scenes. */ setBusVolume(bus: string, volume: number, time: number, ease: EaseShare): void; /** The volume a bus is at, `''` the master: what a game set, else that of its asset; 0 for none. */ busVolume(bus: string): number; /** * Where a playing song or pattern stands as it is heard (#338): the one numbered `playback`, else * the one started last of `song` or `pattern`, of every one where both are `''`; null for none. */ songPosition(playback: number | null, song: string, pattern: string): HeardPosition | null; /** * Whether the voice or playing numbered `id` is still to be heard (#373): waiting on its files, * playing, fading or ringing on; false for 0. */ playing(id: number): boolean; /** * 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` at the end of this tick, before the frame is drawn. */ spawn( assetId: string, position: { x: number; y: number; z: number }, parent: EntityHandle | null, ): EntityHandle | null; /** * Places the joint asset on `owner` at `at` of that object and connects it to `connected`, the * scene when null (#154). Null when the asset is no joint or the object is gone. */ addJoint( assetId: string, owner: EntityHandle, connected: EntityHandle | null, at: { x: number; y: number }, ): 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; /** Starts recording the game view (#405); where it cannot, the log says why. */ startRecording(settings: RecordingSettings): void; /** Ends the recording and saves it; without one, only the log hears of it. */ stopRecording(): void; /** Pauses the editor's Play after this frame, as its pause button does; elsewhere nothing. */ breakpoint(): void; }