@typeonce/ritmo 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/Clock.d.ts +19 -4
- package/dist/Clock.d.ts.map +1 -1
- package/dist/Clock.js +18 -2
- package/dist/Clock.js.map +1 -1
- package/dist/Command.d.ts +101 -45
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +22 -4
- package/dist/Command.js.map +1 -1
- package/dist/Debug.d.ts +66 -8
- package/dist/Debug.d.ts.map +1 -1
- package/dist/Descriptor.d.ts +27 -9
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +13 -9
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +15 -8
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js.map +1 -1
- package/dist/FixedLoop.d.ts +106 -0
- package/dist/FixedLoop.d.ts.map +1 -0
- package/dist/FixedLoop.js +97 -0
- package/dist/FixedLoop.js.map +1 -0
- package/dist/HostClock.d.ts +1 -1
- package/dist/HostClock.d.ts.map +1 -1
- package/dist/HostClock.js +2 -1
- package/dist/HostClock.js.map +1 -1
- package/dist/Machine.d.ts +4 -1
- package/dist/Machine.d.ts.map +1 -1
- package/dist/Machine.js +4 -1
- package/dist/Machine.js.map +1 -1
- package/dist/Query.d.ts +52 -10
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +2 -1
- package/dist/Query.js.map +1 -1
- package/dist/Relation.d.ts +5 -14
- package/dist/Relation.d.ts.map +1 -1
- package/dist/Relation.js +0 -32
- package/dist/Relation.js.map +1 -1
- package/dist/Runtime.d.ts +58 -15
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +288 -70
- package/dist/Runtime.js.map +1 -1
- package/dist/Schedule.d.ts +24 -15
- package/dist/Schedule.d.ts.map +1 -1
- package/dist/Schedule.js +2 -1
- package/dist/Schedule.js.map +1 -1
- package/dist/Schema.d.ts +98 -29
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +4 -4
- package/dist/Schema.js.map +1 -1
- package/dist/Snapshot.d.ts +76 -1
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +42 -1
- package/dist/Snapshot.js.map +1 -1
- package/dist/System.d.ts +68 -24
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +2 -0
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +34 -8
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/debug.d.ts +3 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +83 -15
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/fragments.d.ts +2 -6
- package/dist/internal/fragments.d.ts.map +1 -1
- package/dist/internal/fragments.js +25 -2
- package/dist/internal/fragments.js.map +1 -1
- package/dist/internal/game.d.ts +2 -0
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +16 -3
- package/dist/internal/game.js.map +1 -1
- package/dist/internal/queries.d.ts.map +1 -1
- package/dist/internal/queries.js +27 -18
- package/dist/internal/queries.js.map +1 -1
- package/dist/internal/relations.d.ts +10 -0
- package/dist/internal/relations.d.ts.map +1 -0
- package/dist/internal/relations.js +32 -0
- package/dist/internal/relations.js.map +1 -0
- package/dist/internal/streams.d.ts +2 -0
- package/dist/internal/streams.d.ts.map +1 -1
- package/dist/internal/streams.js +10 -0
- package/dist/internal/streams.js.map +1 -1
- package/dist/internal/world.d.ts +7 -1
- package/dist/internal/world.d.ts.map +1 -1
- package/dist/internal/world.js +53 -3
- package/dist/internal/world.js.map +1 -1
- package/package.json +1 -1
- package/src/Clock.ts +29 -4
- package/src/Command.ts +161 -67
- package/src/Debug.ts +61 -9
- package/src/Descriptor.ts +43 -17
- package/src/Entity.ts +16 -9
- package/src/FixedLoop.ts +218 -0
- package/src/HostClock.ts +2 -1
- package/src/Machine.ts +4 -1
- package/src/Query.ts +47 -10
- package/src/Relation.ts +5 -67
- package/src/Runtime.ts +345 -85
- package/src/Schedule.ts +31 -19
- package/src/Schema.ts +118 -35
- package/src/Snapshot.ts +90 -2
- package/src/System.ts +76 -23
- package/src/index.ts +4 -0
- package/src/internal/cells.ts +42 -10
- package/src/internal/debug.ts +84 -15
- package/src/internal/fragments.ts +23 -3
- package/src/internal/game.ts +19 -3
- package/src/internal/queries.ts +28 -2
- package/src/internal/relations.ts +52 -0
- package/src/internal/streams.ts +11 -0
- package/src/internal/world.ts +60 -4
package/src/Descriptor.ts
CHANGED
|
@@ -280,6 +280,22 @@ const makeConstructedDescriptor = <
|
|
|
280
280
|
[descriptorConstruction]: constructor
|
|
281
281
|
}) as ConstructedDescriptor<Kind, Name, Value, Raw, Error>
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* The message naming why `Value` cannot be the value of a descriptor of
|
|
285
|
+
* `Kind`, or `never` when it can. `any` turns off every check on the
|
|
286
|
+
* descriptor's reads and writes, and `never` erases the fragment it is listed in.
|
|
287
|
+
*/
|
|
288
|
+
type Article<Kind extends string> = Kind extends "event" ? "an" : "a"
|
|
289
|
+
|
|
290
|
+
type ValueRejection<Kind extends string, Value> = 0 extends 1 & Value
|
|
291
|
+
? `${Article<Kind>} ${Kind} value cannot be \`any\`: give it a type (use \`unknown\` for a value you never read)`
|
|
292
|
+
: [Value] extends [never] ? `${Article<Kind>} ${Kind} value cannot be \`never\`: give it a type (use \`{}\` for a marker)`
|
|
293
|
+
: never
|
|
294
|
+
|
|
295
|
+
/** `Make`, or, when `Value` is rejected, a function whose only parameter is the message. */
|
|
296
|
+
type CheckedValue<Kind extends string, Value, Make> = [ValueRejection<Kind, Value>] extends [never] ? Make
|
|
297
|
+
: (name: ValueRejection<Kind, Value>) => never
|
|
298
|
+
|
|
283
299
|
/**
|
|
284
300
|
* Defines a component descriptor.
|
|
285
301
|
*
|
|
@@ -289,6 +305,10 @@ const makeConstructedDescriptor = <
|
|
|
289
305
|
* Components are the only descriptor kind that can be queried directly with
|
|
290
306
|
* `Game.Query.read(...)`, `write(...)`, or `optional(...)`.
|
|
291
307
|
*
|
|
308
|
+
* The value is a real type: `any` and `never` do not compile (here and in
|
|
309
|
+
* every other descriptor constructor). Use `unknown` for a value no system
|
|
310
|
+
* reads, and `Descriptor.Tag` for a marker.
|
|
311
|
+
*
|
|
292
312
|
* @example
|
|
293
313
|
* ```ts
|
|
294
314
|
* const Position = Descriptor.Component<{ x: number; y: number }>()("Position")
|
|
@@ -296,11 +316,11 @@ const makeConstructedDescriptor = <
|
|
|
296
316
|
* const EnemyKind = Descriptor.Component<"grunt" | "boss">()("EnemyKind", { immutable: true })
|
|
297
317
|
* ```
|
|
298
318
|
*/
|
|
299
|
-
export const Component = <Value>()
|
|
319
|
+
export const Component = <Value>(): CheckedValue<"component", Value, <const Name extends string, const Options extends ComponentOptions = {}>(
|
|
300
320
|
name: Name,
|
|
301
321
|
options?: Options
|
|
302
|
-
)
|
|
303
|
-
applyComponentOptions(makeDescriptor("component", name), options) as
|
|
322
|
+
) => WithComponentOptions<Descriptor<"component", Name, Value>, Options>> =>
|
|
323
|
+
((name: string, options?: ComponentOptions) => applyComponentOptions(makeDescriptor("component", name), options)) as never
|
|
304
324
|
|
|
305
325
|
/**
|
|
306
326
|
* Defines a component descriptor that also knows how to validate raw values.
|
|
@@ -334,11 +354,11 @@ export const ConstructedComponent = <Value, Raw, Error, Constructor extends {} =
|
|
|
334
354
|
* const SpriteRef = Descriptor.TransientComponent<{ frame: number }>()("SpriteRef")
|
|
335
355
|
* ```
|
|
336
356
|
*/
|
|
337
|
-
export const TransientComponent = <Value>()
|
|
357
|
+
export const TransientComponent = <Value>(): CheckedValue<"component", Value, <const Name extends string, const Options extends ComponentOptions = {}>(
|
|
338
358
|
name: Name,
|
|
339
359
|
options?: Options
|
|
340
|
-
)
|
|
341
|
-
applyComponentOptions(makeTransientDescriptor("component", name), options) as
|
|
360
|
+
) => WithComponentOptions<TransientDescriptor<"component", Name, Value>, Options>> =>
|
|
361
|
+
((name: string, options?: ComponentOptions) => applyComponentOptions(makeTransientDescriptor("component", name), options)) as never
|
|
342
362
|
|
|
343
363
|
/**
|
|
344
364
|
* Defines a marker component: no data, only presence (`[Player, {}]`).
|
|
@@ -446,9 +466,9 @@ export const isEntityState = (
|
|
|
446
466
|
* const Score = Descriptor.Resource<number>()("Score")
|
|
447
467
|
* ```
|
|
448
468
|
*/
|
|
449
|
-
export const Resource = <Value>()
|
|
469
|
+
export const Resource = <Value>(): CheckedValue<"resource", Value, <const Name extends string>(
|
|
450
470
|
name: Name
|
|
451
|
-
)
|
|
471
|
+
) => Descriptor<"resource", Name, Value>> => ((name: string) => makeDescriptor("resource", name)) as never
|
|
452
472
|
|
|
453
473
|
/**
|
|
454
474
|
* Defines a resource descriptor that also knows how to validate raw values.
|
|
@@ -469,9 +489,9 @@ export const ConstructedResource = <Value, Raw, Error, Constructor extends {} =
|
|
|
469
489
|
* const DeltaTime = Descriptor.TransientResource<number>()("DeltaTime")
|
|
470
490
|
* ```
|
|
471
491
|
*/
|
|
472
|
-
export const TransientResource = <Value>()
|
|
492
|
+
export const TransientResource = <Value>(): CheckedValue<"resource", Value, <const Name extends string>(
|
|
473
493
|
name: Name
|
|
474
|
-
)
|
|
494
|
+
) => TransientDescriptor<"resource", Name, Value>> => ((name: string) => makeTransientDescriptor("resource", name)) as never
|
|
475
495
|
|
|
476
496
|
/**
|
|
477
497
|
* Defines an event descriptor.
|
|
@@ -491,9 +511,9 @@ export const TransientResource = <Value>() => <const Name extends string>(
|
|
|
491
511
|
* const Hit = Descriptor.Event<{ target: number; amount: number }>()("Hit")
|
|
492
512
|
* ```
|
|
493
513
|
*/
|
|
494
|
-
export const Event = <Value>()
|
|
514
|
+
export const Event = <Value>(): CheckedValue<"event", Value, <const Name extends string>(
|
|
495
515
|
name: Name
|
|
496
|
-
)
|
|
516
|
+
) => Descriptor<"event", Name, Value>> => ((name: string) => makeDescriptor("event", name)) as never
|
|
497
517
|
|
|
498
518
|
/**
|
|
499
519
|
* Defines a service descriptor.
|
|
@@ -512,9 +532,9 @@ export const Event = <Value>() => <const Name extends string>(
|
|
|
512
532
|
* const Logger = Descriptor.Service<{ log: (message: string) => void }>()("Logger")
|
|
513
533
|
* ```
|
|
514
534
|
*/
|
|
515
|
-
export const Service = <Value>()
|
|
535
|
+
export const Service = <Value>(): CheckedValue<"service", Value, <const Name extends string>(
|
|
516
536
|
name: Name
|
|
517
|
-
)
|
|
537
|
+
) => Descriptor<"service", Name, Value>> => ((name: string) => makeDescriptor("service", name)) as never
|
|
518
538
|
|
|
519
539
|
/**
|
|
520
540
|
* Checks whether one descriptor is transient (skipped by snapshots).
|
|
@@ -625,7 +645,10 @@ export function constructorOf<D extends Descriptor.Any>(
|
|
|
625
645
|
* `Game.EntityScope(...)` instead when entities only share a lifetime (a
|
|
626
646
|
* level, a menu) and need no tree, order, or traversal.
|
|
627
647
|
*/
|
|
628
|
-
export const Hierarchy =
|
|
648
|
+
export const Hierarchy = <const Name extends string, const RelatedName extends string>(
|
|
649
|
+
name: Name,
|
|
650
|
+
relatedName: RelatedName
|
|
651
|
+
) => makePair("hierarchy", name, relatedName)
|
|
629
652
|
|
|
630
653
|
/**
|
|
631
654
|
* Defines a general relationship pair with direct edges and reverse lookups.
|
|
@@ -634,7 +657,10 @@ export const Hierarchy = RelationModule.Hierarchy
|
|
|
634
657
|
* reverse lookup, but not hierarchy-only behavior such as tree traversal or
|
|
635
658
|
* child reordering.
|
|
636
659
|
*/
|
|
637
|
-
export const Relation =
|
|
660
|
+
export const Relation = <const Name extends string, const RelatedName extends string>(
|
|
661
|
+
name: Name,
|
|
662
|
+
relatedName: RelatedName
|
|
663
|
+
) => makePair("relation", name, relatedName)
|
|
638
664
|
import * as DecodeModule from "./Decode.ts"
|
|
639
|
-
import
|
|
665
|
+
import { makePair } from "./internal/relations.ts"
|
|
640
666
|
import * as ResultModule from "./Result.ts"
|
package/src/Entity.ts
CHANGED
|
@@ -131,7 +131,7 @@ export namespace Handle {
|
|
|
131
131
|
* Drafts exist before the command queue is flushed. This is the place where the
|
|
132
132
|
* API can safely carry exact structural information.
|
|
133
133
|
*/
|
|
134
|
-
export interface EntityDraft<S extends Schema.Any, out P extends ComponentProof, Root = unknown> {
|
|
134
|
+
export interface EntityDraft<S extends Schema.Any, out P extends ComponentProof, Root = unknown, out Relates = never> {
|
|
135
135
|
/**
|
|
136
136
|
* Runtime tag for debugging and pattern matching.
|
|
137
137
|
*/
|
|
@@ -156,6 +156,11 @@ export interface EntityDraft<S extends Schema.Any, out P extends ComponentProof,
|
|
|
156
156
|
* Staged relationship edges attached to the entity before spawn.
|
|
157
157
|
*/
|
|
158
158
|
readonly relations: ReadonlyArray<StagedRelation<S, Root>>
|
|
159
|
+
/**
|
|
160
|
+
* The relations `Game.Command.relate(...)` staged on this draft: spawning
|
|
161
|
+
* it needs them in the system's `Game.System.relates(...)`. Type-level only.
|
|
162
|
+
*/
|
|
163
|
+
readonly __relates?: (_: never) => Relates
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
/**
|
|
@@ -168,7 +173,7 @@ export type StagedComponent = readonly [Descriptor<"component", string, any>, un
|
|
|
168
173
|
*
|
|
169
174
|
* Query execution is the main source of `EntityRef` values.
|
|
170
175
|
*/
|
|
171
|
-
export interface EntityRef<S extends Schema.Any, out P extends ComponentProof, Root = unknown,
|
|
176
|
+
export interface EntityRef<S extends Schema.Any, out P extends ComponentProof, Root = unknown, in Proves = never, in Lacks = never> {
|
|
172
177
|
/**
|
|
173
178
|
* Runtime tag for debugging and pattern matching.
|
|
174
179
|
*/
|
|
@@ -176,15 +181,17 @@ export interface EntityRef<S extends Schema.Any, out P extends ComponentProof, R
|
|
|
176
181
|
/**
|
|
177
182
|
* The component descriptors the query that produced this ref proves (its
|
|
178
183
|
* required slots and `with`): what `commands.despawn`, `insert`, and
|
|
179
|
-
* `remove` check. Type-level only.
|
|
184
|
+
* `remove` check. Type-level only. Contravariant on purpose: a ref proving
|
|
185
|
+
* more is a subtype, so a union of refs (`cond ? a.entity : b.entity`)
|
|
186
|
+
* keeps only what both prove.
|
|
180
187
|
*/
|
|
181
|
-
readonly __proves?: (_:
|
|
188
|
+
readonly __proves?: (_: Proves) => void
|
|
182
189
|
/**
|
|
183
190
|
* The component descriptors the producing query proves absent (its
|
|
184
191
|
* `without`): what `commands.remove` checks against `Schema.requires`.
|
|
185
192
|
* Type-level only.
|
|
186
193
|
*/
|
|
187
|
-
readonly __lacks?: (_:
|
|
194
|
+
readonly __lacks?: (_: Lacks) => void
|
|
188
195
|
/**
|
|
189
196
|
* The schema-bound entity identity.
|
|
190
197
|
*/
|
|
@@ -207,17 +214,17 @@ export interface EntityMut<
|
|
|
207
214
|
out P extends ComponentProof,
|
|
208
215
|
out W extends ComponentProof,
|
|
209
216
|
Root = unknown,
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
in Proves = never,
|
|
218
|
+
in Lacks = never
|
|
212
219
|
> {
|
|
213
220
|
/**
|
|
214
221
|
* Runtime tag for debugging and pattern matching.
|
|
215
222
|
*/
|
|
216
223
|
readonly kind: "EntityMut"
|
|
217
224
|
/** The component descriptors the producing query proves; see `EntityRef`. */
|
|
218
|
-
readonly __proves?: (_:
|
|
225
|
+
readonly __proves?: (_: Proves) => void
|
|
219
226
|
/** The component descriptors the producing query proves absent; see `EntityRef`. */
|
|
220
|
-
readonly __lacks?: (_:
|
|
227
|
+
readonly __lacks?: (_: Lacks) => void
|
|
221
228
|
/**
|
|
222
229
|
* The schema-bound entity identity.
|
|
223
230
|
*/
|
package/src/FixedLoop.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic fixed-step timing for any host: browsers, native shells,
|
|
3
|
+
* desktop runtimes, and headless tests.
|
|
4
|
+
*
|
|
5
|
+
* The loop owns the timing policy that is easy to get subtly wrong in every
|
|
6
|
+
* game: frame clamping, catch-up limits, interpolation, cancellation, and
|
|
7
|
+
* typed update failures. It does not know about an ECS runtime, a renderer,
|
|
8
|
+
* or a platform: frames arrive through a `TickSource` that the host supplies.
|
|
9
|
+
* Use a platform adapter such as `BrowserTickSource.make(window)` from
|
|
10
|
+
* `@typeonce/ritmo-browser`, or wrap a renderer's own ticker with `source`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const loop = FixedLoop.start({
|
|
15
|
+
* source: BrowserTickSource.make(window),
|
|
16
|
+
* step: Clock.hz(60),
|
|
17
|
+
* maxFrameSeconds: 0.25,
|
|
18
|
+
* maxStepsPerFrame: 5,
|
|
19
|
+
* update: () => runtime.tick(update),
|
|
20
|
+
* render: (frame) => draw(frame.alpha),
|
|
21
|
+
* onFailure: (failure) => console.error(failure)
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @module FixedLoop
|
|
26
|
+
* @docGroup runtime
|
|
27
|
+
*/
|
|
28
|
+
import type * as Clock from "./Clock.ts"
|
|
29
|
+
import * as Result from "./Result.ts"
|
|
30
|
+
|
|
31
|
+
/** One host frame: milliseconds elapsed since the previous one. */
|
|
32
|
+
export interface Tick {
|
|
33
|
+
readonly deltaMS: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A stoppable host subscription. Calling `stop` more than once is safe. */
|
|
37
|
+
export interface Subscription {
|
|
38
|
+
stop(): void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Where host frames come from. Implemented by platform adapters
|
|
43
|
+
* (`BrowserTickSource.make(window)`), by `source` around a renderer's ticker,
|
|
44
|
+
* and by deterministic tests.
|
|
45
|
+
*/
|
|
46
|
+
export interface TickSource<TTick extends Tick = Tick> {
|
|
47
|
+
subscribe(onTick: (tick: TTick) => void): Subscription
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Validated fixed-step timing policy. */
|
|
51
|
+
export interface Options {
|
|
52
|
+
/**
|
|
53
|
+
* The fixed step every update advances (`Clock.hz(60)`): the same value
|
|
54
|
+
* the game's clock resource runs at, so the loop and the clock agree.
|
|
55
|
+
*/
|
|
56
|
+
readonly step: Clock.Step
|
|
57
|
+
/** Maximum host-frame duration admitted into the accumulator. */
|
|
58
|
+
readonly maxFrameSeconds: number
|
|
59
|
+
/** Maximum number of simulation updates performed for one host frame. */
|
|
60
|
+
readonly maxStepsPerFrame: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Configuration error returned before a loop subscribes to its host. */
|
|
64
|
+
export interface InvalidOptions {
|
|
65
|
+
readonly kind: "InvalidFixedLoopOptions"
|
|
66
|
+
readonly field: keyof Options
|
|
67
|
+
readonly reason: "mustBeFiniteAndPositive" | "mustBePositiveInteger"
|
|
68
|
+
readonly value: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Data passed to the renderer after all fixed updates for one host frame. */
|
|
72
|
+
export interface Frame {
|
|
73
|
+
/** Clamped time admitted for this host frame. */
|
|
74
|
+
readonly elapsedSeconds: number
|
|
75
|
+
/** Number of fixed updates completed for this host frame. */
|
|
76
|
+
readonly fixedSteps: number
|
|
77
|
+
/** Remaining accumulator fraction in the range `[0, 1)`. */
|
|
78
|
+
readonly alpha: number
|
|
79
|
+
/** Time discarded by frame clamping or the catch-up limit. */
|
|
80
|
+
readonly droppedSeconds: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Failure delivered by an active loop before it stops itself. */
|
|
84
|
+
export type Failure<UpdateError> =
|
|
85
|
+
| {
|
|
86
|
+
readonly kind: "InvalidTickDelta"
|
|
87
|
+
readonly deltaMS: number
|
|
88
|
+
}
|
|
89
|
+
| {
|
|
90
|
+
readonly kind: "UpdateFailure"
|
|
91
|
+
readonly error: UpdateError
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A running fixed-step driver. */
|
|
95
|
+
export interface Driver extends Subscription {
|
|
96
|
+
/** Clears accumulated partial time without removing the host subscription. */
|
|
97
|
+
reset(): void
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Input for a fixed-step driver. */
|
|
101
|
+
export interface StartOptions<TTick extends Tick, UpdateError> extends Options {
|
|
102
|
+
readonly source: TickSource<TTick>
|
|
103
|
+
readonly update: (stepSeconds: number) => Result.Result<void, UpdateError>
|
|
104
|
+
readonly render: (frame: Frame) => void
|
|
105
|
+
readonly onFailure: (failure: Failure<UpdateError>) => void
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Wraps a host-specific subscription function in the canonical source type. */
|
|
109
|
+
export const source = <TTick extends Tick>(
|
|
110
|
+
subscribe: TickSource<TTick>["subscribe"]
|
|
111
|
+
): TickSource<TTick> => ({ subscribe })
|
|
112
|
+
|
|
113
|
+
const validate = (options: Options): Result.Result<void, InvalidOptions> => {
|
|
114
|
+
for (const field of ["maxFrameSeconds"] as const) {
|
|
115
|
+
const value = options[field]
|
|
116
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
117
|
+
return Result.failure({
|
|
118
|
+
kind: "InvalidFixedLoopOptions",
|
|
119
|
+
field,
|
|
120
|
+
reason: "mustBeFiniteAndPositive",
|
|
121
|
+
value
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!Number.isInteger(options.maxStepsPerFrame) || options.maxStepsPerFrame <= 0) {
|
|
126
|
+
return Result.failure({
|
|
127
|
+
kind: "InvalidFixedLoopOptions",
|
|
128
|
+
field: "maxStepsPerFrame",
|
|
129
|
+
reason: "mustBePositiveInteger",
|
|
130
|
+
value: options.maxStepsPerFrame
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
return Result.success(undefined)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Starts a fixed-step loop after validating its timing policy.
|
|
138
|
+
*
|
|
139
|
+
* Update failures and malformed host ticks are delivered to `onFailure`, then
|
|
140
|
+
* the driver stops. Render defects are deliberately not caught: they are
|
|
141
|
+
* programmer defects, not expected game outcomes.
|
|
142
|
+
*/
|
|
143
|
+
export const start = <TTick extends Tick, UpdateError>(
|
|
144
|
+
options: StartOptions<TTick, UpdateError>
|
|
145
|
+
): Result.Result<Driver, InvalidOptions> => {
|
|
146
|
+
const validity = validate(options)
|
|
147
|
+
if (!validity.ok) {
|
|
148
|
+
return validity
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let accumulator = 0
|
|
152
|
+
let stopped = false
|
|
153
|
+
let hostSubscription: Subscription | undefined
|
|
154
|
+
|
|
155
|
+
const stop = (): void => {
|
|
156
|
+
if (stopped) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
stopped = true
|
|
160
|
+
hostSubscription?.stop()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const fail = (failure: Failure<UpdateError>): void => {
|
|
164
|
+
stop()
|
|
165
|
+
options.onFailure(failure)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const onTick = (tick: TTick): void => {
|
|
169
|
+
if (stopped) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
if (!Number.isFinite(tick.deltaMS) || tick.deltaMS < 0) {
|
|
173
|
+
fail({ kind: "InvalidTickDelta", deltaMS: tick.deltaMS })
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const rawSeconds = tick.deltaMS / 1000
|
|
178
|
+
const elapsedSeconds = Math.min(rawSeconds, options.maxFrameSeconds)
|
|
179
|
+
let droppedSeconds = rawSeconds - elapsedSeconds
|
|
180
|
+
accumulator += elapsedSeconds
|
|
181
|
+
|
|
182
|
+
let fixedSteps = 0
|
|
183
|
+
while (accumulator >= options.step.dt && fixedSteps < options.maxStepsPerFrame) {
|
|
184
|
+
const update = options.update(options.step.dt)
|
|
185
|
+
if (!update.ok) {
|
|
186
|
+
fail({ kind: "UpdateFailure", error: update.error })
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
accumulator -= options.step.dt
|
|
190
|
+
fixedSteps += 1
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (accumulator >= options.step.dt) {
|
|
194
|
+
const droppedWholeSteps = Math.floor(accumulator / options.step.dt) * options.step.dt
|
|
195
|
+
accumulator -= droppedWholeSteps
|
|
196
|
+
droppedSeconds += droppedWholeSteps
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
options.render({
|
|
200
|
+
elapsedSeconds,
|
|
201
|
+
fixedSteps,
|
|
202
|
+
alpha: accumulator / options.step.dt,
|
|
203
|
+
droppedSeconds
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
hostSubscription = options.source.subscribe(onTick)
|
|
208
|
+
if (stopped) {
|
|
209
|
+
hostSubscription.stop()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return Result.success({
|
|
213
|
+
stop,
|
|
214
|
+
reset() {
|
|
215
|
+
accumulator = 0
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
}
|
package/src/HostClock.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
15
|
* ```ts
|
|
16
|
-
* const runtime = Game.Runtime.make({ services, debug: { hostClock: HostClock.standard } })
|
|
16
|
+
* const runtime = Game.Runtime.make({ services, debug: { hostClock: HostClock.standard } }).value
|
|
17
17
|
* ```
|
|
18
18
|
*
|
|
19
19
|
* @module HostClock
|
|
@@ -33,6 +33,7 @@ export interface HostClock {
|
|
|
33
33
|
* 0. Prefer a monotonic platform clock when there is one
|
|
34
34
|
* (`BrowserHostClock.make(window)`).
|
|
35
35
|
*/
|
|
36
|
+
// oxlint-disable-next-line ritmo/use-clock-for-time -- HostClock is host time, for hosts and tools; systems read Clock.
|
|
36
37
|
export const standard: HostClock = { now: () => Date.now() }
|
|
37
38
|
|
|
38
39
|
/** `adjust` must leave a finite clock reading and never move backwards. */
|
package/src/Machine.ts
CHANGED
|
@@ -418,7 +418,10 @@ export const inState = <M extends StateMachine.Any>(
|
|
|
418
418
|
})
|
|
419
419
|
|
|
420
420
|
/**
|
|
421
|
-
* Creates a condition that passes when the machine changed during the current schedule execution
|
|
421
|
+
* Creates a condition that passes when the machine changed during the current schedule execution:
|
|
422
|
+
* from the `applyStateTransitions(...)` that committed the change to the end of
|
|
423
|
+
* that schedule (or the next `applyStateTransitions(...)`), not for the rest of
|
|
424
|
+
* the tick call or the next schedule.
|
|
422
425
|
*/
|
|
423
426
|
export const stateChanged = <M extends StateMachine.Any>(
|
|
424
427
|
machine: M
|
package/src/Query.ts
CHANGED
|
@@ -298,7 +298,8 @@ export const added = <D extends ComponentDescriptor>(descriptor: D): AddedFilter
|
|
|
298
298
|
*
|
|
299
299
|
* Any write through a write cell counts, even when the value is equal to the
|
|
300
300
|
* previous one. Writes from a system whose run failed are rolled back and do
|
|
301
|
-
* not count.
|
|
301
|
+
* not count. A system never sees its own writes as changes on its next run
|
|
302
|
+
* (it does see the events it emitted).
|
|
302
303
|
*
|
|
303
304
|
* @example
|
|
304
305
|
* ```ts
|
|
@@ -427,6 +428,8 @@ export namespace Query {
|
|
|
427
428
|
*/
|
|
428
429
|
export type Root<T extends Any> = T extends QuerySpec<any, any, any, any, any, any, any, any, infer R> ? R : never
|
|
429
430
|
|
|
431
|
+
type FilteredDescriptors<F> = F extends { readonly kind: "added" | "changed"; readonly descriptor: infer D } ? D : never
|
|
432
|
+
|
|
430
433
|
type RequiredSelectionDescriptors<T extends Any> = {
|
|
431
434
|
readonly [K in keyof T["selection"]]:
|
|
432
435
|
T["selection"][K] extends ReadAccess<infer D> | WriteAccess<infer D> ? D : never
|
|
@@ -441,12 +444,16 @@ export namespace Query {
|
|
|
441
444
|
export type ProvesComponent<
|
|
442
445
|
T extends Any,
|
|
443
446
|
D extends ComponentDescriptor
|
|
444
|
-
> = [Extract<
|
|
447
|
+
> = [Extract<Proven<T>, D>] extends [never]
|
|
445
448
|
? false
|
|
446
449
|
: true
|
|
447
450
|
|
|
448
|
-
/**
|
|
449
|
-
|
|
451
|
+
/**
|
|
452
|
+
* The component descriptors a query proves its matches have: required
|
|
453
|
+
* selection slots, `with`, and the component of each `added` / `changed`
|
|
454
|
+
* filter (a match has it). A `Query.anyOf` member proves nothing alone.
|
|
455
|
+
*/
|
|
456
|
+
export type Proven<T extends Any> = RequiredSelectionDescriptors<T> | T["with"][number] | FilteredDescriptors<T["filters"][number]>
|
|
450
457
|
|
|
451
458
|
/** The component descriptors a query proves its matches lack: its `without`. */
|
|
452
459
|
export type Excluded<T extends Any> = T["without"][number]
|
|
@@ -562,13 +569,14 @@ export interface ReadCell<T> {
|
|
|
562
569
|
*/
|
|
563
570
|
export interface WriteCell<T> extends ReadCell<T> {
|
|
564
571
|
/**
|
|
565
|
-
* Replaces the current value.
|
|
572
|
+
* Replaces the current value. Takes the readonly shape reads return, so a
|
|
573
|
+
* value read from any cell can be written back as it is.
|
|
566
574
|
*/
|
|
567
|
-
set(value: T): void
|
|
575
|
+
set(value: ReadonlyValue<T>): void
|
|
568
576
|
/**
|
|
569
577
|
* Updates the current value based on the previous one.
|
|
570
578
|
*/
|
|
571
|
-
update(f: (current: ReadonlyValue<T>) => T): void
|
|
579
|
+
update(f: (current: ReadonlyValue<T>) => ReadonlyValue<T>): void
|
|
572
580
|
}
|
|
573
581
|
|
|
574
582
|
/**
|
|
@@ -626,7 +634,10 @@ export interface EntityStateMismatchError<T> {
|
|
|
626
634
|
*/
|
|
627
635
|
export interface EntityStateWriteCell<T, Transitions> extends ConstructedWriteCell<T, T, DecodeError> {
|
|
628
636
|
/**
|
|
629
|
-
* Moves from `from` to `to` if the component is still in `from`.
|
|
637
|
+
* Moves from `from` to `to` if the component is still in `from`. Use it for
|
|
638
|
+
* every move the transition table describes; `set` writes any state
|
|
639
|
+
* without checking the table, for resets that must not depend on the
|
|
640
|
+
* current state.
|
|
630
641
|
*
|
|
631
642
|
* @example
|
|
632
643
|
* ```ts
|
|
@@ -660,6 +671,8 @@ export type WriteCellForDescriptor<D extends ComponentDescriptor> =
|
|
|
660
671
|
*/
|
|
661
672
|
export interface PresentOptionalReadCell<T> extends ReadCell<T> {
|
|
662
673
|
readonly present: true
|
|
674
|
+
/** The value, which is present. */
|
|
675
|
+
value(): T
|
|
663
676
|
}
|
|
664
677
|
|
|
665
678
|
/**
|
|
@@ -667,12 +680,17 @@ export interface PresentOptionalReadCell<T> extends ReadCell<T> {
|
|
|
667
680
|
*/
|
|
668
681
|
export interface AbsentOptionalReadCell {
|
|
669
682
|
readonly present: false
|
|
683
|
+
/** `undefined`: the entity does not hold the component. */
|
|
684
|
+
value(): undefined
|
|
670
685
|
}
|
|
671
686
|
|
|
672
687
|
/**
|
|
673
688
|
* A query slot that may or may not be present on the matched entity.
|
|
674
689
|
*
|
|
675
|
-
*
|
|
690
|
+
* Read it with `value()`: the value, or `undefined` when the entity does not
|
|
691
|
+
* hold the component. Narrow on `present` (then `get()`) only when the
|
|
692
|
+
* component's own value can be `undefined`, where `value()` cannot tell the
|
|
693
|
+
* two apart.
|
|
676
694
|
*/
|
|
677
695
|
export type OptionalReadCell<T> = PresentOptionalReadCell<T> | AbsentOptionalReadCell
|
|
678
696
|
|
|
@@ -756,6 +774,25 @@ export const multipleEntitiesError = (count: number): Query.MultipleEntitiesErro
|
|
|
756
774
|
// `T` appears only as the check type, which inference never reads.
|
|
757
775
|
export type Uninferred<T, Bound> = [T] extends [infer U extends Bound] ? U : never
|
|
758
776
|
|
|
777
|
+
type DescriptorName<D> = D extends { readonly name: infer Name extends string } ? Name : never
|
|
778
|
+
|
|
779
|
+
/** The names a query requires: its read and write slots, `with`, and plain `added` / `changed` filters. */
|
|
780
|
+
type RequiredNames<Selection, With extends ReadonlyArray<unknown>, Filters extends ReadonlyArray<unknown>> =
|
|
781
|
+
| DescriptorName<{ readonly [K in keyof Selection]: Selection[K] extends { readonly mode: "read" | "write"; readonly descriptor: infer D } ? D : never }[keyof Selection]>
|
|
782
|
+
| DescriptorName<With[number]>
|
|
783
|
+
| DescriptorName<Filters[number] extends infer F ? F extends { readonly kind: "added" | "changed"; readonly descriptor: infer D } ? D : never : never>
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Rejects a `without` naming a component the query also requires: such a
|
|
787
|
+
* query can never match. Use it on the `without` list of a query spec.
|
|
788
|
+
*/
|
|
789
|
+
export type ExclusionsPossible<Selection, With extends ReadonlyArray<unknown>, Without extends ReadonlyArray<unknown>, Filters extends ReadonlyArray<unknown>> =
|
|
790
|
+
[Extract<DescriptorName<Without[number]>, RequiredNames<Selection, With, Filters>>] extends [never]
|
|
791
|
+
? unknown
|
|
792
|
+
: {
|
|
793
|
+
readonly __fixQuery__: `${Extract<DescriptorName<Without[number]>, RequiredNames<Selection, With, Filters>>} is both required and excluded (without): this query can never match; remove it from one side`
|
|
794
|
+
}
|
|
795
|
+
|
|
759
796
|
export const Query = <
|
|
760
797
|
const Selection extends Record<string, SelectionAccess<any, Root>>,
|
|
761
798
|
const With extends ReadonlyArray<ComponentDescriptor> = [],
|
|
@@ -769,7 +806,7 @@ export const Query = <
|
|
|
769
806
|
>(spec: {
|
|
770
807
|
readonly selection: Selection
|
|
771
808
|
readonly with?: With
|
|
772
|
-
readonly without?: Without
|
|
809
|
+
readonly without?: Without & ExclusionsPossible<Selection, With, Without, Filters>
|
|
773
810
|
readonly filters?: Filters
|
|
774
811
|
readonly withRelations?: WithRelations
|
|
775
812
|
readonly withoutRelations?: WithoutRelations
|
package/src/Relation.ts
CHANGED
|
@@ -10,11 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```ts
|
|
13
|
-
* //
|
|
14
|
-
* const
|
|
15
|
-
*
|
|
16
|
-
* // Model another directed gameplay relationship.
|
|
17
|
-
* const Target = Relation.Relation("Target", "TargetedBy")
|
|
13
|
+
* // Relations are defined with Descriptor.Hierarchy and Descriptor.Relation.
|
|
14
|
+
* const { relation: ChildOf } = Descriptor.Hierarchy("ChildOf", "Children")
|
|
15
|
+
* const { relation: Targeting } = Descriptor.Relation("Targeting", "TargetedBy")
|
|
18
16
|
* ```
|
|
19
17
|
*
|
|
20
18
|
* @module Relation
|
|
@@ -44,7 +42,8 @@ const relationTypeId: RelationTypeId = "~ritmo/Relation"
|
|
|
44
42
|
|
|
45
43
|
export type RelationKind = "hierarchy" | "relation"
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
/** The nominal brand tying a relation to its reverse side. */
|
|
46
|
+
export type PairBrand<Name extends string, RelatedName extends string, Kind extends RelationKind> = {
|
|
48
47
|
readonly relation: Name
|
|
49
48
|
readonly related: RelatedName
|
|
50
49
|
readonly kind: Kind
|
|
@@ -281,67 +280,6 @@ export const mutationFailure = <
|
|
|
281
280
|
error
|
|
282
281
|
})
|
|
283
282
|
|
|
284
|
-
const makePair = <
|
|
285
|
-
const Name extends string,
|
|
286
|
-
const RelatedName extends string,
|
|
287
|
-
const Kind extends RelationKind
|
|
288
|
-
>(
|
|
289
|
-
kind: Kind,
|
|
290
|
-
name: Name,
|
|
291
|
-
relatedName: RelatedName
|
|
292
|
-
): {
|
|
293
|
-
readonly relation: RelationDefinition<Name, RelatedName, Kind, PairBrand<Name, RelatedName, Kind>>
|
|
294
|
-
readonly related: RelatedDefinition<RelatedName, Name, Kind, PairBrand<Name, RelatedName, Kind>>
|
|
295
|
-
} => {
|
|
296
|
-
type Pair = PairBrand<Name, RelatedName, Kind>
|
|
297
|
-
const related = {
|
|
298
|
-
kind: "related",
|
|
299
|
-
relatedName,
|
|
300
|
-
relationName: name,
|
|
301
|
-
key: Symbol.for(`ritmo/related/${name}`),
|
|
302
|
-
relationKind: kind,
|
|
303
|
-
[relationTypeId]: {
|
|
304
|
-
_Pair: (_: never) => undefined as unknown as Pair
|
|
305
|
-
}
|
|
306
|
-
} as RelatedDefinition<RelatedName, Name, Kind, Pair>
|
|
307
|
-
|
|
308
|
-
const relation = {
|
|
309
|
-
kind: "relation",
|
|
310
|
-
name,
|
|
311
|
-
relatedName,
|
|
312
|
-
key: Symbol.for(`ritmo/relation/${name}`),
|
|
313
|
-
relationKind: kind,
|
|
314
|
-
linkedDespawn: kind === "hierarchy",
|
|
315
|
-
allowSelf: false,
|
|
316
|
-
ordered: kind === "hierarchy",
|
|
317
|
-
related,
|
|
318
|
-
[relationTypeId]: {
|
|
319
|
-
_Pair: (_: never) => undefined as unknown as Pair
|
|
320
|
-
}
|
|
321
|
-
} as RelationDefinition<Name, RelatedName, Kind, Pair>
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
relation,
|
|
325
|
-
related
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export const Hierarchy = <
|
|
330
|
-
const Name extends string,
|
|
331
|
-
const RelatedName extends string
|
|
332
|
-
>(
|
|
333
|
-
name: Name,
|
|
334
|
-
relatedName: RelatedName
|
|
335
|
-
) => makePair("hierarchy", name, relatedName)
|
|
336
|
-
|
|
337
|
-
export const Relation = <
|
|
338
|
-
const Name extends string,
|
|
339
|
-
const RelatedName extends string
|
|
340
|
-
>(
|
|
341
|
-
name: Name,
|
|
342
|
-
relatedName: RelatedName
|
|
343
|
-
) => makePair("relation", name, relatedName)
|
|
344
|
-
|
|
345
283
|
export interface RelationReadAccess<
|
|
346
284
|
R extends Relation.Any,
|
|
347
285
|
S extends Schema.Any,
|