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); }); });