@watchtower-sdk/core 0.3.1 → 0.4.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/index.d.mts +20 -3
- package/dist/index.d.ts +20 -3
- package/dist/index.js +76 -16
- package/dist/index.mjs +76 -16
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -209,11 +209,20 @@ declare class Room {
|
|
|
209
209
|
interface SyncOptions {
|
|
210
210
|
/** Updates per second (default: 20) */
|
|
211
211
|
tickRate?: number;
|
|
212
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Smoothing mode for remote players (default: 'lerp')
|
|
214
|
+
* - 'lerp': Frame-based lerping toward latest position. Zero latency, simple, great for casual games.
|
|
215
|
+
* - 'interpolate': Time-based snapshot interpolation. Adds latency but more accurate for competitive games.
|
|
216
|
+
* - 'none': No smoothing, positions snap immediately.
|
|
217
|
+
*/
|
|
218
|
+
smoothing?: 'lerp' | 'interpolate' | 'none';
|
|
219
|
+
/** Lerp factor - how fast to catch up to target (default: 0.15). Only used in 'lerp' mode. */
|
|
220
|
+
lerpFactor?: number;
|
|
221
|
+
/** @deprecated Use smoothing: 'interpolate' instead */
|
|
213
222
|
interpolate?: boolean;
|
|
214
|
-
/** Interpolation delay in ms - how far "in the past" to render others (default: 100) */
|
|
223
|
+
/** Interpolation delay in ms - how far "in the past" to render others (default: 100). Only used in 'interpolate' mode. */
|
|
215
224
|
interpolationDelay?: number;
|
|
216
|
-
/** Jitter buffer size in ms - smooths network variance (default:
|
|
225
|
+
/** Jitter buffer size in ms - smooths network variance (default: 0). Only used in 'interpolate' mode. */
|
|
217
226
|
jitterBuffer?: number;
|
|
218
227
|
/** Enable auto-reconnection on disconnect (default: true) */
|
|
219
228
|
autoReconnect?: boolean;
|
|
@@ -284,6 +293,7 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
284
293
|
private lastSentState;
|
|
285
294
|
private listeners;
|
|
286
295
|
private snapshots;
|
|
296
|
+
private lerpTargets;
|
|
287
297
|
private jitterQueue;
|
|
288
298
|
private reconnectAttempts;
|
|
289
299
|
private reconnectTimeout;
|
|
@@ -336,6 +346,7 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
336
346
|
private handleMessage;
|
|
337
347
|
private applyFullState;
|
|
338
348
|
private applyPlayerState;
|
|
349
|
+
private setLerpTarget;
|
|
339
350
|
private addSnapshot;
|
|
340
351
|
private applyStateDirect;
|
|
341
352
|
private removePlayer;
|
|
@@ -344,6 +355,12 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
344
355
|
private findPlayersKey;
|
|
345
356
|
private startSyncLoop;
|
|
346
357
|
private startInterpolationLoop;
|
|
358
|
+
/**
|
|
359
|
+
* Frame-based lerping (gnome-chat style)
|
|
360
|
+
* Lerps each remote player's position toward their target by lerpFactor each frame.
|
|
361
|
+
* Simple, zero latency, great for casual games.
|
|
362
|
+
*/
|
|
363
|
+
private updateLerp;
|
|
347
364
|
private measureLatency;
|
|
348
365
|
private stopInterpolationLoop;
|
|
349
366
|
private processJitterQueue;
|
package/dist/index.d.ts
CHANGED
|
@@ -209,11 +209,20 @@ declare class Room {
|
|
|
209
209
|
interface SyncOptions {
|
|
210
210
|
/** Updates per second (default: 20) */
|
|
211
211
|
tickRate?: number;
|
|
212
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Smoothing mode for remote players (default: 'lerp')
|
|
214
|
+
* - 'lerp': Frame-based lerping toward latest position. Zero latency, simple, great for casual games.
|
|
215
|
+
* - 'interpolate': Time-based snapshot interpolation. Adds latency but more accurate for competitive games.
|
|
216
|
+
* - 'none': No smoothing, positions snap immediately.
|
|
217
|
+
*/
|
|
218
|
+
smoothing?: 'lerp' | 'interpolate' | 'none';
|
|
219
|
+
/** Lerp factor - how fast to catch up to target (default: 0.15). Only used in 'lerp' mode. */
|
|
220
|
+
lerpFactor?: number;
|
|
221
|
+
/** @deprecated Use smoothing: 'interpolate' instead */
|
|
213
222
|
interpolate?: boolean;
|
|
214
|
-
/** Interpolation delay in ms - how far "in the past" to render others (default: 100) */
|
|
223
|
+
/** Interpolation delay in ms - how far "in the past" to render others (default: 100). Only used in 'interpolate' mode. */
|
|
215
224
|
interpolationDelay?: number;
|
|
216
|
-
/** Jitter buffer size in ms - smooths network variance (default:
|
|
225
|
+
/** Jitter buffer size in ms - smooths network variance (default: 0). Only used in 'interpolate' mode. */
|
|
217
226
|
jitterBuffer?: number;
|
|
218
227
|
/** Enable auto-reconnection on disconnect (default: true) */
|
|
219
228
|
autoReconnect?: boolean;
|
|
@@ -284,6 +293,7 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
284
293
|
private lastSentState;
|
|
285
294
|
private listeners;
|
|
286
295
|
private snapshots;
|
|
296
|
+
private lerpTargets;
|
|
287
297
|
private jitterQueue;
|
|
288
298
|
private reconnectAttempts;
|
|
289
299
|
private reconnectTimeout;
|
|
@@ -336,6 +346,7 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
336
346
|
private handleMessage;
|
|
337
347
|
private applyFullState;
|
|
338
348
|
private applyPlayerState;
|
|
349
|
+
private setLerpTarget;
|
|
339
350
|
private addSnapshot;
|
|
340
351
|
private applyStateDirect;
|
|
341
352
|
private removePlayer;
|
|
@@ -344,6 +355,12 @@ declare class Sync<T extends Record<string, unknown>> {
|
|
|
344
355
|
private findPlayersKey;
|
|
345
356
|
private startSyncLoop;
|
|
346
357
|
private startInterpolationLoop;
|
|
358
|
+
/**
|
|
359
|
+
* Frame-based lerping (gnome-chat style)
|
|
360
|
+
* Lerps each remote player's position toward their target by lerpFactor each frame.
|
|
361
|
+
* Simple, zero latency, great for casual games.
|
|
362
|
+
*/
|
|
363
|
+
private updateLerp;
|
|
347
364
|
private measureLatency;
|
|
348
365
|
private stopInterpolationLoop;
|
|
349
366
|
private processJitterQueue;
|
package/dist/index.js
CHANGED
|
@@ -293,6 +293,8 @@ var Sync = class {
|
|
|
293
293
|
this.listeners = /* @__PURE__ */ new Map();
|
|
294
294
|
// Snapshot-based interpolation: store timestamped snapshots per player
|
|
295
295
|
this.snapshots = /* @__PURE__ */ new Map();
|
|
296
|
+
// Lerp-based smoothing: store target positions per player (for 'lerp' mode)
|
|
297
|
+
this.lerpTargets = /* @__PURE__ */ new Map();
|
|
296
298
|
// Jitter buffer: queue incoming updates before applying
|
|
297
299
|
this.jitterQueue = [];
|
|
298
300
|
// Auto-reconnect state
|
|
@@ -310,12 +312,20 @@ var Sync = class {
|
|
|
310
312
|
this.state = state;
|
|
311
313
|
this.myId = config.playerId;
|
|
312
314
|
this.config = config;
|
|
315
|
+
let smoothing = options?.smoothing ?? "lerp";
|
|
316
|
+
if (options?.interpolate === false) {
|
|
317
|
+
smoothing = "none";
|
|
318
|
+
} else if (options?.interpolate === true && !options?.smoothing) {
|
|
319
|
+
smoothing = "lerp";
|
|
320
|
+
}
|
|
313
321
|
this.options = {
|
|
314
322
|
tickRate: options?.tickRate ?? 20,
|
|
315
|
-
|
|
323
|
+
smoothing,
|
|
324
|
+
lerpFactor: options?.lerpFactor ?? 0.15,
|
|
325
|
+
interpolate: smoothing !== "none",
|
|
326
|
+
// Legacy compat
|
|
316
327
|
interpolationDelay: options?.interpolationDelay ?? 100,
|
|
317
328
|
jitterBuffer: options?.jitterBuffer ?? 0,
|
|
318
|
-
// 0 = immediate, set to 50+ for smoothing
|
|
319
329
|
autoReconnect: options?.autoReconnect ?? true,
|
|
320
330
|
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10
|
|
321
331
|
};
|
|
@@ -525,17 +535,37 @@ var Sync = class {
|
|
|
525
535
|
}
|
|
526
536
|
applyPlayerState(playerId, playerState, serverTime) {
|
|
527
537
|
const timestamp = serverTime ? serverTime + this.serverTimeOffset : Date.now();
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
538
|
+
switch (this.options.smoothing) {
|
|
539
|
+
case "lerp":
|
|
540
|
+
this.setLerpTarget(playerId, playerState);
|
|
541
|
+
break;
|
|
542
|
+
case "interpolate":
|
|
543
|
+
if (this.options.jitterBuffer > 0) {
|
|
544
|
+
this.jitterQueue.push({
|
|
545
|
+
deliverAt: timestamp + this.options.jitterBuffer,
|
|
546
|
+
playerId,
|
|
547
|
+
state: { ...playerState },
|
|
548
|
+
timestamp
|
|
549
|
+
});
|
|
550
|
+
} else {
|
|
551
|
+
this.addSnapshot(playerId, playerState, timestamp);
|
|
552
|
+
}
|
|
553
|
+
break;
|
|
554
|
+
case "none":
|
|
555
|
+
default:
|
|
556
|
+
this.applyStateDirect(playerId, playerState);
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
setLerpTarget(playerId, playerState) {
|
|
561
|
+
const isNewPlayer = !this.lerpTargets.has(playerId);
|
|
562
|
+
this.lerpTargets.set(playerId, { ...playerState });
|
|
563
|
+
const playersKey = this.findPlayersKey();
|
|
564
|
+
if (playersKey) {
|
|
565
|
+
const players = this.state[playersKey];
|
|
566
|
+
if (isNewPlayer || !players[playerId]) {
|
|
567
|
+
players[playerId] = { ...playerState };
|
|
568
|
+
}
|
|
539
569
|
}
|
|
540
570
|
}
|
|
541
571
|
addSnapshot(playerId, playerState, timestamp) {
|
|
@@ -571,6 +601,7 @@ var Sync = class {
|
|
|
571
601
|
const players = this.state[playersKey];
|
|
572
602
|
delete players[playerId];
|
|
573
603
|
this.snapshots.delete(playerId);
|
|
604
|
+
this.lerpTargets.delete(playerId);
|
|
574
605
|
}
|
|
575
606
|
attemptReconnect() {
|
|
576
607
|
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
|
|
@@ -603,6 +634,7 @@ var Sync = class {
|
|
|
603
634
|
}
|
|
604
635
|
}
|
|
605
636
|
this.snapshots.clear();
|
|
637
|
+
this.lerpTargets.clear();
|
|
606
638
|
this.jitterQueue = [];
|
|
607
639
|
}
|
|
608
640
|
findPlayersKey() {
|
|
@@ -628,15 +660,43 @@ var Sync = class {
|
|
|
628
660
|
}
|
|
629
661
|
startInterpolationLoop() {
|
|
630
662
|
if (this.interpolationInterval) return;
|
|
631
|
-
if (
|
|
663
|
+
if (this.options.smoothing === "none") return;
|
|
632
664
|
this.interpolationInterval = setInterval(() => {
|
|
633
|
-
this.
|
|
634
|
-
|
|
665
|
+
if (this.options.smoothing === "lerp") {
|
|
666
|
+
this.updateLerp();
|
|
667
|
+
} else if (this.options.smoothing === "interpolate") {
|
|
668
|
+
this.processJitterQueue();
|
|
669
|
+
this.updateInterpolation();
|
|
670
|
+
}
|
|
635
671
|
}, 16);
|
|
636
672
|
this.pingInterval = setInterval(() => {
|
|
637
673
|
this.measureLatency();
|
|
638
674
|
}, 2e3);
|
|
639
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Frame-based lerping (gnome-chat style)
|
|
678
|
+
* Lerps each remote player's position toward their target by lerpFactor each frame.
|
|
679
|
+
* Simple, zero latency, great for casual games.
|
|
680
|
+
*/
|
|
681
|
+
updateLerp() {
|
|
682
|
+
const playersKey = this.findPlayersKey();
|
|
683
|
+
if (!playersKey) return;
|
|
684
|
+
const players = this.state[playersKey];
|
|
685
|
+
const lerpFactor = this.options.lerpFactor;
|
|
686
|
+
for (const [playerId, target] of this.lerpTargets) {
|
|
687
|
+
if (playerId === this.myId) continue;
|
|
688
|
+
const player = players[playerId];
|
|
689
|
+
if (!player) continue;
|
|
690
|
+
for (const [key, targetValue] of Object.entries(target)) {
|
|
691
|
+
if (typeof targetValue === "number" && typeof player[key] === "number") {
|
|
692
|
+
const current = player[key];
|
|
693
|
+
player[key] = current + (targetValue - current) * lerpFactor;
|
|
694
|
+
} else if (typeof targetValue !== "number") {
|
|
695
|
+
player[key] = targetValue;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
640
700
|
measureLatency() {
|
|
641
701
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
642
702
|
this.pingStartTime = Date.now();
|
package/dist/index.mjs
CHANGED
|
@@ -266,6 +266,8 @@ var Sync = class {
|
|
|
266
266
|
this.listeners = /* @__PURE__ */ new Map();
|
|
267
267
|
// Snapshot-based interpolation: store timestamped snapshots per player
|
|
268
268
|
this.snapshots = /* @__PURE__ */ new Map();
|
|
269
|
+
// Lerp-based smoothing: store target positions per player (for 'lerp' mode)
|
|
270
|
+
this.lerpTargets = /* @__PURE__ */ new Map();
|
|
269
271
|
// Jitter buffer: queue incoming updates before applying
|
|
270
272
|
this.jitterQueue = [];
|
|
271
273
|
// Auto-reconnect state
|
|
@@ -283,12 +285,20 @@ var Sync = class {
|
|
|
283
285
|
this.state = state;
|
|
284
286
|
this.myId = config.playerId;
|
|
285
287
|
this.config = config;
|
|
288
|
+
let smoothing = options?.smoothing ?? "lerp";
|
|
289
|
+
if (options?.interpolate === false) {
|
|
290
|
+
smoothing = "none";
|
|
291
|
+
} else if (options?.interpolate === true && !options?.smoothing) {
|
|
292
|
+
smoothing = "lerp";
|
|
293
|
+
}
|
|
286
294
|
this.options = {
|
|
287
295
|
tickRate: options?.tickRate ?? 20,
|
|
288
|
-
|
|
296
|
+
smoothing,
|
|
297
|
+
lerpFactor: options?.lerpFactor ?? 0.15,
|
|
298
|
+
interpolate: smoothing !== "none",
|
|
299
|
+
// Legacy compat
|
|
289
300
|
interpolationDelay: options?.interpolationDelay ?? 100,
|
|
290
301
|
jitterBuffer: options?.jitterBuffer ?? 0,
|
|
291
|
-
// 0 = immediate, set to 50+ for smoothing
|
|
292
302
|
autoReconnect: options?.autoReconnect ?? true,
|
|
293
303
|
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10
|
|
294
304
|
};
|
|
@@ -498,17 +508,37 @@ var Sync = class {
|
|
|
498
508
|
}
|
|
499
509
|
applyPlayerState(playerId, playerState, serverTime) {
|
|
500
510
|
const timestamp = serverTime ? serverTime + this.serverTimeOffset : Date.now();
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
511
|
+
switch (this.options.smoothing) {
|
|
512
|
+
case "lerp":
|
|
513
|
+
this.setLerpTarget(playerId, playerState);
|
|
514
|
+
break;
|
|
515
|
+
case "interpolate":
|
|
516
|
+
if (this.options.jitterBuffer > 0) {
|
|
517
|
+
this.jitterQueue.push({
|
|
518
|
+
deliverAt: timestamp + this.options.jitterBuffer,
|
|
519
|
+
playerId,
|
|
520
|
+
state: { ...playerState },
|
|
521
|
+
timestamp
|
|
522
|
+
});
|
|
523
|
+
} else {
|
|
524
|
+
this.addSnapshot(playerId, playerState, timestamp);
|
|
525
|
+
}
|
|
526
|
+
break;
|
|
527
|
+
case "none":
|
|
528
|
+
default:
|
|
529
|
+
this.applyStateDirect(playerId, playerState);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
setLerpTarget(playerId, playerState) {
|
|
534
|
+
const isNewPlayer = !this.lerpTargets.has(playerId);
|
|
535
|
+
this.lerpTargets.set(playerId, { ...playerState });
|
|
536
|
+
const playersKey = this.findPlayersKey();
|
|
537
|
+
if (playersKey) {
|
|
538
|
+
const players = this.state[playersKey];
|
|
539
|
+
if (isNewPlayer || !players[playerId]) {
|
|
540
|
+
players[playerId] = { ...playerState };
|
|
541
|
+
}
|
|
512
542
|
}
|
|
513
543
|
}
|
|
514
544
|
addSnapshot(playerId, playerState, timestamp) {
|
|
@@ -544,6 +574,7 @@ var Sync = class {
|
|
|
544
574
|
const players = this.state[playersKey];
|
|
545
575
|
delete players[playerId];
|
|
546
576
|
this.snapshots.delete(playerId);
|
|
577
|
+
this.lerpTargets.delete(playerId);
|
|
547
578
|
}
|
|
548
579
|
attemptReconnect() {
|
|
549
580
|
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
|
|
@@ -576,6 +607,7 @@ var Sync = class {
|
|
|
576
607
|
}
|
|
577
608
|
}
|
|
578
609
|
this.snapshots.clear();
|
|
610
|
+
this.lerpTargets.clear();
|
|
579
611
|
this.jitterQueue = [];
|
|
580
612
|
}
|
|
581
613
|
findPlayersKey() {
|
|
@@ -601,15 +633,43 @@ var Sync = class {
|
|
|
601
633
|
}
|
|
602
634
|
startInterpolationLoop() {
|
|
603
635
|
if (this.interpolationInterval) return;
|
|
604
|
-
if (
|
|
636
|
+
if (this.options.smoothing === "none") return;
|
|
605
637
|
this.interpolationInterval = setInterval(() => {
|
|
606
|
-
this.
|
|
607
|
-
|
|
638
|
+
if (this.options.smoothing === "lerp") {
|
|
639
|
+
this.updateLerp();
|
|
640
|
+
} else if (this.options.smoothing === "interpolate") {
|
|
641
|
+
this.processJitterQueue();
|
|
642
|
+
this.updateInterpolation();
|
|
643
|
+
}
|
|
608
644
|
}, 16);
|
|
609
645
|
this.pingInterval = setInterval(() => {
|
|
610
646
|
this.measureLatency();
|
|
611
647
|
}, 2e3);
|
|
612
648
|
}
|
|
649
|
+
/**
|
|
650
|
+
* Frame-based lerping (gnome-chat style)
|
|
651
|
+
* Lerps each remote player's position toward their target by lerpFactor each frame.
|
|
652
|
+
* Simple, zero latency, great for casual games.
|
|
653
|
+
*/
|
|
654
|
+
updateLerp() {
|
|
655
|
+
const playersKey = this.findPlayersKey();
|
|
656
|
+
if (!playersKey) return;
|
|
657
|
+
const players = this.state[playersKey];
|
|
658
|
+
const lerpFactor = this.options.lerpFactor;
|
|
659
|
+
for (const [playerId, target] of this.lerpTargets) {
|
|
660
|
+
if (playerId === this.myId) continue;
|
|
661
|
+
const player = players[playerId];
|
|
662
|
+
if (!player) continue;
|
|
663
|
+
for (const [key, targetValue] of Object.entries(target)) {
|
|
664
|
+
if (typeof targetValue === "number" && typeof player[key] === "number") {
|
|
665
|
+
const current = player[key];
|
|
666
|
+
player[key] = current + (targetValue - current) * lerpFactor;
|
|
667
|
+
} else if (typeof targetValue !== "number") {
|
|
668
|
+
player[key] = targetValue;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
613
673
|
measureLatency() {
|
|
614
674
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
615
675
|
this.pingStartTime = Date.now();
|