@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
@@ -7,6 +7,17 @@
7
7
  * build scenes without teaching the ECS runtime about menus, levels, overlays,
8
8
  * or any other game-specific scene model.
9
9
  *
10
+ * Use a scope when a set of entities shares one lifetime and nothing else: a
11
+ * level's walls, a menu's buttons. Membership is flat, unordered, and not
12
+ * queryable. Use `Descriptor.Hierarchy(...)` instead when entities form a
13
+ * parent-child tree: ordered children, `lookup.parent` / `descendants`
14
+ * traversal, and children despawned with their parent.
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
+ *
10
21
  * @module EntityScope
11
22
  * @docGroup core
12
23
  */
@@ -19,9 +30,21 @@ export interface EntityScope<
19
30
  readonly kind: "entityScope"
20
31
  readonly name: Name
21
32
  readonly key: symbol
33
+ /** State changes that despawn every member, applied by `applyStateTransitions(...)`. */
34
+ readonly despawnOn: ReadonlyArray<Trigger>
22
35
  readonly __schemaRoot?: Root | undefined
23
36
  }
24
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
+
25
48
  /** Type-level helpers for entity scopes. */
26
49
  export namespace EntityScope {
27
50
  export type Any = EntityScope<string, any>
@@ -36,11 +59,13 @@ export namespace EntityScope {
36
59
  * branded to that game's root.
37
60
  */
38
61
  export const make = <const Name extends string, Root = unknown>(
39
- name: Name
62
+ name: Name,
63
+ despawnOn: ReadonlyArray<Trigger> = []
40
64
  ): EntityScope<Name, Root> => ({
41
65
  kind: "entityScope",
42
66
  name,
43
- key: keyOf(name)
67
+ key: keyOf(name),
68
+ despawnOn
44
69
  })
45
70
 
46
71
  const keyPrefix = "ritmo/entityScope/"
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The host's wall clock, as an injected contract.
3
+ *
4
+ * Core and the generic devtools modules never read a platform clock
5
+ * (`performance`, `Date`) themselves: whatever measures host time (debug
6
+ * trace durations, session timings, replay windows) takes a `HostClock`.
7
+ * Platform packages provide implementations (`BrowserHostClock.make(window)`
8
+ * in the browser package); `standard` works in every JavaScript engine, and
9
+ * `test()` is a manual clock for deterministic tests.
10
+ *
11
+ * Host time is for measuring the host (how long a tick took), never for game
12
+ * logic: simulation time is a resource the schedule advances.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const runtime = Game.Runtime.make({ services, debug: { hostClock: HostClock.standard } })
17
+ * ```
18
+ *
19
+ * @module HostClock
20
+ * @docGroup runtime
21
+ */
22
+ import * as Result from "./Result.ts"
23
+
24
+ /** A host clock: `now()` in milliseconds, from an arbitrary origin. */
25
+ export interface HostClock {
26
+ readonly now: () => number
27
+ }
28
+
29
+ /**
30
+ * `Date.now`, which every JavaScript engine provides. Its readings can jump
31
+ * (the system clock is adjusted, or the device sleeps), so a duration
32
+ * computed from two readings can be negative: consumers clamp durations to
33
+ * 0. Prefer a monotonic platform clock when there is one
34
+ * (`BrowserHostClock.make(window)`).
35
+ */
36
+ export const standard: HostClock = { now: () => Date.now() }
37
+
38
+ /** `adjust` must leave a finite clock reading and never move backwards. */
39
+ export interface InvalidAdjustment {
40
+ readonly _tag: "InvalidAdjustment"
41
+ readonly ms: number
42
+ }
43
+
44
+ /** A manual clock for tests: starts at 0 and moves only through `adjust`. */
45
+ export interface TestHostClock extends HostClock {
46
+ /** Moves the clock forward by `ms` (0 or more). */
47
+ readonly adjust: (ms: number) => Result.Result<void, InvalidAdjustment>
48
+ }
49
+
50
+ /** A test clock at 0. Every reading is the same until `adjust` moves it. */
51
+ export const test = (): TestHostClock => {
52
+ let current = 0
53
+ return {
54
+ now: () => current,
55
+ adjust: (ms) => {
56
+ if (!Number.isFinite(ms) || ms < 0 || !Number.isFinite(current + ms)) return Result.failure({ _tag: "InvalidAdjustment", ms })
57
+ current += ms
58
+ return Result.success(undefined)
59
+ }
60
+ }
61
+ }
62
+
63
+ /** A duration between two readings, clamped to 0 when the clock went backwards. */
64
+ export const elapsed = (from: number, to: number): number => Math.max(0, to - from)
package/src/Inspector.ts CHANGED
@@ -6,6 +6,12 @@
6
6
  * They run on demand through `runtime.inspect(...)` and return a value without
7
7
  * advancing schedule visibility.
8
8
  *
9
+ * An inspector is the one typed read-only view of the world: tests, saves, UI,
10
+ * `Game.Condition.check(...)` projections, and devtools invariants
11
+ * (`Invariant.fromInspector(...)`) are all built from it. `runtime.inspect`
12
+ * rejects at compile time an inspector that reads a resource, service, or
13
+ * machine the runtime was not given.
14
+ *
9
15
  * @module Inspector
10
16
  * @docGroup runtime
11
17
  */
@@ -74,7 +80,7 @@ export interface InspectorDefinition<
74
80
  > {
75
81
  readonly kind: "inspector"
76
82
  readonly name: Name
77
- readonly system: System.SystemDefinition<Spec, void, never, Root, Name, Needs>
83
+ readonly system: System.SystemDefinition<Spec, never, Root, Name, Needs>
78
84
  readonly requirements: ReadonlyArray<Needs>
79
85
  readonly read: (context: InspectorContext<Spec>) => Value
80
86
  readonly __schemaRoot?: Root | undefined
@@ -111,7 +117,6 @@ export const make = <
111
117
  () => undefined
112
118
  ) as unknown as System.SystemDefinition<
113
119
  System.SystemSpec<S, Access, Root>,
114
- void,
115
120
  never,
116
121
  Root,
117
122
  Name,
package/src/Machine.ts CHANGED
@@ -9,6 +9,10 @@
9
9
  * run around that boundary. Menus, rounds, encounter phases, pause flows, and
10
10
  * restart/reset pipelines are the canonical cases.
11
11
  *
12
+ * A machine holds one value for the whole game. A state that each entity has
13
+ * on its own (an enemy's attack windup, a door's open/closed) is not a
14
+ * machine: use a `Descriptor.EntityState(...)` component instead.
15
+ *
12
16
  * @example
13
17
  * ```ts
14
18
  * // Model a gameplay phase whose transition timing matters.
@@ -33,8 +37,9 @@
33
37
  * @groupDescription Functions
34
38
  * Public constructors for machines, conditions, and explicit transition bundles.
35
39
  */
36
- import type { Schedule } from "./Schedule.ts"
40
+ import type { AnyScheduleOrder, Schedule } from "./Schedule.ts"
37
41
  import * as Requirement from "./Requirement.ts"
42
+ import type * as Result from "./Result.ts"
38
43
  import type { Schema } from "./Schema.ts"
39
44
  import type { SystemFailure } from "./System.ts"
40
45
 
@@ -52,60 +57,71 @@ export type StateValue = string | number
52
57
  export interface StateMachineDefinition<
53
58
  out Name extends string = string,
54
59
  out Values extends readonly [StateValue, ...StateValue[]] = readonly [StateValue, ...StateValue[]],
55
- out Root = unknown
60
+ out Root = unknown,
61
+ out Parent = unknown
56
62
  > {
57
63
  readonly kind: "stateMachine"
58
64
  readonly name: Name
59
65
  readonly key: symbol
60
66
  readonly values: Values
67
+ /** For a sub-state: the machine and the state it exists in. */
68
+ readonly within?: SubStateParent | undefined
61
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
62
78
  }
63
79
 
64
80
  /**
65
- * 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.
66
83
  */
67
- export interface TransitionSnapshot<M extends StateMachineDefinition = StateMachineDefinition> {
68
- readonly from: StateMachine.Value<M>
69
- readonly to: StateMachine.Value<M>
70
- }
84
+ export type IsSubState<M> =
85
+ M extends { readonly __parent?: (_: never) => infer Parent } ? unknown extends Parent ? false : true : false
71
86
 
72
87
  /**
73
- * 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.
74
91
  */
75
- export interface MachineView<M extends StateMachineDefinition = StateMachineDefinition> {
76
- get(): StateMachine.Value<M>
77
- is(value: StateMachine.Value<M>): boolean
78
- }
92
+ export type CurrentValue<M extends StateMachine.Any> =
93
+ IsSubState<M> extends true ? StateMachine.Value<M> | undefined : StateMachine.Value<M>
79
94
 
80
95
  /**
81
- * 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.
82
98
  */
83
- export interface NextMachineView<M extends StateMachineDefinition = StateMachineDefinition> {
84
- getPending(): StateMachine.Value<M> | undefined
85
- set(value: StateMachine.Value<M>): void
86
- setIfChanged(value: StateMachine.Value<M>): void
87
- 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
88
107
  }
89
108
 
90
109
  /**
91
- * A typed read-only view over the last applied transition.
92
- *
93
- * This view is only meaningful inside transition schedules and is therefore
94
- * 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.
95
113
  */
96
- export interface TransitionView<M extends StateMachineDefinition = StateMachineDefinition> {
97
- get(): TransitionSnapshot<M>
98
- }
114
+ export type SetResult<M extends StateMachine.Any> =
115
+ IsSubState<M> extends true ? Result.Result<void, SubStateInactive> : void
99
116
 
100
117
  /**
101
- * A typed read-only stream of committed machine transition events.
102
- *
103
- * These snapshots are published by `applyStateTransitions(...)` when a
104
- * transition commits. Like events, each reader sees the transitions published
105
- * 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.
106
121
  */
107
- export interface TransitionEventView<M extends StateMachineDefinition = StateMachineDefinition> {
108
- all(): ReadonlyArray<TransitionSnapshot<M>>
122
+ export interface TransitionSnapshot<M extends StateMachineDefinition = StateMachineDefinition> {
123
+ readonly from: CurrentValue<M>
124
+ readonly to: CurrentValue<M>
109
125
  }
110
126
 
111
127
  /**
@@ -235,13 +251,16 @@ export interface TransitionScheduleDefinition<
235
251
  M extends StateMachineDefinition = StateMachineDefinition,
236
252
  Needs extends Requirement.Requirement = Requirement.Requirement,
237
253
  Root = unknown,
238
- Failure extends SystemFailure = never
254
+ Failure extends SystemFailure = never,
255
+ Order extends AnyScheduleOrder = AnyScheduleOrder
239
256
  > {
240
257
  readonly steps: ReadonlyArray<Schedule.Step>
241
258
  readonly systems: ReadonlyArray<unknown>
242
259
  readonly schema: S
243
260
  readonly requirements: ReadonlyArray<Needs>
244
261
  readonly __failure?: (_: never) => Failure
262
+ /** The plan's order, for `Game.System.thisTick` checks of the tick that applies it. */
263
+ readonly __order?: (_: never) => Order
245
264
  readonly __schemaRoot?: Root | undefined
246
265
  readonly transition: {
247
266
  readonly machine: M
@@ -256,7 +275,7 @@ export interface TransitionScheduleDefinition<
256
275
  * Type-level and value-level helpers for finite-state machines.
257
276
  */
258
277
  export namespace StateMachine {
259
- export type Any = StateMachineDefinition<string, readonly [StateValue, ...StateValue[]], unknown>
278
+ export type Any = StateMachineDefinition<string, readonly [StateValue, ...StateValue[]], unknown, unknown>
260
279
  export type Value<M extends Any> = M["values"][number]
261
280
  export type Root<M extends Any> = M extends StateMachineDefinition<string, readonly [StateValue, ...StateValue[]], infer R> ? R : never
262
281
  export type AnyCondition<Root = unknown> = Condition<Root>
@@ -264,6 +283,23 @@ export namespace StateMachine {
264
283
  TransitionScheduleDefinition<S, Any, Requirement.Requirement, Root, SystemFailure>
265
284
  }
266
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
+
267
303
  /** Extracts the machine token from one declared machine access slot. */
268
304
  type MachineFromAccess<Access> =
269
305
  Access extends { readonly machine: infer M extends StateMachine.Any } ? M : never
@@ -293,7 +329,7 @@ export type MachineNeedsFromConditions<C extends ReadonlyArray<Condition>> =
293
329
  MachineNeedsFromCondition<C[number]>
294
330
 
295
331
  /**
296
- * Creates a schema-bound finite-state machine definition.
332
+ * Creates a schema-bound finite-state machine definition: one game-wide mode.
297
333
  *
298
334
  * This is the intended default for gameplay phases and other discrete modes
299
335
  * where the transition boundary itself matters.
@@ -304,6 +340,12 @@ export type MachineNeedsFromConditions<C extends ReadonlyArray<Condition>> =
304
340
  * - explicit `applyStateTransitions(...)`
305
341
  * - `inState(...)` gating
306
342
  * - transition events or enter/exit schedules
343
+ *
344
+ * Use `Descriptor.EntityState(...)` instead when every entity carries its own
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 })`).
307
349
  */
308
350
  export const StateMachine = <
309
351
  const Name extends string,
@@ -311,12 +353,14 @@ export const StateMachine = <
311
353
  Root = unknown
312
354
  >(
313
355
  name: Name,
314
- values: Values
356
+ values: Values,
357
+ within?: SubStateParent
315
358
  ): StateMachineDefinition<Name, Values, Root> => ({
316
359
  kind: "stateMachine",
317
360
  name,
318
361
  key: Symbol.for(`ritmo/stateMachine/${name}`),
319
- values
362
+ values,
363
+ ...(within === undefined ? {} : { within })
320
364
  }) as StateMachineDefinition<Name, Values, Root>
321
365
 
322
366
  /**
package/src/Query.ts CHANGED
@@ -69,7 +69,8 @@
69
69
  * Query authoring helpers for selecting components and declaring explicit filters.
70
70
  */
71
71
  import type { DecodeError } from "./Decode.ts"
72
- import type { Descriptor, StateValue } from "./Descriptor.ts"
72
+ import * as DescriptorModule from "./Descriptor.ts"
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"
75
76
  import type * as Relation from "./Relation.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
  *
@@ -384,6 +445,12 @@ export namespace Query {
384
445
  ? false
385
446
  : true
386
447
 
448
+ /** The component descriptors a query proves its matches have: required selection slots and `with`. */
449
+ export type Proven<T extends Any> = RequiredSelectionDescriptors<T> | T["with"][number]
450
+
451
+ /** The component descriptors a query proves its matches lack: its `without`. */
452
+ export type Excluded<T extends Any> = T["without"][number]
453
+
387
454
  /**
388
455
  * The readable proof produced by a query.
389
456
  */
@@ -488,24 +555,20 @@ export interface ReadCell<T> {
488
555
  * A mutable cell returned from query or resource access.
489
556
  *
490
557
  * This gives a narrow mutation surface instead of exposing raw storage objects.
558
+ * Values written here are already valid. Data that still needs validation
559
+ * goes through its descriptor's own validator: declare the descriptor with
560
+ * `Descriptor.ConstructedComponent` / `ConstructedResource` and write with
561
+ * `setRaw` / `updateRaw` on its {@link ConstructedWriteCell}.
491
562
  */
492
563
  export interface WriteCell<T> extends ReadCell<T> {
493
564
  /**
494
565
  * Replaces the current value.
495
566
  */
496
567
  set(value: T): void
497
- /**
498
- * Replaces the current value from an explicit result.
499
- */
500
- setResult<E>(result: Result.Result<T, E>): Result.Result<void, E>
501
568
  /**
502
569
  * Updates the current value based on the previous one.
503
570
  */
504
571
  update(f: (current: ReadonlyValue<T>) => T): void
505
- /**
506
- * Updates the current value from an explicit result-producing callback.
507
- */
508
- updateResult<E>(f: (current: ReadonlyValue<T>) => Result.Result<T, E>): Result.Result<void, E>
509
572
  }
510
573
 
511
574
  /**
@@ -540,13 +603,13 @@ export interface ConstructedWriteCell<T, Raw, Error> extends WriteCell<T> {
540
603
  }
541
604
 
542
605
  /**
543
- * A `transition(from, to)` found the state component in another state, so
606
+ * A `transition(from, to)` found the entity state in another state, so
544
607
  * nothing was written.
545
608
  */
546
- export interface StateMismatchError<T> {
547
- readonly _tag: "StateMismatch"
548
- /** Name of the state component. */
549
- readonly state: string
609
+ export interface EntityStateMismatchError<T> {
610
+ readonly _tag: "EntityStateMismatch"
611
+ /** Name of the `Descriptor.EntityState` component. */
612
+ readonly component: string
550
613
  /** The `from` state the transition expected. */
551
614
  readonly expected: T
552
615
  /** The state the component was actually in. */
@@ -554,20 +617,20 @@ export interface StateMismatchError<T> {
554
617
  }
555
618
 
556
619
  /**
557
- * Writable cell for a state component (`Descriptor.State`).
620
+ * Writable cell for an entity state component (`Descriptor.EntityState`).
558
621
  *
559
622
  * `transition(from, to)` is a compare-and-set: it writes `to` only while the
560
- * current state is `from`, and otherwise returns `StateMismatch` without
623
+ * current state is `from`, and otherwise returns `EntityStateMismatch` without
561
624
  * writing. With a transition graph, `to` must be one of the moves the graph
562
625
  * allows from `from`; without one, any pair of states is accepted.
563
626
  */
564
- export interface StateWriteCell<T, Transitions> extends ConstructedWriteCell<T, T, DecodeError> {
627
+ export interface EntityStateWriteCell<T, Transitions> extends ConstructedWriteCell<T, T, DecodeError> {
565
628
  /**
566
629
  * Moves from `from` to `to` if the component is still in `from`.
567
630
  *
568
631
  * @example
569
632
  * ```ts
570
- * const moved = data.phase.transition("windup", "active")
633
+ * const moved = data.attack.transition("windup", "active")
571
634
  * if (!moved.ok) {
572
635
  * // moved.error.actual is the state some earlier write left
573
636
  * }
@@ -575,19 +638,19 @@ export interface StateWriteCell<T, Transitions> extends ConstructedWriteCell<T,
575
638
  */
576
639
  transition<const From extends T & keyof Transitions>(
577
640
  from: From,
578
- to: StateTarget<T, Transitions, From>
579
- ): Result.Result<void, StateMismatchError<T>>
641
+ to: EntityStateTarget<T, Transitions, From>
642
+ ): Result.Result<void, EntityStateMismatchError<T>>
580
643
  }
581
644
 
582
- type StateTarget<T, Transitions, From extends keyof Transitions> =
645
+ type EntityStateTarget<T, Transitions, From extends keyof Transitions> =
583
646
  Transitions[From] extends ReadonlyArray<infer To> ? To & T : never
584
647
 
585
648
  /**
586
649
  * Write-cell surface produced for one queried component descriptor.
587
650
  */
588
651
  export type WriteCellForDescriptor<D extends ComponentDescriptor> =
589
- D extends import("./Descriptor.ts").StateDescriptor<string, infer States extends readonly [StateValue, ...Array<StateValue>], any>
590
- ? StateWriteCell<States[number], D["transitions"] extends undefined ? { readonly [K in States[number]]: States } : D["transitions"]>
652
+ D extends import("./Descriptor.ts").EntityStateDescriptor<string, infer States extends readonly [EntityStateValue, ...Array<EntityStateValue>], any>
653
+ ? EntityStateWriteCell<States[number], D["transitions"] extends undefined ? { readonly [K in States[number]]: States } : D["transitions"]>
591
654
  : D extends import("./Descriptor.ts").ConstructedDescriptor<"component", string, infer Value, infer Raw, infer Error>
592
655
  ? ConstructedWriteCell<Value, Raw, Error>
593
656
  : WriteCell<Descriptor.Value<D>>
@@ -622,11 +685,11 @@ export type OptionalReadCell<T> = PresentOptionalReadCell<T> | AbsentOptionalRea
622
685
  export type QueryMatch<S extends Schema.Any, Q extends Query.Any> =
623
686
  keyof Query.WriteProof<Q> extends never
624
687
  ? {
625
- readonly entity: EntityRef<S, Query.ReadProof<Q>, Query.Root<Q>>
688
+ readonly entity: EntityRef<S, Query.ReadProof<Q>, Query.Root<Q>, Query.Proven<Q>, Query.Excluded<Q>>
626
689
  readonly data: Query.Cells<Q>
627
690
  }
628
691
  : {
629
- readonly entity: EntityMut<S, Query.ReadProof<Q>, Query.WriteProof<Q>, Query.Root<Q>>
692
+ readonly entity: EntityMut<S, Query.ReadProof<Q>, Query.WriteProof<Q>, Query.Root<Q>, Query.Proven<Q>, Query.Excluded<Q>>
630
693
  readonly data: Query.Cells<Q>
631
694
  }
632
695
 
@@ -685,6 +748,14 @@ export const multipleEntitiesError = (count: number): Query.MultipleEntitiesErro
685
748
  * })
686
749
  * ```
687
750
  */
751
+ /**
752
+ * `T` itself, but not a place inference reads from: a list a query omits
753
+ * stays `[]` even when the call sits where a wider query type is expected
754
+ * (`queries: { ... }` in a spec). Unlike `NoInfer`, it prints as `T`.
755
+ */
756
+ // `T` appears only as the check type, which inference never reads.
757
+ export type Uninferred<T, Bound> = [T] extends [infer U extends Bound] ? U : never
758
+
688
759
  export const Query = <
689
760
  const Selection extends Record<string, SelectionAccess<any, Root>>,
690
761
  const With extends ReadonlyArray<ComponentDescriptor> = [],
@@ -704,14 +775,15 @@ export const Query = <
704
775
  readonly withoutRelations?: WithoutRelations
705
776
  readonly withRelated?: WithRelated
706
777
  readonly withoutRelated?: WithoutRelated
707
- }): QuerySpec<Selection, With, Without, Filters, WithRelations, WithoutRelations, WithRelated, WithoutRelated, Root> => ({
778
+ // Uninferred: an omitted list is `[]`, never inferred from the spec it is written in (`queries: { ... }`).
779
+ }): QuerySpec<Selection, Uninferred<With, ReadonlyArray<ComponentDescriptor>>, Uninferred<Without, ReadonlyArray<ComponentDescriptor>>, Uninferred<Filters, ReadonlyArray<Filter<ComponentDescriptor>>>, Uninferred<WithRelations, ReadonlyArray<Relation.Relation.Any>>, Uninferred<WithoutRelations, ReadonlyArray<Relation.Relation.Any>>, Uninferred<WithRelated, ReadonlyArray<Relation.Relation.Any>>, Uninferred<WithoutRelated, ReadonlyArray<Relation.Relation.Any>>, Root> => ({
708
780
  selection: spec.selection,
709
- with: (spec.with ?? []) as With,
710
- without: (spec.without ?? []) as Without,
711
- filters: (spec.filters ?? []) as Filters,
712
- withRelations: (spec.withRelations ?? []) as WithRelations,
713
- withoutRelations: (spec.withoutRelations ?? []) as WithoutRelations,
714
- withRelated: (spec.withRelated ?? []) as WithRelated,
715
- withoutRelated: (spec.withoutRelated ?? []) as WithoutRelated,
781
+ with: (spec.with ?? []) as unknown as Uninferred<With, ReadonlyArray<ComponentDescriptor>>,
782
+ without: (spec.without ?? []) as unknown as Uninferred<Without, ReadonlyArray<ComponentDescriptor>>,
783
+ filters: (spec.filters ?? []) as unknown as Uninferred<Filters, ReadonlyArray<Filter<ComponentDescriptor>>>,
784
+ withRelations: (spec.withRelations ?? []) as unknown as Uninferred<WithRelations, ReadonlyArray<Relation.Relation.Any>>,
785
+ withoutRelations: (spec.withoutRelations ?? []) as unknown as Uninferred<WithoutRelations, ReadonlyArray<Relation.Relation.Any>>,
786
+ withRelated: (spec.withRelated ?? []) as unknown as Uninferred<WithRelated, ReadonlyArray<Relation.Relation.Any>>,
787
+ withoutRelated: (spec.withoutRelated ?? []) as unknown as Uninferred<WithoutRelated, ReadonlyArray<Relation.Relation.Any>>,
716
788
  __schemaRoot: undefined as unknown as Root
717
789
  })