@rpgjs/client 5.0.0-alpha.32 → 5.0.0-alpha.35

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 (49) hide show
  1. package/dist/Game/Map.d.ts +11 -1
  2. package/dist/Game/Map.js +31 -1
  3. package/dist/Game/Map.js.map +1 -1
  4. package/dist/Game/Object.js +1 -1
  5. package/dist/Game/Object.js.map +1 -1
  6. package/dist/Gui/Gui.js +5 -5
  7. package/dist/RpgClientEngine.d.ts +15 -0
  8. package/dist/RpgClientEngine.js +241 -19
  9. package/dist/RpgClientEngine.js.map +1 -1
  10. package/dist/components/gui/box.ce.js +3 -3
  11. package/dist/components/scenes/canvas.ce.js +1 -1
  12. package/dist/components/scenes/canvas.ce.js.map +1 -1
  13. package/dist/components/scenes/draw-map.ce.js +34 -2
  14. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  15. package/dist/core/inject.js +1 -1
  16. package/dist/core/setup.js +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/module.js +1 -1
  19. package/dist/node_modules/.pnpm/{@signe_di@2.8.2 → @signe_di@2.8.3}/node_modules/@signe/di/dist/index.js.map +1 -1
  20. package/dist/node_modules/.pnpm/{@signe_reactive@2.8.2 → @signe_reactive@2.8.3}/node_modules/@signe/reactive/dist/index.js.map +1 -1
  21. package/dist/node_modules/.pnpm/{@signe_room@2.8.2 → @signe_room@2.8.3}/node_modules/@signe/room/dist/index.js +4 -4
  22. package/dist/node_modules/.pnpm/@signe_room@2.8.3/node_modules/@signe/room/dist/index.js.map +1 -0
  23. package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +1 -1
  24. package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/client/index.js.map +1 -1
  25. package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/index.js +1 -1
  26. package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/index.js.map +1 -1
  27. package/dist/services/AbstractSocket.d.ts +9 -5
  28. package/dist/services/AbstractSocket.js.map +1 -1
  29. package/dist/services/loadMap.js +1 -1
  30. package/dist/services/mmorpg.d.ts +12 -8
  31. package/dist/services/mmorpg.js +68 -13
  32. package/dist/services/mmorpg.js.map +1 -1
  33. package/dist/services/standalone.d.ts +2 -4
  34. package/dist/services/standalone.js +2 -2
  35. package/dist/services/standalone.js.map +1 -1
  36. package/package.json +10 -10
  37. package/src/Game/Map.ts +38 -2
  38. package/src/Game/Object.ts +1 -1
  39. package/src/RpgClientEngine.ts +300 -20
  40. package/src/components/scenes/canvas.ce +1 -1
  41. package/src/components/scenes/draw-map.ce +37 -1
  42. package/src/services/AbstractSocket.ts +10 -2
  43. package/src/services/mmorpg.ts +84 -14
  44. package/src/services/standalone.ts +3 -3
  45. package/dist/node_modules/.pnpm/@signe_room@2.8.2/node_modules/@signe/room/dist/index.js.map +0 -1
  46. /package/dist/node_modules/.pnpm/{@signe_di@2.8.2 → @signe_di@2.8.3}/node_modules/@signe/di/dist/index.js +0 -0
  47. /package/dist/node_modules/.pnpm/{@signe_reactive@2.8.2 → @signe_reactive@2.8.3}/node_modules/@signe/reactive/dist/index.js +0 -0
  48. /package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +0 -0
  49. /package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/client/index.js +0 -0
@@ -18,11 +18,22 @@ import * as PIXI from "pixi.js";
18
18
  import { PrebuiltComponentAnimations } from "./components/animations";
19
19
  import {
20
20
  PredictionController,
21
+ type PredictionHistoryEntry,
21
22
  type PredictionState,
22
23
  } from "@rpgjs/common";
23
24
  import { NotificationManager } from "./Gui/NotificationManager";
24
25
  import { SaveClientService } from "./services/save";
25
26
 
27
+ interface MovementTrajectoryPoint {
28
+ frame: number;
29
+ tick: number;
30
+ timestamp: number;
31
+ input: Direction;
32
+ x: number;
33
+ y: number;
34
+ direction?: Direction;
35
+ }
36
+
26
37
  export class RpgClientEngine<T = any> {
27
38
  private guiService: RpgGui;
28
39
  private webSocket: AbstractWebsocket;
@@ -64,12 +75,18 @@ export class RpgClientEngine<T = any> {
64
75
  private prediction?: PredictionController<Direction>;
65
76
  private readonly SERVER_CORRECTION_THRESHOLD = 30;
66
77
  private inputFrameCounter = 0;
78
+ private pendingPredictionFrames: number[] = [];
79
+ private lastClientPhysicsStepAt = 0;
67
80
  private frameOffset = 0;
68
81
  // Ping/Pong for RTT measurement
69
82
  private rtt: number = 0; // Round-trip time in ms
70
83
  private pingInterval: any = null;
71
84
  private readonly PING_INTERVAL_MS = 5000; // Send ping every 5 seconds
72
85
  private lastInputTime = 0;
86
+ private readonly MOVE_PATH_RESEND_INTERVAL_MS = 120;
87
+ private readonly MAX_MOVE_TRAJECTORY_POINTS = 240;
88
+ private lastMovePathSentAt = 0;
89
+ private lastMovePathSentFrame = 0;
73
90
  // Track map loading state for onAfterLoading hook using RxJS
74
91
  private mapLoadCompleted$ = new BehaviorSubject<boolean>(false);
75
92
  private playerIdReceived$ = new BehaviorSubject<boolean>(false);
@@ -156,6 +173,8 @@ export class RpgClientEngine<T = any> {
156
173
 
157
174
  async start() {
158
175
  this.sceneMap = new RpgClientMap()
176
+ this.sceneMap.configureClientPrediction(this.predictionEnabled);
177
+ this.sceneMap.loadPhysic();
159
178
  this.selector = document.body.querySelector("#rpg") as HTMLElement;
160
179
 
161
180
  const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;
@@ -200,6 +219,9 @@ export class RpgClientEngine<T = any> {
200
219
  window.addEventListener('resize', this.resizeHandler);
201
220
 
202
221
  const tickSubscription = this.tick.subscribe((tick) => {
222
+ this.stepClientPhysicsTick();
223
+ this.flushPendingPredictedStates();
224
+ this.flushPendingMovePath();
203
225
  this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
204
226
 
205
227
  // Clean up old prediction states and input history every 60 ticks (approximately every second at 60fps)
@@ -216,9 +238,56 @@ export class RpgClientEngine<T = any> {
216
238
  saveClient.initialize(this.webSocket);
217
239
  this.initListeners()
218
240
  this.guiService._initialize()
241
+ this.startPingPong();
219
242
  });
220
243
  }
221
244
 
245
+ private prepareSyncPayload(data: any): any {
246
+ const payload = { ...(data ?? {}) };
247
+ delete payload.ack;
248
+ delete payload.timestamp;
249
+
250
+ const myId = this.playerIdSignal();
251
+ const players = payload.players;
252
+ const shouldMaskLocalPosition =
253
+ this.predictionEnabled && !!this.prediction?.hasPendingInputs();
254
+ if (shouldMaskLocalPosition && myId && players && players[myId]) {
255
+ const localPatch = { ...players[myId] };
256
+ delete localPatch.x;
257
+ delete localPatch.y;
258
+ delete localPatch.direction;
259
+ delete localPatch._frames;
260
+ payload.players = {
261
+ ...players,
262
+ [myId]: localPatch,
263
+ };
264
+ }
265
+
266
+ return payload;
267
+ }
268
+
269
+ private normalizeAckWithSyncState(
270
+ ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction },
271
+ syncData: any,
272
+ ): { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction } {
273
+ const myId = this.playerIdSignal();
274
+ if (!myId) {
275
+ return ack;
276
+ }
277
+
278
+ const localPatch = syncData?.players?.[myId];
279
+ if (typeof localPatch?.x !== "number" || typeof localPatch?.y !== "number") {
280
+ return ack;
281
+ }
282
+
283
+ return {
284
+ ...ack,
285
+ x: localPatch.x,
286
+ y: localPatch.y,
287
+ direction: localPatch.direction ?? ack.direction,
288
+ };
289
+ }
290
+
222
291
  private initListeners() {
223
292
  this.webSocket.on("sync", (data) => {
224
293
  if (data.pId) {
@@ -229,16 +298,27 @@ export class RpgClientEngine<T = any> {
229
298
 
230
299
  if (this.sceneResetQueued) {
231
300
  this.sceneMap.reset();
301
+ this.sceneMap.loadPhysic();
232
302
  this.sceneResetQueued = false;
233
303
  }
234
304
 
235
305
  // Apply client-side prediction filtering and server reconciliation
236
306
  this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
237
307
 
238
- load(this.sceneMap, data, true);
308
+ const ack = data?.ack;
309
+ const normalizedAck =
310
+ ack && typeof ack.frame === "number"
311
+ ? this.normalizeAckWithSyncState(ack, data)
312
+ : undefined;
313
+ const payload = this.prepareSyncPayload(data);
314
+ load(this.sceneMap, payload, true);
239
315
 
240
- for (const playerId in data.players ?? {}) {
241
- const player = data.players[playerId]
316
+ if (normalizedAck) {
317
+ this.applyServerAck(normalizedAck);
318
+ }
319
+
320
+ for (const playerId in payload.players ?? {}) {
321
+ const player = payload.players[playerId]
242
322
  if (!player._param) continue
243
323
  for (const param in player._param) {
244
324
  this.sceneMap.players()[playerId]._param()[param] = player._param[param]
@@ -246,12 +326,12 @@ export class RpgClientEngine<T = any> {
246
326
  }
247
327
 
248
328
  // Check if players and events are present in sync data
249
- const players = data.players || this.sceneMap.players();
329
+ const players = payload.players || this.sceneMap.players();
250
330
  if (players && Object.keys(players).length > 0) {
251
331
  this.playersReceived$.next(true);
252
332
  }
253
333
 
254
- const events = data.events || this.sceneMap.events();
334
+ const events = payload.events || this.sceneMap.events();
255
335
  if (events !== undefined) {
256
336
  this.eventsReceived$.next(true);
257
337
  }
@@ -279,7 +359,8 @@ export class RpgClientEngine<T = any> {
279
359
  this.sceneResetQueued = true;
280
360
  // Reset camera follow to default (follow current player) when changing maps
281
361
  this.cameraFollowTargetId.set(null);
282
- this.loadScene(data.mapId);
362
+ const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
363
+ this.loadScene(data.mapId, transferToken);
283
364
  });
284
365
 
285
366
  this.webSocket.on("showComponentAnimation", (data) => {
@@ -342,9 +423,36 @@ export class RpgClientEngine<T = any> {
342
423
  });
343
424
  });
344
425
 
426
+ this.webSocket.on("weatherState", (data) => {
427
+ const raw = (data && typeof data === "object" && "value" in data)
428
+ ? (data as any).value
429
+ : data;
430
+
431
+ if (raw === null) {
432
+ this.sceneMap.weatherState.set(null);
433
+ return;
434
+ }
435
+
436
+ const validEffects = ["rain", "snow", "fog", "cloud"];
437
+ if (!raw || !validEffects.includes((raw as any).effect)) {
438
+ return;
439
+ }
440
+
441
+ this.sceneMap.weatherState.set({
442
+ effect: (raw as any).effect,
443
+ preset: (raw as any).preset,
444
+ params: (raw as any).params,
445
+ transitionMs: (raw as any).transitionMs,
446
+ durationMs: (raw as any).durationMs,
447
+ startedAt: (raw as any).startedAt,
448
+ seed: (raw as any).seed,
449
+ });
450
+ });
451
+
345
452
  this.webSocket.on('open', () => {
346
453
  this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
347
454
  // Start ping/pong for synchronization
455
+ this.startPingPong();
348
456
  })
349
457
 
350
458
  this.webSocket.on('close', () => {
@@ -430,7 +538,7 @@ export class RpgClientEngine<T = any> {
430
538
  });
431
539
  }
432
540
 
433
- private async loadScene(mapId: string) {
541
+ private async loadScene(mapId: string, transferToken?: string) {
434
542
  await lastValueFrom(this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap));
435
543
 
436
544
  // Clear client prediction states when changing maps
@@ -450,7 +558,10 @@ export class RpgClientEngine<T = any> {
450
558
  // Setup RxJS observable to wait for all conditions
451
559
  this.setupOnAfterLoadingObserver();
452
560
 
453
- this.webSocket.updateProperties({ room: mapId })
561
+ this.webSocket.updateProperties({
562
+ room: mapId,
563
+ query: transferToken ? { transferToken } : undefined,
564
+ })
454
565
  await this.webSocket.reconnect(() => {
455
566
  const saveClient = inject(SaveClientService);
456
567
  saveClient.initialize(this.webSocket);
@@ -478,6 +589,7 @@ export class RpgClientEngine<T = any> {
478
589
 
479
590
  // Signal that map loading is completed (this should be last to ensure other checks are done)
480
591
  this.mapLoadCompleted$.next(true);
592
+ this.sceneMap.configureClientPrediction(this.predictionEnabled);
481
593
  this.sceneMap.loadPhysic()
482
594
  }
483
595
 
@@ -1075,22 +1187,24 @@ export class RpgClientEngine<T = any> {
1075
1187
  frame = ++this.inputFrameCounter;
1076
1188
  tick = this.getPhysicsTick();
1077
1189
  }
1190
+ this.inputFrameCounter = frame;
1078
1191
  this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
1079
1192
 
1080
- this.webSocket.emit('move', {
1081
- input,
1082
- timestamp,
1083
- frame,
1084
- tick,
1085
- });
1086
-
1087
1193
  const currentPlayer = this.sceneMap.getCurrentPlayer();
1088
- if (currentPlayer) {
1194
+ const bodyReady = this.ensureCurrentPlayerBody();
1195
+ if (currentPlayer && bodyReady) {
1196
+ currentPlayer.changeDirection(input);
1089
1197
  (this.sceneMap as any).moveBody(currentPlayer, input);
1198
+ if (this.predictionEnabled && this.prediction) {
1199
+ this.pendingPredictionFrames.push(frame);
1200
+ if (this.pendingPredictionFrames.length > 240) {
1201
+ this.pendingPredictionFrames = this.pendingPredictionFrames.slice(-240);
1202
+ }
1203
+ }
1090
1204
  }
1091
- this.lastInputTime = Date.now();
1092
- const myId = this.playerIdSignal();
1093
1205
 
1206
+ this.emitMovePacket(input, frame, tick, timestamp, true);
1207
+ this.lastInputTime = Date.now();
1094
1208
  }
1095
1209
 
1096
1210
  processAction({ action }: { action: number }) {
@@ -1119,6 +1233,127 @@ export class RpgClientEngine<T = any> {
1119
1233
  return this.sceneMap?.getTick?.() ?? 0;
1120
1234
  }
1121
1235
 
1236
+ private ensureCurrentPlayerBody(): boolean {
1237
+ const player = this.sceneMap?.getCurrentPlayer();
1238
+ const myId = this.playerIdSignal();
1239
+ if (!player || !myId) {
1240
+ return false;
1241
+ }
1242
+ if (!player.id) {
1243
+ player.id = myId;
1244
+ }
1245
+ if (this.sceneMap.getBody(myId)) {
1246
+ return true;
1247
+ }
1248
+ try {
1249
+ this.sceneMap.loadPhysic();
1250
+ } catch (error) {
1251
+ console.error("[RPGJS] Unable to initialize client physics before input:", error);
1252
+ return false;
1253
+ }
1254
+ return !!this.sceneMap.getBody(myId);
1255
+ }
1256
+
1257
+ private stepClientPhysicsTick(): void {
1258
+ if (!this.predictionEnabled || !this.sceneMap) {
1259
+ return;
1260
+ }
1261
+ const now = Date.now();
1262
+ if (this.lastClientPhysicsStepAt === 0) {
1263
+ this.lastClientPhysicsStepAt = now;
1264
+ }
1265
+ const deltaMs = Math.max(1, Math.min(100, now - this.lastClientPhysicsStepAt));
1266
+ this.lastClientPhysicsStepAt = now;
1267
+ this.sceneMap.stepClientPhysics(deltaMs);
1268
+ }
1269
+
1270
+ private flushPendingPredictedStates(): void {
1271
+ if (!this.predictionEnabled || !this.prediction || this.pendingPredictionFrames.length === 0) {
1272
+ return;
1273
+ }
1274
+ const state = this.getLocalPlayerState();
1275
+ while (this.pendingPredictionFrames.length > 0) {
1276
+ const frame = this.pendingPredictionFrames.shift();
1277
+ if (typeof frame === "number") {
1278
+ this.prediction.attachPredictedState(frame, state);
1279
+ }
1280
+ }
1281
+ }
1282
+
1283
+ private buildPendingMoveTrajectory(): MovementTrajectoryPoint[] {
1284
+ if (!this.predictionEnabled || !this.prediction) {
1285
+ return [];
1286
+ }
1287
+ const pendingInputs = this.prediction.getPendingInputs();
1288
+ const trajectory: MovementTrajectoryPoint[] = [];
1289
+ for (const entry of pendingInputs) {
1290
+ const state = entry.state;
1291
+ if (!state) continue;
1292
+ if (typeof state.x !== "number" || typeof state.y !== "number") continue;
1293
+ trajectory.push({
1294
+ frame: entry.frame,
1295
+ tick: entry.tick,
1296
+ timestamp: entry.timestamp,
1297
+ input: entry.direction,
1298
+ x: state.x,
1299
+ y: state.y,
1300
+ direction: state.direction ?? entry.direction,
1301
+ });
1302
+ }
1303
+ if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {
1304
+ return trajectory.slice(-this.MAX_MOVE_TRAJECTORY_POINTS);
1305
+ }
1306
+ return trajectory;
1307
+ }
1308
+
1309
+ private emitMovePacket(
1310
+ input: Direction,
1311
+ frame: number,
1312
+ tick: number,
1313
+ timestamp: number,
1314
+ force = false,
1315
+ ): void {
1316
+ const trajectory = this.buildPendingMoveTrajectory();
1317
+ const latestTrajectoryFrame =
1318
+ trajectory.length > 0 ? trajectory[trajectory.length - 1].frame : frame;
1319
+ const shouldThrottle =
1320
+ !force &&
1321
+ latestTrajectoryFrame <= this.lastMovePathSentFrame &&
1322
+ timestamp - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS;
1323
+ if (shouldThrottle) {
1324
+ return;
1325
+ }
1326
+
1327
+ this.webSocket.emit("move", {
1328
+ input,
1329
+ timestamp,
1330
+ frame,
1331
+ tick,
1332
+ trajectory,
1333
+ });
1334
+ this.lastMovePathSentAt = timestamp;
1335
+ this.lastMovePathSentFrame = Math.max(this.lastMovePathSentFrame, latestTrajectoryFrame, frame);
1336
+ }
1337
+
1338
+ private flushPendingMovePath(): void {
1339
+ if (!this.predictionEnabled || !this.prediction) {
1340
+ return;
1341
+ }
1342
+ const pendingInputs = this.prediction.getPendingInputs();
1343
+ if (pendingInputs.length === 0) {
1344
+ return;
1345
+ }
1346
+ const latest = pendingInputs[pendingInputs.length - 1];
1347
+ if (!latest) {
1348
+ return;
1349
+ }
1350
+ const now = Date.now();
1351
+ if (now - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS) {
1352
+ return;
1353
+ }
1354
+ this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);
1355
+ }
1356
+
1122
1357
  private getLocalPlayerState(): PredictionState<Direction> {
1123
1358
  const currentPlayer = this.sceneMap?.getCurrentPlayer();
1124
1359
  if (!currentPlayer) {
@@ -1151,11 +1386,21 @@ export class RpgClientEngine<T = any> {
1151
1386
  private initializePredictionController(): void {
1152
1387
  if (!this.predictionEnabled) {
1153
1388
  this.prediction = undefined;
1389
+ this.sceneMap?.configureClientPrediction?.(false);
1154
1390
  return;
1155
1391
  }
1392
+ const configuredTtl = (this.globalConfig as any)?.prediction?.historyTtlMs;
1393
+ const historyTtlMs = typeof configuredTtl === "number" ? configuredTtl : 10000;
1394
+ const configuredMaxEntries = (this.globalConfig as any)?.prediction?.maxHistoryEntries;
1395
+ const maxHistoryEntries =
1396
+ typeof configuredMaxEntries === "number"
1397
+ ? configuredMaxEntries
1398
+ : Math.max(600, Math.ceil(historyTtlMs / 16) + 120);
1399
+ this.sceneMap?.configureClientPrediction?.(true);
1156
1400
  this.prediction = new PredictionController<Direction>({
1157
1401
  correctionThreshold: (this.globalConfig as any)?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,
1158
- historyTtlMs: (this.globalConfig as any)?.prediction?.historyTtlMs ?? 2000,
1402
+ historyTtlMs,
1403
+ maxHistoryEntries,
1159
1404
  getPhysicsTick: () => this.getPhysicsTick(),
1160
1405
  getCurrentState: () => this.getLocalPlayerState(),
1161
1406
  setAuthoritativeState: (state) => this.applyAuthoritativeState(state),
@@ -1222,6 +1467,10 @@ export class RpgClientEngine<T = any> {
1222
1467
  this.initializePredictionController();
1223
1468
  this.frameOffset = 0;
1224
1469
  this.inputFrameCounter = 0;
1470
+ this.pendingPredictionFrames = [];
1471
+ this.lastClientPhysicsStepAt = 0;
1472
+ this.lastMovePathSentAt = 0;
1473
+ this.lastMovePathSentFrame = 0;
1225
1474
  }
1226
1475
 
1227
1476
  /**
@@ -1292,7 +1541,7 @@ export class RpgClientEngine<T = any> {
1292
1541
 
1293
1542
  private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
1294
1543
  if (this.predictionEnabled && this.prediction) {
1295
- this.prediction.applyServerAck({
1544
+ const result = this.prediction.applyServerAck({
1296
1545
  frame: ack.frame,
1297
1546
  serverTick: ack.serverTick,
1298
1547
  state:
@@ -1300,6 +1549,9 @@ export class RpgClientEngine<T = any> {
1300
1549
  ? { x: ack.x, y: ack.y, direction: ack.direction }
1301
1550
  : undefined,
1302
1551
  });
1552
+ if (result.state && result.needsReconciliation) {
1553
+ this.reconcilePrediction(result.state, result.pendingInputs);
1554
+ }
1303
1555
  return;
1304
1556
  }
1305
1557
 
@@ -1325,6 +1577,32 @@ export class RpgClientEngine<T = any> {
1325
1577
  }
1326
1578
  }
1327
1579
 
1580
+ private reconcilePrediction(
1581
+ authoritativeState: PredictionState<Direction>,
1582
+ pendingInputs: PredictionHistoryEntry<Direction>[],
1583
+ ): void {
1584
+ const player = this.getCurrentPlayer();
1585
+ if (!player) {
1586
+ return;
1587
+ }
1588
+
1589
+ (this.sceneMap as any).stopMovement(player);
1590
+ this.applyAuthoritativeState(authoritativeState);
1591
+
1592
+ if (!pendingInputs.length) {
1593
+ return;
1594
+ }
1595
+
1596
+ // Keep replay bounded while still tolerating high-latency links.
1597
+ const replayInputs = pendingInputs.slice(-600);
1598
+ for (const entry of replayInputs) {
1599
+ if (!entry?.direction) continue;
1600
+ (this.sceneMap as any).moveBody(player, entry.direction);
1601
+ this.sceneMap.stepPredictionTick();
1602
+ this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());
1603
+ }
1604
+ }
1605
+
1328
1606
  /**
1329
1607
  * Replay unacknowledged inputs from a given frame to resimulate client prediction
1330
1608
  * after applying server authority at a certain frame.
@@ -1505,6 +1783,8 @@ export class RpgClientEngine<T = any> {
1505
1783
  this.inputFrameCounter = 0;
1506
1784
  this.frameOffset = 0;
1507
1785
  this.rtt = 0;
1786
+ this.lastMovePathSentAt = 0;
1787
+ this.lastMovePathSentFrame = 0;
1508
1788
 
1509
1789
  // Reset behavior subjects
1510
1790
  this.mapLoadCompleted$.next(false);
@@ -1,5 +1,5 @@
1
1
  <Canvas width={engine.width} height={engine.height}>
2
- <Viewport worldWidth worldHeight clamp>
2
+ <Viewport worldWidth worldHeight clamp sortableChildren={true}>
3
3
  @if (sceneData) {
4
4
  <SceneMap />
5
5
  }
@@ -10,6 +10,10 @@
10
10
  }
11
11
  </Container>
12
12
  }
13
+
14
+ @if (weatherProps) {
15
+ <Weather ...weatherProps() />
16
+ }
13
17
  </Container>
14
18
 
15
19
  <script>
@@ -17,6 +21,7 @@
17
21
  import { effect, signal, computed, mount, on, tick } from 'canvasengine'
18
22
  import { inject } from "../../core/inject";
19
23
  import { RpgClientEngine } from "../../RpgClientEngine";
24
+ import { Weather } from '@canvasengine/presets'
20
25
 
21
26
  const engine = inject(RpgClientEngine);
22
27
  const componentAnimations = engine.componentAnimations
@@ -25,6 +30,7 @@
25
30
  const sceneParams = map()?.params
26
31
  const mapParams = map()?.params
27
32
  const animations = engine.sceneMap.animations
33
+ const weather = engine.sceneMap.weather
28
34
  const backgroundMusic = { src: mapParams?.backgroundMusic, autoplay: true, loop: true }
29
35
  const backgroundAmbientSound = { src: mapParams?.backgroundAmbientSound, autoplay: true, loop: true }
30
36
 
@@ -45,4 +51,34 @@
45
51
  frequency: 10,
46
52
  direction: 'both'
47
53
  }
48
- </script>
54
+
55
+ const weatherProps = computed(() => {
56
+ const state = weather?.()
57
+ if (!state) {
58
+ return null
59
+ }
60
+ const validEffects = ['rain', 'snow', 'fog', 'cloud']
61
+ if (!validEffects.includes(state.effect)) {
62
+ return null
63
+ }
64
+ const params = state.params ?? {}
65
+ return {
66
+ effect: state.effect,
67
+ speed: params.speed,
68
+ windDirection: params.windDirection,
69
+ windStrength: params.windStrength,
70
+ density: params.density,
71
+ maxDrops: params.maxDrops,
72
+ height: params.height,
73
+ scale: params.scale,
74
+ sunIntensity: params.sunIntensity,
75
+ sunAngle: params.sunAngle,
76
+ raySpread: params.raySpread,
77
+ rayTwinkle: params.rayTwinkle,
78
+ rayTwinkleSpeed: params.rayTwinkleSpeed,
79
+ zIndex: params.zIndex ?? 1000,
80
+ alpha: params.alpha,
81
+ blendMode: params.blendMode,
82
+ }
83
+ })
84
+ </script>
@@ -2,6 +2,14 @@ import { Context } from "@signe/di";
2
2
 
3
3
  export const WebSocketToken = "websocket";
4
4
 
5
+ export type SocketQueryValue = string | null | undefined;
6
+ export type SocketQuery = Record<string, SocketQueryValue>;
7
+ export type SocketUpdateProperties = {
8
+ room: string;
9
+ host?: string;
10
+ query?: SocketQuery;
11
+ };
12
+
5
13
  export abstract class AbstractWebsocket {
6
14
  constructor(protected context: Context) {}
7
15
 
@@ -9,6 +17,6 @@ export abstract class AbstractWebsocket {
9
17
  abstract emit(event: string, data: any): void;
10
18
  abstract on(event: string, callback: (data: any) => void): void;
11
19
  abstract off(event: string, callback: (data: any) => void): void;
12
- abstract updateProperties(params: { room: string, host?: string }): void;
13
- abstract reconnect(listeners?: (data: any) => void): void;
20
+ abstract updateProperties(params: SocketUpdateProperties): void;
21
+ abstract reconnect(listeners?: (data: any) => void): Promise<void>;
14
22
  }