@rpgjs/client 5.0.0-alpha.23 → 5.0.0-alpha.25

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 (187) hide show
  1. package/dist/{index30.js → Game/AnimationManager.js} +1 -1
  2. package/dist/Game/AnimationManager.js.map +1 -0
  3. package/dist/{index24.js → Game/Event.js} +2 -2
  4. package/dist/Game/Event.js.map +1 -0
  5. package/dist/{index29.js → Game/Map.js} +6 -6
  6. package/dist/Game/Map.js.map +1 -0
  7. package/dist/{index22.js → Game/Object.js} +4 -4
  8. package/dist/Game/Object.js.map +1 -0
  9. package/dist/{index23.js → Game/Player.js} +2 -2
  10. package/dist/Game/Player.js.map +1 -0
  11. package/dist/{index9.js → Gui/Gui.js} +10 -11
  12. package/dist/{index9.js.map → Gui/Gui.js.map} +1 -1
  13. package/dist/{index19.js → Resource.js} +1 -1
  14. package/dist/Resource.js.map +1 -0
  15. package/dist/RpgClientEngine.d.ts +66 -115
  16. package/dist/{index2.js → RpgClientEngine.js} +234 -176
  17. package/dist/RpgClientEngine.js.map +1 -0
  18. package/dist/{index18.js → Sound.js} +1 -1
  19. package/dist/Sound.js.map +1 -0
  20. package/dist/{index35.js → components/animations/animation.ce.js} +3 -3
  21. package/dist/components/animations/animation.ce.js.map +1 -0
  22. package/dist/{index34.js → components/animations/hit.ce.js} +1 -1
  23. package/dist/components/animations/hit.ce.js.map +1 -0
  24. package/dist/{index12.js → components/animations/index.js} +3 -3
  25. package/dist/components/animations/index.js.map +1 -0
  26. package/dist/{index17.js → components/character.ce.js} +4 -4
  27. package/dist/components/character.ce.js.map +1 -0
  28. package/dist/{index51.js → components/dynamics/parse-value.js} +1 -1
  29. package/dist/components/dynamics/parse-value.js.map +1 -0
  30. package/dist/{index40.js → components/dynamics/text.ce.js} +2 -2
  31. package/dist/components/dynamics/text.ce.js.map +1 -0
  32. package/dist/{index11.js → components/gui/box.ce.js} +8 -8
  33. package/dist/components/gui/box.ce.js.map +1 -0
  34. package/dist/{index10.js → components/gui/dialogbox/index.ce.js} +4 -4
  35. package/dist/components/gui/dialogbox/index.ce.js.map +1 -0
  36. package/dist/{index52.js → components/gui/dialogbox/itemMenu.ce.js} +1 -1
  37. package/dist/components/gui/dialogbox/itemMenu.ce.js.map +1 -0
  38. package/dist/{index47.js → components/gui/dialogbox/selection.ce.js} +4 -4
  39. package/dist/components/gui/dialogbox/selection.ce.js.map +1 -0
  40. package/dist/components/gui/mobile/index.d.ts +1 -1
  41. package/dist/{index25.js → components/gui/mobile/index.js} +5 -5
  42. package/dist/components/gui/mobile/index.js.map +1 -0
  43. package/dist/components/gui/mobile/mobile.ce.js +17 -0
  44. package/dist/components/gui/mobile/mobile.ce.js.map +1 -0
  45. package/dist/{index13.js → components/prebuilt/hp-bar.ce.js} +1 -1
  46. package/dist/components/prebuilt/hp-bar.ce.js.map +1 -0
  47. package/dist/{index14.js → components/prebuilt/light-halo.ce.js} +2 -3
  48. package/dist/components/prebuilt/light-halo.ce.js.map +1 -0
  49. package/dist/{index26.js → components/scenes/canvas.ce.js} +6 -7
  50. package/dist/components/scenes/canvas.ce.js.map +1 -0
  51. package/dist/{index46.js → components/scenes/draw-map.ce.js} +3 -3
  52. package/dist/components/scenes/draw-map.ce.js.map +1 -0
  53. package/dist/{index16.js → components/scenes/event-layer.ce.js} +4 -4
  54. package/dist/components/scenes/event-layer.ce.js.map +1 -0
  55. package/dist/{index6.js → core/inject.js} +2 -2
  56. package/dist/core/inject.js.map +1 -0
  57. package/dist/{index5.js → core/setup.js} +4 -4
  58. package/dist/core/setup.js.map +1 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +25 -24
  61. package/dist/index.js.map +1 -1
  62. package/dist/{index8.js → module.js} +10 -5
  63. package/dist/module.js.map +1 -0
  64. package/dist/{index20.js → node_modules/.pnpm/@signe_di@2.6.0/node_modules/@signe/di/dist/index.js} +1 -1
  65. package/dist/node_modules/.pnpm/@signe_di@2.6.0/node_modules/@signe/di/dist/index.js.map +1 -0
  66. package/dist/{index45.js → node_modules/.pnpm/@signe_reactive@2.6.0/node_modules/@signe/reactive/dist/index.js} +216 -11
  67. package/dist/node_modules/.pnpm/@signe_reactive@2.6.0/node_modules/@signe/reactive/dist/index.js.map +1 -0
  68. package/dist/{index32.js → node_modules/.pnpm/@signe_room@2.6.0/node_modules/@signe/room/dist/index.js} +29 -30
  69. package/dist/node_modules/.pnpm/@signe_room@2.6.0/node_modules/@signe/room/dist/index.js.map +1 -0
  70. package/dist/{index42.js → node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js} +1 -1
  71. package/dist/node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +1 -0
  72. package/dist/{index33.js → node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/client/index.js} +5 -5
  73. package/dist/node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/client/index.js.map +1 -0
  74. package/dist/{index28.js → node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/index.js} +3 -3
  75. package/dist/node_modules/.pnpm/@signe_sync@2.6.0/node_modules/@signe/sync/dist/index.js.map +1 -0
  76. package/dist/{index48.js → node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js} +1 -1
  77. package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +1 -0
  78. package/dist/{index43.js → node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js} +2 -15
  79. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +1 -0
  80. package/dist/{index44.js → node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js} +2 -5
  81. package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +1 -0
  82. package/dist/{index50.js → node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js} +813 -100
  83. package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +1 -0
  84. package/dist/{index36.js → presets/animation.js} +1 -1
  85. package/dist/presets/animation.js.map +1 -0
  86. package/dist/{index39.js → presets/faceset.js} +1 -1
  87. package/dist/presets/faceset.js.map +1 -0
  88. package/dist/presets/index.js +14 -0
  89. package/dist/presets/index.js.map +1 -0
  90. package/dist/{index37.js → presets/lpc.js} +1 -1
  91. package/dist/presets/lpc.js.map +1 -0
  92. package/dist/{index38.js → presets/rmspritesheet.js} +1 -1
  93. package/dist/presets/rmspritesheet.js.map +1 -0
  94. package/dist/{index27.js → services/AbstractSocket.js} +1 -1
  95. package/dist/services/AbstractSocket.js.map +1 -0
  96. package/dist/{index21.js → services/keyboardControls.js} +1 -1
  97. package/dist/services/keyboardControls.js.map +1 -0
  98. package/dist/{index7.js → services/loadMap.js} +2 -2
  99. package/dist/services/loadMap.js.map +1 -0
  100. package/dist/{index4.js → services/mmorpg.js} +6 -6
  101. package/dist/services/mmorpg.js.map +1 -0
  102. package/dist/services/standalone.d.ts +3 -0
  103. package/dist/{index3.js → services/standalone.js} +18 -12
  104. package/dist/services/standalone.js.map +1 -0
  105. package/package.json +9 -9
  106. package/src/Gui/Gui.ts +0 -1
  107. package/src/RpgClientEngine.ts +298 -182
  108. package/src/components/character.ce +2 -3
  109. package/src/components/gui/mobile/index.ts +1 -1
  110. package/src/components/gui/mobile/mobile.ce +73 -88
  111. package/src/components/prebuilt/light-halo.ce +2 -71
  112. package/src/components/scenes/canvas.ce +0 -10
  113. package/src/components/scenes/event-layer.ce +1 -0
  114. package/src/index.ts +2 -1
  115. package/src/module.ts +6 -1
  116. package/src/services/standalone.ts +16 -7
  117. package/vite.config.ts +4 -2
  118. package/dist/Game/TransitionManager.d.ts +0 -56
  119. package/dist/index10.js.map +0 -1
  120. package/dist/index11.js.map +0 -1
  121. package/dist/index12.js.map +0 -1
  122. package/dist/index13.js.map +0 -1
  123. package/dist/index14.js.map +0 -1
  124. package/dist/index15.js +0 -14
  125. package/dist/index15.js.map +0 -1
  126. package/dist/index16.js.map +0 -1
  127. package/dist/index17.js.map +0 -1
  128. package/dist/index18.js.map +0 -1
  129. package/dist/index19.js.map +0 -1
  130. package/dist/index2.js.map +0 -1
  131. package/dist/index20.js.map +0 -1
  132. package/dist/index21.js.map +0 -1
  133. package/dist/index22.js.map +0 -1
  134. package/dist/index23.js.map +0 -1
  135. package/dist/index24.js.map +0 -1
  136. package/dist/index25.js.map +0 -1
  137. package/dist/index26.js.map +0 -1
  138. package/dist/index27.js.map +0 -1
  139. package/dist/index28.js.map +0 -1
  140. package/dist/index29.js.map +0 -1
  141. package/dist/index3.js.map +0 -1
  142. package/dist/index30.js.map +0 -1
  143. package/dist/index31.js +0 -53
  144. package/dist/index31.js.map +0 -1
  145. package/dist/index32.js.map +0 -1
  146. package/dist/index33.js.map +0 -1
  147. package/dist/index34.js.map +0 -1
  148. package/dist/index35.js.map +0 -1
  149. package/dist/index36.js.map +0 -1
  150. package/dist/index37.js.map +0 -1
  151. package/dist/index38.js.map +0 -1
  152. package/dist/index39.js.map +0 -1
  153. package/dist/index4.js.map +0 -1
  154. package/dist/index40.js.map +0 -1
  155. package/dist/index41.js +0 -43
  156. package/dist/index41.js.map +0 -1
  157. package/dist/index42.js.map +0 -1
  158. package/dist/index43.js.map +0 -1
  159. package/dist/index44.js.map +0 -1
  160. package/dist/index45.js.map +0 -1
  161. package/dist/index46.js.map +0 -1
  162. package/dist/index47.js.map +0 -1
  163. package/dist/index48.js.map +0 -1
  164. package/dist/index49.js +0 -7
  165. package/dist/index49.js.map +0 -1
  166. package/dist/index5.js.map +0 -1
  167. package/dist/index50.js.map +0 -1
  168. package/dist/index51.js.map +0 -1
  169. package/dist/index52.js.map +0 -1
  170. package/dist/index53.js +0 -6
  171. package/dist/index53.js.map +0 -1
  172. package/dist/index54.js +0 -12
  173. package/dist/index54.js.map +0 -1
  174. package/dist/index55.js +0 -113
  175. package/dist/index55.js.map +0 -1
  176. package/dist/index56.js +0 -136
  177. package/dist/index56.js.map +0 -1
  178. package/dist/index57.js +0 -137
  179. package/dist/index57.js.map +0 -1
  180. package/dist/index58.js +0 -112
  181. package/dist/index58.js.map +0 -1
  182. package/dist/index59.js +0 -9
  183. package/dist/index59.js.map +0 -1
  184. package/dist/index6.js.map +0 -1
  185. package/dist/index7.js.map +0 -1
  186. package/dist/index8.js.map +0 -1
  187. package/src/Game/TransitionManager.ts +0 -75
@@ -12,8 +12,7 @@ import { load } from "@signe/sync";
12
12
  import { RpgClientMap } from "./Game/Map"
13
13
  import { RpgGui } from "./Gui/Gui";
14
14
  import { AnimationManager } from "./Game/AnimationManager";
15
- import { TransitionManager } from "./Game/TransitionManager";
16
- import { lastValueFrom, Observable } from "rxjs";
15
+ import { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, switchMap, take } from "rxjs";
17
16
  import { GlobalConfigToken } from "./module";
18
17
  import * as PIXI from "pixi.js";
19
18
  import { PrebuiltComponentAnimations } from "./components/animations";
@@ -37,7 +36,6 @@ export class RpgClientEngine<T = any> {
37
36
  spritesheets: Map<string, any> = new Map();
38
37
  sounds: Map<string, any> = new Map();
39
38
  componentAnimations: any[] = [];
40
- transitions: any[] = [];
41
39
  private spritesheetResolver?: (id: string) => any | Promise<any>;
42
40
  private soundResolver?: (id: string) => any | Promise<any>;
43
41
  particleSettings: {
@@ -47,6 +45,8 @@ export class RpgClientEngine<T = any> {
47
45
  }
48
46
  renderer: PIXI.Renderer;
49
47
  tick: Observable<number>;
48
+ private canvasApp?: any;
49
+ private canvasElement?: any;
50
50
  playerIdSignal = signal<string | null>(null);
51
51
  spriteComponentsBehind = signal<any[]>([]);
52
52
  spriteComponentsInFront = signal<any[]>([]);
@@ -55,6 +55,8 @@ export class RpgClientEngine<T = any> {
55
55
  /** Trigger for map shake animation */
56
56
  mapShakeTrigger = trigger();
57
57
 
58
+ controlsReady = signal(undefined);
59
+
58
60
  private predictionEnabled = false;
59
61
  private prediction?: PredictionController<Direction>;
60
62
  private readonly SERVER_CORRECTION_THRESHOLD = 30;
@@ -65,6 +67,16 @@ export class RpgClientEngine<T = any> {
65
67
  private pingInterval: any = null;
66
68
  private readonly PING_INTERVAL_MS = 5000; // Send ping every 5 seconds
67
69
  private lastInputTime = 0;
70
+ // Track map loading state for onAfterLoading hook using RxJS
71
+ private mapLoadCompleted$ = new BehaviorSubject<boolean>(false);
72
+ private playerIdReceived$ = new BehaviorSubject<boolean>(false);
73
+ private playersReceived$ = new BehaviorSubject<boolean>(false);
74
+ private eventsReceived$ = new BehaviorSubject<boolean>(false);
75
+ private onAfterLoadingSubscription?: any;
76
+
77
+ // Store subscriptions and event listeners for cleanup
78
+ private tickSubscriptions: any[] = [];
79
+ private resizeHandler?: () => void;
68
80
 
69
81
  constructor(public context) {
70
82
  this.webSocket = inject(WebSocketToken);
@@ -134,6 +146,7 @@ export class RpgClientEngine<T = any> {
134
146
  ...currentValues,
135
147
  values: new Map([['__default__', controlInstance]])
136
148
  }
149
+ this.controlsReady.set(undefined);
137
150
  }
138
151
 
139
152
  async start() {
@@ -141,16 +154,19 @@ export class RpgClientEngine<T = any> {
141
154
  this.selector = document.body.querySelector("#rpg") as HTMLElement;
142
155
 
143
156
  const { app, canvasElement } = await bootstrapCanvas(this.selector, Canvas);
157
+ this.canvasApp = app;
158
+ this.canvasElement = canvasElement;
144
159
  this.renderer = app.renderer as PIXI.Renderer;
145
160
  this.tick = canvasElement?.propObservables?.context['tick'].observable
146
161
 
147
- this.tick.subscribe(() => {
162
+ const inputCheckSubscription = this.tick.subscribe(() => {
148
163
  if (Date.now() - this.lastInputTime > 100) {
149
164
  const player = this.getCurrentPlayer();
150
165
  if (!player) return;
151
166
  (this.sceneMap as any).stopMovement(player);
152
167
  }
153
- })
168
+ });
169
+ this.tickSubscriptions.push(inputCheckSubscription);
154
170
 
155
171
 
156
172
  this.hooks.callHooks("client-spritesheets-load", this).subscribe();
@@ -163,17 +179,17 @@ export class RpgClientEngine<T = any> {
163
179
  this.hooks.callHooks("client-gui-load", this).subscribe();
164
180
  this.hooks.callHooks("client-particles-load", this).subscribe();
165
181
  this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
166
- this.hooks.callHooks("client-transitions-load", this).subscribe();
167
182
  this.hooks.callHooks("client-sprite-load", this).subscribe();
168
183
 
169
184
  await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
170
185
 
171
186
  // wondow is resize
172
- window.addEventListener('resize', () => {
187
+ this.resizeHandler = () => {
173
188
  this.hooks.callHooks("client-engine-onWindowResize", this).subscribe();
174
- })
189
+ };
190
+ window.addEventListener('resize', this.resizeHandler);
175
191
 
176
- this.tick.subscribe((tick) => {
192
+ const tickSubscription = this.tick.subscribe((tick) => {
177
193
  this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
178
194
 
179
195
  // Clean up old prediction states and input history every 60 ticks (approximately every second at 60fps)
@@ -182,7 +198,8 @@ export class RpgClientEngine<T = any> {
182
198
  this.prediction?.cleanup(now);
183
199
  this.prediction?.tryApplyPendingSnapshot();
184
200
  }
185
- })
201
+ });
202
+ this.tickSubscriptions.push(tickSubscription);
186
203
 
187
204
  await this.webSocket.connection(() => {
188
205
  this.initListeners()
@@ -192,12 +209,27 @@ export class RpgClientEngine<T = any> {
192
209
 
193
210
  private initListeners() {
194
211
  this.webSocket.on("sync", (data) => {
195
- if (data.pId) this.playerIdSignal.set(data.pId)
212
+ if (data.pId) {
213
+ this.playerIdSignal.set(data.pId);
214
+ // Signal that player ID was received
215
+ this.playerIdReceived$.next(true);
216
+ }
196
217
 
197
218
  // Apply client-side prediction filtering and server reconciliation
198
219
  this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
199
220
 
200
221
  load(this.sceneMap, data, true);
222
+
223
+ // Check if players and events are present in sync data
224
+ const players = data.players || this.sceneMap.players();
225
+ if (players && Object.keys(players).length > 0) {
226
+ this.playersReceived$.next(true);
227
+ }
228
+
229
+ const events = data.events || this.sceneMap.events();
230
+ if (events !== undefined) {
231
+ this.eventsReceived$.next(true);
232
+ }
201
233
  });
202
234
 
203
235
  // Handle pong responses for RTT measurement
@@ -269,7 +301,7 @@ export class RpgClientEngine<T = any> {
269
301
 
270
302
  this.webSocket.on("shakeMap", (data) => {
271
303
  const { intensity, duration, frequency, direction } = data || {};
272
- this.mapShakeTrigger.start({
304
+ (this.mapShakeTrigger as any).start({
273
305
  intensity,
274
306
  duration,
275
307
  frequency,
@@ -366,11 +398,25 @@ export class RpgClientEngine<T = any> {
366
398
  }
367
399
 
368
400
  private async loadScene(mapId: string) {
369
- this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap).subscribe();
401
+ await lastValueFrom(this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap));
370
402
 
371
403
  // Clear client prediction states when changing maps
372
404
  this.clearClientPredictionStates();
373
405
 
406
+ // Reset all conditions for new map loading
407
+ this.mapLoadCompleted$.next(false);
408
+ this.playerIdReceived$.next(false);
409
+ this.playersReceived$.next(false);
410
+ this.eventsReceived$.next(false);
411
+
412
+ // Unsubscribe previous subscription if exists
413
+ if (this.onAfterLoadingSubscription) {
414
+ this.onAfterLoadingSubscription.unsubscribe();
415
+ }
416
+
417
+ // Setup RxJS observable to wait for all conditions
418
+ this.setupOnAfterLoadingObserver();
419
+
374
420
  this.webSocket.updateProperties({ room: mapId })
375
421
  await this.webSocket.reconnect(() => {
376
422
  this.initListeners()
@@ -378,7 +424,25 @@ export class RpgClientEngine<T = any> {
378
424
  })
379
425
  const res = await this.loadMapService.load(mapId)
380
426
  this.sceneMap.data.set(res)
381
- this.hooks.callHooks("client-sceneMap-onAfterLoading", this.sceneMap).subscribe();
427
+
428
+ // Check if playerId is already present
429
+ if (this.playerIdSignal()) {
430
+ this.playerIdReceived$.next(true);
431
+ }
432
+
433
+ // Check if players and events are already present in sceneMap
434
+ const players = this.sceneMap.players();
435
+ if (players && Object.keys(players).length > 0) {
436
+ this.playersReceived$.next(true);
437
+ }
438
+
439
+ const events = this.sceneMap.events();
440
+ if (events !== undefined) {
441
+ this.eventsReceived$.next(true);
442
+ }
443
+
444
+ // Signal that map loading is completed (this should be last to ensure other checks are done)
445
+ this.mapLoadCompleted$.next(true);
382
446
  this.sceneMap.loadPhysic()
383
447
  }
384
448
 
@@ -937,158 +1001,13 @@ export class RpgClientEngine<T = any> {
937
1001
  return componentAnimation.instance
938
1002
  }
939
1003
 
940
- /**
941
- * Add a transition to the engine
942
- *
943
- * Transitions are screen effects that can be displayed during scene changes,
944
- * map loading, or any other moment where a visual transition is needed.
945
- * They are displayed on top of the entire canvas and can have custom props
946
- * that can be functions (similar to ComponentAnimation).
947
- *
948
- * @param transition - The transition configuration
949
- * @param transition.id - Unique identifier for the transition
950
- * @param transition.component - The component function to render
951
- * @param transition.props - Optional props to pass to the component (can be a function)
952
- * @returns The added transition configuration
953
- *
954
- * @example
955
- * ```ts
956
- * // Add a fade transition
957
- * engine.addTransition({
958
- * id: 'fade',
959
- * component: FadeComponent
960
- * });
961
- *
962
- * // Add a transition with props
963
- * engine.addTransition({
964
- * id: 'slide',
965
- * component: SlideComponent,
966
- * props: { direction: 'left', duration: 500 }
967
- * });
968
- *
969
- * // Add a transition with function props
970
- * engine.addTransition({
971
- * id: 'custom',
972
- * component: CustomTransition,
973
- * props: (engine) => ({ width: engine.width(), height: engine.height() })
974
- * });
975
- * ```
976
- */
977
- addTransition(transition: {
978
- component: any,
979
- id: string,
980
- props?: any | ((engine: RpgClientEngine) => any)
981
- }) {
982
- const instance = new TransitionManager()
983
- this.transitions.push({
984
- id: transition.id,
985
- component: transition.component,
986
- props: transition.props,
987
- instance: instance,
988
- current: instance.current
989
- })
990
- return transition;
991
- }
992
-
993
- /**
994
- * Remove a transition from the engine
995
- *
996
- * Removes a transition by its ID. This will not affect any currently
997
- * running transitions, only prevent new ones from being started.
998
- *
999
- * @param id - The unique identifier of the transition to remove
1000
- * @returns true if the transition was found and removed, false otherwise
1001
- *
1002
- * @example
1003
- * ```ts
1004
- * // Remove a transition
1005
- * engine.removeTransition('fade');
1006
- * ```
1007
- */
1008
- removeTransition(id: string): boolean {
1009
- const index = this.transitions.findIndex((transition) => transition.id === id);
1010
- if (index !== -1) {
1011
- this.transitions.splice(index, 1);
1012
- return true;
1013
- }
1014
- return false;
1015
- }
1016
-
1017
- /**
1018
- * Modify an existing transition
1019
- *
1020
- * Updates the component or props of an existing transition. This will
1021
- * not affect any currently running transitions, only future ones.
1022
- *
1023
- * @param id - The unique identifier of the transition to modify
1024
- * @param updates - The updates to apply (component and/or props)
1025
- * @returns true if the transition was found and modified, false otherwise
1026
- *
1027
- * @example
1028
- * ```ts
1029
- * // Update transition props
1030
- * engine.modifyTransition('fade', {
1031
- * props: { duration: 2000, color: 'white' }
1032
- * });
1033
- *
1034
- * // Update transition component
1035
- * engine.modifyTransition('fade', {
1036
- * component: NewFadeComponent
1037
- * });
1038
- * ```
1039
- */
1040
- modifyTransition(id: string, updates: {
1041
- component?: any,
1042
- props?: any | ((engine: RpgClientEngine) => any)
1043
- }): boolean {
1044
- const transition = this.transitions.find((transition) => transition.id === id);
1045
- if (!transition) {
1046
- return false;
1047
- }
1048
- if (updates.component !== undefined) {
1049
- transition.component = updates.component;
1050
- }
1051
- if (updates.props !== undefined) {
1052
- transition.props = updates.props;
1053
- }
1054
- return true;
1055
- }
1056
-
1057
- /**
1058
- * Get a transition by its ID
1059
- *
1060
- * Retrieves the TransitionManager instance for a specific transition,
1061
- * which can be used to start the transition.
1062
- *
1063
- * @param id - The unique identifier of the transition
1064
- * @returns The TransitionManager instance for the transition
1065
- * @throws Error if the transition is not found
1066
- *
1067
- * @example
1068
- * ```ts
1069
- * // Get a transition and start it
1070
- * const fadeTransition = engine.getTransition('fade');
1071
- * fadeTransition.start({ duration: 1000 });
1072
- * ```
1073
- */
1074
- getTransition(id: string): TransitionManager {
1075
- const transition = this.transitions.find((transition) => transition.id === id)
1076
- if (!transition) {
1077
- throw new Error(`Transition with id ${id} not found`)
1078
- }
1079
- return transition.instance
1080
- }
1081
-
1082
1004
  /**
1083
1005
  * Start a transition
1084
1006
  *
1085
- * Convenience method to start a transition by its ID. This combines
1086
- * getTransition and start into a single call. The transition will
1087
- * automatically receive an onFinish callback to remove itself when done.
1007
+ * Convenience method to display a transition by its ID using the GUI system.
1088
1008
  *
1089
1009
  * @param id - The unique identifier of the transition to start
1090
- * @param props - Additional props to pass to the transition component
1091
- * @returns The created transition object
1010
+ * @param props - Props to pass to the transition component
1092
1011
  *
1093
1012
  * @example
1094
1013
  * ```ts
@@ -1103,28 +1022,10 @@ export class RpgClientEngine<T = any> {
1103
1022
  * ```
1104
1023
  */
1105
1024
  startTransition(id: string, props: any = {}) {
1106
- const transition = this.transitions.find((t) => t.id === id);
1107
- if (!transition) {
1108
- throw new Error(`Transition with id ${id} not found`);
1109
- }
1110
-
1111
- // Get base props (can be a function or object)
1112
- let baseProps = {};
1113
- if (transition.props) {
1114
- if (typeof transition.props === 'function') {
1115
- baseProps = transition.props(this);
1116
- } else {
1117
- baseProps = transition.props;
1118
- }
1025
+ if (!this.guiService.exists(id)) {
1026
+ throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);
1119
1027
  }
1120
-
1121
- // Merge base props with provided props (provided props take precedence)
1122
- const finalProps = {
1123
- ...baseProps,
1124
- ...props,
1125
- };
1126
-
1127
- return transition.instance.start(finalProps);
1028
+ this.guiService.display(id, props);
1128
1029
  }
1129
1030
 
1130
1031
  async processInput({ input }: { input: Direction }) {
@@ -1230,6 +1131,46 @@ export class RpgClientEngine<T = any> {
1230
1131
  return this.sceneMap.getCurrentPlayer()
1231
1132
  }
1232
1133
 
1134
+ /**
1135
+ * Setup RxJS observer to wait for all conditions before calling onAfterLoading hook
1136
+ *
1137
+ * This method uses RxJS `combineLatest` to wait for all conditions to be met,
1138
+ * regardless of the order in which they arrive:
1139
+ * 1. The map loading is completed (loadMapService.load is finished)
1140
+ * 2. We received a player ID (pId)
1141
+ * 3. Players array has at least one element
1142
+ * 4. Events property is present in the sync data
1143
+ *
1144
+ * Once all conditions are met, it uses `switchMap` to call the onAfterLoading hook once.
1145
+ *
1146
+ * ## Design
1147
+ *
1148
+ * Uses BehaviorSubjects to track each condition state, allowing events to arrive
1149
+ * in any order. The `combineLatest` operator waits until all observables emit `true`,
1150
+ * then `take(1)` ensures the hook is called only once, and `switchMap` handles
1151
+ * the hook execution.
1152
+ *
1153
+ * @example
1154
+ * ```ts
1155
+ * // Called automatically in loadScene to setup the observer
1156
+ * this.setupOnAfterLoadingObserver();
1157
+ * ```
1158
+ */
1159
+ private setupOnAfterLoadingObserver(): void {
1160
+ this.onAfterLoadingSubscription = combineLatest([
1161
+ this.mapLoadCompleted$.pipe(filter(completed => completed === true)),
1162
+ this.playerIdReceived$.pipe(filter(received => received === true)),
1163
+ this.playersReceived$.pipe(filter(received => received === true)),
1164
+ this.eventsReceived$.pipe(filter(received => received === true))
1165
+ ]).pipe(
1166
+ take(1), // Only execute once when all conditions are met
1167
+ switchMap(() => {
1168
+ // Call the hook and return the observable
1169
+ return this.hooks.callHooks("client-sceneMap-onAfterLoading", this.sceneMap);
1170
+ })
1171
+ ).subscribe();
1172
+ }
1173
+
1233
1174
  /**
1234
1175
  * Clear client prediction states for cleanup
1235
1176
  *
@@ -1364,4 +1305,179 @@ export class RpgClientEngine<T = any> {
1364
1305
  private async replayUnackedInputsFromFrame(_startFrame: number): Promise<void> {
1365
1306
  // Prediction controller handles replay internally. Kept for backwards compatibility.
1366
1307
  }
1308
+
1309
+ /**
1310
+ * Clear all client resources and reset state
1311
+ *
1312
+ * This method should be called to clean up all client-side resources when
1313
+ * shutting down or resetting the client engine. It:
1314
+ * - Destroys the PIXI renderer
1315
+ * - Stops all sounds
1316
+ * - Cleans up subscriptions and event listeners
1317
+ * - Resets scene map
1318
+ * - Stops ping/pong interval
1319
+ * - Clears prediction states
1320
+ *
1321
+ * ## Design
1322
+ *
1323
+ * This method is used primarily in testing environments to ensure clean
1324
+ * state between tests. In production, the client engine typically persists
1325
+ * for the lifetime of the application.
1326
+ *
1327
+ * @example
1328
+ * ```ts
1329
+ * // In test cleanup
1330
+ * afterEach(() => {
1331
+ * clientEngine.clear();
1332
+ * });
1333
+ * ```
1334
+ */
1335
+ clear(): void {
1336
+ try {
1337
+ // First, unsubscribe from all tick subscriptions to stop rendering attempts
1338
+ for (const subscription of this.tickSubscriptions) {
1339
+ if (subscription && typeof subscription.unsubscribe === 'function') {
1340
+ subscription.unsubscribe();
1341
+ }
1342
+ }
1343
+ this.tickSubscriptions = [];
1344
+
1345
+ // Stop ping/pong interval
1346
+ if (this.pingInterval) {
1347
+ clearInterval(this.pingInterval);
1348
+ this.pingInterval = null;
1349
+ }
1350
+
1351
+ // Clean up onAfterLoading subscription
1352
+ if (this.onAfterLoadingSubscription && typeof this.onAfterLoadingSubscription.unsubscribe === 'function') {
1353
+ this.onAfterLoadingSubscription.unsubscribe();
1354
+ this.onAfterLoadingSubscription = undefined;
1355
+ }
1356
+
1357
+ // Clean up canvasElement (CanvasEngine) BEFORE destroying PIXI app
1358
+ // This prevents CanvasEngine from trying to render after PIXI is destroyed
1359
+ // CanvasEngine manages its own render loop which could try to access PIXI after destruction
1360
+ if (this.canvasElement) {
1361
+ try {
1362
+ // Try to stop or cleanup canvasElement if it has cleanup methods
1363
+ if (typeof (this.canvasElement as any).destroy === 'function') {
1364
+ (this.canvasElement as any).destroy();
1365
+ }
1366
+ // Clear the reference
1367
+ this.canvasElement = undefined;
1368
+ } catch (error) {
1369
+ // Ignore errors during canvasElement cleanup
1370
+ }
1371
+ }
1372
+
1373
+ // Reset scene map if it exists (this should stop any ongoing animations/renders)
1374
+ if (this.sceneMap && typeof (this.sceneMap as any).reset === 'function') {
1375
+ (this.sceneMap as any).reset();
1376
+ }
1377
+
1378
+ // Stop all sounds
1379
+ this.stopAllSounds();
1380
+
1381
+ // Remove resize event listener
1382
+ if (this.resizeHandler && typeof window !== 'undefined') {
1383
+ window.removeEventListener('resize', this.resizeHandler);
1384
+ this.resizeHandler = undefined;
1385
+ }
1386
+
1387
+ // Destroy PIXI app and renderer if they exist
1388
+ // Destroy the app first, which will destroy the renderer
1389
+ // Store renderer reference before destroying app (since app.destroy() will destroy the renderer)
1390
+ const rendererStillExists = this.renderer && typeof this.renderer.destroy === 'function';
1391
+
1392
+ if (this.canvasApp && typeof this.canvasApp.destroy === 'function') {
1393
+ try {
1394
+ // Stop the ticker first to prevent any render calls during destruction
1395
+ if (this.canvasApp.ticker) {
1396
+ if (typeof this.canvasApp.ticker.stop === 'function') {
1397
+ this.canvasApp.ticker.stop();
1398
+ }
1399
+ // Also remove all listeners from ticker to prevent callbacks
1400
+ if (typeof this.canvasApp.ticker.removeAll === 'function') {
1401
+ this.canvasApp.ticker.removeAll();
1402
+ }
1403
+ }
1404
+
1405
+ // Stop the renderer's ticker if it exists separately
1406
+ if (this.renderer && (this.renderer as any).ticker) {
1407
+ if (typeof (this.renderer as any).ticker.stop === 'function') {
1408
+ (this.renderer as any).ticker.stop();
1409
+ }
1410
+ if (typeof (this.renderer as any).ticker.removeAll === 'function') {
1411
+ (this.renderer as any).ticker.removeAll();
1412
+ }
1413
+ }
1414
+
1415
+ // Remove the canvas from DOM before destroying to prevent render attempts
1416
+ if (this.canvasApp.canvas && this.canvasApp.canvas.parentNode) {
1417
+ this.canvasApp.canvas.parentNode.removeChild(this.canvasApp.canvas);
1418
+ }
1419
+
1420
+ // Destroy with minimal options to avoid issues
1421
+ // Don't pass options that might trigger additional cleanup that could fail
1422
+ this.canvasApp.destroy(true);
1423
+ } catch (error) {
1424
+ // Ignore errors during destruction
1425
+ }
1426
+ this.canvasApp = undefined;
1427
+ // canvasApp.destroy() already destroyed the renderer, so just null it
1428
+ this.renderer = null as any;
1429
+ } else if (rendererStillExists) {
1430
+ // Fallback: destroy renderer directly only if app doesn't exist or wasn't destroyed
1431
+ try {
1432
+ // Stop the renderer's ticker if it has one
1433
+ if ((this.renderer as any).ticker) {
1434
+ if (typeof (this.renderer as any).ticker.stop === 'function') {
1435
+ (this.renderer as any).ticker.stop();
1436
+ }
1437
+ if (typeof (this.renderer as any).ticker.removeAll === 'function') {
1438
+ (this.renderer as any).ticker.removeAll();
1439
+ }
1440
+ }
1441
+
1442
+ this.renderer.destroy(true);
1443
+ } catch (error) {
1444
+ // Ignore errors during destruction
1445
+ }
1446
+ this.renderer = null as any;
1447
+ }
1448
+
1449
+ // Clean up prediction controller
1450
+ if (this.prediction) {
1451
+ // Prediction controller cleanup is handled internally when destroyed
1452
+ this.prediction = undefined;
1453
+ }
1454
+
1455
+ // Reset signals
1456
+ this.playerIdSignal.set(null);
1457
+ this.cameraFollowTargetId.set(null);
1458
+ this.spriteComponentsBehind.set([]);
1459
+ this.spriteComponentsInFront.set([]);
1460
+
1461
+ // Clear maps and arrays
1462
+ this.spritesheets.clear();
1463
+ this.sounds.clear();
1464
+ this.componentAnimations = [];
1465
+ this.particleSettings.emitters = [];
1466
+
1467
+ // Reset state
1468
+ this.stopProcessingInput = false;
1469
+ this.lastInputTime = 0;
1470
+ this.inputFrameCounter = 0;
1471
+ this.frameOffset = 0;
1472
+ this.rtt = 0;
1473
+
1474
+ // Reset behavior subjects
1475
+ this.mapLoadCompleted$.next(false);
1476
+ this.playerIdReceived$.next(false);
1477
+ this.playersReceived$.next(false);
1478
+ this.eventsReceived$.next(false);
1479
+ } catch (error) {
1480
+ console.warn('Error during client engine cleanup:', error);
1481
+ }
1482
+ }
1367
1483
  }
@@ -13,15 +13,14 @@
13
13
  tint
14
14
  hitbox
15
15
  flash={flashConfig}
16
-
17
- />
16
+ />
18
17
  }
19
18
  </Container>
20
19
  @for (compConfig of normalizedComponentsInFront) {
21
20
  <Container dependencies={@compConfig.@dependencies}>
22
21
  <compConfig.component object ...compConfig.props />
23
22
  </Container>
24
- }
23
+ }
25
24
  @for (attachedGui of attachedGuis) {
26
25
  @if (shouldDisplayAttachedGui) {
27
26
  <Container>
@@ -16,7 +16,7 @@ export const withMobile = () => (
16
16
  autoDisplay: true,
17
17
  dependencies: () => {
18
18
  const engine = inject(RpgClientEngine);
19
- return [signal(isMobile() ||undefined), engine.scene.currentPlayer]
19
+ return [signal(isMobile() ||undefined), engine.controlsReady]
20
20
  }
21
21
  }
22
22
  ]