Files
feuerwerk/tests/core/private-variables.test.ts
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

87 lines
3.8 KiB
TypeScript
Raw Permalink 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.
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);
});
});