@watchtower-sdk/core 0.2.2 → 0.3.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 CHANGED
@@ -211,6 +211,14 @@ interface SyncOptions {
211
211
  tickRate?: number;
212
212
  /** Enable interpolation for remote entities (default: true) */
213
213
  interpolate?: boolean;
214
+ /** Interpolation delay in ms - how far "in the past" to render others (default: 100) */
215
+ interpolationDelay?: number;
216
+ /** Jitter buffer size in ms - smooths network variance (default: 50) */
217
+ jitterBuffer?: number;
218
+ /** Enable auto-reconnection on disconnect (default: true) */
219
+ autoReconnect?: boolean;
220
+ /** Max reconnection attempts (default: 10) */
221
+ maxReconnectAttempts?: number;
214
222
  }
215
223
  interface JoinOptions {
216
224
  /** Create room if it doesn't exist */
@@ -268,9 +276,15 @@ declare class Sync<T extends Record<string, unknown>> {
268
276
  private _roomId;
269
277
  private ws;
270
278
  private syncInterval;
279
+ private interpolationInterval;
271
280
  private lastSentState;
272
- private interpolationTargets;
273
281
  private listeners;
282
+ private snapshots;
283
+ private jitterQueue;
284
+ private reconnectAttempts;
285
+ private reconnectTimeout;
286
+ private lastJoinOptions;
287
+ private isReconnecting;
274
288
  constructor(state: T, config: Required<WatchtowerConfig>, options?: SyncOptions);
275
289
  /**
276
290
  * Join a room - your state will sync with everyone in this room
@@ -303,7 +317,7 @@ declare class Sync<T extends Record<string, unknown>> {
303
317
  /**
304
318
  * Subscribe to sync events
305
319
  */
306
- on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected' | 'message', callback: Function): void;
320
+ on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected' | 'reconnecting' | 'reconnected' | 'message', callback: Function): void;
307
321
  /**
308
322
  * Unsubscribe from sync events
309
323
  */
@@ -313,13 +327,20 @@ declare class Sync<T extends Record<string, unknown>> {
313
327
  private handleMessage;
314
328
  private applyFullState;
315
329
  private applyPlayerState;
330
+ private addSnapshot;
331
+ private applyStateDirect;
316
332
  private removePlayer;
333
+ private attemptReconnect;
317
334
  private clearRemotePlayers;
318
335
  private findPlayersKey;
319
336
  private startSyncLoop;
337
+ private startInterpolationLoop;
338
+ private stopInterpolationLoop;
339
+ private processJitterQueue;
320
340
  private stopSyncLoop;
321
341
  private syncMyState;
322
342
  private updateInterpolation;
343
+ private lerpState;
323
344
  private generateRoomCode;
324
345
  private getHeaders;
325
346
  }
package/dist/index.d.ts CHANGED
@@ -211,6 +211,14 @@ interface SyncOptions {
211
211
  tickRate?: number;
212
212
  /** Enable interpolation for remote entities (default: true) */
213
213
  interpolate?: boolean;
214
+ /** Interpolation delay in ms - how far "in the past" to render others (default: 100) */
215
+ interpolationDelay?: number;
216
+ /** Jitter buffer size in ms - smooths network variance (default: 50) */
217
+ jitterBuffer?: number;
218
+ /** Enable auto-reconnection on disconnect (default: true) */
219
+ autoReconnect?: boolean;
220
+ /** Max reconnection attempts (default: 10) */
221
+ maxReconnectAttempts?: number;
214
222
  }
215
223
  interface JoinOptions {
216
224
  /** Create room if it doesn't exist */
@@ -268,9 +276,15 @@ declare class Sync<T extends Record<string, unknown>> {
268
276
  private _roomId;
269
277
  private ws;
270
278
  private syncInterval;
279
+ private interpolationInterval;
271
280
  private lastSentState;
272
- private interpolationTargets;
273
281
  private listeners;
282
+ private snapshots;
283
+ private jitterQueue;
284
+ private reconnectAttempts;
285
+ private reconnectTimeout;
286
+ private lastJoinOptions;
287
+ private isReconnecting;
274
288
  constructor(state: T, config: Required<WatchtowerConfig>, options?: SyncOptions);
275
289
  /**
276
290
  * Join a room - your state will sync with everyone in this room
@@ -303,7 +317,7 @@ declare class Sync<T extends Record<string, unknown>> {
303
317
  /**
304
318
  * Subscribe to sync events
305
319
  */
306
- on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected' | 'message', callback: Function): void;
320
+ on(event: 'join' | 'leave' | 'error' | 'connected' | 'disconnected' | 'reconnecting' | 'reconnected' | 'message', callback: Function): void;
307
321
  /**
308
322
  * Unsubscribe from sync events
309
323
  */
@@ -313,13 +327,20 @@ declare class Sync<T extends Record<string, unknown>> {
313
327
  private handleMessage;
314
328
  private applyFullState;
315
329
  private applyPlayerState;
330
+ private addSnapshot;
331
+ private applyStateDirect;
316
332
  private removePlayer;
333
+ private attemptReconnect;
317
334
  private clearRemotePlayers;
318
335
  private findPlayersKey;
319
336
  private startSyncLoop;
337
+ private startInterpolationLoop;
338
+ private stopInterpolationLoop;
339
+ private processJitterQueue;
320
340
  private stopSyncLoop;
321
341
  private syncMyState;
322
342
  private updateInterpolation;
343
+ private lerpState;
323
344
  private generateRoomCode;
324
345
  private getHeaders;
325
346
  }
package/dist/index.js CHANGED
@@ -288,15 +288,29 @@ var Sync = class {
288
288
  this._roomId = null;
289
289
  this.ws = null;
290
290
  this.syncInterval = null;
291
+ this.interpolationInterval = null;
291
292
  this.lastSentState = "";
292
- this.interpolationTargets = /* @__PURE__ */ new Map();
293
293
  this.listeners = /* @__PURE__ */ new Map();
294
+ // Snapshot-based interpolation: store timestamped snapshots per player
295
+ this.snapshots = /* @__PURE__ */ new Map();
296
+ // Jitter buffer: queue incoming updates before applying
297
+ this.jitterQueue = [];
298
+ // Auto-reconnect state
299
+ this.reconnectAttempts = 0;
300
+ this.reconnectTimeout = null;
301
+ this.lastJoinOptions = void 0;
302
+ this.isReconnecting = false;
294
303
  this.state = state;
295
304
  this.myId = config.playerId;
296
305
  this.config = config;
297
306
  this.options = {
298
307
  tickRate: options?.tickRate ?? 20,
299
- interpolate: options?.interpolate ?? true
308
+ interpolate: options?.interpolate ?? true,
309
+ interpolationDelay: options?.interpolationDelay ?? 100,
310
+ jitterBuffer: options?.jitterBuffer ?? 0,
311
+ // 0 = immediate, set to 50+ for smoothing
312
+ autoReconnect: options?.autoReconnect ?? true,
313
+ maxReconnectAttempts: options?.maxReconnectAttempts ?? 10
300
314
  };
301
315
  }
302
316
  /** Current room ID (null if not in a room) */
@@ -314,23 +328,34 @@ var Sync = class {
314
328
  * @param options - Join options
315
329
  */
316
330
  async join(roomId, options) {
317
- if (this._roomId) {
331
+ if (this._roomId && !this.isReconnecting) {
318
332
  await this.leave();
319
333
  }
320
334
  this._roomId = roomId;
335
+ this.lastJoinOptions = options;
336
+ this.reconnectAttempts = 0;
321
337
  await this.connectWebSocket(roomId, options);
322
338
  this.startSyncLoop();
339
+ this.startInterpolationLoop();
323
340
  }
324
341
  /**
325
342
  * Leave the current room
326
343
  */
327
344
  async leave() {
345
+ if (this.reconnectTimeout) {
346
+ clearTimeout(this.reconnectTimeout);
347
+ this.reconnectTimeout = null;
348
+ }
349
+ this.isReconnecting = false;
328
350
  this.stopSyncLoop();
351
+ this.stopInterpolationLoop();
329
352
  if (this.ws) {
330
353
  this.ws.close();
331
354
  this.ws = null;
332
355
  }
333
356
  this.clearRemotePlayers();
357
+ this.snapshots.clear();
358
+ this.jitterQueue = [];
334
359
  this._roomId = null;
335
360
  }
336
361
  /**
@@ -424,6 +449,9 @@ var Sync = class {
424
449
  this.ws.onclose = () => {
425
450
  this.stopSyncLoop();
426
451
  this.emit("disconnected");
452
+ if (this.options.autoReconnect && this._roomId && !this.isReconnecting) {
453
+ this.attemptReconnect();
454
+ }
427
455
  };
428
456
  this.ws.onmessage = (event) => {
429
457
  try {
@@ -463,15 +491,43 @@ var Sync = class {
463
491
  }
464
492
  }
465
493
  applyPlayerState(playerId, playerState) {
494
+ if (this.options.interpolate && this.options.jitterBuffer > 0) {
495
+ this.jitterQueue.push({
496
+ deliverAt: Date.now() + this.options.jitterBuffer,
497
+ playerId,
498
+ state: { ...playerState }
499
+ });
500
+ } else if (this.options.interpolate) {
501
+ this.addSnapshot(playerId, playerState);
502
+ } else {
503
+ this.applyStateDirect(playerId, playerState);
504
+ }
505
+ }
506
+ addSnapshot(playerId, playerState) {
507
+ const isNewPlayer = !this.snapshots.has(playerId);
508
+ if (isNewPlayer) {
509
+ this.snapshots.set(playerId, []);
510
+ }
511
+ const playerSnapshots = this.snapshots.get(playerId);
512
+ playerSnapshots.push({
513
+ time: Date.now(),
514
+ state: { ...playerState }
515
+ });
516
+ while (playerSnapshots.length > 10) {
517
+ playerSnapshots.shift();
518
+ }
519
+ const playersKey = this.findPlayersKey();
520
+ if (playersKey) {
521
+ const players = this.state[playersKey];
522
+ if (isNewPlayer || !players[playerId]) {
523
+ players[playerId] = { ...playerState };
524
+ }
525
+ }
526
+ }
527
+ applyStateDirect(playerId, playerState) {
466
528
  const playersKey = this.findPlayersKey();
467
529
  if (!playersKey) return;
468
530
  const players = this.state[playersKey];
469
- if (this.options.interpolate && players[playerId]) {
470
- this.interpolationTargets.set(playerId, {
471
- target: { ...playerState },
472
- current: { ...players[playerId] }
473
- });
474
- }
475
531
  players[playerId] = playerState;
476
532
  }
477
533
  removePlayer(playerId) {
@@ -479,7 +535,28 @@ var Sync = class {
479
535
  if (!playersKey) return;
480
536
  const players = this.state[playersKey];
481
537
  delete players[playerId];
482
- this.interpolationTargets.delete(playerId);
538
+ this.snapshots.delete(playerId);
539
+ }
540
+ attemptReconnect() {
541
+ if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
542
+ this.emit("error", new Error("Max reconnection attempts reached"));
543
+ return;
544
+ }
545
+ this.isReconnecting = true;
546
+ this.reconnectAttempts++;
547
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
548
+ this.emit("reconnecting", { attempt: this.reconnectAttempts, delay });
549
+ this.reconnectTimeout = setTimeout(async () => {
550
+ try {
551
+ await this.connectWebSocket(this._roomId, this.lastJoinOptions);
552
+ this.startSyncLoop();
553
+ this.isReconnecting = false;
554
+ this.reconnectAttempts = 0;
555
+ this.emit("reconnected");
556
+ } catch (e) {
557
+ this.isReconnecting = false;
558
+ }
559
+ }, delay);
483
560
  }
484
561
  clearRemotePlayers() {
485
562
  const playersKey = this.findPlayersKey();
@@ -490,7 +567,8 @@ var Sync = class {
490
567
  delete players[playerId];
491
568
  }
492
569
  }
493
- this.interpolationTargets.clear();
570
+ this.snapshots.clear();
571
+ this.jitterQueue = [];
494
572
  }
495
573
  findPlayersKey() {
496
574
  const candidates = ["players", "entities", "gnomes", "users", "clients"];
@@ -511,11 +589,30 @@ var Sync = class {
511
589
  const intervalMs = 1e3 / this.options.tickRate;
512
590
  this.syncInterval = setInterval(() => {
513
591
  this.syncMyState();
514
- if (this.options.interpolate) {
515
- this.updateInterpolation();
516
- }
517
592
  }, intervalMs);
518
593
  }
594
+ startInterpolationLoop() {
595
+ if (this.interpolationInterval) return;
596
+ if (!this.options.interpolate) return;
597
+ this.interpolationInterval = setInterval(() => {
598
+ this.processJitterQueue();
599
+ this.updateInterpolation();
600
+ }, 16);
601
+ }
602
+ stopInterpolationLoop() {
603
+ if (this.interpolationInterval) {
604
+ clearInterval(this.interpolationInterval);
605
+ this.interpolationInterval = null;
606
+ }
607
+ }
608
+ processJitterQueue() {
609
+ const now = Date.now();
610
+ const ready = this.jitterQueue.filter((item) => item.deliverAt <= now);
611
+ this.jitterQueue = this.jitterQueue.filter((item) => item.deliverAt > now);
612
+ for (const item of ready) {
613
+ this.addSnapshot(item.playerId, item.state);
614
+ }
615
+ }
519
616
  stopSyncLoop() {
520
617
  if (this.syncInterval) {
521
618
  clearInterval(this.syncInterval);
@@ -541,16 +638,41 @@ var Sync = class {
541
638
  const playersKey = this.findPlayersKey();
542
639
  if (!playersKey) return;
543
640
  const players = this.state[playersKey];
544
- const lerpFactor = 0.2;
545
- for (const [playerId, interp] of this.interpolationTargets) {
641
+ const renderTime = Date.now() - this.options.interpolationDelay;
642
+ for (const [playerId, playerSnapshots] of this.snapshots) {
643
+ if (playerId === this.myId) continue;
546
644
  const player = players[playerId];
547
645
  if (!player) continue;
548
- for (const [key, targetValue] of Object.entries(interp.target)) {
549
- if (typeof targetValue === "number" && typeof interp.current[key] === "number") {
550
- const current = interp.current[key];
551
- const newValue = current + (targetValue - current) * lerpFactor;
552
- interp.current[key] = newValue;
553
- player[key] = newValue;
646
+ let before = null;
647
+ let after = null;
648
+ for (const snapshot of playerSnapshots) {
649
+ if (snapshot.time <= renderTime) {
650
+ before = snapshot;
651
+ } else if (!after) {
652
+ after = snapshot;
653
+ }
654
+ }
655
+ if (before && after) {
656
+ const total = after.time - before.time;
657
+ const elapsed = renderTime - before.time;
658
+ const alpha = total > 0 ? Math.min(1, elapsed / total) : 1;
659
+ this.lerpState(player, before.state, after.state, alpha);
660
+ } else if (before) {
661
+ this.lerpState(player, player, before.state, 0.3);
662
+ } else if (after) {
663
+ this.lerpState(player, player, after.state, 0.3);
664
+ }
665
+ }
666
+ }
667
+ lerpState(target, from, to, alpha) {
668
+ for (const key of Object.keys(to)) {
669
+ const fromVal = from[key];
670
+ const toVal = to[key];
671
+ if (typeof fromVal === "number" && typeof toVal === "number") {
672
+ target[key] = fromVal + (toVal - fromVal) * alpha;
673
+ } else {
674
+ if (alpha > 0.5) {
675
+ target[key] = toVal;
554
676
  }
555
677
  }
556
678
  }
package/dist/index.mjs CHANGED
@@ -261,15 +261,29 @@ var Sync = class {
261
261
  this._roomId = null;
262
262
  this.ws = null;
263
263
  this.syncInterval = null;
264
+ this.interpolationInterval = null;
264
265
  this.lastSentState = "";
265
- this.interpolationTargets = /* @__PURE__ */ new Map();
266
266
  this.listeners = /* @__PURE__ */ new Map();
267
+ // Snapshot-based interpolation: store timestamped snapshots per player
268
+ this.snapshots = /* @__PURE__ */ new Map();
269
+ // Jitter buffer: queue incoming updates before applying
270
+ this.jitterQueue = [];
271
+ // Auto-reconnect state
272
+ this.reconnectAttempts = 0;
273
+ this.reconnectTimeout = null;
274
+ this.lastJoinOptions = void 0;
275
+ this.isReconnecting = false;
267
276
  this.state = state;
268
277
  this.myId = config.playerId;
269
278
  this.config = config;
270
279
  this.options = {
271
280
  tickRate: options?.tickRate ?? 20,
272
- interpolate: options?.interpolate ?? true
281
+ interpolate: options?.interpolate ?? true,
282
+ interpolationDelay: options?.interpolationDelay ?? 100,
283
+ jitterBuffer: options?.jitterBuffer ?? 0,
284
+ // 0 = immediate, set to 50+ for smoothing
285
+ autoReconnect: options?.autoReconnect ?? true,
286
+ maxReconnectAttempts: options?.maxReconnectAttempts ?? 10
273
287
  };
274
288
  }
275
289
  /** Current room ID (null if not in a room) */
@@ -287,23 +301,34 @@ var Sync = class {
287
301
  * @param options - Join options
288
302
  */
289
303
  async join(roomId, options) {
290
- if (this._roomId) {
304
+ if (this._roomId && !this.isReconnecting) {
291
305
  await this.leave();
292
306
  }
293
307
  this._roomId = roomId;
308
+ this.lastJoinOptions = options;
309
+ this.reconnectAttempts = 0;
294
310
  await this.connectWebSocket(roomId, options);
295
311
  this.startSyncLoop();
312
+ this.startInterpolationLoop();
296
313
  }
297
314
  /**
298
315
  * Leave the current room
299
316
  */
300
317
  async leave() {
318
+ if (this.reconnectTimeout) {
319
+ clearTimeout(this.reconnectTimeout);
320
+ this.reconnectTimeout = null;
321
+ }
322
+ this.isReconnecting = false;
301
323
  this.stopSyncLoop();
324
+ this.stopInterpolationLoop();
302
325
  if (this.ws) {
303
326
  this.ws.close();
304
327
  this.ws = null;
305
328
  }
306
329
  this.clearRemotePlayers();
330
+ this.snapshots.clear();
331
+ this.jitterQueue = [];
307
332
  this._roomId = null;
308
333
  }
309
334
  /**
@@ -397,6 +422,9 @@ var Sync = class {
397
422
  this.ws.onclose = () => {
398
423
  this.stopSyncLoop();
399
424
  this.emit("disconnected");
425
+ if (this.options.autoReconnect && this._roomId && !this.isReconnecting) {
426
+ this.attemptReconnect();
427
+ }
400
428
  };
401
429
  this.ws.onmessage = (event) => {
402
430
  try {
@@ -436,15 +464,43 @@ var Sync = class {
436
464
  }
437
465
  }
438
466
  applyPlayerState(playerId, playerState) {
467
+ if (this.options.interpolate && this.options.jitterBuffer > 0) {
468
+ this.jitterQueue.push({
469
+ deliverAt: Date.now() + this.options.jitterBuffer,
470
+ playerId,
471
+ state: { ...playerState }
472
+ });
473
+ } else if (this.options.interpolate) {
474
+ this.addSnapshot(playerId, playerState);
475
+ } else {
476
+ this.applyStateDirect(playerId, playerState);
477
+ }
478
+ }
479
+ addSnapshot(playerId, playerState) {
480
+ const isNewPlayer = !this.snapshots.has(playerId);
481
+ if (isNewPlayer) {
482
+ this.snapshots.set(playerId, []);
483
+ }
484
+ const playerSnapshots = this.snapshots.get(playerId);
485
+ playerSnapshots.push({
486
+ time: Date.now(),
487
+ state: { ...playerState }
488
+ });
489
+ while (playerSnapshots.length > 10) {
490
+ playerSnapshots.shift();
491
+ }
492
+ const playersKey = this.findPlayersKey();
493
+ if (playersKey) {
494
+ const players = this.state[playersKey];
495
+ if (isNewPlayer || !players[playerId]) {
496
+ players[playerId] = { ...playerState };
497
+ }
498
+ }
499
+ }
500
+ applyStateDirect(playerId, playerState) {
439
501
  const playersKey = this.findPlayersKey();
440
502
  if (!playersKey) return;
441
503
  const players = this.state[playersKey];
442
- if (this.options.interpolate && players[playerId]) {
443
- this.interpolationTargets.set(playerId, {
444
- target: { ...playerState },
445
- current: { ...players[playerId] }
446
- });
447
- }
448
504
  players[playerId] = playerState;
449
505
  }
450
506
  removePlayer(playerId) {
@@ -452,7 +508,28 @@ var Sync = class {
452
508
  if (!playersKey) return;
453
509
  const players = this.state[playersKey];
454
510
  delete players[playerId];
455
- this.interpolationTargets.delete(playerId);
511
+ this.snapshots.delete(playerId);
512
+ }
513
+ attemptReconnect() {
514
+ if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
515
+ this.emit("error", new Error("Max reconnection attempts reached"));
516
+ return;
517
+ }
518
+ this.isReconnecting = true;
519
+ this.reconnectAttempts++;
520
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
521
+ this.emit("reconnecting", { attempt: this.reconnectAttempts, delay });
522
+ this.reconnectTimeout = setTimeout(async () => {
523
+ try {
524
+ await this.connectWebSocket(this._roomId, this.lastJoinOptions);
525
+ this.startSyncLoop();
526
+ this.isReconnecting = false;
527
+ this.reconnectAttempts = 0;
528
+ this.emit("reconnected");
529
+ } catch (e) {
530
+ this.isReconnecting = false;
531
+ }
532
+ }, delay);
456
533
  }
457
534
  clearRemotePlayers() {
458
535
  const playersKey = this.findPlayersKey();
@@ -463,7 +540,8 @@ var Sync = class {
463
540
  delete players[playerId];
464
541
  }
465
542
  }
466
- this.interpolationTargets.clear();
543
+ this.snapshots.clear();
544
+ this.jitterQueue = [];
467
545
  }
468
546
  findPlayersKey() {
469
547
  const candidates = ["players", "entities", "gnomes", "users", "clients"];
@@ -484,11 +562,30 @@ var Sync = class {
484
562
  const intervalMs = 1e3 / this.options.tickRate;
485
563
  this.syncInterval = setInterval(() => {
486
564
  this.syncMyState();
487
- if (this.options.interpolate) {
488
- this.updateInterpolation();
489
- }
490
565
  }, intervalMs);
491
566
  }
567
+ startInterpolationLoop() {
568
+ if (this.interpolationInterval) return;
569
+ if (!this.options.interpolate) return;
570
+ this.interpolationInterval = setInterval(() => {
571
+ this.processJitterQueue();
572
+ this.updateInterpolation();
573
+ }, 16);
574
+ }
575
+ stopInterpolationLoop() {
576
+ if (this.interpolationInterval) {
577
+ clearInterval(this.interpolationInterval);
578
+ this.interpolationInterval = null;
579
+ }
580
+ }
581
+ processJitterQueue() {
582
+ const now = Date.now();
583
+ const ready = this.jitterQueue.filter((item) => item.deliverAt <= now);
584
+ this.jitterQueue = this.jitterQueue.filter((item) => item.deliverAt > now);
585
+ for (const item of ready) {
586
+ this.addSnapshot(item.playerId, item.state);
587
+ }
588
+ }
492
589
  stopSyncLoop() {
493
590
  if (this.syncInterval) {
494
591
  clearInterval(this.syncInterval);
@@ -514,16 +611,41 @@ var Sync = class {
514
611
  const playersKey = this.findPlayersKey();
515
612
  if (!playersKey) return;
516
613
  const players = this.state[playersKey];
517
- const lerpFactor = 0.2;
518
- for (const [playerId, interp] of this.interpolationTargets) {
614
+ const renderTime = Date.now() - this.options.interpolationDelay;
615
+ for (const [playerId, playerSnapshots] of this.snapshots) {
616
+ if (playerId === this.myId) continue;
519
617
  const player = players[playerId];
520
618
  if (!player) continue;
521
- for (const [key, targetValue] of Object.entries(interp.target)) {
522
- if (typeof targetValue === "number" && typeof interp.current[key] === "number") {
523
- const current = interp.current[key];
524
- const newValue = current + (targetValue - current) * lerpFactor;
525
- interp.current[key] = newValue;
526
- player[key] = newValue;
619
+ let before = null;
620
+ let after = null;
621
+ for (const snapshot of playerSnapshots) {
622
+ if (snapshot.time <= renderTime) {
623
+ before = snapshot;
624
+ } else if (!after) {
625
+ after = snapshot;
626
+ }
627
+ }
628
+ if (before && after) {
629
+ const total = after.time - before.time;
630
+ const elapsed = renderTime - before.time;
631
+ const alpha = total > 0 ? Math.min(1, elapsed / total) : 1;
632
+ this.lerpState(player, before.state, after.state, alpha);
633
+ } else if (before) {
634
+ this.lerpState(player, player, before.state, 0.3);
635
+ } else if (after) {
636
+ this.lerpState(player, player, after.state, 0.3);
637
+ }
638
+ }
639
+ }
640
+ lerpState(target, from, to, alpha) {
641
+ for (const key of Object.keys(to)) {
642
+ const fromVal = from[key];
643
+ const toVal = to[key];
644
+ if (typeof fromVal === "number" && typeof toVal === "number") {
645
+ target[key] = fromVal + (toVal - fromVal) * alpha;
646
+ } else {
647
+ if (alpha > 0.5) {
648
+ target[key] = toVal;
527
649
  }
528
650
  }
529
651
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@watchtower-sdk/core",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Simple game backend SDK - saves, multiplayer rooms, and more",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",