@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,300 @@
1
+ import type { SpriteBatch, UiInput, UiNode } from '@driftengine/ui2d';
2
+ import {
3
+ createUiInput,
4
+ drawUiTree,
5
+ layoutUiTree,
6
+ routeUiKey,
7
+ routeUiPointer,
8
+ setUiFocus,
9
+ uiNodeNamed,
10
+ } from '@driftengine/ui2d';
11
+ import type { CapabilityDefinition, Effect, OpaqueType } from 'driftscript';
12
+ import { defineCapability } from 'driftscript';
13
+
14
+ export const INTERFACE_MODULE = 'drift/ui';
15
+
16
+ /**
17
+ * `drift/ui` — a retained interface tree, addressed by the names its nodes were given.
18
+ *
19
+ * **The linker has refused this module by name since the language shipped**, saying it waits on
20
+ * Track F. It does not any more.
21
+ *
22
+ * **`drift/2d` is the other half of this track**, and it declares `SpriteBatch` — which `draw`
23
+ * takes. It was uncallable when it was first written: a module's namespace was the last segment of
24
+ * its path, so a call would have been `2d.sprite(...)`, and `2d` is a number followed by an
25
+ * identifier. DriftScript 1.11.0 answered it at the import, and `sprites.ts` carries the story.
26
+ *
27
+ * ## Names rather than node values, and the reason is the type system
28
+ *
29
+ * Every capability here takes the tree and a name. The alternative — a `UiNode` opaque a script
30
+ * holds — needs a lookup that can fail, and the language has no optional to answer with: a
31
+ * `uiChild` that returned the root when a name was absent would be a silent wrong answer inside a
32
+ * frame loop. Names are total. `has` asks whether one exists, every reader answers a defined value
33
+ * for a name that does not, and a script reads the way it reads: `if ui.hovered(tree, "start")`.
34
+ *
35
+ * It also matches what a tree is for. A host builds the tree, gives its nodes names, and hands the
36
+ * root to a script through `uses` — the way a `Terrain` and a `NavGraph` arrive. The script does not
37
+ * construct interfaces; it drives one.
38
+ *
39
+ * ## The effects, and the line between two of them
40
+ *
41
+ * **A layout, a draw and a change to a node are `scene.write`** — the language's own effect for a
42
+ * thing that draws, and outside `DETERMINISTIC_EFFECTS` for the right reason: an interface is view
43
+ * state, and a `@deterministic` system has no business moving it.
44
+ *
45
+ * **Reading a laid-out box is `scene.read`** and is inside the boundary, because a rect is state
46
+ * the layout already produced.
47
+ *
48
+ * **But `hovered`, `pressed`, `focused` and `activated` are `input.read`**, and that is the one
49
+ * distinction worth stopping on. Those flags live on the tree and are read like any other field —
50
+ * so declaring them `scene.read` would type-check and would be a lie, because what they carry is
51
+ * where the pointer is. `input.read` is outside `DETERMINISTIC_EFFECTS`, so a `@deterministic`
52
+ * system cannot ask whether the mouse is over a button, which is exactly right and is the property
53
+ * the annotation exists to guarantee.
54
+ */
55
+ export const INTERFACE_TYPES: readonly OpaqueType[] = [
56
+ {
57
+ module: INTERFACE_MODULE,
58
+ name: 'UiTree',
59
+ doc: 'The root of a retained interface tree. Its nodes are addressed by the names they were built with.',
60
+ },
61
+ ];
62
+
63
+ const define = (
64
+ name: string,
65
+ params: readonly { name: string; type: string }[],
66
+ returns: string,
67
+ effects: readonly Effect[],
68
+ doc: string,
69
+ ): CapabilityDefinition =>
70
+ defineCapability({
71
+ module: INTERFACE_MODULE,
72
+ name,
73
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
74
+ params: [...params],
75
+ returns,
76
+ effects: [...effects],
77
+ deterministic: effects.every((effect) => effect === 'scene.read'),
78
+ doc,
79
+ implementation: `${INTERFACE_MODULE}.${name}`,
80
+ });
81
+
82
+ const NAMED = [
83
+ { name: 'tree', type: 'UiTree' },
84
+ { name: 'name', type: 'String' },
85
+ ] as const;
86
+
87
+ export const INTERFACE_CAPABILITIES: readonly CapabilityDefinition[] = [
88
+ define(
89
+ 'layout',
90
+ [
91
+ { name: 'tree', type: 'UiTree' },
92
+ { name: 'x', type: 'f32' },
93
+ { name: 'y', type: 'f32' },
94
+ { name: 'w', type: 'f32' },
95
+ { name: 'h', type: 'f32' },
96
+ ],
97
+ 'void',
98
+ ['scene.write'],
99
+ '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.',
100
+ ),
101
+ define(
102
+ 'draw',
103
+ [
104
+ { name: 'tree', type: 'UiTree' },
105
+ { name: 'batch', type: 'SpriteBatch' },
106
+ { name: 'white', type: 'i32' },
107
+ ],
108
+ 'i32',
109
+ ['scene.write'],
110
+ "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.",
111
+ ),
112
+ define(
113
+ 'has',
114
+ NAMED,
115
+ 'bool',
116
+ ['scene.read'],
117
+ '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.',
118
+ ),
119
+ define(
120
+ 'left',
121
+ NAMED,
122
+ 'f32',
123
+ ['scene.read'],
124
+ 'Where a node ended up, along x. Zero for a name the tree does not hold.',
125
+ ),
126
+ define('top', NAMED, 'f32', ['scene.read'], 'The same, along y.'),
127
+ define('width', NAMED, 'f32', ['scene.read'], 'How wide a node ended up.'),
128
+ define('height', NAMED, 'f32', ['scene.read'], 'How tall a node ended up.'),
129
+ define(
130
+ 'visible',
131
+ NAMED,
132
+ 'bool',
133
+ ['scene.read'],
134
+ 'Whether a node is shown. A hidden node is out of the layout and out of the hit test, not merely invisible.',
135
+ ),
136
+ define(
137
+ 'show',
138
+ [...NAMED, { name: 'visible', type: 'bool' }],
139
+ 'void',
140
+ ['scene.write'],
141
+ 'Show or hide a node and everything under it.',
142
+ ),
143
+ define(
144
+ 'setText',
145
+ [...NAMED, { name: 'text', type: 'String' }],
146
+ 'void',
147
+ ['scene.write'],
148
+ "Change what a node says. What draws it is the caller's: this package draws quads, and core already draws two kinds of text.",
149
+ ),
150
+ define(
151
+ 'tint',
152
+ [
153
+ ...NAMED,
154
+ { name: 'r', type: 'f32' },
155
+ { name: 'g', type: 'f32' },
156
+ { name: 'b', type: 'f32' },
157
+ { name: 'a', type: 'f32' },
158
+ ],
159
+ 'void',
160
+ ['scene.write'],
161
+ "Change a node's background colour. A node with no background gets one.",
162
+ ),
163
+ define('hovered', NAMED, 'bool', ['input.read'], 'Whether the pointer is over a node.'),
164
+ define(
165
+ 'pressed',
166
+ NAMED,
167
+ 'bool',
168
+ ['input.read'],
169
+ 'Whether the pointer went down on a node and has not come up.',
170
+ ),
171
+ define('focused', NAMED, 'bool', ['input.read'], 'Whether a node has the keyboard.'),
172
+ /*
173
+ * **`point` answers whether anything was activated and `activated` asks which**, rather than one
174
+ * call returning a name. The language has no optional to answer a miss with, and a two-call shape
175
+ * is what `drift/behavior`'s `doing` already uses for the same reason: ask by the name you have.
176
+ */
177
+ define(
178
+ 'point',
179
+ [
180
+ { name: 'tree', type: 'UiTree' },
181
+ { name: 'x', type: 'f32' },
182
+ { name: 'y', type: 'f32' },
183
+ { name: 'down', type: 'bool' },
184
+ ],
185
+ 'bool',
186
+ ['input.read', 'scene.write'],
187
+ '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.',
188
+ ),
189
+ define(
190
+ 'activated',
191
+ NAMED,
192
+ 'bool',
193
+ ['input.read'],
194
+ 'Whether the last `point` activated this node. Read it in the same step that called `point`; the next one clears it.',
195
+ ),
196
+ define(
197
+ 'key',
198
+ [
199
+ { name: 'tree', type: 'UiTree' },
200
+ { name: 'key', type: 'String' },
201
+ { name: 'shift', type: 'bool' },
202
+ ],
203
+ 'bool',
204
+ ['input.read', 'scene.write'],
205
+ '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.',
206
+ ),
207
+ define(
208
+ 'focus',
209
+ NAMED,
210
+ 'void',
211
+ ['scene.write'],
212
+ 'Give a node the keyboard. A name the tree does not hold focuses nothing.',
213
+ ),
214
+ ];
215
+
216
+ /**
217
+ * **The router's state is kept here, per tree, which is the shape `drift/navigation` set.**
218
+ *
219
+ * A `UiInput` is hover, press and focus between one call and the next, and a script has nowhere to
220
+ * put one — the same position a script routing an agent was in before `navigation.path` started
221
+ * keeping a route per agent. So the host keeps one per tree, in a map keyed weakly so a tree going
222
+ * away takes its router with it.
223
+ *
224
+ * **No services**, for the reason `drift/terrain` gives: every capability takes the tree it acts on,
225
+ * so this is registered unconditionally and a script that never receives one never calls it.
226
+ */
227
+ export function interfaceImplementation(): Record<string, unknown> {
228
+ const routers = new WeakMap<UiNode, UiInput>();
229
+ /* What the last `point` or `key` activated, so `activated` can be asked by name. */
230
+ const activations = new WeakMap<UiNode, UiNode | null>();
231
+
232
+ const routerOf = (tree: UiNode): UiInput => {
233
+ let input = routers.get(tree);
234
+ if (input === undefined) {
235
+ input = createUiInput();
236
+ routers.set(tree, input);
237
+ }
238
+ return input;
239
+ };
240
+
241
+ const nodeOf = (tree: UiNode, name: string): UiNode | null => uiNodeNamed(tree, name);
242
+
243
+ return {
244
+ layout: (tree: UiNode, x: number, y: number, w: number, h: number) => {
245
+ layoutUiTree(tree, x, y, w, h);
246
+ },
247
+ draw: (tree: UiNode, batch: SpriteBatch, white: number) => drawUiTree(batch, tree, white, null),
248
+ has: (tree: UiNode, name: string) => nodeOf(tree, name) !== null,
249
+ left: (tree: UiNode, name: string) => nodeOf(tree, name)?.rect.x ?? 0,
250
+ top: (tree: UiNode, name: string) => nodeOf(tree, name)?.rect.y ?? 0,
251
+ width: (tree: UiNode, name: string) => nodeOf(tree, name)?.rect.w ?? 0,
252
+ height: (tree: UiNode, name: string) => nodeOf(tree, name)?.rect.h ?? 0,
253
+ visible: (tree: UiNode, name: string) => {
254
+ const node = nodeOf(tree, name);
255
+ return node !== null && !node.hidden;
256
+ },
257
+ show: (tree: UiNode, name: string, visible: boolean) => {
258
+ const node = nodeOf(tree, name);
259
+ if (node !== null) node.hidden = !visible;
260
+ },
261
+ setText: (tree: UiNode, name: string, text: string) => {
262
+ const node = nodeOf(tree, name);
263
+ if (node !== null) node.text = text;
264
+ },
265
+ tint: (tree: UiNode, name: string, r: number, g: number, b: number, a: number) => {
266
+ const node = nodeOf(tree, name);
267
+ if (node === null) return;
268
+ /* A node with no background gets one, which is the only allocation in this module and
269
+ happens once per node rather than per frame. */
270
+ if (node.background === null) node.background = new Float32Array(4);
271
+ node.background[0] = r;
272
+ node.background[1] = g;
273
+ node.background[2] = b;
274
+ node.background[3] = a;
275
+ },
276
+ hovered: (tree: UiNode, name: string) => nodeOf(tree, name)?.hovered === true,
277
+ pressed: (tree: UiNode, name: string) => nodeOf(tree, name)?.pressed === true,
278
+ focused: (tree: UiNode, name: string) => {
279
+ const node = nodeOf(tree, name);
280
+ return node !== null && routerOf(tree).focused === node;
281
+ },
282
+ point: (tree: UiNode, x: number, y: number, down: boolean) => {
283
+ const hit = routeUiPointer(routerOf(tree), tree, x, y, down);
284
+ activations.set(tree, hit);
285
+ return hit !== null;
286
+ },
287
+ activated: (tree: UiNode, name: string) => {
288
+ const node = nodeOf(tree, name);
289
+ return node !== null && activations.get(tree) === node;
290
+ },
291
+ key: (tree: UiNode, key: string, shift: boolean) => {
292
+ const hit = routeUiKey(routerOf(tree), tree, key, shift);
293
+ activations.set(tree, hit);
294
+ return hit !== null;
295
+ },
296
+ focus: (tree: UiNode, name: string) => {
297
+ setUiFocus(routerOf(tree), nodeOf(tree, name));
298
+ },
299
+ };
300
+ }
@@ -0,0 +1,367 @@
1
+ import { NavPath, NavSearch, createNavSteer, nearestNavNode } from '@driftengine/core';
2
+ import type { NavGraph, NavSteer } from '@driftengine/core';
3
+ import { type Entity, entityGeneration, entityIndex } from '@driftengine/entities';
4
+ import type { CapabilityDefinition, OpaqueType } from 'driftscript';
5
+ import { defineCapability } from 'driftscript';
6
+
7
+ export const NAVIGATION_MODULE = 'drift/navigation';
8
+
9
+ /**
10
+ * `drift/navigation` — a graph, a route over it, and a point to steer at.
11
+ *
12
+ * **The linker has refused this module by name since the language shipped**, and the reason given
13
+ * was that nothing here paths. That was true and it was never a blocker: it was the row. A consumer
14
+ * reported what its absence costs them — twelve agents that walk straight at whatever they are
15
+ * going to and wedge against a building on the way, about once every two minutes of play — and
16
+ * noted that they already extract a road graph from their world, which is the half of the work that
17
+ * is usually the awkward half.
18
+ *
19
+ * **A graph and not a mesh**, and the header of `navGraph.ts` argues that where the decision is:
20
+ * these consumers arrive with a network already, and open ground crossed by two nodes is crossed in
21
+ * a straight line.
22
+ *
23
+ * **`navigation.read` is a deterministic effect**, which the language decided before this existed
24
+ * and this has to keep true: a route is a function of the graph and its two endpoints and of
25
+ * nothing else, ties included. `navSearch.ts` is where that is held. A `@deterministic` system may
26
+ * therefore path, which is the point — deciding where to walk is exactly the kind of thing that
27
+ * belongs inside the fixed step.
28
+ */
29
+ export const NAVIGATION_TYPES: readonly OpaqueType[] = [
30
+ {
31
+ module: NAVIGATION_MODULE,
32
+ name: 'NavGraph',
33
+ doc: 'A network of places and the ways between them.',
34
+ },
35
+ {
36
+ module: NAVIGATION_MODULE,
37
+ name: 'NavPath',
38
+ doc: 'A route being walked, and how far along it the walker is. Ask it where to steer.',
39
+ },
40
+ ];
41
+
42
+ const define = (
43
+ name: string,
44
+ params: readonly { name: string; type: string }[],
45
+ returns: string,
46
+ effects: readonly string[],
47
+ doc: string,
48
+ deterministic = true,
49
+ allocates = false,
50
+ ): CapabilityDefinition =>
51
+ defineCapability({
52
+ module: NAVIGATION_MODULE,
53
+ name,
54
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
55
+ params: [...params],
56
+ returns,
57
+ ...(allocates ? { allocates: true } : {}),
58
+ effects: effects as CapabilityDefinition['effects'],
59
+ /*
60
+ * **Writing a route is deterministic too, as of DriftScript 1.12.0**, and this paragraph used
61
+ * to say the opposite. `navigation.read` was in `DETERMINISTIC_EFFECTS` and `navigation.write`
62
+ * was not, so a `@deterministic` system could steer along a route and could not compute one —
63
+ * an agent re-pathing inside the fixed step, because a bridge dropped or because it was pushed
64
+ * off the network, had to path from an ordinary system and hand the result in.
65
+ *
66
+ * **It reversed on exactly the argument this comment named**, which is the whole reason the
67
+ * condition was written down rather than the limitation merely stated: a route is a function of
68
+ * the graph and two endpoints, `navSearch.ts` breaks ties on the node index so two runs agree,
69
+ * and the `NavPath` written into is simulation state rather than a host's. The language's own
70
+ * deferral said the track that builds an effect is the one that can answer for its replay
71
+ * behaviour; this track shipped, so it answered.
72
+ *
73
+ * Every capability here is deterministic now, so the parameter keeps its default and the three
74
+ * writers stopped passing `false`.
75
+ */
76
+ deterministic,
77
+ doc,
78
+ implementation: `${NAVIGATION_MODULE}.${name}`,
79
+ });
80
+
81
+ export const NAVIGATION_CAPABILITIES: readonly CapabilityDefinition[] = [
82
+ define(
83
+ 'nearest',
84
+ [
85
+ { name: 'graph', type: 'NavGraph' },
86
+ { name: 'x', type: 'float' },
87
+ { name: 'y', type: 'float' },
88
+ { name: 'z', type: 'float' },
89
+ ],
90
+ 'i32',
91
+ ['navigation.read'],
92
+ 'The node nearest a place, or -1 where the graph has none. How an agent gets onto the network.',
93
+ ),
94
+ define(
95
+ 'nearestWithin',
96
+ [
97
+ { name: 'graph', type: 'NavGraph' },
98
+ { name: 'x', type: 'float' },
99
+ { name: 'y', type: 'float' },
100
+ { name: 'z', type: 'float' },
101
+ { name: 'maxDistance', type: 'float' },
102
+ ],
103
+ 'i32',
104
+ ['navigation.read'],
105
+ 'The nearest node within a distance, or -1. Use it so an agent that has walked off the network is not snapped to the far side of the map.',
106
+ ),
107
+ /*
108
+ * **`route` takes places and not node indices**, which is the whole difference between a binding
109
+ * a script author can use and one they have to learn a data model for first. Getting onto a graph
110
+ * is `nearest` twice, and a script that had to write that would write it identically every time
111
+ * and get the out-of-reach case wrong once.
112
+ */
113
+ define(
114
+ 'route',
115
+ [
116
+ { name: 'path', type: 'NavPath' },
117
+ { name: 'graph', type: 'NavGraph' },
118
+ { name: 'fromX', type: 'float' },
119
+ { name: 'fromY', type: 'float' },
120
+ { name: 'fromZ', type: 'float' },
121
+ { name: 'toX', type: 'float' },
122
+ { name: 'toY', type: 'float' },
123
+ { name: 'toZ', type: 'float' },
124
+ ],
125
+ 'bool',
126
+ ['navigation.write'],
127
+ 'Path from one place to another, storing the route. False where there is no way through, which is an answer and not a failure.',
128
+ ),
129
+ define(
130
+ 'routeBetween',
131
+ [
132
+ { name: 'path', type: 'NavPath' },
133
+ { name: 'graph', type: 'NavGraph' },
134
+ { name: 'from', type: 'i32' },
135
+ { name: 'to', type: 'i32' },
136
+ ],
137
+ 'bool',
138
+ ['navigation.write'],
139
+ 'The same, between two nodes you already have. For a consumer with their own spatial index.',
140
+ ),
141
+ /*
142
+ * **A route per agent, because a script had nowhere to keep one.**
143
+ *
144
+ * Every capability above takes a `NavPath` and nothing here made one, so a consumer kept a path
145
+ * per agent in TypeScript and called a script function once per agent per step — which is the
146
+ * report DriftScript 1.8.0's `uses` clause answers half of. `uses` hands a system the *graph*, one
147
+ * per world; a route is one per walker, and a resource is one per type, so the other half has to
148
+ * arrive as a value a script can ask for by entity. This is it, and with it the loop over agents
149
+ * goes back into the `system` where it can be reloaded.
150
+ *
151
+ * **Kept per entity *index*, with the generation beside it.** A map keyed by the handle would grow
152
+ * for the life of the world — every agent that ever pathed, long after it was destroyed, with no
153
+ * moment at which anything could know to drop one. An index is bounded by the world's ceiling, and
154
+ * a reused index arriving with a different generation is a different agent, so the route is
155
+ * cleared rather than inherited. That is the failure worth naming: without the check a freshly
156
+ * spawned walker would set off along the route of whatever died in its slot.
157
+ *
158
+ * **It allocates the first time it is asked about an agent** and never again, which `allocates`
159
+ * says so a `@hot` function is told. There is no way around it — a route is a buffer — and the
160
+ * alternative, making a script pass its own, is the thing this exists to remove.
161
+ */
162
+ define(
163
+ 'path',
164
+ [{ name: 'agent', type: 'Entity' }],
165
+ 'NavPath',
166
+ ['navigation.read'],
167
+ "The route this agent is following, kept by the host between steps. Empty until something routes it, and empty again if the agent's handle is reused.",
168
+ true,
169
+ true,
170
+ ),
171
+ define(
172
+ 'clear',
173
+ [{ name: 'path', type: 'NavPath' }],
174
+ 'void',
175
+ ['navigation.write'],
176
+ 'Forget the route. The walker is then arrived, wherever it is.',
177
+ ),
178
+ define(
179
+ 'following',
180
+ [{ name: 'path', type: 'NavPath' }],
181
+ 'bool',
182
+ ['navigation.read'],
183
+ 'Whether there is a route to follow.',
184
+ ),
185
+ /*
186
+ * **The steer is read back through four accessors**, which is the shape `input.axisX` and
187
+ * `animation.hitX` already have and the reason is the same: the language has no way to return a
188
+ * record by value into a caller's own storage, and an opaque handle for a three-float answer
189
+ * would be a lifetime a script author has to think about to read a direction.
190
+ *
191
+ * The implementation answers all four from one computation — see `navigationImplementation`,
192
+ * where the reason it can is that the arguments say what the answer depends on.
193
+ */
194
+ define(
195
+ 'steerX',
196
+ [
197
+ { name: 'path', type: 'NavPath' },
198
+ { name: 'x', type: 'float' },
199
+ { name: 'y', type: 'float' },
200
+ { name: 'z', type: 'float' },
201
+ ],
202
+ 'float',
203
+ ['navigation.read'],
204
+ 'Where a walker at this place should aim, along x. A point ahead on the path rather than the next node, so corners are cut smoothly instead of snapped at.',
205
+ ),
206
+ define(
207
+ 'steerY',
208
+ [
209
+ { name: 'path', type: 'NavPath' },
210
+ { name: 'x', type: 'float' },
211
+ { name: 'y', type: 'float' },
212
+ { name: 'z', type: 'float' },
213
+ ],
214
+ 'float',
215
+ ['navigation.read'],
216
+ 'The same aim point, along y.',
217
+ ),
218
+ define(
219
+ 'steerZ',
220
+ [
221
+ { name: 'path', type: 'NavPath' },
222
+ { name: 'x', type: 'float' },
223
+ { name: 'y', type: 'float' },
224
+ { name: 'z', type: 'float' },
225
+ ],
226
+ 'float',
227
+ ['navigation.read'],
228
+ 'The same aim point, along z.',
229
+ ),
230
+ define(
231
+ 'remaining',
232
+ [
233
+ { name: 'path', type: 'NavPath' },
234
+ { name: 'x', type: 'float' },
235
+ { name: 'y', type: 'float' },
236
+ { name: 'z', type: 'float' },
237
+ ],
238
+ 'float',
239
+ ['navigation.read'],
240
+ 'How far is left, following the path rather than as the crow flies.',
241
+ ),
242
+ define(
243
+ 'arrived',
244
+ [
245
+ { name: 'path', type: 'NavPath' },
246
+ { name: 'x', type: 'float' },
247
+ { name: 'y', type: 'float' },
248
+ { name: 'z', type: 'float' },
249
+ ],
250
+ 'bool',
251
+ ['navigation.read'],
252
+ 'Whether the walker is there. True with no route at all, so one check covers both.',
253
+ ),
254
+ ];
255
+
256
+ /** What a consumer hands the host so scripts can path over their world. */
257
+ export interface NavigationServices {
258
+ /** The network. One graph; a consumer with several composes them into one before handing it over. */
259
+ readonly graph: NavGraph;
260
+ /** A search over that graph, reused across every agent. */
261
+ readonly search: NavSearch;
262
+ /** How many nodes a route may have. Longer routes are refused rather than truncated. */
263
+ readonly maxRouteNodes?: number;
264
+ }
265
+
266
+ export function navigationImplementation(services: NavigationServices): Record<string, unknown> {
267
+ const buffer = new Uint32Array(services.maxRouteNodes ?? 256);
268
+ const steer: NavSteer = createNavSteer();
269
+ /*
270
+ * The last place a steer was computed for, so `steerX`, `steerY`, `steerZ`, `remaining` and
271
+ * `arrived` at one position cost one walk of the path rather than five. Correct rather than
272
+ * merely fast: the answer is a function of the path and the place, and the path cannot change
273
+ * between two reads inside a script's own statement.
274
+ */
275
+ let cachedPath: NavPath | null = null;
276
+ let cachedX = NaN;
277
+ let cachedY = NaN;
278
+ let cachedZ = NaN;
279
+
280
+ /*
281
+ * One route per agent, by entity **index**, with the generation beside it.
282
+ *
283
+ * Two sparse arrays rather than a `Map` keyed by the handle: a map grows for the life of the
284
+ * world, one entry per agent that ever pathed, and nothing is in a position to know when to drop
285
+ * one. An index is bounded by the world's ceiling. The generation is what makes a reused index
286
+ * safe — a slot arriving with a different one is a different agent, and its route is cleared
287
+ * rather than inherited, which would otherwise send a freshly spawned walker off along the route
288
+ * of whatever died in its place.
289
+ */
290
+ const routes: (NavPath | undefined)[] = [];
291
+ const owners: number[] = [];
292
+
293
+ const at = (path: NavPath, x: number, y: number, z: number): NavSteer => {
294
+ if (path !== cachedPath || x !== cachedX || y !== cachedY || z !== cachedZ) {
295
+ path.steer(x, y, z, steer);
296
+ cachedPath = path;
297
+ cachedX = x;
298
+ cachedY = y;
299
+ cachedZ = z;
300
+ }
301
+ return steer;
302
+ };
303
+ /** Any write to a path invalidates the cache, since the answer depends on the route. */
304
+ const invalidate = (): void => {
305
+ cachedPath = null;
306
+ };
307
+
308
+ return {
309
+ path(agent: Entity): NavPath {
310
+ const index = entityIndex(agent);
311
+ const generation = entityGeneration(agent);
312
+ let route = routes[index];
313
+ if (route === undefined) {
314
+ route = new NavPath(services.graph, services.maxRouteNodes ?? 256);
315
+ routes[index] = route;
316
+ } else if (owners[index] !== generation) {
317
+ /* The slot was somebody else's. Clearing rather than replacing keeps the allocation this
318
+ agent is about to need and leaves nothing of the last one's route. */
319
+ route.clear();
320
+ invalidate();
321
+ }
322
+ owners[index] = generation;
323
+ return route;
324
+ },
325
+ nearest: (graph: NavGraph, x: number, y: number, z: number) => nearestNavNode(graph, x, y, z),
326
+ nearestWithin: (graph: NavGraph, x: number, y: number, z: number, maxDistance: number) =>
327
+ nearestNavNode(graph, x, y, z, maxDistance),
328
+ route(
329
+ path: NavPath,
330
+ graph: NavGraph,
331
+ fromX: number,
332
+ fromY: number,
333
+ fromZ: number,
334
+ toX: number,
335
+ toY: number,
336
+ toZ: number,
337
+ ): boolean {
338
+ invalidate();
339
+ const from = nearestNavNode(graph, fromX, fromY, fromZ);
340
+ const to = nearestNavNode(graph, toX, toY, toZ);
341
+ if (from < 0 || to < 0) {
342
+ path.clear();
343
+ return false;
344
+ }
345
+ const count = services.search.find(from, to, buffer);
346
+ path.set(buffer, count);
347
+ return count > 1;
348
+ },
349
+ routeBetween(path: NavPath, graph: NavGraph, from: number, to: number): boolean {
350
+ invalidate();
351
+ void graph;
352
+ const count = services.search.find(from, to, buffer);
353
+ path.set(buffer, count);
354
+ return count > 1;
355
+ },
356
+ clear(path: NavPath): void {
357
+ invalidate();
358
+ path.clear();
359
+ },
360
+ following: (path: NavPath) => path.active,
361
+ steerX: (path: NavPath, x: number, y: number, z: number) => at(path, x, y, z).x,
362
+ steerY: (path: NavPath, x: number, y: number, z: number) => at(path, x, y, z).y,
363
+ steerZ: (path: NavPath, x: number, y: number, z: number) => at(path, x, y, z).z,
364
+ remaining: (path: NavPath, x: number, y: number, z: number) => at(path, x, y, z).remainingM,
365
+ arrived: (path: NavPath, x: number, y: number, z: number) => at(path, x, y, z).arrived,
366
+ };
367
+ }