Keep behavior variables private to their own graph (#257)
game-maker / gate (push) Successful in 3m40s
game-maker / suite (push) Successful in 10m56s

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:
adminandClaude Opus 5.5 committed 2026-09-28 13:54:57 +02:00
1 parent cf6565d108
commit e8b3ac0a3b
16 files changed
+455 -49

No files matched your search

+33 -12
View File
@@ -20,6 +20,7 @@ import {
attachRowActions,
createField,
FIELD_INDENT,
type Field,
type FieldHandlers,
fieldDef,
type FieldRow,
@@ -31,7 +32,7 @@ import {
type RowActions,
structFieldValue,
} from './fieldFactory';
import { KIND_ICON_SVG } from '../panels/markIcons';
import { KIND_ICON_SVG, VARIABLE_SWITCH_ICON_SVG } from '../panels/markIcons';
import { addNote, type InspectorHost } from './host';
/** The menu every "add a kind" button opens: one entry per kind, named as the kind table names it. */
@@ -51,8 +52,11 @@ export interface VariableEditing {
addField: (path: readonly string[], kind: NestedKind) => void;
}
/** A behavior's switch for one variable: carried on from one `Run behavior` call to the next (#229). */
export interface VariableKeep {
/**
* A behavior's switch for one variable: carried on from one `Run behavior` call to the next (#229),
* or hidden from everything outside its graph (#257).
*/
export interface VariableSwitch {
on: boolean;
set: (on: boolean) => void;
}
@@ -72,7 +76,8 @@ export interface StructTyping {
export interface VariableOptions {
edit?: VariableEditing;
typing?: StructTyping;
keep?: VariableKeep;
keep?: VariableSwitch;
hidden?: VariableSwitch;
/** ↺/↑ on the first row: an override covers a whole struct, so its fields share them. */
actions?: RowActions;
}
@@ -88,13 +93,15 @@ export function buildVariable(
handlers: FieldHandlers,
options: VariableOptions = {},
): FieldRow {
const { edit, typing, keep, actions } = options;
const { edit, typing, keep, hidden, actions } = options;
const name = nameCell(def.key, 0, edit && ((next) => edit.rename(next)));
const head = gridRow(0, markLabel(def.kind), name);
let setValue: (next: PropertyValue) => void = () => {};
let below: Widget | undefined;
// A struct with a type has the type's fields: they are changed on the Struct asset, not here.
const typed = def.kind === 'struct' ? (typing?.typeName(def.structType) ?? null) : null;
// A private value is set in its own graph only (#257): shown, but not edited here.
const locked = hidden?.on ?? false;
if (def.kind === 'struct') {
head.add(new Spacer(), 1);
if (edit && typed === null) head.add(addFieldMenu(edit, []));
@@ -102,8 +109,10 @@ export function buildVariable(
const field = createField(host, def, value, handlers);
setValue = addValue(head, field);
below = field.below;
lockValue(field, locked);
}
if (keep) head.add(keepButton(keep));
if (hidden) head.add(switchButton('private', hidden));
if (keep) head.add(switchButton('keep', keep));
if (edit) head.add(removeButton(edit.remove));
const refreshActions = actions ? attachRowActions(head, actions) : () => {};
host.body.add(head);
@@ -123,6 +132,7 @@ export function buildVariable(
handlers,
own ? edit : undefined,
own ? typing : undefined,
locked,
);
}
@@ -136,18 +146,26 @@ export function buildVariable(
};
}
/** Lit while the variable is kept; a click flips it. */
function keepButton(keep: VariableKeep): Button {
const button = new Button('Keep', {
/** Lit while the switch is on – the lock while private, the pin while kept; a click flips it. */
function switchButton(icon: keyof typeof VARIABLE_SWITCH_ICON_SVG, of: VariableSwitch): Button {
const button = new Button('', {
variant: 'ghost',
style: 'small',
toggle: true,
onClick: () => keep.set(!keep.on),
icon: VARIABLE_SWITCH_ICON_SVG[icon],
padding: 4,
onClick: () => of.set(!of.on),
});
button.setToggled(keep.on);
button.setToggled(of.on);
return button;
}
/** A value – and the items below it – greyed out and deaf to the pointer while `locked`. */
function lockValue(field: Field, locked: boolean): void {
if (!locked) return;
field.widget.setEnabled(false);
field.below?.setEnabled(false);
}
/**
* One row per field, each with its own value; a field that is a struct again is a head row with its
* fields indented below it (#239). `path` names the struct these fields belong to. Returns the
@@ -161,6 +179,7 @@ function structFieldRows(
handlers: FieldHandlers,
edit: VariableEditing | undefined,
typing: StructTyping | undefined,
locked: boolean,
): (next: PropertyValue) => void {
const setters = new Map<string, (item: PropertyValue) => void>();
for (const field of fields) {
@@ -185,6 +204,7 @@ function structFieldRows(
handlers,
own ? edit : undefined,
own ? typing : undefined,
locked,
),
);
continue;
@@ -192,6 +212,7 @@ function structFieldRows(
const own = handlers.structFieldHandlers?.([...path, field.key], field) ?? READ_ONLY;
const sub = createField(host, fieldDef(field), item, own);
setters.set(field.key, addValue(row, sub));
lockValue(sub, locked);
if (edit) row.add(removeButton(() => edit.removeField(path, field)));
host.body.add(row);
if (field.kind === 'list' && typing) {