@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,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `drift/ecs` and `drift/prefab` — the entity model as DriftScript reaches it.
|
|
3
|
+
*
|
|
4
|
+
* These are the two surfaces the linker has refused by name since Track N shipped, and this file is
|
|
5
|
+
* what kills that refusal. Like every binding here, nothing in it is new engine code: a binding is a
|
|
6
|
+
* description and a lookup.
|
|
7
|
+
*
|
|
8
|
+
* ---
|
|
9
|
+
*
|
|
10
|
+
* ## A component is named by a string, and that is forced rather than chosen
|
|
11
|
+
*
|
|
12
|
+
* A script cannot hold a `ComponentType` — it is an object with a schema, and the language's types
|
|
13
|
+
* are numbers, strings, booleans and opaque handles. So a component is addressed by name and the
|
|
14
|
+
* host resolves it. **The cost is that a typo is a runtime refusal rather than a compile error**,
|
|
15
|
+
* and the fix is DS-5's `component` declaration, which is Track N's to build now that this exists.
|
|
16
|
+
* Refusing in words naming the component and listing what the world has is what the interim owes.
|
|
17
|
+
*
|
|
18
|
+
* ## There is no query here, and that is not an omission
|
|
19
|
+
*
|
|
20
|
+
* Iterating a query needs a loop form the language does not have for it — that is DS-5 and DS-6.
|
|
21
|
+
* What a script can do today is walk a component's dense array by index, which `while` expresses
|
|
22
|
+
* perfectly. **The order is insertion modified by swap-removal**, so an entity removed during a
|
|
23
|
+
* walk moves the last one into a position already passed; a script that removes while walking
|
|
24
|
+
* should walk backwards, and the doc on `at` says so.
|
|
25
|
+
*/
|
|
26
|
+
import {
|
|
27
|
+
ENTITY_INDEX_CEILING,
|
|
28
|
+
type ComponentType,
|
|
29
|
+
type ComponentView,
|
|
30
|
+
type Entity,
|
|
31
|
+
type Prefab,
|
|
32
|
+
type QueryCursor,
|
|
33
|
+
type World,
|
|
34
|
+
instantiate,
|
|
35
|
+
} from '@driftengine/entities';
|
|
36
|
+
import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
|
|
37
|
+
|
|
38
|
+
export const ECS_MODULE = 'drift/ecs';
|
|
39
|
+
export const PREFAB_MODULE = 'drift/prefab';
|
|
40
|
+
|
|
41
|
+
const define = (
|
|
42
|
+
module: string,
|
|
43
|
+
name: string,
|
|
44
|
+
params: readonly { name: string; type: string }[],
|
|
45
|
+
returns: string,
|
|
46
|
+
effects: CapabilityDefinition['effects'],
|
|
47
|
+
deterministic: boolean,
|
|
48
|
+
doc: string,
|
|
49
|
+
): CapabilityDefinition =>
|
|
50
|
+
defineCapability({
|
|
51
|
+
module,
|
|
52
|
+
name,
|
|
53
|
+
signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
|
|
54
|
+
params: [...params],
|
|
55
|
+
returns,
|
|
56
|
+
effects,
|
|
57
|
+
deterministic,
|
|
58
|
+
doc,
|
|
59
|
+
implementation: `${module}.${name}`,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const ENTITY_TYPES: readonly OpaqueType[] = [
|
|
63
|
+
{
|
|
64
|
+
module: ECS_MODULE,
|
|
65
|
+
name: 'World',
|
|
66
|
+
doc: 'A world: its entities, their components and its systems.',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
module: ECS_MODULE,
|
|
70
|
+
name: 'Cursor',
|
|
71
|
+
doc: "A query in progress. Borrowed from the world's pool and given back when the walk ends, so it may not outlive the loop that opened it.",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
module: ECS_MODULE,
|
|
75
|
+
name: 'View',
|
|
76
|
+
doc: "A component's live columns. The object stays valid across a reallocation; the arrays inside it do not, so index through the view rather than holding one.",
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* An entity is `Entity`, and it took the language until DriftScript 1.6.0 to have the word.
|
|
82
|
+
*
|
|
83
|
+
* A handle carries a 26-bit index and a 27-bit generation, which is the entire 53-bit budget a
|
|
84
|
+
* double holds. A `u32` would truncate the generation — and a truncated generation is a stale
|
|
85
|
+
* handle that compares equal to a live one, which is the exact failure the generation exists to
|
|
86
|
+
* prevent. So the width was never negotiable, and until the language could name it these all said
|
|
87
|
+
* `f64` with this paragraph explaining why.
|
|
88
|
+
*
|
|
89
|
+
* **`Entity` is a type of its own there rather than an alias**, which is what makes the change worth
|
|
90
|
+
* making: a handle is assignable *to* an `f64` and not *from* one, so a number a script computed
|
|
91
|
+
* cannot arrive where a handle is wanted, and `e.Health` is legal on a handle and on nothing else.
|
|
92
|
+
* The language's own note asked for this by name — a capability that hands back a handle as an
|
|
93
|
+
* `f64` and expects it to keep working as one is what it said would make the asymmetry wrong.
|
|
94
|
+
*
|
|
95
|
+
* **`read` still answers `f64`**, and that is not an oversight: it reads a component *field*, which
|
|
96
|
+
* is a number, and typing it as a handle would be the same mistake in the other direction.
|
|
97
|
+
*/
|
|
98
|
+
export const ECS_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
99
|
+
define(
|
|
100
|
+
ECS_MODULE,
|
|
101
|
+
'create',
|
|
102
|
+
[{ name: 'world', type: 'World' }],
|
|
103
|
+
'Entity',
|
|
104
|
+
['ecs.write'],
|
|
105
|
+
false,
|
|
106
|
+
'A fresh entity with nothing on it. The handle is exact; do not put one in an `f32`.',
|
|
107
|
+
),
|
|
108
|
+
define(
|
|
109
|
+
ECS_MODULE,
|
|
110
|
+
'destroy',
|
|
111
|
+
[
|
|
112
|
+
{ name: 'world', type: 'World' },
|
|
113
|
+
{ name: 'entity', type: 'Entity' },
|
|
114
|
+
],
|
|
115
|
+
'bool',
|
|
116
|
+
['ecs.write'],
|
|
117
|
+
false,
|
|
118
|
+
'Remove an entity and every component it had. False when the handle was already stale.',
|
|
119
|
+
),
|
|
120
|
+
define(
|
|
121
|
+
ECS_MODULE,
|
|
122
|
+
'alive',
|
|
123
|
+
[
|
|
124
|
+
{ name: 'world', type: 'World' },
|
|
125
|
+
{ name: 'entity', type: 'Entity' },
|
|
126
|
+
],
|
|
127
|
+
'bool',
|
|
128
|
+
['ecs.read'],
|
|
129
|
+
true,
|
|
130
|
+
'Whether this handle still names a live entity. A handle to something destroyed is false, even after its slot is reused.',
|
|
131
|
+
),
|
|
132
|
+
define(
|
|
133
|
+
ECS_MODULE,
|
|
134
|
+
'has',
|
|
135
|
+
[
|
|
136
|
+
{ name: 'world', type: 'World' },
|
|
137
|
+
{ name: 'entity', type: 'Entity' },
|
|
138
|
+
{ name: 'component', type: 'String' },
|
|
139
|
+
],
|
|
140
|
+
'bool',
|
|
141
|
+
['ecs.read'],
|
|
142
|
+
true,
|
|
143
|
+
'Whether an entity carries a component.',
|
|
144
|
+
),
|
|
145
|
+
define(
|
|
146
|
+
ECS_MODULE,
|
|
147
|
+
'attach',
|
|
148
|
+
[
|
|
149
|
+
{ name: 'world', type: 'World' },
|
|
150
|
+
{ name: 'entity', type: 'Entity' },
|
|
151
|
+
{ name: 'component', type: 'String' },
|
|
152
|
+
],
|
|
153
|
+
'void',
|
|
154
|
+
['ecs.write'],
|
|
155
|
+
false,
|
|
156
|
+
'Give an entity a component, every field at the zero for its type. Set them with `write`.',
|
|
157
|
+
),
|
|
158
|
+
define(
|
|
159
|
+
ECS_MODULE,
|
|
160
|
+
'detach',
|
|
161
|
+
[
|
|
162
|
+
{ name: 'world', type: 'World' },
|
|
163
|
+
{ name: 'entity', type: 'Entity' },
|
|
164
|
+
{ name: 'component', type: 'String' },
|
|
165
|
+
],
|
|
166
|
+
'bool',
|
|
167
|
+
['ecs.write'],
|
|
168
|
+
false,
|
|
169
|
+
'Take a component away. False when it did not have one.',
|
|
170
|
+
),
|
|
171
|
+
define(
|
|
172
|
+
ECS_MODULE,
|
|
173
|
+
'read',
|
|
174
|
+
[
|
|
175
|
+
{ name: 'world', type: 'World' },
|
|
176
|
+
{ name: 'entity', type: 'Entity' },
|
|
177
|
+
{ name: 'component', type: 'String' },
|
|
178
|
+
{ name: 'field', type: 'String' },
|
|
179
|
+
],
|
|
180
|
+
'f64',
|
|
181
|
+
['ecs.read'],
|
|
182
|
+
true,
|
|
183
|
+
'One numeric field. Reading a field of a component an entity does not have is 0.',
|
|
184
|
+
),
|
|
185
|
+
define(
|
|
186
|
+
ECS_MODULE,
|
|
187
|
+
'write',
|
|
188
|
+
[
|
|
189
|
+
{ name: 'world', type: 'World' },
|
|
190
|
+
{ name: 'entity', type: 'Entity' },
|
|
191
|
+
{ name: 'component', type: 'String' },
|
|
192
|
+
{ name: 'field', type: 'String' },
|
|
193
|
+
{ name: 'value', type: 'f64' },
|
|
194
|
+
],
|
|
195
|
+
'void',
|
|
196
|
+
['ecs.write'],
|
|
197
|
+
false,
|
|
198
|
+
'Set one numeric field.',
|
|
199
|
+
),
|
|
200
|
+
define(
|
|
201
|
+
ECS_MODULE,
|
|
202
|
+
'count',
|
|
203
|
+
[
|
|
204
|
+
{ name: 'world', type: 'World' },
|
|
205
|
+
{ name: 'component', type: 'String' },
|
|
206
|
+
],
|
|
207
|
+
'u32',
|
|
208
|
+
['ecs.read'],
|
|
209
|
+
true,
|
|
210
|
+
'How many entities carry a component.',
|
|
211
|
+
),
|
|
212
|
+
define(
|
|
213
|
+
ECS_MODULE,
|
|
214
|
+
'at',
|
|
215
|
+
[
|
|
216
|
+
{ name: 'world', type: 'World' },
|
|
217
|
+
{ name: 'component', type: 'String' },
|
|
218
|
+
{ name: 'index', type: 'u32' },
|
|
219
|
+
],
|
|
220
|
+
'Entity',
|
|
221
|
+
['ecs.read'],
|
|
222
|
+
true,
|
|
223
|
+
'The nth entity carrying a component. **The order is insertion modified by swap-removal**, so a walk that removes should count downwards — removing moves the last entity into a position already passed.',
|
|
224
|
+
),
|
|
225
|
+
/*
|
|
226
|
+
* **Two capabilities rather than one, and the split is what `Entity` cost.**
|
|
227
|
+
*
|
|
228
|
+
* A search that finds nothing has to say so, and once the answer is an `Entity` there is no value
|
|
229
|
+
* left to say it with: a handle is not a number a script can compare to −1, which is the whole
|
|
230
|
+
* point of it being its own type. An option would say it and allocates one object per call, on a
|
|
231
|
+
* path written to run every frame for every agent that looks around — the allocation `next`
|
|
232
|
+
* refuses one screen up.
|
|
233
|
+
*
|
|
234
|
+
* So the result is read back through a second accessor, which is exactly what `raycast` and its
|
|
235
|
+
* `hitBody` already do in this repository and for the same reason. **The cost is the same too**:
|
|
236
|
+
* the result is only valid until the next search on the same world, and that is said here and on
|
|
237
|
+
* the accessor rather than left for somebody to find out.
|
|
238
|
+
*/
|
|
239
|
+
define(
|
|
240
|
+
ECS_MODULE,
|
|
241
|
+
'findNearest',
|
|
242
|
+
[
|
|
243
|
+
{ name: 'world', type: 'World' },
|
|
244
|
+
{ name: 'component', type: 'String' },
|
|
245
|
+
{ name: 'fieldX', type: 'String' },
|
|
246
|
+
{ name: 'fieldY', type: 'String' },
|
|
247
|
+
{ name: 'fieldZ', type: 'String' },
|
|
248
|
+
{ name: 'x', type: 'float' },
|
|
249
|
+
{ name: 'y', type: 'float' },
|
|
250
|
+
{ name: 'z', type: 'float' },
|
|
251
|
+
{ name: 'radius', type: 'float' },
|
|
252
|
+
],
|
|
253
|
+
'bool',
|
|
254
|
+
['ecs.read'],
|
|
255
|
+
true,
|
|
256
|
+
"Whether any entity carrying a component has position fields putting it within a radius, and the search `nearest` then reads. **A linear scan of that component's column, not an index** — the entity model keeps no spatial structure, so this costs one pass over everything carrying the component. Position is read from three fields you name, so nothing here knows what a transform is called.",
|
|
257
|
+
),
|
|
258
|
+
define(
|
|
259
|
+
ECS_MODULE,
|
|
260
|
+
'nearest',
|
|
261
|
+
[{ name: 'world', type: 'World' }],
|
|
262
|
+
'Entity',
|
|
263
|
+
['ecs.read'],
|
|
264
|
+
true,
|
|
265
|
+
'The entity the last `findNearest` on this world found. **Valid only until the next one**, and meaningless when that answered false — the same lifetime a raycast hit has.',
|
|
266
|
+
),
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The four a query loop is built on.
|
|
271
|
+
*
|
|
272
|
+
* **Nobody writes these by hand.** They are what `for e in query<…>() { … }` compiles to, and a
|
|
273
|
+
* script author never names one — which is why their docs describe the shape of the generated code
|
|
274
|
+
* rather than how to call them. They are capabilities rather than a second bind hook so that
|
|
275
|
+
* generated code reaches them exactly as it reaches everything else, through `__bind($host)`.
|
|
276
|
+
*/
|
|
277
|
+
export const QUERY_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
278
|
+
define(
|
|
279
|
+
ECS_MODULE,
|
|
280
|
+
'query',
|
|
281
|
+
[
|
|
282
|
+
{ name: 'world', type: 'World' },
|
|
283
|
+
{ name: 'a', type: 'String' },
|
|
284
|
+
{ name: 'b', type: 'String' },
|
|
285
|
+
{ name: 'c', type: 'String' },
|
|
286
|
+
{ name: 'd', type: 'String' },
|
|
287
|
+
],
|
|
288
|
+
'Cursor',
|
|
289
|
+
['ecs.read'],
|
|
290
|
+
true,
|
|
291
|
+
'Open a walk over everything carrying these components. The cursor is borrowed from the world’s pool; ending the walk gives it back.',
|
|
292
|
+
),
|
|
293
|
+
define(
|
|
294
|
+
ECS_MODULE,
|
|
295
|
+
'without',
|
|
296
|
+
[
|
|
297
|
+
{ name: 'cursor', type: 'Cursor' },
|
|
298
|
+
{ name: 'component', type: 'String' },
|
|
299
|
+
],
|
|
300
|
+
'void',
|
|
301
|
+
['ecs.read'],
|
|
302
|
+
true,
|
|
303
|
+
'Narrow an open walk to entities that do **not** carry a component.',
|
|
304
|
+
),
|
|
305
|
+
define(
|
|
306
|
+
ECS_MODULE,
|
|
307
|
+
'next',
|
|
308
|
+
[{ name: 'cursor', type: 'Cursor' }],
|
|
309
|
+
'Entity',
|
|
310
|
+
['ecs.read'],
|
|
311
|
+
true,
|
|
312
|
+
'The next entity, or a **negative** number when the walk is done — which also gives the cursor back. Negative rather than zero, because zero is a legal handle.',
|
|
313
|
+
),
|
|
314
|
+
define(
|
|
315
|
+
ECS_MODULE,
|
|
316
|
+
'view',
|
|
317
|
+
[
|
|
318
|
+
{ name: 'world', type: 'World' },
|
|
319
|
+
{ name: 'component', type: 'String' },
|
|
320
|
+
{ name: 'forWriting', type: 'bool' },
|
|
321
|
+
],
|
|
322
|
+
'View',
|
|
323
|
+
['ecs.read'],
|
|
324
|
+
true,
|
|
325
|
+
'A component’s live columns. `forWriting` decides which of a system’s declarations is checked — a loop that only reads takes a readable view, and asking for more than the system declared is refused.',
|
|
326
|
+
),
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
export const PREFAB_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
330
|
+
define(
|
|
331
|
+
PREFAB_MODULE,
|
|
332
|
+
'spawn',
|
|
333
|
+
[
|
|
334
|
+
{ name: 'world', type: 'World' },
|
|
335
|
+
{ name: 'prefab', type: 'String' },
|
|
336
|
+
],
|
|
337
|
+
'Entity',
|
|
338
|
+
['ecs.write'],
|
|
339
|
+
false,
|
|
340
|
+
'Make one, with every component the prefab names. Overrides are the host’s to apply; a script that wants a different value writes it after.',
|
|
341
|
+
),
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
/** What a host supplies so a name can become a component type or a prefab. */
|
|
345
|
+
export interface EntityServices {
|
|
346
|
+
/**
|
|
347
|
+
* Component types by the name a script writes.
|
|
348
|
+
*
|
|
349
|
+
* There is no world here: every capability takes the world as a parameter, because a consumer
|
|
350
|
+
* with two worlds — a simulation and a preview — would otherwise need two hosts.
|
|
351
|
+
*/
|
|
352
|
+
readonly components: ReadonlyMap<string, ComponentType>;
|
|
353
|
+
/** Prefabs by name. Absent when the host has none, which makes `drift/prefab` refuse in words. */
|
|
354
|
+
readonly prefabs?: ReadonlyMap<string, Prefab>;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function entitiesImplementation(services: EntityServices): Record<string, unknown> {
|
|
358
|
+
const { components } = services;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Resolve a component name, or refuse naming what the world has.
|
|
362
|
+
*
|
|
363
|
+
* **Not `undefined` and not a no-op.** A script that misspells a component would otherwise read
|
|
364
|
+
* zeroes and write into nothing, which is a behaviour that runs, reports success and is wrong —
|
|
365
|
+
* the silent no-op this repository forbids, arriving through a string.
|
|
366
|
+
*/
|
|
367
|
+
const resolve = (name: string): ComponentType => {
|
|
368
|
+
const type = components.get(name);
|
|
369
|
+
if (type !== undefined) return type;
|
|
370
|
+
throw new Error(
|
|
371
|
+
`no component is registered as \`${name}\`. This host has ` +
|
|
372
|
+
`${[...components.keys()].map((k) => `\`${k}\``).join(', ')}. A script naming one that ` +
|
|
373
|
+
'does not exist would otherwise read zeroes and write into nothing.',
|
|
374
|
+
);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/*
|
|
378
|
+
* The last search's result, per world, held **here rather than on the world**.
|
|
379
|
+
*
|
|
380
|
+
* A script cannot receive a record, so the result is read back through an accessor — and putting
|
|
381
|
+
* the state in the binding keeps `World` free of a field that exists only because of a language
|
|
382
|
+
* limitation. A `WeakMap` rather than one shared slot, because two worlds are two questions and a
|
|
383
|
+
* shared slot would let one answer the other's. The same arrangement `physicsImplementation` makes
|
|
384
|
+
* for a raycast hit, for the same reasons.
|
|
385
|
+
*/
|
|
386
|
+
const found = new WeakMap<object, number>();
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
create: (w: World) => w.create(),
|
|
390
|
+
destroy: (w: World, entity: Entity) => w.destroy(entity),
|
|
391
|
+
alive: (w: World, entity: Entity) => w.alive(entity),
|
|
392
|
+
has: (w: World, entity: Entity, component: string) => w.has(entity, resolve(component)),
|
|
393
|
+
attach: (w: World, entity: Entity, component: string) => w.add(entity, resolve(component)),
|
|
394
|
+
detach: (w: World, entity: Entity, component: string) => w.remove(entity, resolve(component)),
|
|
395
|
+
read: (w: World, entity: Entity, component: string, field: string) => {
|
|
396
|
+
const value = w.read(entity, resolve(component), field);
|
|
397
|
+
/* An entity without the component reads `undefined`, and a script's `f64` may not hold one:
|
|
398
|
+
the language has no null, so the honest zero is what a missing value is. */
|
|
399
|
+
return typeof value === 'number' ? value : 0;
|
|
400
|
+
},
|
|
401
|
+
write: (w: World, entity: Entity, component: string, field: string, value: number) =>
|
|
402
|
+
w.write(entity, resolve(component), field, value),
|
|
403
|
+
/*
|
|
404
|
+
* A query opens against whatever the caller passed as the world.
|
|
405
|
+
*
|
|
406
|
+
* **A `SystemView` and a `World` both satisfy this**, structurally, and that is the point: a
|
|
407
|
+
* loop inside a system runs against the view the schedule handed it, so every declaration
|
|
408
|
+
* check the engine makes still applies — and the same generated code runs against a bare world
|
|
409
|
+
* where there is no system to declare anything.
|
|
410
|
+
*/
|
|
411
|
+
query: (w: QuerySource, a: string, b?: string, c?: string, d?: string) =>
|
|
412
|
+
w.query(
|
|
413
|
+
resolve(a),
|
|
414
|
+
b === undefined ? undefined : resolve(b),
|
|
415
|
+
c === undefined ? undefined : resolve(c),
|
|
416
|
+
d === undefined ? undefined : resolve(d),
|
|
417
|
+
),
|
|
418
|
+
without: (cursor: QueryCursor, component: string) => {
|
|
419
|
+
cursor.without(resolve(component));
|
|
420
|
+
},
|
|
421
|
+
/*
|
|
422
|
+
* A negative ends the walk, and zero would not: zero is the handle of the first entity a fresh
|
|
423
|
+
* world ever allocates. Nothing is allocated to say "done" — a result record per step is the
|
|
424
|
+
* per-frame allocation the whole design refuses.
|
|
425
|
+
*/
|
|
426
|
+
next: (cursor: QueryCursor) => {
|
|
427
|
+
const step = cursor.next();
|
|
428
|
+
return step.done ? -1 : (step.value as number);
|
|
429
|
+
},
|
|
430
|
+
view: (w: QuerySource, component: string, forWriting: boolean) =>
|
|
431
|
+
w.view(resolve(component), forWriting),
|
|
432
|
+
count: (w: World, component: string) => w.store(resolve(component)).size,
|
|
433
|
+
at: (w: World, component: string, index: number) => {
|
|
434
|
+
const store = w.store(resolve(component));
|
|
435
|
+
return index < store.size ? (store.dense[index] as number) : 0;
|
|
436
|
+
},
|
|
437
|
+
/*
|
|
438
|
+
* **A boolean rather than an option, and a second accessor for the handle.** `Sound?` is an
|
|
439
|
+
* option because a slot is resolved once at load; this is written to be called every frame by
|
|
440
|
+
* every agent that looks around, and `some(entity)` is one object per call — the same reasoning
|
|
441
|
+
* `next` gives one screen up. A sentinel is not available either: the result is an `Entity`, and
|
|
442
|
+
* a handle is not a number a script can compare to −1, which is the point of it having its own
|
|
443
|
+
* type. So the search answers whether it found anything and `nearest` reads what it found, the
|
|
444
|
+
* shape `raycast` and `hitBody` already have here.
|
|
445
|
+
*
|
|
446
|
+
* **Compared squared, so nothing takes a square root**, and the scan walks the dense columns
|
|
447
|
+
* directly: a store's dense position *is* its column index, so neither `sparse` nor a per-entity
|
|
448
|
+
* `read` is on this path. One pass, no allocation, no call per entity.
|
|
449
|
+
*
|
|
450
|
+
* A field this component does not have reads as absent rather than throwing, and an entity with
|
|
451
|
+
* no position contributes nothing — the honest answer for a component whose author named the
|
|
452
|
+
* wrong field is "nothing was near", reported by finding nothing, rather than a frame-loop
|
|
453
|
+
* throw that `AGENTS.md` forbids.
|
|
454
|
+
*/
|
|
455
|
+
findNearest: (
|
|
456
|
+
w: QuerySource,
|
|
457
|
+
component: string,
|
|
458
|
+
fieldX: string,
|
|
459
|
+
fieldY: string,
|
|
460
|
+
fieldZ: string,
|
|
461
|
+
x: number,
|
|
462
|
+
y: number,
|
|
463
|
+
z: number,
|
|
464
|
+
radius: number,
|
|
465
|
+
) => {
|
|
466
|
+
const type = resolve(component);
|
|
467
|
+
const view = w.view(type, false);
|
|
468
|
+
const columnX = view[fieldX];
|
|
469
|
+
const columnY = view[fieldY];
|
|
470
|
+
const columnZ = view[fieldZ];
|
|
471
|
+
if (columnX === undefined || columnY === undefined || columnZ === undefined) {
|
|
472
|
+
/* A component whose author named the wrong field answers "nothing was near" rather than
|
|
473
|
+
throwing: a frame loop may not throw, and the boolean already carries that answer. */
|
|
474
|
+
found.set(w, -1);
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/*
|
|
479
|
+
* **A cursor rather than the store's dense array, because `world` is not always a `World`.**
|
|
480
|
+
*
|
|
481
|
+
* Inside a system the value a script calls this with is the `SystemView` the schedule handed
|
|
482
|
+
* it, which carries `query` and `view` and no `store` — the same structural pair `query` and
|
|
483
|
+
* `view` above are typed against. A first version read `store.size` and `store.dense`, which
|
|
484
|
+
* works from a bare world and throws `w.store is not a function` the moment a system calls it.
|
|
485
|
+
* Going through the cursor is also what keeps a system's declared reads doing their job.
|
|
486
|
+
*
|
|
487
|
+
* The walk always runs to exhaustion, which is what gives the cursor back: it comes from a
|
|
488
|
+
* pool and is returned when `next` reports done, so an early exit on finding something close
|
|
489
|
+
* enough would leak one per call.
|
|
490
|
+
*/
|
|
491
|
+
const cursor = w.query(type);
|
|
492
|
+
const limit = radius * radius;
|
|
493
|
+
let best = -1;
|
|
494
|
+
let bestDistance = Infinity;
|
|
495
|
+
for (;;) {
|
|
496
|
+
const step = cursor.next();
|
|
497
|
+
if (step.done === true) break;
|
|
498
|
+
const entity = step.value as number;
|
|
499
|
+
const at = view.sparse[entity % ENTITY_INDEX_CEILING] as number;
|
|
500
|
+
const dx = (columnX[at] as number) - x;
|
|
501
|
+
const dy = (columnY[at] as number) - y;
|
|
502
|
+
const dz = (columnZ[at] as number) - z;
|
|
503
|
+
const distance = dx * dx + dy * dy + dz * dz;
|
|
504
|
+
if (distance > limit || distance >= bestDistance) continue;
|
|
505
|
+
bestDistance = distance;
|
|
506
|
+
best = entity;
|
|
507
|
+
}
|
|
508
|
+
found.set(w, best);
|
|
509
|
+
return best >= 0;
|
|
510
|
+
},
|
|
511
|
+
/*
|
|
512
|
+
* The handle the last search found, or zero when it found nothing.
|
|
513
|
+
*
|
|
514
|
+
* **Zero rather than a refusal**, because a frame loop may not throw and a script that read this
|
|
515
|
+
* without testing has a bug in its own logic rather than a broken host. Zero is a legal handle,
|
|
516
|
+
* so it is not a sentinel and is not documented as one — the honest test is the boolean, which
|
|
517
|
+
* is why the search answers one.
|
|
518
|
+
*/
|
|
519
|
+
nearest: (w: QuerySource) => Math.max(0, found.get(w) ?? -1),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* What a query needs from whatever it was handed as a world.
|
|
525
|
+
*
|
|
526
|
+
* **A `SystemView` and a `World` both satisfy it**, which is what lets one piece of generated code
|
|
527
|
+
* run inside a system and outside one. Inside, every call goes through the system's declarations;
|
|
528
|
+
* outside, there are none to go through. Neither is named here, because naming one would make this
|
|
529
|
+
* file choose.
|
|
530
|
+
*/
|
|
531
|
+
interface QuerySource {
|
|
532
|
+
query(a: ComponentType, b?: ComponentType, c?: ComponentType, d?: ComponentType): QueryCursor;
|
|
533
|
+
view(type: ComponentType, forWriting: boolean): ComponentView;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function prefabImplementation(services: EntityServices): Record<string, unknown> {
|
|
537
|
+
const prefabs = services.prefabs ?? new Map<string, Prefab>();
|
|
538
|
+
return {
|
|
539
|
+
spawn: (w: World, name: string) => {
|
|
540
|
+
const prefab = prefabs.get(name);
|
|
541
|
+
if (prefab === undefined) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`no prefab is registered as \`${name}\`. This host has ` +
|
|
544
|
+
`${[...prefabs.keys()].map((k) => `\`${k}\``).join(', ')}.`,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return instantiate(w, prefab);
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|