Make a call node say why it does nothing, and easier to place

A behavior node that cannot run looked exactly like a healthy one: no
start node, no behavior picked, a deleted behavior or a self-call were
all silently skipped. The call still had to be forgiving at runtime -- a
dangling reference must not abort a game -- so the fix is diagnosis, not
an exception: describeProblem() names the one reason, the node draws it
in red under its title, and the inspector repeats it where you pick the
behavior. A missing end node is deliberately not a problem: a call is
always synchronous, and the end node only decides which values come back.

Two ways in: a behavior dragged from the asset panel lands in the graph
under the pointer, and double-clicking a call node opens the graph it
runs. Both reuse machinery that already existed -- the asset panel
already starts the drag, and list views already report double clicks.

The e2e module needed its assets at the head of the asset list: after 18
modules the list is long enough that a newly appended row scrolls out of
view, and the press then lands on the panel behind it, starting no drag
at all. That looked exactly like a rejected drop and cost two full runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-09 20:40:56 +02:00
1 parent 1d8e6ebea0
commit c6f907b4e0
12 files changed
+427 -24

No files matched your search

+20 -3
View File
@@ -69,12 +69,29 @@ export function resolveDef(node: GraphNode, lookup: BehaviorLookup): NodeDef | u
}
/**
* Everything that changes a node's ports, as one string. Views and the inspector rebuild when it
* Why this call cannot do anything, or null if it can. A call is the one node that depends on a
* *different* graph, so it is the one node that can look perfectly healthy and still be dead.
*/
export function callProblem(node: GraphNode, lookup: BehaviorLookup, ownerId = ''): string | null {
if (node.type !== RUN_BEHAVIOR) return null;
const id = String(node.params[BEHAVIOR_PARAM] ?? '');
if (!id) return 'No behavior picked';
if (id === ownerId) return 'A behavior cannot run itself';
const program = lookup(id);
if (!program) return 'That behavior is gone';
if (!terminalNode(program.graph, BEHAVIOR_START)) return 'No start node – does nothing';
return null;
}
/**
* Everything that changes how a node looks, as one string. Views and the inspector rebuild when it
* changes – a port list that is derived rather than fixed has no other way of being noticed.
*/
export function portSignature(node: GraphNode, lookup: BehaviorLookup): string {
export function portSignature(node: GraphNode, lookup: BehaviorLookup, ownerId = ''): string {
const def = resolveDef(node, lookup);
if (!def) return 'missing';
const list = (ports: PortDef[]) => ports.map((p) => `${p.key}:${p.type}`).join(',');
return `${node.type}|${def.label}|${list(def.inputs)}|${list(def.outputs)}`;
return `${node.type}|${def.label}|${list(def.inputs)}|${list(def.outputs)}|${
callProblem(node, lookup, ownerId) ?? ''
}`;
}