@typeonce/ritmo 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +2 -0
  2. package/dist/Brand.d.ts +9 -4
  3. package/dist/Brand.d.ts.map +1 -1
  4. package/dist/Brand.js +9 -4
  5. package/dist/Brand.js.map +1 -1
  6. package/dist/Clock.d.ts +186 -0
  7. package/dist/Clock.d.ts.map +1 -0
  8. package/dist/Clock.js +111 -0
  9. package/dist/Clock.js.map +1 -0
  10. package/dist/Command.d.ts +250 -41
  11. package/dist/Command.d.ts.map +1 -1
  12. package/dist/Command.js +59 -23
  13. package/dist/Command.js.map +1 -1
  14. package/dist/Condition.d.ts +12 -1
  15. package/dist/Condition.d.ts.map +1 -1
  16. package/dist/Condition.js.map +1 -1
  17. package/dist/Debug.d.ts +163 -15
  18. package/dist/Debug.d.ts.map +1 -1
  19. package/dist/Debug.js.map +1 -1
  20. package/dist/Decode.d.ts +1 -1
  21. package/dist/Decode.js +1 -1
  22. package/dist/Descriptor.d.ts +67 -24
  23. package/dist/Descriptor.d.ts.map +1 -1
  24. package/dist/Descriptor.js +32 -15
  25. package/dist/Descriptor.js.map +1 -1
  26. package/dist/Entity.d.ts +22 -5
  27. package/dist/Entity.d.ts.map +1 -1
  28. package/dist/Entity.js +2 -2
  29. package/dist/Entity.js.map +1 -1
  30. package/dist/EntityScope.d.ts +26 -1
  31. package/dist/EntityScope.d.ts.map +1 -1
  32. package/dist/EntityScope.js +14 -2
  33. package/dist/EntityScope.js.map +1 -1
  34. package/dist/HostClock.d.ts +49 -0
  35. package/dist/HostClock.d.ts.map +1 -0
  36. package/dist/HostClock.js +46 -0
  37. package/dist/HostClock.js.map +1 -0
  38. package/dist/Inspector.d.ts +7 -1
  39. package/dist/Inspector.d.ts.map +1 -1
  40. package/dist/Inspector.js.map +1 -1
  41. package/dist/Machine.d.ts +67 -36
  42. package/dist/Machine.d.ts.map +1 -1
  43. package/dist/Machine.js +10 -3
  44. package/dist/Machine.js.map +1 -1
  45. package/dist/Query.d.ts +74 -26
  46. package/dist/Query.d.ts.map +1 -1
  47. package/dist/Query.js +32 -28
  48. package/dist/Query.js.map +1 -1
  49. package/dist/Runtime.d.ts +39 -16
  50. package/dist/Runtime.d.ts.map +1 -1
  51. package/dist/Runtime.js +556 -87
  52. package/dist/Runtime.js.map +1 -1
  53. package/dist/Schedule.d.ts +217 -10
  54. package/dist/Schedule.d.ts.map +1 -1
  55. package/dist/Schedule.js +5 -0
  56. package/dist/Schedule.js.map +1 -1
  57. package/dist/Schema.d.ts +196 -44
  58. package/dist/Schema.d.ts.map +1 -1
  59. package/dist/Schema.js +29 -11
  60. package/dist/Schema.js.map +1 -1
  61. package/dist/Snapshot.d.ts +14 -0
  62. package/dist/Snapshot.d.ts.map +1 -1
  63. package/dist/Snapshot.js.map +1 -1
  64. package/dist/System.d.ts +212 -58
  65. package/dist/System.d.ts.map +1 -1
  66. package/dist/System.js +84 -6
  67. package/dist/System.js.map +1 -1
  68. package/dist/index.d.ts +8 -7
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +8 -7
  71. package/dist/index.js.map +1 -1
  72. package/dist/internal/cells.d.ts +3 -3
  73. package/dist/internal/cells.d.ts.map +1 -1
  74. package/dist/internal/cells.js +9 -11
  75. package/dist/internal/cells.js.map +1 -1
  76. package/dist/internal/debug.d.ts +13 -0
  77. package/dist/internal/debug.d.ts.map +1 -1
  78. package/dist/internal/debug.js +192 -7
  79. package/dist/internal/debug.js.map +1 -1
  80. package/dist/internal/equal.d.ts +8 -0
  81. package/dist/internal/equal.d.ts.map +1 -0
  82. package/dist/internal/equal.js +38 -0
  83. package/dist/internal/equal.js.map +1 -0
  84. package/dist/internal/fragments.d.ts.map +1 -1
  85. package/dist/internal/fragments.js +11 -1
  86. package/dist/internal/fragments.js.map +1 -1
  87. package/dist/internal/game.d.ts.map +1 -1
  88. package/dist/internal/game.js +41 -6
  89. package/dist/internal/game.js.map +1 -1
  90. package/dist/internal/order.d.ts +10 -0
  91. package/dist/internal/order.d.ts.map +1 -0
  92. package/dist/internal/order.js +122 -0
  93. package/dist/internal/order.js.map +1 -0
  94. package/dist/internal/queries.d.ts.map +1 -1
  95. package/dist/internal/queries.js +32 -10
  96. package/dist/internal/queries.js.map +1 -1
  97. package/dist/internal/requirements.d.ts +26 -0
  98. package/dist/internal/requirements.d.ts.map +1 -0
  99. package/dist/internal/requirements.js +46 -0
  100. package/dist/internal/requirements.js.map +1 -0
  101. package/package.json +1 -1
  102. package/src/Brand.ts +9 -4
  103. package/src/Clock.ts +311 -0
  104. package/src/Command.ts +407 -89
  105. package/src/Condition.ts +17 -1
  106. package/src/Debug.ts +172 -14
  107. package/src/Decode.ts +1 -1
  108. package/src/Descriptor.ts +92 -37
  109. package/src/Entity.ts +24 -5
  110. package/src/EntityScope.ts +27 -2
  111. package/src/HostClock.ts +64 -0
  112. package/src/Inspector.ts +7 -2
  113. package/src/Machine.ts +81 -37
  114. package/src/Query.ts +110 -38
  115. package/src/Runtime.ts +607 -111
  116. package/src/Schedule.ts +380 -13
  117. package/src/Schema.ts +249 -63
  118. package/src/Snapshot.ts +4 -0
  119. package/src/System.ts +248 -93
  120. package/src/index.ts +8 -7
  121. package/src/internal/cells.ts +10 -12
  122. package/src/internal/debug.ts +208 -12
  123. package/src/internal/equal.ts +31 -0
  124. package/src/internal/fragments.ts +12 -2
  125. package/src/internal/game.ts +61 -7
  126. package/src/internal/order.ts +144 -0
  127. package/src/internal/queries.ts +37 -13
  128. package/src/internal/requirements.ts +71 -0
  129. package/dist/Definition.d.ts +0 -99
  130. package/dist/Definition.d.ts.map +0 -1
  131. package/dist/Definition.js +0 -68
  132. package/dist/Definition.js.map +0 -1
  133. package/dist/Feature.d.ts +0 -106
  134. package/dist/Feature.d.ts.map +0 -1
  135. package/dist/Feature.js +0 -87
  136. package/dist/Feature.js.map +0 -1
  137. package/dist/Fx.d.ts +0 -128
  138. package/dist/Fx.d.ts.map +0 -1
  139. package/dist/Fx.js +0 -112
  140. package/dist/Fx.js.map +0 -1
  141. package/dist/internal/definition.d.ts +0 -17
  142. package/dist/internal/definition.d.ts.map +0 -1
  143. package/dist/internal/definition.js +0 -25
  144. package/dist/internal/definition.js.map +0 -1
  145. package/src/Definition.ts +0 -106
  146. package/src/Feature.ts +0 -241
  147. package/src/Fx.ts +0 -176
  148. package/src/internal/definition.ts +0 -56
package/src/Condition.ts CHANGED
@@ -45,10 +45,25 @@ type AnyQuery = Query.Query.Any<any>
45
45
  /** Access categories a check may declare. */
46
46
  export interface CheckAccessInput {
47
47
  readonly queries?: Record<string, AnyQuery>
48
- readonly resources?: Record<string, System.ResourceRead<Descriptor<"resource", string, any>>>
48
+ readonly resources?: Record<string, CheckResourceAccess>
49
49
  readonly machines?: Record<string, Machine.MachineRead<Machine.StateMachine.Any>>
50
50
  }
51
51
 
52
+ /** Accepted at the constraint so `ExactCheckAccess` can reject the last two with a message naming the fix. */
53
+ type CheckResourceAccess =
54
+ | System.ResourceRead<Descriptor<"resource", string, any>>
55
+ | System.ResourceChanged<Descriptor<"resource", string, any>>
56
+ | System.ResourceWrite<Descriptor<"resource", string, any>>
57
+
58
+ /** A check reads committed values and owns no cursor: `changedResource` and `writeResource` slots become a message naming the fix. */
59
+ type CheckResources<Resources> = {
60
+ readonly [K in keyof Resources]: Resources[K] extends { readonly mode: "changed" }
61
+ ? "changedResource consumes a per-reader cursor, which a check does not have: use Game.System.readResource here, and changedResource in the gated system"
62
+ : Resources[K] extends { readonly mode: "write" }
63
+ ? "a check cannot write resources: use Game.System.readResource"
64
+ : Resources[K]
65
+ }
66
+
52
67
  type QueryWriteAccess<Q extends AnyQuery> =
53
68
  Extract<Q["selection"][keyof Q["selection"]], { readonly mode: "write" }>
54
69
 
@@ -72,6 +87,7 @@ export type ExactCheckAccess<Access extends CheckAccessInput> = Access & {
72
87
  readonly queries?: Access["queries"] extends Record<string, AnyQuery>
73
88
  ? StatelessQueries<Access["queries"]>
74
89
  : never
90
+ readonly resources?: CheckResources<Access["resources"]>
75
91
  }
76
92
 
77
93
  /** What a check's predicate sees: its declared resources, machines, and queries, read-only. */
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. */
@@ -93,6 +116,10 @@ export type Storage = "constructed" | "transient" | "plain"
93
116
  export interface ComponentDescription {
94
117
  readonly name: string
95
118
  readonly storage: Storage
119
+ /** Declared `immutable: true`: queries only read it, `commands.insert` replaces it. */
120
+ readonly immutable: boolean
121
+ /** The components it directly requires (`Schema.requires`). */
122
+ readonly requires: ReadonlyArray<string>
96
123
  }
97
124
 
98
125
  export interface ResourceDescription {
@@ -113,8 +140,22 @@ export interface RelationDescription {
113
140
  export interface MachineDescription {
114
141
  readonly name: string
115
142
  readonly states: ReadonlyArray<Machine.StateValue>
116
- /** The committed value, or `undefined` when the runtime does not provide the machine. */
143
+ /**
144
+ * The committed value, or `undefined` when the runtime does not provide the
145
+ * machine, or when it is a sub-state whose parent is in another state.
146
+ */
117
147
  readonly current: Machine.StateValue | undefined
148
+ /** Whether the runtime was given an initial value for the machine. */
149
+ readonly provided: boolean
150
+ /** For a sub-state, where it exists, as `Parent=state`. */
151
+ readonly within: string | undefined
152
+ }
153
+
154
+ /** An entity scope and the state changes that despawn it (`Game.EntityScope(name, { despawnOnExit, despawnOnEnter })`). */
155
+ export interface ScopeDescription {
156
+ readonly name: string
157
+ /** Triggers as `onExit(Flow=Playing)` / `onEnter(Flow=Menu)`; empty when only `commands.despawnScope` ends it. */
158
+ readonly despawnOn: ReadonlyArray<string>
118
159
  }
119
160
 
120
161
  export interface ServiceDescription {
@@ -132,6 +173,8 @@ export interface QueryDescription {
132
173
  readonly without: ReadonlyArray<string>
133
174
  readonly added: ReadonlyArray<string>
134
175
  readonly changed: ReadonlyArray<string>
176
+ /** One entry per `anyOf` filter: the components of its `added` and `changed` members. */
177
+ readonly anyOf: ReadonlyArray<{ readonly added: ReadonlyArray<string>; readonly changed: ReadonlyArray<string> }>
135
178
  /** Relation filters and reads, for example `with ChildOf` or `read Children`. */
136
179
  readonly relations: ReadonlyArray<string>
137
180
  }
@@ -142,7 +185,8 @@ export interface SystemDescription {
142
185
  /** Where the system runs, as `schedule#step`. */
143
186
  readonly placements: ReadonlyArray<string>
144
187
  readonly queries: ReadonlyArray<QueryDescription>
145
- readonly resources: { readonly reads: ReadonlyArray<string>; readonly writes: ReadonlyArray<string> }
188
+ /** Resource reads and writes. `changed` lists the reads declared with `changedResource` (also in `reads`). */
189
+ readonly resources: { readonly reads: ReadonlyArray<string>; readonly changed: ReadonlyArray<string>; readonly writes: ReadonlyArray<string> }
146
190
  readonly events: { readonly reads: ReadonlyArray<string>; readonly writes: ReadonlyArray<string> }
147
191
  readonly machines: {
148
192
  readonly reads: ReadonlyArray<string>
@@ -153,7 +197,15 @@ export interface SystemDescription {
153
197
  readonly removed: ReadonlyArray<string>
154
198
  readonly despawned: boolean
155
199
  readonly relationFailures: ReadonlyArray<string>
200
+ /** Events and resources read with `Game.System.thisTick`, and query slots as `queries.<slot>`. */
201
+ readonly thisTick: ReadonlyArray<string>
156
202
  readonly services: ReadonlyArray<string>
203
+ /**
204
+ * The declared command permissions (`Game.System.commands(...)`), by
205
+ * kind: component names for spawns, inserts, removes and despawns, scope
206
+ * names for despawnsScope, relation names for relates.
207
+ */
208
+ readonly commands: CommandPermissions
157
209
  /** Run conditions, rendered, for example `stateChanged(Game/Flow)`. */
158
210
  readonly when: ReadonlyArray<string>
159
211
  /** Pause conditions, rendered: while one fails the system waits and holds its stream entries. */
@@ -162,6 +214,16 @@ export interface SystemDescription {
162
214
  readonly local: boolean
163
215
  }
164
216
 
217
+ /** Declared command permissions of one system, by kind. Empty lists when it declares none. */
218
+ export interface CommandPermissions {
219
+ readonly spawns: ReadonlyArray<string>
220
+ readonly inserts: ReadonlyArray<string>
221
+ readonly removes: ReadonlyArray<string>
222
+ readonly despawns: ReadonlyArray<string>
223
+ readonly despawnsScope: ReadonlyArray<string>
224
+ readonly relates: ReadonlyArray<string>
225
+ }
226
+
165
227
  export type StepDescription =
166
228
  | { readonly kind: "system"; readonly system: string }
167
229
  | { readonly kind: "applyDeferred" }
@@ -175,17 +237,34 @@ export interface ScheduleDescription {
175
237
  /**
176
238
  * A finding about the described schedules. `warning` is almost certainly a
177
239
  * bug; `info` is worth a look but can be intentional.
240
+ *
241
+ * Most lints read declarations only. Some also use what the runtime has seen
242
+ * so far: events sent with `runtime.send(...)` count as written,
243
+ * `relation-failure-never-read` fires for a relation that applied commands
244
+ * have tried to relate or reorder (which can fail) while no described system
245
+ * reads its failures, and `requirement-violated` lists commands skipped
246
+ * because they would have broken a `Schema.requires` requirement, so
247
+ * describe after the game has run for a while. `scope-trigger-never-applied`
248
+ * also reads which machines the runtime provides.
178
249
  */
179
250
  export interface Lint {
180
251
  readonly severity: "warning" | "info"
181
252
  readonly code:
182
253
  | "event-never-read"
254
+ | "relation-failure-never-read"
183
255
  | "event-never-written"
184
256
  | "next-state-never-applied"
257
+ | "resource-changed-never-written"
185
258
  | "component-never-read"
186
259
  | "read-before-write"
260
+ | "this-tick-order"
261
+ | "command-permission-unused"
262
+ | "requirement-violated"
263
+ | "scope-trigger-never-applied"
187
264
  readonly message: string
188
265
  readonly subject: string
266
+ /** What to change to resolve it (or, for `info`, when it is fine as it is). */
267
+ readonly fix: string
189
268
  }
190
269
 
191
270
  /** Readers and writers of one component, resource, or event. */
@@ -202,12 +281,15 @@ export interface Description {
202
281
  readonly events: ReadonlyArray<{ readonly name: string }>
203
282
  readonly relations: ReadonlyArray<RelationDescription>
204
283
  readonly machines: ReadonlyArray<MachineDescription>
284
+ /** Scopes defined through the game, in definition order. */
285
+ readonly scopes: ReadonlyArray<ScopeDescription>
205
286
  readonly services: ReadonlyArray<ServiceDescription>
206
287
  readonly schedules: ReadonlyArray<ScheduleDescription>
207
288
  readonly systems: ReadonlyArray<SystemDescription>
208
289
  /**
209
290
  * Readers and writers across the named schedules. Component writers are
210
- * systems with a write query slot; command inserts are not static.
291
+ * systems with a write query slot or a declared `spawns`, `inserts`, or
292
+ * `removes` permission for it.
211
293
  */
212
294
  readonly access: {
213
295
  readonly components: ReadonlyArray<AccessIndexEntry>
@@ -340,6 +422,32 @@ export type Effect =
340
422
  | { readonly kind: "relate"; readonly entity: number; readonly relation: string; readonly target: number }
341
423
  | { readonly kind: "unrelate"; readonly entity: number; readonly relation: string }
342
424
  | { readonly kind: "relationFailure"; readonly entity: number; readonly relation: string; readonly error: string }
425
+ /** A command skipped because it would leave `component` without `missing` (`Schema.requires`). */
426
+ | {
427
+ readonly kind: "requirementViolated"
428
+ readonly entity: number
429
+ readonly command: RequirementViolation["command"]
430
+ readonly component: string
431
+ readonly missing: string
432
+ }
433
+
434
+ /**
435
+ * Commands of one system skipped because applying them would have left a
436
+ * component without one it requires (`Schema.requires`): the
437
+ * `requirement-violated` lint.
438
+ */
439
+ export interface RequirementViolation {
440
+ readonly system: string
441
+ readonly command: "spawn" | "insert" | "remove"
442
+ /** The requirer. */
443
+ readonly component: string
444
+ /** The required component it would have lacked. */
445
+ readonly missing: string
446
+ /** The last entity it happened to. */
447
+ readonly entity: number
448
+ /** How many commands were skipped this way. */
449
+ readonly count: number
450
+ }
343
451
 
344
452
  export interface AppliedCommand {
345
453
  readonly tag: string
@@ -433,8 +541,10 @@ export interface TransitionEvent {
433
541
  readonly tick: number
434
542
  readonly schedule: string
435
543
  readonly machine: string
436
- readonly from: Machine.StateValue
437
- readonly to: Machine.StateValue
544
+ /** `undefined` when a sub-state starts existing (its parent entered its state). */
545
+ readonly from: Machine.StateValue | undefined
546
+ /** `undefined` when a sub-state stops existing (its parent left its state). */
547
+ readonly to: Machine.StateValue | undefined
438
548
  /**
439
549
  * `applied` committed the new value; `unchanged` was a `setIfChanged` to the
440
550
  * current value; `failed` stopped in an exit or transition schedule and was
@@ -450,6 +560,45 @@ export interface RestoreEvent {
450
560
  readonly ok: boolean
451
561
  }
452
562
 
563
+ /** What `observeHost` delivers. */
564
+ export type HostEvent<S extends Schema.Any = Schema.Any, Root = unknown> =
565
+ | {
566
+ readonly type: "tick.start"
567
+ /** The schedule values passed to this `tick(...)` (or `tryTick`) call, in order. */
568
+ readonly schedules: ReadonlyArray<ExecutableScheduleDefinition<S, any, Root, any, any, AnyScheduleOrder>>
569
+ readonly frame: number
570
+ }
571
+ | {
572
+ readonly type: "tick.end"
573
+ readonly schedules: ReadonlyArray<ExecutableScheduleDefinition<S, any, Root, any, any, AnyScheduleOrder>>
574
+ readonly frame: number
575
+ /** `failed`: a system returned `Result.failure`; `defect`: one threw (rethrown after this event). */
576
+ readonly outcome: "ok" | "failed" | "defect"
577
+ readonly error?: unknown
578
+ }
579
+ | { readonly type: "send"; readonly event: string; readonly value: unknown }
580
+ | FailureIgnoredEvent
581
+
582
+ /**
583
+ * A failed `tick` / `tryTick` result whose `ok` nobody read: the failure
584
+ * went unnoticed, and the systems after the failing one did not run that
585
+ * tick. Found at the start of the next tick (`when: "next-tick"`) or by
586
+ * `checkIgnoredFailures()` (`when: "session-end"`). Debug runtimes only.
587
+ */
588
+ export interface FailureIgnoredEvent {
589
+ readonly type: "failure.ignored"
590
+ /** The frame of the failed tick. */
591
+ readonly frame: number
592
+ /** The schedule holding the failing system (the first one for a tick that ran nothing). */
593
+ readonly schedule: string
594
+ /** The failing system; `undefined` when `tryTick` ran nothing (missing requirements, order). */
595
+ readonly system: string | undefined
596
+ readonly error: unknown
597
+ /** The systems of the tick that did not run because of the failure, in order. */
598
+ readonly skipped: ReadonlyArray<string>
599
+ readonly when: "next-tick" | "session-end"
600
+ }
601
+
453
602
  /** Everything `observe` delivers. */
454
603
  export type TraceEvent =
455
604
  | FrameEvent
@@ -461,14 +610,21 @@ export type TraceEvent =
461
610
  | DeferredEvent
462
611
  | TransitionEvent
463
612
  | RestoreEvent
613
+ | FailureIgnoredEvent
464
614
 
465
615
  /**
466
- * The `debug` runtime option: `true` attaches a handle.
616
+ * The `debug` runtime option: `{ hostClock }` attaches a handle, timing
617
+ * traces with that clock. A value that may be `undefined`
618
+ * (`import.meta.env.DEV ? { hostClock } : undefined`) attaches it when
619
+ * present and types `debug` as optional, so code checks it before use.
467
620
  */
468
- export type Option = true
621
+ export interface Option {
622
+ /** Times trace events (`ms`); see `HostClock`. */
623
+ readonly hostClock: HostClock
624
+ }
469
625
 
470
626
  /**
471
- * The member a runtime made with `debug: true` carries.
627
+ * The member a runtime made with `debug: { hostClock }` carries.
472
628
  */
473
629
  export interface Member<S extends Schema.Any, Root> {
474
630
  readonly debug: Handle<S, Root>
@@ -476,4 +632,6 @@ export interface Member<S extends Schema.Any, Root> {
476
632
 
477
633
  /** Resolves the extra runtime member for a `debug` option type. */
478
634
  export type MemberFor<Enabled, S extends Schema.Any, Root> =
479
- [Enabled] extends [Option] ? Member<S, Root> : {}
635
+ [Enabled] extends [true] ? Member<S, Root>
636
+ : [Enabled] extends [false | undefined] ? {}
637
+ : Partial<Member<S, Root>>
package/src/Decode.ts CHANGED
@@ -188,7 +188,7 @@ export const struct = <const Fields extends Readonly<Record<string, Codec<any>>>
188
188
  /**
189
189
  * A stored entity handle for schema root `Root`, optionally with an intent
190
190
  * component. Decoding never proves the entity exists; resolve it with
191
- * `lookup.getHandle(...)` as usual.
191
+ * `queries.x.getHandle(...)` (through a declared query) as usual.
192
192
  */
193
193
  export const handle = <
194
194
  Root,
package/src/Descriptor.ts CHANGED
@@ -16,8 +16,9 @@
16
16
  * 3. bind one `Game` with `Schema.bind(...)`
17
17
  *
18
18
  * Reach for this module first whenever you introduce a new component,
19
- * resource, event, or service to the game. Discrete modes whose transitions
20
- * matter are state machines (`Game.StateMachine(...)`), not descriptors.
19
+ * resource, event, or service to the game. A game-wide mode whose transitions
20
+ * matter is a state machine (`Game.StateMachine(...)`); a state each entity
21
+ * has on its own is a component (`Descriptor.EntityState(...)`).
21
22
  *
22
23
  * @module Descriptor
23
24
  * @docGroup core
@@ -165,6 +166,44 @@ export interface TransientDescriptor<
165
166
  readonly transient: true
166
167
  }
167
168
 
169
+ /**
170
+ * Marks a component that is never edited in place: `Game.Query.write(...)`
171
+ * rejects it, and the only way to change its value is to replace it with
172
+ * `commands.insert(...)` (applied at `applyDeferred()`, seen by
173
+ * `Game.Query.changed(...)`).
174
+ */
175
+ export interface Immutable {
176
+ readonly immutable: true
177
+ }
178
+
179
+ /**
180
+ * Options of the component descriptor constructors (`Component`,
181
+ * `ConstructedComponent`, `TransientComponent`).
182
+ */
183
+ export interface ComponentOptions {
184
+ /**
185
+ * `true` for data that says what an entity is (a kind, a team, a tile) and
186
+ * is replaced, never edited: queries can only read it, and
187
+ * `commands.insert(...)` replaces it. Leave it out for state that systems
188
+ * update every frame.
189
+ */
190
+ readonly immutable?: true
191
+ }
192
+
193
+ /** The descriptor a component constructor returns for `Options`: `D`, plus `Immutable` when `immutable: true`. */
194
+ export type WithComponentOptions<D, Options extends ComponentOptions> =
195
+ Options extends { readonly immutable: true } ? D & Immutable : D
196
+
197
+ const applyComponentOptions = <D extends object>(descriptor: D, options: ComponentOptions | undefined): D =>
198
+ options?.immutable === true ? { ...descriptor, immutable: true } : descriptor
199
+
200
+ /**
201
+ * Whether a component descriptor was declared `immutable: true` (readable by
202
+ * queries, replaced only with `commands.insert(...)`).
203
+ */
204
+ export const isImmutable = (descriptor: Descriptor.Any): descriptor is Descriptor.Any & Immutable =>
205
+ "immutable" in descriptor && descriptor.immutable === true
206
+
168
207
  /**
169
208
  * Type-level helpers for working with descriptors.
170
209
  */
@@ -253,11 +292,15 @@ const makeConstructedDescriptor = <
253
292
  * @example
254
293
  * ```ts
255
294
  * const Position = Descriptor.Component<{ x: number; y: number }>()("Position")
295
+ * // What an entity is, replaced with commands.insert and never written by a query:
296
+ * const EnemyKind = Descriptor.Component<"grunt" | "boss">()("EnemyKind", { immutable: true })
256
297
  * ```
257
298
  */
258
- export const Component = <Value>() => <const Name extends string>(
259
- name: Name
260
- ): Descriptor<"component", Name, Value> => makeDescriptor("component", name)
299
+ export const Component = <Value>() => <const Name extends string, const Options extends ComponentOptions = {}>(
300
+ name: Name,
301
+ options?: Options
302
+ ): WithComponentOptions<Descriptor<"component", Name, Value>, Options> =>
303
+ applyComponentOptions(makeDescriptor("component", name), options) as WithComponentOptions<Descriptor<"component", Name, Value>, Options>
261
304
 
262
305
  /**
263
306
  * Defines a component descriptor that also knows how to validate raw values.
@@ -274,10 +317,11 @@ export const Component = <Value>() => <const Name extends string>(
274
317
  */
275
318
  export const ConstructedComponent = <Value, Raw, Error, Constructor extends {} = {}>(
276
319
  constructor: ResultConstructor<Value, Raw, Error> & Constructor
277
- ) => <const Name extends string>(
278
- name: Name
279
- ): ConstructedFor<"component", Name, Value, Raw, Error, Constructor> =>
280
- makeConstructedDescriptor("component", name, constructor) as ConstructedFor<"component", Name, Value, Raw, Error, Constructor>
320
+ ) => <const Name extends string, const Options extends ComponentOptions = {}>(
321
+ name: Name,
322
+ options?: Options
323
+ ): WithComponentOptions<ConstructedFor<"component", Name, Value, Raw, Error, Constructor>, Options> =>
324
+ applyComponentOptions(makeConstructedDescriptor("component", name, constructor), options) as WithComponentOptions<ConstructedFor<"component", Name, Value, Raw, Error, Constructor>, Options>
281
325
 
282
326
  /**
283
327
  * Defines a component descriptor that snapshots skip.
@@ -290,9 +334,11 @@ export const ConstructedComponent = <Value, Raw, Error, Constructor extends {} =
290
334
  * const SpriteRef = Descriptor.TransientComponent<{ frame: number }>()("SpriteRef")
291
335
  * ```
292
336
  */
293
- export const TransientComponent = <Value>() => <const Name extends string>(
294
- name: Name
295
- ): TransientDescriptor<"component", Name, Value> => makeTransientDescriptor("component", name)
337
+ export const TransientComponent = <Value>() => <const Name extends string, const Options extends ComponentOptions = {}>(
338
+ name: Name,
339
+ options?: Options
340
+ ): WithComponentOptions<TransientDescriptor<"component", Name, Value>, Options> =>
341
+ applyComponentOptions(makeTransientDescriptor("component", name), options) as WithComponentOptions<TransientDescriptor<"component", Name, Value>, Options>
296
342
 
297
343
  /**
298
344
  * Defines a marker component: no data, only presence (`[Player, {}]`).
@@ -311,70 +357,77 @@ export const Tag = <const Name extends string>(
311
357
  ): DecodableDescriptor<"component", Name, {}, {}, DecodeModule.DecodeError> =>
312
358
  ConstructedComponent(DecodeModule.struct({}))(name)
313
359
 
314
- /** One state of a {@link State} component: a string or number literal. */
315
- export type StateValue = string | number
360
+ /** One state of a {@link EntityState} component: a string or number literal. */
361
+ export type EntityStateValue = string | number
316
362
 
317
363
  /**
318
- * The allowed moves of a {@link State} component: for every state, the
364
+ * The allowed moves of a {@link EntityState} component: for every state, the
319
365
  * states it may move to (`[]` for a state with no way out).
320
366
  */
321
- export type StateTransitions<States extends readonly [StateValue, ...Array<StateValue>]> = {
367
+ export type EntityStateTransitions<States extends readonly [EntityStateValue, ...Array<EntityStateValue>]> = {
322
368
  readonly [From in States[number]]: ReadonlyArray<States[number]>
323
369
  }
324
370
 
325
371
  /**
326
372
  * A component whose value is one of a closed set of states, with an optional
327
- * graph of allowed moves. Built with {@link State}.
373
+ * graph of allowed moves. Built with {@link EntityState}.
328
374
  */
329
- export interface StateDescriptor<
375
+ export interface EntityStateDescriptor<
330
376
  out Name extends string,
331
- in out States extends readonly [StateValue, ...Array<StateValue>],
332
- in out Transitions extends StateTransitions<States> | undefined
377
+ in out States extends readonly [EntityStateValue, ...Array<EntityStateValue>],
378
+ in out Transitions extends EntityStateTransitions<States> | undefined
333
379
  > extends DecodableDescriptor<"component", Name, States[number], States[number], DecodeModule.DecodeError> {
334
380
  readonly states: States
335
381
  readonly transitions: Transitions
336
382
  }
337
383
 
338
384
  /**
339
- * Defines a state component: its value is one of `states`, validated (so
340
- * snapshots reject unknown states), and it behaves like any other component
341
- * (storage, change detection, rollback, traces).
385
+ * Defines a per-entity state: a component whose value is one of `states`,
386
+ * validated (so snapshots reject unknown states), that behaves like any other
387
+ * component (storage, change detection, rollback, traces).
388
+ *
389
+ * Use this when each entity has its own state: an enemy's attack windup, a
390
+ * door's open/closed, a unit's AI mode. Use `Game.StateMachine(...)` instead
391
+ * for one game-wide mode (menu, playing, paused, game over): machines change
392
+ * only at `applyStateTransitions(...)`, run `onEnter`/`onExit` schedules, and
393
+ * gate systems with `Game.Condition.inState(...)`. An entity state does none
394
+ * of that; it is data on an entity.
342
395
  *
343
396
  * With `transitions`, its write cell gains `transition(from, to)`: the pair
344
397
  * is checked against the graph at compile time, and at runtime the move
345
- * happens only if the current state is still `from`, otherwise it returns a
346
- * `StateMismatch` failure. Transitions are ordinary component writes:
398
+ * happens only if the current state is still `from`, otherwise it returns an
399
+ * `EntityStateMismatch` failure. Transitions are ordinary component writes:
347
400
  * immediate, visible to later systems, rolled back with a failed system.
348
401
  *
349
402
  * @example
350
403
  * ```ts
351
- * const Phase = Descriptor.State("Phase", ["ready", "windup", "active"] as const, {
404
+ * const Attack = Descriptor.EntityState("Attack", ["ready", "windup", "active"] as const, {
352
405
  * transitions: { ready: ["windup"], windup: ["active", "ready"], active: ["ready"] }
353
406
  * })
354
- * // in a system with a write slot `phase`:
355
- * const moved = data.phase.transition("windup", "active") // "ready" -> "active" does not compile
407
+ * // in a system with a write slot `attack`:
408
+ * const moved = data.attack.transition("windup", "active") // "ready" -> "active" does not compile
356
409
  * ```
357
410
  */
358
- export const State = <
411
+ export const EntityState = <
359
412
  const Name extends string,
360
- const States extends readonly [StateValue, ...Array<StateValue>],
361
- const Transitions extends StateTransitions<States> | undefined = undefined
413
+ const States extends readonly [EntityStateValue, ...Array<EntityStateValue>],
414
+ const Transitions extends EntityStateTransitions<States> | undefined = undefined
362
415
  >(
363
416
  name: Name,
364
417
  states: States,
365
418
  options?: {
366
419
  readonly transitions: Transitions & { readonly [Key in Exclude<keyof Transitions, States[number]>]: never }
367
420
  }
368
- ): StateDescriptor<Name, States, Transitions> => ({
421
+ ): EntityStateDescriptor<Name, States, Transitions> => ({
369
422
  ...makeConstructedDescriptor("component", name, DecodeModule.literal(...states)),
370
423
  states,
371
424
  transitions: options?.transitions
372
- }) as unknown as StateDescriptor<Name, States, Transitions>
425
+ }) as unknown as EntityStateDescriptor<Name, States, Transitions>
373
426
 
374
- /** Whether a descriptor was built with {@link State}. */
375
- export const isState = (
427
+ /** Whether a descriptor was built with {@link EntityState}. */
428
+ export const isEntityState = (
376
429
  descriptor: Descriptor.Any
377
- ): descriptor is StateDescriptor<string, readonly [StateValue, ...Array<StateValue>], StateTransitions<readonly [StateValue, ...Array<StateValue>]> | undefined> =>
430
+ ): descriptor is EntityStateDescriptor<string, readonly [EntityStateValue, ...Array<EntityStateValue>], EntityStateTransitions<readonly [EntityStateValue, ...Array<EntityStateValue>]> | undefined> =>
378
431
  "states" in descriptor
379
432
 
380
433
  /**
@@ -568,7 +621,9 @@ export function constructorOf<D extends Descriptor.Any>(
568
621
  * `related` is the reverse collection maintained by the runtime.
569
622
  *
570
623
  * Use hierarchy when the relationship must support ordered children,
571
- * ancestor/descendant traversal, and linked recursive despawn.
624
+ * ancestor/descendant traversal, and linked recursive despawn. Use
625
+ * `Game.EntityScope(...)` instead when entities only share a lifetime (a
626
+ * level, a menu) and need no tree, order, or traversal.
572
627
  */
573
628
  export const Hierarchy = RelationModule.Hierarchy
574
629
 
package/src/Entity.ts CHANGED
@@ -56,7 +56,8 @@ import * as Result from "./Result.ts"
56
56
  * @example
57
57
  * ```ts
58
58
  * const handle = Game.Entity.handle(playerId, Player)
59
- * const resolved = lookup.getHandle(handle, PlayerQuery)
59
+ * // `players` is a query the system declares: queries: { players: PlayerQuery }
60
+ * const resolved = queries.players.getHandle(handle)
60
61
  * if (!resolved.ok) return
61
62
  * ```
62
63
  */
@@ -167,11 +168,23 @@ export type StagedComponent = readonly [Descriptor<"component", string, any>, un
167
168
  *
168
169
  * Query execution is the main source of `EntityRef` values.
169
170
  */
170
- export interface EntityRef<S extends Schema.Any, out P extends ComponentProof, Root = unknown> {
171
+ export interface EntityRef<S extends Schema.Any, out P extends ComponentProof, Root = unknown, out Proves = unknown, out Lacks = never> {
171
172
  /**
172
173
  * Runtime tag for debugging and pattern matching.
173
174
  */
174
175
  readonly kind: "EntityRef"
176
+ /**
177
+ * The component descriptors the query that produced this ref proves (its
178
+ * required slots and `with`): what `commands.despawn`, `insert`, and
179
+ * `remove` check. Type-level only.
180
+ */
181
+ readonly __proves?: (_: never) => Proves
182
+ /**
183
+ * The component descriptors the producing query proves absent (its
184
+ * `without`): what `commands.remove` checks against `Schema.requires`.
185
+ * Type-level only.
186
+ */
187
+ readonly __lacks?: (_: never) => Lacks
175
188
  /**
176
189
  * The schema-bound entity identity.
177
190
  */
@@ -193,12 +206,18 @@ export interface EntityMut<
193
206
  S extends Schema.Any,
194
207
  out P extends ComponentProof,
195
208
  out W extends ComponentProof,
196
- Root = unknown
209
+ Root = unknown,
210
+ out Proves = unknown,
211
+ out Lacks = never
197
212
  > {
198
213
  /**
199
214
  * Runtime tag for debugging and pattern matching.
200
215
  */
201
216
  readonly kind: "EntityMut"
217
+ /** The component descriptors the producing query proves; see `EntityRef`. */
218
+ readonly __proves?: (_: never) => Proves
219
+ /** The component descriptors the producing query proves absent; see `EntityRef`. */
220
+ readonly __lacks?: (_: never) => Lacks
202
221
  /**
203
222
  * The schema-bound entity identity.
204
223
  */
@@ -247,7 +266,7 @@ export type HandleTarget<S extends Schema.Any, Root = unknown> =
247
266
  * Converts a current entity into a durable handle for storage.
248
267
  *
249
268
  * The handle is storage-safe, not a proof of liveness: resolve it later with
250
- * `lookup.getHandle(...)`, which fails explicitly when the entity is gone.
269
+ * `queries.x.getHandle(...)` (through a declared query), which fails explicitly when the entity is gone.
251
270
  *
252
271
  * Pass an `intent` component when later code assumes a role such as "player"
253
272
  * or "damage source". An intent does not prove the component is still present;
@@ -283,7 +302,7 @@ export interface InvalidHandle {
283
302
  * handles, so restoring validates them like any other value.
284
303
  *
285
304
  * A handle only names an entity; it never proves the entity exists. Resolving
286
- * it with `lookup.getHandle(...)` stays the checked step, so any positive
305
+ * it with `queries.x.getHandle(...)` (through a declared query) stays the checked step, so any positive
287
306
  * integer id decodes. Object-shaped handles and other non-numeric values
288
307
  * are rejected.
289
308
  *