A sound could only be played as it was uploaded. A Sound FX asset is
a node graph like a shader: Input, effects, Output. Play sound has a
new input fx that sends the voice through it.
- New asset type soundfx ("Sound FX"), not placeable, with a graph
and its own tab. The heading in the asset panel is "Sound effects",
so it does not take the click meant for the + menu entry.
- Nodes in core/soundfx/nodes.ts, category sound, port type audio
that only fits audio: Input, Output, Gain (dB), Pitch (semitones),
EQ (80 Hz and 12 kHz shelves, peaks at 250 Hz, 1 kHz and 4 kHz).
- compileSoundFx walks back from the Output with the shared graph
emitter. An input takes one edge, so the result is one line: a
pitch and a list of gain and filter stages. A node without input
or a graph without Output reports a problem on the node.
- Pitch is the playback rate of the source. It applies to the whole
sound and also changes its length, the node description says so.
- The BehaviorSystem resolves fx and hands the line to the AudioSink.
An fx that is missing or does not compile plays the sound dry.
- SoundLibrary wires source, stages, volume, destination in that
order. stats().last now also names pitch and stage count.
- The tab header picks one of the project's sounds and plays it
through the graph as it stands. Stop there ends only its own voice.
Tests:
- soundfx.test.ts (new, 11), sound-library (9, +2 for the wiring),
node-catalog (+1 for fx), graph and asset-groups adjusted
- e2e 34: ALL PASSED - 26 checks (+5: + menu opens the tab, palette,
2 effects, tab Play and game Play through the FX at -12 st)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 239 of 240
test files green. The red one was sound-library (its fake
AudioContext had no playbackRate), fixed afterwards, gate not
re-run in full.
- Counter-check: without the audio rule in portsCompatible the port
test fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
5.6 KiB
TypeScript
153 lines
5.6 KiB
TypeScript
import type { PropertyValue } from '../../model/Properties';
|
||
import type { PropertyDef } from '../../properties/PropertySchema';
|
||
import type { NodeDef, PortDef } from '../NodeRegistry';
|
||
import { asBoolean, asNumber } from '../values';
|
||
|
||
export const exec = (key = 'exec', label = ''): PortDef => ({ key, label, type: 'exec' });
|
||
export const num = (key: string, label = key): PortDef => ({ key, label, type: 'number' });
|
||
export const bool = (key: string, label = key): PortDef => ({ key, label, type: 'boolean' });
|
||
export const str = (key: string, label = key): PortDef => ({ key, label, type: 'string' });
|
||
export const obj = (key: string, label = key): PortDef => ({ key, label, type: 'object' });
|
||
export const list = (key: string, label = key): PortDef => ({ key, label, type: 'list' });
|
||
export const struct = (key: string, label = key): PortDef => ({ key, label, type: 'struct' });
|
||
export const any = (key: string, label = key): PortDef => ({ key, label, type: 'any' });
|
||
|
||
export const withDefault = (port: PortDef, value: PropertyValue): PortDef => ({ ...port, default: value });
|
||
|
||
/** A whole-number input (an index, a count): its field shows no decimals, the node floors it anyway. */
|
||
export const int = (key: string, value = 0, label = key): PortDef => ({
|
||
key,
|
||
label,
|
||
type: 'number',
|
||
default: value,
|
||
field: { key, label, kind: 'number', default: value, step: 1, decimals: 0 },
|
||
});
|
||
|
||
/** Outputs of the three collision events: the other body, its collider and the own collider. */
|
||
export const collisionOutputs = (): PortDef[] => [
|
||
exec(),
|
||
obj('other'),
|
||
obj('otherCollider', 'other collider'),
|
||
obj('collider'),
|
||
];
|
||
|
||
/**
|
||
* How a contact touches: which way the other side pushes (a unit vector pointing from it towards the
|
||
* own collider), how hard, and the middle of the contact points in the world. The physics engine
|
||
* works them out (#146, #149).
|
||
*/
|
||
export const contactOutputs = (): PortDef[] => [
|
||
num('normalX', 'normal x'),
|
||
num('normalY', 'normal y'),
|
||
num('impulse'),
|
||
num('pointX', 'point x'),
|
||
num('pointY', 'point y'),
|
||
];
|
||
|
||
/** A tile asset; unconnected as an input, it shows the tile picker. */
|
||
export const tilePort = (key = 'tile', label = key): PortDef => ({
|
||
key,
|
||
label,
|
||
type: 'string',
|
||
field: { key, label: 'Tile', kind: 'asset', default: '', assetTypes: ['tile'] },
|
||
});
|
||
|
||
export const PER_CELL_INPUT = 'perCell';
|
||
|
||
/**
|
||
* Which pairs a collision event hears of a map's trigger tiles (#160): off, one per area of a tile –
|
||
* a pond is entered once –, on, one per cell.
|
||
*/
|
||
export const perCellPort = (): PortDef => ({
|
||
key: PER_CELL_INPUT,
|
||
label: 'per cell',
|
||
type: 'boolean',
|
||
default: false,
|
||
field: { key: PER_CELL_INPUT, label: 'Per cell (trigger tiles)', kind: 'boolean', default: false },
|
||
});
|
||
|
||
/**
|
||
* The trigger tile a collider lies on (#160) and its cell in the map, which is then `other`. No
|
||
* tile and cell -1, -1 for anything else: another collider, or a map's solid cells.
|
||
*/
|
||
export const tileOutputs = (): PortDef[] => [tilePort(), num('cellX', 'cell x'), num('cellY', 'cell y')];
|
||
|
||
export const keyParam: PropertyDef = { key: 'key', label: 'Key', kind: 'key', default: 'ArrowRight' };
|
||
|
||
/** No `assetTypes`: any placeable asset is a valid answer here. */
|
||
const assetParam: PropertyDef = { key: 'asset', label: 'Asset', kind: 'asset', default: '' };
|
||
|
||
/**
|
||
* The `asset` input of `Spawn` and `Is asset`; unconnected, it shows the asset picker. The key is the
|
||
* one the setting had before it became a port, so a saved pick is the inline value now.
|
||
*/
|
||
export const assetPort = (): PortDef => ({ key: 'asset', label: 'asset', type: 'string', field: assetParam });
|
||
|
||
/**
|
||
* The `sound` input of the two sound nodes; unconnected, it shows the sound picker – the way
|
||
* `machinePort` shows the machine picker. A `Sound` literal is what feeds it through an edge.
|
||
*/
|
||
export const soundPort = (): PortDef => ({
|
||
key: 'sound',
|
||
label: 'sound',
|
||
type: 'string',
|
||
field: { key: 'sound', label: 'Sound', kind: 'sound', default: '' },
|
||
});
|
||
|
||
/** The `fx` input of `Play sound` (#311); unconnected, it shows the picker of sound FX assets. */
|
||
export const soundFxPort = (): PortDef => ({
|
||
key: 'fx',
|
||
label: 'fx',
|
||
type: 'string',
|
||
field: { key: 'fx', label: 'FX', kind: 'asset', default: '', assetTypes: ['soundfx'] },
|
||
});
|
||
|
||
export function binaryMath(type: string, description: string, fn: (a: number, b: number) => number): NodeDef {
|
||
return {
|
||
type,
|
||
label: type,
|
||
category: 'data',
|
||
description,
|
||
inputs: [num('a'), num('b')],
|
||
outputs: [num('result')],
|
||
params: [],
|
||
run: (_ctx, inputs) => ({ outputs: { result: fn(asNumber(inputs.a), asNumber(inputs.b)) } }),
|
||
};
|
||
}
|
||
|
||
export function unaryMath(type: string, description: string, fn: (value: number) => number): NodeDef {
|
||
return {
|
||
type,
|
||
label: type,
|
||
category: 'data',
|
||
description,
|
||
inputs: [num('value')],
|
||
outputs: [num('result')],
|
||
params: [],
|
||
run: (_ctx, inputs) => ({ outputs: { result: fn(asNumber(inputs.value)) } }),
|
||
};
|
||
}
|
||
|
||
export function binaryLogic(
|
||
type: string,
|
||
description: string,
|
||
fn: (a: boolean, b: boolean) => boolean,
|
||
): NodeDef {
|
||
return {
|
||
type,
|
||
label: type,
|
||
category: 'data',
|
||
description,
|
||
inputs: [bool('a'), bool('b')],
|
||
outputs: [bool('result')],
|
||
params: [],
|
||
run: (_ctx, inputs) => ({ outputs: { result: fn(asBoolean(inputs.a), asBoolean(inputs.b)) } }),
|
||
};
|
||
}
|
||
|
||
/** Scales (x, y) to length 1; the zero vector stays zero. */
|
||
export function normalize(x: number, y: number): { x: number; y: number; length: number } {
|
||
const length = Math.hypot(x, y);
|
||
return length === 0 ? { x: 0, y: 0, length: 0 } : { x: x / length, y: y / length, length };
|
||
}
|