@moku-labs/game 0.9.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.
Files changed (53) hide show
  1. package/README.md +81 -22
  2. package/bin/moku-game.mjs +4 -0
  3. package/dist/app/page.d.mts +44 -0
  4. package/dist/app/page.mjs +379 -0
  5. package/dist/app/system.d.mts +144 -0
  6. package/dist/app/system.mjs +329 -0
  7. package/dist/app.d.mts +37 -0
  8. package/dist/app.mjs +235 -0
  9. package/dist/assets-BHwTLVta.mjs +7391 -0
  10. package/dist/assets.d.mts +1 -1
  11. package/dist/assets.mjs +1 -7390
  12. package/dist/cli.d.mts +70 -0
  13. package/dist/cli.mjs +1685 -0
  14. package/dist/{commands-DuwCxK5t.d.mts → commands-C4QlqijJ.d.mts} +5 -5
  15. package/dist/config-CdARrrhU.mjs +81 -0
  16. package/dist/control.d.mts +11 -10
  17. package/dist/control.mjs +3 -3
  18. package/dist/{define-B6pHpdBi.d.mts → define-CCc0ozKe.d.mts} +2 -2
  19. package/dist/{dev-wKLgOw7c.mjs → dev-Cc50Dk7H.mjs} +1 -1
  20. package/dist/drafts-BawpSaVp.mjs +156 -0
  21. package/dist/fake-MpzwZhIZ.mjs +87 -0
  22. package/dist/{headless-SCQXJxQf.d.mts → headless-G4nuZ-xz.d.mts} +1 -1
  23. package/dist/hot.d.mts +4 -6
  24. package/dist/hot.mjs +8 -9
  25. package/dist/{index-BO9kZzY3.d.mts → index-8TiJQjQA.d.mts} +30 -17
  26. package/dist/index.d.mts +5 -5
  27. package/dist/index.mjs +1 -1
  28. package/dist/inspect-la_KkjCS.mjs +1 -0
  29. package/dist/inspect.d.mts +7 -7
  30. package/dist/inspect.mjs +3 -2
  31. package/dist/jsx-dev-runtime.d.mts +1 -1
  32. package/dist/jsx-runtime.d.mts +1 -1
  33. package/dist/lint.mjs +10 -6
  34. package/dist/memory-CWlqhS7l.mjs +136 -0
  35. package/dist/memory-DwDhfFWK.d.mts +50 -0
  36. package/dist/project.mjs +3 -3
  37. package/dist/{run-CofJEdYx.mjs → run-wYty_Lwo.mjs} +2 -2
  38. package/dist/{runtime-CdNJ0UBm.d.mts → runtime-DOBYM6g4.d.mts} +1 -1
  39. package/dist/{input-z_35Jwfn.mjs → session-DgukhR_7.mjs} +20 -20
  40. package/dist/{sources-DNnzyCYf.mjs → sources-Duzj6lhV.mjs} +2 -2
  41. package/dist/{src-536arXjV.mjs → src-BSbtHZWk.mjs} +35 -300
  42. package/dist/testing.d.mts +4 -50
  43. package/dist/testing.mjs +3 -87
  44. package/dist/{types-VZVWwqCg.d.mts → types-BM4shh8K.d.mts} +259 -259
  45. package/dist/{types-xXfyk56Q.d.mts → types-BUwDHKXm.d.mts} +1377 -1377
  46. package/dist/types-Ca_z_4tQ.d.mts +586 -0
  47. package/dist/{types-CQueODzo.d.mts → types-CefgYzFt.d.mts} +2 -2
  48. package/dist/{types-DUMoYPYe.d.mts → types-DAdwEJMp.d.mts} +2 -2
  49. package/dist/{types-B0v5VVa3.d.mts → types-ZQLYXo5X.d.mts} +304 -304
  50. package/dist/visual.d.mts +6 -6
  51. package/dist/visual.mjs +5 -5
  52. package/llms.txt +120 -43
  53. package/package.json +30 -2
@@ -71,211 +71,243 @@ type Require = <Plugin extends {
71
71
  };
72
72
  }>(plugin: Plugin) => ApiOf<Plugin>;
73
73
  declare namespace types_d_exports$7 {
74
- export { Api$7 as Api, ClockCtx, ClockSource, Config$7 as Config, Elapsed, FakeClock, State$7 as State };
74
+ export { Api$7 as Api, Config$7 as Config, FrameCallback, Phase, State$7 as State, Time, TimeCtx };
75
75
  }
76
76
  /**
77
- * Source of time and timers. The system source is the default; a game passes its own when time
78
- * must come from somewhere else, for example a server.
77
+ * Frame phases, in call order.
79
78
  *
80
79
  * @example
81
80
  * ```ts
82
- * // Server time: the device clock only measures the distance from the last sync. The plugin keeps
83
- * // at most one timer alive, so the source remembers one.
84
- * const serverClock = (serverNow: number): ClockSource => {
85
- * const syncedAt = performance.now();
86
- * let timer: ReturnType<typeof setTimeout> | undefined;
87
- *
88
- * return {
89
- * now: () => serverNow + (performance.now() - syncedAt),
90
- * setTimer: (callback, delayMs) => (timer = setTimeout(callback, delayMs)),
91
- * clearTimer: () => clearTimeout(timer)
92
- * };
93
- * };
94
- *
95
- * createApp({ pluginConfigs: { clock: { source: serverClock(1_790_000_000_000) } } });
81
+ * const phase: Phase = "animate";
96
82
  * ```
97
83
  */
98
- type ClockSource = {
99
- /**
100
- * Reads the current moment.
101
- *
102
- * @returns Epoch milliseconds. A fraction is cut by the plugin; going back is tolerated.
103
- */
104
- now(): number;
105
- /**
106
- * Schedules one callback. The plugin keeps at most one timer alive.
107
- *
108
- * @param callback - Function to run when the delay has passed.
109
- * @param delayMs - Delay in milliseconds, never negative.
110
- * @returns A handle of any shape; the plugin only hands it back to `clearTimer`.
111
- */
112
- setTimer(callback: () => void, delayMs: number): unknown;
113
- /**
114
- * Cancels a pending timer. A handle this source did not produce must be ignored.
115
- *
116
- * @param handle - Handle returned by `setTimer`.
117
- */
118
- clearTimer(handle: unknown): void;
119
- };
84
+ type Phase = "input" | "animate" | "layout" | "sync" | "signals" | "render";
120
85
  /**
121
- * Fake source for tests, from `@moku-labs/game/testing`. It needs no real timer.
86
+ * The Time resource, in scaled milliseconds. `idle` tells whether the loop currently runs at the
87
+ * lowered idle cap.
122
88
  *
123
89
  * @example
124
90
  * ```ts
125
- * // An energy point refills after 60 s. The test does not wait: it moves the clock.
126
- * const clock = fakeClock(1000);
127
- * const app = createApp({ pluginConfigs: { clock: { source: clock } } });
128
- *
129
- * app.clock.scheduleAt(61_000);
130
- * clock.advance(60_000); // the listeners of onElapsed get { now: 61000 }
91
+ * const time: Time = { delta: 16, elapsed: 1600, scale: 1, frame: 100, idle: false };
131
92
  * ```
132
93
  */
133
- type FakeClock = ClockSource & {
134
- /**
135
- * Moves the clock forward and fires every timer that falls due on the way, in due order.
136
- *
137
- * @param ms - Milliseconds to move forward. A negative amount moves nothing.
138
- * @example
139
- * ```ts
140
- * clock.advance(5000); // now() is 5000 later, a timer due in 3000 has fired
141
- * ```
142
- */
143
- advance(ms: number): void;
144
- /**
145
- * Jumps to a moment without firing any timer. Moving back is allowed: that is a player who
146
- * sets the device clock by hand.
147
- *
148
- * @param moment - Moment in epoch milliseconds.
149
- * @example
150
- * ```ts
151
- * clock.set(400); // the device clock went back; app.clock.now() does not decrease
152
- * ```
153
- */
154
- set(moment: number): void;
94
+ type Time = {
95
+ delta: number;
96
+ elapsed: number;
97
+ scale: number;
98
+ frame: number;
99
+ idle: boolean;
155
100
  };
156
101
  /**
157
- * Input delivered to `onElapsed` listeners when a due moment arrives or the game resumes.
102
+ * Callback run once per frame in its phase.
158
103
  *
159
104
  * @example
160
105
  * ```ts
161
- * const input: Elapsed = { now: 1_790_000_060_000 };
106
+ * const advanceTweens: FrameCallback = time => tweens.advance(time.delta);
162
107
  * ```
163
108
  */
164
- type Elapsed = {
165
- now: number;
166
- };
109
+ type FrameCallback = (time: Readonly<Time>) => void;
167
110
  /**
168
- * clock plugin config.
111
+ * time plugin config.
169
112
  *
170
113
  * @example
171
114
  * ```ts
172
- * createApp({ pluginConfigs: { clock: { source: fakeClock(1000) } } });
115
+ * createApp({ pluginConfigs: { time: { maxFps: 30 } } });
173
116
  * ```
174
117
  */
175
118
  type Config$7 = {
176
119
  /**
177
- * Time source. `undefined` means the system source: `Date.now` and `setTimeout`.
120
+ * Frame rate cap. The default is 60. 120 is for WebViews that really deliver 120 Hz frames:
121
+ * Android today; WKWebView on iOS and macOS is capped at 60 by WebKit (bug 294338).
178
122
  */
179
- source: ClockSource | undefined;
123
+ maxFps: 30 | 60 | 120;
124
+ /**
125
+ * Upper bound of one frame's delta in milliseconds.
126
+ */
127
+ maxDeltaMs: number;
128
+ /**
129
+ * Frame rate cap of an idle screen, when nothing woke the clock for `idleAfterMs`. The default
130
+ * is 30; 0 turns the idle cap off and every frame runs at `maxFps`.
131
+ */
132
+ idleFps: 0 | 30;
133
+ /**
134
+ * Unscaled milliseconds without a `wake()` after which the loop drops to `idleFps`.
135
+ */
136
+ idleAfterMs: number;
180
137
  };
181
138
  /**
182
- * clock plugin state.
139
+ * time plugin state.
183
140
  */
184
141
  type State$7 = {
185
- source: ClockSource;
186
- last: number;
187
- dueAt: number | undefined;
188
- handle: unknown;
189
- listeners: Array<(input: Elapsed) => void>;
142
+ callbacks: Record<Phase, FrameCallback[]>;
143
+ /**
144
+ * Scratch array of a frame: the six callback lists as they were when the frame started.
145
+ */
146
+ captured: (readonly FrameCallback[])[];
147
+ time: Time;
148
+ paused: boolean;
149
+ running: boolean;
150
+ /**
151
+ * True while a frame runs; guards `step` re-entry.
152
+ */
153
+ stepping: boolean;
154
+ rafId: number | undefined;
155
+ lastTimestamp: number | undefined;
156
+ /**
157
+ * Elapsed time of the frame source in unscaled milliseconds, the clock of the idle timer: it
158
+ * keeps running while `scale` is 0.
159
+ */
160
+ unscaledElapsedMs: number;
161
+ /**
162
+ * Unscaled elapsed time of the last wake.
163
+ */
164
+ lastWakeMs: number;
165
+ /**
166
+ * True while the loop runs at `idleFps`.
167
+ */
168
+ idle: boolean;
190
169
  };
191
170
  /**
192
- * clock plugin API, `app.clock`. Trusted time for rules that depend on real time: energy refill,
193
- * generators, daily rewards. The clock holds ONE pending moment; the rules decide the next one.
171
+ * time plugin API, `app.time`. The one clock of the screen: frame callbacks per phase, the `Time`
172
+ * resource, the time scale, and `step` for tests and tools.
194
173
  *
195
174
  * @example
196
175
  * ```ts
197
- * // The whole cycle: ask the rules for the nearest moment, wait for it, apply, ask again.
198
- * app.clock.onElapsed(({ now }) => {
199
- * applyRefills(now);
200
- * app.clock.scheduleAt(nextRefillAt(now)); // undefined when nothing is pending
201
- * });
202
- * app.clock.scheduleAt(nextRefillAt(app.clock.now()));
176
+ * // Frame work is a callback in a phase. A test has no frame source: it drives the frames itself.
177
+ * const off = app.time.onFrame("animate", time => counter.advance(time.delta));
178
+ *
179
+ * app.time.step(16);
180
+ * // the callback gets { delta: 16, elapsed: 16, scale: 1, frame: 1, idle: false }
181
+ * off();
203
182
  * ```
204
183
  */
205
184
  type Api$7 = {
206
185
  /**
207
- * Reads trusted time. It never decreases, whatever the device clock does, so a player who sets
208
- * the clock back gains nothing.
186
+ * Registers a frame callback. Callbacks of one phase run in registration order; a callback
187
+ * registered during a frame runs from the next frame on.
209
188
  *
210
- * @returns The current moment in epoch milliseconds, an integer.
189
+ * @param phase - Phase the callback belongs to.
190
+ * @param callback - Function called once per frame with the current `Time`.
191
+ * @returns Unsubscribe function; calling it twice is a no-op.
211
192
  * @example
212
193
  * ```ts
213
- * // Stamp a generator when it is used, and compare later.
214
- * player.generator.usedAt = app.clock.now(); // 1790000000000
215
- * const ready = app.clock.now() - player.generator.usedAt >= 60_000;
194
+ * // The coin counter rolls up while the reward popup is open.
195
+ * const off = app.time.onFrame("animate", time => counter.advance(time.delta));
196
+ *
197
+ * off(); // the popup is closed
216
198
  * ```
217
199
  */
218
- now(): number;
200
+ onFrame(phase: Phase, callback: FrameCallback): () => void;
219
201
  /**
220
- * Replaces the single pending due moment. A moment in the past is not delivered synchronously:
221
- * it fires on the next macrotask of the source.
202
+ * Returns a snapshot of the current `Time`, so a caller cannot write into the frame state.
222
203
  *
223
- * @param moment - Moment in epoch milliseconds, or `undefined` to cancel.
204
+ * @returns A copy of the current `Time`.
224
205
  * @example
225
206
  * ```ts
226
- * // A generator refills in 60 s. Only one moment is pending: the nearest one.
227
- * app.clock.scheduleAt(app.clock.now() + 60_000);
228
- * app.clock.scheduleAt(undefined); // nothing is due any more: cancel
207
+ * // Stamp the start of a combo window in game time, which stands still during a pause.
208
+ * const startedAt = app.time.snapshot().elapsed; // 1600
209
+ * app.time.snapshot(); // { delta: 16, elapsed: 1600, scale: 1, frame: 100, idle: false }
229
210
  * ```
230
211
  */
231
- scheduleAt(moment: number | undefined): void;
212
+ snapshot(): Readonly<Time>;
232
213
  /**
233
- * Registers a listener for the `elapsed` input: a due moment arrived, or `poke` was called.
234
- * The clock does not reschedule by itself.
214
+ * Sets the time scale. Every frame delta is multiplied by it; 0 freezes the game time
215
+ * while the frames keep running. A negative scale is clamped to 0.
235
216
  *
236
- * @param listener - Called with `{ now }`, the trusted moment of the delivery.
237
- * @returns The unsubscribe function.
217
+ * @param scale - New time scale, 0 or greater.
238
218
  * @example
239
219
  * ```ts
240
- * const off = app.clock.onElapsed(({ now }) => {
241
- * applyRefills(now); // now: 1790000060000
242
- * });
220
+ * // Slow motion while the last match of the level resolves.
221
+ * app.time.setScale(0.5); // a frame of 20 ms now has delta 10
222
+ * app.time.setScale(1);
223
+ * ```
224
+ */
225
+ setScale(scale: number): void;
226
+ /**
227
+ * Pauses the clock: no phase runs and `elapsed` stops advancing. Called by `lifecycle`.
243
228
  *
244
- * off(); // the screen that showed the timer is closed
229
+ * @example
230
+ * ```ts
231
+ * // The lifecycle plugin owns the pause policy: the first pause reason stops the clock.
232
+ * // A game pauses through app.lifecycle.push(reason), never here.
233
+ * const time = ctx.require(timePlugin);
234
+ * if (reasons.length === 1) time.pause(); // app.time.isPaused() is true, no phase runs
245
235
  * ```
246
236
  */
247
- onElapsed(listener: (input: Elapsed) => void): () => void;
237
+ pause(): void;
248
238
  /**
249
- * Delivers `elapsed` right now, without touching the pending due moment. The engine calls it
250
- * when the game resumes; a game calls it after it changed something the rules depend on.
239
+ * Resumes the clock and drops the stale timestamp, so the first frame after the pause has
240
+ * a normal delta instead of the whole pause. Called by `lifecycle`.
251
241
  *
252
242
  * @example
253
243
  * ```ts
254
- * // The game synced with its server and the time source jumped forward: let the rules catch up.
255
- * // On resume from background the engine pokes by itself, a game never does that.
256
- * app.clock.poke(); // onElapsed listeners get { now } at once; the pending moment stays
244
+ * // The lifecycle plugin resumes when the last pause reason is gone.
245
+ * const time = ctx.require(timePlugin);
246
+ * if (reasons.length === 0) time.resume(); // the next frame has a normal delta, not the whole pause
257
247
  * ```
258
248
  */
259
- poke(): void;
249
+ resume(): void;
260
250
  /**
261
- * Reads the pending due moment, for inspection and tests.
251
+ * Tells whether the clock is paused.
262
252
  *
263
- * @returns The pending moment, or `undefined` when nothing is scheduled.
253
+ * @returns True while paused.
264
254
  * @example
265
255
  * ```ts
266
- * app.clock.scheduleAt(1500);
267
- * app.clock.dueAt(); // 1500
268
- * await app.stop();
269
- * app.clock.dueAt(); // undefined
256
+ * // A test checks that a pause reason really stopped the frames.
257
+ * app.lifecycle.push("background");
258
+ * app.time.isPaused(); // true
270
259
  * ```
271
260
  */
272
- dueAt(): number | undefined;
261
+ isPaused(): boolean;
262
+ /**
263
+ * Tells whether a real frame source drives the loop. False in plain Bun, where a test
264
+ * drives the frames with `step`.
265
+ *
266
+ * @returns True while the loop runs.
267
+ * @example
268
+ * ```ts
269
+ * // Without a frame source no frame ever comes: finish the fly-in at once instead of waiting.
270
+ * if (!app.time.isRunning()) coin.moveTo(target); // false in plain Bun, true in a browser
271
+ * ```
272
+ */
273
+ isRunning(): boolean;
274
+ /**
275
+ * Resets the idle timer: the next frame runs at `maxFps` again, even after a long idle. Cheap,
276
+ * idempotent and safe inside a frame callback. Every plugin that gives the player something to
277
+ * look at calls it: a pointer sample, a starting animation, a flow edge, a finished load, a
278
+ * scene switch, a locale change, a reconcile.
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * // The flow runner walked an edge, so the screen has work again and the cap goes back up.
283
+ * const time = ctx.require(timePlugin);
284
+ *
285
+ * time.wake(); // app.time.snapshot().idle is false from here on
286
+ * ```
287
+ */
288
+ wake(): void;
289
+ /**
290
+ * Runs exactly one frame with the given unscaled delta, ignoring the fps cap, the pause
291
+ * flag and `maxDeltaMs`. The time scale still applies. For tests and tools.
292
+ *
293
+ * @param deltaMs - Unscaled delta of the frame in milliseconds.
294
+ * @throws {Error} When called from inside a frame callback.
295
+ * @example
296
+ * ```ts
297
+ * // A test plays two frames without a browser.
298
+ * app.time.step(16);
299
+ * app.time.step(4);
300
+ * app.time.snapshot(); // { delta: 4, elapsed: 20, scale: 1, frame: 2, idle: false }
301
+ * ```
302
+ */
303
+ step(deltaMs: number): void;
273
304
  };
274
305
  /**
275
- * Domain context of the clock plugin.
306
+ * Domain context of the time plugin.
276
307
  */
277
- type ClockCtx = PluginCtx<Config$7, State$7> & {
308
+ type TimeCtx = PluginCtx<Config$7, State$7> & {
278
309
  readonly global: object;
310
+ readonly log: Log.LogApi;
279
311
  };
280
312
  declare namespace types_d_exports$6 {
281
313
  export { Api$6 as Api, Config$6 as Config, Events$5 as Events, LifecycleCtx, PauseReason, State$6 as State };
@@ -1056,244 +1088,212 @@ type ModelCtx = PluginCtx<Config$5, State$5, Events$4> & {
1056
1088
  readonly global: object;
1057
1089
  readonly log: Log.LogApi;
1058
1090
  };
1059
- declare namespace types_d_exports$4 {
1060
- export { Api$4 as Api, Config$4 as Config, FrameCallback, Phase, State$4 as State, Time, TimeCtx };
1061
- }
1091
+ declare namespace types_d_exports$4 {
1092
+ export { Api$4 as Api, ClockCtx, ClockSource, Config$4 as Config, Elapsed, FakeClock, State$4 as State };
1093
+ }
1094
+ /**
1095
+ * Source of time and timers. The system source is the default; a game passes its own when time
1096
+ * must come from somewhere else, for example a server.
1097
+ *
1098
+ * @example
1099
+ * ```ts
1100
+ * // Server time: the device clock only measures the distance from the last sync. The plugin keeps
1101
+ * // at most one timer alive, so the source remembers one.
1102
+ * const serverClock = (serverNow: number): ClockSource => {
1103
+ * const syncedAt = performance.now();
1104
+ * let timer: ReturnType<typeof setTimeout> | undefined;
1105
+ *
1106
+ * return {
1107
+ * now: () => serverNow + (performance.now() - syncedAt),
1108
+ * setTimer: (callback, delayMs) => (timer = setTimeout(callback, delayMs)),
1109
+ * clearTimer: () => clearTimeout(timer)
1110
+ * };
1111
+ * };
1112
+ *
1113
+ * createApp({ pluginConfigs: { clock: { source: serverClock(1_790_000_000_000) } } });
1114
+ * ```
1115
+ */
1116
+ type ClockSource = {
1117
+ /**
1118
+ * Reads the current moment.
1119
+ *
1120
+ * @returns Epoch milliseconds. A fraction is cut by the plugin; going back is tolerated.
1121
+ */
1122
+ now(): number;
1123
+ /**
1124
+ * Schedules one callback. The plugin keeps at most one timer alive.
1125
+ *
1126
+ * @param callback - Function to run when the delay has passed.
1127
+ * @param delayMs - Delay in milliseconds, never negative.
1128
+ * @returns A handle of any shape; the plugin only hands it back to `clearTimer`.
1129
+ */
1130
+ setTimer(callback: () => void, delayMs: number): unknown;
1131
+ /**
1132
+ * Cancels a pending timer. A handle this source did not produce must be ignored.
1133
+ *
1134
+ * @param handle - Handle returned by `setTimer`.
1135
+ */
1136
+ clearTimer(handle: unknown): void;
1137
+ };
1062
1138
  /**
1063
- * Frame phases, in call order.
1139
+ * Fake source for tests, from `@moku-labs/game/testing`. It needs no real timer.
1064
1140
  *
1065
1141
  * @example
1066
1142
  * ```ts
1067
- * const phase: Phase = "animate";
1068
- * ```
1069
- */
1070
- type Phase = "input" | "animate" | "layout" | "sync" | "signals" | "render";
1071
- /**
1072
- * The Time resource, in scaled milliseconds. `idle` tells whether the loop currently runs at the
1073
- * lowered idle cap.
1143
+ * // An energy point refills after 60 s. The test does not wait: it moves the clock.
1144
+ * const clock = fakeClock(1000);
1145
+ * const app = createApp({ pluginConfigs: { clock: { source: clock } } });
1074
1146
  *
1075
- * @example
1076
- * ```ts
1077
- * const time: Time = { delta: 16, elapsed: 1600, scale: 1, frame: 100, idle: false };
1147
+ * app.clock.scheduleAt(61_000);
1148
+ * clock.advance(60_000); // the listeners of onElapsed get { now: 61000 }
1078
1149
  * ```
1079
1150
  */
1080
- type Time = {
1081
- delta: number;
1082
- elapsed: number;
1083
- scale: number;
1084
- frame: number;
1085
- idle: boolean;
1151
+ type FakeClock = ClockSource & {
1152
+ /**
1153
+ * Moves the clock forward and fires every timer that falls due on the way, in due order.
1154
+ *
1155
+ * @param ms - Milliseconds to move forward. A negative amount moves nothing.
1156
+ * @example
1157
+ * ```ts
1158
+ * clock.advance(5000); // now() is 5000 later, a timer due in 3000 has fired
1159
+ * ```
1160
+ */
1161
+ advance(ms: number): void;
1162
+ /**
1163
+ * Jumps to a moment without firing any timer. Moving back is allowed: that is a player who
1164
+ * sets the device clock by hand.
1165
+ *
1166
+ * @param moment - Moment in epoch milliseconds.
1167
+ * @example
1168
+ * ```ts
1169
+ * clock.set(400); // the device clock went back; app.clock.now() does not decrease
1170
+ * ```
1171
+ */
1172
+ set(moment: number): void;
1086
1173
  };
1087
1174
  /**
1088
- * Callback run once per frame in its phase.
1175
+ * Input delivered to `onElapsed` listeners when a due moment arrives or the game resumes.
1089
1176
  *
1090
1177
  * @example
1091
1178
  * ```ts
1092
- * const advanceTweens: FrameCallback = time => tweens.advance(time.delta);
1179
+ * const input: Elapsed = { now: 1_790_000_060_000 };
1093
1180
  * ```
1094
1181
  */
1095
- type FrameCallback = (time: Readonly<Time>) => void;
1182
+ type Elapsed = {
1183
+ now: number;
1184
+ };
1096
1185
  /**
1097
- * time plugin config.
1186
+ * clock plugin config.
1098
1187
  *
1099
1188
  * @example
1100
1189
  * ```ts
1101
- * createApp({ pluginConfigs: { time: { maxFps: 30 } } });
1190
+ * createApp({ pluginConfigs: { clock: { source: fakeClock(1000) } } });
1102
1191
  * ```
1103
1192
  */
1104
1193
  type Config$4 = {
1105
1194
  /**
1106
- * Frame rate cap. The default is 60. 120 is for WebViews that really deliver 120 Hz frames:
1107
- * Android today; WKWebView on iOS and macOS is capped at 60 by WebKit (bug 294338).
1108
- */
1109
- maxFps: 30 | 60 | 120;
1110
- /**
1111
- * Upper bound of one frame's delta in milliseconds.
1112
- */
1113
- maxDeltaMs: number;
1114
- /**
1115
- * Frame rate cap of an idle screen, when nothing woke the clock for `idleAfterMs`. The default
1116
- * is 30; 0 turns the idle cap off and every frame runs at `maxFps`.
1117
- */
1118
- idleFps: 0 | 30;
1119
- /**
1120
- * Unscaled milliseconds without a `wake()` after which the loop drops to `idleFps`.
1195
+ * Time source. `undefined` means the system source: `Date.now` and `setTimeout`.
1121
1196
  */
1122
- idleAfterMs: number;
1197
+ source: ClockSource | undefined;
1123
1198
  };
1124
1199
  /**
1125
- * time plugin state.
1200
+ * clock plugin state.
1126
1201
  */
1127
1202
  type State$4 = {
1128
- callbacks: Record<Phase, FrameCallback[]>;
1129
- /**
1130
- * Scratch array of a frame: the six callback lists as they were when the frame started.
1131
- */
1132
- captured: (readonly FrameCallback[])[];
1133
- time: Time;
1134
- paused: boolean;
1135
- running: boolean;
1136
- /**
1137
- * True while a frame runs; guards `step` re-entry.
1138
- */
1139
- stepping: boolean;
1140
- rafId: number | undefined;
1141
- lastTimestamp: number | undefined;
1142
- /**
1143
- * Elapsed time of the frame source in unscaled milliseconds, the clock of the idle timer: it
1144
- * keeps running while `scale` is 0.
1145
- */
1146
- unscaledElapsedMs: number;
1147
- /**
1148
- * Unscaled elapsed time of the last wake.
1149
- */
1150
- lastWakeMs: number;
1151
- /**
1152
- * True while the loop runs at `idleFps`.
1153
- */
1154
- idle: boolean;
1203
+ source: ClockSource;
1204
+ last: number;
1205
+ dueAt: number | undefined;
1206
+ handle: unknown;
1207
+ listeners: Array<(input: Elapsed) => void>;
1155
1208
  };
1156
1209
  /**
1157
- * time plugin API, `app.time`. The one clock of the screen: frame callbacks per phase, the `Time`
1158
- * resource, the time scale, and `step` for tests and tools.
1210
+ * clock plugin API, `app.clock`. Trusted time for rules that depend on real time: energy refill,
1211
+ * generators, daily rewards. The clock holds ONE pending moment; the rules decide the next one.
1159
1212
  *
1160
1213
  * @example
1161
1214
  * ```ts
1162
- * // Frame work is a callback in a phase. A test has no frame source: it drives the frames itself.
1163
- * const off = app.time.onFrame("animate", time => counter.advance(time.delta));
1164
- *
1165
- * app.time.step(16);
1166
- * // the callback gets { delta: 16, elapsed: 16, scale: 1, frame: 1, idle: false }
1167
- * off();
1215
+ * // The whole cycle: ask the rules for the nearest moment, wait for it, apply, ask again.
1216
+ * app.clock.onElapsed(({ now }) => {
1217
+ * applyRefills(now);
1218
+ * app.clock.scheduleAt(nextRefillAt(now)); // undefined when nothing is pending
1219
+ * });
1220
+ * app.clock.scheduleAt(nextRefillAt(app.clock.now()));
1168
1221
  * ```
1169
1222
  */
1170
1223
  type Api$4 = {
1171
1224
  /**
1172
- * Registers a frame callback. Callbacks of one phase run in registration order; a callback
1173
- * registered during a frame runs from the next frame on.
1174
- *
1175
- * @param phase - Phase the callback belongs to.
1176
- * @param callback - Function called once per frame with the current `Time`.
1177
- * @returns Unsubscribe function; calling it twice is a no-op.
1178
- * @example
1179
- * ```ts
1180
- * // The coin counter rolls up while the reward popup is open.
1181
- * const off = app.time.onFrame("animate", time => counter.advance(time.delta));
1182
- *
1183
- * off(); // the popup is closed
1184
- * ```
1185
- */
1186
- onFrame(phase: Phase, callback: FrameCallback): () => void;
1187
- /**
1188
- * Returns a snapshot of the current `Time`, so a caller cannot write into the frame state.
1189
- *
1190
- * @returns A copy of the current `Time`.
1191
- * @example
1192
- * ```ts
1193
- * // Stamp the start of a combo window in game time, which stands still during a pause.
1194
- * const startedAt = app.time.snapshot().elapsed; // 1600
1195
- * app.time.snapshot(); // { delta: 16, elapsed: 1600, scale: 1, frame: 100, idle: false }
1196
- * ```
1197
- */
1198
- snapshot(): Readonly<Time>;
1199
- /**
1200
- * Sets the time scale. Every frame delta is multiplied by it; 0 freezes the game time
1201
- * while the frames keep running. A negative scale is clamped to 0.
1202
- *
1203
- * @param scale - New time scale, 0 or greater.
1204
- * @example
1205
- * ```ts
1206
- * // Slow motion while the last match of the level resolves.
1207
- * app.time.setScale(0.5); // a frame of 20 ms now has delta 10
1208
- * app.time.setScale(1);
1209
- * ```
1210
- */
1211
- setScale(scale: number): void;
1212
- /**
1213
- * Pauses the clock: no phase runs and `elapsed` stops advancing. Called by `lifecycle`.
1225
+ * Reads trusted time. It never decreases, whatever the device clock does, so a player who sets
1226
+ * the clock back gains nothing.
1214
1227
  *
1228
+ * @returns The current moment in epoch milliseconds, an integer.
1215
1229
  * @example
1216
1230
  * ```ts
1217
- * // The lifecycle plugin owns the pause policy: the first pause reason stops the clock.
1218
- * // A game pauses through app.lifecycle.push(reason), never here.
1219
- * const time = ctx.require(timePlugin);
1220
- * if (reasons.length === 1) time.pause(); // app.time.isPaused() is true, no phase runs
1231
+ * // Stamp a generator when it is used, and compare later.
1232
+ * player.generator.usedAt = app.clock.now(); // 1790000000000
1233
+ * const ready = app.clock.now() - player.generator.usedAt >= 60_000;
1221
1234
  * ```
1222
1235
  */
1223
- pause(): void;
1236
+ now(): number;
1224
1237
  /**
1225
- * Resumes the clock and drops the stale timestamp, so the first frame after the pause has
1226
- * a normal delta instead of the whole pause. Called by `lifecycle`.
1238
+ * Replaces the single pending due moment. A moment in the past is not delivered synchronously:
1239
+ * it fires on the next macrotask of the source.
1227
1240
  *
1241
+ * @param moment - Moment in epoch milliseconds, or `undefined` to cancel.
1228
1242
  * @example
1229
1243
  * ```ts
1230
- * // The lifecycle plugin resumes when the last pause reason is gone.
1231
- * const time = ctx.require(timePlugin);
1232
- * if (reasons.length === 0) time.resume(); // the next frame has a normal delta, not the whole pause
1244
+ * // A generator refills in 60 s. Only one moment is pending: the nearest one.
1245
+ * app.clock.scheduleAt(app.clock.now() + 60_000);
1246
+ * app.clock.scheduleAt(undefined); // nothing is due any more: cancel
1233
1247
  * ```
1234
1248
  */
1235
- resume(): void;
1249
+ scheduleAt(moment: number | undefined): void;
1236
1250
  /**
1237
- * Tells whether the clock is paused.
1251
+ * Registers a listener for the `elapsed` input: a due moment arrived, or `poke` was called.
1252
+ * The clock does not reschedule by itself.
1238
1253
  *
1239
- * @returns True while paused.
1254
+ * @param listener - Called with `{ now }`, the trusted moment of the delivery.
1255
+ * @returns The unsubscribe function.
1240
1256
  * @example
1241
1257
  * ```ts
1242
- * // A test checks that a pause reason really stopped the frames.
1243
- * app.lifecycle.push("background");
1244
- * app.time.isPaused(); // true
1245
- * ```
1246
- */
1247
- isPaused(): boolean;
1248
- /**
1249
- * Tells whether a real frame source drives the loop. False in plain Bun, where a test
1250
- * drives the frames with `step`.
1258
+ * const off = app.clock.onElapsed(({ now }) => {
1259
+ * applyRefills(now); // now: 1790000060000
1260
+ * });
1251
1261
  *
1252
- * @returns True while the loop runs.
1253
- * @example
1254
- * ```ts
1255
- * // Without a frame source no frame ever comes: finish the fly-in at once instead of waiting.
1256
- * if (!app.time.isRunning()) coin.moveTo(target); // false in plain Bun, true in a browser
1262
+ * off(); // the screen that showed the timer is closed
1257
1263
  * ```
1258
1264
  */
1259
- isRunning(): boolean;
1265
+ onElapsed(listener: (input: Elapsed) => void): () => void;
1260
1266
  /**
1261
- * Resets the idle timer: the next frame runs at `maxFps` again, even after a long idle. Cheap,
1262
- * idempotent and safe inside a frame callback. Every plugin that gives the player something to
1263
- * look at calls it: a pointer sample, a starting animation, a flow edge, a finished load, a
1264
- * scene switch, a locale change, a reconcile.
1267
+ * Delivers `elapsed` right now, without touching the pending due moment. The engine calls it
1268
+ * when the game resumes; a game calls it after it changed something the rules depend on.
1265
1269
  *
1266
1270
  * @example
1267
1271
  * ```ts
1268
- * // The flow runner walked an edge, so the screen has work again and the cap goes back up.
1269
- * const time = ctx.require(timePlugin);
1270
- *
1271
- * time.wake(); // app.time.snapshot().idle is false from here on
1272
+ * // The game synced with its server and the time source jumped forward: let the rules catch up.
1273
+ * // On resume from background the engine pokes by itself, a game never does that.
1274
+ * app.clock.poke(); // onElapsed listeners get { now } at once; the pending moment stays
1272
1275
  * ```
1273
1276
  */
1274
- wake(): void;
1277
+ poke(): void;
1275
1278
  /**
1276
- * Runs exactly one frame with the given unscaled delta, ignoring the fps cap, the pause
1277
- * flag and `maxDeltaMs`. The time scale still applies. For tests and tools.
1279
+ * Reads the pending due moment, for inspection and tests.
1278
1280
  *
1279
- * @param deltaMs - Unscaled delta of the frame in milliseconds.
1280
- * @throws {Error} When called from inside a frame callback.
1281
+ * @returns The pending moment, or `undefined` when nothing is scheduled.
1281
1282
  * @example
1282
1283
  * ```ts
1283
- * // A test plays two frames without a browser.
1284
- * app.time.step(16);
1285
- * app.time.step(4);
1286
- * app.time.snapshot(); // { delta: 4, elapsed: 20, scale: 1, frame: 2, idle: false }
1284
+ * app.clock.scheduleAt(1500);
1285
+ * app.clock.dueAt(); // 1500
1286
+ * await app.stop();
1287
+ * app.clock.dueAt(); // undefined
1287
1288
  * ```
1288
1289
  */
1289
- step(deltaMs: number): void;
1290
+ dueAt(): number | undefined;
1290
1291
  };
1291
1292
  /**
1292
- * Domain context of the time plugin.
1293
+ * Domain context of the clock plugin.
1293
1294
  */
1294
- type TimeCtx = PluginCtx<Config$4, State$4> & {
1295
+ type ClockCtx = PluginCtx<Config$4, State$4> & {
1295
1296
  readonly global: object;
1296
- readonly log: Log.LogApi;
1297
1297
  };
1298
1298
  //#endregion
1299
1299
  //#region src/plugins/flow/gate/types.d.ts
@@ -2828,9 +2828,9 @@ type Api$3 = RunnerApi & {
2828
2828
  * shared by the runner, gate, inbox and fx.
2829
2829
  */
2830
2830
  type Deps$3 = {
2831
- time: Api$4;
2831
+ time: Api$7;
2832
2832
  model: Api$5;
2833
- clock: Api$7;
2833
+ clock: Api$4;
2834
2834
  };
2835
2835
  /**
2836
2836
  * What the kernel context offers before the deps are attached. `emit` is the kernel's: `index.ts`
@@ -4840,7 +4840,7 @@ type Api$2 = {
4840
4840
  * Resolved dependency APIs.
4841
4841
  */
4842
4842
  type Deps$2 = {
4843
- time: Api$4;
4843
+ time: Api$7;
4844
4844
  model: Api$5;
4845
4845
  flow: Api$3;
4846
4846
  };
@@ -6409,10 +6409,10 @@ type Under = {
6409
6409
  * Resolved dependency APIs. `clock` is the time source of the frame counters.
6410
6410
  */
6411
6411
  type Deps$1 = {
6412
- time: Api$4;
6412
+ time: Api$7;
6413
6413
  lifecycle: Api$6;
6414
6414
  world: Api$2;
6415
- clock: Api$7;
6415
+ clock: Api$4;
6416
6416
  };
6417
6417
  /**
6418
6418
  * What the kernel context offers before the deps are attached.
@@ -7153,7 +7153,7 @@ type Api = {
7153
7153
  type Deps = {
7154
7154
  flow: Api$3;
7155
7155
  renderer: Api$1;
7156
- time: Api$4;
7156
+ time: Api$7;
7157
7157
  };
7158
7158
  /**
7159
7159
  * What the kernel context offers before the deps are attached.
@@ -7181,4 +7181,4 @@ type FlowRest = {
7181
7181
  checkpoint: boolean;
7182
7182
  };
7183
7183
  //#endregion
7184
- export { Ease as $, PlayerStateProvider as $t, Config$2 as A, FlowState as At, ComponentValue as B, Api$4 as Bt, Captured as C, to as Ct, Point as D, Bookmark as Dt, FilterSlot as E, AnyFlow as Et, AnyComponentType as F, TypeTag as Ft, Narrowed as G, Api$5 as Gt, EntitySnapshot as H, State$4 as Ht, AnyComponentValue as I, Descriptor as It, ResourceType as J, Json as Jt, Owner as K, Config$5 as Kt, ComponentHandle as L, GuideOptions as Lt, State$2 as M, NodeTable as Mt, types_d_exports$2 as N, RouteStep as Nt, ViewportSize as O, DefineNode as Ot, AnyComponent as P, SceneIdOf as Pt, DescriptionNode as Q, Patch as Qt, ComponentSchema as R, Hint as Rt, types_d_exports$1 as S, slot as St, DebugSwitches as T, FeatureDescription as Tt, FrameDiff as U, Time as Ut, Entity as V, Config$4 as Vt, Mut as W, types_d_exports$4 as Wt, TagType as X, State$5 as Xt, SystemDefinition as Y, Root as Yt, ChangeHook as Z, types_d_exports$5 as Zt, PixiModule as _, State$7 as _n, State$3 as _t, DefineBundles as a, RngApi as an, ProjectionMotion as at, State$1 as b, Events$6 as bn, defineFlow as bt, LoadReason as c, Config$6 as cn, TrackSegment as ct, Tier as d, State$6 as dn, BundlesOf as dt, ProviderCall as en, LayerSort as et, Usage as f, types_d_exports$6 as fn, Config$3 as ft, PixiFilter as g, FakeClock as gn, Position as gt, Config$1 as h, Config$7 as hn, GameTypes as ht, Config as i, StoreApi as in, NumericFields as it, Explained as j, JournalEntry as jt, Api$2 as k, FlowGraph as kt, Manifest as l, Events$5 as ln, ViewHandle as lt, Api$1 as m, ClockSource as mn, FeaturePlugin as mt, BundleMap as n, SaveUnreadableError as nn, Motion as nt, Events as o, RngState as on, ProjectionSpec as ot, types_d_exports as p, Api$7 as pn, EmitterIdOf as pt, QueryTerm as q, Events$4 as qt, BundleSpec as r, Snapshot as rn, MotionHandle as rt, LoadBundles as s, Api$6 as sn, TrackOptions as st, Api as t, SaveDoc as tn, LayerSpec as tt, State as u, PauseReason as un, Api$3 as ut, PixiSprite as v, types_d_exports$7 as vn, TextStylesOf as vt, RenderStats as w, type as wt, Under as x, Require as xn, exit as xt, PixiTexture as y, Config$8 as yn, types_d_exports$3 as yt, ComponentType as z, Answer as zt };
7184
+ export { DescriptionNode as $, types_d_exports$5 as $t, Api$2 as A, FlowGraph as At, ComponentType as B, Answer as Bt, types_d_exports$1 as C, slot as Ct, FilterSlot as D, AnyFlow as Dt, DebugSwitches as E, FeatureDescription as Et, AnyComponent as F, SceneIdOf as Ft, Mut as G, State$4 as Gt, Entity as H, ClockSource as Ht, AnyComponentType as I, TypeTag as It, QueryTerm as J, Config$5 as Jt, Narrowed as K, types_d_exports$4 as Kt, AnyComponentValue as L, Descriptor as Lt, Explained as M, JournalEntry as Mt, State$2 as N, NodeTable as Nt, Point as O, Bookmark as Ot, types_d_exports$2 as P, RouteStep as Pt, ChangeHook as Q, State$5 as Qt, ComponentHandle as R, GuideOptions as Rt, Under as S, Require as Sn, exit as St, RenderStats as T, type as Tt, EntitySnapshot as U, Config$4 as Ut, ComponentValue as V, Api$4 as Vt, FrameDiff as W, FakeClock as Wt, SystemDefinition as X, Json as Xt, ResourceType as Y, Events$4 as Yt, TagType as Z, Root as Zt, PixiFilter as _, State$7 as _n, Position as _t, Config as a, Snapshot as an, NumericFields as at, PixiTexture as b, Config$8 as bn, types_d_exports$3 as bt, LoadBundles as c, RngState as cn, TrackOptions as ct, State as d, Events$5 as dn, Api$3 as dt, Patch as en, Ease as et, Tier as f, PauseReason as fn, BundlesOf as ft, Config$1 as g, Config$7 as gn, GameTypes as gt, Api$1 as h, Api$7 as hn, FeaturePlugin as ht, BundleSpec as i, SaveUnreadableError as in, MotionHandle as it, Config$2 as j, FlowState as jt, ViewportSize as k, DefineNode as kt, LoadReason as l, Api$6 as ln, TrackSegment as lt, types_d_exports as m, types_d_exports$6 as mn, EmitterIdOf as mt, AssetsIo as n, ProviderCall as nn, LayerSpec as nt, DefineBundles as o, StoreApi as on, ProjectionMotion as ot, Usage as p, State$6 as pn, Config$3 as pt, Owner as q, Api$5 as qt, BundleMap as r, SaveDoc as rn, Motion as rt, Events as s, RngApi as sn, ProjectionSpec as st, Api as t, PlayerStateProvider as tn, LayerSort as tt, Manifest as u, Config$6 as un, ViewHandle as ut, PixiModule as v, Time as vn, State$3 as vt, Captured as w, to as wt, State$1 as x, Events$6 as xn, defineFlow as xt, PixiSprite as y, types_d_exports$7 as yn, TextStylesOf as yt, ComponentSchema as z, Hint as zt };