@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/System.ts CHANGED
@@ -57,7 +57,6 @@
57
57
  */
58
58
  import type { Descriptor } from "./Descriptor.ts"
59
59
  import type * as Entity from "./Entity.ts"
60
- import type { Fx } from "./Fx.ts"
61
60
  import * as Machine from "./Machine.ts"
62
61
  import type * as Relation from "./Relation.ts"
63
62
  import type { Query, QueryMatch } from "./Query.ts"
@@ -65,7 +64,7 @@ import type { ConstructedWriteCell, ReadCell, ReadonlyValue, WriteCell } from ".
65
64
  import * as Requirement from "./Requirement.ts"
66
65
  import type * as Result from "./Result.ts"
67
66
  import type { Schema } from "./Schema.ts"
68
- import type { CommandsApi } from "./Command.ts"
67
+ import type { CommandAccess, CommandsApi, NotDeclared } from "./Command.ts"
69
68
 
70
69
  /**
71
70
  * System declarations, typed execution context, and runtime requirements.
@@ -246,7 +245,7 @@ export const readEvent = <D extends Descriptor<"event", string, any>>(
246
245
  *
247
246
  * If the payload needs to name an entity for later work, emit a durable
248
247
  * `Game.Entity.handle(...)` (optionally with an intent component) and let the later
249
- * reader re-resolve it through `lookup.getHandle(...)`.
248
+ * reader re-resolve it through `queries.x.getHandle(...)` (through a declared query).
250
249
  *
251
250
  * @example
252
251
  * ```ts
@@ -266,6 +265,58 @@ export const writeEvent = <D extends Descriptor<"event", string, any>>(
266
265
  descriptor
267
266
  })
268
267
 
268
+ /** The marker `thisTick(...)` adds to a read. */
269
+ export interface ThisTick {
270
+ readonly thisTick: true
271
+ }
272
+
273
+ /**
274
+ * Requires that every system writing this event or resource runs earlier in
275
+ * the same `runtime.tick(...)` call than the reading system: the reader then
276
+ * sees this tick's data, never last tick's. The library never reorders
277
+ * anything; it only checks.
278
+ *
279
+ * `runtime.tick(...)` checks it at compile time over everything passed to that
280
+ * call, in order (gated groups count by position), and rejects a writer that
281
+ * runs after the reader and a call whose order is not statically known. A
282
+ * read with no writer in the call compiles (a reader reused in several
283
+ * ticks); the `event-never-written` lint reports data nobody writes.
284
+ * `tryTick` checks the same at runtime.
285
+ *
286
+ * Use it on reads whose correctness depends on running after their writers
287
+ * (damage applied the tick it is dealt). A plain read is right when last
288
+ * tick's data is acceptable.
289
+ *
290
+ * On a query (`queries: { enemies: Game.System.thisTick(Enemies) }`), the
291
+ * writers are systems whose declared commands can change what it matches:
292
+ * `spawns` covering every component it requires, or `inserts` / `removes` of
293
+ * any component it names (selection slots, optional ones included, `with`,
294
+ * `without`, filters). Each must run earlier with an `applyDeferred()` (or
295
+ * `applyStateTransitions(...)`) between it and the reader, since commands
296
+ * apply only there. `despawns` never count: a despawned entity stays visible
297
+ * to queries until the next `applyDeferred()`. Nested schedules are
298
+ * flattened, gated groups count by position, and a transition bundle counts
299
+ * at its `applyStateTransitions(...)` step (which applies queued commands
300
+ * first), as if every schedule in it runs, markers inside included.
301
+ *
302
+ * @example
303
+ * ```ts
304
+ * const ApplyDamage = Game.System("ApplyDamage", {
305
+ * events: { damage: Game.System.thisTick(Game.System.readEvent(Damage)) }
306
+ * }, ({ events }) => { ... })
307
+ *
308
+ * runtime.tick(Game.Schedule(DealDamage, ApplyDamage)) // compiles
309
+ * runtime.tick(Game.Schedule(ApplyDamage, DealDamage)) // "ApplyDamage needs Damage from this tick, but its writer DealDamage runs after it"
310
+ *
311
+ * const AimAtNewest = Game.System("AimAtNewest", { queries: { enemies: Game.System.thisTick(Enemies) } }, ...)
312
+ * runtime.tick(Game.Schedule(SpawnWave, Game.Schedule.applyDeferred(), AimAtNewest)) // compiles
313
+ * runtime.tick(Game.Schedule(SpawnWave, AimAtNewest)) // "AimAtNewest needs this tick's commands for queries.enemies, but SpawnWave queues them with no applyDeferred() in between"
314
+ * ```
315
+ */
316
+ export const thisTick = <
317
+ Read extends EventRead<Descriptor<"event", string, any>> | ResourceRead<Descriptor<"resource", string, any>> | Query.Any<any>
318
+ >(read: Read): Read & ThisTick => ({ ...read, thisTick: true })
319
+
269
320
  /**
270
321
  * Declares read access to the current committed value of a finite-state machine.
271
322
  *
@@ -307,7 +358,7 @@ export const transition = <M extends Machine.StateMachine.Any>(
307
358
  * normal events: each run sees those published since its previous run.
308
359
  *
309
360
  * This is one of the clearest signs that the modeled value should be a machine
310
- * rather than a plain state descriptor.
361
+ * rather than a resource or a `Descriptor.EntityState` component.
311
362
  */
312
363
  export const readTransitionEvent = <M extends Machine.StateMachine.Any>(
313
364
  stateMachine: M
@@ -430,8 +481,9 @@ export interface RelationFailureRead<R extends Relation.Relation.Any> {
430
481
  * ticked less often than the one that removes (rendering after several fixed
431
482
  * updates) still sees every removal. A system skipped by its run conditions
432
483
  * keeps its position, like `added`/`changed`, and sees the removals when it
433
- * runs again. Each log is capped at `Runtime.streamCapacity` entries; drops
434
- * show up as `missed` reads in the debug trace. Systems usually pair this
484
+ * runs again. Each log is capped at `Runtime.streamCapacity` entries; a
485
+ * reader that missed dropped ones sees `lagged() === true` (and a `missed`
486
+ * read in the debug trace). Systems usually pair this
435
487
  * with host cleanup such as removing renderer-owned nodes. {@link readDespawned}
436
488
  * complements this for whole-entity teardown.
437
489
  *
@@ -493,6 +545,14 @@ export const readDespawned = (): DespawnedRead => ({
493
545
  */
494
546
  export interface RemovedReadView<S extends Schema.Any, Root = unknown> {
495
547
  all(): ReadonlyArray<import("./Entity.ts").EntityId<S, Root>>
548
+ /**
549
+ * Whether records published since this system's previous run were dropped
550
+ * before it read them. Records wait for every reading system, so this only
551
+ * happens when a reader has not run for so long that the log exceeded its
552
+ * capacity (see `Runtime.streamCapacity`). Check it where a missed record
553
+ * would leak host state (a renderer node never destroyed).
554
+ */
555
+ lagged(): boolean
496
556
  }
497
557
 
498
558
  /**
@@ -500,6 +560,14 @@ export interface RemovedReadView<S extends Schema.Any, Root = unknown> {
500
560
  */
501
561
  export interface DespawnedReadView<S extends Schema.Any, Root = unknown> {
502
562
  all(): ReadonlyArray<import("./Entity.ts").EntityId<S, Root>>
563
+ /**
564
+ * Whether records published since this system's previous run were dropped
565
+ * before it read them. Records wait for every reading system, so this only
566
+ * happens when a reader has not run for so long that the log exceeded its
567
+ * capacity (see `Runtime.streamCapacity`). Check it where a missed record
568
+ * would leak host state (a renderer node never destroyed).
569
+ */
570
+ lagged(): boolean
503
571
  }
504
572
 
505
573
  /**
@@ -531,8 +599,46 @@ export interface QueryHandle<S extends Schema.Any, Q extends Query.Any> {
531
599
  each(): ReadonlyArray<QueryMatch<S, Q>>
532
600
  /**
533
601
  * Retrieves the match for one specific entity id when it satisfies the query.
602
+ *
603
+ * Every read and write of an entity goes through a query the system
604
+ * declares, so its access is visible in the spec: there is no lookup that
605
+ * takes a query at call time.
534
606
  */
535
607
  get(entityId: import("./Entity.ts").EntityId<S, Query.Root<Q>>): Result.Result<QueryMatch<S, Q>, Query.LookupError>
608
+ /**
609
+ * Resolves a stored durable handle back into this query's access.
610
+ *
611
+ * Use this for handles carried in events, resources, or components. A handle
612
+ * is storage-safe, not proof of liveness, so stale or mismatched handles stay
613
+ * explicit typed failures. A handle with an intent component needs a query
614
+ * that proves it (selects or requires it).
615
+ */
616
+ getHandle<H extends Entity.Handle<Query.Root<Q>, any>>(
617
+ handle: [Entity.Handle.Intent<H>] extends [undefined]
618
+ ? H
619
+ : Query.ProvesComponent<Q, Entity.Handle.Intent<H> & Descriptor<"component", string, any>> extends true
620
+ ? H
621
+ : "this query does not prove the handle's intent component: select it, or add it to with"
622
+ ): Result.Result<QueryMatch<S, Q>, Query.LookupError>
623
+ /**
624
+ * The direct children of one hierarchy parent that match this query, in the
625
+ * stored child order. Children that do not match are skipped; a missing
626
+ * parent is a `MissingEntity` failure.
627
+ */
628
+ children<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
629
+ entityId: Entity.EntityId<S, Query.Root<Q>>,
630
+ relation: R
631
+ ): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
632
+ /**
633
+ * The hierarchy descendants of one entity that match this query, in
634
+ * `options.order` (depth-first by default). Descendants that do not match
635
+ * are skipped. `lookup.descendants` returns every descendant's id instead.
636
+ */
637
+ descendants<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
638
+ entityId: Entity.EntityId<S, Query.Root<Q>>,
639
+ relation: R,
640
+ options?: { readonly order?: "breadth" | "depth" }
641
+ ): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
536
642
  /**
537
643
  * Returns a single match when exactly one entity satisfies the query.
538
644
  */
@@ -558,40 +664,19 @@ export interface QueryHandle<S extends Schema.Any, Q extends Query.Any> {
558
664
  }
559
665
 
560
666
  /**
561
- * Typed entity lookup API exposed to systems.
667
+ * Relation lookups exposed to systems: entity ids along relation edges.
562
668
  *
563
- * Use this when you already have an entity id and want a validated, typed view
564
- * over a specific component access specification.
669
+ * Lookups return ids only. To read or write the entities they lead to, pass
670
+ * the ids to a declared query (`queries.x.get(id)`), or use the query
671
+ * handle's `children` / `descendants`, so every access stays in the spec.
565
672
  *
566
673
  * All lookup methods are total and explicit:
567
674
  *
568
675
  * - no hidden exceptions
569
- * - stale handles are treated as normal typed failures
676
+ * - missing entities are normal typed failures
570
677
  * - hierarchy traversal is available only for hierarchy relations
571
678
  */
572
679
  export interface LookupApi<S extends Schema.Any, Root = unknown> {
573
- get<Q extends Query.Any<Root>>(
574
- entityId: Entity.EntityId<S, Root>,
575
- query: Q
576
- ): Result.Result<QueryMatch<S, Q>, Query.LookupError>
577
- /**
578
- * Resolves a stored durable handle back into current-world query access.
579
- *
580
- * Use this for handles carried in events, resources, or components. A handle is
581
- * storage-safe, not proof of liveness, so stale or mismatched handles remain
582
- * explicit typed failures.
583
- */
584
- getHandle<
585
- H extends Entity.Handle<Root, any>,
586
- Q extends Query.Any<Root>
587
- >(
588
- handle: [Entity.Handle.Intent<H>] extends [undefined]
589
- ? H
590
- : Query.ProvesComponent<Q, Entity.Handle.Intent<H> & Descriptor<"component", string, any>> extends true
591
- ? H
592
- : never,
593
- query: Q
594
- ): Result.Result<QueryMatch<S, Q>, Query.LookupError>
595
680
  related<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Any>>(
596
681
  entityId: Entity.EntityId<S, Root>,
597
682
  relation: R
@@ -600,21 +685,6 @@ export interface LookupApi<S extends Schema.Any, Root = unknown> {
600
685
  entityId: Entity.EntityId<S, Root>,
601
686
  relation: R
602
687
  ): Result.Result<ReadonlyArray<Entity.EntityId<S, Root>>, Relation.Relation.MissingEntityError>
603
- /**
604
- * Reads the direct children of one hierarchy parent as typed query matches.
605
- *
606
- * This preserves the stored child order for that hierarchy relation and skips
607
- * entities that do not satisfy the query. Missing parents remain explicit
608
- * `MissingEntity` failures.
609
- */
610
- childMatches<
611
- R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>,
612
- Q extends Query.Any<Root>
613
- >(
614
- entityId: Entity.EntityId<S, Root>,
615
- relation: R,
616
- query: Q
617
- ): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
618
688
  parent<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
619
689
  entityId: Entity.EntityId<S, Root>,
620
690
  relation: R
@@ -630,24 +700,6 @@ export interface LookupApi<S extends Schema.Any, Root = unknown> {
630
700
  readonly order?: "breadth" | "depth"
631
701
  }
632
702
  ): Result.Result<ReadonlyArray<Entity.EntityId<S, Root>>, Relation.Relation.MissingEntityError>
633
- /**
634
- * Traverses hierarchy descendants and resolves only the entities that match
635
- * the given query.
636
- *
637
- * Traversal order stays explicit through `options.order`, and non-matching
638
- * descendants are skipped without turning traversal into a failure.
639
- */
640
- descendantMatches<
641
- R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>,
642
- Q extends Query.Any<Root>
643
- >(
644
- entityId: Entity.EntityId<S, Root>,
645
- relation: R,
646
- query: Q,
647
- options?: {
648
- readonly order?: "breadth" | "depth"
649
- }
650
- ): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
651
703
  root<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
652
704
  entityId: Entity.EntityId<S, Root>,
653
705
  relation: R
@@ -686,6 +738,25 @@ export interface SystemAccessInput {
686
738
  readonly removed?: Record<string, RemovedRead<Descriptor<"component", string, any>>>
687
739
  readonly despawned?: Record<string, DespawnedRead>
688
740
  readonly relationFailures?: Record<string, RelationFailureRead<Relation.Relation.Any>>
741
+ /**
742
+ * The commands the system may queue: `Game.System.commands(...)` with
743
+ * `spawns`, `inserts`, `removes`, `despawns`, `despawnsScope`, and
744
+ * `relates` permissions. Without it the system has no `commands`.
745
+ */
746
+ readonly commands?: CommandAccess.Any
747
+ /**
748
+ * Run conditions that skip the system: while one fails, the system does not
749
+ * run and the events, transition events, and relation failures published
750
+ * meanwhile are dropped for it. Use it for a mode change after which those
751
+ * messages are stale (menu, game over); use `pauseWhen` instead for a freeze
752
+ * whose messages stay valid. `Schedule.when(...)` adds conditions to a whole
753
+ * group.
754
+ *
755
+ * Four gating forms, one rule: `when` drops the events published while
756
+ * gated (the system resumes on fresh messages), `pauseWhen` keeps them (the
757
+ * system resumes and reads the backlog). Put them on one system's spec, or
758
+ * on a group with `Schedule.when(...)` / `Schedule.pauseWhen(...)`.
759
+ */
689
760
  readonly when?: ReadonlyArray<Machine.Condition>
690
761
  /**
691
762
  * Run conditions that pause the system instead of skipping it: while one
@@ -763,6 +834,7 @@ export type SystemSpec<
763
834
  readonly pauseWhen: AccessField<Access, "pauseWhen", readonly []>
764
835
  readonly transitions: AccessField<Access, "transitions", {}>
765
836
  readonly local: LocalFactory<Access>
837
+ readonly commands: "commands" extends keyof Access ? NonNullable<Access["commands"]> : undefined
766
838
  readonly schema: S
767
839
  readonly __schemaRoot: Root
768
840
  }
@@ -857,6 +929,11 @@ type QueryContext<Spec extends AnySystemSpec> = {
857
929
  type LocalValue<Factory> = Factory extends () => infer Local ? Local : undefined
858
930
  type LocalContext<Spec extends AnySystemSpec> = LocalValue<Spec["local"]>
859
931
 
932
+ type CommandsContext<Spec extends AnySystemSpec> =
933
+ Spec["commands"] extends CommandAccess<infer Spawns, infer Inserts, infer Removes, infer Despawns, infer Scopes, infer Relates>
934
+ ? CommandsApi<Spec["schema"], Spec["__schemaRoot"], Spawns, Inserts, Removes, Despawns, Scopes, Relates>
935
+ : NotDeclared
936
+
860
937
  /**
861
938
  * The implementation context derived from a system spec.
862
939
  *
@@ -876,16 +953,12 @@ export interface SystemContext<Spec extends AnySystemSpec> {
876
953
  readonly relationFailures: RelationFailureContext<Spec>
877
954
  readonly transitions: TransitionContext<Spec>
878
955
  readonly services: ServiceContext<Spec>
879
- readonly commands: CommandsApi<Spec["schema"], Spec["__schemaRoot"]>
956
+ /** The commands the spec declares (`commands: Game.System.commands(...)`). */
957
+ readonly commands: CommandsContext<Spec>
880
958
  /** The system's own scratch state (see `local` in the spec); `undefined` when it declares none. */
881
959
  readonly local: LocalContext<Spec>
882
960
  }
883
961
 
884
- /**
885
- * The service environment required by a system.
886
- */
887
- export type SystemDependencies<Spec extends AnySystemSpec> = ServiceContext<Spec>
888
-
889
962
  type DescriptorFromAccess<Access> =
890
963
  Access extends { readonly descriptor: infer D extends Requirement.Requirement } ? D : never
891
964
 
@@ -949,7 +1022,7 @@ export interface SystemFailure<
949
1022
 
950
1023
  /** Extracts the normalized failure carried by one system definition. */
951
1024
  export type FailureOf<Value> =
952
- Value extends SystemDefinition<any, any, infer Error, any, infer Name, any>
1025
+ Value extends SystemDefinition<any, infer Error, any, infer Name, any>
953
1026
  ? [Error] extends [never] ? never : SystemFailure<Name, Error>
954
1027
  : never
955
1028
 
@@ -958,7 +1031,6 @@ export type FailureOf<Value> =
958
1031
  */
959
1032
  export interface SystemDefinition<
960
1033
  Spec extends AnySystemSpec,
961
- out A = void,
962
1034
  out E = never,
963
1035
  out Root = unknown,
964
1036
  out Name extends string = string,
@@ -991,32 +1063,46 @@ export interface SystemDefinition<
991
1063
  /**
992
1064
  * The executable implementation of the system.
993
1065
  */
994
- readonly run: SystemRun<Spec, A, E>
1066
+ readonly run: SystemRun<Spec, E>
995
1067
  /**
996
1068
  * The system this one is a gated copy of, made by `Schedule.when(...)`:
997
1069
  * the same system with extra run conditions. The runtime keys per-system
998
1070
  * state (change detection, event cursors) by the original, so a system and
999
1071
  * its gated copies are one reader.
1000
1072
  */
1001
- readonly base?: SystemDefinition<any, any, any, any, any, any> | undefined
1073
+ readonly base?: SystemDefinition<any, any, any, any, any> | undefined
1002
1074
  }
1003
1075
 
1004
1076
  /**
1005
1077
  * A system implementation.
1006
1078
  *
1007
- * Return nothing for a system that cannot fail. Return an `Fx` when the system
1008
- * has an expected failure (`Fx.fail(...)`) or wants to compose effects; the
1009
- * failure type becomes part of every schedule that contains the system.
1079
+ * Return nothing for a system that cannot fail. Return
1080
+ * `Result.failure(error)` for an expected failure: the system's ECS writes
1081
+ * are rolled back, its commands and events are dropped, and the tick stops
1082
+ * with a typed `SystemFailure` whose `error` is inferred from every
1083
+ * `Result.failure(...)` the body returns. A returned success
1084
+ * (`Result.success(...)`) behaves like returning nothing. Exceptions are
1085
+ * defects, not failures: they roll back the same way and are rethrown.
1086
+ *
1087
+ * @example
1088
+ * ```ts
1089
+ * const Spend = Game.System("Spend", {
1090
+ * resources: { gold: Game.System.writeResource(Gold) }
1091
+ * }, ({ resources }) => {
1092
+ * if (resources.gold.get() < 10) return Result.failure("NotEnoughGold" as const)
1093
+ * resources.gold.update((gold) => gold - 10)
1094
+ * })
1095
+ * ```
1010
1096
  */
1011
- export type SystemRun<Spec extends AnySystemSpec, A = void, E = never> =
1012
- (context: SystemContext<Spec>) => Fx<A, E, SystemDependencies<Spec>> | void
1097
+ export type SystemRun<Spec extends AnySystemSpec, E = never> =
1098
+ (context: SystemContext<Spec>) => Result.Result<unknown, E> | void
1013
1099
 
1014
1100
  /**
1015
1101
  * Defines a system from an explicit spec and a typed implementation.
1016
1102
  *
1017
1103
  * This is the public entrypoint for authoring systems. The implementation only
1018
- * receives the capabilities declared in the spec, and the returned effect keeps
1019
- * service dependencies tracked in the type system.
1104
+ * receives the capabilities declared in the spec; return
1105
+ * `Result.failure(error)` for an expected failure (see {@link SystemRun}).
1020
1106
  *
1021
1107
  * Use the string-name overload in normal code. The name is turned into an
1022
1108
  * internal ordering token automatically, so the system can participate in
@@ -1075,7 +1161,6 @@ const collectSystemRequirements = (
1075
1161
 
1076
1162
  export function System<
1077
1163
  const Access extends SystemAccessInput & { readonly schema: Schema.Any },
1078
- A = void,
1079
1164
  E = never,
1080
1165
  Root = unknown,
1081
1166
  const Name extends string = string
@@ -1084,14 +1169,14 @@ export function System<
1084
1169
  spec: Access & {
1085
1170
  readonly [Key in Exclude<keyof Access, keyof SystemAccessInput | "schema">]: never
1086
1171
  },
1087
- run: SystemRun<SystemSpec<Access["schema"], Access, Root>, A, E>
1088
- ): SystemDefinition<SystemSpec<Access["schema"], Access, Root>, A, E, Root, Name, SystemAccessNeeds<Access>>
1172
+ run: SystemRun<SystemSpec<Access["schema"], Access, Root>, E>
1173
+ ): SystemDefinition<SystemSpec<Access["schema"], Access, Root>, E, Root, Name, SystemAccessNeeds<Access>>
1089
1174
 
1090
1175
  export function System(
1091
1176
  name: string,
1092
1177
  spec: { readonly schema: Schema.Any } & SystemAccessInput,
1093
- run: (context: any) => Fx<any, any, any> | void
1094
- ): SystemDefinition<any, any, any, any, any, any> {
1178
+ run: (context: any) => Result.Result<any, any> | void
1179
+ ): SystemDefinition<any, any, any, any, any> {
1095
1180
  type Spec = SystemSpec<Schema.Any, SystemAccessInput, unknown>
1096
1181
 
1097
1182
  const normalizedSpec = {
@@ -1110,6 +1195,7 @@ export function System(
1110
1195
  pauseWhen: spec.pauseWhen ?? [],
1111
1196
  transitions: spec.transitions ?? {},
1112
1197
  local: spec.local,
1198
+ commands: spec.commands,
1113
1199
  __schemaRoot: undefined
1114
1200
  } as Spec
1115
1201
 
@@ -1123,5 +1209,5 @@ export function System(
1123
1209
  },
1124
1210
  spec: normalizedSpec,
1125
1211
  run
1126
- } as SystemDefinition<any, any, any, any, any, any>
1212
+ } as SystemDefinition<any, any, any, any, any>
1127
1213
  }
package/src/index.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * small number of stable namespaces instead of many deep internal paths.
6
6
  */
7
7
  /**
8
- * Branding helpers for validated values.
8
+ * Branding helpers for packages that author validated value types (like
9
+ * `@typeonce/ritmo-math`). Game code validates through descriptors instead.
9
10
  */
10
11
  export * as Brand from "./Brand.ts"
11
12
  /**
@@ -17,13 +18,13 @@ export * as Decode from "./Decode.ts"
17
18
  */
18
19
  export * as Debug from "./Debug.ts"
19
20
  /**
20
- * Descriptor authoring helpers.
21
+ * Simulation time: the fixed step, the clock resource, durations in whole steps.
21
22
  */
22
- export * as Descriptor from "./Descriptor.ts"
23
+ export * as Clock from "./Clock.ts"
23
24
  /**
24
- * Reusable validated authored definitions.
25
+ * Descriptor authoring helpers.
25
26
  */
26
- export * as Definition from "./Definition.ts"
27
+ export * as Descriptor from "./Descriptor.ts"
27
28
  /**
28
29
  * Entity identity and proof helpers.
29
30
  */
@@ -33,9 +34,9 @@ export * as Entity from "./Entity.ts"
33
34
  */
34
35
  export * as EntityScope from "./EntityScope.ts"
35
36
  /**
36
- * Minimal effect-style computation type.
37
+ * The host's wall clock as an injected contract (`standard`, `test()`).
37
38
  */
38
- export * as Fx from "./Fx.ts"
39
+ export * as HostClock from "./HostClock.ts"
39
40
  /**
40
41
  * Read-only typed world projections.
41
42
  */
@@ -18,7 +18,8 @@ abstract class WriteCellBase<T> {
18
18
  abstract get(): T
19
19
  abstract set(value: T): void
20
20
 
21
- setResult<E>(result: Result.Result<T, E>): Result.Result<void, E> {
21
+ /** Writes a validated candidate only on success. Backs `setRaw` / `updateRaw`. */
22
+ writeResult<E>(result: Result.Result<T, E>): Result.Result<void, E> {
22
23
  if (!result.ok) {
23
24
  return Result.failure(result.error)
24
25
  }
@@ -30,9 +31,6 @@ abstract class WriteCellBase<T> {
30
31
  this.set(f(this.get()))
31
32
  }
32
33
 
33
- updateResult<E>(f: (current: T) => Result.Result<T, E>): Result.Result<void, E> {
34
- return this.setResult(f(this.get()))
35
- }
36
34
  }
37
35
 
38
36
  /**
@@ -43,22 +41,22 @@ const withConstructor = <T, Raw, Error>(
43
41
  constructor: ResultConstructor<T, Raw, Error>
44
42
  ) => Object.assign(cell, {
45
43
  setRaw(raw: Raw): Result.Result<void, Error> {
46
- return cell.setResult(constructor.result(raw))
44
+ return cell.writeResult(constructor.result(raw))
47
45
  },
48
46
  updateRaw(f: (current: T) => Raw): Result.Result<void, Error> {
49
- return cell.setResult(constructor.result(f(cell.get())))
47
+ return cell.writeResult(constructor.result(f(cell.get())))
50
48
  }
51
49
  })
52
50
 
53
51
  /**
54
- * Adds `transition` to the write cell of a state component: a compare-and-set
52
+ * Adds `transition` to the write cell of an entity state component: a compare-and-set
55
53
  * that writes `to` only while the current state is still `from`.
56
54
  */
57
- const withTransition = (cell: WriteCellBase<unknown>, state: string) => Object.assign(cell, {
58
- transition(from: unknown, to: unknown): Result.Result<void, { readonly _tag: "StateMismatch"; readonly state: string; readonly expected: unknown; readonly actual: unknown }> {
55
+ const withTransition = (cell: WriteCellBase<unknown>, component: string) => Object.assign(cell, {
56
+ transition(from: unknown, to: unknown): Result.Result<void, { readonly _tag: "EntityStateMismatch"; readonly component: string; readonly expected: unknown; readonly actual: unknown }> {
59
57
  const actual = cell.get()
60
58
  if (actual !== from) {
61
- return Result.failure({ _tag: "StateMismatch", state, expected: from, actual })
59
+ return Result.failure({ _tag: "EntityStateMismatch", component, expected: from, actual })
62
60
  }
63
61
  cell.set(to)
64
62
  return Result.success(undefined)
@@ -162,11 +160,11 @@ export const componentWrite = (
162
160
  ordinal: number,
163
161
  world: World,
164
162
  constructor: ResultConstructor<unknown, unknown, unknown> | undefined,
165
- state: string | undefined
163
+ entityState: string | undefined
166
164
  ): WriteCellBase<unknown> => {
167
165
  const cell = new ComponentWriteCell(record, ordinal, world)
168
166
  const constructed = constructor === undefined ? cell : withConstructor(cell, constructor)
169
- return state === undefined ? constructed : withTransition(constructed, state)
167
+ return entityState === undefined ? constructed : withTransition(constructed, entityState)
170
168
  }
171
169
 
172
170
  export const componentOptional = (record: EntityRecord, ordinal: number): { readonly present: boolean; get(): unknown } =>