Files
feuerwerk/tools/seed-e2e.mjs
T
2026-09-17 19:00:03 +02:00

137 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);