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, 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 t.select({ kind: 'node', assetId: 'pp-logic', nodeId, nodeIds: [nodeId] }); }; /** A port section takes its choices when it is built, and re-selecting the same node is a no-op. */ const rebuildInspector = async () => { await evaluate('__gm.state.select(null)'); await select('pp-set'); }; /** 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); }; const offered = async (shotName) => { await clickLast('+ Property'); const texts = await json('__gm.overlayTexts()'); if (shotName) await shot(shotName); await press('Escape'); return texts; }; const addProperty = async (search) => { await clickLast('+ Property'); await typeText(search); await press('Enter'); }; const choose = async (setting, option) => { const field = await json(`__gm.inspectorField(${JSON.stringify(setting)}, true)`); await click(field.x, field.y); await clickLast(option); }; /** 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'); }; // 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')); `); // 24b. The run: one node writes both properties, and its own output reads the width back after. await t.play(); const box = (await entity('pp-box'))?.props; const seen = (await entity('pp-brain'))?.props.seen; await stop(); 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(' | '), ); // 24d-ii. Which sprite the node will meet is only known at runtime, so it offers the parameters of // every shader – each entry saying whose it is, and one shared name naming both shaders. await mutate(` __gm.state.project.assets.push({ id: 'pp-neon', type: 'shader', name: 'Neon glow', props: {}, graph: { nodes: [ { id: 'pp-n1', type: 'ShaderProperty', x: 0, y: 0, params: { name: 'brightness', default: 1, min: 0, max: 2 } }, { id: 'pp-n2', type: 'ShaderProperty', x: 0, y: 120, params: { name: 'glow', default: 1, min: 0, max: 2 } }, ], edges: [], }, }); `); await rebuildInspector(); const shared = await offered('02-shader-parameters'); check( 'a shader parameter names the shader it comes from, a shared name both of them', shared.includes('brightness (Adjust colors, Neon glow)') && shared.includes('glow (Neon glow)') && !shared.includes('brightness') && shared.includes('width'), shared.filter((s) => s.includes('(') || s === 'brightness' || s === 'width').join(' | '), ); await mutate(` const list = __gm.state.project.assets; const i = list.findIndex((a) => a.id === 'pp-neon'); if (i >= 0) list.splice(i, 1); `); await rebuildInspector(); const alone = await offered(); check( 'with that shader gone the parameter names only the one left', alone.includes('brightness (Adjust colors)'), alone.filter((s) => s.includes('(')).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); 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('03-height-set'); await undo(); await undo(); 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('04-camera'); await undo(); 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(); `); 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)); }