A behavior often needs working state such as a timer or a counter. Until now every variable showed up on each placed object and in the Properties menu, so the inspector filled up with values nobody should touch. A private variable works like any other inside its graph but is not offered anywhere outside. - Each behavior variable gets a lock switch next to Keep. It starts off, so existing projects stay as they are (`privateVariables`). - A placed object shows no private variable, neither in the inspector nor while the game runs. If all are private, it says so. - Switching a variable to private drops its overrides on every placed object in one undo step. Switching back does not restore them. - A running game does not write a private value back as an override, and a project change during the run does not reset it. - `+ Property` no longer offers private variables. A port that names one by hand still reads and writes it without a warning. - The default value of a private variable is read-only in the asset inspector. Its shape (name, fields, type, delete) stays editable. - Keep is now a pin icon instead of a word. With the lock added, the header of a list variable had no room left and the lock covered `+ Item`. Not done: the animation track picker still offers private variables. Tests: ./check.sh -> 186 files, 2711 tests, CHECK EXIT 0. e2e 1,19,20,21,56 -> ALL PASSED, 119 checks. New module 56 has 7 checks with a real mouse. Counter-checks fail as expected: unit without the write-back filter, the sync guard, dropping overrides or loading the flag. Browser with all variables on the object, an unfiltered menu, or no value lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
6.7 KiB
JavaScript
168 lines
6.7 KiB
JavaScript
/**
|
||
* 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.
|
||
*
|
||
* Everything it adds carries a `pv56` id and goes again at the end.
|
||
*/
|
||
import { MAKE_OBJECT } 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: typing into it changes nothing, hp still takes a value.
|
||
await sleep(150);
|
||
await press_(await json("__gm.inspectorField('timer')"), 'timer value');
|
||
await typeText('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, hp still takes a new one',
|
||
defaults.timer === 42 && defaults.hp === 8,
|
||
JSON.stringify(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),
|
||
);
|
||
|
||
// 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');
|
||
`);
|
||
}
|