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

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 (325) hide show
  1. package/CHANGELOG.md +130 -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 +132 -57
  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 +191 -19
  36. package/dist/RpgClientEngine.js +693 -74
  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 +748 -249
  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.d.ts +51 -2
  90. package/dist/components/gui/mobile/index.js +14 -6
  91. package/dist/components/gui/mobile/index.js.map +1 -1
  92. package/dist/components/gui/mobile/index.spec.d.ts +1 -0
  93. package/dist/components/gui/mobile/mobile.ce.js +309 -59
  94. package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
  95. package/dist/components/gui/notification/notification.ce.js +28 -26
  96. package/dist/components/gui/notification/notification.ce.js.map +1 -1
  97. package/dist/components/gui/save-load.ce.js +81 -256
  98. package/dist/components/gui/save-load.ce.js.map +1 -1
  99. package/dist/components/gui/shop/shop.ce.js +107 -142
  100. package/dist/components/gui/shop/shop.ce.js.map +1 -1
  101. package/dist/components/gui/title-screen.ce.js +50 -73
  102. package/dist/components/gui/title-screen.ce.js.map +1 -1
  103. package/dist/components/index.d.ts +2 -1
  104. package/dist/components/index.js +1 -0
  105. package/dist/components/interaction-components.ce.js +22 -0
  106. package/dist/components/interaction-components.ce.js.map +1 -0
  107. package/dist/components/player-components-utils.d.ts +67 -0
  108. package/dist/components/player-components-utils.js +162 -0
  109. package/dist/components/player-components-utils.js.map +1 -0
  110. package/dist/components/player-components-utils.spec.d.ts +1 -0
  111. package/dist/components/player-components.ce.js +191 -0
  112. package/dist/components/player-components.ce.js.map +1 -0
  113. package/dist/components/prebuilt/hp-bar.ce.js +45 -45
  114. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
  115. package/dist/components/prebuilt/light-halo.ce.js +39 -60
  116. package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
  117. package/dist/components/scenes/canvas.ce.js +205 -27
  118. package/dist/components/scenes/canvas.ce.js.map +1 -1
  119. package/dist/components/scenes/draw-map.ce.js +39 -39
  120. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  121. package/dist/components/scenes/event-layer.ce.js +51 -10
  122. package/dist/components/scenes/event-layer.ce.js.map +1 -1
  123. package/dist/core/inject.js +1 -1
  124. package/dist/core/inject.js.map +1 -1
  125. package/dist/core/setup.js +1 -1
  126. package/dist/core/setup.js.map +1 -1
  127. package/dist/decorators/spritesheet.d.ts +1 -0
  128. package/dist/decorators/spritesheet.js +11 -0
  129. package/dist/decorators/spritesheet.js.map +1 -0
  130. package/dist/i18n.d.ts +55 -0
  131. package/dist/i18n.js +60 -0
  132. package/dist/i18n.js.map +1 -0
  133. package/dist/i18n.spec.d.ts +1 -0
  134. package/dist/index.d.ts +8 -0
  135. package/dist/index.js +29 -21
  136. package/dist/module.js +45 -4
  137. package/dist/module.js.map +1 -1
  138. package/dist/node_modules/.pnpm/{@signe_di@2.9.0 → @signe_di@3.1.0}/node_modules/@signe/di/dist/index.js +7 -117
  139. package/dist/node_modules/.pnpm/@signe_di@3.1.0/node_modules/@signe/di/dist/index.js.map +1 -0
  140. package/dist/node_modules/.pnpm/@signe_reactive@3.1.0/node_modules/@signe/reactive/dist/index.js +239 -0
  141. package/dist/node_modules/.pnpm/@signe_reactive@3.1.0/node_modules/@signe/reactive/dist/index.js.map +1 -0
  142. package/dist/node_modules/.pnpm/@signe_room@3.1.0_react@19.2.7/node_modules/@signe/room/dist/chunk-EUXUH3YW.js +13 -0
  143. 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
  144. package/dist/node_modules/.pnpm/@signe_room@3.1.0_react@19.2.7/node_modules/@signe/room/dist/index.js +698 -0
  145. package/dist/node_modules/.pnpm/@signe_room@3.1.0_react@19.2.7/node_modules/@signe/room/dist/index.js.map +1 -0
  146. package/dist/node_modules/.pnpm/@signe_sync@3.1.0_react@19.2.7/node_modules/@signe/sync/dist/client/index.js +43 -0
  147. 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
  148. 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
  149. package/dist/node_modules/.pnpm/@signe_sync@3.1.0_react@19.2.7/node_modules/@signe/sync/dist/index.js.map +1 -0
  150. 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
  151. package/dist/node_modules/.pnpm/partysocket@1.2.0_react@19.2.7/node_modules/partysocket/dist/index.js.map +1 -0
  152. 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
  153. package/dist/node_modules/.pnpm/partysocket@1.2.0_react@19.2.7/node_modules/partysocket/dist/ws.js.map +1 -0
  154. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/index.js +13 -0
  155. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/index.js.map +1 -0
  156. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js +106 -0
  157. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js.map +1 -0
  158. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js +13 -0
  159. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js.map +1 -0
  160. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js +121 -0
  161. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js.map +1 -0
  162. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js +10 -0
  163. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js.map +1 -0
  164. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js +110 -0
  165. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js.map +1 -0
  166. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js +99 -0
  167. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js.map +1 -0
  168. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js +82 -0
  169. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js.map +1 -0
  170. 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
  171. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js.map +1 -0
  172. package/dist/presets/animation.js.map +1 -1
  173. package/dist/presets/faceset.js.map +1 -1
  174. package/dist/presets/icon.js.map +1 -1
  175. package/dist/presets/index.js.map +1 -1
  176. package/dist/presets/lpc.js.map +1 -1
  177. package/dist/presets/rmspritesheet.js.map +1 -1
  178. package/dist/services/AbstractSocket.d.ts +2 -0
  179. package/dist/services/AbstractSocket.js.map +1 -1
  180. package/dist/services/actionInput.d.ts +14 -0
  181. package/dist/services/actionInput.js +59 -0
  182. package/dist/services/actionInput.js.map +1 -0
  183. package/dist/services/actionInput.spec.d.ts +1 -0
  184. package/dist/services/cameraFollow.d.ts +51 -0
  185. package/dist/services/cameraFollow.js +134 -0
  186. package/dist/services/cameraFollow.js.map +1 -0
  187. package/dist/services/cameraFollow.spec.d.ts +1 -0
  188. package/dist/services/interactions.d.ts +159 -0
  189. package/dist/services/interactions.js +460 -0
  190. package/dist/services/interactions.js.map +1 -0
  191. package/dist/services/interactions.spec.d.ts +1 -0
  192. package/dist/services/keyboardControls.d.ts +1 -0
  193. package/dist/services/keyboardControls.js +1 -0
  194. package/dist/services/keyboardControls.js.map +1 -1
  195. package/dist/services/loadMap.d.ts +9 -0
  196. package/dist/services/loadMap.js +1 -1
  197. package/dist/services/loadMap.js.map +1 -1
  198. package/dist/services/mmorpg-connection.d.ts +5 -0
  199. package/dist/services/mmorpg-connection.js +50 -0
  200. package/dist/services/mmorpg-connection.js.map +1 -0
  201. package/dist/services/mmorpg-connection.spec.d.ts +1 -0
  202. package/dist/services/mmorpg.d.ts +11 -4
  203. package/dist/services/mmorpg.js +57 -33
  204. package/dist/services/mmorpg.js.map +1 -1
  205. package/dist/services/pointerContext.d.ts +11 -0
  206. package/dist/services/pointerContext.js +48 -0
  207. package/dist/services/pointerContext.js.map +1 -0
  208. package/dist/services/pointerContext.spec.d.ts +1 -0
  209. package/dist/services/save.js.map +1 -1
  210. package/dist/services/save.spec.d.ts +1 -0
  211. package/dist/services/standalone-message.d.ts +1 -0
  212. package/dist/services/standalone-message.js +9 -0
  213. package/dist/services/standalone-message.js.map +1 -0
  214. package/dist/services/standalone.d.ts +4 -1
  215. package/dist/services/standalone.js +36 -16
  216. package/dist/services/standalone.js.map +1 -1
  217. package/dist/services/standalone.spec.d.ts +1 -0
  218. package/dist/utils/getEntityProp.js +4 -3
  219. package/dist/utils/getEntityProp.js.map +1 -1
  220. package/dist/utils/getEntityProp.spec.d.ts +1 -0
  221. package/dist/utils/mapId.d.ts +1 -0
  222. package/dist/utils/mapId.js +6 -0
  223. package/dist/utils/mapId.js.map +1 -0
  224. package/dist/utils/readPropValue.d.ts +2 -0
  225. package/dist/utils/readPropValue.js +13 -0
  226. package/dist/utils/readPropValue.js.map +1 -0
  227. package/package.json +13 -13
  228. package/src/Game/AnimationManager.spec.ts +30 -0
  229. package/src/Game/AnimationManager.ts +26 -10
  230. package/src/Game/ClientVisuals.spec.ts +56 -0
  231. package/src/Game/ClientVisuals.ts +184 -0
  232. package/src/Game/EventComponentResolver.spec.ts +84 -0
  233. package/src/Game/EventComponentResolver.ts +74 -0
  234. package/src/Game/Map.ts +134 -2
  235. package/src/Game/Object.spec.ts +59 -0
  236. package/src/Game/Object.ts +181 -77
  237. package/src/Game/ProjectileManager.spec.ts +449 -0
  238. package/src/Game/ProjectileManager.ts +346 -0
  239. package/src/Gui/Gui.spec.ts +359 -0
  240. package/src/Gui/Gui.ts +178 -67
  241. package/src/Resource.ts +1 -2
  242. package/src/RpgClient.ts +220 -17
  243. package/src/RpgClientEngine.ts +995 -133
  244. package/src/components/animations/fx.ce +101 -0
  245. package/src/components/animations/index.ts +4 -2
  246. package/src/components/character.ce +839 -45
  247. package/src/components/dynamics/bar.ce +88 -0
  248. package/src/components/dynamics/image.ce +21 -0
  249. package/src/components/dynamics/parse-value.spec.ts +83 -0
  250. package/src/components/dynamics/parse-value.ts +111 -37
  251. package/src/components/dynamics/shape-utils.spec.ts +46 -0
  252. package/src/components/dynamics/shape-utils.ts +61 -0
  253. package/src/components/dynamics/shape.ce +90 -0
  254. package/src/components/dynamics/text.ce +35 -149
  255. package/src/components/gui/dialogbox/index.ce +52 -21
  256. package/src/components/gui/gameover.ce +6 -4
  257. package/src/components/gui/menu/equip-menu.ce +11 -9
  258. package/src/components/gui/menu/exit-menu.ce +6 -4
  259. package/src/components/gui/menu/items-menu.ce +11 -9
  260. package/src/components/gui/menu/main-menu.ce +14 -12
  261. package/src/components/gui/menu/options-menu.ce +4 -3
  262. package/src/components/gui/menu/skills-menu.ce +2 -1
  263. package/src/components/gui/mobile/index.spec.ts +94 -0
  264. package/src/components/gui/mobile/index.ts +74 -6
  265. package/src/components/gui/mobile/mobile.ce +347 -65
  266. package/src/components/gui/notification/notification.ce +7 -1
  267. package/src/components/gui/save-load.ce +13 -11
  268. package/src/components/gui/shop/shop.ce +20 -18
  269. package/src/components/gui/title-screen.ce +6 -4
  270. package/src/components/index.ts +2 -1
  271. package/src/components/interaction-components.ce +23 -0
  272. package/src/components/player-components-utils.spec.ts +109 -0
  273. package/src/components/player-components-utils.ts +205 -0
  274. package/src/components/player-components.ce +222 -0
  275. package/src/components/prebuilt/hp-bar.ce +4 -3
  276. package/src/components/prebuilt/light-halo.ce +2 -2
  277. package/src/components/scenes/canvas.ce +219 -15
  278. package/src/components/scenes/draw-map.ce +34 -22
  279. package/src/components/scenes/event-layer.ce +55 -4
  280. package/src/core/setup.ts +2 -2
  281. package/src/decorators/spritesheet.ts +8 -0
  282. package/src/i18n.spec.ts +39 -0
  283. package/src/i18n.ts +59 -0
  284. package/src/index.ts +19 -0
  285. package/src/module.ts +61 -11
  286. package/src/services/AbstractSocket.ts +3 -0
  287. package/src/services/actionInput.spec.ts +155 -0
  288. package/src/services/actionInput.ts +120 -0
  289. package/src/services/cameraFollow.spec.ts +220 -0
  290. package/src/services/cameraFollow.ts +222 -0
  291. package/src/services/interactions.spec.ts +175 -0
  292. package/src/services/interactions.ts +722 -0
  293. package/src/services/keyboardControls.ts +2 -1
  294. package/src/services/loadMap.ts +5 -1
  295. package/src/services/mmorpg-connection.spec.ts +99 -0
  296. package/src/services/mmorpg-connection.ts +69 -0
  297. package/src/services/mmorpg.ts +70 -36
  298. package/src/services/pointerContext.spec.ts +36 -0
  299. package/src/services/pointerContext.ts +84 -0
  300. package/src/services/save.spec.ts +127 -0
  301. package/src/services/standalone-message.ts +7 -0
  302. package/src/services/standalone.spec.ts +54 -0
  303. package/src/services/standalone.ts +44 -12
  304. package/src/utils/getEntityProp.spec.ts +96 -0
  305. package/src/utils/getEntityProp.ts +4 -3
  306. package/src/utils/mapId.ts +2 -0
  307. package/src/utils/readPropValue.ts +16 -0
  308. package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +0 -1
  309. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js +0 -457
  310. package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js.map +0 -1
  311. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js +0 -463
  312. package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js.map +0 -1
  313. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js +0 -2191
  314. package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js.map +0 -1
  315. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +0 -10
  316. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +0 -1
  317. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js +0 -91
  318. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js.map +0 -1
  319. package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js.map +0 -1
  320. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js +0 -14
  321. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +0 -1
  322. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +0 -1
  323. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +0 -1
  324. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/index.js +0 -2
  325. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +0 -1
@@ -1,11 +1,14 @@
1
1
  import Canvas from "./components/scenes/canvas.ce";
2
+ import BuiltinSceneMap from "./components/scenes/draw-map.ce";
2
3
  import { inject } from './core/inject'
3
- import { signal, bootstrapCanvas, Howl, trigger } from "canvasengine";
4
+ import { signal, bootstrapCanvas, Howl, trigger, type Trigger } from "canvasengine";
4
5
  import { AbstractWebsocket, WebSocketToken } from "./services/AbstractSocket";
5
6
  import { LoadMapService, LoadMapToken } from "./services/loadMap";
6
7
  import { RpgSound } from "./Sound";
7
8
  import { RpgResource } from "./Resource";
8
- import { Hooks, ModulesToken, Direction } from "@rpgjs/common";
9
+ import { getOrCreateI18nService, Hooks, ModulesToken, Direction, normalizeLightingState, Vector2, type I18nParams, type I18nService } from "@rpgjs/common";
10
+ import type { EventComponentConfig } from "./RpgClient";
11
+ import type { RpgClientEvent } from "./Game/Event";
9
12
  import { load } from "@signe/sync";
10
13
  import { RpgClientMap } from "./Game/Map"
11
14
  import { RpgGui } from "./Gui/Gui";
@@ -14,24 +17,139 @@ import { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, swit
14
17
  import { GlobalConfigToken } from "./module";
15
18
  import * as PIXI from "pixi.js";
16
19
  import { PrebuiltComponentAnimations } from "./components/animations";
20
+ import TextComponent from "./components/dynamics/text.ce";
21
+ import BarComponent from "./components/dynamics/bar.ce";
22
+ import ShapeComponent from "./components/dynamics/shape.ce";
23
+ import ImageComponent from "./components/dynamics/image.ce";
17
24
  import {
18
25
  PredictionController,
19
26
  type PredictionHistoryEntry,
20
27
  type PredictionState,
28
+ type RpgActionInput,
29
+ type RpgActionName,
30
+ type RpgDashInput,
31
+ type RpgMovementInput,
21
32
  } from "@rpgjs/common";
22
33
  import { NotificationManager } from "./Gui/NotificationManager";
23
34
  import { SaveClientService } from "./services/save";
35
+ import { getCanMoveValue } from "./utils/readPropValue";
36
+ import { ProjectileManager, type ClientProjectileImpact, type ClientProjectileSpawn } from "./Game/ProjectileManager";
37
+ import { ClientVisualRegistry, type ClientVisualHandler, type ClientVisualMap, type ClientVisualPacket } from "./Game/ClientVisuals";
38
+ import { normalizeActionInput } from "./services/actionInput";
39
+ import { createClientPointerContext, type ClientPointerContext } from "./services/pointerContext";
40
+ import { RpgClientInteractions } from "./services/interactions";
41
+ import { normalizeRoomMapId } from "./utils/mapId";
42
+ import { EventComponentResolverRegistry, type EventComponentResolver } from "./Game/EventComponentResolver";
43
+ import { RpgClientBuiltinI18n } from "./i18n";
44
+ import type { CameraFollowSmoothMove } from "./services/cameraFollow";
45
+ export type {
46
+ CameraFollowEase,
47
+ CameraFollowSmoothMove,
48
+ CameraFollowSmoothMoveOptions,
49
+ } from "./services/cameraFollow";
24
50
 
25
51
  interface MovementTrajectoryPoint {
26
52
  frame: number;
27
53
  tick: number;
28
54
  timestamp: number;
29
- input: Direction;
55
+ input: RpgMovementInput;
30
56
  x: number;
31
57
  y: number;
32
58
  direction?: Direction;
33
59
  }
34
60
 
61
+ interface CanvasResizeSize {
62
+ width: number;
63
+ height: number;
64
+ }
65
+
66
+ const DEFAULT_DASH_ADDITIONAL_SPEED = 8;
67
+ const DEFAULT_DASH_DURATION_MS = 180;
68
+ const DEFAULT_DASH_COOLDOWN_MS = 450;
69
+
70
+ const isDashInput = (input: RpgMovementInput): input is RpgDashInput =>
71
+ typeof input === "object" && input !== null && input.type === "dash";
72
+
73
+ const isMoveInput = (
74
+ input: RpgMovementInput
75
+ ): input is { type: "move"; direction: Direction } =>
76
+ typeof input === "object" && input !== null && input.type === "move";
77
+
78
+ const resolveMoveDirection = (input: RpgMovementInput): Direction | undefined => {
79
+ if (isMoveInput(input)) return input.direction;
80
+ if (typeof input === "string" || typeof input === "number") {
81
+ return input as Direction;
82
+ }
83
+ return undefined;
84
+ };
85
+
86
+ const directionToVector = (direction: Direction | undefined) => {
87
+ switch (direction) {
88
+ case Direction.Left:
89
+ return { x: -1, y: 0 };
90
+ case Direction.Right:
91
+ return { x: 1, y: 0 };
92
+ case Direction.Up:
93
+ return { x: 0, y: -1 };
94
+ case Direction.Down:
95
+ default:
96
+ return { x: 0, y: 1 };
97
+ }
98
+ };
99
+
100
+ const vectorToDirection = (direction: { x: number; y: number }): Direction => {
101
+ if (Math.abs(direction.x) > Math.abs(direction.y)) {
102
+ return direction.x < 0 ? Direction.Left : Direction.Right;
103
+ }
104
+ return direction.y < 0 ? Direction.Up : Direction.Down;
105
+ };
106
+
107
+ const normalizeDashInput = (
108
+ input: Partial<RpgDashInput>,
109
+ fallbackDirection: Direction | undefined
110
+ ): RpgDashInput | null => {
111
+ const rawDirection = input.direction ?? directionToVector(fallbackDirection);
112
+ const rawX = Number(rawDirection?.x ?? 0);
113
+ const rawY = Number(rawDirection?.y ?? 0);
114
+ const magnitude = Math.hypot(rawX, rawY);
115
+ if (!Number.isFinite(magnitude) || magnitude <= 0) return null;
116
+
117
+ const additionalSpeed =
118
+ typeof input.additionalSpeed === "number" && Number.isFinite(input.additionalSpeed)
119
+ ? Math.max(0, Math.min(input.additionalSpeed, 64))
120
+ : DEFAULT_DASH_ADDITIONAL_SPEED;
121
+ const duration =
122
+ typeof input.duration === "number" && Number.isFinite(input.duration)
123
+ ? Math.max(1, Math.min(input.duration, 1000))
124
+ : DEFAULT_DASH_DURATION_MS;
125
+ const cooldown =
126
+ typeof input.cooldown === "number" && Number.isFinite(input.cooldown)
127
+ ? Math.max(0, Math.min(input.cooldown, 5000))
128
+ : DEFAULT_DASH_COOLDOWN_MS;
129
+
130
+ return {
131
+ type: "dash",
132
+ direction: {
133
+ x: rawX / magnitude,
134
+ y: rawY / magnitude,
135
+ },
136
+ additionalSpeed,
137
+ duration,
138
+ cooldown,
139
+ };
140
+ };
141
+
142
+ type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
143
+ start(config?: T): Promise<void>;
144
+ };
145
+
146
+ type MapShakeOptions = {
147
+ intensity?: number;
148
+ duration?: number;
149
+ frequency?: number;
150
+ direction?: string;
151
+ };
152
+
35
153
  export class RpgClientEngine<T = any> {
36
154
  private guiService: RpgGui;
37
155
  private webSocket: AbstractWebsocket;
@@ -41,13 +159,19 @@ export class RpgClientEngine<T = any> {
41
159
  private selector: HTMLElement;
42
160
  public globalConfig: T;
43
161
  public sceneComponent: any;
162
+ public sceneMapComponent: any = BuiltinSceneMap;
44
163
  stopProcessingInput = false;
45
164
  width = signal("100%");
46
165
  height = signal("100%");
47
- spritesheets: Map<string, any> = new Map();
166
+ spritesheets: Map<string | number, any> = new Map();
167
+ private spritesheetPromises: Map<string | number, Promise<any>> = new Map();
48
168
  sounds: Map<string, any> = new Map();
49
169
  componentAnimations: any[] = [];
50
- private spritesheetResolver?: (id: string) => any | Promise<any>;
170
+ clientVisuals = new ClientVisualRegistry();
171
+ projectiles: ProjectileManager;
172
+ pointer: ClientPointerContext = createClientPointerContext();
173
+ interactions: RpgClientInteractions = new RpgClientInteractions(this);
174
+ private spritesheetResolver?: (id: string | number) => any | Promise<any>;
51
175
  private soundResolver?: (id: string) => any | Promise<any>;
52
176
  particleSettings: {
53
177
  emitters: any[]
@@ -61,21 +185,33 @@ export class RpgClientEngine<T = any> {
61
185
  playerIdSignal = signal<string | null>(null);
62
186
  spriteComponentsBehind = signal<any[]>([]);
63
187
  spriteComponentsInFront = signal<any[]>([]);
188
+ spriteComponents: Map<string, any> = new Map();
189
+ private eventComponentResolvers = new EventComponentResolverRegistry();
64
190
  /** ID of the sprite that the camera should follow. null means follow the current player */
65
191
  cameraFollowTargetId = signal<string | null>(null);
192
+ /** Camera follow transition options used by character components when the target changes */
193
+ cameraFollowSmoothMove: CameraFollowSmoothMove = false;
194
+ /** Incremented for each camera follow command so repeated commands on the same target are applied */
195
+ cameraFollowRevision = signal(0);
66
196
  /** Trigger for map shake animation */
67
- mapShakeTrigger = trigger();
197
+ mapShakeTrigger: ConfigurableTrigger<MapShakeOptions> = trigger<MapShakeOptions>();
68
198
 
69
- controlsReady = signal(undefined);
199
+ controlsReady = signal<boolean | undefined>(undefined);
70
200
  gamePause = signal(false);
71
201
 
72
202
  private predictionEnabled = false;
73
- private prediction?: PredictionController<Direction>;
203
+ private prediction?: PredictionController<RpgMovementInput, Direction>;
74
204
  private readonly SERVER_CORRECTION_THRESHOLD = 30;
205
+ private localMovementAuthority = false;
206
+ private lastLocalMovementInputAt = 0;
207
+ private readonly LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS = 250;
75
208
  private inputFrameCounter = 0;
76
209
  private pendingPredictionFrames: number[] = [];
77
210
  private lastClientPhysicsStepAt = 0;
78
211
  private frameOffset = 0;
212
+ private latestServerTick?: number;
213
+ private latestServerTickAt = 0;
214
+ private dashLockedUntil = 0;
79
215
  // Ping/Pong for RTT measurement
80
216
  private rtt: number = 0; // Round-trip time in ms
81
217
  private pingInterval: any = null;
@@ -92,17 +228,35 @@ export class RpgClientEngine<T = any> {
92
228
  private eventsReceived$ = new BehaviorSubject<boolean>(false);
93
229
  private onAfterLoadingSubscription?: any;
94
230
  private sceneResetQueued = false;
231
+ private mapTransitionInProgress = false;
232
+ private currentMapRoomId?: string;
233
+ private socketListenersInitialized = false;
234
+ private clientReadyForMapChanges = false;
235
+ private pendingMapChanges: any[] = [];
95
236
 
96
237
  // Store subscriptions and event listeners for cleanup
97
238
  private tickSubscriptions: any[] = [];
98
239
  private resizeHandler?: () => void;
240
+ private pointerMoveHandler?: (event: PointerEvent) => void;
241
+ private pointerUpHandler?: (event: PointerEvent) => void;
242
+ private pointerCancelHandler?: (event: PointerEvent) => void;
243
+ private pointerCanvas?: HTMLCanvasElement;
244
+ private pendingSyncPackets: any[] = [];
99
245
  private notificationManager: NotificationManager = new NotificationManager();
246
+ private i18nService: I18nService;
247
+ private locale?: string;
100
248
 
101
249
  constructor(public context) {
102
250
  this.webSocket = inject(WebSocketToken);
103
251
  this.guiService = inject(RpgGui);
104
252
  this.loadMapService = inject(LoadMapToken);
105
253
  this.hooks = inject<Hooks>(ModulesToken);
254
+ this.i18nService = getOrCreateI18nService(context);
255
+ this.i18nService.addMessages(RpgClientBuiltinI18n, "rpgjs-client", 0);
256
+ this.projectiles = new ProjectileManager(
257
+ this.hooks,
258
+ (projectile) => this.predictProjectileImpact(projectile),
259
+ );
106
260
  this.globalConfig = inject(GlobalConfigToken)
107
261
 
108
262
  if (!this.globalConfig) {
@@ -123,10 +277,63 @@ export class RpgClientEngine<T = any> {
123
277
  component: PrebuiltComponentAnimations.Animation
124
278
  })
125
279
 
280
+ this.registerSpriteComponent("rpg:text", TextComponent);
281
+ this.registerSpriteComponent("rpg:hpBar", BarComponent);
282
+ this.registerSpriteComponent("rpg:spBar", BarComponent);
283
+ this.registerSpriteComponent("rpg:bar", BarComponent);
284
+ this.registerSpriteComponent("rpg:shape", ShapeComponent);
285
+ this.registerSpriteComponent("rpg:image", ImageComponent);
286
+
126
287
  this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;
288
+ this.localMovementAuthority = this.resolveLocalMovementAuthority();
127
289
  this.initializePredictionController();
128
290
  }
129
291
 
292
+ private resolveLocalMovementAuthority(): boolean {
293
+ const predictionConfig = (this.globalConfig as any)?.prediction;
294
+ const configured =
295
+ (this.globalConfig as any)?.movementAuthority ??
296
+ predictionConfig?.movementAuthority ??
297
+ predictionConfig?.authority ??
298
+ predictionConfig?.mode;
299
+
300
+ if (
301
+ configured === "server" ||
302
+ configured === "network" ||
303
+ configured === false
304
+ ) {
305
+ return false;
306
+ }
307
+ if (
308
+ configured === "client" ||
309
+ configured === "local" ||
310
+ configured === true
311
+ ) {
312
+ return true;
313
+ }
314
+
315
+ return this.webSocket.mode === "standalone";
316
+ }
317
+
318
+ setLocale(locale: string) {
319
+ this.locale = locale;
320
+ }
321
+
322
+ getLocale(): string {
323
+ return this.locale || this.i18nService.defaultLocale;
324
+ }
325
+
326
+ t(key: string, params?: I18nParams): string {
327
+ return this.i18nService.t(key, params, this.getLocale());
328
+ }
329
+
330
+ i18n() {
331
+ return {
332
+ locale: this.getLocale(),
333
+ t: (key: string, params?: I18nParams) => this.t(key, params),
334
+ };
335
+ }
336
+
130
337
  /**
131
338
  * Assigns a CanvasEngine KeyboardControls instance to the dependency injection context
132
339
  *
@@ -166,13 +373,29 @@ export class RpgClientEngine<T = any> {
166
373
  ...currentValues,
167
374
  values: new Map([['__default__', controlInstance]])
168
375
  }
169
- this.controlsReady.set(undefined);
376
+ this.controlsReady.set(true);
170
377
  }
171
378
 
172
379
  async start() {
173
380
  this.sceneMap = new RpgClientMap()
174
381
  this.sceneMap.configureClientPrediction(this.predictionEnabled);
175
382
  this.sceneMap.loadPhysic();
383
+ this.resolveSceneMapComponent();
384
+
385
+ const saveClient = inject(SaveClientService);
386
+ saveClient.initialize();
387
+ this.initListeners();
388
+ this.guiService._initialize();
389
+
390
+ try {
391
+ await this.webSocket.connection();
392
+ }
393
+ catch (error) {
394
+ this.stopPingPong();
395
+ await this.callConnectError(error);
396
+ throw error;
397
+ }
398
+
176
399
  this.selector = document.body.querySelector("#rpg") as HTMLElement;
177
400
 
178
401
  const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;
@@ -181,9 +404,11 @@ export class RpgClientEngine<T = any> {
181
404
  Canvas,
182
405
  bootstrapOptions
183
406
  );
407
+ this.installCanvasResizeGuard(app);
184
408
  this.canvasApp = app;
185
409
  this.canvasElement = canvasElement;
186
- this.renderer = app.renderer as PIXI.Renderer;
410
+ this.renderer = app.renderer as unknown as PIXI.Renderer;
411
+ this.setupPointerTracking();
187
412
  this.tick = canvasElement?.propObservables?.context['tick'].observable
188
413
 
189
414
  const inputCheckSubscription = this.tick.subscribe(() => {
@@ -198,17 +423,23 @@ export class RpgClientEngine<T = any> {
198
423
 
199
424
  this.hooks.callHooks("client-spritesheets-load", this).subscribe();
200
425
  this.hooks.callHooks("client-spritesheetResolver-load", this).subscribe();
426
+ this.flushPendingSyncPackets();
201
427
  this.hooks.callHooks("client-sounds-load", this).subscribe();
202
428
  this.hooks.callHooks("client-soundResolver-load", this).subscribe();
203
-
429
+
204
430
  RpgSound.init(this);
205
431
  RpgResource.init(this);
206
432
  this.hooks.callHooks("client-gui-load", this).subscribe();
207
433
  this.hooks.callHooks("client-particles-load", this).subscribe();
208
434
  this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
435
+ this.hooks.callHooks("client-clientVisuals-load", this).subscribe();
436
+ this.hooks.callHooks("client-projectiles-load", this).subscribe();
437
+ this.hooks.callHooks("client-interactions-load", this).subscribe();
209
438
  this.hooks.callHooks("client-sprite-load", this).subscribe();
210
439
 
211
440
  await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
441
+ this.clientReadyForMapChanges = true;
442
+ this.flushPendingMapChanges();
212
443
 
213
444
  // wondow is resize
214
445
  this.resizeHandler = () => {
@@ -218,6 +449,7 @@ export class RpgClientEngine<T = any> {
218
449
 
219
450
  const tickSubscription = this.tick.subscribe((tick) => {
220
451
  this.stepClientPhysicsTick();
452
+ this.projectiles.step();
221
453
  this.flushPendingPredictedStates();
222
454
  this.flushPendingMovePath();
223
455
  this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
@@ -231,13 +463,163 @@ export class RpgClientEngine<T = any> {
231
463
  });
232
464
  this.tickSubscriptions.push(tickSubscription);
233
465
 
234
- await this.webSocket.connection(() => {
235
- const saveClient = inject(SaveClientService);
236
- saveClient.initialize(this.webSocket);
237
- this.initListeners()
238
- this.guiService._initialize()
239
- this.startPingPong();
240
- });
466
+ this.startPingPong();
467
+ }
468
+
469
+ private installCanvasResizeGuard(app: any) {
470
+ if (!app || typeof app.resize !== "function") return;
471
+
472
+ const originalResize = app.resize.bind(app);
473
+ app.resize = () => {
474
+ const targetSize = this.readCanvasResizeTargetSize(app);
475
+ const rendererSize = this.readCanvasRendererSize(app);
476
+
477
+ if (
478
+ targetSize &&
479
+ rendererSize &&
480
+ targetSize.width === rendererSize.width &&
481
+ targetSize.height === rendererSize.height
482
+ ) {
483
+ this.cancelCanvasResizeFrame(app);
484
+ return;
485
+ }
486
+
487
+ originalResize();
488
+ };
489
+ }
490
+
491
+ private readCanvasResizeTargetSize(app: any): CanvasResizeSize | null {
492
+ const resizeTarget = app?.resizeTo;
493
+ if (!resizeTarget || typeof window === "undefined") return null;
494
+
495
+ const rawWidth = resizeTarget === window ? window.innerWidth : resizeTarget.clientWidth;
496
+ const rawHeight = resizeTarget === window ? window.innerHeight : resizeTarget.clientHeight;
497
+ const width = Math.round(Number(rawWidth));
498
+ const height = Math.round(Number(rawHeight));
499
+
500
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width < 0 || height < 0) return null;
501
+ return { width, height };
502
+ }
503
+
504
+ private readCanvasRendererSize(app: any): CanvasResizeSize | null {
505
+ const screen = app?.renderer?.screen;
506
+ const width = Math.round(Number(screen?.width));
507
+ const height = Math.round(Number(screen?.height));
508
+
509
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
510
+ return { width, height };
511
+ }
512
+
513
+ private cancelCanvasResizeFrame(app: any) {
514
+ if (typeof app?._cancelResize === "function") {
515
+ app._cancelResize();
516
+ }
517
+ }
518
+
519
+ private resolveSceneMapComponent() {
520
+ const components = this.hooks.getHookFunctions("client-sceneMap-component");
521
+ const component = components[components.length - 1];
522
+ if (component) {
523
+ this.sceneMapComponent = component;
524
+ }
525
+ }
526
+
527
+ private setupPointerTracking() {
528
+ const renderer = this.renderer as any;
529
+ const canvas = renderer?.canvas ?? renderer?.view ?? (this.canvasApp as any)?.canvas;
530
+
531
+ if (!canvas || typeof canvas.addEventListener !== "function") {
532
+ return;
533
+ }
534
+
535
+ this.pointerCanvas = canvas;
536
+ const updatePointer = (event: PointerEvent) => {
537
+ const rect = canvas.getBoundingClientRect();
538
+ const screen = {
539
+ x: event.clientX - rect.left,
540
+ y: event.clientY - rect.top,
541
+ };
542
+ const viewport = this.findViewportInstance();
543
+ let world = screen;
544
+
545
+ if (viewport && typeof viewport.toWorld === "function") {
546
+ const point = viewport.toWorld(screen.x, screen.y);
547
+ world = { x: Number(point.x), y: Number(point.y) };
548
+ } else if (viewport && typeof viewport.toLocal === "function") {
549
+ const point = viewport.toLocal(screen);
550
+ world = { x: Number(point.x), y: Number(point.y) };
551
+ }
552
+
553
+ this.pointer.update(screen, world);
554
+ };
555
+
556
+ this.pointerMoveHandler = (event: PointerEvent) => {
557
+ updatePointer(event);
558
+ this.interactions.handlePointerMove(event);
559
+ };
560
+ this.pointerUpHandler = (event: PointerEvent) => {
561
+ updatePointer(event);
562
+ this.interactions.handlePointerUp(event);
563
+ };
564
+ this.pointerCancelHandler = (event: PointerEvent) => {
565
+ updatePointer(event);
566
+ this.interactions.cancelDrag(event);
567
+ };
568
+
569
+ canvas.addEventListener("pointermove", this.pointerMoveHandler);
570
+ canvas.addEventListener("pointerdown", this.pointerMoveHandler);
571
+ canvas.addEventListener("pointerup", this.pointerUpHandler);
572
+ canvas.addEventListener("pointercancel", this.pointerCancelHandler);
573
+ canvas.addEventListener("pointerleave", this.pointerCancelHandler);
574
+ }
575
+
576
+ updatePointerFromInteractionEvent(event: any): void {
577
+ const global = event?.global ?? event?.data?.global;
578
+
579
+ if (!global) {
580
+ this.pointer.updateFromEvent(event);
581
+ return;
582
+ }
583
+
584
+ const screen = {
585
+ x: Number(global.x),
586
+ y: Number(global.y),
587
+ };
588
+ if (!Number.isFinite(screen.x) || !Number.isFinite(screen.y)) {
589
+ this.pointer.updateFromEvent(event);
590
+ return;
591
+ }
592
+
593
+ const viewport = this.findViewportInstance();
594
+ if (viewport && typeof viewport.toWorld === "function") {
595
+ const point = viewport.toWorld(screen.x, screen.y);
596
+ this.pointer.update(screen, { x: Number(point.x), y: Number(point.y) });
597
+ return;
598
+ }
599
+
600
+ this.pointer.update(screen);
601
+ }
602
+
603
+ private findViewportInstance(): any {
604
+ const find = (node: any): any => {
605
+ if (!node) return undefined;
606
+ if (typeof node?.toWorld === "function" || node?.constructor?.name === "Viewport") {
607
+ return node;
608
+ }
609
+ for (const child of node.children ?? []) {
610
+ const viewport = find(child);
611
+ if (viewport) return viewport;
612
+ }
613
+ return undefined;
614
+ };
615
+
616
+ return find((this.canvasApp as any)?.stage);
617
+ }
618
+
619
+ private clearCameraFollowViewportPlugins(): void {
620
+ const viewport = this.findViewportInstance();
621
+ viewport?.plugins?.remove?.("animate");
622
+ viewport?.plugins?.remove?.("follow");
241
623
  }
242
624
 
243
625
  private prepareSyncPayload(data: any): any {
@@ -247,8 +629,8 @@ export class RpgClientEngine<T = any> {
247
629
 
248
630
  const myId = this.playerIdSignal();
249
631
  const players = payload.players;
250
- const shouldMaskLocalPosition =
251
- this.predictionEnabled && !!this.prediction?.hasPendingInputs();
632
+ const localPatch = myId && players ? players[myId] : undefined;
633
+ const shouldMaskLocalPosition = this.shouldPreserveLocalPlayerPosition(localPatch);
252
634
  if (shouldMaskLocalPosition && myId && players && players[myId]) {
253
635
  const localPatch = { ...players[myId] };
254
636
  delete localPatch.x;
@@ -264,6 +646,31 @@ export class RpgClientEngine<T = any> {
264
646
  return payload;
265
647
  }
266
648
 
649
+ private shouldPreserveLocalPlayerPosition(localPatch?: any): boolean {
650
+ if (!localPatch) {
651
+ return false;
652
+ }
653
+ if (this.predictionEnabled && !!this.prediction?.hasPendingInputs()) {
654
+ return true;
655
+ }
656
+ return this.shouldKeepLocalPlayerMovement();
657
+ }
658
+
659
+ private shouldKeepLocalPlayerMovement(): boolean {
660
+ if (!this.localMovementAuthority || this.mapTransitionInProgress) {
661
+ return false;
662
+ }
663
+ const myId = this.playerIdSignal();
664
+ const player = myId ? this.sceneMap?.players?.()?.[myId] : undefined;
665
+ if (!player) {
666
+ return false;
667
+ }
668
+ if (this.prediction?.hasPendingInputs()) {
669
+ return true;
670
+ }
671
+ return Date.now() - this.lastLocalMovementInputAt <= this.LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS;
672
+ }
673
+
267
674
  private normalizeAckWithSyncState(
268
675
  ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction },
269
676
  syncData: any,
@@ -287,52 +694,15 @@ export class RpgClientEngine<T = any> {
287
694
  }
288
695
 
289
696
  private initListeners() {
290
- this.webSocket.on("sync", (data) => {
291
- if (data.pId) {
292
- this.playerIdSignal.set(data.pId);
293
- // Signal that player ID was received
294
- this.playerIdReceived$.next(true);
295
- }
697
+ if (this.socketListenersInitialized) return;
698
+ this.socketListenersInitialized = true;
296
699
 
297
- if (this.sceneResetQueued) {
298
- this.sceneMap.reset();
299
- this.sceneMap.loadPhysic();
300
- this.sceneResetQueued = false;
301
- }
302
-
303
- // Apply client-side prediction filtering and server reconciliation
304
- this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
305
-
306
- const ack = data?.ack;
307
- const normalizedAck =
308
- ack && typeof ack.frame === "number"
309
- ? this.normalizeAckWithSyncState(ack, data)
310
- : undefined;
311
- const payload = this.prepareSyncPayload(data);
312
- load(this.sceneMap, payload, true);
313
-
314
- if (normalizedAck) {
315
- this.applyServerAck(normalizedAck);
316
- }
317
-
318
- for (const playerId in payload.players ?? {}) {
319
- const player = payload.players[playerId]
320
- if (!player._param) continue
321
- for (const param in player._param) {
322
- this.sceneMap.players()[playerId]._param()[param] = player._param[param]
323
- }
324
- }
325
-
326
- // Check if players and events are present in sync data
327
- const players = payload.players || this.sceneMap.players();
328
- if (players && Object.keys(players).length > 0) {
329
- this.playersReceived$.next(true);
330
- }
331
-
332
- const events = payload.events || this.sceneMap.events();
333
- if (events !== undefined) {
334
- this.eventsReceived$.next(true);
700
+ this.webSocket.on("sync", (data) => {
701
+ if (!this.tick) {
702
+ this.pendingSyncPackets.push(data);
703
+ return;
335
704
  }
705
+ this.applySyncPacket(data);
336
706
  });
337
707
 
338
708
  // Handle pong responses for RTT measurement
@@ -344,6 +714,7 @@ export class RpgClientEngine<T = any> {
344
714
  // This helps us estimate which server tick corresponds to each client input frame
345
715
  const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1000 / 60)); // Estimate ticks during half RTT
346
716
  const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
717
+ this.updateServerTickEstimate(estimatedServerTickNow, now);
347
718
 
348
719
  // Update frame offset (only if we have inputs to calibrate with)
349
720
  if (this.inputFrameCounter > 0) {
@@ -354,11 +725,11 @@ export class RpgClientEngine<T = any> {
354
725
  });
355
726
 
356
727
  this.webSocket.on("changeMap", (data) => {
357
- this.sceneResetQueued = true;
358
- // Reset camera follow to default (follow current player) when changing maps
359
- this.cameraFollowTargetId.set(null);
360
- const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
361
- this.loadScene(data.mapId, transferToken);
728
+ if (!this.clientReadyForMapChanges) {
729
+ this.pendingMapChanges.push(data);
730
+ return;
731
+ }
732
+ this.handleChangeMap(data);
362
733
  });
363
734
 
364
735
  this.webSocket.on("showComponentAnimation", (data) => {
@@ -370,19 +741,63 @@ export class RpgClientEngine<T = any> {
370
741
  this.getComponentAnimation(id).displayEffect(params, player || position)
371
742
  });
372
743
 
744
+ this.webSocket.on("clientVisual", (data) => {
745
+ this.playClientVisual(data);
746
+ });
747
+
748
+ this.webSocket.on("projectile:spawnBatch", (data) => {
749
+ if (!this.shouldProcessProjectilePacket(data)) return;
750
+ this.projectiles.spawnBatch(data?.projectiles ?? [], {
751
+ mapId: data?.mapId,
752
+ currentServerTick: this.estimateServerTick(),
753
+ tickDurationMs: this.getPhysicsTickDurationMs(),
754
+ });
755
+ });
756
+
757
+ this.webSocket.on("projectile:impactBatch", (data) => {
758
+ if (!this.shouldProcessProjectilePacket(data)) return;
759
+ this.projectiles.impactBatch(data?.impacts ?? [], {
760
+ mapId: data?.mapId,
761
+ });
762
+ });
763
+
764
+ this.webSocket.on("projectile:destroyBatch", (data) => {
765
+ if (!this.shouldProcessProjectilePacket(data)) return;
766
+ this.projectiles.destroyBatch(data?.projectiles ?? [], {
767
+ mapId: data?.mapId,
768
+ });
769
+ });
770
+
771
+ this.webSocket.on("projectile:clear", (data) => {
772
+ if (!this.shouldProcessProjectilePacket(data)) return;
773
+ this.projectiles.clear();
774
+ });
775
+
373
776
  this.webSocket.on("notification", (data) => {
374
777
  this.notificationManager.add(data);
375
778
  });
376
779
 
377
- this.webSocket.on("setAnimation", (data) => {
378
- const { animationName, nbTimes, object, graphic } = data;
379
- const player = this.sceneMap.getObjectById(object);
380
- if (graphic !== undefined) {
381
- player.setAnimation(animationName, graphic, nbTimes);
382
- } else {
383
- player.setAnimation(animationName, nbTimes);
384
- }
385
- })
780
+ this.webSocket.on("setAnimation", (data) => {
781
+ const {
782
+ animationName,
783
+ nbTimes,
784
+ object,
785
+ graphic,
786
+ restoreAnimationName,
787
+ restoreGraphics,
788
+ } = data;
789
+ const player = object ? this.sceneMap.getObjectById(object) : undefined;
790
+ if (!player) return;
791
+ const restoreOptions = {
792
+ restoreAnimationName,
793
+ restoreGraphics,
794
+ };
795
+ if (graphic !== undefined) {
796
+ player.setAnimation(animationName, graphic, nbTimes, restoreOptions);
797
+ } else {
798
+ player.setAnimation(animationName, nbTimes, restoreOptions);
799
+ }
800
+ })
386
801
 
387
802
  this.webSocket.on("playSound", (data) => {
388
803
  const { soundId, volume, loop } = data;
@@ -413,7 +828,7 @@ export class RpgClientEngine<T = any> {
413
828
 
414
829
  this.webSocket.on("shakeMap", (data) => {
415
830
  const { intensity, duration, frequency, direction } = data || {};
416
- (this.mapShakeTrigger as any).start({
831
+ this.mapShakeTrigger.start({
417
832
  intensity,
418
833
  duration,
419
834
  frequency,
@@ -447,6 +862,14 @@ export class RpgClientEngine<T = any> {
447
862
  });
448
863
  });
449
864
 
865
+ this.webSocket.on("lightingState", (data) => {
866
+ const raw = (data && typeof data === "object" && "value" in data)
867
+ ? (data as any).value
868
+ : data;
869
+
870
+ this.sceneMap.lightingState.set(normalizeLightingState(raw));
871
+ });
872
+
450
873
  this.webSocket.on('open', () => {
451
874
  this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
452
875
  // Start ping/pong for synchronization
@@ -460,10 +883,115 @@ export class RpgClientEngine<T = any> {
460
883
  })
461
884
 
462
885
  this.webSocket.on('error', (error) => {
463
- this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket).subscribe();
886
+ void this.callConnectError(error);
464
887
  })
465
888
  }
466
889
 
890
+ private beginMapTransfer(nextMapId?: string) {
891
+ this.mapTransitionInProgress = true;
892
+ this.currentMapRoomId = nextMapId;
893
+ this.sceneResetQueued = false;
894
+ this.clearClientPredictionStates();
895
+ this.sceneMap.weatherState.set(null);
896
+ this.sceneMap.lightingState.set(null);
897
+ this.sceneMap.clearLightSpots();
898
+ this.clearComponentAnimations();
899
+ this.projectiles.setMapId(nextMapId);
900
+ this.resetCameraFollow(false);
901
+ this.sceneMap.reset();
902
+ this.sceneMap.loadPhysic();
903
+ }
904
+
905
+ private clearComponentAnimations() {
906
+ this.componentAnimations.forEach((componentAnimation) => {
907
+ componentAnimation.instance?.clear?.();
908
+ });
909
+ }
910
+
911
+ private shouldProcessProjectilePacket(data: any): boolean {
912
+ if (this.mapTransitionInProgress) return false;
913
+ const packetMapId = normalizeRoomMapId(
914
+ typeof data?.mapId === "string" ? data.mapId : undefined,
915
+ );
916
+ const currentMapId = normalizeRoomMapId(this.currentMapRoomId);
917
+ return !packetMapId || !currentMapId || packetMapId === currentMapId;
918
+ }
919
+
920
+ private async callConnectError(error: any) {
921
+ await lastValueFrom(this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket));
922
+ }
923
+
924
+ private flushPendingSyncPackets() {
925
+ const packets = this.pendingSyncPackets;
926
+ this.pendingSyncPackets = [];
927
+ packets.forEach((packet) => this.applySyncPacket(packet));
928
+ }
929
+
930
+ private flushPendingMapChanges() {
931
+ const packets = this.pendingMapChanges;
932
+ this.pendingMapChanges = [];
933
+ packets.forEach((packet) => this.handleChangeMap(packet));
934
+ }
935
+
936
+ private handleChangeMap(data: any) {
937
+ const nextMapId = typeof data?.mapId === "string" ? data.mapId : undefined;
938
+ this.beginMapTransfer(nextMapId);
939
+ const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
940
+ this.loadScene(data.mapId, transferToken);
941
+ }
942
+
943
+ private applySyncPacket(data: any) {
944
+ if (data.pId) {
945
+ this.playerIdSignal.set(data.pId);
946
+ // Signal that player ID was received
947
+ this.playerIdReceived$.next(true);
948
+ }
949
+
950
+ if (this.sceneResetQueued) {
951
+ const weatherState = this.sceneMap.weatherState();
952
+ const lightingState = this.sceneMap.lightingState();
953
+ this.sceneMap.reset();
954
+ this.sceneMap.weatherState.set(weatherState);
955
+ this.sceneMap.lightingState.set(lightingState);
956
+ this.sceneMap.loadPhysic();
957
+ this.sceneResetQueued = false;
958
+ }
959
+
960
+ // Apply client-side prediction filtering and server reconciliation
961
+ this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
962
+
963
+ const ack = data?.ack;
964
+ const normalizedAck =
965
+ ack && typeof ack.frame === "number"
966
+ ? this.normalizeAckWithSyncState(ack, data)
967
+ : undefined;
968
+ const payload = this.prepareSyncPayload(data);
969
+ load(this.sceneMap, payload, true);
970
+
971
+ if (normalizedAck) {
972
+ this.applyServerAck(normalizedAck);
973
+ }
974
+
975
+ for (const playerId in payload.players ?? {}) {
976
+ const player = payload.players[playerId]
977
+ if (!player._param) continue
978
+ for (const param in player._param) {
979
+ this.sceneMap.players()[playerId]._param()[param] = player._param[param]
980
+ }
981
+ }
982
+
983
+ // Check if players and events are present in sync data
984
+ const players = payload.players || this.sceneMap.players();
985
+ if (players && Object.keys(players).length > 0) {
986
+ this.playersReceived$.next(true);
987
+ }
988
+
989
+ const events = payload.events || this.sceneMap.events();
990
+ if (events !== undefined) {
991
+ this.eventsReceived$.next(true);
992
+ }
993
+ }
994
+
467
995
  /**
468
996
  * Start periodic ping/pong for client-server synchronization
469
997
  *
@@ -560,13 +1088,22 @@ export class RpgClientEngine<T = any> {
560
1088
  room: mapId,
561
1089
  query: transferToken ? { transferToken } : undefined,
562
1090
  })
563
- await this.webSocket.reconnect(() => {
564
- const saveClient = inject(SaveClientService);
565
- saveClient.initialize(this.webSocket);
566
- this.initListeners()
567
- this.guiService._initialize()
568
- })
1091
+ try {
1092
+ await this.webSocket.reconnect()
1093
+ }
1094
+ catch (error) {
1095
+ this.mapTransitionInProgress = false;
1096
+ this.stopPingPong();
1097
+ await this.callConnectError(error);
1098
+ throw error;
1099
+ }
569
1100
  const res = await this.loadMapService.load(mapId)
1101
+ const loadedLighting = typeof res?.lighting !== "undefined"
1102
+ ? res.lighting
1103
+ : res?.data?.lighting;
1104
+ if (typeof loadedLighting !== "undefined") {
1105
+ this.sceneMap.lightingState.set(normalizeLightingState(loadedLighting));
1106
+ }
570
1107
  this.sceneMap.data.set(res)
571
1108
 
572
1109
  // Check if playerId is already present
@@ -587,6 +1124,8 @@ export class RpgClientEngine<T = any> {
587
1124
 
588
1125
  // Signal that map loading is completed (this should be last to ensure other checks are done)
589
1126
  this.mapLoadCompleted$.next(true);
1127
+ this.currentMapRoomId = mapId;
1128
+ this.mapTransitionInProgress = false;
590
1129
  this.sceneMap.configureClientPrediction(this.predictionEnabled);
591
1130
  this.sceneMap.loadPhysic()
592
1131
  }
@@ -623,7 +1162,7 @@ export class RpgClientEngine<T = any> {
623
1162
  * });
624
1163
  * ```
625
1164
  */
626
- setSpritesheetResolver(resolver: (id: string) => any | Promise<any>): void {
1165
+ setSpritesheetResolver(resolver: (id: string | number) => any | Promise<any>): void {
627
1166
  this.spritesheetResolver = resolver;
628
1167
  }
629
1168
 
@@ -634,7 +1173,7 @@ export class RpgClientEngine<T = any> {
634
1173
  * If not found and a resolver is set, it calls the resolver to create the spritesheet.
635
1174
  * The resolved spritesheet is automatically cached for future use.
636
1175
  *
637
- * @param id - The spritesheet ID to retrieve
1176
+ * @param id - The spritesheet ID or legacy tile ID to retrieve
638
1177
  * @returns The spritesheet if found or created, or undefined if not found and no resolver
639
1178
  * @returns Promise<any> if the resolver is asynchronous
640
1179
  *
@@ -647,7 +1186,7 @@ export class RpgClientEngine<T = any> {
647
1186
  * const spritesheet = await engine.getSpriteSheet('dynamic-sprite');
648
1187
  * ```
649
1188
  */
650
- getSpriteSheet(id: string): any | Promise<any> {
1189
+ getSpriteSheet(id: string | number): any | Promise<any> {
651
1190
  // Check cache first
652
1191
  if (this.spritesheets.has(id)) {
653
1192
  return this.spritesheets.get(id);
@@ -655,17 +1194,29 @@ export class RpgClientEngine<T = any> {
655
1194
 
656
1195
  // If not in cache and resolver exists, use it
657
1196
  if (this.spritesheetResolver) {
1197
+ if (this.spritesheetPromises.has(id)) {
1198
+ return this.spritesheetPromises.get(id);
1199
+ }
1200
+
658
1201
  const result = this.spritesheetResolver(id);
659
1202
 
660
1203
  // Check if result is a Promise
661
1204
  if (result instanceof Promise) {
662
- return result.then((spritesheet) => {
663
- if (spritesheet) {
664
- // Cache the resolved spritesheet
665
- this.spritesheets.set(id, spritesheet);
666
- }
667
- return spritesheet;
668
- });
1205
+ const promise = result
1206
+ .then((spritesheet) => {
1207
+ if (spritesheet) {
1208
+ // Cache the resolved spritesheet
1209
+ this.spritesheets.set(id, spritesheet);
1210
+ }
1211
+ this.spritesheetPromises.delete(id);
1212
+ return spritesheet;
1213
+ })
1214
+ .catch((error) => {
1215
+ this.spritesheetPromises.delete(id);
1216
+ throw error;
1217
+ });
1218
+ this.spritesheetPromises.set(id, promise);
1219
+ return promise;
669
1220
  } else {
670
1221
  // Synchronous result
671
1222
  if (result) {
@@ -941,19 +1492,22 @@ export class RpgClientEngine<T = any> {
941
1492
  * Set the camera to follow a specific sprite
942
1493
  *
943
1494
  * This method changes which sprite the camera viewport should follow.
944
- * The camera will smoothly animate to the target sprite if smoothMove options are provided.
1495
+ * The camera can smoothly animate to the target sprite before continuous follow starts.
945
1496
  *
946
1497
  * ## Design
947
1498
  *
948
1499
  * The camera follow target is stored in a signal that is read by sprite components.
949
1500
  * Each sprite checks if it should be followed by comparing its ID with the target ID.
950
- * When smoothMove options are provided, the viewport animation is handled by CanvasEngine's
951
- * viewport system.
1501
+ * When smoothMove options are provided, the transition is handled by pixi-viewport's
1502
+ * animation plugin, then continuous follow is handled by CanvasEngine's viewport system.
952
1503
  *
953
1504
  * @param targetId - The ID of the sprite to follow. Set to null to follow the current player
954
1505
  * @param smoothMove - Animation options. Can be a boolean (default: true) or an object with time and ease
955
1506
  * @param smoothMove.time - Duration of the animation in milliseconds (optional)
956
1507
  * @param smoothMove.ease - Easing function name from https://easings.net (optional)
1508
+ * @param smoothMove.speed - Continuous follow speed after the transition (optional)
1509
+ * @param smoothMove.acceleration - Continuous follow acceleration after the transition (optional)
1510
+ * @param smoothMove.radius - Center radius where the target can move without moving the viewport (optional)
957
1511
  *
958
1512
  * @example
959
1513
  * ```ts
@@ -975,19 +1529,19 @@ export class RpgClientEngine<T = any> {
975
1529
  */
976
1530
  setCameraFollow(
977
1531
  targetId: string | null,
978
- smoothMove?: boolean | { time?: number; ease?: string }
1532
+ smoothMove?: CameraFollowSmoothMove
979
1533
  ): void {
980
- // Store smoothMove options for potential future use with viewport animation
981
- // For now, we just set the target ID and let CanvasEngine handle the viewport follow
982
- // The smoothMove options could be used to configure viewport animation if CanvasEngine supports it
1534
+ this.clearCameraFollowViewportPlugins();
1535
+ this.cameraFollowSmoothMove = smoothMove ?? true;
983
1536
  this.cameraFollowTargetId.set(targetId);
984
-
985
- // If smoothMove is an object, we could store it for viewport configuration
986
- // This would require integration with CanvasEngine's viewport animation system
987
- if (typeof smoothMove === "object" && smoothMove !== null) {
988
- // Future: Apply smoothMove.time and smoothMove.ease to viewport animation
989
- // For now, CanvasEngine handles viewport following automatically
990
- }
1537
+ this.cameraFollowRevision.set(this.cameraFollowRevision() + 1);
1538
+ }
1539
+
1540
+ private resetCameraFollow(smoothMove: CameraFollowSmoothMove = false): void {
1541
+ this.clearCameraFollowViewportPlugins();
1542
+ this.cameraFollowSmoothMove = smoothMove;
1543
+ this.cameraFollowTargetId.set(null);
1544
+ this.cameraFollowRevision.set(this.cameraFollowRevision() + 1);
991
1545
  }
992
1546
 
993
1547
  addParticle(particle: any) {
@@ -1081,6 +1635,104 @@ export class RpgClientEngine<T = any> {
1081
1635
  return component
1082
1636
  }
1083
1637
 
1638
+ /**
1639
+ * Register a reusable sprite component that can be addressed by the server.
1640
+ *
1641
+ * Server-side component definitions only carry the component id and
1642
+ * serializable props. The client registry maps that id to the CanvasEngine
1643
+ * component that performs the actual rendering.
1644
+ *
1645
+ * @param id - Stable component id used by server component definitions
1646
+ * @param component - CanvasEngine component to render for this id
1647
+ * @returns The registered component
1648
+ *
1649
+ * @example
1650
+ * ```ts
1651
+ * engine.registerSpriteComponent('guildBadge', GuildBadgeComponent);
1652
+ * ```
1653
+ */
1654
+ registerSpriteComponent(id: string, component: any) {
1655
+ this.spriteComponents.set(id, component);
1656
+ return component;
1657
+ }
1658
+
1659
+ /**
1660
+ * Get a reusable sprite component by id.
1661
+ *
1662
+ * @param id - Component id registered on the client
1663
+ * @returns The CanvasEngine component, or undefined when missing
1664
+ */
1665
+ getSpriteComponent(id: string) {
1666
+ return this.spriteComponents.get(id);
1667
+ }
1668
+
1669
+ /**
1670
+ * Register a custom event component resolver.
1671
+ *
1672
+ * The last resolver returning a component wins. This lets later modules
1673
+ * override earlier defaults without replacing the whole map scene.
1674
+ *
1675
+ * @param resolver - Function receiving the synced event object
1676
+ * @returns The registered resolver
1677
+ */
1678
+ addEventComponentResolver(resolver: EventComponentResolver) {
1679
+ return this.eventComponentResolvers.add(resolver);
1680
+ }
1681
+
1682
+ /**
1683
+ * Resolve the custom CanvasEngine component for an event, if any.
1684
+ *
1685
+ * @param event - Synced client event object
1686
+ * @returns The component/config returned by the last matching resolver
1687
+ */
1688
+ resolveEventComponent(event: RpgClientEvent): EventComponentConfig | null {
1689
+ return this.eventComponentResolvers.resolve(event);
1690
+ }
1691
+
1692
+ registerProjectileComponent(type: string, component: any) {
1693
+ return this.projectiles.register(type, component);
1694
+ }
1695
+
1696
+ getProjectileComponent(type: string) {
1697
+ return this.projectiles.get(type);
1698
+ }
1699
+
1700
+ /**
1701
+ * Register a named client visual macro.
1702
+ *
1703
+ * Client visuals are small client-side functions that group existing visual
1704
+ * primitives such as flash, sound, component animations, sprite animation, or
1705
+ * map shake. The server sends only the visual name and a serializable payload.
1706
+ *
1707
+ * @param name - Stable visual name sent by the server
1708
+ * @param handler - Client-side visual handler
1709
+ * @returns The registered handler
1710
+ */
1711
+ registerClientVisual(name: string, handler: ClientVisualHandler) {
1712
+ return this.clientVisuals.register(name, handler);
1713
+ }
1714
+
1715
+ /**
1716
+ * Register several named client visual macros.
1717
+ *
1718
+ * @param visuals - Map of visual names to client-side handlers
1719
+ */
1720
+ registerClientVisuals(visuals: ClientVisualMap) {
1721
+ this.clientVisuals.registerMany(visuals);
1722
+ }
1723
+
1724
+ /**
1725
+ * Play a registered client visual locally.
1726
+ *
1727
+ * This is also used by the websocket listener when the server calls
1728
+ * `player.clientVisual()` or `map.clientVisual()`.
1729
+ *
1730
+ * @param packet - Visual name and serializable payload
1731
+ */
1732
+ playClientVisual(packet: ClientVisualPacket) {
1733
+ return this.clientVisuals.play(packet, this);
1734
+ }
1735
+
1084
1736
  /**
1085
1737
  * Add a component animation to the engine
1086
1738
  *
@@ -1164,21 +1816,59 @@ export class RpgClientEngine<T = any> {
1164
1816
  * duration: 1000,
1165
1817
  * onFinish: () => console.log('Fade complete')
1166
1818
  * });
1819
+ *
1820
+ * // Wait until the transition component calls onFinish
1821
+ * await engine.startTransition('fade', { duration: 1000 });
1167
1822
  * ```
1168
1823
  */
1169
- startTransition(id: string, props: any = {}) {
1824
+ startTransition(id: string, props: any = {}): Promise<void> {
1170
1825
  if (!this.guiService.exists(id)) {
1171
1826
  throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);
1172
1827
  }
1173
- this.guiService.display(id, props);
1828
+ return new Promise<void>((resolve) => {
1829
+ let finished = false;
1830
+ const finish = (data?: any) => {
1831
+ if (finished) return;
1832
+ finished = true;
1833
+ props?.onFinish?.(data);
1834
+ resolve();
1835
+ };
1836
+
1837
+ this.guiService.display(id, {
1838
+ ...props,
1839
+ onFinish: finish,
1840
+ });
1841
+ });
1174
1842
  }
1175
1843
 
1176
- async processInput({ input }: { input: Direction }) {
1844
+ async processInput({ input }: { input: RpgMovementInput }) {
1845
+ if (this.stopProcessingInput) return;
1846
+
1847
+ const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
1848
+ const canMove =
1849
+ !currentPlayer ||
1850
+ getCanMoveValue(currentPlayer);
1851
+ if (!canMove) {
1852
+ this.interruptCurrentPlayerMovement(currentPlayer);
1853
+ return;
1854
+ }
1855
+
1177
1856
  const timestamp = Date.now();
1857
+ const movementInput = isDashInput(input)
1858
+ ? normalizeDashInput(input, currentPlayer?.direction?.())
1859
+ : input;
1860
+ if (!movementInput) return;
1861
+ if (isDashInput(movementInput)) {
1862
+ const cooldown = movementInput.cooldown ?? DEFAULT_DASH_COOLDOWN_MS;
1863
+ if (timestamp < this.dashLockedUntil) return;
1864
+ this.dashLockedUntil = timestamp + cooldown;
1865
+ }
1866
+ this.lastLocalMovementInputAt = timestamp;
1867
+
1178
1868
  let frame: number;
1179
1869
  let tick: number;
1180
1870
  if (this.predictionEnabled && this.prediction) {
1181
- const meta = this.prediction.recordInput(input, timestamp);
1871
+ const meta = this.prediction.recordInput(movementInput, timestamp);
1182
1872
  frame = meta.frame;
1183
1873
  tick = meta.tick;
1184
1874
  } else {
@@ -1186,13 +1876,11 @@ export class RpgClientEngine<T = any> {
1186
1876
  tick = this.getPhysicsTick();
1187
1877
  }
1188
1878
  this.inputFrameCounter = frame;
1189
- this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
1879
+ this.hooks.callHooks("client-engine-onInput", this, { input: movementInput, playerId: this.playerId }).subscribe();
1190
1880
 
1191
- const currentPlayer = this.sceneMap.getCurrentPlayer();
1192
1881
  const bodyReady = this.ensureCurrentPlayerBody();
1193
1882
  if (currentPlayer && bodyReady) {
1194
- currentPlayer.changeDirection(input);
1195
- (this.sceneMap as any).moveBody(currentPlayer, input);
1883
+ this.applyPredictedMovementInput(currentPlayer, movementInput);
1196
1884
  if (this.predictionEnabled && this.prediction) {
1197
1885
  this.pendingPredictionFrames.push(frame);
1198
1886
  if (this.pendingPredictionFrames.length > 240) {
@@ -1201,14 +1889,42 @@ export class RpgClientEngine<T = any> {
1201
1889
  }
1202
1890
  }
1203
1891
 
1204
- this.emitMovePacket(input, frame, tick, timestamp, true);
1205
- this.lastInputTime = Date.now();
1892
+ this.emitMovePacket(movementInput, frame, tick, timestamp, true);
1893
+ this.lastInputTime = isDashInput(movementInput)
1894
+ ? Date.now() + (movementInput.duration ?? DEFAULT_DASH_DURATION_MS)
1895
+ : Date.now();
1896
+ }
1897
+
1898
+ async processDash(input: Partial<RpgDashInput> = {}) {
1899
+ const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
1900
+ const fallbackDirection =
1901
+ typeof currentPlayer?.direction === "function"
1902
+ ? currentPlayer.direction()
1903
+ : currentPlayer?.direction;
1904
+ const dashInput = normalizeDashInput(input, fallbackDirection);
1905
+ if (!dashInput) return;
1906
+ await this.processInput({ input: dashInput });
1206
1907
  }
1207
1908
 
1208
- processAction({ action }: { action: number }) {
1909
+ processAction(action: RpgActionName, data?: any): void;
1910
+ processAction(action: RpgActionInput): void;
1911
+ processAction(action: RpgActionName | RpgActionInput, data?: any): void {
1209
1912
  if (this.stopProcessingInput) return;
1210
- this.hooks.callHooks("client-engine-onInput", this, { input: 'action', playerId: this.playerId }).subscribe();
1211
- this.webSocket.emit('action', { action })
1913
+ const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
1914
+ const canMove =
1915
+ !currentPlayer ||
1916
+ getCanMoveValue(currentPlayer);
1917
+ if (!canMove) return;
1918
+
1919
+ const payload = normalizeActionInput(action as any, data);
1920
+
1921
+ this.hooks.callHooks("client-engine-onInput", this, {
1922
+ input: payload.action,
1923
+ action: payload.action,
1924
+ data: payload.data,
1925
+ playerId: this.playerId,
1926
+ }).subscribe();
1927
+ this.webSocket.emit('action', payload)
1212
1928
  }
1213
1929
 
1214
1930
  get PIXI() {
@@ -1227,8 +1943,76 @@ export class RpgClientEngine<T = any> {
1227
1943
  return this.sceneMap
1228
1944
  }
1229
1945
 
1946
+ getObjectById(id: string) {
1947
+ return this.sceneMap?.getObjectById(id);
1948
+ }
1949
+
1230
1950
  private getPhysicsTick(): number {
1231
- return this.sceneMap?.getTick?.() ?? 0;
1951
+ return (this.sceneMap as any)?.getTick?.() ?? 0;
1952
+ }
1953
+
1954
+ private getPhysicsTickDurationMs(): number {
1955
+ const timeStep = (this.sceneMap as any)?.physic?.getWorld?.()?.getTimeStep?.();
1956
+ return typeof timeStep === "number" && Number.isFinite(timeStep) && timeStep > 0
1957
+ ? timeStep * 1000
1958
+ : 1000 / 60;
1959
+ }
1960
+
1961
+ private updateServerTickEstimate(serverTick: number | undefined, now = Date.now()): void {
1962
+ if (typeof serverTick !== "number" || !Number.isFinite(serverTick)) {
1963
+ return;
1964
+ }
1965
+ this.latestServerTick = serverTick;
1966
+ this.latestServerTickAt = now;
1967
+ }
1968
+
1969
+ private estimateServerTick(now = Date.now()): number | undefined {
1970
+ if (typeof this.latestServerTick !== "number" || this.latestServerTickAt <= 0) {
1971
+ return undefined;
1972
+ }
1973
+ const elapsedTicks = Math.max(0, (now - this.latestServerTickAt) / this.getPhysicsTickDurationMs());
1974
+ return this.latestServerTick + elapsedTicks;
1975
+ }
1976
+
1977
+ private predictProjectileImpact(projectile: ClientProjectileSpawn): ClientProjectileImpact | null {
1978
+ if (projectile.predictImpact === false) {
1979
+ return null;
1980
+ }
1981
+ const sceneMap = this.sceneMap as any;
1982
+ if (!sceneMap?.physic || !Number.isFinite(projectile.range) || projectile.range <= 0) {
1983
+ return null;
1984
+ }
1985
+ const origin = projectile.origin;
1986
+ const direction = projectile.direction;
1987
+ if (
1988
+ !origin ||
1989
+ !direction ||
1990
+ !Number.isFinite(origin.x) ||
1991
+ !Number.isFinite(origin.y) ||
1992
+ !Number.isFinite(direction.x) ||
1993
+ !Number.isFinite(direction.y) ||
1994
+ (direction.x === 0 && direction.y === 0)
1995
+ ) {
1996
+ return null;
1997
+ }
1998
+
1999
+ const hit = sceneMap.physic.raycast(
2000
+ new Vector2(origin.x, origin.y),
2001
+ new Vector2(direction.x, direction.y),
2002
+ projectile.range,
2003
+ projectile.collisionMask,
2004
+ (entity) => projectile.ignoreOwner === false || !projectile.ownerId || entity.uuid !== projectile.ownerId,
2005
+ );
2006
+ if (!hit) {
2007
+ return null;
2008
+ }
2009
+ return {
2010
+ id: projectile.id,
2011
+ targetId: hit.entity.uuid,
2012
+ x: hit.point.x,
2013
+ y: hit.point.y,
2014
+ distance: hit.distance,
2015
+ };
1232
2016
  }
1233
2017
 
1234
2018
  private ensureCurrentPlayerBody(): boolean {
@@ -1295,7 +2079,7 @@ export class RpgClientEngine<T = any> {
1295
2079
  input: entry.direction,
1296
2080
  x: state.x,
1297
2081
  y: state.y,
1298
- direction: state.direction ?? entry.direction,
2082
+ direction: state.direction ?? resolveMoveDirection(entry.direction),
1299
2083
  });
1300
2084
  }
1301
2085
  if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {
@@ -1305,7 +2089,7 @@ export class RpgClientEngine<T = any> {
1305
2089
  }
1306
2090
 
1307
2091
  private emitMovePacket(
1308
- input: Direction,
2092
+ input: RpgMovementInput,
1309
2093
  frame: number,
1310
2094
  tick: number,
1311
2095
  timestamp: number,
@@ -1337,6 +2121,14 @@ export class RpgClientEngine<T = any> {
1337
2121
  if (!this.predictionEnabled || !this.prediction) {
1338
2122
  return;
1339
2123
  }
2124
+ const player = this.sceneMap?.getCurrentPlayer?.() as any;
2125
+ if (
2126
+ player &&
2127
+ !getCanMoveValue(player)
2128
+ ) {
2129
+ this.interruptCurrentPlayerMovement(player);
2130
+ return;
2131
+ }
1340
2132
  const pendingInputs = this.prediction.getPendingInputs();
1341
2133
  if (pendingInputs.length === 0) {
1342
2134
  return;
@@ -1352,6 +2144,22 @@ export class RpgClientEngine<T = any> {
1352
2144
  this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);
1353
2145
  }
1354
2146
 
2147
+ private applyPredictedMovementInput(
2148
+ player: any,
2149
+ input: RpgMovementInput
2150
+ ): boolean {
2151
+ if (isDashInput(input)) {
2152
+ const direction = vectorToDirection(input.direction);
2153
+ player.changeDirection(direction);
2154
+ return Boolean((this.sceneMap as any).dashBody?.(player, input));
2155
+ }
2156
+
2157
+ const direction = resolveMoveDirection(input);
2158
+ if (!direction) return false;
2159
+ player.changeDirection(direction);
2160
+ return Boolean((this.sceneMap as any).moveBody?.(player, direction));
2161
+ }
2162
+
1355
2163
  private getLocalPlayerState(): PredictionState<Direction> {
1356
2164
  const currentPlayer = this.sceneMap?.getCurrentPlayer();
1357
2165
  if (!currentPlayer) {
@@ -1395,7 +2203,7 @@ export class RpgClientEngine<T = any> {
1395
2203
  ? configuredMaxEntries
1396
2204
  : Math.max(600, Math.ceil(historyTtlMs / 16) + 120);
1397
2205
  this.sceneMap?.configureClientPrediction?.(true);
1398
- this.prediction = new PredictionController<Direction>({
2206
+ this.prediction = new PredictionController<RpgMovementInput, Direction>({
1399
2207
  correctionThreshold: (this.globalConfig as any)?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,
1400
2208
  historyTtlMs,
1401
2209
  maxHistoryEntries,
@@ -1475,6 +2283,34 @@ export class RpgClientEngine<T = any> {
1475
2283
  this.lastMovePathSentFrame = 0;
1476
2284
  }
1477
2285
 
2286
+ /**
2287
+ * Stop local movement immediately and discard pending predicted movement.
2288
+ *
2289
+ * Use this before a blocking action such as an A-RPG attack, dialog, dash
2290
+ * startup, or any client-side state where already buffered movement inputs
2291
+ * must not be replayed after server reconciliation.
2292
+ *
2293
+ * @param player - Player object to stop. Defaults to the current player.
2294
+ * @returns `true` when a player was found and interrupted.
2295
+ *
2296
+ * @example
2297
+ * ```ts
2298
+ * engine.interruptCurrentPlayerMovement();
2299
+ * ```
2300
+ */
2301
+ interruptCurrentPlayerMovement(player: any = this.sceneMap?.getCurrentPlayer?.()): boolean {
2302
+ if (!player) {
2303
+ return false;
2304
+ }
2305
+ (this.sceneMap as any)?.stopMovement?.(player);
2306
+ this.prediction?.clearPendingInputs();
2307
+ this.pendingPredictionFrames = [];
2308
+ this.lastInputTime = 0;
2309
+ this.lastMovePathSentAt = Date.now();
2310
+ this.lastMovePathSentFrame = this.inputFrameCounter;
2311
+ return true;
2312
+ }
2313
+
1478
2314
  /**
1479
2315
  * Trigger a flash animation on a sprite
1480
2316
  *
@@ -1542,12 +2378,14 @@ export class RpgClientEngine<T = any> {
1542
2378
  }
1543
2379
 
1544
2380
  private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
2381
+ this.updateServerTickEstimate(ack.serverTick);
2382
+ const keepLocalMovement = this.shouldKeepLocalPlayerMovement();
1545
2383
  if (this.predictionEnabled && this.prediction) {
1546
2384
  const result = this.prediction.applyServerAck({
1547
2385
  frame: ack.frame,
1548
2386
  serverTick: ack.serverTick,
1549
2387
  state:
1550
- typeof ack.x === "number" && typeof ack.y === "number"
2388
+ !keepLocalMovement && typeof ack.x === "number" && typeof ack.y === "number"
1551
2389
  ? { x: ack.x, y: ack.y, direction: ack.direction }
1552
2390
  : undefined,
1553
2391
  });
@@ -1560,7 +2398,10 @@ export class RpgClientEngine<T = any> {
1560
2398
  if (typeof ack.x !== "number" || typeof ack.y !== "number") {
1561
2399
  return;
1562
2400
  }
1563
- const player = this.getCurrentPlayer();
2401
+ if (keepLocalMovement) {
2402
+ return;
2403
+ }
2404
+ const player = this.getCurrentPlayer() as any;
1564
2405
  const myId = this.playerIdSignal();
1565
2406
  if (!player || !myId) {
1566
2407
  return;
@@ -1581,12 +2422,16 @@ export class RpgClientEngine<T = any> {
1581
2422
 
1582
2423
  private reconcilePrediction(
1583
2424
  authoritativeState: PredictionState<Direction>,
1584
- pendingInputs: PredictionHistoryEntry<Direction>[],
2425
+ pendingInputs: PredictionHistoryEntry<RpgMovementInput, Direction>[],
1585
2426
  ): void {
1586
- const player = this.getCurrentPlayer();
2427
+ const player = this.getCurrentPlayer() as any;
1587
2428
  if (!player) {
1588
2429
  return;
1589
2430
  }
2431
+ if (!getCanMoveValue(player)) {
2432
+ this.interruptCurrentPlayerMovement(player);
2433
+ return;
2434
+ }
1590
2435
 
1591
2436
  (this.sceneMap as any).stopMovement(player);
1592
2437
  this.applyAuthoritativeState(authoritativeState);
@@ -1599,7 +2444,7 @@ export class RpgClientEngine<T = any> {
1599
2444
  const replayInputs = pendingInputs.slice(-600);
1600
2445
  for (const entry of replayInputs) {
1601
2446
  if (!entry?.direction) continue;
1602
- (this.sceneMap as any).moveBody(player, entry.direction);
2447
+ this.applyPredictedMovementInput(player, entry.direction);
1603
2448
  this.sceneMap.stepPredictionTick();
1604
2449
  this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());
1605
2450
  }
@@ -1699,6 +2544,22 @@ export class RpgClientEngine<T = any> {
1699
2544
  this.resizeHandler = undefined;
1700
2545
  }
1701
2546
 
2547
+ if (this.pointerMoveHandler && this.pointerCanvas) {
2548
+ this.pointerCanvas.removeEventListener('pointermove', this.pointerMoveHandler);
2549
+ this.pointerCanvas.removeEventListener('pointerdown', this.pointerMoveHandler);
2550
+ if (this.pointerUpHandler) {
2551
+ this.pointerCanvas.removeEventListener('pointerup', this.pointerUpHandler);
2552
+ }
2553
+ if (this.pointerCancelHandler) {
2554
+ this.pointerCanvas.removeEventListener('pointercancel', this.pointerCancelHandler);
2555
+ this.pointerCanvas.removeEventListener('pointerleave', this.pointerCancelHandler);
2556
+ }
2557
+ this.pointerMoveHandler = undefined;
2558
+ this.pointerUpHandler = undefined;
2559
+ this.pointerCancelHandler = undefined;
2560
+ this.pointerCanvas = undefined;
2561
+ }
2562
+
1702
2563
  // Destroy PIXI app and renderer if they exist
1703
2564
  // Destroy the app first, which will destroy the renderer
1704
2565
  // Store renderer reference before destroying app (since app.destroy() will destroy the renderer)
@@ -1769,9 +2630,10 @@ export class RpgClientEngine<T = any> {
1769
2630
 
1770
2631
  // Reset signals
1771
2632
  this.playerIdSignal.set(null);
1772
- this.cameraFollowTargetId.set(null);
2633
+ this.resetCameraFollow(false);
1773
2634
  this.spriteComponentsBehind.set([]);
1774
2635
  this.spriteComponentsInFront.set([]);
2636
+ this.eventComponentResolvers.clear();
1775
2637
 
1776
2638
  // Clear maps and arrays
1777
2639
  this.spritesheets.clear();