@rpgjs/client 5.0.0-beta.7 → 5.0.0-beta.9

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 (158) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/Game/AnimationManager.js.map +1 -1
  3. package/dist/Game/Event.js.map +1 -1
  4. package/dist/Game/Map.d.ts +9 -1
  5. package/dist/Game/Map.js +63 -5
  6. package/dist/Game/Map.js.map +1 -1
  7. package/dist/Game/Object.d.ts +17 -9
  8. package/dist/Game/Object.js +1 -12
  9. package/dist/Game/Object.js.map +1 -1
  10. package/dist/Game/Player.js.map +1 -1
  11. package/dist/Gui/Gui.d.ts +17 -4
  12. package/dist/Gui/Gui.js +64 -34
  13. package/dist/Gui/Gui.js.map +1 -1
  14. package/dist/Gui/Gui.spec.d.ts +1 -0
  15. package/dist/Gui/NotificationManager.js.map +1 -1
  16. package/dist/Resource.js +1 -1
  17. package/dist/Resource.js.map +1 -1
  18. package/dist/RpgClient.d.ts +35 -2
  19. package/dist/RpgClientEngine.d.ts +41 -5
  20. package/dist/RpgClientEngine.js +50 -5
  21. package/dist/RpgClientEngine.js.map +1 -1
  22. package/dist/Sound.js.map +1 -1
  23. package/dist/_virtual/{_@oxc-project_runtime@0.128.0 → _@oxc-project_runtime@0.130.0}/helpers/decorate.js +1 -1
  24. package/dist/_virtual/{_@oxc-project_runtime@0.128.0 → _@oxc-project_runtime@0.130.0}/helpers/decorateMetadata.js +1 -1
  25. package/dist/components/animations/animation.ce.js.map +1 -1
  26. package/dist/components/animations/hit.ce.js.map +1 -1
  27. package/dist/components/animations/index.js.map +1 -1
  28. package/dist/components/character.ce.js +259 -5
  29. package/dist/components/character.ce.js.map +1 -1
  30. package/dist/components/dynamics/bar.ce.js +96 -0
  31. package/dist/components/dynamics/bar.ce.js.map +1 -0
  32. package/dist/components/dynamics/image.ce.js +23 -0
  33. package/dist/components/dynamics/image.ce.js.map +1 -0
  34. package/dist/components/dynamics/parse-value.d.ts +3 -0
  35. package/dist/components/dynamics/parse-value.js +51 -35
  36. package/dist/components/dynamics/parse-value.js.map +1 -1
  37. package/dist/components/dynamics/parse-value.spec.d.ts +1 -0
  38. package/dist/components/dynamics/shape-utils.d.ts +16 -0
  39. package/dist/components/dynamics/shape-utils.js +73 -0
  40. package/dist/components/dynamics/shape-utils.js.map +1 -0
  41. package/dist/components/dynamics/shape-utils.spec.d.ts +1 -0
  42. package/dist/components/dynamics/shape.ce.js +83 -0
  43. package/dist/components/dynamics/shape.ce.js.map +1 -0
  44. package/dist/components/dynamics/text.ce.js +28 -41
  45. package/dist/components/dynamics/text.ce.js.map +1 -1
  46. package/dist/components/gui/box.ce.js.map +1 -1
  47. package/dist/components/gui/dialogbox/index.ce.js +3 -3
  48. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  49. package/dist/components/gui/gameover.ce.js +1 -1
  50. package/dist/components/gui/gameover.ce.js.map +1 -1
  51. package/dist/components/gui/hud/hud.ce.js +1 -1
  52. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  53. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  54. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  55. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  56. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  57. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  58. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  59. package/dist/components/gui/mobile/index.js.map +1 -1
  60. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  61. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  62. package/dist/components/gui/save-load.ce.js.map +1 -1
  63. package/dist/components/gui/shop/shop.ce.js +1 -1
  64. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  65. package/dist/components/gui/title-screen.ce.js +2 -2
  66. package/dist/components/gui/title-screen.ce.js.map +1 -1
  67. package/dist/components/player-components-utils.d.ts +67 -0
  68. package/dist/components/player-components-utils.js +162 -0
  69. package/dist/components/player-components-utils.js.map +1 -0
  70. package/dist/components/player-components-utils.spec.d.ts +1 -0
  71. package/dist/components/player-components.ce.js +188 -0
  72. package/dist/components/player-components.ce.js.map +1 -0
  73. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  74. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  75. package/dist/components/scenes/canvas.ce.js +147 -4
  76. package/dist/components/scenes/canvas.ce.js.map +1 -1
  77. package/dist/components/scenes/draw-map.ce.js +2 -8
  78. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  79. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  80. package/dist/core/inject.js +1 -1
  81. package/dist/core/inject.js.map +1 -1
  82. package/dist/core/setup.js +1 -1
  83. package/dist/core/setup.js.map +1 -1
  84. package/dist/decorators/spritesheet.d.ts +1 -0
  85. package/dist/decorators/spritesheet.js +11 -0
  86. package/dist/decorators/spritesheet.js.map +1 -0
  87. package/dist/index.d.ts +1 -0
  88. package/dist/index.js +3 -2
  89. package/dist/module.js +4 -1
  90. package/dist/module.js.map +1 -1
  91. package/dist/node_modules/.pnpm/{@signe_di@2.10.0 → @signe_di@3.0.1}/node_modules/@signe/di/dist/index.js +1 -1
  92. package/dist/node_modules/.pnpm/@signe_di@3.0.1/node_modules/@signe/di/dist/index.js.map +1 -0
  93. package/dist/node_modules/.pnpm/{@signe_reactive@2.9.2 → @signe_reactive@3.0.1}/node_modules/@signe/reactive/dist/index.js +15 -3
  94. package/dist/node_modules/.pnpm/@signe_reactive@3.0.1/node_modules/@signe/reactive/dist/index.js.map +1 -0
  95. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js +13 -0
  96. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js.map +1 -0
  97. package/dist/node_modules/.pnpm/{@signe_room@2.10.0 → @signe_room@3.0.1}/node_modules/@signe/room/dist/index.js +124 -39
  98. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/index.js.map +1 -0
  99. package/dist/node_modules/.pnpm/{@signe_sync@2.10.0 → @signe_sync@3.0.1}/node_modules/@signe/sync/dist/client/index.js +1 -1
  100. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/client/index.js.map +1 -0
  101. package/dist/node_modules/.pnpm/{@signe_sync@2.10.0 → @signe_sync@3.0.1}/node_modules/@signe/sync/dist/index.js +36 -13
  102. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/index.js.map +1 -0
  103. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -1
  104. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -1
  105. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -1
  106. package/dist/presets/animation.js.map +1 -1
  107. package/dist/presets/faceset.js.map +1 -1
  108. package/dist/presets/icon.js.map +1 -1
  109. package/dist/presets/index.js.map +1 -1
  110. package/dist/presets/lpc.js.map +1 -1
  111. package/dist/presets/rmspritesheet.js.map +1 -1
  112. package/dist/services/AbstractSocket.js.map +1 -1
  113. package/dist/services/keyboardControls.js.map +1 -1
  114. package/dist/services/loadMap.d.ts +6 -0
  115. package/dist/services/loadMap.js +1 -1
  116. package/dist/services/loadMap.js.map +1 -1
  117. package/dist/services/mmorpg.js +7 -3
  118. package/dist/services/mmorpg.js.map +1 -1
  119. package/dist/services/save.js.map +1 -1
  120. package/dist/services/standalone.js +1 -1
  121. package/dist/services/standalone.js.map +1 -1
  122. package/dist/utils/getEntityProp.js.map +1 -1
  123. package/package.json +10 -10
  124. package/src/Game/Map.ts +91 -2
  125. package/src/Game/Object.ts +22 -35
  126. package/src/Gui/Gui.spec.ts +273 -0
  127. package/src/Gui/Gui.ts +105 -50
  128. package/src/Resource.ts +1 -2
  129. package/src/RpgClient.ts +36 -2
  130. package/src/RpgClientEngine.ts +74 -11
  131. package/src/components/character.ce +318 -9
  132. package/src/components/dynamics/bar.ce +87 -0
  133. package/src/components/dynamics/image.ce +20 -0
  134. package/src/components/dynamics/parse-value.spec.ts +41 -0
  135. package/src/components/dynamics/parse-value.ts +102 -37
  136. package/src/components/dynamics/shape-utils.spec.ts +46 -0
  137. package/src/components/dynamics/shape-utils.ts +61 -0
  138. package/src/components/dynamics/shape.ce +89 -0
  139. package/src/components/dynamics/text.ce +34 -149
  140. package/src/components/player-components-utils.spec.ts +109 -0
  141. package/src/components/player-components-utils.ts +205 -0
  142. package/src/components/player-components.ce +221 -0
  143. package/src/components/scenes/canvas.ce +165 -6
  144. package/src/components/scenes/draw-map.ce +2 -15
  145. package/src/components/scenes/event-layer.ce +1 -2
  146. package/src/core/setup.ts +2 -2
  147. package/src/decorators/spritesheet.ts +8 -0
  148. package/src/index.ts +1 -0
  149. package/src/module.ts +5 -1
  150. package/src/services/loadMap.ts +2 -0
  151. package/src/services/mmorpg.ts +8 -2
  152. package/dist/node_modules/.pnpm/@signe_di@2.10.0/node_modules/@signe/di/dist/index.js.map +0 -1
  153. package/dist/node_modules/.pnpm/@signe_reactive@2.10.0/node_modules/@signe/reactive/dist/index.js +0 -45
  154. package/dist/node_modules/.pnpm/@signe_reactive@2.10.0/node_modules/@signe/reactive/dist/index.js.map +0 -1
  155. package/dist/node_modules/.pnpm/@signe_reactive@2.9.2/node_modules/@signe/reactive/dist/index.js.map +0 -1
  156. package/dist/node_modules/.pnpm/@signe_room@2.10.0/node_modules/@signe/room/dist/index.js.map +0 -1
  157. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/client/index.js.map +0 -1
  158. package/dist/node_modules/.pnpm/@signe_sync@2.10.0/node_modules/@signe/sync/dist/index.js.map +0 -1
@@ -0,0 +1,273 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { Context, injector } from "@signe/di";
3
+ import { signal } from "canvasengine";
4
+ import { PrebuiltGui } from "@rpgjs/common";
5
+ import { WebSocketToken } from "../services/AbstractSocket";
6
+
7
+ vi.mock("../components/gui", () => {
8
+ const component = () => null;
9
+ return {
10
+ DialogboxComponent: component,
11
+ ShopComponent: component,
12
+ SaveLoadComponent: component,
13
+ MainMenuComponent: component,
14
+ NotificationComponent: component,
15
+ TitleScreenComponent: component,
16
+ GameoverComponent: component,
17
+ };
18
+ });
19
+
20
+ const createGui = async () => {
21
+ const { RpgGui } = await import("./Gui");
22
+ const context = new Context();
23
+ const socket = {
24
+ on: vi.fn(),
25
+ emit: vi.fn(),
26
+ };
27
+ await injector(context, [
28
+ {
29
+ provide: WebSocketToken,
30
+ useValue: socket,
31
+ },
32
+ ]);
33
+ return {
34
+ gui: new RpgGui(context),
35
+ socket,
36
+ };
37
+ };
38
+
39
+ const CanvasGui = () => null;
40
+ const VueInventory = {
41
+ name: "inventory",
42
+ render() {
43
+ return null;
44
+ },
45
+ };
46
+ const VueDialog = {
47
+ name: PrebuiltGui.Dialog,
48
+ render() {
49
+ return null;
50
+ },
51
+ };
52
+ const VueMainMenu = {
53
+ name: PrebuiltGui.MainMenu,
54
+ render() {
55
+ return null;
56
+ },
57
+ };
58
+ const VueTooltip = {
59
+ name: "tooltip",
60
+ rpgAttachToSprite: true,
61
+ render() {
62
+ return null;
63
+ },
64
+ };
65
+
66
+ describe("RpgGui Vue integration", () => {
67
+ test("separates CanvasEngine and Vue GUI registries", async () => {
68
+ const { gui } = await createGui();
69
+
70
+ gui.add({
71
+ id: "canvas-tooltip",
72
+ component: CanvasGui,
73
+ attachToSprite: true,
74
+ });
75
+ gui.add({
76
+ id: "inventory",
77
+ component: VueInventory,
78
+ });
79
+ gui.add(VueTooltip);
80
+
81
+ expect(gui.get("canvas-tooltip")?.component).toBe(CanvasGui);
82
+ expect(gui.get("inventory")?.component).toBe(VueInventory);
83
+ expect(gui.get("tooltip")?.component).toBe(VueTooltip);
84
+ expect(gui.getAttachedGuis().map(item => item.name)).toEqual(["canvas-tooltip"]);
85
+ expect(gui.getAttachedVueGuis().map(item => item.name)).toEqual(["tooltip"]);
86
+ });
87
+
88
+ test("synchronizes Vue GUI display and hide states through the Vue bridge", async () => {
89
+ const { gui } = await createGui();
90
+ const bridge = {
91
+ updateGuiState: vi.fn(),
92
+ initializeGuiStates: vi.fn(),
93
+ };
94
+
95
+ gui.add({
96
+ id: "inventory",
97
+ component: VueInventory,
98
+ });
99
+ gui._setVueGuiInstance(bridge);
100
+
101
+ expect(bridge.initializeGuiStates).toHaveBeenCalledWith([
102
+ expect.objectContaining({
103
+ name: "inventory",
104
+ display: false,
105
+ data: {},
106
+ attachToSprite: false,
107
+ }),
108
+ ]);
109
+
110
+ gui.display("inventory", { gold: 12 });
111
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
112
+ expect.objectContaining({
113
+ name: "inventory",
114
+ display: true,
115
+ data: { gold: 12 },
116
+ attachToSprite: false,
117
+ }),
118
+ );
119
+
120
+ gui.hide("inventory");
121
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
122
+ expect.objectContaining({
123
+ name: "inventory",
124
+ display: false,
125
+ }),
126
+ );
127
+ });
128
+
129
+ test("waits for Vue GUI dependencies before display", async () => {
130
+ const { gui } = await createGui();
131
+ const bridge = {
132
+ updateGuiState: vi.fn(),
133
+ initializeGuiStates: vi.fn(),
134
+ };
135
+ const dependency = signal<any>(undefined);
136
+
137
+ gui.add({
138
+ id: "inventory",
139
+ component: VueInventory,
140
+ dependencies: () => [dependency],
141
+ });
142
+ gui._setVueGuiInstance(bridge);
143
+ gui.display("inventory", { items: ["potion"] });
144
+
145
+ expect(gui.isDisplaying("inventory")).toBe(false);
146
+ expect(bridge.updateGuiState).not.toHaveBeenCalledWith(
147
+ expect.objectContaining({
148
+ display: true,
149
+ }),
150
+ );
151
+
152
+ dependency.set({ id: "player" });
153
+
154
+ expect(gui.isDisplaying("inventory")).toBe(true);
155
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
156
+ expect.objectContaining({
157
+ name: "inventory",
158
+ display: true,
159
+ data: { items: ["potion"] },
160
+ }),
161
+ );
162
+ });
163
+
164
+ test("allows Vue GUI entries to replace prebuilt CanvasEngine GUIs", async () => {
165
+ const { gui } = await createGui();
166
+ const bridge = {
167
+ updateGuiState: vi.fn(),
168
+ initializeGuiStates: vi.fn(),
169
+ };
170
+
171
+ gui._setVueGuiInstance(bridge);
172
+ gui.add({
173
+ id: PrebuiltGui.Dialog,
174
+ component: VueDialog,
175
+ });
176
+
177
+ expect(gui.get(PrebuiltGui.Dialog)?.component).toBe(VueDialog);
178
+ expect(gui.getAll()[PrebuiltGui.Dialog].component).toBe(VueDialog);
179
+ expect((gui as any).gui()[PrebuiltGui.Dialog]).toBeUndefined();
180
+ expect(gui.getVueGuis().filter(item => item.name === PrebuiltGui.Dialog)).toHaveLength(1);
181
+
182
+ gui.display(PrebuiltGui.Dialog, { text: "Hello" });
183
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
184
+ expect.objectContaining({
185
+ name: PrebuiltGui.Dialog,
186
+ display: true,
187
+ data: { text: "Hello" },
188
+ }),
189
+ );
190
+
191
+ gui.hide(PrebuiltGui.Dialog);
192
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
193
+ expect.objectContaining({
194
+ name: PrebuiltGui.Dialog,
195
+ display: false,
196
+ }),
197
+ );
198
+ });
199
+
200
+ test("allows CanvasEngine GUI entries to replace Vue GUI entries with the same id", async () => {
201
+ const { gui } = await createGui();
202
+ const bridge = {
203
+ updateGuiState: vi.fn(),
204
+ initializeGuiStates: vi.fn(),
205
+ };
206
+
207
+ gui._setVueGuiInstance(bridge);
208
+ gui.add({
209
+ id: PrebuiltGui.Dialog,
210
+ component: VueDialog,
211
+ });
212
+ gui.add({
213
+ id: PrebuiltGui.Dialog,
214
+ component: CanvasGui,
215
+ });
216
+
217
+ expect(gui.get(PrebuiltGui.Dialog)?.component).toBe(CanvasGui);
218
+ expect(gui.getVueGuis().some(item => item.name === PrebuiltGui.Dialog)).toBe(false);
219
+ expect((gui as any).gui()[PrebuiltGui.Dialog].component).toBe(CanvasGui);
220
+ expect(bridge.initializeGuiStates).toHaveBeenLastCalledWith([]);
221
+ });
222
+
223
+ test("keeps main menu optimistic reducers when a Vue GUI replaces the prebuilt component", async () => {
224
+ const { gui, socket } = await createGui();
225
+ const bridge = {
226
+ updateGuiState: vi.fn(),
227
+ initializeGuiStates: vi.fn(),
228
+ };
229
+
230
+ gui.add({
231
+ id: PrebuiltGui.MainMenu,
232
+ component: VueMainMenu,
233
+ });
234
+ gui._setVueGuiInstance(bridge);
235
+ gui.display(PrebuiltGui.MainMenu, {
236
+ items: [
237
+ {
238
+ id: "potion",
239
+ quantity: 2,
240
+ },
241
+ ],
242
+ });
243
+
244
+ gui.guiInteraction(PrebuiltGui.MainMenu, "useItem", { id: "potion" });
245
+
246
+ expect(gui.get(PrebuiltGui.MainMenu)?.data().items).toEqual([
247
+ {
248
+ id: "potion",
249
+ quantity: 1,
250
+ },
251
+ ]);
252
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
253
+ expect.objectContaining({
254
+ name: PrebuiltGui.MainMenu,
255
+ data: {
256
+ items: [
257
+ {
258
+ id: "potion",
259
+ quantity: 1,
260
+ },
261
+ ],
262
+ },
263
+ }),
264
+ );
265
+ expect(socket.emit).toHaveBeenCalledWith(
266
+ "gui.interaction",
267
+ expect.objectContaining({
268
+ guiId: PrebuiltGui.MainMenu,
269
+ name: "useItem",
270
+ }),
271
+ );
272
+ });
273
+ });
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,12 @@ 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
+ }
10
16
 
11
17
  export interface RpgSpriteBeforeRemoveContext {
12
18
  reason?: string
@@ -93,7 +99,7 @@ export interface RpgSpriteHooks {
93
99
  * }
94
100
  * ```
95
101
  */
96
- componentsBehind?: ComponentFunction[]
102
+ componentsBehind?: SpriteComponentConfig[]
97
103
 
98
104
  /**
99
105
  * Array of components to render in front of the sprite
@@ -108,7 +114,28 @@ export interface RpgSpriteHooks {
108
114
  * }
109
115
  * ```
110
116
  */
111
- 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>
112
139
 
113
140
  /**
114
141
  * As soon as the sprite is initialized
@@ -512,6 +539,13 @@ export interface RpgClient {
512
539
  * ```
513
540
  */
514
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
515
549
  } | any)[],
516
550
 
517
551
  /**