/** * Private behavior variables (#257), with the real mouse: the lock next to a variable's Keep hides it * from the placed object – no row, no override left – and from the `+ Property` menu of a * `Properties` node. A port that names it by hand is still a property of the behavior, and in a run * it reads the value. The row dialog of an animation does not offer it either (#270). * * Everything it adds carries a `pv56` id and goes again at the end. */ import { MAKE_OBJECT, openTab } from './lib/scaffold.mjs'; export async function privateVariables(t) { const { check, json, evaluate, mutate, click, press, typeText, sleep, shot, select, undo, stop } = t; const press_ = async (at, what) => { if (!at) throw new Error(`nothing to click: ${what}`); await click(at.x, at.y); await sleep(150); }; const asset = (id) => json(`__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(id)}) ?? null`); const brain = () => asset('pv56-brain'); const overrides = () => json("__gm.state.activeScene.objects.find((o) => o.id === 'pv56-obj')?.overrides ?? null"); /** The locks in the inspector, top to bottom – one per variable. */ const locks = async () => ((await json("__gm.locateIcons('private')")) ?? []).sort((a, b) => a.y - b.y); const inspectorHas = async (key) => (await json(`__gm.inspectorField(${JSON.stringify(key)})`)) !== null; const node = (id, type, x, y, params = {}, ports) => ({ id, type, x, y, params, ...(ports ? { ports } : {}), }); const edge = (id, from, fromPort, to, toPort) => ({ id, from: { node: from, port: fromPort }, to: { node: to, port: toPort }, }); // On start, a Properties node of type Brain 56 reads `timer` off the behavior itself – a port // named by hand, since the menu does not offer it once the variable is private. const graph = { nodes: [ node('pv56-start', 'OnStart', 0, 0), node('pv56-self', 'Self', 0, 200), node('pv56-props', 'Properties', 240, 200, { objectType: 'behavior', objectBehavior: 'pv56-brain' }, [ { key: 'timer', kind: 'number' }, ]), node('pv56-log', 'Log', 520, 0), ], edges: [ edge('pv56-e1', 'pv56-start', 'exec', 'pv56-log', 'exec'), edge('pv56-e2', 'pv56-self', 'object', 'pv56-props', 'object'), edge('pv56-e3', 'pv56-props', 'timer', 'pv56-log', 'message'), ], }; await mutate(` const p = __gm.state.project; const scene = __gm.state.activeScene; p.assets.push({ id: 'pv56-brain', type: 'behavior', name: 'Brain 56', props: { enabled: true, runInEditor: false, hp: 3, timer: 42 }, variables: [ { key: 'hp', label: 'hp', kind: 'number', default: 3 }, { key: 'timer', label: 'timer', kind: 'number', default: 42 }, ], graph: ${JSON.stringify(graph)}, }); ${MAKE_OBJECT} const placed = make('pv56-obj', 'pv56-brain', '', 400, 'Brain 56'); placed.overrides = { hp: 5, timer: 7 }; scene.objects.push(placed); `); const sceneId = await json('__gm.state.activeScene.id'); // 56a. Each variable has a lock, off. A click on the one of `timer` makes it private and takes the // placed object's override of it; undo brings both back. await select({ kind: 'asset', assetId: 'pv56-brain' }); await sleep(200); const shown = await locks(); check('each variable of the behavior has a lock', shown.length === 2, JSON.stringify(shown)); await press_(shown[1], 'the lock of timer'); const made = { hidden: (await brain()).privateVariables ?? null, overrides: await overrides() }; check( 'a click on the lock makes timer private and drops its override, hp keeps its own', JSON.stringify(made) === JSON.stringify({ hidden: ['timer'], overrides: { hp: 5 } }), JSON.stringify(made), ); await shot('01-lock'); await undo(); const undone = { hidden: (await brain()).privateVariables ?? null, overrides: await overrides() }; check( 'undo makes it public again, with the override back', JSON.stringify(undone) === JSON.stringify({ hidden: null, overrides: { hp: 5, timer: 7 } }), JSON.stringify(undone), ); await press_((await locks())[1], 'the lock of timer, again'); // 56a-ii. While locked the value is read-only: a click into it leaves no field focused, so typing // changes nothing. hp still takes a value. await sleep(150); await press_(await json("__gm.inspectorField('timer')"), 'timer value'); const focusedAfter = await json('__gm.focusedRect()'); await t.typeBlind('9'); await press('Enter'); await press_(await json("__gm.inspectorField('hp')"), 'hp value'); await press('a', { ctrl: true }); await typeText('8'); await press('Enter'); await sleep(150); const defaults = (await brain()).props; check( 'the value of the private timer is read-only and takes no focus, hp still takes a new one', focusedAfter === null && defaults.timer === 42 && defaults.hp === 8, JSON.stringify({ focusedAfter, ...defaults }), ); // 56b. The placed object shows hp only; its Variables tab builds the same rows. await select({ kind: 'object', holderId: sceneId, objectId: 'pv56-obj' }); await sleep(200); const onObject = { hp: await inspectorHas('hp'), timer: await inspectorHas('timer') }; check( 'the placed object shows hp, and no row for the private timer', onObject.hp && !onObject.timer, JSON.stringify(onObject), ); await shot('02-object'); // 56c. + Property of a Properties node on Brain 56 offers hp, not timer; the port that names timer // by hand is no stranger to the behavior, so it carries no warning. await select({ kind: 'node', assetId: 'pv56-brain', nodeId: 'pv56-props', nodeIds: ['pv56-props'] }); await sleep(200); await press_(await json("__gm.locate('+ Property', true)"), '+ Property'); const offered = (await json('__gm.overlayTexts()')) ?? []; await press('Escape'); const warned = (await json(`__gm.locate('"timer" is not a property of "Brain 56".')`)) !== null; check( 'the Properties menu offers hp but not timer, and the hand-named timer port is not flagged', offered.includes('hp') && !offered.includes('timer') && !warned, JSON.stringify({ offered, warned }), ); await shot('03-properties'); // 56d. In a run that port reads the private value. await evaluate('__gm.state.select(null)'); const logFrom = t.consoleLog.length; await t.play(); await sleep(600); await stop(); const logged = t.consoleLog .slice(logFrom) .filter((line) => line.startsWith('[behavior Brain 56]')) .map((line) => line.split(']').pop().trim()); check( 'in a run the hand-named port reads the private timer', logged.includes('42'), JSON.stringify(logged), ); // 56e. The row dialog of an animation over the scene offers hp for the placed behavior, and neither // private variable (#270) – `mood` has no row, so it is not the "already has a row" rule that // hides it. The row that aimed at timer before it went private stays, label and all. await mutate(` const p = __gm.state.project; const brain = p.assets.find((a) => a.id === 'pv56-brain'); brain.variables.push({ key: 'mood', label: 'mood', kind: 'number', default: 1 }); brain.props.mood = 1; brain.privateVariables = ['timer', 'mood']; p.assets.push({ id: 'pv56-walk', type: 'animation', name: 'Walk 56', props: { fps: 10, playback: 'loop' }, root: ${JSON.stringify(sceneId)}, tracks: [{ id: 'pv56-row', target: 'pv56-obj', property: 'timer', keys: [] }], }); __gm.state.select({ kind: 'asset', assetId: 'pv56-walk' }); `); await openTab(t, 'animation'); await sleep(300); await press_(await json('__gm.animation.addRowAt()'), '+ Row'); await evaluate("__gm.animation.rowDialogPick('pv56-obj', 'hp')"); await sleep(150); const dialog = await json('__gm.animation.rowDialog()'); const rows = await json('__gm.animation.rows()'); const properties = dialog?.properties ?? []; check( 'the row dialog offers hp and no private variable, the row on the private timer stays', properties.includes('hp') && !properties.includes('mood') && !properties.includes('timer') && dialog.ok === true && JSON.stringify(rows) === JSON.stringify(['Brain 56 · timer']), JSON.stringify({ properties, ok: dialog?.ok, rows }), ); await shot('04-row-dialog'); await press_(await json("__gm.locate('Cancel', true)"), 'Cancel'); // Clean up. await evaluate('__gm.state.select(null)'); await mutate(` const p = __gm.state.project; const scene = __gm.state.activeScene; scene.objects = scene.objects.filter((o) => o.id !== 'pv56-obj'); p.assets = p.assets.filter((a) => a.id !== 'pv56-brain' && a.id !== 'pv56-walk'); `); await openTab(t, 'scene'); }