137 lines
5.5 KiB
JavaScript
137 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Writes the fixture scene tools/e2e.mjs asserts against: two top-level crates with a collider
|
||
* child each, a camera, the Mover behavior owning the red crate and a Spawner. The objects are
|
||
* uniquely named and positioned exactly where the checks expect them.
|
||
*
|
||
* Usage: node tools/seed-e2e.mjs [projectId] [baseUrl]
|
||
*/
|
||
import { copyFileSync, mkdirSync, readdirSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { sineWav } from './lib/audio.mjs';
|
||
import { edge, node, port, sceneObject, variable } from './lib/builders.mjs';
|
||
import { PROJECTS, cliArgs, uploadFont, uploadSound, writeProject } from './lib/projects.mjs';
|
||
import { adjustColorsShader } from './lib/shaders.mjs';
|
||
|
||
const { projectId, baseUrl } = cliArgs('seed-e2e.mjs', PROJECTS.fixture);
|
||
|
||
/**
|
||
* The plugins that ship with the repository, where the editor loads them from – the build does the
|
||
* same, this makes a run after a plain checkout independent of it. The server is this checkout
|
||
* (locally and in CI), so a file copy is enough.
|
||
*/
|
||
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||
mkdirSync(`${ROOT}data/plugins`, { recursive: true });
|
||
for (const file of readdirSync(`${ROOT}plugins`).filter((f) => f.endsWith('.js')))
|
||
copyFileSync(`${ROOT}plugins/${file}`, `${ROOT}data/plugins/${file}`);
|
||
|
||
/**
|
||
* Two typefaces for the font step to pick and to drop. They are put on the server but not made
|
||
* into assets: the step is about the picker and the drop making them. The files come from the
|
||
* system rather than the repo – the CI image installs the same DejaVu package the host has,
|
||
* because the checks measure text widths (see tools/ci/Dockerfile).
|
||
*/
|
||
const SYSTEM_FONTS = '/usr/share/fonts/truetype/dejavu';
|
||
await uploadFont(baseUrl, `${SYSTEM_FONTS}/DejaVuSerif.ttf`);
|
||
await uploadFont(baseUrl, `${SYSTEM_FONTS}/DejaVuSansMono.ttf`);
|
||
|
||
/**
|
||
* Two sounds for the sound step to pick and to drop (#45), put on the server without being made
|
||
* into assets, exactly as the typefaces are. They are generated rather than read: nothing
|
||
* guarantees an audio file on both the host and in the CI image, and the two differ in length so a
|
||
* check can tell which one it got without listening – the browser runs muted.
|
||
*/
|
||
await uploadSound(baseUrl, sineWav(0.25, 880), 'blip.wav');
|
||
await uploadSound(baseUrl, sineWav(0.75, 220), 'drone.wav');
|
||
|
||
/**
|
||
* "Mover" in five nodes: every frame, move the owner along the normalized arrow-key axis,
|
||
* scaled by the `speed` variable per second (On update's dt feeds the dt input). "Collide with
|
||
* solids" stops the owner's solid colliders at other solid colliders.
|
||
*/
|
||
const moverGraph = {
|
||
nodes: [
|
||
node('n-update', 'OnUpdate', 0, 0),
|
||
node('n-owner', 'Owner', 0, 120),
|
||
node('n-axis', 'AxisInput', 0, 200),
|
||
node('n-speed', 'Variables', 0, 400, {}, [port('speed', 'number')]),
|
||
node('n-move', 'Move', 340, 0, { collide: true }),
|
||
],
|
||
edges: [
|
||
edge('e1', 'n-update', 'exec', 'n-move', 'exec'),
|
||
edge('e6', 'n-update', 'dt', 'n-move', 'dt'),
|
||
edge('e2', 'n-owner', 'object', 'n-move', 'object'),
|
||
edge('e3', 'n-axis', 'x', 'n-move', 'dx'),
|
||
edge('e4', 'n-axis', 'y', 'n-move', 'dy'),
|
||
edge('e5', 'n-speed', 'speed', 'n-move', 'scale'),
|
||
],
|
||
};
|
||
|
||
/** "Spawner": pressing Space places a new Crate at world (0, 150) and logs its name. */
|
||
const spawnerGraph = {
|
||
nodes: [
|
||
node('s-key', 'OnKeyDown', 0, 0, { key: ' ' }),
|
||
node('s-spawn', 'Spawn', 260, 0, { asset: 'crate', y: 150 }),
|
||
node('s-log', 'Log', 520, 0),
|
||
],
|
||
edges: [
|
||
edge('s-e1', 's-key', 'exec', 's-spawn', 'exec'),
|
||
edge('s-e2', 's-spawn', 'exec', 's-log', 'exec'),
|
||
edge('s-e3', 's-spawn', 'object', 's-log', 'message'),
|
||
],
|
||
};
|
||
|
||
const project = {
|
||
formatVersion: 2,
|
||
id: projectId,
|
||
name: 'E2E fixture',
|
||
activeSceneId: 'scene1',
|
||
assets: [
|
||
{
|
||
id: 'scene1',
|
||
type: 'scene',
|
||
name: 'Level 1',
|
||
props: { backgroundColor: 0x22252c },
|
||
objects: [
|
||
sceneObject('o1', 'crate', { x: -150 }),
|
||
sceneObject('o2', 'crate', {
|
||
name: 'Blue crate',
|
||
x: 120,
|
||
y: -80,
|
||
overrides: { width: 140, color: 0x4488ff },
|
||
}),
|
||
sceneObject('o3', 'cam'),
|
||
// The Mover is parented to the red crate (its owner); its position is local to it.
|
||
sceneObject('o4', 'mover', { y: 110, parentId: 'o1' }),
|
||
// Both crates carry the shared collider as a centred child; the blue one is widened to
|
||
// match its 140-wide sprite override.
|
||
sceneObject('oc1', 'collider', { parentId: 'o1' }),
|
||
sceneObject('oc2', 'collider', { parentId: 'o2', overrides: { width: 140 } }),
|
||
// Top-level spawner (no owner) left of the red crate: Space spawns a crate.
|
||
sceneObject('o5', 'spawner', { x: -300 }),
|
||
],
|
||
},
|
||
{ id: 'crate', type: 'sprite', name: 'Crate', props: { width: 100, height: 60, color: 0xe0553f } },
|
||
{ id: 'cam', type: 'camera', name: 'Main camera', props: { width: 640, height: 360, zoom: 1 } },
|
||
{
|
||
id: 'collider',
|
||
type: 'collider',
|
||
name: 'Crate collider',
|
||
props: { shape: 'box', width: 100, height: 60, radius: 50, isTrigger: false },
|
||
},
|
||
{
|
||
id: 'mover',
|
||
type: 'behavior',
|
||
name: 'Mover',
|
||
props: { speed: 200 },
|
||
variables: [variable('speed', 'number', 0)],
|
||
graph: moverGraph,
|
||
},
|
||
{ id: 'spawner', type: 'behavior', name: 'Spawner', props: {}, variables: [], graph: spawnerGraph },
|
||
// Worn by nothing here; the shader step assigns it and reads the pixels.
|
||
adjustColorsShader(),
|
||
],
|
||
};
|
||
|
||
await writeProject(projectId, baseUrl, project);
|