@rpgjs/client 5.0.0-beta.6 → 5.0.0-beta.8

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 (147) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/Game/AnimationManager.d.ts +2 -2
  3. package/dist/Game/AnimationManager.js +18 -9
  4. package/dist/Game/AnimationManager.js.map +1 -1
  5. package/dist/Game/AnimationManager.spec.d.ts +1 -0
  6. package/dist/Game/Map.d.ts +7 -9
  7. package/dist/Game/Map.js +5 -4
  8. package/dist/Game/Map.js.map +1 -1
  9. package/dist/Game/Object.d.ts +44 -20
  10. package/dist/Game/Object.js +28 -14
  11. package/dist/Game/Object.js.map +1 -1
  12. package/dist/Gui/Gui.d.ts +19 -6
  13. package/dist/Gui/Gui.js +64 -34
  14. package/dist/Gui/Gui.js.map +1 -1
  15. package/dist/Gui/Gui.spec.d.ts +1 -0
  16. package/dist/Gui/NotificationManager.d.ts +1 -1
  17. package/dist/Gui/NotificationManager.js.map +1 -1
  18. package/dist/Resource.js +1 -1
  19. package/dist/Resource.js.map +1 -1
  20. package/dist/RpgClient.d.ts +57 -2
  21. package/dist/RpgClientEngine.d.ts +55 -16
  22. package/dist/RpgClientEngine.js +60 -5
  23. package/dist/RpgClientEngine.js.map +1 -1
  24. package/dist/Sound.js.map +1 -1
  25. package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.128.0}/helpers/decorate.js +1 -1
  26. package/dist/_virtual/{_@oxc-project_runtime@0.127.0 → _@oxc-project_runtime@0.128.0}/helpers/decorateMetadata.js +1 -1
  27. package/dist/components/animations/animation.ce.js.map +1 -1
  28. package/dist/components/animations/hit.ce.js.map +1 -1
  29. package/dist/components/character.ce.js +280 -3
  30. package/dist/components/character.ce.js.map +1 -1
  31. package/dist/components/dynamics/bar.ce.js +96 -0
  32. package/dist/components/dynamics/bar.ce.js.map +1 -0
  33. package/dist/components/dynamics/image.ce.js +23 -0
  34. package/dist/components/dynamics/image.ce.js.map +1 -0
  35. package/dist/components/dynamics/parse-value.d.ts +4 -1
  36. package/dist/components/dynamics/parse-value.js +51 -35
  37. package/dist/components/dynamics/parse-value.js.map +1 -1
  38. package/dist/components/dynamics/parse-value.spec.d.ts +1 -0
  39. package/dist/components/dynamics/shape-utils.d.ts +16 -0
  40. package/dist/components/dynamics/shape-utils.js +73 -0
  41. package/dist/components/dynamics/shape-utils.js.map +1 -0
  42. package/dist/components/dynamics/shape-utils.spec.d.ts +1 -0
  43. package/dist/components/dynamics/shape.ce.js +83 -0
  44. package/dist/components/dynamics/shape.ce.js.map +1 -0
  45. package/dist/components/dynamics/text.ce.js +28 -41
  46. package/dist/components/dynamics/text.ce.js.map +1 -1
  47. package/dist/components/gui/box.ce.js.map +1 -1
  48. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  49. package/dist/components/gui/gameover.ce.js.map +1 -1
  50. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  51. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  52. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  53. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  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/index.d.ts +1 -1
  58. package/dist/components/gui/mobile/index.js.map +1 -1
  59. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  60. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  61. package/dist/components/gui/save-load.ce.js.map +1 -1
  62. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  63. package/dist/components/gui/title-screen.ce.js.map +1 -1
  64. package/dist/components/player-components-utils.d.ts +67 -0
  65. package/dist/components/player-components-utils.js +162 -0
  66. package/dist/components/player-components-utils.js.map +1 -0
  67. package/dist/components/player-components-utils.spec.d.ts +1 -0
  68. package/dist/components/player-components.ce.js +188 -0
  69. package/dist/components/player-components.ce.js.map +1 -0
  70. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  71. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  72. package/dist/components/scenes/canvas.ce.js.map +1 -1
  73. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  74. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  75. package/dist/core/inject.js +1 -1
  76. package/dist/core/inject.js.map +1 -1
  77. package/dist/core/setup.js +1 -1
  78. package/dist/core/setup.js.map +1 -1
  79. package/dist/index.js +1 -1
  80. package/dist/module.js +4 -1
  81. package/dist/module.js.map +1 -1
  82. package/dist/node_modules/.pnpm/{@signe_di@2.9.0 → @signe_di@2.10.0}/node_modules/@signe/di/dist/index.js +7 -117
  83. package/dist/node_modules/.pnpm/@signe_di@2.10.0/node_modules/@signe/di/dist/index.js.map +1 -0
  84. package/dist/node_modules/.pnpm/@signe_reactive@2.10.0/node_modules/@signe/reactive/dist/index.js +239 -0
  85. package/dist/node_modules/.pnpm/@signe_reactive@2.10.0/node_modules/@signe/reactive/dist/index.js.map +1 -0
  86. package/dist/node_modules/.pnpm/@signe_room@2.10.0/node_modules/@signe/room/dist/index.js +611 -0
  87. package/dist/node_modules/.pnpm/@signe_room@2.10.0/node_modules/@signe/room/dist/index.js.map +1 -0
  88. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/client/index.js +44 -0
  89. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/client/index.js.map +1 -0
  90. package/dist/node_modules/.pnpm/{@signe_sync@2.9.0 → @signe_sync@2.10.0}/node_modules/@signe/sync/dist/index.js +29 -136
  91. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/index.js.map +1 -0
  92. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -1
  93. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -1
  94. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -1
  95. package/dist/presets/animation.js.map +1 -1
  96. package/dist/presets/faceset.js.map +1 -1
  97. package/dist/presets/icon.js.map +1 -1
  98. package/dist/presets/lpc.js.map +1 -1
  99. package/dist/presets/rmspritesheet.js.map +1 -1
  100. package/dist/services/AbstractSocket.js.map +1 -1
  101. package/dist/services/keyboardControls.js.map +1 -1
  102. package/dist/services/loadMap.d.ts +6 -0
  103. package/dist/services/loadMap.js +1 -1
  104. package/dist/services/loadMap.js.map +1 -1
  105. package/dist/services/mmorpg.js +1 -1
  106. package/dist/services/mmorpg.js.map +1 -1
  107. package/dist/services/save.js.map +1 -1
  108. package/dist/services/standalone.js +1 -1
  109. package/dist/services/standalone.js.map +1 -1
  110. package/dist/utils/getEntityProp.js.map +1 -1
  111. package/package.json +8 -8
  112. package/src/Game/AnimationManager.spec.ts +30 -0
  113. package/src/Game/AnimationManager.ts +22 -10
  114. package/src/Game/Map.ts +12 -2
  115. package/src/Game/Object.ts +68 -43
  116. package/src/Gui/Gui.spec.ts +273 -0
  117. package/src/Gui/Gui.ts +105 -50
  118. package/src/Resource.ts +1 -2
  119. package/src/RpgClient.ts +63 -2
  120. package/src/RpgClientEngine.ts +82 -12
  121. package/src/components/character.ce +353 -1
  122. package/src/components/dynamics/bar.ce +87 -0
  123. package/src/components/dynamics/image.ce +20 -0
  124. package/src/components/dynamics/parse-value.spec.ts +41 -0
  125. package/src/components/dynamics/parse-value.ts +102 -37
  126. package/src/components/dynamics/shape-utils.spec.ts +46 -0
  127. package/src/components/dynamics/shape-utils.ts +61 -0
  128. package/src/components/dynamics/shape.ce +89 -0
  129. package/src/components/dynamics/text.ce +34 -149
  130. package/src/components/player-components-utils.spec.ts +109 -0
  131. package/src/components/player-components-utils.ts +205 -0
  132. package/src/components/player-components.ce +221 -0
  133. package/src/core/setup.ts +2 -2
  134. package/src/module.ts +5 -1
  135. package/src/services/loadMap.ts +2 -0
  136. package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +0 -1
  137. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js +0 -463
  138. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js.map +0 -1
  139. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js +0 -2191
  140. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js.map +0 -1
  141. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +0 -10
  142. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +0 -1
  143. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js +0 -91
  144. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js.map +0 -1
  145. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js.map +0 -1
  146. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js +0 -14
  147. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +0 -1
package/src/Gui/Gui.ts CHANGED
@@ -3,12 +3,12 @@ import { signal, Signal, WritableSignal } from "canvasengine";
3
3
  import { AbstractWebsocket, WebSocketToken } from "../services/AbstractSocket";
4
4
  import { DialogboxComponent, ShopComponent, SaveLoadComponent, MainMenuComponent, NotificationComponent, TitleScreenComponent, GameoverComponent } from "../components/gui";
5
5
  import { combineLatest, Subscription } from "rxjs";
6
- import { delay, PrebuiltGui } from "@rpgjs/common";
6
+ import { PrebuiltGui } from "@rpgjs/common";
7
7
 
8
8
  interface GuiOptions {
9
9
  name?: string;
10
10
  id?: string;
11
- component: any;
11
+ component?: any;
12
12
  display?: boolean;
13
13
  data?: any;
14
14
  /**
@@ -28,19 +28,36 @@ interface GuiOptions {
28
28
  * @default false
29
29
  */
30
30
  attachToSprite?: boolean;
31
+ /**
32
+ * Vue v4 compatibility flag. Prefer attachToSprite in v5 projects.
33
+ */
34
+ rpgAttachToSprite?: boolean;
31
35
  }
32
36
 
33
- interface GuiInstance {
37
+ export interface GuiInstance {
34
38
  name: string;
35
39
  component: any;
36
40
  display: WritableSignal<boolean>;
37
41
  data: WritableSignal<any>;
38
42
  autoDisplay: boolean;
39
- dependencies?: () => Signal[];
43
+ dependencies?: Signal[];
40
44
  subscription?: Subscription;
41
45
  attachToSprite?: boolean;
42
46
  }
43
47
 
48
+ type GuiState = {
49
+ name: string;
50
+ component: any;
51
+ display: boolean;
52
+ data: any;
53
+ attachToSprite: boolean;
54
+ };
55
+
56
+ type VueGuiBridge = {
57
+ updateGuiState?: (state: GuiState) => void;
58
+ initializeGuiStates?: (states: GuiState[]) => void;
59
+ };
60
+
44
61
  interface GuiAction {
45
62
  guiId: string;
46
63
  name: string;
@@ -117,7 +134,7 @@ export class RpgGui {
117
134
  private webSocket: AbstractWebsocket;
118
135
  gui = signal<Record<string, GuiInstance>>({});
119
136
  extraGuis: GuiInstance[] = [];
120
- private vueGuiInstance: any = null; // Reference to VueGui instance
137
+ private vueGuiInstance: VueGuiBridge | null = null;
121
138
  private optimisticReducers = new Map<string, OptimisticReducer[]>();
122
139
  private pendingActions = new Map<string, GuiAction[]>();
123
140
  /**
@@ -196,6 +213,7 @@ export class RpgGui {
196
213
  */
197
214
  _setVueGuiInstance(vueGuiInstance: any) {
198
215
  this.vueGuiInstance = vueGuiInstance;
216
+ this._initializeVueComponents();
199
217
  }
200
218
 
201
219
  /**
@@ -207,21 +225,9 @@ export class RpgGui {
207
225
  * @param data - Component data
208
226
  */
209
227
  private _notifyVueGui(guiId: string, display: boolean, data: any = {}) {
210
- if (this.vueGuiInstance && this.vueGuiInstance.vm) {
211
- // Find the GUI in extraGuis
212
- const extraGui = this.extraGuis.find(gui => gui.name === guiId);
213
- if (extraGui) {
214
- // Update the Vue component's display state and data
215
- this.vueGuiInstance.vm.gui[guiId] = {
216
- name: guiId,
217
- display,
218
- data,
219
- attachToSprite: extraGui.attachToSprite || false
220
- };
221
- // Trigger Vue reactivity
222
- this.vueGuiInstance.vm.gui = Object.assign({}, this.vueGuiInstance.vm.gui);
223
- }
224
- }
228
+ const extraGui = this.extraGuis.find(gui => gui.name === guiId);
229
+ if (!extraGui) return;
230
+ this.vueGuiInstance?.updateGuiState?.(this.toGuiState(extraGui, display, data));
225
231
  }
226
232
 
227
233
  /**
@@ -229,20 +235,9 @@ export class RpgGui {
229
235
  * This should be called after VueGui is mounted
230
236
  */
231
237
  _initializeVueComponents() {
232
- if (this.vueGuiInstance && this.vueGuiInstance.vm) {
233
- // Initialize all extraGuis in the Vue instance
234
- this.extraGuis.forEach(gui => {
235
- this.vueGuiInstance.vm.gui[gui.name] = {
236
- name: gui.name,
237
- display: gui.display(),
238
- data: gui.data(),
239
- attachToSprite: gui.attachToSprite || false
240
- };
241
- });
242
-
243
- // Trigger Vue reactivity
244
- this.vueGuiInstance.vm.gui = Object.assign({}, this.vueGuiInstance.vm.gui);
245
- }
238
+ this.vueGuiInstance?.initializeGuiStates?.(
239
+ this.extraGuis.map(gui => this.toGuiState(gui))
240
+ );
246
241
  }
247
242
 
248
243
  guiInteraction(guiId: string, name: string, data: any) {
@@ -301,35 +296,46 @@ export class RpgGui {
301
296
  * });
302
297
  * ```
303
298
  */
304
- add(gui: GuiOptions) {
305
- const guiId = gui.name || gui.id;
299
+ add(gui: GuiOptions | any) {
300
+ const component = this.resolveComponent(gui);
301
+ const guiId = this.resolveGuiId(gui, component);
306
302
  if (!guiId) {
307
303
  throw new Error("GUI must have a name or id");
308
304
  }
305
+ const attachToSprite = this.resolveAttachToSprite(gui, component);
309
306
  const guiInstance: GuiInstance = {
310
307
  name: guiId,
311
- component: gui.component,
312
- display: signal(gui.display || false),
313
- data: signal(gui.data || {}),
308
+ component,
309
+ display: signal<boolean>(gui.display || false),
310
+ data: signal<any>(gui.data || {}),
314
311
  autoDisplay: gui.autoDisplay || false,
315
312
  dependencies: gui.dependencies ? gui.dependencies() : [],
316
- attachToSprite: gui.attachToSprite || false,
313
+ attachToSprite,
317
314
  };
318
315
 
319
- // Accept both CanvasEngine components (.ce) and Vue components
320
- // Vue components will be handled by VueGui if available
321
- if (typeof gui.component !== 'function') {
322
- guiInstance.component = gui;
323
- this.extraGuis.push(guiInstance);
316
+ if (this.isVueComponentInstance(guiInstance)) {
317
+ this.removeCanvasGui(guiId);
318
+ const existingIndex = this.extraGuis.findIndex(existing => existing.name === guiId);
319
+ if (existingIndex >= 0) {
320
+ this.extraGuis[existingIndex].subscription?.unsubscribe();
321
+ this.extraGuis[existingIndex] = guiInstance;
322
+ } else {
323
+ this.extraGuis.push(guiInstance);
324
+ }
325
+
326
+ this._initializeVueComponents();
324
327
 
325
- // Auto display Vue components if enabled
326
328
  if (guiInstance.autoDisplay) {
327
- this._notifyVueGui(guiId, true, gui.data || {});
329
+ this.display(guiId, gui.data);
330
+ } else {
331
+ this._notifyVueGui(guiId, guiInstance.display(), guiInstance.data());
328
332
  }
329
333
  return;
330
334
  }
331
335
 
336
+ this.removeVueGui(guiId);
332
337
  this.gui()[guiId] = guiInstance;
338
+ this._initializeVueComponents();
333
339
 
334
340
  // Auto display if enabled and it's a CanvasEngine component
335
341
  if (guiInstance.autoDisplay && typeof gui.component === 'function') {
@@ -357,8 +363,15 @@ export class RpgGui {
357
363
  * ```
358
364
  */
359
365
  getAttachedGuis(): GuiInstance[] {
360
- const allGuis = this.getAll();
361
- return Object.values(allGuis).filter(gui => gui.attachToSprite === true);
366
+ return Object.values(this.gui()).filter(gui => gui.attachToSprite === true);
367
+ }
368
+
369
+ getVueGuis(): GuiInstance[] {
370
+ return [...this.extraGuis];
371
+ }
372
+
373
+ getAttachedVueGuis(): GuiInstance[] {
374
+ return this.extraGuis.filter(gui => gui.attachToSprite === true);
362
375
  }
363
376
 
364
377
  /**
@@ -461,7 +474,7 @@ export class RpgGui {
461
474
  // Use runtime dependencies or config dependencies
462
475
  const deps = dependencies.length > 0
463
476
  ? dependencies
464
- : (guiInstance.dependencies ? guiInstance.dependencies() : []);
477
+ : (guiInstance.dependencies ?? []);
465
478
 
466
479
  if (deps.length > 0) {
467
480
  // Subscribe to dependencies
@@ -522,6 +535,48 @@ export class RpgGui {
522
535
  return this.extraGuis.some(gui => gui.name === id);
523
536
  }
524
537
 
538
+ private isVueComponentInstance(gui: GuiInstance) {
539
+ return typeof gui.component !== "function";
540
+ }
541
+
542
+ private removeCanvasGui(guiId: string) {
543
+ const current = this.gui();
544
+ if (!(guiId in current)) return;
545
+ const next = { ...current };
546
+ delete next[guiId];
547
+ this.gui.set(next);
548
+ }
549
+
550
+ private removeVueGui(guiId: string) {
551
+ const removed = this.extraGuis.filter(existing => existing.name === guiId);
552
+ removed.forEach(gui => gui.subscription?.unsubscribe());
553
+ if (removed.length > 0) {
554
+ this.extraGuis = this.extraGuis.filter(existing => existing.name !== guiId);
555
+ }
556
+ }
557
+
558
+ private resolveComponent(gui: GuiOptions | any) {
559
+ return gui?.component ?? gui;
560
+ }
561
+
562
+ private resolveGuiId(gui: GuiOptions | any, component: any) {
563
+ return gui?.name || gui?.id || component?.name || component?.__name;
564
+ }
565
+
566
+ private resolveAttachToSprite(gui: GuiOptions | any, component: any) {
567
+ return !!(gui?.attachToSprite || gui?.rpgAttachToSprite || component?.attachToSprite || component?.rpgAttachToSprite);
568
+ }
569
+
570
+ private toGuiState(gui: GuiInstance, display = gui.display(), data = gui.data()): GuiState {
571
+ return {
572
+ name: gui.name,
573
+ component: gui.component,
574
+ display,
575
+ data,
576
+ attachToSprite: gui.attachToSprite || false,
577
+ };
578
+ }
579
+
525
580
  private clearPendingActions(guiId: string) {
526
581
  this.pendingActions.delete(guiId);
527
582
  }
package/src/Resource.ts CHANGED
@@ -64,7 +64,7 @@ export class RpgResource {
64
64
  // Extract image path from spritesheet
65
65
  const imageLink = spritesheet?.image || spritesheet?.imageSource || undefined;
66
66
  if (imageLink) {
67
- RpgResource._spritesheets.set(id, imageLink);
67
+ RpgResource._spritesheets.set(String(id), imageLink);
68
68
  }
69
69
  });
70
70
 
@@ -147,4 +147,3 @@ export class RpgResource {
147
147
  return RpgResource._sounds;
148
148
  }
149
149
  }
150
-
package/src/RpgClient.ts CHANGED
@@ -7,6 +7,24 @@ import { type MapPhysicsEntityContext, type MapPhysicsInitContext } from '@rpgjs
7
7
  type RpgClass<T = any> = new (...args: any[]) => T
8
8
  type RpgComponent = RpgClientObject
9
9
  type SceneMap = Container
10
+ export type SpriteComponentConfig = ComponentFunction | {
11
+ component: ComponentFunction
12
+ props?: Record<string, any> | ((object: RpgClientObject) => Record<string, any>)
13
+ data?: Record<string, any> | ((object: RpgClientObject) => Record<string, any>)
14
+ dependencies?: (object: RpgClientObject) => any[]
15
+ }
16
+
17
+ export interface RpgSpriteBeforeRemoveContext {
18
+ reason?: string
19
+ data?: any
20
+ transition?: {
21
+ animation?: string
22
+ graphic?: string | string[]
23
+ duration?: number
24
+ effect?: string
25
+ }
26
+ timeoutMs?: number
27
+ }
10
28
 
11
29
  export interface RpgClientEngineHooks {
12
30
  /**
@@ -81,7 +99,7 @@ export interface RpgSpriteHooks {
81
99
  * }
82
100
  * ```
83
101
  */
84
- componentsBehind?: ComponentFunction[]
102
+ componentsBehind?: SpriteComponentConfig[]
85
103
 
86
104
  /**
87
105
  * Array of components to render in front of the sprite
@@ -96,7 +114,28 @@ export interface RpgSpriteHooks {
96
114
  * }
97
115
  * ```
98
116
  */
99
- componentsInFront?: ComponentFunction[]
117
+ componentsInFront?: SpriteComponentConfig[]
118
+
119
+ /**
120
+ * Reusable sprite components addressable by server-side component definitions.
121
+ *
122
+ * The server sends only the component id and serializable props. The client
123
+ * registry maps that id to the CanvasEngine component that renders it.
124
+ *
125
+ * @prop {Record<string, ComponentFunction>} [components]
126
+ * @memberof RpgSpriteHooks
127
+ * @example
128
+ * ```ts
129
+ * import GuildBadge from './components/guild-badge.ce'
130
+ *
131
+ * const sprite: RpgSpriteHooks = {
132
+ * components: {
133
+ * guildBadge: GuildBadge
134
+ * }
135
+ * }
136
+ * ```
137
+ */
138
+ components?: Record<string, ComponentFunction>
100
139
 
101
140
  /**
102
141
  * As soon as the sprite is initialized
@@ -114,6 +153,21 @@ export interface RpgSpriteHooks {
114
153
  */
115
154
  onDestroy?: (sprite: RpgComponent) => any
116
155
 
156
+ /**
157
+ * Called when a sprite removal is requested, before it disappears from the scene.
158
+ *
159
+ * Return a promise to keep the sprite visible while an animation, effect, or
160
+ * sound transition is running. The server still owns gameplay removal and
161
+ * uses the timeout carried by the remove request as a safety limit.
162
+ *
163
+ * @prop { (sprite: RpgSprite, context: RpgSpriteBeforeRemoveContext) => any } [onBeforeRemove]
164
+ * @memberof RpgSpriteHooks
165
+ */
166
+ onBeforeRemove?: (
167
+ sprite: RpgComponent,
168
+ context: RpgSpriteBeforeRemoveContext
169
+ ) => any
170
+
117
171
  /**
118
172
  * As soon as a data is changed on the server side (the name for example), you are able to know the new data but also the old data.
119
173
  *
@@ -485,6 +539,13 @@ export interface RpgClient {
485
539
  * ```
486
540
  */
487
541
  attachToSprite?: boolean
542
+ /**
543
+ * Vue v4 compatibility alias for `attachToSprite`.
544
+ *
545
+ * Prefer `attachToSprite` in v5 projects. This is read by `@rpgjs/vue`
546
+ * for Vue GUI components migrated from the v4 GUI API.
547
+ */
548
+ rpgAttachToSprite?: boolean
488
549
  } | any)[],
489
550
 
490
551
  /**
@@ -1,6 +1,6 @@
1
1
  import Canvas from "./components/scenes/canvas.ce";
2
2
  import { inject } from './core/inject'
3
- import { signal, bootstrapCanvas, Howl, trigger } from "canvasengine";
3
+ import { signal, bootstrapCanvas, Howl, trigger, type Trigger } from "canvasengine";
4
4
  import { AbstractWebsocket, WebSocketToken } from "./services/AbstractSocket";
5
5
  import { LoadMapService, LoadMapToken } from "./services/loadMap";
6
6
  import { RpgSound } from "./Sound";
@@ -14,6 +14,10 @@ import { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, swit
14
14
  import { GlobalConfigToken } from "./module";
15
15
  import * as PIXI from "pixi.js";
16
16
  import { PrebuiltComponentAnimations } from "./components/animations";
17
+ import TextComponent from "./components/dynamics/text.ce";
18
+ import BarComponent from "./components/dynamics/bar.ce";
19
+ import ShapeComponent from "./components/dynamics/shape.ce";
20
+ import ImageComponent from "./components/dynamics/image.ce";
17
21
  import {
18
22
  PredictionController,
19
23
  type PredictionHistoryEntry,
@@ -32,6 +36,17 @@ interface MovementTrajectoryPoint {
32
36
  direction?: Direction;
33
37
  }
34
38
 
39
+ type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
40
+ start(config?: T): Promise<void>;
41
+ };
42
+
43
+ type MapShakeOptions = {
44
+ intensity?: number;
45
+ duration?: number;
46
+ frequency?: number;
47
+ direction?: string;
48
+ };
49
+
35
50
  export class RpgClientEngine<T = any> {
36
51
  private guiService: RpgGui;
37
52
  private webSocket: AbstractWebsocket;
@@ -44,10 +59,10 @@ export class RpgClientEngine<T = any> {
44
59
  stopProcessingInput = false;
45
60
  width = signal("100%");
46
61
  height = signal("100%");
47
- spritesheets: Map<string, any> = new Map();
62
+ spritesheets: Map<string | number, any> = new Map();
48
63
  sounds: Map<string, any> = new Map();
49
64
  componentAnimations: any[] = [];
50
- private spritesheetResolver?: (id: string) => any | Promise<any>;
65
+ private spritesheetResolver?: (id: string | number) => any | Promise<any>;
51
66
  private soundResolver?: (id: string) => any | Promise<any>;
52
67
  particleSettings: {
53
68
  emitters: any[]
@@ -61,10 +76,11 @@ export class RpgClientEngine<T = any> {
61
76
  playerIdSignal = signal<string | null>(null);
62
77
  spriteComponentsBehind = signal<any[]>([]);
63
78
  spriteComponentsInFront = signal<any[]>([]);
79
+ spriteComponents: Map<string, any> = new Map();
64
80
  /** ID of the sprite that the camera should follow. null means follow the current player */
65
81
  cameraFollowTargetId = signal<string | null>(null);
66
82
  /** Trigger for map shake animation */
67
- mapShakeTrigger = trigger();
83
+ mapShakeTrigger: ConfigurableTrigger<MapShakeOptions> = trigger<MapShakeOptions>();
68
84
 
69
85
  controlsReady = signal(undefined);
70
86
  gamePause = signal(false);
@@ -123,6 +139,13 @@ export class RpgClientEngine<T = any> {
123
139
  component: PrebuiltComponentAnimations.Animation
124
140
  })
125
141
 
142
+ this.registerSpriteComponent("rpg:text", TextComponent);
143
+ this.registerSpriteComponent("rpg:hpBar", BarComponent);
144
+ this.registerSpriteComponent("rpg:spBar", BarComponent);
145
+ this.registerSpriteComponent("rpg:bar", BarComponent);
146
+ this.registerSpriteComponent("rpg:shape", ShapeComponent);
147
+ this.registerSpriteComponent("rpg:image", ImageComponent);
148
+
126
149
  this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;
127
150
  this.initializePredictionController();
128
151
  }
@@ -233,7 +256,7 @@ export class RpgClientEngine<T = any> {
233
256
 
234
257
  await this.webSocket.connection(() => {
235
258
  const saveClient = inject(SaveClientService);
236
- saveClient.initialize(this.webSocket);
259
+ saveClient.initialize();
237
260
  this.initListeners()
238
261
  this.guiService._initialize()
239
262
  this.startPingPong();
@@ -425,7 +448,7 @@ export class RpgClientEngine<T = any> {
425
448
 
426
449
  this.webSocket.on("shakeMap", (data) => {
427
450
  const { intensity, duration, frequency, direction } = data || {};
428
- (this.mapShakeTrigger as any).start({
451
+ this.mapShakeTrigger.start({
429
452
  intensity,
430
453
  duration,
431
454
  frequency,
@@ -574,7 +597,7 @@ export class RpgClientEngine<T = any> {
574
597
  })
575
598
  await this.webSocket.reconnect(() => {
576
599
  const saveClient = inject(SaveClientService);
577
- saveClient.initialize(this.webSocket);
600
+ saveClient.initialize();
578
601
  this.initListeners()
579
602
  this.guiService._initialize()
580
603
  })
@@ -635,7 +658,7 @@ export class RpgClientEngine<T = any> {
635
658
  * });
636
659
  * ```
637
660
  */
638
- setSpritesheetResolver(resolver: (id: string) => any | Promise<any>): void {
661
+ setSpritesheetResolver(resolver: (id: string | number) => any | Promise<any>): void {
639
662
  this.spritesheetResolver = resolver;
640
663
  }
641
664
 
@@ -646,7 +669,7 @@ export class RpgClientEngine<T = any> {
646
669
  * If not found and a resolver is set, it calls the resolver to create the spritesheet.
647
670
  * The resolved spritesheet is automatically cached for future use.
648
671
  *
649
- * @param id - The spritesheet ID to retrieve
672
+ * @param id - The spritesheet ID or legacy tile ID to retrieve
650
673
  * @returns The spritesheet if found or created, or undefined if not found and no resolver
651
674
  * @returns Promise<any> if the resolver is asynchronous
652
675
  *
@@ -659,7 +682,7 @@ export class RpgClientEngine<T = any> {
659
682
  * const spritesheet = await engine.getSpriteSheet('dynamic-sprite');
660
683
  * ```
661
684
  */
662
- getSpriteSheet(id: string): any | Promise<any> {
685
+ getSpriteSheet(id: string | number): any | Promise<any> {
663
686
  // Check cache first
664
687
  if (this.spritesheets.has(id)) {
665
688
  return this.spritesheets.get(id);
@@ -1093,6 +1116,37 @@ export class RpgClientEngine<T = any> {
1093
1116
  return component
1094
1117
  }
1095
1118
 
1119
+ /**
1120
+ * Register a reusable sprite component that can be addressed by the server.
1121
+ *
1122
+ * Server-side component definitions only carry the component id and
1123
+ * serializable props. The client registry maps that id to the CanvasEngine
1124
+ * component that performs the actual rendering.
1125
+ *
1126
+ * @param id - Stable component id used by server component definitions
1127
+ * @param component - CanvasEngine component to render for this id
1128
+ * @returns The registered component
1129
+ *
1130
+ * @example
1131
+ * ```ts
1132
+ * engine.registerSpriteComponent('guildBadge', GuildBadgeComponent);
1133
+ * ```
1134
+ */
1135
+ registerSpriteComponent(id: string, component: any) {
1136
+ this.spriteComponents.set(id, component);
1137
+ return component;
1138
+ }
1139
+
1140
+ /**
1141
+ * Get a reusable sprite component by id.
1142
+ *
1143
+ * @param id - Component id registered on the client
1144
+ * @returns The CanvasEngine component, or undefined when missing
1145
+ */
1146
+ getSpriteComponent(id: string) {
1147
+ return this.spriteComponents.get(id);
1148
+ }
1149
+
1096
1150
  /**
1097
1151
  * Add a component animation to the engine
1098
1152
  *
@@ -1176,13 +1230,29 @@ export class RpgClientEngine<T = any> {
1176
1230
  * duration: 1000,
1177
1231
  * onFinish: () => console.log('Fade complete')
1178
1232
  * });
1233
+ *
1234
+ * // Wait until the transition component calls onFinish
1235
+ * await engine.startTransition('fade', { duration: 1000 });
1179
1236
  * ```
1180
1237
  */
1181
- startTransition(id: string, props: any = {}) {
1238
+ startTransition(id: string, props: any = {}): Promise<void> {
1182
1239
  if (!this.guiService.exists(id)) {
1183
1240
  throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);
1184
1241
  }
1185
- this.guiService.display(id, props);
1242
+ return new Promise<void>((resolve) => {
1243
+ let finished = false;
1244
+ const finish = (data?: any) => {
1245
+ if (finished) return;
1246
+ finished = true;
1247
+ props?.onFinish?.(data);
1248
+ resolve();
1249
+ };
1250
+
1251
+ this.guiService.display(id, {
1252
+ ...props,
1253
+ onFinish: finish,
1254
+ });
1255
+ });
1186
1256
  }
1187
1257
 
1188
1258
  async processInput({ input }: { input: Direction }) {