game-maker / suite (push) Successful in 12m1s
Sounds follow the road fonts already paved: the file goes to the server content-addressed under its sha256, the project keeps a named asset that points at it, and nothing is ever re-encoded. The format is read from the file's own signature, not from what the browser claims - the same .m4a arrives as audio/mp4, audio/x-m4a or as nothing at all. Playing is two nodes. "Play sound" takes the asset and may raise or lower its volume; every call is its own voice, so two jumps in a row are two jumps heard. Its loop input is a choice of three - from the asset, on, off - because a checkbox has to spend one of its two positions on "not set" and would lose a case: an asset that loops could then never be played once. Below the graph core stays browser-free: NodeContext hands out an asset id, BehaviorSystem resolves it against the project and folds the asset's own volume and loop into the call, and only viewport/SoundLibrary knows about Web Audio. A run without a sink plays nothing instead of throwing. The CI browser is muted, so the ear is no witness: the debug hook counts started and stopped voices, and step 34 reads that counter.
120 lines
4.6 KiB
TypeScript
120 lines
4.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import {
|
||
CreateAssetCommand,
|
||
SetAssetPropertyCommand,
|
||
SetMachineScopeCommand,
|
||
} from '../../src/core/commands/assetCommands';
|
||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import { AddVariableCommand } from '../../src/core/commands/variableCommands';
|
||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||
import { makeBehavior, makeSpriteScene, place, roundTrip } from '../helpers';
|
||
|
||
describe('serialize', () => {
|
||
it('round-trips a project', () => {
|
||
const f = makeSpriteScene();
|
||
const box = place(f, f.spriteId, { x: 7, y: 8 });
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, box, 'width', 33));
|
||
expect(roundTrip(f.project)).toEqual(f.project);
|
||
});
|
||
|
||
/** A behavior's own rows are not the common ones: coercing against those alone dropped the value. */
|
||
it('keeps a behavior running in the editor across a save', () => {
|
||
const f = makeSpriteScene();
|
||
const behavior = makeBehavior(f, 'B');
|
||
f.stack.execute(new SetAssetPropertyCommand(behavior.id, 'runInEditor', true));
|
||
const loaded = roundTrip(f.project).assets.find((a) => a.id === behavior.id)!;
|
||
expect(loaded.props.runInEditor).toBe(true);
|
||
});
|
||
|
||
/** The declarations of a Variables asset and their starting values are the whole asset. */
|
||
it('round-trips a Variables asset and the scope of a state machine', () => {
|
||
const f = makeSpriteScene();
|
||
const shared = new CreateAssetCommand('variables', 'Score');
|
||
f.stack.execute(shared);
|
||
f.stack.execute(new AddVariableCommand(shared.asset.id, 'score', 'number'));
|
||
f.stack.execute(new SetAssetPropertyCommand(shared.asset.id, 'score', 7));
|
||
const machine = new CreateAssetCommand('statemachine', 'M');
|
||
f.stack.execute(machine);
|
||
f.stack.execute(new SetMachineScopeCommand(machine.asset.id, 'global'));
|
||
expect(roundTrip(f.project)).toEqual(f.project);
|
||
});
|
||
|
||
/** `local` is the absence of the field, so a project made before scopes existed saves back byte-alike. */
|
||
it('writes no scope for a local machine and reads an unknown one as local', () => {
|
||
const f = makeSpriteScene();
|
||
makeBehavior(f, 'B', ['score']);
|
||
f.stack.execute(new CreateAssetCommand('statemachine', 'M'));
|
||
expect(JSON.stringify(serializeProject(f.project))).not.toContain('scope');
|
||
|
||
const loaded = deserializeProject({
|
||
assets: [{ id: 'm1', type: 'statemachine', name: 'M', scope: 'planet' }],
|
||
});
|
||
const machine = loaded.assets[0]!;
|
||
if (machine.type !== 'statemachine') throw new Error('wrong types');
|
||
expect(machine.scope).toBeUndefined();
|
||
});
|
||
|
||
it('repairs broken documents', () => {
|
||
const project = deserializeProject(
|
||
{
|
||
name: 'Broken',
|
||
activeSceneId: 'missing',
|
||
assets: [
|
||
{
|
||
id: 's1',
|
||
type: 'scene',
|
||
name: 'Scene',
|
||
props: { backgroundColor: '#000000', junk: 1 },
|
||
objects: [
|
||
{
|
||
id: 'o1',
|
||
assetId: 'sp1',
|
||
position: { x: '5', y: null },
|
||
overrides: { color: '#00ff00', width: -3, foo: 1 },
|
||
},
|
||
{ id: 'o2', assetId: 'nope', position: {} },
|
||
{ id: 'o3', assetId: 's1', position: {} },
|
||
],
|
||
},
|
||
{ id: 'sp1', type: 'sprite', props: { width: 10 } },
|
||
// An asset type this build does not know (a newer document, a renamed type). It used to
|
||
// be `sound`, which this build does know since #45 – hence a type nothing has claimed.
|
||
{ id: 'x', type: 'video' },
|
||
{ type: 'camera' },
|
||
],
|
||
},
|
||
'fallback-id',
|
||
);
|
||
expect(project.id).toBe('fallback-id');
|
||
expect(project.activeSceneId).toBe('s1');
|
||
expect(project.assets.map((a) => a.id)).toEqual(['s1', 'sp1']);
|
||
const scene = project.assets[0]!;
|
||
expect(scene.type === 'scene' && scene.props).toEqual({ backgroundColor: 0 });
|
||
const objects = scene.type === 'scene' ? scene.objects : [];
|
||
expect(objects).toHaveLength(1);
|
||
expect(objects[0]).toEqual({
|
||
id: 'o1',
|
||
assetId: 'sp1',
|
||
name: '',
|
||
position: { x: 5, y: 0, z: 0 },
|
||
editorOffset: { x: 0, y: 0 },
|
||
overrides: { color: 0x00ff00, width: 1 },
|
||
parentId: '',
|
||
instanceEdits: {},
|
||
});
|
||
expect(project.assets[1]!.name).toBe('sprite');
|
||
expect(project.assets[1]!.props).toEqual({
|
||
enabled: true,
|
||
scale: 1,
|
||
rotation: 0,
|
||
mirrorX: false,
|
||
mirrorY: false,
|
||
width: 10,
|
||
height: 100,
|
||
color: 0xffffff,
|
||
texture: '',
|
||
shader: '',
|
||
});
|
||
});
|
||
});
|