@typeonce/ritmo 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +3 -2
  2. package/dist/Clock.d.ts +19 -4
  3. package/dist/Clock.d.ts.map +1 -1
  4. package/dist/Clock.js +18 -2
  5. package/dist/Clock.js.map +1 -1
  6. package/dist/Command.d.ts +101 -45
  7. package/dist/Command.d.ts.map +1 -1
  8. package/dist/Command.js +22 -4
  9. package/dist/Command.js.map +1 -1
  10. package/dist/Debug.d.ts +66 -8
  11. package/dist/Debug.d.ts.map +1 -1
  12. package/dist/Descriptor.d.ts +27 -9
  13. package/dist/Descriptor.d.ts.map +1 -1
  14. package/dist/Descriptor.js +13 -9
  15. package/dist/Descriptor.js.map +1 -1
  16. package/dist/Entity.d.ts +15 -8
  17. package/dist/Entity.d.ts.map +1 -1
  18. package/dist/Entity.js.map +1 -1
  19. package/dist/FixedLoop.d.ts +106 -0
  20. package/dist/FixedLoop.d.ts.map +1 -0
  21. package/dist/FixedLoop.js +97 -0
  22. package/dist/FixedLoop.js.map +1 -0
  23. package/dist/HostClock.d.ts +1 -1
  24. package/dist/HostClock.d.ts.map +1 -1
  25. package/dist/HostClock.js +2 -1
  26. package/dist/HostClock.js.map +1 -1
  27. package/dist/Machine.d.ts +4 -1
  28. package/dist/Machine.d.ts.map +1 -1
  29. package/dist/Machine.js +4 -1
  30. package/dist/Machine.js.map +1 -1
  31. package/dist/Query.d.ts +52 -10
  32. package/dist/Query.d.ts.map +1 -1
  33. package/dist/Query.js +2 -1
  34. package/dist/Query.js.map +1 -1
  35. package/dist/Relation.d.ts +5 -14
  36. package/dist/Relation.d.ts.map +1 -1
  37. package/dist/Relation.js +0 -32
  38. package/dist/Relation.js.map +1 -1
  39. package/dist/Runtime.d.ts +58 -15
  40. package/dist/Runtime.d.ts.map +1 -1
  41. package/dist/Runtime.js +288 -70
  42. package/dist/Runtime.js.map +1 -1
  43. package/dist/Schedule.d.ts +24 -15
  44. package/dist/Schedule.d.ts.map +1 -1
  45. package/dist/Schedule.js +2 -1
  46. package/dist/Schedule.js.map +1 -1
  47. package/dist/Schema.d.ts +98 -29
  48. package/dist/Schema.d.ts.map +1 -1
  49. package/dist/Schema.js +4 -4
  50. package/dist/Schema.js.map +1 -1
  51. package/dist/Snapshot.d.ts +76 -1
  52. package/dist/Snapshot.d.ts.map +1 -1
  53. package/dist/Snapshot.js +42 -1
  54. package/dist/Snapshot.js.map +1 -1
  55. package/dist/System.d.ts +68 -24
  56. package/dist/System.d.ts.map +1 -1
  57. package/dist/System.js.map +1 -1
  58. package/dist/index.d.ts +4 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +4 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/internal/cells.d.ts +2 -0
  63. package/dist/internal/cells.d.ts.map +1 -1
  64. package/dist/internal/cells.js +34 -8
  65. package/dist/internal/cells.js.map +1 -1
  66. package/dist/internal/debug.d.ts +3 -0
  67. package/dist/internal/debug.d.ts.map +1 -1
  68. package/dist/internal/debug.js +83 -15
  69. package/dist/internal/debug.js.map +1 -1
  70. package/dist/internal/fragments.d.ts +2 -6
  71. package/dist/internal/fragments.d.ts.map +1 -1
  72. package/dist/internal/fragments.js +25 -2
  73. package/dist/internal/fragments.js.map +1 -1
  74. package/dist/internal/game.d.ts +2 -0
  75. package/dist/internal/game.d.ts.map +1 -1
  76. package/dist/internal/game.js +16 -3
  77. package/dist/internal/game.js.map +1 -1
  78. package/dist/internal/queries.d.ts.map +1 -1
  79. package/dist/internal/queries.js +27 -18
  80. package/dist/internal/queries.js.map +1 -1
  81. package/dist/internal/relations.d.ts +10 -0
  82. package/dist/internal/relations.d.ts.map +1 -0
  83. package/dist/internal/relations.js +32 -0
  84. package/dist/internal/relations.js.map +1 -0
  85. package/dist/internal/streams.d.ts +2 -0
  86. package/dist/internal/streams.d.ts.map +1 -1
  87. package/dist/internal/streams.js +10 -0
  88. package/dist/internal/streams.js.map +1 -1
  89. package/dist/internal/world.d.ts +7 -1
  90. package/dist/internal/world.d.ts.map +1 -1
  91. package/dist/internal/world.js +53 -3
  92. package/dist/internal/world.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/Clock.ts +29 -4
  95. package/src/Command.ts +161 -67
  96. package/src/Debug.ts +61 -9
  97. package/src/Descriptor.ts +43 -17
  98. package/src/Entity.ts +16 -9
  99. package/src/FixedLoop.ts +218 -0
  100. package/src/HostClock.ts +2 -1
  101. package/src/Machine.ts +4 -1
  102. package/src/Query.ts +47 -10
  103. package/src/Relation.ts +5 -67
  104. package/src/Runtime.ts +345 -85
  105. package/src/Schedule.ts +31 -19
  106. package/src/Schema.ts +118 -35
  107. package/src/Snapshot.ts +90 -2
  108. package/src/System.ts +76 -23
  109. package/src/index.ts +4 -0
  110. package/src/internal/cells.ts +42 -10
  111. package/src/internal/debug.ts +84 -15
  112. package/src/internal/fragments.ts +23 -3
  113. package/src/internal/game.ts +19 -3
  114. package/src/internal/queries.ts +28 -2
  115. package/src/internal/relations.ts +52 -0
  116. package/src/internal/streams.ts +11 -0
  117. package/src/internal/world.ts +60 -4
package/src/Descriptor.ts CHANGED
@@ -280,6 +280,22 @@ const makeConstructedDescriptor = <
280
280
  [descriptorConstruction]: constructor
281
281
  }) as ConstructedDescriptor<Kind, Name, Value, Raw, Error>
282
282
 
283
+ /**
284
+ * The message naming why `Value` cannot be the value of a descriptor of
285
+ * `Kind`, or `never` when it can. `any` turns off every check on the
286
+ * descriptor's reads and writes, and `never` erases the fragment it is listed in.
287
+ */
288
+ type Article<Kind extends string> = Kind extends "event" ? "an" : "a"
289
+
290
+ type ValueRejection<Kind extends string, Value> = 0 extends 1 & Value
291
+ ? `${Article<Kind>} ${Kind} value cannot be \`any\`: give it a type (use \`unknown\` for a value you never read)`
292
+ : [Value] extends [never] ? `${Article<Kind>} ${Kind} value cannot be \`never\`: give it a type (use \`{}\` for a marker)`
293
+ : never
294
+
295
+ /** `Make`, or, when `Value` is rejected, a function whose only parameter is the message. */
296
+ type CheckedValue<Kind extends string, Value, Make> = [ValueRejection<Kind, Value>] extends [never] ? Make
297
+ : (name: ValueRejection<Kind, Value>) => never
298
+
283
299
  /**
284
300
  * Defines a component descriptor.
285
301
  *
@@ -289,6 +305,10 @@ const makeConstructedDescriptor = <
289
305
  * Components are the only descriptor kind that can be queried directly with
290
306
  * `Game.Query.read(...)`, `write(...)`, or `optional(...)`.
291
307
  *
308
+ * The value is a real type: `any` and `never` do not compile (here and in
309
+ * every other descriptor constructor). Use `unknown` for a value no system
310
+ * reads, and `Descriptor.Tag` for a marker.
311
+ *
292
312
  * @example
293
313
  * ```ts
294
314
  * const Position = Descriptor.Component<{ x: number; y: number }>()("Position")
@@ -296,11 +316,11 @@ const makeConstructedDescriptor = <
296
316
  * const EnemyKind = Descriptor.Component<"grunt" | "boss">()("EnemyKind", { immutable: true })
297
317
  * ```
298
318
  */
299
- export const Component = <Value>() => <const Name extends string, const Options extends ComponentOptions = {}>(
319
+ export const Component = <Value>(): CheckedValue<"component", Value, <const Name extends string, const Options extends ComponentOptions = {}>(
300
320
  name: Name,
301
321
  options?: Options
302
- ): WithComponentOptions<Descriptor<"component", Name, Value>, Options> =>
303
- applyComponentOptions(makeDescriptor("component", name), options) as WithComponentOptions<Descriptor<"component", Name, Value>, Options>
322
+ ) => WithComponentOptions<Descriptor<"component", Name, Value>, Options>> =>
323
+ ((name: string, options?: ComponentOptions) => applyComponentOptions(makeDescriptor("component", name), options)) as never
304
324
 
305
325
  /**
306
326
  * Defines a component descriptor that also knows how to validate raw values.
@@ -334,11 +354,11 @@ export const ConstructedComponent = <Value, Raw, Error, Constructor extends {} =
334
354
  * const SpriteRef = Descriptor.TransientComponent<{ frame: number }>()("SpriteRef")
335
355
  * ```
336
356
  */
337
- export const TransientComponent = <Value>() => <const Name extends string, const Options extends ComponentOptions = {}>(
357
+ export const TransientComponent = <Value>(): CheckedValue<"component", Value, <const Name extends string, const Options extends ComponentOptions = {}>(
338
358
  name: Name,
339
359
  options?: Options
340
- ): WithComponentOptions<TransientDescriptor<"component", Name, Value>, Options> =>
341
- applyComponentOptions(makeTransientDescriptor("component", name), options) as WithComponentOptions<TransientDescriptor<"component", Name, Value>, Options>
360
+ ) => WithComponentOptions<TransientDescriptor<"component", Name, Value>, Options>> =>
361
+ ((name: string, options?: ComponentOptions) => applyComponentOptions(makeTransientDescriptor("component", name), options)) as never
342
362
 
343
363
  /**
344
364
  * Defines a marker component: no data, only presence (`[Player, {}]`).
@@ -446,9 +466,9 @@ export const isEntityState = (
446
466
  * const Score = Descriptor.Resource<number>()("Score")
447
467
  * ```
448
468
  */
449
- export const Resource = <Value>() => <const Name extends string>(
469
+ export const Resource = <Value>(): CheckedValue<"resource", Value, <const Name extends string>(
450
470
  name: Name
451
- ): Descriptor<"resource", Name, Value> => makeDescriptor("resource", name)
471
+ ) => Descriptor<"resource", Name, Value>> => ((name: string) => makeDescriptor("resource", name)) as never
452
472
 
453
473
  /**
454
474
  * Defines a resource descriptor that also knows how to validate raw values.
@@ -469,9 +489,9 @@ export const ConstructedResource = <Value, Raw, Error, Constructor extends {} =
469
489
  * const DeltaTime = Descriptor.TransientResource<number>()("DeltaTime")
470
490
  * ```
471
491
  */
472
- export const TransientResource = <Value>() => <const Name extends string>(
492
+ export const TransientResource = <Value>(): CheckedValue<"resource", Value, <const Name extends string>(
473
493
  name: Name
474
- ): TransientDescriptor<"resource", Name, Value> => makeTransientDescriptor("resource", name)
494
+ ) => TransientDescriptor<"resource", Name, Value>> => ((name: string) => makeTransientDescriptor("resource", name)) as never
475
495
 
476
496
  /**
477
497
  * Defines an event descriptor.
@@ -491,9 +511,9 @@ export const TransientResource = <Value>() => <const Name extends string>(
491
511
  * const Hit = Descriptor.Event<{ target: number; amount: number }>()("Hit")
492
512
  * ```
493
513
  */
494
- export const Event = <Value>() => <const Name extends string>(
514
+ export const Event = <Value>(): CheckedValue<"event", Value, <const Name extends string>(
495
515
  name: Name
496
- ): Descriptor<"event", Name, Value> => makeDescriptor("event", name)
516
+ ) => Descriptor<"event", Name, Value>> => ((name: string) => makeDescriptor("event", name)) as never
497
517
 
498
518
  /**
499
519
  * Defines a service descriptor.
@@ -512,9 +532,9 @@ export const Event = <Value>() => <const Name extends string>(
512
532
  * const Logger = Descriptor.Service<{ log: (message: string) => void }>()("Logger")
513
533
  * ```
514
534
  */
515
- export const Service = <Value>() => <const Name extends string>(
535
+ export const Service = <Value>(): CheckedValue<"service", Value, <const Name extends string>(
516
536
  name: Name
517
- ): Descriptor<"service", Name, Value> => makeDescriptor("service", name)
537
+ ) => Descriptor<"service", Name, Value>> => ((name: string) => makeDescriptor("service", name)) as never
518
538
 
519
539
  /**
520
540
  * Checks whether one descriptor is transient (skipped by snapshots).
@@ -625,7 +645,10 @@ export function constructorOf<D extends Descriptor.Any>(
625
645
  * `Game.EntityScope(...)` instead when entities only share a lifetime (a
626
646
  * level, a menu) and need no tree, order, or traversal.
627
647
  */
628
- export const Hierarchy = RelationModule.Hierarchy
648
+ export const Hierarchy = <const Name extends string, const RelatedName extends string>(
649
+ name: Name,
650
+ relatedName: RelatedName
651
+ ) => makePair("hierarchy", name, relatedName)
629
652
 
630
653
  /**
631
654
  * Defines a general relationship pair with direct edges and reverse lookups.
@@ -634,7 +657,10 @@ export const Hierarchy = RelationModule.Hierarchy
634
657
  * reverse lookup, but not hierarchy-only behavior such as tree traversal or
635
658
  * child reordering.
636
659
  */
637
- export const Relation = RelationModule.Relation
660
+ export const Relation = <const Name extends string, const RelatedName extends string>(
661
+ name: Name,
662
+ relatedName: RelatedName
663
+ ) => makePair("relation", name, relatedName)
638
664
  import * as DecodeModule from "./Decode.ts"
639
- import * as RelationModule from "./Relation.ts"
665
+ import { makePair } from "./internal/relations.ts"
640
666
  import * as ResultModule from "./Result.ts"
package/src/Entity.ts CHANGED
@@ -131,7 +131,7 @@ export namespace Handle {
131
131
  * Drafts exist before the command queue is flushed. This is the place where the
132
132
  * API can safely carry exact structural information.
133
133
  */
134
- export interface EntityDraft<S extends Schema.Any, out P extends ComponentProof, Root = unknown> {
134
+ export interface EntityDraft<S extends Schema.Any, out P extends ComponentProof, Root = unknown, out Relates = never> {
135
135
  /**
136
136
  * Runtime tag for debugging and pattern matching.
137
137
  */
@@ -156,6 +156,11 @@ export interface EntityDraft<S extends Schema.Any, out P extends ComponentProof,
156
156
  * Staged relationship edges attached to the entity before spawn.
157
157
  */
158
158
  readonly relations: ReadonlyArray<StagedRelation<S, Root>>
159
+ /**
160
+ * The relations `Game.Command.relate(...)` staged on this draft: spawning
161
+ * it needs them in the system's `Game.System.relates(...)`. Type-level only.
162
+ */
163
+ readonly __relates?: (_: never) => Relates
159
164
  }
160
165
 
161
166
  /**
@@ -168,7 +173,7 @@ export type StagedComponent = readonly [Descriptor<"component", string, any>, un
168
173
  *
169
174
  * Query execution is the main source of `EntityRef` values.
170
175
  */
171
- export interface EntityRef<S extends Schema.Any, out P extends ComponentProof, Root = unknown, out Proves = unknown, out Lacks = never> {
176
+ export interface EntityRef<S extends Schema.Any, out P extends ComponentProof, Root = unknown, in Proves = never, in Lacks = never> {
172
177
  /**
173
178
  * Runtime tag for debugging and pattern matching.
174
179
  */
@@ -176,15 +181,17 @@ export interface EntityRef<S extends Schema.Any, out P extends ComponentProof, R
176
181
  /**
177
182
  * The component descriptors the query that produced this ref proves (its
178
183
  * required slots and `with`): what `commands.despawn`, `insert`, and
179
- * `remove` check. Type-level only.
184
+ * `remove` check. Type-level only. Contravariant on purpose: a ref proving
185
+ * more is a subtype, so a union of refs (`cond ? a.entity : b.entity`)
186
+ * keeps only what both prove.
180
187
  */
181
- readonly __proves?: (_: never) => Proves
188
+ readonly __proves?: (_: Proves) => void
182
189
  /**
183
190
  * The component descriptors the producing query proves absent (its
184
191
  * `without`): what `commands.remove` checks against `Schema.requires`.
185
192
  * Type-level only.
186
193
  */
187
- readonly __lacks?: (_: never) => Lacks
194
+ readonly __lacks?: (_: Lacks) => void
188
195
  /**
189
196
  * The schema-bound entity identity.
190
197
  */
@@ -207,17 +214,17 @@ export interface EntityMut<
207
214
  out P extends ComponentProof,
208
215
  out W extends ComponentProof,
209
216
  Root = unknown,
210
- out Proves = unknown,
211
- out Lacks = never
217
+ in Proves = never,
218
+ in Lacks = never
212
219
  > {
213
220
  /**
214
221
  * Runtime tag for debugging and pattern matching.
215
222
  */
216
223
  readonly kind: "EntityMut"
217
224
  /** The component descriptors the producing query proves; see `EntityRef`. */
218
- readonly __proves?: (_: never) => Proves
225
+ readonly __proves?: (_: Proves) => void
219
226
  /** The component descriptors the producing query proves absent; see `EntityRef`. */
220
- readonly __lacks?: (_: never) => Lacks
227
+ readonly __lacks?: (_: Lacks) => void
221
228
  /**
222
229
  * The schema-bound entity identity.
223
230
  */
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Deterministic fixed-step timing for any host: browsers, native shells,
3
+ * desktop runtimes, and headless tests.
4
+ *
5
+ * The loop owns the timing policy that is easy to get subtly wrong in every
6
+ * game: frame clamping, catch-up limits, interpolation, cancellation, and
7
+ * typed update failures. It does not know about an ECS runtime, a renderer,
8
+ * or a platform: frames arrive through a `TickSource` that the host supplies.
9
+ * Use a platform adapter such as `BrowserTickSource.make(window)` from
10
+ * `@typeonce/ritmo-browser`, or wrap a renderer's own ticker with `source`.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const loop = FixedLoop.start({
15
+ * source: BrowserTickSource.make(window),
16
+ * step: Clock.hz(60),
17
+ * maxFrameSeconds: 0.25,
18
+ * maxStepsPerFrame: 5,
19
+ * update: () => runtime.tick(update),
20
+ * render: (frame) => draw(frame.alpha),
21
+ * onFailure: (failure) => console.error(failure)
22
+ * })
23
+ * ```
24
+ *
25
+ * @module FixedLoop
26
+ * @docGroup runtime
27
+ */
28
+ import type * as Clock from "./Clock.ts"
29
+ import * as Result from "./Result.ts"
30
+
31
+ /** One host frame: milliseconds elapsed since the previous one. */
32
+ export interface Tick {
33
+ readonly deltaMS: number
34
+ }
35
+
36
+ /** A stoppable host subscription. Calling `stop` more than once is safe. */
37
+ export interface Subscription {
38
+ stop(): void
39
+ }
40
+
41
+ /**
42
+ * Where host frames come from. Implemented by platform adapters
43
+ * (`BrowserTickSource.make(window)`), by `source` around a renderer's ticker,
44
+ * and by deterministic tests.
45
+ */
46
+ export interface TickSource<TTick extends Tick = Tick> {
47
+ subscribe(onTick: (tick: TTick) => void): Subscription
48
+ }
49
+
50
+ /** Validated fixed-step timing policy. */
51
+ export interface Options {
52
+ /**
53
+ * The fixed step every update advances (`Clock.hz(60)`): the same value
54
+ * the game's clock resource runs at, so the loop and the clock agree.
55
+ */
56
+ readonly step: Clock.Step
57
+ /** Maximum host-frame duration admitted into the accumulator. */
58
+ readonly maxFrameSeconds: number
59
+ /** Maximum number of simulation updates performed for one host frame. */
60
+ readonly maxStepsPerFrame: number
61
+ }
62
+
63
+ /** Configuration error returned before a loop subscribes to its host. */
64
+ export interface InvalidOptions {
65
+ readonly kind: "InvalidFixedLoopOptions"
66
+ readonly field: keyof Options
67
+ readonly reason: "mustBeFiniteAndPositive" | "mustBePositiveInteger"
68
+ readonly value: number
69
+ }
70
+
71
+ /** Data passed to the renderer after all fixed updates for one host frame. */
72
+ export interface Frame {
73
+ /** Clamped time admitted for this host frame. */
74
+ readonly elapsedSeconds: number
75
+ /** Number of fixed updates completed for this host frame. */
76
+ readonly fixedSteps: number
77
+ /** Remaining accumulator fraction in the range `[0, 1)`. */
78
+ readonly alpha: number
79
+ /** Time discarded by frame clamping or the catch-up limit. */
80
+ readonly droppedSeconds: number
81
+ }
82
+
83
+ /** Failure delivered by an active loop before it stops itself. */
84
+ export type Failure<UpdateError> =
85
+ | {
86
+ readonly kind: "InvalidTickDelta"
87
+ readonly deltaMS: number
88
+ }
89
+ | {
90
+ readonly kind: "UpdateFailure"
91
+ readonly error: UpdateError
92
+ }
93
+
94
+ /** A running fixed-step driver. */
95
+ export interface Driver extends Subscription {
96
+ /** Clears accumulated partial time without removing the host subscription. */
97
+ reset(): void
98
+ }
99
+
100
+ /** Input for a fixed-step driver. */
101
+ export interface StartOptions<TTick extends Tick, UpdateError> extends Options {
102
+ readonly source: TickSource<TTick>
103
+ readonly update: (stepSeconds: number) => Result.Result<void, UpdateError>
104
+ readonly render: (frame: Frame) => void
105
+ readonly onFailure: (failure: Failure<UpdateError>) => void
106
+ }
107
+
108
+ /** Wraps a host-specific subscription function in the canonical source type. */
109
+ export const source = <TTick extends Tick>(
110
+ subscribe: TickSource<TTick>["subscribe"]
111
+ ): TickSource<TTick> => ({ subscribe })
112
+
113
+ const validate = (options: Options): Result.Result<void, InvalidOptions> => {
114
+ for (const field of ["maxFrameSeconds"] as const) {
115
+ const value = options[field]
116
+ if (!Number.isFinite(value) || value <= 0) {
117
+ return Result.failure({
118
+ kind: "InvalidFixedLoopOptions",
119
+ field,
120
+ reason: "mustBeFiniteAndPositive",
121
+ value
122
+ })
123
+ }
124
+ }
125
+ if (!Number.isInteger(options.maxStepsPerFrame) || options.maxStepsPerFrame <= 0) {
126
+ return Result.failure({
127
+ kind: "InvalidFixedLoopOptions",
128
+ field: "maxStepsPerFrame",
129
+ reason: "mustBePositiveInteger",
130
+ value: options.maxStepsPerFrame
131
+ })
132
+ }
133
+ return Result.success(undefined)
134
+ }
135
+
136
+ /**
137
+ * Starts a fixed-step loop after validating its timing policy.
138
+ *
139
+ * Update failures and malformed host ticks are delivered to `onFailure`, then
140
+ * the driver stops. Render defects are deliberately not caught: they are
141
+ * programmer defects, not expected game outcomes.
142
+ */
143
+ export const start = <TTick extends Tick, UpdateError>(
144
+ options: StartOptions<TTick, UpdateError>
145
+ ): Result.Result<Driver, InvalidOptions> => {
146
+ const validity = validate(options)
147
+ if (!validity.ok) {
148
+ return validity
149
+ }
150
+
151
+ let accumulator = 0
152
+ let stopped = false
153
+ let hostSubscription: Subscription | undefined
154
+
155
+ const stop = (): void => {
156
+ if (stopped) {
157
+ return
158
+ }
159
+ stopped = true
160
+ hostSubscription?.stop()
161
+ }
162
+
163
+ const fail = (failure: Failure<UpdateError>): void => {
164
+ stop()
165
+ options.onFailure(failure)
166
+ }
167
+
168
+ const onTick = (tick: TTick): void => {
169
+ if (stopped) {
170
+ return
171
+ }
172
+ if (!Number.isFinite(tick.deltaMS) || tick.deltaMS < 0) {
173
+ fail({ kind: "InvalidTickDelta", deltaMS: tick.deltaMS })
174
+ return
175
+ }
176
+
177
+ const rawSeconds = tick.deltaMS / 1000
178
+ const elapsedSeconds = Math.min(rawSeconds, options.maxFrameSeconds)
179
+ let droppedSeconds = rawSeconds - elapsedSeconds
180
+ accumulator += elapsedSeconds
181
+
182
+ let fixedSteps = 0
183
+ while (accumulator >= options.step.dt && fixedSteps < options.maxStepsPerFrame) {
184
+ const update = options.update(options.step.dt)
185
+ if (!update.ok) {
186
+ fail({ kind: "UpdateFailure", error: update.error })
187
+ return
188
+ }
189
+ accumulator -= options.step.dt
190
+ fixedSteps += 1
191
+ }
192
+
193
+ if (accumulator >= options.step.dt) {
194
+ const droppedWholeSteps = Math.floor(accumulator / options.step.dt) * options.step.dt
195
+ accumulator -= droppedWholeSteps
196
+ droppedSeconds += droppedWholeSteps
197
+ }
198
+
199
+ options.render({
200
+ elapsedSeconds,
201
+ fixedSteps,
202
+ alpha: accumulator / options.step.dt,
203
+ droppedSeconds
204
+ })
205
+ }
206
+
207
+ hostSubscription = options.source.subscribe(onTick)
208
+ if (stopped) {
209
+ hostSubscription.stop()
210
+ }
211
+
212
+ return Result.success({
213
+ stop,
214
+ reset() {
215
+ accumulator = 0
216
+ }
217
+ })
218
+ }
package/src/HostClock.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * @example
15
15
  * ```ts
16
- * const runtime = Game.Runtime.make({ services, debug: { hostClock: HostClock.standard } })
16
+ * const runtime = Game.Runtime.make({ services, debug: { hostClock: HostClock.standard } }).value
17
17
  * ```
18
18
  *
19
19
  * @module HostClock
@@ -33,6 +33,7 @@ export interface HostClock {
33
33
  * 0. Prefer a monotonic platform clock when there is one
34
34
  * (`BrowserHostClock.make(window)`).
35
35
  */
36
+ // oxlint-disable-next-line ritmo/use-clock-for-time -- HostClock is host time, for hosts and tools; systems read Clock.
36
37
  export const standard: HostClock = { now: () => Date.now() }
37
38
 
38
39
  /** `adjust` must leave a finite clock reading and never move backwards. */
package/src/Machine.ts CHANGED
@@ -418,7 +418,10 @@ export const inState = <M extends StateMachine.Any>(
418
418
  })
419
419
 
420
420
  /**
421
- * Creates a condition that passes when the machine changed during the current schedule execution.
421
+ * Creates a condition that passes when the machine changed during the current schedule execution:
422
+ * from the `applyStateTransitions(...)` that committed the change to the end of
423
+ * that schedule (or the next `applyStateTransitions(...)`), not for the rest of
424
+ * the tick call or the next schedule.
422
425
  */
423
426
  export const stateChanged = <M extends StateMachine.Any>(
424
427
  machine: M
package/src/Query.ts CHANGED
@@ -298,7 +298,8 @@ export const added = <D extends ComponentDescriptor>(descriptor: D): AddedFilter
298
298
  *
299
299
  * Any write through a write cell counts, even when the value is equal to the
300
300
  * previous one. Writes from a system whose run failed are rolled back and do
301
- * not count.
301
+ * not count. A system never sees its own writes as changes on its next run
302
+ * (it does see the events it emitted).
302
303
  *
303
304
  * @example
304
305
  * ```ts
@@ -427,6 +428,8 @@ export namespace Query {
427
428
  */
428
429
  export type Root<T extends Any> = T extends QuerySpec<any, any, any, any, any, any, any, any, infer R> ? R : never
429
430
 
431
+ type FilteredDescriptors<F> = F extends { readonly kind: "added" | "changed"; readonly descriptor: infer D } ? D : never
432
+
430
433
  type RequiredSelectionDescriptors<T extends Any> = {
431
434
  readonly [K in keyof T["selection"]]:
432
435
  T["selection"][K] extends ReadAccess<infer D> | WriteAccess<infer D> ? D : never
@@ -441,12 +444,16 @@ export namespace Query {
441
444
  export type ProvesComponent<
442
445
  T extends Any,
443
446
  D extends ComponentDescriptor
444
- > = [Extract<RequiredSelectionDescriptors<T> | T["with"][number], D>] extends [never]
447
+ > = [Extract<Proven<T>, D>] extends [never]
445
448
  ? false
446
449
  : true
447
450
 
448
- /** The component descriptors a query proves its matches have: required selection slots and `with`. */
449
- export type Proven<T extends Any> = RequiredSelectionDescriptors<T> | T["with"][number]
451
+ /**
452
+ * The component descriptors a query proves its matches have: required
453
+ * selection slots, `with`, and the component of each `added` / `changed`
454
+ * filter (a match has it). A `Query.anyOf` member proves nothing alone.
455
+ */
456
+ export type Proven<T extends Any> = RequiredSelectionDescriptors<T> | T["with"][number] | FilteredDescriptors<T["filters"][number]>
450
457
 
451
458
  /** The component descriptors a query proves its matches lack: its `without`. */
452
459
  export type Excluded<T extends Any> = T["without"][number]
@@ -562,13 +569,14 @@ export interface ReadCell<T> {
562
569
  */
563
570
  export interface WriteCell<T> extends ReadCell<T> {
564
571
  /**
565
- * Replaces the current value.
572
+ * Replaces the current value. Takes the readonly shape reads return, so a
573
+ * value read from any cell can be written back as it is.
566
574
  */
567
- set(value: T): void
575
+ set(value: ReadonlyValue<T>): void
568
576
  /**
569
577
  * Updates the current value based on the previous one.
570
578
  */
571
- update(f: (current: ReadonlyValue<T>) => T): void
579
+ update(f: (current: ReadonlyValue<T>) => ReadonlyValue<T>): void
572
580
  }
573
581
 
574
582
  /**
@@ -626,7 +634,10 @@ export interface EntityStateMismatchError<T> {
626
634
  */
627
635
  export interface EntityStateWriteCell<T, Transitions> extends ConstructedWriteCell<T, T, DecodeError> {
628
636
  /**
629
- * Moves from `from` to `to` if the component is still in `from`.
637
+ * Moves from `from` to `to` if the component is still in `from`. Use it for
638
+ * every move the transition table describes; `set` writes any state
639
+ * without checking the table, for resets that must not depend on the
640
+ * current state.
630
641
  *
631
642
  * @example
632
643
  * ```ts
@@ -660,6 +671,8 @@ export type WriteCellForDescriptor<D extends ComponentDescriptor> =
660
671
  */
661
672
  export interface PresentOptionalReadCell<T> extends ReadCell<T> {
662
673
  readonly present: true
674
+ /** The value, which is present. */
675
+ value(): T
663
676
  }
664
677
 
665
678
  /**
@@ -667,12 +680,17 @@ export interface PresentOptionalReadCell<T> extends ReadCell<T> {
667
680
  */
668
681
  export interface AbsentOptionalReadCell {
669
682
  readonly present: false
683
+ /** `undefined`: the entity does not hold the component. */
684
+ value(): undefined
670
685
  }
671
686
 
672
687
  /**
673
688
  * A query slot that may or may not be present on the matched entity.
674
689
  *
675
- * Callers must narrow on `present` before reading the value.
690
+ * Read it with `value()`: the value, or `undefined` when the entity does not
691
+ * hold the component. Narrow on `present` (then `get()`) only when the
692
+ * component's own value can be `undefined`, where `value()` cannot tell the
693
+ * two apart.
676
694
  */
677
695
  export type OptionalReadCell<T> = PresentOptionalReadCell<T> | AbsentOptionalReadCell
678
696
 
@@ -756,6 +774,25 @@ export const multipleEntitiesError = (count: number): Query.MultipleEntitiesErro
756
774
  // `T` appears only as the check type, which inference never reads.
757
775
  export type Uninferred<T, Bound> = [T] extends [infer U extends Bound] ? U : never
758
776
 
777
+ type DescriptorName<D> = D extends { readonly name: infer Name extends string } ? Name : never
778
+
779
+ /** The names a query requires: its read and write slots, `with`, and plain `added` / `changed` filters. */
780
+ type RequiredNames<Selection, With extends ReadonlyArray<unknown>, Filters extends ReadonlyArray<unknown>> =
781
+ | DescriptorName<{ readonly [K in keyof Selection]: Selection[K] extends { readonly mode: "read" | "write"; readonly descriptor: infer D } ? D : never }[keyof Selection]>
782
+ | DescriptorName<With[number]>
783
+ | DescriptorName<Filters[number] extends infer F ? F extends { readonly kind: "added" | "changed"; readonly descriptor: infer D } ? D : never : never>
784
+
785
+ /**
786
+ * Rejects a `without` naming a component the query also requires: such a
787
+ * query can never match. Use it on the `without` list of a query spec.
788
+ */
789
+ export type ExclusionsPossible<Selection, With extends ReadonlyArray<unknown>, Without extends ReadonlyArray<unknown>, Filters extends ReadonlyArray<unknown>> =
790
+ [Extract<DescriptorName<Without[number]>, RequiredNames<Selection, With, Filters>>] extends [never]
791
+ ? unknown
792
+ : {
793
+ readonly __fixQuery__: `${Extract<DescriptorName<Without[number]>, RequiredNames<Selection, With, Filters>>} is both required and excluded (without): this query can never match; remove it from one side`
794
+ }
795
+
759
796
  export const Query = <
760
797
  const Selection extends Record<string, SelectionAccess<any, Root>>,
761
798
  const With extends ReadonlyArray<ComponentDescriptor> = [],
@@ -769,7 +806,7 @@ export const Query = <
769
806
  >(spec: {
770
807
  readonly selection: Selection
771
808
  readonly with?: With
772
- readonly without?: Without
809
+ readonly without?: Without & ExclusionsPossible<Selection, With, Without, Filters>
773
810
  readonly filters?: Filters
774
811
  readonly withRelations?: WithRelations
775
812
  readonly withoutRelations?: WithoutRelations
package/src/Relation.ts CHANGED
@@ -10,11 +10,9 @@
10
10
  *
11
11
  * @example
12
12
  * ```ts
13
- * // Model a scene-graph style parent/children relationship.
14
- * const Parent = Relation.Hierarchy("Parent", "Children")
15
- *
16
- * // Model another directed gameplay relationship.
17
- * const Target = Relation.Relation("Target", "TargetedBy")
13
+ * // Relations are defined with Descriptor.Hierarchy and Descriptor.Relation.
14
+ * const { relation: ChildOf } = Descriptor.Hierarchy("ChildOf", "Children")
15
+ * const { relation: Targeting } = Descriptor.Relation("Targeting", "TargetedBy")
18
16
  * ```
19
17
  *
20
18
  * @module Relation
@@ -44,7 +42,8 @@ const relationTypeId: RelationTypeId = "~ritmo/Relation"
44
42
 
45
43
  export type RelationKind = "hierarchy" | "relation"
46
44
 
47
- type PairBrand<Name extends string, RelatedName extends string, Kind extends RelationKind> = {
45
+ /** The nominal brand tying a relation to its reverse side. */
46
+ export type PairBrand<Name extends string, RelatedName extends string, Kind extends RelationKind> = {
48
47
  readonly relation: Name
49
48
  readonly related: RelatedName
50
49
  readonly kind: Kind
@@ -281,67 +280,6 @@ export const mutationFailure = <
281
280
  error
282
281
  })
283
282
 
284
- const makePair = <
285
- const Name extends string,
286
- const RelatedName extends string,
287
- const Kind extends RelationKind
288
- >(
289
- kind: Kind,
290
- name: Name,
291
- relatedName: RelatedName
292
- ): {
293
- readonly relation: RelationDefinition<Name, RelatedName, Kind, PairBrand<Name, RelatedName, Kind>>
294
- readonly related: RelatedDefinition<RelatedName, Name, Kind, PairBrand<Name, RelatedName, Kind>>
295
- } => {
296
- type Pair = PairBrand<Name, RelatedName, Kind>
297
- const related = {
298
- kind: "related",
299
- relatedName,
300
- relationName: name,
301
- key: Symbol.for(`ritmo/related/${name}`),
302
- relationKind: kind,
303
- [relationTypeId]: {
304
- _Pair: (_: never) => undefined as unknown as Pair
305
- }
306
- } as RelatedDefinition<RelatedName, Name, Kind, Pair>
307
-
308
- const relation = {
309
- kind: "relation",
310
- name,
311
- relatedName,
312
- key: Symbol.for(`ritmo/relation/${name}`),
313
- relationKind: kind,
314
- linkedDespawn: kind === "hierarchy",
315
- allowSelf: false,
316
- ordered: kind === "hierarchy",
317
- related,
318
- [relationTypeId]: {
319
- _Pair: (_: never) => undefined as unknown as Pair
320
- }
321
- } as RelationDefinition<Name, RelatedName, Kind, Pair>
322
-
323
- return {
324
- relation,
325
- related
326
- }
327
- }
328
-
329
- export const Hierarchy = <
330
- const Name extends string,
331
- const RelatedName extends string
332
- >(
333
- name: Name,
334
- relatedName: RelatedName
335
- ) => makePair("hierarchy", name, relatedName)
336
-
337
- export const Relation = <
338
- const Name extends string,
339
- const RelatedName extends string
340
- >(
341
- name: Name,
342
- relatedName: RelatedName
343
- ) => makePair("relation", name, relatedName)
344
-
345
283
  export interface RelationReadAccess<
346
284
  R extends Relation.Any,
347
285
  S extends Schema.Any,