murow 0.0.1

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 (103) hide show
  1. package/README.md +61 -0
  2. package/dist/core/binary-codec/binary-codec.d.ts +159 -0
  3. package/dist/core/binary-codec/binary-codec.js +336 -0
  4. package/dist/core/binary-codec/index.d.ts +1 -0
  5. package/dist/core/binary-codec/index.js +1 -0
  6. package/dist/core/events/event-system.d.ts +71 -0
  7. package/dist/core/events/event-system.js +88 -0
  8. package/dist/core/events/index.d.ts +1 -0
  9. package/dist/core/events/index.js +1 -0
  10. package/dist/core/fixed-ticker/fixed-ticker.d.ts +105 -0
  11. package/dist/core/fixed-ticker/fixed-ticker.js +91 -0
  12. package/dist/core/fixed-ticker/index.d.ts +1 -0
  13. package/dist/core/fixed-ticker/index.js +1 -0
  14. package/dist/core/generate-id/generate-id.d.ts +21 -0
  15. package/dist/core/generate-id/generate-id.js +25 -0
  16. package/dist/core/generate-id/index.d.ts +1 -0
  17. package/dist/core/generate-id/index.js +1 -0
  18. package/dist/core/index.d.ts +8 -0
  19. package/dist/core/index.js +8 -0
  20. package/dist/core/lerp/index.d.ts +1 -0
  21. package/dist/core/lerp/index.js +1 -0
  22. package/dist/core/lerp/lerp.d.ts +40 -0
  23. package/dist/core/lerp/lerp.js +42 -0
  24. package/dist/core/navmesh/index.d.ts +1 -0
  25. package/dist/core/navmesh/index.js +1 -0
  26. package/dist/core/navmesh/navmesh.d.ts +116 -0
  27. package/dist/core/navmesh/navmesh.js +666 -0
  28. package/dist/core/pooled-codec/index.d.ts +1 -0
  29. package/dist/core/pooled-codec/index.js +1 -0
  30. package/dist/core/pooled-codec/pooled-codec.d.ts +140 -0
  31. package/dist/core/pooled-codec/pooled-codec.js +213 -0
  32. package/dist/core/prediction/index.d.ts +1 -0
  33. package/dist/core/prediction/index.js +1 -0
  34. package/dist/core/prediction/prediction.d.ts +64 -0
  35. package/dist/core/prediction/prediction.js +90 -0
  36. package/dist/core.esm.js +1 -0
  37. package/dist/core.js +1 -0
  38. package/dist/index.d.ts +16 -0
  39. package/dist/index.js +18 -0
  40. package/dist/protocol/index.d.ts +43 -0
  41. package/dist/protocol/index.js +43 -0
  42. package/dist/protocol/intent/index.d.ts +39 -0
  43. package/dist/protocol/intent/index.js +38 -0
  44. package/dist/protocol/intent/intent-registry.d.ts +54 -0
  45. package/dist/protocol/intent/intent-registry.js +73 -0
  46. package/dist/protocol/intent/intent.d.ts +12 -0
  47. package/dist/protocol/intent/intent.js +1 -0
  48. package/dist/protocol/snapshot/index.d.ts +44 -0
  49. package/dist/protocol/snapshot/index.js +43 -0
  50. package/dist/protocol/snapshot/snapshot-codec.d.ts +48 -0
  51. package/dist/protocol/snapshot/snapshot-codec.js +56 -0
  52. package/dist/protocol/snapshot/snapshot-registry.d.ts +100 -0
  53. package/dist/protocol/snapshot/snapshot-registry.js +136 -0
  54. package/dist/protocol/snapshot/snapshot.d.ts +19 -0
  55. package/dist/protocol/snapshot/snapshot.js +30 -0
  56. package/package.json +54 -0
  57. package/src/core/binary-codec/README.md +60 -0
  58. package/src/core/binary-codec/binary-codec.test.ts +300 -0
  59. package/src/core/binary-codec/binary-codec.ts +430 -0
  60. package/src/core/binary-codec/index.ts +1 -0
  61. package/src/core/events/README.md +47 -0
  62. package/src/core/events/event-system.test.ts +243 -0
  63. package/src/core/events/event-system.ts +140 -0
  64. package/src/core/events/index.ts +1 -0
  65. package/src/core/fixed-ticker/README.md +77 -0
  66. package/src/core/fixed-ticker/fixed-ticker.test.ts +151 -0
  67. package/src/core/fixed-ticker/fixed-ticker.ts +158 -0
  68. package/src/core/fixed-ticker/index.ts +1 -0
  69. package/src/core/generate-id/README.md +18 -0
  70. package/src/core/generate-id/generate-id.test.ts +79 -0
  71. package/src/core/generate-id/generate-id.ts +37 -0
  72. package/src/core/generate-id/index.ts +1 -0
  73. package/src/core/index.ts +8 -0
  74. package/src/core/lerp/README.md +79 -0
  75. package/src/core/lerp/index.ts +1 -0
  76. package/src/core/lerp/lerp.test.ts +90 -0
  77. package/src/core/lerp/lerp.ts +42 -0
  78. package/src/core/navmesh/README.md +124 -0
  79. package/src/core/navmesh/index.ts +1 -0
  80. package/src/core/navmesh/navmesh.test.ts +344 -0
  81. package/src/core/navmesh/navmesh.ts +850 -0
  82. package/src/core/pooled-codec/README.md +70 -0
  83. package/src/core/pooled-codec/index.ts +1 -0
  84. package/src/core/pooled-codec/pooled-codec.test.ts +349 -0
  85. package/src/core/pooled-codec/pooled-codec.ts +239 -0
  86. package/src/core/prediction/README.md +64 -0
  87. package/src/core/prediction/index.ts +1 -0
  88. package/src/core/prediction/prediction.test.ts +422 -0
  89. package/src/core/prediction/prediction.ts +101 -0
  90. package/src/index.ts +20 -0
  91. package/src/protocol/README.md +310 -0
  92. package/src/protocol/index.ts +44 -0
  93. package/src/protocol/intent/index.ts +40 -0
  94. package/src/protocol/intent/intent-registry.test.ts +237 -0
  95. package/src/protocol/intent/intent-registry.ts +88 -0
  96. package/src/protocol/intent/intent.ts +12 -0
  97. package/src/protocol/snapshot/index.ts +45 -0
  98. package/src/protocol/snapshot/snapshot-codec.test.ts +138 -0
  99. package/src/protocol/snapshot/snapshot-codec.ts +71 -0
  100. package/src/protocol/snapshot/snapshot-registry.test.ts +302 -0
  101. package/src/protocol/snapshot/snapshot-registry.ts +162 -0
  102. package/src/protocol/snapshot/snapshot.test.ts +76 -0
  103. package/src/protocol/snapshot/snapshot.ts +41 -0
@@ -0,0 +1,243 @@
1
+ import { describe, expect, test, spyOn } from "bun:test";
2
+ import { EventSystem } from "./event-system";
3
+
4
+ type TestEvents = [
5
+ ["userJoined", { userId: string; name: string }],
6
+ ["userLeft", { userId: string }],
7
+ ["messageReceived", { from: string; message: string }],
8
+ ["scoreUpdated", { playerId: string; score: number }]
9
+ ];
10
+
11
+ describe("EventSystem", () => {
12
+ test("should initialize with provided events", () => {
13
+ const events = new EventSystem<TestEvents>({
14
+ events: ["userJoined", "userLeft"],
15
+ });
16
+ expect(events).toBeDefined();
17
+ });
18
+
19
+ test("should register and call event callback", () => {
20
+ const events = new EventSystem<TestEvents>({
21
+ events: ["userJoined"],
22
+ });
23
+
24
+ let called = false;
25
+ events.on("userJoined", (data) => {
26
+ called = true;
27
+ expect(data.userId).toBe("123");
28
+ expect(data.name).toBe("Alice");
29
+ });
30
+
31
+ events.emit("userJoined", { userId: "123", name: "Alice" });
32
+ expect(called).toBe(true);
33
+ });
34
+
35
+ test("should call multiple callbacks for same event", () => {
36
+ const events = new EventSystem<TestEvents>({
37
+ events: ["userJoined"],
38
+ });
39
+
40
+ let count = 0;
41
+ events.on("userJoined", () => count++);
42
+ events.on("userJoined", () => count++);
43
+ events.on("userJoined", () => count++);
44
+
45
+ events.emit("userJoined", { userId: "123", name: "Bob" });
46
+ expect(count).toBe(3);
47
+ });
48
+
49
+ test("should handle once() callback that runs only once", () => {
50
+ const events = new EventSystem<TestEvents>({
51
+ events: ["messageReceived"],
52
+ });
53
+
54
+ let count = 0;
55
+ events.once("messageReceived", () => count++);
56
+
57
+ events.emit("messageReceived", { from: "Alice", message: "Hello" });
58
+ events.emit("messageReceived", { from: "Bob", message: "Hi" });
59
+ events.emit("messageReceived", { from: "Charlie", message: "Hey" });
60
+
61
+ expect(count).toBe(1);
62
+ });
63
+
64
+ test("should remove callback with off()", () => {
65
+ const events = new EventSystem<TestEvents>({
66
+ events: ["scoreUpdated"],
67
+ });
68
+
69
+ let count = 0;
70
+ const callback = () => count++;
71
+
72
+ events.on("scoreUpdated", callback);
73
+ events.emit("scoreUpdated", { playerId: "p1", score: 100 });
74
+ expect(count).toBe(1);
75
+
76
+ events.off("scoreUpdated", callback);
77
+ events.emit("scoreUpdated", { playerId: "p1", score: 200 });
78
+ expect(count).toBe(1); // Should still be 1
79
+ });
80
+
81
+ test("should clear specific event callbacks", () => {
82
+ const events = new EventSystem<TestEvents>({
83
+ events: ["userJoined", "userLeft"],
84
+ });
85
+
86
+ let joinCount = 0;
87
+ let leaveCount = 0;
88
+
89
+ events.on("userJoined", () => joinCount++);
90
+ events.on("userJoined", () => joinCount++);
91
+ events.on("userLeft", () => leaveCount++);
92
+
93
+ events.clear("userJoined");
94
+
95
+ events.emit("userJoined", { userId: "123", name: "Alice" });
96
+ events.emit("userLeft", { userId: "123" });
97
+
98
+ expect(joinCount).toBe(0);
99
+ expect(leaveCount).toBe(1);
100
+ });
101
+
102
+ test("should clear all event callbacks", () => {
103
+ const events = new EventSystem<TestEvents>({
104
+ events: ["userJoined", "userLeft", "messageReceived"],
105
+ });
106
+
107
+ let count = 0;
108
+ events.on("userJoined", () => count++);
109
+ events.on("userLeft", () => count++);
110
+ events.on("messageReceived", () => count++);
111
+
112
+ events.clear();
113
+
114
+ events.emit("userJoined", { userId: "123", name: "Alice" });
115
+ events.emit("userLeft", { userId: "123" });
116
+ events.emit("messageReceived", { from: "Alice", message: "Hi" });
117
+
118
+ expect(count).toBe(0);
119
+ });
120
+
121
+ test("should warn when registering callback for non-existent event", () => {
122
+ const events = new EventSystem<TestEvents>({
123
+ events: ["userJoined"],
124
+ });
125
+
126
+ const consoleWarn = spyOn(console, "warn");
127
+ // @ts-expect-error Testing invalid event name
128
+ events.on("nonExistent", () => {});
129
+ expect(consoleWarn).toHaveBeenCalledWith('Event "nonExistent" does not exist.');
130
+ consoleWarn.mockRestore();
131
+ });
132
+
133
+ test("should warn when emitting non-existent event", () => {
134
+ const events = new EventSystem<TestEvents>({
135
+ events: ["userJoined"],
136
+ });
137
+
138
+ const consoleWarn = spyOn(console, "warn");
139
+ // @ts-expect-error Testing invalid event name
140
+ events.emit("nonExistent", {});
141
+ expect(consoleWarn).toHaveBeenCalledWith('Event "nonExistent" does not exist.');
142
+ consoleWarn.mockRestore();
143
+ });
144
+
145
+ test("should pass correct data to callbacks", () => {
146
+ const events = new EventSystem<TestEvents>({
147
+ events: ["scoreUpdated"],
148
+ });
149
+
150
+ const receivedData: Array<{ playerId: string; score: number }> = [];
151
+ events.on("scoreUpdated", (data) => receivedData.push(data));
152
+
153
+ events.emit("scoreUpdated", { playerId: "p1", score: 100 });
154
+ events.emit("scoreUpdated", { playerId: "p2", score: 200 });
155
+
156
+ expect(receivedData).toEqual([
157
+ { playerId: "p1", score: 100 },
158
+ { playerId: "p2", score: 200 },
159
+ ]);
160
+ });
161
+
162
+ test("should handle callbacks that throw errors", () => {
163
+ const events = new EventSystem<TestEvents>({
164
+ events: ["userJoined"],
165
+ });
166
+
167
+ let successCount = 0;
168
+ events.on("userJoined", () => {
169
+ throw new Error("Callback error");
170
+ });
171
+ events.on("userJoined", () => successCount++);
172
+
173
+ expect(() =>
174
+ events.emit("userJoined", { userId: "123", name: "Alice" })
175
+ ).toThrow();
176
+
177
+ // First callback threw, so second wasn't reached
178
+ expect(successCount).toBe(0);
179
+ });
180
+
181
+ test("should maintain callback order", () => {
182
+ const events = new EventSystem<TestEvents>({
183
+ events: ["messageReceived"],
184
+ });
185
+
186
+ const order: number[] = [];
187
+ events.on("messageReceived", () => order.push(1));
188
+ events.on("messageReceived", () => order.push(2));
189
+ events.on("messageReceived", () => order.push(3));
190
+
191
+ events.emit("messageReceived", { from: "Test", message: "Hello" });
192
+ expect(order).toEqual([1, 2, 3]);
193
+ });
194
+
195
+ test("should allow same callback to be registered multiple times", () => {
196
+ const events = new EventSystem<TestEvents>({
197
+ events: ["userJoined"],
198
+ });
199
+
200
+ let count = 0;
201
+ const callback = () => count++;
202
+
203
+ events.on("userJoined", callback);
204
+ events.on("userJoined", callback);
205
+
206
+ events.emit("userJoined", { userId: "123", name: "Alice" });
207
+ // Set only stores unique callbacks, so it should be called once
208
+ expect(count).toBe(1);
209
+ });
210
+
211
+ test("should handle rapid event emissions", () => {
212
+ const events = new EventSystem<TestEvents>({
213
+ events: ["scoreUpdated"],
214
+ });
215
+
216
+ let count = 0;
217
+ events.on("scoreUpdated", () => count++);
218
+
219
+ for (let i = 0; i < 1000; i++) {
220
+ events.emit("scoreUpdated", { playerId: "p1", score: i });
221
+ }
222
+
223
+ expect(count).toBe(1000);
224
+ });
225
+
226
+ test("should handle once() with multiple callbacks", () => {
227
+ const events = new EventSystem<TestEvents>({
228
+ events: ["userJoined"],
229
+ });
230
+
231
+ let count1 = 0;
232
+ let count2 = 0;
233
+
234
+ events.once("userJoined", () => count1++);
235
+ events.on("userJoined", () => count2++);
236
+
237
+ events.emit("userJoined", { userId: "123", name: "Alice" });
238
+ events.emit("userJoined", { userId: "456", name: "Bob" });
239
+
240
+ expect(count1).toBe(1);
241
+ expect(count2).toBe(2);
242
+ });
243
+ });
@@ -0,0 +1,140 @@
1
+ /* ---------- Types ---------- */
2
+ type Callback<Props> = (props: Props) => void;
3
+
4
+
5
+ type EventMapFromTuple<T extends [string, unknown][]> = {
6
+ [K in T[number]as K[0]]: K[1];
7
+ };
8
+
9
+ /* ---------- Interfaces ---------- */
10
+ interface EventSystemProps<EventNames extends string> {
11
+ /**
12
+ * @description
13
+ * The list of events to ever be registered.
14
+ */
15
+ events: EventNames[];
16
+ }
17
+
18
+ /**
19
+ * @description
20
+ * A callback-based event handling system designed to simplify
21
+ * event-driven programming.
22
+ */
23
+ export class EventSystem<EventTuple extends [string, unknown][]> {
24
+ /**
25
+ * @private
26
+ * @description
27
+ * The map of registered events and their callbacks.
28
+ */
29
+ private callbacks: Map<string, Set<Callback<unknown>>>;
30
+
31
+ /**
32
+ * @private
33
+ * @description
34
+ * The list of events that were registered.
35
+ */
36
+ private events: string[];
37
+
38
+ constructor({ events }: EventSystemProps<string>) {
39
+ this.callbacks = new Map();
40
+ this.events = events;
41
+
42
+ for (const name of this.events) {
43
+ this.callbacks.set(name, new Set());
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @description
49
+ * Registers a callback for an event.
50
+ *
51
+ * @param name Event name
52
+ * @param callback Callback to run when the event is emitted
53
+ */
54
+ on<EventName extends keyof EventMapFromTuple<EventTuple> & string>(
55
+ name: EventName,
56
+ callback: Callback<EventMapFromTuple<EventTuple>[EventName]>
57
+ ): void {
58
+ const event = this.callbacks.get(name) as Set<Callback<EventMapFromTuple<EventTuple>[EventName]>> | undefined;
59
+ if (!event) return console.warn(`Event "${name}" does not exist.`);
60
+
61
+ event.add(callback);
62
+ }
63
+
64
+ /**
65
+ * @description
66
+ * Registers a callback for an event that runs only once.
67
+ *
68
+ * @param name Event name
69
+ * @param callback Callback to run when the event is emitted
70
+ */
71
+ once<EventName extends keyof EventMapFromTuple<EventTuple> & string>(
72
+ name: EventName,
73
+ callback: Callback<EventMapFromTuple<EventTuple>[EventName]>
74
+ ): void {
75
+ const wrapper: Callback<EventMapFromTuple<EventTuple>[EventName]> = (props) => {
76
+ callback(props);
77
+ this.off(name, wrapper);
78
+ };
79
+
80
+ this.on(name, wrapper);
81
+ }
82
+
83
+ /**
84
+ * @description
85
+ * Emits an event, running all registered callbacks.
86
+ *
87
+ * @param name Event name
88
+ * @param data Event data
89
+ */
90
+ emit<EventName extends keyof EventMapFromTuple<EventTuple> & string>(
91
+ name: EventName,
92
+ data: EventMapFromTuple<EventTuple>[EventName]
93
+ ) {
94
+ const event = this.callbacks.get(name);
95
+ if (!event) return console.warn(`Event "${name}" does not exist.`);
96
+
97
+ for (const callback of event) {
98
+ callback(data);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * @description
104
+ * Removes a callback from an event.
105
+ *
106
+ * @param name Event name
107
+ * @param callback Callback to remove
108
+ */
109
+ off<EventName extends keyof EventMapFromTuple<EventTuple> & string>(
110
+ name: EventName,
111
+ callback: Callback<EventMapFromTuple<EventTuple>[EventName]>
112
+ ): void {
113
+ const event = this.callbacks.get(name) as Set<Callback<EventMapFromTuple<EventTuple>[EventName]>> | undefined;
114
+ if (!event) return console.warn(`Event "${name}" does not exist.`);
115
+
116
+ event.delete(callback);
117
+ }
118
+
119
+ /**
120
+ * @description
121
+ * Removes all callbacks.
122
+ *
123
+ * @param name Optional event name
124
+ */
125
+ clear<EventName extends keyof EventMapFromTuple<EventTuple> & string>(name?: EventName): void {
126
+ if (!name) {
127
+ this.callbacks.clear();
128
+ for (const name of this.events) {
129
+ this.callbacks.set(name, new Set());
130
+ }
131
+
132
+ return;
133
+ }
134
+
135
+ const event = this.callbacks.get(name);
136
+ if (!event) return console.warn(`Event "${name}" does not exist.`);
137
+
138
+ event.clear();
139
+ }
140
+ }
@@ -0,0 +1 @@
1
+ export * from './event-system';
@@ -0,0 +1,77 @@
1
+ # FixedTicker
2
+
3
+ `FixedTicker` is a utility class for managing fixed-rate update ticks in JavaScript/TypeScript applications. It is designed for deterministic game loops, working both in the browser (e.g., with Pixi.js) and on Node.js servers.
4
+
5
+ This class ensures that updates run at a fixed timestep, which is essential for predictable, lockstep multiplayer simulations.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - Fixed-timestep updates with configurable tick rate.
12
+ - Accumulates elapsed time and determines how many ticks to process per frame.
13
+ - Limits the maximum number of ticks per frame to prevent runaway loops.
14
+ - Provides a tick count for deterministic simulation.
15
+ - Optional callback for detecting skipped ticks (useful for debugging or network reconciliation).
16
+ - Compatible with both client and server environments.
17
+
18
+ ## Usage
19
+
20
+ ```typescript
21
+ import { FixedTicker } from './fixed-ticker';
22
+
23
+ const ticker = new FixedTicker({
24
+ rate: 30, // ticks per second
25
+ onTick: (deltaTime, tick) => {
26
+ // Your fixed-step game logic here
27
+ // deltaTime = 1 / rate
28
+ console.log(`Tick ${tick} - dt: ${deltaTime}`);
29
+ },
30
+ onTickSkipped: (skipped) => {
31
+ console.warn(`Skipped ${skipped} ticks due to high delta time`);
32
+ }
33
+ });
34
+
35
+ // Call once per frame, passing elapsed time in seconds
36
+ function gameLoop(deltaTime: number) {
37
+ ticker.tick(deltaTime);
38
+
39
+ // Optional: interpolate visuals using accumulatedTime
40
+ const alpha = ticker.accumulatedTime / (1 / 30); // for smooth rendering
41
+ }
42
+ ```
43
+
44
+ ## Notes
45
+
46
+ - This class does not automatically guarantee determinism; your game logic must also be deterministic (e.g., using seeded random numbers and consistent math operations).
47
+ - For lockstep multiplayer, always use the tickCount to synchronize inputs and simulation steps.
48
+ - Use accumulatedTime for smooth client-side rendering between fixed ticks.
49
+
50
+ ## Example: Deterministic Multiplayer Loop
51
+
52
+ ```typescript
53
+ const ticker = new FixedTicker({
54
+ rate: 12, // will fire the onTick callback 12 times per second
55
+ onTick: (deltaTime, tick) => {
56
+ applyInputsForTick(tick);
57
+ updateSimulation(deltaTime);
58
+ }
59
+ });
60
+
61
+ // in browser
62
+ function frame(deltaTime: number) {
63
+ ticker.tick(deltaTime);
64
+ render(interpolate(ticker.accumulatedTime));
65
+ }
66
+
67
+ // in Node.js
68
+ setInterval(() => {
69
+ const deltaTime = 1 / ticker.rate; // fixed timestep
70
+ ticker.tick(deltaTime);
71
+ updateSimulation(deltaTime);
72
+ }, 1000 / ticker.rate);
73
+ ```
74
+
75
+ ---
76
+
77
+ `FixedTicker` provides a reliable, minimal, and deterministic foundation for game loops and simulations where consistent, fixed-step updates are crucial.
@@ -0,0 +1,151 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { FixedTicker } from "./fixed-ticker";
3
+
4
+ describe("FixedTicker", () => {
5
+ test("should initialize with correct rate", () => {
6
+ const ticker = new FixedTicker({ rate: 60, onTick: () => { } });
7
+ expect(ticker.rate).toBe(60);
8
+ });
9
+
10
+ test("should call onTick with correct deltaTime", () => {
11
+ let calledDelta = 0;
12
+ const ticker = new FixedTicker({
13
+ rate: 60,
14
+ onTick: (dt) => {
15
+ calledDelta = dt;
16
+ },
17
+ });
18
+
19
+ ticker.tick(1 / 60);
20
+ expect(calledDelta).toBeCloseTo(1 / 60, 5);
21
+ });
22
+
23
+ test("should accumulate time and execute multiple ticks", () => {
24
+ let tickCount = 0;
25
+ const ticker = new FixedTicker({
26
+ rate: 60,
27
+ onTick: () => {
28
+ tickCount++;
29
+ },
30
+ });
31
+
32
+ // Simulate 3 frames worth of time (add small epsilon for floating point)
33
+ ticker.tick(3.01 / 60);
34
+ expect(tickCount).toBe(3);
35
+ });
36
+
37
+ test("should track tick count correctly", () => {
38
+ const ticker = new FixedTicker({
39
+ rate: 60,
40
+ onTick: () => { },
41
+ });
42
+
43
+ ticker.tick(5.01 / 60);
44
+ expect(ticker.tickCount).toBe(5);
45
+ });
46
+
47
+ test("should reset tick count", () => {
48
+ const ticker = new FixedTicker({
49
+ rate: 60,
50
+ onTick: () => { },
51
+ });
52
+
53
+ ticker.tick(3.01 / 60);
54
+ expect(ticker.tickCount).toBe(3);
55
+
56
+ ticker.resetTickCount();
57
+ expect(ticker.tickCount).toBe(0);
58
+ });
59
+
60
+ test("should pass tick number to onTick callback", () => {
61
+ const tickNumbers: number[] = [];
62
+ const ticker = new FixedTicker({
63
+ rate: 60,
64
+ onTick: (_dt, tick) => {
65
+ if (tick !== undefined) tickNumbers.push(tick);
66
+ },
67
+ });
68
+
69
+ ticker.tick(3.01 / 60);
70
+ expect(tickNumbers).toEqual([0, 1, 2]);
71
+ });
72
+
73
+ test("should limit ticks per frame to maxTicksPerFrame", () => {
74
+ let tickCount = 0;
75
+ const ticker = new FixedTicker({
76
+ rate: 60,
77
+ onTick: () => {
78
+ tickCount++;
79
+ },
80
+ });
81
+
82
+ // Simulate a huge delta time (2 seconds)
83
+ ticker.tick(2);
84
+ // maxTicksPerFrame should be Math.max(1, Math.floor(60 / 2)) = 30
85
+ expect(tickCount).toBe(30);
86
+ });
87
+
88
+ test("should provide accumulated time", () => {
89
+ const ticker = new FixedTicker({
90
+ rate: 60,
91
+ onTick: () => { },
92
+ });
93
+
94
+ // Give it 1.5 ticks worth of time
95
+ ticker.tick(1.5 / 60);
96
+ expect(ticker.accumulatedTime).toBeCloseTo(0.5 / 60, 5);
97
+ });
98
+
99
+ test("should call onTickSkipped when ticks are skipped", () => {
100
+ let skippedCount = 0;
101
+ const ticker = new FixedTicker({
102
+ rate: 60,
103
+ onTick: () => { },
104
+ });
105
+
106
+ // Access the private onTickSkipped through constructor
107
+ const tickerWithSkip = new FixedTicker({
108
+ rate: 60,
109
+ onTick: () => { },
110
+ });
111
+
112
+ // Monkey patch to add onTickSkipped
113
+ (tickerWithSkip as any).onTickSkipped = (count: number) => {
114
+ skippedCount = count;
115
+ };
116
+
117
+ // Simulate huge delta that exceeds maxTicksPerFrame
118
+ tickerWithSkip.tick(2);
119
+ expect(skippedCount).toBeGreaterThan(0);
120
+ });
121
+
122
+ test("should handle zero delta time", () => {
123
+ let tickCount = 0;
124
+ const ticker = new FixedTicker({
125
+ rate: 60,
126
+ onTick: () => {
127
+ tickCount++;
128
+ },
129
+ });
130
+
131
+ ticker.tick(0);
132
+ expect(tickCount).toBe(0);
133
+ expect(ticker.tickCount).toBe(0);
134
+ });
135
+
136
+ test("should handle very small delta times", () => {
137
+ let tickCount = 0;
138
+ const ticker = new FixedTicker({
139
+ rate: 60,
140
+ onTick: () => {
141
+ tickCount++;
142
+ },
143
+ });
144
+
145
+ // Simulate 100 very small frames that add up to slightly more than 1 tick
146
+ for (let i = 0; i < 100; i++) {
147
+ ticker.tick(1.01 / 60 / 100);
148
+ }
149
+ expect(tickCount).toBe(1);
150
+ });
151
+ });