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

+51
View File
@@ -1,6 +1,7 @@
import {
BEHAVIOR_PARAM,
declaresPropertyPorts,
declaresSoundFxPorts,
declaresVariablePorts,
type DeclaredPort,
findNode,
@@ -17,6 +18,8 @@ import {
OBJECT_BEHAVIOR_PARAM,
propertyTargetOf,
SHADER_CALL,
SOUND_FX_INPUT,
soundFxSourceOf,
variablesSourceOf,
} from '../graph';
import { isGraphAsset } from '../model/Asset';
@@ -342,6 +345,54 @@ export class RenamePortCommand extends RenameCommand {
if (ports.some((p) => p.key === to)) throw new CommandError(`"${to}" already exists`);
node.ports = ports.map((p) => (p.key === from ? { ...p, key: to } : p));
for (const site of sites) carryPort(site, from, to);
if (node.type === SOUND_FX_INPUT) renameSoundFxParamPorts(project, this.assetId, from, to);
}
}
/** The default of a sound FX parameter (#311): what it is wherever no `Play sound` hands a value in. */
export class SetPortDefaultCommand implements Command {
readonly label: string;
private was: number | undefined;
constructor(
private readonly assetId: string,
private readonly nodeId: string,
private readonly key: string,
private readonly value: number,
) {
this.label = `Set default of ${key}`;
}
execute(project: Project): void {
const node = requireNode(project, this.assetId, this.nodeId);
if (node.type !== SOUND_FX_INPUT) throw new CommandError('Only a sound FX Input has defaults');
const port = (node.ports ?? []).find((p) => p.key === this.key);
if (!port) throw new CommandError(`Port "${this.key}" not found`);
if (!Number.isFinite(this.value)) throw new CommandError('A default is a number');
this.was = port.default;
this.mark(node, this.value);
}
undo(project: Project): void {
this.mark(requireNode(project, this.assetId, this.nodeId), this.was);
}
private mark(node: GraphNode, value: number | undefined): void {
node.ports = (node.ports ?? []).map((port) => {
if (port.key !== this.key) return port;
const { default: _, ...plain } = port;
return value === undefined ? plain : { ...plain, default: value };
});
}
}
/** A renamed parameter of a sound FX, followed into every node that sets it. */
function renameSoundFxParamPorts(project: Project, fxId: string, from: string, to: string): void {
for (const asset of project.assets) {
if (!isGraphAsset(asset)) continue;
for (const node of asset.graph.nodes)
if (declaresSoundFxPorts(node.type) && soundFxSourceOf(node.params) === fxId)
renamePortOn(asset.graph, node, from, to);
}
}