- `Texture` and `Color` can be picked as a variable kind, a struct field kind and a declared port kind; a new `Texture` node hands a picture id into the graph. Declared, a texture is its bare id – the `$texture` tag stays a list-only thing. - The `String` node is labelled "Text" like its kind; the type id stays. - Whole-number inputs (`Item at`, `Set item`, `Sub list`, `Fill`, `Repeat`, `Substring`, `To text`) show no decimals (`int()` in shared.ts). - All server projects are reseeded, so `legacyNodes.ts`, `migrateDeclaredLists` and the `ownerId` fallback go, with their tests. - Note `reseed-because-of-list` moved to done/; new note `refactor-types`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
254 lines
9.8 KiB
JavaScript
254 lines
9.8 KiB
JavaScript
import { edge, node, propertiesOf, variable, written } from '../lib/builders.mjs';
|
||
import { MAKE_OBJECT } from './lib/scaffold.mjs';
|
||
|
||
/**
|
||
* Properties: properties of the object handed in, each read at any time and written where it is
|
||
* switched to Set. Its type decides what `+ Property` offers. The step brings its own box and
|
||
* behavior and removes them again.
|
||
*/
|
||
|
||
/** `On update → Properties{width 40, color red} on the owner`, whose width is then read into `seen`. */
|
||
const logicGraph = {
|
||
nodes: [
|
||
node('pp-update', 'OnUpdate', 0, 0),
|
||
node('pp-owner', 'Owner', 0, 240),
|
||
node('pp-set', 'Properties', 260, 0, { ...propertiesOf('sprite'), width: 40, color: 0xff0000 }, [
|
||
written('width', 'number'),
|
||
written('color', 'color'),
|
||
]),
|
||
node('pp-store', 'Variables', 560, 0, {}, [written('seen', 'number')]),
|
||
node('pp-any', 'Properties', 260, 420),
|
||
node('pp-beh', 'Properties', 560, 420, propertiesOf('behavior')),
|
||
],
|
||
edges: [
|
||
edge('pp-e1', 'pp-update', 'exec', 'pp-set', 'exec'),
|
||
edge('pp-e2', 'pp-owner', 'object', 'pp-set', 'object'),
|
||
edge('pp-e3', 'pp-set', 'exec', 'pp-store', 'exec'),
|
||
edge('pp-e4', 'pp-set', 'width', 'pp-store', 'seen'),
|
||
],
|
||
};
|
||
|
||
export async function propertyNodes(t) {
|
||
const { check, sleep, evaluate, json, shot, click, doubleClick, locate, find, undo, entity, asset } = t;
|
||
const { mutate, stop, typeText, press } = t;
|
||
const portsOf = async (nodeId) =>
|
||
((await asset('pp-logic')).graph.nodes.find((n) => n.id === nodeId).ports ?? []).map(
|
||
(p) => `${p.key}:${p.kind}${p.write ? ':set' : ''}`,
|
||
);
|
||
const title = (nodeId) => json(`__gm.graph.nodeLabel(${JSON.stringify(nodeId)})`);
|
||
const settings = async (nodeId) => (await json(`__gm.graph.nodeSettings(${JSON.stringify(nodeId)})`)) ?? [];
|
||
const select = async (nodeId) => {
|
||
await evaluate(
|
||
`__gm.state.select({ kind: 'node', assetId: 'pp-logic', nodeId: ${JSON.stringify(nodeId)}, nodeIds: [${JSON.stringify(nodeId)}] })`,
|
||
);
|
||
await sleep(300);
|
||
};
|
||
/** The inspector is drawn over the graph, so the last match is the row rather than the port. */
|
||
const clickLast = async (text) => {
|
||
const p = await locate(text, true);
|
||
await click(p.x, p.y);
|
||
await sleep(200);
|
||
};
|
||
const offered = async () => {
|
||
await clickLast('+ Property');
|
||
const texts = await json('__gm.overlayTexts()');
|
||
await press('Escape');
|
||
await sleep(200);
|
||
return texts;
|
||
};
|
||
const addProperty = async (search) => {
|
||
await clickLast('+ Property');
|
||
await typeText(search);
|
||
await sleep(150);
|
||
await press('Enter');
|
||
await sleep(300);
|
||
};
|
||
const choose = async (setting, option) => {
|
||
const field = await json(`__gm.inspectorField(${JSON.stringify(setting)}, true)`);
|
||
await click(field.x, field.y);
|
||
await sleep(200);
|
||
await clickLast(option);
|
||
await sleep(200);
|
||
};
|
||
/** The name field, not the node's port or the unconnected-input row below it that say the same. */
|
||
const rename = async (from, to) => {
|
||
const canvas = await json('__gm.graph.canvasRect()');
|
||
const hits = await json(`__gm.locateAll(${JSON.stringify(from)})`);
|
||
const field = hits.find((p) => p.x > canvas.x + canvas.w);
|
||
await doubleClick(field.x, field.y);
|
||
await typeText(to);
|
||
await press('Enter');
|
||
await sleep(300);
|
||
};
|
||
|
||
// 24a. A box wearing a behavior that reshapes it through one node.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
const a = structuredClone(p.assets.find((x) => x.id === 'mover'));
|
||
Object.assign(a, {
|
||
id: 'pp-logic',
|
||
name: 'Reshape',
|
||
props: { enabled: true, runInEditor: false, seen: 0 },
|
||
variables: ${JSON.stringify([variable('seen', 'number', 0)])},
|
||
graph: ${JSON.stringify(logicGraph)},
|
||
});
|
||
p.assets.unshift(a);
|
||
${MAKE_OBJECT}
|
||
scene.objects.push(make('pp-box', 'crate', '', 900, 'Reshaped box'), make('pp-brain', 'pp-logic', 'pp-box', 900, 'Reshape'));
|
||
`);
|
||
await sleep(400);
|
||
|
||
// 24b. The run: one node writes both properties, and its own output reads the width back after.
|
||
await t.play();
|
||
await sleep(400);
|
||
const box = (await entity('pp-box'))?.props;
|
||
const seen = (await entity('pp-brain'))?.props.seen;
|
||
await stop();
|
||
await sleep(300);
|
||
check(
|
||
'one node writes several properties and reads them back',
|
||
box?.width === 40 && box?.color === 0xff0000 && seen === 40,
|
||
JSON.stringify({ width: box?.width, color: box?.color, seen }),
|
||
);
|
||
|
||
// 24c. Titled Properties; the type stands on the node as a setting row, chosen above the list it decides.
|
||
await select('pp-set');
|
||
const typeRow = await json(`__gm.inspectorField('Type', true)`);
|
||
const header = await locate('PROPERTIES', true);
|
||
check(
|
||
'the node is titled Properties and shows its type, chosen above its properties',
|
||
(await title('pp-set')) === 'Properties' &&
|
||
(await settings('pp-set')).join() === 'Type: Sprite' &&
|
||
typeRow &&
|
||
typeRow.y < header.y,
|
||
JSON.stringify({
|
||
title: await title('pp-set'),
|
||
rows: await settings('pp-set'),
|
||
type: typeRow?.y,
|
||
header: header.y,
|
||
}),
|
||
);
|
||
check(
|
||
'the node shows a typed-in color as a color',
|
||
(await find('color #ff0000')) !== null,
|
||
JSON.stringify(await json(`__gm.graph.nodeInputs('pp-set')`)),
|
||
);
|
||
await shot('01-sprite-properties');
|
||
|
||
// 24d. A Sprite offers the fields of a sprite, and nothing of the other types.
|
||
const sprite = await offered();
|
||
check(
|
||
"a Sprite offers a sprite's properties",
|
||
['enabled', 'width', 'height', 'color', 'texture'].every((k) => sprite.includes(k)) &&
|
||
!sprite.includes('radius') &&
|
||
!sprite.includes('zoom'),
|
||
sprite.join(' | '),
|
||
);
|
||
|
||
// 24e. Picked, then switched to Set: its typed-in value starts at the property's own default –
|
||
// a height of 100, not a bare 0 that would flatten the box.
|
||
await addProperty('height');
|
||
const grown = await portsOf('pp-set');
|
||
check(
|
||
'picking a property adds it as an output',
|
||
grown.join(',') === 'width:number:set,color:color:set,height:number',
|
||
grown.join(','),
|
||
);
|
||
const toggle = await json(`__gm.inspectorField('Set', true)`);
|
||
await click(toggle.x, toggle.y);
|
||
await sleep(300);
|
||
const inputs = await json(`__gm.graph.nodeInputs('pp-set')`);
|
||
check(
|
||
"switched to Set, its typed-in value starts at the property's own default",
|
||
inputs.includes('height 100'),
|
||
JSON.stringify(inputs),
|
||
);
|
||
await shot('02-height-set');
|
||
await undo();
|
||
await undo();
|
||
await sleep(300);
|
||
check('undo takes both back', (await portsOf('pp-set')).length === 2, (await portsOf('pp-set')).join());
|
||
|
||
// 24f. Another type offers its own fields; a port it does not have is marked, not dropped.
|
||
await choose('Type', 'Camera');
|
||
const camera = await offered();
|
||
check(
|
||
'switching to Camera shows it on the node and offers the fields of a camera',
|
||
(await settings('pp-set')).join() === 'Type: Camera' &&
|
||
camera.includes('zoom') &&
|
||
!camera.includes('texture'),
|
||
JSON.stringify({ rows: await settings('pp-set'), offered: camera }),
|
||
);
|
||
check(
|
||
'a port the new type does not have stays, marked',
|
||
(await portsOf('pp-set')).length === 2 && (await find('"color" is not a property of a Camera.')) !== null,
|
||
(await portsOf('pp-set')).join(),
|
||
);
|
||
await shot('03-camera');
|
||
await undo();
|
||
await sleep(300);
|
||
check('undo switches back', (await settings('pp-set')).join() === 'Type: Sprite', await settings('pp-set'));
|
||
|
||
// 24g. Any names its ports by hand, and the kind is picked when adding one. A name that happens
|
||
// to be a property's stays of the kind picked.
|
||
await select('pp-any');
|
||
const kinds = await offered();
|
||
check(
|
||
'Any offers the kinds to pick from, not property names',
|
||
['Number', 'Text', 'Boolean', 'Color', 'Texture', 'List', 'Struct'].every((k) => kinds.includes(k)) &&
|
||
!kinds.includes('width'),
|
||
kinds.join(' | '),
|
||
);
|
||
await addProperty('text');
|
||
check('a port is added of the kind picked', (await portsOf('pp-any'))[0] === 'property1:text');
|
||
await rename('property1', 'width');
|
||
check(
|
||
'and renamed by typing, keeping its kind',
|
||
(await portsOf('pp-any'))[0] === 'width:text',
|
||
(await portsOf('pp-any')).join(),
|
||
);
|
||
await evaluate("__gm.state.notice = ''");
|
||
await rename('width', 'object');
|
||
const notice = await json('__gm.state.notice');
|
||
check(
|
||
'the object input keeps its key: a port cannot take it',
|
||
(await portsOf('pp-any'))[0] === 'width:text' && notice.includes('"object" is taken'),
|
||
JSON.stringify({ ports: await portsOf('pp-any'), notice }),
|
||
);
|
||
await shot('04-any');
|
||
|
||
// 24h. A Behavior offers the variables of the behavior picked.
|
||
await select('pp-beh');
|
||
const before = await offered();
|
||
check(
|
||
'a Behavior offers nothing until one is picked',
|
||
before.length === 0 && (await find('Pick a behavior first.')) !== null,
|
||
JSON.stringify(before),
|
||
);
|
||
await choose('Behavior', 'Reshape (behavior)');
|
||
const vars = await offered();
|
||
check(
|
||
'picking the behavior offers its variables and shows it on the node',
|
||
vars.includes('seen') &&
|
||
vars.includes('enabled') &&
|
||
(await settings('pp-beh')).join() === 'Type: Behavior,Behavior: Reshape',
|
||
JSON.stringify({ rows: await settings('pp-beh'), offered: vars }),
|
||
);
|
||
await addProperty('seen');
|
||
check('a variable is picked as a port', (await portsOf('pp-beh')).join() === 'seen:number');
|
||
await shot('05-behavior');
|
||
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
p.assets = p.assets.filter((a) => a.id !== 'pp-logic');
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects = scene.objects.filter((o) => o.id !== 'pp-box' && o.id !== 'pp-brain');
|
||
__gm.state.select(null);
|
||
__gm.state.preview();
|
||
`);
|
||
await sleep(300);
|
||
const left = await json('__gm.state.project.assets.filter((a) => a.id === "pp-logic").length');
|
||
check('the step leaves the fixture as it found it', left === 0, String(left));
|
||
}
|