import { edge, node, port, variable, written } from '../lib/builders.mjs'; import { MAKE_OBJECT } from './lib/scaffold.mjs'; /** * Get properties / Set properties: several properties of an object in one node, picked the way * the Variables node picks its variables. A built-in property keeps its own field and default; a name the * list does not know is typed. The step brings its own box and behavior and removes them again. */ /** `On update → Set properties{width 40, color red} on the owner`, then its width read back into `seen`. */ const logicGraph = { nodes: [ node('pp-update', 'OnUpdate', 0, 0), node('pp-owner', 'Owner', 0, 240), node('pp-set', 'SetProperties', 260, 0, { width: 40, color: 0xff0000 }, [ port('width', 'number'), port('color', 'number'), ]), node('pp-get', 'GetProperties', 260, 300, {}, [port('width', 'number')]), node('pp-store', 'Variables', 560, 0, {}, [written('seen', 'number')]), node('pp-single', 'SetProperty', 560, 300, { property: 'width' }), ], 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-owner', 'object', 'pp-get', 'object'), edge('pp-e5', 'pp-get', '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}`, ); 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 addProperty = async (search) => { await clickLast('+ Property'); await typeText(search); await sleep(150); await press('Enter'); await sleep(300); }; /** 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, another reads one of them back. 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, another reads them back', box?.width === 40 && box?.color === 0xff0000 && seen === 40, JSON.stringify({ width: box?.width, color: box?.color, seen }), ); // 24c. A typed-in value reads in the words of its property: a color as its hex. await select('pp-set'); check( 'the node shows a typed-in color as a color', (await find('color #ff0000')) !== null, JSON.stringify(await json(`__gm.graph.nodeLabel('pp-set')`)), ); await shot('01-property-ports'); // 24d. The list names the type each property belongs to; a key several types share is there once // per type, and taken for all of them once picked. await clickLast('+ Property'); const offered = await json('__gm.overlayTexts()'); await press('Escape'); await sleep(200); check( 'the list names each property with its type', ['Object enabled', 'Sprite height', 'Camera zoom', 'Text align'].every((l) => offered.includes(l)) && !offered.includes('height'), offered.join(' | '), ); // 24e. Picked from the built-in properties, and an unconnected one starts at that property's // 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 a port', grown.join(',') === 'width:number,color:number,height:number', grown.join(','), ); check( "and its typed-in value starts at the property's own default", (await find('height 100')) !== null, JSON.stringify(await find('height 0')), ); await shot('02-property-added'); await undo(); await sleep(300); check('undo takes it back', (await portsOf('pp-set')).length === 2); // 24f. A name the list does not know – a variable of another behavior's object – is typed, with // a kind of its own. Typing a built-in name turns it into that property, kind and all. await addProperty('custom text'); check('a custom port is named and typed by hand', (await portsOf('pp-set'))[2] === 'property1:text'); await rename('property1', 'speed'); check( 'and renamed by typing', (await portsOf('pp-set'))[2] === 'speed:text', (await portsOf('pp-set')).join(), ); await rename('speed', 'radius'); check( 'typing a built-in name makes it that property, with its kind', (await portsOf('pp-set'))[2] === 'radius:number', (await portsOf('pp-set')).join(), ); await shot('03-custom-renamed'); await addProperty('custom number'); await evaluate("__gm.state.notice = ''"); await rename('property1', 'object'); const notice = await json('__gm.state.notice'); check( 'the object input keeps its key: a port cannot take it', (await portsOf('pp-set'))[3] === 'property1:number' && notice.includes('"object" is taken'), JSON.stringify({ ports: await portsOf('pp-set'), notice }), ); // 24g. Get properties grows outputs from the same list. await select('pp-get'); await addProperty('color'); check( 'Get properties picks from the same list, as outputs', (await portsOf('pp-get')).join(',') === 'width:number,color:number' && Boolean(await json(`__gm.graph.portAt('pp-get', 'color', true)`)), (await portsOf('pp-get')).join(','), ); // 24h. The single Set property names its property by text; the button beside it suggests the // same list, and picking an entry writes its key. await select('pp-single'); const param = async () => (await asset('pp-logic')).graph.nodes.find((n) => n.id === 'pp-single').params.property; const field = await json(`__gm.inspectorField('Property', true)`); await click(2 * field.x - field.left - 14, field.y); await sleep(200); const suggested = await json('__gm.overlayTexts()'); check( 'the property setting suggests the built-in properties, the current one marked', suggested.includes('● Sprite width') && suggested.includes('Text align'), suggested.join(' | '), ); await shot('04-property-suggestions'); await typeText('text align'); await sleep(150); await press('Enter'); await sleep(300); check( 'picking a suggestion writes its key', (await param()) === 'align' && (await find('Property: align')) !== null, JSON.stringify(await param()), ); await undo(); await sleep(300); check('undo takes it back', (await param()) === 'width', JSON.stringify(await param())); 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)); }