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

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 (154) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/Game/AnimationManager.d.ts +1 -0
  3. package/dist/Game/AnimationManager.js +3 -0
  4. package/dist/Game/AnimationManager.js.map +1 -1
  5. package/dist/Game/ClientVisuals.d.ts +61 -0
  6. package/dist/Game/ClientVisuals.js +96 -0
  7. package/dist/Game/ClientVisuals.js.map +1 -0
  8. package/dist/Game/ClientVisuals.spec.d.ts +1 -0
  9. package/dist/Game/EventComponentResolver.d.ts +16 -0
  10. package/dist/Game/EventComponentResolver.js +52 -0
  11. package/dist/Game/EventComponentResolver.js.map +1 -0
  12. package/dist/Game/EventComponentResolver.spec.d.ts +1 -0
  13. package/dist/Game/Map.js +9 -0
  14. package/dist/Game/Map.js.map +1 -1
  15. package/dist/Game/Object.js +2 -2
  16. package/dist/Game/Object.js.map +1 -1
  17. package/dist/Game/Object.spec.d.ts +1 -0
  18. package/dist/Game/ProjectileManager.d.ts +98 -0
  19. package/dist/Game/ProjectileManager.js +196 -0
  20. package/dist/Game/ProjectileManager.js.map +1 -0
  21. package/dist/Game/ProjectileManager.spec.d.ts +1 -0
  22. package/dist/RpgClient.d.ts +117 -13
  23. package/dist/RpgClientEngine.d.ts +82 -4
  24. package/dist/RpgClientEngine.js +296 -51
  25. package/dist/RpgClientEngine.js.map +1 -1
  26. package/dist/components/animations/fx.ce.js +58 -0
  27. package/dist/components/animations/fx.ce.js.map +1 -0
  28. package/dist/components/animations/hit.ce.js.map +1 -1
  29. package/dist/components/animations/index.d.ts +1 -0
  30. package/dist/components/animations/index.js +3 -1
  31. package/dist/components/animations/index.js.map +1 -1
  32. package/dist/components/character.ce.js +140 -40
  33. package/dist/components/character.ce.js.map +1 -1
  34. package/dist/components/dynamics/bar.ce.js +4 -3
  35. package/dist/components/dynamics/bar.ce.js.map +1 -1
  36. package/dist/components/dynamics/image.ce.js +2 -1
  37. package/dist/components/dynamics/image.ce.js.map +1 -1
  38. package/dist/components/dynamics/shape.ce.js +3 -2
  39. package/dist/components/dynamics/shape.ce.js.map +1 -1
  40. package/dist/components/dynamics/text.ce.js +9 -8
  41. package/dist/components/dynamics/text.ce.js.map +1 -1
  42. package/dist/components/gui/dialogbox/index.ce.js +3 -2
  43. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  44. package/dist/components/gui/gameover.ce.js +3 -2
  45. package/dist/components/gui/gameover.ce.js.map +1 -1
  46. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  47. package/dist/components/gui/menu/equip-menu.ce.js +2 -1
  48. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  49. package/dist/components/gui/menu/exit-menu.ce.js +2 -1
  50. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  51. package/dist/components/gui/menu/items-menu.ce.js +3 -2
  52. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  53. package/dist/components/gui/menu/main-menu.ce.js +3 -2
  54. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  55. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  56. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  57. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  58. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  59. package/dist/components/gui/save-load.ce.js +2 -1
  60. package/dist/components/gui/save-load.ce.js.map +1 -1
  61. package/dist/components/gui/shop/shop.ce.js +3 -2
  62. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  63. package/dist/components/gui/title-screen.ce.js +3 -2
  64. package/dist/components/gui/title-screen.ce.js.map +1 -1
  65. package/dist/components/index.d.ts +2 -1
  66. package/dist/components/index.js +1 -0
  67. package/dist/components/player-components.ce.js +11 -10
  68. package/dist/components/player-components.ce.js.map +1 -1
  69. package/dist/components/prebuilt/hp-bar.ce.js +4 -3
  70. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  71. package/dist/components/prebuilt/light-halo.ce.js +2 -1
  72. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  73. package/dist/components/scenes/canvas.ce.js +12 -4
  74. package/dist/components/scenes/canvas.ce.js.map +1 -1
  75. package/dist/components/scenes/draw-map.ce.js +6 -3
  76. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  77. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  78. package/dist/index.d.ts +4 -0
  79. package/dist/index.js +10 -5
  80. package/dist/module.js +18 -0
  81. package/dist/module.js.map +1 -1
  82. package/dist/services/actionInput.d.ts +14 -0
  83. package/dist/services/actionInput.js +59 -0
  84. package/dist/services/actionInput.js.map +1 -0
  85. package/dist/services/actionInput.spec.d.ts +1 -0
  86. package/dist/services/mmorpg-connection.d.ts +5 -0
  87. package/dist/services/mmorpg-connection.js +50 -0
  88. package/dist/services/mmorpg-connection.js.map +1 -0
  89. package/dist/services/mmorpg-connection.spec.d.ts +1 -0
  90. package/dist/services/mmorpg.d.ts +10 -4
  91. package/dist/services/mmorpg.js +48 -30
  92. package/dist/services/mmorpg.js.map +1 -1
  93. package/dist/services/pointerContext.d.ts +11 -0
  94. package/dist/services/pointerContext.js +48 -0
  95. package/dist/services/pointerContext.js.map +1 -0
  96. package/dist/services/pointerContext.spec.d.ts +1 -0
  97. package/dist/services/standalone-message.d.ts +1 -0
  98. package/dist/services/standalone-message.js +9 -0
  99. package/dist/services/standalone-message.js.map +1 -0
  100. package/dist/services/standalone.d.ts +3 -1
  101. package/dist/services/standalone.js +34 -15
  102. package/dist/services/standalone.js.map +1 -1
  103. package/dist/services/standalone.spec.d.ts +1 -0
  104. package/dist/utils/mapId.d.ts +1 -0
  105. package/dist/utils/mapId.js +6 -0
  106. package/dist/utils/mapId.js.map +1 -0
  107. package/package.json +7 -7
  108. package/src/Game/AnimationManager.ts +4 -0
  109. package/src/Game/ClientVisuals.spec.ts +56 -0
  110. package/src/Game/ClientVisuals.ts +184 -0
  111. package/src/Game/EventComponentResolver.spec.ts +84 -0
  112. package/src/Game/EventComponentResolver.ts +74 -0
  113. package/src/Game/Map.ts +10 -0
  114. package/src/Game/Object.spec.ts +46 -0
  115. package/src/Game/Object.ts +2 -2
  116. package/src/Game/ProjectileManager.spec.ts +449 -0
  117. package/src/Game/ProjectileManager.ts +346 -0
  118. package/src/RpgClient.ts +130 -15
  119. package/src/RpgClientEngine.ts +405 -69
  120. package/src/components/animations/fx.ce +101 -0
  121. package/src/components/animations/index.ts +4 -2
  122. package/src/components/character.ce +185 -40
  123. package/src/components/dynamics/bar.ce +4 -3
  124. package/src/components/dynamics/image.ce +2 -1
  125. package/src/components/dynamics/shape.ce +3 -2
  126. package/src/components/dynamics/text.ce +9 -8
  127. package/src/components/gui/dialogbox/index.ce +3 -2
  128. package/src/components/gui/gameover.ce +2 -1
  129. package/src/components/gui/menu/equip-menu.ce +2 -1
  130. package/src/components/gui/menu/exit-menu.ce +2 -1
  131. package/src/components/gui/menu/items-menu.ce +3 -2
  132. package/src/components/gui/menu/main-menu.ce +2 -1
  133. package/src/components/gui/save-load.ce +2 -1
  134. package/src/components/gui/shop/shop.ce +3 -2
  135. package/src/components/gui/title-screen.ce +2 -1
  136. package/src/components/index.ts +2 -1
  137. package/src/components/player-components.ce +11 -10
  138. package/src/components/prebuilt/hp-bar.ce +4 -3
  139. package/src/components/prebuilt/light-halo.ce +2 -2
  140. package/src/components/scenes/canvas.ce +10 -2
  141. package/src/components/scenes/draw-map.ce +17 -3
  142. package/src/index.ts +4 -0
  143. package/src/module.ts +24 -0
  144. package/src/services/actionInput.spec.ts +155 -0
  145. package/src/services/actionInput.ts +120 -0
  146. package/src/services/mmorpg-connection.spec.ts +99 -0
  147. package/src/services/mmorpg-connection.ts +69 -0
  148. package/src/services/mmorpg.ts +60 -34
  149. package/src/services/pointerContext.spec.ts +36 -0
  150. package/src/services/pointerContext.ts +84 -0
  151. package/src/services/standalone-message.ts +7 -0
  152. package/src/services/standalone.spec.ts +34 -0
  153. package/src/services/standalone.ts +42 -12
  154. package/src/utils/mapId.ts +2 -0
@@ -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,30 +14,39 @@ 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 { normalizeRoomMapId } from "./utils/mapId.js";
23
+ import { ProjectileManager } from "./Game/ProjectileManager.js";
24
+ import { ClientVisualRegistry } from "./Game/ClientVisuals.js";
25
+ import { createClientPointerContext } from "./services/pointerContext.js";
26
+ import { EventComponentResolverRegistry } from "./Game/EventComponentResolver.js";
20
27
  import { Howl, bootstrapCanvas, signal, trigger } from "canvasengine";
21
- import { Direction, ModulesToken, PredictionController, normalizeLightingState } from "@rpgjs/common";
28
+ import { Direction, ModulesToken, PredictionController, Vector2, normalizeLightingState } from "@rpgjs/common";
22
29
  import { BehaviorSubject, combineLatest, filter, lastValueFrom, switchMap, take } from "rxjs";
23
30
  import * as PIXI from "pixi.js";
24
31
  //#region src/RpgClientEngine.ts
25
32
  var RpgClientEngine = class {
26
33
  constructor(context) {
27
34
  this.context = context;
35
+ this.sceneMapComponent = __ce_component$1;
28
36
  this.stopProcessingInput = false;
29
37
  this.width = signal("100%");
30
38
  this.height = signal("100%");
31
39
  this.spritesheets = /* @__PURE__ */ new Map();
32
40
  this.sounds = /* @__PURE__ */ new Map();
33
41
  this.componentAnimations = [];
42
+ this.clientVisuals = new ClientVisualRegistry();
43
+ this.pointer = createClientPointerContext();
34
44
  this.particleSettings = { emitters: [] };
35
45
  this.playerIdSignal = signal(null);
36
46
  this.spriteComponentsBehind = signal([]);
37
47
  this.spriteComponentsInFront = signal([]);
38
48
  this.spriteComponents = /* @__PURE__ */ new Map();
49
+ this.eventComponentResolvers = new EventComponentResolverRegistry();
39
50
  this.cameraFollowTargetId = signal(null);
40
51
  this.mapShakeTrigger = trigger();
41
52
  this.controlsReady = signal(void 0);
@@ -46,6 +57,7 @@ var RpgClientEngine = class {
46
57
  this.pendingPredictionFrames = [];
47
58
  this.lastClientPhysicsStepAt = 0;
48
59
  this.frameOffset = 0;
60
+ this.latestServerTickAt = 0;
49
61
  this.rtt = 0;
50
62
  this.pingInterval = null;
51
63
  this.PING_INTERVAL_MS = 5e3;
@@ -59,12 +71,16 @@ var RpgClientEngine = class {
59
71
  this.playersReceived$ = new BehaviorSubject(false);
60
72
  this.eventsReceived$ = new BehaviorSubject(false);
61
73
  this.sceneResetQueued = false;
74
+ this.mapTransitionInProgress = false;
75
+ this.socketListenersInitialized = false;
62
76
  this.tickSubscriptions = [];
77
+ this.pendingSyncPackets = [];
63
78
  this.notificationManager = new NotificationManager();
64
79
  this.webSocket = inject(WebSocketToken);
65
80
  this.guiService = inject(RpgGui);
66
81
  this.loadMapService = inject(LoadMapToken);
67
82
  this.hooks = inject(ModulesToken);
83
+ this.projectiles = new ProjectileManager(this.hooks, (projectile) => this.predictProjectileImpact(projectile));
68
84
  this.globalConfig = inject(GlobalConfigToken);
69
85
  if (!this.globalConfig) this.globalConfig = {};
70
86
  if (!this.globalConfig.box) this.globalConfig.box = {
@@ -78,12 +94,12 @@ var RpgClientEngine = class {
78
94
  id: "animation",
79
95
  component: PrebuiltComponentAnimations.Animation
80
96
  });
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);
97
+ this.registerSpriteComponent("rpg:text", __ce_component$2);
98
+ this.registerSpriteComponent("rpg:hpBar", __ce_component$3);
99
+ this.registerSpriteComponent("rpg:spBar", __ce_component$3);
100
+ this.registerSpriteComponent("rpg:bar", __ce_component$3);
101
+ this.registerSpriteComponent("rpg:shape", __ce_component$4);
102
+ this.registerSpriteComponent("rpg:image", __ce_component$5);
87
103
  this.predictionEnabled = this.globalConfig?.prediction?.enabled !== false;
88
104
  this.initializePredictionController();
89
105
  }
@@ -132,13 +148,26 @@ var RpgClientEngine = class {
132
148
  this.sceneMap = new RpgClientMap();
133
149
  this.sceneMap.configureClientPrediction(this.predictionEnabled);
134
150
  this.sceneMap.loadPhysic();
151
+ this.resolveSceneMapComponent();
152
+ inject(SaveClientService).initialize();
153
+ this.initListeners();
154
+ this.guiService._initialize();
155
+ try {
156
+ await this.webSocket.connection();
157
+ } catch (error) {
158
+ this.stopPingPong();
159
+ await this.callConnectError(error);
160
+ throw error;
161
+ }
135
162
  this.selector = document.body.querySelector("#rpg");
136
163
  const bootstrapOptions = this.globalConfig?.bootstrapCanvasOptions;
137
164
  const { app, canvasElement } = await bootstrapCanvas(this.selector, __ce_component, bootstrapOptions);
138
165
  this.canvasApp = app;
139
166
  this.canvasElement = canvasElement;
140
167
  this.renderer = app.renderer;
168
+ this.setupPointerTracking();
141
169
  this.tick = canvasElement?.propObservables?.context["tick"].observable;
170
+ this.flushPendingSyncPackets();
142
171
  const inputCheckSubscription = this.tick.subscribe(() => {
143
172
  if (Date.now() - this.lastInputTime > 100) {
144
173
  const player = this.getCurrentPlayer();
@@ -156,6 +185,8 @@ var RpgClientEngine = class {
156
185
  this.hooks.callHooks("client-gui-load", this).subscribe();
157
186
  this.hooks.callHooks("client-particles-load", this).subscribe();
158
187
  this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
188
+ this.hooks.callHooks("client-clientVisuals-load", this).subscribe();
189
+ this.hooks.callHooks("client-projectiles-load", this).subscribe();
159
190
  this.hooks.callHooks("client-sprite-load", this).subscribe();
160
191
  await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
161
192
  this.resizeHandler = () => {
@@ -164,6 +195,7 @@ var RpgClientEngine = class {
164
195
  window.addEventListener("resize", this.resizeHandler);
165
196
  const tickSubscription = this.tick.subscribe((tick) => {
166
197
  this.stepClientPhysicsTick();
198
+ this.projectiles.step();
167
199
  this.flushPendingPredictedStates();
168
200
  this.flushPendingMovePath();
169
201
  this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
@@ -174,12 +206,46 @@ var RpgClientEngine = class {
174
206
  }
175
207
  });
176
208
  this.tickSubscriptions.push(tickSubscription);
177
- await this.webSocket.connection(() => {
178
- inject(SaveClientService).initialize();
179
- this.initListeners();
180
- this.guiService._initialize();
181
- this.startPingPong();
182
- });
209
+ this.startPingPong();
210
+ }
211
+ resolveSceneMapComponent() {
212
+ const components = this.hooks.getHookFunctions("client-sceneMap-component");
213
+ const component = components[components.length - 1];
214
+ if (component) this.sceneMapComponent = component;
215
+ }
216
+ setupPointerTracking() {
217
+ const renderer = this.renderer;
218
+ const canvas = renderer?.canvas ?? renderer?.view ?? this.canvasApp?.canvas;
219
+ if (!canvas || typeof canvas.addEventListener !== "function") return;
220
+ this.pointerCanvas = canvas;
221
+ this.pointerMoveHandler = (event) => {
222
+ const rect = canvas.getBoundingClientRect();
223
+ const screen = {
224
+ x: event.clientX - rect.left,
225
+ y: event.clientY - rect.top
226
+ };
227
+ const viewport = this.findViewportInstance();
228
+ let world = screen;
229
+ if (viewport && typeof viewport.toWorld === "function") {
230
+ const point = viewport.toWorld(screen.x, screen.y);
231
+ world = {
232
+ x: Number(point.x),
233
+ y: Number(point.y)
234
+ };
235
+ } else if (viewport && typeof viewport.toLocal === "function") {
236
+ const point = viewport.toLocal(screen);
237
+ world = {
238
+ x: Number(point.x),
239
+ y: Number(point.y)
240
+ };
241
+ }
242
+ this.pointer.update(screen, world);
243
+ };
244
+ canvas.addEventListener("pointermove", this.pointerMoveHandler);
245
+ canvas.addEventListener("pointerdown", this.pointerMoveHandler);
246
+ }
247
+ findViewportInstance() {
248
+ return (this.canvasApp?.stage?.children ?? []).find((child) => typeof child?.toWorld === "function" || child?.constructor?.name === "Viewport");
183
249
  }
184
250
  prepareSyncPayload(data) {
185
251
  const payload = { ...data ?? {} };
@@ -213,43 +279,27 @@ var RpgClientEngine = class {
213
279
  };
214
280
  }
215
281
  initListeners() {
282
+ if (this.socketListenersInitialized) return;
283
+ this.socketListenersInitialized = true;
216
284
  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];
285
+ if (!this.tick) {
286
+ this.pendingSyncPackets.push(data);
287
+ return;
236
288
  }
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);
289
+ this.applySyncPacket(data);
240
290
  });
241
291
  this.webSocket.on("pong", (data) => {
242
292
  const now = Date.now();
243
293
  this.rtt = now - data.clientTime;
244
294
  const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1e3 / 60));
245
295
  const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
296
+ this.updateServerTickEstimate(estimatedServerTickNow, now);
246
297
  if (this.inputFrameCounter > 0) this.frameOffset = estimatedServerTickNow - data.clientFrame;
247
298
  console.debug(`[Ping/Pong] RTT: ${this.rtt}ms, ServerTick: ${data.serverTick}, FrameOffset: ${this.frameOffset}`);
248
299
  });
249
300
  this.webSocket.on("changeMap", (data) => {
250
- this.sceneResetQueued = true;
251
- this.sceneMap.clearLightSpots();
252
- this.cameraFollowTargetId.set(null);
301
+ const nextMapId = typeof data?.mapId === "string" ? data.mapId : void 0;
302
+ this.beginMapTransfer(nextMapId);
253
303
  const transferToken = typeof data?.transferToken === "string" ? data.transferToken : void 0;
254
304
  this.loadScene(data.mapId, transferToken);
255
305
  });
@@ -259,6 +309,29 @@ var RpgClientEngine = class {
259
309
  const player = object ? this.sceneMap.getObjectById(object) : void 0;
260
310
  this.getComponentAnimation(id).displayEffect(params, player || position);
261
311
  });
312
+ this.webSocket.on("clientVisual", (data) => {
313
+ this.playClientVisual(data);
314
+ });
315
+ this.webSocket.on("projectile:spawnBatch", (data) => {
316
+ if (!this.shouldProcessProjectilePacket(data)) return;
317
+ this.projectiles.spawnBatch(data?.projectiles ?? [], {
318
+ mapId: data?.mapId,
319
+ currentServerTick: this.estimateServerTick(),
320
+ tickDurationMs: this.getPhysicsTickDurationMs()
321
+ });
322
+ });
323
+ this.webSocket.on("projectile:impactBatch", (data) => {
324
+ if (!this.shouldProcessProjectilePacket(data)) return;
325
+ this.projectiles.impactBatch(data?.impacts ?? [], { mapId: data?.mapId });
326
+ });
327
+ this.webSocket.on("projectile:destroyBatch", (data) => {
328
+ if (!this.shouldProcessProjectilePacket(data)) return;
329
+ this.projectiles.destroyBatch(data?.projectiles ?? [], { mapId: data?.mapId });
330
+ });
331
+ this.webSocket.on("projectile:clear", (data) => {
332
+ if (!this.shouldProcessProjectilePacket(data)) return;
333
+ this.projectiles.clear();
334
+ });
262
335
  this.webSocket.on("notification", (data) => {
263
336
  this.notificationManager.add(data);
264
337
  });
@@ -346,9 +419,71 @@ var RpgClientEngine = class {
346
419
  this.stopPingPong();
347
420
  });
348
421
  this.webSocket.on("error", (error) => {
349
- this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket).subscribe();
422
+ this.callConnectError(error);
423
+ });
424
+ }
425
+ beginMapTransfer(nextMapId) {
426
+ this.mapTransitionInProgress = true;
427
+ this.currentMapRoomId = nextMapId;
428
+ this.sceneResetQueued = false;
429
+ this.clearClientPredictionStates();
430
+ this.sceneMap.weatherState.set(null);
431
+ this.sceneMap.lightingState.set(null);
432
+ this.sceneMap.clearLightSpots();
433
+ this.clearComponentAnimations();
434
+ this.projectiles.setMapId(nextMapId);
435
+ this.cameraFollowTargetId.set(null);
436
+ this.sceneMap.reset();
437
+ this.sceneMap.loadPhysic();
438
+ }
439
+ clearComponentAnimations() {
440
+ this.componentAnimations.forEach((componentAnimation) => {
441
+ componentAnimation.instance?.clear?.();
350
442
  });
351
443
  }
444
+ shouldProcessProjectilePacket(data) {
445
+ if (this.mapTransitionInProgress) return false;
446
+ const packetMapId = normalizeRoomMapId(typeof data?.mapId === "string" ? data.mapId : void 0);
447
+ const currentMapId = normalizeRoomMapId(this.currentMapRoomId);
448
+ return !packetMapId || !currentMapId || packetMapId === currentMapId;
449
+ }
450
+ async callConnectError(error) {
451
+ await lastValueFrom(this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket));
452
+ }
453
+ flushPendingSyncPackets() {
454
+ const packets = this.pendingSyncPackets;
455
+ this.pendingSyncPackets = [];
456
+ packets.forEach((packet) => this.applySyncPacket(packet));
457
+ }
458
+ applySyncPacket(data) {
459
+ if (data.pId) {
460
+ this.playerIdSignal.set(data.pId);
461
+ this.playerIdReceived$.next(true);
462
+ }
463
+ if (this.sceneResetQueued) {
464
+ const weatherState = this.sceneMap.weatherState();
465
+ const lightingState = this.sceneMap.lightingState();
466
+ this.sceneMap.reset();
467
+ this.sceneMap.weatherState.set(weatherState);
468
+ this.sceneMap.lightingState.set(lightingState);
469
+ this.sceneMap.loadPhysic();
470
+ this.sceneResetQueued = false;
471
+ }
472
+ this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
473
+ const ack = data?.ack;
474
+ const normalizedAck = ack && typeof ack.frame === "number" ? this.normalizeAckWithSyncState(ack, data) : void 0;
475
+ const payload = this.prepareSyncPayload(data);
476
+ load(this.sceneMap, payload, true);
477
+ if (normalizedAck) this.applyServerAck(normalizedAck);
478
+ for (const playerId in payload.players ?? {}) {
479
+ const player = payload.players[playerId];
480
+ if (!player._param) continue;
481
+ for (const param in player._param) this.sceneMap.players()[playerId]._param()[param] = player._param[param];
482
+ }
483
+ const players = payload.players || this.sceneMap.players();
484
+ if (players && Object.keys(players).length > 0) this.playersReceived$.next(true);
485
+ if ((payload.events || this.sceneMap.events()) !== void 0) this.eventsReceived$.next(true);
486
+ }
352
487
  /**
353
488
  * Start periodic ping/pong for client-server synchronization
354
489
  *
@@ -425,11 +560,14 @@ var RpgClientEngine = class {
425
560
  room: mapId,
426
561
  query: transferToken ? { transferToken } : void 0
427
562
  });
428
- await this.webSocket.reconnect(() => {
429
- inject(SaveClientService).initialize();
430
- this.initListeners();
431
- this.guiService._initialize();
432
- });
563
+ try {
564
+ await this.webSocket.reconnect();
565
+ } catch (error) {
566
+ this.mapTransitionInProgress = false;
567
+ this.stopPingPong();
568
+ await this.callConnectError(error);
569
+ throw error;
570
+ }
433
571
  const res = await this.loadMapService.load(mapId);
434
572
  this.sceneMap.data.set(res);
435
573
  if (this.playerIdSignal()) this.playerIdReceived$.next(true);
@@ -437,6 +575,8 @@ var RpgClientEngine = class {
437
575
  if (players && Object.keys(players).length > 0) this.playersReceived$.next(true);
438
576
  if (this.sceneMap.events() !== void 0) this.eventsReceived$.next(true);
439
577
  this.mapLoadCompleted$.next(true);
578
+ this.currentMapRoomId = mapId;
579
+ this.mapTransitionInProgress = false;
440
580
  this.sceneMap.configureClientPrediction(this.predictionEnabled);
441
581
  this.sceneMap.loadPhysic();
442
582
  }
@@ -859,6 +999,66 @@ var RpgClientEngine = class {
859
999
  return this.spriteComponents.get(id);
860
1000
  }
861
1001
  /**
1002
+ * Register a custom event component resolver.
1003
+ *
1004
+ * The last resolver returning a component wins. This lets later modules
1005
+ * override earlier defaults without replacing the whole map scene.
1006
+ *
1007
+ * @param resolver - Function receiving the synced event object
1008
+ * @returns The registered resolver
1009
+ */
1010
+ addEventComponentResolver(resolver) {
1011
+ return this.eventComponentResolvers.add(resolver);
1012
+ }
1013
+ /**
1014
+ * Resolve the custom CanvasEngine component for an event, if any.
1015
+ *
1016
+ * @param event - Synced client event object
1017
+ * @returns The component/config returned by the last matching resolver
1018
+ */
1019
+ resolveEventComponent(event) {
1020
+ return this.eventComponentResolvers.resolve(event);
1021
+ }
1022
+ registerProjectileComponent(type, component) {
1023
+ return this.projectiles.register(type, component);
1024
+ }
1025
+ getProjectileComponent(type) {
1026
+ return this.projectiles.get(type);
1027
+ }
1028
+ /**
1029
+ * Register a named client visual macro.
1030
+ *
1031
+ * Client visuals are small client-side functions that group existing visual
1032
+ * primitives such as flash, sound, component animations, sprite animation, or
1033
+ * map shake. The server sends only the visual name and a serializable payload.
1034
+ *
1035
+ * @param name - Stable visual name sent by the server
1036
+ * @param handler - Client-side visual handler
1037
+ * @returns The registered handler
1038
+ */
1039
+ registerClientVisual(name, handler) {
1040
+ return this.clientVisuals.register(name, handler);
1041
+ }
1042
+ /**
1043
+ * Register several named client visual macros.
1044
+ *
1045
+ * @param visuals - Map of visual names to client-side handlers
1046
+ */
1047
+ registerClientVisuals(visuals) {
1048
+ this.clientVisuals.registerMany(visuals);
1049
+ }
1050
+ /**
1051
+ * Play a registered client visual locally.
1052
+ *
1053
+ * This is also used by the websocket listener when the server calls
1054
+ * `player.clientVisual()` or `map.clientVisual()`.
1055
+ *
1056
+ * @param packet - Visual name and serializable payload
1057
+ */
1058
+ playClientVisual(packet) {
1059
+ return this.clientVisuals.play(packet, this);
1060
+ }
1061
+ /**
862
1062
  * Add a component animation to the engine
863
1063
  *
864
1064
  * Component animations are temporary visual effects that can be displayed
@@ -990,15 +1190,18 @@ var RpgClientEngine = class {
990
1190
  this.emitMovePacket(input, frame, tick, timestamp, true);
991
1191
  this.lastInputTime = Date.now();
992
1192
  }
993
- processAction({ action }) {
1193
+ processAction(action, data) {
994
1194
  if (this.stopProcessingInput) return;
995
1195
  const currentPlayer = this.sceneMap.getCurrentPlayer();
996
1196
  if (!(!currentPlayer || getCanMoveValue(currentPlayer))) return;
1197
+ const payload = normalizeActionInput(action, data);
997
1198
  this.hooks.callHooks("client-engine-onInput", this, {
998
- input: "action",
1199
+ input: payload.action,
1200
+ action: payload.action,
1201
+ data: payload.data,
999
1202
  playerId: this.playerId
1000
1203
  }).subscribe();
1001
- this.webSocket.emit("action", { action });
1204
+ this.webSocket.emit("action", payload);
1002
1205
  }
1003
1206
  get PIXI() {
1004
1207
  return PIXI;
@@ -1012,9 +1215,43 @@ var RpgClientEngine = class {
1012
1215
  get scene() {
1013
1216
  return this.sceneMap;
1014
1217
  }
1218
+ getObjectById(id) {
1219
+ return this.sceneMap?.getObjectById(id);
1220
+ }
1015
1221
  getPhysicsTick() {
1016
1222
  return this.sceneMap?.getTick?.() ?? 0;
1017
1223
  }
1224
+ getPhysicsTickDurationMs() {
1225
+ const timeStep = this.sceneMap?.physic?.getWorld?.()?.getTimeStep?.();
1226
+ return typeof timeStep === "number" && Number.isFinite(timeStep) && timeStep > 0 ? timeStep * 1e3 : 1e3 / 60;
1227
+ }
1228
+ updateServerTickEstimate(serverTick, now = Date.now()) {
1229
+ if (typeof serverTick !== "number" || !Number.isFinite(serverTick)) return;
1230
+ this.latestServerTick = serverTick;
1231
+ this.latestServerTickAt = now;
1232
+ }
1233
+ estimateServerTick(now = Date.now()) {
1234
+ if (typeof this.latestServerTick !== "number" || this.latestServerTickAt <= 0) return;
1235
+ const elapsedTicks = Math.max(0, (now - this.latestServerTickAt) / this.getPhysicsTickDurationMs());
1236
+ return this.latestServerTick + elapsedTicks;
1237
+ }
1238
+ predictProjectileImpact(projectile) {
1239
+ if (projectile.predictImpact === false) return null;
1240
+ const sceneMap = this.sceneMap;
1241
+ if (!sceneMap?.physic || !Number.isFinite(projectile.range) || projectile.range <= 0) return null;
1242
+ const origin = projectile.origin;
1243
+ const direction = projectile.direction;
1244
+ 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;
1245
+ 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);
1246
+ if (!hit) return null;
1247
+ return {
1248
+ id: projectile.id,
1249
+ targetId: hit.entity.uuid,
1250
+ x: hit.point.x,
1251
+ y: hit.point.y,
1252
+ distance: hit.distance
1253
+ };
1254
+ }
1018
1255
  ensureCurrentPlayerBody() {
1019
1256
  const player = this.sceneMap?.getCurrentPlayer();
1020
1257
  const myId = this.playerIdSignal();
@@ -1281,6 +1518,7 @@ var RpgClientEngine = class {
1281
1518
  if (sprite && typeof sprite.flash === "function") sprite.flash(options);
1282
1519
  }
1283
1520
  applyServerAck(ack) {
1521
+ this.updateServerTickEstimate(ack.serverTick);
1284
1522
  if (this.predictionEnabled && this.prediction) {
1285
1523
  const result = this.prediction.applyServerAck({
1286
1524
  frame: ack.frame,
@@ -1385,6 +1623,12 @@ var RpgClientEngine = class {
1385
1623
  window.removeEventListener("resize", this.resizeHandler);
1386
1624
  this.resizeHandler = void 0;
1387
1625
  }
1626
+ if (this.pointerMoveHandler && this.pointerCanvas) {
1627
+ this.pointerCanvas.removeEventListener("pointermove", this.pointerMoveHandler);
1628
+ this.pointerCanvas.removeEventListener("pointerdown", this.pointerMoveHandler);
1629
+ this.pointerMoveHandler = void 0;
1630
+ this.pointerCanvas = void 0;
1631
+ }
1388
1632
  const rendererStillExists = this.renderer && typeof this.renderer.destroy === "function";
1389
1633
  if (this.canvasApp && typeof this.canvasApp.destroy === "function") {
1390
1634
  try {
@@ -1416,6 +1660,7 @@ var RpgClientEngine = class {
1416
1660
  this.cameraFollowTargetId.set(null);
1417
1661
  this.spriteComponentsBehind.set([]);
1418
1662
  this.spriteComponentsInFront.set([]);
1663
+ this.eventComponentResolvers.clear();
1419
1664
  this.spritesheets.clear();
1420
1665
  this.sounds.clear();
1421
1666
  this.componentAnimations = [];