Keep behavior variables private to their own graph (#257)
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>
This commit is contained in:
1 parent
cf6565d108
commit
e8b3ac0a3b
16 files changed
+455
-49
No files matched your search
@@ -136,7 +136,8 @@ export async function nestedBehavior(t) {
|
||||
await clickText('+ Variable');
|
||||
await clickText('Number');
|
||||
const keptAdded = await kept();
|
||||
const keepButton = await locate('Keep');
|
||||
// A pin since #257: a list's head has no room for the word beside the lock.
|
||||
const keepButton = ((await t.json("__gm.locateIcons('keep')")) ?? [])[0] ?? null;
|
||||
if (keepButton) await click(keepButton.x, keepButton.y);
|
||||
const keptClicked = await kept();
|
||||
// A second click has to flip it back: a button drawn for the old switch would clear it again.
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 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');
|
||||
`);
|
||||
}
|
||||
@@ -58,6 +58,7 @@ import { hierarchyGroups } from './52-hierarchy-groups.mjs';
|
||||
import { curves } from './53-curves.mjs';
|
||||
import { structTypes } from './54-struct-types.mjs';
|
||||
import { typedLists } from './55-typed-lists.mjs';
|
||||
import { privateVariables } from './56-private-variables.mjs';
|
||||
|
||||
export const STEPS = [
|
||||
hierarchy,
|
||||
@@ -115,4 +116,5 @@ export const STEPS = [
|
||||
curves,
|
||||
structTypes,
|
||||
typedLists,
|
||||
privateVariables,
|
||||
];
|
||||
Reference in new issue
Block a user