@driftengine/script 3.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +81 -0
  4. package/dist/bindings/ai.d.ts +66 -0
  5. package/dist/bindings/ai.js +173 -0
  6. package/dist/bindings/animation.d.ts +13 -0
  7. package/dist/bindings/animation.js +177 -0
  8. package/dist/bindings/audio.d.ts +56 -0
  9. package/dist/bindings/audio.js +126 -0
  10. package/dist/bindings/behavior.d.ts +36 -0
  11. package/dist/bindings/behavior.js +101 -0
  12. package/dist/bindings/chemistry.d.ts +72 -0
  13. package/dist/bindings/chemistry.js +253 -0
  14. package/dist/bindings/core.d.ts +172 -0
  15. package/dist/bindings/core.js +814 -0
  16. package/dist/bindings/editor.d.ts +53 -0
  17. package/dist/bindings/editor.js +248 -0
  18. package/dist/bindings/entities.d.ts +73 -0
  19. package/dist/bindings/entities.js +339 -0
  20. package/dist/bindings/interface.d.ts +55 -0
  21. package/dist/bindings/interface.js +200 -0
  22. package/dist/bindings/navigation.d.ts +36 -0
  23. package/dist/bindings/navigation.js +255 -0
  24. package/dist/bindings/network.d.ts +51 -0
  25. package/dist/bindings/network.js +96 -0
  26. package/dist/bindings/render.d.ts +71 -0
  27. package/dist/bindings/render.js +143 -0
  28. package/dist/bindings/rollback.d.ts +41 -0
  29. package/dist/bindings/rollback.js +74 -0
  30. package/dist/bindings/sprites.d.ts +53 -0
  31. package/dist/bindings/sprites.js +186 -0
  32. package/dist/bindings/terrain.d.ts +54 -0
  33. package/dist/bindings/terrain.js +138 -0
  34. package/dist/bindings/xr.d.ts +56 -0
  35. package/dist/bindings/xr.js +155 -0
  36. package/dist/entityHost.d.ts +80 -0
  37. package/dist/entityHost.js +159 -0
  38. package/dist/host.d.ts +153 -0
  39. package/dist/host.js +233 -0
  40. package/dist/index.d.ts +21 -0
  41. package/dist/index.js +18 -0
  42. package/package.json +68 -0
  43. package/src/bindings/ai.ts +306 -0
  44. package/src/bindings/animation.ts +346 -0
  45. package/src/bindings/audio.ts +185 -0
  46. package/src/bindings/behavior.ts +181 -0
  47. package/src/bindings/chemistry.ts +548 -0
  48. package/src/bindings/core.ts +1604 -0
  49. package/src/bindings/editor.ts +393 -0
  50. package/src/bindings/entities.ts +550 -0
  51. package/src/bindings/interface.ts +300 -0
  52. package/src/bindings/navigation.ts +367 -0
  53. package/src/bindings/network.ts +178 -0
  54. package/src/bindings/render.ts +191 -0
  55. package/src/bindings/rollback.ts +110 -0
  56. package/src/bindings/sprites.ts +310 -0
  57. package/src/bindings/terrain.ts +180 -0
  58. package/src/bindings/xr.ts +272 -0
  59. package/src/entityHost.ts +219 -0
  60. package/src/host.ts +465 -0
  61. package/src/index.ts +41 -0
@@ -0,0 +1,346 @@
1
+ /**
2
+ * `drift/animation` — Track A, complete, and the one binding with a trap to disarm.
3
+ *
4
+ * **`BlendTree` allocates its own scratch poses, and without a bind pose they start at zero
5
+ * translation.** `sampleClip` leaves a channel no track mentions exactly as it found it, so a
6
+ * rotation-only clip — which is most of them — preserves whatever translations the pose already
7
+ * held. `blendPoses` cannot do that: it interpolates *every* channel of two poses. So a tree or a
8
+ * state machine built without a bind pose collapses every joint onto its parent's origin, and the
9
+ * symptom is a figure folded in on itself rather than an error.
10
+ *
11
+ * The TypeScript constructor makes `bind` optional, and that is right there: its caller is holding
12
+ * the skeleton and knows what a rest pose is. **A script never meets that argument**, because a
13
+ * script does not build a tree — it drives one its host built and handed it, which is what
14
+ * `blendSet` and `blendAt` are for and what the block above them explains. `reach` is this file's
15
+ * worked example of the rule that used to live here: its pole hint is required at the boundary
16
+ * because a chain that is already straight has no plane to bend in, and a script author meets that
17
+ * trap first.
18
+ *
19
+ * **`blendPoses` does not take one and must not be given one**, which the paraphrase this binding
20
+ * was first written from got wrong. It writes into a caller-owned `out`, so the caller already
21
+ * controls what the untouched channels start as; adding a bind-pose parameter here would be
22
+ * inventing an argument the engine does not have. The rule belongs to the thing that allocates.
23
+ */
24
+ import {
25
+ type AnimationClip,
26
+ type BlendTree,
27
+ type Pose,
28
+ type RootMotion,
29
+ Skeleton,
30
+ blendPoses,
31
+ createPose,
32
+ createRootMotion,
33
+ extractRootMotion,
34
+ restPose,
35
+ sampleClip,
36
+ solveTwoBone,
37
+ stripRootMotion,
38
+ } from '@driftengine/animation';
39
+ import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
40
+
41
+ export const ANIMATION_MODULE = 'drift/animation';
42
+
43
+ export const ANIMATION_TYPES: readonly OpaqueType[] = [
44
+ {
45
+ module: ANIMATION_MODULE,
46
+ name: 'Skeleton',
47
+ doc: 'A joint hierarchy, parents before children.',
48
+ },
49
+ { module: ANIMATION_MODULE, name: 'Pose', doc: 'One set of joint transforms.' },
50
+ { module: ANIMATION_MODULE, name: 'Clip', doc: 'Keyframed tracks over time.' },
51
+ { module: ANIMATION_MODULE, name: 'Blend', doc: 'A blend tree over clips.' },
52
+ { module: ANIMATION_MODULE, name: 'Motion', doc: "A root's displacement over an interval." },
53
+ ];
54
+
55
+ const define = (
56
+ name: string,
57
+ params: readonly { name: string; type: string }[],
58
+ returns: string,
59
+ effects: CapabilityDefinition['effects'],
60
+ deterministic: boolean,
61
+ doc: string,
62
+ ): CapabilityDefinition =>
63
+ defineCapability({
64
+ module: ANIMATION_MODULE,
65
+ name,
66
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
67
+ params: [...params],
68
+ returns,
69
+ effects,
70
+ deterministic,
71
+ doc,
72
+ implementation: `${ANIMATION_MODULE}.${name}`,
73
+ });
74
+
75
+ export const ANIMATION_CAPABILITIES: readonly CapabilityDefinition[] = [
76
+ define(
77
+ 'pose',
78
+ [{ name: 'jointCount', type: 'u32' }],
79
+ 'Pose',
80
+ ['pure'],
81
+ true,
82
+ 'A new pose at rest. This is where a bind pose comes from; take one and keep it.',
83
+ ),
84
+ define(
85
+ 'toRest',
86
+ [
87
+ { name: 'pose', type: 'Pose' },
88
+ { name: 'jointCount', type: 'u32' },
89
+ ],
90
+ 'void',
91
+ ['animation.write'],
92
+ false,
93
+ 'Return a pose to rest without allocating another, which is what a state machine does on re-entry.',
94
+ ),
95
+ define(
96
+ 'sample',
97
+ [
98
+ { name: 'clip', type: 'Clip' },
99
+ { name: 'at', type: 'f32' },
100
+ { name: 'into', type: 'Pose' },
101
+ ],
102
+ 'void',
103
+ ['animation.write'],
104
+ false,
105
+ 'Sample a clip at a time. A channel no track mentions is left as it was, so a rotation-only clip preserves the rest of the pose.',
106
+ ),
107
+ define(
108
+ 'blend',
109
+ [
110
+ { name: 'a', type: 'Pose' },
111
+ { name: 'b', type: 'Pose' },
112
+ { name: 'amount', type: 'f32' },
113
+ { name: 'into', type: 'Pose' },
114
+ ],
115
+ 'void',
116
+ ['animation.write'],
117
+ false,
118
+ 'Blend two poses into a third, clamped at the ends. Safe when `into` is also `a` or `b`. It interpolates every channel, so `into` decides what an untouched joint blends from.',
119
+ ),
120
+ /*
121
+ * **A script drives a tree; it does not build one. And `blendTree` used to say otherwise.**
122
+ *
123
+ * That capability took a `Skeleton` and handed it to `new BlendTree(skeleton as never, …)`,
124
+ * whose first parameter is the *root node* of the graph. Run, it threw `Cannot read properties
125
+ * of undefined (reading 'length')` — measured 2026-08-28, not reasoned about — because `prepare`
126
+ * asked a skeleton for its children. Nothing caught it: `host.test.ts` asserted that the
127
+ * *compiler* required three arguments, which it did, and no test ever called the implementation.
128
+ * A capability nobody could use, with a message naming nothing a script author could act on.
129
+ *
130
+ * **Why building one is a design and not an oversight.** A tree is a graph of nodes, and a
131
+ * script has no node value: it would need a `clip` node, a `lerp`, a set with a list of stops
132
+ * and a weight per stop, and then a name for the whole shape. That is a surface to design, and
133
+ * `docs/IMPROVEMENTS.md` carries it with what it needs.
134
+ *
135
+ * **What works today is the shape DriftScript 1.8.1 opened.** A host builds the tree in
136
+ * TypeScript, where the skeleton and the clips already are, and hands it to a system through
137
+ * `uses`; the script then sets its parameters and evaluates it. So these two are the whole
138
+ * script-side surface of a blend tree, and they are enough for the case the language was given
139
+ * `uses` for.
140
+ */
141
+ define(
142
+ 'blendSet',
143
+ [
144
+ { name: 'tree', type: 'Blend' },
145
+ { name: 'parameter', type: 'String' },
146
+ { name: 'value', type: 'f32' },
147
+ ],
148
+ 'void',
149
+ ['animation.write'],
150
+ false,
151
+ "Set one of a tree's parameters. A name no node in that tree declares is refused by name, because a typo that silently did nothing would present as an animation that will not respond. Both kinds of parameter are set this way: a blend weight, and a clock — the time a `clip` node named for its own sampling.",
152
+ ),
153
+ define(
154
+ 'blendAt',
155
+ [
156
+ { name: 'tree', type: 'Blend' },
157
+ { name: 'at', type: 'f32' },
158
+ { name: 'into', type: 'Pose' },
159
+ ],
160
+ 'void',
161
+ ['animation.write'],
162
+ false,
163
+ 'Evaluate a whole tree into a pose. `at` is the clock for every node that did not name one of its own, so a tree of ordinary clips is sampled at one time and a node with a clock reads that parameter instead.',
164
+ ),
165
+ define(
166
+ 'reach',
167
+ [
168
+ { name: 'skeleton', type: 'Skeleton' },
169
+ { name: 'pose', type: 'Pose' },
170
+ { name: 'root', type: 'u32' },
171
+ { name: 'mid', type: 'u32' },
172
+ { name: 'tip', type: 'u32' },
173
+ { name: 'targetX', type: 'f32' },
174
+ { name: 'targetY', type: 'f32' },
175
+ { name: 'targetZ', type: 'f32' },
176
+ /*
177
+ * The pole hint decides the one thing a target cannot: which way the joint bends. Required
178
+ * for the same reason as the bind pose — a chain that is already straight has no plane to
179
+ * bend in, and without a hint the elbow flips unpredictably as a character turns. The engine
180
+ * requires it too; it is listed here so a reader of the signature knows what it is for.
181
+ */
182
+ { name: 'poleX', type: 'f32' },
183
+ { name: 'poleY', type: 'f32' },
184
+ { name: 'poleZ', type: 'f32' },
185
+ ],
186
+ 'bool',
187
+ ['animation.write'],
188
+ false,
189
+ 'Bend a two-bone chain so its tip reaches a target. Answers whether it was reachable; an unreachable target straightens the chain toward it rather than giving up.',
190
+ ),
191
+ /*
192
+ * **Root motion is two calls and it is a mistake to make only one of them.** `rootMotion` hands
193
+ * the caller the displacement and `stripRoot` takes it out of the pose; a script that extracts
194
+ * without stripping moves its character twice, at exactly double speed, with nothing failing.
195
+ * Both descriptions say so, because a script author has no signature to read.
196
+ *
197
+ * The displacement is read back through separate accessors, the shape `hitX` and `input.axisX`
198
+ * already use — a script has no way to receive an engine record. Here the accessors take the
199
+ * `Motion` rather than reading a scratch the last call wrote, so two characters extracted in one
200
+ * frame do not clobber each other; the raycast surface keys its scratch on the world for the
201
+ * same reason, and animation has no equivalent handle to key on.
202
+ */
203
+ define(
204
+ 'motion',
205
+ [],
206
+ 'Motion',
207
+ ['pure'],
208
+ true,
209
+ 'A new root displacement, at zero. Take one and keep it; nothing here allocates per frame.',
210
+ ),
211
+ define(
212
+ 'rootMotion',
213
+ [
214
+ { name: 'clip', type: 'Clip' },
215
+ { name: 'joint', type: 'u32' },
216
+ { name: 'from', type: 'f32' },
217
+ { name: 'to', type: 'f32' },
218
+ { name: 'into', type: 'Motion' },
219
+ ],
220
+ 'void',
221
+ ['animation.write'],
222
+ false,
223
+ 'How far the root travelled between two times, in its own frame at `from`. Loops are accumulated, so a clip that wraps between the two times still answers a step forward. Pair every call with `stripRoot` or the character moves twice.',
224
+ ),
225
+ define(
226
+ 'stripRoot',
227
+ [
228
+ { name: 'clip', type: 'Clip' },
229
+ { name: 'joint', type: 'u32' },
230
+ { name: 'pose', type: 'Pose' },
231
+ ],
232
+ 'void',
233
+ ['animation.write'],
234
+ false,
235
+ "Pin a sampled pose's root to the clip's value at time zero, so the displacement `rootMotion` handed you is not also in the pose. Every other joint is left as it was.",
236
+ ),
237
+ define(
238
+ 'motionX',
239
+ [{ name: 'motion', type: 'Motion' }],
240
+ 'f32',
241
+ ['pure'],
242
+ true,
243
+ "The displacement along the root's own x axis at the earlier time.",
244
+ ),
245
+ define(
246
+ 'motionY',
247
+ [{ name: 'motion', type: 'Motion' }],
248
+ 'f32',
249
+ ['pure'],
250
+ true,
251
+ "The displacement along the root's own y axis at the earlier time.",
252
+ ),
253
+ define(
254
+ 'motionZ',
255
+ [{ name: 'motion', type: 'Motion' }],
256
+ 'f32',
257
+ ['pure'],
258
+ true,
259
+ "The displacement along the root's own z axis at the earlier time.",
260
+ ),
261
+ define(
262
+ 'motionTurnX',
263
+ [{ name: 'motion', type: 'Motion' }],
264
+ 'f32',
265
+ ['pure'],
266
+ true,
267
+ 'The x part of the rotation the root turned through, as a quaternion.',
268
+ ),
269
+ define(
270
+ 'motionTurnY',
271
+ [{ name: 'motion', type: 'Motion' }],
272
+ 'f32',
273
+ ['pure'],
274
+ true,
275
+ 'The y part of the rotation the root turned through, as a quaternion.',
276
+ ),
277
+ define(
278
+ 'motionTurnZ',
279
+ [{ name: 'motion', type: 'Motion' }],
280
+ 'f32',
281
+ ['pure'],
282
+ true,
283
+ 'The z part of the rotation the root turned through, as a quaternion.',
284
+ ),
285
+ define(
286
+ 'motionTurnW',
287
+ [{ name: 'motion', type: 'Motion' }],
288
+ 'f32',
289
+ ['pure'],
290
+ true,
291
+ 'The w part of the rotation the root turned through, as a quaternion. One means it did not turn.',
292
+ ),
293
+ ];
294
+
295
+ /**
296
+ * The implementations.
297
+ *
298
+ * `blendTree` passes the bind pose straight through to the engine's optional parameter — the
299
+ * requirement lives entirely in the *description*, which is exactly where a capability boundary
300
+ * should hold a rule. The engine's own callers keep their option; script authors get the safe
301
+ * behaviour without the engine changing.
302
+ */
303
+ export function animationImplementation(): Record<string, unknown> {
304
+ return {
305
+ pose: (jointCount: number) => createPose(jointCount),
306
+ toRest: (pose: Pose, jointCount: number) => restPose(jointCount, pose),
307
+ sample: (clip: AnimationClip, at: number, into: Pose) => sampleClip(clip, at, into),
308
+ blend: (a: Pose, b: Pose, amount: number, into: Pose) => blendPoses(a, b, amount, into),
309
+ blendSet: (tree: BlendTree, parameter: string, value: number) => tree.set(parameter, value),
310
+ blendAt: (tree: BlendTree, at: number, into: Pose) => tree.evaluate(at, into),
311
+ motion: () => createRootMotion(),
312
+ rootMotion: (clip: AnimationClip, joint: number, from: number, to: number, into: RootMotion) =>
313
+ extractRootMotion(clip, joint, from, to, into),
314
+ stripRoot: (clip: AnimationClip, joint: number, pose: Pose) =>
315
+ stripRootMotion(clip, joint, pose),
316
+ motionX: (motion: RootMotion) => motion.translation[0] as number,
317
+ motionY: (motion: RootMotion) => motion.translation[1] as number,
318
+ motionZ: (motion: RootMotion) => motion.translation[2] as number,
319
+ motionTurnX: (motion: RootMotion) => motion.rotation[0] as number,
320
+ motionTurnY: (motion: RootMotion) => motion.rotation[1] as number,
321
+ motionTurnZ: (motion: RootMotion) => motion.rotation[2] as number,
322
+ motionTurnW: (motion: RootMotion) => motion.rotation[3] as number,
323
+ reach: (
324
+ skeleton: Skeleton,
325
+ pose: Pose,
326
+ root: number,
327
+ mid: number,
328
+ tip: number,
329
+ targetX: number,
330
+ targetY: number,
331
+ targetZ: number,
332
+ poleX: number,
333
+ poleY: number,
334
+ poleZ: number,
335
+ ) =>
336
+ solveTwoBone(
337
+ skeleton,
338
+ pose,
339
+ root,
340
+ mid,
341
+ tip,
342
+ [targetX, targetY, targetZ],
343
+ [poleX, poleY, poleZ],
344
+ ),
345
+ };
346
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * `drift/audio` — what this engine lets a script do to sound.
3
+ *
4
+ * **Five capabilities: a slot, two ways to play it, and the two cheap positional helpers.** This
5
+ * header read "playback, placement, the mix, and the rhythm the music is doing" until 2026-08-27,
6
+ * and three of those four were never here — there is no mix verb, no rhythm verb, no HRTF-panned
7
+ * source, no listener and no reverb zone. The sentence was written from *Track G's* deliverables
8
+ * rather than from the list beneath it, which is the same failure `drift/physics` had one file
9
+ * over, and the same one the first `drift/camera` binding had: a comment describing what a
10
+ * subsystem ought to expose rather than what this array does.
11
+ *
12
+ * It is still the right first binding and still the thing that shows what a binding *is* — registry
13
+ * entries describing capabilities the engine already has, plus a map of implementations the host
14
+ * supplies at link time. **No new engine code.**
15
+ *
16
+ * **What is missing, and why it is not guessed at here.** `MixConsole`, `AudioListenerGraph`,
17
+ * `SpatialSource` and `ReverbZone` are four stateful objects with lifetimes, and a script-side
18
+ * surface for them is a design about handles and ownership rather than a table entry — who disposes
19
+ * a source, what happens to its probe slot, whether a zone outlives the script that made it.
20
+ * `docs/CAPABILITIES.md` §4 records it. Taking that badly would put a leak in front of every script
21
+ * author, which is worse than a surface they can see is absent.
22
+ *
23
+ * Three constraints this surface inherits, none of them negotiable:
24
+ *
25
+ * - **`audio.write` is outside the determinism boundary.** `ARCHITECTURE.md` places audio there, so
26
+ * a `@deterministic` function may not call any of it. A simulation decides *that* a door opened
27
+ * and emits an event; something outside the fixed step decides what it sounds like. The compiler
28
+ * is only making a line that already exists visible.
29
+ * - **`audio.play` in a `@hot` function is a diagnostic**, for the reason the engine's own rules
30
+ * forbid allocation there.
31
+ * - **A sound slot may be absent, so `sound` returns an option.** The registry probes several
32
+ * format candidates per slot and the file often is not there. A script that ignores that plays
33
+ * silence and reports success, which is the failure the language's no-implicit-null rule exists
34
+ * to prevent — and this is the first place in the engine where that rule earns its keep.
35
+ */
36
+ import { AudioGraph, SoundRegistry, distanceGain, stereoPan } from '@driftengine/audio';
37
+ import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
38
+
39
+ export const AUDIO_MODULE = 'drift/audio';
40
+
41
+ /**
42
+ * A decoded sound, held by a script and passed back to the engine.
43
+ *
44
+ * Opaque on purpose: it is an `AudioBuffer`, and a script that could read its fields would be a
45
+ * script depending on a representation the engine cannot then change. The design calls these safe
46
+ * handles, and this is one.
47
+ */
48
+ export const AUDIO_TYPES: readonly OpaqueType[] = [
49
+ {
50
+ module: AUDIO_MODULE,
51
+ name: 'Sound',
52
+ doc: 'A decoded sound, resolved from a slot. Held and passed back; never read into.',
53
+ },
54
+ ];
55
+
56
+ const define = (
57
+ name: string,
58
+ params: readonly { name: string; type: string }[],
59
+ returns: string,
60
+ effects: CapabilityDefinition['effects'],
61
+ deterministic: boolean,
62
+ doc: string,
63
+ implementation: string,
64
+ ): CapabilityDefinition =>
65
+ defineCapability({
66
+ module: AUDIO_MODULE,
67
+ name,
68
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
69
+ params: [...params],
70
+ returns,
71
+ effects,
72
+ deterministic,
73
+ doc,
74
+ implementation,
75
+ });
76
+
77
+ export const AUDIO_CAPABILITIES: readonly CapabilityDefinition[] = [
78
+ define(
79
+ 'sound',
80
+ [{ name: 'slot', type: 'String' }],
81
+ /*
82
+ * `Sound?`, and the `?` is the whole point.
83
+ *
84
+ * The registry resolves a slot by probing format candidates, and a missing file is ordinary
85
+ * rather than exceptional. Returning a bare `Sound` would make every script that forgot to
86
+ * check play silence and report success.
87
+ */
88
+ 'Sound?',
89
+ ['pure'],
90
+ true,
91
+ 'Resolve a sound slot. Absent when nothing was registered or nothing decoded.',
92
+ 'SoundRegistry.get',
93
+ ),
94
+ define(
95
+ 'play',
96
+ [
97
+ { name: 'sound', type: 'Sound' },
98
+ { name: 'gain', type: 'f32' },
99
+ ],
100
+ 'void',
101
+ ['audio.write'],
102
+ false,
103
+ 'Play a resolved sound through the mix.',
104
+ 'AudioGraph.play',
105
+ ),
106
+ define(
107
+ 'playPanned',
108
+ [
109
+ { name: 'sound', type: 'Sound' },
110
+ { name: 'gain', type: 'f32' },
111
+ { name: 'pan', type: 'f32' },
112
+ ],
113
+ 'void',
114
+ ['audio.write'],
115
+ false,
116
+ 'Play a resolved sound at a stereo position, -1 left to 1 right.',
117
+ 'AudioGraph.play',
118
+ ),
119
+ define(
120
+ 'distanceGain',
121
+ [
122
+ { name: 'distance', type: 'f32' },
123
+ { name: 'radius', type: 'f32' },
124
+ ],
125
+ 'f32',
126
+ /*
127
+ * `pure`, and therefore callable from a `@deterministic` function.
128
+ *
129
+ * It is arithmetic over two numbers — the cheap positional function, not the HRTF path. That a
130
+ * capability in an otherwise effectful module is deterministic is the reason effects are
131
+ * declared per capability rather than per module.
132
+ */
133
+ ['pure'],
134
+ true,
135
+ 'How loud something is at a distance, falling off to nothing at the radius.',
136
+ 'audio.distanceGain',
137
+ ),
138
+ define(
139
+ 'stereoPan',
140
+ [
141
+ { name: 'dx', type: 'f32' },
142
+ { name: 'dz', type: 'f32' },
143
+ { name: 'yaw', type: 'f32' },
144
+ ],
145
+ 'f32',
146
+ ['pure'],
147
+ true,
148
+ 'Where something sits in the stereo field, relative to a listener facing yaw.',
149
+ 'audio.stereoPan',
150
+ ),
151
+ ];
152
+
153
+ /** How the runtime represents an option. Matches what the compiler generates for `some`/`none`. */
154
+ type Option<T> = { readonly tag: 'some'; readonly value: T } | { readonly tag: 'none' };
155
+
156
+ const some = <T>(value: T): Option<T> => ({ tag: 'some', value });
157
+ const none: Option<never> = { tag: 'none' };
158
+
159
+ /**
160
+ * The implementations, which the host hands to a module's `__bind`.
161
+ *
162
+ * A plain object rather than anything the registry holds — R2: the registry describes and never
163
+ * becomes the call path, so this is a separate thing that happens to have matching keys. The
164
+ * registry's `implementation` strings name these; nothing enforces that mechanically, which is why
165
+ * a test asserts every definition has an implementation here.
166
+ */
167
+ export function audioImplementation(
168
+ graph: AudioGraph,
169
+ registry: SoundRegistry,
170
+ ): Record<string, unknown> {
171
+ return {
172
+ sound(slot: string): Option<AudioBuffer> {
173
+ const buffer = registry.get(slot);
174
+ return buffer === undefined ? none : some(buffer);
175
+ },
176
+ play(sound: AudioBuffer, gain: number): void {
177
+ graph.play(sound, gain);
178
+ },
179
+ playPanned(sound: AudioBuffer, gain: number, pan: number): void {
180
+ graph.play(sound, gain, pan);
181
+ },
182
+ distanceGain,
183
+ stereoPan,
184
+ };
185
+ }