@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.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +81 -0
- package/dist/bindings/ai.d.ts +66 -0
- package/dist/bindings/ai.js +173 -0
- package/dist/bindings/animation.d.ts +13 -0
- package/dist/bindings/animation.js +177 -0
- package/dist/bindings/audio.d.ts +56 -0
- package/dist/bindings/audio.js +126 -0
- package/dist/bindings/behavior.d.ts +36 -0
- package/dist/bindings/behavior.js +101 -0
- package/dist/bindings/chemistry.d.ts +72 -0
- package/dist/bindings/chemistry.js +253 -0
- package/dist/bindings/core.d.ts +172 -0
- package/dist/bindings/core.js +814 -0
- package/dist/bindings/editor.d.ts +53 -0
- package/dist/bindings/editor.js +248 -0
- package/dist/bindings/entities.d.ts +73 -0
- package/dist/bindings/entities.js +339 -0
- package/dist/bindings/interface.d.ts +55 -0
- package/dist/bindings/interface.js +200 -0
- package/dist/bindings/navigation.d.ts +36 -0
- package/dist/bindings/navigation.js +255 -0
- package/dist/bindings/network.d.ts +51 -0
- package/dist/bindings/network.js +96 -0
- package/dist/bindings/render.d.ts +71 -0
- package/dist/bindings/render.js +143 -0
- package/dist/bindings/rollback.d.ts +41 -0
- package/dist/bindings/rollback.js +74 -0
- package/dist/bindings/sprites.d.ts +53 -0
- package/dist/bindings/sprites.js +186 -0
- package/dist/bindings/terrain.d.ts +54 -0
- package/dist/bindings/terrain.js +138 -0
- package/dist/bindings/xr.d.ts +56 -0
- package/dist/bindings/xr.js +155 -0
- package/dist/entityHost.d.ts +80 -0
- package/dist/entityHost.js +159 -0
- package/dist/host.d.ts +153 -0
- package/dist/host.js +233 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +18 -0
- package/package.json +68 -0
- package/src/bindings/ai.ts +306 -0
- package/src/bindings/animation.ts +346 -0
- package/src/bindings/audio.ts +185 -0
- package/src/bindings/behavior.ts +181 -0
- package/src/bindings/chemistry.ts +548 -0
- package/src/bindings/core.ts +1604 -0
- package/src/bindings/editor.ts +393 -0
- package/src/bindings/entities.ts +550 -0
- package/src/bindings/interface.ts +300 -0
- package/src/bindings/navigation.ts +367 -0
- package/src/bindings/network.ts +178 -0
- package/src/bindings/render.ts +191 -0
- package/src/bindings/rollback.ts +110 -0
- package/src/bindings/sprites.ts +310 -0
- package/src/bindings/terrain.ts +180 -0
- package/src/bindings/xr.ts +272 -0
- package/src/entityHost.ts +219 -0
- package/src/host.ts +465 -0
- 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
|
+
}
|