@rpgjs/client 5.0.0-beta.10 → 5.0.0-beta.11

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 (118) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/Game/ProjectileManager.d.ts +89 -0
  3. package/dist/Game/ProjectileManager.js +179 -0
  4. package/dist/Game/ProjectileManager.js.map +1 -0
  5. package/dist/Game/ProjectileManager.spec.d.ts +1 -0
  6. package/dist/RpgClient.d.ts +53 -13
  7. package/dist/RpgClientEngine.d.ts +25 -4
  8. package/dist/RpgClientEngine.js +197 -48
  9. package/dist/RpgClientEngine.js.map +1 -1
  10. package/dist/components/animations/hit.ce.js.map +1 -1
  11. package/dist/components/character.ce.js +32 -30
  12. package/dist/components/character.ce.js.map +1 -1
  13. package/dist/components/dynamics/bar.ce.js +4 -3
  14. package/dist/components/dynamics/bar.ce.js.map +1 -1
  15. package/dist/components/dynamics/image.ce.js +2 -1
  16. package/dist/components/dynamics/image.ce.js.map +1 -1
  17. package/dist/components/dynamics/shape.ce.js +3 -2
  18. package/dist/components/dynamics/shape.ce.js.map +1 -1
  19. package/dist/components/dynamics/text.ce.js +9 -8
  20. package/dist/components/dynamics/text.ce.js.map +1 -1
  21. package/dist/components/gui/dialogbox/index.ce.js +3 -2
  22. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  23. package/dist/components/gui/gameover.ce.js +3 -2
  24. package/dist/components/gui/gameover.ce.js.map +1 -1
  25. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  26. package/dist/components/gui/menu/equip-menu.ce.js +2 -1
  27. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  28. package/dist/components/gui/menu/exit-menu.ce.js +2 -1
  29. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  30. package/dist/components/gui/menu/items-menu.ce.js +3 -2
  31. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  32. package/dist/components/gui/menu/main-menu.ce.js +3 -2
  33. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  34. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  35. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  36. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  37. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  38. package/dist/components/gui/save-load.ce.js +2 -1
  39. package/dist/components/gui/save-load.ce.js.map +1 -1
  40. package/dist/components/gui/shop/shop.ce.js +3 -2
  41. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  42. package/dist/components/gui/title-screen.ce.js +3 -2
  43. package/dist/components/gui/title-screen.ce.js.map +1 -1
  44. package/dist/components/index.d.ts +2 -1
  45. package/dist/components/index.js +1 -0
  46. package/dist/components/player-components.ce.js +11 -10
  47. package/dist/components/player-components.ce.js.map +1 -1
  48. package/dist/components/prebuilt/hp-bar.ce.js +4 -3
  49. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  50. package/dist/components/prebuilt/light-halo.ce.js +2 -1
  51. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  52. package/dist/components/scenes/canvas.ce.js +12 -4
  53. package/dist/components/scenes/canvas.ce.js.map +1 -1
  54. package/dist/components/scenes/draw-map.ce.js +6 -3
  55. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  56. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  57. package/dist/index.d.ts +3 -0
  58. package/dist/index.js +9 -5
  59. package/dist/module.js +11 -0
  60. package/dist/module.js.map +1 -1
  61. package/dist/services/actionInput.d.ts +12 -0
  62. package/dist/services/actionInput.js +27 -0
  63. package/dist/services/actionInput.js.map +1 -0
  64. package/dist/services/actionInput.spec.d.ts +1 -0
  65. package/dist/services/mmorpg-connection.d.ts +5 -0
  66. package/dist/services/mmorpg-connection.js +50 -0
  67. package/dist/services/mmorpg-connection.js.map +1 -0
  68. package/dist/services/mmorpg-connection.spec.d.ts +1 -0
  69. package/dist/services/mmorpg.d.ts +10 -4
  70. package/dist/services/mmorpg.js +48 -30
  71. package/dist/services/mmorpg.js.map +1 -1
  72. package/dist/services/pointerContext.d.ts +11 -0
  73. package/dist/services/pointerContext.js +48 -0
  74. package/dist/services/pointerContext.js.map +1 -0
  75. package/dist/services/pointerContext.spec.d.ts +1 -0
  76. package/dist/services/standalone-message.d.ts +1 -0
  77. package/dist/services/standalone-message.js +9 -0
  78. package/dist/services/standalone-message.js.map +1 -0
  79. package/dist/services/standalone.js +3 -2
  80. package/dist/services/standalone.js.map +1 -1
  81. package/dist/services/standalone.spec.d.ts +1 -0
  82. package/package.json +7 -7
  83. package/src/Game/ProjectileManager.spec.ts +338 -0
  84. package/src/Game/ProjectileManager.ts +324 -0
  85. package/src/RpgClient.ts +62 -15
  86. package/src/RpgClientEngine.ts +287 -65
  87. package/src/components/character.ce +34 -32
  88. package/src/components/dynamics/bar.ce +4 -3
  89. package/src/components/dynamics/image.ce +2 -1
  90. package/src/components/dynamics/shape.ce +3 -2
  91. package/src/components/dynamics/text.ce +9 -8
  92. package/src/components/gui/dialogbox/index.ce +3 -2
  93. package/src/components/gui/gameover.ce +2 -1
  94. package/src/components/gui/menu/equip-menu.ce +2 -1
  95. package/src/components/gui/menu/exit-menu.ce +2 -1
  96. package/src/components/gui/menu/items-menu.ce +3 -2
  97. package/src/components/gui/menu/main-menu.ce +2 -1
  98. package/src/components/gui/save-load.ce +2 -1
  99. package/src/components/gui/shop/shop.ce +3 -2
  100. package/src/components/gui/title-screen.ce +2 -1
  101. package/src/components/index.ts +2 -1
  102. package/src/components/player-components.ce +11 -10
  103. package/src/components/prebuilt/hp-bar.ce +4 -3
  104. package/src/components/prebuilt/light-halo.ce +2 -2
  105. package/src/components/scenes/canvas.ce +10 -2
  106. package/src/components/scenes/draw-map.ce +17 -3
  107. package/src/index.ts +3 -0
  108. package/src/module.ts +13 -0
  109. package/src/services/actionInput.spec.ts +101 -0
  110. package/src/services/actionInput.ts +53 -0
  111. package/src/services/mmorpg-connection.spec.ts +99 -0
  112. package/src/services/mmorpg-connection.ts +69 -0
  113. package/src/services/mmorpg.ts +60 -34
  114. package/src/services/pointerContext.spec.ts +36 -0
  115. package/src/services/pointerContext.ts +84 -0
  116. package/src/services/standalone-message.ts +7 -0
  117. package/src/services/standalone.spec.ts +34 -0
  118. package/src/services/standalone.ts +3 -2
@@ -1,9 +1,11 @@
1
1
  import { inject } from "./core/inject.js";
2
2
  import { WebSocketToken } from "./services/AbstractSocket.js";
3
+ import { normalizeActionInput } from "./services/actionInput.js";
3
4
  import { getCanMoveValue } from "./utils/readPropValue.js";
4
5
  import { SaveClientService } from "./services/save.js";
5
6
  import { RpgGui } from "./Gui/Gui.js";
6
7
  import __ce_component from "./components/scenes/canvas.ce.js";
8
+ import __ce_component$1 from "./components/scenes/draw-map.ce.js";
7
9
  import { LoadMapToken } from "./services/loadMap.js";
8
10
  import { RpgSound } from "./Sound.js";
9
11
  import { RpgResource } from "./Resource.js";
@@ -12,25 +14,29 @@ import { RpgClientMap } from "./Game/Map.js";
12
14
  import { AnimationManager } from "./Game/AnimationManager.js";
13
15
  import { GlobalConfigToken } from "./module.js";
14
16
  import { PrebuiltComponentAnimations } from "./components/animations/index.js";
15
- import __ce_component$1 from "./components/dynamics/text.ce.js";
16
- import __ce_component$2 from "./components/dynamics/bar.ce.js";
17
- import __ce_component$3 from "./components/dynamics/shape.ce.js";
18
- import __ce_component$4 from "./components/dynamics/image.ce.js";
17
+ import __ce_component$2 from "./components/dynamics/text.ce.js";
18
+ import __ce_component$3 from "./components/dynamics/bar.ce.js";
19
+ import __ce_component$4 from "./components/dynamics/shape.ce.js";
20
+ import __ce_component$5 from "./components/dynamics/image.ce.js";
19
21
  import { NotificationManager } from "./Gui/NotificationManager.js";
22
+ import { ProjectileManager } from "./Game/ProjectileManager.js";
23
+ import { createClientPointerContext } from "./services/pointerContext.js";
20
24
  import { Howl, bootstrapCanvas, signal, trigger } from "canvasengine";
21
- import { Direction, ModulesToken, PredictionController, normalizeLightingState } from "@rpgjs/common";
25
+ import { Direction, ModulesToken, PredictionController, Vector2, normalizeLightingState } from "@rpgjs/common";
22
26
  import { BehaviorSubject, combineLatest, filter, lastValueFrom, switchMap, take } from "rxjs";
23
27
  import * as PIXI from "pixi.js";
24
28
  //#region src/RpgClientEngine.ts
25
29
  var RpgClientEngine = class {
26
30
  constructor(context) {
27
31
  this.context = context;
32
+ this.sceneMapComponent = __ce_component$1;
28
33
  this.stopProcessingInput = false;
29
34
  this.width = signal("100%");
30
35
  this.height = signal("100%");
31
36
  this.spritesheets = /* @__PURE__ */ new Map();
32
37
  this.sounds = /* @__PURE__ */ new Map();
33
38
  this.componentAnimations = [];
39
+ this.pointer = createClientPointerContext();
34
40
  this.particleSettings = { emitters: [] };
35
41
  this.playerIdSignal = signal(null);
36
42
  this.spriteComponentsBehind = signal([]);
@@ -46,6 +52,7 @@ var RpgClientEngine = class {
46
52
  this.pendingPredictionFrames = [];
47
53
  this.lastClientPhysicsStepAt = 0;
48
54
  this.frameOffset = 0;
55
+ this.latestServerTickAt = 0;
49
56
  this.rtt = 0;
50
57
  this.pingInterval = null;
51
58
  this.PING_INTERVAL_MS = 5e3;
@@ -60,11 +67,13 @@ var RpgClientEngine = class {
60
67
  this.eventsReceived$ = new BehaviorSubject(false);
61
68
  this.sceneResetQueued = false;
62
69
  this.tickSubscriptions = [];
70
+ this.pendingSyncPackets = [];
63
71
  this.notificationManager = new NotificationManager();
64
72
  this.webSocket = inject(WebSocketToken);
65
73
  this.guiService = inject(RpgGui);
66
74
  this.loadMapService = inject(LoadMapToken);
67
75
  this.hooks = inject(ModulesToken);
76
+ this.projectiles = new ProjectileManager(this.hooks, (projectile) => this.predictProjectileImpact(projectile));
68
77
  this.globalConfig = inject(GlobalConfigToken);
69
78
  if (!this.globalConfig) this.globalConfig = {};
70
79
  if (!this.globalConfig.box) this.globalConfig.box = {
@@ -78,12 +87,12 @@ var RpgClientEngine = class {
78
87
  id: "animation",
79
88
  component: PrebuiltComponentAnimations.Animation
80
89
  });
81
- this.registerSpriteComponent("rpg:text", __ce_component$1);
82
- this.registerSpriteComponent("rpg:hpBar", __ce_component$2);
83
- this.registerSpriteComponent("rpg:spBar", __ce_component$2);
84
- this.registerSpriteComponent("rpg:bar", __ce_component$2);
85
- this.registerSpriteComponent("rpg:shape", __ce_component$3);
86
- this.registerSpriteComponent("rpg:image", __ce_component$4);
90
+ this.registerSpriteComponent("rpg:text", __ce_component$2);
91
+ this.registerSpriteComponent("rpg:hpBar", __ce_component$3);
92
+ this.registerSpriteComponent("rpg:spBar", __ce_component$3);
93
+ this.registerSpriteComponent("rpg:bar", __ce_component$3);
94
+ this.registerSpriteComponent("rpg:shape", __ce_component$4);
95
+ this.registerSpriteComponent("rpg:image", __ce_component$5);
87
96
  this.predictionEnabled = this.globalConfig?.prediction?.enabled !== false;
88
97
  this.initializePredictionController();
89
98
  }
@@ -132,13 +141,26 @@ var RpgClientEngine = class {
132
141
  this.sceneMap = new RpgClientMap();
133
142
  this.sceneMap.configureClientPrediction(this.predictionEnabled);
134
143
  this.sceneMap.loadPhysic();
144
+ this.resolveSceneMapComponent();
145
+ inject(SaveClientService).initialize();
146
+ this.initListeners();
147
+ this.guiService._initialize();
148
+ try {
149
+ await this.webSocket.connection();
150
+ } catch (error) {
151
+ this.stopPingPong();
152
+ await this.callConnectError(error);
153
+ throw error;
154
+ }
135
155
  this.selector = document.body.querySelector("#rpg");
136
156
  const bootstrapOptions = this.globalConfig?.bootstrapCanvasOptions;
137
157
  const { app, canvasElement } = await bootstrapCanvas(this.selector, __ce_component, bootstrapOptions);
138
158
  this.canvasApp = app;
139
159
  this.canvasElement = canvasElement;
140
160
  this.renderer = app.renderer;
161
+ this.setupPointerTracking();
141
162
  this.tick = canvasElement?.propObservables?.context["tick"].observable;
163
+ this.flushPendingSyncPackets();
142
164
  const inputCheckSubscription = this.tick.subscribe(() => {
143
165
  if (Date.now() - this.lastInputTime > 100) {
144
166
  const player = this.getCurrentPlayer();
@@ -156,6 +178,7 @@ var RpgClientEngine = class {
156
178
  this.hooks.callHooks("client-gui-load", this).subscribe();
157
179
  this.hooks.callHooks("client-particles-load", this).subscribe();
158
180
  this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
181
+ this.hooks.callHooks("client-projectiles-load", this).subscribe();
159
182
  this.hooks.callHooks("client-sprite-load", this).subscribe();
160
183
  await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
161
184
  this.resizeHandler = () => {
@@ -164,6 +187,7 @@ var RpgClientEngine = class {
164
187
  window.addEventListener("resize", this.resizeHandler);
165
188
  const tickSubscription = this.tick.subscribe((tick) => {
166
189
  this.stepClientPhysicsTick();
190
+ this.projectiles.step();
167
191
  this.flushPendingPredictedStates();
168
192
  this.flushPendingMovePath();
169
193
  this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
@@ -174,12 +198,46 @@ var RpgClientEngine = class {
174
198
  }
175
199
  });
176
200
  this.tickSubscriptions.push(tickSubscription);
177
- await this.webSocket.connection(() => {
178
- inject(SaveClientService).initialize();
179
- this.initListeners();
180
- this.guiService._initialize();
181
- this.startPingPong();
182
- });
201
+ this.startPingPong();
202
+ }
203
+ resolveSceneMapComponent() {
204
+ const components = this.hooks.getHookFunctions("client-sceneMap-component");
205
+ const component = components[components.length - 1];
206
+ if (component) this.sceneMapComponent = component;
207
+ }
208
+ setupPointerTracking() {
209
+ const renderer = this.renderer;
210
+ const canvas = renderer?.canvas ?? renderer?.view ?? this.canvasApp?.canvas;
211
+ if (!canvas || typeof canvas.addEventListener !== "function") return;
212
+ this.pointerCanvas = canvas;
213
+ this.pointerMoveHandler = (event) => {
214
+ const rect = canvas.getBoundingClientRect();
215
+ const screen = {
216
+ x: event.clientX - rect.left,
217
+ y: event.clientY - rect.top
218
+ };
219
+ const viewport = this.findViewportInstance();
220
+ let world = screen;
221
+ if (viewport && typeof viewport.toWorld === "function") {
222
+ const point = viewport.toWorld(screen.x, screen.y);
223
+ world = {
224
+ x: Number(point.x),
225
+ y: Number(point.y)
226
+ };
227
+ } else if (viewport && typeof viewport.toLocal === "function") {
228
+ const point = viewport.toLocal(screen);
229
+ world = {
230
+ x: Number(point.x),
231
+ y: Number(point.y)
232
+ };
233
+ }
234
+ this.pointer.update(screen, world);
235
+ };
236
+ canvas.addEventListener("pointermove", this.pointerMoveHandler);
237
+ canvas.addEventListener("pointerdown", this.pointerMoveHandler);
238
+ }
239
+ findViewportInstance() {
240
+ return (this.canvasApp?.stage?.children ?? []).find((child) => typeof child?.toWorld === "function" || child?.constructor?.name === "Viewport");
183
241
  }
184
242
  prepareSyncPayload(data) {
185
243
  const payload = { ...data ?? {} };
@@ -214,41 +272,27 @@ var RpgClientEngine = class {
214
272
  }
215
273
  initListeners() {
216
274
  this.webSocket.on("sync", (data) => {
217
- if (data.pId) {
218
- this.playerIdSignal.set(data.pId);
219
- this.playerIdReceived$.next(true);
220
- }
221
- if (this.sceneResetQueued) {
222
- this.sceneMap.reset();
223
- this.sceneMap.loadPhysic();
224
- this.sceneResetQueued = false;
225
- }
226
- this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
227
- const ack = data?.ack;
228
- const normalizedAck = ack && typeof ack.frame === "number" ? this.normalizeAckWithSyncState(ack, data) : void 0;
229
- const payload = this.prepareSyncPayload(data);
230
- load(this.sceneMap, payload, true);
231
- if (normalizedAck) this.applyServerAck(normalizedAck);
232
- for (const playerId in payload.players ?? {}) {
233
- const player = payload.players[playerId];
234
- if (!player._param) continue;
235
- for (const param in player._param) this.sceneMap.players()[playerId]._param()[param] = player._param[param];
275
+ if (!this.tick) {
276
+ this.pendingSyncPackets.push(data);
277
+ return;
236
278
  }
237
- const players = payload.players || this.sceneMap.players();
238
- if (players && Object.keys(players).length > 0) this.playersReceived$.next(true);
239
- if ((payload.events || this.sceneMap.events()) !== void 0) this.eventsReceived$.next(true);
279
+ this.applySyncPacket(data);
240
280
  });
241
281
  this.webSocket.on("pong", (data) => {
242
282
  const now = Date.now();
243
283
  this.rtt = now - data.clientTime;
244
284
  const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1e3 / 60));
245
285
  const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
286
+ this.updateServerTickEstimate(estimatedServerTickNow, now);
246
287
  if (this.inputFrameCounter > 0) this.frameOffset = estimatedServerTickNow - data.clientFrame;
247
288
  console.debug(`[Ping/Pong] RTT: ${this.rtt}ms, ServerTick: ${data.serverTick}, FrameOffset: ${this.frameOffset}`);
248
289
  });
249
290
  this.webSocket.on("changeMap", (data) => {
250
291
  this.sceneResetQueued = true;
292
+ this.sceneMap.weatherState.set(null);
293
+ this.sceneMap.lightingState.set(null);
251
294
  this.sceneMap.clearLightSpots();
295
+ this.projectiles.clear();
252
296
  this.cameraFollowTargetId.set(null);
253
297
  const transferToken = typeof data?.transferToken === "string" ? data.transferToken : void 0;
254
298
  this.loadScene(data.mapId, transferToken);
@@ -259,6 +303,21 @@ var RpgClientEngine = class {
259
303
  const player = object ? this.sceneMap.getObjectById(object) : void 0;
260
304
  this.getComponentAnimation(id).displayEffect(params, player || position);
261
305
  });
306
+ this.webSocket.on("projectile:spawnBatch", (data) => {
307
+ this.projectiles.spawnBatch(data?.projectiles ?? [], {
308
+ currentServerTick: this.estimateServerTick(),
309
+ tickDurationMs: this.getPhysicsTickDurationMs()
310
+ });
311
+ });
312
+ this.webSocket.on("projectile:impactBatch", (data) => {
313
+ this.projectiles.impactBatch(data?.impacts ?? []);
314
+ });
315
+ this.webSocket.on("projectile:destroyBatch", (data) => {
316
+ this.projectiles.destroyBatch(data?.projectiles ?? []);
317
+ });
318
+ this.webSocket.on("projectile:clear", () => {
319
+ this.projectiles.clear();
320
+ });
262
321
  this.webSocket.on("notification", (data) => {
263
322
  this.notificationManager.add(data);
264
323
  });
@@ -346,9 +405,46 @@ var RpgClientEngine = class {
346
405
  this.stopPingPong();
347
406
  });
348
407
  this.webSocket.on("error", (error) => {
349
- this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket).subscribe();
408
+ this.callConnectError(error);
350
409
  });
351
410
  }
411
+ async callConnectError(error) {
412
+ await lastValueFrom(this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket));
413
+ }
414
+ flushPendingSyncPackets() {
415
+ const packets = this.pendingSyncPackets;
416
+ this.pendingSyncPackets = [];
417
+ packets.forEach((packet) => this.applySyncPacket(packet));
418
+ }
419
+ applySyncPacket(data) {
420
+ if (data.pId) {
421
+ this.playerIdSignal.set(data.pId);
422
+ this.playerIdReceived$.next(true);
423
+ }
424
+ if (this.sceneResetQueued) {
425
+ const weatherState = this.sceneMap.weatherState();
426
+ const lightingState = this.sceneMap.lightingState();
427
+ this.sceneMap.reset();
428
+ this.sceneMap.weatherState.set(weatherState);
429
+ this.sceneMap.lightingState.set(lightingState);
430
+ this.sceneMap.loadPhysic();
431
+ this.sceneResetQueued = false;
432
+ }
433
+ this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
434
+ const ack = data?.ack;
435
+ const normalizedAck = ack && typeof ack.frame === "number" ? this.normalizeAckWithSyncState(ack, data) : void 0;
436
+ const payload = this.prepareSyncPayload(data);
437
+ load(this.sceneMap, payload, true);
438
+ if (normalizedAck) this.applyServerAck(normalizedAck);
439
+ for (const playerId in payload.players ?? {}) {
440
+ const player = payload.players[playerId];
441
+ if (!player._param) continue;
442
+ for (const param in player._param) this.sceneMap.players()[playerId]._param()[param] = player._param[param];
443
+ }
444
+ const players = payload.players || this.sceneMap.players();
445
+ if (players && Object.keys(players).length > 0) this.playersReceived$.next(true);
446
+ if ((payload.events || this.sceneMap.events()) !== void 0) this.eventsReceived$.next(true);
447
+ }
352
448
  /**
353
449
  * Start periodic ping/pong for client-server synchronization
354
450
  *
@@ -425,11 +521,17 @@ var RpgClientEngine = class {
425
521
  room: mapId,
426
522
  query: transferToken ? { transferToken } : void 0
427
523
  });
428
- await this.webSocket.reconnect(() => {
429
- inject(SaveClientService).initialize();
430
- this.initListeners();
431
- this.guiService._initialize();
432
- });
524
+ try {
525
+ await this.webSocket.reconnect(() => {
526
+ inject(SaveClientService).initialize();
527
+ this.initListeners();
528
+ this.guiService._initialize();
529
+ });
530
+ } catch (error) {
531
+ this.stopPingPong();
532
+ await this.callConnectError(error);
533
+ throw error;
534
+ }
433
535
  const res = await this.loadMapService.load(mapId);
434
536
  this.sceneMap.data.set(res);
435
537
  if (this.playerIdSignal()) this.playerIdReceived$.next(true);
@@ -858,6 +960,12 @@ var RpgClientEngine = class {
858
960
  getSpriteComponent(id) {
859
961
  return this.spriteComponents.get(id);
860
962
  }
963
+ registerProjectileComponent(type, component) {
964
+ return this.projectiles.register(type, component);
965
+ }
966
+ getProjectileComponent(type) {
967
+ return this.projectiles.get(type);
968
+ }
861
969
  /**
862
970
  * Add a component animation to the engine
863
971
  *
@@ -990,15 +1098,18 @@ var RpgClientEngine = class {
990
1098
  this.emitMovePacket(input, frame, tick, timestamp, true);
991
1099
  this.lastInputTime = Date.now();
992
1100
  }
993
- processAction({ action }) {
1101
+ processAction(action, data) {
994
1102
  if (this.stopProcessingInput) return;
995
1103
  const currentPlayer = this.sceneMap.getCurrentPlayer();
996
1104
  if (!(!currentPlayer || getCanMoveValue(currentPlayer))) return;
1105
+ const payload = normalizeActionInput(action, data);
997
1106
  this.hooks.callHooks("client-engine-onInput", this, {
998
- input: "action",
1107
+ input: payload.action,
1108
+ action: payload.action,
1109
+ data: payload.data,
999
1110
  playerId: this.playerId
1000
1111
  }).subscribe();
1001
- this.webSocket.emit("action", { action });
1112
+ this.webSocket.emit("action", payload);
1002
1113
  }
1003
1114
  get PIXI() {
1004
1115
  return PIXI;
@@ -1015,6 +1126,37 @@ var RpgClientEngine = class {
1015
1126
  getPhysicsTick() {
1016
1127
  return this.sceneMap?.getTick?.() ?? 0;
1017
1128
  }
1129
+ getPhysicsTickDurationMs() {
1130
+ const timeStep = this.sceneMap?.physic?.getWorld?.()?.getTimeStep?.();
1131
+ return typeof timeStep === "number" && Number.isFinite(timeStep) && timeStep > 0 ? timeStep * 1e3 : 1e3 / 60;
1132
+ }
1133
+ updateServerTickEstimate(serverTick, now = Date.now()) {
1134
+ if (typeof serverTick !== "number" || !Number.isFinite(serverTick)) return;
1135
+ this.latestServerTick = serverTick;
1136
+ this.latestServerTickAt = now;
1137
+ }
1138
+ estimateServerTick(now = Date.now()) {
1139
+ if (typeof this.latestServerTick !== "number" || this.latestServerTickAt <= 0) return;
1140
+ const elapsedTicks = Math.max(0, (now - this.latestServerTickAt) / this.getPhysicsTickDurationMs());
1141
+ return this.latestServerTick + elapsedTicks;
1142
+ }
1143
+ predictProjectileImpact(projectile) {
1144
+ if (projectile.predictImpact === false) return null;
1145
+ const sceneMap = this.sceneMap;
1146
+ if (!sceneMap?.physic || !Number.isFinite(projectile.range) || projectile.range <= 0) return null;
1147
+ const origin = projectile.origin;
1148
+ const direction = projectile.direction;
1149
+ if (!origin || !direction || !Number.isFinite(origin.x) || !Number.isFinite(origin.y) || !Number.isFinite(direction.x) || !Number.isFinite(direction.y) || direction.x === 0 && direction.y === 0) return null;
1150
+ const hit = sceneMap.physic.raycast(new Vector2(origin.x, origin.y), new Vector2(direction.x, direction.y), projectile.range, projectile.collisionMask, (entity) => projectile.ignoreOwner === false || !projectile.ownerId || entity.uuid !== projectile.ownerId);
1151
+ if (!hit) return null;
1152
+ return {
1153
+ id: projectile.id,
1154
+ targetId: hit.entity.uuid,
1155
+ x: hit.point.x,
1156
+ y: hit.point.y,
1157
+ distance: hit.distance
1158
+ };
1159
+ }
1018
1160
  ensureCurrentPlayerBody() {
1019
1161
  const player = this.sceneMap?.getCurrentPlayer();
1020
1162
  const myId = this.playerIdSignal();
@@ -1281,6 +1423,7 @@ var RpgClientEngine = class {
1281
1423
  if (sprite && typeof sprite.flash === "function") sprite.flash(options);
1282
1424
  }
1283
1425
  applyServerAck(ack) {
1426
+ this.updateServerTickEstimate(ack.serverTick);
1284
1427
  if (this.predictionEnabled && this.prediction) {
1285
1428
  const result = this.prediction.applyServerAck({
1286
1429
  frame: ack.frame,
@@ -1385,6 +1528,12 @@ var RpgClientEngine = class {
1385
1528
  window.removeEventListener("resize", this.resizeHandler);
1386
1529
  this.resizeHandler = void 0;
1387
1530
  }
1531
+ if (this.pointerMoveHandler && this.pointerCanvas) {
1532
+ this.pointerCanvas.removeEventListener("pointermove", this.pointerMoveHandler);
1533
+ this.pointerCanvas.removeEventListener("pointerdown", this.pointerMoveHandler);
1534
+ this.pointerMoveHandler = void 0;
1535
+ this.pointerCanvas = void 0;
1536
+ }
1388
1537
  const rendererStillExists = this.renderer && typeof this.renderer.destroy === "function";
1389
1538
  if (this.canvasApp && typeof this.canvasApp.destroy === "function") {
1390
1539
  try {