Files
feuerwerk/src/core/graph/NodeRegistry.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

126 lines
4.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, 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;
}