@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,550 @@
1
+ /**
2
+ * `drift/ecs` and `drift/prefab` — the entity model as DriftScript reaches it.
3
+ *
4
+ * These are the two surfaces the linker has refused by name since Track N shipped, and this file is
5
+ * what kills that refusal. Like every binding here, nothing in it is new engine code: a binding is a
6
+ * description and a lookup.
7
+ *
8
+ * ---
9
+ *
10
+ * ## A component is named by a string, and that is forced rather than chosen
11
+ *
12
+ * A script cannot hold a `ComponentType` — it is an object with a schema, and the language's types
13
+ * are numbers, strings, booleans and opaque handles. So a component is addressed by name and the
14
+ * host resolves it. **The cost is that a typo is a runtime refusal rather than a compile error**,
15
+ * and the fix is DS-5's `component` declaration, which is Track N's to build now that this exists.
16
+ * Refusing in words naming the component and listing what the world has is what the interim owes.
17
+ *
18
+ * ## There is no query here, and that is not an omission
19
+ *
20
+ * Iterating a query needs a loop form the language does not have for it — that is DS-5 and DS-6.
21
+ * What a script can do today is walk a component's dense array by index, which `while` expresses
22
+ * perfectly. **The order is insertion modified by swap-removal**, so an entity removed during a
23
+ * walk moves the last one into a position already passed; a script that removes while walking
24
+ * should walk backwards, and the doc on `at` says so.
25
+ */
26
+ import {
27
+ ENTITY_INDEX_CEILING,
28
+ type ComponentType,
29
+ type ComponentView,
30
+ type Entity,
31
+ type Prefab,
32
+ type QueryCursor,
33
+ type World,
34
+ instantiate,
35
+ } from '@driftengine/entities';
36
+ import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
37
+
38
+ export const ECS_MODULE = 'drift/ecs';
39
+ export const PREFAB_MODULE = 'drift/prefab';
40
+
41
+ const define = (
42
+ module: string,
43
+ name: string,
44
+ params: readonly { name: string; type: string }[],
45
+ returns: string,
46
+ effects: CapabilityDefinition['effects'],
47
+ deterministic: boolean,
48
+ doc: string,
49
+ ): CapabilityDefinition =>
50
+ defineCapability({
51
+ module,
52
+ name,
53
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
54
+ params: [...params],
55
+ returns,
56
+ effects,
57
+ deterministic,
58
+ doc,
59
+ implementation: `${module}.${name}`,
60
+ });
61
+
62
+ export const ENTITY_TYPES: readonly OpaqueType[] = [
63
+ {
64
+ module: ECS_MODULE,
65
+ name: 'World',
66
+ doc: 'A world: its entities, their components and its systems.',
67
+ },
68
+ {
69
+ module: ECS_MODULE,
70
+ name: 'Cursor',
71
+ doc: "A query in progress. Borrowed from the world's pool and given back when the walk ends, so it may not outlive the loop that opened it.",
72
+ },
73
+ {
74
+ module: ECS_MODULE,
75
+ name: 'View',
76
+ doc: "A component's live columns. The object stays valid across a reallocation; the arrays inside it do not, so index through the view rather than holding one.",
77
+ },
78
+ ];
79
+
80
+ /**
81
+ * An entity is `Entity`, and it took the language until DriftScript 1.6.0 to have the word.
82
+ *
83
+ * A handle carries a 26-bit index and a 27-bit generation, which is the entire 53-bit budget a
84
+ * double holds. A `u32` would truncate the generation — and a truncated generation is a stale
85
+ * handle that compares equal to a live one, which is the exact failure the generation exists to
86
+ * prevent. So the width was never negotiable, and until the language could name it these all said
87
+ * `f64` with this paragraph explaining why.
88
+ *
89
+ * **`Entity` is a type of its own there rather than an alias**, which is what makes the change worth
90
+ * making: a handle is assignable *to* an `f64` and not *from* one, so a number a script computed
91
+ * cannot arrive where a handle is wanted, and `e.Health` is legal on a handle and on nothing else.
92
+ * The language's own note asked for this by name — a capability that hands back a handle as an
93
+ * `f64` and expects it to keep working as one is what it said would make the asymmetry wrong.
94
+ *
95
+ * **`read` still answers `f64`**, and that is not an oversight: it reads a component *field*, which
96
+ * is a number, and typing it as a handle would be the same mistake in the other direction.
97
+ */
98
+ export const ECS_CAPABILITIES: readonly CapabilityDefinition[] = [
99
+ define(
100
+ ECS_MODULE,
101
+ 'create',
102
+ [{ name: 'world', type: 'World' }],
103
+ 'Entity',
104
+ ['ecs.write'],
105
+ false,
106
+ 'A fresh entity with nothing on it. The handle is exact; do not put one in an `f32`.',
107
+ ),
108
+ define(
109
+ ECS_MODULE,
110
+ 'destroy',
111
+ [
112
+ { name: 'world', type: 'World' },
113
+ { name: 'entity', type: 'Entity' },
114
+ ],
115
+ 'bool',
116
+ ['ecs.write'],
117
+ false,
118
+ 'Remove an entity and every component it had. False when the handle was already stale.',
119
+ ),
120
+ define(
121
+ ECS_MODULE,
122
+ 'alive',
123
+ [
124
+ { name: 'world', type: 'World' },
125
+ { name: 'entity', type: 'Entity' },
126
+ ],
127
+ 'bool',
128
+ ['ecs.read'],
129
+ true,
130
+ 'Whether this handle still names a live entity. A handle to something destroyed is false, even after its slot is reused.',
131
+ ),
132
+ define(
133
+ ECS_MODULE,
134
+ 'has',
135
+ [
136
+ { name: 'world', type: 'World' },
137
+ { name: 'entity', type: 'Entity' },
138
+ { name: 'component', type: 'String' },
139
+ ],
140
+ 'bool',
141
+ ['ecs.read'],
142
+ true,
143
+ 'Whether an entity carries a component.',
144
+ ),
145
+ define(
146
+ ECS_MODULE,
147
+ 'attach',
148
+ [
149
+ { name: 'world', type: 'World' },
150
+ { name: 'entity', type: 'Entity' },
151
+ { name: 'component', type: 'String' },
152
+ ],
153
+ 'void',
154
+ ['ecs.write'],
155
+ false,
156
+ 'Give an entity a component, every field at the zero for its type. Set them with `write`.',
157
+ ),
158
+ define(
159
+ ECS_MODULE,
160
+ 'detach',
161
+ [
162
+ { name: 'world', type: 'World' },
163
+ { name: 'entity', type: 'Entity' },
164
+ { name: 'component', type: 'String' },
165
+ ],
166
+ 'bool',
167
+ ['ecs.write'],
168
+ false,
169
+ 'Take a component away. False when it did not have one.',
170
+ ),
171
+ define(
172
+ ECS_MODULE,
173
+ 'read',
174
+ [
175
+ { name: 'world', type: 'World' },
176
+ { name: 'entity', type: 'Entity' },
177
+ { name: 'component', type: 'String' },
178
+ { name: 'field', type: 'String' },
179
+ ],
180
+ 'f64',
181
+ ['ecs.read'],
182
+ true,
183
+ 'One numeric field. Reading a field of a component an entity does not have is 0.',
184
+ ),
185
+ define(
186
+ ECS_MODULE,
187
+ 'write',
188
+ [
189
+ { name: 'world', type: 'World' },
190
+ { name: 'entity', type: 'Entity' },
191
+ { name: 'component', type: 'String' },
192
+ { name: 'field', type: 'String' },
193
+ { name: 'value', type: 'f64' },
194
+ ],
195
+ 'void',
196
+ ['ecs.write'],
197
+ false,
198
+ 'Set one numeric field.',
199
+ ),
200
+ define(
201
+ ECS_MODULE,
202
+ 'count',
203
+ [
204
+ { name: 'world', type: 'World' },
205
+ { name: 'component', type: 'String' },
206
+ ],
207
+ 'u32',
208
+ ['ecs.read'],
209
+ true,
210
+ 'How many entities carry a component.',
211
+ ),
212
+ define(
213
+ ECS_MODULE,
214
+ 'at',
215
+ [
216
+ { name: 'world', type: 'World' },
217
+ { name: 'component', type: 'String' },
218
+ { name: 'index', type: 'u32' },
219
+ ],
220
+ 'Entity',
221
+ ['ecs.read'],
222
+ true,
223
+ 'The nth entity carrying a component. **The order is insertion modified by swap-removal**, so a walk that removes should count downwards — removing moves the last entity into a position already passed.',
224
+ ),
225
+ /*
226
+ * **Two capabilities rather than one, and the split is what `Entity` cost.**
227
+ *
228
+ * A search that finds nothing has to say so, and once the answer is an `Entity` there is no value
229
+ * left to say it with: a handle is not a number a script can compare to −1, which is the whole
230
+ * point of it being its own type. An option would say it and allocates one object per call, on a
231
+ * path written to run every frame for every agent that looks around — the allocation `next`
232
+ * refuses one screen up.
233
+ *
234
+ * So the result is read back through a second accessor, which is exactly what `raycast` and its
235
+ * `hitBody` already do in this repository and for the same reason. **The cost is the same too**:
236
+ * the result is only valid until the next search on the same world, and that is said here and on
237
+ * the accessor rather than left for somebody to find out.
238
+ */
239
+ define(
240
+ ECS_MODULE,
241
+ 'findNearest',
242
+ [
243
+ { name: 'world', type: 'World' },
244
+ { name: 'component', type: 'String' },
245
+ { name: 'fieldX', type: 'String' },
246
+ { name: 'fieldY', type: 'String' },
247
+ { name: 'fieldZ', type: 'String' },
248
+ { name: 'x', type: 'float' },
249
+ { name: 'y', type: 'float' },
250
+ { name: 'z', type: 'float' },
251
+ { name: 'radius', type: 'float' },
252
+ ],
253
+ 'bool',
254
+ ['ecs.read'],
255
+ true,
256
+ "Whether any entity carrying a component has position fields putting it within a radius, and the search `nearest` then reads. **A linear scan of that component's column, not an index** — the entity model keeps no spatial structure, so this costs one pass over everything carrying the component. Position is read from three fields you name, so nothing here knows what a transform is called.",
257
+ ),
258
+ define(
259
+ ECS_MODULE,
260
+ 'nearest',
261
+ [{ name: 'world', type: 'World' }],
262
+ 'Entity',
263
+ ['ecs.read'],
264
+ true,
265
+ 'The entity the last `findNearest` on this world found. **Valid only until the next one**, and meaningless when that answered false — the same lifetime a raycast hit has.',
266
+ ),
267
+ ];
268
+
269
+ /**
270
+ * The four a query loop is built on.
271
+ *
272
+ * **Nobody writes these by hand.** They are what `for e in query<…>() { … }` compiles to, and a
273
+ * script author never names one — which is why their docs describe the shape of the generated code
274
+ * rather than how to call them. They are capabilities rather than a second bind hook so that
275
+ * generated code reaches them exactly as it reaches everything else, through `__bind($host)`.
276
+ */
277
+ export const QUERY_CAPABILITIES: readonly CapabilityDefinition[] = [
278
+ define(
279
+ ECS_MODULE,
280
+ 'query',
281
+ [
282
+ { name: 'world', type: 'World' },
283
+ { name: 'a', type: 'String' },
284
+ { name: 'b', type: 'String' },
285
+ { name: 'c', type: 'String' },
286
+ { name: 'd', type: 'String' },
287
+ ],
288
+ 'Cursor',
289
+ ['ecs.read'],
290
+ true,
291
+ 'Open a walk over everything carrying these components. The cursor is borrowed from the world’s pool; ending the walk gives it back.',
292
+ ),
293
+ define(
294
+ ECS_MODULE,
295
+ 'without',
296
+ [
297
+ { name: 'cursor', type: 'Cursor' },
298
+ { name: 'component', type: 'String' },
299
+ ],
300
+ 'void',
301
+ ['ecs.read'],
302
+ true,
303
+ 'Narrow an open walk to entities that do **not** carry a component.',
304
+ ),
305
+ define(
306
+ ECS_MODULE,
307
+ 'next',
308
+ [{ name: 'cursor', type: 'Cursor' }],
309
+ 'Entity',
310
+ ['ecs.read'],
311
+ true,
312
+ 'The next entity, or a **negative** number when the walk is done — which also gives the cursor back. Negative rather than zero, because zero is a legal handle.',
313
+ ),
314
+ define(
315
+ ECS_MODULE,
316
+ 'view',
317
+ [
318
+ { name: 'world', type: 'World' },
319
+ { name: 'component', type: 'String' },
320
+ { name: 'forWriting', type: 'bool' },
321
+ ],
322
+ 'View',
323
+ ['ecs.read'],
324
+ true,
325
+ 'A component’s live columns. `forWriting` decides which of a system’s declarations is checked — a loop that only reads takes a readable view, and asking for more than the system declared is refused.',
326
+ ),
327
+ ];
328
+
329
+ export const PREFAB_CAPABILITIES: readonly CapabilityDefinition[] = [
330
+ define(
331
+ PREFAB_MODULE,
332
+ 'spawn',
333
+ [
334
+ { name: 'world', type: 'World' },
335
+ { name: 'prefab', type: 'String' },
336
+ ],
337
+ 'Entity',
338
+ ['ecs.write'],
339
+ false,
340
+ 'Make one, with every component the prefab names. Overrides are the host’s to apply; a script that wants a different value writes it after.',
341
+ ),
342
+ ];
343
+
344
+ /** What a host supplies so a name can become a component type or a prefab. */
345
+ export interface EntityServices {
346
+ /**
347
+ * Component types by the name a script writes.
348
+ *
349
+ * There is no world here: every capability takes the world as a parameter, because a consumer
350
+ * with two worlds — a simulation and a preview — would otherwise need two hosts.
351
+ */
352
+ readonly components: ReadonlyMap<string, ComponentType>;
353
+ /** Prefabs by name. Absent when the host has none, which makes `drift/prefab` refuse in words. */
354
+ readonly prefabs?: ReadonlyMap<string, Prefab>;
355
+ }
356
+
357
+ export function entitiesImplementation(services: EntityServices): Record<string, unknown> {
358
+ const { components } = services;
359
+
360
+ /**
361
+ * Resolve a component name, or refuse naming what the world has.
362
+ *
363
+ * **Not `undefined` and not a no-op.** A script that misspells a component would otherwise read
364
+ * zeroes and write into nothing, which is a behaviour that runs, reports success and is wrong —
365
+ * the silent no-op this repository forbids, arriving through a string.
366
+ */
367
+ const resolve = (name: string): ComponentType => {
368
+ const type = components.get(name);
369
+ if (type !== undefined) return type;
370
+ throw new Error(
371
+ `no component is registered as \`${name}\`. This host has ` +
372
+ `${[...components.keys()].map((k) => `\`${k}\``).join(', ')}. A script naming one that ` +
373
+ 'does not exist would otherwise read zeroes and write into nothing.',
374
+ );
375
+ };
376
+
377
+ /*
378
+ * The last search's result, per world, held **here rather than on the world**.
379
+ *
380
+ * A script cannot receive a record, so the result is read back through an accessor — and putting
381
+ * the state in the binding keeps `World` free of a field that exists only because of a language
382
+ * limitation. A `WeakMap` rather than one shared slot, because two worlds are two questions and a
383
+ * shared slot would let one answer the other's. The same arrangement `physicsImplementation` makes
384
+ * for a raycast hit, for the same reasons.
385
+ */
386
+ const found = new WeakMap<object, number>();
387
+
388
+ return {
389
+ create: (w: World) => w.create(),
390
+ destroy: (w: World, entity: Entity) => w.destroy(entity),
391
+ alive: (w: World, entity: Entity) => w.alive(entity),
392
+ has: (w: World, entity: Entity, component: string) => w.has(entity, resolve(component)),
393
+ attach: (w: World, entity: Entity, component: string) => w.add(entity, resolve(component)),
394
+ detach: (w: World, entity: Entity, component: string) => w.remove(entity, resolve(component)),
395
+ read: (w: World, entity: Entity, component: string, field: string) => {
396
+ const value = w.read(entity, resolve(component), field);
397
+ /* An entity without the component reads `undefined`, and a script's `f64` may not hold one:
398
+ the language has no null, so the honest zero is what a missing value is. */
399
+ return typeof value === 'number' ? value : 0;
400
+ },
401
+ write: (w: World, entity: Entity, component: string, field: string, value: number) =>
402
+ w.write(entity, resolve(component), field, value),
403
+ /*
404
+ * A query opens against whatever the caller passed as the world.
405
+ *
406
+ * **A `SystemView` and a `World` both satisfy this**, structurally, and that is the point: a
407
+ * loop inside a system runs against the view the schedule handed it, so every declaration
408
+ * check the engine makes still applies — and the same generated code runs against a bare world
409
+ * where there is no system to declare anything.
410
+ */
411
+ query: (w: QuerySource, a: string, b?: string, c?: string, d?: string) =>
412
+ w.query(
413
+ resolve(a),
414
+ b === undefined ? undefined : resolve(b),
415
+ c === undefined ? undefined : resolve(c),
416
+ d === undefined ? undefined : resolve(d),
417
+ ),
418
+ without: (cursor: QueryCursor, component: string) => {
419
+ cursor.without(resolve(component));
420
+ },
421
+ /*
422
+ * A negative ends the walk, and zero would not: zero is the handle of the first entity a fresh
423
+ * world ever allocates. Nothing is allocated to say "done" — a result record per step is the
424
+ * per-frame allocation the whole design refuses.
425
+ */
426
+ next: (cursor: QueryCursor) => {
427
+ const step = cursor.next();
428
+ return step.done ? -1 : (step.value as number);
429
+ },
430
+ view: (w: QuerySource, component: string, forWriting: boolean) =>
431
+ w.view(resolve(component), forWriting),
432
+ count: (w: World, component: string) => w.store(resolve(component)).size,
433
+ at: (w: World, component: string, index: number) => {
434
+ const store = w.store(resolve(component));
435
+ return index < store.size ? (store.dense[index] as number) : 0;
436
+ },
437
+ /*
438
+ * **A boolean rather than an option, and a second accessor for the handle.** `Sound?` is an
439
+ * option because a slot is resolved once at load; this is written to be called every frame by
440
+ * every agent that looks around, and `some(entity)` is one object per call — the same reasoning
441
+ * `next` gives one screen up. A sentinel is not available either: the result is an `Entity`, and
442
+ * a handle is not a number a script can compare to −1, which is the point of it having its own
443
+ * type. So the search answers whether it found anything and `nearest` reads what it found, the
444
+ * shape `raycast` and `hitBody` already have here.
445
+ *
446
+ * **Compared squared, so nothing takes a square root**, and the scan walks the dense columns
447
+ * directly: a store's dense position *is* its column index, so neither `sparse` nor a per-entity
448
+ * `read` is on this path. One pass, no allocation, no call per entity.
449
+ *
450
+ * A field this component does not have reads as absent rather than throwing, and an entity with
451
+ * no position contributes nothing — the honest answer for a component whose author named the
452
+ * wrong field is "nothing was near", reported by finding nothing, rather than a frame-loop
453
+ * throw that `AGENTS.md` forbids.
454
+ */
455
+ findNearest: (
456
+ w: QuerySource,
457
+ component: string,
458
+ fieldX: string,
459
+ fieldY: string,
460
+ fieldZ: string,
461
+ x: number,
462
+ y: number,
463
+ z: number,
464
+ radius: number,
465
+ ) => {
466
+ const type = resolve(component);
467
+ const view = w.view(type, false);
468
+ const columnX = view[fieldX];
469
+ const columnY = view[fieldY];
470
+ const columnZ = view[fieldZ];
471
+ if (columnX === undefined || columnY === undefined || columnZ === undefined) {
472
+ /* A component whose author named the wrong field answers "nothing was near" rather than
473
+ throwing: a frame loop may not throw, and the boolean already carries that answer. */
474
+ found.set(w, -1);
475
+ return false;
476
+ }
477
+
478
+ /*
479
+ * **A cursor rather than the store's dense array, because `world` is not always a `World`.**
480
+ *
481
+ * Inside a system the value a script calls this with is the `SystemView` the schedule handed
482
+ * it, which carries `query` and `view` and no `store` — the same structural pair `query` and
483
+ * `view` above are typed against. A first version read `store.size` and `store.dense`, which
484
+ * works from a bare world and throws `w.store is not a function` the moment a system calls it.
485
+ * Going through the cursor is also what keeps a system's declared reads doing their job.
486
+ *
487
+ * The walk always runs to exhaustion, which is what gives the cursor back: it comes from a
488
+ * pool and is returned when `next` reports done, so an early exit on finding something close
489
+ * enough would leak one per call.
490
+ */
491
+ const cursor = w.query(type);
492
+ const limit = radius * radius;
493
+ let best = -1;
494
+ let bestDistance = Infinity;
495
+ for (;;) {
496
+ const step = cursor.next();
497
+ if (step.done === true) break;
498
+ const entity = step.value as number;
499
+ const at = view.sparse[entity % ENTITY_INDEX_CEILING] as number;
500
+ const dx = (columnX[at] as number) - x;
501
+ const dy = (columnY[at] as number) - y;
502
+ const dz = (columnZ[at] as number) - z;
503
+ const distance = dx * dx + dy * dy + dz * dz;
504
+ if (distance > limit || distance >= bestDistance) continue;
505
+ bestDistance = distance;
506
+ best = entity;
507
+ }
508
+ found.set(w, best);
509
+ return best >= 0;
510
+ },
511
+ /*
512
+ * The handle the last search found, or zero when it found nothing.
513
+ *
514
+ * **Zero rather than a refusal**, because a frame loop may not throw and a script that read this
515
+ * without testing has a bug in its own logic rather than a broken host. Zero is a legal handle,
516
+ * so it is not a sentinel and is not documented as one — the honest test is the boolean, which
517
+ * is why the search answers one.
518
+ */
519
+ nearest: (w: QuerySource) => Math.max(0, found.get(w) ?? -1),
520
+ };
521
+ }
522
+
523
+ /**
524
+ * What a query needs from whatever it was handed as a world.
525
+ *
526
+ * **A `SystemView` and a `World` both satisfy it**, which is what lets one piece of generated code
527
+ * run inside a system and outside one. Inside, every call goes through the system's declarations;
528
+ * outside, there are none to go through. Neither is named here, because naming one would make this
529
+ * file choose.
530
+ */
531
+ interface QuerySource {
532
+ query(a: ComponentType, b?: ComponentType, c?: ComponentType, d?: ComponentType): QueryCursor;
533
+ view(type: ComponentType, forWriting: boolean): ComponentView;
534
+ }
535
+
536
+ export function prefabImplementation(services: EntityServices): Record<string, unknown> {
537
+ const prefabs = services.prefabs ?? new Map<string, Prefab>();
538
+ return {
539
+ spawn: (w: World, name: string) => {
540
+ const prefab = prefabs.get(name);
541
+ if (prefab === undefined) {
542
+ throw new Error(
543
+ `no prefab is registered as \`${name}\`. This host has ` +
544
+ `${[...prefabs.keys()].map((k) => `\`${k}\``).join(', ')}.`,
545
+ );
546
+ }
547
+ return instantiate(w, prefab);
548
+ },
549
+ };
550
+ }