Files
feuerwerk/tools/seed-e2e.mjs
T
admin 5671a38044
game-maker / suite (push) Successful in 12m1s
A game that stays silent is only half a game, so a sound is an asset like any other (#45)
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.
2026-09-15 07:54:28 +02:00

125 lines
4.9 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 { 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);
/**
* 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);