Engine additions that the example game needed: - `enabled` as a shared property of every placeable asset (default on, an override on the scene object). A switched-off object is not drawn, does not collide and its behavior does not run - and neither does anything parented below it. Disabled objects are now hidden in the scene view as well, so the editor shows what the game shows; they stay in the hierarchy panel, which is where they are selected and switched back on. - Asset type `text` (text, fontSize, color, align), rendered in scene and game. - Pointer input in the game view: `On click`, `Mouse position`, `Mouse button`, `Is hovered`, hit-tested against colliders, else sprite/text footprints. - New nodes: Set/Is enabled, Set text, Is asset, Name of, Equals, To text, Join text, Floor/Ceil/Round, Modulo, Random, Time, Restart game. Tooling: shared CDP harness (tools/harness.mjs), central project registry with a guard that keeps destructive scripts off the playground (tools/projects.mjs), plus smoke suites for the demo scene and the jump-and-run game. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
152 lines
6.2 KiB
TypeScript
152 lines
6.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
CreateAssetCommand,
|
|
DeleteAssetCommand,
|
|
RenameAssetCommand,
|
|
ResetAssetPropertyCommand,
|
|
SetActiveSceneCommand,
|
|
SetAssetPropertyCommand,
|
|
} from '../../src/core/commands/assetCommands';
|
|
import { CommandStack } from '../../src/core/commands/CommandStack';
|
|
import {
|
|
ClearOverrideCommand,
|
|
MoveSceneObjectCommand,
|
|
PlaceAssetCommand,
|
|
RemoveSceneObjectCommand,
|
|
SetOverrideCommand,
|
|
} from '../../src/core/commands/sceneObjectCommands';
|
|
import { createEmptyProject, findScene } from '../../src/core/model/Project';
|
|
|
|
function setup() {
|
|
const project = createEmptyProject('p1', 'Test');
|
|
const stack = new CommandStack(project);
|
|
const scene = new CreateAssetCommand('scene', 'Level 1');
|
|
const sprite = new CreateAssetCommand('sprite', 'Box');
|
|
stack.execute(scene);
|
|
stack.execute(sprite);
|
|
return { project, stack, sceneId: scene.asset.id, spriteId: sprite.asset.id };
|
|
}
|
|
|
|
describe('asset commands', () => {
|
|
it('creates assets with defaults and activates the first scene', () => {
|
|
const { project, sceneId, spriteId } = setup();
|
|
expect(project.assets).toHaveLength(2);
|
|
expect(project.activeSceneId).toBe(sceneId);
|
|
expect(project.assets.find((a) => a.id === spriteId)?.props).toEqual({ enabled: true, width: 100, height: 100, color: 0xe0553f });
|
|
});
|
|
|
|
it('renames and sets properties with undo/redo', () => {
|
|
const { project, stack, spriteId } = setup();
|
|
stack.execute(new RenameAssetCommand(spriteId, 'Crate'));
|
|
stack.execute(new SetAssetPropertyCommand(spriteId, 'width', 250));
|
|
const sprite = () => project.assets.find((a) => a.id === spriteId)!;
|
|
expect(sprite().name).toBe('Crate');
|
|
expect(sprite().props.width).toBe(250);
|
|
stack.undo();
|
|
expect(sprite().props.width).toBe(100);
|
|
stack.undo();
|
|
expect(sprite().name).toBe('Box');
|
|
stack.redo();
|
|
stack.redo();
|
|
expect(sprite().name).toBe('Crate');
|
|
expect(sprite().props.width).toBe(250);
|
|
});
|
|
|
|
it('rejects unknown properties', () => {
|
|
const { stack, spriteId } = setup();
|
|
expect(() => stack.execute(new SetAssetPropertyCommand(spriteId, 'x', 5))).toThrow(/not defined/);
|
|
expect(() => stack.execute(new ResetAssetPropertyCommand(spriteId, 'x'))).toThrow(/not defined/);
|
|
});
|
|
|
|
it('resets a property to its schema default with undo', () => {
|
|
const { project, stack, spriteId } = setup();
|
|
const sprite = () => project.assets.find((a) => a.id === spriteId)!;
|
|
stack.execute(new SetAssetPropertyCommand(spriteId, 'height', 60));
|
|
stack.execute(new ResetAssetPropertyCommand(spriteId, 'height'));
|
|
expect(sprite().props.height).toBe(100);
|
|
stack.undo();
|
|
expect(sprite().props.height).toBe(60);
|
|
stack.redo();
|
|
expect(sprite().props.height).toBe(100);
|
|
});
|
|
|
|
it('deletes an asset together with its scene objects and restores both on undo', () => {
|
|
const { project, stack, sceneId, spriteId } = setup();
|
|
stack.execute(new PlaceAssetCommand(sceneId, spriteId, { x: 1, y: 2 }));
|
|
stack.execute(new PlaceAssetCommand(sceneId, spriteId, { x: 3, y: 4 }));
|
|
expect(findScene(project, sceneId)!.objects).toHaveLength(2);
|
|
stack.execute(new DeleteAssetCommand(spriteId));
|
|
expect(project.assets).toHaveLength(1);
|
|
expect(findScene(project, sceneId)!.objects).toHaveLength(0);
|
|
stack.undo();
|
|
expect(project.assets).toHaveLength(2);
|
|
expect(findScene(project, sceneId)!.objects.map((o) => o.position)).toEqual([
|
|
{ x: 1, y: 2, z: 0 },
|
|
{ x: 3, y: 4, z: 0 },
|
|
]);
|
|
});
|
|
|
|
it('switches the active scene when the active one is deleted', () => {
|
|
const { project, stack, sceneId } = setup();
|
|
const second = new CreateAssetCommand('scene', 'Level 2');
|
|
stack.execute(second);
|
|
stack.execute(new SetActiveSceneCommand(second.asset.id));
|
|
stack.execute(new DeleteAssetCommand(second.asset.id));
|
|
expect(project.activeSceneId).toBe(sceneId);
|
|
stack.undo();
|
|
expect(project.activeSceneId).toBe(second.asset.id);
|
|
});
|
|
});
|
|
|
|
describe('scene object commands', () => {
|
|
it('places assets and refuses scenes', () => {
|
|
const { project, stack, sceneId, spriteId } = setup();
|
|
const place = new PlaceAssetCommand(sceneId, spriteId, { x: 5, y: 6 });
|
|
stack.execute(place);
|
|
const object = findScene(project, sceneId)!.objects[0]!;
|
|
expect(object.id).toBe(place.objectId);
|
|
expect(object.assetId).toBe(spriteId);
|
|
expect(object.position).toEqual({ x: 5, y: 6, z: 0 });
|
|
expect(object.overrides).toEqual({});
|
|
|
|
expect(() => stack.execute(new PlaceAssetCommand(sceneId, sceneId, { x: 0, y: 0 }))).toThrow(/cannot be placed/);
|
|
expect(findScene(project, sceneId)!.objects).toHaveLength(1);
|
|
});
|
|
|
|
it('moves, overrides, clears and removes with undo', () => {
|
|
const { project, stack, sceneId, spriteId } = setup();
|
|
const place = new PlaceAssetCommand(sceneId, spriteId, { x: 0, y: 0 });
|
|
stack.execute(place);
|
|
const object = () => findScene(project, sceneId)!.objects.find((o) => o.id === place.objectId)!;
|
|
|
|
stack.execute(new MoveSceneObjectCommand(sceneId, place.objectId, { x: 40, y: 50, z: 3 }));
|
|
expect(object().position).toEqual({ x: 40, y: 50, z: 3 });
|
|
stack.undo();
|
|
expect(object().position).toEqual({ x: 0, y: 0, z: 0 });
|
|
stack.redo();
|
|
|
|
stack.execute(new SetOverrideCommand(sceneId, place.objectId, 'color', 0x123456));
|
|
expect(object().overrides).toEqual({ color: 0x123456 });
|
|
expect(() => stack.execute(new SetOverrideCommand(sceneId, place.objectId, 'nope', 1))).toThrow();
|
|
|
|
stack.execute(new ClearOverrideCommand(sceneId, place.objectId, 'color'));
|
|
expect(object().overrides).toEqual({});
|
|
stack.undo();
|
|
expect(object().overrides).toEqual({ color: 0x123456 });
|
|
|
|
stack.execute(new RemoveSceneObjectCommand(sceneId, place.objectId));
|
|
expect(findScene(project, sceneId)!.objects).toHaveLength(0);
|
|
stack.undo();
|
|
expect(object().position).toEqual({ x: 40, y: 50, z: 3 });
|
|
});
|
|
|
|
it('clears the redo stack on a new command', () => {
|
|
const { stack, sceneId, spriteId } = setup();
|
|
stack.execute(new PlaceAssetCommand(sceneId, spriteId, { x: 0, y: 0 }));
|
|
stack.undo();
|
|
expect(stack.canRedo).toBe(true);
|
|
stack.execute(new PlaceAssetCommand(sceneId, spriteId, { x: 1, y: 1 }));
|
|
expect(stack.canRedo).toBe(false);
|
|
});
|
|
});
|