A behavior variable switched private is hidden from the placed object and from the Properties node, but the row dialog of an animation kept offering it. Rows that exist already stay as they are, as decided in the issue. - offeredDefs in src/core/model/animation.ts is what the dialog offers now: what the target can animate, less private variables and less what another row animates already. The filter moved there from RowDialog.ts, so it can be tested without the editor. - animatableDefsOf is unchanged. The label and the runtime read it, so a row on a variable that went private keeps playing and keeps its label. - Deviation from the proposal: under Change target the row's own variable stays on offer, private or not. The dialog would open with no property and a disabled button otherwise. Other private variables are not offered there either. - The command does not refuse a private variable. Only the dialog does not offer it. Finding, not fixed: e2e 56 fails when it runs alone, in 56a-ii from #257 (typing "9" did not arrive: focus is BODY). With 55 before it the module passes, so a full run does not show it. It was not measured on HEAD without this change. Tests: - Gate: 198 files, 2883 tests (2882 + 1), exit 0 - e2e 55,56: 27 checks passed, 56 has +56e - e2e 25: 31 checks passed, it uses the same dialog - Three unit counter-probes each fail the new test - The browser counter-probe fails 56e, mood shows up in the dialog - e2e total 1610 is computed, not measured in a full run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
8.5 KiB
JavaScript
208 lines
8.5 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. The row dialog of an animation does not offer it either (#270).
|
||
*
|
||
* Everything it adds carries a `pv56` id and goes again at the end.
|
||
*/
|
||
import { MAKE_OBJECT, openTab } 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),
|
||
);
|
||
|
||
// 56e. The row dialog of an animation over the scene offers hp for the placed behavior, and neither
|
||
// private variable (#270) – `mood` has no row, so it is not the "already has a row" rule that
|
||
// hides it. The row that aimed at timer before it went private stays, label and all.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const brain = p.assets.find((a) => a.id === 'pv56-brain');
|
||
brain.variables.push({ key: 'mood', label: 'mood', kind: 'number', default: 1 });
|
||
brain.props.mood = 1;
|
||
brain.privateVariables = ['timer', 'mood'];
|
||
p.assets.push({
|
||
id: 'pv56-walk',
|
||
type: 'animation',
|
||
name: 'Walk 56',
|
||
props: { fps: 10, playback: 'loop' },
|
||
root: ${JSON.stringify(sceneId)},
|
||
tracks: [{ id: 'pv56-row', target: 'pv56-obj', property: 'timer', keys: [] }],
|
||
});
|
||
__gm.state.select({ kind: 'asset', assetId: 'pv56-walk' });
|
||
`);
|
||
await openTab(t, 'animation');
|
||
await sleep(300);
|
||
await press_(await json('__gm.animation.addRowAt()'), '+ Row');
|
||
await evaluate("__gm.animation.rowDialogPick('pv56-obj', 'hp')");
|
||
await sleep(150);
|
||
const dialog = await json('__gm.animation.rowDialog()');
|
||
const rows = await json('__gm.animation.rows()');
|
||
const properties = dialog?.properties ?? [];
|
||
check(
|
||
'the row dialog offers hp and no private variable, the row on the private timer stays',
|
||
properties.includes('hp') &&
|
||
!properties.includes('mood') &&
|
||
!properties.includes('timer') &&
|
||
dialog.ok === true &&
|
||
JSON.stringify(rows) === JSON.stringify(['Brain 56 · timer']),
|
||
JSON.stringify({ properties, ok: dialog?.ok, rows }),
|
||
);
|
||
await shot('04-row-dialog');
|
||
await press_(await json("__gm.locate('Cancel', true)"), 'Cancel');
|
||
|
||
// 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' && a.id !== 'pv56-walk');
|
||
`);
|
||
await openTab(t, 'scene');
|
||
}
|