import { edge, node, port, variable, written } from '../lib/builders.mjs'; import { MAKE_OBJECT } from './lib/scaffold.mjs'; /** * Scopes: a variable or a state machine declared `global` keeps one value for the whole run instead * of one per behavior instance. The step checks the run (two objects counting into one number), the * switch in the inspector, and that a global variable stops being a property of the placed object. * * The step brings its own boxes, behavior and machine; everything it adds is removed at the end. */ /** `On start → Variables{set score} ← Add(score, 1)`: every instance adds one to the same number. */ const counterGraph = { nodes: [ node('sc-start', 'OnStart', 0, 0), node('sc-read', 'Variables', 0, 200, {}, [port('score', 'number')]), node('sc-add', 'Add', 260, 200, { b: 1 }), node('sc-write', 'Variables', 560, 0, {}, [written('score', 'number')]), ], edges: [ edge('sc-e1', 'sc-start', 'exec', 'sc-write', 'exec'), edge('sc-e2', 'sc-read', 'score', 'sc-add', 'a'), edge('sc-e3', 'sc-add', 'result', 'sc-write', 'score'), ], }; const globalScore = { ...variable('score', 'number', 0), scope: 'global' }; export async function scope(t) { const { check, sleep, evaluate, json, shot, click, locate, asset, mutate, stop, has } = t; const selectAsset = async (id) => { await evaluate(`__gm.state.select({ kind: 'asset', assetId: ${JSON.stringify(id)} })`); await sleep(300); }; const selectObject = async (id) => { await evaluate( `__gm.state.select({ kind: 'object', holderId: __gm.state.activeScene.id, objectId: ${JSON.stringify(id)} })`, ); await sleep(300); }; /** The inspector is drawn over the graph, so the last match is the row rather than a port. */ const clickLast = async (text) => { const p = await locate(text, true); await click(p.x, p.y); await sleep(250); }; const scopeOfVariable = async () => (await asset('sc-logic')).variables[0].scope ?? 'local'; // 32a. Two boxes wearing one behavior whose `score` is global. await mutate(` const p = __gm.state.project; const scene = __gm.state.activeScene; const mover = p.assets.find((a) => a.id === 'mover'); const a = structuredClone(mover); Object.assign(a, { id: 'sc-logic', name: 'Counter', props: { enabled: true, runInEditor: false, score: 0 }, variables: ${JSON.stringify([globalScore])}, graph: ${JSON.stringify(counterGraph)}, }); p.assets.unshift(a); ${MAKE_OBJECT} scene.objects.push( make('sc-box-a', 'crate', '', 1500, 'Counter box A'), make('sc-brain-a', 'sc-logic', 'sc-box-a', 0, 'Counter A'), make('sc-box-b', 'crate', '', 1700, 'Counter box B'), make('sc-brain-b', 'sc-logic', 'sc-box-b', 0, 'Counter B'), ); `); await sleep(400); // 32b. The run: two instances, one number. A per-instance variable would stand at 1. await t.play(); await sleep(600); const world = await json('__gm.world()'); const carriers = world.entities.filter((e) => e.props && 'score' in e.props).map((e) => e.name); check( 'two behavior instances count into one global variable', world.shared.variables.score === 2, `score = ${JSON.stringify(world.shared.variables.score)} after both started – 1 would mean each kept its own`, ); check( 'and the value belongs to the run, not to either placed object', carriers.length === 0, `objects carrying "score": ${JSON.stringify(carriers)}`, ); await stop(); await sleep(400); // 32c. A global variable shows no row on the object placed from the behavior – overriding one // value of something every object shares would mean nothing. await selectObject('sc-brain-a'); const rowWhileGlobal = await has('score'); const saysWhereItWent = await has('Shared by the whole run: score.'); check( 'the object names the variable it no longer holds, instead of claiming there is none', saysWhereItWent && !(await has('The behavior declares no variables.')), `note found: ${saysWhereItWent}`, ); await shot('01-object-without-global'); // 32d. The switch sits on the declaration, in the behavior's own inspector. await selectAsset('sc-logic'); const picker = await locate('⊕'); check( 'a global variable is marked as shared where it is declared', Boolean(picker), JSON.stringify(picker), ); await shot('02-declaration'); await click(picker.x, picker.y); await sleep(250); await clickLast('Local – one value per object'); await sleep(350); check( 'and the menu switches it back to one value per object', (await scopeOfVariable()) === 'local', `scope is now ${await scopeOfVariable()}`, ); await selectObject('sc-brain-a'); const rowWhileLocal = await has('score'); check( 'the row appears on the placed object exactly while the variable is local', !rowWhileGlobal && rowWhileLocal, `global: ${rowWhileGlobal}, local: ${rowWhileLocal}`, ); // 32e. A machine carries the same switch, and its own inspector is where it lives. await mutate(` const p = __gm.state.project; p.assets.unshift({ id: 'sc-machine', type: 'statemachine', name: 'Shared doors', props: {}, graph: { nodes: [], edges: [] }, }); `); await selectAsset('sc-machine'); const machineScope = await has('Local'); const pick = await locate('Local', true); await click(pick.x, pick.y); await sleep(250); await clickLast('Global – one current state for the whole run'); await sleep(350); const machine = await asset('sc-machine'); check( 'a state machine is switched between one current state per object and one for the run', machineScope && machine.scope === 'global', `dropdown found: ${machineScope}, scope now ${JSON.stringify(machine.scope)}`, ); await shot('03-machine-scope'); // 32f. Everything this step added goes again. await mutate(` const p = __gm.state.project; const scene = __gm.state.activeScene; scene.objects = scene.objects.filter((o) => !o.id.startsWith('sc-')); p.assets = p.assets.filter((a) => !a.id.startsWith('sc-')); __gm.state.select(null); `); await sleep(300); }