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

+61 -8
View File
@@ -5,7 +5,13 @@ import {
type PropertyValue,
type StructValue,
} from '../model/Properties';
import { isKeptVariable, setKeptVariable, type VariableOwnerAsset } from '../model/Asset';
import {
isKeptVariable,
isPrivateVariable,
setKeptVariable,
setPrivateVariable,
type VariableOwnerAsset,
} from '../model/Asset';
import { overrideSites } from '../model/instance';
import type { Project } from '../model/Project';
import type { VariableKind } from '../properties/kinds';
@@ -93,7 +99,12 @@ function restoreOverrides(
/** Removes a variable together with its default and every instance override. */
export class RemoveVariableCommand implements Command {
readonly label: string;
private removed?: { def: Removed<PropertyDef>; value: PropertyValue | undefined; kept: boolean };
private removed?: {
def: Removed<PropertyDef>;
value: PropertyValue | undefined;
kept: boolean;
hidden: boolean;
};
private overrides: TakenOverride[] = [];
constructor(
@@ -108,9 +119,13 @@ export class RemoveVariableCommand implements Command {
const def = removeFirst(asset.variables, (v) => v.key === this.key);
if (!def) throw new CommandError(`Variable "${this.key}" not found`);
const kept = asset.type === 'behavior' && isKeptVariable(asset, this.key);
this.removed = { def, value: asset.props[this.key], kept };
const hidden = isPrivateVariable(asset, this.key);
this.removed = { def, value: asset.props[this.key], kept, hidden };
delete asset.props[this.key];
if (asset.type === 'behavior') setKeptVariable(asset, this.key, false);
if (asset.type === 'behavior') {
setKeptVariable(asset, this.key, false);
setPrivateVariable(asset, this.key, false);
}
this.overrides = takeOverrides(project, this.assetId, this.key);
}
@@ -119,7 +134,10 @@ export class RemoveVariableCommand implements Command {
const asset = requireVariableOwner(project, this.assetId);
restoreAt(asset.variables, [this.removed.def]);
if (this.removed.value !== undefined) asset.props[this.key] = this.removed.value;
if (this.removed.kept && asset.type === 'behavior') setKeptVariable(asset, this.key, true);
if (asset.type === 'behavior') {
if (this.removed.kept) setKeptVariable(asset, this.key, true);
if (this.removed.hidden) setPrivateVariable(asset, this.key, true);
}
restoreOverrides(project, this.assetId, this.key, this.overrides);
}
}
@@ -143,9 +161,12 @@ export class RenameVariableCommand extends RenameCommand {
if (asset.variables.some((v) => v.key === to)) throw new CommandError(`Variable "${to}" already exists`);
def.key = to;
def.label = to;
// Renamed in place, so the switch keeps its spot in the list.
if (asset.type === 'behavior' && asset.keptVariables)
asset.keptVariables = asset.keptVariables.map((k) => (k === from ? to : k));
// Renamed in place, so the switches keep their spot in the list.
if (asset.type === 'behavior') {
if (asset.keptVariables) asset.keptVariables = asset.keptVariables.map((k) => (k === from ? to : k));
if (asset.privateVariables)
asset.privateVariables = asset.privateVariables.map((k) => (k === from ? to : k));
}
if (asset.props[from] !== undefined) {
asset.props[to] = asset.props[from]!;
delete asset.props[from];
@@ -185,6 +206,38 @@ export class SetVariableKeptCommand implements Command {
}
}
/**
* Whether a behavior's variable is seen outside its own graph (#257). Going private drops the
* overrides of every placed object: nobody would see them any more, yet they would still act.
*/
export class SetVariablePrivateCommand implements Command {
readonly label: string;
private previous = false;
private overrides: TakenOverride[] = [];
constructor(
private readonly assetId: string,
private readonly key: string,
private readonly hidden: boolean,
) {
this.label = hidden ? `Make ${key} private` : `Make ${key} public`;
}
execute(project: Project): void {
const asset = requireBehavior(project, this.assetId);
if (!asset.variables.some((v) => v.key === this.key))
throw new CommandError(`Variable "${this.key}" not found`);
this.previous = isPrivateVariable(asset, this.key);
setPrivateVariable(asset, this.key, this.hidden);
this.overrides = this.hidden ? takeOverrides(project, this.assetId, this.key) : [];
}
undo(project: Project): void {
setPrivateVariable(requireBehavior(project, this.assetId), this.key, this.previous);
restoreOverrides(project, this.assetId, this.key, this.overrides);
}
}
/**
* A struct's fields are no properties of anything placed, so the reserved property names are free
* there – as they are for a struct field.