@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,1604 @@
1
+ /**
2
+ * The wired `drift/*` surfaces other than audio, in one file because each is a table.
3
+ *
4
+ * A binding is a **description plus a lookup**. Nothing here is new engine code, and if a binding
5
+ * needed the engine to grow a function, that function would belong in the package owning the
6
+ * subsystem and this would then describe it like any other.
7
+ *
8
+ * **What is deliberately absent, and why**, because an empty module is a decision rather than an
9
+ * oversight:
10
+ *
11
+ * - **`drift/core`.** §11 gives its provider as `startLoop` and `LoopHooks`. A script does not call
12
+ * those — they are what *drives* a script. There is nothing for a script to call, so the module
13
+ * is not registered rather than registered empty, and a target does not claim it.
14
+ * - **`drift/xr`.** No provider: Track I is the one track nobody has taken, and the linker refuses
15
+ * the module by name. **Twelve names stood in this list beside it and every one of them has since
16
+ * been bound** — `render`, `ui`, `2d`, `terrain`, `ecs`, `prefab`, `navigation`, `behavior`,
17
+ * `network`, `rollback`, `editor` and `ai` — and this sentence went on calling them unprovided,
18
+ * because a comment listing what is missing has no guard on it and goes stale in the one
19
+ * direction nothing checks.
20
+ */
21
+ import {
22
+ type Aabb,
23
+ ActionMap,
24
+ CinematicCamera,
25
+ type Collider,
26
+ ColliderSet,
27
+ type KeyValueStore,
28
+ RemoteSaveStore,
29
+ MessageQueue,
30
+ PhysicsWorld,
31
+ type QueuedMessage,
32
+ type RayHit,
33
+ SceneNode,
34
+ type WindState,
35
+ angleDelta,
36
+ boxCollider,
37
+ createRayHit,
38
+ hashToUnit,
39
+ mulberry32,
40
+ wrapAngle,
41
+ } from '@driftengine/core';
42
+ import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
43
+
44
+ export const TIME_MODULE = 'drift/time';
45
+ export const RANDOM_MODULE = 'drift/random';
46
+ export const EVENTS_MODULE = 'drift/events';
47
+ export const PERSISTENCE_MODULE = 'drift/persistence';
48
+ export const SCENE_MODULE = 'drift/scene';
49
+ export const PHYSICS_MODULE = 'drift/physics';
50
+ export const INPUT_MODULE = 'drift/input';
51
+ export const CAMERA_MODULE = 'drift/camera';
52
+
53
+ const define = (
54
+ module: string,
55
+ name: string,
56
+ params: readonly { name: string; type: string }[],
57
+ returns: string,
58
+ effects: CapabilityDefinition['effects'],
59
+ deterministic: boolean,
60
+ doc: string,
61
+ ): CapabilityDefinition =>
62
+ defineCapability({
63
+ module,
64
+ name,
65
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
66
+ params: [...params],
67
+ returns,
68
+ effects,
69
+ deterministic,
70
+ doc,
71
+ implementation: `${module}.${name}`,
72
+ });
73
+
74
+ /**
75
+ * Handles a script holds and passes back, never reads into.
76
+ *
77
+ * A field on any of these would be a promise about a representation the engine could then never
78
+ * change — the design's safe-handle rule. `Node` is the one a script touches most, and it is still
79
+ * opaque: a position is read through a capability rather than off the object.
80
+ */
81
+ export const CORE_TYPES: readonly OpaqueType[] = [
82
+ { module: SCENE_MODULE, name: 'Node', doc: 'A node in the transform hierarchy.' },
83
+ /*
84
+ * The frame's weather, already sampled. **A state and not a profile**, which is the whole of why
85
+ * a script may read it inside the fixed step: a profile plus a clock is a wall-clock read, and a
86
+ * `WindState` is what the field said at a time somebody else chose.
87
+ */
88
+ { module: SCENE_MODULE, name: 'Wind', doc: 'The wind as it was sampled this frame.' },
89
+ {
90
+ module: PHYSICS_MODULE,
91
+ name: 'Colliders',
92
+ doc: 'A set of convex colliders with a spatial hash.',
93
+ },
94
+ /*
95
+ * `PhysicsWorld`, not `World`: `drift/ecs` already registers that name, and the registry refuses
96
+ * two hosts claiming one — at registration, where somebody can act on it, rather than at a
97
+ * resolution nobody can see. An entity world and a rigid body world are genuinely two things.
98
+ */
99
+ {
100
+ module: PHYSICS_MODULE,
101
+ name: 'PhysicsWorld',
102
+ doc: 'A rigid body world, stepped on the fixed clock.',
103
+ },
104
+ {
105
+ module: INPUT_MODULE,
106
+ name: 'Actions',
107
+ doc: 'A bound action map: what the player is pressing.',
108
+ },
109
+ { module: CAMERA_MODULE, name: 'Camera', doc: 'A cinematic camera.' },
110
+ {
111
+ module: EVENTS_MODULE,
112
+ name: 'Queue',
113
+ doc: 'A priority message queue with dedupe and a ceiling.',
114
+ },
115
+ { module: PERSISTENCE_MODULE, name: 'Store', doc: 'A key-value store the consumer supplied.' },
116
+ ];
117
+
118
+ /**
119
+ * `drift/time` — the loop's three clocks, and the one that is deterministic is not here.
120
+ *
121
+ * All three carry `clock.read`, so **none may be called from a `@deterministic` function** — and
122
+ * that is right even for the fixed step. A simulation is *given* its delta as a parameter; a
123
+ * simulation that reached out and read one has stopped being a function of its inputs, which is
124
+ * what replay depends on.
125
+ */
126
+ export const TIME_CAPABILITIES: readonly CapabilityDefinition[] = [
127
+ define(
128
+ TIME_MODULE,
129
+ 'fixedDelta',
130
+ [],
131
+ 'f32',
132
+ ['clock.read'],
133
+ false,
134
+ 'The fixed simulation step, in seconds. Prefer the `dt` your function was given.',
135
+ ),
136
+ define(
137
+ TIME_MODULE,
138
+ 'frameDelta',
139
+ [],
140
+ 'f32',
141
+ ['clock.read'],
142
+ false,
143
+ 'Seconds since the previous rendered frame, after clamping.',
144
+ ),
145
+ define(
146
+ TIME_MODULE,
147
+ 'wallDelta',
148
+ [],
149
+ 'f32',
150
+ ['clock.read'],
151
+ false,
152
+ 'Seconds of real time since the previous frame, unclamped. Outside the boundary in every sense.',
153
+ ),
154
+ define(
155
+ TIME_MODULE,
156
+ 'elapsed',
157
+ [],
158
+ 'f32',
159
+ ['clock.read'],
160
+ false,
161
+ 'Seconds since the loop started.',
162
+ ),
163
+ ];
164
+
165
+ /**
166
+ * `drift/random` — a seeded generator whose sequence is **frozen**.
167
+ *
168
+ * Deterministic by construction, so a `@deterministic` function may call it. That is the whole
169
+ * reason it is a capability rather than a `std/*` function: the sequence is a promise about *this
170
+ * engine's* stored replays, and a standard library that shipped its own would either break that
171
+ * promise or silently become the thing that defines it.
172
+ */
173
+ export const RANDOM_CAPABILITIES: readonly CapabilityDefinition[] = [
174
+ define(
175
+ RANDOM_MODULE,
176
+ 'unit',
177
+ [{ name: 'seed', type: 'u32' }],
178
+ 'f32',
179
+ ['pure'],
180
+ true,
181
+ 'A value from 0 to 1 for a seed. The same seed always gives the same value.',
182
+ ),
183
+ define(
184
+ RANDOM_MODULE,
185
+ 'range',
186
+ [
187
+ { name: 'seed', type: 'u32' },
188
+ { name: 'low', type: 'f32' },
189
+ { name: 'high', type: 'f32' },
190
+ ],
191
+ 'f32',
192
+ ['pure'],
193
+ true,
194
+ 'A value between two bounds for a seed.',
195
+ ),
196
+ define(
197
+ RANDOM_MODULE,
198
+ 'index',
199
+ [
200
+ { name: 'seed', type: 'u32' },
201
+ { name: 'count', type: 'u32' },
202
+ ],
203
+ 'u32',
204
+ ['pure'],
205
+ true,
206
+ 'An index into a collection of `count` items. Zero for an empty one.',
207
+ ),
208
+ ];
209
+
210
+ /** `drift/events` — the engine's priority queue, with dedupe over a window and a backlog ceiling. */
211
+ export const EVENTS_CAPABILITIES: readonly CapabilityDefinition[] = [
212
+ define(
213
+ EVENTS_MODULE,
214
+ 'push',
215
+ [
216
+ { name: 'queue', type: 'Queue' },
217
+ { name: 'id', type: 'String' },
218
+ { name: 'priority', type: 'f32' },
219
+ { name: 'holdFor', type: 'f32' },
220
+ ],
221
+ 'void',
222
+ ['pure'],
223
+ true,
224
+ 'Queue a message by identity. A repeat of the same id inside the dedupe window is dropped.',
225
+ ),
226
+ define(
227
+ EVENTS_MODULE,
228
+ 'current',
229
+ [{ name: 'queue', type: 'Queue' }],
230
+ 'String?',
231
+ ['pure'],
232
+ true,
233
+ "The id of the message showing now, if any. The queue carries identity and priority; the words are the consumer's.",
234
+ ),
235
+ define(
236
+ EVENTS_MODULE,
237
+ 'clear',
238
+ [{ name: 'queue', type: 'Queue' }],
239
+ 'void',
240
+ ['pure'],
241
+ true,
242
+ 'Drop everything queued.',
243
+ ),
244
+ ];
245
+
246
+ /** `drift/persistence` — the store the consumer supplied, which may be a browser, a server or nothing. */
247
+ export const PERSISTENCE_CAPABILITIES: readonly CapabilityDefinition[] = [
248
+ define(
249
+ PERSISTENCE_MODULE,
250
+ 'read',
251
+ [
252
+ { name: 'store', type: 'Store' },
253
+ { name: 'key', type: 'String' },
254
+ ],
255
+ 'String?',
256
+ ['persistence.read'],
257
+ false,
258
+ 'What is stored under a key. Absent when nothing is, which is ordinary rather than exceptional.',
259
+ ),
260
+ define(
261
+ PERSISTENCE_MODULE,
262
+ 'write',
263
+ [
264
+ { name: 'store', type: 'Store' },
265
+ { name: 'key', type: 'String' },
266
+ { name: 'value', type: 'String' },
267
+ ],
268
+ 'void',
269
+ ['persistence.write'],
270
+ false,
271
+ 'Store a value under a key.',
272
+ ),
273
+ define(
274
+ PERSISTENCE_MODULE,
275
+ 'remove',
276
+ [
277
+ { name: 'store', type: 'Store' },
278
+ { name: 'key', type: 'String' },
279
+ ],
280
+ 'void',
281
+ ['persistence.write'],
282
+ false,
283
+ 'Forget a key.',
284
+ ),
285
+ /*
286
+ * **The two a script needs when the store answers later, and the one it cannot have.**
287
+ *
288
+ * A `RemoteSaveStore` writes into a queue that drains behind the synchronous surface, so a write
289
+ * that reads back correctly may not have landed anywhere. A script drawing a "saving" indicator
290
+ * has to be able to ask. Both answer honestly for a *synchronous* store too — a browser store
291
+ * has nothing pending and is always idle — so this is not a capability that only works for one
292
+ * implementation.
293
+ *
294
+ * **`flush` is not bound**, and the reason is the language rather than the engine: it answers a
295
+ * promise and DriftScript has no await, so a binding would either block or hand back something
296
+ * a script cannot hold. A consumer flushes from TypeScript, at the one place it knows it is
297
+ * leaving.
298
+ */
299
+ define(
300
+ PERSISTENCE_MODULE,
301
+ 'saveStatus',
302
+ [{ name: 'store', type: 'Store' }],
303
+ 'String',
304
+ ['persistence.read'],
305
+ false,
306
+ 'Whether writes have landed: `idle`, `pending`, `saving` or `failed`. A store that writes synchronously is always `idle`, which is the true answer rather than a stub.',
307
+ ),
308
+ define(
309
+ PERSISTENCE_MODULE,
310
+ 'pendingSaves',
311
+ [{ name: 'store', type: 'Store' }],
312
+ 'u32',
313
+ ['persistence.read'],
314
+ false,
315
+ 'How many keys are waiting to reach the backend. Zero for a store that writes synchronously.',
316
+ ),
317
+ ];
318
+
319
+ /**
320
+ * `drift/scene` — the transform hierarchy from Phase 1.1.
321
+ *
322
+ * Reads are deterministic and writes are not, which is the boundary drawn per capability rather
323
+ * than per module. A simulation may ask where something is; moving it is a change to the world.
324
+ */
325
+ export const SCENE_CAPABILITIES: readonly CapabilityDefinition[] = [
326
+ define(
327
+ SCENE_MODULE,
328
+ 'positionX',
329
+ [{ name: 'node', type: 'Node' }],
330
+ 'f32',
331
+ ['scene.read'],
332
+ true,
333
+ "A node's local x.",
334
+ ),
335
+ define(
336
+ SCENE_MODULE,
337
+ 'positionY',
338
+ [{ name: 'node', type: 'Node' }],
339
+ 'f32',
340
+ ['scene.read'],
341
+ true,
342
+ "A node's local y.",
343
+ ),
344
+ define(
345
+ SCENE_MODULE,
346
+ 'positionZ',
347
+ [{ name: 'node', type: 'Node' }],
348
+ 'f32',
349
+ ['scene.read'],
350
+ true,
351
+ "A node's local z.",
352
+ ),
353
+ define(
354
+ SCENE_MODULE,
355
+ 'setPosition',
356
+ [
357
+ { name: 'node', type: 'Node' },
358
+ { name: 'x', type: 'f32' },
359
+ { name: 'y', type: 'f32' },
360
+ { name: 'z', type: 'f32' },
361
+ ],
362
+ 'void',
363
+ ['scene.write'],
364
+ false,
365
+ 'Move a node, marking it and its subtree for a world update.',
366
+ ),
367
+ define(
368
+ SCENE_MODULE,
369
+ 'setScale',
370
+ [
371
+ { name: 'node', type: 'Node' },
372
+ { name: 'x', type: 'f32' },
373
+ { name: 'y', type: 'f32' },
374
+ { name: 'z', type: 'f32' },
375
+ ],
376
+ 'void',
377
+ ['scene.write'],
378
+ false,
379
+ 'Scale a node.',
380
+ ),
381
+ define(
382
+ SCENE_MODULE,
383
+ 'setRotation',
384
+ [
385
+ { name: 'node', type: 'Node' },
386
+ { name: 'x', type: 'f32' },
387
+ { name: 'y', type: 'f32' },
388
+ { name: 'z', type: 'f32' },
389
+ { name: 'radians', type: 'f32' },
390
+ ],
391
+ 'void',
392
+ ['scene.write'],
393
+ false,
394
+ 'Rotate a node about an axis.',
395
+ ),
396
+ /*
397
+ * **The wind, read by a behaviour rather than by the renderer.**
398
+ *
399
+ * The per-vertex channel bends geometry to this same wind in the vertex stage; these four let a
400
+ * script ask the question a behaviour asks — take shelter in a gust, luff a sail, turn downwind.
401
+ *
402
+ * **Deterministic, and that is a claim rather than a convenience.** `wind.ts` builds the signal
403
+ * as a fundamental plus integer harmonics, repeating exactly at the fundamental's period,
404
+ * precisely so a replay watched an hour later sees the gust the original run saw. A non-integer
405
+ * harmonic would look organic and never close, and these would then be reading a wall clock
406
+ * through a handle. The comment in that file is the thing holding this up.
407
+ *
408
+ * **`f32` and not `float`**, which every capability here takes: `float` is width-polymorphic and
409
+ * one returning it must also take one, or `defineCapability` throws. These take a `Wind`.
410
+ */
411
+ define(
412
+ SCENE_MODULE,
413
+ 'windDirectionX',
414
+ [{ name: 'wind', type: 'Wind' }],
415
+ 'f32',
416
+ ['scene.read'],
417
+ true,
418
+ "The prevailing wind's x, normalised; zero when there is no wind to have a direction.",
419
+ ),
420
+ define(
421
+ SCENE_MODULE,
422
+ 'windDirectionZ',
423
+ [{ name: 'wind', type: 'Wind' }],
424
+ 'f32',
425
+ ['scene.read'],
426
+ true,
427
+ "The prevailing wind's z, normalised; zero when there is no wind to have a direction.",
428
+ ),
429
+ define(
430
+ SCENE_MODULE,
431
+ 'windSpeed',
432
+ [{ name: 'wind', type: 'Wind' }],
433
+ 'f32',
434
+ ['scene.read'],
435
+ true,
436
+ 'How hard it is blowing, metres a second, never negative.',
437
+ ),
438
+ define(
439
+ SCENE_MODULE,
440
+ 'windGust',
441
+ [{ name: 'wind', type: 'Wind' }],
442
+ 'f32',
443
+ ['scene.read'],
444
+ true,
445
+ 'The gust alone, -1 to 1, for a behaviour that wants the deviation and not the total.',
446
+ ),
447
+ define(
448
+ SCENE_MODULE,
449
+ 'distance',
450
+ [
451
+ { name: 'a', type: 'Node' },
452
+ { name: 'b', type: 'Node' },
453
+ ],
454
+ 'f32',
455
+ ['scene.read'],
456
+ true,
457
+ 'The distance between two nodes, in local space.',
458
+ ),
459
+ ];
460
+
461
+ /**
462
+ * `drift/physics` — bodies, contacts, queries and forces. **Not joints, not the controller and not
463
+ * ragdolls**, which is what this line said until 2026-08-27 and what none of the thirty-six
464
+ * capabilities below has ever done.
465
+ *
466
+ * **This module was two read-only functions until 2026-08-26**, and its own comment said why: there
467
+ * was no solver to write into, and a capability that pretended otherwise would have been the mock
468
+ * the design refused. Track B built the solver, so the surface is real — but the sentence that
469
+ * announced it was written from the *track's* deliverables rather than from this list, which is the
470
+ * same failure as a binding written from memory of what a camera ought to have. A reader looking
471
+ * for `joint` here found a comment that promised one.
472
+ *
473
+ * **Six of the thirty-six are `nearestWithin` and its accessors, added 2026-08-28**, and they exist
474
+ * because a consumer reported that `anyWithin` answers *whether* and never *what*. A script could
475
+ * already find the nearest entity — `drift/ecs` grew `findNearest` and `nearest` for that — and
476
+ * could not find the nearest wall, so what is in reach of a character was decided in TypeScript.
477
+ *
478
+ * **The two `within` questions measure different volumes, and the names cannot say so on their
479
+ * own.** `anyWithin` tests the axis-aligned *box* around the sphere, because that is what the
480
+ * collider hash indexes and testing anything narrower would cost a pass it does not need for a
481
+ * yes-or-no. `nearestWithin` is already walking each candidate to rank it, so it measures the
482
+ * sphere exactly and answers false where `anyWithin` answers true — a collider that reaches only
483
+ * the box corners. Both say which they do, on themselves, because a reader comparing the two names
484
+ * would have no reason to suspect it.
485
+ *
486
+ * **It measures to a collider's bounds rather than to the shape inside them**, which is the one
487
+ * place this surface is coarser than the sweep beside it. `ColliderSet`'s own header draws that
488
+ * line: the bounds are the broad phase and the shape resolves a contact, so a proximity answer that
489
+ * ran the convex sweep per candidate per frame would be paying contact prices for a distance. The
490
+ * documentation on each accessor says bounds, so nobody reads a banked slab's box as its surface.
491
+ *
492
+ * **What is missing, and the design question that is why it is still missing**: `JointDesc` carries
493
+ * twenty optional fields, so a script-side constructor is either an enormous signature or a builder
494
+ * this language has no shape for. That is a decision somebody has to take deliberately, and taking
495
+ * it badly would put a wrong-by-construction surface in front of every script author. Recorded here
496
+ * rather than guessed at.
497
+ *
498
+ * **Two parameters are required here that TypeScript leaves optional**, which is step four of the
499
+ * note at the top of `host.ts`, and both are the same shape of trap — an omission that produces a
500
+ * *wrong result* rather than an error:
501
+ *
502
+ * - **A raycast's layer mask.** Omitted, the first thing a ray finds is very often the body it
503
+ * started inside, because `raycastWorld` reports that body at fraction zero deliberately. A
504
+ * TypeScript caller has the context to know when that matters; a script author meets it first and
505
+ * reads it as the engine seeing through walls.
506
+ * - **An impulse's application point.** Omitted, the impulse goes through the centre of mass and
507
+ * produces no rotation whatever. Nothing throws; a crate simply slides instead of tumbling, which
508
+ * reads as a broken impulse rather than as a missing argument.
509
+ *
510
+ * **Every body and event index is `i32`, not `u32`.** `hitBody` has to be able to answer −1, and a
511
+ * script has no cast, so a mixed surface would make the obvious code — raycast, check the body,
512
+ * apply an impulse to it — impossible to write. One signed type throughout costs nothing and is
513
+ * what a query result already is.
514
+ *
515
+ * **A raycast's result is read back through separate accessors**, in the same shape `input.axisX`
516
+ * and `input.axisY` already use for a vector. A script has no way to receive an engine record, and
517
+ * inventing a flattened signature would drift from the real one the first time a hit gains a field —
518
+ * which is the reasoning `drift/camera` gives for leaving `cut` out. *What it costs* is that a hit
519
+ * is only valid until the next `raycast` on the same world, and the documentation says so on every
520
+ * accessor.
521
+ */
522
+ export const PHYSICS_CAPABILITIES: readonly CapabilityDefinition[] = [
523
+ define(
524
+ PHYSICS_MODULE,
525
+ 'colliderCount',
526
+ [{ name: 'colliders', type: 'Colliders' }],
527
+ 'u32',
528
+ ['physics.read'],
529
+ true,
530
+ 'How many colliders a set holds.',
531
+ ),
532
+ define(
533
+ PHYSICS_MODULE,
534
+ 'colliderCapacity',
535
+ [{ name: 'colliders', type: 'Colliders' }],
536
+ 'u32',
537
+ ['physics.read'],
538
+ true,
539
+ 'How many slots the set has allocated, live or not. Equal to `colliderCount` until a group is removed; after that the live colliders are sparse within it.',
540
+ ),
541
+ define(
542
+ PHYSICS_MODULE,
543
+ 'colliderBytes',
544
+ [{ name: 'colliders', type: 'Colliders' }],
545
+ 'u32',
546
+ ['physics.read'],
547
+ true,
548
+ 'What the set costs in memory, in bytes. The slots and shapes are exact; the spatial index includes an estimate of per-object overhead a JavaScript engine does not expose.',
549
+ ),
550
+ /*
551
+ * **A builder rather than one call, because a script has no list to hand across.** A region is
552
+ * many boxes and one group, so a per-box call returning a group would mint one group per box and
553
+ * give a streamer nothing to drop.
554
+ *
555
+ * **`end` returns the handle, not `begin`.** The group does not exist until the boxes are in, and
556
+ * a handle issued before that would be a promise the binding could only keep by inventing an id
557
+ * the engine had not minted.
558
+ */
559
+ define(
560
+ PHYSICS_MODULE,
561
+ 'beginColliderGroup',
562
+ [{ name: 'colliders', type: 'Colliders' }],
563
+ 'void',
564
+ ['physics.write'],
565
+ false,
566
+ 'Open a group. Add boxes with `addColliderBox`, then `endColliderGroup` to put them in and get the handle back.',
567
+ ),
568
+ define(
569
+ PHYSICS_MODULE,
570
+ 'addColliderBox',
571
+ [
572
+ { name: 'colliders', type: 'Colliders' },
573
+ { name: 'cx', type: 'f32' },
574
+ { name: 'cy', type: 'f32' },
575
+ { name: 'cz', type: 'f32' },
576
+ { name: 'hx', type: 'f32' },
577
+ { name: 'hy', type: 'f32' },
578
+ { name: 'hz', type: 'f32' },
579
+ ],
580
+ 'void',
581
+ ['physics.write'],
582
+ false,
583
+ 'A box in the open group, by centre and half-extents — the same numbers a mesh builder takes, so nothing is authored twice. Nothing is queryable until `endColliderGroup`.',
584
+ ),
585
+ define(
586
+ PHYSICS_MODULE,
587
+ 'endColliderGroup',
588
+ [{ name: 'colliders', type: 'Colliders' }],
589
+ 'u32',
590
+ ['physics.write'],
591
+ false,
592
+ 'Put the open group into the set and return the handle that drops it again. Keep the handle: it is the only way to remove those colliders.',
593
+ ),
594
+ define(
595
+ PHYSICS_MODULE,
596
+ 'removeColliderGroup',
597
+ [
598
+ { name: 'colliders', type: 'Colliders' },
599
+ { name: 'group', type: 'u32' },
600
+ ],
601
+ 'void',
602
+ ['physics.write'],
603
+ false,
604
+ 'Drop a group and everything in it. Refuses a handle the set does not hold, and refuses the colliders the set was constructed with — a set meant to stream is constructed empty.',
605
+ ),
606
+ define(
607
+ PHYSICS_MODULE,
608
+ 'anyWithin',
609
+ [
610
+ { name: 'colliders', type: 'Colliders' },
611
+ { name: 'x', type: 'f32' },
612
+ { name: 'y', type: 'f32' },
613
+ { name: 'z', type: 'f32' },
614
+ { name: 'radius', type: 'f32' },
615
+ ],
616
+ 'bool',
617
+ ['physics.read'],
618
+ true,
619
+ 'Whether any collider overlaps the *box* around a sphere. The cheapest answer there is, and it over-reports a collider that only reaches the box corners. `nearestWithin` measures the sphere.',
620
+ ),
621
+ define(
622
+ PHYSICS_MODULE,
623
+ 'nearestWithin',
624
+ [
625
+ { name: 'colliders', type: 'Colliders' },
626
+ { name: 'x', type: 'f32' },
627
+ { name: 'y', type: 'f32' },
628
+ { name: 'z', type: 'f32' },
629
+ { name: 'radius', type: 'f32' },
630
+ ],
631
+ 'bool',
632
+ ['physics.read'],
633
+ true,
634
+ 'Find the closest collider within a radius and record it. Read the result with the `near` functions. Valid until the next `nearestWithin` on this set.',
635
+ ),
636
+ define(
637
+ PHYSICS_MODULE,
638
+ 'nearCollider',
639
+ [{ name: 'colliders', type: 'Colliders' }],
640
+ 'i32',
641
+ ['physics.read'],
642
+ true,
643
+ 'The collider the last `nearestWithin` on this set found, or \u22121.',
644
+ ),
645
+ define(
646
+ PHYSICS_MODULE,
647
+ 'nearX',
648
+ [{ name: 'colliders', type: 'Colliders' }],
649
+ 'f32',
650
+ ['physics.read'],
651
+ true,
652
+ 'The closest point on that collider\u2019s bounds, along x \u2014 the point on the wall rather than the wall\u2019s centre.',
653
+ ),
654
+ define(
655
+ PHYSICS_MODULE,
656
+ 'nearY',
657
+ [{ name: 'colliders', type: 'Colliders' }],
658
+ 'f32',
659
+ ['physics.read'],
660
+ true,
661
+ 'The closest point on that collider\u2019s bounds, along y.',
662
+ ),
663
+ define(
664
+ PHYSICS_MODULE,
665
+ 'nearZ',
666
+ [{ name: 'colliders', type: 'Colliders' }],
667
+ 'f32',
668
+ ['physics.read'],
669
+ true,
670
+ 'The closest point on that collider\u2019s bounds, along z.',
671
+ ),
672
+ define(
673
+ PHYSICS_MODULE,
674
+ 'nearDistance',
675
+ [{ name: 'colliders', type: 'Colliders' }],
676
+ 'f32',
677
+ ['physics.read'],
678
+ true,
679
+ 'How far that collider\u2019s bounds are, in metres. Zero when the query point is inside them.',
680
+ ),
681
+
682
+ define(
683
+ PHYSICS_MODULE,
684
+ 'bodyCount',
685
+ [{ name: 'world', type: 'PhysicsWorld' }],
686
+ 'i32',
687
+ ['physics.read'],
688
+ true,
689
+ 'How many bodies the world holds.',
690
+ ),
691
+ define(
692
+ PHYSICS_MODULE,
693
+ 'bodyX',
694
+ [
695
+ { name: 'world', type: 'PhysicsWorld' },
696
+ { name: 'body', type: 'i32' },
697
+ ],
698
+ 'f32',
699
+ ['physics.read'],
700
+ true,
701
+ "A body's world position along x.",
702
+ ),
703
+ define(
704
+ PHYSICS_MODULE,
705
+ 'bodyY',
706
+ [
707
+ { name: 'world', type: 'PhysicsWorld' },
708
+ { name: 'body', type: 'i32' },
709
+ ],
710
+ 'f32',
711
+ ['physics.read'],
712
+ true,
713
+ "A body's world position along y.",
714
+ ),
715
+ define(
716
+ PHYSICS_MODULE,
717
+ 'bodyZ',
718
+ [
719
+ { name: 'world', type: 'PhysicsWorld' },
720
+ { name: 'body', type: 'i32' },
721
+ ],
722
+ 'f32',
723
+ ['physics.read'],
724
+ true,
725
+ "A body's world position along z.",
726
+ ),
727
+ define(
728
+ PHYSICS_MODULE,
729
+ 'bodyVelX',
730
+ [
731
+ { name: 'world', type: 'PhysicsWorld' },
732
+ { name: 'body', type: 'i32' },
733
+ ],
734
+ 'f32',
735
+ ['physics.read'],
736
+ true,
737
+ "A body's velocity along x, in metres per second.",
738
+ ),
739
+ define(
740
+ PHYSICS_MODULE,
741
+ 'bodyVelY',
742
+ [
743
+ { name: 'world', type: 'PhysicsWorld' },
744
+ { name: 'body', type: 'i32' },
745
+ ],
746
+ 'f32',
747
+ ['physics.read'],
748
+ true,
749
+ "A body's velocity along y, in metres per second.",
750
+ ),
751
+ define(
752
+ PHYSICS_MODULE,
753
+ 'bodyVelZ',
754
+ [
755
+ { name: 'world', type: 'PhysicsWorld' },
756
+ { name: 'body', type: 'i32' },
757
+ ],
758
+ 'f32',
759
+ ['physics.read'],
760
+ true,
761
+ "A body's velocity along z, in metres per second.",
762
+ ),
763
+ define(
764
+ PHYSICS_MODULE,
765
+ 'bodyMass',
766
+ [
767
+ { name: 'world', type: 'PhysicsWorld' },
768
+ { name: 'body', type: 'i32' },
769
+ ],
770
+ 'f32',
771
+ ['physics.read'],
772
+ true,
773
+ "A body's mass in kilograms, or zero where it has none because it is static or kinematic.",
774
+ ),
775
+ define(
776
+ PHYSICS_MODULE,
777
+ 'sleeping',
778
+ [
779
+ { name: 'world', type: 'PhysicsWorld' },
780
+ { name: 'body', type: 'i32' },
781
+ ],
782
+ 'bool',
783
+ ['physics.read'],
784
+ true,
785
+ 'Whether a body has been still long enough to stop being solved.',
786
+ ),
787
+
788
+ define(
789
+ PHYSICS_MODULE,
790
+ 'raycast',
791
+ [
792
+ { name: 'world', type: 'PhysicsWorld' },
793
+ { name: 'x', type: 'f32' },
794
+ { name: 'y', type: 'f32' },
795
+ { name: 'z', type: 'f32' },
796
+ { name: 'dx', type: 'f32' },
797
+ { name: 'dy', type: 'f32' },
798
+ { name: 'dz', type: 'f32' },
799
+ { name: 'maxDistance', type: 'f32' },
800
+ { name: 'mask', type: 'i32' },
801
+ ],
802
+ 'bool',
803
+ ['physics.read'],
804
+ true,
805
+ 'Cast a ray and record what it hit. `mask` is required: without one the first thing a ray finds is often the body it started inside. Read the result with the `hit` functions.',
806
+ ),
807
+ define(
808
+ PHYSICS_MODULE,
809
+ 'hitBody',
810
+ [{ name: 'world', type: 'PhysicsWorld' }],
811
+ 'i32',
812
+ ['physics.read'],
813
+ true,
814
+ 'The body the last `raycast` on this world found, or −1. Valid until the next raycast.',
815
+ ),
816
+ define(
817
+ PHYSICS_MODULE,
818
+ 'hitX',
819
+ [{ name: 'world', type: 'PhysicsWorld' }],
820
+ 'f32',
821
+ ['physics.read'],
822
+ true,
823
+ 'Where the last raycast struck, along x. Valid until the next raycast.',
824
+ ),
825
+ define(
826
+ PHYSICS_MODULE,
827
+ 'hitY',
828
+ [{ name: 'world', type: 'PhysicsWorld' }],
829
+ 'f32',
830
+ ['physics.read'],
831
+ true,
832
+ 'Where the last raycast struck, along y. Valid until the next raycast.',
833
+ ),
834
+ define(
835
+ PHYSICS_MODULE,
836
+ 'hitZ',
837
+ [{ name: 'world', type: 'PhysicsWorld' }],
838
+ 'f32',
839
+ ['physics.read'],
840
+ true,
841
+ 'Where the last raycast struck, along z. Valid until the next raycast.',
842
+ ),
843
+ define(
844
+ PHYSICS_MODULE,
845
+ 'hitNormalX',
846
+ [{ name: 'world', type: 'PhysicsWorld' }],
847
+ 'f32',
848
+ ['physics.read'],
849
+ true,
850
+ 'The surface normal the last raycast struck, along x.',
851
+ ),
852
+ define(
853
+ PHYSICS_MODULE,
854
+ 'hitNormalY',
855
+ [{ name: 'world', type: 'PhysicsWorld' }],
856
+ 'f32',
857
+ ['physics.read'],
858
+ true,
859
+ 'The surface normal the last raycast struck, along y.',
860
+ ),
861
+ define(
862
+ PHYSICS_MODULE,
863
+ 'hitNormalZ',
864
+ [{ name: 'world', type: 'PhysicsWorld' }],
865
+ 'f32',
866
+ ['physics.read'],
867
+ true,
868
+ 'The surface normal the last raycast struck, along z.',
869
+ ),
870
+ define(
871
+ PHYSICS_MODULE,
872
+ 'hitFraction',
873
+ [{ name: 'world', type: 'PhysicsWorld' }],
874
+ 'f32',
875
+ ['physics.read'],
876
+ true,
877
+ 'How far along the ray the last hit was, from 0 to 1.',
878
+ ),
879
+
880
+ define(
881
+ PHYSICS_MODULE,
882
+ 'contactCount',
883
+ [{ name: 'world', type: 'PhysicsWorld' }],
884
+ 'i32',
885
+ ['physics.read'],
886
+ true,
887
+ 'How many contact events the last step produced. Drain them after stepping, never during.',
888
+ ),
889
+ define(
890
+ PHYSICS_MODULE,
891
+ 'contactKind',
892
+ [
893
+ { name: 'world', type: 'PhysicsWorld' },
894
+ { name: 'index', type: 'i32' },
895
+ ],
896
+ 'i32',
897
+ ['physics.read'],
898
+ true,
899
+ "An event's kind: 0 entered, 1 still touching, 2 left.",
900
+ ),
901
+ define(
902
+ PHYSICS_MODULE,
903
+ 'contactA',
904
+ [
905
+ { name: 'world', type: 'PhysicsWorld' },
906
+ { name: 'index', type: 'i32' },
907
+ ],
908
+ 'i32',
909
+ ['physics.read'],
910
+ true,
911
+ 'The lower-indexed body of a contact event.',
912
+ ),
913
+ define(
914
+ PHYSICS_MODULE,
915
+ 'contactB',
916
+ [
917
+ { name: 'world', type: 'PhysicsWorld' },
918
+ { name: 'index', type: 'i32' },
919
+ ],
920
+ 'i32',
921
+ ['physics.read'],
922
+ true,
923
+ 'The higher-indexed body of a contact event.',
924
+ ),
925
+
926
+ define(
927
+ PHYSICS_MODULE,
928
+ 'applyImpulse',
929
+ [
930
+ { name: 'world', type: 'PhysicsWorld' },
931
+ { name: 'body', type: 'i32' },
932
+ { name: 'px', type: 'f32' },
933
+ { name: 'py', type: 'f32' },
934
+ { name: 'pz', type: 'f32' },
935
+ { name: 'atX', type: 'f32' },
936
+ { name: 'atY', type: 'f32' },
937
+ { name: 'atZ', type: 'f32' },
938
+ ],
939
+ 'void',
940
+ ['physics.write'],
941
+ false,
942
+ 'Apply an impulse at a world point. The point is required: through the centre of mass an impulse produces no rotation at all, which reads as a broken impulse rather than a missing argument.',
943
+ ),
944
+ define(
945
+ PHYSICS_MODULE,
946
+ 'applyForce',
947
+ [
948
+ { name: 'world', type: 'PhysicsWorld' },
949
+ { name: 'body', type: 'i32' },
950
+ { name: 'fx', type: 'f32' },
951
+ { name: 'fy', type: 'f32' },
952
+ { name: 'fz', type: 'f32' },
953
+ { name: 'dt', type: 'f32' },
954
+ { name: 'atX', type: 'f32' },
955
+ { name: 'atY', type: 'f32' },
956
+ { name: 'atZ', type: 'f32' },
957
+ ],
958
+ 'void',
959
+ ['physics.write'],
960
+ false,
961
+ 'A force applied for one step, which is an impulse of force times dt. The point is required for the same reason.',
962
+ ),
963
+ define(
964
+ PHYSICS_MODULE,
965
+ 'setVelocity',
966
+ [
967
+ { name: 'world', type: 'PhysicsWorld' },
968
+ { name: 'body', type: 'i32' },
969
+ { name: 'vx', type: 'f32' },
970
+ { name: 'vy', type: 'f32' },
971
+ { name: 'vz', type: 'f32' },
972
+ ],
973
+ 'void',
974
+ ['physics.write'],
975
+ false,
976
+ "Set a body's velocity outright, waking its island.",
977
+ ),
978
+ define(
979
+ PHYSICS_MODULE,
980
+ 'setPosition',
981
+ [
982
+ { name: 'world', type: 'PhysicsWorld' },
983
+ { name: 'body', type: 'i32' },
984
+ { name: 'x', type: 'f32' },
985
+ { name: 'y', type: 'f32' },
986
+ { name: 'z', type: 'f32' },
987
+ ],
988
+ 'void',
989
+ ['physics.write'],
990
+ false,
991
+ 'Move a body outright. A teleport rather than a push: the broadphase moves with it.',
992
+ ),
993
+ define(
994
+ PHYSICS_MODULE,
995
+ 'wake',
996
+ [
997
+ { name: 'world', type: 'PhysicsWorld' },
998
+ { name: 'body', type: 'i32' },
999
+ ],
1000
+ 'void',
1001
+ ['physics.write'],
1002
+ false,
1003
+ 'Wake a body and everything sharing its island, because a sleeping neighbour would settle it again.',
1004
+ ),
1005
+ define(
1006
+ PHYSICS_MODULE,
1007
+ 'setGravity',
1008
+ [
1009
+ { name: 'world', type: 'PhysicsWorld' },
1010
+ { name: 'x', type: 'f32' },
1011
+ { name: 'y', type: 'f32' },
1012
+ { name: 'z', type: 'f32' },
1013
+ ],
1014
+ 'void',
1015
+ ['physics.write'],
1016
+ false,
1017
+ "The world's gravity, in metres per second squared.",
1018
+ ),
1019
+ ];
1020
+
1021
+ /** `drift/input` — Track H, complete: action maps, rebinding, gamepad, pointer and touch. */
1022
+ export const INPUT_CAPABILITIES: readonly CapabilityDefinition[] = [
1023
+ define(
1024
+ INPUT_MODULE,
1025
+ 'down',
1026
+ [
1027
+ { name: 'actions', type: 'Actions' },
1028
+ { name: 'action', type: 'String' },
1029
+ ],
1030
+ 'bool',
1031
+ ['input.read'],
1032
+ false,
1033
+ 'Whether an action is held.',
1034
+ ),
1035
+ define(
1036
+ INPUT_MODULE,
1037
+ 'pressed',
1038
+ [
1039
+ { name: 'actions', type: 'Actions' },
1040
+ { name: 'action', type: 'String' },
1041
+ ],
1042
+ 'bool',
1043
+ ['input.read'],
1044
+ false,
1045
+ 'Whether an action went down this frame.',
1046
+ ),
1047
+ define(
1048
+ INPUT_MODULE,
1049
+ 'axisX',
1050
+ [
1051
+ { name: 'actions', type: 'Actions' },
1052
+ { name: 'action', type: 'String' },
1053
+ ],
1054
+ 'f32',
1055
+ ['input.read'],
1056
+ false,
1057
+ "A directional action's horizontal component, shortened to the rim with the vertical one so a keyboard diagonal is not faster than a straight line. For two independent controls such as throttle and steering, read rawAxisX instead.",
1058
+ ),
1059
+ define(
1060
+ INPUT_MODULE,
1061
+ 'axisY',
1062
+ [
1063
+ { name: 'actions', type: 'Actions' },
1064
+ { name: 'action', type: 'String' },
1065
+ ],
1066
+ 'f32',
1067
+ ['input.read'],
1068
+ false,
1069
+ "A directional action's vertical component, shortened to the rim with the horizontal one. For two independent controls, read rawAxisY instead.",
1070
+ ),
1071
+ /*
1072
+ * **The pair that is not normalised, and the reason both pairs exist.** `axisX` and `axisY` treat
1073
+ * an action's two axes as one direction, which is right on foot and wrong at a wheel: throttle
1074
+ * and steering are two controls that happen to share an action, and dividing both by √2 means a
1075
+ * driver holding forward and left can never reach full lock while accelerating. A consumer lost
1076
+ * two weeks to that in TypeScript before anybody looked at `ActionMap.vector`, and a script
1077
+ * author reading `axisX` would have met it identically, because this binding calls that method.
1078
+ *
1079
+ * Added rather than corrected in place: `axisX` behaves as it always has, because changing what
1080
+ * it returns would make every scripted character quietly faster on a diagonal.
1081
+ */
1082
+ define(
1083
+ INPUT_MODULE,
1084
+ 'rawAxisX',
1085
+ [
1086
+ { name: 'actions', type: 'Actions' },
1087
+ { name: 'action', type: 'String' },
1088
+ ],
1089
+ 'f32',
1090
+ ['input.read'],
1091
+ false,
1092
+ "An action's horizontal axis on its own, not shortened against the vertical one. This is what a throttle or a steering input wants: holding two keys gives a full 1 on each.",
1093
+ ),
1094
+ define(
1095
+ INPUT_MODULE,
1096
+ 'rawAxisY',
1097
+ [
1098
+ { name: 'actions', type: 'Actions' },
1099
+ { name: 'action', type: 'String' },
1100
+ ],
1101
+ 'f32',
1102
+ ['input.read'],
1103
+ false,
1104
+ "An action's vertical axis on its own, not shortened against the horizontal one.",
1105
+ ),
1106
+ /*
1107
+ * **Rumble answers a boolean and a script has to read it**, which is the whole reason this row
1108
+ * exists in the shape it does. Most pads on most browsers have no motors, so a settings screen
1109
+ * that offers the control unconditionally is offering something a player's hardware ignores. The
1110
+ * host layer answers false rather than accepting the call and doing nothing quietly, and these
1111
+ * three carry that answer across the language boundary unchanged.
1112
+ *
1113
+ * `input.write` does not exist as an effect and this does not invent one: driving a motor is a
1114
+ * write to the *host*, which is what `host` already means, and it is outside the deterministic
1115
+ * boundary for the same reason `audio.write` is — a replay must not be able to tell whether the
1116
+ * player's pad was plugged in.
1117
+ */
1118
+ define(
1119
+ INPUT_MODULE,
1120
+ 'canRumble',
1121
+ [{ name: 'actions', type: 'Actions' }],
1122
+ 'bool',
1123
+ ['host'],
1124
+ false,
1125
+ 'Whether the pad these actions read has motors this browser can drive. Grey the control out when it is false; most pads on most browsers cannot.',
1126
+ ),
1127
+ define(
1128
+ INPUT_MODULE,
1129
+ 'rumble',
1130
+ [
1131
+ { name: 'actions', type: 'Actions' },
1132
+ { name: 'durationMs', type: 'f32' },
1133
+ { name: 'strong', type: 'f32' },
1134
+ { name: 'weak', type: 'f32' },
1135
+ ],
1136
+ 'bool',
1137
+ ['host'],
1138
+ false,
1139
+ 'Rumble for a duration in milliseconds, at two magnitudes in [0, 1]: `strong` is the low-frequency motor and `weak` the high-frequency one. Answers whether the platform took it; false is a real answer, not an error.',
1140
+ ),
1141
+ define(
1142
+ INPUT_MODULE,
1143
+ 'stopRumble',
1144
+ [{ name: 'actions', type: 'Actions' }],
1145
+ 'bool',
1146
+ ['host'],
1147
+ false,
1148
+ 'Stop whatever that pad is playing. Answers whether the platform took it.',
1149
+ ),
1150
+ ];
1151
+
1152
+ /**
1153
+ * `drift/camera` — the cinematic cameras, and **only what they actually have**.
1154
+ *
1155
+ * The first version of this binding described `lookAt` and `setPosition`. `CinematicCamera` has
1156
+ * neither: it has `cut`, `snap`, `shotAgeSec` and `update`. The implementation reached them through
1157
+ * an optional-call cast, so a script calling `camera.lookAt(…)` compiled, linked, ran, and did
1158
+ * **nothing** — the silent no-op this repository's rules forbid, arriving by way of a binding
1159
+ * written from memory of what a camera ought to have rather than from the class.
1160
+ *
1161
+ * `cut` is deliberately still absent: it takes a `ShotParams`, and a script has no way to construct
1162
+ * an engine record yet. Binding it would mean inventing a flattened signature that drifts from the
1163
+ * real one the first time a shot gains a field. It waits for a way to express the record.
1164
+ */
1165
+ export const CAMERA_CAPABILITIES: readonly CapabilityDefinition[] = [
1166
+ define(
1167
+ CAMERA_MODULE,
1168
+ 'snap',
1169
+ [{ name: 'camera', type: 'Camera' }],
1170
+ 'void',
1171
+ ['scene.write'],
1172
+ false,
1173
+ 'Jump the camera to where it is heading, skipping the ease. What a scene cut needs.',
1174
+ ),
1175
+ define(
1176
+ CAMERA_MODULE,
1177
+ 'shotAge',
1178
+ [{ name: 'camera', type: 'Camera' }],
1179
+ 'f32',
1180
+ ['scene.read'],
1181
+ true,
1182
+ 'Seconds since the current shot began. A read, so a deterministic function may ask.',
1183
+ ),
1184
+ ];
1185
+
1186
+ /** How the runtime represents an option. Matches what the compiler generates. */
1187
+ type Option<T> = { readonly tag: 'some'; readonly value: T } | { readonly tag: 'none' };
1188
+ const some = <T>(value: T): Option<T> => ({ tag: 'some', value });
1189
+ const none: Option<never> = { tag: 'none' };
1190
+
1191
+ /** What a host must supply for each module it wants to provide. */
1192
+ export interface CoreServices {
1193
+ readonly clocks?: {
1194
+ fixedDelta(): number;
1195
+ frameDelta(): number;
1196
+ wallDelta(): number;
1197
+ elapsed(): number;
1198
+ };
1199
+ readonly store?: KeyValueStore;
1200
+ readonly queue?: MessageQueue<QueuedMessage>;
1201
+ readonly actions?: ActionMap;
1202
+ }
1203
+
1204
+ /**
1205
+ * Round a double to the nearest single, because every signature in this file is declared `f32`.
1206
+ *
1207
+ * **Still correct at language 1.5.0, and the reason is worth writing down beside it.** That release
1208
+ * introduced a `float` type meaning "`f32` or `f64`, the same one throughout the call", and moved
1209
+ * `std/math`'s rounding out of its implementations to the call site — because a library that
1210
+ * rounded every result would be destroying precision the moment a call resolved to double. The
1211
+ * language's own note to hosts is therefore: implement a `float` capability once, in double, and do
1212
+ * **not** round; and rounding stays right for a signature left written `f32`.
1213
+ *
1214
+ * Every capability here is written `f32` and none is written `float`, so this is the second case
1215
+ * and it is unchanged. **What would make it wrong** is somebody widening one of these signatures to
1216
+ * `float` and leaving the call here — at which point a double-precision call would be silently
1217
+ * narrowed by the host after the compiler had promised it would not be.
1218
+ */
1219
+ const f = Math.fround;
1220
+
1221
+ export function timeImplementation(
1222
+ clocks: NonNullable<CoreServices['clocks']>,
1223
+ ): Record<string, unknown> {
1224
+ return {
1225
+ fixedDelta: () => f(clocks.fixedDelta()),
1226
+ frameDelta: () => f(clocks.frameDelta()),
1227
+ wallDelta: () => f(clocks.wallDelta()),
1228
+ elapsed: () => f(clocks.elapsed()),
1229
+ };
1230
+ }
1231
+
1232
+ /**
1233
+ * The seeded generator, dispatching to core's frozen sequence.
1234
+ *
1235
+ * `index` returns 0 for an empty collection rather than an error, because "pick from nothing" has
1236
+ * one sane answer and a script asking is usually mid-way through a list that will have items next
1237
+ * frame.
1238
+ */
1239
+ export function randomImplementation(): Record<string, unknown> {
1240
+ return {
1241
+ unit: (seed: number) => f(hashToUnit(seed)),
1242
+ range: (seed: number, low: number, high: number) => f(low + hashToUnit(seed) * (high - low)),
1243
+ index: (seed: number, count: number) =>
1244
+ count <= 0 ? 0 : Math.floor(hashToUnit(seed) * count) % count,
1245
+ };
1246
+ }
1247
+
1248
+ export function eventsImplementation(): Record<string, unknown> {
1249
+ return {
1250
+ push(queue: MessageQueue<QueuedMessage>, id: string, priority: number, holdFor: number) {
1251
+ /* `holdFor` arrives in seconds because units erase to the base unit, and the queue counts in
1252
+ milliseconds. Converting here rather than exposing milliseconds to a script keeps every
1253
+ duration in the language one kind of number. */
1254
+ queue.push({ id, priority, durationMs: holdFor * 1000 });
1255
+ },
1256
+ current(queue: MessageQueue<QueuedMessage>): Option<string> {
1257
+ const message = queue.current;
1258
+ return message === null ? none : some(message.id);
1259
+ },
1260
+ clear(queue: MessageQueue<QueuedMessage>) {
1261
+ queue.clear();
1262
+ },
1263
+ };
1264
+ }
1265
+
1266
+ export function persistenceImplementation(): Record<string, unknown> {
1267
+ return {
1268
+ read(store: KeyValueStore, key: string): Option<string> {
1269
+ const value = store.read(key);
1270
+ /* `null` becomes `none`, which is the whole reason this is a capability rather than a direct
1271
+ call: the language has no null, so the boundary is where one stops existing. */
1272
+ return value === null ? none : some(value);
1273
+ },
1274
+ write(store: KeyValueStore, key: string, value: string) {
1275
+ store.write(key, value);
1276
+ },
1277
+ remove(store: KeyValueStore, key: string) {
1278
+ store.remove(key);
1279
+ },
1280
+ /* `instanceof` rather than a duck-typed property check: a store that happens to carry a
1281
+ `status` field meaning something else would otherwise be read as this one. */
1282
+ saveStatus: (store: KeyValueStore) =>
1283
+ store instanceof RemoteSaveStore ? store.status : 'idle',
1284
+ pendingSaves: (store: KeyValueStore) => (store instanceof RemoteSaveStore ? store.pending : 0),
1285
+ };
1286
+ }
1287
+
1288
+ export function sceneImplementation(): Record<string, unknown> {
1289
+ return {
1290
+ positionX: (node: SceneNode) => f(node.position[0]),
1291
+ positionY: (node: SceneNode) => f(node.position[1]),
1292
+ positionZ: (node: SceneNode) => f(node.position[2]),
1293
+ setPosition: (node: SceneNode, x: number, y: number, z: number) => node.setPosition(x, y, z),
1294
+ setScale: (node: SceneNode, x: number, y: number, z: number) => node.setScale(x, y, z),
1295
+ setRotation: (node: SceneNode, x: number, y: number, z: number, radians: number) =>
1296
+ node.setRotationAxisAngle(x, y, z, radians),
1297
+ /* Normalised on the way out rather than stored that way: `WindState` carries a velocity, and
1298
+ a direction is what a behaviour asks for. Zero speed has no direction, and zero is the
1299
+ honest answer rather than a NaN from dividing by it. */
1300
+ windDirectionX: (wind: WindState) => f(wind.speed > 1e-5 ? wind.velocityX / wind.speed : 0),
1301
+ windDirectionZ: (wind: WindState) => f(wind.speed > 1e-5 ? wind.velocityZ / wind.speed : 0),
1302
+ windSpeed: (wind: WindState) => f(wind.speed),
1303
+ windGust: (wind: WindState) => f(wind.gust),
1304
+ distance: (a: SceneNode, b: SceneNode) => {
1305
+ const dx = a.position[0] - b.position[0];
1306
+ const dy = a.position[1] - b.position[1];
1307
+ const dz = a.position[2] - b.position[2];
1308
+ return f(Math.sqrt(dx * dx + dy * dy + dz * dz));
1309
+ },
1310
+ };
1311
+ }
1312
+
1313
+ export function physicsImplementation(): Record<string, unknown> {
1314
+ /* One scratch buffer, reused. `query` writes indices into an out-parameter and returns how many
1315
+ it wrote, so a fresh array per call would allocate on a path a script runs every frame — which
1316
+ is the rule the whole engine is built on. Sized generously; a broad-phase answer only needs to
1317
+ know whether the count was non-zero. */
1318
+ const hits = new Int32Array(256);
1319
+ /*
1320
+ * The last raycast, per world, held **here rather than on the engine**.
1321
+ *
1322
+ * A script cannot receive a record, so a hit is read back through accessors — and putting the
1323
+ * state in the binding keeps `PhysicsWorld` free of a field that exists only because of a
1324
+ * language limitation. A `WeakMap` rather than one shared slot, because two worlds are two
1325
+ * questions and a shared slot would let one answer the other's.
1326
+ */
1327
+ const lastHit = new WeakMap<PhysicsWorld, RayHit>();
1328
+ const hitOf = (world: PhysicsWorld): RayHit => {
1329
+ let hit = lastHit.get(world);
1330
+ if (!hit) {
1331
+ hit = createRayHit();
1332
+ lastHit.set(world, hit);
1333
+ }
1334
+ return hit;
1335
+ };
1336
+ const filter = { mask: 0xffffffff };
1337
+
1338
+ /*
1339
+ * The last `nearestWithin`, per collider set, in the same shape and for the same reasons.
1340
+ *
1341
+ * A consumer reported the gap this closes: `anyWithin` answered *whether* something was near and
1342
+ * never *what*, so a script could find the nearest person through `drift/ecs` and could not find
1343
+ * the nearest wall at all — and the rule for what is in reach moved into TypeScript, which is
1344
+ * exactly the rule somebody wants to edit with the game running.
1345
+ *
1346
+ * **The answer is a search plus accessors rather than a value**, which is the third time this
1347
+ * file has drawn that conclusion — `raycast` with its `hit` functions, `findNearest` with
1348
+ * `nearest`, and now this. A script cannot receive a record, and returning an option would
1349
+ * allocate one object per call on a path an agent runs every frame.
1350
+ */
1351
+ const lastNear = new WeakMap<
1352
+ ColliderSet,
1353
+ {
1354
+ collider: number;
1355
+ distance: number;
1356
+ x: number;
1357
+ y: number;
1358
+ z: number;
1359
+ }
1360
+ >();
1361
+ const nearOf = (colliders: ColliderSet) => {
1362
+ let near = lastNear.get(colliders);
1363
+ if (!near) {
1364
+ near = { collider: -1, distance: 0, x: 0, y: 0, z: 0 };
1365
+ lastNear.set(colliders, near);
1366
+ }
1367
+ return near;
1368
+ };
1369
+ /* One reused box for the scan, because `bounds` writes into a caller's object and a fresh one
1370
+ per candidate is the per-frame allocation the engine forbids. */
1371
+ const box: Aabb = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
1372
+
1373
+ /*
1374
+ * The boxes of a group being built, per set. A `WeakMap` for the same reason `nearOf` uses one:
1375
+ * two sets are two builders, and a shared slot would let one set's region land in the other.
1376
+ */
1377
+ const pending = new WeakMap<ColliderSet, Collider[]>();
1378
+
1379
+ return {
1380
+ colliderCount: (colliders: ColliderSet) => colliders.count,
1381
+ colliderCapacity: (colliders: ColliderSet) => colliders.capacity,
1382
+ colliderBytes: (colliders: ColliderSet) => colliders.bytes().total,
1383
+ beginColliderGroup(colliders: ColliderSet): void {
1384
+ if (pending.has(colliders)) {
1385
+ throw new Error(
1386
+ 'beginColliderGroup: a group is already open on this set. Close it with ' +
1387
+ '`endColliderGroup` before opening another.',
1388
+ );
1389
+ }
1390
+ pending.set(colliders, []);
1391
+ },
1392
+ addColliderBox(
1393
+ colliders: ColliderSet,
1394
+ cx: number,
1395
+ cy: number,
1396
+ cz: number,
1397
+ hx: number,
1398
+ hy: number,
1399
+ hz: number,
1400
+ ): void {
1401
+ const open = pending.get(colliders);
1402
+ if (open === undefined) {
1403
+ throw new Error(
1404
+ 'addColliderBox: no group is open on this set. Call `beginColliderGroup` first.',
1405
+ );
1406
+ }
1407
+ open.push(boxCollider(cx, cy, cz, hx, hy, hz));
1408
+ },
1409
+ endColliderGroup(colliders: ColliderSet): number {
1410
+ const open = pending.get(colliders);
1411
+ if (open === undefined) {
1412
+ throw new Error('endColliderGroup: no group is open on this set.');
1413
+ }
1414
+ pending.delete(colliders);
1415
+ return colliders.add(open);
1416
+ },
1417
+ removeColliderGroup(colliders: ColliderSet, group: number): void {
1418
+ colliders.remove(group);
1419
+ },
1420
+ anyWithin(colliders: ColliderSet, x: number, y: number, z: number, radius: number): boolean {
1421
+ const found = colliders.query(
1422
+ x - radius,
1423
+ y - radius,
1424
+ z - radius,
1425
+ x + radius,
1426
+ y + radius,
1427
+ z + radius,
1428
+ hits,
1429
+ );
1430
+ return found > 0;
1431
+ },
1432
+ nearestWithin(
1433
+ colliders: ColliderSet,
1434
+ x: number,
1435
+ y: number,
1436
+ z: number,
1437
+ radius: number,
1438
+ ): boolean {
1439
+ const near = nearOf(colliders);
1440
+ near.collider = -1;
1441
+ near.distance = 0;
1442
+ near.x = 0;
1443
+ near.y = 0;
1444
+ near.z = 0;
1445
+
1446
+ const found = colliders.query(
1447
+ x - radius,
1448
+ y - radius,
1449
+ z - radius,
1450
+ x + radius,
1451
+ y + radius,
1452
+ z + radius,
1453
+ hits,
1454
+ );
1455
+
1456
+ let best = radius * radius;
1457
+ for (let i = 0; i < found; i += 1) {
1458
+ const index = hits[i] as number;
1459
+ colliders.bounds(index, box);
1460
+ /* The closest point on the box, which is the clamp and nothing more. Inside it, the clamp
1461
+ is the query point itself and the distance is zero — which is the right answer for a
1462
+ character standing in a wall and the one a square root would also give. */
1463
+ const px = x < box.minX ? box.minX : x > box.maxX ? box.maxX : x;
1464
+ const py = y < box.minY ? box.minY : y > box.maxY ? box.maxY : y;
1465
+ const pz = z < box.minZ ? box.minZ : z > box.maxZ ? box.maxZ : z;
1466
+ const dx = px - x;
1467
+ const dy = py - y;
1468
+ const dz = pz - z;
1469
+ const squared = dx * dx + dy * dy + dz * dz;
1470
+ /* Squared throughout the scan and rooted once at the end: the comparison is the same and
1471
+ this runs over every candidate the hash returned, every frame an agent looks around. */
1472
+ if (squared > best) continue;
1473
+ best = squared;
1474
+ near.collider = index;
1475
+ near.distance = squared;
1476
+ near.x = px;
1477
+ near.y = py;
1478
+ near.z = pz;
1479
+ }
1480
+
1481
+ if (near.collider < 0) return false;
1482
+ near.distance = Math.sqrt(near.distance);
1483
+ return true;
1484
+ },
1485
+ nearCollider: (colliders: ColliderSet) => nearOf(colliders).collider,
1486
+ nearX: (colliders: ColliderSet) => f(nearOf(colliders).x),
1487
+ nearY: (colliders: ColliderSet) => f(nearOf(colliders).y),
1488
+ nearZ: (colliders: ColliderSet) => f(nearOf(colliders).z),
1489
+ nearDistance: (colliders: ColliderSet) => f(nearOf(colliders).distance),
1490
+
1491
+ bodyCount: (world: PhysicsWorld) => world.bodies.count,
1492
+ bodyX: (world: PhysicsWorld, body: number) => f(world.bodies.posX[body] ?? 0),
1493
+ bodyY: (world: PhysicsWorld, body: number) => f(world.bodies.posY[body] ?? 0),
1494
+ bodyZ: (world: PhysicsWorld, body: number) => f(world.bodies.posZ[body] ?? 0),
1495
+ bodyVelX: (world: PhysicsWorld, body: number) => f(world.bodies.velX[body] ?? 0),
1496
+ bodyVelY: (world: PhysicsWorld, body: number) => f(world.bodies.velY[body] ?? 0),
1497
+ bodyVelZ: (world: PhysicsWorld, body: number) => f(world.bodies.velZ[body] ?? 0),
1498
+ bodyMass: (world: PhysicsWorld, body: number) => f(world.bodyMass(body)),
1499
+ sleeping: (world: PhysicsWorld, body: number) => world.sleeping(body),
1500
+
1501
+ raycast(
1502
+ world: PhysicsWorld,
1503
+ x: number,
1504
+ y: number,
1505
+ z: number,
1506
+ dx: number,
1507
+ dy: number,
1508
+ dz: number,
1509
+ maxDistance: number,
1510
+ mask: number,
1511
+ ): boolean {
1512
+ filter.mask = mask;
1513
+ return world.raycast(x, y, z, dx, dy, dz, maxDistance, hitOf(world), filter);
1514
+ },
1515
+ hitBody: (world: PhysicsWorld) => hitOf(world).body,
1516
+ hitX: (world: PhysicsWorld) => f(hitOf(world).x),
1517
+ hitY: (world: PhysicsWorld) => f(hitOf(world).y),
1518
+ hitZ: (world: PhysicsWorld) => f(hitOf(world).z),
1519
+ hitNormalX: (world: PhysicsWorld) => f(hitOf(world).nx),
1520
+ hitNormalY: (world: PhysicsWorld) => f(hitOf(world).ny),
1521
+ hitNormalZ: (world: PhysicsWorld) => f(hitOf(world).nz),
1522
+ hitFraction: (world: PhysicsWorld) => f(hitOf(world).fraction),
1523
+
1524
+ contactCount: (world: PhysicsWorld) => world.events.count,
1525
+ contactKind: (world: PhysicsWorld, index: number) => world.events.data[index * 3] ?? 0,
1526
+ contactA: (world: PhysicsWorld, index: number) => world.events.data[index * 3 + 1] ?? 0,
1527
+ contactB: (world: PhysicsWorld, index: number) => world.events.data[index * 3 + 2] ?? 0,
1528
+
1529
+ applyImpulse: (
1530
+ world: PhysicsWorld,
1531
+ body: number,
1532
+ px: number,
1533
+ py: number,
1534
+ pz: number,
1535
+ atX: number,
1536
+ atY: number,
1537
+ atZ: number,
1538
+ ) => world.applyImpulse(body, px, py, pz, atX, atY, atZ),
1539
+ applyForce: (
1540
+ world: PhysicsWorld,
1541
+ body: number,
1542
+ fx: number,
1543
+ fy: number,
1544
+ fz: number,
1545
+ dt: number,
1546
+ atX: number,
1547
+ atY: number,
1548
+ atZ: number,
1549
+ ) => world.applyForce(body, fx, fy, fz, dt, atX, atY, atZ),
1550
+ setVelocity: (world: PhysicsWorld, body: number, vx: number, vy: number, vz: number) =>
1551
+ world.setVelocity(body, vx, vy, vz),
1552
+ setPosition: (world: PhysicsWorld, body: number, x: number, y: number, z: number) =>
1553
+ world.setPosition(body, x, y, z),
1554
+ wake: (world: PhysicsWorld, body: number) => world.wakeIsland(body),
1555
+ setGravity(world: PhysicsWorld, x: number, y: number, z: number): void {
1556
+ world.gravityX = x;
1557
+ world.gravityY = y;
1558
+ world.gravityZ = z;
1559
+ },
1560
+ };
1561
+ }
1562
+
1563
+ export function inputImplementation(): Record<string, unknown> {
1564
+ const scratch = { x: 0, y: 0 };
1565
+ return {
1566
+ down: (actions: ActionMap, action: string) => actions.down(action),
1567
+ pressed: (actions: ActionMap, action: string) => actions.pressed(action),
1568
+ axisX(actions: ActionMap, action: string) {
1569
+ /* One scratch object reused, because `vector` writes into an out-parameter and a fresh
1570
+ literal per call would allocate on a path a script runs every frame. */
1571
+ actions.vector(action, scratch);
1572
+ return f(scratch.x);
1573
+ },
1574
+ axisY(actions: ActionMap, action: string) {
1575
+ actions.vector(action, scratch);
1576
+ return f(scratch.y);
1577
+ },
1578
+ /* No scratch: `axis` returns the number rather than filling an out-parameter, because one axis
1579
+ is one value and there was nothing to write into. */
1580
+ rawAxisX: (actions: ActionMap, action: string) => f(actions.axis(action, 'x')),
1581
+ rawAxisY: (actions: ActionMap, action: string) => f(actions.axis(action, 'y')),
1582
+ canRumble: (actions: ActionMap) => actions.canRumble,
1583
+ rumble: (actions: ActionMap, durationMs: number, strong: number, weak: number) =>
1584
+ actions.rumble(durationMs, strong, weak),
1585
+ stopRumble: (actions: ActionMap) => actions.stopRumble(),
1586
+ };
1587
+ }
1588
+
1589
+ export function cameraImplementation(): Record<string, unknown> {
1590
+ /* Called directly, with no optional-call cast anywhere. A cast that tolerates a missing method is
1591
+ a binding that cannot fail when the engine renames one — which is how the first version of this
1592
+ described two methods the class does not have. */
1593
+ return {
1594
+ snap: (camera: CinematicCamera) => camera.snap(),
1595
+ shotAge: (camera: CinematicCamera) => f(camera.shotAgeSec),
1596
+ };
1597
+ }
1598
+
1599
+ /* Referenced so the imports that document what this binds are not dropped as unused. `mulberry32`,
1600
+ `angleDelta` and `wrapAngle` are the engine's, named here because a reader looking for what a
1601
+ binding dispatches to should find it in one place. */
1602
+ void mulberry32;
1603
+ void angleDelta;
1604
+ void wrapAngle;