@typeonce/ritmo 0.6.0 → 0.7.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 +157 -33
- package/dist/Command.d.ts.map +1 -1
- package/dist/Command.js +49 -15
- package/dist/Command.js.map +1 -1
- package/dist/Debug.d.ts +104 -12
- 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 +31 -21
- package/dist/Descriptor.d.ts.map +1 -1
- package/dist/Descriptor.js +21 -12
- package/dist/Descriptor.js.map +1 -1
- package/dist/Entity.d.ts +14 -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 +6 -0
- package/dist/EntityScope.d.ts.map +1 -1
- package/dist/EntityScope.js +6 -0
- 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 +12 -3
- 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 +28 -24
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +0 -24
- package/dist/Query.js.map +1 -1
- package/dist/Runtime.d.ts +36 -16
- package/dist/Runtime.d.ts.map +1 -1
- package/dist/Runtime.js +230 -67
- 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 +61 -25
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +0 -8
- package/dist/Schema.js.map +1 -1
- package/dist/System.d.ts +149 -52
- package/dist/System.d.ts.map +1 -1
- package/dist/System.js +52 -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 +6 -0
- package/dist/internal/debug.d.ts.map +1 -1
- package/dist/internal/debug.js +95 -1
- package/dist/internal/debug.js.map +1 -1
- package/dist/internal/game.d.ts.map +1 -1
- package/dist/internal/game.js +9 -2
- 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 +120 -0
- package/dist/internal/order.js.map +1 -0
- package/dist/internal/queries.js +2 -2
- package/dist/internal/queries.js.map +1 -1
- package/package.json +1 -1
- package/src/Brand.ts +9 -4
- package/src/Clock.ts +311 -0
- package/src/Command.ts +271 -73
- package/src/Debug.ts +111 -10
- package/src/Decode.ts +1 -1
- package/src/Descriptor.ts +37 -27
- package/src/Entity.ts +15 -5
- package/src/EntityScope.ts +6 -0
- package/src/HostClock.ts +64 -0
- package/src/Inspector.ts +7 -2
- package/src/Machine.ts +13 -3
- package/src/Query.ts +41 -33
- package/src/Runtime.ts +273 -91
- package/src/Schedule.ts +380 -13
- package/src/Schema.ts +70 -41
- package/src/System.ts +174 -88
- package/src/index.ts +8 -7
- package/src/internal/cells.ts +10 -12
- package/src/internal/debug.ts +104 -4
- package/src/internal/game.ts +11 -3
- package/src/internal/order.ts +140 -0
- package/src/internal/queries.ts +3 -3
- 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/System.ts
CHANGED
|
@@ -57,7 +57,6 @@
|
|
|
57
57
|
*/
|
|
58
58
|
import type { Descriptor } from "./Descriptor.ts"
|
|
59
59
|
import type * as Entity from "./Entity.ts"
|
|
60
|
-
import type { Fx } from "./Fx.ts"
|
|
61
60
|
import * as Machine from "./Machine.ts"
|
|
62
61
|
import type * as Relation from "./Relation.ts"
|
|
63
62
|
import type { Query, QueryMatch } from "./Query.ts"
|
|
@@ -65,7 +64,7 @@ import type { ConstructedWriteCell, ReadCell, ReadonlyValue, WriteCell } from ".
|
|
|
65
64
|
import * as Requirement from "./Requirement.ts"
|
|
66
65
|
import type * as Result from "./Result.ts"
|
|
67
66
|
import type { Schema } from "./Schema.ts"
|
|
68
|
-
import type { CommandsApi } from "./Command.ts"
|
|
67
|
+
import type { CommandAccess, CommandsApi, NotDeclared } from "./Command.ts"
|
|
69
68
|
|
|
70
69
|
/**
|
|
71
70
|
* System declarations, typed execution context, and runtime requirements.
|
|
@@ -246,7 +245,7 @@ export const readEvent = <D extends Descriptor<"event", string, any>>(
|
|
|
246
245
|
*
|
|
247
246
|
* If the payload needs to name an entity for later work, emit a durable
|
|
248
247
|
* `Game.Entity.handle(...)` (optionally with an intent component) and let the later
|
|
249
|
-
* reader re-resolve it through `
|
|
248
|
+
* reader re-resolve it through `queries.x.getHandle(...)` (through a declared query).
|
|
250
249
|
*
|
|
251
250
|
* @example
|
|
252
251
|
* ```ts
|
|
@@ -266,6 +265,58 @@ export const writeEvent = <D extends Descriptor<"event", string, any>>(
|
|
|
266
265
|
descriptor
|
|
267
266
|
})
|
|
268
267
|
|
|
268
|
+
/** The marker `thisTick(...)` adds to a read. */
|
|
269
|
+
export interface ThisTick {
|
|
270
|
+
readonly thisTick: true
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Requires that every system writing this event or resource runs earlier in
|
|
275
|
+
* the same `runtime.tick(...)` call than the reading system: the reader then
|
|
276
|
+
* sees this tick's data, never last tick's. The library never reorders
|
|
277
|
+
* anything; it only checks.
|
|
278
|
+
*
|
|
279
|
+
* `runtime.tick(...)` checks it at compile time over everything passed to that
|
|
280
|
+
* call, in order (gated groups count by position), and rejects a writer that
|
|
281
|
+
* runs after the reader and a call whose order is not statically known. A
|
|
282
|
+
* read with no writer in the call compiles (a reader reused in several
|
|
283
|
+
* ticks); the `event-never-written` lint reports data nobody writes.
|
|
284
|
+
* `tryTick` checks the same at runtime.
|
|
285
|
+
*
|
|
286
|
+
* Use it on reads whose correctness depends on running after their writers
|
|
287
|
+
* (damage applied the tick it is dealt). A plain read is right when last
|
|
288
|
+
* tick's data is acceptable.
|
|
289
|
+
*
|
|
290
|
+
* On a query (`queries: { enemies: Game.System.thisTick(Enemies) }`), the
|
|
291
|
+
* writers are systems whose declared commands can change what it matches:
|
|
292
|
+
* `spawns` covering every component it requires, or `inserts` / `removes` of
|
|
293
|
+
* any component it names (selection slots, optional ones included, `with`,
|
|
294
|
+
* `without`, filters). Each must run earlier with an `applyDeferred()` (or
|
|
295
|
+
* `applyStateTransitions(...)`) between it and the reader, since commands
|
|
296
|
+
* apply only there. `despawns` never count: a despawned entity stays visible
|
|
297
|
+
* to queries until the next `applyDeferred()`. Nested schedules are
|
|
298
|
+
* flattened, gated groups count by position, and a transition bundle counts
|
|
299
|
+
* at its `applyStateTransitions(...)` step (which applies queued commands
|
|
300
|
+
* first), as if every schedule in it runs, markers inside included.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* const ApplyDamage = Game.System("ApplyDamage", {
|
|
305
|
+
* events: { damage: Game.System.thisTick(Game.System.readEvent(Damage)) }
|
|
306
|
+
* }, ({ events }) => { ... })
|
|
307
|
+
*
|
|
308
|
+
* runtime.tick(Game.Schedule(DealDamage, ApplyDamage)) // compiles
|
|
309
|
+
* runtime.tick(Game.Schedule(ApplyDamage, DealDamage)) // "ApplyDamage needs Damage from this tick, but its writer DealDamage runs after it"
|
|
310
|
+
*
|
|
311
|
+
* const AimAtNewest = Game.System("AimAtNewest", { queries: { enemies: Game.System.thisTick(Enemies) } }, ...)
|
|
312
|
+
* runtime.tick(Game.Schedule(SpawnWave, Game.Schedule.applyDeferred(), AimAtNewest)) // compiles
|
|
313
|
+
* runtime.tick(Game.Schedule(SpawnWave, AimAtNewest)) // "AimAtNewest needs this tick's commands for queries.enemies, but SpawnWave queues them with no applyDeferred() in between"
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
export const thisTick = <
|
|
317
|
+
Read extends EventRead<Descriptor<"event", string, any>> | ResourceRead<Descriptor<"resource", string, any>> | Query.Any<any>
|
|
318
|
+
>(read: Read): Read & ThisTick => ({ ...read, thisTick: true })
|
|
319
|
+
|
|
269
320
|
/**
|
|
270
321
|
* Declares read access to the current committed value of a finite-state machine.
|
|
271
322
|
*
|
|
@@ -307,7 +358,7 @@ export const transition = <M extends Machine.StateMachine.Any>(
|
|
|
307
358
|
* normal events: each run sees those published since its previous run.
|
|
308
359
|
*
|
|
309
360
|
* This is one of the clearest signs that the modeled value should be a machine
|
|
310
|
-
* rather than a
|
|
361
|
+
* rather than a resource or a `Descriptor.EntityState` component.
|
|
311
362
|
*/
|
|
312
363
|
export const readTransitionEvent = <M extends Machine.StateMachine.Any>(
|
|
313
364
|
stateMachine: M
|
|
@@ -430,8 +481,9 @@ export interface RelationFailureRead<R extends Relation.Relation.Any> {
|
|
|
430
481
|
* ticked less often than the one that removes (rendering after several fixed
|
|
431
482
|
* updates) still sees every removal. A system skipped by its run conditions
|
|
432
483
|
* keeps its position, like `added`/`changed`, and sees the removals when it
|
|
433
|
-
* runs again. Each log is capped at `Runtime.streamCapacity` entries;
|
|
434
|
-
*
|
|
484
|
+
* runs again. Each log is capped at `Runtime.streamCapacity` entries; a
|
|
485
|
+
* reader that missed dropped ones sees `lagged() === true` (and a `missed`
|
|
486
|
+
* read in the debug trace). Systems usually pair this
|
|
435
487
|
* with host cleanup such as removing renderer-owned nodes. {@link readDespawned}
|
|
436
488
|
* complements this for whole-entity teardown.
|
|
437
489
|
*
|
|
@@ -493,6 +545,14 @@ export const readDespawned = (): DespawnedRead => ({
|
|
|
493
545
|
*/
|
|
494
546
|
export interface RemovedReadView<S extends Schema.Any, Root = unknown> {
|
|
495
547
|
all(): ReadonlyArray<import("./Entity.ts").EntityId<S, Root>>
|
|
548
|
+
/**
|
|
549
|
+
* Whether records published since this system's previous run were dropped
|
|
550
|
+
* before it read them. Records wait for every reading system, so this only
|
|
551
|
+
* happens when a reader has not run for so long that the log exceeded its
|
|
552
|
+
* capacity (see `Runtime.streamCapacity`). Check it where a missed record
|
|
553
|
+
* would leak host state (a renderer node never destroyed).
|
|
554
|
+
*/
|
|
555
|
+
lagged(): boolean
|
|
496
556
|
}
|
|
497
557
|
|
|
498
558
|
/**
|
|
@@ -500,6 +560,14 @@ export interface RemovedReadView<S extends Schema.Any, Root = unknown> {
|
|
|
500
560
|
*/
|
|
501
561
|
export interface DespawnedReadView<S extends Schema.Any, Root = unknown> {
|
|
502
562
|
all(): ReadonlyArray<import("./Entity.ts").EntityId<S, Root>>
|
|
563
|
+
/**
|
|
564
|
+
* Whether records published since this system's previous run were dropped
|
|
565
|
+
* before it read them. Records wait for every reading system, so this only
|
|
566
|
+
* happens when a reader has not run for so long that the log exceeded its
|
|
567
|
+
* capacity (see `Runtime.streamCapacity`). Check it where a missed record
|
|
568
|
+
* would leak host state (a renderer node never destroyed).
|
|
569
|
+
*/
|
|
570
|
+
lagged(): boolean
|
|
503
571
|
}
|
|
504
572
|
|
|
505
573
|
/**
|
|
@@ -531,8 +599,46 @@ export interface QueryHandle<S extends Schema.Any, Q extends Query.Any> {
|
|
|
531
599
|
each(): ReadonlyArray<QueryMatch<S, Q>>
|
|
532
600
|
/**
|
|
533
601
|
* Retrieves the match for one specific entity id when it satisfies the query.
|
|
602
|
+
*
|
|
603
|
+
* Every read and write of an entity goes through a query the system
|
|
604
|
+
* declares, so its access is visible in the spec: there is no lookup that
|
|
605
|
+
* takes a query at call time.
|
|
534
606
|
*/
|
|
535
607
|
get(entityId: import("./Entity.ts").EntityId<S, Query.Root<Q>>): Result.Result<QueryMatch<S, Q>, Query.LookupError>
|
|
608
|
+
/**
|
|
609
|
+
* Resolves a stored durable handle back into this query's access.
|
|
610
|
+
*
|
|
611
|
+
* Use this for handles carried in events, resources, or components. A handle
|
|
612
|
+
* is storage-safe, not proof of liveness, so stale or mismatched handles stay
|
|
613
|
+
* explicit typed failures. A handle with an intent component needs a query
|
|
614
|
+
* that proves it (selects or requires it).
|
|
615
|
+
*/
|
|
616
|
+
getHandle<H extends Entity.Handle<Query.Root<Q>, any>>(
|
|
617
|
+
handle: [Entity.Handle.Intent<H>] extends [undefined]
|
|
618
|
+
? H
|
|
619
|
+
: Query.ProvesComponent<Q, Entity.Handle.Intent<H> & Descriptor<"component", string, any>> extends true
|
|
620
|
+
? H
|
|
621
|
+
: "this query does not prove the handle's intent component: select it, or add it to with"
|
|
622
|
+
): Result.Result<QueryMatch<S, Q>, Query.LookupError>
|
|
623
|
+
/**
|
|
624
|
+
* The direct children of one hierarchy parent that match this query, in the
|
|
625
|
+
* stored child order. Children that do not match are skipped; a missing
|
|
626
|
+
* parent is a `MissingEntity` failure.
|
|
627
|
+
*/
|
|
628
|
+
children<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
|
|
629
|
+
entityId: Entity.EntityId<S, Query.Root<Q>>,
|
|
630
|
+
relation: R
|
|
631
|
+
): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
|
|
632
|
+
/**
|
|
633
|
+
* The hierarchy descendants of one entity that match this query, in
|
|
634
|
+
* `options.order` (depth-first by default). Descendants that do not match
|
|
635
|
+
* are skipped. `lookup.descendants` returns every descendant's id instead.
|
|
636
|
+
*/
|
|
637
|
+
descendants<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
|
|
638
|
+
entityId: Entity.EntityId<S, Query.Root<Q>>,
|
|
639
|
+
relation: R,
|
|
640
|
+
options?: { readonly order?: "breadth" | "depth" }
|
|
641
|
+
): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
|
|
536
642
|
/**
|
|
537
643
|
* Returns a single match when exactly one entity satisfies the query.
|
|
538
644
|
*/
|
|
@@ -558,40 +664,19 @@ export interface QueryHandle<S extends Schema.Any, Q extends Query.Any> {
|
|
|
558
664
|
}
|
|
559
665
|
|
|
560
666
|
/**
|
|
561
|
-
*
|
|
667
|
+
* Relation lookups exposed to systems: entity ids along relation edges.
|
|
562
668
|
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
669
|
+
* Lookups return ids only. To read or write the entities they lead to, pass
|
|
670
|
+
* the ids to a declared query (`queries.x.get(id)`), or use the query
|
|
671
|
+
* handle's `children` / `descendants`, so every access stays in the spec.
|
|
565
672
|
*
|
|
566
673
|
* All lookup methods are total and explicit:
|
|
567
674
|
*
|
|
568
675
|
* - no hidden exceptions
|
|
569
|
-
* -
|
|
676
|
+
* - missing entities are normal typed failures
|
|
570
677
|
* - hierarchy traversal is available only for hierarchy relations
|
|
571
678
|
*/
|
|
572
679
|
export interface LookupApi<S extends Schema.Any, Root = unknown> {
|
|
573
|
-
get<Q extends Query.Any<Root>>(
|
|
574
|
-
entityId: Entity.EntityId<S, Root>,
|
|
575
|
-
query: Q
|
|
576
|
-
): Result.Result<QueryMatch<S, Q>, Query.LookupError>
|
|
577
|
-
/**
|
|
578
|
-
* Resolves a stored durable handle back into current-world query access.
|
|
579
|
-
*
|
|
580
|
-
* Use this for handles carried in events, resources, or components. A handle is
|
|
581
|
-
* storage-safe, not proof of liveness, so stale or mismatched handles remain
|
|
582
|
-
* explicit typed failures.
|
|
583
|
-
*/
|
|
584
|
-
getHandle<
|
|
585
|
-
H extends Entity.Handle<Root, any>,
|
|
586
|
-
Q extends Query.Any<Root>
|
|
587
|
-
>(
|
|
588
|
-
handle: [Entity.Handle.Intent<H>] extends [undefined]
|
|
589
|
-
? H
|
|
590
|
-
: Query.ProvesComponent<Q, Entity.Handle.Intent<H> & Descriptor<"component", string, any>> extends true
|
|
591
|
-
? H
|
|
592
|
-
: never,
|
|
593
|
-
query: Q
|
|
594
|
-
): Result.Result<QueryMatch<S, Q>, Query.LookupError>
|
|
595
680
|
related<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Any>>(
|
|
596
681
|
entityId: Entity.EntityId<S, Root>,
|
|
597
682
|
relation: R
|
|
@@ -600,21 +685,6 @@ export interface LookupApi<S extends Schema.Any, Root = unknown> {
|
|
|
600
685
|
entityId: Entity.EntityId<S, Root>,
|
|
601
686
|
relation: R
|
|
602
687
|
): Result.Result<ReadonlyArray<Entity.EntityId<S, Root>>, Relation.Relation.MissingEntityError>
|
|
603
|
-
/**
|
|
604
|
-
* Reads the direct children of one hierarchy parent as typed query matches.
|
|
605
|
-
*
|
|
606
|
-
* This preserves the stored child order for that hierarchy relation and skips
|
|
607
|
-
* entities that do not satisfy the query. Missing parents remain explicit
|
|
608
|
-
* `MissingEntity` failures.
|
|
609
|
-
*/
|
|
610
|
-
childMatches<
|
|
611
|
-
R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>,
|
|
612
|
-
Q extends Query.Any<Root>
|
|
613
|
-
>(
|
|
614
|
-
entityId: Entity.EntityId<S, Root>,
|
|
615
|
-
relation: R,
|
|
616
|
-
query: Q
|
|
617
|
-
): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
|
|
618
688
|
parent<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
|
|
619
689
|
entityId: Entity.EntityId<S, Root>,
|
|
620
690
|
relation: R
|
|
@@ -630,24 +700,6 @@ export interface LookupApi<S extends Schema.Any, Root = unknown> {
|
|
|
630
700
|
readonly order?: "breadth" | "depth"
|
|
631
701
|
}
|
|
632
702
|
): Result.Result<ReadonlyArray<Entity.EntityId<S, Root>>, Relation.Relation.MissingEntityError>
|
|
633
|
-
/**
|
|
634
|
-
* Traverses hierarchy descendants and resolves only the entities that match
|
|
635
|
-
* the given query.
|
|
636
|
-
*
|
|
637
|
-
* Traversal order stays explicit through `options.order`, and non-matching
|
|
638
|
-
* descendants are skipped without turning traversal into a failure.
|
|
639
|
-
*/
|
|
640
|
-
descendantMatches<
|
|
641
|
-
R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>,
|
|
642
|
-
Q extends Query.Any<Root>
|
|
643
|
-
>(
|
|
644
|
-
entityId: Entity.EntityId<S, Root>,
|
|
645
|
-
relation: R,
|
|
646
|
-
query: Q,
|
|
647
|
-
options?: {
|
|
648
|
-
readonly order?: "breadth" | "depth"
|
|
649
|
-
}
|
|
650
|
-
): Result.Result<ReadonlyArray<QueryMatch<S, Q>>, Relation.Relation.MissingEntityError>
|
|
651
703
|
root<R extends Extract<Schema.Relations<S>[keyof Schema.Relations<S>], Relation.Relation.Hierarchy>>(
|
|
652
704
|
entityId: Entity.EntityId<S, Root>,
|
|
653
705
|
relation: R
|
|
@@ -686,6 +738,25 @@ export interface SystemAccessInput {
|
|
|
686
738
|
readonly removed?: Record<string, RemovedRead<Descriptor<"component", string, any>>>
|
|
687
739
|
readonly despawned?: Record<string, DespawnedRead>
|
|
688
740
|
readonly relationFailures?: Record<string, RelationFailureRead<Relation.Relation.Any>>
|
|
741
|
+
/**
|
|
742
|
+
* The commands the system may queue: `Game.System.commands(...)` with
|
|
743
|
+
* `spawns`, `inserts`, `removes`, `despawns`, `despawnsScope`, and
|
|
744
|
+
* `relates` permissions. Without it the system has no `commands`.
|
|
745
|
+
*/
|
|
746
|
+
readonly commands?: CommandAccess.Any
|
|
747
|
+
/**
|
|
748
|
+
* Run conditions that skip the system: while one fails, the system does not
|
|
749
|
+
* run and the events, transition events, and relation failures published
|
|
750
|
+
* meanwhile are dropped for it. Use it for a mode change after which those
|
|
751
|
+
* messages are stale (menu, game over); use `pauseWhen` instead for a freeze
|
|
752
|
+
* whose messages stay valid. `Schedule.when(...)` adds conditions to a whole
|
|
753
|
+
* group.
|
|
754
|
+
*
|
|
755
|
+
* Four gating forms, one rule: `when` drops the events published while
|
|
756
|
+
* gated (the system resumes on fresh messages), `pauseWhen` keeps them (the
|
|
757
|
+
* system resumes and reads the backlog). Put them on one system's spec, or
|
|
758
|
+
* on a group with `Schedule.when(...)` / `Schedule.pauseWhen(...)`.
|
|
759
|
+
*/
|
|
689
760
|
readonly when?: ReadonlyArray<Machine.Condition>
|
|
690
761
|
/**
|
|
691
762
|
* Run conditions that pause the system instead of skipping it: while one
|
|
@@ -763,6 +834,7 @@ export type SystemSpec<
|
|
|
763
834
|
readonly pauseWhen: AccessField<Access, "pauseWhen", readonly []>
|
|
764
835
|
readonly transitions: AccessField<Access, "transitions", {}>
|
|
765
836
|
readonly local: LocalFactory<Access>
|
|
837
|
+
readonly commands: "commands" extends keyof Access ? NonNullable<Access["commands"]> : undefined
|
|
766
838
|
readonly schema: S
|
|
767
839
|
readonly __schemaRoot: Root
|
|
768
840
|
}
|
|
@@ -857,6 +929,11 @@ type QueryContext<Spec extends AnySystemSpec> = {
|
|
|
857
929
|
type LocalValue<Factory> = Factory extends () => infer Local ? Local : undefined
|
|
858
930
|
type LocalContext<Spec extends AnySystemSpec> = LocalValue<Spec["local"]>
|
|
859
931
|
|
|
932
|
+
type CommandsContext<Spec extends AnySystemSpec> =
|
|
933
|
+
Spec["commands"] extends CommandAccess<infer Spawns, infer Inserts, infer Removes, infer Despawns, infer Scopes, infer Relates>
|
|
934
|
+
? CommandsApi<Spec["schema"], Spec["__schemaRoot"], Spawns, Inserts, Removes, Despawns, Scopes, Relates>
|
|
935
|
+
: NotDeclared
|
|
936
|
+
|
|
860
937
|
/**
|
|
861
938
|
* The implementation context derived from a system spec.
|
|
862
939
|
*
|
|
@@ -876,16 +953,12 @@ export interface SystemContext<Spec extends AnySystemSpec> {
|
|
|
876
953
|
readonly relationFailures: RelationFailureContext<Spec>
|
|
877
954
|
readonly transitions: TransitionContext<Spec>
|
|
878
955
|
readonly services: ServiceContext<Spec>
|
|
879
|
-
|
|
956
|
+
/** The commands the spec declares (`commands: Game.System.commands(...)`). */
|
|
957
|
+
readonly commands: CommandsContext<Spec>
|
|
880
958
|
/** The system's own scratch state (see `local` in the spec); `undefined` when it declares none. */
|
|
881
959
|
readonly local: LocalContext<Spec>
|
|
882
960
|
}
|
|
883
961
|
|
|
884
|
-
/**
|
|
885
|
-
* The service environment required by a system.
|
|
886
|
-
*/
|
|
887
|
-
export type SystemDependencies<Spec extends AnySystemSpec> = ServiceContext<Spec>
|
|
888
|
-
|
|
889
962
|
type DescriptorFromAccess<Access> =
|
|
890
963
|
Access extends { readonly descriptor: infer D extends Requirement.Requirement } ? D : never
|
|
891
964
|
|
|
@@ -949,7 +1022,7 @@ export interface SystemFailure<
|
|
|
949
1022
|
|
|
950
1023
|
/** Extracts the normalized failure carried by one system definition. */
|
|
951
1024
|
export type FailureOf<Value> =
|
|
952
|
-
Value extends SystemDefinition<any,
|
|
1025
|
+
Value extends SystemDefinition<any, infer Error, any, infer Name, any>
|
|
953
1026
|
? [Error] extends [never] ? never : SystemFailure<Name, Error>
|
|
954
1027
|
: never
|
|
955
1028
|
|
|
@@ -958,7 +1031,6 @@ export type FailureOf<Value> =
|
|
|
958
1031
|
*/
|
|
959
1032
|
export interface SystemDefinition<
|
|
960
1033
|
Spec extends AnySystemSpec,
|
|
961
|
-
out A = void,
|
|
962
1034
|
out E = never,
|
|
963
1035
|
out Root = unknown,
|
|
964
1036
|
out Name extends string = string,
|
|
@@ -991,32 +1063,46 @@ export interface SystemDefinition<
|
|
|
991
1063
|
/**
|
|
992
1064
|
* The executable implementation of the system.
|
|
993
1065
|
*/
|
|
994
|
-
readonly run: SystemRun<Spec,
|
|
1066
|
+
readonly run: SystemRun<Spec, E>
|
|
995
1067
|
/**
|
|
996
1068
|
* The system this one is a gated copy of, made by `Schedule.when(...)`:
|
|
997
1069
|
* the same system with extra run conditions. The runtime keys per-system
|
|
998
1070
|
* state (change detection, event cursors) by the original, so a system and
|
|
999
1071
|
* its gated copies are one reader.
|
|
1000
1072
|
*/
|
|
1001
|
-
readonly base?: SystemDefinition<any, any, any, any, any
|
|
1073
|
+
readonly base?: SystemDefinition<any, any, any, any, any> | undefined
|
|
1002
1074
|
}
|
|
1003
1075
|
|
|
1004
1076
|
/**
|
|
1005
1077
|
* A system implementation.
|
|
1006
1078
|
*
|
|
1007
|
-
* Return nothing for a system that cannot fail. Return
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1079
|
+
* Return nothing for a system that cannot fail. Return
|
|
1080
|
+
* `Result.failure(error)` for an expected failure: the system's ECS writes
|
|
1081
|
+
* are rolled back, its commands and events are dropped, and the tick stops
|
|
1082
|
+
* with a typed `SystemFailure` whose `error` is inferred from every
|
|
1083
|
+
* `Result.failure(...)` the body returns. A returned success
|
|
1084
|
+
* (`Result.success(...)`) behaves like returning nothing. Exceptions are
|
|
1085
|
+
* defects, not failures: they roll back the same way and are rethrown.
|
|
1086
|
+
*
|
|
1087
|
+
* @example
|
|
1088
|
+
* ```ts
|
|
1089
|
+
* const Spend = Game.System("Spend", {
|
|
1090
|
+
* resources: { gold: Game.System.writeResource(Gold) }
|
|
1091
|
+
* }, ({ resources }) => {
|
|
1092
|
+
* if (resources.gold.get() < 10) return Result.failure("NotEnoughGold" as const)
|
|
1093
|
+
* resources.gold.update((gold) => gold - 10)
|
|
1094
|
+
* })
|
|
1095
|
+
* ```
|
|
1010
1096
|
*/
|
|
1011
|
-
export type SystemRun<Spec extends AnySystemSpec,
|
|
1012
|
-
(context: SystemContext<Spec>) =>
|
|
1097
|
+
export type SystemRun<Spec extends AnySystemSpec, E = never> =
|
|
1098
|
+
(context: SystemContext<Spec>) => Result.Result<unknown, E> | void
|
|
1013
1099
|
|
|
1014
1100
|
/**
|
|
1015
1101
|
* Defines a system from an explicit spec and a typed implementation.
|
|
1016
1102
|
*
|
|
1017
1103
|
* This is the public entrypoint for authoring systems. The implementation only
|
|
1018
|
-
* receives the capabilities declared in the spec
|
|
1019
|
-
*
|
|
1104
|
+
* receives the capabilities declared in the spec; return
|
|
1105
|
+
* `Result.failure(error)` for an expected failure (see {@link SystemRun}).
|
|
1020
1106
|
*
|
|
1021
1107
|
* Use the string-name overload in normal code. The name is turned into an
|
|
1022
1108
|
* internal ordering token automatically, so the system can participate in
|
|
@@ -1075,7 +1161,6 @@ const collectSystemRequirements = (
|
|
|
1075
1161
|
|
|
1076
1162
|
export function System<
|
|
1077
1163
|
const Access extends SystemAccessInput & { readonly schema: Schema.Any },
|
|
1078
|
-
A = void,
|
|
1079
1164
|
E = never,
|
|
1080
1165
|
Root = unknown,
|
|
1081
1166
|
const Name extends string = string
|
|
@@ -1084,14 +1169,14 @@ export function System<
|
|
|
1084
1169
|
spec: Access & {
|
|
1085
1170
|
readonly [Key in Exclude<keyof Access, keyof SystemAccessInput | "schema">]: never
|
|
1086
1171
|
},
|
|
1087
|
-
run: SystemRun<SystemSpec<Access["schema"], Access, Root>,
|
|
1088
|
-
): SystemDefinition<SystemSpec<Access["schema"], Access, Root>,
|
|
1172
|
+
run: SystemRun<SystemSpec<Access["schema"], Access, Root>, E>
|
|
1173
|
+
): SystemDefinition<SystemSpec<Access["schema"], Access, Root>, E, Root, Name, SystemAccessNeeds<Access>>
|
|
1089
1174
|
|
|
1090
1175
|
export function System(
|
|
1091
1176
|
name: string,
|
|
1092
1177
|
spec: { readonly schema: Schema.Any } & SystemAccessInput,
|
|
1093
|
-
run: (context: any) =>
|
|
1094
|
-
): SystemDefinition<any, any, any, any, any
|
|
1178
|
+
run: (context: any) => Result.Result<any, any> | void
|
|
1179
|
+
): SystemDefinition<any, any, any, any, any> {
|
|
1095
1180
|
type Spec = SystemSpec<Schema.Any, SystemAccessInput, unknown>
|
|
1096
1181
|
|
|
1097
1182
|
const normalizedSpec = {
|
|
@@ -1110,6 +1195,7 @@ export function System(
|
|
|
1110
1195
|
pauseWhen: spec.pauseWhen ?? [],
|
|
1111
1196
|
transitions: spec.transitions ?? {},
|
|
1112
1197
|
local: spec.local,
|
|
1198
|
+
commands: spec.commands,
|
|
1113
1199
|
__schemaRoot: undefined
|
|
1114
1200
|
} as Spec
|
|
1115
1201
|
|
|
@@ -1123,5 +1209,5 @@ export function System(
|
|
|
1123
1209
|
},
|
|
1124
1210
|
spec: normalizedSpec,
|
|
1125
1211
|
run
|
|
1126
|
-
} as SystemDefinition<any, any, any, any, any
|
|
1212
|
+
} as SystemDefinition<any, any, any, any, any>
|
|
1127
1213
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* small number of stable namespaces instead of many deep internal paths.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
-
* Branding helpers for validated
|
|
8
|
+
* Branding helpers for packages that author validated value types (like
|
|
9
|
+
* `@typeonce/ritmo-math`). Game code validates through descriptors instead.
|
|
9
10
|
*/
|
|
10
11
|
export * as Brand from "./Brand.ts"
|
|
11
12
|
/**
|
|
@@ -17,13 +18,13 @@ export * as Decode from "./Decode.ts"
|
|
|
17
18
|
*/
|
|
18
19
|
export * as Debug from "./Debug.ts"
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
+
* Simulation time: the fixed step, the clock resource, durations in whole steps.
|
|
21
22
|
*/
|
|
22
|
-
export * as
|
|
23
|
+
export * as Clock from "./Clock.ts"
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
+
* Descriptor authoring helpers.
|
|
25
26
|
*/
|
|
26
|
-
export * as
|
|
27
|
+
export * as Descriptor from "./Descriptor.ts"
|
|
27
28
|
/**
|
|
28
29
|
* Entity identity and proof helpers.
|
|
29
30
|
*/
|
|
@@ -33,9 +34,9 @@ export * as Entity from "./Entity.ts"
|
|
|
33
34
|
*/
|
|
34
35
|
export * as EntityScope from "./EntityScope.ts"
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
+
* The host's wall clock as an injected contract (`standard`, `test()`).
|
|
37
38
|
*/
|
|
38
|
-
export * as
|
|
39
|
+
export * as HostClock from "./HostClock.ts"
|
|
39
40
|
/**
|
|
40
41
|
* Read-only typed world projections.
|
|
41
42
|
*/
|
package/src/internal/cells.ts
CHANGED
|
@@ -18,7 +18,8 @@ abstract class WriteCellBase<T> {
|
|
|
18
18
|
abstract get(): T
|
|
19
19
|
abstract set(value: T): void
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/** Writes a validated candidate only on success. Backs `setRaw` / `updateRaw`. */
|
|
22
|
+
writeResult<E>(result: Result.Result<T, E>): Result.Result<void, E> {
|
|
22
23
|
if (!result.ok) {
|
|
23
24
|
return Result.failure(result.error)
|
|
24
25
|
}
|
|
@@ -30,9 +31,6 @@ abstract class WriteCellBase<T> {
|
|
|
30
31
|
this.set(f(this.get()))
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
updateResult<E>(f: (current: T) => Result.Result<T, E>): Result.Result<void, E> {
|
|
34
|
-
return this.setResult(f(this.get()))
|
|
35
|
-
}
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
/**
|
|
@@ -43,22 +41,22 @@ const withConstructor = <T, Raw, Error>(
|
|
|
43
41
|
constructor: ResultConstructor<T, Raw, Error>
|
|
44
42
|
) => Object.assign(cell, {
|
|
45
43
|
setRaw(raw: Raw): Result.Result<void, Error> {
|
|
46
|
-
return cell.
|
|
44
|
+
return cell.writeResult(constructor.result(raw))
|
|
47
45
|
},
|
|
48
46
|
updateRaw(f: (current: T) => Raw): Result.Result<void, Error> {
|
|
49
|
-
return cell.
|
|
47
|
+
return cell.writeResult(constructor.result(f(cell.get())))
|
|
50
48
|
}
|
|
51
49
|
})
|
|
52
50
|
|
|
53
51
|
/**
|
|
54
|
-
* Adds `transition` to the write cell of
|
|
52
|
+
* Adds `transition` to the write cell of an entity state component: a compare-and-set
|
|
55
53
|
* that writes `to` only while the current state is still `from`.
|
|
56
54
|
*/
|
|
57
|
-
const withTransition = (cell: WriteCellBase<unknown>,
|
|
58
|
-
transition(from: unknown, to: unknown): Result.Result<void, { readonly _tag: "
|
|
55
|
+
const withTransition = (cell: WriteCellBase<unknown>, component: string) => Object.assign(cell, {
|
|
56
|
+
transition(from: unknown, to: unknown): Result.Result<void, { readonly _tag: "EntityStateMismatch"; readonly component: string; readonly expected: unknown; readonly actual: unknown }> {
|
|
59
57
|
const actual = cell.get()
|
|
60
58
|
if (actual !== from) {
|
|
61
|
-
return Result.failure({ _tag: "
|
|
59
|
+
return Result.failure({ _tag: "EntityStateMismatch", component, expected: from, actual })
|
|
62
60
|
}
|
|
63
61
|
cell.set(to)
|
|
64
62
|
return Result.success(undefined)
|
|
@@ -162,11 +160,11 @@ export const componentWrite = (
|
|
|
162
160
|
ordinal: number,
|
|
163
161
|
world: World,
|
|
164
162
|
constructor: ResultConstructor<unknown, unknown, unknown> | undefined,
|
|
165
|
-
|
|
163
|
+
entityState: string | undefined
|
|
166
164
|
): WriteCellBase<unknown> => {
|
|
167
165
|
const cell = new ComponentWriteCell(record, ordinal, world)
|
|
168
166
|
const constructed = constructor === undefined ? cell : withConstructor(cell, constructor)
|
|
169
|
-
return
|
|
167
|
+
return entityState === undefined ? constructed : withTransition(constructed, entityState)
|
|
170
168
|
}
|
|
171
169
|
|
|
172
170
|
export const componentOptional = (record: EntityRecord, ordinal: number): { readonly present: boolean; get(): unknown } =>
|