@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.
Files changed (141) hide show
  1. package/README.md +3 -2
  2. package/dist/Clock.d.ts +19 -4
  3. package/dist/Clock.d.ts.map +1 -1
  4. package/dist/Clock.js +18 -2
  5. package/dist/Clock.js.map +1 -1
  6. package/dist/Command.d.ts +181 -40
  7. package/dist/Command.d.ts.map +1 -1
  8. package/dist/Command.js +31 -11
  9. package/dist/Command.js.map +1 -1
  10. package/dist/Condition.d.ts +12 -1
  11. package/dist/Condition.d.ts.map +1 -1
  12. package/dist/Condition.js.map +1 -1
  13. package/dist/Debug.d.ts +127 -13
  14. package/dist/Debug.d.ts.map +1 -1
  15. package/dist/Descriptor.d.ts +61 -10
  16. package/dist/Descriptor.d.ts.map +1 -1
  17. package/dist/Descriptor.js +22 -10
  18. package/dist/Descriptor.js.map +1 -1
  19. package/dist/Entity.d.ts +22 -7
  20. package/dist/Entity.d.ts.map +1 -1
  21. package/dist/Entity.js.map +1 -1
  22. package/dist/EntityScope.d.ts +20 -1
  23. package/dist/EntityScope.d.ts.map +1 -1
  24. package/dist/EntityScope.js +8 -2
  25. package/dist/EntityScope.js.map +1 -1
  26. package/dist/FixedLoop.d.ts +106 -0
  27. package/dist/FixedLoop.d.ts.map +1 -0
  28. package/dist/FixedLoop.js +97 -0
  29. package/dist/FixedLoop.js.map +1 -0
  30. package/dist/HostClock.d.ts +1 -1
  31. package/dist/HostClock.d.ts.map +1 -1
  32. package/dist/HostClock.js +2 -1
  33. package/dist/HostClock.js.map +1 -1
  34. package/dist/Machine.d.ts +59 -34
  35. package/dist/Machine.d.ts.map +1 -1
  36. package/dist/Machine.js +10 -3
  37. package/dist/Machine.js.map +1 -1
  38. package/dist/Query.d.ts +100 -14
  39. package/dist/Query.d.ts.map +1 -1
  40. package/dist/Query.js +34 -5
  41. package/dist/Query.js.map +1 -1
  42. package/dist/Relation.d.ts +5 -14
  43. package/dist/Relation.d.ts.map +1 -1
  44. package/dist/Relation.js +0 -32
  45. package/dist/Relation.js.map +1 -1
  46. package/dist/Runtime.d.ts +61 -15
  47. package/dist/Runtime.d.ts.map +1 -1
  48. package/dist/Runtime.js +594 -70
  49. package/dist/Runtime.js.map +1 -1
  50. package/dist/Schedule.d.ts +24 -15
  51. package/dist/Schedule.d.ts.map +1 -1
  52. package/dist/Schedule.js +2 -1
  53. package/dist/Schedule.js.map +1 -1
  54. package/dist/Schema.d.ts +233 -48
  55. package/dist/Schema.d.ts.map +1 -1
  56. package/dist/Schema.js +33 -7
  57. package/dist/Schema.js.map +1 -1
  58. package/dist/Snapshot.d.ts +90 -1
  59. package/dist/Snapshot.d.ts.map +1 -1
  60. package/dist/Snapshot.js +42 -1
  61. package/dist/Snapshot.js.map +1 -1
  62. package/dist/System.d.ts +132 -31
  63. package/dist/System.d.ts.map +1 -1
  64. package/dist/System.js +32 -0
  65. package/dist/System.js.map +1 -1
  66. package/dist/index.d.ts +4 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +4 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/internal/cells.d.ts +2 -0
  71. package/dist/internal/cells.d.ts.map +1 -1
  72. package/dist/internal/cells.js +34 -8
  73. package/dist/internal/cells.js.map +1 -1
  74. package/dist/internal/debug.d.ts +10 -0
  75. package/dist/internal/debug.d.ts.map +1 -1
  76. package/dist/internal/debug.js +171 -12
  77. package/dist/internal/debug.js.map +1 -1
  78. package/dist/internal/equal.d.ts +8 -0
  79. package/dist/internal/equal.d.ts.map +1 -0
  80. package/dist/internal/equal.js +38 -0
  81. package/dist/internal/equal.js.map +1 -0
  82. package/dist/internal/fragments.d.ts +2 -6
  83. package/dist/internal/fragments.d.ts.map +1 -1
  84. package/dist/internal/fragments.js +36 -3
  85. package/dist/internal/fragments.js.map +1 -1
  86. package/dist/internal/game.d.ts +2 -0
  87. package/dist/internal/game.d.ts.map +1 -1
  88. package/dist/internal/game.js +48 -7
  89. package/dist/internal/game.js.map +1 -1
  90. package/dist/internal/order.d.ts.map +1 -1
  91. package/dist/internal/order.js +4 -2
  92. package/dist/internal/order.js.map +1 -1
  93. package/dist/internal/queries.d.ts.map +1 -1
  94. package/dist/internal/queries.js +57 -26
  95. package/dist/internal/queries.js.map +1 -1
  96. package/dist/internal/relations.d.ts +10 -0
  97. package/dist/internal/relations.d.ts.map +1 -0
  98. package/dist/internal/relations.js +32 -0
  99. package/dist/internal/relations.js.map +1 -0
  100. package/dist/internal/requirements.d.ts +26 -0
  101. package/dist/internal/requirements.d.ts.map +1 -0
  102. package/dist/internal/requirements.js +46 -0
  103. package/dist/internal/requirements.js.map +1 -0
  104. package/dist/internal/streams.d.ts +2 -0
  105. package/dist/internal/streams.d.ts.map +1 -1
  106. package/dist/internal/streams.js +10 -0
  107. package/dist/internal/streams.js.map +1 -1
  108. package/dist/internal/world.d.ts +7 -1
  109. package/dist/internal/world.d.ts.map +1 -1
  110. package/dist/internal/world.js +53 -3
  111. package/dist/internal/world.js.map +1 -1
  112. package/package.json +1 -1
  113. package/src/Clock.ts +29 -4
  114. package/src/Command.ts +281 -67
  115. package/src/Condition.ts +17 -1
  116. package/src/Debug.ts +124 -15
  117. package/src/Descriptor.ts +92 -21
  118. package/src/Entity.ts +23 -7
  119. package/src/EntityScope.ts +21 -2
  120. package/src/FixedLoop.ts +218 -0
  121. package/src/HostClock.ts +2 -1
  122. package/src/Machine.ts +72 -35
  123. package/src/Query.ts +118 -17
  124. package/src/Relation.ts +5 -67
  125. package/src/Runtime.ts +659 -85
  126. package/src/Schedule.ts +31 -19
  127. package/src/Schema.ts +297 -57
  128. package/src/Snapshot.ts +94 -2
  129. package/src/System.ts +151 -29
  130. package/src/index.ts +4 -0
  131. package/src/internal/cells.ts +42 -10
  132. package/src/internal/debug.ts +179 -14
  133. package/src/internal/equal.ts +31 -0
  134. package/src/internal/fragments.ts +35 -5
  135. package/src/internal/game.ts +69 -7
  136. package/src/internal/order.ts +8 -4
  137. package/src/internal/queries.ts +62 -12
  138. package/src/internal/relations.ts +52 -0
  139. package/src/internal/requirements.ts +71 -0
  140. package/src/internal/streams.ts +11 -0
  141. 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 { Schema } from "./Schema.ts"
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
- > = number extends Entries["length"]
146
- // A widened array (a spread of entries built at runtime): each entry may or may not be there.
147
- ? P & MaybeEntries<EntryValue<Entries[number]>>
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 Head extends SpawnEntry<any>,
150
- ...infer Tail extends Array<SpawnEntry<any>>
153
+ ...infer Init extends ReadonlyArray<SpawnEntry<any>>,
154
+ infer Last extends SpawnEntry<any>
151
155
  ]
152
- ? FoldEntries<Tail, Draft.InsertEntry<P, EntryValue<Head>>>
153
- : P
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>) => void
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 a component from a live entity.
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 removeComponent: (id: Entity.EntityId<S, any>, descriptor: Descriptor<"component", string, any>) => void
245
+ readonly insertComponents: (id: Entity.EntityId<S, any>, entries: ReadonlyArray<Entity.StagedComponent>) => void
231
246
  /**
232
- * Inserts or replaces a component on a live entity.
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 writeComponent: (id: Entity.EntityId<S, any>, descriptor: Descriptor<"component", string, any>, value: unknown) => void
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(...)` / `insert(...)`.
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 `insert(...)` for how
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>>(...components: Components): Permission<"spawns", Components[number]> =>
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>>(...components: Components): Permission<"inserts", Components[number]> =>
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>>(...components: Components): Permission<"removes", Components[number]> =>
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>>(...components: Components): Permission<"despawns", Components[number]> =>
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>>(...relations: Relations): Permission<"relates", Relations[number]> =>
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
- type DespawnAllowed<E, Despawns> =
524
- E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __proves?: (_: never) => infer Proves }
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
- ? Fix<`despawn needs an entity from a query proving ${Names<Despawns>} (declared in despawns)`>
527
- : unknown
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
- ? Fix<`despawn needs an entity spawned with ${Names<Despawns>} (declared in despawns)`>
531
- : unknown
532
- : Fix<`despawn needs an entity from a query proving ${Names<Despawns>} (declared in despawns), not a raw id: pass match.entity`>
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>(draft: Entity.EntityDraft<S, P, Root> & DraftAllowed<P, Spawns>) => SpawnedId<S, Root, HeldNames<P>>
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: Entity.EntityId<S, Root>,
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 & DespawnAllowed<E, Despawns>) => void
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 a component removal on an existing entity.
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
- : <D extends Extract<Schema.Components<S>[keyof Schema.Components<S>], ComponentDescriptor>>(
629
- entity: Entity.EntityId<S, Root>,
630
- descriptor: D & RemoveAllowed<D, Removes>
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>, ...entries: ReadonlyArray<SchemaEntry<S>>) => 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>, descriptor: ComponentDescriptor) => 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(entity, ...entries) {
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
- for (const [descriptor, value] of entries) {
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.destroyEntity(id)
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(entity, descriptor) {
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.removeComponent(entity, descriptor)
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, System.ResourceRead<Descriptor<"resource", string, any>>>
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. */