@typeonce/ritmo 0.7.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 (141) 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 +181 -40
  7. package/dist/Command.d.ts.map +1 -1
  8. package/dist/Command.js +31 -11
  9. package/dist/Command.js.map +1 -1
  10. package/dist/Condition.d.ts +12 -1
  11. package/dist/Condition.d.ts.map +1 -1
  12. package/dist/Condition.js.map +1 -1
  13. package/dist/Debug.d.ts +127 -13
  14. package/dist/Debug.d.ts.map +1 -1
  15. package/dist/Descriptor.d.ts +61 -10
  16. package/dist/Descriptor.d.ts.map +1 -1
  17. package/dist/Descriptor.js +22 -10
  18. package/dist/Descriptor.js.map +1 -1
  19. package/dist/Entity.d.ts +22 -7
  20. package/dist/Entity.d.ts.map +1 -1
  21. package/dist/Entity.js.map +1 -1
  22. package/dist/EntityScope.d.ts +20 -1
  23. package/dist/EntityScope.d.ts.map +1 -1
  24. package/dist/EntityScope.js +8 -2
  25. package/dist/EntityScope.js.map +1 -1
  26. package/dist/FixedLoop.d.ts +106 -0
  27. package/dist/FixedLoop.d.ts.map +1 -0
  28. package/dist/FixedLoop.js +97 -0
  29. package/dist/FixedLoop.js.map +1 -0
  30. package/dist/HostClock.d.ts +1 -1
  31. package/dist/HostClock.d.ts.map +1 -1
  32. package/dist/HostClock.js +2 -1
  33. package/dist/HostClock.js.map +1 -1
  34. package/dist/Machine.d.ts +59 -34
  35. package/dist/Machine.d.ts.map +1 -1
  36. package/dist/Machine.js +10 -3
  37. package/dist/Machine.js.map +1 -1
  38. package/dist/Query.d.ts +100 -14
  39. package/dist/Query.d.ts.map +1 -1
  40. package/dist/Query.js +34 -5
  41. package/dist/Query.js.map +1 -1
  42. package/dist/Relation.d.ts +5 -14
  43. package/dist/Relation.d.ts.map +1 -1
  44. package/dist/Relation.js +0 -32
  45. package/dist/Relation.js.map +1 -1
  46. package/dist/Runtime.d.ts +61 -15
  47. package/dist/Runtime.d.ts.map +1 -1
  48. package/dist/Runtime.js +594 -70
  49. package/dist/Runtime.js.map +1 -1
  50. package/dist/Schedule.d.ts +24 -15
  51. package/dist/Schedule.d.ts.map +1 -1
  52. package/dist/Schedule.js +2 -1
  53. package/dist/Schedule.js.map +1 -1
  54. package/dist/Schema.d.ts +233 -48
  55. package/dist/Schema.d.ts.map +1 -1
  56. package/dist/Schema.js +33 -7
  57. package/dist/Schema.js.map +1 -1
  58. package/dist/Snapshot.d.ts +90 -1
  59. package/dist/Snapshot.d.ts.map +1 -1
  60. package/dist/Snapshot.js +42 -1
  61. package/dist/Snapshot.js.map +1 -1
  62. package/dist/System.d.ts +132 -31
  63. package/dist/System.d.ts.map +1 -1
  64. package/dist/System.js +32 -0
  65. package/dist/System.js.map +1 -1
  66. package/dist/index.d.ts +4 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +4 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/internal/cells.d.ts +2 -0
  71. package/dist/internal/cells.d.ts.map +1 -1
  72. package/dist/internal/cells.js +34 -8
  73. package/dist/internal/cells.js.map +1 -1
  74. package/dist/internal/debug.d.ts +10 -0
  75. package/dist/internal/debug.d.ts.map +1 -1
  76. package/dist/internal/debug.js +171 -12
  77. package/dist/internal/debug.js.map +1 -1
  78. package/dist/internal/equal.d.ts +8 -0
  79. package/dist/internal/equal.d.ts.map +1 -0
  80. package/dist/internal/equal.js +38 -0
  81. package/dist/internal/equal.js.map +1 -0
  82. package/dist/internal/fragments.d.ts +2 -6
  83. package/dist/internal/fragments.d.ts.map +1 -1
  84. package/dist/internal/fragments.js +36 -3
  85. package/dist/internal/fragments.js.map +1 -1
  86. package/dist/internal/game.d.ts +2 -0
  87. package/dist/internal/game.d.ts.map +1 -1
  88. package/dist/internal/game.js +48 -7
  89. package/dist/internal/game.js.map +1 -1
  90. package/dist/internal/order.d.ts.map +1 -1
  91. package/dist/internal/order.js +4 -2
  92. package/dist/internal/order.js.map +1 -1
  93. package/dist/internal/queries.d.ts.map +1 -1
  94. package/dist/internal/queries.js +57 -26
  95. package/dist/internal/queries.js.map +1 -1
  96. package/dist/internal/relations.d.ts +10 -0
  97. package/dist/internal/relations.d.ts.map +1 -0
  98. package/dist/internal/relations.js +32 -0
  99. package/dist/internal/relations.js.map +1 -0
  100. package/dist/internal/requirements.d.ts +26 -0
  101. package/dist/internal/requirements.d.ts.map +1 -0
  102. package/dist/internal/requirements.js +46 -0
  103. package/dist/internal/requirements.js.map +1 -0
  104. package/dist/internal/streams.d.ts +2 -0
  105. package/dist/internal/streams.d.ts.map +1 -1
  106. package/dist/internal/streams.js +10 -0
  107. package/dist/internal/streams.js.map +1 -1
  108. package/dist/internal/world.d.ts +7 -1
  109. package/dist/internal/world.d.ts.map +1 -1
  110. package/dist/internal/world.js +53 -3
  111. package/dist/internal/world.js.map +1 -1
  112. package/package.json +1 -1
  113. package/src/Clock.ts +29 -4
  114. package/src/Command.ts +281 -67
  115. package/src/Condition.ts +17 -1
  116. package/src/Debug.ts +124 -15
  117. package/src/Descriptor.ts +92 -21
  118. package/src/Entity.ts +23 -7
  119. package/src/EntityScope.ts +21 -2
  120. package/src/FixedLoop.ts +218 -0
  121. package/src/HostClock.ts +2 -1
  122. package/src/Machine.ts +72 -35
  123. package/src/Query.ts +118 -17
  124. package/src/Relation.ts +5 -67
  125. package/src/Runtime.ts +659 -85
  126. package/src/Schedule.ts +31 -19
  127. package/src/Schema.ts +297 -57
  128. package/src/Snapshot.ts +94 -2
  129. package/src/System.ts +151 -29
  130. package/src/index.ts +4 -0
  131. package/src/internal/cells.ts +42 -10
  132. package/src/internal/debug.ts +179 -14
  133. package/src/internal/equal.ts +31 -0
  134. package/src/internal/fragments.ts +35 -5
  135. package/src/internal/game.ts +69 -7
  136. package/src/internal/order.ts +8 -4
  137. package/src/internal/queries.ts +62 -12
  138. package/src/internal/relations.ts +52 -0
  139. package/src/internal/requirements.ts +71 -0
  140. package/src/internal/streams.ts +11 -0
  141. package/src/internal/world.ts +60 -4
package/src/Debug.ts CHANGED
@@ -35,7 +35,7 @@
35
35
  *
36
36
  * @example
37
37
  * ```ts
38
- * const runtime = Game.Runtime.make({ services, resources, debug: { hostClock: HostClock.standard } })
38
+ * const runtime = Game.Runtime.make({ services, resources, debug: { hostClock: HostClock.standard } }).value
39
39
  * runtime.debug.nameSchedules({ setup: setupSchedule, update: updateSchedule })
40
40
  *
41
41
  * const stop = runtime.debug.observe((event) => {
@@ -116,6 +116,10 @@ export type Storage = "constructed" | "transient" | "plain"
116
116
  export interface ComponentDescription {
117
117
  readonly name: string
118
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>
119
123
  }
120
124
 
121
125
  export interface ResourceDescription {
@@ -136,8 +140,22 @@ export interface RelationDescription {
136
140
  export interface MachineDescription {
137
141
  readonly name: string
138
142
  readonly states: ReadonlyArray<Machine.StateValue>
139
- /** 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
+ */
140
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>
141
159
  }
142
160
 
143
161
  export interface ServiceDescription {
@@ -155,6 +173,8 @@ export interface QueryDescription {
155
173
  readonly without: ReadonlyArray<string>
156
174
  readonly added: ReadonlyArray<string>
157
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> }>
158
178
  /** Relation filters and reads, for example `with ChildOf` or `read Children`. */
159
179
  readonly relations: ReadonlyArray<string>
160
180
  }
@@ -165,7 +185,8 @@ export interface SystemDescription {
165
185
  /** Where the system runs, as `schedule#step`. */
166
186
  readonly placements: ReadonlyArray<string>
167
187
  readonly queries: ReadonlyArray<QueryDescription>
168
- 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> }
169
190
  readonly events: { readonly reads: ReadonlyArray<string>; readonly writes: ReadonlyArray<string> }
170
191
  readonly machines: {
171
192
  readonly reads: ReadonlyArray<string>
@@ -185,7 +206,11 @@ export interface SystemDescription {
185
206
  * names for despawnsScope, relation names for relates.
186
207
  */
187
208
  readonly commands: CommandPermissions
188
- /** Run conditions, rendered, for example `stateChanged(Game/Flow)`. */
209
+ /**
210
+ * Run conditions, rendered, for example `stateChanged(Game/Flow)`. A system
211
+ * placed under several `Schedule.when` / `pauseWhen` groups is described
212
+ * once, with the conditions of every placement.
213
+ */
189
214
  readonly when: ReadonlyArray<string>
190
215
  /** Pause conditions, rendered: while one fails the system waits and holds its stream entries. */
191
216
  readonly pauseWhen: ReadonlyArray<string>
@@ -217,11 +242,19 @@ export interface ScheduleDescription {
217
242
  * A finding about the described schedules. `warning` is almost certainly a
218
243
  * bug; `info` is worth a look but can be intentional.
219
244
  *
220
- * Most lints read declarations only. Two also use what the runtime has seen
221
- * so far: events sent with `runtime.send(...)` count as written, and
245
+ * Most lints read declarations only. Some also use what the runtime has seen
246
+ * so far: events sent with `runtime.send(...)` count as written,
222
247
  * `relation-failure-never-read` fires for a relation that applied commands
223
248
  * have tried to relate or reorder (which can fail) while no described system
224
- * reads its failures, so describe after the game has run for a while.
249
+ * reads its failures, and `requirement-violated` lists commands skipped
250
+ * because they would have broken a `Schema.requires` requirement,
251
+ * `next-state-replaced` lists next states one system queued and another
252
+ * replaced with a different value before the marker (the last wins),
253
+ * `stale-target` lists `despawn` / `remove` commands given a `SpawnedId`
254
+ * kept from an earlier run (skipped), `stream-loss-unread` lists stream
255
+ * readers that never check `lagged()` and lost entries (or are paused), so
256
+ * describe after the game has run for a while. `scope-trigger-never-applied`
257
+ * also reads which machines the runtime provides.
225
258
  */
226
259
  export interface Lint {
227
260
  readonly severity: "warning" | "info"
@@ -230,10 +263,16 @@ export interface Lint {
230
263
  | "relation-failure-never-read"
231
264
  | "event-never-written"
232
265
  | "next-state-never-applied"
266
+ | "resource-changed-never-written"
233
267
  | "component-never-read"
234
268
  | "read-before-write"
235
269
  | "this-tick-order"
236
270
  | "command-permission-unused"
271
+ | "requirement-violated"
272
+ | "next-state-replaced"
273
+ | "stale-target"
274
+ | "stream-loss-unread"
275
+ | "scope-trigger-never-applied"
237
276
  readonly message: string
238
277
  readonly subject: string
239
278
  /** What to change to resolve it (or, for `info`, when it is fine as it is). */
@@ -254,6 +293,8 @@ export interface Description {
254
293
  readonly events: ReadonlyArray<{ readonly name: string }>
255
294
  readonly relations: ReadonlyArray<RelationDescription>
256
295
  readonly machines: ReadonlyArray<MachineDescription>
296
+ /** Scopes defined through the game, in definition order. */
297
+ readonly scopes: ReadonlyArray<ScopeDescription>
257
298
  readonly services: ReadonlyArray<ServiceDescription>
258
299
  readonly schedules: ReadonlyArray<ScheduleDescription>
259
300
  readonly systems: ReadonlyArray<SystemDescription>
@@ -393,6 +434,78 @@ export type Effect =
393
434
  | { readonly kind: "relate"; readonly entity: number; readonly relation: string; readonly target: number }
394
435
  | { readonly kind: "unrelate"; readonly entity: number; readonly relation: string }
395
436
  | { readonly kind: "relationFailure"; readonly entity: number; readonly relation: string; readonly error: string }
437
+ /** A command skipped because it would leave `component` without `missing` (`Schema.requires`). */
438
+ | {
439
+ readonly kind: "requirementViolated"
440
+ readonly entity: number
441
+ readonly command: RequirementViolation["command"]
442
+ readonly component: string
443
+ readonly missing: string
444
+ }
445
+ /** A `despawn` / `remove` skipped because it was given a `SpawnedId` from an earlier run. */
446
+ | { readonly kind: "staleTarget"; readonly entity: number; readonly command: StaleTarget["command"] }
447
+
448
+ /**
449
+ * A next state one system queued and another replaced with a different value
450
+ * before the same `applyStateTransitions(...)`: the `next-state-replaced`
451
+ * lint. Only the last value is committed.
452
+ */
453
+ export interface NextStateReplacement {
454
+ readonly machine: string
455
+ readonly replaced: { readonly system: string; readonly value: Machine.StateValue }
456
+ readonly by: { readonly system: string; readonly value: Machine.StateValue }
457
+ /** How many times it happened. */
458
+ readonly count: number
459
+ }
460
+
461
+ /**
462
+ * Commands of one system skipped because applying them would have left a
463
+ * component without one it requires (`Schema.requires`): the
464
+ * `requirement-violated` lint.
465
+ */
466
+ export interface RequirementViolation {
467
+ readonly system: string
468
+ readonly command: "spawn" | "insert" | "remove"
469
+ /** The requirer. */
470
+ readonly component: string
471
+ /** The required component it would have lacked. */
472
+ readonly missing: string
473
+ /** The last entity it happened to. */
474
+ readonly entity: number
475
+ /** How many commands were skipped this way. */
476
+ readonly count: number
477
+ }
478
+
479
+ /**
480
+ * Commands of one system skipped because they were given a `SpawnedId` kept
481
+ * from an earlier run, whose entity may since have been despawned and its id
482
+ * reused: the `stale-target` lint.
483
+ */
484
+ export interface StaleTarget {
485
+ readonly system: string
486
+ readonly command: "despawn" | "remove"
487
+ /** The last entity id it happened to. */
488
+ readonly entity: number
489
+ /** How many commands were skipped this way. */
490
+ readonly count: number
491
+ }
492
+
493
+ /**
494
+ * One system's reads of one stream (events, transition events, removed,
495
+ * despawned, relation failures) on a debug runtime: the
496
+ * `stream-loss-unread` lint.
497
+ */
498
+ export interface StreamLoss {
499
+ readonly system: string
500
+ /** The stream, as `events Hit`, `removed Sprite`, `despawned`, ... */
501
+ readonly stream: string
502
+ /** Runs that read it after entries were dropped before they were read. */
503
+ readonly lostRuns: number
504
+ /** Whether the system called `lagged()` on it. */
505
+ readonly checksLagged: boolean
506
+ /** Whether the system has `pauseWhen` conditions (a long pause can drop entries). */
507
+ readonly paused: boolean
508
+ }
396
509
 
397
510
  export interface AppliedCommand {
398
511
  readonly tag: string
@@ -486,8 +599,10 @@ export interface TransitionEvent {
486
599
  readonly tick: number
487
600
  readonly schedule: string
488
601
  readonly machine: string
489
- readonly from: Machine.StateValue
490
- readonly to: Machine.StateValue
602
+ /** `undefined` when a sub-state starts existing (its parent entered its state). */
603
+ readonly from: Machine.StateValue | undefined
604
+ /** `undefined` when a sub-state stops existing (its parent left its state). */
605
+ readonly to: Machine.StateValue | undefined
491
606
  /**
492
607
  * `applied` committed the new value; `unchanged` was a `setIfChanged` to the
493
608
  * current value; `failed` stopped in an exit or transition schedule and was
@@ -572,9 +687,3 @@ export interface Option {
572
687
  export interface Member<S extends Schema.Any, Root> {
573
688
  readonly debug: Handle<S, Root>
574
689
  }
575
-
576
- /** Resolves the extra runtime member for a `debug` option type. */
577
- export type MemberFor<Enabled, S extends Schema.Any, Root> =
578
- [Enabled] extends [true] ? Member<S, Root>
579
- : [Enabled] extends [false | undefined] ? {}
580
- : Partial<Member<S, Root>>
package/src/Descriptor.ts CHANGED
@@ -166,6 +166,44 @@ export interface TransientDescriptor<
166
166
  readonly transient: true
167
167
  }
168
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
+
169
207
  /**
170
208
  * Type-level helpers for working with descriptors.
171
209
  */
@@ -242,6 +280,22 @@ const makeConstructedDescriptor = <
242
280
  [descriptorConstruction]: constructor
243
281
  }) as ConstructedDescriptor<Kind, Name, Value, Raw, Error>
244
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
+
245
299
  /**
246
300
  * Defines a component descriptor.
247
301
  *
@@ -251,14 +305,22 @@ const makeConstructedDescriptor = <
251
305
  * Components are the only descriptor kind that can be queried directly with
252
306
  * `Game.Query.read(...)`, `write(...)`, or `optional(...)`.
253
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
+ *
254
312
  * @example
255
313
  * ```ts
256
314
  * const Position = Descriptor.Component<{ x: number; y: number }>()("Position")
315
+ * // What an entity is, replaced with commands.insert and never written by a query:
316
+ * const EnemyKind = Descriptor.Component<"grunt" | "boss">()("EnemyKind", { immutable: true })
257
317
  * ```
258
318
  */
259
- export const Component = <Value>() => <const Name extends string>(
260
- name: Name
261
- ): Descriptor<"component", Name, Value> => makeDescriptor("component", name)
319
+ export const Component = <Value>(): CheckedValue<"component", Value, <const Name extends string, const Options extends ComponentOptions = {}>(
320
+ name: Name,
321
+ options?: Options
322
+ ) => WithComponentOptions<Descriptor<"component", Name, Value>, Options>> =>
323
+ ((name: string, options?: ComponentOptions) => applyComponentOptions(makeDescriptor("component", name), options)) as never
262
324
 
263
325
  /**
264
326
  * Defines a component descriptor that also knows how to validate raw values.
@@ -275,10 +337,11 @@ export const Component = <Value>() => <const Name extends string>(
275
337
  */
276
338
  export const ConstructedComponent = <Value, Raw, Error, Constructor extends {} = {}>(
277
339
  constructor: ResultConstructor<Value, Raw, Error> & Constructor
278
- ) => <const Name extends string>(
279
- name: Name
280
- ): ConstructedFor<"component", Name, Value, Raw, Error, Constructor> =>
281
- makeConstructedDescriptor("component", name, constructor) as ConstructedFor<"component", Name, Value, Raw, Error, Constructor>
340
+ ) => <const Name extends string, const Options extends ComponentOptions = {}>(
341
+ name: Name,
342
+ options?: Options
343
+ ): WithComponentOptions<ConstructedFor<"component", Name, Value, Raw, Error, Constructor>, Options> =>
344
+ applyComponentOptions(makeConstructedDescriptor("component", name, constructor), options) as WithComponentOptions<ConstructedFor<"component", Name, Value, Raw, Error, Constructor>, Options>
282
345
 
283
346
  /**
284
347
  * Defines a component descriptor that snapshots skip.
@@ -291,9 +354,11 @@ export const ConstructedComponent = <Value, Raw, Error, Constructor extends {} =
291
354
  * const SpriteRef = Descriptor.TransientComponent<{ frame: number }>()("SpriteRef")
292
355
  * ```
293
356
  */
294
- export const TransientComponent = <Value>() => <const Name extends string>(
295
- name: Name
296
- ): TransientDescriptor<"component", Name, Value> => makeTransientDescriptor("component", name)
357
+ export const TransientComponent = <Value>(): CheckedValue<"component", Value, <const Name extends string, const Options extends ComponentOptions = {}>(
358
+ name: Name,
359
+ options?: Options
360
+ ) => WithComponentOptions<TransientDescriptor<"component", Name, Value>, Options>> =>
361
+ ((name: string, options?: ComponentOptions) => applyComponentOptions(makeTransientDescriptor("component", name), options)) as never
297
362
 
298
363
  /**
299
364
  * Defines a marker component: no data, only presence (`[Player, {}]`).
@@ -401,9 +466,9 @@ export const isEntityState = (
401
466
  * const Score = Descriptor.Resource<number>()("Score")
402
467
  * ```
403
468
  */
404
- export const Resource = <Value>() => <const Name extends string>(
469
+ export const Resource = <Value>(): CheckedValue<"resource", Value, <const Name extends string>(
405
470
  name: Name
406
- ): Descriptor<"resource", Name, Value> => makeDescriptor("resource", name)
471
+ ) => Descriptor<"resource", Name, Value>> => ((name: string) => makeDescriptor("resource", name)) as never
407
472
 
408
473
  /**
409
474
  * Defines a resource descriptor that also knows how to validate raw values.
@@ -424,9 +489,9 @@ export const ConstructedResource = <Value, Raw, Error, Constructor extends {} =
424
489
  * const DeltaTime = Descriptor.TransientResource<number>()("DeltaTime")
425
490
  * ```
426
491
  */
427
- export const TransientResource = <Value>() => <const Name extends string>(
492
+ export const TransientResource = <Value>(): CheckedValue<"resource", Value, <const Name extends string>(
428
493
  name: Name
429
- ): TransientDescriptor<"resource", Name, Value> => makeTransientDescriptor("resource", name)
494
+ ) => TransientDescriptor<"resource", Name, Value>> => ((name: string) => makeTransientDescriptor("resource", name)) as never
430
495
 
431
496
  /**
432
497
  * Defines an event descriptor.
@@ -446,9 +511,9 @@ export const TransientResource = <Value>() => <const Name extends string>(
446
511
  * const Hit = Descriptor.Event<{ target: number; amount: number }>()("Hit")
447
512
  * ```
448
513
  */
449
- export const Event = <Value>() => <const Name extends string>(
514
+ export const Event = <Value>(): CheckedValue<"event", Value, <const Name extends string>(
450
515
  name: Name
451
- ): Descriptor<"event", Name, Value> => makeDescriptor("event", name)
516
+ ) => Descriptor<"event", Name, Value>> => ((name: string) => makeDescriptor("event", name)) as never
452
517
 
453
518
  /**
454
519
  * Defines a service descriptor.
@@ -467,9 +532,9 @@ export const Event = <Value>() => <const Name extends string>(
467
532
  * const Logger = Descriptor.Service<{ log: (message: string) => void }>()("Logger")
468
533
  * ```
469
534
  */
470
- export const Service = <Value>() => <const Name extends string>(
535
+ export const Service = <Value>(): CheckedValue<"service", Value, <const Name extends string>(
471
536
  name: Name
472
- ): Descriptor<"service", Name, Value> => makeDescriptor("service", name)
537
+ ) => Descriptor<"service", Name, Value>> => ((name: string) => makeDescriptor("service", name)) as never
473
538
 
474
539
  /**
475
540
  * Checks whether one descriptor is transient (skipped by snapshots).
@@ -580,7 +645,10 @@ export function constructorOf<D extends Descriptor.Any>(
580
645
  * `Game.EntityScope(...)` instead when entities only share a lifetime (a
581
646
  * level, a menu) and need no tree, order, or traversal.
582
647
  */
583
- 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)
584
652
 
585
653
  /**
586
654
  * Defines a general relationship pair with direct edges and reverse lookups.
@@ -589,7 +657,10 @@ export const Hierarchy = RelationModule.Hierarchy
589
657
  * reverse lookup, but not hierarchy-only behavior such as tree traversal or
590
658
  * child reordering.
591
659
  */
592
- 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)
593
664
  import * as DecodeModule from "./Decode.ts"
594
- import * as RelationModule from "./Relation.ts"
665
+ import { makePair } from "./internal/relations.ts"
595
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,17 +173,25 @@ 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> {
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
  */
175
180
  readonly kind: "EntityRef"
176
181
  /**
177
182
  * The component descriptors the query that produced this ref proves (its
178
- * required slots and `with`): what `commands.despawn` checks against the
179
- * system's `despawns` permission. Type-level only.
183
+ * required slots and `with`): what `commands.despawn`, `insert`, and
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.
187
+ */
188
+ readonly __proves?: (_: Proves) => void
189
+ /**
190
+ * The component descriptors the producing query proves absent (its
191
+ * `without`): what `commands.remove` checks against `Schema.requires`.
192
+ * Type-level only.
180
193
  */
181
- readonly __proves?: (_: never) => Proves
194
+ readonly __lacks?: (_: Lacks) => void
182
195
  /**
183
196
  * The schema-bound entity identity.
184
197
  */
@@ -201,14 +214,17 @@ export interface EntityMut<
201
214
  out P extends ComponentProof,
202
215
  out W extends ComponentProof,
203
216
  Root = unknown,
204
- out Proves = unknown
217
+ in Proves = never,
218
+ in Lacks = never
205
219
  > {
206
220
  /**
207
221
  * Runtime tag for debugging and pattern matching.
208
222
  */
209
223
  readonly kind: "EntityMut"
210
224
  /** The component descriptors the producing query proves; see `EntityRef`. */
211
- readonly __proves?: (_: never) => Proves
225
+ readonly __proves?: (_: Proves) => void
226
+ /** The component descriptors the producing query proves absent; see `EntityRef`. */
227
+ readonly __lacks?: (_: Lacks) => void
212
228
  /**
213
229
  * The schema-bound entity identity.
214
230
  */
@@ -13,6 +13,11 @@
13
13
  * parent-child tree: ordered children, `lookup.parent` / `descendants`
14
14
  * traversal, and children despawned with their parent.
15
15
  *
16
+ * A scope whose lifetime is a game state declares it:
17
+ * `Game.EntityScope(name, { despawnOnExit: [Flow, "Playing"] })` despawns
18
+ * every member when `applyStateTransitions(...)` commits that state change,
19
+ * with no system, permission, or marker to place.
20
+ *
16
21
  * @module EntityScope
17
22
  * @docGroup core
18
23
  */
@@ -25,9 +30,21 @@ export interface EntityScope<
25
30
  readonly kind: "entityScope"
26
31
  readonly name: Name
27
32
  readonly key: symbol
33
+ /** State changes that despawn every member, applied by `applyStateTransitions(...)`. */
34
+ readonly despawnOn: ReadonlyArray<Trigger>
28
35
  readonly __schemaRoot?: Root | undefined
29
36
  }
30
37
 
38
+ /**
39
+ * One state change that despawns a scope: leaving (`exit`) or entering
40
+ * (`enter`) `state` of `machine`.
41
+ */
42
+ export interface Trigger {
43
+ readonly phase: "enter" | "exit"
44
+ readonly machine: { readonly key: symbol; readonly name: string }
45
+ readonly state: string | number
46
+ }
47
+
31
48
  /** Type-level helpers for entity scopes. */
32
49
  export namespace EntityScope {
33
50
  export type Any = EntityScope<string, any>
@@ -42,11 +59,13 @@ export namespace EntityScope {
42
59
  * branded to that game's root.
43
60
  */
44
61
  export const make = <const Name extends string, Root = unknown>(
45
- name: Name
62
+ name: Name,
63
+ despawnOn: ReadonlyArray<Trigger> = []
46
64
  ): EntityScope<Name, Root> => ({
47
65
  kind: "entityScope",
48
66
  name,
49
- key: keyOf(name)
67
+ key: keyOf(name),
68
+ despawnOn
50
69
  })
51
70
 
52
71
  const keyPrefix = "ritmo/entityScope/"