Let a game hand values to the parameters of a sound FX #311
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Failing after 14m15s

A sound FX played the same every time: its numbers were settings of
the nodes. Now the Input declares parameters with a default, every
number of Gain, Pitch and EQ is an input they can feed, and a Play
sound hands in values for them.

- Input declares number parameters in the inspector, each with a
  Default (DeclaredPort.default, SetPortDefaultCommand). They are
  outputs of the Input. Input and Output count as terminals now, one
  of each per graph.
- Gain, Pitch and EQ take their numbers at inputs under the keys the
  settings had, so saved graphs sound as before without a migration.
  Each value is held to its range however it arrives.
- The compiler gives an FxPlan with the parameters still open.
  fxChainOf fills them in when a voice starts. The player still only
  sees an FxChain.
- New behavior node Sound FX parameters: names an FX and picks its
  parameters like a Variables node picks variables. Each picked one is
  a number input that starts at the FX's default. Its output has the
  new port type fxparams, which only fits Play sound's fx params.
- Play sound warns when the parameters name another FX than the one
  it plays. Names the FX does not know are left out.
- Renaming a parameter at the Input renames it in every Sound FX
  parameters node naming that FX, edges and values included.
- The Sound FX tab has a number field per parameter in its header.
  Play uses those values, they are never written into the asset.
- A value counts from the start of a voice. Changing a playing voice
  is #315, a Mix node is #316.

Tests:
- soundfx.test.ts 22 (+11): parameters in the plan, held values, a
  0-by-default stage, EQ band, save with defaults, wrong port types,
  default command with undo, Sound FX parameters into Play sound,
  defaults of picked ones, node problems, mismatch, rename with undo
- node-catalog 368 (+3): Play sound fx params, Sound FX parameters
- e2e 34: ALL PASSED - 28 checks (+2: header field plays depth 5,
  a Sound FX parameters node gives the game's voice depth 7)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 240 test
  files, 3636 tests green
- Counter-check: with fxChainOf ignoring the values and the rename
  not followed, 5 of the new tests fail.
- Browser probe: header fields, inspector Default and the picked
  parameter looked at in two screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-05 17:26:20 +02:00
1 parent de14f0f9e2
commit 183f356c99
25 files changed
+785 -99

No files matched your search

+40 -6
View File
@@ -272,7 +272,9 @@ export async function sound(t) {
// 34g-3. A sound FX asset (#311): "+ → Sound FX" opens its tab, which says what keeps the graph
// from compiling; built into Input → Pitch → Gain → Output, the header plays the sound
// through it, and a `Play sound` naming it sends the game's voice the same way.
// through it, and a `Play sound` naming it sends the game's voice the same way. The pitch
// comes from a parameter `depth` of the Input: the header has a field for it, and a
// `Sound FX parameters` node hands the game's value in.
await showTab('scene');
await clickText('+');
await pickAssetType(t, 'Sound FX');
@@ -309,8 +311,8 @@ export async function sound(t) {
fx.name = 'Low';
fx.graph = ${JSON.stringify({
nodes: [
node('fx-in', 'SoundFxInput', 0, 0),
node('fx-pitch', 'SoundFxPitch', 220, 0, { semitones: -12 }),
node('fx-in', 'SoundFxInput', 0, 0, {}, [{ key: 'depth', kind: 'number', default: -12 }]),
node('fx-pitch', 'SoundFxPitch', 220, 0),
node('fx-gain', 'SoundFxGain', 440, 0, { gain: -6 }),
node('fx-out', 'SoundFxOutput', 660, 0),
],
@@ -318,12 +320,15 @@ export async function sound(t) {
edge('fx-e1', 'fx-in', 'out', 'fx-pitch', 'in'),
edge('fx-e2', 'fx-pitch', 'out', 'fx-gain', 'in'),
edge('fx-e3', 'fx-gain', 'out', 'fx-out', 'in'),
edge('fx-e4', 'fx-in', 'depth', 'fx-pitch', 'semitones'),
],
})};`);
const fxBuilt = await json('{ info: __gm.soundfx.info(), preview: __gm.soundfx.preview() }');
const fxBuilt = await json(
'{ info: __gm.soundfx.info(), preview: __gm.soundfx.preview(), params: __gm.soundfx.params() }',
);
check(
'Input → Pitch → Gain → Output compiles to two effects, and Play is offered',
fxBuilt.info === '2 effects' && fxBuilt.preview.play === true,
'Input → Pitch → Gain → Output compiles to two effects, Play is offered, depth starts at its default',
fxBuilt.info === '2 effects' && fxBuilt.preview.play === true && fxBuilt.params.depth === -12,
JSON.stringify(fxBuilt),
);
await evaluate('__gm.sound.stopAll()');
@@ -337,6 +342,16 @@ export async function sound(t) {
JSON.stringify(heardFx),
);
await evaluate('__gm.sound.stopAll()');
await evaluate('__gm.soundfx.setParam("depth", 5)');
const beforeParam = await stats();
await click(fxPlayAt.x, fxPlayAt.y);
const heardParam = await waitFor(stats, (s) => s.started > beforeParam.started);
check(
'the header plays with the value its depth field is set to',
heardParam.last?.pitch === 5 && heardParam.last.stages === 1,
JSON.stringify(heardParam),
);
await evaluate('__gm.sound.stopAll()');
await showTab('scene');
await mutate(`
const graph = __gm.state.project.assets.find((a) => a.id === 'sn-logic').graph;
@@ -352,6 +367,25 @@ export async function sound(t) {
await stop();
await mutate(`
const graph = __gm.state.project.assets.find((a) => a.id === 'sn-logic').graph;
graph.nodes.push(${JSON.stringify(
node('sn-fxp', 'SoundFxParameters', 0, 120, { soundFxAsset: fxId, depth: 7 }, [
{ key: 'depth', kind: 'number' },
]),
)});
graph.edges.push(${JSON.stringify(edge('sn-e-fxp', 'sn-fxp', 'params', 'sn-play', 'fxParams'))});`);
const beforeGameParam = await stats();
await play();
const gameParam = await waitFor(stats, (s) => s.started > beforeGameParam.started);
check(
'a Sound FX parameters node hands depth 7 to the game’s voice',
gameParam.last?.pitch === 7 && gameParam.last.stages === 1,
JSON.stringify(gameParam),
);
await stop();
await mutate(`
const graph = __gm.state.project.assets.find((a) => a.id === 'sn-logic').graph;
graph.nodes = graph.nodes.filter((n) => n.id !== 'sn-fxp');
graph.edges = graph.edges.filter((e) => e.id !== 'sn-e-fxp');
Object.assign(graph.nodes.find((n) => n.id === 'sn-play').params, { loop: '', fx: '' });
const project = __gm.state.project;
project.assets = project.assets.filter((a) => a.id !== ${JSON.stringify(fxId)});`);