Edit the graph during play and watch values and flow run through it

The graph was only locked by five edit-mode guards in GraphCanvas. The
running game reads the graph by reference each frame, so a new node, edge,
changed value, disconnect or undo takes effect from the next frame. Stop
discards these edits like everything else changed in play. A new On start
node does not fire for a behavior that has already started.

During play every data output shows the value it last produced, in the
highlight colour and refreshed ten times a second. Values are kept per
graph, because node ids are only unique within one; with several
instances the most recent value wins. The Variables node reads the live
variable bag, so only its own outputs are kept, as a snapshot.

Each time an exec edge fires, a bead runs along it in 0.3 s, at most one
every 0.15 s per edge, following the same curve the edge is drawn with.
Beads run on game time and freeze while paused; the game logic is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-11 21:19:07 +02:00
1 parent 495756b500
commit 656d61f187
17 files changed
+542 -37

No files matched your search

+26 -4
View File
@@ -1,11 +1,13 @@
import {
asNumber,
type EntityHandle,
type Graph,
type GraphNode,
GraphInterpreter,
isHandleEnabled,
type NodeContext,
type TraceListener,
type Value,
} from '../../core/graph';
import { stepKeyRepeat } from '../../core/graph/keyRepeat';
import { behaviorLookup } from '../../core/model/behaviorPrograms';
@@ -24,12 +26,19 @@ export const TRACE_FADE = 0.35;
export interface Trace {
edges: Map<string, number>;
nodes: Map<string, number>;
values: Map<string, Record<string, Value>>;
}
interface Produced {
time: number;
values: Record<string, Value>;
}
interface Instance {
entity: Entity;
interpreter: GraphInterpreter;
trace: Trace;
trace: Omit<Trace, 'values'>;
values: Map<Graph, Map<string, Produced>>;
}
/**
@@ -100,13 +109,19 @@ export class BehaviorSystem implements System {
continue;
const asset = findAssetOf(this.project, entity.assetId, 'behavior');
if (!asset) continue;
const trace: Trace = { edges: new Map(), nodes: new Map() };
const trace = { edges: new Map<string, number>(), nodes: new Map<string, number>() };
const values = new Map<Graph, Map<string, Produced>>();
const interpreter = new GraphInterpreter(asset.graph, behaviorLookup(this.project));
interpreter.trace = {
onEdge: (id) => trace.edges.set(id, world.time),
onNode: (id) => trace.nodes.set(id, world.time),
onValues: (graph, id, produced) => {
let byNode = values.get(graph);
if (!byNode) values.set(graph, (byNode = new Map<string, Produced>()));
byNode.set(id, { time: world.time, values: produced });
},
} satisfies TraceListener;
const instance = { entity, interpreter, trace };
const instance = { entity, interpreter, trace, values };
this.instances.push(instance);
this.run(instance, (i, ctx) => i.fire('OnStart', ctx), 0);
}
@@ -203,15 +218,22 @@ export class BehaviorSystem implements System {
* those that ran it as a node, which is the only way a nested behavior's graph ever lights up.
*/
traceFor(assetId: string): Trace {
const merged: Trace = { edges: new Map(), nodes: new Map() };
const merged: Trace = { edges: new Map(), nodes: new Map(), values: new Map() };
const latest = (into: Map<string, number>, from: Map<string, number>) => {
for (const [id, t] of from) into.set(id, Math.max(t, into.get(id) ?? -Infinity));
};
const graph = findAssetOf(this.project, assetId, 'behavior')?.graph;
const newest = new Map<string, Produced>();
for (const instance of this.instances) {
if (instance.entity.assetId !== assetId && !instance.interpreter.calledBehaviors.has(assetId)) continue;
latest(merged.edges, instance.trace.edges);
latest(merged.nodes, instance.trace.nodes);
for (const [id, produced] of (graph && instance.values.get(graph)) ?? []) {
const known = newest.get(id);
if (!known || produced.time >= known.time) newest.set(id, produced);
}
}
for (const [id, produced] of newest) merged.values.set(id, produced.values);
return merged;
}