import { edge, node, port, variable, written } from '../lib/builders.mjs'; import { MAKE_OBJECT } from './lib/scaffold.mjs'; /** * Shared state: a Variables asset holds one value per key for the whole run, and a state machine * declared `global` one current state. The step checks the run (two objects counting into one * number), that the node offers the asset's declarations, and the switch on the machine. * * The step brings its own boxes, behavior, Variables asset and machine; everything goes at the end. */ const SOURCE = { variablesAsset: 'sc-vars' }; /** `On start → Variables{set score} ← Add(score, 1)`, counting on the shared asset rather than its own. */ const counterGraph = { nodes: [ node('sc-start', 'OnStart', 0, 0), node('sc-read', 'Variables', 0, 200, SOURCE, [port('score', 'number')]), node('sc-add', 'Add', 260, 200, { b: 1 }), node('sc-write', 'Variables', 560, 0, SOURCE, [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'), ], }; export async function scope(t) { const { check, sleep, json, shot, click, locate, asset, mutate, stop, has } = t; const selectAsset = async (id) => { await t.select({ kind: 'asset', assetId: id }); }; const selectNode = async (nodeId) => { await t.select({ kind: 'node', assetId: 'sc-logic', nodeId }); }; /** 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); }; // 32a. A Variables asset, a behavior counting on it, and two boxes wearing that behavior. 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 }, variables: [], graph: ${JSON.stringify(counterGraph)}, }); p.assets.unshift(a, { id: 'sc-vars', type: 'variables', name: 'Score', props: { score: 0 }, variables: ${JSON.stringify([variable('score', 'number', 0)])}, }); ${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'), ); `); // 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); const shared = world.shared.variables['sc-vars']; check( 'two behavior instances count into one shared variable', shared && shared.score === 2, `score = ${JSON.stringify(shared && shared.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(); // 32c. The asset is where the declaration lives, and it says what it is for. await selectAsset('sc-vars'); const declared = await has('score'); const explains = await has('Every graph pointing here shares them.'); check( 'the Variables asset lists its declarations and says what they are', declared && explains, `row: ${declared}, note: ${explains}`, ); await shot('01-variables-asset'); // 32d. The node picks its ports from the asset it names – the whole point of naming one. await selectNode('sc-read'); const offersShared = await has('score'); const complains = await has('"score" is not a variable of this behavior.'); check( 'a node naming the asset offers its variables instead of the behavior’s', offersShared && !complains, `row: ${offersShared}, warning shown: ${complains}`, ); await shot('02-node-source'); // Emptying the setting points the node back at the behavior, which declares nothing – so the // very same port is now a stranger. That is the switch doing something visible. await mutate(` const g = __gm.state.project.assets.find((a) => a.id === 'sc-logic').graph; g.nodes.find((n) => n.id === 'sc-read').params.variablesAsset = ''; `); const complainsNow = await has('"score" is not a variable of this behavior.'); check( 'and clearing it points the node back at the behavior, which has no such variable', complainsNow, `warning after clearing: ${complainsNow}`, ); // 32e. A machine carries its scope on the asset too, with its own switch. 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 clickLast('Global'); 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); `); }