@typeonce/ritmo 0.7.0 → 0.9.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/README.md +3 -2
- package/dist/Clock.d.ts +19 -4
- package/dist/Clock.d.ts.map +1 -1
- package/dist/Clock.js +18 -2
- package/dist/Clock.js.map +1 -1
- package/dist/Command.d.ts +181 -40
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +31 -11
- package/dist/Command.js.map +1 -1
- package/dist/Condition.d.ts +12 -1
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Debug.d.ts +127 -13
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Descriptor.d.ts +61 -10
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +22 -10
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +22 -7
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js.map +1 -1
- package/dist/EntityScope.d.ts +20 -1
- package/dist/EntityScope.d.ts.map +1 -1
- package/dist/EntityScope.js +8 -2
- package/dist/EntityScope.js.map +1 -1
- package/dist/FixedLoop.d.ts +106 -0
- package/dist/FixedLoop.d.ts.map +1 -0
- package/dist/FixedLoop.js +97 -0
- package/dist/FixedLoop.js.map +1 -0
- package/dist/HostClock.d.ts +1 -1
- package/dist/HostClock.d.ts.map +1 -1
- package/dist/HostClock.js +2 -1
- package/dist/HostClock.js.map +1 -1
- package/dist/Machine.d.ts +59 -34
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +10 -3
- package/dist/Machine.js.map +1 -1
- package/dist/Query.d.ts +100 -14
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +34 -5
- package/dist/Query.js.map +1 -1
- package/dist/Relation.d.ts +5 -14
- package/dist/Relation.d.ts.map +1 -1
- package/dist/Relation.js +0 -32
- package/dist/Relation.js.map +1 -1
- package/dist/Runtime.d.ts +61 -15
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +594 -70
- package/dist/Runtime.js.map +1 -1
- package/dist/Schedule.d.ts +24 -15
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +2 -1
- package/dist/Schedule.js.map +1 -1
- package/dist/Schema.d.ts +233 -48
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +33 -7
- package/dist/Schema.js.map +1 -1
- package/dist/Snapshot.d.ts +90 -1
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +42 -1
- package/dist/Snapshot.js.map +1 -1
- package/dist/System.d.ts +132 -31
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js +32 -0
- package/dist/System.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +2 -0
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +34 -8
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/debug.d.ts +10 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +171 -12
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/equal.d.ts +8 -0
- package/dist/internal/equal.d.ts.map +1 -0
- package/dist/internal/equal.js +38 -0
- package/dist/internal/equal.js.map +1 -0
- package/dist/internal/fragments.d.ts +2 -6
- package/dist/internal/fragments.d.ts.map +1 -1
- package/dist/internal/fragments.js +36 -3
- package/dist/internal/fragments.js.map +1 -1
- package/dist/internal/game.d.ts +2 -0
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +48 -7
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/order.d.ts.map +1 -1
- package/dist/internal/order.js +4 -2
- package/dist/internal/order.js.map +1 -1
- package/dist/internal/queries.d.ts.map +1 -1
- package/dist/internal/queries.js +57 -26
- package/dist/internal/queries.js.map +1 -1
- package/dist/internal/relations.d.ts +10 -0
- package/dist/internal/relations.d.ts.map +1 -0
- package/dist/internal/relations.js +32 -0
- package/dist/internal/relations.js.map +1 -0
- package/dist/internal/requirements.d.ts +26 -0
- package/dist/internal/requirements.d.ts.map +1 -0
- package/dist/internal/requirements.js +46 -0
- package/dist/internal/requirements.js.map +1 -0
- package/dist/internal/streams.d.ts +2 -0
- package/dist/internal/streams.d.ts.map +1 -1
- package/dist/internal/streams.js +10 -0
- package/dist/internal/streams.js.map +1 -1
- package/dist/internal/world.d.ts +7 -1
- package/dist/internal/world.d.ts.map +1 -1
- package/dist/internal/world.js +53 -3
- package/dist/internal/world.js.map +1 -1
- package/package.json +1 -1
- package/src/Clock.ts +29 -4
- package/src/Command.ts +281 -67
- package/src/Condition.ts +17 -1
- package/src/Debug.ts +124 -15
- package/src/Descriptor.ts +92 -21
- package/src/Entity.ts +23 -7
- package/src/EntityScope.ts +21 -2
- package/src/FixedLoop.ts +218 -0
- package/src/HostClock.ts +2 -1
- package/src/Machine.ts +72 -35
- package/src/Query.ts +118 -17
- package/src/Relation.ts +5 -67
- package/src/Runtime.ts +659 -85
- package/src/Schedule.ts +31 -19
- package/src/Schema.ts +297 -57
- package/src/Snapshot.ts +94 -2
- package/src/System.ts +151 -29
- package/src/index.ts +4 -0
- package/src/internal/cells.ts +42 -10
- package/src/internal/debug.ts +179 -14
- package/src/internal/equal.ts +31 -0
- package/src/internal/fragments.ts +35 -5
- package/src/internal/game.ts +69 -7
- package/src/internal/order.ts +8 -4
- package/src/internal/queries.ts +62 -12
- package/src/internal/relations.ts +52 -0
- package/src/internal/requirements.ts +71 -0
- package/src/internal/streams.ts +11 -0
- package/src/internal/world.ts +60 -4
package/src/Command.ts
CHANGED
|
@@ -65,7 +65,8 @@ import * as Entity from "./Entity.ts"
|
|
|
65
65
|
import type * as EntityScope from "./EntityScope.ts"
|
|
66
66
|
import type * as Relation from "./Relation.ts"
|
|
67
67
|
import * as Result from "./Result.ts"
|
|
68
|
-
import type {
|
|
68
|
+
import type { ReadonlyValue } from "./Query.ts"
|
|
69
|
+
import type { RequirementPair, Schema } from "./Schema.ts"
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
72
|
* Type-level helpers for staged entity construction.
|
|
@@ -102,7 +103,7 @@ export namespace Draft {
|
|
|
102
103
|
/**
|
|
103
104
|
* A typed descriptor/value pair used by the flat command authoring APIs.
|
|
104
105
|
*/
|
|
105
|
-
export type Entry<D extends Descriptor<"component", string, any>> = readonly [D, Descriptor.Value<D
|
|
106
|
+
export type Entry<D extends Descriptor<"component", string, any>> = readonly [D, ReadonlyValue<Descriptor.Value<D>>]
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
109
|
* Extracts the component-descriptor union from a schema.
|
|
@@ -142,15 +143,21 @@ type EntryValue<T> =
|
|
|
142
143
|
export type FoldEntries<
|
|
143
144
|
Entries extends ReadonlyArray<SpawnEntry<any>>,
|
|
144
145
|
P extends Entity.ComponentProof = {}
|
|
145
|
-
> =
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
> = Entries extends readonly [
|
|
147
|
+
infer Head extends SpawnEntry<any>,
|
|
148
|
+
...infer Tail extends ReadonlyArray<SpawnEntry<any>>
|
|
149
|
+
]
|
|
150
|
+
? FoldEntries<Tail, Draft.InsertEntry<P, EntryValue<Head>>>
|
|
151
|
+
// Fixed entries after a spread are there too.
|
|
148
152
|
: Entries extends readonly [
|
|
149
|
-
infer
|
|
150
|
-
|
|
153
|
+
...infer Init extends ReadonlyArray<SpawnEntry<any>>,
|
|
154
|
+
infer Last extends SpawnEntry<any>
|
|
151
155
|
]
|
|
152
|
-
? FoldEntries<
|
|
153
|
-
:
|
|
156
|
+
? FoldEntries<Init, Draft.InsertEntry<P, EntryValue<Last>>>
|
|
157
|
+
: number extends Entries["length"]
|
|
158
|
+
// A widened array (a spread of entries built at runtime): each entry may or may not be there.
|
|
159
|
+
? P & MaybeEntries<EntryValue<Entries[number]>>
|
|
160
|
+
: P
|
|
154
161
|
|
|
155
162
|
/** The proof of entries that may each be present: every component they can hold, optional. */
|
|
156
163
|
type MaybeEntries<Entry> = {
|
|
@@ -175,10 +182,11 @@ export type DraftFor<
|
|
|
175
182
|
S extends Schema.Any,
|
|
176
183
|
Entries extends ReadonlyArray<SpawnEntry<S>>,
|
|
177
184
|
P extends Entity.ComponentProof,
|
|
178
|
-
Root
|
|
185
|
+
Root,
|
|
186
|
+
Relates = never
|
|
179
187
|
> = [Extract<Entries[number], { readonly ok: boolean }>] extends [never]
|
|
180
|
-
? Entity.EntityDraft<S, FoldEntries<Entries, P>, Root>
|
|
181
|
-
: Result.Result<Entity.EntityDraft<S, FoldEntries<Entries, P>, Root>, EntryErrors<Entries>>
|
|
188
|
+
? Entity.EntityDraft<S, FoldEntries<Entries, P>, Root, Relates>
|
|
189
|
+
: Result.Result<Entity.EntityDraft<S, FoldEntries<Entries, P>, Root, Relates>, EntryErrors<Entries>>
|
|
182
190
|
|
|
183
191
|
/**
|
|
184
192
|
* A deferred world mutation.
|
|
@@ -206,9 +214,10 @@ export type DeferredCommand<S extends Schema.Any> = {
|
|
|
206
214
|
export interface InternalWorld<S extends Schema.Any> {
|
|
207
215
|
/**
|
|
208
216
|
* Creates the entity for an id reserved by `commands.spawn(...)` with its
|
|
209
|
-
* staged components.
|
|
217
|
+
* staged components. Returns `false`, creating nothing, when the
|
|
218
|
+
* components break a `Schema.requires` requirement.
|
|
210
219
|
*/
|
|
211
|
-
readonly spawnEntity: (id: Entity.EntityId<S, any>, components: ReadonlyArray<Entity.StagedComponent>) =>
|
|
220
|
+
readonly spawnEntity: (id: Entity.EntityId<S, any>, components: ReadonlyArray<Entity.StagedComponent>) => boolean
|
|
212
221
|
/**
|
|
213
222
|
* Removes an entity and its outgoing and incoming relations.
|
|
214
223
|
*/
|
|
@@ -225,13 +234,21 @@ export interface InternalWorld<S extends Schema.Any> {
|
|
|
225
234
|
*/
|
|
226
235
|
readonly destroyEntityScope: (scope: EntityScope.EntityScope.Any) => void
|
|
227
236
|
/**
|
|
228
|
-
* Removes
|
|
237
|
+
* Removes components from a live entity, all or none: nothing is removed
|
|
238
|
+
* when that would leave a component without one it requires.
|
|
239
|
+
*/
|
|
240
|
+
readonly removeComponents: (id: Entity.EntityId<S, any>, descriptors: ReadonlyArray<Descriptor<"component", string, any>>) => void
|
|
241
|
+
/**
|
|
242
|
+
* Inserts or replaces components on a live entity, all or none: nothing is
|
|
243
|
+
* written when an inserted component would miss one it requires.
|
|
229
244
|
*/
|
|
230
|
-
readonly
|
|
245
|
+
readonly insertComponents: (id: Entity.EntityId<S, any>, entries: ReadonlyArray<Entity.StagedComponent>) => void
|
|
231
246
|
/**
|
|
232
|
-
*
|
|
247
|
+
* Reports a `despawn` or `remove` given a numeric id this run did not
|
|
248
|
+
* spawn: an id kept from an earlier run, or a raw id that erased types let
|
|
249
|
+
* through. The command is skipped.
|
|
233
250
|
*/
|
|
234
|
-
readonly
|
|
251
|
+
readonly reportStaleTarget: (command: "despawn" | "remove", id: Entity.EntityId<S, any>) => void
|
|
235
252
|
/**
|
|
236
253
|
* Attempts to attach one relation edge between two live entities.
|
|
237
254
|
*/
|
|
@@ -267,12 +284,13 @@ const unspawnedId = Entity.makeEntityId<any, any>(-1)
|
|
|
267
284
|
*/
|
|
268
285
|
export const entry = <D extends Descriptor<"component", string, any>>(
|
|
269
286
|
descriptor: D,
|
|
270
|
-
value: Descriptor.Value<D
|
|
287
|
+
value: ReadonlyValue<Descriptor.Value<D>>
|
|
271
288
|
): Entry<D> => [descriptor, value]
|
|
272
289
|
|
|
273
290
|
/**
|
|
274
291
|
* Validates raw input through a constructed descriptor's own validator and
|
|
275
|
-
* returns an entry result for `spawn(...)` / `
|
|
292
|
+
* returns an entry result for `Game.Command.spawn(...)` / `extend(...)`.
|
|
293
|
+
* `commands.insert` takes valid entries only: check the result first.
|
|
276
294
|
*
|
|
277
295
|
* This is the one way to put unvalidated data on an entity: the descriptor
|
|
278
296
|
* owns its validator (`Descriptor.ConstructedComponent(...)`, built with
|
|
@@ -313,12 +331,13 @@ export const extend = <
|
|
|
313
331
|
S extends Schema.Any,
|
|
314
332
|
P extends Entity.ComponentProof,
|
|
315
333
|
Root,
|
|
316
|
-
const Entries extends ReadonlyArray<SpawnEntry<S
|
|
334
|
+
const Entries extends ReadonlyArray<SpawnEntry<S>>,
|
|
335
|
+
Relates = never
|
|
317
336
|
>(
|
|
318
|
-
draft: Entity.EntityDraft<S, P, Root>,
|
|
337
|
+
draft: Entity.EntityDraft<S, P, Root, Relates>,
|
|
319
338
|
...entries: Entries
|
|
320
|
-
): DraftFor<S, Entries, P, Root> =>
|
|
321
|
-
build(draft.id, { ...draft.proof }, [...draft.components], draft.relations, entries) as DraftFor<S, Entries, P, Root>
|
|
339
|
+
): DraftFor<S, Entries, P, Root, Relates> =>
|
|
340
|
+
build(draft.id, { ...draft.proof }, [...draft.components], draft.relations, entries) as DraftFor<S, Entries, P, Root, Relates>
|
|
322
341
|
|
|
323
342
|
/**
|
|
324
343
|
* Shared draft builder. Plain entries take a fast path; the first `Result`
|
|
@@ -364,7 +383,7 @@ const build = (
|
|
|
364
383
|
*
|
|
365
384
|
* Drafts are plain values: build them anywhere (including small factories),
|
|
366
385
|
* then queue them with `commands.spawn(...)`. Entries may be `[Descriptor,
|
|
367
|
-
* value]` pairs or results from `entryRaw(...)`; see `
|
|
386
|
+
* value]` pairs or results from `entryRaw(...)`; see `extend(...)` for how
|
|
368
387
|
* failures are reported.
|
|
369
388
|
*
|
|
370
389
|
* @example
|
|
@@ -396,12 +415,13 @@ export const relate = <
|
|
|
396
415
|
S extends Schema.Any,
|
|
397
416
|
P extends Entity.ComponentProof,
|
|
398
417
|
R extends Relation.Relation.Any,
|
|
399
|
-
Root = unknown
|
|
418
|
+
Root = unknown,
|
|
419
|
+
Relates = never
|
|
400
420
|
>(
|
|
401
|
-
draft: Entity.EntityDraft<S, P, Root>,
|
|
421
|
+
draft: Entity.EntityDraft<S, P, Root, Relates>,
|
|
402
422
|
relation: R,
|
|
403
423
|
target: Entity.EntityId<S, Root>
|
|
404
|
-
): Entity.EntityDraft<S, P, Root> =>
|
|
424
|
+
): Entity.EntityDraft<S, P, Root, Relates | R> =>
|
|
405
425
|
Entity.draft(draft.id, draft.proof, draft.components, [
|
|
406
426
|
...draft.relations,
|
|
407
427
|
{
|
|
@@ -424,7 +444,7 @@ type ComponentDescriptor = Descriptor<"component", string, any>
|
|
|
424
444
|
* Allows `spawn` / `spawnIn` of drafts whose components are all among `components`.
|
|
425
445
|
* Use it for entities the system creates; use `extend` on a draft to build one.
|
|
426
446
|
*/
|
|
427
|
-
export const spawns = <const Components extends ReadonlyArray<ComponentDescriptor
|
|
447
|
+
export const spawns = <const Components extends readonly [ComponentDescriptor, ...ReadonlyArray<ComponentDescriptor>]>(...components: Components): Permission<"spawns", Components[number]> =>
|
|
428
448
|
({ permission: "spawns", values: components })
|
|
429
449
|
|
|
430
450
|
/**
|
|
@@ -432,11 +452,11 @@ export const spawns = <const Components extends ReadonlyArray<ComponentDescripto
|
|
|
432
452
|
* replace at the next `applyDeferred()`. To change a value now, on an entity
|
|
433
453
|
* you matched, write through a query (`Game.Query.write`) instead.
|
|
434
454
|
*/
|
|
435
|
-
export const inserts = <const Components extends ReadonlyArray<ComponentDescriptor
|
|
455
|
+
export const inserts = <const Components extends readonly [ComponentDescriptor, ...ReadonlyArray<ComponentDescriptor>]>(...components: Components): Permission<"inserts", Components[number]> =>
|
|
436
456
|
({ permission: "inserts", values: components })
|
|
437
457
|
|
|
438
458
|
/** Allows `remove(entity, X)` with `X` among `components`. */
|
|
439
|
-
export const removes = <const Components extends ReadonlyArray<ComponentDescriptor
|
|
459
|
+
export const removes = <const Components extends readonly [ComponentDescriptor, ...ReadonlyArray<ComponentDescriptor>]>(...components: Components): Permission<"removes", Components[number]> =>
|
|
440
460
|
({ permission: "removes", values: components })
|
|
441
461
|
|
|
442
462
|
/**
|
|
@@ -444,7 +464,7 @@ export const removes = <const Components extends ReadonlyArray<ComponentDescript
|
|
|
444
464
|
* of `components` (`match.entity`), never a raw id: the system can only
|
|
445
465
|
* despawn what it has proven is, for example, an `Enemy`.
|
|
446
466
|
*/
|
|
447
|
-
export const despawns = <const Components extends ReadonlyArray<ComponentDescriptor
|
|
467
|
+
export const despawns = <const Components extends readonly [ComponentDescriptor, ...ReadonlyArray<ComponentDescriptor>]>(...components: Components): Permission<"despawns", Components[number]> =>
|
|
448
468
|
({ permission: "despawns", values: components })
|
|
449
469
|
|
|
450
470
|
/** Allows `despawnScope(scope)` for this scope. */
|
|
@@ -452,7 +472,7 @@ export const despawnsScope = <const Scope extends EntityScope.EntityScope.Any>(s
|
|
|
452
472
|
({ permission: "despawnsScope", values: [scope] })
|
|
453
473
|
|
|
454
474
|
/** Allows `relate`, `unrelate`, and `reorderChildren` for these relations. */
|
|
455
|
-
export const relates = <const Relations extends ReadonlyArray<Relation.Relation.Any
|
|
475
|
+
export const relates = <const Relations extends readonly [Relation.Relation.Any, ...ReadonlyArray<Relation.Relation.Any>]>(...relations: Relations): Permission<"relates", Relations[number]> =>
|
|
456
476
|
({ permission: "relates", values: relations })
|
|
457
477
|
|
|
458
478
|
type PermissionValue<Permissions, Kind extends PermissionKind> =
|
|
@@ -507,6 +527,12 @@ type Names<D> = D extends { readonly name: infer Name extends string } ? Name :
|
|
|
507
527
|
/** Every key of every member of a union of proofs (a draft with a conditional entry is a union). */
|
|
508
528
|
type AllKeys<P> = P extends unknown ? keyof P & string : never
|
|
509
529
|
|
|
530
|
+
/** Relations staged on a draft need `relates`, like `commands.relate`. */
|
|
531
|
+
type DraftRelationsAllowed<Relates, Declared> =
|
|
532
|
+
[Exclude<Relates, Declared>] extends [never]
|
|
533
|
+
? unknown
|
|
534
|
+
: Fix<`${Names<Exclude<Relates, Declared>>} is related by this draft but not declared in this system's commands: add it to Game.System.relates(...)`>
|
|
535
|
+
|
|
510
536
|
type DraftAllowed<P, Spawns> =
|
|
511
537
|
[Exclude<AllKeys<P>, Names<Spawns>>] extends [never]
|
|
512
538
|
? unknown
|
|
@@ -520,16 +546,24 @@ type EntriesAllowed<Entries extends ReadonlyArray<readonly [ComponentDescriptor,
|
|
|
520
546
|
type RemoveAllowed<D, Removes> =
|
|
521
547
|
[D] extends [Removes] ? unknown : Fix<`${Names<D>} is not declared in this system's commands: add it to Game.System.removes(...)`>
|
|
522
548
|
|
|
523
|
-
|
|
524
|
-
|
|
549
|
+
/**
|
|
550
|
+
* Why each possible target of a despawn is not allowed (`never` when it is).
|
|
551
|
+
* Computed per member, so a target chosen from several queries
|
|
552
|
+
* (`cond ? a.entity : b.entity`) must pass for every one of them.
|
|
553
|
+
*/
|
|
554
|
+
type DespawnRejections<E, Despawns> =
|
|
555
|
+
E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __proves?: (_: infer Proves) => void }
|
|
525
556
|
? [Extract<Proves, Despawns>] extends [never]
|
|
526
|
-
?
|
|
527
|
-
:
|
|
557
|
+
? `despawn needs an entity from a query proving ${Names<Despawns>} (declared in despawns)`
|
|
558
|
+
: never
|
|
528
559
|
: E extends { readonly [spawnedTypeId]: (_: never) => infer Spawned }
|
|
529
560
|
? [Extract<Spawned, Names<Despawns>>] extends [never]
|
|
530
|
-
?
|
|
531
|
-
:
|
|
532
|
-
:
|
|
561
|
+
? `despawn needs an entity spawned with ${Names<Despawns>} (declared in despawns)`
|
|
562
|
+
: never
|
|
563
|
+
: `despawn needs an entity from a query proving ${Names<Despawns>} (declared in despawns), not a raw id: pass match.entity`
|
|
564
|
+
|
|
565
|
+
type DespawnAllowed<E, Despawns> =
|
|
566
|
+
[DespawnRejections<E, Despawns>] extends [never] ? unknown : Fix<DespawnRejections<E, Despawns>>
|
|
533
567
|
|
|
534
568
|
declare const spawnedTypeId: unique symbol
|
|
535
569
|
|
|
@@ -537,11 +571,153 @@ declare const spawnedTypeId: unique symbol
|
|
|
537
571
|
* The id `spawn` / `spawnIn` return: an `EntityId` that also records the
|
|
538
572
|
* components its draft holds in every case, so the system that queued it can
|
|
539
573
|
* `despawn` it before the next `applyDeferred()` (no query can match it yet).
|
|
574
|
+
* It is valid only in the run that spawned it: kept in a closure or a
|
|
575
|
+
* resource and passed to `despawn` / `remove` in a later run, the command is
|
|
576
|
+
* skipped and reported (the `stale-target` lint). Keep the entity id or a
|
|
577
|
+
* handle and resolve it again through a query instead.
|
|
540
578
|
* Stored as a plain `EntityId`, it is a raw id again.
|
|
541
579
|
*/
|
|
542
580
|
export type SpawnedId<S extends Schema.Any, Root, Components extends string> =
|
|
543
581
|
Entity.EntityId<S, Root> & { readonly [spawnedTypeId]: (_: never) => Components }
|
|
544
582
|
|
|
583
|
+
/** One message per missing requirement: `"Player requires Velocity"`. */
|
|
584
|
+
type MissingMessages<Pairs> = Pairs extends readonly [infer From extends string, infer To extends string] ? `${From} requires ${To}` : never
|
|
585
|
+
|
|
586
|
+
/** The requirements (`[requirer, required]`) of the components in `From` that `Held` lacks. */
|
|
587
|
+
type MissingPairsFrom<S extends Schema.Any, From extends string, Held extends string> =
|
|
588
|
+
Extract<Schema.Requires<S>, readonly [From, string]> extends infer Pair
|
|
589
|
+
? Pair extends readonly [string, infer To extends string]
|
|
590
|
+
? To extends Held ? never : Pair
|
|
591
|
+
: never
|
|
592
|
+
: never
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* The requirements a draft may break. A draft's proof is a union of
|
|
596
|
+
* alternatives when an entry is conditional, and has optional keys for
|
|
597
|
+
* spread entries: in each alternative, every component it may hold needs
|
|
598
|
+
* every component it requires surely held. Direct requirements are enough:
|
|
599
|
+
* each required component must be held too, with its own requirements.
|
|
600
|
+
*/
|
|
601
|
+
type DraftViolations<S extends Schema.Any, P> =
|
|
602
|
+
P extends unknown ? MissingPairsFrom<S, keyof P & string, RequiredKeys<P>> : never
|
|
603
|
+
|
|
604
|
+
type DraftRequirementsMet<S extends Schema.Any, P> =
|
|
605
|
+
[DraftViolations<S, P>] extends [never]
|
|
606
|
+
? unknown
|
|
607
|
+
: Fix<`${MissingMessages<DraftViolations<S, P>>}: add the missing entries to the draft`>
|
|
608
|
+
|
|
609
|
+
/** What one possible target holds: a query match's proof, or a `SpawnedId`'s draft. A raw id proves nothing. */
|
|
610
|
+
type HeldBy<E> =
|
|
611
|
+
E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __proves?: (_: infer Proves) => void }
|
|
612
|
+
? Names<Proves>
|
|
613
|
+
: E extends { readonly [spawnedTypeId]: (_: never) => infer Spawned extends string }
|
|
614
|
+
? Spawned
|
|
615
|
+
: never
|
|
616
|
+
|
|
617
|
+
/** What one possible target's query proves absent: its `without`. */
|
|
618
|
+
type LackedBy<E> =
|
|
619
|
+
E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __lacks?: (_: infer Lacks) => void }
|
|
620
|
+
? Names<Lacks>
|
|
621
|
+
: never
|
|
622
|
+
|
|
623
|
+
/** The names in `All` that some member of `E` does not hold (or lack). */
|
|
624
|
+
type MissingFromHeld<E, All> = E extends unknown ? Exclude<All, HeldBy<E>> : never
|
|
625
|
+
type MissingFromLacked<E, All> = E extends unknown ? Exclude<All, LackedBy<E>> : never
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* What an insert or remove target is known to hold. For a target chosen
|
|
629
|
+
* from several queries (`cond ? a.entity : b.entity`), only what every one
|
|
630
|
+
* of them proves.
|
|
631
|
+
*/
|
|
632
|
+
type TargetHolds<E> = Exclude<HeldBy<E>, MissingFromHeld<E, HeldBy<E>>>
|
|
633
|
+
|
|
634
|
+
/** What a remove target is known to lack: its query's `without` (for several queries, what all of them exclude). */
|
|
635
|
+
type TargetLacks<E> = Exclude<LackedBy<E>, MissingFromLacked<E, LackedBy<E>>>
|
|
636
|
+
|
|
637
|
+
type EntryNames<Entries extends ReadonlyArray<readonly [ComponentDescriptor, unknown]>> = Names<Entries[number][0]>
|
|
638
|
+
|
|
639
|
+
/** The names one entry surely holds: none when it is conditional between descriptors. */
|
|
640
|
+
type SureEntryNames<Entry, All extends string = EntryNamesOf<Entry>> =
|
|
641
|
+
Exclude<All, Entry extends unknown ? Exclude<All, EntryNamesOf<Entry>> : never>
|
|
642
|
+
type EntryNamesOf<Entry> = Entry extends readonly [infer D, unknown] ? Names<D> : never
|
|
643
|
+
|
|
644
|
+
/** The names surely inserted: fixed entries, not spread or conditional ones. */
|
|
645
|
+
type SurelyInserted<Entries> =
|
|
646
|
+
Entries extends readonly [infer Head, ...infer Tail]
|
|
647
|
+
? SureEntryNames<Head> | SurelyInserted<Tail>
|
|
648
|
+
: Entries extends readonly [...infer Init, infer Last]
|
|
649
|
+
? SureEntryNames<Last> | SurelyInserted<Init>
|
|
650
|
+
: never
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Inserted components (any entry may be one) with requirements neither
|
|
654
|
+
* surely inserted alongside nor proven held by the target.
|
|
655
|
+
*/
|
|
656
|
+
type InsertViolations<S extends Schema.Any, E, Entries extends ReadonlyArray<readonly [ComponentDescriptor, unknown]>> =
|
|
657
|
+
MissingPairsFrom<S, EntryNames<Entries>, SurelyInserted<Entries> | TargetHolds<E>>
|
|
658
|
+
|
|
659
|
+
type InsertRequirementsMet<S extends Schema.Any, E, Entries extends ReadonlyArray<readonly [ComponentDescriptor, unknown]>> =
|
|
660
|
+
[InsertViolations<S, E, Entries>] extends [never]
|
|
661
|
+
? unknown
|
|
662
|
+
: Fix<`${MissingMessages<InsertViolations<S, E, Entries>>}: insert it in the same call, or pass match.entity from a query that proves it`>
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Removing `Removed` is sound when every component that requires one of
|
|
666
|
+
* them is removed in the same call or proven absent by the target's query
|
|
667
|
+
* (`without`): otherwise the entity may keep a requirer without its
|
|
668
|
+
* requirement.
|
|
669
|
+
*/
|
|
670
|
+
type RemoveViolations<S extends Schema.Any, E, Removed extends string> =
|
|
671
|
+
Extract<Schema.Requires<S>, readonly [string, Removed]> extends infer Pair
|
|
672
|
+
? Pair extends readonly [infer From extends string, infer To extends string]
|
|
673
|
+
? From extends Removed | TargetLacks<E>
|
|
674
|
+
? never
|
|
675
|
+
: `${To} is required by ${From}: remove ${From} in the same call, or match the entity with a query that has without: [${From}]`
|
|
676
|
+
: never
|
|
677
|
+
: never
|
|
678
|
+
|
|
679
|
+
type RemoveRequirementsMet<S extends Schema.Any, E, Removed extends string> =
|
|
680
|
+
[RemoveViolations<S, E, Removed>] extends [never] ? unknown : Fix<RemoveViolations<S, E, Removed>>
|
|
681
|
+
|
|
682
|
+
/** A remove target: `match.entity`, or the id `spawn` returned in this run; never a raw id, not even as one of several. */
|
|
683
|
+
type RemoveTargetRejections<S extends Schema.Any, Root, E> =
|
|
684
|
+
E extends { readonly kind: "EntityRef" | "EntityMut"; readonly id: Entity.EntityId<S, Root> }
|
|
685
|
+
? never
|
|
686
|
+
: E extends SpawnedId<S, Root, string>
|
|
687
|
+
? never
|
|
688
|
+
: "remove needs an entity from a query (pass match.entity) or the id spawn returned in this run, not a raw id"
|
|
689
|
+
|
|
690
|
+
type RemoveTargetAllowed<S extends Schema.Any, Root, E> =
|
|
691
|
+
[RemoveTargetRejections<S, Root, E>] extends [never] ? unknown : Fix<RemoveTargetRejections<S, Root, E>>
|
|
692
|
+
|
|
693
|
+
/** The transitive closure of the names `N` require (cycles stop). */
|
|
694
|
+
type RequiredClosure<Pairs extends RequirementPair, N extends string, Seen extends string = never> =
|
|
695
|
+
[Exclude<N, Seen>] extends [never]
|
|
696
|
+
? Seen
|
|
697
|
+
: RequiredClosure<Pairs, Extract<Pairs, readonly [Exclude<N, Seen>, string]>[1], Seen | N>
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* The component descriptors that `Names` require, transitively
|
|
701
|
+
* (`Schema.requires`): what `Game.System.spawns(...)` also allows.
|
|
702
|
+
*/
|
|
703
|
+
export type RequiredDescriptors<S extends Schema.Any, Names extends string> =
|
|
704
|
+
// Non-literal names (an unresolved descriptor, alone or next to known ones) would never
|
|
705
|
+
// close: leave the error to the descriptor.
|
|
706
|
+
0 extends 1 & Names
|
|
707
|
+
? never
|
|
708
|
+
: string extends Names
|
|
709
|
+
? never
|
|
710
|
+
: [Extract<Schema.Requires<S>, readonly [Names, string]>] extends [never]
|
|
711
|
+
? never
|
|
712
|
+
: Extract<Schema.ComponentDescriptor<S>, { readonly name: Exclude<RequiredClosure<Schema.Requires<S>, Names>, Names> }>
|
|
713
|
+
|
|
714
|
+
type SchemaComponent<S extends Schema.Any> = Extract<Schema.Components<S>[keyof Schema.Components<S>], ComponentDescriptor>
|
|
715
|
+
|
|
716
|
+
/** An entity a system has proven something about: `match.entity`, or the id `spawn` returned. */
|
|
717
|
+
type EntityTarget<S extends Schema.Any, Root> =
|
|
718
|
+
| { readonly kind: "EntityRef" | "EntityMut"; readonly id: Entity.EntityId<S, Root> }
|
|
719
|
+
| SpawnedId<S, Root, string>
|
|
720
|
+
|
|
545
721
|
/** The keys every member of a union of proofs requires (what a draft holds in every case). */
|
|
546
722
|
type RequiredKeys<P> = P extends unknown ? { [K in keyof P]-?: {} extends Pick<P, K> ? never : K }[keyof P] & string : never
|
|
547
723
|
type MissingSomewhere<P, All> = P extends unknown ? Exclude<All, RequiredKeys<P>> : never
|
|
@@ -557,7 +733,8 @@ type HeldNames<P> = Exclude<AllKeys<P>, MissingSomewhere<P, AllKeys<P>>>
|
|
|
557
733
|
* `spawn` needs `Game.System.spawns(...)` listing every component of the
|
|
558
734
|
* draft, `insert` needs `inserts`, `remove` needs `removes`, `despawn` needs
|
|
559
735
|
* `despawns` and an entity from a query proving one of them, `despawnScope`
|
|
560
|
-
* needs `despawnsScope`, and relation edits need `relates
|
|
736
|
+
* needs `despawnsScope`, and relation edits need `relates`, including
|
|
737
|
+
* relations a spawned draft stages with `Game.Command.relate(...)`.
|
|
561
738
|
*
|
|
562
739
|
* Resource and event writes are not commands: they go through the
|
|
563
740
|
* `writeResource(...)` / `writeEvent(...)` access a system declares.
|
|
@@ -574,33 +751,48 @@ export interface CommandsApi<
|
|
|
574
751
|
> {
|
|
575
752
|
/**
|
|
576
753
|
* Queues a staged entity for spawning and returns its stable runtime id.
|
|
754
|
+
*
|
|
755
|
+
* The draft must hold every component its components require
|
|
756
|
+
* (`Schema.requires`); drafts are checked here, so a partial draft built
|
|
757
|
+
* with `Game.Command.spawn(...)` and completed with `extend(...)` is fine.
|
|
758
|
+
* A component the draft may hold (a conditional or spread entry) needs
|
|
759
|
+
* what it requires surely held.
|
|
577
760
|
*/
|
|
578
761
|
readonly spawn: [Spawns] extends [never]
|
|
579
762
|
? MethodNotDeclared<"spawn", "spawns">
|
|
580
|
-
: <P extends Entity.ComponentProof
|
|
763
|
+
: <P extends Entity.ComponentProof, R = never>(
|
|
764
|
+
draft: Entity.EntityDraft<S, P, Root, R> & DraftAllowed<P, Spawns> & DraftRequirementsMet<S, P> & DraftRelationsAllowed<R, Relates>
|
|
765
|
+
) => SpawnedId<S, Root, HeldNames<P>>
|
|
581
766
|
/**
|
|
582
767
|
* Queues an entity spawn owned by one lifetime scope (removed together
|
|
583
|
-
* with `despawnScope(...)`).
|
|
768
|
+
* with `despawnScope(...)`). The draft is checked like `spawn(...)`.
|
|
584
769
|
*/
|
|
585
770
|
readonly spawnIn: [Spawns] extends [never]
|
|
586
771
|
? MethodNotDeclared<"spawnIn", "spawns">
|
|
587
|
-
: <P extends Entity.ComponentProof>(
|
|
772
|
+
: <P extends Entity.ComponentProof, R = never>(
|
|
588
773
|
scope: EntityScope.EntityScope<string, Root>,
|
|
589
|
-
draft: Entity.EntityDraft<S, P, Root> & DraftAllowed<P, Spawns>
|
|
774
|
+
draft: Entity.EntityDraft<S, P, Root, R> & DraftAllowed<P, Spawns> & DraftRequirementsMet<S, P> & DraftRelationsAllowed<R, Relates>
|
|
590
775
|
) => SpawnedId<S, Root, HeldNames<P>>
|
|
591
776
|
/**
|
|
592
777
|
* Queues component inserts (or replacements) on an existing entity, applied
|
|
593
778
|
* at the next `applyDeferred()`. To change a value now, on an entity you
|
|
594
|
-
* matched, write through a query (`Game.Query.write`) instead
|
|
779
|
+
* matched, write through a query (`Game.Query.write`) instead; components
|
|
780
|
+
* declared `immutable: true` change only through `insert`.
|
|
781
|
+
*
|
|
782
|
+
* Pass `match.entity`: its query proves what the entity holds, which must
|
|
783
|
+
* cover what the inserted components require (`Schema.requires`) together
|
|
784
|
+
* with the entries inserted in the same call. The id `spawn` returned in
|
|
785
|
+
* this run proves its draft. A raw `EntityId` proves nothing, so it only
|
|
786
|
+
* inserts components without requirements.
|
|
595
787
|
*
|
|
596
788
|
* Entries must already be valid; validate raw input with
|
|
597
789
|
* `Game.Command.entryRaw(...)` first.
|
|
598
790
|
*/
|
|
599
791
|
readonly insert: [Inserts] extends [never]
|
|
600
792
|
? MethodNotDeclared<"insert", "inserts">
|
|
601
|
-
: <const Entries extends ReadonlyArray<SchemaEntry<S>>>(
|
|
602
|
-
entity:
|
|
603
|
-
...entries: Entries & EntriesAllowed<Entries, Inserts>
|
|
793
|
+
: <const E extends Entity.EntityId<S, Root> | EntityTarget<S, Root>, const Entries extends ReadonlyArray<SchemaEntry<S>>>(
|
|
794
|
+
entity: E,
|
|
795
|
+
...entries: Entries & EntriesAllowed<Entries, Inserts> & InsertRequirementsMet<S, E, Entries>
|
|
604
796
|
) => Entity.EntityId<S, Root>
|
|
605
797
|
/**
|
|
606
798
|
* Queues an entity removal. A despawned entity stays visible to queries
|
|
@@ -613,7 +805,7 @@ export interface CommandsApi<
|
|
|
613
805
|
*/
|
|
614
806
|
readonly despawn: [Despawns] extends [never]
|
|
615
807
|
? MethodNotDeclared<"despawn", "despawns">
|
|
616
|
-
: <const E>(entity: E
|
|
808
|
+
: <const E>(entity: unknown extends DespawnAllowed<E, Despawns> ? E : DespawnAllowed<E, Despawns>) => void
|
|
617
809
|
/**
|
|
618
810
|
* Queues removal of every entity owned by one lifetime scope.
|
|
619
811
|
*/
|
|
@@ -621,13 +813,22 @@ export interface CommandsApi<
|
|
|
621
813
|
? MethodNotDeclared<"despawnScope", "despawnsScope">
|
|
622
814
|
: (scope: Scopes) => void
|
|
623
815
|
/**
|
|
624
|
-
* Queues
|
|
816
|
+
* Queues the removal of one or more components from an existing entity,
|
|
817
|
+
* applied together at the next `applyDeferred()`.
|
|
818
|
+
*
|
|
819
|
+
* Pass `match.entity` from a query, or the id `spawn` returned in this run;
|
|
820
|
+
* never a raw id. A component another one requires (`Schema.requires`) can
|
|
821
|
+
* only go together with that requirer, in the same call, or from an
|
|
822
|
+
* entity whose query has `without: [Requirer]`.
|
|
625
823
|
*/
|
|
626
824
|
readonly remove: [Removes] extends [never]
|
|
627
825
|
? MethodNotDeclared<"remove", "removes">
|
|
628
|
-
: <
|
|
629
|
-
|
|
630
|
-
|
|
826
|
+
: <
|
|
827
|
+
const E,
|
|
828
|
+
const Ds extends readonly [SchemaComponent<S>, ...Array<SchemaComponent<S>>]
|
|
829
|
+
>(
|
|
830
|
+
entity: unknown extends RemoveTargetAllowed<S, Root, E> ? E : RemoveTargetAllowed<S, Root, E>,
|
|
831
|
+
...descriptors: Ds & RemoveAllowed<Ds[number], Removes> & RemoveRequirementsMet<S, E, Names<Ds[number]>>
|
|
631
832
|
) => Entity.EntityId<S, Root>
|
|
632
833
|
/**
|
|
633
834
|
* Queues one live relation mutation for deferred application.
|
|
@@ -662,12 +863,12 @@ export interface CommandsApi<
|
|
|
662
863
|
|
|
663
864
|
/** The command queue the runtime gives each system: every command, unchecked, and `flush`. */
|
|
664
865
|
export interface CommandQueue<S extends Schema.Any, Root = unknown> {
|
|
665
|
-
readonly spawn: (draft: Entity.EntityDraft<S, any, Root>) => Entity.EntityId<S, Root>
|
|
666
|
-
readonly spawnIn: (scope: EntityScope.EntityScope<string, Root>, draft: Entity.EntityDraft<S, any, Root>) => Entity.EntityId<S, Root>
|
|
667
|
-
readonly insert: (entity: Entity.EntityId<S, Root
|
|
866
|
+
readonly spawn: (draft: Entity.EntityDraft<S, any, Root, any>) => Entity.EntityId<S, Root>
|
|
867
|
+
readonly spawnIn: (scope: EntityScope.EntityScope<string, Root>, draft: Entity.EntityDraft<S, any, Root, any>) => Entity.EntityId<S, Root>
|
|
868
|
+
readonly insert: (entity: Entity.EntityId<S, Root> | { readonly id: Entity.EntityId<S, Root> }, ...entries: ReadonlyArray<SchemaEntry<S>>) => Entity.EntityId<S, Root>
|
|
668
869
|
readonly despawn: (entity: Entity.EntityId<S, Root> | { readonly id: Entity.EntityId<S, Root> }) => void
|
|
669
870
|
readonly despawnScope: (scope: EntityScope.EntityScope<string, Root>) => void
|
|
670
|
-
readonly remove: (entity: Entity.EntityId<S, Root
|
|
871
|
+
readonly remove: (entity: Entity.EntityId<S, Root> | { readonly id: Entity.EntityId<S, Root> }, ...descriptors: ReadonlyArray<ComponentDescriptor>) => Entity.EntityId<S, Root>
|
|
671
872
|
readonly relate: (entity: Entity.EntityId<S, Root>, relation: Relation.Relation.Any, target: Entity.EntityId<S, Root>) => void
|
|
672
873
|
readonly unrelate: (entity: Entity.EntityId<S, Root>, relation: Relation.Relation.Any) => void
|
|
673
874
|
readonly reorderChildren: (entity: Entity.EntityId<S, Root>, relation: Relation.Relation.Hierarchy, children: ReadonlyArray<Entity.EntityId<S, Root>>) => void
|
|
@@ -693,14 +894,21 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
|
|
|
693
894
|
* the system effect completes.
|
|
694
895
|
*/
|
|
695
896
|
const queue: Array<DeferredCommand<S>> = []
|
|
897
|
+
/**
|
|
898
|
+
* Ids this run spawned: the only numeric targets `despawn` and `remove`
|
|
899
|
+
* accept. Their types reject raw ids, so any other number is an id kept
|
|
900
|
+
* from an earlier run (or erased types), and proves nothing now.
|
|
901
|
+
*/
|
|
902
|
+
const spawnedThisRun = new Set<number>()
|
|
696
903
|
|
|
697
904
|
return {
|
|
698
905
|
spawn(draft) {
|
|
699
906
|
const id = allocateId()
|
|
907
|
+
spawnedThisRun.add(id)
|
|
700
908
|
queue.push({
|
|
701
909
|
tag: "spawn",
|
|
702
910
|
apply(world) {
|
|
703
|
-
world.spawnEntity(id, draft.components)
|
|
911
|
+
if (!world.spawnEntity(id, draft.components)) return
|
|
704
912
|
for (const stagedRelation of draft.relations) {
|
|
705
913
|
world.tryRelate(id, stagedRelation.relation, stagedRelation.target)
|
|
706
914
|
}
|
|
@@ -710,10 +918,11 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
|
|
|
710
918
|
},
|
|
711
919
|
spawnIn(scope, draft) {
|
|
712
920
|
const id = allocateId()
|
|
921
|
+
spawnedThisRun.add(id)
|
|
713
922
|
queue.push({
|
|
714
923
|
tag: "spawnIn",
|
|
715
924
|
apply(world) {
|
|
716
|
-
world.spawnEntity(id, draft.components)
|
|
925
|
+
if (!world.spawnEntity(id, draft.components)) return
|
|
717
926
|
world.assignEntityScope(id, scope)
|
|
718
927
|
for (const stagedRelation of draft.relations) {
|
|
719
928
|
world.tryRelate(id, stagedRelation.relation, stagedRelation.target)
|
|
@@ -722,23 +931,24 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
|
|
|
722
931
|
})
|
|
723
932
|
return id
|
|
724
933
|
},
|
|
725
|
-
insert(
|
|
934
|
+
insert(target, ...entries) {
|
|
935
|
+
const entity = typeof target === "object" ? target.id : target
|
|
726
936
|
queue.push({
|
|
727
937
|
tag: "insert",
|
|
728
938
|
apply(world) {
|
|
729
|
-
|
|
730
|
-
world.writeComponent(entity, descriptor, value)
|
|
731
|
-
}
|
|
939
|
+
world.insertComponents(entity, entries)
|
|
732
940
|
}
|
|
733
941
|
})
|
|
734
942
|
return entity
|
|
735
943
|
},
|
|
736
944
|
despawn(entity) {
|
|
737
945
|
const id = typeof entity === "object" ? entity.id : entity
|
|
946
|
+
const stale = typeof entity !== "object" && !spawnedThisRun.has(id)
|
|
738
947
|
queue.push({
|
|
739
948
|
tag: "despawn",
|
|
740
949
|
apply(world) {
|
|
741
|
-
world.
|
|
950
|
+
if (stale) world.reportStaleTarget("despawn", id)
|
|
951
|
+
else world.destroyEntity(id)
|
|
742
952
|
}
|
|
743
953
|
})
|
|
744
954
|
},
|
|
@@ -750,11 +960,14 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
|
|
|
750
960
|
}
|
|
751
961
|
})
|
|
752
962
|
},
|
|
753
|
-
remove(
|
|
963
|
+
remove(target, ...descriptors) {
|
|
964
|
+
const entity = typeof target === "object" ? target.id : target
|
|
965
|
+
const stale = typeof target !== "object" && !spawnedThisRun.has(entity)
|
|
754
966
|
queue.push({
|
|
755
967
|
tag: "remove",
|
|
756
968
|
apply(world) {
|
|
757
|
-
world.
|
|
969
|
+
if (stale) world.reportStaleTarget("remove", entity)
|
|
970
|
+
else world.removeComponents(entity, descriptors)
|
|
758
971
|
}
|
|
759
972
|
})
|
|
760
973
|
return entity
|
|
@@ -784,6 +997,7 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
|
|
|
784
997
|
})
|
|
785
998
|
},
|
|
786
999
|
flush() {
|
|
1000
|
+
spawnedThisRun.clear()
|
|
787
1001
|
return queue.splice(0, queue.length)
|
|
788
1002
|
}
|
|
789
1003
|
}
|
package/src/Condition.ts
CHANGED
|
@@ -45,10 +45,25 @@ type AnyQuery = Query.Query.Any<any>
|
|
|
45
45
|
/** Access categories a check may declare. */
|
|
46
46
|
export interface CheckAccessInput {
|
|
47
47
|
readonly queries?: Record<string, AnyQuery>
|
|
48
|
-
readonly resources?: Record<string,
|
|
48
|
+
readonly resources?: Record<string, CheckResourceAccess>
|
|
49
49
|
readonly machines?: Record<string, Machine.MachineRead<Machine.StateMachine.Any>>
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** Accepted at the constraint so `ExactCheckAccess` can reject the last two with a message naming the fix. */
|
|
53
|
+
type CheckResourceAccess =
|
|
54
|
+
| System.ResourceRead<Descriptor<"resource", string, any>>
|
|
55
|
+
| System.ResourceChanged<Descriptor<"resource", string, any>>
|
|
56
|
+
| System.ResourceWrite<Descriptor<"resource", string, any>>
|
|
57
|
+
|
|
58
|
+
/** A check reads committed values and owns no cursor: `changedResource` and `writeResource` slots become a message naming the fix. */
|
|
59
|
+
type CheckResources<Resources> = {
|
|
60
|
+
readonly [K in keyof Resources]: Resources[K] extends { readonly mode: "changed" }
|
|
61
|
+
? "changedResource consumes a per-reader cursor, which a check does not have: use Game.System.readResource here, and changedResource in the gated system"
|
|
62
|
+
: Resources[K] extends { readonly mode: "write" }
|
|
63
|
+
? "a check cannot write resources: use Game.System.readResource"
|
|
64
|
+
: Resources[K]
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
type QueryWriteAccess<Q extends AnyQuery> =
|
|
53
68
|
Extract<Q["selection"][keyof Q["selection"]], { readonly mode: "write" }>
|
|
54
69
|
|
|
@@ -72,6 +87,7 @@ export type ExactCheckAccess<Access extends CheckAccessInput> = Access & {
|
|
|
72
87
|
readonly queries?: Access["queries"] extends Record<string, AnyQuery>
|
|
73
88
|
? StatelessQueries<Access["queries"]>
|
|
74
89
|
: never
|
|
90
|
+
readonly resources?: CheckResources<Access["resources"]>
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
/** What a check's predicate sees: its declared resources, machines, and queries, read-only. */
|