@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,177 @@
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 { Skeleton, blendPoses, createPose, createRootMotion, extractRootMotion, restPose, sampleClip, solveTwoBone, stripRootMotion, } from '@driftengine/animation';
25
+ import { defineCapability } from 'driftscript';
26
+ export const ANIMATION_MODULE = 'drift/animation';
27
+ export const ANIMATION_TYPES = [
28
+ {
29
+ module: ANIMATION_MODULE,
30
+ name: 'Skeleton',
31
+ doc: 'A joint hierarchy, parents before children.',
32
+ },
33
+ { module: ANIMATION_MODULE, name: 'Pose', doc: 'One set of joint transforms.' },
34
+ { module: ANIMATION_MODULE, name: 'Clip', doc: 'Keyframed tracks over time.' },
35
+ { module: ANIMATION_MODULE, name: 'Blend', doc: 'A blend tree over clips.' },
36
+ { module: ANIMATION_MODULE, name: 'Motion', doc: "A root's displacement over an interval." },
37
+ ];
38
+ const define = (name, params, returns, effects, deterministic, doc) => defineCapability({
39
+ module: ANIMATION_MODULE,
40
+ name,
41
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
42
+ params: [...params],
43
+ returns,
44
+ effects,
45
+ deterministic,
46
+ doc,
47
+ implementation: `${ANIMATION_MODULE}.${name}`,
48
+ });
49
+ export const ANIMATION_CAPABILITIES = [
50
+ define('pose', [{ name: 'jointCount', type: 'u32' }], 'Pose', ['pure'], true, 'A new pose at rest. This is where a bind pose comes from; take one and keep it.'),
51
+ define('toRest', [
52
+ { name: 'pose', type: 'Pose' },
53
+ { name: 'jointCount', type: 'u32' },
54
+ ], 'void', ['animation.write'], false, 'Return a pose to rest without allocating another, which is what a state machine does on re-entry.'),
55
+ define('sample', [
56
+ { name: 'clip', type: 'Clip' },
57
+ { name: 'at', type: 'f32' },
58
+ { name: 'into', type: 'Pose' },
59
+ ], 'void', ['animation.write'], false, '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.'),
60
+ define('blend', [
61
+ { name: 'a', type: 'Pose' },
62
+ { name: 'b', type: 'Pose' },
63
+ { name: 'amount', type: 'f32' },
64
+ { name: 'into', type: 'Pose' },
65
+ ], 'void', ['animation.write'], false, '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.'),
66
+ /*
67
+ * **A script drives a tree; it does not build one. And `blendTree` used to say otherwise.**
68
+ *
69
+ * That capability took a `Skeleton` and handed it to `new BlendTree(skeleton as never, …)`,
70
+ * whose first parameter is the *root node* of the graph. Run, it threw `Cannot read properties
71
+ * of undefined (reading 'length')` — measured 2026-08-28, not reasoned about — because `prepare`
72
+ * asked a skeleton for its children. Nothing caught it: `host.test.ts` asserted that the
73
+ * *compiler* required three arguments, which it did, and no test ever called the implementation.
74
+ * A capability nobody could use, with a message naming nothing a script author could act on.
75
+ *
76
+ * **Why building one is a design and not an oversight.** A tree is a graph of nodes, and a
77
+ * script has no node value: it would need a `clip` node, a `lerp`, a set with a list of stops
78
+ * and a weight per stop, and then a name for the whole shape. That is a surface to design, and
79
+ * `docs/IMPROVEMENTS.md` carries it with what it needs.
80
+ *
81
+ * **What works today is the shape DriftScript 1.8.1 opened.** A host builds the tree in
82
+ * TypeScript, where the skeleton and the clips already are, and hands it to a system through
83
+ * `uses`; the script then sets its parameters and evaluates it. So these two are the whole
84
+ * script-side surface of a blend tree, and they are enough for the case the language was given
85
+ * `uses` for.
86
+ */
87
+ define('blendSet', [
88
+ { name: 'tree', type: 'Blend' },
89
+ { name: 'parameter', type: 'String' },
90
+ { name: 'value', type: 'f32' },
91
+ ], 'void', ['animation.write'], false, "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."),
92
+ define('blendAt', [
93
+ { name: 'tree', type: 'Blend' },
94
+ { name: 'at', type: 'f32' },
95
+ { name: 'into', type: 'Pose' },
96
+ ], 'void', ['animation.write'], false, '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.'),
97
+ define('reach', [
98
+ { name: 'skeleton', type: 'Skeleton' },
99
+ { name: 'pose', type: 'Pose' },
100
+ { name: 'root', type: 'u32' },
101
+ { name: 'mid', type: 'u32' },
102
+ { name: 'tip', type: 'u32' },
103
+ { name: 'targetX', type: 'f32' },
104
+ { name: 'targetY', type: 'f32' },
105
+ { name: 'targetZ', type: 'f32' },
106
+ /*
107
+ * The pole hint decides the one thing a target cannot: which way the joint bends. Required
108
+ * for the same reason as the bind pose — a chain that is already straight has no plane to
109
+ * bend in, and without a hint the elbow flips unpredictably as a character turns. The engine
110
+ * requires it too; it is listed here so a reader of the signature knows what it is for.
111
+ */
112
+ { name: 'poleX', type: 'f32' },
113
+ { name: 'poleY', type: 'f32' },
114
+ { name: 'poleZ', type: 'f32' },
115
+ ], 'bool', ['animation.write'], false, '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.'),
116
+ /*
117
+ * **Root motion is two calls and it is a mistake to make only one of them.** `rootMotion` hands
118
+ * the caller the displacement and `stripRoot` takes it out of the pose; a script that extracts
119
+ * without stripping moves its character twice, at exactly double speed, with nothing failing.
120
+ * Both descriptions say so, because a script author has no signature to read.
121
+ *
122
+ * The displacement is read back through separate accessors, the shape `hitX` and `input.axisX`
123
+ * already use — a script has no way to receive an engine record. Here the accessors take the
124
+ * `Motion` rather than reading a scratch the last call wrote, so two characters extracted in one
125
+ * frame do not clobber each other; the raycast surface keys its scratch on the world for the
126
+ * same reason, and animation has no equivalent handle to key on.
127
+ */
128
+ define('motion', [], 'Motion', ['pure'], true, 'A new root displacement, at zero. Take one and keep it; nothing here allocates per frame.'),
129
+ define('rootMotion', [
130
+ { name: 'clip', type: 'Clip' },
131
+ { name: 'joint', type: 'u32' },
132
+ { name: 'from', type: 'f32' },
133
+ { name: 'to', type: 'f32' },
134
+ { name: 'into', type: 'Motion' },
135
+ ], 'void', ['animation.write'], false, '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.'),
136
+ define('stripRoot', [
137
+ { name: 'clip', type: 'Clip' },
138
+ { name: 'joint', type: 'u32' },
139
+ { name: 'pose', type: 'Pose' },
140
+ ], 'void', ['animation.write'], false, "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."),
141
+ define('motionX', [{ name: 'motion', type: 'Motion' }], 'f32', ['pure'], true, "The displacement along the root's own x axis at the earlier time."),
142
+ define('motionY', [{ name: 'motion', type: 'Motion' }], 'f32', ['pure'], true, "The displacement along the root's own y axis at the earlier time."),
143
+ define('motionZ', [{ name: 'motion', type: 'Motion' }], 'f32', ['pure'], true, "The displacement along the root's own z axis at the earlier time."),
144
+ define('motionTurnX', [{ name: 'motion', type: 'Motion' }], 'f32', ['pure'], true, 'The x part of the rotation the root turned through, as a quaternion.'),
145
+ define('motionTurnY', [{ name: 'motion', type: 'Motion' }], 'f32', ['pure'], true, 'The y part of the rotation the root turned through, as a quaternion.'),
146
+ define('motionTurnZ', [{ name: 'motion', type: 'Motion' }], 'f32', ['pure'], true, 'The z part of the rotation the root turned through, as a quaternion.'),
147
+ define('motionTurnW', [{ name: 'motion', type: 'Motion' }], 'f32', ['pure'], true, 'The w part of the rotation the root turned through, as a quaternion. One means it did not turn.'),
148
+ ];
149
+ /**
150
+ * The implementations.
151
+ *
152
+ * `blendTree` passes the bind pose straight through to the engine's optional parameter — the
153
+ * requirement lives entirely in the *description*, which is exactly where a capability boundary
154
+ * should hold a rule. The engine's own callers keep their option; script authors get the safe
155
+ * behaviour without the engine changing.
156
+ */
157
+ export function animationImplementation() {
158
+ return {
159
+ pose: (jointCount) => createPose(jointCount),
160
+ toRest: (pose, jointCount) => restPose(jointCount, pose),
161
+ sample: (clip, at, into) => sampleClip(clip, at, into),
162
+ blend: (a, b, amount, into) => blendPoses(a, b, amount, into),
163
+ blendSet: (tree, parameter, value) => tree.set(parameter, value),
164
+ blendAt: (tree, at, into) => tree.evaluate(at, into),
165
+ motion: () => createRootMotion(),
166
+ rootMotion: (clip, joint, from, to, into) => extractRootMotion(clip, joint, from, to, into),
167
+ stripRoot: (clip, joint, pose) => stripRootMotion(clip, joint, pose),
168
+ motionX: (motion) => motion.translation[0],
169
+ motionY: (motion) => motion.translation[1],
170
+ motionZ: (motion) => motion.translation[2],
171
+ motionTurnX: (motion) => motion.rotation[0],
172
+ motionTurnY: (motion) => motion.rotation[1],
173
+ motionTurnZ: (motion) => motion.rotation[2],
174
+ motionTurnW: (motion) => motion.rotation[3],
175
+ reach: (skeleton, pose, root, mid, tip, targetX, targetY, targetZ, poleX, poleY, poleZ) => solveTwoBone(skeleton, pose, root, mid, tip, [targetX, targetY, targetZ], [poleX, poleY, poleZ]),
176
+ };
177
+ }
@@ -0,0 +1,56 @@
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 } from '@driftengine/audio';
37
+ import { type CapabilityDefinition, type OpaqueType } from 'driftscript';
38
+ export declare const AUDIO_MODULE = "drift/audio";
39
+ /**
40
+ * A decoded sound, held by a script and passed back to the engine.
41
+ *
42
+ * Opaque on purpose: it is an `AudioBuffer`, and a script that could read its fields would be a
43
+ * script depending on a representation the engine cannot then change. The design calls these safe
44
+ * handles, and this is one.
45
+ */
46
+ export declare const AUDIO_TYPES: readonly OpaqueType[];
47
+ export declare const AUDIO_CAPABILITIES: readonly CapabilityDefinition[];
48
+ /**
49
+ * The implementations, which the host hands to a module's `__bind`.
50
+ *
51
+ * A plain object rather than anything the registry holds — R2: the registry describes and never
52
+ * becomes the call path, so this is a separate thing that happens to have matching keys. The
53
+ * registry's `implementation` strings name these; nothing enforces that mechanically, which is why
54
+ * a test asserts every definition has an implementation here.
55
+ */
56
+ export declare function audioImplementation(graph: AudioGraph, registry: SoundRegistry): Record<string, unknown>;
@@ -0,0 +1,126 @@
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 { defineCapability } from 'driftscript';
38
+ export const AUDIO_MODULE = 'drift/audio';
39
+ /**
40
+ * A decoded sound, held by a script and passed back to the engine.
41
+ *
42
+ * Opaque on purpose: it is an `AudioBuffer`, and a script that could read its fields would be a
43
+ * script depending on a representation the engine cannot then change. The design calls these safe
44
+ * handles, and this is one.
45
+ */
46
+ export const AUDIO_TYPES = [
47
+ {
48
+ module: AUDIO_MODULE,
49
+ name: 'Sound',
50
+ doc: 'A decoded sound, resolved from a slot. Held and passed back; never read into.',
51
+ },
52
+ ];
53
+ const define = (name, params, returns, effects, deterministic, doc, implementation) => defineCapability({
54
+ module: AUDIO_MODULE,
55
+ name,
56
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
57
+ params: [...params],
58
+ returns,
59
+ effects,
60
+ deterministic,
61
+ doc,
62
+ implementation,
63
+ });
64
+ export const AUDIO_CAPABILITIES = [
65
+ define('sound', [{ name: 'slot', type: 'String' }],
66
+ /*
67
+ * `Sound?`, and the `?` is the whole point.
68
+ *
69
+ * The registry resolves a slot by probing format candidates, and a missing file is ordinary
70
+ * rather than exceptional. Returning a bare `Sound` would make every script that forgot to
71
+ * check play silence and report success.
72
+ */
73
+ 'Sound?', ['pure'], true, 'Resolve a sound slot. Absent when nothing was registered or nothing decoded.', 'SoundRegistry.get'),
74
+ define('play', [
75
+ { name: 'sound', type: 'Sound' },
76
+ { name: 'gain', type: 'f32' },
77
+ ], 'void', ['audio.write'], false, 'Play a resolved sound through the mix.', 'AudioGraph.play'),
78
+ define('playPanned', [
79
+ { name: 'sound', type: 'Sound' },
80
+ { name: 'gain', type: 'f32' },
81
+ { name: 'pan', type: 'f32' },
82
+ ], 'void', ['audio.write'], false, 'Play a resolved sound at a stereo position, -1 left to 1 right.', 'AudioGraph.play'),
83
+ define('distanceGain', [
84
+ { name: 'distance', type: 'f32' },
85
+ { name: 'radius', type: 'f32' },
86
+ ], 'f32',
87
+ /*
88
+ * `pure`, and therefore callable from a `@deterministic` function.
89
+ *
90
+ * It is arithmetic over two numbers — the cheap positional function, not the HRTF path. That a
91
+ * capability in an otherwise effectful module is deterministic is the reason effects are
92
+ * declared per capability rather than per module.
93
+ */
94
+ ['pure'], true, 'How loud something is at a distance, falling off to nothing at the radius.', 'audio.distanceGain'),
95
+ define('stereoPan', [
96
+ { name: 'dx', type: 'f32' },
97
+ { name: 'dz', type: 'f32' },
98
+ { name: 'yaw', type: 'f32' },
99
+ ], 'f32', ['pure'], true, 'Where something sits in the stereo field, relative to a listener facing yaw.', 'audio.stereoPan'),
100
+ ];
101
+ const some = (value) => ({ tag: 'some', value });
102
+ const none = { tag: 'none' };
103
+ /**
104
+ * The implementations, which the host hands to a module's `__bind`.
105
+ *
106
+ * A plain object rather than anything the registry holds — R2: the registry describes and never
107
+ * becomes the call path, so this is a separate thing that happens to have matching keys. The
108
+ * registry's `implementation` strings name these; nothing enforces that mechanically, which is why
109
+ * a test asserts every definition has an implementation here.
110
+ */
111
+ export function audioImplementation(graph, registry) {
112
+ return {
113
+ sound(slot) {
114
+ const buffer = registry.get(slot);
115
+ return buffer === undefined ? none : some(buffer);
116
+ },
117
+ play(sound, gain) {
118
+ graph.play(sound, gain);
119
+ },
120
+ playPanned(sound, gain, pan) {
121
+ graph.play(sound, gain, pan);
122
+ },
123
+ distanceGain,
124
+ stereoPan,
125
+ };
126
+ }
@@ -0,0 +1,36 @@
1
+ import { BehaviorRunner } from '@driftengine/core';
2
+ import { type CapabilityDefinition, type OpaqueType } from 'driftscript';
3
+ export declare const BEHAVIOR_MODULE = "drift/behavior";
4
+ /**
5
+ * `drift/behavior` — driving and watching a behaviour tree from a script.
6
+ *
7
+ * **A script drives a tree; it does not author one.** A tree is nested data with a name on every
8
+ * node, and the useful thing to have in a script is not a builder for it but the two verbs a
9
+ * hand-written routine cannot offer: advance it, and ask what it is doing. Authoring stays in
10
+ * TypeScript beside the actions the tree calls, which is where the consumer's own verbs already
11
+ * are.
12
+ *
13
+ * **`doing` is the capability worth pointing at.** It answers whether the agent is currently inside
14
+ * a node of a given name, which is what turns a tree from something that runs into something a
15
+ * script can react to — play a sound while an agent is sheltering, show an icon while it is walking
16
+ * home, refuse to talk to it mid-errand — without the tree and the script keeping a second copy of
17
+ * the same state in step.
18
+ *
19
+ * The same effect seam as `drift/navigation` and for the same reason: `behavior.read` is in the
20
+ * language's deterministic set and `behavior.write` is not, so a `@deterministic` system may watch
21
+ * a tree and not tick one. See `bindings/navigation.ts`, where the argument and its reversal
22
+ * condition are written out.
23
+ */
24
+ export declare const BEHAVIOR_TYPES: readonly OpaqueType[];
25
+ export declare const BEHAVIOR_CAPABILITIES: readonly CapabilityDefinition[];
26
+ /** What a consumer hands the host so scripts can drive their agents' routines. */
27
+ export interface BehaviorServices {
28
+ /**
29
+ * What a character's tick is handed: a world, an entity, whatever the tree's own actions read.
30
+ *
31
+ * A function of the character rather than one context for all of them, because a routine is per
32
+ * agent and the thing its actions act on almost always is too.
33
+ */
34
+ context(character: BehaviorRunner<unknown>): unknown;
35
+ }
36
+ export declare function behaviorImplementation(services: BehaviorServices): Record<string, unknown>;
@@ -0,0 +1,101 @@
1
+ import { BehaviorRunner } from '@driftengine/core';
2
+ import { defineCapability } from 'driftscript';
3
+ export const BEHAVIOR_MODULE = 'drift/behavior';
4
+ /**
5
+ * `drift/behavior` — driving and watching a behaviour tree from a script.
6
+ *
7
+ * **A script drives a tree; it does not author one.** A tree is nested data with a name on every
8
+ * node, and the useful thing to have in a script is not a builder for it but the two verbs a
9
+ * hand-written routine cannot offer: advance it, and ask what it is doing. Authoring stays in
10
+ * TypeScript beside the actions the tree calls, which is where the consumer's own verbs already
11
+ * are.
12
+ *
13
+ * **`doing` is the capability worth pointing at.** It answers whether the agent is currently inside
14
+ * a node of a given name, which is what turns a tree from something that runs into something a
15
+ * script can react to — play a sound while an agent is sheltering, show an icon while it is walking
16
+ * home, refuse to talk to it mid-errand — without the tree and the script keeping a second copy of
17
+ * the same state in step.
18
+ *
19
+ * The same effect seam as `drift/navigation` and for the same reason: `behavior.read` is in the
20
+ * language's deterministic set and `behavior.write` is not, so a `@deterministic` system may watch
21
+ * a tree and not tick one. See `bindings/navigation.ts`, where the argument and its reversal
22
+ * condition are written out.
23
+ */
24
+ export const BEHAVIOR_TYPES = [
25
+ {
26
+ module: BEHAVIOR_MODULE,
27
+ name: 'Behavior',
28
+ doc: 'One agent running one behaviour tree: where it is in its routine.',
29
+ },
30
+ ];
31
+ const define = (name, params, returns, effects, doc, deterministic = true) => defineCapability({
32
+ module: BEHAVIOR_MODULE,
33
+ name,
34
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
35
+ params: [...params],
36
+ returns,
37
+ effects: effects,
38
+ deterministic,
39
+ doc,
40
+ implementation: `${BEHAVIOR_MODULE}.${name}`,
41
+ });
42
+ export const BEHAVIOR_CAPABILITIES = [
43
+ define('tick', [{ name: 'behavior', type: 'Behavior' }], 'i32', ['behavior.write'], 'Advance the routine one tick. Answers 0 failed, 1 finished, 2 still going. Does nothing while paused.', false),
44
+ define('step', [{ name: 'behavior', type: 'Behavior' }], 'i32', ['behavior.write'], 'Advance one tick even while paused, which is what a step button is.', false),
45
+ define('restart', [{ name: 'behavior', type: 'Behavior' }], 'void', ['behavior.write'], 'Forget where the routine had got to, so the next tick starts it from the top.', false),
46
+ define('setPaused', [
47
+ { name: 'behavior', type: 'Behavior' },
48
+ { name: 'paused', type: 'bool' },
49
+ ], 'void', ['behavior.write'], 'Hold this one agent while the world carries on.', false),
50
+ define('paused', [{ name: 'behavior', type: 'Behavior' }], 'bool', ['behavior.read'], 'Whether it is being held.'),
51
+ define('status', [{ name: 'behavior', type: 'Behavior' }], 'i32', ['behavior.read'], 'What the last tick answered, without ticking again.'),
52
+ /*
53
+ * **The reason a tree beats a loop, from a script's side.** A hand-written routine keeps its
54
+ * state where nothing else can see it, so a script that wants to react to what an agent is doing
55
+ * keeps a second copy and the two drift. This reads the tree's own answer.
56
+ */
57
+ define('doing', [
58
+ { name: 'behavior', type: 'Behavior' },
59
+ { name: 'node', type: 'String' },
60
+ ], 'bool', ['behavior.read'], "Whether the agent is currently inside a node of this name — the routine's own answer, not a copy of it."),
61
+ define('depth', [{ name: 'behavior', type: 'Behavior' }], 'i32', ['behavior.read'], 'How deep in the tree the last tick reached. For a debug readout.'),
62
+ ];
63
+ export function behaviorImplementation(services) {
64
+ /* Reused across every `doing` call: the path is at most the tree's depth and this is read every
65
+ tick by anything watching an agent. */
66
+ let scratch = new Uint32Array(32);
67
+ return {
68
+ tick: (character) => character.tick(services.context(character)),
69
+ step: (character) => character.stepOnce(services.context(character)),
70
+ restart: (character) => character.reset(),
71
+ setPaused: (character, paused) => {
72
+ character.paused = paused;
73
+ },
74
+ paused: (character) => character.paused,
75
+ status: (character) => character.status,
76
+ depth(character) {
77
+ scratch = fit(scratch, character);
78
+ return character.activePath(scratch);
79
+ },
80
+ doing(character, node) {
81
+ scratch = fit(scratch, character);
82
+ const depth = character.activePath(scratch);
83
+ for (let i = 0; i < depth; i++) {
84
+ if (character.nameOf(scratch[i] ?? 0) === node)
85
+ return true;
86
+ }
87
+ return false;
88
+ },
89
+ };
90
+ }
91
+ /**
92
+ * Grow the scratch to hold this tree's deepest possible path.
93
+ *
94
+ * A path cannot be longer than the tree has nodes, so this settles after the first call per tree
95
+ * and never runs again — which is what keeps a per-tick read allocation-free in the steady state
96
+ * without asking a consumer to declare a depth they would have to keep correct.
97
+ */
98
+ function fit(scratch, character) {
99
+ const needed = character.tree.nodeCount;
100
+ return scratch.length >= needed ? scratch : new Uint32Array(needed);
101
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `drift/chemistry` — Track P's surface, and the thirteenth `drift/*` module.
3
+ *
4
+ * Like every binding here, nothing in it is new engine code: a binding is a **description and a
5
+ * lookup**. Where the surface needed a quantity `@driftengine/chemistry` did not have — a moisture
6
+ * content on a dry basis, a heat release rate, a char depth — that function was grown in the package
7
+ * that owns the subsystem and this describes it, per the rule in `host.ts`.
8
+ *
9
+ * ---
10
+ *
11
+ * ## Parcels are `i32`, and that is `drift/physics`'s convention followed rather than improved on
12
+ *
13
+ * A body is an `i32` there, a negative value means none, and a script author who has learned one
14
+ * surface has learned this one. A second convention for the same shape of thing is the kind of
15
+ * inconsistency nobody can defend a year later. `§20.2`.
16
+ *
17
+ * ## `chemistry.write` is inside the determinism boundary
18
+ *
19
+ * `capability.ts` wrote the rule that decides it and this track is the one entitled to apply it: a
20
+ * parcel's composition and enthalpy are simulated quantities in exactly the sense a component's
21
+ * fields are. They integrate on the fixed step and touch no clock. A rule that refused
22
+ * `chemistry.write` to a `@deterministic` function would refuse the canonical operation of the
23
+ * package.
24
+ *
25
+ * ## Two capabilities the design lists are **not** here, and neither is an oversight
26
+ *
27
+ * - **`wake`.** `§14`'s sleeping, budgets and LOD tiers were the design's CH-6 and were displaced
28
+ * when the build re-scoped that phase onto the temperature reading the ignition criteria had to
29
+ * read. Nothing sleeps, so a capability to wake it would be a no-op wearing a name — which is the
30
+ * rule against silent no-ops, arriving through a binding. It goes in with `§14`.
31
+ * - **`setAmbient`.** The ambient is the far field every live chunk was filled from and every vented
32
+ * kilogram was booked against. Changing it mid-run would leave the field holding air drawn from an
33
+ * atmosphere that no longer exists and the conservation ledger measuring against a moved baseline —
34
+ * which would make `elementTotals`, the single most valuable assertion in the track, meaningless.
35
+ * A consumer who wants a different atmosphere constructs the field with one. **Reversal:**
36
+ * re-baselining every live chunk and re-booking the ledger, which is a feature rather than a
37
+ * setter.
38
+ */
39
+ import { AtmosphereField, ChemistryWorld, ParcelStore, SpeciesRegistry, SubstanceRegistry } from '@driftengine/chemistry';
40
+ import { type ComponentType } from '@driftengine/entities';
41
+ import { type CapabilityDefinition, type OpaqueType } from 'driftscript';
42
+ export declare const CHEMISTRY_MODULE = "drift/chemistry";
43
+ /**
44
+ * The three opaque types `§20.2` names.
45
+ *
46
+ * `Chemistry` is the world as `PhysicsWorld` is one. A `Substance` and a `Species` are registrations,
47
+ * and a script holds them to pass back rather than to read into — a field on either would be a
48
+ * promise about a representation the engine could then never change.
49
+ */
50
+ export declare const CHEMISTRY_TYPES: readonly OpaqueType[];
51
+ export declare const CHEMISTRY_CAPABILITIES: readonly CapabilityDefinition[];
52
+ /**
53
+ * A parcel handle on an entity, and `§19` says why it is declared here.
54
+ *
55
+ * Track M's design is explicit that `@driftengine/entities` declares **no components** — it is the
56
+ * model, not a library of them. And `@driftengine/chemistry` imports no engine package at all, so it
57
+ * cannot declare one either. This binding layer is the one place that already depends on core,
58
+ * entities and now chemistry at once, which makes it the only honest home.
59
+ *
60
+ * `standoff` is not a chemistry quantity and is here because it is the one a consumer always ends up
61
+ * adding: how far this was placed from the fire, which is what `1/r²` is measured over.
62
+ */
63
+ export declare const CHEMISTRY_COMPONENT: ComponentType;
64
+ /** What a consumer supplies to make `drift/chemistry` reachable. */
65
+ export interface ChemistryServices {
66
+ readonly world: ChemistryWorld;
67
+ readonly parcels: ParcelStore;
68
+ readonly air: AtmosphereField;
69
+ readonly substances: SubstanceRegistry;
70
+ readonly species: SpeciesRegistry;
71
+ }
72
+ export declare function chemistryImplementation(services: ChemistryServices): Record<string, unknown>;