@typeonce/ritmo 0.7.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 (87) hide show
  1. package/dist/Command.d.ts +102 -17
  2. package/dist/Command.d.ts.map +1 -1
  3. package/dist/Command.js +10 -8
  4. package/dist/Command.js.map +1 -1
  5. package/dist/Condition.d.ts +12 -1
  6. package/dist/Condition.d.ts.map +1 -1
  7. package/dist/Condition.js.map +1 -1
  8. package/dist/Debug.d.ts +63 -7
  9. package/dist/Debug.d.ts.map +1 -1
  10. package/dist/Descriptor.d.ts +36 -3
  11. package/dist/Descriptor.d.ts.map +1 -1
  12. package/dist/Descriptor.js +11 -3
  13. package/dist/Descriptor.js.map +1 -1
  14. package/dist/Entity.d.ts +12 -4
  15. package/dist/Entity.d.ts.map +1 -1
  16. package/dist/Entity.js.map +1 -1
  17. package/dist/EntityScope.d.ts +20 -1
  18. package/dist/EntityScope.d.ts.map +1 -1
  19. package/dist/EntityScope.js +8 -2
  20. package/dist/EntityScope.js.map +1 -1
  21. package/dist/Machine.d.ts +55 -33
  22. package/dist/Machine.d.ts.map +1 -1
  23. package/dist/Machine.js +6 -2
  24. package/dist/Machine.js.map +1 -1
  25. package/dist/Query.d.ts +48 -4
  26. package/dist/Query.d.ts.map +1 -1
  27. package/dist/Query.js +32 -4
  28. package/dist/Query.js.map +1 -1
  29. package/dist/Runtime.d.ts +3 -0
  30. package/dist/Runtime.d.ts.map +1 -1
  31. package/dist/Runtime.js +330 -24
  32. package/dist/Runtime.js.map +1 -1
  33. package/dist/Schema.d.ts +141 -25
  34. package/dist/Schema.d.ts.map +1 -1
  35. package/dist/Schema.js +29 -3
  36. package/dist/Schema.js.map +1 -1
  37. package/dist/Snapshot.d.ts +14 -0
  38. package/dist/Snapshot.d.ts.map +1 -1
  39. package/dist/Snapshot.js.map +1 -1
  40. package/dist/System.d.ts +64 -7
  41. package/dist/System.d.ts.map +1 -1
  42. package/dist/System.js +32 -0
  43. package/dist/System.js.map +1 -1
  44. package/dist/internal/debug.d.ts +7 -0
  45. package/dist/internal/debug.d.ts.map +1 -1
  46. package/dist/internal/debug.js +97 -6
  47. package/dist/internal/debug.js.map +1 -1
  48. package/dist/internal/equal.d.ts +8 -0
  49. package/dist/internal/equal.d.ts.map +1 -0
  50. package/dist/internal/equal.js +38 -0
  51. package/dist/internal/equal.js.map +1 -0
  52. package/dist/internal/fragments.d.ts.map +1 -1
  53. package/dist/internal/fragments.js +11 -1
  54. package/dist/internal/fragments.js.map +1 -1
  55. package/dist/internal/game.d.ts.map +1 -1
  56. package/dist/internal/game.js +33 -5
  57. package/dist/internal/game.js.map +1 -1
  58. package/dist/internal/order.d.ts.map +1 -1
  59. package/dist/internal/order.js +4 -2
  60. package/dist/internal/order.js.map +1 -1
  61. package/dist/internal/queries.d.ts.map +1 -1
  62. package/dist/internal/queries.js +30 -8
  63. package/dist/internal/queries.js.map +1 -1
  64. package/dist/internal/requirements.d.ts +26 -0
  65. package/dist/internal/requirements.d.ts.map +1 -0
  66. package/dist/internal/requirements.js +46 -0
  67. package/dist/internal/requirements.js.map +1 -0
  68. package/package.json +1 -1
  69. package/src/Command.ts +149 -29
  70. package/src/Condition.ts +17 -1
  71. package/src/Debug.ts +64 -7
  72. package/src/Descriptor.ts +55 -10
  73. package/src/Entity.ts +13 -4
  74. package/src/EntityScope.ts +21 -2
  75. package/src/Machine.ts +68 -34
  76. package/src/Query.ts +71 -7
  77. package/src/Runtime.ts +336 -22
  78. package/src/Schema.ts +185 -28
  79. package/src/Snapshot.ts +4 -0
  80. package/src/System.ts +75 -6
  81. package/src/internal/debug.ts +104 -8
  82. package/src/internal/equal.ts +31 -0
  83. package/src/internal/fragments.ts +12 -2
  84. package/src/internal/game.ts +51 -5
  85. package/src/internal/order.ts +8 -4
  86. package/src/internal/queries.ts +34 -10
  87. package/src/internal/requirements.ts +71 -0
package/src/Debug.ts CHANGED
@@ -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>
@@ -217,11 +238,14 @@ export interface ScheduleDescription {
217
238
  * A finding about the described schedules. `warning` is almost certainly a
218
239
  * bug; `info` is worth a look but can be intentional.
219
240
  *
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
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,
222
243
  * `relation-failure-never-read` fires for a relation that applied commands
223
244
  * 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.
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.
225
249
  */
226
250
  export interface Lint {
227
251
  readonly severity: "warning" | "info"
@@ -230,10 +254,13 @@ export interface Lint {
230
254
  | "relation-failure-never-read"
231
255
  | "event-never-written"
232
256
  | "next-state-never-applied"
257
+ | "resource-changed-never-written"
233
258
  | "component-never-read"
234
259
  | "read-before-write"
235
260
  | "this-tick-order"
236
261
  | "command-permission-unused"
262
+ | "requirement-violated"
263
+ | "scope-trigger-never-applied"
237
264
  readonly message: string
238
265
  readonly subject: string
239
266
  /** What to change to resolve it (or, for `info`, when it is fine as it is). */
@@ -254,6 +281,8 @@ export interface Description {
254
281
  readonly events: ReadonlyArray<{ readonly name: string }>
255
282
  readonly relations: ReadonlyArray<RelationDescription>
256
283
  readonly machines: ReadonlyArray<MachineDescription>
284
+ /** Scopes defined through the game, in definition order. */
285
+ readonly scopes: ReadonlyArray<ScopeDescription>
257
286
  readonly services: ReadonlyArray<ServiceDescription>
258
287
  readonly schedules: ReadonlyArray<ScheduleDescription>
259
288
  readonly systems: ReadonlyArray<SystemDescription>
@@ -393,6 +422,32 @@ export type Effect =
393
422
  | { readonly kind: "relate"; readonly entity: number; readonly relation: string; readonly target: number }
394
423
  | { readonly kind: "unrelate"; readonly entity: number; readonly relation: string }
395
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
+ }
396
451
 
397
452
  export interface AppliedCommand {
398
453
  readonly tag: string
@@ -486,8 +541,10 @@ export interface TransitionEvent {
486
541
  readonly tick: number
487
542
  readonly schedule: string
488
543
  readonly machine: string
489
- readonly from: Machine.StateValue
490
- 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
491
548
  /**
492
549
  * `applied` committed the new value; `unchanged` was a `setIfChanged` to the
493
550
  * current value; `failed` stopped in an exit or transition schedule and was
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
  */
@@ -254,11 +292,15 @@ const makeConstructedDescriptor = <
254
292
  * @example
255
293
  * ```ts
256
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 })
257
297
  * ```
258
298
  */
259
- export const Component = <Value>() => <const Name extends string>(
260
- name: Name
261
- ): 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>
262
304
 
263
305
  /**
264
306
  * Defines a component descriptor that also knows how to validate raw values.
@@ -275,10 +317,11 @@ export const Component = <Value>() => <const Name extends string>(
275
317
  */
276
318
  export const ConstructedComponent = <Value, Raw, Error, Constructor extends {} = {}>(
277
319
  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>
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>
282
325
 
283
326
  /**
284
327
  * Defines a component descriptor that snapshots skip.
@@ -291,9 +334,11 @@ export const ConstructedComponent = <Value, Raw, Error, Constructor extends {} =
291
334
  * const SpriteRef = Descriptor.TransientComponent<{ frame: number }>()("SpriteRef")
292
335
  * ```
293
336
  */
294
- export const TransientComponent = <Value>() => <const Name extends string>(
295
- name: Name
296
- ): 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>
297
342
 
298
343
  /**
299
344
  * Defines a marker component: no data, only presence (`[Player, {}]`).
package/src/Entity.ts CHANGED
@@ -168,17 +168,23 @@ export type StagedComponent = readonly [Descriptor<"component", string, any>, un
168
168
  *
169
169
  * Query execution is the main source of `EntityRef` values.
170
170
  */
171
- export interface EntityRef<S extends Schema.Any, out P extends ComponentProof, Root = unknown, out Proves = unknown> {
171
+ export interface EntityRef<S extends Schema.Any, out P extends ComponentProof, Root = unknown, out Proves = unknown, out Lacks = never> {
172
172
  /**
173
173
  * Runtime tag for debugging and pattern matching.
174
174
  */
175
175
  readonly kind: "EntityRef"
176
176
  /**
177
177
  * 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.
178
+ * required slots and `with`): what `commands.despawn`, `insert`, and
179
+ * `remove` check. Type-level only.
180
180
  */
181
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
182
188
  /**
183
189
  * The schema-bound entity identity.
184
190
  */
@@ -201,7 +207,8 @@ export interface EntityMut<
201
207
  out P extends ComponentProof,
202
208
  out W extends ComponentProof,
203
209
  Root = unknown,
204
- out Proves = unknown
210
+ out Proves = unknown,
211
+ out Lacks = never
205
212
  > {
206
213
  /**
207
214
  * Runtime tag for debugging and pattern matching.
@@ -209,6 +216,8 @@ export interface EntityMut<
209
216
  readonly kind: "EntityMut"
210
217
  /** The component descriptors the producing query proves; see `EntityRef`. */
211
218
  readonly __proves?: (_: never) => Proves
219
+ /** The component descriptors the producing query proves absent; see `EntityRef`. */
220
+ readonly __lacks?: (_: never) => Lacks
212
221
  /**
213
222
  * The schema-bound entity identity.
214
223
  */
@@ -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/"
package/src/Machine.ts CHANGED
@@ -39,6 +39,7 @@
39
39
  */
40
40
  import type { AnyScheduleOrder, Schedule } from "./Schedule.ts"
41
41
  import * as Requirement from "./Requirement.ts"
42
+ import type * as Result from "./Result.ts"
42
43
  import type { Schema } from "./Schema.ts"
43
44
  import type { SystemFailure } from "./System.ts"
44
45
 
@@ -56,60 +57,71 @@ export type StateValue = string | number
56
57
  export interface StateMachineDefinition<
57
58
  out Name extends string = string,
58
59
  out Values extends readonly [StateValue, ...StateValue[]] = readonly [StateValue, ...StateValue[]],
59
- out Root = unknown
60
+ out Root = unknown,
61
+ out Parent = unknown
60
62
  > {
61
63
  readonly kind: "stateMachine"
62
64
  readonly name: Name
63
65
  readonly key: symbol
64
66
  readonly values: Values
67
+ /** For a sub-state: the machine and the state it exists in. */
68
+ readonly within?: SubStateParent | undefined
65
69
  readonly __schemaRoot?: Root | undefined
70
+ /** The machine a sub-state exists within; `unknown` for a top-level machine. */
71
+ readonly __parent?: (_: never) => Parent
72
+ }
73
+
74
+ /** Where a sub-state exists: while `machine` is in `state`. */
75
+ export interface SubStateParent {
76
+ readonly machine: StateMachine.Any
77
+ readonly state: StateValue
66
78
  }
67
79
 
68
80
  /**
69
- * The runtime-visible transition payload for one machine.
81
+ * Whether `M` is a sub-state (declared with `within`): it has no value while
82
+ * its parent machine is in another state.
70
83
  */
71
- export interface TransitionSnapshot<M extends StateMachineDefinition = StateMachineDefinition> {
72
- readonly from: StateMachine.Value<M>
73
- readonly to: StateMachine.Value<M>
74
- }
84
+ export type IsSubState<M> =
85
+ M extends { readonly __parent?: (_: never) => infer Parent } ? unknown extends Parent ? false : true : false
75
86
 
76
87
  /**
77
- * A typed read-only view over the current committed state.
88
+ * The committed value of `M`. A sub-state has none (`undefined`) while its
89
+ * parent machine is not in the state it exists in; a top-level machine
90
+ * always has one.
78
91
  */
79
- export interface MachineView<M extends StateMachineDefinition = StateMachineDefinition> {
80
- get(): StateMachine.Value<M>
81
- is(value: StateMachine.Value<M>): boolean
82
- }
92
+ export type CurrentValue<M extends StateMachine.Any> =
93
+ IsSubState<M> extends true ? StateMachine.Value<M> | undefined : StateMachine.Value<M>
83
94
 
84
95
  /**
85
- * A typed queued-write view over the next state.
96
+ * Why queuing a sub-state's next value failed: its parent machine is not in
97
+ * the state the sub-state exists in, so there is nothing to change.
86
98
  */
87
- export interface NextMachineView<M extends StateMachineDefinition = StateMachineDefinition> {
88
- getPending(): StateMachine.Value<M> | undefined
89
- set(value: StateMachine.Value<M>): void
90
- setIfChanged(value: StateMachine.Value<M>): void
91
- reset(): void
99
+ export interface SubStateInactive {
100
+ readonly _tag: "SubStateInactive"
101
+ /** The sub-state's name. */
102
+ readonly machine: string
103
+ /** The parent machine's name. */
104
+ readonly parent: string
105
+ /** The parent state the sub-state exists in. */
106
+ readonly requires: StateValue
92
107
  }
93
108
 
94
109
  /**
95
- * A typed read-only view over the last applied transition.
96
- *
97
- * This view is only meaningful inside transition schedules and is therefore
98
- * only exposed to systems that explicitly declare transition access.
110
+ * What `nextMachines.x.set(...)` returns: nothing for a top-level machine,
111
+ * and for a sub-state a `Result` that fails with `SubStateInactive` when its
112
+ * parent is in another state.
99
113
  */
100
- export interface TransitionView<M extends StateMachineDefinition = StateMachineDefinition> {
101
- get(): TransitionSnapshot<M>
102
- }
114
+ export type SetResult<M extends StateMachine.Any> =
115
+ IsSubState<M> extends true ? Result.Result<void, SubStateInactive> : void
103
116
 
104
117
  /**
105
- * A typed read-only stream of committed machine transition events.
106
- *
107
- * These snapshots are published by `applyStateTransitions(...)` when a
108
- * transition commits. Like events, each reader sees the transitions published
109
- * since its own previous run.
118
+ * The runtime-visible transition payload for one machine. For a sub-state,
119
+ * `from` is `undefined` when it starts existing (its parent entered its
120
+ * state) and `to` is `undefined` when it stops.
110
121
  */
111
- export interface TransitionEventView<M extends StateMachineDefinition = StateMachineDefinition> {
112
- all(): ReadonlyArray<TransitionSnapshot<M>>
122
+ export interface TransitionSnapshot<M extends StateMachineDefinition = StateMachineDefinition> {
123
+ readonly from: CurrentValue<M>
124
+ readonly to: CurrentValue<M>
113
125
  }
114
126
 
115
127
  /**
@@ -263,7 +275,7 @@ export interface TransitionScheduleDefinition<
263
275
  * Type-level and value-level helpers for finite-state machines.
264
276
  */
265
277
  export namespace StateMachine {
266
- export type Any = StateMachineDefinition<string, readonly [StateValue, ...StateValue[]], unknown>
278
+ export type Any = StateMachineDefinition<string, readonly [StateValue, ...StateValue[]], unknown, unknown>
267
279
  export type Value<M extends Any> = M["values"][number]
268
280
  export type Root<M extends Any> = M extends StateMachineDefinition<string, readonly [StateValue, ...StateValue[]], infer R> ? R : never
269
281
  export type AnyCondition<Root = unknown> = Condition<Root>
@@ -271,6 +283,23 @@ export namespace StateMachine {
271
283
  TransitionScheduleDefinition<S, Any, Requirement.Requirement, Root, SystemFailure>
272
284
  }
273
285
 
286
+ /**
287
+ * `unknown` when `M` was defined by the game rooted at `Root`; otherwise a
288
+ * type naming the fix, so passing another game's machine fails with that
289
+ * message.
290
+ */
291
+ export type SameGame<M, Root> =
292
+ M extends StateMachineDefinition<infer Name, readonly [StateValue, ...StateValue[]], infer MachineRoot>
293
+ ? [Root] extends [MachineRoot]
294
+ ? [MachineRoot] extends [Root]
295
+ ? unknown
296
+ : ForeignMachine<`${Name} is a state machine of another game: use a machine defined with this game's Game.StateMachine(...)`>
297
+ : ForeignMachine<`${Name} is a state machine of another game: use a machine defined with this game's Game.StateMachine(...)`>
298
+ : unknown
299
+
300
+ /** The message type `SameGame` resolves to: a type error prints `Message`. */
301
+ type ForeignMachine<Message extends string> = { readonly __foreignMachine__: Message }
302
+
274
303
  /** Extracts the machine token from one declared machine access slot. */
275
304
  type MachineFromAccess<Access> =
276
305
  Access extends { readonly machine: infer M extends StateMachine.Any } ? M : never
@@ -314,6 +343,9 @@ export type MachineNeedsFromConditions<C extends ReadonlyArray<Condition>> =
314
343
  *
315
344
  * Use `Descriptor.EntityState(...)` instead when every entity carries its own
316
345
  * state (per-entity, written immediately, no enter/exit schedules).
346
+ *
347
+ * Pass `within` for a sub-state: a machine that exists only while another
348
+ * machine is in one state (`Game.StateMachine(name, values, { within })`).
317
349
  */
318
350
  export const StateMachine = <
319
351
  const Name extends string,
@@ -321,12 +353,14 @@ export const StateMachine = <
321
353
  Root = unknown
322
354
  >(
323
355
  name: Name,
324
- values: Values
356
+ values: Values,
357
+ within?: SubStateParent
325
358
  ): StateMachineDefinition<Name, Values, Root> => ({
326
359
  kind: "stateMachine",
327
360
  name,
328
361
  key: Symbol.for(`ritmo/stateMachine/${name}`),
329
- values
362
+ values,
363
+ ...(within === undefined ? {} : { within })
330
364
  }) as StateMachineDefinition<Name, Values, Root>
331
365
 
332
366
  /**
package/src/Query.ts CHANGED
@@ -69,6 +69,7 @@
69
69
  * Query authoring helpers for selecting components and declaring explicit filters.
70
70
  */
71
71
  import type { DecodeError } from "./Decode.ts"
72
+ import * as DescriptorModule from "./Descriptor.ts"
72
73
  import type { Descriptor, EntityStateValue } from "./Descriptor.ts"
73
74
  import type { EntityId, EntityMut, EntityRef } from "./Entity.ts"
74
75
  import type * as Result from "./Result.ts"
@@ -179,10 +180,24 @@ export interface ChangedFilter<D extends ComponentDescriptor> {
179
180
  readonly descriptor: D
180
181
  }
181
182
 
182
- export type Filter<D extends ComponentDescriptor> =
183
+ /** An `added` or `changed` filter: the filters `anyOf` combines. */
184
+ export type LifecycleFilter<D extends ComponentDescriptor> =
183
185
  | AddedFilter<D>
184
186
  | ChangedFilter<D>
185
187
 
188
+ /**
189
+ * A filter that matches entities passing at least one of its `added` /
190
+ * `changed` filters since the reading system's previous run.
191
+ */
192
+ export interface AnyOfFilter<D extends ComponentDescriptor> {
193
+ readonly kind: "anyOf"
194
+ readonly filters: ReadonlyArray<LifecycleFilter<D>>
195
+ }
196
+
197
+ export type Filter<D extends ComponentDescriptor> =
198
+ | LifecycleFilter<D>
199
+ | AnyOfFilter<D>
200
+
186
201
  /**
187
202
  * Declares read-only access to a component in a query selection.
188
203
  *
@@ -203,11 +218,29 @@ export const read = <D extends ComponentDescriptor>(descriptor: D): ReadAccess<D
203
218
  * place. A write slot both requires component presence and exposes the slot as
204
219
  * a `WriteCell`, so mutation capability stays visible in the query spec rather
205
220
  * than appearing ad hoc in the loop body.
221
+ *
222
+ * Components declared `immutable: true` cannot be written: replace them with
223
+ * `commands.insert(...)` instead.
206
224
  */
207
- export const write = <D extends ComponentDescriptor>(descriptor: D): WriteAccess<D> => ({
208
- mode: "write",
209
- descriptor
210
- })
225
+ export const write = <D extends ComponentDescriptor>(descriptor: D & Writable<D>): WriteAccess<D> => {
226
+ if (DescriptorModule.isImmutable(descriptor)) {
227
+ throw new Error(immutableMessage(descriptor.name))
228
+ }
229
+ return { mode: "write", descriptor }
230
+ }
231
+
232
+ const immutableMessage = (name: string) =>
233
+ `${name} is immutable: replace it with commands.insert(entity, Game.Command.entry(${name}, ...))`
234
+
235
+ type NameOf<D> = D extends { readonly name: infer Name extends string } ? Name : never
236
+
237
+ /**
238
+ * `unknown` for a component a query may write; for one declared
239
+ * `immutable: true`, an argument error naming the fix.
240
+ */
241
+ export type Writable<D> = D extends DescriptorModule.Immutable
242
+ ? { readonly __immutable__: `${NameOf<D>} is immutable: replace it with commands.insert(entity, Game.Command.entry(${NameOf<D>}, ...))` }
243
+ : unknown
211
244
 
212
245
  /**
213
246
  * Declares maybe-present read-only access to a component in a query
@@ -282,6 +315,34 @@ export const changed = <D extends ComponentDescriptor>(descriptor: D): ChangedFi
282
315
  descriptor
283
316
  })
284
317
 
318
+ /**
319
+ * Declares a filter that matches entities passing at least one of `filters`
320
+ * (`added` / `changed`) since the reading system's previous run. Each entity
321
+ * is matched once, however many of its filters pass.
322
+ *
323
+ * Filters listed side by side in `filters: [...]` must all pass; use `anyOf`
324
+ * when any one of them is enough, for example to redraw when either the
325
+ * position or the sprite changed.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * const RedrawQuery = Game.Query({
330
+ * selection: { position: Game.Query.read(Position), sprite: Game.Query.read(Sprite) },
331
+ * filters: [Game.Query.anyOf(Game.Query.changed(Position), Game.Query.changed(Sprite))]
332
+ * })
333
+ * ```
334
+ */
335
+ export const anyOf = <
336
+ const Filters extends readonly [
337
+ LifecycleFilter<ComponentDescriptor>,
338
+ LifecycleFilter<ComponentDescriptor>,
339
+ ...ReadonlyArray<LifecycleFilter<ComponentDescriptor>>
340
+ ]
341
+ >(...filters: Filters): AnyOfFilter<Filters[number]["descriptor"]> => ({
342
+ kind: "anyOf",
343
+ filters
344
+ })
345
+
285
346
  /**
286
347
  * A fully explicit query specification.
287
348
  *
@@ -387,6 +448,9 @@ export namespace Query {
387
448
  /** The component descriptors a query proves its matches have: required selection slots and `with`. */
388
449
  export type Proven<T extends Any> = RequiredSelectionDescriptors<T> | T["with"][number]
389
450
 
451
+ /** The component descriptors a query proves its matches lack: its `without`. */
452
+ export type Excluded<T extends Any> = T["without"][number]
453
+
390
454
  /**
391
455
  * The readable proof produced by a query.
392
456
  */
@@ -621,11 +685,11 @@ export type OptionalReadCell<T> = PresentOptionalReadCell<T> | AbsentOptionalRea
621
685
  export type QueryMatch<S extends Schema.Any, Q extends Query.Any> =
622
686
  keyof Query.WriteProof<Q> extends never
623
687
  ? {
624
- readonly entity: EntityRef<S, Query.ReadProof<Q>, Query.Root<Q>, Query.Proven<Q>>
688
+ readonly entity: EntityRef<S, Query.ReadProof<Q>, Query.Root<Q>, Query.Proven<Q>, Query.Excluded<Q>>
625
689
  readonly data: Query.Cells<Q>
626
690
  }
627
691
  : {
628
- readonly entity: EntityMut<S, Query.ReadProof<Q>, Query.WriteProof<Q>, Query.Root<Q>, Query.Proven<Q>>
692
+ readonly entity: EntityMut<S, Query.ReadProof<Q>, Query.WriteProof<Q>, Query.Root<Q>, Query.Proven<Q>, Query.Excluded<Q>>
629
693
  readonly data: Query.Cells<Q>
630
694
  }
631
695