@byloth/core 2.0.0-rc.9 → 2.0.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/dist/core.js +3371 -608
- package/dist/core.js.map +1 -1
- package/dist/core.umd.cjs +2 -2
- package/dist/core.umd.cjs.map +1 -1
- package/package.json +13 -10
- package/src/core/types.ts +41 -0
- package/src/helpers.ts +11 -2
- package/src/index.ts +12 -9
- package/src/models/aggregators/aggregated-async-iterator.ts +765 -21
- package/src/models/aggregators/aggregated-iterator.ts +698 -22
- package/src/models/aggregators/reduced-iterator.ts +699 -10
- package/src/models/aggregators/types.ts +153 -10
- package/src/models/callbacks/callable-object.ts +42 -6
- package/src/models/callbacks/index.ts +2 -2
- package/src/models/callbacks/publisher.ts +139 -4
- package/src/models/callbacks/switchable-callback.ts +138 -4
- package/src/models/callbacks/types.ts +16 -0
- package/src/models/exceptions/core.ts +112 -3
- package/src/models/exceptions/index.ts +340 -13
- package/src/models/index.ts +4 -8
- package/src/models/iterators/smart-async-iterator.ts +687 -22
- package/src/models/iterators/smart-iterator.ts +631 -21
- package/src/models/iterators/types.ts +268 -9
- package/src/models/json/json-storage.ts +388 -110
- package/src/models/json/types.ts +10 -1
- package/src/models/promises/deferred-promise.ts +75 -5
- package/src/models/promises/index.ts +1 -3
- package/src/models/promises/smart-promise.ts +232 -4
- package/src/models/promises/timed-promise.ts +38 -1
- package/src/models/promises/types.ts +84 -2
- package/src/models/timers/clock.ts +91 -19
- package/src/models/timers/countdown.ts +152 -22
- package/src/models/timers/game-loop.ts +243 -0
- package/src/models/timers/index.ts +2 -1
- package/src/models/types.ts +6 -5
- package/src/utils/async.ts +43 -0
- package/src/utils/curve.ts +75 -0
- package/src/utils/date.ts +204 -10
- package/src/utils/dom.ts +16 -2
- package/src/utils/index.ts +3 -2
- package/src/utils/iterator.ts +200 -17
- package/src/utils/math.ts +55 -3
- package/src/utils/random.ts +109 -2
- package/src/utils/string.ts +11 -0
- package/src/models/game-loop.ts +0 -83
- package/src/models/promises/long-running-task.ts +0 -294
- package/src/models/promises/thenable.ts +0 -97
|
@@ -1,20 +1,55 @@
|
|
|
1
1
|
import { TimeUnit } from "../../utils/date.js";
|
|
2
|
-
import { RangeException, RuntimeException } from "../exceptions/index.js";
|
|
3
2
|
|
|
4
3
|
import Publisher from "../callbacks/publisher.js";
|
|
5
|
-
import
|
|
4
|
+
import { FatalErrorException, RangeException, RuntimeException } from "../exceptions/index.js";
|
|
5
|
+
import type { Callback } from "../types.js";
|
|
6
|
+
|
|
7
|
+
import GameLoop from "./game-loop.js";
|
|
6
8
|
|
|
7
9
|
interface ClockEventMap
|
|
8
10
|
{
|
|
9
11
|
start: () => void;
|
|
10
12
|
stop: () => void;
|
|
11
13
|
tick: (elapsedTime: number) => void;
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
[key: string]: Callback<any[], any>;
|
|
12
17
|
}
|
|
13
18
|
|
|
19
|
+
/**
|
|
20
|
+
* A class representing a clock.
|
|
21
|
+
*
|
|
22
|
+
* It can be started, stopped and, when running, it ticks at a specific frame rate.
|
|
23
|
+
* It's possible to subscribe to these events to receive notifications when they occur.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* const clock = new Clock();
|
|
27
|
+
*
|
|
28
|
+
* clock.onStart(() => { console.log("The clock has started."); });
|
|
29
|
+
* clock.onTick((elapsedTime) => { console.log(`The clock has ticked at ${elapsedTime}ms.`); });
|
|
30
|
+
* clock.onStop(() => { console.log("The clock has stopped."); });
|
|
31
|
+
*
|
|
32
|
+
* clock.start();
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
14
35
|
export default class Clock extends GameLoop
|
|
15
36
|
{
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The {@link Publisher} object that will be used to publish the events of the clock.
|
|
39
|
+
*/
|
|
40
|
+
protected override _publisher: Publisher<ClockEventMap>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Initializes a new instance of the {@link Clock} class.
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* const clock = new Clock();
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @param msIfNotBrowser
|
|
50
|
+
* The interval in milliseconds at which the clock will tick if the environment is not a browser.
|
|
51
|
+
* `TimeUnit.Second` by default.
|
|
52
|
+
*/
|
|
18
53
|
public constructor(msIfNotBrowser: number = TimeUnit.Second)
|
|
19
54
|
{
|
|
20
55
|
super((elapsedTime) => this._publisher.publish("tick", elapsedTime), msIfNotBrowser);
|
|
@@ -22,33 +57,70 @@ export default class Clock extends GameLoop
|
|
|
22
57
|
this._publisher = new Publisher();
|
|
23
58
|
}
|
|
24
59
|
|
|
25
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Starts the execution of the clock.
|
|
62
|
+
*
|
|
63
|
+
* If the clock is already running, a {@link RuntimeException} will be thrown.
|
|
64
|
+
*
|
|
65
|
+
* ```ts
|
|
66
|
+
* clock.onStart(() => { [...] }); // This callback will be executed.
|
|
67
|
+
* clock.start();
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @param elapsedTime The elapsed time to set as default when the clock starts. Default is `0`.
|
|
71
|
+
*/
|
|
72
|
+
public override start(elapsedTime = 0): void
|
|
26
73
|
{
|
|
27
74
|
if (this._isRunning) { throw new RuntimeException("The clock has already been started."); }
|
|
28
75
|
|
|
29
|
-
|
|
76
|
+
this._startTime = performance.now() - elapsedTime;
|
|
77
|
+
this._start();
|
|
78
|
+
this._isRunning = true;
|
|
30
79
|
|
|
31
80
|
this._publisher.publish("start");
|
|
32
81
|
}
|
|
33
82
|
|
|
34
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Stops the execution of the clock.
|
|
85
|
+
*
|
|
86
|
+
* If the clock hasn't yet started, a {@link RuntimeException} will be thrown.
|
|
87
|
+
*
|
|
88
|
+
* ```ts
|
|
89
|
+
* clock.onStop(() => { [...] }); // This callback will be executed.
|
|
90
|
+
* clock.stop();
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
public override stop(): void
|
|
35
94
|
{
|
|
36
|
-
if (!(this._isRunning)) { throw new RuntimeException("The clock hadn't yet started."); }
|
|
95
|
+
if (!(this._isRunning)) { throw new RuntimeException("The clock had already stopped or hadn't yet started."); }
|
|
96
|
+
if (!(this._handle)) { throw new FatalErrorException(); }
|
|
37
97
|
|
|
38
|
-
|
|
98
|
+
this._stop();
|
|
99
|
+
this._handle = undefined;
|
|
100
|
+
this._isRunning = false;
|
|
39
101
|
|
|
40
102
|
this._publisher.publish("stop");
|
|
41
103
|
}
|
|
42
104
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Subscribes to the `tick` event of the clock.
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* clock.onTick((elapsedTime) => { [...] }); // This callback will be executed.
|
|
110
|
+
* clock.start();
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* @param callback The callback that will be executed when the clock ticks.
|
|
114
|
+
* @param tickStep
|
|
115
|
+
* The minimum time in milliseconds that must pass from the previous execution of the callback to the next one.
|
|
116
|
+
*
|
|
117
|
+
* - If it's a positive number, the callback will be executed only if the
|
|
118
|
+
* time passed from the previous execution is greater than this number.
|
|
119
|
+
* - If it's `0`, the callback will be executed every tick without even checking for the time.
|
|
120
|
+
* - If it's a negative number, a {@link RangeException} will be thrown.
|
|
121
|
+
*
|
|
122
|
+
* @returns A function that can be used to unsubscribe from the event.
|
|
123
|
+
*/
|
|
52
124
|
public onTick(callback: (elapsedTime: number) => void, tickStep = 0): () => void
|
|
53
125
|
{
|
|
54
126
|
if (tickStep < 0) { throw new RangeException("The tick step must be a non-negative number."); }
|
|
@@ -65,5 +137,5 @@ export default class Clock extends GameLoop
|
|
|
65
137
|
});
|
|
66
138
|
}
|
|
67
139
|
|
|
68
|
-
public readonly [Symbol.toStringTag]: string = "Clock";
|
|
140
|
+
public override readonly [Symbol.toStringTag]: string = "Clock";
|
|
69
141
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { TimeUnit } from "../../utils/date.js";
|
|
2
2
|
|
|
3
|
+
import Publisher from "../callbacks/publisher.js";
|
|
3
4
|
import { FatalErrorException, RangeException, RuntimeException } from "../exceptions/index.js";
|
|
4
5
|
import { DeferredPromise, SmartPromise } from "../promises/index.js";
|
|
6
|
+
import type { Callback } from "../types.js";
|
|
5
7
|
|
|
6
|
-
import
|
|
7
|
-
import GameLoop from "../game-loop.js";
|
|
8
|
+
import GameLoop from "./game-loop.js";
|
|
8
9
|
|
|
9
10
|
interface CountdownEventMap
|
|
10
11
|
{
|
|
@@ -12,37 +13,95 @@ interface CountdownEventMap
|
|
|
12
13
|
stop: (reason: unknown) => void;
|
|
13
14
|
tick: (remainingTime: number) => void;
|
|
14
15
|
expire: () => void;
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
[key: string]: Callback<any[], any>;
|
|
15
19
|
}
|
|
16
20
|
|
|
21
|
+
/**
|
|
22
|
+
* A class representing a countdown.
|
|
23
|
+
*
|
|
24
|
+
* It can be started, stopped, when running it ticks at a specific frame rate and it expires when the time's up.
|
|
25
|
+
* It's possible to subscribe to these events to receive notifications when they occur.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* const countdown = new Countdown(10_000);
|
|
29
|
+
*
|
|
30
|
+
* countdown.onStart(() => { console.log("The countdown has started."); });
|
|
31
|
+
* countdown.onTick((remainingTime) => { console.log(`The countdown has ${remainingTime}ms remaining.`); });
|
|
32
|
+
* countdown.onStop((reason) => { console.log(`The countdown has stopped because of ${reason}.`); });
|
|
33
|
+
* countdown.onExpire(() => { console.log("The countdown has expired."); });
|
|
34
|
+
*
|
|
35
|
+
* countdown.start();
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
17
38
|
export default class Countdown extends GameLoop
|
|
18
39
|
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The {@link Publisher} object that will be used to publish the events of the countdown.
|
|
42
|
+
*/
|
|
43
|
+
protected override _publisher: Publisher<CountdownEventMap>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The total duration of the countdown in milliseconds.
|
|
47
|
+
*
|
|
48
|
+
* This protected property is the only one that can be modified directly by the derived classes.
|
|
49
|
+
* If you're looking for the public and readonly property, use the {@link Countdown.duration} getter instead.
|
|
50
|
+
*/
|
|
22
51
|
protected _duration: number;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The total duration of the countdown in milliseconds.
|
|
55
|
+
*/
|
|
23
56
|
public get duration(): number
|
|
24
57
|
{
|
|
25
58
|
return this._duration;
|
|
26
59
|
}
|
|
27
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The remaining time of the countdown in milliseconds.
|
|
63
|
+
* It's calculated as the difference between the total duration and the elapsed time.
|
|
64
|
+
*/
|
|
28
65
|
public get remainingTime(): number
|
|
29
66
|
{
|
|
30
67
|
return this._duration - this.elapsedTime;
|
|
31
68
|
}
|
|
32
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The {@link DeferredPromise} that will be resolved or rejected when the countdown expires or stops.
|
|
72
|
+
*/
|
|
73
|
+
protected _deferrer?: DeferredPromise<void>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Initializes a new instance of the {@link Countdown} class.
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* const countdown = new Countdown(10_000);
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @param duration
|
|
83
|
+
* The total duration of the countdown in milliseconds.
|
|
84
|
+
*
|
|
85
|
+
* @param msIfNotBrowser
|
|
86
|
+
* The interval in milliseconds at which the countdown will tick if the environment is not a browser.
|
|
87
|
+
* `TimeUnit.Second` by default.
|
|
88
|
+
*/
|
|
33
89
|
public constructor(duration: number, msIfNotBrowser: number = TimeUnit.Second)
|
|
34
90
|
{
|
|
35
91
|
const callback = () =>
|
|
36
92
|
{
|
|
37
93
|
const remainingTime = this.remainingTime;
|
|
38
|
-
this._publisher.publish("tick", remainingTime);
|
|
39
|
-
|
|
40
94
|
if (remainingTime <= 0)
|
|
41
95
|
{
|
|
42
96
|
this._deferrerStop();
|
|
43
97
|
|
|
98
|
+
this._publisher.publish("tick", 0);
|
|
44
99
|
this._publisher.publish("expire");
|
|
45
100
|
}
|
|
101
|
+
else
|
|
102
|
+
{
|
|
103
|
+
this._publisher.publish("tick", remainingTime);
|
|
104
|
+
}
|
|
46
105
|
};
|
|
47
106
|
|
|
48
107
|
super(callback, msIfNotBrowser);
|
|
@@ -51,12 +110,24 @@ export default class Countdown extends GameLoop
|
|
|
51
110
|
this._duration = duration;
|
|
52
111
|
}
|
|
53
112
|
|
|
113
|
+
/**
|
|
114
|
+
* The internal method actually responsible for stopping the
|
|
115
|
+
* countdown and resolving or rejecting the {@link Countdown._deferrer} promise.
|
|
116
|
+
*
|
|
117
|
+
* @param reason
|
|
118
|
+
* The reason why the countdown has stopped.
|
|
119
|
+
*
|
|
120
|
+
* - If it's `undefined`, the promise will be resolved.
|
|
121
|
+
* - If it's a value, the promise will be rejected with that value.
|
|
122
|
+
*/
|
|
54
123
|
protected _deferrerStop(reason?: unknown): void
|
|
55
124
|
{
|
|
56
125
|
if (!(this._isRunning)) { throw new RuntimeException("The countdown hadn't yet started."); }
|
|
57
126
|
if (!(this._deferrer)) { throw new FatalErrorException(); }
|
|
58
127
|
|
|
59
|
-
|
|
128
|
+
this._stop();
|
|
129
|
+
this._handle = undefined;
|
|
130
|
+
this._isRunning = false;
|
|
60
131
|
|
|
61
132
|
if (reason !== undefined) { this._deferrer.reject(reason); }
|
|
62
133
|
else { this._deferrer.resolve(); }
|
|
@@ -64,9 +135,25 @@ export default class Countdown extends GameLoop
|
|
|
64
135
|
this._deferrer = undefined;
|
|
65
136
|
}
|
|
66
137
|
|
|
67
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Starts the execution of the countdown.
|
|
140
|
+
*
|
|
141
|
+
* If the countdown is already running, a {@link RuntimeException} will be thrown.
|
|
142
|
+
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* countdown.onStart(() => { [...] }); // This callback will be executed.
|
|
145
|
+
* countdown.start();
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* @param remainingTime
|
|
149
|
+
* The remaining time to set as default when the countdown starts.
|
|
150
|
+
* Default is the {@link Countdown.duration} itself.
|
|
151
|
+
*
|
|
152
|
+
* @returns A {@link SmartPromise} that will be resolved or rejected when the countdown expires or stops.
|
|
153
|
+
*/
|
|
154
|
+
public override start(remainingTime: number = this.duration): SmartPromise<void>
|
|
68
155
|
{
|
|
69
|
-
if (this._isRunning) { throw new RuntimeException("The countdown
|
|
156
|
+
if (this._isRunning) { throw new RuntimeException("The countdown had already stopped or hadn't yet started."); }
|
|
70
157
|
if (this._deferrer) { throw new FatalErrorException(); }
|
|
71
158
|
|
|
72
159
|
this._deferrer = new DeferredPromise();
|
|
@@ -76,33 +163,76 @@ export default class Countdown extends GameLoop
|
|
|
76
163
|
|
|
77
164
|
return this._deferrer;
|
|
78
165
|
}
|
|
79
|
-
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Stops the execution of the countdown.
|
|
169
|
+
*
|
|
170
|
+
* If the countdown hasn't yet started, a {@link RuntimeException} will be thrown.
|
|
171
|
+
*
|
|
172
|
+
* ```ts
|
|
173
|
+
* countdown.onStop(() => { [...] }); // This callback will be executed.
|
|
174
|
+
* countdown.stop();
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* @param reason
|
|
178
|
+
* The reason why the countdown has stopped.
|
|
179
|
+
*
|
|
180
|
+
* - If it's `undefined`, the promise will be resolved.
|
|
181
|
+
* - If it's a value, the promise will be rejected with that value.
|
|
182
|
+
*/
|
|
183
|
+
public override stop(reason?: unknown): void
|
|
80
184
|
{
|
|
185
|
+
// TODO: Once solved Issues #6 & #10, make the `reason` parameter required.
|
|
186
|
+
// - https://github.com/Byloth/core/issues/6
|
|
187
|
+
// - https://github.com/Byloth/core/issues/10
|
|
188
|
+
//
|
|
81
189
|
this._deferrerStop(reason);
|
|
82
190
|
|
|
83
191
|
this._publisher.publish("stop", reason);
|
|
84
192
|
}
|
|
85
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Subscribes to the `expire` event of the countdown.
|
|
196
|
+
*
|
|
197
|
+
* ```ts
|
|
198
|
+
* countdown.onExpire(() => { [...] }); // This callback will be executed once the countdown has expired.
|
|
199
|
+
* countdown.start();
|
|
200
|
+
* ```
|
|
201
|
+
*
|
|
202
|
+
* @param callback The callback that will be executed when the countdown expires.
|
|
203
|
+
*
|
|
204
|
+
* @returns A function that can be used to unsubscribe from the event.
|
|
205
|
+
*/
|
|
86
206
|
public onExpire(callback: () => void): () => void
|
|
87
207
|
{
|
|
88
208
|
return this._publisher.subscribe("expire", callback);
|
|
89
209
|
}
|
|
90
210
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Subscribes to the `tick` event of the countdown.
|
|
213
|
+
*
|
|
214
|
+
* ```ts
|
|
215
|
+
* countdown.onTick((remainingTime) => { [...] }); // This callback will be executed.
|
|
216
|
+
* countdown.start();
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* @param callback The callback that will be executed when the countdown ticks.
|
|
220
|
+
* @param tickStep
|
|
221
|
+
* The minimum time in milliseconds that must pass from the previous execution of the callback to the next one.
|
|
222
|
+
*
|
|
223
|
+
* - If it's a positive number, the callback will be executed only if the
|
|
224
|
+
* time passed from the previous execution is greater than this number.
|
|
225
|
+
* - If it's `0`, the callback will be executed every tick without even checking for the time.
|
|
226
|
+
* - If it's a negative number, a {@link RangeException} will be thrown.
|
|
227
|
+
*
|
|
228
|
+
* @returns A function that can be used to unsubscribe from the event.
|
|
229
|
+
*/
|
|
100
230
|
public onTick(callback: (remainingTime: number) => void, tickStep = 0): () => void
|
|
101
231
|
{
|
|
102
232
|
if (tickStep < 0) { throw new RangeException("The tick step must be a non-negative number."); }
|
|
103
233
|
if (tickStep === 0) { return this._publisher.subscribe("tick", callback); }
|
|
104
234
|
|
|
105
|
-
let lastTick =
|
|
235
|
+
let lastTick = this.remainingTime;
|
|
106
236
|
|
|
107
237
|
return this._publisher.subscribe("tick", (remainingTime: number) =>
|
|
108
238
|
{
|
|
@@ -113,5 +243,5 @@ export default class Countdown extends GameLoop
|
|
|
113
243
|
});
|
|
114
244
|
}
|
|
115
245
|
|
|
116
|
-
public readonly [Symbol.toStringTag]: string = "Countdown";
|
|
246
|
+
public override readonly [Symbol.toStringTag]: string = "Countdown";
|
|
117
247
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { Interval } from "../../core/types.js";
|
|
2
|
+
import { isBrowser } from "../../helpers.js";
|
|
3
|
+
|
|
4
|
+
import Publisher from "../callbacks/publisher.js";
|
|
5
|
+
import { FatalErrorException, RuntimeException } from "../exceptions/index.js";
|
|
6
|
+
import type { Callback } from "../types.js";
|
|
7
|
+
|
|
8
|
+
interface GameLoopEventMap
|
|
9
|
+
{
|
|
10
|
+
start: () => void;
|
|
11
|
+
stop: () => void;
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
[key: string]: Callback<any[], any>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A class representing a {@link https://en.wikipedia.org/wiki/Video_game_programming#Game_structure|game loop} pattern
|
|
19
|
+
* that allows to run a function at a specific frame rate.
|
|
20
|
+
*
|
|
21
|
+
* In a browser environment, it uses the native {@link requestAnimationFrame}
|
|
22
|
+
* function to run the callback at the refresh rate of the screen.
|
|
23
|
+
* In a non-browser environment, however, it uses the {@link setInterval}
|
|
24
|
+
* function to run the callback at the specified fixed interval of time.
|
|
25
|
+
*
|
|
26
|
+
* Every time the callback is executed, it receives the
|
|
27
|
+
* elapsed time since the start of the game loop.
|
|
28
|
+
* It's also possible to subscribe to the `start` & `stop` events to receive notifications when they occur.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const loop = new GameLoop((elapsedTime: number) =>
|
|
32
|
+
* {
|
|
33
|
+
* console.log(`The game loop has been running for ${elapsedTime}ms.`);
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* loop.onStart(() => { console.log("The game loop has started."); });
|
|
37
|
+
* loop.onStop(() => { console.log("The game loop has stopped."); });
|
|
38
|
+
*
|
|
39
|
+
* loop.start();
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export default class GameLoop
|
|
43
|
+
{
|
|
44
|
+
/**
|
|
45
|
+
* The handle of the interval or the animation frame, depending on the environment.
|
|
46
|
+
* It's used to stop the game loop when the {@link GameLoop._stop} method is called.
|
|
47
|
+
*/
|
|
48
|
+
protected _handle?: number | Interval;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The time when the game loop has started.
|
|
52
|
+
* In addition to indicating the {@link https://en.wikipedia.org/wiki/Unix_time|Unix timestamp}
|
|
53
|
+
* of the start of the game loop, it's also used to calculate the elapsed time.
|
|
54
|
+
*
|
|
55
|
+
* This protected property is the only one that can be modified directly by the derived classes.
|
|
56
|
+
* If you're looking for the public and readonly property, use the {@link GameLoop.startTime} getter instead.
|
|
57
|
+
*/
|
|
58
|
+
protected _startTime: number;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The time when the game loop has started.
|
|
62
|
+
* In addition to indicating the {@link https://en.wikipedia.org/wiki/Unix_time|Unix timestamp}
|
|
63
|
+
* of the start of the game loop, it's also used to calculate the elapsed time.
|
|
64
|
+
*/
|
|
65
|
+
public get startTime(): number
|
|
66
|
+
{
|
|
67
|
+
return this._startTime;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A flag indicating whether the game loop is currently running or not.
|
|
72
|
+
*
|
|
73
|
+
* This protected property is the only one that can be modified directly by the derived classes.
|
|
74
|
+
* If you're looking for the public and readonly property, use the {@link GameLoop.isRunning} getter instead.
|
|
75
|
+
*/
|
|
76
|
+
protected _isRunning: boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A flag indicating whether the game loop is currently running or not.
|
|
80
|
+
*/
|
|
81
|
+
public get isRunning(): boolean
|
|
82
|
+
{
|
|
83
|
+
return this._isRunning;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The elapsed time since the start of the game loop.
|
|
88
|
+
* It's calculated as the difference between the current time and the {@link GameLoop.startTime}.
|
|
89
|
+
*/
|
|
90
|
+
public get elapsedTime(): number
|
|
91
|
+
{
|
|
92
|
+
return performance.now() - this._startTime;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The {@link Publisher} object that will be used to publish the events of the game loop.
|
|
97
|
+
*/
|
|
98
|
+
protected _publisher: Publisher<GameLoopEventMap>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The internal method actually responsible for starting the game loop.
|
|
102
|
+
*
|
|
103
|
+
* Depending on the current environment, it could use the
|
|
104
|
+
* {@link requestAnimationFrame} or the {@link setInterval} function.
|
|
105
|
+
*/
|
|
106
|
+
protected _start: () => void;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The internal method actually responsible for stopping the game loop.
|
|
110
|
+
*
|
|
111
|
+
* Depending on the current environment, it could use the
|
|
112
|
+
* {@link cancelAnimationFrame} or the {@link clearInterval} function.
|
|
113
|
+
*/
|
|
114
|
+
protected _stop: () => void;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Initializes a new instance of the {@link GameLoop} class.
|
|
118
|
+
*
|
|
119
|
+
* ```ts
|
|
120
|
+
* const loop = new GameLoop((elapsedTime: number) => { [...] });
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* @param callback The function that will be executed at each iteration of the game loop.
|
|
124
|
+
* @param msIfNotBrowser
|
|
125
|
+
* The interval in milliseconds that will be used if the current environment isn't a browser. Default is `40`.
|
|
126
|
+
*/
|
|
127
|
+
public constructor(callback: FrameRequestCallback, msIfNotBrowser = 40)
|
|
128
|
+
{
|
|
129
|
+
this._startTime = 0;
|
|
130
|
+
this._isRunning = false;
|
|
131
|
+
|
|
132
|
+
if (isBrowser)
|
|
133
|
+
{
|
|
134
|
+
this._start = () =>
|
|
135
|
+
{
|
|
136
|
+
callback(this.elapsedTime);
|
|
137
|
+
|
|
138
|
+
this._handle = window.requestAnimationFrame(this._start);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
this._stop = () => window.cancelAnimationFrame(this._handle as number);
|
|
142
|
+
}
|
|
143
|
+
else
|
|
144
|
+
{
|
|
145
|
+
// eslint-disable-next-line no-console
|
|
146
|
+
console.warn(
|
|
147
|
+
"Not a browser environment detected. " +
|
|
148
|
+
`Using setInterval@${msIfNotBrowser}ms instead of requestAnimationFrame...`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
this._start = () =>
|
|
152
|
+
{
|
|
153
|
+
this._handle = setInterval(() => callback(this.elapsedTime), msIfNotBrowser);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
this._stop = () => clearInterval(this._handle as Interval);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this._publisher = new Publisher();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Starts the execution of the game loop.
|
|
164
|
+
*
|
|
165
|
+
* If the game loop is already running, a {@link RuntimeException} will be thrown.
|
|
166
|
+
*
|
|
167
|
+
* ```ts
|
|
168
|
+
* loop.onStart(() => { [...] }); // This callback will be executed.
|
|
169
|
+
* loop.start();
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @param elapsedTime The elapsed time to set as default when the game loop starts. Default is `0`.
|
|
173
|
+
*/
|
|
174
|
+
public start(elapsedTime = 0): void
|
|
175
|
+
{
|
|
176
|
+
if (this._isRunning) { throw new RuntimeException("The game loop has already been started."); }
|
|
177
|
+
|
|
178
|
+
this._startTime = performance.now() - elapsedTime;
|
|
179
|
+
this._start();
|
|
180
|
+
this._isRunning = true;
|
|
181
|
+
|
|
182
|
+
this._publisher.publish("start");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Stops the execution of the game loop.
|
|
187
|
+
*
|
|
188
|
+
* If the game loop hasn't yet started, a {@link RuntimeException} will be thrown.
|
|
189
|
+
*
|
|
190
|
+
* ```ts
|
|
191
|
+
* loop.onStop(() => { [...] }); // This callback will be executed.
|
|
192
|
+
* loop.stop();
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
public stop(): void
|
|
196
|
+
{
|
|
197
|
+
if (!(this._isRunning))
|
|
198
|
+
{
|
|
199
|
+
throw new RuntimeException("The game loop had already stopped or hadn't yet started.");
|
|
200
|
+
}
|
|
201
|
+
if (!(this._handle)) { throw new FatalErrorException(); }
|
|
202
|
+
|
|
203
|
+
this._stop();
|
|
204
|
+
this._handle = undefined;
|
|
205
|
+
this._isRunning = false;
|
|
206
|
+
|
|
207
|
+
this._publisher.publish("stop");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Subscribes to the `start` event of the game loop.
|
|
212
|
+
*
|
|
213
|
+
* ```ts
|
|
214
|
+
* loop.onStart(() => { console.log("The game loop has started."); });
|
|
215
|
+
* ```
|
|
216
|
+
*
|
|
217
|
+
* @param callback The function that will be executed when the game loop starts.
|
|
218
|
+
*
|
|
219
|
+
* @returns A function that can be used to unsubscribe from the event.
|
|
220
|
+
*/
|
|
221
|
+
public onStart(callback: () => void): () => void
|
|
222
|
+
{
|
|
223
|
+
return this._publisher.subscribe("start", callback);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Subscribes to the `stop` event of the game loop.
|
|
228
|
+
*
|
|
229
|
+
* ```ts
|
|
230
|
+
* loop.onStop(() => { console.log("The game loop has stopped."); });
|
|
231
|
+
* ```
|
|
232
|
+
*
|
|
233
|
+
* @param callback The function that will be executed when the game loop stops.
|
|
234
|
+
*
|
|
235
|
+
* @returns A function that can be used to unsubscribe from the event.
|
|
236
|
+
*/
|
|
237
|
+
public onStop(callback: () => void): () => void
|
|
238
|
+
{
|
|
239
|
+
return this._publisher.subscribe("stop", callback);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
public readonly [Symbol.toStringTag]: string = "GameLoop";
|
|
243
|
+
}
|
package/src/models/types.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export type {
|
|
2
2
|
KeyedIteratee,
|
|
3
|
+
AsyncKeyedIteratee,
|
|
3
4
|
MaybeAsyncKeyedIteratee,
|
|
4
|
-
|
|
5
|
-
MaybeAsyncKeyedTypeGuardIteratee,
|
|
5
|
+
KeyedTypeGuardPredicate,
|
|
6
6
|
KeyedReducer,
|
|
7
|
+
AsyncKeyedReducer,
|
|
7
8
|
MaybeAsyncKeyedReducer
|
|
8
9
|
|
|
9
10
|
} from "./aggregators/types.js";
|
|
@@ -13,10 +14,11 @@ export type {
|
|
|
13
14
|
AsyncGeneratorFunction,
|
|
14
15
|
MaybeAsyncGeneratorFunction,
|
|
15
16
|
Iteratee,
|
|
17
|
+
AsyncIteratee,
|
|
16
18
|
MaybeAsyncIteratee,
|
|
17
|
-
|
|
18
|
-
MaybeAsyncTypeGuardIteratee,
|
|
19
|
+
TypeGuardPredicate,
|
|
19
20
|
Reducer,
|
|
21
|
+
AsyncReducer,
|
|
20
22
|
MaybeAsyncReducer,
|
|
21
23
|
IteratorLike,
|
|
22
24
|
AsyncIteratorLike,
|
|
@@ -32,7 +34,6 @@ export type {
|
|
|
32
34
|
} from "./json/types.js";
|
|
33
35
|
|
|
34
36
|
export type {
|
|
35
|
-
LongRunningTaskOptions,
|
|
36
37
|
MaybePromise,
|
|
37
38
|
FulfilledHandler,
|
|
38
39
|
RejectedHandler,
|