- Graph: nodes.ts split into nodes/<category>/<Type>.ts (62 files) with shared.ts factories and index.ts registry; NodeContext/values/NodeRegistry separated, writeTyped() replaces duplicated typed writes - Comments: one style (JSDoc on non-obvious exports, // for decisions and traps), wrong/stale comments fixed or removed, ids.ts now real 10 base-36 chars - Core: chainOf, shared worldPosition/editorPosition core, commands/require.ts, commands/naming.ts, util/list.ts, setBagValue/toPosition/propDef; dead code removed (isBehaviorAsset, SCENE_OBJECT_SCHEMA, CommandStack.subscribe, Input.axis, setMode, titleText, World.worldPosition, unused ctor params) - Editor: Inspector split into inspector/ sections, App.ts into Editor.ts + debugHook.ts + shortcuts.ts, Viewport into items.ts + symbols.ts, shared "Add collider" action, unified type labels/swatches - UI toolkit: Widget.trackHover/onPointerRelease, drawControlBox, math helpers, Popup.openBelow, Menu/Dialog/ColorPicker/ListRow in own files, named Theme sizes - Tools: seed-demo/smoke-demo and demo-check project removed, tools/lib (harness, projects, builders), e2e.mjs as runner over tools/e2e/*.mjs sections - Tests: tests/helpers.ts replaces copied fixtures and NodeContext doubles Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
110 lines
5.0 KiB
TypeScript
110 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { AddColliderCommand } from '../../src/core/commands/colliderCommands';
|
||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import { AddVariableCommand } from '../../src/core/commands/variableCommands';
|
||
import { isHandleEnabled } from '../../src/core/graph';
|
||
import { findAsset, findScene } from '../../src/core/model/Project';
|
||
import { schemaFor } from '../../src/core/properties/PropertySchema';
|
||
import { resolveSceneObjectProps } from '../../src/core/properties/resolve';
|
||
import { fakeHandle, graphEditor, makeBehavior, makeProject, makeSpriteScene, place, roundTrip, runNode, startWorld } from '../helpers';
|
||
|
||
/** A 100×100 wall at x = 200 and a 100×100 mover at the origin, both with box colliders, in a running world. */
|
||
function wallAndMover() {
|
||
const f = makeSpriteScene();
|
||
const moverId = place(f, f.spriteId, { x: 0, y: 0 });
|
||
const wallId = place(f, f.spriteId, { x: 200, y: 0 });
|
||
const wallCollider = new AddColliderCommand(f.sceneId, wallId);
|
||
f.stack.execute(new AddColliderCommand(f.sceneId, moverId));
|
||
f.stack.execute(wallCollider);
|
||
const { world, collisions } = startWorld(f, { collisions: true });
|
||
return { world, collisions, mover: world.findByObjectId(moverId)!, wall: world.findByObjectId(wallCollider.objectId)! };
|
||
}
|
||
|
||
describe('the enabled switch', () => {
|
||
it('is a common property of every placeable type, defaults to on and overrides per object', () => {
|
||
expect(schemaFor('sprite')[0]).toMatchObject({ key: 'enabled', kind: 'boolean', default: true });
|
||
expect(schemaFor('behavior').map((d) => d.key)).toEqual(['enabled']);
|
||
expect(schemaFor('scene').some((d) => d.key === 'enabled')).toBe(false);
|
||
|
||
const f = makeSpriteScene();
|
||
const { project, stack, sceneId, spriteId, object } = f;
|
||
const box = place(f, spriteId, { x: 0, y: 0 });
|
||
expect(resolveSceneObjectProps(findAsset(project, spriteId)!, object(box)).enabled).toBe(true);
|
||
|
||
// Switching an instance off is an ordinary override – and survives a round trip.
|
||
stack.execute(new SetOverrideCommand(sceneId, box, 'enabled', false));
|
||
expect(object(box).overrides).toEqual({ enabled: false });
|
||
expect(findScene(roundTrip(project), sceneId)!.objects[0]!.overrides).toEqual({ enabled: false });
|
||
stack.undo();
|
||
expect(object(box).overrides).toEqual({});
|
||
});
|
||
|
||
it('is inherited: a child of a switched-off parent is off as well', () => {
|
||
const parent = fakeHandle({ enabled: false });
|
||
const child = fakeHandle({ enabled: true }, parent);
|
||
expect(isHandleEnabled(child)).toBe(false);
|
||
expect(child.props.enabled).toBe(true); // its own switch is untouched
|
||
parent.props.enabled = true;
|
||
expect(isHandleEnabled(child)).toBe(true);
|
||
expect(runNode('IsEnabled', { object: child })).toEqual({ result: true, own: true });
|
||
});
|
||
|
||
it('cannot be shadowed by a behavior variable', () => {
|
||
const f = makeProject();
|
||
const behavior = makeBehavior(f);
|
||
expect(() => new AddVariableCommand(behavior.id, 'enabled', 'number').execute(f.project)).toThrow();
|
||
});
|
||
|
||
it('takes a collider out of the collision system while it is off', () => {
|
||
const { world, collisions, wall, mover } = wallAndMover();
|
||
|
||
// The wall blocks: moving 200 to the right stops at 100 units of distance (two 100-wide boxes).
|
||
collisions.moveAndSlide(world, mover, 200, 0);
|
||
expect(mover.x).toBeCloseTo(100, 5);
|
||
|
||
// Switched off, the wall is not there at all.
|
||
mover.x = 0;
|
||
wall.props.enabled = false;
|
||
collisions.moveAndSlide(world, mover, 200, 0);
|
||
expect(mover.x).toBeCloseTo(200, 5);
|
||
});
|
||
|
||
it('stops a behavior while it is off and runs On start when it is switched on', () => {
|
||
const f = makeSpriteScene();
|
||
const behavior = makeBehavior(f, 'Counter', ['ticks']);
|
||
// On start and On update both add one to `ticks`.
|
||
const edit = graphEditor(f, behavior);
|
||
const start = edit.addNode('OnStart');
|
||
const update = edit.addNode('OnUpdate');
|
||
const one = edit.addNode('Number', { value: 1 });
|
||
const add = edit.addNode('Add');
|
||
const get = edit.addNode('GetVariable', { variable: 'ticks' });
|
||
const set = edit.addNode('SetVariable', { variable: 'ticks' });
|
||
edit.wire([
|
||
[update, 'exec', set, 'exec'],
|
||
[get, 'value', add, 'a'],
|
||
[one, 'value', add, 'b'],
|
||
[add, 'result', set, 'value'],
|
||
[start, 'exec', set, 'exec'],
|
||
]);
|
||
const counter = place(f, behavior.id, { x: 0, y: 0 });
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, counter, 'enabled', false));
|
||
|
||
const { world } = startWorld(f, { behaviors: true });
|
||
const entity = world.findByObjectId(counter)!;
|
||
world.tick(0.1);
|
||
world.tick(0.1);
|
||
expect(entity.props.ticks).toBe(0); // off: no On start, no ticks
|
||
|
||
entity.props.enabled = true; // what "Set enabled" does
|
||
world.tick(0.1);
|
||
expect(entity.props.ticks).toBe(2); // On start plus this frame's On update
|
||
world.tick(0.1);
|
||
expect(entity.props.ticks).toBe(3);
|
||
|
||
entity.props.enabled = false;
|
||
world.tick(0.1);
|
||
expect(entity.props.ticks).toBe(3); // frozen again, state kept
|
||
});
|
||
});
|