@rpgjs/client 5.0.0-beta.2 → 5.0.0-beta.20

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 (314) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/dist/Game/AnimationManager.d.ts +2 -1
  3. package/dist/Game/AnimationManager.js +21 -9
  4. package/dist/Game/AnimationManager.js.map +1 -1
  5. package/dist/Game/AnimationManager.spec.d.ts +1 -0
  6. package/dist/Game/ClientVisuals.d.ts +61 -0
  7. package/dist/Game/ClientVisuals.js +96 -0
  8. package/dist/Game/ClientVisuals.js.map +1 -0
  9. package/dist/Game/ClientVisuals.spec.d.ts +1 -0
  10. package/dist/Game/Event.js.map +1 -1
  11. package/dist/Game/EventComponentResolver.d.ts +16 -0
  12. package/dist/Game/EventComponentResolver.js +52 -0
  13. package/dist/Game/EventComponentResolver.js.map +1 -0
  14. package/dist/Game/EventComponentResolver.spec.d.ts +1 -0
  15. package/dist/Game/Map.d.ts +9 -1
  16. package/dist/Game/Map.js +83 -5
  17. package/dist/Game/Map.js.map +1 -1
  18. package/dist/Game/Object.d.ts +49 -15
  19. package/dist/Game/Object.js +101 -42
  20. package/dist/Game/Object.js.map +1 -1
  21. package/dist/Game/Object.spec.d.ts +1 -0
  22. package/dist/Game/Player.js.map +1 -1
  23. package/dist/Game/ProjectileManager.d.ts +98 -0
  24. package/dist/Game/ProjectileManager.js +196 -0
  25. package/dist/Game/ProjectileManager.js.map +1 -0
  26. package/dist/Game/ProjectileManager.spec.d.ts +1 -0
  27. package/dist/Gui/Gui.d.ts +20 -6
  28. package/dist/Gui/Gui.js +96 -54
  29. package/dist/Gui/Gui.js.map +1 -1
  30. package/dist/Gui/Gui.spec.d.ts +1 -0
  31. package/dist/Gui/NotificationManager.js.map +1 -1
  32. package/dist/Resource.js +1 -1
  33. package/dist/Resource.js.map +1 -1
  34. package/dist/RpgClient.d.ts +194 -15
  35. package/dist/RpgClientEngine.d.ts +176 -11
  36. package/dist/RpgClientEngine.js +669 -68
  37. package/dist/RpgClientEngine.js.map +1 -1
  38. package/dist/Sound.js.map +1 -1
  39. package/dist/_virtual/{_@oxc-project_runtime@0.122.0/helpers → _@oxc-project_runtime@0.133.0/helpers/esm}/decorate.js +1 -1
  40. package/dist/_virtual/{_@oxc-project_runtime@0.122.0/helpers → _@oxc-project_runtime@0.133.0/helpers/esm}/decorateMetadata.js +1 -1
  41. package/dist/_virtual/_rolldown/runtime.js +13 -0
  42. package/dist/components/animations/animation.ce.js +6 -5
  43. package/dist/components/animations/animation.ce.js.map +1 -1
  44. package/dist/components/animations/fx.ce.js +60 -0
  45. package/dist/components/animations/fx.ce.js.map +1 -0
  46. package/dist/components/animations/hit.ce.js +24 -28
  47. package/dist/components/animations/hit.ce.js.map +1 -1
  48. package/dist/components/animations/index.d.ts +1 -0
  49. package/dist/components/animations/index.js +6 -4
  50. package/dist/components/animations/index.js.map +1 -1
  51. package/dist/components/character.ce.js +606 -248
  52. package/dist/components/character.ce.js.map +1 -1
  53. package/dist/components/dynamics/bar.ce.js +99 -0
  54. package/dist/components/dynamics/bar.ce.js.map +1 -0
  55. package/dist/components/dynamics/image.ce.js +26 -0
  56. package/dist/components/dynamics/image.ce.js.map +1 -0
  57. package/dist/components/dynamics/parse-value.d.ts +3 -0
  58. package/dist/components/dynamics/parse-value.js +54 -35
  59. package/dist/components/dynamics/parse-value.js.map +1 -1
  60. package/dist/components/dynamics/parse-value.spec.d.ts +1 -0
  61. package/dist/components/dynamics/shape-utils.d.ts +16 -0
  62. package/dist/components/dynamics/shape-utils.js +73 -0
  63. package/dist/components/dynamics/shape-utils.js.map +1 -0
  64. package/dist/components/dynamics/shape-utils.spec.d.ts +1 -0
  65. package/dist/components/dynamics/shape.ce.js +86 -0
  66. package/dist/components/dynamics/shape.ce.js.map +1 -0
  67. package/dist/components/dynamics/text.ce.js +36 -56
  68. package/dist/components/dynamics/text.ce.js.map +1 -1
  69. package/dist/components/gui/box.ce.js +9 -9
  70. package/dist/components/gui/box.ce.js.map +1 -1
  71. package/dist/components/gui/dialogbox/index.ce.js +75 -65
  72. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  73. package/dist/components/gui/gameover.ce.js +47 -68
  74. package/dist/components/gui/gameover.ce.js.map +1 -1
  75. package/dist/components/gui/hud/hud.ce.js +23 -31
  76. package/dist/components/gui/hud/hud.ce.js.map +1 -1
  77. package/dist/components/gui/menu/equip-menu.ce.js +121 -172
  78. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
  79. package/dist/components/gui/menu/exit-menu.ce.js +15 -10
  80. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
  81. package/dist/components/gui/menu/items-menu.ce.js +62 -77
  82. package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
  83. package/dist/components/gui/menu/main-menu.ce.js +89 -104
  84. package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
  85. package/dist/components/gui/menu/options-menu.ce.js +12 -8
  86. package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
  87. package/dist/components/gui/menu/skills-menu.ce.js +17 -19
  88. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
  89. package/dist/components/gui/mobile/index.js +2 -2
  90. package/dist/components/gui/mobile/index.js.map +1 -1
  91. package/dist/components/gui/mobile/mobile.ce.js +7 -5
  92. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  93. package/dist/components/gui/notification/notification.ce.js +28 -26
  94. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  95. package/dist/components/gui/save-load.ce.js +81 -256
  96. package/dist/components/gui/save-load.ce.js.map +1 -1
  97. package/dist/components/gui/shop/shop.ce.js +107 -142
  98. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  99. package/dist/components/gui/title-screen.ce.js +50 -73
  100. package/dist/components/gui/title-screen.ce.js.map +1 -1
  101. package/dist/components/index.d.ts +2 -1
  102. package/dist/components/index.js +1 -0
  103. package/dist/components/interaction-components.ce.js +22 -0
  104. package/dist/components/interaction-components.ce.js.map +1 -0
  105. package/dist/components/player-components-utils.d.ts +67 -0
  106. package/dist/components/player-components-utils.js +162 -0
  107. package/dist/components/player-components-utils.js.map +1 -0
  108. package/dist/components/player-components-utils.spec.d.ts +1 -0
  109. package/dist/components/player-components.ce.js +191 -0
  110. package/dist/components/player-components.ce.js.map +1 -0
  111. package/dist/components/prebuilt/hp-bar.ce.js +45 -45
  112. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  113. package/dist/components/prebuilt/light-halo.ce.js +39 -60
  114. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  115. package/dist/components/scenes/canvas.ce.js +205 -27
  116. package/dist/components/scenes/canvas.ce.js.map +1 -1
  117. package/dist/components/scenes/draw-map.ce.js +39 -39
  118. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  119. package/dist/components/scenes/event-layer.ce.js +51 -10
  120. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  121. package/dist/core/inject.js +1 -1
  122. package/dist/core/inject.js.map +1 -1
  123. package/dist/core/setup.js +1 -1
  124. package/dist/core/setup.js.map +1 -1
  125. package/dist/decorators/spritesheet.d.ts +1 -0
  126. package/dist/decorators/spritesheet.js +11 -0
  127. package/dist/decorators/spritesheet.js.map +1 -0
  128. package/dist/i18n.d.ts +55 -0
  129. package/dist/i18n.js +60 -0
  130. package/dist/i18n.js.map +1 -0
  131. package/dist/i18n.spec.d.ts +1 -0
  132. package/dist/index.d.ts +7 -0
  133. package/dist/index.js +29 -21
  134. package/dist/module.js +45 -4
  135. package/dist/module.js.map +1 -1
  136. package/dist/node_modules/.pnpm/{@signe_di@2.9.0 → @signe_di@3.1.0}/node_modules/@signe/di/dist/index.js +7 -117
  137. package/dist/node_modules/.pnpm/@signe_di@3.1.0/node_modules/@signe/di/dist/index.js.map +1 -0
  138. package/dist/node_modules/.pnpm/@signe_reactive@3.1.0/node_modules/@signe/reactive/dist/index.js +239 -0
  139. package/dist/node_modules/.pnpm/@signe_reactive@3.1.0/node_modules/@signe/reactive/dist/index.js.map +1 -0
  140. package/dist/node_modules/.pnpm/@signe_room@3.1.0_react@19.2.7/node_modules/@signe/room/dist/chunk-EUXUH3YW.js +13 -0
  141. package/dist/node_modules/.pnpm/@signe_room@3.1.0_react@19.2.7/node_modules/@signe/room/dist/chunk-EUXUH3YW.js.map +1 -0
  142. package/dist/node_modules/.pnpm/@signe_room@3.1.0_react@19.2.7/node_modules/@signe/room/dist/index.js +698 -0
  143. package/dist/node_modules/.pnpm/@signe_room@3.1.0_react@19.2.7/node_modules/@signe/room/dist/index.js.map +1 -0
  144. package/dist/node_modules/.pnpm/@signe_sync@3.1.0_react@19.2.7/node_modules/@signe/sync/dist/client/index.js +43 -0
  145. package/dist/node_modules/.pnpm/@signe_sync@3.1.0_react@19.2.7/node_modules/@signe/sync/dist/client/index.js.map +1 -0
  146. package/dist/node_modules/.pnpm/{@signe_sync@2.9.0 → @signe_sync@3.1.0_react@19.2.7}/node_modules/@signe/sync/dist/index.js +57 -141
  147. package/dist/node_modules/.pnpm/@signe_sync@3.1.0_react@19.2.7/node_modules/@signe/sync/dist/index.js.map +1 -0
  148. package/dist/node_modules/.pnpm/{partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js → partysocket@1.2.0_react@19.2.7/node_modules/partysocket/dist/index.js} +30 -17
  149. package/dist/node_modules/.pnpm/partysocket@1.2.0_react@19.2.7/node_modules/partysocket/dist/index.js.map +1 -0
  150. package/dist/node_modules/.pnpm/{partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js → partysocket@1.2.0_react@19.2.7/node_modules/partysocket/dist/ws.js} +65 -34
  151. package/dist/node_modules/.pnpm/partysocket@1.2.0_react@19.2.7/node_modules/partysocket/dist/ws.js.map +1 -0
  152. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/index.js +13 -0
  153. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/index.js.map +1 -0
  154. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js +106 -0
  155. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js.map +1 -0
  156. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js +13 -0
  157. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js.map +1 -0
  158. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js +121 -0
  159. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js.map +1 -0
  160. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js +10 -0
  161. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js.map +1 -0
  162. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js +110 -0
  163. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js.map +1 -0
  164. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js +99 -0
  165. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js.map +1 -0
  166. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js +82 -0
  167. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js.map +1 -0
  168. package/dist/node_modules/.pnpm/{zod@3.24.2/node_modules/zod/lib/index.js → zod@3.25.76/node_modules/zod/v3/types.js} +74 -611
  169. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js.map +1 -0
  170. package/dist/presets/animation.js.map +1 -1
  171. package/dist/presets/faceset.js.map +1 -1
  172. package/dist/presets/icon.js.map +1 -1
  173. package/dist/presets/index.js.map +1 -1
  174. package/dist/presets/lpc.js.map +1 -1
  175. package/dist/presets/rmspritesheet.js.map +1 -1
  176. package/dist/services/AbstractSocket.d.ts +2 -0
  177. package/dist/services/AbstractSocket.js.map +1 -1
  178. package/dist/services/actionInput.d.ts +14 -0
  179. package/dist/services/actionInput.js +59 -0
  180. package/dist/services/actionInput.js.map +1 -0
  181. package/dist/services/actionInput.spec.d.ts +1 -0
  182. package/dist/services/interactions.d.ts +159 -0
  183. package/dist/services/interactions.js +460 -0
  184. package/dist/services/interactions.js.map +1 -0
  185. package/dist/services/interactions.spec.d.ts +1 -0
  186. package/dist/services/keyboardControls.d.ts +1 -0
  187. package/dist/services/keyboardControls.js +1 -0
  188. package/dist/services/keyboardControls.js.map +1 -1
  189. package/dist/services/loadMap.d.ts +9 -0
  190. package/dist/services/loadMap.js +1 -1
  191. package/dist/services/loadMap.js.map +1 -1
  192. package/dist/services/mmorpg-connection.d.ts +5 -0
  193. package/dist/services/mmorpg-connection.js +50 -0
  194. package/dist/services/mmorpg-connection.js.map +1 -0
  195. package/dist/services/mmorpg-connection.spec.d.ts +1 -0
  196. package/dist/services/mmorpg.d.ts +11 -4
  197. package/dist/services/mmorpg.js +57 -33
  198. package/dist/services/mmorpg.js.map +1 -1
  199. package/dist/services/pointerContext.d.ts +11 -0
  200. package/dist/services/pointerContext.js +48 -0
  201. package/dist/services/pointerContext.js.map +1 -0
  202. package/dist/services/pointerContext.spec.d.ts +1 -0
  203. package/dist/services/save.js.map +1 -1
  204. package/dist/services/save.spec.d.ts +1 -0
  205. package/dist/services/standalone-message.d.ts +1 -0
  206. package/dist/services/standalone-message.js +9 -0
  207. package/dist/services/standalone-message.js.map +1 -0
  208. package/dist/services/standalone.d.ts +4 -1
  209. package/dist/services/standalone.js +36 -16
  210. package/dist/services/standalone.js.map +1 -1
  211. package/dist/services/standalone.spec.d.ts +1 -0
  212. package/dist/utils/getEntityProp.js +4 -3
  213. package/dist/utils/getEntityProp.js.map +1 -1
  214. package/dist/utils/getEntityProp.spec.d.ts +1 -0
  215. package/dist/utils/mapId.d.ts +1 -0
  216. package/dist/utils/mapId.js +6 -0
  217. package/dist/utils/mapId.js.map +1 -0
  218. package/dist/utils/readPropValue.d.ts +2 -0
  219. package/dist/utils/readPropValue.js +13 -0
  220. package/dist/utils/readPropValue.js.map +1 -0
  221. package/package.json +13 -13
  222. package/src/Game/AnimationManager.spec.ts +30 -0
  223. package/src/Game/AnimationManager.ts +26 -10
  224. package/src/Game/ClientVisuals.spec.ts +56 -0
  225. package/src/Game/ClientVisuals.ts +184 -0
  226. package/src/Game/EventComponentResolver.spec.ts +84 -0
  227. package/src/Game/EventComponentResolver.ts +74 -0
  228. package/src/Game/Map.ts +134 -2
  229. package/src/Game/Object.spec.ts +59 -0
  230. package/src/Game/Object.ts +181 -77
  231. package/src/Game/ProjectileManager.spec.ts +449 -0
  232. package/src/Game/ProjectileManager.ts +346 -0
  233. package/src/Gui/Gui.spec.ts +340 -0
  234. package/src/Gui/Gui.ts +129 -57
  235. package/src/Resource.ts +1 -2
  236. package/src/RpgClient.ts +220 -17
  237. package/src/RpgClientEngine.ts +959 -116
  238. package/src/components/animations/fx.ce +101 -0
  239. package/src/components/animations/index.ts +4 -2
  240. package/src/components/character.ce +681 -45
  241. package/src/components/dynamics/bar.ce +88 -0
  242. package/src/components/dynamics/image.ce +21 -0
  243. package/src/components/dynamics/parse-value.spec.ts +83 -0
  244. package/src/components/dynamics/parse-value.ts +111 -37
  245. package/src/components/dynamics/shape-utils.spec.ts +46 -0
  246. package/src/components/dynamics/shape-utils.ts +61 -0
  247. package/src/components/dynamics/shape.ce +90 -0
  248. package/src/components/dynamics/text.ce +35 -149
  249. package/src/components/gui/dialogbox/index.ce +52 -21
  250. package/src/components/gui/gameover.ce +6 -4
  251. package/src/components/gui/menu/equip-menu.ce +11 -9
  252. package/src/components/gui/menu/exit-menu.ce +6 -4
  253. package/src/components/gui/menu/items-menu.ce +11 -9
  254. package/src/components/gui/menu/main-menu.ce +14 -12
  255. package/src/components/gui/menu/options-menu.ce +4 -3
  256. package/src/components/gui/menu/skills-menu.ce +2 -1
  257. package/src/components/gui/notification/notification.ce +7 -1
  258. package/src/components/gui/save-load.ce +13 -11
  259. package/src/components/gui/shop/shop.ce +20 -18
  260. package/src/components/gui/title-screen.ce +6 -4
  261. package/src/components/index.ts +2 -1
  262. package/src/components/interaction-components.ce +23 -0
  263. package/src/components/player-components-utils.spec.ts +109 -0
  264. package/src/components/player-components-utils.ts +205 -0
  265. package/src/components/player-components.ce +222 -0
  266. package/src/components/prebuilt/hp-bar.ce +4 -3
  267. package/src/components/prebuilt/light-halo.ce +2 -2
  268. package/src/components/scenes/canvas.ce +219 -15
  269. package/src/components/scenes/draw-map.ce +34 -22
  270. package/src/components/scenes/event-layer.ce +55 -4
  271. package/src/core/setup.ts +2 -2
  272. package/src/decorators/spritesheet.ts +8 -0
  273. package/src/i18n.spec.ts +39 -0
  274. package/src/i18n.ts +59 -0
  275. package/src/index.ts +7 -0
  276. package/src/module.ts +61 -11
  277. package/src/services/AbstractSocket.ts +3 -0
  278. package/src/services/actionInput.spec.ts +155 -0
  279. package/src/services/actionInput.ts +120 -0
  280. package/src/services/interactions.spec.ts +175 -0
  281. package/src/services/interactions.ts +722 -0
  282. package/src/services/keyboardControls.ts +2 -1
  283. package/src/services/loadMap.ts +5 -1
  284. package/src/services/mmorpg-connection.spec.ts +99 -0
  285. package/src/services/mmorpg-connection.ts +69 -0
  286. package/src/services/mmorpg.ts +70 -36
  287. package/src/services/pointerContext.spec.ts +36 -0
  288. package/src/services/pointerContext.ts +84 -0
  289. package/src/services/save.spec.ts +127 -0
  290. package/src/services/standalone-message.ts +7 -0
  291. package/src/services/standalone.spec.ts +54 -0
  292. package/src/services/standalone.ts +44 -12
  293. package/src/utils/getEntityProp.spec.ts +96 -0
  294. package/src/utils/getEntityProp.ts +4 -3
  295. package/src/utils/mapId.ts +2 -0
  296. package/src/utils/readPropValue.ts +16 -0
  297. package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +0 -1
  298. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js +0 -457
  299. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js.map +0 -1
  300. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js +0 -463
  301. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js.map +0 -1
  302. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js +0 -2191
  303. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js.map +0 -1
  304. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +0 -10
  305. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +0 -1
  306. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js +0 -91
  307. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js.map +0 -1
  308. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js.map +0 -1
  309. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js +0 -14
  310. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +0 -1
  311. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +0 -1
  312. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +0 -1
  313. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/index.js +0 -2
  314. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +0 -1
@@ -0,0 +1,346 @@
1
+ import { computed, signal } from "canvasengine";
2
+ import { Hooks } from "@rpgjs/common";
3
+ import { normalizeRoomMapId } from "../utils/mapId";
4
+
5
+ export interface ClientProjectileSpawn {
6
+ id: string;
7
+ type: string;
8
+ ownerId?: string;
9
+ origin: { x: number; y: number };
10
+ direction: { x: number; y: number };
11
+ speed: number;
12
+ range: number;
13
+ ttl: number;
14
+ spawnTick: number;
15
+ delay?: number;
16
+ index?: number;
17
+ count?: number;
18
+ params?: Record<string, unknown>;
19
+ collisionMask?: number;
20
+ ignoreOwner?: boolean;
21
+ predictImpact?: boolean;
22
+ }
23
+
24
+ export interface ClientProjectileImpact {
25
+ id: string;
26
+ targetId?: string;
27
+ x: number;
28
+ y: number;
29
+ distance?: number;
30
+ }
31
+
32
+ export interface ClientProjectileDestroy {
33
+ id: string;
34
+ reason?: string;
35
+ targetId?: string;
36
+ x?: number;
37
+ y?: number;
38
+ distance?: number;
39
+ }
40
+
41
+ export interface RenderedProjectileProps extends ClientProjectileSpawn {
42
+ x: number;
43
+ y: number;
44
+ angle: number;
45
+ distance: number;
46
+ elapsed: number;
47
+ progress: number;
48
+ impact?: ClientProjectileImpact;
49
+ impactElapsed?: number;
50
+ impactProgress?: number;
51
+ destroyed?: boolean;
52
+ }
53
+
54
+ export interface RenderedProjectile {
55
+ id: string;
56
+ type: string;
57
+ component: any;
58
+ props: RenderedProjectileProps;
59
+ }
60
+
61
+ export type ProjectilePredictionResolver = (
62
+ projectile: ClientProjectileSpawn,
63
+ ) => ClientProjectileImpact | null | undefined;
64
+
65
+ export interface ProjectileSpawnClock {
66
+ now?: number;
67
+ currentServerTick?: number;
68
+ tickDurationMs?: number;
69
+ mapId?: string;
70
+ }
71
+
72
+ interface RuntimeProjectile {
73
+ spawn: ClientProjectileSpawn;
74
+ component: any;
75
+ createdAt: number;
76
+ impact?: ClientProjectileImpact;
77
+ visualImpact?: ClientProjectileImpact;
78
+ predictedImpact?: ClientProjectileImpact;
79
+ impactStartedAt?: number;
80
+ destroyAt?: number;
81
+ destroyReason?: string;
82
+ }
83
+
84
+ export class ProjectileManager {
85
+ private readonly components = new Map<string, any>();
86
+ private readonly projectiles = new Map<string, RuntimeProjectile>();
87
+ private readonly version = signal(0);
88
+ private readonly impactDurationMs = 350;
89
+ private mapId?: string;
90
+
91
+ constructor(
92
+ private readonly hooks: Hooks,
93
+ private readonly predictionResolver?: ProjectilePredictionResolver,
94
+ ) {}
95
+
96
+ current = computed<RenderedProjectile[]>(() => {
97
+ this.version();
98
+ const now = Date.now();
99
+ const rendered: RenderedProjectile[] = [];
100
+ for (const projectile of this.projectiles.values()) {
101
+ const props = this.toProps(projectile, now);
102
+ if (!props) {
103
+ continue;
104
+ }
105
+ rendered.push({
106
+ id: projectile.spawn.id,
107
+ type: projectile.spawn.type,
108
+ component: projectile.component,
109
+ props,
110
+ });
111
+ }
112
+ return rendered;
113
+ });
114
+
115
+ register(type: string, component: any): any {
116
+ this.components.set(type, component);
117
+ return component;
118
+ }
119
+
120
+ get(type: string): any {
121
+ return this.components.get(type);
122
+ }
123
+
124
+ setMapId(mapId: string | undefined): void {
125
+ const normalizedMapId = normalizeRoomMapId(mapId);
126
+ if (this.mapId === normalizedMapId) return;
127
+ this.mapId = normalizedMapId;
128
+ this.clear();
129
+ }
130
+
131
+ getMapId(): string | undefined {
132
+ return this.mapId;
133
+ }
134
+
135
+ spawnBatch(projectiles: ClientProjectileSpawn[], clock: ProjectileSpawnClock = {}): void {
136
+ if (!this.acceptsMap(clock.mapId)) return;
137
+ const now = clock.now ?? Date.now();
138
+ for (const projectile of projectiles) {
139
+ const component = this.components.get(projectile.type);
140
+ if (!component) {
141
+ continue;
142
+ }
143
+ const runtime: RuntimeProjectile = {
144
+ spawn: {
145
+ ...projectile,
146
+ delay: projectile.delay ?? 0,
147
+ index: projectile.index ?? 0,
148
+ count: projectile.count ?? 1,
149
+ },
150
+ component,
151
+ createdAt: now,
152
+ };
153
+ this.setPredictedImpact(runtime);
154
+ this.projectiles.set(projectile.id, runtime);
155
+ this.hooks.callHooks("client-projectiles-onSpawn", runtime.spawn).subscribe();
156
+ }
157
+ this.touch();
158
+ }
159
+
160
+ impactBatch(impacts: ClientProjectileImpact[], context: { mapId?: string } = {}): void {
161
+ if (!this.acceptsMap(context.mapId)) return;
162
+ const now = Date.now();
163
+ for (const impact of impacts) {
164
+ const projectile = this.projectiles.get(impact.id);
165
+ if (!projectile) {
166
+ continue;
167
+ }
168
+ this.setImpact(projectile, impact, now);
169
+ this.hooks.callHooks("client-projectiles-onImpact", this.toProps(projectile, now)).subscribe();
170
+ }
171
+ this.touch();
172
+ }
173
+
174
+ destroyBatch(projectiles: ClientProjectileDestroy[], context: { mapId?: string } = {}): void {
175
+ if (!this.acceptsMap(context.mapId)) return;
176
+ const now = Date.now();
177
+ for (const destroyed of projectiles) {
178
+ const projectile = this.projectiles.get(destroyed.id);
179
+ if (!projectile) {
180
+ continue;
181
+ }
182
+ if (destroyed.reason === "hit") {
183
+ const current = this.toProps(projectile, now);
184
+ this.setImpact(projectile, {
185
+ id: destroyed.id,
186
+ targetId: destroyed.targetId ?? projectile.impact?.targetId,
187
+ x: destroyed.x ?? projectile.impact?.x ?? current?.x ?? projectile.spawn.origin.x,
188
+ y: destroyed.y ?? projectile.impact?.y ?? current?.y ?? projectile.spawn.origin.y,
189
+ distance: destroyed.distance ?? projectile.impact?.distance ?? current?.distance,
190
+ }, now);
191
+ }
192
+ projectile.destroyReason = destroyed.reason;
193
+ projectile.destroyAt = projectile.destroyAt ?? (
194
+ projectile.impact && projectile.impactStartedAt !== undefined
195
+ ? projectile.impactStartedAt + this.impactDurationMs
196
+ : now
197
+ );
198
+ this.hooks.callHooks("client-projectiles-onDestroy", this.toProps(projectile, now)).subscribe();
199
+ }
200
+ this.touch();
201
+ }
202
+
203
+ clear(): void {
204
+ this.projectiles.clear();
205
+ this.touch();
206
+ }
207
+
208
+ step(): void {
209
+ const now = Date.now();
210
+ let changed = false;
211
+ for (const [id, projectile] of this.projectiles) {
212
+ const props = this.toProps(projectile, now);
213
+ if (
214
+ (!props && !this.isWaitingForDelay(projectile, now)) ||
215
+ (projectile.destroyAt !== undefined && now >= projectile.destroyAt)
216
+ ) {
217
+ this.projectiles.delete(id);
218
+ changed = true;
219
+ }
220
+ }
221
+ this.touch(changed || this.projectiles.size > 0);
222
+ }
223
+
224
+ private toProps(projectile: RuntimeProjectile, now: number): RenderedProjectileProps | null {
225
+ const spawn = projectile.spawn;
226
+ const delayMs = (spawn.delay ?? 0) * 1000;
227
+ const elapsedMs = now - projectile.createdAt - delayMs;
228
+ if (elapsedMs < 0) {
229
+ return null;
230
+ }
231
+ const elapsed = elapsedMs / 1000;
232
+ const ttl = Math.max(0.001, spawn.ttl);
233
+ const rawDistance = Math.min(spawn.speed * elapsed, spawn.range);
234
+ const predictedImpact = this.getActivePredictedImpact(projectile, now, rawDistance);
235
+ const visualImpact = projectile.visualImpact ?? projectile.impact;
236
+ const distance = visualImpact?.distance ?? predictedImpact?.distance ?? rawDistance;
237
+ const progress = Math.min(1, distance / spawn.range);
238
+ const x = visualImpact?.x ?? predictedImpact?.x ?? spawn.origin.x + spawn.direction.x * distance;
239
+ const y = visualImpact?.y ?? predictedImpact?.y ?? spawn.origin.y + spawn.direction.y * distance;
240
+ const impactElapsedMs = projectile.impactStartedAt !== undefined
241
+ ? Math.max(0, now - projectile.impactStartedAt)
242
+ : undefined;
243
+ return {
244
+ ...spawn,
245
+ x,
246
+ y,
247
+ angle: Math.atan2(spawn.direction.y, spawn.direction.x),
248
+ distance,
249
+ elapsed,
250
+ progress,
251
+ impact: projectile.impact,
252
+ impactElapsed: impactElapsedMs === undefined ? undefined : impactElapsedMs / 1000,
253
+ impactProgress: impactElapsedMs === undefined
254
+ ? undefined
255
+ : Math.min(1, impactElapsedMs / this.impactDurationMs),
256
+ destroyed: projectile.destroyAt !== undefined,
257
+ ttl,
258
+ };
259
+ }
260
+
261
+ private acceptsMap(mapId: string | undefined): boolean {
262
+ const normalizedMapId = normalizeRoomMapId(mapId);
263
+ return !normalizedMapId || !this.mapId || normalizedMapId === this.mapId;
264
+ }
265
+
266
+ private isWaitingForDelay(projectile: RuntimeProjectile, now: number): boolean {
267
+ const delayMs = (projectile.spawn.delay ?? 0) * 1000;
268
+ return now - projectile.createdAt - delayMs < 0;
269
+ }
270
+
271
+ private setPredictedImpact(projectile: RuntimeProjectile): void {
272
+ if (projectile.spawn.predictImpact === false) {
273
+ return;
274
+ }
275
+ const impact = this.predictionResolver?.(projectile.spawn);
276
+ if (!impact || !Number.isFinite(impact.x) || !Number.isFinite(impact.y)) {
277
+ return;
278
+ }
279
+ const distance = typeof impact.distance === "number" && Number.isFinite(impact.distance)
280
+ ? impact.distance
281
+ : Math.hypot(impact.x - projectile.spawn.origin.x, impact.y - projectile.spawn.origin.y);
282
+ if (!Number.isFinite(distance) || distance < 0 || distance > projectile.spawn.range) {
283
+ return;
284
+ }
285
+ projectile.predictedImpact = {
286
+ ...impact,
287
+ distance,
288
+ };
289
+ }
290
+
291
+ private getActivePredictedImpact(
292
+ projectile: RuntimeProjectile,
293
+ now: number,
294
+ rawDistance: number,
295
+ ): ClientProjectileImpact | undefined {
296
+ if (!projectile.predictedImpact || projectile.impact) {
297
+ return undefined;
298
+ }
299
+ const distance = projectile.predictedImpact.distance;
300
+ if (distance === undefined || rawDistance < distance) {
301
+ return undefined;
302
+ }
303
+ return projectile.predictedImpact;
304
+ }
305
+
306
+ private setImpact(projectile: RuntimeProjectile, impact: ClientProjectileImpact, now: number): void {
307
+ projectile.visualImpact = this.resolveVisualImpact(projectile, impact, now);
308
+ projectile.impact = impact;
309
+ projectile.predictedImpact = undefined;
310
+ projectile.impactStartedAt = projectile.impactStartedAt ?? now;
311
+ const impactDestroyAt = projectile.impactStartedAt + this.impactDurationMs;
312
+ projectile.destroyAt = Math.max(projectile.destroyAt ?? 0, impactDestroyAt);
313
+ }
314
+
315
+ private resolveVisualImpact(
316
+ projectile: RuntimeProjectile,
317
+ impact: ClientProjectileImpact,
318
+ now: number,
319
+ ): ClientProjectileImpact {
320
+ const predicted = projectile.predictedImpact;
321
+ if (!predicted || !this.isSameTarget(predicted, impact)) {
322
+ return impact;
323
+ }
324
+ const distance = predicted.distance;
325
+ if (distance === undefined) {
326
+ return impact;
327
+ }
328
+ const delayMs = (projectile.spawn.delay ?? 0) * 1000;
329
+ const elapsedMs = now - projectile.createdAt - delayMs;
330
+ if (elapsedMs < 0) {
331
+ return impact;
332
+ }
333
+ const rawDistance = Math.min(projectile.spawn.speed * (elapsedMs / 1000), projectile.spawn.range);
334
+ return rawDistance >= distance ? predicted : impact;
335
+ }
336
+
337
+ private isSameTarget(a: ClientProjectileImpact, b: ClientProjectileImpact): boolean {
338
+ return a.targetId !== undefined && a.targetId === b.targetId;
339
+ }
340
+
341
+ private touch(force = true): void {
342
+ if (force) {
343
+ this.version.update((value) => value + 1);
344
+ }
345
+ }
346
+ }
@@ -0,0 +1,340 @@
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("tracks GUI open ids and sends them back when closing", async () => {
68
+ const { gui, socket } = await createGui();
69
+ await gui._initialize();
70
+ const openHandler = socket.on.mock.calls.find(([event]) => event === "gui.open")?.[1];
71
+
72
+ openHandler({
73
+ guiId: PrebuiltGui.Dialog,
74
+ guiOpenId: "open-1",
75
+ data: { message: "Hello" },
76
+ });
77
+
78
+ expect(gui.get(PrebuiltGui.Dialog)?.openId).toBe("open-1");
79
+
80
+ gui.guiClose(PrebuiltGui.Dialog, 0, gui.get(PrebuiltGui.Dialog)?.openId);
81
+
82
+ expect(socket.emit).toHaveBeenCalledWith("gui.exit", {
83
+ guiId: PrebuiltGui.Dialog,
84
+ guiOpenId: "open-1",
85
+ data: 0,
86
+ });
87
+ });
88
+
89
+ test("does not emit malformed GUI open ids", async () => {
90
+ const { gui, socket } = await createGui();
91
+
92
+ gui.guiClose(PrebuiltGui.Dialog, 0, (() => "open-1") as any);
93
+
94
+ expect(socket.emit).toHaveBeenCalledWith("gui.exit", {
95
+ guiId: PrebuiltGui.Dialog,
96
+ guiOpenId: undefined,
97
+ data: 0,
98
+ });
99
+ });
100
+
101
+ test("ignores stale server close events for previous GUI opens", async () => {
102
+ const { gui, socket } = await createGui();
103
+ await gui._initialize();
104
+ const openHandler = socket.on.mock.calls.find(([event]) => event === "gui.open")?.[1];
105
+ const exitHandler = socket.on.mock.calls.find(([event]) => event === "gui.exit")?.[1];
106
+
107
+ openHandler({
108
+ guiId: PrebuiltGui.Dialog,
109
+ guiOpenId: "open-1",
110
+ data: { message: "First" },
111
+ });
112
+ openHandler({
113
+ guiId: PrebuiltGui.Dialog,
114
+ guiOpenId: "open-2",
115
+ data: { message: "Second" },
116
+ });
117
+
118
+ exitHandler({
119
+ guiId: PrebuiltGui.Dialog,
120
+ guiOpenId: "open-1",
121
+ });
122
+
123
+ expect(gui.isDisplaying(PrebuiltGui.Dialog)).toBe(true);
124
+ expect(gui.get(PrebuiltGui.Dialog)?.data()).toEqual({ message: "Second" });
125
+
126
+ exitHandler({
127
+ guiId: PrebuiltGui.Dialog,
128
+ guiOpenId: "open-2",
129
+ });
130
+
131
+ expect(gui.isDisplaying(PrebuiltGui.Dialog)).toBe(false);
132
+ });
133
+
134
+ test("separates CanvasEngine and Vue GUI registries", async () => {
135
+ const { gui } = await createGui();
136
+
137
+ gui.add({
138
+ id: "canvas-tooltip",
139
+ component: CanvasGui,
140
+ attachToSprite: true,
141
+ });
142
+ gui.add({
143
+ id: "inventory",
144
+ component: VueInventory,
145
+ });
146
+ gui.add(VueTooltip);
147
+
148
+ expect(gui.get("canvas-tooltip")?.component).toBe(CanvasGui);
149
+ expect(gui.get("inventory")?.component).toBe(VueInventory);
150
+ expect(gui.get("tooltip")?.component).toBe(VueTooltip);
151
+ expect(gui.getAttachedGuis().map(item => item.name)).toEqual(["canvas-tooltip"]);
152
+ expect(gui.getAttachedVueGuis().map(item => item.name)).toEqual(["tooltip"]);
153
+ });
154
+
155
+ test("synchronizes Vue GUI display and hide states through the Vue bridge", async () => {
156
+ const { gui } = await createGui();
157
+ const bridge = {
158
+ updateGuiState: vi.fn(),
159
+ initializeGuiStates: vi.fn(),
160
+ };
161
+
162
+ gui.add({
163
+ id: "inventory",
164
+ component: VueInventory,
165
+ });
166
+ gui._setVueGuiInstance(bridge);
167
+
168
+ expect(bridge.initializeGuiStates).toHaveBeenCalledWith([
169
+ expect.objectContaining({
170
+ name: "inventory",
171
+ display: false,
172
+ data: {},
173
+ attachToSprite: false,
174
+ }),
175
+ ]);
176
+
177
+ gui.display("inventory", { gold: 12 });
178
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
179
+ expect.objectContaining({
180
+ name: "inventory",
181
+ display: true,
182
+ data: { gold: 12 },
183
+ attachToSprite: false,
184
+ }),
185
+ );
186
+
187
+ gui.hide("inventory");
188
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
189
+ expect.objectContaining({
190
+ name: "inventory",
191
+ display: false,
192
+ }),
193
+ );
194
+ });
195
+
196
+ test("waits for Vue GUI dependencies before display", async () => {
197
+ const { gui } = await createGui();
198
+ const bridge = {
199
+ updateGuiState: vi.fn(),
200
+ initializeGuiStates: vi.fn(),
201
+ };
202
+ const dependency = signal<any>(undefined);
203
+
204
+ gui.add({
205
+ id: "inventory",
206
+ component: VueInventory,
207
+ dependencies: () => [dependency],
208
+ });
209
+ gui._setVueGuiInstance(bridge);
210
+ gui.display("inventory", { items: ["potion"] });
211
+
212
+ expect(gui.isDisplaying("inventory")).toBe(false);
213
+ expect(bridge.updateGuiState).not.toHaveBeenCalledWith(
214
+ expect.objectContaining({
215
+ display: true,
216
+ }),
217
+ );
218
+
219
+ dependency.set({ id: "player" });
220
+
221
+ expect(gui.isDisplaying("inventory")).toBe(true);
222
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
223
+ expect.objectContaining({
224
+ name: "inventory",
225
+ display: true,
226
+ data: { items: ["potion"] },
227
+ }),
228
+ );
229
+ });
230
+
231
+ test("allows Vue GUI entries to replace prebuilt CanvasEngine GUIs", async () => {
232
+ const { gui } = await createGui();
233
+ const bridge = {
234
+ updateGuiState: vi.fn(),
235
+ initializeGuiStates: vi.fn(),
236
+ };
237
+
238
+ gui._setVueGuiInstance(bridge);
239
+ gui.add({
240
+ id: PrebuiltGui.Dialog,
241
+ component: VueDialog,
242
+ });
243
+
244
+ expect(gui.get(PrebuiltGui.Dialog)?.component).toBe(VueDialog);
245
+ expect(gui.getAll()[PrebuiltGui.Dialog].component).toBe(VueDialog);
246
+ expect((gui as any).gui()[PrebuiltGui.Dialog]).toBeUndefined();
247
+ expect(gui.getVueGuis().filter(item => item.name === PrebuiltGui.Dialog)).toHaveLength(1);
248
+
249
+ gui.display(PrebuiltGui.Dialog, { text: "Hello" });
250
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
251
+ expect.objectContaining({
252
+ name: PrebuiltGui.Dialog,
253
+ display: true,
254
+ data: { text: "Hello" },
255
+ }),
256
+ );
257
+
258
+ gui.hide(PrebuiltGui.Dialog);
259
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
260
+ expect.objectContaining({
261
+ name: PrebuiltGui.Dialog,
262
+ display: false,
263
+ }),
264
+ );
265
+ });
266
+
267
+ test("allows CanvasEngine GUI entries to replace Vue GUI entries with the same id", async () => {
268
+ const { gui } = await createGui();
269
+ const bridge = {
270
+ updateGuiState: vi.fn(),
271
+ initializeGuiStates: vi.fn(),
272
+ };
273
+
274
+ gui._setVueGuiInstance(bridge);
275
+ gui.add({
276
+ id: PrebuiltGui.Dialog,
277
+ component: VueDialog,
278
+ });
279
+ gui.add({
280
+ id: PrebuiltGui.Dialog,
281
+ component: CanvasGui,
282
+ });
283
+
284
+ expect(gui.get(PrebuiltGui.Dialog)?.component).toBe(CanvasGui);
285
+ expect(gui.getVueGuis().some(item => item.name === PrebuiltGui.Dialog)).toBe(false);
286
+ expect((gui as any).gui()[PrebuiltGui.Dialog].component).toBe(CanvasGui);
287
+ expect(bridge.initializeGuiStates).toHaveBeenLastCalledWith([]);
288
+ });
289
+
290
+ test("keeps main menu optimistic reducers when a Vue GUI replaces the prebuilt component", async () => {
291
+ const { gui, socket } = await createGui();
292
+ const bridge = {
293
+ updateGuiState: vi.fn(),
294
+ initializeGuiStates: vi.fn(),
295
+ };
296
+
297
+ gui.add({
298
+ id: PrebuiltGui.MainMenu,
299
+ component: VueMainMenu,
300
+ });
301
+ gui._setVueGuiInstance(bridge);
302
+ gui.display(PrebuiltGui.MainMenu, {
303
+ items: [
304
+ {
305
+ id: "potion",
306
+ quantity: 2,
307
+ },
308
+ ],
309
+ });
310
+
311
+ gui.guiInteraction(PrebuiltGui.MainMenu, "useItem", { id: "potion" });
312
+
313
+ expect(gui.get(PrebuiltGui.MainMenu)?.data().items).toEqual([
314
+ {
315
+ id: "potion",
316
+ quantity: 1,
317
+ },
318
+ ]);
319
+ expect(bridge.updateGuiState).toHaveBeenLastCalledWith(
320
+ expect.objectContaining({
321
+ name: PrebuiltGui.MainMenu,
322
+ data: {
323
+ items: [
324
+ {
325
+ id: "potion",
326
+ quantity: 1,
327
+ },
328
+ ],
329
+ },
330
+ }),
331
+ );
332
+ expect(socket.emit).toHaveBeenCalledWith(
333
+ "gui.interaction",
334
+ expect.objectContaining({
335
+ guiId: PrebuiltGui.MainMenu,
336
+ name: "useItem",
337
+ }),
338
+ );
339
+ });
340
+ });