@typeonce/ritmo 0.6.0 → 0.8.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 (148) hide show
  1. package/README.md +2 -0
  2. package/dist/Brand.d.ts +9 -4
  3. package/dist/Brand.d.ts.map +1 -1
  4. package/dist/Brand.js +9 -4
  5. package/dist/Brand.js.map +1 -1
  6. package/dist/Clock.d.ts +186 -0
  7. package/dist/Clock.d.ts.map +1 -0
  8. package/dist/Clock.js +111 -0
  9. package/dist/Clock.js.map +1 -0
  10. package/dist/Command.d.ts +250 -41
  11. package/dist/Command.d.ts.map +1 -1
  12. package/dist/Command.js +59 -23
  13. package/dist/Command.js.map +1 -1
  14. package/dist/Condition.d.ts +12 -1
  15. package/dist/Condition.d.ts.map +1 -1
  16. package/dist/Condition.js.map +1 -1
  17. package/dist/Debug.d.ts +163 -15
  18. package/dist/Debug.d.ts.map +1 -1
  19. package/dist/Debug.js.map +1 -1
  20. package/dist/Decode.d.ts +1 -1
  21. package/dist/Decode.js +1 -1
  22. package/dist/Descriptor.d.ts +67 -24
  23. package/dist/Descriptor.d.ts.map +1 -1
  24. package/dist/Descriptor.js +32 -15
  25. package/dist/Descriptor.js.map +1 -1
  26. package/dist/Entity.d.ts +22 -5
  27. package/dist/Entity.d.ts.map +1 -1
  28. package/dist/Entity.js +2 -2
  29. package/dist/Entity.js.map +1 -1
  30. package/dist/EntityScope.d.ts +26 -1
  31. package/dist/EntityScope.d.ts.map +1 -1
  32. package/dist/EntityScope.js +14 -2
  33. package/dist/EntityScope.js.map +1 -1
  34. package/dist/HostClock.d.ts +49 -0
  35. package/dist/HostClock.d.ts.map +1 -0
  36. package/dist/HostClock.js +46 -0
  37. package/dist/HostClock.js.map +1 -0
  38. package/dist/Inspector.d.ts +7 -1
  39. package/dist/Inspector.d.ts.map +1 -1
  40. package/dist/Inspector.js.map +1 -1
  41. package/dist/Machine.d.ts +67 -36
  42. package/dist/Machine.d.ts.map +1 -1
  43. package/dist/Machine.js +10 -3
  44. package/dist/Machine.js.map +1 -1
  45. package/dist/Query.d.ts +74 -26
  46. package/dist/Query.d.ts.map +1 -1
  47. package/dist/Query.js +32 -28
  48. package/dist/Query.js.map +1 -1
  49. package/dist/Runtime.d.ts +39 -16
  50. package/dist/Runtime.d.ts.map +1 -1
  51. package/dist/Runtime.js +556 -87
  52. package/dist/Runtime.js.map +1 -1
  53. package/dist/Schedule.d.ts +217 -10
  54. package/dist/Schedule.d.ts.map +1 -1
  55. package/dist/Schedule.js +5 -0
  56. package/dist/Schedule.js.map +1 -1
  57. package/dist/Schema.d.ts +196 -44
  58. package/dist/Schema.d.ts.map +1 -1
  59. package/dist/Schema.js +29 -11
  60. package/dist/Schema.js.map +1 -1
  61. package/dist/Snapshot.d.ts +14 -0
  62. package/dist/Snapshot.d.ts.map +1 -1
  63. package/dist/Snapshot.js.map +1 -1
  64. package/dist/System.d.ts +212 -58
  65. package/dist/System.d.ts.map +1 -1
  66. package/dist/System.js +84 -6
  67. package/dist/System.js.map +1 -1
  68. package/dist/index.d.ts +8 -7
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +8 -7
  71. package/dist/index.js.map +1 -1
  72. package/dist/internal/cells.d.ts +3 -3
  73. package/dist/internal/cells.d.ts.map +1 -1
  74. package/dist/internal/cells.js +9 -11
  75. package/dist/internal/cells.js.map +1 -1
  76. package/dist/internal/debug.d.ts +13 -0
  77. package/dist/internal/debug.d.ts.map +1 -1
  78. package/dist/internal/debug.js +192 -7
  79. package/dist/internal/debug.js.map +1 -1
  80. package/dist/internal/equal.d.ts +8 -0
  81. package/dist/internal/equal.d.ts.map +1 -0
  82. package/dist/internal/equal.js +38 -0
  83. package/dist/internal/equal.js.map +1 -0
  84. package/dist/internal/fragments.d.ts.map +1 -1
  85. package/dist/internal/fragments.js +11 -1
  86. package/dist/internal/fragments.js.map +1 -1
  87. package/dist/internal/game.d.ts.map +1 -1
  88. package/dist/internal/game.js +41 -6
  89. package/dist/internal/game.js.map +1 -1
  90. package/dist/internal/order.d.ts +10 -0
  91. package/dist/internal/order.d.ts.map +1 -0
  92. package/dist/internal/order.js +122 -0
  93. package/dist/internal/order.js.map +1 -0
  94. package/dist/internal/queries.d.ts.map +1 -1
  95. package/dist/internal/queries.js +32 -10
  96. package/dist/internal/queries.js.map +1 -1
  97. package/dist/internal/requirements.d.ts +26 -0
  98. package/dist/internal/requirements.d.ts.map +1 -0
  99. package/dist/internal/requirements.js +46 -0
  100. package/dist/internal/requirements.js.map +1 -0
  101. package/package.json +1 -1
  102. package/src/Brand.ts +9 -4
  103. package/src/Clock.ts +311 -0
  104. package/src/Command.ts +407 -89
  105. package/src/Condition.ts +17 -1
  106. package/src/Debug.ts +172 -14
  107. package/src/Decode.ts +1 -1
  108. package/src/Descriptor.ts +92 -37
  109. package/src/Entity.ts +24 -5
  110. package/src/EntityScope.ts +27 -2
  111. package/src/HostClock.ts +64 -0
  112. package/src/Inspector.ts +7 -2
  113. package/src/Machine.ts +81 -37
  114. package/src/Query.ts +110 -38
  115. package/src/Runtime.ts +607 -111
  116. package/src/Schedule.ts +380 -13
  117. package/src/Schema.ts +249 -63
  118. package/src/Snapshot.ts +4 -0
  119. package/src/System.ts +248 -93
  120. package/src/index.ts +8 -7
  121. package/src/internal/cells.ts +10 -12
  122. package/src/internal/debug.ts +208 -12
  123. package/src/internal/equal.ts +31 -0
  124. package/src/internal/fragments.ts +12 -2
  125. package/src/internal/game.ts +61 -7
  126. package/src/internal/order.ts +144 -0
  127. package/src/internal/queries.ts +37 -13
  128. package/src/internal/requirements.ts +71 -0
  129. package/dist/Definition.d.ts +0 -99
  130. package/dist/Definition.d.ts.map +0 -1
  131. package/dist/Definition.js +0 -68
  132. package/dist/Definition.js.map +0 -1
  133. package/dist/Feature.d.ts +0 -106
  134. package/dist/Feature.d.ts.map +0 -1
  135. package/dist/Feature.js +0 -87
  136. package/dist/Feature.js.map +0 -1
  137. package/dist/Fx.d.ts +0 -128
  138. package/dist/Fx.d.ts.map +0 -1
  139. package/dist/Fx.js +0 -112
  140. package/dist/Fx.js.map +0 -1
  141. package/dist/internal/definition.d.ts +0 -17
  142. package/dist/internal/definition.d.ts.map +0 -1
  143. package/dist/internal/definition.js +0 -25
  144. package/dist/internal/definition.js.map +0 -1
  145. package/src/Definition.ts +0 -106
  146. package/src/Feature.ts +0 -241
  147. package/src/Fx.ts +0 -176
  148. package/src/internal/definition.ts +0 -56
package/src/Command.ts CHANGED
@@ -65,7 +65,7 @@ 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 { RequirementPair, Schema } from "./Schema.ts"
69
69
 
70
70
  /**
71
71
  * Type-level helpers for staged entity construction.
@@ -125,7 +125,7 @@ export type SchemaEntry<S extends Schema.Any> = EntryOf<SchemaComponentDescripto
125
125
 
126
126
  /**
127
127
  * One entry accepted by `spawn(...)` and `insert(...)`: a plain descriptor/value
128
- * pair, or a `Result` of one (from `entryRaw(...)` or `entryResult(...)`).
128
+ * pair, or a `Result` of one (from `entryRaw(...)`).
129
129
  */
130
130
  export type SpawnEntry<S extends Schema.Any> = SchemaEntry<S> | Result.Result<SchemaEntry<S>, any>
131
131
 
@@ -142,12 +142,21 @@ type EntryValue<T> =
142
142
  export type FoldEntries<
143
143
  Entries extends ReadonlyArray<SpawnEntry<any>>,
144
144
  P extends Entity.ComponentProof = {}
145
- > = Entries extends readonly [
146
- infer Head extends SpawnEntry<any>,
147
- ...infer Tail extends Array<SpawnEntry<any>>
148
- ]
149
- ? FoldEntries<Tail, Draft.InsertEntry<P, EntryValue<Head>>>
150
- : P
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]>>
148
+ : Entries extends readonly [
149
+ infer Head extends SpawnEntry<any>,
150
+ ...infer Tail extends Array<SpawnEntry<any>>
151
+ ]
152
+ ? FoldEntries<Tail, Draft.InsertEntry<P, EntryValue<Head>>>
153
+ : P
154
+
155
+ /** The proof of entries that may each be present: every component they can hold, optional. */
156
+ type MaybeEntries<Entry> = {
157
+ readonly [E in Entry as E extends readonly [infer D extends Descriptor<"component", string, any>, unknown] ? Descriptor.Name<D> : never]?:
158
+ E extends readonly [infer D extends Descriptor<"component", string, any>, unknown] ? Descriptor.Value<D> : never
159
+ }
151
160
 
152
161
  /**
153
162
  * Per-entry failures, aligned with the entries: `null` for entries that
@@ -197,9 +206,10 @@ export type DeferredCommand<S extends Schema.Any> = {
197
206
  export interface InternalWorld<S extends Schema.Any> {
198
207
  /**
199
208
  * Creates the entity for an id reserved by `commands.spawn(...)` with its
200
- * staged components.
209
+ * staged components. Returns `false`, creating nothing, when the
210
+ * components break a `Schema.requires` requirement.
201
211
  */
202
- readonly spawnEntity: (id: Entity.EntityId<S, any>, components: ReadonlyArray<Entity.StagedComponent>) => void
212
+ readonly spawnEntity: (id: Entity.EntityId<S, any>, components: ReadonlyArray<Entity.StagedComponent>) => boolean
203
213
  /**
204
214
  * Removes an entity and its outgoing and incoming relations.
205
215
  */
@@ -216,13 +226,15 @@ export interface InternalWorld<S extends Schema.Any> {
216
226
  */
217
227
  readonly destroyEntityScope: (scope: EntityScope.EntityScope.Any) => void
218
228
  /**
219
- * Removes a component from a live entity.
229
+ * Removes components from a live entity, all or none: nothing is removed
230
+ * when that would leave a component without one it requires.
220
231
  */
221
- readonly removeComponent: (id: Entity.EntityId<S, any>, descriptor: Descriptor<"component", string, any>) => void
232
+ readonly removeComponents: (id: Entity.EntityId<S, any>, descriptors: ReadonlyArray<Descriptor<"component", string, any>>) => void
222
233
  /**
223
- * Inserts or replaces a component on a live entity.
234
+ * Inserts or replaces components on a live entity, all or none: nothing is
235
+ * written when an inserted component would miss one it requires.
224
236
  */
225
- readonly writeComponent: (id: Entity.EntityId<S, any>, descriptor: Descriptor<"component", string, any>, value: unknown) => void
237
+ readonly insertComponents: (id: Entity.EntityId<S, any>, entries: ReadonlyArray<Entity.StagedComponent>) => void
226
238
  /**
227
239
  * Attempts to attach one relation edge between two live entities.
228
240
  */
@@ -262,20 +274,15 @@ export const entry = <D extends Descriptor<"component", string, any>>(
262
274
  ): Entry<D> => [descriptor, value]
263
275
 
264
276
  /**
265
- * Lifts an already validated value into an entry result, keeping the failure
266
- * visible to `spawn(...)` / `insert(...)`.
267
- */
268
- export const entryResult = <D extends Descriptor<"component", string, any>, Error>(
269
- descriptor: D,
270
- result: Result.Result<Descriptor.Value<D>, Error>
271
- ): Result.Result<Entry<D>, Error> =>
272
- result.ok
273
- ? Result.success([descriptor, result.value] as Entry<D>)
274
- : Result.failure(result.error)
275
-
276
- /**
277
- * Validates raw input through a constructed descriptor and returns an entry
278
- * result.
277
+ * Validates raw input through a constructed descriptor's own validator and
278
+ * returns an entry result for `spawn(...)` / `insert(...)`.
279
+ *
280
+ * This is the one way to put unvalidated data on an entity: the descriptor
281
+ * owns its validator (`Descriptor.ConstructedComponent(...)`, built with
282
+ * `Decode` or `Descriptor.fromStandardSchema`). Values that are already
283
+ * valid use a plain `[Descriptor, value]` entry instead. To validate authored
284
+ * constants once at module load, call the constructor (`Vector2.result(...)`)
285
+ * and keep the carried value.
279
286
  *
280
287
  * @example
281
288
  * ```ts
@@ -290,15 +297,22 @@ export const entryResult = <D extends Descriptor<"component", string, any>, Erro
290
297
  export const entryRaw = <D extends DescriptorModule.ConstructedDescriptor<"component", string, any, any, any>>(
291
298
  descriptor: D,
292
299
  raw: Descriptor.Raw<D>
293
- ): Result.Result<Entry<D>, Descriptor.ConstructionError<D>> =>
294
- entryResult(descriptor, DescriptorModule.constructorOf(descriptor).result(raw) as Result.Result<Descriptor.Value<D>, Descriptor.ConstructionError<D>>)
300
+ ): Result.Result<Entry<D>, Descriptor.ConstructionError<D>> => {
301
+ const validated = DescriptorModule.constructorOf(descriptor).result(raw) as Result.Result<Descriptor.Value<D>, Descriptor.ConstructionError<D>>
302
+ return validated.ok
303
+ ? Result.success([descriptor, validated.value] as Entry<D>)
304
+ : Result.failure(validated.error)
305
+ }
295
306
 
296
307
  /**
297
- * Adds entries to a draft. Plain entries return the new draft; if any entry is
298
- * a `Result`, the draft is returned as a `Result` whose error lists every
299
- * entry's failure (or `null`).
308
+ * Adds entries to a draft, a value not yet spawned. Plain entries return the
309
+ * new draft; if any entry is a `Result`, the draft is returned as a `Result`
310
+ * whose error lists every entry's failure (or `null`).
311
+ *
312
+ * Use this while building what a spawn will hold. Use `commands.insert` to
313
+ * add or replace components on an entity that already exists.
300
314
  */
301
- export const insert = <
315
+ export const extend = <
302
316
  S extends Schema.Any,
303
317
  P extends Entity.ComponentProof,
304
318
  Root,
@@ -399,78 +413,384 @@ export const relate = <
399
413
  }
400
414
  ])
401
415
 
416
+ /** One permission of `Game.System.commands(...)`: a kind of command, and what it may touch. */
417
+ export interface Permission<out Kind extends PermissionKind, out Value> {
418
+ readonly permission: Kind
419
+ readonly values: ReadonlyArray<Value>
420
+ }
421
+
422
+ export type PermissionKind = "spawns" | "inserts" | "removes" | "despawns" | "despawnsScope" | "relates"
423
+
424
+ type ComponentDescriptor = Descriptor<"component", string, any>
425
+
426
+ /**
427
+ * Allows `spawn` / `spawnIn` of drafts whose components are all among `components`.
428
+ * Use it for entities the system creates; use `extend` on a draft to build one.
429
+ */
430
+ export const spawns = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"spawns", Components[number]> =>
431
+ ({ permission: "spawns", values: components })
432
+
433
+ /**
434
+ * Allows `insert(entity, [X, value])` with `X` among `components`: add or
435
+ * replace at the next `applyDeferred()`. To change a value now, on an entity
436
+ * you matched, write through a query (`Game.Query.write`) instead.
437
+ */
438
+ export const inserts = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"inserts", Components[number]> =>
439
+ ({ permission: "inserts", values: components })
440
+
441
+ /** Allows `remove(entity, X)` with `X` among `components`. */
442
+ export const removes = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"removes", Components[number]> =>
443
+ ({ permission: "removes", values: components })
444
+
445
+ /**
446
+ * Allows `despawn(entity)` where `entity` comes from a query match proving one
447
+ * of `components` (`match.entity`), never a raw id: the system can only
448
+ * despawn what it has proven is, for example, an `Enemy`.
449
+ */
450
+ export const despawns = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"despawns", Components[number]> =>
451
+ ({ permission: "despawns", values: components })
452
+
453
+ /** Allows `despawnScope(scope)` for this scope. */
454
+ export const despawnsScope = <const Scope extends EntityScope.EntityScope.Any>(scope: Scope): Permission<"despawnsScope", Scope> =>
455
+ ({ permission: "despawnsScope", values: [scope] })
456
+
457
+ /** Allows `relate`, `unrelate`, and `reorderChildren` for these relations. */
458
+ export const relates = <const Relations extends ReadonlyArray<Relation.Relation.Any>>(...relations: Relations): Permission<"relates", Relations[number]> =>
459
+ ({ permission: "relates", values: relations })
460
+
461
+ type PermissionValue<Permissions, Kind extends PermissionKind> =
462
+ Permissions extends Permission<Kind, infer Value> ? Value : never
463
+
464
+ /**
465
+ * The commands a system may queue, declared in its spec as
466
+ * `commands: Game.System.commands(...)`. Without it, the system has no
467
+ * `commands`.
468
+ */
469
+ export interface CommandAccess<
470
+ out Spawns = never,
471
+ out Inserts = never,
472
+ out Removes = never,
473
+ out Despawns = never,
474
+ out Scopes = never,
475
+ out Relates = never
476
+ > {
477
+ readonly kind: "commands"
478
+ readonly permissions: ReadonlyArray<Permission<PermissionKind, unknown>>
479
+ readonly __permissions?: (_: never) => readonly [Spawns, Inserts, Removes, Despawns, Scopes, Relates]
480
+ }
481
+
482
+ export namespace CommandAccess {
483
+ export type Any = CommandAccess<any, any, any, any, any, any>
484
+ }
485
+
486
+ /** Declares the commands a system may queue; see the permission builders. */
487
+ export const commands = <const Permissions extends ReadonlyArray<Permission<PermissionKind, unknown>>>(
488
+ ...permissions: Permissions
489
+ ): CommandAccess<
490
+ PermissionValue<Permissions[number], "spawns">,
491
+ PermissionValue<Permissions[number], "inserts">,
492
+ PermissionValue<Permissions[number], "removes">,
493
+ PermissionValue<Permissions[number], "despawns">,
494
+ PermissionValue<Permissions[number], "despawnsScope">,
495
+ PermissionValue<Permissions[number], "relates">
496
+ > => ({ kind: "commands", permissions })
497
+
498
+ /** The `commands` of a system that declares none. */
499
+ export type NotDeclared =
500
+ "commands are not declared for this system: add commands: Game.System.commands(...) to its spec, with the permissions it needs (spawns, inserts, removes, despawns, despawnsScope, relates)"
501
+
502
+ /** A method whose permission is not declared: any call fails with an argument error naming the fix. */
503
+ type MethodNotDeclared<Method extends string, Builder extends string> =
504
+ (...fix: ReadonlyArray<`${Method} is not declared in this system's commands: add Game.System.${Builder}(...) to Game.System.commands(...)`>) => never
505
+
506
+ type Fix<Message extends string> = { readonly __fixCommands__: Message }
507
+
508
+ type Names<D> = D extends { readonly name: infer Name extends string } ? Name : never
509
+
510
+ /** Every key of every member of a union of proofs (a draft with a conditional entry is a union). */
511
+ type AllKeys<P> = P extends unknown ? keyof P & string : never
512
+
513
+ type DraftAllowed<P, Spawns> =
514
+ [Exclude<AllKeys<P>, Names<Spawns>>] extends [never]
515
+ ? unknown
516
+ : Fix<`${Exclude<AllKeys<P>, Names<Spawns>>} is not declared in this system's commands: add it to Game.System.spawns(...)`>
517
+
518
+ type EntriesAllowed<Entries extends ReadonlyArray<readonly [ComponentDescriptor, unknown]>, Inserts> =
519
+ [Exclude<Entries[number][0], Inserts>] extends [never]
520
+ ? unknown
521
+ : Fix<`${Names<Exclude<Entries[number][0], Inserts>>} is not declared in this system's commands: add it to Game.System.inserts(...)`>
522
+
523
+ type RemoveAllowed<D, Removes> =
524
+ [D] extends [Removes] ? unknown : Fix<`${Names<D>} is not declared in this system's commands: add it to Game.System.removes(...)`>
525
+
526
+ type DespawnAllowed<E, Despawns> =
527
+ E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __proves?: (_: never) => infer Proves }
528
+ ? [Extract<Proves, Despawns>] extends [never]
529
+ ? Fix<`despawn needs an entity from a query proving ${Names<Despawns>} (declared in despawns)`>
530
+ : unknown
531
+ : E extends { readonly [spawnedTypeId]: (_: never) => infer Spawned }
532
+ ? [Extract<Spawned, Names<Despawns>>] extends [never]
533
+ ? Fix<`despawn needs an entity spawned with ${Names<Despawns>} (declared in despawns)`>
534
+ : unknown
535
+ : Fix<`despawn needs an entity from a query proving ${Names<Despawns>} (declared in despawns), not a raw id: pass match.entity`>
536
+
537
+ declare const spawnedTypeId: unique symbol
538
+
539
+ /**
540
+ * The id `spawn` / `spawnIn` return: an `EntityId` that also records the
541
+ * components its draft holds in every case, so the system that queued it can
542
+ * `despawn` it before the next `applyDeferred()` (no query can match it yet).
543
+ * Stored as a plain `EntityId`, it is a raw id again.
544
+ */
545
+ export type SpawnedId<S extends Schema.Any, Root, Components extends string> =
546
+ Entity.EntityId<S, Root> & { readonly [spawnedTypeId]: (_: never) => Components }
547
+
548
+ /** The requirements (`[requirer, required]`) of the components in `Held` that `Held` lacks. */
549
+ type MissingPairs<S extends Schema.Any, Held extends string> =
550
+ Extract<Schema.Requires<S>, readonly [Held, string]> extends infer Pair
551
+ ? Pair extends readonly [string, infer To extends string]
552
+ ? To extends Held ? never : Pair
553
+ : never
554
+ : never
555
+
556
+ /** One message per missing requirement: `"Player requires Velocity"`. */
557
+ type MissingMessages<Pairs> = Pairs extends readonly [infer From extends string, infer To extends string] ? `${From} requires ${To}` : never
558
+
559
+ /**
560
+ * Accepts a draft holding every requirement of every component it holds.
561
+ * Direct requirements are enough: each required component must be held
562
+ * too, with its own requirements.
563
+ */
564
+ type DraftRequirementsMet<S extends Schema.Any, Held extends string> =
565
+ [MissingPairs<S, Held>] extends [never]
566
+ ? unknown
567
+ : Fix<`${MissingMessages<MissingPairs<S, Held>>}: add the missing entries to the draft`>
568
+
569
+ /** What an insert or remove target is known to hold: a query match's proof, or a `SpawnedId`'s draft. A raw id proves nothing. */
570
+ type TargetHolds<E> =
571
+ E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __proves?: (_: never) => infer Proves }
572
+ ? Names<Proves>
573
+ : E extends { readonly [spawnedTypeId]: (_: never) => infer Spawned extends string }
574
+ ? Spawned
575
+ : never
576
+
577
+ /** What a remove target is known to lack: its query's `without`. */
578
+ type TargetLacks<E> =
579
+ E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __lacks?: (_: never) => infer Lacks }
580
+ ? Names<Lacks>
581
+ : never
582
+
583
+ type EntryNames<Entries extends ReadonlyArray<readonly [ComponentDescriptor, unknown]>> = Names<Entries[number][0]>
584
+
585
+ /** Inserted components with requirements neither inserted alongside nor proven held by the target. */
586
+ type InsertRequirementsMet<S extends Schema.Any, E, Entries extends ReadonlyArray<readonly [ComponentDescriptor, unknown]>> =
587
+ [MissingPairs<S, EntryNames<Entries> | TargetHolds<E>> & readonly [EntryNames<Entries>, string]] extends [never]
588
+ ? unknown
589
+ : Fix<`${MissingMessages<MissingPairs<S, EntryNames<Entries> | TargetHolds<E>> & readonly [EntryNames<Entries>, string]>}: insert it in the same call, or pass match.entity from a query that proves it`>
590
+
591
+ /**
592
+ * Removing `Removed` is sound when every component that requires one of
593
+ * them is removed in the same call or proven absent by the target's query
594
+ * (`without`): otherwise the entity may keep a requirer without its
595
+ * requirement.
596
+ */
597
+ type RemoveViolations<S extends Schema.Any, E, Removed extends string> =
598
+ Extract<Schema.Requires<S>, readonly [string, Removed]> extends infer Pair
599
+ ? Pair extends readonly [infer From extends string, infer To extends string]
600
+ ? From extends Removed | TargetLacks<E>
601
+ ? never
602
+ : `${To} is required by ${From}: remove ${From} in the same call, or match the entity with a query that has without: [${From}]`
603
+ : never
604
+ : never
605
+
606
+ type RemoveRequirementsMet<S extends Schema.Any, E, Removed extends string> =
607
+ [RemoveViolations<S, E, Removed>] extends [never] ? unknown : Fix<RemoveViolations<S, E, Removed>>
608
+
609
+ /** A remove target: `match.entity`, or the id `spawn` returned in this run; never a raw id. */
610
+ type RemoveTargetAllowed<S extends Schema.Any, Root, E> =
611
+ E extends { readonly kind: "EntityRef" | "EntityMut"; readonly id: Entity.EntityId<S, Root> }
612
+ ? unknown
613
+ : E extends SpawnedId<S, Root, string>
614
+ ? unknown
615
+ : Fix<"remove needs an entity from a query (pass match.entity) or the id spawn returned in this run, not a raw id">
616
+
617
+ /** The transitive closure of the names `N` require (cycles stop). */
618
+ type RequiredClosure<Pairs extends RequirementPair, N extends string, Seen extends string = never> =
619
+ [Exclude<N, Seen>] extends [never]
620
+ ? Seen
621
+ : RequiredClosure<Pairs, Extract<Pairs, readonly [Exclude<N, Seen>, string]>[1], Seen | N>
622
+
402
623
  /**
403
- * Public command API exposed to systems.
624
+ * The component descriptors that `Names` require, transitively
625
+ * (`Schema.requires`): what `Game.System.spawns(...)` also allows.
626
+ */
627
+ export type RequiredDescriptors<S extends Schema.Any, Names extends string> =
628
+ // Non-literal names (an unresolved descriptor, alone or next to known ones) would never
629
+ // close: leave the error to the descriptor.
630
+ 0 extends 1 & Names
631
+ ? never
632
+ : string extends Names
633
+ ? never
634
+ : [Extract<Schema.Requires<S>, readonly [Names, string]>] extends [never]
635
+ ? never
636
+ : Extract<Schema.ComponentDescriptor<S>, { readonly name: Exclude<RequiredClosure<Schema.Requires<S>, Names>, Names> }>
637
+
638
+ type SchemaComponent<S extends Schema.Any> = Extract<Schema.Components<S>[keyof Schema.Components<S>], ComponentDescriptor>
639
+
640
+ /** An entity a system has proven something about: `match.entity`, or the id `spawn` returned. */
641
+ type EntityTarget<S extends Schema.Any, Root> =
642
+ | { readonly kind: "EntityRef" | "EntityMut"; readonly id: Entity.EntityId<S, Root> }
643
+ | SpawnedId<S, Root, string>
644
+
645
+ /** The keys every member of a union of proofs requires (what a draft holds in every case). */
646
+ type RequiredKeys<P> = P extends unknown ? { [K in keyof P]-?: {} extends Pick<P, K> ? never : K }[keyof P] & string : never
647
+ type MissingSomewhere<P, All> = P extends unknown ? Exclude<All, RequiredKeys<P>> : never
648
+ type HeldNames<P> = Exclude<AllKeys<P>, MissingSomewhere<P, AllKeys<P>>>
649
+
650
+ /**
651
+ * The command API a system gets for its declared permissions. Commands are
652
+ * the structural mutation entrypoint: spawns, inserts, removals, despawns,
653
+ * and relation edits. They are queued in order and applied only at the next
654
+ * `applyDeferred()` (or `applyStateTransitions(...)`) schedule step.
404
655
  *
405
- * Commands are the structural mutation entrypoint: spawns, inserts, removals,
406
- * despawns, and relation edits. They are queued in order and applied only at
407
- * the next `applyDeferred()` (or `applyStateTransitions(...)`) schedule step.
656
+ * Each method exists only with its permission, and only for what it names:
657
+ * `spawn` needs `Game.System.spawns(...)` listing every component of the
658
+ * draft, `insert` needs `inserts`, `remove` needs `removes`, `despawn` needs
659
+ * `despawns` and an entity from a query proving one of them, `despawnScope`
660
+ * needs `despawnsScope`, and relation edits need `relates`.
408
661
  *
409
662
  * Resource and event writes are not commands: they go through the
410
663
  * `writeResource(...)` / `writeEvent(...)` access a system declares.
411
664
  */
412
- export interface CommandsApi<S extends Schema.Any, Root = unknown> {
665
+ export interface CommandsApi<
666
+ S extends Schema.Any,
667
+ Root = unknown,
668
+ Spawns = never,
669
+ Inserts = never,
670
+ Removes = never,
671
+ Despawns = never,
672
+ Scopes = never,
673
+ Relates = never
674
+ > {
413
675
  /**
414
676
  * Queues a staged entity for spawning and returns its stable runtime id.
677
+ *
678
+ * The draft must hold every component its components require
679
+ * (`Schema.requires`); drafts are checked here, so a partial draft built
680
+ * with `Game.Command.spawn(...)` and completed with `extend(...)` is fine.
415
681
  */
416
- readonly spawn: <P extends Entity.ComponentProof>(draft: Entity.EntityDraft<S, P, Root>) => Entity.EntityId<S, Root>
682
+ readonly spawn: [Spawns] extends [never]
683
+ ? MethodNotDeclared<"spawn", "spawns">
684
+ : <P extends Entity.ComponentProof>(draft: Entity.EntityDraft<S, P, Root> & DraftAllowed<P, Spawns> & DraftRequirementsMet<S, HeldNames<P>>) => SpawnedId<S, Root, HeldNames<P>>
417
685
  /**
418
- * Queues an entity spawn owned by one lifetime scope.
419
- *
420
- * Scoped entities can later be removed together with `despawnScope(...)`.
686
+ * Queues an entity spawn owned by one lifetime scope (removed together
687
+ * with `despawnScope(...)`). The draft is checked like `spawn(...)`.
421
688
  */
422
- readonly spawnIn: <P extends Entity.ComponentProof>(
423
- scope: EntityScope.EntityScope<string, Root>,
424
- draft: Entity.EntityDraft<S, P, Root>
425
- ) => Entity.EntityId<S, Root>
689
+ readonly spawnIn: [Spawns] extends [never]
690
+ ? MethodNotDeclared<"spawnIn", "spawns">
691
+ : <P extends Entity.ComponentProof>(
692
+ scope: EntityScope.EntityScope<string, Root>,
693
+ draft: Entity.EntityDraft<S, P, Root> & DraftAllowed<P, Spawns> & DraftRequirementsMet<S, HeldNames<P>>
694
+ ) => SpawnedId<S, Root, HeldNames<P>>
426
695
  /**
427
- * Queues component inserts (or replacements) on an existing entity.
696
+ * Queues component inserts (or replacements) on an existing entity, applied
697
+ * at the next `applyDeferred()`. To change a value now, on an entity you
698
+ * matched, write through a query (`Game.Query.write`) instead; components
699
+ * declared `immutable: true` change only through `insert`.
700
+ *
701
+ * Pass `match.entity`: its query proves what the entity holds, which must
702
+ * cover what the inserted components require (`Schema.requires`) together
703
+ * with the entries inserted in the same call. The id `spawn` returned in
704
+ * this run proves its draft. A raw `EntityId` proves nothing, so it only
705
+ * inserts components without requirements.
428
706
  *
429
707
  * Entries must already be valid; validate raw input with
430
708
  * `Game.Command.entryRaw(...)` first.
431
709
  */
432
- readonly insert: (
433
- entity: Entity.EntityId<S, Root>,
434
- ...entries: ReadonlyArray<SchemaEntry<S>>
435
- ) => Entity.EntityId<S, Root>
710
+ readonly insert: [Inserts] extends [never]
711
+ ? MethodNotDeclared<"insert", "inserts">
712
+ : <const E extends Entity.EntityId<S, Root> | EntityTarget<S, Root>, const Entries extends ReadonlyArray<SchemaEntry<S>>>(
713
+ entity: E,
714
+ ...entries: Entries & EntriesAllowed<Entries, Inserts> & InsertRequirementsMet<S, E, Entries>
715
+ ) => Entity.EntityId<S, Root>
436
716
  /**
437
- * Queues an entity removal.
717
+ * Queues an entity removal. A despawned entity stays visible to queries
718
+ * until the next `applyDeferred()`, and despawns never count as writers for
719
+ * `Game.System.thisTick` query reads.
720
+ *
721
+ * Pass `match.entity` from a query proving one of the `despawns`
722
+ * components, or the id `spawn` returned in this run (it holds its draft's
723
+ * components); never a raw id.
438
724
  */
439
- readonly despawn: (entity: Entity.EntityId<S, Root>) => void
725
+ readonly despawn: [Despawns] extends [never]
726
+ ? MethodNotDeclared<"despawn", "despawns">
727
+ : <const E>(entity: unknown extends DespawnAllowed<E, Despawns> ? E : DespawnAllowed<E, Despawns>) => void
440
728
  /**
441
729
  * Queues removal of every entity owned by one lifetime scope.
442
730
  */
443
- readonly despawnScope: (scope: EntityScope.EntityScope<string, Root>) => void
731
+ readonly despawnScope: [Scopes] extends [never]
732
+ ? MethodNotDeclared<"despawnScope", "despawnsScope">
733
+ : (scope: Scopes) => void
444
734
  /**
445
- * Queues a component removal on an existing entity.
735
+ * Queues the removal of one or more components from an existing entity,
736
+ * applied together at the next `applyDeferred()`.
737
+ *
738
+ * Pass `match.entity` from a query, or the id `spawn` returned in this run;
739
+ * never a raw id. A component another one requires (`Schema.requires`) can
740
+ * only go together with that requirer, in the same call, or from an
741
+ * entity whose query has `without: [Requirer]`.
446
742
  */
447
- readonly remove: <D extends Extract<Schema.Components<S>[keyof Schema.Components<S>], Descriptor<"component", string, any>>>(
448
- entity: Entity.EntityId<S, Root>,
449
- descriptor: D
450
- ) => Entity.EntityId<S, Root>
743
+ readonly remove: [Removes] extends [never]
744
+ ? MethodNotDeclared<"remove", "removes">
745
+ : <
746
+ const E,
747
+ const Ds extends readonly [SchemaComponent<S>, ...Array<SchemaComponent<S>>]
748
+ >(
749
+ entity: unknown extends RemoveTargetAllowed<S, Root, E> ? E : RemoveTargetAllowed<S, Root, E>,
750
+ ...descriptors: Ds & RemoveAllowed<Ds[number], Removes> & RemoveRequirementsMet<S, E, Names<Ds[number]>>
751
+ ) => Entity.EntityId<S, Root>
451
752
  /**
452
753
  * Queues one live relation mutation for deferred application.
453
754
  */
454
- readonly relate: <R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Any>>(
455
- entity: Entity.EntityId<S, Root>,
456
- relation: R,
457
- target: Entity.EntityId<S, Root>
458
- ) => void
755
+ readonly relate: [Relates] extends [never]
756
+ ? MethodNotDeclared<"relate", "relates">
757
+ : <R extends Extract<Relates, Relation.Relation.Any>>(
758
+ entity: Entity.EntityId<S, Root>,
759
+ relation: R,
760
+ target: Entity.EntityId<S, Root>
761
+ ) => void
459
762
  /**
460
763
  * Queues removal of one outgoing relation when present.
461
764
  */
462
- readonly unrelate: <R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Any>>(
463
- entity: Entity.EntityId<S, Root>,
464
- relation: R
465
- ) => void
765
+ readonly unrelate: [Relates] extends [never]
766
+ ? MethodNotDeclared<"unrelate", "relates">
767
+ : <R extends Extract<Relates, Relation.Relation.Any>>(
768
+ entity: Entity.EntityId<S, Root>,
769
+ relation: R
770
+ ) => void
466
771
  /**
467
772
  * Queues a hierarchy-only reorder of one parent's current children.
468
773
  */
469
- readonly reorderChildren: <R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
470
- entity: Entity.EntityId<S, Root>,
471
- relation: R,
472
- children: ReadonlyArray<Entity.EntityId<S, Root>>
473
- ) => void
774
+ readonly reorderChildren: [Relates] extends [never]
775
+ ? MethodNotDeclared<"reorderChildren", "relates">
776
+ : <R extends Extract<Relates, Relation.Relation.Hierarchy>>(
777
+ entity: Entity.EntityId<S, Root>,
778
+ relation: R,
779
+ children: ReadonlyArray<Entity.EntityId<S, Root>>
780
+ ) => void
781
+ }
782
+
783
+ /** The command queue the runtime gives each system: every command, unchecked, and `flush`. */
784
+ export interface CommandQueue<S extends Schema.Any, Root = unknown> {
785
+ readonly spawn: (draft: Entity.EntityDraft<S, any, Root>) => Entity.EntityId<S, Root>
786
+ readonly spawnIn: (scope: EntityScope.EntityScope<string, Root>, draft: Entity.EntityDraft<S, any, Root>) => Entity.EntityId<S, Root>
787
+ readonly insert: (entity: Entity.EntityId<S, Root> | { readonly id: Entity.EntityId<S, Root> }, ...entries: ReadonlyArray<SchemaEntry<S>>) => Entity.EntityId<S, Root>
788
+ readonly despawn: (entity: Entity.EntityId<S, Root> | { readonly id: Entity.EntityId<S, Root> }) => void
789
+ readonly despawnScope: (scope: EntityScope.EntityScope<string, Root>) => void
790
+ readonly remove: (entity: Entity.EntityId<S, Root> | { readonly id: Entity.EntityId<S, Root> }, ...descriptors: ReadonlyArray<ComponentDescriptor>) => Entity.EntityId<S, Root>
791
+ readonly relate: (entity: Entity.EntityId<S, Root>, relation: Relation.Relation.Any, target: Entity.EntityId<S, Root>) => void
792
+ readonly unrelate: (entity: Entity.EntityId<S, Root>, relation: Relation.Relation.Any) => void
793
+ readonly reorderChildren: (entity: Entity.EntityId<S, Root>, relation: Relation.Relation.Hierarchy, children: ReadonlyArray<Entity.EntityId<S, Root>>) => void
474
794
  /**
475
795
  * Drains the queued commands in insertion order.
476
796
  */
@@ -485,7 +805,7 @@ export interface CommandsApi<S extends Schema.Any, Root = unknown> {
485
805
  */
486
806
  export const makeCommands = <S extends Schema.Any, Root = unknown>(
487
807
  allocateId: () => Entity.EntityId<S, Root>
488
- ): CommandsApi<S, Root> => {
808
+ ): CommandQueue<S, Root> => {
489
809
  /**
490
810
  * The per-system command buffer.
491
811
  *
@@ -495,12 +815,12 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
495
815
  const queue: Array<DeferredCommand<S>> = []
496
816
 
497
817
  return {
498
- spawn<P extends Entity.ComponentProof>(draft: Entity.EntityDraft<S, P, Root>): Entity.EntityId<S, Root> {
818
+ spawn(draft) {
499
819
  const id = allocateId()
500
820
  queue.push({
501
821
  tag: "spawn",
502
822
  apply(world) {
503
- world.spawnEntity(id, draft.components)
823
+ if (!world.spawnEntity(id, draft.components)) return
504
824
  for (const stagedRelation of draft.relations) {
505
825
  world.tryRelate(id, stagedRelation.relation, stagedRelation.target)
506
826
  }
@@ -508,15 +828,12 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
508
828
  })
509
829
  return id
510
830
  },
511
- spawnIn<P extends Entity.ComponentProof>(
512
- scope: EntityScope.EntityScope<string, Root>,
513
- draft: Entity.EntityDraft<S, P, Root>
514
- ): Entity.EntityId<S, Root> {
831
+ spawnIn(scope, draft) {
515
832
  const id = allocateId()
516
833
  queue.push({
517
834
  tag: "spawnIn",
518
835
  apply(world) {
519
- world.spawnEntity(id, draft.components)
836
+ if (!world.spawnEntity(id, draft.components)) return
520
837
  world.assignEntityScope(id, scope)
521
838
  for (const stagedRelation of draft.relations) {
522
839
  world.tryRelate(id, stagedRelation.relation, stagedRelation.target)
@@ -525,22 +842,22 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
525
842
  })
526
843
  return id
527
844
  },
528
- insert(entity, ...entries) {
845
+ insert(target, ...entries) {
846
+ const entity = typeof target === "object" ? target.id : target
529
847
  queue.push({
530
848
  tag: "insert",
531
849
  apply(world) {
532
- for (const [descriptor, value] of entries) {
533
- world.writeComponent(entity, descriptor, value)
534
- }
850
+ world.insertComponents(entity, entries)
535
851
  }
536
852
  })
537
853
  return entity
538
854
  },
539
855
  despawn(entity) {
856
+ const id = typeof entity === "object" ? entity.id : entity
540
857
  queue.push({
541
858
  tag: "despawn",
542
859
  apply(world) {
543
- world.destroyEntity(entity)
860
+ world.destroyEntity(id)
544
861
  }
545
862
  })
546
863
  },
@@ -552,11 +869,12 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
552
869
  }
553
870
  })
554
871
  },
555
- remove(entity, descriptor) {
872
+ remove(target, ...descriptors) {
873
+ const entity = typeof target === "object" ? target.id : target
556
874
  queue.push({
557
875
  tag: "remove",
558
876
  apply(world) {
559
- world.removeComponent(entity, descriptor)
877
+ world.removeComponents(entity, descriptors)
560
878
  }
561
879
  })
562
880
  return entity