@rpgjs/client 5.0.0-alpha.8 → 5.0.0-beta.1

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 (305) hide show
  1. package/dist/Game/AnimationManager.d.ts +8 -0
  2. package/dist/Game/AnimationManager.js +26 -0
  3. package/dist/Game/AnimationManager.js.map +1 -0
  4. package/dist/Game/Event.d.ts +1 -1
  5. package/dist/Game/Event.js +12 -0
  6. package/dist/Game/Event.js.map +1 -0
  7. package/dist/Game/Map.d.ts +23 -2
  8. package/dist/Game/Map.js +80 -0
  9. package/dist/Game/Map.js.map +1 -0
  10. package/dist/Game/Object.d.ts +157 -0
  11. package/dist/Game/Object.js +211 -0
  12. package/dist/Game/Object.js.map +1 -0
  13. package/dist/Game/Player.d.ts +1 -1
  14. package/dist/Game/Player.js +12 -0
  15. package/dist/Game/Player.js.map +1 -0
  16. package/dist/Gui/Gui.d.ts +177 -5
  17. package/dist/Gui/Gui.js +445 -0
  18. package/dist/Gui/Gui.js.map +1 -0
  19. package/dist/Gui/NotificationManager.d.ts +23 -0
  20. package/dist/Gui/NotificationManager.js +49 -0
  21. package/dist/Gui/NotificationManager.js.map +1 -0
  22. package/dist/Resource.d.ts +97 -0
  23. package/dist/Resource.js +133 -0
  24. package/dist/Resource.js.map +1 -0
  25. package/dist/RpgClient.d.ts +290 -58
  26. package/dist/RpgClientEngine.d.ts +649 -9
  27. package/dist/RpgClientEngine.js +1334 -0
  28. package/dist/RpgClientEngine.js.map +1 -0
  29. package/dist/Sound.d.ts +199 -0
  30. package/dist/Sound.js +167 -0
  31. package/dist/Sound.js.map +1 -0
  32. package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/decorate.js +9 -0
  33. package/dist/_virtual/_@oxc-project_runtime@0.122.0/helpers/decorateMetadata.js +6 -0
  34. package/dist/components/animations/animation.ce.js +24 -0
  35. package/dist/components/animations/animation.ce.js.map +1 -0
  36. package/dist/components/animations/hit.ce.js +70 -0
  37. package/dist/components/animations/hit.ce.js.map +1 -0
  38. package/dist/components/animations/index.d.ts +4 -0
  39. package/dist/components/animations/index.js +11 -0
  40. package/dist/components/animations/index.js.map +1 -0
  41. package/dist/components/character.ce.js +392 -0
  42. package/dist/components/character.ce.js.map +1 -0
  43. package/dist/components/dynamics/parse-value.d.ts +1 -0
  44. package/dist/components/dynamics/parse-value.js +44 -0
  45. package/dist/components/dynamics/parse-value.js.map +1 -0
  46. package/dist/components/dynamics/text.ce.js +73 -0
  47. package/dist/components/dynamics/text.ce.js.map +1 -0
  48. package/dist/components/gui/box.ce.js +28 -0
  49. package/dist/components/gui/box.ce.js.map +1 -0
  50. package/dist/components/gui/dialogbox/index.ce.js +205 -0
  51. package/dist/components/gui/dialogbox/index.ce.js.map +1 -0
  52. package/dist/components/gui/gameover.ce.js +193 -0
  53. package/dist/components/gui/gameover.ce.js.map +1 -0
  54. package/dist/components/gui/hud/hud.ce.js +92 -0
  55. package/dist/components/gui/hud/hud.ce.js.map +1 -0
  56. package/dist/components/gui/index.d.ts +15 -3
  57. package/dist/components/gui/index.js +14 -0
  58. package/dist/components/gui/menu/equip-menu.ce.js +481 -0
  59. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -0
  60. package/dist/components/gui/menu/exit-menu.ce.js +54 -0
  61. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -0
  62. package/dist/components/gui/menu/items-menu.ce.js +344 -0
  63. package/dist/components/gui/menu/items-menu.ce.js.map +1 -0
  64. package/dist/components/gui/menu/main-menu.ce.js +417 -0
  65. package/dist/components/gui/menu/main-menu.ce.js.map +1 -0
  66. package/dist/components/gui/menu/options-menu.ce.js +48 -0
  67. package/dist/components/gui/menu/options-menu.ce.js.map +1 -0
  68. package/dist/components/gui/menu/skills-menu.ce.js +107 -0
  69. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -0
  70. package/dist/components/gui/mobile/index.d.ts +8 -0
  71. package/dist/components/gui/mobile/index.js +21 -0
  72. package/dist/components/gui/mobile/index.js.map +1 -0
  73. package/dist/components/gui/mobile/mobile.ce.js +78 -0
  74. package/dist/components/gui/mobile/mobile.ce.js.map +1 -0
  75. package/dist/components/gui/notification/notification.ce.js +64 -0
  76. package/dist/components/gui/notification/notification.ce.js.map +1 -0
  77. package/dist/components/gui/save-load.ce.js +389 -0
  78. package/dist/components/gui/save-load.ce.js.map +1 -0
  79. package/dist/components/gui/shop/shop.ce.js +652 -0
  80. package/dist/components/gui/shop/shop.ce.js.map +1 -0
  81. package/dist/components/gui/title-screen.ce.js +190 -0
  82. package/dist/components/gui/title-screen.ce.js.map +1 -0
  83. package/dist/components/index.d.ts +1 -0
  84. package/dist/components/index.js +4 -0
  85. package/dist/components/prebuilt/hp-bar.ce.js +116 -0
  86. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -0
  87. package/dist/components/prebuilt/index.d.ts +19 -0
  88. package/dist/components/prebuilt/index.js +2 -0
  89. package/dist/components/prebuilt/light-halo.ce.js +94 -0
  90. package/dist/components/prebuilt/light-halo.ce.js.map +1 -0
  91. package/dist/components/scenes/canvas.ce.js +60 -0
  92. package/dist/components/scenes/canvas.ce.js.map +1 -0
  93. package/dist/components/scenes/draw-map.ce.js +89 -0
  94. package/dist/components/scenes/draw-map.ce.js.map +1 -0
  95. package/dist/components/scenes/event-layer.ce.js +28 -0
  96. package/dist/components/scenes/event-layer.ce.js.map +1 -0
  97. package/dist/core/inject.js +18 -0
  98. package/dist/core/inject.js.map +1 -0
  99. package/dist/core/setup.js +16 -0
  100. package/dist/core/setup.js.map +1 -0
  101. package/dist/index.d.ts +15 -1
  102. package/dist/index.js +44 -14
  103. package/dist/module.d.ts +43 -4
  104. package/dist/module.js +176 -0
  105. package/dist/module.js.map +1 -0
  106. package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js +277 -0
  107. package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +1 -0
  108. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js +457 -0
  109. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js.map +1 -0
  110. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js +463 -0
  111. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js.map +1 -0
  112. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js +2191 -0
  113. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js.map +1 -0
  114. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +10 -0
  115. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +1 -0
  116. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js +91 -0
  117. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js.map +1 -0
  118. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js +325 -0
  119. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js.map +1 -0
  120. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js +14 -0
  121. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +1 -0
  122. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js +115 -0
  123. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -0
  124. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js +401 -0
  125. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -0
  126. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/index.js +2 -0
  127. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js +3756 -0
  128. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -0
  129. package/dist/presets/animation.d.ts +31 -0
  130. package/dist/presets/animation.js +39 -0
  131. package/dist/presets/animation.js.map +1 -0
  132. package/dist/presets/faceset.d.ts +30 -0
  133. package/dist/presets/faceset.js +51 -0
  134. package/dist/presets/faceset.js.map +1 -0
  135. package/dist/presets/icon.d.ts +20 -0
  136. package/dist/presets/icon.js +15 -0
  137. package/dist/presets/icon.js.map +1 -0
  138. package/dist/presets/index.d.ts +123 -0
  139. package/dist/presets/index.js +17 -0
  140. package/dist/presets/index.js.map +1 -0
  141. package/dist/presets/lpc.d.ts +89 -0
  142. package/dist/presets/lpc.js +98 -0
  143. package/dist/presets/lpc.js.map +1 -0
  144. package/dist/presets/rmspritesheet.js +42 -0
  145. package/dist/presets/rmspritesheet.js.map +1 -0
  146. package/dist/services/AbstractSocket.d.ts +9 -5
  147. package/dist/services/AbstractSocket.js +11 -0
  148. package/dist/services/AbstractSocket.js.map +1 -0
  149. package/dist/services/keyboardControls.d.ts +15 -0
  150. package/dist/services/keyboardControls.js +23 -0
  151. package/dist/services/keyboardControls.js.map +1 -0
  152. package/dist/services/loadMap.js +123 -0
  153. package/dist/services/loadMap.js.map +1 -0
  154. package/dist/services/mmorpg.d.ts +21 -9
  155. package/dist/services/mmorpg.js +131 -0
  156. package/dist/services/mmorpg.js.map +1 -0
  157. package/dist/services/save.d.ts +19 -0
  158. package/dist/services/save.js +77 -0
  159. package/dist/services/save.js.map +1 -0
  160. package/dist/services/standalone.d.ts +67 -7
  161. package/dist/services/standalone.js +168 -0
  162. package/dist/services/standalone.js.map +1 -0
  163. package/dist/utils/getEntityProp.d.ts +39 -0
  164. package/dist/utils/getEntityProp.js +52 -0
  165. package/dist/utils/getEntityProp.js.map +1 -0
  166. package/package.json +13 -9
  167. package/src/Game/{EffectManager.ts → AnimationManager.ts} +3 -2
  168. package/src/Game/Event.ts +1 -1
  169. package/src/Game/Map.ts +95 -3
  170. package/src/Game/Object.ts +330 -14
  171. package/src/Game/Player.ts +1 -1
  172. package/src/Gui/Gui.ts +506 -18
  173. package/src/Gui/NotificationManager.ts +69 -0
  174. package/src/Resource.ts +150 -0
  175. package/src/RpgClient.ts +300 -58
  176. package/src/RpgClientEngine.ts +1707 -48
  177. package/src/Sound.ts +253 -0
  178. package/src/components/{effects → animations}/animation.ce +3 -6
  179. package/src/components/{effects → animations}/index.ts +1 -1
  180. package/src/components/character.ce +406 -40
  181. package/src/components/dynamics/parse-value.ts +80 -0
  182. package/src/components/dynamics/text.ce +183 -0
  183. package/src/components/gui/box.ce +17 -0
  184. package/src/components/gui/dialogbox/index.ce +204 -187
  185. package/src/components/gui/gameover.ce +158 -0
  186. package/src/components/gui/hud/hud.ce +61 -0
  187. package/src/components/gui/index.ts +30 -4
  188. package/src/components/gui/menu/equip-menu.ce +410 -0
  189. package/src/components/gui/menu/exit-menu.ce +41 -0
  190. package/src/components/gui/menu/items-menu.ce +317 -0
  191. package/src/components/gui/menu/main-menu.ce +294 -0
  192. package/src/components/gui/menu/options-menu.ce +35 -0
  193. package/src/components/gui/menu/skills-menu.ce +83 -0
  194. package/src/components/gui/mobile/index.ts +24 -0
  195. package/src/components/gui/mobile/mobile.ce +80 -0
  196. package/src/components/gui/notification/notification.ce +51 -0
  197. package/src/components/gui/save-load.ce +208 -0
  198. package/src/components/gui/shop/shop.ce +493 -0
  199. package/src/components/gui/title-screen.ce +163 -0
  200. package/src/components/index.ts +3 -0
  201. package/src/components/prebuilt/hp-bar.ce +255 -0
  202. package/src/components/prebuilt/index.ts +24 -0
  203. package/src/components/prebuilt/light-halo.ce +148 -0
  204. package/src/components/scenes/canvas.ce +20 -15
  205. package/src/components/scenes/draw-map.ce +60 -13
  206. package/src/components/scenes/event-layer.ce +9 -2
  207. package/src/components/scenes/transition.ce +60 -0
  208. package/src/index.ts +16 -2
  209. package/src/module.ts +145 -9
  210. package/src/presets/animation.ts +46 -0
  211. package/src/presets/faceset.ts +60 -0
  212. package/src/presets/icon.ts +17 -0
  213. package/src/presets/index.ts +9 -1
  214. package/src/presets/lpc.ts +108 -0
  215. package/src/services/AbstractSocket.ts +10 -2
  216. package/src/services/keyboardControls.ts +20 -0
  217. package/src/services/loadMap.ts +1 -1
  218. package/src/services/mmorpg.ts +100 -12
  219. package/src/services/save.ts +103 -0
  220. package/src/services/standalone.ts +110 -18
  221. package/src/utils/getEntityProp.ts +87 -0
  222. package/tsconfig.json +1 -1
  223. package/vite.config.ts +4 -2
  224. package/dist/Game/EffectManager.d.ts +0 -5
  225. package/dist/components/effects/index.d.ts +0 -4
  226. package/dist/index.js.map +0 -1
  227. package/dist/index10.js +0 -8
  228. package/dist/index10.js.map +0 -1
  229. package/dist/index11.js +0 -10
  230. package/dist/index11.js.map +0 -1
  231. package/dist/index12.js +0 -8
  232. package/dist/index12.js.map +0 -1
  233. package/dist/index13.js +0 -17
  234. package/dist/index13.js.map +0 -1
  235. package/dist/index14.js +0 -91
  236. package/dist/index14.js.map +0 -1
  237. package/dist/index15.js +0 -50
  238. package/dist/index15.js.map +0 -1
  239. package/dist/index16.js +0 -191
  240. package/dist/index16.js.map +0 -1
  241. package/dist/index17.js +0 -9
  242. package/dist/index17.js.map +0 -1
  243. package/dist/index18.js +0 -387
  244. package/dist/index18.js.map +0 -1
  245. package/dist/index19.js +0 -31
  246. package/dist/index19.js.map +0 -1
  247. package/dist/index2.js +0 -112
  248. package/dist/index2.js.map +0 -1
  249. package/dist/index20.js +0 -24
  250. package/dist/index20.js.map +0 -1
  251. package/dist/index21.js +0 -2421
  252. package/dist/index21.js.map +0 -1
  253. package/dist/index22.js +0 -114
  254. package/dist/index22.js.map +0 -1
  255. package/dist/index23.js +0 -109
  256. package/dist/index23.js.map +0 -1
  257. package/dist/index24.js +0 -71
  258. package/dist/index24.js.map +0 -1
  259. package/dist/index25.js +0 -21
  260. package/dist/index25.js.map +0 -1
  261. package/dist/index26.js +0 -41
  262. package/dist/index26.js.map +0 -1
  263. package/dist/index27.js +0 -5
  264. package/dist/index27.js.map +0 -1
  265. package/dist/index28.js +0 -322
  266. package/dist/index28.js.map +0 -1
  267. package/dist/index29.js +0 -27
  268. package/dist/index29.js.map +0 -1
  269. package/dist/index3.js +0 -87
  270. package/dist/index3.js.map +0 -1
  271. package/dist/index30.js +0 -11
  272. package/dist/index30.js.map +0 -1
  273. package/dist/index31.js +0 -11
  274. package/dist/index31.js.map +0 -1
  275. package/dist/index32.js +0 -12
  276. package/dist/index32.js.map +0 -1
  277. package/dist/index33.js +0 -4403
  278. package/dist/index33.js.map +0 -1
  279. package/dist/index34.js +0 -316
  280. package/dist/index34.js.map +0 -1
  281. package/dist/index35.js +0 -174
  282. package/dist/index35.js.map +0 -1
  283. package/dist/index36.js +0 -501
  284. package/dist/index36.js.map +0 -1
  285. package/dist/index37.js +0 -61
  286. package/dist/index37.js.map +0 -1
  287. package/dist/index38.js +0 -20
  288. package/dist/index38.js.map +0 -1
  289. package/dist/index39.js +0 -20
  290. package/dist/index39.js.map +0 -1
  291. package/dist/index4.js +0 -67
  292. package/dist/index4.js.map +0 -1
  293. package/dist/index5.js +0 -16
  294. package/dist/index5.js.map +0 -1
  295. package/dist/index6.js +0 -17
  296. package/dist/index6.js.map +0 -1
  297. package/dist/index7.js +0 -39
  298. package/dist/index7.js.map +0 -1
  299. package/dist/index8.js +0 -90
  300. package/dist/index8.js.map +0 -1
  301. package/dist/index9.js +0 -76
  302. package/dist/index9.js.map +0 -1
  303. package/src/components/gui/dialogbox/itemMenu.ce +0 -23
  304. package/src/components/gui/dialogbox/selection.ce +0 -67
  305. /package/src/components/{effects → animations}/hit.ce +0 -0
package/src/Gui/Gui.ts CHANGED
@@ -1,46 +1,263 @@
1
1
  import { Context, inject } from "@signe/di";
2
- import { signal } from "canvasengine";
2
+ import { signal, Signal, WritableSignal } from "canvasengine";
3
3
  import { AbstractWebsocket, WebSocketToken } from "../services/AbstractSocket";
4
- import { PrebuiltGui } from "../components/gui";
4
+ import { DialogboxComponent, ShopComponent, SaveLoadComponent, MainMenuComponent, NotificationComponent, TitleScreenComponent, GameoverComponent } from "../components/gui";
5
+ import { combineLatest, Subscription } from "rxjs";
6
+ import { delay, PrebuiltGui } from "@rpgjs/common";
5
7
 
6
8
  interface GuiOptions {
7
- name: string;
9
+ name?: string;
10
+ id?: string;
8
11
  component: any;
9
12
  display?: boolean;
10
13
  data?: any;
14
+ /**
15
+ * Auto display the GUI when added to the system
16
+ * @default false
17
+ */
18
+ autoDisplay?: boolean;
19
+ /**
20
+ * Function that returns an array of Signal dependencies
21
+ * The GUI will only display when all dependencies are resolved (!= undefined)
22
+ * @returns Array of Signal dependencies
23
+ */
24
+ dependencies?: () => Signal[];
25
+ /**
26
+ * Attach the GUI to sprites instead of displaying globally
27
+ * When true, the GUI will be rendered in character.ce for each sprite
28
+ * @default false
29
+ */
30
+ attachToSprite?: boolean;
31
+ }
32
+
33
+ interface GuiInstance {
34
+ name: string;
35
+ component: any;
36
+ display: WritableSignal<boolean>;
37
+ data: WritableSignal<any>;
38
+ autoDisplay: boolean;
39
+ dependencies?: () => Signal[];
40
+ subscription?: Subscription;
41
+ attachToSprite?: boolean;
42
+ }
43
+
44
+ interface GuiAction {
45
+ guiId: string;
46
+ name: string;
47
+ data: any;
48
+ clientActionId: string;
11
49
  }
12
50
 
51
+ type OptimisticReducer = (data: any, action: GuiAction) => any;
52
+
13
53
  const throwError = (id: string) => {
14
54
  throw `The GUI named ${id} is non-existent. Please add the component in the gui property of the decorator @RpgClient`;
15
55
  };
16
56
 
57
+ const updateItemQuantity = (items: any[], id: string) => {
58
+ const index = items.findIndex((item) => item?.id === id);
59
+ if (index === -1) return items;
60
+ const item = items[index];
61
+ if (item?.usable === false) return items;
62
+ if (item?.consumable === false) return items;
63
+ const quantity = typeof item?.quantity === "number" ? item.quantity : 1;
64
+ const nextQuantity = Math.max(0, quantity - 1);
65
+ if (nextQuantity === quantity) return items;
66
+ if (nextQuantity <= 0) {
67
+ return items.filter((_, idx) => idx !== index);
68
+ }
69
+ const nextItems = items.slice();
70
+ nextItems[index] = { ...item, quantity: nextQuantity };
71
+ return nextItems;
72
+ };
73
+
74
+ const updateEquippedFlag = (items: any[], id: string, equip: boolean) => {
75
+ const index = items.findIndex((item) => item?.id === id);
76
+ if (index === -1) return items;
77
+ const item = items[index];
78
+ if (item?.equipped === equip) return items;
79
+ const nextItems = items.slice();
80
+ nextItems[index] = { ...item, equipped: equip };
81
+ return nextItems;
82
+ };
83
+
84
+ const mainMenuOptimisticReducer: OptimisticReducer = (data, action) => {
85
+ if (!data || typeof data !== "object") return data;
86
+ if (action.name === "useItem") {
87
+ if (!Array.isArray(data.items)) return data;
88
+ const id = action.data?.id;
89
+ if (!id) return data;
90
+ const nextItems = updateItemQuantity(data.items, id);
91
+ if (nextItems === data.items) return data;
92
+ return { ...data, items: nextItems };
93
+ }
94
+ if (action.name === "equipItem") {
95
+ const id = action.data?.id;
96
+ if (!id || typeof action.data?.equip !== "boolean") return data;
97
+ const equip = action.data.equip;
98
+ let nextItems = data.items;
99
+ let nextEquips = data.equips;
100
+ if (Array.isArray(data.items)) {
101
+ nextItems = updateEquippedFlag(data.items, id, equip);
102
+ }
103
+ if (Array.isArray(data.equips)) {
104
+ nextEquips = updateEquippedFlag(data.equips, id, equip);
105
+ }
106
+ if (nextItems === data.items && nextEquips === data.equips) return data;
107
+ return {
108
+ ...data,
109
+ ...(nextItems !== data.items ? { items: nextItems } : {}),
110
+ ...(nextEquips !== data.equips ? { equips: nextEquips } : {})
111
+ };
112
+ }
113
+ return data;
114
+ };
115
+
17
116
  export class RpgGui {
18
117
  private webSocket: AbstractWebsocket;
19
- gui = signal<any>({});
118
+ gui = signal<Record<string, GuiInstance>>({});
119
+ extraGuis: GuiInstance[] = [];
120
+ private vueGuiInstance: any = null; // Reference to VueGui instance
121
+ private optimisticReducers = new Map<string, OptimisticReducer[]>();
122
+ private pendingActions = new Map<string, GuiAction[]>();
123
+ /**
124
+ * Signal tracking which player IDs should display attached GUIs
125
+ * Key: player ID, Value: boolean (true = show, false = hide)
126
+ */
127
+ attachedGuiDisplayState = signal<Record<string, boolean>>({});
20
128
 
21
129
  constructor(private context: Context) {
22
130
  this.webSocket = inject(context, WebSocketToken);
23
131
  this.add({
24
132
  name: "rpg-dialog",
25
- component: PrebuiltGui.Dialogbox,
133
+ component: DialogboxComponent,
26
134
  });
135
+ this.add({
136
+ name: PrebuiltGui.MainMenu,
137
+ component: MainMenuComponent,
138
+ });
139
+ this.add({
140
+ name: PrebuiltGui.Shop,
141
+ component: ShopComponent,
142
+ });
143
+ this.add({
144
+ name: PrebuiltGui.Notification,
145
+ component: NotificationComponent,
146
+ autoDisplay: true,
147
+ });
148
+ this.add({
149
+ name: PrebuiltGui.Save,
150
+ component: SaveLoadComponent,
151
+ });
152
+ this.add({
153
+ name: PrebuiltGui.TitleScreen,
154
+ component: TitleScreenComponent,
155
+ });
156
+ this.add({
157
+ name: PrebuiltGui.Gameover,
158
+ component: GameoverComponent,
159
+ });
160
+
161
+ this.registerOptimisticReducer(PrebuiltGui.MainMenu, mainMenuOptimisticReducer);
27
162
  }
28
163
 
29
164
  async _initialize() {
30
165
  this.webSocket.on("gui.open", (data: { guiId: string; data: any }) => {
166
+ this.clearPendingActions(data.guiId);
31
167
  this.display(data.guiId, data.data);
32
168
  });
33
169
 
34
170
  this.webSocket.on("gui.exit", (guiId: string) => {
35
171
  this.hide(guiId);
36
172
  });
173
+
174
+ this.webSocket.on("gui.update", (payload: { guiId: string; data: any; clientActionId?: string }) => {
175
+ this.applyServerUpdate(payload.guiId, payload.data, payload.clientActionId);
176
+ });
177
+
178
+ /**
179
+ * Listen for tooltip display state changes from server
180
+ * This is triggered by showAttachedGui/hideAttachedGui on the server
181
+ */
182
+ this.webSocket.on("gui.tooltip", (data: { players: string[]; display: boolean }) => {
183
+ const currentState = { ...this.attachedGuiDisplayState() };
184
+ data.players.forEach((playerId) => {
185
+ currentState[playerId] = data.display;
186
+ });
187
+ this.attachedGuiDisplayState.set(currentState);
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Set the VueGui instance reference for Vue component management
193
+ * This is called by VueGui when it's initialized
194
+ *
195
+ * @param vueGuiInstance - The VueGui instance
196
+ */
197
+ _setVueGuiInstance(vueGuiInstance: any) {
198
+ this.vueGuiInstance = vueGuiInstance;
199
+ }
200
+
201
+ /**
202
+ * Notify VueGui about GUI state changes
203
+ * This synchronizes the Vue component display state
204
+ *
205
+ * @param guiId - The GUI component ID
206
+ * @param display - Display state
207
+ * @param data - Component data
208
+ */
209
+ 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
+ }
225
+ }
226
+
227
+ /**
228
+ * Initialize Vue components in the VueGui instance
229
+ * This should be called after VueGui is mounted
230
+ */
231
+ _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
+ }
37
246
  }
38
247
 
39
248
  guiInteraction(guiId: string, name: string, data: any) {
249
+ const clientActionId = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}`;
250
+ const actionData = { ...(data || {}), clientActionId };
251
+ this.applyOptimisticAction({
252
+ guiId,
253
+ name,
254
+ data: actionData,
255
+ clientActionId
256
+ });
40
257
  this.webSocket.emit("gui.interaction", {
41
258
  guiId,
42
259
  name,
43
- data,
260
+ data: actionData,
44
261
  });
45
262
  }
46
263
 
@@ -51,42 +268,313 @@ export class RpgGui {
51
268
  });
52
269
  }
53
270
 
271
+ /**
272
+ * Add a GUI component to the system
273
+ *
274
+ * By default, only CanvasEngine components (.ce files) are accepted.
275
+ * Vue components should be handled by the @rpgjs/vue package.
276
+ *
277
+ * @param gui - GUI configuration options
278
+ * @param gui.name - Name or ID of the GUI component
279
+ * @param gui.id - Alternative ID if name is not provided
280
+ * @param gui.component - The component to render (must be a CanvasEngine component)
281
+ * @param gui.display - Initial display state (default: false)
282
+ * @param gui.data - Initial data for the component
283
+ * @param gui.autoDisplay - Auto display when added (default: false)
284
+ * @param gui.dependencies - Function returning Signal dependencies
285
+ * @param gui.attachToSprite - Attach GUI to sprites instead of global display (default: false)
286
+ *
287
+ * @example
288
+ * ```ts
289
+ * gui.add({
290
+ * name: 'inventory',
291
+ * component: InventoryComponent, // Must be a .ce component
292
+ * autoDisplay: true,
293
+ * dependencies: () => [playerSignal, inventorySignal]
294
+ * });
295
+ *
296
+ * // Attach to sprites
297
+ * gui.add({
298
+ * name: 'tooltip',
299
+ * component: TooltipComponent,
300
+ * attachToSprite: true
301
+ * });
302
+ * ```
303
+ */
54
304
  add(gui: GuiOptions) {
55
- this.gui()[gui.name] = {
56
- name: gui.name,
305
+ const guiId = gui.name || gui.id;
306
+ if (!guiId) {
307
+ throw new Error("GUI must have a name or id");
308
+ }
309
+ const guiInstance: GuiInstance = {
310
+ name: guiId,
57
311
  component: gui.component,
58
312
  display: signal(gui.display || false),
59
313
  data: signal(gui.data || {}),
314
+ autoDisplay: gui.autoDisplay || false,
315
+ dependencies: gui.dependencies ? gui.dependencies() : [],
316
+ attachToSprite: gui.attachToSprite || false,
60
317
  };
318
+
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);
324
+
325
+ // Auto display Vue components if enabled
326
+ if (guiInstance.autoDisplay) {
327
+ this._notifyVueGui(guiId, true, gui.data || {});
328
+ }
329
+ return;
330
+ }
331
+
332
+ this.gui()[guiId] = guiInstance;
333
+
334
+ // Auto display if enabled and it's a CanvasEngine component
335
+ if (guiInstance.autoDisplay && typeof gui.component === 'function') {
336
+ this.display(guiId, gui.data);
337
+ }
61
338
  }
62
339
 
63
- get(id: string | GuiOptions) {
64
- if (typeof id != "string") {
65
- id = id.name;
340
+ registerOptimisticReducer(guiId: string, reducer: OptimisticReducer) {
341
+ const existing = this.optimisticReducers.get(guiId) || [];
342
+ this.optimisticReducers.set(guiId, existing.concat(reducer));
343
+ }
344
+
345
+ /**
346
+ * Get all attached GUI components (attachToSprite: true)
347
+ *
348
+ * Returns all GUI instances that are configured to be attached to sprites.
349
+ * These GUIs should be rendered in character.ce instead of canvas.ce.
350
+ *
351
+ * @returns Array of GUI instances with attachToSprite: true
352
+ *
353
+ * @example
354
+ * ```ts
355
+ * const attachedGuis = gui.getAttachedGuis();
356
+ * // Use in character.ce to render tooltips
357
+ * ```
358
+ */
359
+ getAttachedGuis(): GuiInstance[] {
360
+ const allGuis = this.getAll();
361
+ return Object.values(allGuis).filter(gui => gui.attachToSprite === true);
362
+ }
363
+
364
+ /**
365
+ * Check if a player should display attached GUIs
366
+ *
367
+ * @param playerId - The player ID to check
368
+ * @returns true if attached GUIs should be displayed for this player
369
+ */
370
+ shouldDisplayAttachedGui(playerId: string): boolean {
371
+ return this.attachedGuiDisplayState()[playerId] === true;
372
+ }
373
+
374
+ get(id: string): GuiInstance | undefined {
375
+ // Check CanvasEngine GUIs first
376
+ const canvasGui = this.gui()[id];
377
+ if (canvasGui) {
378
+ return canvasGui;
66
379
  }
67
- return this.gui()[id];
380
+
381
+ // Check Vue GUIs in extraGuis
382
+ return this.extraGuis.find(gui => gui.name === id);
68
383
  }
69
384
 
70
385
  exists(id: string): boolean {
71
386
  return !!this.get(id);
72
387
  }
73
388
 
74
- getAll() {
75
- return this.gui();
389
+ getAll(): Record<string, GuiInstance> {
390
+ const allGuis = { ...this.gui() };
391
+
392
+ // Add extraGuis to the result
393
+ this.extraGuis.forEach(gui => {
394
+ allGuis[gui.name] = gui;
395
+ });
396
+
397
+ return allGuis;
76
398
  }
77
399
 
78
- display(id: string, data = {}) {
400
+ /**
401
+ * Display a GUI component
402
+ *
403
+ * Displays the GUI immediately if no dependencies are configured,
404
+ * or waits for all dependencies to be resolved if dependencies are present.
405
+ * Automatically manages subscriptions to prevent memory leaks.
406
+ * Works with both CanvasEngine components and Vue components.
407
+ *
408
+ * @param id - The GUI component ID
409
+ * @param data - Data to pass to the component
410
+ * @param dependencies - Optional runtime dependencies (overrides config dependencies)
411
+ *
412
+ * @example
413
+ * ```ts
414
+ * // Display immediately
415
+ * gui.display('inventory', { items: [] });
416
+ *
417
+ * // Display with runtime dependencies
418
+ * gui.display('shop', { shopId: 1 }, [playerSignal, shopSignal]);
419
+ * ```
420
+ */
421
+ display(id: string, data = {}, dependencies: Signal[] = []) {
79
422
  if (!this.exists(id)) {
80
423
  throw throwError(id);
81
424
  }
82
- this.get(id).data.set(data);
83
- this.get(id).display.set(true);
425
+
426
+ const guiInstance = this.get(id)!;
427
+
428
+ // Check if it's a Vue component (in extraGuis)
429
+ const isVueComponent = this.extraGuis.some(gui => gui.name === id);
430
+
431
+ if (isVueComponent) {
432
+ // Handle Vue component display
433
+ this._handleVueComponentDisplay(id, data, dependencies, guiInstance);
434
+ } else {
435
+ guiInstance.data.set(data);
436
+ guiInstance.display.set(true);
437
+ }
438
+ }
439
+
440
+ isDisplaying(id: string): boolean {
441
+ const guiInstance = this.get(id);
442
+ if (!guiInstance) return false;
443
+ return guiInstance.display();
444
+ }
445
+
446
+ /**
447
+ * Handle Vue component display logic
448
+ *
449
+ * @param id - GUI component ID
450
+ * @param data - Component data
451
+ * @param dependencies - Runtime dependencies
452
+ * @param guiInstance - GUI instance
453
+ */
454
+ private _handleVueComponentDisplay(id: string, data: any, dependencies: Signal[], guiInstance: GuiInstance) {
455
+ // Unsubscribe from previous subscription if exists
456
+ if (guiInstance.subscription) {
457
+ guiInstance.subscription.unsubscribe();
458
+ guiInstance.subscription = undefined;
459
+ }
460
+
461
+ // Use runtime dependencies or config dependencies
462
+ const deps = dependencies.length > 0
463
+ ? dependencies
464
+ : (guiInstance.dependencies ? guiInstance.dependencies() : []);
465
+
466
+ if (deps.length > 0) {
467
+ // Subscribe to dependencies
468
+ guiInstance.subscription = combineLatest(
469
+ deps.map(dependency => dependency.observable)
470
+ ).subscribe((values) => {
471
+ if (values.every(value => value !== undefined)) {
472
+ guiInstance.data.set(data);
473
+ guiInstance.display.set(true);
474
+ this._notifyVueGui(id, true, data);
475
+ }
476
+ });
477
+ return;
478
+ }
479
+
480
+ // No dependencies, display immediately
481
+ guiInstance.data.set(data);
482
+ guiInstance.display.set(true);
483
+ this._notifyVueGui(id, true, data);
84
484
  }
85
485
 
486
+ /**
487
+ * Hide a GUI component
488
+ *
489
+ * Hides the GUI and cleans up any active subscriptions.
490
+ * Works with both CanvasEngine components and Vue components.
491
+ *
492
+ * @param id - The GUI component ID
493
+ *
494
+ * @example
495
+ * ```ts
496
+ * gui.hide('inventory');
497
+ * ```
498
+ */
86
499
  hide(id: string) {
87
500
  if (!this.exists(id)) {
88
501
  throw throwError(id);
89
502
  }
90
- this.get(id).display.set(false);
503
+
504
+ const guiInstance = this.get(id)!;
505
+
506
+ // Unsubscribe if there's an active subscription
507
+ if (guiInstance.subscription) {
508
+ guiInstance.subscription.unsubscribe();
509
+ guiInstance.subscription = undefined;
510
+ }
511
+
512
+ guiInstance.display.set(false)
513
+
514
+ // Check if it's a Vue component and notify VueGui
515
+ const isVueComponent = this.extraGuis.some(gui => gui.name === id);
516
+ if (isVueComponent) {
517
+ this._notifyVueGui(id, false);
518
+ }
519
+ }
520
+
521
+ private isVueComponent(id: string) {
522
+ return this.extraGuis.some(gui => gui.name === id);
523
+ }
524
+
525
+ private clearPendingActions(guiId: string) {
526
+ this.pendingActions.delete(guiId);
527
+ }
528
+
529
+ private applyReducers(guiId: string, data: any, actions: GuiAction[]) {
530
+ const reducers = this.optimisticReducers.get(guiId);
531
+ if (!reducers || reducers.length === 0) return data;
532
+ let next = data;
533
+ for (const action of actions) {
534
+ for (const reducer of reducers) {
535
+ const updated = reducer(next, action);
536
+ if (updated !== undefined && updated !== null && updated !== next) {
537
+ next = updated;
538
+ }
539
+ }
540
+ }
541
+ return next;
542
+ }
543
+
544
+ private applyOptimisticAction(action: GuiAction) {
545
+ const guiInstance = this.get(action.guiId);
546
+ if (!guiInstance) return;
547
+ const reducers = this.optimisticReducers.get(action.guiId);
548
+ if (!reducers || reducers.length === 0) return;
549
+ const currentData = guiInstance.data();
550
+ const nextData = this.applyReducers(action.guiId, currentData, [action]);
551
+ if (nextData === currentData) return;
552
+ guiInstance.data.set(nextData);
553
+ const pending = this.pendingActions.get(action.guiId) || [];
554
+ pending.push(action);
555
+ this.pendingActions.set(action.guiId, pending);
556
+ if (this.isVueComponent(action.guiId)) {
557
+ this._notifyVueGui(action.guiId, guiInstance.display(), nextData);
558
+ }
559
+ }
560
+
561
+ private applyServerUpdate(guiId: string, data: any, clientActionId?: string) {
562
+ const guiInstance = this.get(guiId);
563
+ if (!guiInstance) return;
564
+ let pending = this.pendingActions.get(guiId) || [];
565
+ if (clientActionId) {
566
+ pending = pending.filter(action => action.clientActionId !== clientActionId);
567
+ } else {
568
+ pending = [];
569
+ }
570
+ let nextData = data;
571
+ if (pending.length) {
572
+ nextData = this.applyReducers(guiId, nextData, pending);
573
+ }
574
+ guiInstance.data.set(nextData);
575
+ this.pendingActions.set(guiId, pending);
576
+ if (this.isVueComponent(guiId)) {
577
+ this._notifyVueGui(guiId, guiInstance.display(), nextData);
578
+ }
91
579
  }
92
580
  }
@@ -0,0 +1,69 @@
1
+ import { signal, animatedSignal } from "canvasengine";
2
+
3
+ export type NotificationType = "info" | "warn" | "error";
4
+
5
+ export interface NotificationPayload {
6
+ message: string;
7
+ type?: NotificationType;
8
+ icon?: string;
9
+ time?: number;
10
+ sound?: string;
11
+ }
12
+
13
+ export interface NotificationItem extends NotificationPayload {
14
+ id: number;
15
+ opacity: any;
16
+ offset: any;
17
+ layoutY: any;
18
+ removing: boolean;
19
+ }
20
+
21
+ const DEFAULT_DURATION = 220;
22
+
23
+ export class NotificationManager {
24
+ stack = signal<NotificationItem[]>([]);
25
+ private _counter = 0;
26
+
27
+ add(payload: NotificationPayload, engine?: { playSound?: (id: string) => void }) {
28
+ const id = ++this._counter;
29
+ const opacity = animatedSignal(0, { duration: DEFAULT_DURATION });
30
+ const offset = animatedSignal(12, { duration: DEFAULT_DURATION });
31
+ const layoutY = animatedSignal(0, { duration: DEFAULT_DURATION });
32
+ const item: NotificationItem = {
33
+ id,
34
+ message: payload.message,
35
+ type: payload.type || "info",
36
+ icon: payload.icon,
37
+ time: payload.time,
38
+ sound: payload.sound,
39
+ opacity,
40
+ offset,
41
+ layoutY,
42
+ removing: false,
43
+ };
44
+ this.stack.update((list) => [...list, item]);
45
+ opacity.set(1);
46
+ offset.set(0);
47
+
48
+ if (payload.sound && engine?.playSound) {
49
+ engine.playSound(payload.sound);
50
+ }
51
+
52
+ const delay = typeof payload.time === "number" ? payload.time : 2000;
53
+ setTimeout(() => {
54
+ this.remove(id);
55
+ }, delay);
56
+ }
57
+
58
+ remove(id: number) {
59
+ const list = this.stack();
60
+ const item = list.find((it) => it.id === id);
61
+ if (!item || item.removing) return;
62
+ item.removing = true;
63
+ item.opacity.set(0);
64
+ item.offset.set(-8);
65
+ setTimeout(() => {
66
+ this.stack.update((items) => items.filter((it) => it.id !== id));
67
+ }, DEFAULT_DURATION);
68
+ }
69
+ }