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

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