PropertyValue was number | string | boolean | list. A struct joins it, which costs two things the typechecker found rather than I did. An object on a data edge used to mean a live entity, so asEntity would have taken a struct for one and asString would have read .name off it. EntityHandle now carries a symbol brand – a symbol so no field name can impersonate it, and it never serializes, which suits a thing that exists only while the game runs. It is built in exactly one place. String(value) would have written a struct as [object Object] at 39 call sites. They go through textOf(), which is identical for primitives. normalizeGraph dropped everything but primitives from node params, so the List node lost its value on every save ([3,1,4] round-tripped to undefined). Variables were spared: they have a schema, params have none.
99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
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;
|
||
}
|