@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,814 @@
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 { ActionMap, CinematicCamera, ColliderSet, RemoteSaveStore, MessageQueue, PhysicsWorld, SceneNode, angleDelta, boxCollider, createRayHit, hashToUnit, mulberry32, wrapAngle, } from '@driftengine/core';
22
+ import { defineCapability } from 'driftscript';
23
+ export const TIME_MODULE = 'drift/time';
24
+ export const RANDOM_MODULE = 'drift/random';
25
+ export const EVENTS_MODULE = 'drift/events';
26
+ export const PERSISTENCE_MODULE = 'drift/persistence';
27
+ export const SCENE_MODULE = 'drift/scene';
28
+ export const PHYSICS_MODULE = 'drift/physics';
29
+ export const INPUT_MODULE = 'drift/input';
30
+ export const CAMERA_MODULE = 'drift/camera';
31
+ const define = (module, name, params, returns, effects, deterministic, doc) => defineCapability({
32
+ module,
33
+ name,
34
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
35
+ params: [...params],
36
+ returns,
37
+ effects,
38
+ deterministic,
39
+ doc,
40
+ implementation: `${module}.${name}`,
41
+ });
42
+ /**
43
+ * Handles a script holds and passes back, never reads into.
44
+ *
45
+ * A field on any of these would be a promise about a representation the engine could then never
46
+ * change — the design's safe-handle rule. `Node` is the one a script touches most, and it is still
47
+ * opaque: a position is read through a capability rather than off the object.
48
+ */
49
+ export const CORE_TYPES = [
50
+ { module: SCENE_MODULE, name: 'Node', doc: 'A node in the transform hierarchy.' },
51
+ /*
52
+ * The frame's weather, already sampled. **A state and not a profile**, which is the whole of why
53
+ * a script may read it inside the fixed step: a profile plus a clock is a wall-clock read, and a
54
+ * `WindState` is what the field said at a time somebody else chose.
55
+ */
56
+ { module: SCENE_MODULE, name: 'Wind', doc: 'The wind as it was sampled this frame.' },
57
+ {
58
+ module: PHYSICS_MODULE,
59
+ name: 'Colliders',
60
+ doc: 'A set of convex colliders with a spatial hash.',
61
+ },
62
+ /*
63
+ * `PhysicsWorld`, not `World`: `drift/ecs` already registers that name, and the registry refuses
64
+ * two hosts claiming one — at registration, where somebody can act on it, rather than at a
65
+ * resolution nobody can see. An entity world and a rigid body world are genuinely two things.
66
+ */
67
+ {
68
+ module: PHYSICS_MODULE,
69
+ name: 'PhysicsWorld',
70
+ doc: 'A rigid body world, stepped on the fixed clock.',
71
+ },
72
+ {
73
+ module: INPUT_MODULE,
74
+ name: 'Actions',
75
+ doc: 'A bound action map: what the player is pressing.',
76
+ },
77
+ { module: CAMERA_MODULE, name: 'Camera', doc: 'A cinematic camera.' },
78
+ {
79
+ module: EVENTS_MODULE,
80
+ name: 'Queue',
81
+ doc: 'A priority message queue with dedupe and a ceiling.',
82
+ },
83
+ { module: PERSISTENCE_MODULE, name: 'Store', doc: 'A key-value store the consumer supplied.' },
84
+ ];
85
+ /**
86
+ * `drift/time` — the loop's three clocks, and the one that is deterministic is not here.
87
+ *
88
+ * All three carry `clock.read`, so **none may be called from a `@deterministic` function** — and
89
+ * that is right even for the fixed step. A simulation is *given* its delta as a parameter; a
90
+ * simulation that reached out and read one has stopped being a function of its inputs, which is
91
+ * what replay depends on.
92
+ */
93
+ export const TIME_CAPABILITIES = [
94
+ define(TIME_MODULE, 'fixedDelta', [], 'f32', ['clock.read'], false, 'The fixed simulation step, in seconds. Prefer the `dt` your function was given.'),
95
+ define(TIME_MODULE, 'frameDelta', [], 'f32', ['clock.read'], false, 'Seconds since the previous rendered frame, after clamping.'),
96
+ define(TIME_MODULE, 'wallDelta', [], 'f32', ['clock.read'], false, 'Seconds of real time since the previous frame, unclamped. Outside the boundary in every sense.'),
97
+ define(TIME_MODULE, 'elapsed', [], 'f32', ['clock.read'], false, 'Seconds since the loop started.'),
98
+ ];
99
+ /**
100
+ * `drift/random` — a seeded generator whose sequence is **frozen**.
101
+ *
102
+ * Deterministic by construction, so a `@deterministic` function may call it. That is the whole
103
+ * reason it is a capability rather than a `std/*` function: the sequence is a promise about *this
104
+ * engine's* stored replays, and a standard library that shipped its own would either break that
105
+ * promise or silently become the thing that defines it.
106
+ */
107
+ export const RANDOM_CAPABILITIES = [
108
+ define(RANDOM_MODULE, 'unit', [{ name: 'seed', type: 'u32' }], 'f32', ['pure'], true, 'A value from 0 to 1 for a seed. The same seed always gives the same value.'),
109
+ define(RANDOM_MODULE, 'range', [
110
+ { name: 'seed', type: 'u32' },
111
+ { name: 'low', type: 'f32' },
112
+ { name: 'high', type: 'f32' },
113
+ ], 'f32', ['pure'], true, 'A value between two bounds for a seed.'),
114
+ define(RANDOM_MODULE, 'index', [
115
+ { name: 'seed', type: 'u32' },
116
+ { name: 'count', type: 'u32' },
117
+ ], 'u32', ['pure'], true, 'An index into a collection of `count` items. Zero for an empty one.'),
118
+ ];
119
+ /** `drift/events` — the engine's priority queue, with dedupe over a window and a backlog ceiling. */
120
+ export const EVENTS_CAPABILITIES = [
121
+ define(EVENTS_MODULE, 'push', [
122
+ { name: 'queue', type: 'Queue' },
123
+ { name: 'id', type: 'String' },
124
+ { name: 'priority', type: 'f32' },
125
+ { name: 'holdFor', type: 'f32' },
126
+ ], 'void', ['pure'], true, 'Queue a message by identity. A repeat of the same id inside the dedupe window is dropped.'),
127
+ define(EVENTS_MODULE, 'current', [{ name: 'queue', type: 'Queue' }], 'String?', ['pure'], true, "The id of the message showing now, if any. The queue carries identity and priority; the words are the consumer's."),
128
+ define(EVENTS_MODULE, 'clear', [{ name: 'queue', type: 'Queue' }], 'void', ['pure'], true, 'Drop everything queued.'),
129
+ ];
130
+ /** `drift/persistence` — the store the consumer supplied, which may be a browser, a server or nothing. */
131
+ export const PERSISTENCE_CAPABILITIES = [
132
+ define(PERSISTENCE_MODULE, 'read', [
133
+ { name: 'store', type: 'Store' },
134
+ { name: 'key', type: 'String' },
135
+ ], 'String?', ['persistence.read'], false, 'What is stored under a key. Absent when nothing is, which is ordinary rather than exceptional.'),
136
+ define(PERSISTENCE_MODULE, 'write', [
137
+ { name: 'store', type: 'Store' },
138
+ { name: 'key', type: 'String' },
139
+ { name: 'value', type: 'String' },
140
+ ], 'void', ['persistence.write'], false, 'Store a value under a key.'),
141
+ define(PERSISTENCE_MODULE, 'remove', [
142
+ { name: 'store', type: 'Store' },
143
+ { name: 'key', type: 'String' },
144
+ ], 'void', ['persistence.write'], false, 'Forget a key.'),
145
+ /*
146
+ * **The two a script needs when the store answers later, and the one it cannot have.**
147
+ *
148
+ * A `RemoteSaveStore` writes into a queue that drains behind the synchronous surface, so a write
149
+ * that reads back correctly may not have landed anywhere. A script drawing a "saving" indicator
150
+ * has to be able to ask. Both answer honestly for a *synchronous* store too — a browser store
151
+ * has nothing pending and is always idle — so this is not a capability that only works for one
152
+ * implementation.
153
+ *
154
+ * **`flush` is not bound**, and the reason is the language rather than the engine: it answers a
155
+ * promise and DriftScript has no await, so a binding would either block or hand back something
156
+ * a script cannot hold. A consumer flushes from TypeScript, at the one place it knows it is
157
+ * leaving.
158
+ */
159
+ define(PERSISTENCE_MODULE, 'saveStatus', [{ name: 'store', type: 'Store' }], 'String', ['persistence.read'], false, '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.'),
160
+ define(PERSISTENCE_MODULE, 'pendingSaves', [{ name: 'store', type: 'Store' }], 'u32', ['persistence.read'], false, 'How many keys are waiting to reach the backend. Zero for a store that writes synchronously.'),
161
+ ];
162
+ /**
163
+ * `drift/scene` — the transform hierarchy from Phase 1.1.
164
+ *
165
+ * Reads are deterministic and writes are not, which is the boundary drawn per capability rather
166
+ * than per module. A simulation may ask where something is; moving it is a change to the world.
167
+ */
168
+ export const SCENE_CAPABILITIES = [
169
+ define(SCENE_MODULE, 'positionX', [{ name: 'node', type: 'Node' }], 'f32', ['scene.read'], true, "A node's local x."),
170
+ define(SCENE_MODULE, 'positionY', [{ name: 'node', type: 'Node' }], 'f32', ['scene.read'], true, "A node's local y."),
171
+ define(SCENE_MODULE, 'positionZ', [{ name: 'node', type: 'Node' }], 'f32', ['scene.read'], true, "A node's local z."),
172
+ define(SCENE_MODULE, 'setPosition', [
173
+ { name: 'node', type: 'Node' },
174
+ { name: 'x', type: 'f32' },
175
+ { name: 'y', type: 'f32' },
176
+ { name: 'z', type: 'f32' },
177
+ ], 'void', ['scene.write'], false, 'Move a node, marking it and its subtree for a world update.'),
178
+ define(SCENE_MODULE, 'setScale', [
179
+ { name: 'node', type: 'Node' },
180
+ { name: 'x', type: 'f32' },
181
+ { name: 'y', type: 'f32' },
182
+ { name: 'z', type: 'f32' },
183
+ ], 'void', ['scene.write'], false, 'Scale a node.'),
184
+ define(SCENE_MODULE, 'setRotation', [
185
+ { name: 'node', type: 'Node' },
186
+ { name: 'x', type: 'f32' },
187
+ { name: 'y', type: 'f32' },
188
+ { name: 'z', type: 'f32' },
189
+ { name: 'radians', type: 'f32' },
190
+ ], 'void', ['scene.write'], false, 'Rotate a node about an axis.'),
191
+ /*
192
+ * **The wind, read by a behaviour rather than by the renderer.**
193
+ *
194
+ * The per-vertex channel bends geometry to this same wind in the vertex stage; these four let a
195
+ * script ask the question a behaviour asks — take shelter in a gust, luff a sail, turn downwind.
196
+ *
197
+ * **Deterministic, and that is a claim rather than a convenience.** `wind.ts` builds the signal
198
+ * as a fundamental plus integer harmonics, repeating exactly at the fundamental's period,
199
+ * precisely so a replay watched an hour later sees the gust the original run saw. A non-integer
200
+ * harmonic would look organic and never close, and these would then be reading a wall clock
201
+ * through a handle. The comment in that file is the thing holding this up.
202
+ *
203
+ * **`f32` and not `float`**, which every capability here takes: `float` is width-polymorphic and
204
+ * one returning it must also take one, or `defineCapability` throws. These take a `Wind`.
205
+ */
206
+ define(SCENE_MODULE, 'windDirectionX', [{ name: 'wind', type: 'Wind' }], 'f32', ['scene.read'], true, "The prevailing wind's x, normalised; zero when there is no wind to have a direction."),
207
+ define(SCENE_MODULE, 'windDirectionZ', [{ name: 'wind', type: 'Wind' }], 'f32', ['scene.read'], true, "The prevailing wind's z, normalised; zero when there is no wind to have a direction."),
208
+ define(SCENE_MODULE, 'windSpeed', [{ name: 'wind', type: 'Wind' }], 'f32', ['scene.read'], true, 'How hard it is blowing, metres a second, never negative.'),
209
+ define(SCENE_MODULE, 'windGust', [{ name: 'wind', type: 'Wind' }], 'f32', ['scene.read'], true, 'The gust alone, -1 to 1, for a behaviour that wants the deviation and not the total.'),
210
+ define(SCENE_MODULE, 'distance', [
211
+ { name: 'a', type: 'Node' },
212
+ { name: 'b', type: 'Node' },
213
+ ], 'f32', ['scene.read'], true, 'The distance between two nodes, in local space.'),
214
+ ];
215
+ /**
216
+ * `drift/physics` — bodies, contacts, queries and forces. **Not joints, not the controller and not
217
+ * ragdolls**, which is what this line said until 2026-08-27 and what none of the thirty-six
218
+ * capabilities below has ever done.
219
+ *
220
+ * **This module was two read-only functions until 2026-08-26**, and its own comment said why: there
221
+ * was no solver to write into, and a capability that pretended otherwise would have been the mock
222
+ * the design refused. Track B built the solver, so the surface is real — but the sentence that
223
+ * announced it was written from the *track's* deliverables rather than from this list, which is the
224
+ * same failure as a binding written from memory of what a camera ought to have. A reader looking
225
+ * for `joint` here found a comment that promised one.
226
+ *
227
+ * **Six of the thirty-six are `nearestWithin` and its accessors, added 2026-08-28**, and they exist
228
+ * because a consumer reported that `anyWithin` answers *whether* and never *what*. A script could
229
+ * already find the nearest entity — `drift/ecs` grew `findNearest` and `nearest` for that — and
230
+ * could not find the nearest wall, so what is in reach of a character was decided in TypeScript.
231
+ *
232
+ * **The two `within` questions measure different volumes, and the names cannot say so on their
233
+ * own.** `anyWithin` tests the axis-aligned *box* around the sphere, because that is what the
234
+ * collider hash indexes and testing anything narrower would cost a pass it does not need for a
235
+ * yes-or-no. `nearestWithin` is already walking each candidate to rank it, so it measures the
236
+ * sphere exactly and answers false where `anyWithin` answers true — a collider that reaches only
237
+ * the box corners. Both say which they do, on themselves, because a reader comparing the two names
238
+ * would have no reason to suspect it.
239
+ *
240
+ * **It measures to a collider's bounds rather than to the shape inside them**, which is the one
241
+ * place this surface is coarser than the sweep beside it. `ColliderSet`'s own header draws that
242
+ * line: the bounds are the broad phase and the shape resolves a contact, so a proximity answer that
243
+ * ran the convex sweep per candidate per frame would be paying contact prices for a distance. The
244
+ * documentation on each accessor says bounds, so nobody reads a banked slab's box as its surface.
245
+ *
246
+ * **What is missing, and the design question that is why it is still missing**: `JointDesc` carries
247
+ * twenty optional fields, so a script-side constructor is either an enormous signature or a builder
248
+ * this language has no shape for. That is a decision somebody has to take deliberately, and taking
249
+ * it badly would put a wrong-by-construction surface in front of every script author. Recorded here
250
+ * rather than guessed at.
251
+ *
252
+ * **Two parameters are required here that TypeScript leaves optional**, which is step four of the
253
+ * note at the top of `host.ts`, and both are the same shape of trap — an omission that produces a
254
+ * *wrong result* rather than an error:
255
+ *
256
+ * - **A raycast's layer mask.** Omitted, the first thing a ray finds is very often the body it
257
+ * started inside, because `raycastWorld` reports that body at fraction zero deliberately. A
258
+ * TypeScript caller has the context to know when that matters; a script author meets it first and
259
+ * reads it as the engine seeing through walls.
260
+ * - **An impulse's application point.** Omitted, the impulse goes through the centre of mass and
261
+ * produces no rotation whatever. Nothing throws; a crate simply slides instead of tumbling, which
262
+ * reads as a broken impulse rather than as a missing argument.
263
+ *
264
+ * **Every body and event index is `i32`, not `u32`.** `hitBody` has to be able to answer −1, and a
265
+ * script has no cast, so a mixed surface would make the obvious code — raycast, check the body,
266
+ * apply an impulse to it — impossible to write. One signed type throughout costs nothing and is
267
+ * what a query result already is.
268
+ *
269
+ * **A raycast's result is read back through separate accessors**, in the same shape `input.axisX`
270
+ * and `input.axisY` already use for a vector. A script has no way to receive an engine record, and
271
+ * inventing a flattened signature would drift from the real one the first time a hit gains a field —
272
+ * which is the reasoning `drift/camera` gives for leaving `cut` out. *What it costs* is that a hit
273
+ * is only valid until the next `raycast` on the same world, and the documentation says so on every
274
+ * accessor.
275
+ */
276
+ export const PHYSICS_CAPABILITIES = [
277
+ define(PHYSICS_MODULE, 'colliderCount', [{ name: 'colliders', type: 'Colliders' }], 'u32', ['physics.read'], true, 'How many colliders a set holds.'),
278
+ define(PHYSICS_MODULE, 'colliderCapacity', [{ name: 'colliders', type: 'Colliders' }], 'u32', ['physics.read'], true, '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.'),
279
+ define(PHYSICS_MODULE, 'colliderBytes', [{ name: 'colliders', type: 'Colliders' }], 'u32', ['physics.read'], true, '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.'),
280
+ /*
281
+ * **A builder rather than one call, because a script has no list to hand across.** A region is
282
+ * many boxes and one group, so a per-box call returning a group would mint one group per box and
283
+ * give a streamer nothing to drop.
284
+ *
285
+ * **`end` returns the handle, not `begin`.** The group does not exist until the boxes are in, and
286
+ * a handle issued before that would be a promise the binding could only keep by inventing an id
287
+ * the engine had not minted.
288
+ */
289
+ define(PHYSICS_MODULE, 'beginColliderGroup', [{ name: 'colliders', type: 'Colliders' }], 'void', ['physics.write'], false, 'Open a group. Add boxes with `addColliderBox`, then `endColliderGroup` to put them in and get the handle back.'),
290
+ define(PHYSICS_MODULE, 'addColliderBox', [
291
+ { name: 'colliders', type: 'Colliders' },
292
+ { name: 'cx', type: 'f32' },
293
+ { name: 'cy', type: 'f32' },
294
+ { name: 'cz', type: 'f32' },
295
+ { name: 'hx', type: 'f32' },
296
+ { name: 'hy', type: 'f32' },
297
+ { name: 'hz', type: 'f32' },
298
+ ], 'void', ['physics.write'], false, '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`.'),
299
+ define(PHYSICS_MODULE, 'endColliderGroup', [{ name: 'colliders', type: 'Colliders' }], 'u32', ['physics.write'], false, '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.'),
300
+ define(PHYSICS_MODULE, 'removeColliderGroup', [
301
+ { name: 'colliders', type: 'Colliders' },
302
+ { name: 'group', type: 'u32' },
303
+ ], 'void', ['physics.write'], false, '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.'),
304
+ define(PHYSICS_MODULE, 'anyWithin', [
305
+ { name: 'colliders', type: 'Colliders' },
306
+ { name: 'x', type: 'f32' },
307
+ { name: 'y', type: 'f32' },
308
+ { name: 'z', type: 'f32' },
309
+ { name: 'radius', type: 'f32' },
310
+ ], 'bool', ['physics.read'], true, '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.'),
311
+ define(PHYSICS_MODULE, 'nearestWithin', [
312
+ { name: 'colliders', type: 'Colliders' },
313
+ { name: 'x', type: 'f32' },
314
+ { name: 'y', type: 'f32' },
315
+ { name: 'z', type: 'f32' },
316
+ { name: 'radius', type: 'f32' },
317
+ ], 'bool', ['physics.read'], true, '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.'),
318
+ define(PHYSICS_MODULE, 'nearCollider', [{ name: 'colliders', type: 'Colliders' }], 'i32', ['physics.read'], true, 'The collider the last `nearestWithin` on this set found, or \u22121.'),
319
+ define(PHYSICS_MODULE, 'nearX', [{ name: 'colliders', type: 'Colliders' }], 'f32', ['physics.read'], true, 'The closest point on that collider\u2019s bounds, along x \u2014 the point on the wall rather than the wall\u2019s centre.'),
320
+ define(PHYSICS_MODULE, 'nearY', [{ name: 'colliders', type: 'Colliders' }], 'f32', ['physics.read'], true, 'The closest point on that collider\u2019s bounds, along y.'),
321
+ define(PHYSICS_MODULE, 'nearZ', [{ name: 'colliders', type: 'Colliders' }], 'f32', ['physics.read'], true, 'The closest point on that collider\u2019s bounds, along z.'),
322
+ define(PHYSICS_MODULE, 'nearDistance', [{ name: 'colliders', type: 'Colliders' }], 'f32', ['physics.read'], true, 'How far that collider\u2019s bounds are, in metres. Zero when the query point is inside them.'),
323
+ define(PHYSICS_MODULE, 'bodyCount', [{ name: 'world', type: 'PhysicsWorld' }], 'i32', ['physics.read'], true, 'How many bodies the world holds.'),
324
+ define(PHYSICS_MODULE, 'bodyX', [
325
+ { name: 'world', type: 'PhysicsWorld' },
326
+ { name: 'body', type: 'i32' },
327
+ ], 'f32', ['physics.read'], true, "A body's world position along x."),
328
+ define(PHYSICS_MODULE, 'bodyY', [
329
+ { name: 'world', type: 'PhysicsWorld' },
330
+ { name: 'body', type: 'i32' },
331
+ ], 'f32', ['physics.read'], true, "A body's world position along y."),
332
+ define(PHYSICS_MODULE, 'bodyZ', [
333
+ { name: 'world', type: 'PhysicsWorld' },
334
+ { name: 'body', type: 'i32' },
335
+ ], 'f32', ['physics.read'], true, "A body's world position along z."),
336
+ define(PHYSICS_MODULE, 'bodyVelX', [
337
+ { name: 'world', type: 'PhysicsWorld' },
338
+ { name: 'body', type: 'i32' },
339
+ ], 'f32', ['physics.read'], true, "A body's velocity along x, in metres per second."),
340
+ define(PHYSICS_MODULE, 'bodyVelY', [
341
+ { name: 'world', type: 'PhysicsWorld' },
342
+ { name: 'body', type: 'i32' },
343
+ ], 'f32', ['physics.read'], true, "A body's velocity along y, in metres per second."),
344
+ define(PHYSICS_MODULE, 'bodyVelZ', [
345
+ { name: 'world', type: 'PhysicsWorld' },
346
+ { name: 'body', type: 'i32' },
347
+ ], 'f32', ['physics.read'], true, "A body's velocity along z, in metres per second."),
348
+ define(PHYSICS_MODULE, 'bodyMass', [
349
+ { name: 'world', type: 'PhysicsWorld' },
350
+ { name: 'body', type: 'i32' },
351
+ ], 'f32', ['physics.read'], true, "A body's mass in kilograms, or zero where it has none because it is static or kinematic."),
352
+ define(PHYSICS_MODULE, 'sleeping', [
353
+ { name: 'world', type: 'PhysicsWorld' },
354
+ { name: 'body', type: 'i32' },
355
+ ], 'bool', ['physics.read'], true, 'Whether a body has been still long enough to stop being solved.'),
356
+ define(PHYSICS_MODULE, 'raycast', [
357
+ { name: 'world', type: 'PhysicsWorld' },
358
+ { name: 'x', type: 'f32' },
359
+ { name: 'y', type: 'f32' },
360
+ { name: 'z', type: 'f32' },
361
+ { name: 'dx', type: 'f32' },
362
+ { name: 'dy', type: 'f32' },
363
+ { name: 'dz', type: 'f32' },
364
+ { name: 'maxDistance', type: 'f32' },
365
+ { name: 'mask', type: 'i32' },
366
+ ], 'bool', ['physics.read'], true, '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.'),
367
+ define(PHYSICS_MODULE, 'hitBody', [{ name: 'world', type: 'PhysicsWorld' }], 'i32', ['physics.read'], true, 'The body the last `raycast` on this world found, or −1. Valid until the next raycast.'),
368
+ define(PHYSICS_MODULE, 'hitX', [{ name: 'world', type: 'PhysicsWorld' }], 'f32', ['physics.read'], true, 'Where the last raycast struck, along x. Valid until the next raycast.'),
369
+ define(PHYSICS_MODULE, 'hitY', [{ name: 'world', type: 'PhysicsWorld' }], 'f32', ['physics.read'], true, 'Where the last raycast struck, along y. Valid until the next raycast.'),
370
+ define(PHYSICS_MODULE, 'hitZ', [{ name: 'world', type: 'PhysicsWorld' }], 'f32', ['physics.read'], true, 'Where the last raycast struck, along z. Valid until the next raycast.'),
371
+ define(PHYSICS_MODULE, 'hitNormalX', [{ name: 'world', type: 'PhysicsWorld' }], 'f32', ['physics.read'], true, 'The surface normal the last raycast struck, along x.'),
372
+ define(PHYSICS_MODULE, 'hitNormalY', [{ name: 'world', type: 'PhysicsWorld' }], 'f32', ['physics.read'], true, 'The surface normal the last raycast struck, along y.'),
373
+ define(PHYSICS_MODULE, 'hitNormalZ', [{ name: 'world', type: 'PhysicsWorld' }], 'f32', ['physics.read'], true, 'The surface normal the last raycast struck, along z.'),
374
+ define(PHYSICS_MODULE, 'hitFraction', [{ name: 'world', type: 'PhysicsWorld' }], 'f32', ['physics.read'], true, 'How far along the ray the last hit was, from 0 to 1.'),
375
+ define(PHYSICS_MODULE, 'contactCount', [{ name: 'world', type: 'PhysicsWorld' }], 'i32', ['physics.read'], true, 'How many contact events the last step produced. Drain them after stepping, never during.'),
376
+ define(PHYSICS_MODULE, 'contactKind', [
377
+ { name: 'world', type: 'PhysicsWorld' },
378
+ { name: 'index', type: 'i32' },
379
+ ], 'i32', ['physics.read'], true, "An event's kind: 0 entered, 1 still touching, 2 left."),
380
+ define(PHYSICS_MODULE, 'contactA', [
381
+ { name: 'world', type: 'PhysicsWorld' },
382
+ { name: 'index', type: 'i32' },
383
+ ], 'i32', ['physics.read'], true, 'The lower-indexed body of a contact event.'),
384
+ define(PHYSICS_MODULE, 'contactB', [
385
+ { name: 'world', type: 'PhysicsWorld' },
386
+ { name: 'index', type: 'i32' },
387
+ ], 'i32', ['physics.read'], true, 'The higher-indexed body of a contact event.'),
388
+ define(PHYSICS_MODULE, 'applyImpulse', [
389
+ { name: 'world', type: 'PhysicsWorld' },
390
+ { name: 'body', type: 'i32' },
391
+ { name: 'px', type: 'f32' },
392
+ { name: 'py', type: 'f32' },
393
+ { name: 'pz', type: 'f32' },
394
+ { name: 'atX', type: 'f32' },
395
+ { name: 'atY', type: 'f32' },
396
+ { name: 'atZ', type: 'f32' },
397
+ ], 'void', ['physics.write'], false, '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.'),
398
+ define(PHYSICS_MODULE, 'applyForce', [
399
+ { name: 'world', type: 'PhysicsWorld' },
400
+ { name: 'body', type: 'i32' },
401
+ { name: 'fx', type: 'f32' },
402
+ { name: 'fy', type: 'f32' },
403
+ { name: 'fz', type: 'f32' },
404
+ { name: 'dt', type: 'f32' },
405
+ { name: 'atX', type: 'f32' },
406
+ { name: 'atY', type: 'f32' },
407
+ { name: 'atZ', type: 'f32' },
408
+ ], 'void', ['physics.write'], false, 'A force applied for one step, which is an impulse of force times dt. The point is required for the same reason.'),
409
+ define(PHYSICS_MODULE, 'setVelocity', [
410
+ { name: 'world', type: 'PhysicsWorld' },
411
+ { name: 'body', type: 'i32' },
412
+ { name: 'vx', type: 'f32' },
413
+ { name: 'vy', type: 'f32' },
414
+ { name: 'vz', type: 'f32' },
415
+ ], 'void', ['physics.write'], false, "Set a body's velocity outright, waking its island."),
416
+ define(PHYSICS_MODULE, 'setPosition', [
417
+ { name: 'world', type: 'PhysicsWorld' },
418
+ { name: 'body', type: 'i32' },
419
+ { name: 'x', type: 'f32' },
420
+ { name: 'y', type: 'f32' },
421
+ { name: 'z', type: 'f32' },
422
+ ], 'void', ['physics.write'], false, 'Move a body outright. A teleport rather than a push: the broadphase moves with it.'),
423
+ define(PHYSICS_MODULE, 'wake', [
424
+ { name: 'world', type: 'PhysicsWorld' },
425
+ { name: 'body', type: 'i32' },
426
+ ], 'void', ['physics.write'], false, 'Wake a body and everything sharing its island, because a sleeping neighbour would settle it again.'),
427
+ define(PHYSICS_MODULE, 'setGravity', [
428
+ { name: 'world', type: 'PhysicsWorld' },
429
+ { name: 'x', type: 'f32' },
430
+ { name: 'y', type: 'f32' },
431
+ { name: 'z', type: 'f32' },
432
+ ], 'void', ['physics.write'], false, "The world's gravity, in metres per second squared."),
433
+ ];
434
+ /** `drift/input` — Track H, complete: action maps, rebinding, gamepad, pointer and touch. */
435
+ export const INPUT_CAPABILITIES = [
436
+ define(INPUT_MODULE, 'down', [
437
+ { name: 'actions', type: 'Actions' },
438
+ { name: 'action', type: 'String' },
439
+ ], 'bool', ['input.read'], false, 'Whether an action is held.'),
440
+ define(INPUT_MODULE, 'pressed', [
441
+ { name: 'actions', type: 'Actions' },
442
+ { name: 'action', type: 'String' },
443
+ ], 'bool', ['input.read'], false, 'Whether an action went down this frame.'),
444
+ define(INPUT_MODULE, 'axisX', [
445
+ { name: 'actions', type: 'Actions' },
446
+ { name: 'action', type: 'String' },
447
+ ], 'f32', ['input.read'], false, "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."),
448
+ define(INPUT_MODULE, 'axisY', [
449
+ { name: 'actions', type: 'Actions' },
450
+ { name: 'action', type: 'String' },
451
+ ], 'f32', ['input.read'], false, "A directional action's vertical component, shortened to the rim with the horizontal one. For two independent controls, read rawAxisY instead."),
452
+ /*
453
+ * **The pair that is not normalised, and the reason both pairs exist.** `axisX` and `axisY` treat
454
+ * an action's two axes as one direction, which is right on foot and wrong at a wheel: throttle
455
+ * and steering are two controls that happen to share an action, and dividing both by √2 means a
456
+ * driver holding forward and left can never reach full lock while accelerating. A consumer lost
457
+ * two weeks to that in TypeScript before anybody looked at `ActionMap.vector`, and a script
458
+ * author reading `axisX` would have met it identically, because this binding calls that method.
459
+ *
460
+ * Added rather than corrected in place: `axisX` behaves as it always has, because changing what
461
+ * it returns would make every scripted character quietly faster on a diagonal.
462
+ */
463
+ define(INPUT_MODULE, 'rawAxisX', [
464
+ { name: 'actions', type: 'Actions' },
465
+ { name: 'action', type: 'String' },
466
+ ], 'f32', ['input.read'], false, "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."),
467
+ define(INPUT_MODULE, 'rawAxisY', [
468
+ { name: 'actions', type: 'Actions' },
469
+ { name: 'action', type: 'String' },
470
+ ], 'f32', ['input.read'], false, "An action's vertical axis on its own, not shortened against the horizontal one."),
471
+ /*
472
+ * **Rumble answers a boolean and a script has to read it**, which is the whole reason this row
473
+ * exists in the shape it does. Most pads on most browsers have no motors, so a settings screen
474
+ * that offers the control unconditionally is offering something a player's hardware ignores. The
475
+ * host layer answers false rather than accepting the call and doing nothing quietly, and these
476
+ * three carry that answer across the language boundary unchanged.
477
+ *
478
+ * `input.write` does not exist as an effect and this does not invent one: driving a motor is a
479
+ * write to the *host*, which is what `host` already means, and it is outside the deterministic
480
+ * boundary for the same reason `audio.write` is — a replay must not be able to tell whether the
481
+ * player's pad was plugged in.
482
+ */
483
+ define(INPUT_MODULE, 'canRumble', [{ name: 'actions', type: 'Actions' }], 'bool', ['host'], false, '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.'),
484
+ define(INPUT_MODULE, 'rumble', [
485
+ { name: 'actions', type: 'Actions' },
486
+ { name: 'durationMs', type: 'f32' },
487
+ { name: 'strong', type: 'f32' },
488
+ { name: 'weak', type: 'f32' },
489
+ ], 'bool', ['host'], false, '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.'),
490
+ define(INPUT_MODULE, 'stopRumble', [{ name: 'actions', type: 'Actions' }], 'bool', ['host'], false, 'Stop whatever that pad is playing. Answers whether the platform took it.'),
491
+ ];
492
+ /**
493
+ * `drift/camera` — the cinematic cameras, and **only what they actually have**.
494
+ *
495
+ * The first version of this binding described `lookAt` and `setPosition`. `CinematicCamera` has
496
+ * neither: it has `cut`, `snap`, `shotAgeSec` and `update`. The implementation reached them through
497
+ * an optional-call cast, so a script calling `camera.lookAt(…)` compiled, linked, ran, and did
498
+ * **nothing** — the silent no-op this repository's rules forbid, arriving by way of a binding
499
+ * written from memory of what a camera ought to have rather than from the class.
500
+ *
501
+ * `cut` is deliberately still absent: it takes a `ShotParams`, and a script has no way to construct
502
+ * an engine record yet. Binding it would mean inventing a flattened signature that drifts from the
503
+ * real one the first time a shot gains a field. It waits for a way to express the record.
504
+ */
505
+ export const CAMERA_CAPABILITIES = [
506
+ define(CAMERA_MODULE, 'snap', [{ name: 'camera', type: 'Camera' }], 'void', ['scene.write'], false, 'Jump the camera to where it is heading, skipping the ease. What a scene cut needs.'),
507
+ define(CAMERA_MODULE, 'shotAge', [{ name: 'camera', type: 'Camera' }], 'f32', ['scene.read'], true, 'Seconds since the current shot began. A read, so a deterministic function may ask.'),
508
+ ];
509
+ const some = (value) => ({ tag: 'some', value });
510
+ const none = { tag: 'none' };
511
+ /**
512
+ * Round a double to the nearest single, because every signature in this file is declared `f32`.
513
+ *
514
+ * **Still correct at language 1.5.0, and the reason is worth writing down beside it.** That release
515
+ * introduced a `float` type meaning "`f32` or `f64`, the same one throughout the call", and moved
516
+ * `std/math`'s rounding out of its implementations to the call site — because a library that
517
+ * rounded every result would be destroying precision the moment a call resolved to double. The
518
+ * language's own note to hosts is therefore: implement a `float` capability once, in double, and do
519
+ * **not** round; and rounding stays right for a signature left written `f32`.
520
+ *
521
+ * Every capability here is written `f32` and none is written `float`, so this is the second case
522
+ * and it is unchanged. **What would make it wrong** is somebody widening one of these signatures to
523
+ * `float` and leaving the call here — at which point a double-precision call would be silently
524
+ * narrowed by the host after the compiler had promised it would not be.
525
+ */
526
+ const f = Math.fround;
527
+ export function timeImplementation(clocks) {
528
+ return {
529
+ fixedDelta: () => f(clocks.fixedDelta()),
530
+ frameDelta: () => f(clocks.frameDelta()),
531
+ wallDelta: () => f(clocks.wallDelta()),
532
+ elapsed: () => f(clocks.elapsed()),
533
+ };
534
+ }
535
+ /**
536
+ * The seeded generator, dispatching to core's frozen sequence.
537
+ *
538
+ * `index` returns 0 for an empty collection rather than an error, because "pick from nothing" has
539
+ * one sane answer and a script asking is usually mid-way through a list that will have items next
540
+ * frame.
541
+ */
542
+ export function randomImplementation() {
543
+ return {
544
+ unit: (seed) => f(hashToUnit(seed)),
545
+ range: (seed, low, high) => f(low + hashToUnit(seed) * (high - low)),
546
+ index: (seed, count) => count <= 0 ? 0 : Math.floor(hashToUnit(seed) * count) % count,
547
+ };
548
+ }
549
+ export function eventsImplementation() {
550
+ return {
551
+ push(queue, id, priority, holdFor) {
552
+ /* `holdFor` arrives in seconds because units erase to the base unit, and the queue counts in
553
+ milliseconds. Converting here rather than exposing milliseconds to a script keeps every
554
+ duration in the language one kind of number. */
555
+ queue.push({ id, priority, durationMs: holdFor * 1000 });
556
+ },
557
+ current(queue) {
558
+ const message = queue.current;
559
+ return message === null ? none : some(message.id);
560
+ },
561
+ clear(queue) {
562
+ queue.clear();
563
+ },
564
+ };
565
+ }
566
+ export function persistenceImplementation() {
567
+ return {
568
+ read(store, key) {
569
+ const value = store.read(key);
570
+ /* `null` becomes `none`, which is the whole reason this is a capability rather than a direct
571
+ call: the language has no null, so the boundary is where one stops existing. */
572
+ return value === null ? none : some(value);
573
+ },
574
+ write(store, key, value) {
575
+ store.write(key, value);
576
+ },
577
+ remove(store, key) {
578
+ store.remove(key);
579
+ },
580
+ /* `instanceof` rather than a duck-typed property check: a store that happens to carry a
581
+ `status` field meaning something else would otherwise be read as this one. */
582
+ saveStatus: (store) => store instanceof RemoteSaveStore ? store.status : 'idle',
583
+ pendingSaves: (store) => (store instanceof RemoteSaveStore ? store.pending : 0),
584
+ };
585
+ }
586
+ export function sceneImplementation() {
587
+ return {
588
+ positionX: (node) => f(node.position[0]),
589
+ positionY: (node) => f(node.position[1]),
590
+ positionZ: (node) => f(node.position[2]),
591
+ setPosition: (node, x, y, z) => node.setPosition(x, y, z),
592
+ setScale: (node, x, y, z) => node.setScale(x, y, z),
593
+ setRotation: (node, x, y, z, radians) => node.setRotationAxisAngle(x, y, z, radians),
594
+ /* Normalised on the way out rather than stored that way: `WindState` carries a velocity, and
595
+ a direction is what a behaviour asks for. Zero speed has no direction, and zero is the
596
+ honest answer rather than a NaN from dividing by it. */
597
+ windDirectionX: (wind) => f(wind.speed > 1e-5 ? wind.velocityX / wind.speed : 0),
598
+ windDirectionZ: (wind) => f(wind.speed > 1e-5 ? wind.velocityZ / wind.speed : 0),
599
+ windSpeed: (wind) => f(wind.speed),
600
+ windGust: (wind) => f(wind.gust),
601
+ distance: (a, b) => {
602
+ const dx = a.position[0] - b.position[0];
603
+ const dy = a.position[1] - b.position[1];
604
+ const dz = a.position[2] - b.position[2];
605
+ return f(Math.sqrt(dx * dx + dy * dy + dz * dz));
606
+ },
607
+ };
608
+ }
609
+ export function physicsImplementation() {
610
+ /* One scratch buffer, reused. `query` writes indices into an out-parameter and returns how many
611
+ it wrote, so a fresh array per call would allocate on a path a script runs every frame — which
612
+ is the rule the whole engine is built on. Sized generously; a broad-phase answer only needs to
613
+ know whether the count was non-zero. */
614
+ const hits = new Int32Array(256);
615
+ /*
616
+ * The last raycast, per world, held **here rather than on the engine**.
617
+ *
618
+ * A script cannot receive a record, so a hit is read back through accessors — and putting the
619
+ * state in the binding keeps `PhysicsWorld` free of a field that exists only because of a
620
+ * language limitation. A `WeakMap` rather than one shared slot, because two worlds are two
621
+ * questions and a shared slot would let one answer the other's.
622
+ */
623
+ const lastHit = new WeakMap();
624
+ const hitOf = (world) => {
625
+ let hit = lastHit.get(world);
626
+ if (!hit) {
627
+ hit = createRayHit();
628
+ lastHit.set(world, hit);
629
+ }
630
+ return hit;
631
+ };
632
+ const filter = { mask: 0xffffffff };
633
+ /*
634
+ * The last `nearestWithin`, per collider set, in the same shape and for the same reasons.
635
+ *
636
+ * A consumer reported the gap this closes: `anyWithin` answered *whether* something was near and
637
+ * never *what*, so a script could find the nearest person through `drift/ecs` and could not find
638
+ * the nearest wall at all — and the rule for what is in reach moved into TypeScript, which is
639
+ * exactly the rule somebody wants to edit with the game running.
640
+ *
641
+ * **The answer is a search plus accessors rather than a value**, which is the third time this
642
+ * file has drawn that conclusion — `raycast` with its `hit` functions, `findNearest` with
643
+ * `nearest`, and now this. A script cannot receive a record, and returning an option would
644
+ * allocate one object per call on a path an agent runs every frame.
645
+ */
646
+ const lastNear = new WeakMap();
647
+ const nearOf = (colliders) => {
648
+ let near = lastNear.get(colliders);
649
+ if (!near) {
650
+ near = { collider: -1, distance: 0, x: 0, y: 0, z: 0 };
651
+ lastNear.set(colliders, near);
652
+ }
653
+ return near;
654
+ };
655
+ /* One reused box for the scan, because `bounds` writes into a caller's object and a fresh one
656
+ per candidate is the per-frame allocation the engine forbids. */
657
+ const box = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
658
+ /*
659
+ * The boxes of a group being built, per set. A `WeakMap` for the same reason `nearOf` uses one:
660
+ * two sets are two builders, and a shared slot would let one set's region land in the other.
661
+ */
662
+ const pending = new WeakMap();
663
+ return {
664
+ colliderCount: (colliders) => colliders.count,
665
+ colliderCapacity: (colliders) => colliders.capacity,
666
+ colliderBytes: (colliders) => colliders.bytes().total,
667
+ beginColliderGroup(colliders) {
668
+ if (pending.has(colliders)) {
669
+ throw new Error('beginColliderGroup: a group is already open on this set. Close it with ' +
670
+ '`endColliderGroup` before opening another.');
671
+ }
672
+ pending.set(colliders, []);
673
+ },
674
+ addColliderBox(colliders, cx, cy, cz, hx, hy, hz) {
675
+ const open = pending.get(colliders);
676
+ if (open === undefined) {
677
+ throw new Error('addColliderBox: no group is open on this set. Call `beginColliderGroup` first.');
678
+ }
679
+ open.push(boxCollider(cx, cy, cz, hx, hy, hz));
680
+ },
681
+ endColliderGroup(colliders) {
682
+ const open = pending.get(colliders);
683
+ if (open === undefined) {
684
+ throw new Error('endColliderGroup: no group is open on this set.');
685
+ }
686
+ pending.delete(colliders);
687
+ return colliders.add(open);
688
+ },
689
+ removeColliderGroup(colliders, group) {
690
+ colliders.remove(group);
691
+ },
692
+ anyWithin(colliders, x, y, z, radius) {
693
+ const found = colliders.query(x - radius, y - radius, z - radius, x + radius, y + radius, z + radius, hits);
694
+ return found > 0;
695
+ },
696
+ nearestWithin(colliders, x, y, z, radius) {
697
+ const near = nearOf(colliders);
698
+ near.collider = -1;
699
+ near.distance = 0;
700
+ near.x = 0;
701
+ near.y = 0;
702
+ near.z = 0;
703
+ const found = colliders.query(x - radius, y - radius, z - radius, x + radius, y + radius, z + radius, hits);
704
+ let best = radius * radius;
705
+ for (let i = 0; i < found; i += 1) {
706
+ const index = hits[i];
707
+ colliders.bounds(index, box);
708
+ /* The closest point on the box, which is the clamp and nothing more. Inside it, the clamp
709
+ is the query point itself and the distance is zero — which is the right answer for a
710
+ character standing in a wall and the one a square root would also give. */
711
+ const px = x < box.minX ? box.minX : x > box.maxX ? box.maxX : x;
712
+ const py = y < box.minY ? box.minY : y > box.maxY ? box.maxY : y;
713
+ const pz = z < box.minZ ? box.minZ : z > box.maxZ ? box.maxZ : z;
714
+ const dx = px - x;
715
+ const dy = py - y;
716
+ const dz = pz - z;
717
+ const squared = dx * dx + dy * dy + dz * dz;
718
+ /* Squared throughout the scan and rooted once at the end: the comparison is the same and
719
+ this runs over every candidate the hash returned, every frame an agent looks around. */
720
+ if (squared > best)
721
+ continue;
722
+ best = squared;
723
+ near.collider = index;
724
+ near.distance = squared;
725
+ near.x = px;
726
+ near.y = py;
727
+ near.z = pz;
728
+ }
729
+ if (near.collider < 0)
730
+ return false;
731
+ near.distance = Math.sqrt(near.distance);
732
+ return true;
733
+ },
734
+ nearCollider: (colliders) => nearOf(colliders).collider,
735
+ nearX: (colliders) => f(nearOf(colliders).x),
736
+ nearY: (colliders) => f(nearOf(colliders).y),
737
+ nearZ: (colliders) => f(nearOf(colliders).z),
738
+ nearDistance: (colliders) => f(nearOf(colliders).distance),
739
+ bodyCount: (world) => world.bodies.count,
740
+ bodyX: (world, body) => f(world.bodies.posX[body] ?? 0),
741
+ bodyY: (world, body) => f(world.bodies.posY[body] ?? 0),
742
+ bodyZ: (world, body) => f(world.bodies.posZ[body] ?? 0),
743
+ bodyVelX: (world, body) => f(world.bodies.velX[body] ?? 0),
744
+ bodyVelY: (world, body) => f(world.bodies.velY[body] ?? 0),
745
+ bodyVelZ: (world, body) => f(world.bodies.velZ[body] ?? 0),
746
+ bodyMass: (world, body) => f(world.bodyMass(body)),
747
+ sleeping: (world, body) => world.sleeping(body),
748
+ raycast(world, x, y, z, dx, dy, dz, maxDistance, mask) {
749
+ filter.mask = mask;
750
+ return world.raycast(x, y, z, dx, dy, dz, maxDistance, hitOf(world), filter);
751
+ },
752
+ hitBody: (world) => hitOf(world).body,
753
+ hitX: (world) => f(hitOf(world).x),
754
+ hitY: (world) => f(hitOf(world).y),
755
+ hitZ: (world) => f(hitOf(world).z),
756
+ hitNormalX: (world) => f(hitOf(world).nx),
757
+ hitNormalY: (world) => f(hitOf(world).ny),
758
+ hitNormalZ: (world) => f(hitOf(world).nz),
759
+ hitFraction: (world) => f(hitOf(world).fraction),
760
+ contactCount: (world) => world.events.count,
761
+ contactKind: (world, index) => world.events.data[index * 3] ?? 0,
762
+ contactA: (world, index) => world.events.data[index * 3 + 1] ?? 0,
763
+ contactB: (world, index) => world.events.data[index * 3 + 2] ?? 0,
764
+ applyImpulse: (world, body, px, py, pz, atX, atY, atZ) => world.applyImpulse(body, px, py, pz, atX, atY, atZ),
765
+ applyForce: (world, body, fx, fy, fz, dt, atX, atY, atZ) => world.applyForce(body, fx, fy, fz, dt, atX, atY, atZ),
766
+ setVelocity: (world, body, vx, vy, vz) => world.setVelocity(body, vx, vy, vz),
767
+ setPosition: (world, body, x, y, z) => world.setPosition(body, x, y, z),
768
+ wake: (world, body) => world.wakeIsland(body),
769
+ setGravity(world, x, y, z) {
770
+ world.gravityX = x;
771
+ world.gravityY = y;
772
+ world.gravityZ = z;
773
+ },
774
+ };
775
+ }
776
+ export function inputImplementation() {
777
+ const scratch = { x: 0, y: 0 };
778
+ return {
779
+ down: (actions, action) => actions.down(action),
780
+ pressed: (actions, action) => actions.pressed(action),
781
+ axisX(actions, action) {
782
+ /* One scratch object reused, because `vector` writes into an out-parameter and a fresh
783
+ literal per call would allocate on a path a script runs every frame. */
784
+ actions.vector(action, scratch);
785
+ return f(scratch.x);
786
+ },
787
+ axisY(actions, action) {
788
+ actions.vector(action, scratch);
789
+ return f(scratch.y);
790
+ },
791
+ /* No scratch: `axis` returns the number rather than filling an out-parameter, because one axis
792
+ is one value and there was nothing to write into. */
793
+ rawAxisX: (actions, action) => f(actions.axis(action, 'x')),
794
+ rawAxisY: (actions, action) => f(actions.axis(action, 'y')),
795
+ canRumble: (actions) => actions.canRumble,
796
+ rumble: (actions, durationMs, strong, weak) => actions.rumble(durationMs, strong, weak),
797
+ stopRumble: (actions) => actions.stopRumble(),
798
+ };
799
+ }
800
+ export function cameraImplementation() {
801
+ /* Called directly, with no optional-call cast anywhere. A cast that tolerates a missing method is
802
+ a binding that cannot fail when the engine renames one — which is how the first version of this
803
+ described two methods the class does not have. */
804
+ return {
805
+ snap: (camera) => camera.snap(),
806
+ shotAge: (camera) => f(camera.shotAgeSec),
807
+ };
808
+ }
809
+ /* Referenced so the imports that document what this binds are not dropped as unused. `mulberry32`,
810
+ `angleDelta` and `wrapAngle` are the engine's, named here because a reader looking for what a
811
+ binding dispatches to should find it in one place. */
812
+ void mulberry32;
813
+ void angleDelta;
814
+ void wrapAngle;