Spawn node: place assets at runtime into the play copy, Spawner demo behavior

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-07 16:57:19 +02:00
1 parent c3a533c4e5
commit 91ee2feadd
13 files changed
+219 -37

No files matched your search

+40 -1
View File
@@ -4,7 +4,7 @@ import { CommandStack } from '../../src/core/commands/CommandStack';
import { AddNodeCommand, ConnectCommand, DisconnectCommand, RemoveNodeCommand, SetNodeParamCommand } from '../../src/core/commands/graphCommands';
import { PlaceAssetCommand, SetParentCommand } from '../../src/core/commands/sceneObjectCommands';
import { AddVariableCommand, RemoveVariableCommand, RenameVariableCommand } from '../../src/core/commands/variableCommands';
import { type EntityHandle, GraphInterpreter, type NodeContext, allNodeDefs, portsCompatible } from '../../src/core/graph';
import { type EntityHandle, type Graph, GraphInterpreter, type NodeContext, allNodeDefs, portsCompatible } from '../../src/core/graph';
import type { BehaviorAsset } from '../../src/core/model/Asset';
import { createEmptyProject } from '../../src/core/model/Project';
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
@@ -32,6 +32,7 @@ function context(overrides: Partial<NodeContext> = {}): NodeContext & { logs: st
return { blockedX: false, blockedY: false };
},
log: (m: string) => logs.push(m),
spawn: () => null,
destroy: () => {},
stop: () => { ctx.stopped = true; },
...overrides,
@@ -382,3 +383,41 @@ describe('behavior serialization', () => {
expect(cleaned.graph.edges.length).toBe(behavior.graph.edges.length);
});
});
describe('Spawn node', () => {
const graph: Graph = {
nodes: [
{ id: 'start', type: 'OnStart', x: 0, y: 0, params: {} },
{ id: 'spawn', type: 'Spawn', x: 0, y: 0, params: { asset: 'crate', x: 5, y: 7 } },
{ id: 'log', type: 'Log', x: 0, y: 0, params: {} },
],
edges: [
{ id: 'e1', from: { node: 'start', port: 'exec' }, to: { node: 'spawn', port: 'exec' } },
{ id: 'e2', from: { node: 'spawn', port: 'exec' }, to: { node: 'log', port: 'exec' } },
{ id: 'e3', from: { node: 'spawn', port: 'object' }, to: { node: 'log', port: 'message' } },
],
};
it('asks the context for a new instance at the inline position and outputs it', () => {
const calls: unknown[] = [];
const ctx = context({
spawn: (assetId, position, parent) => {
calls.push({ assetId, position, parent });
return entity('Crate', position.x);
},
});
new GraphInterpreter(graph).fire('OnStart', ctx);
expect(calls).toEqual([{ assetId: 'crate', position: { x: 5, y: 7, z: 0 }, parent: null }]);
expect(ctx.logs).toEqual(['Crate']); // the spawned object flowed into Log
});
it('logs a hint and continues when no asset is selected or the asset is unknown', () => {
const ctx = context({ spawn: () => null });
const noAsset: Graph = { ...graph, nodes: graph.nodes.map((n) => (n.id === 'spawn' ? { ...n, params: {} } : n)) };
new GraphInterpreter(noAsset).fire('OnStart', ctx);
expect(ctx.logs).toEqual(['Spawn: no asset selected', '']);
const ctx2 = context({ spawn: () => null });
new GraphInterpreter(graph).fire('OnStart', ctx2);
expect(ctx2.logs[0]).toBe('Spawn: asset crate not found');
});
});