Enabled switch, text assets, mouse input and the jump-and-run example game
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>
This commit is contained in:
1 parent
4900d7bbf6
commit
76aa441dce
33 files changed
+1905
-301
No files matched your search
@@ -32,7 +32,7 @@ describe('asset commands', () => {
|
||||
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({ width: 100, height: 100, color: 0xe0553f });
|
||||
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', () => {
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildAsset, CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import { AddNodeCommand, ConnectCommand } from '../../src/core/commands/graphCommands';
|
||||
import { CommandStack } from '../../src/core/commands/CommandStack';
|
||||
import { AddColliderCommand } from '../../src/core/commands/colliderCommands';
|
||||
import { PlaceAssetCommand, SetOverrideCommand, SetParentCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||
import { AddVariableCommand } from '../../src/core/commands/variableCommands';
|
||||
import { type EntityHandle, isHandleEnabled, requireNodeDef, type NodeContext, type Value } from '../../src/core/graph';
|
||||
import type { BehaviorAsset } from '../../src/core/model/Asset';
|
||||
import type { PropertyBag } from '../../src/core/model/Properties';
|
||||
import { createEmptyProject, findScene } from '../../src/core/model/Project';
|
||||
import { footprintOf, textSize } from '../../src/core/properties/bounds';
|
||||
import { schemaFor } from '../../src/core/properties/PropertySchema';
|
||||
import { resolveSceneObjectProps } from '../../src/core/properties/resolve';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
import { BehaviorSystem } from '../../src/runtime/systems/BehaviorSystem';
|
||||
import { CollisionSystem } from '../../src/runtime/systems/CollisionSystem';
|
||||
import { Input } from '../../src/runtime/systems/Input';
|
||||
import { World } from '../../src/runtime/World';
|
||||
|
||||
/** Runs one node definition in isolation – enough for the small data nodes. */
|
||||
function run(type: string, inputs: Record<string, Value>, params: PropertyBag = {}, ctx: Partial<NodeContext> = {}): Record<string, Value> {
|
||||
return requireNodeDef(type).run!(ctx as NodeContext, inputs, params).outputs ?? {};
|
||||
}
|
||||
|
||||
function handle(props: PropertyBag, parent: EntityHandle | null = null): EntityHandle {
|
||||
return { id: 'h', assetId: 'a', name: 'h', x: 0, y: 0, z: 0, props, parent };
|
||||
}
|
||||
|
||||
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 project = createEmptyProject('p', 'P');
|
||||
const stack = new CommandStack(project);
|
||||
const scene = new CreateAssetCommand('scene', 'Level');
|
||||
const sprite = new CreateAssetCommand('sprite', 'Box');
|
||||
stack.execute(scene);
|
||||
stack.execute(sprite);
|
||||
const placed = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 0, y: 0 });
|
||||
stack.execute(placed);
|
||||
const object = findScene(project, scene.asset.id)!.objects[0]!;
|
||||
expect(resolveSceneObjectProps(sprite.asset, object).enabled).toBe(true);
|
||||
|
||||
// Switching an instance off is an ordinary override – and survives a round trip.
|
||||
stack.execute(new SetOverrideCommand(scene.asset.id, placed.objectId, 'enabled', false));
|
||||
expect(object.overrides).toEqual({ enabled: false });
|
||||
const loaded = deserializeProject(serializeProject(project), 'p');
|
||||
expect(findScene(loaded, scene.asset.id)!.objects[0]!.overrides).toEqual({ enabled: false });
|
||||
stack.undo();
|
||||
expect(object.overrides).toEqual({});
|
||||
});
|
||||
|
||||
it('is inherited: a child of a switched-off parent is off as well', () => {
|
||||
const parent = handle({ enabled: false });
|
||||
const child = handle({ 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(run('IsEnabled', { object: child })).toEqual({ result: true, own: true });
|
||||
});
|
||||
|
||||
it('cannot be shadowed by a behavior variable', () => {
|
||||
const project = createEmptyProject('p', 'P');
|
||||
const behavior = buildAsset('behavior', 'B', 'b') as BehaviorAsset;
|
||||
project.assets.push(behavior);
|
||||
expect(() => new AddVariableCommand('b', 'enabled', 'number').execute(project)).toThrow();
|
||||
});
|
||||
|
||||
it('takes a collider out of the collision system while it is off', () => {
|
||||
const { world, wall, mover } = collisionSetup();
|
||||
const collisions = new CollisionSystem();
|
||||
world.addSystem(collisions);
|
||||
|
||||
// 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 project = createEmptyProject('p', 'P');
|
||||
const stack = new CommandStack(project);
|
||||
const scene = new CreateAssetCommand('scene', 'Level');
|
||||
const behavior = new CreateAssetCommand('behavior', 'Counter');
|
||||
stack.execute(scene);
|
||||
stack.execute(behavior);
|
||||
stack.execute(new AddVariableCommand(behavior.asset.id, 'ticks', 'number'));
|
||||
const start = new AddNodeCommand(behavior.asset.id, 'OnStart', 0, 0);
|
||||
const update = new AddNodeCommand(behavior.asset.id, 'OnUpdate', 0, 100);
|
||||
const one = new AddNodeCommand(behavior.asset.id, 'Number', 0, 200, { value: 1 });
|
||||
const add = new AddNodeCommand(behavior.asset.id, 'Add', 100, 200);
|
||||
const get = new AddNodeCommand(behavior.asset.id, 'GetVariable', 0, 300, { variable: 'ticks' });
|
||||
const set = new AddNodeCommand(behavior.asset.id, 'SetVariable', 200, 100, { variable: 'ticks' });
|
||||
for (const c of [start, update, one, add, get, set]) stack.execute(c);
|
||||
for (const [from, fromPort, to, toPort] of [
|
||||
[update, 'exec', set, 'exec'],
|
||||
[get, 'value', add, 'a'],
|
||||
[one, 'value', add, 'b'],
|
||||
[add, 'result', set, 'value'],
|
||||
[start, 'exec', set, 'exec'],
|
||||
] as const) {
|
||||
stack.execute(new ConnectCommand(behavior.asset.id, { node: from.node.id, port: fromPort }, { node: to.node.id, port: toPort }));
|
||||
}
|
||||
const placed = new PlaceAssetCommand(scene.asset.id, behavior.asset.id, { x: 0, y: 0 });
|
||||
stack.execute(placed);
|
||||
stack.execute(new SetOverrideCommand(scene.asset.id, placed.objectId, 'enabled', false));
|
||||
|
||||
const world = World.instantiate(project, scene.asset.id);
|
||||
world.addSystem(new BehaviorSystem(project, new Input(), null));
|
||||
const entity = world.findByObjectId(placed.objectId)!;
|
||||
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
|
||||
});
|
||||
});
|
||||
|
||||
describe('text objects and the mouse', () => {
|
||||
it('measures a text footprint from its content and font size', () => {
|
||||
expect(textSize({ text: 'ab', fontSize: 100 }).w).toBeCloseTo(110, 5);
|
||||
expect(textSize({ text: 'ab', fontSize: 100 }).h).toBeCloseTo(125, 5);
|
||||
expect(footprintOf('text', { text: 'abcd', fontSize: 100 }).w).toBeCloseTo(220, 5);
|
||||
expect(run('SetText', { object: handle({ text: 'old' }), text: 42 })).toEqual({});
|
||||
});
|
||||
|
||||
it('hit-tests an object through its colliders, else through its sprites and texts', () => {
|
||||
const project = createEmptyProject('p', 'P');
|
||||
const stack = new CommandStack(project);
|
||||
const scene = new CreateAssetCommand('scene', 'Level');
|
||||
const sprite = new CreateAssetCommand('sprite', 'Button');
|
||||
stack.execute(scene);
|
||||
stack.execute(sprite);
|
||||
const button = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 0, y: 0 });
|
||||
stack.execute(button);
|
||||
const label = new CreateAssetCommand('text', 'Label');
|
||||
stack.execute(label);
|
||||
const text = new PlaceAssetCommand(scene.asset.id, label.asset.id, { x: 0, y: 200 });
|
||||
stack.execute(text);
|
||||
stack.execute(new SetParentCommand(scene.asset.id, text.objectId, button.objectId));
|
||||
|
||||
const world = World.instantiate(project, scene.asset.id);
|
||||
const entity = world.findByObjectId(button.objectId)!;
|
||||
expect(world.hitTest(entity, 10, 10)).toBe(true); // inside the 100×100 sprite
|
||||
expect(world.hitTest(entity, 10, 200)).toBe(true); // on the label parented below it
|
||||
expect(world.hitTest(entity, 400, 400)).toBe(false);
|
||||
|
||||
// A collider takes over the hit test for the whole subtree.
|
||||
stack.execute(new AddColliderCommand(scene.asset.id, button.objectId));
|
||||
stack.execute(new SetOverrideCommand(scene.asset.id, findScene(project, scene.asset.id)!.objects.at(-1)!.id, 'width', 20));
|
||||
world.syncFromModel();
|
||||
expect(world.hitTest(entity, 5, 5)).toBe(true);
|
||||
expect(world.hitTest(entity, 40, 0)).toBe(false); // outside the 20-wide collider
|
||||
expect(world.hitTest(entity, 10, 200)).toBe(false); // the label no longer answers
|
||||
|
||||
// Switched off, nothing of it can be hit.
|
||||
entity.props.enabled = false;
|
||||
expect(world.hitTest(entity, 5, 5)).toBe(false);
|
||||
});
|
||||
|
||||
it('reports pointer edges for exactly one tick', () => {
|
||||
const project = createEmptyProject('p', 'P');
|
||||
const stack = new CommandStack(project);
|
||||
const scene = new CreateAssetCommand('scene', 'Level');
|
||||
stack.execute(scene);
|
||||
const world = World.instantiate(project, scene.asset.id);
|
||||
world.movePointer(12, 34, true);
|
||||
world.setPointerDown(true);
|
||||
expect(world.pointer).toMatchObject({ x: 12, y: 34, inside: true, down: true, pressed: true });
|
||||
world.tick(0.1);
|
||||
expect(world.pointer).toMatchObject({ down: true, pressed: false, released: false });
|
||||
world.setPointerDown(false);
|
||||
expect(world.pointer).toMatchObject({ down: false, released: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('nodes added for game flow', () => {
|
||||
it('formats numbers and joins text', () => {
|
||||
expect(run('ToText', { value: 12.345, decimals: 1 })).toEqual({ text: '12.3' });
|
||||
expect(run('ToText', { value: 'x', decimals: 0 })).toEqual({ text: 'x' });
|
||||
expect(run('Join', { a: 'Coins: ', b: 3 })).toEqual({ text: 'Coins: 3' });
|
||||
expect(run('Modulo', { a: 125, b: 60 })).toEqual({ result: 5 });
|
||||
expect(run('Modulo', { a: 5, b: 0 })).toEqual({ result: 0 });
|
||||
expect(run('Floor', { value: 2.9 })).toEqual({ result: 2 });
|
||||
});
|
||||
|
||||
it('compares values and identifies assets', () => {
|
||||
const coin = handle({});
|
||||
coin.assetId = 'coin';
|
||||
expect(run('IsAsset', { object: coin }, { asset: 'coin' })).toEqual({ result: true });
|
||||
expect(run('IsAsset', { object: coin }, { asset: 'other' })).toEqual({ result: false });
|
||||
expect(run('IsAsset', { object: null }, { asset: 'coin' })).toEqual({ result: false });
|
||||
expect(run('Equals', { a: 'a', b: 'a' })).toEqual({ result: true });
|
||||
expect(run('Equals', { a: coin, b: coin })).toEqual({ result: true });
|
||||
expect(run('Equals', { a: coin, b: null })).toEqual({ result: false });
|
||||
expect(run('NameOf', { object: coin })).toEqual({ name: 'h' });
|
||||
});
|
||||
|
||||
it('switches objects on and off and asks for a restart', () => {
|
||||
const target = handle({ enabled: true });
|
||||
requireNodeDef('SetEnabled').run!({} as NodeContext, { object: target, enabled: false }, {});
|
||||
expect(target.props.enabled).toBe(false);
|
||||
|
||||
let restarted = false;
|
||||
requireNodeDef('RestartGame').run!({ restart: () => { restarted = true; } } as NodeContext, {}, {});
|
||||
expect(restarted).toBe(true);
|
||||
});
|
||||
|
||||
it('fires On click when the mouse is pressed on the owner', () => {
|
||||
const project = createEmptyProject('p', 'P');
|
||||
const stack = new CommandStack(project);
|
||||
const scene = new CreateAssetCommand('scene', 'Level');
|
||||
const sprite = new CreateAssetCommand('sprite', 'Button');
|
||||
const behavior = new CreateAssetCommand('behavior', 'OnPress');
|
||||
for (const c of [scene, sprite, behavior]) stack.execute(c);
|
||||
stack.execute(new AddVariableCommand(behavior.asset.id, 'clicks', 'number'));
|
||||
const click = new AddNodeCommand(behavior.asset.id, 'OnClick', 0, 0);
|
||||
const get = new AddNodeCommand(behavior.asset.id, 'GetVariable', 0, 100, { variable: 'clicks' });
|
||||
const one = new AddNodeCommand(behavior.asset.id, 'Number', 0, 200, { value: 1 });
|
||||
const add = new AddNodeCommand(behavior.asset.id, 'Add', 100, 150);
|
||||
const set = new AddNodeCommand(behavior.asset.id, 'SetVariable', 200, 0, { variable: 'clicks' });
|
||||
for (const c of [click, get, one, add, set]) stack.execute(c);
|
||||
for (const [from, fromPort, to, toPort] of [
|
||||
[click, 'exec', set, 'exec'],
|
||||
[get, 'value', add, 'a'],
|
||||
[one, 'value', add, 'b'],
|
||||
[add, 'result', set, 'value'],
|
||||
] as const) {
|
||||
stack.execute(new ConnectCommand(behavior.asset.id, { node: from.node.id, port: fromPort }, { node: to.node.id, port: toPort }));
|
||||
}
|
||||
const button = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 0, y: 0 });
|
||||
stack.execute(button);
|
||||
const logic = new PlaceAssetCommand(scene.asset.id, behavior.asset.id, { x: 0, y: 0 });
|
||||
stack.execute(logic);
|
||||
stack.execute(new SetParentCommand(scene.asset.id, logic.objectId, button.objectId));
|
||||
|
||||
const world = World.instantiate(project, scene.asset.id);
|
||||
world.addSystem(new BehaviorSystem(project, new Input(), null));
|
||||
const entity = world.findByObjectId(logic.objectId)!;
|
||||
|
||||
world.movePointer(500, 500, true); // beside the button
|
||||
world.setPointerDown(true);
|
||||
world.tick(0.1);
|
||||
expect(entity.props.clicks).toBe(0);
|
||||
|
||||
world.setPointerDown(false);
|
||||
world.movePointer(10, 10, true); // on the button
|
||||
world.setPointerDown(true);
|
||||
world.tick(0.1);
|
||||
expect(entity.props.clicks).toBe(1);
|
||||
world.tick(0.1); // still held – the press was one frame
|
||||
expect(entity.props.clicks).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
/** A 100×100 wall at x = 200 and a 100×100 mover at the origin, both with box colliders. */
|
||||
function collisionSetup() {
|
||||
const project = createEmptyProject('p', 'P');
|
||||
const stack = new CommandStack(project);
|
||||
const scene = new CreateAssetCommand('scene', 'Level');
|
||||
const sprite = new CreateAssetCommand('sprite', 'Box');
|
||||
stack.execute(scene);
|
||||
stack.execute(sprite);
|
||||
const moverObject = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 0, y: 0 });
|
||||
const wallObject = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 200, y: 0 });
|
||||
stack.execute(moverObject);
|
||||
stack.execute(wallObject);
|
||||
const moverCollider = new AddColliderCommand(scene.asset.id, moverObject.objectId);
|
||||
const wallCollider = new AddColliderCommand(scene.asset.id, wallObject.objectId);
|
||||
stack.execute(moverCollider);
|
||||
stack.execute(wallCollider);
|
||||
const world = World.instantiate(project, scene.asset.id);
|
||||
return {
|
||||
world,
|
||||
mover: world.findByObjectId(moverObject.objectId)!,
|
||||
wall: world.findByObjectId(wallCollider.objectId)!,
|
||||
};
|
||||
}
|
||||
@@ -9,10 +9,12 @@ import type { BehaviorAsset } from '../../src/core/model/Asset';
|
||||
import { createEmptyProject } from '../../src/core/model/Project';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
|
||||
function entity(name: string, x = 0): EntityHandle {
|
||||
return { id: name, name, x, y: 0, z: 0, props: {}, parent: null };
|
||||
function entity(name: string, x = 0, assetId = `asset-${name}`): EntityHandle {
|
||||
return { id: name, assetId, name, x, y: 0, z: 0, props: {}, parent: null };
|
||||
}
|
||||
|
||||
const NO_POINTER = { x: 0, y: 0, inside: false, down: false, pressed: false, released: false };
|
||||
|
||||
function context(overrides: Partial<NodeContext> = {}): NodeContext & { logs: string[]; stopped: boolean } {
|
||||
const logs: string[] = [];
|
||||
const ctx = {
|
||||
@@ -25,6 +27,8 @@ function context(overrides: Partial<NodeContext> = {}): NodeContext & { logs: st
|
||||
stopped: false,
|
||||
findByName: () => null,
|
||||
isKeyDown: () => false,
|
||||
pointer: { ...NO_POINTER },
|
||||
hitTest: () => false,
|
||||
isTouching: () => false,
|
||||
moveAndSlide: (target: EntityHandle, dx: number, dy: number) => {
|
||||
target.x += dx;
|
||||
@@ -35,6 +39,7 @@ function context(overrides: Partial<NodeContext> = {}): NodeContext & { logs: st
|
||||
spawn: () => null,
|
||||
destroy: () => {},
|
||||
stop: () => { ctx.stopped = true; },
|
||||
restart: () => {},
|
||||
...overrides,
|
||||
};
|
||||
return ctx;
|
||||
@@ -326,7 +331,7 @@ describe('variables', () => {
|
||||
|
||||
stack.execute(new RenameVariableCommand('mover', 'speed', 'velocity'));
|
||||
expect(behavior.variables[0]!.key).toBe('velocity');
|
||||
expect(behavior.props).toEqual({ velocity: 0 });
|
||||
expect(behavior.props).toEqual({ enabled: true, velocity: 0 });
|
||||
expect(object.overrides).toEqual({ velocity: 5 });
|
||||
expect(behavior.graph.nodes.find((n) => n.id === nodes.speed.node.id)!.params.variable).toBe('velocity');
|
||||
stack.undo();
|
||||
@@ -373,7 +378,7 @@ describe('behavior serialization', () => {
|
||||
const loaded = deserializeProject(serializeProject(project)).assets.find((a) => a.id === 'mover') as BehaviorAsset;
|
||||
expect(loaded.variables).toEqual(behavior.variables);
|
||||
expect(loaded.graph).toEqual(behavior.graph);
|
||||
expect(loaded.props).toEqual({ speed: 0 });
|
||||
expect(loaded.props).toEqual({ enabled: true, speed: 0 });
|
||||
|
||||
const raw = serializeProject(project) as { assets: { graph?: { nodes: unknown[]; edges: unknown[] } }[] };
|
||||
raw.assets[0]!.graph!.nodes.push({ id: 'zzz', type: 'DoesNotExist', x: 0, y: 0, params: {} });
|
||||
|
||||
@@ -133,8 +133,8 @@ describe('runtime hierarchy', () => {
|
||||
});
|
||||
|
||||
it('Get position exposes local and world coordinates, Parent walks up', () => {
|
||||
const parent = { id: 'p', name: 'p', x: 100, y: 20, z: 0, props: {}, parent: null };
|
||||
const child = { id: 'c', name: 'c', x: 5, y: 5, z: 0, props: {}, parent };
|
||||
const parent = { id: 'p', assetId: 'a-p', name: 'p', x: 100, y: 20, z: 0, props: {}, parent: null };
|
||||
const child = { id: 'c', assetId: 'a-c', name: 'c', x: 5, y: 5, z: 0, props: {}, parent };
|
||||
const graph = { nodes: [
|
||||
{ id: 'start', type: 'OnStart', x: 0, y: 0, params: {} },
|
||||
{ id: 'self', type: 'Self', x: 0, y: 0, params: {} },
|
||||
@@ -156,7 +156,7 @@ describe('runtime hierarchy', () => {
|
||||
{ id: 'e9', from: { node: 'ppos', port: 'worldX' }, to: { node: 'log3', port: 'message' } },
|
||||
] };
|
||||
const logs: string[] = [];
|
||||
const ctx: NodeContext = { self: child, owner: parent, variables: {}, time: 0, dt: 0, findByName: () => null, isKeyDown: () => false, isTouching: () => false, moveAndSlide: () => ({ blockedX: false, blockedY: false }), log: (m) => logs.push(m), spawn: () => null, destroy: () => {}, stop: () => {} };
|
||||
const ctx: NodeContext = { self: child, owner: parent, variables: {}, time: 0, dt: 0, findByName: () => null, isKeyDown: () => false, pointer: { x: 0, y: 0, inside: false, down: false, pressed: false, released: false }, hitTest: () => false, isTouching: () => false, moveAndSlide: () => ({ blockedX: false, blockedY: false }), log: (m) => logs.push(m), spawn: () => null, destroy: () => {}, stop: () => {}, restart: () => {} };
|
||||
new GraphInterpreter(graph).fire('OnStart', ctx);
|
||||
expect(logs).toEqual(['105', '5', '100']);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('PropertySchema', () => {
|
||||
});
|
||||
|
||||
it('provides defaults for every key', () => {
|
||||
expect(defaultsFor('sprite')).toEqual({ width: 100, height: 100, color: 0xe0553f });
|
||||
expect(defaultsFor('sprite')).toEqual({ enabled: true, width: 100, height: 100, color: 0xe0553f });
|
||||
});
|
||||
|
||||
it('coerces values against the definition', () => {
|
||||
@@ -33,14 +33,14 @@ describe('resolve', () => {
|
||||
it('fills asset props with defaults and ignores unknown keys', () => {
|
||||
const sprite = buildAsset('sprite', 'Box');
|
||||
sprite.props = { width: 50, bogus: 1 };
|
||||
expect(resolveAssetProps(sprite)).toEqual({ width: 50, height: 100, color: 0xe0553f });
|
||||
expect(resolveAssetProps(sprite)).toEqual({ enabled: true, width: 50, height: 100, color: 0xe0553f });
|
||||
});
|
||||
|
||||
it('applies sparse overrides on top of asset values', () => {
|
||||
const sprite = buildAsset('sprite', 'Box');
|
||||
sprite.props.color = 0x00ff00;
|
||||
const object = createSceneObject('o1', sprite.id, { x: 10, y: 20 });
|
||||
expect(resolveSceneObjectProps(sprite, object)).toEqual({ width: 100, height: 100, color: 0x00ff00 });
|
||||
expect(resolveSceneObjectProps(sprite, object)).toEqual({ enabled: true, width: 100, height: 100, color: 0x00ff00 });
|
||||
|
||||
object.overrides.color = 0x0000ff;
|
||||
expect(resolveSceneObjectProps(sprite, object).color).toBe(0x0000ff);
|
||||
|
||||
@@ -56,6 +56,6 @@ describe('serialize', () => {
|
||||
templateId: '',
|
||||
});
|
||||
expect(project.assets[1]!.name).toBe('sprite');
|
||||
expect(project.assets[1]!.props).toEqual({ width: 10, height: 100, color: 0xe0553f });
|
||||
expect(project.assets[1]!.props).toEqual({ enabled: true, width: 10, height: 100, color: 0xe0553f });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user