@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,393 @@
1
+ import type { Gizmo, SceneNode } from '@driftengine/core';
2
+ import type { EditorHost } from '@driftengine/editor';
3
+ import type { CapabilityDefinition, OpaqueType } from 'driftscript';
4
+ import { defineCapability } from 'driftscript';
5
+
6
+ export const EDITOR_MODULE = 'drift/editor';
7
+
8
+ /**
9
+ * `drift/editor` — the transform tool, driven from a script that never ships.
10
+ *
11
+ * `Gizmo` in `@driftengine/core` answers what a ray is over and what a drag does to a transform;
12
+ * this is the surface a tool script drives it through.
13
+ *
14
+ * **`editor` is an `Effect` outside `DETERMINISTIC_EFFECTS`, and belongs there.** A gizmo is a view
15
+ * of a transform being edited by a person, so a `@deterministic` system has no business touching
16
+ * one. The annotation refusing it is the property `@editor` exists to pair with.
17
+ *
18
+ * **A name is unique across the module, not per type.** Two opaque types make that easy to forget:
19
+ * a module is one namespace and one implementation map, so `mode` cannot mean the gizmo's mode to a
20
+ * `Gizmo` and the session's to an `Editor`. They are `gizmoMode` and `mode`.
21
+ *
22
+ * **Two opaque types.** `Gizmo` is the transform tool; `Editor` is `@driftengine/editor`'s
23
+ * `EditorHost` — the selection, the tree and the play state. Both reach a script through `uses`,
24
+ * the way a `Terrain` and a `NavGraph` do, so this module needs nothing from the host and is
25
+ * registered unconditionally.
26
+ *
27
+ * ## Nothing here picks, and nothing edits a field by name
28
+ *
29
+ * A pick needs a ray, a ray needs a camera and a pointer, and both belong to the application. The
30
+ * host runs `hover`, `beginDrag` and `updateDrag`; a script reads what they found and says what
31
+ * kind of tool it is. That is what an editor script contains: a key that switches to rotation, a
32
+ * modifier that switches to local space, and a read of where the thing ended up.
33
+ *
34
+ * The same argument one level along: an inspector row is addressed by its index in a list the host
35
+ * built this frame, and a script wanting to write `Health.current` on a selection would be doing
36
+ * entity work `drift/ecs` already does properly, with a component handle and a checked field.
37
+ *
38
+ * ## Three capabilities where an argument would have done, deliberately
39
+ *
40
+ * `translateMode`, `rotateMode` and `scaleMode` rather than `setMode(gizmo, mode)`. The language has
41
+ * no enum, so the argument would be a string or a number, and an unrecognised one leaves exactly two
42
+ * options — change nothing, the silent no-op `host.ts` rules out, or throw inside a frame, which
43
+ * `AGENTS.md` rules out. Three names make the invalid call impossible to write, and the checker
44
+ * rather than a runtime branch is what refuses it. `space` is the same argument again.
45
+ *
46
+ * The *readers* are strings because a reader is total: there is no invalid value to answer.
47
+ */
48
+ export const EDITOR_TYPES: readonly OpaqueType[] = [
49
+ {
50
+ module: EDITOR_MODULE,
51
+ name: 'Gizmo',
52
+ doc: 'A transform tool: what the pointer is over, and the transform a drag is editing.',
53
+ },
54
+ {
55
+ module: EDITOR_MODULE,
56
+ name: 'Editor',
57
+ doc: 'An editing session: what is selected, the tree of what exists, and whether the world is playing.',
58
+ },
59
+ ];
60
+
61
+ const define = (
62
+ name: string,
63
+ params: readonly { name: string; type: string }[],
64
+ returns: string,
65
+ doc: string,
66
+ ): CapabilityDefinition =>
67
+ defineCapability({
68
+ module: EDITOR_MODULE,
69
+ name,
70
+ signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
71
+ params: [...params],
72
+ returns,
73
+ effects: ['editor'],
74
+ /*
75
+ * **False on every one of them, reads included.** `editor` is outside
76
+ * `DETERMINISTIC_EFFECTS`, and it should be: what a gizmo holds is a person's pointer part way
77
+ * through moving something, so a system that read it would produce a different simulation on
78
+ * replay. This is the effect `@editor` was designed to pair with.
79
+ */
80
+ deterministic: false,
81
+ doc,
82
+ implementation: `${EDITOR_MODULE}.${name}`,
83
+ });
84
+
85
+ const OF = [{ name: 'gizmo', type: 'Gizmo' }] as const;
86
+ const ON = [{ name: 'editor', type: 'Editor' }] as const;
87
+
88
+ /*
89
+ * **`f32` rather than `float`, and the language is right to insist.** `float` is width-polymorphic
90
+ * and takes its width from a `float` parameter; none of these has one, so nothing would fix it.
91
+ * A transform is stored as `Float32Array`, so `f32` is what the number actually is rather than a
92
+ * width chosen to satisfy a checker. `bindings/terrain.ts` records the same finding.
93
+ */
94
+ export const EDITOR_CAPABILITIES: readonly CapabilityDefinition[] = [
95
+ define('translateMode', OF, 'void', 'Make this an arrow gizmo, which moves what it is on.'),
96
+ define('rotateMode', OF, 'void', 'Make this a ring gizmo, which turns what it is on.'),
97
+ define(
98
+ 'scaleMode',
99
+ OF,
100
+ 'void',
101
+ 'Make this a scale gizmo. Scale is always in the object’s own axes, whatever the space says, because a non-uniform scale along a world axis would shear it.',
102
+ ),
103
+ define(
104
+ 'gizmoMode',
105
+ OF,
106
+ 'String',
107
+ 'Which of the three the gizmo is: "translate", "rotate" or "scale".',
108
+ ),
109
+
110
+ define('worldSpace', OF, 'void', 'Point the handles along the world axes.'),
111
+ define('localSpace', OF, 'void', 'Point the handles along the object’s own axes.'),
112
+ define('space', OF, 'String', 'Which the handles are pointing along: "world" or "local".'),
113
+
114
+ define(
115
+ 'size',
116
+ OF,
117
+ 'f32',
118
+ 'How big the gizmo is in metres. The host keeps this the same on screen as the camera moves.',
119
+ ),
120
+ define(
121
+ 'setSize',
122
+ [...OF, { name: 'metres', type: 'f32' }],
123
+ 'void',
124
+ 'Set how big the gizmo is in metres. Non-positive draws nothing and picks nothing rather than failing.',
125
+ ),
126
+
127
+ /*
128
+ * **A name rather than a number, and total.** The handles are integer constants in TypeScript, and
129
+ * a script comparing against a magic number is a script that breaks silently the day a handle is
130
+ * added in the middle. `"none"` is a real answer rather than a missing one.
131
+ */
132
+ define(
133
+ 'hovered',
134
+ OF,
135
+ 'String',
136
+ 'What the pointer is over, as a name: "none", or one of "translate.x", "translate.yz", "rotate.y", "scale.uniform" and their siblings.',
137
+ ),
138
+ define('dragging', OF, 'bool', 'Whether a drag is running.'),
139
+ define(
140
+ 'dragAngle',
141
+ OF,
142
+ 'f32',
143
+ 'How far a rotation drag has turned, in radians, signed and past a full turn if it went that far. Zero when the drag is not a rotation. This is what to snap or clamp; the rotation itself cannot tell a three-quarter turn from a quarter turn back.',
144
+ ),
145
+
146
+ define('positionX', OF, 'f32', 'Where the thing being edited is, along x.'),
147
+ define('positionY', OF, 'f32', 'The same, along y.'),
148
+ define('positionZ', OF, 'f32', 'The same, along z.'),
149
+ define(
150
+ 'setPosition',
151
+ [...OF, { name: 'x', type: 'f32' }, { name: 'y', type: 'f32' }, { name: 'z', type: 'f32' }],
152
+ 'void',
153
+ 'Put the gizmo, and the transform it is editing, somewhere. A drag that is running is not interrupted, so do this between drags.',
154
+ ),
155
+
156
+ define('scaleX', OF, 'f32', 'How much the thing being edited is scaled, along x.'),
157
+ define('scaleY', OF, 'f32', 'The same, along y.'),
158
+ define('scaleZ', OF, 'f32', 'The same, along z.'),
159
+
160
+ /*
161
+ * **Four reads of a quaternion rather than three Euler angles.** Euler angles would be friendlier
162
+ * to read and would be a different rotation depending on the order they are recomposed in, which
163
+ * is a wrong answer a script author cannot see. `drift/physics` reports a body's orientation the
164
+ * same way, so a script moving one onto the other passes four numbers through unchanged.
165
+ */
166
+ define('rotationX', OF, 'f32', 'The orientation being edited, as a quaternion: x.'),
167
+ define('rotationY', OF, 'f32', 'The same: y.'),
168
+ define('rotationZ', OF, 'f32', 'The same: z.'),
169
+ define('rotationW', OF, 'f32', 'The same: w. Identity is (0, 0, 0, 1).'),
170
+
171
+ /*
172
+ * **The session, from `@driftengine/editor`.** Everything above drives one tool; everything below
173
+ * drives the editor around it — which is what an `@editor` script is actually for.
174
+ */
175
+ define('mode', ON, 'String', 'What the editor is doing: "edit", "play" or "paused".'),
176
+ define(
177
+ 'playing',
178
+ ON,
179
+ 'bool',
180
+ 'Whether the world is advancing. False while paused and while editing.',
181
+ ),
182
+ define(
183
+ 'play',
184
+ ON,
185
+ 'bool',
186
+ 'Start playing, taking a snapshot of the world first so `stop` can put it back. Resuming from paused does not re-snapshot. False when no world is bound to snapshot.',
187
+ ),
188
+ define('pause', ON, 'void', 'Hold the world where it is. The frame keeps drawing.'),
189
+ define(
190
+ 'step',
191
+ ON,
192
+ 'void',
193
+ 'Advance exactly one fixed tick, then hold. Does nothing while editing, where there is nothing to step.',
194
+ ),
195
+ define(
196
+ 'stop',
197
+ ON,
198
+ 'bool',
199
+ 'Leave play and put the world back as it was. Every entity handle changes, so a selection is re-found by its place in the snapshot and one that play created is dropped. False when there was nothing to put back.',
200
+ ),
201
+
202
+ define('rowCount', ON, 'i32', 'How many rows the tree showed at its last rebuild.'),
203
+ define(
204
+ 'rebuildTree',
205
+ [...ON, { name: 'root', type: 'Node' }],
206
+ 'void',
207
+ 'Walk a node and everything under it into rows, honouring which branches are collapsed.',
208
+ ),
209
+ define(
210
+ 'rowName',
211
+ [...ON, { name: 'row', type: 'i32' }],
212
+ 'String',
213
+ 'What the node on a row is called, or "" for a row that does not exist.',
214
+ ),
215
+ define(
216
+ 'rowDepth',
217
+ [...ON, { name: 'row', type: 'i32' }],
218
+ 'i32',
219
+ 'How deep a row sits, with the root at zero. Minus one for a row that does not exist.',
220
+ ),
221
+ define(
222
+ 'rowExpanded',
223
+ [...ON, { name: 'row', type: 'i32' }],
224
+ 'bool',
225
+ 'Whether a row is showing its children. Meaningless for a row with none.',
226
+ ),
227
+ define(
228
+ 'toggleRow',
229
+ [...ON, { name: 'row', type: 'i32' }],
230
+ 'void',
231
+ 'Open a closed row or close an open one. Call `rebuildTree` afterwards to see it.',
232
+ ),
233
+ define(
234
+ 'selectRow',
235
+ [...ON, { name: 'row', type: 'i32' }],
236
+ 'void',
237
+ 'Select the node on a row, which points the gizmo at it. A row that does not exist selects nothing.',
238
+ ),
239
+ define('selectedRow', ON, 'i32', 'Which row is selected, or minus one.'),
240
+
241
+ define('fieldCount', ON, 'i32', 'How many fields the inspector is showing.'),
242
+ define(
243
+ 'fieldLabel',
244
+ [...ON, { name: 'field', type: 'i32' }],
245
+ 'String',
246
+ 'A field\u2019s name as it was declared, or "" for a field that does not exist.',
247
+ ),
248
+ define(
249
+ 'fieldGroup',
250
+ [...ON, { name: 'field', type: 'i32' }],
251
+ 'String',
252
+ 'What a field belongs to: a component\u2019s name, or "transform" for a node.',
253
+ ),
254
+ define(
255
+ 'fieldKind',
256
+ [...ON, { name: 'field', type: 'i32' }],
257
+ 'String',
258
+ 'How to show a field: "number", "integer", "boolean", "text", "entity" or "enum". Taken from the declared type, because a read cannot tell them apart.',
259
+ ),
260
+ define(
261
+ 'fieldNumber',
262
+ [...ON, { name: 'field', type: 'i32' }],
263
+ 'f32',
264
+ 'A field\u2019s value as a number. Zero for a field that is not one, and for one that does not exist.',
265
+ ),
266
+ define(
267
+ 'setFieldNumber',
268
+ [...ON, { name: 'field', type: 'i32' }, { name: 'value', type: 'f32' }],
269
+ 'bool',
270
+ 'Write a number into a field. False where the field does not exist or will not take one.',
271
+ ),
272
+ ];
273
+
274
+ /**
275
+ * Handle constants as the names a script reads, in the order the constants run.
276
+ *
277
+ * A table rather than a `switch`, so adding a handle is one line here and the reader stays total.
278
+ */
279
+ const HANDLE_NAMES: readonly string[] = [
280
+ 'none',
281
+ 'translate.x',
282
+ 'translate.y',
283
+ 'translate.z',
284
+ 'translate.yz',
285
+ 'translate.zx',
286
+ 'translate.xy',
287
+ 'rotate.x',
288
+ 'rotate.y',
289
+ 'rotate.z',
290
+ 'scale.x',
291
+ 'scale.y',
292
+ 'scale.z',
293
+ 'scale.uniform',
294
+ ];
295
+
296
+ /**
297
+ * **No services, and that is the shape rather than an omission.**
298
+ *
299
+ * Every capability here takes the gizmo it is asking about, so the module needs nothing from the
300
+ * host to answer — a `Gizmo` reaches a script the way a `Terrain` and a `NavGraph` do, as a resource
301
+ * handed in through `uses`. So this is registered unconditionally, and a script that never receives
302
+ * one simply never calls it.
303
+ */
304
+ export function editorImplementation(): Record<string, unknown> {
305
+ return {
306
+ translateMode: (gizmo: Gizmo) => {
307
+ gizmo.mode = 'translate';
308
+ },
309
+ rotateMode: (gizmo: Gizmo) => {
310
+ gizmo.mode = 'rotate';
311
+ },
312
+ scaleMode: (gizmo: Gizmo) => {
313
+ gizmo.mode = 'scale';
314
+ },
315
+ gizmoMode: (gizmo: Gizmo) => gizmo.mode,
316
+
317
+ worldSpace: (gizmo: Gizmo) => {
318
+ gizmo.space = 'world';
319
+ },
320
+ localSpace: (gizmo: Gizmo) => {
321
+ gizmo.space = 'local';
322
+ },
323
+ space: (gizmo: Gizmo) => gizmo.space,
324
+
325
+ size: (gizmo: Gizmo) => gizmo.size,
326
+ setSize: (gizmo: Gizmo, metres: number) => {
327
+ gizmo.size = metres;
328
+ },
329
+
330
+ hovered: (gizmo: Gizmo) => HANDLE_NAMES[gizmo.hovered] ?? 'none',
331
+ dragging: (gizmo: Gizmo) => gizmo.dragging,
332
+ dragAngle: (gizmo: Gizmo) => gizmo.dragAngle,
333
+
334
+ positionX: (gizmo: Gizmo) => gizmo.position[0] ?? 0,
335
+ positionY: (gizmo: Gizmo) => gizmo.position[1] ?? 0,
336
+ positionZ: (gizmo: Gizmo) => gizmo.position[2] ?? 0,
337
+ setPosition: (gizmo: Gizmo, x: number, y: number, z: number) => {
338
+ gizmo.position[0] = x;
339
+ gizmo.position[1] = y;
340
+ gizmo.position[2] = z;
341
+ },
342
+
343
+ scaleX: (gizmo: Gizmo) => gizmo.scale[0] ?? 1,
344
+ scaleY: (gizmo: Gizmo) => gizmo.scale[1] ?? 1,
345
+ scaleZ: (gizmo: Gizmo) => gizmo.scale[2] ?? 1,
346
+
347
+ rotationX: (gizmo: Gizmo) => gizmo.rotation[0] ?? 0,
348
+ rotationY: (gizmo: Gizmo) => gizmo.rotation[1] ?? 0,
349
+ rotationZ: (gizmo: Gizmo) => gizmo.rotation[2] ?? 0,
350
+ rotationW: (gizmo: Gizmo) => gizmo.rotation[3] ?? 1,
351
+
352
+ mode: (editor: EditorHost) => editor.mode,
353
+ playing: (editor: EditorHost) => editor.mode === 'play',
354
+ play: (editor: EditorHost) => editor.play(),
355
+ pause: (editor: EditorHost) => {
356
+ editor.pause();
357
+ },
358
+ step: (editor: EditorHost) => {
359
+ editor.step();
360
+ },
361
+ stop: (editor: EditorHost) => editor.stop(),
362
+
363
+ rowCount: (editor: EditorHost) => editor.tree.rows.length,
364
+ rebuildTree: (editor: EditorHost, root: SceneNode) => {
365
+ editor.tree.rebuild(root);
366
+ },
367
+ /* Every row reader answers a defined value for a row that does not exist, because the language
368
+ has no optional to answer with and a frame loop is the worst place to learn that. */
369
+ rowName: (editor: EditorHost, row: number) => editor.tree.rows[row]?.name ?? '',
370
+ rowDepth: (editor: EditorHost, row: number) => editor.tree.rows[row]?.depth ?? -1,
371
+ rowExpanded: (editor: EditorHost, row: number) => editor.tree.rows[row]?.expanded ?? false,
372
+ toggleRow: (editor: EditorHost, row: number) => {
373
+ const at = editor.tree.rows[row];
374
+ if (at !== undefined) editor.tree.toggle(at.node);
375
+ },
376
+ selectRow: (editor: EditorHost, row: number) => {
377
+ editor.select(editor.tree.rows[row]?.node ?? null);
378
+ },
379
+ selectedRow: (editor: EditorHost) =>
380
+ editor.tree.selected === null ? -1 : editor.tree.rowOf(editor.tree.selected),
381
+
382
+ fieldCount: (editor: EditorHost) => editor.inspector.fields.length,
383
+ fieldLabel: (editor: EditorHost, field: number) => editor.inspector.fields[field]?.label ?? '',
384
+ fieldGroup: (editor: EditorHost, field: number) => editor.inspector.fields[field]?.group ?? '',
385
+ fieldKind: (editor: EditorHost, field: number) => editor.inspector.fields[field]?.kind ?? '',
386
+ fieldNumber: (editor: EditorHost, field: number) => {
387
+ const value = editor.inspector.fields[field]?.value;
388
+ return typeof value === 'number' ? value : 0;
389
+ },
390
+ setFieldNumber: (editor: EditorHost, field: number, value: number) =>
391
+ editor.inspector.set(field, value),
392
+ };
393
+ }