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

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 (133) hide show
  1. package/CHANGELOG.md +19 -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.d.ts +2 -0
  16. package/dist/Game/Object.js +22 -8
  17. package/dist/Game/Object.js.map +1 -1
  18. package/dist/Game/Object.spec.d.ts +1 -0
  19. package/dist/Game/ProjectileManager.d.ts +11 -2
  20. package/dist/Game/ProjectileManager.js +19 -2
  21. package/dist/Game/ProjectileManager.js.map +1 -1
  22. package/dist/Gui/Gui.d.ts +3 -2
  23. package/dist/Gui/Gui.js +18 -6
  24. package/dist/Gui/Gui.js.map +1 -1
  25. package/dist/RpgClient.d.ts +85 -1
  26. package/dist/RpgClientEngine.d.ts +77 -2
  27. package/dist/RpgClientEngine.js +290 -31
  28. package/dist/RpgClientEngine.js.map +1 -1
  29. package/dist/components/animations/fx.ce.js +58 -0
  30. package/dist/components/animations/fx.ce.js.map +1 -0
  31. package/dist/components/animations/index.d.ts +1 -0
  32. package/dist/components/animations/index.js +3 -1
  33. package/dist/components/animations/index.js.map +1 -1
  34. package/dist/components/character.ce.js +192 -19
  35. package/dist/components/character.ce.js.map +1 -1
  36. package/dist/components/gui/dialogbox/index.ce.js +27 -12
  37. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  38. package/dist/components/gui/gameover.ce.js +4 -3
  39. package/dist/components/gui/gameover.ce.js.map +1 -1
  40. package/dist/components/gui/menu/equip-menu.ce.js +9 -8
  41. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  42. package/dist/components/gui/menu/exit-menu.ce.js +7 -5
  43. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  44. package/dist/components/gui/menu/items-menu.ce.js +8 -7
  45. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  46. package/dist/components/gui/menu/main-menu.ce.js +12 -11
  47. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  48. package/dist/components/gui/menu/options-menu.ce.js +7 -5
  49. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  50. package/dist/components/gui/menu/skills-menu.ce.js +4 -2
  51. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  52. package/dist/components/gui/notification/notification.ce.js +4 -1
  53. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  54. package/dist/components/gui/save-load.ce.js +10 -9
  55. package/dist/components/gui/save-load.ce.js.map +1 -1
  56. package/dist/components/gui/shop/shop.ce.js +17 -16
  57. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  58. package/dist/components/gui/title-screen.ce.js +4 -3
  59. package/dist/components/gui/title-screen.ce.js.map +1 -1
  60. package/dist/components/interaction-components.ce.js +20 -0
  61. package/dist/components/interaction-components.ce.js.map +1 -0
  62. package/dist/components/scenes/canvas.ce.js +12 -7
  63. package/dist/components/scenes/canvas.ce.js.map +1 -1
  64. package/dist/components/scenes/draw-map.ce.js +18 -13
  65. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  66. package/dist/i18n.d.ts +55 -0
  67. package/dist/i18n.js +60 -0
  68. package/dist/i18n.js.map +1 -0
  69. package/dist/i18n.spec.d.ts +1 -0
  70. package/dist/index.d.ts +3 -0
  71. package/dist/index.js +5 -2
  72. package/dist/module.js +30 -3
  73. package/dist/module.js.map +1 -1
  74. package/dist/services/actionInput.d.ts +3 -1
  75. package/dist/services/actionInput.js +33 -1
  76. package/dist/services/actionInput.js.map +1 -1
  77. package/dist/services/interactions.d.ts +159 -0
  78. package/dist/services/interactions.js +460 -0
  79. package/dist/services/interactions.js.map +1 -0
  80. package/dist/services/interactions.spec.d.ts +1 -0
  81. package/dist/services/keyboardControls.d.ts +1 -0
  82. package/dist/services/keyboardControls.js +1 -0
  83. package/dist/services/keyboardControls.js.map +1 -1
  84. package/dist/services/standalone.d.ts +3 -1
  85. package/dist/services/standalone.js +31 -13
  86. package/dist/services/standalone.js.map +1 -1
  87. package/dist/utils/mapId.d.ts +1 -0
  88. package/dist/utils/mapId.js +6 -0
  89. package/dist/utils/mapId.js.map +1 -0
  90. package/package.json +4 -4
  91. package/src/Game/AnimationManager.ts +4 -0
  92. package/src/Game/ClientVisuals.spec.ts +56 -0
  93. package/src/Game/ClientVisuals.ts +184 -0
  94. package/src/Game/EventComponentResolver.spec.ts +84 -0
  95. package/src/Game/EventComponentResolver.ts +74 -0
  96. package/src/Game/Map.ts +10 -0
  97. package/src/Game/Object.spec.ts +59 -0
  98. package/src/Game/Object.ts +36 -12
  99. package/src/Game/ProjectileManager.spec.ts +111 -0
  100. package/src/Game/ProjectileManager.ts +24 -2
  101. package/src/Gui/Gui.spec.ts +67 -0
  102. package/src/Gui/Gui.ts +24 -7
  103. package/src/RpgClient.ts +96 -1
  104. package/src/RpgClientEngine.ts +378 -45
  105. package/src/components/animations/fx.ce +101 -0
  106. package/src/components/animations/index.ts +4 -2
  107. package/src/components/character.ce +243 -17
  108. package/src/components/gui/dialogbox/index.ce +35 -14
  109. package/src/components/gui/gameover.ce +4 -3
  110. package/src/components/gui/menu/equip-menu.ce +9 -8
  111. package/src/components/gui/menu/exit-menu.ce +4 -3
  112. package/src/components/gui/menu/items-menu.ce +8 -7
  113. package/src/components/gui/menu/main-menu.ce +12 -11
  114. package/src/components/gui/menu/options-menu.ce +4 -3
  115. package/src/components/gui/menu/skills-menu.ce +2 -1
  116. package/src/components/gui/notification/notification.ce +7 -1
  117. package/src/components/gui/save-load.ce +11 -10
  118. package/src/components/gui/shop/shop.ce +17 -16
  119. package/src/components/gui/title-screen.ce +4 -3
  120. package/src/components/interaction-components.ce +23 -0
  121. package/src/components/scenes/canvas.ce +12 -7
  122. package/src/components/scenes/draw-map.ce +16 -5
  123. package/src/i18n.spec.ts +39 -0
  124. package/src/i18n.ts +59 -0
  125. package/src/index.ts +3 -0
  126. package/src/module.ts +43 -10
  127. package/src/services/actionInput.spec.ts +54 -0
  128. package/src/services/actionInput.ts +68 -1
  129. package/src/services/interactions.spec.ts +175 -0
  130. package/src/services/interactions.ts +722 -0
  131. package/src/services/keyboardControls.ts +2 -1
  132. package/src/services/standalone.ts +39 -10
  133. package/src/utils/mapId.ts +2 -0
@@ -19,13 +19,72 @@ import __ce_component$3 from "./components/dynamics/bar.ce.js";
19
19
  import __ce_component$4 from "./components/dynamics/shape.ce.js";
20
20
  import __ce_component$5 from "./components/dynamics/image.ce.js";
21
21
  import { NotificationManager } from "./Gui/NotificationManager.js";
22
+ import { normalizeRoomMapId } from "./utils/mapId.js";
22
23
  import { ProjectileManager } from "./Game/ProjectileManager.js";
24
+ import { ClientVisualRegistry } from "./Game/ClientVisuals.js";
23
25
  import { createClientPointerContext } from "./services/pointerContext.js";
26
+ import { RpgClientInteractions } from "./services/interactions.js";
27
+ import { EventComponentResolverRegistry } from "./Game/EventComponentResolver.js";
28
+ import { RpgClientBuiltinI18n } from "./i18n.js";
24
29
  import { Howl, bootstrapCanvas, signal, trigger } from "canvasengine";
25
- import { Direction, ModulesToken, PredictionController, Vector2, normalizeLightingState } from "@rpgjs/common";
30
+ import { Direction, ModulesToken, PredictionController, Vector2, getOrCreateI18nService, normalizeLightingState } from "@rpgjs/common";
26
31
  import { BehaviorSubject, combineLatest, filter, lastValueFrom, switchMap, take } from "rxjs";
27
32
  import * as PIXI from "pixi.js";
28
33
  //#region src/RpgClientEngine.ts
34
+ var DEFAULT_DASH_ADDITIONAL_SPEED = 8;
35
+ var DEFAULT_DASH_DURATION_MS = 180;
36
+ var DEFAULT_DASH_COOLDOWN_MS = 450;
37
+ var isDashInput = (input) => typeof input === "object" && input !== null && input.type === "dash";
38
+ var isMoveInput = (input) => typeof input === "object" && input !== null && input.type === "move";
39
+ var resolveMoveDirection = (input) => {
40
+ if (isMoveInput(input)) return input.direction;
41
+ if (typeof input === "string" || typeof input === "number") return input;
42
+ };
43
+ var directionToVector = (direction) => {
44
+ switch (direction) {
45
+ case Direction.Left: return {
46
+ x: -1,
47
+ y: 0
48
+ };
49
+ case Direction.Right: return {
50
+ x: 1,
51
+ y: 0
52
+ };
53
+ case Direction.Up: return {
54
+ x: 0,
55
+ y: -1
56
+ };
57
+ case Direction.Down:
58
+ default: return {
59
+ x: 0,
60
+ y: 1
61
+ };
62
+ }
63
+ };
64
+ var vectorToDirection = (direction) => {
65
+ if (Math.abs(direction.x) > Math.abs(direction.y)) return direction.x < 0 ? Direction.Left : Direction.Right;
66
+ return direction.y < 0 ? Direction.Up : Direction.Down;
67
+ };
68
+ var normalizeDashInput = (input, fallbackDirection) => {
69
+ const rawDirection = input.direction ?? directionToVector(fallbackDirection);
70
+ const rawX = Number(rawDirection?.x ?? 0);
71
+ const rawY = Number(rawDirection?.y ?? 0);
72
+ const magnitude = Math.hypot(rawX, rawY);
73
+ if (!Number.isFinite(magnitude) || magnitude <= 0) return null;
74
+ const additionalSpeed = typeof input.additionalSpeed === "number" && Number.isFinite(input.additionalSpeed) ? Math.max(0, Math.min(input.additionalSpeed, 64)) : DEFAULT_DASH_ADDITIONAL_SPEED;
75
+ const duration = typeof input.duration === "number" && Number.isFinite(input.duration) ? Math.max(1, Math.min(input.duration, 1e3)) : DEFAULT_DASH_DURATION_MS;
76
+ const cooldown = typeof input.cooldown === "number" && Number.isFinite(input.cooldown) ? Math.max(0, Math.min(input.cooldown, 5e3)) : DEFAULT_DASH_COOLDOWN_MS;
77
+ return {
78
+ type: "dash",
79
+ direction: {
80
+ x: rawX / magnitude,
81
+ y: rawY / magnitude
82
+ },
83
+ additionalSpeed,
84
+ duration,
85
+ cooldown
86
+ };
87
+ };
29
88
  var RpgClientEngine = class {
30
89
  constructor(context) {
31
90
  this.context = context;
@@ -34,14 +93,18 @@ var RpgClientEngine = class {
34
93
  this.width = signal("100%");
35
94
  this.height = signal("100%");
36
95
  this.spritesheets = /* @__PURE__ */ new Map();
96
+ this.spritesheetPromises = /* @__PURE__ */ new Map();
37
97
  this.sounds = /* @__PURE__ */ new Map();
38
98
  this.componentAnimations = [];
99
+ this.clientVisuals = new ClientVisualRegistry();
39
100
  this.pointer = createClientPointerContext();
101
+ this.interactions = new RpgClientInteractions(this);
40
102
  this.particleSettings = { emitters: [] };
41
103
  this.playerIdSignal = signal(null);
42
104
  this.spriteComponentsBehind = signal([]);
43
105
  this.spriteComponentsInFront = signal([]);
44
106
  this.spriteComponents = /* @__PURE__ */ new Map();
107
+ this.eventComponentResolvers = new EventComponentResolverRegistry();
45
108
  this.cameraFollowTargetId = signal(null);
46
109
  this.mapShakeTrigger = trigger();
47
110
  this.controlsReady = signal(void 0);
@@ -53,6 +116,7 @@ var RpgClientEngine = class {
53
116
  this.lastClientPhysicsStepAt = 0;
54
117
  this.frameOffset = 0;
55
118
  this.latestServerTickAt = 0;
119
+ this.dashLockedUntil = 0;
56
120
  this.rtt = 0;
57
121
  this.pingInterval = null;
58
122
  this.PING_INTERVAL_MS = 5e3;
@@ -66,6 +130,8 @@ var RpgClientEngine = class {
66
130
  this.playersReceived$ = new BehaviorSubject(false);
67
131
  this.eventsReceived$ = new BehaviorSubject(false);
68
132
  this.sceneResetQueued = false;
133
+ this.mapTransitionInProgress = false;
134
+ this.socketListenersInitialized = false;
69
135
  this.tickSubscriptions = [];
70
136
  this.pendingSyncPackets = [];
71
137
  this.notificationManager = new NotificationManager();
@@ -73,6 +139,8 @@ var RpgClientEngine = class {
73
139
  this.guiService = inject(RpgGui);
74
140
  this.loadMapService = inject(LoadMapToken);
75
141
  this.hooks = inject(ModulesToken);
142
+ this.i18nService = getOrCreateI18nService(context);
143
+ this.i18nService.addMessages(RpgClientBuiltinI18n, "rpgjs-client", 0);
76
144
  this.projectiles = new ProjectileManager(this.hooks, (projectile) => this.predictProjectileImpact(projectile));
77
145
  this.globalConfig = inject(GlobalConfigToken);
78
146
  if (!this.globalConfig) this.globalConfig = {};
@@ -96,6 +164,21 @@ var RpgClientEngine = class {
96
164
  this.predictionEnabled = this.globalConfig?.prediction?.enabled !== false;
97
165
  this.initializePredictionController();
98
166
  }
167
+ setLocale(locale) {
168
+ this.locale = locale;
169
+ }
170
+ getLocale() {
171
+ return this.locale || this.i18nService.defaultLocale;
172
+ }
173
+ t(key, params) {
174
+ return this.i18nService.t(key, params, this.getLocale());
175
+ }
176
+ i18n() {
177
+ return {
178
+ locale: this.getLocale(),
179
+ t: (key, params) => this.t(key, params)
180
+ };
181
+ }
99
182
  /**
100
183
  * Assigns a CanvasEngine KeyboardControls instance to the dependency injection context
101
184
  *
@@ -160,7 +243,6 @@ var RpgClientEngine = class {
160
243
  this.renderer = app.renderer;
161
244
  this.setupPointerTracking();
162
245
  this.tick = canvasElement?.propObservables?.context["tick"].observable;
163
- this.flushPendingSyncPackets();
164
246
  const inputCheckSubscription = this.tick.subscribe(() => {
165
247
  if (Date.now() - this.lastInputTime > 100) {
166
248
  const player = this.getCurrentPlayer();
@@ -171,6 +253,7 @@ var RpgClientEngine = class {
171
253
  this.tickSubscriptions.push(inputCheckSubscription);
172
254
  this.hooks.callHooks("client-spritesheets-load", this).subscribe();
173
255
  this.hooks.callHooks("client-spritesheetResolver-load", this).subscribe();
256
+ this.flushPendingSyncPackets();
174
257
  this.hooks.callHooks("client-sounds-load", this).subscribe();
175
258
  this.hooks.callHooks("client-soundResolver-load", this).subscribe();
176
259
  RpgSound.init(this);
@@ -178,7 +261,9 @@ var RpgClientEngine = class {
178
261
  this.hooks.callHooks("client-gui-load", this).subscribe();
179
262
  this.hooks.callHooks("client-particles-load", this).subscribe();
180
263
  this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
264
+ this.hooks.callHooks("client-clientVisuals-load", this).subscribe();
181
265
  this.hooks.callHooks("client-projectiles-load", this).subscribe();
266
+ this.hooks.callHooks("client-interactions-load", this).subscribe();
182
267
  this.hooks.callHooks("client-sprite-load", this).subscribe();
183
268
  await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
184
269
  this.resizeHandler = () => {
@@ -210,7 +295,7 @@ var RpgClientEngine = class {
210
295
  const canvas = renderer?.canvas ?? renderer?.view ?? this.canvasApp?.canvas;
211
296
  if (!canvas || typeof canvas.addEventListener !== "function") return;
212
297
  this.pointerCanvas = canvas;
213
- this.pointerMoveHandler = (event) => {
298
+ const updatePointer = (event) => {
214
299
  const rect = canvas.getBoundingClientRect();
215
300
  const screen = {
216
301
  x: event.clientX - rect.left,
@@ -233,11 +318,59 @@ var RpgClientEngine = class {
233
318
  }
234
319
  this.pointer.update(screen, world);
235
320
  };
321
+ this.pointerMoveHandler = (event) => {
322
+ updatePointer(event);
323
+ this.interactions.handlePointerMove(event);
324
+ };
325
+ this.pointerUpHandler = (event) => {
326
+ updatePointer(event);
327
+ this.interactions.handlePointerUp(event);
328
+ };
329
+ this.pointerCancelHandler = (event) => {
330
+ updatePointer(event);
331
+ this.interactions.cancelDrag(event);
332
+ };
236
333
  canvas.addEventListener("pointermove", this.pointerMoveHandler);
237
334
  canvas.addEventListener("pointerdown", this.pointerMoveHandler);
335
+ canvas.addEventListener("pointerup", this.pointerUpHandler);
336
+ canvas.addEventListener("pointercancel", this.pointerCancelHandler);
337
+ canvas.addEventListener("pointerleave", this.pointerCancelHandler);
338
+ }
339
+ updatePointerFromInteractionEvent(event) {
340
+ const global = event?.global ?? event?.data?.global;
341
+ if (!global) {
342
+ this.pointer.updateFromEvent(event);
343
+ return;
344
+ }
345
+ const screen = {
346
+ x: Number(global.x),
347
+ y: Number(global.y)
348
+ };
349
+ if (!Number.isFinite(screen.x) || !Number.isFinite(screen.y)) {
350
+ this.pointer.updateFromEvent(event);
351
+ return;
352
+ }
353
+ const viewport = this.findViewportInstance();
354
+ if (viewport && typeof viewport.toWorld === "function") {
355
+ const point = viewport.toWorld(screen.x, screen.y);
356
+ this.pointer.update(screen, {
357
+ x: Number(point.x),
358
+ y: Number(point.y)
359
+ });
360
+ return;
361
+ }
362
+ this.pointer.update(screen);
238
363
  }
239
364
  findViewportInstance() {
240
- return (this.canvasApp?.stage?.children ?? []).find((child) => typeof child?.toWorld === "function" || child?.constructor?.name === "Viewport");
365
+ const find = (node) => {
366
+ if (!node) return void 0;
367
+ if (typeof node?.toWorld === "function" || node?.constructor?.name === "Viewport") return node;
368
+ for (const child of node.children ?? []) {
369
+ const viewport = find(child);
370
+ if (viewport) return viewport;
371
+ }
372
+ };
373
+ return find(this.canvasApp?.stage);
241
374
  }
242
375
  prepareSyncPayload(data) {
243
376
  const payload = { ...data ?? {} };
@@ -271,6 +404,8 @@ var RpgClientEngine = class {
271
404
  };
272
405
  }
273
406
  initListeners() {
407
+ if (this.socketListenersInitialized) return;
408
+ this.socketListenersInitialized = true;
274
409
  this.webSocket.on("sync", (data) => {
275
410
  if (!this.tick) {
276
411
  this.pendingSyncPackets.push(data);
@@ -288,12 +423,8 @@ var RpgClientEngine = class {
288
423
  console.debug(`[Ping/Pong] RTT: ${this.rtt}ms, ServerTick: ${data.serverTick}, FrameOffset: ${this.frameOffset}`);
289
424
  });
290
425
  this.webSocket.on("changeMap", (data) => {
291
- this.sceneResetQueued = true;
292
- this.sceneMap.weatherState.set(null);
293
- this.sceneMap.lightingState.set(null);
294
- this.sceneMap.clearLightSpots();
295
- this.projectiles.clear();
296
- this.cameraFollowTargetId.set(null);
426
+ const nextMapId = typeof data?.mapId === "string" ? data.mapId : void 0;
427
+ this.beginMapTransfer(nextMapId);
297
428
  const transferToken = typeof data?.transferToken === "string" ? data.transferToken : void 0;
298
429
  this.loadScene(data.mapId, transferToken);
299
430
  });
@@ -303,19 +434,27 @@ var RpgClientEngine = class {
303
434
  const player = object ? this.sceneMap.getObjectById(object) : void 0;
304
435
  this.getComponentAnimation(id).displayEffect(params, player || position);
305
436
  });
437
+ this.webSocket.on("clientVisual", (data) => {
438
+ this.playClientVisual(data);
439
+ });
306
440
  this.webSocket.on("projectile:spawnBatch", (data) => {
441
+ if (!this.shouldProcessProjectilePacket(data)) return;
307
442
  this.projectiles.spawnBatch(data?.projectiles ?? [], {
443
+ mapId: data?.mapId,
308
444
  currentServerTick: this.estimateServerTick(),
309
445
  tickDurationMs: this.getPhysicsTickDurationMs()
310
446
  });
311
447
  });
312
448
  this.webSocket.on("projectile:impactBatch", (data) => {
313
- this.projectiles.impactBatch(data?.impacts ?? []);
449
+ if (!this.shouldProcessProjectilePacket(data)) return;
450
+ this.projectiles.impactBatch(data?.impacts ?? [], { mapId: data?.mapId });
314
451
  });
315
452
  this.webSocket.on("projectile:destroyBatch", (data) => {
316
- this.projectiles.destroyBatch(data?.projectiles ?? []);
453
+ if (!this.shouldProcessProjectilePacket(data)) return;
454
+ this.projectiles.destroyBatch(data?.projectiles ?? [], { mapId: data?.mapId });
317
455
  });
318
- this.webSocket.on("projectile:clear", () => {
456
+ this.webSocket.on("projectile:clear", (data) => {
457
+ if (!this.shouldProcessProjectilePacket(data)) return;
319
458
  this.projectiles.clear();
320
459
  });
321
460
  this.webSocket.on("notification", (data) => {
@@ -408,6 +547,31 @@ var RpgClientEngine = class {
408
547
  this.callConnectError(error);
409
548
  });
410
549
  }
550
+ beginMapTransfer(nextMapId) {
551
+ this.mapTransitionInProgress = true;
552
+ this.currentMapRoomId = nextMapId;
553
+ this.sceneResetQueued = false;
554
+ this.clearClientPredictionStates();
555
+ this.sceneMap.weatherState.set(null);
556
+ this.sceneMap.lightingState.set(null);
557
+ this.sceneMap.clearLightSpots();
558
+ this.clearComponentAnimations();
559
+ this.projectiles.setMapId(nextMapId);
560
+ this.cameraFollowTargetId.set(null);
561
+ this.sceneMap.reset();
562
+ this.sceneMap.loadPhysic();
563
+ }
564
+ clearComponentAnimations() {
565
+ this.componentAnimations.forEach((componentAnimation) => {
566
+ componentAnimation.instance?.clear?.();
567
+ });
568
+ }
569
+ shouldProcessProjectilePacket(data) {
570
+ if (this.mapTransitionInProgress) return false;
571
+ const packetMapId = normalizeRoomMapId(typeof data?.mapId === "string" ? data.mapId : void 0);
572
+ const currentMapId = normalizeRoomMapId(this.currentMapRoomId);
573
+ return !packetMapId || !currentMapId || packetMapId === currentMapId;
574
+ }
411
575
  async callConnectError(error) {
412
576
  await lastValueFrom(this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket));
413
577
  }
@@ -522,12 +686,9 @@ var RpgClientEngine = class {
522
686
  query: transferToken ? { transferToken } : void 0
523
687
  });
524
688
  try {
525
- await this.webSocket.reconnect(() => {
526
- inject(SaveClientService).initialize();
527
- this.initListeners();
528
- this.guiService._initialize();
529
- });
689
+ await this.webSocket.reconnect();
530
690
  } catch (error) {
691
+ this.mapTransitionInProgress = false;
531
692
  this.stopPingPong();
532
693
  await this.callConnectError(error);
533
694
  throw error;
@@ -539,6 +700,8 @@ var RpgClientEngine = class {
539
700
  if (players && Object.keys(players).length > 0) this.playersReceived$.next(true);
540
701
  if (this.sceneMap.events() !== void 0) this.eventsReceived$.next(true);
541
702
  this.mapLoadCompleted$.next(true);
703
+ this.currentMapRoomId = mapId;
704
+ this.mapTransitionInProgress = false;
542
705
  this.sceneMap.configureClientPrediction(this.predictionEnabled);
543
706
  this.sceneMap.loadPhysic();
544
707
  }
@@ -599,12 +762,20 @@ var RpgClientEngine = class {
599
762
  getSpriteSheet(id) {
600
763
  if (this.spritesheets.has(id)) return this.spritesheets.get(id);
601
764
  if (this.spritesheetResolver) {
765
+ if (this.spritesheetPromises.has(id)) return this.spritesheetPromises.get(id);
602
766
  const result = this.spritesheetResolver(id);
603
- if (result instanceof Promise) return result.then((spritesheet) => {
604
- if (spritesheet) this.spritesheets.set(id, spritesheet);
605
- return spritesheet;
606
- });
607
- else {
767
+ if (result instanceof Promise) {
768
+ const promise = result.then((spritesheet) => {
769
+ if (spritesheet) this.spritesheets.set(id, spritesheet);
770
+ this.spritesheetPromises.delete(id);
771
+ return spritesheet;
772
+ }).catch((error) => {
773
+ this.spritesheetPromises.delete(id);
774
+ throw error;
775
+ });
776
+ this.spritesheetPromises.set(id, promise);
777
+ return promise;
778
+ } else {
608
779
  if (result) this.spritesheets.set(id, result);
609
780
  return result;
610
781
  }
@@ -960,6 +1131,27 @@ var RpgClientEngine = class {
960
1131
  getSpriteComponent(id) {
961
1132
  return this.spriteComponents.get(id);
962
1133
  }
1134
+ /**
1135
+ * Register a custom event component resolver.
1136
+ *
1137
+ * The last resolver returning a component wins. This lets later modules
1138
+ * override earlier defaults without replacing the whole map scene.
1139
+ *
1140
+ * @param resolver - Function receiving the synced event object
1141
+ * @returns The registered resolver
1142
+ */
1143
+ addEventComponentResolver(resolver) {
1144
+ return this.eventComponentResolvers.add(resolver);
1145
+ }
1146
+ /**
1147
+ * Resolve the custom CanvasEngine component for an event, if any.
1148
+ *
1149
+ * @param event - Synced client event object
1150
+ * @returns The component/config returned by the last matching resolver
1151
+ */
1152
+ resolveEventComponent(event) {
1153
+ return this.eventComponentResolvers.resolve(event);
1154
+ }
963
1155
  registerProjectileComponent(type, component) {
964
1156
  return this.projectiles.register(type, component);
965
1157
  }
@@ -967,6 +1159,39 @@ var RpgClientEngine = class {
967
1159
  return this.projectiles.get(type);
968
1160
  }
969
1161
  /**
1162
+ * Register a named client visual macro.
1163
+ *
1164
+ * Client visuals are small client-side functions that group existing visual
1165
+ * primitives such as flash, sound, component animations, sprite animation, or
1166
+ * map shake. The server sends only the visual name and a serializable payload.
1167
+ *
1168
+ * @param name - Stable visual name sent by the server
1169
+ * @param handler - Client-side visual handler
1170
+ * @returns The registered handler
1171
+ */
1172
+ registerClientVisual(name, handler) {
1173
+ return this.clientVisuals.register(name, handler);
1174
+ }
1175
+ /**
1176
+ * Register several named client visual macros.
1177
+ *
1178
+ * @param visuals - Map of visual names to client-side handlers
1179
+ */
1180
+ registerClientVisuals(visuals) {
1181
+ this.clientVisuals.registerMany(visuals);
1182
+ }
1183
+ /**
1184
+ * Play a registered client visual locally.
1185
+ *
1186
+ * This is also used by the websocket listener when the server calls
1187
+ * `player.clientVisual()` or `map.clientVisual()`.
1188
+ *
1189
+ * @param packet - Visual name and serializable payload
1190
+ */
1191
+ playClientVisual(packet) {
1192
+ return this.clientVisuals.play(packet, this);
1193
+ }
1194
+ /**
970
1195
  * Add a component animation to the engine
971
1196
  *
972
1197
  * Component animations are temporary visual effects that can be displayed
@@ -1071,10 +1296,17 @@ var RpgClientEngine = class {
1071
1296
  return;
1072
1297
  }
1073
1298
  const timestamp = Date.now();
1299
+ const movementInput = isDashInput(input) ? normalizeDashInput(input, currentPlayer?.direction?.()) : input;
1300
+ if (!movementInput) return;
1301
+ if (isDashInput(movementInput)) {
1302
+ const cooldown = movementInput.cooldown ?? DEFAULT_DASH_COOLDOWN_MS;
1303
+ if (timestamp < this.dashLockedUntil) return;
1304
+ this.dashLockedUntil = timestamp + cooldown;
1305
+ }
1074
1306
  let frame;
1075
1307
  let tick;
1076
1308
  if (this.predictionEnabled && this.prediction) {
1077
- const meta = this.prediction.recordInput(input, timestamp);
1309
+ const meta = this.prediction.recordInput(movementInput, timestamp);
1078
1310
  frame = meta.frame;
1079
1311
  tick = meta.tick;
1080
1312
  } else {
@@ -1083,20 +1315,25 @@ var RpgClientEngine = class {
1083
1315
  }
1084
1316
  this.inputFrameCounter = frame;
1085
1317
  this.hooks.callHooks("client-engine-onInput", this, {
1086
- input,
1318
+ input: movementInput,
1087
1319
  playerId: this.playerId
1088
1320
  }).subscribe();
1089
1321
  const bodyReady = this.ensureCurrentPlayerBody();
1090
1322
  if (currentPlayer && bodyReady) {
1091
- currentPlayer.changeDirection(input);
1092
- this.sceneMap.moveBody(currentPlayer, input);
1323
+ this.applyPredictedMovementInput(currentPlayer, movementInput);
1093
1324
  if (this.predictionEnabled && this.prediction) {
1094
1325
  this.pendingPredictionFrames.push(frame);
1095
1326
  if (this.pendingPredictionFrames.length > 240) this.pendingPredictionFrames = this.pendingPredictionFrames.slice(-240);
1096
1327
  }
1097
1328
  }
1098
- this.emitMovePacket(input, frame, tick, timestamp, true);
1099
- this.lastInputTime = Date.now();
1329
+ this.emitMovePacket(movementInput, frame, tick, timestamp, true);
1330
+ this.lastInputTime = isDashInput(movementInput) ? Date.now() + (movementInput.duration ?? DEFAULT_DASH_DURATION_MS) : Date.now();
1331
+ }
1332
+ async processDash(input = {}) {
1333
+ const currentPlayer = this.sceneMap.getCurrentPlayer();
1334
+ const dashInput = normalizeDashInput(input, typeof currentPlayer?.direction === "function" ? currentPlayer.direction() : currentPlayer?.direction);
1335
+ if (!dashInput) return;
1336
+ await this.processInput({ input: dashInput });
1100
1337
  }
1101
1338
  processAction(action, data) {
1102
1339
  if (this.stopProcessingInput) return;
@@ -1123,6 +1360,9 @@ var RpgClientEngine = class {
1123
1360
  get scene() {
1124
1361
  return this.sceneMap;
1125
1362
  }
1363
+ getObjectById(id) {
1364
+ return this.sceneMap?.getObjectById(id);
1365
+ }
1126
1366
  getPhysicsTick() {
1127
1367
  return this.sceneMap?.getTick?.() ?? 0;
1128
1368
  }
@@ -1202,7 +1442,7 @@ var RpgClientEngine = class {
1202
1442
  input: entry.direction,
1203
1443
  x: state.x,
1204
1444
  y: state.y,
1205
- direction: state.direction ?? entry.direction
1445
+ direction: state.direction ?? resolveMoveDirection(entry.direction)
1206
1446
  });
1207
1447
  }
1208
1448
  if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) return trajectory.slice(-this.MAX_MOVE_TRAJECTORY_POINTS);
@@ -1237,6 +1477,17 @@ var RpgClientEngine = class {
1237
1477
  if (now - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS) return;
1238
1478
  this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);
1239
1479
  }
1480
+ applyPredictedMovementInput(player, input) {
1481
+ if (isDashInput(input)) {
1482
+ const direction = vectorToDirection(input.direction);
1483
+ player.changeDirection(direction);
1484
+ return Boolean(this.sceneMap.dashBody?.(player, input));
1485
+ }
1486
+ const direction = resolveMoveDirection(input);
1487
+ if (!direction) return false;
1488
+ player.changeDirection(direction);
1489
+ return Boolean(this.sceneMap.moveBody?.(player, direction));
1490
+ }
1240
1491
  getLocalPlayerState() {
1241
1492
  const currentPlayer = this.sceneMap?.getCurrentPlayer();
1242
1493
  if (!currentPlayer) return {
@@ -1462,7 +1713,7 @@ var RpgClientEngine = class {
1462
1713
  const replayInputs = pendingInputs.slice(-600);
1463
1714
  for (const entry of replayInputs) {
1464
1715
  if (!entry?.direction) continue;
1465
- this.sceneMap.moveBody(player, entry.direction);
1716
+ this.applyPredictedMovementInput(player, entry.direction);
1466
1717
  this.sceneMap.stepPredictionTick();
1467
1718
  this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());
1468
1719
  }
@@ -1531,7 +1782,14 @@ var RpgClientEngine = class {
1531
1782
  if (this.pointerMoveHandler && this.pointerCanvas) {
1532
1783
  this.pointerCanvas.removeEventListener("pointermove", this.pointerMoveHandler);
1533
1784
  this.pointerCanvas.removeEventListener("pointerdown", this.pointerMoveHandler);
1785
+ if (this.pointerUpHandler) this.pointerCanvas.removeEventListener("pointerup", this.pointerUpHandler);
1786
+ if (this.pointerCancelHandler) {
1787
+ this.pointerCanvas.removeEventListener("pointercancel", this.pointerCancelHandler);
1788
+ this.pointerCanvas.removeEventListener("pointerleave", this.pointerCancelHandler);
1789
+ }
1534
1790
  this.pointerMoveHandler = void 0;
1791
+ this.pointerUpHandler = void 0;
1792
+ this.pointerCancelHandler = void 0;
1535
1793
  this.pointerCanvas = void 0;
1536
1794
  }
1537
1795
  const rendererStillExists = this.renderer && typeof this.renderer.destroy === "function";
@@ -1565,6 +1823,7 @@ var RpgClientEngine = class {
1565
1823
  this.cameraFollowTargetId.set(null);
1566
1824
  this.spriteComponentsBehind.set([]);
1567
1825
  this.spriteComponentsInFront.set([]);
1826
+ this.eventComponentResolvers.clear();
1568
1827
  this.spritesheets.clear();
1569
1828
  this.sounds.clear();
1570
1829
  this.componentAnimations = [];