@driftengine/script 4.7.4 → 4.8.1
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/README.md +9 -4
- package/capabilities.json +229 -54
- package/dist/bindings/behavior.js +10 -6
- package/dist/bindings/chemistry.d.ts +4 -0
- package/dist/bindings/chemistry.js +40 -4
- package/dist/bindings/core.js +23 -4
- package/dist/bindings/editor.d.ts +10 -7
- package/dist/bindings/editor.js +72 -40
- package/dist/bindings/entities.js +16 -4
- package/dist/bindings/variants.d.ts +25 -0
- package/dist/bindings/variants.js +9 -0
- package/dist/bindings/xr.js +27 -14
- package/package.json +13 -13
- package/src/bindings/behavior.ts +19 -7
- package/src/bindings/chemistry.ts +54 -6
- package/src/bindings/core.ts +38 -6
- package/src/bindings/editor.ts +118 -44
- package/src/bindings/entities.ts +16 -4
- package/src/bindings/variants.ts +41 -0
- package/src/bindings/xr.ts +52 -25
package/src/bindings/behavior.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { BehaviorRunner } from '@driftengine/core';
|
|
2
2
|
import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
|
|
3
|
+
import { hostEnum } from './variants.ts';
|
|
3
4
|
|
|
4
5
|
export const BEHAVIOR_MODULE = 'drift/behavior';
|
|
5
6
|
|
|
7
|
+
/** What a tick answered, in the engine's own order: `FAILURE`, `SUCCESS`, `RUNNING`. */
|
|
8
|
+
const TREE_STATUS = hostEnum(
|
|
9
|
+
BEHAVIOR_MODULE,
|
|
10
|
+
'TreeStatus',
|
|
11
|
+
['Failure', 'Success', 'Running'],
|
|
12
|
+
'What a behaviour tree answered: it failed, it finished, or it is still going.',
|
|
13
|
+
);
|
|
14
|
+
|
|
6
15
|
/**
|
|
7
16
|
* `drift/behavior` — driving and watching a behaviour tree from a script.
|
|
8
17
|
*
|
|
@@ -29,6 +38,7 @@ export const BEHAVIOR_TYPES: readonly OpaqueType[] = [
|
|
|
29
38
|
name: 'Behavior',
|
|
30
39
|
doc: 'One agent running one behaviour tree: where it is in its routine.',
|
|
31
40
|
},
|
|
41
|
+
TREE_STATUS.type,
|
|
32
42
|
];
|
|
33
43
|
|
|
34
44
|
const define = (
|
|
@@ -55,15 +65,15 @@ export const BEHAVIOR_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
55
65
|
define(
|
|
56
66
|
'tick',
|
|
57
67
|
[{ name: 'behavior', type: 'Behavior' }],
|
|
58
|
-
'
|
|
68
|
+
'TreeStatus',
|
|
59
69
|
['behavior.write'],
|
|
60
|
-
'Advance the routine one tick
|
|
70
|
+
'Advance the routine one tick, and answer whether it failed, finished or is still going. Does nothing while paused.',
|
|
61
71
|
false,
|
|
62
72
|
),
|
|
63
73
|
define(
|
|
64
74
|
'step',
|
|
65
75
|
[{ name: 'behavior', type: 'Behavior' }],
|
|
66
|
-
'
|
|
76
|
+
'TreeStatus',
|
|
67
77
|
['behavior.write'],
|
|
68
78
|
'Advance one tick even while paused, which is what a step button is.',
|
|
69
79
|
false,
|
|
@@ -97,7 +107,7 @@ export const BEHAVIOR_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
97
107
|
define(
|
|
98
108
|
'status',
|
|
99
109
|
[{ name: 'behavior', type: 'Behavior' }],
|
|
100
|
-
'
|
|
110
|
+
'TreeStatus',
|
|
101
111
|
['behavior.read'],
|
|
102
112
|
'What the last tick answered, without ticking again.',
|
|
103
113
|
),
|
|
@@ -142,14 +152,16 @@ export function behaviorImplementation(services: BehaviorServices): Record<strin
|
|
|
142
152
|
let scratch: Uint32Array<ArrayBuffer> = new Uint32Array(32);
|
|
143
153
|
|
|
144
154
|
return {
|
|
145
|
-
tick: (character: BehaviorRunner<unknown>) =>
|
|
146
|
-
|
|
155
|
+
tick: (character: BehaviorRunner<unknown>) =>
|
|
156
|
+
TREE_STATUS.of[character.tick(services.context(character))],
|
|
157
|
+
step: (character: BehaviorRunner<unknown>) =>
|
|
158
|
+
TREE_STATUS.of[character.stepOnce(services.context(character))],
|
|
147
159
|
restart: (character: BehaviorRunner<unknown>) => character.reset(),
|
|
148
160
|
setPaused: (character: BehaviorRunner<unknown>, paused: boolean) => {
|
|
149
161
|
character.paused = paused;
|
|
150
162
|
},
|
|
151
163
|
paused: (character: BehaviorRunner<unknown>) => character.paused,
|
|
152
|
-
status: (character: BehaviorRunner<unknown>) => character.status,
|
|
164
|
+
status: (character: BehaviorRunner<unknown>) => TREE_STATUS.of[character.status],
|
|
153
165
|
depth(character: BehaviorRunner<unknown>): number {
|
|
154
166
|
scratch = fit(scratch, character);
|
|
155
167
|
return character.activePath(scratch);
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from '@driftengine/chemistry';
|
|
46
46
|
import { type ComponentType, defineComponent } from '@driftengine/entities';
|
|
47
47
|
import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
|
|
48
|
+
import { hostEnum } from './variants.ts';
|
|
48
49
|
|
|
49
50
|
export const CHEMISTRY_MODULE = 'drift/chemistry';
|
|
50
51
|
|
|
@@ -68,6 +69,44 @@ const define = (
|
|
|
68
69
|
implementation: `${CHEMISTRY_MODULE}.${name}`,
|
|
69
70
|
});
|
|
70
71
|
|
|
72
|
+
/** A parcel's phase, in the package's order: `PHASE_SOLID`, `_LIQUID`, `_GAS`, then `PHASE_MIXED`. */
|
|
73
|
+
export const MATTER_PHASE = hostEnum(
|
|
74
|
+
CHEMISTRY_MODULE,
|
|
75
|
+
'MatterPhase',
|
|
76
|
+
['Solid', 'Liquid', 'Gas', 'Mixed'],
|
|
77
|
+
'What a parcel is made of now. Mixed is a real answer: wet wood is a solid and a liquid at once.',
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
/** What happened to a parcel this tick, in the order of the package's `EVENT_*` constants. */
|
|
81
|
+
export const CHEMISTRY_EVENT = hostEnum(
|
|
82
|
+
CHEMISTRY_MODULE,
|
|
83
|
+
'ChemistryEvent',
|
|
84
|
+
[
|
|
85
|
+
'Ignited',
|
|
86
|
+
'Extinguished',
|
|
87
|
+
'SmoulderStart',
|
|
88
|
+
'SmoulderEnd',
|
|
89
|
+
'Consumed',
|
|
90
|
+
'Charred',
|
|
91
|
+
'StructuralFail',
|
|
92
|
+
'Frozen',
|
|
93
|
+
'Melted',
|
|
94
|
+
'Boiled',
|
|
95
|
+
'Condensed',
|
|
96
|
+
'Sublimed',
|
|
97
|
+
'Corroded',
|
|
98
|
+
'Calcined',
|
|
99
|
+
'Dissolved',
|
|
100
|
+
'Denatured',
|
|
101
|
+
'Browned',
|
|
102
|
+
'Caramelised',
|
|
103
|
+
'Fermented',
|
|
104
|
+
'Decayed',
|
|
105
|
+
'Exploded',
|
|
106
|
+
],
|
|
107
|
+
'What happened to a parcel this tick: it caught, went out, melted, boiled and the rest.',
|
|
108
|
+
);
|
|
109
|
+
|
|
71
110
|
/**
|
|
72
111
|
* The three opaque types `§20.2` names.
|
|
73
112
|
*
|
|
@@ -91,6 +130,8 @@ export const CHEMISTRY_TYPES: readonly OpaqueType[] = [
|
|
|
91
130
|
name: 'Species',
|
|
92
131
|
doc: 'A registered chemical species, such as `O2` or `cellulose`.',
|
|
93
132
|
},
|
|
133
|
+
MATTER_PHASE.type,
|
|
134
|
+
CHEMISTRY_EVENT.type,
|
|
94
135
|
];
|
|
95
136
|
|
|
96
137
|
const READ = ['chemistry.read'] as const;
|
|
@@ -199,9 +240,9 @@ export const CHEMISTRY_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
199
240
|
define(
|
|
200
241
|
'phase',
|
|
201
242
|
[P, PARCEL],
|
|
202
|
-
'
|
|
243
|
+
'MatterPhase',
|
|
203
244
|
READ,
|
|
204
|
-
'
|
|
245
|
+
'Whether the parcel is solid, liquid, gas or mixed. Mixed is a real answer: wet wood is a solid and a liquid at once.',
|
|
205
246
|
),
|
|
206
247
|
define('burning', [P, PARCEL], 'bool', READ, 'Whether a flame stands over it.'),
|
|
207
248
|
define(
|
|
@@ -401,9 +442,9 @@ export const CHEMISTRY_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
401
442
|
define(
|
|
402
443
|
'eventKind',
|
|
403
444
|
[P, { name: 'index', type: 'i32' }],
|
|
404
|
-
'
|
|
445
|
+
'ChemistryEvent',
|
|
405
446
|
READ,
|
|
406
|
-
'
|
|
447
|
+
'What happened in one of this tick’s events. An index outside `eventCount` is refused, naming both.',
|
|
407
448
|
),
|
|
408
449
|
define(
|
|
409
450
|
'eventParcel',
|
|
@@ -496,7 +537,7 @@ export function chemistryImplementation(services: ChemistryServices): Record<str
|
|
|
496
537
|
charDepth: (_: unknown, parcel: number) => parcels.charDepthOf(parcel),
|
|
497
538
|
wetness: (_: unknown, parcel: number) => parcels.wetnessOf(parcel),
|
|
498
539
|
wettable: (_: unknown, parcel: number) => parcels.wettable(parcel),
|
|
499
|
-
phase: (_: unknown, parcel: number) => parcels.parcelPhaseOf(parcel),
|
|
540
|
+
phase: (_: unknown, parcel: number) => MATTER_PHASE.of[parcels.parcelPhaseOf(parcel)],
|
|
500
541
|
burning: (_: unknown, parcel: number) => parcels.burning(parcel),
|
|
501
542
|
smouldering: (_: unknown, parcel: number) => parcels.smouldering(parcel),
|
|
502
543
|
/* Watts inside, kilowatts out: a fire is quoted in kW everywhere a person reads one. */
|
|
@@ -548,7 +589,14 @@ export function chemistryImplementation(services: ChemistryServices): Record<str
|
|
|
548
589
|
air.addHeat(x, y, z, joules),
|
|
549
590
|
|
|
550
591
|
eventCount: () => world.events.count,
|
|
551
|
-
eventKind
|
|
592
|
+
eventKind(_: unknown, index: number) {
|
|
593
|
+
/* The buffer is read unchecked, and past `count` it holds the last tick's events or nothing:
|
|
594
|
+
a kind that did not happen this tick, or no variant at all. */
|
|
595
|
+
if (!(index >= 0 && index < world.events.count)) {
|
|
596
|
+
throw new RangeError(`event ${index} is outside this tick's ${world.events.count}`);
|
|
597
|
+
}
|
|
598
|
+
return CHEMISTRY_EVENT.of[world.events.kindOf(index)];
|
|
599
|
+
},
|
|
552
600
|
eventParcel: (_: unknown, index: number) => world.events.parcelOf(index),
|
|
553
601
|
eventSpecies: (_: unknown, index: number) => world.events.speciesOf(index),
|
|
554
602
|
eventValue: (_: unknown, index: number) => world.events.valueOf(index),
|
package/src/bindings/core.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type Collider,
|
|
26
26
|
ColliderSet,
|
|
27
27
|
type KeyValueStore,
|
|
28
|
+
type SaveStatus,
|
|
28
29
|
RemoteSaveStore,
|
|
29
30
|
MessageQueue,
|
|
30
31
|
PhysicsWorld,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
wrapAngle,
|
|
42
43
|
} from '@driftengine/core';
|
|
43
44
|
import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
|
|
45
|
+
import { type Variant, hostEnum } from './variants.ts';
|
|
44
46
|
|
|
45
47
|
export const TIME_MODULE = 'drift/time';
|
|
46
48
|
export const RANDOM_MODULE = 'drift/random';
|
|
@@ -72,6 +74,28 @@ const define = (
|
|
|
72
74
|
implementation: `${module}.${name}`,
|
|
73
75
|
});
|
|
74
76
|
|
|
77
|
+
/** A contact event's kind, in `@driftengine/physics`'s order: `EVENT_ENTER`, `_STAY`, `_EXIT`. */
|
|
78
|
+
const CONTACT_KIND = hostEnum(
|
|
79
|
+
'drift/physics',
|
|
80
|
+
'ContactKind',
|
|
81
|
+
['Enter', 'Stay', 'Exit'],
|
|
82
|
+
'Whether two bodies began touching this step, are still touching, or have parted.',
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
/** A remote store's `SaveStatus`, one variant per string the store answers with. */
|
|
86
|
+
const SAVE_STATUS = hostEnum(
|
|
87
|
+
'drift/persistence',
|
|
88
|
+
'SaveStatus',
|
|
89
|
+
['Idle', 'Pending', 'Saving', 'Failed'],
|
|
90
|
+
'Whether a store’s writes have landed: idle, waiting to send, sending, or failed.',
|
|
91
|
+
);
|
|
92
|
+
const SAVE_STATUS_OF: Readonly<Record<SaveStatus, Variant>> = {
|
|
93
|
+
idle: SAVE_STATUS.of[0] as Variant,
|
|
94
|
+
pending: SAVE_STATUS.of[1] as Variant,
|
|
95
|
+
saving: SAVE_STATUS.of[2] as Variant,
|
|
96
|
+
failed: SAVE_STATUS.of[3] as Variant,
|
|
97
|
+
};
|
|
98
|
+
|
|
75
99
|
/**
|
|
76
100
|
* Handles a script holds and passes back, never reads into.
|
|
77
101
|
*
|
|
@@ -119,6 +143,8 @@ export const CORE_TYPES: readonly OpaqueType[] = [
|
|
|
119
143
|
doc: 'A priority message queue with dedupe and a ceiling.',
|
|
120
144
|
},
|
|
121
145
|
{ module: PERSISTENCE_MODULE, name: 'Store', doc: 'A key-value store the consumer supplied.' },
|
|
146
|
+
CONTACT_KIND.type,
|
|
147
|
+
SAVE_STATUS.type,
|
|
122
148
|
];
|
|
123
149
|
|
|
124
150
|
/**
|
|
@@ -306,10 +332,10 @@ export const PERSISTENCE_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
306
332
|
PERSISTENCE_MODULE,
|
|
307
333
|
'saveStatus',
|
|
308
334
|
[{ name: 'store', type: 'Store' }],
|
|
309
|
-
'
|
|
335
|
+
'SaveStatus',
|
|
310
336
|
['persistence.read'],
|
|
311
337
|
false,
|
|
312
|
-
'Whether writes have landed:
|
|
338
|
+
'Whether writes have landed: idle, pending, saving or failed. A store that writes synchronously is always idle, which is the true answer rather than a stub.',
|
|
313
339
|
),
|
|
314
340
|
define(
|
|
315
341
|
PERSISTENCE_MODULE,
|
|
@@ -899,10 +925,10 @@ export const PHYSICS_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
899
925
|
{ name: 'world', type: 'PhysicsWorld' },
|
|
900
926
|
{ name: 'index', type: 'i32' },
|
|
901
927
|
],
|
|
902
|
-
'
|
|
928
|
+
'ContactKind',
|
|
903
929
|
['physics.read'],
|
|
904
930
|
true,
|
|
905
|
-
"An event's kind:
|
|
931
|
+
"An event's kind: the two bodies began touching, are still touching, or have parted.",
|
|
906
932
|
),
|
|
907
933
|
define(
|
|
908
934
|
PHYSICS_MODULE,
|
|
@@ -1341,7 +1367,7 @@ export function persistenceImplementation(): Record<string, unknown> {
|
|
|
1341
1367
|
/* `instanceof` rather than a duck-typed property check: a store that happens to carry a
|
|
1342
1368
|
`status` field meaning something else would otherwise be read as this one. */
|
|
1343
1369
|
saveStatus: (store: KeyValueStore) =>
|
|
1344
|
-
store instanceof RemoteSaveStore ? store.status : 'idle',
|
|
1370
|
+
SAVE_STATUS_OF[store instanceof RemoteSaveStore ? store.status : 'idle'],
|
|
1345
1371
|
pendingSaves: (store: KeyValueStore) => (store instanceof RemoteSaveStore ? store.pending : 0),
|
|
1346
1372
|
};
|
|
1347
1373
|
}
|
|
@@ -1583,7 +1609,13 @@ export function physicsImplementation(): Record<string, unknown> {
|
|
|
1583
1609
|
hitFraction: (world: PhysicsWorld) => f(hitOf(world).fraction),
|
|
1584
1610
|
|
|
1585
1611
|
contactCount: (world: PhysicsWorld) => world.events.count,
|
|
1586
|
-
contactKind
|
|
1612
|
+
contactKind(world: PhysicsWorld, index: number) {
|
|
1613
|
+
/* Past `count` the buffer holds an older step's events or nothing, so neither is answered. */
|
|
1614
|
+
if (!(index >= 0 && index < world.events.count)) {
|
|
1615
|
+
throw new RangeError(`contact ${index} is outside this step's ${world.events.count}`);
|
|
1616
|
+
}
|
|
1617
|
+
return CONTACT_KIND.of[world.events.data[index * 3] as number];
|
|
1618
|
+
},
|
|
1587
1619
|
contactA: (world: PhysicsWorld, index: number) => world.events.data[index * 3 + 1] ?? 0,
|
|
1588
1620
|
contactB: (world: PhysicsWorld, index: number) => world.events.data[index * 3 + 2] ?? 0,
|
|
1589
1621
|
|
package/src/bindings/editor.ts
CHANGED
|
@@ -1,10 +1,84 @@
|
|
|
1
1
|
import type { Gizmo, SceneNode } from '@driftengine/core';
|
|
2
2
|
import type { EditorHost } from '@driftengine/editor';
|
|
3
|
+
import type { EditorMode, FieldKind } from '@driftengine/editor';
|
|
3
4
|
import type { CapabilityDefinition, OpaqueType } from 'driftscript';
|
|
4
5
|
import { defineCapability } from 'driftscript';
|
|
6
|
+
import { type Variant, hostEnum } from './variants.ts';
|
|
5
7
|
|
|
6
8
|
export const EDITOR_MODULE = 'drift/editor';
|
|
7
9
|
|
|
10
|
+
/** Which tool the gizmo is, one variant per `Gizmo.mode`. */
|
|
11
|
+
const GIZMO_MODE = hostEnum(
|
|
12
|
+
EDITOR_MODULE,
|
|
13
|
+
'GizmoMode',
|
|
14
|
+
['Translate', 'Rotate', 'Scale'],
|
|
15
|
+
'Which tool a gizmo is: it moves, turns or scales.',
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/** Which axes the handles follow, one variant per `Gizmo.space`. */
|
|
19
|
+
const GIZMO_SPACE = hostEnum(
|
|
20
|
+
EDITOR_MODULE,
|
|
21
|
+
'GizmoSpace',
|
|
22
|
+
['World', 'Local'],
|
|
23
|
+
'Which axes a gizmo’s handles point along: the world’s or the object’s own.',
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The handle under the pointer, in the order of `Gizmo`'s integer constants, `None` first.
|
|
28
|
+
*
|
|
29
|
+
* A variant per handle where this used to answer a name such as `"translate.yz"`, which a script
|
|
30
|
+
* compared as a string: a misspelt name compared false forever and nothing said so. A handle added
|
|
31
|
+
* in the middle now changes this list, and every script `match` over it that misses it stops
|
|
32
|
+
* compiling.
|
|
33
|
+
*/
|
|
34
|
+
const GIZMO_HANDLE = hostEnum(
|
|
35
|
+
EDITOR_MODULE,
|
|
36
|
+
'GizmoHandle',
|
|
37
|
+
[
|
|
38
|
+
'None',
|
|
39
|
+
'TranslateX',
|
|
40
|
+
'TranslateY',
|
|
41
|
+
'TranslateZ',
|
|
42
|
+
'TranslateYZ',
|
|
43
|
+
'TranslateZX',
|
|
44
|
+
'TranslateXY',
|
|
45
|
+
'RotateX',
|
|
46
|
+
'RotateY',
|
|
47
|
+
'RotateZ',
|
|
48
|
+
'ScaleX',
|
|
49
|
+
'ScaleY',
|
|
50
|
+
'ScaleZ',
|
|
51
|
+
'ScaleUniform',
|
|
52
|
+
],
|
|
53
|
+
'Which handle the pointer is over, or `None`.',
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
/** What the editor is doing, one variant per `EditorMode`. */
|
|
57
|
+
const EDITOR_MODE = hostEnum(
|
|
58
|
+
EDITOR_MODULE,
|
|
59
|
+
'EditorMode',
|
|
60
|
+
['Edit', 'Play', 'Paused'],
|
|
61
|
+
'Whether the editor is editing, playing the world, or paused part way through playing it.',
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/** How the inspector shows a field, one variant per `FieldKind`. */
|
|
65
|
+
const FIELD_KIND = hostEnum(
|
|
66
|
+
EDITOR_MODULE,
|
|
67
|
+
'FieldKind',
|
|
68
|
+
['Number', 'Integer', 'Boolean', 'Text', 'Entity', 'Enum'],
|
|
69
|
+
'How an inspector field is shown, taken from the type it was declared with.',
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const byName = (names: readonly string[], of: readonly Variant[]): ReadonlyMap<string, Variant> =>
|
|
73
|
+
new Map(names.map((name, index) => [name, of[index] as Variant]));
|
|
74
|
+
const GIZMO_MODE_OF = byName(['translate', 'rotate', 'scale'], GIZMO_MODE.of);
|
|
75
|
+
const GIZMO_SPACE_OF = byName(['world', 'local'], GIZMO_SPACE.of);
|
|
76
|
+
const EDITOR_MODE_OF = byName(['edit', 'play', 'paused'] satisfies EditorMode[], EDITOR_MODE.of);
|
|
77
|
+
const FIELD_KIND_OF = byName(
|
|
78
|
+
['number', 'integer', 'boolean', 'text', 'entity', 'enum'] satisfies FieldKind[],
|
|
79
|
+
FIELD_KIND.of,
|
|
80
|
+
);
|
|
81
|
+
|
|
8
82
|
/**
|
|
9
83
|
* `drift/editor` — the transform tool, driven from a script that never ships.
|
|
10
84
|
*
|
|
@@ -37,13 +111,16 @@ export const EDITOR_MODULE = 'drift/editor';
|
|
|
37
111
|
*
|
|
38
112
|
* ## Three capabilities where an argument would have done, deliberately
|
|
39
113
|
*
|
|
40
|
-
* `translateMode`, `rotateMode` and `scaleMode` rather than `setMode(gizmo, mode)`.
|
|
41
|
-
*
|
|
42
|
-
* options — change nothing, the silent no-op `host.ts`
|
|
43
|
-
* `AGENTS.md` rules out. Three names make the invalid call
|
|
44
|
-
* rather than a runtime branch is what refuses it.
|
|
114
|
+
* `translateMode`, `rotateMode` and `scaleMode` rather than `setMode(gizmo, mode)`. When these were
|
|
115
|
+
* written a host could not declare an enum, so the argument would have been a string or a number,
|
|
116
|
+
* and an unrecognised one leaves exactly two options — change nothing, the silent no-op `host.ts`
|
|
117
|
+
* rules out, or throw inside a frame, which `AGENTS.md` rules out. Three names make the invalid call
|
|
118
|
+
* impossible to write, and the checker rather than a runtime branch is what refuses it. A
|
|
119
|
+
* `GizmoMode` argument would do the same now; the three stay because scripts call them. `space` is
|
|
120
|
+
* the same argument again.
|
|
45
121
|
*
|
|
46
|
-
* The *readers*
|
|
122
|
+
* The *readers* answer variants, a `GizmoMode`, a `GizmoSpace` and a `GizmoHandle`, so a script
|
|
123
|
+
* that matches on one is told when a mode or a handle is added.
|
|
47
124
|
*/
|
|
48
125
|
export const EDITOR_TYPES: readonly OpaqueType[] = [
|
|
49
126
|
{
|
|
@@ -56,6 +133,11 @@ export const EDITOR_TYPES: readonly OpaqueType[] = [
|
|
|
56
133
|
name: 'Editor',
|
|
57
134
|
doc: 'An editing session: what is selected, the tree of what exists, and whether the world is playing.',
|
|
58
135
|
},
|
|
136
|
+
GIZMO_MODE.type,
|
|
137
|
+
GIZMO_SPACE.type,
|
|
138
|
+
GIZMO_HANDLE.type,
|
|
139
|
+
EDITOR_MODE.type,
|
|
140
|
+
FIELD_KIND.type,
|
|
59
141
|
];
|
|
60
142
|
|
|
61
143
|
const define = (
|
|
@@ -103,13 +185,18 @@ export const EDITOR_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
103
185
|
define(
|
|
104
186
|
'gizmoMode',
|
|
105
187
|
OF,
|
|
106
|
-
'
|
|
107
|
-
'Which of the three the gizmo is:
|
|
188
|
+
'GizmoMode',
|
|
189
|
+
'Which of the three the gizmo is: translate, rotate or scale.',
|
|
108
190
|
),
|
|
109
191
|
|
|
110
192
|
define('worldSpace', OF, 'void', 'Point the handles along the world axes.'),
|
|
111
193
|
define('localSpace', OF, 'void', 'Point the handles along the object’s own axes.'),
|
|
112
|
-
define(
|
|
194
|
+
define(
|
|
195
|
+
'space',
|
|
196
|
+
OF,
|
|
197
|
+
'GizmoSpace',
|
|
198
|
+
'Which the handles are pointing along: the world or the object.',
|
|
199
|
+
),
|
|
113
200
|
|
|
114
201
|
define(
|
|
115
202
|
'size',
|
|
@@ -125,15 +212,16 @@ export const EDITOR_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
125
212
|
),
|
|
126
213
|
|
|
127
214
|
/*
|
|
128
|
-
* **A
|
|
129
|
-
*
|
|
130
|
-
*
|
|
215
|
+
* **A variant, and total.** The handles are integer constants in TypeScript, and a script comparing
|
|
216
|
+
* against a magic number breaks silently the day a handle is added in the middle; a script
|
|
217
|
+
* comparing against a name breaks silently the day it misspells one. `None` is a real answer
|
|
218
|
+
* rather than a missing one.
|
|
131
219
|
*/
|
|
132
220
|
define(
|
|
133
221
|
'hovered',
|
|
134
222
|
OF,
|
|
135
|
-
'
|
|
136
|
-
'What the pointer is over
|
|
223
|
+
'GizmoHandle',
|
|
224
|
+
'What the pointer is over: `None`, or a handle such as `TranslateX`, `TranslateYZ`, `RotateY` or `ScaleUniform`.',
|
|
137
225
|
),
|
|
138
226
|
define('dragging', OF, 'bool', 'Whether a drag is running.'),
|
|
139
227
|
define(
|
|
@@ -172,7 +260,7 @@ export const EDITOR_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
172
260
|
* **The session, from `@driftengine/editor`.** Everything above drives one tool; everything below
|
|
173
261
|
* drives the editor around it — which is what an `@editor` script is actually for.
|
|
174
262
|
*/
|
|
175
|
-
define('mode', ON, '
|
|
263
|
+
define('mode', ON, 'EditorMode', 'What the editor is doing: editing, playing or paused.'),
|
|
176
264
|
define(
|
|
177
265
|
'playing',
|
|
178
266
|
ON,
|
|
@@ -254,8 +342,8 @@ export const EDITOR_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
254
342
|
define(
|
|
255
343
|
'fieldKind',
|
|
256
344
|
[...ON, { name: 'field', type: 'i32' }],
|
|
257
|
-
'
|
|
258
|
-
'How to show a field:
|
|
345
|
+
'FieldKind',
|
|
346
|
+
'How to show a field: a number, an integer, a boolean, text, an entity or an enum. Taken from the declared type, because a read cannot tell them apart. A field outside `fieldCount` is refused, naming both.',
|
|
259
347
|
),
|
|
260
348
|
define(
|
|
261
349
|
'fieldNumber',
|
|
@@ -271,28 +359,6 @@ export const EDITOR_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
|
271
359
|
),
|
|
272
360
|
];
|
|
273
361
|
|
|
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
362
|
/**
|
|
297
363
|
* **No services, and that is the shape rather than an omission.**
|
|
298
364
|
*
|
|
@@ -312,7 +378,7 @@ export function editorImplementation(): Record<string, unknown> {
|
|
|
312
378
|
scaleMode: (gizmo: Gizmo) => {
|
|
313
379
|
gizmo.mode = 'scale';
|
|
314
380
|
},
|
|
315
|
-
gizmoMode: (gizmo: Gizmo) => gizmo.mode,
|
|
381
|
+
gizmoMode: (gizmo: Gizmo) => GIZMO_MODE_OF.get(gizmo.mode),
|
|
316
382
|
|
|
317
383
|
worldSpace: (gizmo: Gizmo) => {
|
|
318
384
|
gizmo.space = 'world';
|
|
@@ -320,14 +386,14 @@ export function editorImplementation(): Record<string, unknown> {
|
|
|
320
386
|
localSpace: (gizmo: Gizmo) => {
|
|
321
387
|
gizmo.space = 'local';
|
|
322
388
|
},
|
|
323
|
-
space: (gizmo: Gizmo) => gizmo.space,
|
|
389
|
+
space: (gizmo: Gizmo) => GIZMO_SPACE_OF.get(gizmo.space),
|
|
324
390
|
|
|
325
391
|
size: (gizmo: Gizmo) => gizmo.size,
|
|
326
392
|
setSize: (gizmo: Gizmo, metres: number) => {
|
|
327
393
|
gizmo.size = metres;
|
|
328
394
|
},
|
|
329
395
|
|
|
330
|
-
hovered: (gizmo: Gizmo) =>
|
|
396
|
+
hovered: (gizmo: Gizmo) => GIZMO_HANDLE.of[gizmo.hovered] ?? GIZMO_HANDLE.of[0],
|
|
331
397
|
dragging: (gizmo: Gizmo) => gizmo.dragging,
|
|
332
398
|
dragAngle: (gizmo: Gizmo) => gizmo.dragAngle,
|
|
333
399
|
|
|
@@ -349,7 +415,7 @@ export function editorImplementation(): Record<string, unknown> {
|
|
|
349
415
|
rotationZ: (gizmo: Gizmo) => gizmo.rotation[2] ?? 0,
|
|
350
416
|
rotationW: (gizmo: Gizmo) => gizmo.rotation[3] ?? 1,
|
|
351
417
|
|
|
352
|
-
mode: (editor: EditorHost) => editor.mode,
|
|
418
|
+
mode: (editor: EditorHost) => EDITOR_MODE_OF.get(editor.mode),
|
|
353
419
|
playing: (editor: EditorHost) => editor.mode === 'play',
|
|
354
420
|
play: (editor: EditorHost) => editor.play(),
|
|
355
421
|
pause: (editor: EditorHost) => {
|
|
@@ -382,7 +448,15 @@ export function editorImplementation(): Record<string, unknown> {
|
|
|
382
448
|
fieldCount: (editor: EditorHost) => editor.inspector.fields.length,
|
|
383
449
|
fieldLabel: (editor: EditorHost, field: number) => editor.inspector.fields[field]?.label ?? '',
|
|
384
450
|
fieldGroup: (editor: EditorHost, field: number) => editor.inspector.fields[field]?.group ?? '',
|
|
385
|
-
fieldKind
|
|
451
|
+
fieldKind(editor: EditorHost, field: number) {
|
|
452
|
+
const found = editor.inspector.fields[field];
|
|
453
|
+
if (found === undefined) {
|
|
454
|
+
throw new RangeError(
|
|
455
|
+
`field ${field} is outside the ${editor.inspector.fields.length} shown`,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return FIELD_KIND_OF.get(found.kind);
|
|
459
|
+
},
|
|
386
460
|
fieldNumber: (editor: EditorHost, field: number) => {
|
|
387
461
|
const value = editor.inspector.fields[field]?.value;
|
|
388
462
|
return typeof value === 'number' ? value : 0;
|
package/src/bindings/entities.ts
CHANGED
|
@@ -399,10 +399,17 @@ export function entitiesImplementation(services: EntityServices): Record<string,
|
|
|
399
399
|
attach: (w: World, entity: Entity, component: string) => w.add(entity, resolve(component)),
|
|
400
400
|
detach: (w: World, entity: Entity, component: string) => w.remove(entity, resolve(component)),
|
|
401
401
|
read: (w: World, entity: Entity, component: string, field: string) => {
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
402
|
+
const type = resolve(component);
|
|
403
|
+
const value = w.read(entity, type, field);
|
|
404
|
+
if (value !== undefined) return value;
|
|
405
|
+
/*
|
|
406
|
+
* Nothing to read: an absent optional field, or an entity without the component. An optional
|
|
407
|
+
* field answers `undefined`, which the generated code turns into `none`; the compiler wraps
|
|
408
|
+
* every read of one. Any other field answers zero, since a script's `f64` cannot hold
|
|
409
|
+
* nothing. Answering zero for an optional field too was what made it unreadable: `none` and
|
|
410
|
+
* a stored zero were the same answer.
|
|
411
|
+
*/
|
|
412
|
+
return optionalField(type, field) ? undefined : 0;
|
|
406
413
|
},
|
|
407
414
|
write: (w: World, entity: Entity, component: string, field: string, value: number) =>
|
|
408
415
|
w.write(entity, resolve(component), field, value),
|
|
@@ -551,3 +558,8 @@ interface QuerySource {
|
|
|
551
558
|
query(a: ComponentType, b?: ComponentType, c?: ComponentType, d?: ComponentType): QueryCursor;
|
|
552
559
|
view(type: ComponentType, forWriting: boolean): ComponentView;
|
|
553
560
|
}
|
|
561
|
+
|
|
562
|
+
/** Whether a component declared this field as an option, `option:<inner>` in its schema. */
|
|
563
|
+
function optionalField(type: ComponentType, field: string): boolean {
|
|
564
|
+
return type.schema.fields.some((f) => f.name === field && f.type.startsWith('option:'));
|
|
565
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A host enum's variants, as the values a script holds and hands back.
|
|
3
|
+
*
|
|
4
|
+
* An engine answer that is one of a few things is an enum on the script's side: DriftScript lets a
|
|
5
|
+
* host declare one (`OpaqueType.variants`), a script names its variants and a `match` over it has
|
|
6
|
+
* to cover them all. The engine keeps its own numbering, and this is the one place a number becomes
|
|
7
|
+
* a variant and back. Before it, each such capability answered with the number and a sentence
|
|
8
|
+
* saying what each number meant, and every script restated that sentence as constants.
|
|
9
|
+
*
|
|
10
|
+
* One frozen object per variant, made once, so answering allocates nothing. A script compares a
|
|
11
|
+
* variant by its tag, so a script's own copy of `{ tag: 'Solid' }` equals this one.
|
|
12
|
+
*/
|
|
13
|
+
import type { OpaqueType } from 'driftscript';
|
|
14
|
+
|
|
15
|
+
export interface Variant {
|
|
16
|
+
readonly tag: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HostEnum {
|
|
20
|
+
/** The type to register, so a script can name it. */
|
|
21
|
+
readonly type: OpaqueType;
|
|
22
|
+
/** The variant for an engine code, in the order the engine numbers them. */
|
|
23
|
+
readonly of: readonly Variant[];
|
|
24
|
+
/** The engine's code for a variant a script handed back, or -1 for anything else. */
|
|
25
|
+
code(variant: Variant): number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function hostEnum(
|
|
29
|
+
module: string,
|
|
30
|
+
name: string,
|
|
31
|
+
variants: readonly string[],
|
|
32
|
+
doc: string,
|
|
33
|
+
): HostEnum {
|
|
34
|
+
const of = variants.map((tag) => Object.freeze({ tag }));
|
|
35
|
+
const codes = new Map(variants.map((tag, index) => [tag, index]));
|
|
36
|
+
return {
|
|
37
|
+
type: { module, name, doc, variants },
|
|
38
|
+
of,
|
|
39
|
+
code: (variant) => codes.get(variant.tag) ?? -1,
|
|
40
|
+
};
|
|
41
|
+
}
|