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
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||
import {
|
||||
AddVariableCommand,
|
||||
RemoveVariableCommand,
|
||||
RenameVariableCommand,
|
||||
SetVariablePrivateCommand,
|
||||
} from '../../src/core/commands/variableCommands';
|
||||
import { type BehaviorAsset, isPrivateVariable, publicVariables } from '../../src/core/model/Asset';
|
||||
import { findAssetOf } from '../../src/core/model/Project';
|
||||
import { makeSpriteScene, place, roundTrip, startWorld } from '../helpers';
|
||||
|
||||
/** A behavior `Brain` with `hp` and `timer`, placed once with an override of each. */
|
||||
function setup() {
|
||||
const f = makeSpriteScene();
|
||||
const create = new CreateAssetCommand('behavior', 'Brain');
|
||||
f.stack.execute(create);
|
||||
const id = create.asset.id;
|
||||
f.stack.execute(new AddVariableCommand(id, 'hp', 'number'));
|
||||
f.stack.execute(new AddVariableCommand(id, 'timer', 'number'));
|
||||
const brain = place(f, id, { x: 0, y: 0 });
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, brain, 'hp', 5));
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, brain, 'timer', 7));
|
||||
const behavior = () => findAssetOf(f.project, id, 'behavior') as BehaviorAsset;
|
||||
return { ...f, id, brain, behavior };
|
||||
}
|
||||
|
||||
describe('private behavior variables (#257)', () => {
|
||||
it('going private drops the overrides of every placed object, undo brings them back', () => {
|
||||
const f = setup();
|
||||
f.stack.execute(new SetVariablePrivateCommand(f.id, 'timer', true));
|
||||
expect(f.behavior().privateVariables).toEqual(['timer']);
|
||||
expect(f.object(f.brain).overrides).toEqual({ hp: 5 });
|
||||
expect(publicVariables(f.behavior()).map((d) => d.key)).toEqual(['hp']);
|
||||
|
||||
f.stack.undo();
|
||||
expect(f.behavior().privateVariables).toBeUndefined();
|
||||
expect(f.object(f.brain).overrides).toEqual({ hp: 5, timer: 7 });
|
||||
});
|
||||
|
||||
it('public again shows the variable with no override – the dropped one stays dropped', () => {
|
||||
const f = setup();
|
||||
f.stack.execute(new SetVariablePrivateCommand(f.id, 'timer', true));
|
||||
f.stack.execute(new SetVariablePrivateCommand(f.id, 'timer', false));
|
||||
expect(isPrivateVariable(f.behavior(), 'timer')).toBe(false);
|
||||
expect(f.object(f.brain).overrides).toEqual({ hp: 5 });
|
||||
});
|
||||
|
||||
it('rename carries the switch, remove and undo keep it', () => {
|
||||
const f = setup();
|
||||
f.stack.execute(new SetVariablePrivateCommand(f.id, 'timer', true));
|
||||
f.stack.execute(new RenameVariableCommand(f.id, 'timer', 'clock'));
|
||||
expect(f.behavior().privateVariables).toEqual(['clock']);
|
||||
f.stack.execute(new RemoveVariableCommand(f.id, 'clock'));
|
||||
expect(f.behavior().privateVariables).toBeUndefined();
|
||||
f.stack.undo();
|
||||
expect(f.behavior().privateVariables).toEqual(['clock']);
|
||||
});
|
||||
|
||||
it('survives saving and loading, names nothing undeclared', () => {
|
||||
const f = setup();
|
||||
f.stack.execute(new SetVariablePrivateCommand(f.id, 'timer', true));
|
||||
f.behavior().privateVariables!.push('ghost');
|
||||
const loaded = findAssetOf(roundTrip(f.project), f.id, 'behavior') as BehaviorAsset;
|
||||
expect(loaded.privateVariables).toEqual(['timer']);
|
||||
});
|
||||
|
||||
it('a run writes no override for a private value and keeps it through a change to the project', () => {
|
||||
const f = setup();
|
||||
f.stack.execute(new SetVariablePrivateCommand(f.id, 'timer', true));
|
||||
const { world } = startWorld(f);
|
||||
const entity = world.findByObjectId(f.brain)!;
|
||||
expect(entity.props.timer).toBe(0);
|
||||
entity.props.timer = 3;
|
||||
entity.props.hp = 9;
|
||||
world.syncToModel();
|
||||
expect(f.object(f.brain).overrides).toEqual({ hp: 9 });
|
||||
|
||||
// Any edit during the run reads the scene back into the entities.
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, f.brain, 'hp', 1));
|
||||
world.syncFromModel();
|
||||
expect(entity.props.hp).toBe(1);
|
||||
expect(entity.props.timer).toBe(3);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user