A behavior run by another one had a single flow port at its Start and at its End, so it could not branch for its caller. And it could not be used as an event: a call needs a pulse at its flow input, and the events of a called graph never fire. An input like "Confirm" (Space or Enter) had to be built again in every graph. - A declared port of a Start or an End node can be of the kind Flow. It sits above the values and shares their names, exec stays reserved. Only these two nodes declare one, a struct does not. - Run behavior shows a flow port of the Start as a further flow input. A call arriving through it leaves the Start through the output of that name and not through exec. - Run behavior shows a flow port of the End as a further flow output. The call goes on through the one the End was reached by last, and then through exec. exec fires always, as before. - On behavior is a new event node without a flow input. The behavior it names runs at the node for as long as the behavior around it runs, with all of its own events and one set of variables per node. - Its outputs are those of the End. It fires once the inner pulse is through, by the output the End was reached by last, exec only for End.exec. A pulse that never reaches the End is not seen. - The value ports of the Start are its inputs, read anew for each inner pulse. The Start itself never fires there. - It nests to any depth. Under a Run behavior it stays silent. A behavior hearing itself, however far round, is not started and the node says "A behavior cannot listen to itself". - The palette offers every other behavior with an End once more as "On <name>". A node naming one without End says so. - An On node removed while the game runs ends its behavior with On disable and On destroy. The sounds of the entity end only with the behavior placed on it. - Renaming and deleting a declared port follows the edges at On behavior nodes as it does at Run behavior nodes. - The flow outputs of the Start of a behavior placed on an object never fire, like its exec. - The engine prompt of the benchmark names flow ports and On behavior. Tests: - Gate on melektaus-atx right before this commit: 215 files, 3319 tests (3278 + 41), exit 0. - e2e 19 on melektaus-atx: 39 checks (+4), all pass. e2e 1-6,17-24,26,36,51: 628 checks, all pass. - 19 unit tests in flow-ports.test.ts, 19 in on-behavior.test.ts on a real world, the rest in the node catalog. - Four new e2e checks in module 19: a flow port declared by mouse at the End, On Confirm in the graph, the call leaving through the further output while playing, Enter and Space on a real keyboard. - 19 unit counter-probes all fail their tests. One did not at first (a removed node kept its instance), a test was added. Two browser counter-probes fail their checks. - One screenshot looked at: On Confirm with exec and pressed, the second flow pin at the calling node. - The e2e sum of 1680 is counted (1676 + 4), the full suite and the smokes did not run. - Not seen in the browser: flow inputs at the Start, nesting, the ring, values through On behavior, the palette entry. No test holds that the sounds of the entity go on when a heard behavior ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
476 lines
19 KiB
TypeScript
476 lines
19 KiB
TypeScript
/**
|
||
* `On behavior` (#294): another behavior used as an event. It lives at the node for as long as the
|
||
* behavior around it runs, its own events fire, and each of its pulses that reaches its end node
|
||
* goes on at the node hearing it. Driven through a real world, since the running game keeps it.
|
||
*/
|
||
import { describe, expect, it } from 'vitest';
|
||
import { AddNodeCommand, RemoveNodeCommand } from '../../src/core/commands/graphCommands';
|
||
import { AddPortCommand, RemovePortCommand, RenamePortCommand } from '../../src/core/commands/portCommands';
|
||
import {
|
||
BEHAVIOR_END,
|
||
BEHAVIOR_PARAM,
|
||
BEHAVIOR_START,
|
||
callProblem,
|
||
type GraphNode,
|
||
ON_BEHAVIOR,
|
||
resolveDef,
|
||
RUN_BEHAVIOR,
|
||
} from '../../src/core/graph';
|
||
import type { BehaviorAsset } from '../../src/core/model/Asset';
|
||
import { graphLookup } from '../../src/core/model/graphLookup';
|
||
import {
|
||
graphEditor,
|
||
makeBehavior,
|
||
makeSpriteScene,
|
||
place,
|
||
type ProjectFixture,
|
||
type SceneFixture,
|
||
startWorld,
|
||
} from '../helpers';
|
||
|
||
/** "Confirm": Space or Enter going down reaches the end node by its flow port `pressed`. */
|
||
function confirm(f: ProjectFixture, name = 'Confirm'): { asset: BehaviorAsset; end: GraphNode } {
|
||
const asset = makeBehavior(f, name);
|
||
const edit = graphEditor(f, asset);
|
||
const end = edit.addNode(BEHAVIOR_END);
|
||
f.stack.execute(new AddPortCommand(asset.id, end.id, { key: 'pressed', kind: 'flow' }));
|
||
for (const key of [' ', 'Enter']) edit.connect(edit.addNode('OnKeyDown', { key }), 'exec', end, 'pressed');
|
||
return { asset, end };
|
||
}
|
||
|
||
/** A behavior on a box hearing `heard`, with a Log on each named output of the node. */
|
||
function hearing(
|
||
f: SceneFixture,
|
||
heard: BehaviorAsset,
|
||
outputs: readonly string[],
|
||
name = 'Player',
|
||
): { asset: BehaviorAsset; node: GraphNode; objectId: string } {
|
||
const asset = makeBehavior(f, name);
|
||
const edit = graphEditor(f, asset);
|
||
const node = edit.addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: heard.id });
|
||
for (const port of outputs)
|
||
edit.connect(node, port, edit.addNode('Log', { message: `${name} ${port}` }), 'exec');
|
||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||
return { asset, node, objectId: place(f, asset.id, { x: 0, y: 0 }, box) };
|
||
}
|
||
|
||
describe('On behavior', () => {
|
||
it('fires when an event of the behavior it hears reaches that behavior’s end node', () => {
|
||
const f = makeSpriteScene();
|
||
hearing(f, confirm(f).asset, ['pressed', 'exec']);
|
||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||
|
||
frame();
|
||
expect(world.log).toEqual([]);
|
||
|
||
input.press('Enter');
|
||
frame();
|
||
expect(world.log).toEqual(['Player pressed']);
|
||
|
||
input.release('Enter');
|
||
input.press(' ');
|
||
frame();
|
||
expect(world.log).toEqual(['Player pressed', 'Player pressed']);
|
||
});
|
||
|
||
it('stays silent for a pulse that does not reach the end node', () => {
|
||
const f = makeSpriteScene();
|
||
const { asset } = confirm(f);
|
||
const edit = graphEditor(f, asset);
|
||
edit.connect(edit.addNode('OnUpdate'), 'exec', edit.addNode('Log', { message: 'inner update' }), 'exec');
|
||
hearing(f, asset, ['pressed', 'exec']);
|
||
const { world, frame } = startWorld(f, { behaviors: true });
|
||
|
||
frame();
|
||
frame();
|
||
expect(world.log).toEqual(['inner update', 'inner update']);
|
||
});
|
||
|
||
it('fires `exec` only when the end node was reached by its plain input', () => {
|
||
const f = makeSpriteScene();
|
||
const { asset, end } = confirm(f);
|
||
const edit = graphEditor(f, asset);
|
||
edit.connect(edit.addNode('OnKeyDown', { key: 'x' }), 'exec', end, 'exec');
|
||
hearing(f, asset, ['pressed', 'exec']);
|
||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||
|
||
input.press('x');
|
||
frame();
|
||
expect(world.log).toEqual(['Player exec']);
|
||
|
||
input.press('Enter');
|
||
frame();
|
||
expect(world.log).toEqual(['Player exec', 'Player pressed']);
|
||
});
|
||
|
||
it('fires once, by the way reached last, when one pulse reaches the end node twice', () => {
|
||
const f = makeSpriteScene();
|
||
const asset = makeBehavior(f, 'Twice');
|
||
const edit = graphEditor(f, asset);
|
||
const end = edit.addNode(BEHAVIOR_END);
|
||
for (const key of ['first', 'second'])
|
||
f.stack.execute(new AddPortCommand(asset.id, end.id, { key, kind: 'flow' }));
|
||
const both = edit.addNode('Sequence');
|
||
edit.wire([
|
||
[edit.addNode('OnKeyDown', { key: 'a' }), 'exec', both, 'exec'],
|
||
[both, 'a', end, 'first'],
|
||
[both, 'b', end, 'second'],
|
||
]);
|
||
hearing(f, asset, ['first', 'second', 'exec']);
|
||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||
|
||
input.press('a');
|
||
frame();
|
||
expect(world.log).toEqual(['Player second']);
|
||
});
|
||
|
||
it('has the outputs of the end node and the value inputs of the start node, under the behavior’s name', () => {
|
||
const f = makeSpriteScene();
|
||
const { asset, end } = confirm(f);
|
||
const start = graphEditor(f, asset).addNode(BEHAVIOR_START);
|
||
f.stack.execute(new AddPortCommand(asset.id, start.id, { key: 'way', kind: 'flow' }));
|
||
f.stack.execute(new AddPortCommand(asset.id, start.id, { key: 'amount', kind: 'number' }));
|
||
f.stack.execute(new AddPortCommand(asset.id, end.id, { key: 'score', kind: 'number' }));
|
||
const { asset: outer, node } = hearing(f, asset, []);
|
||
const def = resolveDef(node, outer.graph, graphLookup(f.project))!;
|
||
expect(def.label).toBe('On Confirm');
|
||
expect(def.category).toBe('event');
|
||
expect(def.inputs.map((p) => `${p.key}:${p.type}`)).toEqual(['amount:number']);
|
||
expect(def.outputs.map((p) => `${p.key}:${p.type}`)).toEqual([
|
||
'exec:exec',
|
||
'pressed:exec',
|
||
'score:number',
|
||
]);
|
||
});
|
||
|
||
it('hands its inputs to the start node, read anew for every pulse, and the end node’s values out', () => {
|
||
const f = makeSpriteScene();
|
||
const asset = makeBehavior(f, 'Doubled');
|
||
const edit = graphEditor(f, asset);
|
||
const start = edit.addNode(BEHAVIOR_START);
|
||
const end = edit.addNode(BEHAVIOR_END);
|
||
f.stack.execute(new AddPortCommand(asset.id, start.id, { key: 'amount', kind: 'number' }));
|
||
f.stack.execute(new AddPortCommand(asset.id, end.id, { key: 'twice', kind: 'number' }));
|
||
const times = edit.addNode('Multiply', { b: 2 });
|
||
edit.wire([
|
||
[edit.addNode('OnKeyDown', { key: 'a' }), 'exec', end, 'exec'],
|
||
[start, 'amount', times, 'a'],
|
||
[times, 'result', end, 'twice'],
|
||
]);
|
||
|
||
const outer = makeBehavior(f, 'Player', ['count']);
|
||
const oEdit = graphEditor(f, outer);
|
||
const node = oEdit.addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: asset.id });
|
||
const log = oEdit.addNode('Log');
|
||
const plus = oEdit.addNode('Add', { b: 1 });
|
||
const set = oEdit.variables(['count'], { write: true });
|
||
const get = oEdit.variables(['count']);
|
||
oEdit.wire([
|
||
[get, 'count', node, 'amount'],
|
||
[node, 'exec', log, 'exec'],
|
||
[node, 'twice', log, 'message'],
|
||
[log, 'exec', set, 'exec'],
|
||
[get, 'count', plus, 'a'],
|
||
[plus, 'result', set, 'count'],
|
||
]);
|
||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||
place(f, outer.id, { x: 0, y: 0 }, box);
|
||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||
|
||
for (let i = 0; i < 3; i++) {
|
||
input.press('a');
|
||
frame();
|
||
input.release('a');
|
||
frame();
|
||
}
|
||
expect(world.log).toEqual(['0', '2', '4']);
|
||
});
|
||
|
||
it('keeps the variables of the behavior it hears alive, one set per node', () => {
|
||
const f = makeSpriteScene();
|
||
const asset = makeBehavior(f, 'Counter', ['count']);
|
||
const edit = graphEditor(f, asset);
|
||
const end = edit.addNode(BEHAVIOR_END);
|
||
f.stack.execute(new AddPortCommand(asset.id, end.id, { key: 'count', kind: 'number' }));
|
||
const plus = edit.addNode('Add', { b: 1 });
|
||
const set = edit.variables(['count'], { write: true });
|
||
const get = edit.variables(['count']);
|
||
edit.wire([
|
||
[edit.addNode('OnKeyDown', { key: 'a' }), 'exec', set, 'exec'],
|
||
[get, 'count', plus, 'a'],
|
||
[plus, 'result', set, 'count'],
|
||
[set, 'exec', end, 'exec'],
|
||
[get, 'count', end, 'count'],
|
||
]);
|
||
// Not kept from call to call: what holds the count here is the node it lives at.
|
||
asset.keptVariables = [];
|
||
|
||
const outer = makeBehavior(f, 'Player', ['count']);
|
||
const oEdit = graphEditor(f, outer);
|
||
for (const name of ['one', 'two']) {
|
||
const node = oEdit.addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: asset.id });
|
||
const say = oEdit.addNode('Join', { a: `${name} ` });
|
||
const log = oEdit.addNode('Log');
|
||
oEdit.wire([
|
||
[node, 'exec', log, 'exec'],
|
||
[node, 'count', say, 'b'],
|
||
[say, 'text', log, 'message'],
|
||
]);
|
||
}
|
||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||
const objectId = place(f, outer.id, { x: 0, y: 0 }, box);
|
||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||
|
||
for (let i = 0; i < 2; i++) {
|
||
input.press('a');
|
||
frame();
|
||
input.release('a');
|
||
frame();
|
||
}
|
||
expect(world.log).toEqual(['one 1', 'two 1', 'one 2', 'two 2']);
|
||
// The outer behavior's own variable of the same name is another one.
|
||
expect(world.entities.find((e) => e.objectId === objectId)?.props.count).toBe(0);
|
||
});
|
||
|
||
it('runs the lifecycle events of the behavior it hears in step with the one around it', () => {
|
||
const f = makeSpriteScene();
|
||
const asset = makeBehavior(f, 'Life');
|
||
const edit = graphEditor(f, asset);
|
||
const end = edit.addNode(BEHAVIOR_END);
|
||
for (const type of ['OnStart', 'OnEnable', 'OnDisable', 'OnDestroy']) {
|
||
f.stack.execute(new AddPortCommand(asset.id, end.id, { key: type, kind: 'flow' }));
|
||
edit.connect(edit.addNode(type), 'exec', end, type);
|
||
}
|
||
const { asset: outer } = hearing(f, asset, ['OnStart', 'OnEnable', 'OnDisable', 'OnDestroy']);
|
||
const oEdit = graphEditor(f, outer);
|
||
oEdit.connect(oEdit.addNode('OnStart'), 'exec', oEdit.addNode('Log', { message: 'own start' }), 'exec');
|
||
const { world, frame } = startWorld(f, { behaviors: true });
|
||
|
||
frame();
|
||
expect(world.log).toEqual(['own start', 'Player OnStart', 'Player OnEnable']);
|
||
world.destroy();
|
||
expect(world.log.slice(3)).toEqual(['Player OnDisable', 'Player OnDestroy']);
|
||
});
|
||
|
||
it('hears a timer of that behavior', () => {
|
||
const f = makeSpriteScene();
|
||
const asset = makeBehavior(f, 'Tick');
|
||
const edit = graphEditor(f, asset);
|
||
edit.connect(edit.addNode('OnTimer', { interval: 0.5 }), 'exec', edit.addNode(BEHAVIOR_END), 'exec');
|
||
hearing(f, asset, ['exec']);
|
||
const { world, frame } = startWorld(f, { behaviors: true });
|
||
|
||
frame(0.3);
|
||
expect(world.log).toEqual([]);
|
||
frame(0.3);
|
||
expect(world.log).toEqual(['Player exec']);
|
||
});
|
||
|
||
it('goes as deep as behaviors hear each other', () => {
|
||
const f = makeSpriteScene();
|
||
const inner = confirm(f).asset;
|
||
const middle = makeBehavior(f, 'Menu');
|
||
const mEdit = graphEditor(f, middle);
|
||
const end = mEdit.addNode(BEHAVIOR_END);
|
||
f.stack.execute(new AddPortCommand(middle.id, end.id, { key: 'chosen', kind: 'flow' }));
|
||
mEdit.connect(mEdit.addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: inner.id }), 'pressed', end, 'chosen');
|
||
hearing(f, middle, ['chosen', 'exec']);
|
||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||
|
||
input.press('Enter');
|
||
frame();
|
||
expect(world.log).toEqual(['Player chosen']);
|
||
});
|
||
|
||
it('stays silent in a behavior that is run as a node, like every event there', () => {
|
||
const f = makeSpriteScene();
|
||
const inner = confirm(f).asset;
|
||
const called = makeBehavior(f, 'Called');
|
||
const cEdit = graphEditor(f, called);
|
||
cEdit.connect(
|
||
cEdit.addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: inner.id }),
|
||
'pressed',
|
||
cEdit.addNode('Log', { message: 'heard in a call' }),
|
||
'exec',
|
||
);
|
||
cEdit.addNode(BEHAVIOR_START);
|
||
|
||
const outer = makeBehavior(f, 'Player');
|
||
const oEdit = graphEditor(f, outer);
|
||
oEdit.connect(
|
||
oEdit.addNode('OnUpdate'),
|
||
'exec',
|
||
oEdit.addNode(RUN_BEHAVIOR, { [BEHAVIOR_PARAM]: called.id }),
|
||
'exec',
|
||
);
|
||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||
place(f, outer.id, { x: 0, y: 0 }, box);
|
||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||
|
||
input.press('Enter');
|
||
frame();
|
||
expect(world.log).toEqual([]);
|
||
});
|
||
|
||
it('does not count as a running behavior of its own', () => {
|
||
const f = makeSpriteScene();
|
||
const { objectId } = hearing(f, confirm(f).asset, ['pressed']);
|
||
const { behaviors, frame } = startWorld(f, { behaviors: true });
|
||
frame();
|
||
expect(behaviors?.runningIds()).toEqual([objectId]);
|
||
});
|
||
|
||
it('lights up the graph of the behavior it hears', () => {
|
||
const f = makeSpriteScene();
|
||
const { asset, end } = confirm(f);
|
||
hearing(f, asset, ['pressed']);
|
||
const { behaviors, input, frame } = startWorld(f, { behaviors: true });
|
||
|
||
frame();
|
||
expect(behaviors?.traceFor(asset.id).nodes.has(end.id)).toBe(false);
|
||
input.press('Enter');
|
||
frame();
|
||
expect(behaviors?.traceFor(asset.id).nodes.has(end.id)).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('On behavior while the graph is edited during a run', () => {
|
||
it('starts hearing a node added to the running graph, and stops hearing a removed one', () => {
|
||
const f = makeSpriteScene();
|
||
const heard = confirm(f).asset;
|
||
const outer = makeBehavior(f, 'Player');
|
||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||
place(f, outer.id, { x: 0, y: 0 }, box);
|
||
const { world, behaviors, input, frame } = startWorld(f, { behaviors: true });
|
||
const tap = () => {
|
||
input.press('Enter');
|
||
frame();
|
||
input.release('Enter');
|
||
frame();
|
||
};
|
||
|
||
tap();
|
||
expect(world.log).toEqual([]);
|
||
|
||
const add = new AddNodeCommand(outer.id, ON_BEHAVIOR, 0, 0, { [BEHAVIOR_PARAM]: heard.id });
|
||
f.stack.execute(add);
|
||
const edit = graphEditor(f, outer);
|
||
edit.connect(add.node, 'pressed', edit.addNode('Log', { message: 'heard' }), 'exec');
|
||
behaviors?.syncFromModel();
|
||
tap();
|
||
expect(world.log).toEqual(['heard']);
|
||
|
||
f.stack.execute(new RemoveNodeCommand(outer.id, add.node.id));
|
||
behaviors?.syncFromModel();
|
||
tap();
|
||
expect(world.log).toEqual(['heard']);
|
||
});
|
||
it('ends the behavior a removed node was hearing: it says goodbye and runs no more', () => {
|
||
const f = makeSpriteScene();
|
||
const heard = makeBehavior(f, 'Chatty');
|
||
const hEdit = graphEditor(f, heard);
|
||
hEdit.addNode(BEHAVIOR_END);
|
||
hEdit.connect(
|
||
hEdit.addNode('OnUpdate'),
|
||
'exec',
|
||
hEdit.addNode('Log', { message: 'inner update' }),
|
||
'exec',
|
||
);
|
||
hEdit.connect(hEdit.addNode('OnDestroy'), 'exec', hEdit.addNode('Log', { message: 'inner bye' }), 'exec');
|
||
const { asset: outer, node } = hearing(f, heard, []);
|
||
const { world, behaviors, frame } = startWorld(f, { behaviors: true });
|
||
|
||
frame();
|
||
expect(world.log).toEqual(['inner update']);
|
||
|
||
f.stack.execute(new RemoveNodeCommand(outer.id, node.id));
|
||
behaviors?.syncFromModel();
|
||
frame();
|
||
frame();
|
||
expect(world.log).toEqual(['inner update', 'inner bye']);
|
||
});
|
||
});
|
||
|
||
describe('an On behavior node that cannot fire', () => {
|
||
const problemOf = (f: ProjectFixture, outer: BehaviorAsset, node: GraphNode) =>
|
||
callProblem(node, graphLookup(f.project), outer.id);
|
||
|
||
it('says so: nothing picked, the behavior gone, or no end node to be reached', () => {
|
||
const f = makeSpriteScene();
|
||
const outer = makeBehavior(f, 'Player');
|
||
const edit = graphEditor(f, outer);
|
||
expect(problemOf(f, outer, edit.addNode(ON_BEHAVIOR))).toBe('No behavior picked');
|
||
expect(problemOf(f, outer, edit.addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: 'nope' }))).toBe(
|
||
'That behavior is gone',
|
||
);
|
||
const endless = makeBehavior(f, 'Endless');
|
||
expect(problemOf(f, outer, edit.addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: endless.id }))).toBe(
|
||
'No End node – never fires',
|
||
);
|
||
expect(
|
||
problemOf(f, outer, edit.addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: confirm(f).asset.id })),
|
||
).toBeNull();
|
||
});
|
||
|
||
it('is reported for a behavior hearing itself, directly or through others, and such a circle does not run', () => {
|
||
const f = makeSpriteScene();
|
||
const a = makeBehavior(f, 'A');
|
||
const b = makeBehavior(f, 'B');
|
||
const aEdit = graphEditor(f, a);
|
||
const bEdit = graphEditor(f, b);
|
||
const aEnd = aEdit.addNode(BEHAVIOR_END);
|
||
const bEnd = bEdit.addNode(BEHAVIOR_END);
|
||
const hearsB = aEdit.addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: b.id });
|
||
const hearsA = bEdit.addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: a.id });
|
||
const hearsSelf = aEdit.addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: a.id });
|
||
aEdit.connect(hearsB, 'exec', aEnd, 'exec');
|
||
bEdit.connect(hearsA, 'exec', bEnd, 'exec');
|
||
bEdit.connect(bEdit.addNode('OnKeyDown', { key: 'a' }), 'exec', bEnd, 'exec');
|
||
aEdit.connect(hearsB, 'exec', aEdit.addNode('Log', { message: 'A heard B' }), 'exec');
|
||
|
||
expect(problemOf(f, a, hearsB)).toBe('A behavior cannot listen to itself');
|
||
expect(problemOf(f, b, hearsA)).toBe('A behavior cannot listen to itself');
|
||
expect(problemOf(f, a, hearsSelf)).toBe('A behavior cannot listen to itself');
|
||
|
||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||
place(f, a.id, { x: 0, y: 0 }, box);
|
||
const { world, input, frame } = startWorld(f, { behaviors: true });
|
||
input.press('a');
|
||
frame();
|
||
// B runs once below A; the A it names in turn is the one already running, so it is not built again.
|
||
expect(world.log).toEqual(['A heard B']);
|
||
expect(world.stopRequested).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('the ports of an On behavior node follow the behavior it hears', () => {
|
||
it('keeps its edges through a rename at the end node and loses them with the port', () => {
|
||
const f = makeSpriteScene();
|
||
const { asset, end } = confirm(f);
|
||
const { asset: outer } = hearing(f, asset, ['pressed']);
|
||
const ports = () => outer.graph.edges.map((e) => e.from.port);
|
||
expect(ports()).toEqual(['pressed']);
|
||
|
||
f.stack.execute(new RenamePortCommand(asset.id, end.id, 'pressed', 'confirmed'));
|
||
expect(ports()).toEqual(['confirmed']);
|
||
|
||
f.stack.execute(new RemovePortCommand(asset.id, end.id, 'confirmed'));
|
||
expect(ports()).toEqual([]);
|
||
f.stack.undo();
|
||
expect(ports()).toEqual(['confirmed']);
|
||
});
|
||
|
||
it('keeps the typed-in value of an input through a rename at the start node', () => {
|
||
const f = makeSpriteScene();
|
||
const { asset } = confirm(f);
|
||
const start = graphEditor(f, asset).addNode(BEHAVIOR_START);
|
||
f.stack.execute(new AddPortCommand(asset.id, start.id, { key: 'amount', kind: 'number' }));
|
||
const outer = makeBehavior(f, 'Player');
|
||
const node = graphEditor(f, outer).addNode(ON_BEHAVIOR, { [BEHAVIOR_PARAM]: asset.id, amount: 5 });
|
||
|
||
f.stack.execute(new RenamePortCommand(asset.id, start.id, 'amount', 'size'));
|
||
expect(node.params.size).toBe(5);
|
||
expect(node.params.amount).toBeUndefined();
|
||
});
|
||
});
|