Tooling: - Prettier as the single formatting authority (110 columns, single quotes, measured against the existing style); Markdown excluded because table-cell padding turns the README glossary into 1000-character lines. - Type-aware ESLint ending in eslint-config-prettier, so a rule always means "this is a bug", never "this looks different". - .editorconfig and an `npm run check` that runs typecheck, lint, format and tests in one go. Findings fixed on the first run: three more duplicate imports, four String(unknown) calls (parameter types narrowed instead of casts), two dead initialisers, two redundant casts, a console.log for a real error. Two warnings were false here and are disabled with a reason in place. Tests 132 -> 269: - node-catalog.test.ts covers every node with a run(), with a guard that fails when a new node has no row. - behavior-events.test.ts covers the ten event nodes through a running World, checking timing rather than existence. - Input's state machine (press/release/releaseAll) is reachable without DOM events now; the DOM handlers are thin adapters. Compare fell back to == without a configured operator although the schema default is <; aligned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
5.1 KiB
TypeScript
125 lines
5.1 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
|
||
});
|
||
});
|