@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,339 @@
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 { ENTITY_INDEX_CEILING, instantiate, } from '@driftengine/entities';
27
+ import { defineCapability } from 'driftscript';
28
+ export const ECS_MODULE = 'drift/ecs';
29
+ export const PREFAB_MODULE = 'drift/prefab';
30
+ const define = (module, name, params, returns, effects, deterministic, doc) => defineCapability({
31
+ module,
32
+ name,
33
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
34
+ params: [...params],
35
+ returns,
36
+ effects,
37
+ deterministic,
38
+ doc,
39
+ implementation: `${module}.${name}`,
40
+ });
41
+ export const ENTITY_TYPES = [
42
+ {
43
+ module: ECS_MODULE,
44
+ name: 'World',
45
+ doc: 'A world: its entities, their components and its systems.',
46
+ },
47
+ {
48
+ module: ECS_MODULE,
49
+ name: 'Cursor',
50
+ 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.",
51
+ },
52
+ {
53
+ module: ECS_MODULE,
54
+ name: 'View',
55
+ 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.",
56
+ },
57
+ ];
58
+ /**
59
+ * An entity is `Entity`, and it took the language until DriftScript 1.6.0 to have the word.
60
+ *
61
+ * A handle carries a 26-bit index and a 27-bit generation, which is the entire 53-bit budget a
62
+ * double holds. A `u32` would truncate the generation — and a truncated generation is a stale
63
+ * handle that compares equal to a live one, which is the exact failure the generation exists to
64
+ * prevent. So the width was never negotiable, and until the language could name it these all said
65
+ * `f64` with this paragraph explaining why.
66
+ *
67
+ * **`Entity` is a type of its own there rather than an alias**, which is what makes the change worth
68
+ * making: a handle is assignable *to* an `f64` and not *from* one, so a number a script computed
69
+ * cannot arrive where a handle is wanted, and `e.Health` is legal on a handle and on nothing else.
70
+ * The language's own note asked for this by name — a capability that hands back a handle as an
71
+ * `f64` and expects it to keep working as one is what it said would make the asymmetry wrong.
72
+ *
73
+ * **`read` still answers `f64`**, and that is not an oversight: it reads a component *field*, which
74
+ * is a number, and typing it as a handle would be the same mistake in the other direction.
75
+ */
76
+ export const ECS_CAPABILITIES = [
77
+ define(ECS_MODULE, 'create', [{ name: 'world', type: 'World' }], 'Entity', ['ecs.write'], false, 'A fresh entity with nothing on it. The handle is exact; do not put one in an `f32`.'),
78
+ define(ECS_MODULE, 'destroy', [
79
+ { name: 'world', type: 'World' },
80
+ { name: 'entity', type: 'Entity' },
81
+ ], 'bool', ['ecs.write'], false, 'Remove an entity and every component it had. False when the handle was already stale.'),
82
+ define(ECS_MODULE, 'alive', [
83
+ { name: 'world', type: 'World' },
84
+ { name: 'entity', type: 'Entity' },
85
+ ], 'bool', ['ecs.read'], true, 'Whether this handle still names a live entity. A handle to something destroyed is false, even after its slot is reused.'),
86
+ define(ECS_MODULE, 'has', [
87
+ { name: 'world', type: 'World' },
88
+ { name: 'entity', type: 'Entity' },
89
+ { name: 'component', type: 'String' },
90
+ ], 'bool', ['ecs.read'], true, 'Whether an entity carries a component.'),
91
+ define(ECS_MODULE, 'attach', [
92
+ { name: 'world', type: 'World' },
93
+ { name: 'entity', type: 'Entity' },
94
+ { name: 'component', type: 'String' },
95
+ ], 'void', ['ecs.write'], false, 'Give an entity a component, every field at the zero for its type. Set them with `write`.'),
96
+ define(ECS_MODULE, 'detach', [
97
+ { name: 'world', type: 'World' },
98
+ { name: 'entity', type: 'Entity' },
99
+ { name: 'component', type: 'String' },
100
+ ], 'bool', ['ecs.write'], false, 'Take a component away. False when it did not have one.'),
101
+ define(ECS_MODULE, 'read', [
102
+ { name: 'world', type: 'World' },
103
+ { name: 'entity', type: 'Entity' },
104
+ { name: 'component', type: 'String' },
105
+ { name: 'field', type: 'String' },
106
+ ], 'f64', ['ecs.read'], true, 'One numeric field. Reading a field of a component an entity does not have is 0.'),
107
+ define(ECS_MODULE, 'write', [
108
+ { name: 'world', type: 'World' },
109
+ { name: 'entity', type: 'Entity' },
110
+ { name: 'component', type: 'String' },
111
+ { name: 'field', type: 'String' },
112
+ { name: 'value', type: 'f64' },
113
+ ], 'void', ['ecs.write'], false, 'Set one numeric field.'),
114
+ define(ECS_MODULE, 'count', [
115
+ { name: 'world', type: 'World' },
116
+ { name: 'component', type: 'String' },
117
+ ], 'u32', ['ecs.read'], true, 'How many entities carry a component.'),
118
+ define(ECS_MODULE, 'at', [
119
+ { name: 'world', type: 'World' },
120
+ { name: 'component', type: 'String' },
121
+ { name: 'index', type: 'u32' },
122
+ ], 'Entity', ['ecs.read'], true, '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.'),
123
+ /*
124
+ * **Two capabilities rather than one, and the split is what `Entity` cost.**
125
+ *
126
+ * A search that finds nothing has to say so, and once the answer is an `Entity` there is no value
127
+ * left to say it with: a handle is not a number a script can compare to −1, which is the whole
128
+ * point of it being its own type. An option would say it and allocates one object per call, on a
129
+ * path written to run every frame for every agent that looks around — the allocation `next`
130
+ * refuses one screen up.
131
+ *
132
+ * So the result is read back through a second accessor, which is exactly what `raycast` and its
133
+ * `hitBody` already do in this repository and for the same reason. **The cost is the same too**:
134
+ * the result is only valid until the next search on the same world, and that is said here and on
135
+ * the accessor rather than left for somebody to find out.
136
+ */
137
+ define(ECS_MODULE, 'findNearest', [
138
+ { name: 'world', type: 'World' },
139
+ { name: 'component', type: 'String' },
140
+ { name: 'fieldX', type: 'String' },
141
+ { name: 'fieldY', type: 'String' },
142
+ { name: 'fieldZ', type: 'String' },
143
+ { name: 'x', type: 'float' },
144
+ { name: 'y', type: 'float' },
145
+ { name: 'z', type: 'float' },
146
+ { name: 'radius', type: 'float' },
147
+ ], 'bool', ['ecs.read'], true, "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."),
148
+ define(ECS_MODULE, 'nearest', [{ name: 'world', type: 'World' }], 'Entity', ['ecs.read'], true, '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.'),
149
+ ];
150
+ /**
151
+ * The four a query loop is built on.
152
+ *
153
+ * **Nobody writes these by hand.** They are what `for e in query<…>() { … }` compiles to, and a
154
+ * script author never names one — which is why their docs describe the shape of the generated code
155
+ * rather than how to call them. They are capabilities rather than a second bind hook so that
156
+ * generated code reaches them exactly as it reaches everything else, through `__bind($host)`.
157
+ */
158
+ export const QUERY_CAPABILITIES = [
159
+ define(ECS_MODULE, 'query', [
160
+ { name: 'world', type: 'World' },
161
+ { name: 'a', type: 'String' },
162
+ { name: 'b', type: 'String' },
163
+ { name: 'c', type: 'String' },
164
+ { name: 'd', type: 'String' },
165
+ ], 'Cursor', ['ecs.read'], true, 'Open a walk over everything carrying these components. The cursor is borrowed from the world’s pool; ending the walk gives it back.'),
166
+ define(ECS_MODULE, 'without', [
167
+ { name: 'cursor', type: 'Cursor' },
168
+ { name: 'component', type: 'String' },
169
+ ], 'void', ['ecs.read'], true, 'Narrow an open walk to entities that do **not** carry a component.'),
170
+ define(ECS_MODULE, 'next', [{ name: 'cursor', type: 'Cursor' }], 'Entity', ['ecs.read'], true, '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.'),
171
+ define(ECS_MODULE, 'view', [
172
+ { name: 'world', type: 'World' },
173
+ { name: 'component', type: 'String' },
174
+ { name: 'forWriting', type: 'bool' },
175
+ ], 'View', ['ecs.read'], true, '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.'),
176
+ ];
177
+ export const PREFAB_CAPABILITIES = [
178
+ define(PREFAB_MODULE, 'spawn', [
179
+ { name: 'world', type: 'World' },
180
+ { name: 'prefab', type: 'String' },
181
+ ], 'Entity', ['ecs.write'], false, '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.'),
182
+ ];
183
+ export function entitiesImplementation(services) {
184
+ const { components } = services;
185
+ /**
186
+ * Resolve a component name, or refuse naming what the world has.
187
+ *
188
+ * **Not `undefined` and not a no-op.** A script that misspells a component would otherwise read
189
+ * zeroes and write into nothing, which is a behaviour that runs, reports success and is wrong —
190
+ * the silent no-op this repository forbids, arriving through a string.
191
+ */
192
+ const resolve = (name) => {
193
+ const type = components.get(name);
194
+ if (type !== undefined)
195
+ return type;
196
+ throw new Error(`no component is registered as \`${name}\`. This host has ` +
197
+ `${[...components.keys()].map((k) => `\`${k}\``).join(', ')}. A script naming one that ` +
198
+ 'does not exist would otherwise read zeroes and write into nothing.');
199
+ };
200
+ /*
201
+ * The last search's result, per world, held **here rather than on the world**.
202
+ *
203
+ * A script cannot receive a record, so the result is read back through an accessor — and putting
204
+ * the state in the binding keeps `World` free of a field that exists only because of a language
205
+ * limitation. A `WeakMap` rather than one shared slot, because two worlds are two questions and a
206
+ * shared slot would let one answer the other's. The same arrangement `physicsImplementation` makes
207
+ * for a raycast hit, for the same reasons.
208
+ */
209
+ const found = new WeakMap();
210
+ return {
211
+ create: (w) => w.create(),
212
+ destroy: (w, entity) => w.destroy(entity),
213
+ alive: (w, entity) => w.alive(entity),
214
+ has: (w, entity, component) => w.has(entity, resolve(component)),
215
+ attach: (w, entity, component) => w.add(entity, resolve(component)),
216
+ detach: (w, entity, component) => w.remove(entity, resolve(component)),
217
+ read: (w, entity, component, field) => {
218
+ const value = w.read(entity, resolve(component), field);
219
+ /* An entity without the component reads `undefined`, and a script's `f64` may not hold one:
220
+ the language has no null, so the honest zero is what a missing value is. */
221
+ return typeof value === 'number' ? value : 0;
222
+ },
223
+ write: (w, entity, component, field, value) => w.write(entity, resolve(component), field, value),
224
+ /*
225
+ * A query opens against whatever the caller passed as the world.
226
+ *
227
+ * **A `SystemView` and a `World` both satisfy this**, structurally, and that is the point: a
228
+ * loop inside a system runs against the view the schedule handed it, so every declaration
229
+ * check the engine makes still applies — and the same generated code runs against a bare world
230
+ * where there is no system to declare anything.
231
+ */
232
+ query: (w, a, b, c, d) => w.query(resolve(a), b === undefined ? undefined : resolve(b), c === undefined ? undefined : resolve(c), d === undefined ? undefined : resolve(d)),
233
+ without: (cursor, component) => {
234
+ cursor.without(resolve(component));
235
+ },
236
+ /*
237
+ * A negative ends the walk, and zero would not: zero is the handle of the first entity a fresh
238
+ * world ever allocates. Nothing is allocated to say "done" — a result record per step is the
239
+ * per-frame allocation the whole design refuses.
240
+ */
241
+ next: (cursor) => {
242
+ const step = cursor.next();
243
+ return step.done ? -1 : step.value;
244
+ },
245
+ view: (w, component, forWriting) => w.view(resolve(component), forWriting),
246
+ count: (w, component) => w.store(resolve(component)).size,
247
+ at: (w, component, index) => {
248
+ const store = w.store(resolve(component));
249
+ return index < store.size ? store.dense[index] : 0;
250
+ },
251
+ /*
252
+ * **A boolean rather than an option, and a second accessor for the handle.** `Sound?` is an
253
+ * option because a slot is resolved once at load; this is written to be called every frame by
254
+ * every agent that looks around, and `some(entity)` is one object per call — the same reasoning
255
+ * `next` gives one screen up. A sentinel is not available either: the result is an `Entity`, and
256
+ * a handle is not a number a script can compare to −1, which is the point of it having its own
257
+ * type. So the search answers whether it found anything and `nearest` reads what it found, the
258
+ * shape `raycast` and `hitBody` already have here.
259
+ *
260
+ * **Compared squared, so nothing takes a square root**, and the scan walks the dense columns
261
+ * directly: a store's dense position *is* its column index, so neither `sparse` nor a per-entity
262
+ * `read` is on this path. One pass, no allocation, no call per entity.
263
+ *
264
+ * A field this component does not have reads as absent rather than throwing, and an entity with
265
+ * no position contributes nothing — the honest answer for a component whose author named the
266
+ * wrong field is "nothing was near", reported by finding nothing, rather than a frame-loop
267
+ * throw that `AGENTS.md` forbids.
268
+ */
269
+ findNearest: (w, component, fieldX, fieldY, fieldZ, x, y, z, radius) => {
270
+ const type = resolve(component);
271
+ const view = w.view(type, false);
272
+ const columnX = view[fieldX];
273
+ const columnY = view[fieldY];
274
+ const columnZ = view[fieldZ];
275
+ if (columnX === undefined || columnY === undefined || columnZ === undefined) {
276
+ /* A component whose author named the wrong field answers "nothing was near" rather than
277
+ throwing: a frame loop may not throw, and the boolean already carries that answer. */
278
+ found.set(w, -1);
279
+ return false;
280
+ }
281
+ /*
282
+ * **A cursor rather than the store's dense array, because `world` is not always a `World`.**
283
+ *
284
+ * Inside a system the value a script calls this with is the `SystemView` the schedule handed
285
+ * it, which carries `query` and `view` and no `store` — the same structural pair `query` and
286
+ * `view` above are typed against. A first version read `store.size` and `store.dense`, which
287
+ * works from a bare world and throws `w.store is not a function` the moment a system calls it.
288
+ * Going through the cursor is also what keeps a system's declared reads doing their job.
289
+ *
290
+ * The walk always runs to exhaustion, which is what gives the cursor back: it comes from a
291
+ * pool and is returned when `next` reports done, so an early exit on finding something close
292
+ * enough would leak one per call.
293
+ */
294
+ const cursor = w.query(type);
295
+ const limit = radius * radius;
296
+ let best = -1;
297
+ let bestDistance = Infinity;
298
+ for (;;) {
299
+ const step = cursor.next();
300
+ if (step.done === true)
301
+ break;
302
+ const entity = step.value;
303
+ const at = view.sparse[entity % ENTITY_INDEX_CEILING];
304
+ const dx = columnX[at] - x;
305
+ const dy = columnY[at] - y;
306
+ const dz = columnZ[at] - z;
307
+ const distance = dx * dx + dy * dy + dz * dz;
308
+ if (distance > limit || distance >= bestDistance)
309
+ continue;
310
+ bestDistance = distance;
311
+ best = entity;
312
+ }
313
+ found.set(w, best);
314
+ return best >= 0;
315
+ },
316
+ /*
317
+ * The handle the last search found, or zero when it found nothing.
318
+ *
319
+ * **Zero rather than a refusal**, because a frame loop may not throw and a script that read this
320
+ * without testing has a bug in its own logic rather than a broken host. Zero is a legal handle,
321
+ * so it is not a sentinel and is not documented as one — the honest test is the boolean, which
322
+ * is why the search answers one.
323
+ */
324
+ nearest: (w) => Math.max(0, found.get(w) ?? -1),
325
+ };
326
+ }
327
+ export function prefabImplementation(services) {
328
+ const prefabs = services.prefabs ?? new Map();
329
+ return {
330
+ spawn: (w, name) => {
331
+ const prefab = prefabs.get(name);
332
+ if (prefab === undefined) {
333
+ throw new Error(`no prefab is registered as \`${name}\`. This host has ` +
334
+ `${[...prefabs.keys()].map((k) => `\`${k}\``).join(', ')}.`);
335
+ }
336
+ return instantiate(w, prefab);
337
+ },
338
+ };
339
+ }
@@ -0,0 +1,55 @@
1
+ import type { CapabilityDefinition, OpaqueType } from 'driftscript';
2
+ export declare const INTERFACE_MODULE = "drift/ui";
3
+ /**
4
+ * `drift/ui` — a retained interface tree, addressed by the names its nodes were given.
5
+ *
6
+ * **The linker has refused this module by name since the language shipped**, saying it waits on
7
+ * Track F. It does not any more.
8
+ *
9
+ * **`drift/2d` is the other half of this track**, and it declares `SpriteBatch` — which `draw`
10
+ * takes. It was uncallable when it was first written: a module's namespace was the last segment of
11
+ * its path, so a call would have been `2d.sprite(...)`, and `2d` is a number followed by an
12
+ * identifier. DriftScript 1.11.0 answered it at the import, and `sprites.ts` carries the story.
13
+ *
14
+ * ## Names rather than node values, and the reason is the type system
15
+ *
16
+ * Every capability here takes the tree and a name. The alternative — a `UiNode` opaque a script
17
+ * holds — needs a lookup that can fail, and the language has no optional to answer with: a
18
+ * `uiChild` that returned the root when a name was absent would be a silent wrong answer inside a
19
+ * frame loop. Names are total. `has` asks whether one exists, every reader answers a defined value
20
+ * for a name that does not, and a script reads the way it reads: `if ui.hovered(tree, "start")`.
21
+ *
22
+ * It also matches what a tree is for. A host builds the tree, gives its nodes names, and hands the
23
+ * root to a script through `uses` — the way a `Terrain` and a `NavGraph` arrive. The script does not
24
+ * construct interfaces; it drives one.
25
+ *
26
+ * ## The effects, and the line between two of them
27
+ *
28
+ * **A layout, a draw and a change to a node are `scene.write`** — the language's own effect for a
29
+ * thing that draws, and outside `DETERMINISTIC_EFFECTS` for the right reason: an interface is view
30
+ * state, and a `@deterministic` system has no business moving it.
31
+ *
32
+ * **Reading a laid-out box is `scene.read`** and is inside the boundary, because a rect is state
33
+ * the layout already produced.
34
+ *
35
+ * **But `hovered`, `pressed`, `focused` and `activated` are `input.read`**, and that is the one
36
+ * distinction worth stopping on. Those flags live on the tree and are read like any other field —
37
+ * so declaring them `scene.read` would type-check and would be a lie, because what they carry is
38
+ * where the pointer is. `input.read` is outside `DETERMINISTIC_EFFECTS`, so a `@deterministic`
39
+ * system cannot ask whether the mouse is over a button, which is exactly right and is the property
40
+ * the annotation exists to guarantee.
41
+ */
42
+ export declare const INTERFACE_TYPES: readonly OpaqueType[];
43
+ export declare const INTERFACE_CAPABILITIES: readonly CapabilityDefinition[];
44
+ /**
45
+ * **The router's state is kept here, per tree, which is the shape `drift/navigation` set.**
46
+ *
47
+ * A `UiInput` is hover, press and focus between one call and the next, and a script has nowhere to
48
+ * put one — the same position a script routing an agent was in before `navigation.path` started
49
+ * keeping a route per agent. So the host keeps one per tree, in a map keyed weakly so a tree going
50
+ * away takes its router with it.
51
+ *
52
+ * **No services**, for the reason `drift/terrain` gives: every capability takes the tree it acts on,
53
+ * so this is registered unconditionally and a script that never receives one never calls it.
54
+ */
55
+ export declare function interfaceImplementation(): Record<string, unknown>;
@@ -0,0 +1,200 @@
1
+ import { createUiInput, drawUiTree, layoutUiTree, routeUiKey, routeUiPointer, setUiFocus, uiNodeNamed, } from '@driftengine/ui2d';
2
+ import { defineCapability } from 'driftscript';
3
+ export const INTERFACE_MODULE = 'drift/ui';
4
+ /**
5
+ * `drift/ui` — a retained interface tree, addressed by the names its nodes were given.
6
+ *
7
+ * **The linker has refused this module by name since the language shipped**, saying it waits on
8
+ * Track F. It does not any more.
9
+ *
10
+ * **`drift/2d` is the other half of this track**, and it declares `SpriteBatch` — which `draw`
11
+ * takes. It was uncallable when it was first written: a module's namespace was the last segment of
12
+ * its path, so a call would have been `2d.sprite(...)`, and `2d` is a number followed by an
13
+ * identifier. DriftScript 1.11.0 answered it at the import, and `sprites.ts` carries the story.
14
+ *
15
+ * ## Names rather than node values, and the reason is the type system
16
+ *
17
+ * Every capability here takes the tree and a name. The alternative — a `UiNode` opaque a script
18
+ * holds — needs a lookup that can fail, and the language has no optional to answer with: a
19
+ * `uiChild` that returned the root when a name was absent would be a silent wrong answer inside a
20
+ * frame loop. Names are total. `has` asks whether one exists, every reader answers a defined value
21
+ * for a name that does not, and a script reads the way it reads: `if ui.hovered(tree, "start")`.
22
+ *
23
+ * It also matches what a tree is for. A host builds the tree, gives its nodes names, and hands the
24
+ * root to a script through `uses` — the way a `Terrain` and a `NavGraph` arrive. The script does not
25
+ * construct interfaces; it drives one.
26
+ *
27
+ * ## The effects, and the line between two of them
28
+ *
29
+ * **A layout, a draw and a change to a node are `scene.write`** — the language's own effect for a
30
+ * thing that draws, and outside `DETERMINISTIC_EFFECTS` for the right reason: an interface is view
31
+ * state, and a `@deterministic` system has no business moving it.
32
+ *
33
+ * **Reading a laid-out box is `scene.read`** and is inside the boundary, because a rect is state
34
+ * the layout already produced.
35
+ *
36
+ * **But `hovered`, `pressed`, `focused` and `activated` are `input.read`**, and that is the one
37
+ * distinction worth stopping on. Those flags live on the tree and are read like any other field —
38
+ * so declaring them `scene.read` would type-check and would be a lie, because what they carry is
39
+ * where the pointer is. `input.read` is outside `DETERMINISTIC_EFFECTS`, so a `@deterministic`
40
+ * system cannot ask whether the mouse is over a button, which is exactly right and is the property
41
+ * the annotation exists to guarantee.
42
+ */
43
+ export const INTERFACE_TYPES = [
44
+ {
45
+ module: INTERFACE_MODULE,
46
+ name: 'UiTree',
47
+ doc: 'The root of a retained interface tree. Its nodes are addressed by the names they were built with.',
48
+ },
49
+ ];
50
+ const define = (name, params, returns, effects, doc) => defineCapability({
51
+ module: INTERFACE_MODULE,
52
+ name,
53
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
54
+ params: [...params],
55
+ returns,
56
+ effects: [...effects],
57
+ deterministic: effects.every((effect) => effect === 'scene.read'),
58
+ doc,
59
+ implementation: `${INTERFACE_MODULE}.${name}`,
60
+ });
61
+ const NAMED = [
62
+ { name: 'tree', type: 'UiTree' },
63
+ { name: 'name', type: 'String' },
64
+ ];
65
+ export const INTERFACE_CAPABILITIES = [
66
+ define('layout', [
67
+ { name: 'tree', type: 'UiTree' },
68
+ { name: 'x', type: 'f32' },
69
+ { name: 'y', type: 'f32' },
70
+ { name: 'w', type: 'f32' },
71
+ { name: 'h', type: 'f32' },
72
+ ], 'void', ['scene.write'], 'Lay the tree out into a box. The box is what is available: a tree sized by its contents comes to what its contents come to.'),
73
+ define('draw', [
74
+ { name: 'tree', type: 'UiTree' },
75
+ { name: 'batch', type: 'SpriteBatch' },
76
+ { name: 'white', type: 'i32' },
77
+ ], 'i32', ['scene.write'], "Put the tree in a batch and answer how many quads that was. `white` is the sprite pass's own white slot, which is what a background is drawn on."),
78
+ define('has', NAMED, 'bool', ['scene.read'], 'Whether the tree holds a node of this name. Every reader below answers a defined value without it, so this is for a script that wants to know rather than one that would crash.'),
79
+ define('left', NAMED, 'f32', ['scene.read'], 'Where a node ended up, along x. Zero for a name the tree does not hold.'),
80
+ define('top', NAMED, 'f32', ['scene.read'], 'The same, along y.'),
81
+ define('width', NAMED, 'f32', ['scene.read'], 'How wide a node ended up.'),
82
+ define('height', NAMED, 'f32', ['scene.read'], 'How tall a node ended up.'),
83
+ define('visible', NAMED, 'bool', ['scene.read'], 'Whether a node is shown. A hidden node is out of the layout and out of the hit test, not merely invisible.'),
84
+ define('show', [...NAMED, { name: 'visible', type: 'bool' }], 'void', ['scene.write'], 'Show or hide a node and everything under it.'),
85
+ define('setText', [...NAMED, { name: 'text', type: 'String' }], 'void', ['scene.write'], "Change what a node says. What draws it is the caller's: this package draws quads, and core already draws two kinds of text."),
86
+ define('tint', [
87
+ ...NAMED,
88
+ { name: 'r', type: 'f32' },
89
+ { name: 'g', type: 'f32' },
90
+ { name: 'b', type: 'f32' },
91
+ { name: 'a', type: 'f32' },
92
+ ], 'void', ['scene.write'], "Change a node's background colour. A node with no background gets one."),
93
+ define('hovered', NAMED, 'bool', ['input.read'], 'Whether the pointer is over a node.'),
94
+ define('pressed', NAMED, 'bool', ['input.read'], 'Whether the pointer went down on a node and has not come up.'),
95
+ define('focused', NAMED, 'bool', ['input.read'], 'Whether a node has the keyboard.'),
96
+ /*
97
+ * **`point` answers whether anything was activated and `activated` asks which**, rather than one
98
+ * call returning a name. The language has no optional to answer a miss with, and a two-call shape
99
+ * is what `drift/behavior`'s `doing` already uses for the same reason: ask by the name you have.
100
+ */
101
+ define('point', [
102
+ { name: 'tree', type: 'UiTree' },
103
+ { name: 'x', type: 'f32' },
104
+ { name: 'y', type: 'f32' },
105
+ { name: 'down', type: 'bool' },
106
+ ], 'bool', ['input.read', 'scene.write'], 'Route the pointer, and answer whether this call activated anything. An activation is a press and a release on the same node, so somebody who pressed the wrong button can slide off it.'),
107
+ define('activated', NAMED, 'bool', ['input.read'], 'Whether the last `point` activated this node. Read it in the same step that called `point`; the next one clears it.'),
108
+ define('key', [
109
+ { name: 'tree', type: 'UiTree' },
110
+ { name: 'key', type: 'String' },
111
+ { name: 'shift', type: 'bool' },
112
+ ], 'bool', ['input.read', 'scene.write'], 'Route a key, and answer whether it activated what has focus. Tab moves focus, Enter and space activate, and everything else is reported unhandled so the caller can have it.'),
113
+ define('focus', NAMED, 'void', ['scene.write'], 'Give a node the keyboard. A name the tree does not hold focuses nothing.'),
114
+ ];
115
+ /**
116
+ * **The router's state is kept here, per tree, which is the shape `drift/navigation` set.**
117
+ *
118
+ * A `UiInput` is hover, press and focus between one call and the next, and a script has nowhere to
119
+ * put one — the same position a script routing an agent was in before `navigation.path` started
120
+ * keeping a route per agent. So the host keeps one per tree, in a map keyed weakly so a tree going
121
+ * away takes its router with it.
122
+ *
123
+ * **No services**, for the reason `drift/terrain` gives: every capability takes the tree it acts on,
124
+ * so this is registered unconditionally and a script that never receives one never calls it.
125
+ */
126
+ export function interfaceImplementation() {
127
+ const routers = new WeakMap();
128
+ /* What the last `point` or `key` activated, so `activated` can be asked by name. */
129
+ const activations = new WeakMap();
130
+ const routerOf = (tree) => {
131
+ let input = routers.get(tree);
132
+ if (input === undefined) {
133
+ input = createUiInput();
134
+ routers.set(tree, input);
135
+ }
136
+ return input;
137
+ };
138
+ const nodeOf = (tree, name) => uiNodeNamed(tree, name);
139
+ return {
140
+ layout: (tree, x, y, w, h) => {
141
+ layoutUiTree(tree, x, y, w, h);
142
+ },
143
+ draw: (tree, batch, white) => drawUiTree(batch, tree, white, null),
144
+ has: (tree, name) => nodeOf(tree, name) !== null,
145
+ left: (tree, name) => nodeOf(tree, name)?.rect.x ?? 0,
146
+ top: (tree, name) => nodeOf(tree, name)?.rect.y ?? 0,
147
+ width: (tree, name) => nodeOf(tree, name)?.rect.w ?? 0,
148
+ height: (tree, name) => nodeOf(tree, name)?.rect.h ?? 0,
149
+ visible: (tree, name) => {
150
+ const node = nodeOf(tree, name);
151
+ return node !== null && !node.hidden;
152
+ },
153
+ show: (tree, name, visible) => {
154
+ const node = nodeOf(tree, name);
155
+ if (node !== null)
156
+ node.hidden = !visible;
157
+ },
158
+ setText: (tree, name, text) => {
159
+ const node = nodeOf(tree, name);
160
+ if (node !== null)
161
+ node.text = text;
162
+ },
163
+ tint: (tree, name, r, g, b, a) => {
164
+ const node = nodeOf(tree, name);
165
+ if (node === null)
166
+ return;
167
+ /* A node with no background gets one, which is the only allocation in this module and
168
+ happens once per node rather than per frame. */
169
+ if (node.background === null)
170
+ node.background = new Float32Array(4);
171
+ node.background[0] = r;
172
+ node.background[1] = g;
173
+ node.background[2] = b;
174
+ node.background[3] = a;
175
+ },
176
+ hovered: (tree, name) => nodeOf(tree, name)?.hovered === true,
177
+ pressed: (tree, name) => nodeOf(tree, name)?.pressed === true,
178
+ focused: (tree, name) => {
179
+ const node = nodeOf(tree, name);
180
+ return node !== null && routerOf(tree).focused === node;
181
+ },
182
+ point: (tree, x, y, down) => {
183
+ const hit = routeUiPointer(routerOf(tree), tree, x, y, down);
184
+ activations.set(tree, hit);
185
+ return hit !== null;
186
+ },
187
+ activated: (tree, name) => {
188
+ const node = nodeOf(tree, name);
189
+ return node !== null && activations.get(tree) === node;
190
+ },
191
+ key: (tree, key, shift) => {
192
+ const hit = routeUiKey(routerOf(tree), tree, key, shift);
193
+ activations.set(tree, hit);
194
+ return hit !== null;
195
+ },
196
+ focus: (tree, name) => {
197
+ setUiFocus(routerOf(tree), nodeOf(tree, name));
198
+ },
199
+ };
200
+ }
@@ -0,0 +1,36 @@
1
+ import { NavSearch } from '@driftengine/core';
2
+ import type { NavGraph } from '@driftengine/core';
3
+ import type { CapabilityDefinition, OpaqueType } from 'driftscript';
4
+ export declare const NAVIGATION_MODULE = "drift/navigation";
5
+ /**
6
+ * `drift/navigation` — a graph, a route over it, and a point to steer at.
7
+ *
8
+ * **The linker has refused this module by name since the language shipped**, and the reason given
9
+ * was that nothing here paths. That was true and it was never a blocker: it was the row. A consumer
10
+ * reported what its absence costs them — twelve agents that walk straight at whatever they are
11
+ * going to and wedge against a building on the way, about once every two minutes of play — and
12
+ * noted that they already extract a road graph from their world, which is the half of the work that
13
+ * is usually the awkward half.
14
+ *
15
+ * **A graph and not a mesh**, and the header of `navGraph.ts` argues that where the decision is:
16
+ * these consumers arrive with a network already, and open ground crossed by two nodes is crossed in
17
+ * a straight line.
18
+ *
19
+ * **`navigation.read` is a deterministic effect**, which the language decided before this existed
20
+ * and this has to keep true: a route is a function of the graph and its two endpoints and of
21
+ * nothing else, ties included. `navSearch.ts` is where that is held. A `@deterministic` system may
22
+ * therefore path, which is the point — deciding where to walk is exactly the kind of thing that
23
+ * belongs inside the fixed step.
24
+ */
25
+ export declare const NAVIGATION_TYPES: readonly OpaqueType[];
26
+ export declare const NAVIGATION_CAPABILITIES: readonly CapabilityDefinition[];
27
+ /** What a consumer hands the host so scripts can path over their world. */
28
+ export interface NavigationServices {
29
+ /** The network. One graph; a consumer with several composes them into one before handing it over. */
30
+ readonly graph: NavGraph;
31
+ /** A search over that graph, reused across every agent. */
32
+ readonly search: NavSearch;
33
+ /** How many nodes a route may have. Longer routes are refused rather than truncated. */
34
+ readonly maxRouteNodes?: number;
35
+ }
36
+ export declare function navigationImplementation(services: NavigationServices): Record<string, unknown>;