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.
126 lines
4.2 KiB
TypeScript
126 lines
4.2 KiB
TypeScript
import type { PropertyBag, PropertyValue } from '../model/Properties';
|
||
import type { PropertyDef } from '../properties/PropertySchema';
|
||
import type { PortType } from './Graph';
|
||
import type { NodeContext } from './NodeContext';
|
||
import { asBoolean, asNumber, asString, type Value } from './values';
|
||
|
||
export interface PortDef {
|
||
key: string;
|
||
label: string;
|
||
type: PortType;
|
||
/** Inline value used while the input is unconnected and the node has no own value (default 0/''/false). */
|
||
default?: PropertyValue;
|
||
}
|
||
|
||
export type NodeCategory = 'event' | 'flow' | 'data' | 'variable' | 'action';
|
||
|
||
/**
|
||
* How a loop node asks for one of its exec outputs to be run repeatedly: `port` carries the body,
|
||
* `index` is the number output seeded with the round number.
|
||
*/
|
||
export interface RepeatResult {
|
||
port: string;
|
||
index: string;
|
||
times: number;
|
||
}
|
||
|
||
/** How a node asks for another behavior's graph to be run in place, `inputs` seeding its start node. */
|
||
export interface CallResult {
|
||
behaviorId: string;
|
||
inputs: Record<string, Value>;
|
||
}
|
||
|
||
export interface RunResult {
|
||
/** Cached for the rest of the pulse. */
|
||
outputs?: Record<string, Value>;
|
||
/** Exec output(s) to continue along; omitted = the node ends the flow. */
|
||
next?: string | string[];
|
||
/** Runs `port` that many times before continuing along `next`. */
|
||
repeat?: RepeatResult;
|
||
/** Runs another behavior before continuing along `next`; its results join `outputs`. */
|
||
call?: CallResult;
|
||
}
|
||
|
||
/** Palette, port rendering, connection validation and inspector fields are all derived from this. */
|
||
export interface NodeDef {
|
||
type: string;
|
||
label: string;
|
||
category: NodeCategory;
|
||
description: string;
|
||
inputs: PortDef[];
|
||
outputs: PortDef[];
|
||
/** Node settings that are not ports (key name, operator, variable). */
|
||
params: PropertyDef[];
|
||
/**
|
||
* Settings that say nothing at this value set, so the node body leaves them off; the inspector
|
||
* still offers them. Without it a switched-off repeat is two rows of "0" on every key node.
|
||
*/
|
||
hiddenParams?(params: PropertyBag): string[];
|
||
/**
|
||
* Pure data nodes are evaluated on demand when a downstream input needs their value; exec nodes
|
||
* run when the flow reaches them. Event nodes have no `run` – the interpreter seeds their outputs.
|
||
*/
|
||
run?(ctx: NodeContext, inputs: Record<string, Value>, params: PropertyBag): RunResult;
|
||
}
|
||
|
||
const defs = new Map<string, NodeDef>();
|
||
|
||
export function registerNode(def: NodeDef): NodeDef {
|
||
if (defs.has(def.type)) throw new Error(`Node type "${def.type}" registered twice`);
|
||
defs.set(def.type, def);
|
||
return def;
|
||
}
|
||
|
||
export function nodeDef(type: string): NodeDef | undefined {
|
||
return defs.get(type);
|
||
}
|
||
|
||
export function requireNodeDef(type: string): NodeDef {
|
||
const def = defs.get(type);
|
||
if (!def) throw new Error(`Unknown node type "${type}"`);
|
||
return def;
|
||
}
|
||
|
||
export function allNodeDefs(): NodeDef[] {
|
||
return [...defs.values()];
|
||
}
|
||
|
||
export function isEventNode(def: NodeDef): boolean {
|
||
return def.category === 'event';
|
||
}
|
||
|
||
export function hasExecInput(def: NodeDef): boolean {
|
||
return def.inputs.some((p) => p.type === 'exec');
|
||
}
|
||
|
||
/**
|
||
* Data inputs that may carry an inline value while unconnected. An object is a live handle and a
|
||
* struct is assembled from its parts, so neither has a value that could be typed on one line.
|
||
*/
|
||
export function inlineInputs(def: NodeDef): PortDef[] {
|
||
return def.inputs.filter((p) => p.type !== 'exec' && p.type !== 'object' && p.type !== 'struct');
|
||
}
|
||
|
||
export function inlineInputDef(port: PortDef): PropertyDef {
|
||
switch (port.type) {
|
||
case 'boolean':
|
||
return { key: port.key, label: port.label, kind: 'boolean', default: asBoolean(port.default ?? false) };
|
||
case 'string':
|
||
case 'any':
|
||
return { key: port.key, label: port.label, kind: 'text', default: asString(port.default ?? '') };
|
||
default:
|
||
return {
|
||
key: port.key,
|
||
label: port.label,
|
||
kind: 'number',
|
||
default: asNumber(port.default ?? 0),
|
||
step: 1,
|
||
};
|
||
}
|
||
}
|
||
|
||
/** The value an unconnected input carries: the node's own inline value, else the port default. */
|
||
export function inlineValue(node: { params: PropertyBag }, port: PortDef): Value {
|
||
return node.params[port.key] ?? port.default ?? null;
|
||
}
|