Files
feuerwerk/tools/e2e/56-private-variables.mjs
T
adminandClaude Opus 5.5 e8b3ac0a3b
game-maker / gate (push) Successful in 3m40s
game-maker / suite (push) Successful in 10m56s
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>
2026-09-28 13:54:57 +02:00

168 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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');
`);
}