@typeonce/ritmo 0.6.0 → 0.7.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 (125) 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 +157 -33
  11. package/dist/Command.d.ts.map +1 -1
  12. package/dist/Command.js +49 -15
  13. package/dist/Command.js.map +1 -1
  14. package/dist/Debug.d.ts +104 -12
  15. package/dist/Debug.d.ts.map +1 -1
  16. package/dist/Debug.js.map +1 -1
  17. package/dist/Decode.d.ts +1 -1
  18. package/dist/Decode.js +1 -1
  19. package/dist/Descriptor.d.ts +31 -21
  20. package/dist/Descriptor.d.ts.map +1 -1
  21. package/dist/Descriptor.js +21 -12
  22. package/dist/Descriptor.js.map +1 -1
  23. package/dist/Entity.d.ts +14 -5
  24. package/dist/Entity.d.ts.map +1 -1
  25. package/dist/Entity.js +2 -2
  26. package/dist/Entity.js.map +1 -1
  27. package/dist/EntityScope.d.ts +6 -0
  28. package/dist/EntityScope.d.ts.map +1 -1
  29. package/dist/EntityScope.js +6 -0
  30. package/dist/EntityScope.js.map +1 -1
  31. package/dist/HostClock.d.ts +49 -0
  32. package/dist/HostClock.d.ts.map +1 -0
  33. package/dist/HostClock.js +46 -0
  34. package/dist/HostClock.js.map +1 -0
  35. package/dist/Inspector.d.ts +7 -1
  36. package/dist/Inspector.d.ts.map +1 -1
  37. package/dist/Inspector.js.map +1 -1
  38. package/dist/Machine.d.ts +12 -3
  39. package/dist/Machine.d.ts.map +1 -1
  40. package/dist/Machine.js +4 -1
  41. package/dist/Machine.js.map +1 -1
  42. package/dist/Query.d.ts +28 -24
  43. package/dist/Query.d.ts.map +1 -1
  44. package/dist/Query.js +0 -24
  45. package/dist/Query.js.map +1 -1
  46. package/dist/Runtime.d.ts +36 -16
  47. package/dist/Runtime.d.ts.map +1 -1
  48. package/dist/Runtime.js +230 -67
  49. package/dist/Runtime.js.map +1 -1
  50. package/dist/Schedule.d.ts +217 -10
  51. package/dist/Schedule.d.ts.map +1 -1
  52. package/dist/Schedule.js +5 -0
  53. package/dist/Schedule.js.map +1 -1
  54. package/dist/Schema.d.ts +61 -25
  55. package/dist/Schema.d.ts.map +1 -1
  56. package/dist/Schema.js +0 -8
  57. package/dist/Schema.js.map +1 -1
  58. package/dist/System.d.ts +149 -52
  59. package/dist/System.d.ts.map +1 -1
  60. package/dist/System.js +52 -6
  61. package/dist/System.js.map +1 -1
  62. package/dist/index.d.ts +8 -7
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +8 -7
  65. package/dist/index.js.map +1 -1
  66. package/dist/internal/cells.d.ts +3 -3
  67. package/dist/internal/cells.d.ts.map +1 -1
  68. package/dist/internal/cells.js +9 -11
  69. package/dist/internal/cells.js.map +1 -1
  70. package/dist/internal/debug.d.ts +6 -0
  71. package/dist/internal/debug.d.ts.map +1 -1
  72. package/dist/internal/debug.js +95 -1
  73. package/dist/internal/debug.js.map +1 -1
  74. package/dist/internal/game.d.ts.map +1 -1
  75. package/dist/internal/game.js +9 -2
  76. package/dist/internal/game.js.map +1 -1
  77. package/dist/internal/order.d.ts +10 -0
  78. package/dist/internal/order.d.ts.map +1 -0
  79. package/dist/internal/order.js +120 -0
  80. package/dist/internal/order.js.map +1 -0
  81. package/dist/internal/queries.js +2 -2
  82. package/dist/internal/queries.js.map +1 -1
  83. package/package.json +1 -1
  84. package/src/Brand.ts +9 -4
  85. package/src/Clock.ts +311 -0
  86. package/src/Command.ts +271 -73
  87. package/src/Debug.ts +111 -10
  88. package/src/Decode.ts +1 -1
  89. package/src/Descriptor.ts +37 -27
  90. package/src/Entity.ts +15 -5
  91. package/src/EntityScope.ts +6 -0
  92. package/src/HostClock.ts +64 -0
  93. package/src/Inspector.ts +7 -2
  94. package/src/Machine.ts +13 -3
  95. package/src/Query.ts +41 -33
  96. package/src/Runtime.ts +273 -91
  97. package/src/Schedule.ts +380 -13
  98. package/src/Schema.ts +70 -41
  99. package/src/System.ts +174 -88
  100. package/src/index.ts +8 -7
  101. package/src/internal/cells.ts +10 -12
  102. package/src/internal/debug.ts +104 -4
  103. package/src/internal/game.ts +11 -3
  104. package/src/internal/order.ts +140 -0
  105. package/src/internal/queries.ts +3 -3
  106. package/dist/Definition.d.ts +0 -99
  107. package/dist/Definition.d.ts.map +0 -1
  108. package/dist/Definition.js +0 -68
  109. package/dist/Definition.js.map +0 -1
  110. package/dist/Feature.d.ts +0 -106
  111. package/dist/Feature.d.ts.map +0 -1
  112. package/dist/Feature.js +0 -87
  113. package/dist/Feature.js.map +0 -1
  114. package/dist/Fx.d.ts +0 -128
  115. package/dist/Fx.d.ts.map +0 -1
  116. package/dist/Fx.js +0 -112
  117. package/dist/Fx.js.map +0 -1
  118. package/dist/internal/definition.d.ts +0 -17
  119. package/dist/internal/definition.d.ts.map +0 -1
  120. package/dist/internal/definition.js +0 -25
  121. package/dist/internal/definition.js.map +0 -1
  122. package/src/Definition.ts +0 -106
  123. package/src/Feature.ts +0 -241
  124. package/src/Fx.ts +0 -176
  125. package/src/internal/definition.ts +0 -56
package/src/Command.ts CHANGED
@@ -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
@@ -262,20 +271,15 @@ export const entry = <D extends Descriptor<"component", string, any>>(
262
271
  ): Entry<D> => [descriptor, value]
263
272
 
264
273
  /**
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.
274
+ * Validates raw input through a constructed descriptor's own validator and
275
+ * returns an entry result for `spawn(...)` / `insert(...)`.
276
+ *
277
+ * This is the one way to put unvalidated data on an entity: the descriptor
278
+ * owns its validator (`Descriptor.ConstructedComponent(...)`, built with
279
+ * `Decode` or `Descriptor.fromStandardSchema`). Values that are already
280
+ * valid use a plain `[Descriptor, value]` entry instead. To validate authored
281
+ * constants once at module load, call the constructor (`Vector2.result(...)`)
282
+ * and keep the carried value.
279
283
  *
280
284
  * @example
281
285
  * ```ts
@@ -290,15 +294,22 @@ export const entryResult = <D extends Descriptor<"component", string, any>, Erro
290
294
  export const entryRaw = <D extends DescriptorModule.ConstructedDescriptor<"component", string, any, any, any>>(
291
295
  descriptor: D,
292
296
  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>>)
297
+ ): Result.Result<Entry<D>, Descriptor.ConstructionError<D>> => {
298
+ const validated = DescriptorModule.constructorOf(descriptor).result(raw) as Result.Result<Descriptor.Value<D>, Descriptor.ConstructionError<D>>
299
+ return validated.ok
300
+ ? Result.success([descriptor, validated.value] as Entry<D>)
301
+ : Result.failure(validated.error)
302
+ }
295
303
 
296
304
  /**
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`).
305
+ * Adds entries to a draft, a value not yet spawned. Plain entries return the
306
+ * new draft; if any entry is a `Result`, the draft is returned as a `Result`
307
+ * whose error lists every entry's failure (or `null`).
308
+ *
309
+ * Use this while building what a spawn will hold. Use `commands.insert` to
310
+ * add or replace components on an entity that already exists.
300
311
  */
301
- export const insert = <
312
+ export const extend = <
302
313
  S extends Schema.Any,
303
314
  P extends Entity.ComponentProof,
304
315
  Root,
@@ -399,78 +410,267 @@ export const relate = <
399
410
  }
400
411
  ])
401
412
 
413
+ /** One permission of `Game.System.commands(...)`: a kind of command, and what it may touch. */
414
+ export interface Permission<out Kind extends PermissionKind, out Value> {
415
+ readonly permission: Kind
416
+ readonly values: ReadonlyArray<Value>
417
+ }
418
+
419
+ export type PermissionKind = "spawns" | "inserts" | "removes" | "despawns" | "despawnsScope" | "relates"
420
+
421
+ type ComponentDescriptor = Descriptor<"component", string, any>
422
+
423
+ /**
424
+ * Allows `spawn` / `spawnIn` of drafts whose components are all among `components`.
425
+ * Use it for entities the system creates; use `extend` on a draft to build one.
426
+ */
427
+ export const spawns = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"spawns", Components[number]> =>
428
+ ({ permission: "spawns", values: components })
429
+
430
+ /**
431
+ * Allows `insert(entity, [X, value])` with `X` among `components`: add or
432
+ * replace at the next `applyDeferred()`. To change a value now, on an entity
433
+ * you matched, write through a query (`Game.Query.write`) instead.
434
+ */
435
+ export const inserts = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"inserts", Components[number]> =>
436
+ ({ permission: "inserts", values: components })
437
+
438
+ /** Allows `remove(entity, X)` with `X` among `components`. */
439
+ export const removes = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"removes", Components[number]> =>
440
+ ({ permission: "removes", values: components })
441
+
402
442
  /**
403
- * Public command API exposed to systems.
443
+ * Allows `despawn(entity)` where `entity` comes from a query match proving one
444
+ * of `components` (`match.entity`), never a raw id: the system can only
445
+ * despawn what it has proven is, for example, an `Enemy`.
446
+ */
447
+ export const despawns = <const Components extends ReadonlyArray<ComponentDescriptor>>(...components: Components): Permission<"despawns", Components[number]> =>
448
+ ({ permission: "despawns", values: components })
449
+
450
+ /** Allows `despawnScope(scope)` for this scope. */
451
+ export const despawnsScope = <const Scope extends EntityScope.EntityScope.Any>(scope: Scope): Permission<"despawnsScope", Scope> =>
452
+ ({ permission: "despawnsScope", values: [scope] })
453
+
454
+ /** 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]> =>
456
+ ({ permission: "relates", values: relations })
457
+
458
+ type PermissionValue<Permissions, Kind extends PermissionKind> =
459
+ Permissions extends Permission<Kind, infer Value> ? Value : never
460
+
461
+ /**
462
+ * The commands a system may queue, declared in its spec as
463
+ * `commands: Game.System.commands(...)`. Without it, the system has no
464
+ * `commands`.
465
+ */
466
+ export interface CommandAccess<
467
+ out Spawns = never,
468
+ out Inserts = never,
469
+ out Removes = never,
470
+ out Despawns = never,
471
+ out Scopes = never,
472
+ out Relates = never
473
+ > {
474
+ readonly kind: "commands"
475
+ readonly permissions: ReadonlyArray<Permission<PermissionKind, unknown>>
476
+ readonly __permissions?: (_: never) => readonly [Spawns, Inserts, Removes, Despawns, Scopes, Relates]
477
+ }
478
+
479
+ export namespace CommandAccess {
480
+ export type Any = CommandAccess<any, any, any, any, any, any>
481
+ }
482
+
483
+ /** Declares the commands a system may queue; see the permission builders. */
484
+ export const commands = <const Permissions extends ReadonlyArray<Permission<PermissionKind, unknown>>>(
485
+ ...permissions: Permissions
486
+ ): CommandAccess<
487
+ PermissionValue<Permissions[number], "spawns">,
488
+ PermissionValue<Permissions[number], "inserts">,
489
+ PermissionValue<Permissions[number], "removes">,
490
+ PermissionValue<Permissions[number], "despawns">,
491
+ PermissionValue<Permissions[number], "despawnsScope">,
492
+ PermissionValue<Permissions[number], "relates">
493
+ > => ({ kind: "commands", permissions })
494
+
495
+ /** The `commands` of a system that declares none. */
496
+ export type NotDeclared =
497
+ "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)"
498
+
499
+ /** A method whose permission is not declared: any call fails with an argument error naming the fix. */
500
+ type MethodNotDeclared<Method extends string, Builder extends string> =
501
+ (...fix: ReadonlyArray<`${Method} is not declared in this system's commands: add Game.System.${Builder}(...) to Game.System.commands(...)`>) => never
502
+
503
+ type Fix<Message extends string> = { readonly __fixCommands__: Message }
504
+
505
+ type Names<D> = D extends { readonly name: infer Name extends string } ? Name : never
506
+
507
+ /** Every key of every member of a union of proofs (a draft with a conditional entry is a union). */
508
+ type AllKeys<P> = P extends unknown ? keyof P & string : never
509
+
510
+ type DraftAllowed<P, Spawns> =
511
+ [Exclude<AllKeys<P>, Names<Spawns>>] extends [never]
512
+ ? unknown
513
+ : Fix<`${Exclude<AllKeys<P>, Names<Spawns>>} is not declared in this system's commands: add it to Game.System.spawns(...)`>
514
+
515
+ type EntriesAllowed<Entries extends ReadonlyArray<readonly [ComponentDescriptor, unknown]>, Inserts> =
516
+ [Exclude<Entries[number][0], Inserts>] extends [never]
517
+ ? unknown
518
+ : Fix<`${Names<Exclude<Entries[number][0], Inserts>>} is not declared in this system's commands: add it to Game.System.inserts(...)`>
519
+
520
+ type RemoveAllowed<D, Removes> =
521
+ [D] extends [Removes] ? unknown : Fix<`${Names<D>} is not declared in this system's commands: add it to Game.System.removes(...)`>
522
+
523
+ type DespawnAllowed<E, Despawns> =
524
+ E extends { readonly kind: "EntityRef" | "EntityMut"; readonly __proves?: (_: never) => infer Proves }
525
+ ? [Extract<Proves, Despawns>] extends [never]
526
+ ? Fix<`despawn needs an entity from a query proving ${Names<Despawns>} (declared in despawns)`>
527
+ : unknown
528
+ : E extends { readonly [spawnedTypeId]: (_: never) => infer Spawned }
529
+ ? [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`>
533
+
534
+ declare const spawnedTypeId: unique symbol
535
+
536
+ /**
537
+ * The id `spawn` / `spawnIn` return: an `EntityId` that also records the
538
+ * components its draft holds in every case, so the system that queued it can
539
+ * `despawn` it before the next `applyDeferred()` (no query can match it yet).
540
+ * Stored as a plain `EntityId`, it is a raw id again.
541
+ */
542
+ export type SpawnedId<S extends Schema.Any, Root, Components extends string> =
543
+ Entity.EntityId<S, Root> & { readonly [spawnedTypeId]: (_: never) => Components }
544
+
545
+ /** The keys every member of a union of proofs requires (what a draft holds in every case). */
546
+ type RequiredKeys<P> = P extends unknown ? { [K in keyof P]-?: {} extends Pick<P, K> ? never : K }[keyof P] & string : never
547
+ type MissingSomewhere<P, All> = P extends unknown ? Exclude<All, RequiredKeys<P>> : never
548
+ type HeldNames<P> = Exclude<AllKeys<P>, MissingSomewhere<P, AllKeys<P>>>
549
+
550
+ /**
551
+ * The command API a system gets for its declared permissions. Commands are
552
+ * the structural mutation entrypoint: spawns, inserts, removals, despawns,
553
+ * and relation edits. They are queued in order and applied only at the next
554
+ * `applyDeferred()` (or `applyStateTransitions(...)`) schedule step.
404
555
  *
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.
556
+ * Each method exists only with its permission, and only for what it names:
557
+ * `spawn` needs `Game.System.spawns(...)` listing every component of the
558
+ * draft, `insert` needs `inserts`, `remove` needs `removes`, `despawn` needs
559
+ * `despawns` and an entity from a query proving one of them, `despawnScope`
560
+ * needs `despawnsScope`, and relation edits need `relates`.
408
561
  *
409
562
  * Resource and event writes are not commands: they go through the
410
563
  * `writeResource(...)` / `writeEvent(...)` access a system declares.
411
564
  */
412
- export interface CommandsApi<S extends Schema.Any, Root = unknown> {
565
+ export interface CommandsApi<
566
+ S extends Schema.Any,
567
+ Root = unknown,
568
+ Spawns = never,
569
+ Inserts = never,
570
+ Removes = never,
571
+ Despawns = never,
572
+ Scopes = never,
573
+ Relates = never
574
+ > {
413
575
  /**
414
576
  * Queues a staged entity for spawning and returns its stable runtime id.
415
577
  */
416
- readonly spawn: <P extends Entity.ComponentProof>(draft: Entity.EntityDraft<S, P, Root>) => Entity.EntityId<S, Root>
578
+ readonly spawn: [Spawns] extends [never]
579
+ ? MethodNotDeclared<"spawn", "spawns">
580
+ : <P extends Entity.ComponentProof>(draft: Entity.EntityDraft<S, P, Root> & DraftAllowed<P, Spawns>) => SpawnedId<S, Root, HeldNames<P>>
417
581
  /**
418
- * Queues an entity spawn owned by one lifetime scope.
419
- *
420
- * Scoped entities can later be removed together with `despawnScope(...)`.
582
+ * Queues an entity spawn owned by one lifetime scope (removed together
583
+ * with `despawnScope(...)`).
421
584
  */
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>
585
+ readonly spawnIn: [Spawns] extends [never]
586
+ ? MethodNotDeclared<"spawnIn", "spawns">
587
+ : <P extends Entity.ComponentProof>(
588
+ scope: EntityScope.EntityScope<string, Root>,
589
+ draft: Entity.EntityDraft<S, P, Root> & DraftAllowed<P, Spawns>
590
+ ) => SpawnedId<S, Root, HeldNames<P>>
426
591
  /**
427
- * Queues component inserts (or replacements) on an existing entity.
592
+ * Queues component inserts (or replacements) on an existing entity, applied
593
+ * at the next `applyDeferred()`. To change a value now, on an entity you
594
+ * matched, write through a query (`Game.Query.write`) instead.
428
595
  *
429
596
  * Entries must already be valid; validate raw input with
430
597
  * `Game.Command.entryRaw(...)` first.
431
598
  */
432
- readonly insert: (
433
- entity: Entity.EntityId<S, Root>,
434
- ...entries: ReadonlyArray<SchemaEntry<S>>
435
- ) => Entity.EntityId<S, Root>
599
+ readonly insert: [Inserts] extends [never]
600
+ ? MethodNotDeclared<"insert", "inserts">
601
+ : <const Entries extends ReadonlyArray<SchemaEntry<S>>>(
602
+ entity: Entity.EntityId<S, Root>,
603
+ ...entries: Entries & EntriesAllowed<Entries, Inserts>
604
+ ) => Entity.EntityId<S, Root>
436
605
  /**
437
- * Queues an entity removal.
606
+ * Queues an entity removal. A despawned entity stays visible to queries
607
+ * until the next `applyDeferred()`, and despawns never count as writers for
608
+ * `Game.System.thisTick` query reads.
609
+ *
610
+ * Pass `match.entity` from a query proving one of the `despawns`
611
+ * components, or the id `spawn` returned in this run (it holds its draft's
612
+ * components); never a raw id.
438
613
  */
439
- readonly despawn: (entity: Entity.EntityId<S, Root>) => void
614
+ readonly despawn: [Despawns] extends [never]
615
+ ? MethodNotDeclared<"despawn", "despawns">
616
+ : <const E>(entity: E & DespawnAllowed<E, Despawns>) => void
440
617
  /**
441
618
  * Queues removal of every entity owned by one lifetime scope.
442
619
  */
443
- readonly despawnScope: (scope: EntityScope.EntityScope<string, Root>) => void
620
+ readonly despawnScope: [Scopes] extends [never]
621
+ ? MethodNotDeclared<"despawnScope", "despawnsScope">
622
+ : (scope: Scopes) => void
444
623
  /**
445
624
  * Queues a component removal on an existing entity.
446
625
  */
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>
626
+ readonly remove: [Removes] extends [never]
627
+ ? 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>
631
+ ) => Entity.EntityId<S, Root>
451
632
  /**
452
633
  * Queues one live relation mutation for deferred application.
453
634
  */
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
635
+ readonly relate: [Relates] extends [never]
636
+ ? MethodNotDeclared<"relate", "relates">
637
+ : <R extends Extract<Relates, Relation.Relation.Any>>(
638
+ entity: Entity.EntityId<S, Root>,
639
+ relation: R,
640
+ target: Entity.EntityId<S, Root>
641
+ ) => void
459
642
  /**
460
643
  * Queues removal of one outgoing relation when present.
461
644
  */
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
645
+ readonly unrelate: [Relates] extends [never]
646
+ ? MethodNotDeclared<"unrelate", "relates">
647
+ : <R extends Extract<Relates, Relation.Relation.Any>>(
648
+ entity: Entity.EntityId<S, Root>,
649
+ relation: R
650
+ ) => void
466
651
  /**
467
652
  * Queues a hierarchy-only reorder of one parent's current children.
468
653
  */
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
654
+ readonly reorderChildren: [Relates] extends [never]
655
+ ? MethodNotDeclared<"reorderChildren", "relates">
656
+ : <R extends Extract<Relates, Relation.Relation.Hierarchy>>(
657
+ entity: Entity.EntityId<S, Root>,
658
+ relation: R,
659
+ children: ReadonlyArray<Entity.EntityId<S, Root>>
660
+ ) => void
661
+ }
662
+
663
+ /** The command queue the runtime gives each system: every command, unchecked, and `flush`. */
664
+ 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>
668
+ readonly despawn: (entity: Entity.EntityId<S, Root> | { readonly id: Entity.EntityId<S, Root> }) => void
669
+ readonly despawnScope: (scope: EntityScope.EntityScope<string, Root>) => void
670
+ readonly remove: (entity: Entity.EntityId<S, Root>, descriptor: ComponentDescriptor) => Entity.EntityId<S, Root>
671
+ readonly relate: (entity: Entity.EntityId<S, Root>, relation: Relation.Relation.Any, target: Entity.EntityId<S, Root>) => void
672
+ readonly unrelate: (entity: Entity.EntityId<S, Root>, relation: Relation.Relation.Any) => void
673
+ readonly reorderChildren: (entity: Entity.EntityId<S, Root>, relation: Relation.Relation.Hierarchy, children: ReadonlyArray<Entity.EntityId<S, Root>>) => void
474
674
  /**
475
675
  * Drains the queued commands in insertion order.
476
676
  */
@@ -485,7 +685,7 @@ export interface CommandsApi<S extends Schema.Any, Root = unknown> {
485
685
  */
486
686
  export const makeCommands = <S extends Schema.Any, Root = unknown>(
487
687
  allocateId: () => Entity.EntityId<S, Root>
488
- ): CommandsApi<S, Root> => {
688
+ ): CommandQueue<S, Root> => {
489
689
  /**
490
690
  * The per-system command buffer.
491
691
  *
@@ -495,7 +695,7 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
495
695
  const queue: Array<DeferredCommand<S>> = []
496
696
 
497
697
  return {
498
- spawn<P extends Entity.ComponentProof>(draft: Entity.EntityDraft<S, P, Root>): Entity.EntityId<S, Root> {
698
+ spawn(draft) {
499
699
  const id = allocateId()
500
700
  queue.push({
501
701
  tag: "spawn",
@@ -508,10 +708,7 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
508
708
  })
509
709
  return id
510
710
  },
511
- spawnIn<P extends Entity.ComponentProof>(
512
- scope: EntityScope.EntityScope<string, Root>,
513
- draft: Entity.EntityDraft<S, P, Root>
514
- ): Entity.EntityId<S, Root> {
711
+ spawnIn(scope, draft) {
515
712
  const id = allocateId()
516
713
  queue.push({
517
714
  tag: "spawnIn",
@@ -537,10 +734,11 @@ export const makeCommands = <S extends Schema.Any, Root = unknown>(
537
734
  return entity
538
735
  },
539
736
  despawn(entity) {
737
+ const id = typeof entity === "object" ? entity.id : entity
540
738
  queue.push({
541
739
  tag: "despawn",
542
740
  apply(world) {
543
- world.destroyEntity(entity)
741
+ world.destroyEntity(id)
544
742
  }
545
743
  })
546
744
  },
package/src/Debug.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Opt-in runtime introspection for tools, tests, and coding agents.
3
3
  *
4
- * A runtime made with `debug: true` carries a `debug` handle. Runtimes made
4
+ * A runtime made with `debug: { hostClock }` carries a `debug` handle. Runtimes made
5
5
  * without it have no handle at all, so production code cannot reach into
6
6
  * internals by accident and pays nothing for them.
7
7
  *
@@ -35,7 +35,7 @@
35
35
  *
36
36
  * @example
37
37
  * ```ts
38
- * const runtime = Game.Runtime.make({ services, resources, debug: true })
38
+ * const runtime = Game.Runtime.make({ services, resources, debug: { hostClock: HostClock.standard } })
39
39
  * runtime.debug.nameSchedules({ setup: setupSchedule, update: updateSchedule })
40
40
  *
41
41
  * const stop = runtime.debug.observe((event) => {
@@ -46,15 +46,16 @@
46
46
  * stop()
47
47
  * ```
48
48
  */
49
+ import type { HostClock } from "./HostClock.ts"
49
50
  import type * as Machine from "./Machine.ts"
50
- import type { ExecutableScheduleDefinition } from "./Schedule.ts"
51
+ import type { AnyScheduleOrder, ExecutableScheduleDefinition } from "./Schedule.ts"
51
52
  import type { Schema } from "./Schema.ts"
52
53
 
53
54
  /** Version of the description, dump, and trace formats. */
54
55
  export const formatVersion = 1 as const
55
56
 
56
57
  /**
57
- * The debug handle of a runtime made with `debug: true`.
58
+ * The debug handle of a runtime made with `debug: { hostClock }`.
58
59
  */
59
60
  export interface Handle<S extends Schema.Any = Schema.Any, Root = unknown> {
60
61
  /**
@@ -64,8 +65,15 @@ export interface Handle<S extends Schema.Any = Schema.Any, Root = unknown> {
64
65
  * `onEnter(Game/Flow=Playing)`. Naming again replaces earlier names.
65
66
  */
66
67
  readonly nameSchedules: (
67
- schedules: Readonly<Record<string, ExecutableScheduleDefinition<S, any, Root, any, any>>>
68
+ schedules: Readonly<Record<string, ExecutableScheduleDefinition<S, any, Root, any, any, AnyScheduleOrder>>>
68
69
  ) => void
70
+ /**
71
+ * Declares the events the host publishes with `runtime.send`, so
72
+ * `describe()` counts them as written (`event-never-written`) before the
73
+ * first send happens. Events actually sent count as well. Declaring again
74
+ * replaces the list.
75
+ */
76
+ readonly declareHostEvents: (events: ReadonlyArray<Schema.EventDescriptor<S>>) => void
69
77
  /** The static description of the schema and the named schedules. */
70
78
  readonly describe: () => Description
71
79
  /** The current world as plain data. */
@@ -75,6 +83,21 @@ export interface Handle<S extends Schema.Any = Schema.Any, Root = unknown> {
75
83
  * active only while at least one listener is registered.
76
84
  */
77
85
  readonly observe: (listener: (event: TraceEvent) => void) => () => void
86
+ /**
87
+ * Delivers what the host does to the runtime, until the returned function
88
+ * is called: each tick's start and end (with the schedule values it runs)
89
+ * and each `runtime.send`. Unlike `observe`, it turns no tracing on, so a
90
+ * tool can listen for a whole play session (replay recorders do).
91
+ */
92
+ readonly observeHost: (listener: (event: HostEvent<S, Root>) => void) => () => void
93
+ /**
94
+ * Reports the last failed tick result if nobody read its `ok`, as a
95
+ * `failure.ignored` event (to `observe` and `observeHost` listeners), with
96
+ * `when: "session-end"`. Call it when a session ends; the start of the
97
+ * next tick checks the same with `when: "next-tick"`. Emits nothing to a
98
+ * console.
99
+ */
100
+ readonly checkIgnoredFailures: () => void
78
101
  /** Retention and reader state of every stream that has readers or entries. */
79
102
  readonly streams: () => ReadonlyArray<StreamStatus>
80
103
  /** Live entity count and, per component, how many entities have it. Cheap enough to sample every frame. */
@@ -153,7 +176,15 @@ export interface SystemDescription {
153
176
  readonly removed: ReadonlyArray<string>
154
177
  readonly despawned: boolean
155
178
  readonly relationFailures: ReadonlyArray<string>
179
+ /** Events and resources read with `Game.System.thisTick`, and query slots as `queries.<slot>`. */
180
+ readonly thisTick: ReadonlyArray<string>
156
181
  readonly services: ReadonlyArray<string>
182
+ /**
183
+ * The declared command permissions (`Game.System.commands(...)`), by
184
+ * kind: component names for spawns, inserts, removes and despawns, scope
185
+ * names for despawnsScope, relation names for relates.
186
+ */
187
+ readonly commands: CommandPermissions
157
188
  /** Run conditions, rendered, for example `stateChanged(Game/Flow)`. */
158
189
  readonly when: ReadonlyArray<string>
159
190
  /** Pause conditions, rendered: while one fails the system waits and holds its stream entries. */
@@ -162,6 +193,16 @@ export interface SystemDescription {
162
193
  readonly local: boolean
163
194
  }
164
195
 
196
+ /** Declared command permissions of one system, by kind. Empty lists when it declares none. */
197
+ export interface CommandPermissions {
198
+ readonly spawns: ReadonlyArray<string>
199
+ readonly inserts: ReadonlyArray<string>
200
+ readonly removes: ReadonlyArray<string>
201
+ readonly despawns: ReadonlyArray<string>
202
+ readonly despawnsScope: ReadonlyArray<string>
203
+ readonly relates: ReadonlyArray<string>
204
+ }
205
+
165
206
  export type StepDescription =
166
207
  | { readonly kind: "system"; readonly system: string }
167
208
  | { readonly kind: "applyDeferred" }
@@ -175,17 +216,28 @@ export interface ScheduleDescription {
175
216
  /**
176
217
  * A finding about the described schedules. `warning` is almost certainly a
177
218
  * bug; `info` is worth a look but can be intentional.
219
+ *
220
+ * Most lints read declarations only. Two also use what the runtime has seen
221
+ * so far: events sent with `runtime.send(...)` count as written, and
222
+ * `relation-failure-never-read` fires for a relation that applied commands
223
+ * have tried to relate or reorder (which can fail) while no described system
224
+ * reads its failures, so describe after the game has run for a while.
178
225
  */
179
226
  export interface Lint {
180
227
  readonly severity: "warning" | "info"
181
228
  readonly code:
182
229
  | "event-never-read"
230
+ | "relation-failure-never-read"
183
231
  | "event-never-written"
184
232
  | "next-state-never-applied"
185
233
  | "component-never-read"
186
234
  | "read-before-write"
235
+ | "this-tick-order"
236
+ | "command-permission-unused"
187
237
  readonly message: string
188
238
  readonly subject: string
239
+ /** What to change to resolve it (or, for `info`, when it is fine as it is). */
240
+ readonly fix: string
189
241
  }
190
242
 
191
243
  /** Readers and writers of one component, resource, or event. */
@@ -207,7 +259,8 @@ export interface Description {
207
259
  readonly systems: ReadonlyArray<SystemDescription>
208
260
  /**
209
261
  * Readers and writers across the named schedules. Component writers are
210
- * systems with a write query slot; command inserts are not static.
262
+ * systems with a write query slot or a declared `spawns`, `inserts`, or
263
+ * `removes` permission for it.
211
264
  */
212
265
  readonly access: {
213
266
  readonly components: ReadonlyArray<AccessIndexEntry>
@@ -450,6 +503,45 @@ export interface RestoreEvent {
450
503
  readonly ok: boolean
451
504
  }
452
505
 
506
+ /** What `observeHost` delivers. */
507
+ export type HostEvent<S extends Schema.Any = Schema.Any, Root = unknown> =
508
+ | {
509
+ readonly type: "tick.start"
510
+ /** The schedule values passed to this `tick(...)` (or `tryTick`) call, in order. */
511
+ readonly schedules: ReadonlyArray<ExecutableScheduleDefinition<S, any, Root, any, any, AnyScheduleOrder>>
512
+ readonly frame: number
513
+ }
514
+ | {
515
+ readonly type: "tick.end"
516
+ readonly schedules: ReadonlyArray<ExecutableScheduleDefinition<S, any, Root, any, any, AnyScheduleOrder>>
517
+ readonly frame: number
518
+ /** `failed`: a system returned `Result.failure`; `defect`: one threw (rethrown after this event). */
519
+ readonly outcome: "ok" | "failed" | "defect"
520
+ readonly error?: unknown
521
+ }
522
+ | { readonly type: "send"; readonly event: string; readonly value: unknown }
523
+ | FailureIgnoredEvent
524
+
525
+ /**
526
+ * A failed `tick` / `tryTick` result whose `ok` nobody read: the failure
527
+ * went unnoticed, and the systems after the failing one did not run that
528
+ * tick. Found at the start of the next tick (`when: "next-tick"`) or by
529
+ * `checkIgnoredFailures()` (`when: "session-end"`). Debug runtimes only.
530
+ */
531
+ export interface FailureIgnoredEvent {
532
+ readonly type: "failure.ignored"
533
+ /** The frame of the failed tick. */
534
+ readonly frame: number
535
+ /** The schedule holding the failing system (the first one for a tick that ran nothing). */
536
+ readonly schedule: string
537
+ /** The failing system; `undefined` when `tryTick` ran nothing (missing requirements, order). */
538
+ readonly system: string | undefined
539
+ readonly error: unknown
540
+ /** The systems of the tick that did not run because of the failure, in order. */
541
+ readonly skipped: ReadonlyArray<string>
542
+ readonly when: "next-tick" | "session-end"
543
+ }
544
+
453
545
  /** Everything `observe` delivers. */
454
546
  export type TraceEvent =
455
547
  | FrameEvent
@@ -461,14 +553,21 @@ export type TraceEvent =
461
553
  | DeferredEvent
462
554
  | TransitionEvent
463
555
  | RestoreEvent
556
+ | FailureIgnoredEvent
464
557
 
465
558
  /**
466
- * The `debug` runtime option: `true` attaches a handle.
559
+ * The `debug` runtime option: `{ hostClock }` attaches a handle, timing
560
+ * traces with that clock. A value that may be `undefined`
561
+ * (`import.meta.env.DEV ? { hostClock } : undefined`) attaches it when
562
+ * present and types `debug` as optional, so code checks it before use.
467
563
  */
468
- export type Option = true
564
+ export interface Option {
565
+ /** Times trace events (`ms`); see `HostClock`. */
566
+ readonly hostClock: HostClock
567
+ }
469
568
 
470
569
  /**
471
- * The member a runtime made with `debug: true` carries.
570
+ * The member a runtime made with `debug: { hostClock }` carries.
472
571
  */
473
572
  export interface Member<S extends Schema.Any, Root> {
474
573
  readonly debug: Handle<S, Root>
@@ -476,4 +575,6 @@ export interface Member<S extends Schema.Any, Root> {
476
575
 
477
576
  /** Resolves the extra runtime member for a `debug` option type. */
478
577
  export type MemberFor<Enabled, S extends Schema.Any, Root> =
479
- [Enabled] extends [Option] ? Member<S, Root> : {}
578
+ [Enabled] extends [true] ? Member<S, Root>
579
+ : [Enabled] extends [false | undefined] ? {}
580
+ : Partial<Member<S, Root>>