@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
@@ -6,7 +6,7 @@ import { LoadMapToken } from './services/loadMap.js';
6
6
  import { RpgSound } from './Sound.js';
7
7
  import { RpgResource } from './Resource.js';
8
8
  import { Direction, PredictionController, ModulesToken } from '@rpgjs/common';
9
- import { load } from './node_modules/.pnpm/@signe_sync@2.8.2/node_modules/@signe/sync/dist/index.js';
9
+ import { load } from './node_modules/.pnpm/@signe_sync@2.8.3/node_modules/@signe/sync/dist/index.js';
10
10
  import { RpgClientMap } from './Game/Map.js';
11
11
  import { RpgGui } from './Gui/Gui.js';
12
12
  import { AnimationManager } from './Game/AnimationManager.js';
@@ -41,6 +41,8 @@ class RpgClientEngine {
41
41
  this.predictionEnabled = false;
42
42
  this.SERVER_CORRECTION_THRESHOLD = 30;
43
43
  this.inputFrameCounter = 0;
44
+ this.pendingPredictionFrames = [];
45
+ this.lastClientPhysicsStepAt = 0;
44
46
  this.frameOffset = 0;
45
47
  // Ping/Pong for RTT measurement
46
48
  this.rtt = 0;
@@ -49,6 +51,10 @@ class RpgClientEngine {
49
51
  this.PING_INTERVAL_MS = 5e3;
50
52
  // Send ping every 5 seconds
51
53
  this.lastInputTime = 0;
54
+ this.MOVE_PATH_RESEND_INTERVAL_MS = 120;
55
+ this.MAX_MOVE_TRAJECTORY_POINTS = 240;
56
+ this.lastMovePathSentAt = 0;
57
+ this.lastMovePathSentFrame = 0;
52
58
  // Track map loading state for onAfterLoading hook using RxJS
53
59
  this.mapLoadCompleted$ = new BehaviorSubject(false);
54
60
  this.playerIdReceived$ = new BehaviorSubject(false);
@@ -125,6 +131,8 @@ class RpgClientEngine {
125
131
  }
126
132
  async start() {
127
133
  this.sceneMap = new RpgClientMap();
134
+ this.sceneMap.configureClientPrediction(this.predictionEnabled);
135
+ this.sceneMap.loadPhysic();
128
136
  this.selector = document.body.querySelector("#rpg");
129
137
  const bootstrapOptions = this.globalConfig?.bootstrapCanvasOptions;
130
138
  const { app, canvasElement } = await bootstrapCanvas(
@@ -160,6 +168,9 @@ class RpgClientEngine {
160
168
  };
161
169
  window.addEventListener("resize", this.resizeHandler);
162
170
  const tickSubscription = this.tick.subscribe((tick) => {
171
+ this.stepClientPhysicsTick();
172
+ this.flushPendingPredictedStates();
173
+ this.flushPendingMovePath();
163
174
  this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
164
175
  if (tick % 60 === 0) {
165
176
  const now = Date.now();
@@ -173,8 +184,45 @@ class RpgClientEngine {
173
184
  saveClient.initialize(this.webSocket);
174
185
  this.initListeners();
175
186
  this.guiService._initialize();
187
+ this.startPingPong();
176
188
  });
177
189
  }
190
+ prepareSyncPayload(data) {
191
+ const payload = { ...data ?? {} };
192
+ delete payload.ack;
193
+ delete payload.timestamp;
194
+ const myId = this.playerIdSignal();
195
+ const players = payload.players;
196
+ const shouldMaskLocalPosition = this.predictionEnabled && !!this.prediction?.hasPendingInputs();
197
+ if (shouldMaskLocalPosition && myId && players && players[myId]) {
198
+ const localPatch = { ...players[myId] };
199
+ delete localPatch.x;
200
+ delete localPatch.y;
201
+ delete localPatch.direction;
202
+ delete localPatch._frames;
203
+ payload.players = {
204
+ ...players,
205
+ [myId]: localPatch
206
+ };
207
+ }
208
+ return payload;
209
+ }
210
+ normalizeAckWithSyncState(ack, syncData) {
211
+ const myId = this.playerIdSignal();
212
+ if (!myId) {
213
+ return ack;
214
+ }
215
+ const localPatch = syncData?.players?.[myId];
216
+ if (typeof localPatch?.x !== "number" || typeof localPatch?.y !== "number") {
217
+ return ack;
218
+ }
219
+ return {
220
+ ...ack,
221
+ x: localPatch.x,
222
+ y: localPatch.y,
223
+ direction: localPatch.direction ?? ack.direction
224
+ };
225
+ }
178
226
  initListeners() {
179
227
  this.webSocket.on("sync", (data) => {
180
228
  if (data.pId) {
@@ -183,22 +231,29 @@ class RpgClientEngine {
183
231
  }
184
232
  if (this.sceneResetQueued) {
185
233
  this.sceneMap.reset();
234
+ this.sceneMap.loadPhysic();
186
235
  this.sceneResetQueued = false;
187
236
  }
188
237
  this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
189
- load(this.sceneMap, data, true);
190
- for (const playerId in data.players ?? {}) {
191
- const player = data.players[playerId];
238
+ const ack = data?.ack;
239
+ const normalizedAck = ack && typeof ack.frame === "number" ? this.normalizeAckWithSyncState(ack, data) : void 0;
240
+ const payload = this.prepareSyncPayload(data);
241
+ load(this.sceneMap, payload, true);
242
+ if (normalizedAck) {
243
+ this.applyServerAck(normalizedAck);
244
+ }
245
+ for (const playerId in payload.players ?? {}) {
246
+ const player = payload.players[playerId];
192
247
  if (!player._param) continue;
193
248
  for (const param in player._param) {
194
249
  this.sceneMap.players()[playerId]._param()[param] = player._param[param];
195
250
  }
196
251
  }
197
- const players = data.players || this.sceneMap.players();
252
+ const players = payload.players || this.sceneMap.players();
198
253
  if (players && Object.keys(players).length > 0) {
199
254
  this.playersReceived$.next(true);
200
255
  }
201
- const events = data.events || this.sceneMap.events();
256
+ const events = payload.events || this.sceneMap.events();
202
257
  if (events !== void 0) {
203
258
  this.eventsReceived$.next(true);
204
259
  }
@@ -216,7 +271,8 @@ class RpgClientEngine {
216
271
  this.webSocket.on("changeMap", (data) => {
217
272
  this.sceneResetQueued = true;
218
273
  this.cameraFollowTargetId.set(null);
219
- this.loadScene(data.mapId);
274
+ const transferToken = typeof data?.transferToken === "string" ? data.transferToken : void 0;
275
+ this.loadScene(data.mapId, transferToken);
220
276
  });
221
277
  this.webSocket.on("showComponentAnimation", (data) => {
222
278
  const { params, object, position, id } = data;
@@ -269,8 +325,29 @@ class RpgClientEngine {
269
325
  direction
270
326
  });
271
327
  });
328
+ this.webSocket.on("weatherState", (data) => {
329
+ const raw = data && typeof data === "object" && "value" in data ? data.value : data;
330
+ if (raw === null) {
331
+ this.sceneMap.weatherState.set(null);
332
+ return;
333
+ }
334
+ const validEffects = ["rain", "snow", "fog", "cloud"];
335
+ if (!raw || !validEffects.includes(raw.effect)) {
336
+ return;
337
+ }
338
+ this.sceneMap.weatherState.set({
339
+ effect: raw.effect,
340
+ preset: raw.preset,
341
+ params: raw.params,
342
+ transitionMs: raw.transitionMs,
343
+ durationMs: raw.durationMs,
344
+ startedAt: raw.startedAt,
345
+ seed: raw.seed
346
+ });
347
+ });
272
348
  this.webSocket.on("open", () => {
273
349
  this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
350
+ this.startPingPong();
274
351
  });
275
352
  this.webSocket.on("close", () => {
276
353
  this.hooks.callHooks("client-engine-onDisconnected", this, this.socket).subscribe();
@@ -343,7 +420,7 @@ class RpgClientEngine {
343
420
  clientFrame
344
421
  });
345
422
  }
346
- async loadScene(mapId) {
423
+ async loadScene(mapId, transferToken) {
347
424
  await lastValueFrom(this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap));
348
425
  this.clearClientPredictionStates();
349
426
  this.mapLoadCompleted$.next(false);
@@ -354,7 +431,10 @@ class RpgClientEngine {
354
431
  this.onAfterLoadingSubscription.unsubscribe();
355
432
  }
356
433
  this.setupOnAfterLoadingObserver();
357
- this.webSocket.updateProperties({ room: mapId });
434
+ this.webSocket.updateProperties({
435
+ room: mapId,
436
+ query: transferToken ? { transferToken } : void 0
437
+ });
358
438
  await this.webSocket.reconnect(() => {
359
439
  const saveClient = inject(SaveClientService);
360
440
  saveClient.initialize(this.webSocket);
@@ -375,6 +455,7 @@ class RpgClientEngine {
375
455
  this.eventsReceived$.next(true);
376
456
  }
377
457
  this.mapLoadCompleted$.next(true);
458
+ this.sceneMap.configureClientPrediction(this.predictionEnabled);
378
459
  this.sceneMap.loadPhysic();
379
460
  }
380
461
  addSpriteSheet(spritesheetClass, id) {
@@ -899,19 +980,22 @@ class RpgClientEngine {
899
980
  frame = ++this.inputFrameCounter;
900
981
  tick = this.getPhysicsTick();
901
982
  }
983
+ this.inputFrameCounter = frame;
902
984
  this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
903
- this.webSocket.emit("move", {
904
- input,
905
- timestamp,
906
- frame,
907
- tick
908
- });
909
985
  const currentPlayer = this.sceneMap.getCurrentPlayer();
910
- if (currentPlayer) {
986
+ const bodyReady = this.ensureCurrentPlayerBody();
987
+ if (currentPlayer && bodyReady) {
988
+ currentPlayer.changeDirection(input);
911
989
  this.sceneMap.moveBody(currentPlayer, input);
990
+ if (this.predictionEnabled && this.prediction) {
991
+ this.pendingPredictionFrames.push(frame);
992
+ if (this.pendingPredictionFrames.length > 240) {
993
+ this.pendingPredictionFrames = this.pendingPredictionFrames.slice(-240);
994
+ }
995
+ }
912
996
  }
997
+ this.emitMovePacket(input, frame, tick, timestamp, true);
913
998
  this.lastInputTime = Date.now();
914
- this.playerIdSignal();
915
999
  }
916
1000
  processAction({ action }) {
917
1001
  if (this.stopProcessingInput) return;
@@ -933,6 +1017,110 @@ class RpgClientEngine {
933
1017
  getPhysicsTick() {
934
1018
  return this.sceneMap?.getTick?.() ?? 0;
935
1019
  }
1020
+ ensureCurrentPlayerBody() {
1021
+ const player = this.sceneMap?.getCurrentPlayer();
1022
+ const myId = this.playerIdSignal();
1023
+ if (!player || !myId) {
1024
+ return false;
1025
+ }
1026
+ if (!player.id) {
1027
+ player.id = myId;
1028
+ }
1029
+ if (this.sceneMap.getBody(myId)) {
1030
+ return true;
1031
+ }
1032
+ try {
1033
+ this.sceneMap.loadPhysic();
1034
+ } catch (error) {
1035
+ console.error("[RPGJS] Unable to initialize client physics before input:", error);
1036
+ return false;
1037
+ }
1038
+ return !!this.sceneMap.getBody(myId);
1039
+ }
1040
+ stepClientPhysicsTick() {
1041
+ if (!this.predictionEnabled || !this.sceneMap) {
1042
+ return;
1043
+ }
1044
+ const now = Date.now();
1045
+ if (this.lastClientPhysicsStepAt === 0) {
1046
+ this.lastClientPhysicsStepAt = now;
1047
+ }
1048
+ const deltaMs = Math.max(1, Math.min(100, now - this.lastClientPhysicsStepAt));
1049
+ this.lastClientPhysicsStepAt = now;
1050
+ this.sceneMap.stepClientPhysics(deltaMs);
1051
+ }
1052
+ flushPendingPredictedStates() {
1053
+ if (!this.predictionEnabled || !this.prediction || this.pendingPredictionFrames.length === 0) {
1054
+ return;
1055
+ }
1056
+ const state = this.getLocalPlayerState();
1057
+ while (this.pendingPredictionFrames.length > 0) {
1058
+ const frame = this.pendingPredictionFrames.shift();
1059
+ if (typeof frame === "number") {
1060
+ this.prediction.attachPredictedState(frame, state);
1061
+ }
1062
+ }
1063
+ }
1064
+ buildPendingMoveTrajectory() {
1065
+ if (!this.predictionEnabled || !this.prediction) {
1066
+ return [];
1067
+ }
1068
+ const pendingInputs = this.prediction.getPendingInputs();
1069
+ const trajectory = [];
1070
+ for (const entry of pendingInputs) {
1071
+ const state = entry.state;
1072
+ if (!state) continue;
1073
+ if (typeof state.x !== "number" || typeof state.y !== "number") continue;
1074
+ trajectory.push({
1075
+ frame: entry.frame,
1076
+ tick: entry.tick,
1077
+ timestamp: entry.timestamp,
1078
+ input: entry.direction,
1079
+ x: state.x,
1080
+ y: state.y,
1081
+ direction: state.direction ?? entry.direction
1082
+ });
1083
+ }
1084
+ if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {
1085
+ return trajectory.slice(-this.MAX_MOVE_TRAJECTORY_POINTS);
1086
+ }
1087
+ return trajectory;
1088
+ }
1089
+ emitMovePacket(input, frame, tick, timestamp, force = false) {
1090
+ const trajectory = this.buildPendingMoveTrajectory();
1091
+ const latestTrajectoryFrame = trajectory.length > 0 ? trajectory[trajectory.length - 1].frame : frame;
1092
+ const shouldThrottle = !force && latestTrajectoryFrame <= this.lastMovePathSentFrame && timestamp - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS;
1093
+ if (shouldThrottle) {
1094
+ return;
1095
+ }
1096
+ this.webSocket.emit("move", {
1097
+ input,
1098
+ timestamp,
1099
+ frame,
1100
+ tick,
1101
+ trajectory
1102
+ });
1103
+ this.lastMovePathSentAt = timestamp;
1104
+ this.lastMovePathSentFrame = Math.max(this.lastMovePathSentFrame, latestTrajectoryFrame, frame);
1105
+ }
1106
+ flushPendingMovePath() {
1107
+ if (!this.predictionEnabled || !this.prediction) {
1108
+ return;
1109
+ }
1110
+ const pendingInputs = this.prediction.getPendingInputs();
1111
+ if (pendingInputs.length === 0) {
1112
+ return;
1113
+ }
1114
+ const latest = pendingInputs[pendingInputs.length - 1];
1115
+ if (!latest) {
1116
+ return;
1117
+ }
1118
+ const now = Date.now();
1119
+ if (now - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS) {
1120
+ return;
1121
+ }
1122
+ this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);
1123
+ }
936
1124
  getLocalPlayerState() {
937
1125
  const currentPlayer = this.sceneMap?.getCurrentPlayer();
938
1126
  if (!currentPlayer) {
@@ -963,11 +1151,18 @@ class RpgClientEngine {
963
1151
  initializePredictionController() {
964
1152
  if (!this.predictionEnabled) {
965
1153
  this.prediction = void 0;
1154
+ this.sceneMap?.configureClientPrediction?.(false);
966
1155
  return;
967
1156
  }
1157
+ const configuredTtl = this.globalConfig?.prediction?.historyTtlMs;
1158
+ const historyTtlMs = typeof configuredTtl === "number" ? configuredTtl : 1e4;
1159
+ const configuredMaxEntries = this.globalConfig?.prediction?.maxHistoryEntries;
1160
+ const maxHistoryEntries = typeof configuredMaxEntries === "number" ? configuredMaxEntries : Math.max(600, Math.ceil(historyTtlMs / 16) + 120);
1161
+ this.sceneMap?.configureClientPrediction?.(true);
968
1162
  this.prediction = new PredictionController({
969
1163
  correctionThreshold: this.globalConfig?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,
970
- historyTtlMs: this.globalConfig?.prediction?.historyTtlMs ?? 2e3,
1164
+ historyTtlMs,
1165
+ maxHistoryEntries,
971
1166
  getPhysicsTick: () => this.getPhysicsTick(),
972
1167
  getCurrentState: () => this.getLocalPlayerState(),
973
1168
  setAuthoritativeState: (state) => this.applyAuthoritativeState(state)
@@ -1031,6 +1226,10 @@ class RpgClientEngine {
1031
1226
  this.initializePredictionController();
1032
1227
  this.frameOffset = 0;
1033
1228
  this.inputFrameCounter = 0;
1229
+ this.pendingPredictionFrames = [];
1230
+ this.lastClientPhysicsStepAt = 0;
1231
+ this.lastMovePathSentAt = 0;
1232
+ this.lastMovePathSentFrame = 0;
1034
1233
  }
1035
1234
  /**
1036
1235
  * Trigger a flash animation on a sprite
@@ -1089,11 +1288,14 @@ class RpgClientEngine {
1089
1288
  }
1090
1289
  applyServerAck(ack) {
1091
1290
  if (this.predictionEnabled && this.prediction) {
1092
- this.prediction.applyServerAck({
1291
+ const result = this.prediction.applyServerAck({
1093
1292
  frame: ack.frame,
1094
1293
  serverTick: ack.serverTick,
1095
1294
  state: typeof ack.x === "number" && typeof ack.y === "number" ? { x: ack.x, y: ack.y, direction: ack.direction } : void 0
1096
1295
  });
1296
+ if (result.state && result.needsReconciliation) {
1297
+ this.reconcilePrediction(result.state, result.pendingInputs);
1298
+ }
1097
1299
  return;
1098
1300
  }
1099
1301
  if (typeof ack.x !== "number" || typeof ack.y !== "number") {
@@ -1117,6 +1319,24 @@ class RpgClientEngine {
1117
1319
  player.changeDirection(ack.direction);
1118
1320
  }
1119
1321
  }
1322
+ reconcilePrediction(authoritativeState, pendingInputs) {
1323
+ const player = this.getCurrentPlayer();
1324
+ if (!player) {
1325
+ return;
1326
+ }
1327
+ this.sceneMap.stopMovement(player);
1328
+ this.applyAuthoritativeState(authoritativeState);
1329
+ if (!pendingInputs.length) {
1330
+ return;
1331
+ }
1332
+ const replayInputs = pendingInputs.slice(-600);
1333
+ for (const entry of replayInputs) {
1334
+ if (!entry?.direction) continue;
1335
+ this.sceneMap.moveBody(player, entry.direction);
1336
+ this.sceneMap.stepPredictionTick();
1337
+ this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());
1338
+ }
1339
+ }
1120
1340
  /**
1121
1341
  * Replay unacknowledged inputs from a given frame to resimulate client prediction
1122
1342
  * after applying server authority at a certain frame.
@@ -1248,6 +1468,8 @@ class RpgClientEngine {
1248
1468
  this.inputFrameCounter = 0;
1249
1469
  this.frameOffset = 0;
1250
1470
  this.rtt = 0;
1471
+ this.lastMovePathSentAt = 0;
1472
+ this.lastMovePathSentFrame = 0;
1251
1473
  this.mapLoadCompleted$.next(false);
1252
1474
  this.playerIdReceived$.next(false);
1253
1475
  this.playersReceived$.next(false);