@rpgjs/client 5.0.0-alpha.3 → 5.0.0-alpha.30

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