@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.
- package/README.md +2 -0
- package/dist/Brand.d.ts +9 -4
- package/dist/Brand.d.ts.map +1 -1
- package/dist/Brand.js +9 -4
- package/dist/Brand.js.map +1 -1
- package/dist/Clock.d.ts +186 -0
- package/dist/Clock.d.ts.map +1 -0
- package/dist/Clock.js +111 -0
- package/dist/Clock.js.map +1 -0
- package/dist/Command.d.ts +250 -41
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +59 -23
- package/dist/Command.js.map +1 -1
- package/dist/Condition.d.ts +12 -1
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Debug.d.ts +163 -15
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Debug.js.map +1 -1
- package/dist/Decode.d.ts +1 -1
- package/dist/Decode.js +1 -1
- package/dist/Descriptor.d.ts +67 -24
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +32 -15
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +22 -5
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +2 -2
- package/dist/Entity.js.map +1 -1
- package/dist/EntityScope.d.ts +26 -1
- package/dist/EntityScope.d.ts.map +1 -1
- package/dist/EntityScope.js +14 -2
- package/dist/EntityScope.js.map +1 -1
- package/dist/HostClock.d.ts +49 -0
- package/dist/HostClock.d.ts.map +1 -0
- package/dist/HostClock.js +46 -0
- package/dist/HostClock.js.map +1 -0
- package/dist/Inspector.d.ts +7 -1
- package/dist/Inspector.d.ts.map +1 -1
- package/dist/Inspector.js.map +1 -1
- package/dist/Machine.d.ts +67 -36
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +10 -3
- package/dist/Machine.js.map +1 -1
- package/dist/Query.d.ts +74 -26
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +32 -28
- package/dist/Query.js.map +1 -1
- package/dist/Runtime.d.ts +39 -16
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +556 -87
- package/dist/Runtime.js.map +1 -1
- package/dist/Schedule.d.ts +217 -10
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +5 -0
- package/dist/Schedule.js.map +1 -1
- package/dist/Schema.d.ts +196 -44
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +29 -11
- package/dist/Schema.js.map +1 -1
- package/dist/Snapshot.d.ts +14 -0
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js.map +1 -1
- package/dist/System.d.ts +212 -58
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js +84 -6
- package/dist/System.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +3 -3
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +9 -11
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/debug.d.ts +13 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +192 -7
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/equal.d.ts +8 -0
- package/dist/internal/equal.d.ts.map +1 -0
- package/dist/internal/equal.js +38 -0
- package/dist/internal/equal.js.map +1 -0
- package/dist/internal/fragments.d.ts.map +1 -1
- package/dist/internal/fragments.js +11 -1
- package/dist/internal/fragments.js.map +1 -1
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +41 -6
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/order.d.ts +10 -0
- package/dist/internal/order.d.ts.map +1 -0
- package/dist/internal/order.js +122 -0
- package/dist/internal/order.js.map +1 -0
- package/dist/internal/queries.d.ts.map +1 -1
- package/dist/internal/queries.js +32 -10
- package/dist/internal/queries.js.map +1 -1
- package/dist/internal/requirements.d.ts +26 -0
- package/dist/internal/requirements.d.ts.map +1 -0
- package/dist/internal/requirements.js +46 -0
- package/dist/internal/requirements.js.map +1 -0
- package/package.json +1 -1
- package/src/Brand.ts +9 -4
- package/src/Clock.ts +311 -0
- package/src/Command.ts +407 -89
- package/src/Condition.ts +17 -1
- package/src/Debug.ts +172 -14
- package/src/Decode.ts +1 -1
- package/src/Descriptor.ts +92 -37
- package/src/Entity.ts +24 -5
- package/src/EntityScope.ts +27 -2
- package/src/HostClock.ts +64 -0
- package/src/Inspector.ts +7 -2
- package/src/Machine.ts +81 -37
- package/src/Query.ts +110 -38
- package/src/Runtime.ts +607 -111
- package/src/Schedule.ts +380 -13
- package/src/Schema.ts +249 -63
- package/src/Snapshot.ts +4 -0
- package/src/System.ts +248 -93
- package/src/index.ts +8 -7
- package/src/internal/cells.ts +10 -12
- package/src/internal/debug.ts +208 -12
- package/src/internal/equal.ts +31 -0
- package/src/internal/fragments.ts +12 -2
- package/src/internal/game.ts +61 -7
- package/src/internal/order.ts +144 -0
- package/src/internal/queries.ts +37 -13
- package/src/internal/requirements.ts +71 -0
- package/dist/Definition.d.ts +0 -99
- package/dist/Definition.d.ts.map +0 -1
- package/dist/Definition.js +0 -68
- package/dist/Definition.js.map +0 -1
- package/dist/Feature.d.ts +0 -106
- package/dist/Feature.d.ts.map +0 -1
- package/dist/Feature.js +0 -87
- package/dist/Feature.js.map +0 -1
- package/dist/Fx.d.ts +0 -128
- package/dist/Fx.d.ts.map +0 -1
- package/dist/Fx.js +0 -112
- package/dist/Fx.js.map +0 -1
- package/dist/internal/definition.d.ts +0 -17
- package/dist/internal/definition.d.ts.map +0 -1
- package/dist/internal/definition.js +0 -25
- package/dist/internal/definition.js.map +0 -1
- package/src/Definition.ts +0 -106
- package/src/Feature.ts +0 -241
- package/src/Fx.ts +0 -176
- package/src/internal/definition.ts +0 -56
package/src/EntityScope.ts
CHANGED
|
@@ -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/"
|
package/src/HostClock.ts
ADDED
|
@@ -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,
|
|
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
|
-
*
|
|
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
|
|
68
|
-
readonly
|
|
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
|
|
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
|
|
76
|
-
|
|
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
|
-
*
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
|
97
|
-
|
|
98
|
-
}
|
|
114
|
+
export type SetResult<M extends StateMachine.Any> =
|
|
115
|
+
IsSubState<M> extends true ? Result.Result<void, SubStateInactive> : void
|
|
99
116
|
|
|
100
117
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
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
|
|
108
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
|
606
|
+
* A `transition(from, to)` found the entity state in another state, so
|
|
544
607
|
* nothing was written.
|
|
545
608
|
*/
|
|
546
|
-
export interface
|
|
547
|
-
readonly _tag: "
|
|
548
|
-
/** Name of the
|
|
549
|
-
readonly
|
|
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
|
|
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 `
|
|
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
|
|
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.
|
|
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:
|
|
579
|
-
): Result.Result<void,
|
|
641
|
+
to: EntityStateTarget<T, Transitions, From>
|
|
642
|
+
): Result.Result<void, EntityStateMismatchError<T>>
|
|
580
643
|
}
|
|
581
644
|
|
|
582
|
-
type
|
|
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").
|
|
590
|
-
?
|
|
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
|
-
|
|
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
|
})
|