@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/Clock.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulation time: a fixed step, a clock resource the schedule advances, and
|
|
3
|
+
* durations in whole steps.
|
|
4
|
+
*
|
|
5
|
+
* - `Clock.hz(60)` is the step (`dt = 1/60`). The host loop and the clock
|
|
6
|
+
* share it: `FixedLoop.start({ step, ... })`.
|
|
7
|
+
* - `Clock.resource(name, step)` is the resource holding the time
|
|
8
|
+
* (`{ step, hz, dt }`), and `Clock.start(step)` its first value.
|
|
9
|
+
* - `Clock.system(Game, { name, resource, hold })` advances it every tick:
|
|
10
|
+
* `step + 1` and `dt = 1/hz`, or `dt = 0` while a `hold` condition passes.
|
|
11
|
+
* - `Clock.duration(step, 0.5)` is a span in whole steps; `Clock.every(t, d)`
|
|
12
|
+
* is true on the step each period begins; `Clock.elapsed(t)` is exact.
|
|
13
|
+
*
|
|
14
|
+
* Time is counted in steps, never accumulated in floating point: elapsed
|
|
15
|
+
* seconds are `step / hz`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const step = Clock.hz(60)
|
|
20
|
+
* const Time = Clock.resource("Game/Time", step)
|
|
21
|
+
* const Game = Schema.bind(Schema.fragment({ resources: { Time } }))
|
|
22
|
+
* const AdvanceTime = Clock.system(Game, { name: "Game/AdvanceTime", resource: Time, hold: [paused] })
|
|
23
|
+
* const runtime = Game.Runtime.make({ services, resources: { Time: Clock.start(step) } })
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @module Clock
|
|
27
|
+
* @docGroup runtime
|
|
28
|
+
*/
|
|
29
|
+
import type { Descriptor } from "./Descriptor.ts"
|
|
30
|
+
import * as DescriptorModule from "./Descriptor.ts"
|
|
31
|
+
import type { Condition, ConditionNeeds, NotCondition, OrCondition } from "./Machine.ts"
|
|
32
|
+
import * as Result from "./Result.ts"
|
|
33
|
+
import type * as Schedule from "./Schedule.ts"
|
|
34
|
+
import type { Schema } from "./Schema.ts"
|
|
35
|
+
import type * as System from "./System.ts"
|
|
36
|
+
|
|
37
|
+
// ── The step ────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
declare const stepBrand: unique symbol
|
|
40
|
+
declare const durationBrand: unique symbol
|
|
41
|
+
declare const resourceBrand: unique symbol
|
|
42
|
+
type IsUnion<T, Whole = T> = T extends Whole ? [Whole] extends [T] ? false : true : never
|
|
43
|
+
|
|
44
|
+
/** A fixed step: `hz` steps per second, `dt = 1 / hz` seconds each. */
|
|
45
|
+
export interface Step<Hz extends number = number> {
|
|
46
|
+
readonly [stepBrand]: Hz
|
|
47
|
+
readonly kind: "ClockStep"
|
|
48
|
+
readonly hz: Hz
|
|
49
|
+
readonly dt: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type HzMessage = "Clock.hz needs a positive whole number of steps per second"
|
|
53
|
+
|
|
54
|
+
/** `N` when it is a literal positive whole number; otherwise the message naming what is wrong. */
|
|
55
|
+
export type ValidHz<N extends number> =
|
|
56
|
+
number extends N ? "Clock.hz needs a literal"
|
|
57
|
+
: true extends IsUnion<N> ? "Clock.hz needs one literal rate"
|
|
58
|
+
: `${N}` extends `-${string}` | `${string}.${string}` | `${string}e${string}` | "0" ? HzMessage
|
|
59
|
+
: AtLeast<Trimmed<Reversed<`${N}`>>, Reversed<"9007199254740992">> extends true ? HzMessage
|
|
60
|
+
: N
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The step of `hz` steps per second. `hz` must be a positive whole literal
|
|
64
|
+
* (`Clock.hz(60)`); anything else is a compile error, and the same check
|
|
65
|
+
* runs at runtime (it throws, which only a cast can reach).
|
|
66
|
+
*/
|
|
67
|
+
export const hz = <const N extends number>(hz: ValidHz<N>): Step<N> => {
|
|
68
|
+
const value = hz as number
|
|
69
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`Clock.hz needs a positive whole number of steps per second, got ${value}`)
|
|
70
|
+
return { kind: "ClockStep", hz: value as N, dt: 1 / value } as Step<N>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── The time value and its resource ─────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/** The clock: steps advanced since the start, the rate, and this tick's seconds. */
|
|
76
|
+
export interface Time<Hz extends number = number> {
|
|
77
|
+
/** Steps advanced since the start (held ticks do not count). */
|
|
78
|
+
readonly step: number
|
|
79
|
+
readonly hz: Hz
|
|
80
|
+
/** Seconds this tick: `1 / hz` when the clock advanced, 0 while held. */
|
|
81
|
+
readonly dt: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A saved or provided clock value that does not fit this game's clock. */
|
|
85
|
+
export interface InvalidTime {
|
|
86
|
+
readonly _tag: "InvalidTime"
|
|
87
|
+
readonly message: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The first value of a clock: step 0, nothing elapsed yet. */
|
|
91
|
+
export const start = <Hz extends number>(step: Step<Hz>): Time<Hz> => ({ step: 0, hz: step.hz, dt: 0 })
|
|
92
|
+
|
|
93
|
+
const timeOf = <Hz extends number>(step: Step<Hz>, raw: unknown): Result.Result<Time<Hz>, InvalidTime> => {
|
|
94
|
+
if (typeof raw !== "object" || raw === null) return Result.failure({ _tag: "InvalidTime", message: "a clock value is an object { step, hz, dt }" })
|
|
95
|
+
const value = raw as { readonly step?: unknown; readonly hz?: unknown; readonly dt?: unknown }
|
|
96
|
+
if (value.hz !== step.hz) {
|
|
97
|
+
return Result.failure({
|
|
98
|
+
_tag: "InvalidTime",
|
|
99
|
+
message: typeof value.hz === "number" ? `saved at ${value.hz} Hz, this game runs at ${step.hz} Hz` : `hz must be ${step.hz}`
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
if (typeof value.step !== "number" || !Number.isSafeInteger(value.step) || value.step < 0) {
|
|
103
|
+
return Result.failure({ _tag: "InvalidTime", message: "step must be a whole number of steps, 0 or more" })
|
|
104
|
+
}
|
|
105
|
+
if (value.dt !== 0 && value.dt !== step.dt) return Result.failure({ _tag: "InvalidTime", message: `dt must be 0 or 1/${step.hz}` })
|
|
106
|
+
return Result.success({ step: value.step, hz: step.hz, dt: value.dt })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The descriptor `Clock.resource` returns: a validated resource holding `Time<Hz>`. */
|
|
110
|
+
export type ClockResource<Name extends string, Hz extends number> =
|
|
111
|
+
DescriptorModule.DecodableDescriptor<"resource", Name, Time<Hz>, Time<Hz>, InvalidTime> & { readonly [resourceBrand]: Hz }
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A resource holding a clock that runs at `step`. Its validator rejects a
|
|
115
|
+
* value of another rate, so restoring a save made at another step length
|
|
116
|
+
* fails (`InvalidResource` naming it: "saved at 60 Hz, this game runs at
|
|
117
|
+
* 120 Hz"). A game may hold several clocks under different names.
|
|
118
|
+
*
|
|
119
|
+
* It is a validated resource, so a runtime given its value
|
|
120
|
+
* (`resources: { Time: Clock.start(step) }`) is made as a `Result`, like any
|
|
121
|
+
* runtime given validated resources.
|
|
122
|
+
*/
|
|
123
|
+
export const resource = <const Name extends string, Hz extends number>(name: Name, step: Step<Hz>): ClockResource<Name, Hz> =>
|
|
124
|
+
DescriptorModule.ConstructedResource({
|
|
125
|
+
result: (raw: Time<Hz>) => timeOf(step, raw),
|
|
126
|
+
decode: (raw: unknown) => timeOf(step, raw)
|
|
127
|
+
})(name) as unknown as ClockResource<Name, Hz>
|
|
128
|
+
|
|
129
|
+
/** Seconds of simulation time since the start: `step / hz`, exact (no accumulated floats). */
|
|
130
|
+
export const elapsed = (time: Time): number => time.step / time.hz
|
|
131
|
+
|
|
132
|
+
// ── The system ──────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
type ClockAccess<R, When extends ReadonlyArray<Condition>> = {
|
|
135
|
+
readonly resources: { readonly clock: System.ResourceWrite<R & Descriptor<"resource", string, any>> }
|
|
136
|
+
readonly when: When
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
type ClockSystem<S extends Schema.Any, Root, R, Name extends string, When extends ReadonlyArray<Condition>> =
|
|
140
|
+
Schema.BoundSystem<S, Root, System.SystemSpec<S, ClockAccess<R, When>, Root>, never, Name, System.SystemAccessNeeds<ClockAccess<R, When>>>
|
|
141
|
+
|
|
142
|
+
type HeldWhen<Hold extends ReadonlyArray<Condition>> = readonly [OrCondition<Hold, ConditionNeeds<Hold[number]>>]
|
|
143
|
+
type RunsWhen<Hold extends ReadonlyArray<Condition>> =
|
|
144
|
+
readonly [NotCondition<OrCondition<Hold, ConditionNeeds<Hold[number]>>, ConditionNeeds<Hold[number]>>]
|
|
145
|
+
|
|
146
|
+
/** What `Clock.system` builds: the clock's two writers, in order. */
|
|
147
|
+
export type ClockSchedule<S extends Schema.Any, Root, R, Name extends string, Hold extends ReadonlyArray<Condition>> =
|
|
148
|
+
Schedule.AnonymousScheduleBuildFor<S, readonly [
|
|
149
|
+
ClockSystem<S, Root, R, `${Name}/Hold`, HeldWhen<Hold>>,
|
|
150
|
+
ClockSystem<S, Root, R, Name, RunsWhen<Hold>>
|
|
151
|
+
], Root>
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Advances a clock resource every tick: while any `hold` condition passes
|
|
155
|
+
* (a pause, a menu), time stands still (`dt = 0`, `every` false); otherwise
|
|
156
|
+
* `step + 1` and `dt = 1 / hz`. A plugin: list it in the schedule like any
|
|
157
|
+
* system, before every system that reads the time this tick.
|
|
158
|
+
*
|
|
159
|
+
* It is two systems: `<name>/Hold` sets `dt` to 0 while held, and `<name>`
|
|
160
|
+
* advances otherwise. Gating one system with `when` instead would leave the
|
|
161
|
+
* last advancing tick's `dt` (and `every`) in place for the whole pause.
|
|
162
|
+
*/
|
|
163
|
+
export const system = <
|
|
164
|
+
S extends Schema.Any,
|
|
165
|
+
Root,
|
|
166
|
+
const Name extends string,
|
|
167
|
+
R extends Schema.ResourceDescriptor<S> & ClockResource<any, any>,
|
|
168
|
+
const Hold extends ReadonlyArray<Condition<Root>> = readonly []
|
|
169
|
+
>(
|
|
170
|
+
Game: Schema.Game<S, Root>,
|
|
171
|
+
options: { readonly name: Name; readonly resource: R; readonly hold?: Hold }
|
|
172
|
+
): ClockSchedule<S, Root, R, Name, Hold> => {
|
|
173
|
+
// Internally the descriptors are erased; the result type above carries the exact access.
|
|
174
|
+
const Sys = Game.System as unknown as (name: string, spec: object, run: (context: { readonly resources: { readonly clock: { update: (f: (time: Time) => Time) => void } } }) => void) => unknown
|
|
175
|
+
const Build = Game.Schedule as unknown as (...entries: ReadonlyArray<unknown>) => unknown
|
|
176
|
+
const held = Game.Condition.or(...(options.hold ?? []))
|
|
177
|
+
const write = { clock: Game.System.writeResource(options.resource) }
|
|
178
|
+
const Hold = Sys(`${options.name}/Hold`, { resources: write, when: [held] }, ({ resources }) => {
|
|
179
|
+
resources.clock.update((time) => time.dt === 0 ? time : { step: time.step, hz: time.hz, dt: 0 })
|
|
180
|
+
})
|
|
181
|
+
const Advance = Sys(options.name, { resources: write, when: [Game.Condition.not(held)] }, ({ resources }) => {
|
|
182
|
+
resources.clock.update((time) => ({ step: time.step + 1, hz: time.hz, dt: 1 / time.hz }))
|
|
183
|
+
})
|
|
184
|
+
return Build(Hold, Advance) as ClockSchedule<S, Root, R, Name, Hold>
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Durations ───────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
/** A span of simulation time in whole steps of one clock rate. */
|
|
190
|
+
export interface Duration<Hz extends number = number> {
|
|
191
|
+
readonly [durationBrand]: Hz
|
|
192
|
+
readonly kind: "ClockDuration"
|
|
193
|
+
readonly steps: number
|
|
194
|
+
readonly hz: Hz
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Why `durationRaw` refused a number of seconds. */
|
|
198
|
+
export interface InvalidDuration {
|
|
199
|
+
readonly _tag: "InvalidDuration"
|
|
200
|
+
readonly seconds: number
|
|
201
|
+
readonly message: string
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Type-level decimal arithmetic for literal seconds: digits as reversed tuples.
|
|
205
|
+
type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
|
|
206
|
+
type Units<D extends Digit> = {
|
|
207
|
+
"0": []; "1": [0]; "2": [0, 0]; "3": [0, 0, 0]; "4": [0, 0, 0, 0]; "5": [0, 0, 0, 0, 0]
|
|
208
|
+
"6": [0, 0, 0, 0, 0, 0]; "7": [0, 0, 0, 0, 0, 0, 0]; "8": [0, 0, 0, 0, 0, 0, 0, 0]; "9": [0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
209
|
+
}[D]
|
|
210
|
+
type Reversed<S extends string, Out extends Array<Digit> = []> =
|
|
211
|
+
S extends `${infer Head extends Digit}${infer Tail}` ? Reversed<Tail, [Head, ...Out]> : Out
|
|
212
|
+
type SumDigit<A extends Digit, B extends Digit, Carry extends 0 | 1> =
|
|
213
|
+
`${[...Units<A>, ...Units<B>, ...(Carry extends 1 ? [0] : [])]["length"] & number}` extends infer Sum extends string
|
|
214
|
+
? Sum extends `1${infer Low extends Digit}` ? [Low, 1] : Sum extends Digit ? [Sum, 0] : never
|
|
215
|
+
: never
|
|
216
|
+
type Add<A extends Array<Digit>, B extends Array<Digit>, Carry extends 0 | 1 = 0, Out extends Array<Digit> = []> =
|
|
217
|
+
[A, B] extends [[], []] ? Carry extends 1 ? [...Out, "1"] : Out
|
|
218
|
+
: SumDigit<A extends [infer X extends Digit, ...Array<Digit>] ? X : "0", B extends [infer Y extends Digit, ...Array<Digit>] ? Y : "0", Carry> extends [infer D extends Digit, infer C extends 0 | 1]
|
|
219
|
+
? Add<A extends [Digit, ...infer RA extends Array<Digit>] ? RA : [], B extends [Digit, ...infer RB extends Array<Digit>] ? RB : [], C, [...Out, D]>
|
|
220
|
+
: never
|
|
221
|
+
type Times<A extends Array<Digit>, Count extends Array<0>, Out extends Array<Digit> = []> =
|
|
222
|
+
Count extends [0, ...infer Rest extends Array<0>]
|
|
223
|
+
? Add<Out, A> extends infer Sum extends Array<Digit> ? Times<A, Rest, Sum> : never
|
|
224
|
+
: Out
|
|
225
|
+
type Multiply<A extends Array<Digit>, B extends Array<Digit>, Shift extends Array<Digit> = [], Out extends Array<Digit> = []> =
|
|
226
|
+
B extends [infer D extends Digit, ...infer Rest extends Array<Digit>]
|
|
227
|
+
? Times<A, Units<D>> extends infer Partial extends Array<Digit>
|
|
228
|
+
? Add<Out, [...Shift, ...Partial]> extends infer Sum extends Array<Digit>
|
|
229
|
+
? Multiply<A, Rest, ["0", ...Shift], Sum>
|
|
230
|
+
: never
|
|
231
|
+
: never
|
|
232
|
+
: Out
|
|
233
|
+
type Trimmed<A extends Array<Digit>> = A extends [...infer Rest extends Array<Digit>, "0"] ? Trimmed<Rest> : A
|
|
234
|
+
/** Whether reversed `A` >= reversed `B` (both trimmed of leading zeros). */
|
|
235
|
+
type AtLeast<A extends Array<Digit>, B extends Array<Digit>> =
|
|
236
|
+
A["length"] extends B["length"] ? MostSignificantAtLeast<A, B> : LongerThan<A, B>
|
|
237
|
+
/** Whether `A` has more elements than `B`. */
|
|
238
|
+
type LongerThan<A extends Array<unknown>, B extends Array<unknown>> =
|
|
239
|
+
B extends [unknown, ...infer RB] ? A extends [unknown, ...infer RA] ? LongerThan<RA, RB> : false : A extends [] ? false : true
|
|
240
|
+
/** Equal lengths: compare from the most significant digit (the end of a reversed tuple). */
|
|
241
|
+
type MostSignificantAtLeast<A extends Array<Digit>, B extends Array<Digit>> =
|
|
242
|
+
[A, B] extends [[...infer RA extends Array<Digit>, infer X extends Digit], [...infer RB extends Array<Digit>, infer Y extends Digit]]
|
|
243
|
+
? X extends Y ? MostSignificantAtLeast<RA, RB> : LongerThan<Units<X>, Units<Y>>
|
|
244
|
+
: true
|
|
245
|
+
type ZerosAfterOne<Fraction extends string, Out extends Array<Digit> = []> =
|
|
246
|
+
Fraction extends `${Digit}${infer Rest}` ? ZerosAfterOne<Rest, ["0", ...Out]> : [...Out, "1"]
|
|
247
|
+
|
|
248
|
+
/** Whether `0.Fraction` seconds at `Hz` steps per second is at least one step: `Fraction * Hz >= 10^digits`. */
|
|
249
|
+
type FractionReachesStep<Fraction extends string, Hz extends number> =
|
|
250
|
+
AtLeast<Trimmed<Multiply<Reversed<Fraction>, Reversed<`${Hz}`>>>, ZerosAfterOne<Fraction>>
|
|
251
|
+
|
|
252
|
+
type MantissaDigits<S extends string> = S extends `${infer Whole}.${infer Fraction}` ? `${Whole}${Fraction}` : S
|
|
253
|
+
type ScientificFraction<Mantissa extends string, Exponent extends string, Count extends Array<0> = [0], Zeros extends string = ""> =
|
|
254
|
+
`${Count["length"]}` extends Exponent ? `${Zeros}${MantissaDigits<Mantissa>}`
|
|
255
|
+
: ScientificFraction<Mantissa, Exponent, [...Count, 0], `${Zeros}0`>
|
|
256
|
+
|
|
257
|
+
/** `Seconds` when it is a literal, non-negative, and at least one step at `Hz`; otherwise the message. */
|
|
258
|
+
export type ValidDuration<Hz extends number, Seconds extends number> =
|
|
259
|
+
number extends Seconds ? "Clock.duration needs a literal number of seconds: use Clock.durationRaw for data"
|
|
260
|
+
: true extends IsUnion<Seconds> ? "Clock.duration needs one literal duration: use Clock.durationRaw for data"
|
|
261
|
+
: number extends Hz ? "Clock.duration needs a step built with Clock.hz(literal)"
|
|
262
|
+
: `${Seconds}` extends `-${string}` ? "Clock.duration needs a non-negative number of seconds"
|
|
263
|
+
: `${Seconds}` extends "0" ? `${Seconds}s is shorter than one step (1/${Hz}s)`
|
|
264
|
+
: `${Seconds}` extends `${string}e+${string}` ? "a duration must fit in a safe whole number of steps"
|
|
265
|
+
: `${Seconds}` extends `${infer Mantissa}e-${infer Exponent}`
|
|
266
|
+
? FractionReachesStep<ScientificFraction<Mantissa, Exponent>, Hz> extends true ? Seconds : `${Seconds}s is shorter than one step (1/${Hz}s)`
|
|
267
|
+
: `${Seconds}` extends `0.${infer Fraction}`
|
|
268
|
+
? FractionReachesStep<Fraction, Hz> extends true ? Seconds : `${Seconds}s is shorter than one step (1/${Hz}s)`
|
|
269
|
+
: Seconds
|
|
270
|
+
|
|
271
|
+
const stepsOf = (hz: number, seconds: number): Result.Result<number, InvalidDuration> => {
|
|
272
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
273
|
+
return Result.failure({ _tag: "InvalidDuration", seconds, message: "a duration needs a finite, non-negative number of seconds" })
|
|
274
|
+
}
|
|
275
|
+
// Exact check first (at least one whole step), then round to the nearest whole step, half up.
|
|
276
|
+
if (seconds * hz < 1 - 1e-9) {
|
|
277
|
+
return Result.failure({ _tag: "InvalidDuration", seconds, message: `${seconds}s is shorter than one step (1/${hz}s)` })
|
|
278
|
+
}
|
|
279
|
+
const steps = Math.max(1, Math.round(seconds * hz))
|
|
280
|
+
if (!Number.isSafeInteger(steps)) return Result.failure({ _tag: "InvalidDuration", seconds, message: "a duration must fit in a safe whole number of steps" })
|
|
281
|
+
return Result.success(steps)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A literal number of seconds as whole steps of `step`: at least one step,
|
|
286
|
+
* rounded to the nearest whole step (half up), so `Clock.duration(Clock.hz(60), 0.025)`
|
|
287
|
+
* is 2 steps (1.5 rounds up). Shorter than one step is a compile error
|
|
288
|
+
* ("0.001s is shorter than one step (1/60s)"). For seconds read from data,
|
|
289
|
+
* use `durationRaw`.
|
|
290
|
+
*/
|
|
291
|
+
export const duration = <Hz extends number, const Seconds extends number>(step: Step<Hz>, seconds: ValidDuration<Hz, Seconds>): Duration<Hz> => {
|
|
292
|
+
const steps = stepsOf(step.hz, seconds as number)
|
|
293
|
+
if (!steps.ok) throw new Error(steps.error.message)
|
|
294
|
+
return { kind: "ClockDuration", steps: steps.value, hz: step.hz } as Duration<Hz>
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Seconds from data as whole steps of `step`, the same rules as `duration`, as a `Result`. */
|
|
298
|
+
export const durationRaw = <Hz extends number>(step: Step<Hz>, seconds: number): Result.Result<Duration<Hz>, InvalidDuration> => {
|
|
299
|
+
const steps = stepsOf(step.hz, seconds)
|
|
300
|
+
return steps.ok ? Result.success({ kind: "ClockDuration", steps: steps.value, hz: step.hz } as Duration<Hz>) : steps
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The whole steps a duration spans. */
|
|
304
|
+
export const steps = (duration: Duration): number => duration.steps
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Whether a period of `duration` begins on this tick: the clock advanced
|
|
308
|
+
* (not held) to a step that is a multiple of the period. False while held.
|
|
309
|
+
*/
|
|
310
|
+
export const every = <Hz extends number>(time: Time<Hz>, duration: Duration<NoInfer<Hz>>): boolean =>
|
|
311
|
+
time.dt > 0 && time.step % duration.steps === 0
|