@rpgjs/client 5.0.0-alpha.9 → 5.0.0-beta.10

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 (347) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE +19 -0
  3. package/dist/Game/AnimationManager.d.ts +8 -0
  4. package/dist/Game/AnimationManager.js +35 -0
  5. package/dist/Game/AnimationManager.js.map +1 -0
  6. package/dist/Game/AnimationManager.spec.d.ts +1 -0
  7. package/dist/Game/Event.d.ts +1 -1
  8. package/dist/Game/Event.js +12 -0
  9. package/dist/Game/Event.js.map +1 -0
  10. package/dist/Game/Map.d.ts +31 -2
  11. package/dist/Game/Map.js +138 -0
  12. package/dist/Game/Map.js.map +1 -0
  13. package/dist/Game/Object.d.ts +189 -0
  14. package/dist/Game/Object.js +255 -0
  15. package/dist/Game/Object.js.map +1 -0
  16. package/dist/Game/Player.d.ts +1 -1
  17. package/dist/Game/Player.js +12 -0
  18. package/dist/Game/Player.js.map +1 -0
  19. package/dist/Gui/Gui.d.ts +192 -7
  20. package/dist/Gui/Gui.js +475 -0
  21. package/dist/Gui/Gui.js.map +1 -0
  22. package/dist/Gui/Gui.spec.d.ts +1 -0
  23. package/dist/Gui/NotificationManager.d.ts +23 -0
  24. package/dist/Gui/NotificationManager.js +49 -0
  25. package/dist/Gui/NotificationManager.js.map +1 -0
  26. package/dist/Resource.d.ts +97 -0
  27. package/dist/Resource.js +133 -0
  28. package/dist/Resource.js.map +1 -0
  29. package/dist/RpgClient.d.ts +295 -13
  30. package/dist/RpgClientEngine.d.ts +671 -15
  31. package/dist/RpgClientEngine.js +1442 -0
  32. package/dist/RpgClientEngine.js.map +1 -0
  33. package/dist/Sound.d.ts +199 -0
  34. package/dist/Sound.js +167 -0
  35. package/dist/Sound.js.map +1 -0
  36. package/dist/_virtual/_@oxc-project_runtime@0.130.0/helpers/decorate.js +9 -0
  37. package/dist/_virtual/_@oxc-project_runtime@0.130.0/helpers/decorateMetadata.js +6 -0
  38. package/dist/components/animations/animation.ce.js +23 -0
  39. package/dist/components/animations/animation.ce.js.map +1 -0
  40. package/dist/components/animations/hit.ce.js +64 -0
  41. package/dist/components/animations/hit.ce.js.map +1 -0
  42. package/dist/components/animations/index.d.ts +4 -0
  43. package/dist/components/animations/index.js +11 -0
  44. package/dist/components/animations/index.js.map +1 -0
  45. package/dist/components/character.ce.js +572 -0
  46. package/dist/components/character.ce.js.map +1 -0
  47. package/dist/components/dynamics/bar.ce.js +96 -0
  48. package/dist/components/dynamics/bar.ce.js.map +1 -0
  49. package/dist/components/dynamics/image.ce.js +23 -0
  50. package/dist/components/dynamics/image.ce.js.map +1 -0
  51. package/dist/components/dynamics/parse-value.d.ts +4 -0
  52. package/dist/components/dynamics/parse-value.js +63 -0
  53. package/dist/components/dynamics/parse-value.js.map +1 -0
  54. package/dist/components/dynamics/parse-value.spec.d.ts +1 -0
  55. package/dist/components/dynamics/shape-utils.d.ts +16 -0
  56. package/dist/components/dynamics/shape-utils.js +73 -0
  57. package/dist/components/dynamics/shape-utils.js.map +1 -0
  58. package/dist/components/dynamics/shape-utils.spec.d.ts +1 -0
  59. package/dist/components/dynamics/shape.ce.js +83 -0
  60. package/dist/components/dynamics/shape.ce.js.map +1 -0
  61. package/dist/components/dynamics/text.ce.js +50 -0
  62. package/dist/components/dynamics/text.ce.js.map +1 -0
  63. package/dist/components/gui/box.ce.js +26 -0
  64. package/dist/components/gui/box.ce.js.map +1 -0
  65. package/dist/components/gui/dialogbox/index.ce.js +198 -0
  66. package/dist/components/gui/dialogbox/index.ce.js.map +1 -0
  67. package/dist/components/gui/gameover.ce.js +169 -0
  68. package/dist/components/gui/gameover.ce.js.map +1 -0
  69. package/dist/components/gui/hud/hud.ce.js +83 -0
  70. package/dist/components/gui/hud/hud.ce.js.map +1 -0
  71. package/dist/components/gui/index.d.ts +15 -3
  72. package/dist/components/gui/index.js +14 -0
  73. package/dist/components/gui/menu/equip-menu.ce.js +427 -0
  74. package/dist/components/gui/menu/equip-menu.ce.js.map +1 -0
  75. package/dist/components/gui/menu/exit-menu.ce.js +55 -0
  76. package/dist/components/gui/menu/exit-menu.ce.js.map +1 -0
  77. package/dist/components/gui/menu/items-menu.ce.js +326 -0
  78. package/dist/components/gui/menu/items-menu.ce.js.map +1 -0
  79. package/dist/components/gui/menu/main-menu.ce.js +399 -0
  80. package/dist/components/gui/menu/main-menu.ce.js.map +1 -0
  81. package/dist/components/gui/menu/options-menu.ce.js +49 -0
  82. package/dist/components/gui/menu/options-menu.ce.js.map +1 -0
  83. package/dist/components/gui/menu/skills-menu.ce.js +102 -0
  84. package/dist/components/gui/menu/skills-menu.ce.js.map +1 -0
  85. package/dist/components/gui/mobile/index.d.ts +8 -0
  86. package/dist/components/gui/mobile/index.js +21 -0
  87. package/dist/components/gui/mobile/index.js.map +1 -0
  88. package/dist/components/gui/mobile/mobile.ce.js +79 -0
  89. package/dist/components/gui/mobile/mobile.ce.js.map +1 -0
  90. package/dist/components/gui/notification/notification.ce.js +62 -0
  91. package/dist/components/gui/notification/notification.ce.js.map +1 -0
  92. package/dist/components/gui/save-load.ce.js +211 -0
  93. package/dist/components/gui/save-load.ce.js.map +1 -0
  94. package/dist/components/gui/shop/shop.ce.js +614 -0
  95. package/dist/components/gui/shop/shop.ce.js.map +1 -0
  96. package/dist/components/gui/title-screen.ce.js +164 -0
  97. package/dist/components/gui/title-screen.ce.js.map +1 -0
  98. package/dist/components/index.d.ts +1 -0
  99. package/dist/components/index.js +4 -0
  100. package/dist/components/player-components-utils.d.ts +67 -0
  101. package/dist/components/player-components-utils.js +162 -0
  102. package/dist/components/player-components-utils.js.map +1 -0
  103. package/dist/components/player-components-utils.spec.d.ts +1 -0
  104. package/dist/components/player-components.ce.js +188 -0
  105. package/dist/components/player-components.ce.js.map +1 -0
  106. package/dist/components/prebuilt/hp-bar.ce.js +113 -0
  107. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -0
  108. package/dist/components/prebuilt/index.d.ts +19 -0
  109. package/dist/components/prebuilt/index.js +2 -0
  110. package/dist/components/prebuilt/light-halo.ce.js +70 -0
  111. package/dist/components/prebuilt/light-halo.ce.js.map +1 -0
  112. package/dist/components/scenes/canvas.ce.js +196 -0
  113. package/dist/components/scenes/canvas.ce.js.map +1 -0
  114. package/dist/components/scenes/draw-map.ce.js +79 -0
  115. package/dist/components/scenes/draw-map.ce.js.map +1 -0
  116. package/dist/components/scenes/event-layer.ce.js +29 -0
  117. package/dist/components/scenes/event-layer.ce.js.map +1 -0
  118. package/dist/core/inject.js +18 -0
  119. package/dist/core/inject.js.map +1 -0
  120. package/dist/core/setup.js +16 -0
  121. package/dist/core/setup.js.map +1 -0
  122. package/dist/decorators/spritesheet.d.ts +1 -0
  123. package/dist/decorators/spritesheet.js +11 -0
  124. package/dist/decorators/spritesheet.js.map +1 -0
  125. package/dist/index.d.ts +16 -1
  126. package/dist/index.js +45 -14
  127. package/dist/module.d.ts +43 -4
  128. package/dist/module.js +179 -0
  129. package/dist/module.js.map +1 -0
  130. package/dist/node_modules/.pnpm/@signe_di@3.0.1/node_modules/@signe/di/dist/index.js +167 -0
  131. package/dist/node_modules/.pnpm/@signe_di@3.0.1/node_modules/@signe/di/dist/index.js.map +1 -0
  132. package/dist/node_modules/.pnpm/@signe_reactive@3.0.1/node_modules/@signe/reactive/dist/index.js +239 -0
  133. package/dist/node_modules/.pnpm/@signe_reactive@3.0.1/node_modules/@signe/reactive/dist/index.js.map +1 -0
  134. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js +13 -0
  135. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/chunk-EUXUH3YW.js.map +1 -0
  136. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/index.js +696 -0
  137. package/dist/node_modules/.pnpm/@signe_room@3.0.1/node_modules/@signe/room/dist/index.js.map +1 -0
  138. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/client/index.js +44 -0
  139. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/client/index.js.map +1 -0
  140. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/index.js +241 -0
  141. package/dist/node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/index.js.map +1 -0
  142. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js +115 -0
  143. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -0
  144. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js +401 -0
  145. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -0
  146. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/index.js +2 -0
  147. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js +3756 -0
  148. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -0
  149. package/dist/presets/animation.d.ts +31 -0
  150. package/dist/presets/animation.js +39 -0
  151. package/dist/presets/animation.js.map +1 -0
  152. package/dist/presets/faceset.d.ts +30 -0
  153. package/dist/presets/faceset.js +51 -0
  154. package/dist/presets/faceset.js.map +1 -0
  155. package/dist/presets/icon.d.ts +20 -0
  156. package/dist/presets/icon.js +15 -0
  157. package/dist/presets/icon.js.map +1 -0
  158. package/dist/presets/index.d.ts +123 -0
  159. package/dist/presets/index.js +17 -0
  160. package/dist/presets/index.js.map +1 -0
  161. package/dist/presets/lpc.d.ts +89 -0
  162. package/dist/presets/lpc.js +98 -0
  163. package/dist/presets/lpc.js.map +1 -0
  164. package/dist/presets/rmspritesheet.js +42 -0
  165. package/dist/presets/rmspritesheet.js.map +1 -0
  166. package/dist/services/AbstractSocket.d.ts +9 -5
  167. package/dist/services/AbstractSocket.js +11 -0
  168. package/dist/services/AbstractSocket.js.map +1 -0
  169. package/dist/services/keyboardControls.d.ts +15 -0
  170. package/dist/services/keyboardControls.js +23 -0
  171. package/dist/services/keyboardControls.js.map +1 -0
  172. package/dist/services/loadMap.d.ts +6 -0
  173. package/dist/services/loadMap.js +123 -0
  174. package/dist/services/loadMap.js.map +1 -0
  175. package/dist/services/mmorpg.d.ts +21 -9
  176. package/dist/services/mmorpg.js +136 -0
  177. package/dist/services/mmorpg.js.map +1 -0
  178. package/dist/services/save.d.ts +19 -0
  179. package/dist/services/save.js +77 -0
  180. package/dist/services/save.js.map +1 -0
  181. package/dist/services/save.spec.d.ts +1 -0
  182. package/dist/services/standalone.d.ts +67 -7
  183. package/dist/services/standalone.js +168 -0
  184. package/dist/services/standalone.js.map +1 -0
  185. package/dist/utils/getEntityProp.d.ts +39 -0
  186. package/dist/utils/getEntityProp.js +53 -0
  187. package/dist/utils/getEntityProp.js.map +1 -0
  188. package/dist/utils/getEntityProp.spec.d.ts +1 -0
  189. package/dist/utils/readPropValue.d.ts +2 -0
  190. package/dist/utils/readPropValue.js +13 -0
  191. package/dist/utils/readPropValue.js.map +1 -0
  192. package/package.json +14 -11
  193. package/src/Game/AnimationManager.spec.ts +30 -0
  194. package/src/Game/AnimationManager.ts +33 -0
  195. package/src/Game/Event.ts +1 -1
  196. package/src/Game/Map.ts +184 -3
  197. package/src/Game/Object.ts +409 -14
  198. package/src/Game/Player.ts +1 -1
  199. package/src/Gui/Gui.spec.ts +273 -0
  200. package/src/Gui/Gui.ts +566 -23
  201. package/src/Gui/NotificationManager.ts +69 -0
  202. package/src/Resource.ts +149 -0
  203. package/src/RpgClient.ts +309 -14
  204. package/src/RpgClientEngine.ts +1790 -63
  205. package/src/Sound.ts +253 -0
  206. package/src/components/{effects → animations}/animation.ce +3 -6
  207. package/src/components/{effects → animations}/index.ts +1 -1
  208. package/src/components/character.ce +801 -59
  209. package/src/components/dynamics/bar.ce +87 -0
  210. package/src/components/dynamics/image.ce +20 -0
  211. package/src/components/dynamics/parse-value.spec.ts +83 -0
  212. package/src/components/dynamics/parse-value.ts +154 -0
  213. package/src/components/dynamics/shape-utils.spec.ts +46 -0
  214. package/src/components/dynamics/shape-utils.ts +61 -0
  215. package/src/components/dynamics/shape.ce +89 -0
  216. package/src/components/dynamics/text.ce +68 -0
  217. package/src/components/gui/box.ce +17 -0
  218. package/src/components/gui/dialogbox/index.ce +213 -187
  219. package/src/components/gui/gameover.ce +158 -0
  220. package/src/components/gui/hud/hud.ce +61 -0
  221. package/src/components/gui/index.ts +30 -4
  222. package/src/components/gui/menu/equip-menu.ce +410 -0
  223. package/src/components/gui/menu/exit-menu.ce +41 -0
  224. package/src/components/gui/menu/items-menu.ce +317 -0
  225. package/src/components/gui/menu/main-menu.ce +294 -0
  226. package/src/components/gui/menu/options-menu.ce +35 -0
  227. package/src/components/gui/menu/skills-menu.ce +83 -0
  228. package/src/components/gui/mobile/index.ts +24 -0
  229. package/src/components/gui/mobile/mobile.ce +80 -0
  230. package/src/components/gui/notification/notification.ce +51 -0
  231. package/src/components/gui/save-load.ce +208 -0
  232. package/src/components/gui/shop/shop.ce +493 -0
  233. package/src/components/gui/title-screen.ce +163 -0
  234. package/src/components/index.ts +3 -0
  235. package/src/components/player-components-utils.spec.ts +109 -0
  236. package/src/components/player-components-utils.ts +205 -0
  237. package/src/components/player-components.ce +221 -0
  238. package/src/components/prebuilt/hp-bar.ce +255 -0
  239. package/src/components/prebuilt/index.ts +24 -0
  240. package/src/components/prebuilt/light-halo.ce +148 -0
  241. package/src/components/scenes/canvas.ce +185 -21
  242. package/src/components/scenes/draw-map.ce +55 -21
  243. package/src/components/scenes/event-layer.ce +8 -2
  244. package/src/components/scenes/transition.ce +60 -0
  245. package/src/core/setup.ts +2 -2
  246. package/src/decorators/spritesheet.ts +8 -0
  247. package/src/index.ts +17 -2
  248. package/src/module.ts +132 -10
  249. package/src/presets/animation.ts +46 -0
  250. package/src/presets/faceset.ts +60 -0
  251. package/src/presets/icon.ts +17 -0
  252. package/src/presets/index.ts +9 -1
  253. package/src/presets/lpc.ts +108 -0
  254. package/src/services/AbstractSocket.ts +10 -2
  255. package/src/services/keyboardControls.ts +20 -0
  256. package/src/services/loadMap.ts +3 -1
  257. package/src/services/mmorpg.ts +106 -12
  258. package/src/services/save.spec.ts +127 -0
  259. package/src/services/save.ts +103 -0
  260. package/src/services/standalone.ts +110 -18
  261. package/src/utils/getEntityProp.spec.ts +96 -0
  262. package/src/utils/getEntityProp.ts +88 -0
  263. package/src/utils/readPropValue.ts +16 -0
  264. package/vite.config.ts +4 -2
  265. package/dist/Game/EffectManager.d.ts +0 -5
  266. package/dist/components/effects/index.d.ts +0 -4
  267. package/dist/index.js.map +0 -1
  268. package/dist/index10.js +0 -8
  269. package/dist/index10.js.map +0 -1
  270. package/dist/index11.js +0 -10
  271. package/dist/index11.js.map +0 -1
  272. package/dist/index12.js +0 -8
  273. package/dist/index12.js.map +0 -1
  274. package/dist/index13.js +0 -17
  275. package/dist/index13.js.map +0 -1
  276. package/dist/index14.js +0 -107
  277. package/dist/index14.js.map +0 -1
  278. package/dist/index15.js +0 -50
  279. package/dist/index15.js.map +0 -1
  280. package/dist/index16.js +0 -191
  281. package/dist/index16.js.map +0 -1
  282. package/dist/index17.js +0 -9
  283. package/dist/index17.js.map +0 -1
  284. package/dist/index18.js +0 -387
  285. package/dist/index18.js.map +0 -1
  286. package/dist/index19.js +0 -31
  287. package/dist/index19.js.map +0 -1
  288. package/dist/index2.js +0 -181
  289. package/dist/index2.js.map +0 -1
  290. package/dist/index20.js +0 -24
  291. package/dist/index20.js.map +0 -1
  292. package/dist/index21.js +0 -2421
  293. package/dist/index21.js.map +0 -1
  294. package/dist/index22.js +0 -114
  295. package/dist/index22.js.map +0 -1
  296. package/dist/index23.js +0 -109
  297. package/dist/index23.js.map +0 -1
  298. package/dist/index24.js +0 -71
  299. package/dist/index24.js.map +0 -1
  300. package/dist/index25.js +0 -21
  301. package/dist/index25.js.map +0 -1
  302. package/dist/index26.js +0 -41
  303. package/dist/index26.js.map +0 -1
  304. package/dist/index27.js +0 -5
  305. package/dist/index27.js.map +0 -1
  306. package/dist/index28.js +0 -322
  307. package/dist/index28.js.map +0 -1
  308. package/dist/index29.js +0 -27
  309. package/dist/index29.js.map +0 -1
  310. package/dist/index3.js +0 -87
  311. package/dist/index3.js.map +0 -1
  312. package/dist/index30.js +0 -11
  313. package/dist/index30.js.map +0 -1
  314. package/dist/index31.js +0 -11
  315. package/dist/index31.js.map +0 -1
  316. package/dist/index32.js +0 -174
  317. package/dist/index32.js.map +0 -1
  318. package/dist/index33.js +0 -501
  319. package/dist/index33.js.map +0 -1
  320. package/dist/index34.js +0 -12
  321. package/dist/index34.js.map +0 -1
  322. package/dist/index35.js +0 -4403
  323. package/dist/index35.js.map +0 -1
  324. package/dist/index36.js +0 -316
  325. package/dist/index36.js.map +0 -1
  326. package/dist/index37.js +0 -61
  327. package/dist/index37.js.map +0 -1
  328. package/dist/index38.js +0 -20
  329. package/dist/index38.js.map +0 -1
  330. package/dist/index39.js +0 -20
  331. package/dist/index39.js.map +0 -1
  332. package/dist/index4.js +0 -67
  333. package/dist/index4.js.map +0 -1
  334. package/dist/index5.js +0 -16
  335. package/dist/index5.js.map +0 -1
  336. package/dist/index6.js +0 -17
  337. package/dist/index6.js.map +0 -1
  338. package/dist/index7.js +0 -39
  339. package/dist/index7.js.map +0 -1
  340. package/dist/index8.js +0 -108
  341. package/dist/index8.js.map +0 -1
  342. package/dist/index9.js +0 -76
  343. package/dist/index9.js.map +0 -1
  344. package/src/Game/EffectManager.ts +0 -20
  345. package/src/components/gui/dialogbox/itemMenu.ce +0 -23
  346. package/src/components/gui/dialogbox/selection.ce +0 -67
  347. /package/src/components/{effects → animations}/hit.ce +0 -0
@@ -0,0 +1,1442 @@
1
+ import { inject } from "./core/inject.js";
2
+ import { WebSocketToken } from "./services/AbstractSocket.js";
3
+ import { getCanMoveValue } from "./utils/readPropValue.js";
4
+ import { SaveClientService } from "./services/save.js";
5
+ import { RpgGui } from "./Gui/Gui.js";
6
+ import __ce_component from "./components/scenes/canvas.ce.js";
7
+ import { LoadMapToken } from "./services/loadMap.js";
8
+ import { RpgSound } from "./Sound.js";
9
+ import { RpgResource } from "./Resource.js";
10
+ import { load } from "./node_modules/.pnpm/@signe_sync@3.0.1/node_modules/@signe/sync/dist/index.js";
11
+ import { RpgClientMap } from "./Game/Map.js";
12
+ import { AnimationManager } from "./Game/AnimationManager.js";
13
+ import { GlobalConfigToken } from "./module.js";
14
+ import { PrebuiltComponentAnimations } from "./components/animations/index.js";
15
+ import __ce_component$1 from "./components/dynamics/text.ce.js";
16
+ import __ce_component$2 from "./components/dynamics/bar.ce.js";
17
+ import __ce_component$3 from "./components/dynamics/shape.ce.js";
18
+ import __ce_component$4 from "./components/dynamics/image.ce.js";
19
+ import { NotificationManager } from "./Gui/NotificationManager.js";
20
+ import { Howl, bootstrapCanvas, signal, trigger } from "canvasengine";
21
+ import { Direction, ModulesToken, PredictionController, normalizeLightingState } from "@rpgjs/common";
22
+ import { BehaviorSubject, combineLatest, filter, lastValueFrom, switchMap, take } from "rxjs";
23
+ import * as PIXI from "pixi.js";
24
+ //#region src/RpgClientEngine.ts
25
+ var RpgClientEngine = class {
26
+ constructor(context) {
27
+ this.context = context;
28
+ this.stopProcessingInput = false;
29
+ this.width = signal("100%");
30
+ this.height = signal("100%");
31
+ this.spritesheets = /* @__PURE__ */ new Map();
32
+ this.sounds = /* @__PURE__ */ new Map();
33
+ this.componentAnimations = [];
34
+ this.particleSettings = { emitters: [] };
35
+ this.playerIdSignal = signal(null);
36
+ this.spriteComponentsBehind = signal([]);
37
+ this.spriteComponentsInFront = signal([]);
38
+ this.spriteComponents = /* @__PURE__ */ new Map();
39
+ this.cameraFollowTargetId = signal(null);
40
+ this.mapShakeTrigger = trigger();
41
+ this.controlsReady = signal(void 0);
42
+ this.gamePause = signal(false);
43
+ this.predictionEnabled = false;
44
+ this.SERVER_CORRECTION_THRESHOLD = 30;
45
+ this.inputFrameCounter = 0;
46
+ this.pendingPredictionFrames = [];
47
+ this.lastClientPhysicsStepAt = 0;
48
+ this.frameOffset = 0;
49
+ this.rtt = 0;
50
+ this.pingInterval = null;
51
+ this.PING_INTERVAL_MS = 5e3;
52
+ this.lastInputTime = 0;
53
+ this.MOVE_PATH_RESEND_INTERVAL_MS = 120;
54
+ this.MAX_MOVE_TRAJECTORY_POINTS = 240;
55
+ this.lastMovePathSentAt = 0;
56
+ this.lastMovePathSentFrame = 0;
57
+ this.mapLoadCompleted$ = new BehaviorSubject(false);
58
+ this.playerIdReceived$ = new BehaviorSubject(false);
59
+ this.playersReceived$ = new BehaviorSubject(false);
60
+ this.eventsReceived$ = new BehaviorSubject(false);
61
+ this.sceneResetQueued = false;
62
+ this.tickSubscriptions = [];
63
+ this.notificationManager = new NotificationManager();
64
+ this.webSocket = inject(WebSocketToken);
65
+ this.guiService = inject(RpgGui);
66
+ this.loadMapService = inject(LoadMapToken);
67
+ this.hooks = inject(ModulesToken);
68
+ this.globalConfig = inject(GlobalConfigToken);
69
+ if (!this.globalConfig) this.globalConfig = {};
70
+ if (!this.globalConfig.box) this.globalConfig.box = {
71
+ styles: {
72
+ backgroundColor: "#1a1a2e",
73
+ backgroundOpacity: .9
74
+ },
75
+ sounds: {}
76
+ };
77
+ this.addComponentAnimation({
78
+ id: "animation",
79
+ component: PrebuiltComponentAnimations.Animation
80
+ });
81
+ this.registerSpriteComponent("rpg:text", __ce_component$1);
82
+ this.registerSpriteComponent("rpg:hpBar", __ce_component$2);
83
+ this.registerSpriteComponent("rpg:spBar", __ce_component$2);
84
+ this.registerSpriteComponent("rpg:bar", __ce_component$2);
85
+ this.registerSpriteComponent("rpg:shape", __ce_component$3);
86
+ this.registerSpriteComponent("rpg:image", __ce_component$4);
87
+ this.predictionEnabled = this.globalConfig?.prediction?.enabled !== false;
88
+ this.initializePredictionController();
89
+ }
90
+ /**
91
+ * Assigns a CanvasEngine KeyboardControls instance to the dependency injection context
92
+ *
93
+ * This method registers a KeyboardControls instance from CanvasEngine into the DI container,
94
+ * making it available for injection throughout the application. The particularity is that
95
+ * this method is automatically called when a sprite is displayed on the map, allowing the
96
+ * controls to be automatically associated with the active sprite.
97
+ *
98
+ * ## Design
99
+ *
100
+ * - The instance is stored in the DI context under the `KeyboardControls` token
101
+ * - It's automatically assigned when a sprite component mounts (in `character.ce`)
102
+ * - The controls instance comes from the CanvasEngine component's directives
103
+ * - Once registered, it can be retrieved using `inject(KeyboardControls)` from anywhere
104
+ *
105
+ * @param controlInstance - The CanvasEngine KeyboardControls instance to register
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * // The method is automatically called when a sprite is displayed:
110
+ * // client.setKeyboardControls(element.directives.controls)
111
+ *
112
+ * // Later, retrieve and use the controls instance:
113
+ * import { Input, inject, KeyboardControls } from '@rpgjs/client'
114
+ *
115
+ * const controls = inject(KeyboardControls)
116
+ * const control = controls.getControl(Input.Enter)
117
+ *
118
+ * if (control) {
119
+ * console.log(control.actionName) // 'action'
120
+ * }
121
+ * ```
122
+ */
123
+ setKeyboardControls(controlInstance) {
124
+ const currentValues = this.context.values["inject:KeyboardControls"];
125
+ this.context.values["inject:KeyboardControls"] = {
126
+ ...currentValues,
127
+ values: new Map([["__default__", controlInstance]])
128
+ };
129
+ this.controlsReady.set(void 0);
130
+ }
131
+ async start() {
132
+ this.sceneMap = new RpgClientMap();
133
+ this.sceneMap.configureClientPrediction(this.predictionEnabled);
134
+ this.sceneMap.loadPhysic();
135
+ this.selector = document.body.querySelector("#rpg");
136
+ const bootstrapOptions = this.globalConfig?.bootstrapCanvasOptions;
137
+ const { app, canvasElement } = await bootstrapCanvas(this.selector, __ce_component, bootstrapOptions);
138
+ this.canvasApp = app;
139
+ this.canvasElement = canvasElement;
140
+ this.renderer = app.renderer;
141
+ this.tick = canvasElement?.propObservables?.context["tick"].observable;
142
+ const inputCheckSubscription = this.tick.subscribe(() => {
143
+ if (Date.now() - this.lastInputTime > 100) {
144
+ const player = this.getCurrentPlayer();
145
+ if (!player) return;
146
+ this.sceneMap.stopMovement(player);
147
+ }
148
+ });
149
+ this.tickSubscriptions.push(inputCheckSubscription);
150
+ this.hooks.callHooks("client-spritesheets-load", this).subscribe();
151
+ this.hooks.callHooks("client-spritesheetResolver-load", this).subscribe();
152
+ this.hooks.callHooks("client-sounds-load", this).subscribe();
153
+ this.hooks.callHooks("client-soundResolver-load", this).subscribe();
154
+ RpgSound.init(this);
155
+ RpgResource.init(this);
156
+ this.hooks.callHooks("client-gui-load", this).subscribe();
157
+ this.hooks.callHooks("client-particles-load", this).subscribe();
158
+ this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
159
+ this.hooks.callHooks("client-sprite-load", this).subscribe();
160
+ await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
161
+ this.resizeHandler = () => {
162
+ this.hooks.callHooks("client-engine-onWindowResize", this).subscribe();
163
+ };
164
+ window.addEventListener("resize", this.resizeHandler);
165
+ const tickSubscription = this.tick.subscribe((tick) => {
166
+ this.stepClientPhysicsTick();
167
+ this.flushPendingPredictedStates();
168
+ this.flushPendingMovePath();
169
+ this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
170
+ if (tick % 60 === 0) {
171
+ const now = Date.now();
172
+ this.prediction?.cleanup(now);
173
+ this.prediction?.tryApplyPendingSnapshot();
174
+ }
175
+ });
176
+ this.tickSubscriptions.push(tickSubscription);
177
+ await this.webSocket.connection(() => {
178
+ inject(SaveClientService).initialize();
179
+ this.initListeners();
180
+ this.guiService._initialize();
181
+ this.startPingPong();
182
+ });
183
+ }
184
+ prepareSyncPayload(data) {
185
+ const payload = { ...data ?? {} };
186
+ delete payload.ack;
187
+ delete payload.timestamp;
188
+ const myId = this.playerIdSignal();
189
+ const players = payload.players;
190
+ if (this.predictionEnabled && !!this.prediction?.hasPendingInputs() && myId && players && players[myId]) {
191
+ const localPatch = { ...players[myId] };
192
+ delete localPatch.x;
193
+ delete localPatch.y;
194
+ delete localPatch.direction;
195
+ delete localPatch._frames;
196
+ payload.players = {
197
+ ...players,
198
+ [myId]: localPatch
199
+ };
200
+ }
201
+ return payload;
202
+ }
203
+ normalizeAckWithSyncState(ack, syncData) {
204
+ const myId = this.playerIdSignal();
205
+ if (!myId) return ack;
206
+ const localPatch = syncData?.players?.[myId];
207
+ if (typeof localPatch?.x !== "number" || typeof localPatch?.y !== "number") return ack;
208
+ return {
209
+ ...ack,
210
+ x: localPatch.x,
211
+ y: localPatch.y,
212
+ direction: localPatch.direction ?? ack.direction
213
+ };
214
+ }
215
+ initListeners() {
216
+ this.webSocket.on("sync", (data) => {
217
+ if (data.pId) {
218
+ this.playerIdSignal.set(data.pId);
219
+ this.playerIdReceived$.next(true);
220
+ }
221
+ if (this.sceneResetQueued) {
222
+ this.sceneMap.reset();
223
+ this.sceneMap.loadPhysic();
224
+ this.sceneResetQueued = false;
225
+ }
226
+ this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
227
+ const ack = data?.ack;
228
+ const normalizedAck = ack && typeof ack.frame === "number" ? this.normalizeAckWithSyncState(ack, data) : void 0;
229
+ const payload = this.prepareSyncPayload(data);
230
+ load(this.sceneMap, payload, true);
231
+ if (normalizedAck) this.applyServerAck(normalizedAck);
232
+ for (const playerId in payload.players ?? {}) {
233
+ const player = payload.players[playerId];
234
+ if (!player._param) continue;
235
+ for (const param in player._param) this.sceneMap.players()[playerId]._param()[param] = player._param[param];
236
+ }
237
+ const players = payload.players || this.sceneMap.players();
238
+ if (players && Object.keys(players).length > 0) this.playersReceived$.next(true);
239
+ if ((payload.events || this.sceneMap.events()) !== void 0) this.eventsReceived$.next(true);
240
+ });
241
+ this.webSocket.on("pong", (data) => {
242
+ const now = Date.now();
243
+ this.rtt = now - data.clientTime;
244
+ const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1e3 / 60));
245
+ const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
246
+ if (this.inputFrameCounter > 0) this.frameOffset = estimatedServerTickNow - data.clientFrame;
247
+ console.debug(`[Ping/Pong] RTT: ${this.rtt}ms, ServerTick: ${data.serverTick}, FrameOffset: ${this.frameOffset}`);
248
+ });
249
+ this.webSocket.on("changeMap", (data) => {
250
+ this.sceneResetQueued = true;
251
+ this.sceneMap.clearLightSpots();
252
+ this.cameraFollowTargetId.set(null);
253
+ const transferToken = typeof data?.transferToken === "string" ? data.transferToken : void 0;
254
+ this.loadScene(data.mapId, transferToken);
255
+ });
256
+ this.webSocket.on("showComponentAnimation", (data) => {
257
+ const { params, object, position, id } = data;
258
+ if (!object && position === void 0) throw new Error("Please provide an object or x and y coordinates");
259
+ const player = object ? this.sceneMap.getObjectById(object) : void 0;
260
+ this.getComponentAnimation(id).displayEffect(params, player || position);
261
+ });
262
+ this.webSocket.on("notification", (data) => {
263
+ this.notificationManager.add(data);
264
+ });
265
+ this.webSocket.on("setAnimation", (data) => {
266
+ const { animationName, nbTimes, object, graphic, restoreAnimationName, restoreGraphics } = data;
267
+ const player = object ? this.sceneMap.getObjectById(object) : void 0;
268
+ if (!player) return;
269
+ const restoreOptions = {
270
+ restoreAnimationName,
271
+ restoreGraphics
272
+ };
273
+ if (graphic !== void 0) player.setAnimation(animationName, graphic, nbTimes, restoreOptions);
274
+ else player.setAnimation(animationName, nbTimes, restoreOptions);
275
+ });
276
+ this.webSocket.on("playSound", (data) => {
277
+ const { soundId, volume, loop } = data;
278
+ this.playSound(soundId, {
279
+ volume,
280
+ loop
281
+ });
282
+ });
283
+ this.webSocket.on("stopSound", (data) => {
284
+ const { soundId } = data;
285
+ this.stopSound(soundId);
286
+ });
287
+ this.webSocket.on("stopAllSounds", () => {
288
+ this.stopAllSounds();
289
+ });
290
+ this.webSocket.on("cameraFollow", (data) => {
291
+ const { targetId, smoothMove } = data;
292
+ this.setCameraFollow(targetId, smoothMove);
293
+ });
294
+ this.webSocket.on("flash", (data) => {
295
+ const { object, type, duration, cycles, alpha, tint } = data;
296
+ const sprite = object ? this.sceneMap.getObjectById(object) : void 0;
297
+ if (sprite && typeof sprite.flash === "function") sprite.flash({
298
+ type,
299
+ duration,
300
+ cycles,
301
+ alpha,
302
+ tint
303
+ });
304
+ });
305
+ this.webSocket.on("shakeMap", (data) => {
306
+ const { intensity, duration, frequency, direction } = data || {};
307
+ this.mapShakeTrigger.start({
308
+ intensity,
309
+ duration,
310
+ frequency,
311
+ direction
312
+ });
313
+ });
314
+ this.webSocket.on("weatherState", (data) => {
315
+ const raw = data && typeof data === "object" && "value" in data ? data.value : data;
316
+ if (raw === null) {
317
+ this.sceneMap.weatherState.set(null);
318
+ return;
319
+ }
320
+ if (!raw || ![
321
+ "rain",
322
+ "snow",
323
+ "fog",
324
+ "cloud"
325
+ ].includes(raw.effect)) return;
326
+ this.sceneMap.weatherState.set({
327
+ effect: raw.effect,
328
+ preset: raw.preset,
329
+ params: raw.params,
330
+ transitionMs: raw.transitionMs,
331
+ durationMs: raw.durationMs,
332
+ startedAt: raw.startedAt,
333
+ seed: raw.seed
334
+ });
335
+ });
336
+ this.webSocket.on("lightingState", (data) => {
337
+ const raw = data && typeof data === "object" && "value" in data ? data.value : data;
338
+ this.sceneMap.lightingState.set(normalizeLightingState(raw));
339
+ });
340
+ this.webSocket.on("open", () => {
341
+ this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
342
+ this.startPingPong();
343
+ });
344
+ this.webSocket.on("close", () => {
345
+ this.hooks.callHooks("client-engine-onDisconnected", this, this.socket).subscribe();
346
+ this.stopPingPong();
347
+ });
348
+ this.webSocket.on("error", (error) => {
349
+ this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket).subscribe();
350
+ });
351
+ }
352
+ /**
353
+ * Start periodic ping/pong for client-server synchronization
354
+ *
355
+ * Sends ping requests to the server to measure round-trip time (RTT) and
356
+ * calculate the frame offset between client and server ticks.
357
+ *
358
+ * ## Design
359
+ *
360
+ * - Sends ping every 5 seconds
361
+ * - Measures RTT for latency compensation
362
+ * - Calculates frame offset to map client frames to server ticks
363
+ * - Used for accurate server reconciliation
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * // Called automatically when connection opens
368
+ * this.startPingPong();
369
+ * ```
370
+ */
371
+ startPingPong() {
372
+ this.stopPingPong();
373
+ this.sendPing();
374
+ this.pingInterval = setInterval(() => {
375
+ this.sendPing();
376
+ }, this.PING_INTERVAL_MS);
377
+ }
378
+ /**
379
+ * Stop periodic ping/pong
380
+ *
381
+ * Stops the ping interval when disconnecting or changing maps.
382
+ *
383
+ * @example
384
+ * ```ts
385
+ * // Called automatically when connection closes
386
+ * this.stopPingPong();
387
+ * ```
388
+ */
389
+ stopPingPong() {
390
+ if (this.pingInterval) {
391
+ clearInterval(this.pingInterval);
392
+ this.pingInterval = null;
393
+ }
394
+ }
395
+ /**
396
+ * Send a ping request to the server
397
+ *
398
+ * Sends current client time and frame counter to the server,
399
+ * which will respond with its server tick for synchronization.
400
+ *
401
+ * @example
402
+ * ```ts
403
+ * // Send a ping to measure RTT
404
+ * this.sendPing();
405
+ * ```
406
+ */
407
+ sendPing() {
408
+ const clientTime = Date.now();
409
+ const clientFrame = this.getPhysicsTick();
410
+ this.webSocket.emit("ping", {
411
+ clientTime,
412
+ clientFrame
413
+ });
414
+ }
415
+ async loadScene(mapId, transferToken) {
416
+ await lastValueFrom(this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap));
417
+ this.clearClientPredictionStates();
418
+ this.mapLoadCompleted$.next(false);
419
+ this.playerIdReceived$.next(false);
420
+ this.playersReceived$.next(false);
421
+ this.eventsReceived$.next(false);
422
+ if (this.onAfterLoadingSubscription) this.onAfterLoadingSubscription.unsubscribe();
423
+ this.setupOnAfterLoadingObserver();
424
+ this.webSocket.updateProperties({
425
+ room: mapId,
426
+ query: transferToken ? { transferToken } : void 0
427
+ });
428
+ await this.webSocket.reconnect(() => {
429
+ inject(SaveClientService).initialize();
430
+ this.initListeners();
431
+ this.guiService._initialize();
432
+ });
433
+ const res = await this.loadMapService.load(mapId);
434
+ this.sceneMap.data.set(res);
435
+ if (this.playerIdSignal()) this.playerIdReceived$.next(true);
436
+ const players = this.sceneMap.players();
437
+ if (players && Object.keys(players).length > 0) this.playersReceived$.next(true);
438
+ if (this.sceneMap.events() !== void 0) this.eventsReceived$.next(true);
439
+ this.mapLoadCompleted$.next(true);
440
+ this.sceneMap.configureClientPrediction(this.predictionEnabled);
441
+ this.sceneMap.loadPhysic();
442
+ }
443
+ addSpriteSheet(spritesheetClass, id) {
444
+ this.spritesheets.set(id || spritesheetClass.id, spritesheetClass);
445
+ return spritesheetClass;
446
+ }
447
+ /**
448
+ * Set a resolver function for spritesheets
449
+ *
450
+ * The resolver is called when a spritesheet is requested but not found in the cache.
451
+ * It can be synchronous (returns directly) or asynchronous (returns a Promise).
452
+ * The resolved spritesheet is automatically cached for future use.
453
+ *
454
+ * @param resolver - Function that takes a spritesheet ID and returns a spritesheet or Promise of spritesheet
455
+ *
456
+ * @example
457
+ * ```ts
458
+ * // Synchronous resolver
459
+ * engine.setSpritesheetResolver((id) => {
460
+ * if (id === 'dynamic-sprite') {
461
+ * return { id: 'dynamic-sprite', image: 'path/to/image.png', framesWidth: 32, framesHeight: 32 };
462
+ * }
463
+ * return undefined;
464
+ * });
465
+ *
466
+ * // Asynchronous resolver (loading from API)
467
+ * engine.setSpritesheetResolver(async (id) => {
468
+ * const response = await fetch(`/api/spritesheets/${id}`);
469
+ * const data = await response.json();
470
+ * return data;
471
+ * });
472
+ * ```
473
+ */
474
+ setSpritesheetResolver(resolver) {
475
+ this.spritesheetResolver = resolver;
476
+ }
477
+ /**
478
+ * Get a spritesheet by ID, using resolver if not found in cache
479
+ *
480
+ * This method first checks if the spritesheet exists in the cache.
481
+ * If not found and a resolver is set, it calls the resolver to create the spritesheet.
482
+ * The resolved spritesheet is automatically cached for future use.
483
+ *
484
+ * @param id - The spritesheet ID or legacy tile ID to retrieve
485
+ * @returns The spritesheet if found or created, or undefined if not found and no resolver
486
+ * @returns Promise<any> if the resolver is asynchronous
487
+ *
488
+ * @example
489
+ * ```ts
490
+ * // Synchronous usage
491
+ * const spritesheet = engine.getSpriteSheet('my-sprite');
492
+ *
493
+ * // Asynchronous usage (when resolver returns Promise)
494
+ * const spritesheet = await engine.getSpriteSheet('dynamic-sprite');
495
+ * ```
496
+ */
497
+ getSpriteSheet(id) {
498
+ if (this.spritesheets.has(id)) return this.spritesheets.get(id);
499
+ if (this.spritesheetResolver) {
500
+ const result = this.spritesheetResolver(id);
501
+ if (result instanceof Promise) return result.then((spritesheet) => {
502
+ if (spritesheet) this.spritesheets.set(id, spritesheet);
503
+ return spritesheet;
504
+ });
505
+ else {
506
+ if (result) this.spritesheets.set(id, result);
507
+ return result;
508
+ }
509
+ }
510
+ }
511
+ /**
512
+ * Add a sound to the engine
513
+ *
514
+ * Adds a sound to the engine's sound cache. The sound can be:
515
+ * - A simple object with `id` and `src` properties
516
+ * - A Howler instance
517
+ * - An object with a `play()` method
518
+ *
519
+ * If the sound has a `src` property, a Howler instance will be created automatically.
520
+ *
521
+ * @param sound - The sound object or Howler instance
522
+ * @param id - Optional sound ID (if not provided, uses sound.id)
523
+ * @returns The added sound
524
+ *
525
+ * @example
526
+ * ```ts
527
+ * // Simple sound object
528
+ * engine.addSound({ id: 'click', src: 'click.mp3' });
529
+ *
530
+ * // With explicit ID
531
+ * engine.addSound({ src: 'music.mp3' }, 'background-music');
532
+ * ```
533
+ */
534
+ addSound(sound, id) {
535
+ const soundId = id || sound.id;
536
+ if (!soundId) {
537
+ console.warn("Sound added without an ID. It will not be retrievable.");
538
+ return sound;
539
+ }
540
+ if (sound.src && typeof sound.src === "string") {
541
+ const howlOptions = {
542
+ src: [sound.src],
543
+ loop: sound.loop || false,
544
+ volume: sound.volume !== void 0 ? sound.volume : 1
545
+ };
546
+ const howl = new Howl.Howl(howlOptions);
547
+ this.sounds.set(soundId, howl);
548
+ return howl;
549
+ }
550
+ if (sound && typeof sound.play === "function") {
551
+ this.sounds.set(soundId, sound);
552
+ return sound;
553
+ }
554
+ this.sounds.set(soundId, sound);
555
+ return sound;
556
+ }
557
+ /**
558
+ * Set a resolver function for sounds
559
+ *
560
+ * The resolver is called when a sound is requested but not found in the cache.
561
+ * It can be synchronous (returns directly) or asynchronous (returns a Promise).
562
+ * The resolved sound is automatically cached for future use.
563
+ *
564
+ * @param resolver - Function that takes a sound ID and returns a sound or Promise of sound
565
+ *
566
+ * @example
567
+ * ```ts
568
+ * // Synchronous resolver
569
+ * engine.setSoundResolver((id) => {
570
+ * if (id === 'dynamic-sound') {
571
+ * return { id: 'dynamic-sound', src: 'path/to/sound.mp3' };
572
+ * }
573
+ * return undefined;
574
+ * });
575
+ *
576
+ * // Asynchronous resolver (loading from API)
577
+ * engine.setSoundResolver(async (id) => {
578
+ * const response = await fetch(`/api/sounds/${id}`);
579
+ * const data = await response.json();
580
+ * return data;
581
+ * });
582
+ * ```
583
+ */
584
+ setSoundResolver(resolver) {
585
+ this.soundResolver = resolver;
586
+ }
587
+ /**
588
+ * Get a sound by ID, using resolver if not found in cache
589
+ *
590
+ * This method first checks if the sound exists in the cache.
591
+ * If not found and a resolver is set, it calls the resolver to create the sound.
592
+ * The resolved sound is automatically cached for future use.
593
+ *
594
+ * @param id - The sound ID to retrieve
595
+ * @returns The sound if found or created, or undefined if not found and no resolver
596
+ * @returns Promise<any> if the resolver is asynchronous
597
+ *
598
+ * @example
599
+ * ```ts
600
+ * // Synchronous usage
601
+ * const sound = engine.getSound('my-sound');
602
+ *
603
+ * // Asynchronous usage (when resolver returns Promise)
604
+ * const sound = await engine.getSound('dynamic-sound');
605
+ * ```
606
+ */
607
+ getSound(id) {
608
+ if (this.sounds.has(id)) return this.sounds.get(id);
609
+ if (this.soundResolver) {
610
+ const result = this.soundResolver(id);
611
+ if (result instanceof Promise) return result.then((sound) => {
612
+ if (sound) this.sounds.set(id, sound);
613
+ return sound;
614
+ });
615
+ else {
616
+ if (result) this.sounds.set(id, result);
617
+ return result;
618
+ }
619
+ }
620
+ }
621
+ /**
622
+ * Play a sound by its ID
623
+ *
624
+ * This method retrieves a sound from the cache or resolver and plays it.
625
+ * If the sound is not found, it will attempt to resolve it using the soundResolver.
626
+ * Uses Howler.js for audio playback instead of native Audio elements.
627
+ *
628
+ * @param soundId - The sound ID to play
629
+ * @param options - Optional sound configuration
630
+ * @param options.volume - Volume level (0.0 to 1.0, overrides sound default)
631
+ * @param options.loop - Whether the sound should loop (overrides sound default)
632
+ *
633
+ * @example
634
+ * ```ts
635
+ * // Play a sound synchronously
636
+ * engine.playSound('item-pickup');
637
+ *
638
+ * // Play a sound with volume and loop
639
+ * engine.playSound('background-music', { volume: 0.5, loop: true });
640
+ *
641
+ * // Play a sound asynchronously (when resolver returns Promise)
642
+ * await engine.playSound('dynamic-sound', { volume: 0.8 });
643
+ * ```
644
+ */
645
+ async playSound(soundId, options) {
646
+ const sound = await this.getSound(soundId);
647
+ if (sound && sound.play) {
648
+ const howlSoundId = sound._sounds?.[0]?._id;
649
+ if (options?.volume !== void 0) if (howlSoundId !== void 0) sound.volume(Math.max(0, Math.min(1, options.volume)), howlSoundId);
650
+ else sound.volume(Math.max(0, Math.min(1, options.volume)));
651
+ if (options?.loop !== void 0) if (howlSoundId !== void 0) sound.loop(options.loop, howlSoundId);
652
+ else sound.loop(options.loop);
653
+ if (howlSoundId !== void 0) sound.play(howlSoundId);
654
+ else sound.play();
655
+ } else if (sound && sound.src) {
656
+ const howlOptions = {
657
+ src: [sound.src],
658
+ loop: options?.loop !== void 0 ? options.loop : sound.loop || false,
659
+ volume: options?.volume !== void 0 ? Math.max(0, Math.min(1, options.volume)) : sound.volume !== void 0 ? sound.volume : 1
660
+ };
661
+ const howl = new Howl.Howl(howlOptions);
662
+ this.sounds.set(soundId, howl);
663
+ howl.play();
664
+ } else console.warn(`Sound with id "${soundId}" not found or cannot be played`);
665
+ }
666
+ /**
667
+ * Stop a sound that is currently playing
668
+ *
669
+ * This method stops a sound that was previously started with `playSound()`.
670
+ *
671
+ * @param soundId - The sound ID to stop
672
+ *
673
+ * @example
674
+ * ```ts
675
+ * // Start a looping sound
676
+ * engine.playSound('background-music', { loop: true });
677
+ *
678
+ * // Later, stop it
679
+ * engine.stopSound('background-music');
680
+ * ```
681
+ */
682
+ stopSound(soundId) {
683
+ const sound = this.sounds.get(soundId);
684
+ if (sound && sound.stop) sound.stop();
685
+ else console.warn(`Sound with id "${soundId}" not found or cannot be stopped`);
686
+ }
687
+ /**
688
+ * Stop all currently playing sounds
689
+ *
690
+ * This method stops all sounds that are currently playing.
691
+ * Useful when changing maps to prevent sound overlap.
692
+ *
693
+ * @example
694
+ * ```ts
695
+ * // Stop all sounds
696
+ * engine.stopAllSounds();
697
+ * ```
698
+ */
699
+ stopAllSounds() {
700
+ this.sounds.forEach((sound) => {
701
+ if (sound && sound.stop) sound.stop();
702
+ });
703
+ }
704
+ /**
705
+ * Set the camera to follow a specific sprite
706
+ *
707
+ * This method changes which sprite the camera viewport should follow.
708
+ * The camera will smoothly animate to the target sprite if smoothMove options are provided.
709
+ *
710
+ * ## Design
711
+ *
712
+ * The camera follow target is stored in a signal that is read by sprite components.
713
+ * Each sprite checks if it should be followed by comparing its ID with the target ID.
714
+ * When smoothMove options are provided, the viewport animation is handled by CanvasEngine's
715
+ * viewport system.
716
+ *
717
+ * @param targetId - The ID of the sprite to follow. Set to null to follow the current player
718
+ * @param smoothMove - Animation options. Can be a boolean (default: true) or an object with time and ease
719
+ * @param smoothMove.time - Duration of the animation in milliseconds (optional)
720
+ * @param smoothMove.ease - Easing function name from https://easings.net (optional)
721
+ *
722
+ * @example
723
+ * ```ts
724
+ * // Follow another player with default smooth animation
725
+ * engine.setCameraFollow(otherPlayerId, true);
726
+ *
727
+ * // Follow an event with custom smooth animation
728
+ * engine.setCameraFollow(eventId, {
729
+ * time: 1000,
730
+ * ease: "easeInOutQuad"
731
+ * });
732
+ *
733
+ * // Follow without animation (instant)
734
+ * engine.setCameraFollow(targetId, false);
735
+ *
736
+ * // Return to following current player
737
+ * engine.setCameraFollow(null);
738
+ * ```
739
+ */
740
+ setCameraFollow(targetId, smoothMove) {
741
+ this.cameraFollowTargetId.set(targetId);
742
+ if (typeof smoothMove === "object" && smoothMove !== null) {}
743
+ }
744
+ addParticle(particle) {
745
+ this.particleSettings.emitters.push(particle);
746
+ return particle;
747
+ }
748
+ /**
749
+ * Add a component to render behind sprites
750
+ * Components added with this method will be displayed with a lower z-index than the sprite
751
+ *
752
+ * Supports multiple formats:
753
+ * 1. Direct component: `ShadowComponent`
754
+ * 2. Configuration object: `{ component: LightHalo, props: {...} }`
755
+ * 3. With dynamic props: `{ component: LightHalo, props: (object) => {...} }`
756
+ * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`
757
+ *
758
+ * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).
759
+ * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.
760
+ *
761
+ * @param component - The component to add behind sprites, or a configuration object
762
+ * @param component.component - The component function to render
763
+ * @param component.props - Static props object or function that receives the sprite object and returns props
764
+ * @param component.dependencies - Function that receives the sprite object and returns an array of Signals
765
+ * @returns The added component or configuration
766
+ *
767
+ * @example
768
+ * ```ts
769
+ * // Add a shadow component behind all sprites
770
+ * engine.addSpriteComponentBehind(ShadowComponent);
771
+ *
772
+ * // Add a component with static props
773
+ * engine.addSpriteComponentBehind({
774
+ * component: LightHalo,
775
+ * props: { radius: 30 }
776
+ * });
777
+ *
778
+ * // Add a component with dynamic props and dependencies
779
+ * engine.addSpriteComponentBehind({
780
+ * component: HealthBar,
781
+ * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
782
+ * dependencies: (object) => [object.hp, object.param.maxHp]
783
+ * });
784
+ * ```
785
+ */
786
+ addSpriteComponentBehind(component) {
787
+ this.spriteComponentsBehind.update((components) => [...components, component]);
788
+ return component;
789
+ }
790
+ /**
791
+ * Add a component to render in front of sprites
792
+ * Components added with this method will be displayed with a higher z-index than the sprite
793
+ *
794
+ * Supports multiple formats:
795
+ * 1. Direct component: `HealthBarComponent`
796
+ * 2. Configuration object: `{ component: StatusIndicator, props: {...} }`
797
+ * 3. With dynamic props: `{ component: HealthBar, props: (object) => {...} }`
798
+ * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`
799
+ *
800
+ * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).
801
+ * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.
802
+ *
803
+ * @param component - The component to add in front of sprites, or a configuration object
804
+ * @param component.component - The component function to render
805
+ * @param component.props - Static props object or function that receives the sprite object and returns props
806
+ * @param component.dependencies - Function that receives the sprite object and returns an array of Signals
807
+ * @returns The added component or configuration
808
+ *
809
+ * @example
810
+ * ```ts
811
+ * // Add a health bar component in front of all sprites
812
+ * engine.addSpriteComponentInFront(HealthBarComponent);
813
+ *
814
+ * // Add a component with static props
815
+ * engine.addSpriteComponentInFront({
816
+ * component: StatusIndicator,
817
+ * props: { type: 'poison' }
818
+ * });
819
+ *
820
+ * // Add a component with dynamic props and dependencies
821
+ * engine.addSpriteComponentInFront({
822
+ * component: HealthBar,
823
+ * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),
824
+ * dependencies: (object) => [object.hp, object.param.maxHp]
825
+ * });
826
+ * ```
827
+ */
828
+ addSpriteComponentInFront(component) {
829
+ this.spriteComponentsInFront.update((components) => [...components, component]);
830
+ return component;
831
+ }
832
+ /**
833
+ * Register a reusable sprite component that can be addressed by the server.
834
+ *
835
+ * Server-side component definitions only carry the component id and
836
+ * serializable props. The client registry maps that id to the CanvasEngine
837
+ * component that performs the actual rendering.
838
+ *
839
+ * @param id - Stable component id used by server component definitions
840
+ * @param component - CanvasEngine component to render for this id
841
+ * @returns The registered component
842
+ *
843
+ * @example
844
+ * ```ts
845
+ * engine.registerSpriteComponent('guildBadge', GuildBadgeComponent);
846
+ * ```
847
+ */
848
+ registerSpriteComponent(id, component) {
849
+ this.spriteComponents.set(id, component);
850
+ return component;
851
+ }
852
+ /**
853
+ * Get a reusable sprite component by id.
854
+ *
855
+ * @param id - Component id registered on the client
856
+ * @returns The CanvasEngine component, or undefined when missing
857
+ */
858
+ getSpriteComponent(id) {
859
+ return this.spriteComponents.get(id);
860
+ }
861
+ /**
862
+ * Add a component animation to the engine
863
+ *
864
+ * Component animations are temporary visual effects that can be displayed
865
+ * on sprites or objects, such as hit indicators, spell effects, or status animations.
866
+ *
867
+ * @param componentAnimation - The component animation configuration
868
+ * @param componentAnimation.id - Unique identifier for the animation
869
+ * @param componentAnimation.component - The component function to render
870
+ * @returns The added component animation configuration
871
+ *
872
+ * @example
873
+ * ```ts
874
+ * // Add a hit animation component
875
+ * engine.addComponentAnimation({
876
+ * id: 'hit',
877
+ * component: HitComponent
878
+ * });
879
+ *
880
+ * // Add an explosion effect component
881
+ * engine.addComponentAnimation({
882
+ * id: 'explosion',
883
+ * component: ExplosionComponent
884
+ * });
885
+ * ```
886
+ */
887
+ addComponentAnimation(componentAnimation) {
888
+ const instance = new AnimationManager();
889
+ this.componentAnimations.push({
890
+ id: componentAnimation.id,
891
+ component: componentAnimation.component,
892
+ instance,
893
+ current: instance.current
894
+ });
895
+ return componentAnimation;
896
+ }
897
+ /**
898
+ * Get a component animation by its ID
899
+ *
900
+ * Retrieves the EffectManager instance for a specific component animation,
901
+ * which can be used to display the animation on sprites or objects.
902
+ *
903
+ * @param id - The unique identifier of the component animation
904
+ * @returns The EffectManager instance for the animation
905
+ * @throws Error if the component animation is not found
906
+ *
907
+ * @example
908
+ * ```ts
909
+ * // Get the hit animation and display it
910
+ * const hitAnimation = engine.getComponentAnimation('hit');
911
+ * hitAnimation.displayEffect({ text: "Critical!" }, player);
912
+ * ```
913
+ */
914
+ getComponentAnimation(id) {
915
+ const componentAnimation = this.componentAnimations.find((componentAnimation) => componentAnimation.id === id);
916
+ if (!componentAnimation) throw new Error(`Component animation with id ${id} not found`);
917
+ return componentAnimation.instance;
918
+ }
919
+ /**
920
+ * Start a transition
921
+ *
922
+ * Convenience method to display a transition by its ID using the GUI system.
923
+ *
924
+ * @param id - The unique identifier of the transition to start
925
+ * @param props - Props to pass to the transition component
926
+ *
927
+ * @example
928
+ * ```ts
929
+ * // Start a fade transition
930
+ * engine.startTransition('fade', { duration: 1000, color: 'black' });
931
+ *
932
+ * // Start with onFinish callback
933
+ * engine.startTransition('fade', {
934
+ * duration: 1000,
935
+ * onFinish: () => console.log('Fade complete')
936
+ * });
937
+ *
938
+ * // Wait until the transition component calls onFinish
939
+ * await engine.startTransition('fade', { duration: 1000 });
940
+ * ```
941
+ */
942
+ startTransition(id, props = {}) {
943
+ if (!this.guiService.exists(id)) throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);
944
+ return new Promise((resolve) => {
945
+ let finished = false;
946
+ const finish = (data) => {
947
+ if (finished) return;
948
+ finished = true;
949
+ props?.onFinish?.(data);
950
+ resolve();
951
+ };
952
+ this.guiService.display(id, {
953
+ ...props,
954
+ onFinish: finish
955
+ });
956
+ });
957
+ }
958
+ async processInput({ input }) {
959
+ if (this.stopProcessingInput) return;
960
+ const currentPlayer = this.sceneMap.getCurrentPlayer();
961
+ if (!(!currentPlayer || getCanMoveValue(currentPlayer))) {
962
+ this.interruptCurrentPlayerMovement(currentPlayer);
963
+ return;
964
+ }
965
+ const timestamp = Date.now();
966
+ let frame;
967
+ let tick;
968
+ if (this.predictionEnabled && this.prediction) {
969
+ const meta = this.prediction.recordInput(input, timestamp);
970
+ frame = meta.frame;
971
+ tick = meta.tick;
972
+ } else {
973
+ frame = ++this.inputFrameCounter;
974
+ tick = this.getPhysicsTick();
975
+ }
976
+ this.inputFrameCounter = frame;
977
+ this.hooks.callHooks("client-engine-onInput", this, {
978
+ input,
979
+ playerId: this.playerId
980
+ }).subscribe();
981
+ const bodyReady = this.ensureCurrentPlayerBody();
982
+ if (currentPlayer && bodyReady) {
983
+ currentPlayer.changeDirection(input);
984
+ this.sceneMap.moveBody(currentPlayer, input);
985
+ if (this.predictionEnabled && this.prediction) {
986
+ this.pendingPredictionFrames.push(frame);
987
+ if (this.pendingPredictionFrames.length > 240) this.pendingPredictionFrames = this.pendingPredictionFrames.slice(-240);
988
+ }
989
+ }
990
+ this.emitMovePacket(input, frame, tick, timestamp, true);
991
+ this.lastInputTime = Date.now();
992
+ }
993
+ processAction({ action }) {
994
+ if (this.stopProcessingInput) return;
995
+ const currentPlayer = this.sceneMap.getCurrentPlayer();
996
+ if (!(!currentPlayer || getCanMoveValue(currentPlayer))) return;
997
+ this.hooks.callHooks("client-engine-onInput", this, {
998
+ input: "action",
999
+ playerId: this.playerId
1000
+ }).subscribe();
1001
+ this.webSocket.emit("action", { action });
1002
+ }
1003
+ get PIXI() {
1004
+ return PIXI;
1005
+ }
1006
+ get socket() {
1007
+ return this.webSocket;
1008
+ }
1009
+ get playerId() {
1010
+ return this.playerIdSignal();
1011
+ }
1012
+ get scene() {
1013
+ return this.sceneMap;
1014
+ }
1015
+ getPhysicsTick() {
1016
+ return this.sceneMap?.getTick?.() ?? 0;
1017
+ }
1018
+ ensureCurrentPlayerBody() {
1019
+ const player = this.sceneMap?.getCurrentPlayer();
1020
+ const myId = this.playerIdSignal();
1021
+ if (!player || !myId) return false;
1022
+ if (!player.id) player.id = myId;
1023
+ if (this.sceneMap.getBody(myId)) return true;
1024
+ try {
1025
+ this.sceneMap.loadPhysic();
1026
+ } catch (error) {
1027
+ console.error("[RPGJS] Unable to initialize client physics before input:", error);
1028
+ return false;
1029
+ }
1030
+ return !!this.sceneMap.getBody(myId);
1031
+ }
1032
+ stepClientPhysicsTick() {
1033
+ if (!this.predictionEnabled || !this.sceneMap) return;
1034
+ const now = Date.now();
1035
+ if (this.lastClientPhysicsStepAt === 0) this.lastClientPhysicsStepAt = now;
1036
+ const deltaMs = Math.max(1, Math.min(100, now - this.lastClientPhysicsStepAt));
1037
+ this.lastClientPhysicsStepAt = now;
1038
+ this.sceneMap.stepClientPhysics(deltaMs);
1039
+ }
1040
+ flushPendingPredictedStates() {
1041
+ if (!this.predictionEnabled || !this.prediction || this.pendingPredictionFrames.length === 0) return;
1042
+ const state = this.getLocalPlayerState();
1043
+ while (this.pendingPredictionFrames.length > 0) {
1044
+ const frame = this.pendingPredictionFrames.shift();
1045
+ if (typeof frame === "number") this.prediction.attachPredictedState(frame, state);
1046
+ }
1047
+ }
1048
+ buildPendingMoveTrajectory() {
1049
+ if (!this.predictionEnabled || !this.prediction) return [];
1050
+ const pendingInputs = this.prediction.getPendingInputs();
1051
+ const trajectory = [];
1052
+ for (const entry of pendingInputs) {
1053
+ const state = entry.state;
1054
+ if (!state) continue;
1055
+ if (typeof state.x !== "number" || typeof state.y !== "number") continue;
1056
+ trajectory.push({
1057
+ frame: entry.frame,
1058
+ tick: entry.tick,
1059
+ timestamp: entry.timestamp,
1060
+ input: entry.direction,
1061
+ x: state.x,
1062
+ y: state.y,
1063
+ direction: state.direction ?? entry.direction
1064
+ });
1065
+ }
1066
+ if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) return trajectory.slice(-this.MAX_MOVE_TRAJECTORY_POINTS);
1067
+ return trajectory;
1068
+ }
1069
+ emitMovePacket(input, frame, tick, timestamp, force = false) {
1070
+ const trajectory = this.buildPendingMoveTrajectory();
1071
+ const latestTrajectoryFrame = trajectory.length > 0 ? trajectory[trajectory.length - 1].frame : frame;
1072
+ if (!force && latestTrajectoryFrame <= this.lastMovePathSentFrame && timestamp - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS) return;
1073
+ this.webSocket.emit("move", {
1074
+ input,
1075
+ timestamp,
1076
+ frame,
1077
+ tick,
1078
+ trajectory
1079
+ });
1080
+ this.lastMovePathSentAt = timestamp;
1081
+ this.lastMovePathSentFrame = Math.max(this.lastMovePathSentFrame, latestTrajectoryFrame, frame);
1082
+ }
1083
+ flushPendingMovePath() {
1084
+ if (!this.predictionEnabled || !this.prediction) return;
1085
+ const player = this.sceneMap?.getCurrentPlayer?.();
1086
+ if (player && !getCanMoveValue(player)) {
1087
+ this.interruptCurrentPlayerMovement(player);
1088
+ return;
1089
+ }
1090
+ const pendingInputs = this.prediction.getPendingInputs();
1091
+ if (pendingInputs.length === 0) return;
1092
+ const latest = pendingInputs[pendingInputs.length - 1];
1093
+ if (!latest) return;
1094
+ const now = Date.now();
1095
+ if (now - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS) return;
1096
+ this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);
1097
+ }
1098
+ getLocalPlayerState() {
1099
+ const currentPlayer = this.sceneMap?.getCurrentPlayer();
1100
+ if (!currentPlayer) return {
1101
+ x: 0,
1102
+ y: 0,
1103
+ direction: Direction.Down
1104
+ };
1105
+ const topLeft = this.sceneMap.getBodyPosition(currentPlayer.id, "top-left");
1106
+ return {
1107
+ x: topLeft?.x ?? currentPlayer.x(),
1108
+ y: topLeft?.y ?? currentPlayer.y(),
1109
+ direction: currentPlayer.direction()
1110
+ };
1111
+ }
1112
+ applyAuthoritativeState(state) {
1113
+ const player = this.sceneMap?.getCurrentPlayer();
1114
+ if (!player) return;
1115
+ const hitbox = typeof player.hitbox === "function" ? player.hitbox() : player.hitbox;
1116
+ const width = hitbox?.w ?? 0;
1117
+ const height = hitbox?.h ?? 0;
1118
+ if (!this.sceneMap.updateHitbox(player.id, state.x, state.y, width, height)) this.sceneMap.setBodyPosition(player.id, state.x, state.y, "top-left");
1119
+ player.x.set(Math.round(state.x));
1120
+ player.y.set(Math.round(state.y));
1121
+ if (state.direction) player.changeDirection(state.direction);
1122
+ }
1123
+ initializePredictionController() {
1124
+ if (!this.predictionEnabled) {
1125
+ this.prediction = void 0;
1126
+ this.sceneMap?.configureClientPrediction?.(false);
1127
+ return;
1128
+ }
1129
+ const configuredTtl = this.globalConfig?.prediction?.historyTtlMs;
1130
+ const historyTtlMs = typeof configuredTtl === "number" ? configuredTtl : 1e4;
1131
+ const configuredMaxEntries = this.globalConfig?.prediction?.maxHistoryEntries;
1132
+ const maxHistoryEntries = typeof configuredMaxEntries === "number" ? configuredMaxEntries : Math.max(600, Math.ceil(historyTtlMs / 16) + 120);
1133
+ this.sceneMap?.configureClientPrediction?.(true);
1134
+ this.prediction = new PredictionController({
1135
+ correctionThreshold: this.globalConfig?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,
1136
+ historyTtlMs,
1137
+ maxHistoryEntries,
1138
+ getPhysicsTick: () => this.getPhysicsTick(),
1139
+ getCurrentState: () => this.getLocalPlayerState(),
1140
+ setAuthoritativeState: (state) => this.applyAuthoritativeState(state)
1141
+ });
1142
+ }
1143
+ getCurrentPlayer() {
1144
+ return this.sceneMap.getCurrentPlayer();
1145
+ }
1146
+ emitSceneMapHook(hookName, ...args) {
1147
+ this.hooks.callHooks(`client-sceneMap-${hookName}`, ...args).subscribe();
1148
+ }
1149
+ /**
1150
+ * Setup RxJS observer to wait for all conditions before calling onAfterLoading hook
1151
+ *
1152
+ * This method uses RxJS `combineLatest` to wait for all conditions to be met,
1153
+ * regardless of the order in which they arrive:
1154
+ * 1. The map loading is completed (loadMapService.load is finished)
1155
+ * 2. We received a player ID (pId)
1156
+ * 3. Players array has at least one element
1157
+ * 4. Events property is present in the sync data
1158
+ *
1159
+ * Once all conditions are met, it uses `switchMap` to call the onAfterLoading hook once.
1160
+ *
1161
+ * ## Design
1162
+ *
1163
+ * Uses BehaviorSubjects to track each condition state, allowing events to arrive
1164
+ * in any order. The `combineLatest` operator waits until all observables emit `true`,
1165
+ * then `take(1)` ensures the hook is called only once, and `switchMap` handles
1166
+ * the hook execution.
1167
+ *
1168
+ * @example
1169
+ * ```ts
1170
+ * // Called automatically in loadScene to setup the observer
1171
+ * this.setupOnAfterLoadingObserver();
1172
+ * ```
1173
+ */
1174
+ setupOnAfterLoadingObserver() {
1175
+ this.onAfterLoadingSubscription = combineLatest([
1176
+ this.mapLoadCompleted$.pipe(filter((completed) => completed === true)),
1177
+ this.playerIdReceived$.pipe(filter((received) => received === true)),
1178
+ this.playersReceived$.pipe(filter((received) => received === true)),
1179
+ this.eventsReceived$.pipe(filter((received) => received === true))
1180
+ ]).pipe(take(1), switchMap(() => {
1181
+ return this.hooks.callHooks("client-sceneMap-onAfterLoading", this.sceneMap);
1182
+ })).subscribe();
1183
+ }
1184
+ /**
1185
+ * Clear client prediction states for cleanup
1186
+ *
1187
+ * Removes old prediction states and input history to prevent memory leaks.
1188
+ * Should be called when changing maps or disconnecting.
1189
+ *
1190
+ * @example
1191
+ * ```ts
1192
+ * // Clear prediction states when changing maps
1193
+ * engine.clearClientPredictionStates();
1194
+ * ```
1195
+ */
1196
+ clearClientPredictionStates() {
1197
+ this.initializePredictionController();
1198
+ this.frameOffset = 0;
1199
+ this.inputFrameCounter = 0;
1200
+ this.pendingPredictionFrames = [];
1201
+ this.lastClientPhysicsStepAt = 0;
1202
+ this.lastMovePathSentAt = 0;
1203
+ this.lastMovePathSentFrame = 0;
1204
+ }
1205
+ /**
1206
+ * Stop local movement immediately and discard pending predicted movement.
1207
+ *
1208
+ * Use this before a blocking action such as an A-RPG attack, dialog, dash
1209
+ * startup, or any client-side state where already buffered movement inputs
1210
+ * must not be replayed after server reconciliation.
1211
+ *
1212
+ * @param player - Player object to stop. Defaults to the current player.
1213
+ * @returns `true` when a player was found and interrupted.
1214
+ *
1215
+ * @example
1216
+ * ```ts
1217
+ * engine.interruptCurrentPlayerMovement();
1218
+ * ```
1219
+ */
1220
+ interruptCurrentPlayerMovement(player = this.sceneMap?.getCurrentPlayer?.()) {
1221
+ if (!player) return false;
1222
+ this.sceneMap?.stopMovement?.(player);
1223
+ this.prediction?.clearPendingInputs();
1224
+ this.pendingPredictionFrames = [];
1225
+ this.lastInputTime = 0;
1226
+ this.lastMovePathSentAt = Date.now();
1227
+ this.lastMovePathSentFrame = this.inputFrameCounter;
1228
+ return true;
1229
+ }
1230
+ /**
1231
+ * Trigger a flash animation on a sprite
1232
+ *
1233
+ * This method allows you to trigger a flash effect on any sprite from client-side code.
1234
+ * The flash can be configured with various options including type (alpha, tint, or both),
1235
+ * duration, cycles, and color.
1236
+ *
1237
+ * ## Design
1238
+ *
1239
+ * The flash is applied directly to the sprite object using its flash trigger.
1240
+ * This is useful for client-side visual feedback, UI interactions, or local effects
1241
+ * that don't need to be synchronized with the server.
1242
+ *
1243
+ * @param spriteId - The ID of the sprite to flash. If not provided, flashes the current player
1244
+ * @param options - Flash configuration options
1245
+ * @param options.type - Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both' (default: 'alpha')
1246
+ * @param options.duration - Duration of the flash animation in milliseconds (default: 300)
1247
+ * @param options.cycles - Number of flash cycles (flash on/off) (default: 1)
1248
+ * @param options.alpha - Alpha value when flashing, from 0 to 1 (default: 0.3)
1249
+ * @param options.tint - Tint color when flashing as hex value or color name (default: 0xffffff - white)
1250
+ *
1251
+ * @example
1252
+ * ```ts
1253
+ * // Flash the current player with default settings
1254
+ * engine.flash();
1255
+ *
1256
+ * // Flash a specific sprite with red tint
1257
+ * engine.flash('sprite-id', { type: 'tint', tint: 0xff0000 });
1258
+ *
1259
+ * // Flash with both alpha and tint for dramatic effect
1260
+ * engine.flash(undefined, {
1261
+ * type: 'both',
1262
+ * alpha: 0.5,
1263
+ * tint: 0xff0000,
1264
+ * duration: 200,
1265
+ * cycles: 2
1266
+ * });
1267
+ *
1268
+ * // Quick damage flash on current player
1269
+ * engine.flash(undefined, {
1270
+ * type: 'tint',
1271
+ * tint: 'red',
1272
+ * duration: 150,
1273
+ * cycles: 1
1274
+ * });
1275
+ * ```
1276
+ */
1277
+ flash(spriteId, options) {
1278
+ const targetId = spriteId || this.playerId;
1279
+ if (!targetId) return;
1280
+ const sprite = this.sceneMap.getObjectById(targetId);
1281
+ if (sprite && typeof sprite.flash === "function") sprite.flash(options);
1282
+ }
1283
+ applyServerAck(ack) {
1284
+ if (this.predictionEnabled && this.prediction) {
1285
+ const result = this.prediction.applyServerAck({
1286
+ frame: ack.frame,
1287
+ serverTick: ack.serverTick,
1288
+ state: typeof ack.x === "number" && typeof ack.y === "number" ? {
1289
+ x: ack.x,
1290
+ y: ack.y,
1291
+ direction: ack.direction
1292
+ } : void 0
1293
+ });
1294
+ if (result.state && result.needsReconciliation) this.reconcilePrediction(result.state, result.pendingInputs);
1295
+ return;
1296
+ }
1297
+ if (typeof ack.x !== "number" || typeof ack.y !== "number") return;
1298
+ const player = this.getCurrentPlayer();
1299
+ const myId = this.playerIdSignal();
1300
+ if (!player || !myId) return;
1301
+ const hitbox = typeof player.hitbox === "function" ? player.hitbox() : player.hitbox;
1302
+ const width = hitbox?.w ?? 0;
1303
+ const height = hitbox?.h ?? 0;
1304
+ if (!this.sceneMap.updateHitbox(myId, ack.x, ack.y, width, height)) this.sceneMap.setBodyPosition(myId, ack.x, ack.y, "top-left");
1305
+ player.x.set(Math.round(ack.x));
1306
+ player.y.set(Math.round(ack.y));
1307
+ if (ack.direction) player.changeDirection(ack.direction);
1308
+ }
1309
+ reconcilePrediction(authoritativeState, pendingInputs) {
1310
+ const player = this.getCurrentPlayer();
1311
+ if (!player) return;
1312
+ if (!getCanMoveValue(player)) {
1313
+ this.interruptCurrentPlayerMovement(player);
1314
+ return;
1315
+ }
1316
+ this.sceneMap.stopMovement(player);
1317
+ this.applyAuthoritativeState(authoritativeState);
1318
+ if (!pendingInputs.length) return;
1319
+ const replayInputs = pendingInputs.slice(-600);
1320
+ for (const entry of replayInputs) {
1321
+ if (!entry?.direction) continue;
1322
+ this.sceneMap.moveBody(player, entry.direction);
1323
+ this.sceneMap.stepPredictionTick();
1324
+ this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());
1325
+ }
1326
+ }
1327
+ /**
1328
+ * Replay unacknowledged inputs from a given frame to resimulate client prediction
1329
+ * after applying server authority at a certain frame.
1330
+ *
1331
+ * @param startFrame - The last server-acknowledged frame
1332
+ *
1333
+ * @example
1334
+ * ```ts
1335
+ * // After applying a server correction at frame N
1336
+ * this.replayUnackedInputsFromFrame(N);
1337
+ * ```
1338
+ */
1339
+ async replayUnackedInputsFromFrame(_startFrame) {}
1340
+ /**
1341
+ * Clear all client resources and reset state
1342
+ *
1343
+ * This method should be called to clean up all client-side resources when
1344
+ * shutting down or resetting the client engine. It:
1345
+ * - Destroys the PIXI renderer
1346
+ * - Stops all sounds
1347
+ * - Cleans up subscriptions and event listeners
1348
+ * - Resets scene map
1349
+ * - Stops ping/pong interval
1350
+ * - Clears prediction states
1351
+ *
1352
+ * ## Design
1353
+ *
1354
+ * This method is used primarily in testing environments to ensure clean
1355
+ * state between tests. In production, the client engine typically persists
1356
+ * for the lifetime of the application.
1357
+ *
1358
+ * @example
1359
+ * ```ts
1360
+ * // In test cleanup
1361
+ * afterEach(() => {
1362
+ * clientEngine.clear();
1363
+ * });
1364
+ * ```
1365
+ */
1366
+ clear() {
1367
+ try {
1368
+ for (const subscription of this.tickSubscriptions) if (subscription && typeof subscription.unsubscribe === "function") subscription.unsubscribe();
1369
+ this.tickSubscriptions = [];
1370
+ if (this.pingInterval) {
1371
+ clearInterval(this.pingInterval);
1372
+ this.pingInterval = null;
1373
+ }
1374
+ if (this.onAfterLoadingSubscription && typeof this.onAfterLoadingSubscription.unsubscribe === "function") {
1375
+ this.onAfterLoadingSubscription.unsubscribe();
1376
+ this.onAfterLoadingSubscription = void 0;
1377
+ }
1378
+ if (this.canvasElement) try {
1379
+ if (typeof this.canvasElement.destroy === "function") this.canvasElement.destroy();
1380
+ this.canvasElement = void 0;
1381
+ } catch (error) {}
1382
+ if (this.sceneMap && typeof this.sceneMap.reset === "function") this.sceneMap.reset(true);
1383
+ this.stopAllSounds();
1384
+ if (this.resizeHandler && typeof window !== "undefined") {
1385
+ window.removeEventListener("resize", this.resizeHandler);
1386
+ this.resizeHandler = void 0;
1387
+ }
1388
+ const rendererStillExists = this.renderer && typeof this.renderer.destroy === "function";
1389
+ if (this.canvasApp && typeof this.canvasApp.destroy === "function") {
1390
+ try {
1391
+ if (this.canvasApp.ticker) {
1392
+ if (typeof this.canvasApp.ticker.stop === "function") this.canvasApp.ticker.stop();
1393
+ if (typeof this.canvasApp.ticker.removeAll === "function") this.canvasApp.ticker.removeAll();
1394
+ }
1395
+ if (this.renderer && this.renderer.ticker) {
1396
+ if (typeof this.renderer.ticker.stop === "function") this.renderer.ticker.stop();
1397
+ if (typeof this.renderer.ticker.removeAll === "function") this.renderer.ticker.removeAll();
1398
+ }
1399
+ if (this.canvasApp.canvas && this.canvasApp.canvas.parentNode) this.canvasApp.canvas.parentNode.removeChild(this.canvasApp.canvas);
1400
+ this.canvasApp.destroy(true);
1401
+ } catch (error) {}
1402
+ this.canvasApp = void 0;
1403
+ this.renderer = null;
1404
+ } else if (rendererStillExists) {
1405
+ try {
1406
+ if (this.renderer.ticker) {
1407
+ if (typeof this.renderer.ticker.stop === "function") this.renderer.ticker.stop();
1408
+ if (typeof this.renderer.ticker.removeAll === "function") this.renderer.ticker.removeAll();
1409
+ }
1410
+ this.renderer.destroy(true);
1411
+ } catch (error) {}
1412
+ this.renderer = null;
1413
+ }
1414
+ if (this.prediction) this.prediction = void 0;
1415
+ this.playerIdSignal.set(null);
1416
+ this.cameraFollowTargetId.set(null);
1417
+ this.spriteComponentsBehind.set([]);
1418
+ this.spriteComponentsInFront.set([]);
1419
+ this.spritesheets.clear();
1420
+ this.sounds.clear();
1421
+ this.componentAnimations = [];
1422
+ this.particleSettings.emitters = [];
1423
+ this.stopProcessingInput = false;
1424
+ this.lastInputTime = 0;
1425
+ this.inputFrameCounter = 0;
1426
+ this.frameOffset = 0;
1427
+ this.rtt = 0;
1428
+ this.lastMovePathSentAt = 0;
1429
+ this.lastMovePathSentFrame = 0;
1430
+ this.mapLoadCompleted$.next(false);
1431
+ this.playerIdReceived$.next(false);
1432
+ this.playersReceived$.next(false);
1433
+ this.eventsReceived$.next(false);
1434
+ } catch (error) {
1435
+ console.warn("Error during client engine cleanup:", error);
1436
+ }
1437
+ }
1438
+ };
1439
+ //#endregion
1440
+ export { RpgClientEngine };
1441
+
1442
+ //# sourceMappingURL=RpgClientEngine.js.map