game-maker / suite (push) Failing after 3m59s
A shader is an asset carrying a node graph (Texture color, Output, Property, Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite wears a shader through its Shader picker, and the rows below it are the graph's Property nodes. Property schema, resolution and deviation take the project because a sprite's rows now live in another asset. The built-in registry of the prototype is gone; "Adjust colors" ships as a graph in the fixture and the three example games. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
124 lines
5.6 KiB
TypeScript
124 lines
5.6 KiB
TypeScript
/**
|
||
* Editing one field of a struct rebuilds the whole bundle – an override is one value per property
|
||
* key, not per field. The question here is what the untouched fields are filled from: an override
|
||
* is sparse and starts out unset, so only the *resolved* value carries the asset's own numbers.
|
||
*/
|
||
import { beforeEach, describe, expect, it } from 'vitest';
|
||
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
|
||
import { PlaceAssetCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import { AddStructFieldCommand, AddVariableCommand } from '../../src/core/commands/variableCommands';
|
||
import type { PropertyValue } from '../../src/core/model/Properties';
|
||
import type { SceneObject } from '../../src/core/model/SceneObject';
|
||
import { createEmptyProject, findAsset, findScene } from '../../src/core/model/Project';
|
||
import { requirePropDef } from '../../src/core/commands/require';
|
||
import { resolveAssetProps, resolveSceneObjectProps } from '../../src/core/properties/resolve';
|
||
import { structFieldsOf } from '../../src/editor/inspector/previewHandlers';
|
||
import { EditorState } from '../../src/editor/state';
|
||
|
||
const FIELDS = [
|
||
{ key: 'x', kind: 'number' },
|
||
{ key: 'y', kind: 'number' },
|
||
{ key: 'tag', kind: 'text' },
|
||
] as const;
|
||
|
||
function setup() {
|
||
const state = new EditorState();
|
||
state.loadProject(createEmptyProject('p', 'P'), 1);
|
||
const scene = new CreateAssetCommand('scene', 'Level');
|
||
const behavior = new CreateAssetCommand('behavior', 'B');
|
||
// A rejected command only returns false, so an unasserted setup would let the tests below pass
|
||
// for the wrong reason.
|
||
expect(state.execute(scene)).toBe(true);
|
||
expect(state.execute(behavior)).toBe(true);
|
||
expect(state.execute(new AddVariableCommand(behavior.asset.id, 'piece', 'struct'))).toBe(true);
|
||
for (const field of FIELDS)
|
||
expect(state.execute(new AddStructFieldCommand(behavior.asset.id, 'piece', { ...field }))).toBe(true);
|
||
// The asset's own values: what the inspector shows for an object that overrides nothing.
|
||
expect(
|
||
state.execute(new SetAssetPropertyCommand(behavior.asset.id, 'piece', { x: 3, y: 4, tag: 'T' })),
|
||
).toBe(true);
|
||
const place = new PlaceAssetCommand(scene.asset.id, behavior.asset.id, { x: 0, y: 0 });
|
||
expect(state.execute(place)).toBe(true);
|
||
return { state, sceneId: scene.asset.id, behaviorId: behavior.asset.id, objectId: place.objectId };
|
||
}
|
||
|
||
describe('editing one field of a struct', () => {
|
||
let f: ReturnType<typeof setup>;
|
||
beforeEach(() => {
|
||
f = setup();
|
||
});
|
||
|
||
const asset = () => findAsset(f.state.project, f.behaviorId)!;
|
||
const object = (): SceneObject =>
|
||
findScene(f.state.project, f.sceneId)!.objects.find((o) => o.id === f.objectId)!;
|
||
const structDef = () => {
|
||
const def = requirePropDef(f.state.project, asset(), 'piece');
|
||
if (def.kind !== 'struct') throw new Error('piece is not a struct');
|
||
return def;
|
||
};
|
||
|
||
/** The spec `objectSection` builds for an override, resolved value included. */
|
||
const objectHandlers = () =>
|
||
structFieldsOf<SceneObject>({ state: f.state }, structDef(), {
|
||
target: object,
|
||
read: (o) => o.overrides.piece,
|
||
current: (o) => resolveSceneObjectProps(f.state.project, asset(), o).piece,
|
||
write: (o, value) => (o.overrides.piece = value),
|
||
remove: (o) => delete o.overrides.piece,
|
||
commit: (value) => new SetOverrideCommand(f.sceneId, f.objectId, 'piece', value),
|
||
});
|
||
|
||
it('fills the untouched fields of a first override from the asset, not from an empty bundle', () => {
|
||
objectHandlers()({ key: 'x', kind: 'number' }).onChange(9);
|
||
expect(object().overrides.piece).toEqual({ x: 9, y: 4, tag: 'T' });
|
||
});
|
||
|
||
it('leaves the others alone on a second edit, and undo goes back one field at a time', () => {
|
||
objectHandlers()({ key: 'x', kind: 'number' }).onChange(9);
|
||
objectHandlers()({ key: 'tag', kind: 'text' }).onChange('hi');
|
||
expect(object().overrides.piece).toEqual({ x: 9, y: 4, tag: 'hi' });
|
||
|
||
f.state.commands.undo();
|
||
expect(object().overrides.piece).toEqual({ x: 9, y: 4, tag: 'T' });
|
||
f.state.commands.undo();
|
||
expect(object().overrides.piece).toBeUndefined();
|
||
});
|
||
|
||
it('builds on the preview while typing, and still undoes to before it', () => {
|
||
const handlers = objectHandlers()({ key: 'x', kind: 'number' });
|
||
handlers.onInput(1);
|
||
handlers.onInput(12);
|
||
handlers.onChange(12);
|
||
expect(object().overrides.piece).toEqual({ x: 12, y: 4, tag: 'T' });
|
||
|
||
f.state.commands.undo();
|
||
expect(object().overrides.piece).toBeUndefined();
|
||
});
|
||
|
||
it('copies those values in rather than keeping them tied to the asset', () => {
|
||
objectHandlers()({ key: 'x', kind: 'number' }).onChange(9);
|
||
f.state.execute(new SetAssetPropertyCommand(f.behaviorId, 'piece', { x: 3, y: 99, tag: 'T' }));
|
||
// The whole bundle is overridden now, so the asset moving on no longer reaches y.
|
||
expect(resolveSceneObjectProps(f.state.project, asset(), object()).piece).toEqual({
|
||
x: 9,
|
||
y: 4,
|
||
tag: 'T',
|
||
});
|
||
});
|
||
|
||
it('takes the stored value where that already is the resolved one – the dense asset bag', () => {
|
||
const handlers = structFieldsOf<{ props: Record<string, PropertyValue> }>(
|
||
{ state: f.state },
|
||
structDef(),
|
||
{
|
||
target: () => asset(),
|
||
read: (a) => a.props.piece,
|
||
write: (a, value) => (a.props.piece = value),
|
||
commit: (value) => new SetAssetPropertyCommand(f.behaviorId, 'piece', value),
|
||
},
|
||
);
|
||
handlers({ key: 'y', kind: 'number' }).onChange(8);
|
||
expect(resolveAssetProps(f.state.project, asset()).piece).toEqual({ x: 3, y: 8, tag: 'T' });
|
||
});
|
||
});
|