@johnmorrisdotca/uchusen-rockets 0.10.0 → 0.11.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.
@@ -0,0 +1,115 @@
1
+ import type { BodySource } from "./bodies.ts";
2
+ import { type Phase, type Trip, type Vec3 } from "./trip.ts";
3
+ /**
4
+ * How fast the screen plays: a multiple of normal speed.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import type { PlaySpeed } from "@johnmorrisdotca/uchusen-rockets/journey";
9
+ *
10
+ * const fast: PlaySpeed = 4;
11
+ * console.log(fast);
12
+ * ```
13
+ */
14
+ export type PlaySpeed = 0.5 | 1 | 2 | 4;
15
+ /**
16
+ * Where in a trip a moment of playback is: the phase, how far through it, the mission time and how many mission seconds each screen second shows.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import type { Place } from "@johnmorrisdotca/uchusen-rockets/journey";
21
+ *
22
+ * const words = (place: Place) => `${place.phase.id} ${Math.round(place.fraction * 100)} %`;
23
+ * console.log(typeof words);
24
+ * ```
25
+ */
26
+ export interface Place {
27
+ phase: Phase;
28
+ index: number;
29
+ /** How far through the phase, 0 to 1. */
30
+ fraction: number;
31
+ /** Mission seconds from lift-off (negative before). */
32
+ missionS: number;
33
+ /** Mission seconds shown per second on the screen in this phase. */
34
+ scale: number;
35
+ }
36
+ /**
37
+ * What is true of the craft at a moment: its place, speed, acceleration, the distances, the clocks, the signal time home and how far it has turned round.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import type { State } from "@johnmorrisdotca/uchusen-rockets/journey";
42
+ *
43
+ * const speed = (state: State) => `${Math.round(state.speedMs)} m/s`;
44
+ * console.log(typeof speed);
45
+ * ```
46
+ */
47
+ export interface State extends Place {
48
+ dateMs: number;
49
+ /** Which way the engine is firing: not at all, pushing it on, or against its motion. */
50
+ burn: "off" | "forward" | "reverse";
51
+ /** How the speed is measured: from the start or destination (for a ship that burns all the way), from the Earth, the Sun or the destination. */
52
+ speedFrame: "start" | "earth" | "sun" | "destination";
53
+ speedMs: number;
54
+ gamma: number;
55
+ /** The acceleration, in g, positive along the motion. */
56
+ accelG: number;
57
+ /** Path flown, km, and the fraction of the path, 0 to 1. */
58
+ doneKm: number;
59
+ progress: number;
60
+ /** The straight line from the craft to the destination, km. */
61
+ toDestinationKm: number;
62
+ /** The craft's place in the trip's frame, km. */
63
+ positionKm: Vec3;
64
+ /** Time on board since lift-off and time since lift-off in the frame of the places, seconds. */
65
+ shipS: number;
66
+ frameS: number;
67
+ /** How long a signal takes to get home from here, seconds. */
68
+ signalS: number;
69
+ /** How far the craft has turned round, 0 (facing forward) to 1 (facing back). */
70
+ turned: number;
71
+ }
72
+ /**
73
+ * The total time the screen takes to play a trip at a speed.
74
+ * @param trip A planned trip.
75
+ * @param speed The play speed.
76
+ * @returns Seconds.
77
+ * @example
78
+ * ```ts no-run
79
+ * import { playSeconds, type Trip } from "@johnmorrisdotca/uchusen-rockets/journey";
80
+ *
81
+ * declare const trip: Trip;
82
+ * console.log(playSeconds(trip, 2));
83
+ * ```
84
+ */
85
+ export declare const playSeconds: (trip: Trip, speed?: PlaySpeed) => number;
86
+ /**
87
+ * Which phase a moment of playback is in, and the mission time it shows. Inside a phase the two are linear; across phases the scale changes, which is how a minute of burn and a month of coast both fit on the screen.
88
+ * @param trip A planned trip.
89
+ * @param playbackS Seconds since Play was pressed, at the normal speed (a faster speed multiplies the clock before calling).
90
+ * @returns The phase, how far through it, the mission time and the scale.
91
+ * @example
92
+ * ```ts no-run
93
+ * import { placeAt, type Trip } from "@johnmorrisdotca/uchusen-rockets/journey";
94
+ *
95
+ * declare const trip: Trip;
96
+ * console.log(placeAt(trip, 0).phase.id, placeAt(trip, 1e9).phase.id);
97
+ * ```
98
+ */
99
+ export declare function placeAt(trip: Trip, playbackS: number): Place;
100
+ /**
101
+ * Everything true of the craft at a moment of playback: its place, speed, acceleration, the distances, the clocks and the signal time home.
102
+ * @param trip A planned trip (one that can be played).
103
+ * @param source Where the places are.
104
+ * @param playbackS Seconds since Play was pressed, at the normal speed.
105
+ * @returns The state.
106
+ * @example
107
+ * ```ts no-run
108
+ * import { stateAt, type BodySource, type Trip } from "@johnmorrisdotca/uchusen-rockets/journey";
109
+ *
110
+ * declare const trip: Trip;
111
+ * declare const source: BodySource;
112
+ * console.log(Math.round(stateAt(trip, source, 30).speedMs));
113
+ * ```
114
+ */
115
+ export declare function stateAt(trip: Trip, source: BodySource, playbackS: number): State;
@@ -0,0 +1,193 @@
1
+ import { C, G0 } from "./constants.js";
2
+ import { accelerating, transferPoint } from "./physics.js";
3
+ import { positionKm } from "./trip.js";
4
+ /**
5
+ * The total time the screen takes to play a trip at a speed.
6
+ * @param trip A planned trip.
7
+ * @param speed The play speed.
8
+ * @returns Seconds.
9
+ * @example
10
+ * ```ts no-run
11
+ * import { playSeconds, type Trip } from "@johnmorrisdotca/uchusen-rockets/journey";
12
+ *
13
+ * declare const trip: Trip;
14
+ * console.log(playSeconds(trip, 2));
15
+ * ```
16
+ */
17
+ export const playSeconds = (trip, speed = 1) => trip.phases.reduce((sum, p) => sum + p.screenS, 0) / speed;
18
+ /**
19
+ * Which phase a moment of playback is in, and the mission time it shows. Inside a phase the two are linear; across phases the scale changes, which is how a minute of burn and a month of coast both fit on the screen.
20
+ * @param trip A planned trip.
21
+ * @param playbackS Seconds since Play was pressed, at the normal speed (a faster speed multiplies the clock before calling).
22
+ * @returns The phase, how far through it, the mission time and the scale.
23
+ * @example
24
+ * ```ts no-run
25
+ * import { placeAt, type Trip } from "@johnmorrisdotca/uchusen-rockets/journey";
26
+ *
27
+ * declare const trip: Trip;
28
+ * console.log(placeAt(trip, 0).phase.id, placeAt(trip, 1e9).phase.id);
29
+ * ```
30
+ */
31
+ export function placeAt(trip, playbackS) {
32
+ let left = Math.max(0, playbackS);
33
+ const last = trip.phases.length - 1;
34
+ for (let index = 0; index <= last; index += 1) {
35
+ const phase = trip.phases[index];
36
+ if (left < phase.screenS || index === last) {
37
+ const fraction = Math.min(1, left / phase.screenS);
38
+ return { phase, index, fraction, missionS: phase.startS + fraction * (phase.endS - phase.startS), scale: (phase.endS - phase.startS) / phase.screenS };
39
+ }
40
+ left -= phase.screenS;
41
+ }
42
+ throw new Error("A trip has phases.");
43
+ }
44
+ const mix = (a, b, f) => a + (b - a) * f;
45
+ /**
46
+ * Everything true of the craft at a moment of playback: its place, speed, acceleration, the distances, the clocks and the signal time home.
47
+ * @param trip A planned trip (one that can be played).
48
+ * @param source Where the places are.
49
+ * @param playbackS Seconds since Play was pressed, at the normal speed.
50
+ * @returns The state.
51
+ * @example
52
+ * ```ts no-run
53
+ * import { stateAt, type BodySource, type Trip } from "@johnmorrisdotca/uchusen-rockets/journey";
54
+ *
55
+ * declare const trip: Trip;
56
+ * declare const source: BodySource;
57
+ * console.log(Math.round(stateAt(trip, source, 30).speedMs));
58
+ * ```
59
+ */
60
+ export function stateAt(trip, source, playbackS) {
61
+ const place = placeAt(trip, playbackS);
62
+ const t = place.missionS;
63
+ const dateMs = trip.departMs - trip.liftoffBeforeS * 1000 + t * 1000;
64
+ const base = { ...place, dateMs, shipS: Math.max(0, t), frameS: Math.max(0, t), turned: 0 };
65
+ const dest = (ms) => positionKm(source, trip.to, ms, trip.frame);
66
+ const home = (p, ms) => {
67
+ const earth = positionKm(source, "earth", ms, trip.frame);
68
+ return Math.hypot(p[0] - earth[0], p[1] - earth[1], p[2] - earth[2]) / (C / 1000);
69
+ };
70
+ const b = trip.ballistic;
71
+ if (b) {
72
+ const origin = positionKm(source, trip.from, trip.departMs, trip.frame);
73
+ const id = place.phase.id;
74
+ const f = place.fraction;
75
+ const turned = id === "flip" ? f : id === "brake" || id === "flyby" || id === "arrival" ? 1 : 0;
76
+ let speed = 0;
77
+ let frameName = "earth";
78
+ let accel = 0;
79
+ let burn = "off";
80
+ let done = 0;
81
+ let pos = origin;
82
+ if (id === "preheat" || id === "ignition")
83
+ speed = 0;
84
+ else if (id === "ascent") {
85
+ speed = b.parkSpeed * 1000 * f;
86
+ accel = (b.parkSpeed * 1000) / b.ascentS / G0;
87
+ burn = "forward";
88
+ }
89
+ else if (id === "burn") {
90
+ speed = mix(b.parkSpeed, b.burnEndSpeed, f) * 1000;
91
+ accel = (trip.dv.departure / b.burnS) / G0;
92
+ burn = "forward";
93
+ }
94
+ else if (id === "coast" || id === "flip") {
95
+ const tau = t - (b.ascentS + b.burnS);
96
+ const p = transferPoint(b.mu, b.r1, b.r2, b.theta0, Math.min(tau, b.transitS));
97
+ pos = [p.r * Math.cos(p.theta), p.r * Math.sin(p.theta), 0];
98
+ speed = p.v * 1000;
99
+ frameName = trip.frame === "sun" ? "sun" : "earth";
100
+ done = arcAt(b.arc, Math.min(tau, b.transitS) / b.transitS);
101
+ }
102
+ else {
103
+ pos = dest(dateMs);
104
+ done = trip.pathKm;
105
+ frameName = "destination";
106
+ if (id === "brake") {
107
+ speed = mix(b.captureSpeedIn, b.captureSpeedOut, f) * 1000;
108
+ accel = -(trip.dv.arrival / b.brakeS) / G0;
109
+ burn = "reverse";
110
+ }
111
+ else if (id === "flyby")
112
+ speed = b.captureSpeedIn * 1000;
113
+ else
114
+ speed = (trip.verdict.level === "flyby" ? b.captureSpeedIn : b.captureSpeedOut) * 1000;
115
+ }
116
+ if (id === "ascent" || id === "burn")
117
+ done = 0;
118
+ const target = dest(dateMs);
119
+ return {
120
+ ...base,
121
+ burn,
122
+ speedFrame: frameName,
123
+ speedMs: speed,
124
+ gamma: 1,
125
+ accelG: accel,
126
+ doneKm: done,
127
+ progress: Math.min(1, done / trip.pathKm),
128
+ toDestinationKm: Math.hypot(target[0] - pos[0], target[1] - pos[1], target[2] - pos[2]),
129
+ positionKm: pos,
130
+ signalS: home(pos, dateMs),
131
+ turned,
132
+ };
133
+ }
134
+ const c = trip.continuous;
135
+ const tb = c.burnS;
136
+ const total = tb * 2 + c.flipS;
137
+ const tt = Math.max(0, Math.min(t, total));
138
+ let x = 0;
139
+ let v = 0;
140
+ let gamma = 1;
141
+ let proper = 0;
142
+ let accel = 0;
143
+ let burn = "off";
144
+ const peak = accelerating(c.a, tb);
145
+ if (tt <= tb) {
146
+ const s = accelerating(c.a, tt);
147
+ ({ x, v, gamma, proper } = s);
148
+ accel = t > 0 ? 1 : 0;
149
+ burn = t > 0 ? "forward" : "off";
150
+ }
151
+ else if (tt <= tb + c.flipS) {
152
+ x = peak.x + peak.v * (tt - tb);
153
+ v = peak.v;
154
+ gamma = peak.gamma;
155
+ proper = peak.proper + (tt - tb) / peak.gamma;
156
+ }
157
+ else {
158
+ const s = accelerating(c.a, total - tt);
159
+ const d = (peak.x * 2 + peak.v * c.flipS);
160
+ x = d - s.x;
161
+ v = s.v;
162
+ gamma = s.gamma;
163
+ proper = 2 * peak.proper + c.flipS / peak.gamma - s.proper;
164
+ accel = -1;
165
+ burn = "reverse";
166
+ }
167
+ const path = trip.pathKm * 1000;
168
+ const turned = place.phase.id === "flip" ? place.fraction : tt > tb + c.flipS ? 1 : 0;
169
+ const f = path > 0 ? x / path : 0;
170
+ const pos = [mix(c.from[0], c.to[0], f), mix(c.from[1], c.to[1], f), mix(c.from[2], c.to[2], f)];
171
+ return {
172
+ ...base,
173
+ burn: place.phase.id === "arrival" ? "off" : burn,
174
+ speedFrame: "start",
175
+ speedMs: place.phase.id === "arrival" ? 0 : v,
176
+ gamma,
177
+ accelG: place.phase.id === "arrival" ? 0 : (accel * c.a) / G0,
178
+ doneKm: x / 1000,
179
+ progress: Math.min(1, Math.max(0, f)),
180
+ toDestinationKm: Math.max(0, (path - x) / 1000),
181
+ positionKm: pos,
182
+ shipS: t <= 0 ? 0 : proper,
183
+ frameS: Math.max(0, t),
184
+ signalS: home(pos, dateMs),
185
+ turned,
186
+ };
187
+ }
188
+ /** The path flown, km, a fraction of the way through the transit (0 to 1), from the table of cumulative lengths. */
189
+ function arcAt(arc, fraction) {
190
+ const at = Math.max(0, Math.min(1, fraction)) * (arc.length - 1);
191
+ const i = Math.min(arc.length - 2, Math.floor(at));
192
+ return mix(arc[i], arc[i + 1], at - i);
193
+ }
@@ -0,0 +1,213 @@
1
+ import type { BodySource } from "./bodies.ts";
2
+ import type { CapacityKind, Craft } from "./crafts.ts";
3
+ /** The kinds of phase a trip is made of: the countdown, the climb, the burns, the coast, the turn round, the braking and the arrival. */
4
+ export type PhaseId = "preheat" | "ignition" | "ascent" | "burn" | "coast" | "flip" | "brake" | "flyby" | "accelerate" | "decelerate" | "arrival";
5
+ /**
6
+ * One stretch of a trip: its length in mission seconds (from lift-off, which is 0) and how long it takes on the screen at normal speed.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import type { Phase } from "@johnmorrisdotca/uchusen-rockets/journey";
11
+ *
12
+ * const coast: Phase = { id: "coast", startS: 1_050, endS: 430_000, screenS: 22 };
13
+ * console.log(coast.endS - coast.startS);
14
+ * ```
15
+ */
16
+ export interface Phase {
17
+ id: PhaseId;
18
+ startS: number;
19
+ endS: number;
20
+ screenS: number;
21
+ }
22
+ /** How a craft stands on a trip: rated, rated at least, fine, not rated, flyby only, or cannot. */
23
+ export type VerdictLevel = "rated" | "at-least" | "ok" | "not-rated" | "flyby" | "cannot";
24
+ /**
25
+ * What a craft can do on a trip, in one plain line, with the lines behind it.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import type { Verdict } from "@johnmorrisdotca/uchusen-rockets/journey";
30
+ *
31
+ * const verdict: Verdict = { level: "cannot", line: "Not enough fuel.", because: [], playable: false };
32
+ * console.log(verdict.line, verdict.playable);
33
+ * ```
34
+ */
35
+ export interface Verdict {
36
+ level: VerdictLevel;
37
+ /** The one line shown beside the picker. */
38
+ line: string;
39
+ /** More lines, each a fact the verdict rests on. */
40
+ because: string[];
41
+ /** Whether Play is offered. */
42
+ playable: boolean;
43
+ }
44
+ /** A place or a speed as x, y and z. */
45
+ export type Vec3 = readonly [number, number, number];
46
+ /**
47
+ * A trip, planned: when it leaves, its phases, its verdict, its distances, its speed changes and what is true of the craft at the peak. Plain data, with no function in it.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import type { Trip } from "@johnmorrisdotca/uchusen-rockets/journey";
52
+ *
53
+ * const describe = (trip: Trip) => `${trip.from} to ${trip.to}: ${trip.verdict.line}`;
54
+ * console.log(typeof describe);
55
+ * ```
56
+ */
57
+ export interface Trip {
58
+ from: string;
59
+ to: string;
60
+ craftId: string;
61
+ drive: "ballistic" | "continuous";
62
+ /** What the positions are measured from: the Sun, or the Earth for a trip to the Moon. */
63
+ frame: "sun" | "earth";
64
+ /** When the departure burn (or the first burn of a ship that burns all the way) begins, milliseconds since 1970. */
65
+ departMs: number;
66
+ /** When lift-off is, seconds before `departMs` (0 for a ship that starts in orbit). */
67
+ liftoffBeforeS: number;
68
+ phases: Phase[];
69
+ totalS: number;
70
+ verdict: Verdict;
71
+ /** Names of the figures in this plan that are estimates, not sources. */
72
+ estimated: string[];
73
+ /** The straight line from the start to the end at departure, km. */
74
+ straightKm: number;
75
+ /** The length of the path flown, km. */
76
+ pathKm: number;
77
+ /** Speed changes, m/s. */
78
+ dv: {
79
+ departure: number;
80
+ arrival: number;
81
+ total: number;
82
+ };
83
+ /** The greatest acceleration felt, g. */
84
+ peakG: number;
85
+ /** The greatest speed, m/s, and the Lorentz factor then. */
86
+ peakSpeed: number;
87
+ peakGamma: number;
88
+ /** Time on board and in the frame of the two ends, seconds, from lift-off. */
89
+ shipS: number;
90
+ frameS: number;
91
+ /** The propellant a ship that burns all the way uses, tonnes, and carries. */
92
+ propellantUsedT?: number;
93
+ propellantCarriedT?: number;
94
+ /** The propellant of the trip: tonnes used (and carried, where it is known), or null with the reason no figure can be given. */
95
+ fuel: {
96
+ usedT: number | null;
97
+ carriedT: number | null;
98
+ note: string;
99
+ };
100
+ /** What the craft carries to the end of this trip: kilograms (at least, where only a harder trip is rated), or null with the reason. */
101
+ payload: {
102
+ kg: number | null;
103
+ atLeast: boolean;
104
+ note: string;
105
+ };
106
+ ballistic?: {
107
+ mu: number;
108
+ r1: number;
109
+ r2: number;
110
+ theta0: number;
111
+ transitS: number;
112
+ parkKm: number;
113
+ parkSpeed: number;
114
+ burnEndSpeed: number;
115
+ captureKm: number;
116
+ captureSpeedIn: number;
117
+ captureSpeedOut: number;
118
+ burnS: number;
119
+ brakeS: number;
120
+ flipS: number;
121
+ ascentS: number;
122
+ vInfDeparture: number;
123
+ vInfArrival: number;
124
+ /** Cumulative path length at equal steps of the transit, km. */
125
+ arc: number[];
126
+ /** Where the departure and the destination body are in the frame, km, and the destination at arrival. */
127
+ c3: number;
128
+ };
129
+ continuous?: {
130
+ from: Vec3;
131
+ to: Vec3;
132
+ a: number;
133
+ burnS: number;
134
+ flipS: number;
135
+ peakRapidity: number;
136
+ blockedBySun: boolean;
137
+ };
138
+ }
139
+ /** A place's position in a frame, km: round the Sun for a planet, round the Earth for the Moon (and the Earth's own is the origin of the Earth's frame). */
140
+ export declare function positionKm(source: BodySource, id: string, ms: number, frame: "sun" | "earth"): Vec3;
141
+ /**
142
+ * A day a ballistic craft can leave on, with the flight time and the two speed changes of the Sun's frame.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * import type { Window } from "@johnmorrisdotca/uchusen-rockets/journey";
147
+ *
148
+ * const next: Window = { dateMs: Date.UTC(2026, 10, 4), days: 265, dv1: 3.2, dv2: 2.9 };
149
+ * console.log(new Date(next.dateMs).toISOString());
150
+ * ```
151
+ */
152
+ export interface Window {
153
+ dateMs: number;
154
+ /** The flight time, days. */
155
+ days: number;
156
+ /** The heliocentric speed changes, km/s. */
157
+ dv1: number;
158
+ dv2: number;
159
+ }
160
+ /**
161
+ * The next dates a craft can leave one planet for another on a Hohmann transfer: the days on which the destination will be where the craft arrives. Found by scanning day by day and bisecting each crossing, from the planets' real positions.
162
+ * @param source Where the planets are.
163
+ * @param from The starting planet's id.
164
+ * @param to The destination planet's id.
165
+ * @param afterMs The earliest date, milliseconds since 1970.
166
+ * @param count How many windows.
167
+ * @returns The windows, soonest first.
168
+ * @example
169
+ * ```ts no-run
170
+ * import { windows, type BodySource } from "@johnmorrisdotca/uchusen-rockets/journey";
171
+ *
172
+ * declare const source: BodySource;
173
+ * console.log(windows(source, "earth", "mars", Date.UTC(2026, 10, 4), 2).map((one) => new Date(one.dateMs).toISOString().slice(0, 10)));
174
+ * ```
175
+ */
176
+ export declare function windows(source: BodySource, from: string, to: string, afterMs: number, count: number): Window[];
177
+ /** The energy (km^2/s^2, the speed squared at infinity) each kind of payload figure is for. Lower is easier. */
178
+ export declare function c3Of(source: BodySource, kind: CapacityKind): number;
179
+ /**
180
+ * Plans a trip: the transfer or the flip and burn, the burns, the times, and what the craft can and cannot do. A craft that cannot make the trip comes back with a verdict that says why in one line and no phases.
181
+ * @param source Where the places are.
182
+ * @param craft The craft.
183
+ * @param from The starting place's id.
184
+ * @param to The destination's id.
185
+ * @param departMs When the departure burn begins, milliseconds since 1970 (for a ballistic trip to a planet, one of `windows`).
186
+ * @returns The planned trip.
187
+ * @example
188
+ * ```ts no-run
189
+ * import { craftOf, planTrip, type BodySource } from "@johnmorrisdotca/uchusen-rockets/journey";
190
+ *
191
+ * declare const source: BodySource;
192
+ * const trip = planTrip(source, craftOf("saturn-v")!, "earth", "moon", Date.UTC(2026, 10, 5));
193
+ * console.log(trip.verdict.line, Math.round(trip.dv.departure));
194
+ * ```
195
+ */
196
+ export declare function planTrip(source: BodySource, craft: Craft, from: string, to: string, departMs: number): Trip;
197
+ /**
198
+ * The departure date, for a craft that burns all the way, with the shortest trip in the next two years.
199
+ * @param source Where the places are.
200
+ * @param craft The craft.
201
+ * @param from The starting place's id.
202
+ * @param to The destination's id.
203
+ * @param afterMs The earliest date.
204
+ * @returns The date, milliseconds since 1970.
205
+ * @example
206
+ * ```ts no-run
207
+ * import { bestDate, craftOf, type BodySource } from "@johnmorrisdotca/uchusen-rockets/journey";
208
+ *
209
+ * declare const source: BodySource;
210
+ * console.log(new Date(bestDate(source, craftOf("kaminari")!, "earth", "mars", Date.UTC(2026, 10, 4))).toISOString());
211
+ * ```
212
+ */
213
+ export declare function bestDate(source: BodySource, craft: Craft, from: string, to: string, afterMs: number): number;