176 lines
7.3 KiB
TypeScript
176 lines
7.3 KiB
TypeScript
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<string, PropertyBag>;
|
||
states: Record<string, string>;
|
||
}
|
||
|
||
/**
|
||
* 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<string, string>;
|
||
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;
|
||
}
|