- Graph: nodes.ts split into nodes/<category>/<Type>.ts (62 files) with shared.ts factories and index.ts registry; NodeContext/values/NodeRegistry separated, writeTyped() replaces duplicated typed writes - Comments: one style (JSDoc on non-obvious exports, // for decisions and traps), wrong/stale comments fixed or removed, ids.ts now real 10 base-36 chars - Core: chainOf, shared worldPosition/editorPosition core, commands/require.ts, commands/naming.ts, util/list.ts, setBagValue/toPosition/propDef; dead code removed (isBehaviorAsset, SCENE_OBJECT_SCHEMA, CommandStack.subscribe, Input.axis, setMode, titleText, World.worldPosition, unused ctor params) - Editor: Inspector split into inspector/ sections, App.ts into Editor.ts + debugHook.ts + shortcuts.ts, Viewport into items.ts + symbols.ts, shared "Add collider" action, unified type labels/swatches - UI toolkit: Widget.trackHover/onPointerRelease, drawControlBox, math helpers, Popup.openBelow, Menu/Dialog/ColorPicker/ListRow in own files, named Theme sizes - Tools: seed-demo/smoke-demo and demo-check project removed, tools/lib (harness, projects, builders), e2e.mjs as runner over tools/e2e/*.mjs sections - Tests: tests/helpers.ts replaces copied fixtures and NodeContext doubles Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
112 lines
5.0 KiB
TypeScript
112 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { type Graph, GraphInterpreter } from '../../src/core/graph';
|
||
import { fakeContext, fakeEntity, fakeHandle, graphEditor, makeBehavior, makeSpriteScene, place, runNode, startWorld } from '../helpers';
|
||
|
||
describe('game-flow nodes', () => {
|
||
it('formats numbers and joins text', () => {
|
||
expect(runNode('ToText', { value: 12.345, decimals: 1 })).toEqual({ text: '12.3' });
|
||
expect(runNode('ToText', { value: 'x', decimals: 0 })).toEqual({ text: 'x' });
|
||
expect(runNode('Join', { a: 'Coins: ', b: 3 })).toEqual({ text: 'Coins: 3' });
|
||
expect(runNode('Modulo', { a: 125, b: 60 })).toEqual({ result: 5 });
|
||
expect(runNode('Modulo', { a: 5, b: 0 })).toEqual({ result: 0 });
|
||
expect(runNode('Floor', { value: 2.9 })).toEqual({ result: 2 });
|
||
});
|
||
|
||
it('compares values and identifies assets', () => {
|
||
const coin = fakeHandle({});
|
||
coin.assetId = 'coin';
|
||
expect(runNode('IsAsset', { object: coin }, { asset: 'coin' })).toEqual({ result: true });
|
||
expect(runNode('IsAsset', { object: coin }, { asset: 'other' })).toEqual({ result: false });
|
||
expect(runNode('IsAsset', { object: null }, { asset: 'coin' })).toEqual({ result: false });
|
||
expect(runNode('Equals', { a: 'a', b: 'a' })).toEqual({ result: true });
|
||
expect(runNode('Equals', { a: coin, b: coin })).toEqual({ result: true });
|
||
expect(runNode('Equals', { a: coin, b: null })).toEqual({ result: false });
|
||
expect(runNode('NameOf', { object: coin })).toEqual({ name: 'h' });
|
||
});
|
||
|
||
it('switches objects on and off and asks for a restart', () => {
|
||
const target = fakeHandle({ enabled: true });
|
||
runNode('SetEnabled', { object: target, enabled: false });
|
||
expect(target.props.enabled).toBe(false);
|
||
|
||
let restarted = false;
|
||
runNode('RestartGame', {}, {}, fakeContext({ restart: () => { restarted = true; } }));
|
||
expect(restarted).toBe(true);
|
||
});
|
||
|
||
it('fires On click when the mouse is pressed on the owner', () => {
|
||
const f = makeSpriteScene('Button');
|
||
const behavior = makeBehavior(f, 'OnPress', ['clicks']);
|
||
// On click → clicks = clicks + 1
|
||
const edit = graphEditor(f, behavior);
|
||
const click = edit.addNode('OnClick');
|
||
const get = edit.addNode('GetVariable', { variable: 'clicks' });
|
||
const one = edit.addNode('Number', { value: 1 });
|
||
const add = edit.addNode('Add');
|
||
const set = edit.addNode('SetVariable', { variable: 'clicks' });
|
||
edit.wire([
|
||
[click, 'exec', set, 'exec'],
|
||
[get, 'value', add, 'a'],
|
||
[one, 'value', add, 'b'],
|
||
[add, 'result', set, 'value'],
|
||
]);
|
||
const button = place(f, f.spriteId, { x: 0, y: 0 });
|
||
const logic = place(f, behavior.id, { x: 0, y: 0 }, button);
|
||
|
||
const { world } = startWorld(f, { behaviors: true });
|
||
const entity = world.findByObjectId(logic)!;
|
||
|
||
world.movePointer(500, 500, true); // beside the button
|
||
world.setPointerDown(true);
|
||
world.tick(0.1);
|
||
expect(entity.props.clicks).toBe(0);
|
||
|
||
world.setPointerDown(false);
|
||
world.movePointer(10, 10, true); // on the button
|
||
world.setPointerDown(true);
|
||
world.tick(0.1);
|
||
expect(entity.props.clicks).toBe(1);
|
||
world.tick(0.1); // still held – the press was one frame
|
||
expect(entity.props.clicks).toBe(1);
|
||
});
|
||
});
|
||
|
||
describe('Spawn node', () => {
|
||
// On start → Spawn(crate at (5,7)) → Log(the spawned object)
|
||
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 = fakeContext({
|
||
spawn: (assetId, position, parent) => {
|
||
calls.push({ assetId, position, parent });
|
||
return fakeEntity('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 = fakeContext({ 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 = fakeContext({ spawn: () => null });
|
||
new GraphInterpreter(graph).fire('OnStart', ctx2);
|
||
expect(ctx2.logs[0]).toBe('Spawn: asset crate not found');
|
||
});
|
||
});
|