Files
feuerwerk/src/core/graph/NodeContext.ts
T
root 2cf788eedd Let a value be a bundle of named fields
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.
2026-09-10 08:14:18 +02:00

99 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}